├── runtime.txt ├── Procfile ├── bot ├── workers │ ├── __init__.py │ └── worker.py ├── database │ ├── __init__.py │ └── database.py ├── utils │ ├── __init__.py │ ├── broadcast.py │ └── utils.py ├── plugins │ ├── settings.py │ ├── admin │ │ ├── broadcast.py │ │ ├── status.py │ │ ├── admin.py │ │ ├── cancel_broadcast.py │ │ ├── broadcast_status.py │ │ ├── banned_users.py │ │ ├── unban_user.py │ │ └── ban_user.py │ ├── trim_video.py │ ├── screenshot.py │ ├── sample.py │ ├── manual_screenshot_1.py │ ├── set_watermark_text.py │ ├── trim_manual_screenshots.py │ ├── urls.py │ ├── mediainfo.py │ ├── 1.py │ ├── start.py │ ├── help.py │ └── settings_cb.py ├── __main__.py ├── processes │ ├── exception.py │ ├── __init__.py │ ├── base.py │ ├── mediainfo.py │ ├── sample.py │ ├── trim.py │ ├── screenshot.py │ └── manual_screenshot.py ├── config.py ├── screenshotbot.py └── messages.py ├── requirements.txt ├── .gitignore ├── app.json ├── README.md └── LICENSE /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.1 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: python -m bot -------------------------------------------------------------------------------- /bot/workers/__init__.py: -------------------------------------------------------------------------------- 1 | from .worker import Worker -------------------------------------------------------------------------------- /bot/database/__init__.py: -------------------------------------------------------------------------------- 1 | from .database import Database 2 | -------------------------------------------------------------------------------- /bot/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .utils import Utilities, ProcessTypes 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyrogram==2.0.57 2 | tgcrypto 3 | motor 4 | dnspython 5 | async-timeout 6 | aiohttp -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | __pycache__/ 3 | *.session 4 | cconfig.py 5 | screenshots/ 6 | videos/ 7 | thumbnails/ 8 | *.sh -------------------------------------------------------------------------------- /bot/plugins/settings.py: -------------------------------------------------------------------------------- 1 | from pyrogram import filters 2 | 3 | from bot.screenshotbot import ScreenShotBot 4 | from bot.utils import Utilities 5 | from bot.database import Database 6 | 7 | 8 | db = Database() 9 | 10 | 11 | @ScreenShotBot.on_message(filters.private & filters.command("settings")) 12 | async def start(c, m): 13 | 14 | await Utilities.display_settings(c, m, db) 15 | -------------------------------------------------------------------------------- /bot/__main__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from .screenshotbot import ScreenShotBot 4 | from .config import Config 5 | 6 | if __name__ == "__main__": 7 | 8 | logging.basicConfig(level=logging.DEBUG if Config.DEBUG else logging.INFO) 9 | logging.getLogger("pyrogram").setLevel( 10 | logging.INFO if Config.DEBUG else logging.WARNING 11 | ) 12 | 13 | ScreenShotBot().run() 14 | -------------------------------------------------------------------------------- /bot/plugins/admin/broadcast.py: -------------------------------------------------------------------------------- 1 | from pyrogram import filters 2 | 3 | from bot.config import Config 4 | from bot.screenshotbot import ScreenShotBot 5 | 6 | 7 | @ScreenShotBot.on_message( 8 | filters.private 9 | & filters.command("broadcast") 10 | & filters.user(Config.AUTH_USERS) 11 | & filters.reply 12 | ) 13 | async def broadcast_(c, m): 14 | await c.start_broadcast( 15 | broadcast_message=m.reply_to_message, admin_id=m.from_user.id 16 | ) 17 | -------------------------------------------------------------------------------- /bot/plugins/admin/status.py: -------------------------------------------------------------------------------- 1 | from pyrogram import filters 2 | 3 | from bot.config import Config 4 | from bot.database import Database 5 | from bot.screenshotbot import ScreenShotBot 6 | 7 | db = Database() 8 | 9 | 10 | @ScreenShotBot.on_message( 11 | filters.private & filters.command("status") & filters.user(Config.AUTH_USERS) 12 | ) 13 | async def sts(c, m): 14 | total_users = await db.total_users_count() 15 | text = f"Total user(s) till date: {total_users}\n\n" 16 | text += f"Active users, today: {len(c.CHAT_FLOOD)}" 17 | await m.reply_text(text=text, quote=True) 18 | -------------------------------------------------------------------------------- /bot/plugins/admin/admin.py: -------------------------------------------------------------------------------- 1 | from pyrogram import filters 2 | 3 | from bot.config import Config 4 | from bot.screenshotbot import ScreenShotBot 5 | 6 | 7 | @ScreenShotBot.on_message( 8 | filters.private & filters.command("admin") & filters.user(Config.AUTH_USERS) 9 | ) 10 | async def admin(c, m): 11 | 12 | text = "Current admins of the bot:\n\n" 13 | admins = await c.get_users(Config.AUTH_USERS) 14 | for admn in admins: 15 | text += f"\t- {admn.mention}\n" 16 | 17 | text += "\nAvailable admin commands are:\n" 18 | text += ( 19 | "\t- /unban_user\n\t- /broadcast\n\t- /banned_users\n\t- /ban_user\n\t- /status" 20 | ) 21 | 22 | await m.reply_text(text, quote=True) 23 | -------------------------------------------------------------------------------- /bot/processes/exception.py: -------------------------------------------------------------------------------- 1 | class BaseException(Exception): 2 | def __init__(self, for_user, for_admin, extra_details=None): 3 | self.for_user = for_user 4 | self.for_admin = for_admin 5 | self.extra_details = extra_details 6 | 7 | def __str__(self): 8 | return ( 9 | f"{self.__class__.__name__}(\n\tfor_user='{self.for_user}',\n\t" 10 | f"for_admin='{self.for_admin}',\n\textra_details='{self.extra_details}')" 11 | ) 12 | 13 | def __repr__(self): 14 | return( 15 | f"{self.__class__.__name__}(\n\tfor_user='{self.for_user}',\n\t" 16 | f"for_admin='{self.for_admin}',\n\textra_details='{self.extra_details}')" 17 | ) 18 | -------------------------------------------------------------------------------- /bot/plugins/admin/cancel_broadcast.py: -------------------------------------------------------------------------------- 1 | from pyrogram import filters 2 | 3 | from bot.config import Config 4 | from bot.screenshotbot import ScreenShotBot 5 | 6 | 7 | @ScreenShotBot.on_callback_query( 8 | filters.create(lambda _, __, query: query.data.startswith("cncl_bdct")) 9 | & filters.user(Config.AUTH_USERS) 10 | ) 11 | async def cncl_broadcast_(c, cb): 12 | 13 | _, broadcast_id = cb.data.split("+") 14 | 15 | if not c.broadcast_ids.get(broadcast_id): 16 | await cb.answer( 17 | text=f"No active broadcast with id {broadcast_id}", show_alert=True 18 | ) 19 | return 20 | 21 | broadcast_handler = c.broadcast_ids[broadcast_id] 22 | broadcast_handler.cancel() 23 | 24 | await cb.answer(text="Broadcast will be canceled soon.", show_alert=True) 25 | -------------------------------------------------------------------------------- /bot/plugins/trim_video.py: -------------------------------------------------------------------------------- 1 | from pyrogram import filters 2 | from pyrogram.types import ForceReply 3 | 4 | from ..screenshotbot import ScreenShotBot 5 | from ..config import Config 6 | 7 | 8 | @ScreenShotBot.on_callback_query( 9 | filters.create(lambda _, __, query: query.data.startswith("trim")) 10 | ) 11 | async def _(c, m): 12 | await m.answer() 13 | dur = m.message.text.markdown.split("\n")[-1] 14 | await m.message.delete(True) 15 | await c.send_message( 16 | m.from_user.id, 17 | f"#trim_video\n\n{dur}\n\nNow send your start and end seconds in the given format and should " 18 | f"be upto {Config.MAX_TRIM_DURATION}s. \n**start:end**\n\nEg: `400:500` ==> This trims video from 400s to 500s", 19 | reply_to_message_id=m.message.reply_to_message.message_id, 20 | reply_markup=ForceReply(), 21 | ) 22 | -------------------------------------------------------------------------------- /bot/plugins/screenshot.py: -------------------------------------------------------------------------------- 1 | from pyrogram import filters 2 | 3 | from bot.utils import ProcessTypes 4 | from bot.screenshotbot import ScreenShotBot 5 | from bot.processes import ProcessFactory 6 | from bot.messages import Messages as ms 7 | from bot.config import Config 8 | 9 | 10 | @ScreenShotBot.on_callback_query( 11 | filters.create(lambda _, __, query: query.data.startswith("scht")) 12 | ) 13 | async def _(c, m): 14 | try: 15 | await m.answer() 16 | except Exception: 17 | pass 18 | 19 | await m.edit_message_text( 20 | ms.ADDED_TO_QUEUE.format(per_user_process_count=Config.MAX_PROCESSES_PER_USER), 21 | ) 22 | c.process_pool.new_task( 23 | ( 24 | m.from_user.id, 25 | ProcessFactory( 26 | process_type=ProcessTypes.SCREENSHOTS, client=c, input_message=m 27 | ), 28 | ) 29 | ) 30 | -------------------------------------------------------------------------------- /bot/plugins/sample.py: -------------------------------------------------------------------------------- 1 | from pyrogram import filters 2 | 3 | from bot.utils import ProcessTypes 4 | from bot.screenshotbot import ScreenShotBot 5 | from bot.processes import ProcessFactory 6 | from bot.messages import Messages as ms 7 | from bot.config import Config 8 | 9 | 10 | @ScreenShotBot.on_callback_query( 11 | filters.create(lambda _, __, query: query.data.startswith("smpl")) 12 | ) 13 | async def _(c, m): 14 | # c.process_pool.new_task(Utilities().sample_fn(c, m)) 15 | try: 16 | await m.answer() 17 | except Exception: 18 | pass 19 | 20 | await m.edit_message_text( 21 | ms.ADDED_TO_QUEUE.format(per_user_process_count=Config.MAX_PROCESSES_PER_USER), 22 | ) 23 | c.process_pool.new_task( 24 | ( 25 | m.from_user.id, 26 | ProcessFactory( 27 | process_type=ProcessTypes.SAMPLE_VIDEO, client=c, input_message=m 28 | ), 29 | ) 30 | ) 31 | -------------------------------------------------------------------------------- /bot/plugins/admin/broadcast_status.py: -------------------------------------------------------------------------------- 1 | from pyrogram import filters 2 | 3 | from bot.config import Config 4 | from bot.screenshotbot import ScreenShotBot 5 | 6 | 7 | @ScreenShotBot.on_callback_query( 8 | filters.create(lambda _, __, query: query.data.startswith("sts_bdct")) 9 | & filters.user(Config.AUTH_USERS) 10 | ) 11 | async def sts_broadcast_(c, cb): 12 | 13 | _, broadcast_id = cb.data.split("+") 14 | 15 | if not c.broadcast_ids.get(broadcast_id): 16 | await cb.answer( 17 | text=f"No active broadcast with id {broadcast_id}", show_alert=True 18 | ) 19 | return 20 | 21 | sts_txt = "" 22 | broadcast_handler = c.broadcast_ids[broadcast_id] 23 | broadcast_progress = broadcast_handler.get_progress() 24 | for key, value in broadcast_progress.items(): 25 | sts_txt += f"{key} = {value}\n" 26 | 27 | await cb.answer( 28 | text=f"Broadcast Status for {broadcast_id}\n\n{sts_txt}", show_alert=True 29 | ) 30 | -------------------------------------------------------------------------------- /bot/plugins/manual_screenshot_1.py: -------------------------------------------------------------------------------- 1 | from pyrogram import filters 2 | from pyrogram.types import ForceReply 3 | 4 | from bot.screenshotbot import ScreenShotBot 5 | 6 | 7 | @ScreenShotBot.on_callback_query( 8 | filters.create(lambda _, __, query: query.data.startswith("mscht")) 9 | ) 10 | async def _(c, m): 11 | await m.answer() 12 | dur = m.message.text.markdown.split("\n")[-1] 13 | await m.message.delete(True) 14 | await c.send_message( 15 | m.from_user.id, 16 | f"#manual_screenshot\n\n{dur}\n\nNow send your list of seconds separated by `,`(comma).\nEg: `0,10,40,60,120`." 17 | "\nThis will generate screenshots at 0, 10, 40, 60, and 120 seconds. \n\n" 18 | "1. The list can have a maximum of 10 valid positions.\n" 19 | "2. The position has to be greater than or equal to 0, or less than the video length in order to be valid.", 20 | reply_to_message_id=m.message.reply_to_message.message_id, 21 | reply_markup=ForceReply(), 22 | ) 23 | -------------------------------------------------------------------------------- /bot/plugins/set_watermark_text.py: -------------------------------------------------------------------------------- 1 | from pyrogram import filters 2 | 3 | from bot.screenshotbot import ScreenShotBot 4 | from bot.database import Database 5 | 6 | 7 | db = Database() 8 | 9 | 10 | @ScreenShotBot.on_message(filters.private & filters.command("set_watermark")) 11 | async def _(c, m): 12 | 13 | if len(m.command) == 1: 14 | await m.reply_text( 15 | text="You can add custom watermark text to the screenshots.\n\nUsage: `/set_watermark text`. " 16 | "Text should not Exceed 30 characters.", 17 | quote=True 18 | ) 19 | return 20 | 21 | watermark_text = " ".join(m.command[1:]) 22 | if len(watermark_text) > 30: 23 | await m.reply_text( 24 | text=f"The watermark text you provided (__{watermark_text}__) is `{len(watermark_text)}` " 25 | "characters long! You cannot set watermark text greater than 30 characters.", 26 | quote=True 27 | ) 28 | return 29 | 30 | await db.update_watermark_text(m.chat.id, watermark_text) 31 | await m.reply_text( 32 | text=f"You have successfully set __{watermark_text}__ as your watermark text. From now on this will " 33 | "be applied to your screenshots! To remove watermark text see /settings.", 34 | quote=True 35 | ) 36 | -------------------------------------------------------------------------------- /bot/plugins/trim_manual_screenshots.py: -------------------------------------------------------------------------------- 1 | from pyrogram import filters 2 | from pyrogram.types import ForceReply 3 | 4 | from bot.utils import ProcessTypes 5 | from bot.processes import ProcessFactory 6 | from bot.screenshotbot import ScreenShotBot 7 | from bot.messages import Messages as ms 8 | from bot.config import Config 9 | 10 | 11 | reply_markup_filter = filters.create( 12 | lambda _, __, message: message.reply_to_message.reply_markup 13 | and isinstance(message.reply_to_message.reply_markup, ForceReply) 14 | ) 15 | 16 | 17 | @ScreenShotBot.on_message(filters.private & filters.reply & reply_markup_filter) 18 | async def _(c, m): 19 | reply_message = await m.reply_text( 20 | ms.ADDED_TO_QUEUE.format(per_user_process_count=Config.MAX_PROCESSES_PER_USER), 21 | quote=True, 22 | ) 23 | if m.reply_to_message.text.startswith("#trim_video"): 24 | process_type = ProcessTypes.TRIM_VIDEO 25 | else: 26 | process_type = ProcessTypes.MANNUAL_SCREENSHOTS 27 | 28 | c.process_pool.new_task( 29 | ( 30 | m.from_user.id, 31 | ProcessFactory( 32 | process_type=process_type, 33 | client=c, 34 | input_message=m, 35 | reply_message=reply_message, 36 | ), 37 | ) 38 | ) 39 | -------------------------------------------------------------------------------- /bot/plugins/admin/banned_users.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | from pyrogram import filters 4 | 5 | from bot.config import Config 6 | from bot.database import Database 7 | from bot.screenshotbot import ScreenShotBot 8 | 9 | 10 | db = Database() 11 | 12 | 13 | @ScreenShotBot.on_message( 14 | filters.private & filters.command("banned_users") & filters.user(Config.AUTH_USERS) 15 | ) 16 | async def _banned_usrs(c, m): 17 | all_banned_users = await db.get_all_banned_users() 18 | banned_usr_count = 0 19 | text = "" 20 | async for banned_user in all_banned_users: 21 | user_id = banned_user["id"] 22 | ban_duration = banned_user["ban_status"]["ban_duration"] 23 | banned_on = banned_user["ban_status"]["banned_on"] 24 | ban_reason = banned_user["ban_status"]["ban_reason"] 25 | banned_usr_count += 1 26 | text += f"> **user_id**: `{user_id}`, **Ban Duration**: `{ban_duration}`, **Banned on**: " 27 | f"`{banned_on}`, **Reason**: `{ban_reason}`\n\n" 28 | reply_text = f"Total banned user(s): `{banned_usr_count}`\n\n{text}" 29 | if len(reply_text) > 4096: 30 | banned_usrs = io.BytesIO() 31 | banned_usrs.name = "banned-users.txt" 32 | banned_usrs.write(reply_text.encode()) 33 | await m.reply_document(banned_usrs, True) 34 | return 35 | await m.reply_text(reply_text, True) 36 | -------------------------------------------------------------------------------- /bot/plugins/admin/unban_user.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import traceback 3 | 4 | from pyrogram import filters 5 | 6 | from bot.config import Config 7 | from bot.database import Database 8 | from bot.screenshotbot import ScreenShotBot 9 | 10 | 11 | log = logging.getLogger(__name__) 12 | db = Database() 13 | 14 | 15 | @ScreenShotBot.on_message( 16 | filters.private & filters.command("unban_user") & filters.user(Config.AUTH_USERS) 17 | ) 18 | async def unban(c, m): 19 | if len(m.command) == 1: 20 | await m.reply_text( 21 | "Use this command to unban any user.\n\nUsage:\n\n`/unban_user user_id`\n\n" 22 | "Eg: `/unban_user 1234567`\n This will unban user with id `1234567`.", 23 | quote=True, 24 | ) 25 | return 26 | 27 | try: 28 | user_id = int(m.command[1]) 29 | unban_log_text = f"Unbanning user {user_id}" 30 | 31 | try: 32 | await c.send_message(user_id, "Your ban was lifted!") 33 | unban_log_text += "\n\nUser notified successfully!" 34 | except Exception as e: 35 | log.debug(e, exc_info=True) 36 | unban_log_text += ( 37 | f"\n\nUser notification failed! \n\n`{traceback.format_exc()}`" 38 | ) 39 | await db.remove_ban(user_id) 40 | log.debug(unban_log_text) 41 | await m.reply_text(unban_log_text, quote=True) 42 | except Exception as e: 43 | log.error(e, exc_info=True) 44 | await m.reply_text( 45 | f"Error occoured! Traceback given below\n\n`{traceback.format_exc()}`", 46 | quote=True, 47 | ) 48 | -------------------------------------------------------------------------------- /bot/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class Config: 5 | 6 | API_ID = int(os.environ.get("API_ID")) 7 | API_HASH = os.environ.get("API_HASH") 8 | BOT_TOKEN = os.environ.get("BOT_TOKEN") 9 | SESSION_NAME = os.environ.get("SESSION_NAME", ":memory:") 10 | LOG_CHANNEL = int(os.environ.get("LOG_CHANNEL")) 11 | DATABASE_URL = os.environ.get("DATABASE_URL") 12 | AUTH_USERS = [int(i) for i in os.environ.get("AUTH_USERS", "").split(" ")] 13 | MAX_PROCESSES_PER_USER = int(os.environ.get("MAX_PROCESSES_PER_USER", 2)) 14 | MAX_TRIM_DURATION = int(os.environ.get("MAX_TRIM_DURATION", 600)) 15 | TRACK_CHANNEL = int(os.environ.get("TRACK_CHANNEL", False)) 16 | SLOW_SPEED_DELAY = int(os.environ.get("SLOW_SPEED_DELAY", 5)) 17 | HOST = os.environ.get("HOST", "") 18 | TIMEOUT = int(os.environ.get("TIMEOUT", 60 * 30)) 19 | DEBUG = bool(os.environ.get("DEBUG")) 20 | WORKER_COUNT = int(os.environ.get("WORKER_COUNT", 20)) 21 | IAM_HEADER = os.environ.get("IAM_HEADER", "") 22 | 23 | COLORS = [ 24 | "white", 25 | "black", 26 | "red", 27 | "blue", 28 | "green", 29 | "yellow", 30 | "orange", 31 | "purple", 32 | "brown", 33 | "gold", 34 | "silver", 35 | "pink", 36 | ] 37 | FONT_SIZES_NAME = ["Small", "Medium", "Large"] 38 | FONT_SIZES = [30, 40, 50] 39 | POSITIONS = [ 40 | "Top Left", 41 | "Top Center", 42 | "Top Right", 43 | "Center Left", 44 | "Centered", 45 | "Center Right", 46 | "Bottom Left", 47 | "Bottom Center", 48 | "Bottom Right", 49 | ] 50 | -------------------------------------------------------------------------------- /bot/processes/__init__.py: -------------------------------------------------------------------------------- 1 | from bot.utils import ProcessTypes 2 | from .sample import SampleVideoProcess 3 | from .manual_screenshot import ManualScreenshotsProcess 4 | from .trim import TrimVideoProcess 5 | from .screenshot import ScreenshotsProcess 6 | from .mediainfo import MediaInfoProcess 7 | 8 | 9 | class ProcessFactory: 10 | def __init__(self, process_type, client, input_message, reply_message=None): 11 | self.process_type = process_type 12 | self.client = client 13 | self.input_message = input_message 14 | if ( 15 | process_type in [ProcessTypes.TRIM_VIDEO, ProcessTypes.MANNUAL_SCREENSHOTS] 16 | and not reply_message 17 | ): 18 | raise ValueError("reply_message should not be empty for this process type") 19 | self.reply_message = reply_message 20 | 21 | def get_handler(self): 22 | if self.process_type == ProcessTypes.SAMPLE_VIDEO: 23 | return SampleVideoProcess(self.client, self.input_message) 24 | elif self.process_type == ProcessTypes.MANNUAL_SCREENSHOTS: 25 | return ManualScreenshotsProcess(self.client, self.input_message, self.reply_message) 26 | elif self.process_type == ProcessTypes.TRIM_VIDEO: 27 | return TrimVideoProcess(self.client, self.input_message, self.reply_message) 28 | elif self.process_type == ProcessTypes.SCREENSHOTS: 29 | return ScreenshotsProcess(self.client, self.input_message) 30 | elif self.process_type == ProcessTypes.MEDIAINFO: 31 | return MediaInfoProcess(self.client, self.input_message) 32 | else: 33 | raise NotImplementedError 34 | -------------------------------------------------------------------------------- /bot/processes/base.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from bot.screenshotbot import ScreenShotBot 3 | from abc import ABC, abstractmethod 4 | 5 | from bot.config import Config 6 | from bot.messages import Messages as ms 7 | from bot.utils import Utilities 8 | 9 | 10 | class BaseProcess(ABC): 11 | def __init__(self, client, input_message): 12 | self.client = client 13 | self.input_message = input_message 14 | self.chat_id = self.input_message.from_user.id 15 | 16 | self._file_link = None 17 | self._media_message = None 18 | 19 | @property 20 | def file_link(self): 21 | if self._file_link is None: 22 | if self.media_message.media: 23 | self._file_link = asyncio.run(Utilities.generate_stream_link(self.media_message)) 24 | else: 25 | self._file_link = self.media_message.text 26 | return self._file_link 27 | 28 | async def track_user_activity(self): 29 | if Config.TRACK_CHANNEL: 30 | tr_msg = await self.media_message.forward(Config.TRACK_CHANNEL) 31 | await tr_msg.reply_text(ms.TRACK_USER_ACTIVITY.format(chat_id=self.chat_id)) 32 | 33 | @property 34 | def media_message(self): 35 | assert self._media_message is not None 36 | return self._media_message 37 | 38 | @media_message.setter 39 | def media_message(self, val): 40 | assert val is not None 41 | self._media_message = val 42 | 43 | @abstractmethod 44 | async def set_media_message(self): 45 | pass 46 | 47 | @abstractmethod 48 | async def process(self): 49 | pass 50 | 51 | @abstractmethod 52 | async def cancelled(self): 53 | pass 54 | -------------------------------------------------------------------------------- /bot/plugins/urls.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pyrogram import filters 4 | from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton 5 | 6 | from ..utils import Utilities 7 | from ..screenshotbot import ScreenShotBot 8 | from ..config import Config 9 | 10 | 11 | @ScreenShotBot.on_message( 12 | filters.private 13 | & ((filters.text) | filters.media) 14 | & filters.incoming 15 | ) 16 | async def _(c, m): 17 | 18 | if m.media: 19 | if not Utilities.is_valid_file(m): 20 | return 21 | else: 22 | if not Utilities.is_url(m.text): 23 | return 24 | 25 | snt = await m.reply_text( 26 | "Hi there, Please wait while I'm getting everything ready to process your request!", 27 | quote=True, 28 | ) 29 | 30 | if m.media: 31 | await snt.delete() 32 | file_link = await Utilities.generate_stream_link(m) 33 | snt = await m.reply_text( 34 | "Getting Your data...", 35 | quote=True, 36 | ) 37 | else: 38 | file_link = m.text 39 | 40 | duration = await Utilities.get_duration(file_link) 41 | if isinstance(duration, str): 42 | await snt.edit_text("😟 Sorry! I cannot open the file.") 43 | log = await m.forward(Config.LOG_CHANNEL) 44 | await log.reply_text(duration, True) 45 | return 46 | 47 | btns = Utilities.gen_ik_buttons() 48 | 49 | if duration >= 600: 50 | btns.append([InlineKeyboardButton("Generate Sample Video", "smpl")]) 51 | 52 | await snt.edit_text( 53 | text=f"__Choose one of the options 🧐.__\n\n**⏰ Total duration:** `{datetime.timedelta(seconds=duration)}` (`{duration}s`)", 54 | reply_markup=InlineKeyboardMarkup(btns), 55 | ) 56 | -------------------------------------------------------------------------------- /bot/plugins/admin/ban_user.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import traceback 3 | 4 | from pyrogram import filters 5 | 6 | from bot.config import Config 7 | from bot.screenshotbot import ScreenShotBot 8 | from bot.database import Database 9 | 10 | 11 | log = logging.getLogger(__name__) 12 | db = Database() 13 | 14 | 15 | @ScreenShotBot.on_message( 16 | filters.private & filters.command("ban_user") & filters.user(Config.AUTH_USERS) 17 | ) 18 | async def ban(c, m): 19 | 20 | if len(m.command) == 1: 21 | await m.reply_text( 22 | "Use this command to ban any user from the bot.\n\nUsage:\n\n`/ban_user user_id " 23 | "ban_duration ban_reason`\n\nEg: `/ban_user 1234567 28 You misused me.`\n This will " 24 | "ban user with id `1234567` for `28` days for the reason `You misused me`.", 25 | quote=True, 26 | ) 27 | return 28 | 29 | try: 30 | user_id = int(m.command[1]) 31 | ban_duration = int(m.command[2]) 32 | ban_reason = " ".join(m.command[3:]) 33 | ban_log_text = f"Banning user {user_id} for {ban_duration} day(s) for the reason {ban_reason}." 34 | 35 | try: 36 | await c.send_message( 37 | user_id, 38 | f"You are banned to use this bot for **{ban_duration}** day(s) for " 39 | f"the reason __{ban_reason}__ \n\n**Message from the admin**", 40 | ) 41 | ban_log_text += "\n\nUser notified successfully!" 42 | except Exception as e: 43 | log.debug(e, exc_info=True) 44 | ban_log_text += ( 45 | f"\n\nUser notification failed! \n\n`{traceback.format_exc()}`" 46 | ) 47 | await db.ban_user(user_id, ban_duration, ban_reason) 48 | log.debug(ban_log_text) 49 | await m.reply_text(ban_log_text, quote=True) 50 | except Exception as e: 51 | log.error(e, exc_info=True) 52 | await m.reply_text( 53 | f"Error occoured! Traceback given below\n\n`{traceback.format_exc()}`", 54 | quote=True, 55 | ) 56 | -------------------------------------------------------------------------------- /bot/plugins/mediainfo.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import logging 3 | import os 4 | 5 | from pyrogram import filters 6 | from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup 7 | import aiohttp 8 | 9 | from bot.screenshotbot import ScreenShotBot 10 | from bot.messages import Messages as ms 11 | from bot.config import Config 12 | from bot.utils import ProcessTypes 13 | from bot.processes import ProcessFactory 14 | 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | @ScreenShotBot.on_callback_query( 20 | filters.create(lambda _, __, query: query.data.startswith("mi")) 21 | ) 22 | async def _(c, m): 23 | try: 24 | await m.answer() 25 | except Exception: 26 | pass 27 | 28 | await m.edit_message_text( 29 | ms.ADDED_TO_QUEUE.format(per_user_process_count=Config.MAX_PROCESSES_PER_USER), 30 | ) 31 | c.process_pool.new_task( 32 | ( 33 | m.from_user.id, 34 | ProcessFactory( 35 | process_type=ProcessTypes.MEDIAINFO, client=c, input_message=m 36 | ), 37 | ) 38 | ) 39 | 40 | 41 | @ScreenShotBot.on_callback_query( 42 | filters.create(lambda _, __, query: query.data.startswith("webmi")) 43 | ) 44 | async def __(c, m): 45 | # https://github.com/eyaadh/megadlbot_oss/blob/306fb21dbdbdc8dc17294a6cb7b7cdafb11e44da/mega/helpers/media_info.py#L30 46 | try: 47 | await m.answer() 48 | except Exception: 49 | pass 50 | 51 | with tempfile.TemporaryDirectory() as temp_dir: 52 | temp_file_name = os.path.join(temp_dir, "mediainfo.txt") 53 | media_info = await m.message.download(temp_file_name) 54 | neko_endpoint = "https://nekobin.com/api/documents" 55 | async with aiohttp.ClientSession() as nekoSession: 56 | payload = {"content": open(media_info, "r").read()} 57 | async with nekoSession.post(neko_endpoint, data=payload) as resp: 58 | resp = await resp.json() 59 | neko_link = f"https://nekobin.com/{resp['result']['key']}" 60 | logger.debug(neko_link) 61 | await m.edit_message_reply_markup( 62 | InlineKeyboardMarkup([[InlineKeyboardButton("Web URL", url=neko_link)]]) 63 | ) 64 | -------------------------------------------------------------------------------- /bot/workers/worker.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from collections import defaultdict 4 | from contextlib import asynccontextmanager 5 | 6 | from async_timeout import timeout 7 | 8 | from bot.config import Config 9 | 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class TooMuchProcess(Exception): 15 | pass 16 | 17 | 18 | class Worker: 19 | def __init__(self): 20 | self.worker_count = Config.WORKER_COUNT 21 | self.user_process_count = defaultdict(lambda: 0) 22 | self.queue = asyncio.Queue() 23 | 24 | async def start(self): 25 | for _ in range(self.worker_count): 26 | asyncio.create_task(self._worker()) 27 | logger.debug("Started %s workers", self.worker_count) 28 | 29 | async def stop(self): 30 | logger.debug("Stopping workers") 31 | for _ in range(self.worker_count): 32 | self.new_task(None) 33 | await self.queue.join() 34 | logger.debug("Stopped workers") 35 | 36 | def new_task(self, task): 37 | self.queue.put_nowait(task) 38 | 39 | @asynccontextmanager 40 | async def count_user_process(self, chat_id): 41 | if self.user_process_count[chat_id] >= Config.MAX_PROCESSES_PER_USER: 42 | raise TooMuchProcess 43 | 44 | self.user_process_count[chat_id] += 1 45 | try: 46 | yield 47 | finally: 48 | self.user_process_count[chat_id] -= 1 49 | 50 | async def _worker(self): 51 | while True: 52 | task = await self.queue.get() 53 | try: 54 | if task is None: 55 | break 56 | 57 | chat_id, process_factory = task 58 | handler = process_factory.get_handler() 59 | try: 60 | async with self.count_user_process(chat_id), timeout( 61 | Config.TIMEOUT 62 | ): 63 | await handler.process() 64 | except (asyncio.TimeoutError, asyncio.CancelledError): 65 | await handler.cancelled() 66 | except TooMuchProcess: 67 | await asyncio.sleep(10) 68 | self.new_task((chat_id, process_factory)) 69 | 70 | except Exception as e: 71 | logger.error(e, exc_info=True) 72 | finally: 73 | self.queue.task_done() 74 | -------------------------------------------------------------------------------- /bot/plugins/1.py: -------------------------------------------------------------------------------- 1 | import time 2 | import datetime 3 | 4 | from pyrogram import filters 5 | from bot.utils import Utilities 6 | from bot.screenshotbot import ScreenShotBot 7 | from bot.config import Config 8 | from bot.database import Database 9 | 10 | 11 | db = Database() 12 | 13 | 14 | @ScreenShotBot.on_callback_query() 15 | async def __(c, m): 16 | await foo(c, m, cb=True) 17 | 18 | 19 | @ScreenShotBot.on_message(filters.private) 20 | async def _(c, m): 21 | await foo(c, m) 22 | 23 | 24 | async def foo(c, m, cb=False): 25 | chat_id = m.from_user.id 26 | consumed_time = int(time.time()) - c.CHAT_FLOOD[chat_id] 27 | if consumed_time < Config.SLOW_SPEED_DELAY: 28 | wait_time = Config.SLOW_SPEED_DELAY - consumed_time 29 | text = f"⏱ Please wait {Utilities.TimeFormatter(seconds=wait_time)}, " 30 | text += f"there is a delay of {Utilities.TimeFormatter(seconds=Config.SLOW_SPEED_DELAY)} between " 31 | text += "requests to reduce overload. \n\nSo kindly please cooperate with us." 32 | 33 | if cb: 34 | if not m.data.startswith("set") and m.data not in ['home', 'help', 'close']: 35 | try: 36 | if consumed_time < Config.SLOW_SPEED_DELAY: 37 | return await m.answer(text, show_alert=True) 38 | else: 39 | c.CHAT_FLOOD[chat_id] = int(time.time()) 40 | except: 41 | pass 42 | else: 43 | if (m.text and not m.text.startswith("/")) or (m.caption): 44 | try: 45 | if consumed_time < Config.SLOW_SPEED_DELAY: 46 | return await m.reply_text(text, quote=True) 47 | else: 48 | c.CHAT_FLOOD[chat_id] = int(time.time()) 49 | except: 50 | pass 51 | 52 | 53 | if not await db.is_user_exist(chat_id): 54 | await db.add_user(chat_id) 55 | await c.send_message(Config.LOG_CHANNEL, f"New User {m.from_user.mention}.") 56 | 57 | ban_status = await db.get_ban_status(chat_id) 58 | if ban_status["is_banned"]: 59 | if ( 60 | datetime.date.today() - datetime.date.fromisoformat(ban_status["banned_on"]) 61 | ).days > ban_status["ban_duration"]: 62 | await db.remove_ban(chat_id) 63 | else: 64 | return 65 | 66 | last_used_on = await db.get_last_used_on(chat_id) 67 | if last_used_on != datetime.date.today().isoformat(): 68 | await db.update_last_used_on(chat_id) 69 | 70 | await m.continue_propagation() 71 | -------------------------------------------------------------------------------- /bot/plugins/start.py: -------------------------------------------------------------------------------- 1 | from pyrogram import filters 2 | from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton 3 | 4 | from bot.config import Config 5 | from ..screenshotbot import ScreenShotBot 6 | 7 | 8 | @ScreenShotBot.on_message(filters.private & filters.command("start")) 9 | async def start(c, m, cb=False): 10 | owner_id = Config.AUTH_USERS[0] 11 | username = 'Ns_AnoNymous' 12 | mention = '[Anonymous](https://t.me/Ns_AnoNymous)' 13 | try: 14 | owner = await c.get_users(owner_id) 15 | username = owner.username if owner.username else 'Ns_AnoNymous' 16 | mention = owner.mention(style="md") 17 | except Exception as e: 18 | print(e) 19 | 20 | BUTTONS = [[ 21 | InlineKeyboardButton("My Father 🧔", url=f"https://t.me/{username}"), 22 | InlineKeyboardButton("Updates Channel 🔰", url="https://telegram.dog/NsBotsOfficial") 23 | ],[ 24 | InlineKeyboardButton("Source code 😎", url="https://github.com/Ns-AnoNymouS/animated-lamp") 25 | ],[ 26 | InlineKeyboardButton("Help ⁉️", callback_data="help"), 27 | InlineKeyboardButton("Settings ⚙", callback_data="set+settings") 28 | ],[ 29 | InlineKeyboardButton("Close 📛", callback_data="close") 30 | ]] 31 | 32 | TEXT = f"👋 Hi {m.from_user.mention},\n\nI'm Screenshot Generator Bot. I can provide screenshots, sample video from " 33 | TEXT += "your video files and also can trim. For more details check help.\n\n" 34 | TEXT += f"**Maintained By:** {mention}" 35 | 36 | if cb: 37 | try: 38 | await m.message.edit( 39 | text=TEXT, 40 | reply_markup=InlineKeyboardMarkup(BUTTONS) 41 | ) 42 | except: 43 | pass 44 | else: 45 | await m.reply_text( 46 | text=TEXT, 47 | quote=True, 48 | reply_markup=InlineKeyboardMarkup(BUTTONS) 49 | ) 50 | 51 | 52 | # i generally liked to use regex filters for callback 53 | # but since odysseusmax used lambda i am also using the same 54 | @ScreenShotBot.on_callback_query( 55 | filters.create(lambda _, __, query: query.data.startswith("home")) 56 | ) 57 | async def home_cb(c, m): 58 | await m.answer() 59 | await start(c, m, True) 60 | 61 | 62 | @ScreenShotBot.on_callback_query( 63 | filters.create(lambda _, __, query: query.data.startswith("close")) 64 | ) 65 | async def close_cb(c, m): 66 | try: 67 | await m.message.delete() 68 | await m.message.reply_to_message.delete() 69 | except: 70 | pass 71 | -------------------------------------------------------------------------------- /bot/plugins/help.py: -------------------------------------------------------------------------------- 1 | from pyrogram import filters 2 | from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup 3 | 4 | from bot.screenshotbot import ScreenShotBot 5 | from bot.config import Config 6 | 7 | 8 | BUTTONS = [[ 9 | InlineKeyboardButton('Home 🏡', callback_data='home'), 10 | InlineKeyboardButton('Close 📛', callback_data='close') 11 | ]] 12 | 13 | HELP_TEXT = """ 14 | Hi {mention}. Welcome to Screenshot Generator Bot. You can use me to generate: 15 | 16 | 1. Screenshots. 17 | 2. Sample Video. 18 | 3. Trim Video. 19 | 20 | 👉 I support any kind of **telegram video file** (streaming video or document video files) provided it --has proper mime-type-- and --is not corrupted--. 21 | 👉 I also support **Streaming URLs**. The URL should be a --streaming URL--, --non IP specific--, and --should return proper response codes--. 22 | Just send me the telegram file or the streaming URL. 23 | 24 | See /settings to configure bot's behavior. 25 | Use /set_watermark to set custom watermarks to your screenshots. 26 | 27 | **General FAQ.** 28 | 29 | 👉 If the bot dosen't respond to telegram files you forward, first check /start and --confirm bot is alive--. Then make sure the file is a **video file** which satisfies above mentioned conditions. 30 | 👉 If bot replies __😟 Sorry! I cannot open the file.__, the file might be --currupted-- or --is malformatted--. 31 | 32 | __If issues persists contact my father.__ 33 | 34 | {admin_notification} 35 | """ 36 | ADMIN_NOTIFICATION_TEXT = ( 37 | "Since you are one of the admins, you can check /admin to view the admin commands." 38 | ) 39 | 40 | 41 | @ScreenShotBot.on_message(filters.private & filters.command("help")) 42 | async def help_(c, m): 43 | 44 | await m.reply_text( 45 | text=HELP_TEXT.format( 46 | mention=m.from_user.mention, 47 | admin_notification=ADMIN_NOTIFICATION_TEXT 48 | if m.from_user.id in Config.AUTH_USERS 49 | else "", 50 | ), 51 | reply_markup=InlineKeyboardMarkup(BUTTONS), 52 | quote=True, 53 | ) 54 | 55 | 56 | @ScreenShotBot.on_callback_query( 57 | filters.create(lambda _, __, query: query.data.startswith("help")) 58 | ) 59 | async def help_cb(c, m): 60 | await m.answer() 61 | await m.message.edit( 62 | text=HELP_TEXT.format( 63 | mention=m.from_user.mention, 64 | admin_notification=ADMIN_NOTIFICATION_TEXT 65 | if m.from_user.id in Config.AUTH_USERS 66 | else "", 67 | ), 68 | reply_markup=InlineKeyboardMarkup(BUTTONS) 69 | ) 70 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Screenshot Bot", 3 | "description": "Screenshot /Sample Video / Trim Video files", 4 | "logo": "https://telegra.ph/file/a3a32dcceb8201635c1f0.jpg", 5 | "stack": "heroku-20", 6 | "keywords": [ 7 | "telegram", 8 | "Screenshot", 9 | "Sample video", 10 | "Trim Video" 11 | ], 12 | "repository": "https://github.com/Ns-AnoNymouS/screenshot-bot", 13 | "env": { 14 | "API_ID": { 15 | "description": "Your Telegram API ID. Get this value from my.telegram.org!", 16 | "value": "" 17 | }, 18 | "API_HASH": { 19 | "description": " Your Telegram API HASH. Get this value from my.telegram.org!", 20 | "value": "" 21 | }, 22 | "BOT_TOKEN": { 23 | "description": "Your Bot token, as a string.", 24 | "value": "" 25 | }, 26 | "SESSION_NAME": { 27 | "description": "Name you want to call your bot's session, Eg: bot username.", 28 | "value": "" 29 | }, 30 | "LOG_CHANNEL": { 31 | "description": "Log channel's id.", 32 | "value": "" 33 | }, 34 | "DATABASE_URL": { 35 | "description": "Mongodb database URI from https://cloud.mongodb.com/", 36 | "value": "" 37 | }, 38 | "AUTH_USERS": { 39 | "description": "Authorised user(s) id separated by space.", 40 | "value": "" 41 | }, 42 | "MAX_PROCESSES_PER_USER": { 43 | "description": "Number of parallel processes each user can have, defaults to 2.", 44 | "value": "2", 45 | "required": false 46 | }, 47 | "MAX_TRIM_DURATION": { 48 | "description": "Maximum allowed seconds for trimming. Defaults to 600.", 49 | "value": "600", 50 | "required": false 51 | }, 52 | "TRACK_CHANNEL": { 53 | "description": "User activity tracking channel's id. Only needed if you want to track and block any user. Disabled by default.", 54 | "required": false 55 | }, 56 | "SLOW_SPEED_DELAY": { 57 | "description": "Delay required between each request. Defaults to 5s.", 58 | "value": "5", 59 | "required": false 60 | }, 61 | "TIMEOUT": { 62 | "description": "Maximum time alloted to each process in seconds, after which process will be cancelled. Defaults to 1800s(30 mins)", 63 | "value": "1800", 64 | "required": false 65 | }, 66 | "DEBUG": { 67 | "description": "Set some value to use DEBUG logging level. INFO by default", 68 | "required": false 69 | }, 70 | "WORKER_COUNT": { 71 | "description": "Maximum number of processes should bot handle at a time to control overload on bot default value is 20.", 72 | "required": false 73 | } 74 | }, 75 | "addons": [ 76 | ], 77 | "buildpacks": [{ 78 | "url": "https://github.com/jonathanong/heroku-buildpack-ffmpeg-latest" 79 | }, { 80 | "url": "heroku/python" 81 | }] 82 | } 83 | -------------------------------------------------------------------------------- /bot/processes/mediainfo.py: -------------------------------------------------------------------------------- 1 | import io 2 | import time 3 | import logging 4 | import datetime 5 | 6 | from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton 7 | 8 | from bot.config import Config 9 | from bot.utils import Utilities 10 | from bot.messages import Messages as ms 11 | from .exception import BaseException 12 | from .base import BaseProcess 13 | 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | 18 | class MediaInfoProcessFailure(BaseException): 19 | pass 20 | 21 | 22 | class MediaInfoProcess(BaseProcess): 23 | async def cancelled(self): 24 | await self.input_message.edit_message_text(ms.PROCESS_TIMEOUT) 25 | 26 | async def set_media_message(self): 27 | self.media_message = self.input_message.message.reply_to_message 28 | 29 | async def process(self): 30 | await self.set_media_message() 31 | await self.input_message.edit_message_text(ms.PROCESSING_REQUEST) 32 | try: 33 | if self.media_message.empty: 34 | raise MediaInfoProcessFailure( 35 | for_user=ms.MEDIA_MESSAGE_DELETED, 36 | for_admin=ms.MEDIA_MESSAGE_DELETED, 37 | ) 38 | 39 | await self.track_user_activity() 40 | start_time = time.time() 41 | await self.input_message.edit_message_text(ms.MEDIAINFO_START) 42 | 43 | log.info( 44 | "Generating mediainfo from %s for %s", 45 | self.file_link, 46 | self.chat_id, 47 | ) 48 | 49 | media_info = await Utilities.get_media_info(self.file_link) 50 | log.debug(media_info) 51 | media_info_file = io.BytesIO() 52 | media_info_file.name = "mediainfo.json" 53 | media_info_file.write(media_info) 54 | 55 | await self.media_message.reply_document( 56 | document=media_info_file, 57 | quote=True, 58 | reply_markup=InlineKeyboardMarkup( 59 | [[InlineKeyboardButton("Get Web URL", callback_data="webmi")]] 60 | ), 61 | ) 62 | 63 | await self.input_message.edit_message_text( 64 | ms.PROCESS_UPLOAD_CONFIRM.format( 65 | total_process_duration=datetime.timedelta( 66 | seconds=int(time.time() - start_time) 67 | ) 68 | ) 69 | ) 70 | try: 71 | os.remove(self.file_link) 72 | except: pass 73 | 74 | except MediaInfoProcessFailure as e: 75 | log.error(e) 76 | await self.input_message.edit_message_text(text=e.for_user) 77 | log_msg = await self.media_message.forward(Config.LOG_CHANNEL) 78 | await log_msg.reply_text( 79 | e.for_admin, 80 | quote=True, 81 | ) 82 | -------------------------------------------------------------------------------- /bot/screenshotbot.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | import logging 3 | import time 4 | import string 5 | import random 6 | import asyncio 7 | from contextlib import contextmanager 8 | 9 | from pyrogram import Client 10 | from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton 11 | 12 | from bot.config import Config 13 | from bot.workers import Worker 14 | from bot.utils.broadcast import Broadcast 15 | 16 | 17 | log = logging.getLogger(__name__) 18 | 19 | 20 | class ScreenShotBot(Client): 21 | def __init__(self): 22 | super().__init__( 23 | name=Config.SESSION_NAME, 24 | bot_token=Config.BOT_TOKEN, 25 | api_id=Config.API_ID, 26 | api_hash=Config.API_HASH, 27 | plugins=dict(root="bot/plugins"), 28 | ) 29 | self.process_pool = Worker() 30 | self.CHAT_FLOOD = defaultdict( 31 | lambda: int(time.time()) - Config.SLOW_SPEED_DELAY - 1 32 | ) 33 | self.broadcast_ids = {} 34 | 35 | async def start(self): 36 | await super().start() 37 | await self.process_pool.start() 38 | me = await self.get_me() 39 | print(f"New session started for {me.first_name}({me.username})") 40 | 41 | async def stop(self): 42 | await self.process_pool.stop() 43 | await super().stop() 44 | print("Session stopped. Bye!!") 45 | 46 | @contextmanager 47 | def track_broadcast(self, handler): 48 | broadcast_id = "" 49 | while True: 50 | broadcast_id = "".join( 51 | random.choice(string.ascii_letters) for _ in range(3) 52 | ) 53 | if broadcast_id not in self.broadcast_ids: 54 | break 55 | 56 | self.broadcast_ids[broadcast_id] = handler 57 | try: 58 | yield broadcast_id 59 | finally: 60 | self.broadcast_ids.pop(broadcast_id) 61 | 62 | async def start_broadcast(self, broadcast_message, admin_id): 63 | asyncio.create_task(self._start_broadcast(broadcast_message, admin_id)) 64 | 65 | async def _start_broadcast(self, broadcast_message, admin_id): 66 | try: 67 | broadcast_handler = Broadcast( 68 | client=self, broadcast_message=broadcast_message 69 | ) 70 | with self.track_broadcast(broadcast_handler) as broadcast_id: 71 | reply_message = await self.send_message( 72 | chat_id=admin_id, 73 | text="Broadcast started. Use the buttons to check the progress or to cancel the broadcast.", 74 | reply_to_message_id=broadcast_message.message_id, 75 | reply_markup=InlineKeyboardMarkup( 76 | [[ 77 | InlineKeyboardButton( 78 | text="Check Progress", 79 | callback_data=f"sts_bdct+{broadcast_id}", 80 | ), 81 | InlineKeyboardButton( 82 | text="Cancel!", 83 | callback_data=f"cncl_bdct+{broadcast_id}", 84 | ), 85 | ]] 86 | ), 87 | ) 88 | 89 | await broadcast_handler.start() 90 | 91 | await reply_message.edit_text("Broadcast completed") 92 | except Exception as e: 93 | log.error(e, exc_info=True) 94 | -------------------------------------------------------------------------------- /bot/plugins/settings_cb.py: -------------------------------------------------------------------------------- 1 | from pyrogram import filters 2 | 3 | from bot.screenshotbot import ScreenShotBot 4 | from bot.utils import Utilities 5 | from bot.config import Config 6 | from bot.database import Database 7 | 8 | 9 | db = Database() 10 | 11 | 12 | @ScreenShotBot.on_callback_query( 13 | filters.create(lambda _, __, query: query.data.startswith("set")) 14 | ) 15 | async def settings_cb(c, m): 16 | try: 17 | _, typ, action = m.data.split("+") # Reverse compatibility. 18 | except Exception: 19 | _, typ = m.data.split("+") 20 | chat_id = m.from_user.id 21 | alert_text = None 22 | 23 | if typ == "af": 24 | as_file = await db.is_as_file(chat_id) 25 | await db.update_as_file(chat_id, not as_file) 26 | alert_text = "Successfully changed screenshot upload mode!" 27 | 28 | elif typ == "wm": 29 | watermark_text = await db.get_watermark_text(chat_id) 30 | if watermark_text: 31 | await db.update_watermark_text(chat_id) 32 | alert_text = "Successfully removed watermark text." 33 | else: 34 | alert_text = "Use /set_watermark to add new watermark text." 35 | await m.answer(alert_text, show_alert=True) 36 | 37 | elif typ == "sv": 38 | sample_duration = await db.get_sample_duration(chat_id) 39 | if sample_duration + 30 >= 180: 40 | sample_duration = 0 41 | sample_duration += 30 42 | await db.update_sample_duration(chat_id, sample_duration) 43 | alert_text = f"Sample video duration changed to {sample_duration}s" 44 | 45 | elif typ == "wc": 46 | watermark_color_code = await db.get_watermark_color(chat_id) 47 | if watermark_color_code + 1 == len(Config.COLORS): 48 | watermark_color_code = -1 49 | watermark_color_code += 1 50 | await db.update_watermark_color(chat_id, watermark_color_code) 51 | alert_text = f"Successfully changed watermark text color to {Config.COLORS[watermark_color_code]}" 52 | 53 | elif typ == "sm": 54 | screenshot_mode = await db.get_screenshot_mode(chat_id) 55 | if screenshot_mode == 0: 56 | screenshot_mode = 1 57 | else: 58 | screenshot_mode = 0 59 | await db.update_screenshot_mode(chat_id, screenshot_mode) 60 | alert_text = "Successfully changed screenshot generation mode" 61 | 62 | elif typ == "fs": 63 | font_size = await db.get_font_size(chat_id) 64 | if font_size == len(Config.FONT_SIZES) - 1: 65 | font_size = -1 66 | font_size += 1 67 | await db.update_font_size(chat_id, font_size) 68 | alert_text = ( 69 | f"Successfully changed font size to {Config.FONT_SIZES_NAME[font_size]}" 70 | ) 71 | elif typ == "wp": 72 | current_pos = await db.get_watermark_position(chat_id) 73 | if current_pos == len(Config.POSITIONS) - 1: 74 | current_pos = -1 75 | current_pos += 1 76 | await db.update_watermark_position(chat_id, current_pos) 77 | alert_text = f"Successfully changed watermark position to {Config.POSITIONS[current_pos]}" 78 | 79 | 80 | #i dont like this alert if you want you can add so that i am not removing anything and commented them 81 | await m.answer() #alert_text, show_alert=True) 82 | 83 | await Utilities.display_settings(c, m, db, cb=True) 84 | 85 | 86 | @ScreenShotBot.on_callback_query( 87 | filters.create(lambda _, __, query: query.data.startswith("rj")) 88 | ) 89 | async def _(c, m): 90 | await m.answer("😂 press the other button 😂") 91 | -------------------------------------------------------------------------------- /bot/utils/broadcast.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | import datetime 3 | import logging 4 | import asyncio 5 | import time 6 | import io 7 | 8 | from pyrogram.errors import ( 9 | FloodWait, 10 | InputUserDeactivated, 11 | UserIsBlocked, 12 | PeerIdInvalid, 13 | ) 14 | 15 | from bot.database import Database 16 | from bot.config import Config 17 | 18 | 19 | log = logging.getLogger(__name__) 20 | db = Database() 21 | 22 | 23 | class Broadcast: 24 | def __init__(self, client, broadcast_message): 25 | self.client = client 26 | self.broadcast_message = broadcast_message 27 | 28 | self.cancelled = False 29 | self.progress = dict(total=0, current=0, failed=0, success=0) 30 | 31 | def get_progress(self): 32 | return self.progress 33 | 34 | def cancel(self): 35 | self.cancelled = True 36 | 37 | async def _send_msg(self, user_id): 38 | try: 39 | await self.broadcast_message.copy(chat_id=user_id) 40 | return 200, None 41 | except FloodWait as e: 42 | await asyncio.sleep(e.value + 1) 43 | return self._send_msg(user_id) 44 | except InputUserDeactivated as e: 45 | log.error(e) 46 | return 400, f"{user_id} : deactivated\n" 47 | except UserIsBlocked as e: 48 | log.error(e) 49 | return 400, f"{user_id} : blocked the bot\n" 50 | except PeerIdInvalid as e: 51 | log.error(e) 52 | return 400, f"{user_id} : user id invalid\n" 53 | except Exception as e: 54 | log.error(e, exc_info=True) 55 | return 500, f"{user_id} : {traceback.format_exc()}\n" 56 | 57 | async def start(self): 58 | all_users = await db.get_all_users() 59 | 60 | start_time = time.time() 61 | total_users = await db.total_users_count() 62 | done = 0 63 | failed = 0 64 | success = 0 65 | 66 | log_file = io.BytesIO() 67 | log_file.name = f"{datetime.datetime.utcnow()}_broadcast.txt" 68 | broadcast_log = "" 69 | async for user in all_users: 70 | await asyncio.sleep(0.5) 71 | sts, msg = await self._send_msg(user_id=int(user["id"])) 72 | if msg is not None: 73 | broadcast_log += msg 74 | 75 | if sts == 200: 76 | success += 1 77 | else: 78 | failed += 1 79 | 80 | if sts == 400: 81 | await db.delete_user(user["id"]) 82 | 83 | done += 1 84 | self.progress.update(dict(current=done, failed=failed, success=success)) 85 | if self.cancelled: 86 | break 87 | 88 | log_file.write(broadcast_log.encode()) 89 | completed_in = datetime.timedelta(seconds=int(time.time() - start_time)) 90 | await asyncio.sleep(3) 91 | update_text = ( 92 | f"#broadcast completed in `{completed_in}`\n\nTotal users {total_users}.\n" 93 | f"Total done {done}, {success} success and {failed} failed.\n" 94 | "📋 Status: {}".format("Completed" if not self.cancelled else "Cancelled") 95 | ) 96 | 97 | if failed == 0: 98 | await self.client.send_message( 99 | chat_id=Config.LOG_CHANNEL, 100 | text=update_text, 101 | ) 102 | else: 103 | await self.client.send_document( 104 | chat_id=Config.LOG_CHANNEL, 105 | document=log_file, 106 | caption=update_text, 107 | ) 108 | -------------------------------------------------------------------------------- /bot/messages.py: -------------------------------------------------------------------------------- 1 | class Messages: 2 | ADDED_TO_QUEUE = ( 3 | "Your request has been added to the queue. If you have more than {per_user_process_count} " 4 | "ongoing processes, then this process will only start after one of them finishes." 5 | ) 6 | MEDIA_MESSAGE_DELETED = "Why did you delete the file 😠, Now i cannot help you 😒." 7 | CANNOT_OPEN_FILE = "😟 Sorry! I cannot open the file." 8 | PROCESS_TIMEOUT = ( 9 | "😟 Sorry! process failed due to timeout. Your process was " 10 | "taking too long to complete, hence cancelled." 11 | ) 12 | TRACK_USER_ACTIVITY = "User id: `{chat_id}`" 13 | PROCESSING_REQUEST = "Processing your request, Please wait! 😴" 14 | SCREENSHOT_AT = "ScreenShot at {time}" 15 | SCREENSHOT_PROCESS_FAILED = "😟 Sorry! Screenshot generation failed possibly due to some infrastructure failure 😥." 16 | SCREENSHOT_PROCESS_SUCCESS = ( 17 | "🤓 You requested {count} screenshots and " 18 | "{total_count} screenshots generated, " 19 | "Now starting to upload!" 20 | ) 21 | PROCESS_UPLOAD_CONFIRM = ( 22 | "Successfully completed process in {total_process_duration}\n\n" 23 | "If You find me helpful, please rate me [here](tg://resolve?domain=botsarchive&post=1206)." 24 | ) 25 | WRONG_FORMAT = "Please follow the specified format" 26 | VIDEO_PROCESS_CAPTION = "Sample video. {duration}s from {start}" 27 | SCREENSHOTS_START = "😀 Generating screenshots!." 28 | 29 | SAMPLE_VIDEO_PROCESS_START = "😀 Generating Sample Video! This might take some time." 30 | SAMPLE_VIDEO_PROCESS_FAILED = "😟 Sorry! Sample video generation failed possibly due to some infrastructure failure 😥." 31 | SAMPLE_VIDEO_PROCESS_SUCCESS = ( 32 | "🤓 Sample video was generated successfully!, Now starting to upload!" 33 | ) 34 | SAMPLE_VIDEO_PROCESS_FAILED_GENERATION = ( 35 | "stream link : {file_link}\n\n duration {sample_duration} sample video " 36 | "generation failed\n\n{ffmpeg_output}" 37 | ) 38 | SAMPLE_VIDEO_PROCESS_OPEN_ERROR = ( 39 | "stream link : {file_link}\n\nSample video requested\n\n{duration}" 40 | ) 41 | 42 | SCREENSHOTS_PROGRESS = "😀 `{current}` of `{total}` generated!" 43 | MANUAL_SCREENSHOTS_OPEN_ERROR = ( 44 | "stream link : {file_link}\n\nRequested manual screenshots\n\n{duration}" 45 | ) 46 | MANUAL_SCREENSHOTS_NO_VALID_POSITIONS = ( 47 | "😟 Sorry! None of the given positions where valid!" 48 | ) 49 | MANUAL_SCREENSHOTS_VALID_PISITIONS_ABOVE_LIMIT = ( 50 | "😟 Sorry! Only 10 screenshots can be generated. Found {valid_positions_count} " 51 | "valid positions in your request" 52 | ) 53 | MANUAL_SCREENSHOTS_INVALID_POSITIONS_ALERT = ( 54 | "Found {invalid_positions_count} invalid positions ({invalid_positions}).\n\n" 55 | "😀 Generating screenshots after ignoring these!." 56 | ) 57 | MANUAL_SCREENSHOTS_FAILED_GENERATION = ( 58 | "stream link : {file_link}\n\nmanual screenshots {raw_user_input}." 59 | ) 60 | 61 | TRIM_VIDEO_INVALID_RANGE = "The range you provided is invalid!" 62 | TRIM_VIDEO_DURATION_ERROR = ( 63 | "Please provide any range that's upto {max_duration}s." 64 | " Your requested range **{start}:{end}** is `{request_duration}s` long!" 65 | ) 66 | TRIM_VIDEO_OPEN_ERROR = "stream link : {file_link}\n\ntrim video requested\n\n{start}:{end}\n\n{duration}" 67 | TRIM_VIDEO_RANGE_OUT_OF_VIDEO_DURATION = ( 68 | "😟 Sorry! The requested range is out of the video's duration!." 69 | ) 70 | TRIM_VIDEO_PROCESS_FAILED = ( 71 | "😟 Sorry! video trimming failed possibly due to some infrastructure failure 😥." 72 | ) 73 | TRIM_VIDEO_PROCESS_FAILED_GENERATION = "stream link : {file_link}\n\nVideo trim failed.\n\n{start}:{end}\n\n{ffmpeg_output}" 74 | TRIM_VIDEO_PROCESS_SUCCESS = ( 75 | "🤓 Video trimmed successfully!, Now starting to upload!" 76 | ) 77 | TRIM_VIDEO_START = "😀 Trimming Your Video! This might take some time." 78 | 79 | SCREENSHOTS_OPEN_ERROR = "stream link : {file_link}\n\nRequested screenshots: {num_screenshots}.\n\n{duration}" 80 | SCREENSHOTS_FAILED_GENERATION = ( 81 | "stream link : {file_link}\n\n{num_screenshots} screenshots where requested " 82 | "and Screen shots where not generated." 83 | ) 84 | 85 | SETTINGS = "Here You can configure my behavior.\n\nPress the button to change the settings." 86 | 87 | MEDIAINFO_START = "Finding the media info, media info will be send here shortly!" 88 | -------------------------------------------------------------------------------- /bot/processes/sample.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import logging 4 | import tempfile 5 | import datetime 6 | 7 | from bot.config import Config 8 | from bot.utils import Utilities 9 | from bot.messages import Messages as ms 10 | from bot.database import Database 11 | from .exception import BaseException 12 | from .base import BaseProcess 13 | 14 | 15 | log = logging.getLogger(__name__) 16 | db = Database() 17 | 18 | 19 | class SampleVideoProcessFailure(BaseException): 20 | pass 21 | 22 | 23 | class SampleVideoProcess(BaseProcess): 24 | async def set_media_message(self): 25 | self.media_message = self.input_message.message.reply_to_message 26 | 27 | async def cancelled(self): 28 | await self.input_message.edit_message_text(ms.PROCESS_TIMEOUT) 29 | 30 | async def process(self): 31 | async def upload_notify(*args): 32 | await self.client.send_chat_action(self.chat_id, "upload_video") 33 | 34 | await self.set_media_message() 35 | await self.input_message.edit_message_text(ms.PROCESSING_REQUEST) 36 | try: 37 | if self.media_message.empty: 38 | raise SampleVideoProcessFailure( 39 | for_user=ms.MEDIA_MESSAGE_DELETED, 40 | for_admin=ms.MEDIA_MESSAGE_DELETED, 41 | ) 42 | 43 | await self.track_user_activity() 44 | 45 | await self.input_message.edit_message_text( 46 | text=ms.SAMPLE_VIDEO_PROCESS_START 47 | ) 48 | start_time = time.time() 49 | duration = await Utilities.get_duration(self.file_link) 50 | if isinstance(duration, str): 51 | raise SampleVideoProcessFailure( 52 | for_user=ms.CANNOT_OPEN_FILE, 53 | for_admin=ms.SAMPLE_VIDEO_PROCESS_OPEN_ERROR.format( 54 | file_link=self.file_link, duration=duration 55 | ), 56 | ) 57 | 58 | reduced_sec = duration - int(duration * 10 / 100) 59 | sample_duration = await db.get_sample_duration(self.chat_id) 60 | temp_output_folder = tempfile.TemporaryDirectory() 61 | temp_thumbnail_folder = tempfile.TemporaryDirectory() 62 | with temp_output_folder as output_folder, temp_thumbnail_folder as thumbnail_folder: 63 | sample_file = os.path.join(output_folder, "sample_video.mkv") 64 | start_at = Utilities.get_random_start_at(reduced_sec, sample_duration) 65 | subtitle_option = await Utilities.fix_subtitle_codec(self.file_link) 66 | log.info( 67 | "Generating sample video (duration %ss from %s) from location: %s for %s", 68 | sample_duration, 69 | start_at, 70 | self.file_link, 71 | self.chat_id, 72 | ) 73 | ffmpeg_cmd = [ 74 | "ffmpeg", 75 | #"-hide_banner", 76 | "-ss", 77 | str(start_at), 78 | "-i", 79 | self.file_link, 80 | "-t", 81 | str(sample_duration), 82 | "-map", 83 | "0", 84 | "-c", 85 | "copy", 86 | sample_file, 87 | ] 88 | for option in subtitle_option: 89 | ffmpeg_cmd.insert(-1, option) 90 | 91 | log.debug(ffmpeg_cmd) 92 | output = await Utilities.run_subprocess(ffmpeg_cmd) 93 | log.debug( 94 | "FFmpeg output\n %s \n %s", output[0].decode(), output[1].decode() 95 | ) 96 | if (not os.path.exists(sample_file)) or ( 97 | os.path.getsize(sample_file) == 0 98 | ): 99 | ffmpeg_output = output[0].decode() + "\n" + output[1].decode() 100 | log_msg = ms.SAMPLE_VIDEO_PROCESS_FAILED_GENERATION.format( 101 | file_link=self.file_link, 102 | sample_duration=sample_duration, 103 | ffmpeg_output=ffmpeg_output, 104 | ) 105 | raise SampleVideoProcessFailure( 106 | for_user=ms.SAMPLE_VIDEO_PROCESS_FAILED, for_admin=log_msg 107 | ) 108 | 109 | thumb = await Utilities.generate_thumbnail_file( 110 | sample_file, thumbnail_folder 111 | ) 112 | width, height = await Utilities.get_dimentions(sample_file) 113 | await self.input_message.edit_message_text( 114 | text=ms.SAMPLE_VIDEO_PROCESS_SUCCESS 115 | ) 116 | await upload_notify() 117 | await self.media_message.reply_video( 118 | video=str(sample_file), 119 | quote=True, 120 | caption=ms.VIDEO_PROCESS_CAPTION.format( 121 | duration=sample_duration, 122 | start=datetime.timedelta(seconds=start_at), 123 | ), 124 | duration=sample_duration, 125 | thumb=thumb, 126 | width=width, 127 | height=height, 128 | supports_streaming=True, 129 | progress=upload_notify, 130 | ) 131 | 132 | await self.input_message.edit_message_text( 133 | text=ms.PROCESS_UPLOAD_CONFIRM.format( 134 | total_process_duration=datetime.timedelta( 135 | seconds=int(time.time() - start_time) 136 | ) 137 | ) 138 | ) 139 | try: 140 | os.remove(self.file_link) 141 | except: pass 142 | except SampleVideoProcessFailure as e: 143 | log.error(e) 144 | await self.input_message.edit_message_text(text=e.for_user) 145 | log_msg = await self.media_message.forward(Config.LOG_CHANNEL) 146 | await log_msg.reply_text( 147 | e.for_admin, 148 | quote=True, 149 | ) 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Screenshotit_bot](https://tx.me/screenshotit_bot) 2 | 3 | > Telegram Bot For Screenshot Generation. Check Description for the live example 4 | 5 | ## Added Heroku Support 😋 6 | I had removed host in this repo so there is a less chances of heroku suspension. 7 | For now it is not suspended by heroku but dont know when it gonna suspended. 8 | Since i had removed host bot will download the entire file and then generate screenshots 9 | 10 | 11 | ## Description 12 | 13 | An attempt to implement the screenshot generation of telegram files. Live version can be found here [@Screenshot_NsBot](https://t.me/Screenshot_NsBot "Screenshot Generator Bot"). 14 | 15 | ## Installation Guide 16 | 17 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://www.heroku.com/deploy?template=https://github.com/Ns-AnoNymouS/screenshot-bot) 18 | 19 | ### Prerequisites 20 | 21 | * FFmpeg. 22 | * Python3 (3.6 or higher). 23 | 24 | ### Local setup 25 | 26 | > The setup given here is for a linux environment (Debian/Ubuntu). 27 | 28 | * Clone to local machine. 29 | 30 | ``` bash 31 | $ git clone https://github.com/odysseusmax/animated-lamp.git 32 | $ cd animated-lamp 33 | ```` 34 | 35 | * Create and activate virtual environment. 36 | 37 | ``` 38 | $ python3 -m venv venv 39 | $ source venv/bin/activate 40 | ``` 41 | 42 | * Install dependencies. 43 | 44 | ``` 45 | $ pip3 install -U -r requirements.txt 46 | ``` 47 | 48 | ### Environment Variables 49 | 50 | Properly setup the environment variables or populate `config.py` with the values. Setting up environment variables is advised as some of the values are sensitive data, and should be kept secret. 51 | 52 | * `API_ID`(required) - Get your telegram API_ID from [https://my.telegram.org/](https://my.telegram.org/). 53 | 54 | * `API_HASH`(required) - Get your telegram API_HASH from [https://my.telegram.org/](https://my.telegram.org/). 55 | 56 | * `BOT_TOKEN`(required) - Obtain your bot token from [Bot Father](https://t.me/BotFather "Bot Father"). 57 | 58 | * `LOG_CHANNEL`(required) - Log channel's id. 59 | 60 | * `DATABASE_URL`(required) - Mongodb database URI. 61 | 62 | * `AUTH_USERS`(required) - Admin(s) of the bot. User's telegram id separated by space. Atleast one id should be specified. 63 | 64 | * `SESSION_NAME`(optional) - Name you want to call your bot's session, Eg: bot's username. 65 | 66 | * `MAX_PROCESSES_PER_USER`(optional) - Number of parallel processes each user can have, defaults to 2. 67 | 68 | * `MAX_TRIM_DURATION`(optional) - Maximum allowed video trim duration in seconds. Defaults to 600s. 69 | 70 | * `TRACK_CHANNEL`(optional) - User activity tracking channel's id. Only needed if you want to track and block any user. Disabled by default. 71 | 72 | * `SLOW_SPEED_DELAY`(optional) - Delay required between each interaction from users in seconds. Defaults to 5s. 73 | 74 | * `TIMEOUT` (optional) - Maximum time alloted to each process in seconds, after which process will be cancelled. Defaults to 1800s(30 mins). 75 | 76 | * `DEBUG` (optional) - Set some value to use DEBUG logging level. INFO by default. 77 | 78 | * `WORKER_COUNT` (optional) - Number of process to be handled at a time. Defaults to `20`. 79 | 80 | ### Run bot 81 | 82 | `$ python3 -m bot` 83 | 84 | Now go and `/start` the bot. If everything went right, bot will respond with welcome message. 85 | 86 | ## Supported commands and functions 87 | 88 | ### Commands 89 | 90 | **General commands** 91 | 92 | ``` 93 | start - Command to start bot or check whether bot is alive. 94 | help - Command to know about how to use bot. 95 | settings - Command to configure bot's behavior' 96 | set_watermark - Command to add custom watermark text to screenshots. Usage: `/set_watermark watermark_text`. 97 | ``` 98 | 99 | **Admin commands** 100 | 101 | > Any user specified in `AUTH_USERS` can use these commands. 102 | 103 | ``` 104 | admin - to check available admin commands 105 | status - Returns number of total users. 106 | ban_user - Command to ban any user. Usage: `/ban_user user_id ban_duration ban_reason`. `user_id` - telegram id of the user, `ban_duration` - ban duration in days, `ban_reason` - reason for ban. All 3 parameters are required. 107 | unban_user - Command to unban any banned user. Usage: `/unban_user user_id`. `user_id` - telegram id of the user. The parameter is required. 108 | banned_users - Command to view all banned users. Usage: `/banned_users`. This takes no parameters. 109 | broadcast - Command to broadcast some message to all users. Usage: reply `/broadcast` to the message you want to broadcast. 110 | ``` 111 | 112 | ### Functions 113 | * `Screenshot Generation` - Generates screenshots from telegram video files or streaming links. Number of screenshots range from 2-10. 114 | 115 | * `Sample Video Generation` - Generates sample video from telegram video files or streaming links. Video duration range from 30s to 150s. Configurable in `/settings`. 116 | 117 | * `Video Trimming` - Trims any telegram video files or streaming links. 118 | 119 | ### Settings 120 | In bot settings. 121 | 122 | * `Upload Mode` - Screenshot upload mode. Either `as image file` or `as document file`. Defaults to `as image file`. 123 | 124 | * `Watermark` - Watermark text to be embedded to screenshots. Texts upto 30 characters supported. Disabled by default. 125 | 126 | * `Watermark Color` - Font color to be used for watermark. Any of `white`, `black`, `red`, `blue`, `green`, `yellow`, `orange`, `purple`, `brown`, `gold`, `silver`, `pink`. Defaults to `white`. 127 | 128 | * `Watermark Font Size` - Font size to be used for watermarks. Any of `small(30)`, `medium(40)`, `large(50)`. Defaults to `medium`. 129 | 130 | * `Watermark Position` - Watermark text's position. Defaults to `bottom left`. 131 | 132 | * `Sample Video Duration` - Sample video's duration. Any of `30s`, `60s`, `90s`, `120s`, `150s`. Defaults to `30s`. 133 | 134 | * `Screenshot Genetation Mode` - Either `random` or `equally spaced`. Defaults to `equally spaced`. 135 | 136 | 137 | ## Contributions 138 | Contributions are welcome. 139 | 140 | ## Contact 141 | You can contact me 142 | 143 | ## Credits 144 | All credits goes to [odysseusmax](https://github.com/odysseusmax) he had made everything the best i just 145 | Changed some small things to make the bot supported by heroku. 146 | 147 | 148 | ## Thanks 149 | Thanks to [odysseusmax](https://github.com/odysseusmax) for his [Animated Lamp](https://github.com/odysseusmax/animated-lamp "Animated Lamp"). 150 | 151 | Thanks to [Dan](https://github.com/delivrance "Dan") for his [Pyrogram](https://github.com/pyrogram/pyrogram "Pyrogram") library. 152 | 153 | 154 | ## Dependencies 155 | * pyrogram 156 | * tgcrypto 157 | * motor 158 | * dnspython 159 | * async-timeout 160 | * aiohttp 161 | 162 | 163 | ## License 164 | Code released under [The GNU General Public License](LICENSE). 165 | -------------------------------------------------------------------------------- /bot/database/database.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import motor.motor_asyncio 4 | 5 | from bot.config import Config 6 | 7 | 8 | class Singleton(type): 9 | __instances__ = {} 10 | 11 | def __call__(cls, *args, **kwargs): 12 | if cls not in cls.__instances__: 13 | cls.__instances__[cls] = super(Singleton, cls).__call__(*args, **kwargs) 14 | 15 | return cls.__instances__[cls] 16 | 17 | 18 | class Database(metaclass=Singleton): 19 | def __init__(self): 20 | self._client = motor.motor_asyncio.AsyncIOMotorClient(Config.DATABASE_URL) 21 | self.db = self._client[Config.SESSION_NAME] 22 | self.col = self.db.users 23 | 24 | self.cache = {} 25 | 26 | def new_user(self, id): 27 | return dict( 28 | id=id, 29 | join_date=datetime.date.today().isoformat(), 30 | last_used_on=datetime.date.today().isoformat(), 31 | as_file=False, 32 | watermark_text="", 33 | sample_duration=30, 34 | as_round=False, 35 | watermark_color=0, 36 | screenshot_mode=0, 37 | font_size=1, 38 | ban_status=dict( 39 | is_banned=False, 40 | ban_duration=0, 41 | banned_on=datetime.date.max.isoformat(), 42 | ban_reason="", 43 | ), 44 | ) 45 | 46 | async def get_user(self, id): 47 | user = self.cache.get(id) 48 | if user is not None: 49 | return user 50 | 51 | user = await self.col.find_one({"id": int(id)}) 52 | self.cache[id] = user 53 | return user 54 | 55 | async def add_user(self, id): 56 | user = self.new_user(id) 57 | await self.col.insert_one(user) 58 | 59 | async def is_user_exist(self, id): 60 | user = await self.get_user(id) 61 | return True if user else False 62 | 63 | async def total_users_count(self): 64 | count = await self.col.count_documents({}) 65 | return count 66 | 67 | async def is_as_file(self, id): 68 | user = await self.get_user(id) 69 | return user.get("as_file", False) 70 | 71 | async def is_as_round(self, id): 72 | user = await self.get_user(id) 73 | return user.get("as_round", False) 74 | 75 | async def update_as_file(self, id, as_file): 76 | self.cache[id]["as_file"] = as_file 77 | await self.col.update_one({"id": id}, {"$set": {"as_file": as_file}}) 78 | 79 | async def update_as_round(self, id, as_round): 80 | self.cache[id]["as_round"] = as_round 81 | await self.col.update_one({"id": id}, {"$set": {"as_round": as_round}}) 82 | 83 | async def update_watermark_text(self, id, watermark_text=""): 84 | self.cache[id]["watermark_text"] = watermark_text 85 | await self.col.update_one( 86 | {"id": id}, {"$set": {"watermark_text": watermark_text}} 87 | ) 88 | 89 | async def update_sample_duration(self, id, sample_duration): 90 | self.cache[id]["sample_duration"] = sample_duration 91 | await self.col.update_one( 92 | {"id": id}, {"$set": {"sample_duration": sample_duration}} 93 | ) 94 | 95 | async def update_watermark_color(self, id, watermark_color): 96 | self.cache[id]["watermark_color"] = watermark_color 97 | await self.col.update_one( 98 | {"id": id}, {"$set": {"watermark_color": watermark_color}} 99 | ) 100 | 101 | async def update_screenshot_mode(self, id, screenshot_mode): 102 | self.cache[id]["screenshot_mode"] = screenshot_mode 103 | await self.col.update_one( 104 | {"id": id}, {"$set": {"screenshot_mode": screenshot_mode}} 105 | ) 106 | 107 | async def update_font_size(self, id, font_size): 108 | self.cache[id]["font_size"] = font_size 109 | await self.col.update_one({"id": id}, {"$set": {"font_size": font_size}}) 110 | 111 | async def update_watermark_position(self, id, watermark_position): 112 | self.cache[id]["watermark_position"] = watermark_position 113 | await self.col.update_one( 114 | {"id": id}, {"$set": {"watermark_position": watermark_position}} 115 | ) 116 | 117 | async def update_last_used_on(self, id): 118 | self.cache[id]["last_used_on"] = datetime.date.today().isoformat() 119 | await self.col.update_one( 120 | {"id": id}, {"$set": {"last_used_on": datetime.date.today().isoformat()}} 121 | ) 122 | 123 | async def remove_ban(self, id): 124 | await self.get_user(id) 125 | ban_status = dict( 126 | is_banned=False, 127 | ban_duration=0, 128 | banned_on=datetime.date.max.isoformat(), 129 | ban_reason="", 130 | ) 131 | self.cache[id]["ban_status"] = ban_status 132 | await self.col.update_one({"id": id}, {"$set": {"ban_status": ban_status}}) 133 | 134 | async def ban_user(self, user_id, ban_duration, ban_reason): 135 | await self.get_user(user_id) 136 | ban_status = dict( 137 | is_banned=True, 138 | ban_duration=ban_duration, 139 | banned_on=datetime.date.today().isoformat(), 140 | ban_reason=ban_reason, 141 | ) 142 | self.cache[user_id]["ban_status"] = ban_status 143 | await self.col.update_one({"id": user_id}, {"$set": {"ban_status": ban_status}}) 144 | 145 | async def get_watermark_text(self, id): 146 | user = await self.get_user(id) 147 | return user.get("watermark_text", "") 148 | 149 | async def get_sample_duration(self, id): 150 | user = await self.get_user(id) 151 | return user.get("sample_duration", 30) 152 | 153 | async def get_watermark_color(self, id): 154 | user = await self.get_user(id) 155 | return user.get("watermark_color", 0) 156 | 157 | async def get_watermark_position(self, id): 158 | user = await self.get_user(id) 159 | return user.get("watermark_position", 6) 160 | 161 | async def get_screenshot_mode(self, id): 162 | user = await self.get_user(id) 163 | return user.get("screenshot_mode", 0) 164 | 165 | async def get_font_size(self, id): 166 | user = await self.get_user(id) 167 | return user.get("font_size", 1) 168 | 169 | async def get_ban_status(self, id): 170 | default = dict( 171 | is_banned=False, 172 | ban_duration=0, 173 | banned_on=datetime.date.max.isoformat(), 174 | ban_reason="", 175 | ) 176 | user = await self.get_user(id) 177 | return user.get("ban_status", default) 178 | 179 | async def get_all_banned_users(self): 180 | banned_users = self.col.find({"ban_status.is_banned": True}) 181 | return banned_users 182 | 183 | async def get_all_users(self): 184 | all_users = self.col.find({}) 185 | return all_users 186 | 187 | async def delete_user(self, user_id): 188 | user_id = int(user_id) 189 | if self.cache.get(user_id): 190 | self.cache.pop(user_id) 191 | await self.col.delete_many({"id": user_id}) 192 | 193 | async def get_last_used_on(self, id): 194 | user = await self.get_user(id) 195 | return user.get("last_used_on", datetime.date.today().isoformat()) 196 | -------------------------------------------------------------------------------- /bot/processes/trim.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import tempfile 4 | import logging 5 | import datetime 6 | 7 | from bot.config import Config 8 | from bot.utils import Utilities 9 | from bot.messages import Messages as ms 10 | from .exception import BaseException 11 | from .base import BaseProcess 12 | 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | 17 | class TrimVideoProcessFailure(BaseException): 18 | pass 19 | 20 | 21 | class TrimVideoProcess(BaseProcess): 22 | def __init__(self, client, input_message, reply_message): 23 | super().__init__(client, input_message) 24 | self.reply_message = reply_message 25 | 26 | async def cancelled(self): 27 | await self.reply_message.edit_text(ms.PROCESS_TIMEOUT) 28 | 29 | async def set_media_message(self): 30 | message = await self.client.get_messages( 31 | self.chat_id, self.input_message.reply_to_message.message_id 32 | ) 33 | await self.input_message.reply_to_message.delete() 34 | self.media_message = message.reply_to_message 35 | 36 | async def process(self): 37 | async def upload_notify(*args): 38 | await self.client.send_chat_action(self.chat_id, "upload_video") 39 | 40 | await self.set_media_message() 41 | await self.reply_message.edit_text(ms.PROCESSING_REQUEST) 42 | try: 43 | if self.media_message.empty: 44 | raise TrimVideoProcessFailure( 45 | for_user=ms.MEDIA_MESSAGE_DELETED, 46 | for_admin=ms.MEDIA_MESSAGE_DELETED, 47 | ) 48 | 49 | try: 50 | start, end = [int(i) for i in self.input_message.text.split(":")] 51 | except Exception: 52 | raise TrimVideoProcessFailure( 53 | for_user=ms.WRONG_FORMAT, 54 | for_admin=ms.WRONG_FORMAT, 55 | ) 56 | 57 | if 0 > start > end: 58 | raise TrimVideoProcessFailure( 59 | for_user=ms.TRIM_VIDEO_INVALID_RANGE, 60 | for_admin=ms.TRIM_VIDEO_INVALID_RANGE, 61 | ) 62 | 63 | request_duration = end - start 64 | if request_duration > Config.MAX_TRIM_DURATION: 65 | raise TrimVideoProcessFailure( 66 | for_user=ms.TRIM_VIDEO_DURATION_ERROR.format( 67 | max_duration=Config.MAX_TRIM_DURATION, 68 | start=start, 69 | end=end, 70 | request_duration=request_duration, 71 | ), 72 | for_admin=ms.TRIM_VIDEO_INVALID_RANGE, 73 | ) 74 | 75 | await self.track_user_activity() 76 | start_time = time.time() 77 | await self.reply_message.edit_text(ms.TRIM_VIDEO_START) 78 | 79 | duration = await Utilities.get_duration(self.file_link) 80 | if isinstance(duration, str): 81 | raise TrimVideoProcessFailure( 82 | for_user=ms.CANNOT_OPEN_FILE, 83 | for_admin=ms.TRIM_VIDEO_OPEN_ERROR.format( 84 | file_link=self.file_link, 85 | start=start, 86 | end=end, 87 | duration=duration, 88 | ), 89 | ) 90 | 91 | if (start >= duration) or (end >= duration): 92 | raise TrimVideoProcessFailure( 93 | for_user=ms.TRIM_VIDEO_RANGE_OUT_OF_VIDEO_DURATION, 94 | for_admin=ms.TRIM_VIDEO_RANGE_OUT_OF_VIDEO_DURATION, 95 | ) 96 | 97 | log.info( 98 | "Trimming video (duration %ss from %s) from location: %s for %s", 99 | request_duration, 100 | start, 101 | self.file_link, 102 | self.chat_id, 103 | ) 104 | 105 | temp_output_folder = tempfile.TemporaryDirectory() 106 | temp_thumbnail_folder = tempfile.TemporaryDirectory() 107 | with temp_output_folder as output_folder, temp_thumbnail_folder as thumbnail_folder: 108 | trim_video_file = os.path.join(output_folder, "trim_video.mkv") 109 | subtitle_option = await Utilities.fix_subtitle_codec(self.file_link) 110 | 111 | ffmpeg_cmd = [ 112 | "ffmpeg", 113 | "-ss", 114 | str(start), 115 | "-i", 116 | self.file_link, 117 | "-t", 118 | str(request_duration), 119 | "-map", 120 | "0", 121 | "-c", 122 | "copy", 123 | trim_video_file, 124 | ] 125 | for option in subtitle_option: 126 | ffmpeg_cmd.insert(-1, option) 127 | 128 | log.debug(ffmpeg_cmd) 129 | output = await Utilities.run_subprocess(ffmpeg_cmd) 130 | log.debug( 131 | "FFmpeg output\n %s \n %s", output[0].decode(), output[1].decode() 132 | ) 133 | 134 | if (not os.path.exists(trim_video_file)) or ( 135 | os.path.getsize(trim_video_file) == 0 136 | ): 137 | ffmpeg_output = output[0].decode() + "\n" + output[1].decode() 138 | raise TrimVideoProcessFailure( 139 | for_user=ms.TRIM_VIDEO_PROCESS_FAILED, 140 | for_admin=ms.TRIM_VIDEO_PROCESS_FAILED_GENERATION.format( 141 | file_link=self.file_link, 142 | start=start, 143 | end=end, 144 | ffmpeg_output=ffmpeg_output, 145 | ), 146 | ) 147 | 148 | thumb = await Utilities.generate_thumbnail_file( 149 | trim_video_file, thumbnail_folder 150 | ) 151 | width, height = await Utilities.get_dimentions(trim_video_file) 152 | await self.reply_message.edit_text(ms.TRIM_VIDEO_PROCESS_SUCCESS) 153 | await upload_notify() 154 | await self.media_message.reply_video( 155 | video=str(trim_video_file), 156 | quote=True, 157 | caption=ms.VIDEO_PROCESS_CAPTION.format( 158 | duration=request_duration, 159 | start=datetime.timedelta(seconds=start), 160 | ), 161 | duration=request_duration, 162 | width=width, 163 | height=height, 164 | thumb=thumb, 165 | supports_streaming=True, 166 | progress=upload_notify, 167 | ) 168 | 169 | await self.reply_message.edit_text( 170 | ms.PROCESS_UPLOAD_CONFIRM.format( 171 | total_process_duration=datetime.timedelta( 172 | seconds=int(time.time() - start_time) 173 | ) 174 | ) 175 | ) 176 | try: 177 | os.remove(self.file_link) 178 | except: pass 179 | 180 | except TrimVideoProcessFailure as e: 181 | log.error(e) 182 | await self.reply_message.edit_text(text=e.for_user) 183 | log_msg = await self.media_message.forward(Config.LOG_CHANNEL) 184 | await log_msg.reply_text( 185 | e.for_admin, 186 | quote=True, 187 | ) 188 | -------------------------------------------------------------------------------- /bot/processes/screenshot.py: -------------------------------------------------------------------------------- 1 | import os 2 | import io 3 | import time 4 | import math 5 | import logging 6 | import tempfile 7 | import datetime 8 | 9 | from pyrogram.types import InputMediaPhoto, InputMediaDocument 10 | 11 | from bot.config import Config 12 | from bot.utils import Utilities 13 | from bot.messages import Messages as ms 14 | from bot.database import Database 15 | from .base import BaseProcess 16 | from .exception import BaseException 17 | 18 | 19 | log = logging.getLogger(__name__) 20 | db = Database() 21 | 22 | 23 | class ScreenshotsProcessFailure(BaseException): 24 | pass 25 | 26 | 27 | class ScreenshotsProcess(BaseProcess): 28 | async def set_media_message(self): 29 | self.media_message = self.input_message.message.reply_to_message 30 | 31 | async def cancelled(self): 32 | await self.input_message.edit_message_text(ms.PROCESS_TIMEOUT) 33 | 34 | async def process(self): 35 | await self.set_media_message() 36 | _, num_screenshots = self.input_message.data.split("+") 37 | num_screenshots = int(num_screenshots) 38 | await self.input_message.edit_message_text(ms.PROCESSING_REQUEST) 39 | try: 40 | if self.media_message.empty: 41 | raise ScreenshotsProcessFailure( 42 | for_user=ms.MEDIA_MESSAGE_DELETED, 43 | for_admin=ms.MEDIA_MESSAGE_DELETED, 44 | ) 45 | 46 | await self.track_user_activity() 47 | start_time = time.time() 48 | await self.input_message.edit_message_text(ms.SCREENSHOTS_START) 49 | duration = await Utilities.get_duration(self.file_link) 50 | if isinstance(duration, str): 51 | raise ScreenshotsProcessFailure( 52 | for_user=ms.CANNOT_OPEN_FILE, 53 | for_admin=ms.SCREENSHOTS_OPEN_ERROR.format( 54 | file_link=self.file_link, 55 | num_screenshots=num_screenshots, 56 | duration=duration, 57 | ), 58 | ) 59 | 60 | log.info( 61 | "Generating %s screenshots from location: %s for %s", 62 | num_screenshots, 63 | self.file_link, 64 | self.chat_id, 65 | ) 66 | 67 | reduced_sec = duration - int(duration * 2 / 100) 68 | screenshots = [] 69 | watermark = await db.get_watermark_text(self.chat_id) 70 | as_file = await db.is_as_file(self.chat_id) 71 | screenshot_mode = await db.get_screenshot_mode(self.chat_id) 72 | ffmpeg_errors = "" 73 | watermark_options = "scale=1280:-1" 74 | if watermark: 75 | watermark_color_code = await db.get_watermark_color(self.chat_id) 76 | watermark_color = Config.COLORS[watermark_color_code] 77 | watermark_position = await db.get_watermark_position(self.chat_id) 78 | font_size = await db.get_font_size(self.chat_id) 79 | width, height = await Utilities.get_dimentions(self.file_link) 80 | fontsize = int( 81 | (math.sqrt(width ** 2 + height ** 2) / 1388.0) 82 | * Config.FONT_SIZES[font_size] 83 | ) 84 | x_pos, y_pos = Utilities.get_watermark_coordinates( 85 | watermark_position, width, height 86 | ) 87 | watermark_options = ( 88 | f"drawtext=fontcolor={watermark_color}:fontsize={fontsize}:x={x_pos}:" 89 | f"y={y_pos}:text={watermark}, scale=1280:-1" 90 | ) 91 | 92 | ffmpeg_cmd = [ 93 | "ffmpeg", 94 | "-ss", 95 | "", # To be replaced in loop 96 | "-i", 97 | self.file_link, 98 | "-vf", 99 | watermark_options, 100 | "-y", 101 | "-vframes", 102 | "1", 103 | "", # To be replaced in loop 104 | ] 105 | 106 | screenshot_secs = [ 107 | int(reduced_sec / num_screenshots) * i 108 | if screenshot_mode == 0 109 | else Utilities.get_random_start_at(reduced_sec) 110 | for i in range(1, 1 + num_screenshots) 111 | ] 112 | 113 | with tempfile.TemporaryDirectory() as output_folder: 114 | for i, sec in enumerate(screenshot_secs): 115 | thumbnail_file = os.path.join(output_folder, f"{i+1}.png") 116 | ffmpeg_cmd[2] = str(sec) 117 | ffmpeg_cmd[-1] = thumbnail_file 118 | log.debug(ffmpeg_cmd) 119 | output = await Utilities.run_subprocess(ffmpeg_cmd) 120 | log.debug( 121 | "FFmpeg output\n %s \n %s", 122 | output[0].decode(), 123 | output[1].decode(), 124 | ) 125 | await self.input_message.edit_message_text( 126 | ms.SCREENSHOTS_PROGRESS.format( 127 | current=i + 1, total=num_screenshots 128 | ) 129 | ) 130 | if os.path.exists(thumbnail_file): 131 | if as_file: 132 | InputMedia = InputMediaDocument 133 | else: 134 | InputMedia = InputMediaPhoto 135 | 136 | screenshots.append( 137 | InputMedia( 138 | thumbnail_file, 139 | caption=ms.SCREENSHOT_AT.format( 140 | time=datetime.timedelta(seconds=sec) 141 | ), 142 | ) 143 | ) 144 | continue 145 | 146 | ffmpeg_errors += ( 147 | output[0].decode() + "\n" + output[1].decode() + "\n\n" 148 | ) 149 | 150 | if not screenshots: 151 | error_file = None 152 | if ffmpeg_errors: 153 | error_file = io.BytesIO() 154 | error_file.name = "errors.txt" 155 | error_file.write(ffmpeg_errors.encode()) 156 | raise ScreenshotsProcessFailure( 157 | for_user=ms.SCREENSHOT_PROCESS_FAILED, 158 | for_admin=ms.SCREENSHOTS_FAILED_GENERATION.format( 159 | file_link=(self.file_link), num_screenshots=num_screenshots 160 | ), 161 | extra_details=error_file, 162 | ) 163 | 164 | await self.input_message.edit_message_text( 165 | text=ms.SCREENSHOT_PROCESS_SUCCESS.format( 166 | count=num_screenshots, total_count=len(screenshots) 167 | ) 168 | ) 169 | await self.client.send_chat_action(self.chat_id, "upload_photo") 170 | await self.media_message.reply_media_group(screenshots, True) 171 | await self.input_message.edit_message_text( 172 | ms.PROCESS_UPLOAD_CONFIRM.format( 173 | total_process_duration=datetime.timedelta( 174 | seconds=int(time.time() - start_time) 175 | ) 176 | ) 177 | ) 178 | try: 179 | os.remove(self.file_link) 180 | except: pass 181 | 182 | except ScreenshotsProcessFailure as e: 183 | log.error(e) 184 | await self.input_message.edit_message_text(e.for_user) 185 | log_msg = await self.media_message.forward(Config.LOG_CHANNEL) 186 | if e.extra_details: 187 | await log_msg.reply_document( 188 | document=e.extra_details, quote=True, caption=e.for_admin 189 | ) 190 | else: 191 | await log_msg.reply_text( 192 | text=e.for_admin, 193 | quote=True, 194 | ) 195 | -------------------------------------------------------------------------------- /bot/processes/manual_screenshot.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import time 4 | import math 5 | import logging 6 | import tempfile 7 | import datetime 8 | 9 | from pyrogram.types import InputMediaPhoto, InputMediaDocument 10 | 11 | from bot.config import Config 12 | from bot.utils import Utilities 13 | from bot.messages import Messages as ms 14 | from bot.database import Database 15 | from .base import BaseProcess 16 | from .exception import BaseException 17 | 18 | 19 | log = logging.getLogger(__name__) 20 | db = Database() 21 | 22 | 23 | class ManualScreenshotsProcessFailure(BaseException): 24 | pass 25 | 26 | 27 | class ManualScreenshotsProcess(BaseProcess): 28 | def __init__(self, client, input_message, reply_message): 29 | super().__init__(client, input_message) 30 | self.reply_message = reply_message 31 | 32 | async def set_media_message(self): 33 | message = await self.client.get_messages( 34 | self.chat_id, self.input_message.reply_to_message.message_id 35 | ) 36 | await self.input_message.reply_to_message.delete() 37 | self.media_message = message.reply_to_message 38 | 39 | async def cancelled(self): 40 | await self.reply_message.edit_text(ms.PROCESS_TIMEOUT) 41 | 42 | async def process(self): 43 | await self.set_media_message() 44 | await self.reply_message.edit_text(ms.PROCESSING_REQUEST) 45 | try: 46 | if self.media_message.empty: 47 | raise ManualScreenshotsProcessFailure( 48 | for_user=ms.MEDIA_MESSAGE_DELETED, 49 | for_admin=ms.MEDIA_MESSAGE_DELETED, 50 | ) 51 | 52 | try: 53 | raw_user_input = [ 54 | int(i.strip()) for i in self.input_message.text.split(",") 55 | ] 56 | except Exception: 57 | raise ManualScreenshotsProcessFailure( 58 | for_user=ms.WRONG_FORMAT, 59 | for_admin=ms.WRONG_FORMAT, 60 | ) 61 | 62 | await self.track_user_activity() 63 | start_time = time.time() 64 | 65 | duration = await Utilities.get_duration(self.file_link) 66 | if isinstance(duration, str): 67 | raise ManualScreenshotsProcessFailure( 68 | for_user=ms.CANNOT_OPEN_FILE, 69 | for_admin=ms.MANUAL_SCREENSHOTS_OPEN_ERROR.format( 70 | file_link=self.file_link, duration=duration 71 | ), 72 | ) 73 | 74 | valid_positions = [] 75 | invalid_positions = [] 76 | for pos in raw_user_input: 77 | if 0 > pos > duration: 78 | invalid_positions.append(str(pos)) 79 | else: 80 | valid_positions.append(pos) 81 | 82 | if not valid_positions: 83 | raise ManualScreenshotsProcessFailure( 84 | for_user=ms.MANUAL_SCREENSHOTS_NO_VALID_POSITIONS, 85 | for_admin=ms.MANUAL_SCREENSHOTS_NO_VALID_POSITIONS, 86 | ) 87 | 88 | if len(valid_positions) > 10: 89 | raise ManualScreenshotsProcessFailure( 90 | for_user=ms.MANUAL_SCREENSHOTS_VALID_PISITIONS_ABOVE_LIMIT.format( 91 | valid_positions_count=len(valid_positions) 92 | ), 93 | for_admin=ms.MANUAL_SCREENSHOTS_VALID_PISITIONS_ABOVE_LIMIT.format( 94 | valid_positions_count=len(valid_positions) 95 | ), 96 | ) 97 | 98 | if invalid_positions: 99 | txt = ms.MANUAL_SCREENSHOTS_INVALID_POSITIONS_ALERT.format( 100 | invalid_positions_count=len(invalid_positions), 101 | invalid_positions=", ".join(invalid_positions), 102 | ) 103 | else: 104 | txt = ms.SCREENSHOTS_START 105 | 106 | await self.reply_message.edit_text(txt) 107 | screenshots = [] 108 | ffmpeg_errors = "" 109 | as_file = await db.is_as_file(self.chat_id) 110 | watermark = await db.get_watermark_text(self.chat_id) 111 | watermark_options = "scale=1280:-1" 112 | if watermark: 113 | watermark_color_code = await db.get_watermark_color(self.chat_id) 114 | watermark_color = Config.COLORS[watermark_color_code] 115 | watermark_position = await db.get_watermark_position(self.chat_id) 116 | font_size = await db.get_font_size(self.chat_id) 117 | width, height = await Utilities.get_dimentions(self.file_link) 118 | fontsize = int( 119 | (math.sqrt(width ** 2 + height ** 2) / 1388.0) 120 | * Config.FONT_SIZES[font_size] 121 | ) 122 | x_pos, y_pos = Utilities.get_watermark_coordinates( 123 | watermark_position, width, height 124 | ) 125 | watermark_options = ( 126 | f"drawtext=fontcolor={watermark_color}:fontsize={fontsize}:x={x_pos}:" 127 | f"y={y_pos}:text={watermark}, scale=1280:-1" 128 | ) 129 | 130 | ffmpeg_cmd = [ 131 | "ffmpeg", 132 | "-ss", 133 | "", # To be replaced in loop 134 | "-i", 135 | self.file_link, 136 | "-vf", 137 | watermark_options, 138 | "-y", 139 | "-vframes", 140 | "1", 141 | "", # To be replaced in loop 142 | ] 143 | 144 | log.info( 145 | "Generating screenshots at positions %s from location: %s for %s", 146 | valid_positions, 147 | self.file_link, 148 | self.chat_id, 149 | ) 150 | 151 | with tempfile.TemporaryDirectory() as output_folder: 152 | for i, sec in enumerate(valid_positions): 153 | thumbnail_file = os.path.join(output_folder, f"{i+1}.png") 154 | ffmpeg_cmd[2] = str(sec) 155 | ffmpeg_cmd[-1] = thumbnail_file 156 | log.debug(ffmpeg_cmd) 157 | output = await Utilities.run_subprocess(ffmpeg_cmd) 158 | log.debug( 159 | "FFmpeg output\n %s \n %s", 160 | output[0].decode(), 161 | output[1].decode(), 162 | ) 163 | await self.reply_message.edit_text( 164 | ms.SCREENSHOTS_PROGRESS.format( 165 | current=i + 1, total=len(valid_positions) 166 | ) 167 | ) 168 | if os.path.exists(thumbnail_file): 169 | if as_file: 170 | InputMedia = InputMediaDocument 171 | else: 172 | InputMedia = InputMediaPhoto 173 | screenshots.append( 174 | InputMedia( 175 | thumbnail_file, 176 | caption=ms.SCREENSHOT_AT.format( 177 | time=datetime.timedelta(seconds=sec) 178 | ), 179 | ) 180 | ) 181 | continue 182 | 183 | ffmpeg_errors += ( 184 | output[0].decode() + "\n" + output[1].decode() + "\n\n" 185 | ) 186 | 187 | if not screenshots: 188 | error_file = None 189 | if ffmpeg_errors: 190 | error_file = io.BytesIO() 191 | error_file.name = "errors.txt" 192 | error_file.write(ffmpeg_errors.encode()) 193 | raise ManualScreenshotsProcessFailure( 194 | for_user=ms.SCREENSHOT_PROCESS_FAILED, 195 | for_admin=ms.MANUAL_SCREENSHOTS_FAILED_GENERATION.format( 196 | file_link=self.file_link, raw_user_input=raw_user_input 197 | ), 198 | extra_details=error_file, 199 | ) 200 | 201 | await self.reply_message.edit_text( 202 | text=ms.SCREENSHOT_PROCESS_SUCCESS.format( 203 | count=len(valid_positions), total_count=len(screenshots) 204 | ) 205 | ) 206 | await self.media_message.reply_chat_action("upload_video") 207 | await self.media_message.reply_media_group(screenshots, True) 208 | await self.reply_message.edit_text( 209 | ms.PROCESS_UPLOAD_CONFIRM.format( 210 | total_process_duration=datetime.timedelta( 211 | seconds=int(time.time() - start_time) 212 | ) 213 | ) 214 | ) 215 | except ManualScreenshotsProcessFailure as e: 216 | log.error(e) 217 | await self.reply_message.edit_text(e.for_user) 218 | log_msg = await self.media_message.forward(Config.LOG_CHANNEL) 219 | if e.extra_details: 220 | await log_msg.reply_document( 221 | document=e.extra_details, quote=True, caption=e.for_admin 222 | ) 223 | else: 224 | await log_msg.reply_text( 225 | text=e.for_admin, 226 | quote=True, 227 | ) 228 | -------------------------------------------------------------------------------- /bot/utils/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import math 3 | import time 4 | import random 5 | import asyncio 6 | import logging 7 | from urllib.parse import urljoin 8 | 9 | from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton 10 | from pyrogram.emoji import * 11 | from bot.config import Config 12 | from bot.messages import Messages 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | 17 | class ProcessTypes: 18 | SAMPLE_VIDEO = 1 19 | TRIM_VIDEO = 2 20 | MANNUAL_SCREENSHOTS = 3 21 | SCREENSHOTS = 4 22 | MEDIAINFO = 5 23 | 24 | 25 | class Utilities: 26 | @staticmethod 27 | def TimeFormatter(seconds: int) -> str: 28 | minutes, seconds = divmod(seconds, 60) 29 | hours, minutes = divmod(minutes, 60) 30 | days, hours = divmod(hours, 24) 31 | formatted_txt = f"{days} days, " if days else "" 32 | formatted_txt += f"{hours} hrs, " if hours else "" 33 | formatted_txt += f"{minutes} min, " if minutes else "" 34 | formatted_txt += f"{seconds} sec, " if seconds else "" 35 | return formatted_txt[:-2] 36 | 37 | @staticmethod 38 | def is_valid_file(msg): 39 | if not msg.media: 40 | return False 41 | if msg.video: 42 | return True 43 | if (msg.document) and any( 44 | mime in msg.document.mime_type 45 | for mime in ["video", "application/octet-stream"] 46 | ): 47 | return True 48 | return False 49 | 50 | @staticmethod 51 | def is_url(text): 52 | return text.startswith("http") 53 | 54 | @staticmethod 55 | def get_random_start_at(seconds, dur=0): 56 | return random.randint(0, seconds - dur) 57 | 58 | @staticmethod 59 | async def run_subprocess(cmd): 60 | process = await asyncio.create_subprocess_exec( 61 | *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE 62 | ) 63 | return await process.communicate() 64 | 65 | @staticmethod 66 | async def generate_thumbnail_file(file_path, output_folder): 67 | os.makedirs(output_folder, exist_ok=True) 68 | thumb_file = os.path.join(output_folder, "thumb.jpg") 69 | ffmpeg_cmd = [ 70 | "ffmpeg", 71 | "-ss", 72 | "0", 73 | "-i", 74 | file_path, 75 | "-vframes", 76 | "1", 77 | "-vf", 78 | "scale=320:-1", 79 | "-y", 80 | str(thumb_file), 81 | ] 82 | output = await Utilities.run_subprocess(ffmpeg_cmd) 83 | log.debug(output) 84 | if not os.path.exists(thumb_file): 85 | return None 86 | return thumb_file 87 | 88 | @staticmethod 89 | async def generate_stream_link(media_msg): 90 | media_location = f"/app/bot/DOWNLOADS/{media_msg.from_user.id}{media_msg.message_id}/download.mkv" 91 | if not os.path.exists(media_location): 92 | status_msg = await media_msg.reply_text("**Downloading Media File....📥**", quote=True) 93 | start_time = time.time() 94 | media_location = await media_msg.download( 95 | file_name=media_location, 96 | progress=Utilities.progress_bar, 97 | progress_args=(start_time, status_msg) 98 | ) 99 | log.info(media_location) 100 | await status_msg.delete() 101 | return media_location 102 | 103 | @staticmethod 104 | async def progress_bar(current, total, start, msg): 105 | present = time.time() 106 | if round((present - start) % 5) == 0 or current == total: 107 | speed = current / (present - start) 108 | percentage = current * 100 / total 109 | time_to_complete = round(((total - current) / speed)) 110 | time_to_complete = Utilities.TimeFormatter(time_to_complete) 111 | progressbar = "[{0}{1}]".format( 112 | ''.join([f"{BLACK_MEDIUM_SMALL_SQUARE}" for i in range(math.floor(percentage / 10))]), 113 | ''.join([f"{WHITE_MEDIUM_SMALL_SQUARE}" for i in range(10 - math.floor(percentage / 10))]) 114 | ) 115 | current_message = f"**Downloading:** {round(percentage, 2)}%\n\n" 116 | current_message += f"{progressbar}\n\n" 117 | current_message += f"{HOLLOW_RED_CIRCLE} **Speed**: {Utilities.humanbytes(speed)}/s\n\n" 118 | current_message += f"{HOLLOW_RED_CIRCLE} **Done**: {Utilities.humanbytes(current)}\n\n" 119 | current_message += f"{HOLLOW_RED_CIRCLE} **Size**: {Utilities.humanbytes(total)}\n\n" 120 | current_message += f"{HOLLOW_RED_CIRCLE} **Time Left**: {time_to_complete}\n\n" 121 | try: 122 | await msg.edit( 123 | text=current_message 124 | ) 125 | except: 126 | pass 127 | 128 | @staticmethod 129 | def humanbytes(size): 130 | # this code taken from SpEcHiDe Anydl repo 131 | if not size: 132 | return 0 133 | power = 2**10 134 | n = 0 135 | Dic_powerN = {0: ' ', 1: 'K', 2: 'M', 3: 'G', 4: 'T'} 136 | while size > power: 137 | size /= power 138 | n += 1 139 | return str(round(size, 2)) + " " + Dic_powerN[n] + 'B' 140 | 141 | @staticmethod 142 | def TimeFormatter(seconds: int) -> str: 143 | # this code taken from SpEcHiDe Anydl repo 144 | minutes, seconds = divmod(seconds, 60) 145 | hours, minutes = divmod(minutes, 60) 146 | days, hours = divmod(hours, 24) 147 | formatted_txt = f"{days} days, " if days else "" 148 | formatted_txt += f"{hours} hrs, " if hours else "" 149 | formatted_txt += f"{minutes} min, " if minutes else "" 150 | formatted_txt += f"{seconds} sec, " if seconds else "" 151 | return formatted_txt[:-2] 152 | 153 | @staticmethod 154 | async def get_media_info(file_link): 155 | ffprobe_cmd = [ 156 | "ffprobe", 157 | "-i", 158 | file_link, 159 | "-v", 160 | "quiet", 161 | "-of", 162 | "json", 163 | "-show_streams", 164 | "-show_format", 165 | "-show_chapters", 166 | "-show_programs", 167 | ] 168 | data, err = await Utilities.run_subprocess(ffprobe_cmd) 169 | return data 170 | 171 | @staticmethod 172 | async def get_dimentions(file_link): 173 | ffprobe_cmd = [ 174 | "ffprobe", 175 | "-i", 176 | file_link, 177 | "-v", 178 | "error", 179 | "-show_entries", 180 | "stream=width,height", 181 | "-of", 182 | "csv=p=0:s=x", 183 | "-select_streams", 184 | "v:0", 185 | ] 186 | 187 | output = await Utilities.run_subprocess(ffprobe_cmd) 188 | log.debug(output) 189 | try: 190 | width, height = [int(i.strip()) for i in output[0].decode().split("x")] 191 | except Exception as e: 192 | log.debug(e, exc_info=True) 193 | width, height = 1280, 534 194 | return width, height 195 | 196 | @staticmethod 197 | async def get_duration(file_link): 198 | ffmpeg_dur_cmd = [ 199 | "ffprobe", 200 | "-i", 201 | file_link, 202 | "-v", 203 | "error", 204 | "-show_entries", 205 | "format=duration", 206 | "-of", 207 | "csv=p=0:s=x", 208 | "-select_streams", 209 | "v:0", 210 | ] 211 | out, err = await Utilities.run_subprocess(ffmpeg_dur_cmd) 212 | log.debug(f"{out} \n {err}") 213 | out = out.decode().strip() 214 | if not out: 215 | return err.decode() 216 | duration = round(float(out)) 217 | if duration: 218 | return duration 219 | return "No duration!" 220 | 221 | @staticmethod 222 | async def fix_subtitle_codec(file_link): 223 | fixable_codecs = ["mov_text"] 224 | 225 | ffmpeg_dur_cmd = [ 226 | "ffprobe", 227 | "-i", 228 | file_link, 229 | "-v", 230 | "error", 231 | "-select_streams", 232 | "s", 233 | "-show_entries", 234 | "stream=codec_name", 235 | "-of", 236 | "default=noprint_wrappers=1:nokey=1", 237 | ] 238 | 239 | out, err = await Utilities.run_subprocess(ffmpeg_dur_cmd) 240 | log.debug(f"{out} \n {err}") 241 | out = out.decode().strip() 242 | if not out: 243 | return [] 244 | 245 | fix_cmd = [] 246 | codecs = [i.strip() for i in out.split("\n")] 247 | for indx, codec in enumerate(codecs): 248 | if any(fixable_codec in codec for fixable_codec in fixable_codecs): 249 | fix_cmd += [f"-c:s:{indx}", "srt"] 250 | 251 | return fix_cmd 252 | 253 | @staticmethod 254 | def get_watermark_coordinates(pos, width, height): 255 | def gcd(m, n): 256 | return m if not n else gcd(n, m % n) 257 | 258 | def ratio(x, y): 259 | d = gcd(x, y) 260 | return x / d, y / d 261 | 262 | a_ratio = ratio(width, height) 263 | x_fact = 2 264 | x_pad = round((width * x_fact) / 100) 265 | y_pad = round((x_pad * a_ratio[1]) / a_ratio[0]) 266 | 267 | # https://superuser.com/questions/939357/how-to-position-drawtext-text 268 | 269 | if pos == 0: 270 | return x_pad, y_pad # top left 271 | elif pos == 1: 272 | return "(w-text_w)/2", f"{y_pad}" # top center 273 | elif pos == 2: 274 | return f"w-tw-{x_pad}", f"{y_pad}" # top right 275 | elif pos == 3: 276 | return x_pad, "(h-text_h)/2" # center left 277 | elif pos == 4: 278 | return "(w-text_w)/2", "(h-text_h)/2" # centered 279 | elif pos == 5: 280 | return f"w-tw-{x_pad}", "(h-text_h)/2" # center right 281 | elif pos == 6: 282 | return x_pad, f"h-th-{y_pad}" # bottom left 283 | elif pos == 7: 284 | return "(w-text_w)/2", f"h-th-{y_pad}" # bottom center 285 | else: 286 | return f"w-tw-{x_pad}", f"h-th-{y_pad}" # bottom right 287 | 288 | @staticmethod 289 | async def display_settings(c, m, db, cb=False): 290 | chat_id = m.from_user.id if cb else m.chat.id 291 | 292 | as_file = await db.is_as_file(chat_id) 293 | watermark_text = await db.get_watermark_text(chat_id) 294 | sample_duration = await db.get_sample_duration(chat_id) 295 | watermark_color_code = await db.get_watermark_color(chat_id) 296 | watermark_position = await db.get_watermark_position(chat_id) 297 | screenshot_mode = await db.get_screenshot_mode(chat_id) 298 | font_size = await db.get_font_size(chat_id) 299 | mode_txt = "Document" if as_file else "Image" 300 | wm_txt = watermark_text if watermark_text else "No watermark exists!" 301 | genmode = "Equally spaced" if screenshot_mode == 0 else "Random screenshots" 302 | 303 | sv_btn = [ 304 | InlineKeyboardButton("⏱ Sample video Duration", "rj"), 305 | InlineKeyboardButton(f"{sample_duration}s", "set+sv") 306 | ] 307 | wc_btn = [ 308 | InlineKeyboardButton("🎨 Watermark Color", "rj"), 309 | InlineKeyboardButton(f"{Config.COLORS[watermark_color_code]}", "set+wc") 310 | ] 311 | fs_btn = [ 312 | InlineKeyboardButton(f"𝔸𝕒 Watermark Font Size", "rj"), 313 | InlineKeyboardButton(f"{Config.FONT_SIZES_NAME[font_size]}", "set+fs") 314 | ] 315 | wp_btn = [ 316 | InlineKeyboardButton("🎭 Watermark Position", "rj"), 317 | InlineKeyboardButton(f"{Config.POSITIONS[watermark_position]}", "set+wp") 318 | ] 319 | as_file_btn = [ 320 | InlineKeyboardButton("📤 Upload Mode", "rj"), 321 | InlineKeyboardButton(f"{mode_txt}", "set+af") 322 | ] 323 | wm_btn = [ 324 | InlineKeyboardButton("💧 Watermark", "rj"), 325 | InlineKeyboardButton(f"{wm_txt}", "set+wm") 326 | ] 327 | sm_btn = [ 328 | InlineKeyboardButton("📸 SS Gen Mode", "rj"), 329 | InlineKeyboardButton(f"{genmode}", "set+sm") 330 | ] 331 | 332 | settings_btn = [as_file_btn, wm_btn, wc_btn, fs_btn, wp_btn, sv_btn, sm_btn] 333 | 334 | if cb: 335 | try: 336 | await m.message.edit(text=Messages.SETTINGS, reply_markup=InlineKeyboardMarkup(settings_btn)) 337 | except: 338 | pass 339 | return 340 | 341 | await m.reply_text( 342 | text=Messages.SETTINGS, 343 | quote=True, 344 | reply_markup=InlineKeyboardMarkup(settings_btn), 345 | ) 346 | 347 | @staticmethod 348 | def gen_ik_buttons(): 349 | btns = [] 350 | i_keyboard = [] 351 | for i in range(2, 11): 352 | i_keyboard.append(InlineKeyboardButton(f"{i}", f"scht+{i}")) 353 | if (i > 2) and (i % 2) == 1: 354 | btns.append(i_keyboard) 355 | i_keyboard = [] 356 | if i == 10: 357 | btns.append(i_keyboard) 358 | btns.append([InlineKeyboardButton("Manual Screenshots", "mscht")]) 359 | btns.append([InlineKeyboardButton("Trim Video", "trim")]) 360 | btns.append([InlineKeyboardButton("Get Media Information", "mi")]) 361 | return btns 362 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------