freddie 0.0.41
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.
- package/AGENTS.md +180 -0
- package/CHANGELOG.md +32 -0
- package/README.md +130 -0
- package/bin/freddie.js +116 -0
- package/package.json +59 -0
- package/skills/creative/README.md +3 -0
- package/skills/creative/architecture-diagram/SKILL.md +52 -0
- package/skills/creative/ascii-video/SKILL.md +60 -0
- package/skills/creative/concept-diagrams/SKILL.md +65 -0
- package/skills/data/README.md +3 -0
- package/skills/data/etl-pipelines/SKILL.md +60 -0
- package/skills/data/sql-explainer/SKILL.md +60 -0
- package/skills/ops/README.md +3 -0
- package/skills/ops/incident-response/SKILL.md +74 -0
- package/skills/ops/log-triage/SKILL.md +79 -0
- package/skills/planning/README.md +3 -0
- package/skills/planning/okr-drafter/SKILL.md +60 -0
- package/skills/planning/weekly-review/SKILL.md +64 -0
- package/skills/software-development/README.md +3 -0
- package/skills/software-development/code-review/SKILL.md +70 -0
- package/skills/software-development/rfc-writer/SKILL.md +68 -0
- package/skills/software-development/systematic-debugging/SKILL.md +80 -0
- package/src/acp/auth.js +21 -0
- package/src/acp/entry.js +2 -0
- package/src/acp/events.js +10 -0
- package/src/acp/main.js +8 -0
- package/src/acp/permissions.js +29 -0
- package/src/acp/server.js +84 -0
- package/src/acp/session.js +26 -0
- package/src/acp/tools.js +17 -0
- package/src/agent/account_usage.js +19 -0
- package/src/agent/acptoapi-bridge.js +80 -0
- package/src/agent/anthropic_adapter.js +10 -0
- package/src/agent/auxiliary_client.js +20 -0
- package/src/agent/bedrock_adapter.js +11 -0
- package/src/agent/codex_responses_adapter.js +10 -0
- package/src/agent/compress/compressor.js +55 -0
- package/src/agent/compress/fallback.js +14 -0
- package/src/agent/compress/index.js +6 -0
- package/src/agent/compress/policy.js +47 -0
- package/src/agent/compress/prompt.js +46 -0
- package/src/agent/compress/prune.js +16 -0
- package/src/agent/compress/tokens.js +31 -0
- package/src/agent/context_references.js +40 -0
- package/src/agent/copilot_acp_client.js +6 -0
- package/src/agent/credential_pool.js +30 -0
- package/src/agent/credential_sources.js +18 -0
- package/src/agent/curator.js +5 -0
- package/src/agent/display.js +23 -0
- package/src/agent/error_classifier.js +15 -0
- package/src/agent/file_safety.js +9 -0
- package/src/agent/gemini_cloudcode_adapter.js +9 -0
- package/src/agent/gemini_native_adapter.js +11 -0
- package/src/agent/gemini_schema.js +19 -0
- package/src/agent/google_code_assist.js +8 -0
- package/src/agent/google_oauth.js +21 -0
- package/src/agent/image_gen_provider.js +8 -0
- package/src/agent/image_gen_registry.js +6 -0
- package/src/agent/image_routing.js +13 -0
- package/src/agent/insights.js +9 -0
- package/src/agent/llm_resolver.js +21 -0
- package/src/agent/lmstudio_reasoning.js +13 -0
- package/src/agent/machine.js +102 -0
- package/src/agent/manual_compression_feedback.js +5 -0
- package/src/agent/memory_manager.js +14 -0
- package/src/agent/memory_provider.js +1 -0
- package/src/agent/model_metadata.js +28 -0
- package/src/agent/models_dev.js +13 -0
- package/src/agent/moonshot_schema.js +11 -0
- package/src/agent/oauth_endpoints.js +79 -0
- package/src/agent/onboarding.js +16 -0
- package/src/agent/pi-bridge.js +37 -0
- package/src/agent/prompt_builder.js +12 -0
- package/src/agent/prompt_caching.js +24 -0
- package/src/agent/rate_limit_tracker.js +12 -0
- package/src/agent/redact.js +25 -0
- package/src/agent/retry_utils.js +17 -0
- package/src/agent/shell_hooks.js +16 -0
- package/src/agent/skill_commands.js +16 -0
- package/src/agent/skill_preprocessing.js +12 -0
- package/src/agent/skill_utils.js +14 -0
- package/src/agent/subdirectory_hints.js +17 -0
- package/src/agent/title_generator.js +13 -0
- package/src/agent/trajectory.js +9 -0
- package/src/agent/usage_pricing.js +16 -0
- package/src/auth.js +84 -0
- package/src/batch.js +32 -0
- package/src/cli/auth_commands.js +17 -0
- package/src/cli/azure_detect.js +9 -0
- package/src/cli/backup.js +17 -0
- package/src/cli/banner.js +13 -0
- package/src/cli/browser_connect.js +11 -0
- package/src/cli/callbacks.js +5 -0
- package/src/cli/claw.js +8 -0
- package/src/cli/cli_output.js +19 -0
- package/src/cli/clipboard.js +24 -0
- package/src/cli/codex_models.js +8 -0
- package/src/cli/colors.js +13 -0
- package/src/cli/completer.js +98 -0
- package/src/cli/completion.js +21 -0
- package/src/cli/copilot_auth.js +9 -0
- package/src/cli/curator_cli.js +5 -0
- package/src/cli/curses.js +15 -0
- package/src/cli/debug.js +6 -0
- package/src/cli/default_soul.js +20 -0
- package/src/cli/dingtalk_auth.js +12 -0
- package/src/cli/doctor.js +15 -0
- package/src/cli/dump.js +11 -0
- package/src/cli/env_loader.js +25 -0
- package/src/cli/fallback_cmd.js +9 -0
- package/src/cli/gateway_cli.js +17 -0
- package/src/cli/hooks.js +9 -0
- package/src/cli/interactive.js +61 -0
- package/src/cli/logs.js +32 -0
- package/src/cli/main.js +7 -0
- package/src/cli/mcp_config.js +9 -0
- package/src/cli/memory_setup.js +12 -0
- package/src/cli/model_catalog.js +23 -0
- package/src/cli/model_normalize.js +12 -0
- package/src/cli/model_switch.js +11 -0
- package/src/cli/models.js +13 -0
- package/src/cli/nous_subscription.js +12 -0
- package/src/cli/oneshot.js +6 -0
- package/src/cli/pairing.js +21 -0
- package/src/cli/platforms.js +14 -0
- package/src/cli/plugins.js +4 -0
- package/src/cli/plugins_cmd.js +21 -0
- package/src/cli/profiles_cli.js +6 -0
- package/src/cli/providers.js +18 -0
- package/src/cli/pty_bridge.js +16 -0
- package/src/cli/relaunch.js +7 -0
- package/src/cli/runtime_provider.js +9 -0
- package/src/cli/setup.js +131 -0
- package/src/cli/skills_config.js +6 -0
- package/src/cli/skills_hub.js +8 -0
- package/src/cli/slack_cli.js +17 -0
- package/src/cli/status.js +10 -0
- package/src/cli/timeouts.js +5 -0
- package/src/cli/tips.js +14 -0
- package/src/cli/tools_config.js +15 -0
- package/src/cli/uninstall.js +8 -0
- package/src/cli/vercel_auth.js +13 -0
- package/src/cli/voice.js +6 -0
- package/src/cli/web_server.js +13 -0
- package/src/cli/webhook.js +12 -0
- package/src/commands/profile.js +72 -0
- package/src/commands/registry.js +94 -0
- package/src/config.js +125 -0
- package/src/context/engine.js +42 -0
- package/src/cron/cron-parse.js +27 -0
- package/src/cron/scheduler.js +63 -0
- package/src/db.js +178 -0
- package/src/gateway/base.js +13 -0
- package/src/gateway/builtin_hooks/boot.js +5 -0
- package/src/gateway/builtin_hooks/broadcast.js +3 -0
- package/src/gateway/builtin_hooks/deny.js +6 -0
- package/src/gateway/builtin_hooks/index.js +17 -0
- package/src/gateway/builtin_hooks/presence.js +4 -0
- package/src/gateway/builtin_hooks/routing.js +7 -0
- package/src/gateway/helpers.js +27 -0
- package/src/gateway/platforms/api_server.js +21 -0
- package/src/gateway/platforms/bluebubbles.js +32 -0
- package/src/gateway/platforms/dingtalk.js +32 -0
- package/src/gateway/platforms/discord.js +24 -0
- package/src/gateway/platforms/email.js +51 -0
- package/src/gateway/platforms/feishu.js +32 -0
- package/src/gateway/platforms/feishu_comment.js +12 -0
- package/src/gateway/platforms/feishu_comment_rules.js +11 -0
- package/src/gateway/platforms/homeassistant.js +32 -0
- package/src/gateway/platforms/matrix.js +40 -0
- package/src/gateway/platforms/mattermost.js +29 -0
- package/src/gateway/platforms/qqbot.js +32 -0
- package/src/gateway/platforms/signal.js +33 -0
- package/src/gateway/platforms/slack.js +34 -0
- package/src/gateway/platforms/sms.js +34 -0
- package/src/gateway/platforms/telegram.js +38 -0
- package/src/gateway/platforms/telegram_network.js +17 -0
- package/src/gateway/platforms/webhook.js +19 -0
- package/src/gateway/platforms/wecom.js +32 -0
- package/src/gateway/platforms/wecom_callback.js +15 -0
- package/src/gateway/platforms/wecom_crypto.js +16 -0
- package/src/gateway/platforms/weixin.js +32 -0
- package/src/gateway/platforms/whatsapp.js +40 -0
- package/src/gateway/platforms/yuanbao.js +9 -0
- package/src/gateway/platforms/yuanbao_media.js +5 -0
- package/src/gateway/platforms/yuanbao_proto.js +9 -0
- package/src/gateway/platforms/yuanbao_sticker.js +6 -0
- package/src/gateway/run.js +42 -0
- package/src/gateway/service.js +143 -0
- package/src/home.js +44 -0
- package/src/index.js +47 -0
- package/src/mcp/server.js +49 -0
- package/src/observability/debug.js +31 -0
- package/src/observability/log.js +38 -0
- package/src/plugins/achievements/index.js +9 -0
- package/src/plugins/cockpit/index.js +8 -0
- package/src/plugins/context_engine/index.js +13 -0
- package/src/plugins/disk_cleanup/index.js +22 -0
- package/src/plugins/google_meet/index.js +19 -0
- package/src/plugins/image_gen/index.js +5 -0
- package/src/plugins/manager.js +66 -0
- package/src/plugins/memory/_index.js +8 -0
- package/src/plugins/memory/byterover.js +25 -0
- package/src/plugins/memory/hindsight.js +25 -0
- package/src/plugins/memory/holographic.js +31 -0
- package/src/plugins/memory/honcho.js +25 -0
- package/src/plugins/memory/mem0.js +25 -0
- package/src/plugins/memory/openviking.js +25 -0
- package/src/plugins/memory/provider.js +35 -0
- package/src/plugins/memory/retaindb.js +25 -0
- package/src/plugins/memory/supermemory.js +25 -0
- package/src/plugins/observability/index.js +18 -0
- package/src/plugins/platforms/index.js +20 -0
- package/src/plugins/spotify/index.js +22 -0
- package/src/rl/atropos.js +22 -0
- package/src/rl/cli.js +18 -0
- package/src/sessions.js +84 -0
- package/src/skills/index.js +49 -0
- package/src/skin/engine.js +81 -0
- package/src/swe/runner.js +26 -0
- package/src/time.js +25 -0
- package/src/tools/ansi_strip.js +8 -0
- package/src/tools/approval.js +15 -0
- package/src/tools/bash.js +35 -0
- package/src/tools/binary_extensions.js +22 -0
- package/src/tools/browser.js +48 -0
- package/src/tools/budget_config.js +13 -0
- package/src/tools/checkpoint.js +29 -0
- package/src/tools/clarify.js +15 -0
- package/src/tools/code_execution.js +27 -0
- package/src/tools/credential_files.js +16 -0
- package/src/tools/cronjob.js +16 -0
- package/src/tools/debug_helpers.js +9 -0
- package/src/tools/delegate.js +28 -0
- package/src/tools/discord_tool.js +13 -0
- package/src/tools/edit.js +31 -0
- package/src/tools/env_passthrough.js +15 -0
- package/src/tools/environments/base.js +26 -0
- package/src/tools/environments/daytona.js +48 -0
- package/src/tools/environments/docker.js +14 -0
- package/src/tools/environments/file_sync.js +60 -0
- package/src/tools/environments/index.js +36 -0
- package/src/tools/environments/local.js +31 -0
- package/src/tools/environments/modal.js +33 -0
- package/src/tools/environments/singularity.js +38 -0
- package/src/tools/environments/ssh.js +14 -0
- package/src/tools/environments/vercel_sandbox.js +47 -0
- package/src/tools/feishu_doc.js +15 -0
- package/src/tools/feishu_drive.js +14 -0
- package/src/tools/file_operations.js +17 -0
- package/src/tools/file_state.js +16 -0
- package/src/tools/file_tools.js +23 -0
- package/src/tools/fuzzy_match.js +8 -0
- package/src/tools/grep.js +51 -0
- package/src/tools/homeassistant_tool.js +15 -0
- package/src/tools/image_gen.js +33 -0
- package/src/tools/interrupt.js +18 -0
- package/src/tools/managed_tool_gateway.js +11 -0
- package/src/tools/mcp_oauth.js +21 -0
- package/src/tools/mcp_oauth_manager.js +20 -0
- package/src/tools/mcp_tool.js +36 -0
- package/src/tools/memory.js +66 -0
- package/src/tools/mixture_of_agents.js +14 -0
- package/src/tools/neutts_synth.js +13 -0
- package/src/tools/openrouter_client.js +13 -0
- package/src/tools/osv_check.js +11 -0
- package/src/tools/patch_parser.js +42 -0
- package/src/tools/path_security.js +16 -0
- package/src/tools/process_registry.js +17 -0
- package/src/tools/read.js +26 -0
- package/src/tools/registry.js +54 -0
- package/src/tools/rl_training.js +13 -0
- package/src/tools/schema_sanitizer.js +18 -0
- package/src/tools/send_message.js +32 -0
- package/src/tools/session_search.js +23 -0
- package/src/tools/skill_manager.js +17 -0
- package/src/tools/skill_usage.js +20 -0
- package/src/tools/skills_guard.js +17 -0
- package/src/tools/skills_hub.js +31 -0
- package/src/tools/skills_index.js +14 -0
- package/src/tools/skills_sync.js +19 -0
- package/src/tools/skills_tool.js +11 -0
- package/src/tools/slash_confirm.js +16 -0
- package/src/tools/terminal.js +29 -0
- package/src/tools/tirith_security.js +25 -0
- package/src/tools/todo.js +54 -0
- package/src/tools/tool_backend_helpers.js +26 -0
- package/src/tools/tool_output_limits.js +15 -0
- package/src/tools/tool_result_storage.js +20 -0
- package/src/tools/transcription.js +19 -0
- package/src/tools/tts.js +19 -0
- package/src/tools/url_safety.js +15 -0
- package/src/tools/vision.js +18 -0
- package/src/tools/voice_mode.js +10 -0
- package/src/tools/web_search.js +37 -0
- package/src/tools/web_tools.js +18 -0
- package/src/tools/website_policy.js +14 -0
- package/src/tools/write.js +25 -0
- package/src/tools/xai_http.js +13 -0
- package/src/tools/yuanbao_tools.js +13 -0
- package/src/toolset_distributions.js +18 -0
- package/src/toolsets.js +26 -0
- package/src/tui/index.js +26 -0
- package/src/utils.js +54 -0
- package/src/web/app.js +547 -0
- package/src/web/index.html +167 -0
- package/src/web/server.js +109 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { db } from '../db.js'
|
|
2
|
+
import { parseCron, matches } from './cron-parse.js'
|
|
3
|
+
import { runTurn } from '../agent/machine.js'
|
|
4
|
+
import { logger } from '../observability/log.js'
|
|
5
|
+
|
|
6
|
+
const log = logger('cron')
|
|
7
|
+
|
|
8
|
+
let _interval = null
|
|
9
|
+
|
|
10
|
+
async function init() {
|
|
11
|
+
const d = await db()
|
|
12
|
+
await d.exec(`CREATE TABLE IF NOT EXISTS cron_jobs (id INTEGER PRIMARY KEY AUTOINCREMENT, cron TEXT NOT NULL, prompt TEXT NOT NULL, model TEXT, last_run INTEGER, created INTEGER NOT NULL, enabled INTEGER NOT NULL DEFAULT 1)`)
|
|
13
|
+
return d
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function createJob({ cron, prompt, model = null }) {
|
|
17
|
+
parseCron(cron)
|
|
18
|
+
const d = await init()
|
|
19
|
+
const info = await d.prepare(`INSERT INTO cron_jobs (cron, prompt, model, created, enabled) VALUES (?, ?, ?, ?, 1)`).run(cron, prompt, model, Date.now())
|
|
20
|
+
const id = Number(info.lastInsertRowid)
|
|
21
|
+
log.info('job created', { id, cron })
|
|
22
|
+
return id
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function listJobs() {
|
|
26
|
+
return await (await init()).prepare(`SELECT * FROM cron_jobs ORDER BY id DESC`).all()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function cancelJob(id) {
|
|
30
|
+
await (await init()).prepare(`UPDATE cron_jobs SET enabled = 0 WHERE id = ?`).run(id)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function deleteJob(id) {
|
|
34
|
+
await (await init()).prepare(`DELETE FROM cron_jobs WHERE id = ?`).run(id)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function tick(now = new Date(), { callLLM = null } = {}) {
|
|
38
|
+
const d = await init()
|
|
39
|
+
const jobs = await d.prepare(`SELECT * FROM cron_jobs WHERE enabled = 1`).all()
|
|
40
|
+
const fired = []
|
|
41
|
+
for (const j of jobs) {
|
|
42
|
+
try {
|
|
43
|
+
const parsed = parseCron(j.cron)
|
|
44
|
+
if (!matches(parsed, now)) continue
|
|
45
|
+
const minuteKey = Math.floor(now.getTime() / 60000)
|
|
46
|
+
if (j.last_run && Math.floor(j.last_run / 60000) === minuteKey) continue
|
|
47
|
+
await d.prepare(`UPDATE cron_jobs SET last_run = ? WHERE id = ?`).run(now.getTime(), j.id)
|
|
48
|
+
fired.push(j)
|
|
49
|
+
runTurn({ prompt: j.prompt, callLLM }).catch(e => log.error('cron run failed', { id: j.id, err: String(e) }))
|
|
50
|
+
} catch (e) { log.error('cron tick failed', { id: j.id, err: String(e) }) }
|
|
51
|
+
}
|
|
52
|
+
return fired
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function startScheduler({ callLLM = null, intervalMs = 30000 } = {}) {
|
|
56
|
+
stopScheduler()
|
|
57
|
+
_interval = setInterval(() => { tick(new Date(), { callLLM }) }, intervalMs)
|
|
58
|
+
return _interval
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function stopScheduler() {
|
|
62
|
+
if (_interval) { clearInterval(_interval); _interval = null }
|
|
63
|
+
}
|
package/src/db.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import { createClient } from '@libsql/client'
|
|
4
|
+
import { getFophHome } from './home.js'
|
|
5
|
+
|
|
6
|
+
let _db = null
|
|
7
|
+
let _dbPromise = null
|
|
8
|
+
const DB_PATH = () => path.join(getFophHome(), 'state', 'sessions.db')
|
|
9
|
+
const USE_MEMORY_DB = () => process.env.FREDDIE_TEST_DB === 'memory'
|
|
10
|
+
|
|
11
|
+
export async function db() {
|
|
12
|
+
if (_db) return _db
|
|
13
|
+
if (_dbPromise) return await _dbPromise
|
|
14
|
+
|
|
15
|
+
_dbPromise = (async () => {
|
|
16
|
+
let client
|
|
17
|
+
let dbPath = null
|
|
18
|
+
|
|
19
|
+
if (USE_MEMORY_DB()) {
|
|
20
|
+
// In-memory mode for tests: no file persistence
|
|
21
|
+
client = createClient({ url: 'file::memory:' })
|
|
22
|
+
} else {
|
|
23
|
+
const dir = path.join(getFophHome(), 'state')
|
|
24
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
25
|
+
dbPath = DB_PATH()
|
|
26
|
+
client = createClient({ url: `file:${dbPath}` })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_db = new DbAdapter(client, dbPath)
|
|
30
|
+
_dbPromise = null
|
|
31
|
+
return _db
|
|
32
|
+
})()
|
|
33
|
+
|
|
34
|
+
return await _dbPromise
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class DbAdapter {
|
|
38
|
+
constructor(client, dbPath) {
|
|
39
|
+
this.client = client
|
|
40
|
+
this.dbPath = dbPath
|
|
41
|
+
this._fts5_unavailable = false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
prepare(sql) {
|
|
45
|
+
return new PreparedStatement(this.client, sql)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async exec(sql) {
|
|
49
|
+
try {
|
|
50
|
+
const statements = sql.split(';').filter(s => s.trim())
|
|
51
|
+
const results = []
|
|
52
|
+
for (const stmt of statements) {
|
|
53
|
+
if (stmt.trim()) {
|
|
54
|
+
const result = await this.client.execute({ sql: stmt.trim() })
|
|
55
|
+
results.push(result)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return results
|
|
59
|
+
} catch (e) {
|
|
60
|
+
throw e
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async run(...args) {
|
|
65
|
+
const [sql, ...params] = args
|
|
66
|
+
const result = await this.client.execute({ sql, args: params })
|
|
67
|
+
return {
|
|
68
|
+
changes: result.rowsAffected,
|
|
69
|
+
lastInsertRowid: result.lastInsertRowid ? BigInt(result.lastInsertRowid) : 0n
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
transaction(fn) {
|
|
74
|
+
return async (...args) => {
|
|
75
|
+
try {
|
|
76
|
+
await this.client.execute('BEGIN TRANSACTION')
|
|
77
|
+
const result = await fn(...args)
|
|
78
|
+
await this.client.execute('COMMIT')
|
|
79
|
+
return result
|
|
80
|
+
} catch (e) {
|
|
81
|
+
try {
|
|
82
|
+
await this.client.execute('ROLLBACK')
|
|
83
|
+
} catch (_) {}
|
|
84
|
+
throw e
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async close() {
|
|
90
|
+
if (this.client) {
|
|
91
|
+
await this.client.close()
|
|
92
|
+
this.client = null
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async clearAll() {
|
|
97
|
+
try {
|
|
98
|
+
const result = await this.client.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
|
99
|
+
if (result.rows && result.rows.length > 0) {
|
|
100
|
+
for (const [tableName] of result.rows) {
|
|
101
|
+
try {
|
|
102
|
+
await this.client.execute(`DROP TABLE IF EXISTS ${tableName}`)
|
|
103
|
+
} catch (e) {
|
|
104
|
+
// Ignore drop errors
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {
|
|
109
|
+
// Ignore errors
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
class PreparedStatement {
|
|
115
|
+
constructor(client, sql) {
|
|
116
|
+
this.client = client
|
|
117
|
+
this.sql = sql
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
bind(params = []) {
|
|
121
|
+
this.params = params
|
|
122
|
+
return this
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async run(...params) {
|
|
126
|
+
const p = Array.isArray(params[0]) ? params[0] : params
|
|
127
|
+
const result = await this.client.execute({ sql: this.sql, args: p })
|
|
128
|
+
return {
|
|
129
|
+
changes: result.rowsAffected,
|
|
130
|
+
lastInsertRowid: result.lastInsertRowid ? BigInt(result.lastInsertRowid) : 0n
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async get(...params) {
|
|
135
|
+
const p = Array.isArray(params[0]) ? params[0] : params
|
|
136
|
+
const result = await this.client.execute({ sql: this.sql, args: p })
|
|
137
|
+
if (!result.rows || result.rows.length === 0) return null
|
|
138
|
+
const row = result.rows[0]
|
|
139
|
+
const obj = {}
|
|
140
|
+
result.columns.forEach((col, i) => {
|
|
141
|
+
obj[col] = row[i]
|
|
142
|
+
})
|
|
143
|
+
return obj
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async all(...params) {
|
|
147
|
+
const p = Array.isArray(params[0]) ? params[0] : params
|
|
148
|
+
const result = await this.client.execute({ sql: this.sql, args: p })
|
|
149
|
+
if (!result.rows || result.rows.length === 0) return []
|
|
150
|
+
return result.rows.map(row => {
|
|
151
|
+
const obj = {}
|
|
152
|
+
result.columns.forEach((col, i) => {
|
|
153
|
+
obj[col] = row[i]
|
|
154
|
+
})
|
|
155
|
+
return obj
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function closeDb() {
|
|
161
|
+
if (_db) {
|
|
162
|
+
await _db.close()
|
|
163
|
+
_db = null
|
|
164
|
+
}
|
|
165
|
+
_dbPromise = null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function resetForTests() {
|
|
169
|
+
// Clear all tables from current db (if open) to clean state
|
|
170
|
+
if (_db) {
|
|
171
|
+
await _db.clearAll()
|
|
172
|
+
}
|
|
173
|
+
await closeDb()
|
|
174
|
+
|
|
175
|
+
// Reset module state for fresh in-memory init
|
|
176
|
+
_db = null
|
|
177
|
+
_dbPromise = null
|
|
178
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
export class BasePlatformAdapter extends EventEmitter {
|
|
3
|
+
constructor(opts = {}) { super(); this.opts = opts; this.platform = opts.platform || 'unknown'; this._running = false }
|
|
4
|
+
getRequiredEnv() { return [] }
|
|
5
|
+
isReady() {
|
|
6
|
+
const need = this.getRequiredEnv()
|
|
7
|
+
return need.every(e => process.env[e] || this.opts[e.toLowerCase()])
|
|
8
|
+
}
|
|
9
|
+
async start() { if (!this.isReady()) throw new Error(this.constructor.name + ': missing env: ' + this.getRequiredEnv().join(', ')); this._running = true }
|
|
10
|
+
async stop() { this._running = false }
|
|
11
|
+
async send(_reply) { throw new Error(this.constructor.name + '.send must be overridden') }
|
|
12
|
+
isRunning() { return this._running }
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { bootHook } from './boot.js'
|
|
2
|
+
export { presenceInboundHook, isOnline, presenceMap } from './presence.js'
|
|
3
|
+
export { broadcastHook, addSubscriber } from './broadcast.js'
|
|
4
|
+
export { routingHook } from './routing.js'
|
|
5
|
+
export { denyHook } from './deny.js'
|
|
6
|
+
import { bootHook } from './boot.js'
|
|
7
|
+
import { presenceInboundHook } from './presence.js'
|
|
8
|
+
import { broadcastHook } from './broadcast.js'
|
|
9
|
+
import { routingHook } from './routing.js'
|
|
10
|
+
import { denyHook } from './deny.js'
|
|
11
|
+
export function registerBuiltinHooks(gateway) {
|
|
12
|
+
gateway.addHook('inbound', presenceInboundHook)
|
|
13
|
+
gateway.addHook('inbound', denyHook)
|
|
14
|
+
gateway.addHook('inbound', bootHook)
|
|
15
|
+
gateway.addHook('inbound', routingHook)
|
|
16
|
+
gateway.addHook('outbound', broadcastHook)
|
|
17
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
const _presence = new Map()
|
|
2
|
+
export const presenceInboundHook = async (msg) => { if (msg?.from) _presence.set(msg.from, Date.now()); return msg }
|
|
3
|
+
export function isOnline(from, withinMs = 5 * 60_000) { const ts = _presence.get(from) || 0; return Date.now() - ts < withinMs }
|
|
4
|
+
export function presenceMap() { return Object.fromEntries(_presence) }
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { resolveCommand, getCommand } from '../../commands/registry.js'
|
|
2
|
+
export const routingHook = async (msg) => {
|
|
3
|
+
if (typeof msg?.text !== 'string' || !msg.text.startsWith('/')) return msg
|
|
4
|
+
const name = resolveCommand(msg.text)
|
|
5
|
+
if (!name) return msg
|
|
6
|
+
return { ...msg, slashCommand: name, slashArgs: msg.text.split(/\s+/).slice(1).join(' '), description: getCommand(name)?.description }
|
|
7
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
export function hmacSign(secret, body) { return crypto.createHmac('sha256', secret).update(body).digest('hex') }
|
|
3
|
+
export function hmacVerify(secret, body, signature) {
|
|
4
|
+
const expected = hmacSign(secret, body)
|
|
5
|
+
try { return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(String(signature))) } catch { return false }
|
|
6
|
+
}
|
|
7
|
+
export function aesEncrypt(key, plaintext) {
|
|
8
|
+
const iv = crypto.randomBytes(16)
|
|
9
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key, 'hex').slice(0, 32), iv)
|
|
10
|
+
return iv.toString('hex') + ':' + cipher.update(plaintext, 'utf8', 'hex') + cipher.final('hex')
|
|
11
|
+
}
|
|
12
|
+
export function aesDecrypt(key, ciphertext) {
|
|
13
|
+
const [iv, data] = String(ciphertext).split(':')
|
|
14
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key, 'hex').slice(0, 32), Buffer.from(iv, 'hex'))
|
|
15
|
+
return decipher.update(data, 'hex', 'utf8') + decipher.final('utf8')
|
|
16
|
+
}
|
|
17
|
+
export function parseSignedQuery(query, secret, sigParam = 'signature') {
|
|
18
|
+
const sig = query[sigParam]
|
|
19
|
+
if (!sig) return { valid: false, reason: 'no signature' }
|
|
20
|
+
const sorted = Object.keys(query).filter(k => k !== sigParam).sort().map(k => k + '=' + query[k]).join('&')
|
|
21
|
+
return { valid: hmacVerify(secret, sorted, sig), payload: query }
|
|
22
|
+
}
|
|
23
|
+
export function rateLimitWindow(map, key, windowMs = 1000) {
|
|
24
|
+
const last = map.get(key) || 0; const now = Date.now()
|
|
25
|
+
if (now - last < windowMs) return { allowed: false, retryIn: windowMs - (now - last) }
|
|
26
|
+
map.set(key, now); return { allowed: true }
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
|
|
4
|
+
export class ApiServerAdapter extends EventEmitter {
|
|
5
|
+
constructor({ port = 0 } = {}) { super(); this.port = port; this._server = null; this._messages = [] }
|
|
6
|
+
async start() {
|
|
7
|
+
const app = express()
|
|
8
|
+
app.use(express.json())
|
|
9
|
+
app.post('/messages', (req, res) => {
|
|
10
|
+
const m = { from: req.body?.from || 'api', text: req.body?.text || '', raw: req.body }
|
|
11
|
+
this.emit('message', m)
|
|
12
|
+
res.json({ ok: true })
|
|
13
|
+
})
|
|
14
|
+
app.get('/messages', (_, res) => res.json(this._messages))
|
|
15
|
+
await new Promise(resolve => { this._server = app.listen(this.port, () => resolve()) })
|
|
16
|
+
this.port = this._server.address().port
|
|
17
|
+
}
|
|
18
|
+
async stop() { if (this._server) await new Promise(r => this._server.close(() => r())) }
|
|
19
|
+
async send(reply) { this._messages.push(reply) }
|
|
20
|
+
drain() { const out = [...this._messages]; this._messages.length = 0; return out }
|
|
21
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
|
|
4
|
+
export class BluebubblesAdapter extends EventEmitter {
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
super()
|
|
7
|
+
this.platform = 'bluebubbles'
|
|
8
|
+
this.token = opts.token || process.env.BLUEBUBBLES_PASSWORD
|
|
9
|
+
this.port = opts.port || 0
|
|
10
|
+
this.api = opts.api || "http://localhost:1234/api/v1/message/text"
|
|
11
|
+
this._server = null
|
|
12
|
+
}
|
|
13
|
+
getRequiredEnv() { return ["BLUEBUBBLES_PASSWORD"] }
|
|
14
|
+
async start() {
|
|
15
|
+
if (!this.token) throw new Error('BluebubblesAdapter: ' + this.getRequiredEnv().join(', ') + ' required')
|
|
16
|
+
const app = express()
|
|
17
|
+
app.use(express.json())
|
|
18
|
+
app.post('/webhook', (req, res) => {
|
|
19
|
+
const text = req.body?.text || req.body?.message?.text || req.body?.content || ''
|
|
20
|
+
const from = req.body?.from || req.body?.user_id || req.body?.sender_id || ''
|
|
21
|
+
this.emit('message', { from: String(from), text, raw: req.body })
|
|
22
|
+
res.json({ ok: true })
|
|
23
|
+
})
|
|
24
|
+
await new Promise(r => { this._server = app.listen(this.port, () => r()) })
|
|
25
|
+
this.port = this._server.address().port
|
|
26
|
+
}
|
|
27
|
+
async stop() { if (this._server) await new Promise(r => this._server.close(() => r())) }
|
|
28
|
+
async send(reply) {
|
|
29
|
+
if (!this.token) throw new Error('BluebubblesAdapter: token required')
|
|
30
|
+
return fetch(this.api, { method: 'POST', headers: { authorization: `Bearer ${this.token}`, 'content-type': 'application/json' }, body: JSON.stringify({ to: reply.to, text: reply.text }) }).then(r => r.json())
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
|
|
4
|
+
export class DingtalkAdapter extends EventEmitter {
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
super()
|
|
7
|
+
this.platform = 'dingtalk'
|
|
8
|
+
this.token = opts.token || process.env.DINGTALK_ACCESS_TOKEN
|
|
9
|
+
this.port = opts.port || 0
|
|
10
|
+
this.api = opts.api || "https://oapi.dingtalk.com/robot/send"
|
|
11
|
+
this._server = null
|
|
12
|
+
}
|
|
13
|
+
getRequiredEnv() { return ["DINGTALK_ACCESS_TOKEN"] }
|
|
14
|
+
async start() {
|
|
15
|
+
if (!this.token) throw new Error('DingtalkAdapter: ' + this.getRequiredEnv().join(', ') + ' required')
|
|
16
|
+
const app = express()
|
|
17
|
+
app.use(express.json())
|
|
18
|
+
app.post('/webhook', (req, res) => {
|
|
19
|
+
const text = req.body?.text || req.body?.message?.text || req.body?.content || ''
|
|
20
|
+
const from = req.body?.from || req.body?.user_id || req.body?.sender_id || ''
|
|
21
|
+
this.emit('message', { from: String(from), text, raw: req.body })
|
|
22
|
+
res.json({ ok: true })
|
|
23
|
+
})
|
|
24
|
+
await new Promise(r => { this._server = app.listen(this.port, () => r()) })
|
|
25
|
+
this.port = this._server.address().port
|
|
26
|
+
}
|
|
27
|
+
async stop() { if (this._server) await new Promise(r => this._server.close(() => r())) }
|
|
28
|
+
async send(reply) {
|
|
29
|
+
if (!this.token) throw new Error('DingtalkAdapter: token required')
|
|
30
|
+
return fetch(this.api, { method: 'POST', headers: { authorization: `Bearer ${this.token}`, 'content-type': 'application/json' }, body: JSON.stringify({ to: reply.to, text: reply.text }) }).then(r => r.json())
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
|
|
3
|
+
export class DiscordAdapter extends EventEmitter {
|
|
4
|
+
constructor(opts = {}) {
|
|
5
|
+
super()
|
|
6
|
+
this.platform = 'discord'
|
|
7
|
+
this.token = opts.token || process.env.DISCORD_BOT_TOKEN
|
|
8
|
+
this.api = opts.api || 'https://discord.com/api/v10'
|
|
9
|
+
this._ws = null
|
|
10
|
+
}
|
|
11
|
+
getRequiredEnv() { return ['DISCORD_BOT_TOKEN'] }
|
|
12
|
+
async start() {
|
|
13
|
+
if (!this.token) throw new Error('DiscordAdapter: DISCORD_BOT_TOKEN required')
|
|
14
|
+
const gw = await fetch(`${this.api}/gateway/bot`, { headers: { authorization: `Bot ${this.token}` } }).then(r => r.json())
|
|
15
|
+
if (!gw.url) throw new Error('DiscordAdapter: gateway lookup failed: ' + JSON.stringify(gw))
|
|
16
|
+
this.gatewayUrl = gw.url + '/?v=10&encoding=json'
|
|
17
|
+
}
|
|
18
|
+
async stop() { try { this._ws?.close?.() } catch {} }
|
|
19
|
+
async send(reply) {
|
|
20
|
+
if (!this.token) throw new Error('DiscordAdapter: token required')
|
|
21
|
+
const url = `${this.api}/channels/${reply.to}/messages`
|
|
22
|
+
return fetch(url, { method: 'POST', headers: { authorization: `Bot ${this.token}`, 'content-type': 'application/json' }, body: JSON.stringify({ content: reply.text }) }).then(r => r.json())
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
import net from 'node:net'
|
|
3
|
+
|
|
4
|
+
export class EmailAdapter extends EventEmitter {
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
super()
|
|
7
|
+
this.platform = 'email'
|
|
8
|
+
this.smtpHost = opts.smtpHost || process.env.SMTP_HOST
|
|
9
|
+
this.smtpPort = opts.smtpPort || Number(process.env.SMTP_PORT || 587)
|
|
10
|
+
this.smtpUser = opts.smtpUser || process.env.SMTP_USER
|
|
11
|
+
this.smtpPass = opts.smtpPass || process.env.SMTP_PASS
|
|
12
|
+
this.imapHost = opts.imapHost || process.env.IMAP_HOST
|
|
13
|
+
this._running = false
|
|
14
|
+
}
|
|
15
|
+
getRequiredEnv() { return ['SMTP_HOST', 'SMTP_USER', 'SMTP_PASS'] }
|
|
16
|
+
async start() {
|
|
17
|
+
if (!this.smtpHost || !this.smtpUser || !this.smtpPass) throw new Error('EmailAdapter: SMTP_HOST/USER/PASS required')
|
|
18
|
+
this._running = true
|
|
19
|
+
}
|
|
20
|
+
async stop() { this._running = false }
|
|
21
|
+
async send(reply) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const sock = net.createConnection(this.smtpPort, this.smtpHost)
|
|
24
|
+
const lines = []
|
|
25
|
+
const send = s => sock.write(s + '\r\n')
|
|
26
|
+
sock.on('data', d => {
|
|
27
|
+
const text = d.toString()
|
|
28
|
+
lines.push(text)
|
|
29
|
+
const code = parseInt(text.slice(0, 3), 10)
|
|
30
|
+
if (code >= 400) { sock.end(); return reject(new Error('SMTP error: ' + text)) }
|
|
31
|
+
})
|
|
32
|
+
sock.on('error', reject)
|
|
33
|
+
sock.on('connect', () => {
|
|
34
|
+
send('EHLO freddie')
|
|
35
|
+
send('AUTH LOGIN')
|
|
36
|
+
send(Buffer.from(this.smtpUser).toString('base64'))
|
|
37
|
+
send(Buffer.from(this.smtpPass).toString('base64'))
|
|
38
|
+
send('MAIL FROM:<' + this.smtpUser + '>')
|
|
39
|
+
send('RCPT TO:<' + reply.to + '>')
|
|
40
|
+
send('DATA')
|
|
41
|
+
send('Subject: ' + (reply.subject || 'freddie'))
|
|
42
|
+
send('To: ' + reply.to)
|
|
43
|
+
send('')
|
|
44
|
+
send(reply.text)
|
|
45
|
+
send('.')
|
|
46
|
+
send('QUIT')
|
|
47
|
+
setTimeout(() => { sock.end(); resolve({ ok: true, log: lines.join('') }) }, 500)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
|
|
4
|
+
export class FeishuAdapter extends EventEmitter {
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
super()
|
|
7
|
+
this.platform = 'feishu'
|
|
8
|
+
this.token = opts.token || process.env.FEISHU_APP_TOKEN
|
|
9
|
+
this.port = opts.port || 0
|
|
10
|
+
this.api = opts.api || "https://open.feishu.cn/open-apis/im/v1/messages"
|
|
11
|
+
this._server = null
|
|
12
|
+
}
|
|
13
|
+
getRequiredEnv() { return ["FEISHU_APP_TOKEN"] }
|
|
14
|
+
async start() {
|
|
15
|
+
if (!this.token) throw new Error('FeishuAdapter: ' + this.getRequiredEnv().join(', ') + ' required')
|
|
16
|
+
const app = express()
|
|
17
|
+
app.use(express.json())
|
|
18
|
+
app.post('/webhook', (req, res) => {
|
|
19
|
+
const text = req.body?.text || req.body?.message?.text || req.body?.content || ''
|
|
20
|
+
const from = req.body?.from || req.body?.user_id || req.body?.sender_id || ''
|
|
21
|
+
this.emit('message', { from: String(from), text, raw: req.body })
|
|
22
|
+
res.json({ ok: true })
|
|
23
|
+
})
|
|
24
|
+
await new Promise(r => { this._server = app.listen(this.port, () => r()) })
|
|
25
|
+
this.port = this._server.address().port
|
|
26
|
+
}
|
|
27
|
+
async stop() { if (this._server) await new Promise(r => this._server.close(() => r())) }
|
|
28
|
+
async send(reply) {
|
|
29
|
+
if (!this.token) throw new Error('FeishuAdapter: token required')
|
|
30
|
+
return fetch(this.api, { method: 'POST', headers: { authorization: `Bearer ${this.token}`, 'content-type': 'application/json' }, body: JSON.stringify({ to: reply.to, text: reply.text }) }).then(r => r.json())
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export async function listComments({ token, docToken }) {
|
|
2
|
+
const r = await fetch('https://open.feishu.cn/open-apis/comments/v1/files/' + docToken + '/comments', { headers: { authorization: 'Bearer ' + token } })
|
|
3
|
+
return await r.json()
|
|
4
|
+
}
|
|
5
|
+
export async function postComment({ token, docToken, content }) {
|
|
6
|
+
const r = await fetch('https://open.feishu.cn/open-apis/comments/v1/files/' + docToken + '/comments', { method: 'POST', headers: { authorization: 'Bearer ' + token, 'content-type': 'application/json' }, body: JSON.stringify({ comment: { content } }) })
|
|
7
|
+
return await r.json()
|
|
8
|
+
}
|
|
9
|
+
export async function resolveComment({ token, docToken, commentId }) {
|
|
10
|
+
const r = await fetch('https://open.feishu.cn/open-apis/comments/v1/files/' + docToken + '/comments/' + commentId + '/patch', { method: 'PATCH', headers: { authorization: 'Bearer ' + token, 'content-type': 'application/json' }, body: JSON.stringify({ is_solved: true }) })
|
|
11
|
+
return await r.json()
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const COMMENT_RULES = {
|
|
2
|
+
auto_reply_keywords: ['question', 'pls', 'help'],
|
|
3
|
+
skip_authors: [],
|
|
4
|
+
max_comment_age_hours: 168,
|
|
5
|
+
}
|
|
6
|
+
export function shouldAutoReply(comment, rules = COMMENT_RULES) {
|
|
7
|
+
const text = String(comment?.content || '').toLowerCase()
|
|
8
|
+
if (rules.skip_authors.includes(comment.author)) return false
|
|
9
|
+
if (rules.max_comment_age_hours && comment.created && (Date.now() - comment.created) > rules.max_comment_age_hours * 3600_000) return false
|
|
10
|
+
return rules.auto_reply_keywords.some(k => text.includes(k.toLowerCase()))
|
|
11
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
|
|
4
|
+
export class HomeassistantAdapter extends EventEmitter {
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
super()
|
|
7
|
+
this.platform = 'homeassistant'
|
|
8
|
+
this.token = opts.token || process.env.HASS_TOKEN
|
|
9
|
+
this.port = opts.port || 0
|
|
10
|
+
this.api = opts.api || "http://homeassistant.local:8123/api/services/notify/notify"
|
|
11
|
+
this._server = null
|
|
12
|
+
}
|
|
13
|
+
getRequiredEnv() { return ["HASS_TOKEN"] }
|
|
14
|
+
async start() {
|
|
15
|
+
if (!this.token) throw new Error('HomeassistantAdapter: ' + this.getRequiredEnv().join(', ') + ' required')
|
|
16
|
+
const app = express()
|
|
17
|
+
app.use(express.json())
|
|
18
|
+
app.post('/webhook', (req, res) => {
|
|
19
|
+
const text = req.body?.text || req.body?.message?.text || req.body?.content || ''
|
|
20
|
+
const from = req.body?.from || req.body?.user_id || req.body?.sender_id || ''
|
|
21
|
+
this.emit('message', { from: String(from), text, raw: req.body })
|
|
22
|
+
res.json({ ok: true })
|
|
23
|
+
})
|
|
24
|
+
await new Promise(r => { this._server = app.listen(this.port, () => r()) })
|
|
25
|
+
this.port = this._server.address().port
|
|
26
|
+
}
|
|
27
|
+
async stop() { if (this._server) await new Promise(r => this._server.close(() => r())) }
|
|
28
|
+
async send(reply) {
|
|
29
|
+
if (!this.token) throw new Error('HomeassistantAdapter: token required')
|
|
30
|
+
return fetch(this.api, { method: 'POST', headers: { authorization: `Bearer ${this.token}`, 'content-type': 'application/json' }, body: JSON.stringify({ to: reply.to, text: reply.text }) }).then(r => r.json())
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
|
|
3
|
+
export class MatrixAdapter extends EventEmitter {
|
|
4
|
+
constructor(opts = {}) {
|
|
5
|
+
super()
|
|
6
|
+
this.platform = 'matrix'
|
|
7
|
+
this.homeserver = opts.homeserver || process.env.MATRIX_HOMESERVER
|
|
8
|
+
this.token = opts.token || process.env.MATRIX_ACCESS_TOKEN
|
|
9
|
+
this.since = null
|
|
10
|
+
this._running = false
|
|
11
|
+
}
|
|
12
|
+
getRequiredEnv() { return ['MATRIX_HOMESERVER', 'MATRIX_ACCESS_TOKEN'] }
|
|
13
|
+
async start() {
|
|
14
|
+
if (!this.homeserver || !this.token) throw new Error('MatrixAdapter: MATRIX_HOMESERVER + MATRIX_ACCESS_TOKEN required')
|
|
15
|
+
this._running = true
|
|
16
|
+
this._loop()
|
|
17
|
+
}
|
|
18
|
+
async stop() { this._running = false }
|
|
19
|
+
async _loop() {
|
|
20
|
+
while (this._running) {
|
|
21
|
+
try {
|
|
22
|
+
const url = `${this.homeserver}/_matrix/client/v3/sync?timeout=25000${this.since ? '&since=' + this.since : ''}`
|
|
23
|
+
const res = await fetch(url, { headers: { authorization: `Bearer ${this.token}` } })
|
|
24
|
+
const data = await res.json()
|
|
25
|
+
this.since = data.next_batch
|
|
26
|
+
const rooms = data.rooms?.join || {}
|
|
27
|
+
for (const [roomId, room] of Object.entries(rooms)) {
|
|
28
|
+
for (const ev of (room.timeline?.events || [])) {
|
|
29
|
+
if (ev.type === 'm.room.message') this.emit('message', { from: roomId, text: ev.content?.body || '', user: ev.sender, raw: ev })
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} catch (e) { await new Promise(r => setTimeout(r, 2000)) }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async send(reply) {
|
|
36
|
+
const txnId = Date.now() + '-' + Math.random().toString(36).slice(2, 8)
|
|
37
|
+
const url = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(reply.to)}/send/m.room.message/${txnId}`
|
|
38
|
+
return fetch(url, { method: 'PUT', headers: { authorization: `Bearer ${this.token}`, 'content-type': 'application/json' }, body: JSON.stringify({ msgtype: 'm.text', body: reply.text }) }).then(r => r.json())
|
|
39
|
+
}
|
|
40
|
+
}
|