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.
- tg_ringer-0.1.0/.github/workflows/ci.yml +65 -0
- tg_ringer-0.1.0/.gitignore +21 -0
- tg_ringer-0.1.0/LICENSE +21 -0
- tg_ringer-0.1.0/PKG-INFO +201 -0
- tg_ringer-0.1.0/README.md +173 -0
- tg_ringer-0.1.0/config.example +10 -0
- tg_ringer-0.1.0/docs/_config.yml +4 -0
- tg_ringer-0.1.0/docs/index.md +99 -0
- tg_ringer-0.1.0/pyproject.toml +48 -0
- tg_ringer-0.1.0/tests/test_smoke.py +41 -0
- tg_ringer-0.1.0/tg_ringer/__init__.py +23 -0
- tg_ringer-0.1.0/tg_ringer/cli.py +154 -0
- tg_ringer-0.1.0/tg_ringer/client.py +117 -0
|
@@ -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/*
|
tg_ringer-0.1.0/LICENSE
ADDED
|
@@ -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.
|
tg_ringer-0.1.0/PKG-INFO
ADDED
|
@@ -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,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()
|