@rhizogrip/spore-plugin-telegram 0.2.0 → 0.3.0
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/README.md +18 -28
- package/package.json +1 -1
- package/src/forward.js +4 -2
- package/src/service.js +5 -3
package/README.md
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# @rhizogrip/spore-plugin-telegram
|
|
2
2
|
|
|
3
3
|
A service that puts your spores in Telegram chats. It runs on the machine the spores are
|
|
4
|
-
on, once per bot, as a systemd user unit
|
|
5
|
-
everything it does goes through `@rhizogrip/spore`'s exports. It gives spores nothing, so it needs
|
|
4
|
+
on, once per bot, as a systemd user unit. It gives spores nothing, so it needs
|
|
6
5
|
no `spore plugin allow`: `spore service` offers it once it is installed.
|
|
7
6
|
|
|
8
7
|
```bash
|
|
@@ -36,9 +35,8 @@ passed to a spore's runner.
|
|
|
36
35
|
message rather than losing one. One Telegram will not take is settled refused, with
|
|
37
36
|
Telegram's reason, and one it still had not taken when its retries ran out is tried again
|
|
38
37
|
after the spore's next turn or the service's next start.
|
|
39
|
-
- **A reply
|
|
40
|
-
|
|
41
|
-
the messages it answered as `asked`.
|
|
38
|
+
- **A reply never mixes turns.** Each chat message gets the answer, the end state and the files
|
|
39
|
+
of the turn that answered it.
|
|
42
40
|
- **A restart loses nothing.** A turn running when the service stopped finishes into the
|
|
43
41
|
chat's `working…` message; a turn that ended while it was down is posted when it is back.
|
|
44
42
|
- **Nothing fails in silence.** A message a spore refuses is told to the chat; a message
|
|
@@ -50,11 +48,11 @@ Write to the bot and the message goes to the spore the chat is mapped to. The bo
|
|
|
50
48
|
`working…`, shows which tools run as they run, and edits the answer into that message when
|
|
51
49
|
the turn ends. How the turn ended is added only when it makes the answer partial — a wrap
|
|
52
50
|
or a stop. A longer answer than Telegram's 4096 continues in further messages. What the
|
|
53
|
-
turn left in `data/out/` follows as documents
|
|
54
|
-
instead.
|
|
51
|
+
turn left in `data/out/` follows as documents, up to the 50 a turn hands back, then a line
|
|
52
|
+
counting the rest; a file over Telegram's 50 MB is named instead.
|
|
55
53
|
|
|
56
|
-
A message sent while a turn runs lands between its rounds,
|
|
57
|
-
|
|
54
|
+
A message sent while a turn runs lands between its rounds, and its `working…` message says it
|
|
55
|
+
was heard. A turn ending in a question posts the
|
|
58
56
|
question, and the reply answers it. The conversation is the spore's transcript; the service
|
|
59
57
|
keeps none.
|
|
60
58
|
|
|
@@ -84,18 +82,18 @@ except when the turn ended `quiet`, which chose to say nothing. A turn that ende
|
|
|
84
82
|
answer any other way is told. A spore with no default tells no chat. A default naming a chat
|
|
85
83
|
that does not reach the spore through this bot is refused, and the log says why.
|
|
86
84
|
|
|
87
|
-
A turn
|
|
85
|
+
A turn started with `spore send` is shown only there; no chat is told.
|
|
88
86
|
|
|
89
87
|
## The allow-list is the routing
|
|
90
88
|
|
|
91
89
|
A bot's `chats` say which chats may talk to it and which spore each reaches, as
|
|
92
90
|
`{ "<chat id>": "<spore>" }`. They are read on every message and followed as they change, so
|
|
93
91
|
a chat is added, moved or removed by editing a line. It lives beside `config.json` and
|
|
94
|
-
`sources.json
|
|
92
|
+
`sources.json`. Spores cannot read or change it.
|
|
95
93
|
|
|
96
94
|
While a turn works, its chat is shown what kind of call ran — `reading a file`, `running a
|
|
97
95
|
command`, `asking forgejo` by the source's name, a run of the same one counted, a slow one
|
|
98
|
-
timed, a failed
|
|
96
|
+
timed, a command that exited non-zero given its exit code, a call that failed marked — and never what it was given: a Telegram message leaves the
|
|
99
97
|
machine, and a command line or a path is yours. A chat can ask for the calls themselves:
|
|
100
98
|
|
|
101
99
|
```json
|
|
@@ -118,24 +116,18 @@ refused chat 148392011 (@alice) — to allow it, add "148392011": "<a spore>" to
|
|
|
118
116
|
which is also how you learn your own chat id: start with `"chats": {}` and message the bot. A
|
|
119
117
|
group's id is negative, and it is the group you add rather than the people in it.
|
|
120
118
|
|
|
121
|
-
## What a chat is told
|
|
122
|
-
|
|
123
|
-
Which tool ran, and whether it worked — never its arguments or its result. On a console
|
|
124
|
-
those are the point; in a chat they are a path or a command line leaving the machine.
|
|
125
|
-
`forward.js` is that rule. The answer and the stop reason cross in full.
|
|
126
|
-
|
|
127
119
|
## What it keeps
|
|
128
120
|
|
|
129
121
|
`$XDG_STATE_HOME/spore/telegram/<bot>/state.json`, one per bot, written whole and renamed into
|
|
130
122
|
place: where it stands in each spore it follows, in that spore's database, and every message a chat sent that no turn has answered
|
|
131
|
-
yet, with the `working…` message its answer goes into.
|
|
132
|
-
|
|
133
|
-
|
|
123
|
+
yet, with the `working…` message its answer goes into. A spore added later is followed from its
|
|
124
|
+
latest turn; earlier answers are not posted. Nothing is kept in the repository or in the spores'
|
|
125
|
+
home.
|
|
134
126
|
|
|
135
127
|
## Busy
|
|
136
128
|
|
|
137
129
|
A turn that would start with no admission slot free is refused, and the chat is told it is
|
|
138
|
-
busy.
|
|
130
|
+
busy. The only limit is the machine's admission slots, which every other caller shares.
|
|
139
131
|
|
|
140
132
|
## Running it as a service
|
|
141
133
|
|
|
@@ -146,18 +138,16 @@ spore service enable telegram@triage # and at every login
|
|
|
146
138
|
spore service log telegram@triage -f
|
|
147
139
|
```
|
|
148
140
|
|
|
149
|
-
`define` runs the service's
|
|
150
|
-
|
|
141
|
+
`define` first runs the service's start-up checks (`--check`) and refuses a missing bot file or
|
|
142
|
+
token. The unit it
|
|
151
143
|
writes is the template `~/.config/systemd/user/spore-telegram@.service`, marked as spore's, and
|
|
152
144
|
each bot is an instance of it; `spore service` never touches a unit it did not write. `spore
|
|
153
145
|
service rm telegram@<bot>` stops and disables one bot; `spore service rm telegram` stops them all
|
|
154
146
|
and removes the template — each given `--yes` when a bot it stops is running. `spore service ls` says where each service
|
|
155
147
|
stands, and whether lingering is on — without it, user services stop when you log out.
|
|
156
148
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
the whole group. The service picks them up again from what it saved. A refusal — exit 2 —
|
|
160
|
-
is left refused rather than retried every few seconds.
|
|
149
|
+
Restarting the service does not stop the turns it started (`KillMode=process`). The service
|
|
150
|
+
picks them up again from what it saved. After a refusal (exit 2), systemd does not restart it.
|
|
161
151
|
|
|
162
152
|
`~/.config/spore/environment` is what the service and every runner it starts need, since a
|
|
163
153
|
systemd user session starts with almost nothing: each bot's token, `PATH`, the provider
|
package/package.json
CHANGED
package/src/forward.js
CHANGED
|
@@ -71,8 +71,9 @@ export function track (items, event) {
|
|
|
71
71
|
const item = items.find(i => i.callId === event.callId)
|
|
72
72
|
if (!item) return false
|
|
73
73
|
item.durationMs = event.durationMs
|
|
74
|
-
item.
|
|
75
|
-
|
|
74
|
+
item.exit = event.outcome === 'exit' ? event.exit : null
|
|
75
|
+
item.failed = event.outcome === undefined ? Boolean(event.error) : !['ok', 'exit'].includes(event.outcome)
|
|
76
|
+
return item.failed || item.exit !== null || item.durationMs >= SLOW_MS
|
|
76
77
|
}
|
|
77
78
|
const note = (text) => {
|
|
78
79
|
items.push({ plain: text, detailed: text })
|
|
@@ -89,6 +90,7 @@ export function render (items, { detailed = false, last = 6 } = {}) {
|
|
|
89
90
|
for (const item of items) {
|
|
90
91
|
const text = (detailed ? item.detailed : item.plain) +
|
|
91
92
|
(item.failed ? ' — failed' : '') +
|
|
93
|
+
(item.exit != null ? ` — exit ${item.exit}` : '') +
|
|
92
94
|
(item.durationMs >= SLOW_MS ? ` — ${Math.round(item.durationMs / 1000)} s` : '')
|
|
93
95
|
const previous = lines.at(-1)
|
|
94
96
|
if (previous?.text === text) previous.count++
|
package/src/service.js
CHANGED
|
@@ -8,6 +8,7 @@ import { announce, pieces, render, reply, track, waiting } from './forward.js'
|
|
|
8
8
|
import { loadState, saveState } from './state.js'
|
|
9
9
|
|
|
10
10
|
const MAX_DOCUMENT = 50 * 1024 * 1024
|
|
11
|
+
const leftOut = (more) => `and ${more} more file${more === 1 ? '' : 's'} in data/out/, in the spore's workspace`
|
|
11
12
|
const COMMAND = /^\/([a-z]+)(@\w+)?(?:\s+(.+))?$/is
|
|
12
13
|
const NOT_MODIFIED = /message is not modified/
|
|
13
14
|
const TAKES_ARGUMENTS = new Set(['extend', 'wrap'])
|
|
@@ -77,7 +78,7 @@ export function createService ({ telegram, botFile, statePath, env, log, configF
|
|
|
77
78
|
for (const part of rest) await attempt(() => telegram.say(chat, part))
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
const hand = async (attempt, chat, ws, files) => {
|
|
81
|
+
const hand = async (attempt, chat, ws, { files, more }) => {
|
|
81
82
|
for (const file of files) {
|
|
82
83
|
const path = resolve(ws, file.path)
|
|
83
84
|
if (!existsSync(path)) {
|
|
@@ -91,6 +92,7 @@ export function createService ({ telegram, botFile, statePath, env, log, configF
|
|
|
91
92
|
}
|
|
92
93
|
await attempt(() => telegram.document(chat, { path, mime: file.mime }, file.path))
|
|
93
94
|
}
|
|
95
|
+
if (more) await attempt(() => telegram.say(chat, leftOut(more)))
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
const status = (ws) => {
|
|
@@ -316,7 +318,7 @@ export function createService ({ telegram, botFile, statePath, env, log, configF
|
|
|
316
318
|
const turn = turnsSince(ws, entry.turnNo - 1).ended[0]
|
|
317
319
|
const heard = turn.asked.filter(message => entry.answers.includes(message.inboxNo))
|
|
318
320
|
const text = heard.some(message => message.replyTo) ? reply(turn) : announce({ record: turn.record, asked: heard })
|
|
319
|
-
const
|
|
321
|
+
const handed = { files: turn.record.deliverables ?? [], more: turn.record.deliverablesMore ?? 0 }
|
|
320
322
|
try {
|
|
321
323
|
await delivery.to(chat, async (attempt) => {
|
|
322
324
|
if (notes.length) {
|
|
@@ -324,7 +326,7 @@ export function createService ({ telegram, botFile, statePath, env, log, configF
|
|
|
324
326
|
} else {
|
|
325
327
|
for (const part of pieces(text)) await attempt(() => telegram.say(chat, part))
|
|
326
328
|
}
|
|
327
|
-
await hand(attempt, chat, ws,
|
|
329
|
+
await hand(attempt, chat, ws, handed)
|
|
328
330
|
})
|
|
329
331
|
} catch (err) {
|
|
330
332
|
if (err.retryable || stopping.signal.aborted) {
|