@rhizogrip/spore-plugin-telegram 0.5.0 → 0.6.1

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 CHANGED
@@ -67,7 +67,7 @@ says so in your `working…` message, and the next turn answers it.
67
67
 
68
68
  ```
69
69
  /status what the spore is doing, its turns and what they cost, and the running turn's limits
70
- /stop end the turn it is running
70
+ /stop end the turn it is running, and set aside what you sent that it had not read
71
71
  /wrap [10m] ask the running turn to wrap up, now or in a while
72
72
  /extend wall 30m give it more — wall, cost or rounds — up to the extend.<limit> its spore.cmk grants
73
73
  ```
@@ -104,13 +104,25 @@ timed, a command that exited non-zero given its exit code, a call that failed ma
104
104
  machine, and a command line or a path is yours. A chat can ask for the calls themselves:
105
105
 
106
106
  ```json
107
- { "token": "${MAIN_BOT_TOKEN}", "chats": { "51168024": { "spore": "main", "details": true } } }
107
+ { "token": "${TRIAGE_BOT_TOKEN}", "chats": { "148392011": { "spore": "triage", "details": true } } }
108
108
  ```
109
109
 
110
110
  It is then shown `reading data/report.md`, `running git log --oneline -5`,
111
111
  `source forgejo --path …` — the first line, cut short. Choose it for a chat you would let
112
112
  read the commands; the others keep the kinds.
113
113
 
114
+ A chat can be shown a turn more quietly, which is what a group gets unless its entry says
115
+ otherwise: `"show": "react"` puts a 👨‍💻 on your message when the spore takes it and shows Telegram's
116
+ `typing…` while the turn runs, then takes the 👨‍💻 off when the answer arrives — or puts a 💔 there
117
+ when the turn ended without one. `"show": "working"` is the `working…` message, what a private
118
+ chat gets; `"show": "both"` is both. The bot's two emoji are its own to choose, from the ones
119
+ Telegram lets a bot react with (✅ and ❌ are not among them; the log lists them when one is refused):
120
+
121
+ ```json
122
+ { "token": "${OPS_BOT_TOKEN}", "react": { "taken": "👀", "failed": "🤷" },
123
+ "chats": { "-1001234567": { "spore": "triage", "show": "react" } } }
124
+ ```
125
+
114
126
  Two chats may point at one spore and then share its conversation. A reply goes to the
115
127
  chat that asked; a turn nobody in a chat started goes to the spore's default.
116
128
 
@@ -121,7 +133,9 @@ refused chat 148392011 (@alice) — to allow it, add "148392011": "<a spore>" to
121
133
  ```
122
134
 
123
135
  which is also how you learn your own chat id: start with `"chats": {}` and message the bot. A
124
- group's id is negative, and it is the group you add rather than the people in it.
136
+ group's id is negative, and it is the group you add rather than the people in it. Where a spore's
137
+ `replyTo` default is one of this bot's chats, that chat is told the same line, once per chat the
138
+ file does not name, so you hear of someone trying the bot without reading its log.
125
139
 
126
140
  ## What it keeps
127
141
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhizogrip/spore-plugin-telegram",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
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.16.0"
34
+ "@rhizogrip/spore": "^1.19.0"
35
35
  },
36
36
  "scripts": {
37
37
  "test": "vitest run"
38
38
  },
39
39
  "spore": {
40
- "works": "^1.16.0",
40
+ "works": "^1.19.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
@@ -13,14 +13,30 @@ const NOT_MODIFIED = /message is not modified/
13
13
  const CANNOT_PARSE = /can't parse entities/i
14
14
  const SHAPE = '{ "token": "${TRIAGE_BOT_TOKEN}", "chats": { "<chat id>": "<spore>" } }' // eslint-disable-line no-template-curly-in-string
15
15
 
16
+ /** What a bot may react with: `ReactionTypeEmoji` in the Bot API. */
17
+ const REACTIONS = [
18
+ '❤', '👍', '👎', '🔥', '🥰', '👏', '😁', '🤔', '🤯', '😱',
19
+ '🤬', '😢', '🎉', '🤩', '🤮', '💩', '🙏', '👌', '🕊', '🤡',
20
+ '🥱', '🥴', '😍', '🐳', '❤‍🔥', '🌚', '🌭', '💯', '🤣', '⚡',
21
+ '🍌', '🏆', '💔', '🤨', '😐', '🍓', '🍾', '💋', '🖕', '😈',
22
+ '😴', '😭', '🤓', '👻', '👨‍💻', '👀', '🎃', '🙈', '😇', '😨',
23
+ '🤝', '✍', '🤗', '🫡', '🎅', '🎄', '☃', '💅', '🤪', '🗿',
24
+ '🆒', '💘', '🙉', '🦄', '😘', '💊', '🙊', '😎', '👾', '🤷‍♂',
25
+ '🤷', '🤷‍♀', '😡'
26
+ ]
27
+
28
+ const FIELDS = ['token', 'chats', 'react']
29
+ const CHAT_FIELDS = ['spore', 'details', 'show']
30
+
16
31
  const said = (err) => [err.message, err.detail].filter(Boolean).join(' — ')
17
32
 
18
33
  const invalid = (path, problem, detail) => new SporeError(`${path} ${problem}`, { type: 'TELEGRAM_BOT_INVALID', detail })
19
34
 
20
35
  /**
21
- * `{ "<chat id>": "<spore>" }`, or `{ "<chat id>": { "spore": "<spore>", "details": true } }` for a
22
- * chat shown what each call does — its command or path — rather than only what kind of call it
23
- * is: a Telegram message leaves the machine, so that is a choice made per chat.
36
+ * `{ "<chat id>": "<spore>" }`, or `{ "<chat id>": { "spore": "<spore>", "details": true, "show":
37
+ * "react" } }`: `details` for a chat shown what each call does — its command or path — rather than
38
+ * only what kind of call it is, since a Telegram message leaves the machine; `show` for how a turn
39
+ * shows itself there.
24
40
  */
25
41
  function routes (raw, path) {
26
42
  if (typeof raw.chats !== 'object' || raw.chats === null || Array.isArray(raw.chats)) {
@@ -29,22 +45,26 @@ function routes (raw, path) {
29
45
  const table = new Map()
30
46
  for (const [chat, entry] of Object.entries(raw.chats)) {
31
47
  const spore = typeof entry === 'string' ? entry : entry?.spore
32
- const extra = typeof entry === 'object' && entry !== null ? Object.keys(entry).filter(key => !['spore', 'details'].includes(key)) : []
33
- if (typeof spore !== 'string' || !spore || extra.length || (typeof entry === 'object' && entry.details !== undefined && typeof entry.details !== 'boolean')) {
34
- throw invalid(path, `maps ${chat} to something that is not a spore`, 'each chat is a chat id against the name of a spore it may reach, or { "spore": "<name>", "details": true }')
48
+ const extra = typeof entry === 'object' && entry !== null ? Object.keys(entry).filter(key => !CHAT_FIELDS.includes(key)) : []
49
+ if (extra.length) {
50
+ const bots = extra.filter(key => FIELDS.includes(key))
51
+ throw invalid(path, `gives chat ${chat} ${extra.join(', ')}, which a chat does not take`, `a chat takes ${CHAT_FIELDS.join(', ')}${bots.length ? `; ${bots.join(', ')} is the bot's, beside chats` : ''}`)
52
+ }
53
+ if (typeof spore !== 'string' || !spore || (typeof entry === 'object' && entry.details !== undefined && typeof entry.details !== 'boolean')) {
54
+ throw invalid(path, `maps ${chat} to something that is not a spore`, 'each chat is a chat id against the name of a spore it may reach, or { "spore": "<name>", "details": true, "show": "react" }')
35
55
  }
36
- table.set(chat, { spores: [spore], details: typeof entry === 'object' && entry.details === true })
56
+ table.set(chat, { spores: [spore], details: typeof entry === 'object' && entry.details === true, show: typeof entry === 'object' ? entry.show ?? null : null })
37
57
  }
38
58
  return table
39
59
  }
40
60
 
41
- const HELP = 'Anything you write here goes to the spore. /status says what it is doing; /stop ends the turn it is running; /wrap [10m] asks it to wrap up; /extend wall 30m gives it more, as far as its spore.cmk allows.'
61
+ const HELP = 'Anything you write here goes to the spore. /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.'
42
62
 
43
63
  export const TELEGRAM = {
44
64
  kind: 'telegram',
45
65
  label: 'Telegram',
46
66
  noun: 'chat',
47
- fields: ['token', 'chats'],
67
+ fields: FIELDS,
48
68
  credentials: ['token'],
49
69
  stateFields: [],
50
70
  sourceVariables: [],
@@ -52,8 +72,9 @@ export const TELEGRAM = {
52
72
  example: SHAPE,
53
73
  routes,
54
74
  usage: { help: HELP, extend: '/extend wall 30m, /extend cost 1.50 or /extend rounds 20' },
55
- limits: { message: 4096, file: 50 * 1024 * 1024, editEveryMs: 2500 },
56
- can: { upload: true, remove: true },
75
+ limits: { message: 4096, file: 50 * 1024 * 1024, editEveryMs: 2500, typingEveryMs: 4000 },
76
+ can: { upload: true, remove: true, react: true, typing: true },
77
+ reactions: REACTIONS,
57
78
  hint (err, { file, bot }) {
58
79
  if (err.type === 'TELEGRAM_NO_BOT') {
59
80
  const variable = `${bot.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_BOT_TOKEN`
@@ -81,6 +102,7 @@ export function normalize (update, bot) {
81
102
  message: message.message_id,
82
103
  person: { id: String(message.from.id), name: `${[first, last].filter(Boolean).join(' ')}${username ? ` (@${username})` : ''}`, username },
83
104
  where: message.chat.type === 'private' ? 'a private chat on Telegram' : `the Telegram group "${message.chat.title}"`,
105
+ shared: message.chat.type !== 'private',
84
106
  text,
85
107
  command: command ? { verb: command[1].toLowerCase(), args: command[3] ?? '' } : null,
86
108
  addresses: null,
@@ -146,6 +168,10 @@ export function createAdapter ({ telegram, bot }) {
146
168
 
147
169
  remove: (conversation, note) => telegram.remove(Number(conversation), note),
148
170
 
149
- upload: (conversation, file, caption) => telegram.document(Number(conversation), file, caption)
171
+ upload: (conversation, file, caption) => telegram.document(Number(conversation), file, caption),
172
+
173
+ react: (conversation, message, emoji) => telegram.react(Number(conversation), message, emoji),
174
+
175
+ typing: (conversation) => telegram.typing(Number(conversation))
150
176
  }
151
177
  }
package/src/api.js CHANGED
@@ -64,6 +64,12 @@ export function createTelegram (token, { fetchImpl = fetch, base = BASE } = {})
64
64
 
65
65
  remove: (chat, message) => call('deleteMessage', { chat_id: chat, message_id: message }),
66
66
 
67
+ /** `emoji` on the message as the bot's one reaction, or none when it is null. */
68
+ react: (chat, message, emoji) =>
69
+ call('setMessageReaction', { chat_id: chat, message_id: message, reaction: emoji === null ? [] : [{ type: 'emoji', emoji }] }),
70
+
71
+ typing: (chat) => call('sendChatAction', { chat_id: chat, action: 'typing' }),
72
+
67
73
  document: (chat, { path, mime }, caption) => {
68
74
  const form = new FormData()
69
75
  form.append('chat_id', String(chat))