Fixes

Instagram: Stop api call if no key is added.
Reddit: Fix bug for links without any media.
Youtube: Stop PlayLists and Channel Urls from triggering download.MediaHandler: Clean up repetitive code and squash it.
Client & Bot: Improve Error Logging.
Authorise: Fix Chat name for DMs.

Additions

Add: Dockerfile, run
Add: Plugin loader.

Beta: Bot Mode.
This commit is contained in:
anonymousx97 2023-07-23 21:59:02 +05:30
parent dec02b64a4
commit 0bceaea93b
16 changed files with 169 additions and 52 deletions

11
Dockerfile Normal file
View File

@ -0,0 +1,11 @@
FROM ghcr.io/anonymousx97/build_essentials:main
# adding email and username to the bot
RUN git config --global user.email "88324835+anonymousx97@users.noreply.github" \
git config --global user.name "anonymousx97"
# Exposing Ports for Web Server
EXPOSE 8080 22 8022
# Adding Remote Container Start Command
CMD bash -c "$(curl -fsSL https://raw.githubusercontent.com/anonymousx97/Docker/main/start)"

View File

@ -26,6 +26,8 @@
``` ```
### Installation: ### Installation:
Note: If deploying using Dockerfile add UPSTREAM_REPO var with repo link as it's value.
```bash ```bash
# Install required packages. # Install required packages.
apt install -y python3 git curl python-pip ffmpeg apt install -y python3 git curl python-pip ffmpeg

View File

@ -30,6 +30,8 @@ class Instagram(ScraperConfig):
async def api_dl(self): async def api_dl(self):
if not Config.API_KEYS:
return
param = {"api_key": await self.get_key(), "url": self.url, "proxy": "residential", "js": False} param = {"api_key": await self.get_key(), "url": self.url, "proxy": "residential", "js": False}
response = await aiohttp_tools.get_json(url="https://api.webscraping.ai/html", timeout=30, params=param) 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"]: if not response or "data" not in response or not response["data"]["shortcode_media"]:
@ -62,4 +64,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

View File

@ -45,7 +45,8 @@ class Reddit(ScraperConfig):
else: else:
self.link = json_.get("preview", {}).get("reddit_video_preview", {}).get("fallback_url", json_.get("url_overridden_by_dest", "")).strip() self.link = json_.get("preview", {}).get("reddit_video_preview", {}).get("fallback_url", json_.get("url_overridden_by_dest", "")).strip()
if not self.link:
return
if self.link.endswith(".gif"): if self.link.endswith(".gif"):
self.gif = self.success = True self.gif = self.success = True
else: else:

View File

@ -1,6 +1,7 @@
import asyncio import asyncio
import os import os
import time import time
from urllib.parse import urlparse
import yt_dlp import yt_dlp
@ -39,7 +40,11 @@ class YT_DL(ScraperConfig):
} }
async def download_or_extract(self): async def download_or_extract(self):
if "/playlist" in self.url:
return
if "youtu" in self.url: if "youtu" in self.url:
if not urlparse(self.url).query:
return
self._opts["format"] = "bv[ext=mp4][res=480]+ba[ext=m4a]/b[ext=mp4]" self._opts["format"] = "bv[ext=mp4][res=480]+ba[ext=m4a]/b[ext=mp4]"
if "shorts" in self.url: if "shorts" in self.url:
self._opts["format"] = "bv[ext=mp4][res=720]+ba[ext=m4a]/b[ext=mp4]" self._opts["format"] = "bv[ext=mp4][res=720]+ba[ext=m4a]/b[ext=mp4]"

View File

@ -4,9 +4,6 @@ import os
import traceback import traceback
from urllib.parse import urlparse from urllib.parse import urlparse
from pyrogram.errors import MediaEmpty, PhotoSaveFileInvalid, WebpageCurlFailed
from pyrogram.types import InputMediaPhoto, InputMediaVideo
from app.api.gallerydl import Gallery_DL from app.api.gallerydl import Gallery_DL
from app.api.instagram import Instagram from app.api.instagram import Instagram
from app.api.reddit import Reddit from app.api.reddit import Reddit
@ -14,9 +11,10 @@ from app.api.threads import Threads
from app.api.tiktok import Tiktok from app.api.tiktok import Tiktok
from app.api.ytdl import YT_DL from app.api.ytdl import YT_DL
from app.core import aiohttp_tools, shell from app.core import aiohttp_tools, shell
from pyrogram.errors import MediaEmpty, PhotoSaveFileInvalid, WebpageCurlFailed
from pyrogram.types import InputMediaPhoto, InputMediaVideo
# Thanks Jeel Patel for the concept TG[@jeelpatel231] # Thanks Jeel Patel for the concept TG[@jeelpatel231]
url_map = { url_map = {
"tiktok.com": Tiktok, "tiktok.com": Tiktok,
"www.instagram.com": Instagram, "www.instagram.com": Instagram,
@ -33,11 +31,15 @@ url_map = {
class ExtractAndSendMedia: class ExtractAndSendMedia:
def __init__(self, message): def __init__(self, message):
self.exceptions, self.media_objects = [], [] self.exceptions, self.media_objects = [], []
self.__client = message._client
self.message = message self.message = message
self.doc = "-d" in message.flags self.doc = "-d" in message.flags
self.spoiler = "-s" in message.flags self.spoiler = "-s" in message.flags
self.sender = "" if "-ns" in message.flags else f"\nShared by : {self.extract_sender()}" self.sender = "" if "-ns" in message.flags else f"\nShared by : {self.extract_sender()}"
self.args_ = {"chat_id": self.message.chat.id, "reply_to_message_id": message.reply_id} self.args_ = {
"chat_id": self.message.chat.id,
"reply_to_message_id": message.reply_id,
}
def extract_sender(self): def extract_sender(self):
author = self.message.author_signature author = self.message.author_signature
@ -68,41 +70,41 @@ class ExtractAndSendMedia:
elif obj.group: elif obj.group:
await self.send_group(obj, caption=caption) await self.send_group(obj, caption=caption)
elif obj.photo: elif obj.photo:
await self.send_photo(obj.link, caption=caption) await self.send(
media={"photo":obj.link},
method=self.__client.send_photo,
caption=caption,
has_spoiler=self.spoiler,
)
elif obj.video: elif obj.video:
await self.send_video(obj.link, thumb=obj.thumb, caption=caption) await self.send(
media={"video":obj.link},
method=self.__client.send_video,
thumb=await aiohttp_tools.thumb_dl(obj.thumb),
caption=caption,
has_spoiler=self.spoiler,
)
elif obj.gif: elif obj.gif:
await self.send_animation(obj.link, caption=caption) await self.send(
media={"animation":obj.link},
method=self.__client.send_animation,
caption=caption,
has_spoiler=self.spoiler,
unsave=True,
)
except BaseException: except BaseException:
self.exceptions.append(traceback.format_exc()) self.exceptions.append("\n".join([obj.caption_url.strip(), traceback.format_exc()]))
async def send_photo(self, photo, caption): async def send(self, media, method, **kwargs):
try: try:
try: try:
await self.message._client.send_photo(**self.args_, photo=photo, caption=caption, has_spoiler=self.spoiler) await method(**media, **self.args_, **kwargs)
except (MediaEmpty, WebpageCurlFailed): except (MediaEmpty, WebpageCurlFailed):
photo = await aiohttp_tools.in_memory_dl(photo) key, value = list(media.items())[0]
await self.message._client.send_photo(**self.args_, photo=photo, caption=caption, has_spoiler=self.spoiler) media[key] = await aiohttp_tools.in_memory_dl(value)
await method(**media, **self.args_, **kwargs)
except PhotoSaveFileInvalid: except PhotoSaveFileInvalid:
await self.message._client.send_document(**self.args, document=photo, caption=caption, force_document=True) await self.__client.send_document(**self.args_, document=media, caption=caption, force_document=True)
async def send_video(self, video, caption, thumb):
try:
await self.message._client.send_video(
**self.args_, video=video, caption=caption, thumb=await aiohttp_tools.thumb_dl(thumb), has_spoiler=self.spoiler
)
except (MediaEmpty, WebpageCurlFailed):
video = await aiohttp_tools.in_memory_dl(video)
await self.message._client.send_video(
**self.args_, video=video, caption=caption, thumb=await aiohttp_tools.thumb_dl(thumb), has_spoiler=self.spoiler
)
async def send_animation(self, gif, caption):
try:
await self.message._client.send_animation(**self.args_, animation=gif, caption=caption, unsave=True, has_spoiler=self.spoiler)
except (MediaEmpty, WebpageCurlFailed):
gif = await aiohttp_tools.in_memory_dl(gif)
await self.message._client.send_animation(**self.args_, animation=gif, caption=caption, unsave=True, has_spoiler=self.spoiler)
async def send_document(self, docs, caption, path=""): async def send_document(self, docs, caption, path=""):
if not path: if not path:
@ -112,10 +114,10 @@ class ExtractAndSendMedia:
docs = glob.glob(f"{path}/*") docs = glob.glob(f"{path}/*")
for doc in docs: for doc in docs:
try: try:
await self.message._client.send_document(**self.args_, document=doc, caption=caption, force_document=True) await self.__client.send_document(**self.args_, document=doc, caption=caption, force_document=True)
except (MediaEmpty, WebpageCurlFailed): except (MediaEmpty, WebpageCurlFailed):
doc = await aiohttp_tools.in_memory_dl(doc) doc = await aiohttp_tools.in_memory_dl(doc)
await self.message._client.send_document(**self.args_, document=doc, caption=caption, force_document=True) await self.__client.send_document(**self.args_, document=doc, caption=caption, force_document=True)
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
async def send_group(self, media, caption): async def send_group(self, media, caption):
@ -125,9 +127,9 @@ class ExtractAndSendMedia:
sorted = await self.sort_media_urls(media.link, caption) sorted = await self.sort_media_urls(media.link, caption)
for data in sorted: for data in sorted:
if isinstance(data, list): if isinstance(data, list):
await self.message._client.send_media_group(**self.args_, media=data) await self.__client.send_media_group(**self.args_, media=data)
else: else:
await self.send_animation(data, caption=caption) await self.send(media={"animation": data}, method=self.__client.send_animation, caption=caption, has_spoiler=self.spoiler, unsave=True)
await asyncio.sleep(1) await asyncio.sleep(1)
async def sort_media_path(self, path, caption): async def sort_media_path(self, path, caption):

View File

@ -16,12 +16,15 @@ from app.core.message import Message
class BOT(Client): class BOT(Client):
_LOADED = False
def __init__(self): def __init__(self):
if string := os.environ.get("STRING_SESSION"):
mode_arg = { "session_string": string }
else:
mode_arg = { "bot_token": os.environ.get("BOT_TOKEN") }
super().__init__( super().__init__(
name="bot", name="bot",
session_string=os.environ.get("STRING_SESSION"), **mode_arg,
api_id=int(os.environ.get("API_ID")), api_id=int(os.environ.get("API_ID")),
api_hash=os.environ.get("API_HASH"), api_hash=os.environ.get("API_HASH"),
in_memory=True, in_memory=True,
@ -60,12 +63,11 @@ class BOT(Client):
async def import_modules(self): async def import_modules(self):
for module_ in glob.glob("app/*/*.py"): for module_ in glob.glob("app/*/*.py"):
importlib.import_module(os.path.splitext(module_.replace("/", "."))[0]) importlib.import_module(os.path.splitext(module_.replace("/", "."))[0])
self._LOADED = True
async def log(self, text, chat=None, func=None, name="log.txt"): async def log(self, text, chat=None, func=None, name="log.txt",disable_web_page_preview=True):
if chat or func: if chat or func:
text = f"<b>Function:</b> {func}\n<b>Chat:</b> {chat}\n<b>Traceback:</b>\n{text}" text = f"<b>Function:</b> {func}\n<b>Chat:</b> {chat}\n<b>Traceback:</b>\n{text}"
return await self.send_message(chat_id=Config.LOG_CHAT, text=text, name=name) return await self.send_message(chat_id=Config.LOG_CHAT, text=text, name=name, disable_web_page_preview=disable_web_page_preview)
async def restart(self): async def restart(self):
await aiohttp_tools.session_switch() await aiohttp_tools.session_switch()

View File

@ -24,7 +24,7 @@ class Message(MSG):
self.reply_id = replied.id self.reply_id = replied.id
def flags_n_input(self): def flags_n_input(self):
self.flags = [i for i in self.text.split() if i.startswith("-")] self.flags = [i for i in self.text.split() if i.startswith("-") ]
split_cmd_str = self.text.split(maxsplit=1) split_cmd_str = self.text.split(maxsplit=1)
if len(split_cmd_str) > 1: if len(split_cmd_str) > 1:
self.input = split_cmd_str[-1] self.input = split_cmd_str[-1]

View File

@ -15,7 +15,7 @@ class ScraperConfig:
self.gif = False self.gif = False
def set_sauce(self, url): def set_sauce(self, url):
self.caption_url = f"\n\n<a href={url}>Sauce</a>" self.caption_url = f"\n\n<a href='{url}'>Sauce</a>"
@classmethod @classmethod
async def start(cls, url): async def start(cls, url):

View File

@ -94,7 +94,7 @@ async def add_chat(bot, message):
message_id=Config.AUTO_DL_MESSAGE_ID, message_id=Config.AUTO_DL_MESSAGE_ID,
): ):
return await message.reply(err) return await message.reply(err)
await message.reply(f"<b>{message.chat.title}</b> Added to Authorised List.") await message.reply(f"<b>{message.chat.title or message.chat.first_name}</b> Added to Authorised List.")
@bot.add_cmd(cmd="delchat") @bot.add_cmd(cmd="delchat")
@ -110,7 +110,7 @@ async def add_chat(bot, message):
message_id=Config.AUTO_DL_MESSAGE_ID, message_id=Config.AUTO_DL_MESSAGE_ID,
): ):
return await message.reply(err) return await message.reply(err)
await message.reply(f"<b>{message.chat.title}</b> Added Removed from Authorised List.") await message.reply(f"<b>{message.chat.title or message.chat.first_name}</b> Added Removed from Authorised List.")

View File

@ -29,4 +29,4 @@ async def restart(bot, message):
@bot.add_cmd(cmd="update") @bot.add_cmd(cmd="update")
async def chat_update(bot, message): async def chat_update(bot, message):
await bot.set_filter_list() await bot.set_filter_list()
await message.reply("Filters Refreshed") await message.reply("Filters Refreshed")

22
app/plugins/loader.py Normal file
View File

@ -0,0 +1,22 @@
import importlib
import sys
import traceback
from app import bot
@bot.add_cmd(cmd="load")
async def loader(bot, message):
if (
not message.replied
or not message.replied.document
or not message.replied.document.file_name.endswith(".py")
):
return await message.reply("reply to a plugin.")
file_name = message.replied.document.file_name.rstrip(".py")
sys.modules.pop(f"app.temp.{file_name}", None)
plugin = await message.replied.download("app/temp/")
try:
importlib.import_module(f"app.temp.{file_name}")
except BaseException:
return await message.reply(str(traceback.format_exc()))
await message.reply(f"Loaded {file_name}.py.")

56
app/plugins/song.py Normal file
View File

@ -0,0 +1,56 @@
import asyncio
import glob
import shutil
from time import time
from urllib.parse import urlparse
import yt_dlp
from app import bot
from app.api.ytdl import FakeLogger
from app.core.aiohttp_tools import in_memory_dl
domains = ["www.youtube.com", "youtube.com", "m.youtube.com", "youtu.be", "www.youtube-nocookie.com", "music.youtube.com"]
@bot.add_cmd(cmd="song")
async def song_dl(bot, message):
reply_query = None
audio_file = None
artist = None
if message.replied:
for link in message.replied.text.split():
if urlparse(link).netloc in domains:
reply_query = link
break
query = reply_query or message.flt_input
if not query:
return await message.reply("Give a song name or link to download.")
response = await message.reply("Searching....")
dl_path = f"downloads/{time()}/"
query_or_search = query if query.startswith("http") else f"ytsearch:{query}"
if "-m" in message.flags:
aformat = "mp3"
else:
aformat = "opus"
yt_opts = {
"logger": FakeLogger(),
"outtmpl": dl_path + "%(title)s.%(ext)s",
"format": "bestaudio",
"postprocessors": [{"key": "FFmpegExtractAudio", "preferredcodec": aformat}, {"key": "FFmpegMetadata"}, {"key": "EmbedThumbnail"}],
}
ytdl = yt_dlp.YoutubeDL(yt_opts)
yt_info = await asyncio.to_thread(ytdl.extract_info, query_or_search)
if not query_or_search.startswith("http"):
yt_info = yt_info["entries"][0]
duration = yt_info["duration"]
artist = yt_info["channel"]
thumb = await in_memory_dl(yt_info["thumbnail"])
down_path = glob.glob(dl_path + "*")
if not down_path:
return await response.edit("Not found")
await response.edit("Uploading....")
for audio_file in down_path:
if audio_file.endswith((".opus", ".mp3")):
await message.reply_audio(audio=audio_file, duration=int(duration), performer=str(artist), thumb=thumb)
await response.delete()
shutil.rmtree(dl_path, ignore_errors=True)

View File

@ -7,7 +7,7 @@ from pyrogram.enums import ParseMode
from app import Config from app import Config
from app.core import shell from app.core import shell
from app.core.aiohttp_tools import SESSION, in_memory_dl from app.core import aiohttp_tools as aio
# Run shell commands # Run shell commands

View File

@ -11,7 +11,7 @@ async def dl(bot, message):
media = await ExtractAndSendMedia.process(message) media = await ExtractAndSendMedia.process(message)
if media.exceptions: if media.exceptions:
exceptions = "\n".join(media.exceptions) exceptions = "\n".join(media.exceptions)
await bot.log(text=exceptions, func="DL", chat=message.chat.id, name="traceback.txt") await bot.log(text=exceptions, func="DL", chat=message.chat.title or message.chat.first_name, name="traceback.txt")
return await reply.edit(f"Media Download Failed.") return await reply.edit(f"Media Download Failed.")
if media.media_objects: if media.media_objects:
await message.delete() await message.delete()
@ -26,7 +26,7 @@ async def cmd_dispatcher(bot, message):
try: try:
await func(bot, parsed_message) await func(bot, parsed_message)
except BaseException: except BaseException:
await bot.log(text=str(traceback.format_exc()), chat=message.chat.id, func=func.__name__, name="traceback.txt") await bot.log(text=str(traceback.format_exc()), chat=message.chat.title or message.chat.first_name, func=func.__name__, name="traceback.txt")
@bot.on_message(filters.chat_filter) @bot.on_message(filters.chat_filter)
@ -36,4 +36,4 @@ async def dl_dispatcher(bot, message):
try: try:
await func(bot, parsed_message) await func(bot, parsed_message)
except BaseException: except BaseException:
await bot.log(text=str(traceback.format_exc()), chat=message.chat.id, func=func.__name__, name="traceback.txt") await bot.log(text=str(traceback.format_exc()), chat=message.chat.title or message.chat.first_name, func=func.__name__, name="traceback.txt")

14
run Normal file
View File

@ -0,0 +1,14 @@
#!/bin/sh
if [ "$API_PORT" ] ; then
py_code="
from aiohttp import web
app = web.Application()
app.router.add_get('/', lambda _: web.Response(text='Web Server Running...'))
web.run_app(app, host='0.0.0.0', port=$API_PORT, reuse_port=True, print=None)
"
python3 -q -c "$py_code" & echo "Dummy Web Server Started..."
fi
python3 -m app