mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat(photon): add native poll sending
This commit is contained in:
parent
95d303138b
commit
077c583c75
6 changed files with 84 additions and 3 deletions
2
contributors/emails/hermes@kortify.local
Normal file
2
contributors/emails/hermes@kortify.local
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
arnoldfrancisca
|
||||
# PR #43665/#43718 salvage
|
||||
|
|
@ -153,8 +153,10 @@ All env vars are documented in `plugin.yaml`. The most important:
|
|||
restart is best-effort — the live reaction handle is lost, so a stale
|
||||
tapback heals when the next reaction replaces it. Group spaces stay
|
||||
reachable across restarts via spectrum-ts' `space.get(id)`.
|
||||
- **Message effects, polls** — supported by `spectrum-ts` but not yet
|
||||
exposed; the sidecar is the natural place to add them.
|
||||
- **Native polls are supported.** Hermes posts poll content through
|
||||
`spectrum-ts`' `poll(...)` builder via the sidecar's `/send-poll` endpoint.
|
||||
- **Message effects** — supported by `spectrum-ts` but not yet exposed; the
|
||||
sidecar is the natural place to add them.
|
||||
- **Cron/standalone sends require a running gateway.** Processes outside
|
||||
the gateway (cron subprocesses, `hermes send`) cannot spawn the sidecar;
|
||||
they authenticate to the gateway's live sidecar via the runtime record at
|
||||
|
|
|
|||
|
|
@ -1547,6 +1547,29 @@ class PhotonAdapter(BasePlatformAdapter):
|
|||
chat_id, animation_url, caption, reply_to, metadata,
|
||||
)
|
||||
|
||||
async def send_poll(
|
||||
self,
|
||||
chat_id: str,
|
||||
title: str,
|
||||
options: list[str],
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> SendResult:
|
||||
"""Send a native iMessage poll through Photon Spectrum."""
|
||||
choices = [str(option).strip() for option in options if str(option).strip()]
|
||||
if len(choices) < 2:
|
||||
return SendResult(
|
||||
success=False,
|
||||
error="Photon polls require at least two non-empty options",
|
||||
)
|
||||
try:
|
||||
data = await self._sidecar_call(
|
||||
"/send-poll",
|
||||
{"spaceId": chat_id, "title": title.strip(), "options": choices},
|
||||
)
|
||||
except Exception as e:
|
||||
return SendResult(success=False, error=str(e))
|
||||
return SendResult(success=True, message_id=data.get("messageId"))
|
||||
|
||||
async def send_typing(self, chat_id: str, metadata=None) -> None:
|
||||
now = time.time()
|
||||
if now - self._typing_last_sent.get(chat_id, 0.0) < _TYPING_COOLDOWN_SECONDS:
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@
|
|||
// - POST /unreact -> {"ok": true} | 400 soft failure
|
||||
// body: {"spaceId": "...", "messageId": "<target msg id>",
|
||||
// "reactionId": "..." | null (restart-recovery fallback)}
|
||||
// - POST /send-poll -> {"ok": true, "messageId": "..."}
|
||||
// body: {"spaceId": "...", "title": "...", "options": ["...", "..."]}
|
||||
// - POST /typing -> {"ok": true}
|
||||
// body: {"spaceId": "...", "state": "start" | "stop"}
|
||||
// - POST /shutdown -> {"ok": true}; then process exits
|
||||
|
|
@ -237,12 +239,14 @@ let Spectrum,
|
|||
voice,
|
||||
spectrumText,
|
||||
spectrumMarkdown,
|
||||
spectrumTyping;
|
||||
spectrumTyping,
|
||||
spectrumPoll;
|
||||
try {
|
||||
({
|
||||
Spectrum,
|
||||
attachment,
|
||||
voice,
|
||||
poll: spectrumPoll,
|
||||
text: spectrumText,
|
||||
markdown: spectrumMarkdown,
|
||||
typing: spectrumTyping,
|
||||
|
|
@ -879,6 +883,21 @@ const server = http.createServer(async (req, res) => {
|
|||
}
|
||||
return badRequest(res, "no tracked reaction for message");
|
||||
}
|
||||
if (req.url === "/send-poll") {
|
||||
const { spaceId, title, options } = body || {};
|
||||
const choices = Array.isArray(options)
|
||||
? options.map((option) => String(option || "").trim()).filter(Boolean)
|
||||
: [];
|
||||
if (!spaceId || typeof title !== "string" || !title.trim()) {
|
||||
return badRequest(res, "spaceId and title are required");
|
||||
}
|
||||
if (choices.length < 2) {
|
||||
return badRequest(res, "options must contain at least two choices");
|
||||
}
|
||||
const space = await resolveSpace(spaceId);
|
||||
const result = await space.send(spectrumPoll(title.trim(), choices));
|
||||
return ok(res, { messageId: result?.id || null });
|
||||
}
|
||||
if (req.url === "/typing") {
|
||||
const { spaceId, state = "start" } = body || {};
|
||||
if (!spaceId) return badRequest(res, "spaceId is required");
|
||||
|
|
|
|||
|
|
@ -182,6 +182,39 @@ async def test_send_image_url_fetch_failure_falls_back_to_text(
|
|||
assert "https://example.com/cat.jpg" in calls[0][1]["text"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_poll_hits_poll_endpoint(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
adapter = _make_adapter(monkeypatch)
|
||||
calls = _capture_sidecar(adapter)
|
||||
|
||||
result = await adapter.send_poll(
|
||||
"any;-;+1",
|
||||
"Pick one",
|
||||
[" Alpha ", "", "Beta"],
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
assert result.message_id == "msg-123"
|
||||
assert calls == [
|
||||
(
|
||||
"/send-poll",
|
||||
{"spaceId": "any;-;+1", "title": "Pick one", "options": ["Alpha", "Beta"]},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_poll_requires_two_options(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
adapter = _make_adapter(monkeypatch)
|
||||
calls = _capture_sidecar(adapter)
|
||||
|
||||
result = await adapter.send_poll("any;-;+1", "Pick one", ["Alpha", " "])
|
||||
|
||||
assert result.success is False
|
||||
assert "at least two" in (result.error or "")
|
||||
assert calls == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_attachment_rejects_unsafe_path(
|
||||
monkeypatch: pytest.MonkeyPatch
|
||||
|
|
|
|||
|
|
@ -202,6 +202,8 @@ Common issues:
|
|||
`voice()` content builders via the sidecar's `/send-attachment`
|
||||
endpoint. Captions arrive as a separate iMessage bubble after the
|
||||
media.
|
||||
- **Native polls are supported.** Hermes sends poll content through
|
||||
spectrum-ts' `poll()` builder via the sidecar's `/send-poll` endpoint.
|
||||
- **Photon's free quotas:** 5,000 messages per server per day,
|
||||
50 new-conversation initiations per shared line per day. Increases
|
||||
available — email `help@photon.codes`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue