tg-ringer 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -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"
tg_ringer/cli.py ADDED
@@ -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()
tg_ringer/client.py ADDED
@@ -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()
@@ -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,8 @@
1
+ tg_ringer/__init__.py,sha256=sStEUxNmsOR4VuIVKlzzqDUEYnVgEJDE7OR0GY2u1Hg,628
2
+ tg_ringer/cli.py,sha256=NR_ZE3bQxnolrJeCNz6rzfwBDveHOentf4I_KBhfy5c,4436
3
+ tg_ringer/client.py,sha256=8wlpGbvH83qXGwuQkEyBZH6TZ-r_owVnWNnr2Q_AIoM,4064
4
+ tg_ringer-0.1.0.dist-info/METADATA,sha256=jSB8_Az5DiStFxL9FG-bpMNR92k3RwOqsTOhKR5JDts,6710
5
+ tg_ringer-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
6
+ tg_ringer-0.1.0.dist-info/entry_points.txt,sha256=yCbHVDpsOcf-xYAnb0hEoxXDC7bzMQoE84PylJqQGME,49
7
+ tg_ringer-0.1.0.dist-info/licenses/LICENSE,sha256=1awIoOgnZD8e6oshrGpw5gImDnUr0uVgoZk4pKJCx2s,1064
8
+ tg_ringer-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tg-ringer = tg_ringer.cli:main
@@ -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.