diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..e2dfabb Binary files /dev/null and b/.DS_Store differ diff --git a/.env b/.env new file mode 100644 index 0000000..ceff82b --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +API_ID_1 = 24012189 +API_HASH_1 = 5a04d4beafc73f2b3a9d5a64ccb53ae5 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4fd654c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.11 + +WORKDIR /forwardspam_nedvizhka_Ciprus +COPY ./ ./ + +RUN rm -rf /etc/localtime +RUN ln -s /usr/share/zoneinfo/Europe/Moscow /etc/localtime +RUN echo "Europe/Moscow" > /etc/timezone + +RUN pip install --upgrade pip +RUN pip install --no-cache-dir -r requirements.txt + +CMD ["python","-u", "main.py"] \ No newline at end of file diff --git a/bot/__init__.py b/bot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/__pycache__/__init__.cpython-310.pyc b/bot/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..f173ed1 Binary files /dev/null and b/bot/__pycache__/__init__.cpython-310.pyc differ diff --git a/bot/__pycache__/album_handler.cpython-310.pyc b/bot/__pycache__/album_handler.cpython-310.pyc new file mode 100644 index 0000000..e960c07 Binary files /dev/null and b/bot/__pycache__/album_handler.cpython-310.pyc differ diff --git a/bot/__pycache__/config.cpython-310.pyc b/bot/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000..5604f5a Binary files /dev/null and b/bot/__pycache__/config.cpython-310.pyc differ diff --git a/bot/__pycache__/handlers.cpython-310.pyc b/bot/__pycache__/handlers.cpython-310.pyc new file mode 100644 index 0000000..faaf6f2 Binary files /dev/null and b/bot/__pycache__/handlers.cpython-310.pyc differ diff --git a/bot/__pycache__/loader.cpython-310.pyc b/bot/__pycache__/loader.cpython-310.pyc new file mode 100644 index 0000000..927a97b Binary files /dev/null and b/bot/__pycache__/loader.cpython-310.pyc differ diff --git a/bot/__pycache__/scheduler.cpython-310.pyc b/bot/__pycache__/scheduler.cpython-310.pyc new file mode 100644 index 0000000..2f31d46 Binary files /dev/null and b/bot/__pycache__/scheduler.cpython-310.pyc differ diff --git a/bot/__pycache__/sending.cpython-310.pyc b/bot/__pycache__/sending.cpython-310.pyc new file mode 100644 index 0000000..66edcdd Binary files /dev/null and b/bot/__pycache__/sending.cpython-310.pyc differ diff --git a/bot/album_handler.py b/bot/album_handler.py new file mode 100644 index 0000000..08f45d3 --- /dev/null +++ b/bot/album_handler.py @@ -0,0 +1,72 @@ +import asyncio +import traceback +from dataclasses import dataclass +from collections import defaultdict +from typing import Coroutine, TypeVar + +from pyrogram import Client, filters +from pyrogram.types import Message + +from bot import config +from bot.loader import app1 +from bot.sending import forward_post + +_tasks = set() +T = TypeVar("T") + + +def background(coro: Coroutine[None, None, T]) -> asyncio.Task[T]: + loop = asyncio.get_event_loop() + task = loop.create_task(coro) + _tasks.add(task) + task.add_done_callback(_tasks.remove) + return task + + +@dataclass +class Album: + media_group_id: str + messages: list[Message] + + +# chat_id: group_id: album +_albums: defaultdict[int, dict[str, Album]] = defaultdict(dict) + + +@app1.on_message(filters.media_group) +async def on_media_group(client: Client, message: Message): + try: + chat_id = message.chat.id + media_group_id = message.media_group_id + if media_group_id is None: + return + + if media_group_id not in _albums[chat_id]: + album = Album(messages=[message], media_group_id=media_group_id) + _albums[chat_id][media_group_id] = album + + async def task(): + await asyncio.sleep(1) + _albums[chat_id].pop(media_group_id, None) + try: + album.messages.sort(key=lambda m: m.id) + await on_album(client, album) + except Exception: + traceback.print_exc() + + background(task()) + else: + album = _albums[chat_id][media_group_id] + album.messages.append(message) + finally: + message.continue_propagation() + + +async def on_album(client: Client, album: Album): + """ + Обрабатывает новые альбомы + """ + if album.messages[0].chat.id == config.rent_channel_id: + await forward_post(client, album.messages[0], config.groups_for_rent) + if album.messages[0].chat.id == config.sell_channel_id: + await forward_post(client, album.messages[0], config.groups_for_sell) diff --git a/bot/config.py b/bot/config.py new file mode 100644 index 0000000..102d10a --- /dev/null +++ b/bot/config.py @@ -0,0 +1,26 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + + +API_ID_1 = os.getenv("API_ID_1") +API_HASH_1 = os.getenv("API_HASH_1") + +admins: list[int] = [5899041406, 800530092, 1046931046] + +# Интвервал спама постов (в секундах) +spam_interval = 200 + + +# айди канала/группы берем из веб версии тг +# или с помощью @username_to_id_bot + +# Канал по аренде +rent_channel_id = -1002054380269 +# Канал по продаже +sell_channel_id = -1002123908331 + +# Группы для перессылки +groups_for_rent = ["-1002227589874_13"] +groups_for_sell = ["-1002227589874_13"] \ No newline at end of file diff --git a/bot/cyprus_off.session b/bot/cyprus_off.session new file mode 100644 index 0000000..529f142 Binary files /dev/null and b/bot/cyprus_off.session differ diff --git a/bot/cyprus_off.session-journal b/bot/cyprus_off.session-journal new file mode 100644 index 0000000..fd949ce Binary files /dev/null and b/bot/cyprus_off.session-journal differ diff --git a/bot/handlers.py b/bot/handlers.py new file mode 100644 index 0000000..428e0af --- /dev/null +++ b/bot/handlers.py @@ -0,0 +1,74 @@ +import asyncio +from pprint import pprint + +from pyrogram.enums import ParseMode +from pyrogram.errors import PeerIdInvalid +from pyrogram.types import Message + +from bot import config +from bot.loader import app1, scheduler +from pyrogram import filters + +from bot.sending import is_valid_time_format, sending, check_stop_sign, forward_post, get_number_posts, set_number_posts + + +class Sending: + SEND = True + + +recently_media_groups = set() + + +@app1.on_message(filters.chat(config.admins) & filters.command('current')) +async def current_tasks(client, message: Message): + """Присылает текущие таймслоты на пересылку""" + text = "Текущие расписание постов:\n" + '\n'.join([time for _, time, _ in scheduler.tasks]) + if len(scheduler.tasks) == 0: text += "Пусто" + # print(message.text) + await message.reply(text) + + +@app1.on_message(filters.chat(config.admins) & filters.command('add')) +async def add(client, message: Message): + """Добавление нового времени""" + if len(message.command) < 2 or not is_valid_time_format(message.command[1]): + await message.reply("Введите /add HH:MM") + return + time = message.command[1] + await scheduler.add_task(task=sending, run_time=time) + await current_tasks(app1, message) + + +@app1.on_message(filters.chat(config.admins) & filters.command('delete')) +async def delete(client, message: Message): + """Удаление нового времени""" + if len(message.command) < 2 or not is_valid_time_format(message.command[1]): + await message.reply("Введите /add HH:MM") + return + time = message.command[1] + if await scheduler.remove_task(sending, time): + await message.reply(f"Успешно удалено время {time}") + await current_tasks(client, message) + + +@app1.on_message(filters.chat(config.admins) & filters.command('switch')) +async def switch(client, message: Message): + """ вкл/выкл пересылки """ + if Sending.SEND: + Sending.SEND = False + else: + Sending.SEND = True + await message.reply(f"моментальная пересылка {'вкл' if Sending.SEND else 'выкл'}") + + +@app1.on_message(filters.chat(config.admins) & filters.command('set_posts')) +async def set_posts(client, message: Message): + """ установить нужное количество постов для пересылки """ + global g + try: + set_number_posts(int(message.command[1])) + await message.reply(f"Количество постов {get_number_posts()} установлено") + except: + await message.reply("Введите /set_posts число") + + diff --git a/bot/loader.py b/bot/loader.py new file mode 100644 index 0000000..3ab7aa3 --- /dev/null +++ b/bot/loader.py @@ -0,0 +1,14 @@ +from pyrogram import Client + +from bot import config +from bot.scheduler import AsyncScheduler + +api_id_1 = config.API_ID_1 +api_hash_1 = config.API_HASH_1 + +app1 = Client("cyprus_off", api_id=api_id_1, api_hash=api_hash_1, workdir='bot') + +print(f'client is wait ...') +print(app1) + +scheduler = AsyncScheduler() diff --git a/bot/loader.py.save b/bot/loader.py.save new file mode 100644 index 0000000..2c487be --- /dev/null +++ b/bot/loader.py.save @@ -0,0 +1,11 @@ +from pyrogram import Client + +from bot import config +from bot.scheduler import AsyncScheduler + +api_id_1 = config.API_ID_1 +api_hash_1 = config.API_HASH_1 + +app1 = Client("cyprus_off", api_id=api_id_1, api_hash=api_hash_1, workdir='bot') + +scheduler = AsyncScheduler() diff --git a/bot/scheduler.py b/bot/scheduler.py new file mode 100644 index 0000000..32bc00c --- /dev/null +++ b/bot/scheduler.py @@ -0,0 +1,37 @@ +import asyncio +import time +from typing import Callable, Awaitable, Coroutine + + +class AsyncScheduler: + """ + Запускает, останавливает ассинхронные задачаи в заданное время + """ + def __init__(self): + self.tasks = [] + + async def task_runner(self, task: Callable, run_time: str): + """ + Запускает задачу, которая выполняет каждый день в заданный run_time + :param task: await Функция + :param run_time: строка "HH:MM" + """ + while True: + current_time = time.strftime("%H:%M", time.localtime()) + if current_time == run_time: + await task() + await asyncio.sleep(60) + + async def add_task(self, task, run_time): + """Добавление новой задачи в определенное время""" + task_instance = asyncio.create_task(self.task_runner(task, run_time)) + self.tasks.append((task, run_time, task_instance)) + + async def remove_task(self, task, run_time) -> bool: + """Удаление сущесвтующей задачи по функции и времени""" + for t, r_t, task_instance in self.tasks: + if t == task and r_t == run_time: + task_instance.cancel() + self.tasks.remove((t, r_t, task_instance)) + return True + return False diff --git a/bot/sending.py b/bot/sending.py new file mode 100644 index 0000000..df6d0e4 --- /dev/null +++ b/bot/sending.py @@ -0,0 +1,108 @@ +import asyncio +import re +from datetime import datetime + +from pyrogram import Client +from pyrogram.types import Message, InputMedia, InputMediaPhoto + +from bot import config +from bot.loader import app1 + +from random import randint + + +NUMBER_POSTS = 20 + + +def get_number_posts(): + global NUMBER_POSTS + return NUMBER_POSTS + + +def set_number_posts(x: int): + global NUMBER_POSTS + NUMBER_POSTS = x + + +async def sending(): + """ + Функция для пересылки по расписанию. + """ + await forwards_to_chats(config.rent_channel_id, config.sell_channel_id, config.groups_for_rent, get_number_posts()) + # await forwards_to_chats(config.sell_channel_id, config.groups_for_sell, get_number_posts()) + + +# contains date: client: media groups +class DailyAlbums: + todays_albums = set() + + @classmethod + def add_album(cls, client: Client, media_group_id: int): + album_key = (client.api_id, datetime.now().strftime('%d.%m.%Y'), media_group_id) + cls.todays_albums.add(album_key) + + @classmethod + def contains_album(cls, client: Client, media_group_id: int) -> bool: + album_key = (client.api_id, datetime.now().strftime('%d.%m.%Y'), media_group_id) + return album_key in cls.todays_albums + + +async def fetch_posts(chat_id: int, daily_albums: DailyAlbums, limit: int): + """ + Получает посты из чата, проверяя, что они ещё не находятся в daily_albums. + """ + posts = [] + async for post in app1.get_chat_history(chat_id=chat_id, limit=200): + if post.media_group_id and post.caption and not daily_albums.contains_album(app1, post.media_group_id): + posts.append(post) + daily_albums.add_album(app1, post.media_group_id) + if len(posts) >= limit: + break + await asyncio.sleep(0.1) + return posts + + +async def forwards_to_chats(rent_channel_id: int, sell_channel_id: int, groups: list[str], limit: int): + """ + Пересылает посты из rent_channel_id и sell_channel_id в группы. + """ + if not groups: + return + + daily_albums = DailyAlbums() + messages = await fetch_posts(rent_channel_id, daily_albums, limit) + messages2 = await fetch_posts(sell_channel_id, daily_albums, limit) + + all_messages = messages + messages2 + for i in range(min(limit, len(all_messages))): + await forward_post(app1, all_messages[i], groups) + await asyncio.sleep(config.spam_interval) + + +def is_valid_time_format(time_str): + """Проверяют строку на формат HH:MM""" + time_pattern = re.compile(r'^[0-2][0-9]:[0-5][0-9]$') + return bool(time_pattern.match(time_str)) + + +def check_stop_sign(message: Message): + return "⛔️" in str(message.caption) or "⛔️" in str(message.text) + + +async def forward_post(client: Client, message: Message, groups: list[int, str]): + # === СТОП ЗНАК === + if check_stop_sign(message) or not (message.caption or message.text): + return + # =============================== + for spam_group in groups: + group_id, reply_id = (spam_group, None) if '_' not in str(spam_group) else list( + map(int, str(spam_group).split('_'))) + try: + if message.media_group_id: + await client.copy_media_group(chat_id=group_id, from_chat_id=message.chat.id, message_id=message.id, + reply_to_message_id=reply_id) + else: + await message.copy(group_id, reply_to_message_id=reply_id) + except Exception as e: + print(f"Ошибка: {e}\n\nГруппа: {group_id}\n\n-----------------------\n\n") + await asyncio.sleep(2) diff --git a/bot/test.py b/bot/test.py new file mode 100644 index 0000000..e704b13 --- /dev/null +++ b/bot/test.py @@ -0,0 +1,11 @@ +from pyrogram import Client + +api_id = 29370778 +api_hash = '9beeb54652c5ce4f323d67f57dc233be' + +with Client("my_account", api_id, api_hash) as app: + # Идентификатор чата, из которого вы хотите получить сообщения + chat_id = "your_chat_id" + # Количество сообщений, которые вы хотите получить (максимальное значение - 100) + me = app.get_me() + print(me) diff --git a/main.py b/main.py new file mode 100644 index 0000000..31696fb --- /dev/null +++ b/main.py @@ -0,0 +1,22 @@ +import asyncio + +from pyrogram import compose, idle + +from bot import config +from bot.loader import app1 + +from bot import handlers +from bot import album_handler + +def main(): + print("Bot started.") + # compose([app1]) + + print('test') + + app1.run() + + +if __name__ == '__main__': + # main() + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..dbc7d52 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +croniter==2.0.1 +pyaes==1.6.1 +https://github.com/KurimuzonAkuma/pyrogram/archive/dev.zip +PySocks==1.7.1 +python-dateutil==2.8.2 +python-dotenv==1.0.1 +pytz==2024.1 +six==1.16.0 +TgCrypto==1.2.5 +tzdata==2023.4 +tzlocal==5.2