netizen-cli 0.10.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- netizen_cli/__init__.py +3 -0
- netizen_cli/__main__.py +4 -0
- netizen_cli/admin/__init__.py +1 -0
- netizen_cli/admin/auth.py +928 -0
- netizen_cli/admin/errors.py +9 -0
- netizen_cli/admin/port_config.py +115 -0
- netizen_cli/admin/presentation.py +257 -0
- netizen_cli/admin/queries.py +337 -0
- netizen_cli/admin/static/admin.css +260 -0
- netizen_cli/admin/static/admin.js +2898 -0
- netizen_cli/admin/static/index.html +327 -0
- netizen_cli/admin/transport.py +935 -0
- netizen_cli/admin/web.py +2717 -0
- netizen_cli/bindings.py +3215 -0
- netizen_cli/builtin_skills.py +93 -0
- netizen_cli/cards/__init__.py +105 -0
- netizen_cli/cards/callbacks.py +565 -0
- netizen_cli/cards/controls.py +2273 -0
- netizen_cli/cards/defaults.py +213 -0
- netizen_cli/cards/model_info.py +80 -0
- netizen_cli/cards/questions.py +220 -0
- netizen_cli/cards/reply.py +2247 -0
- netizen_cli/cards/scheduled.py +836 -0
- netizen_cli/channel/__init__.py +1 -0
- netizen_cli/channel/completion_mentions.py +60 -0
- netizen_cli/channel/input_preparation.py +644 -0
- netizen_cli/channel/messages.py +57 -0
- netizen_cli/channel/ports.py +52 -0
- netizen_cli/channel/question_inputs.py +51 -0
- netizen_cli/channel/reactions.py +293 -0
- netizen_cli/channel/reply_presenter.py +1505 -0
- netizen_cli/channel/topics.py +70 -0
- netizen_cli/channel_app.py +6593 -0
- netizen_cli/cli.py +287 -0
- netizen_cli/cli_data.py +536 -0
- netizen_cli/cli_packages.py +526 -0
- netizen_cli/cli_services.py +651 -0
- netizen_cli/cli_setup.py +242 -0
- netizen_cli/cli_update.py +303 -0
- netizen_cli/cli_update_restore.py +53 -0
- netizen_cli/cli_update_worker.py +333 -0
- netizen_cli/codex_runtime.py +7125 -0
- netizen_cli/completion_mention.py +16 -0
- netizen_cli/database_migrations.py +218 -0
- netizen_cli/defaults/__init__.py +5 -0
- netizen_cli/defaults/models.py +39 -0
- netizen_cli/defaults/service.py +232 -0
- netizen_cli/defaults/store.py +260 -0
- netizen_cli/deployment/__init__.py +1 -0
- netizen_cli/deployment/restart_worker.py +134 -0
- netizen_cli/deployment/update_executor.py +258 -0
- netizen_cli/deployment/update_protocol.py +281 -0
- netizen_cli/domain.py +416 -0
- netizen_cli/error_messages.py +124 -0
- netizen_cli/experience.py +531 -0
- netizen_cli/feishu_app_onboarding.py +187 -0
- netizen_cli/feishu_app_permissions.py +123 -0
- netizen_cli/git_status.py +63 -0
- netizen_cli/image_inputs.py +579 -0
- netizen_cli/instance.py +84 -0
- netizen_cli/lark_app.py +125 -0
- netizen_cli/main.py +903 -0
- netizen_cli/management/__init__.py +83 -0
- netizen_cli/management/blocking_io.py +352 -0
- netizen_cli/management/chat_labels.py +266 -0
- netizen_cli/management/coordination.py +32 -0
- netizen_cli/management/service.py +2187 -0
- netizen_cli/management/updates.py +214 -0
- netizen_cli/markdown_images.py +78 -0
- netizen_cli/message_content.py +786 -0
- netizen_cli/message_history.py +643 -0
- netizen_cli/message_preparation.py +60 -0
- netizen_cli/message_projection.py +923 -0
- netizen_cli/migrations/__init__.py +1 -0
- netizen_cli/migrations/schema.py +103 -0
- netizen_cli/migrations/v14.py +438 -0
- netizen_cli/model_settings.py +269 -0
- netizen_cli/package_resources.py +22 -0
- netizen_cli/projects.py +327 -0
- netizen_cli/prompt_projection.py +327 -0
- netizen_cli/quoted_context.py +312 -0
- netizen_cli/resources/config.example.yaml +35 -0
- netizen_cli/resources/skills/netizen-lark/SKILL.md +64 -0
- netizen_cli/resources/skills/netizen-user-guide/SKILL.md +37 -0
- netizen_cli/resources/skills/netizen-user-guide/references/user-guide.md +842 -0
- netizen_cli/result_images.py +123 -0
- netizen_cli/runtime/__init__.py +1 -0
- netizen_cli/runtime/contracts.py +792 -0
- netizen_cli/runtime/name_writes.py +67 -0
- netizen_cli/runtime/thread_naming.py +451 -0
- netizen_cli/schedules/__init__.py +1 -0
- netizen_cli/schedules/mcp.py +535 -0
- netizen_cli/schedules/models.py +394 -0
- netizen_cli/schedules/scheduler.py +374 -0
- netizen_cli/schedules/service.py +766 -0
- netizen_cli/schedules/store.py +771 -0
- netizen_cli/sdk_gap_adapter.py +1151 -0
- netizen_cli/service_launcher.py +583 -0
- netizen_cli/session_settings.py +126 -0
- netizen_cli/settings.py +216 -0
- netizen_cli/skill_references.py +40 -0
- netizen_cli/terminal_cleanup.py +155 -0
- netizen_cli/turn_activity.py +688 -0
- netizen_cli/turn_files.py +812 -0
- netizen_cli/turn_patch_children.py +254 -0
- netizen_cli/turn_plan_observer.py +315 -0
- netizen_cli/user_questions.py +106 -0
- netizen_cli-0.10.0.dist-info/METADATA +18 -0
- netizen_cli-0.10.0.dist-info/RECORD +112 -0
- netizen_cli-0.10.0.dist-info/WHEEL +5 -0
- netizen_cli-0.10.0.dist-info/entry_points.txt +2 -0
- netizen_cli-0.10.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Channel transport operations used by the application and presenters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal, Protocol
|
|
6
|
+
|
|
7
|
+
from lark_channel import MediaSource
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ReplyChannel(Protocol):
|
|
11
|
+
@property
|
|
12
|
+
def bot_identity(self) -> Any: ...
|
|
13
|
+
|
|
14
|
+
async def reply(self, message: Any, content: Any, opts: Any = None) -> object: ...
|
|
15
|
+
|
|
16
|
+
async def send(self, to: str, content: Any, opts: Any = None) -> object: ...
|
|
17
|
+
|
|
18
|
+
async def add_reaction(self, message_id: str, emoji_type: str) -> object: ...
|
|
19
|
+
|
|
20
|
+
async def remove_reaction(
|
|
21
|
+
self,
|
|
22
|
+
message_id: str,
|
|
23
|
+
reaction_id: str,
|
|
24
|
+
) -> object: ...
|
|
25
|
+
|
|
26
|
+
async def update_card(self, message_id: str, card: dict[str, Any]) -> object: ...
|
|
27
|
+
|
|
28
|
+
async def upload_media(
|
|
29
|
+
self,
|
|
30
|
+
source: MediaSource,
|
|
31
|
+
*,
|
|
32
|
+
kind: Literal["image", "file"],
|
|
33
|
+
file_name: str | None = None,
|
|
34
|
+
file_type: str | None = None,
|
|
35
|
+
) -> str: ...
|
|
36
|
+
|
|
37
|
+
async def fetch_message(self, message_id: str) -> dict[str, Any]: ...
|
|
38
|
+
|
|
39
|
+
async def fetch_inbound_message(self, message_id: str) -> Any: ...
|
|
40
|
+
|
|
41
|
+
async def fetch_quoted_context(self, message_id: str) -> Any: ...
|
|
42
|
+
|
|
43
|
+
async def download_resource(
|
|
44
|
+
self,
|
|
45
|
+
file_key: str,
|
|
46
|
+
resource_type: str = "image",
|
|
47
|
+
message_id: str | None = None,
|
|
48
|
+
) -> bytes | None: ...
|
|
49
|
+
|
|
50
|
+
async def get_chat_info(self, chat_id: str) -> Any: ...
|
|
51
|
+
|
|
52
|
+
async def get_chat_members(self, chat_id: str, *, id_type: str = "open_id") -> Any: ...
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""A card submission's real operator and separate, bot-authored reply anchor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..prompt_projection import PromptProjectionError, project_identity
|
|
9
|
+
from ..user_questions import QuestionTarget
|
|
10
|
+
from .ports import ReplyChannel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class CardAnswerConversation:
|
|
15
|
+
chat_id: str
|
|
16
|
+
chat_type: str
|
|
17
|
+
thread_id: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class CardAnswerOrigin:
|
|
22
|
+
app_id: str
|
|
23
|
+
chat_id: str
|
|
24
|
+
message_id: str
|
|
25
|
+
conversation: CardAnswerConversation
|
|
26
|
+
source_card_id: str
|
|
27
|
+
target: QuestionTarget
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def id(self) -> str:
|
|
31
|
+
return self.message_id
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def card_answer_sender(
|
|
35
|
+
channel: ReplyChannel, chat_id: str, operator: Any,
|
|
36
|
+
) -> dict[str, Any]:
|
|
37
|
+
"""Resolve attribution for the callback operator, never the card's author."""
|
|
38
|
+
sender = project_identity(operator)
|
|
39
|
+
open_id = sender.get("open_id")
|
|
40
|
+
if not open_id:
|
|
41
|
+
raise PromptProjectionError("卡片回调缺少回答者身份,本条回答未执行。")
|
|
42
|
+
if "display_name" not in sender:
|
|
43
|
+
members = await channel.get_chat_members(chat_id, id_type="open_id")
|
|
44
|
+
member = next((item for item in members if item.id == open_id), None)
|
|
45
|
+
name = getattr(member, "name", None)
|
|
46
|
+
if not isinstance(name, str) or not name.strip():
|
|
47
|
+
raise PromptProjectionError(
|
|
48
|
+
"无法获取回答者姓名,本条回答未执行;请检查群成员读取权限后重试。"
|
|
49
|
+
)
|
|
50
|
+
sender["display_name"] = name
|
|
51
|
+
return sender
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Best-effort lifecycle for exact Turn reactions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from .ports import ReplyChannel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
_TYPING_REACTION = "Typing"
|
|
15
|
+
_THINKING_REACTION = "THINKING"
|
|
16
|
+
_THINKING_VISIBLE_SECONDS = 2.0
|
|
17
|
+
_THINKING_HIDDEN_SECONDS = 13.0
|
|
18
|
+
_REACTION_OPERATION_TIMEOUT_SECONDS = 3.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(slots=True)
|
|
22
|
+
class _TurnReactionPulse:
|
|
23
|
+
turn_id: str
|
|
24
|
+
message_id: str
|
|
25
|
+
stopped: asyncio.Event
|
|
26
|
+
typing_reaction_id: str | None = None
|
|
27
|
+
thinking_reaction_id: str | None = None
|
|
28
|
+
task: asyncio.Task[None] | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _ReactionController:
|
|
32
|
+
"""Best-effort, in-memory lifecycle for one exact Turn's reactions."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
channel: ReplyChannel,
|
|
37
|
+
*,
|
|
38
|
+
visible_seconds: float = _THINKING_VISIBLE_SECONDS,
|
|
39
|
+
hidden_seconds: float = _THINKING_HIDDEN_SECONDS,
|
|
40
|
+
operation_timeout_seconds: float = _REACTION_OPERATION_TIMEOUT_SECONDS,
|
|
41
|
+
) -> None:
|
|
42
|
+
if visible_seconds <= 0 or hidden_seconds <= 0:
|
|
43
|
+
raise ValueError("thinking reaction pulse intervals must be positive")
|
|
44
|
+
if operation_timeout_seconds <= 0:
|
|
45
|
+
raise ValueError("reaction operation timeout must be positive")
|
|
46
|
+
self._channel = channel
|
|
47
|
+
self._visible_seconds = visible_seconds
|
|
48
|
+
self._hidden_seconds = hidden_seconds
|
|
49
|
+
self._operation_timeout_seconds = operation_timeout_seconds
|
|
50
|
+
self._pulses: dict[str, _TurnReactionPulse] = {}
|
|
51
|
+
self._closed = False
|
|
52
|
+
|
|
53
|
+
async def start(
|
|
54
|
+
self,
|
|
55
|
+
turn_id: str,
|
|
56
|
+
message_id: str,
|
|
57
|
+
*,
|
|
58
|
+
pulse_enabled: bool,
|
|
59
|
+
) -> bool:
|
|
60
|
+
if type(pulse_enabled) is not bool:
|
|
61
|
+
raise ValueError("reaction pulse setting must be a boolean")
|
|
62
|
+
if not turn_id or not message_id:
|
|
63
|
+
logger.error(
|
|
64
|
+
"failed to start turn reactions: missing identity",
|
|
65
|
+
extra={"turn_id": turn_id, "message_id": message_id},
|
|
66
|
+
)
|
|
67
|
+
return False
|
|
68
|
+
if self._closed:
|
|
69
|
+
logger.warning(
|
|
70
|
+
"turn reaction controller is closed",
|
|
71
|
+
extra={"turn_id": turn_id, "message_id": message_id},
|
|
72
|
+
)
|
|
73
|
+
return False
|
|
74
|
+
if turn_id in self._pulses:
|
|
75
|
+
await self.stop(turn_id)
|
|
76
|
+
pulse = _TurnReactionPulse(
|
|
77
|
+
turn_id=turn_id,
|
|
78
|
+
message_id=message_id,
|
|
79
|
+
stopped=asyncio.Event(),
|
|
80
|
+
)
|
|
81
|
+
self._pulses[turn_id] = pulse
|
|
82
|
+
reaction_id = await self._add_reaction(pulse, _TYPING_REACTION)
|
|
83
|
+
if self._pulses.get(turn_id) is not pulse:
|
|
84
|
+
if reaction_id is not None:
|
|
85
|
+
await self._remove_reaction(
|
|
86
|
+
pulse,
|
|
87
|
+
reaction_id,
|
|
88
|
+
_TYPING_REACTION,
|
|
89
|
+
)
|
|
90
|
+
return False
|
|
91
|
+
if reaction_id is None:
|
|
92
|
+
self._pulses.pop(turn_id, None)
|
|
93
|
+
return False
|
|
94
|
+
pulse.typing_reaction_id = reaction_id
|
|
95
|
+
if not pulse_enabled:
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
reaction_id = await self._add_reaction(pulse, _THINKING_REACTION)
|
|
99
|
+
if self._pulses.get(turn_id) is not pulse:
|
|
100
|
+
if reaction_id is not None:
|
|
101
|
+
await self._remove_reaction(
|
|
102
|
+
pulse,
|
|
103
|
+
reaction_id,
|
|
104
|
+
_THINKING_REACTION,
|
|
105
|
+
)
|
|
106
|
+
return False
|
|
107
|
+
if reaction_id is None:
|
|
108
|
+
# Keep the stable Typing placeholder even when the optional pulse
|
|
109
|
+
# cannot start. Terminal/shutdown cleanup still owns its exact ID.
|
|
110
|
+
return True
|
|
111
|
+
pulse.thinking_reaction_id = reaction_id
|
|
112
|
+
pulse.task = asyncio.create_task(
|
|
113
|
+
self._pulse(pulse),
|
|
114
|
+
name=f"netizen-thinking-{turn_id}",
|
|
115
|
+
)
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
async def freeze(self, turn_id: str) -> None:
|
|
119
|
+
"""Stop future pulse operations without removing visible reactions."""
|
|
120
|
+
|
|
121
|
+
pulse = self._pulses.get(turn_id)
|
|
122
|
+
if pulse is None:
|
|
123
|
+
return
|
|
124
|
+
pulse.stopped.set()
|
|
125
|
+
await self._wait_for_task(pulse)
|
|
126
|
+
|
|
127
|
+
async def stop(self, turn_id: str) -> None:
|
|
128
|
+
pulse = self._pulses.pop(turn_id, None)
|
|
129
|
+
if pulse is None:
|
|
130
|
+
return
|
|
131
|
+
pulse.stopped.set()
|
|
132
|
+
await self._wait_for_task(pulse)
|
|
133
|
+
reaction_id = pulse.thinking_reaction_id
|
|
134
|
+
if reaction_id is not None:
|
|
135
|
+
if await self._remove_reaction(
|
|
136
|
+
pulse,
|
|
137
|
+
reaction_id,
|
|
138
|
+
_THINKING_REACTION,
|
|
139
|
+
):
|
|
140
|
+
pulse.thinking_reaction_id = None
|
|
141
|
+
reaction_id = pulse.typing_reaction_id
|
|
142
|
+
if reaction_id is not None:
|
|
143
|
+
if await self._remove_reaction(
|
|
144
|
+
pulse,
|
|
145
|
+
reaction_id,
|
|
146
|
+
_TYPING_REACTION,
|
|
147
|
+
):
|
|
148
|
+
pulse.typing_reaction_id = None
|
|
149
|
+
|
|
150
|
+
async def close(self) -> None:
|
|
151
|
+
if self._closed:
|
|
152
|
+
return
|
|
153
|
+
self._closed = True
|
|
154
|
+
await asyncio.gather(
|
|
155
|
+
*(self.stop(turn_id) for turn_id in tuple(self._pulses)),
|
|
156
|
+
return_exceptions=False,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
async def _wait_for_task(self, pulse: _TurnReactionPulse) -> None:
|
|
160
|
+
task = pulse.task
|
|
161
|
+
if task is None or task is asyncio.current_task():
|
|
162
|
+
return
|
|
163
|
+
try:
|
|
164
|
+
await task
|
|
165
|
+
except asyncio.CancelledError:
|
|
166
|
+
raise
|
|
167
|
+
except Exception:
|
|
168
|
+
logger.exception(
|
|
169
|
+
"thinking reaction pulse failed while stopping",
|
|
170
|
+
extra={
|
|
171
|
+
"turn_id": pulse.turn_id,
|
|
172
|
+
"message_id": pulse.message_id,
|
|
173
|
+
},
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
async def _pulse(self, pulse: _TurnReactionPulse) -> None:
|
|
177
|
+
while not pulse.stopped.is_set():
|
|
178
|
+
if await self._wait(pulse.stopped, self._visible_seconds):
|
|
179
|
+
return
|
|
180
|
+
reaction_id = pulse.thinking_reaction_id
|
|
181
|
+
if reaction_id is None:
|
|
182
|
+
return
|
|
183
|
+
if not await self._remove_reaction(
|
|
184
|
+
pulse,
|
|
185
|
+
reaction_id,
|
|
186
|
+
_THINKING_REACTION,
|
|
187
|
+
):
|
|
188
|
+
# Preserve the exact ID so terminal/shutdown cleanup gets one
|
|
189
|
+
# final best-effort removal attempt without a retry storm.
|
|
190
|
+
return
|
|
191
|
+
pulse.thinking_reaction_id = None
|
|
192
|
+
if await self._wait(pulse.stopped, self._hidden_seconds):
|
|
193
|
+
return
|
|
194
|
+
reaction_id = await self._add_reaction(pulse, _THINKING_REACTION)
|
|
195
|
+
if reaction_id is None:
|
|
196
|
+
return
|
|
197
|
+
pulse.thinking_reaction_id = reaction_id
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
async def _wait(stopped: asyncio.Event, seconds: float) -> bool:
|
|
201
|
+
try:
|
|
202
|
+
await asyncio.wait_for(stopped.wait(), timeout=seconds)
|
|
203
|
+
except TimeoutError:
|
|
204
|
+
return False
|
|
205
|
+
return True
|
|
206
|
+
|
|
207
|
+
async def _add_reaction(
|
|
208
|
+
self,
|
|
209
|
+
pulse: _TurnReactionPulse,
|
|
210
|
+
emoji_type: str,
|
|
211
|
+
) -> str | None:
|
|
212
|
+
try:
|
|
213
|
+
async with asyncio.timeout(self._operation_timeout_seconds):
|
|
214
|
+
result = await self._channel.add_reaction(
|
|
215
|
+
pulse.message_id,
|
|
216
|
+
emoji_type,
|
|
217
|
+
)
|
|
218
|
+
except asyncio.CancelledError:
|
|
219
|
+
raise
|
|
220
|
+
except Exception:
|
|
221
|
+
logger.exception(
|
|
222
|
+
"failed to add turn reaction",
|
|
223
|
+
extra={
|
|
224
|
+
"turn_id": pulse.turn_id,
|
|
225
|
+
"message_id": pulse.message_id,
|
|
226
|
+
"emoji_type": emoji_type,
|
|
227
|
+
},
|
|
228
|
+
)
|
|
229
|
+
return None
|
|
230
|
+
if getattr(result, "success", False) is not True:
|
|
231
|
+
logger.error(
|
|
232
|
+
"failed to add turn reaction: unsuccessful result",
|
|
233
|
+
extra={
|
|
234
|
+
"turn_id": pulse.turn_id,
|
|
235
|
+
"message_id": pulse.message_id,
|
|
236
|
+
"emoji_type": emoji_type,
|
|
237
|
+
},
|
|
238
|
+
)
|
|
239
|
+
return None
|
|
240
|
+
raw = getattr(result, "raw", None)
|
|
241
|
+
if raw is None and isinstance(result, dict):
|
|
242
|
+
raw = result
|
|
243
|
+
data = raw.get("data") if isinstance(raw, dict) else None
|
|
244
|
+
reaction_id = data.get("reaction_id") if isinstance(data, dict) else None
|
|
245
|
+
if not isinstance(reaction_id, str) or not reaction_id:
|
|
246
|
+
logger.error(
|
|
247
|
+
"failed to add turn reaction: missing reaction ID",
|
|
248
|
+
extra={
|
|
249
|
+
"turn_id": pulse.turn_id,
|
|
250
|
+
"message_id": pulse.message_id,
|
|
251
|
+
"emoji_type": emoji_type,
|
|
252
|
+
},
|
|
253
|
+
)
|
|
254
|
+
return None
|
|
255
|
+
return reaction_id
|
|
256
|
+
|
|
257
|
+
async def _remove_reaction(
|
|
258
|
+
self,
|
|
259
|
+
pulse: _TurnReactionPulse,
|
|
260
|
+
reaction_id: str,
|
|
261
|
+
emoji_type: str,
|
|
262
|
+
) -> bool:
|
|
263
|
+
try:
|
|
264
|
+
async with asyncio.timeout(self._operation_timeout_seconds):
|
|
265
|
+
result = await self._channel.remove_reaction(
|
|
266
|
+
pulse.message_id,
|
|
267
|
+
reaction_id,
|
|
268
|
+
)
|
|
269
|
+
except asyncio.CancelledError:
|
|
270
|
+
raise
|
|
271
|
+
except Exception:
|
|
272
|
+
logger.exception(
|
|
273
|
+
"failed to remove turn reaction",
|
|
274
|
+
extra={
|
|
275
|
+
"turn_id": pulse.turn_id,
|
|
276
|
+
"message_id": pulse.message_id,
|
|
277
|
+
"reaction_id": reaction_id,
|
|
278
|
+
"emoji_type": emoji_type,
|
|
279
|
+
},
|
|
280
|
+
)
|
|
281
|
+
return False
|
|
282
|
+
if getattr(result, "success", False) is not True:
|
|
283
|
+
logger.error(
|
|
284
|
+
"failed to remove turn reaction: unsuccessful result",
|
|
285
|
+
extra={
|
|
286
|
+
"turn_id": pulse.turn_id,
|
|
287
|
+
"message_id": pulse.message_id,
|
|
288
|
+
"reaction_id": reaction_id,
|
|
289
|
+
"emoji_type": emoji_type,
|
|
290
|
+
},
|
|
291
|
+
)
|
|
292
|
+
return False
|
|
293
|
+
return True
|