Skip to content

Commit

Permalink
Some fixes (#16)
Browse files Browse the repository at this point in the history
* update invoker
* add more high level api
* add dockerfile
* fix bugs
* some small changes
  • Loading branch information
Ovizro authored Aug 13, 2024
1 parent 403ca3d commit f7f8dcc
Show file tree
Hide file tree
Showing 45 changed files with 1,051 additions and 322 deletions.
4 changes: 1 addition & 3 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ branch = True
omit =
__main__.py
karuha/plugin_server.py
karuha/utils/locks.py

[report]
# Regexes for lines to exclude from consideration
Expand All @@ -29,8 +30,5 @@ exclude_lines =
class (\w+)\(Protocol\):
@(typing\.)?overload

# Don't complain about deprecated code
@(typing(_extensions)?\.)?deprecated(.*)


ignore_errors = True
8 changes: 8 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM python:3.10

WORKDIR /opt/karuha
COPY . .

RUN pip install .[all] -i https://pypi.tuna.tsinghua.edu.cn/simple

CMD [ "python" , "-m" , "karuha" ]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ Of course, Karuha provides more APIs than just these. If you are interested in l
Features that may be added in the future include:

- [x] APIs related to user information getting and setting
- [ ] Match rule for command
- [x] Match rule for command
- [ ] Automatic argument parsing in argparse format for commands

### Module Development
Expand Down
4 changes: 3 additions & 1 deletion README_cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,10 @@ async def hi(session: MessageSession, argv: List[str]) -> None:

在接下来可能会添加的功能包括:

- [ ] 用户信息获取与设置相关的API
- [x] 用户信息获取与设置相关的API
- [x] 消息匹配规则
- [ ] argparse格式的命令参数自动解析
- [ ] 代理发送机器人

### 模块开发

Expand Down
37 changes: 37 additions & 0 deletions examples/echo_ex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from typing import List, Optional

from pydantic_core import ValidationError

from karuha import MessageSession, on_command
from karuha.text import Drafty, Head, Message
from karuha.utils.argparse import ArgumentParser


@on_command
async def echo(session: MessageSession, message: Message, argv: List[str], reply: Head[Optional[int]]) -> None:
parser = ArgumentParser(session, "echo")
parser.add_argument("-r", "--raw", action="store_true", help="echo raw text")
parser.add_argument("-d", "--drafty", action="store_true", help="decode text as drafty")
parser.add_argument("-R", "--reply", action="store_true", help="echo reply message")
parser.add_argument("text", nargs="*", help="text to echo", default=())
ns = parser.parse_args(argv)
if ns.reply:
if reply is None:
await session.finish("No reply message")
message = await session.get_data(seq_id=reply)
text = message.plain_text
else:
text = " ".join(ns.text)
if ns.raw:
raw_text = message.raw_text
if isinstance(raw_text, Drafty):
raw_text = raw_text.model_dump_json(indent=4, exclude_defaults=True)
await session.finish(raw_text)
elif ns.drafty:
try:
df = Drafty.model_validate_json(text)
except ValidationError:
await session.finish("Invalid Drafty JSON")
await session.send(df)
else:
await session.send(text)
138 changes: 138 additions & 0 deletions examples/exec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""
Execute python code or shell commands
NOTE: Executing the commands in this example requires the user to be under staff management.
NOTE: Allowing code execution from a user is a very dangerous behavior and may lead to server compromise.
NOTE: This module is not recommended to be used in production.
Run with:
python -m karuha ./config.json --module exec
"""

import asyncio
import os
import sys
from io import StringIO
from traceback import format_exc
from typing import List, Optional

from karuha import MessageSession, on_command
from karuha.utils.argparse import ArgumentParser


@on_command("eval")
async def eval_(session: MessageSession, name: str, user_id: str, text: str) -> None:
user = await session.get_user(user_id, ensure_user=True)
if not user.staff:
await session.finish("Permission denied")
text = text[text.index(name) + len(name):]
try:
result = eval(text, {"session": session})
except: # noqa: E722
await session.send(format_exc())
else:
await session.send(f"eavl result: {result}")


@on_command("exec")
async def exec_(session: MessageSession, name: str, user_id: str, text: str) -> None:
user = await session.get_user(user_id)
if not user.staff:
await session.finish("Permission denied")
text = text[text.index(name) + len(name):]
ss = StringIO()
stdout = sys.stdout
stderr = sys.stderr
try:
sys.stdout = sys.stderr = ss
exec(text, {"session": session})
except: # noqa: E722
await session.finish(format_exc())
finally:
sys.stdout = stdout
sys.stderr = stderr
if out := ss.getvalue():
await session.send(out)


class DateProtocol(asyncio.SubprocessProtocol):
def __init__(self, exit_future: Optional[asyncio.Future] = None) -> None:
self.exit_future = exit_future
self.output = asyncio.Queue()
self.pipe_closed = False
self.exited = False

def pipe_connection_lost(self, fd: int, exc: Optional[Exception]) -> None:
self.pipe_closed = True
self.check_for_exit()

def pipe_data_received(self, fd: int, data: bytes) -> None:
self.output.put_nowait(data)

def process_exited(self) -> None:
self.exited = True
# process_exited() method can be called before
# pipe_connection_lost() method: wait until both methods are
# called.
self.check_for_exit()

async def wait(self) -> None:
if self.pipe_closed and self.exited:
return
if self.exit_future is None:
self.exit_future = asyncio.Future()
await self.exit_future

def check_for_exit(self) -> None:
if self.pipe_closed and self.exited and self.exit_future:
self.exit_future.set_result(True)


@on_command
async def run(session: MessageSession, name: str, user_id: str, argv: List[str]) -> None:
user = await session.get_user(user_id)
if not user.staff:
await session.finish("Permission denied")
parser = ArgumentParser(session, name)
parser.add_argument("-c", "--cwd", help="working directory")
parser.add_argument("-e", "--env", action="append", help="environment variable")
parser.add_argument("command", nargs="*", help="command to run")
ns = parser.parse_args(argv)
if not ns.command:
await session.finish("No command specified")

session.bot.logger.info(f"run: {ns.command}")
loop = asyncio.get_running_loop()
transport, protocol = await loop.subprocess_exec(
DateProtocol,
*ns.command,
cwd=ns.cwd,
env=dict(os.environ, **dict((e.split("=", 1) for e in ns.env or ()))),
stdin=None,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)

wait_task = asyncio.create_task(protocol.wait())
while not wait_task.done():
done, _ = await asyncio.wait(
(wait_task, protocol.output.get()),
return_when=asyncio.FIRST_COMPLETED
)
if wait_task in done:
done.remove(wait_task)
if not done:
break
data: bytes = done.pop().result() # type: ignore
await session.send(data.decode())

while not protocol.output.empty():
data = protocol.output.get_nowait()
await session.send(data.decode())

code = transport.get_returncode()
transport.close()
if code is not None:
await session.send(f"Process exited with code {code}")
19 changes: 19 additions & 0 deletions examples/hi_ex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import List

from karuha import MessageSession
from karuha.command import on_command, rule
from karuha.utils.argparse import ArgumentParser


@on_command(alias=("hello",), rule=rule(to_me=True))
async def hi(session: MessageSession, name: str, user_id: str, argv: List[str]) -> None:
parser = ArgumentParser(session, name)
parser.add_argument("name", nargs="*", help="name to greet")
parser.add_argument("-p", "--in-private", action="store_true", help="send message in private chat")
ns = parser.parse_args(argv)
if ns.name:
name = ' '.join(ns.name)
else:
user = await session.get_user(user_id)
name = user.fn or "world"
await session.send(f"Hello {name}!", topic=user_id if ns.in_private else None)
13 changes: 7 additions & 6 deletions examples/tino.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
"""

import asyncio
from pathlib import Path
import random
from pathlib import Path
from argparse import ArgumentParser

from aiofiles import open as aio_open

import karuha
from karuha import MessageSession, on_command
from karuha import MessageSession, on_rule


_quotes = None
Expand All @@ -43,12 +43,13 @@ async def get_quotes():
return _quotes


@on_command
@on_rule()
async def quote(session: MessageSession) -> None:
"""
Reply with a random quote for each message.
"""
quotes = await get_quotes()
await session.send(
random.choice(quotes)
)
await session.send(random.choice(quotes))


if __name__ == "__main__":
Expand Down
25 changes: 25 additions & 0 deletions examples/welcome.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""
Send a welcome message to new users.
NOTE: Running this example requires enabling the plugin server
Run with:
python -m karuha ./config.json --module exec
"""

import json

from karuha import BaseSession
from karuha.event.plugin import AccountCreateEvent, on_new_account


@on_new_account
async def welcome(event: AccountCreateEvent, session: BaseSession) -> None:
user_name = "new user"
if event.action:
public = json.loads(event.public)
if isinstance(public, dict) and "fn" in public:
user_name = public["fn"]
await session.send(f"Hello {user_name}, welcome to Tinode!")
5 changes: 5 additions & 0 deletions karuha/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from .event import on, on_event, Event
from .text import Drafty, BaseText, PlainText, Message, TextChain
from .command import CommandCollection, AbstractCommand, AbstractCommandParser, BaseSession, MessageSession, CommandSession, get_collection, on_command, rule, on_rule
from .data import get_user, get_topic, try_get_user, try_get_topic
from .runner import get_bot, add_bot, try_add_bot, get_all_bots, async_run, run


Expand Down Expand Up @@ -53,6 +54,10 @@
"MessageSession",
"CommandSession",
"rule",
# data
"get_user",
"get_topic",
"try_get_user",
# decorator
"on",
"on_event",
Expand Down
5 changes: 4 additions & 1 deletion karuha/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@
))

default_config = os.environ.get("KARUHA_CONFIG", "config.json")
default_modules = os.environ.get("KARUHA_MODULES", "").split(os.pathsep)

parser = ArgumentParser("Karuha", description=description)
parser.add_argument("config", type=Path, nargs='?', default=default_config, help="path of the Karuha config")
parser.add_argument("--auto-create", action="store_true", help="auto create config")
parser.add_argument("--encoding", default="utf-8", help="config encoding")
parser.add_argument("-m", "--module", type=str, action="append", help="module to load")
parser.add_argument("-m", "--module", type=str, action="append", help="module to load", default=default_modules)
parser.add_argument("-v", "--version", action="version", version=version_info)


Expand All @@ -53,5 +54,7 @@
)
if namespace.module:
for module in namespace.module:
if not module:
continue
import_module(module)
run()
2 changes: 1 addition & 1 deletion karuha/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ async def login(self) -> Tuple[str, Dict[str, Any]]:
elif ctrl.code < 200 or ctrl.code >= 400:
err_text = f"fail to login: {ctrl.text}"
self.logger.error(err_text)
self.cancel()
# self.cancel()
raise KaruhaBotError(err_text, bot=self, code=ctrl.code)

self.logger.info(f"login successful (schema {schema})")
Expand Down
Loading

0 comments on commit f7f8dcc

Please sign in to comment.