Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some fixes #16

Merged
merged 6 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading