telefi 0.3.0__tar.gz
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.
- telefi-0.3.0/LICENSE +21 -0
- telefi-0.3.0/PKG-INFO +199 -0
- telefi-0.3.0/README.md +179 -0
- telefi-0.3.0/pyproject.toml +41 -0
- telefi-0.3.0/setup.cfg +4 -0
- telefi-0.3.0/telefi/__init__.py +9 -0
- telefi-0.3.0/telefi/__main__.py +127 -0
- telefi-0.3.0/telefi/adapters.py +95 -0
- telefi-0.3.0/telefi/commands.py +63 -0
- telefi-0.3.0/telefi/compat.py +35 -0
- telefi-0.3.0/telefi/core.py +291 -0
- telefi-0.3.0/telefi/inbound.py +240 -0
- telefi-0.3.0/telefi/logs.py +69 -0
- telefi-0.3.0/telefi/render.py +80 -0
- telefi-0.3.0/telefi/samples.py +67 -0
- telefi-0.3.0/telefi/schema.py +61 -0
- telefi-0.3.0/telefi/schemas/commands.v1.json +72 -0
- telefi-0.3.0/telefi/schemas/envelope.v1.json +178 -0
- telefi-0.3.0/telefi/schemas/hub_config.v1.json +193 -0
- telefi-0.3.0/telefi/schemas/inbound.v1.json +80 -0
- telefi-0.3.0/telefi.egg-info/PKG-INFO +199 -0
- telefi-0.3.0/telefi.egg-info/SOURCES.txt +25 -0
- telefi-0.3.0/telefi.egg-info/dependency_links.txt +1 -0
- telefi-0.3.0/telefi.egg-info/entry_points.txt +2 -0
- telefi-0.3.0/telefi.egg-info/requires.txt +3 -0
- telefi-0.3.0/telefi.egg-info/top_level.txt +1 -0
- telefi-0.3.0/tests/test_telefi.py +329 -0
telefi-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Huang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
telefi-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: telefi
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Telegram as a two-way control surface for scripts, CI and agents: drop a JSON envelope in a folder, get a readable message, and receive the owner's replies back as events.
|
|
5
|
+
Author: Michael Huang
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sibulus13/telefi
|
|
8
|
+
Project-URL: Issues, https://github.com/sibulus13/telefi/issues
|
|
9
|
+
Keywords: telegram,two-way,chatops,human-in-the-loop,notifications,agents
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: Microsoft :: Windows :: Windows 11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Topic :: Communications :: Chat
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest==8.4.2; extra == "dev"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# TeleFi
|
|
22
|
+
|
|
23
|
+
> **Developed and verified on Windows 11** (build 26200, Python 3.12.4). Pure standard-library
|
|
24
|
+
> Python, so other platforms should work, but only Windows 11 is tested today.
|
|
25
|
+
|
|
26
|
+
**Telegram as a two-way control surface for your scripts, CI and autonomous agents.** Programs
|
|
27
|
+
drop a small JSON file in a folder; TeleFi sends it to your Telegram in a readable format and
|
|
28
|
+
records the delivery. You answer from your phone (a button, a typed note, a `/command`) and TeleFi
|
|
29
|
+
hands your answer back to the program as a validated JSON event. Pure Python standard library:
|
|
30
|
+
no runtime dependencies.
|
|
31
|
+
|
|
32
|
+
```mermaid
|
|
33
|
+
flowchart TD
|
|
34
|
+
P["your programs<br/>(any language)"] -->|"envelope JSON<br/>into spool/out/"| H["TeleFi hub"]
|
|
35
|
+
H --> T["your Telegram<br/>(glance-first message)"]
|
|
36
|
+
H --> L["local records<br/>outbox / inbox / conversation"]
|
|
37
|
+
T -->|"tap · reply · /command"| H
|
|
38
|
+
H -->|"inbound JSON<br/>to a queue or a command"| P
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Why TeleFi
|
|
42
|
+
|
|
43
|
+
- **Steer autonomous code from your phone.** An agent or CI job hits a failure; you tap *Retry with
|
|
44
|
+
note*, type "use the 2016-2025 window", and the job receives that note as an event. No laptop, no
|
|
45
|
+
remote session.
|
|
46
|
+
- **One way to notify, from any language.** Producers write a JSON file: no SDK, no network code, no
|
|
47
|
+
bot token in every program. A producer works even while the hub is down (the file waits).
|
|
48
|
+
- **Readable at a glance.** A one-line headline, a one-line "so what", details folded away, links
|
|
49
|
+
as buttons. Every program's messages look the same.
|
|
50
|
+
- **Everything is on record.** Every message, every delivery result, every reply and the whole chat
|
|
51
|
+
transcript, as JSON lines on your disk, rotated before they fill up.
|
|
52
|
+
- **Contracts, not conventions.** Messages, replies, config and command lists are JSON Schemas; a
|
|
53
|
+
new program adopts TeleFi by reading `telefi schema envelope.v1`.
|
|
54
|
+
|
|
55
|
+
## Features
|
|
56
|
+
|
|
57
|
+
| feature | how |
|
|
58
|
+
|---|---|
|
|
59
|
+
| Routing | config rules by source, kind and severity -> Telegram, a local file, or a mock |
|
|
60
|
+
| Dedup, holds, ack | one delivery per `dedup_key` per window; `not_before` holds a message (a grace period); Ack silences re-alerts for N days |
|
|
61
|
+
| Glance-first format | headline + insight, collapsed details, URL buttons, action buttons |
|
|
62
|
+
| Two-way | buttons, ForceReply prompts for notes, replies to any message, `/commands` |
|
|
63
|
+
| Command registry | each program ships a manifest; `/help`, `/list` and Telegram's `/` menu are built from them |
|
|
64
|
+
| Sinks | `queue` (a folder your program drains) or `command` (a program fed the event on stdin; its last output line is the reply) |
|
|
65
|
+
| Log rotation | each log rotates at `rotate_at` x `max_bytes` (the safety margin), keeping `keep` old files |
|
|
66
|
+
| Safe tests | a mock adapter by default; live sends only with `TELEFI_LIVE=1` |
|
|
67
|
+
|
|
68
|
+
## Requirements
|
|
69
|
+
|
|
70
|
+
| | |
|
|
71
|
+
|---|---|
|
|
72
|
+
| OS | Windows 11 (verified). Linux/macOS untested. |
|
|
73
|
+
| Python | 3.12 (verified with 3.12.4) |
|
|
74
|
+
| Runtime dependencies | none ([requirements.txt](requirements.txt) says so explicitly) |
|
|
75
|
+
| Test dependencies | pinned in [requirements-dev.txt](requirements-dev.txt) (pytest 8.4.2 and its deps) |
|
|
76
|
+
| Telegram | a bot token from @BotFather and your chat id |
|
|
77
|
+
|
|
78
|
+
## Try it locally, step by step
|
|
79
|
+
|
|
80
|
+
Each step is self-contained and was run end to end on 2026-10-25. Steps 1-5 need no Telegram.
|
|
81
|
+
|
|
82
|
+
**1. Install and run the tests** (expect all green, one live test skipped)
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
git clone <this repo> telefi && cd telefi
|
|
86
|
+
pip install -e . -r requirements-dev.txt
|
|
87
|
+
python -m pytest
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**2. Use a throwaway home** so nothing touches a real install
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
export TELEFI_HOME="$PWD/.try" # PowerShell: $env:TELEFI_HOME = "$PWD\.try"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**3. See every message type exactly as Telegram shows it** (sends nothing)
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
python -m telefi preview
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**4. Route the samples through the full pipeline with the mock channel**
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
python -m telefi samples
|
|
106
|
+
# ci.failure [error] -> telegram=ok, file=ok ... mode: mock (nothing sent)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**5. Publish your own message, deliver it, and read the record**
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
python -c "from telefi import envelope, publish; publish(envelope(source='me', kind='info', severity='info', title='hello from TeleFi'))"
|
|
113
|
+
python -m telefi drain # info routes to the local file only
|
|
114
|
+
tail -1 .try/outbox.jsonl # the envelope and each channel's result
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**6. Connect Telegram.** Copy `examples/.env.example` to `.env` (git-ignored) and fill it in. Create a bot with [@BotFather](https://t.me/BotFather), send it any message,
|
|
118
|
+
and read your chat id from `https://api.telegram.org/bot<TOKEN>/getUpdates`.
|
|
119
|
+
|
|
120
|
+
| variable | required | meaning |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `TELEGRAM_BOT_TOKEN` | yes | the bot token from BotFather |
|
|
123
|
+
| `TELEGRAM_CHAT_ID` | yes | the only chat TeleFi talks to; every other chat is ignored |
|
|
124
|
+
| `TELEFI_HOME` | no | config and records (default `~/.telefi`) |
|
|
125
|
+
| `TELEFI_SPOOL` | no | the drop folder (default `$TELEFI_HOME/spool`) |
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
python -m telefi samples --send # one real message of each type
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**7. Turn on replies.** Copy the full example config (every option, documented in
|
|
132
|
+
[docs/CONFIGURATION.md](docs/CONFIGURATION.md)); it enables replies, routes `/ci` commands to
|
|
133
|
+
`examples/echo_sink.py` and lists `examples/commands.example.json`:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
cp examples/hub.example.json "$TELEFI_HOME/hub.json"
|
|
137
|
+
python -m telefi serve # leave running; one hub per bot
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
In Telegram: send `/help`, then `/list`, then `/ci retry 7 try again`; the echo sink answers
|
|
141
|
+
`retry received`. Tap *Retry with note* on a sample from step 6, type a note, and see it echoed.
|
|
142
|
+
|
|
143
|
+
**8. Check the records**
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
tail -3 "$TELEFI_HOME/conversation.jsonl" # both directions, in order
|
|
147
|
+
tail -1 "$TELEFI_HOME/inbox.jsonl" # your reply and what the sink returned
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Run it in the background (optional)
|
|
151
|
+
|
|
152
|
+
The hub is one long-running command, `python -m telefi serve`: run it under whatever you already
|
|
153
|
+
use to keep processes alive. Two ready-made examples:
|
|
154
|
+
|
|
155
|
+
- Windows 11 (verified): `powershell -NoProfile -File examples/windows-service.ps1` registers a
|
|
156
|
+
scheduled task running `pythonw.exe -m telefi serve` (no console window), restarted every 5
|
|
157
|
+
minutes if it stops.
|
|
158
|
+
- Linux (untested): `examples/telefi.service` (systemd user unit).
|
|
159
|
+
|
|
160
|
+
Command sinks run in the hub's working directory, so give them absolute paths (or `cwd`).
|
|
161
|
+
|
|
162
|
+
## Send from your program
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from telefi import envelope, publish
|
|
166
|
+
|
|
167
|
+
publish(envelope(source="ci", kind="failure", severity="error",
|
|
168
|
+
title="Build #43 failed", insight="Tests passed; the deploy step timed out.",
|
|
169
|
+
body="step deploy: timeout after 600s", dedup_key="ci:build:43",
|
|
170
|
+
actions=[{"id": "retry_note", "label": "Retry with note", "namespace": "ci",
|
|
171
|
+
"input": {"prompt": "What should change?"}}]))
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
From any other language, write the same JSON to `$TELEFI_HOME/spool/out/<id>.json` atomically
|
|
175
|
+
(write a temp file, then rename): see `examples/publish.mjs` for Node. Replies reach your program
|
|
176
|
+
through a command sink (`examples/echo_sink.py`) or a queue folder (`examples/queue_drain.py`).
|
|
177
|
+
|
|
178
|
+
## Examples (one per configurable part, all validated by the tests)
|
|
179
|
+
|
|
180
|
+
| file | what it configures |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `examples/.env.example` | secrets and paths |
|
|
183
|
+
| `examples/hub.minimal.json` | send-only hub |
|
|
184
|
+
| `examples/hub.example.json` | every hub option: routes, channels, dedup, logs, replies, sinks |
|
|
185
|
+
| `examples/commands.example.json` | an app's `/commands` |
|
|
186
|
+
| `examples/envelope.example.json` | every message field |
|
|
187
|
+
| `examples/inbound.example.json` | what your program receives |
|
|
188
|
+
| `examples/echo_sink.py`, `examples/queue_drain.py` | the two kinds of reply handler |
|
|
189
|
+
| `examples/publish.mjs` | publishing from Node |
|
|
190
|
+
| `examples/windows-service.ps1`, `examples/telefi.service` | running in the background (optional) |
|
|
191
|
+
|
|
192
|
+
## Reference
|
|
193
|
+
|
|
194
|
+
- Contracts: `python -m telefi schema envelope.v1 | inbound.v1 | hub_config.v1 | commands.v1`
|
|
195
|
+
- [Configuration reference](docs/CONFIGURATION.md) · [Design](docs/DESIGN.md) · [Similar projects](docs/LANDSCAPE.md)
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
MIT, see [LICENSE](LICENSE).
|
telefi-0.3.0/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# TeleFi
|
|
2
|
+
|
|
3
|
+
> **Developed and verified on Windows 11** (build 26200, Python 3.12.4). Pure standard-library
|
|
4
|
+
> Python, so other platforms should work, but only Windows 11 is tested today.
|
|
5
|
+
|
|
6
|
+
**Telegram as a two-way control surface for your scripts, CI and autonomous agents.** Programs
|
|
7
|
+
drop a small JSON file in a folder; TeleFi sends it to your Telegram in a readable format and
|
|
8
|
+
records the delivery. You answer from your phone (a button, a typed note, a `/command`) and TeleFi
|
|
9
|
+
hands your answer back to the program as a validated JSON event. Pure Python standard library:
|
|
10
|
+
no runtime dependencies.
|
|
11
|
+
|
|
12
|
+
```mermaid
|
|
13
|
+
flowchart TD
|
|
14
|
+
P["your programs<br/>(any language)"] -->|"envelope JSON<br/>into spool/out/"| H["TeleFi hub"]
|
|
15
|
+
H --> T["your Telegram<br/>(glance-first message)"]
|
|
16
|
+
H --> L["local records<br/>outbox / inbox / conversation"]
|
|
17
|
+
T -->|"tap · reply · /command"| H
|
|
18
|
+
H -->|"inbound JSON<br/>to a queue or a command"| P
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Why TeleFi
|
|
22
|
+
|
|
23
|
+
- **Steer autonomous code from your phone.** An agent or CI job hits a failure; you tap *Retry with
|
|
24
|
+
note*, type "use the 2016-2025 window", and the job receives that note as an event. No laptop, no
|
|
25
|
+
remote session.
|
|
26
|
+
- **One way to notify, from any language.** Producers write a JSON file: no SDK, no network code, no
|
|
27
|
+
bot token in every program. A producer works even while the hub is down (the file waits).
|
|
28
|
+
- **Readable at a glance.** A one-line headline, a one-line "so what", details folded away, links
|
|
29
|
+
as buttons. Every program's messages look the same.
|
|
30
|
+
- **Everything is on record.** Every message, every delivery result, every reply and the whole chat
|
|
31
|
+
transcript, as JSON lines on your disk, rotated before they fill up.
|
|
32
|
+
- **Contracts, not conventions.** Messages, replies, config and command lists are JSON Schemas; a
|
|
33
|
+
new program adopts TeleFi by reading `telefi schema envelope.v1`.
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
| feature | how |
|
|
38
|
+
|---|---|
|
|
39
|
+
| Routing | config rules by source, kind and severity -> Telegram, a local file, or a mock |
|
|
40
|
+
| Dedup, holds, ack | one delivery per `dedup_key` per window; `not_before` holds a message (a grace period); Ack silences re-alerts for N days |
|
|
41
|
+
| Glance-first format | headline + insight, collapsed details, URL buttons, action buttons |
|
|
42
|
+
| Two-way | buttons, ForceReply prompts for notes, replies to any message, `/commands` |
|
|
43
|
+
| Command registry | each program ships a manifest; `/help`, `/list` and Telegram's `/` menu are built from them |
|
|
44
|
+
| Sinks | `queue` (a folder your program drains) or `command` (a program fed the event on stdin; its last output line is the reply) |
|
|
45
|
+
| Log rotation | each log rotates at `rotate_at` x `max_bytes` (the safety margin), keeping `keep` old files |
|
|
46
|
+
| Safe tests | a mock adapter by default; live sends only with `TELEFI_LIVE=1` |
|
|
47
|
+
|
|
48
|
+
## Requirements
|
|
49
|
+
|
|
50
|
+
| | |
|
|
51
|
+
|---|---|
|
|
52
|
+
| OS | Windows 11 (verified). Linux/macOS untested. |
|
|
53
|
+
| Python | 3.12 (verified with 3.12.4) |
|
|
54
|
+
| Runtime dependencies | none ([requirements.txt](requirements.txt) says so explicitly) |
|
|
55
|
+
| Test dependencies | pinned in [requirements-dev.txt](requirements-dev.txt) (pytest 8.4.2 and its deps) |
|
|
56
|
+
| Telegram | a bot token from @BotFather and your chat id |
|
|
57
|
+
|
|
58
|
+
## Try it locally, step by step
|
|
59
|
+
|
|
60
|
+
Each step is self-contained and was run end to end on 2026-10-25. Steps 1-5 need no Telegram.
|
|
61
|
+
|
|
62
|
+
**1. Install and run the tests** (expect all green, one live test skipped)
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone <this repo> telefi && cd telefi
|
|
66
|
+
pip install -e . -r requirements-dev.txt
|
|
67
|
+
python -m pytest
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**2. Use a throwaway home** so nothing touches a real install
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
export TELEFI_HOME="$PWD/.try" # PowerShell: $env:TELEFI_HOME = "$PWD\.try"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**3. See every message type exactly as Telegram shows it** (sends nothing)
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
python -m telefi preview
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**4. Route the samples through the full pipeline with the mock channel**
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
python -m telefi samples
|
|
86
|
+
# ci.failure [error] -> telegram=ok, file=ok ... mode: mock (nothing sent)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**5. Publish your own message, deliver it, and read the record**
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
python -c "from telefi import envelope, publish; publish(envelope(source='me', kind='info', severity='info', title='hello from TeleFi'))"
|
|
93
|
+
python -m telefi drain # info routes to the local file only
|
|
94
|
+
tail -1 .try/outbox.jsonl # the envelope and each channel's result
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**6. Connect Telegram.** Copy `examples/.env.example` to `.env` (git-ignored) and fill it in. Create a bot with [@BotFather](https://t.me/BotFather), send it any message,
|
|
98
|
+
and read your chat id from `https://api.telegram.org/bot<TOKEN>/getUpdates`.
|
|
99
|
+
|
|
100
|
+
| variable | required | meaning |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `TELEGRAM_BOT_TOKEN` | yes | the bot token from BotFather |
|
|
103
|
+
| `TELEGRAM_CHAT_ID` | yes | the only chat TeleFi talks to; every other chat is ignored |
|
|
104
|
+
| `TELEFI_HOME` | no | config and records (default `~/.telefi`) |
|
|
105
|
+
| `TELEFI_SPOOL` | no | the drop folder (default `$TELEFI_HOME/spool`) |
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
python -m telefi samples --send # one real message of each type
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**7. Turn on replies.** Copy the full example config (every option, documented in
|
|
112
|
+
[docs/CONFIGURATION.md](docs/CONFIGURATION.md)); it enables replies, routes `/ci` commands to
|
|
113
|
+
`examples/echo_sink.py` and lists `examples/commands.example.json`:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
cp examples/hub.example.json "$TELEFI_HOME/hub.json"
|
|
117
|
+
python -m telefi serve # leave running; one hub per bot
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
In Telegram: send `/help`, then `/list`, then `/ci retry 7 try again`; the echo sink answers
|
|
121
|
+
`retry received`. Tap *Retry with note* on a sample from step 6, type a note, and see it echoed.
|
|
122
|
+
|
|
123
|
+
**8. Check the records**
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
tail -3 "$TELEFI_HOME/conversation.jsonl" # both directions, in order
|
|
127
|
+
tail -1 "$TELEFI_HOME/inbox.jsonl" # your reply and what the sink returned
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Run it in the background (optional)
|
|
131
|
+
|
|
132
|
+
The hub is one long-running command, `python -m telefi serve`: run it under whatever you already
|
|
133
|
+
use to keep processes alive. Two ready-made examples:
|
|
134
|
+
|
|
135
|
+
- Windows 11 (verified): `powershell -NoProfile -File examples/windows-service.ps1` registers a
|
|
136
|
+
scheduled task running `pythonw.exe -m telefi serve` (no console window), restarted every 5
|
|
137
|
+
minutes if it stops.
|
|
138
|
+
- Linux (untested): `examples/telefi.service` (systemd user unit).
|
|
139
|
+
|
|
140
|
+
Command sinks run in the hub's working directory, so give them absolute paths (or `cwd`).
|
|
141
|
+
|
|
142
|
+
## Send from your program
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from telefi import envelope, publish
|
|
146
|
+
|
|
147
|
+
publish(envelope(source="ci", kind="failure", severity="error",
|
|
148
|
+
title="Build #43 failed", insight="Tests passed; the deploy step timed out.",
|
|
149
|
+
body="step deploy: timeout after 600s", dedup_key="ci:build:43",
|
|
150
|
+
actions=[{"id": "retry_note", "label": "Retry with note", "namespace": "ci",
|
|
151
|
+
"input": {"prompt": "What should change?"}}]))
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
From any other language, write the same JSON to `$TELEFI_HOME/spool/out/<id>.json` atomically
|
|
155
|
+
(write a temp file, then rename): see `examples/publish.mjs` for Node. Replies reach your program
|
|
156
|
+
through a command sink (`examples/echo_sink.py`) or a queue folder (`examples/queue_drain.py`).
|
|
157
|
+
|
|
158
|
+
## Examples (one per configurable part, all validated by the tests)
|
|
159
|
+
|
|
160
|
+
| file | what it configures |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `examples/.env.example` | secrets and paths |
|
|
163
|
+
| `examples/hub.minimal.json` | send-only hub |
|
|
164
|
+
| `examples/hub.example.json` | every hub option: routes, channels, dedup, logs, replies, sinks |
|
|
165
|
+
| `examples/commands.example.json` | an app's `/commands` |
|
|
166
|
+
| `examples/envelope.example.json` | every message field |
|
|
167
|
+
| `examples/inbound.example.json` | what your program receives |
|
|
168
|
+
| `examples/echo_sink.py`, `examples/queue_drain.py` | the two kinds of reply handler |
|
|
169
|
+
| `examples/publish.mjs` | publishing from Node |
|
|
170
|
+
| `examples/windows-service.ps1`, `examples/telefi.service` | running in the background (optional) |
|
|
171
|
+
|
|
172
|
+
## Reference
|
|
173
|
+
|
|
174
|
+
- Contracts: `python -m telefi schema envelope.v1 | inbound.v1 | hub_config.v1 | commands.v1`
|
|
175
|
+
- [Configuration reference](docs/CONFIGURATION.md) · [Design](docs/DESIGN.md) · [Similar projects](docs/LANDSCAPE.md)
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "telefi"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Telegram as a two-way control surface for scripts, CI and agents: drop a JSON envelope in a folder, get a readable message, and receive the owner's replies back as events."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Michael Huang" }]
|
|
13
|
+
keywords = ["telegram", "two-way", "chatops", "human-in-the-loop", "notifications", "agents"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: Microsoft :: Windows :: Windows 11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Topic :: Communications :: Chat",
|
|
19
|
+
]
|
|
20
|
+
# Runtime is pure standard library: no third-party dependencies.
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/sibulus13/telefi"
|
|
25
|
+
Issues = "https://github.com/sibulus13/telefi/issues"
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
telefi = "telefi.__main__:main"
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = ["pytest==8.4.2"]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
include = ["telefi*"]
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.package-data]
|
|
37
|
+
"telefi" = ["schemas/*.json"]
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
addopts = "-q"
|
|
41
|
+
testpaths = ["tests"]
|
telefi-0.3.0/setup.cfg
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""TeleFi: a Telegram adapter layer for two-way messaging; channels and reply sinks are adapters.
|
|
2
|
+
|
|
3
|
+
Producers: `from telefi import envelope, publish` (Python) or write an envelope.v1 JSON file
|
|
4
|
+
into the spool's out/ directory (any language). Design: docs/HUB_DESIGN.md. Format: render.py.
|
|
5
|
+
"""
|
|
6
|
+
from telefi.core import drain, envelope, publish, spool_root
|
|
7
|
+
from telefi.render import render_telegram
|
|
8
|
+
|
|
9
|
+
__all__ = ["drain", "envelope", "publish", "render_telegram", "spool_root"]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""TeleFi CLI.
|
|
2
|
+
|
|
3
|
+
python -m telefi drain deliver everything in the spool once (the scheduled task)
|
|
4
|
+
python -m telefi serve [--interval 30] drain forever; also long-polls Telegram if inbound.enabled
|
|
5
|
+
python -m telefi preview [type ...] print each sample exactly as the owner would see it (no send)
|
|
6
|
+
python -m telefi samples [--send] route the samples; mock unless --send (then really delivers)
|
|
7
|
+
python -m telefi validate <file.json> check an envelope or inbound event against its schema
|
|
8
|
+
python -m telefi schema <name> print a published schema (envelope.v1 | inbound.v1 | hub_config.v1)
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
import tempfile
|
|
16
|
+
import time
|
|
17
|
+
import traceback
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from telefi import core, inbound, schema
|
|
21
|
+
from telefi.render import render_plain
|
|
22
|
+
from telefi.samples import samples
|
|
23
|
+
|
|
24
|
+
SERVE_RETRY_S = 5
|
|
25
|
+
SERVE_RETRY_MAX_S = 300
|
|
26
|
+
PRUNE_EVERY_S = 3600
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _preview(names: list[str], buttons: bool) -> int:
|
|
30
|
+
for name, env in samples().items():
|
|
31
|
+
if names and name not in names:
|
|
32
|
+
continue
|
|
33
|
+
print(f"===== {name} (routes to: {', '.join(core.route(env, core.DEFAULT_CONFIG)) or 'nothing'})")
|
|
34
|
+
print(render_plain(env, buttons=buttons))
|
|
35
|
+
print()
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _samples(send: bool) -> int:
|
|
40
|
+
cfg = core.load_config()
|
|
41
|
+
if not send:
|
|
42
|
+
cfg = {**cfg, "mode": "mock"}
|
|
43
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
44
|
+
spool = Path(tmp) / "spool"
|
|
45
|
+
for env in samples().values():
|
|
46
|
+
core.publish(env, spool=spool)
|
|
47
|
+
for rec in core.drain(cfg, spool=spool):
|
|
48
|
+
env = rec["envelope"]
|
|
49
|
+
res = ", ".join(f"{d['channel']}={'ok' if d['ok'] else 'FAILED ' + str(d['error'])}" for d in rec["deliveries"])
|
|
50
|
+
print(f"{env['source']}.{env['kind']} [{env['severity']}] -> {res or 'no route'}")
|
|
51
|
+
print("mode:", "LIVE (really sent)" if send else "mock (nothing sent)")
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _serve(interval: float) -> int:
|
|
56
|
+
"""Drain + long-poll forever. A failing round is logged to ~/.telefi/hub.log and retried; it
|
|
57
|
+
never ends the process (the scheduled task would only restart it 5 minutes later)."""
|
|
58
|
+
cfg = core.load_config()
|
|
59
|
+
log = core.HOME / "hub.log"
|
|
60
|
+
core.append_jsonl(log, {"at": core.now_iso(), "event": "serve started", "inbound": cfg.get("inbound", {}).get("enabled")})
|
|
61
|
+
if cfg.get("inbound", {}).get("enabled"):
|
|
62
|
+
menu = inbound.register_menu(cfg)
|
|
63
|
+
core.append_jsonl(log, {"at": core.now_iso(), "event": "command menu", "ok": menu["ok"], "error": menu.get("error")})
|
|
64
|
+
failures, pruned_at = 0, 0.0
|
|
65
|
+
while True:
|
|
66
|
+
try:
|
|
67
|
+
core.drain(cfg)
|
|
68
|
+
if time.monotonic() - pruned_at > PRUNE_EVERY_S:
|
|
69
|
+
core.prune_done(core.spool_root(cfg))
|
|
70
|
+
pruned_at = time.monotonic()
|
|
71
|
+
if cfg.get("inbound", {}).get("enabled"):
|
|
72
|
+
inbound.poll_once(cfg, timeout_s=int(interval))
|
|
73
|
+
else:
|
|
74
|
+
time.sleep(interval)
|
|
75
|
+
failures = 0
|
|
76
|
+
except Exception:
|
|
77
|
+
failures += 1
|
|
78
|
+
wait = min(SERVE_RETRY_S * 2 ** (failures - 1), SERVE_RETRY_MAX_S) # an outage never spins
|
|
79
|
+
core.append_jsonl(log, {"at": core.now_iso(), "event": "round failed", "retry_in_s": wait,
|
|
80
|
+
"error": traceback.format_exc()[-2000:]})
|
|
81
|
+
time.sleep(wait)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main(argv=None) -> int:
|
|
85
|
+
if sys.stdout is not None: # None under pythonw.exe (no console)
|
|
86
|
+
sys.stdout.reconfigure(encoding="utf-8") # Windows consoles default to cp1252
|
|
87
|
+
ap = argparse.ArgumentParser(prog="python -m telefi")
|
|
88
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
89
|
+
sub.add_parser("drain")
|
|
90
|
+
s = sub.add_parser("serve")
|
|
91
|
+
s.add_argument("--interval", type=float, default=30.0)
|
|
92
|
+
p = sub.add_parser("preview")
|
|
93
|
+
p.add_argument("types", nargs="*")
|
|
94
|
+
p.add_argument("--no-buttons", action="store_true", help="hide action buttons (as sent while inbound is disabled)")
|
|
95
|
+
sm = sub.add_parser("samples")
|
|
96
|
+
sm.add_argument("--send", action="store_true")
|
|
97
|
+
v = sub.add_parser("validate")
|
|
98
|
+
v.add_argument("file")
|
|
99
|
+
sc = sub.add_parser("schema")
|
|
100
|
+
sc.add_argument("name", choices=schema.SCHEMAS)
|
|
101
|
+
a = ap.parse_args(argv)
|
|
102
|
+
|
|
103
|
+
if a.cmd == "drain":
|
|
104
|
+
recs = core.drain(core.load_config())
|
|
105
|
+
failed = [r for r in recs if r.get("rejected") or any(not d["ok"] for d in r["deliveries"])]
|
|
106
|
+
print(f"[hub] delivered {len(recs) - len(failed)}/{len(recs)}", flush=True)
|
|
107
|
+
return 1 if failed else 0
|
|
108
|
+
if a.cmd == "serve":
|
|
109
|
+
return _serve(a.interval)
|
|
110
|
+
if a.cmd == "preview":
|
|
111
|
+
return _preview(a.types, buttons=not a.no_buttons)
|
|
112
|
+
if a.cmd == "samples":
|
|
113
|
+
return _samples(a.send)
|
|
114
|
+
if a.cmd == "validate":
|
|
115
|
+
obj = json.loads(Path(a.file).read_text(encoding="utf-8"))
|
|
116
|
+
name = "inbound.v1" if str(obj.get("schema", "")).endswith(".inbound.v1") else "envelope.v1"
|
|
117
|
+
bad = schema.validate(obj, name)
|
|
118
|
+
print("\n".join(bad) or f"valid {name}")
|
|
119
|
+
return 1 if bad else 0
|
|
120
|
+
if a.cmd == "schema":
|
|
121
|
+
print(json.dumps(schema.load(a.name), indent=2))
|
|
122
|
+
return 0
|
|
123
|
+
return 2
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
sys.exit(main())
|