From 6ad7ceb41c330142a2221f8a083ff30924355f1b Mon Sep 17 00:00:00 2001 From: ovizro Date: Thu, 16 Nov 2023 06:10:59 +0000 Subject: [PATCH] Add message event --- .github/workflows/build_test.yml | 8 +- .github/workflows/python-publish.yml | 124 +++++---------------------- README.md | 16 ++-- README_cn.md | 16 ++-- karuha/__init__.py | 9 +- karuha/event/__init__.py | 6 +- karuha/event/base.py | 13 ++- karuha/event/handler.py | 12 ++- karuha/event/message.py | 59 +++++++++++++ karuha/text/textchain.py | 5 ++ karuha/version.py | 2 +- 11 files changed, 135 insertions(+), 135 deletions(-) create mode 100644 karuha/event/message.py diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index 09896a5..6da77a6 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -14,13 +14,7 @@ jobs: strategy: matrix: python-version: [3.8, 3.9, "3.10", "3.11"] - os: [windows-latest, ubuntu-20.04, macos-latest] - # exclude: - # # package dependencies error on macos 3.9+ for unkwown reason - # - os: macos-latest - # python-version: 3.9 - # - os: macos-latest - # python-version: "3.10" + os: [windows-latest, ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} timeout-minutes: 30 steps: diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index f52ae0b..fd506f3 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,5 +1,5 @@ # This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by @@ -9,114 +9,32 @@ name: Upload Python Package on: - create: + release: + types: [published] workflow_dispatch: permissions: contents: read jobs: - deploy-wheels: - name: Deploy wheels on ${{ matrix.os }} for ${{ matrix.arch }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [36, 37, 38, 39, 310] - manylinux-image: [manylinux2010, manylinux2014, manylinux_2_24] - arch: [auto] - include: - - os: ubuntu-latest - manylinux-image: manylinux2014 - arch: aarch64 - python-version: 36 - - os: ubuntu-latest - manylinux-image: manylinux2014 - arch: aarch64 - python-version: 37 - - os: ubuntu-latest - manylinux-image: manylinux2014 - arch: aarch64 - python-version: 38 - - os: ubuntu-latest - manylinux-image: manylinux2014 - arch: aarch64 - python-version: 39 - - os: ubuntu-latest - manylinux-image: manylinux2014 - arch: aarch64 - python-version: 310 - exclude: - # manyliunx image is not a valid variation on MacOS and Windows - - os: macos-latest - manylinux-image: manylinux2010 - - os: windows-latest - manylinux-image: manylinux2010 - - os: macos-latest - manylinux-image: manylinux2014 - - os: windows-latest - manylinux-image: manylinux2014 + deploy: - steps: - - uses: actions/checkout@v2 - - name: Set up QEMU - if: ${{ matrix.arch == 'aarch64' }} - uses: docker/setup-qemu-action@v1 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install twine setuptools wheel - - name: Install cibuildwheel - run: python -m pip install cibuildwheel -U - - - name: Build wheels - run: python -m cibuildwheel --output-dir wheelhouse - env: - CIBW_BUILD: 'cp${{ matrix.python-version }}-*' - CIBW_SKIP: '*musllinux*' - CIBW_ARCHS: ${{matrix.arch}} - CIBW_MANYLINUX_*_IMAGE: ${{ matrix.manylinux-image }} - CIBW_MANYLINUX_I686_IMAGE: ${{ matrix.manylinux-image }} - - - name: Publish wheels to PyPI Unix - if: matrix.os != 'windows-latest' - continue-on-error: true - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: | - twine upload wheelhouse/*.whl - - name: Publish wheels to PyPI Windows - if: matrix.os == 'windows-latest' - continue-on-error: true - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: | - twine upload (Get-ChildItem wheelhouse/*.whl) - deploy-tar: runs-on: ubuntu-latest + steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install twine setuptools wheel - - name: Build source tar - run: | - python setup.py sdist - - name: Publish wheels to PyPI - continue-on-error: true - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: | - twine upload dist/*tar* + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.8' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/README.md b/README.md index 01ba9a4..df02bb4 100644 --- a/README.md +++ b/README.md @@ -68,25 +68,27 @@ Because this is a relatively low-level API, I will only give an example to show ```python import karuha -from karuha import Bot, DataEvent +from karuha import Bot, MessageEvent, PublishEvent bot = Bot( + "chatbot", "basic", "chatbot:123456" ) -@karuha.on(DataEvent) -async def reply(event: DataEvent) -> None: - if event.server_message.content.decode() == "\"Hello chatbot!\"": # message.content is a json string - await event.bot.publish( - event.server_message.topic, +@karuha.on(MessageEvent) +async def reply(event: MessageEvent) -> None: + if event.text == "Hello!": + PublishEvent.new( + event.bot, + event.topic, "Hello world!" ) -if __name__ = "__main__": +if __name__ == "__main__": karuha.load_config() karuha.add_bot(bot) karuha.run() diff --git a/README_cn.md b/README_cn.md index 2cfe99a..fe14a6f 100644 --- a/README_cn.md +++ b/README_cn.md @@ -70,25 +70,27 @@ ```python import karuha -from karuha import Bot, DataEvent +from karuha import Bot, MessageEvent, PublishEvent bot = Bot( + "chatbot", "basic", "chatbot:123456" ) -@karuha.on(DataEvent) -async def reply(event: DataEvent) -> None: - if event.server_message.content.decode() == "\"Hello chatbot!\"": # message.content is a json string - await event.bot.publish( - event.server_message.topic, +@karuha.on(MessageEvent) +async def reply(event: MessageEvent) -> None: + if event.text == "Hello!": + PublishEvent.new( + event.bot, + event.topic, "Hello world!" ) -if __name__ = "__main__": +if __name__ == "__main__": karuha.load_config() karuha.add_bot(bot) karuha.run() diff --git a/karuha/__init__.py b/karuha/__init__.py index c33b6e1..4e78d89 100644 --- a/karuha/__init__.py +++ b/karuha/__init__.py @@ -16,7 +16,8 @@ from .config import get_config, load_config, init_config, save_config, Config from .config import Server as ServerConfig, Bot as BotConfig from .bot import Bot -from .event import on, BotEvent, DataEvent, CtrlEvent, PresEvent, MetaEvent, InfoEvent +from .event import * +from .text import Drafty, BaseText, PlainText from .plugin_server import init_server from .logger import logger from .exception import KaruhaException @@ -103,11 +104,5 @@ def run() -> None: "ServerConfig", "Bot", "on", - "BotEvent", - "DataEvent", - "CtrlEvent", - "PresEvent", - "MetaEvent", - "InfoEvent", "KaruhaException" ] diff --git a/karuha/event/__init__.py b/karuha/event/__init__.py index b59e3b1..2953214 100644 --- a/karuha/event/__init__.py +++ b/karuha/event/__init__.py @@ -2,6 +2,7 @@ from .base import Event from .bot import BotEvent, ClientEvent, PublishEvent, SubscribeEvent, LeaveEvent, ServerEvent, DataEvent, CtrlEvent, MetaEvent, PresEvent, InfoEvent +from .message import MessageEvent from . import handler @@ -16,6 +17,7 @@ def wrapper(func: Callable[[T_Event], Any]) -> Callable[[T_Event], Any]: __all__ = [ + "on", "Event", "BotEvent", @@ -29,5 +31,7 @@ def wrapper(func: Callable[[T_Event], Any]) -> Callable[[T_Event], Any]: "CtrlEvent", "MetaEvent", "PresEvent", - "InfoEvent" + "InfoEvent", + + "MessageEvent" ] \ No newline at end of file diff --git a/karuha/event/base.py b/karuha/event/base.py index b455bf3..d15e4c9 100644 --- a/karuha/event/base.py +++ b/karuha/event/base.py @@ -1,10 +1,13 @@ import asyncio from typing import Any, Callable, ClassVar, Coroutine, List -from typing_extensions import Self +from typing_extensions import Self, ParamSpec from ..logger import logger +P = ParamSpec("P") + + class Event(object): __slots__ = [] @@ -18,6 +21,14 @@ def add_handler(cls, handler: Callable[[Self], Coroutine]) -> None: def remove_handler(cls, handler: Callable[[Self], Coroutine]) -> None: cls.__handlers__.remove(handler) + @classmethod # type: ignore + def new(cls: Callable[P, Self], *args: P.args, **kwds: P.kwargs) -> Self: # type: ignore + event = cls( + *args, **kwds + ) + event.trigger() + return event + def call_handler(self, handler: Callable[[Self], Coroutine]) -> None: asyncio.create_task(handler(self)) diff --git a/karuha/event/handler.py b/karuha/event/handler.py index 4ae9c3e..b54b7c1 100644 --- a/karuha/event/handler.py +++ b/karuha/event/handler.py @@ -2,15 +2,25 @@ from ..text import BaseText from .bot import DataEvent, CtrlEvent, PresEvent, PublishEvent, SubscribeEvent, LeaveEvent +from .message import MessageEvent @DataEvent.add_handler async def _(event: DataEvent) -> None: msg = event.server_message - event.bot.logger.info(f"({msg.topic})=> {msg.content.decode()}") await event.bot.note_read(msg.topic, msg.seq_id) +@DataEvent.add_handler +async def _(event: DataEvent) -> None: + MessageEvent.from_data_event(event).trigger() + + +@MessageEvent.add_handler +async def _(event: MessageEvent) -> None: + event.bot.logger.info(f"({event.topic})=> {event.text}") + + @CtrlEvent.add_handler async def _(event: CtrlEvent) -> None: tid = event.server_message.id diff --git a/karuha/event/message.py b/karuha/event/message.py new file mode 100644 index 0000000..2811eff --- /dev/null +++ b/karuha/event/message.py @@ -0,0 +1,59 @@ +import json +from typing import Dict +from typing_extensions import Self + +from ..bot import Bot +from ..text import Drafty, PlainText, drafty2text +from .bot import BotEvent, DataEvent + + +class MessageEvent(BotEvent): + __slots__ = ["topic", "uid", "seq_id", "head", "raw_content", "raw_text", "text"] + + def __init__(self, bot: Bot, /, topic: str, uid: str, seq_id: int, head: Dict[str, str], content: bytes) -> None: + super().__init__(bot) + self.topic = topic + self.uid = uid + self.seq_id = seq_id + self.head = head + self._set_text(content) + + @classmethod + def from_data_event(cls, event: DataEvent, /) -> Self: + message = event.server_message + return cls( + event.bot, + message.topic, + message.from_user_id, + message.seq_id, + {k: json.loads(v) for k, v in message.head}, + message.content + ) + + def _set_text(self, content: bytes, /) -> None: + self.raw_content = content + + try: + raw_text = json.loads(content) + except json.JSONDecodeError: + raw_text = content.decode() + topic = self.topic + seq_id = self.seq_id + self.bot.logger.error(f"cannot decode text {raw_text} ({topic=},{seq_id=})") + + if not isinstance(raw_text, str): + try: + self.raw_text = Drafty.model_validate(raw_text) + except Exception: + self.bot.logger.error(f"unknown text format {raw_text}") + raw_text = str(raw_text) + else: + try: + self.text = drafty2text(self.raw_text) + except Exception: + self.bot.logger.error(f"cannot decode drafty {self.raw_text}") + self.text = self.raw_text.txt + return + + self.raw_text = raw_text + self.text = PlainText(raw_text) diff --git a/karuha/text/textchain.py b/karuha/text/textchain.py index 1d3f5e1..db57209 100644 --- a/karuha/text/textchain.py +++ b/karuha/text/textchain.py @@ -35,6 +35,11 @@ def to_drafty(self) -> Drafty: fmt.append(DraftyFormat(at=p, len=1, tp="BR")) start = p + 1 return Drafty(txt=self.text.replace('\n', ' '), fmt=fmt) + + def __eq__(self, __value: Any) -> bool: + if isinstance(__value, str): + return self.text == __value + return super().__eq__(__value) def __len__(self) -> int: return len(self.text) diff --git a/karuha/version.py b/karuha/version.py index d46e69e..0ae4ebc 100644 --- a/karuha/version.py +++ b/karuha/version.py @@ -1,4 +1,4 @@ from importlib.metadata import distribution -APP_VERSION = __version__ = "0.1.0b1" +APP_VERSION = __version__ = "0.1.0b2" LIB_VERSION = distribution("tinode_grpc").version