feat(nextcloudtalk): 新增投票和消息置顶相关功能,优化代码结构

1. 新增close_poll、get_poll_results方法实现投票管理
2. 新增pin_message、unpin_message、list_pins实现消息置顶功能
3. 调整导入顺序优化代码整洁度
4. 修复去重逻辑的判断顺序问题
5. 清理多余的空行和导入冗余
This commit is contained in:
Kris 2026-05-13 16:12:46 +08:00
parent 939f1ba82a
commit b317387e0c
4 changed files with 151 additions and 19 deletions

View File

@ -11,11 +11,11 @@ from urllib.parse import urlparse
from yuxi.channels.base import BaseChannelAdapter
from yuxi.channels.capabilities import ChannelCapabilities
from yuxi.channels.meta import ChannelMeta
from yuxi.channels.infra.circuit_breaker import CircuitBreaker, CircuitBreakerOpenError
from yuxi.channels.exceptions import (
ChannelAuthenticationError,
)
from yuxi.channels.infra.circuit_breaker import CircuitBreaker, CircuitBreakerOpenError
from yuxi.channels.meta import ChannelMeta
from yuxi.channels.models import (
ChannelMessage,
ChannelResponse,
@ -30,12 +30,12 @@ from yuxi.utils.datetime_utils import utc_now_naive
from .client import NextcloudTalkClient, verify_hmac_signature
from .config import (
resolve_room_enabled,
resolve_room_system_prompt,
resolve_room_skills,
resolve_dm_system_prompt,
resolve_dm_enabled,
resolve_dm_skills,
resolve_dm_system_prompt,
resolve_room_enabled,
resolve_room_skills,
resolve_room_system_prompt,
)
from .dedup import NextcloudTalkDedupGuard
from .formatter import format_outbound
@ -47,15 +47,21 @@ from .pairing import send_pairing_challenge
from .probe import probe_capabilities, resolve_api_base, resolve_api_version
from .security import check_dm_policy, check_group_policy, check_mention_gate, collect_security_warnings
from .send import (
send_media,
send_reaction,
send_text,
send_reaction_delete,
get_reactions as _get_reactions,
edit_message as _send_edit,
delete_message as _send_delete,
)
from .session import resolve_session_route, make_thread_key
from .send import (
edit_message as _send_edit,
)
from .send import (
get_reactions as _get_reactions,
)
from .send import (
send_media,
send_reaction,
send_reaction_delete,
send_text,
)
from .session import make_thread_key, resolve_session_route
logger = logging.getLogger(__name__)
@ -109,6 +115,11 @@ class NextcloudTalkAdapter(BaseChannelAdapter):
media=True,
block_streaming=True,
polls=True,
approval=True,
vision=True,
pin=True,
unpin=True,
list_pins=True,
)
meta = ChannelMeta(
id="nextcloud-talk",
@ -790,6 +801,131 @@ class NextcloudTalkAdapter(BaseChannelAdapter):
except Exception as e:
return DeliveryResult(success=False, error=str(e))
async def close_poll(self, chat_id: str, poll_id: str) -> DeliveryResult:
if not self._client:
return DeliveryResult(success=False, error="Channel not connected")
async def _do_close():
path = f"{self._api_base}/poll/{chat_id}/{poll_id}"
try:
result = await self._client.delete(path)
ocs = result.get("ocs", {})
if ocs.get("meta", {}).get("status") == "ok":
return DeliveryResult(success=True, message_id=poll_id)
return DeliveryResult(
success=False,
error=ocs.get("meta", {}).get("message", "Failed to close poll"),
)
except Exception as e:
return DeliveryResult(success=False, error=str(e))
try:
return await self._circuit_breaker.call(_do_close)
except CircuitBreakerOpenError:
return DeliveryResult(success=False, error="Circuit breaker open")
except Exception as e:
return DeliveryResult(success=False, error=str(e))
async def get_poll_results(self, chat_id: str, poll_id: str) -> DeliveryResult:
if not self._client:
return DeliveryResult(success=False, error="Channel not connected")
async def _do_get():
path = f"{self._api_base}/poll/{chat_id}/{poll_id}"
try:
result = await self._client.get(path)
ocs = result.get("ocs", {})
if ocs.get("meta", {}).get("status") == "ok":
data = ocs.get("data", {})
return DeliveryResult(
success=True,
metadata={
"question": data.get("question", ""),
"options": data.get("options", []),
"votes": data.get("votes", {}),
"numVoters": data.get("numVoters", 0),
"status": data.get("status", "open"),
},
)
return DeliveryResult(
success=False,
error=ocs.get("meta", {}).get("message", "Failed to get poll results"),
)
except Exception as e:
return DeliveryResult(success=False, error=str(e))
try:
return await self._circuit_breaker.call(_do_get)
except CircuitBreakerOpenError:
return DeliveryResult(success=False, error="Circuit breaker open")
except Exception as e:
return DeliveryResult(success=False, error=str(e))
async def pin_message(self, chat_id: str, msg_id: str) -> DeliveryResult:
if not self._client:
return DeliveryResult(success=False, error="Channel not connected")
async def _do_pin():
path = f"{self._api_base}/pin/{chat_id}/{msg_id}"
try:
result = await self._client.post(path)
ocs = result.get("ocs", {})
if ocs.get("meta", {}).get("status") == "ok":
return DeliveryResult(success=True, message_id=msg_id)
return DeliveryResult(
success=False,
error=ocs.get("meta", {}).get("message", "Failed to pin message"),
)
except Exception as e:
return DeliveryResult(success=False, error=str(e))
try:
return await self._circuit_breaker.call(_do_pin)
except CircuitBreakerOpenError:
return DeliveryResult(success=False, error="Circuit breaker open")
except Exception as e:
return DeliveryResult(success=False, error=str(e))
async def unpin_message(self, chat_id: str, msg_id: str) -> DeliveryResult:
if not self._client:
return DeliveryResult(success=False, error="Channel not connected")
async def _do_unpin():
path = f"{self._api_base}/pin/{chat_id}/{msg_id}"
try:
result = await self._client.delete(path)
ocs = result.get("ocs", {})
if ocs.get("meta", {}).get("status") == "ok":
return DeliveryResult(success=True, message_id=msg_id)
return DeliveryResult(
success=False,
error=ocs.get("meta", {}).get("message", "Failed to unpin message"),
)
except Exception as e:
return DeliveryResult(success=False, error=str(e))
try:
return await self._circuit_breaker.call(_do_unpin)
except CircuitBreakerOpenError:
return DeliveryResult(success=False, error="Circuit breaker open")
except Exception as e:
return DeliveryResult(success=False, error=str(e))
async def list_pins(self, chat_id: str) -> list[dict]:
if not self._client:
return []
path = f"{self._api_base}/pin/{chat_id}"
try:
result = await self._client.get(path)
ocs = result.get("ocs", {})
if ocs.get("meta", {}).get("status") == "ok":
data = ocs.get("data", [])
if isinstance(data, list):
return data
return []
except Exception:
return []
async def list_conversations(self) -> list[dict[str, Any]]:
if not self._client:
return []

View File

@ -108,10 +108,8 @@ class NextcloudTalkDedupGuard:
if not token or not message_id:
return False
self._load_persisted()
key = self._make_key(token, message_id)
if key in self._committed:
return True
self._gc()
key = self._make_key(token, message_id)
return key in self._committed
def stats(self) -> dict[str, int]:

View File

@ -1,6 +1,6 @@
from __future__ import annotations
from datetime import datetime, UTC
from datetime import UTC, datetime
from typing import Any
from yuxi.channels.models import (
@ -14,7 +14,6 @@ from yuxi.channels.models import (
MessageType,
)
_AS2_TYPE_MAP: dict[str, EventType] = {
"Create": EventType.MESSAGE_RECEIVED,
"Announce": EventType.MESSAGE_RECEIVED,

View File

@ -4,7 +4,6 @@ import logging
import os
from typing import Any
logger = logging.getLogger(__name__)
SECRET_TARGETS = [