loopengine 0.3.0 → 0.3.2
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/dist/core/agent-config.d.ts +2 -0
- package/dist/core/http-notifier.js +14 -3
- package/dist/core/http-notify-triggers/telegram.d.ts +20 -1
- package/dist/core/http-notify-triggers/telegram.js +63 -17
- package/dist/core/messaging/connector.d.ts +7 -0
- package/dist/core/messaging/connector.js +1 -1
- package/dist/core/messaging/connectors/slack/events.d.ts +2 -1
- package/dist/core/messaging/connectors/slack/events.js +6 -2
- package/dist/core/messaging/connectors/slack/setup.d.ts +2 -1
- package/dist/core/messaging/connectors/slack/setup.js +10 -5
- package/dist/core/messaging/connectors/telegram/format.d.ts +6 -0
- package/dist/core/messaging/connectors/telegram/format.js +57 -0
- package/dist/core/messaging/connectors/telegram/index.d.ts +39 -0
- package/dist/core/messaging/connectors/telegram/index.js +136 -0
- package/dist/core/messaging/connectors/telegram/store.d.ts +31 -0
- package/dist/core/messaging/connectors/telegram/store.js +69 -0
- package/dist/core/messaging/connectors/telegram/updates.d.ts +40 -0
- package/dist/core/messaging/connectors/telegram/updates.js +112 -0
- package/dist/core/messaging/jobs.d.ts +44 -0
- package/dist/core/messaging/jobs.js +100 -0
- package/dist/core/messaging/threads.d.ts +12 -0
- package/dist/core/messaging/threads.js +36 -0
- package/dist/core/notifier-file.d.ts +13 -5
- package/dist/core/notifier-file.js +41 -6
- package/dist/core/run-callbacks.d.ts +39 -0
- package/dist/core/run-callbacks.js +125 -0
- package/dist/core/system-agents/workflow-planner.d.ts +37 -4
- package/dist/core/system-agents/workflow-planner.js +79 -5
- package/dist/core/workflow-runs.d.ts +131 -0
- package/dist/core/workflow-runs.js +229 -0
- package/dist/core/workflow-triggers.d.ts +140 -0
- package/dist/core/workflow-triggers.js +378 -0
- package/dist/web/agents-config-page.js +168 -50
- package/dist/web/notifier-admin.d.ts +69 -13
- package/dist/web/notifier-admin.js +127 -31
- package/dist/web/workflow-admin.d.ts +10 -0
- package/dist/web/workflow-admin.js +42 -3
- package/dist/web/workflow-page.js +93 -2
- package/package.json +7 -1
|
@@ -860,7 +860,7 @@ ${consoleShellScript}
|
|
|
860
860
|
started: ['good', 'Started a run'], filtered: ['', 'Skipped by the filter'], duplicate: ['', 'A retry, skipped'],
|
|
861
861
|
busy: ['', 'Already running'], rejected: ['bad', 'Signature check failed'], invalid: ['bad', 'Couldn' + String.fromCharCode(8217) + 't start'], error: ['bad', 'Failed'],
|
|
862
862
|
};
|
|
863
|
-
var HOOK_SENDERS = { stripe: 'Stripe', github: 'GitHub', shopify: 'Shopify', standard: 'Standard Webhooks' };
|
|
863
|
+
var HOOK_SENDERS = { stripe: 'Stripe', github: 'GitHub', shopify: 'Shopify', standard: 'Standard Webhooks', custom: 'your app' };
|
|
864
864
|
|
|
865
865
|
function hookRowHtml(t) {
|
|
866
866
|
var last = (t.deliveries || []).slice(0, 3).map(function (d) {
|
|
@@ -870,20 +870,30 @@ ${consoleShellScript}
|
|
|
870
870
|
}).join('');
|
|
871
871
|
var rules = [];
|
|
872
872
|
if (t.verify) rules.push(t.verify.sender === 'custom'
|
|
873
|
-
? 'checks the signature in ' + escapeHtml(t.verify.header) + ' (' + escapeHtml(t.verify.algorithm || 'sha256') + ', ' + escapeHtml(t.verify.encoding || 'hex') + ')'
|
|
873
|
+
? 'checks the signature in ' + escapeHtml(t.verify.header || '(header not set yet)') + ' (' + escapeHtml(t.verify.algorithm || 'sha256') + ', ' + escapeHtml(t.verify.encoding || 'hex') + ')'
|
|
874
874
|
: 'checks the ' + escapeHtml(HOOK_SENDERS[t.verify.sender] || t.verify.sender) + ' signature');
|
|
875
875
|
if (t.filter) rules.push('only when ' + escapeHtml(t.filter.path) + ' is ' + t.filter.values.map(escapeHtml).join(' or '));
|
|
876
876
|
if (t.mapping) rules.push(Object.keys(t.mapping).map(function (p) { return escapeHtml(p) + ' ' + String.fromCharCode(8592) + ' ' + escapeHtml(t.mapping[p]); }).join(', '));
|
|
877
877
|
if (t.payload_param) rules.push('the whole payload goes to ' + escapeHtml(t.payload_param));
|
|
878
878
|
if (t.notify) rules.push('sends each result to you');
|
|
879
879
|
if (t.callback_url) rules.push('reports to ' + escapeHtml(t.callback_url));
|
|
880
|
+
// Made by the workflow planner without its secret: it turns on once
|
|
881
|
+
// the sender's signing secret is added.
|
|
882
|
+
var needsSecret = t.verify && !t.verify.secret_set;
|
|
880
883
|
return '<div class="workflow-hook" data-trigger="' + escapeHtml(t.trigger_id) + '">' +
|
|
881
884
|
'<div style="display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap;align-items:baseline"><strong>' + escapeHtml(t.name) + '</strong>' +
|
|
882
|
-
'<code class="hint">/hooks/workflows/' + escapeHtml(t.token_hint) + '</code>' + (t.enabled ? '' : ' <span class="le-pill">Off</span>') + '</div>' +
|
|
885
|
+
'<code class="hint">/hooks/workflows/' + escapeHtml(t.token_hint) + '</code>' + (needsSecret ? ' <span class="le-pill warn">Needs its signing secret</span>' : t.enabled ? '' : ' <span class="le-pill">Off</span>') + '</div>' +
|
|
883
886
|
(rules.length ? '<p class="hint" style="margin:2px 0">' + rules.join(' ' + String.fromCharCode(183) + ' ') + '</p>' : '') +
|
|
887
|
+
(needsSecret
|
|
888
|
+
? '<div class="workflow-param-row" style="margin:4px 0">' +
|
|
889
|
+
(t.verify.sender === 'custom' && !t.verify.header ? '<input class="hook-secret-header" placeholder="Signature header, e.g. X-Signature">' : '') +
|
|
890
|
+
'<input class="hook-secret-input" type="password" autocomplete="off" placeholder="' + escapeHtml(t.verify.sender === 'custom' ? 'The secret your app signs with' : 'The signing secret ' + (HOOK_SENDERS[t.verify.sender] || 'the sender') + ' shows') + '" style="flex:1">' +
|
|
891
|
+
'<button type="button" class="primary hook-secret-save">Add and turn on</button></div>' +
|
|
892
|
+
'<p class="hint" style="margin:0">Paste the URL into the sender first. Lost it? New URL makes another.</p>'
|
|
893
|
+
: '') +
|
|
884
894
|
(last ? '<ul class="schedule-history">' + last + '</ul>' : '<p class="hint" style="margin:2px 0">No deliveries yet.</p>') +
|
|
885
895
|
'<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:4px">' +
|
|
886
|
-
'<button type="button" class="hook-toggle">' + (t.enabled ? 'Turn off' : 'Turn on') + '</button>' +
|
|
896
|
+
(needsSecret ? '' : '<button type="button" class="hook-toggle">' + (t.enabled ? 'Turn off' : 'Turn on') + '</button>') +
|
|
887
897
|
'<button type="button" class="hook-rotate">New URL</button>' +
|
|
888
898
|
'<button type="button" class="delete-btn hook-delete">Delete</button></div></div>';
|
|
889
899
|
}
|
|
@@ -930,11 +940,14 @@ ${consoleShellScript}
|
|
|
930
940
|
workflowEditing = true;
|
|
931
941
|
box.hidden = false;
|
|
932
942
|
var base = '/agents/' + encodeURIComponent(name) + '/workflows/' + encodeURIComponent(w.id) + '/triggers';
|
|
943
|
+
var hooksById = {};
|
|
933
944
|
function render(shownUrl) {
|
|
934
945
|
box.innerHTML = '<p class="hint" style="margin:0">Loading' + String.fromCharCode(8230) + '</p>';
|
|
935
946
|
fetchJsonRetryOnce(base).then(function (r) {
|
|
936
947
|
if (!r.ok) { box.innerHTML = '<p class="error">' + escapeHtml(r.body.error || 'Could not load webhooks') + '</p>'; return; }
|
|
937
948
|
var list = r.body.triggers || [];
|
|
949
|
+
hooksById = {};
|
|
950
|
+
list.forEach(function (t) { hooksById[t.trigger_id] = t; });
|
|
938
951
|
box.innerHTML = '<h4 style="margin:10px 0 4px">Webhooks</h4>' +
|
|
939
952
|
'<p class="hint" style="margin:0 0 6px">A secret URL an outside service (Stripe, GitHub, a form, monitoring) posts its events to; each one starts a run.</p>' +
|
|
940
953
|
(shownUrl ? hookUrlHtml(shownUrl) : '') +
|
|
@@ -953,9 +966,25 @@ ${consoleShellScript}
|
|
|
953
966
|
for (var i = 0; i < rows.length; i++) {
|
|
954
967
|
(function (row) {
|
|
955
968
|
var id = row.dataset.trigger;
|
|
956
|
-
row.querySelector('.hook-toggle')
|
|
969
|
+
var toggle = row.querySelector('.hook-toggle');
|
|
970
|
+
if (toggle) toggle.addEventListener('click', function (ev) {
|
|
957
971
|
act(id, '', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ enabled: ev.currentTarget.textContent === 'Turn on' }) });
|
|
958
972
|
});
|
|
973
|
+
var saveSecret = row.querySelector('.hook-secret-save');
|
|
974
|
+
if (saveSecret) saveSecret.addEventListener('click', function () {
|
|
975
|
+
var t = hooksById[id];
|
|
976
|
+
var secret = row.querySelector('.hook-secret-input').value.trim();
|
|
977
|
+
if (!secret || !t) return;
|
|
978
|
+
var verify = { sender: t.verify.sender, secret: secret };
|
|
979
|
+
if (t.verify.sender === 'custom') {
|
|
980
|
+
var headerInput = row.querySelector('.hook-secret-header');
|
|
981
|
+
verify.header = headerInput ? headerInput.value.trim() : t.verify.header;
|
|
982
|
+
verify.algorithm = t.verify.algorithm || 'sha256';
|
|
983
|
+
verify.encoding = t.verify.encoding || 'hex';
|
|
984
|
+
if (t.verify.prefix) verify.prefix = t.verify.prefix;
|
|
985
|
+
}
|
|
986
|
+
act(id, '', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ verify: verify, enabled: true }) });
|
|
987
|
+
});
|
|
959
988
|
row.querySelector('.hook-rotate').addEventListener('click', function (ev) {
|
|
960
989
|
var btn = ev.currentTarget;
|
|
961
990
|
if (btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again: the old URL stops working'; return; }
|
|
@@ -1576,17 +1605,34 @@ ${consoleShellScript}
|
|
|
1576
1605
|
return '<strong>Nowhere yet.</strong> Set up an app below; the first messaging app you save becomes primary.' + fallback + rule;
|
|
1577
1606
|
}
|
|
1578
1607
|
|
|
1579
|
-
// Slack
|
|
1608
|
+
// Slack and Telegram: whether replies to a request are answered. On
|
|
1580
1609
|
// unless turned off; locked on LoopEngine Cloud's free plan.
|
|
1581
|
-
function renderConversationsField(n, f) {
|
|
1610
|
+
function renderConversationsField(n, f, app) {
|
|
1582
1611
|
var free = n.plan === 'free';
|
|
1583
1612
|
var on = !free && f.conversations !== false;
|
|
1613
|
+
var where = app === 'telegram' ? 'Answer replies to a request' : 'Answer replies in a request's thread';
|
|
1584
1614
|
return '<div class="body-field" style="margin:4px 0 0">' +
|
|
1585
|
-
'<label class="tool-picker-item" style="border:0;padding:0;justify-content:flex-start"><input type="checkbox" name="conversations" id="
|
|
1586
|
-
|
|
1615
|
+
'<label class="tool-picker-item" style="border:0;padding:0;justify-content:flex-start"><input type="checkbox" name="conversations" id="' + app + 'Conversations"' + (on ? ' checked' : '') + (free ? ' disabled' : '') + '> ' +
|
|
1616
|
+
where + (free ? ' <span class="le-pill accent">Pro</span>' : '') + '</label>' +
|
|
1587
1617
|
'<p class="hint" style="margin:2px 0 0">' + (free
|
|
1588
|
-
? 'Talking a request through in
|
|
1589
|
-
:
|
|
1618
|
+
? 'Talking a request through in ' + CHANNEL_TITLES[app] + ' needs LoopEngine Pro. Approve and Deny buttons work on every plan.'
|
|
1619
|
+
: (app === 'telegram'
|
|
1620
|
+
? 'People can reply to a request to ask the agent about it, and it answers there. Approvers (below) can also decide it that way.'
|
|
1621
|
+
: 'People can ask the agent about a request in its thread, and it answers there. Approvers (below) can also decide it there.')) + '</p></div>';
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// Slack and Telegram: whether a DM to the bot, or a mention of it,
|
|
1625
|
+
// runs the agent. Off unless turned on; locked on the free plan.
|
|
1626
|
+
function renderJobsField(n, f, app) {
|
|
1627
|
+
var free = n.plan === 'free';
|
|
1628
|
+
var on = !free && f.jobs === true;
|
|
1629
|
+
var how = app === 'telegram'
|
|
1630
|
+
? 'A private chat with the bot, or @mentioning it in a group, runs the agent on that message, and it answers with a reply. Replying to its answer carries the job on. In a group the bot only sees mentions when it's an admin or its privacy mode is off (@BotFather, /setprivacy); replies to its messages always reach it.'
|
|
1631
|
+
: 'A DM to the bot, or @mentioning it in a channel, runs the agent on that message, and it answers in a thread. Replies in the thread carry the job on.';
|
|
1632
|
+
return '<div class="body-field" style="margin:4px 0 0">' +
|
|
1633
|
+
'<label class="tool-picker-item" style="border:0;padding:0;justify-content:flex-start"><input type="checkbox" name="jobs" id="' + app + 'Jobs"' + (on ? ' checked' : '') + (free ? ' disabled' : '') + '> ' +
|
|
1634
|
+
'Start jobs from messages' + (free ? ' <span class="le-pill accent">Pro</span>' : '') + '</label>' +
|
|
1635
|
+
'<p class="hint" style="margin:2px 0 0">' + (free ? 'Starting jobs from ' + CHANNEL_TITLES[app] + ' needs LoopEngine Pro.' : how + ' Only approvers (below) can give it jobs; with none listed, anyone in its ' + (app === 'telegram' ? 'chat' : 'channel') + ' can, but not by direct message. A request it needs approved waits in the same thread.') + '</p></div>';
|
|
1590
1636
|
}
|
|
1591
1637
|
|
|
1592
1638
|
// A secret's state at a glance: a pill by the label, and a placeholder
|
|
@@ -1631,7 +1677,15 @@ ${consoleShellScript}
|
|
|
1631
1677
|
'</ol></details>' +
|
|
1632
1678
|
secretField(n, 'telegram', 'Bot token') +
|
|
1633
1679
|
'<label>Chat ID <span class="hint">(a group or channel id like -1001234567890, or @channelname)</span><input name="chatId" placeholder="-1001234567890" value="' + escapeHtml(f.chatId || '') + '"></label>' +
|
|
1634
|
-
'<
|
|
1680
|
+
'<div class="body-field" style="margin:0">Replies and buttons' +
|
|
1681
|
+
'<p class="hint" style="margin:2px 0 6px">' + (n.telegram && n.telegram.webhookSet
|
|
1682
|
+
? 'Telegram sends the bot's messages and button clicks to this server, so requests have Approve and Deny buttons and replies reach the agent.'
|
|
1683
|
+
: 'Each message links to a page where the person approves, denies or answers. Connect the bot to this server (save the bot token first) to add Approve and Deny buttons and let people reply.') + '</p>' +
|
|
1684
|
+
'<span style="display:flex;gap:6px;flex-wrap:wrap"><button type="button" id="telegramWebhookSet"' + (n.secretsSet && n.secretsSet.telegram ? '' : ' disabled') + '>' + (n.telegram && n.telegram.webhookSet ? 'Connect again' : 'Connect the bot') + '</button>' +
|
|
1685
|
+
(n.telegram && n.telegram.webhookSet ? '<button type="button" id="telegramWebhookCheck">Check connection</button>' : '') + '</span>' +
|
|
1686
|
+
'<div class="hint" id="telegramWebhookResult"></div></div>' +
|
|
1687
|
+
renderConversationsField(n, f, 'telegram') +
|
|
1688
|
+
renderJobsField(n, f, 'telegram');
|
|
1635
1689
|
}
|
|
1636
1690
|
if (channel === 'slack') {
|
|
1637
1691
|
return '<details class="slack-step"' + (n.secretsSet && n.secretsSet.slack ? '' : ' open') + '><summary>Create the Slack app (once)</summary>' +
|
|
@@ -1652,7 +1706,8 @@ ${consoleShellScript}
|
|
|
1652
1706
|
(n.slack && n.slack.signingSecretSet ? 'buttons and replies in Slack are on' : 'needed for buttons and replies in Slack; without it, messages only carry the approval link') + ')</span>' +
|
|
1653
1707
|
'<input type="password" name="signingSecret" autocomplete="off" placeholder="' + escapeHtml(secretPlaceholder(n.slack && n.slack.signingSecretSet, 'Paste the Signing Secret')) + '"></label>' +
|
|
1654
1708
|
'<p class="hint" style="margin:0">Buttons send their clicks to <code id="slackInteractivityUrl"></code> (the manifest already points there).</p>' +
|
|
1655
|
-
renderConversationsField(n, f) +
|
|
1709
|
+
renderConversationsField(n, f, 'slack') +
|
|
1710
|
+
renderJobsField(n, f, 'slack') +
|
|
1656
1711
|
'<p class="hint" style="margin:0" id="slackEventsNote"></p>';
|
|
1657
1712
|
}
|
|
1658
1713
|
if (channel === 'lark') {
|
|
@@ -1757,40 +1812,48 @@ ${consoleShellScript}
|
|
|
1757
1812
|
return html;
|
|
1758
1813
|
}
|
|
1759
1814
|
|
|
1760
|
-
// Slack
|
|
1761
|
-
//
|
|
1762
|
-
//
|
|
1763
|
-
function renderApproversHtml(n) {
|
|
1764
|
-
var
|
|
1765
|
-
if (n.source === 'code' || !
|
|
1766
|
-
var
|
|
1815
|
+
// Slack and Telegram: the people who can decide by replying to a
|
|
1816
|
+
// request, whose button clicks count once any are listed, and who can
|
|
1817
|
+
// give the agent jobs. Shown once that app's connection is saved.
|
|
1818
|
+
function renderApproversHtml(n, app) {
|
|
1819
|
+
var conn = connectionFor(n, app);
|
|
1820
|
+
if (n.source === 'code' || !conn) return '';
|
|
1821
|
+
var title = CHANNEL_TITLES[app];
|
|
1822
|
+
var approvers = conn.approvers || [];
|
|
1767
1823
|
var rows = approvers.map(function (a) {
|
|
1768
1824
|
return '<tr><td>' + escapeHtml(a.name || a.user) + '</td><td><code>' + escapeHtml(a.user) + '</code></td>' +
|
|
1769
1825
|
'<td><button type="button" class="delete-btn approver-remove" data-user="' + escapeHtml(a.user) + '"' + (n.canSave === false ? ' disabled' : '') + '>Remove</button></td></tr>';
|
|
1770
1826
|
}).join('');
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
'<p class="
|
|
1827
|
+
var where = app === 'telegram' ? 'by replying to it' : 'by replying in its thread';
|
|
1828
|
+
return '<section class="approvers-section" data-app="' + app + '"><h3>' + title + ' approvers</h3>' +
|
|
1829
|
+
(n.primary !== app ? '<p class="hint" style="margin:0 0 8px">' + title + ' isn't primary, so requests aren't sent there until it is. Jobs started there still use this list.</p>' : '') +
|
|
1830
|
+
'<p class="muted" style="margin:0 0 10px">People who can approve, deny, change or answer a request ' + where + ', like “yes, go ahead” or “make it $40”. A plain “yes” runs it; a reply that's less clear (“sure, send it”, “yes but check the amount”) shows exactly what will run with a Confirm button first, and a change is shown to approve first. Once anyone is listed, only approvers' button clicks count, and only approvers can give the agent jobs. Anyone can still ask about a request.</p>' +
|
|
1774
1831
|
(approvers.length
|
|
1775
|
-
? '<div class="le-table-wrap" style="max-width:560px"><table><thead><tr><th>Name</th><th>
|
|
1776
|
-
: '<p class="hint">No approvers yet. Until you add someone, anyone
|
|
1832
|
+
? '<div class="le-table-wrap" style="max-width:560px"><table><thead><tr><th>Name</th><th>' + title + ' ID</th><th></th></tr></thead><tbody>' + rows + '</tbody></table></div>'
|
|
1833
|
+
: '<p class="hint">No approvers yet. Until you add someone, anyone can use the buttons, and deciding by reply is off.</p>') +
|
|
1777
1834
|
(n.canSave === false
|
|
1778
1835
|
? '<p class="hint">Changing approvers needs <code>LOOPENGINE_ADMIN_AUTH</code> set on the server.</p>'
|
|
1779
|
-
: '<p style="margin:10px 0 0"><button type="button"
|
|
1780
|
-
'<
|
|
1781
|
-
|
|
1836
|
+
: '<p style="margin:10px 0 0"><button type="button" class="approvers-load">' + (app === 'telegram' ? 'Add people who messaged the bot' : 'Add people from the channel') + '</button></p>' +
|
|
1837
|
+
(app === 'telegram' ? '<p class="hint" style="margin:6px 0 0">Telegram can't list a chat's members, so people show up here once they've written to the bot (a private message to it is enough), or add someone by their numeric user id:</p>' +
|
|
1838
|
+
'<span style="display:flex;gap:6px;max-width:560px;margin-top:6px"><input class="approver-manual-id" placeholder="User id, like 123456789" style="flex:1;min-width:0"><input class="approver-manual-name" placeholder="Name" style="flex:1;min-width:0"><button type="button" class="approver-manual-add">Add</button></span>' : '') +
|
|
1839
|
+
'<div class="approvers-picker"></div>') +
|
|
1840
|
+
'<div class="hint approvers-result"></div></section>';
|
|
1782
1841
|
}
|
|
1783
1842
|
|
|
1784
1843
|
function wireApproversHandlers(name, n) {
|
|
1785
|
-
var
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1844
|
+
var sections = detail.querySelectorAll('.approvers-section');
|
|
1845
|
+
for (var s = 0; s < sections.length; s++) wireApproversSection(name, n, sections[s]);
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
function wireApproversSection(name, n, section) {
|
|
1849
|
+
var app = section.dataset.app;
|
|
1850
|
+
var result = section.querySelector('.approvers-result');
|
|
1851
|
+
var conn = connectionFor(n, app);
|
|
1852
|
+
var current = (conn && conn.approvers) || [];
|
|
1790
1853
|
function save(list, message) {
|
|
1791
1854
|
result.textContent = 'Saving' + String.fromCharCode(8230);
|
|
1792
1855
|
fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + '/notifier/approvers', {
|
|
1793
|
-
method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ approvers: list }),
|
|
1856
|
+
method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ app: app, approvers: list }),
|
|
1794
1857
|
}).then(function (r) {
|
|
1795
1858
|
if (!r.ok) { result.textContent = r.body.error || 'Could not save'; return; }
|
|
1796
1859
|
loadApprovalsSection(name, message);
|
|
@@ -1801,31 +1864,46 @@ ${consoleShellScript}
|
|
|
1801
1864
|
removes[i].addEventListener('click', function (ev) {
|
|
1802
1865
|
var btn = ev.currentTarget;
|
|
1803
1866
|
if (btn.dataset.confirming !== '1') { btn.dataset.confirming = '1'; btn.textContent = 'Click again to remove'; return; }
|
|
1804
|
-
save(current.filter(function (a) { return a.user !== btn.dataset.user; }), '
|
|
1867
|
+
save(current.filter(function (a) { return a.user !== btn.dataset.user; }), CHANNEL_TITLES[app] + ' approver removed.');
|
|
1805
1868
|
});
|
|
1806
1869
|
}
|
|
1807
|
-
var
|
|
1870
|
+
var manualAdd = section.querySelector('.approver-manual-add');
|
|
1871
|
+
if (manualAdd) {
|
|
1872
|
+
manualAdd.addEventListener('click', function () {
|
|
1873
|
+
var id = section.querySelector('.approver-manual-id').value.trim();
|
|
1874
|
+
var who = section.querySelector('.approver-manual-name').value.trim();
|
|
1875
|
+
if (!/^[0-9]{1,20}$/.test(id)) { result.textContent = 'A Telegram user id is a number, like 123456789.'; return; }
|
|
1876
|
+
if (current.some(function (a) { return a.user === id; })) { result.textContent = 'They are already an approver.'; return; }
|
|
1877
|
+
save(current.concat([who ? { user: id, name: who } : { user: id }]), 'Added 1 ' + CHANNEL_TITLES[app] + ' approver.');
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
var load = section.querySelector('.approvers-load');
|
|
1808
1881
|
if (!load) return;
|
|
1809
1882
|
load.addEventListener('click', function () {
|
|
1810
1883
|
load.disabled = true;
|
|
1811
1884
|
result.textContent = 'Loading people' + String.fromCharCode(8230);
|
|
1812
|
-
|
|
1885
|
+
var path = app === 'telegram' ? '/notifier/telegram-people' : '/notifier/slack-members';
|
|
1886
|
+
fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + path).then(function (r) {
|
|
1813
1887
|
load.disabled = false;
|
|
1814
1888
|
if (!r.ok) { result.textContent = r.body.error || 'Could not load people'; return; }
|
|
1815
1889
|
var taken = current.map(function (a) { return a.user; });
|
|
1816
|
-
var people = r.body.members.filter(function (m) { return taken.indexOf(m.id) === -1; });
|
|
1890
|
+
var people = (r.body.members || r.body.people || []).filter(function (m) { return taken.indexOf(m.id) === -1; });
|
|
1817
1891
|
result.textContent = '';
|
|
1818
|
-
var picker = section.querySelector('
|
|
1819
|
-
if (!people.length) {
|
|
1892
|
+
var picker = section.querySelector('.approvers-picker');
|
|
1893
|
+
if (!people.length) {
|
|
1894
|
+
picker.innerHTML = '<p class="hint">' + (app === 'telegram' ? (taken.length ? 'Everyone who has written to the bot is already an approver.' : 'Nobody has written to the bot yet. Send it a message, then try again.') : 'Everyone in the channel is already an approver.') + '</p>';
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1820
1897
|
picker.innerHTML = '<div class="tool-picker" style="max-width:560px;margin-top:8px">' + people.map(function (m) {
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1898
|
+
var label = m.name + (m.username && m.name !== '@' + m.username ? ' (@' + m.username + ')' : '');
|
|
1899
|
+
return '<label class="tool-picker-item" style="justify-content:flex-start"><input type="checkbox" value="' + escapeHtml(m.id) + '" data-name="' + escapeHtml(m.name) + '"> ' + escapeHtml(label) + ' <span class="hint">' + escapeHtml(m.id) + '</span></label>';
|
|
1900
|
+
}).join('') + '</div><p style="margin:8px 0 0"><button type="button" class="primary approvers-add">Add selected</button></p>';
|
|
1901
|
+
picker.querySelector('.approvers-add').addEventListener('click', function () {
|
|
1824
1902
|
var picked = picker.querySelectorAll('input:checked');
|
|
1825
1903
|
if (!picked.length) { result.textContent = 'Pick at least one person.'; return; }
|
|
1826
1904
|
var list = current.slice();
|
|
1827
1905
|
for (var k = 0; k < picked.length; k++) list.push({ user: picked[k].value, name: picked[k].dataset.name });
|
|
1828
|
-
save(list, picked.length === 1 ? 'Added 1 approver.' : 'Added ' + picked.length + ' approvers.');
|
|
1906
|
+
save(list, picked.length === 1 ? 'Added 1 ' + CHANNEL_TITLES[app] + ' approver.' : 'Added ' + picked.length + ' ' + CHANNEL_TITLES[app] + ' approvers.');
|
|
1829
1907
|
});
|
|
1830
1908
|
});
|
|
1831
1909
|
});
|
|
@@ -1837,7 +1915,8 @@ ${consoleShellScript}
|
|
|
1837
1915
|
'<p class="muted" style="margin:0 0 12px">' + renderNotifierStatus(n) + '</p>' +
|
|
1838
1916
|
(n.error ? '<p class="error">' + escapeHtml(n.error) + '</p>' : '') +
|
|
1839
1917
|
(n.source === 'code' ? '' : renderConnectionsHtml(n)) + '</section>' +
|
|
1840
|
-
renderApproversHtml(n) +
|
|
1918
|
+
renderApproversHtml(n, 'slack') +
|
|
1919
|
+
renderApproversHtml(n, 'telegram') +
|
|
1841
1920
|
'<section><h3>Recent deliveries</h3>' + renderDeliveries(n.deliveries || []) + '</section>';
|
|
1842
1921
|
}
|
|
1843
1922
|
|
|
@@ -1901,6 +1980,7 @@ ${consoleShellScript}
|
|
|
1901
1980
|
});
|
|
1902
1981
|
}
|
|
1903
1982
|
if (channel === 'slack') wireSlackFields(name, form, result);
|
|
1983
|
+
if (channel === 'telegram') wireTelegramFields(name, form);
|
|
1904
1984
|
|
|
1905
1985
|
form.addEventListener('submit', function (ev) {
|
|
1906
1986
|
ev.preventDefault();
|
|
@@ -1913,12 +1993,15 @@ ${consoleShellScript}
|
|
|
1913
1993
|
if (channel === 'email') { body.to = data.get('emailTo'); body.from = data.get('emailFrom'); body.provider = data.get('emailProvider'); }
|
|
1914
1994
|
if (data.get('secret')) body.secret = data.get('secret');
|
|
1915
1995
|
if (channel === 'slack' && data.get('signingSecret')) body.signingSecret = data.get('signingSecret');
|
|
1916
|
-
if (channel === 'slack') {
|
|
1917
|
-
var conversationsBox = form.querySelector('#
|
|
1918
|
-
var
|
|
1996
|
+
if (channel === 'slack' || channel === 'telegram') {
|
|
1997
|
+
var conversationsBox = form.querySelector('#' + channel + 'Conversations');
|
|
1998
|
+
var jobsBox = form.querySelector('#' + channel + 'Jobs');
|
|
1999
|
+
var savedConn = lastNotifier && connectionFor(lastNotifier, channel);
|
|
1919
2000
|
// A locked switch (free plan) keeps whatever was saved before.
|
|
1920
2001
|
if (conversationsBox && !conversationsBox.disabled) body.conversations = conversationsBox.checked;
|
|
1921
|
-
else if (
|
|
2002
|
+
else if (savedConn && savedConn.conversations !== undefined) body.conversations = savedConn.conversations;
|
|
2003
|
+
if (jobsBox && !jobsBox.disabled) body.jobs = jobsBox.checked;
|
|
2004
|
+
else if (savedConn && savedConn.jobs !== undefined) body.jobs = savedConn.jobs;
|
|
1922
2005
|
}
|
|
1923
2006
|
// Ticked "Make this the primary messaging app" (or none is primary yet,
|
|
1924
2007
|
// which the server treats the same) makes this one primary.
|
|
@@ -1958,6 +2041,37 @@ ${consoleShellScript}
|
|
|
1958
2041
|
}
|
|
1959
2042
|
}
|
|
1960
2043
|
|
|
2044
|
+
// Telegram's section: Connect the bot points its updates at this
|
|
2045
|
+
// server; Check connection says where they go now.
|
|
2046
|
+
function wireTelegramFields(name, form) {
|
|
2047
|
+
var out = form.querySelector('#telegramWebhookResult');
|
|
2048
|
+
var url = '/agents/' + encodeURIComponent(name) + '/notifier/telegram-webhook';
|
|
2049
|
+
var setBtn = form.querySelector('#telegramWebhookSet');
|
|
2050
|
+
if (setBtn) {
|
|
2051
|
+
setBtn.addEventListener('click', function () {
|
|
2052
|
+
setBtn.disabled = true;
|
|
2053
|
+
out.textContent = 'Connecting' + String.fromCharCode(8230);
|
|
2054
|
+
fetchJsonRetryOnce(url, { method: 'POST' }).then(function (r) {
|
|
2055
|
+
setBtn.disabled = false;
|
|
2056
|
+
if (!r.ok) { out.textContent = r.body.error || 'Could not connect the bot'; return; }
|
|
2057
|
+
loadApprovalsSection(name, 'Telegram connected' + (r.body.botUsername ? ': people can now reply to @' + r.body.botUsername + String.fromCharCode(8217) + 's requests.' : '.'));
|
|
2058
|
+
});
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
var checkBtn = form.querySelector('#telegramWebhookCheck');
|
|
2062
|
+
if (checkBtn) {
|
|
2063
|
+
checkBtn.addEventListener('click', function () {
|
|
2064
|
+
out.textContent = 'Checking' + String.fromCharCode(8230);
|
|
2065
|
+
fetchJsonRetryOnce(url).then(function (r) {
|
|
2066
|
+
if (!r.ok) { out.textContent = r.body.error || 'Could not check'; return; }
|
|
2067
|
+
var b = r.body;
|
|
2068
|
+
out.textContent = (b.pointsHere ? 'Connected' + (b.botUsername ? ' (@' + b.botUsername + ')' : '') + '.' : b.url ? 'The bot sends its updates somewhere else: ' + b.url + '. Connect again to point it here.' : 'The bot isn' + String.fromCharCode(8217) + 't sending its updates anywhere. Connect again.') +
|
|
2069
|
+
(b.lastError ? ' Telegram' + String.fromCharCode(8217) + 's last delivery error: ' + b.lastError : '');
|
|
2070
|
+
});
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
|
|
1961
2075
|
// Slack's section: the manifest follows the link address; Load channels
|
|
1962
2076
|
// fills the picker.
|
|
1963
2077
|
function wireSlackFields(name, form, result) {
|
|
@@ -2040,7 +2154,11 @@ ${consoleShellScript}
|
|
|
2040
2154
|
}
|
|
2041
2155
|
showPendingCount(pending.length);
|
|
2042
2156
|
if (message) {
|
|
2043
|
-
var
|
|
2157
|
+
var approverApp = message.indexOf('Telegram') !== -1 ? 'telegram' : 'slack';
|
|
2158
|
+
// Otherwise the section that was open, which is the one just saved.
|
|
2159
|
+
var r = message.indexOf('pprover') !== -1
|
|
2160
|
+
? content.querySelector('.approvers-section[data-app="' + approverApp + '"] .approvers-result')
|
|
2161
|
+
: content.querySelector('details.msg-conn[open] .notifier-result') || content.querySelector('#notifierResult');
|
|
2044
2162
|
if (r) r.textContent = message;
|
|
2045
2163
|
}
|
|
2046
2164
|
}).catch(function (err) {
|
|
@@ -4,6 +4,7 @@ import { type DeliveryResult } from '../core/http-notify-triggers/webhook.js';
|
|
|
4
4
|
import type { HttpNotifierConfig } from '../core/agent-config.js';
|
|
5
5
|
import { type MessagingApprover, type NotifierFile, type NotifierFileChannel } from '../core/notifier-file.js';
|
|
6
6
|
import { type SlackChannel, type SlackMember } from '../core/messaging/connectors/slack/index.js';
|
|
7
|
+
import { type TelegramPerson } from '../core/messaging/connectors/telegram/index.js';
|
|
7
8
|
/** The deployment-wide default webhook (LOOPENGINE_DEFAULT_WEBHOOK_URL /
|
|
8
9
|
* _SECRET), which applies to the plain /messages route when an agent has
|
|
9
10
|
* no notifier of its own. */
|
|
@@ -31,6 +32,10 @@ export declare function describeAgentNotifier(config: AgentConfig, defaultWebhoo
|
|
|
31
32
|
eventsPath: string;
|
|
32
33
|
manifestTemplate: Record<string, unknown>;
|
|
33
34
|
};
|
|
35
|
+
telegram: {
|
|
36
|
+
webhookSet: boolean;
|
|
37
|
+
webhookPath: string;
|
|
38
|
+
};
|
|
34
39
|
deliveries: import("../core/notifier-deliveries.js").NotifierDelivery[];
|
|
35
40
|
channel?: HttpNotifierConfig["channel"];
|
|
36
41
|
events?: string[];
|
|
@@ -56,24 +61,75 @@ export declare function slackCredentialsFor(config: AgentConfig): {
|
|
|
56
61
|
botToken?: string;
|
|
57
62
|
signingSecret?: string;
|
|
58
63
|
};
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
/** The apps people can talk to an agent in (core/messaging/). */
|
|
65
|
+
export type TwoWayApp = 'slack' | 'telegram';
|
|
66
|
+
/** Whether replies in an app's request threads are answered: on unless
|
|
67
|
+
* that app's section of the Messaging tab turned Conversations off. An
|
|
68
|
+
* agent whose notifier is set in code answers in that app only. */
|
|
69
|
+
export declare function conversationsOn(config: AgentConfig, app: TwoWayApp): boolean;
|
|
62
70
|
export declare function slackConversationsOn(config: AgentConfig): boolean;
|
|
63
|
-
/**
|
|
64
|
-
* messaging.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
*
|
|
71
|
+
/** Whether a DM to the agent's bot in `app`, or a mention of it, starts a
|
|
72
|
+
* job (core/messaging/jobs.ts). Off unless turned on in that app's
|
|
73
|
+
* section; never for a notifier set in code. */
|
|
74
|
+
export declare function jobsOn(config: AgentConfig, app: TwoWayApp): boolean;
|
|
75
|
+
/** The people who can decide this agent's requests from `app`: from that
|
|
76
|
+
* app's connection in messaging.yml; none when the notifier is set in
|
|
77
|
+
* code. */
|
|
78
|
+
export declare function agentApprovers(config: AgentConfig, app?: TwoWayApp): MessagingApprover[];
|
|
79
|
+
/** Whether a person's button click counts: anyone's does while the
|
|
80
|
+
* agent has no approvers in that app (as before approvers existed), only
|
|
81
|
+
* theirs once it has some. `message` says who can, for a refused click. */
|
|
82
|
+
export declare function clickAllowed(config: AgentConfig, app: TwoWayApp, user: string | undefined): {
|
|
83
|
+
allowed: boolean;
|
|
84
|
+
message?: string;
|
|
85
|
+
};
|
|
69
86
|
export declare function slackClickAllowed(config: AgentConfig, user: string | undefined): {
|
|
70
87
|
allowed: boolean;
|
|
71
88
|
message?: string;
|
|
72
89
|
};
|
|
73
|
-
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
export declare function
|
|
90
|
+
/** Whether a person may start a job from a message, or continue one: an
|
|
91
|
+
* approver, once the app has any; with none, anyone in the agent's own
|
|
92
|
+
* channel or chat, but nobody by DM. */
|
|
93
|
+
export declare function jobAllowed(config: AgentConfig, app: TwoWayApp, message: {
|
|
94
|
+
user: string;
|
|
95
|
+
conversation: string;
|
|
96
|
+
direct?: boolean;
|
|
97
|
+
}): {
|
|
98
|
+
allowed: boolean;
|
|
99
|
+
message?: string;
|
|
100
|
+
};
|
|
101
|
+
/** Replaces the approver list on the agent's saved connection for `app`,
|
|
102
|
+
* primary or not. Throws NotifierFileError when there's no connection
|
|
103
|
+
* for that app to add them to. */
|
|
104
|
+
export declare function setAgentApprovers(config: AgentConfig, approvers: unknown, app?: TwoWayApp): MessagingApprover[];
|
|
105
|
+
/** The bot token behind an agent's Telegram notifier and the secret its
|
|
106
|
+
* webhook was set up with, like slackCredentialsFor. */
|
|
107
|
+
export declare function telegramCredentialsFor(config: AgentConfig): {
|
|
108
|
+
botToken?: string;
|
|
109
|
+
webhookSecret?: string;
|
|
110
|
+
};
|
|
111
|
+
/** The address remembered for links when the app's connection was saved. */
|
|
112
|
+
export declare function connectionPublicUrl(config: AgentConfig, app: TwoWayApp): string | undefined;
|
|
113
|
+
/** The Telegram chat the agent posts requests in, if any. */
|
|
114
|
+
export declare function telegramHomeChat(config: AgentConfig): string | undefined;
|
|
115
|
+
/** People who have written to the agent's Telegram bot, newest first,
|
|
116
|
+
* for picking approvers: the Bot API can't list a chat's members. */
|
|
117
|
+
export declare function listAgentTelegramPeople(config: AgentConfig): TelegramPerson[];
|
|
118
|
+
/** Points the agent's Telegram bot at POST /hooks/telegram/<agent> on
|
|
119
|
+
* `baseUrl` (LOOPENGINE_PUBLIC_URL wins), with a secret made once and
|
|
120
|
+
* kept in the agent's env. From then on requests get buttons and replies
|
|
121
|
+
* reach the agent. */
|
|
122
|
+
export declare function setupAgentTelegramWebhook(config: AgentConfig, baseUrl: string): Promise<{
|
|
123
|
+
url: string;
|
|
124
|
+
botUsername?: string;
|
|
125
|
+
}>;
|
|
126
|
+
/** Where the agent's Telegram bot sends its updates now. */
|
|
127
|
+
export declare function agentTelegramWebhookInfo(config: AgentConfig): Promise<{
|
|
128
|
+
url?: string;
|
|
129
|
+
lastError?: string;
|
|
130
|
+
pointsHere: boolean;
|
|
131
|
+
botUsername?: string;
|
|
132
|
+
}>;
|
|
77
133
|
/** The people in the agent's Slack channel, for picking approvers. */
|
|
78
134
|
export declare function listAgentSlackMembers(config: AgentConfig): Promise<SlackMember[]>;
|
|
79
135
|
/** Channels for the console's picker, with the token being typed in or,
|