cherry-shared2 0.1.26__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.
- cherry_shared/InfoService.py +369 -0
- cherry_shared/__init__.py +5 -0
- cherry_shared/blockchains.py +265 -0
- cherry_shared/bot_strings.py +529 -0
- cherry_shared/constants.py +36 -0
- cherry_shared/emojis.py +63 -0
- cherry_shared/functions.py +511 -0
- cherry_shared/launchpads.py +193 -0
- cherry_shared/types/__init__.py +6 -0
- cherry_shared/types/blockchain.py +51 -0
- cherry_shared/types/dexscreener.py +376 -0
- cherry_shared/types/launchpad.py +72 -0
- cherry_shared/types/leaderboardEntry.py +62 -0
- cherry_shared/types/raid_info.py +92 -0
- cherry_shared/types/user_wallet.py +9 -0
- cherry_shared2-0.1.26.dist-info/METADATA +91 -0
- cherry_shared2-0.1.26.dist-info/RECORD +19 -0
- cherry_shared2-0.1.26.dist-info/WHEEL +5 -0
- cherry_shared2-0.1.26.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import random
|
|
5
|
+
import re
|
|
6
|
+
import string
|
|
7
|
+
import time
|
|
8
|
+
from typing import Optional, Union
|
|
9
|
+
from pyrogram.types import Message, Photo, Video, Document, VideoNote, Animation, Audio
|
|
10
|
+
from itertools import zip_longest
|
|
11
|
+
from cherry_shared.emojis import Emojis
|
|
12
|
+
from web3 import Web3
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def extract_arguments(text: str) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Returns the argument after the command.
|
|
18
|
+
:param text: String to extract the arguments from a command
|
|
19
|
+
:return: the arguments if `text` is a command (according to is_command), else None.
|
|
20
|
+
"""
|
|
21
|
+
regexp = re.compile(r"/\w*(@\w*)*\s*([\s\S]*)", re.IGNORECASE)
|
|
22
|
+
result = regexp.match(text)
|
|
23
|
+
return result.group(2) if is_command(text) else None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def extract_entities(entities: list, start: int, len: int) -> list:
|
|
27
|
+
"""
|
|
28
|
+
Returns the entities of a Message Object.
|
|
29
|
+
:param entities: list of MessageEntity
|
|
30
|
+
:param start: start index of the reduced text
|
|
31
|
+
:param len: length of the reduced text
|
|
32
|
+
:return: list of new reduced MessageEntity
|
|
33
|
+
"""
|
|
34
|
+
if not entities:
|
|
35
|
+
return []
|
|
36
|
+
new_entities = []
|
|
37
|
+
for entity in entities:
|
|
38
|
+
if entity.offset < start:
|
|
39
|
+
continue
|
|
40
|
+
if entity.offset + entity.length > start + len:
|
|
41
|
+
continue
|
|
42
|
+
entity.offset -= start
|
|
43
|
+
new_entities.append(entity)
|
|
44
|
+
return new_entities
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_command(text: str) -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Checks if `text` is a command. Telegram chat commands start with the '/' character.
|
|
50
|
+
|
|
51
|
+
:param text: Text to check.
|
|
52
|
+
:return: True if `text` is a command, else False.
|
|
53
|
+
"""
|
|
54
|
+
if not text:
|
|
55
|
+
return False
|
|
56
|
+
return text.startswith("/")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
url_pattern = re.compile(
|
|
60
|
+
r"(?<!\S)(?:https?://)" # Scheme with negative lookbehind
|
|
61
|
+
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # Domain
|
|
62
|
+
r"localhost|" # Localhost
|
|
63
|
+
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # IP address
|
|
64
|
+
r"(?::\d+)?" # Port (optional)
|
|
65
|
+
r"(?:/?|[/?]\S*?)(?=[\s\]\"']|$)", # Path (non-greedy) with lookahead for space, closing bracket, quote, or end of string
|
|
66
|
+
re.IGNORECASE,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_url_from_text(text: str):
|
|
71
|
+
try:
|
|
72
|
+
matches = url_pattern.findall(text)
|
|
73
|
+
if matches:
|
|
74
|
+
return matches[0]
|
|
75
|
+
except Exception as e:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_file_id(message: Message):
|
|
80
|
+
"""Returns file_id of media message"""
|
|
81
|
+
media = get_media(message)
|
|
82
|
+
if media:
|
|
83
|
+
return media.file_id
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_media(
|
|
88
|
+
m: Message,
|
|
89
|
+
) -> Optional[Union[Photo, Video, Document, Audio, VideoNote, Animation]]:
|
|
90
|
+
"""Returns file_id of media message"""
|
|
91
|
+
if not m.media:
|
|
92
|
+
return None
|
|
93
|
+
media = str(m.media).lower().split(".")[1]
|
|
94
|
+
if media in ("photo", "video", "audio", "document", "video_note", "animation"):
|
|
95
|
+
return getattr(m, media, None)
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def smart_split(text: str, chars_per_string: int = 4096) -> list[str]:
|
|
100
|
+
r"""
|
|
101
|
+
Splits one string into multiple strings, with a maximum amount of `chars_per_string` characters per string.
|
|
102
|
+
This is very useful for splitting one giant message into multiples.
|
|
103
|
+
If `chars_per_string` > 4096: `chars_per_string` = 4096.
|
|
104
|
+
Splits by '\n', '. ' or ' ' in exactly this priority.
|
|
105
|
+
:param text: The text to split
|
|
106
|
+
:type text: :obj:`str`
|
|
107
|
+
:param chars_per_string: The number of maximum characters per part the text is split to.
|
|
108
|
+
:type chars_per_string: :obj:`int`
|
|
109
|
+
:return: The splitted text as a list of strings.
|
|
110
|
+
:rtype: :obj:`list` of :obj:`str`
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def _text_before_last(substr: str) -> str:
|
|
114
|
+
return substr.join(part.split(substr)[:-1]) + substr
|
|
115
|
+
|
|
116
|
+
if chars_per_string > chars_per_string:
|
|
117
|
+
chars_per_string = chars_per_string
|
|
118
|
+
|
|
119
|
+
parts = []
|
|
120
|
+
while True:
|
|
121
|
+
if len(text) < chars_per_string:
|
|
122
|
+
parts.append(text)
|
|
123
|
+
return parts
|
|
124
|
+
|
|
125
|
+
part = text[:chars_per_string]
|
|
126
|
+
|
|
127
|
+
if "\n" in part:
|
|
128
|
+
part = _text_before_last("\n")
|
|
129
|
+
elif ". " in part:
|
|
130
|
+
part = _text_before_last(". ")
|
|
131
|
+
elif " " in part:
|
|
132
|
+
part = _text_before_last(" ")
|
|
133
|
+
|
|
134
|
+
parts.append(part)
|
|
135
|
+
text = text[len(part) :]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class Address: # TODO: Must find address in text not match it
|
|
139
|
+
_base58_pattern = re.compile(r"^[1-9A-HJ-NP-Za-km-zA-L]{43,44}$")
|
|
140
|
+
_sei_pattern = re.compile(r"^sei1[a-z0-9]{58}$")
|
|
141
|
+
_evm_address_pattern = re.compile(r"0x[a-fA-F0-9]{40}")
|
|
142
|
+
_evm_txHash_pattern = re.compile(r"0x[a-fA-F0-9]{66}")
|
|
143
|
+
_trx_address_pattern = re.compile(r"T[1-9A-HJ-NP-Za-km-z]{33}")
|
|
144
|
+
_ton_address_pattern = re.compile(r"^[A-Za-z0-9_-]{48}$")
|
|
145
|
+
_sui_address_pattern = re.compile(r"^(0x[a-fA-F0-9]{64}|[A-Za-z0-9_-]{48})$")
|
|
146
|
+
_sui_address_module = re.compile(
|
|
147
|
+
r"^0x[a-fA-F0-9]{64}::[a-zA-Z_][a-zA-Z0-9_]*::[a-zA-Z_][a-zA-Z0-9_]*$"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def __init__(self, chain: str, address: str) -> None:
|
|
151
|
+
self.chain = chain
|
|
152
|
+
self.address = address
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def is_solana_address(cls, address: str):
|
|
156
|
+
return cls._base58_pattern.match(address)
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def is_sei_contract_address(cls, address: str):
|
|
160
|
+
return cls._sei_pattern.match(address)
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
def is_evm_address(cls, text: str):
|
|
164
|
+
return cls._evm_address_pattern.match(text)
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def is_tx_hash(cls, text: str):
|
|
168
|
+
return re.match(cls._evm_txHash_pattern, text)
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def is_trx_address(cls, text: str):
|
|
172
|
+
return cls._trx_address_pattern.match(text)
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def is_ton_address(cls, text: str):
|
|
176
|
+
return cls._ton_address_pattern.match(text)
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def is_sui_address(cls, text: str):
|
|
180
|
+
return cls._sui_address_pattern.match(text) or cls._sui_address_module.match(
|
|
181
|
+
text
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
def detect(cls, address: str):
|
|
186
|
+
if address.startswith("0x"):
|
|
187
|
+
sui = cls.is_sui_address(address)
|
|
188
|
+
if sui:
|
|
189
|
+
address = sui.group(0)
|
|
190
|
+
return cls("sui", address)
|
|
191
|
+
|
|
192
|
+
eth = cls.is_evm_address(address)
|
|
193
|
+
if eth:
|
|
194
|
+
address = eth.group(0)
|
|
195
|
+
checksum = Web3.to_checksum_address(address)
|
|
196
|
+
return cls("evm", checksum)
|
|
197
|
+
|
|
198
|
+
elif address.startswith("sei"):
|
|
199
|
+
sei = cls.is_sei_contract_address(address)
|
|
200
|
+
if sei:
|
|
201
|
+
return cls("sei-network", sei.group(0))
|
|
202
|
+
else:
|
|
203
|
+
sol = cls.is_solana_address(address)
|
|
204
|
+
if sol:
|
|
205
|
+
return cls("solana", sol.group(0))
|
|
206
|
+
|
|
207
|
+
trx = cls.is_trx_address(address)
|
|
208
|
+
if trx:
|
|
209
|
+
return cls("tron", trx.group(0))
|
|
210
|
+
|
|
211
|
+
def __str__(self):
|
|
212
|
+
return f"Address({self.address}, {self.chain})"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def generate_promo_code(user_id: int, code_length: int = 5):
|
|
216
|
+
byte_string = str(user_id).encode()
|
|
217
|
+
hash = hashlib.sha256(byte_string).hexdigest()
|
|
218
|
+
seed = int(hash[:8], 16)
|
|
219
|
+
random.seed(seed)
|
|
220
|
+
code_chars = string.ascii_uppercase + string.digits
|
|
221
|
+
promo_code = "".join(random.choice(code_chars) for _ in range(code_length))
|
|
222
|
+
return promo_code
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def generate_random_promo_code(code_length: int = 5):
|
|
226
|
+
"""
|
|
227
|
+
Generate a single random promo code.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
code_length (int, optional): The length of the promo code. Defaults to 8.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
str: A randomly generated promo code.
|
|
234
|
+
"""
|
|
235
|
+
code_chars = string.ascii_uppercase + string.digits
|
|
236
|
+
promo_code = "".join(random.choice(code_chars) for _ in range(code_length))
|
|
237
|
+
return promo_code
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def format_number(value, f_points=1):
|
|
241
|
+
if value < 1000:
|
|
242
|
+
return f"{value:.{f_points}f}"
|
|
243
|
+
elif value < 1000000:
|
|
244
|
+
return f"{value / 1000:.{f_points}f}K"
|
|
245
|
+
elif value < 1000000000:
|
|
246
|
+
return f"{value / 1000000:.{f_points}f}M"
|
|
247
|
+
else:
|
|
248
|
+
return f"{value / 1000000000:.{f_points}f}B"
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def time_difference(dt: datetime):
|
|
252
|
+
now_utc = datetime.utcnow()
|
|
253
|
+
diff = dt - now_utc
|
|
254
|
+
total_seconds = int(diff.total_seconds())
|
|
255
|
+
hours, remainder = divmod(total_seconds, 3600)
|
|
256
|
+
minutes, _ = divmod(remainder, 60)
|
|
257
|
+
|
|
258
|
+
if hours > 0:
|
|
259
|
+
return f"{hours} hours and {minutes} minutes"
|
|
260
|
+
else:
|
|
261
|
+
return f"{minutes} minutes"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def contains_text(text1: str, text2: str) -> bool:
|
|
265
|
+
"""
|
|
266
|
+
Check if text1 is present in text2, ignoring case.
|
|
267
|
+
|
|
268
|
+
:param text1: The first string to compare.
|
|
269
|
+
:param text2: The second string to compare.
|
|
270
|
+
:return: True if text1 is present in text2, False otherwise.
|
|
271
|
+
"""
|
|
272
|
+
pattern = re.compile(re.escape(text1), re.IGNORECASE)
|
|
273
|
+
return pattern.search(text2) is not None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def truncate_text(text: str, max_length: int) -> str:
|
|
277
|
+
if not text:
|
|
278
|
+
return None
|
|
279
|
+
if len(text) > max_length:
|
|
280
|
+
return text[: max_length - 2] + ".."
|
|
281
|
+
else:
|
|
282
|
+
return text
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def estimate_async_runtime(func):
|
|
286
|
+
async def wrapper(*args, **kwargs):
|
|
287
|
+
start_time = time.process_time_ns()
|
|
288
|
+
result = await func(*args, **kwargs)
|
|
289
|
+
end_time = time.process_time_ns()
|
|
290
|
+
runtime = (end_time - start_time) / 1_000_000
|
|
291
|
+
print(f"Estimated runtime of {func.__name__}: {runtime:.2f} ms")
|
|
292
|
+
return result
|
|
293
|
+
|
|
294
|
+
return wrapper
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def estimate_sync_runtime(func):
|
|
298
|
+
def wrapper(*args, **kwargs):
|
|
299
|
+
start_time = time.process_time_ns()
|
|
300
|
+
result = func(*args, **kwargs)
|
|
301
|
+
end_time = time.process_time_ns()
|
|
302
|
+
runtime = (end_time - start_time) / 1_000_000
|
|
303
|
+
print(f"Estimated runtime of {func.__name__}: {runtime:.2f} ms")
|
|
304
|
+
return result
|
|
305
|
+
|
|
306
|
+
return wrapper
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def create_progress_bar(current_progress, max_length=12):
|
|
310
|
+
filled_blocks = round(current_progress / 100 * max_length)
|
|
311
|
+
|
|
312
|
+
# Ensure the bar is not completely green unless the progress is 100%
|
|
313
|
+
if filled_blocks == max_length and current_progress < 100:
|
|
314
|
+
filled_blocks -= 1
|
|
315
|
+
|
|
316
|
+
empty_blocks = max_length - filled_blocks
|
|
317
|
+
progress_bar = "".join(["🟩"] * filled_blocks + ["⬛️"] * empty_blocks)
|
|
318
|
+
|
|
319
|
+
return progress_bar
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def animated_progress_bar(current_progress: int, max_length=10):
|
|
323
|
+
|
|
324
|
+
filled_blocks = round(current_progress / 100 * max_length)
|
|
325
|
+
|
|
326
|
+
# Ensure the bar is not completely green unless the progress is 100%
|
|
327
|
+
if filled_blocks == max_length and current_progress < 100:
|
|
328
|
+
filled_blocks -= 1
|
|
329
|
+
|
|
330
|
+
empty_blocks = max_length - filled_blocks
|
|
331
|
+
|
|
332
|
+
progress_bar = [Emojis.progress_bar.mid_full] * filled_blocks + [
|
|
333
|
+
Emojis.progress_bar.mid_empty
|
|
334
|
+
] * empty_blocks
|
|
335
|
+
|
|
336
|
+
# Replace the first block
|
|
337
|
+
if progress_bar[0] == Emojis.progress_bar.mid_full:
|
|
338
|
+
progress_bar[0] = Emojis.progress_bar.start_full
|
|
339
|
+
else:
|
|
340
|
+
progress_bar[0] = Emojis.progress_bar.start_empty
|
|
341
|
+
|
|
342
|
+
# Replace the last block
|
|
343
|
+
if progress_bar[-1] == Emojis.progress_bar.mid_full:
|
|
344
|
+
progress_bar[-1] = Emojis.progress_bar.end_full
|
|
345
|
+
else:
|
|
346
|
+
progress_bar[-1] = Emojis.progress_bar.end_empty
|
|
347
|
+
|
|
348
|
+
progress_bar = "".join(progress_bar)
|
|
349
|
+
return progress_bar
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def get_elapsed_time_str(start: float, show_seconds: bool = False) -> str:
|
|
353
|
+
"""
|
|
354
|
+
Get the elapsed time since a given start time.
|
|
355
|
+
|
|
356
|
+
Parameters:
|
|
357
|
+
- start (float): The start time, as returned by time.time().
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
- str: The elapsed time, in the format "Xh Ym Zs", but without hours and minutes if their value is 0.
|
|
361
|
+
"""
|
|
362
|
+
elapsed_seconds = int(time.time() - start)
|
|
363
|
+
if elapsed_seconds < 1:
|
|
364
|
+
return "a few moments"
|
|
365
|
+
hours, remainder = divmod(elapsed_seconds, 3600)
|
|
366
|
+
minutes, seconds = divmod(remainder, 60)
|
|
367
|
+
parts = []
|
|
368
|
+
if hours > 0:
|
|
369
|
+
parts.append(f"{hours}h")
|
|
370
|
+
if minutes > 0:
|
|
371
|
+
parts.append(f"{minutes}m")
|
|
372
|
+
if seconds > 0:
|
|
373
|
+
if show_seconds:
|
|
374
|
+
parts.append(f"{seconds}s")
|
|
375
|
+
elif not show_seconds and minutes + hours == 0:
|
|
376
|
+
parts.append("a few seconds")
|
|
377
|
+
return " ".join(parts)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def get_elapsed_time_full(
|
|
381
|
+
start: float = None, seconds: int = None, show_seconds: bool = False
|
|
382
|
+
) -> str:
|
|
383
|
+
"""
|
|
384
|
+
Get the elapsed time since a given start time, in full words.
|
|
385
|
+
|
|
386
|
+
Parameters:
|
|
387
|
+
- start (float): The start time, as returned by time.time()..
|
|
388
|
+
- seconds (int): estimated seconds.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
- str: The elapsed time, in the format "X hours Y minutes Z seconds", but without hours and minutes if their value is 0, and with singular/plural forms.
|
|
392
|
+
"""
|
|
393
|
+
elapsed_seconds = int(time.time() - start) if start else seconds
|
|
394
|
+
hours, remainder = divmod(elapsed_seconds, 3600)
|
|
395
|
+
minutes, reminded_seconds = divmod(remainder, 60)
|
|
396
|
+
parts = []
|
|
397
|
+
if hours > 0:
|
|
398
|
+
parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
|
|
399
|
+
if minutes > 0:
|
|
400
|
+
parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
|
|
401
|
+
if reminded_seconds > 0:
|
|
402
|
+
if show_seconds:
|
|
403
|
+
parts.append(
|
|
404
|
+
f"{reminded_seconds} second{'s' if reminded_seconds != 1 else ''}"
|
|
405
|
+
)
|
|
406
|
+
elif hours + minutes == 0:
|
|
407
|
+
parts.append(f"a few moments")
|
|
408
|
+
|
|
409
|
+
return " and ".join(parts)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def grouper(iterable, n, fillvalue=None):
|
|
413
|
+
"""Collect data into fixed-length chunks or blocks"""
|
|
414
|
+
args = [iter(iterable)] * n
|
|
415
|
+
return [list(filter(None, i)) for i in zip_longest(*args, fillvalue=fillvalue)]
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def get_str_time_full(seconds: float, show_seconds: bool = False) -> str:
|
|
419
|
+
try:
|
|
420
|
+
hours, remainder = divmod(seconds, 3600)
|
|
421
|
+
minutes, seconds = divmod(remainder, 60)
|
|
422
|
+
parts = []
|
|
423
|
+
if hours > 0:
|
|
424
|
+
parts.append(f"{int(hours)} Hour{'s' if int(hours) != 1 else ''}")
|
|
425
|
+
if minutes > 0:
|
|
426
|
+
parts.append(f"{int(minutes)} Minute{'s' if int(minutes) != 1 else ''}")
|
|
427
|
+
if seconds > 0:
|
|
428
|
+
parts.append(f"{int(seconds)} Second{'s' if int(seconds)!=1 else ''}")
|
|
429
|
+
if not parts:
|
|
430
|
+
return "0 seconds"
|
|
431
|
+
return " and ".join(parts)
|
|
432
|
+
except Exception as e:
|
|
433
|
+
print(f"get_str_time_full: {e}")
|
|
434
|
+
return "0 seconds"
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def get_str_time_full2(
|
|
438
|
+
start: float = None, seconds: int = None, show_seconds: bool = False
|
|
439
|
+
) -> str:
|
|
440
|
+
"""
|
|
441
|
+
Get the elapsed time since a given start time, in full words.
|
|
442
|
+
|
|
443
|
+
Parameters:
|
|
444
|
+
- start (float): The start time, as returned by time.time().
|
|
445
|
+
- seconds (int): estimated seconds.
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
- str: The elapsed time, in the format "X hours Y minutes Z seconds", but without hours and minutes if their value is 0, and with singular/plural forms.
|
|
449
|
+
"""
|
|
450
|
+
elapsed_seconds = int(time.time() - start) if start else seconds
|
|
451
|
+
hours, remainder = divmod(elapsed_seconds, 3600)
|
|
452
|
+
minutes, reminded_seconds = divmod(remainder, 60)
|
|
453
|
+
parts = []
|
|
454
|
+
if hours > 0:
|
|
455
|
+
parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
|
|
456
|
+
if minutes > 0:
|
|
457
|
+
parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
|
|
458
|
+
if reminded_seconds > 0:
|
|
459
|
+
if show_seconds:
|
|
460
|
+
parts.append(
|
|
461
|
+
f"{reminded_seconds} second{'s' if reminded_seconds != 1 else ''}"
|
|
462
|
+
)
|
|
463
|
+
elif hours + minutes == 0:
|
|
464
|
+
parts.append(f"a few moments")
|
|
465
|
+
|
|
466
|
+
return " and ".join(parts)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def time_ago_str(event_timestamp):
|
|
470
|
+
current_time = time.time()
|
|
471
|
+
time_difference = datetime.fromtimestamp(current_time) - datetime.fromtimestamp(
|
|
472
|
+
event_timestamp
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
days_ago = time_difference.days
|
|
476
|
+
seconds_ago = time_difference.seconds
|
|
477
|
+
|
|
478
|
+
if days_ago == 0:
|
|
479
|
+
hours_ago = seconds_ago // 3600
|
|
480
|
+
if hours_ago > 0:
|
|
481
|
+
return f"{hours_ago} hour{'s' if hours_ago > 1 else ''} ago"
|
|
482
|
+
|
|
483
|
+
minutes_ago = seconds_ago // 60
|
|
484
|
+
if minutes_ago > 0:
|
|
485
|
+
return f"{minutes_ago} minute{'s' if minutes_ago > 1 else ''} ago"
|
|
486
|
+
|
|
487
|
+
return "just now"
|
|
488
|
+
|
|
489
|
+
if days_ago < 7:
|
|
490
|
+
return f"{days_ago} day{'s' if days_ago > 1 else ''} ago"
|
|
491
|
+
|
|
492
|
+
weeks_ago = days_ago // 7
|
|
493
|
+
if days_ago < 30:
|
|
494
|
+
return f"{weeks_ago} week{'s' if weeks_ago > 1 else ''} ago"
|
|
495
|
+
|
|
496
|
+
months_ago = days_ago // 30
|
|
497
|
+
return f"{months_ago} month{'s' if months_ago > 1 else ''} ago"
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def seconds_until_next_minute_mod_5():
|
|
501
|
+
now = datetime.now()
|
|
502
|
+
minutes_to_add = (5 - now.minute % 5) % 5
|
|
503
|
+
next_time = (now + timedelta(minutes=minutes_to_add)).replace(
|
|
504
|
+
second=0, microsecond=0
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# If we're within a minute divisible by 5 but not at the exact start, move to the next 5-minute mark
|
|
508
|
+
if now.minute % 5 == 0 and now.second > 0:
|
|
509
|
+
next_time += timedelta(minutes=5)
|
|
510
|
+
|
|
511
|
+
return (next_time - now).total_seconds()
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import List, Union
|
|
3
|
+
from cherry_shared.blockchains import ChainType
|
|
4
|
+
from cherry_shared.constants import Constants
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LaunchPadId(Enum):
|
|
8
|
+
PINKSALE = "pinksale"
|
|
9
|
+
FJORD = "fjord"
|
|
10
|
+
SOLPAD = "solpad"
|
|
11
|
+
PUMPFUN = "pumpfun"
|
|
12
|
+
LetsBonk = "letsbonk"
|
|
13
|
+
MOONSHOT = "moonshot"
|
|
14
|
+
SUNPUMP = "sunpump"
|
|
15
|
+
FOURMEME = "fourmeme"
|
|
16
|
+
ETHERVISTA = "ethervista"
|
|
17
|
+
MOVEPUMP = "movepump"
|
|
18
|
+
BOOPFUN = "boopfun"
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def all():
|
|
22
|
+
return [
|
|
23
|
+
LaunchPadId.PUMPFUN,
|
|
24
|
+
# LaunchPadId.FJORD,
|
|
25
|
+
# LaunchPadId.SOLPAD,
|
|
26
|
+
# LaunchPadId.LetsBonk,
|
|
27
|
+
# LaunchPadId.MOONSHOT,
|
|
28
|
+
# LaunchPadId.SUNPUMP,
|
|
29
|
+
# LaunchPadId.FOURMEME,
|
|
30
|
+
# LaunchPadId.ETHERVISTA,
|
|
31
|
+
# LaunchPadId.MOVEPUMP,
|
|
32
|
+
# LaunchPadId.BOOPFUN,
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LaunchPad:
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
id: LaunchPadId,
|
|
40
|
+
group: int,
|
|
41
|
+
name: str,
|
|
42
|
+
website: str,
|
|
43
|
+
info_route: str,
|
|
44
|
+
supported_chains: List[ChainType],
|
|
45
|
+
trending_channel: str,
|
|
46
|
+
):
|
|
47
|
+
self.id = id
|
|
48
|
+
self.group = group
|
|
49
|
+
self.name = name
|
|
50
|
+
self.website = website
|
|
51
|
+
self.info_route = info_route
|
|
52
|
+
self.supported_chains = supported_chains
|
|
53
|
+
self.trending_channel = trending_channel
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class LaunchPads:
|
|
57
|
+
# GROUP 1
|
|
58
|
+
pumpfun = LaunchPad(
|
|
59
|
+
LaunchPadId.PUMPFUN,
|
|
60
|
+
1,
|
|
61
|
+
"Pump.fun",
|
|
62
|
+
"https://pump.fun",
|
|
63
|
+
"pumpfun",
|
|
64
|
+
[ChainType.SOLANA],
|
|
65
|
+
"https://t.me/pumpfuncherry",
|
|
66
|
+
)
|
|
67
|
+
letsbonk = LaunchPad(
|
|
68
|
+
LaunchPadId.LetsBonk,
|
|
69
|
+
1,
|
|
70
|
+
"LetsBonk",
|
|
71
|
+
"https://letsbonk.com",
|
|
72
|
+
"letsbonk",
|
|
73
|
+
[ChainType.SOLANA],
|
|
74
|
+
Constants.trending_channel_link,
|
|
75
|
+
)
|
|
76
|
+
moonshot = LaunchPad(
|
|
77
|
+
LaunchPadId.MOONSHOT,
|
|
78
|
+
1,
|
|
79
|
+
"MoonShot",
|
|
80
|
+
"https://dexscreener.com/moonshot",
|
|
81
|
+
"moonshot",
|
|
82
|
+
[ChainType.SOLANA],
|
|
83
|
+
"https://t.me/moonshotcherry",
|
|
84
|
+
)
|
|
85
|
+
sunpump = LaunchPad(
|
|
86
|
+
LaunchPadId.SUNPUMP,
|
|
87
|
+
1,
|
|
88
|
+
"SunPump",
|
|
89
|
+
"https://sunpump.meme",
|
|
90
|
+
"sunpump",
|
|
91
|
+
[ChainType.TRON],
|
|
92
|
+
"https://t.me/sunpumptrendingcherry",
|
|
93
|
+
)
|
|
94
|
+
fourmeme = LaunchPad(
|
|
95
|
+
LaunchPadId.FOURMEME,
|
|
96
|
+
1,
|
|
97
|
+
"Four.Meme",
|
|
98
|
+
"https://four.meme",
|
|
99
|
+
"fourmemes",
|
|
100
|
+
[ChainType.EVM],
|
|
101
|
+
Constants.trending_channel_link,
|
|
102
|
+
)
|
|
103
|
+
ethervista = LaunchPad(
|
|
104
|
+
LaunchPadId.ETHERVISTA,
|
|
105
|
+
1,
|
|
106
|
+
"Ether Vista",
|
|
107
|
+
"https://ethervista.app",
|
|
108
|
+
"ethervista",
|
|
109
|
+
[ChainType.EVM],
|
|
110
|
+
Constants.trending_channel_link,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
movepump = LaunchPad(
|
|
114
|
+
LaunchPadId.MOVEPUMP,
|
|
115
|
+
1,
|
|
116
|
+
"Move Pump",
|
|
117
|
+
"https://movepump.com",
|
|
118
|
+
"movepump",
|
|
119
|
+
[ChainType.SUI],
|
|
120
|
+
Constants.trending_channel_link,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
boopfun = LaunchPad(
|
|
124
|
+
LaunchPadId.BOOPFUN,
|
|
125
|
+
1,
|
|
126
|
+
"Boop.Fun",
|
|
127
|
+
"https://boop.fun",
|
|
128
|
+
"boopfun",
|
|
129
|
+
[ChainType.SOLANA],
|
|
130
|
+
Constants.trending_channel_link,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# GROUP 2
|
|
134
|
+
pinksale = LaunchPad(
|
|
135
|
+
LaunchPadId.PINKSALE,
|
|
136
|
+
2,
|
|
137
|
+
"PinkSale",
|
|
138
|
+
"https://www.pinksale.finance",
|
|
139
|
+
"pink",
|
|
140
|
+
[ChainType.EVM, ChainType.SOLANA],
|
|
141
|
+
Constants.trending_channel_link,
|
|
142
|
+
)
|
|
143
|
+
fjord = LaunchPad(
|
|
144
|
+
LaunchPadId.FJORD,
|
|
145
|
+
2,
|
|
146
|
+
"FJORD Sale",
|
|
147
|
+
"https://app.fjordfoundry.com",
|
|
148
|
+
"fjord",
|
|
149
|
+
[ChainType.EVM],
|
|
150
|
+
Constants.trending_channel_link,
|
|
151
|
+
)
|
|
152
|
+
solpad = LaunchPad(
|
|
153
|
+
LaunchPadId.SOLPAD,
|
|
154
|
+
2,
|
|
155
|
+
"SolPad",
|
|
156
|
+
"https://solpad.io",
|
|
157
|
+
"solpad",
|
|
158
|
+
[ChainType.SOLANA],
|
|
159
|
+
Constants.trending_channel_link,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def all() -> List[LaunchPad]:
|
|
164
|
+
return [
|
|
165
|
+
LaunchPads.pumpfun,
|
|
166
|
+
# LaunchPads.letsbonk,
|
|
167
|
+
# LaunchPads.moonshot,
|
|
168
|
+
# LaunchPads.sunpump,
|
|
169
|
+
# LaunchPads.fourmeme,
|
|
170
|
+
# LaunchPads.ethervista,
|
|
171
|
+
# LaunchPads.boopfun,
|
|
172
|
+
# LaunchPads.movepump,
|
|
173
|
+
# LaunchPads.pinksale,
|
|
174
|
+
# LaunchPads.fjord,
|
|
175
|
+
# LaunchPads.solpad,
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def get_by_id(id: Union[LaunchPadId | str]) -> LaunchPad:
|
|
180
|
+
if isinstance(id, str):
|
|
181
|
+
id = LaunchPadId(id)
|
|
182
|
+
|
|
183
|
+
for lp in LaunchPads.all():
|
|
184
|
+
if lp.id == id:
|
|
185
|
+
return lp
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def get_group(group_id: int):
|
|
189
|
+
return [lp for lp in LaunchPads.all() if lp.group == group_id]
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def get_by_chain_type(chainType: ChainType):
|
|
193
|
+
return [l for l in LaunchPads.all() if chainType in l.supported_chains]
|