├── .gitignore ├── LICENSE ├── README.md ├── config.example.py ├── hoshino ├── __init__.py ├── aiohttpx.py ├── aiorequests.py ├── log.py ├── modules │ ├── botmanage │ │ ├── broadcast.py │ │ ├── data_cleaner.py │ │ ├── feedback.py │ │ ├── help.py.bak │ │ ├── join_group.py │ │ ├── ls.py │ │ └── service_manage.py │ ├── custom │ │ ├── QAmirai │ │ │ ├── .gitignore │ │ │ ├── __init__.py │ │ │ ├── data.py │ │ │ └── readme.txt │ │ ├── anti_recall.py │ │ ├── antiqks.py │ │ ├── arc │ │ │ ├── __init__.py │ │ │ ├── arcaea_crawler.py │ │ │ └── ds.txt │ │ ├── calendar │ │ │ ├── __init__.py │ │ │ ├── calendar.py │ │ │ └── campaign.py │ │ ├── chooseone.py │ │ ├── echo.py │ │ ├── gift.py │ │ ├── gorestart.py │ │ ├── high_eq.py │ │ ├── longwang.py │ │ ├── nbnhhsh.py │ │ ├── nodolls.py │ │ ├── ocr.py │ │ ├── online_notice.py │ │ ├── poke.py │ │ ├── query.py │ │ ├── rss │ │ │ ├── __init__.py │ │ │ └── data.py │ │ ├── search_saucenao.py │ │ ├── setblock.py │ │ ├── shanghao.py │ │ ├── throwandcreep.py │ │ └── treat.py │ ├── deepchat │ │ └── deepchat.py │ ├── dice │ │ └── dice.py │ ├── groupmaster │ │ ├── anti_abuse.py │ │ ├── chat.py │ │ ├── group_approve.py │ │ ├── group_notice.py │ │ ├── random_repeater.py │ │ └── sleeping_set.py │ ├── hourcall │ │ └── hourcall.py │ ├── mikan │ │ └── mikan.py │ ├── pcrclanbattle │ │ └── clanbattle │ │ │ ├── README.md │ │ │ ├── READMEv1.md │ │ │ ├── __init__.py │ │ │ ├── argparse │ │ │ ├── __init__.py │ │ │ └── argtype.py │ │ │ ├── battlemaster.py │ │ │ ├── cmdv2.py │ │ │ ├── dao │ │ │ ├── __init__.py │ │ │ └── sqlitedao.py │ │ │ └── exception.py │ ├── priconne │ │ ├── .gitignore │ │ ├── __init__.py │ │ ├── arena │ │ │ ├── README.md │ │ │ ├── __init__.py │ │ │ └── arena.py │ │ ├── arena_reminder.py │ │ ├── chara.py │ │ ├── cherugo.py │ │ ├── comic.py │ │ ├── gacha │ │ │ ├── README.md │ │ │ ├── __init__.py │ │ │ ├── config_sample.json │ │ │ └── gacha.py │ │ ├── login_bonus.py │ │ ├── news │ │ │ ├── __init__.py │ │ │ └── spider.py │ │ ├── priconne_data_sample.py │ │ └── whois.py │ ├── setu │ │ └── setu.py │ ├── steam │ │ └── steam │ │ │ ├── __init__.py │ │ │ └── subscribes.json │ └── twitter │ │ └── twitter.py ├── res.py ├── service.py └── util.py ├── requirements.txt └── run.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Config Files 2 | config.py 3 | config.json 4 | local_coolq_config/ 5 | 6 | # Database 7 | db.json 8 | database.json 9 | *.db 10 | *.sqlite 11 | res/ 12 | img/ 13 | imgs/ 14 | image/ 15 | images/ 16 | 17 | # 非公开的组件 18 | # hoshino/modules/deepchat/ 19 | 20 | # 暂时删除的代码 21 | TRASH/ 22 | kokkoro-OLD/ 23 | 24 | # VSCode Folder 25 | .vscode/ 26 | 27 | # Byte-compiled / optimized / DLL files 28 | __pycache__/ 29 | *.py[cod] 30 | *$py.class 31 | 32 | # C extensions 33 | *.so 34 | 35 | # Distribution / packaging 36 | .Python 37 | build/ 38 | develop-eggs/ 39 | dist/ 40 | downloads/ 41 | eggs/ 42 | .eggs/ 43 | lib/ 44 | lib64/ 45 | parts/ 46 | sdist/ 47 | var/ 48 | wheels/ 49 | *.egg-info/ 50 | .installed.cfg 51 | *.egg 52 | MANIFEST 53 | 54 | # PyInstaller 55 | # Usually these files are written by a python script from a template 56 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 57 | *.manifest 58 | *.spec 59 | 60 | # Installer logs 61 | pip-log.txt 62 | pip-delete-this-directory.txt 63 | 64 | # Unit test / coverage reports 65 | htmlcov/ 66 | .tox/ 67 | .coverage 68 | .coverage.* 69 | .cache 70 | nosetests.xml 71 | coverage.xml 72 | *.cover 73 | .hypothesis/ 74 | .pytest_cache/ 75 | 76 | # Translations 77 | *.mo 78 | *.pot 79 | 80 | # Django stuff: 81 | *.log 82 | local_settings.py 83 | db.sqlite3 84 | 85 | # Flask stuff: 86 | instance/ 87 | .webassets-cache 88 | 89 | # Scrapy stuff: 90 | .scrapy 91 | 92 | # Sphinx documentation 93 | docs/_build/ 94 | 95 | # PyBuilder 96 | target/ 97 | 98 | # Jupyter Notebook 99 | .ipynb_checkpoints 100 | 101 | # pyenv 102 | .python-version 103 | 104 | # celery beat schedule file 105 | celerybeat-schedule 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HoshinoBot 2 | 3 | **Forked from [Ice-Cirno/HoshinoBot](https://github.com/Ice-Cirno/HoshinoBot) and customized by AkiraXie** 4 | 5 | **The repo has been archived in memory of my coding life about Nonebot1, and won't fix the state .** 6 | ## 特别提醒 7 | 8 | 此HoshinoBot基于[Ice-Cirno](https://github.com/Ice-Cirno)的HoshinoBot V1,并进行了一定的定制化,添加了一些插件。与现在的HoshinoBot V2有一定的差异,如需搭建原版HoshinoBot请查阅[这里](https://github.com/Ice-Cirno/HoshinoBot)。 9 | 10 | 本文着重于定制化功能的介绍。由于coolq平台停运,现在机器人平台有很多,如何搭建是一个见仁见智的问题,请自行查阅相关文档部署。 11 | 12 | 本bot需要安装额外的依赖,可以通过``pip3 install -r requirements.txt``安装。 13 | 14 | 本bot需要静态资源,可以从[这里](https://pan.akiraxie.me/A:/res.zip)下载。 15 | 16 | 17 | ## 功能介绍 18 | 19 | 本bot除了支持原版HoshinoBot的功能以外,另外增加了一些定制化功能,一部分是通过添加了额外的插件实现,另外一部分就是通过修改了原版的代码实现。 20 | 21 | | 标记 | 说明 | 22 | | -------- | ------------------------------------------------------------ | 23 | | **权限** | 表示该功能需要一定的权限才可使用 | 24 | | **自动** | 表示该功能已经设置好了定时任务,将会自动进行,如果输入命令则会强制进行。 | 25 | | () | 括号内表示必要的参数
*不含括号 | 26 | | [] | 中括号内表示可省略的参数
*不含中括号 | 27 | | \ | 表示可选择参数 | 28 | 29 | *括号内为服务名称,跟HoshinoBot其他的服务一样,可以开关 30 | 31 | ### 插件版功能介绍 32 | 33 | 添加的插件均在均在[custom](hoshino/modules/custom)文件夹。 34 | 35 | #### 你问我答(QA) 36 | 37 | | 命令 | 介绍 | 38 | | ----------------- | --------------------------------------- | 39 | | 我问xx你答yy | | 40 | | 有人问xxx你答yyy | **权限**:管理员 | 41 | | 看看我问 | 查看设置的‘我问’ | 42 | | 看看有人问 | (**权限**:管理员)查看本群设置的’有人问‘ | 43 | | (不再\不要)回答xx | *如果要删除’有人问‘,则需要管理员权限 | 44 | 45 | #### 选择(chooseone) 46 | 47 | | 命令 | 介绍 | 48 | | --------------------- | ----------------------------------- | 49 | | 选择a还是b[还是c,...] | 可以进行N选1(N>=2)
*不支持空字符 | 50 | 51 | #### 日程(calendar-bili/calendar-jp) 52 | 53 | 该日程功能仅支持日服和B服的日程查看,之所以不支持台服一是目前干炸里脊的台服数据库已经不再维护,二是台服数据库就算抓包更新也会有一些骚操作,实属阴间,就打扰了。。 54 | 55 | B服和日服对应两个服务,想要查看或者推送日程需要打开相应的服务。 56 | 57 | 另外,开启服务会额外地每六个小时推送**买药小助手**。 58 | 59 | | 命令 | 介绍 | 60 | | -------------- | ------------------------------- | 61 | | (B服\日服)日程 | 查看相应服的日程表
***自动** | 62 | | 更新数据库 | **权限**:主人 ,**自动** | 63 | 64 | #### 能不能好好说话(nbnhhsh) 65 | 66 | | 命令 | 介绍 | 67 | | ----------------- | ---------------- | 68 | | (??\??)nbnhhsh | 翻译**英文**简写 | 69 | 70 | #### 套娃(nodolls) 71 | 72 | | 命令 | 介绍 | 73 | | ------------ | ---------------- | 74 | | 禁止禁止套娃 | 禁止禁止禁止套娃 | 75 | 76 | #### SauceNao搜图(saucenao) 77 | 此功能需要向saucenao-api进行post,境外服务器支持良好,境内服务器延迟会较大。 78 | 79 | | 命令 | 介绍 | 80 | | ------------------------------------------------------------ | ----------------------- | 81 | | 识图/搜图 图片A[ 图片B 图片C]
*命令和图片之间要用空格隔开 | 返回n张图片的信息(n>=1) | 82 | 83 | #### 网抑云语录(wangyiyun) 84 | 85 | | 命令 | 介绍 | 86 | | -------------------- | ----------------- | 87 | | 上号/网抑云/生而为人 | 感受悲伤的痛楚吧~ | 88 | 89 | #### 迫害龙王(longwang) 90 | 91 | | 命令 | 介绍 | 92 | | -------- | ---------------- | 93 | | 迫害龙王 | 用力迫害群龙王! | 94 | 95 | 96 | 97 | ### 修改版功能介绍 98 | 99 | 源码的修改主要集中在[priconne](hoshino/modules/priconne)目录。 100 | 101 | #### 卡面(whois) 102 | 103 | | 命令 | 介绍 | 104 | | ----------------- | ------------------------------------------------------------ | 105 | | [X星]A(卡面\立绘) | 可以按星级查询角色A的卡面(立绘)
* 正确示例:1星羊驼卡面; 姐姐卡面; 3星吃货立绘
* 星级是1-6正整数,可以不输入,不输入将返回最高星级 | 106 | 107 | #### 更新卡池 108 | 109 | | 命令 | 介绍 | 110 | | --------------- | ------------------------------------------------------------ | 111 | | 更新(卡池\数据) | (**权限**:主人 ,**自动**)从指定api拉取并更新卡池配置和角色数据 | 112 | 113 | #### 仓库(gacha) 114 | 115 | | 命令 | 介绍 | 116 | | ---- | ------------ | 117 | | 仓库 | 展示三星仓库 | 118 | 119 | 120 | ## 待办事项 121 | 122 | - [x] 迫害龙王 123 | 124 | ~~其实已经写好了不过现在mirai还不支持获取cookie,就放在待办里面~~ 125 | 126 | - [x] RSS推送 127 | 终于写好了!(咕咕咕 128 | 129 | - [x] Saucenao识图 130 | 131 | 132 | ## Thanks 133 | 134 | [111234567890](https://github.com/111234567890) 135 | [tngsohack](https://github.com/kkbllt) 136 | [yuudi](https://github.com/yuudi) 137 | [ice-cirno](https://github.com/ice-cirno) 138 | [Lan](https://github.com/Lancercmd) 139 | 140 | ## 友情链接 141 | 142 | **Ice-Cirno/HoshinoBot**:https://github.com/Ice-Cirno/HoshinoBot 143 | 144 | **干炸里脊资源站**: https://redive.estertion.win/ 145 | 146 | **公主连结Re: Dive Fan Club - 硬核的竞技场数据分析站**: https://pcrdfans.com/ 147 | 148 | **yobot**: https://yobot.win/ 149 | 150 | -------------------------------------------------------------------------------- /config.example.py: -------------------------------------------------------------------------------- 1 | # 这是一份实例配置文件 2 | # 将其修改为你需要的配置,并将文件名修改为config.py 3 | 4 | from nonebot.default_config import * 5 | 6 | DEBUG = False 7 | 8 | SUPERUSERS = [10000] # 填写超级用户的QQ号,可填多个用半角逗号","隔开 9 | COMMAND_START = {''} # 命令前缀(空字符串匹配任何消息) 10 | COMMAND_SEP = set() # 命令分隔符(hoshino不需要该特性,保持为set()即可) 11 | NICKNAME = '' # 机器人的昵称。呼叫昵称等同于@bot,可用元组配置多个昵称 12 | 13 | 14 | # hoshino监听的端口与ip 15 | PORT = 8080 16 | HOST = '127.0.0.1' # Windows部署使用此条配置 17 | # HOST = '172.17.0.1' # linux + docker使用此条配置 18 | # docker桥的ip可能随环境不同而有变化 19 | # 使用这行命令`ip addr show docker0 | grep -Po 'inet \K[\d.]+'`查看你的docker桥ip 20 | # HOST = '172.18.0.1' # 阿里云的linux + docker多数情况是这样 21 | # HOST = '0.0.0.0' # 开放公网访问使用此条配置(不安全) 22 | 23 | IS_CQPRO = False # 是否使用Pro版酷Q功能 24 | GO_CQHTTP_WEBPORT = 9999 25 | # 资源库文件夹 Nonebot访问本机资源 26 | RESOURCE_DIR = './res/' 27 | 28 | # 资源库 URL 用于docker中的酷Q读取宿主机资源,注意以'/'结尾 29 | # 若留空则图片均采用base64编码发送,开销较大但部署方便 30 | # 若不清楚本项作用,请保持默认 31 | RESOURCE_URL = '' 32 | 33 | # 启用的模块 34 | # 初次尝试部署时请先保持默认 35 | # 如欲启用新模块,请认真阅读部署说明,逐个启用逐个配置 36 | # 切忌一次性开启多个 37 | MODULES_ON = { 38 | 'botmanage', 39 | 'dice', 40 | 'groupmaster', 41 | # 'hourcall', 42 | # 'kancolle', 43 | # 'mikan', 44 | 'pcrclanbattle', 45 | 'priconne', 46 | 'custom', 47 | # 'setu', 48 | 'translate', 49 | # 'twitter', 50 | } 51 | -------------------------------------------------------------------------------- /hoshino/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os import path 3 | import logging 4 | import nonebot 5 | 6 | os.makedirs(os.path.expanduser('~/.hoshino'), exist_ok=True) 7 | from .log import logger 8 | 9 | def init(config) -> nonebot.NoneBot: 10 | 11 | nonebot.init(config) 12 | bot = nonebot.get_bot() 13 | 14 | from .log import error_handler, critical_handler 15 | logger.setLevel(logging.DEBUG if bot.config.DEBUG else logging.INFO) 16 | nonebot.logger.setLevel(logging.DEBUG if bot.config.DEBUG else logging.INFO) 17 | nonebot.logger.addHandler(error_handler) 18 | nonebot.logger.addHandler(critical_handler) 19 | 20 | for module_name in config.MODULES_ON: 21 | nonebot.load_plugins( 22 | path.join(path.dirname(__file__), 'modules', module_name), 23 | f'hoshino.modules.{module_name}' 24 | ) 25 | 26 | return bot 27 | 28 | 29 | def get_bot() -> nonebot.NoneBot: 30 | return nonebot.get_bot() 31 | 32 | 33 | from nonebot import NoneBot, CommandSession, MessageSegment 34 | 35 | from .service import Service, Privilege,scheduled_job,sucmd 36 | from .res import R,ResImg 37 | -------------------------------------------------------------------------------- /hoshino/aiohttpx.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | from httpx import Response 3 | 4 | 5 | async def get(url: str, *args, **kwargs) -> Response: 6 | async with httpx.AsyncClient() as client: 7 | resp = await client.get(url, *args, **kwargs) 8 | return resp 9 | 10 | 11 | async def post(url: str, *args, **kwargs) -> Response: 12 | async with httpx.AsyncClient() as client: 13 | resp = await client.post(url, *args, **kwargs) 14 | return resp 15 | 16 | 17 | async def head(url: str, *args, **kwargs) -> Response: 18 | async with httpx.AsyncClient() as client: 19 | resp = await client.head(url, *args, **kwargs) 20 | return resp 21 | -------------------------------------------------------------------------------- /hoshino/aiorequests.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from functools import partial 3 | from typing import Optional, Any 4 | 5 | import requests 6 | from requests import * 7 | 8 | 9 | async def run_sync_func(func, *args, **kwargs) -> Any: 10 | return await asyncio.get_event_loop().run_in_executor( 11 | None, partial(func, *args, **kwargs)) 12 | 13 | 14 | class AsyncResponse: 15 | def __init__(self, response: requests.Response): 16 | self.raw_response = response 17 | 18 | @property 19 | def ok(self) -> bool: 20 | return self.raw_response.ok 21 | 22 | @property 23 | def status_code(self) -> int: 24 | return self.raw_response.status_code 25 | 26 | @property 27 | def headers(self): 28 | return self.raw_response.headers 29 | 30 | @property 31 | def url(self): 32 | return self.raw_response.url 33 | 34 | @property 35 | def encoding(self): 36 | return self.raw_response.encoding 37 | 38 | @property 39 | def cookies(self): 40 | return self.raw_response.cookies 41 | 42 | def __repr__(self): 43 | return '' % self.raw_response.status_code 44 | 45 | def __bool__(self): 46 | return self.ok 47 | 48 | @property 49 | async def content(self) -> Optional[bytes]: 50 | return await run_sync_func(lambda: self.raw_response.content) 51 | 52 | @property 53 | async def text(self) -> str: 54 | return await run_sync_func(lambda: self.raw_response.text) 55 | 56 | async def json(self, **kwargs) -> Any: 57 | return await run_sync_func(self.raw_response.json, **kwargs) 58 | 59 | def raise_for_status(self): 60 | self.raw_response.raise_for_status() 61 | 62 | 63 | async def request(method, url, **kwargs) -> AsyncResponse: 64 | return AsyncResponse(await run_sync_func(requests.request, 65 | method=method, url=url, **kwargs)) 66 | 67 | 68 | async def get(url, params=None, **kwargs) -> AsyncResponse: 69 | return AsyncResponse( 70 | await run_sync_func(requests.get, url=url, params=params, **kwargs)) 71 | 72 | 73 | async def options(url, **kwargs) -> AsyncResponse: 74 | return AsyncResponse( 75 | await run_sync_func(requests.options, url=url, **kwargs)) 76 | 77 | 78 | async def head(url, **kwargs) -> AsyncResponse: 79 | return AsyncResponse(await run_sync_func(requests.head, url=url, **kwargs)) 80 | 81 | 82 | async def post(url, data=None, json=None, **kwargs) -> AsyncResponse: 83 | return AsyncResponse(await run_sync_func(requests.post, url=url, 84 | data=data, json=json, **kwargs)) 85 | 86 | 87 | async def put(url, data=None, **kwargs) -> AsyncResponse: 88 | return AsyncResponse( 89 | await run_sync_func(requests.put, url=url, data=data, **kwargs)) 90 | 91 | 92 | async def patch(url, data=None, **kwargs) -> AsyncResponse: 93 | return AsyncResponse( 94 | await run_sync_func(requests.patch, url=url, data=data, **kwargs)) 95 | 96 | 97 | async def delete(url, **kwargs) -> AsyncResponse: 98 | return AsyncResponse( 99 | await run_sync_func(requests.delete, url=url, **kwargs)) 100 | -------------------------------------------------------------------------------- /hoshino/log.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Provide logger object. 3 | 4 | Any other modules in "hoshino" should use "logger" from this module to log messages. 5 | ''' 6 | 7 | import os 8 | import sys 9 | import logging 10 | 11 | _error_log_file = os.path.expanduser('~/.hoshino/error.log') 12 | _critical_log_file = os.path.expanduser('~/.hoshino/critical.log') 13 | 14 | formatter = logging.Formatter('[%(asctime)s %(name)s] %(levelname)s: %(message)s') 15 | logger = logging.getLogger('hoshino') 16 | default_handler = logging.StreamHandler(sys.stdout) 17 | default_handler.setFormatter(formatter) 18 | error_handler = logging.FileHandler(_error_log_file, encoding='utf8') 19 | error_handler.setLevel(logging.ERROR) 20 | error_handler.setFormatter(formatter) 21 | critical_handler = logging.FileHandler(_critical_log_file, encoding='utf8') 22 | critical_handler.setLevel(logging.CRITICAL) 23 | critical_handler.setFormatter(formatter) 24 | logger.addHandler(default_handler) 25 | logger.addHandler(error_handler) 26 | logger.addHandler(critical_handler) 27 | -------------------------------------------------------------------------------- /hoshino/modules/botmanage/broadcast.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from nonebot import on_command, CommandSession 4 | from nonebot import permission as perm 5 | from nonebot import CQHttpError 6 | 7 | from hoshino.log import logger 8 | 9 | 10 | @on_command('broadcast', aliases=('bc', '广播'), permission=perm.SUPERUSER) 11 | async def broadcast(session:CommandSession): 12 | msg = session.current_arg 13 | self_ids = session.bot._wsr_api_clients.keys() 14 | for sid in self_ids: 15 | gl = await session.bot.get_group_list(self_id=sid) 16 | gl = [ g['group_id'] for g in gl ] 17 | for g in gl: 18 | await asyncio.sleep(0.1) 19 | try: 20 | await session.bot.send_group_msg(self_id=sid, group_id=g, message=msg) 21 | logger.info(f'群{g} 投递广播成功') 22 | except CQHttpError as e: 23 | logger.error(f'Error: 群{g} 投递广播失败 {type(e)}') 24 | try: 25 | await session.send(f'Error: 群{g} 投递广播失败 {type(e)}') 26 | except CQHttpError as e: 27 | logger.critical(f'向广播发起者进行错误回报时发生错误:{type(e)}') 28 | await session.send(f'广播完成!') 29 | -------------------------------------------------------------------------------- /hoshino/modules/botmanage/data_cleaner.py: -------------------------------------------------------------------------------- 1 | import nonebot 2 | import nonebot.permission as perm 3 | from nonebot import on_command, CommandSession 4 | from hoshino.log import logger 5 | 6 | 7 | # @nonebot.scheduler.scheduled_job('cron', hour='4') 8 | # async def image_cleaner(): 9 | # self_id = list(nonebot.get_bot()._wsr_api_clients.keys())[0] 10 | # await nonebot.get_bot().clean_data_dir(self_id=self_id, data_dir='image') 11 | # logger.info('Image 文件夹已清理') 12 | 13 | 14 | @on_command('清理数据', permission=perm.SUPERUSER, only_to_me=True) 15 | async def clean_image(session:CommandSession): 16 | await nonebot.get_bot().clean_data_dir(self_id=session.ctx['self_id'], data_dir='image') 17 | await session.send('Image 文件夹已清理') 18 | -------------------------------------------------------------------------------- /hoshino/modules/botmanage/feedback.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service,CommandSession,Privilege as Priv 2 | from hoshino.util import DailyNumberLimiter 3 | 4 | _max = 1 5 | _lmt = DailyNumberLimiter(_max) 6 | EXCEED_NOTICE = f'您今天已经喝过{_max}杯了,请明早5点后再来!' 7 | sv=Service('feedback',enable_on_default=True,visible=False,manage_priv=Priv.SUPERUSER) 8 | @sv.on_command('来杯咖啡') 9 | async def feedback(session:CommandSession): 10 | uid = session.ctx['user_id'] 11 | if not _lmt.check(uid): 12 | session.finish(EXCEED_NOTICE, at_sender=True) 13 | coffee = session.bot.config.SUPERUSERS[0] 14 | text = session.current_arg 15 | if not text: 16 | await session.send(f"来杯咖啡[空格]后输入您要反馈的内容~", at_sender=True) 17 | else: 18 | await session.bot.send_private_msg(self_id=session.ctx['self_id'], user_id=coffee, message=f'Q{uid}@群{session.ctx["group_id"]}\n{text}') 19 | await session.send(f'您的反馈已发送!\n=======\n{text}', at_sender=True) 20 | _lmt.increase(uid) 21 | -------------------------------------------------------------------------------- /hoshino/modules/botmanage/help.py.bak: -------------------------------------------------------------------------------- 1 | from hoshino import Service, Privilege as Priv 2 | 3 | sv = Service('_help_', manage_priv=Priv.SUPERUSER, visible=False) 4 | 5 | MANUAL = ''' 6 | ===================== 7 | - HoshinoBot使用说明 - 8 | ===================== 9 | 输入方括号[]内的关键词即可触发相应的功能 10 | ※注意其中的【空格】不可省略! 11 | ※部分功能必须手动at本bot才会触发(复制无效) 12 | ※本bot的功能采取模块化管理,群管理可控制开关 13 | ※※调教时请注意使用频率,您的滥用可能会导致bot账号被封禁 14 | ===从此开始↓一行距=== 15 | 16 | ================== 17 | - 公主连接Re:Dive - 18 | ================== 19 | [@bot来发十连] 十连转蛋模拟 20 | [@bot来发单抽] 单抽转蛋模拟 21 | [@bot来一井] 4w5钻!买定离手! 22 | [@bot妈] 给主さま盖章章 23 | [查看卡池] 查看bot现在的卡池及出率 24 | [怎么拆 妹弓] 后以空格隔开接角色名,查询竞技场解法 25 | [pcr速查] 常用网址/速查表 26 | [bcr速查] B服萌新攻略 27 | [rank表] 查看rank推荐表 28 | [黄骑充电表] 查询黄骑1动充电规律 29 | [@bot官漫132] 官方四格阅览 30 | [禁用 pcr-twitter] 禁用日服官推转发 31 | [启用 pcr-arena-reminder-jp] 背刺时间提醒(UTC+9) 32 | [启用 pcr-arena-reminder-tw] 背刺时间提醒(UTC+8) 33 | [挖矿 15001] 查询矿场中还剩多少钻 34 | [切噜一下] 后以空格隔开接想要转换为切噜语的话 35 | [切噜~♪切啰巴切拉切蹦切蹦] 切噜语翻译 36 | [!帮助] 查看会战管理功能的说明 37 | =========== 38 | - 通用功能 - 39 | =========== 40 | [启用 bangumi] 开启番剧更新推送 41 | - [@bot来点新番] 查看最近的更新(↑需先开启番剧更新推送↑) 42 | [.r] 掷骰子 43 | [.r 3d12] 掷3次12面骰子 44 | [@bot精致睡眠] 8小时精致睡眠(bot需具有群管理权限) 45 | [给我来一份精致昏睡下午茶套餐] 叫一杯先辈特调红茶(bot需具有群管理权限) 46 | [@bot来杯咖啡] 联系维护组,空格后接反馈内容 47 | ========== 48 | - 艦これ - 49 | ========== 50 | [启用 hourcall] 时报 51 | [启用 kc-reminder] 演习/月常远征提醒 52 | [启用 kc-twitter] 艦これ官推转发 53 | [启用 kc-query] 开启kancolle查询功能(下详) 54 | - [*晓改二] 舰娘信息查询 55 | - [*震电] 装备信息查询 56 | - [人事表200102] 查询战果人事表(年/月/服务器) 57 | [.qj 晓] 预测ケッコンカッコカリ运值增加(准确率高达25%) 58 | ================= 59 | - 群管理限定功能 - 60 | ================= 61 | [翻译 もう一度、キミとつながる物語] 机器翻译 62 | [lssv] 查看功能模块的开关状态 63 | 64 | ======== 65 | ※除帮助中写明外 另有其他隐藏功能:) 66 | ※服务器运行需要成本,赞助支持请私戳作者 67 | ※本bot开源,可自行搭建 68 | ※您的支持是本bot更新维护的动力 69 | 70 | ※※初次使用请仔细阅读帮助开头的注意事项 71 | ※※调教时请注意使用频率,您的滥用可能会导致bot账号被封禁 72 | '''.strip() 73 | 74 | @sv.on_command('help', aliases=('manual', '帮助', '说明', '使用说明', '幫助', '說明', '使用說明', '菜单', '菜單'), only_to_me=False) 75 | async def send_help(session): 76 | await session.send(MANUAL) 77 | -------------------------------------------------------------------------------- /hoshino/modules/botmanage/join_group.py: -------------------------------------------------------------------------------- 1 | import nonebot 2 | from nonebot import on_request, RequestSession 3 | 4 | 5 | @on_request('group.invite') 6 | async def handle_group_invite(session:RequestSession): 7 | if session.ctx['user_id'] in nonebot.get_bot().config.SUPERUSERS: 8 | await session.approve() 9 | else: 10 | await session.reject(reason='邀请入群请联系维护组') 11 | -------------------------------------------------------------------------------- /hoshino/modules/botmanage/ls.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, CommandSession 2 | from nonebot import permission as perm 3 | from nonebot.argparse import ArgumentParser 4 | 5 | from hoshino.service import Service 6 | 7 | async def ls_group(session: CommandSession): 8 | bot = session.bot 9 | self_ids = bot._wsr_api_clients.keys() 10 | for sid in self_ids: 11 | gl = await bot.get_group_list(self_id=sid) 12 | msg = [ "{group_id} {group_name}".format_map(g) for g in gl ] 13 | msg = "\n".join(msg) 14 | msg = f"bot:{sid}\n| 群号 | 群名 | 共{len(gl)}个群\n" + msg 15 | await bot.send_private_msg(self_id=sid, user_id=bot.config.SUPERUSERS[0], message=msg) 16 | 17 | 18 | async def ls_friend(session: CommandSession): 19 | gl = await session.bot.get_friend_list(self_id=session.ctx['self_id']) 20 | msg = [ "{user_id} {nickname}".format_map(g) for g in gl ] 21 | msg = "\n".join(msg) 22 | msg = f"| QQ号 | 昵称 | 共{len(gl)}个好友\n" + msg 23 | await session.send(msg) 24 | 25 | 26 | async def ls_service(session: CommandSession, service_name:str): 27 | all_services = Service.get_loaded_services() 28 | if service_name in all_services: 29 | sv = all_services[service_name] 30 | on_g = '\n'.join(map(lambda x: str(x), sv.enable_group)) 31 | off_g = '\n'.join(map(lambda x: str(x), sv.disable_group)) 32 | default_ = 'enabled' if sv.enable_on_default else 'disabled' 33 | msg = f"服务{sv.name}:\n默认:{default_}\nuse_priv={sv.use_priv}\nmanage_priv={sv.manage_priv}\nvisible={sv.visible}\n启用群:\n{on_g}\n禁用群:\n{off_g}" 34 | session.finish(msg) 35 | else: 36 | session.finish(f'未找到服务{service_name}') 37 | 38 | 39 | async def ls_bot(session:CommandSession): 40 | self_ids = session.bot._wsr_api_clients.keys() 41 | msg = str(self_ids) 42 | await session.send(msg) 43 | 44 | 45 | @on_command('ls', permission=perm.SUPERUSER, shell_like=True) 46 | async def ls(session: CommandSession): 47 | parser = ArgumentParser(session=session) 48 | switch = parser.add_mutually_exclusive_group() 49 | switch.add_argument('-g', '--group', action='store_true') 50 | switch.add_argument('-f', '--friend', action='store_true') 51 | switch.add_argument('-b', '--bot', action='store_true') 52 | switch.add_argument('-s', '--service') 53 | args = parser.parse_args(session.argv) 54 | 55 | if args.group: 56 | await ls_group(session) 57 | elif args.friend: 58 | await ls_friend(session) 59 | elif args.bot: 60 | await ls_bot(session) 61 | elif args.service: 62 | await ls_service(session, args.service) 63 | -------------------------------------------------------------------------------- /hoshino/modules/botmanage/service_manage.py: -------------------------------------------------------------------------------- 1 | from functools import cmp_to_key 2 | 3 | from nonebot import on_command, CommandSession 4 | from nonebot import permission as perm 5 | from nonebot import CQHttpError 6 | from nonebot.argparse import ArgumentParser 7 | 8 | from hoshino.service import Service, Privilege as Priv 9 | 10 | PRIV_TIP = f'群主={Priv.OWNER} 群管={Priv.ADMIN} 群员={Priv.NORMAL} bot维护组={Priv.SUPERUSER}' 11 | 12 | @on_command('lssv', aliases=('服务列表', '功能列表'), permission=perm.GROUP_ADMIN, only_to_me=False, shell_like=True) 13 | async def lssv(session:CommandSession): 14 | parser = ArgumentParser(session=session) 15 | parser.add_argument('-a', '--all', action='store_true') 16 | parser.add_argument('-g', '--group', type=int, default=0) 17 | args = parser.parse_args(session.argv) 18 | 19 | verbose_all = args.all 20 | if session.ctx['user_id'] in session.bot.config.SUPERUSERS: 21 | gid = args.group or session.ctx.get('group_id') 22 | if not gid: 23 | session.finish('Usage: -g|--group [-a|--all]') 24 | else: 25 | gid = session.ctx['group_id'] 26 | 27 | msg = [f"群{gid}服务一览:"] 28 | svs = Service.get_loaded_services().values() 29 | svs = map(lambda sv: (sv, sv.check_enabled(gid)), svs) 30 | key = cmp_to_key(lambda x, y: (y[1] - x[1]) or (-1 if x[0].name < y[0].name else 1 if x[0].name > y[0].name else 0)) 31 | svs = sorted(svs, key=key) 32 | for sv, on in svs: 33 | if sv.visible or verbose_all: 34 | x = '○' if on else '×' 35 | msg.append(f"|{x}| {sv.name}") 36 | await session.send('\n'.join(msg)) 37 | 38 | 39 | @on_command('enable', aliases=('启用', '开启', '打开'), permission=perm.GROUP, only_to_me=False) 40 | async def enable_service(session:CommandSession): 41 | await switch_service(session, turn_on=True) 42 | 43 | @on_command('disable', aliases=('禁用', '关闭'), permission=perm.GROUP, only_to_me=False) 44 | async def disable_service(session:CommandSession): 45 | await switch_service(session, turn_on=False) 46 | 47 | async def switch_service(session:CommandSession, turn_on:bool): 48 | action_tip = '启用' if turn_on else '禁用' 49 | if session.ctx['message_type'] == 'group': 50 | names = session.current_arg_text.split() 51 | if not names: 52 | session.finish(f"空格后接要{action_tip}的服务名", at_sender=True) 53 | group_id = session.ctx['group_id'] 54 | svs = Service.get_loaded_services() 55 | succ, notfound = [], [] 56 | for name in names: 57 | if name in svs: 58 | sv = svs[name] 59 | u_priv = sv.get_user_priv(session.ctx) 60 | if u_priv >= sv.manage_priv: 61 | sv.set_enable(group_id) if turn_on else sv.set_disable(group_id) 62 | succ.append(name) 63 | else: 64 | try: 65 | await session.send(f'权限不足!{action_tip}{name}需要:{sv.manage_priv},您的:{u_priv}\n{PRIV_TIP}', at_sender=True) 66 | except: 67 | pass 68 | else: 69 | notfound.append(name) 70 | msg = [] 71 | if succ: 72 | msg.append(f'已{action_tip}服务:' + ', '.join(succ)) 73 | if notfound: 74 | msg.append('未找到服务:' + ', '.join(notfound)) 75 | if msg: 76 | session.finish('\n'.join(msg), at_sender=True) 77 | 78 | else: 79 | if session.ctx['user_id'] not in session.bot.config.SUPERUSERS: 80 | return 81 | args = session.current_arg_text.split() 82 | if len(args) < 2: 83 | session.finish('Usage: [, ...]') 84 | name, *group_ids = args 85 | svs = Service.get_loaded_services() 86 | if name not in svs: 87 | session.finish(f'未找到服务:{name}') 88 | sv = svs[name] 89 | succ = [] 90 | for gid in group_ids: 91 | try: 92 | gid = int(gid) 93 | sv.set_enable(gid) if turn_on else sv.set_disable(gid) 94 | succ.append(gid) 95 | except: 96 | try: 97 | await session.send(f'非法群号:{gid}') 98 | except: 99 | pass 100 | session.finish(f'服务{name}已于{len(succ)}个群内{action_tip}:{succ}') 101 | -------------------------------------------------------------------------------- /hoshino/modules/custom/QAmirai/.gitignore: -------------------------------------------------------------------------------- 1 | qa.db -------------------------------------------------------------------------------- /hoshino/modules/custom/QAmirai/__init__.py: -------------------------------------------------------------------------------- 1 | import time 2 | from .data import Question 3 | from hoshino import Service, Privilege as Priv, CommandSession 4 | answers = {} 5 | sv = Service('QA-mirai') 6 | 7 | 8 | 9 | def union(group_id, user_id): 10 | return (group_id << 32) | user_id 11 | 12 | # recovery from database 13 | for qu in Question.select(): 14 | if qu.quest not in answers: 15 | answers[qu.quest] = {} 16 | answers[qu.quest][union(qu.rep_group, qu.rep_member)] = qu.answer 17 | 18 | 19 | @sv.on_message('group') 20 | async def setqa(bot, context): 21 | message = context['raw_message'] 22 | if message.startswith('我问'): 23 | msg = message[2:].split('你答', 1) 24 | if len(msg) == 1: 25 | return 26 | if len(msg[0])==0 or len(msg[1])==0: 27 | await bot.send(context, '提问和回答不可以是空字符串!\n', at_sender=True) 28 | return 29 | q, a = msg 30 | if 'granbluefantasy.jp' in q or 'granbluefantasy.jp' in a: 31 | await bot.send(context, '骑空士还挺会玩儿?爬!\n', at_sender=True) 32 | return 33 | if q not in answers: 34 | answers[q] = {} 35 | answers[q][union(context['group_id'], context['user_id'])] = a 36 | Question.replace( 37 | quest=q, 38 | rep_group=context['group_id'], 39 | rep_member=context['user_id'], 40 | answer=a, 41 | creator=context['user_id'], 42 | create_time=time.time(), 43 | ).execute() 44 | await bot.send(context, f'好的我记住了', at_sender=False) 45 | elif message.startswith('大家问') or message.startswith('有人问'): 46 | if not sv.check_priv(context, required_priv=Priv.ADMIN): 47 | await bot.send(context, f'只有管理员才可以用{message[:3]}', at_sender=False) 48 | return 49 | msg = message[3:].split('你答', 1) 50 | if len(msg) == 1: 51 | return 52 | if len(msg[0])==0 or len(msg[1])==0: 53 | await bot.send(context, '提问和回答不可以是空字符串!\n', at_sender=True) 54 | return 55 | q, a = msg 56 | if q not in answers: 57 | answers[q] = {} 58 | answers[q][union(context['group_id'], 1)] = a 59 | Question.replace( 60 | quest=q, 61 | rep_group=context['group_id'], 62 | rep_member=1, 63 | answer=a, 64 | creator=context['user_id'], 65 | create_time=time.time(), 66 | ).execute() 67 | await bot.send(context, f'好的我记住了', at_sender=False) 68 | elif message.startswith('不要回答') or message.startswith('不再回答'): 69 | q = context['raw_message'][4:] 70 | ans = answers.get(q) 71 | if ans is None: 72 | await bot.send(context, f'我不记得有这个问题', at_sender=False) 73 | specific = union(context['group_id'], context['user_id']) 74 | a = ans.get(specific) 75 | if a: 76 | Question.delete().where( 77 | Question.quest == q, 78 | Question.rep_group == context['group_id'], 79 | Question.rep_member == context['user_id'], 80 | ).execute() 81 | del ans[specific] 82 | if not ans: 83 | del answers[q] 84 | await bot.send(context, f'我不再回答"{a}"了', at_sender=False) 85 | elif message.startswith('删除有人问') or message.startswith('删除大家问'): 86 | q = context['raw_message'][5:] 87 | ans = answers.get(q) 88 | if not sv.check_priv(context, required_priv=Priv.ADMIN): 89 | await bot.send(context, f'只有管理员才能删除"有人问"的问题', at_sender=False) 90 | return 91 | wild = union(context['group_id'], 1) 92 | a = ans.get(wild) 93 | if a: 94 | Question.delete().where( 95 | Question.quest == q, 96 | Question.rep_group == context['group_id'], 97 | Question.rep_member == 1, 98 | ).execute() 99 | del ans[wild] 100 | if not ans: 101 | del answers[q] 102 | await bot.send(context, f'我不再回答"{a}"了', at_sender=False) 103 | 104 | 105 | @sv.on_command('查QA', aliases=('看看我问')) 106 | async def lookqa(session: CommandSession): 107 | uid = session.ctx['user_id'] 108 | gid = session.ctx['group_id'] 109 | result = Question.select(Question.quest).where( 110 | Question.rep_group == gid, Question.rep_member == uid) 111 | msg = ['您在该群中设置的问题是:'] 112 | for res in result: 113 | msg.append(res.quest) 114 | await session.send('/'.join(msg), at_sender=True) 115 | 116 | 117 | @sv.on_command('查有人问', aliases=('看看有人问', '看看大家问', '查找有人问')) 118 | async def lookgqa(session: CommandSession): 119 | gid = session.ctx['group_id'] 120 | result = Question.select(Question.quest).where( 121 | Question.rep_group == gid, Question.rep_member == 1) 122 | msg = ['该群设置的"有人问"是:'] 123 | for res in result: 124 | msg.append(res.quest) 125 | await session.send('/'.join(msg), at_sender=True) 126 | 127 | @sv.on_command('删除我问', aliases=('删我问', '删人问')) 128 | async def delqa(session: CommandSession): 129 | ctx = session.ctx 130 | gid = ctx['group_id'] 131 | q=session.current_arg_text.strip() 132 | ans = answers.get(q) 133 | if not sv.check_priv(ctx, required_priv=Priv.ADMIN): 134 | session.finish('只有管理员才能删除指定人的问题', at_sender=False) 135 | if ans is None: 136 | session.finish('我不记得有这个问题') 137 | for m in ctx['message']: 138 | if m.type == 'at' and m.data['qq'] != 'all' : 139 | uid = int(m.data['qq']) 140 | break 141 | specific = union(gid, uid) 142 | a = ans.get(specific) 143 | if a: 144 | Question.delete().where( 145 | Question.quest == q, 146 | Question.rep_group == gid, 147 | Question.rep_member == uid, 148 | ).execute() 149 | del ans[specific] 150 | if not ans: 151 | del answers[q] 152 | session.finish(f'删除{q}成功\n不再回答"{a}"', at_sender=False) 153 | @sv.on_message('group') 154 | async def answer(bot, context): 155 | ans = answers.get(context['raw_message']) 156 | if ans: 157 | a = ans.get(union(context['group_id'], context['user_id'])) 158 | if a: 159 | await bot.send(context, a, at_sender=False) 160 | return 161 | b = ans.get(union(context['group_id'], 1)) 162 | if b: 163 | await bot.send(context, b, at_sender=False) 164 | return 165 | -------------------------------------------------------------------------------- /hoshino/modules/custom/QAmirai/data.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | import peewee as pw 4 | 5 | db = pw.SqliteDatabase( 6 | os.path.join(os.path.dirname(__file__), 'qa.db') 7 | ) 8 | 9 | 10 | class Question(pw.Model): 11 | quest = pw.TextField() 12 | answer = pw.TextField() 13 | rep_group = pw.IntegerField(default=0) # 0 for none and 1 for all 14 | rep_member = pw.IntegerField(default=0) 15 | allow_private = pw.BooleanField(default=False) 16 | creator = pw.IntegerField() 17 | create_time = pw.TimestampField() 18 | 19 | class Meta: 20 | database = db 21 | primary_key = pw.CompositeKey('quest', 'rep_group', 'rep_member') 22 | 23 | 24 | def init(): 25 | if not os.path.exists(os.path.join(os.path.dirname(__file__), 'qa.db')): 26 | db.connect() 27 | db.create_tables([Question]) 28 | db.close() 29 | 30 | 31 | init() 32 | -------------------------------------------------------------------------------- /hoshino/modules/custom/QAmirai/readme.txt: -------------------------------------------------------------------------------- 1 | 该插件改自QA,用于mirai-HoshinoBot平台,可以在答案中设置静态或动态图片 2 | 放入到HoshinoBot的任意一个module下即可 3 | 该插件与QA尽量不要共存 4 | -------------------------------------------------------------------------------- /hoshino/modules/custom/anti_recall.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service,Privilege as Priv 2 | sv=Service('anti_group_recall',visible=False,enable_on_default=False,manage_priv=Priv.SUPERUSER) 3 | @sv.on_notice('group_recall') 4 | async def _(session): 5 | gid=session.event.group_id 6 | uid=session.event.user_id 7 | oid=session.event.operator_id 8 | msgid=session.event.message_id 9 | msgdic=await session.bot.get_msg(message_id=msgid) 10 | msg=msgdic['raw_message'] 11 | user_dic = await session.bot.get_group_member_info(group_id=session.event.group_id, user_id=session.event.user_id, 12 | no_cache=True) 13 | user_card = user_dic['card'] if user_dic['card'] else user_dic['nickname'] 14 | if oid==uid: 15 | await session.bot.send_group_msg(message=f'{user_card}({uid})撤回消息:\n{msg}',group_id=gid) 16 | else: 17 | await session.bot.send_group_msg(message=f'管理员撤回了{user_card}({uid})的消息:\n{msg}',group_id=gid) -------------------------------------------------------------------------------- /hoshino/modules/custom/antiqks.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | from hoshino import R, Service,Privilege as Priv,aiorequests 3 | 4 | sv = Service('antiqks',visible=False,manage_priv=Priv.SUPERUSER,enable_on_default=True) 5 | 6 | 7 | qksimg = R.img('qksimg.jpg').cqcode 8 | 9 | 10 | @sv.on_keyword(["granbluefantasy.jp"], normalize=True, event='group',can_private=1) 11 | async def qks_keyword(bot, ctx): 12 | msg = f'?¿\n{qksimg}' 13 | await bot.send(ctx, msg, at_sender=True) 14 | async def check_gbf(url): 15 | try: 16 | resp=await aiorequests.head(url,allow_redirects=False) 17 | except: 18 | return 19 | h = resp.headers 20 | s = resp.status_code 21 | try: 22 | if s == 301 or s == 302: 23 | if 'granbluefantasy.jp' in h['Location']: 24 | return True,h['Location'] 25 | else: 26 | return False,h['Location'] 27 | except: 28 | return None 29 | 30 | #潜在的安全风险和概率影响性能,故antiqks请谨慎开启 31 | @sv.on_rex(r'https?:\/\/[a-z0-9A-Z\.\-]{4,11}\/[a-zA-Z0-9\-]+', normalize=False, event='group',can_private=1) 32 | async def qks_rex(bot, ctx, match): 33 | msg = f'?¿?¿?¿\n{qksimg}' 34 | res = match.group(0) 35 | if (a:=await check_gbf(res)) is None: 36 | return 37 | elif a[0] or (await check_gbf(a[1]))[0] : 38 | await bot.send(ctx, msg, at_sender=True) 39 | return 40 | 41 | 42 | -------------------------------------------------------------------------------- /hoshino/modules/custom/arc/__init__.py: -------------------------------------------------------------------------------- 1 | from aiocqhttp.exceptions import Error as CQHttpError 2 | import requests 3 | import demjson 4 | import random 5 | import math 6 | import time as _time 7 | import os 8 | from .arcaea_crawler import * 9 | from hoshino import Service,CommandSession,Privilege as Priv 10 | sv=Service('arc',visible=False,enable_on_default=False,manage_priv=Priv.SUPERUSER) 11 | dsname=os.path.join(os.path.dirname(__file__),'ds.txt') 12 | f = open(dsname, 'r', encoding='utf-8') 13 | dss = f.readlines() 14 | f.close() 15 | 16 | help_text = '''欢迎使用Arcaea Bot。支持的命令如下: 17 | .ds <曲名/等级> 查询定数 18 | .arc <玩家名/好友码> 查询玩家的ptt、r10/b30和最近游玩的歌曲 19 | .best <玩家名/好友码> 查询玩家ptt前n的歌曲''' 20 | 21 | 22 | @sv.on_command('archelp', only_to_me=False) 23 | async def helpp(session: CommandSession): 24 | await session.send(help_text) 25 | 26 | 27 | @sv.on_command('arcbest', only_to_me=False) 28 | async def lookup(session: CommandSession): 29 | await session.send("Looking up %s\nWarning: .best命令具有刷屏风险,请尽量私聊查询~" % session.state['id']) 30 | QueryThread(session.cmd, session.ctx, session.bot, session.state).start() 31 | 32 | 33 | @lookup.args_parser 34 | async def _(session: CommandSession): 35 | arr = session.current_arg_text.strip().split(' ') 36 | session.state['id'] = arr[0] 37 | try: 38 | session.state['num'] = int(arr[1]) 39 | except Exception: 40 | session.state['num'] = 0 41 | 42 | 43 | @sv.on_command('arcaea', aliases=['arc'], only_to_me=False) 44 | async def arcaea(session: CommandSession): 45 | await session.send("Querying %s" % session.state['id']) 46 | QueryThread(session.cmd, session.ctx, session.bot, session.state).start() 47 | 48 | 49 | @arcaea.args_parser 50 | async def _(session: CommandSession): 51 | session.state['id'] = session.current_arg_text.strip() 52 | 53 | 54 | @sv.on_command('arcds', only_to_me=False) 55 | async def ds(session: CommandSession): 56 | result_str = "" 57 | num = 0 58 | for line in dss: 59 | if session.state['arg'].lower() in line.lower(): 60 | num += 1 61 | result_str += line.replace('\t', ' ') 62 | await session.send("共找到%d条结果:\n" % num + result_str[:-1]) 63 | 64 | 65 | @ds.args_parser 66 | async def _(session: CommandSession): 67 | session.state['arg'] = session.current_arg_text.strip() -------------------------------------------------------------------------------- /hoshino/modules/custom/arc/arcaea_crawler.py: -------------------------------------------------------------------------------- 1 | import websocket 2 | import brotli 3 | import json 4 | import threading 5 | import os 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 | namecache=os.path.join(os.path.dirname(__file__),'arc_namecache.txt') 11 | f = open(namecache, 'w') 12 | f.close() 13 | 14 | 15 | def load_cache(): 16 | cache = {} 17 | f = open(namecache, '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(namecache, '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() -------------------------------------------------------------------------------- /hoshino/modules/custom/arc/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 -------------------------------------------------------------------------------- /hoshino/modules/custom/calendar/__init__.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service, R, sucmd, scheduled_job 2 | from .calendar import * 3 | from PIL import ImageFont 4 | from hoshino.util import text2CQ 5 | svjp = Service('calendar-jp', enable_on_default=False) 6 | svbl = Service('calendar-bili', enable_on_default=False) 7 | svtw = Service('calendar-tw', enable_on_default=False) 8 | fontpath = R.img('priconne/gadget/simhei.ttf').path 9 | font = ImageFont.truetype(fontpath, 20) 10 | 11 | 12 | @scheduled_job('cron', hour='*/3', jitter=40) 13 | async def db_check_ver(): 14 | await check_ver(svjp, 'jp') 15 | await check_ver(svbl, 'bili') 16 | await check_ver(svtw, 'tw') 17 | 18 | 19 | @svjp.scheduled_job('cron', hour='15', minute='05') 20 | async def push_jp_calendar(): 21 | await svjp.broadcast(text2CQ(await db_message(svjp, 'jp'), font), 'calendar-jp') 22 | 23 | 24 | @svbl.scheduled_job('cron', hour='15', minute='15') 25 | async def push_bl_calendar(): 26 | await svbl.broadcast(text2CQ(await db_message(svbl, 'bili'), font), 'calendar-bilibili') 27 | 28 | 29 | @svtw.scheduled_job('cron', hour='15', minute='25') 30 | async def push_bl_calendar(): 31 | await svtw.broadcast(text2CQ(await db_message(svtw, 'tw'), font), 'calendar-tw') 32 | 33 | 34 | @sucmd('updatedb', aliases=('更新数据库'), force_private=False) 35 | async def forceupdatedb(session): 36 | codejp = await check_ver(svjp, 'jp') 37 | codebl = await check_ver(svbl, 'bili') 38 | codetw = await check_ver(svbl, 'tw') 39 | successcount = 0 40 | failcount = 0 41 | for i in [codebl, codejp, codetw]: 42 | if i == 0: 43 | successcount += 1 44 | elif i == -1: 45 | failcount += 1 46 | if failcount != 0: 47 | session.finish(f'检测数据库更新失败,失败数量:{failcount}.请前往后台查看') 48 | else: 49 | session.finish(f'检测数据库版本成功,{successcount}个数据库有更新') 50 | 51 | 52 | @svbl.on_rex(r'^[bB国]服(当前|预定)?日程$', normalize=True, event='group') 53 | async def look_bilibili_calendar(bot, ctx, match): 54 | is_now = match.group(1) == '当前' 55 | is_future = match.group(1) == '预定' 56 | is_all = not match.group(1) 57 | if is_now: 58 | await bot.send(ctx, text2CQ(await db_message(svbl, 'bili', 'now'), font), at_sender=True) 59 | if is_future: 60 | await bot.send(ctx, text2CQ(await db_message(svbl, 'bili', 'future'), font), at_sender=True) 61 | if is_all: 62 | await bot.send(ctx, text2CQ(await db_message(svbl, 'bili', 'all'), font), at_sender=True) 63 | 64 | 65 | @svjp.on_rex(r'^日服(当前|预定)?日程$', normalize=True, event='group') 66 | async def look_jp_calendar(bot, ctx, match): 67 | is_now = match.group(1) == '当前' 68 | is_future = match.group(1) == '预定' 69 | is_all = not match.group(1) 70 | if is_now: 71 | await bot.send(ctx, text2CQ(await db_message(svjp, 'jp', 'now'), font), at_sender=True) 72 | if is_future: 73 | await bot.send(ctx, text2CQ(await db_message(svjp, 'jp', 'future'), font), at_sender=True) 74 | if is_all: 75 | await bot.send(ctx, text2CQ(await db_message(svjp, 'jp', 'all'), font), at_sender=True) 76 | 77 | 78 | @svtw.on_rex(r'^台服(当前|预定)?日程$', normalize=True, event='group') 79 | async def look_tw_calendar(bot, ctx, match): 80 | is_now = match.group(1) == '当前' 81 | is_future = match.group(1) == '预定' 82 | is_all = not match.group(1) 83 | if is_now: 84 | await bot.send(ctx, text2CQ(await db_message(svtw, 'tw', 'now'), font), at_sender=True) 85 | if is_future: 86 | await bot.send(ctx, text2CQ(await db_message(svtw, 'tw', 'future'), font), at_sender=True) 87 | if is_all: 88 | await bot.send(ctx, text2CQ(await db_message(svtw, 'tw', 'all'), font), at_sender=True) 89 | -------------------------------------------------------------------------------- /hoshino/modules/custom/calendar/calendar.py: -------------------------------------------------------------------------------- 1 | from hoshino import aiorequests 2 | import json 3 | import brotli 4 | import time 5 | import os 6 | import sqlite3 7 | from . import Service 8 | 9 | from .campaign import parse_campaign 10 | 11 | _resource_path = os.path.expanduser('~/.hoshino') 12 | 13 | 14 | # 开辟一个数组来储存地址,依次是服务器数据库,服务器版本Json,本地数据库,本地版本Json,消息提示词 15 | # bilibili 16 | bili_url = 'https://redive.estertion.win/db/redive_cn.db.br' 17 | bili_verurl = 'https://redive.estertion.win/last_version_cn.json' 18 | bili_db = os.path.join(_resource_path, 'redive_bili.db') 19 | bili_ver = os.path.join(_resource_path, 'bili_ver.json') 20 | bililist = [bili_url, bili_verurl, bili_db, bili_ver,"b服日程"] 21 | # jp 22 | jp_url = 'https://redive.estertion.win/db/redive_jp.db.br' 23 | jp_verurl = 'https://redive.estertion.win/last_version_jp.json' 24 | jp_db = os.path.join(_resource_path, 'redive_jp.db') 25 | jp_ver = os.path.join(_resource_path, 'jp_ver.json') 26 | jplist = [jp_url, jp_verurl, jp_db, jp_ver,"日服日程"] 27 | # tw(sonet f**k you!) 28 | # 台服api由tngsohack提供,感谢! 29 | tw_url= 'https://api.redive.lolikon.icu/br/redive_tw.db.br' 30 | tw_verurl='https://api.redive.lolikon.icu/json/lastver_tw.json' 31 | tw_db = os.path.join(_resource_path, 'redive_tw.db') 32 | tw_ver = os.path.join(_resource_path, 'tw_ver.json') 33 | twlist = [tw_url, tw_verurl, tw_db, tw_ver,"台服日程"] 34 | 35 | regiondic={'bili':bililist,'tw':twlist,'jp':jplist} 36 | 37 | 38 | async def updateDB(sv: Service, serid:str): 39 | ls=regiondic[serid] 40 | ver_res =await aiorequests.get(ls[1]) 41 | if ver_res.status_code != 200: 42 | sv.logger.warning('连接服务器失败') 43 | return 44 | ver_get=await ver_res.content 45 | ver = json.loads(ver_get) 46 | ver_path = ls[3] 47 | db_res =await aiorequests.get(ls[0]) 48 | if db_res.status_code != 200: 49 | sv.logger.warning('连接服务器失败') 50 | return 51 | data_get =await db_res.content 52 | data = brotli.decompress(data_get) 53 | db_path = ls[2] 54 | with open(db_path, 'wb') as dbfile: 55 | dbfile.write(data) 56 | dbfile.close() 57 | with open(ver_path, 'w', encoding='utf8') as vfile: 58 | json.dump(ver, vfile, ensure_ascii=False) 59 | vfile.close() 60 | sv.logger.info(f'{serid}数据库更新成功') 61 | 62 | 63 | async def check_ver(sv: Service, serid:str): 64 | ls=regiondic[serid] 65 | try: 66 | with open(ls[3], encoding='utf8') as vfile: 67 | local_ver = json.load(vfile) 68 | except FileNotFoundError as e: 69 | sv.logger.warning(f'未发现{serid}数据库,将会稍后创建') 70 | await updateDB(sv, serid) 71 | return 0 72 | ver_res = await aiorequests.get(ls[1]) 73 | if ver_res.status_code != 200: 74 | sv.logger.warning('连接服务器失败') 75 | return -1 76 | ver_get = await ver_res.content 77 | online_ver = json.loads(ver_get) 78 | if local_ver == online_ver: 79 | sv.logger.info(f'未发现{serid}数据库更新') 80 | return 1 81 | else: 82 | sv.logger.info(f'发现{serid}数据库更新,将会稍后更新') 83 | await updateDB(sv, serid) 84 | return 0 85 | 86 | 87 | def campaign_logout(campaign, value): 88 | name = parse_campaign(campaign) 89 | if name is None: 90 | return None 91 | vlue = f'{int(value)/1000}倍' 92 | return name, vlue 93 | 94 | 95 | async def db_message(sv, serid:str, tense='all', lastday=7): 96 | database_path = regiondic[serid][2] 97 | fmsg=regiondic[serid][4] 98 | if not os.path.exists(database_path): 99 | await updateDB(sv, serid) 100 | db = sqlite3.connect(database_path) 101 | selectcampaign = ''' 102 | SELECT campaign_category, value, start_time, end_time 103 | FROM campaign_schedule 104 | ORDER BY start_time''' 105 | selectevent = ''' 106 | SELECT a.start_time, a.end_time, b.title 107 | FROM hatsune_schedule AS a JOIN event_story_data AS b ON a.event_id = b.value ''' 108 | campaign_data = db.execute(selectcampaign) 109 | event_data = db.execute(selectevent) 110 | cmsg1 = '当前日程\n|区域|倍率|===开始时间===|===结束时间===|' 111 | cmsg2 = '预定日程\n|区域|倍率|===开始时间===|===结束时间===|' 112 | for cdata in campaign_data: 113 | e_time = time.strptime(cdata[3], '%Y/%m/%d %H:%M:%S') 114 | e_time_int = int(time.mktime(e_time)) 115 | s_time = time.strptime(cdata[2], '%Y/%m/%d %H:%M:%S') 116 | s_time_int = int(time.mktime(s_time)) 117 | localtime_int = int(time.time()) 118 | now_data = campaign_logout(cdata[0], cdata[1]) 119 | if now_data is None: 120 | continue 121 | name, value = ','.join(now_data).split(',') 122 | stime = time.strftime('%Y-%m-%d %H:%M:%S', s_time) 123 | etime = time.strftime('%Y-%m-%d %H:%M:%S', e_time) 124 | if localtime_int < e_time_int and localtime_int > s_time_int: 125 | cmsg1 += f'\n|{name}|{value}|{stime}|{etime}|' 126 | if s_time_int > localtime_int: 127 | if lastday != 0: 128 | if s_time_int > localtime_int+(84600*lastday): 129 | continue 130 | cmsg2 += f'\n|{name}|{value}|{stime}|{etime}|' 131 | 132 | emsg1 = '====当前活动=====' 133 | emsg2 = '====预定活动=====' 134 | for edata in event_data: 135 | e_time = time.strptime(edata[1], '%Y/%m/%d %H:%M:%S') 136 | e_time_int = int(time.mktime(e_time)) 137 | s_time = time.strptime(edata[0], '%Y/%m/%d %H:%M:%S') 138 | s_time_int = int(time.mktime(s_time)) 139 | localtime_int = int(time.time()) 140 | title = edata[2] 141 | stime = time.strftime('%Y-%m-%d %H:%M:%S', s_time) 142 | etime = time.strftime('%Y-%m-%d %H:%M:%S', e_time) 143 | if localtime_int < e_time_int and localtime_int > s_time_int: 144 | emsg1 += ('\n{}\n开始时间:{}\n结束时间:{}').format( 145 | title, stime, etime) 146 | if s_time_int > localtime_int: 147 | if lastday == 0: 148 | pass 149 | if lastday != 0: 150 | if s_time_int > localtime_int+(84600*lastday): 151 | continue 152 | emsg2 += ('\n{}\n开始时间:{}\n结束时间:{}').format( 153 | title, stime, etime) 154 | 155 | if tense == 'all': 156 | fmsg += '\n'+cmsg1+'\n'+cmsg2+'\n'+emsg1+'\n'+emsg2 157 | if tense == 'future': 158 | fmsg += '\n'+cmsg2+'\n'+emsg2 159 | if tense == 'now': 160 | fmsg += '\n'+cmsg1+'\n'+emsg1 161 | return fmsg 162 | -------------------------------------------------------------------------------- /hoshino/modules/custom/calendar/campaign.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum, auto 2 | from functools import lru_cache 3 | from typing import Union 4 | 5 | 6 | class PcrdCampaignCategory(IntEnum): 7 | none = 0 8 | halfStaminaNormal = 11 9 | halfStaminaHard = auto() 10 | halfStaminaBoth = auto() 11 | halfStaminaShrine = auto() 12 | halfStaminaTemple = auto() 13 | halfStaminaVeryHard = auto() 14 | 15 | dropRareNormal = 21 16 | dropRareHard = auto() 17 | dropRareBoth = auto() 18 | dropRareVeryHard = auto() 19 | 20 | dropAmountNormal = 31 21 | dropAmountHard = auto() 22 | dropAmountBoth = auto() 23 | dropAmountExploration = auto() 24 | dropAmountDungeon = auto() 25 | dropAmountCoop = auto() 26 | dropAmountShrine = auto() 27 | dropAmountTemple = auto() 28 | dropAmountVeryHard = auto() 29 | 30 | manaNormal = 41 31 | manaHard = auto() 32 | manaBoth = auto() 33 | manaExploration = auto() 34 | manaDungeon = auto() 35 | manaCoop = auto() 36 | manaTemple = 48 37 | manaVeryHard = auto() 38 | 39 | coinDungeon = 51 40 | 41 | cooltimeArena = 61 42 | cooltimeGrandArena = auto() 43 | 44 | masterCoin = 90 45 | masterCoinNormal = auto() 46 | masterCoinHard = auto() 47 | masterCoinVeryHard = auto() 48 | masterCoinShrine = auto() 49 | masterCoinTemple = auto() 50 | masterCoinEventNormal = auto() 51 | masterCoinEventHard = auto() 52 | masterCoinRevivalEventNormal = auto() 53 | masterCoinRevivalEventHard = auto() 54 | 55 | halfStaminaEventNormal = 111 56 | halfStaminaEventHard = auto() 57 | halfStaminaEventBoth = auto() 58 | 59 | dropRareEventNormal = 121 60 | dropRareEventHard = auto() 61 | dropRareEventBoth = auto() 62 | 63 | dropAmountEventNormal = 131 64 | dropAmountEventHard = auto() 65 | dropAmountEventBoth = auto() 66 | 67 | manaEventNormal = 141 68 | manaEventHard = auto() 69 | manaEventBoth = auto() 70 | 71 | expEventNormal = 151 72 | expEventHard = auto() 73 | expEventBoth = auto() 74 | 75 | halfStaminaRevivalEventNormal = 211 76 | halfStaminaRevivalEventHard = auto() 77 | 78 | dropRareRevivalEventNormal = 221 79 | dropRareRevivalEventHard = auto() 80 | 81 | dropAmountRevivalEventNormal = 231 82 | dropAmountRevivalEventHard = auto() 83 | 84 | manaRevivalEventNormal = 241 85 | manaRevivalEventHard = auto() 86 | 87 | expRevivalEventNormal = 251 88 | expRevivalEventHard = auto() 89 | 90 | halfStaminaSideStoryNormal = 311 91 | halfStaminaSideStoryHard = auto() 92 | 93 | dropRareSideStoryNormal = 321 94 | dropRareSideStoryHard = auto() 95 | 96 | dropAmountSideStoryNormal = 331 97 | dropAmountSideStoryHard = auto() 98 | 99 | manaSideStoryNormal = 341 100 | manaSideStoryHard = auto() 101 | 102 | expSideStoryNormal = 351 103 | expSideStoryHard = auto() 104 | 105 | 106 | short_name = { 107 | 'manaDungeon': '地城', 108 | 'masterCoinNormal': '大师', 109 | 'dropAmountNormal': 'N 图', 110 | 'dropAmountHard': 'H 图', 111 | 'dropAmountEventNormal': '活动N图', 112 | 'dropAmountEventHard': '活动H图', 113 | 'dropAmountShrine': '圣迹', 114 | 'dropAmountTemple': '神殿', 115 | 'manaExploration': '探索', 116 | 'dropAmountVeryHard': 'VH图', 117 | } 118 | 119 | 120 | @lru_cache(maxsize=128) 121 | def parse_campaign(cata: int) -> Union[int, None]: 122 | try: 123 | c = PcrdCampaignCategory(cata) 124 | except ValueError: 125 | return None 126 | name = short_name.get(c.name) 127 | return name 128 | -------------------------------------------------------------------------------- /hoshino/modules/custom/chooseone.py: -------------------------------------------------------------------------------- 1 | import random 2 | from hoshino import Service 3 | 4 | sv = Service('chooseone') 5 | 6 | 7 | @sv.on_message('group',can_private=1) 8 | async def chooseone(bot, ctx): 9 | message = ctx['raw_message'] 10 | if message.startswith('选择卡池'): 11 | return 12 | if message.startswith('选择'): 13 | msg = message[2:].split('还是') 14 | if len(msg) == 1: 15 | return 16 | choices=list(filter(lambda x:len(x)!=0,msg)) 17 | if not choices: 18 | await bot.send(ctx,'选项不能全为空!',at_sender=True) 19 | return 20 | msgs=['您的选项是:'] 21 | idchoices=list(f'{i+1}. {choice}' for i,choice in enumerate(choices)) 22 | msgs.extend(idchoices) 23 | if random.randrange(1000)<=76: 24 | msgs.append('建议您选择: “我全都要”') 25 | else: 26 | final=random.choice(choices) 27 | msgs.append(f'建议您选择: {final}') 28 | await bot.send(ctx,'\n'.join(msgs),at_sender=True) -------------------------------------------------------------------------------- /hoshino/modules/custom/echo.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service,Privilege as Priv,sucmd 2 | from nonebot.message import unescape 3 | sv=Service('say',visible=False,manage_priv=Priv.SUPERUSER,enable_on_default=False) 4 | 5 | @sv.on_command('say') 6 | async def say(session): 7 | msg=session.current_arg 8 | try: 9 | await session.send(unescape(msg)) 10 | except Exception as e: 11 | sv.logger.error(type(e)) 12 | session.finish('调取say失败,请稍后再试') 13 | @sucmd('echo',False) 14 | async def echo(session): 15 | msg=session.current_arg 16 | try: 17 | await session.send(unescape(msg)) 18 | except Exception as e: 19 | sv.logger.error(type(e)) 20 | session.finish('调取echo失败,请稍后再试') -------------------------------------------------------------------------------- /hoshino/modules/custom/gift.py: -------------------------------------------------------------------------------- 1 | from hoshino import sucmd 2 | from random import randint 3 | @sucmd('gift',aliases=('礼物','送礼'),force_private=False) 4 | async def gift(session): 5 | msg=session.ctx['message'] 6 | for m in msg: 7 | if m.type == 'at' and m.data['qq'] != 'all' : 8 | qq = m.data['qq'] 9 | break 10 | session.finish(f'[CQ:gift,qq={qq},id={randint(0,13)}]') -------------------------------------------------------------------------------- /hoshino/modules/custom/gorestart.py: -------------------------------------------------------------------------------- 1 | from hoshino import sucmd,aiorequests 2 | import json 3 | @sucmd('restart',aliases=('重启go','重启gocq'),force_private=False) 4 | async def gorestart(session): 5 | access_token=session.bot.config.ACCESS_TOKEN 6 | go_port=session.bot.config.GO_CQHTTP_WEBPORT 7 | try: 8 | res=await aiorequests.post(f'http://127.0.0.1:{go_port}/admin/do_process_restart?access_token={access_token}') 9 | if res.status_code !=200: 10 | resj=await res.json() 11 | session.finish(f'重启go-cqhttp失败,请前往服务器查看,出错如下:\n{json.dumps(resj)}') 12 | except Exception as e: 13 | session.finish(f'重启go-cqhttp失败,请前往服务器查看,出错如下:\n{e}') -------------------------------------------------------------------------------- /hoshino/modules/custom/high_eq.py: -------------------------------------------------------------------------------- 1 | from PIL import ImageFont, ImageDraw, Image 2 | from hoshino import Service,R,Privilege as Priv 3 | from hoshino.util import pic2b64 4 | sv=Service('high_eq',visible=False,manage_priv=Priv.SUPERUSER,enable_on_default=False) 5 | fontpath=R.img('priconne/gadget/FZY3K.TTF').path 6 | path = R.img('high_eq_image.png').path 7 | def draw_text(img_pil, text, offset_x): 8 | draw = ImageDraw.Draw(img_pil) 9 | font = ImageFont.truetype(fontpath, 48) 10 | width, height = draw.textsize(text, font) 11 | x = 5 12 | if width > 390: 13 | font = ImageFont.truetype(fontpath, int(390 * 48 / width)) 14 | width, height = draw.textsize(text, font) 15 | else: 16 | x = int((400 - width) / 2) 17 | draw.rectangle((x + offset_x - 2, 360, x + 2 + width + offset_x, 360 + height * 1.2), fill=(0, 0, 0, 255)) 18 | draw.text((x + offset_x, 360), text, font=font, fill=(255, 255, 255, 255)) 19 | @sv.on_rex(r'低情商(.+)高情商(.+)', normalize=True,can_private=1) 20 | async def high_eq(bot,ctx,match): 21 | left = match.group(1).strip().strip(":").strip("。") 22 | right = match.group(2).strip().strip(":").strip("。") 23 | if len(left) > 15 or len(right) > 15: 24 | await bot.send(ctx,"为了图片质量,请不要多于15个字符") 25 | return 26 | img_p = Image.open(path) 27 | draw_text(img_p, left, 0) 28 | draw_text(img_p, right, 400) 29 | await bot.send(ctx,f'[CQ:image,file={pic2b64(img_p)}]') 30 | @sv.on_rex(r'高情商(.+)低情商(.+)', normalize=True,can_private=1) 31 | async def high_eq(bot,ctx,match): 32 | left = match.group(2).strip().strip(":").strip("。") 33 | right = match.group(1).strip().strip(":").strip("。") 34 | if len(left) > 15 or len(right) > 15: 35 | await bot.send(ctx,"为了图片质量,请不要多于15个字符") 36 | return 37 | img_p = Image.open(path) 38 | draw_text(img_p, left, 0) 39 | draw_text(img_p, right, 400) 40 | await bot.send(ctx,f'[CQ:image,file={pic2b64(img_p)}]') 41 | -------------------------------------------------------------------------------- /hoshino/modules/custom/longwang.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | import random 4 | import os 5 | 6 | from hoshino import Service,aiorequests,R 7 | 8 | sv = Service('longwang',enable_on_default=False,visible=False) 9 | 10 | @sv.on_command('迫害龙王') 11 | async def longwang(session): 12 | gid = session.ctx['group_id'] 13 | dragon_king=await session.bot.get_group_honor_info(group_id=gid,type='talkative') 14 | dragon_king=dragon_king['current_talkative']['user_id'] 15 | longwanglist=list() 16 | longwangmelist=list() 17 | for lw in os.listdir(R.img('longwang/').path): 18 | if lw.startswith('longwangme'): 19 | longwangmelist.append(lw) 20 | else: 21 | longwanglist.append(lw) 22 | longwangme=R.img('longwang/',random.choice(longwangmelist)).cqcode 23 | longwangs=[R.img('longwang/',x).cqcode for x in longwanglist] 24 | longwangs.extend(['龙王出来挨透','龙王出来喷水']) 25 | if dragon_king==session.ctx['self_id']: 26 | session.finish(f'{longwangme}') 27 | reply=random.choice(longwangs) 28 | session.finish(f'[CQ:at,qq={dragon_king}]\n{reply}') 29 | -------------------------------------------------------------------------------- /hoshino/modules/custom/nbnhhsh.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service 2 | import aiohttp 3 | 4 | sv = Service('nbnhhsh') 5 | 6 | 7 | @sv.on_rex(r'^[\?\?]{1,2} ?([a-z0-9]+)$', normalize=True, event='group',can_private=1) 8 | async def hhsh(bot, ctx, match): 9 | async with aiohttp.TCPConnector(verify_ssl=False) as connector: 10 | async with aiohttp.request( 11 | 'POST', 12 | url='https://lab.magiconch.com/api/nbnhhsh/guess', 13 | json={"text": match.group(1)}, 14 | connector=connector, 15 | ) as resp: 16 | j = await resp.json() 17 | if len(j) == 0: 18 | await bot.send(ctx, f'{match.group(1)}: 没有结果') 19 | return 20 | res = j[0] 21 | name=res.get('name') 22 | trans=res.get('trans',['没有结果']) 23 | msg = '{}: {}'.format( 24 | name, 25 | ' '.join(trans), 26 | ) 27 | await bot.send(ctx,msg) 28 | -------------------------------------------------------------------------------- /hoshino/modules/custom/nodolls.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service,Privilege as Priv 2 | 3 | 4 | sv=Service('nodolls',visible=False,enable_on_default=False,manage_priv=Priv.SUPERUSER) 5 | 6 | 7 | def dddolls(s: str): 8 | l = len(s) 9 | if l>=15: 10 | return 11 | for i in range(l//2, 1, -1): 12 | if s[:i] == s[i:i+i]: 13 | return ''.join([s[:i],s]) 14 | return 15 | @sv.on_message('group') 16 | async def nodolls(bot, ctx): 17 | message = ctx['raw_message'] 18 | res=dddolls(message) 19 | if res: 20 | await bot.send(ctx,res) 21 | -------------------------------------------------------------------------------- /hoshino/modules/custom/ocr.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service,Privilege as Priv 2 | sv=Service('ocr',visible=False,enable_on_default=False,manage_priv=Priv.SUPERUSER) 3 | @sv.on_command('ocr',aliases=('识字','文字识别'),can_private=1) 4 | async def ooocr(session): 5 | msg=session.ctx['message'] 6 | imglist=[ 7 | s.data['file'] 8 | for s in msg 9 | if s.type == 'image' and 'file' in s.data 10 | ] 11 | if not imglist: 12 | return 13 | for count,i in enumerate(imglist): 14 | try: 15 | res=await session.bot.ocr_image(image=i) 16 | except: 17 | session.finish('请求ocrAPI失败') 18 | reply=[f'第{count+1}张图片的ocr结果是:'] 19 | texts=res['texts'] 20 | for t in texts: 21 | reply.append(t['text']) 22 | await session.send('/'.join(reply),at_sender=True) 23 | -------------------------------------------------------------------------------- /hoshino/modules/custom/online_notice.py: -------------------------------------------------------------------------------- 1 | import nonebot 2 | bot= nonebot.get_bot() 3 | 4 | #@bot.on_meta_event("heartbeat") 5 | async def heartnotice(ev): 6 | await bot.send_private_msg( user_id=bot.config.SUPERUSERS[0], message='心跳包~') 7 | 8 | @bot.on_meta_event('lifecycle.connect') 9 | async def onlinenotice(ev): 10 | await bot.send_private_msg( user_id=bot.config.SUPERUSERS[0], message='生命周期上线~') 11 | 12 | -------------------------------------------------------------------------------- /hoshino/modules/custom/poke.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service,Privilege as Priv 2 | sv=Service('poke',visible=False,enable_on_default=False,manage_priv=Priv.SUPERUSER) 3 | @sv.on_notice('notify.poke') 4 | async def poke(session): 5 | uid=session.event.user_id 6 | tid=session.event.target_id 7 | gid=session.event.group_id 8 | if tid==session.event.self_id: 9 | await session.bot.send_group_msg(message=f'[CQ:poke,qq={uid}]',group_id=gid) 10 | else: 11 | return -------------------------------------------------------------------------------- /hoshino/modules/custom/query.py: -------------------------------------------------------------------------------- 1 | from hoshino import R, CommandSession, util, Service,sucmd,aiorequests 2 | import numpy as np 3 | sv = Service('query') 4 | p1 = R.img('priconne/quick/tqian.png') 5 | p2 = R.img('priconne/quick/tzhong.png') 6 | p3 = R.img('priconne/quick/thou.png') 7 | p4 = R.img('priconne/quick/bqian.png') 8 | p5 = R.img('priconne/quick/bzhong.png') 9 | p6 = R.img('priconne/quick/bhou.png') 10 | p7 = R.img('priconne/quick/rqian.png') 11 | p8 = R.img('priconne/quick/rzhong.png') 12 | p9 = R.img('priconne/quick/rhou.png') 13 | brank=(p4,p5,p6) 14 | trank=(p1,p2,p3) 15 | rrank=(p7,p8,p9) 16 | posdic={"前":0,"中":1,"后":2} 17 | serdic={'b':brank,'国':brank,'台':trank,'日':rrank,'t':trank,'j':rrank} 18 | 19 | async def send_rank(bot, ctx,ser,pos): 20 | msg=['Rank表仅供参考,以公会要求为准','不定期更新,来源见图'] 21 | poslist=set([posdic[i] for i in pos]) if pos else [0,1,2] 22 | serlist=set([serdic[i] for i in ser]) 23 | for s in serlist: 24 | msg.extend([f'{s[p].cqcode}' for p in poslist]) 25 | await bot.send(ctx, '图片较大,请稍等片刻') 26 | await bot.send(ctx,'\n'.join(msg)) 27 | 28 | 29 | @sv.on_rex(r'^([台国日btj]{1,3})服?([前中后]{0,3})rank表?', normalize=True, event='group',can_private=1) 30 | async def rank_sheet(bot, ctx, match): 31 | await send_rank(bot,ctx,match.group(1),match.group(2)) 32 | 33 | 34 | 35 | 36 | this_season = np.zeros(15001, dtype=int) 37 | all_season = np.zeros(15001, dtype=int) 38 | 39 | this_season[1:11] = 50 40 | this_season[11:101] = 10 41 | this_season[101:201] = 5 42 | this_season[201:501] = 3 43 | this_season[501:1001] = 2 44 | this_season[1001:2001] = 2 45 | this_season[2001:4000] = 1 46 | this_season[4000:8000:100] = 50 47 | this_season[8100:15001:100] = 15 48 | 49 | all_season[1:11] = 500 50 | all_season[11:101] = 50 51 | all_season[101:201] = 30 52 | all_season[201:501] = 10 53 | all_season[501:1001] = 5 54 | all_season[1001:2001] = 3 55 | all_season[2001:4001] = 2 56 | all_season[4001:7999] = 1 57 | all_season[8100:15001:100] = 30 58 | 59 | @sv.on_command('挖矿计算', aliases=('挖矿', 'jjc钻石', '竞技场钻石', 'jjc钻石查询', '竞技场钻石查询'),can_private=1) 60 | async def arena_miner(session: CommandSession): 61 | try: 62 | rank = int(session.current_arg_text) 63 | except: 64 | return 65 | rank = np.clip(rank, 1, 15001) 66 | s_all = all_season[1:rank].sum() 67 | s_this = this_season[1:rank].sum() 68 | msg = f"\n最高排名奖励还剩{s_this}钻\n历届最高排名还剩{s_all}钻" 69 | session.finish(msg,at_sender=1) 70 | 71 | 72 | yukari_pic = R.img('priconne/quick/yukari.jpg').cqcode 73 | YUKARI_SHEET = f''' 74 | {yukari_pic} 75 | ※大圈是1动充电对象 PvP测试 76 | ※黄骑四号位例外较多 77 | ※对面羊驼或中后卫坦 有可能歪 78 | ※我方羊驼算一号位''' 79 | @sv.on_command('yukari_charge', aliases=('黄骑充电', '黄骑充电表', '酒鬼充电', '酒鬼充电表'),can_private=1) 80 | async def yukari(session: CommandSession): 81 | await session.send('图片较大,请稍等片刻',) 82 | await session.send(YUKARI_SHEET, at_sender=True) 83 | await util.silence(session.ctx, 60) 84 | 85 | 86 | @sv.on_command('star', aliases=('星级表', '升星表'),can_private=1) 87 | async def star(session: CommandSession): 88 | await session.send(R.img('priconne/quick/star.jpg').cqcode, at_sender=True) 89 | await util.silence(session.ctx, 60) 90 | 91 | byk=R.img('priconne/quick/banyuekan.jpg') 92 | @sv.on_command('半月刊',aliases=('活动半月刊','b服半月刊','国服半月刊'),can_private=1) 93 | async def banyuekan(session): 94 | await session.send('图片较大,请稍等片刻') 95 | await session.send(f'{byk.cqcode}', at_sender=True) 96 | await util.silence(session.ctx, 60) 97 | 98 | -------------------------------------------------------------------------------- /hoshino/modules/custom/rss/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import List 3 | from nonebot.argparse import ArgumentParser 4 | from nonebot import CommandSession 5 | from PIL import Image, ImageFont 6 | from .data import Rss, Rssdata, BASE_URL 7 | from hoshino import Service, aiohttpx, R,sucmd 8 | from hoshino.util import pic2b64, get_text_size, text2pic,CQimage,text2CQ 9 | sv = Service('rss', 10 | enable_on_default=False, visible=False) 11 | fontpath1 = R.img('priconne/gadget/simsun.ttc').path 12 | fontpath2 = R.img('priconne/gadget/simhei.ttf').path 13 | font1 = ImageFont.truetype(fontpath1, 18) 14 | font2 = ImageFont.truetype(fontpath2, 22) 15 | font3 = ImageFont.truetype(fontpath2, 20) 16 | 17 | 18 | def info2pic(info: dict) -> str: 19 | title = f"标题: {info['标题']}\n" 20 | text = f"正文:\n{info['正文']}\n时间: {info['时间']}" 21 | titlesize = get_text_size(title, font2,(20,20,20,0)) 22 | textsize = get_text_size(text, font1,(20,20,10,10)) 23 | sumsize = max(titlesize[0], textsize[0]), titlesize[1]+textsize[1] 24 | base = Image.new('RGBA', sumsize, (255, 255, 255, 255)) 25 | titlepic = text2pic(title, font2,(20,20,20,0)) 26 | base.paste(titlepic, (0, 0)) 27 | textpic = text2pic(text, font1,(20,20,10,20)) 28 | base.paste(textpic, (0, titlesize[1])) 29 | return pic2b64(base) 30 | 31 | 32 | def infos2pic(infos: List[dict]) -> str: 33 | texts = [] 34 | for info in infos: 35 | text = f"标题: {info['标题']}\n时间: {info['时间']}\n======" 36 | texts.append(text) 37 | texts = '\n'.join(texts) 38 | return text2CQ(texts,font3) 39 | 40 | 41 | @sv.on_command('添加订阅', aliases=('addrss', '增加订阅'), shell_like=True) 42 | async def addrss(session: CommandSession): 43 | parser = ArgumentParser(session=session) 44 | parser.add_argument('name') 45 | parser.add_argument('url') 46 | parser.add_argument('-r', '--rsshub', action='store_true') 47 | args = parser.parse_args(session.argv) 48 | name = args.name 49 | url = BASE_URL+args.url if args.rsshub else args.url 50 | try: 51 | stats = await aiohttpx.head(url, timeout=5, allow_redirects=True) 52 | except Exception as e: 53 | sv.logger.exception(e) 54 | sv.logger.error(type(e)) 55 | session.finish('请求路由失败,请稍后再试') 56 | if stats.status_code != 200: 57 | session.finish('请求路由失败,请检查路由状态') 58 | rss = Rss(url) 59 | if not await rss.has_entries: 60 | session.finish('暂不支持该RSS') 61 | try: 62 | Rssdata.replace(url=rss.url, name=name, group=session.event.group_id, date=await rss.last_update).execute() 63 | except Exception as e: 64 | sv.logger.exception(e) 65 | sv.logger.error(type(e)) 66 | session.finish('添加订阅失败') 67 | session.finish(f'添加订阅{name}成功') 68 | 69 | 70 | @sv.on_command('删除订阅', aliases=('delrss', '取消订阅')) 71 | async def delrss(session: CommandSession): 72 | try: 73 | name = session.current_arg_text.strip() 74 | except: 75 | return 76 | 77 | try: 78 | Rssdata.delete().where(Rssdata.name == name, Rssdata.group == 79 | session.event.group_id).execute() 80 | except Exception as e: 81 | sv.logger.exception(e) 82 | sv.logger.error(type(e)) 83 | session.finish('删除订阅失败') 84 | session.finish(f'删除订阅{name}成功') 85 | 86 | 87 | @sv.scheduled_job('interval', minutes=3, jitter=20) 88 | async def push_rss(): 89 | bot = sv.bot 90 | glist = await sv.get_enable_groups() 91 | for gid in glist.keys(): 92 | res = Rssdata.select(Rssdata.url, Rssdata.name, 93 | Rssdata.date).where(Rssdata.group == gid) 94 | for r in res: 95 | rss = Rss(r.url) 96 | if not (await rss.has_entries): 97 | continue 98 | if (lstdate := await rss.last_update) != r.date: 99 | try: 100 | await asyncio.sleep(0.5) 101 | newinfo = await rss.get_new_entry_info() 102 | msg = [f'订阅 {r.name} 更新啦!'] 103 | msg.append(CQimage(info2pic(newinfo))) 104 | msg.append(f'链接: {newinfo["链接"]}') 105 | Rssdata.update(date=lstdate).where( 106 | Rssdata.group == gid, Rssdata.name == r.name, Rssdata.url == r.url).execute() 107 | await bot.send_group_msg(message='\n'.join(msg), group_id=gid) 108 | except Exception as e: 109 | sv.logger.exception(e) 110 | sv.logger.error(f'{type(e)} occured when pushing rss') 111 | 112 | 113 | @sv.on_command('订阅列表', aliases=('查看本群订阅')) 114 | async def lookrsslist(session: CommandSession): 115 | try: 116 | res = Rssdata.select(Rssdata.url, Rssdata.name).where(Rssdata.group == 117 | session.event.group_id) 118 | msg = ['本群订阅如下:'] 119 | for r in res: 120 | msg.append(f'订阅标题:{r.name}\n订阅链接:{await Rss(r.url).link}\n=====') 121 | except Exception as e: 122 | sv.logger.exception(e) 123 | sv.logger.error(type(e)) 124 | session.finish('查询订阅列表失败') 125 | session.finish('\n'.join(msg)) 126 | 127 | 128 | @sv.on_command('看订阅', aliases=('查订阅', '查看订阅'), shell_like=True) 129 | async def lookrss(session: CommandSession): 130 | parser = ArgumentParser(session=session) 131 | parser.add_argument('name') 132 | parser.add_argument('-l', '--limit', default=5, type=int) 133 | args = parser.parse_args(session.argv) 134 | limit = args.limit 135 | name = args.name 136 | try: 137 | res = Rssdata.select(Rssdata.url).where(Rssdata.name == name, Rssdata.group == 138 | session.event.group_id) 139 | r = res[0] 140 | rss = Rss(r.url, limit) 141 | infos = await rss.get_all_entry_info() 142 | except Exception as e: 143 | sv.logger.exception(e) 144 | sv.logger.error(type(e)) 145 | session.finish(f'查订阅{name}失败') 146 | msg = [f'{name}的最近记录:'] 147 | msg.append(infos2pic(infos)) 148 | msg.append('详情可看: '+await rss.link) 149 | session.finish('\n'.join(msg)) 150 | @sucmd('testrss',0,shell_like=True) 151 | async def testrss(session: CommandSession): 152 | parser = ArgumentParser(session=session) 153 | parser.add_argument('name') 154 | args = parser.parse_args(session.argv) 155 | name = args.name 156 | try: 157 | res = Rssdata.select(Rssdata.url).where(Rssdata.name == name, Rssdata.group == 158 | session.event.group_id) 159 | r = res[0] 160 | rss = Rss(r.url) 161 | newinfo = await rss.get_new_entry_info() 162 | except Exception as e: 163 | sv.logger.exception(e) 164 | sv.logger.error(type(e)) 165 | session.finish(f'test {name}失败') 166 | msg = [f'订阅 {name} TEST!'] 167 | msg.append(CQimage(info2pic(newinfo))) 168 | msg.append(f'链接: {newinfo["链接"]}') 169 | session.finish('\n'.join(msg)) 170 | -------------------------------------------------------------------------------- /hoshino/modules/custom/rss/data.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from datetime import datetime, timedelta 4 | from typing import List, Dict, Optional 5 | import feedparser 6 | from feedparser import FeedParserDict 7 | import peewee as pw 8 | from bs4 import BeautifulSoup 9 | from hoshino.aiohttpx import get 10 | BASE_URL = "https://rsshub.akiraxie.me/" 11 | 12 | 13 | class Rss: 14 | def __init__(self, url: str, limit: int = 1) -> None: 15 | super().__init__() 16 | self.url = url 17 | self.limit = limit 18 | 19 | @property 20 | async def feed(self) -> FeedParserDict: 21 | ret = await get(self.url, params={'limit': self.limit,'timeout':5}) 22 | return feedparser.parse(ret.content) 23 | 24 | @property 25 | async def feed_entries(self) -> Optional[List]: 26 | feed = await self.feed 27 | if len(feed.entries) != 0: 28 | return feed.entries 29 | else: 30 | return 31 | @property 32 | async def link(self)->str: 33 | feed=await self.feed 34 | return feed.feed.link 35 | @property 36 | async def has_entries(self) -> bool: 37 | return (await self.feed_entries) is not None 38 | 39 | @staticmethod 40 | def format_time(timestr: str) -> str: 41 | try: 42 | struct_time = time.strptime(timestr, '%a, %d %b %Y %H:%M:%S %Z') 43 | except: 44 | struct_time = time.strptime(timestr, '%Y-%m-%dT%H:%M:%SZ') 45 | dt = datetime.fromtimestamp(time.mktime(struct_time)) 46 | return str(dt+timedelta(hours=8)) 47 | 48 | @staticmethod 49 | def _get_rssdic(entry: FeedParserDict,flag:bool=False) -> Dict: 50 | ret = {'标题': entry.title, 51 | '时间': entry.updated, 52 | '链接': entry.link, } 53 | try: 54 | ret['时间'] = Rss.format_time(ret['时间']) 55 | except: 56 | pass 57 | if flag: 58 | ret['正文']=BeautifulSoup(entry.summary,"lxml").get_text() 59 | return ret 60 | 61 | async def get_new_entry_info(self) -> Optional[Dict]: 62 | try: 63 | entries = await self.feed_entries 64 | return Rss._get_rssdic(entries[0],True) 65 | except: 66 | return None 67 | 68 | async def get_all_entry_info(self) -> Optional[List[Dict]]: 69 | try: 70 | ret = [] 71 | entries = await self.feed_entries 72 | lmt = min(self.limit,len(entries)) 73 | for entry in entries[:lmt]: 74 | entrydic = self._get_rssdic(entry) 75 | ret.append(entrydic) 76 | return ret 77 | except: 78 | return None 79 | 80 | @property 81 | async def last_update(self) -> Optional[str]: 82 | try: 83 | return (await self.get_new_entry_info())['时间'] 84 | except: 85 | return None 86 | 87 | 88 | db = pw.SqliteDatabase( 89 | os.path.join(os.path.dirname(__file__), 'rssdata.db') 90 | ) 91 | 92 | 93 | class Rssdata(pw.Model): 94 | url = pw.TextField() 95 | name = pw.TextField() 96 | date = pw.TextField() 97 | group = pw.IntegerField() 98 | 99 | class Meta: 100 | database = db 101 | primary_key = pw.CompositeKey('url', 'group') 102 | 103 | 104 | def init(): 105 | if not os.path.exists(os.path.join(os.path.dirname(__file__), 'rssdata.db')): 106 | db.connect() 107 | db.create_tables([Rssdata]) 108 | db.close() 109 | 110 | 111 | init() 112 | -------------------------------------------------------------------------------- /hoshino/modules/custom/search_saucenao.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service,Privilege as Priv,MessageSegment 2 | from saucenao_api import SauceNao 3 | 4 | sv = Service('saucenao',visible=False,manage_priv=Priv.SUPERUSER,enable_on_default=False) 5 | 6 | 7 | @sv.on_command('搜索图片',aliases=('识图','搜图'),can_private=1) 8 | async def searchpic(session): 9 | piclist=session.current_arg_images 10 | if len(piclist)==0: 11 | session.finish('未识别到图片,请发送图片以识别',at_sender=True) 12 | sauce=SauceNao() 13 | for i in piclist: 14 | count=1 15 | reply=[] 16 | res=sauce.from_url(i) 17 | if not res: 18 | await session.send(f'第{count}张图识别失败,请稍后再试',at_sender=True) 19 | continue 20 | best=res[0] 21 | reply.append('相似度:'+str(best.similarity)+'%') 22 | reply.append('标题:「'+best.title+'」') 23 | reply.append('作者:「'+best.author+'」') 24 | reply.append(f'[CQ:image,cache=0,file={best.thumbnail}]') 25 | reply.append('图片地址:'+best.url) 26 | await session.send('\n'.join(reply)) 27 | count=count+1 -------------------------------------------------------------------------------- /hoshino/modules/custom/setblock.py: -------------------------------------------------------------------------------- 1 | from hoshino import sucmd,Service 2 | from datetime import timedelta 3 | @sucmd('setblock',aliases=('拉黑','屏蔽'),force_private=False) 4 | async def setblock(session): 5 | ctx=session.ctx 6 | try: 7 | timelen=5 if not session.current_arg_text else int(session.current_arg_text) 8 | except: 9 | return 10 | count=0 11 | for m in ctx['message']: 12 | if m.type == 'at' and m.data['qq'] != 'all' : 13 | uid = int(m.data['qq']) 14 | Service.set_block_user(uid,timedelta(minutes=timelen)) 15 | count+=1 16 | session.finish(f'已拉黑{count}人{timelen}分钟,嘿嘿嘿~') -------------------------------------------------------------------------------- /hoshino/modules/custom/shanghao.py: -------------------------------------------------------------------------------- 1 | import string 2 | import random 3 | from hoshino import Service,Privilege as Priv, aiorequests 4 | sv=Service('wangyiyun',visible=False,manage_priv=Priv.SUPERUSER,enable_on_default=False) 5 | @sv.on_command('上号',aliases=('网抑云',"网易云","生而为人"),can_private=1) 6 | async def shanghao(session): 7 | format_string=''.join(random.sample(string.ascii_letters + string.digits, 16)) 8 | try: 9 | resp=await aiorequests.post(f'https://nd.2890.ltd/api/?format={format_string}') 10 | j =await resp.json() 11 | except Exception as e: 12 | sv.logger.exception(e) 13 | sv.logger.error(type(e)) 14 | session.finish("调取api失败,请稍后再试") 15 | try: 16 | if j['status'] !=1: 17 | session.finish("请求api失败") 18 | content=j['data']['content']['content'] 19 | except Exception as e: 20 | sv.logger.exception(e) 21 | sv.logger.error(type(e)) 22 | session.finish('调取api失败') 23 | await session.finish(content,at_sender=True) -------------------------------------------------------------------------------- /hoshino/modules/custom/throwandcreep.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service,Privilege as Priv,aiorequests,MessageSegment,R 2 | from hoshino.util import pic2b64 3 | from PIL import Image, ImageDraw 4 | import random 5 | from io import BytesIO 6 | import os 7 | sv=Service('throwandcreep',enable_on_default=False,visible=False,manage_priv=Priv.SUPERUSER) 8 | base_path=R.img(f'throwandcreep/').path 9 | os.makedirs(os.path.join(base_path,'pa'),exist_ok=True) 10 | 11 | 12 | def get_circle_avatar(avatar, size): 13 | avatar.thumbnail((size, size)) 14 | scale = 5 15 | mask = Image.new('L', (size*scale, size*scale), 0) 16 | draw = ImageDraw.Draw(mask) 17 | draw.ellipse((0, 0, size * scale, size * scale), fill=255) 18 | mask = mask.resize((size, size), Image.ANTIALIAS) 19 | ret_img = avatar.copy() 20 | ret_img.putalpha(mask) 21 | return ret_img 22 | 23 | async def throw(qq): 24 | avatar_url=f'http://q1.qlogo.cn/g?b=qq&nk={qq}&s=160' 25 | imgres=await aiorequests.get(avatar_url) 26 | if not imgres: 27 | return -1 28 | img=await imgres.content 29 | avatar = Image.open(BytesIO(img)).convert('RGBA') 30 | avatar = get_circle_avatar(avatar, 139) 31 | randomangle=random.randrange(360) 32 | throw_img=Image.open(os.path.join(base_path,'throw.jpg')) 33 | throw_img.paste(avatar.rotate(randomangle),(17, 180),avatar.rotate(randomangle)) 34 | throw_img=pic2b64(throw_img) 35 | throw_img=str(MessageSegment.image(throw_img)) 36 | return throw_img 37 | 38 | async def creep(qq): 39 | cid = random.randint(0, 53) 40 | avatar_url=f'http://q1.qlogo.cn/g?b=qq&nk={qq}&s=160' 41 | imgres=await aiorequests.get(avatar_url) 42 | if not imgres: 43 | return -1 44 | img=await imgres.content 45 | avatar = Image.open(BytesIO(img)).convert('RGBA') 46 | avatar = get_circle_avatar(avatar, 100) 47 | creep_img = Image.open(os.path.join(base_path,'pa',f'爬{cid}.jpg')).convert('RGBA') 48 | creep_img = creep_img.resize((500, 500), Image.ANTIALIAS) 49 | creep_img.paste(avatar, (0, 400, 100, 500), avatar) 50 | creep_img=pic2b64(creep_img) 51 | creep_img=str(MessageSegment.image(creep_img)) 52 | return creep_img 53 | 54 | @sv.on_keyword(('丟','dio')) 55 | async def diu(bot,ctx): 56 | qq='' 57 | msg=ctx['message'] 58 | for segment in msg: 59 | if segment['type'] == 'at': 60 | qq= segment['data']['qq'] 61 | break 62 | if qq == '' or qq== 'all': 63 | return 64 | reply=await throw(qq) 65 | if isinstance(reply,str): 66 | await bot.send(ctx,f'{reply}') 67 | else: 68 | await bot.send(ctx,'丢丢失败,请稍后再试') 69 | @sv.on_keyword(('爬','爪巴')) 70 | async def pa(bot,ctx): 71 | qq='' 72 | msg=ctx['message'] 73 | for segment in msg: 74 | if segment['type'] == 'at': 75 | qq= segment['data']['qq'] 76 | break 77 | if qq == '' or qq== 'all': 78 | return 79 | reply=await creep(qq) 80 | if isinstance(reply,str): 81 | await bot.send(ctx,f'{reply}') 82 | else: 83 | await bot.send(ctx,'爬爬失败,请稍后再试') -------------------------------------------------------------------------------- /hoshino/modules/custom/treat.py: -------------------------------------------------------------------------------- 1 | 2 | import random 3 | from hoshino import R, CommandSession, Service 4 | 5 | sv1 = Service('justplay') 6 | nothing = R.img('priconne/quick/我什么都没有.png').cqcode 7 | prof = R.img('priconne/quick/专业团队.png').cqcode 8 | resp = R.img('priconne/quick/respect.jpg').cqcode 9 | longtree = R.img('priconne/quick/大树.jpg').cqcode 10 | shorttree = R.img('priconne/quick/小树.jpg').cqcode 11 | multisp = R.img('priconne/quick/多人运动.jpg').cqcode 12 | 13 | 14 | @sv1.on_keyword(('我什么都没有')) 15 | async def havenothing(bot, ctx): 16 | a = random.randint(1, 100) 17 | if a <= 10: 18 | await bot.send(ctx, f'{nothing}') 19 | 20 | 21 | @sv1.on_command('professional', aliases=('专业团队', '黑人抬棺'), only_to_me=False) 22 | async def profe(session: CommandSession): 23 | a = random.choice([prof, resp]) 24 | await session.send(a) 25 | 26 | 27 | @sv1.on_command('duorenyundong', aliases=('多人运动', '三人忘刀'), only_to_me=False) 28 | async def multisport(session: CommandSession): 29 | await session.send(f'{multisp}') 30 | 31 | 32 | @sv1.on_keyword(('挂树', '查树')) 33 | async def tree(bot, ctx): 34 | a = random.randint(1, 1000) 35 | if a <= 10: 36 | await bot.send(ctx, f'{longtree}') 37 | elif a <= 110: 38 | await bot.send(ctx, f'{shorttree}') 39 | 40 | 41 | @sv1.on_command('huhuhu', aliases=('呼呼呼')) 42 | async def huhuhu(session: CommandSession): 43 | n = random.randint(1, 5) 44 | huhu = R.img('priconne/quick/huhuhu{}.jpg'.format(n)).cqcode 45 | await session.send(huhu) 46 | 47 | 48 | @sv1.on_keyword(('我服了')) 49 | async def fule(bot, ctx): 50 | a = random.randint(1, 100) 51 | if a <= 15: 52 | await bot.send(ctx, R.img('priconne/quick/我服了.jpg').cqcode) 53 | -------------------------------------------------------------------------------- /hoshino/modules/deepchat/deepchat.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from hoshino import aiorequests 4 | from nonebot import NoneBot 5 | from hoshino import util 6 | from hoshino.service import Service, Privilege 7 | 8 | sv = Service('deepchat', manage_priv=Privilege.SUPERUSER, enable_on_default=False, visible=False) 9 | 10 | api = util.load_config(__file__)['deepchat_api'] 11 | 12 | @sv.on_message('group') 13 | async def deepchat(bot:NoneBot, ctx): 14 | msg = ctx['message'].extract_plain_text() 15 | if not msg or random.random() > 0.025: 16 | return 17 | payload = { 18 | "msg": msg, 19 | "group": ctx['group_id'], 20 | "qq": ctx['user_id'] 21 | } 22 | sv.logger.info(payload) 23 | rsp = await aiorequests.post(api, data=payload) 24 | j = await rsp.json() 25 | sv.logger.info(j) 26 | if j['msg']: 27 | await bot.send(ctx, j['msg']) 28 | -------------------------------------------------------------------------------- /hoshino/modules/dice/dice.py: -------------------------------------------------------------------------------- 1 | import re 2 | import random 3 | 4 | from hoshino.service import Service 5 | 6 | sv = Service('dice') 7 | 8 | async def do_dice(bot, ctx, num, min_, max_, opr, offset, TIP="的掷骰结果是:"): 9 | if num == 0: 10 | await bot.send(ctx, '咦?我骰子呢?') 11 | return 12 | min_, max_ = min(min_, max_), max(min_, max_) 13 | rolls = list(map(lambda _: random.randint(min_, max_), range(num))) 14 | sum_ = sum(rolls) 15 | rolls_str = '+'.join(map(lambda x: str(x), rolls)) 16 | if len(rolls_str) > 100: 17 | rolls_str = str(sum_) 18 | res = sum_ + opr * offset 19 | msg = [ 20 | f'{TIP}\n', str(num) if num > 1 else '', 'D', 21 | f'{min_}~' if min_ != 1 else '', str(max_), 22 | (' +-'[opr] + str(offset)) if offset else '', 23 | '=', rolls_str, (' +-'[opr] + str(offset)) if offset else '', 24 | f'={res}' if offset or num > 1 else '', 25 | ] 26 | msg = ''.join(msg) 27 | await bot.send(ctx, msg, at_sender=True) 28 | 29 | 30 | @sv.on_rex(re.compile(r'^\.r\s*((?P\d{0,2})d((?P\d{1,4})~)?(?P\d{0,4})((?P[+-])(?P\d{0,5}))?)?\b', re.I), 31 | normalize=False) 32 | async def dice(bot, ctx, match): 33 | num, min_, max_, opr, offset = 1, 1, 100, 1, 0 34 | if s := match.group('num'): 35 | num = int(s) 36 | if s := match.group('min'): 37 | min_ = int(s) 38 | if s := match.group('max'): 39 | max_ = int(s) 40 | if s := match.group('opr'): 41 | opr = -1 if s == '-' else 1 42 | if s := match.group('offset'): 43 | offset = int(s) 44 | await do_dice(bot, ctx, num, min_, max_, opr, offset) 45 | 46 | 47 | @sv.on_command('.qj', only_to_me=False) 48 | async def kc_marriage(session): 49 | args = session.current_arg_text.split() 50 | tip = f'与{args[0]}的ケッコンカッコカリ结果是:' if args else '的ケッコンカッコカリ结果是:' 51 | await do_dice(session.bot, session.ctx, 1, 3, 6, 1, 0, tip) 52 | -------------------------------------------------------------------------------- /hoshino/modules/groupmaster/anti_abuse.py: -------------------------------------------------------------------------------- 1 | import random 2 | from datetime import timedelta 3 | 4 | import nonebot 5 | from nonebot import on_command, message_preprocessor, Message, MessageSegment 6 | from nonebot.message import _check_calling_me_nickname 7 | try: # TODO: drop support for nonebot v1.5 8 | from nonebot.command import parse_command 9 | except: # TODO: bump dependence to nonebot v1.6 10 | from nonebot.command import CommandManager 11 | parse_command = CommandManager().parse_command 12 | 13 | from hoshino import logger, util, Service, R 14 | 15 | bot = nonebot.get_bot() 16 | BLANK_MESSAGE = Message(MessageSegment.text('')) 17 | 18 | @message_preprocessor 19 | async def black_filter(bot, ctx, plugin_manager=None): # plugin_manager is new feature of nonebot v1.6 20 | first_msg_seg = ctx['message'][0] 21 | if first_msg_seg.type == 'hb': 22 | return # pass normal Luck Money Pack to avoid abuse 23 | if ctx['message_type'] == 'group' and Service.check_block_group(ctx['group_id']) \ 24 | or Service.check_block_user(ctx['user_id']): 25 | ctx['message'] = BLANK_MESSAGE 26 | 27 | 28 | def _check_hbtitle_is_cmd(ctx, title): 29 | ctx = ctx.copy() # 复制一份,避免影响原有的ctx 30 | ctx['message'] = Message(title) 31 | _check_calling_me_nickname(bot, ctx) 32 | cmd, _ = parse_command(bot, str(ctx['message']).lstrip()) 33 | return bool(cmd) 34 | 35 | 36 | @bot.on_message('group') 37 | async def hb_handler(ctx): 38 | self_id = ctx['self_id'] 39 | user_id = ctx['user_id'] 40 | group_id = ctx['group_id'] 41 | first_msg_seg = ctx['message'][0] 42 | if first_msg_seg.type == 'hb': 43 | title = first_msg_seg['data']['title'] 44 | if _check_hbtitle_is_cmd(ctx, title): 45 | Service.set_block_group(group_id, timedelta(hours=1)) 46 | Service.set_block_user(user_id, timedelta(days=30)) 47 | util.silence(ctx, 7 * 24 * 60 * 60) 48 | msg_from = f"{ctx['user_id']}@[群:{ctx['group_id']}]" 49 | logger.critical(f'Self: {ctx["self_id"]}, Message {ctx["message_id"]} from {msg_from} detected as abuse: {ctx["message"]}') 50 | await bot.send(ctx, "检测到滥用行为,您的操作已被记录并加入黑名单。\nbot拒绝响应本群消息1小时", at_sender=True) 51 | try: 52 | await bot.set_group_kick(self_id=self_id, group_id=group_id, user_id=user_id, reject_add_request=True) 53 | logger.critical(f"已将{user_id}移出群{group_id}") 54 | except: 55 | pass 56 | 57 | 58 | # ============================================ # 59 | 60 | BANNED_WORD = ( 61 | 'rbq', 'RBQ', '憨批', '废物', '死妈', '崽种', '傻逼', '傻逼玩意', 62 | '没用东西', '傻B', '傻b', 'SB', 'sb', '煞笔', 'cnm', '爬', 'kkp', 63 | 'nmsl', 'D区', '口区', '我是你爹', 'nmbiss', '弱智', '给爷爬', '杂种爬','爪巴' 64 | ) 65 | @on_command('ban_word', aliases=BANNED_WORD, only_to_me=True) 66 | async def ban_word(session): 67 | ctx = session.ctx 68 | user_id = ctx['user_id'] 69 | msg_from = str(user_id) 70 | if ctx['message_type'] == 'group': 71 | msg_from += f'@[群:{ctx["group_id"]}]' 72 | elif ctx['message_type'] == 'discuss': 73 | msg_from += f'@[讨论组:{ctx["discuss_id"]}]' 74 | logger.critical(f'Self: {ctx["self_id"]}, Message {ctx["message_id"]} from {msg_from}: {ctx["message"]}') 75 | # await session.send(random.choice(BANNED_WORD)) 76 | Service.set_block_user(user_id, timedelta(hours=8)) 77 | pic = R.img(f"chieri{random.randint(1, 4)}.jpg").cqcode 78 | await session.send(f"不理你啦!バーカー\n{pic}", at_sender=True) 79 | await util.silence(session.ctx, 8*60*60) 80 | -------------------------------------------------------------------------------- /hoshino/modules/groupmaster/chat.py: -------------------------------------------------------------------------------- 1 | import random 2 | from datetime import timedelta 3 | 4 | from nonebot import on_command 5 | from hoshino import util 6 | from hoshino.res import R 7 | from hoshino.service import Service, Privilege as Priv 8 | 9 | # basic function for debug, not included in Service('chat') 10 | @on_command('zai?', aliases=('在?', '在?', '在吗', '在么?', '在嘛', '在嘛?')) 11 | async def say_hello(session): 12 | await session.send('馬鹿!あほ!!変態!!!') 13 | 14 | sv = Service('chat', manage_priv=Priv.SUPERUSER, visible=False) 15 | 16 | @sv.on_command('沙雕机器人', aliases=('沙雕機器人',), only_to_me=False) 17 | async def say_sorry(session): 18 | await session.send('ごめんなさい!嘤嘤嘤(〒︿〒)') 19 | 20 | @sv.on_command('老婆', aliases=('waifu', 'laopo'), only_to_me=True) 21 | async def chat_waifu(session): 22 | if not sv.check_priv(session.ctx, Priv.SUPERUSER): 23 | await session.send(R.img('laopo.jpg').cqcode) 24 | else: 25 | await session.send('mua~') 26 | 27 | @sv.on_command('老公', only_to_me=True) 28 | async def chat_laogong(session): 29 | await session.send('你给我滚!', at_sender=True) 30 | 31 | @sv.on_command('mua', only_to_me=True) 32 | async def chat_mua(session): 33 | await session.send('笨蛋~', at_sender=True) 34 | 35 | @sv.on_command('来点星奏', only_to_me=False) 36 | async def seina(session): 37 | await session.send(R.img('星奏.png').cqcode) 38 | 39 | @sv.on_command('我有个朋友说他好了', aliases=('我朋友说他好了', ), only_to_me=False) 40 | async def ddhaole(session): 41 | await session.send('那个朋友是不是你弟弟?') 42 | await util.silence(session.ctx, 30) 43 | 44 | @sv.on_command('我好了', only_to_me=False) 45 | async def nihaole(session): 46 | await session.send('不许好,憋回去!') 47 | await util.silence(session.ctx, 30) 48 | 49 | # ============================================ # 50 | 51 | @sv.on_keyword(('确实', '有一说一', 'u1s1', 'yysy')) 52 | async def chat_queshi(bot, ctx): 53 | if random.random() < 0.05: 54 | await bot.send(ctx, R.img('确实.jpg').cqcode) 55 | 56 | @sv.on_keyword(('会战', '刀')) 57 | async def chat_clanba(bot, ctx): 58 | if random.random() < 0.03: 59 | await bot.send(ctx, R.img('我的天啊你看看都几度了.jpg').cqcode) 60 | 61 | @sv.on_keyword(('内鬼')) 62 | async def chat_neigui(bot, ctx): 63 | if random.random() < 0.10: 64 | await bot.send(ctx, R.img('内鬼.png').cqcode) 65 | -------------------------------------------------------------------------------- /hoshino/modules/groupmaster/group_approve.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_request, RequestSession 2 | from hoshino.util import load_config 3 | 4 | ''' Config Example 5 | { 6 | ..., 7 | "group_approve": { 8 | "123456": { 9 | "keywords": ["key1", "key2", "key3"], 10 | "reject_when_not_match": true/false 11 | } 12 | } 13 | } 14 | ''' 15 | 16 | @on_request('group.add') 17 | async def group_approve(session:RequestSession): 18 | cfg = load_config(__file__) 19 | cfg = cfg.get('group_approve', {}) 20 | gid = str(session.event.group_id) 21 | if gid not in cfg: 22 | return 23 | key = cfg[gid].get('keywords', []) 24 | for k in key: 25 | if k in session.event.comment: 26 | await session.approve() 27 | return 28 | if cfg[gid].get('reject_when_not_match', False): 29 | await session.reject() 30 | return 31 | -------------------------------------------------------------------------------- /hoshino/modules/groupmaster/group_notice.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_notice, NoticeSession 2 | from hoshino import util 3 | 4 | 5 | @on_notice('group_decrease.leave') 6 | async def leave_notice(session:NoticeSession): 7 | cfg = util.load_config(__file__) 8 | no_leave_notice_group = cfg.get('no_leave_notice', []) 9 | if session.ctx['group_id'] not in no_leave_notice_group: 10 | await session.send(f"{session.ctx['user_id']}退群了。") 11 | 12 | 13 | @on_notice('group_increase') 14 | async def increace_notice(session:NoticeSession): 15 | cfg = util.load_config(__file__) 16 | welcome_dic = cfg.get('increase_welcome', {}) 17 | gid = str(session.ctx['group_id']) 18 | if gid in welcome_dic: 19 | await session.send(welcome_dic[gid], at_sender=True) 20 | 21 | 22 | @on_notice('group_decrease.kick_me') 23 | async def kick_me_alert(session:NoticeSession): 24 | group_id = session.event.group_id 25 | operator_id = session.event.operator_id 26 | coffee = session.bot.config.SUPERUSERS[0] 27 | await session.bot.send_private_msg(self_id=session.event.self_id, user_id=coffee, message=f'被Q{operator_id}踢出群{group_id}') 28 | -------------------------------------------------------------------------------- /hoshino/modules/groupmaster/random_repeater.py: -------------------------------------------------------------------------------- 1 | import random 2 | from nonebot import get_bot, CQHttpError 3 | from hoshino import logger 4 | 5 | bot = get_bot() 6 | 7 | PROB_A = 1.6 8 | group_stat = {} # group_id: (last_msg, is_repeated, p) 9 | 10 | ''' 11 | 不复读率 随 复读次数 指数级衰减 12 | 从第2条复读,即第3条重复消息开始有几率触发复读 13 | 14 | a 设为一个略大于1的小数,最好不要超过2,建议1.6 15 | 复读概率计算式:p_n = 1 - 1/a^n 16 | 递推式:p_n+1 = 1 - (1 - p_n) / a 17 | ''' 18 | @bot.on_message('group') 19 | async def random_repeater(context): 20 | group_id = context['group_id'] 21 | msg = str(context['message']) 22 | 23 | if group_id not in group_stat: 24 | group_stat[group_id] = (msg, False, 0) 25 | return 26 | 27 | last_msg, is_repeated, p = group_stat[group_id] 28 | if last_msg == msg: # 群友正在复读 29 | if not is_repeated: # 机器人尚未复读过,开始测试复读 30 | if random.random() < p: # 概率测试通过,复读并设flag 31 | try: 32 | group_stat[group_id] = (msg, True, 0) 33 | await bot.send(context, msg) 34 | except CQHttpError as e: 35 | logger.error(f'复读失败: {type(e)}') 36 | else: # 概率测试失败,蓄力 37 | p = 1 - (1 - p) / PROB_A 38 | group_stat[group_id] = (msg, False, p) 39 | else: # 不是复读,重置 40 | group_stat[group_id] = (msg, False, 0) 41 | 42 | 43 | def _test_a(a): 44 | ''' 45 | 该函数打印prob_n用于选取调节a 46 | 注意:由于依指数变化,a的轻微变化会对概率有很大影响 47 | ''' 48 | p0 = 0 49 | for _ in range(10): 50 | p0 = (p0 - 1) / a + 1 51 | print(p0) 52 | -------------------------------------------------------------------------------- /hoshino/modules/groupmaster/sleeping_set.py: -------------------------------------------------------------------------------- 1 | import re 2 | import math 3 | import random 4 | 5 | import nonebot 6 | from nonebot import on_command, CommandSession 7 | from nonebot import on_natural_language, NLPSession 8 | from nonebot import permission as perm 9 | 10 | from hoshino.util import silence 11 | 12 | 13 | @on_command('sleep_8h', aliases=('睡眠套餐', '休眠套餐', '精致睡眠', '来一份精致睡眠套餐', '精緻睡眠', '來一份精緻睡眠套餐'), permission=perm.GROUP) 14 | async def sleep_8h(session: CommandSession): 15 | await silence(session.ctx, 8*60*60, ignore_super_user=True) 16 | 17 | 18 | @on_natural_language(keywords={'套餐'}, permission=perm.GROUP, only_to_me=False) 19 | async def sleep(session:NLPSession): 20 | arg = session.msg_text.strip() 21 | rex = re.compile(r'(来|來)(.*(份|个)(.*)(睡|茶)(.*))套餐') 22 | base = 0 if '午' in arg else 5*60*60 23 | m = rex.search(arg) 24 | if m: 25 | length = len(m.group(1)) 26 | sleep_time = base + round(math.sqrt(length) * 60 * 30 + 60 * random.randint(-15, 15)) 27 | await silence(session.ctx, sleep_time, ignore_super_user=True) 28 | -------------------------------------------------------------------------------- /hoshino/modules/hourcall/hourcall.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | from datetime import datetime 3 | from hoshino import util 4 | from hoshino.service import Service 5 | 6 | sv = Service('hourcall', enable_on_default=False) 7 | 8 | def get_hour_call(): 9 | """从HOUR_CALLS中挑出一组时报,每日更换,一日之内保持相同""" 10 | config = util.load_config(__file__) 11 | now = datetime.now(pytz.timezone('Asia/Shanghai')) 12 | hc_groups = config["HOUR_CALLS"] 13 | g = hc_groups[ now.day % len(hc_groups) ] 14 | return config[g] 15 | 16 | @sv.scheduled_job('cron', hour='*') 17 | async def hour_call(): 18 | now = datetime.now(pytz.timezone('Asia/Shanghai')) 19 | if 2 <= now.hour <= 4: 20 | return # 宵禁 免打扰 21 | msg = get_hour_call()[now.hour] 22 | await sv.broadcast(msg, 'hourcall', 0) 23 | -------------------------------------------------------------------------------- /hoshino/modules/mikan/mikan.py: -------------------------------------------------------------------------------- 1 | import random 2 | import requests 3 | import asyncio 4 | from lxml import etree 5 | from datetime import datetime 6 | 7 | from hoshino import util 8 | from hoshino.service import Service 9 | 10 | sv = Service('bangumi', enable_on_default=False) 11 | 12 | class Mikan(object): 13 | link_cache = set() 14 | rss_cache = [] 15 | 16 | @staticmethod 17 | def get_token(): 18 | config = util.load_config(__file__) 19 | return config["MIKAN_TOKEN"] 20 | 21 | 22 | @staticmethod 23 | def get_rss(): 24 | res = [] 25 | try: 26 | resp = requests.get('https://mikanani.me/RSS/MyBangumi', params={'token': Mikan.get_token()}, timeout=1) 27 | rss = etree.XML(resp.content) 28 | except Exception as e: 29 | sv.logger.error(f'[get_rss] Error: {e}') 30 | return [] 31 | 32 | for i in rss.xpath('/rss/channel/item'): 33 | link = i.find('./link').text 34 | description = i.find('./description').text 35 | pubDate = i.find('.//xmlns:pubDate', namespaces={'xmlns': 'https://mikanani.me/0.1/'}).text 36 | pubDate = pubDate[:19] 37 | pubDate = datetime.strptime(pubDate, r'%Y-%m-%dT%H:%M:%S') 38 | res.append( (link, description, pubDate) ) 39 | return res 40 | 41 | 42 | @staticmethod 43 | def update_cache(): 44 | rss = Mikan.get_rss() 45 | new_bangumi = [] 46 | flag = False 47 | for item in rss: 48 | if item[0] not in Mikan.link_cache: 49 | flag = True 50 | new_bangumi.append(item) 51 | if flag: 52 | Mikan.link_cache = { item[0] for item in rss } 53 | Mikan.rss_cache = rss 54 | return new_bangumi 55 | 56 | 57 | DEVICES = [ 58 | '22号对水上电探改四(后期调整型)', 59 | '15m二重测距仪+21号电探改二', 60 | 'FuMO25 雷达', 61 | 'SK+SG 雷达', 62 | 'SG 雷达(初期型)', 63 | 'GFCS Mk.37', 64 | '潜水舰搭载电探&逆探(E27)', 65 | 'HF/DF+Type144/147 ASDIC', 66 | '三式指挥联络机(对潜)', 67 | 'O号观测机改二', 68 | 'S-51J改', 69 | '二式陆上侦察机(熟练)', 70 | '东海(九〇一空)', 71 | '二式大艇', 72 | 'PBY-5A Catalina', 73 | '零式水上侦察机11型乙(熟练)', 74 | '紫云', 75 | 'Ar196改', 76 | 'Ro.43水侦', 77 | 'OS2U', 78 | 'S9 Osprey', 79 | '彩云(东加罗林空)', 80 | '彩云(侦四)', 81 | '试制景云(舰侦型)', 82 | ] 83 | 84 | @sv.scheduled_job('cron', minute='*/3', second='15') 85 | async def mikan_poller(): 86 | if not Mikan.rss_cache: 87 | Mikan.update_cache() 88 | sv.logger.info(f'订阅缓存为空,已加载至最新') 89 | return 90 | new_bangumi = Mikan.update_cache() 91 | if not new_bangumi: 92 | sv.logger.info(f'未检索到番剧更新!') 93 | else: 94 | sv.logger.info(f'检索到{len(new_bangumi)}条番剧更新!') 95 | msg = [ f'{i[1]} 【{i[2].strftime(r"%Y-%m-%d %H:%M")}】\n▲下载 {i[0]}' for i in new_bangumi ] 96 | randomiser = lambda m: f'{random.choice(DEVICES)}监测到番剧更新!{"!"*random.randint(0,4)}\n{m}' 97 | await sv.broadcast(msg, '蜜柑番剧', 0.5, randomiser) 98 | 99 | 100 | DISABLE_NOTICE = '本群蜜柑番剧功能已禁用\n使用【启用 bangumi】以启用(需群管理)\n开启本功能后将自动推送字幕组更新' 101 | 102 | @sv.on_command('来点新番', aliases=('來點新番', ), deny_tip=DISABLE_NOTICE, only_to_me=False) 103 | async def send_bangumi(session): 104 | if not Mikan.rss_cache: 105 | Mikan.update_cache() 106 | 107 | msg = [ f'{i[1]} 【{i[2].strftime(r"%Y-%m-%d %H:%M")}】\n▲链接 {i[0]}' for i in Mikan.rss_cache[:min(5, len(Mikan.rss_cache))] ] 108 | msg = '\n'.join(msg) 109 | await session.send(f'最近更新的番剧:\n{msg}') 110 | -------------------------------------------------------------------------------- /hoshino/modules/pcrclanbattle/clanbattle/README.md: -------------------------------------------------------------------------------- 1 | # PCR会战管理 2 | 3 | v2.0 🐒也会用的会战管理 4 | 5 | [toc] 6 | 7 | ## 特色 8 | 9 | - 简洁灵活,学习简单 10 | - 预约Boss,自动提醒 11 | - 一键催刀,警察减负 12 | - 全角半角,繁简均可 13 | 14 | 15 | ## 快速开始 16 | 17 | 0. 与维护组联系(py),邀请bot入群 18 | 19 | 1. 群初次使用时,需要设置公会名与服务器地区,使用命令 `!建会 N<公会名> S<服务器地区>` 20 | 21 | ``` 22 | !建会 Nリトルリリカル Sjp 23 | !建会 N小小甜心 Stw 24 | !建会 N今天版号批了吗 Scn 25 | ``` 26 | 27 | 2. 使用命令 `!入会 <昵称>` 进行注册 28 | 29 | ``` 30 | !入会 祐树 31 | ``` 32 | 33 | 3. 使用命令 `!出刀 <伤害值>` 上报伤害 34 | 35 | ``` 36 | !出刀 514w 37 | !收尾 38 | !出补时刀 114w 39 | ``` 40 | 41 | 4. 忙于工作/学习/娱乐时,使用命令 `!预约 ` 预约出刀 42 | 43 | ``` 44 | !预约 5 45 | ``` 46 | 47 | 5. 不慎失误时,使用命令 `!挂树` 等待救援 48 | 49 | ``` 50 | !挂树 51 | ``` 52 | 53 | 6. 夜深人静时,使用命令 `!催刀` 在群内at未出刀的成员,督促出刀 54 | 55 | ``` 56 | !催刀 57 | ``` 58 | 59 | 60 | 61 | ## Hoshino会战管理命令一览 62 | ### 使用前必读 63 | 64 | > 💡 会战系命令均以感叹号`!`开头,半全角均可 65 | > 💡 命令与参数之间**必须**以*空格*隔开 66 | > 💡 `X<参数名>`表示参数以字母`X`引导,使用时需输入`X`,无需输入尖括号`<>` 67 | > 💡 `()`包括的参数为可选 68 | 69 | ### 公会管理命令 70 | 71 | | 使用场景 | 命令格式 | 备注 | 使用例 | 72 | | -------- | ------------- | -------------- | ----------- | 73 | | 初次使用 | `!建会 N<公会名> S<服务器地区>` | 日服jp,台服tw,国服cn | `!建会 Nリトルリリカル Sjp`
`!建会 N小小甜心 Stw`
`!建会 N今天版号批了吗 Scn` | 74 | | 成员注册 | `!入会 <昵称>` | | `!入会 祐树` | 75 | | 成员注册
*管理拉人* | `!入会 @ <昵称>` | 可直接@ 需本人/管理/群主 | `!入会 @1802002609 N祐树` | 76 | | 成员查看 | `!查看成员` | | `!查看成员` | 77 | | 成员除名 | `!退会 (@)` | 可直接@ 需本人/管理/群主 | `!退会` | 78 | | 成员清空 | `!清空成员` | 需管理/群主 | `!清空成员` | 79 | | 批量注册 | `!一键入会` | 需管理/群主,群员少于40时可用 | `!一键入会` | 80 | 81 | > 如需修改公会名/服务器地区,再次使用`!建会`命令 82 | > 如需修改昵称,再次使用`!入会`命令 83 | 84 | ----- 85 | 86 | ### 出刀记录命令 87 | 88 | | 使用场景 | 命令格式 | 备注 | 使用例 | 89 | | :--------------- | :--------------------- | :------------------------- | :--------------------- | 90 | | 伤害上报 | `!出刀 <伤害值>` | 支持以w或k作为单位 | `!出刀 600w` | 91 | | 伤害上报
*收尾* | `!出尾刀` | 自动计算伤害;亦可手动指定 | `!出尾刀` | 92 | | 伤害上报
*补偿时间* | `!出补时刀 <伤害值>` | 支持以w或k作为单位 | `!出补时刀 100w` | 93 | | 伤害上报
*代打* | `!出刀 <伤害值> Q` | 可直接@ | `!出刀 500w @1802002609` | 94 | |伤害补报
*指定周目/Boss*|`!出刀 <伤害值> R<周目数> B`|尾刀/补时刀使用`!出尾刀`/`!出补时刀`|`!出刀 500w R4 B2`| 95 | | 掉线记录 | `!掉刀` | 记录伤害为0,本日余刀减1 | `!掉刀` | 96 | | 删除记录 | `!删刀 E<记录编号>` | 群管理可删除他人记录 | `!删刀 E42` | 97 | 98 | > **名词解释** 99 | > 💡**尾刀**:队伍首次出击,并击败Boss的刀 100 | > 💡**补时刀**:用补偿时间出的刀 101 | 102 | > ⚠️若用**补时刀**击败Boss则无更多补偿时间,此时应报**补时刀**而非*尾刀* 103 | 104 | ----- 105 | 106 | ### 预约及锁定Boss命令 107 | 108 | | 使用场景 | 命令格式 | 备注 | 使用例 | 109 | | :-------------------- | :------------------- | :----------------- | :------------ | 110 | | 预约Boss | `!预约 M<留言>` | 到达Boss时自动提醒 | `!预约 1 M哥布林杀手参上` | 111 | | 预约取消 | `!预约取消 ` | | `!预约取消 1` | 112 | | 预约查询 | `!预约查询` | | `!预约查询` | 113 | | 清空预约队列 | `!预约清空 ` | 需管理/群主 | `!预约清空 4` | 114 | | 锁定Boss | `!锁定` | 公会全体成员均需**自觉**使用
注:锁定对报刀无强制约束力,不使用本功能也不影响报刀
| `!锁定` | 115 | | 解锁Boss | `!解锁` | 报刀时自动解锁 | `!解锁` | 116 | 117 | 118 | 119 | ----- 120 | 121 | ### 等待救援命令 122 | 123 | | 使用场景 | 命令格式 | 备注 | 使用例 | 124 | | :------------------ | :----------------- | :----------------------------- | :--------------------------------------------- | 125 | | 失误挂树 | `!挂树` | Boss斩杀后自动提醒 | `!挂树` | 126 | | 取消挂树 | `!下树` | *防止被恶意使用,不可主动下树* | ~~你下尼🐴呢?
🌳不是玩具!
给👴🏻老实挂着~~ | 127 | | 查看挂树成员 | `!查树` | | `!查树` | 128 | 129 | ----- 130 | 131 | ### 查询/统计命令 132 | 133 | | 使用场景 | 命令格式 | 备注 | 使用例 | 134 | | :--------------------- | :------------------ | :----------------------------------- | :---------------------- | 135 | | 进度查询 | `!进度` | 查看会战攻略进度 | `!进度` | 136 | | 统计分数 | `!统计` | 统计本月会战分数 | `!统计` | 137 | | 余刀查询 | `!查刀` | 查看成员余刀数,已下班成员不会被列出 | `!查刀` | 138 | | 一键催刀 | `!催刀` | 群内at催刀,需管理/群主 | `!催刀` | 139 | | 查看个人今日出刀记录 | `!出刀记录 @` | | `!出刀记录 @1802002609` | 140 | | 查看全公会今日出刀记录 | `!出刀记录` | | `!出刀记录` | 141 | 142 | 143 | 144 | ## FAQ 145 | 146 | **Q1: 用补偿时间收了尾,应当如何报刀?** 147 | 148 | A1: 若用补时刀击败Boss则无更多补偿时间(游戏设定如此),此时应报**补时刀**而非*尾刀*。一刀最多打2个Boss,一天最多出6刀,你无法用补时刀获取补时。 149 | 150 | **Q2: 如何重置出刀记录?** 151 | 152 | A2: Hoshino会战管理不提供一键重置功能,因为这需要删除本月所有的出刀记录,属于非常危险的操作。每月20号会自动切换至下个月(\*具体日期可能会变更),正常使用时无需操心。若因测试功能而产生记录,建议手动使用`!删刀`命令逐条删除;实在过多的情况(超过50条)可以联系维护组后台操作数据库删除,操作数据库前务必备份! 153 | 154 | **Q3: 我可以下树吗?** 155 | 156 | A3: 不可以。请等待战友击杀当前boss,如若掉线滑刀,请使用`!掉刀`命令上报。 157 | 158 | **Q4: 锁定功能如何使用?锁定后别人就不能报刀了吗?** 159 | 160 | A4: 锁定Boss的功能需要全公会人全部自觉使用才能达到效果,锁定后其他人将无法再次锁定,直到自己报刀或主动解锁(管理员可强制解锁)。锁定后其他人仍然可以报刀(但会触发警告),因为报刀行为通常发生在实际出刀之后,我们不可能阻止不在群内交流的人出自闭刀。所以,若要发挥本功能的作用,需要会长提前协调大家自觉使用。 161 | 162 | **Q5: 报刀报错了怎么办?** 163 | 164 | A5: 使用`!删刀`命令删除,记录编号以字母E开头,会在出刀时给出,也可用`!出刀记录`命令查看。 165 | 166 | **Q6: 我忘记报刀了/我删了很久之前的刀,如何补报?** 167 | 168 | A6: 同样使用`!出刀`/`!出尾刀`/`!出补时刀`命令,指定周目数和Boss编号即可,例如`!出刀 500w R11 B2`将补报一条11周目2号Boss的记录,注意此处的`B`是`Boss`的缩写,而非*阶段数*,阶段数将由周目数自动计算。 169 | 170 | **Q7: 有人没报刀导致bot的进度落后,如何调整进度?** 171 | 172 | A7: 同样使用`!出刀`命令,指定周目数和Boss编号。进度将根据最"靠后"的记录进行计算。比如当前进度显示为1周目1号Boss,此时上报`!出刀 500w R11 B2`会将进度调整至11周目2号Boss。只要确保每条报刀记录均准确无误、团员均能及时报刀,bot的进度计算与伤害自动修正就会保持正确。 173 | 174 | -------------------------------------------------------------------------------- /hoshino/modules/pcrclanbattle/clanbattle/READMEv1.md: -------------------------------------------------------------------------------- 1 | # HoshinoPlan:clanbattle 2 | 3 | **此为1.0版文档,现以完全重写更新为2.0版** 4 | **2.0版命令得到了极大的优化,更加方便快捷,同时与1.0版数据保持共通** 5 | **1.0版命令不再更新,但仍可用,计划于2020年4月会战后停止支持** 6 | 7 | Nonebot会战管理插件开发计划 8 | 9 | ## 简介 10 | 11 | 基于nonebot的QQ机器人框架,开发插件`clanbattle`用于群内会战管理。 12 | 13 | 14 | 15 | ## 快速指南 - QuickStart 16 | 17 | #### 会战开始前 - Before Clanbattle 18 | 19 | **Step 1:** 邀请机器人至群聊; 20 | 21 | **Step 2:** 给群添加一个公会: 22 | 23 | ``` 24 | add-clan --name 鼬鼬美食殿 25 | ``` 26 | 这个命令将会将“鼬鼬美食殿”注册为本群的1会,您可以使用自己想用的公会名; 27 | 28 | **Step 3:** 通知群员加入公会,每个人使用下面的命令: 29 | 30 | ``` 31 | join-clan --name 祐树 32 | ``` 33 | 34 | 这个命令将会将命令发送者加入本群的1会,注册昵称为“祐树”,群员可以使用自己的游戏昵称。 35 | 36 | 37 | 38 | ------ 39 | 40 | #### 会战日 - Clanbattle Days 41 | 42 | **Step 1 (Optional):** 预约想要挑战的Boss *本功能正在绝赞开发中...* 43 | 44 | **Step 2 (Recommand):** 正式战前进入挑战队列 *本功能正在绝赞开发中...* 45 | 46 | 47 | 48 | **Step 3:** 上报伤害 49 | 50 | 简易版命令(**学不会命令行?**那就记住这个就好了): 51 | 52 | ``` 53 | 刀 114w r8 b1 54 | ``` 55 | 56 | 该命令将会为发送者记录下挑战伤害:对第8周目的Boss1造成了1,140,000点伤害(请修改为实际的参数) 57 | 58 | 59 | 60 | 完整功能版命令: 61 | 62 | ``` 63 | dmg 1919810 -r11 -b4 --last 64 | ``` 65 | 66 | 该命令将会为发送者记录下挑战伤害:对第11周目的Boss4造成了1,919,810点伤害,收掉尾刀(请修改为实际的参数) 67 | 68 | 关于`dmg`命令的详细说明请见下方[支持命令一览]节,完整的参数及相关说明为: 69 | 70 | ``` 71 | dmg -r -b damage [--uid] [--alt] [--ext | --last | --timeout] 72 | ``` 73 | 74 | - r/round: 周目数 75 | - b/boss: Boss编号 76 | - ext/last/timeout: 补时刀/尾刀/掉刀 标志,仅能指定其中一种 77 | - uid: 上报对象的QQ号(用于为他人代报) 78 | - alt: 上报对象的小号编号 79 | 80 | 81 | 82 | ----- 83 | 84 | #### 会战日结束 - End of a Clanbattle Day 85 | 86 | **让我来看看哪个幸运儿还没有出刀:** `show-remain` 查询本会余刀情况; 87 | 88 | **一键催刀:** 管理员使用`show-remain`会自动at有余刀的成员,提醒其出刀;*私聊催刀绝赞开发中...* 89 | 90 | **谁是分奴?谁是弟弟?:** `stat` 查看本会分数排名; 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | ## 支持命令一览 99 | 100 | *SUPERUSER不受权限控制影响* 101 | 102 | ### 公会管理 103 | 104 | | 进度 | 命令 | 参数 | 权限 | 说明 | 105 | |---| -------------------- | ----------------- | ----------------------- | ------------------------------------------------------------ | 106 | | ok | add-clan | [--cid] [--name] | GROUP_ADMIN | 添加新公会,编号为id,默认为该群最大公会id+1,若无公会则为1;name为公会名 | 107 | | ok | list-clan | (empty) | GROUP_MEMBER | 默认显示当前QQ群的所有公会;--all 显示管理的所有公会,仅SUPERUSER可用 | 108 | | | mod-clan | --cid --new_name | GROUP_ADMIN | 修改公会的name | 109 | | | del-clan | --cid | GROUP_ADMIN | 删除公会 | 110 | 111 | ### 成员管理 112 | 113 | |进度| 命令 | 参数 | 权限 | 说明 | 114 | |--| -------------------- | ------------------------------- | --------------- | ---- | 115 | |ok| add-member / join-clan | [--cid] [--uid] [--alt] [--name] | GROUP_MEMBER | 将[uid]的小号[alt]加入[cid]会,游戏内ID为[name]。参数缺省时将会将命令发送者的大号加入1会,自动获取群名片作为name。 | 116 | |ok| list-member | [--cid] | GROUP_MEMBER | 列出[cid]会的成员。默认为1会 | 117 | || mod-member | [--uid] [--alt] --name | OWNER / GROUP_ADMIN | 修改名称 | 118 | |ok| del-member / quit-clan | [--uid] [--alt] | OWNER / GROUP_ADMIN | 退出公会 / 删除成员 | 119 | 120 | ### Boss信息查询 121 | 122 | 123 | 124 | ### 出刀管理 125 | 126 | |进度| 命令 | 别名 | 参数 | 权限 | 说明 | 127 | |--| ------------------- |----| ------------------------------------- | ------------ | ------------------------------------------------------------ | 128 | |ok| add-challenge |dmg| --round --boss damage [--uid] [--alt] | GROUP_MEMBER | 报刀。[uid]的账号[alt]对[round]周目第[boss]个Boss造成了[damage]点伤害。 | 129 | |ok| add-challenge-e |dmge / 刀| r? b? 伤害数字 | GROUP_MEMBER | 简易报刀。例,对5周目老4造成了1919810点伤害:dmge 1919810 r5 b4 | 130 | || mod-challenge | mod-dmg | | GROUP_ADMIN / 记录所有者 | 改刀 | 131 | 132 | ### 预约/排队 133 | 134 | | 进度 | 命令 | 别名 | 参数 | 权限 | 说明 | 135 | | ---- | -------|-- | ---- | ------------ | -------------------- | 136 | | | subscribe |sub / 预约| --boss | GROUP_MEMBER | 预约boss,到达时提醒 | 137 | | | enqueue |enq / 入队| | GROUP_MEMBER | 进入出刀队列 | 138 | | | dequeue | deq / 出队 | | GROUP_MEMBER | 退出出刀队列 | 139 | 140 | 141 | 142 | 143 | 144 | ### 会战统计 145 | 146 | |进度| 命令 | 参数 | 权限 | 说明 | 147 | |-| ----------- | ----------------- | ------------ | ------------------------------------------ | 148 | |ok| show-remain | [--cid] | GROUP_MEMBER | 查看今日余刀,管理员调用时将进行一键催刀。 | 149 | |ok| stat | [--cid] | GROUP_MEMBER | 当月会战分数统计,按分数排名 | 150 | || plot | [--cid] [--today] [--all] | | 绘制会战分数报表 | 151 | 152 | 153 | 154 | 155 | 156 | ## FAQ 157 | 158 | **Q:这个机器人是干什么的?要怎么用?** 159 | 160 | **A:** 本机器人旨在帮助公会记录会战出刀情况,并支持分数统计、一键催刀等功能。邀请机器人账号进入群后,输入命令即可调用相应功能,详见下节[支持命令一览]。 161 | 162 | **Q:命令是啥?能吃吗?** 163 | 164 | **A:** 命令是指机器人可以识别的一系列指令,可以指挥机器人做你希望做的事情。但机器人很笨,所以在你给出指令时请务必注意**空格**、**短横线**(减号)等符号,否则将无法成功执行。 165 | 166 | **Q:我就是个玩母猪链接的,记不住那么多,教我怎么报刀就行了** 167 | 168 | **A:** 首次使用机器人时,请输入`join-clan`加入到本群公会(如需加入2会,请输入`join-clan --cid 2`);之后,您只需要使用`dmge`命令进行简易报刀就可以了,格式为`dmge 伤害数字 r周目 b老几 [last|ext|timeout]`,具体例子见下: 169 | 170 | - 骑士A对5周目老4造成了1919810点伤害 171 | 172 | ``` 173 | dmge 1919810 r5 b4 174 | ``` 175 | 176 | - 骑士B对6周目老1造成了114514点伤害收了尾刀 177 | 178 | ``` 179 | dmge 114514 r6 b1 last 180 | ``` 181 | 182 | - 骑士B又用补偿时间对6周目老2造成了1919点伤害 183 | 184 | ``` 185 | dmge 1919 r6 b2 ext 186 | ``` 187 | 188 | - 骑士C打11周目老1时,伊莉雅连续暴毙两次挂树上,但挂的时间太长掉线了,无奈掉刀 189 | 190 | ``` 191 | dmge r11 b1 timeout 192 | ``` 193 | 194 | 如果您觉得dmge不好输入,也可使用汉字`刀`来替代。相信以上例子已经覆盖了绝大多数普通成员使用场景。 195 | 196 | **Q:我手滑报刀报错了,怎么办?** 197 | 198 | **A:** 目前请联系管理员在后台进行更改,改刀命令正在绝赞开发中... 199 | 200 | **Q:除了报刀还有啥功能?** 201 | 202 | **A:** 查看本日余刀`show-remain`(管理员使用时将进行一键催刀)、本月会战分数统计`stat`等,boss订阅、出刀排队等功能正在开发中,更多功能请查阅[支持命令一览]。另外还有常用链接速查、精致睡眠套餐、在线翻译、~~涩图~~等彩蛋功能。 203 | 204 | **Q:我是会长,我们一个群里有99个分会,有群友手里5个小号打会战,机器人报刀不会乱吗?** 205 | 206 | **A:** 开发时已考虑过这种场景,您可以使用`--cid`参数来指定分会编号(不指定时默认为1,即1会),成员可以使用`--alt`参数指定小号的编号,具体使用请参考每条命令的帮助,详见[支持命令一览]。 207 | 208 | **Q:要是有人漏报、没有及时报、报伤害报多了,数据会不会混乱?** 209 | 210 | **A:** 由于机器人无法直接获取游戏内数据,设计原则上只能信任用户提供的数据。但开发时已考虑到上述情况并进行了相应处理,数据异常时会自动修正或进行提醒,报刀顺序并不会影响当前会战进度的计算。不过,由于本机器人仍在开发中,确实有可能出现意料之外的情况,如有bug,欢迎联系开发组并提供复现方法。 211 | 212 | **Q:报刀太麻烦了,能不能做个直接获取游戏内数据的机器人出来?** 213 | 214 | **A:** 理论上是可以的,只需要将机器人账号拉入公会(29人打会战),定时刷新会战情况,抓下数据分析就能够实现。限于开发成本与技术难度,目前不计划实现,如果您有移动端网络抓包、数据解密等方面的经验,欢迎与开发组联系。 215 | 216 | **Q:我们就是个休闲公会,没必要整这么麻烦** 217 | 218 | **A:** ~~机器人本来也不是给咸鱼休闲公会开发的呢亲~!~~ 219 | 220 | **Q:你的代码不错,但下一秒就是我的啦!** 221 | 222 | **A:** 没问题呢亲,注意开源协议即可。关于部署请参考后续文档(*咕咕咕*) 223 | 224 | 225 | 226 | 227 | 228 | ------ 229 | 230 | 231 | 232 | # 开发笔记 233 | 234 | 235 | 236 | ## 应用背景及需求 237 | 238 | 本插件运行于nonebot之上,nonebot与酷Q通过CQHttp通信,完成QQ消息的接受与发送。 239 | 240 | 本插件应支持: 241 | 242 | - [x] 单机器人账号管理多个公会 243 | - [x] 多个公会共用一个QQ群:分为一会/二会/三会/... 244 | - [x] 单个QQ号可能拥有多个游戏账号、分属不同公会 245 | - [ ] 对公会使用进行授权 246 | - [ ] 管理公会的增查删改 247 | - 权限:群主:可修改、删除、查询本公会但不能添加新公会 248 | - [ ] 公会人员的增查删改 249 | - 权限:群主:均可;本人:仅可对自己进行 250 | - [x] 报刀报伤害 251 | - [ ] 出刀排队/预约boss 252 | - [ ] 新boss提醒 253 | 254 | 255 | 256 | 257 | 258 | ## 数据储存 259 | 260 | ### 术语 261 | 262 | - gid:group id,QQ群号 263 | - cid:clan id,群内下属分会编号 264 | 265 | 以上两字段唯一标识一个公会 266 | 267 | - uid:user id,用户的QQ号(不是游戏九码!) 268 | - alt:用户的小号编号(默认为0,主账号;可填1,2,3,... 也可用游戏内九码,前者平时使用更方便,后者更精确不怕忘) 269 | 270 | 以上两字段唯一标识一个游戏账号(不依赖九码) 271 | 272 | ``` 273 | clanbattle.db 274 | ├─clan 275 | ├─member 276 | ├─subscribe 277 | ├─battle_[gid]_[cid]_[yyyymm] 278 | 279 | ``` 280 | 281 | ### config.json 282 | 283 | 配置boss血量信息、提供简易版作业等 284 | 285 | ### TABLE: clan 286 | 287 | 公会以gid+cid作为唯一标识,name可修改可重复 288 | 289 | - gid:qq群号 290 | - cid:该群下的公会号 291 | - name:公会名 292 | - server:服务器 293 | 294 | 295 | 296 | ### TABLE: member 297 | 298 | 记录成员信息 299 | 300 | - uid:qq号 301 | - alt:小号编号 302 | - name:游戏内名字 303 | - gid:所属公会群 304 | - cid:所属公会分会 305 | 306 | 307 | 308 | ### TABLE: subscribe 309 | 310 | 订阅信息 311 | 312 | - sid:订阅编号 313 | - uid:qq号 314 | - alt:小号编号(一般情况不太需要) 315 | - gid:所属公会群 316 | - cid:所属分会 317 | - flag:Infight | 0 | 0 | 0 | SubBoss12345 | 318 | 319 | 320 | 321 | ### TABLE: battle_[gid]\_[cid]\_[yyyymm] 322 | 323 | 记录出刀信息 324 | 325 | - eid:出刀记录编号 326 | - uid:出刀者qq号 327 | - alt:出刀者小号编号 328 | - time:出刀时间`ddhhMMss` 329 | - round:周目数 330 | - boss:Boss编号 331 | - dmg:伤害 332 | - flag:出刀标志 normal/last/extend 333 | 334 | 335 | 336 | ## 开发思路 337 | 338 | ### 一次报刀发生了什么? 339 | 340 | 首先,获取到dmg命令,提取其中的round、boss、damage、flag信息,同时获取报刀者的uid(若给出了alt,则确认其小号); 341 | 342 | 然后,根据uid和alt查member表,查出其所属公会的gid和cid; 343 | 344 | 之后,查询对应的battle记录,录入出刀信息,写入数据库; 345 | 346 | 最后,更新该公会的进度,触发订阅信息。 347 | 348 | ### 一次统计报表发生了什么? 349 | 350 | 首先,获取到plot命令,得知gid和cid; 351 | 352 | 查询对应时间段的battle记录,统计每个账号的出刀记录; 353 | 354 | 汇总报表(并绘图),输出; 355 | 356 | ### 分层设计 357 | 358 | - 命令层 - `__init__.py`中的各种命令:接受机器人指令并调用服务层处理 359 | - 服务层 - BattleMaster:将数据层的基本操作组合,形成服务 360 | - 数据层 - DAOs:直接与SQLite交互,支持基本的增查删改 361 | 362 | 363 | 364 | ## 心得 365 | 366 | - 学习了SQLite的使用(增查删改/CRUD) 367 | - 实践了一下DAO的设计模式;但由于本项目并没有那么庞大,而且Python语言十分便利,并没有完全按照Java的面向接口开发模式来操作,不过以后如果需要改用别的数据库,只需要增加一个dao/xxxdao.py,修改battlemaster的import语句即可。 368 | 369 | - 学习了python的命令行参数处理工具 [Argparse](https://docs.python.org/zh-cn/3.6/howto/argparse.html) -------------------------------------------------------------------------------- /hoshino/modules/pcrclanbattle/clanbattle/__init__.py: -------------------------------------------------------------------------------- 1 | # 公主连接Re:Dive会战管理插件 2 | # clan == クラン == 戰隊(直译为氏族)(CLANNAD的CLAN(笑)) 3 | 4 | from typing import Callable, Dict, Tuple, Iterable 5 | 6 | from nonebot import NoneBot, on_command, CommandSession 7 | from hoshino import util 8 | from hoshino.service import Service, Privilege 9 | from hoshino.res import R 10 | from .argparse import ArgParser 11 | from .exception import * 12 | 13 | sv = Service('clanbattle', manage_priv=Privilege.SUPERUSER, enable_on_default=True) 14 | SORRY = 'ごめんなさい!嘤嘤嘤(〒︿〒)' 15 | 16 | _registry:Dict[str, Tuple[Callable, ArgParser]] = {} 17 | 18 | @sv.on_message('group') 19 | async def _clanbattle_bus(bot:NoneBot, ctx): 20 | # check prefix 21 | start = '' 22 | for m in ctx['message']: 23 | if m.type == 'text': 24 | start = m.data.get('text', '').lstrip() 25 | break 26 | if not start or start[0] not in '!!': 27 | return 28 | 29 | # find cmd 30 | plain_text = ctx['message'].extract_plain_text() 31 | cmd, *args = plain_text.split() 32 | cmd = util.normalize_str(cmd[1:]) 33 | if cmd in _registry: 34 | func, parser = _registry[cmd] 35 | try: 36 | sv.logger.info(f'Message {ctx["message_id"]} is a clanbattle command, start to process by {func.__name__}.') 37 | args = parser.parse(args, ctx['message']) 38 | await func(bot, ctx, args) 39 | sv.logger.info(f'Message {ctx["message_id"]} is a clanbattle command, handled by {func.__name__}.') 40 | except DatabaseError as e: 41 | await bot.send(ctx, f'DatabaseError: {e.message}\n{SORRY}\n※请及时联系维护组', at_sender=True) 42 | except ClanBattleError as e: 43 | await bot.send(ctx, e.message, at_sender=True) 44 | except Exception as e: 45 | sv.logger.exception(e) 46 | sv.logger.error(f'{type(e)} occured when {func.__name__} handling message {ctx["message_id"]}.') 47 | await bot.send(ctx, f'Error: 机器人出现未预料的错误\n{SORRY}\n※请及时联系维护组', at_sender=True) 48 | 49 | 50 | def cb_cmd(name, parser:ArgParser) -> Callable: 51 | if isinstance(name, str): 52 | name = (name, ) 53 | if not isinstance(name, Iterable): 54 | raise ValueError('`name` of cb_cmd must be `str` or `Iterable[str]`') 55 | names = map(lambda x: util.normalize_str(x), name) 56 | def deco(func) -> Callable: 57 | for n in names: 58 | if n in _registry: 59 | sv.logger.warning(f'出现重名命令:{func.__name__} 与 {_registry[n].__name__}命令名冲突') 60 | else: 61 | _registry[n] = (func, parser) 62 | return func 63 | return deco 64 | 65 | 66 | # from .cmdv1 import * 67 | from .cmdv2 import * 68 | 69 | 70 | QUICK_START = f''' 71 | ====================== 72 | - Hoshino会战管理v2.0 - 73 | ====================== 74 | 快速开始指南 75 | 【必读事项】 76 | ※会战系命令均以感叹号!开头,半全角均可 77 | ※命令与参数之间必须以【空格】隔开 78 | ※下面以使用场景-使用例给出常用指令的说明 79 | 【群初次使用】 80 | !建会 N自警団(カォン) Sjp 81 | !建会 N哞哞自衛隊 Stw 82 | !建会 N自卫团 Scn 83 | 【注册成员】 84 | !入会 祐树 85 | !入会 佐树 @123456789 86 | 【上报伤害】 87 | !出刀 514w 88 | !收尾 89 | !补时刀 114w 90 | 【预约Boss】 91 | !预约 5 M留言 92 | !取消预约 5 93 | 【锁定Boss】 94 | !锁定 95 | !解锁 96 | 【查询余刀&催刀】 97 | !查刀 98 | !催刀 99 | 100 | ※前往 t.cn/A6wBzowv 查看完整命令一览表 101 | ※如有问题请先阅读一览表底部的FAQ 102 | ※使用前请务必【逐字】阅读开头的必读事项 103 | '''.rstrip() 104 | 105 | @on_command('!帮助', aliases=('!帮助', '!幫助', '!幫助', '!help', '!help'), only_to_me=False) 106 | async def cb_help(session:CommandSession): 107 | await session.send(QUICK_START, at_sender=True) 108 | -------------------------------------------------------------------------------- /hoshino/modules/pcrclanbattle/clanbattle/argparse/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict 2 | from nonebot import Message 3 | 4 | from ..exception import * 5 | 6 | 7 | class ArgHolder: 8 | __slots__ = ('type', 'default', 'tip') 9 | def __init__(self, type=str, default=None, tip=None): 10 | self.type = type 11 | self.default = default 12 | self.tip = tip 13 | 14 | 15 | class ParseResult(dict): 16 | def __getattr__(self, key): 17 | return self[key] 18 | def __setattr__(self, key, value): 19 | self[key] = value 20 | 21 | 22 | class ArgParser: 23 | def __init__(self, usage, arg_dict=None): 24 | self.usage = f"【用法/用例】\n{usage}\n\n※无需输入尖括号,圆括号内为可选参数,用空格隔开命令与参数" 25 | self.arg_dict:Dict[str, ArgHolder] = arg_dict or {} 26 | 27 | 28 | def add_arg(self, name, *, type=str, default=None, tip=None): 29 | self.arg_dict[name] = ArgHolder(type, default, tip) 30 | 31 | 32 | def parse(self, args:List[str], message:Message) -> ParseResult: 33 | result = ParseResult() 34 | 35 | # 解析参数,以一个字符开头,或无前缀 36 | for arg in args: 37 | name, x = arg[0].upper(), arg[1:] 38 | if name in self.arg_dict: 39 | holder = self.arg_dict[name] 40 | elif '' in self.arg_dict: 41 | holder = self.arg_dict[''] 42 | name, x = '', arg 43 | else: 44 | raise ParseError(f'命令含有未知参数', self.usage) 45 | 46 | try: 47 | result.setdefault(name, holder.type(x)) # 多个参数只取第1个 48 | except ParseError as e: 49 | e.append(self.usage) 50 | raise e 51 | except Exception: 52 | msg = f"请给出正确的{holder.tip or '参数'}" 53 | if name: 54 | msg += f"以{name}开头" 55 | raise ParseError(msg, self.usage) 56 | 57 | # 检查所有参数是否以赋值 58 | for name, holder in self.arg_dict.items(): 59 | if name not in result: 60 | if holder.default is None: # 缺失必要参数 抛异常 61 | msg = f"请给出{holder.tip or '缺少的参数'}" 62 | if name: 63 | msg += f"以{name}开头" 64 | raise ParseError(msg, self.usage) 65 | else: 66 | result[name] = holder.default 67 | 68 | # 解析Message内的at 69 | result['at'] = 0 70 | for seg in message: 71 | if seg.type == 'at': 72 | result['at'] = int(seg.data['qq']) 73 | 74 | return result 75 | -------------------------------------------------------------------------------- /hoshino/modules/pcrclanbattle/clanbattle/argparse/argtype.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from hoshino import util 4 | from ..exception import ParseError 5 | from ..battlemaster import BattleMaster 6 | 7 | _unit_rate = {'': 1, 'k': 1000, 'w': 10000, '千': 1000, '万': 10000} 8 | _rex_dint = re.compile(r'^(\d+)([wk千万]?)$', re.I) 9 | _rex1_bcode = re.compile(r'^老?([1-5])王?$') 10 | _rex2_bcode = re.compile(r'^老?([一二三四五])王?$') 11 | _rex_rcode = re.compile(r'^[1-9]\d{0,2}$') 12 | 13 | def damage_int(x:str) -> int: 14 | x = util.normalize_str(x) 15 | if m := _rex_dint.match(x): 16 | x = int(m.group(1)) * _unit_rate[m.group(2).lower()] 17 | if x < 100000000: 18 | return x 19 | raise ParseError('伤害值不合法 伤害值应为小于一亿的非负整数') 20 | 21 | 22 | def boss_code(x:str) -> int: 23 | x = util.normalize_str(x) 24 | if m := _rex1_bcode.match(x): 25 | return int(m.group(1)) 26 | elif m := _rex2_bcode.match(x): 27 | return '零一二三四五'.find(m.group(1)) 28 | raise ParseError('Boss编号不合法 应为1-5的整数') 29 | 30 | 31 | def round_code(x:str) -> int: 32 | x = util.normalize_str(x) 33 | if _rex_rcode.match(x): 34 | return int(x) 35 | raise ParseError('周目数不合法 应为不大于999的非负整数') 36 | 37 | 38 | def server_code(x:str) -> int: 39 | x = util.normalize_str(x) 40 | if x in ('jp', '日', '日服'): 41 | return BattleMaster.SERVER_JP 42 | elif x in ('tw', '台', '台服'): 43 | return BattleMaster.SERVER_TW 44 | elif x in ('cn', '国', '国服', 'b', 'b服'): 45 | return BattleMaster.SERVER_CN 46 | raise ParseError('未知服务器地区 请用jp/tw/cn') 47 | 48 | 49 | def server_name(x:int) -> str: 50 | if x == BattleMaster.SERVER_JP: 51 | return 'jp' 52 | elif x == BattleMaster.SERVER_TW: 53 | return 'tw' 54 | elif x == BattleMaster.SERVER_CN: 55 | return 'cn' 56 | else: 57 | return 'unknown' 58 | 59 | __all__ = [ 60 | 'damage_int', 'boss_code', 'round_code', 'server_code', 'server_name' 61 | ] 62 | -------------------------------------------------------------------------------- /hoshino/modules/pcrclanbattle/clanbattle/dao/__init__.py: -------------------------------------------------------------------------------- 1 | # do nothing -------------------------------------------------------------------------------- /hoshino/modules/pcrclanbattle/clanbattle/exception.py: -------------------------------------------------------------------------------- 1 | class ClanBattleError(Exception): 2 | def __init__(self, msg, *msgs): 3 | self._msgs = [msg, *msgs] 4 | 5 | def __str__(self): 6 | return '\n'.join(self._msgs) 7 | 8 | @property 9 | def message(self): 10 | return str(self) 11 | 12 | def append(self, msg:str): 13 | self._msgs.append(msg) 14 | 15 | 16 | class ParseError(ClanBattleError): 17 | pass 18 | 19 | 20 | class NotFoundError(ClanBattleError): 21 | pass 22 | 23 | 24 | class AlreadyExistError(ClanBattleError): 25 | pass 26 | 27 | 28 | class PermissionDeniedError(ClanBattleError): 29 | pass 30 | 31 | 32 | class DatabaseError(ClanBattleError): 33 | pass 34 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/.gitignore: -------------------------------------------------------------------------------- 1 | priconne_data.py 2 | gacha/config.json -------------------------------------------------------------------------------- /hoshino/modules/priconne/__init__.py: -------------------------------------------------------------------------------- 1 | # 数据初始化:拷贝sample 2 | import shutil 3 | import os 4 | pcrdatapath=os.path.join(os.path.dirname(__file__),'priconne_data.py') 5 | jsonpath=os.path.join(os.path.dirname(__file__),'gacha','config.json') 6 | spcrdatapath=os.path.join(os.path.dirname(__file__),'priconne_data_sample.py') 7 | sjsonpath=os.path.join(os.path.dirname(__file__),'gacha','config_sample.json') 8 | if not os.path.exists(pcrdatapath): 9 | shutil.copy(spcrdatapath,pcrdatapath) 10 | if not os.path.exists(jsonpath): 11 | shutil.copy(sjsonpath,jsonpath) -------------------------------------------------------------------------------- /hoshino/modules/priconne/arena/README.md: -------------------------------------------------------------------------------- 1 | 本模块基于 0皆无0(NGA uid=60429400)dalao的[PCR姬器人:可可萝·Android](https://bbs.nga.cn/read.php?tid=18434108),移植至nonebot框架而成。 2 | 3 | 重构 by IceCoffee 4 | 5 | 源代码的使用已获原作者授权。 6 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/arena/__init__.py: -------------------------------------------------------------------------------- 1 | from .arena import refresh_quick_key_dic, do_query 2 | from ..chara import Chara 3 | import re 4 | import time 5 | from collections import defaultdict 6 | 7 | from nonebot import CommandSession, MessageSegment, get_bot 8 | from hoshino.util import silence, concat_pic, pic2b64, FreqLimiter 9 | from hoshino.service import Service, Privilege as Priv 10 | 11 | sv = Service('pcr-arena', manage_priv=Priv.SUPERUSER) 12 | 13 | 14 | lmt = FreqLimiter(5) 15 | 16 | aliases = ('怎么拆', '怎么解', '怎么打', '如何拆', '如何解', '如何打', 17 | '怎麼拆', '怎麼解', '怎麼打', 'jjc查询', 'jjc查詢') 18 | aliases_b = tuple('b' + a for a in aliases) + tuple('B' + 19 | a for a in aliases)+tuple('国' + a for a in aliases) 20 | aliases_tw = tuple('台' + a for a in aliases) 21 | aliases_jp = tuple('日' + a for a in aliases) 22 | 23 | 24 | @sv.on_command('竞技场查询', aliases=aliases, only_to_me=False, can_private=1) 25 | async def arena_query(session: CommandSession): 26 | await _arena_query(session, region=1) 27 | 28 | 29 | @sv.on_command('b竞技场查询', aliases=aliases_b, only_to_me=False, can_private=1) 30 | async def arena_query_b(session: CommandSession): 31 | await _arena_query(session, region=2) 32 | 33 | 34 | @sv.on_command('台竞技场查询', aliases=aliases_tw, only_to_me=False, can_private=1) 35 | async def arena_query_tw(session: CommandSession): 36 | await _arena_query(session, region=3) 37 | 38 | 39 | @sv.on_command('日竞技场查询', aliases=aliases_jp, only_to_me=False, can_private=1) 40 | async def arena_query_jp(session: CommandSession): 41 | await _arena_query(session, region=4) 42 | 43 | 44 | async def _arena_query(session: CommandSession, region: int): 45 | 46 | refresh_quick_key_dic() 47 | uid = session.ctx['user_id'] 48 | 49 | if not lmt.check(uid): 50 | session.finish('您查询得过于频繁,请稍等片刻', at_sender=True) 51 | lmt.start_cd(uid) 52 | 53 | # 处理输入数据 54 | argv = session.current_arg_text.strip() 55 | argv = re.sub(r'[??,,_]', '', argv) 56 | defen, unknown = Chara.parse_team(argv) 57 | 58 | if not defen: 59 | session.finish('请输入防守方角色', at_sender=True) 60 | if 5 < len(defen): 61 | session.finish('编队不能多于5名角色', at_sender=True) 62 | if len(defen) < 5: 63 | session.finish('由于pcrdfans.com的限制,编队必须为5个角色', at_sender=True) 64 | if len(defen) != len(set(defen)): 65 | await session.finish('编队中出现重复角色', at_sender=True) 66 | if 1004 in defen: 67 | await session.send('\n⚠️您正在查询普通版炸弹人\n※万圣版可用万圣炸弹人/瓜炸等别称', at_sender=True) 68 | # 执行查询 69 | sv.logger.info('Doing query...') 70 | try: 71 | res = await do_query(defen, uid, region) 72 | except TypeError: 73 | session.finish( 74 | '查询出错,请再次查询\n如果多次查询失败,请先移步pcrdfans.com进行查询,并可联系维护组', at_sender=True) 75 | sv.logger.info('Got response!') 76 | 77 | # 处理查询结果 78 | if res == 117: 79 | session.finish('高峰期bot限流,请移步pcrdfans.com查询。', at_sender=True) 80 | if res is None: 81 | session.finish( 82 | '查询出错,请再次查询\n如果多次查询失败,请先移步pcrdfans.com进行查询,并可联系维护组', at_sender=True) 83 | if not len(res): 84 | session.finish( 85 | '抱歉没有查询到解法\n※没有作业说明随便拆 发挥你的想象力~★\n作业上传请前往pcrdfans.com', at_sender=True) 86 | res = res[:min(6, len(res))] # 限制显示数量,截断结果 87 | 88 | # 发送回复 89 | 90 | sv.logger.info('Arena generating picture...') 91 | atk_team = [Chara.gen_team_pic(team=entry['atk'], text="\n".join([ 92 | f"赞 {entry['up']}", 93 | f"踩 {entry['down']}", 94 | ])) for entry in res] 95 | atk_team = concat_pic(atk_team) 96 | atk_team = pic2b64(atk_team) 97 | atk_team = str(MessageSegment.image(atk_team)) 98 | sv.logger.info('Arena picture ready!') 99 | 100 | defen = [Chara.fromid(x).name for x in defen] 101 | defen = f"防守方【{' '.join(defen)}】" 102 | 103 | msg = [ 104 | defen, 105 | str(atk_team), 106 | ] 107 | msg.append('Support by pcrdfans_com') 108 | sv.logger.debug('Arena sending result...') 109 | if unknown: 110 | await session.send(f'无法识别"{unknown}",请仅输入角色名规范查询') 111 | await session.send('\n'.join(msg), at_sender=1) 112 | sv.logger.debug('Arena result sent!') 113 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/arena/arena.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import base64 4 | 5 | try: 6 | import ujson as json 7 | except: 8 | import json 9 | 10 | from hoshino import aiohttpx, aiorequests, util,logger 11 | from ..chara import Chara 12 | 13 | 14 | 15 | ''' 16 | Database for arena likes & dislikes 17 | DB is a dict like: { 'md5_id': {'like': set(qq), 'dislike': set(qq)} } 18 | ''' 19 | DB_PATH = os.path.expanduser('~/.hoshino/arena_db.json') 20 | DB = {} 21 | try: 22 | with open(DB_PATH, encoding='utf8') as f: 23 | DB = json.load(f) 24 | for k in DB: 25 | DB[k] = { 26 | 'like': set(DB[k].get('like', set())), 27 | 'dislike': set(DB[k].get('dislike', set())) 28 | } 29 | except FileNotFoundError: 30 | logger.warning(f'arena_db.json not found, will create when needed.') 31 | 32 | 33 | def dump_db(): 34 | ''' 35 | Dump the arena databese. 36 | json do not accept set object, this function will help to convert. 37 | ''' 38 | j = {} 39 | for k in DB: 40 | j[k] = { 41 | 'like': list(DB[k].get('like', set())), 42 | 'dislike': list(DB[k].get('dislike', set())) 43 | } 44 | with open(DB_PATH, 'w', encoding='utf8') as f: 45 | json.dump(j, f, ensure_ascii=False) 46 | 47 | 48 | _last_query_time = 0 49 | quick_key_dic = {} # {quick_key: true_id} 50 | 51 | 52 | def refresh_quick_key_dic(): 53 | global _last_query_time 54 | now = time.time() 55 | if now - _last_query_time > 300: 56 | quick_key_dic.clear() 57 | _last_query_time = now 58 | 59 | 60 | def gen_quick_key(true_id: str, user_id: int) -> str: 61 | qkey = int(true_id[-6:], 16) 62 | while qkey in quick_key_dic and quick_key_dic[qkey] != true_id: 63 | qkey = (qkey + 1) & 0xffffff 64 | quick_key_dic[qkey] = true_id 65 | mask = user_id & 0xffffff 66 | qkey ^= mask 67 | return base64.b32encode(qkey.to_bytes(3, 'little')).decode()[:5] 68 | 69 | 70 | 71 | 72 | 73 | def __get_auth_key(): 74 | config = util.load_config(__file__) 75 | return config["AUTH_KEY"] 76 | 77 | 78 | async def do_query(id_list, user_id, region=1): 79 | id_list = [x * 100 + 1 for x in id_list] 80 | header = { 81 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36', 82 | 'authorization': __get_auth_key() 83 | } 84 | payload = {"_sign": "a", "def": id_list, "nonce": "a", 85 | "page": 1, "sort": 1, "ts": int(time.time()), "region": region} 86 | logger.debug(f'Arena query {payload=}') 87 | try: 88 | resp = await aiorequests.post('https://api.pcrdfans.com/x/v1/search', timeout=5, headers=header, json=payload) 89 | res = await resp.json() 90 | logger.debug(f'len(res)={len(res)}') 91 | except Exception as e: 92 | logger.exception(e) 93 | return None 94 | if res['code'] == 117: 95 | return 117 96 | if res['code']: 97 | logger.error(f"Arena query failed.\nResponse={res}\nPayload={payload}") 98 | return None 99 | 100 | res = res['data']['result'] 101 | res = [ 102 | { 103 | 'qkey': gen_quick_key(entry['id'], user_id), 104 | 'atk': [Chara(c['id'] // 100, c['star'], c['equip']) for c in entry['atk']], 105 | 'up': entry['up'], 106 | 'down': entry['down'], 107 | } for entry in res 108 | ] 109 | return res 110 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/arena_reminder.py: -------------------------------------------------------------------------------- 1 | from hoshino.service import Service 2 | 3 | sv = Service('pcr-arena-reminder') 4 | svjp = Service('pcr-arena-reminder-jp', enable_on_default=False) 5 | msg = '骑士君~准备好背刺了吗?' 6 | 7 | @sv.scheduled_job('cron', hour='14', minute='45') 8 | async def pcr_reminder(): 9 | await sv.broadcast(msg, 'pcr-reminder', 0.2) 10 | 11 | @svjp.scheduled_job('cron', hour='13', minute='45') 12 | async def pcr_reminder_jp(): 13 | await svjp.broadcast(msg, 'pcr-reminder-jp', 0.2) 14 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/cherugo.py: -------------------------------------------------------------------------------- 1 | """切噜语(ちぇる語, Language Cheru)转换 2 | 3 | 定义: 4 | W_cheru = '切' ^ `CHERU_SET`+ 5 | 切噜词均以'切'开头,可用字符集为`CHERU_SET` 6 | 7 | L_cheru = {W_cheru ∪ `\\W`}* 8 | 切噜语由切噜词与标点符号连接而成 9 | """ 10 | 11 | import re 12 | from itertools import zip_longest 13 | from nonebot.message import escape 14 | from hoshino import Service, CommandSession,util,R 15 | 16 | sv = Service('pcr-cherugo') 17 | qksimg = R.img('qksimg.jpg').cqcode 18 | 19 | CHERU_SET = '切卟叮咧哔唎啪啰啵嘭噜噼巴拉蹦铃' 20 | CHERU_DIC = { c: i for i, c in enumerate(CHERU_SET) } 21 | ENCODING = 'gb18030' 22 | rex_split = re.compile(r'\b', re.U) 23 | rex_word = re.compile(r'^\w+$', re.U) 24 | rex_cheru_word:re.Pattern = re.compile(rf'切[{CHERU_SET}]+', re.U) 25 | 26 | def grouper(iterable, n, fillvalue=None): 27 | args = [iter(iterable)] * n 28 | return zip_longest(*args, fillvalue=fillvalue) 29 | 30 | def word2cheru(w:str) -> str: 31 | c = ['切'] 32 | for b in w.encode(ENCODING): 33 | c.append(CHERU_SET[b & 0xf]) 34 | c.append(CHERU_SET[(b >> 4) & 0xf]) 35 | return ''.join(c) 36 | 37 | def cheru2word(c:str) -> str: 38 | if not c[0] == '切' or len(c) < 2: 39 | return c 40 | b = [] 41 | for b1, b2 in grouper(c[1:], 2, '切'): 42 | x = CHERU_DIC.get(b2, 0) 43 | x = x << 4 | CHERU_DIC.get(b1, 0) 44 | b.append(x) 45 | return bytes(b).decode(ENCODING, 'replace') 46 | 47 | def str2cheru(s:str) -> str: 48 | c = [] 49 | for w in rex_split.split(s): 50 | if rex_word.search(w): 51 | w = word2cheru(w) 52 | c.append(w) 53 | return ''.join(c) 54 | 55 | def cheru2str(c:str) -> str: 56 | return rex_cheru_word.sub(lambda w: cheru2word(w.group()), c) 57 | 58 | 59 | @sv.on_command('切噜一下') 60 | async def cherulize(session:CommandSession): 61 | s = session.current_arg_text 62 | if 'granbluefantasy.jp' in s: 63 | session.finish(f'骑空士爬\n{qksimg}', at_sender=True) 64 | if len(s) > 500: 65 | session.finish('切、切噜太长切不动勒切噜噜...', at_sender=True) 66 | session.finish('切噜~♪' + str2cheru(s)) 67 | 68 | @sv.on_rex(r'^切噜~♪', normalize=False) 69 | async def decherulize(bot, ctx, match): 70 | s = ctx['plain_text'][4:] 71 | if len(s) > 1501: 72 | await bot.send(ctx, '切、切噜太长切不动勒切噜噜...', at_sender=True) 73 | return 74 | msg = '的切噜噜是:\n' + escape(cheru2str(s)) 75 | if 'granbluefantasy.jp' in msg: 76 | await bot.send(ctx,f'骑空士爬\n{qksimg}', at_sender=True) 77 | await util.silence(ctx, 60) 78 | return 79 | await bot.send(ctx, msg, at_sender=True) 80 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/comic.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import random 4 | import asyncio 5 | from urllib.parse import urljoin, urlparse, parse_qs 6 | try: 7 | import ujson as json 8 | except: 9 | import json 10 | 11 | from nonebot import CQHttpError, NLPSession 12 | 13 | from hoshino import aiorequests 14 | from hoshino.res import R 15 | from hoshino.service import Service 16 | 17 | sv = Service('pcr-comic') 18 | 19 | def load_index(): 20 | with open(R.get('img/priconne/comic/index.json').path, encoding='utf8') as f: 21 | return json.load(f) 22 | 23 | def get_pic_name(id_): 24 | pre = 'episode_' 25 | end = '.png' 26 | return f'{pre}{id_}{end}' 27 | 28 | @sv.on_rex(r'^官漫\s*(\d{0,4})', normalize=False) 29 | async def comic(bot, ctx, match): 30 | episode = match.group(1) 31 | if not episode: 32 | await bot.send(ctx, '请输入漫画集数 如:官漫132', at_sender=True) 33 | return 34 | index = load_index() 35 | if episode not in index: 36 | await bot.send(ctx, f'未查找到第{episode}话,敬请期待官方更新', at_sender=True) 37 | return 38 | title = index[episode]['title'] 39 | pic = R.img('priconne/comic/', get_pic_name(episode)).cqcode 40 | msg = f'プリンセスコネクト!Re:Dive公式4コマ\n第{episode}話 {title}\n{pic}' 41 | await bot.send(ctx, msg, at_sender=True) 42 | 43 | 44 | async def download_img(save_path, link): 45 | ''' 46 | 从link下载图片保存至save_path(目录+文件名) 47 | 会覆盖原有文件,需保证目录存在 48 | ''' 49 | sv.logger.info(f'download_img from {link}') 50 | resp = await aiorequests.get(link, stream=True) 51 | sv.logger.info(f'status_code={resp.status_code}') 52 | if 200 == resp.status_code: 53 | if re.search(r'image', resp.headers['content-type'], re.I): 54 | sv.logger.info(f'is image, saving to {save_path}') 55 | with open(save_path, 'wb') as f: 56 | f.write(await resp.content) 57 | sv.logger.info('saved!') 58 | 59 | 60 | async def download_comic(id_): 61 | ''' 62 | 下载指定id的官方四格漫画,同时更新漫画目录index.json 63 | episode_num可能会小于id 64 | ''' 65 | base = 'https://comic.priconne-redive.jp/api/detail/' 66 | save_dir = R.img('priconne/comic/').path 67 | index = load_index() 68 | 69 | # 先从api获取detail,其中包含图片真正的链接 70 | sv.logger.info(f'getting comic {id_} ...') 71 | url = base + id_ 72 | sv.logger.info(f'url={url}') 73 | resp = await aiorequests.get(url) 74 | sv.logger.info(f'status_code={resp.status_code}') 75 | if 200 != resp.status_code: 76 | return 77 | data = await resp.json() 78 | data = data[0] 79 | 80 | episode = data['episode_num'] 81 | title = data['title'] 82 | link = data['cartoon'] 83 | index[episode] = {'title': title, 'link': link} 84 | sv.logger.info(f'episode={index[episode]}') 85 | 86 | # 下载图片并保存 87 | await download_img(os.path.join(save_dir, get_pic_name(episode)), link) 88 | 89 | # 保存官漫目录信息 90 | with open(os.path.join(save_dir, 'index.json'), 'w', encoding='utf8') as f: 91 | json.dump(index, f, ensure_ascii=False) 92 | 93 | 94 | @sv.scheduled_job('cron', minute='*/20', second='25') 95 | async def update_seeker(): 96 | ''' 97 | 轮询官方四格漫画更新 98 | 若有更新则推送至订阅群 99 | ''' 100 | index_api = 'https://comic.priconne-redive.jp/api/index' 101 | index = load_index() 102 | 103 | # 获取最新漫画信息 104 | resp = await aiorequests.get(index_api, timeout=10) 105 | data = await resp.json() 106 | id_ = data['latest_cartoon']['id'] 107 | episode = data['latest_cartoon']['episode_num'] 108 | title = data['latest_cartoon']['title'] 109 | 110 | # 检查是否已在目录中 111 | # 同一episode可能会被更新为另一张图片(官方修正),此时episode不变而id改变 112 | # 所以需要两步判断 113 | if episode in index: 114 | qs = urlparse(index[episode]['link']).query 115 | old_id = parse_qs(qs)['id'][0] 116 | if id_ == old_id: 117 | sv.logger.info('未检测到官漫更新') 118 | return 119 | 120 | # 确定已有更新,下载图片 121 | sv.logger.info(f'发现更新 id={id_}') 122 | await download_comic(id_) 123 | 124 | # 推送至各个订阅群 125 | pic = R.img('priconne/comic', get_pic_name(episode)).cqcode 126 | msg = f'プリンセスコネクト!Re:Dive公式4コマ更新!\n第{episode}話 {title}\n{pic}' 127 | await sv.broadcast(msg, 'PCR官方四格', 0.5) 128 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/gacha/README.md: -------------------------------------------------------------------------------- 1 | 本模块基于 0皆无0(NGA uid=60429400)dalao的[PCR姬器人:可可萝·Android](https://bbs.nga.cn/read.php?tid=18434108),移植至nonebot框架而成。 2 | 3 | 重构 by IceCoffee 4 | 5 | 源代码的使用已获原作者授权。 6 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/gacha/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from collections import defaultdict 3 | try: 4 | import ujson as json 5 | except: 6 | import json 7 | 8 | from hoshino import util 9 | from hoshino import MessageSegment, Service, Privilege as Priv, aiorequests 10 | from hoshino.util import silence, concat_pic, pic2b64, DailyNumberLimiter 11 | 12 | from .gacha import Gacha 13 | from ..chara import Chara 14 | 15 | sv = Service('gacha') 16 | jewel_limit = DailyNumberLimiter(7500) 17 | tenjo_limit = DailyNumberLimiter(1) 18 | 19 | 20 | JEWEL_EXCEED_NOTICE = f'您今天已经抽过{jewel_limit.max}钻了,欢迎明早5点后再来!' 21 | TENJO_EXCEED_NOTICE = f'您今天已经抽过{tenjo_limit.max}张天井券了,欢迎明早5点后再来!' 22 | POOL = ('MIX', 'JP', 'TW', 'BL') 23 | DEFAULT_POOL = POOL[0] 24 | 25 | _pool_config_file = os.path.expanduser('~/.hoshino/group_pool_config.json') 26 | _group_pool = {} 27 | try: 28 | with open(_pool_config_file, encoding='utf8') as f: 29 | _group_pool = json.load(f) 30 | except FileNotFoundError as e: 31 | sv.logger.warning( 32 | 'group_pool_config.json not found, will create when needed.') 33 | _group_pool = defaultdict(lambda: DEFAULT_POOL, _group_pool) 34 | 35 | 36 | def dump_pool_config(): 37 | with open(_pool_config_file, 'w', encoding='utf8') as f: 38 | json.dump(_group_pool, f, ensure_ascii=False) 39 | 40 | 41 | gacha_10_aliases = ('抽十连', '十连', '十连!', '十连抽', '来个十连', '来发十连', '来次十连', '抽个十连', '抽发十连', '抽次十连', '十连扭蛋', '扭蛋十连', 42 | '10连', '10连!', '10连抽', '来个10连', '来发10连', '来次10连', '抽个10连', '抽发10连', '抽次10连', '10连扭蛋', '扭蛋10连', 43 | '十連', '十連!', '十連抽', '來個十連', '來發十連', '來次十連', '抽個十連', '抽發十連', '抽次十連', '十連轉蛋', '轉蛋十連', 44 | '10連', '10連!', '10連抽', '來個10連', '來發10連', '來次10連', '抽個10連', '抽發10連', '抽次10連', '10連轉蛋', '轉蛋10連') 45 | gacha_1_aliases = ('单抽', '单抽!', '来发单抽', '来个单抽', '来次单抽', '扭蛋单抽', '单抽扭蛋', 46 | '單抽', '單抽!', '來發單抽', '來個單抽', '來次單抽', '轉蛋單抽', '單抽轉蛋') 47 | gacha_300_aliases = ('抽一井', '来一井', '来发井', '抽发井', '天井扭蛋', 48 | '扭蛋天井', '天井轉蛋', '轉蛋天井', '抽井') 49 | 50 | 51 | _collection_path = os.path.expanduser('~/.hoshino/collections') 52 | if not os.path.exists(_collection_path): 53 | os.mkdir(_collection_path) 54 | 55 | 56 | def load_user_collection(uid: str): 57 | collectionfile = os.path.join(_collection_path, f'{uid}.json') 58 | try: 59 | with open(collectionfile, encoding='utf8') as fp: 60 | ucollection = json.load(fp) 61 | return ucollection 62 | except: 63 | f = open(collectionfile, 'w', encoding='utf8') 64 | ucollection = {} 65 | ucollection[uid] = [] 66 | json.dump(ucollection, f, ensure_ascii=False) 67 | f.close() 68 | return ucollection 69 | 70 | 71 | def dump_user_collection(uid: str, ucollection): 72 | with open(os.path.join(_collection_path, f'{uid}.json'), 'w', encoding='utf8') as f: 73 | json.dump(ucollection, f, ensure_ascii=False) 74 | f.close() 75 | 76 | 77 | @sv.on_command('卡池资讯', aliases=('查看卡池', '看看卡池', '康康卡池', '卡池資訊', '看看up', 'kkup','看看UP'), only_to_me=False,can_private=1) 78 | async def gacha_info(session): 79 | gid = session.ctx.get('group_id',0) 80 | gacha = Gacha(_group_pool[str(gid)]) if gid!=0 else Gacha('MIX') 81 | up_chara = gacha.up 82 | if sv.bot.config.IS_CQPRO: 83 | up_chara = map(lambda x: str( 84 | Chara.fromname(x).icon.cqcode) + x, up_chara) 85 | up_chara = '\n'.join(up_chara) 86 | await session.send(f"本期卡池主打的角色:\n{up_chara}\nUP角色合计={(gacha.up_prob/10):.1f}% 3★出率={(gacha.s3_prob)/10:.1f}%") 87 | 88 | 89 | POOL_NAME_TIP = '请选择以下卡池\n> 选择卡池 jp\n> 选择卡池 tw\n> 选择卡池 bilibili\n> 选择卡池 mix' 90 | @sv.on_command('切换卡池', aliases=('选择卡池', '切換卡池', '選擇卡池'), only_to_me=False, perm=Priv.ADMIN) 91 | async def set_pool(session): 92 | name = util.normalize_str(session.current_arg_text) 93 | if not name: 94 | session.finish(POOL_NAME_TIP, at_sender=True) 95 | elif name in ('b', 'b服', 'bl', 'bilibili', '国', '国服', 'cn'): 96 | name = 'BL' 97 | elif name in ('台', '台服', 'tw', 'sonet'): 98 | name = 'TW' 99 | elif name in ('日', '日服', 'jp', 'cy', 'cygames'): 100 | name = 'JP' 101 | elif name in ('混', '混合', 'mix'): 102 | name = 'MIX' 103 | else: 104 | session.finish(f'未知服务器地区 {POOL_NAME_TIP}', at_sender=True) 105 | gid = str(session.ctx['group_id']) 106 | _group_pool[gid] = name 107 | dump_pool_config() 108 | await session.send(f'卡池已切换为{name}池', at_sender=True) 109 | await gacha_info(session) 110 | 111 | 112 | async def check_jewel_num(session): 113 | uid = session.ctx['user_id'] 114 | if not jewel_limit.check(uid): 115 | await session.finish(JEWEL_EXCEED_NOTICE, at_sender=True) 116 | 117 | 118 | async def check_tenjo_num(session): 119 | uid = session.ctx['user_id'] 120 | if not tenjo_limit.check(uid): 121 | await session.finish(TENJO_EXCEED_NOTICE, at_sender=True) 122 | 123 | 124 | @sv.on_command('仓库', aliases=('看看仓库', '我的仓库'),can_private=1) 125 | async def show_collection(session): 126 | uid = str(session.ctx['user_id']) 127 | ucollection = load_user_collection(uid) 128 | uset = set(ucollection[uid]) 129 | uset.discard("未知角色") 130 | uset = list(uset) 131 | length = len(uset) 132 | if length <= 0: 133 | session.finish('您的仓库为空,请多多抽卡哦~', at_sender=True) 134 | else: 135 | result = list(map(lambda x: Chara.fromname(x), uset)) 136 | step = 6 137 | pics = [] 138 | for i in range(0, length, step): 139 | j = min(length, i + step) 140 | pics.append(Chara.gen_team_pic( 141 | result[i:j], star_slot_verbose=False)) 142 | res = concat_pic(pics) 143 | res = pic2b64(res) 144 | res = MessageSegment.image(res) 145 | msg = [ 146 | f'仅展示三星角色~', 147 | f'{res}', 148 | f'您共有{length}个三星角色~' 149 | ] 150 | ucollection[uid] = list(uset) 151 | dump_user_collection(uid, ucollection) 152 | await session.send('\n'.join(msg), at_sender=True) 153 | 154 | 155 | @sv.on_command('gacha_1', aliases=gacha_1_aliases, only_to_me=False,can_private=1) 156 | async def gacha_1(session): 157 | await check_jewel_num(session) 158 | uid = session.ctx['user_id'] 159 | jewel_limit.increase(uid, 150) 160 | uid = str(session.ctx['user_id']) 161 | ucollection = load_user_collection(uid) 162 | uset = set(ucollection[uid]) 163 | gid = session.ctx.get('group_id',0) 164 | gacha = Gacha(_group_pool[str(gid)]) if gid!=0 else Gacha('MIX') 165 | chara, hiishi = gacha.gacha_one( 166 | gacha.up_prob, gacha.s3_prob, gacha.s2_prob) 167 | silence_time = hiishi * 60 168 | if chara.star == 3: 169 | uset.add(chara.name) 170 | ucollection[uid] = list(uset) 171 | res = f'{chara.name} {"★"*chara.star}' 172 | if sv.bot.config.IS_CQPRO: 173 | res = f'{chara.icon.cqcode} {res}' 174 | dump_user_collection(uid, ucollection) 175 | if gid!=0: 176 | await silence(session.ctx, silence_time) 177 | await session.send(f'素敵な仲間が増えますよ!\n{res}', at_sender=True) 178 | 179 | 180 | @sv.on_command('gacha_10', aliases=gacha_10_aliases, only_to_me=False,can_private=1) 181 | async def gacha_10(session): 182 | SUPER_LUCKY_LINE = 170 183 | await check_jewel_num(session) 184 | uid = session.ctx['user_id'] 185 | jewel_limit.increase(uid, 1500) 186 | uid = str(session.ctx['user_id']) 187 | ucollection = load_user_collection(uid) 188 | uset = set(ucollection[uid]) 189 | gid = session.ctx.get('group_id',0) 190 | gacha = Gacha(_group_pool[str(gid)]) if gid!=0 else Gacha('MIX') 191 | result, hiishi = gacha.gacha_ten() 192 | silence_time = hiishi * 6 if hiishi < SUPER_LUCKY_LINE else hiishi * 60 193 | for c in result: 194 | if 3 == c.star: 195 | uset.add(c.name) 196 | ucollection[uid] = list(uset) 197 | if sv.bot.config.IS_CQPRO: 198 | res1 = Chara.gen_team_pic(result[:5], star_slot_verbose=False) 199 | res2 = Chara.gen_team_pic(result[5:], star_slot_verbose=False) 200 | res = concat_pic([res1, res2]) 201 | res = pic2b64(res) 202 | res = MessageSegment.image(res) 203 | result = [f'{c.name}{"★"*c.star}' for c in result] 204 | res1 = ' '.join(result[0:5]) 205 | res2 = ' '.join(result[5:]) 206 | res = f'{res}\n{res1}\n{res2}' 207 | else: 208 | result = [f'{c.name}{"★"*c.star}' for c in result] 209 | res1 = ' '.join(result[0:5]) 210 | res2 = ' '.join(result[5:]) 211 | res = f'{res1}\n{res2}' 212 | dump_user_collection(uid, ucollection) 213 | if hiishi >= SUPER_LUCKY_LINE: 214 | await session.send('恭喜海豹!おめでとうございます!') 215 | await session.send(f'素敵な仲間が増えますよ!\n{res}', at_sender=True) 216 | if gid!=0: 217 | await silence(session.ctx, silence_time) 218 | 219 | 220 | @sv.on_command('gacha_300', aliases=gacha_300_aliases, only_to_me=False,can_private=1) 221 | async def gacha_300(session): 222 | await check_tenjo_num(session) 223 | uid = session.ctx['user_id'] 224 | tenjo_limit.increase(uid) 225 | uid = str(session.ctx['user_id']) 226 | ucollection = load_user_collection(uid) 227 | uset = set(ucollection[uid]) 228 | gid = session.ctx.get('group_id',0) 229 | gacha = Gacha(_group_pool[str(gid)]) if gid!=0 else Gacha('MIX') 230 | result,up = gacha.gacha_tenjou() 231 | s3 = len(result['s3']) 232 | s2 = len(result['s2']) 233 | s1 = len(result['s1']) 234 | res = result['s3'] 235 | for c in res: 236 | uset.add(c.name) 237 | ucollection[uid] = list(uset) 238 | dump_user_collection(uid, ucollection) 239 | lenth = len(res) 240 | if lenth <= 0: 241 | res = "竟...竟然没有3★?!" 242 | else: 243 | step = 4 244 | pics = [] 245 | for i in range(0, lenth, step): 246 | j = min(lenth, i + step) 247 | pics.append(Chara.gen_team_pic(res[i:j], star_slot_verbose=False)) 248 | res = concat_pic(pics) 249 | res = pic2b64(res) 250 | res = MessageSegment.image(res) 251 | 252 | msg = [ 253 | f"\n素敵な仲間が増えますよ! {res}", 254 | f"★★★×{s3} ★★×{s2} ★×{s1}", 255 | f"获得{up}个up角色与女神秘石×{50*(s3) + 10*s2 + s1}!\n第{result['first_up_pos']}抽首次获得up角色" if up else f"获得女神秘石{50*(up+s3) + 10*s2 + s1}个!" 256 | ] 257 | 258 | if up == 0 and s3 == 0: 259 | msg.append("太惨了,咱们还是退款删游吧...") 260 | elif up == 0 and s3 > 7: 261 | msg.append("up呢?我的up呢?") 262 | elif up == 0 and s3 <= 3: 263 | msg.append("这位酋长,大月卡考虑一下?") 264 | elif up == 0: 265 | msg.append("据说天井的概率只有12.16%") 266 | elif up <= 2: 267 | if result['first_up_pos'] < 50: 268 | msg.append("你的喜悦我收到了,滚去喂鲨鱼吧!") 269 | elif result['first_up_pos'] < 100: 270 | msg.append("已经可以了,您已经很欧了") 271 | elif result['first_up_pos'] > 290: 272 | msg.append("标 准 结 局") 273 | elif result['first_up_pos'] > 250: 274 | msg.append("补井还是不补井,这是一个问题...") 275 | else: 276 | msg.append("期望之内,亚洲水平") 277 | elif up == 3: 278 | msg.append("抽井母五一气呵成!您就是欧洲人?") 279 | elif up >= 4: 280 | msg.append("记忆碎片一大堆!您是托吧?") 281 | await session.send('\n'.join(msg), at_sender=True) 282 | silence_time = (100*up+50*s3 + 10*s2 + s1) * 1 283 | if gid!=0: 284 | await silence(session.ctx, silence_time) 285 | 286 | 287 | @sv.on_rex(r'^氪金$', normalize=False) 288 | async def kakin(bot, ctx, match): 289 | if ctx['user_id'] not in bot.config.SUPERUSERS: 290 | return 291 | count = 0 292 | for m in ctx['message']: 293 | if m.type == 'at' and m.data['qq'] != 'all': 294 | uid = int(m.data['qq']) 295 | jewel_limit.reset(uid) 296 | tenjo_limit.reset(uid) 297 | count += 1 298 | if count: 299 | await bot.send(ctx, f"已为{count}位用户充值完毕!谢谢惠顾~") 300 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/gacha/config_sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "MIX": { 3 | "up_prob": 7, 4 | "s3_prob": 25, 5 | "s2_prob": 180, 6 | "up": ["七七香(夏日)", "琪爱儿"], 7 | "_comment": "star3 仅填3星常驻角色。不要填UP角,否则出率会偏高", 8 | "star3": [ 9 | "杏奈", "真步", "璃乃", "初音", "霞", "伊绪", 10 | "咲恋", "望", "妮诺", "秋乃", "镜华", "智", "真琴", 11 | "伊莉亚", "纯", "静流", "莫妮卡", "流夏", "吉塔", 12 | "亚里莎", "空花(大江户)", "妮诺(大江户)", 13 | "克萝依", "碧(插班生)", "美美(万圣节)", "露娜", 14 | "伊莉亚(圣诞节)", "霞(魔法少女)", "安", "古蕾娅", 15 | "铃(游骑兵)", "真阳(游骑兵)", "璃乃(奇境)", "祈梨", "优妮" 16 | ], 17 | "other_normal_star3": [], 18 | "star2": [ 19 | "茉莉", "茜里", "宫子", "雪", "七七香", "美里", 20 | "铃奈", "香织", "美美", "绫音", "铃", "惠理子", 21 | "忍", "真阳", "栞", "千歌", "空花", "珠希", "美冬", 22 | "深月", "纺希" 23 | ], 24 | "star1": [ 25 | "日和", "怜", "禊", "胡桃", "依里", "铃莓", 26 | "优花梨", "碧", "美咲", "莉玛", "步未" 27 | ] 28 | }, 29 | "TW": { 30 | "up_prob": 7, 31 | "s3_prob": 25, 32 | "s2_prob": 180, 33 | "up": ["琪爱儿"], 34 | "_comment": "star3 仅填3星常驻角色。不要填UP角,否则出率会偏高", 35 | "star3": [ 36 | "杏奈", "真步", "璃乃", "初音", "霞", "伊绪", 37 | "咲恋", "望", "妮诺", "秋乃", "镜华", "智", "真琴", 38 | "伊莉亚", "纯", "静流", "莫妮卡", "流夏", "吉塔", 39 | "亚里莎", "空花(大江户)", "妮诺(大江户)", "安", "古蕾娅", 40 | "克萝依", "碧(插班生)", "美美(万圣节)", "露娜", 41 | "伊莉亚(圣诞节)", "霞(魔法少女)", "优妮" 42 | ], 43 | "other_normal_star3": [], 44 | "star2": [ 45 | "茉莉", "茜里", "宫子", "雪", "七七香", "美里", 46 | "铃奈", "香织", "美美", "绫音", "铃", "惠理子", 47 | "忍", "真阳", "栞", "千歌", "空花", "珠希", "美冬", 48 | "深月", "纺希" 49 | ], 50 | "star1": [ 51 | "日和", "怜", "禊", "胡桃", "依里", "铃莓", 52 | "优花梨", "碧", "美咲", "莉玛", "步未" 53 | ] 54 | }, 55 | "JP": { 56 | "up_prob": 7, 57 | "s3_prob": 25, 58 | "s2_prob": 180, 59 | "up": ["七七香(夏日)"], 60 | "_comment": "star3 仅填3星常驻角色。不要填UP角,否则出率会偏高", 61 | "star3": [ 62 | "杏奈", "真步", "璃乃", "初音", "霞", "伊绪", 63 | "咲恋", "望", "妮诺", "秋乃", "镜华", "智", "真琴", 64 | "伊莉亚", "纯", "静流", "莫妮卡", "流夏", "吉塔", 65 | "亚里莎", "安", "古蕾娅", "空花(大江户)", "妮诺(大江户)", 66 | "克萝依", "碧(插班生)", "美美(万圣节)", "露娜", 67 | "伊莉亚(圣诞节)", "霞(魔法少女)", "优妮", "琪爱儿", 68 | "铃(游骑兵)", "真阳(游骑兵)", "璃乃(奇境)", "祈梨" 69 | ], 70 | "other_normal_star3": ["璃乃(奇境)"], 71 | "star2": [ 72 | "茉莉", "茜里", "宫子", "雪", "七七香", "美里", 73 | "铃奈", "香织", "美美", "绫音", "铃", "惠理子", 74 | "忍", "真阳", "栞", "千歌", "空花", "珠希", "美冬", 75 | "深月", "纺希" 76 | ], 77 | "star1": [ 78 | "日和", "怜", "禊", "胡桃", "依里", "铃莓", 79 | "优花梨", "碧", "美咲", "莉玛", "步未" 80 | ] 81 | }, 82 | "BL": { 83 | "up_prob": 7, 84 | "s3_prob": 25, 85 | "s2_prob": 180, 86 | "up": ["望"], 87 | "_comment": "star3 仅填3星常驻角色。不要填UP角,否则出率会偏高", 88 | "star3": [ 89 | "杏奈", "璃乃", "镜华", "伊绪", 90 | "伊莉亚", "妮诺", "秋乃", "亚里莎", 91 | "静流", "莫妮卡", "吉塔", "纯", "初音" 92 | ], 93 | "other_normal_star3": [ 94 | "霞", "智", "伊莉亚", "纯", "流夏", "安", "古蕾娅", "空花(大江户)", "妮诺(大江户)", 95 | "克萝依", "碧(插班生)", "美美(万圣节)", "露娜", 96 | "伊莉亚(圣诞节)", "霞(魔法少女)", "优妮", "琪爱儿" 97 | ], 98 | "star2": [ 99 | "茉莉", "茜里", "宫子", "雪", "铃", 100 | "铃奈", "香织", "美美", "绫音", "惠理子", 101 | "真阳", "栞", "千歌", "空花", "珠希", "美冬", 102 | "深月", "美里", "纺希" 103 | ], 104 | "star1": [ 105 | "日和", "怜", "禊", "胡桃", "依里", "铃莓", 106 | "优花梨", "碧", "美咲", "莉玛" 107 | ] 108 | } 109 | } -------------------------------------------------------------------------------- /hoshino/modules/priconne/gacha/gacha.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from hoshino import util 4 | from ..chara import Chara 5 | 6 | 7 | class Gacha(object): 8 | 9 | def __init__(self, pool_name:str="MIX"): 10 | super().__init__() 11 | self.load_pool(pool_name) 12 | 13 | 14 | def load_pool(self, pool_name:str): 15 | config = util.load_config(__file__) 16 | pool = config[pool_name] 17 | self.up_prob = pool["up_prob"] 18 | self.s3_prob = pool["s3_prob"] 19 | self.s2_prob = pool["s2_prob"] 20 | self.s1_prob = 1000 - self.s2_prob - self.s3_prob 21 | self.up = pool["up"] 22 | self.star3 = pool["star3"] 23 | self.star2 = pool["star2"] 24 | self.star1 = pool["star1"] 25 | 26 | 27 | def gacha_one(self, up_prob:int, s3_prob:int, s2_prob:int, s1_prob:int=None): 28 | ''' 29 | sx_prob: x星概率,要求和为1000 30 | up_prob: UP角色概率(从3星划出) 31 | up_chara: UP角色名列表 32 | 33 | return: (单抽结果:Chara, 秘石数:int) 34 | --------------------------- 35 | |up| | 20 | 78 | 36 | | *** | ** | * | 37 | --------------------------- 38 | ''' 39 | if s1_prob is None: 40 | s1_prob = 1000 - s3_prob - s2_prob 41 | total_ = s3_prob + s2_prob + s1_prob 42 | pick = random.randint(1, total_) 43 | if pick <= up_prob: 44 | return Chara.fromname(random.choice(self.up), 3), 100 45 | elif pick <= s3_prob: 46 | return Chara.fromname(random.choice(self.star3), 3), 50 47 | elif pick <= s2_prob + s3_prob: 48 | return Chara.fromname(random.choice(self.star2), 2), 10 49 | else: 50 | return Chara.fromname(random.choice(self.star1), 1), 1 51 | 52 | 53 | def gacha_ten(self): 54 | result = [] 55 | hiishi = 0 56 | up = self.up_prob 57 | s3 = self.s3_prob 58 | s2 = self.s2_prob 59 | s1 = 1000 - s3 - s2 60 | for _ in range(9): # 前9连 61 | c, y = self.gacha_one(up, s3, s2, s1) 62 | result.append(c) 63 | hiishi += y 64 | c, y = self.gacha_one(up, s3, s2 + s1, 0) # 保底第10抽 65 | result.append(c) 66 | hiishi += y 67 | 68 | return result, hiishi 69 | 70 | 71 | def gacha_tenjou(self): 72 | result = {'s3': [], 's2':[], 's1':[]} 73 | first_up_pos = 999999 74 | upnum=0 75 | up = self.up_prob 76 | s3 = self.s3_prob 77 | s2 = self.s2_prob 78 | s1 = 1000 - s3 - s2 79 | for i in range(30): #三十个十连 80 | for j in range(1,10): # 前9连 81 | c, y = self.gacha_one(up, s3, s2, s1) 82 | if 100 == y: 83 | result['s3'].append(c) 84 | first_up_pos = min(i*10+j,first_up_pos) 85 | upnum+=1 86 | elif 50 == y: 87 | result['s3'].append(c) 88 | elif 10 == y: 89 | result['s2'].append(c) 90 | elif 1==y: 91 | result['s1'].append(c) 92 | else: 93 | pass # should never reach here 94 | c, y = self.gacha_one(up, s3, s2 + s1, 0) # 保底第10抽 95 | if 100 == y: 96 | result['s3'].append(c) 97 | first_up_pos = min((i+1)*10,first_up_pos) 98 | upnum+=1 99 | elif 50 == y: 100 | result['s3'].append(c) 101 | elif 10 == y: 102 | result['s2'].append(c) 103 | elif 1==y: 104 | result['s1'].append(c) 105 | else: 106 | pass # should never reach here 107 | result['first_up_pos'] = first_up_pos 108 | return result,upnum 109 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/login_bonus.py: -------------------------------------------------------------------------------- 1 | import random 2 | from hoshino import Service, R 3 | from hoshino.util import DailyNumberLimiter 4 | 5 | sv = Service('pcr-login-bonus') 6 | 7 | lmt = DailyNumberLimiter(1) 8 | login_presents = [ 9 | '扫荡券×5', '卢币×1000', '普通EXP药水×5', '宝石×50', '玛那×3000', 10 | '扫荡券×10', '卢币×1500', '普通EXP药水×15', '宝石×80', '白金转蛋券×1', 11 | '扫荡券×15', '卢币×2000', '上级精炼石×3', '宝石×100', '白金转蛋券×1', 12 | ] 13 | todo_list = [ 14 | '找伊绪老师上课', 15 | '给宫子买布丁', 16 | '和真琴寻找伤害优衣的人', 17 | '找镜哥探讨女装', 18 | '跟吉塔一起登上骑空艇', 19 | '和霞一起调查伤害优衣的人', 20 | '和佩可小姐一起吃午饭', 21 | '找小小甜心玩过家家', 22 | '帮碧寻找新朋友', 23 | '去真步真步王国', 24 | '找镜华补习数学', 25 | '陪胡桃排练话剧', 26 | '和初音一起午睡', 27 | '成为露娜的朋友', 28 | '帮铃莓打扫咲恋育幼院', 29 | '和静流小姐一起做巧克力', 30 | '去伊丽莎白农场给栞小姐送书', 31 | '观看慈乐之音的演出', 32 | '解救挂树的队友', 33 | '来一发十连', 34 | '井一发当期的限定池', 35 | '给妈妈买一束康乃馨', 36 | '购买黄金保值', 37 | '竞技场背刺', 38 | '给别的女人打钱', 39 | '氪一单', 40 | '努力工作,尽早报答妈妈的养育之恩', 41 | '成为魔法少女', 42 | '搓一把日麻' 43 | ] 44 | 45 | @sv.on_command('签到', aliases=('盖章', '妈', '妈?', '妈妈', '妈!', '妈!', '妈妈!'), only_to_me=True) 46 | async def give_okodokai(session): 47 | uid = session.ctx['user_id'] 48 | if not lmt.check(uid): 49 | session.finish('明日はもう一つプレゼントをご用意してお待ちしますね', at_sender=True) 50 | lmt.increase(uid) 51 | present = random.choice(login_presents) 52 | todo = random.choice(todo_list) 53 | await session.send(f'\nおかえりなさいませ、主さま{R.img("priconne/kokkoro_stamp.png").cqcode}\n{present}を獲得しました\n私からのプレゼントです\n主人今天要{todo}吗?', at_sender=True) 54 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/news/__init__.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service 2 | from .spider import * 3 | 4 | svtw = Service('pcr-news-tw') 5 | svbl = Service('pcr-news-bili') 6 | 7 | async def news_poller(spider:BaseSpider, sv:Service, TAG): 8 | if not spider.item_cache: 9 | await spider.get_update() 10 | sv.logger.info(f'{TAG}新闻缓存为空,已加载至最新') 11 | return 12 | news = await spider.get_update() 13 | if not news: 14 | sv.logger.info(f'未检索到{TAG}新闻更新') 15 | return 16 | sv.logger.info(f'检索到{len(news)}条{TAG}新闻更新!') 17 | await sv.broadcast(spider.format_items(news), TAG, interval_time=0.5) 18 | 19 | @svtw.scheduled_job('interval', minutes=7, jitter=20) 20 | async def sonet_news_poller(): 21 | await news_poller(SonetSpider, svtw, '台服官网') 22 | 23 | @svbl.scheduled_job('interval', minutes=7, jitter=20) 24 | async def bili_news_poller(): 25 | await news_poller(BiliSpider, svbl, 'B服官网') 26 | 27 | 28 | async def send_news(session, spider:BaseSpider, max_num=5): 29 | if not spider.item_cache: 30 | await spider.get_update() 31 | news = spider.item_cache 32 | news = news[:min(max_num, len(news))] 33 | await session.send(spider.format_items(news), at_sender=True) 34 | 35 | @svtw.on_command('台服新闻', aliases=('台服news')) 36 | async def send_sonet_news(session): 37 | await send_news(session, SonetSpider) 38 | 39 | @svbl.on_command('B服新闻', aliases=('b服新闻','国服新闻')) 40 | async def send_bili_news(session): 41 | await send_news(session, BiliSpider) 42 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/news/spider.py: -------------------------------------------------------------------------------- 1 | """Ref: https://github.com/yuudi/yobot/blob/master/src/client/ybplugins/spider 2 | GPLv3 Licensed. Thank @yuudi for his contribution! 3 | """ 4 | import abc 5 | from bs4 import BeautifulSoup 6 | from dataclasses import dataclass 7 | from typing import List, Union 8 | from urllib.parse import urljoin 9 | try: 10 | import ujson as json 11 | except: 12 | import json 13 | 14 | from hoshino import aiorequests 15 | 16 | 17 | @dataclass 18 | class Item: 19 | idx: Union[str, int] 20 | content: str = "" 21 | 22 | def __eq__(self, other): 23 | return self.idx == other.idx 24 | 25 | 26 | class BaseSpider(abc.ABC): 27 | url = None 28 | src_name = None 29 | idx_cache = set() 30 | item_cache = [] 31 | 32 | @classmethod 33 | async def get_response(cls) -> aiorequests.AsyncResponse: 34 | resp = await aiorequests.get(cls.url) 35 | resp.raise_for_status() 36 | return resp 37 | 38 | @staticmethod 39 | @abc.abstractmethod 40 | async def get_items(resp: aiorequests.AsyncResponse) -> List[Item]: 41 | raise NotImplementedError 42 | 43 | @classmethod 44 | async def get_update(cls) -> List[Item]: 45 | resp = await cls.get_response() 46 | items = await cls.get_items(resp) 47 | updates = [ i for i in items if i.idx not in cls.idx_cache ] 48 | if updates: 49 | cls.idx_cache = set(i.idx for i in items) 50 | cls.item_cache = items 51 | return updates 52 | 53 | @classmethod 54 | def format_items(cls, items) -> str: 55 | return f'{cls.src_name}新闻\n' + '\n'.join(map(lambda i: i.content, items)) 56 | 57 | 58 | 59 | class SonetSpider(BaseSpider): 60 | url = "http://www.princessconnect.so-net.tw/news/" 61 | src_name = "台服官网" 62 | 63 | @staticmethod 64 | async def get_items(resp:aiorequests.AsyncResponse): 65 | soup = BeautifulSoup(await resp.text, 'lxml') 66 | return [ 67 | Item(idx=dd.a["href"], 68 | content=f"{dd.text}\nwww.princessconnect.so-net.tw{dd.a['href']}" 69 | ) for dd in soup.find_all("dd") 70 | ] 71 | 72 | 73 | 74 | class BiliSpider(BaseSpider): 75 | url = "https://api.biligame.com/news/list?gameExtensionId=267&positionId=2&typeId=&pageNum=1&pageSize=5" 76 | src_name = "B服官网" 77 | 78 | @staticmethod 79 | async def get_items(resp:aiorequests.AsyncResponse): 80 | content = await resp.json() 81 | items = [ 82 | Item(idx=n["id"], 83 | content="{title}\ngame.bilibili.com/pcr/news.html#detail={id}".format_map(n) 84 | ) for n in content["data"] 85 | ] 86 | return items 87 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/priconne_data_sample.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 本文件配置公主连接Re:dive的相关游戏数据 3 | ''' 4 | 5 | 6 | class _PriconneData: 7 | # 遵照格式: id: [台服官译简体, 日文原名, 英文名(罗马音), B服官译, 常见别称, 带错别字的别称等] (<-依此顺序) 8 | # 若暂无台服官译则用日文原名占位,台日用全角括号,英文用半角括号 9 | CHARA = { 10 | 1000: ["未知角色", "未知キャラ", "Unknown"], 11 | 1001: ["日和", "ヒヨリ", "Hiyori", "日和莉", "猫拳", "🐱👊"], 12 | 1002: ["优衣", "ユイ", "Yui", "种田", "普田", "由衣", "结衣", "ue", "↗↘↗↘"], 13 | 1003: ["怜", "レイ", "Rei", "剑圣", "普怜", "伶"], 14 | 1004: ["禊", "ミソギ", "Misogi", "未奏希", "炸弹", "炸弹人", "💣"], 15 | 1005: ["茉莉", "マツリ", "Matsuri", "跳跳虎", "老虎", "虎", "🐅"], 16 | 1006: ["茜里", "アカリ", "Akari", "妹法", "妹妹法"], 17 | 1007: ["宫子", "ミヤコ", "Miyako", "布丁", "布", "🍮"], 18 | 1008: ["雪", "ユキ", "Yuki", "小雪", "镜子", "镜法", "伪娘", "男孩子", "男孩纸"], 19 | 1009: ["杏奈", "アンナ", "Anna", "中二", "煤气罐"], 20 | 1010: ["真步", "マホ", "Maho", "狐狸", "真扎", "咕噜灵波", "真布", "🦊"], 21 | 1011: ["璃乃", "リノ", "Rino", "妹弓"], 22 | 1012: ["初音", "ハツネ", "Hatsune", "hego", "星法", "星星法", "⭐法", "睡法"], 23 | 1013: ["七七香", "ナナカ", "Nanaka", "娜娜卡", "77k", "77香", "眼镜"], 24 | 1014: ["霞", "カスミ", "Kasumi", "香澄", "侦探", "杜宾犬", "驴", "驴子", "🔍"], 25 | 1015: ["美里", "ミサト", "Misato", "圣母"], 26 | 1016: ["铃奈", "スズナ", "Suzuna", "暴击弓", "暴弓", "爆击弓", "爆弓", "政委"], 27 | 1017: ["香织", "カオリ", "Kaori", "琉球犬", "狗子", "狗", "狗拳", "🐶", "🐕", "🐶👊🏻", "🐶👊"], 28 | 1018: ["伊绪", "イオ", "Io", "老师", "魅魔"], 29 | 30 | 1020: ["美美", "ミミ", "Mimi", "兔子", "兔兔", "萝卜霸断剑", "人参霸断剑", "天兔霸断剑", "🐇", "🐰"], 31 | 1021: ["胡桃", "クルミ", "Kurumi", "铃铛", "🔔"], 32 | 1022: ["依里", "ヨリ", "Yori", "姐法", "姐姐法"], 33 | 1023: ["绫音", "アヤネ", "Ayane", "熊锤", "🐻🔨", "🐻"], 34 | 35 | 1025: ["铃莓", "スズメ", "Suzume", "女仆", "妹抖"], 36 | 1026: ["铃", "リン", "Rin", "松鼠", "🐿", "🐿️"], 37 | 1027: ["惠理子", "エリコ", "Eriko", "病娇"], 38 | 1028: ["咲恋", "サレン", "Saren", "充电宝", "青梅竹马", "幼驯染", "院长", "园长", "普电", "🔋"], 39 | 1029: ["望", "ノゾミ", "Nozomi", "偶像", "小望", "🎤"], 40 | 1030: ["妮诺", "ニノン", "Ninon", "妮侬", "扇子"], 41 | 1031: ["忍", "シノブ", "Shinobu", "普忍", "鬼父", "💀"], 42 | 1032: ["秋乃", "アキノ", "Akino", "哈哈剑"], 43 | 1033: ["真阳", "マヒル", "Mahiru", "奶牛", "🐄", "🐮", "真☀"], 44 | 1034: ["优花梨", "ユカリ", "Yukari", "由加莉", "黄骑", "酒鬼", "奶骑", "圣骑", "🍺", "🍺👻"], 45 | 46 | 1036: ["镜华", "キョウカ", "Kyouka", "小仓唯", "xcw", "小苍唯", "8岁", "八岁", "喷水萝", "八岁喷水萝", "8岁喷水萝"], 47 | 1037: ["智", "トモ", "Tomo", "卜毛"], 48 | 1038: ["栞", "シオリ", "Shiori", "tp弓", "TP弓", "小栞", "白虎弓", "白虎妹"], 49 | 50 | 1040: ["碧", "アオイ", "Aoi", "香菜", "香菜弓", "绿毛弓", "毒弓", "绿帽弓", "绿帽"], 51 | 52 | 1042: ["千歌", "チカ", "Chika", "绿毛奶"], 53 | 1043: ["真琴", "マコト", "Makoto", "狼", "🐺", "月月", "朋"], 54 | 1044: ["伊莉亚", "イリヤ", "Iriya", "伊利亚", "伊莉雅", "伊利雅", "yly", "吸血鬼", "那个女人"], 55 | 1045: ["空花", "クウカ", "Kuuka", "抖m", "抖"], 56 | 1046: ["珠希", "タマキ", "Tamaki", "猫剑", "🐱剑", "🐱🗡️"], 57 | 1047: ["纯", "ジュン", "Jun", "黑骑", "saber", "SABER"], 58 | 1048: ["美冬", "ミフユ", "Mifuyu", "子龙", "赵子龙"], 59 | 1049: ["静流", "シズル", "Shizuru", "姐姐"], 60 | 1050: ["美咲", "ミサキ", "Misaki", "大眼", "👀", "👁️", "👁"], 61 | 1051: ["深月", "ミツキ", "Mitsuki", "眼罩", "抖s"], 62 | 1052: ["莉玛", "リマ", "Rima", "Lima", "草泥马", "羊驼", "🦙", "🐐"], 63 | 1053: ["莫妮卡", "モニカ", "Monika", "毛二力"], 64 | 1054: ["纺希", "ツムギ", "Tsumugi", "裁缝", "蜘蛛侠", "🕷️", "🕸️"], 65 | 1055: ["步未", "アユミ", "Ayumi", "步美", "路人", "路人妹"], 66 | 1056: ["流夏", "ルカ", "Ruka", "大姐", "大姐头"], 67 | 1057: ["吉塔", "ジータ", "Jiita", "姬塔", "团长", "吉他", "骑空士", "🎸"], 68 | 1058: ["贪吃佩可", "ペコリーヌ", "Pecoriinu", "佩可莉姆", "吃货", "佩可", "公主", "饭团", "🍙"], 69 | 1059: ["可可萝", "コッコロ", "Kokkoro", "可可罗", "妈", "普白"], 70 | 1060: ["凯留", "キャル", "Kyaru", "凯露", "希留耶", "Kiruya", "黑猫", "臭鼬", "普黑"], 71 | 1061: ["矛依未", "ムイミ", "Muimi", "诺维姆", "Noemu", "夏娜", "511", "无意义", "天楼霸断剑"], 72 | 73 | 1063: ["亚里莎", "アリサ", "Arisa", "鸭梨瞎", "瞎子", "亚里沙", "鸭梨傻", "亚丽莎", "亚莉莎", "瞎子弓", "🍐🦐"], 74 | 75 | 1065: ["嘉夜", "カヤ", "Kaya", "憨憨龙", "🐲👊🏻", "🐉👊🏻"], 76 | 1066: ["祈梨", "イノリ", "Inori"], 77 | 1067: ["穗希", "ホマレ", "Homare"], 78 | 1068: ["拉比林斯达", "ラビリスタ", "Rabirisuta", "迷宫女王", "晶"], 79 | 1069: ["真那", "マナ", "Mana", "霸瞳皇帝", "霸瞳", "千里真那", "霸铜"], 80 | 1070: ["似似花", "ネネカ", "Neneka", "变貌大妃", "nnk", "448", "捏捏卡", "变貌", "大妃"], 81 | 1071: ["克莉丝提娜", "クリスティーナ", "Kurisutiina", "Christina", "Cristina", "誓约女君", "克总", "女帝", "克"], 82 | 83 | 1073: ["拉基拉基", "ラジニカーント", "Kajinigaanto", "Rajiraji", "跳跃王", "垃圾垃圾"], 84 | 85 | 1075: ["贪吃佩可(夏日)", "ペコリーヌ(サマー)", "Pekoriinu(Summer)", "水吃", "水饭", "水吃货", "水佩可", "水公主", "水饭团", "水🍙", "泳吃", "泳饭", "泳吃货", "泳佩可", "泳公主", "泳饭团", "泳🍙", "泳装吃货", "泳装公主", "泳装饭团", "泳装🍙", "佩可(夏日)", "🥡", "👙🍙", "泼妇"], 86 | 1076: ["可可萝(夏日)", "コッコロ(サマー)", "Kokkoro(Summer)", "水白", "水可", "水可可", "水可可萝", "水可可罗", "泳装可可萝", "泳装可可罗","水妈"], 87 | 1077: ["铃莓(夏日)", "スズメ(サマー)", "Suzume(Summer)", "水女仆", "水妹抖"], 88 | 1078: ["凯留(夏日)", "キャル(サマー)", "Kyaru(Summer)", "水黑", "水黑猫", "水臭鼬", "泳装黑猫", "泳装臭鼬", "潶", "溴", "💧黑"], 89 | 1079: ["珠希(夏日)", "タマキ(サマー)", "Tamaki(Summer)", "水猫剑", "水猫", "渵", "💧🐱🗡️", "水🐱🗡️"], 90 | 1080: ["美冬(夏日)", "ミフユ(サマー)", "Mifuyu(Summer)", "水子龙", "水美冬"], 91 | 1081: ["忍(万圣节)", "シノブ(ハロウィン)", "Shinobu(Halloween)", "万圣忍", "瓜忍", "🎃忍", "🎃💀"], 92 | 1082: ["宫子(万圣节)", "ミヤコ(ハロウィン)", "Miyako(Halloween)", "万圣宫子", "万圣布丁", "狼丁", "狼布丁", "万圣🍮", "🐺🍮", "🎃🍮", "👻🍮"], 93 | 1083: ["美咲(万圣节)", "ミサキ(ハロウィン)", "Misaki(Halloween)", "万圣美咲", "万圣大眼", "瓜眼", "🎃眼", "🎃👀", "🎃👁️", "🎃👁"], 94 | 1084: ["千歌(圣诞节)", "チカ(クリスマス)", "Chika(Xmas)", "圣诞千歌", "圣千", "蛋鸽", "🎄💰🎶", "🎄千🎶", "🎄1000🎶"], 95 | 1085: ["胡桃(圣诞节)", "クルミ(クリスマス)", "Kurumi(Xmas)", "圣诞胡桃", "圣诞铃铛"], 96 | 1086: ["绫音(圣诞节)", "アヤネ(クリスマス)", "Ayane(Xmas)", "圣诞熊锤", "蛋锤", "圣锤", "🎄🐻🔨", "🎄🐻"], 97 | 1087: ["日和(新年)", "ヒヨリ(ニューイヤー)", "Hiyori(NewYear)", "新年日和", "春猫", "👘🐱"], 98 | 1088: ["优衣(新年)", "ユイ(ニューイヤー)", "Yui(NewYear)", "新年优衣", "春田", "新年由衣"], 99 | 1089: ["怜(新年)", "レイ(ニューイヤー)", "Rei(NewYear)", "春剑", "春怜", "春伶"], 100 | 1090: ["惠理子(情人节)", "エリコ(バレンタイン)", "Eriko(Valentine)", "情人节病娇", "恋病", "情病", "恋病娇", "情病娇"], 101 | 1091: ["静流(情人节)", "シズル(バレンタイン)", "Shizuru(Valentine)","情人节静流", "情姐", "情人节姐姐"], 102 | 1092: ["安", "アン", "An", "胖安", "55kg"], 103 | 1093: ["露", "ルゥ", "Ruu", "逃课女王"], 104 | 1094: ["古蕾娅", "グレア", "Gurea", "龙姬", "古雷娅", "古蕾亚", "古雷亚", "🐲🐔", "🐉🐔"], 105 | 1095: ["空花(大江户)", "クウカ(オーエド)", "Kuuka(Ooedo)", "江户空花", "江户抖m", "江m", "花m", "江花"], 106 | 1096: ["妮诺(大江户)", "ニノン(オーエド)", "Ninon(Ooedo)", "江户扇子", "忍扇"], 107 | 1097: ["雷姆", "レム", "Remu", "蕾姆"], 108 | 1098: ["拉姆", "ラム", "Ramu"], 109 | 1099: ["爱蜜莉雅", "エミリア", "Emiria", "艾米莉亚", "emt", "EMT"], 110 | 1100: ["铃奈(夏日)", "スズナ(サマー)", "Suzuna(Summer)", "瀑击弓", "水爆", "水爆弓", "水暴", "瀑", "水暴弓", "瀑弓", "泳装暴弓", "泳装爆弓"], 111 | 1101: ["伊绪(夏日)", "イオ(サマー)", "Io(Summer)", "水魅魔", "水老师", "泳装魅魔", "泳装老师"], 112 | 1102: ["美咲(夏日)", "ミサキ(サマー)", "Misaki(Summer)", "水大眼", "泳装大眼"], 113 | 1103: ["咲恋(夏日)", "サレン(サマー)", "Saren(Summer)", "水电", "泳装充电宝", "泳装咲恋", "水着咲恋", "水电站", "水电宝", "水充", "👙🔋"], 114 | 1104: ["真琴(夏日)", "マコト(サマー)", "Makoto(Summer)", "水狼", "浪", "水🐺", "泳狼", "泳月", "泳月月", "泳朋", "水月", "水月月", "水朋", "👙🐺"], 115 | 1105: ["香织(夏日)", "カオリ(サマー)", "Kaori(Summer)", "水狗", "泃", "水🐶", "水🐕", "泳狗"], 116 | 1106: ["真步(夏日)", "マホ(サマー)", "Maho(Summer)", "水狐狸", "水狐", "水壶", "水真步", "水maho", "氵🦊", "水🦊", "💧🦊"], 117 | 1107: ["碧(插班生)", "アオイ(編入生)", "Aoi(Hennyuusei)", "生菜"], 118 | 1108: ["克萝依", "クロエ", "Kuroe", "华哥", "黑江"], 119 | 1109: ["琪爱儿", "チエル", "Chieru", "切露", "茄露", "茄噜", "切噜"], 120 | 1110: ["优妮", "ユニ", "Yuni", "u2", "优妮辈先", "辈先", "书记","油腻","尤尼","尤妮"], 121 | 1111: ["镜华(万圣节)", "キョウカ(ハロウィン)", "Kyouka(Halloween)", "万圣镜华", "万圣小仓唯", "万圣xcw", "猫仓唯", "黑猫仓唯", "mcw", "猫唯", "猫仓", "喵唯"], 122 | 1112: ["禊(万圣节)", "ミソギ(ハロウィン)", "Misogi(Halloween)", "万圣禊", "万圣炸弹人", "瓜炸弹人", "万圣炸弹", "万圣炸", "瓜炸", "南瓜炸", "🎃💣"], 123 | 1113: ["美美(万圣节)", "ミミ(ハロウィン)", "Mimi(Halloween)", "万圣兔", "万圣兔子", "万圣兔兔", "绷带兔", "绷带兔子", "万圣美美", "绷带美美", "万圣🐰", "绷带🐰", "🎃🐰", "万圣🐇", "绷带🐇", "🎃🐇"], 124 | 1114: ["露娜", "ルナ", "Runa", "露仓唯", "露cw"], 125 | 1115: ["克莉丝提娜(圣诞节)", "クリスティーナ(クリスマス)", "Kurisutiina(Xmas)", "Christina(Xmas)", "Cristina(Xmas)", "圣诞克", "圣诞克总", "圣诞女帝", "蛋克", "圣克", "必胜客"], 126 | 1116: ["望(圣诞节)", "ノゾミ(クリスマス)", "Nozomi(Xmas)", "圣诞望", "圣诞偶像", "蛋偶像", "蛋望"], 127 | 1117: ["伊莉亚(圣诞节)", "イリヤ(クリスマス)", "Iriya(Xmas)", "圣诞伊莉亚", "圣诞伊利亚", "圣诞伊莉雅", "圣诞伊利雅", "圣诞yly", "圣诞吸血鬼", "圣伊", "圣yly"], 128 | 129 | 1119: ["可可萝(新年)", "コッコロ(ニューイヤー)", "Kokkoro(NewYear)", "春可可", "春白", "新年妈", "春妈"], 130 | 1120: ["凯留(新年)", "キャル(ニューイヤー)", "Kyaru(NewYear)", "春凯留", "春黑猫", "春黑", "春臭鼬", "新年凯留", "新年黑猫", "新年臭鼬", "唯一神"], 131 | 1121: ["铃莓(新年)", "スズメ(ニューイヤー)", "Suzume(NewYear)", "春铃莓", "春女仆", "春妹抖", "新年铃莓", "新年女仆", "新年妹抖"], 132 | 1122: ["霞(魔法少女)", "カスミ(マジカル)", "Kasumi(MagiGirl)", "魔法少女霞", "魔法侦探", "魔法杜宾犬", "魔法驴", "魔法驴子", "魔驴", "魔法霞", "魔法少驴"], 133 | 1123: ["栞(魔法少女)", "シオリ(マジカル)", "Shiori(MagiGirl)", "魔法少女栞", "魔法tp弓", "魔法TP弓", "魔法小栞", "魔法白虎弓", "魔法白虎妹", "魔法白虎"], 134 | 1124: ["卯月(偶像大师)", "ウヅキ(デレマス)", "Udsuki(DEREM@S)", "卯月", "卵用", "Udsuki(DEREMAS)", "岛村卯月"], 135 | 1125: ["凛(偶像大师)", "リン(デレマス)", "Rin(DEREM@S)", "凛", "Rin(DEREMAS)", "涩谷凛", "西部凛"], 136 | 1126: ["未央(偶像大师)", "ミオ(デレマス)", "Mio(DEREM@S)", "未央", "Mio(DEREMAS)", "本田未央"], 137 | 1127: ["铃(游骑兵)", "リン(レンジャー)", "Rin(Ranger)", "骑兵松鼠", "游侠松鼠", "游骑兵松鼠", "护林员松鼠", "护林松鼠", "游侠🐿️", "武松"], 138 | 1128: ["真阳(游骑兵)", "マヒル(レンジャー)", "Mahiru(Ranger)", "骑兵奶牛", "游侠奶牛", "游骑兵奶牛", "护林员奶牛", "护林奶牛", "游侠🐄", "游侠🐮"], 139 | 1129: ["璃乃(奇境)", "リノ(ワンダー)", "Rino(Wonder)", "璃乃(仙境)", "爽弓", "爱丽丝弓", "爱弓", "兔弓", "奇境妹弓", "仙境妹弓", "白丝妹弓"], 140 | 1130: ["步未(奇境)", "アユミ(ワンダー)", "Ayumi(Wonder)", "步未(仙境)", "路人兔", "兔人妹", "爱丽丝路人", "奇境路人", "仙境路人"], 141 | 1131: ["流夏(夏日)", "ルカ(サマー)", "Ruka(Summer)", "水流夏", "水大姐头", "水大姐", "泳装大姐头", "泳装流夏", "流夏(泳装)"], 142 | 1132: ["杏奈(夏日)", "アンナ(サマー)", "Anna(Summer)", "水中二", "泳装中二", "冲二", "喷水龙王"], 143 | 1133: ["七七香(夏日)", "ナナカ(サマー)", "Nanaka(Summer)", "水七七香", "泳装七七香", "泳装眼镜", "水眼镜"], 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | # =================================== # 152 | 153 | 154 | 155 | 1802: ['优衣(公主)', "ユイ(プリンセス)", "Yui(Princess)", "公主种田", "公主优衣", "掉毛", "飞翼", "飞翼高达", "公田"], 156 | 1804: ["贪吃佩可(公主)", "ペコリーヌ(プリンセス)", "Pekoriinu(Princess)", "公主吃", "公主饭", "公主吃货", "公主佩可", "公主饭团", "公主🍙", "命运高达", "高达", "命运公主", "高达公主", "春哥高达", "🤖🍙", "🤖"], 157 | 1805: ["可可萝(公主)", "コッコロ(プリンセス)", "Kokkoro(Princess)", "公主妈", "月光妈", "蝶妈", "蝴蝶妈", "月光蝶妈", "公主可", "公主可萝", "公主可可萝", "月光可", "月光可萝", "月光可可萝", "蝶可", "蝶可萝", "蝶可可萝"], 158 | 159 | 160 | # =================================== # 161 | 1908: ["花凛", "カリン", "Karin", "绿毛恶魔"], 162 | 163 | 4031: ["骷髅", "髑髏", "Dokuro", "骷髅老爹", "老爹"], 164 | 165 | 9000: ["祐树", "ユウキ", "Yuuki", "祐樹", "骑士", "骑士君"], 166 | 9401: ["爱梅斯", "アメス", "Amesu", "菲欧", "フィオ", "Fio"], 167 | } 168 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/whois.py: -------------------------------------------------------------------------------- 1 | from hoshino.util import FreqLimiter 2 | from .chara import Chara 3 | from hoshino import Service 4 | from hoshino import R 5 | sv = Service('whois') 6 | _lmt = FreqLimiter(5) 7 | _lmt1 = FreqLimiter(5) 8 | 9 | 10 | @sv.on_rex(r'^[谁誰]是\s*(.{1,20})$', normalize=False, can_private=1) 11 | async def whois(bot, ctx, match): 12 | uid = ctx['user_id'] 13 | if not _lmt.check(uid): 14 | await bot.send(ctx, '您查询得太快了,请稍等一会儿', at_sender=True) 15 | return 16 | _lmt.start_cd(uid) 17 | 18 | name = match.group(1) 19 | chara = Chara.fromname(name, star=0) 20 | if chara.id == Chara.UNKNOWN: 21 | msg = [f'兰德索尔似乎没有叫"{name}"的人'] 22 | if uid not in bot.config.SUPERUSERS: 23 | _lmt.start_cd(uid, 300) 24 | msg.append('您的下次查询将于5分钟后可用') 25 | await bot.send(ctx, '\n'.join(msg), at_sender=True) 26 | return 27 | 28 | msg = f'\n{chara.name}\n{chara.icon.cqcode}' 29 | await bot.send(ctx, msg, at_sender=True) 30 | STARDIC = {"一": 1, "二": 2, "三": 3, "四": 4, "五": 5, "六": 6} 31 | 32 | 33 | @sv.on_rex(r'^[看查]?\s?([1-6一二三四五六][xX星])?\s?(.{1,20})(立绘|卡面)$', can_private=1) 34 | async def lookcard(bot, ctx, match): 35 | uid = ctx['user_id'] 36 | if not _lmt1.check(uid): 37 | await bot.send(ctx, '您查询得太快了,请稍等一会儿', at_sender=True) 38 | return 39 | _lmt1.start_cd(uid) 40 | name = match.group(2) 41 | star = match.group(1)[0] if match.group(1) else 0 42 | star = STARDIC.get(star, star) 43 | chara = Chara.fromname(name, star=int(star)) 44 | if chara.id == Chara.UNKNOWN: 45 | msg = [f'兰德索尔似乎没有叫"{name}"的人'] 46 | if uid not in bot.config.SUPERUSERS: 47 | _lmt1.start_cd(uid, 300) 48 | msg.append('您的下次查询将于5分钟后可用') 49 | await bot.send(ctx, '\n'.join(msg), at_sender=True) 50 | return 51 | await bot.send(ctx, "图片较大,请稍等片刻") 52 | msg = f'\n{chara.card}' 53 | await bot.send(ctx, msg, at_sender=True) 54 | -------------------------------------------------------------------------------- /hoshino/modules/setu/setu.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import time 4 | import random 5 | 6 | from nonebot import on_command, CommandSession, MessageSegment, NoneBot 7 | from nonebot.exceptions import CQHttpError 8 | 9 | from hoshino import R, Service, Privilege 10 | from hoshino.util import FreqLimiter, DailyNumberLimiter 11 | 12 | _max = 5 13 | EXCEED_NOTICE = f'您今天已经冲过{_max}次了,请明早5点后再来!' 14 | _nlmt = DailyNumberLimiter(_max) 15 | _flmt = FreqLimiter(5) 16 | 17 | sv = Service('setu', manage_priv=Privilege.SUPERUSER, enable_on_default=True, visible=False) 18 | setu_folder = R.img('setu/').path 19 | 20 | def setu_gener(): 21 | while True: 22 | filelist = os.listdir(setu_folder) 23 | random.shuffle(filelist) 24 | for filename in filelist: 25 | if os.path.isfile(os.path.join(setu_folder, filename)): 26 | yield R.img('setu/', filename) 27 | 28 | setu_gener = setu_gener() 29 | 30 | def get_setu(): 31 | return setu_gener.__next__() 32 | 33 | 34 | @sv.on_rex(re.compile(r'不够[涩瑟色]|[涩瑟色]图|来一?[点份张].*[涩瑟色]|再来[点份张]|看过了|铜'), normalize=True) 35 | async def setu(bot:NoneBot, ctx, match): 36 | """随机叫一份涩图,对每个用户有冷却时间""" 37 | uid = ctx['user_id'] 38 | if not _nlmt.check(uid): 39 | await bot.send(ctx, EXCEED_NOTICE, at_sender=True) 40 | return 41 | if not _flmt.check(uid): 42 | await bot.send(ctx, '您冲得太快了,请稍候再冲', at_sender=True) 43 | return 44 | _flmt.start_cd(uid) 45 | _nlmt.increase(uid) 46 | 47 | # conditions all ok, send a setu. 48 | pic = get_setu() 49 | try: 50 | await bot.send(ctx, pic.cqcode) 51 | except CQHttpError: 52 | sv.logger.error(f"发送图片{pic.path}失败") 53 | try: 54 | await bot.send(ctx, '涩图太涩,发不出去勒...') 55 | except: 56 | pass 57 | -------------------------------------------------------------------------------- /hoshino/modules/steam/steam/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | from lxml import etree 3 | import json 4 | import os 5 | from asyncio import sleep 6 | from nonebot import CommandSession 7 | from hoshino import service, aiorequests 8 | from hoshino.util import load_config 9 | 10 | sv = service.Service("steam", enable_on_default=False,visible=False) 11 | 12 | subscribe_file = os.path.join(os.path.dirname(__file__), 'subscribes.json') 13 | with open(subscribe_file, mode="r") as f: 14 | f = f.read() 15 | sub = json.loads(f) 16 | cfg = load_config(__file__) 17 | playing_state = {} 18 | async def format_id(id:str)->str: 19 | if id.startswith('76561198') and len(id)==17: 20 | return id 21 | else: 22 | resp= await aiorequests.get(f'https://steamcommunity.com/id/{id}?xml=1') 23 | xml=etree.XML(await resp.content) 24 | return xml.xpath('/profile/steamID64')[0].text 25 | 26 | @sv.on_command("添加steam订阅") 27 | async def steam(session: CommandSession): 28 | account = session.current_arg_text.strip() 29 | try: 30 | rsp = await get_account_status(account) 31 | if rsp["personaname"] == "": 32 | await session.send("查询失败!") 33 | elif rsp["gameextrainfo"] == "": 34 | await session.send(f"%s 没在玩游戏!" % rsp["personaname"]) 35 | else: 36 | await session.send(f"%s 正在玩 %s !" % (rsp["personaname"], rsp["gameextrainfo"])) 37 | await update_steam_ids(account, session.event["group_id"]) 38 | await session.send("订阅成功") 39 | except: 40 | await session.send("订阅失败") 41 | 42 | 43 | @sv.on_command("取消steam订阅") 44 | async def steam(session: CommandSession): 45 | account = session.current_arg_text.strip() 46 | try: 47 | await del_steam_ids(account, session.event["group_id"]) 48 | await session.send("订阅成功") 49 | except: 50 | await session.send("订阅失败") 51 | 52 | 53 | @sv.on_command("steam订阅列表",aliases=('查看本群steam','本群steam订阅')) 54 | async def steam(session: CommandSession): 55 | group_id = session.event["group_id"] 56 | msg = '======steam======\n' 57 | await update_game_status() 58 | for key, val in playing_state.items(): 59 | if group_id in sub["subscribes"][str(key)]: 60 | if val["gameextrainfo"] == "": 61 | msg += "%s 没在玩游戏\n" % val["personaname"] 62 | else: 63 | msg += "%s 正在游玩 %s\n" % (val["personaname"], 64 | val["gameextrainfo"]) 65 | await session.send(msg) 66 | 67 | 68 | @sv.on_command("查询steam账号",aliases=('查看steam','查看steam订阅','steam')) 69 | async def steam(session: CommandSession): 70 | account = session.current_arg_text.strip() 71 | rsp = await get_account_status(account) 72 | if rsp["personaname"] == "": 73 | await session.send("查询失败!") 74 | elif rsp["gameextrainfo"] == "": 75 | await session.send(f"%s 没在玩游戏!" % rsp["personaname"]) 76 | else: 77 | await session.send(f"%s 正在玩 %s !" % (rsp["personaname"], rsp["gameextrainfo"])) 78 | 79 | 80 | async def get_account_status(id) -> dict: 81 | id=await format_id(id) 82 | params = { 83 | "key": cfg["key"], 84 | "format": "json", 85 | "steamids": id 86 | } 87 | resp = await aiorequests.get("https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/", params=params) 88 | rsp = await resp.json() 89 | friend = rsp["response"]["players"][0] 90 | return { 91 | "personaname": friend["personaname"] if "personaname" in friend else "", 92 | "gameextrainfo": friend["gameextrainfo"] if "gameextrainfo" in friend else "" 93 | } 94 | 95 | 96 | async def update_game_status() -> None: 97 | params = { 98 | "key": cfg["key"], 99 | "format": "json", 100 | "steamids": ",".join(sub["subscribes"].keys()) 101 | } 102 | resp = await aiorequests.get("https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/", params=params) 103 | rsp = await resp.json() 104 | for friend in rsp["response"]["players"]: 105 | playing_state[friend["steamid"]] = { 106 | "personaname": friend["personaname"], 107 | "gameextrainfo": friend["gameextrainfo"] if "gameextrainfo" in friend else "" 108 | } 109 | 110 | 111 | async def update_steam_ids(steam_id, group): 112 | steam_id=await format_id(steam_id) 113 | if steam_id not in sub["subscribes"]: 114 | sub["subscribes"][str(steam_id)] = [] 115 | if group not in sub["subscribes"][str(steam_id)]: 116 | sub["subscribes"][str(steam_id)].append(group) 117 | with open(subscribe_file, mode="w") as fil: 118 | json.dump(sub, fil, indent=4, ensure_ascii=False) 119 | await update_game_status() 120 | 121 | 122 | async def del_steam_ids(steam_id, group): 123 | steam_id=await format_id(steam_id) 124 | if group in sub["subscribes"][str(steam_id)]: 125 | sub["subscribes"][str(steam_id)].remove(group) 126 | with open(subscribe_file, mode="w") as fil: 127 | json.dump(sub, fil, indent=4, ensure_ascii=False) 128 | await update_game_status() 129 | 130 | 131 | @sv.scheduled_job('cron', minute='*/2') 132 | async def check_steam_status(): 133 | old_state = playing_state.copy() 134 | await update_game_status() 135 | for key, val in playing_state.items(): 136 | if val["gameextrainfo"] != old_state[key]["gameextrainfo"]: 137 | glist=set(sub["subscribes"][key])&set((await sv.get_enable_groups()).keys()) 138 | if val["gameextrainfo"] == "": 139 | await broadcast(glist, 140 | "%s 不玩 %s 了!" % (val["personaname"], old_state[key]["gameextrainfo"])) 141 | else: 142 | await broadcast(glist, 143 | "%s 开始游玩 %s !" % (val["personaname"], val["gameextrainfo"])) 144 | 145 | 146 | async def broadcast(group_list: Iterable, msg): 147 | for group in group_list: 148 | await sv.bot.send_group_msg(group_id=group, message=msg) 149 | await sleep(0.5) 150 | -------------------------------------------------------------------------------- /hoshino/modules/steam/steam/subscribes.json: -------------------------------------------------------------------------------- 1 | { 2 | "subscribes": { 3 | } 4 | } -------------------------------------------------------------------------------- /hoshino/modules/twitter/twitter.py: -------------------------------------------------------------------------------- 1 | import re 2 | import pytz 3 | import random 4 | import asyncio 5 | from datetime import datetime 6 | from functools import partial, wraps 7 | from collections import defaultdict 8 | from TwitterAPI import TwitterAPI, TwitterResponse 9 | 10 | from nonebot import MessageSegment as ms 11 | from hoshino import util 12 | from hoshino.service import Service, Privilege as Priv 13 | 14 | cfg = util.load_config(__file__) 15 | api = TwitterAPI(cfg['consumer_key'], cfg['consumer_secret'], cfg['access_token_key'], cfg['access_token_secret']) 16 | sv = Service('twitter-poller', use_priv=Priv.ADMIN, manage_priv=Priv.SUPERUSER, visible=False) 17 | 18 | URL_TIMELINE = 'statuses/user_timeline' 19 | 20 | subr_dic = { 21 | Service('kc-twitter', enable_on_default=False): ['KanColle_STAFF', 'C2_STAFF', 'ywwuyi'], 22 | Service('pcr-twitter', enable_on_default=True): ['priconne_redive', 'priconne_anime'], 23 | Service('pripri-twitter', enable_on_default=False, visible=False): ['pripri_anime'], 24 | Service('shiratama-twitter', enable_on_default=False, visible=False): ['shiratamacaron'], 25 | Service('kc-doujin-twitter', enable_on_default=False): ['suzukitoto0323', 'watanohara2'], 26 | } 27 | 28 | latest_info = {} # { account: {last_tweet_id: int, profile_image: str } } 29 | for _, ids in subr_dic.items(): # initialize 30 | for account in ids: 31 | latest_info[account] = {'last_tweet_id': 0, 'profile_image': '', 'media_only': False} 32 | 33 | for account in ('shiratamacaron', 'suzukitoto0323', 'watanohara2'): 34 | latest_info[account]['media_only'] = True 35 | 36 | 37 | @wraps(api.request) 38 | async def twt_request(*args, **kwargs): 39 | return await asyncio.get_event_loop().run_in_executor( 40 | None, partial(api.request, *args, **kwargs)) 41 | 42 | 43 | def update_latest_info(account:str, rsp:TwitterResponse): 44 | for item in rsp.get_iterator(): 45 | if item['id'] > latest_info[account]['last_tweet_id']: 46 | latest_info[account]['last_tweet_id'] = item['id'] 47 | if item['user']['screen_name'] == account: 48 | latest_info[account]['profile_image'] = item['user']['profile_image_url'] 49 | 50 | 51 | def time_formatter(time_str): 52 | dt = datetime.strptime(time_str, r"%a %b %d %H:%M:%S %z %Y") 53 | dt = dt.astimezone(pytz.timezone('Asia/Shanghai')) 54 | return f"{util.month_name(dt.month)}{util.date_name(dt.day)}・{util.time_name(dt.hour, dt.minute)}" 55 | 56 | 57 | def tweet_formatter(item): 58 | name = item['user']['name'] 59 | time = time_formatter(item['created_at']) 60 | text = item['full_text'] 61 | try: 62 | img = item['entities']['media'][0]['media_url'] 63 | assert re.search(r'\.(jpg|jpeg|png|gif|jfif|webp)$', img, re.I) 64 | img = f"\n{ms.image(img)}" 65 | except: 66 | img = '' 67 | return f"@{name}\n{time}\n\n{text}{img}" 68 | 69 | 70 | def has_media(item): 71 | try: 72 | return bool(item['entities']['media'][0]['media_url']) 73 | except: 74 | return False 75 | 76 | 77 | async def poll_new_tweets(account:str): 78 | if not latest_info[account]['last_tweet_id']: # on the 1st time 79 | params = {'screen_name': account, 'count': '1'} 80 | rsp = await twt_request(URL_TIMELINE, params) 81 | update_latest_info(account, rsp) 82 | return [] 83 | else: # on other times 84 | params = { 85 | 'screen_name': account, 86 | 'count': '10', 87 | 'since_id': latest_info[account]['last_tweet_id'], 88 | 'tweet_mode': 'extended', 89 | 'include_rts': False, 90 | } 91 | rsp = await twt_request(URL_TIMELINE, params) 92 | old_profile_image = latest_info[account]['profile_image'] 93 | update_latest_info(account, rsp) 94 | new_profile_image = latest_info[account]['profile_image'] 95 | 96 | items = rsp.get_iterator() 97 | if latest_info[account]['media_only']: 98 | items = filter(has_media, items) 99 | tweets = list(map(tweet_formatter, items)) 100 | if new_profile_image != old_profile_image and old_profile_image: 101 | big_img = re.sub(r'_normal(\.(jpg|jpeg|png|gif|jfif|webp))$', r'\1', new_profile_image, re.I) 102 | tweets.append(f"@{account} 更换了头像\n{ms.image(big_img)}") 103 | return tweets 104 | 105 | 106 | # Requests/15-min window: 900 == 1 req/s 107 | _subr_num = len(latest_info) 108 | _freq = 8 * _subr_num 109 | sv.logger.info(f"twitter_poller works at {_subr_num} / {_freq} seconds") 110 | 111 | @sv.scheduled_job('interval', seconds=_freq) 112 | async def twitter_poller(): 113 | buf = {} 114 | for account in latest_info: 115 | try: 116 | buf[account] = await poll_new_tweets(account) 117 | if l := len(buf[account]): 118 | sv.logger.info(f"成功获取@{account}的新推文{l}条") 119 | else: 120 | sv.logger.info(f"未检测到@{account}的新推文") 121 | except Exception as e: 122 | sv.logger.exception(e) 123 | sv.logger.error(f"获取@{account}的推文时出现异常{type(e)}") 124 | 125 | for ssv, subr_list in subr_dic.items(): 126 | twts = [] 127 | for account in subr_list: 128 | twts.extend(buf.get(account, [])) 129 | await ssv.broadcast(twts, ssv.name, 0.5) 130 | 131 | @sv.on_command('看推', only_to_me=True) # for test 132 | async def one_tweet(session): 133 | args = session.current_arg_text.split() 134 | try: 135 | account = args[0] 136 | except: 137 | account = 'KanColle_STAFF' 138 | try: 139 | count = min(int(args[1]), 15) 140 | if count <= 0: 141 | count = 3 142 | except: 143 | count = 3 144 | params = { 145 | 'screen_name': account, 146 | 'count': count, 147 | 'tweet_mode': 'extended', 148 | } 149 | rsp = await twt_request(URL_TIMELINE, params) 150 | items = rsp.get_iterator() 151 | if account in latest_info and latest_info[account]['media_only']: 152 | items = filter(has_media, items) 153 | twts = list(map(tweet_formatter, items)) 154 | for t in twts: 155 | await session.send(t) 156 | await asyncio.sleep(0.5) 157 | -------------------------------------------------------------------------------- /hoshino/res.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PIL import Image 3 | from urllib.request import pathname2url 4 | from urllib.parse import urljoin 5 | 6 | from nonebot import get_bot 7 | from nonebot import MessageSegment 8 | 9 | from hoshino.util import pic2b64 10 | from hoshino import logger 11 | 12 | class R: 13 | 14 | @staticmethod 15 | def get(path, *paths): 16 | return ResObj(os.path.join(path, *paths)) 17 | 18 | @staticmethod 19 | def img(path, *paths): 20 | return ResImg(os.path.join('img', path, *paths)) 21 | 22 | 23 | 24 | class ResObj: 25 | 26 | def __init__(self, res_path): 27 | res_dir = os.path.expanduser(get_bot().config.RESOURCE_DIR) 28 | self.fullpath = os.path.abspath(os.path.join(res_dir, res_path)) 29 | if not self.fullpath.startswith(os.path.abspath(res_dir)): 30 | raise ValueError('Cannot access outside RESOUCE_DIR') 31 | 32 | 33 | 34 | @property 35 | def url(self): 36 | """ 37 | @return: 资源文件的url,供cqhttp使用 38 | """ 39 | return urljoin(get_bot().config.RESOURCE_URL, pathname2url(self.__path)) 40 | 41 | 42 | @property 43 | def path(self): 44 | """ 45 | @return: 资源文件的绝对路径,供bot内部使用 46 | """ 47 | return os.path.normpath(self.fullpath) 48 | 49 | 50 | @property 51 | def exist(self): 52 | return os.path.exists(self.path) 53 | 54 | 55 | class ResImg(ResObj): 56 | @property 57 | def cqcode(self) -> MessageSegment: 58 | if get_bot().config.RESOURCE_URL: 59 | return MessageSegment.image(self.url) 60 | else: 61 | try: 62 | return MessageSegment.image('file:///'+self.path) 63 | except Exception as e: 64 | logger.exception(e) 65 | return MessageSegment.text('[图片]') 66 | def open(self) -> Image: 67 | return Image.open(self.path) -------------------------------------------------------------------------------- /hoshino/util.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from typing import Tuple 4 | import pytz 5 | import base64 6 | import zhconv 7 | import unicodedata 8 | from io import BytesIO 9 | from PIL import Image, ImageFont, ImageDraw 10 | from functools import reduce 11 | from collections import defaultdict 12 | from matplotlib import pyplot as plt 13 | from datetime import datetime, timedelta 14 | try: 15 | import ujson as json 16 | except: 17 | import json 18 | 19 | from nonebot import get_bot 20 | from aiocqhttp.exceptions import ActionFailed 21 | 22 | from .log import logger 23 | 24 | 25 | def load_config(inbuilt_file_var): 26 | """ 27 | Just use `config = load_config(__file__)`, 28 | you can get the config.json as a dict. 29 | """ 30 | filename = os.path.join(os.path.dirname(inbuilt_file_var), 'config.json') 31 | try: 32 | with open(filename, encoding='utf8') as f: 33 | config = json.load(f) 34 | return config 35 | except Exception as e: 36 | logger.exception(e) 37 | return {} 38 | 39 | 40 | async def delete_msg(ctx): 41 | try: 42 | if get_bot().config.IS_CQPRO: 43 | msg_id = ctx['message_id'] 44 | await get_bot().delete_msg(self_id=ctx['self_id'], message_id=msg_id) 45 | except ActionFailed as e: 46 | logger.error(f'撤回失败 retcode={e.retcode}') 47 | except Exception as e: 48 | logger.exception(e) 49 | 50 | 51 | async def silence(ctx, ban_time, ignore_super_user=False): 52 | try: 53 | self_id = ctx['self_id'] 54 | group_id = ctx['group_id'] 55 | user_id = ctx['user_id'] 56 | bot = get_bot() 57 | if ignore_super_user or user_id not in bot.config.SUPERUSERS: 58 | await bot.set_group_ban(self_id=self_id, group_id=group_id, user_id=user_id, duration=ban_time) 59 | except ActionFailed as e: 60 | logger.error(f'禁言失败 retcode={e.retcode}') 61 | except Exception as e: 62 | logger.exception(e) 63 | 64 | 65 | def get_text_size(text: str, font: ImageFont.ImageFont,padding:Tuple[int,int,int,int]=(20,20,20,20),spacing:int=5) -> tuple: 66 | ''' 67 | 返回文本转图片的图片大小 68 | 69 | *`text`:用来转图的文本 70 | *`font`:一个`ImageFont`实例 71 | *`padding`:一个四元`int`元组,分别是左、右、上、下的留白大小 72 | *`spacing`: 文本行间距 73 | ''' 74 | with Image.new('RGBA', (1,1), (255, 255, 255, 255)) as base: 75 | dr = ImageDraw.ImageDraw(base) 76 | ret=dr.textsize(text,font=font,spacing=spacing) 77 | return ret[0]+padding[0]+padding[1],ret[1]+padding[2]+padding[3] 78 | 79 | 80 | def text2pic(text: str, font: ImageFont.ImageFont,padding:Tuple[int,int,int,int]=(20,20,20,20),spacing:int=5) -> Image.Image: 81 | ''' 82 | 返回一个文本转化后的`Image`实例 83 | 84 | *`text`:用来转图的文本 85 | *`font`:一个`ImageFont`实例 86 | *`padding`:一个四元`int`元组,分别是左、右、上、下的留白大小 87 | *`spacing`: 文本行间距 88 | ''' 89 | size = get_text_size(text, font,padding,spacing) 90 | base = Image.new('RGBA', size, (255, 255, 255, 255)) 91 | dr = ImageDraw.ImageDraw(base) 92 | dr.text((padding[0], padding[2]), text, font=font, fill='#000000',spacing=spacing) 93 | return base 94 | 95 | 96 | def CQimage(image: str, cache: int = 1) -> str: 97 | return f'[CQ:image,cache={cache},file={image}]' 98 | 99 | 100 | def pic2b64(pic: Image.Image) -> str: 101 | buf = BytesIO() 102 | pic.save(buf, format='PNG') 103 | base64_str = base64.b64encode( 104 | buf.getvalue()).decode() # , encoding='utf8') 105 | return 'base64://' + base64_str 106 | 107 | 108 | def text2CQ(text: str, font: ImageFont.ImageFont) -> str: 109 | return CQimage(pic2b64(text2pic(text, font))) 110 | 111 | 112 | def fig2b64(plt: plt) -> str: 113 | buf = BytesIO() 114 | plt.savefig(buf, format='PNG', dpi=100) 115 | base64_str = base64.b64encode(buf.getvalue()).decode() 116 | return 'base64://' + base64_str 117 | 118 | 119 | def concat_pic(pics, border=5): 120 | num = len(pics) 121 | w, h = pics[0].size 122 | des = Image.new('RGBA', (w, num * h + (num-1) * border), 123 | (255, 255, 255, 255)) 124 | for i, pic in enumerate(pics): 125 | des.paste(pic, (0, i * (h + border)), pic) 126 | return des 127 | 128 | 129 | def normalize_str(string) -> str: 130 | """ 131 | 规范化unicode字符串 并 转为小写 并 转为简体 132 | """ 133 | string = unicodedata.normalize('NFKC', string) 134 | string = string.lower() 135 | string = zhconv.convert(string, 'zh-hans') 136 | return string 137 | 138 | 139 | MONTH_NAME = ('睦月', '如月', '弥生', '卯月', '皐月', '水無月' 140 | '文月', '葉月', '長月', '神無月', '霜月', '師走') 141 | 142 | 143 | def month_name(x: int) -> str: 144 | return MONTH_NAME[x - 1] 145 | 146 | 147 | DATE_NAME = ( 148 | '初一', '初二', '初三', '初四', '初五', '初六', '初七', '初八', '初九', '初十', 149 | '十一', '十二', '十三', '十四', '十五', '十六', '十七', '十八', '十九', '二十', 150 | '廿一', '廿二', '廿三', '廿四', '廿五', '廿六', '廿七', '廿八', '廿九', '三十', 151 | '卅一' 152 | ) 153 | 154 | 155 | def date_name(x: int) -> str: 156 | return DATE_NAME[x - 1] 157 | 158 | 159 | NUM_NAME = ( 160 | '〇〇', '〇一', '〇二', '〇三', '〇四', '〇五', '〇六', '〇七', '〇八', '〇九', 161 | '一〇', '一一', '一二', '一三', '一四', '一五', '一六', '一七', '一八', '一九', 162 | '二〇', '二一', '二二', '二三', '二四', '二五', '二六', '二七', '二八', '二九', 163 | '三〇', '三一', '三二', '三三', '三四', '三五', '三六', '三七', '三八', '三九', 164 | '四〇', '四一', '四二', '四三', '四四', '四五', '四六', '四七', '四八', '四九', 165 | '五〇', '五一', '五二', '五三', '五四', '五五', '五六', '五七', '五八', '五九', 166 | '六〇', '六一', '六二', '六三', '六四', '六五', '六六', '六七', '六八', '六九', 167 | '七〇', '七一', '七二', '七三', '七四', '七五', '七六', '七七', '七八', '七九', 168 | '八〇', '八一', '八二', '八三', '八四', '八五', '八六', '八七', '八八', '八九', 169 | '九〇', '九一', '九二', '九三', '九四', '九五', '九六', '九七', '九八', '九九', 170 | ) 171 | 172 | 173 | def time_name(hh: int, mm: int) -> str: 174 | return NUM_NAME[hh] + NUM_NAME[mm] 175 | 176 | 177 | class FreqLimiter: 178 | def __init__(self, default_cd_seconds): 179 | self.next_time = defaultdict(float) 180 | self.default_cd = default_cd_seconds 181 | 182 | def check(self, key) -> bool: 183 | return bool(time.time() >= self.next_time[key]) 184 | 185 | def start_cd(self, key, cd_time=0): 186 | self.next_time[key] = time.time( 187 | ) + cd_time if cd_time > 0 else self.default_cd 188 | 189 | 190 | class DailyNumberLimiter: 191 | tz = pytz.timezone('Asia/Shanghai') 192 | 193 | def __init__(self, max_num): 194 | self.today = -1 195 | self.count = defaultdict(int) 196 | self.max = max_num 197 | 198 | def check(self, key) -> bool: 199 | now = datetime.now(self.tz) 200 | day = (now - timedelta(hours=5)).day 201 | if day != self.today: 202 | self.today = day 203 | self.count.clear() 204 | return bool(self.count[key] < self.max) 205 | 206 | def get_num(self, key): 207 | return self.count[key] 208 | 209 | def increase(self, key, num=1): 210 | self.count[key] += num 211 | 212 | def reset(self, key): 213 | self.count[key] = 0 214 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | nonebot[scheduler] 2 | aiocqhttp 3 | lxml>=4.4.1 4 | pytz>=2019.3 5 | requests>=2.22.0 6 | zhconv>=1.4.0 7 | Pillow>=6.2.1 8 | TwitterAPI>=2.5.10 9 | matplotlib>=3.2.0 10 | numpy>=1.18.0 11 | beautifulsoup4>=4.9.0 12 | brotli 13 | peewee 14 | aiohttp 15 | saucenao_api 16 | websocket-client 17 | logzero 18 | demjson 19 | ujson 20 | feedparser 21 | pygtrie -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import config 2 | import hoshino 3 | import asyncio 4 | bot = hoshino.init(config) 5 | app = bot.asgi 6 | 7 | if __name__ == '__main__': 8 | bot.run(use_reloader=False,loop=asyncio.get_event_loop(),debug=False) 9 | --------------------------------------------------------------------------------