tier2 1.0.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.
tier2/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .core import *
2
+
3
+ __version__ = "1.0.0"
tier2/core.py ADDED
@@ -0,0 +1,253 @@
1
+ """
2
+ ## core.py
3
+ Utility functions for:
4
+ 1. Generating random digit-only IDs stored in JWT tokens
5
+ 2. Verifying those IDs
6
+ 3. Sending messages via Telegram, Discord, WhatsApp
7
+ Dependencies:
8
+ pip install pyjwt requests
9
+ Environment variables required:
10
+ TELEGRAM_BOT_TOKEN: from @BotFather (e.g. "123456:ABC-DEF...")
11
+ DISCORD_BOT_TOKEN: from Discord Developer Portal (e.g. "MTA...")
12
+ WHATSAPP_API_TOKEN: from Meta Cloud API (e.g. "EAAx...")
13
+ WHATSAPP_PHONE_ID: Meta phone number ID for your WhatsApp sender
14
+ JWT_SECRET: any strong random string for signing JWT tokens
15
+ """
16
+
17
+ import os
18
+ import re
19
+ import random
20
+ import string
21
+ import logging
22
+ from datetime import datetime, timezone
23
+ from typing import Dict, Optional
24
+
25
+ import jwt
26
+ import requests
27
+
28
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s | %(message)s")
29
+ log = logging.getLogger(__name__)
30
+
31
+ JWT_SECRET = os.environ.get("JWT_SECRET", "change-me-to-a-strong-secret")
32
+ TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
33
+ DISCORD_BOT_TOKEN = os.environ.get("DISCORD_BOT_TOKEN", "")
34
+ WHATSAPP_API_TOKEN = os.environ.get("WHATSAPP_API_TOKEN", "")
35
+ WHATSAPP_PHONE_ID = os.environ.get("WHATSAPP_PHONE_ID", "")
36
+
37
+ _token_store: Dict[str, str] = {}
38
+
39
+ def create_random_id(
40
+ user_name: str,
41
+ id_length: int = 6,
42
+ id_timeout: int = 60,
43
+ ) -> str:
44
+ """
45
+ Generate a random digit-only ID of *id_length* characters, embed it
46
+ with *user_name* in a JWT that expires after *id_timeout* seconds,
47
+ store the token in memory, and return the plain ID string.
48
+ Parameters:
49
+ user_name : Unique identifier for the user (e.g. email, username).
50
+ id_length : Number of digits in the ID (default 6).
51
+ id_timeout : Token lifetime in seconds (default 60).
52
+ Returns:
53
+ str: the plain digit ID, e.g. "482931"
54
+ """
55
+ if id_length < 1:
56
+ raise ValueError("id_length must be at least 1")
57
+ if id_timeout < 1:
58
+ raise ValueError("id_timeout must be at least 1 second")
59
+ # Generate a random digit-only string
60
+ random_id = "".join(random.choices(string.digits, k=id_length))
61
+ # Build JWT payload
62
+ now = datetime.now(tz=timezone.utc)
63
+ payload = {
64
+ "sub": user_name, # subject
65
+ "id": random_id, # the OTP-style ID
66
+ "iat": now, # issued at
67
+ "exp": now.timestamp() + id_timeout, # expiry (Unix timestamp)
68
+ }
69
+ token = jwt.encode(payload, JWT_SECRET, algorithm="HS256")
70
+ # Store token keyed by user_name (overwrites any previous token)
71
+ _token_store[user_name] = token
72
+ log.info("Created ID for '%s' (expires in %ss): %s", user_name, id_timeout, random_id)
73
+ return random_id
74
+
75
+ def verify_given_id(user_name: str, id_string: str) -> bool:
76
+ """
77
+ Verify that *id_string* matches the active, non-expired token stored for *user_name*.
78
+ Parameters:
79
+ user_name : The user whose token to look up.
80
+ id_string : The digit ID to verify.
81
+ Returns:
82
+ True: token found, not expired, and ID matches.
83
+ False: token missing, expired, or ID mismatch.
84
+ """
85
+ token = _token_store.get(user_name)
86
+ if not token:
87
+ log.warning("verify_given_id: no token found for user '%s'", user_name)
88
+ return False
89
+ try:
90
+ payload = jwt.decode(
91
+ token,
92
+ JWT_SECRET,
93
+ algorithms=["HS256"],
94
+ options={"verify_exp": True},
95
+ )
96
+ except jwt.ExpiredSignatureError:
97
+ log.warning("verify_given_id: token expired for user '%s'", user_name)
98
+ _token_store.pop(user_name, None) # clean up
99
+ return False
100
+ except jwt.InvalidTokenError as exc:
101
+ log.warning("verify_given_id: invalid token for '%s': %s", user_name, exc)
102
+ return False
103
+ # Compare IDs (constant-time-ish via == on short strings)
104
+ if payload.get("id") != id_string:
105
+ log.warning("verify_given_id: ID mismatch for user '%s'", user_name)
106
+ return False
107
+ # Optionally consume token (one-time use): comment out to allow re-use
108
+ _token_store.pop(user_name, None)
109
+ log.info("verify_given_id: ID verified successfully for '%s'", user_name)
110
+ return True
111
+
112
+ def text_to_telegram(message: str, recipient: str) -> bool:
113
+ """
114
+ Send *message* to a Telegram user/chat.
115
+ Parameters:
116
+ message : Text to send.
117
+ recipient : Telegram chat_id (numeric string or @username).
118
+ Get a user's chat_id by messaging @userinfobot on Telegram.
119
+ Environment:
120
+ TELEGRAM_BOT_TOKEN: Bot token from @BotFather.
121
+ Returns:
122
+ True: on success
123
+ False: on failure
124
+ """
125
+ if not TELEGRAM_BOT_TOKEN:
126
+ log.error("text_to_telegram: TELEGRAM_BOT_TOKEN is not set")
127
+ return False
128
+ url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
129
+ payload = {
130
+ "chat_id": recipient,
131
+ "text": message,
132
+ "parse_mode": "HTML",
133
+ }
134
+ try:
135
+ resp = requests.post(url, json=payload, timeout=10)
136
+ data = resp.json()
137
+ if resp.ok and data.get("ok"):
138
+ log.info("text_to_telegram: message sent to %s", recipient)
139
+ return True
140
+ else:
141
+ log.error("text_to_telegram: API error: %s", data.get("description", resp.text))
142
+ return False
143
+ except requests.RequestException as exc:
144
+ log.error("text_to_telegram: request failed: %s", exc)
145
+ return False
146
+
147
+ def text_to_discord(message: str, recipient: str) -> bool:
148
+ """
149
+ Send *message* to a Discord user via DM.
150
+ Parameters:
151
+ message : Text to send.
152
+ recipient : Discord user ID (numeric string, e.g. "123456789012345678").
153
+ Enable Developer Mode in Discord → right-click user → Copy ID.
154
+ Environment:
155
+ DISCORD_BOT_TOKEN: Bot token from Discord Developer Portal.
156
+ The bot must share a server with the recipient, or the recipient must have DMs open.
157
+ Returns:
158
+ True: on success
159
+ False: on failure
160
+ """
161
+ if not DISCORD_BOT_TOKEN:
162
+ log.error("text_to_discord: DISCORD_BOT_TOKEN is not set")
163
+ return False
164
+ headers = {
165
+ "Authorization": f"Bot {DISCORD_BOT_TOKEN}",
166
+ "Content-Type": "application/json",
167
+ }
168
+ base = "https://discord.com/api/v10"
169
+ try:
170
+ # Step 1: open / retrieve DM channel
171
+ dm_resp = requests.post(
172
+ f"{base}/users/@me/channels",
173
+ headers=headers,
174
+ json={"recipient_id": recipient},
175
+ timeout=10,
176
+ )
177
+ if not dm_resp.ok:
178
+ log.error("text_to_discord: failed to open DM — %s", dm_resp.text)
179
+ return False
180
+ channel_id = dm_resp.json()["id"]
181
+ # Step 2: send message to DM channel
182
+ msg_resp = requests.post(
183
+ f"{base}/channels/{channel_id}/messages",
184
+ headers=headers,
185
+ json={"content": message},
186
+ timeout=10,
187
+ )
188
+ if msg_resp.ok:
189
+ log.info("text_to_discord: message sent to user %s", recipient)
190
+ return True
191
+ else:
192
+ log.error("text_to_discord: send failed — %s", msg_resp.text)
193
+ return False
194
+ except requests.RequestException as exc:
195
+ log.error("text_to_discord: request failed — %s", exc)
196
+ return False
197
+
198
+ def text_to_whatsapp(message: str, recipient: str) -> bool:
199
+ """
200
+ Send *message* to a WhatsApp number via Meta Cloud API.
201
+ Parameters:
202
+ message : Text to send.
203
+ recipient : Recipient's phone number in E.164 format, e.g. "84901234567" (country code + number, no + or spaces).
204
+ Environment:
205
+ WHATSAPP_API_TOKEN: Permanent or temporary token from Meta for Developers.
206
+ WHATSAPP_PHONE_ID: Phone Number ID of your WhatsApp sender (found in Meta → WhatsApp → API Setup).
207
+ Returns:
208
+ True: on success
209
+ False: on failure
210
+ Notes:
211
+ - The recipient must have previously messaged your WhatsApp number within
212
+ 24 hours (session window), OR you must use an approved Message Template.
213
+ - For OTP use-cases, use a pre-approved "otp" template instead of free text.
214
+ """
215
+ if not WHATSAPP_API_TOKEN:
216
+ log.error("text_to_whatsapp: WHATSAPP_API_TOKEN is not set")
217
+ return False
218
+ if not WHATSAPP_PHONE_ID:
219
+ log.error("text_to_whatsapp: WHATSAPP_PHONE_ID is not set")
220
+ return False
221
+ url = f"https://graph.facebook.com/v19.0/{WHATSAPP_PHONE_ID}/messages"
222
+ headers = {
223
+ "Authorization": f"Bearer {WHATSAPP_API_TOKEN}",
224
+ "Content-Type": "application/json",
225
+ }
226
+ payload = {
227
+ "messaging_product": "whatsapp",
228
+ "to": recipient,
229
+ "type": "text",
230
+ "text": {"body": message},
231
+ }
232
+ try:
233
+ resp = requests.post(url, headers=headers, json=payload, timeout=10)
234
+ data = resp.json()
235
+ if resp.ok and "messages" in data:
236
+ log.info("text_to_whatsapp: message sent to %s", recipient)
237
+ return True
238
+ else:
239
+ error = data.get("error", {}).get("message", resp.text)
240
+ log.error("text_to_whatsapp: API error — %s", error)
241
+ return False
242
+ except requests.RequestException as exc:
243
+ log.error("text_to_whatsapp: request failed — %s", exc)
244
+ return False
245
+
246
+ def get_active_token(user_name: str) -> Optional[str]:
247
+ ## Return the raw JWT for *user_name*, or None if not found
248
+ return _token_store.get(user_name)
249
+
250
+ def invalidate_token(user_name: str) -> None:
251
+ ## Manually invalidate/delete the token for *user_name*
252
+ _token_store.pop(user_name, None)
253
+ log.info("Token invalidated for '%s'", user_name)
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: tier2
3
+ Version: 1.0.0
4
+ Summary: Literp Tier Two Credential Lib
5
+ Home-page: https://github.com/asinerum/tier2
6
+ Author: Asinerum Conlang Project
7
+ Author-email: asinerum.com@gmail.com
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.7
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: pyjwt>=2.10.1
16
+ Requires-Dist: requests>=2.32.5
17
+ Dynamic: license-file
18
+
19
+ # LITERP TIER TWO CREDENTIAL LIBRARY
20
+
21
+ Detailed tips, tricks, and examples, can be found at project's repository
22
+ https://github.com/asinerum/tier2
23
+
24
+ (C) 2026 Asinerum Conlang Project
@@ -0,0 +1,7 @@
1
+ tier2/__init__.py,sha256=3lUTe5iXnYlQ4pT510T9Uu3NFzq5rTyiVw-GAC1x13E,46
2
+ tier2/core.py,sha256=Qt1CsZqGlRMJmYIHEDg-g2PzLgK2T6jmlpes_Bq9WDA,8840
3
+ tier2-1.0.0.dist-info/licenses/LICENSE,sha256=4npUbkrpgB6lqMiYYeUxZAP4SOkjVSwK8-7jW60mxvw,1081
4
+ tier2-1.0.0.dist-info/METADATA,sha256=pPk1YX3dnYXKJ81z5l6FqTk83cLHw_EywpFrZkQUP18,737
5
+ tier2-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ tier2-1.0.0.dist-info/top_level.txt,sha256=G8NGyFbIyrhyREYmzDHIs1E1jgDcpp_IGwraRh-6vqg,6
7
+ tier2-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Asinerum Conlang Project
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 @@
1
+ tier2