├── .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 |
--------------------------------------------------------------------------------