warp-beacon 2.3.14__py3-none-any.whl → 2.3.16__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.
@@ -1,2 +1,2 @@
1
- __version__ = "2.3.14"
1
+ __version__ = "2.3.16"
2
2
 
@@ -44,6 +44,7 @@ class JobSettings(TypedDict):
44
44
  job_postponed_until: int
45
45
  message_leftover: str
46
46
  replay: bool
47
+ short_text: bool
47
48
 
48
49
  class AbstractJob(ABC):
49
50
  job_id: uuid.UUID = None
@@ -80,6 +81,7 @@ class AbstractJob(ABC):
80
81
  job_postponed_until: int = -1
81
82
  message_leftover: str = ""
82
83
  replay: bool = False
84
+ short_text: bool = False
83
85
 
84
86
  def __init__(self, **kwargs: Unpack[JobSettings]) -> None:
85
87
  if kwargs:
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  import time
3
3
  from random import randrange
4
+ import datetime
4
5
  import threading
5
6
  import json
6
7
 
@@ -81,6 +82,12 @@ class IGScheduler(object):
81
82
  self.load_state()
82
83
  while self.running:
83
84
  try:
85
+ now = datetime.datetime.now()
86
+ if 4 <= now.hour < 7:
87
+ logging.info("Scheduler is paused due to night hours (4:00 - 7:00)")
88
+ self.event.wait(timeout=10800)
89
+ continue
90
+
84
91
  if self.state["remaining"] <= 0:
85
92
  self.state["remaining"] = randrange(8400, 26200)
86
93
  logging.info("Next scheduler activity in '%s' seconds", self.state["remaining"])
@@ -56,8 +56,15 @@ class AccountSelector(object):
56
56
  logging.info("Account proxy matched '%s'", proxy)
57
57
  matched_proxy.append(proxy)
58
58
  if matched_proxy:
59
- random.seed(random.seed(time.time_ns() ^ int.from_bytes(os.urandom(len(matched_proxy)), "big")))
59
+ if len(matched_proxy) > 1:
60
+ random.seed(random.seed(time.time_ns() ^ int.from_bytes(os.urandom(len(matched_proxy)), "big")))
61
+ # ensure new proxy in case if previous account required captcha
62
+ last_proxy = self.accounts.get("last_proxy", None)
63
+ if last_proxy and last_proxy in matched_proxy:
64
+ matched_proxy.remove(last_proxy)
60
65
  prox_choice = random.choice(matched_proxy)
66
+ # saving chosen proxy for history
67
+ self.accounts["last_proxy"] = prox_choice
61
68
  logging.info("Chosen proxy: '%s'", prox_choice)
62
69
  return prox_choice
63
70
  except Exception as e:
@@ -103,8 +110,8 @@ class AccountSelector(object):
103
110
  def bump_acc_fail(self, key: str, amount: int = 1) -> int:
104
111
  try:
105
112
  idx = self.account_index[self.current_module_name].value
106
- self.accounts_meta_data[idx][key] += amount
107
- return self.accounts_meta_data[idx][key]
113
+ self.accounts_meta_data[self.current_module_name][idx][key] += amount
114
+ return self.accounts_meta_data[self.current_module_name][idx][key]
108
115
  except Exception as e:
109
116
  logging.warning("Failed to record fail stats")
110
117
  logging.exception(e)
@@ -14,7 +14,7 @@ import urllib3
14
14
  from urllib.parse import urljoin, urlparse
15
15
 
16
16
  from instagrapi.mixins.story import Story
17
- #from instagrapi.types import Media
17
+ from instagrapi.types import Media
18
18
  from instagrapi import Client
19
19
  from instagrapi.mixins.challenge import ChallengeChoice
20
20
  from instagrapi.exceptions import LoginRequired, PleaseWaitFewMinutes, MediaNotFound, ClientNotFoundError, UserNotFound, ChallengeRequired, \
@@ -187,19 +187,20 @@ class InstagramScraper(ScraperAbstract):
187
187
 
188
188
  return ret_val
189
189
 
190
- def download_video(self, url: str, media_info: dict) -> dict:
190
+ def download_video(self, url: str, media_info: Media) -> dict:
191
191
  self.cl.request_timeout = int(os.environ.get("IG_REQUEST_TIMEOUT", default=60))
192
192
  path = self._download_hndlr(self.cl.video_download_by_url, url, folder='/tmp')
193
- return {"local_media_path": str(path), "media_type": JobType.VIDEO, "media_info": {"duration": round(media_info.video_duration)}}
193
+ return {"local_media_path": str(path), "canonical_name": self.extract_canonical_name(media_info), \
194
+ "media_type": JobType.VIDEO, "media_info": {"duration": round(media_info.video_duration)}}
194
195
 
195
- def download_photo(self, url: str) -> dict:
196
+ def download_photo(self, url: str, media_info: Media) -> dict:
196
197
  path = str(self._download_hndlr(self.cl.photo_download_by_url, url, folder='/tmp'))
197
198
  path_lowered = path.lower()
198
199
  if ".webp" in path_lowered:
199
200
  path = InstagramScraper.convert_webp_to_png(path)
200
201
  if ".heic" in path_lowered:
201
202
  path = InstagramScraper.convert_heic_to_png(path)
202
- return {"local_media_path": path, "media_type": JobType.IMAGE}
203
+ return {"local_media_path": path, "canonical_name": self.extract_canonical_name(media_info), "media_type": JobType.IMAGE}
203
204
 
204
205
  def download_story(self, story_info: Story) -> dict:
205
206
  path, media_type, media_info = "", JobType.UNKNOWN, {}
@@ -236,20 +237,33 @@ class InstagramScraper(ScraperAbstract):
236
237
 
237
238
  return {"media_type": JobType.COLLECTION, "save_items": True, "items": chunks}
238
239
 
239
- def download_album(self, media_info: dict) -> dict:
240
+ def download_album(self, media_info: Media) -> dict:
240
241
  chunks = []
241
242
  for media_chunk in Utils.chunker(media_info.resources, 10):
242
243
  chunk = []
243
244
  for media in media_chunk:
244
245
  _media_info = self._download_hndlr(self.cl.media_info, media.pk)
245
246
  if media.media_type == 1: # photo
246
- chunk.append(self.download_photo(url=_media_info.thumbnail_url))
247
+ chunk.append(self.download_photo(url=_media_info.thumbnail_url, media_info=_media_info))
247
248
  elif media.media_type == 2: # video
248
249
  chunk.append(self.download_video(url=_media_info.video_url, media_info=_media_info))
249
250
  chunks.append(chunk)
250
251
 
251
252
  return {"media_type": JobType.COLLECTION, "items": chunks}
252
253
 
254
+ def extract_canonical_name(self, media: Media) -> str:
255
+ ret = ""
256
+ try:
257
+ if media.title:
258
+ ret = media.title
259
+ if media.caption_text:
260
+ ret += "\n" + media.caption_text
261
+ except Exception as e:
262
+ logging.warning("Failed to extract canonical media name!")
263
+ logging.exception(e)
264
+
265
+ return ret
266
+
253
267
  def download(self, job: DownloadJob) -> Optional[list[dict]]:
254
268
  res = []
255
269
  while True:
@@ -261,7 +275,7 @@ class InstagramScraper(ScraperAbstract):
261
275
  if media_info.media_type == 2 and media_info.product_type == "clips": # Reels
262
276
  res.append(self.download_video(url=media_info.video_url, media_info=media_info))
263
277
  elif media_info.media_type == 1: # Photo
264
- res.append(self.download_photo(url=media_info.thumbnail_url))
278
+ res.append(self.download_photo(url=media_info.thumbnail_url, media_info=media_info))
265
279
  elif media_info.media_type == 8: # Album
266
280
  res.append(self.download_album(media_info=media_info))
267
281
  elif scrap_type == "story":
@@ -52,12 +52,15 @@ class Storage(object):
52
52
  path = urlparse(url).path.strip('/')
53
53
  return path
54
54
 
55
- def db_find(self, uniq_id: str) -> list[dict]:
55
+ def db_find(self, uniq_id: str, origin: str = "") -> list[dict]:
56
56
  document = None
57
57
  ret = []
58
58
  try:
59
59
  logging.debug("uniq_id to search is '%s'", uniq_id)
60
- cursor = self.db.find({"uniq_id": uniq_id})
60
+ find_opts = {"uniq_id": uniq_id}
61
+ if origin:
62
+ find_opts["origin"] = origin
63
+ cursor = self.db.find(find_opts)
61
64
  for document in cursor:
62
65
  ret.append(
63
66
  {
@@ -21,6 +21,7 @@ from warp_beacon.uploader import AsyncUploader
21
21
  from warp_beacon.jobs.upload_job import UploadJob
22
22
  from warp_beacon.jobs.types import JobType
23
23
  from warp_beacon.telegram.utils import Utils
24
+ from warp_beacon.telegram.caption_shortener import CaptionShortner
24
25
  from warp_beacon.scheduler.scheduler import IGScheduler
25
26
 
26
27
  import logging
@@ -91,6 +92,8 @@ class Bot(object):
91
92
  self.client.add_handler(MessageHandler(self.handlers.random, filters.command("random")))
92
93
  self.client.add_handler(MessageHandler(self.handlers.handler))
93
94
  self.client.add_handler(CallbackQueryHandler(self.handlers.simple_button_handler))
95
+ self.client.add_handler(CallbackQueryHandler(self.handlers.read_more_handler, filters=filters.create(lambda _, q: q.data.startswith("readmore:"))))
96
+
94
97
 
95
98
  self.placeholder = PlaceholderMessage(self)
96
99
 
@@ -158,9 +161,14 @@ class Bot(object):
158
161
 
159
162
  def build_signature_caption(self, job: UploadJob) -> str:
160
163
  caption = ""
164
+ is_group = job.chat_type in (ChatType.GROUP, ChatType.SUPERGROUP)
161
165
  if job.canonical_name:
162
- caption = f"<b>{html.escape(job.canonical_name)}</b>"
163
- if job.chat_type in (ChatType.GROUP, ChatType.SUPERGROUP):
166
+ if is_group and CaptionShortner.need_short(job.canonical_name):
167
+ caption = f"{html.escape(CaptionShortner.smart_truncate_html(job.canonical_name))} ..."
168
+ job.short_text = True
169
+ else:
170
+ caption = f"<b>{html.escape(job.canonical_name)}</b>"
171
+ if is_group:
164
172
  if job.canonical_name:
165
173
  caption += "\n—\n"
166
174
  if job.message_leftover:
@@ -257,7 +265,6 @@ class Bot(object):
257
265
  args["duration"] = round(job.media_info["duration"])
258
266
  args["title"] = job.canonical_name
259
267
  args["caption"] = self.build_signature_caption(job)
260
- #args["file_name"] = "%s%s" % (job.canonical_name, os.path.splitext(job.local_media_path)[-1]),
261
268
  elif job.media_type == JobType.ANIMATION:
262
269
  if job.tg_file_id:
263
270
  if job.placeholder_message_id:
@@ -337,8 +344,16 @@ class Bot(object):
337
344
  args["disable_notification"] = True
338
345
  args["reply_to_message_id"] = job.message_id
339
346
 
340
- if os.environ.get("ENABLE_DONATES", None) == "true" and job.media_type is not JobType.COLLECTION:
341
- args["reply_markup"] = InlineKeyboardMarkup([[InlineKeyboardButton("❤ Donate", url=os.environ.get("DONATE_LINK", "https://pay.cryptocloud.plus/pos/W5BMtNQt5bJFoW2E"))]])
347
+ if job.media_type is not JobType.COLLECTION:
348
+ render_donates = os.environ.get("ENABLE_DONATES", None) == "true"
349
+ keyboard_buttons = [[]]
350
+ if job.short_text:
351
+ keyboard_buttons[0].append(InlineKeyboardButton("📖 Read more", callback_data=f"read_more:{job.job_origin}:{job.uniq_id}"))
352
+ if render_donates:
353
+ keyboard_buttons[0].append(InlineKeyboardButton("❤ Donate", url=os.environ.get("DONATE_LINK", "https://pay.cryptocloud.plus/pos/W5BMtNQt5bJFoW2E")))
354
+
355
+ if keyboard_buttons[0]: #job.short_text or render_donates:
356
+ args["reply_markup"] = InlineKeyboardMarkup(keyboard_buttons)
342
357
 
343
358
  return args
344
359
 
@@ -0,0 +1,66 @@
1
+ import logging
2
+ from typing import Union
3
+ from bs4 import NavigableString, Tag, Comment
4
+ from bs4 import BeautifulSoup
5
+
6
+ CAPTION_LENGTH_LIMIT = 85
7
+
8
+ class CaptionShortner(object):
9
+ @staticmethod
10
+ def strip_html(text: str) -> str:
11
+ try:
12
+ soup = BeautifulSoup(text, "html.parser")
13
+ return soup.get_text()
14
+ except Exception as e:
15
+ logging.warning("Failed to stript HTML tags!")
16
+ logging.exception(e)
17
+
18
+ return text
19
+
20
+ @staticmethod
21
+ def need_short(text: str) -> bool:
22
+ wo_html = CaptionShortner.strip_html(text)
23
+ if len(wo_html) > CAPTION_LENGTH_LIMIT:
24
+ return True
25
+ return False
26
+
27
+ @staticmethod
28
+ def smart_truncate_html(html: str, limit: int = CAPTION_LENGTH_LIMIT) -> str:
29
+ result = ""
30
+ try:
31
+ soup = BeautifulSoup(html, "html.parser")
32
+ length = 0
33
+
34
+ def walk(node: Union[NavigableString, Tag, Comment]) -> None:
35
+ nonlocal result, length
36
+ if length >= limit:
37
+ return
38
+
39
+ if isinstance(node, str):
40
+ words = node.split()
41
+ for word in words:
42
+ if length + len(word) + 1 > limit:
43
+ return
44
+ if result and not result.endswith(" "):
45
+ result += " "
46
+ length += 1
47
+ result += word
48
+ length += len(word)
49
+ elif isinstance(node, Tag):
50
+ if node.name == '[document]':
51
+ for child in node.children:
52
+ walk(child)
53
+ return
54
+
55
+ for child in node.children:
56
+ walk(child)
57
+ if length >= limit:
58
+ break
59
+
60
+ result += f"</{node.name}>"
61
+
62
+ walk(soup)
63
+ except Exception as e:
64
+ logging.warning("Fail in smart_truncate_html!")
65
+ logging.exception(e)
66
+ return result
@@ -220,4 +220,28 @@ class Handlers(object):
220
220
  show_alert=True
221
221
  )
222
222
  self.bot.downloader.auth_event.set()
223
- self.bot.downloader.auth_event.clear()
223
+ self.bot.downloader.auth_event.clear()
224
+
225
+ async def read_more_handler(self, client: Client, query: CallbackQuery) -> None:
226
+ origin, uniq_id = '', ''
227
+ #read_more:{job.job_origin}:{job.uniq_id}
228
+ if query.data:
229
+ parts = query.data.split(':')
230
+ if len(parts) == 3:
231
+ _, origin, uniq_id = parts
232
+ db_results = []
233
+ if uniq_id and origin:
234
+ db_results = self.storage.db_find(uniq_id=uniq_id.strip(), origin=origin.strip())
235
+ first_entity = {}
236
+ if db_results:
237
+ first_entity = db_results[0]
238
+
239
+ try:
240
+ await client.answer_callback_query(
241
+ callback_query_id=query.id,
242
+ show_alert=True,
243
+ text=first_entity.get("canonical_name", "Failed to fetch data.")
244
+ )
245
+ except Exception as e:
246
+ logging.warning("read_more_handler: Failed for uniq_id='%s', origin='%s", uniq_id, origin)
247
+ logging.exception(e)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: warp_beacon
3
- Version: 2.3.14
3
+ Version: 2.3.16
4
4
  Summary: Telegram bot for expanding external media links
5
5
  Home-page: https://github.com/sb0y/warp_beacon
6
6
  Author: Andrey Bagrintsev
@@ -236,6 +236,7 @@ Requires-Dist: urlextract
236
236
  Requires-Dist: pillow
237
237
  Requires-Dist: pymongo
238
238
  Requires-Dist: instagrapi==2.0.0
239
+ Requires-Dist: bs4
239
240
  Dynamic: author
240
241
  Dynamic: home-page
241
242
 
@@ -4,12 +4,12 @@ var/warp_beacon/accounts.json,sha256=OsXdncs6h88xrF_AP6_WDCK1waGBn9SR-uYdIeK37GM
4
4
  var/warp_beacon/placeholder.gif,sha256=cE5CGJVaop4Sx21zx6j4AyoHU0ncmvQuS2o6hJfEH88,6064
5
5
  var/warp_beacon/proxies.json,sha256=VnjlQDXumOEq72ZFjbh6IqHS1TEHqn8HPYAZqWCeSIA,95
6
6
  warp_beacon/__init__.py,sha256=_rThNODmz0nDp_n4mWo_HKaNFE5jk1_7cRhHyYaencI,163
7
- warp_beacon/__version__.py,sha256=iJ-Q1BLOe6hox693J7orVPTRwodzQ-BF6JHxjBKJRBs,24
7
+ warp_beacon/__version__.py,sha256=46dM2-R2MWq0Q0EeQhZmiBQOj5XnP13aHKco-UoQ5Rs,24
8
8
  warp_beacon/warp_beacon.py,sha256=7KEtZDj-pdhtl6m-zFLsSojs1ZR4o7L0xbqtdmYPvfE,342
9
9
  warp_beacon/compress/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  warp_beacon/compress/video.py,sha256=_PDMVYCyzLYxHv1uZmmzGcG_8rjaZr7BTXsXTTy_oS4,2846
11
11
  warp_beacon/jobs/__init__.py,sha256=ED8_tPle4iL4kqNW0apAVkgNQtRRTnYfAJwBjO1g0JY,180
12
- warp_beacon/jobs/abstract.py,sha256=LyM4PzOXXlQxll1_tjpD-zjKpkJHDXl1pC4ziEdrLTM,3043
12
+ warp_beacon/jobs/abstract.py,sha256=lYZplMq4Uo9vUB6ziMsC_FPer2NsvhdmFJ_I54L7v7Y,3087
13
13
  warp_beacon/jobs/download_job.py,sha256=5HiPcnJppFMhO14___3eSkoMygM3y-vhpGkMAuNhK7s,854
14
14
  warp_beacon/jobs/types.py,sha256=Ae8zINgbs7cOcYkYoOCOACA7duyhnIGMQAJ_SJB1QRQ,176
15
15
  warp_beacon/jobs/upload_job.py,sha256=_ul4psPej1jLEs-BMcMR80GbXDSmm38jE9yoZtecclY,741
@@ -19,31 +19,32 @@ warp_beacon/mediainfo/audio.py,sha256=ous88kwQj4bDIChN5wnGil5LqTs0IQHH0d-nyrL0-Z
19
19
  warp_beacon/mediainfo/silencer.py,sha256=qxMuViOoVwUYb60uCVvqHiGrqByR1_4_rqMT-XdMkwc,1813
20
20
  warp_beacon/mediainfo/video.py,sha256=UBZrhTN5IDI-aYu6tsJEILo9nFkjHhkldGVFmvV7tEI,2480
21
21
  warp_beacon/scheduler/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- warp_beacon/scheduler/scheduler.py,sha256=F9jF6myIHOI0TmC0-rBt2Un8gVvuhBW9zL-nEAEj0Bc,2585
22
+ warp_beacon/scheduler/scheduler.py,sha256=gcV-e-9MjL8JEfu7T3Gy1imUrXz266ub9Ub4O27W5Hw,2786
23
23
  warp_beacon/scraper/__init__.py,sha256=V_C5SmAmRtjFfVBtTotOHCW3ILMQ8m_ulrBF6ry59_A,16944
24
24
  warp_beacon/scraper/abstract.py,sha256=6A6KuBUHZhu8VAyBwLgmnxMPHJcLpgwLapmULy8hpoA,2726
25
- warp_beacon/scraper/account_selector.py,sha256=zYFn9qd5x_BOox5g1iG7QjqaLe_5ezKD_2xFy06fPrk,4731
25
+ warp_beacon/scraper/account_selector.py,sha256=zv5ci_Y7W-tzKeFklffpLts19a1wFg69fqefK-_i_pk,5122
26
26
  warp_beacon/scraper/exceptions.py,sha256=Qkz76yo-X5kucEZIP9tWaK-oYO-kvsPEl8Y0W63oDhU,1300
27
27
  warp_beacon/scraper/fail_handler.py,sha256=_blvckfTZ4xWVancQKVRXH5ClKGwfrBxMwvXIFZh1qA,975
28
28
  warp_beacon/scraper/link_resolver.py,sha256=Rc9ZuMyOo3iPywDHwjngy-WRQ2SXhJwxcg-5ripx7tM,2447
29
29
  warp_beacon/scraper/instagram/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
- warp_beacon/scraper/instagram/instagram.py,sha256=b10mk8IGX9vDKcMVrCXvcELUSEmwOLUJJVKOuFTQSa8,13592
30
+ warp_beacon/scraper/instagram/instagram.py,sha256=coAYqsiWUKhsI_azNYNuAbR9bE36gYC0OD4ah-SUedM,14086
31
31
  warp_beacon/scraper/youtube/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  warp_beacon/scraper/youtube/abstract.py,sha256=cBtExei2Cb3o4YBtHqi8If_FdmE6NyJqNKacm5aw7S0,9243
33
33
  warp_beacon/scraper/youtube/music.py,sha256=qbijpSv54fsrIYHeY-nfmw4vo6oBmedQHsVG8pXNfrc,1380
34
34
  warp_beacon/scraper/youtube/shorts.py,sha256=ujGEV7ILXHqBRa99SyITsnR7ulAHJDtumAh51kVX880,1231
35
35
  warp_beacon/scraper/youtube/youtube.py,sha256=fGrbjBngvvNdpzhb1yZVedNW0_tCrLc31VSYMSHzcQY,2135
36
- warp_beacon/storage/__init__.py,sha256=2uvyIR0APIW6gOxwJRvCji7wS2q6I7dghvLyWsRqRxo,3312
36
+ warp_beacon/storage/__init__.py,sha256=xEzexwWZTMtjVesQ73BLhS0VkWoqRXSUmuXS4h5HOvY,3402
37
37
  warp_beacon/storage/mongo.py,sha256=qC4ZiO8XXvPnP0rJwz4CJx42pqFsyAjCiW10W5QdT6E,527
38
38
  warp_beacon/telegram/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
- warp_beacon/telegram/bot.py,sha256=u3yvXIcqFtVkrf82IXiC4y-UnrEySw49_immBoXIBN4,15725
40
- warp_beacon/telegram/handlers.py,sha256=7gldKrX-CTDQNfD1NQ08cC31uPR-95mhN59OujAEqZg,7704
39
+ warp_beacon/telegram/bot.py,sha256=GKcbgxHxku2dsd_gsf9FGzg7iRkGtZSXr3LwNYayKtk,16387
40
+ warp_beacon/telegram/caption_shortener.py,sha256=6J4Dp35mOmJEyctiuN8WXM39bkQ13m78HoghsYX3fbU,1548
41
+ warp_beacon/telegram/handlers.py,sha256=iWEGYHcxRsjGRdoe8dkldR4jGlFFr9SPIRrFpfIGvKE,8476
41
42
  warp_beacon/telegram/placeholder_message.py,sha256=wN9-BRiyrtHG-EvXtZkGJHt2CX71munQ57ITttjt0mw,6400
42
43
  warp_beacon/telegram/utils.py,sha256=9uebX53G16mV7ER7WgfdWBLFHHw14S8HBt9URrIskg0,4440
43
44
  warp_beacon/uploader/__init__.py,sha256=5KRWsxPRGuQ56YhCEnJsXnb-yQp8dpvWEsPDf0dD-fw,4964
44
- warp_beacon-2.3.14.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
45
- warp_beacon-2.3.14.dist-info/METADATA,sha256=pePZ8lNGtYb8zk8jQERG5QtOkogr_MlshyE976i-xsU,21704
46
- warp_beacon-2.3.14.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
47
- warp_beacon-2.3.14.dist-info/entry_points.txt,sha256=eSB61Rb89d56WY0O-vEIQwkn18J-4CMrJcLA_R_8h3g,119
48
- warp_beacon-2.3.14.dist-info/top_level.txt,sha256=XusFB1mGUStI2gqbtRuJzQrfrAXmdMaT3QTyfvdLEXI,1064
49
- warp_beacon-2.3.14.dist-info/RECORD,,
45
+ warp_beacon-2.3.16.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
46
+ warp_beacon-2.3.16.dist-info/METADATA,sha256=YpbeMS-spZeNRB-_iGmDnxejpu8wl1lELzDqRZlefQs,21723
47
+ warp_beacon-2.3.16.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
48
+ warp_beacon-2.3.16.dist-info/entry_points.txt,sha256=eSB61Rb89d56WY0O-vEIQwkn18J-4CMrJcLA_R_8h3g,119
49
+ warp_beacon-2.3.16.dist-info/top_level.txt,sha256=2iKFlYwJ-meO9sCX4OGEP1hhQN17t2KFksQ5dXMhXUA,1103
50
+ warp_beacon-2.3.16.dist-info/RECORD,,
@@ -31,6 +31,7 @@ warp_beacon/storage
31
31
  warp_beacon/storage/mongo
32
32
  warp_beacon/telegram
33
33
  warp_beacon/telegram/bot
34
+ warp_beacon/telegram/caption_shortener
34
35
  warp_beacon/telegram/handlers
35
36
  warp_beacon/telegram/placeholder_message
36
37
  warp_beacon/telegram/utils