|
@@ -58,6 +58,7 @@ from gateway.platforms.base import (
|
|
|
cache_media_bytes,
|
|
cache_media_bytes,
|
|
|
get_inbound_media_max_bytes,
|
|
get_inbound_media_max_bytes,
|
|
|
validate_inbound_media_size,
|
|
validate_inbound_media_size,
|
|
|
|
|
+ validate_media_delivery_path,
|
|
|
)
|
|
)
|
|
|
from gateway.session import SessionSource
|
|
from gateway.session import SessionSource
|
|
|
|
|
|
|
@@ -95,6 +96,7 @@ try:
|
|
|
stream_events,
|
|
stream_events,
|
|
|
)
|
|
)
|
|
|
from chattolib.types import (
|
|
from chattolib.types import (
|
|
|
|
|
+ DirectoryMember,
|
|
|
Message,
|
|
Message,
|
|
|
MessageAttachment,
|
|
MessageAttachment,
|
|
|
MessageSearchGroupBy,
|
|
MessageSearchGroupBy,
|
|
@@ -5159,6 +5161,310 @@ CHATTO_DELETE_MESSAGE_SCHEMA = {
|
|
|
},
|
|
},
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+CHATTO_EDIT_MESSAGE_SCHEMA = {
|
|
|
|
|
+ "name": "chatto_edit_message",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "Edit the text of an already-posted message in a Chatto room, by its "
|
|
|
|
|
+ "event ID. Use it to correct the bot's own earlier messages — editing "
|
|
|
|
|
+ "another user's message is refused by the server. The full new text "
|
|
|
|
|
+ "replaces the old body; content over the per-message limit is refused "
|
|
|
|
|
+ "rather than truncated."
|
|
|
|
|
+ ),
|
|
|
|
|
+ "parameters": {
|
|
|
|
|
+ "type": "object",
|
|
|
|
|
+ "properties": {
|
|
|
|
|
+ "room_id": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": "The Chatto room ID the message lives in.",
|
|
|
|
|
+ },
|
|
|
|
|
+ "room_name": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "Optional: human-readable room name — one of ``room_id`` or "
|
|
|
|
|
+ "``room_name`` is required."
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ "event_id": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": "The event ID of the message to edit.",
|
|
|
|
|
+ },
|
|
|
|
|
+ "message": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": "The full new message text (markdown supported).",
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ "required": ["event_id", "message"],
|
|
|
|
|
+ "additionalProperties": False,
|
|
|
|
|
+ },
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+CHATTO_UPLOAD_FILE_SCHEMA = {
|
|
|
|
|
+ "name": "chatto_upload_file",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "Upload a local file (image, video, audio or document) from this "
|
|
|
|
|
+ "machine and post it into a Chatto room as an attachment, with an "
|
|
|
|
|
+ "optional caption. Returns the asset ID and the message ID."
|
|
|
|
|
+ ),
|
|
|
|
|
+ "parameters": {
|
|
|
|
|
+ "type": "object",
|
|
|
|
|
+ "properties": {
|
|
|
|
|
+ "room_id": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": "The Chatto room ID of the destination.",
|
|
|
|
|
+ },
|
|
|
|
|
+ "room_name": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "Optional: human-readable room name — one of ``room_id`` or "
|
|
|
|
|
+ "``room_name`` is required."
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ "file_path": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "Absolute path of the local file to upload (e.g. a file "
|
|
|
|
|
+ "this session produced earlier)."
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ "caption": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": "Optional text posted alongside the attachment (markdown).",
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ "required": ["file_path"],
|
|
|
|
|
+ "additionalProperties": False,
|
|
|
|
|
+ },
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+CHATTO_ADD_REACTION_SCHEMA = {
|
|
|
|
|
+ "name": "chatto_add_reaction",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "Add an emoji reaction to a message in a Chatto room. The emoji is "
|
|
|
|
|
+ "given as its gemoji shortcode name (e.g. 'white_check_mark', 'tada', "
|
|
|
|
|
+ "'rocket'); a Unicode emoji is translated when a shortcode is known "
|
|
|
|
|
+ "for it. Use reactions to acknowledge or rate messages."
|
|
|
|
|
+ ),
|
|
|
|
|
+ "parameters": {
|
|
|
|
|
+ "type": "object",
|
|
|
|
|
+ "properties": {
|
|
|
|
|
+ "room_id": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": "The Chatto room ID the message lives in.",
|
|
|
|
|
+ },
|
|
|
|
|
+ "room_name": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "Optional: human-readable room name — one of ``room_id`` or "
|
|
|
|
|
+ "``room_name`` is required."
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ "event_id": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": "The event ID of the message to react to.",
|
|
|
|
|
+ },
|
|
|
|
|
+ "emoji": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "The reaction as a gemoji shortcode (e.g. 'white_check_mark', "
|
|
|
|
|
+ "'tada', 'rocket') or a known display emoji."
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ "required": ["event_id", "emoji"],
|
|
|
|
|
+ "additionalProperties": False,
|
|
|
|
|
+ },
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+CHATTO_REMOVE_REACTION_SCHEMA = {
|
|
|
|
|
+ "name": "chatto_remove_reaction",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "Remove the bot's emoji reaction from a message in a Chatto room. The "
|
|
|
|
|
+ "emoji is given as its gemoji shortcode name (see chatto_add_reaction)."
|
|
|
|
|
+ ),
|
|
|
|
|
+ "parameters": {
|
|
|
|
|
+ "type": "object",
|
|
|
|
|
+ "properties": {
|
|
|
|
|
+ "room_id": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": "The Chatto room ID the message lives in.",
|
|
|
|
|
+ },
|
|
|
|
|
+ "room_name": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "Optional: human-readable room name — one of ``room_id`` or "
|
|
|
|
|
+ "``room_name`` is required."
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ "event_id": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": "The event ID of the message the reaction is on.",
|
|
|
|
|
+ },
|
|
|
|
|
+ "emoji": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": "The reaction to remove, as a gemoji shortcode name.",
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ "required": ["event_id", "emoji"],
|
|
|
|
|
+ "additionalProperties": False,
|
|
|
|
|
+ },
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+CHATTO_MARK_ROOM_AS_READ_SCHEMA = {
|
|
|
|
|
+ "name": "chatto_mark_room_as_read",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "Mark a Chatto room as read for the bot. Updates the server's read "
|
|
|
|
|
+ "marker so the room stops counting as unread. Useful after the bot "
|
|
|
|
|
+ "has fetched a room's context in bulk. Use chatto_mark_thread_as_read "
|
|
|
|
|
+ "for a single thread instead."
|
|
|
|
|
+ ),
|
|
|
|
|
+ "parameters": {
|
|
|
|
|
+ "type": "object",
|
|
|
|
|
+ "properties": {
|
|
|
|
|
+ "room_id": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": "The Chatto room ID to mark read.",
|
|
|
|
|
+ },
|
|
|
|
|
+ "room_name": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "Optional: human-readable room name — one of ``room_id`` or "
|
|
|
|
|
+ "``room_name`` is required."
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ "up_to_event_id": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "Optional: mark read only up to this event ID instead of "
|
|
|
|
|
+ "the room's newest message."
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ "required": [],
|
|
|
|
|
+ "additionalProperties": False,
|
|
|
|
|
+ },
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+CHATTO_CREATE_ROOM_SCHEMA = {
|
|
|
|
|
+ "name": "chatto_create_room",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "Create a new Chatto channel in a room group. Returns the new room's "
|
|
|
|
|
+ "ID. Room creation is a server-side privileged action — the bot "
|
|
|
|
|
+ "account needs the room.create permission, and the refusal is "
|
|
|
|
|
+ "reported when it does not."
|
|
|
|
|
+ ),
|
|
|
|
|
+ "parameters": {
|
|
|
|
|
+ "type": "object",
|
|
|
|
|
+ "properties": {
|
|
|
|
|
+ "name": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": "The new room's display name.",
|
|
|
|
|
+ },
|
|
|
|
|
+ "group_id": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "The room group to create the room in "
|
|
|
|
|
+ "(see chatto_list_room_groups)."
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ "description": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": "Optional room description (markdown).",
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ "required": ["name", "group_id"],
|
|
|
|
|
+ "additionalProperties": False,
|
|
|
|
|
+ },
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+CHATTO_LIST_ROOM_MEMBERS_SCHEMA = {
|
|
|
|
|
+ "name": "chatto_list_room_members",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "List a Chatto room's members with their login, display name and "
|
|
|
|
|
+ "presence, in pages. Use it to find who is in a room before mentioning "
|
|
|
|
|
+ "them or to resolve a user ID to a login."
|
|
|
|
|
+ ),
|
|
|
|
|
+ "parameters": {
|
|
|
|
|
+ "type": "object",
|
|
|
|
|
+ "properties": {
|
|
|
|
|
+ "room_id": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": "The Chatto room ID whose members to list.",
|
|
|
|
|
+ },
|
|
|
|
|
+ "room_name": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "Optional: human-readable room name — one of ``room_id`` or "
|
|
|
|
|
+ "``room_name`` is required."
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ "limit": {
|
|
|
|
|
+ "type": "integer",
|
|
|
|
|
+ "description": "Maximum members per page, 1-100 (default 50).",
|
|
|
|
|
+ },
|
|
|
|
|
+ "offset": {
|
|
|
|
|
+ "type": "integer",
|
|
|
|
|
+ "description": "Offset into the member list, for the next page.",
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ "required": [],
|
|
|
|
|
+ "additionalProperties": False,
|
|
|
|
|
+ },
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+CHATTO_GET_USER_SCHEMA = {
|
|
|
|
|
+ "name": "chatto_get_user",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "Look up a Chatto user in the member directory: display name, login, "
|
|
|
|
|
+ "presence and roles. Exactly one of ``user_id`` or ``user_login`` is "
|
|
|
|
|
+ "required."
|
|
|
|
|
+ ),
|
|
|
|
|
+ "parameters": {
|
|
|
|
|
+ "type": "object",
|
|
|
|
|
+ "properties": {
|
|
|
|
|
+ "user_id": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "The Chatto user ID (e.g. 'Uxxxxxxxx'). Exactly one of "
|
|
|
|
|
+ "`user_id` or `user_login` is required."
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ "user_login": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "The Chatto login name (e.g. 'nickk'). Exactly one of "
|
|
|
|
|
+ "`user_id` or `user_login` is required."
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ "required": [],
|
|
|
|
|
+ "additionalProperties": False,
|
|
|
|
|
+ },
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+CHATTO_SET_PRESENCE_SCHEMA = {
|
|
|
|
|
+ "name": "chatto_set_presence",
|
|
|
|
|
+ "description": (
|
|
|
|
|
+ "Set the bot's presence status in Chatto: 'online', 'away' or "
|
|
|
|
|
+ "'do_not_disturb'. The status expires server-side after about a "
|
|
|
|
|
+ "minute without refresh; while the gateway runs it is re-announced "
|
|
|
|
|
+ "automatically, so an explicit 'away' lasts until the next refresh "
|
|
|
|
|
+ "cycle unless the server supports preference persistence. 'offline' "
|
|
|
|
|
+ "cannot be set — presence lapses when the gateway disconnects."
|
|
|
|
|
+ ),
|
|
|
|
|
+ "parameters": {
|
|
|
|
|
+ "type": "object",
|
|
|
|
|
+ "properties": {
|
|
|
|
|
+ "status": {
|
|
|
|
|
+ "type": "string",
|
|
|
|
|
+ "enum": ["online", "away", "do_not_disturb"],
|
|
|
|
|
+ "description": "The presence status to announce.",
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ "required": ["status"],
|
|
|
|
|
+ "additionalProperties": False,
|
|
|
|
|
+ },
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
CHATTO_SET_CUSTOM_STATUS_SCHEMA = {
|
|
CHATTO_SET_CUSTOM_STATUS_SCHEMA = {
|
|
|
"name": "chatto_set_custom_status",
|
|
"name": "chatto_set_custom_status",
|
|
|
"description": (
|
|
"description": (
|
|
@@ -7387,39 +7693,503 @@ async def _handle_chatto_delete_message(args: dict) -> str:
|
|
|
return json.dumps({"success": False, "error": f"Chatto API error: {exc}"})
|
|
return json.dumps({"success": False, "error": f"Chatto API error: {exc}"})
|
|
|
|
|
|
|
|
|
|
|
|
|
-async def _delete_message(args: dict) -> str:
|
|
|
|
|
- """Delete one message in a Chatto room. See _handle_chatto_delete_message."""
|
|
|
|
|
- room_id = args.get("room_id") or ""
|
|
|
|
|
- room_name = args.get("room_name") or ""
|
|
|
|
|
- event_id = args.get("event_id") or ""
|
|
|
|
|
|
|
+async def _handle_chatto_edit_message(args: dict) -> str:
|
|
|
|
|
+ """Tool handler for chatto_edit_message. See _handle_chatto_delete_message."""
|
|
|
|
|
+ try:
|
|
|
|
|
+ return await _edit_message(args)
|
|
|
|
|
+ except _ToolRefusal as refusal:
|
|
|
|
|
+ return refusal.result_json
|
|
|
|
|
+ except ChattoAuthError as exc:
|
|
|
|
|
+ return json.dumps({"success": False, "error": f"Authentication error: {exc}"})
|
|
|
|
|
+ except ChattoError as exc:
|
|
|
|
|
+ return json.dumps({"success": False, "error": f"Chatto API error: {exc}"})
|
|
|
|
|
|
|
|
- if not room_id and not room_name:
|
|
|
|
|
- raise _refuse("Exactly one of room_id or room_name is required")
|
|
|
|
|
- if not event_id:
|
|
|
|
|
- raise _refuse("event_id is required")
|
|
|
|
|
|
|
|
|
|
- cfg = _chatto_config_for_tools()
|
|
|
|
|
- async with _tool_client(cfg) as client:
|
|
|
|
|
- resolved = await _resolve_room(client, room_id, room_name)
|
|
|
|
|
- room = resolved.room
|
|
|
|
|
|
|
+async def _handle_chatto_add_reaction(args: dict) -> str:
|
|
|
|
|
+ """Tool handler for chatto_add_reaction. See _handle_chatto_delete_message."""
|
|
|
|
|
+ try:
|
|
|
|
|
+ return await _add_reaction(args)
|
|
|
|
|
+ except _ToolRefusal as refusal:
|
|
|
|
|
+ return refusal.result_json
|
|
|
|
|
+ except ChattoAuthError as exc:
|
|
|
|
|
+ return json.dumps({"success": False, "error": f"Authentication error: {exc}"})
|
|
|
|
|
+ except ChattoError as exc:
|
|
|
|
|
+ return json.dumps({"success": False, "error": f"Chatto API error: {exc}"})
|
|
|
|
|
|
|
|
- # Deleting changes what the room shows everyone, so it is a write:
|
|
|
|
|
- # a room on neither mention list stays read-only, exactly as it does
|
|
|
|
|
- # for chatto_send_room and chatto_pin_message.
|
|
|
|
|
- if not _room_is_writable(cfg, room):
|
|
|
|
|
- raise _read_only_room_refusal(room.name or room.id)
|
|
|
|
|
- if not resolved.is_member:
|
|
|
|
|
- raise _not_joined_refusal(room, "delete a message there")
|
|
|
|
|
|
|
|
|
|
- deleted = await client.delete_message(room.id, event_id)
|
|
|
|
|
- return json.dumps(
|
|
|
|
|
- {
|
|
|
|
|
- "success": True,
|
|
|
|
|
- "deleted": bool(deleted),
|
|
|
|
|
- "room_id": room.id,
|
|
|
|
|
- "event_id": event_id,
|
|
|
|
|
- }
|
|
|
|
|
- )
|
|
|
|
|
|
|
+async def _handle_chatto_remove_reaction(args: dict) -> str:
|
|
|
|
|
+ """Tool handler for chatto_remove_reaction. See _handle_chatto_delete_message."""
|
|
|
|
|
+ try:
|
|
|
|
|
+ return await _remove_reaction(args)
|
|
|
|
|
+ except _ToolRefusal as refusal:
|
|
|
|
|
+ return refusal.result_json
|
|
|
|
|
+ except ChattoAuthError as exc:
|
|
|
|
|
+ return json.dumps({"success": False, "error": f"Authentication error: {exc}"})
|
|
|
|
|
+ except ChattoError as exc:
|
|
|
|
|
+ return json.dumps({"success": False, "error": f"Chatto API error: {exc}"})
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _handle_chatto_mark_room_as_read(args: dict) -> str:
|
|
|
|
|
+ """Tool handler for chatto_mark_room_as_read. See _handle_chatto_delete_message."""
|
|
|
|
|
+ try:
|
|
|
|
|
+ return await _mark_room_as_read(args)
|
|
|
|
|
+ except _ToolRefusal as refusal:
|
|
|
|
|
+ return refusal.result_json
|
|
|
|
|
+ except ChattoAuthError as exc:
|
|
|
|
|
+ return json.dumps({"success": False, "error": f"Authentication error: {exc}"})
|
|
|
|
|
+ except ChattoError as exc:
|
|
|
|
|
+ return json.dumps({"success": False, "error": f"Chatto API error: {exc}"})
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _handle_chatto_create_room(args: dict) -> str:
|
|
|
|
|
+ """Tool handler for chatto_create_room. See _handle_chatto_delete_message."""
|
|
|
|
|
+ try:
|
|
|
|
|
+ return await _create_room(args)
|
|
|
|
|
+ except _ToolRefusal as refusal:
|
|
|
|
|
+ return refusal.result_json
|
|
|
|
|
+ except ChattoAuthError as exc:
|
|
|
|
|
+ return json.dumps({"success": False, "error": f"Authentication error: {exc}"})
|
|
|
|
|
+ except ChattoError as exc:
|
|
|
|
|
+ return json.dumps({"success": False, "error": f"Chatto API error: {exc}"})
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _handle_chatto_list_room_members(args: dict) -> str:
|
|
|
|
|
+ """Tool handler for chatto_list_room_members. See _handle_chatto_delete_message."""
|
|
|
|
|
+ try:
|
|
|
|
|
+ return await _list_room_members(args)
|
|
|
|
|
+ except _ToolRefusal as refusal:
|
|
|
|
|
+ return refusal.result_json
|
|
|
|
|
+ except ChattoAuthError as exc:
|
|
|
|
|
+ return json.dumps({"success": False, "error": f"Authentication error: {exc}"})
|
|
|
|
|
+ except ChattoError as exc:
|
|
|
|
|
+ return json.dumps({"success": False, "error": f"Chatto API error: {exc}"})
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _handle_chatto_get_user(args: dict) -> str:
|
|
|
|
|
+ """Tool handler for chatto_get_user. See _handle_chatto_delete_message."""
|
|
|
|
|
+ try:
|
|
|
|
|
+ return await _get_user(args)
|
|
|
|
|
+ except _ToolRefusal as refusal:
|
|
|
|
|
+ return refusal.result_json
|
|
|
|
|
+ except ChattoAuthError as exc:
|
|
|
|
|
+ return json.dumps({"success": False, "error": f"Authentication error: {exc}"})
|
|
|
|
|
+ except ChattoError as exc:
|
|
|
|
|
+ return json.dumps({"success": False, "error": f"Chatto API error: {exc}"})
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _handle_chatto_set_presence(args: dict) -> str:
|
|
|
|
|
+ """Tool handler for chatto_set_presence. See _handle_chatto_delete_message."""
|
|
|
|
|
+ try:
|
|
|
|
|
+ return await _set_presence(args)
|
|
|
|
|
+ except _ToolRefusal as refusal:
|
|
|
|
|
+ return refusal.result_json
|
|
|
|
|
+ except ChattoAuthError as exc:
|
|
|
|
|
+ return json.dumps({"success": False, "error": f"Authentication error: {exc}"})
|
|
|
|
|
+ except ChattoError as exc:
|
|
|
|
|
+ return json.dumps({"success": False, "error": f"Chatto API error: {exc}"})
|
|
|
|
|
+ except ValueError as exc:
|
|
|
|
|
+ # chattolib refuses UNSPECIFIED/OFFLINE outright with a ValueError.
|
|
|
|
|
+ return json.dumps({"success": False, "error": str(exc)})
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _handle_chatto_upload_file(args: dict) -> str:
|
|
|
|
|
+ """Tool handler for chatto_upload_file. See _handle_chatto_delete_message."""
|
|
|
|
|
+ try:
|
|
|
|
|
+ return await _upload_file(args)
|
|
|
|
|
+ except _ToolRefusal as refusal:
|
|
|
|
|
+ return refusal.result_json
|
|
|
|
|
+ except ChattoAuthError as exc:
|
|
|
|
|
+ return json.dumps({"success": False, "error": f"Authentication error: {exc}"})
|
|
|
|
|
+ except ChattoError as exc:
|
|
|
|
|
+ return json.dumps({"success": False, "error": f"Chatto API error: {exc}"})
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _delete_message(args: dict) -> str:
|
|
|
|
|
+ """Delete one message in a Chatto room. See _handle_chatto_delete_message."""
|
|
|
|
|
+ room_id = args.get("room_id") or ""
|
|
|
|
|
+ room_name = args.get("room_name") or ""
|
|
|
|
|
+ event_id = args.get("event_id") or ""
|
|
|
|
|
+
|
|
|
|
|
+ if not room_id and not room_name:
|
|
|
|
|
+ raise _refuse("Exactly one of room_id or room_name is required")
|
|
|
|
|
+ if not event_id:
|
|
|
|
|
+ raise _refuse("event_id is required")
|
|
|
|
|
+
|
|
|
|
|
+ cfg = _chatto_config_for_tools()
|
|
|
|
|
+ async with _tool_client(cfg) as client:
|
|
|
|
|
+ resolved = await _resolve_room(client, room_id, room_name)
|
|
|
|
|
+ room = resolved.room
|
|
|
|
|
+
|
|
|
|
|
+ # Deleting changes what the room shows everyone, so it is a write:
|
|
|
|
|
+ # a room on neither mention list stays read-only, exactly as it does
|
|
|
|
|
+ # for chatto_send_room and chatto_pin_message.
|
|
|
|
|
+ if not _room_is_writable(cfg, room):
|
|
|
|
|
+ raise _read_only_room_refusal(room.name or room.id)
|
|
|
|
|
+ if not resolved.is_member:
|
|
|
|
|
+ raise _not_joined_refusal(room, "delete a message there")
|
|
|
|
|
+
|
|
|
|
|
+ deleted = await client.delete_message(room.id, event_id)
|
|
|
|
|
+ return json.dumps(
|
|
|
|
|
+ {
|
|
|
|
|
+ "success": True,
|
|
|
|
|
+ "deleted": bool(deleted),
|
|
|
|
|
+ "room_id": room.id,
|
|
|
|
|
+ "event_id": event_id,
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _edit_message(args: dict) -> str:
|
|
|
|
|
+ """Edit one message's text. See _handle_chatto_edit_message."""
|
|
|
|
|
+ room_id = args.get("room_id") or ""
|
|
|
|
|
+ room_name = args.get("room_name") or ""
|
|
|
|
|
+ event_id = args.get("event_id") or ""
|
|
|
|
|
+ message = args.get("message") or ""
|
|
|
|
|
+
|
|
|
|
|
+ if not room_id and not room_name:
|
|
|
|
|
+ raise _refuse("Exactly one of room_id or room_name is required")
|
|
|
|
|
+ if not event_id:
|
|
|
|
|
+ raise _refuse("event_id is required")
|
|
|
|
|
+ if not message:
|
|
|
|
|
+ raise _refuse("message (the full new text) is required")
|
|
|
|
|
+
|
|
|
|
|
+ cfg = _chatto_config_for_tools()
|
|
|
|
|
+ async with _tool_client(cfg) as client:
|
|
|
|
|
+ resolved = await _resolve_room(client, room_id, room_name)
|
|
|
|
|
+ room = resolved.room
|
|
|
|
|
+
|
|
|
|
|
+ # An edit is a write like any other: the read-only promise holds.
|
|
|
|
|
+ if not _room_is_writable(cfg, room):
|
|
|
|
|
+ raise _read_only_room_refusal(room.name or room.id)
|
|
|
|
|
+ if not resolved.is_member:
|
|
|
|
|
+ raise _not_joined_refusal(room, "edit a message there")
|
|
|
|
|
+
|
|
|
|
|
+ formatted = _normalise_outbound_text(message)
|
|
|
|
|
+ if len(formatted) > ChattoConstants.MAX_MESSAGE_LENGTH:
|
|
|
|
|
+ raise _refuse(
|
|
|
|
|
+ f"message exceeds the {ChattoConstants.MAX_MESSAGE_LENGTH}-char "
|
|
|
|
|
+ f"per-message limit ({len(formatted)}); it was not sent"
|
|
|
|
|
+ )
|
|
|
|
|
+ await client.update_message(room.id, event_id, body=formatted)
|
|
|
|
|
+ return json.dumps(
|
|
|
|
|
+ {
|
|
|
|
|
+ "success": True,
|
|
|
|
|
+ "room_id": room.id,
|
|
|
|
|
+ "event_id": event_id,
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _emoji_shortcode(emoji: str) -> str:
|
|
|
|
|
+ """The shortcode name a reaction must be sent as (see _shortcode_for_reaction_emoji)."""
|
|
|
|
|
+ return _shortcode_for_reaction_emoji(emoji)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _reaction_result(action: str, room: Room, event_id: str, emoji: str, done: bool) -> str:
|
|
|
|
|
+ """One reaction tool's result shape, shared by add and remove."""
|
|
|
|
|
+ result: dict[str, Any] = {
|
|
|
|
|
+ "success": True,
|
|
|
|
|
+ "action": action,
|
|
|
|
|
+ "room_id": room.id,
|
|
|
|
|
+ "event_id": event_id,
|
|
|
|
|
+ "emoji": emoji,
|
|
|
|
|
+ }
|
|
|
|
|
+ if action == "add":
|
|
|
|
|
+ result["added"] = bool(done)
|
|
|
|
|
+ else:
|
|
|
|
|
+ result["removed"] = bool(done)
|
|
|
|
|
+ return json.dumps(result)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _add_reaction(args: dict) -> str:
|
|
|
|
|
+ """Add an emoji reaction. See _handle_chatto_add_reaction."""
|
|
|
|
|
+ room_id = args.get("room_id") or ""
|
|
|
|
|
+ room_name = args.get("room_name") or ""
|
|
|
|
|
+ event_id = args.get("event_id") or ""
|
|
|
|
|
+ emoji = (args.get("emoji") or "").strip()
|
|
|
|
|
+
|
|
|
|
|
+ if not room_id and not room_name:
|
|
|
|
|
+ raise _refuse("Exactly one of room_id or room_name is required")
|
|
|
|
|
+ if not event_id:
|
|
|
|
|
+ raise _refuse("event_id is required")
|
|
|
|
|
+ if not emoji:
|
|
|
|
|
+ raise _refuse("emoji is required")
|
|
|
|
|
+
|
|
|
|
|
+ cfg = _chatto_config_for_tools()
|
|
|
|
|
+ async with _tool_client(cfg) as client:
|
|
|
|
|
+ resolved = await _resolve_room(client, room_id, room_name)
|
|
|
|
|
+ room = resolved.room
|
|
|
|
|
+
|
|
|
|
|
+ # A reaction is visible to everyone — a write like a pin.
|
|
|
|
|
+ if not _room_is_writable(cfg, room):
|
|
|
|
|
+ raise _read_only_room_refusal(room.name or room.id)
|
|
|
|
|
+ if not resolved.is_member:
|
|
|
|
|
+ raise _not_joined_refusal(room, "react there")
|
|
|
|
|
+
|
|
|
|
|
+ shortcode = _emoji_shortcode(emoji)
|
|
|
|
|
+ added = await client.add_reaction(room.id, event_id, shortcode)
|
|
|
|
|
+ return _reaction_result("add", room, event_id, shortcode, added)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _remove_reaction(args: dict) -> str:
|
|
|
|
|
+ """Remove an emoji reaction. See _handle_chatto_remove_reaction."""
|
|
|
|
|
+ room_id = args.get("room_id") or ""
|
|
|
|
|
+ room_name = args.get("room_name") or ""
|
|
|
|
|
+ event_id = args.get("event_id") or ""
|
|
|
|
|
+ emoji = (args.get("emoji") or "").strip()
|
|
|
|
|
+
|
|
|
|
|
+ if not room_id and not room_name:
|
|
|
|
|
+ raise _refuse("Exactly one of room_id or room_name is required")
|
|
|
|
|
+ if not event_id:
|
|
|
|
|
+ raise _refuse("event_id is required")
|
|
|
|
|
+ if not emoji:
|
|
|
|
|
+ raise _refuse("emoji is required")
|
|
|
|
|
+
|
|
|
|
|
+ cfg = _chatto_config_for_tools()
|
|
|
|
|
+ async with _tool_client(cfg) as client:
|
|
|
|
|
+ resolved = await _resolve_room(client, room_id, room_name)
|
|
|
|
|
+ room = resolved.room
|
|
|
|
|
+
|
|
|
|
|
+ if not _room_is_writable(cfg, room):
|
|
|
|
|
+ raise _read_only_room_refusal(room.name or room.id)
|
|
|
|
|
+ if not resolved.is_member:
|
|
|
|
|
+ raise _not_joined_refusal(room, "react there")
|
|
|
|
|
+
|
|
|
|
|
+ shortcode = _emoji_shortcode(emoji)
|
|
|
|
|
+ removed = await client.remove_reaction(room.id, event_id, shortcode)
|
|
|
|
|
+ return _reaction_result("remove", room, event_id, shortcode, removed)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _mark_room_as_read(args: dict) -> str:
|
|
|
|
|
+ """Mark a room read for the bot. See _handle_chatto_mark_room_as_read."""
|
|
|
|
|
+ room_id = args.get("room_id") or ""
|
|
|
|
|
+ room_name = args.get("room_name") or ""
|
|
|
|
|
+ up_to_event_id = args.get("up_to_event_id") or ""
|
|
|
|
|
+
|
|
|
|
|
+ if not room_id and not room_name:
|
|
|
|
|
+ raise _refuse("Exactly one of room_id or room_name is required")
|
|
|
|
|
+
|
|
|
|
|
+ async with _tool_client(_chatto_config_for_tools()) as client:
|
|
|
|
|
+ resolved = await _resolve_room(client, room_id, room_name)
|
|
|
|
|
+ room = resolved.room
|
|
|
|
|
+
|
|
|
|
|
+ if not resolved.is_member:
|
|
|
|
|
+ # A read marker is private state, but the server scopes it to
|
|
|
|
|
+ # membership — same remedy as every other room-scoped call.
|
|
|
|
|
+ raise _not_joined_refusal(room, "mark it read")
|
|
|
|
|
+
|
|
|
|
|
+ last_read, previous = await client.mark_room_as_read(room.id, up_to_event_id)
|
|
|
|
|
+ return json.dumps(
|
|
|
|
|
+ {
|
|
|
|
|
+ "success": True,
|
|
|
|
|
+ "room_id": room.id,
|
|
|
|
|
+ "last_read_at": _iso_or_none(last_read),
|
|
|
|
|
+ "previous_last_read_at": _iso_or_none(previous),
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _create_room(args: dict) -> str:
|
|
|
|
|
+ """Create a channel in a room group. See _handle_chatto_create_room."""
|
|
|
|
|
+ name = (args.get("name") or "").strip()
|
|
|
|
|
+ group_id = (args.get("group_id") or "").strip()
|
|
|
|
|
+ description = args.get("description") or ""
|
|
|
|
|
+
|
|
|
|
|
+ if not name:
|
|
|
|
|
+ raise _refuse("name is required")
|
|
|
|
|
+ if not group_id:
|
|
|
|
|
+ raise _refuse(
|
|
|
|
|
+ "group_id is required — chatto_list_room_groups lists the groups"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ async with _tool_client(_chatto_config_for_tools()) as client:
|
|
|
|
|
+ room = await client.create_room(
|
|
|
|
|
+ name=name, group_id=group_id, description=description
|
|
|
|
|
+ )
|
|
|
|
|
+ return json.dumps(
|
|
|
|
|
+ {
|
|
|
|
|
+ "success": True,
|
|
|
|
|
+ "room_id": room.id,
|
|
|
|
|
+ "name": room.name,
|
|
|
|
|
+ "kind": room.kind.value,
|
|
|
|
|
+ "hint": (
|
|
|
|
|
+ "The bot is not a member of the new room yet. Invite it in "
|
|
|
|
|
+ "Chatto, or DM it `/join "
|
|
|
|
|
+ f"{room.id}`"
|
|
|
|
|
+ ),
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _member_out(member: DirectoryMember) -> dict[str, Any]:
|
|
|
|
|
+ """One directory member as the tools' JSON shape."""
|
|
|
|
|
+ user = member.user
|
|
|
|
|
+ out: dict[str, Any] = {
|
|
|
|
|
+ "roles": list(member.roles or []),
|
|
|
|
|
+ }
|
|
|
|
|
+ if user is not None:
|
|
|
|
|
+ out.update(
|
|
|
|
|
+ {
|
|
|
|
|
+ "user_id": user.id,
|
|
|
|
|
+ "login": user.login,
|
|
|
|
|
+ "display_name": user.display_name,
|
|
|
|
|
+ "presence": _PRESENCE_LABELS.get(user.presence_status, ""),
|
|
|
|
|
+ "deleted": user.deleted,
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ return out
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _list_room_members(args: dict) -> str:
|
|
|
|
|
+ """List a room's members. See _handle_chatto_list_room_members."""
|
|
|
|
|
+ room_id = args.get("room_id") or ""
|
|
|
|
|
+ room_name = args.get("room_name") or ""
|
|
|
|
|
+ limit = _int_arg(args, "limit", 50, low=1, high=100)
|
|
|
|
|
+ offset = _int_arg(args, "offset", 0, low=0, high=100_000)
|
|
|
|
|
+
|
|
|
|
|
+ if not room_id and not room_name:
|
|
|
|
|
+ raise _refuse("Exactly one of room_id or room_name is required")
|
|
|
|
|
+
|
|
|
|
|
+ async with _tool_client(_chatto_config_for_tools()) as client:
|
|
|
|
|
+ resolved = await _resolve_room(client, room_id, room_name)
|
|
|
|
|
+ room = resolved.room
|
|
|
|
|
+
|
|
|
|
|
+ # The server enforces room membership for the member directory too,
|
|
|
|
|
+ # so an unjoined room is refused with the usual remedy.
|
|
|
|
|
+ if not resolved.is_member:
|
|
|
|
|
+ raise _not_joined_refusal(room, "list its members")
|
|
|
|
|
+
|
|
|
|
|
+ member_ids, page = await client.list_room_members(
|
|
|
|
|
+ room.id, limit=limit, offset=offset
|
|
|
|
|
+ )
|
|
|
|
|
+ members = await client.batch_get_room_members(room.id, member_ids)
|
|
|
|
|
+ return json.dumps(
|
|
|
|
|
+ {
|
|
|
|
|
+ "success": True,
|
|
|
|
|
+ "room_id": room.id,
|
|
|
|
|
+ "count": len(members),
|
|
|
|
|
+ "offset": offset,
|
|
|
|
|
+ "has_more": page.has_more,
|
|
|
|
|
+ "total_count": page.total_count,
|
|
|
|
|
+ "members": [_member_out(member) for member in members],
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _get_user(args: dict) -> str:
|
|
|
|
|
+ """Look up one user. See _handle_chatto_get_user."""
|
|
|
|
|
+ user_id = (args.get("user_id") or "").strip()
|
|
|
|
|
+ user_login = (args.get("user_login") or "").strip()
|
|
|
|
|
+
|
|
|
|
|
+ if bool(user_id) == bool(user_login):
|
|
|
|
|
+ raise _refuse("Exactly one of user_id or user_login is required")
|
|
|
|
|
+
|
|
|
|
|
+ async with _tool_client(_chatto_config_for_tools()) as client:
|
|
|
|
|
+ if user_id:
|
|
|
|
|
+ matches = await client.batch_get_users([user_id])
|
|
|
|
|
+ member = matches[0] if matches else None
|
|
|
|
|
+ else:
|
|
|
|
|
+ # A login needs the server-wide directory, which batch_get_users
|
|
|
|
|
+ # cannot search; list_users' search does.
|
|
|
|
|
+ members, _page = await client.list_users(search=user_login, limit=25)
|
|
|
|
|
+ member = next(
|
|
|
|
|
+ (
|
|
|
|
|
+ m
|
|
|
|
|
+ for m in members
|
|
|
|
|
+ if m.user is not None
|
|
|
|
|
+ and m.user.login.casefold() == user_login.casefold()
|
|
|
|
|
+ ),
|
|
|
|
|
+ None,
|
|
|
|
|
+ )
|
|
|
|
|
+ if member is None or member.user is None:
|
|
|
|
|
+ raise _refuse(
|
|
|
|
|
+ f"No user found for "
|
|
|
|
|
+ f"{'user_id ' + user_id if user_id else 'login ' + user_login}. "
|
|
|
|
|
+ "The directory search may be scoped to members of rooms the "
|
|
|
|
|
+ "bot can see."
|
|
|
|
|
+ )
|
|
|
|
|
+ return json.dumps({"success": True, **_member_out(member)})
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+_PRESENCE_BY_NAME = {
|
|
|
|
|
+ "online": PresenceStatus.ONLINE,
|
|
|
|
|
+ "away": PresenceStatus.AWAY,
|
|
|
|
|
+ "do_not_disturb": PresenceStatus.DO_NOT_DISTURB,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _set_presence(args: dict) -> str:
|
|
|
|
|
+ """Set the bot's presence status. See _handle_chatto_set_presence."""
|
|
|
|
|
+ status_name = (args.get("status") or "").strip()
|
|
|
|
|
+ status = _PRESENCE_BY_NAME.get(status_name)
|
|
|
|
|
+ if status is None:
|
|
|
|
|
+ raise _refuse(
|
|
|
|
|
+ "status must be one of: online, away, do_not_disturb"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ async with _tool_client(_chatto_config_for_tools()) as client:
|
|
|
|
|
+ set_status = await client.set_presence(status)
|
|
|
|
|
+ return json.dumps(
|
|
|
|
|
+ {
|
|
|
|
|
+ "success": True,
|
|
|
|
|
+ "status": _PRESENCE_LABELS.get(set_status, set_status.value),
|
|
|
|
|
+ "note": (
|
|
|
|
|
+ "Presence expires server-side without refresh; the running "
|
|
|
|
|
+ "gateway re-announces it periodically."
|
|
|
|
|
+ ),
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _upload_file(args: dict) -> str:
|
|
|
|
|
+ """Upload a local file and post it. See _handle_chatto_upload_file."""
|
|
|
|
|
+ room_id = args.get("room_id") or ""
|
|
|
|
|
+ room_name = args.get("room_name") or ""
|
|
|
|
|
+ file_path = (args.get("file_path") or "").strip()
|
|
|
|
|
+ caption = args.get("caption") or ""
|
|
|
|
|
+
|
|
|
|
|
+ if not room_id and not room_name:
|
|
|
|
|
+ raise _refuse("Exactly one of room_id or room_name is required")
|
|
|
|
|
+ if not file_path:
|
|
|
|
|
+ raise _refuse("file_path is required")
|
|
|
|
|
+
|
|
|
|
|
+ # Same path gate as the adapter's own sends: a model-named path must not
|
|
|
|
|
+ # become a channel for reading host secrets into the chat.
|
|
|
|
|
+ safe_path = validate_media_delivery_path(file_path)
|
|
|
|
|
+ if not safe_path:
|
|
|
|
|
+ raise _refuse(
|
|
|
|
|
+ f"file_path '{file_path}' is not a deliverable file on this host "
|
|
|
|
|
+ "(missing, or outside the paths allowed for media delivery)"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ cfg = _chatto_config_for_tools()
|
|
|
|
|
+ async with _tool_client(cfg) as client:
|
|
|
|
|
+ resolved = await _resolve_room(client, room_id, room_name)
|
|
|
|
|
+ room = resolved.room
|
|
|
|
|
+
|
|
|
|
|
+ if not _room_is_writable(cfg, room):
|
|
|
|
|
+ raise _read_only_room_refusal(room.name or room.id)
|
|
|
|
|
+ if not resolved.is_member:
|
|
|
|
|
+ raise _not_joined_refusal(room, "post an attachment there")
|
|
|
|
|
+
|
|
|
|
|
+ asset = await client.upload_attachment(
|
|
|
|
|
+ room.id, safe_path, filename=Path(safe_path).name
|
|
|
|
|
+ )
|
|
|
|
|
+ message = await client.post_message(
|
|
|
|
|
+ room.id,
|
|
|
|
|
+ _normalise_outbound_text(caption),
|
|
|
|
|
+ attachment_asset_ids=[asset.id],
|
|
|
|
|
+ )
|
|
|
|
|
+ return json.dumps(
|
|
|
|
|
+ {
|
|
|
|
|
+ "success": True,
|
|
|
|
|
+ "room_id": room.id,
|
|
|
|
|
+ "message_id": message.id,
|
|
|
|
|
+ "asset_id": asset.id,
|
|
|
|
|
+ "filename": asset.filename,
|
|
|
|
|
+ "size": asset.size,
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
|
|
|
|
|
def _register_bundled_skills(ctx) -> None:
|
|
def _register_bundled_skills(ctx) -> None:
|
|
@@ -7490,6 +8260,51 @@ _PLUGIN_TOOLS = (
|
|
|
CHATTO_DELETE_MESSAGE_SCHEMA,
|
|
CHATTO_DELETE_MESSAGE_SCHEMA,
|
|
|
_handle_chatto_delete_message,
|
|
_handle_chatto_delete_message,
|
|
|
),
|
|
),
|
|
|
|
|
+ (
|
|
|
|
|
+ "chatto_edit_message",
|
|
|
|
|
+ CHATTO_EDIT_MESSAGE_SCHEMA,
|
|
|
|
|
+ _handle_chatto_edit_message,
|
|
|
|
|
+ ),
|
|
|
|
|
+ (
|
|
|
|
|
+ "chatto_upload_file",
|
|
|
|
|
+ CHATTO_UPLOAD_FILE_SCHEMA,
|
|
|
|
|
+ _handle_chatto_upload_file,
|
|
|
|
|
+ ),
|
|
|
|
|
+ (
|
|
|
|
|
+ "chatto_add_reaction",
|
|
|
|
|
+ CHATTO_ADD_REACTION_SCHEMA,
|
|
|
|
|
+ _handle_chatto_add_reaction,
|
|
|
|
|
+ ),
|
|
|
|
|
+ (
|
|
|
|
|
+ "chatto_remove_reaction",
|
|
|
|
|
+ CHATTO_REMOVE_REACTION_SCHEMA,
|
|
|
|
|
+ _handle_chatto_remove_reaction,
|
|
|
|
|
+ ),
|
|
|
|
|
+ (
|
|
|
|
|
+ "chatto_mark_room_as_read",
|
|
|
|
|
+ CHATTO_MARK_ROOM_AS_READ_SCHEMA,
|
|
|
|
|
+ _handle_chatto_mark_room_as_read,
|
|
|
|
|
+ ),
|
|
|
|
|
+ (
|
|
|
|
|
+ "chatto_create_room",
|
|
|
|
|
+ CHATTO_CREATE_ROOM_SCHEMA,
|
|
|
|
|
+ _handle_chatto_create_room,
|
|
|
|
|
+ ),
|
|
|
|
|
+ (
|
|
|
|
|
+ "chatto_list_room_members",
|
|
|
|
|
+ CHATTO_LIST_ROOM_MEMBERS_SCHEMA,
|
|
|
|
|
+ _handle_chatto_list_room_members,
|
|
|
|
|
+ ),
|
|
|
|
|
+ (
|
|
|
|
|
+ "chatto_get_user",
|
|
|
|
|
+ CHATTO_GET_USER_SCHEMA,
|
|
|
|
|
+ _handle_chatto_get_user,
|
|
|
|
|
+ ),
|
|
|
|
|
+ (
|
|
|
|
|
+ "chatto_set_presence",
|
|
|
|
|
+ CHATTO_SET_PRESENCE_SCHEMA,
|
|
|
|
|
+ _handle_chatto_set_presence,
|
|
|
|
|
+ ),
|
|
|
(
|
|
(
|
|
|
"chatto_set_custom_status",
|
|
"chatto_set_custom_status",
|
|
|
CHATTO_SET_CUSTOM_STATUS_SCHEMA,
|
|
CHATTO_SET_CUSTOM_STATUS_SCHEMA,
|