mirror of
https://github.com/anonymousx97/social-dl.git
synced 2025-02-20 11:13:19 +08:00
v2.2.2
Fixes: • Fixed instagram set sauce bug. • Fixed Tasks getting overwritten bug. • Fixed Get_output() wasn't being cancelled on cmd cancellation. Add/Misc: • Add Task IDs in Custom message object. • Clean up/Improve logic for .shell .
This commit is contained in:
parent
1f11bc8405
commit
ae04775a3c
@ -6,7 +6,6 @@ load_dotenv("config.env")
|
|||||||
from .config import Config
|
from .config import Config
|
||||||
from .core.client import BOT
|
from .core.client import BOT
|
||||||
|
|
||||||
|
|
||||||
if not os.environ.get("TERMUX_APK_RELEASE"):
|
if not os.environ.get("TERMUX_APK_RELEASE"):
|
||||||
import uvloop
|
import uvloop
|
||||||
|
|
||||||
|
@ -5,16 +5,14 @@ from app import Config
|
|||||||
from app.core import aiohttp_tools
|
from app.core import aiohttp_tools
|
||||||
from app.core.scraper_config import ScraperConfig
|
from app.core.scraper_config import ScraperConfig
|
||||||
|
|
||||||
API_KEYS = { "KEYS": Config.API_KEYS , "counter": 0 }
|
API_KEYS = {"KEYS": Config.API_KEYS, "counter": 0}
|
||||||
|
|
||||||
|
|
||||||
class Instagram(ScraperConfig):
|
class Instagram(ScraperConfig):
|
||||||
def __init__(self, url):
|
def __init__(self, url):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
shortcode = os.path.basename(urlparse(url).path.rstrip("/"))
|
shortcode = os.path.basename(urlparse(url).path.rstrip("/"))
|
||||||
self.url = (
|
self.url = f"https://www.instagram.com/graphql/query?query_hash=2b0673e0dc4580674a88d426fe00ea90&variables=%7B%22shortcode%22%3A%22{shortcode}%22%7D"
|
||||||
f"https://www.instagram.com/graphql/query?query_hash=2b0673e0dc4580674a88d426fe00ea90&variables=%7B%22shortcode%22%3A%22{shortcode}%22%7D"
|
|
||||||
)
|
|
||||||
self.set_sauce(url)
|
|
||||||
|
|
||||||
async def download_or_extract(self):
|
async def download_or_extract(self):
|
||||||
for func in [self.no_api_dl, self.api_dl]:
|
for func in [self.no_api_dl, self.api_dl]:
|
||||||
@ -24,17 +22,31 @@ class Instagram(ScraperConfig):
|
|||||||
|
|
||||||
async def no_api_dl(self):
|
async def no_api_dl(self):
|
||||||
response = await aiohttp_tools.get_json(url=self.url)
|
response = await aiohttp_tools.get_json(url=self.url)
|
||||||
if not response or "data" not in response or not response["data"]["shortcode_media"]:
|
if (
|
||||||
|
not response
|
||||||
|
or "data" not in response
|
||||||
|
or not response["data"]["shortcode_media"]
|
||||||
|
):
|
||||||
return
|
return
|
||||||
return await self.parse_ghraphql(response["data"]["shortcode_media"])
|
return await self.parse_ghraphql(response["data"]["shortcode_media"])
|
||||||
|
|
||||||
|
|
||||||
async def api_dl(self):
|
async def api_dl(self):
|
||||||
if not Config.API_KEYS:
|
if not Config.API_KEYS:
|
||||||
return
|
return
|
||||||
param = {"api_key": await self.get_key(), "url": self.url, "proxy": "residential", "js": False}
|
param = {
|
||||||
response = await aiohttp_tools.get_json(url="https://api.webscraping.ai/html", timeout=30, params=param)
|
"api_key": await self.get_key(),
|
||||||
if not response or "data" not in response or not response["data"]["shortcode_media"]:
|
"url": self.url,
|
||||||
|
"proxy": "residential",
|
||||||
|
"js": False,
|
||||||
|
}
|
||||||
|
response = await aiohttp_tools.get_json(
|
||||||
|
url="https://api.webscraping.ai/html", timeout=30, params=param
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
not response
|
||||||
|
or "data" not in response
|
||||||
|
or not response["data"]["shortcode_media"]
|
||||||
|
):
|
||||||
return
|
return
|
||||||
self.caption = ".."
|
self.caption = ".."
|
||||||
return await self.parse_ghraphql(response["data"]["shortcode_media"])
|
return await self.parse_ghraphql(response["data"]["shortcode_media"])
|
||||||
@ -44,7 +56,10 @@ class Instagram(ScraperConfig):
|
|||||||
if not type_check:
|
if not type_check:
|
||||||
return
|
return
|
||||||
elif type_check == "GraphSidecar":
|
elif type_check == "GraphSidecar":
|
||||||
self.link = [i["node"].get("video_url") or i["node"].get("display_url") for i in json_["edge_sidecar_to_children"]["edges"]]
|
self.link = [
|
||||||
|
i["node"].get("video_url") or i["node"].get("display_url")
|
||||||
|
for i in json_["edge_sidecar_to_children"]["edges"]
|
||||||
|
]
|
||||||
self.group = True
|
self.group = True
|
||||||
else:
|
else:
|
||||||
if link := json_.get("video_url"):
|
if link := json_.get("video_url"):
|
||||||
@ -64,4 +79,4 @@ class Instagram(ScraperConfig):
|
|||||||
count = 0
|
count = 0
|
||||||
ret_key = keys[count]
|
ret_key = keys[count]
|
||||||
API_KEYS["counter"] = count
|
API_KEYS["counter"] = count
|
||||||
return ret_key
|
return ret_key
|
||||||
|
@ -56,7 +56,10 @@ class Scraper:
|
|||||||
if self.config["Scraper"]["Use_different_protocols"] == "False":
|
if self.config["Scraper"]["Use_different_protocols"] == "False":
|
||||||
self.proxies = {"all": self.config["Scraper"]["All"]}
|
self.proxies = {"all": self.config["Scraper"]["All"]}
|
||||||
else:
|
else:
|
||||||
self.proxies = {"http": self.config["Scraper"]["Http_proxy"], "https": self.config["Scraper"]["Https_proxy"]}
|
self.proxies = {
|
||||||
|
"http": self.config["Scraper"]["Http_proxy"],
|
||||||
|
"https": self.config["Scraper"]["Https_proxy"],
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
self.proxies = None
|
self.proxies = None
|
||||||
# 配置文件不存在则不使用代理/If the configuration file does not exist, do not use
|
# 配置文件不存在则不使用代理/If the configuration file does not exist, do not use
|
||||||
@ -75,7 +78,10 @@ class Scraper:
|
|||||||
try:
|
try:
|
||||||
# 从输入文字中提取索引链接存入列表/Extract index links from input text and store in
|
# 从输入文字中提取索引链接存入列表/Extract index links from input text and store in
|
||||||
# list
|
# list
|
||||||
url = re.findall("http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", text)
|
url = re.findall(
|
||||||
|
"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+",
|
||||||
|
text,
|
||||||
|
)
|
||||||
# 判断是否有链接/Check if there is a link
|
# 判断是否有链接/Check if there is a link
|
||||||
if len(url) > 0:
|
if len(url) > 0:
|
||||||
return url[0]
|
return url[0]
|
||||||
@ -88,7 +94,9 @@ class Scraper:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_x_bogus_url(url: str, headers: dict) -> str:
|
def generate_x_bogus_url(url: str, headers: dict) -> str:
|
||||||
query = urllib.parse.urlparse(url).query
|
query = urllib.parse.urlparse(url).query
|
||||||
xbogus = execjs.compile(open("./X-Bogus.js").read()).call("sign", query, headers["User-Agent"])
|
xbogus = execjs.compile(open("./X-Bogus.js").read()).call(
|
||||||
|
"sign", query, headers["User-Agent"]
|
||||||
|
)
|
||||||
new_url = url + "&X-Bogus=" + xbogus
|
new_url = url + "&X-Bogus=" + xbogus
|
||||||
return new_url
|
return new_url
|
||||||
|
|
||||||
@ -127,7 +135,13 @@ class Scraper:
|
|||||||
print("正在通过抖音分享链接获取原始链接...")
|
print("正在通过抖音分享链接获取原始链接...")
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url, headers=self.headers, proxy=self.proxies, allow_redirects=False, timeout=10) as response:
|
async with session.get(
|
||||||
|
url,
|
||||||
|
headers=self.headers,
|
||||||
|
proxy=self.proxies,
|
||||||
|
allow_redirects=False,
|
||||||
|
timeout=10,
|
||||||
|
) as response:
|
||||||
if response.status == 302:
|
if response.status == 302:
|
||||||
url = (
|
url = (
|
||||||
response.headers["Location"].split("?")[0]
|
response.headers["Location"].split("?")[0]
|
||||||
@ -165,7 +179,13 @@ class Scraper:
|
|||||||
print("正在通过TikTok分享链接获取原始链接...")
|
print("正在通过TikTok分享链接获取原始链接...")
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url, headers=self.headers, proxy=self.proxies, allow_redirects=False, timeout=10) as response:
|
async with session.get(
|
||||||
|
url,
|
||||||
|
headers=self.headers,
|
||||||
|
proxy=self.proxies,
|
||||||
|
allow_redirects=False,
|
||||||
|
timeout=10,
|
||||||
|
) as response:
|
||||||
if response.status == 301:
|
if response.status == 301:
|
||||||
url = (
|
url = (
|
||||||
response.headers["Location"].split("?")[0]
|
response.headers["Location"].split("?")[0]
|
||||||
@ -209,7 +229,9 @@ class Scraper:
|
|||||||
"""
|
"""
|
||||||
# 调用JavaScript函数
|
# 调用JavaScript函数
|
||||||
query = urllib.parse.urlparse(url).query
|
query = urllib.parse.urlparse(url).query
|
||||||
xbogus = execjs.compile(open("./X-Bogus.js").read()).call("sign", query, self.headers["User-Agent"])
|
xbogus = execjs.compile(open("./X-Bogus.js").read()).call(
|
||||||
|
"sign", query, self.headers["User-Agent"]
|
||||||
|
)
|
||||||
if not quiet_mode:
|
if not quiet_mode:
|
||||||
print("生成的X-Bogus签名为: {}".format(xbogus))
|
print("生成的X-Bogus签名为: {}".format(xbogus))
|
||||||
new_url = url + "&X-Bogus=" + xbogus
|
new_url = url + "&X-Bogus=" + xbogus
|
||||||
@ -275,7 +297,12 @@ class Scraper:
|
|||||||
if not quiet_mode:
|
if not quiet_mode:
|
||||||
print("正在获取视频数据API: {}".format(api_url))
|
print("正在获取视频数据API: {}".format(api_url))
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(api_url, headers=self.douyin_api_headers, proxy=self.proxies, timeout=10) as response:
|
async with session.get(
|
||||||
|
api_url,
|
||||||
|
headers=self.douyin_api_headers,
|
||||||
|
proxy=self.proxies,
|
||||||
|
timeout=10,
|
||||||
|
) as response:
|
||||||
response = await response.json()
|
response = await response.json()
|
||||||
# 获取视频数据/Get video data
|
# 获取视频数据/Get video data
|
||||||
video_data = response["aweme_detail"]
|
video_data = response["aweme_detail"]
|
||||||
@ -297,12 +324,19 @@ class Scraper:
|
|||||||
print("正在获取抖音视频数据...")
|
print("正在获取抖音视频数据...")
|
||||||
try:
|
try:
|
||||||
# 构造访问链接/Construct the access link
|
# 构造访问链接/Construct the access link
|
||||||
api_url = f"https://live.douyin.com/webcast/web/enter/?aid=6383&web_rid={web_rid}"
|
api_url = (
|
||||||
|
f"https://live.douyin.com/webcast/web/enter/?aid=6383&web_rid={web_rid}"
|
||||||
|
)
|
||||||
# 访问API/Access API
|
# 访问API/Access API
|
||||||
if not quiet_mode:
|
if not quiet_mode:
|
||||||
print("正在获取视频数据API: {}".format(api_url))
|
print("正在获取视频数据API: {}".format(api_url))
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(api_url, headers=self.douyin_api_headers, proxy=self.proxies, timeout=10) as response:
|
async with session.get(
|
||||||
|
api_url,
|
||||||
|
headers=self.douyin_api_headers,
|
||||||
|
proxy=self.proxies,
|
||||||
|
timeout=10,
|
||||||
|
) as response:
|
||||||
response = await response.json()
|
response = await response.json()
|
||||||
# 获取视频数据/Get video data
|
# 获取视频数据/Get video data
|
||||||
video_data = response["data"]
|
video_data = response["data"]
|
||||||
@ -319,12 +353,16 @@ class Scraper:
|
|||||||
|
|
||||||
# 获取单个抖音视频数据/Get single Douyin video data
|
# 获取单个抖音视频数据/Get single Douyin video data
|
||||||
@retry(stop=stop_after_attempt(4), wait=wait_fixed(7))
|
@retry(stop=stop_after_attempt(4), wait=wait_fixed(7))
|
||||||
async def get_douyin_user_profile_videos(self, profile_url: str, tikhub_token: str) -> Union[dict, None]:
|
async def get_douyin_user_profile_videos(
|
||||||
|
self, profile_url: str, tikhub_token: str
|
||||||
|
) -> Union[dict, None]:
|
||||||
try:
|
try:
|
||||||
api_url = f"https://api.tikhub.io/douyin_profile_videos/?douyin_profile_url={profile_url}&cursor=0&count=20"
|
api_url = f"https://api.tikhub.io/douyin_profile_videos/?douyin_profile_url={profile_url}&cursor=0&count=20"
|
||||||
_headers = {"Authorization": f"Bearer {tikhub_token}"}
|
_headers = {"Authorization": f"Bearer {tikhub_token}"}
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(api_url, headers=_headers, proxy=self.proxies, timeout=10) as response:
|
async with session.get(
|
||||||
|
api_url, headers=_headers, proxy=self.proxies, timeout=10
|
||||||
|
) as response:
|
||||||
response = await response.json()
|
response = await response.json()
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -335,12 +373,16 @@ class Scraper:
|
|||||||
|
|
||||||
# 获取抖音主页点赞视频数据/Get Douyin profile like video data
|
# 获取抖音主页点赞视频数据/Get Douyin profile like video data
|
||||||
@retry(stop=stop_after_attempt(4), wait=wait_fixed(7))
|
@retry(stop=stop_after_attempt(4), wait=wait_fixed(7))
|
||||||
async def get_douyin_profile_liked_data(self, profile_url: str, tikhub_token: str) -> Union[dict, None]:
|
async def get_douyin_profile_liked_data(
|
||||||
|
self, profile_url: str, tikhub_token: str
|
||||||
|
) -> Union[dict, None]:
|
||||||
try:
|
try:
|
||||||
api_url = f"https://api.tikhub.io/douyin_profile_liked_videos/?douyin_profile_url={profile_url}&cursor=0&count=20"
|
api_url = f"https://api.tikhub.io/douyin_profile_liked_videos/?douyin_profile_url={profile_url}&cursor=0&count=20"
|
||||||
_headers = {"Authorization": f"Bearer {tikhub_token}"}
|
_headers = {"Authorization": f"Bearer {tikhub_token}"}
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(api_url, headers=_headers, proxy=self.proxies, timeout=10) as response:
|
async with session.get(
|
||||||
|
api_url, headers=_headers, proxy=self.proxies, timeout=10
|
||||||
|
) as response:
|
||||||
response = await response.json()
|
response = await response.json()
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -351,12 +393,16 @@ class Scraper:
|
|||||||
|
|
||||||
# 获取抖音视频评论数据/Get Douyin video comment data
|
# 获取抖音视频评论数据/Get Douyin video comment data
|
||||||
@retry(stop=stop_after_attempt(4), wait=wait_fixed(7))
|
@retry(stop=stop_after_attempt(4), wait=wait_fixed(7))
|
||||||
async def get_douyin_video_comments(self, video_url: str, tikhub_token: str) -> Union[dict, None]:
|
async def get_douyin_video_comments(
|
||||||
|
self, video_url: str, tikhub_token: str
|
||||||
|
) -> Union[dict, None]:
|
||||||
try:
|
try:
|
||||||
api_url = f"https://api.tikhub.io/douyin_video_comments/?douyin_video_url={video_url}&cursor=0&count=20"
|
api_url = f"https://api.tikhub.io/douyin_video_comments/?douyin_video_url={video_url}&cursor=0&count=20"
|
||||||
_headers = {"Authorization": f"Bearer {tikhub_token}"}
|
_headers = {"Authorization": f"Bearer {tikhub_token}"}
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(api_url, headers=_headers, proxy=self.proxies, timeout=10) as response:
|
async with session.get(
|
||||||
|
api_url, headers=_headers, proxy=self.proxies, timeout=10
|
||||||
|
) as response:
|
||||||
response = await response.json()
|
response = await response.json()
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -406,7 +452,12 @@ class Scraper:
|
|||||||
if not quiet_mode:
|
if not quiet_mode:
|
||||||
print("正在获取视频数据API: {}".format(api_url))
|
print("正在获取视频数据API: {}".format(api_url))
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(api_url, headers=self.tiktok_api_headers, proxy=self.proxies, timeout=10) as response:
|
async with session.get(
|
||||||
|
api_url,
|
||||||
|
headers=self.tiktok_api_headers,
|
||||||
|
proxy=self.proxies,
|
||||||
|
timeout=10,
|
||||||
|
) as response:
|
||||||
response = await response.json()
|
response = await response.json()
|
||||||
video_data = response["aweme_list"][0]
|
video_data = response["aweme_list"][0]
|
||||||
if not quiet_mode:
|
if not quiet_mode:
|
||||||
@ -419,12 +470,16 @@ class Scraper:
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
@retry(stop=stop_after_attempt(4), wait=wait_fixed(7))
|
@retry(stop=stop_after_attempt(4), wait=wait_fixed(7))
|
||||||
async def get_tiktok_user_profile_videos(self, tiktok_video_url: str, tikhub_token: str) -> Union[dict, None]:
|
async def get_tiktok_user_profile_videos(
|
||||||
|
self, tiktok_video_url: str, tikhub_token: str
|
||||||
|
) -> Union[dict, None]:
|
||||||
try:
|
try:
|
||||||
api_url = f"https://api.tikhub.io/tiktok_profile_videos/?tiktok_video_url={tiktok_video_url}&cursor=0&count=20"
|
api_url = f"https://api.tikhub.io/tiktok_profile_videos/?tiktok_video_url={tiktok_video_url}&cursor=0&count=20"
|
||||||
_headers = {"Authorization": f"Bearer {tikhub_token}"}
|
_headers = {"Authorization": f"Bearer {tikhub_token}"}
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(api_url, headers=_headers, proxy=self.proxies, timeout=10) as response:
|
async with session.get(
|
||||||
|
api_url, headers=_headers, proxy=self.proxies, timeout=10
|
||||||
|
) as response:
|
||||||
response = await response.json()
|
response = await response.json()
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -434,12 +489,16 @@ class Scraper:
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
@retry(stop=stop_after_attempt(4), wait=wait_fixed(7))
|
@retry(stop=stop_after_attempt(4), wait=wait_fixed(7))
|
||||||
async def get_tiktok_user_profile_liked_videos(self, tiktok_video_url: str, tikhub_token: str) -> Union[dict, None]:
|
async def get_tiktok_user_profile_liked_videos(
|
||||||
|
self, tiktok_video_url: str, tikhub_token: str
|
||||||
|
) -> Union[dict, None]:
|
||||||
try:
|
try:
|
||||||
api_url = f"https://api.tikhub.io/tiktok_profile_liked_videos/?tiktok_video_url={tiktok_video_url}&cursor=0&count=20"
|
api_url = f"https://api.tikhub.io/tiktok_profile_liked_videos/?tiktok_video_url={tiktok_video_url}&cursor=0&count=20"
|
||||||
_headers = {"Authorization": f"Bearer {tikhub_token}"}
|
_headers = {"Authorization": f"Bearer {tikhub_token}"}
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(api_url, headers=_headers, proxy=self.proxies, timeout=10) as response:
|
async with session.get(
|
||||||
|
api_url, headers=_headers, proxy=self.proxies, timeout=10
|
||||||
|
) as response:
|
||||||
response = await response.json()
|
response = await response.json()
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -458,13 +517,21 @@ class Scraper:
|
|||||||
print("当前链接平台为:{}".format(url_platform))
|
print("当前链接平台为:{}".format(url_platform))
|
||||||
# 获取视频ID/Get video ID
|
# 获取视频ID/Get video ID
|
||||||
print("正在获取视频ID...")
|
print("正在获取视频ID...")
|
||||||
video_id = await self.get_douyin_video_id(video_url) if url_platform == "douyin" else await self.get_tiktok_video_id(video_url)
|
video_id = (
|
||||||
|
await self.get_douyin_video_id(video_url)
|
||||||
|
if url_platform == "douyin"
|
||||||
|
else await self.get_tiktok_video_id(video_url)
|
||||||
|
)
|
||||||
if video_id:
|
if video_id:
|
||||||
if not quiet_mode:
|
if not quiet_mode:
|
||||||
print("获取视频ID成功,视频ID为:{}".format(video_id))
|
print("获取视频ID成功,视频ID为:{}".format(video_id))
|
||||||
# 获取视频数据/Get video data
|
# 获取视频数据/Get video data
|
||||||
print("正在获取视频数据...")
|
print("正在获取视频数据...")
|
||||||
data = await self.get_douyin_video_data(video_id) if url_platform == "douyin" else await self.get_tiktok_video_data(video_id)
|
data = (
|
||||||
|
await self.get_douyin_video_data(video_id)
|
||||||
|
if url_platform == "douyin"
|
||||||
|
else await self.get_tiktok_video_data(video_id)
|
||||||
|
)
|
||||||
if data:
|
if data:
|
||||||
if not quiet_mode:
|
if not quiet_mode:
|
||||||
print("获取视频数据成功,正在判断数据类型...")
|
print("获取视频数据成功,正在判断数据类型...")
|
||||||
@ -571,7 +638,10 @@ class Scraper:
|
|||||||
no_watermark_image_list.append(i["url_list"][0])
|
no_watermark_image_list.append(i["url_list"][0])
|
||||||
watermark_image_list.append(i["download_url_list"][0])
|
watermark_image_list.append(i["download_url_list"][0])
|
||||||
api_data = {
|
api_data = {
|
||||||
"image_data": {"no_watermark_image_list": no_watermark_image_list, "watermark_image_list": watermark_image_list}
|
"image_data": {
|
||||||
|
"no_watermark_image_list": no_watermark_image_list,
|
||||||
|
"watermark_image_list": watermark_image_list,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
# TikTok数据处理/TikTok data processing
|
# TikTok数据处理/TikTok data processing
|
||||||
elif url_platform == "tiktok":
|
elif url_platform == "tiktok":
|
||||||
@ -585,8 +655,12 @@ class Scraper:
|
|||||||
"video_data": {
|
"video_data": {
|
||||||
"wm_video_url": wm_video,
|
"wm_video_url": wm_video,
|
||||||
"wm_video_url_HQ": wm_video,
|
"wm_video_url_HQ": wm_video,
|
||||||
"nwm_video_url": data["video"]["play_addr"]["url_list"][0],
|
"nwm_video_url": data["video"]["play_addr"][
|
||||||
"nwm_video_url_HQ": data["video"]["bit_rate"][0]["play_addr"]["url_list"][0],
|
"url_list"
|
||||||
|
][0],
|
||||||
|
"nwm_video_url_HQ": data["video"]["bit_rate"][0][
|
||||||
|
"play_addr"
|
||||||
|
]["url_list"][0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
# TikTok图片数据处理/TikTok image data processing
|
# TikTok图片数据处理/TikTok image data processing
|
||||||
@ -598,25 +672,38 @@ class Scraper:
|
|||||||
# 有水印图片列表/With watermark image list
|
# 有水印图片列表/With watermark image list
|
||||||
watermark_image_list = []
|
watermark_image_list = []
|
||||||
for i in data["image_post_info"]["images"]:
|
for i in data["image_post_info"]["images"]:
|
||||||
no_watermark_image_list.append(i["display_image"]["url_list"][0])
|
no_watermark_image_list.append(
|
||||||
watermark_image_list.append(i["owner_watermark_image"]["url_list"][0])
|
i["display_image"]["url_list"][0]
|
||||||
|
)
|
||||||
|
watermark_image_list.append(
|
||||||
|
i["owner_watermark_image"]["url_list"][0]
|
||||||
|
)
|
||||||
api_data = {
|
api_data = {
|
||||||
"image_data": {"no_watermark_image_list": no_watermark_image_list, "watermark_image_list": watermark_image_list}
|
"image_data": {
|
||||||
|
"no_watermark_image_list": no_watermark_image_list,
|
||||||
|
"watermark_image_list": watermark_image_list,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
# 更新数据/Update data
|
# 更新数据/Update data
|
||||||
result_data.update(api_data)
|
result_data.update(api_data)
|
||||||
# print("数据处理完成,最终数据: \n{}".format(result_data))
|
# print("数据处理完成,最终数据: \n{}".format(result_data))
|
||||||
# 返回数据/Return data
|
# 返回数据/Return data
|
||||||
return result_data
|
return result_data
|
||||||
except Exception as e:
|
except Exception:
|
||||||
if not quiet_mode:
|
if not quiet_mode:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
print("数据处理失败!")
|
print("数据处理失败!")
|
||||||
return {"status": "failed", "message": "数据处理失败!/Data processing failed!"}
|
return {
|
||||||
|
"status": "failed",
|
||||||
|
"message": "数据处理失败!/Data processing failed!",
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
if not quiet_mode:
|
if not quiet_mode:
|
||||||
print("[抖音|TikTok方法]返回数据为空,无法处理!")
|
print("[抖音|TikTok方法]返回数据为空,无法处理!")
|
||||||
return {"status": "failed", "message": "返回数据为空,无法处理!/Return data is empty and cannot be processed!"}
|
return {
|
||||||
|
"status": "failed",
|
||||||
|
"message": "返回数据为空,无法处理!/Return data is empty and cannot be processed!",
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
if not quiet_mode:
|
if not quiet_mode:
|
||||||
print("获取视频ID失败!")
|
print("获取视频ID失败!")
|
||||||
@ -633,12 +720,24 @@ class Scraper:
|
|||||||
"platform": data.get("platform"),
|
"platform": data.get("platform"),
|
||||||
"type": data.get("type"),
|
"type": data.get("type"),
|
||||||
"desc": data.get("desc"),
|
"desc": data.get("desc"),
|
||||||
"wm_video_url": data["video_data"]["wm_video_url"] if data["type"] == "video" else None,
|
"wm_video_url": data["video_data"]["wm_video_url"]
|
||||||
"wm_video_url_HQ": data["video_data"]["wm_video_url_HQ"] if data["type"] == "video" else None,
|
if data["type"] == "video"
|
||||||
"nwm_video_url": data["video_data"]["nwm_video_url"] if data["type"] == "video" else None,
|
else None,
|
||||||
"nwm_video_url_HQ": data["video_data"]["nwm_video_url_HQ"] if data["type"] == "video" else None,
|
"wm_video_url_HQ": data["video_data"]["wm_video_url_HQ"]
|
||||||
"no_watermark_image_list": data["image_data"]["no_watermark_image_list"] if data["type"] == "image" else None,
|
if data["type"] == "video"
|
||||||
"watermark_image_list": data["image_data"]["watermark_image_list"] if data["type"] == "image" else None,
|
else None,
|
||||||
|
"nwm_video_url": data["video_data"]["nwm_video_url"]
|
||||||
|
if data["type"] == "video"
|
||||||
|
else None,
|
||||||
|
"nwm_video_url_HQ": data["video_data"]["nwm_video_url_HQ"]
|
||||||
|
if data["type"] == "video"
|
||||||
|
else None,
|
||||||
|
"no_watermark_image_list": data["image_data"]["no_watermark_image_list"]
|
||||||
|
if data["type"] == "image"
|
||||||
|
else None,
|
||||||
|
"watermark_image_list": data["image_data"]["watermark_image_list"]
|
||||||
|
if data["type"] == "image"
|
||||||
|
else None,
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
@ -679,4 +778,4 @@ if __name__ == "__main__":
|
|||||||
# api.generate_x_bogus(params)
|
# api.generate_x_bogus(params)
|
||||||
douyin_url = "https://v.douyin.com/rLyrQxA/6.66"
|
douyin_url = "https://v.douyin.com/rLyrQxA/6.66"
|
||||||
tiktok_url = "https://vt.tiktok.com/ZSRwWXtdr/"
|
tiktok_url = "https://vt.tiktok.com/ZSRwWXtdr/"
|
||||||
asyncio.run(async_test(_douyin_url=douyin_url, _tiktok_url=tiktok_url))
|
asyncio.run(async_test(_douyin_url=douyin_url, _tiktok_url=tiktok_url))
|
||||||
|
@ -12,8 +12,42 @@ class Message(MSG):
|
|||||||
super().__dict__.update(message.__dict__)
|
super().__dict__.update(message.__dict__)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def text_list(self):
|
def cmd(self):
|
||||||
return self.text.split()
|
raw_cmd = self.text_list[0]
|
||||||
|
cmd = raw_cmd.lstrip(Config.TRIGGER)
|
||||||
|
return cmd if cmd in Config.CMD_DICT else None
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def flags(self):
|
||||||
|
return [i for i in self.text_list if i.startswith("-")]
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def flt_input(self):
|
||||||
|
split_lines = self.input.splitlines()
|
||||||
|
split_n_joined = [
|
||||||
|
" ".join([word for word in line.split(" ") if word not in self.flags])
|
||||||
|
for line in split_lines
|
||||||
|
]
|
||||||
|
return "\n".join(split_n_joined)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def input(self):
|
||||||
|
if len(self.text_list) > 1:
|
||||||
|
return self.text.split(maxsplit=1)[-1]
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def replied(self):
|
||||||
|
if self.reply_to_message:
|
||||||
|
return Message.parse_message(self.reply_to_message)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def reply_id(self):
|
||||||
|
return self.replied.id if self.replied else None
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def replied_task_id(self):
|
||||||
|
return self.replied.task_id if self.replied else None
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def reply_text_list(self):
|
def reply_text_list(self):
|
||||||
@ -27,47 +61,30 @@ class Message(MSG):
|
|||||||
return reply_text_list
|
return reply_text_list
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def cmd(self):
|
def task_id(self):
|
||||||
raw_cmd = self.text_list[0]
|
return f"{self.chat.id}-{self.id}"
|
||||||
cmd = raw_cmd.lstrip(Config.TRIGGER)
|
|
||||||
return cmd if cmd in Config.CMD_DICT else None
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def flags(self):
|
def text_list(self):
|
||||||
return [i for i in self.text_list if i.startswith("-")]
|
return self.text.split()
|
||||||
|
|
||||||
@cached_property
|
async def async_deleter(self, del_in, task, block):
|
||||||
def input(self):
|
if block:
|
||||||
if len(self.text_list) > 1:
|
x = await task
|
||||||
return self.text.split(maxsplit=1)[-1]
|
await asyncio.sleep(del_in)
|
||||||
return ""
|
await x.delete()
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def flt_input(self):
|
|
||||||
split_lines = self.input.splitlines()
|
|
||||||
split_n_joined = [
|
|
||||||
" ".join([word for word in line.split(" ") if word not in self.flags])
|
|
||||||
for line in split_lines
|
|
||||||
]
|
|
||||||
return "\n".join(split_n_joined)
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def replied(self):
|
|
||||||
if self.reply_to_message:
|
|
||||||
return Message.parse_message(self.reply_to_message)
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def reply_id(self):
|
|
||||||
return self.replied.id if self.replied else None
|
|
||||||
|
|
||||||
async def reply(self, text, del_in: int = 0, block=True, **kwargs):
|
|
||||||
task = self._client.send_message(
|
|
||||||
chat_id=self.chat.id, text=text, reply_to_message_id=self.id, **kwargs
|
|
||||||
)
|
|
||||||
if del_in:
|
|
||||||
await self.async_deleter(task=task, del_in=del_in, block=block)
|
|
||||||
else:
|
else:
|
||||||
return await task
|
asyncio.create_task(
|
||||||
|
self.async_deleter(del_in=del_in, task=task, block=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def delete(self, reply=False):
|
||||||
|
try:
|
||||||
|
await super().delete()
|
||||||
|
if reply and self.replied:
|
||||||
|
await self.replied.delete()
|
||||||
|
except MessageDeleteForbidden:
|
||||||
|
pass
|
||||||
|
|
||||||
async def edit(self, text, del_in: int = 0, block=True, **kwargs):
|
async def edit(self, text, del_in: int = 0, block=True, **kwargs):
|
||||||
if len(str(text)) < 4096:
|
if len(str(text)) < 4096:
|
||||||
@ -83,23 +100,14 @@ class Message(MSG):
|
|||||||
)
|
)
|
||||||
return reply
|
return reply
|
||||||
|
|
||||||
async def delete(self, reply=False):
|
async def reply(self, text, del_in: int = 0, block=True, **kwargs):
|
||||||
try:
|
task = self._client.send_message(
|
||||||
await super().delete()
|
chat_id=self.chat.id, text=text, reply_to_message_id=self.id, **kwargs
|
||||||
if reply and self.replied:
|
)
|
||||||
await self.replied.delete()
|
if del_in:
|
||||||
except MessageDeleteForbidden:
|
await self.async_deleter(task=task, del_in=del_in, block=block)
|
||||||
pass
|
|
||||||
|
|
||||||
async def async_deleter(self, del_in, task, block):
|
|
||||||
if block:
|
|
||||||
x = await task
|
|
||||||
await asyncio.sleep(del_in)
|
|
||||||
await x.delete()
|
|
||||||
else:
|
else:
|
||||||
asyncio.create_task(
|
return await task
|
||||||
self.async_deleter(del_in=del_in, task=task, block=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_message(cls, message):
|
def parse_message(cls, message):
|
||||||
|
@ -27,7 +27,6 @@ async def run_shell_cmd(cmd):
|
|||||||
|
|
||||||
|
|
||||||
class AsyncShell:
|
class AsyncShell:
|
||||||
|
|
||||||
def __init__(self, process):
|
def __init__(self, process):
|
||||||
self.process = process
|
self.process = process
|
||||||
self.full_std = ""
|
self.full_std = ""
|
||||||
@ -46,6 +45,11 @@ class AsyncShell:
|
|||||||
while not self.is_done:
|
while not self.is_done:
|
||||||
yield self.full_std
|
yield self.full_std
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
if not self.is_done:
|
||||||
|
self.process.kill()
|
||||||
|
self._task.cancel()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def run_cmd(cls, cmd, name="shell"):
|
async def run_cmd(cls, cmd, name="shell"):
|
||||||
sub_process = cls(
|
sub_process = cls(
|
||||||
@ -53,6 +57,8 @@ class AsyncShell:
|
|||||||
cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT
|
cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
asyncio.create_task(sub_process.read_output())
|
sub_process._task = asyncio.create_task(
|
||||||
|
sub_process.read_output(), name="AsyncShell"
|
||||||
|
)
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
return sub_process
|
return sub_process
|
||||||
|
@ -17,9 +17,7 @@ async def run_cmd(bot, message):
|
|||||||
cmd = message.input.strip()
|
cmd = message.input.strip()
|
||||||
reply = await message.reply("executing...")
|
reply = await message.reply("executing...")
|
||||||
try:
|
try:
|
||||||
proc_stdout = await asyncio.Task(
|
proc_stdout = await asyncio.Task(shell.run_shell_cmd(cmd), name=reply.task_id)
|
||||||
shell.run_shell_cmd(cmd), name=f"{message.chat.id}-{reply.id}"
|
|
||||||
)
|
|
||||||
except asyncio.exceptions.CancelledError:
|
except asyncio.exceptions.CancelledError:
|
||||||
return await reply.edit("`Cancelled...`")
|
return await reply.edit("`Cancelled...`")
|
||||||
output = f"`${cmd}`\n\n`{proc_stdout}`"
|
output = f"`${cmd}`\n\n`{proc_stdout}`"
|
||||||
@ -28,12 +26,12 @@ async def run_cmd(bot, message):
|
|||||||
|
|
||||||
# Shell but Live Output
|
# Shell but Live Output
|
||||||
async def live_shell(bot, message):
|
async def live_shell(bot, message):
|
||||||
|
cmd = message.input.strip()
|
||||||
|
reply = await message.reply("`getting live output....`")
|
||||||
|
sub_process = await shell.AsyncShell.run_cmd(cmd)
|
||||||
|
sleep_for = 1
|
||||||
|
output = ""
|
||||||
try:
|
try:
|
||||||
cmd = message.input.strip()
|
|
||||||
reply = await message.reply("`getting live output....`")
|
|
||||||
sub_process = await shell.AsyncShell.run_cmd(cmd)
|
|
||||||
sleep_for = 1
|
|
||||||
output = ""
|
|
||||||
async for stdout in sub_process.get_output():
|
async for stdout in sub_process.get_output():
|
||||||
if output != stdout:
|
if output != stdout:
|
||||||
if len(stdout) <= 4096:
|
if len(stdout) <= 4096:
|
||||||
@ -43,11 +41,9 @@ async def live_shell(bot, message):
|
|||||||
parse_mode=ParseMode.MARKDOWN,
|
parse_mode=ParseMode.MARKDOWN,
|
||||||
)
|
)
|
||||||
output = stdout
|
output = stdout
|
||||||
if sleep_for >= 5:
|
if sleep_for >= 6:
|
||||||
sleep_for = 1
|
sleep_for = 1
|
||||||
await asyncio.Task(
|
await asyncio.Task(asyncio.sleep(sleep_for), name=reply.task_id)
|
||||||
asyncio.sleep(sleep_for), name=f"{message.chat.id}-{reply.id}"
|
|
||||||
)
|
|
||||||
sleep_for += 1
|
sleep_for += 1
|
||||||
return await reply.edit(
|
return await reply.edit(
|
||||||
f"`$ {cmd}\n\n``{sub_process.full_std}`",
|
f"`$ {cmd}\n\n``{sub_process.full_std}`",
|
||||||
@ -55,7 +51,8 @@ async def live_shell(bot, message):
|
|||||||
disable_web_page_preview=True,
|
disable_web_page_preview=True,
|
||||||
)
|
)
|
||||||
except asyncio.exceptions.CancelledError:
|
except asyncio.exceptions.CancelledError:
|
||||||
return await reply.edit("`Cancelled...`")
|
sub_process.cancel()
|
||||||
|
return await reply.edit(f"`Cancelled....`")
|
||||||
|
|
||||||
|
|
||||||
# Run Python code
|
# Run Python code
|
||||||
@ -74,7 +71,7 @@ async def executor(bot, message):
|
|||||||
# Create and initialise the function
|
# Create and initialise the function
|
||||||
exec(f"async def _exec(bot, message):\n {formatted_code}")
|
exec(f"async def _exec(bot, message):\n {formatted_code}")
|
||||||
func_out = await asyncio.Task(
|
func_out = await asyncio.Task(
|
||||||
locals()["_exec"](bot, message), name=f"{message.chat.id}-{reply.id}"
|
locals()["_exec"](bot, message), name=reply.task_id
|
||||||
)
|
)
|
||||||
except asyncio.exceptions.CancelledError:
|
except asyncio.exceptions.CancelledError:
|
||||||
return await reply.edit("`Cancelled....`")
|
return await reply.edit("`Cancelled....`")
|
||||||
@ -82,9 +79,11 @@ async def executor(bot, message):
|
|||||||
func_out = str(traceback.format_exc())
|
func_out = str(traceback.format_exc())
|
||||||
sys.stdout = sys.__stdout__
|
sys.stdout = sys.__stdout__
|
||||||
sys.stderr = sys.__stderr__
|
sys.stderr = sys.__stderr__
|
||||||
output = f"`{codeOut.getvalue().strip() or codeErr.getvalue().strip() or func_out}`"
|
output = codeErr.getvalue().strip() or codeOut.getvalue().strip()
|
||||||
|
if func_out is not None:
|
||||||
|
output = "\n\n".join([output, str(func_out)]).strip()
|
||||||
if "-s" not in message.flags:
|
if "-s" not in message.flags:
|
||||||
output = f"> `{code}`\n\n>> {output}"
|
output = f"> `{code}`\n\n>> `{output}`"
|
||||||
return await reply.edit(
|
return await reply.edit(
|
||||||
output,
|
output,
|
||||||
name="exec.txt",
|
name="exec.txt",
|
||||||
|
@ -5,13 +5,13 @@ from app import bot
|
|||||||
|
|
||||||
@bot.add_cmd(cmd=["cancel", "c"])
|
@bot.add_cmd(cmd=["cancel", "c"])
|
||||||
async def cancel_task(bot, message):
|
async def cancel_task(bot, message):
|
||||||
task_id = message.reply_id
|
task_id = message.replied_task_id
|
||||||
if not task_id:
|
if not task_id:
|
||||||
return await message.reply(
|
return await message.reply(
|
||||||
"Reply To a Command or Bot's Response Message.", del_in=8
|
"Reply To a Command or Bot's Response Message.", del_in=8
|
||||||
)
|
)
|
||||||
all_tasks = asyncio.all_tasks()
|
all_tasks = asyncio.all_tasks()
|
||||||
tasks = [x for x in all_tasks if x.get_name() == f"{message.chat.id}-{task_id}"]
|
tasks = [x for x in all_tasks if x.get_name() == task_id]
|
||||||
if not tasks:
|
if not tasks:
|
||||||
return await message.reply("Task not in Currently Running Tasks.", del_in=8)
|
return await message.reply("Task not in Currently Running Tasks.", del_in=8)
|
||||||
response = ""
|
response = ""
|
||||||
|
@ -13,7 +13,7 @@ async def dl(bot, message):
|
|||||||
chat_id=message.chat.id, text="`trying to download...`"
|
chat_id=message.chat.id, text="`trying to download...`"
|
||||||
)
|
)
|
||||||
coro = ExtractAndSendMedia.process(message)
|
coro = ExtractAndSendMedia.process(message)
|
||||||
task = asyncio.Task(coro, name=f"{message.chat.id}-{message.id}")
|
task = asyncio.Task(coro, name=message.task_id)
|
||||||
media = await task
|
media = await task
|
||||||
if media.exceptions:
|
if media.exceptions:
|
||||||
exceptions = "\n".join(media.exceptions)
|
exceptions = "\n".join(media.exceptions)
|
||||||
@ -32,11 +32,11 @@ async def dl(bot, message):
|
|||||||
@bot.on_message(filters.user_filter)
|
@bot.on_message(filters.user_filter)
|
||||||
@bot.on_edited_message(filters.user_filter)
|
@bot.on_edited_message(filters.user_filter)
|
||||||
async def cmd_dispatcher(bot, message):
|
async def cmd_dispatcher(bot, message):
|
||||||
parsed_message = Message.parse_message(message)
|
message = Message.parse_message(message)
|
||||||
func = Config.CMD_DICT[parsed_message.cmd]
|
func = Config.CMD_DICT[message.cmd]
|
||||||
coro = func(bot, parsed_message)
|
coro = func(bot, message)
|
||||||
try:
|
try:
|
||||||
task = asyncio.Task(coro, name=f"{message.chat.id}-{message.id}")
|
task = asyncio.Task(coro, name=message.task_id)
|
||||||
await task
|
await task
|
||||||
except asyncio.exceptions.CancelledError:
|
except asyncio.exceptions.CancelledError:
|
||||||
await bot.log(text=f"<b>#Cancelled</b>:\n<code>{message.text}</code>")
|
await bot.log(text=f"<b>#Cancelled</b>:\n<code>{message.text}</code>")
|
||||||
@ -51,10 +51,10 @@ async def cmd_dispatcher(bot, message):
|
|||||||
|
|
||||||
@bot.on_message(filters.chat_filter)
|
@bot.on_message(filters.chat_filter)
|
||||||
async def dl_dispatcher(bot, message):
|
async def dl_dispatcher(bot, message):
|
||||||
parsed_message = Message.parse_message(message)
|
message = Message.parse_message(message)
|
||||||
coro = dl(bot, parsed_message)
|
coro = dl(bot, message)
|
||||||
try:
|
try:
|
||||||
task = asyncio.Task(coro, name=f"{message.chat.id}-{message.id}")
|
task = asyncio.Task(coro, name=message.task_id)
|
||||||
await task
|
await task
|
||||||
except asyncio.exceptions.CancelledError:
|
except asyncio.exceptions.CancelledError:
|
||||||
await bot.log(text=f"<b>#Cancelled</b>:\n<code>{message.text}</code>")
|
await bot.log(text=f"<b>#Cancelled</b>:\n<code>{message.text}</code>")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user