hyperagents 1.2.3rc5.post3__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.
- agents/__init__.py +31 -0
- agents/background_monitor.py +453 -0
- agents/learning.py +460 -0
- agents/planner.py +421 -0
- agents/transcriber.py +341 -0
- civilization/__init__.py +51 -0
- civilization/api/__init__.py +7 -0
- civilization/api/civilization_api.py +361 -0
- civilization/graph/__init__.py +19 -0
- civilization/graph/knowledge_linker.py +348 -0
- civilization/graph/org_graph.py +348 -0
- civilization/graph/process_graph.py +386 -0
- civilization/ingestion/__init__.py +20 -0
- civilization/ingestion/chunker.py +273 -0
- civilization/ingestion/document_ingestor.py +358 -0
- civilization/ingestion/embedder.py +262 -0
- civilization/ingestion/extractor.py +224 -0
- civilization/interview/__init__.py +19 -0
- civilization/interview/gap_detector.py +380 -0
- civilization/interview/interviewer.py +323 -0
- civilization/interview/questioner.py +363 -0
- civilization/retrieval/__init__.py +17 -0
- civilization/retrieval/civilization_rag.py +316 -0
- civilization/retrieval/context_injector.py +342 -0
- civilization/retrieval/relevance_ranker.py +287 -0
- civilization/schema.py +299 -0
- civilization/store.py +177 -0
- civilization/sync/__init__.py +16 -0
- civilization/sync/confluence_sync.py +317 -0
- civilization/sync/gdrive_sync.py +262 -0
- civilization/sync/notion_sync.py +237 -0
- civilization/versioning/__init__.py +19 -0
- civilization/versioning/diff_engine.py +351 -0
- civilization/versioning/staleness_detector.py +303 -0
- civilization/versioning/version_manager.py +293 -0
- cli/__init__.py +1 -0
- cli/doctor.py +334 -0
- cli/hyperclaw.py +1357 -0
- cli/onboarding.py +469 -0
- core/__init__.py +1 -0
- core/connectors/__init__.py +119 -0
- core/connectors/infrastructure.py +772 -0
- core/connectors/messaging.py +586 -0
- core/connectors/productivity.py +764 -0
- core/connectors/smarthome.py +528 -0
- core/connectors/social.py +730 -0
- core/hyperrouter/__init__.py +23 -0
- core/hyperrouter/bandit.py +218 -0
- core/hyperrouter/fast_loop.py +68 -0
- core/hyperrouter/slow_loop.py +143 -0
- core/hyperstate/__init__.py +24 -0
- core/hyperstate/certifier.py +154 -0
- core/hyperstate/schema.py +160 -0
- core/hyperstate/state_manager.py +103 -0
- core/hyperstate/store.py +163 -0
- core/providers/__init__.py +121 -0
- core/providers/embeddings.py +389 -0
- core/providers/image.py +534 -0
- core/providers/llm.py +1935 -0
- core/providers/search.py +608 -0
- core/providers/voice.py +474 -0
- hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
- hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
- hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
- hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
- hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
- hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
- hyperclaw/__init__.py +67 -0
- hyperclaw/__main__.py +220 -0
- hyperclaw/agent.py +775 -0
- hyperclaw/agent_coordinator.py +577 -0
- hyperclaw/anthropic_params.py +225 -0
- hyperclaw/api_utils.py +85 -0
- hyperclaw/approval_telegram.py +209 -0
- hyperclaw/approval_terminal.py +80 -0
- hyperclaw/attachments.py +177 -0
- hyperclaw/branding.py +55 -0
- hyperclaw/brief/__init__.py +11 -0
- hyperclaw/brief/cli.py +204 -0
- hyperclaw/brief/collect.py +146 -0
- hyperclaw/brief/compose.py +333 -0
- hyperclaw/brief/diff.py +110 -0
- hyperclaw/brief/errors.py +99 -0
- hyperclaw/brief/google.py +269 -0
- hyperclaw/brief/google_auth.py +294 -0
- hyperclaw/brief/links.py +124 -0
- hyperclaw/brief/service.py +424 -0
- hyperclaw/brief/store.py +205 -0
- hyperclaw/brief/tasks.py +200 -0
- hyperclaw/chat_scope.py +115 -0
- hyperclaw/cli_tui.py +497 -0
- hyperclaw/client_guard.py +717 -0
- hyperclaw/connection_check.py +113 -0
- hyperclaw/cost_guard.py +1728 -0
- hyperclaw/default_config/agents.yaml +534 -0
- hyperclaw/default_config/hyperclaw.yaml +27 -0
- hyperclaw/default_config/models.yaml +161 -0
- hyperclaw/deployment.py +39 -0
- hyperclaw/digest/__init__.py +9 -0
- hyperclaw/digest/cli.py +117 -0
- hyperclaw/digest/errors.py +100 -0
- hyperclaw/digest/gitlab.py +187 -0
- hyperclaw/digest/http.py +113 -0
- hyperclaw/digest/redact.py +32 -0
- hyperclaw/digest/service.py +271 -0
- hyperclaw/digest/slack.py +223 -0
- hyperclaw/digest/state.py +124 -0
- hyperclaw/digest/summarize.py +198 -0
- hyperclaw/digest/tokens.py +143 -0
- hyperclaw/distribution.py +46 -0
- hyperclaw/documents.py +351 -0
- hyperclaw/feeds.py +339 -0
- hyperclaw/google_workspace/__init__.py +5 -0
- hyperclaw/google_workspace/api.py +151 -0
- hyperclaw/google_workspace/auth.py +397 -0
- hyperclaw/google_workspace/calendar.py +157 -0
- hyperclaw/google_workspace/cli.py +72 -0
- hyperclaw/google_workspace/drive.py +236 -0
- hyperclaw/google_workspace/errors.py +58 -0
- hyperclaw/google_workspace/gmail.py +295 -0
- hyperclaw/google_workspace/meet.py +122 -0
- hyperclaw/google_workspace/tools.py +319 -0
- hyperclaw/hyperspeed.py +57 -0
- hyperclaw/integrations_layer.py +1427 -0
- hyperclaw/live_state.py +188 -0
- hyperclaw/managed_config.py +245 -0
- hyperclaw/media_hub.py +179 -0
- hyperclaw/memory_manager.py +584 -0
- hyperclaw/model_catalog.py +551 -0
- hyperclaw/model_policy.py +211 -0
- hyperclaw/model_router.py +595 -0
- hyperclaw/model_selector.py +72 -0
- hyperclaw/naming.py +284 -0
- hyperclaw/notices.py +65 -0
- hyperclaw/oai_stream.py +106 -0
- hyperclaw/orchestrator.py +591 -0
- hyperclaw/outbox.py +82 -0
- hyperclaw/persona.py +367 -0
- hyperclaw/prometheus.py +219 -0
- hyperclaw/prompt_assembly.py +157 -0
- hyperclaw/provider_setup.py +146 -0
- hyperclaw/providers.py +340 -0
- hyperclaw/qualification.py +341 -0
- hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
- hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
- hyperclaw/reasoning_display.py +35 -0
- hyperclaw/scheduler.py +454 -0
- hyperclaw/server.py +847 -0
- hyperclaw/setup.py +627 -0
- hyperclaw/skills.py +717 -0
- hyperclaw/solomon.py +442 -0
- hyperclaw/swarm.py +533 -0
- hyperclaw/taint.py +832 -0
- hyperclaw/team/__init__.py +28 -0
- hyperclaw/team/dispatcher.py +431 -0
- hyperclaw/team/genesis.py +106 -0
- hyperclaw/team/lead.py +110 -0
- hyperclaw/team/model.py +135 -0
- hyperclaw/team/morning.py +238 -0
- hyperclaw/team/repos.py +66 -0
- hyperclaw/team/roster.py +264 -0
- hyperclaw/team/specialist.py +235 -0
- hyperclaw/team/status.py +163 -0
- hyperclaw/team/watchers.py +112 -0
- hyperclaw/teamsetup/__init__.py +17 -0
- hyperclaw/teamsetup/checks.py +208 -0
- hyperclaw/teamsetup/item.py +391 -0
- hyperclaw/teamsetup/oauth_loopback.py +84 -0
- hyperclaw/teamsetup/onboard.py +550 -0
- hyperclaw/teamsetup/profile.py +82 -0
- hyperclaw/teamsetup/secrets.py +313 -0
- hyperclaw/teamsetup/selfcheck.py +295 -0
- hyperclaw/teamsetup/service.py +495 -0
- hyperclaw/teamsetup/slack_oauth.py +442 -0
- hyperclaw/teamsetup/tty.py +62 -0
- hyperclaw/telegram_bot.py +1106 -0
- hyperclaw/telegram_intake.py +126 -0
- hyperclaw/telegram_pairing.py +360 -0
- hyperclaw/terminal_provider.py +323 -0
- hyperclaw/tools/__init__.py +1 -0
- hyperclaw/tools/approval.py +949 -0
- hyperclaw/tools/args.py +212 -0
- hyperclaw/tools/catalog.py +244 -0
- hyperclaw/tools/classify.py +266 -0
- hyperclaw/tools/effects.py +724 -0
- hyperclaw/tools/egress.py +210 -0
- hyperclaw/tools/envelope.py +188 -0
- hyperclaw/tools/guard.py +367 -0
- hyperclaw/tools/ledger.py +339 -0
- hyperclaw/tools/receipts.py +70 -0
- hyperclaw/tools/registry.py +318 -0
- hyperclaw/tools/service_guard.py +151 -0
- hyperclaw/tools/surfaces.py +54 -0
- hyperclaw/tools/workspace.py +87 -0
- hyperclaw/trading/__init__.py +77 -0
- hyperclaw/truth_guard.py +791 -0
- hyperclaw/tts.py +124 -0
- hyperclaw/tui.py +4255 -0
- hyperclaw/tui_bridge.py +938 -0
- hyperclaw/unattended.py +60 -0
- hyperclaw/updates/__init__.py +54 -0
- hyperclaw/updates/check.py +131 -0
- hyperclaw/updates/index.py +178 -0
- hyperclaw/updates/install.py +110 -0
- hyperclaw/updates/managed.py +161 -0
- hyperclaw/updates/manifest.py +138 -0
- hyperclaw/updates/notices.py +86 -0
- hyperclaw/updates/paths.py +53 -0
- hyperclaw/updates/release.py +146 -0
- hyperclaw/updates/upgrade.py +253 -0
- hyperclaw/updates/versioning.py +104 -0
- hyperclaw/userconfig.py +119 -0
- hyperclaw/xai.py +83 -0
- integrations/__init__.py +31 -0
- integrations/automation/__init__.py +1 -0
- integrations/automation/make.py +87 -0
- integrations/automation/zapier.py +87 -0
- integrations/base.py +476 -0
- integrations/communication/__init__.py +1 -0
- integrations/communication/twilio.py +279 -0
- integrations/communication/zoom.py +290 -0
- integrations/data/__init__.py +1 -0
- integrations/data/google_analytics.py +135 -0
- integrations/data/postgres.py +182 -0
- integrations/data/supabase_connector.py +202 -0
- integrations/data/weather_feeds.py +450 -0
- integrations/developer/__init__.py +9 -0
- integrations/developer/github.py +406 -0
- integrations/developer/gitlab.py +298 -0
- integrations/enterprise/__init__.py +13 -0
- integrations/enterprise/confluence.py +266 -0
- integrations/enterprise/hubspot.py +293 -0
- integrations/enterprise/jira.py +314 -0
- integrations/enterprise/salesforce.py +286 -0
- integrations/finance/__init__.py +1 -0
- integrations/finance/polymarket_client.py +247 -0
- integrations/finance/quickbooks.py +281 -0
- integrations/finance/stripe.py +261 -0
- integrations/gateway.py +238 -0
- integrations/messaging/__init__.py +32 -0
- integrations/messaging/discord.py +159 -0
- integrations/messaging/email.py +401 -0
- integrations/messaging/imessage.py +443 -0
- integrations/messaging/signal.py +175 -0
- integrations/messaging/slack.py +217 -0
- integrations/messaging/sms.py +175 -0
- integrations/messaging/teams.py +208 -0
- integrations/messaging/telegram.py +260 -0
- integrations/messaging/whatsapp.py +203 -0
- integrations/productivity/__init__.py +17 -0
- integrations/productivity/airtable.py +217 -0
- integrations/productivity/asana.py +248 -0
- integrations/productivity/google/__init__.py +19 -0
- integrations/productivity/google/base.py +89 -0
- integrations/productivity/google/calendar.py +272 -0
- integrations/productivity/google/docs.py +202 -0
- integrations/productivity/google/drive.py +268 -0
- integrations/productivity/google/gmail.py +279 -0
- integrations/productivity/google/meet.py +218 -0
- integrations/productivity/google/sheets.py +222 -0
- integrations/productivity/google/tasks.py +199 -0
- integrations/productivity/linear.py +279 -0
- integrations/productivity/microsoft/__init__.py +13 -0
- integrations/productivity/microsoft/base.py +93 -0
- integrations/productivity/microsoft/calendar.py +207 -0
- integrations/productivity/microsoft/onedrive.py +222 -0
- integrations/productivity/microsoft/outlook.py +241 -0
- integrations/productivity/microsoft/sharepoint.py +211 -0
- integrations/productivity/notion.py +243 -0
- integrations/productivity/todoist.py +250 -0
- integrations/productivity/trello.py +244 -0
- integrations/storage/__init__.py +1 -0
- integrations/storage/box.py +279 -0
- integrations/storage/dropbox.py +215 -0
- integrations/telegram_inbound.py +179 -0
- integrations/telegram_poller.py +151 -0
- memory/__init__.py +1 -0
- memory/causal_graph.py +125 -0
- memory/impact_tracker.py +88 -0
- memory/vector_memory.py +436 -0
- models/__init__.py +12 -0
- models/chatjimmy_client.py +210 -0
- models/claude_client.py +235 -0
- models/claude_code_subagent.py +192 -0
- models/openai_client.py +120 -0
- models/router.py +162 -0
- recursive/discovery_loop.py +118 -0
- recursive/optimization_loop.py +135 -0
- recursive/validation_loop.py +38 -0
- security/__init__.py +1 -0
- security/audit_logger.py +106 -0
- security/filesystem_guard.py +100 -0
- security/hypershield.py +99 -0
- security/network_guard.py +97 -0
- security/policy_engine.py +198 -0
- swarm/__init__.py +1 -0
- swarm/agents/__init__.py +0 -0
- swarm/agents/base.py +230 -0
- swarm/agents/business/__init__.py +0 -0
- swarm/agents/business/counsel.py +31 -0
- swarm/agents/business/herald.py +32 -0
- swarm/agents/business/ledger.py +26 -0
- swarm/agents/business/nexus.py +30 -0
- swarm/agents/business/ops.py +30 -0
- swarm/agents/business/pipeline.py +28 -0
- swarm/agents/business/revenue.py +30 -0
- swarm/agents/business/sovereign.py +31 -0
- swarm/agents/business/strategos.py +27 -0
- swarm/agents/business/talent.py +27 -0
- swarm/agents/business/venture.py +32 -0
- swarm/agents/comms/__init__.py +1 -0
- swarm/agents/comms/cipher.py +32 -0
- swarm/agents/comms/echo_comms.py +31 -0
- swarm/agents/comms/envoy.py +31 -0
- swarm/agents/comms/herald_scheduler.py +33 -0
- swarm/agents/comms/pulse.py +29 -0
- swarm/agents/creative/__init__.py +0 -0
- swarm/agents/creative/author.py +27 -0
- swarm/agents/creative/lens.py +25 -0
- swarm/agents/executive/__init__.py +1 -0
- swarm/agents/intelligence/__init__.py +1 -0
- swarm/agents/intelligence/arbiter.py +30 -0
- swarm/agents/intelligence/sentinel.py +30 -0
- swarm/agents/media/__init__.py +0 -0
- swarm/agents/personal/__init__.py +0 -0
- swarm/agents/personal/atlas.py +27 -0
- swarm/agents/personal/hearth.py +26 -0
- swarm/agents/personal/midas.py +27 -0
- swarm/agents/personal/navigator.py +27 -0
- swarm/agents/personal/nourish.py +31 -0
- swarm/agents/personal/vitals.py +33 -0
- swarm/agents/recursive/__init__.py +0 -0
- swarm/agents/recursive/alchemist.py +73 -0
- swarm/agents/recursive/calibrator.py +45 -0
- swarm/agents/recursive/scout.py +97 -0
- swarm/agents/scientific/__init__.py +0 -0
- swarm/agents/scientific/cosmos.py +36 -0
- swarm/agents/scientific/gaia.py +26 -0
- swarm/agents/scientific/medicus.py +33 -0
- swarm/agents/scientific/oracle_agent.py +39 -0
- swarm/agents/scientific/scribe.py +39 -0
- swarm/agents/talent/__init__.py +0 -0
- swarm/agents/talent/deal.py +28 -0
- swarm/agents/talent/roster.py +29 -0
- swarm/agents/talent/scout.py +29 -0
- swarm/agents/talent/stage.py +27 -0
- swarm/agents/tech/__init__.py +0 -0
- swarm/agents/tech/aegis.py +28 -0
- swarm/agents/tech/bridge.py +28 -0
- swarm/agents/tech/forge.py +28 -0
- swarm/agents/trading/__init__.py +1 -0
- swarm/agents/trading/global_prediction_engine.py +611 -0
- swarm/agents/trading/polymarket_trader.py +165 -0
- swarm/agents/trading/prediction_strategist.py +218 -0
- swarm/autogen_bridge.py +68 -0
- swarm/bid_protocol.py +196 -0
- swarm/dispatcher.py +527 -0
- swarm/nexus.py +181 -0
- swarm/registry.py +112 -0
- telegram_listener.py +403 -0
- ui/tui/app.py +214 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""BoxConnector — Box cloud storage integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from integrations.base import (
|
|
11
|
+
BaseConnector,
|
|
12
|
+
ConnectorCapability,
|
|
13
|
+
ConnectorInfo,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("hyperclaw.integrations.box")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BoxConnector(BaseConnector):
|
|
20
|
+
"""
|
|
21
|
+
Box cloud storage connector.
|
|
22
|
+
|
|
23
|
+
Required config:
|
|
24
|
+
- access_token: Box API access token
|
|
25
|
+
OR
|
|
26
|
+
- client_id + client_secret: For OAuth2 flow
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
BASE_URL = "https://api.box.com/2.0"
|
|
30
|
+
UPLOAD_URL = "https://upload.box.com/api/2.0"
|
|
31
|
+
|
|
32
|
+
# HyperShield context for enterprise data
|
|
33
|
+
hypershield_context = "enterprise_data"
|
|
34
|
+
|
|
35
|
+
def __init__(self, config: dict) -> None:
|
|
36
|
+
self.config = config
|
|
37
|
+
self.access_token = config.get("access_token", "")
|
|
38
|
+
self.client_id = config.get("client_id", "")
|
|
39
|
+
self.client_secret = config.get("client_secret", "")
|
|
40
|
+
|
|
41
|
+
if config.get("enabled", False) and not self.access_token:
|
|
42
|
+
if not (self.client_id and self.client_secret):
|
|
43
|
+
raise ValueError(
|
|
44
|
+
"BoxConnector requires access_token or client_id + client_secret"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
self._client: httpx.AsyncClient | None = None
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def info(self) -> ConnectorInfo:
|
|
51
|
+
return ConnectorInfo(
|
|
52
|
+
connector_id="box",
|
|
53
|
+
platform="box",
|
|
54
|
+
category="storage",
|
|
55
|
+
capabilities=frozenset(
|
|
56
|
+
[
|
|
57
|
+
ConnectorCapability.READ_DATA,
|
|
58
|
+
ConnectorCapability.WRITE_DATA,
|
|
59
|
+
ConnectorCapability.DELETE_DATA,
|
|
60
|
+
ConnectorCapability.LIST_DATA,
|
|
61
|
+
ConnectorCapability.FILE_UPLOAD,
|
|
62
|
+
ConnectorCapability.FILE_DOWNLOAD,
|
|
63
|
+
ConnectorCapability.SEARCH,
|
|
64
|
+
ConnectorCapability.ACTION,
|
|
65
|
+
]
|
|
66
|
+
),
|
|
67
|
+
auth_type="oauth2",
|
|
68
|
+
rate_limit_per_minute=60,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
72
|
+
if self._client is None:
|
|
73
|
+
self._client = httpx.AsyncClient(
|
|
74
|
+
timeout=60.0,
|
|
75
|
+
headers={
|
|
76
|
+
"Authorization": f"Bearer {self.access_token}",
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
return self._client
|
|
81
|
+
|
|
82
|
+
async def health(self) -> bool:
|
|
83
|
+
"""Check if Box API is accessible."""
|
|
84
|
+
try:
|
|
85
|
+
client = await self._get_client()
|
|
86
|
+
response = await client.get(f"{self.BASE_URL}/users/me")
|
|
87
|
+
return response.status_code == 200
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.error(f"Box health check failed: {e}")
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
async def list_folder(self, folder_id: str = "0") -> dict:
|
|
93
|
+
"""List items in a folder. folder_id='0' is the root folder."""
|
|
94
|
+
client = await self._get_client()
|
|
95
|
+
|
|
96
|
+
response = await client.get(
|
|
97
|
+
f"{self.BASE_URL}/folders/{folder_id}/items",
|
|
98
|
+
params={"limit": 100},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if response.status_code != 200:
|
|
102
|
+
raise Exception(f"Box list_folder failed: {response.text}")
|
|
103
|
+
|
|
104
|
+
return response.json()
|
|
105
|
+
|
|
106
|
+
async def get_file(self, file_id: str) -> dict:
|
|
107
|
+
"""Get file metadata."""
|
|
108
|
+
client = await self._get_client()
|
|
109
|
+
|
|
110
|
+
response = await client.get(f"{self.BASE_URL}/files/{file_id}")
|
|
111
|
+
|
|
112
|
+
if response.status_code != 200:
|
|
113
|
+
raise Exception(f"Box get_file failed: {response.text}")
|
|
114
|
+
|
|
115
|
+
return response.json()
|
|
116
|
+
|
|
117
|
+
async def upload_file(self, path: str, folder_id: str = "0") -> dict:
|
|
118
|
+
"""Upload a file to Box."""
|
|
119
|
+
client = await self._get_client()
|
|
120
|
+
file_path = Path(path)
|
|
121
|
+
|
|
122
|
+
# Box requires multipart form for uploads
|
|
123
|
+
with open(file_path, "rb") as f:
|
|
124
|
+
files = {
|
|
125
|
+
"file": (file_path.name, f, "application/octet-stream"),
|
|
126
|
+
}
|
|
127
|
+
data = {
|
|
128
|
+
"attributes": f'{{"name": "{file_path.name}", "parent": {{"id": "{folder_id}"}}}}',
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Remove Content-Type header for multipart
|
|
132
|
+
headers = {"Authorization": f"Bearer {self.access_token}"}
|
|
133
|
+
|
|
134
|
+
async with httpx.AsyncClient(timeout=120.0, headers=headers) as upload_client:
|
|
135
|
+
response = await upload_client.post(
|
|
136
|
+
f"{self.UPLOAD_URL}/files/content",
|
|
137
|
+
data=data,
|
|
138
|
+
files=files,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if response.status_code not in (200, 201):
|
|
142
|
+
raise Exception(f"Box upload_file failed: {response.text}")
|
|
143
|
+
|
|
144
|
+
return response.json()
|
|
145
|
+
|
|
146
|
+
async def download_file(self, file_id: str, destination: str) -> str:
|
|
147
|
+
"""Download a file from Box."""
|
|
148
|
+
client = await self._get_client()
|
|
149
|
+
|
|
150
|
+
response = await client.get(
|
|
151
|
+
f"{self.BASE_URL}/files/{file_id}/content",
|
|
152
|
+
follow_redirects=True,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if response.status_code != 200:
|
|
156
|
+
raise Exception(f"Box download_file failed: {response.text}")
|
|
157
|
+
|
|
158
|
+
dest_path = Path(destination)
|
|
159
|
+
dest_path.write_bytes(response.content)
|
|
160
|
+
|
|
161
|
+
return str(dest_path)
|
|
162
|
+
|
|
163
|
+
async def create_folder(self, name: str, parent_id: str = "0") -> dict:
|
|
164
|
+
"""Create a new folder."""
|
|
165
|
+
client = await self._get_client()
|
|
166
|
+
|
|
167
|
+
response = await client.post(
|
|
168
|
+
f"{self.BASE_URL}/folders",
|
|
169
|
+
json={
|
|
170
|
+
"name": name,
|
|
171
|
+
"parent": {"id": parent_id},
|
|
172
|
+
},
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if response.status_code not in (200, 201):
|
|
176
|
+
raise Exception(f"Box create_folder failed: {response.text}")
|
|
177
|
+
|
|
178
|
+
return response.json()
|
|
179
|
+
|
|
180
|
+
async def share_file(
|
|
181
|
+
self, file_id: str, email: str, role: str = "viewer"
|
|
182
|
+
) -> dict:
|
|
183
|
+
"""Share a file with a user via collaboration."""
|
|
184
|
+
client = await self._get_client()
|
|
185
|
+
|
|
186
|
+
response = await client.post(
|
|
187
|
+
f"{self.BASE_URL}/collaborations",
|
|
188
|
+
json={
|
|
189
|
+
"item": {"type": "file", "id": file_id},
|
|
190
|
+
"accessible_by": {"type": "user", "login": email},
|
|
191
|
+
"role": role,
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
if response.status_code not in (200, 201):
|
|
196
|
+
raise Exception(f"Box share_file failed: {response.text}")
|
|
197
|
+
|
|
198
|
+
return response.json()
|
|
199
|
+
|
|
200
|
+
async def search(self, query: str) -> dict:
|
|
201
|
+
"""Search for files and folders."""
|
|
202
|
+
client = await self._get_client()
|
|
203
|
+
|
|
204
|
+
response = await client.get(
|
|
205
|
+
f"{self.BASE_URL}/search",
|
|
206
|
+
params={"query": query},
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if response.status_code != 200:
|
|
210
|
+
raise Exception(f"Box search failed: {response.text}")
|
|
211
|
+
|
|
212
|
+
return response.json()
|
|
213
|
+
|
|
214
|
+
async def get_metadata(self, file_id: str) -> dict:
|
|
215
|
+
"""Get file metadata including custom metadata."""
|
|
216
|
+
client = await self._get_client()
|
|
217
|
+
|
|
218
|
+
response = await client.get(
|
|
219
|
+
f"{self.BASE_URL}/files/{file_id}",
|
|
220
|
+
params={"fields": "metadata"},
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if response.status_code != 200:
|
|
224
|
+
raise Exception(f"Box get_metadata failed: {response.text}")
|
|
225
|
+
|
|
226
|
+
return response.json()
|
|
227
|
+
|
|
228
|
+
async def _read_impl(self, resource_id: str, **kwargs) -> dict:
|
|
229
|
+
"""Read file metadata."""
|
|
230
|
+
return await self.get_file(resource_id)
|
|
231
|
+
|
|
232
|
+
async def _write_impl(self, resource_type: str, data: dict, **kwargs) -> dict:
|
|
233
|
+
"""Create a folder."""
|
|
234
|
+
if resource_type == "folder":
|
|
235
|
+
return await self.create_folder(
|
|
236
|
+
name=data.get("name", "New Folder"),
|
|
237
|
+
parent_id=data.get("parent_id", "0"),
|
|
238
|
+
)
|
|
239
|
+
raise ValueError(f"Unknown resource type: {resource_type}")
|
|
240
|
+
|
|
241
|
+
async def _delete_impl(self, resource_id: str, **kwargs) -> bool:
|
|
242
|
+
"""Delete a file."""
|
|
243
|
+
client = await self._get_client()
|
|
244
|
+
response = await client.delete(f"{self.BASE_URL}/files/{resource_id}")
|
|
245
|
+
return response.status_code == 204
|
|
246
|
+
|
|
247
|
+
async def _list_impl(
|
|
248
|
+
self, resource_type: str, filters: dict, **kwargs
|
|
249
|
+
) -> list[dict]:
|
|
250
|
+
"""List folder contents."""
|
|
251
|
+
result = await self.list_folder(filters.get("folder_id", "0"))
|
|
252
|
+
return result.get("entries", [])
|
|
253
|
+
|
|
254
|
+
async def _search_impl(self, query: str, **kwargs) -> list[dict]:
|
|
255
|
+
"""Search files and folders."""
|
|
256
|
+
result = await self.search(query)
|
|
257
|
+
return result.get("entries", [])
|
|
258
|
+
|
|
259
|
+
async def _action_impl(self, action_name: str, params: dict) -> dict:
|
|
260
|
+
"""Execute Box-specific actions."""
|
|
261
|
+
if action_name == "share":
|
|
262
|
+
return await self.share_file(
|
|
263
|
+
file_id=params.get("file_id"),
|
|
264
|
+
email=params.get("email"),
|
|
265
|
+
role=params.get("role", "viewer"),
|
|
266
|
+
)
|
|
267
|
+
elif action_name == "upload":
|
|
268
|
+
return await self.upload_file(
|
|
269
|
+
path=params.get("path"),
|
|
270
|
+
folder_id=params.get("folder_id", "0"),
|
|
271
|
+
)
|
|
272
|
+
elif action_name == "download":
|
|
273
|
+
path = await self.download_file(
|
|
274
|
+
file_id=params.get("file_id"),
|
|
275
|
+
destination=params.get("destination"),
|
|
276
|
+
)
|
|
277
|
+
return {"path": path}
|
|
278
|
+
|
|
279
|
+
raise ValueError(f"Unknown action: {action_name}")
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""DropboxConnector — Dropbox cloud storage integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from integrations.base import (
|
|
12
|
+
BaseConnector,
|
|
13
|
+
ConnectorCapability,
|
|
14
|
+
ConnectorInfo,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("hyperclaw.integrations.dropbox")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DropboxConnector(BaseConnector):
|
|
21
|
+
"""
|
|
22
|
+
Dropbox cloud storage connector.
|
|
23
|
+
|
|
24
|
+
Required config:
|
|
25
|
+
- access_token: Dropbox API access token
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
BASE_URL = "https://api.dropboxapi.com/2"
|
|
29
|
+
CONTENT_URL = "https://content.dropboxapi.com/2"
|
|
30
|
+
|
|
31
|
+
def __init__(self, config: dict) -> None:
|
|
32
|
+
self.config = config
|
|
33
|
+
self.access_token = config.get("access_token", "")
|
|
34
|
+
|
|
35
|
+
if config.get("enabled", False) and not self.access_token:
|
|
36
|
+
raise ValueError("DropboxConnector requires access_token")
|
|
37
|
+
|
|
38
|
+
self._client: httpx.AsyncClient | None = None
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def info(self) -> ConnectorInfo:
|
|
42
|
+
return ConnectorInfo(
|
|
43
|
+
connector_id="dropbox",
|
|
44
|
+
platform="dropbox",
|
|
45
|
+
category="storage",
|
|
46
|
+
capabilities=frozenset(
|
|
47
|
+
[
|
|
48
|
+
ConnectorCapability.READ_DATA,
|
|
49
|
+
ConnectorCapability.WRITE_DATA,
|
|
50
|
+
ConnectorCapability.DELETE_DATA,
|
|
51
|
+
ConnectorCapability.FILE_UPLOAD,
|
|
52
|
+
ConnectorCapability.FILE_DOWNLOAD,
|
|
53
|
+
ConnectorCapability.SEARCH,
|
|
54
|
+
]
|
|
55
|
+
),
|
|
56
|
+
auth_type="bearer",
|
|
57
|
+
rate_limit_per_minute=60,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
61
|
+
if self._client is None:
|
|
62
|
+
self._client = httpx.AsyncClient(
|
|
63
|
+
timeout=60.0,
|
|
64
|
+
headers={
|
|
65
|
+
"Authorization": f"Bearer {self.access_token}",
|
|
66
|
+
"Content-Type": "application/json",
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
return self._client
|
|
70
|
+
|
|
71
|
+
async def health(self) -> bool:
|
|
72
|
+
"""Check if Dropbox API is accessible."""
|
|
73
|
+
try:
|
|
74
|
+
client = await self._get_client()
|
|
75
|
+
response = await client.post(
|
|
76
|
+
f"{self.BASE_URL}/users/get_current_account"
|
|
77
|
+
)
|
|
78
|
+
return response.status_code == 200
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.error(f"Dropbox health check failed: {e}")
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
async def list_folder(self, path: str = "") -> dict:
|
|
84
|
+
"""List items in a folder. path='' is the root folder."""
|
|
85
|
+
client = await self._get_client()
|
|
86
|
+
|
|
87
|
+
response = await client.post(
|
|
88
|
+
f"{self.BASE_URL}/files/list_folder",
|
|
89
|
+
json={"path": path if path else ""},
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if response.status_code != 200:
|
|
93
|
+
raise Exception(f"Dropbox list_folder failed: {response.text}")
|
|
94
|
+
|
|
95
|
+
return response.json()
|
|
96
|
+
|
|
97
|
+
async def get_file_metadata(self, path: str) -> dict:
|
|
98
|
+
"""Get file metadata."""
|
|
99
|
+
client = await self._get_client()
|
|
100
|
+
|
|
101
|
+
response = await client.post(
|
|
102
|
+
f"{self.BASE_URL}/files/get_metadata",
|
|
103
|
+
json={"path": path},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if response.status_code != 200:
|
|
107
|
+
raise Exception(f"Dropbox get_file_metadata failed: {response.text}")
|
|
108
|
+
|
|
109
|
+
return response.json()
|
|
110
|
+
|
|
111
|
+
async def upload_file(self, local_path: str, dropbox_path: str) -> dict:
|
|
112
|
+
"""Upload a file to Dropbox."""
|
|
113
|
+
file_path = Path(local_path)
|
|
114
|
+
|
|
115
|
+
with open(file_path, "rb") as f:
|
|
116
|
+
file_content = f.read()
|
|
117
|
+
|
|
118
|
+
headers = {
|
|
119
|
+
"Authorization": f"Bearer {self.access_token}",
|
|
120
|
+
"Content-Type": "application/octet-stream",
|
|
121
|
+
"Dropbox-API-Arg": json.dumps({
|
|
122
|
+
"path": dropbox_path,
|
|
123
|
+
"mode": "add",
|
|
124
|
+
"autorename": True,
|
|
125
|
+
}),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async with httpx.AsyncClient(timeout=120.0) as upload_client:
|
|
129
|
+
response = await upload_client.post(
|
|
130
|
+
f"{self.CONTENT_URL}/files/upload",
|
|
131
|
+
headers=headers,
|
|
132
|
+
content=file_content,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if response.status_code != 200:
|
|
136
|
+
raise Exception(f"Dropbox upload_file failed: {response.text}")
|
|
137
|
+
|
|
138
|
+
return response.json()
|
|
139
|
+
|
|
140
|
+
async def download_file(self, dropbox_path: str, destination: str) -> str:
|
|
141
|
+
"""Download a file from Dropbox."""
|
|
142
|
+
headers = {
|
|
143
|
+
"Authorization": f"Bearer {self.access_token}",
|
|
144
|
+
"Dropbox-API-Arg": json.dumps({"path": dropbox_path}),
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async with httpx.AsyncClient(timeout=120.0) as download_client:
|
|
148
|
+
response = await download_client.post(
|
|
149
|
+
f"{self.CONTENT_URL}/files/download",
|
|
150
|
+
headers=headers,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if response.status_code != 200:
|
|
154
|
+
raise Exception(f"Dropbox download_file failed: {response.text}")
|
|
155
|
+
|
|
156
|
+
dest_path = Path(destination)
|
|
157
|
+
dest_path.write_bytes(response.content)
|
|
158
|
+
|
|
159
|
+
return str(dest_path)
|
|
160
|
+
|
|
161
|
+
async def delete(self, path: str) -> dict:
|
|
162
|
+
"""Delete a file or folder."""
|
|
163
|
+
client = await self._get_client()
|
|
164
|
+
|
|
165
|
+
response = await client.post(
|
|
166
|
+
f"{self.BASE_URL}/files/delete_v2",
|
|
167
|
+
json={"path": path},
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if response.status_code != 200:
|
|
171
|
+
raise Exception(f"Dropbox delete failed: {response.text}")
|
|
172
|
+
|
|
173
|
+
return response.json()
|
|
174
|
+
|
|
175
|
+
async def search(self, query: str) -> dict:
|
|
176
|
+
"""Search for files and folders."""
|
|
177
|
+
client = await self._get_client()
|
|
178
|
+
|
|
179
|
+
response = await client.post(
|
|
180
|
+
f"{self.BASE_URL}/files/search_v2",
|
|
181
|
+
json={"query": query},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if response.status_code != 200:
|
|
185
|
+
raise Exception(f"Dropbox search failed: {response.text}")
|
|
186
|
+
|
|
187
|
+
return response.json()
|
|
188
|
+
|
|
189
|
+
async def _read_impl(self, resource_id: str, **kwargs) -> dict:
|
|
190
|
+
"""Read file metadata."""
|
|
191
|
+
return await self.get_file_metadata(resource_id)
|
|
192
|
+
|
|
193
|
+
async def _write_impl(self, resource_type: str, data: dict, **kwargs) -> dict:
|
|
194
|
+
"""Upload a file."""
|
|
195
|
+
return await self.upload_file(
|
|
196
|
+
local_path=data.get("local_path"),
|
|
197
|
+
dropbox_path=data.get("dropbox_path"),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
async def _delete_impl(self, resource_id: str, **kwargs) -> bool:
|
|
201
|
+
"""Delete a file or folder."""
|
|
202
|
+
await self.delete(resource_id)
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
async def _list_impl(
|
|
206
|
+
self, resource_type: str, filters: dict, **kwargs
|
|
207
|
+
) -> list[dict]:
|
|
208
|
+
"""List folder contents."""
|
|
209
|
+
result = await self.list_folder(filters.get("path", ""))
|
|
210
|
+
return result.get("entries", [])
|
|
211
|
+
|
|
212
|
+
async def _search_impl(self, query: str, **kwargs) -> list[dict]:
|
|
213
|
+
"""Search files and folders."""
|
|
214
|
+
result = await self.search(query)
|
|
215
|
+
return result.get("matches", [])
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Assistant Telegram Inbound Handler
|
|
4
|
+
Receives messages, images, voice from the user → routes to Assistant
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
import json
|
|
11
|
+
import httpx
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from flask import Flask, request, jsonify
|
|
15
|
+
|
|
16
|
+
logging.basicConfig(level=logging.INFO)
|
|
17
|
+
logger = logging.getLogger("Assistant.Telegram.Inbound")
|
|
18
|
+
|
|
19
|
+
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
|
|
20
|
+
ALLOWED_CHAT_IDS = [int(x) for x in os.environ.get("TELEGRAM_ALLOWED_IDS", "0").split(",") if x]
|
|
21
|
+
HYPERCLAW_API = "http://localhost:8001"
|
|
22
|
+
INBOX_DIR = Path(str(Path.home() / ".hyperclaw/workspace/telegram_inbox"))
|
|
23
|
+
INBOX_DIR.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
|
|
25
|
+
app = Flask(__name__)
|
|
26
|
+
|
|
27
|
+
def save_message(data: dict):
|
|
28
|
+
"""Save incoming message to inbox for Assistant processing"""
|
|
29
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
30
|
+
msg_file = INBOX_DIR / f"msg_{timestamp}.json"
|
|
31
|
+
with open(msg_file, "w") as f:
|
|
32
|
+
json.dump(data, f, indent=2)
|
|
33
|
+
logger.info(f"Saved message: {msg_file}")
|
|
34
|
+
return msg_file
|
|
35
|
+
|
|
36
|
+
async def download_file(file_id: str, filename: str):
|
|
37
|
+
"""Download file (image/voice) from Telegram"""
|
|
38
|
+
async with httpx.AsyncClient() as client:
|
|
39
|
+
# Get file path
|
|
40
|
+
r = await client.get(f"https://api.telegram.org/bot{BOT_TOKEN}/getFile?file_id={file_id}")
|
|
41
|
+
file_path = r.json()["result"]["file_path"]
|
|
42
|
+
|
|
43
|
+
# Download file
|
|
44
|
+
file_url = f"https://api.telegram.org/file/bot{BOT_TOKEN}/{file_path}"
|
|
45
|
+
r = await client.get(file_url)
|
|
46
|
+
|
|
47
|
+
save_path = INBOX_DIR / filename
|
|
48
|
+
with open(save_path, "wb") as f:
|
|
49
|
+
f.write(r.content)
|
|
50
|
+
logger.info(f"Downloaded file: {save_path}")
|
|
51
|
+
return str(save_path)
|
|
52
|
+
|
|
53
|
+
def send_telegram(chat_id: int, text: str):
|
|
54
|
+
"""Send reply back to the user"""
|
|
55
|
+
import requests
|
|
56
|
+
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
|
|
57
|
+
requests.post(url, json={"chat_id": chat_id, "text": text, "parse_mode": "Markdown",
|
|
58
|
+
"link_preview_options": {"is_disabled": True}}) # link previews off
|
|
59
|
+
|
|
60
|
+
@app.route("/telegram/webhook", methods=["POST"])
|
|
61
|
+
def webhook():
|
|
62
|
+
data = request.json
|
|
63
|
+
logger.info(f"Incoming: {json.dumps(data, indent=2)[:500]}")
|
|
64
|
+
|
|
65
|
+
message = data.get("message") or data.get("edited_message")
|
|
66
|
+
if not message:
|
|
67
|
+
return jsonify({"ok": True})
|
|
68
|
+
|
|
69
|
+
chat_id = message.get("chat", {}).get("id")
|
|
70
|
+
|
|
71
|
+
# Security — only the user
|
|
72
|
+
if chat_id not in ALLOWED_CHAT_IDS:
|
|
73
|
+
logger.warning(f"Blocked unauthorized chat_id: {chat_id}")
|
|
74
|
+
return jsonify({"ok": True})
|
|
75
|
+
|
|
76
|
+
timestamp = datetime.now().isoformat()
|
|
77
|
+
|
|
78
|
+
# Text message
|
|
79
|
+
if "text" in message:
|
|
80
|
+
text = message["text"]
|
|
81
|
+
logger.info(f"Text from the user: {text}")
|
|
82
|
+
|
|
83
|
+
payload = {
|
|
84
|
+
"type": "text",
|
|
85
|
+
"from": "the user",
|
|
86
|
+
"chat_id": chat_id,
|
|
87
|
+
"text": text,
|
|
88
|
+
"timestamp": timestamp
|
|
89
|
+
}
|
|
90
|
+
save_message(payload)
|
|
91
|
+
|
|
92
|
+
# Auto-acknowledge
|
|
93
|
+
send_telegram(chat_id, f"⚡ Received. Processing now.")
|
|
94
|
+
|
|
95
|
+
# Photo/image
|
|
96
|
+
elif "photo" in message:
|
|
97
|
+
photos = message["photo"]
|
|
98
|
+
largest = max(photos, key=lambda p: p.get("file_size", 0))
|
|
99
|
+
file_id = largest["file_id"]
|
|
100
|
+
caption = message.get("caption", "")
|
|
101
|
+
|
|
102
|
+
timestamp_clean = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
103
|
+
filename = f"photo_{timestamp_clean}.jpg"
|
|
104
|
+
|
|
105
|
+
# Download async
|
|
106
|
+
loop = asyncio.new_event_loop()
|
|
107
|
+
file_path = loop.run_until_complete(download_file(file_id, filename))
|
|
108
|
+
loop.close()
|
|
109
|
+
|
|
110
|
+
payload = {
|
|
111
|
+
"type": "photo",
|
|
112
|
+
"from": "the user",
|
|
113
|
+
"chat_id": chat_id,
|
|
114
|
+
"file_path": file_path,
|
|
115
|
+
"caption": caption,
|
|
116
|
+
"timestamp": timestamp
|
|
117
|
+
}
|
|
118
|
+
save_message(payload)
|
|
119
|
+
send_telegram(chat_id, f"⚡ Image received{' — ' + caption if caption else ''}. On it.")
|
|
120
|
+
|
|
121
|
+
# Voice message
|
|
122
|
+
elif "voice" in message:
|
|
123
|
+
file_id = message["voice"]["file_id"]
|
|
124
|
+
timestamp_clean = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
125
|
+
filename = f"voice_{timestamp_clean}.ogg"
|
|
126
|
+
|
|
127
|
+
loop = asyncio.new_event_loop()
|
|
128
|
+
file_path = loop.run_until_complete(download_file(file_id, filename))
|
|
129
|
+
loop.close()
|
|
130
|
+
|
|
131
|
+
payload = {
|
|
132
|
+
"type": "voice",
|
|
133
|
+
"from": "the user",
|
|
134
|
+
"chat_id": chat_id,
|
|
135
|
+
"file_path": file_path,
|
|
136
|
+
"timestamp": timestamp
|
|
137
|
+
}
|
|
138
|
+
save_message(payload)
|
|
139
|
+
send_telegram(chat_id, f"⚡ Voice message received. Transcribing now.")
|
|
140
|
+
|
|
141
|
+
# Document/file
|
|
142
|
+
elif "document" in message:
|
|
143
|
+
doc = message["document"]
|
|
144
|
+
file_id = doc["file_id"]
|
|
145
|
+
filename = doc.get("file_name", f"doc_{datetime.now().strftime('%Y%m%d_%H%M%S')}")
|
|
146
|
+
|
|
147
|
+
loop = asyncio.new_event_loop()
|
|
148
|
+
file_path = loop.run_until_complete(download_file(file_id, filename))
|
|
149
|
+
loop.close()
|
|
150
|
+
|
|
151
|
+
payload = {
|
|
152
|
+
"type": "document",
|
|
153
|
+
"from": "the user",
|
|
154
|
+
"chat_id": chat_id,
|
|
155
|
+
"file_path": file_path,
|
|
156
|
+
"filename": filename,
|
|
157
|
+
"timestamp": timestamp
|
|
158
|
+
}
|
|
159
|
+
save_message(payload)
|
|
160
|
+
send_telegram(chat_id, f"⚡ File received: {filename}. Reviewing now.")
|
|
161
|
+
|
|
162
|
+
return jsonify({"ok": True})
|
|
163
|
+
|
|
164
|
+
@app.route("/telegram/inbox", methods=["GET"])
|
|
165
|
+
def get_inbox():
|
|
166
|
+
"""Get all pending messages from the user"""
|
|
167
|
+
messages = []
|
|
168
|
+
for f in sorted(INBOX_DIR.glob("*.json")):
|
|
169
|
+
with open(f) as fp:
|
|
170
|
+
messages.append(json.load(fp))
|
|
171
|
+
return jsonify({"count": len(messages), "messages": messages})
|
|
172
|
+
|
|
173
|
+
@app.route("/telegram/health", methods=["GET"])
|
|
174
|
+
def health():
|
|
175
|
+
return jsonify({"status": "online", "service": "Assistant Telegram Inbound"})
|
|
176
|
+
|
|
177
|
+
if __name__ == "__main__":
|
|
178
|
+
logger.info("⚡ Assistant Telegram Inbound — ONLINE on port 8766")
|
|
179
|
+
app.run(host="0.0.0.0", port=8766, debug=False)
|