@rhizogrip/spore-plugin-telegram 0.7.0 → 0.8.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 +56 -18
- package/bin/spore-telegram.js +1 -1
- package/package.json +3 -3
- package/src/adapter.js +98 -33
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ no `spore plugin allow`: `spore service` offers it once it is installed.
|
|
|
8
8
|
npm i -g @rhizogrip/spore-plugin-telegram
|
|
9
9
|
echo 'TRIAGE_BOT_TOKEN=…' >> ~/.config/spore/environment # from @BotFather; it never reaches a spore
|
|
10
10
|
mkdir -p ~/.config/spore/telegram
|
|
11
|
-
echo '{"token": "${TRIAGE_BOT_TOKEN}", "
|
|
11
|
+
echo '{"token": "${TRIAGE_BOT_TOKEN}", "people": {"148392011": "triage"}}' > ~/.config/spore/telegram/triage.json
|
|
12
12
|
spore service define telegram@triage && spore service start telegram@triage
|
|
13
13
|
```
|
|
14
14
|
|
|
@@ -19,9 +19,9 @@ cannot read, or while another process reads the same bot's updates.
|
|
|
19
19
|
## One bot per file
|
|
20
20
|
|
|
21
21
|
Each bot is a file, `~/.config/spore/telegram/<bot>.json`, and a service instance of its
|
|
22
|
-
own, `telegram@<bot>`: its name, its avatar, its token, its
|
|
23
|
-
should have a face of its own on Telegram gets its own bot. One bot may still reach
|
|
24
|
-
spores, and one spore may be reached through several bots. A chat of a bot is
|
|
22
|
+
own, `telegram@<bot>`: its name, its avatar, its token, its people and groups, and its state. A
|
|
23
|
+
spore that should have a face of its own on Telegram gets its own bot. One bot may still reach
|
|
24
|
+
several spores, and one spore may be reached through several bots. A chat of a bot is
|
|
25
25
|
`telegram:<bot>/<chat id>` wherever spore names a place an answer goes. No bot's token is
|
|
26
26
|
passed to a spore's runner.
|
|
27
27
|
|
|
@@ -45,8 +45,9 @@ passed to a spore's runner.
|
|
|
45
45
|
|
|
46
46
|
## A turn in a chat
|
|
47
47
|
|
|
48
|
-
Write to the bot and the message goes to the spore
|
|
49
|
-
|
|
48
|
+
Write to the bot in private and the message goes to the spore your id is mapped to; in a group,
|
|
49
|
+
mention it or reply to it (below). The spore is told who wrote it and where — your name and
|
|
50
|
+
username, the private chat or the group. The bot posts
|
|
50
51
|
`working…` and shows which tools run as they run. When the turn ends, the answer arrives as a new
|
|
51
52
|
message replying to yours, so it notifies you however long the turn took, and `working…` is
|
|
52
53
|
removed; one Telegram will no longer let the bot remove, past 48 hours, says `answered below`. The
|
|
@@ -93,10 +94,19 @@ A turn started with `spore send` is shown only there; no chat is told.
|
|
|
93
94
|
|
|
94
95
|
## The allow-list is the routing
|
|
95
96
|
|
|
96
|
-
A bot's `
|
|
97
|
-
`{ "<
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
A bot's `people` say who may talk to it in private and which spore each reaches, as
|
|
98
|
+
`{ "<user id>": "<spore>" }` — a private chat's id is its person's. Its `groups` say which groups
|
|
99
|
+
it serves, each with its spore and who there may address it:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{ "token": "${TRIAGE_BOT_TOKEN}",
|
|
103
|
+
"people": { "148392011": "triage" },
|
|
104
|
+
"groups": { "-1001234567": { "spores": ["triage"], "people": ["148392011", "*"] } } }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Both are read on every message and followed as they change, so a person or a group is added,
|
|
108
|
+
moved or removed by editing a line. The file lives beside `config.json` and `sources.json`. Spores
|
|
109
|
+
cannot read or change it.
|
|
100
110
|
|
|
101
111
|
While a turn works, its chat is shown what kind of call ran — `reading a file`, `running a
|
|
102
112
|
command`, `asking forgejo` by the source's name, a run of the same one counted, a slow one
|
|
@@ -104,12 +114,12 @@ timed, a command that exited non-zero given its exit code, a call that failed ma
|
|
|
104
114
|
machine, and a command line or a path is yours. A chat can ask for the calls themselves:
|
|
105
115
|
|
|
106
116
|
```json
|
|
107
|
-
{ "token": "${TRIAGE_BOT_TOKEN}", "
|
|
117
|
+
{ "token": "${TRIAGE_BOT_TOKEN}", "people": { "148392011": { "spore": "triage", "details": true } } }
|
|
108
118
|
```
|
|
109
119
|
|
|
110
120
|
It is then shown `reading data/report.md`, `running git log --oneline -5`,
|
|
111
121
|
`source forgejo --path …` — the first line, cut short. Choose it for a chat you would let
|
|
112
|
-
read the commands; the others keep the kinds.
|
|
122
|
+
read the commands; the others keep the kinds. A group takes `"details": true` the same way.
|
|
113
123
|
|
|
114
124
|
A chat can be shown a turn more quietly, which is what a group gets unless its entry says
|
|
115
125
|
otherwise: `"show": "react"` puts a 👨💻 on your message when the spore takes it and shows Telegram's
|
|
@@ -120,7 +130,7 @@ Telegram lets a bot react with (✅ and ❌ are not among them; the log lists th
|
|
|
120
130
|
|
|
121
131
|
```json
|
|
122
132
|
{ "token": "${OPS_BOT_TOKEN}", "react": { "taken": "👀", "failed": "🤷" },
|
|
123
|
-
"
|
|
133
|
+
"people": { "148392011": { "spore": "triage", "show": "react" } } }
|
|
124
134
|
```
|
|
125
135
|
|
|
126
136
|
Two chats may point at one spore and then share its conversation. A reply goes to the
|
|
@@ -129,13 +139,41 @@ chat that asked; a turn nobody in a chat started goes to the spore's default.
|
|
|
129
139
|
A chat not in the file gets silence, and the log gets the line to add:
|
|
130
140
|
|
|
131
141
|
```
|
|
132
|
-
refused chat 148392011 (@alice) — to allow it, add "148392011": "<a spore>" to the
|
|
142
|
+
refused chat 148392011 (@alice) — to allow it, add "148392011": "<a spore>" to the people of …/telegram/triage.json
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
which is also how you learn your own user id: start with `"people": {}` and message the bot. A
|
|
146
|
+
group the file does not name is told nothing either, when someone there mentions the bot; the log
|
|
147
|
+
says the `groups` entry to add, its id negative. Where a spore's `replyTo` default is one of this
|
|
148
|
+
bot's chats, that chat is told the same line, once per chat the file does not name, so you hear of
|
|
149
|
+
someone trying the bot without reading its log.
|
|
150
|
+
|
|
151
|
+
## A group
|
|
152
|
+
|
|
153
|
+
A group has one spore — in a group the bot speaks as itself, so a second spore there is a second
|
|
154
|
+
bot — and `people`, who may address it: user ids, or `"*"` for everyone in the group, written out.
|
|
155
|
+
A message reaches the spore when it mentions the bot (`@triage_bot what failed?`), replies to one of
|
|
156
|
+
its messages, or is a command for it — `/status`, or `/status@triage_bot` where several bots share
|
|
157
|
+
the group; a command naming another bot is not seen. Anything else said there is not for the
|
|
158
|
+
spore, and the bot's mention is not part of what the spore is told. Someone the entry does not list
|
|
159
|
+
who mentions the bot gets silence, and the log gets the line that would let them in.
|
|
160
|
+
|
|
161
|
+
`/status` is for anyone listed; `/stop`, `/wrap` and `/extend` are for the people the entry names
|
|
162
|
+
by id — not everyone `"*"` lets talk — and anyone else is told so.
|
|
163
|
+
|
|
164
|
+
A group can let its spore hear what is said there that is not to it:
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
"groups": { "-1001234567": { "spores": ["triage"], "people": ["148392011", "209876543"], "overhear": true } }
|
|
133
168
|
```
|
|
134
169
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
170
|
+
What the people it lists say in the group then reaches the spore as overheard: no turn starts for
|
|
171
|
+
it and no answer is owed, and the next turn the spore runs reads it as what was said around it, the
|
|
172
|
+
latest 100 messages at a time. It is everything they say there, kept in the spore's records and
|
|
173
|
+
read by its next turn, so choose it for a group whose talk the spore should follow. Telegram gives a
|
|
174
|
+
bot what is not said to it only with its privacy mode off — `/setprivacy` in @BotFather, then the
|
|
175
|
+
bot added to the group again — or as an admin of the group; with neither, the service says so for
|
|
176
|
+
each group that overhears when it starts.
|
|
139
177
|
|
|
140
178
|
## What it keeps
|
|
141
179
|
|
package/bin/spore-telegram.js
CHANGED
|
@@ -14,6 +14,6 @@ await runService({
|
|
|
14
14
|
open: async ({ bot, credentials }) => {
|
|
15
15
|
const telegram = createTelegram(credentials.token)
|
|
16
16
|
const me = await telegram.me()
|
|
17
|
-
return { adapter: createAdapter({ telegram, bot }), who: `@${me.username}` }
|
|
17
|
+
return { adapter: createAdapter({ telegram, bot, me }), who: `@${me.username}` }
|
|
18
18
|
}
|
|
19
19
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhizogrip/spore-plugin-telegram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Telegram for spores: a bot of yours in chats, each chat reaching its spore, run as a service",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Christophe Le Bars",
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
"marked": "^18.0.11"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@rhizogrip/spore": "^1.
|
|
34
|
+
"@rhizogrip/spore": "^1.21.0"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"test": "vitest run"
|
|
38
38
|
},
|
|
39
39
|
"spore": {
|
|
40
|
-
"works": "^1.
|
|
40
|
+
"works": "^1.21.0",
|
|
41
41
|
"services": {
|
|
42
42
|
"telegram": {
|
|
43
43
|
"what": "a bot of yours in Telegram chats, each chat reaching its spore",
|
package/src/adapter.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram for the relay (`@rhizogrip/spore/relay`): the bot file it reads, the long poll that
|
|
3
|
-
* hears chats, and the Bot API calls that answer them. A chat is a conversation, keyed by its id
|
|
4
|
-
*
|
|
3
|
+
* hears chats, and the Bot API calls that answer them. A chat is a conversation, keyed by its id:
|
|
4
|
+
* a private chat's is its person's, under `people`, and a group's is negative, under `groups`.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { SporeError } from '@rhizogrip/spore'
|
|
@@ -11,7 +11,8 @@ import { toHtml } from './format.js'
|
|
|
11
11
|
const COMMAND = /^\/([a-z]+)(@\w+)?(?:\s+(.+))?$/is
|
|
12
12
|
const NOT_MODIFIED = /message is not modified/
|
|
13
13
|
const CANNOT_PARSE = /can't parse entities/i
|
|
14
|
-
const SHAPE = '{ "token": "${TRIAGE_BOT_TOKEN}", "
|
|
14
|
+
const SHAPE = '{ "token": "${TRIAGE_BOT_TOKEN}", "people": { "<your user id>": "<spore>" }, "groups": { "<group id>": { "spores": ["<spore>"], "people": ["<user id>"] } } }' // eslint-disable-line no-template-curly-in-string
|
|
15
|
+
const VERBS = new Set(['status', 'stop', 'wrap', 'extend', 'help'])
|
|
15
16
|
|
|
16
17
|
/** What a bot may react with: `ReactionTypeEmoji` in the Bot API. */
|
|
17
18
|
const REACTIONS = [
|
|
@@ -25,40 +26,75 @@ const REACTIONS = [
|
|
|
25
26
|
'🤷', '🤷♀', '😡'
|
|
26
27
|
]
|
|
27
28
|
|
|
28
|
-
const FIELDS = ['token', '
|
|
29
|
-
const
|
|
29
|
+
const FIELDS = ['token', 'people', 'groups', 'react']
|
|
30
|
+
const PERSON_FIELDS = ['spore', 'details', 'show']
|
|
31
|
+
const GROUP_FIELDS = ['spores', 'people', 'details', 'show', 'overhear']
|
|
32
|
+
const PERSON = /^\d+$/
|
|
33
|
+
const GROUP = /^-\d+$/
|
|
30
34
|
|
|
31
35
|
const said = (err) => [err.message, err.detail].filter(Boolean).join(' — ')
|
|
32
36
|
|
|
33
|
-
const invalid = (path, problem, detail) => new SporeError(`${path} ${problem}`, { type: 'TELEGRAM_BOT_INVALID', detail })
|
|
37
|
+
const invalid = (path, problem, detail = `a bot is ${SHAPE}`) => new SporeError(`${path} ${problem}`, { type: 'TELEGRAM_BOT_INVALID', detail })
|
|
38
|
+
|
|
39
|
+
const strays = (path, kind, id, entry, takes) => {
|
|
40
|
+
const extra = Object.keys(entry).filter(key => !takes.includes(key))
|
|
41
|
+
if (!extra.length) return
|
|
42
|
+
const bots = extra.filter(key => FIELDS.includes(key))
|
|
43
|
+
throw invalid(path, `gives ${kind} ${id} ${extra.join(', ')}, which a ${kind} does not take`, `a ${kind} takes ${takes.join(', ')}${bots.length ? `; ${bots.join(', ')} is the bot's, beside people and groups` : ''}`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const flags = (path, kind, id, entry, names) => {
|
|
47
|
+
for (const name of names) {
|
|
48
|
+
if (entry[name] !== undefined && typeof entry[name] !== 'boolean') throw invalid(path, `has a ${kind} ${id} whose ${name} is not true or false`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
34
51
|
|
|
35
52
|
/**
|
|
36
|
-
* `{ "<
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
53
|
+
* The private chats, `people: { "<person id>": "<spore>" }` or `{ "spore", "details", "show" }`,
|
|
54
|
+
* and the groups, `groups: { "<group id>": { "spores": ["<spore>"], "people": [...], "details",
|
|
55
|
+
* "show", "overhear" } }`. `details` shows a chat what each call does — its command or path — rather
|
|
56
|
+
* than only what kind of call it is, since a Telegram message leaves the machine; `show` how a turn
|
|
57
|
+
* shows itself there. A group names who may address its spore — user ids, or `*` for everyone in it
|
|
58
|
+
* — and has one spore: in a group a bot speaks as itself, so a second spore is a second bot.
|
|
59
|
+
* `overhear` gives the spore what they say there that is not to it.
|
|
40
60
|
*/
|
|
41
61
|
function routes (raw, path) {
|
|
42
|
-
if (
|
|
43
|
-
throw invalid(path, 'has no
|
|
62
|
+
if (raw.people === undefined && raw.groups === undefined) {
|
|
63
|
+
throw invalid(path, 'has no people and no groups', `a bot is ${SHAPE} — "people": {} while you learn your user id: the log names every chat it refuses`)
|
|
64
|
+
}
|
|
65
|
+
const entries = (field) => {
|
|
66
|
+
const value = raw[field] ?? {}
|
|
67
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) throw invalid(path, `has ${field} that is not { "<id>": … }`)
|
|
68
|
+
return Object.entries(value)
|
|
44
69
|
}
|
|
45
70
|
const table = new Map()
|
|
46
|
-
for (const [
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
71
|
+
for (const [person, entry] of entries('people')) {
|
|
72
|
+
if (!PERSON.test(person)) throw invalid(path, `names "${person}" among its people, which is not a person's id`, 'a person\'s id is a positive number; a group\'s is negative, and goes under groups')
|
|
73
|
+
const shaped = typeof entry === 'object' && entry !== null && !Array.isArray(entry)
|
|
74
|
+
if (shaped) {
|
|
75
|
+
strays(path, 'person', person, entry, PERSON_FIELDS)
|
|
76
|
+
flags(path, 'person', person, entry, ['details'])
|
|
52
77
|
}
|
|
53
|
-
|
|
54
|
-
|
|
78
|
+
const spore = shaped ? entry.spore : entry
|
|
79
|
+
if (typeof spore !== 'string' || !spore) throw invalid(path, `maps person ${person} to something that is not a spore`, 'each person is their id against the name of the spore they reach, or { "spore": "<name>", "details": true, "show": "react" }')
|
|
80
|
+
table.set(person, { spores: [spore], details: shaped && entry.details === true, show: shaped ? entry.show ?? null : null, people: null, overhear: false })
|
|
81
|
+
}
|
|
82
|
+
for (const [group, entry] of entries('groups')) {
|
|
83
|
+
if (!GROUP.test(group)) throw invalid(path, `names "${group}" among its groups, which is not a group's id`, 'a group\'s id is negative (-100… for a supergroup); a person\'s goes under people')
|
|
84
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) throw invalid(path, `has a group ${group} that is not { "spores": [...], "people": [...] }`)
|
|
85
|
+
strays(path, 'group', group, entry, GROUP_FIELDS)
|
|
86
|
+
flags(path, 'group', group, entry, ['details', 'overhear'])
|
|
87
|
+
if (!Array.isArray(entry.spores) || !entry.spores.length || !entry.spores.every(spore => typeof spore === 'string' && spore)) throw invalid(path, `has a group ${group} with no spores`)
|
|
88
|
+
if (entry.spores.length > 1) throw invalid(path, `gives group ${group} ${entry.spores.length} spores`, 'a bot speaks in a group as itself, so a group has one spore — a second spore there is a second bot')
|
|
89
|
+
if (!Array.isArray(entry.people) || !entry.people.every(who => who === '*' || (typeof who === 'string' && PERSON.test(who)))) {
|
|
90
|
+
throw invalid(path, `has a group ${group} whose people are not user ids or *`, 'people is who may address its spore — ["<user id>", …], or ["*"] for everyone in the group, written out')
|
|
55
91
|
}
|
|
56
|
-
table.set(
|
|
92
|
+
table.set(group, { spores: entry.spores, details: entry.details === true, show: entry.show ?? null, people: entry.people, overhear: entry.overhear === true })
|
|
57
93
|
}
|
|
58
94
|
return table
|
|
59
95
|
}
|
|
60
96
|
|
|
61
|
-
const HELP = 'Anything you write
|
|
97
|
+
const HELP = 'Anything you write to me in private goes to the spore; in a group, mention me or reply to me. /status says what it is doing; /stop ends the turn it is running and sets aside what you sent that it had not read; /wrap [10m] asks it to wrap up; /extend wall 30m gives it more, as far as its spore.cmk allows.'
|
|
62
98
|
|
|
63
99
|
export const TELEGRAM = {
|
|
64
100
|
kind: 'telegram',
|
|
@@ -79,23 +115,44 @@ export const TELEGRAM = {
|
|
|
79
115
|
hint (err, { file, bot }) {
|
|
80
116
|
if (err.type === 'TELEGRAM_NO_BOT') {
|
|
81
117
|
const variable = `${bot.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_BOT_TOKEN`
|
|
82
|
-
return `mkdir -p ${file.slice(0, file.lastIndexOf('/'))} && echo '{"token": "$\{${variable}}", "
|
|
118
|
+
return `mkdir -p ${file.slice(0, file.lastIndexOf('/'))} && echo '{"token": "$\{${variable}}", "people": {}}' > ${file}\n with people {}, start the bot and message it — the log names every chat it refuses, with its id`
|
|
83
119
|
}
|
|
84
120
|
if (err.type === 'TELEGRAM_FAILED') return `the token comes from @BotFather, in the variable ${file} names`
|
|
85
121
|
if (err.type === 'TELEGRAM_CONFLICT') return 'one bot token is read by one process — stop the other one, or remove its webhook'
|
|
86
122
|
return null
|
|
87
123
|
},
|
|
88
|
-
refusal: (message, botFile) =>
|
|
89
|
-
`refused
|
|
124
|
+
refusal: (message, botFile) => message.shared
|
|
125
|
+
? `refused ${message.where}, chat ${message.conversation} — to allow it, add "${message.conversation}": { "spores": ["<a spore>"], "people": ["${message.person.id}"] } to the groups of ${botFile}`
|
|
126
|
+
: `refused chat ${message.conversation}${message.person.username ? ` (@${message.person.username})` : ''} — to allow it, add "${message.conversation}": "<a spore>" to the people of ${botFile}`
|
|
90
127
|
}
|
|
91
128
|
|
|
92
129
|
const FILES = ['photo', 'document', 'video', 'audio', 'voice', 'video_note', 'animation', 'sticker']
|
|
93
130
|
|
|
94
|
-
|
|
131
|
+
/** Whether the bot's own `@username` is among what Telegram marked as mentions. */
|
|
132
|
+
const mentions = (message, me) => {
|
|
133
|
+
const source = message.text ?? message.caption ?? ''
|
|
134
|
+
const own = `@${me.username}`.toLowerCase()
|
|
135
|
+
return (message.entities ?? message.caption_entities ?? [])
|
|
136
|
+
.some(entity => entity.type === 'mention' && source.slice(entity.offset, entity.offset + entity.length).toLowerCase() === own)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* An update as the relay takes it, or null for none it takes: what is not a chat message, and a
|
|
141
|
+
* command naming another bot. In a group, a message reaches the spore when it mentions the bot,
|
|
142
|
+
* replies to one of its messages, or is a command for it — one of the relay's verbs, or one naming
|
|
143
|
+
* this bot; anything else names no spore (`addresses: []`). The bot's mention is not part of what
|
|
144
|
+
* was said to the spore.
|
|
145
|
+
*/
|
|
146
|
+
export function normalize (update, bot, me) {
|
|
95
147
|
const message = update.message
|
|
96
148
|
if (message?.chat?.id === undefined) return null
|
|
97
|
-
const
|
|
98
|
-
const command =
|
|
149
|
+
const written = (message.text ?? message.caption ?? '').trim() || null
|
|
150
|
+
const command = written?.match(COMMAND)
|
|
151
|
+
if (command?.[2] && command[2].slice(1).toLowerCase() !== me.username.toLowerCase()) return null
|
|
152
|
+
const shared = message.chat.type !== 'private'
|
|
153
|
+
const addressed = !shared || Boolean(command && (command[2] || VERBS.has(command[1].toLowerCase()))) ||
|
|
154
|
+
mentions(message, me) || message.reply_to_message?.from?.id === me.id
|
|
155
|
+
const text = command ? written : written?.replace(new RegExp(`@${me.username}\\b[,:]?\\s*`, 'gi'), '').trim() || null
|
|
99
156
|
const { first_name: first, last_name: last, username } = message.from
|
|
100
157
|
return {
|
|
101
158
|
id: `${bot}:${update.update_id}`,
|
|
@@ -103,19 +160,27 @@ export function normalize (update, bot) {
|
|
|
103
160
|
message: message.message_id,
|
|
104
161
|
person: { id: String(message.from.id), name: `${[first, last].filter(Boolean).join(' ')}${username ? ` (@${username})` : ''}`, username, groups: [] },
|
|
105
162
|
spore: null,
|
|
106
|
-
where:
|
|
107
|
-
shared
|
|
163
|
+
where: shared ? `the Telegram group "${message.chat.title}"` : 'a private chat on Telegram',
|
|
164
|
+
shared,
|
|
108
165
|
text,
|
|
109
166
|
command: command ? { verb: command[1].toLowerCase(), args: command[3] ?? '' } : null,
|
|
110
|
-
addresses: null,
|
|
167
|
+
addresses: addressed ? null : [],
|
|
111
168
|
files: FILES.filter(kind => message[kind]).length
|
|
112
169
|
}
|
|
113
170
|
}
|
|
114
171
|
|
|
115
|
-
export function createAdapter ({ telegram, bot }) {
|
|
172
|
+
export function createAdapter ({ telegram, bot, me }) {
|
|
116
173
|
return {
|
|
117
174
|
...TELEGRAM,
|
|
118
175
|
|
|
176
|
+
/** Each group that overhears, said when the bot gets only what is addressed to it. */
|
|
177
|
+
prepare: async (table, log) => {
|
|
178
|
+
if (me.can_read_all_group_messages) return
|
|
179
|
+
for (const [group, { overhear }] of table) {
|
|
180
|
+
if (overhear) log(`group ${group} overhears, and @${me.username} gets only what is said to it there: turn its privacy mode off with /setprivacy in @BotFather and add it to the group again, or make it an admin of the group`)
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
|
|
119
184
|
/**
|
|
120
185
|
* One long poll after another until `signal` aborts. An update is handed to `onMessage` and
|
|
121
186
|
* awaited before the offset moves past it, since moving it is what confirms the update to
|
|
@@ -141,7 +206,7 @@ export function createAdapter ({ telegram, bot }) {
|
|
|
141
206
|
continue
|
|
142
207
|
}
|
|
143
208
|
for (const update of updates) {
|
|
144
|
-
const message = normalize(update, bot)
|
|
209
|
+
const message = normalize(update, bot, me)
|
|
145
210
|
if (message) await onMessage(message)
|
|
146
211
|
offset = update.update_id + 1
|
|
147
212
|
}
|