tg-ringer 0.1.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.
@@ -0,0 +1,65 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["**"]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: ci-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ lint:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: "3.12"
21
+ - name: Install ruff
22
+ run: pip install ruff
23
+ - name: Ruff lint
24
+ run: ruff check .
25
+ - name: Ruff format check
26
+ run: ruff format --check tg_caller tests
27
+
28
+ test:
29
+ runs-on: ubuntu-latest
30
+ strategy:
31
+ fail-fast: false
32
+ matrix:
33
+ python-version: ["3.9", "3.10", "3.11", "3.12"]
34
+ steps:
35
+ - uses: actions/checkout@v4
36
+ - uses: actions/setup-python@v5
37
+ with:
38
+ python-version: ${{ matrix.python-version }}
39
+ - name: Install package + test deps
40
+ run: pip install -e ".[dev]"
41
+ - name: Run tests
42
+ run: pytest -q
43
+ - name: Import smoke check
44
+ run: python -c "import tg_caller; print(tg_caller.__version__)"
45
+ - name: CLI smoke check
46
+ run: tg-caller --help
47
+
48
+ build:
49
+ runs-on: ubuntu-latest
50
+ needs: [lint, test]
51
+ steps:
52
+ - uses: actions/checkout@v4
53
+ - uses: actions/setup-python@v5
54
+ with:
55
+ python-version: "3.12"
56
+ - name: Build + validate distribution
57
+ run: |
58
+ pip install build twine
59
+ python -m build
60
+ twine check dist/*
61
+ - name: Upload artifacts
62
+ uses: actions/upload-artifact@v4
63
+ with:
64
+ name: dist
65
+ path: dist/*
@@ -0,0 +1,21 @@
1
+ # SECRETS — never commit
2
+ *.session
3
+ *.session-journal
4
+ .login_state.json
5
+ env
6
+ .env
7
+ config
8
+ *.token
9
+
10
+ # python
11
+ .venv/
12
+ venv/
13
+ __pycache__/
14
+ *.pyc
15
+ *.egg-info/
16
+ build/
17
+ dist/
18
+ .eggs/
19
+
20
+ # os
21
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jdp5949
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.
@@ -0,0 +1,201 @@
1
+ Metadata-Version: 2.4
2
+ Name: tg-ringer
3
+ Version: 0.1.0
4
+ Summary: Ring (call) and message any Telegram user from your own account — urgent alerts via a real Telegram call.
5
+ Project-URL: Homepage, https://github.com/jdp5949/tg-ringer
6
+ Project-URL: Documentation, https://jdp5949.github.io/tg-ringer/
7
+ Project-URL: Repository, https://github.com/jdp5949/tg-ringer
8
+ Project-URL: Issues, https://github.com/jdp5949/tg-ringer/issues
9
+ Author: jdp5949
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: alert,call,mtproto,notification,telegram,telethon,userbot
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: Communications :: Chat
19
+ Classifier: Topic :: Communications :: Telephony
20
+ Requires-Python: >=3.9
21
+ Requires-Dist: telethon<2,>=1.36
22
+ Provides-Extra: dev
23
+ Requires-Dist: build; extra == 'dev'
24
+ Requires-Dist: pytest; extra == 'dev'
25
+ Requires-Dist: ruff; extra == 'dev'
26
+ Requires-Dist: twine; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # tg-ringer
30
+
31
+ Ring (call) and message **any Telegram user from your own account** — a lightweight
32
+ [Telethon](https://github.com/LonamiWebs/Telethon) userbot for **urgent alerts**.
33
+
34
+ It places a real **private Telegram call** so the target's phone *rings* (no audio
35
+ is streamed — the ring itself is the alert), then hangs up. It can also send direct
36
+ account-to-account messages.
37
+
38
+ > **This is a userbot (your real account), not a bot.** That is the point — bots
39
+ > cannot place calls. See [⚠️ ToS & bans](#️-tos--bans) before using.
40
+
41
+ ---
42
+
43
+ ## When to use it
44
+
45
+ | You want… | Use this? |
46
+ |-----------|-----------|
47
+ | Phone to **ring** on a critical event (build failed, server down, prod alert) | ✅ yes |
48
+ | A free alternative to paid call APIs, and you already live in Telegram | ✅ yes |
49
+ | Account-to-account DM from a script (faster than Bot API on a warm connection) | ✅ yes |
50
+ | Spoken/TTS audio in the call | ❌ no — ring only (see [limitations](#limitations)) |
51
+ | Reach someone with **no internet** (real cellular call) | ❌ no — Telegram is VoIP; use Twilio/PSTN |
52
+ | Mass messaging / spam | ❌ absolutely not — instant ban |
53
+
54
+ ---
55
+
56
+ ## Install
57
+
58
+ ```bash
59
+ pip install tg-ringer
60
+ ```
61
+
62
+ Requires Python 3.9+.
63
+
64
+ ---
65
+
66
+ ## Setup (one time)
67
+
68
+ 1. **Get API credentials** at <https://my.telegram.org> → *API development tools* →
69
+ create an app. Copy the **`api_id`** (number) and **`api_hash`** (string).
70
+
71
+ 2. **Configure.** Either export env vars or write a config file:
72
+
73
+ ```bash
74
+ mkdir -p ~/.config/tg-ringer
75
+ cat > ~/.config/tg-ringer/config <<'EOF'
76
+ TG_API_ID=1234567
77
+ TG_API_HASH=0123456789abcdef0123456789abcdef
78
+ TG_TARGET=+15551234567 # optional default target
79
+ RING_SECONDS=20 # optional
80
+ EOF
81
+ ```
82
+
83
+ 3. **Log in** (interactive — sends a code to your Telegram app):
84
+
85
+ ```bash
86
+ tg-ringer login
87
+ ```
88
+
89
+ Enter the **userbot account's** phone number, then the login code (delivered
90
+ *inside Telegram*, not SMS), and a 2FA password if you have one. This creates a
91
+ session file so future calls run unattended.
92
+
93
+ > Use a **separate account** as the userbot — not the one you want to ring. You
94
+ > cannot call yourself.
95
+
96
+ ---
97
+
98
+ ## CLI usage
99
+
100
+ ```bash
101
+ # Ring a number (or @username, or numeric id) — phone rings, then hangs up
102
+ tg-ringer call +15551234567
103
+ tg-ringer call @someuser --seconds 30
104
+ tg-ringer call # uses TG_TARGET
105
+
106
+ # Send a direct message
107
+ tg-ringer msg +15551234567 "deploy finished"
108
+ echo "piped body" | tg-ringer msg @someuser
109
+
110
+ # Who am I logged in as?
111
+ tg-ringer whoami
112
+ ```
113
+
114
+ ### In scripts
115
+
116
+ ```bash
117
+ long_task && tg-ringer msg "$ALERT" "✅ done" || tg-ringer call "$ALERT"
118
+ ```
119
+
120
+ ---
121
+
122
+ ## Library usage
123
+
124
+ ```python
125
+ import asyncio
126
+ from tg_ringer import TgCaller
127
+
128
+ async def main():
129
+ async with TgCaller(api_id=1234567, api_hash="...", session="userbot") as tg:
130
+ await tg.ring("+15551234567", seconds=20) # phone rings 20s
131
+ await tg.message("+15551234567", "heads up") # direct message
132
+
133
+ asyncio.run(main())
134
+ ```
135
+
136
+ `TgCaller` methods (all async):
137
+
138
+ | Method | Does |
139
+ |--------|------|
140
+ | `ring(target, seconds=20)` | Place a private call; phone rings then hangs up. Returns call id. |
141
+ | `message(target, text)` | Send a direct message. Returns message id. |
142
+ | `resolve(target)` | Resolve a `@username`, numeric id, or `+phone` to an entity. |
143
+ | `whoami()` | Return the logged-in account. |
144
+
145
+ `target` may be a `@username`, a numeric user id, or a `+E164` phone number. A
146
+ phone number is imported as a temporary contact so it can be reached.
147
+
148
+ ---
149
+
150
+ ## Limitations
151
+
152
+ - **Ring only, no audio.** Playing TTS/sound needs the full encrypted call to
153
+ connect (WebRTC/Opus). `pytgcalls` covers *group* voice chats, not private 1-to-1
154
+ calls; private-call audio needs the old `libtgvoip` stack (fragile). For a spoken
155
+ message, use a PSTN provider (e.g. Twilio).
156
+ - **Internet required on the receiver.** Telegram calls are VoIP.
157
+ - **Calls only land if Telegram lets them.** New accounts, and especially **VoIP
158
+ numbers**, hit anti-spam (`PeerFloodError`). Best results when caller and target
159
+ are **mutual contacts**.
160
+
161
+ ---
162
+
163
+ ## ⚠️ ToS & bans
164
+
165
+ Automating a **user** account (userbot) is a **gray area** under Telegram's Terms of
166
+ Service. Risks you accept by using this:
167
+
168
+ - Accounts can be **limited or banned**, especially VoIP numbers, new accounts, or
169
+ any account making automated calls/messages to non-contacts.
170
+ - Keep volume low. Make the caller and target **mutual contacts**. Do **not** spam.
171
+ - Use a throwaway/secondary account as the userbot.
172
+
173
+ You are responsible for how you use this. See `@SpamBot` in Telegram to check an
174
+ account's restriction status.
175
+
176
+ ---
177
+
178
+ ## Security
179
+
180
+ - Your `api_hash` and the `*.session` file grant **full access to the userbot
181
+ account**. Never commit or share them. The config and session live under
182
+ `~/.config/tg-ringer/` and are git-ignored in this repo.
183
+ - Revoke a leaked session from any Telegram client: *Settings → Devices → Terminate*.
184
+
185
+ ---
186
+
187
+ ## Troubleshooting
188
+
189
+ | Symptom | Fix |
190
+ |---------|-----|
191
+ | `PeerFloodError` | Account anti-spam limited. Make caller+target mutual contacts; check `@SpamBot`; wait; or use a non-VoIP number. |
192
+ | `session not authorized` | Run `tg-ringer login`. |
193
+ | Login code never arrives | It's delivered **in the Telegram app** ("Telegram" service chat), not SMS. The userbot number must be logged into a Telegram client. |
194
+ | Target not on Telegram | `+phone` must belong to a Telegram account. |
195
+ | No notification but message sent | Receiver chat is muted / OS notifications off. |
196
+
197
+ ---
198
+
199
+ ## License
200
+
201
+ MIT © jdp5949
@@ -0,0 +1,173 @@
1
+ # tg-ringer
2
+
3
+ Ring (call) and message **any Telegram user from your own account** — a lightweight
4
+ [Telethon](https://github.com/LonamiWebs/Telethon) userbot for **urgent alerts**.
5
+
6
+ It places a real **private Telegram call** so the target's phone *rings* (no audio
7
+ is streamed — the ring itself is the alert), then hangs up. It can also send direct
8
+ account-to-account messages.
9
+
10
+ > **This is a userbot (your real account), not a bot.** That is the point — bots
11
+ > cannot place calls. See [⚠️ ToS & bans](#️-tos--bans) before using.
12
+
13
+ ---
14
+
15
+ ## When to use it
16
+
17
+ | You want… | Use this? |
18
+ |-----------|-----------|
19
+ | Phone to **ring** on a critical event (build failed, server down, prod alert) | ✅ yes |
20
+ | A free alternative to paid call APIs, and you already live in Telegram | ✅ yes |
21
+ | Account-to-account DM from a script (faster than Bot API on a warm connection) | ✅ yes |
22
+ | Spoken/TTS audio in the call | ❌ no — ring only (see [limitations](#limitations)) |
23
+ | Reach someone with **no internet** (real cellular call) | ❌ no — Telegram is VoIP; use Twilio/PSTN |
24
+ | Mass messaging / spam | ❌ absolutely not — instant ban |
25
+
26
+ ---
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install tg-ringer
32
+ ```
33
+
34
+ Requires Python 3.9+.
35
+
36
+ ---
37
+
38
+ ## Setup (one time)
39
+
40
+ 1. **Get API credentials** at <https://my.telegram.org> → *API development tools* →
41
+ create an app. Copy the **`api_id`** (number) and **`api_hash`** (string).
42
+
43
+ 2. **Configure.** Either export env vars or write a config file:
44
+
45
+ ```bash
46
+ mkdir -p ~/.config/tg-ringer
47
+ cat > ~/.config/tg-ringer/config <<'EOF'
48
+ TG_API_ID=1234567
49
+ TG_API_HASH=0123456789abcdef0123456789abcdef
50
+ TG_TARGET=+15551234567 # optional default target
51
+ RING_SECONDS=20 # optional
52
+ EOF
53
+ ```
54
+
55
+ 3. **Log in** (interactive — sends a code to your Telegram app):
56
+
57
+ ```bash
58
+ tg-ringer login
59
+ ```
60
+
61
+ Enter the **userbot account's** phone number, then the login code (delivered
62
+ *inside Telegram*, not SMS), and a 2FA password if you have one. This creates a
63
+ session file so future calls run unattended.
64
+
65
+ > Use a **separate account** as the userbot — not the one you want to ring. You
66
+ > cannot call yourself.
67
+
68
+ ---
69
+
70
+ ## CLI usage
71
+
72
+ ```bash
73
+ # Ring a number (or @username, or numeric id) — phone rings, then hangs up
74
+ tg-ringer call +15551234567
75
+ tg-ringer call @someuser --seconds 30
76
+ tg-ringer call # uses TG_TARGET
77
+
78
+ # Send a direct message
79
+ tg-ringer msg +15551234567 "deploy finished"
80
+ echo "piped body" | tg-ringer msg @someuser
81
+
82
+ # Who am I logged in as?
83
+ tg-ringer whoami
84
+ ```
85
+
86
+ ### In scripts
87
+
88
+ ```bash
89
+ long_task && tg-ringer msg "$ALERT" "✅ done" || tg-ringer call "$ALERT"
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Library usage
95
+
96
+ ```python
97
+ import asyncio
98
+ from tg_ringer import TgCaller
99
+
100
+ async def main():
101
+ async with TgCaller(api_id=1234567, api_hash="...", session="userbot") as tg:
102
+ await tg.ring("+15551234567", seconds=20) # phone rings 20s
103
+ await tg.message("+15551234567", "heads up") # direct message
104
+
105
+ asyncio.run(main())
106
+ ```
107
+
108
+ `TgCaller` methods (all async):
109
+
110
+ | Method | Does |
111
+ |--------|------|
112
+ | `ring(target, seconds=20)` | Place a private call; phone rings then hangs up. Returns call id. |
113
+ | `message(target, text)` | Send a direct message. Returns message id. |
114
+ | `resolve(target)` | Resolve a `@username`, numeric id, or `+phone` to an entity. |
115
+ | `whoami()` | Return the logged-in account. |
116
+
117
+ `target` may be a `@username`, a numeric user id, or a `+E164` phone number. A
118
+ phone number is imported as a temporary contact so it can be reached.
119
+
120
+ ---
121
+
122
+ ## Limitations
123
+
124
+ - **Ring only, no audio.** Playing TTS/sound needs the full encrypted call to
125
+ connect (WebRTC/Opus). `pytgcalls` covers *group* voice chats, not private 1-to-1
126
+ calls; private-call audio needs the old `libtgvoip` stack (fragile). For a spoken
127
+ message, use a PSTN provider (e.g. Twilio).
128
+ - **Internet required on the receiver.** Telegram calls are VoIP.
129
+ - **Calls only land if Telegram lets them.** New accounts, and especially **VoIP
130
+ numbers**, hit anti-spam (`PeerFloodError`). Best results when caller and target
131
+ are **mutual contacts**.
132
+
133
+ ---
134
+
135
+ ## ⚠️ ToS & bans
136
+
137
+ Automating a **user** account (userbot) is a **gray area** under Telegram's Terms of
138
+ Service. Risks you accept by using this:
139
+
140
+ - Accounts can be **limited or banned**, especially VoIP numbers, new accounts, or
141
+ any account making automated calls/messages to non-contacts.
142
+ - Keep volume low. Make the caller and target **mutual contacts**. Do **not** spam.
143
+ - Use a throwaway/secondary account as the userbot.
144
+
145
+ You are responsible for how you use this. See `@SpamBot` in Telegram to check an
146
+ account's restriction status.
147
+
148
+ ---
149
+
150
+ ## Security
151
+
152
+ - Your `api_hash` and the `*.session` file grant **full access to the userbot
153
+ account**. Never commit or share them. The config and session live under
154
+ `~/.config/tg-ringer/` and are git-ignored in this repo.
155
+ - Revoke a leaked session from any Telegram client: *Settings → Devices → Terminate*.
156
+
157
+ ---
158
+
159
+ ## Troubleshooting
160
+
161
+ | Symptom | Fix |
162
+ |---------|-----|
163
+ | `PeerFloodError` | Account anti-spam limited. Make caller+target mutual contacts; check `@SpamBot`; wait; or use a non-VoIP number. |
164
+ | `session not authorized` | Run `tg-ringer login`. |
165
+ | Login code never arrives | It's delivered **in the Telegram app** ("Telegram" service chat), not SMS. The userbot number must be logged into a Telegram client. |
166
+ | Target not on Telegram | `+phone` must belong to a Telegram account. |
167
+ | No notification but message sent | Receiver chat is muted / OS notifications off. |
168
+
169
+ ---
170
+
171
+ ## License
172
+
173
+ MIT © jdp5949
@@ -0,0 +1,10 @@
1
+ # Copy to ~/.config/tg-ringer/config and fill in.
2
+ # Get TG_API_ID / TG_API_HASH at https://my.telegram.org (API development tools).
3
+
4
+ TG_API_ID=1234567
5
+ TG_API_HASH=0123456789abcdef0123456789abcdef
6
+
7
+ # Optional:
8
+ # TG_SESSION=/Users/you/.config/tg-ringer/userbot # session file path (no .session)
9
+ # TG_TARGET=+15551234567 # default target
10
+ # RING_SECONDS=20 # default ring duration
@@ -0,0 +1,4 @@
1
+ title: tg-ringer
2
+ description: Ring and message any Telegram user from your own account — urgent alerts via a real Telegram call.
3
+ theme: jekyll-theme-cayman
4
+ show_downloads: false
@@ -0,0 +1,99 @@
1
+ # tg-ringer
2
+
3
+ **Ring (call) and message any Telegram user from your own account.**
4
+ A tiny [Telethon](https://github.com/LonamiWebs/Telethon) userbot for **urgent alerts** —
5
+ it places a real private Telegram call so your phone *rings*, then hangs up.
6
+
7
+ [⭐ GitHub repo](https://github.com/jdp5949/tg-ringer) ·
8
+ [📦 PyPI](https://pypi.org/project/tg-ringer/)
9
+
10
+ ---
11
+
12
+ ## Why
13
+
14
+ Telegram **bots cannot place calls**. A *userbot* (your own account, via MTProto)
15
+ can. When a build fails or prod goes down at 3am, a silent push is easy to miss — a
16
+ **ringing phone** is not. `tg-ringer` turns any script event into a phone ring, for
17
+ free, using Telegram you already have.
18
+
19
+ ---
20
+
21
+ ## When to use it
22
+
23
+ ✅ Phone should **ring** on a critical event (CI failure, server down, prod alert)
24
+ ✅ Free alternative to paid call APIs, if you already use Telegram
25
+ ✅ Account-to-account DMs from scripts
26
+
27
+ ❌ Spoken/TTS audio in the call — *ring only* (use Twilio for voice)
28
+ ❌ Reaching someone with **no internet** — Telegram is VoIP (use PSTN)
29
+ ❌ Mass messaging / spam — instant ban
30
+
31
+ ---
32
+
33
+ ## Quick start
34
+
35
+ ```bash
36
+ pip install tg-ringer
37
+ ```
38
+
39
+ 1. Get `api_id` / `api_hash` at <https://my.telegram.org>.
40
+ 2. Put them in `~/.config/tg-ringer/config`:
41
+
42
+ ```ini
43
+ TG_API_ID=1234567
44
+ TG_API_HASH=0123456789abcdef0123456789abcdef
45
+ TG_TARGET=+15551234567
46
+ ```
47
+
48
+ 3. Log in once (use a **separate** account as the userbot — you can't call yourself):
49
+
50
+ ```bash
51
+ tg-ringer login
52
+ ```
53
+
54
+ 4. Ring it:
55
+
56
+ ```bash
57
+ tg-ringer call +15551234567
58
+ tg-ringer msg +15551234567 "deploy done"
59
+ ```
60
+
61
+ ---
62
+
63
+ ## In scripts
64
+
65
+ ```bash
66
+ long_task && tg-ringer msg "$ALERT" "✅ done" || tg-ringer call "$ALERT"
67
+ ```
68
+
69
+ ## In Python
70
+
71
+ ```python
72
+ import asyncio
73
+ from tg_ringer import TgCaller
74
+
75
+ async def main():
76
+ async with TgCaller(api_id, api_hash, "userbot") as tg:
77
+ await tg.ring("+15551234567", seconds=20)
78
+ await tg.message("+15551234567", "heads up")
79
+
80
+ asyncio.run(main())
81
+ ```
82
+
83
+ ---
84
+
85
+ ## ⚠️ Heads up
86
+
87
+ - **ToS gray area.** Userbots can be **limited or banned** — especially VoIP numbers
88
+ and new accounts placing automated calls. Keep volume low, make caller + target
89
+ **mutual contacts**, use a throwaway account, never spam.
90
+ - **Ring only** — no audio. Receiver needs internet (Telegram is VoIP).
91
+ - `PeerFloodError`? Anti-spam limit. Mutual contacts + `@SpamBot` check + patience,
92
+ or use a non-VoIP number.
93
+
94
+ Full docs & troubleshooting: see the
95
+ [README](https://github.com/jdp5949/tg-ringer#readme).
96
+
97
+ ---
98
+
99
+ <sub>MIT © jdp5949 · This is an independent project, not affiliated with Telegram.</sub>
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "tg-ringer"
7
+ version = "0.1.0"
8
+ description = "Ring (call) and message any Telegram user from your own account — urgent alerts via a real Telegram call."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "jdp5949" }]
13
+ keywords = ["telegram", "call", "alert", "notification", "userbot", "mtproto", "telethon"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Communications :: Telephony",
21
+ "Topic :: Communications :: Chat",
22
+ ]
23
+ dependencies = ["telethon>=1.36,<2"]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/jdp5949/tg-ringer"
27
+ Documentation = "https://jdp5949.github.io/tg-ringer/"
28
+ Repository = "https://github.com/jdp5949/tg-ringer"
29
+ Issues = "https://github.com/jdp5949/tg-ringer/issues"
30
+
31
+ [project.scripts]
32
+ tg-ringer = "tg_ringer.cli:main"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["tg_ringer"]
36
+
37
+ [project.optional-dependencies]
38
+ dev = ["ruff", "pytest", "build", "twine"]
39
+
40
+ [tool.ruff]
41
+ line-length = 88
42
+ target-version = "py39"
43
+
44
+ [tool.ruff.lint]
45
+ select = ["E", "F", "I", "UP", "B"]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
@@ -0,0 +1,41 @@
1
+ """Offline smoke tests — no network, no Telegram credentials required."""
2
+
3
+ import pytest
4
+
5
+ import tg_ringer
6
+ from tg_ringer import TgCaller, cli
7
+
8
+
9
+ def test_version():
10
+ assert isinstance(tg_ringer.__version__, str)
11
+ assert tg_ringer.__version__.count(".") >= 2
12
+
13
+
14
+ def test_exports_client():
15
+ assert TgCaller is tg_ringer.TgCaller
16
+
17
+
18
+ def test_client_constructs(tmp_path):
19
+ # Building the client must not connect or require valid creds.
20
+ tg = TgCaller(12345, "0" * 32, session=str(tmp_path / "s"))
21
+ assert tg.client is not None
22
+
23
+
24
+ def test_cli_help_exits_zero(capsys):
25
+ with pytest.raises(SystemExit) as exc:
26
+ cli.main(["--help"])
27
+ assert exc.value.code == 0
28
+ assert "tg-ringer" in capsys.readouterr().out
29
+
30
+
31
+ def test_cli_requires_subcommand():
32
+ with pytest.raises(SystemExit) as exc:
33
+ cli.main([])
34
+ assert exc.value.code != 0
35
+
36
+
37
+ @pytest.mark.parametrize("sub", ["login", "call", "msg", "whoami"])
38
+ def test_subcommand_help(sub, capsys):
39
+ with pytest.raises(SystemExit) as exc:
40
+ cli.main([sub, "--help"])
41
+ assert exc.value.code == 0
@@ -0,0 +1,23 @@
1
+ """tg-ringer — ring and message any Telegram user from your own account (userbot).
2
+
3
+ Account-to-account (MTProto), not a bot. The userbot places a real private
4
+ Telegram call so the target's phone *rings* (use as an urgent alert), or sends
5
+ a direct message.
6
+
7
+ Basic use:
8
+
9
+ import asyncio
10
+ from tg_ringer import TgCaller
11
+
12
+ async def main():
13
+ async with TgCaller(api_id, api_hash, "userbot") as tg:
14
+ await tg.ring("+15551234567", seconds=20)
15
+ await tg.message("+15551234567", "heads up")
16
+
17
+ asyncio.run(main())
18
+ """
19
+
20
+ from .client import TgCaller
21
+
22
+ __all__ = ["TgCaller"]
23
+ __version__ = "0.1.0"
@@ -0,0 +1,154 @@
1
+ """Command-line interface for tg-ringer.
2
+
3
+ Commands:
4
+ tg-ringer login one-time interactive login (phone + code)
5
+ tg-ringer call TARGET [-s N] ring a user/number for N seconds
6
+ tg-ringer msg TARGET TEXT send a direct message
7
+ tg-ringer whoami show the logged-in userbot account
8
+
9
+ Config (env vars, or ~/.config/tg-ringer/config as KEY=VALUE lines):
10
+ TG_API_ID required
11
+ TG_API_HASH required
12
+ TG_SESSION session file path (default ~/.config/tg-ringer/userbot)
13
+ TG_TARGET default target for call/msg when none is given
14
+ RING_SECONDS default ring duration (default 20)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import asyncio
21
+ import os
22
+ import sys
23
+ from pathlib import Path
24
+
25
+ CONFIG_DIR = Path(
26
+ os.environ.get("TG_RINGER_HOME", Path.home() / ".config" / "tg-ringer")
27
+ )
28
+ CONFIG_FILE = CONFIG_DIR / "config"
29
+
30
+
31
+ def _load_config() -> None:
32
+ """Load KEY=VALUE lines from the config file into os.environ (no override)."""
33
+ if not CONFIG_FILE.exists():
34
+ return
35
+ for line in CONFIG_FILE.read_text().splitlines():
36
+ line = line.strip()
37
+ if not line or line.startswith("#") or "=" not in line:
38
+ continue
39
+ key, _, val = line.partition("=")
40
+ os.environ.setdefault(key.strip(), val.strip().strip('"').strip("'"))
41
+
42
+
43
+ def _creds() -> tuple[int, str]:
44
+ _load_config()
45
+ try:
46
+ return int(os.environ["TG_API_ID"]), os.environ["TG_API_HASH"]
47
+ except KeyError:
48
+ sys.exit(
49
+ "missing TG_API_ID / TG_API_HASH (env or config file). "
50
+ "Get them at https://my.telegram.org"
51
+ )
52
+
53
+
54
+ def _session() -> str:
55
+ sess = os.environ.get("TG_SESSION")
56
+ if sess:
57
+ return sess
58
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
59
+ return str(CONFIG_DIR / "userbot")
60
+
61
+
62
+ def _target(arg: str | None) -> str:
63
+ t = arg or os.environ.get("TG_TARGET")
64
+ if not t:
65
+ sys.exit("no target: pass one or set TG_TARGET")
66
+ return t
67
+
68
+
69
+ def cmd_login(_args) -> None:
70
+ # Telethon's sync context manager runs the interactive login (phone, code,
71
+ # optional 2FA password) via stdin prompts.
72
+ from telethon.sync import TelegramClient
73
+
74
+ api_id, api_hash = _creds()
75
+ with TelegramClient(_session(), api_id, api_hash) as client:
76
+ me = client.get_me()
77
+ print(f"Logged in as {me.first_name} (id {me.id}, @{me.username})")
78
+ print(f"Session: {_session()}.session")
79
+
80
+
81
+ def _run(coro):
82
+ from .client import TgCaller
83
+
84
+ api_id, api_hash = _creds()
85
+
86
+ async def runner():
87
+ async with TgCaller(api_id, api_hash, _session()) as tg:
88
+ return await coro(tg)
89
+
90
+ return asyncio.run(runner())
91
+
92
+
93
+ def cmd_call(args) -> None:
94
+ target = _target(args.target)
95
+ seconds = args.seconds or int(os.environ.get("RING_SECONDS", "20"))
96
+
97
+ async def go(tg):
98
+ print(f"ringing {target} for {seconds}s ...")
99
+ cid = await tg.ring(target, seconds=seconds)
100
+ print(f"done (call id {cid})")
101
+
102
+ _run(go)
103
+
104
+
105
+ def cmd_msg(args) -> None:
106
+ target = _target(args.target)
107
+ text = " ".join(args.text) if args.text else sys.stdin.read()
108
+
109
+ async def go(tg):
110
+ mid = await tg.message(target, text)
111
+ print(f"sent (msg id {mid})")
112
+
113
+ _run(go)
114
+
115
+
116
+ def cmd_whoami(_args) -> None:
117
+ async def go(tg):
118
+ me = await tg.whoami()
119
+ print(f"{me.first_name} (id {me.id}, @{me.username})")
120
+
121
+ _run(go)
122
+
123
+
124
+ def main(argv=None) -> None:
125
+ p = argparse.ArgumentParser(
126
+ prog="tg-ringer",
127
+ description="Ring/message Telegram users from your own account.",
128
+ )
129
+ sub = p.add_subparsers(dest="cmd", required=True)
130
+
131
+ sub.add_parser("login", help="one-time interactive login").set_defaults(
132
+ func=cmd_login
133
+ )
134
+
135
+ pc = sub.add_parser("call", help="ring a user/number")
136
+ pc.add_argument("target", nargs="?", help="username, id, or +phone")
137
+ pc.add_argument("-s", "--seconds", type=int, help="ring duration")
138
+ pc.set_defaults(func=cmd_call)
139
+
140
+ pm = sub.add_parser("msg", help="send a direct message")
141
+ pm.add_argument("target", help="username, id, or +phone")
142
+ pm.add_argument("text", nargs="*", help="message text (or pipe via stdin)")
143
+ pm.set_defaults(func=cmd_msg)
144
+
145
+ sub.add_parser("whoami", help="show logged-in account").set_defaults(
146
+ func=cmd_whoami
147
+ )
148
+
149
+ args = p.parse_args(argv)
150
+ args.func(args)
151
+
152
+
153
+ if __name__ == "__main__":
154
+ main()
@@ -0,0 +1,117 @@
1
+ """Core async client: resolve targets, ring (private call), and message."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import hashlib
7
+ import secrets
8
+
9
+ from telethon import TelegramClient
10
+ from telethon.tl.functions.contacts import ImportContactsRequest
11
+ from telethon.tl.functions.messages import GetDhConfigRequest
12
+ from telethon.tl.functions.phone import DiscardCallRequest, RequestCallRequest
13
+ from telethon.tl.types import (
14
+ InputPhoneCall,
15
+ InputPhoneContact,
16
+ PhoneCallDiscardReasonHangup,
17
+ PhoneCallProtocol,
18
+ )
19
+
20
+
21
+ class TgCaller:
22
+ """Userbot wrapper around Telethon for ringing and messaging users.
23
+
24
+ Args:
25
+ api_id: Telegram API id (https://my.telegram.org).
26
+ api_hash: Telegram API hash.
27
+ session: Telethon session name or path (a ``.session`` file).
28
+ """
29
+
30
+ def __init__(self, api_id: int, api_hash: str, session: str = "tgcaller"):
31
+ self.client = TelegramClient(session, api_id, api_hash)
32
+
33
+ async def __aenter__(self) -> TgCaller:
34
+ await self.client.connect()
35
+ if not await self.client.is_user_authorized():
36
+ raise RuntimeError("session not authorized — run `tg-ringer login` first")
37
+ return self
38
+
39
+ async def __aexit__(self, *exc) -> None:
40
+ await self.client.disconnect()
41
+
42
+ async def resolve(self, target):
43
+ """Resolve a username, numeric id, or +phone number to an entity.
44
+
45
+ A ``+phone`` is imported as a temporary contact so it can be reached;
46
+ this is what lets you ring a number you have not chatted with before.
47
+ """
48
+ if isinstance(target, str) and target.startswith("+"):
49
+ res = await self.client(
50
+ ImportContactsRequest(
51
+ [
52
+ InputPhoneContact(
53
+ client_id=0,
54
+ phone=target,
55
+ first_name="alert",
56
+ last_name="target",
57
+ )
58
+ ]
59
+ )
60
+ )
61
+ if not res.users:
62
+ raise ValueError(f"{target} is not on Telegram / not resolvable")
63
+ return res.users[0]
64
+ return await self.client.get_input_entity(target)
65
+
66
+ async def ring(self, target, seconds: int = 20) -> int:
67
+ """Place a private call so ``target``'s phone rings, then hang up.
68
+
69
+ No audio is streamed — the *ring* is the alert. Returns the call id.
70
+ """
71
+ peer = await self.resolve(target)
72
+
73
+ dh = await self.client(GetDhConfigRequest(version=0, random_length=256))
74
+ p = int.from_bytes(dh.p, "big")
75
+ g = dh.g
76
+ a = int.from_bytes(secrets.token_bytes(256), "big") % p
77
+ g_a = pow(g, a, p)
78
+ g_a_hash = hashlib.sha256(g_a.to_bytes(256, "big")).digest()
79
+
80
+ protocol = PhoneCallProtocol(
81
+ min_layer=65,
82
+ max_layer=92,
83
+ udp_p2p=True,
84
+ udp_reflector=True,
85
+ library_versions=["4.0.0"],
86
+ )
87
+ res = await self.client(
88
+ RequestCallRequest(
89
+ user_id=peer,
90
+ random_id=secrets.randbelow(2**31),
91
+ g_a_hash=g_a_hash,
92
+ protocol=protocol,
93
+ )
94
+ )
95
+ call = res.phone_call
96
+ try:
97
+ await asyncio.sleep(seconds)
98
+ finally:
99
+ await self.client(
100
+ DiscardCallRequest(
101
+ peer=InputPhoneCall(id=call.id, access_hash=call.access_hash),
102
+ duration=0,
103
+ reason=PhoneCallDiscardReasonHangup(),
104
+ connection_id=0,
105
+ )
106
+ )
107
+ return call.id
108
+
109
+ async def message(self, target, text: str) -> int:
110
+ """Send a direct message to ``target``. Returns the message id."""
111
+ peer = await self.resolve(target)
112
+ msg = await self.client.send_message(peer, text)
113
+ return msg.id
114
+
115
+ async def whoami(self):
116
+ """Return the logged-in userbot account (Telethon User)."""
117
+ return await self.client.get_me()