tier2 1.0.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.
- tier2-1.0.0/LICENSE +21 -0
- tier2-1.0.0/PKG-INFO +24 -0
- tier2-1.0.0/README.md +6 -0
- tier2-1.0.0/pyproject.toml +3 -0
- tier2-1.0.0/setup.cfg +31 -0
- tier2-1.0.0/src/tier2/__init__.py +3 -0
- tier2-1.0.0/src/tier2/core.py +253 -0
- tier2-1.0.0/src/tier2.egg-info/PKG-INFO +24 -0
- tier2-1.0.0/src/tier2.egg-info/SOURCES.txt +11 -0
- tier2-1.0.0/src/tier2.egg-info/dependency_links.txt +1 -0
- tier2-1.0.0/src/tier2.egg-info/requires.txt +2 -0
- tier2-1.0.0/src/tier2.egg-info/top_level.txt +1 -0
tier2-1.0.0/LICENSE
ADDED
|
@@ -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.
|
tier2-1.0.0/PKG-INFO
ADDED
|
@@ -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
|
tier2-1.0.0/README.md
ADDED
tier2-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
name = tier2
|
|
3
|
+
version = 1.0.0
|
|
4
|
+
author = Asinerum Conlang Project
|
|
5
|
+
author_email = asinerum.com@gmail.com
|
|
6
|
+
description = Literp Tier Two Credential Lib
|
|
7
|
+
long_description = file: README.md
|
|
8
|
+
long_description_content_type = text/markdown
|
|
9
|
+
url = https://github.com/asinerum/tier2
|
|
10
|
+
license = MIT
|
|
11
|
+
classifiers =
|
|
12
|
+
Programming Language :: Python :: 3
|
|
13
|
+
License :: OSI Approved :: MIT License
|
|
14
|
+
Operating System :: OS Independent
|
|
15
|
+
|
|
16
|
+
[options]
|
|
17
|
+
package_dir =
|
|
18
|
+
= src
|
|
19
|
+
packages = find:
|
|
20
|
+
python_requires = >=3.7
|
|
21
|
+
install_requires =
|
|
22
|
+
pyjwt >= 2.10.1
|
|
23
|
+
requests >= 2.32.5
|
|
24
|
+
|
|
25
|
+
[options.packages.find]
|
|
26
|
+
where = src
|
|
27
|
+
|
|
28
|
+
[egg_info]
|
|
29
|
+
tag_build =
|
|
30
|
+
tag_date = 0
|
|
31
|
+
|
|
@@ -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,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.cfg
|
|
5
|
+
src/tier2/__init__.py
|
|
6
|
+
src/tier2/core.py
|
|
7
|
+
src/tier2.egg-info/PKG-INFO
|
|
8
|
+
src/tier2.egg-info/SOURCES.txt
|
|
9
|
+
src/tier2.egg-info/dependency_links.txt
|
|
10
|
+
src/tier2.egg-info/requires.txt
|
|
11
|
+
src/tier2.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tier2
|