├── hoshino ├── modules │ ├── priconne │ │ ├── __init__.py │ │ ├── arena │ │ │ ├── README.md │ │ │ └── arena.py │ │ ├── gacha │ │ │ ├── README.md │ │ │ ├── gacha.py │ │ │ ├── config.json │ │ │ └── __init__.py │ │ ├── buy_potion_reminder.py │ │ ├── query │ │ │ ├── __init__.py │ │ │ ├── miner.py │ │ │ ├── whois.py │ │ │ └── query.py │ │ ├── arena_reminder.py │ │ ├── login_bonus.py │ │ ├── pcr_data_updater.py │ │ ├── news │ │ │ ├── __init__.py │ │ │ └── spider.py │ │ ├── cherugo.py │ │ ├── games │ │ │ ├── __init__.py │ │ │ ├── desc_guess.py │ │ │ └── avatar_guess.py │ │ └── comic.py │ ├── pcrclanbattle │ │ ├── clanbattle │ │ │ ├── dao │ │ │ │ └── __init__.py │ │ │ ├── exception.py │ │ │ ├── argparse │ │ │ │ ├── argtype.py │ │ │ │ └── __init__.py │ │ │ ├── config.json │ │ │ ├── __init__.py │ │ │ ├── README.md │ │ │ └── READMEv1.md │ │ ├── version_selector.py │ │ └── hedao.py │ ├── botmanage │ │ ├── get_cqcode.py │ │ ├── data_cleaner.py │ │ ├── group_invite.py │ │ ├── alert.py │ │ ├── group_leave.py │ │ ├── feedback.py │ │ ├── broadcast.py │ │ ├── help.py │ │ ├── billing.py │ │ ├── ls.py │ │ └── service_manage.py │ ├── groupmaster │ │ ├── join_approve.py │ │ ├── loli_is_justice.py │ │ ├── anti_asoul.py │ │ ├── anti_lex.py │ │ ├── sleeping_set.py │ │ ├── anti_msg_recall.py │ │ ├── antiqks.py │ │ ├── group_notice.py │ │ ├── anti_kfc.py │ │ ├── random_repeater.py │ │ ├── chat.py │ │ ├── anti_abuse.py │ │ └── anti_holo.py │ ├── hourcall │ │ └── hourcall.py │ ├── deepchat │ │ └── deepchat.py │ ├── flac │ │ └── flac.py │ ├── kancolle │ │ ├── query │ │ │ ├── _senka_spider.py │ │ │ ├── fleet.py │ │ │ ├── senka.py │ │ │ └── __init__.py │ │ └── reminder.py │ ├── twitter │ │ └── stream │ │ │ ├── util.py │ │ │ ├── __init__.py │ │ │ ├── track.py │ │ │ └── follow.py │ ├── twitter-v2 │ │ └── stream │ │ │ ├── __init__.py │ │ │ ├── util.py │ │ │ └── follow.py │ ├── setu │ │ └── setu.py │ ├── translate │ │ └── translate.py │ ├── dice │ │ └── dice.py │ ├── picfinder │ │ └── README.md │ └── mikan │ │ └── mikan.py ├── config_example │ ├── priconne.py │ ├── deepchat.py │ ├── mikan.py │ ├── groupmaster.py │ ├── __init__.py │ ├── hourcall.py │ ├── picfinder.py │ ├── pcrclanbattle.py │ ├── __bot__.py │ └── twitter.py ├── util │ ├── textfilter │ │ ├── README.md │ │ └── filter.py │ └── __init__.py ├── typing.py ├── log.py ├── msghandler.py ├── R.py ├── __init__.py ├── priv.py ├── aiorequests.py └── trigger.py ├── run.py ├── requirements.txt └── .gitignore /hoshino/modules/priconne/__init__.py: -------------------------------------------------------------------------------- 1 | # do nothing -------------------------------------------------------------------------------- /hoshino/modules/pcrclanbattle/clanbattle/dao/__init__.py: -------------------------------------------------------------------------------- 1 | # do nothing -------------------------------------------------------------------------------- /hoshino/config_example/priconne.py: -------------------------------------------------------------------------------- 1 | class arena: 2 | AUTH_KEY = "" 3 | -------------------------------------------------------------------------------- /hoshino/config_example/deepchat.py: -------------------------------------------------------------------------------- 1 | # see https://github.com/peterli110/AutoRepeater 2 | deepchat_api = "http://127.0.0.1:7777/message" 3 | -------------------------------------------------------------------------------- /hoshino/config_example/mikan.py: -------------------------------------------------------------------------------- 1 | MIKAN_TOKEN = "" 2 | PROXIES = None 3 | # PROXIES = {"http": "http://127.0.0.1:7890", "https": "http://127.0.0.1:7890"} 4 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import hoshino 2 | import asyncio 3 | 4 | bot = hoshino.init() 5 | app = bot.asgi 6 | 7 | if __name__ == '__main__': 8 | bot.run(use_reloader=False, loop=asyncio.get_event_loop()) 9 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/arena/README.md: -------------------------------------------------------------------------------- 1 | 本模块基于 0皆无0(NGA uid=60429400)dalao的[PCR姬器人:可可萝·Android](https://nga.178.com/read.php?tid=18434108),移植至nonebot框架而成。 2 | 3 | 重构 by IceCoffee 4 | 5 | 源代码的使用已获原作者授权。 6 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/gacha/README.md: -------------------------------------------------------------------------------- 1 | 本模块基于 0皆无0(NGA uid=60429400)dalao的[PCR姬器人:可可萝·Android](https://nga.178.com/read.php?tid=18434108),移植至nonebot框架而成。 2 | 3 | 重构 by IceCoffee 4 | 5 | 源代码的使用已获原作者授权。 6 | -------------------------------------------------------------------------------- /hoshino/util/textfilter/README.md: -------------------------------------------------------------------------------- 1 | 本模块参考自 [textfilter](https://github.com/observerss/textfilter) (NO LICENSE) 2 | 敏感词库参考自 [funNLP](https://github.com/fighting41love/funNLP/tree/master/data/%E6%95%8F%E6%84%9F%E8%AF%8D%E5%BA%93) (NO LICENSE) 3 | 感谢原作者们! 4 | -------------------------------------------------------------------------------- /hoshino/config_example/groupmaster.py: -------------------------------------------------------------------------------- 1 | increase_welcome = { 2 | "default": "欢迎入群!", 3 | 1000000: "欢迎新群员", 4 | } 5 | 6 | join_approve = { 7 | 1000000: { 8 | "keywords": ["入群暗号"], 9 | "reject_when_not_match": True 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /hoshino/modules/botmanage/get_cqcode.py: -------------------------------------------------------------------------------- 1 | from hoshino import sucmd 2 | from hoshino.typing import CommandSession 3 | from hoshino.util import escape 4 | 5 | @sucmd('取码', force_private=False) 6 | async def get_cqcode(session: CommandSession): 7 | await session.send(escape(str(session.current_arg))) 8 | -------------------------------------------------------------------------------- /hoshino/modules/botmanage/data_cleaner.py: -------------------------------------------------------------------------------- 1 | import hoshino 2 | from hoshino.typing import CommandSession 3 | 4 | @hoshino.sucmd('清理数据') 5 | async def clean_image(session: CommandSession): 6 | await hoshino.get_bot().clean_data_dir(self_id=session.event.self_id, 7 | data_dir='image') 8 | await session.send('Image 文件夹已清理') 9 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/buy_potion_reminder.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service, R 2 | 3 | sv = Service('buy_potion_reminder', enable_on_default=False, help_='买药提醒') 4 | 5 | @sv.scheduled_job('cron', hour='*/6') 6 | async def hour_call(): 7 | pic = R.img("BuyPotion.jpg").cqcode 8 | msg = f'骑士君,该上线买经验药水啦~\n{pic}' 9 | await sv.broadcast(msg, 'buy_potion_reminder') 10 | -------------------------------------------------------------------------------- /hoshino/typing.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | from typing import (Any, Callable, Dict, Iterable, List, NamedTuple, Optional, 3 | Set, Tuple, Union) 4 | 5 | from aiocqhttp import Event as CQEvent 6 | from nonebot import (CommandSession, CQHttpError, Message, MessageSegment, 7 | NLPSession, NoticeSession, RequestSession) 8 | 9 | from . import HoshinoBot 10 | -------------------------------------------------------------------------------- /hoshino/modules/botmanage/group_invite.py: -------------------------------------------------------------------------------- 1 | import nonebot 2 | from nonebot import RequestSession, on_request 3 | from hoshino import util 4 | 5 | 6 | @on_request('group.invite') 7 | async def handle_group_invite(session: RequestSession): 8 | if session.ctx['user_id'] in nonebot.get_bot().config.SUPERUSERS: 9 | await session.approve() 10 | else: 11 | await session.reject(reason='邀请入群请联系维护组') 12 | -------------------------------------------------------------------------------- /hoshino/modules/botmanage/alert.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_notice, NoticeSession 2 | 3 | @on_notice('group_decrease.kick_me') 4 | async def kick_me_alert(session: NoticeSession): 5 | group_id = session.event.group_id 6 | operator_id = session.event.operator_id 7 | coffee = session.bot.config.SUPERUSERS[0] 8 | await session.bot.send_private_msg(self_id=session.event.self_id, user_id=coffee, message=f'被Q{operator_id}踢出群{group_id}') 9 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/query/__init__.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service 2 | 3 | sv_help = ''' 4 | [pcr速查] 常用网址/图书馆 5 | [bcr速查] B服萌新攻略 6 | [日rank] rank推荐表 7 | [台rank] rank推荐表 8 | [陆rank] rank推荐表 9 | [挖矿15001] 矿场余钻 10 | [黄骑充电表] 黄骑1动规律 11 | [一个顶俩] 台服接龙小游戏 12 | [谁是霸瞳] 角色别称查询 13 | '''.strip() 14 | 15 | sv = Service('pcr-query', help_=sv_help, bundle='pcr查询') 16 | 17 | from .query import * 18 | from .whois import * 19 | from .miner import * 20 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Quart==0.14.1 2 | MarkupSafe~=1.0 3 | Jinja2~=2.11.2 4 | werkzeug~=1.0.1 5 | wsproto~=0.15.0 6 | nonebot[scheduler]==1.8.0 7 | aiocqhttp~=1.4.0 8 | aiohttp~=3.8.1 9 | lxml>=4.9.1 10 | pytz>=2019.3 11 | requests>=2.23.0 12 | sogou_tr_free~=0.0.8 13 | zhconv~=1.4.1 14 | Pillow~=9.1.0 15 | matplotlib~=3.2.1 16 | numpy~=1.22.3 17 | beautifulsoup4~=4.9.0 18 | pygtrie~=2.3.3 19 | tinydb~=4.1.1 20 | peony-twitter[all]~=2.1.2 21 | cloudscraper~=1.2.60 22 | -------------------------------------------------------------------------------- /hoshino/modules/groupmaster/join_approve.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_request, RequestSession 2 | import hoshino 3 | 4 | @on_request('group.add') 5 | async def join_approve(session: RequestSession): 6 | cfg = hoshino.config.groupmaster.join_approve 7 | gid = session.event.group_id 8 | if gid not in cfg: 9 | return 10 | for k in cfg[gid].get('keywords', []): 11 | if k in session.event.comment: 12 | await session.approve() 13 | return 14 | if cfg[gid].get('reject_when_not_match', False): 15 | await session.reject() 16 | return 17 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/arena_reminder.py: -------------------------------------------------------------------------------- 1 | from hoshino.service import Service 2 | 3 | svtw = Service('pcr-arena-reminder-tw', enable_on_default=False, help_='背刺时间提醒(台B)', bundle='pcr订阅') 4 | svjp = Service('pcr-arena-reminder-jp', enable_on_default=False, help_='背刺时间提醒(日)', bundle='pcr订阅') 5 | msg = '骑士君、准备好背刺了吗?' 6 | 7 | @svtw.scheduled_job('cron', hour='14', minute='45') 8 | async def pcr_reminder_tw(): 9 | await svtw.broadcast(msg, 'pcr-reminder-tw', 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/config_example/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import os 3 | from hoshino import log 4 | from nonebot.default_config import * 5 | from .__bot__ import * 6 | 7 | # check correctness 8 | RES_DIR = os.path.expanduser(RES_DIR) 9 | assert RES_PROTOCOL in ('http', 'file', 'base64') 10 | assert len(SUPERUSERS) >= 1 11 | 12 | # load module configs 13 | logger = log.new_logger('config', DEBUG) 14 | for module in MODULES_ON: 15 | try: 16 | importlib.import_module('hoshino.config.' + module) 17 | logger.info(f'Succeeded to load config of "{module}"') 18 | except ModuleNotFoundError: 19 | logger.warning(f'Not found config of "{module}"') 20 | -------------------------------------------------------------------------------- /hoshino/modules/groupmaster/loli_is_justice.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from hoshino import Service, priv, util 3 | from hoshino.typing import CQEvent, CQHttpError, MessageSegment as ms 4 | 5 | sv = Service('loli-is-justice', enable_on_default=False, visible=False) 6 | 7 | @sv.on_keyword('炼铜', '恋童') 8 | async def _(bot, ev: CQEvent): 9 | priv.set_block_user(ev.user_id, timedelta(hours=1)) 10 | await util.silence(ev, 3600, skip_su=False) 11 | await bot.send(ev, f'{ms.at(ev.user_id)} 控二次元萝莉管你毛事?肿瘤痴差不多得了😅') 12 | try: 13 | await bot.delete_msg(self_id=ev.self_id, message_id=ev.message_id) 14 | except CQHttpError: 15 | pass 16 | -------------------------------------------------------------------------------- /hoshino/modules/groupmaster/anti_asoul.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from hoshino import Service, priv, util 3 | from hoshino.typing import CQEvent, CQHttpError, MessageSegment as ms 4 | 5 | sv = Service('anti-asoul', enable_on_default=False) 6 | 7 | @sv.on_keyword('嘉然', '然然', '嘉心糖', '嘉人') 8 | @sv.on_rex(r'(嘉[\.\s]*(然|人))|(嘉[\.\s]*心[\.\s]*糖)') 9 | async def anti_holo(bot, ev: CQEvent): 10 | priv.set_block_user(ev.user_id, timedelta(minutes=1)) 11 | await util.silence(ev, 60, skip_su=False) 12 | await bot.send(ev, f'{ms.at(ev.user_id)} 你可少看点虚拟管人吧😅') 13 | try: 14 | await bot.delete_msg(self_id=ev.self_id, message_id=ev.message_id) 15 | except CQHttpError: 16 | pass 17 | -------------------------------------------------------------------------------- /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/hourcall/hourcall.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | from datetime import datetime 3 | import hoshino 4 | from hoshino import Service 5 | 6 | sv = Service('hourcall', enable_on_default=False, help_='时报') 7 | tz = pytz.timezone('Asia/Shanghai') 8 | 9 | def get_hour_call(): 10 | """挑出一组时报,每日更换,一日之内保持相同""" 11 | cfg = hoshino.config.hourcall 12 | now = datetime.now(tz) 13 | hc_groups = cfg.HOUR_CALLS_ON 14 | g = hc_groups[ now.day % len(hc_groups) ] 15 | return cfg.HOUR_CALLS[g] 16 | 17 | 18 | @sv.scheduled_job('cron', hour='*') 19 | async def hour_call(): 20 | now = datetime.now(tz) 21 | if 2 <= now.hour <= 4: 22 | return # 宵禁 免打扰 23 | msg = get_hour_call()[now.hour] 24 | await sv.broadcast(msg, 'hourcall', 0) 25 | -------------------------------------------------------------------------------- /hoshino/modules/groupmaster/anti_lex.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service, R 2 | from hoshino.util import FreqLimiter 3 | from hoshino.typing import CQEvent, HoshinoBot 4 | 5 | sv = Service('anti-lex', enable_on_default=False, help_='反蕾打卡提醒') 6 | lmt = FreqLimiter(3600) 7 | 8 | 9 | @sv.scheduled_job('cron', hour='*/8') 10 | async def hour_call(): 11 | pic = R.img("lexbiss.jpg").cqcode 12 | msg = f'{pic}\n共创和谐环境人人有责 拿出行动天天打卡🍒Σ打卡帖nga.178.com/read.php?tid=29780767' 13 | await sv.broadcast(msg, 'anti-lex') 14 | 15 | 16 | @sv.on_keyword('蕾皇', 'lex') 17 | async def keyword_anti(bot: HoshinoBot, ev: CQEvent): 18 | pic = R.img("lexbiss.jpg").cqcode 19 | if lmt.check(ev.group_id): 20 | await bot.send(ev, pic) 21 | lmt.start_cd(ev.group_id) 22 | -------------------------------------------------------------------------------- /hoshino/modules/botmanage/group_leave.py: -------------------------------------------------------------------------------- 1 | import re 2 | from hoshino import sucmd 3 | from hoshino.typing import CommandSession 4 | 5 | @sucmd('quit', aliases=('退群',)) 6 | async def quit_group(session: CommandSession): 7 | args = session.current_arg_text.split() 8 | failed = [] 9 | succ = [] 10 | for arg in args: 11 | if not re.fullmatch(r'^\d+$', arg): 12 | failed.append(arg) 13 | continue 14 | try: 15 | await session.bot.set_group_leave(self_id=session.event.self_id, group_id=arg) 16 | succ.append(arg) 17 | except: 18 | failed.append(arg) 19 | msg = f'已尝试退出{len(succ)}个群' 20 | if failed: 21 | msg += f"\n失败{len(failed)}个群:{failed}" 22 | await session.send(msg) 23 | -------------------------------------------------------------------------------- /hoshino/config_example/hourcall.py: -------------------------------------------------------------------------------- 1 | HOUR_CALLS_ON = [ 2 | "HOUR_CALL_1", 3 | ] 4 | 5 | HOUR_CALLS = { 6 | "HOUR_CALL_1": [ 7 | "午夜0点.", 8 | "1 o'clock.", 9 | "2 o'clock.", 10 | "3 o'clock.", 11 | "4 o'clock.", 12 | "5 o'clock.", 13 | "6 o'clock.", 14 | "7 o'clock.", 15 | "8 o'clock.", 16 | "9 o'clock.", 17 | "10 o'clock.", 18 | "11 o'clock.", 19 | "Noon. 好,该吃午饭了。", 20 | "1 o'clock.", 21 | "2 o'clock.", 22 | "3 o'clock.", 23 | "4 o'clock.", 24 | "5 o'clock.", 25 | "6 o'clock.", 26 | "7 o'clock.", 27 | "8 o'clock.", 28 | "9 o'clock.", 29 | "10 o'clock.", 30 | "11 o'clock.", 31 | ], 32 | } -------------------------------------------------------------------------------- /hoshino/modules/groupmaster/sleeping_set.py: -------------------------------------------------------------------------------- 1 | import re 2 | import math 3 | import random 4 | 5 | 6 | 7 | from hoshino import Service, priv, util 8 | from hoshino.typing import CQEvent 9 | 10 | sv = Service('sleeping-set', help_=''' 11 | [精致睡眠] 8小时精致睡眠(bot需具有群管理权限) 12 | [给我来一份精致昏睡下午茶套餐] 叫一杯先辈特调红茶(bot需具有群管理权限) 13 | '''.strip()) 14 | 15 | @sv.on_fullmatch('睡眠套餐', '休眠套餐', '精致睡眠', '来一份精致睡眠套餐') 16 | async def sleep_8h(bot, ev): 17 | await util.silence(ev, 8*60*60, skip_su=False) 18 | 19 | 20 | @sv.on_rex(r'(来|來)(.*(份|个)(.*)(睡|茶)(.*))套餐') 21 | async def sleep(bot, ev: CQEvent): 22 | base = 0 if '午' in ev.plain_text else 5*60*60 23 | length = len(ev.plain_text) 24 | sleep_time = base + round(math.sqrt(length) * 60 * 30 + 60 * random.randint(-15, 15)) 25 | await util.silence(ev, sleep_time, skip_su=False) 26 | -------------------------------------------------------------------------------- /hoshino/modules/groupmaster/anti_msg_recall.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service, util 2 | from hoshino.typing import NoticeSession, Message 3 | 4 | sv = Service('anti-msg-recall', help_='防撤回', enable_on_default=False) 5 | 6 | @sv.on_notice('group_recall') 7 | async def anti_msg_recall(session: NoticeSession): 8 | uid = session.event.user_id 9 | if uid == session.event.operator_id: 10 | data = await session.bot.get_msg(self_id=session.event.self_id, message_id=session.event.message_id) 11 | cardname = data.get('sender', {}).get('card') 12 | nickname = data.get('sender', {}).get('nickname') 13 | name = cardname or nickname or uid 14 | msg = data.get('message') 15 | msg = util.filt_message(Message(msg)) 16 | if msg: 17 | await session.send(f'{name}({uid})撤回了:\n{msg}') 18 | -------------------------------------------------------------------------------- /hoshino/config_example/picfinder.py: -------------------------------------------------------------------------------- 1 | threshold = 70 # SauceNAO相似度阈值,低于该相似度自动追加ascii2d搜索 2 | SAUCENAO_KEY = "" # SauceNAO 的 API key 3 | SAUCENAO_RESULT_NUM = 3 # SauceNAO搜索结果显示数量 4 | ASCII_RESULT_NUM = 3 # ascii2d搜索结果显示数量 5 | SEARCH_TIMEOUT = 60 # 连续搜索模式超时时长 6 | DAILY_LIMIT = 5 # 搜图每日限额 7 | CHAIN_REPLY = True # 是否启用合并转发回复模式 8 | THUMB_ON = True # 是否启用缩略图 9 | CHECK = True # 是否开启手机截屏判定 10 | IGNORE_STAMP = True # 是否在批量搜索中忽略表情包 11 | 12 | # 自定义Host,不使用留空即可 13 | # 格式示例:'https://ascii2d.net' , 'http://localhost:12345' 14 | HOST_CUSTOM = { 15 | 'SAUCENAO': '', 16 | 'ASCII': '' 17 | } 18 | 19 | # 网络代理 20 | proxies = { 21 | 'http': '', 22 | 'https': '' 23 | } 24 | 25 | # 频道白名单 格式{频道id: [子频道id]} 26 | enableguild = {} 27 | 28 | helptext = ''' 29 | [@bot+图片] 单张/多张搜图 30 | [星乃搜图] 进入批量搜图模式 31 | [谢谢星乃] 退出批量搜图模式 32 | '''.strip() 33 | -------------------------------------------------------------------------------- /hoshino/modules/botmanage/feedback.py: -------------------------------------------------------------------------------- 1 | import hoshino 2 | from hoshino import Service, priv 3 | from hoshino.typing import CQEvent 4 | from hoshino.util import DailyNumberLimiter 5 | 6 | sv = Service('_feedback_', manage_priv=priv.SUPERUSER, help_='[来杯咖啡] 后接反馈内容 联系维护组') 7 | 8 | _max = 1 9 | lmt = DailyNumberLimiter(_max) 10 | EXCEED_NOTICE = f'您今天已经喝过{_max}杯了,请明早5点后再来!' 11 | 12 | @sv.on_prefix('来杯咖啡') 13 | async def feedback(bot, ev: CQEvent): 14 | uid = ev.user_id 15 | if not lmt.check(uid): 16 | await bot.finish(ev, EXCEED_NOTICE, at_sender=True) 17 | coffee = hoshino.config.SUPERUSERS[0] 18 | text = str(ev.message).strip() 19 | if not text: 20 | await bot.send(ev, "请发送来杯咖啡+您要反馈的内容~", at_sender=True) 21 | else: 22 | await bot.send_private_msg(self_id=ev.self_id, user_id=coffee, message=f'Q{uid}@群{ev.group_id}\n{text}') 23 | await bot.send(ev, f'您的反馈已发送至维护组!\n======\n{text}', at_sender=True) 24 | lmt.increase(uid) 25 | -------------------------------------------------------------------------------- /hoshino/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | 5 | os.makedirs('./log', exist_ok=True) 6 | _error_log_file = os.path.expanduser('./log/error.log') 7 | _critical_log_file = os.path.expanduser('./log/critical.log') 8 | 9 | formatter = logging.Formatter('[%(asctime)s %(name)s] %(levelname)s: %(message)s') 10 | default_handler = logging.StreamHandler(sys.stdout) 11 | default_handler.setFormatter(formatter) 12 | error_handler = logging.FileHandler(_error_log_file, encoding='utf8') 13 | error_handler.setLevel(logging.ERROR) 14 | error_handler.setFormatter(formatter) 15 | critical_handler = logging.FileHandler(_critical_log_file, encoding='utf8') 16 | critical_handler.setLevel(logging.CRITICAL) 17 | critical_handler.setFormatter(formatter) 18 | 19 | 20 | def new_logger(name, debug=True): 21 | logger = logging.getLogger(name) 22 | logger.addHandler(default_handler) 23 | logger.addHandler(error_handler) 24 | logger.addHandler(critical_handler) 25 | logger.setLevel(logging.DEBUG if debug else logging.INFO) 26 | return logger 27 | -------------------------------------------------------------------------------- /hoshino/modules/groupmaster/antiqks.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | from hoshino import R, Service, util 3 | 4 | sv = Service('antiqks', help_='识破骑空士的阴谋') 5 | 6 | qksimg = R.img('antiqks.jpg').cqcode 7 | 8 | @sv.on_keyword("granbluefantasy.jp") 9 | async def qks_keyword(bot, ev): 10 | msg = f'骑空士爪巴\n{qksimg}' 11 | await bot.send(ev, msg, at_sender=True) 12 | await util.silence(ev, 60) 13 | 14 | # 有潜在的安全问题 15 | # @sv.on_rex(r'[a-zA-Z0-9\.]{4,12}\/[a-zA-Z0-9]+') 16 | async def qks_rex(bot, ev): 17 | match = ev.match 18 | msg = f'骑空士爪巴远点\n{qksimg}' 19 | res = 'http://'+match.group(0) 20 | async with aiohttp.TCPConnector(verify_ssl=False) as connector: 21 | async with aiohttp.request( 22 | 'GET', 23 | url=res, 24 | allow_redirects=False, 25 | connector=connector, 26 | ) as resp: 27 | h = resp.headers 28 | s = resp.status 29 | if s == 301 or s == 302: 30 | if 'granbluefantasy.jp' in h['Location']: 31 | await bot.send(ev, msg, at_sender=True) 32 | await util.silence(ev, 60) 33 | -------------------------------------------------------------------------------- /hoshino/config_example/pcrclanbattle.py: -------------------------------------------------------------------------------- 1 | class JP: 2 | BOSS_HP = [ 3 | [ 6000000, 8000000, 10000000, 12000000, 15000000], 4 | [ 6000000, 8000000, 10000000, 12000000, 15000000], 5 | [12000000, 14000000, 17000000, 19000000, 22000000], 6 | [22000000, 23000000, 27000000, 29000000, 31000000], 7 | [95000000, 100000000, 110000000, 120000000, 130000000], 8 | ] 9 | 10 | 11 | class TW: 12 | BOSS_HP = [ 13 | [ 6000000, 8000000, 10000000, 12000000, 15000000], 14 | [ 6000000, 8000000, 10000000, 12000000, 15000000], 15 | [12000000, 14000000, 17000000, 19000000, 22000000], 16 | [19000000, 20000000, 23000000, 25000000, 27000000], 17 | [95000000, 100000000, 110000000, 120000000, 130000000], 18 | ] 19 | 20 | 21 | class BL: 22 | BOSS_HP = [ 23 | [ 6000000, 8000000, 10000000, 12000000, 15000000], 24 | [ 6000000, 8000000, 10000000, 12000000, 15000000], 25 | [ 7000000, 9000000, 13000000, 15000000, 20000000], 26 | [15000000, 16000000, 18000000, 19000000, 20000000], 27 | [15000000, 16000000, 18000000, 19000000, 20000000], 28 | ] 29 | -------------------------------------------------------------------------------- /hoshino/modules/deepchat/deepchat.py: -------------------------------------------------------------------------------- 1 | import random 2 | import hoshino 3 | from hoshino import Service, aiorequests, priv 4 | from hoshino.util import DailyNumberLimiter 5 | from hoshino.typing import CQEvent 6 | 7 | sv = Service('deepchat', manage_priv=priv.SUPERUSER, enable_on_default=False, visible=False) 8 | lmt = DailyNumberLimiter(10) 9 | 10 | @sv.on_message('group') 11 | async def deepchat(bot, ev: CQEvent): 12 | gid = ev.group_id 13 | 14 | if not lmt.check(gid): 15 | lmt.reset(gid) 16 | if lmt.get_num(gid): 17 | lmt.increase(gid) 18 | return 19 | 20 | msg = ev['message'].extract_plain_text() 21 | if not msg or random.random() > 0.060: 22 | return 23 | if not lmt.check(ev.group_id): 24 | return 25 | payload = { 26 | "msg": msg, 27 | "group": ev.group_id, 28 | "qq": ev.user_id, 29 | } 30 | sv.logger.debug(payload) 31 | api = hoshino.config.deepchat.deepchat_api 32 | rsp = await aiorequests.post(api, data=payload, timeout=10) 33 | rsp = await rsp.json() 34 | sv.logger.debug(rsp) 35 | if rsp['msg']: 36 | await bot.send(ev, rsp['msg']) 37 | lmt.increase(gid) 38 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/query/miner.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from hoshino.typing import CQEvent, MessageSegment as ms 3 | from . import sv 4 | 5 | this_season = np.zeros(15001, dtype=int) 6 | all_season = np.zeros(15001, dtype=int) 7 | 8 | this_season[1:11] = 50 9 | this_season[11:101] = 10 10 | this_season[101:201] = 5 11 | this_season[201:501] = 3 12 | this_season[501:1001] = 2 13 | this_season[1001:2001] = 2 14 | this_season[2001:4000] = 1 15 | this_season[4000:8100:100] = 50 16 | this_season[8100:15001:100] = 15 17 | 18 | all_season[1:11] = 500 19 | all_season[11:101] = 50 20 | all_season[101:201] = 30 21 | all_season[201:501] = 10 22 | all_season[501:1001] = 5 23 | all_season[1001:2001] = 3 24 | all_season[2001:4001] = 2 25 | all_season[4001:8000] = 1 26 | all_season[8000:15001:100] = 30 27 | 28 | 29 | @sv.on_prefix('挖矿', 'jjc钻石', '竞技场钻石', 'jjc钻石查询', '竞技场钻石查询') 30 | async def arena_miner(bot, ev: CQEvent): 31 | try: 32 | rank = int(ev.message.extract_plain_text()) 33 | except: 34 | return 35 | rank = np.clip(rank, 1, 15001) 36 | s_all = all_season[1:rank].sum() 37 | s_this = this_season[1:rank].sum() 38 | msg = f"{ms.at(ev.user_id)}\n最高排名奖励还剩{s_this}钻\n历届最高排名还剩{s_all}钻" 39 | await bot.send(ev, msg) 40 | -------------------------------------------------------------------------------- /hoshino/config_example/__bot__.py: -------------------------------------------------------------------------------- 1 | """这是一份实例配置文件 2 | 3 | 将其修改为你需要的配置,并将文件夹config_example重命名为config 4 | """ 5 | 6 | # hoshino监听的端口与ip 7 | PORT = 8080 8 | HOST = '127.0.0.1' # 本地部署使用此条配置(QQ客户端和bot端运行在同一台计算机) 9 | # HOST = '0.0.0.0' # 开放公网访问使用此条配置(不安全) 10 | 11 | DEBUG = False # 调试模式 12 | 13 | BLACK_LIST = [1974906693] # 黑名单,权限为 BLACK = -999 14 | WHITE_LIST = [] # 白名单,权限为 WHITE = 51 15 | SUPERUSERS = [10000] # 填写超级用户的QQ号,可填多个用半角逗号","隔开,权限为 SUPERUSER = 999 16 | NICKNAME = '' # 机器人的昵称。呼叫昵称等同于@bot,可用元组配置多个昵称 17 | 18 | COMMAND_START = {''} # 命令前缀(空字符串匹配任何消息) 19 | COMMAND_SEP = set() # 命令分隔符(hoshino不需要该特性,保持为set()即可) 20 | 21 | # 发送图片的协议 22 | # 可选 http, file, base64 23 | # 当QQ客户端与bot端不在同一台计算机时,可用http协议 24 | RES_PROTOCOL = 'file' 25 | # 资源库文件夹,需可读可写,windows下注意反斜杠转义 26 | RES_DIR = r'./res/' 27 | # 使用http协议时需填写,原则上该url应指向RES_DIR目录 28 | RES_URL = 'http://127.0.0.1:5000/static/' 29 | 30 | 31 | # 启用的模块 32 | # 初次尝试部署时请先保持默认 33 | # 如欲启用新模块,请认真阅读部署说明,逐个启用逐个配置 34 | # 切忌一次性开启多个 35 | MODULES_ON = { 36 | 'botmanage', 37 | 'dice', 38 | 'groupmaster', 39 | # 'hourcall', 40 | # 'kancolle', 41 | # 'mikan', 42 | 'pcrclanbattle', 43 | 'priconne', 44 | # 'setu', 45 | # 'translate', 46 | # 'twitter', 47 | } 48 | -------------------------------------------------------------------------------- /hoshino/modules/groupmaster/group_notice.py: -------------------------------------------------------------------------------- 1 | 2 | import hoshino 3 | from hoshino import Service, util 4 | from hoshino.typing import NoticeSession, CQHttpError 5 | 6 | sv1 = Service('group-leave-notice', help_='退群通知') 7 | 8 | @sv1.on_notice('group_decrease.leave') 9 | async def leave_notice(session: NoticeSession): 10 | ev = session.event 11 | name = ev.user_id 12 | if ev.user_id == ev.self_id: 13 | return 14 | try: 15 | info = await session.bot.get_stranger_info(self_id=ev.self_id, user_id=ev.user_id) 16 | name = info['nickname'] or name 17 | name = util.filt_message(name) 18 | except CQHttpError as e: 19 | sv1.logger.exception(e) 20 | await session.send(f"{name}({ev.user_id})退群了。") 21 | 22 | 23 | sv2 = Service('group-welcome', help_='入群欢迎') 24 | 25 | @sv2.on_notice('group_increase') 26 | async def increace_welcome(session: NoticeSession): 27 | 28 | if session.event.user_id == session.event.self_id: 29 | return # ignore myself 30 | 31 | welcomes = hoshino.config.groupmaster.increase_welcome 32 | gid = session.event.group_id 33 | if gid in welcomes: 34 | await session.send(welcomes[gid], at_sender=True) 35 | # elif 'default' in welcomes: 36 | # await session.send(welcomes['default'], at_sender=True) 37 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/query/whois.py: -------------------------------------------------------------------------------- 1 | from hoshino.typing import CQEvent 2 | from hoshino.util import FreqLimiter, filt_message 3 | 4 | from .. import chara 5 | from . import sv 6 | 7 | lmt = FreqLimiter(5) 8 | 9 | @sv.on_suffix('是谁') 10 | @sv.on_prefix('谁是') 11 | async def whois(bot, ev: CQEvent): 12 | name = ev.message.extract_plain_text().strip() 13 | if not name: 14 | return 15 | id_ = chara.name2id(name) 16 | confi = 100 17 | guess = False 18 | if id_ == chara.UNKNOWN: 19 | id_, guess_name, confi = chara.guess_id(name) 20 | guess = True 21 | c = chara.fromid(id_) 22 | 23 | if confi < 60: 24 | return 25 | 26 | uid = ev.user_id 27 | if not lmt.check(uid): 28 | await bot.finish(ev, f'兰德索尔花名册冷却中(剩余 {int(lmt.left_time(uid)) + 1}秒)', at_sender=True) 29 | 30 | lmt.start_cd(uid, 120 if guess else 0) 31 | if guess: 32 | name = filt_message(name) 33 | msg = f'兰德索尔似乎没有叫"{name}"的人...\n角色别称补全计划: github.com/Ice9Coffee/LandosolRoster' 34 | await bot.send(ev, msg) 35 | msg = f'您有{confi}%的可能在找{guess_name} {await c.get_icon_cqcode()} {c.name}' 36 | await bot.send(ev, msg) 37 | else: 38 | msg = f'{await c.get_icon_cqcode()} {c.name}' 39 | await bot.send(ev, msg, at_sender=True) 40 | -------------------------------------------------------------------------------- /hoshino/modules/pcrclanbattle/version_selector.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service, priv 2 | from hoshino.typing import CQEvent 3 | 4 | sv = Service('clanbattle-version-selector', manage_priv=priv.SUPERUSER, visible=False) 5 | 6 | help_str = ''' 7 | 请*群管理*或*群主*发送【】内的命令 8 | 【启用会战v2】 9 | Hoshino开源版 命令以感叹号开头 10 | 适用于2021年6月前的日服、2021年10月前的台服、2023年6月前的B服 11 | 12 | 【启用会战v3】 13 | 指令简化版 无web面板 14 | 适用于2021年6月前的日服、2021年10月前的台服、2023年6月前的B服 15 | 16 | 【启用会战v4】 17 | 适用于2021年7月后的日服、2021年11月后的台服、2023年7月后的B服 18 | web绝赞开发中 19 | '''.strip() 20 | 21 | @sv.on_prefix('会战启用', '启用会战') 22 | async def version_select(bot, ev: CQEvent): 23 | gid = ev.group_id 24 | arg = ev.message.extract_plain_text() 25 | svs = Service.get_loaded_services() 26 | cbsvs = { 27 | 'v2': svs.get('clanbattle'), 28 | 'v3': svs.get('clanbattlev3'), 29 | 'v4': svs.get('clanbattlev4'), 30 | } 31 | if arg not in cbsvs: 32 | await bot.finish(ev, help_str) 33 | if not priv.check_priv(ev, priv.ADMIN): 34 | await bot.finish(ev, '只有*群管理*和*群主*才能切换会战管理版本') 35 | if not cbsvs[arg]: 36 | await bot.finish(ev, f'本bot未实装clanbattle{arg},请加入Hoshinoのお茶会(787493356)体验!') 37 | for k, v in cbsvs.items(): 38 | v.set_enable(gid) if k == arg else v.set_disable(gid) 39 | await bot.send(ev, f'已启用clanbattle{arg}\n{cbsvs[arg].help}') 40 | -------------------------------------------------------------------------------- /hoshino/modules/flac/flac.py: -------------------------------------------------------------------------------- 1 | """无损音乐搜索 数据来自acgjc.com""" 2 | from hoshino import Service, priv, logger, aiorequests 3 | from hoshino.typing import CQEvent 4 | from urllib.parse import quote 5 | 6 | sv = Service('flac', help_='[搜无损] +关键词搜索') 7 | 8 | @sv.on_prefix('搜无损') 9 | async def search_flac(bot, ev: CQEvent): 10 | keyword = ev.message.extract_plain_text() 11 | resp = await aiorequests.get('http://mtage.top:8099/acg-music/search', params={'title-keyword': keyword}, timeout=1) 12 | res = await resp.json() 13 | if res['success'] is False: 14 | logger.error(f"Flac query failed.\nerrorCode={res['errorCode']}\nerrorMsg={res['errorMsg']}") 15 | await bot.finish(ev, f'查询失败 请至acgjc官网查询 www.acgjc.com/?s={quote(keyword)}', at_sender=True) 16 | 17 | music_list = res['result']['content'] 18 | music_list = music_list[:min(5, len(music_list))] 19 | 20 | details = [" ".join([ 21 | f"{ele['title']}", 22 | f"{ele['downloadLink']}", 23 | f"密码:{ele['downloadPass']}" if ele['downloadPass'] else "" 24 | ]) for ele in music_list] 25 | 26 | msg = [ 27 | f"共 {res['result']['totalElements']} 条结果" if len(music_list) > 0 else '没有任何结果', 28 | *details, 29 | '数据来自 www.acgjc.com', 30 | f'更多结果可见 www.acgjc.com/?s={quote(keyword)}' 31 | ] 32 | 33 | await bot.send(ev, '\n'.join(msg), at_sender=True) 34 | -------------------------------------------------------------------------------- /hoshino/modules/kancolle/query/_senka_spider.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import requests 4 | from PIL import Image 5 | from io import BytesIO 6 | 7 | proxies = { 8 | "http": "http://127.0.0.1:10809", 9 | "https": "http://127.0.0.1:10809", 10 | } 11 | 12 | def get_rank_id(yy, mm, ss): 13 | return f"rank{yy:02d}{mm:02d}{ss:02d}.jpg" 14 | 15 | def get_url(yy, mm, ss): 16 | return f"http://203.104.209.7/kcscontents/information/image/{get_rank_id(yy, mm, ss)}" 17 | 18 | def download_img(save_path, link): 19 | print('download_img from ', link) 20 | resp = requests.get(link, stream=True, proxies=proxies) 21 | print(f'status_code={resp.status_code}', end=' ') 22 | if 200 == resp.status_code: 23 | if re.search(r'image', resp.headers['content-type'], re.I): 24 | print(f'content=type is image, saving to {save_path}', end='...') 25 | img = Image.open(BytesIO(resp.content)) 26 | img.save(save_path) 27 | print('OK', end='') 28 | print('\n', end='') 29 | 30 | 31 | if __name__ == "__main__": 32 | for yy in range(20, 12, -1): 33 | for mm in range(12, 0, -1): 34 | for ss in range(1, 21): 35 | url = get_url(yy, mm, ss) 36 | save_path = os.path.expanduser(f'~/.hoshino/tmp/{get_rank_id(yy, mm, ss)}') 37 | download_img(save_path, url) 38 | -------------------------------------------------------------------------------- /hoshino/modules/twitter/stream/util.py: -------------------------------------------------------------------------------- 1 | import re 2 | from datetime import datetime 3 | 4 | import peony 5 | import pytz 6 | from hoshino import util 7 | from hoshino.typing import MessageSegment as ms 8 | 9 | 10 | def format_time(time_str): 11 | dt = datetime.strptime(time_str, r"%a %b %d %H:%M:%S %z %Y") 12 | dt = dt.astimezone(pytz.timezone("Asia/Shanghai")) 13 | return f"{util.month_name(dt.month)}{util.date_name(dt.day)}・{util.time_name(dt.hour, dt.minute)}" 14 | 15 | 16 | def format_tweet(tweet): 17 | name = tweet.user.name 18 | # avatar = tweet.user.get("profile_image_url_https", "https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png") 19 | # avatar = re.sub(r'_normal(\.(jpg|jpeg|png|gif|jfif|webp))$', r'_200x200\1', avatar, re.I) 20 | 21 | if peony.events.retweet(tweet): 22 | return f"@{name} 转推了\n>>>>>\n{format_tweet(tweet.retweeted_status)}" 23 | 24 | time = format_time(tweet.created_at) 25 | text = tweet.text 26 | media = tweet.get("extended_entities", {}).get("media", []) 27 | imgs = "".join([str(ms.image(m.media_url)) for m in media]) 28 | msg = f"@{name}\n{time}\n\n{text}" 29 | if imgs: 30 | msg = f"{msg}\n{imgs}" 31 | 32 | if "quoted_status" in tweet: 33 | quoted_msg = format_tweet(tweet.quoted_status) 34 | msg = f"{quoted_msg}\n\n<<<<<\n{msg}" 35 | 36 | return msg 37 | -------------------------------------------------------------------------------- /hoshino/modules/pcrclanbattle/hedao.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service 2 | from hoshino.typing import CQEvent 3 | 4 | sv = Service("pcr-hedao", help_="请输入:合刀 刀1伤害 刀2伤害 剩余血量\n如:合刀 50 60 70") 5 | 6 | 7 | @sv.on_prefix("合刀") 8 | async def feedback(bot, ev: CQEvent): 9 | cmd = ev.message.extract_plain_text() 10 | content = cmd.split() 11 | print(content) 12 | if len(content) != 3: 13 | await bot.finish(ev, sv.help) 14 | try: 15 | d1 = float(content[0]) 16 | d2 = float(content[1]) 17 | rest = float(content[2]) 18 | except (ValueError, RuntimeError): 19 | await bot.finish(ev, sv.help) 20 | if d1 + d2 < rest: 21 | await bot.finish(ev, "醒醒!这两刀是打不死boss的") 22 | dd1 = d1 23 | dd2 = d2 24 | if d1 >= rest: 25 | dd1 = rest 26 | if d2 >= rest: 27 | dd2 = rest 28 | res1 = (1 - (rest - dd1) / dd2) * 90 + 20 29 | # 1先出,2能得到的时间 30 | res2 = (1 - (rest - dd2) / dd1) * 90 + 20 31 | # 2先出,1能得到的时间 32 | res1 = round(res1, 2) 33 | res2 = round(res2, 2) 34 | res1 = min(res1, 90) 35 | res2 = min(res2, 90) 36 | reply = f"{d1}先出,另一刀可获得 {res1} 秒补偿刀\n{d2}先出,另一刀可获得 {res2} 秒补偿刀\n" 37 | if d1 >= rest or d2 >= rest: 38 | reply += "\n注:" 39 | if d1 >= rest: 40 | reply += f"\n第一刀可直接秒杀boss,伤害按 {rest} 计算" 41 | if d2 >= rest: 42 | reply += f"\n第二刀可直接秒杀boss,伤害按 {rest} 计算" 43 | await bot.send(ev, reply) 44 | -------------------------------------------------------------------------------- /hoshino/modules/botmanage/broadcast.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import hoshino 4 | from hoshino.service import sucmd 5 | from hoshino.typing import CommandSession, CQHttpError 6 | 7 | 8 | @sucmd('broadcast', aliases=('bc', '广播'), force_private=False) 9 | async def broadcast(session: CommandSession): 10 | msg = session.current_arg 11 | bot = session.bot 12 | ev = session.event 13 | su = session.event.user_id 14 | for sid in hoshino.get_self_ids(): 15 | gl = await bot.get_group_list(self_id=sid) 16 | gl = [g['group_id'] for g in gl] 17 | try: 18 | await bot.send_private_msg(self_id=sid, user_id=su, message=f"开始向{len(gl)}个群广播:\n{msg}") 19 | except Exception as e: 20 | hoshino.logger.error(f'向广播发起者发送广播摘要失败:{type(e)}') 21 | for g in gl: 22 | await asyncio.sleep(0.5) 23 | try: 24 | await bot.send_group_msg(self_id=sid, group_id=g, message=msg) 25 | hoshino.logger.info(f'群{g} 投递广播成功') 26 | except CQHttpError as e: 27 | hoshino.logger.error(f'群{g} 投递广播失败:{type(e)}') 28 | try: 29 | await bot.send_private_msg(self_id=sid, user_id=su, message=f'群{g} 投递广播失败:{type(e)}') 30 | except Exception as e: 31 | hoshino.logger.critical(f'向广播发起者进行错误回报时发生错误:{type(e)}') 32 | await bot.send_private_msg(self_id=ev.self_id, user_id=su, message='广播完成!') 33 | -------------------------------------------------------------------------------- /hoshino/modules/twitter-v2/stream/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import importlib 3 | 4 | from hoshino import Service, priv, sucmd, get_bot 5 | from hoshino.config import twitter as cfg 6 | from hoshino.typing import MessageSegment as CommandSession 7 | 8 | sv = Service("twitter-poller", use_priv=priv.SU, manage_priv=priv.SU, visible=False) 9 | bot = get_bot() 10 | daemon = None 11 | 12 | from .follow import follow_stream 13 | 14 | @bot.on_startup 15 | async def start_daemon(): 16 | global daemon 17 | 18 | loop = asyncio.get_event_loop() 19 | daemon = loop.create_task(stream_daemon(follow_stream)) 20 | 21 | 22 | async def stream_daemon(stream_func): 23 | while True: 24 | try: 25 | await stream_func() 26 | except (KeyboardInterrupt, asyncio.CancelledError): 27 | sv.logger.info("Twitter stream daemon exited.") 28 | raise 29 | except Exception as e: 30 | sv.logger.exception(e) 31 | sv.logger.error(f"Error {type(e)} Occurred in twitter stream. Restarting stream in 1 min.") 32 | await asyncio.sleep(60) 33 | 34 | 35 | @sucmd("reload-twitter-stream-daemon", force_private=False, aliases=("重启转推", "重载转推")) 36 | async def reload_twitter_stream_daemon(session: CommandSession): 37 | try: 38 | daemon.cancel() 39 | importlib.reload(cfg) 40 | await start_daemon() 41 | await session.send("ok") 42 | except Exception as e: 43 | sv.logger.exception(e) 44 | await session.send(f"Error: {type(e)}") 45 | -------------------------------------------------------------------------------- /hoshino/msghandler.py: -------------------------------------------------------------------------------- 1 | from nonebot.command import SwitchException 2 | 3 | from hoshino import CanceledException, message_preprocessor, trigger 4 | from hoshino.typing import CQEvent 5 | 6 | 7 | @message_preprocessor 8 | async def handle_message(bot, event: CQEvent, _): 9 | 10 | if event.detail_type != 'group': 11 | return 12 | 13 | for t in trigger.chain: 14 | for service_func in t.find_handler(event): 15 | if service_func.only_to_me and not event['to_me']: 16 | continue # not to me, ignore. 17 | 18 | if not service_func.sv._check_all(event): 19 | continue # permission denied. 20 | 21 | service_func.sv.logger.info(f'Message {event.message_id} triggered {service_func.__name__}.') 22 | try: 23 | await service_func.func(bot, event) 24 | except SwitchException: # the func says: continue to trigger another function. 25 | continue 26 | except CanceledException: # the func says: stop triggering. 27 | raise 28 | except Exception as e: # other general errors. 29 | service_func.sv.logger.error(f'{type(e)} occured when {service_func.__name__} handling message {event.message_id}.') 30 | service_func.sv.logger.exception(e) 31 | # the func completed successfully, stop triggering. (1 message for 1 function at most.) 32 | raise CanceledException('Handled by Hoshino') 33 | # exception raised, no need for break 34 | -------------------------------------------------------------------------------- /hoshino/modules/kancolle/reminder.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | from datetime import datetime 3 | 4 | from hoshino.service import Service 5 | 6 | sv = Service('kc-reminder', enable_on_default=False, help_='演习/任务/月常远征提醒', bundle='kancolle') 7 | 8 | @sv.scheduled_job('cron', hour='13', minute='30') 9 | async def enshu_reminder(): 10 | msgs = [ 11 | '演习即将刷新!\n莫让下午的自己埋怨上午的自己:\n「演習」で練度向上!0/3', 12 | '[CQ:at,qq=all] 演习即将刷新!', 13 | ] 14 | await sv.broadcast(msgs, 'enshu_reminder', 0.2) 15 | 16 | 17 | @sv.scheduled_job('cron', hour='1', minute='30') 18 | async def enshu_reminder_evening(): 19 | await sv.broadcast('夜猫子提督、夜は長いよ!夜戦、夜戦!\n「演習」で他提督を圧倒せよ!0/5', 'enshu_reminder_evening', 0.2) 20 | 21 | 22 | @sv.scheduled_job('cron', day='10-14', hour='22') 23 | async def ensei_reminder(): 24 | now = datetime.now(pytz.timezone('Asia/Shanghai')) 25 | remain_days = 15 - now.day 26 | if remain_days == 1: 27 | msg = '【远征提醒小助手】提醒您月常远征即将截止!\n你还有60分钟整备「ミ二号船团」并「与欧洲方面友军接触」!' 28 | elif remain_days == 2: 29 | msg = '【远征提醒小助手】提醒您月常远征还有2天刷新!\n9螺丝 爱领不领 随便你 哼~' 30 | elif remain_days == 3: 31 | msg = '【远征提醒小助手】提醒您月常远征还有3天刷新!\n现在开始还来得及...大概...' 32 | else: 33 | msg = f'【远征提醒小助手】提醒您月常远征还有{remain_days}天刷新!' 34 | msgs = [ 35 | msg, 36 | f'[CQ:at,qq=all] 月常远征还有{remain_days}天刷新!', 37 | ] 38 | await sv.broadcast(msgs, 'ensei_reminder', 0.5) 39 | 40 | 41 | @sv.scheduled_job('cron', hour='3', minute='30') 42 | async def daily_quest_refresh_reminder(): 43 | await sv.broadcast('現在時刻〇三三〇です。提督、そろそろ朝になっちゃいます。\n「遠征」を3回成功させよう!0/3', 'daily_quest_refresh_reminder', 0.2) 44 | -------------------------------------------------------------------------------- /hoshino/modules/setu/setu.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | 4 | from nonebot.exceptions import CQHttpError 5 | 6 | from hoshino import R, Service, priv 7 | from hoshino.util import FreqLimiter, DailyNumberLimiter 8 | 9 | _max = 5 10 | EXCEED_NOTICE = f'您今天已经冲过{_max}次了,请明早5点后再来!' 11 | _nlmt = DailyNumberLimiter(_max) 12 | _flmt = FreqLimiter(5) 13 | 14 | sv = Service('setu', manage_priv=priv.SUPERUSER, enable_on_default=True, visible=False) 15 | setu_folder = R.img('setu/').path 16 | 17 | def setu_gener(): 18 | while True: 19 | filelist = os.listdir(setu_folder) 20 | random.shuffle(filelist) 21 | for filename in filelist: 22 | if os.path.isfile(os.path.join(setu_folder, filename)): 23 | yield R.img('setu/', filename) 24 | 25 | setu_gener = setu_gener() 26 | 27 | def get_setu(): 28 | return setu_gener.__next__() 29 | 30 | 31 | @sv.on_rex(r'不够[涩瑟色]|[涩瑟色]图|来一?[点份张].*[涩瑟色]|再来[点份张]|看过了|铜') 32 | async def setu(bot, ev): 33 | """随机叫一份涩图,对每个用户有冷却时间""" 34 | uid = ev['user_id'] 35 | if not _nlmt.check(uid): 36 | await bot.send(ev, EXCEED_NOTICE, at_sender=True) 37 | return 38 | if not _flmt.check(uid): 39 | await bot.send(ev, '您冲得太快了,请稍候再冲', at_sender=True) 40 | return 41 | _flmt.start_cd(uid) 42 | _nlmt.increase(uid) 43 | 44 | # conditions all ok, send a setu. 45 | pic = get_setu() 46 | try: 47 | await bot.send(ev, pic.cqcode) 48 | except CQHttpError: 49 | sv.logger.error(f"发送图片{pic.path}失败") 50 | try: 51 | await bot.send(ev, '涩图太涩,发不出去勒...') 52 | except: 53 | pass 54 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/login_bonus.py: -------------------------------------------------------------------------------- 1 | import random 2 | from hoshino import Service, R 3 | from hoshino.typing import CQEvent 4 | from hoshino.util import DailyNumberLimiter 5 | 6 | sv = Service('pcr-login-bonus', bundle='pcr娱乐', help_='[星乃签到] 给主さま盖章章') 7 | 8 | lmt = DailyNumberLimiter(1) 9 | login_presents = [ 10 | '扫荡券×5', '卢币×1000', '普通EXP药水×5', '宝石×50', '玛那×3000', 11 | '扫荡券×10', '卢币×1500', '普通EXP药水×15', '宝石×80', '白金转蛋券×1', 12 | '扫荡券×15', '卢币×2000', '上级精炼石×3', '宝石×100', '白金转蛋券×1', 13 | ] 14 | todo_list = [ 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 | 46 | @sv.on_fullmatch('签到', '盖章', '妈', '妈?', '妈妈', '妈!', '妈!', '妈妈!', only_to_me=True) 47 | async def give_okodokai(bot, ev: CQEvent): 48 | uid = ev.user_id 49 | if not lmt.check(uid): 50 | await bot.send(ev, '明日はもう一つプレゼントをご用意してお待ちしますね', at_sender=True) 51 | return 52 | lmt.increase(uid) 53 | present = random.choice(login_presents) 54 | todo = random.choice(todo_list) 55 | await bot.send(ev, f'\nおかえりなさいませ、主さま{R.img("priconne/kokkoro_stamp.png").cqcode}\n{present}を獲得しました\n私からのプレゼントです\n主人今天要{todo}吗?', at_sender=True) 56 | -------------------------------------------------------------------------------- /hoshino/modules/translate/translate.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, CommandSession 2 | from nonebot import permission as perm 3 | 4 | from sogou_tr import sogou_tr 5 | from datetime import datetime, timedelta 6 | 7 | # sogou_tr使用帮助: 8 | # print(sogou_tr('hello world')) # -> '你好世界' 9 | # print(sogou_tr('hello world', to_lang='de')) # ->'Hallo Welt' 10 | # print(sogou_tr('hello world', to_lang='fr')) # ->'Salut tout le monde' 11 | # print(sogou_tr('hello world', to_lang='ja')) # ->'ハローワールド' 12 | 13 | 14 | @on_command('translate', aliases=('翻译', '翻譯', '翻訳'), permission=perm.GROUP_ADMIN, only_to_me=False) 15 | async def translate(session: CommandSession): 16 | text = session.get('text') 17 | if text: 18 | translation = await get_translation(text) 19 | await session.send(f'机翻译文:\n{translation}') 20 | else: 21 | await session.send('翻译姬待命中...') 22 | 23 | 24 | @translate.args_parser 25 | async def _(session: CommandSession): 26 | stripped_arg = session.current_arg_text.strip() # 删去首尾空白 27 | if stripped_arg: 28 | session.state['text'] = stripped_arg 29 | else: 30 | session.state['text'] = None 31 | return 32 | 33 | 34 | async def get_translation(text: str) -> str: 35 | if not hasattr(get_translation, 'cdtime'): 36 | get_translation.cdtime = datetime.now() - timedelta(seconds=3) 37 | now = datetime.now() 38 | if(now < get_translation.cdtime): 39 | return '翻译姬冷却中...' 40 | else: 41 | get_translation.cdtime = datetime.now() + timedelta(seconds=1) 42 | ret = sogou_tr(text) 43 | # print(sogou_tr.json) 44 | return ret if '0' != sogou_tr.json.get('errorCode') else '翻译姬出错了 ごめんなさい!' 45 | -------------------------------------------------------------------------------- /hoshino/modules/twitter/stream/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import importlib 3 | 4 | from hoshino import Service, priv, sucmd, get_bot 5 | from hoshino.config import twitter as cfg 6 | from hoshino.typing import MessageSegment as CommandSession 7 | 8 | sv = Service("twitter-poller", use_priv=priv.SU, manage_priv=priv.SU, visible=False) 9 | bot = get_bot() 10 | daemon1 = None 11 | daemon2 = None 12 | 13 | from .follow import follow_stream 14 | from .track import track_stream 15 | 16 | @bot.on_startup 17 | async def start_daemon(): 18 | global daemon1 19 | global daemon2 20 | 21 | loop = asyncio.get_event_loop() 22 | daemon1 = loop.create_task(stream_daemon(follow_stream)) 23 | daemon2 = loop.create_task(stream_daemon(track_stream)) 24 | 25 | 26 | async def stream_daemon(stream_func): 27 | while True: 28 | try: 29 | await stream_func() 30 | except (KeyboardInterrupt, asyncio.CancelledError): 31 | sv.logger.info("Twitter stream daemon exited.") 32 | raise 33 | except Exception as e: 34 | sv.logger.exception(e) 35 | sv.logger.error(f"Error {type(e)} Occurred in twitter stream. Restarting stream in 5s.") 36 | await asyncio.sleep(5) 37 | 38 | 39 | @sucmd("reload-twitter-stream-daemon", force_private=False, aliases=("重启转推", "重载转推")) 40 | async def reload_twitter_stream_daemon(session: CommandSession): 41 | try: 42 | daemon1.cancel() 43 | daemon2.cancel() 44 | importlib.reload(cfg) 45 | await start_daemon() 46 | await session.send("ok") 47 | except Exception as e: 48 | sv.logger.exception(e) 49 | await session.send(f"Error: {type(e)}") 50 | -------------------------------------------------------------------------------- /hoshino/modules/kancolle/query/fleet.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import random 4 | 5 | from hoshino import util, R 6 | 7 | from . import sv 8 | 9 | ship_folder = R.img('kancolle/ship/').path 10 | equip_folder = R.img('kancolle/equip/').path 11 | 12 | def _load_data(): 13 | config = util.load_config(__file__) 14 | db = config.get("data", {}) 15 | rex = re.compile(r"\[CQ:image,file=(.*)\]") 16 | for k, v in db.items(): 17 | m = rex.search(v) 18 | if m: 19 | img = str(R.img('kancolle/', m.group(1)).cqcode) 20 | db[k] = rex.sub(img, v) 21 | return db 22 | 23 | DB = _load_data() 24 | 25 | 26 | @sv.on_fullmatch('随机舰娘') 27 | async def random_ship(bot, ev): 28 | filelist = os.listdir(ship_folder) 29 | path = None 30 | while not path or not os.path.isfile(path): 31 | filename = random.choice(filelist) 32 | path = os.path.join(ship_folder, filename) 33 | pic = R.img('kancolle/ship/', filename).cqcode 34 | await bot.send(ev, pic, at_sender=True) 35 | 36 | 37 | @sv.on_fullmatch('随机装备') 38 | async def random_equip(bot, ev): 39 | filelist = os.listdir(equip_folder) 40 | path = None 41 | while not path or not os.path.isfile(path): 42 | filename = random.choice(filelist) 43 | path = os.path.join(equip_folder, filename) 44 | pic = R.img('kancolle/equip/', filename).cqcode 45 | await bot.send(ev, pic, at_sender=True) 46 | 47 | 48 | @sv.on_prefix('*') 49 | async def kc_query(bot, ev): 50 | key = ev.message.extract_plain_text() 51 | if key in DB: 52 | sv.logger.info(DB[key]) 53 | await bot.send(ev, DB[key], at_sender=True) 54 | else: 55 | sv.logger.info(f"{key} not found!") 56 | -------------------------------------------------------------------------------- /hoshino/modules/groupmaster/anti_kfc.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from datetime import timedelta 3 | from hoshino import Service, priv, util 4 | from hoshino.typing import CQEvent, CQHttpError, MessageSegment as ms 5 | 6 | sv = Service('anti-kfc', enable_on_default=False) 7 | 8 | CRAZY_THURSDAY_ALIAS = list(map(''.join, itertools.product(('疯狂', '狂乱'), ('星期四', '木曜日', '星期寺')))) 9 | THURSDAY_ALIAS = ['⭐期四', '⭐期4'] 10 | VME50_ALIAS = list(map(''.join, itertools.product( 11 | ('v', 'give', '给', '送', '微', 'send', 'transfer', 'vi'), 12 | ('', ' '), 13 | ('', '我', '卧', '窝', '窩', '沃', '在下', '朕', '孤', '私', '俺', '僕', '咱', 'me', 'i', 'w', 'wo', 'vo'), 14 | ('', ' '), 15 | ('五', '50', '5十', '5百', 'five', 'fifty', 'half 100', 'half100', 'half百', 'half1百', 'half 1百'), 16 | ))) 17 | 18 | 19 | @sv.on_keyword( 20 | 'kfc', '肯德基', '肯德鸡', '肯德🐓', '肯德🐔', 21 | *CRAZY_THURSDAY_ALIAS, 22 | *THURSDAY_ALIAS) 23 | async def anti_kfc_crazy_thursday(bot, ev: CQEvent): 24 | priv.set_block_user(ev.user_id, timedelta(seconds=240)) 25 | await util.silence(ev, 4 * 60, skip_su=False) 26 | await bot.send(ev, f'{ms.at(ev.user_id)} 本群正在对美实施经济制裁,本周不参加疯狂星期四!') 27 | try: 28 | await bot.delete_msg(self_id=ev.self_id, message_id=ev.message_id) 29 | except CQHttpError: 30 | pass 31 | 32 | 33 | @sv.on_keyword(*VME50_ALIAS) 34 | async def anti_vme50(bot, ev: CQEvent): 35 | priv.set_block_user(ev.user_id, timedelta(seconds=240)) 36 | await util.silence(ev, 4 * 60, skip_su=False) 37 | await bot.send(ev, f'{ms.at(ev.user_id)} 反诈中心星乃分部提醒您:以疯狂星期四等名义向您索要钱财的均为诈骗!') 38 | # try: 39 | # await bot.delete_msg(self_id=ev.self_id, message_id=ev.message_id) 40 | # except CQHttpError: 41 | # pass 42 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/pcr_data_updater.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | 4 | import hoshino 5 | from hoshino import Service, aiorequests, priv, sucmd 6 | from hoshino.config import SUPERUSERS 7 | from hoshino.typing import CommandSession 8 | 9 | from . import chara 10 | 11 | sv = Service("pcr-data-updater", use_priv=priv.SU, manage_priv=priv.SU, visible=False) 12 | 13 | 14 | async def report_to_su(sess, msg_with_sess, msg_wo_sess): 15 | if sess: 16 | await sess.send(msg_with_sess) 17 | else: 18 | bot = hoshino.get_bot() 19 | sid = bot.get_self_ids() 20 | if len(sid) > 0: 21 | sid = random.choice(sid) 22 | await bot.send_private_msg(self_id=sid, user_id=SUPERUSERS[0], message=msg_wo_sess) 23 | 24 | 25 | async def pull_chara(sess: CommandSession = None): 26 | try: 27 | rsp = await aiorequests.get('https://raw.githubusercontent.com/Ice9Coffee/LandosolRoster/master/_pcr_data.py', timeout=300) 28 | rsp.raise_for_status() 29 | rsp = await rsp.text 30 | 31 | filename = os.path.join(os.path.dirname(__file__), '_pcr_data.py') 32 | with open(filename, 'w', encoding='utf8') as f: 33 | f.write(rsp) 34 | result = chara.roster.update() 35 | 36 | except Exception as e: 37 | sv.logger.exception(e) 38 | await report_to_su(sess, f'Error: {e}', f'pcr_data定时更新时遇到错误:\n{e}') 39 | return 40 | 41 | result = f"角色别称导入成功 {result['success']},重名 {result['duplicate']}" 42 | await report_to_su(sess, result, f'pcr_data定时更新:\n{result}') 43 | 44 | 45 | sucmd('update-pcr-chara', force_private=False, aliases=('重载花名册', '更新花名册'))(pull_chara) 46 | sv.scheduled_job('cron', hour=5, jitter=300)(pull_chara) 47 | -------------------------------------------------------------------------------- /hoshino/modules/groupmaster/random_repeater.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import hoshino 4 | from hoshino import Service, util 5 | from hoshino.typing import CQEvent, CQHttpError, Message 6 | 7 | sv = Service('random-repeater', help_='随机复读机') 8 | 9 | PROB_A = 1.4 10 | group_stat = {} # group_id: (last_msg, is_repeated, p) 11 | 12 | ''' 13 | 不复读率 随 复读次数 指数级衰减 14 | 从第2条复读,即第3条重复消息开始有几率触发复读 15 | 16 | a 设为一个略大于1的小数,最好不要超过2,建议1.6 17 | 复读概率计算式:p_n = 1 - 1/a^n 18 | 递推式:p_n+1 = 1 - (1 - p_n) / a 19 | ''' 20 | @sv.on_message() 21 | async def random_repeater(bot, ev: CQEvent): 22 | group_id = ev.group_id 23 | msg = str(ev.message) 24 | 25 | if group_id not in group_stat: 26 | group_stat[group_id] = (msg, False, 0) 27 | return 28 | 29 | last_msg, is_repeated, p = group_stat[group_id] 30 | if last_msg == msg: # 群友正在复读 31 | if not is_repeated: # 机器人尚未复读过,开始测试复读 32 | if random.random() < p: # 概率测试通过,复读并设flag 33 | try: 34 | group_stat[group_id] = (msg, True, 0) 35 | await bot.send(ev, util.filt_message(ev.message)) 36 | except CQHttpError as e: 37 | hoshino.logger.error(f'复读失败: {type(e)}') 38 | hoshino.logger.exception(e) 39 | else: # 概率测试失败,蓄力 40 | p = 1 - (1 - p) / PROB_A 41 | group_stat[group_id] = (msg, False, p) 42 | else: # 不是复读,重置 43 | group_stat[group_id] = (msg, False, 0) 44 | 45 | 46 | def _test_a(a): 47 | ''' 48 | 该函数打印prob_n用于选取调节a 49 | 注意:由于依指数变化,a的轻微变化会对概率有很大影响 50 | ''' 51 | p0 = 0 52 | for _ in range(10): 53 | p0 = (p0 - 1) / a + 1 54 | print(p0) 55 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/news/__init__.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service, util 2 | from .spider import * 3 | 4 | svtw = Service('pcr-news-tw', bundle='pcr订阅', help_='台服官网新闻') 5 | svbl = Service('pcr-news-bili', bundle='pcr订阅', help_='B服官网新闻') 6 | svjp = Service('pcr-news-jp', bundle='pcr订阅', help_='日服官网新闻') 7 | 8 | async def news_poller(spider:BaseSpider, sv:Service, TAG): 9 | if not spider.item_cache: 10 | await spider.get_update() 11 | sv.logger.info(f'{TAG}新闻缓存为空,已加载至最新') 12 | return 13 | news = await spider.get_update() 14 | if not news: 15 | sv.logger.info(f'未检索到{TAG}新闻更新') 16 | return 17 | sv.logger.info(f'检索到{len(news)}条{TAG}新闻更新!') 18 | randomizer = util.randomizer(spider.src_name + '新闻') 19 | await sv.broadcast(spider.format_items(news), TAG, 0.5, randomizer) 20 | 21 | @svtw.scheduled_job('cron', minute='*/5', jitter=20) 22 | async def sonet_news_poller(): 23 | await news_poller(SonetSpider, svtw, '台服官网') 24 | 25 | @svbl.scheduled_job('cron', minute='*/5', jitter=20) 26 | async def bili_news_poller(): 27 | await news_poller(BiliSpider, svbl, 'B服官网') 28 | 29 | @svjp.scheduled_job('cron', minute='*/5', jitter=20) 30 | async def jp_news_poller(): 31 | await news_poller(JpSpider, svjp, '日服官网') 32 | 33 | async def send_news(bot, ev, spider:BaseSpider, max_num=5): 34 | if not spider.item_cache: 35 | await spider.get_update() 36 | news = spider.item_cache 37 | news = news[:min(max_num, len(news))] 38 | await bot.send(ev, spider.format_items(news), at_sender=True) 39 | 40 | @svtw.on_fullmatch('台服新闻', '台服日程') 41 | async def send_sonet_news(bot, ev): 42 | await send_news(bot, ev, SonetSpider) 43 | 44 | @svbl.on_fullmatch('B服新闻', 'b服新闻', 'B服日程', 'b服日程') 45 | async def send_bili_news(bot, ev): 46 | await send_news(bot, ev, BiliSpider) 47 | 48 | @svjp.on_fullmatch(('日服新闻', '日服日程')) 49 | async def send_jp_news(bot, ev): 50 | await send_news(bot, ev, JpSpider) 51 | -------------------------------------------------------------------------------- /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/R.py: -------------------------------------------------------------------------------- 1 | import os 2 | from urllib.parse import urljoin 3 | from urllib.request import pathname2url 4 | 5 | from nonebot import MessageSegment 6 | from PIL import Image 7 | 8 | import hoshino 9 | from hoshino import util 10 | 11 | class ResObj: 12 | def __init__(self, res_path): 13 | res_dir = os.path.expanduser(hoshino.config.RES_DIR) 14 | fullpath = os.path.abspath(os.path.join(res_dir, res_path)) 15 | if not fullpath.startswith(os.path.abspath(res_dir)): 16 | raise ValueError('Cannot access outside RESOUCE_DIR') 17 | self.__path = os.path.normpath(res_path) 18 | 19 | @property 20 | def url(self): 21 | """资源文件的url,供Onebot(或其他远程服务)使用""" 22 | return urljoin(hoshino.config.RES_URL, pathname2url(self.__path)) 23 | 24 | @property 25 | def path(self): 26 | """资源文件的路径,供Hoshino内部使用""" 27 | return os.path.join(hoshino.config.RES_DIR, self.__path) 28 | 29 | @property 30 | def exist(self): 31 | return os.path.exists(self.path) 32 | 33 | 34 | class ResImg(ResObj): 35 | @property 36 | def cqcode(self) -> MessageSegment: 37 | if hoshino.config.RES_PROTOCOL == 'http': 38 | return MessageSegment.image(self.url) 39 | elif hoshino.config.RES_PROTOCOL == 'file': 40 | return MessageSegment.image(f'file:///{os.path.abspath(self.path)}') 41 | else: 42 | try: 43 | return MessageSegment.image(util.pic2b64(self.open())) 44 | except Exception as e: 45 | hoshino.logger.exception(e) 46 | return MessageSegment.text('[图片出错]') 47 | 48 | def open(self) -> Image: 49 | try: 50 | return Image.open(self.path) 51 | except FileNotFoundError: 52 | hoshino.logger.error(f'缺少图片资源:{self.path}') 53 | raise 54 | 55 | 56 | def get(path, *paths): 57 | return ResObj(os.path.join(path, *paths)) 58 | 59 | def img(path, *paths): 60 | return ResImg(os.path.join('img', path, *paths)) 61 | -------------------------------------------------------------------------------- /hoshino/modules/dice/dice.py: -------------------------------------------------------------------------------- 1 | import re 2 | import random 3 | 4 | from hoshino import Service 5 | from hoshino.typing import CQEvent 6 | from hoshino.util import filt_message 7 | 8 | sv = Service('dice', help_=''' 9 | [.r] 掷骰子 10 | [.r 3d12] 掷3次12面骰子 11 | '''.strip()) 12 | 13 | async def do_dice(bot, ev, num, min_, max_, opr, offset, TIP="的掷骰结果是:"): 14 | if num == 0: 15 | await bot.send(ev, '咦?我骰子呢?') 16 | return 17 | min_, max_ = min(min_, max_), max(min_, max_) 18 | rolls = list(map(lambda _: random.randint(min_, max_), range(num))) 19 | sum_ = sum(rolls) 20 | rolls_str = '+'.join(map(lambda x: str(x), rolls)) 21 | if len(rolls_str) > 100: 22 | rolls_str = str(sum_) 23 | res = sum_ + opr * offset 24 | msg = [ 25 | f'{TIP}\n', str(num) if num > 1 else '', 'D', 26 | f'{min_}~' if min_ != 1 else '', str(max_), 27 | (' +-'[opr] + str(offset)) if offset else '', 28 | '=', rolls_str, (' +-'[opr] + str(offset)) if offset else '', 29 | f'={res}' if offset or num > 1 else '', 30 | ] 31 | msg = ''.join(msg) 32 | await bot.send(ev, msg, at_sender=True) 33 | 34 | 35 | @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)) 36 | async def dice(bot, ev): 37 | num, min_, max_, opr, offset = 1, 1, 100, 1, 0 38 | match = ev['match'] 39 | if s := match.group('num'): 40 | num = int(s) 41 | if s := match.group('min'): 42 | min_ = int(s) 43 | if s := match.group('max'): 44 | max_ = int(s) 45 | if s := match.group('opr'): 46 | opr = -1 if s == '-' else 1 47 | if s := match.group('offset'): 48 | offset = int(s) 49 | await do_dice(bot, ev, num, min_, max_, opr, offset) 50 | 51 | 52 | @sv.on_prefix('.qj') 53 | async def kc_marriage(bot, ev: CQEvent): 54 | wife = filt_message(ev.message.extract_plain_text().strip()) 55 | tip = f'与{wife}的ケッコンカッコカリ结果是:' if wife else '的ケッコンカッコカリ结果是:' 56 | await do_dice(bot, ev, 1, 3, 6, 1, 0, tip) 57 | -------------------------------------------------------------------------------- /hoshino/config_example/twitter.py: -------------------------------------------------------------------------------- 1 | consumer_key = "" 2 | consumer_secret = "" 3 | access_token_key = "" 4 | access_token_secret = "" 5 | proxy = None # 代理设置 当你的服务器需要使用代理访问Twitter时设置 6 | 7 | follows = { 8 | "twitter-stream-test": ["Ice9Coffee"], 9 | "kc-twitter": ["KanColle_STAFF", "C2_STAFF", "ywwuyi", "FlatIsNice"], 10 | "pcr-twitter": ["priconne_redive", "priconne_anime"], 11 | "uma-twitter": ["uma_musu", "uma_musu_anime"], 12 | "ba-twitter": ["Blue_ArchiveJP"], 13 | # "genshin-twitter": ["Genshin_7"], 14 | "sr-twitter": ["houkaistarrail"], 15 | "zzz-twitter": ["ZZZ_JP"], 16 | "pripri-twitter": ["pripri_anime"], 17 | "coffee-favorite-twitter": ["shiratamacaron", "k_yuizaki", "suzukitoto0323", "usagicandy_taku", "usagi_takumichi"], 18 | "depress-artist-twitter": ["tkmiz"], 19 | "moe-artist-twitter": [ 20 | "shiratamacaron", "k_yuizaki", "suzukitoto0323", "usagicandy_taku", "usagi_takumichi", 21 | "koma_momozu", "koma_momozu_", "santamatsuri", "panno_mimi", "suimya", "Anmi_", "mamgon", 22 | "kazukiadumi", "kazukiadumi_sub", "Setmen_uU", "bakuPA", "kantoku_5th", "done_kanda", "hoshi_u3", 23 | "siragagaga", "fuzichoco", "miyu_miyasaka", "naco_miyasaka", "tsukimi08", 24 | "tsubakinoniwa", "ominaeshin", "gomalio_y", "izumiyuhina", 25 | "1kurusk", "amsrntk3", "kani_biimu", "nahaki_401", "nahaki11", "sakura_oriko", 26 | "ukiukisoda", "yukkieeeeeen", "t_takahashi0830", "riko0202", "amedamacon", 27 | "Zoirun", "rulu_py", "zo3mie", "kurororo_rororo", "_namori_", "rasra25", 28 | "mignon", "yyish", "tsukiyopoke", "halu_1113", "HenreaderH_", "SiErACitrus", 29 | "heripiro", "tukiman02", "kakusatou_3333", "wasabilabel", "akakura1341", 30 | "warabimoti_yoz", "koudasuzu", "roro046", "nimono_", "ma_sakasama", 31 | ], 32 | } 33 | 34 | media_only_users = [ 35 | *follows["moe-artist-twitter"], 36 | *follows["depress-artist-twitter"], 37 | ] 38 | 39 | forward_retweet_users = [] 40 | 41 | uma_ura9_black_list = [ 42 | 'YaibA_No9', 'ReToken', 43 | ] 44 | -------------------------------------------------------------------------------- /hoshino/modules/kancolle/query/senka.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from io import BytesIO 4 | 5 | from PIL import Image 6 | 7 | from hoshino import R, aiorequests 8 | from hoshino.typing import CQEvent 9 | 10 | from . import sv 11 | 12 | senka_dir = R.img('kancolle/senka/').path 13 | os.makedirs(senka_dir, exist_ok=True) 14 | 15 | SERVERS = ''' 16 | [01] 横镇 [02] 吴镇 17 | [03] 佐镇 [04] 舞鹤 18 | [05] 大凑 [06] 特鲁克 19 | [07] 林加 [08] 拉包尔 20 | [09] 肖特兰 [10] 布因 21 | [11] 塔威塔威 [12] 帕劳 22 | [13] 文莱 [14] 单冠湾 23 | [15] 幌筵 [16] 宿毛湾 24 | [17] 鹿屋 [18] 岩川 25 | [19] 佐伯湾 [20] 柱岛 26 | '''.strip() 27 | 28 | def rank_filename(yy, mm, ss): 29 | return f"rank{yy:02d}{mm:02d}{ss:02d}.jpg" 30 | 31 | def rank_url(yy, mm, ss): 32 | return f"http://203.104.209.7/kcscontents/information/image/{rank_filename(yy, mm, ss)}" 33 | 34 | async def get_img(yy, mm, ss): 35 | img = R.img('kancolle/senka/', rank_filename(yy, mm, ss)) 36 | if not img.exist: 37 | link = rank_url(yy, mm, ss) 38 | resp = await aiorequests.get(link, stream=True) 39 | if 200 == resp.status_code: 40 | if re.search(r'image', resp.headers['content-type'], re.I): 41 | i = Image.open(BytesIO(await resp.content)) 42 | i.save(img.path) 43 | return img if img.exist else None 44 | 45 | syntax_rex = re.compile(r'^\d{6}$') 46 | 47 | @sv.on_prefix('人事表') 48 | async def rank_result(bot, ev: CQEvent): 49 | m = syntax_rex.match(ev.message.extract_plain_text().strip()) 50 | if not m: 51 | await bot.finish(ev, "用法:人事表yymmss\n例:查询21年06月吴镇\n> 人事表210602\n※服务器编号见https://senka.su/") 52 | rankid = m.group() 53 | yy, mm, ss = int(rankid[0:2]), int(rankid[2:4]), int(rankid[4:6]) 54 | if yy >= 13 and 1 <= mm <= 12 and 1 <= ss <= 20: 55 | img = await get_img(yy, mm, ss) 56 | if img: 57 | await bot.send(ev, img.cqcode) 58 | else: 59 | await bot.send(ev, f"人事表获取失败!\n这可能是由于20{yy:02d}年{mm}月战果尚未发放或网络异常") 60 | else: 61 | await bot.finish(ev, f"参数异常:欲查询20{yy:02d}年{mm}月服务器{ss}不合法\n服务器一览:\n{SERVERS}") 62 | -------------------------------------------------------------------------------- /hoshino/modules/pcrclanbattle/clanbattle/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "BOSS_HP": ["BOSS_HP_JP", "BOSS_HP_TW", "BOSS_HP_CN"], 3 | "SCORE_RATE": ["SCORE_RATE_JP", "SCORE_RATE_TW", "SCORE_RATE_CN"], 4 | 5 | "BOSS_HP_JP": [ 6 | [ 6000000, 8000000, 10000000, 12000000, 15000000], 7 | [ 6000000, 8000000, 10000000, 12000000, 15000000], 8 | [12000000, 14000000, 17000000, 19000000, 22000000], 9 | [19000000, 20000000, 23000000, 25000000, 27000000], 10 | [95000000, 100000000, 110000000, 120000000, 130000000] 11 | ], 12 | "SCORE_RATE_JP": [ 13 | [1.2, 1.2, 1.3, 1.4, 1.5], 14 | [1.6, 1.6, 1.8, 1.9, 2.0], 15 | [2.0, 2.0, 2.4, 2.4, 2.6], 16 | [3.5, 3.5, 3.7, 3.8, 4.0], 17 | [3.5, 3.5, 3.7, 3.8, 4.0] 18 | ], 19 | 20 | "_comment_TW": " ↓ Taiwan Province of China ↓ ", 21 | "BOSS_HP_TW": [ 22 | [ 6000000, 8000000, 10000000, 12000000, 15000000], 23 | [ 6000000, 8000000, 10000000, 12000000, 15000000], 24 | [12000000, 14000000, 17000000, 19000000, 22000000], 25 | [19000000, 20000000, 23000000, 25000000, 27000000], 26 | [85000000, 90000000, 95000000, 100000000, 110000000] 27 | ], 28 | "SCORE_RATE_TW": [ 29 | [1.2, 1.2, 1.3, 1.4, 1.5], 30 | [1.6, 1.6, 1.8, 1.9, 2.0], 31 | [2.0, 2.0, 2.4, 2.4, 2.6], 32 | [3.5, 3.5, 3.7, 3.8, 4.0], 33 | [3.5, 3.5, 3.7, 3.8, 4.0] 34 | ], 35 | 36 | "_comment_CN": " CN server of bilibili ", 37 | "BOSS_HP_CN": [ 38 | [ 6000000, 8000000, 10000000, 12000000, 15000000], 39 | [ 6000000, 8000000, 10000000, 12000000, 15000000], 40 | [ 7000000, 9000000, 12000000, 15000000, 20000000], 41 | [17000000, 18000000, 20000000, 21000000, 23000000], 42 | [85000000, 90000000, 95000000, 100000000, 110000000] 43 | ], 44 | "SCORE_RATE_CN": [ 45 | [1.2, 1.2, 1.3, 1.4, 1.5], 46 | [1.6, 1.6, 1.8, 1.9, 2.0], 47 | [2.0, 2.0, 2.4, 2.4, 2.6], 48 | [3.5, 3.5, 3.7, 3.8, 4.0], 49 | [3.5, 3.5, 3.7, 3.8, 4.0] 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Config Files 2 | config.py 3 | config.json 4 | hoshino/config/ 5 | local_coolq_config/ 6 | 7 | # Database 8 | db.json 9 | database.json 10 | *.db 11 | *.sqlite 12 | res/ 13 | img/ 14 | imgs/ 15 | image/ 16 | images/ 17 | 18 | # 非公开的组件 19 | hoshino/modules/pcrclanbattle/clanbattlev3 20 | hoshino/modules/pcrclanbattle/clanbattlev4 21 | hoshino/modules/umamusume 22 | hoshino/modules/pcrjjc3-tw 23 | _git/ 24 | 25 | # log 26 | log/ 27 | 28 | # 暂时删除的代码 29 | TRASH/ 30 | kokkoro-OLD/ 31 | 32 | # VSCode Folder 33 | .vscode/ 34 | 35 | # Byte-compiled / optimized / DLL files 36 | __pycache__/ 37 | *.py[cod] 38 | *$py.class 39 | 40 | # C extensions 41 | *.so 42 | 43 | # Distribution / packaging 44 | .Python 45 | build/ 46 | develop-eggs/ 47 | dist/ 48 | downloads/ 49 | eggs/ 50 | .eggs/ 51 | lib/ 52 | lib64/ 53 | parts/ 54 | sdist/ 55 | var/ 56 | wheels/ 57 | *.egg-info/ 58 | .installed.cfg 59 | *.egg 60 | MANIFEST 61 | 62 | # PyInstaller 63 | # Usually these files are written by a python script from a template 64 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 65 | *.manifest 66 | *.spec 67 | 68 | # Installer logs 69 | pip-log.txt 70 | pip-delete-this-directory.txt 71 | 72 | # Unit test / coverage reports 73 | htmlcov/ 74 | .tox/ 75 | .coverage 76 | .coverage.* 77 | .cache 78 | nosetests.xml 79 | coverage.xml 80 | *.cover 81 | .hypothesis/ 82 | .pytest_cache/ 83 | 84 | # Translations 85 | *.mo 86 | *.pot 87 | 88 | # Django stuff: 89 | *.log 90 | local_settings.py 91 | db.sqlite3 92 | 93 | # Flask stuff: 94 | instance/ 95 | .webassets-cache 96 | 97 | # Scrapy stuff: 98 | .scrapy 99 | 100 | # Sphinx documentation 101 | docs/_build/ 102 | 103 | # PyBuilder 104 | target/ 105 | 106 | # Jupyter Notebook 107 | .ipynb_checkpoints 108 | 109 | # pyenv 110 | .python-version 111 | 112 | # celery beat schedule file 113 | celerybeat-schedule 114 | 115 | # SageMath parsed files 116 | *.sage.py 117 | 118 | # Environments 119 | .env 120 | .venv 121 | env/ 122 | venv/ 123 | ENV/ 124 | env.bak/ 125 | venv.bak/ 126 | 127 | # Spyder project settings 128 | .spyderproject 129 | .spyproject 130 | 131 | # Rope project settings 132 | .ropeproject 133 | 134 | # mkdocs documentation 135 | /site 136 | 137 | # mypy 138 | .mypy_cache/ -------------------------------------------------------------------------------- /hoshino/modules/groupmaster/chat.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from nonebot import on_command 4 | 5 | from hoshino import R, Service, priv, util 6 | 7 | 8 | # basic function for debug, not included in Service('chat') 9 | @on_command('zai?', aliases=('在?', '在?', '在吗', '在么?', '在嘛', '在嘛?'), only_to_me=True) 10 | async def say_hello(session): 11 | await session.send('はい!私はいつも貴方の側にいますよ!') 12 | 13 | 14 | sv = Service('chat', visible=False) 15 | 16 | @sv.on_fullmatch('沙雕机器人') 17 | async def say_sorry(bot, ev): 18 | await bot.send(ev, 'ごめんなさい!嘤嘤嘤(〒︿〒)') 19 | 20 | 21 | @sv.on_fullmatch('老婆', 'waifu', 'laopo', only_to_me=True) 22 | async def chat_waifu(bot, ev): 23 | if not priv.check_priv(ev, priv.SUPERUSER): 24 | await bot.send(ev, R.img('laopo.jpg').cqcode) 25 | else: 26 | await bot.send(ev, 'mua~') 27 | 28 | 29 | @sv.on_fullmatch('老公', only_to_me=True) 30 | async def chat_laogong(bot, ev): 31 | await bot.send(ev, '你给我滚!', at_sender=True) 32 | 33 | 34 | @sv.on_fullmatch('mua', only_to_me=True) 35 | async def chat_mua(bot, ev): 36 | await bot.send(ev, '笨蛋~', at_sender=True) 37 | 38 | 39 | @sv.on_fullmatch('来点星奏') 40 | async def seina(bot, ev): 41 | await bot.send(ev, R.img('星奏.png').cqcode) 42 | 43 | 44 | @sv.on_fullmatch('我有个朋友说他好了', '我朋友说他好了') 45 | async def ddhaole(bot, ev): 46 | await bot.send(ev, '那个朋友是不是你弟弟?') 47 | await util.silence(ev, 30) 48 | 49 | 50 | @sv.on_fullmatch('我好了') 51 | async def nihaole(bot, ev): 52 | await bot.send(ev, '不许好,憋回去!') 53 | await util.silence(ev, 30) 54 | 55 | 56 | # ============================================ # 57 | 58 | 59 | @sv.on_keyword('确实', '有一说一', 'u1s1', 'yysy') 60 | async def chat_queshi(bot, ctx): 61 | if random.random() < 0.05: 62 | await bot.send(ctx, R.img('确实.jpg').cqcode) 63 | 64 | 65 | @sv.on_keyword('会战') 66 | async def chat_clanba(bot, ctx): 67 | if random.random() < 0.02: 68 | await bot.send(ctx, R.img('我的天啊你看看都几度了.jpg').cqcode) 69 | 70 | 71 | @sv.on_keyword('内鬼') 72 | async def chat_neigui(bot, ctx): 73 | if random.random() < 0.10: 74 | await bot.send(ctx, R.img('内鬼.png').cqcode) 75 | 76 | nyb_player = f'''{R.img('newyearburst.gif').cqcode} 77 | 正在播放:New Year Burst 78 | ──●━━━━ 1:05/1:30 79 | ⇆ ㅤ◁ ㅤㅤ❚❚ ㅤㅤ▷ ㅤ↻ 80 | '''.strip() 81 | 82 | @sv.on_keyword('春黑', '新黑') 83 | async def new_year_burst(bot, ev): 84 | if random.random() < 0.02: 85 | await bot.send(ev, nyb_player) 86 | -------------------------------------------------------------------------------- /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 | 14 | from hoshino import Service, util 15 | from hoshino.typing import CQEvent 16 | 17 | sv = Service('pcr-cherugo', bundle='pcr娱乐', help_=''' 18 | [切噜一下] 转换为切噜语 19 | [切噜~♪切啰巴切拉切蹦切蹦] 切噜语翻译 20 | '''.strip()) 21 | 22 | CHERU_SET = '切卟叮咧哔唎啪啰啵嘭噜噼巴拉蹦铃' 23 | CHERU_DIC = {c: i for i, c in enumerate(CHERU_SET)} 24 | ENCODING = 'gb18030' 25 | rex_split = re.compile(r'\b', re.U) 26 | rex_word = re.compile(r'^\w+$', re.U) 27 | rex_cheru_word: re.Pattern = re.compile(rf'切[{CHERU_SET}]+', re.U) 28 | 29 | 30 | def grouper(iterable, n, fillvalue=None): 31 | args = [iter(iterable)] * n 32 | return zip_longest(*args, fillvalue=fillvalue) 33 | 34 | 35 | def word2cheru(w: str) -> str: 36 | c = ['切'] 37 | for b in w.encode(ENCODING): 38 | c.append(CHERU_SET[b & 0xf]) 39 | c.append(CHERU_SET[(b >> 4) & 0xf]) 40 | return ''.join(c) 41 | 42 | 43 | def cheru2word(c: str) -> str: 44 | if not c[0] == '切' or len(c) < 2: 45 | return c 46 | b = [] 47 | for b1, b2 in grouper(c[1:], 2, '切'): 48 | x = CHERU_DIC.get(b2, 0) 49 | x = x << 4 | CHERU_DIC.get(b1, 0) 50 | b.append(x) 51 | return bytes(b).decode(ENCODING, 'replace') 52 | 53 | 54 | def str2cheru(s: str) -> str: 55 | c = [] 56 | for w in rex_split.split(s): 57 | if rex_word.search(w): 58 | w = word2cheru(w) 59 | c.append(w) 60 | return ''.join(c) 61 | 62 | 63 | def cheru2str(c: str) -> str: 64 | return rex_cheru_word.sub(lambda w: cheru2word(w.group()), c) 65 | 66 | 67 | @sv.on_prefix('切噜一下') 68 | async def cherulize(bot, ev: CQEvent): 69 | s = ev.message.extract_plain_text() 70 | if len(s) > 500: 71 | await bot.send(ev, '切、切噜太长切不动勒切噜噜...', at_sender=True) 72 | return 73 | await bot.send(ev, '切噜~♪' + str2cheru(s)) 74 | 75 | 76 | @sv.on_prefix('切噜~♪') 77 | async def decherulize(bot, ev: CQEvent): 78 | s = ev.message.extract_plain_text() 79 | if len(s) > 1501: 80 | await bot.send(ev, '切、切噜太长切不动勒切噜噜...', at_sender=True) 81 | return 82 | msg = '的切噜噜是:\n' + util.filt_message(cheru2str(s)) 83 | await bot.send(ev, msg, at_sender=True) 84 | -------------------------------------------------------------------------------- /hoshino/modules/botmanage/help.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service, priv 2 | from hoshino.typing import CQEvent 3 | 4 | sv = Service('_help_', manage_priv=priv.SUPERUSER, visible=False) 5 | 6 | TOP_MANUAL = ''' 7 | ==================== 8 | = HoshinoBot使用说明 = 9 | ==================== 10 | 发送[]内的关键词触发 11 | 12 | ==== 查看详细说明 ==== 13 | [帮助pcr会战][帮助pcr查询] 14 | [帮助pcr娱乐][帮助pcr订阅] 15 | [帮助artist][帮助kancolle] 16 | [帮助umamusume] 17 | [帮助通用] 18 | 19 | ====== 常用指令 ====== 20 | [启用会战] 选择会战版本 21 | [怎么拆日和] 竞技场查询 22 | [pcr速查] 常用网址 23 | [星乃来发十连] 转蛋模拟 24 | [官漫286] 官方四格(日文) 25 | [猜头像][猜角色] 小游戏 26 | [.rd] roll点 27 | 28 | [切噜一下+待加密文本] 29 | ▲切噜语转换 30 | [来杯咖啡+反馈内容] 31 | ▲联系维护组 32 | 33 | ====== 被动技能 ====== 34 | pcr推特转发(日) 35 | pcr台B服官网公告推送 36 | pcr四格漫画(日)更新推送 37 | pcr竞技场背刺时间提醒* 38 | 赛马娘推特转发* 39 | URA9因子嗅探者* 40 | 萌系画师推特转发* 41 | 撤回终结者* 42 | 入群欢迎* & 退群通知 43 | 番剧字幕组更新推送*° 44 | *: 默认关闭 45 | °: 不支持自定义 46 | 47 | ====== 模块开关 ====== 48 | ※限群管理/群主控制※ 49 | [lssv] 查看各模块开关状态 50 | [启用+空格+service] 51 | [禁用+空格+service] 52 | 53 | ===================== 54 | ※Hoshino开源Project: 55 | github.com/Ice9Coffee/HoshinoBot 56 | 您对项目作者的支持与Star是本bot更新维护的动力 57 | 💰+⭐=❤ 58 | '''.strip() 59 | # 魔改请保留 github.com/Ice9Coffee/HoshinoBot 项目地址 60 | 61 | 62 | def gen_service_manual(service: Service, gid: int): 63 | spit_line = '=' * max(0, 18 - len(service.name)) 64 | manual = [f"|{'○' if service.check_enabled(gid) else '×'}| {service.name} {spit_line}"] 65 | if service.help: 66 | manual.append(service.help) 67 | return '\n'.join(manual) 68 | 69 | 70 | def gen_bundle_manual(bundle_name, service_list, gid): 71 | manual = [bundle_name] 72 | service_list = sorted(service_list, key=lambda s: s.name) 73 | for s in service_list: 74 | if s.visible: 75 | manual.append(gen_service_manual(s, gid)) 76 | return '\n'.join(manual) 77 | 78 | 79 | @sv.on_prefix('help', '帮助') 80 | @sv.on_suffix('help', '帮助') 81 | async def send_help(bot, ev: CQEvent): 82 | gid = ev.group_id 83 | arg = ev.message.extract_plain_text().strip() 84 | bundles = Service.get_bundles() 85 | services = Service.get_loaded_services() 86 | if not arg: 87 | await bot.send(ev, TOP_MANUAL) 88 | elif arg in bundles: 89 | msg = gen_bundle_manual(arg, bundles[arg], gid) 90 | await bot.send(ev, msg) 91 | elif arg in services: 92 | s = services[arg] 93 | msg = gen_service_manual(s, gid) 94 | await bot.send(ev, msg) 95 | # else: ignore 96 | -------------------------------------------------------------------------------- /hoshino/modules/picfinder/README.md: -------------------------------------------------------------------------------- 1 | # picfinder_take 2 | Hoshino开源社区插件的官方整合
3 | 将与[原仓库](https://github.com/pcrbot/picfinder_take)同步更新
4 | 以下为原README 5 | 6 | -------- 7 | 8 | 9 | 个人~~缝合~~修改的Hoshino bot搜图插件。 10 | 11 | 这个插件的主要思路其实是在hoshino上还原隔壁 [@Tsuk1ko](https://github.com/Tsuk1ko) 大佬家的[竹竹](https://github.com/Tsuk1ko/cq-picsearcher-bot)的搜图交互体验(所以插件名是たけ) 12 | 13 | 代码主体部分参考了 [@Watanabe-Asa](https://github.com/Watanabe-Asa)大佬的 [搜图](https://github.com/pcrbot/Salmon-plugin-transplant#%E6%90%9C%E5%9B%BE)与 [@Cappuccilo](https://github.com/Cappuccilo)大佬的 [以图搜图](https://github.com/pcrbot/cappuccilo_plugins#%E4%BB%A5%E5%9B%BE%E6%90%9C%E5%9B%BE),感谢各位大佬的代码) 14 | 15 | --- 16 | 17 | - 6.1更新:增加私聊搜图、截屏识别和代理功能 18 | 19 | - 6.24更新:增加回复搜图功能,搜图请求更换为异步(感谢 [@蓝红心](https://github.com/LHXnois)大佬) 20 | 21 | - 7.9更新:增加自定义HOST功能,在qiang内又不想用全局代理的用户(?)可以单独为SauceNao和ascii2d配置反代或ip直连)(继续感谢 [@蓝红心](https://github.com/LHXnois)大佬) 22 | 23 | - 7.26更新:回复搜图增加at支持 24 | 25 | - 10.20更新:增加批量搜图计数与吞图提示 26 | 27 | - 12.12更新:增加频道搜图支持,适配go-cqhttp-1.0.0beat8-fix2(还是感谢 [@蓝红心](https://github.com/LHXnois)大佬) 28 | 29 | - 22.01.10更新:适配新版图片CQcode,增加IGNORE_STAMP配置项,可在批量搜索中自动忽略subType不为0的图片(比如表情包);用cloudscraper绕过ascii2d近期新增的cf认证 30 | 31 | ## 特点 32 | 33 | - 搜索SauceNao,在相似率过低时自动补充搜索ascii2d,相似率阈值可在config中调整。搜索结果显示数量可在config中调整。 34 | 35 | - 解析SauceNao和ascii2d搜索结果的作品详细信息。SauceNao全部42个index格式解析都已完成;ascii2d常见格式应该也能解析,一些奇怪格式的外部登录不敢保证) 36 | 37 | - 获取SauceNao和ascii2d的结果缩略图,缩略图可在config中关闭。 38 | 39 | - 搜图结果可由普通回复切换为合并转发回复,减少刷屏情况。发送模式可在config中调整。 40 | 41 | - 增加批量搜图模式,解决移动端文字命令+图片发送麻烦的问题。 42 | 43 | - 增加搜图每人每日限额,可在config中调整限额。要懂得节制噢.jpg 44 | 45 | - 增加简单的手机截屏识别功能,判断为整屏手机截屏时会拒绝搜索 ~~(你会截你马个图.jpg)~~ 46 | 47 | - 增加私聊搜图功能,有效缓解腾讯吞图(但反之临时会话下搜索结果含图片与链接时概率被吞无法发送,若要稳定使用需加bot好友) 48 | 49 | - 增加代理设置,方便qiang内使用 50 | 51 | - 增加回复搜图功能,可直接对群友发送的图片进行回复搜索,省去转发过程) 52 | 53 | - New!增加IGNORE_STAMP配置项,可在批量搜索中自动忽略沙雕群友发的表情包,减少资源浪费与刷屏) 54 | 55 | 56 | ## 用法 57 | 58 | - 申请并在config中配置SauceNao的API key 59 | 60 | - 发送 ``@bot+图片`` 或 ``[bot昵称]搜图+图片`` 进行单张搜索。 61 | 62 | - 发送 ``[bot昵称]搜图`` 进入连续搜图模式,连续搜图模式下同一用户所发送所有图片都将直接搜索; 63 | 64 | 发送 ``谢谢[bot昵称]`` 退出连续搜图模式,或停止发图等待超时后自动退出连续搜图模式。 65 | 66 | - 对他人发送的图片回复 ``@bot 搜图`` 或``[bot昵称]搜图`` 进行回复搜索。 67 | 68 | - 私聊下直接发送图片即可进行搜索。 69 | 70 | 71 | ## 频道使用 72 | 73 | 对特定子频道中收到的所有图片进行搜索(本质应该算是个常驻的连续搜图?) 74 | 在qq群这样可能影响讨论,但是在频道可以专门开一个搜图子频道只用来搜图! 75 | 76 | 食用方法: 77 | 78 | - 创建一个专门的子频道 79 | - 搞到频道id和子频道id(看log) 80 | - 按{频道id: [子频道id]}格式填到config.py里的enableguild里 81 | - 发送图片开始搜图 82 | 83 | 开启慢速模式然后给bot管理体验更佳 84 | -------------------------------------------------------------------------------- /hoshino/modules/botmanage/billing.py: -------------------------------------------------------------------------------- 1 | import re 2 | import hoshino 3 | from hoshino import sucmd, HoshinoBot 4 | from hoshino.typing import CommandSession, CQHttpError, MessageSegment as ms 5 | 6 | @sucmd('billing') 7 | async def billing(session: CommandSession): 8 | bot = session.bot 9 | args = session.current_arg_text.split() 10 | try: 11 | for i in range(0, len(args), 2): 12 | args[i] = int(args[i]) 13 | assert re.fullmatch(r'\d{4}-\d{2}-\d{2}', args[i + 1]), f"{args[i + 1]}不是合法日期" 14 | except (ValueError, AssertionError) as e: 15 | await session.finish(str(e)) 16 | 17 | try: 18 | sid_group = {} 19 | for sid in hoshino.get_self_ids(): 20 | gs = await bot.get_group_list(self_id=sid) 21 | sid_group[sid] = [g['group_id'] for g in gs] 22 | except CQHttpError as e: 23 | await session.finish(str(e)) 24 | 25 | failed = [] 26 | not_found = [] 27 | for i in range(0, len(args), 2): 28 | gid = args[i] 29 | date = args[i + 1] 30 | bill_sent_flag = False 31 | for sid, groups in sid_group.items(): 32 | if gid in groups: 33 | msg = f"本群bot将于/已于{date}到期,请及时联系{hoshino.config.SUPERUSERS[0]}续费,以免影响使用!" 34 | oid = await get_group_owner_id(bot, sid, gid) 35 | if oid: 36 | msg = str(ms.at(oid)) + msg 37 | try: 38 | await bot.send_group_msg(self_id=sid, group_id=gid, message=msg) 39 | bill_sent_flag = True 40 | except CQHttpError: 41 | failed.append(gid) 42 | try: 43 | await session.send(f"bot{sid} 向 群{gid} 发送billing失败!") 44 | except CQHttpError: 45 | hoshino.logger.critical((f"bot{sid} 向 群{gid} 发送billing失败!且回报SUPERUSER失败!")) 46 | if not bill_sent_flag and gid not in failed: 47 | not_found.append(gid) 48 | 49 | msg = f"发送bill完毕!\n失败{len(failed)}:{failed}\n未找到{len(not_found)}:{not_found}" 50 | await session.send(msg) 51 | 52 | 53 | async def get_group_owner_id(bot: HoshinoBot, self_id, group_id) -> int: 54 | try: 55 | members = await bot.get_group_member_list(self_id=self_id, group_id=group_id) 56 | except CQHttpError: 57 | return 0 58 | for m in members: 59 | if m.get('role') == 'owner': 60 | return m.get('user_id', 0) 61 | return 0 62 | -------------------------------------------------------------------------------- /hoshino/modules/twitter/stream/track.py: -------------------------------------------------------------------------------- 1 | # 订阅关键词 2 | 3 | import re 4 | 5 | import peony 6 | from peony import PeonyClient 7 | 8 | from hoshino import Service 9 | from hoshino.config import twitter as cfg 10 | 11 | from .util import format_tweet 12 | 13 | sv = Service('uma-ura9-sniffer', enable_on_default=False, help_='嗅探新鲜出炉的9URA种马', bundle='umamusume') 14 | 15 | async def track_stream(): 16 | client = PeonyClient( 17 | consumer_key=cfg.consumer_key, 18 | consumer_secret=cfg.consumer_secret, 19 | access_token=cfg.access_token_key, 20 | access_token_secret=cfg.access_token_secret, 21 | proxy=cfg.proxy, 22 | ) 23 | async with client: 24 | stream = client.stream.statuses.filter.post(track=['URA9']) 25 | 26 | async for tweet in stream: 27 | sv.logger.info("Got twitter event.") 28 | if peony.events.tweet(tweet): 29 | screen_name = tweet.user.screen_name 30 | if screen_name in cfg.uma_ura9_black_list: 31 | continue # black list 32 | if peony.events.retweet(tweet): 33 | continue # 忽略纯转推 34 | if tweet.get("quoted_status"): 35 | continue # 忽略引用转推 36 | if tweet.get("in_reply_to_screen_name"): 37 | continue # 忽略评论 38 | 39 | if not re.search(r'\d{9}', tweet.text): 40 | continue # 忽略无id的推特 41 | if re.search(r'ura(シナリオ)?([::])?\s*[0-5012345]', tweet.text, re.I): 42 | continue # 忽略低ura因子 43 | if re.search(r'目指|狙|チャレンジ|微妙', tweet.text, re.I): 44 | continue # 忽略未达成 45 | if re.search(r'青(因子)?\s*[01245780124578]', tweet.text, re.I): 46 | continue # 忽略低星蓝 47 | if re.search(r'(スピ(ード)?|スタ(ミナ)?|パワー?|根性?|賢さ?)\s*[01245780124578]', tweet.text, re.I): 48 | continue # 忽略低星蓝 49 | if re.search(r'ura(シナリオ)?([::])?9(では|じゃ|周回)', tweet.text, re.I): 50 | continue # 忽略否定型 51 | if re.search(r'青(因子)?9(では|じゃ|周回)', tweet.text, re.I): 52 | continue # 忽略否定型 53 | 54 | msg = format_tweet(tweet) 55 | 56 | sv.logger.info(f"推送推文:\n{msg}") 57 | await sv.broadcast(msg, f" @{screen_name} 推文", 0.2) 58 | 59 | else: 60 | sv.logger.debug("Ignore non-tweet event.") 61 | -------------------------------------------------------------------------------- /hoshino/modules/botmanage/ls.py: -------------------------------------------------------------------------------- 1 | from nonebot.argparse import ArgumentParser 2 | from hoshino import Service, sucmd 3 | from hoshino.typing import CommandSession 4 | 5 | 6 | async def ls_group(session: CommandSession): 7 | bot = session.bot 8 | self_ids = bot.get_self_ids() 9 | for sid in self_ids: 10 | gl = await bot.get_group_list(self_id=sid) 11 | msg = [ "{group_id} {group_name}".format_map(g) for g in gl ] 12 | msg = "\n".join(msg) 13 | msg = f"bot:{sid}\n| 群号 | 群名 | 共{len(gl)}个群\n" + msg 14 | await bot.send_private_msg(self_id=sid, user_id=bot.config.SUPERUSERS[0], message=msg) 15 | 16 | 17 | async def ls_friend(session: CommandSession): 18 | gl = await session.bot.get_friend_list(self_id=session.event.self_id) 19 | msg = [ "{user_id} {nickname}".format_map(g) for g in gl ] 20 | msg = "\n".join(msg) 21 | msg = f"| QQ号 | 昵称 | 共{len(gl)}个好友\n" + msg 22 | await session.send(msg) 23 | 24 | 25 | async def ls_service(session: CommandSession, service_name: str): 26 | all_services = Service.get_loaded_services() 27 | if service_name in all_services: 28 | sv = all_services[service_name] 29 | on_g = '\n'.join(map(str, sv.enable_group)) 30 | off_g = '\n'.join(map(str, sv.disable_group)) 31 | default_ = 'enabled' if sv.enable_on_default else 'disabled' 32 | 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}" 33 | session.finish(msg) 34 | else: 35 | session.finish(f'未找到服务{service_name}') 36 | 37 | 38 | async def ls_bot(session: CommandSession): 39 | self_ids = session.bot.get_self_ids() 40 | await session.send(f"共{len(self_ids)}个bot\n{self_ids}") 41 | 42 | 43 | @sucmd('ls', shell_like=True) 44 | async def ls(session: CommandSession): 45 | parser = ArgumentParser(session=session) 46 | switch = parser.add_mutually_exclusive_group() 47 | switch.add_argument('-g', '--group', action='store_true') 48 | switch.add_argument('-f', '--friend', action='store_true') 49 | switch.add_argument('-b', '--bot', action='store_true') 50 | switch.add_argument('-s', '--service') 51 | args = parser.parse_args(session.argv) 52 | 53 | if args.group: 54 | await ls_group(session) 55 | elif args.friend: 56 | await ls_friend(session) 57 | elif args.bot: 58 | await ls_bot(session) 59 | elif args.service: 60 | await ls_service(session, args.service) 61 | -------------------------------------------------------------------------------- /hoshino/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | from typing import Iterable 4 | 5 | import nonebot 6 | from aiocqhttp import Event as CQEvent 7 | from nonebot import message_preprocessor 8 | from nonebot.message import CanceledException 9 | 10 | 11 | class HoshinoBot(nonebot.NoneBot): 12 | def __init__(self, config_object=None): 13 | ''' 14 | You should NOT instantiate a HoshinoBot! 15 | This is for intellisense code completion. 16 | Use `hoshino.init()` instead. 17 | ''' 18 | super().__init__(config_object) 19 | raise Exception("You should NOT instantiate a HoshinoBot! Use `hoshino.init()` instead.") 20 | 21 | @staticmethod 22 | def get_self_ids() -> Iterable[str]: 23 | return get_self_ids() 24 | 25 | @staticmethod 26 | def finish(event, message, **kwargs): 27 | if message: 28 | bot = get_bot() 29 | asyncio.run_coroutine_threadsafe(bot.send(event, message, **kwargs), bot.loop) 30 | raise CanceledException('ServiceFunc of HoshinoBot finished.') 31 | 32 | @staticmethod 33 | async def silence(ev: CQEvent, ban_time, skip_su=True): 34 | return await util.silence(ev, ban_time, skip_su) 35 | 36 | 37 | from .service import Service, sucmd 38 | from . import config, log, util 39 | 40 | __all__ = [ 41 | 'HoshinoBot', 42 | 'Service', 43 | 'sucmd', 44 | 'get_bot', 45 | 'message_preprocessor', 46 | ] 47 | 48 | _bot = None 49 | logger = log.new_logger('hoshino', config.DEBUG) 50 | 51 | 52 | def init() -> HoshinoBot: 53 | global _bot 54 | nonebot.init(config) 55 | _bot = nonebot.get_bot() 56 | _bot.get_self_ids = HoshinoBot.get_self_ids 57 | _bot.finish = HoshinoBot.finish 58 | _bot.silence = HoshinoBot.silence 59 | 60 | nonebot.logger.addHandler(log.error_handler) 61 | nonebot.logger.addHandler(log.critical_handler) 62 | 63 | # Hoshino User Data 64 | os.makedirs(os.path.expanduser('~/.hoshino'), exist_ok=True) 65 | 66 | for module_name in config.MODULES_ON: 67 | nonebot.load_plugins( 68 | os.path.join(os.path.dirname(__file__), 'modules', module_name), 69 | f'hoshino.modules.{module_name}') 70 | 71 | from . import msghandler 72 | 73 | return _bot 74 | 75 | 76 | def get_bot() -> HoshinoBot: 77 | if _bot is None: 78 | raise ValueError('HoshinoBot has not been initialized') 79 | return _bot 80 | 81 | 82 | def get_self_ids() -> Iterable[str]: 83 | return list(get_bot()._wsr_api_clients.keys()) 84 | -------------------------------------------------------------------------------- /hoshino/priv.py: -------------------------------------------------------------------------------- 1 | """The privilege of user discribed in an `int` number. 2 | 3 | `0` is for Default or NotSet. The other numbers may change in future versions. 4 | """ 5 | 6 | from datetime import datetime 7 | 8 | import hoshino 9 | from hoshino import config 10 | from hoshino.typing import CQEvent 11 | 12 | BLACK = -999 13 | DEFAULT = 0 14 | NORMAL = 1 15 | PRIVATE = 10 16 | ADMIN = 21 17 | OWNER = 22 18 | WHITE = 51 19 | SUPERUSER = 999 20 | SU = SUPERUSER 21 | 22 | #===================== block list =======================# 23 | _black_group = {} # Dict[group_id, expr_time] 24 | _black_user = {} # Dict[user_id, expr_time] 25 | 26 | 27 | def set_block_group(group_id, time): 28 | _black_group[group_id] = datetime.now() + time 29 | 30 | 31 | def set_block_user(user_id, time): 32 | if user_id not in hoshino.config.SUPERUSERS: 33 | _black_user[user_id] = datetime.now() + time 34 | 35 | 36 | def check_block_group(group_id): 37 | if group_id in _black_group and datetime.now() > _black_group[group_id]: 38 | del _black_group[group_id] # 拉黑时间过期 39 | return False 40 | return bool(group_id in _black_group) 41 | 42 | 43 | def check_block_user(user_id): 44 | if user_id in config.BLACK_LIST: 45 | return True 46 | if user_id in _black_user and datetime.now() > _black_user[user_id]: 47 | del _black_user[user_id] # 拉黑时间过期 48 | return False 49 | return bool(user_id in _black_user) 50 | 51 | 52 | #========================================================# 53 | 54 | 55 | def get_user_priv(ev: CQEvent): 56 | uid = ev.user_id 57 | if uid in hoshino.config.SUPERUSERS: 58 | return SUPERUSER 59 | if check_block_user(uid): 60 | return BLACK 61 | if uid in config.WHITE_LIST: 62 | return WHITE 63 | if ev['message_type'] == 'group': 64 | if not ev.anonymous: 65 | role = ev.sender.get('role') 66 | if role == 'member': 67 | return NORMAL 68 | elif role == 'admin': 69 | return ADMIN 70 | elif role == 'administrator': 71 | return ADMIN # for cqhttpmirai 72 | elif role == 'owner': 73 | return OWNER 74 | return NORMAL 75 | if ev['message_type'] == 'private': 76 | return PRIVATE 77 | return NORMAL 78 | 79 | 80 | def check_priv(ev: CQEvent, require: int) -> bool: 81 | if ev['message_type'] == 'group': 82 | return bool(get_user_priv(ev) >= require) 83 | else: 84 | return False # 不允许私聊 85 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/games/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sqlite3 3 | 4 | 5 | class Dao: 6 | def __init__(self, db_path): 7 | self.db_path = db_path 8 | os.makedirs(os.path.dirname(db_path), exist_ok=True) 9 | self._create_table() 10 | 11 | def connect(self): 12 | return sqlite3.connect(self.db_path) 13 | 14 | def _create_table(self): 15 | with self.connect() as conn: 16 | conn.execute( 17 | "CREATE TABLE IF NOT EXISTS win_record " 18 | "(gid INT NOT NULL, uid INT NOT NULL, count INT NOT NULL, PRIMARY KEY(gid, uid))" 19 | ) 20 | 21 | def get_win_count(self, gid, uid): 22 | with self.connect() as conn: 23 | r = conn.execute( 24 | "SELECT count FROM win_record WHERE gid=? AND uid=?", (gid, uid) 25 | ).fetchone() 26 | return r[0] if r else 0 27 | 28 | def record_winning(self, gid, uid): 29 | n = self.get_win_count(gid, uid) 30 | n += 1 31 | with self.connect() as conn: 32 | conn.execute( 33 | "INSERT OR REPLACE INTO win_record (gid, uid, count) VALUES (?, ?, ?)", 34 | (gid, uid, n), 35 | ) 36 | return n 37 | 38 | def get_ranking(self, gid): 39 | with self.connect() as conn: 40 | r = conn.execute( 41 | "SELECT uid, count FROM win_record WHERE gid=? ORDER BY count DESC LIMIT 10", 42 | (gid,), 43 | ).fetchall() 44 | return r 45 | 46 | 47 | class GameMaster: 48 | def __init__(self, db_path): 49 | self.db_path = db_path 50 | self.playing = {} 51 | 52 | def is_playing(self, gid): 53 | return gid in self.playing 54 | 55 | def start_game(self, gid): 56 | return Game(gid, self) 57 | 58 | def get_game(self, gid): 59 | return self.playing[gid] if gid in self.playing else None 60 | 61 | @property 62 | def db(self): 63 | return Dao(self.db_path) 64 | 65 | 66 | class Game: 67 | def __init__(self, gid, game_master): 68 | self.gid = gid 69 | self.gm = game_master 70 | self.answer = 0 71 | self.winner = 0 72 | 73 | def __enter__(self): 74 | self.gm.playing[self.gid] = self 75 | return self 76 | 77 | def __exit__(self, type_, value, trace): 78 | del self.gm.playing[self.gid] 79 | 80 | def record(self): 81 | return self.gm.db.record_winning(self.gid, self.winner) 82 | 83 | 84 | from . import desc_guess 85 | from . import avatar_guess 86 | -------------------------------------------------------------------------------- /hoshino/modules/pcrclanbattle/clanbattle/argparse/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict 2 | from nonebot import Message 3 | from hoshino.util import filt_message 4 | 5 | from ..exception import * 6 | 7 | 8 | class ArgHolder: 9 | __slots__ = ('type', 'default', 'tip') 10 | def __init__(self, type=str, default=None, tip=None): 11 | self.type = type 12 | self.default = default 13 | self.tip = tip 14 | 15 | 16 | class ParseResult(dict): 17 | def __getattr__(self, key): 18 | return self[key] 19 | def __setattr__(self, key, value): 20 | self[key] = value 21 | 22 | 23 | class ArgParser: 24 | def __init__(self, usage, arg_dict=None): 25 | self.usage = f"【用法/用例】\n{usage}\n\n※无需输入尖括号,圆括号内为可选参数,用空格隔开命令与参数" 26 | self.arg_dict: Dict[str, ArgHolder] = arg_dict or {} 27 | 28 | 29 | def add_arg(self, name, *, type=str, default=None, tip=None): 30 | self.arg_dict[name] = ArgHolder(type, default, tip) 31 | 32 | 33 | def parse(self, args: List[str], message: Message) -> ParseResult: 34 | result = ParseResult() 35 | 36 | # 解析参数,以一个字符开头,或无前缀 37 | for arg in args: 38 | name, x = arg[0].upper(), arg[1:] 39 | if name in self.arg_dict: 40 | holder = self.arg_dict[name] 41 | elif '' in self.arg_dict: 42 | holder = self.arg_dict[''] 43 | name, x = '', arg 44 | else: 45 | raise ParseError('命令含有未知参数', self.usage) 46 | 47 | try: 48 | if holder.type == str: 49 | result.setdefault(name, filt_message(holder.type(x))) 50 | else: 51 | result.setdefault(name, holder.type(x)) # 多个参数只取第1个 52 | except ParseError as e: 53 | e.append(self.usage) 54 | raise e 55 | except Exception: 56 | msg = f"请给出正确的{holder.tip or '参数'}" 57 | if name: 58 | msg += f"以{name}开头" 59 | raise ParseError(msg, self.usage) 60 | 61 | # 检查所有参数是否以赋值 62 | for name, holder in self.arg_dict.items(): 63 | if name not in result: 64 | if holder.default is None: # 缺失必要参数 抛异常 65 | msg = f"请给出{holder.tip or '缺少的参数'}" 66 | if name: 67 | msg += f"以{name}开头" 68 | raise ParseError(msg, self.usage) 69 | else: 70 | result[name] = holder.default 71 | 72 | # 解析Message内的at 73 | result['at'] = 0 74 | for seg in message: 75 | if seg.type == 'at': 76 | result['at'] = int(seg.data['qq']) 77 | 78 | return result 79 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/games/desc_guess.py: -------------------------------------------------------------------------------- 1 | # ref: https://github.com/GWYOG/GWYOG-Hoshino-plugins/blob/master/pcrdescguess 2 | # Originally written by @GWYOG 3 | # Reflacted by @Ice9Coffee 4 | # GPL-3.0 Licensed 5 | # Thanks to @GWYOG for his great contribution! 6 | 7 | import asyncio 8 | import os 9 | import random 10 | 11 | from hoshino import Service, util 12 | from hoshino.modules.priconne import chara 13 | from hoshino.typing import CQEvent, MessageSegment as Seg 14 | 15 | from .. import _pcr_data 16 | from . import GameMaster 17 | 18 | 19 | PREPARE_TIME = 5 20 | ONE_TURN_TIME = 12 21 | TURN_NUMBER = 5 22 | DB_PATH = os.path.expanduser("~/.hoshino/pcr_desc_guess.db") 23 | 24 | gm = GameMaster(DB_PATH) 25 | sv = Service("pcr-desc-guess", bundle="pcr娱乐", help_=""" 26 | [猜角色] 猜猜bot在描述哪位角色 27 | [猜角色排行] 显示小游戏的群排行榜(只显示前十) 28 | """.strip() 29 | ) 30 | 31 | 32 | @sv.on_fullmatch("猜角色排行", "猜角色排名", "猜角色排行榜", "猜角色群排行") 33 | async def description_guess_group_ranking(bot, ev: CQEvent): 34 | ranking = gm.db.get_ranking(ev.group_id) 35 | msg = ["【猜角色小游戏排行榜】"] 36 | for i, item in enumerate(ranking): 37 | uid, count = item 38 | m = await bot.get_group_member_info(self_id=ev.self_id, group_id=ev.group_id, user_id=uid) 39 | name = util.filt_message(m["card"]) or util.filt_message(m["nickname"]) or str(uid) 40 | msg.append(f"第{i + 1}名:{name} 猜对{count}次") 41 | await bot.send(ev, "\n".join(msg)) 42 | 43 | 44 | @sv.on_fullmatch("猜角色", "猜人物") 45 | async def description_guess(bot, ev: CQEvent): 46 | if gm.is_playing(ev.group_id): 47 | await bot.finish(ev, "游戏仍在进行中…") 48 | with gm.start_game(ev.group_id) as game: 49 | game.answer = random.choice(list(_pcr_data.CHARA_PROFILE.keys())) 50 | profile = _pcr_data.CHARA_PROFILE[game.answer] 51 | kws = list(profile.keys()) 52 | kws.remove('名字') 53 | random.shuffle(kws) 54 | kws = kws[:TURN_NUMBER] 55 | await bot.send(ev, f"{PREPARE_TIME}秒后每隔{ONE_TURN_TIME}秒我会给出某位角色的一个描述,根据这些描述猜猜她是谁~") 56 | await asyncio.sleep(PREPARE_TIME) 57 | for i, k in enumerate(kws): 58 | await bot.send(ev, f"提示{i + 1}/{len(kws)}:\n她的{k}是 {profile[k]}") 59 | await asyncio.sleep(ONE_TURN_TIME) 60 | if game.winner: 61 | return 62 | c = chara.fromid(game.answer) 63 | await bot.send(ev, f"正确答案是:{c.name} {await c.get_icon_cqcode()}\n很遗憾,没有人答对~") 64 | 65 | 66 | @sv.on_message() 67 | async def on_input_chara_name(bot, ev: CQEvent): 68 | game = gm.get_game(ev.group_id) 69 | if not game or game.winner: 70 | return 71 | c = chara.fromname(ev.message.extract_plain_text()) 72 | if c.id != chara.UNKNOWN and c.id == game.answer: 73 | game.winner = ev.user_id 74 | n = game.record() 75 | msg = f"正确答案是:{c.name}{await c.get_icon_cqcode()}\n{Seg.at(ev.user_id)}猜对了,真厉害!TA已经猜对{n}次了~\n(此轮游戏将在几秒后自动结束,请耐心等待)" 76 | await bot.send(ev, msg) 77 | -------------------------------------------------------------------------------- /hoshino/modules/mikan/mikan.py: -------------------------------------------------------------------------------- 1 | import random 2 | from datetime import datetime 3 | 4 | from lxml import etree 5 | 6 | import hoshino 7 | from hoshino import Service, aiorequests, util 8 | 9 | sv = Service('bangumi', enable_on_default=False, help_='蜜柑番剧更新推送') 10 | 11 | class Mikan: 12 | link_cache = set() 13 | rss_cache = [] 14 | 15 | @staticmethod 16 | def get_token(): 17 | return hoshino.config.mikan.MIKAN_TOKEN 18 | 19 | 20 | @staticmethod 21 | def get_proxies(): 22 | return hoshino.config.mikan.PROXIES 23 | 24 | 25 | @staticmethod 26 | async def get_rss(): 27 | res = [] 28 | try: 29 | resp = await aiorequests.get('https://mikanani.me/RSS/MyBangumi', params={'token': Mikan.get_token()}, proxies=Mikan.get_proxies(), timeout=10) 30 | rss = etree.XML(await resp.content) 31 | except Exception as e: 32 | sv.logger.error(f'[get_rss] Error: {e}') 33 | return [] 34 | 35 | for i in rss.xpath('/rss/channel/item'): 36 | link: str = i.find('./link').text 37 | link = link.replace("https://mikanani.me/Home/Episode/", "magnet:?xt=urn:btih:") 38 | description = i.find('./description').text 39 | pubDate = i.find('.//xmlns:pubDate', namespaces={'xmlns': 'https://mikanani.me/0.1/'}).text 40 | pubDate = pubDate[:19] 41 | pubDate = datetime.strptime(pubDate, r'%Y-%m-%dT%H:%M:%S') 42 | res.append( (link, description, pubDate) ) 43 | return res 44 | 45 | 46 | @staticmethod 47 | async def update_cache(): 48 | rss = await Mikan.get_rss() 49 | new_bangumi = [] 50 | flag = False 51 | for item in rss: 52 | if item[0] not in Mikan.link_cache: 53 | flag = True 54 | new_bangumi.append(item) 55 | if flag: 56 | Mikan.link_cache = { item[0] for item in rss } 57 | Mikan.rss_cache = rss 58 | return new_bangumi 59 | 60 | 61 | @sv.scheduled_job('cron', minute='*/3', second='15') 62 | async def mikan_poller(): 63 | if not Mikan.rss_cache: 64 | await Mikan.update_cache() 65 | sv.logger.info(f'订阅缓存为空,已加载至最新') 66 | return 67 | new_bangumi = await Mikan.update_cache() 68 | if not new_bangumi: 69 | sv.logger.info(f'未检索到番剧更新!') 70 | else: 71 | sv.logger.info(f'检索到{len(new_bangumi)}条番剧更新!') 72 | msg = [ f'{i[1]} 【{i[2].strftime(r"%Y-%m-%d %H:%M")}】\n▲下载 {i[0]}' for i in new_bangumi ] 73 | await sv.broadcast(msg, '蜜柑番剧', 0.5, util.randomizer('番剧更新')) 74 | 75 | 76 | DISABLE_NOTICE = '本群蜜柑番剧功能已禁用\n使用【启用 bangumi】以启用(需群管理)\n开启本功能后将自动推送字幕组更新' 77 | 78 | @sv.on_fullmatch('来点新番') 79 | async def send_bangumi(bot, ev): 80 | if not Mikan.rss_cache: 81 | await Mikan.update_cache() 82 | 83 | 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))] ] 84 | msg = '\n'.join(msg) 85 | await bot.send(ev, f'最近更新的番剧:\n{msg}') 86 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/games/avatar_guess.py: -------------------------------------------------------------------------------- 1 | # ref: https://github.com/GWYOG/GWYOG-Hoshino-plugins/blob/master/pcravatarguess 2 | # Originally written by @GWYOG 3 | # Reflacted by @Ice9Coffee 4 | # GPL-3.0 Licensed 5 | # Thanks to @GWYOG for his great contribution! 6 | 7 | import asyncio 8 | import os 9 | import random 10 | 11 | from hoshino import Service, util 12 | from hoshino.modules.priconne import _pcr_data, chara 13 | from hoshino.typing import CQEvent 14 | from hoshino.typing import MessageSegment as Seg 15 | 16 | from . import GameMaster 17 | 18 | PATCH_SIZE = 32 19 | ONE_TURN_TIME = 20 20 | DB_PATH = os.path.expanduser("~/.hoshino/pcr_avatar_guess.db") 21 | BLACKLIST_ID = [1072, 1908, 4031, 9000] 22 | 23 | gm = GameMaster(DB_PATH) 24 | sv = Service( 25 | "pcr-avatar-guess", 26 | bundle="pcr娱乐", 27 | help_=""" 28 | [猜头像] 猜猜bot随机发送的头像的一小部分来自哪位角色 29 | [猜头像排行] 显示小游戏的群排行榜(只显示前十) 30 | """.strip(), 31 | ) 32 | 33 | 34 | @sv.on_fullmatch("猜头像排行", "猜头像排名", "猜头像排行榜", "猜头像群排行") 35 | async def description_guess_group_ranking(bot, ev: CQEvent): 36 | ranking = gm.db.get_ranking(ev.group_id) 37 | msg = ["【猜头像小游戏排行榜】"] 38 | for i, item in enumerate(ranking): 39 | uid, count = item 40 | m = await bot.get_group_member_info( 41 | self_id=ev.self_id, group_id=ev.group_id, user_id=uid 42 | ) 43 | name = util.filt_message(m["card"]) or util.filt_message(m["nickname"]) or str(uid) 44 | msg.append(f"第{i + 1}名:{name} 猜对{count}次") 45 | await bot.send(ev, "\n".join(msg)) 46 | 47 | 48 | @sv.on_fullmatch("猜头像") 49 | async def avatar_guess(bot, ev: CQEvent): 50 | if gm.is_playing(ev.group_id): 51 | await bot.finish(ev, "游戏仍在进行中…") 52 | with gm.start_game(ev.group_id) as game: 53 | ids = list(_pcr_data.CHARA_NAME.keys()) 54 | game.answer = random.choice(ids), random.choice((3, 6)) 55 | while chara.is_npc(game.answer[0]): 56 | game.answer = random.choice(ids), random.choice((3, 6)) 57 | c = chara.fromid(game.answer[0], game.answer[1]) 58 | img = (await c.get_icon()).open() 59 | w, h = img.size 60 | l = random.randint(0, w - PATCH_SIZE) 61 | u = random.randint(0, h - PATCH_SIZE) 62 | cropped = img.crop((l, u, l + PATCH_SIZE, u + PATCH_SIZE)) 63 | cropped = Seg.image(util.pic2b64(cropped)) 64 | await bot.send(ev, f"猜猜这个图片是哪位角色头像的一部分?({ONE_TURN_TIME}s后公布答案) {cropped}") 65 | await asyncio.sleep(ONE_TURN_TIME) 66 | if game.winner: 67 | return 68 | await bot.send(ev, f"正确答案是:{c.name} {await c.get_icon_cqcode()}\n很遗憾,没有人答对~") 69 | 70 | 71 | @sv.on_message() 72 | async def on_input_chara_name(bot, ev: CQEvent): 73 | game = gm.get_game(ev.group_id) 74 | if not game or game.winner: 75 | return 76 | c = chara.fromname(ev.message.extract_plain_text(), game.answer[1]) 77 | if c.id != chara.UNKNOWN and c.id == game.answer[0]: 78 | game.winner = ev.user_id 79 | n = game.record() 80 | msg = f"正确答案是:{c.name}{await c.get_icon_cqcode()}\n{Seg.at(ev.user_id)}猜对了,真厉害!TA已经猜对{n}次了~\n(此轮游戏将在几秒后自动结束,请耐心等待)" 81 | await bot.send(ev, msg) 82 | -------------------------------------------------------------------------------- /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/modules/groupmaster/anti_abuse.py: -------------------------------------------------------------------------------- 1 | # TODO: rewrite this 2 | 3 | 4 | import random 5 | from datetime import timedelta 6 | 7 | import nonebot 8 | from nonebot import Message, MessageSegment, message_preprocessor, on_command 9 | from nonebot.message import _check_calling_me_nickname 10 | 11 | import hoshino 12 | from hoshino import R, Service, util 13 | 14 | ''' 15 | from nonebot.command import CommandManager 16 | def parse_command(bot, cmd_str): 17 | parse_command = CommandManager().parse_command(bot, cmd_str) 18 | 19 | 20 | bot = nonebot.get_bot() 21 | BLANK_MESSAGE = Message(MessageSegment.text('')) 22 | 23 | @message_preprocessor 24 | async def black_filter(bot, ctx, plugin_manager=None): # plugin_manager is new feature of nonebot v1.6 25 | first_msg_seg = ctx['message'][0] 26 | if first_msg_seg.type == 'hb': 27 | return # pass normal Luck Money Pack to avoid abuse 28 | if ctx['message_type'] == 'group' and hoshino.priv.check_block_group(ctx['group_id']) \ 29 | or hoshino.priv.check_block_user(ctx['user_id']): 30 | ctx['message'] = BLANK_MESSAGE 31 | 32 | 33 | def _check_hbtitle_is_cmd(ctx, title): 34 | ctx = ctx.copy() # 复制一份,避免影响原有的ctx 35 | ctx['message'] = Message(title) 36 | _check_calling_me_nickname(bot, ctx) 37 | cmd, _ = parse_command(bot, str(ctx['message']).lstrip()) 38 | return bool(cmd) 39 | 40 | 41 | @bot.on_message('group') 42 | async def hb_handler(ctx): 43 | self_id = ctx['self_id'] 44 | user_id = ctx['user_id'] 45 | group_id = ctx['group_id'] 46 | first_msg_seg = ctx['message'][0] 47 | if first_msg_seg.type == 'redbag': 48 | title = first_msg_seg['data']['title'] 49 | if _check_hbtitle_is_cmd(ctx, title): 50 | hoshino.priv.set_block_group(group_id, timedelta(hours=1)) 51 | hoshino.priv.set_block_user(user_id, timedelta(days=30)) 52 | await util.silence(ctx, 7 * 24 * 60 * 60) 53 | msg_from = f"{ctx['user_id']}@[群:{ctx['group_id']}]" 54 | hoshino.logger.critical(f'Self: {ctx["self_id"]}, Message {ctx["message_id"]} from {msg_from} detected as abuse: {ctx["message"]}') 55 | await bot.send(ctx, "检测到滥用行为,您的操作已被记录并加入黑名单。\nbot拒绝响应本群消息1小时", at_sender=True) 56 | try: 57 | await bot.set_group_kick(self_id=self_id, group_id=group_id, user_id=user_id, reject_add_request=True) 58 | hoshino.logger.critical(f"已将{user_id}移出群{group_id}") 59 | except: 60 | pass 61 | 62 | ''' 63 | # ============================================ # 64 | 65 | BANNED_WORD = ( 66 | 'rbq', 'RBQ', '憨批', '废物', '死妈', '崽种', '傻逼', '傻逼玩意', 67 | '没用东西', '傻B', '傻b', 'SB', 'sb', '煞笔', 'cnm', '爬', 'kkp', 68 | 'nmsl', 'D区', '口区', '我是你爹', 'nmbiss', '弱智', '给爷爬', '杂种爬','爪巴' 69 | ) 70 | @on_command('ban_word', aliases=BANNED_WORD, only_to_me=True) 71 | async def ban_word(session): 72 | ctx = session.ctx 73 | user_id = ctx['user_id'] 74 | if hoshino.priv.check_block_user(user_id): 75 | return 76 | msg_from = str(user_id) 77 | if ctx['message_type'] == 'group': 78 | msg_from += f'@[群:{ctx["group_id"]}]' 79 | elif ctx['message_type'] == 'discuss': 80 | msg_from += f'@[讨论组:{ctx["discuss_id"]}]' 81 | hoshino.logger.critical(f'Self: {ctx["self_id"]}, Message {ctx["message_id"]} from {msg_from}: {ctx["message"]}') 82 | hoshino.priv.set_block_user(user_id, timedelta(hours=8)) 83 | pic = R.img(f"chieri{random.randint(1, 4)}.jpg").cqcode 84 | await session.send(f"不理你啦!バーカー\n{pic}", at_sender=True) 85 | await util.silence(session.ctx, 8*60*60) 86 | -------------------------------------------------------------------------------- /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 | 5 | import abc 6 | import re 7 | from dataclasses import dataclass 8 | from typing import List, Union 9 | 10 | from bs4 import BeautifulSoup 11 | from hoshino import aiorequests 12 | 13 | 14 | @dataclass 15 | class Item: 16 | idx: Union[str, int] 17 | content: str = "" 18 | 19 | def __eq__(self, other): 20 | return self.idx == other.idx 21 | 22 | 23 | class BaseSpider(abc.ABC): 24 | url = None 25 | src_name = None 26 | header = {} 27 | idx_cache = set() 28 | item_cache = [] 29 | 30 | @classmethod 31 | async def get_response(cls) -> aiorequests.AsyncResponse: 32 | resp = await aiorequests.get(cls.url, headers=cls.header) 33 | resp.raise_for_status() 34 | return resp 35 | 36 | @staticmethod 37 | @abc.abstractmethod 38 | async def get_items(resp: aiorequests.AsyncResponse) -> List[Item]: 39 | raise NotImplementedError 40 | 41 | @classmethod 42 | async def get_update(cls) -> List[Item]: 43 | resp = await cls.get_response() 44 | items = await cls.get_items(resp) 45 | updates = [i for i in items if i.idx not in cls.idx_cache] 46 | if updates: 47 | cls.idx_cache.update(i.idx for i in items) 48 | cls.item_cache = items 49 | return updates 50 | 51 | @classmethod 52 | def format_items(cls, items) -> str: 53 | return '\n'.join(map(lambda i: i.content, items)) 54 | 55 | 56 | 57 | class SonetSpider(BaseSpider): 58 | url = "https://www.princessconnect.so-net.tw/news/" 59 | src_name = "台服官网" 60 | 61 | @staticmethod 62 | async def get_items(resp:aiorequests.AsyncResponse): 63 | soup = BeautifulSoup(await resp.text, 'lxml') 64 | return [ 65 | Item(idx=li.a["href"], 66 | content=f"{li.a.text}\n▲www.princessconnect.so-net.tw{li.a['href']}" 67 | ) for li in soup.select("article.news_con ul>li") 68 | ] 69 | 70 | 71 | 72 | class BiliSpider(BaseSpider): 73 | url = "http://api.biligame.com/news/list?gameExtensionId=267&positionId=2&pageNum=1&pageSize=7&typeId=" 74 | src_name = "B服官网" 75 | 76 | @staticmethod 77 | async def get_items(resp:aiorequests.AsyncResponse): 78 | content = await resp.json() 79 | items = [ 80 | Item(idx=n["id"], 81 | content="{title}\n▲game.bilibili.com/pcr/news.html#detail={id}".format_map(n) 82 | ) for n in content["data"] 83 | ] 84 | return items 85 | 86 | 87 | class JpSpider(BaseSpider): 88 | url = "https://priconne-redive.jp/news/" 89 | src_name = "日服官网" 90 | header = { 91 | 'user-agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36' 92 | } 93 | 94 | @staticmethod 95 | async def get_items(resp: aiorequests.AsyncResponse): 96 | data = await resp.content 97 | data = data.decode() 98 | title = re.findall('

(.*?)

', data) # 全部标题 99 | 100 | data_post_ids = re.findall('data-post-id="(.*[0-9])"', data) # 全部ID 101 | items = [] 102 | for i in range(len(title)): 103 | t = title[i] 104 | news_id = data_post_ids[i] 105 | items.append(Item( 106 | idx=news_id, 107 | content=f"{t}\nhttps://priconne-redive.jp/news/event/{news_id}/" 108 | )) 109 | return items 110 | -------------------------------------------------------------------------------- /hoshino/modules/pcrclanbattle/clanbattle/__init__.py: -------------------------------------------------------------------------------- 1 | # 公主连接Re:Dive会战管理插件 2 | # clan == クラン == 戰隊(直译为氏族)(CLANNAD的CLAN(笑)) 3 | 4 | from nonebot import on_command 5 | 6 | from hoshino import R, Service, util 7 | from hoshino.typing import * 8 | 9 | from .argparse import ArgParser 10 | from .exception import * 11 | 12 | sv = Service('clanbattle', help_='Hoshino开源版 命令以感叹号开头 发送【!帮助】查看说明', bundle='pcr会战') 13 | SORRY = 'ごめんなさい!嘤嘤嘤(〒︿〒)' 14 | 15 | _registry:Dict[str, Tuple[Callable, ArgParser]] = {} 16 | 17 | @sv.on_message('group') 18 | async def _clanbattle_bus(bot, ctx): 19 | # check prefix 20 | start = '' 21 | for m in ctx['message']: 22 | if m.type == 'text': 23 | start = m.data.get('text', '').lstrip() 24 | break 25 | if not start or start[0] not in '!!': 26 | return 27 | 28 | # find cmd 29 | plain_text = ctx['message'].extract_plain_text() 30 | if len(plain_text) <= 1: 31 | return 32 | cmd, *args = plain_text[1:].split() 33 | cmd = util.normalize_str(cmd) 34 | if cmd in _registry: 35 | func, parser = _registry[cmd] 36 | try: 37 | sv.logger.info(f'Message {ctx["message_id"]} is a clanbattle command, start to process by {func.__name__}.') 38 | args = parser.parse(args, ctx['message']) 39 | await func(bot, ctx, args) 40 | sv.logger.info(f'Message {ctx["message_id"]} is a clanbattle command, handled by {func.__name__}.') 41 | except DatabaseError as e: 42 | await bot.send(ctx, f'DatabaseError: {e.message}\n{SORRY}\n※请及时联系维护组', at_sender=True) 43 | except ClanBattleError as e: 44 | await bot.send(ctx, e.message, at_sender=True) 45 | except Exception as e: 46 | sv.logger.exception(e) 47 | sv.logger.error(f'{type(e)} occured when {func.__name__} handling message {ctx["message_id"]}.') 48 | await bot.send(ctx, f'Error: 机器人出现未预料的错误\n{SORRY}\n※请及时联系维护组', at_sender=True) 49 | 50 | 51 | def cb_cmd(name, parser:ArgParser) -> Callable: 52 | if isinstance(name, str): 53 | name = (name, ) 54 | if not isinstance(name, Iterable): 55 | raise ValueError('`name` of cb_cmd must be `str` or `Iterable[str]`') 56 | names = map(lambda x: util.normalize_str(x), name) 57 | def deco(func) -> Callable: 58 | for n in names: 59 | if n in _registry: 60 | sv.logger.warning(f'出现重名命令:{func.__name__} 与 {_registry[n].__name__}命令名冲突') 61 | else: 62 | _registry[n] = (func, parser) 63 | return func 64 | return deco 65 | 66 | 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 | ※点击链接分享查看完整命令表 101 | ※或前往 v2.hoshinobot.cc 102 | ※如有问题请先阅读一览表底部的FAQ 103 | ※使用前请务必【逐字】阅读开头的必读事项 104 | '''.rstrip() 105 | 106 | @on_command('!帮助', aliases=('!帮助', '!幫助', '!幫助', '!help', '!help'), only_to_me=False) 107 | async def cb_help(session:CommandSession): 108 | await session.send(QUICK_START, at_sender=True) 109 | msg = MessageSegment.share(url='https://github.com/Ice9Coffee/HoshinoBot/wiki/%E4%BC%9A%E6%88%98%E7%AE%A1%E7%90%86v2', 110 | title='Hoshino会战管理v2', 111 | content='完整命令一览表') 112 | await session.send(msg) 113 | -------------------------------------------------------------------------------- /hoshino/modules/twitter-v2/stream/util.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import pytz 4 | 5 | from hoshino import util 6 | from hoshino.typing import MessageSegment as ms 7 | 8 | from . import sv 9 | 10 | 11 | def format_time(time_str): 12 | dt = datetime.strptime(time_str, r"%Y-%m-%dT%H:%M:%S.%f%z") 13 | dt = dt.astimezone(pytz.timezone("Asia/Shanghai")) 14 | return f"{util.month_name(dt.month)}{util.date_name(dt.day)}・{util.time_name(dt.hour, dt.minute)}" 15 | 16 | 17 | def is_retweet(tweet): 18 | data = tweet.get("data") 19 | if isinstance(data, list): 20 | data = data[0] 21 | if "referenced_tweets" not in data: 22 | return False 23 | return data["referenced_tweets"][0]["type"] == "retweeted" 24 | 25 | 26 | def is_quote(tweet): 27 | data = tweet.get("data") 28 | if isinstance(data, list): 29 | data = data[0] 30 | if "referenced_tweets" not in data: 31 | return False 32 | return data["referenced_tweets"][0]["type"] == "quoted" 33 | 34 | 35 | def is_reply(tweet): 36 | data = tweet.get("data") 37 | if isinstance(data, list): 38 | data = data[0] 39 | if "referenced_tweets" not in data: 40 | return False 41 | return data["referenced_tweets"][0]["type"] == "replied_to" 42 | 43 | 44 | async def get_tweet(tweet_id, client): 45 | params = { 46 | "ids": tweet_id, 47 | "tweet.fields": [ 48 | "created_at", 49 | "entities", 50 | "referenced_tweets", 51 | "text", 52 | "author_id", 53 | "in_reply_to_user_id", 54 | "attachments", 55 | ], 56 | "expansions": ["author_id", "attachments.media_keys"], # 不要删media_keys,否则下行不生效 57 | "media.fields": ["url", "preview_image_url"], 58 | } 59 | resp = await client.api.tweets.get(**params) 60 | try: 61 | resp.data = resp.data[0] 62 | except KeyError: 63 | pass 64 | sv.logger.debug(type(resp)) 65 | sv.logger.debug(type(resp.data)) 66 | sv.logger.debug(resp) 67 | return resp 68 | 69 | MAX_DEPTH = 1 70 | async def format_tweet(tweet, client, quote_depth=0): 71 | name = tweet.get("includes")["users"][0]["name"] 72 | try: 73 | data = tweet.get("data")[0] 74 | except KeyError: 75 | data = tweet.get("data") 76 | 77 | if quote_depth < MAX_DEPTH and is_retweet(tweet): 78 | retweeted_tweet = await get_tweet(data["referenced_tweets"][0]["id"], client) 79 | retweeted_msg = await format_tweet(retweeted_tweet, client, quote_depth + 1) 80 | return f"@{name} 转推了\n>>>>>\n{retweeted_msg}" 81 | 82 | time = format_time(data["created_at"]) 83 | text = data["text"] 84 | msg = f"@{name}\n{time}\n\n{text}" 85 | 86 | if "media" in tweet.get("includes"): 87 | media = tweet.get("includes")["media"] 88 | imgs = "".join([str(ms.image(m.url)) for m in media]) 89 | msg = f"{msg}\n{imgs}" 90 | 91 | if quote_depth < MAX_DEPTH and (is_quote(tweet) or is_reply(tweet)): 92 | quoted_tweet = await get_tweet(data["referenced_tweets"][0]["id"], client) 93 | quoted_msg = await format_tweet(quoted_tweet, client, quote_depth + 1) 94 | msg = f"{quoted_msg}\n\n<<<<<\n{msg}" 95 | 96 | return msg 97 | 98 | 99 | def cut_list(lists): 100 | """ 101 | 将关注列表拆分为不超过512字节的多个列表 102 | :param lists: 初始列表 103 | :return: 一个二维数组 [[x,x],[x,x]] 104 | """ 105 | res_data = [[]] 106 | length = -4 # 5 - 9 = -4 107 | 108 | for x in lists: 109 | length += len(x) + 9 # len(' OR from:') = 9 110 | if length > 512: 111 | length = len(x) + 5 # len('from:') = 5 112 | res_data.append([]) 113 | res_data[-1].append(x) 114 | 115 | if len(res_data) > 5: 116 | sv.logger.warning("关键词列表过长!Twitter API限制只能添加5个rule") 117 | return res_data 118 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/gacha/gacha.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from hoshino import util 4 | from .. import chara 5 | 6 | 7 | class Gacha(object): 8 | 9 | def __init__(self, pool_name: str = "MIX"): 10 | super().__init__() 11 | self.pool_name = pool_name 12 | if pool_name == 'BL': 13 | self.tenjou_line = 300 14 | self.tenjou_rate = '12.16%' 15 | else: 16 | self.tenjou_line = 200 17 | self.tenjou_rate = '24.54%' 18 | self.memo_pieces = 200 if (pool_name == 'JP' or pool_name == 'MIX') else 100 19 | self.load_pool(pool_name) 20 | 21 | 22 | def load_pool(self, pool_name: str): 23 | config = util.load_config(__file__) 24 | pool = config[pool_name] 25 | self.up_prob = pool["up_prob"] 26 | self.s3_prob = pool["s3_prob"] 27 | self.s2_prob = pool["s2_prob"] 28 | self.s1_prob = 1000 - self.s2_prob - self.s3_prob 29 | self.up = pool["up"] 30 | self.star3 = pool["star3"] 31 | self.star2 = pool["star2"] 32 | self.star1 = pool["star1"] 33 | 34 | 35 | def gacha_one(self, up_prob: int, s3_prob: int, s2_prob: int, s1_prob: int = None): 36 | ''' 37 | sx_prob: x星概率,要求和为1000 38 | up_prob: UP角色概率(从3星划出) 39 | up_chara: UP角色名列表 40 | 41 | return: (单抽结果:Chara, 秘石数:int) 42 | --------------------------- 43 | |up| | 20 | 78 | 44 | | *** | ** | * | 45 | --------------------------- 46 | ''' 47 | if s1_prob is None: 48 | s1_prob = 1000 - s3_prob - s2_prob 49 | total_ = s3_prob + s2_prob + s1_prob 50 | pick = random.randint(1, total_) 51 | if pick <= up_prob: 52 | return chara.fromname(random.choice(self.up), 3), 100 53 | elif pick <= s3_prob: 54 | return chara.fromname(random.choice(self.star3), 3), 50 55 | elif pick <= s2_prob + s3_prob: 56 | return chara.fromname(random.choice(self.star2), 2), 10 57 | else: 58 | return chara.fromname(random.choice(self.star1), 1), 1 59 | 60 | 61 | def gacha_ten(self): 62 | result = [] 63 | hiishi = 0 64 | up = self.up_prob 65 | s3 = self.s3_prob 66 | s2 = self.s2_prob 67 | s1 = 1000 - s3 - s2 68 | for _ in range(9): # 前9连 69 | c, y = self.gacha_one(up, s3, s2, s1) 70 | result.append(c) 71 | hiishi += y 72 | c, y = self.gacha_one(up, s3, s2 + s1, 0) # 保底第10抽 73 | result.append(c) 74 | hiishi += y 75 | 76 | return result, hiishi 77 | 78 | 79 | def gacha_tenjou(self): 80 | total_div_10 = self.tenjou_line // 10 81 | result = {'up': [], 's3': [], 's2':[], 's1':[]} 82 | first_up_pos = 999999 83 | up = self.up_prob 84 | s3 = self.s3_prob 85 | s2 = self.s2_prob 86 | s1 = 1000 - s3 - s2 87 | for i in range(9 * total_div_10): 88 | c, y = self.gacha_one(up, s3, s2, s1) 89 | if 100 == y: 90 | result['up'].append(c) 91 | first_up_pos = min(first_up_pos, 10 * ((i+1) // 9) + ((i+1) % 9)) 92 | elif 50 == y: 93 | result['s3'].append(c) 94 | elif 10 == y: 95 | result['s2'].append(c) 96 | elif 1 == y: 97 | result['s1'].append(c) 98 | else: 99 | pass # should never reach here 100 | for i in range(total_div_10): 101 | c, y = self.gacha_one(up, s3, s2 + s1, 0) 102 | if 100 == y: 103 | result['up'].append(c) 104 | first_up_pos = min(first_up_pos, 10 * (i+1)) 105 | elif 50 == y: 106 | result['s3'].append(c) 107 | elif 10 == y: 108 | result['s2'].append(c) 109 | else: 110 | pass # should never reach here 111 | result['first_up_pos'] = first_up_pos 112 | return result 113 | -------------------------------------------------------------------------------- /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 hoshino import aiorequests, R, Service 12 | from hoshino.typing import * 13 | 14 | sv_help = ''' 15 | 官方四格漫画更新(日文) 16 | [官漫132] 阅览指定话 17 | '''.strip() 18 | sv = Service('pcr-comic', help_=sv_help, bundle='pcr订阅') 19 | 20 | def load_index(): 21 | with open(R.get('img/priconne/comic/index.json').path, encoding='utf8') as f: 22 | return json.load(f) 23 | 24 | def get_pic_name(id_): 25 | pre = 'episode_' 26 | end = '.png' 27 | return f'{pre}{id_}{end}' 28 | 29 | 30 | @sv.on_prefix('官漫') 31 | async def comic(bot, ev: CQEvent): 32 | episode = ev.message.extract_plain_text() 33 | if not re.fullmatch(r'\d{0,3}', episode): 34 | return 35 | episode = episode.lstrip('0') 36 | if not episode: 37 | await bot.send(ev, '请输入漫画集数 如:官漫132', at_sender=True) 38 | return 39 | index = load_index() 40 | if episode not in index: 41 | await bot.send(ev, f'未查找到第{episode}话,敬请期待官方更新', at_sender=True) 42 | return 43 | title = index[episode]['title'] 44 | pic = R.img('priconne/comic/', get_pic_name(episode)).cqcode 45 | msg = f'プリンセスコネクト!Re:Dive公式4コマ\n第{episode}話 {title}\n{pic}' 46 | await bot.send(ev, msg, at_sender=True) 47 | 48 | 49 | async def download_img(save_path, link): 50 | ''' 51 | 从link下载图片保存至save_path(目录+文件名) 52 | 会覆盖原有文件,需保证目录存在 53 | ''' 54 | sv.logger.info(f'download_img from {link}') 55 | resp = await aiorequests.get(link, stream=True) 56 | sv.logger.info(f'status_code={resp.status_code}') 57 | if 200 == resp.status_code: 58 | if re.search(r'image', resp.headers['content-type'], re.I): 59 | sv.logger.info(f'is image, saving to {save_path}') 60 | with open(save_path, 'wb') as f: 61 | f.write(await resp.content) 62 | sv.logger.info('saved!') 63 | 64 | 65 | async def download_comic(id_): 66 | ''' 67 | 下载指定id的官方四格漫画,同时更新漫画目录index.json 68 | episode_num可能会小于id 69 | ''' 70 | base = 'https://comic.priconne-redive.jp/api/detail/' 71 | save_dir = R.img('priconne/comic/').path 72 | index = load_index() 73 | 74 | # 先从api获取detail,其中包含图片真正的链接 75 | sv.logger.info(f'getting comic {id_} ...') 76 | url = base + id_ 77 | sv.logger.info(f'url={url}') 78 | resp = await aiorequests.get(url) 79 | sv.logger.info(f'status_code={resp.status_code}') 80 | if 200 != resp.status_code: 81 | return 82 | data = await resp.json() 83 | data = data[0] 84 | 85 | episode = data['episode_num'] 86 | title = data['title'] 87 | link = data['cartoon'] 88 | index[episode] = {'title': title, 'link': link} 89 | sv.logger.info(f'episode={index[episode]}') 90 | 91 | # 下载图片并保存 92 | await download_img(os.path.join(save_dir, get_pic_name(episode)), link) 93 | 94 | # 保存官漫目录信息 95 | with open(os.path.join(save_dir, 'index.json'), 'w', encoding='utf8') as f: 96 | json.dump(index, f, ensure_ascii=False) 97 | 98 | 99 | @sv.scheduled_job('cron', minute='*/5', second='25') 100 | async def update_seeker(): 101 | ''' 102 | 轮询官方四格漫画更新 103 | 若有更新则推送至订阅群 104 | ''' 105 | index_api = 'https://comic.priconne-redive.jp/api/index' 106 | index = load_index() 107 | 108 | # 获取最新漫画信息 109 | resp = await aiorequests.get(index_api, timeout=10) 110 | data = await resp.json() 111 | id_ = data['latest_cartoon']['id'] 112 | episode = data['latest_cartoon']['episode_num'] 113 | title = data['latest_cartoon']['title'] 114 | 115 | # 检查是否已在目录中 116 | # 同一episode可能会被更新为另一张图片(官方修正),此时episode不变而id改变 117 | # 所以需要两步判断 118 | if episode in index: 119 | qs = urlparse(index[episode]['link']).query 120 | old_id = parse_qs(qs)['id'][0] 121 | if id_ == old_id: 122 | sv.logger.info('未检测到官漫更新') 123 | return 124 | 125 | # 确定已有更新,下载图片 126 | sv.logger.info(f'发现更新 id={id_}') 127 | await download_comic(id_) 128 | 129 | # 推送至各个订阅群 130 | pic = R.img('priconne/comic', get_pic_name(episode)).cqcode 131 | msg = f'プリンセスコネクト!Re:Dive公式4コマ更新!\n第{episode}話 {title}\n{pic}' 132 | await sv.broadcast(msg, 'PCR官方四格', 0.5) 133 | -------------------------------------------------------------------------------- /hoshino/modules/botmanage/service_manage.py: -------------------------------------------------------------------------------- 1 | from functools import cmp_to_key 2 | 3 | from nonebot import CommandSession, on_command 4 | from nonebot import permission as perm 5 | from nonebot.argparse import ArgumentParser 6 | 7 | from hoshino import Service, priv, util 8 | 9 | PRIV_TIP = f'群主={priv.OWNER} 群管={priv.ADMIN} 群员={priv.NORMAL} bot维护组={priv.SUPERUSER}' 10 | 11 | @on_command('lssv', aliases=('服务列表', '功能列表'), permission=perm.GROUP_ADMIN, only_to_me=False, shell_like=True) 12 | async def lssv(session: CommandSession): 13 | parser = ArgumentParser(session=session) 14 | parser.add_argument('-a', '--all', action='store_true') 15 | parser.add_argument('-H', '--hidden', 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 | only_hidden = args.hidden 21 | if session.ctx['user_id'] in session.bot.config.SUPERUSERS: 22 | gid = args.group or session.ctx.get('group_id') 23 | if not gid: 24 | session.finish('Usage: -g|--group [-a|--all]') 25 | else: 26 | gid = session.ctx['group_id'] 27 | 28 | msg = [f"群{gid}服务一览:"] 29 | svs = Service.get_loaded_services().values() 30 | svs = map(lambda sv: (sv, sv.check_enabled(gid)), svs) 31 | 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)) 32 | svs = sorted(svs, key=key) 33 | for sv, on in svs: 34 | if verbose_all or (sv.visible ^ only_hidden): 35 | x = '○' if on else '×' 36 | msg.append(f"|{x}| {sv.name}") 37 | await session.send('\n'.join(msg)) 38 | 39 | 40 | @on_command('enable', aliases=('启用', '开启', '打开'), permission=perm.GROUP, only_to_me=False) 41 | async def enable_service(session: CommandSession): 42 | await switch_service(session, turn_on=True) 43 | 44 | @on_command('disable', aliases=('禁用', '关闭'), permission=perm.GROUP, only_to_me=False) 45 | async def disable_service(session: CommandSession): 46 | await switch_service(session, turn_on=False) 47 | 48 | async def switch_service(session: CommandSession, turn_on: bool): 49 | action_tip = '启用' if turn_on else '禁用' 50 | if session.ctx['message_type'] == 'group': 51 | names = session.current_arg_text.split() 52 | if not names: 53 | session.finish(f"空格后接要{action_tip}的服务名", at_sender=True) 54 | group_id = session.ctx['group_id'] 55 | svs = Service.get_loaded_services() 56 | succ, notfound = [], [] 57 | for name in names: 58 | if name in svs: 59 | sv = svs[name] 60 | u_priv = priv.get_user_priv(session.ctx) 61 | if u_priv >= sv.manage_priv: 62 | sv.set_enable(group_id) if turn_on else sv.set_disable(group_id) 63 | succ.append(name) 64 | else: 65 | try: 66 | await session.send(f'权限不足!{action_tip}{name}需要:{sv.manage_priv},您的:{u_priv}\n{PRIV_TIP}', at_sender=True) 67 | except: 68 | pass 69 | else: 70 | notfound.append(util.filt_message(name)) 71 | msg = [] 72 | if succ: 73 | msg.append(f'已{action_tip}服务:' + ', '.join(succ)) 74 | if notfound: 75 | msg.append('未找到服务:' + ', '.join(notfound)) 76 | if msg: 77 | session.finish('\n'.join(msg), at_sender=True) 78 | 79 | else: 80 | if session.ctx['user_id'] not in session.bot.config.SUPERUSERS: 81 | return 82 | args = session.current_arg_text.split() 83 | if len(args) < 2: 84 | session.finish('Usage: [, ...]') 85 | name, *group_ids = args 86 | svs = Service.get_loaded_services() 87 | if name not in svs: 88 | session.finish(f'未找到服务:{name}') 89 | sv = svs[name] 90 | succ = [] 91 | for gid in group_ids: 92 | try: 93 | gid = int(gid) 94 | sv.set_enable(gid) if turn_on else sv.set_disable(gid) 95 | succ.append(gid) 96 | except: 97 | try: 98 | await session.send(f'非法群号:{gid}') 99 | except: 100 | pass 101 | session.finish(f'服务{name}已于{len(succ)}个群内{action_tip}:{succ}') 102 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/query/query.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from datetime import datetime 3 | from hoshino import util, R 4 | from hoshino.typing import CQEvent 5 | from . import sv 6 | 7 | rank_cn = '18-5' 8 | pcn = R.img(f'priconne/quick/r{rank_cn}-cn-0.png').cqcode 9 | 10 | 11 | def get_support_rank(t: datetime, server): 12 | if server == 'jp': 13 | delta = t - datetime(2021, 8, 15) 14 | elif server == 'tw': 15 | delta = t - datetime(2021, 12, 15) 16 | else: 17 | raise ValueError('Unknown server') 18 | years, days = divmod(delta.days, 365) 19 | rank = 21 + (years * 12 + days // 30) // 3 20 | return rank 21 | 22 | 23 | @sv.on_rex(r'^(\*?([日台国陆b])服?([前中后]*)卫?)?rank(表|推荐|指南)?$') 24 | async def rank_sheet(bot, ev): 25 | match = ev['match'] 26 | is_jp = match.group(2) == '日' 27 | is_tw = match.group(2) == '台' 28 | is_cn = match.group(2) and match.group(2) in '国陆b' 29 | if not is_jp and not is_tw and not is_cn: 30 | await bot.send(ev, '\n请问您要查询哪个服务器的rank表?\n*日rank表\n*台rank表\n*陆rank表', at_sender=True) 31 | return 32 | msg = [ 33 | '\n※rank表仅供参考,升r有风险,强化需谨慎\n※请以会长要求为准', 34 | ] 35 | if is_jp: 36 | await bot.send(ev, f"\n休闲:输出拉满 辅助R{get_support_rank(datetime.now(), 'jp')}-0\n一档:问你家会长", at_sender=True) 37 | elif is_tw: 38 | await bot.send(ev, f"\n休闲:输出拉满 辅助R{get_support_rank(datetime.now(), 'tw')}-0\n一档:问你家会长", at_sender=True) 39 | elif is_cn: 40 | await bot.send(ev, f"https://www.bilibili.com/read/cv19044402\n{pcn}", at_sender=True) 41 | # msg.append(f'※不定期搬运自nga\n※制作by樱花铁道之夜\nR{rank_cn} rank表:\n{pcn}') 42 | # await bot.send(ev, '\n'.join(msg), at_sender=True) 43 | # await util.silence(ev, 60) 44 | 45 | 46 | @sv.on_fullmatch('jjc', 'JJC', 'JJC作业', 'JJC作业网', 'JJC数据库', 'jjc作业', 'jjc作业网', 'jjc数据库') 47 | async def say_arina_database(bot, ev): 48 | await bot.send(ev, '公主连接Re:Dive 竞技场编成数据库\n日文:https://nomae.net/arenadb \n中文:https://pcrdfans.com/battle') 49 | 50 | 51 | OTHER_KEYWORDS = '【日rank】【台rank】【b服rank】【jjc作业网】【黄骑充电表】【一个顶俩】' 52 | PCR_SITES = f''' 53 | 【繁中wiki/兰德索尔图书馆】pcredivewiki.tw 54 | 【日文wiki/GameWith】gamewith.jp/pricone-re 55 | 【日文wiki/AppMedia】appmedia.jp/priconne-redive 56 | 【竞技场作业库(中文)】pcrdfans.com/battle 57 | 【竞技场作业库(日文)】nomae.net/arenadb 58 | 【论坛/NGA社区】nga.178.com/thread.php?fid=-10308342 59 | 【iOS实用工具/初音笔记】nga.178.com/read.php?tid=14878762 60 | 【安卓实用工具/静流笔记】nga.178.com/read.php?tid=20499613 61 | 【台服卡池千里眼】nga.178.com/read.php?tid=28236922 62 | 【日官网】priconne-redive.jp 63 | 【台官网】www.princessconnect.so-net.tw 64 | 65 | ===其他查询关键词=== 66 | {OTHER_KEYWORDS} 67 | ※B服速查请输入【bcr速查】''' 68 | 69 | BCR_SITES = f''' 70 | 【妈宝骑士攻略(懒人攻略合集)】nga.178.com/read.php?tid=20980776 71 | 【机制详解】nga.178.com/read.php?tid=19104807 72 | 【初始推荐】nga.178.com/read.php?tid=20789582 73 | 【术语黑话】nga.178.com/read.php?tid=18422680 74 | 【角色点评】nga.178.com/read.php?tid=20804052 75 | 【秘石规划】nga.178.com/read.php?tid=20101864 76 | 【卡池亿里眼】nga.178.com/read.php?tid=20816796 77 | 【为何卡R卡星】nga.178.com/read.php?tid=20732035 78 | 【推图阵容推荐】nga.178.com/read.php?tid=21010038 79 | 80 | ===其他查询关键词=== 81 | {OTHER_KEYWORDS} 82 | ※日台服速查请输入【pcr速查】''' 83 | 84 | @sv.on_fullmatch('pcr速查', 'pcr图书馆', '图书馆') 85 | async def pcr_sites(bot, ev: CQEvent): 86 | await bot.send(ev, PCR_SITES, at_sender=True) 87 | await util.silence(ev, 60) 88 | @sv.on_fullmatch('bcr速查', 'bcr攻略') 89 | async def bcr_sites(bot, ev: CQEvent): 90 | await bot.send(ev, BCR_SITES, at_sender=True) 91 | await util.silence(ev, 60) 92 | 93 | 94 | YUKARI_SHEET_ALIAS = map(lambda x: ''.join(x), itertools.product(('黄骑', '酒鬼'), ('充电', '充电表', '充能', '充能表'))) 95 | YUKARI_SHEET = f''' 96 | {R.img('priconne/quick/黄骑充电.jpg').cqcode} 97 | ※大圈是1动充电对象 PvP测试 98 | ※黄骑四号位例外较多 99 | ※对面羊驼或中后卫坦 有可能歪 100 | ※我方羊驼算一号位 101 | ※图片搬运自漪夢奈特''' 102 | @sv.on_fullmatch(YUKARI_SHEET_ALIAS) 103 | async def yukari_sheet(bot, ev): 104 | await bot.send(ev, YUKARI_SHEET, at_sender=True) 105 | await util.silence(ev, 60) 106 | 107 | 108 | DRAGON_TOOL = f''' 109 | 拼音对照表:{R.img('priconne/KyaruMiniGame/注音文字.jpg').cqcode}{R.img('priconne/KyaruMiniGame/接龙.jpg').cqcode} 110 | 龍的探索者們小遊戲單字表 https://hanshino.nctu.me/online/KyaruMiniGame 111 | 镜像 https://hoshino.monster/KyaruMiniGame 112 | 网站内有全词条和搜索,或需科学上网''' 113 | @sv.on_fullmatch('一个顶俩', '拼音接龙', '韵母接龙') 114 | async def dragon(bot, ev): 115 | await bot.send(ev, DRAGON_TOOL, at_sender=True) 116 | await util.silence(ev, 60) 117 | 118 | 119 | @sv.on_fullmatch('千里眼') 120 | async def future_gacha(bot, ev): 121 | await bot.send(ev, "亿里眼·一之章 nga.178.com/read.php?tid=21317816\n亿里眼·二之章 nga.178.com/read.php?tid=25358671") 122 | await util.silence(ev, 60) 123 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/gacha/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "MIX": { 3 | "up_prob": 7, 4 | "s3_prob": 30, 5 | "s2_prob": 180, 6 | "up": [ "伊莉亚(新年)", "杏奈(海盗)" ], 7 | "_comment": "star3 仅填3星常驻角色。不要填UP角,否则出率会偏高", 8 | "star3": [ 9 | "杏奈","真步","璃乃","初音","霞","伊绪", 10 | "咲恋","望","妮诺","秋乃","镜华","智","真琴", 11 | "伊莉亚","纯","静流","莫妮卡","流夏","吉塔", 12 | "亚里莎","安","古蕾娅", "空花(大江户)", "妮诺(大江户)", 13 | "克萝依", "碧(插班生)", "美美(万圣节)", "露娜", 14 | "伊莉亚(圣诞节)", "霞(魔法少女)", "优妮", "琪爱儿", 15 | "铃(游侠)", "真阳(游侠)", "璃乃(奇幻)", "祈梨", 16 | "七七香(夏日)", "纯(夏日)", "茜里(天使)", "依里(天使)", 17 | "莫妮卡(魔法少女)", "智(魔法少女)", "咲恋(圣诞节)", 18 | "雪菲", "霞(夏日)", "真琴(灰姑娘)", "千歌(夏日)", "雪(大江户)" 19 | ], 20 | "other_normal_star3": [ ], 21 | "star2": [ 22 | "茉莉","茜里","宫子","雪","七七香","美里", 23 | "铃奈","香织","美美","绫音","铃","惠理子", 24 | "忍","真阳","栞","千歌","空花","珠希","美冬", 25 | "深月","纺希" 26 | ], 27 | "star1" : [ 28 | "日和","怜","禊","胡桃","依里","铃莓", 29 | "优花梨","碧","美咲","莉玛","步未" 30 | ] 31 | }, 32 | "TW": { 33 | "up_prob": 7, 34 | "s3_prob": 30, 35 | "s2_prob": 180, 36 | "up": [ "伊莉亚(新年)" ], 37 | "_comment": "star3 仅填3星常驻角色。不要填UP角,否则出率会偏高", 38 | "star3": [ 39 | "杏奈","真步","璃乃","初音","霞","伊绪", 40 | "咲恋","望","妮诺","秋乃","镜华","智","真琴", 41 | "伊莉亚","纯","静流","莫妮卡","流夏","吉塔", 42 | "亚里莎","安","古蕾娅", "空花(大江户)", "妮诺(大江户)", 43 | "克萝依", "碧(插班生)", "美美(万圣节)", "露娜", 44 | "伊莉亚(圣诞节)", "霞(魔法少女)", "优妮", "琪爱儿", 45 | "铃(游侠)", "真阳(游侠)", "璃乃(奇幻)", "祈梨", 46 | "七七香(夏日)", "纯(夏日)", "茜里(天使)", "依里(天使)", 47 | "莫妮卡(魔法少女)", "智(魔法少女)", "咲恋(圣诞节)", 48 | "雪菲", "霞(夏日)", "真琴(灰姑娘)" 49 | ], 50 | "other_normal_star3": [ ], 51 | "star2": [ 52 | "茉莉","茜里","宫子","雪","七七香","美里", 53 | "铃奈","香织","美美","绫音","铃","惠理子", 54 | "忍","真阳","栞","千歌","空花","珠希","美冬", 55 | "深月","纺希" 56 | ], 57 | "star1" : [ 58 | "日和","怜","禊","胡桃","依里","铃莓", 59 | "优花梨","碧","美咲","莉玛","步未" 60 | ] 61 | }, 62 | "JP": { 63 | "up_prob": 7, 64 | "s3_prob": 30, 65 | "s2_prob": 180, 66 | "up": [ "杏奈(海盗)" ], 67 | "_comment": "star3 仅填3星常驻角色。不要填UP角,否则出率会偏高", 68 | "star3": [ 69 | "杏奈","真步","璃乃","初音","霞","伊绪", 70 | "咲恋","望","妮诺","秋乃","镜华","智","真琴", 71 | "伊莉亚","纯","静流","莫妮卡","流夏","吉塔", 72 | "亚里莎","安","古蕾娅", "空花(大江户)", "妮诺(大江户)", 73 | "克萝依", "碧(插班生)", "美美(万圣节)", "露娜", 74 | "伊莉亚(圣诞节)", "霞(魔法少女)", "优妮", "琪爱儿", 75 | "铃(游侠)", "真阳(游侠)", "璃乃(奇幻)", "祈梨", 76 | "七七香(夏日)", "纯(夏日)", "茜里(天使)", "依里(天使)", 77 | "莫妮卡(魔法少女)", "智(魔法少女)", "咲恋(圣诞节)", 78 | "雪菲", "霞(夏日)", "真琴(灰姑娘)", "真步(灰姑娘)" 79 | ], 80 | "other_normal_star3": [ ], 81 | "star2": [ 82 | "茉莉","茜里","宫子","雪","七七香","美里", 83 | "铃奈","香织","美美","绫音","铃","惠理子", 84 | "忍","真阳","栞","千歌","空花","珠希","美冬", 85 | "深月","纺希" 86 | ], 87 | "star1" : [ 88 | "日和","怜","禊","胡桃","依里","铃莓", 89 | "优花梨","碧","美咲","莉玛","步未" 90 | ] 91 | }, 92 | "BL": { 93 | "up_prob": 7, 94 | "s3_prob": 25, 95 | "s2_prob": 180, 96 | "up": [ "真阳(游侠)" ], 97 | "_comment": "star3 仅填3星常驻角色。不要填UP角,否则出率会偏高", 98 | "star3": [ 99 | "杏奈","真步","璃乃","初音","霞","伊绪", 100 | "咲恋","望","妮诺","秋乃","镜华","智","真琴", 101 | "伊莉亚","纯","静流","莫妮卡","流夏","吉塔", 102 | "亚里莎","安","古蕾娅", "空花(大江户)", "妮诺(大江户)", 103 | "克萝依", "碧(插班生)", "美美(万圣节)", "露娜", 104 | "伊莉亚(圣诞节)", "霞(魔法少女)", "优妮", "琪爱儿", 105 | "铃(游侠)" 106 | ], 107 | "other_normal_star3": [ 108 | "伊莉亚(圣诞节)", "霞(魔法少女)", "优妮", "琪爱儿", 109 | "铃(游侠)", "真阳(游侠)", "璃乃(奇幻)", "祈梨", 110 | "七七香(夏日)", "纯(夏日)", "茜里(天使)", "依里(天使)", 111 | "莫妮卡(魔法少女)", "智(魔法少女)", "咲恋(圣诞节)", 112 | "雪菲", "霞(夏日)", "真琴(灰姑娘)", "真步(灰姑娘)" 113 | ], 114 | "star2": [ 115 | "茜里","宫子","雪","铃", 116 | "铃奈","香织","美美","绫音","惠理子", 117 | "真阳","栞","千歌","空花","珠希","美冬", 118 | "深月", "美里", "纺希", "忍", "茉莉" 119 | ], 120 | "star1" : [ 121 | "日和","怜","禊","胡桃","依里","铃莓", 122 | "优花梨","碧","美咲","莉玛","步未" 123 | ] 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /hoshino/modules/groupmaster/anti_holo.py: -------------------------------------------------------------------------------- 1 | # 站在历史正确的一边 2 | # ——告台湾情治部门书 3 | # 2020年10月15日 10:19:47 来源: 人民日报 作者: 安平 4 | # 5 | # 近期,媒体公开报道了我国家安全机关组织实施“迅雷—2020”专项行动的有关情况。 6 | # 从披露内容看,这是继“2018—雷霆行动”之后,国家安全机关针对台湾当局及其间谍情报机关的又一次重拳出击。 7 | # 此次专项行动,共破获数百起台湾间谍窃密案件,抓获一批台湾间谍及运用人员,打掉多个台湾间谍情报机关针对祖国大陆布建的间谍情报网络。 8 | # 从2018年的百余起到2020年的数百起,案件数字的攀升, 9 | # 背后折射出的是台湾当局及其间谍情报机关困兽犹斗,近乎疯狂地对祖国大陆实施各类情报渗透破坏活动。 10 | # 11 | # 无风不起浪。一个时期以来,在国际大气候和台湾岛内小气候影响下,蔡英文当局愈加挟洋自重,加紧与外部反华势力勾连聚合, 12 | # 频频制造事端,严重危害两岸和平和台海局势稳定。根据国际危机组织观察,台湾海峡已被列入“重大形势恶化地区”。将心比心, 13 | # 两岸人民都不愿兵戎相见。但如不幸战事爆发,罪魁祸首必是“台独”。更须值得警惕的是,战事未发,情报先行。 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 | # 历史若镜,岁月淘沙。解决台湾问题,实现祖国完全统一,是大势所趋、民心所向。完成这一历史大业,必须依靠两岸同胞的和衷共济、共同奋斗。 46 | # 我们相信,只要两岸同胞共担民族大义,勠力同心,相向而行,必能共享中华民族复兴的伟大荣光。 47 | # (全文完) 48 | 49 | SB_HOLO = ''' 50 | Hololive ホロ 木口 术口 51 | 时乃空 ときのそら Tokino 空妈 52 | 萝卜子 ロボ子さん Roboko 大根子 53 | 樱巫女 さくらみこ Sakura Miko 樱火龙 elite工厂厂长 黄油精英巫女 赌鬼巫女 樱污女 54 | 星街彗星 星街 すいせい Hoshimati Suisei 星姐 阿星 彗彗 suisui 有点神经病的蓝发大姐姐 有点大姐姐的蓝发神经病 55 | 夜空梅露 夜空メル Yozora Mel 梅露 梅球王 Banpire 清楚系天才美少女吸血鬼 56 | 夏色祭 まつり Natsuiro Matsuri 马自立 祭妹 夏哥 夏半首 57 | 赤井心 赤井はあと Akai Haato 心心 心大人 はぁちゃま 哈恰玛 哈恰嘛 58 | 亚绮 罗森塔尔 アキ ローゼンタール Aki Rosenthal akirose 李姐 力速双A弱精灵 59 | 白上吹雪 白上フブキ Shirakami Fubuki 🌽 fbk 小狐狸 屑狐狸 喵喵狐 debuki 玉米人 赫鲁晓狐 工具狐 60 | 人见酷丽丝 人見クリス Hitomi Kurisu 61 | 凑阿库娅 湊あくあ Minato Aqua 阿库娅 洋葱 阿夸 夸哥 夸神 海王 山田赫敏 大亏哥 桐谷夸人 62 | 紫咲诗音 紫咲シオン Murasaki Shion 诗音 傻紫 小学生 紫咲紫苑 63 | 百鬼绫目 百鬼あやめ Nakiri Ayame 狗狗 绫目 64 | 愈月巧可 癒月ちょこ Yuzuki Choco 巧可老师 65 | 大空昴 大空スバル Ōzora Subaru 古神 66 | 大神澪 大神ミオ Ōkami Mio 大神三才 二哈 三才妈妈 67 | 猫又小粥 猫又おかゆ Nekomata Okayu 小粥 阿汤 汤哥 饭团猫 大脸猫 68 | 戌神 沁音 戌神ころね Inugami Korone 吼啦迷迭吼啦哟 面包狗 外神 69 | 兔田佩克拉 兎田ぺこら Usada Pekora 佩克拉 佩可拉 屑兔子 70 | 润羽露西娅 潤羽るしあ Uruha Rushia 死灵魔法师 死灵使 绿粽子 三亚王 狂爆粽子 阿马粽 71 | 不知火芙蕾雅 不知火フレア Shiranui Flare 芙蕾雅 芙碳 阿火 ぬいぬい nuinui フーたん 72 | 白银诺艾尔 白銀ノエル Shirogane Noel 73 | 宝钟 玛琳 宝鐘マリン Houshou Marine 无船承运人 山贼 74 | 天音彼方 天音かなた Amane Kanata 彼方碳 かなたん PP天使 天妈 音妹 天音 天音腾格尔 75 | 桐生可可 桐生 ココ Kiryū Kiryuu Coco 🐉 虫皇 龙皇 憨憨龙 ass龙 宝批龙 西成女王 76 | 角卷绵芽 角巻わため Tsunomaki Watame wtm 毛球 顶顶羊 咚咚羊 畜生羊 木口锤石 77 | 常暗永远 常闇トワ Tokoyami Towa 永远大人 トワ様 78 | 姬森璐娜 姫森 ルーナ Himemori Luuna 璐娜 英语教师 79 | 雪花菈米 雪花ラミィYukihana Lamy 菈米 雪花纯生 雪妈 菈米妈妈 80 | 桃铃音音 桃鈴ねね Momosuzu Nene 铃桃音音 81 | 狮白牡丹 獅白ぼたん Shishiro Botan 狮白 82 | 魔乃阿萝耶 魔乃アロエ Mano Aloe 魔乃 阿萝耶 83 | 尾丸波尔卡 尾丸ポルカ Omaru Polka 特质系西索 虚拟关羽 84 | AZKi AZiK 85 | Ayunda Risu 栗鼠 印尼面包狗 猫狗的女儿 86 | Moona Hoshinova 87 | Airani Iofifteen 88 | 噶呜 古拉 Gawr Gura がうる ぐら 鲨鲨 小鲨鱼 小傻鱼 傻鱼 89 | 一伊那尔栖 Ninomae Ina'nis 一伊那尓栖 伊那 90 | 森美声 Mori Calliope 巨乳版露西娅 巨乳露西娅 91 | 小鸟游琪亚拉 Takanashi Kiara 小鳥遊キアラ 琪亚拉 92 | 华生 阿米莉亚 Watson Amelia ワトソン アメリア 艾米 93 | 友人A YuujinA 94 | 谷乡元昭 YAGOO 谷郷元昭 Tanigou Motoaki 95 | 斯哈斯哈 96 | '''.split() 97 | # 复制完了 快吐了 98 | 99 | from datetime import timedelta 100 | from hoshino import Service, priv, util, R, HoshinoBot 101 | from hoshino.typing import CQEvent 102 | 103 | HAHAHA_VTB_TIANGOU = R.img('hahaha_vtb_tiangou.jpg') 104 | sv = Service('anti-holo', manage_priv=priv.SUPERUSER) 105 | 106 | # @sv.on_keyword(SB_HOLO) 107 | async def anti_holo(bot: HoshinoBot, ev: CQEvent): 108 | priv.set_block_user(ev.user_id, timedelta(minutes=1)) 109 | await util.silence(ev, 60, skip_su=False) 110 | await bot.send(ev, HAHAHA_VTB_TIANGOU.cqcode) 111 | await bot.delete_msg(self_id=ev.self_id, message_id=ev.message_id) 112 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/arena/arena.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | import time 4 | 5 | from hoshino import aiorequests, config 6 | 7 | from .. import chara 8 | from . import sv 9 | 10 | try: 11 | import ujson as json 12 | except: 13 | import json 14 | 15 | 16 | logger = sv.logger 17 | 18 | """ 19 | Database for arena likes & dislikes 20 | DB is a dict like: { 'md5_id': {'like': set(qq), 'dislike': set(qq)} } 21 | """ 22 | DB_PATH = os.path.expanduser("~/.hoshino/arena_db.json") 23 | DB = {} 24 | try: 25 | with open(DB_PATH, encoding="utf8") as f: 26 | DB = json.load(f) 27 | for k in DB: 28 | DB[k] = { 29 | "like": set(DB[k].get("like", set())), 30 | "dislike": set(DB[k].get("dislike", set())), 31 | } 32 | except FileNotFoundError: 33 | logger.warning(f"arena_db.json not found, will create when needed.") 34 | 35 | 36 | def dump_db(): 37 | """ 38 | Dump the arena databese. 39 | json do not accept set object, this function will help to convert. 40 | """ 41 | j = {} 42 | for k in DB: 43 | j[k] = { 44 | "like": list(DB[k].get("like", set())), 45 | "dislike": list(DB[k].get("dislike", set())), 46 | } 47 | with open(DB_PATH, "w", encoding="utf8") as f: 48 | json.dump(j, f, ensure_ascii=False) 49 | 50 | 51 | def get_likes(id_): 52 | return DB.get(id_, {}).get("like", set()) 53 | 54 | 55 | def add_like(id_, uid): 56 | e = DB.get(id_, {}) 57 | l = e.get("like", set()) 58 | k = e.get("dislike", set()) 59 | l.add(uid) 60 | k.discard(uid) 61 | e["like"] = l 62 | e["dislike"] = k 63 | DB[id_] = e 64 | 65 | 66 | def get_dislikes(id_): 67 | return DB.get(id_, {}).get("dislike", set()) 68 | 69 | 70 | def add_dislike(id_, uid): 71 | e = DB.get(id_, {}) 72 | l = e.get("like", set()) 73 | k = e.get("dislike", set()) 74 | l.discard(uid) 75 | k.add(uid) 76 | e["like"] = l 77 | e["dislike"] = k 78 | DB[id_] = e 79 | 80 | 81 | _last_query_time = 0 82 | quick_key_dic = {} # {quick_key: true_id} 83 | 84 | 85 | def refresh_quick_key_dic(): 86 | global _last_query_time 87 | now = time.time() 88 | if now - _last_query_time > 300: 89 | quick_key_dic.clear() 90 | _last_query_time = now 91 | 92 | 93 | def gen_quick_key(true_id: str, user_id: int) -> str: 94 | qkey = int(true_id[-6:], 16) 95 | while qkey in quick_key_dic and quick_key_dic[qkey] != true_id: 96 | qkey = (qkey + 1) & 0xFFFFFF 97 | quick_key_dic[qkey] = true_id 98 | mask = user_id & 0xFFFFFF 99 | qkey ^= mask 100 | return base64.b32encode(qkey.to_bytes(3, "little")).decode()[:5] 101 | 102 | 103 | def get_true_id(quick_key: str, user_id: int) -> str: 104 | mask = user_id & 0xFFFFFF 105 | if not isinstance(quick_key, str) or len(quick_key) != 5: 106 | return None 107 | qkey = (quick_key + "===").encode() 108 | qkey = int.from_bytes(base64.b32decode(qkey, casefold=True, map01=b"I"), "little") 109 | qkey ^= mask 110 | return quick_key_dic.get(qkey, None) 111 | 112 | 113 | def __get_auth_key(): 114 | return config.priconne.arena.AUTH_KEY 115 | 116 | 117 | async def do_query(id_list, user_id, region=1): 118 | id_list = [x * 100 + 1 for x in id_list] 119 | header = { 120 | "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", 121 | "authorization": __get_auth_key(), 122 | } 123 | payload = { 124 | "_sign": "a", 125 | "def": id_list, 126 | "nonce": "a", 127 | "page": 1, 128 | "sort": 1, 129 | "ts": int(time.time()), 130 | "region": region, 131 | } 132 | logger.debug(f"Arena query {payload=}") 133 | try: 134 | resp = await aiorequests.post( 135 | "https://api.pcrdfans.com/x/v1/search", 136 | headers=header, 137 | json=payload, 138 | timeout=10, 139 | ) 140 | res = await resp.json() 141 | logger.debug(f"len(res)={len(res)}") 142 | except Exception as e: 143 | logger.exception(e) 144 | return None 145 | 146 | if res["code"]: 147 | logger.error(f"Arena query failed.\nResponse={res}\nPayload={payload}") 148 | raise aiorequests.HTTPError(response=res) 149 | 150 | result = res.get("data", {}).get("result") 151 | if result is None: 152 | return None 153 | ret = [] 154 | for entry in result: 155 | eid = entry["id"] 156 | likes = get_likes(eid) 157 | dislikes = get_dislikes(eid) 158 | ret.append( 159 | { 160 | "qkey": gen_quick_key(eid, user_id), 161 | "atk": [ 162 | chara.fromid(c["id"] // 100, c["star"], c["equip"]) 163 | for c in entry["atk"] 164 | ], 165 | "def": [ 166 | chara.fromid(c["id"] // 100, c["star"], c["equip"]) 167 | for c in entry["def"] 168 | ], 169 | "up": entry["up"], 170 | "down": entry["down"], 171 | "my_up": len(likes), 172 | "my_down": len(dislikes), 173 | "user_like": 1 174 | if user_id in likes 175 | else -1 176 | if user_id in dislikes 177 | else 0, 178 | } 179 | ) 180 | 181 | return ret 182 | 183 | 184 | async def do_like(qkey, user_id, action): 185 | true_id = get_true_id(qkey, user_id) 186 | if true_id is None: 187 | raise KeyError 188 | add_like(true_id, user_id) if action > 0 else add_dislike(true_id, user_id) 189 | dump_db() 190 | # TODO: upload to website 191 | -------------------------------------------------------------------------------- /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/kancolle/query/__init__.py: -------------------------------------------------------------------------------- 1 | from hoshino.service import Service 2 | from hoshino import R 3 | from hoshino.util import FreqLimiter 4 | from hoshino.typing import CQEvent 5 | 6 | sv_help = ''' 7 | [人事表200102] 战果人事表(数字为 年/月/服务器) 8 | [.qj 晓] 预测运值增加(准确率高达25%)(需开启dice) 9 | [1-1] 直达梦美妈妈攻略贴 10 | [*晓改二] 舰娘信息查询(暂不可用) 11 | [*震电] 装备信息查询(暂不可用) 12 | === 速查表 === 13 | [dd2][远征][蜜瓜][抗击坠][攻略] 14 | '''.strip() 15 | sv = Service('kc-query', enable_on_default=False, help_=sv_help, bundle='kancolle') 16 | 17 | 18 | limiter = FreqLimiter(120) 19 | 20 | @sv.on_fullmatch('攻略', 'e1', 'e2', 'e3', 'e4', 'e5', 'E1', 'E2', 'E3', 'E4', 'E5') 21 | async def _(bot, ev: CQEvent): 22 | if limiter.check(ev.group_id): 23 | await bot.send(ev, ''' 24 | 2022夏&初秋活「大規模反攻上陸!トーチ作戦!」 25 | 秋月牧场 nga.178.com/read.php?tid=33228729 26 | 双子座 nga.178.com/read.php?tid=33238545 27 | 十级证书 nga.178.com/read.php?tid=33243768 28 | 风岛 zekamashi.net/202208-event/torch-sougou/ 29 | IceCirno的活动记录 nga.178.com/read.php?tid=33282039 30 | 其他关键词:[拆包][带路][信息搬运] 31 | '''.strip()) 32 | limiter.start_cd(ev.group_id) 33 | 34 | sv.on_fullmatch('拆包')(lambda bot, ev: bot.send(ev, '2022夏活拆包集中讨论\nhttps://nga.178.com/read.php?tid=33241849')) 35 | sv.on_fullmatch('带路')(lambda bot, ev: bot.send(ev, '极简版[大規模反攻上陸!トーチ作戦!]信息搬运贴\nhttps://nga.178.com/read.php?tid=33242680')) 36 | sv.on_fullmatch('信息搬运')(lambda bot, ev: bot.send(ev, '[大规模反复迷路]2022年夏活海图带路条件\nhttps://nga.178.com/read.php?tid=33233876')) 37 | 38 | sv.on_fullmatch('驱逐改二', 'dd改二', 'DD改二', 'dd2')(lambda bot, ev: bot.send(ev, R.img('kancolle/quick/驱逐改二早见表.jpg').cqcode + R.img('kancolle/quick/驱逐改早见表.jpg').cqcode)) 39 | sv.on_fullmatch('远征')(lambda bot, ev: bot.send(ev, f"nga.178.com/read.php?tid=21866416 {R.img('kancolle/quick/远征大成功.png').cqcode} {R.img('kancolle/quick/远征大成功简.png').cqcode}")) 40 | sv.on_fullmatch('蜜瓜', '夕张')(lambda bot, ev: bot.send(ev, f"https://zh.kcwiki.cn/wiki/%E5%A4%95%E5%BC%A0#.E6.88.98.E6.96.97.E7.89.B9.E6.80.A7 {R.img('kancolle/quick/夕张改二装备适性.png').cqcode}")) 41 | sv.on_fullmatch('对空回避', '抗击坠', '抗击坠表')(lambda bot, ev: bot.send(ev, f"https://docs.google.com/spreadsheets/d/1cfV8sHvX1vMEQcckQaG1cBCXWctnS0P_GT9z74EotPw {R.img('kancolle/quick/对空回避.png').cqcode}")) 42 | 43 | # ==================================== # 44 | 45 | sv.on_fullmatch('日常', '周常', '月常', '季常', '年常')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454450573")) 46 | 47 | sv.on_fullmatch('1-1')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454451942")) 48 | sv.on_fullmatch('1-2')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454452006")) 49 | sv.on_fullmatch('1-3')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454452053")) 50 | sv.on_fullmatch('1-4')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454452119")) 51 | sv.on_fullmatch('1-5')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454452178")) 52 | sv.on_fullmatch('1-6')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454452236")) 53 | 54 | sv.on_fullmatch('2-1')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454452562")) 55 | sv.on_fullmatch('2-2')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454452628")) 56 | sv.on_fullmatch('2-3')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454452698")) 57 | sv.on_fullmatch('2-4')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454452788")) 58 | sv.on_fullmatch('2-5')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454452843")) 59 | 60 | sv.on_fullmatch('3-1')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454453286")) 61 | sv.on_fullmatch('3-2')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454453344")) 62 | sv.on_fullmatch('3-3')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454453396")) 63 | sv.on_fullmatch('3-4')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454453451")) 64 | sv.on_fullmatch('3-5')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454453495")) 65 | 66 | sv.on_fullmatch('7-1')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454455961")) 67 | sv.on_fullmatch('7-2')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454456055")) 68 | sv.on_fullmatch('7-3')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454456110")) 69 | sv.on_fullmatch('7-4')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454456265")) 70 | 71 | sv.on_fullmatch('4-1')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454453876")) 72 | sv.on_fullmatch('4-2')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454454019")) 73 | sv.on_fullmatch('4-3')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454454084")) 74 | sv.on_fullmatch('4-4')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454454163")) 75 | sv.on_fullmatch('4-5')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454454205")) 76 | 77 | sv.on_fullmatch('5-1')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454454625")) 78 | sv.on_fullmatch('5-2')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454454730")) 79 | sv.on_fullmatch('5-3')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454454783")) 80 | sv.on_fullmatch('5-4')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454454829")) 81 | sv.on_fullmatch('5-5')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454454888")) 82 | 83 | sv.on_fullmatch('6-1')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454455307")) 84 | sv.on_fullmatch('6-2')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454455364")) 85 | sv.on_fullmatch('6-3')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454455426")) 86 | sv.on_fullmatch('6-4')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454455513")) 87 | sv.on_fullmatch('6-5')(lambda bot, ev: bot.send(ev, "https://nga.178.com/read.php?pid=454455575")) 88 | 89 | 90 | 91 | from .fleet import * 92 | from .senka import * 93 | -------------------------------------------------------------------------------- /hoshino/util/textfilter/filter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | # ref: https://github.com/observerss/textfilter 5 | 6 | from collections import defaultdict 7 | import re 8 | 9 | __all__ = ['NaiveFilter', 'BSFilter', 'DFAFilter'] 10 | __author__ = 'observer' 11 | __date__ = '2012.01.05' 12 | 13 | 14 | class NaiveFilter(): 15 | 16 | '''Filter Messages from keywords 17 | 18 | very simple filter implementation 19 | 20 | >>> f = NaiveFilter() 21 | >>> f.add("sexy") 22 | >>> f.filter("hello sexy baby") 23 | hello **** baby 24 | ''' 25 | 26 | def __init__(self): 27 | self.keywords = set([]) 28 | 29 | def parse(self, path): 30 | for keyword in open(path, encoding='utf8'): 31 | self.keywords.add(keyword.strip().decode('utf-8').lower()) 32 | 33 | def filter(self, message, repl="*"): 34 | message = message.lower() 35 | for kw in self.keywords: 36 | message = message.replace(kw, repl) 37 | return message 38 | 39 | 40 | class BSFilter: 41 | 42 | '''Filter Messages from keywords 43 | 44 | Use Back Sorted Mapping to reduce replacement times 45 | 46 | >>> f = BSFilter() 47 | >>> f.add("sexy") 48 | >>> f.filter("hello sexy baby") 49 | hello **** baby 50 | ''' 51 | 52 | def __init__(self): 53 | self.keywords = [] 54 | self.kwsets = set([]) 55 | self.bsdict = defaultdict(set) 56 | self.pat_en = re.compile(r'^[0-9a-zA-Z]+$') # english phrase or not 57 | 58 | def add(self, keyword): 59 | # if not isinstance(keyword, unicode): 60 | # keyword = keyword.decode('utf-8') 61 | keyword = keyword.lower() 62 | if keyword not in self.kwsets: 63 | self.keywords.append(keyword) 64 | self.kwsets.add(keyword) 65 | index = len(self.keywords) - 1 66 | for word in keyword.split(): 67 | if self.pat_en.search(word): 68 | self.bsdict[word].add(index) 69 | else: 70 | for char in word: 71 | self.bsdict[char].add(index) 72 | 73 | def parse(self, path): 74 | with open(path, 'r', encoding='utf8') as f: 75 | for keyword in f: 76 | self.add(keyword.strip()) 77 | 78 | def filter(self, message, repl="*"): 79 | # if not isinstance(message, unicode): 80 | # message = message.decode('utf-8') 81 | message = message.lower() 82 | for word in message.split(): 83 | if self.pat_en.search(word): 84 | for index in self.bsdict[word]: 85 | message = message.replace(self.keywords[index], repl) 86 | else: 87 | for char in word: 88 | for index in self.bsdict[char]: 89 | message = message.replace(self.keywords[index], repl) 90 | return message 91 | 92 | 93 | class DFAFilter(): 94 | 95 | '''Filter Messages from keywords 96 | 97 | Use DFA to keep algorithm perform constantly 98 | 99 | >>> f = DFAFilter() 100 | >>> f.add("sexy") 101 | >>> f.filter("hello sexy baby") 102 | hello **** baby 103 | ''' 104 | 105 | def __init__(self): 106 | self.keyword_chains = {} 107 | self.delimit = '\x00' 108 | 109 | def add(self, keyword): 110 | # if not isinstance(keyword, unicode): 111 | # keyword = keyword.decode('utf-8') 112 | # keyword = keyword.lower() 113 | chars = keyword.strip() 114 | if not chars: 115 | return 116 | level = self.keyword_chains 117 | for i in range(len(chars)): 118 | if chars[i] in level: 119 | level = level[chars[i]] 120 | else: 121 | if not isinstance(level, dict): 122 | break 123 | for j in range(i, len(chars)): 124 | level[chars[j]] = {} 125 | last_level, last_char = level, chars[j] 126 | level = level[chars[j]] 127 | last_level[last_char] = {self.delimit: 0} 128 | break 129 | if i == len(chars) - 1: 130 | level[self.delimit] = 0 131 | 132 | def parse(self, path): 133 | with open(path, 'r', encoding='utf8') as f: 134 | for keyword in f: 135 | self.add(keyword.strip()) 136 | 137 | def filter(self, message, repl="*"): 138 | # if not isinstance(message, unicode): 139 | # message = message.decode('utf-8') 140 | # message = message.lower() 141 | ret = [] 142 | start = 0 143 | while start < len(message): 144 | level = self.keyword_chains 145 | step_ins = 0 146 | for char in message[start:]: 147 | if char in level: 148 | step_ins += 1 149 | if self.delimit not in level[char]: 150 | level = level[char] 151 | else: 152 | ret.append(repl * step_ins) 153 | start += step_ins - 1 154 | break 155 | else: 156 | ret.append(message[start]) 157 | break 158 | else: 159 | ret.append(message[start]) 160 | start += 1 161 | 162 | return ''.join(ret) 163 | 164 | 165 | def test_first_character(): 166 | gfw = DFAFilter() 167 | gfw.add("1989年") 168 | assert gfw.filter("1989", "*") == "1989" 169 | 170 | 171 | if __name__ == "__main__": 172 | # gfw = NaiveFilter() 173 | # gfw = BSFilter() 174 | gfw = DFAFilter() 175 | gfw.parse("keywords") 176 | import time 177 | t = time.time() 178 | print(gfw.filter("法轮功 我操操操", "*")) 179 | print(gfw.filter("针孔摄像机 我操操操", "*")) 180 | print(gfw.filter("售假人民币 我操操操", "*")) 181 | print(gfw.filter("传世私服 我操操操", "*")) 182 | print(time.time() - t) 183 | 184 | test_first_character() 185 | -------------------------------------------------------------------------------- /hoshino/trigger.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import defaultdict 3 | import copy 4 | 5 | import pygtrie 6 | import zhconv 7 | 8 | import hoshino 9 | from hoshino import util 10 | from hoshino.typing import CQEvent, List, Iterable 11 | 12 | from typing import TYPE_CHECKING 13 | 14 | if TYPE_CHECKING: 15 | from hoshino.service import ServiceFunc 16 | 17 | 18 | class BaseTrigger: 19 | def add(self, x, sf: "ServiceFunc"): 20 | raise NotImplementedError 21 | 22 | def find_handler(self, event: CQEvent) -> List["ServiceFunc"]: 23 | raise NotImplementedError 24 | 25 | 26 | class PrefixTrigger(BaseTrigger): 27 | def __init__(self): 28 | super().__init__() 29 | self.trie = pygtrie.CharTrie() 30 | 31 | def add(self, prefix: str, sf: "ServiceFunc"): 32 | prefix_cht = zhconv.convert(prefix, "zh-hant") 33 | if prefix in self.trie: 34 | self.trie[prefix].append(sf) 35 | if prefix_cht != prefix: 36 | self.trie[prefix_cht].append(sf) 37 | hoshino.logger.warning(f"Prefix trigger `{prefix}` added multiple handlers: {sf.__name__}@{sf.sv.name}") 38 | else: 39 | self.trie[prefix] = [sf] 40 | if prefix_cht != prefix: 41 | self.trie[prefix_cht] = [sf] 42 | hoshino.logger.debug(f"Succeed to add prefix trigger `{prefix}`") 43 | 44 | def find_handler(self, event: CQEvent) -> Iterable["ServiceFunc"]: 45 | first_msg_seg = event.message[0] 46 | if first_msg_seg.type != "text": 47 | return 48 | first_text = first_msg_seg.data["text"].lstrip() 49 | item = self.trie.longest_prefix(first_text) 50 | if not item: 51 | return 52 | 53 | oldmsg = copy.deepcopy(event.message) 54 | event["prefix"] = item.key 55 | first_text = first_text[len(item.key):].lstrip() 56 | if not first_text and len(event.message) > 1: 57 | del event.message[0] 58 | else: 59 | first_msg_seg.data["text"] = first_text 60 | 61 | for sf in item.value: 62 | yield sf 63 | 64 | event.message = oldmsg 65 | 66 | 67 | class SuffixTrigger(BaseTrigger): 68 | def __init__(self): 69 | super().__init__() 70 | self.trie = pygtrie.CharTrie() 71 | 72 | def add(self, suffix: str, sf: "ServiceFunc"): 73 | suffix_r = suffix[::-1] 74 | suffix_r_cht = zhconv.convert(suffix_r, "zh-hant") 75 | if suffix_r in self.trie: 76 | self.trie[suffix_r].append(sf) 77 | if suffix_r_cht != suffix_r: 78 | self.trie[suffix_r_cht].append(sf) 79 | hoshino.logger.warning(f"Suffix trigger `{suffix}` added multi handler: `{sf.__name__}`") 80 | else: 81 | self.trie[suffix_r] = [sf] 82 | if suffix_r_cht != suffix_r: 83 | self.trie[suffix_r_cht] = [sf] 84 | hoshino.logger.debug(f"Succeed to add suffix trigger `{suffix}`") 85 | 86 | def find_handler(self, event: CQEvent) -> Iterable["ServiceFunc"]: 87 | last_msg_seg = event.message[-1] 88 | if last_msg_seg.type != "text": 89 | return 90 | last_text = last_msg_seg.data["text"].rstrip() 91 | item = self.trie.longest_prefix(last_text[::-1]) 92 | if not item: 93 | return 94 | 95 | oldmsg = copy.deepcopy(event.message) 96 | event["suffix"] = item.key[::-1] 97 | last_text = last_text[: -len(item.key)].rstrip() 98 | if not last_text and len(event.message) > 1: 99 | del event.message[-1] 100 | else: 101 | last_msg_seg.data["text"] = last_text 102 | 103 | for sf in item.value: 104 | yield sf 105 | 106 | event.message = oldmsg 107 | 108 | 109 | class KeywordTrigger(BaseTrigger): 110 | def __init__(self): 111 | super().__init__() 112 | self.allkw = {} 113 | 114 | def add(self, keyword: str, sf: "ServiceFunc"): 115 | if sf.normalize_text: 116 | keyword = util.normalize_str(keyword) 117 | if keyword in self.allkw: 118 | self.allkw[keyword].append(sf) 119 | hoshino.logger.warning(f"Keyword trigger `{keyword}` added multi handler: `{sf.__name__}`") 120 | else: 121 | self.allkw[keyword] = [sf] 122 | hoshino.logger.debug(f"Succeed to add keyword trigger `{keyword}`") 123 | 124 | def find_handler(self, event: CQEvent) -> Iterable["ServiceFunc"]: 125 | for kw, sfs in self.allkw.items(): 126 | for sf in sfs: 127 | text = event.norm_text if sf.normalize_text else event.plain_text 128 | if kw in text: 129 | yield sf 130 | 131 | 132 | class RexTrigger(BaseTrigger): 133 | def __init__(self): 134 | super().__init__() 135 | self.allrex = defaultdict(list) 136 | 137 | def add(self, rex: re.Pattern, sf: "ServiceFunc"): 138 | self.allrex[rex].append(sf) 139 | hoshino.logger.debug(f"Succeed to add rex trigger `{rex.pattern}`") 140 | 141 | def find_handler(self, event: CQEvent) -> "ServiceFunc": 142 | for rex, sfs in self.allrex.items(): 143 | for sf in sfs: 144 | text = event.norm_text if sf.normalize_text else event.plain_text 145 | match = rex.search(text) 146 | if match: 147 | event["match"] = match 148 | yield sf 149 | 150 | 151 | class _PlainTextExtractor(BaseTrigger): 152 | def find_handler(self, event: CQEvent): 153 | event.plain_text = event.message.extract_plain_text().strip() 154 | return [] 155 | 156 | 157 | class _TextNormalizer(_PlainTextExtractor): 158 | def find_handler(self, event: CQEvent): 159 | super().find_handler(event) 160 | event.norm_text = util.normalize_str(event.plain_text) 161 | return [] 162 | 163 | 164 | prefix = PrefixTrigger() 165 | suffix = SuffixTrigger() 166 | keyword = KeywordTrigger() 167 | rex = RexTrigger() 168 | 169 | chain: List[BaseTrigger] = [ 170 | prefix, 171 | suffix, 172 | _TextNormalizer(), 173 | rex, 174 | keyword, 175 | ] 176 | -------------------------------------------------------------------------------- /hoshino/modules/twitter/stream/follow.py: -------------------------------------------------------------------------------- 1 | # 订阅推主 2 | 3 | import asyncio 4 | import itertools 5 | import json 6 | import os 7 | import re 8 | from collections import defaultdict 9 | from dataclasses import dataclass, field 10 | from typing import Dict, Iterable, Set 11 | 12 | import peony 13 | from peony import PeonyClient 14 | from peony.exceptions import PeonyException 15 | 16 | from hoshino import Service, priv 17 | from hoshino.config import twitter as cfg 18 | from hoshino.typing import MessageSegment as ms 19 | 20 | from . import sv 21 | from .util import format_tweet 22 | 23 | service_collection = [ 24 | Service("twitter-stream-test", enable_on_default=False, manage_priv=priv.SUPERUSER, visible=False), 25 | Service("kc-twitter", help_="艦これ推特转发", enable_on_default=False, bundle="kancolle"), 26 | Service("pcr-twitter", help_="日服Twitter转发", enable_on_default=True, bundle="pcr订阅"), 27 | Service("uma-twitter", help_="ウマ娘推特转发", enable_on_default=False, bundle="umamusume"), 28 | Service("pripri-twitter", help_="番剧《公主代理人》官推转发", enable_on_default=False), 29 | Service("coffee-favorite-twitter", help_="咖啡精选画师推特转发", enable_on_default=False, bundle="artist"), 30 | Service("moe-artist-twitter", help_="萌系画师推特转发", enable_on_default=False, bundle="artist"), 31 | Service("depress-artist-twitter", help_="致郁系画师推特转发", enable_on_default=False, bundle="artist"), 32 | ] 33 | 34 | 35 | class UserIdCache: 36 | _cache_file = os.path.expanduser("~/.hoshino/twitter_uid_cache.json") 37 | 38 | def __init__(self) -> None: 39 | self.cache = {} 40 | if os.path.isfile(self._cache_file): 41 | try: 42 | with open(self._cache_file, "r", encoding="utf8") as f: 43 | self.cache = json.load(f) 44 | except Exception as e: 45 | sv.logger.exception(e) 46 | sv.logger.error(f"{type(e)} occured when loading `~/.hoshino/twitter_uid_cache.json`, using empty cache.") 47 | 48 | def get(self, screen_name): 49 | return self.cache.get(screen_name) 50 | 51 | async def convert(self, client: PeonyClient, screen_names: Iterable[str], cached=True): 52 | 53 | if not cached: 54 | self.cache = {} 55 | 56 | ids = [] 57 | for x in screen_names: 58 | if x not in self.cache: 59 | try: 60 | user = await client.api.users.show.get(screen_name=x) 61 | self.cache[x] = user.id 62 | except PeonyException as e: 63 | sv.logger.error(f"{e} occurred when getting id of `{x}`") 64 | 65 | id_ = self.cache.get(x) 66 | if id_: 67 | ids.append(id_) 68 | 69 | with open(self._cache_file, "w", encoding="utf8") as f: 70 | json.dump(self.cache, f) 71 | 72 | return ids 73 | 74 | 75 | _id_cache = UserIdCache() 76 | 77 | 78 | @dataclass 79 | class FollowEntry: 80 | services: Set[Service] = field(default_factory=set) 81 | profile_image: str = None 82 | media_only: bool = False 83 | forward_retweet: bool = False 84 | 85 | 86 | class TweetRouter: 87 | def __init__(self): 88 | self.follows: Dict[int, FollowEntry] = defaultdict(FollowEntry) 89 | 90 | def add(self, service: Service, follow_names: Iterable[str]): 91 | for name in follow_names: 92 | id_ = _id_cache.get(name) 93 | if id_ is None: 94 | continue 95 | self.follows[id_].services.add(service) 96 | 97 | def set_media_only(self, screen_name, media_only=True): 98 | id_ = _id_cache.get(screen_name) 99 | if id_ in self.follows: 100 | self.follows[id_].media_only = media_only 101 | else: 102 | sv.logger.warning(f"No user named `{screen_name}` or `{screen_name}` not in follows. Ignore media_only set.") 103 | 104 | def set_forward_retweet(self, screen_name, forward_retweet=True): 105 | id_ = _id_cache.get(screen_name) 106 | if id_ in self.follows: 107 | self.follows[id_].forward_retweet = forward_retweet 108 | else: 109 | sv.logger.warning(f"No user named `{screen_name}` or `{screen_name}` not in follows. Ignore forward_retweet set.") 110 | 111 | def load(self, service_follow_dict, media_only_users, forward_retweet_users): 112 | for s in service_collection: 113 | self.add(s, service_follow_dict[s.name]) 114 | for x in media_only_users: 115 | self.set_media_only(x) 116 | for x in forward_retweet_users: 117 | self.set_forward_retweet(x) 118 | 119 | 120 | async def follow_stream(): 121 | client = PeonyClient( 122 | consumer_key=cfg.consumer_key, 123 | consumer_secret=cfg.consumer_secret, 124 | access_token=cfg.access_token_key, 125 | access_token_secret=cfg.access_token_secret, 126 | proxy=cfg.proxy, 127 | ) 128 | async with client: 129 | follow_screen_names = set(itertools.chain(*cfg.follows.values())) 130 | follow_ids = await _id_cache.convert(client, follow_screen_names) 131 | 132 | router = TweetRouter() 133 | router.load(cfg.follows, cfg.media_only_users, cfg.forward_retweet_users) 134 | 135 | sv.logger.info(f"订阅推主={follow_screen_names}, {follow_ids=}") 136 | stream = client.stream.statuses.filter.post(follow=follow_ids) 137 | 138 | async for tweet in stream: 139 | sv.logger.info("Got twitter event.") 140 | if peony.events.tweet(tweet): 141 | uid = tweet.user.id 142 | screen_name = tweet.user.screen_name 143 | if uid not in router.follows: 144 | continue # 推主不在订阅列表 145 | 146 | entry = router.follows[uid] 147 | if peony.events.retweet(tweet) and not entry.forward_retweet: 148 | continue # 除非配置制定,忽略纯转推 149 | 150 | reply_to = tweet.get("in_reply_to_user_id") 151 | if reply_to and reply_to != uid: 152 | continue # 忽略对他人的评论,保留自评论 153 | 154 | media = tweet.get("extended_entities", {}).get("media", []) 155 | if entry.media_only and not media: 156 | continue # 无附带媒体,订阅选项media_only=True时忽略 157 | 158 | msg = format_tweet(tweet) 159 | 160 | old_profile_img = entry.profile_image 161 | entry.profile_image = tweet.user.get("profile_image_url_https") or entry.profile_image 162 | if old_profile_img and entry.profile_image != old_profile_img: 163 | big_img = re.sub(r'_normal(\.(jpg|jpeg|png|gif|jfif|webp))$', r'\1', entry.profile_image, re.I) 164 | msg = [msg, f"@{screen_name} 更换了头像\n{ms.image(big_img)}"] 165 | 166 | sv.logger.info(f"推送推文:\n{msg}") 167 | for s in entry.services: 168 | asyncio.get_event_loop().create_task(s.broadcast(msg, f" @{screen_name} 推文", 0.2)) 169 | 170 | else: 171 | sv.logger.debug("Ignore non-tweet event.") 172 | -------------------------------------------------------------------------------- /hoshino/modules/twitter-v2/stream/follow.py: -------------------------------------------------------------------------------- 1 | # 订阅推主 2 | 3 | import asyncio 4 | from collections import defaultdict 5 | from dataclasses import dataclass, field 6 | from typing import Dict, Iterable, Set 7 | 8 | import peony 9 | 10 | from hoshino import Service, priv 11 | from hoshino.config import twitter as cfg 12 | 13 | from . import sv 14 | from .util import cut_list, format_tweet 15 | 16 | # import logging 17 | # sv.logger.setLevel(logging.DEBUG) 18 | 19 | service_collection = [ 20 | Service("twitter-stream-test", enable_on_default=False, manage_priv=priv.SU, visible=False), 21 | Service("kc-twitter", help_="艦これ推特转发", enable_on_default=False, bundle="kancolle"), 22 | Service("pcr-twitter", help_="日服Twitter转发", enable_on_default=True, bundle="pcr订阅"), 23 | # Service("uma-twitter", help_="ウマ娘推特转发", enable_on_default=False, bundle="umamusume"), 24 | Service("ba-twitter", help_="蔚蓝档案日服推特转发", enable_on_default=False, bundle="ba"), 25 | Service("sr-twitter", help_="崩坏星穹铁道日服推特转发", enable_on_default=False, bundle="mihoyo"), 26 | Service("zzz-twitter", help_="绝区零日服推特转发", enable_on_default=False, bundle="mihoyo"), 27 | # Service("pripri-twitter", help_="番剧《公主代理人》官推转发", enable_on_default=False), 28 | # Service("coffee-favorite-twitter", help_="咖啡精选画师推特转发", enable_on_default=False, bundle="artist"), 29 | Service("moe-artist-twitter", help_="萌系画师推特转发", enable_on_default=False, bundle="artist"), 30 | # Service("depress-artist-twitter", help_="致郁系画师推特转发", enable_on_default=False, bundle="artist"), 31 | ] 32 | 33 | 34 | @dataclass 35 | class FollowEntry: 36 | services: Set[Service] = field(default_factory=set) 37 | profile_image: str = None 38 | media_only: bool = False 39 | forward_retweet: bool = False 40 | 41 | 42 | class TweetRouter: 43 | def __init__(self): 44 | self.follows: Dict[str, FollowEntry] = defaultdict(FollowEntry) 45 | 46 | def add(self, service: Service, follow_names: Iterable[str]): 47 | for name in follow_names: 48 | self.follows[name].services.add(service) 49 | 50 | def set_media_only(self, screen_name, media_only=True): 51 | if screen_name in self.follows: 52 | self.follows[screen_name].media_only = media_only 53 | else: 54 | sv.logger.warning(f"No user named `{screen_name}` or `{screen_name}` not in follows. Ignore media_only set.") 55 | 56 | def set_forward_retweet(self, screen_name, forward_retweet=True): 57 | if screen_name in self.follows: 58 | self.follows[screen_name].forward_retweet = forward_retweet 59 | else: 60 | sv.logger.warning(f"No user named `{screen_name}` or `{screen_name}` not in follows. Ignore forward_retweet set.") 61 | 62 | def load(self, service_follow_dict, media_only_users, forward_retweet_users): 63 | for s in service_collection: 64 | self.add(s, service_follow_dict[s.name]) 65 | for x in media_only_users: 66 | self.set_media_only(x) 67 | for x in forward_retweet_users: 68 | self.set_forward_retweet(x) 69 | 70 | 71 | async def follow_stream(): 72 | follow_names = set() 73 | for s in service_collection: 74 | follow_names.update(cfg.follows.get(s.name, [])) 75 | 76 | client = peony.BasePeonyClient( 77 | consumer_key=cfg.consumer_key, 78 | consumer_secret=cfg.consumer_secret, 79 | access_token=cfg.access_token_key, 80 | access_token_secret=cfg.access_token_secret, 81 | auth=peony.oauth.OAuth2Headers, 82 | api_version="2", 83 | proxy=cfg.proxy, 84 | suffix="", 85 | ) 86 | async with client: 87 | router = TweetRouter() 88 | router.load(cfg.follows, cfg.media_only_users, cfg.forward_retweet_users) 89 | 90 | sv.logger.info(f"订阅推主{len(follow_names)}个帐号: {follow_names}") 91 | 92 | # 删除上一次的规则 93 | resp = await client.api.tweets.search.stream.rules.get() # 先获取上一次的规则 94 | data = resp.get("data") 95 | if resp.get("data"): 96 | ids = list(map(lambda rule: rule["id"], data)) 97 | payload = {"delete": {"ids": ids}} 98 | resp = await client.api.tweets.search.stream.rules.post(_json=payload) 99 | 100 | # 写入新规则 101 | follow_name_chunks = cut_list(follow_names) # 处理数据,准备将config里follow所有人写入规则 102 | rules = [] # 规则最多10个,所以要分批写入 103 | for chunk in follow_name_chunks: 104 | rule = {"value": f'from:{" OR from:".join(chunk)}'} 105 | rules.append(rule) 106 | data = {"add": rules} 107 | resp = await client.api.tweets.search.stream.rules.post(_json=data) 108 | sv.logger.debug(resp) # 如果stream流启动失败请查看规则是否已经写入 109 | fields = { 110 | "tweet.fields": [ 111 | "created_at", 112 | "entities", 113 | "referenced_tweets", 114 | "text", 115 | "author_id", 116 | "in_reply_to_user_id", 117 | "attachments", 118 | ], 119 | "expansions": [ 120 | "author_id", 121 | "attachments.media_keys", # 不要删media_keys,否则下行不生效 122 | ], 123 | "media.fields": ["url", "preview_image_url"], 124 | } 125 | 126 | stream = client.api.tweets.search.stream.get.stream(**fields) # 启动stream流 127 | async for tweet in stream: # 当stream流收到信息: 128 | sv.logger.info("Got twitter event.") 129 | 130 | if tweet.get("data"): 131 | sv.logger.debug(tweet) 132 | username = tweet.get("includes")["users"][0]["username"] 133 | entry = router.follows[username] 134 | sv.logger.debug(entry) 135 | 136 | if username not in router.follows: # 推主不在订阅列表 137 | sv.logger.debug(f"推主{username}不在订阅列表") 138 | continue 139 | 140 | if "referenced_tweets" in tweet.get("data"): 141 | if ("retweeted" in tweet.get("data").referenced_tweets[0].type and not entry.forward_retweet): 142 | sv.logger.debug("纯转推,已忽略") 143 | continue # 除非配置制定,忽略纯转推 144 | 145 | if "in_reply_to_user_id" in tweet.get("data"): 146 | if tweet.get("data").in_reply_to_user_id != username: 147 | sv.logger.debug("回复他人,已忽略") 148 | continue # 忽略对他人的评论,保留自评论 149 | 150 | if "media" not in tweet.get("includes") and entry.media_only: 151 | sv.logger.debug("无附带媒体,订阅选项media_only=True,已忽略") 152 | continue # 无附带媒体,订阅选项media_only=True时忽略 153 | 154 | msg = await format_tweet(tweet, client) 155 | sv.logger.info(f"推送推文:\n{msg}") 156 | for s in entry.services: 157 | asyncio.get_event_loop().create_task(s.broadcast(msg, f" @{username} 推文", 0.2)) 158 | 159 | else: 160 | sv.logger.debug("Ignore non-tweet event.") 161 | -------------------------------------------------------------------------------- /hoshino/util/__init__.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | import random 4 | import time 5 | import unicodedata 6 | from collections import defaultdict 7 | from datetime import datetime, timedelta 8 | from io import BytesIO 9 | 10 | import pytz 11 | import zhconv 12 | from aiocqhttp.exceptions import ActionFailed 13 | from aiocqhttp.message import escape 14 | from matplotlib import pyplot as plt 15 | from PIL import Image 16 | 17 | import hoshino 18 | from hoshino.typing import CQEvent, Message, Union 19 | 20 | try: 21 | import ujson as json 22 | except: 23 | import json 24 | 25 | 26 | 27 | 28 | def load_config(inbuilt_file_var): 29 | """ 30 | Just use `config = load_config(__file__)`, 31 | you can get the config.json as a dict. 32 | """ 33 | filename = os.path.join(os.path.dirname(inbuilt_file_var), 'config.json') 34 | try: 35 | with open(filename, encoding='utf8') as f: 36 | config = json.load(f) 37 | return config 38 | except Exception as e: 39 | hoshino.logger.exception(e) 40 | return {} 41 | 42 | 43 | async def delete_msg(ev: CQEvent): 44 | try: 45 | await hoshino.get_bot().delete_msg(self_id=ev.self_id, message_id=ev.message_id) 46 | except ActionFailed as e: 47 | hoshino.logger.error(f'撤回失败: {e}') 48 | except Exception as e: 49 | hoshino.logger.exception(e) 50 | 51 | 52 | async def silence(ev: CQEvent, ban_time, skip_su=True): 53 | try: 54 | if skip_su and ev.user_id in hoshino.config.SUPERUSERS: 55 | return 56 | await hoshino.get_bot().set_group_ban(self_id=ev.self_id, group_id=ev.group_id, user_id=ev.user_id, duration=ban_time) 57 | except ActionFailed as e: 58 | if 'NOT_MANAGEABLE' in str(e): 59 | return 60 | else: 61 | hoshino.logger.error(f'禁言失败 {e}') 62 | except Exception as e: 63 | hoshino.logger.exception(e) 64 | 65 | 66 | def pic2b64(pic: Image) -> str: 67 | buf = BytesIO() 68 | pic.save(buf, format='PNG') 69 | base64_str = base64.b64encode(buf.getvalue()).decode() 70 | return 'base64://' + base64_str 71 | 72 | 73 | def fig2b64(plt: plt) -> str: 74 | buf = BytesIO() 75 | plt.savefig(buf, format='PNG', dpi=100) 76 | base64_str = base64.b64encode(buf.getvalue()).decode() 77 | return 'base64://' + base64_str 78 | 79 | 80 | def concat_pic(pics, border=5): 81 | num = len(pics) 82 | w, h = pics[0].size 83 | des = Image.new('RGBA', (w, num * h + (num-1) * border), (255, 255, 255, 255)) 84 | for i, pic in enumerate(pics): 85 | des.paste(pic, (0, i * (h + border)), pic) 86 | return des 87 | 88 | 89 | def normalize_str(string) -> str: 90 | """ 91 | 规范化unicode字符串 并 转为小写 并 转为简体 92 | """ 93 | string = unicodedata.normalize('NFKC', string) 94 | string = string.lower() 95 | string = zhconv.convert(string, 'zh-hans') 96 | return string 97 | 98 | 99 | MONTH_NAME = ('睦月', '如月', '弥生', '卯月', '皐月', '水無月', 100 | '文月', '葉月', '長月', '神無月', '霜月', '師走') 101 | def month_name(x:int) -> str: 102 | return MONTH_NAME[x - 1] 103 | 104 | DATE_NAME = ( 105 | '初一', '初二', '初三', '初四', '初五', '初六', '初七', '初八', '初九', '初十', 106 | '十一', '十二', '十三', '十四', '十五', '十六', '十七', '十八', '十九', '二十', 107 | '廿一', '廿二', '廿三', '廿四', '廿五', '廿六', '廿七', '廿八', '廿九', '三十', 108 | '卅一' 109 | ) 110 | def date_name(x: int) -> str: 111 | return DATE_NAME[x - 1] 112 | 113 | NUM_NAME = ( 114 | '〇〇', '〇一', '〇二', '〇三', '〇四', '〇五', '〇六', '〇七', '〇八', '〇九', 115 | '一〇', '一一', '一二', '一三', '一四', '一五', '一六', '一七', '一八', '一九', 116 | '二〇', '二一', '二二', '二三', '二四', '二五', '二六', '二七', '二八', '二九', 117 | '三〇', '三一', '三二', '三三', '三四', '三五', '三六', '三七', '三八', '三九', 118 | '四〇', '四一', '四二', '四三', '四四', '四五', '四六', '四七', '四八', '四九', 119 | '五〇', '五一', '五二', '五三', '五四', '五五', '五六', '五七', '五八', '五九', 120 | '六〇', '六一', '六二', '六三', '六四', '六五', '六六', '六七', '六八', '六九', 121 | '七〇', '七一', '七二', '七三', '七四', '七五', '七六', '七七', '七八', '七九', 122 | '八〇', '八一', '八二', '八三', '八四', '八五', '八六', '八七', '八八', '八九', 123 | '九〇', '九一', '九二', '九三', '九四', '九五', '九六', '九七', '九八', '九九', 124 | ) 125 | def time_name(hh: int, mm: int) -> str: 126 | return NUM_NAME[hh] + NUM_NAME[mm] 127 | 128 | 129 | class FreqLimiter: 130 | def __init__(self, default_cd_seconds): 131 | self.next_time = defaultdict(float) 132 | self.default_cd = default_cd_seconds 133 | 134 | def check(self, key) -> bool: 135 | return bool(time.time() >= self.next_time[key]) 136 | 137 | def start_cd(self, key, cd_time=0): 138 | self.next_time[key] = time.time() + (cd_time if cd_time > 0 else self.default_cd) 139 | 140 | def left_time(self, key) -> float: 141 | return self.next_time[key] - time.time() 142 | 143 | 144 | class DailyNumberLimiter: 145 | tz = pytz.timezone('Asia/Shanghai') 146 | 147 | def __init__(self, max_num): 148 | self.today = -1 149 | self.count = defaultdict(int) 150 | self.max = max_num 151 | 152 | def check(self, key) -> bool: 153 | now = datetime.now(self.tz) 154 | day = (now - timedelta(hours=5)).day 155 | if day != self.today: 156 | self.today = day 157 | self.count.clear() 158 | return bool(self.count[key] < self.max) 159 | 160 | def get_num(self, key): 161 | return self.count[key] 162 | 163 | def increase(self, key, num=1): 164 | self.count[key] += num 165 | 166 | def reset(self, key): 167 | self.count[key] = 0 168 | 169 | 170 | from .textfilter.filter import DFAFilter 171 | 172 | gfw = DFAFilter() 173 | gfw.parse(os.path.join(os.path.dirname(__file__), 'textfilter/sensitive_words.txt')) 174 | 175 | 176 | def filt_message(message: Union[Message, str]): 177 | if isinstance(message, str): 178 | return gfw.filter(message) 179 | elif isinstance(message, Message): 180 | for seg in message: 181 | if seg.type == 'text': 182 | seg.data['text'] = gfw.filter(seg.data.get('text', '')) 183 | return message 184 | else: 185 | raise TypeError 186 | 187 | 188 | 189 | def render_list(lines, prompt="") -> str: 190 | n = len(lines) 191 | if n == 0: 192 | return prompt 193 | if n == 1: 194 | return prompt + "\n┗" + lines[0] 195 | return prompt + "\n┣" + "\n┣".join(lines[:-1]) + "\n┗" + lines[-1] 196 | 197 | 198 | DEVICES = [ 199 | '22号对水上电探改四(后期调整型)', 200 | '42号对空电探改二', 201 | '15m二重测距仪改+21号电探改二+熟练射击指挥所', 202 | 'FuMO25 雷达', 203 | 'SK+SG 雷达', 204 | 'SG 雷达(后期型)', 205 | 'GFCS Mk.37', 206 | '潜水舰搭载电探&逆探(E27)', 207 | 'HF/DF+Type144/147 ASDIC', 208 | '三式指挥联络机(对潜)', 209 | 'O号观测机改二', 210 | 'S-51J改', 211 | '二式陆上侦察机(熟练)', 212 | '东海(九〇一空)', 213 | '二式大艇', 214 | 'PBY-5A Catalina', 215 | '零式水上侦察机11型乙(熟练)', 216 | '零式水上侦察机11型乙改(夜侦)', 217 | '紫云', 218 | 'Ar196改', 219 | 'Ro.43水侦', 220 | 'OS2U', 221 | 'S9 Osprey', 222 | '彩云(东加罗林空)', 223 | '彩云(侦四)', 224 | '试制景云(舰侦型)', 225 | ] 226 | 227 | def randomizer(target): 228 | return lambda m: f'{random.choice(DEVICES)}监测到{target}!{"!"*random.randint(0,4)}\n{m}' 229 | -------------------------------------------------------------------------------- /hoshino/modules/priconne/gacha/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | from collections import defaultdict 4 | 5 | from hoshino import Service, priv, util 6 | from hoshino.typing import * 7 | from hoshino.util import DailyNumberLimiter, concat_pic, pic2b64, silence 8 | 9 | from .. import chara 10 | from .gacha import Gacha 11 | 12 | try: 13 | import ujson as json 14 | except: 15 | import json 16 | 17 | 18 | sv_help = ''' 19 | [星乃来发十连] 转蛋模拟 20 | [星乃来发单抽] 转蛋模拟 21 | [星乃来一井] 4w5钻! 22 | [查看卡池] 模拟卡池&出率 23 | [切换卡池] 更换模拟卡池 24 | '''.strip() 25 | sv = Service('gacha', help_=sv_help, bundle='pcr娱乐') 26 | jewel_limit = DailyNumberLimiter(6000) 27 | tenjo_limit = DailyNumberLimiter(1) 28 | 29 | JEWEL_EXCEED_NOTICE = f'您今天已经抽过{jewel_limit.max}钻了,欢迎明早5点后再来!' 30 | TENJO_EXCEED_NOTICE = f'您今天已经抽过{tenjo_limit.max}张天井券了,欢迎明早5点后再来!' 31 | POOL = ('MIX', 'JP', 'TW', 'BL') 32 | DEFAULT_POOL = POOL[0] 33 | 34 | _pool_config_file = os.path.expanduser('~/.hoshino/group_pool_config.json') 35 | _group_pool = {} 36 | try: 37 | with open(_pool_config_file, encoding='utf8') as f: 38 | _group_pool = json.load(f) 39 | except FileNotFoundError as e: 40 | sv.logger.warning('group_pool_config.json not found, will create when needed.') 41 | _group_pool = defaultdict(lambda: DEFAULT_POOL, _group_pool) 42 | 43 | def dump_pool_config(): 44 | with open(_pool_config_file, 'w', encoding='utf8') as f: 45 | json.dump(_group_pool, f, ensure_ascii=False) 46 | 47 | 48 | gacha_10_aliases = ('抽十连', '十连', '十连!', '十连抽', '来个十连', '来发十连', '来次十连', '抽个十连', '抽发十连', '抽次十连', '十连扭蛋', '扭蛋十连', 49 | '10连', '10连!', '10连抽', '来个10连', '来发10连', '来次10连', '抽个10连', '抽发10连', '抽次10连', '10连扭蛋', '扭蛋10连') 50 | gacha_1_aliases = ('单抽', '单抽!', '来发单抽', '来个单抽', '来次单抽', '扭蛋单抽', '单抽扭蛋') 51 | gacha_tenjou_aliases = ('抽一井', '来一井', '来发井', '抽发井', '天井扭蛋', '扭蛋天井') 52 | 53 | @sv.on_fullmatch('卡池资讯', '查看卡池', '看看卡池', '康康卡池', '看看up', '看看UP') 54 | async def gacha_info(bot, ev: CQEvent): 55 | gid = str(ev.group_id) 56 | gacha = Gacha(_group_pool[gid]) 57 | up_chara = [] 58 | for x in gacha.up: 59 | icon = await chara.fromname(x, star=3).get_icon() 60 | up_chara.append(str(icon.cqcode) + x) 61 | up_chara = '\n'.join(up_chara) 62 | await bot.send(ev, f"本期卡池主打的角色:\n{up_chara}\nUP角色合计={(gacha.up_prob/10):.1f}% 3★出率={(gacha.s3_prob)/10:.1f}%") 63 | 64 | 65 | POOL_NAME_TIP = '请选择以下卡池\n> 切换卡池jp\n> 切换卡池tw\n> 切换卡池b\n> 切换卡池mix' 66 | @sv.on_prefix('切换卡池', '选择卡池') 67 | async def set_pool(bot, ev: CQEvent): 68 | if not priv.check_priv(ev, priv.ADMIN): 69 | await bot.finish(ev, '只有群管理才能切换卡池', at_sender=True) 70 | name = util.normalize_str(ev.message.extract_plain_text()) 71 | if not name: 72 | await bot.finish(ev, POOL_NAME_TIP, at_sender=True) 73 | elif name in ('国', '国服', 'cn'): 74 | await bot.finish(ev, '请选择以下卡池\n> 选择卡池 b服\n> 选择卡池 台服') 75 | elif name in ('b', 'b服', 'bl', 'bilibili'): 76 | name = 'BL' 77 | elif name in ('台', '台服', 'tw', 'sonet'): 78 | name = 'TW' 79 | elif name in ('日', '日服', 'jp', 'cy', 'cygames'): 80 | name = 'JP' 81 | elif name in ('混', '混合', 'mix'): 82 | name = 'MIX' 83 | else: 84 | await bot.finish(ev, f'未知服务器地区 {POOL_NAME_TIP}', at_sender=True) 85 | gid = str(ev.group_id) 86 | _group_pool[gid] = name 87 | dump_pool_config() 88 | await bot.send(ev, f'卡池已切换为{name}池', at_sender=True) 89 | await gacha_info(bot, ev) 90 | 91 | 92 | async def check_jewel_num(bot, ev: CQEvent): 93 | if not jewel_limit.check(ev.user_id): 94 | await bot.finish(ev, JEWEL_EXCEED_NOTICE, at_sender=True) 95 | 96 | 97 | async def check_tenjou_num(bot, ev: CQEvent): 98 | if not tenjo_limit.check(ev.user_id): 99 | await bot.finish(ev, TENJO_EXCEED_NOTICE, at_sender=True) 100 | 101 | 102 | @sv.on_prefix(gacha_1_aliases, only_to_me=True) 103 | async def gacha_1(bot, ev: CQEvent): 104 | 105 | await check_jewel_num(bot, ev) 106 | jewel_limit.increase(ev.user_id, 150) 107 | 108 | gid = str(ev.group_id) 109 | gacha = Gacha(_group_pool[gid]) 110 | chara, hiishi = gacha.gacha_one(gacha.up_prob, gacha.s3_prob, gacha.s2_prob) 111 | silence_time = hiishi * 60 112 | 113 | res = f'{await chara.get_icon_cqcode()} {chara.name} {"★"*chara.star}' 114 | 115 | await silence(ev, silence_time) 116 | await bot.send(ev, f'素敵な仲間が増えますよ!\n{res}', at_sender=True) 117 | 118 | 119 | @sv.on_prefix(gacha_10_aliases, only_to_me=True) 120 | async def gacha_10(bot, ev: CQEvent): 121 | SUPER_LUCKY_LINE = 170 122 | 123 | await check_jewel_num(bot, ev) 124 | jewel_limit.increase(ev.user_id, 1500) 125 | 126 | gid = str(ev.group_id) 127 | gacha = Gacha(_group_pool[gid]) 128 | result, hiishi = gacha.gacha_ten() 129 | silence_time = hiishi * 6 if hiishi < SUPER_LUCKY_LINE else hiishi * 60 130 | 131 | res1 = await chara.gen_team_pic(result[:5], star_slot_verbose=False) 132 | res2 = await chara.gen_team_pic(result[5:], star_slot_verbose=False) 133 | res = concat_pic([res1, res2]) 134 | res = pic2b64(res) 135 | res = MessageSegment.image(res) 136 | result = [f'{c.name}{"★"*c.star}' for c in result] 137 | res1 = ' '.join(result[0:5]) 138 | res2 = ' '.join(result[5:]) 139 | res = f'{res}\n{res1}\n{res2}' 140 | # 纯文字版 141 | # result = [f'{c.name}{"★"*c.star}' for c in result] 142 | # res1 = ' '.join(result[0:5]) 143 | # res2 = ' '.join(result[5:]) 144 | # res = f'{res1}\n{res2}' 145 | 146 | if hiishi >= SUPER_LUCKY_LINE: 147 | await bot.send(ev, '恭喜海豹!おめでとうございます!') 148 | await bot.send(ev, f'素敵な仲間が増えますよ!\n{res}\n', at_sender=True) 149 | await silence(ev, silence_time) 150 | 151 | 152 | @sv.on_prefix(gacha_tenjou_aliases, only_to_me=True) 153 | async def gacha_tenjou(bot, ev: CQEvent): 154 | 155 | await check_tenjou_num(bot, ev) 156 | tenjo_limit.increase(ev.user_id) 157 | 158 | gid = str(ev.group_id) 159 | gacha = Gacha(_group_pool[gid]) 160 | result = gacha.gacha_tenjou() 161 | up = len(result['up']) 162 | s3 = len(result['s3']) 163 | s2 = len(result['s2']) 164 | s1 = len(result['s1']) 165 | 166 | res = [*(result['up']), *(result['s3'])] 167 | random.shuffle(res) 168 | lenth = len(res) 169 | if lenth <= 0: 170 | res = "竟...竟然没有3★?!" 171 | else: 172 | step = 4 173 | pics = [] 174 | for i in range(0, lenth, step): 175 | j = min(lenth, i + step) 176 | pics.append(await chara.gen_team_pic(res[i:j], star_slot_verbose=False)) 177 | res = concat_pic(pics) 178 | res = pic2b64(res) 179 | res = MessageSegment.image(res) 180 | 181 | msg = [ 182 | f"\n素敵な仲間が増えますよ! {res}", 183 | f"★★★×{up+s3} ★★×{s2} ★×{s1}", 184 | f"获得记忆碎片×{gacha.memo_pieces*up}与女神秘石×{50*(up+s3) + 10*s2 + s1}!\n第{result['first_up_pos']}抽首次获得up角色" if up else f"获得女神秘石{50*(up+s3) + 10*s2 + s1}个!" 185 | ] 186 | 187 | if up == 0 and s3 == 0: 188 | msg.append("太惨了,咱们还是退款删游吧...") 189 | elif up == 0 and s3 > 7: 190 | msg.append("up呢?我的up呢?") 191 | elif up == 0 and s3 <= 3: 192 | msg.append("这位酋长,梦幻包考虑一下?") 193 | elif up == 0: 194 | msg.append("据说天井的概率只有" + gacha.tenjou_rate) 195 | elif up <= 2: 196 | if result['first_up_pos'] < 50: 197 | msg.append("你的喜悦我收到了,滚去喂鲨鱼吧!") 198 | elif result['first_up_pos'] < 100: 199 | msg.append("已经可以了,您已经很欧了") 200 | elif result['first_up_pos'] > gacha.tenjou_line - 10: 201 | msg.append("标 准 结 局") 202 | elif result['first_up_pos'] > gacha.tenjou_line - 50: 203 | msg.append("补井还是不补井,这是一个问题...") 204 | else: 205 | msg.append("期望之内,亚洲水平") 206 | elif up == 3: 207 | msg.append("抽井母五一气呵成!多出30等专武~") 208 | elif up >= 4: 209 | msg.append("记忆碎片一大堆!您是托吧?") 210 | 211 | await bot.send(ev, '\n'.join(msg), at_sender=True) 212 | silence_time = (100*up + 50*(up+s3) + 10*s2 + s1) * 1 213 | await silence(ev, silence_time) 214 | 215 | 216 | @sv.on_prefix('氪金') 217 | async def kakin(bot, ev: CQEvent): 218 | if ev.user_id not in bot.config.SUPERUSERS: 219 | return 220 | count = 0 221 | for m in ev.message: 222 | if m.type == 'at' and m.data['qq'] != 'all': 223 | uid = int(m.data['qq']) 224 | jewel_limit.reset(uid) 225 | tenjo_limit.reset(uid) 226 | count += 1 227 | if count: 228 | await bot.send(ev, f"已为{count}位用户充值完毕!谢谢惠顾~") 229 | -------------------------------------------------------------------------------- /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) --------------------------------------------------------------------------------