diff --git a/README.md b/README.md index 88b05b0..4b9059f 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,13 @@ # plugins-for-Hoshino -Hoshino插件合集on-mirai +Hoshino插件合集 ## 环境 -mirai + cqhttp-mirai -cqhttp-mirai项目地址 -https://github.com/yyuueexxiinngg/cqhttp-mirai.git +go-cqhttp +go-cqhttp项目地址 +https://github.com/Mrs4s/go-cqhttp.git ## 使用方法 -1. util4sh.py放在hoshino目录下,shebot的插件使用的一些函数及类均在此文件 -2. shebot文件夹放在modules下并在config里启用该模组 -3. 安装requirements.txt里的依赖 +1. shebot文件夹放在modules下并在config里启用该模组 +2. 安装requirements.txt里的依赖 ## 插件列表 -### QA(你问我答) -#### 指令 -|指令|说明| -|-----|-----| -|我问xxx你答xxx|| -|有人问xxx你答xxx|| -|删除问答<问>|| -|查看问答<页数>|| -|查看全部问答<页数>|SUPERUSER| - -ps: -答句可以用 | 分隔,将随机挑选一个回复 -答句可以用 + 分隔,将分多条消息发送 -支持以下变量: -【艾特全体】、【艾特当前】、【随机图片<文件夹>】 -文件夹需要创建在res/image下 -### reply(自定义回复) -#### 指令(SUPERUSER使用) -|指令|说明| -|-----|-----| -|fullmatch<触发词>#<回复>|完全匹配| -|keyword<触发词>#<回复>|关键词匹配| -|rex<触发词>#<回复>|正则匹配| -|删除fullmatch\|keyword\|rex<触发词>|| - -### scheduledMessage(定时发送消息) -#### 指令(OWNER以上权限使用) -|指令|说明| -|-----|-----| -|闹钟|根据bot发送的消息进行设置| -|查看计划任务|| -|删除计划任务<任务id>|id通过查看计划任务获得| - -ps: -1. 多个时间逗号隔开,逗号使用英文逗号,且之间不要空格 -2. 使用命令添加的仅当前群生效,如果要设置多个群,直接将jobs.json里groups字段设置为'default' -3. 同样支持以下变量:【艾特全体】、【艾特当前】、【随机图片<文件夹>】 - -### scheduledCommand(定时执行命令) -#### 指令(ADMIN以上权限使用) -|指令|说明| -|-----|-----| -|add_delay_job|根据bot发送的消息进行设置| -|add_cron_job|根据bot发送的消息进行设置| -|show_cron_job|查看定时执行命令| -|cancel_cron_job|删除定时命令| - -ps: -1. 多个时间逗号隔开,逗号使用英文逗号,且之间不要空格 ### liveNotice(直播提醒) #### 指令 @@ -66,34 +16,34 @@ ps: | live | 根据提示订阅一个直播间,目前只支持哔哩哔哩和斗鱼直播 | ps: -1. 使用的是我个人的rss源,如果要换成自己的,在util4sh.py里的class RSS中更换 +1. 使用的是我个人的rss源,如果要换成自己的,在_util里的class RSS中更换 2. 目前只支持斗鱼和哔哩哔哩,别问为什么就这两个,因为我只看这两个 -### biliVideo(B站up投稿提醒) +### infoPush(rss消息推送) #### 指令 -| 指令 | 说明 | -| ---------------------- | -------------------------------------------------------- | -| video | 根据提示订阅B站up的投稿提醒 | | +在__init__.py里按照注释添加service和路由,所有路由请见https://docs.rsshub.app/。 并非所有类型的路由都经过测试,bilibili动态,pcr官网,github issue这些经测试都可以正常使用。 ps: 同直播提醒 -### infoPush(偏通用型的消息推送) +### webServiceManager(Hoshino服务开关网页控制) #### 指令 -在__init__.py里按照注释添加service和路由,所有路由请见https://docs.rsshub.app/。 并非所有类型的路由都经过测试,bilibili动态,pcr官网,github issue这些经测试都可以正常使用。 +| 指令 | 说明 | +| ---------------------- | -------------------------------------------------------- | +| 服务管理/bot设置(私聊bot) | bot将发送服务管理的链接 | +本插件为hoshinoV2的服务管理插件,提供web控制各个群的各项服务启(禁)用,并支持一键启(禁)用某群所有服务,或者一键启(禁)用所有群某服务 -ps: -同直播提醒 +安装使用: +修改view.py里公网ip和密码 +私聊bot发送bot设置 -### 切噜语触发命令 -这个不是插件,算是魔改hoshino,修改后hoshino的命令可以使用切噜语触发。不过考虑到hoshino精妙的代码,不希望改动过多,所以选择添加一个装饰器实现。使用方法:在hoshino下msghandler.py添加两行代码 -1. from hoshino.util4sh import cherugo_also,这行代码写在@message_preprocessor上方任意位置 -2. @cherugo_also ,这行代码插入@message_preprocessor下方一行。 -因为修改了hoshino消息处理(理论上应该对正常使用没影响,)如果不想使用或者出现不可预料的bug将@cherugo_also装饰器注释即可。 -## 其他说明 -不定期更新 +注意事项: +hoshino config文件夹__bot__.py中的HOST请设置为 0.0.0.0以开放公网访问 +请注意服务器放行端口 +请设置好密码,开放公网访问带来的任何安全问题与本人无关 ### setu(涩图) 看里面的readme.txt,写累了 + ## 其他说明 不定期更新 diff --git a/requirements.txt b/requirements.txt index b116d25..0a96e30 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -filetype==1.0.7 \ No newline at end of file +filetype \ No newline at end of file diff --git a/shebot/QA/__init__.py b/shebot/QA/__init__.py deleted file mode 100644 index 033ed2d..0000000 --- a/shebot/QA/__init__.py +++ /dev/null @@ -1,147 +0,0 @@ -from .data_source import Record, get_user_quota, send_msg, show -from hoshino.service import Service -from hoshino.util4sh import Res as R -from aiocqhttp.event import Event -from hoshino.priv import get_user_priv -from hoshino.priv import * - -RECORDS = Record().get_records() - -sv = Service('我问你答') -@sv.on_rex(r'我问(.+?)你答.{0,1000}') -async def add_reply_for_self(bot, event: Event): - uid = event['user_id'] - gid = event.get('group_id') - user_priv = get_user_priv(event) - user_quata = get_user_quota(user_priv) - - # mirai要下载图片 - await R.save_image(event) - - qu = event.raw_message.split('你答')[0].strip('我问').strip() - ans = event.raw_message.split('你答')[1].strip() - rec = Record(qu,ans,uid,gid,0) - - if rec.count_user_records(uid) >= user_quata: - await bot.send(event, '您的额度不足,请删除记录后再来', at_sender=True) - return - - if rec.insert_database(): - await bot.send(event,'问答添加成功',at_sender=True) - if rec.get_records: - global RECORDS - RECORDS = rec.get_records() - else: - await bot.send(event,'问答添加失败,问答不规范或者冲突',at_sender=True) - -@sv.on_rex(r'有人问(.{1,1000})你答(.{0,1000})') -async def add_reply_for_group(bot, event): - uid = event['user_id'] - gid = event.get('group_id') - user_priv = get_user_priv(event) - user_quata = get_user_quota(user_priv) - - await R.save_image(event) - - qu = event.raw_message.split('你答')[0].strip('有人问').strip() - ans = event.raw_message.split('你答')[1].strip() - rec = Record(qu, ans, uid, gid, 1) - - if rec.count_user_records(uid) >=user_quata: - await bot.send(event,'您的额度不足,请删除记录后再来',at_sender=True) - return - - if rec.insert_database(): - await bot.send(event,'问答添加成功',at_sender=True) - if rec.get_records: - global RECORDS - RECORDS = rec.get_records() - else: - await bot.send(event,'问答添加失败,问答不规范或者冲突',at_sender=True) - - -#应该是不能正常删除图片触发,但是不想改了 -@sv.on_prefix('删除问答') -async def delete_reply(bot, event: Event): - global RECORDS - gid = event.group_id - uid = event.user_id - qu = event.raw_message.strip('删除问答') - rec = Record() - - if check_priv(event, SUPERUSER): - if rec.delete_force(qu): - RECORDS = rec.get_records() - await bot.send(event,'问答删除成功') - else: - await bot.send(event, f'删除失败,可能该问答不存在') - return - - if (qu,gid) not in RECORDS : - await bot.send(event,'本群未找到该问答',at_sender=True) - return - - if not check_priv(event, ADMIN) and uid != RECORDS[qu,gid]['rec_maker']: - await bot.send(event,'只有管理员以上权限才能删除别人的问答',at_sender=True) - return - - if rec.delete(qu,gid): - RECORDS = rec.get_records() - await bot.send(event,'问答删除成功') - else: - await bot.send(event,'神秘bug,删除失败',at_sender=True) - - -@sv.on_message() -async def send(bot,event: Event): - qu = event.raw_message - uid = event.user_id - gid = event.group_id - if (qu,gid) in RECORDS: - rec = RECORDS[qu,gid] - if rec['is_open'] == 0 and rec['rec_maker'] != uid: - #人不对 - return - reply = rec['answer'] - await send_msg(event,reply,at_sender=False) - -@sv.on_prefix('查看问答') -async def show_group_reply(bot, event: Event): - gid = event.get('group_id') - try: - page = int(event.raw_message.strip('查看问答')) - except: - page = 1 - records = {(m, n) : v for (m, n), v in RECORDS.items() if gid == n} - reply, pages = show(records,page) - await bot.send(event,reply.strip()+f'\n\n当前第{page}页,共{pages}页',at_sender=False) - -@sv.on_prefix('查看所有问答') -async def show_all_reply(bot, event: Event): - if not check_priv(event, SUPERUSER): - await bot.send(event,'只有主人可以使用',at_sender=True) - return - try: - page = int(event.raw_message.strip('查看所有问答')) - except: - page = 1 - reply,pages = show(RECORDS,page) - await bot.send(event,reply.strip()+f'\n\n当前第{page}页,共{pages}页',at_sender=False) - -@sv.on_prefix('查看我问') -async def show_group_reply(bot, event: Event): - gid = event.get('group_id') - uid = event.user_id - try: - page = int(event.raw_message.strip('查看我问')) - except: - page = 1 - records = {(m, n) : v for (m, n), v in RECORDS.items() if gid == n and uid == RECORDS[(m, n)]['rec_maker']} - reply, pages = show(records,page) - await bot.send(event,reply.strip()+f'\n\n当前第{page}页,共{pages}页',at_sender=False) - - - - - - diff --git a/shebot/QA/data_source.py b/shebot/QA/data_source.py deleted file mode 100644 index e4d7a2d..0000000 --- a/shebot/QA/data_source.py +++ /dev/null @@ -1,119 +0,0 @@ -import sqlite3 -import os -import traceback -from typing import AsyncIterator - -from aiocqhttp import event -from hoshino.priv import * -from nonebot.log import logger - -class Record: - def __init__(self,qu=None,ans=None,rec_maker=None,group_id=None,is_open=None): - with sqlite3.connect(os.path.dirname(__file__)+'/records.db3') as conn: - cur = conn.cursor() - self.conn = conn - self.cur = cur - self.question = qu - self.answer = ans - self.rec_maker = rec_maker - self.group_id = group_id - self.is_open = is_open - - def __del__(self): - self.cur.close() - self.conn.close() - - def get_records(self): - records = {} - try: - sql = f'select question,answer,rec_maker,group_id,is_open from records' - self.cur.execute(sql) - rows = self.cur.fetchall() - for row in rows: - records[row[0],row[3]] = {'answer':row[1],'rec_maker':row[2],'is_open':row[4]} - return records - except Exception as ex: - logger.info(ex) - return None - def insert_database(self): - try: - sql = f'insert into records(question,answer,rec_maker,group_id,is_open)values\ - ("{self.question}","{self.answer}",{self.rec_maker},{self.group_id},{self.is_open})' - logger.info(f'执行一条sql {sql}') - self.cur.execute(sql) - self.conn.commit() - return True - except: - traceback.print_exc() - return False - - def delete(self,qu,gid): - try: - sql = f'delete from records where question="{qu}" and group_id={gid}' - self.cur.execute(sql) - self.conn.commit() - return True - except Exception as ex: - logger.info(ex) - return False - - def delete_force(self,qu): - try: - sql = f'delete from records where question="{qu}"' - self.cur.execute(sql) - self.conn.commit() - return True - except Exception as ex: - logger.info(ex) - return False - - - def count_user_records(self,uid): - try: - sql = f'select count(*) from records where rec_maker={uid}' - self.cur.execute(sql) - row = self.cur.fetchone() - return row[0] - except Exception as ex: - logger.info(ex) - return None - -def get_user_quota(user_priv): - if user_priv == SUPERUSER: - return 999 - if user_priv == OWNER: - return 10 - elif user_priv == ADMIN: - return 5 - else: - return 3 - -def show(records:dict,page:int): - rec_num = len(records) - pages = int(rec_num/5) + 1 - if page > pages: - return None - - if pages == page: - reply = '' - for (k,v) in list(records.keys())[5*page-5:]: - reply += (k+' : '+records[(k,v)]['answer']+'\n') - return reply,pages - else : - reply = '' - for (k,v) in list(records.keys())[5*page-5:5*page]: - reply += (k+' : '+records[(k,v)]['answer']+'\n') - return reply,pages - -import nonebot -from random import choice -import asyncio -async def send_msg(event, msg, at_sender:bool=False): - bot = nonebot.get_bot() - replys = msg.split('|') - reply = choice(replys) - final_replys = reply.split('+') - for r in final_replys: - await bot.send(event, r, at_sender=at_sender) - await asyncio.sleep(0.5) - diff --git a/shebot/QA/records.db3 b/shebot/QA/records.db3 deleted file mode 100644 index 0d83f53..0000000 Binary files a/shebot/QA/records.db3 and /dev/null differ diff --git a/shebot/_res.py b/shebot/_res.py new file mode 100644 index 0000000..a20a598 --- /dev/null +++ b/shebot/_res.py @@ -0,0 +1,103 @@ +import os +import re +from io import BytesIO +from os import path +from typing import Union + +import nonebot +from aiocqhttp.event import Event +from aiocqhttp.message import Message +from filetype.filetype import guess_mime +from PIL import Image +from nonebot.message import MessageSegment + + +from ._util import download_async, get_md5, get_random_file, logger, bot + +class Res: + base_dir = path.join(path.dirname(__file__), 'res') + image_dir = path.join(base_dir, 'image') + record_dir = path.join(base_dir, 'record') + img_cache_dir = path.join(image_dir, 'cache') + + def check_exist(res_path: str) -> None: + return path.exists(res_path) + + @classmethod + def image(cls, pic_path: str) -> 'MessageSegment': + if cls.check_exist(pic_path): + return MessageSegment.image(f'file:///{pic_path}') + elif cls.check_exist(path.join(cls.image_dir, pic_path)): + return MessageSegment.image(f'file:///{path.join(cls.image_dir, pic_path)}') + else: + return '【图片丢了】' + + @classmethod + def record(cls, rec_path) -> 'MessageSegment': + if cls.check_exist(rec_path): + return MessageSegment.record(f'file:///{rec_path}') + elif cls.check_exist(path.join(cls.record_dir, rec_path)): + return MessageSegment.record(f'file:///{path.join(cls.record_dir, rec_path)}') + else: + return '【图片丢了】' + + @classmethod + def cardimage(cls, pic_path: str) -> 'MessageSegment': + if cls.check_exist(pic_path): + return f"[CQ:cardimage,file=file:///{pic_path}]" + elif cls.check_exist(path.join(cls.image_dir, pic_path)): + return f"[CQ:cardimage,file=file:///{path.join(cls.image_dir, pic_path)}]" + else: + return '【图片丢了】' + + @classmethod + def get_random_image(cls, folder=None) -> 'MessageSegment': + if not folder: + image_path = cls.image_dir + else: + image_path = path.join(cls.image_dir, folder) + image_name = get_random_file(image_path) + return cls.image(path.join(image_path, image_name)) + + @classmethod + def get_random_record(cls, folder=None) -> 'MessageSegment': + if not folder: + record_path = cls.record_dir + else: + record_path = path.join(cls.record_dir, folder) + rec_name = get_random_file(record_path) + return cls.record(path.join(record_path, rec_name)) + + @classmethod + async def image_from_url(cls, url: str, cache=True) -> 'MessageSegment': + fname = get_md5(url) + image = path.join(cls.img_cache_dir, f'{fname}.jpg') + if not path.exists(image) or not cache: + image = await download_async(url, cls.img_cache_dir, f'{fname}.jpg') + return cls.image(image) + + @classmethod + async def cardimage_from_url(cls, url: str, cache=True) -> 'MessageSegment': + fname = get_md5(url) + image = path.join(cls.img_cache_dir, f'{fname}.jpg') + if not path.exists(image) or not cache: + image = await download_async(url, cls.img_cache_dir, f'{fname}.jpg') + return cls.cardimage(image) + + @classmethod + def image_from_memory(cls, data: Union[bytes, Image.Image]) -> 'MessageSegment': + if isinstance(data, Image.Image): + out = BytesIO() + data.save(out,format='png') + data = out.getvalue() + if not isinstance(data, bytes): + raise ValueError('不支持的参数类型') + ftype = guess_mime(data) + if not ftype or not ftype.startswith('image'): + raise ValueError('不是有效的图片类型') + fn = get_md5(data) + save_path = path.join(cls.img_cache_dir, fn) + with open(save_path, 'wb') as f: + f.write(data) + return cls.image(path.join(cls.img_cache_dir, fn)) + diff --git a/shebot/_util.py b/shebot/_util.py new file mode 100644 index 0000000..9f6ee08 --- /dev/null +++ b/shebot/_util.py @@ -0,0 +1,129 @@ +import asyncio +import hashlib +import json +import os +import random +import re +from os import path +from typing import List, Union + +import aiohttp +import filetype +import nonebot +from aiocqhttp.event import Event +from aiocqhttp.exceptions import ActionFailed +from nonebot import scheduler + +from hoshino.log import new_logger +from hoshino.service import Service + +logger = new_logger('shebot', debug=False) +bot = nonebot.get_bot() + +async def download_async(url: str, save_path: str, save_name: str, auto_extension=False) -> None: + timeout = aiohttp.ClientTimeout(total=30) + async with aiohttp.ClientSession(timeout=timeout) as session: + async with session.get(url) as resp: + content = await resp.read() + if auto_extension: #没有指定后缀,自动识别后缀名 + try: + extension = filetype.guess_mime(content).split('/')[1] + except: + raise ValueError('不是有效文件类型') + abs_path = path.join(save_path, f'{save_name}.{extension}') + else: + abs_path = path.join(save_path, save_name) + with open(abs_path, 'wb') as f: + f.write(content) + return abs_path + +def get_random_file(path) -> str: + files = os.listdir(path) + rfile = random.choice(files) + return rfile + +def get_md5(val: Union[bytes, str]) -> str: + if isinstance(val, str): + val = val.encode('utf-8') + m = hashlib.md5() + m.update(val) + return m.hexdigest() + +async def broadcast(msg,groups=None,sv_name=None): + bot = nonebot.get_bot() + #当groups指定时,在groups中广播;当groups未指定,但sv_name指定,将在开启该服务的群广播 + svs = Service.get_loaded_services() + if not groups and sv_name not in svs: + raise ValueError(f'不存在服务 {sv_name}') + enable_groups = await svs[sv_name].get_enable_groups() + send_groups = enable_groups.keys() if not groups else groups + for gid in send_groups: + try: + await bot.send_group_msg(group_id=gid,message=msg) + logger.info(f'群{gid}投递消息成功') + await asyncio.sleep(0.5) + except ActionFailed as e: + logger.error(f'在群{gid}投递消息失败, retcode={e.retcode}') + except Exception as e: + logger.exception(e) + +def extract_url_from_event(event: Event) -> List[str]: + urls = re.findall(r'http.*?term=\d', str(event.message)) + return urls + +def save_config(config:dict,path:str): + try: + with open(path,'w',encoding='utf8') as f: + json.dump(config, f, ensure_ascii=False, indent=2) + return True + except Exception as ex: + logger.error(ex) + return False + +def load_config(path): + try: + with open(path, mode='r', encoding='utf-8') as f: + config = json.load(f) + return config + except Exception as ex: + logger.error(f'exception occured when loading config in {path} {ex}') + logger.exception(ex) + return {} + +class RSS(): + def __init__(self): + self.base_url = 'http://101.32.36.8:1200' + self.route :str= None + self.xml : bytes = None + self.filter : dict = dict() + self.filterout :dict = dict() #out为过滤掉 + ''' + filter 选出想要的内容 + filter: 过滤标题和描述 + filter_title: 过滤标题 + filter_description: 过滤描述 + filter_author: 过滤作者 + filter_time: 过滤时间,仅支持数字,单位为秒。返回指定时间范围内的内容。如果条目没有输出pubDate或者格式不正确将不会被过滤 + ''' + self.filter_case_sensitive = True #过滤是否区分大小写,默认区分大小写 + self.limit = 10 #限制最大条数,主要用于排行榜类 RSS + + async def get(self): + url = self.base_url + self.route + params = {} + for key in self.filter: + if self.filter[key]: + params[key] = self.filter[key] + for key in self.filterout: + if self.filterout[key]: + params[key] = self.filterout[key] + params['limit'] = self.limit + async with aiohttp.ClientSession() as session: + async with session.get(url,params=params) as resp: + self.xml = await resp.read() + + def parse_xml(self): + #在实现类中编写解析xml函数 + raise NotImplementedError + + diff --git a/shebot/biliVideo/__init__.py b/shebot/biliVideo/__init__.py deleted file mode 100644 index 4ffe45b..0000000 --- a/shebot/biliVideo/__init__.py +++ /dev/null @@ -1,107 +0,0 @@ -from hoshino.util4sh import RSS, load_config, save_config, broadcast -from nonebot import CommandSession -from os import path -from lxml import etree -from hoshino.service import Service - -class Video: - def __init__(self,title=None,desc=None,link=None,time=None): - self.title = title - self.desc = desc - self.link = link - self.time = time - def __eq__(self, other): - return self.time == other.time - -class BiliVideo(RSS): - def __init__(self,UID:int): - super().__init__() - self.route = f'/bilibili/user/video/{UID}' - self.limit = 1 - self.UID = UID - self.latest = Video() - def parse_xml(self): - rss = etree.XML(self.xml) - item = rss.xpath('/rss/channel/item')[0] - title = item.find('.title').text.strip() - desc = item.find('.description').text.strip() - link = item.find('.link').text.strip() - time = item.find('.pubDate').text.strip() - return Video(title,desc,link,time) - - - -sv = Service('B站投稿提醒') -subs_path = path.join(path.dirname(__file__),'subs.json') -_subscribes = load_config(subs_path) -_BVs = [] - -for subs in _subscribes: - UID = _subscribes[subs]['UID'] - BV = BiliVideo(UID) - BV.latest.time = _subscribes[subs]['latest_time'] - _BVs.append(BV) - -@sv.scheduled_job('cron',minute='*/10',second='20') -async def check_BiliVideo(): - for BV in _BVs: - await BV.get() - video = BV.parse_xml() - if BV.latest != video: #投稿有更新 - sv.logger.info(f'检测到up{BV.UID}投稿更新') - BV.latest = video - global _subscribes - _subscribes[str(BV.UID)]['latest_time'] = video.time - save_config(_subscribes,subs_path) - groups = _subscribes[str(BV.UID)]['subs_groups'] - await broadcast(f'up投稿提醒========\n{video.title}\n{video.link}',groups=groups) - else: - sv.logger.info(f'未检测到up{BV.UID}投稿更新') - -@sv.on_command('video',aliases=('投稿提醒')) -async def subscribe(session:CommandSession): - UID_str = session.get('UID',prompt='请输入订阅up的UID') - if not UID_str.isdigit(): - del session.state['UID'] - session.pause('参数错误,请重新输入') - global _subscribes - gid = session.event['group_id'] - if UID_str in _subscribes.keys(): - if gid not in _subscribes[UID_str]['subs_groups']: - _subscribes[UID_str]['subs_groups'].append(gid) - else: - await session.send('本群已经订阅过该UP了') - return - else: - _subscribes[UID_str] = { - "UID" : int(UID_str), - "subs_groups" : [gid], - "latest_time" : "" - } - BV = BiliVideo(int(UID_str)) - global _BVs - _BVs.append(BV) - if save_config(_subscribes,subs_path): - await session.send('订阅成功') - else: - await session.send('订阅失败,请与bot维护中联系') - -@sv.on_command('cancel_video',aliases=('取消UP投稿提醒','取消投稿提醒')) -async def cancel(session:CommandSession): - UID_str = session.get('UID',prompt='请输入UID') - global _subscribes - global _BVs - if UID_str in _subscribes.keys(): - if len(_subscribes[UID_str]['subs_groups']) ==1: #只有一个群订阅该up投稿 - for BV in _BVs[::-1]: - if BV.UID == int(UID_str): - _BVs.remove(BV) - del _subscribes[UID_str] - save_config(_subscribes,subs_path) - sv.logger.info(f'成功取消up{UID_str}的投稿提醒') - session.send(f'成功取消up{UID_str}的投稿提醒') - else: - gid = session.event['group_id'] - _subscribes[UID_str]['subs_groups'].remove(gid) - save_config(_subscribes,subs_path) - session.send(f'成功取消up{UID_str}的投稿提醒') \ No newline at end of file diff --git a/shebot/biliVideo/subs.json b/shebot/biliVideo/subs.json deleted file mode 100644 index 9e26dfe..0000000 --- a/shebot/biliVideo/subs.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/shebot/infoPush/__init__.py b/shebot/infoPush/__init__.py index d3c6fc8..55dd5a2 100644 --- a/shebot/infoPush/__init__.py +++ b/shebot/infoPush/__init__.py @@ -1,11 +1,13 @@ -import nonebot import re +from collections import defaultdict +from os import path from typing import Dict -from hoshino.util4sh import RSS, load_config, save_config, broadcast + +import nonebot from lxml import etree + from hoshino.service import Service -from os import path -from collections import defaultdict +from .._util import RSS, load_config, save_config, broadcast class Info(RSS): def __init__(self,route) -> None: @@ -41,9 +43,7 @@ def check_update(self) -> bool: #info类可以通过pubDate判断是否更新 Info('/bilibili/user/dynamic/353840826') ], Service('B站up动态') : [ - Info('/bilibili/user/dynamic/282994'), - Info('/bilibili/user/dynamic/11073'), - Info('/bilibili/user/dynamic/673816') + Info('/bilibili/user/dynamic/282994') ] #Service('HoshinoIssue推送') : [Info('/github/issue/Ice-Cirno/Hoshinobot')] } @@ -58,16 +58,7 @@ def check_update(self) -> bool: #info类可以通过pubDate判断是否更新 for info in infos: info._latest = _latest_data.get(info.route, '') -from hoshino.util4sh import Res as R -async def handle_xml_img(xml_str: str) -> str: - for label in re.findall('', xml_str): - if re.search('(".+?")', label): - url = re.search('(".+?")', label).group(1) - pic = await R.image_from_url(url) - xml_str = re.sub('<.+?>', str(pic), xml_str, 1) - return xml_str - -@nonebot.scheduler.scheduled_job('cron', minute='*/5', second='30') +@nonebot.scheduler.scheduled_job('cron', minute='*', second='30') async def check(): for sv in _inf_svs: for info in _inf_svs[sv]: diff --git a/shebot/infoPush/latest_data.json b/shebot/infoPush/latest_data.json index e69de29..9e26dfe 100644 --- a/shebot/infoPush/latest_data.json +++ b/shebot/infoPush/latest_data.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/shebot/infoPush/readme.txt b/shebot/infoPush/readme.txt new file mode 100644 index 0000000..9a9e3d8 --- /dev/null +++ b/shebot/infoPush/readme.txt @@ -0,0 +1,3 @@ +这是一个rss 订阅插件,使用的是我本人的rss订阅源 +更换请在_util.py里将self.base_url = 'http://101.32.36.8:1200'改为其他rss源 +添加推送内容请在 __init__.py 里按照示例添加路由和service,路由详情见 https://docs.rsshub.app/ \ No newline at end of file diff --git a/shebot/liveNotice/__init__.py b/shebot/liveNotice/__init__.py index 65fded3..547d011 100644 --- a/shebot/liveNotice/__init__.py +++ b/shebot/liveNotice/__init__.py @@ -1,9 +1,12 @@ -from hoshino.service import Service -from hoshino.util4sh import RSS, load_config, save_config, broadcast -from lxml import etree from os import path + +from lxml import etree from nonebot import CommandSession +from hoshino.service import Service +from .._util import load_config, save_config, broadcast, RSS + + class Live(RSS): def __init__(self,route:str): super().__init__() @@ -128,12 +131,12 @@ async def cancel(session:CommandSession): del _subscribes[room] save_config(_subscribes,subs_path) sv.logger.info(f'成功取消直播间{room}的开播提醒') - session.send(f'成功取消直播间{room}直播提醒') + await session.send(f'成功取消直播间{room}直播提醒') else: gid = session.event['group_id'] _subscribes[room]['subs_groups'].remove(gid) save_config(_subscribes,subs_path) - session.send(f'成功取消直播间{room}直播提醒') + await session.send(f'成功取消直播间{room}直播提醒') diff --git a/shebot/liveNotice/readme.txt b/shebot/liveNotice/readme.txt new file mode 100644 index 0000000..69fcb07 --- /dev/null +++ b/shebot/liveNotice/readme.txt @@ -0,0 +1,4 @@ +指令 +live 根据提示订阅直播间 +取消直播推送/取消直播提醒 根据提示取消已订阅的直播推送 + diff --git a/shebot/liveNotice/subs.json b/shebot/liveNotice/subs.json index c00bfff..9e26dfe 100644 --- a/shebot/liveNotice/subs.json +++ b/shebot/liveNotice/subs.json @@ -1,34 +1 @@ -{ - "4316215": { - "platform": "bilibili", - "room": 4316215, - "subs_groups": [ - 907959319 - ], - "latest_time": "Thu, 06 Aug 2020 13:00:54 GMT" - }, - "307876": { - "platform": "douyu", - "room": 307876, - "subs_groups": [ - 907959319 - ], - "latest_time": "Wed, 05 Aug 2020 10:25:49 GMT" - }, - "2159209": { - "platform": "douyu", - "room": 2159209, - "subs_groups": [ - 907959319 - ], - "latest_time": "Thu, 06 Aug 2020 05:12:23 GMT" - }, - "21752694": { - "platform": "bilibili", - "room": 21752694, - "subs_groups": [ - 907959319 - ], - "latest_time": "Thu, 06 Aug 2020 15:30:52 GMT" - } -} \ No newline at end of file +{} \ No newline at end of file diff --git a/shebot/reply/__init__.py b/shebot/reply/__init__.py deleted file mode 100644 index 05c6d62..0000000 --- a/shebot/reply/__init__.py +++ /dev/null @@ -1,166 +0,0 @@ -import re -from os import path -from hoshino.service import Service -import sqlite3 -from collections import defaultdict -from random import choice - - -_dbpath = path.join(path.dirname(__file__), 'reply.db3') - -class SqlTable: - conn = sqlite3.connect(_dbpath) - cur = conn.cursor() - def __init__(self, db_path: str, table_name: str) -> None: - self.table = table_name - - def select(self) -> dict: - SqlTable.cur.execute(f'select * from {self.table}') - rows = SqlTable.cur.fetchall() - return {r[0]:list(set(r[1].split('|'))) for r in rows} - - def insert(self, word: str, reply: str) -> None: - SqlTable.cur.execute(f'insert into {self.table} (word, reply) values(?, ?)', [word, reply]) - SqlTable.conn.commit() - - def update(self, word: str, reply: str) -> None: - SqlTable.cur.execute(f'update {self.table} set reply = ? where word = ?', [reply, word]) - SqlTable.conn.commit() - - def delete(self, word: str) -> None: - self.cur.execute(f'delete from {self.table} where word = ?', [word]) - self.conn.commit() - - def __del__(self) -> None: - print('close database') - SqlTable.conn.commit() - self.conn.close() - -class BaseHandler: - - def __init__(self, table_name) -> None: - self.sqltable = SqlTable(_dbpath, table_name) - self._dict = self.sqltable.select() or defaultdict(list) - - def add(self, x: str, reply: str) -> None: - raise NotImplementedError - - def delete(self, x: str) -> None: - if x in self._dict: - del self._dict[x] - self.sqltable.delete(x) - else: - raise ValueError('不存在的关键字') - - def find_reply(self, event) -> str: - raise NotImplementedError - - -class FullmatchHandler(BaseHandler): - def __init__(self) -> None: - super().__init__('fullmatch') - - def add(self, word: str, reply: str) -> None: - if word in self._dict: - if reply in self._dict[word]: - raise ValueError('该回复已经存在,无需重复添加') - self._dict[word].append(reply) - #将集合元素以|连接后存入数据库 - reply_str = '|'.join(self._dict[word]) - self.sqltable.update(word, reply_str) - else: - self._dict[word] = list(reply.split('|')) - self.sqltable.insert(word, reply) - - def find_reply(self, event) -> str: - msg = event.raw_message.strip() - replys = self._dict.get(msg) - if replys: - return choice(replys) - else: - return None - - -class KeywordHandler(BaseHandler): - def __init__(self) -> None: - super().__init__('keyword') - - def add(self, keyword: str, reply: str) -> None: - if keyword in self._dict: - self._dict[keyword].append(reply) - reply_str = '|'.join(self._dict[keyword]) - self.sqltable.update(keyword, reply_str) - return - for k in self._dict: - if keyword in k or k in keyword: - raise ValueError('关键字冲突') - self._dict[keyword] = list(reply.split('|')) - self.sqltable.insert(keyword, reply) - - def find_reply(self, event) -> str: - msg = event.raw_message.strip() - for k in self._dict: - if k in msg: - return choice(self._dict[k]) - return None - -class RexHandler(BaseHandler): - def __init__(self) -> None: - super().__init__('rex') - - def add(self, pattern: str, reply: str) -> None: - self._dict[pattern] = list(reply.split('|')) - self.sqltable.insert(pattern, reply) - - def find_reply(self, event) -> str: - msg = event.raw_message.strip() - for pattern in self._dict: - match = re.search(pattern, msg) - if match: - reply = choice(self._dict[pattern]) - return reply - - -fullmatch = FullmatchHandler() -keyword = KeywordHandler() -rex = RexHandler() -chain = [fullmatch, keyword, rex] - -import nonebot -bot = nonebot.get_bot() -@bot.on_message() -async def reply(ctx): - for h in chain: - reply = h.find_reply(ctx) - if reply: - await bot.send(ctx, reply) - return - else: - pass - -sv = Service('自定义问答', use_priv=999) -from hoshino.util4sh import Res -@sv.on_rex(r'((?:fullmatch)|(?:keyword)|(?:rex)).{1,200}#.{1,200}') -async def add_reply(bot, event): - await Res.save_image(event) - match = event['match'] - handler = match.group(1) - word, reply = tuple(event.raw_message.strip(handler).strip().split('#')) - try: - eval(handler).add(word, reply) - await bot.send(event, '添加成功') - except Exception as ex: - sv.logger.error(f'添加失败{ex}') - await bot.send(event, f'添加失败,{ex}') - -@sv.on_rex(r'删除((?:fullmatch)|(?:keyword)|(?:rex))(.+)') -async def delete_reply(bot, event): - match = event['match'] - handler = match.group(1) - word = match.group(2) - try: - eval(handler).delete(word) - await bot.send(event, '删除成功') - except ValueError as vr: - sv.logger.error(vr) - await bot.send(event, f'删除失败,{vr}') \ No newline at end of file diff --git a/shebot/reply/reply.db3 b/shebot/reply/reply.db3 deleted file mode 100644 index e46d62c..0000000 Binary files a/shebot/reply/reply.db3 and /dev/null differ diff --git a/shebot/res/image/nr18_setu/25117805 b/shebot/res/image/nr18_setu/25117805 new file mode 100644 index 0000000..bc382b6 Binary files /dev/null and b/shebot/res/image/nr18_setu/25117805 differ diff --git a/shebot/res/image/r18_setu/60389386 b/shebot/res/image/r18_setu/60389386 new file mode 100644 index 0000000..3e271fd Binary files /dev/null and b/shebot/res/image/r18_setu/60389386 differ diff --git a/shebot/res/image/search_setu/67329343 b/shebot/res/image/search_setu/67329343 new file mode 100644 index 0000000..f71891e Binary files /dev/null and b/shebot/res/image/search_setu/67329343 differ diff --git "a/shebot/res/image/\345\233\276\347\211\207\346\226\207\344\273\266\345\244\271" "b/shebot/res/image/\345\233\276\347\211\207\346\226\207\344\273\266\345\244\271" deleted file mode 100644 index e69de29..0000000 diff --git "a/shebot/res/record/\350\257\255\351\237\263\346\226\207\344\273\266\345\244\271(\350\231\275\347\204\266\347\233\256\345\211\215\346\262\241\347\224\250)" "b/shebot/res/record/\350\257\255\351\237\263\346\226\207\344\273\266\345\244\271(\350\231\275\347\204\266\347\233\256\345\211\215\346\262\241\347\224\250)" deleted file mode 100644 index e69de29..0000000 diff --git a/shebot/scheduledCommand/__init__.py b/shebot/scheduledCommand/__init__.py deleted file mode 100644 index 9b37356..0000000 --- a/shebot/scheduledCommand/__init__.py +++ /dev/null @@ -1,141 +0,0 @@ -import nonebot -from aiocqhttp import Event -from apscheduler.job import Job -from hoshino.service import Service -from nonebot import CommandSession,Message -from aiocqhttp import Event,Message -from hoshino.util4sh import bot, load_config,save_config,add_cron_job,add_delay_job -from collections import defaultdict -from os import path -from typing import List -from hoshino.msghandler import handle_message -from traceback import print_exc -from hoshino.priv import * - -bot = nonebot.get_bot() -sv = Service('定时命令',use_priv=ADMIN) -async def task(event): - if not event: - return - uid = event['user_id'] - await bot.send(event,f'下一条消息为成员{uid}设置的定时命令执行结果') - try: - await handle_message(bot, event, '_') - except Exception as ex: - sv.logger.error(f'执行scheduled command时发生错误{ex}') - for func in bot._bus._subscribers['message']: - try: - await func(event) - except Exception as ex: - sv.logger.debug(ex) - -_task_path = path.join(path.dirname(__file__),'tasks.json') -_tasks = load_config(_task_path) if load_config(_task_path) else defaultdict(list) -_running_jobs:List[Job] = [] - -from aiocqhttp import MessageSegment -@sv.on_command('add_cron_job',aliases=('设置周期任务')) -async def _(session:CommandSession): - hour:str = session.get('hour',prompt='您想几点运行,多个时间用逗号隔开') - minute:str = session.get('minute',prompt='您想几分运行,多个时间用逗号隔开') - cmd:str = session.get('cmd',prompt='请输入要运行的指令') - cmd = cmd.strip('命令') #Hoshino消息只处理一次,加上命令前缀防止触发命令 - session.event.raw_message = cmd - session.event.message = Message(MessageSegment.text(cmd)) - try: - global _running_jobs - job = add_cron_job(task,hour=hour,minute=minute,args=[session.event]) - _running_jobs.append(job) - global _tasks - - #对event先处理,剔除由RexTrigger添加的match对象 - if 'match' in session.event: - del session.event['match'] - - _tasks['data'].append(dict({ - "id" : job.id, - "event" : session.event, - "hour" : hour, - "minute" : minute - })) - save_config(_tasks,_task_path) - await session.send('设置成功') - except ValueError as ex: - sv.logger.error(f'添加定时任务时出现异常{ex}') - await session.send('参数错误,请重新设置') - except: - print_exc() - -@sv.on_command('add_delay_job',aliases=('设置延时任务')) -async def _(session:CommandSession): - delay:str = session.get('delay',prompt='请输入延迟时间,单位为秒') - if not delay.isdigit(): - await session.send('参数错误,请确保输入为数字') - session.finish() - cmd:str = session.get('cmd',prompt='请输入要运行的指令') - cmd = cmd.strip('命令') - #将当前event中的消息段替换 - session.event['raw_message'] = cmd - session.event['message'] = Message([{'type': 'text', 'data': {'text': f'{cmd}'}}]) - try: - add_delay_job(task=task,delay_time=int(delay),args=[session.event]) - await session.send('设置成功') - except ValueError as ex: - await sv.logger.error(f'添加延时任务时出现异常{ex}') - await session.send('参数错误,请重新设置') - -@sv.on_command('show_cron_job',aliases=('查看周期任务')) -async def _(session:CommandSession): - gid = session.event['group_id'] - reply = '' - for jb in _running_jobs: - if jb.args[0].get('group_id') == gid: - id = jb.id - cmd = jb.args[0].get('raw_message') - next_run_time = jb.next_run_time - reply += f'id:\n{id}\n命令:\n{cmd}\n下次运行时间:\n{next_run_time}\n\n' - reply = reply.strip() - if not reply: - await session.send('周期任务列表为空') - else: - await session.send(reply,at_sender=False) - -@sv.on_command('cancel_cron_job',aliases=('取消周期任务')) -async def _(session:CommandSession): - stripped_arg = session.current_arg_text.strip() - if session.is_first_run and stripped_arg: - id = stripped_arg - else: - id = session.get('id',prompt='请输入任务id,任务id可通过/show_cron_job得到') - global _running_jobs - job_num = len(_running_jobs) - for jb in _running_jobs[::-1]: - if jb.id == id: - _running_jobs.remove(jb) - global _tasks - for tsk in _tasks['data'][::-1]: - if tsk['id'] == id: - _tasks['data'].remove(tsk) - if job_num == len(_running_jobs): - await session.send('没有找到该任务') - session.finish() - else: - save_config(_tasks,_task_path) - await session.send('删除周期任务成功') - -#在bot启动时创建计划任务 -@bot.on_startup -async def _(): - for tsk in _tasks['data']: - #构造Event对象event - event = Event(tsk['event']) - event.message = Message(tsk['event']['message']) #构造消息段对象 - hour = tsk['hour'] - minute = tsk['minute'] - id = tsk['id'] - try: - job = add_cron_job(task,id=id,hour=hour,minute=minute,args=[event]) - global _running_jobs - _running_jobs.append(job) - except ValueError as ex: - sv.logger.error(f'添加定时任务时出现异常{ex}') \ No newline at end of file diff --git a/shebot/scheduledCommand/tasks.json b/shebot/scheduledCommand/tasks.json deleted file mode 100644 index e69de29..0000000 diff --git a/shebot/scheduledMessage/__init__.py b/shebot/scheduledMessage/__init__.py deleted file mode 100644 index d180f87..0000000 --- a/shebot/scheduledMessage/__init__.py +++ /dev/null @@ -1,124 +0,0 @@ -import os -import time -from aiocqhttp.event import Event -import nonebot -from nonebot.command import CommandSession -from hoshino.service import Service -from hoshino.util4sh import add_cron_job, load_config,save_config,broadcast -from hoshino.priv import * - -job_path = os.path.join(os.path.dirname(__file__),'jobs.json') -sv = Service('闹钟', use_priv = OWNER) -bot = nonebot.get_bot() - - -async def send_group_msg(gid,msg): - await bot.send_group_msg(group_id=gid,message=msg) - -saved_jobs = load_config(job_path) -running_jobs = [] #正在运行的任务 - -@sv.on_command('hourclock', aliases=('闹钟','设置计划任务')) -async def add_job(session: CommandSession): - global saved_jobs - global running_jobs - stripped_arg = session.current_arg_text.strip() - id = str(time.time()) - if session.is_first_run and stripped_arg: - id = stripped_arg - gid = session.event['group_id'] - hour: str = session.get('hour',prompt='您想几点运行,多个时间用逗号隔开') - minute: str = session.get('minute',prompt='您想几分运行,多个时间用逗号隔开') - msg: str = session.get('msg', prompt='请输入要发送的内容') - saved_jobs[id] = { - 'groups' : [gid], - 'hour' : hour, - 'minute' : minute, - 'msg' : msg - } - try: - job = add_cron_job(send_group_msg,id=id,hour=hour,minute=minute,args=[gid,msg]) - running_jobs.append(job) - save_config(saved_jobs,job_path) - await bot.send(session.event,f'好的,我记住了,将在每天{hour}点{minute}分发送{msg}',at_sender=True) - except Exception as ex: - sv.logger.error(f'添加定时任务时出现异常{ex}') - await session.finish('参数错误,请重新设置') - -@sv.on_keyword('查看计划任务') -async def _(bot,event): - if not running_jobs: - await bot.send(event,'没有正在运行的计划任务',at_sender=False) - return - reply = '' - for job in running_jobs: - try: - reply += f'id:\n{job.id}\n下次运行时间:\n{job.next_run_time.strftime("%Y-%m-%d-%H:%M:%S")}\n发送内容:\n{job.args[0]}\n\n' - except: - running_jobs.remove(job) - await bot.send(event,reply.strip(),at_sender=False) - -@sv.on_prefix('删除计划任务') -async def delete_job(bot, event: Event): - global running_jobs - global saved_jobs - id = event.raw_message.strip('删除计划任务') - if id in saved_jobs: - del saved_jobs[id] - save_config(saved_jobs,job_path) - for job in running_jobs: - if id == job.id: - running_jobs.remove(job) - job.remove() - await bot.send(event,'已删除计划任务') - -@bot.on_startup -async def load_saved_jobs(): - #将json文件中的计划任务启动 - global running_jobs - for k in saved_jobs: - groups = saved_jobs[k]['groups'] - hour = saved_jobs[k]['hour'] - minute = saved_jobs[k]['minute'] - msg = saved_jobs[k]['msg'] - job = add_cron_job(broadcast,id=k,hour=hour,minute=minute,args=[msg,set(groups),sv.name]) if groups!='default' else \ - add_cron_job(broadcast,id=k,hour=hour,minute=minute,args=[msg,None,sv.name]) - running_jobs.append(job) - print(f'job {job.id} added') - - -""" robj = re.match(r'(\d{1,2})点(\d{1,2})发送(.{1,100})',message) - if robj: - hr = int(robj.group(1)) - min = int(robj.group(2)) - msg = robj.group(3) - now = datetime.datetime.now() - run_date = datetime.datetime(now.year,now.month,now.day,hr,min) - try: - job = add_date_job(send_group_msg,id=now_str,run_date=run_date,args=[gid,msg]) - running_jobs.append(job) - await bot.send(event,'好的,我记住了',at_sender=True) - except: - await bot.send(event,'添加任务失败,请检查参数') - return - - robj = re.match(r'(\d{1,2})?小?时?(\d{1,2})分钟后发送(.{1,100})',message) - if robj: - try: - delta_hr = int(robj.group(1)) - except: - delta_hr = 0 - try: - delta_min = int(robj.group(2)) - except: - delta_min = 0 - msg = robj.group(3) - now = datetime.datetime.now() - run_date = now + datetime.timedelta(hours=delta_hr,minutes=delta_min) - try: - job = add_date_job(send_group_msg,id=now_str,run_date=run_date,args=[gid,msg]) - running_jobs.append(job) - await bot.send(event,f'好的,我记住了',at_sender=True) - except: - await bot.send(event,'添加任务失败,请检查参数') - return """ \ No newline at end of file diff --git a/shebot/scheduledMessage/jobs.json b/shebot/scheduledMessage/jobs.json deleted file mode 100644 index e69de29..0000000 diff --git a/shebot/setu/__init__.py b/shebot/setu/__init__.py index e67ce24..7f2b16a 100644 --- a/shebot/setu/__init__.py +++ b/shebot/setu/__init__.py @@ -1,21 +1,18 @@ -import re -import os -import asyncio -from aiocqhttp.event import Event -import threading - -from os import path -from .api import get_final_setu_async -from .data_source import SetuWarehouse, save_config,load_config,send_setus -from .config import * +import threading + from hoshino.service import Service -from aiocqhttp.message import MessageSegment +from hoshino.priv import * +from hoshino.util import DailyNumberLimiter, FreqLimiter +from .api import * +from .config import * +from .data_source import * +from .._util import * +from .._res import Res as R g_config = load_config() g_r18_groups = set(g_config.get('r18_groups',[])) g_delete_groups = set(g_config.get('delete_groups',[])) -from hoshino.util4sh import Res as R #初始化色图仓库 nr18_path = path.join(R.image_dir, 'nr18_setu') #存放非r18图片 r18_path = path.join(R.image_dir, 'r18_setu') #存放r18图片 @@ -23,7 +20,7 @@ if not os.path.exists(search_path): os.mkdir(search_path) wh = SetuWarehouse(nr18_path) -r18_wh = SetuWarehouse(r18_path,r18=1) +r18_wh = SetuWarehouse(r18_path, r18=1) #启动一个线程一直补充色图 thd = threading.Thread(target=wh.keep_supply) @@ -38,14 +35,10 @@ thd_r18.start() #设置limiter -from hoshino.util import DailyNumberLimiter, FreqLimiter _num_limiter = DailyNumberLimiter(DAILY_MAX_NUM) _freq_limiter = FreqLimiter(5) - -from hoshino.priv import * -from hoshino.util4sh import download_async -sv = Service('涩图') +sv = Service('色图') @sv.on_rex(r'^来?([1-5])?[份点张]?[涩色瑟]图(.{0,10})$') async def send_common_setu(bot,event: Event): uid = event.user_id @@ -62,17 +55,6 @@ async def send_common_setu(bot,event: Event): await bot.send(event, TOO_FREQUENT_NOTICE) return - if not ONLINE_MODE: - sv.logger.info('发送本地涩图') - pic = R.get_random_image('nr18_setu') - ret = await bot.send(event, pic) - msg_id = ret['message_id'] - if is_to_delete: - #30秒后删除 - await asyncio.sleep(30) - await bot.delete_msg(self_id=self_id, message_id=msg_id) - return - robj = event['match'] try: num = int(robj.group(1)) @@ -84,29 +66,42 @@ async def send_common_setu(bot,event: Event): _freq_limiter.start_cd(uid,num*5) keyword = robj.group(2).strip() + + if not ONLINE_MODE and not keyword: + sv.logger.info('发送本地涩图') + _num_limiter.increase(uid) + pic = R.get_random_image('nr18_setu') + ret = await bot.send(event, pic) + msg_id = ret['message_id'] + if is_to_delete: + #30秒后删除 + await asyncio.sleep(DELETE_AFTER) + await bot.delete_msg(self_id=self_id, message_id=msg_id) + return if keyword: await bot.send(event, '正在搜索,请稍等~') + _freq_limiter.start_cd(uid, 30) # To relieve net pressure sv.logger.info(f'含有关键字{keyword},尝试搜索') - if user_priv < SUPERUSER: + if not check_priv(event, SUPERUSER): # SUPERUSER 搜图可以搜出r18 setus = await get_final_setu_async(search_path,keyword=keyword,r18=0) else: - setus = await get_final_setu_async(search_path,keyword=keyword,r18=2) + setus = await get_final_setu_async(search_path,keyword=keyword,r18=2,num=3) if not setus: await bot.send(event, f'没有找到关键字{keyword}的涩图,您将被禁用涩图服务60s') _freq_limiter.start_cd(uid, 60) sv.logger.info(f'{uid} searched keyword {keyword} and returned no result') return - setu = setus[0] - pic_path = await download_async(setu.url, search_path, setu.pid) - pic = MessageSegment.image(pic_path) - reply = f'{setu.title}\n画师:{setu.author}\npid:{setu.pid}{pic}' - try: - await bot.send(event,reply,at_sender=False) - _num_limiter.increase(uid) - _freq_limiter.start_cd(uid, 30) - except Exception as ex: - await bot.send(event, f'搜索关键字{keyword}发生异常') - sv.logger.error(f'搜索涩图时发生异常 {ex}') + + for setu in setus: + print(setus) + pic_path = await download_async(setu.url, search_path, str(setu.pid)) + pic = R.image(pic_path) + reply = f'{setu.title}\n画师:{setu.author}\npid:{setu.pid}{pic}' + try: + await bot.send(event,reply,at_sender=False) + except Exception as ex: + await bot.send(event, f'搜索关键字{keyword}发生异常') + sv.logger.error(f'搜索涩图时发生异常 {ex}') else: setus = wh.fetch(num) if not setus:#send_setus为空 @@ -123,7 +118,7 @@ async def send_r18_setu(bot, event): self_id = event['self_id'] if gid not in g_r18_groups and gid!= 0: - await bot.send(event,'本群未开启r18色图,请私聊机器人,但注意不要过分请求,否则拉黑') + await bot.send(event,'本群未开启r18色图') return if not _num_limiter.check(uid): @@ -135,13 +130,14 @@ async def send_r18_setu(bot, event): return if not ONLINE_MODE: + _num_limiter.increase(uid) sv.logger.info('发送本地r18涩图') - pic = R.get_random_image('r18_path') + pic = R.get_random_image('r18_setu') ret = await bot.send(event,pic) msg_id = ret['message_id'] if is_to_delete: - await asyncio.sleep(30) + await asyncio.sleep(DELETE_AFTER) await bot.delete_msg(self_id=self_id, message_id=msg_id) return @@ -154,13 +150,12 @@ async def send_r18_setu(bot, event): @sv.on_message() async def switch(bot, event): - global g_is_online global g_delete_groups global g_config global g_r18_groups msg = event['raw_message'] gid = event.get('group_id') - if get_user_priv(event) < SUPERUSER: + if not check_priv(event, SUPERUSER): return elif msg == '本群涩图撤回': @@ -217,11 +212,3 @@ async def switch(bot, event): else: pass - - - - - - - - diff --git a/shebot/setu/config.py b/shebot/setu/config.py index 829cb56..82b5019 100644 --- a/shebot/setu/config.py +++ b/shebot/setu/config.py @@ -1,13 +1,14 @@ import re -APIKEY = 'XXXXXXXXXXXXXXXXXXXXX' #填lolicon apikey -ONLINE_MODE = True #选择在线模式还是本地模式,选择False时,程序将不会访问api和启动下载线程,插件发送本地涩图 -WITH_URL = False #是否发图时附带链接 -DAILY_MAX_NUM = 10 #每日最大涩图数(仅在在线模式下生效) +APIKEY = 'XXXXXXXXXXXXXXXXXXXXX' # 填lolicon apikey +ONLINE_MODE = True # 选择在线模式还是本地模式,选择False时,程序将不会访问api和启动下载线程,插件发送本地涩图 +WITH_URL = False # 是否发图时附带链接 +DAILY_MAX_NUM = 10 # 每日最大涩图数(仅在在线模式下生效) +DELETE_AFTER = 30 # 色图撤回时间,单位为s EXCEED_NOTICE = f'您今天已经冲过{DAILY_MAX_NUM}次了,请明早5点后再来!' TOO_FREQUENT_NOTICE = f'您冲得太快了,请稍后再来~' ###################################### -#本地模式涩图文件夹请在酷q image目录下找nr18_setu,r18_setu文件夹,分别代表非r18和r18色图 +#本地模式涩图文件夹请在shebot/res目录下创建nr18_setu,r18_setu文件夹,分别代表非r18和r18色图 diff --git a/shebot/setu/data_source.py b/shebot/setu/data_source.py index 1f0be5f..fd855ae 100644 --- a/shebot/setu/data_source.py +++ b/shebot/setu/data_source.py @@ -3,6 +3,7 @@ import json from .api import get_final_setu from queue import Queue +from .config import DELETE_AFTER class SetuWarehouse: @@ -58,7 +59,7 @@ def load_config(): except: return {} -from hoshino.util4sh import Res as R +from .._res import Res as R async def send_setus(bot,ctx,folder,setus,with_url=False,is_to_delete=False): reply = '' for setu in setus: @@ -73,5 +74,5 @@ async def send_setus(bot,ctx,folder,setus,with_url=False,is_to_delete=False): if is_to_delete: msg_id = ret['message_id'] self_id = ctx['self_id'] - await asyncio.sleep(30) + await asyncio.sleep(DELETE_AFTER) await bot.delete_msg(self_id=self_id, message_id=msg_id) diff --git a/shebot/setu/readme.txt b/shebot/setu/readme.txt index 1049a3b..654b188 100644 --- a/shebot/setu/readme.txt +++ b/shebot/setu/readme.txt @@ -13,7 +13,7 @@ r18色图 :就这不够色 0. 该插件由自用的涩图插件移植hoshinov2插件,在移植可能会有些许bug,见谅 1. 如果程序一直出现‘’正在补充色图‘’输出且无其他错误,说明你的apikey是有问题的,请重新获取apikey 2. 如果程序一直出现‘’正在补充色图‘’且中间一直穿插其他错误,多半是被api暂时屏蔽了,请过段时间再试 -3. r18涩图均旋转90度发送(个人感觉被腾讯吞图概率大幅度减小),如果不想旋转请注释api.py 64行img = img.transpose(Image.ROTATE_90) +3. r18涩图均旋转90度发送(腾讯吞图概率大幅度减小),如果不想旋转请注释api.py 64行img = img.transpose(Image.ROTATE_90) ************* 顺带一提,因为hoshino不支持私聊,所以想私聊发r18请按照下面说明魔改hoshino diff --git a/shebot/setu/setu_config.json b/shebot/setu/setu_config.json index e69de29..adf4a98 100644 --- a/shebot/setu/setu_config.json +++ b/shebot/setu/setu_config.json @@ -0,0 +1,3 @@ +{ + "r18_groups": [] +} \ No newline at end of file diff --git a/shebot/var_replace.py b/shebot/var_replace.py deleted file mode 100644 index 9ecc9c3..0000000 --- a/shebot/var_replace.py +++ /dev/null @@ -1,80 +0,0 @@ -from typing import Any, Callable, Dict -from aiocqhttp.message import MessageSegment -import nonebot -from aiocqhttp.event import Event -from nonebot.message import Message -import re - -class VarHandler: - def __init__(self) -> None: - self.allvar = {} - - def add(self, var: str, func: Callable) -> None: - self.allvar[var] = func - - def find_func(self, var: str) -> Callable: - return self.allvar.get(var) - -var_handler = VarHandler() -def replace(origin_msg_str: str, event: Event) -> MessageSegment: - allvar = re.findall('【.+?】', origin_msg_str) #取出变量列表,变量可能含有参数 - - if not allvar: - #没有变量,直接返回原消息 - return origin_msg_str - - replaced_msg_str = origin_msg_str - - for compelete_v in allvar: - # 取出参数列表 - args = [x.strip('<').strip('>') for x in re.findall('<.+?>', compelete_v)] - stripped_v = re.sub('<.+?>', '', compelete_v) - func = var_handler.find_func(stripped_v) - if func: - try: - replace_msg = func(*args) - except TypeError: #异常说明函数需要event参数 - replace_msg = func(*args, event) - except: - replace_msg = compelete_v - continue - replaced_msg_str = replaced_msg_str.replace(compelete_v, str(replace_msg)) - else: - continue #没有找到处理函数 - return replaced_msg_str - -def add_replace(var_name: str): - def deco(func): - var = f'【{var_name}】' - var_handler.add(var, func) - return deco - -bot = nonebot.get_bot() -@bot.before_sending -async def var_replace(event: Event, message: Message, kwargs: Dict[str, Any]): - origin_msg_str = str(message) - repaced_msg_str = replace(origin_msg_str, event) - message.clear() - message.extend(repaced_msg_str) - - - -from hoshino.util4sh import get_random_file, Res as R -from os import path -""" -可以按照如下格式添加变量,如果需要使用event,添加在最后一个参数 -返回值为str或者MessageSegment -""" -@add_replace('随机图片') -def random_pic(folder: str) -> 'MessageSegment': - #例 【随机图片<智乃>】 - pic = get_random_file(path.join(R.image_dir, folder)) - return R.image(f'{folder}/{pic}') - -@add_replace('艾特全体') -def at_all() -> 'MessageSegment': - return MessageSegment.at('all') - -@add_replace('艾特当前') -def at_current(event: Event) -> 'MessageSegment': - return MessageSegment.at(event.user_id) diff --git a/shebot/web4reply/__init__.py b/shebot/web4reply/__init__.py deleted file mode 100644 index 6efe2d0..0000000 --- a/shebot/web4reply/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -from aiocqhttp.event import Event -import nonebot -from .view4reply import reply -from .data_source import get_random_str - -app = nonebot.get_bot().server_app -if not app.config.get('SECRET_KEY'): - app.config['SECRET_KEY'] = 'DFFSGHSHDBBCLSDO' - -app.register_blueprint(reply) - -from .view4reply import public_address -bot = nonebot.get_bot() -@bot.on_message -async def send_url(ev: Event): - msg = ev.raw_message - if msg == '添加自定义回复': - await bot.send(ev, f'{public_address}:{bot.config.PORT}/reply/show') - diff --git a/shebot/web4reply/readme.txt b/shebot/web4reply/readme.txt deleted file mode 100644 index 09728dd..0000000 --- a/shebot/web4reply/readme.txt +++ /dev/null @@ -1,3 +0,0 @@ -view4reply.py里设置好自己服务器ip及登录密码 -static目录下show_reply.js里修改api_url : 'http://XXX.XX.X.XX:9000/reply/api/里面的ip -向bot发送添加自定义回复,点击链接登录(移动设备可能显示异常) \ No newline at end of file diff --git a/shebot/web4reply/static/show_reply.js b/shebot/web4reply/static/show_reply.js deleted file mode 100644 index 8d88bbd..0000000 --- a/shebot/web4reply/static/show_reply.js +++ /dev/null @@ -1,125 +0,0 @@ -var vm = new Vue({ - el: '#app', - data: { - api_url : 'http://ip:端口/reply/api/', - tableData : [], - dialogFormVisible : false, - form: { - word: '', - reply: '' - }, - formLabelWidth: '120px', - trigger : "fullmatch", - fileList : [], - cqcode : '', - copyDisable : false, - varDialogVisible : false, - varData : [{ - var : '【艾特全体】', - desc : '艾特全体成员' - },{ - var : '【艾特当前】', - desc : '艾特触发本回复的成员' - },{ - var : '【随机图片<文件夹>】', - desc : '随机发送文件夹内一张图片' - },{ - var : '【随机语音<文件夹>】', - desc : '随机发送文件夹内一条语音' - }] - }, - - mounted() { - this.refresh(); - }, - - methods: { - refresh: function (){ - var thisvue = this; - axios.get(thisvue.api_url + 'show' - ).then(function (resp) { - thisvue.tableData = resp.data.data; - }).catch(function (error) { - thisvue.$alert(error, '加载数据错误') - }); - }, - - delete_rec: function(scope){ - var thisvue = this; - var row = scope.$index; - thisvue.$confirm('是否删除 "' + scope.row.word + '"', '提示', { - confirmButtonText: '确定', - cancelButtonText: '取消', - type: 'danger' - }).then(() => { - thisvue.tableData.splice(row, 1); - axios.post(thisvue.api_url + 'delete', { - "word" : scope.row.word, - "trigger" : scope.row.trigger - }).catch(function(error) { - thisvue.$message.error('删除失败') - }) - }) - }, - - add_rec: function(){ - var thisvue = this; - if (thisvue.form.word == '' || thisvue.form.reply == ''){ - thisvue.$message.error('触发词或者回复为空,添加失败'); - return; - } - var rec = { - "trigger" : thisvue.trigger, - "word" : thisvue.form.word, - "reply" : thisvue.form.reply - } - thisvue.dialogFormVisible = false; - axios.post(thisvue.api_url + 'add', rec).then(function(resp){ - if(resp.data == 'success'){ - thisvue.$message.info('添加成功') - thisvue.form.word = '' - thisvue.form.reply = '' - } - }).catch(function(error){ - thisvue.$message.error('添加失败') - }) - }, - - handleRemove: function(file, fileList) { - console.log(file, fileList); - }, - handlePreview: function(file) { - console.log(file); - }, - handleExceed: function(files, fileList) { - this.$message.warning(`当前限制选择 1 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`); - }, - beforeRemove: function(file, fileList) { - return this.$confirm(`确定移除 ${ file.name }?`); - }, - uploadSuccess: function(resp, file, fileList){ - var thisvue = this; - thisvue.cqcode = resp - }, - - uploadError: function(resp, file, fileList){ - this.$alert('上传失败,请检查文件类型') - }, - - doCopy: function(){ - this.$copyText(this.cqcode).then(function (e){ - this.$alert('已复制') - }); - }, - - copyVar: function(scope){ - var thisvue = this; - thisvue.$copyText(scope.row.var).then(function (e){ - thisvue.$alert('已复制') - }); - } - - }, - - delimiters: ['[[', ']]'], -}) diff --git a/shebot/web4reply/static/vue-clipboard.js b/shebot/web4reply/static/vue-clipboard.js deleted file mode 100644 index 4a5d89b..0000000 --- a/shebot/web4reply/static/vue-clipboard.js +++ /dev/null @@ -1,107 +0,0 @@ -(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i - - - - - - - - - 自定义回复 - - -
- - -
- 添加 - - - - - 完全匹配 - 关键字匹配 - 正则匹配 - - - - - - - - - - - - 上传图片或者音频 -
只能上传jpg/png/mp3文件
-
-

- 复制酷Q码 - 查看变量 - - - - - - - - - - - -
- -
- 刷新 -
-
-
- - - - - - - - -
- - - - \ No newline at end of file diff --git a/shebot/web4reply/view4reply.py b/shebot/web4reply/view4reply.py deleted file mode 100644 index f601bb0..0000000 --- a/shebot/web4reply/view4reply.py +++ /dev/null @@ -1,115 +0,0 @@ -import json -import os -from .data_source import render_template, static_folder -from quart import request, session, redirect, url_for,send_from_directory -from ..reply import fullmatch, rex, keyword -from os import path -from hoshino.util4sh import Res as R -from quart import Blueprint - -reply = Blueprint('reply', __name__, static_folder='/static') - -@reply.route('/staticfile/') -async def staticfile(filename): - return await send_from_directory(static_folder, filename) - -@reply.route('/reply/show/') -async def show_reply(): - url = f'{public_address}:{port}' - return await render_template('show_reply.html', url = url, url_for = url_for) - -@reply.route('/reply/api/', methods=['GET', 'POST']) -async def call_action(action: str): - data = await request.get_data() - _triggers = { - '完全匹配' : 'fullmatch', - '关键字匹配' : 'keyword', - '正则匹配' : 'rex' - } - - if action == 'show': - f_dic = fullmatch.sqltable.select() - k_dic = keyword.sqltable.select() - r_dic = rex.sqltable.select() - reply_list = [{'trigger':'完全匹配', 'word':k, 'reply':'|'.join(v) } for k,v in f_dic.items()] - reply_list.extend([{'trigger':'关键字匹配', 'word':k, 'reply':'|'.join(v) } for k,v in k_dic.items()]) - reply_list.extend([{'trigger':'正则匹配', 'word':k, 'reply':'|'.join(v) } for k,v in r_dic.items()]) - return { - "data" : reply_list - } - - elif action == 'delete': - data = json.loads(data.decode()) - word = data['word'] - trigger = data['trigger'] - eval(_triggers[trigger]).delete(word) - return 'success' - - elif action == 'add': - data = json.loads(data.decode()) - trigger = data['trigger'] - word = data['word'] - reply = data['reply'] - eval(trigger).add(word, reply) - return 'success' - - elif action == 'upload': - fs = await request.files - fobj = fs.get('file') - f_type = fobj.content_type - if f_type.startswith('image'): - folder = R.image_dir - if not path.exists(folder): - os.mkdir(folder) - with open(path.join(folder, fobj.filename), 'wb') as f: - f.write(fobj.read()) - print(str(R.image(fobj.filename))) - return str(R.image(fobj.filename)) - elif f_type.startswith('audio'): - folder = R.record_dir - folder = R.image_dir - if not path.exists(folder): - os.mkdir(folder) - with open(path.join(folder, fobj.filename), 'wb') as f: - f.write(fobj.read()) - return str(R.record(fobj.filename)) - else: - raise TypeError('不支持的文件类型') - -import nonebot -from datetime import timedelta - -app = nonebot.get_bot().server_app -public_address = 'xxx.xx.xx.xx' #改为你服务器的公网ip -port = nonebot.get_bot().config.PORT -passwd = '123456' #登录密码 -@reply.before_request -async def _(): - user_ip = request.remote_addr - if request.path == '/login': - return - if session.get('user_ip') == user_ip:#登录过 - return - #没有登录,重定向至登录界面 - return redirect('/login') - -@reply.route('/login',methods=['GET','POST']) -async def login(): - print(request.method) - if request.method == 'GET': - return await render_template('login.html',passwd=passwd,public_address=public_address,port=port) - else: - login_data = await request.form - input_psd = login_data.get('password') - if input_psd == passwd: - user_ip = request.remote_addr - session['user_ip'] = user_ip - session.permanent = True - app.permanent_session_lifetime = timedelta(weeks=2) - return redirect('/reply/show') - else: - return redirect('/login') - - - - diff --git a/shebot/webServiceManager/__init__.py b/shebot/webServiceManager/__init__.py new file mode 100644 index 0000000..6f08d46 --- /dev/null +++ b/shebot/webServiceManager/__init__.py @@ -0,0 +1,4 @@ +import nonebot +from .view import switcher +app = nonebot.get_bot().server_app +app.register_blueprint(switcher) diff --git a/shebot/web4reply/data_source.py b/shebot/webServiceManager/data_source.py similarity index 83% rename from shebot/web4reply/data_source.py rename to shebot/webServiceManager/data_source.py index ffff038..06b9904 100644 --- a/shebot/web4reply/data_source.py +++ b/shebot/webServiceManager/data_source.py @@ -2,10 +2,8 @@ import os import random -from quart.helpers import url_for - -static_folder = os.path.join(os.path.dirname(__file__),'static') template_folder = os.path.join(os.path.dirname(__file__),'templates') +print(template_folder) env = jinja2.Environment( loader=jinja2.FileSystemLoader(template_folder), enable_async=True diff --git a/shebot/webServiceManager/readme.txt b/shebot/webServiceManager/readme.txt new file mode 100644 index 0000000..ac3d822 --- /dev/null +++ b/shebot/webServiceManager/readme.txt @@ -0,0 +1,11 @@ +本插件为hoshinoV2的服务管理插件,提供web控制各个群的各项服务启(禁)用,并支持一键启(禁)用某群所有服务,或者一键启(禁)用所有群某服务 + +安装使用: +修改view.py里公网ip和密码 +私聊bot发送bot设置 + +注意事项: +hoshino config文件夹__bot__.py中的HOST请设置为 0.0.0.0以开放公网访问 +请注意服务器放行端口 +请设置好密码,开放公网访问带来的任何安全问题与本人无关 + diff --git a/shebot/webServiceManager/templates/by_group.html b/shebot/webServiceManager/templates/by_group.html new file mode 100644 index 0000000..fc1d563 --- /dev/null +++ b/shebot/webServiceManager/templates/by_group.html @@ -0,0 +1,43 @@ + + + 机器人设置 + + + + + +

按群管理

+
+ +
+ \ No newline at end of file diff --git a/shebot/webServiceManager/templates/by_service.html b/shebot/webServiceManager/templates/by_service.html new file mode 100644 index 0000000..90cd980 --- /dev/null +++ b/shebot/webServiceManager/templates/by_service.html @@ -0,0 +1,44 @@ + + + 机器人设置 + + + + + +

按服务管理

+
+
    + {% for item in items %} +
  • + +
  • + {% endfor %} +
+
+ \ No newline at end of file diff --git a/shebot/webServiceManager/templates/group_services.html b/shebot/webServiceManager/templates/group_services.html new file mode 100644 index 0000000..5ea5183 --- /dev/null +++ b/shebot/webServiceManager/templates/group_services.html @@ -0,0 +1,125 @@ + + + + 机器人设置 + + + + + + +
+

群{{ group_id }}服务一览

+
+
    + {% for key in conf[group_id] %} +
  • +
    + {{ key }} + +
    +
  • + {% endfor %} +
    + + + + + +
    +
+ + + \ No newline at end of file diff --git a/shebot/web4reply/templates/login.html b/shebot/webServiceManager/templates/login.html similarity index 97% rename from shebot/web4reply/templates/login.html rename to shebot/webServiceManager/templates/login.html index 83286ae..e17377d 100644 --- a/shebot/web4reply/templates/login.html +++ b/shebot/webServiceManager/templates/login.html @@ -18,6 +18,6 @@ - + \ No newline at end of file diff --git a/shebot/webServiceManager/templates/main.html b/shebot/webServiceManager/templates/main.html new file mode 100644 index 0000000..946bf08 --- /dev/null +++ b/shebot/webServiceManager/templates/main.html @@ -0,0 +1,27 @@ + + + + + + 机器人设置 + + + +

设置主页

+
+ 按群管理 +
+
+
+ 按插件管理 +
+ + \ No newline at end of file diff --git a/shebot/webServiceManager/templates/service_groups.html b/shebot/webServiceManager/templates/service_groups.html new file mode 100644 index 0000000..17547cd --- /dev/null +++ b/shebot/webServiceManager/templates/service_groups.html @@ -0,0 +1,134 @@ + + + + + + 机器人设置 + + + +
+

{{ sv_name }}

+
+
+
    + {% for group in groups %} +
  • +
    + + {{ group["group_id"] }}{{ group["group_name"] }} + + +
    +
    +
    +
  • + {% endfor %} +   +
    + + + +
    +
+
+ + + + \ No newline at end of file diff --git a/shebot/webServiceManager/view.py b/shebot/webServiceManager/view.py new file mode 100644 index 0000000..b283323 --- /dev/null +++ b/shebot/webServiceManager/view.py @@ -0,0 +1,113 @@ +import nonebot +import json + +from datetime import timedelta +from hoshino import Service +from quart import request,session,redirect,Blueprint +from .data_source import render_template,get_random_str + +switcher = Blueprint('switcher',__name__) +bot = nonebot.get_bot() +app = bot.server_app +if not app.config.get('SECRET_KEY'): + app.config['SECRET_KEY'] = get_random_str(10) + +public_address = '127.0.0.1' #改为你服务器的公网ip,域名应该也可以,我没试过 +port = bot.config.PORT +passwd = '123456' #登录密码 + +@switcher.before_request +async def _(): + user_ip = request.remote_addr + if request.path == '/login': + return + if request.path == '/check': + return + if session.get('user_ip') == user_ip: + return + return redirect('/login') + +@switcher.route('/login',methods=['GET','POST']) +async def login(): + print(request.method) + if request.method == 'GET': + return await render_template('login.html',passwd=passwd,public_address=public_address,port=port) + else: + login_data = await request.form + input_psd = login_data.get('password') + if input_psd == passwd: + user_ip = request.remote_addr + session['user_ip'] = user_ip + session.permanent = True + app.permanent_session_lifetime = timedelta(weeks=2) + return redirect('/manager') + else: + return redirect('/login') + +@switcher.route('/manager') +async def manager(): + return await render_template('main.html',public_address=public_address,port=port) + +@switcher.route('/group') +async def test(): + groups = await get_groups() + return await render_template('by_group.html',items=groups,public_address=public_address,port=port) + +@switcher.route('/service') +async def show_all_services(): + svs = Service.get_loaded_services() + sv_names = list(svs) + return await render_template('by_service.html',items=sv_names,public_address=public_address,port=port) + +@switcher.route('/group/') +async def show_group_services(gid_str:str): + gid = int(gid_str) + svs = Service.get_loaded_services() + conf = {} + conf[gid_str] = {} + for key in svs: + conf[gid_str][key] = svs[key].check_enabled(gid) + return await render_template('group_services.html',group_id=gid_str,conf=conf,public_address=public_address,port=port) + +@switcher.route('/service/') +async def show_service_groups(sv_name:str): + svs = Service.get_loaded_services() + groups = await get_groups() + conf = {} + for group in groups : + gid = group['group_id'] + gid_str = str(gid) + conf[gid_str] = {} + if svs[sv_name].check_enabled(gid): + conf[gid_str][sv_name] = True + else: + conf[gid_str][sv_name] = False + return await render_template('service_groups.html',sv_name=sv_name,conf=conf,groups=groups,public_address=public_address,port=port) + +async def get_groups(): + return await bot.get_group_list() + +@switcher.route('/set/',methods=['GET','POST']) +async def set_group(): + #接收前端传来的配置数据,数据格式{"":{'serviceA':True,'serviceB':False}} + if request.method == 'POST': + data = await request.get_data() + conf = json.loads(data.decode()) + svs = Service.get_loaded_services() + for k in conf: + gid = int(k) + for sv_name in conf[k]: + if conf[k][sv_name]: + svs[sv_name].set_enable(gid) + svs[sv_name].logger.info(f'启用群 {gid} 服务 {sv_name} ') + else: + svs[sv_name].set_disable(gid) + svs[sv_name].logger.info(f'禁用群 {gid} 服务 {sv_name}') + return 'ok' + + +@bot.on_message('private') +async def setting(ctx): + message = ctx['raw_message'] + if message in ['服务管理', 'bot设置']: + await bot.send(ctx,f'http://{public_address}:{port}/manager',at_sender=False) diff --git a/util4sh.py b/util4sh.py deleted file mode 100644 index b0683c8..0000000 --- a/util4sh.py +++ /dev/null @@ -1,197 +0,0 @@ -from traceback import print_exc -from nonebot.message import MessageSegment, Message -from aiocqhttp.event import Event -from os import path -from hoshino.log import new_logger -from typing import Callable -import re -import random -import aiohttp -import filetype -import os - -logger = new_logger('shebot') - -async def download_async(url: str, save_path: str, save_name: str, suffix=None) -> None: - timeout = aiohttp.ClientTimeout(total=30) - async with aiohttp.ClientSession(timeout=timeout) as session: - async with session.get(url) as resp: - content = await resp.read() - if not suffix: #没有指定后缀,自动识别后缀名 - try: - suffix = filetype.guess_mime(content).split('/')[1] - except: - raise ValueError('不是有效文件类型') - abs_path = path.join(save_path, f'{save_name}.{suffix}') - with open(abs_path, 'wb') as f: - f.write(content) - return abs_path - -def get_random_file(path) -> str: - files = os.listdir(path) - rfile = random.choice(files) - return rfile - -import hashlib -def get_str_md5(text: str) -> str: - m = hashlib.md5() - m.update(text.encode('utf-8')) - md5_str = m.hexdigest() - return md5_str - -class Res: - res_dir = path.join(path.dirname(__file__), 'modules','shebot', 'res') - image_dir = path.join(res_dir, 'image') - record_dir = path.join(res_dir, 'record') - @classmethod - def image(cls, pic_path: str) -> 'MessageSegment': - return MessageSegment.image(f'file:///{path.join(cls.image_dir, pic_path)}') - - @classmethod - def record(cls, rec_path) -> 'MessageSegment': - return MessageSegment.record(f'file:///{path.join(cls.record_dir, rec_path)}') - - @classmethod - async def save_image(cls, event: Event, folder=None) -> None: - if not folder: - image_path = cls.image_dir - else: - image_path = path.join(cls.image_dir, folder) - if not path.isdir(image_path): - os.mkdir(image_path) - for i, m in enumerate(event.message): - match = re.match('\[CQ:image.+?\]', str(m)) - if match: - try: - url = re.findall(r'http.*?term=\d', str(m))[0] - save_name = re.findall(r'(?<=-)[^-]*?(?=/)',url)[0] - image = await download_async(url, image_path, save_name) - event.message[i] = MessageSegment.image(f'file:///{image}') - except Exception as ex: - print_exc() - event.raw_message = str(event.message) - - @classmethod - def get_random_image(cls, folder=None) -> 'MessageSegment': - if not folder: - image_path = cls.image_dir - else: - image_path = path.join(cls.image_dir, folder) - image_name = get_random_file(image_path) - return MessageSegment.image(f'file:///{path.join(image_path, image_name)}') - - @classmethod - async def image_from_url(cls, url: str, cache=True) -> 'MessageSegment': - fname = get_str_md5(url) - image = path.join(cls.image_dir, f'{fname}.jpg') - if not path.exists(image) or not cache: - image = await download_async(url, cls.image_dir, fname, 'jpg') - return MessageSegment.image(f'file://{image}') - -from nonebot import scheduler -import datetime -import nonebot -bot = nonebot.get_bot() -def add_delay_job(task,id=None,delay_time:int=30,args=[]): - now = datetime.datetime.now() - job = scheduler.add_job(task,'date',id=id,run_date=now+datetime.timedelta(seconds=delay_time),misfire_grace_time=5,args=args) - return job - -def add_date_job(task,id=None,run_date=None,args=[]): - job = scheduler.add_job(task,'date',id=id,run_date=run_date,args=args) - return job - -def add_cron_job(task,id=None,hour='*',minute='0',second='0',args=[]): - job = scheduler.add_job(task,'cron',id=id,hour=hour,minute=minute,second=second,misfire_grace_time=5,args=args) - return job - -import json -def save_config(config:dict,path:str): - try: - with open(path,'w',encoding='utf8') as f: - json.dump(config, f, ensure_ascii=False, indent=2) - return True - except Exception as ex: - logger.error(ex) - return False - -def load_config(path): - try: - with open(path, mode='r', encoding='utf-8') as f: - config = json.load(f) - return config - except Exception as ex: - logger.error(f'exception occured when loading config in {path} {ex}') - logger.exception(ex) - return {} - -import asyncio -from hoshino.service import Service -async def broadcast(msg,groups=None,sv_name=None): - bot = nonebot.get_bot() - #当groups指定时,在groups中广播;当groups未指定,但sv_name指定,将在开启该服务的群广播 - svs = Service.get_loaded_services() - if not groups and sv_name not in svs: - raise ValueError(f'不存在服务 {sv_name}') - if not groups: - enable_groups = await svs[sv_name].get_enable_groups() - send_groups = enable_groups.keys() - else: - send_groups = groups - - for gid in send_groups: - try: - await bot.send_group_msg(group_id=gid,message=msg) - logger.info(f'群{gid}投递消息成功') - await asyncio.sleep(0.5) - except: - logger.error(f'在群{gid}投递消息失败') - -class RSS(): - def __init__(self): - self.base_url = 'http://112.74.76.48:1200' - self.route :str= None - self.xml : bytes = None - self.filter : dict = dict() - self.filterout :dict = dict() #out为过滤掉 - ''' - filter 选出想要的内容 - filter: 过滤标题和描述 - filter_title: 过滤标题 - filter_description: 过滤描述 - filter_author: 过滤作者 - filter_time: 过滤时间,仅支持数字,单位为秒。返回指定时间范围内的内容。如果条目没有输出pubDate或者格式不正确将不会被过滤 - ''' - self.filter_case_sensitive = True #过滤是否区分大小写,默认区分大小写 - self.limit = 10 #限制最大条数,主要用于排行榜类 RSS - - async def get(self): - url = self.base_url + self.route - params = {} - for key in self.filter: - if self.filter[key]: - params[key] = self.filter[key] - for key in self.filterout: - if self.filterout[key]: - params[key] = self.filterout[key] - params['limit'] = self.limit - async with aiohttp.ClientSession() as session: - async with session.get(url,params=params) as resp: - self.xml = await resp.read() - - def parse_xml(self): - #在实现类中编写解析xml函数 - raise NotImplementedError - -from hoshino.modules.priconne.cherugo import cheru2str -def cherugo_also(func: Callable) -> Callable: - async def deco(*args): - bot, event, _ = tuple(args) - await func(bot, event, _) - raw_message = event.raw_message - cherugo_decode = cheru2str(raw_message) - event.raw_message = cherugo_decode - event.message.clear() - event.message.append(MessageSegment.text(cherugo_decode)) - await func(bot, event, _) - return deco \ No newline at end of file