实现了完整的Flock渠道接入能力,包含消息收发、Webhook事件监听、账号配置、安全校验、媒体文件处理等功能,支持私聊和群组聊天,适配ForcePilot插件规范。
213 lines
6.7 KiB
Python
213 lines
6.7 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from .config import _apply_env_overrides, _dict_to_account
|
|
from .constants import (
|
|
ENDPOINT_SEND_MESSAGE,
|
|
MAX_TEXT_LENGTH,
|
|
)
|
|
from .format import chunk_text, markdown_to_flockml, strip_markdown_for_plain
|
|
from .types import FlockOutboundResult
|
|
from .utils import create_http_client, flock_api_call
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
text_chunk_limit = MAX_TEXT_LENGTH
|
|
|
|
|
|
def chunker(text: str, limit: int, ctx: object | None = None) -> list[str]:
|
|
return chunk_text(text, max(limit, 100))
|
|
|
|
|
|
async def send_text(
|
|
target_id: str,
|
|
content: str,
|
|
*,
|
|
reply_to_id: str | None = None,
|
|
thread_id: str | None = None,
|
|
mention_ids: list[str] | None = None,
|
|
notification: str | None = None,
|
|
send_as: dict | None = None,
|
|
on_behalf_of: str | None = None,
|
|
visible_to: list[str] | None = None,
|
|
account_id: str = "default",
|
|
config: dict,
|
|
) -> FlockOutboundResult:
|
|
account_data = config.get("accounts", {}).get(account_id, {})
|
|
account = _dict_to_account(account_data)
|
|
account = _apply_env_overrides(account)
|
|
|
|
if not account.bot_token and not account.incoming_webhook_url:
|
|
return FlockOutboundResult(
|
|
success=False,
|
|
error="No valid send method configured (bot_token or incoming_webhook_url required)",
|
|
)
|
|
|
|
flockml = markdown_to_flockml(content)
|
|
plain_text = strip_markdown_for_plain(content)
|
|
|
|
if account.bot_token:
|
|
return await _send_via_bot_token(
|
|
account.bot_token, target_id, plain_text, flockml,
|
|
reply_to_id, thread_id, mention_ids, notification, send_as, on_behalf_of, visible_to,
|
|
)
|
|
|
|
if account.incoming_webhook_url:
|
|
return await _send_via_webhook(account.incoming_webhook_url, plain_text, flockml)
|
|
|
|
return FlockOutboundResult(success=False, error="No valid send method configured")
|
|
|
|
|
|
async def _send_via_bot_token(
|
|
bot_token: str,
|
|
target_id: str,
|
|
plain_text: str,
|
|
flockml: str,
|
|
reply_to_id: str | None,
|
|
thread_id: str | None,
|
|
mention_ids: list[str] | None,
|
|
notification: str | None,
|
|
send_as: dict | None,
|
|
on_behalf_of: str | None,
|
|
visible_to: list[str] | None,
|
|
) -> FlockOutboundResult:
|
|
payload: dict = {
|
|
"to": target_id,
|
|
"text": plain_text,
|
|
"flockml": flockml,
|
|
}
|
|
if thread_id:
|
|
payload["threadId"] = thread_id
|
|
if mention_ids:
|
|
payload["mentions"] = mention_ids
|
|
if notification:
|
|
payload["notification"] = notification
|
|
if send_as:
|
|
payload["sendAs"] = send_as
|
|
if on_behalf_of:
|
|
payload["onBehalfOf"] = on_behalf_of
|
|
if visible_to:
|
|
payload["visibleTo"] = visible_to
|
|
|
|
client = create_http_client()
|
|
try:
|
|
result = await flock_api_call(client, ENDPOINT_SEND_MESSAGE, bot_token, payload)
|
|
msg_uid = result.get("uid", "")
|
|
logger.info("Flock message sent to %s, uid=%s", target_id, msg_uid)
|
|
return FlockOutboundResult(success=True, message_uid=msg_uid)
|
|
except Exception as e:
|
|
logger.error("Flock send via bot token failed: %s", e)
|
|
retryable = getattr(e, "retryable", False)
|
|
return FlockOutboundResult(success=False, error=str(e), retryable=retryable)
|
|
finally:
|
|
await client.aclose()
|
|
|
|
|
|
async def _send_via_webhook(
|
|
incoming_webhook_url: str,
|
|
plain_text: str,
|
|
flockml: str,
|
|
) -> FlockOutboundResult:
|
|
client = create_http_client()
|
|
try:
|
|
resp = await client.post(
|
|
incoming_webhook_url,
|
|
json={"text": plain_text, "flockml": flockml},
|
|
)
|
|
if resp.status_code == 200:
|
|
logger.info("Flock message sent via incoming webhook")
|
|
return FlockOutboundResult(success=True)
|
|
logger.warning("Flock webhook send failed: HTTP %d", resp.status_code)
|
|
return FlockOutboundResult(
|
|
success=False,
|
|
error=f"Incoming webhook returned HTTP {resp.status_code}",
|
|
)
|
|
except Exception as e:
|
|
logger.error("Flock webhook send error: %s", e)
|
|
return FlockOutboundResult(success=False, error=str(e))
|
|
finally:
|
|
await client.aclose()
|
|
|
|
|
|
async def send_media(
|
|
target_id: str,
|
|
media_url: str,
|
|
media_type: str,
|
|
*,
|
|
reply_to_id: str | None = None,
|
|
thread_id: str | None = None,
|
|
account_id: str = "default",
|
|
config: dict,
|
|
) -> FlockOutboundResult:
|
|
account_data = config.get("accounts", {}).get(account_id, {})
|
|
account = _dict_to_account(account_data)
|
|
account = _apply_env_overrides(account)
|
|
|
|
if not account.bot_token:
|
|
return FlockOutboundResult(success=False, error="bot_token required for media upload")
|
|
|
|
if media_type == "image":
|
|
payload: dict = {
|
|
"to": target_id,
|
|
"text": "",
|
|
"attachments": [
|
|
{
|
|
"title": "Image",
|
|
"views": {
|
|
"image": {
|
|
"original": {"src": media_url},
|
|
"thumbnail": {"src": media_url},
|
|
}
|
|
},
|
|
}
|
|
],
|
|
}
|
|
if thread_id:
|
|
payload["threadId"] = thread_id
|
|
|
|
client = create_http_client()
|
|
try:
|
|
result = await flock_api_call(client, ENDPOINT_SEND_MESSAGE, account.bot_token, payload)
|
|
return FlockOutboundResult(success=True, message_uid=result.get("uid", ""))
|
|
except Exception as e:
|
|
logger.error("Flock image send failed: %s", e)
|
|
retryable = getattr(e, "retryable", False)
|
|
return FlockOutboundResult(success=False, error=str(e), retryable=retryable)
|
|
finally:
|
|
await client.aclose()
|
|
|
|
return FlockOutboundResult(
|
|
success=False,
|
|
error=f"Unsupported media type: {media_type}. Use 'image' or use media.upload_file for files.",
|
|
)
|
|
|
|
|
|
async def send_payload(
|
|
ctx: object,
|
|
config: dict,
|
|
account_id: str = "default",
|
|
) -> FlockOutboundResult:
|
|
target_id = getattr(ctx, "target_id", "")
|
|
content = getattr(ctx, "content", "")
|
|
thread_id = getattr(ctx, "thread_id", None)
|
|
reply_to_id = getattr(ctx, "reply_to_id", None)
|
|
mention_ids = getattr(ctx, "mention_ids", None)
|
|
notification = getattr(ctx, "notification", None)
|
|
send_as = getattr(ctx, "send_as", None)
|
|
on_behalf_of = getattr(ctx, "on_behalf_of", None)
|
|
visible_to = getattr(ctx, "visible_to", None)
|
|
return await send_text(
|
|
target_id,
|
|
content,
|
|
reply_to_id=reply_to_id,
|
|
thread_id=thread_id,
|
|
mention_ids=mention_ids,
|
|
notification=notification,
|
|
send_as=send_as,
|
|
on_behalf_of=on_behalf_of,
|
|
visible_to=visible_to,
|
|
account_id=account_id,
|
|
config=config,
|
|
)
|