diff --git a/README.md b/README.md index 53bf400..3bab4dd 100644 --- a/README.md +++ b/README.md @@ -15,20 +15,34 @@ ## 示例 +使用命令行: +```shell +# 生成配置文件 +$ entari new +``` +```shell +# 运行 +$ entari +``` + 使用配置文件: ```yaml # config.yml basic: network: - type: ws + host: "127.0.0.1" port: 5140 - path: satori + path: "satori" + ignore_self_message: true log_level: INFO -plugin: - example_plugin: {} - ::echo: {} + prefix: ["/"] +plugins: + ~record_message: true ::auto_reload: - watch_dirs: ["plugins"] + watch_dirs: ["."] + ::echo: true + ::inspect: true ``` ```python @@ -60,8 +74,8 @@ app.run() from arclet.entari import Session, Entari, WS, command @command.on("add {a} {b}") -async def add(a: int, b: int, session: Session): - await session.send(f"{a + b = }") +def add(a: int, b: int): + return f"{a + b = }" app = Entari(WS(port=5500, token="XXX")) diff --git a/arclet/entari/__init__.py b/arclet/entari/__init__.py index 2aff814..c1fae5a 100644 --- a/arclet/entari/__init__.py +++ b/arclet/entari/__init__.py @@ -44,11 +44,7 @@ from .core import Entari as Entari from .event import MessageCreatedEvent as MessageCreatedEvent from .event import MessageEvent as MessageEvent -from .filter import direct_message as direct_message -from .filter import notice_me as notice_me -from .filter import public_message as public_message -from .filter import reply_me as reply_me -from .filter import to_me as to_me +from .filter import Filter as Filter from .message import MessageChain as MessageChain from .plugin import Plugin as Plugin from .plugin import PluginMetadata as PluginMetadata @@ -63,5 +59,6 @@ WS = WebsocketsInfo WH = WebhookInfo +filter_ = Filter __version__ = "0.9.0" diff --git a/arclet/entari/event/protocol.py b/arclet/entari/event/protocol.py index 10e5b59..a76d574 100644 --- a/arclet/entari/event/protocol.py +++ b/arclet/entari/event/protocol.py @@ -61,6 +61,7 @@ class ReplyMeSupplier(SupplyAuxiliary): async def __call__(self, scope: Scope, interface: Interface): if self.id in interface.executed: return + message: MessageChain = interface.ctx["$message_content"] account = await interface.query(Account, "account", force_return=True) reply = await interface.query(Reply, "reply", force_return=True) if not account: @@ -69,7 +70,14 @@ async def __call__(self, scope: Scope, interface: Interface): is_reply_me = False else: is_reply_me = _is_reply_me(reply, account) - return interface.update(**{"is_reply_me": is_reply_me}) + if is_reply_me and message and isinstance(message[0], Text): + message = message.copy() + text = message[0].text.lstrip() + if not text: + message.pop(0) + else: + message[0] = Text(text) + return interface.update(**{"$message_content": message, "is_reply_me": is_reply_me}) @property def scopes(self) -> set[Scope]: diff --git a/arclet/entari/filter.py b/arclet/entari/filter.py deleted file mode 100644 index b3580ae..0000000 --- a/arclet/entari/filter.py +++ /dev/null @@ -1,187 +0,0 @@ -from typing import Optional - -from arclet.letoderea import Interface, JudgeAuxiliary, Scope -from satori import Channel, ChannelType, Guild, User -from satori.client import Account - - -class UserFilter(JudgeAuxiliary): - def __init__(self, *user_ids: str): - self.user_ids = set(user_ids) - super().__init__() - - async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: - if not (user := await interface.query(User, "user", force_return=True)): - return False - return user.id in self.user_ids - - @property - def scopes(self) -> set[Scope]: - return {Scope.prepare} - - @property - def id(self) -> str: - return "entari.filter/user" - - -class GuildFilter(JudgeAuxiliary): - def __init__(self, *guild_ids: str): - self.guild_ids = set(guild_ids) - super().__init__() - - async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: - if not (guild := await interface.query(Guild, "guild", force_return=True)): - return False - return guild.id in self.guild_ids - - @property - def scopes(self) -> set[Scope]: - return {Scope.prepare} - - @property - def id(self) -> str: - return "entari.filter/guild" - - -class ChannelFilter(JudgeAuxiliary): - def __init__(self, *channel_ids: str): - self.channel_ids = set(channel_ids) - super().__init__() - - async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: - if not (channel := await interface.query(Channel, "channel", force_return=True)): - return False - return channel.id in self.channel_ids - - @property - def scopes(self) -> set[Scope]: - return {Scope.prepare} - - @property - def id(self) -> str: - return "entari.filter/channel" - - -class SelfFilter(JudgeAuxiliary): - def __init__(self, *self_ids: str): - self.self_ids = set(self_ids) - super().__init__() - - async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: - if not (account := await interface.query(Account, "account", force_return=True)): - return False - return account.self_id in self.self_ids - - @property - def scopes(self) -> set[Scope]: - return {Scope.prepare} - - @property - def id(self) -> str: - return "entari.filter/self" - - -class PlatformFilter(JudgeAuxiliary): - def __init__(self, *platforms: str): - self.platforms = set(platforms) - super().__init__() - - async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: - if not (account := await interface.query(Account, "account", force_return=True)): - return False - return account.platform in self.platforms - - @property - def scopes(self) -> set[Scope]: - return {Scope.prepare} - - @property - def id(self) -> str: - return "entari.filter/platform" - - -class DirectMessageJudger(JudgeAuxiliary): - async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: - if not (channel := await interface.query(Channel, "channel", force_return=True)): - return False - return channel.type == ChannelType.DIRECT - - @property - def scopes(self) -> set[Scope]: - return {Scope.prepare} - - @property - def id(self) -> str: - return "entari.filter/direct_message" - - -direct_message = DirectMessageJudger() - - -class PublicMessageJudger(JudgeAuxiliary): - async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: - if not (channel := await interface.query(Channel, "channel", force_return=True)): - return False - return channel.type != ChannelType.DIRECT - - @property - def scopes(self) -> set[Scope]: - return {Scope.prepare} - - @property - def id(self) -> str: - return "entari.filter/public_message" - - -public_message = PublicMessageJudger() - - -class ReplyMeJudger(JudgeAuxiliary): - - async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: - return interface.ctx.get("is_reply_me", False) - - @property - def scopes(self) -> set[Scope]: - return {Scope.prepare} - - @property - def id(self) -> str: - return "entari.filter/judge_reply_me" - - -reply_me = ReplyMeJudger() - - -class NoticeMeJudger(JudgeAuxiliary): - async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: - return interface.ctx.get("is_notice_me", False) - - @property - def scopes(self) -> set[Scope]: - return {Scope.prepare} - - @property - def id(self) -> str: - return "entari.filter/judge_notice_me" - - -notice_me = NoticeMeJudger() - - -class ToMeJudger(JudgeAuxiliary): - async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: - is_reply_me = interface.ctx.get("is_reply_me", False) - is_notice_me = interface.ctx.get("is_notice_me", False) - return is_reply_me or is_notice_me - - @property - def scopes(self) -> set[Scope]: - return {Scope.prepare} - - @property - def id(self) -> str: - return "entari.filter/judge_to_me" - - -to_me = ToMeJudger() diff --git a/arclet/entari/filter/__init__.py b/arclet/entari/filter/__init__.py new file mode 100644 index 0000000..7b92e04 --- /dev/null +++ b/arclet/entari/filter/__init__.py @@ -0,0 +1,122 @@ +from collections.abc import Awaitable +from typing import Callable, Optional, Union +from typing_extensions import Self, TypeAlias + +from arclet.letoderea import Interface, JudgeAuxiliary, Scope +from arclet.letoderea import bind as _bind +from arclet.letoderea.typing import run_sync +from tarina import is_async + +from ..session import Session +from .common import ChannelFilter, GuildFilter, PlatformFilter, SelfFilter, UserFilter +from .message import DirectMessageJudger, NoticeMeJudger, PublicMessageJudger, ReplyMeJudger, ToMeJudger +from .op import ExcludeFilter, IntersectFilter, UnionFilter + +_SessionFilter: TypeAlias = Union[Callable[[Session], bool], Callable[[Session], Awaitable[bool]]] + + +class Filter(JudgeAuxiliary): + def __init__(self, callback: Optional[_SessionFilter] = None, priority: int = 10): + super().__init__(priority=priority) + self.steps = [] + if callback: + if is_async(callback): + self.callback = callback + else: + self.callback = run_sync(callback) + else: + self.callback = None + + async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: + for step in sorted(self.steps, key=lambda x: x.priority): + if not await step(scope, interface): + return False + if self.callback: + session = await interface.query(Session, "session", force_return=True) + if not session: + return False + if not await self.callback(session): # type: ignore + return False + return True + + @property + def scopes(self) -> set[Scope]: + return {Scope.prepare} + + @property + def id(self) -> str: + return "entari.filter" + + def user(self, *user_ids: str) -> Self: + self.steps.append(UserFilter(*user_ids, priority=6)) + return self + + def guild(self, *guild_ids: str) -> Self: + self.steps.append(GuildFilter(*guild_ids, priority=4)) + return self + + def channel(self, *channel_ids: str) -> Self: + self.steps.append(ChannelFilter(*channel_ids, priority=5)) + return self + + def self(self, *self_ids: str) -> Self: + self.steps.append(SelfFilter(*self_ids, priority=3)) + return self + + def platform(self, *platforms: str) -> Self: + self.steps.append(PlatformFilter(*platforms, priority=2)) + return self + + @property + def direct(self) -> Self: + self.steps.append(DirectMessageJudger(priority=8)) + return self + + private = direct + + @property + def public(self) -> Self: + self.steps.append(PublicMessageJudger(priority=8)) + return self + + @property + def reply_me(self) -> Self: + self.steps.append(ReplyMeJudger(priority=9)) + return self + + @property + def notice_me(self) -> Self: + self.steps.append(NoticeMeJudger(priority=10)) + return self + + @property + def to_me(self) -> Self: + self.steps.append(ToMeJudger(priority=11)) + return self + + def bind(self, func): + return _bind(self)(func) + + def and_(self, other: Union["Filter", _SessionFilter]) -> "Filter": + new = Filter(priority=self.priority) + _other = other if isinstance(other, Filter) else Filter(callback=other) + new.steps.append(IntersectFilter(self, _other, priority=1)) + return new + + intersect = and_ + + def or_(self, other: Union["Filter", _SessionFilter]) -> "Filter": + new = Filter(priority=self.priority) + _other = other if isinstance(other, Filter) else Filter(callback=other) + new.steps.append(UnionFilter(self, _other, priority=1)) + return new + + union = or_ + + def not_(self, other: Union["Filter", _SessionFilter]) -> "Filter": + new = Filter(priority=self.priority) + _other = other if isinstance(other, Filter) else Filter(callback=other) + new.steps.append(ExcludeFilter(self, _other, priority=1)) + return new + + exclude = not_ diff --git a/arclet/entari/filter/common.py b/arclet/entari/filter/common.py new file mode 100644 index 0000000..bf8bc34 --- /dev/null +++ b/arclet/entari/filter/common.py @@ -0,0 +1,100 @@ +from typing import Optional + +from arclet.letoderea import Interface, JudgeAuxiliary, Scope +from satori import Channel, Guild, User +from satori.client import Account + + +class UserFilter(JudgeAuxiliary): + def __init__(self, *user_ids: str, priority: int = 10): + self.user_ids = set(user_ids) + super().__init__(priority=priority) + + async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: + if not (user := await interface.query(User, "user", force_return=True)): + return False + return user.id in self.user_ids + + @property + def scopes(self) -> set[Scope]: + return {Scope.prepare} + + @property + def id(self) -> str: + return "entari.filter/user" + + +class GuildFilter(JudgeAuxiliary): + def __init__(self, *guild_ids: str, priority: int = 10): + self.guild_ids = set(guild_ids) + super().__init__(priority=priority) + + async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: + if not (guild := await interface.query(Guild, "guild", force_return=True)): + return False + return guild.id in self.guild_ids + + @property + def scopes(self) -> set[Scope]: + return {Scope.prepare} + + @property + def id(self) -> str: + return "entari.filter/guild" + + +class ChannelFilter(JudgeAuxiliary): + def __init__(self, *channel_ids: str, priority: int = 10): + self.channel_ids = set(channel_ids) + super().__init__(priority=priority) + + async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: + if not (channel := await interface.query(Channel, "channel", force_return=True)): + return False + return channel.id in self.channel_ids + + @property + def scopes(self) -> set[Scope]: + return {Scope.prepare} + + @property + def id(self) -> str: + return "entari.filter/channel" + + +class SelfFilter(JudgeAuxiliary): + def __init__(self, *self_ids: str, priority: int = 20): + self.self_ids = set(self_ids) + super().__init__(priority=priority) + + async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: + if not (account := await interface.query(Account, "account", force_return=True)): + return False + return account.self_id in self.self_ids + + @property + def scopes(self) -> set[Scope]: + return {Scope.prepare} + + @property + def id(self) -> str: + return "entari.filter/self" + + +class PlatformFilter(JudgeAuxiliary): + def __init__(self, *platforms: str, priority: int = 10): + self.platforms = set(platforms) + super().__init__(priority=priority) + + async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: + if not (account := await interface.query(Account, "account", force_return=True)): + return False + return account.platform in self.platforms + + @property + def scopes(self) -> set[Scope]: + return {Scope.prepare} + + @property + def id(self) -> str: + return "entari.filter/platform" diff --git a/arclet/entari/filter/message.py b/arclet/entari/filter/message.py new file mode 100644 index 0000000..fa8c8ee --- /dev/null +++ b/arclet/entari/filter/message.py @@ -0,0 +1,76 @@ +from typing import Optional + +from arclet.letoderea import Interface, JudgeAuxiliary, Scope +from satori import Channel, ChannelType + + +class DirectMessageJudger(JudgeAuxiliary): + async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: + if not (channel := await interface.query(Channel, "channel", force_return=True)): + return False + return channel.type == ChannelType.DIRECT + + @property + def scopes(self) -> set[Scope]: + return {Scope.prepare} + + @property + def id(self) -> str: + return "entari.filter/direct_message" + + +class PublicMessageJudger(JudgeAuxiliary): + async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: + if not (channel := await interface.query(Channel, "channel", force_return=True)): + return False + return channel.type != ChannelType.DIRECT + + @property + def scopes(self) -> set[Scope]: + return {Scope.prepare} + + @property + def id(self) -> str: + return "entari.filter/public_message" + + +class ReplyMeJudger(JudgeAuxiliary): + + async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: + return interface.ctx.get("is_reply_me", False) + + @property + def scopes(self) -> set[Scope]: + return {Scope.prepare} + + @property + def id(self) -> str: + return "entari.filter/judge_reply_me" + + +class NoticeMeJudger(JudgeAuxiliary): + async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: + return interface.ctx.get("is_notice_me", False) + + @property + def scopes(self) -> set[Scope]: + return {Scope.prepare} + + @property + def id(self) -> str: + return "entari.filter/judge_notice_me" + + +class ToMeJudger(JudgeAuxiliary): + async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: + is_reply_me = interface.ctx.get("is_reply_me", False) + is_notice_me = interface.ctx.get("is_notice_me", False) + return is_reply_me or is_notice_me + + @property + def scopes(self) -> set[Scope]: + return {Scope.prepare} + + @property + def id(self) -> str: + return "entari.filter/judge_to_me" diff --git a/arclet/entari/filter/op.py b/arclet/entari/filter/op.py new file mode 100644 index 0000000..438e2c3 --- /dev/null +++ b/arclet/entari/filter/op.py @@ -0,0 +1,57 @@ +from typing import Optional + +from arclet.letoderea import Interface, JudgeAuxiliary, Scope + + +class IntersectFilter(JudgeAuxiliary): + def __init__(self, left: JudgeAuxiliary, right: JudgeAuxiliary, priority: int = 10): + self.left = left + self.right = right + super().__init__(priority=priority) + + async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: + return await self.left(scope, interface) and await self.right(scope, interface) + + @property + def scopes(self) -> set[Scope]: + return {Scope.prepare} + + @property + def id(self) -> str: + return "entari.filter/intersect" + + +class UnionFilter(JudgeAuxiliary): + def __init__(self, left: JudgeAuxiliary, right: JudgeAuxiliary, priority: int = 10): + self.left = left + self.right = right + super().__init__(priority=priority) + + async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: + return await self.left(scope, interface) or await self.right(scope, interface) + + @property + def scopes(self) -> set[Scope]: + return {Scope.prepare} + + @property + def id(self) -> str: + return "entari.filter/union" + + +class ExcludeFilter(JudgeAuxiliary): + def __init__(self, left: JudgeAuxiliary, right: JudgeAuxiliary, priority: int = 10): + self.left = left + self.right = right + super().__init__(priority=priority) + + async def __call__(self, scope: Scope, interface: Interface) -> Optional[bool]: + return await self.left(scope, interface) and not await self.right(scope, interface) + + @property + def scopes(self) -> set[Scope]: + return {Scope.prepare} + + @property + def id(self) -> str: + return "entari.filter/exclude" diff --git a/example_plugin.py b/example_plugin.py index e63abd2..5195520 100644 --- a/example_plugin.py +++ b/example_plugin.py @@ -5,10 +5,8 @@ MessageChain, MessageCreatedEvent, Plugin, + Filter, command, - public_message, - to_me, - bind, metadata, keeping, ) @@ -32,7 +30,7 @@ async def cleanup(): @disp_message -@bind(public_message) +@Filter().public.bind async def _(msg: MessageChain, session: Session): content = msg.extract_plain_text() if re.match(r"(.{0,3})(上传|设定)(.{0,3})(上传|设定)(.{0,3})", content): @@ -42,9 +40,14 @@ async def _(msg: MessageChain, session: Session): disp_message1 = plug.dispatch(MessageCreatedEvent) -@disp_message1.on(auxiliaries=[public_message, to_me]) -async def _(event: MessageCreatedEvent): - print(event.content) +@disp_message1.on(auxiliaries=[Filter().public.to_me.and_(lambda sess: str(sess.content) == "aaa")]) +async def _(session: Session): + return await session.send("Filter: public message, to me, and content is 'aaa'") + + +@disp_message1.on(auxiliaries=[Filter().public.to_me.not_(lambda sess: str(sess.content) == "aaa")]) +async def _(session: Session): + return await session.send("Filter: public message, to me, but content is not 'aaa'") @command.on("add {a} {b}")