-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* update invoker * add more high level api * add dockerfile * fix bugs * some small changes
- Loading branch information
Showing
45 changed files
with
1,051 additions
and
322 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.