diff options
| author | 2024-02-26 13:21:50 +0800 | |
|---|---|---|
| committer | 2024-02-26 13:21:50 +0800 | |
| commit | e6e9453a024dff943ea52b4fb588f2cf7fec509b (patch) | |
| tree | 966fb87d7f01d3c916ef3fe66ec14897ec3b0069 /examples | |
| parent | 3eb819d7c48020df35336e141c38ab4d9926c21e (diff) | |
| download | HydroRoll-e6e9453a024dff943ea52b4fb588f2cf7fec509b.tar.gz HydroRoll-e6e9453a024dff943ea52b4fb588f2cf7fec509b.zip | |
refactor(command): 命令词法解析器 (#82)
* refactor(command): 重构命令路由
* feat(lua): 包装异步方法`self.event.reply` => `msg:echo`
* feat(lua): 包装异步输入流方法`self.event.ask` => `msg:ask`
* 'Refactored by Sourcery' (#83)
Co-authored-by: Sourcery AI <>
* feat(Token|Lexer): 添加`Token`与`Lexer`类
* refactor(Lexer): 词法分析器添加`advance`方法
* chore: lint code
* refactor: sync gensokyo adapter
* feat: Cli parser (#85)
* feat(cli): 添加`Cli`类,解析命令行参数
* fix: 修复错误的`dest`与`action`
* feat(cli): 实现`install_package` 与 `build_template`
* feat(cli): 实现`-c|--config`指令配置镜像常量等
* feat(cli): 使用高效率的异步网络库`aiohttp`
* fix(cli): `TYPE_CHECKING` with partially module `typing`
* refactor!: examples, tests, src... rewrite in rust
---------
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Diffstat (limited to 'examples')
30 files changed, 1492 insertions, 0 deletions
diff --git a/examples/HydroRoll/data/censor.json b/examples/HydroRoll/data/censor.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/examples/HydroRoll/data/censor.json @@ -0,0 +1 @@ +{}
\ No newline at end of file diff --git a/examples/HydroRoll/data/reply.psi b/examples/HydroRoll/data/reply.psi new file mode 100644 index 0000000..fdb5ee6 --- /dev/null +++ b/examples/HydroRoll/data/reply.psi @@ -0,0 +1,9 @@ +""" +{ + ".core": "core", +} +""" + +func main(){ + +}
\ No newline at end of file diff --git a/examples/config.example.toml b/examples/config.example.toml new file mode 100644 index 0000000..68ddef6 --- /dev/null +++ b/examples/config.example.toml @@ -0,0 +1,18 @@ +[bot] +plugins = [] +plugin_dirs = ["plugins"] +adapters = ["iamai.adapter.cqhttp","iamai.adapter.apscheduler"] + +[bot.log] +level = "DEBUG" +verbose_exception = true + +[adapter.cqhttp] +adapter_type = "reverse-ws" +host = "127.0.0.1" +port = 15800 +url = "/cqhttp/ws" +show_raw = false + +[adapter.apscheduler] +scheduler_config = { "apscheduler.timezone" = "Asia/Shanghai" }
\ No newline at end of file diff --git a/examples/config.toml b/examples/config.toml new file mode 100644 index 0000000..debfa20 --- /dev/null +++ b/examples/config.toml @@ -0,0 +1,45 @@ +[bot] +plugins = [] +plugin_dirs = ["plugins"] +rules = [] +rule_dirs = ["rules"] +adapters = [ + "iamai.adapter.onebot11", + # "iamai.adapter.gensokyo", + # "iamai.adapter.apscheduler", + # "iamai.adapter.dingtalk" +] + +[bot.log] +level = "INFO" +verbose_exception = true + +[adapter.onebot11] +adapter_type = "reverse-ws" +host = "127.0.0.1" +port = 8080 +url = "/cqhttp/ws" +show_raw = true + +[adapter.gensokyo] +adapter_type = "reverse-ws" +host = "127.0.0.1" +port = 8081 +url = "/gsk/ws" +show_raw = true + +[adapter.dingtalk] +adapter_type = "stream" +host = "127.0.0.1" +port = 15700 +url = "/dingtalk" +app_secret = "FnQU_a88xRpmcs3oPNXSgoQgm4TidGduVqKhLHR7_NgF6MLBUUbwYdE6MkOFWZFb" +app_key = "dingo7xu5djthkxpoick" + +[adapter.apscheduler] +scheduler_config = { "apscheduler.timezone" = "Asia/Shanghai" } + +[plugin.HydroRoll] +uid = '' +rules = [] +rule_dirs = ["rules"]
\ No newline at end of file diff --git a/examples/main.py b/examples/main.py new file mode 100644 index 0000000..211b3d3 --- /dev/null +++ b/examples/main.py @@ -0,0 +1,8 @@ +from iamai import Bot + +bot = Bot(hot_reload=True) + +if __name__ == "__main__": + bot.run() + + diff --git a/examples/plugins/HydroRoll/__init__.py b/examples/plugins/HydroRoll/__init__.py new file mode 100644 index 0000000..8a5cd74 --- /dev/null +++ b/examples/plugins/HydroRoll/__init__.py @@ -0,0 +1,151 @@ +"""中间件""" +import re +import json +import joblib +import os +import shutil + +import oneroll +from iamai import ConfigModel, Plugin +from iamai.log import logger +from iamai.exceptions import GetEventTimeout +from iamai.event import MessageEvent, Event +from iamai.typing import StateT + +from .config import Directory, GlobalConfig, Models +from .utils import * +from .models.Transformer import query + +from .config import ( + BasePluginConfig, + CommandPluginConfig, + RegexPluginConfig, + GlobalConfig, +) + +from ast import literal_eval +from os.path import dirname, join, abspath +from abc import ABC, abstractmethod +from typing import Any, Generic, TypeVar +from typing_extensions import Annotated + +ConfigT = TypeVar("ConfigT", bound=BasePluginConfig) +RegexPluginConfigT = TypeVar("RegexPluginConfigT", bound=RegexPluginConfig) +CommandPluginConfigT = TypeVar("CommandPluginConfigT", bound=CommandPluginConfig) + + +BASE_DIR = dirname(abspath("__file__")) +HYDRO_DIR = dirname(abspath(__file__)) +APP_DIR = join(BASE_DIR, "HydroRoll") + +# logger.info(GlobalConfig._copyright) + + +class Dice(Plugin[MessageEvent, Annotated[dict, {}], RegexPluginConfig]): + """中间件""" + + priority = 0 + + # TODO: infini should be able to handle all signals and tokens from Psi. + logger.info("Loading infini...") + + def __post_init__(self): + self.state = {} + self.model_path_list = [] + self.bot.global_state["HydroRoll"] = {} + self.model_dict = Models().get_models_dict() + + self.model_path_list.append(join(BASE_DIR, "models")) + self.model_path_list.append(join(HYDRO_DIR, "models")) + self.model_path_list.append(join(BASE_DIR, "HydroRoll", "models")) + + self.load_models() + + async def handle(self) -> None: + """ + @TODO: infini should be able to handle all signals and tokens from Psi. + @BODY: infini actives the rule-packages. + """ + global flag + + args = self.event.get_plain_text().split(" ") + command_list = ["/r", ".root", ".roots", ".core", ".set", ".get", ".test"] + current_cmd = args[0] + text = ( + self.event.get_plain_text()[2:] + if len(self.event.get_plain_text()) >= 2 + else None + ) + flag = True in [cmd.startswith(current_cmd) for cmd in command_list] + # logger.info(f"Command {current_cmd} not found with flag {flag}") + logger.info(f"text: {text}") + if text and self.event.get_plain_text().startswith("/r"): + logger.info(text) + try: + await self.event.reply(f"{oneroll.roll(text)}") + except Exception as e: + await self.event.reply(f"{e!r}") + if args[0] in [".root", ".roots"]: + try: + import aiohttp + + async with aiohttp.ClientSession() as session: + async with session.get( + "https://api.hydroroll.team/api/roots" + ) as response: + data = await response.json() + await self.event.reply(data["line"]) + except Exception as e: + await self.event.reply(f"{e!r}") + elif args[0] == ".core": + await self.event.reply(f"{self.state}") + # if args[0].startswith(".set"): + # resolve = Set(args[1:]) # TODO: handle multiple sets + # elif args[0].startswith(".get"): + # resolve = Get(args[1:]) # TODO: handle multiple gets + elif args[0].startswith(".test"): + try: + result = eval(self.event.message.get_plain_text()[5:]) + await self.event.reply(str(result)) + except Exception as error: + await self.event.reply(f"{error!r}") + + async def rule(self) -> bool: + """ + @TODO: Psi should be able to handle all message first. + @BODY: lexer module will return a list of tokens, parser module will parse the tokens into a tree, and executor module will execute the tokens with a stack with a bool return value. + """ + logger.info("loading psi...") + return isinstance(self.event, MessageEvent) + + def _init_directory(self, _prefix: str = ""): + """初始化水系目录""" + for _ in Directory(BASE_DIR).get_dice_dir_list(_prefix): + if not os.path.exists(_): + os.makedirs(_) + + def _init_file(self, _prefix: str = ""): + """初始化文件""" + + def init_directory(self, _prefix: str = "HydroRoll"): + """在指定目录生成水系文件结构""" + self._init_directory(_prefix=_prefix) + + def _load_model(self, path: str, model_file: str): + if model_file is None: + model_file = "" + return joblib.load(join(path, f"{model_file}")) + + def _load_models(self, model_path_list: list, model_dict: dict) -> dict: + """加载指定模型, 当然也可能是数据集""" + models = {} + for path in model_path_list: + for model_name, model_file in model_dict.items(): + if os.path.exists(join(path, model_file)): + models[model_name] = self._load_model(path, model_file) + logger.success(f'Succeeded to load model "{model_name}"') + return models + + def load_models(self): + """我想睡觉, 但我失眠了。""" + self.models = self._load_models(self.model_path_list, self.model_dict) diff --git a/examples/plugins/HydroRoll/command/ROUTES b/examples/plugins/HydroRoll/command/ROUTES new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/examples/plugins/HydroRoll/command/ROUTES diff --git a/examples/plugins/HydroRoll/command/__init__.py b/examples/plugins/HydroRoll/command/__init__.py new file mode 100644 index 0000000..ec4be24 --- /dev/null +++ b/examples/plugins/HydroRoll/command/__init__.py @@ -0,0 +1,74 @@ +SET, GET, ALIAS = ("SET", "GET", "ALIAS") +INTEGER = "INTEGER" +EOF = "EOF" + +class Token(object): + def __init__(self, type, value): + self.type = type + self.value = value + + def __str__(self): + return f"Token({self.type}, {self.value}" + + def __repr__(self) -> str: + return self.__str__() + + +class Lexer(object): + def __init__(self, text): + self.text = text + self.pos = 0 + self.current_char = self.text[self.pos] + + def error(self): + raise Exception("Invalid Character") + + def advance(self): + """Advance the `pos` pointer and set the `current_char` variable.""" + self.pos += 1 + if self.pos > len(self.text) - 1: + self.current_char = None # Indicates end of input + else: + self.current_char = self.text[self.pos] + + def skip_whitespace(self): + while self.current_char is not None and self.current_char.isspace(): + self.advance() + + def integer(self): + """Return a (multidigit) integer consumed from the input.""" + result = "" + while self.current_char is not None and self.current_char.isdigit(): + result += self.current_char + self.advance() + return int(result) + + def get_next_token(self): + """Lexical analyzer (also known as scanner or tokenizer) + + This method is responsible for breaking a sentence + apart into tokens. One token at a time. + """ + while self.current_char is not None: + if self.current_char.isspace(): + self.skip_whitespace() + continue + + if self.current_char.isdigit(): + return Token(INTEGER, self.integer()) + + if self.current_char == "get": + self.advance() + return Token(GET, "get") + + if self.current_char == "set": + self.advance() + return Token(SET, "set") + + if self.current_char == "Alias": + self.advance() + return Token(ALIAS, "Alias") + + self.error() + + return Token(EOF, None) diff --git a/examples/plugins/HydroRoll/command/alias_command.py b/examples/plugins/HydroRoll/command/alias_command.py new file mode 100644 index 0000000..462c062 --- /dev/null +++ b/examples/plugins/HydroRoll/command/alias_command.py @@ -0,0 +1,4 @@ +from .typing import CommandBase + +class AliasCommand(CommandBase): + ...
\ No newline at end of file diff --git a/examples/plugins/HydroRoll/command/get_command.py b/examples/plugins/HydroRoll/command/get_command.py new file mode 100644 index 0000000..b8a8a96 --- /dev/null +++ b/examples/plugins/HydroRoll/command/get_command.py @@ -0,0 +1,4 @@ +from .typing import CommandBase + +class GetCommand(CommandBase): + ...
\ No newline at end of file diff --git a/examples/plugins/HydroRoll/command/set_command.py b/examples/plugins/HydroRoll/command/set_command.py new file mode 100644 index 0000000..1f1d4dd --- /dev/null +++ b/examples/plugins/HydroRoll/command/set_command.py @@ -0,0 +1,4 @@ +from .typing import CommandBase + +class SetCommand(CommandBase): + ...
\ No newline at end of file diff --git a/examples/plugins/HydroRoll/command/typing.py b/examples/plugins/HydroRoll/command/typing.py new file mode 100644 index 0000000..b52d4a6 --- /dev/null +++ b/examples/plugins/HydroRoll/command/typing.py @@ -0,0 +1,4 @@ +from pydantic import BaseModel + +class CommandBase(BaseModel): + ...
\ No newline at end of file diff --git a/examples/plugins/HydroRoll/config.py b/examples/plugins/HydroRoll/config.py new file mode 100644 index 0000000..c380a01 --- /dev/null +++ b/examples/plugins/HydroRoll/config.py @@ -0,0 +1,110 @@ +from randomgen import AESCounter +from numpy.random import Generator +import argparse +import sys +from os.path import dirname, dirname, join, abspath +import platform +from importlib.metadata import version +import os +from typing import Set, Optional +from iamai import ConfigModel +import datetime + +from typing import Set + +from pydantic import Field + + +class BasePluginConfig(ConfigModel): + message_str: str = "{message}" + """最终发送消息的格式。""" + + +class RegexPluginConfig(BasePluginConfig): + pass + + +class CommandPluginConfig(RegexPluginConfig): + __config_name__ = "HydroRoll" + command_prefix: Set[str] = Field(default_factory=lambda: {".", "。"}) + """命令前缀。""" + command: Set[str] = Field(default_factory=set) + """命令文本。""" + ignore_case: bool = True + """忽略大小写。""" + + +class Color: + # 定义ANSI转义序列 + RESET = "\033[0m" + BLUE_BASE = "\033[36m" + BLUE_DARK = "\033[34m" + BLUE_DARKER = "\033[32m" + BLUE_DARKEST = "\033[30m" + + +# 定义全局配置类 +class GlobalConfig: + _name = "HydroRoll[水系]" + _version = "0.1.0" + _svn = "2" + _author = "简律纯" + _iamai_version = version("iamai") + _python_ver = sys.version + _python_ver_raw = ".".join(map(str, platform.python_version_tuple()[:3])) + _base_dir = dirname(abspath("__file__")) + _hydro_dir = dirname(abspath(__file__)) + _copyright = f"""\033[36m + _ __ _ _ + /\ /\_ _ __| |_ __ ___ /__\ ___ | | | + / /_/ / | | |/ _` | '__/ _ \ / \/// _ \| | | +/ __ /| |_| | (_| | | | (_) / _ \ (_) | | | +\/ /_/ \__, |\__,_|_| \___/\/ \_/\___/|_|_| + |___/ + +\033[4m{_name} [版本 {_version}]\033[0m\033[36m +(c) HydroRoll-Team contributors, {_author}。 +Github: https://github.com/HydroRoll-Team +Under the MIT License, see LICENSE for more details. +""" + + +class Directory(object): + def __init__(self, _path: str) -> None: + self.current_path = _path + + def get_dice_dir_list(self, _prefix: str) -> list: + return [ + os.path.join(self.current_path, f"{_prefix}", *dirs) + for dirs in [ + ["config"], + ["data"], + ["rules"], + ["scripts", "lua"], + ["scripts", "js"], + ["scripts", "psi"], + ["web", "frontend"], + ["web", "backend"], + ] + ] + + +class FileManager(object): + def __init__(self, _path: str) -> None: + self.current_path = _path + + def get_file_list(self, _dir: str): + return { + "web;frontend": "index.html", + "data": "censor.json", + } + + +class Models: + """模型管理类""" + + def __init__(self) -> None: + self.builtin_models = {"hola": "hola.pkl"} + + def get_models_dict(self) -> dict: + return self.builtin_models diff --git a/examples/plugins/HydroRoll/exceptions.py b/examples/plugins/HydroRoll/exceptions.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/examples/plugins/HydroRoll/exceptions.py diff --git a/examples/plugins/HydroRoll/models/Transformer.py b/examples/plugins/HydroRoll/models/Transformer.py new file mode 100644 index 0000000..b52422e --- /dev/null +++ b/examples/plugins/HydroRoll/models/Transformer.py @@ -0,0 +1,9 @@ +import requests + +API_URL = "https://api-inference.huggingface.co/models/sentence-transformers/all-MiniLM-L6-v2" +headers = {"Authorization": "Bearer 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"} + +def query(payload): + response = requests.post(API_URL, headers=headers, json=payload) + return response.json() + diff --git a/examples/plugins/HydroRoll/models/__init__.py b/examples/plugins/HydroRoll/models/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/examples/plugins/HydroRoll/models/__init__.py diff --git a/examples/plugins/HydroRoll/models/cos_sim.py b/examples/plugins/HydroRoll/models/cos_sim.py new file mode 100644 index 0000000..24b743d --- /dev/null +++ b/examples/plugins/HydroRoll/models/cos_sim.py @@ -0,0 +1,100 @@ +"""余弦相似度比较""" + + +import joblib +import jieba +import numpy as np + +from sklearn.feature_extraction.text import TfidfTransformer +from sklearn.feature_extraction.text import CountVectorizer +from sklearn.metrics.pairwise import cosine_similarity + + +class cosSim: + def __init__(self, simple: list = [], test_data: list = []): + self.simple = simple + self.inputs = test_data + self.texts = self.simple + self.texts.extend(self.inputs) + + @property + def corpuss(self): + return [" ".join(jieba.cut(text)) for text in self.simple] + + @property + def vocabulary(self): + return self.getVocabulary(self.corpuss) + + @property + def vectors(self): + return self.getVectors(self.corpuss, self.vocabulary) + + @property + def input_corpuss(self): + return [" ".join(jieba.cut(text)) for text in self.inputs] + + @property + def input_vocabulary(self): + return self.getVocabulary(self.input_corpuss) + + @property + def input_vector(self): + return self.getVectors(self.input_corpuss, self.input_vocabulary) + + def append(self, add_test_data: list = []): + self.inputs.extend(add_test_data) + + @property + def similarities(self): + similarities = [] + corpuss = [" ".join(jieba.cut(text)) for text in self.texts] + vocabulary = self.getVocabulary(corpuss) + vector = self.getVectors(corpuss, vocabulary) + for v in vector[len(self.texts)-1:]: + sim = [] + for v1 in vector[:len(self.simple)+1]: + sim.append(self.cos_sim(v1, v)) + print('sim', sim) + similarities.append(max(sim)) + + return similarities + + @staticmethod + def cos_sim(vector_a, vector_b): + """ + 计算两个向量之间的余弦相似度 + :param vector_a: 向量 a + :param vector_b: 向量 b + :return: sim + """ + vector_a = np.array(vector_a).reshape(1, -1) + vector_b = np.array(vector_b).reshape(1, -1) + return cosine_similarity(vector_a, vector_b)[0][0] + + @staticmethod + def getVocabulary(corpuss): + vectorizer = CountVectorizer(max_features=500) + transformer = TfidfTransformer() + tfidf = transformer.fit_transform(vectorizer.fit_transform(corpuss)) + words = vectorizer.get_feature_names_out() + return words + + @staticmethod + def getVectors(corpus, vocabulary): + vectorizer = CountVectorizer(vocabulary=vocabulary) + transformer = TfidfTransformer() + tfidf = transformer.fit_transform(vectorizer.fit_transform(corpus)) + vectors = tfidf.toarray() + return vectors + + def save(self, filename): + joblib.dump(self, filename) + + @staticmethod + def load(filename): + return joblib.load(filename) + + def reload(self): + self.texts = self.simple + self.texts.extend(self.inputs) + self.similarities
\ No newline at end of file diff --git a/examples/plugins/HydroRoll/models/hola.py b/examples/plugins/HydroRoll/models/hola.py new file mode 100644 index 0000000..255f5dd --- /dev/null +++ b/examples/plugins/HydroRoll/models/hola.py @@ -0,0 +1,62 @@ +from cos_sim import cosSim +import numpy as np + +texts = [ + "你好 HydroRoll", + "你好 水系", + "水系你好", + "HydroRoll hi~", + "水系, hola" + "你好呀 水系", + "hi 水系", + "hi HydroRoll", + "hello 水系", + "hello HydroRoll", + "hola 水系", + "hola HydroRoll", +] + +# print(model.corpuss) + +# print(model.vocabulary) + + + +model = cosSim( + simple=texts, + test_data=[ + # 'Hi! HydroRoll is a roll system.', + # 'Hello, this is a system which named HydroRoll', + # '短文本匹配技术应用是很广泛的,包括搜索、问答、推荐、计算广告等领域,相关技术也沉淀多年,从无监督方法到有监督方法层出不穷,工业界也是都有应用,短文本匹配算是自然语言处理领域的重要技术了,虽然任务简单,但是想要做好并不是那么容易的事情。', + # '短文本匹配技术在搜索、问答、推荐和计算广告等领域有广泛的应用。这项技术已经发展多年,从无监督方法到有监督方法层出不穷。在工业界,短文本匹配技术已经得到了广泛的应用。虽然短文本匹配任务看起来简单,但要做好并不容易。', + # '你好~水系。', + # 'hola~~~~~~~hola水系!' + ] +) + +# print(model.vectors) + +# print(model.input_vector) + +# print(model.input_vocabulary) + +# print(cosSim.cos_sim(vector_a=model.input_vector[4], vector_b=model.input_vector[5])) + + +print(model.similarities) + +print(model.inputs) + +# model.append(['你好水']) + +# model.append(['你好']) + +print(model.inputs) + +print(model.similarities) + +model.reload() + +print(model.input_corpuss) + +print(model.similarities)
\ No newline at end of file diff --git a/examples/plugins/HydroRoll/utils.py b/examples/plugins/HydroRoll/utils.py new file mode 100644 index 0000000..4c9405c --- /dev/null +++ b/examples/plugins/HydroRoll/utils.py @@ -0,0 +1,170 @@ +import difflib +import re +import time +import random +from abc import ABC, abstractmethod +from typing import Type, Union, Generic, TypeVar +from iamai import Plugin + +import re +from abc import ABC, abstractmethod +from typing import Any, Generic, TypeVar + +from iamai import MessageEvent, Plugin +from iamai.typing import StateT + +from .config import BasePluginConfig, CommandPluginConfig, RegexPluginConfig + +ConfigT = TypeVar("ConfigT", bound=BasePluginConfig) +RegexPluginConfigT = TypeVar("RegexPluginConfigT", bound=RegexPluginConfig) +CommandPluginConfigT = TypeVar("CommandPluginConfigT", bound=CommandPluginConfig) + + +class BasePlugin( + Plugin[MessageEvent[Any], StateT, ConfigT], + ABC, + Generic[StateT, ConfigT], +): + def format_str(self, format_str: str, message_str: str = "") -> str: + return format_str.format( + message=message_str, + user_name=self.get_event_sender_name(), + user_id=self.get_event_sender_id(), + ) + + def get_event_sender_name(self) -> str: + from iamai.adapter.gensokyo.event import MessageEvent as OneBotMessageEvent + + if isinstance(self.event, OneBotMessageEvent): + return self.event.sender.nickname or "" + return "" + + def get_event_sender_id(self) -> str: + from iamai.adapter.gensokyo.event import MessageEvent as OneBotMessageEvent + + if isinstance(self.event, OneBotMessageEvent): + if self.event.sender.user_id is not None: + return str(self.event.sender.user_id) + return "" + return "" + + async def rule(self) -> bool: + return isinstance(self.event, MessageEvent) and self.str_match( + self.event.get_plain_text() + ) + + @abstractmethod + def str_match(self, msg_str: str) -> bool: + raise NotImplementedError + + +class RegexPluginBase(BasePlugin[StateT, RegexPluginConfigT], ABC): + msg_match: re.Match[str] + re_pattern: re.Pattern[str] + + def str_match(self, msg_str: str) -> bool: + msg_str = msg_str.strip() + msg_match = self.re_pattern.fullmatch(msg_str) + if msg_match is None: + return False + self.msg_match = msg_match + return bool(self.msg_match) + + +class CommandPluginBase(RegexPluginBase[StateT, CommandPluginConfigT], ABC): + command_match: re.Match[str] + command_re_pattern: re.Pattern[str] + + def str_match(self, msg_str: str) -> bool: + if not hasattr(self, "re_pattern"): + self.re_pattern = re.compile( + f'[{"".join(self.config.command_prefix)}]' + f'({"|".join(self.config.command)})' + r"\s*(?P<command_args>.*)", + flags=re.I if self.config.ignore_case else 0, + ) + msg_str = msg_str.strip() + msg_match = self.re_pattern.fullmatch(msg_str) + if not msg_match: + return False + self.msg_match = msg_match + command_match = self.re_pattern.fullmatch(self.msg_match.group("command_args")) + if not command_match: + return False + self.command_match = command_match + return True + + +class PseudoRandomGenerator: + """线性同余法随机数生成器""" + + def __init__(self, seed): + self.seed = seed + + def generate(self): + while True: + self.seed = (self.seed * 1103515245 + 12345) % (2**31) + yield self.seed + + +class HydroDice: + """水系掷骰组件 + + 一些 API 相关的工具函数 + + """ + + def __init__(self, seed): + self.generator = PseudoRandomGenerator(seed) + + def roll_dice( + self, + _counts: int | str, + _sides: int | str, + is_reversed: bool = False, + streamline: bool = False, + threshold: int | str = 5, + ) -> str: + """普通掷骰 + Args: + _counts (int | str): 掷骰个数. + _sides (int | str): 每个骰子的面数. + is_reversed (bool, optional): 倒序输出. Defaults to False. + streamline (bool, optional): 忽略过程. Defaults to False. + threshold (int | str, optional): streamline 的阈值. Defaults to 5. + + Returns: + str: 表达式结果. + """ + rolls = [] + for _ in range(int(_counts)): + roll = next(self.generator.generate()) % _sides + 1 + rolls.append(roll) + total = sum(rolls) + + if streamline: + return str(total) + if len(rolls) > int(threshold): + return str(total) + rolls_str = " + ".join(str(r) for r in rolls) + return f"{total} = {rolls_str}" if is_reversed else f"{rolls_str} = {total}" + + +def find_max_similarity(input_string, string_list): + """寻找最大的相似度""" + max_similarity = 0 + max_string = "" + + for string in string_list: + similarity = difflib.SequenceMatcher(None, input_string, string).quick_ratio() + if similarity > max_similarity: + max_similarity = similarity + max_string = string + + return max_string, max_similarity + + +def check_file(filename: str) -> bool: + """根据给定参数校验文件夹内文件完整性""" + + return False diff --git a/examples/plugins/_bradge-kook-cqhttp.py b/examples/plugins/_bradge-kook-cqhttp.py new file mode 100644 index 0000000..4b2712d --- /dev/null +++ b/examples/plugins/_bradge-kook-cqhttp.py @@ -0,0 +1,25 @@ +from iamai import Plugin + + +class Bradge(Plugin): + async def handle(self) -> None: + if self.event.adapter.name == "kook": + await self.bot.get_adapter("cqhttp").call_api( + "send_group_msg", + group_id=971050440, + message=f"[{self.event.adapter.name} - {self.event.extra.author.username}]\n{self.event.message}" + ) + elif self.event.adapter.name == "cqhttp": + if self.event.group_id == 971050440: + await self.bot.get_adapter("kook").call_api( + api="message/create", + target_id=1661426334688259, + content=f"[{self.event.adapter.name} - {self.event.sender.nickname}]\n{self.event.message}" + ) + + async def rule(self) -> bool: + if self.event.adapter.name not in ["cqhttp","kook"]: + return False + if self.event.type not in ["message","9",9]: + return False + return True
\ No newline at end of file diff --git a/examples/plugins/cachetool.py b/examples/plugins/cachetool.py new file mode 100644 index 0000000..77885d2 --- /dev/null +++ b/examples/plugins/cachetool.py @@ -0,0 +1,33 @@ +from cachetools import cached +import time + +from iamai import Plugin +from iamai.event import MessageEvent + + +class CachedPlugin(Plugin): + async def handle(self) -> None: + # without cached + def fib(n): + return n if n < 2 else fib(n - 1) + fib(n - 2) + + s = time.time() + await self.event.reply(f"{fib(36)}") + await self.event.reply(f"Time Taken: {time.time() - s}") + + # Now using cached + s = time.time() + + # Use this decorator to enable caching + @cached(cache={}) + def fib(n): + return n if n < 2 else fib(n - 1) + fib(n - 2) + + await self.event.reply(f"{fib(36)}") + await self.event.reply(f"Time Taken(cached): {time.time() - s}") + + async def rule(self) -> bool: + return ( + isinstance(self.event, MessageEvent) + and self.event.get_plain_text() == ".cachetools" + ) diff --git a/examples/plugins/draftbottles/__init__.py b/examples/plugins/draftbottles/__init__.py new file mode 100644 index 0000000..1a71f1d --- /dev/null +++ b/examples/plugins/draftbottles/__init__.py @@ -0,0 +1,48 @@ +from typing import Union +from iamai import Plugin, Event, Depends +from iamai.log import logger +from .config import Config +from iamai.event import MessageEvent +from .database import Database +from .permission import Permission +from .workroutes import WorkRoutes +from .inspector import Inspector + + +class Bottles(Plugin, config=Config): + database: Database = Depends() + permission: Permission = Depends() + workroutes: WorkRoutes = Depends() + inspector: Inspector = Depends() + + def __init__(self): + self.text = None + self.prefix = None + self.suffix = None + + async def handle(self) -> None: + self.namespace = next( + ( + key + for key, value in self.config.command_list.items() + if value == self.prefix + ), + "", + ) + if method := getattr(self.inspector, self.namespace, None): + result = await method(self.suffix, self.config) + if result: + await self.event.reply(result) + + async def rule(self) -> bool: + if not isinstance(self.event, MessageEvent): + return False + if not self.permission.is_admin(): + return False + self.text = self.event.get_plain_text() + for prefix in list(self.config.command_list.values()): + if self.text.startswith(prefix): + self.prefix = prefix + self.suffix = self.text[len(self.prefix) + 1 :] + return True + return False diff --git a/examples/plugins/draftbottles/config.py b/examples/plugins/draftbottles/config.py new file mode 100644 index 0000000..f5aaa72 --- /dev/null +++ b/examples/plugins/draftbottles/config.py @@ -0,0 +1,52 @@ +from iamai import ConfigModel + + +class Config(ConfigModel): + __config_name__ = "draft_bottles" + + usage: str = """\ + 指令: + 扔漂流瓶 [文本/图片] + 捡漂流瓶 + 查看漂流瓶 [漂流瓶编号] + 点赞漂流瓶 [漂流瓶编号] + 评论漂流瓶 [漂流瓶编号] [文本] + 举报漂流瓶 [漂流瓶编号] + 删除漂流瓶 [漂流瓶编号] + 我的漂流瓶 + SUPERUSER指令: + 清空漂流瓶 + 恢复漂流瓶 [漂流瓶编号] + 删除漂流瓶评论 [漂流瓶编号] [QQ号] + 漂流瓶白名单 [QQ / 群聊 / 举报] [QQ号 / 群号] + 漂流瓶黑名单 [QQ / 群聊] [QQ号 / 群号] + 漂流瓶详情 [漂流瓶编号] + """.strip() + + command_list: dict = { + "test": "/dfb", + "throw": "扔漂流瓶", + "get": "捡漂流瓶", + "report": "举报漂流瓶", + "comment": "评论漂流瓶", + "check": "查看漂流瓶", + "remove": "删除漂流瓶", + "listb": "我的漂流瓶", + "like": "点赞漂流瓶", + "resume": "恢复漂流瓶", + "clear": "清空漂流瓶", + "delete": "删除漂流瓶评论", + "details": "漂流瓶详情", + } + + ban_list: dict = { + "groups": [], + "users": [], + } + + white_list: dict = { + "groups": [], + "users": [], + } + + max_content_length: int = 1024 diff --git a/examples/plugins/draftbottles/database.py b/examples/plugins/draftbottles/database.py new file mode 100644 index 0000000..dededac --- /dev/null +++ b/examples/plugins/draftbottles/database.py @@ -0,0 +1,11 @@ +class Database: + admin_list: list = [2753364619] + + def __init__(self) -> None: + ... + + def connect(self): + ... + + def close(self): + ... diff --git a/examples/plugins/draftbottles/inspector.py b/examples/plugins/draftbottles/inspector.py new file mode 100644 index 0000000..de757b2 --- /dev/null +++ b/examples/plugins/draftbottles/inspector.py @@ -0,0 +1,128 @@ +from iamai import Event, Depends, Bot +from .database import Database +from .permission import Permission +from .workroutes import WorkRoutes +from .config import Config +from iamai.exceptions import GetEventTimeout +from iamai.adapter.onebot11.message import CQHTTPMessageSegment as ms +import oneroll + +class Inspector: + event: Event = Depends() + bot: Bot = Depends() + database: Database = Depends() + permission: Permission = Depends() + workroutes: WorkRoutes = Depends() + + async def test(self, *args): + suffix = list(args)[0] + a = "1" + try: + return f"{eval(suffix)}" + except Exception as e: + return f"{e!r}" + + async def throw(self, *args): + suffix = list(args)[0] + config = list(args)[1] + if len(suffix) == 0: + """没有内容,则进入输入流""" + try: + content_event = await self.event.ask( + "在漂流瓶中要写下什么呢?(输入“取消”来取消扔漂流瓶操作。)", timeout=10 + ) # type: ignore + except GetEventTimeout: + return "超时。" + except Exception as e: + return f"{e!r}" + else: + if content_event.message.get_plain_text().lower() in ["取消", "cancel"]: + return ms.reply(content_event.message_id) + ms.text("已取消扔漂流瓶操作。") + """有内容,进行审核""" + content = content_event.message.get_plain_text() + self._throw(content=content, event=content_event) + else: + """有内容,进行审核""" + self._throw(content=suffix, event=self.event) + + async def get(self, *args): + suffix = list(args)[0] + try: + return f"{eval(suffix)}" + except Exception as e: + return f"{e!r}" + + async def report(self, *args): + suffix = list(args)[0] + try: + return f"{eval(suffix)}" + except Exception as e: + return f"{e!r}" + + async def comment(self, *args): + suffix = list(args)[0] + try: + return f"{eval(suffix)}" + except Exception as e: + return f"{e!r}" + + async def check(self, *args): + suffix = list(args)[0] + try: + return f"{eval(suffix)}" + except Exception as e: + return f"{e!r}" + + async def remove(self, *args): + suffix = list(args)[0] + try: + return f"{eval(suffix)}" + except Exception as e: + return f"{e!r}" + + async def listb(self, *args): + suffix = list(args)[0] + try: + return f"{eval(suffix)}" + except Exception as e: + return f"{e!r}" + + async def like(self, *args): + suffix = list(args)[0] + try: + return f"{eval(suffix)}" + except Exception as e: + return f"{e!r}" + + async def resume(self, *args): + suffix = list(args)[0] + try: + return f"{eval(suffix)}" + except Exception as e: + return f"{e!r}" + + async def clear(self, *args): + suffix = list(args)[0] + try: + return f"{eval(suffix)}" + except Exception as e: + return f"{e!r}" + + async def delete(self, *args): + suffix = list(args)[0] + try: + return f"{eval(suffix)}" + except Exception as e: + return f"{e!r}" + + async def details(self, *args): + suffix = list(args)[0] + try: + return f"{eval(suffix)}" + except Exception as e: + return f"{e!r}" + + @staticmethod + def _throw(content: str, **kwargs): + """扔出漂流瓶""" + event = kwargs.pop('event', None) diff --git a/examples/plugins/draftbottles/permission.py b/examples/plugins/draftbottles/permission.py new file mode 100644 index 0000000..456dc8b --- /dev/null +++ b/examples/plugins/draftbottles/permission.py @@ -0,0 +1,10 @@ +from iamai import Event, Depends +from .database import Database + + +class Permission: + event: Event = Depends() + database: Database = Depends() + + def is_admin(self): + return self.event.user_id in self.database.admin_list diff --git a/examples/plugins/draftbottles/workroutes.py b/examples/plugins/draftbottles/workroutes.py new file mode 100644 index 0000000..f539780 --- /dev/null +++ b/examples/plugins/draftbottles/workroutes.py @@ -0,0 +1,9 @@ +from iamai import Event, Depends +from .permission import Permission +from .database import Database + + +class WorkRoutes: + event: Event = Depends() + database: Database = Depends() + permission: Permission = Depends() diff --git a/examples/plugins/lua.py b/examples/plugins/lua.py new file mode 100644 index 0000000..6a36827 --- /dev/null +++ b/examples/plugins/lua.py @@ -0,0 +1,64 @@ +from iamai import Plugin +from iamai.exceptions import GetEventTimeout +from numpy.random import Generator +from iamai.adapter.onebot11.message import CQHTTPMessage, CQHTTPMessageSegment +from iamai.log import logger +from lupa import LuaRuntime +from iamai.utils import sync_func_wrapper +import asyncio + +lua = LuaRuntime(unpack_returned_tuples=True) +ms = CQHTTPMessageSegment + + +class Lua(Plugin): + priority = 1 + prefix = "/lua" + + async def handle(self) -> None: + try: + self.suffix = self.event.message.get_plain_text()[len(self.prefix) + 1 :] + + class msg: + priority = self.priority + prefix = self.prefix + fromMsg = self.event.message + suffix = self.suffix + event = self.event + + def echo(self, message=None): + if not message: + return self.__str__ + + loop = asyncio.get_event_loop() + coro = self.event.reply(message) + asyncio.run_coroutine_threadsafe(coro, loop) + + def ask(self, message=None, timeout=10, **kwargs): + if not message: + return self.__str__ + + loop = asyncio.get_event_loop() + try: + coro = self.event.ask(message, timeout=timeout) + asyncio.run_coroutine_threadsafe(coro, loop) + except GetEventTimeout: + return self.__str__ + else: + coro = self.event.reply(**kwargs) + asyncio.run_coroutine_threadsafe(coro, loop) + + lua.globals().msg = msg + lua.globals().event = self.event + # logger.info(lua.eval(self.suffix)) + if result := lua.eval(self.suffix): + await self.event.reply(result) + except Exception as e: + await self.event.reply(f"ERROR!{e!r}") + logger.info(f"ERROR with message: {e}") + + async def rule(self) -> bool: + return ( + self.event.type == "message" + and self.event.message.get_plain_text().startswith(self.prefix) + ) diff --git a/examples/plugins/nivis.❄ b/examples/plugins/nivis.❄ new file mode 100644 index 0000000..783fdb1 --- /dev/null +++ b/examples/plugins/nivis.❄ @@ -0,0 +1,295 @@ +""" SPI - Simple Pascal Interpreter """ + +############################################################################### +# # +# LEXER # +# # +############################################################################### + +# Token types +# +# EOF (end-of-file) token is used to indicate that +# there is no more input left for lexical analysis +from iamai import Plugin +from HydroRoll.utils import HydroDice + +INTEGER, PLUS, MINUS, MUL, DIV, LPAREN, RPAREN, EOF = ( + "INTEGER", + "PLUS", + "MINUS", + "MUL", + "DIV", + "(", + ")", + "EOF", +) + +DICE = "DICE" + + +class Token(object): + """A single token from the lexer.""" + + def __init__(self, _type, _value): + self.type = _type + self.value = _value + + def __str__(self): + """String representation of the class instance. + + Examples: + Token(INTEGER, 3) + Token(PLUS, '+') + Token(MUL, '*') + """ + return f"Token({self.type}, {repr(self.value)})" + + def __repr__(self): + return self.__str__() + + +class Lexer(object): + """A lexer for the Psi language.""" + + def __init__(self, text): + # client string input, e.g. "4 + 2 * 3 - 6 / 2" + self.text = text + # self.pos is an index into self.text + self.pos = 0 + self.current_char = self.text[self.pos] + + def error(self): + """Raise an exception at the current position.""" + raise ValueError("Invalid character") + + def advance(self): + """Advance the `pos` pointer and set the `current_char` variable.""" + self.pos += 1 + if self.pos > len(self.text) - 1: + self.current_char = None # Indicates end of input + else: + self.current_char = self.text[self.pos] + + def skip_whitespace(self): + while self.current_char is not None and self.current_char.isspace(): + self.advance() + + def integer(self): + """Return a (multidigit) integer consumed from the input.""" + result = "" + while self.current_char is not None and self.current_char.isdigit(): + result += self.current_char + self.advance() + return int(result) + + def get_next_token(self): + """Lexical analyzer (also known as scanner or tokenizer)""" + while self.current_char is not None: + if self.current_char.isspace(): + self.skip_whitespace() + continue + + token_type = { + "+": PLUS, + "-": MINUS, + "d": DICE, + "*": MUL, + "/": DIV, + "(": LPAREN, + ")": RPAREN, + }.get(self.current_char) + + if token_type: + self.advance() + return Token(token_type, self.current_char) + + if self.current_char.isdigit(): + return Token(INTEGER, self.integer()) + + self.error() + + return Token(EOF, None) + + +############################################################################### +# # +# PARSER # +# # +############################################################################### + + +class AST(object): + pass + + +class BinOp(AST): + def __init__(self, left, op, right): + self.left = left + self.token = self.op = op + self.right = right + + +class Num(AST): + def __init__(self, token): + self.token = token + self.value = token.value + + +class UnaryOp(AST): + def __init__(self, op, expr): + self.token = self.op = op + self.expr = expr + + +class Parser(object): + def __init__(self, lexer): + self.lexer = lexer + # set current token to the first token taken from the input + self.current_token = self.lexer.get_next_token() + + def error(self): + raise Exception("Invalid syntax") + + def eat(self, token_type): + # compare the current token type with the passed token + # type and if they match then "eat" the current token + # and assign the next token to the self.current_token, + # otherwise raise an exception. + if self.current_token.type == token_type: + self.current_token = self.lexer.get_next_token() + else: + self.error() + + def factor(self): + """factor : (PLUS | MINUS | DICE) factor | INTEGER | LPAREN expr RPAREN""" + token = self.current_token + if token.type == PLUS: + self.eat(PLUS) + node = UnaryOp(token, self.factor()) + return node + elif token.type == MINUS: + self.eat(MINUS) + node = UnaryOp(token, self.factor()) + return node + elif token.type == DICE: + self.eat(DICE) + left = Num(Token(INTEGER, 1)) # 默认骰子个数为1 + right = self.factor() + node = BinOp(left, token, right) + return node + elif token.type == INTEGER: + self.eat(INTEGER) + return Num(token) + elif token.type == LPAREN: + self.eat(LPAREN) + node = self.expr() + self.eat(RPAREN) + return node + + def term(self): + """term : factor ((MUL | DIV) factor)*""" + node = self.factor() + + while self.current_token.type in (MUL, DIV): + token = self.current_token + if token.type == MUL: + self.eat(MUL) + elif token.type == DIV: + self.eat(DIV) + + node = BinOp(left=node, op=token, right=self.factor()) + + return node + + def expr(self): + """ + expr : term ((PLUS | MINUS) term)* + term : factor ((MUL | DIV) factor)* + factor : (PLUS | MINUS) factor | INTEGER | LPAREN expr RPAREN + """ + node = self.term() + + while self.current_token.type in (PLUS, MINUS): + token = self.current_token + if token.type == PLUS: + self.eat(PLUS) + elif token.type == MINUS: + self.eat(MINUS) + + node = BinOp(left=node, op=token, right=self.term()) + + return node + + def parse(self): + node = self.expr() + if self.current_token.type != EOF: + self.error() + return node + + +############################################################################### +# # +# INTERPRETER # +# # +############################################################################### + + +class NodeVisitor(object): + def visit(self, node): + method_name = "visit_" + type(node).__name__ + visitor = getattr(self, method_name, self.generic_visit) + return visitor(node) + + def generic_visit(self, node): + raise Exception("No visit_{} method".format(type(node).__name__)) + + +class Interpreter(NodeVisitor): + def __init__(self, parser): + self.parser = parser + + def visit_BinOp(self, node): + if node.op.type == PLUS: + return self.visit(node.left) + self.visit(node.right) + elif node.op.type == MINUS: + return self.visit(node.left) - self.visit(node.right) + elif node.op.type == DICE: + return int( + HydroDice(1).roll_dice( + _counts=self.visit(node.left), + _sides=self.visit(node.right), + streamline=True, + ) + ) + elif node.op.type == MUL: + return self.visit(node.left) * self.visit(node.right) + elif node.op.type == DIV: + return self.visit(node.left) // self.visit(node.right) + + def visit_Num(self, node): + return node.value + + def visit_UnaryOp(self, node): + op = node.op.type + if op == PLUS: + return +self.visit(node.expr) + elif op == MINUS: + return -self.visit(node.expr) + + def interpret(self): + tree = self.parser.parse() + if tree is None: + return "" + return self.visit(tree) + + +class Psi(Plugin): + async def handle(self) -> None: + lexer = Lexer(self.event.message.get_plain_text()[4:]) + parser = Parser(lexer) + interpreter = Interpreter(parser) + result = interpreter.interpret() + await self.event.reply(str(result)) + + async def rule(self) -> bool: + return self.event.type == "message" and self.event.message.startswith(".psi") diff --git a/examples/plugins/snow.nivis b/examples/plugins/snow.nivis new file mode 100644 index 0000000..4b31630 --- /dev/null +++ b/examples/plugins/snow.nivis @@ -0,0 +1,44 @@ +from iamai import Plugin +from numpy.random import Generator +from iamai.adapter.onebot11.message import CQHTTPMessage, CQHTTPMessageSegment + +ms = CQHTTPMessageSegment + + +class Exec(Plugin): + priority = 1 + + async def handle(self) -> None: + try: + content = [ + { + "type": "node", + "data": { + "name": f"{self.event.sender.nickname}", + "uin": f"{self.event.sender.user_id}", + "content": [ + { + "type": "text", + "data": { + "text": f"{eval(self.event.message.get_plain_text()[6:])}" + }, + }, + # eval(self.event.message.get_plain_text()[6:]) + ], + }, + } + ] + res = await self.event.adapter.send_group_forward_msg( + group_id=int(self.event.group_id), messages=content + ) + except Exception as e: + # await self.event.reply(f"ERROR!{e!r}") + await self.bot.get_adapter("onebot11").send_guild_channel_msg( + f"{eval(self.event.message.get_plain_text()[6:])}" + ) + + async def rule(self) -> bool: + return ( + self.event.type == "message" + and self.event.message.get_plain_text().startswith(".show") + ) |
