diff --git a/app/custom.py b/app/custom.py index e06d444..90a860b 100644 --- a/app/custom.py +++ b/app/custom.py @@ -8,6 +8,7 @@ from json import JSONDecodeError import tweepy from dotenv import load_dotenv +from telethon.tl.functions.channels import GetMessagesRequest load_dotenv() @@ -69,7 +70,7 @@ def load_x_v2_api_oauth2_handler(): async def create_tweet(content, media_ids=None): client = load_x_v2_api() - post = client.create_tweet(text=content, media_ids=media_ids) + post = client.create_tweet(text=content, media_ids=media_ids if media_ids else None) if post.data: open("{}/created.txt".format(os.getenv('DATA_FOLDER')), "a+").write("{}\n".format(post.data['id'])) return post.data @@ -96,78 +97,107 @@ async def find_text_and_download_media(bot, message): text = message.message media_path, media_size = None, None media_channel_id = message.peer_id.channel_id - # everything else - if hasattr(message.media, 'document'): - try: - # check video length - if message.document.attributes[0].duration > 140: - raise Exception("Video is too long") - except (AttributeError, IndexError): - pass - mime_type = message.document.mime_type.split('/') - media_id = message.media.document.id - media_path = "{}/{}.{}".format(media_folder, media_id, mime_type[1]) - media_size = message.media.document.size - # compressed pictures - elif hasattr(message.media, 'photo'): - media_id = message.media.photo.id - media_path = "{}/{}.{}".format(media_folder, media_id, 'jpg') - media_size = message.media.photo.size if hasattr(message.media.photo, 'size') else message.media.photo.sizes[-1].sizes[-1] - # check filesize - if media_size > 536870912: - raise Exception("File is too big") # create a download progress file os.makedirs(progress_folder := "{}/progress".format(os.getenv('DATA_FOLDER')), exist_ok=True) media_progress_file = "{}/{}-{}.json".format(progress_folder, media_channel_id, message.id) progress_data = {"last_update": time.time(), "percent": 0, "file_path": media_path, "cancelled": False, "id": None} - if media_path: - # get local media size - if os.path.exists(media_path): - _media_size = os.path.getsize(media_path) - else: - _media_size = 0 - # check if sizes match - if media_size > _media_size: - progress_message = await message.reply("Downloading from TG... 0%") - progress_data['id'] = progress_message.id - # deleting existing file - if os.path.exists(media_path): - os.remove(media_path) - open(media_progress_file, "w").write(json.dumps(progress_data)) - - # callback to update the download progress - async def progress_callback(current, total): - progress = json.loads(open(media_progress_file, "r").read()) - percent = round(current / total, 2) - if progress['cancelled'] is True: - await bot.edit_message(progress_message, "Downloading from TG... cancelled!!") - raise Exception('Cancelled') - if float(progress['last_update']) + 3 < time.time(): - try: - await bot.edit_message(progress_message, "Downloading from TG... {}%".format(round(percent * 100, 1))) - except Exception as e: - logging.error(e) - else: - progress['last_update'] = time.time() - progress['percent'] = percent - open(media_progress_file, "w").write(json.dumps(progress)) - - # download a new copy of the media - await bot.download_media(message, media_path, progress_callback=progress_callback) - # show it's completed - await bot.edit_message(progress_message, "Downloading from TG... 100%") - open(media_progress_file, "w").write(json.dumps(progress_data)) - return text, media_path - -async def upload_media(filename): - api = load_x_v1_api() - media = api.media_upload(filename) - if hasattr(media, 'image') or media.processing_info['state'] == 'succeeded': - return media.media_id + media = [] + # get any other messages + try: + result = await bot(GetMessagesRequest(id=list(range(message.id, message.id + int(os.getenv('X_MAX_MEDIA')))), channel=message.chat_id)) + for m in result.messages: + if m.date != message.date: + break + media.append(m.media) + except Exception as e: + logging.error(e) else: - return False + if len(media) == 0: + return text, media + + # setup the message to notify user media is downloading + progress_message = await message.reply("Downloading from TG... 0%") + progress_data['id'] = progress_message.id + open(media_progress_file, "w").write(json.dumps(progress_data)) + + # download all media + media_paths = [] + for m in media: + # everything else + if hasattr(m, 'document'): + try: + # check video length + if m.document.attributes[0].duration > 140: + raise Exception("Video is too long") + except (AttributeError, IndexError): + pass + mime_type = m.document.mime_type.split('/') + media_id = m.document.id + media_path = "{}/{}.{}".format(media_folder, media_id, mime_type[1]) + media_size = m.document.size + # compressed pictures + elif hasattr(m, 'photo'): + media_id = m.photo.id + media_path = "{}/{}.{}".format(media_folder, media_id, 'jpg') + if hasattr(m.photo, 'size'): + media_size = m.photo.size + else: + media_size = m.photo.sizes[-1].sizes[-1] + # check filesize + if media_size > 536870912: + raise Exception("File is too big") + if media_path: + # get local media size, this deletes any incomplete downloads + if os.path.exists(media_path): + _media_size = os.path.getsize(media_path) + else: + _media_size = 0 + # check if sizes match + if media_size > _media_size: + # deleting existing file + if os.path.exists(media_path): + os.remove(media_path) + + # callback to update the download progress + async def progress_callback(current, total): + progress = json.loads(open(media_progress_file, "r").read()) + percent = round(current / total, 2) + if progress['cancelled'] is True: + await bot.edit_message(progress_message, "Downloading from TG... cancelled!!") + raise Exception('Cancelled') + if float(progress['last_update']) + 3 < time.time(): + try: + await bot.edit_message(progress_message, "Downloading from TG... {}%".format(round(percent * 100, 1))) + except Exception as e: + logging.error(e) + else: + progress['last_update'] = time.time() + progress['percent'] = percent + open(media_progress_file, "w").write(json.dumps(progress)) + + # download a new copy of the media + await bot.download_media(m, media_path, progress_callback=progress_callback) + # show it's completed + await bot.edit_message(progress_message, "Downloading from TG... 100%") + open(media_progress_file, "w").write(json.dumps(progress_data)) + + # store path to file in array + if os.path.exists(media_path): + media_paths.append(media_path) + + return text, media_paths + + +async def upload_media(filenames): + uploaded = [] + api = load_x_v1_api() + for f in filenames: + media = api.media_upload(f) + if hasattr(media, 'image') or media.processing_info['state'] == 'succeeded': + uploaded.append(media.media_id) + return uploaded def refresh_x_oauth2_token(): diff --git a/app/main.py b/app/main.py index 2332bcd..6a7ae4e 100644 --- a/app/main.py +++ b/app/main.py @@ -25,7 +25,7 @@ for package in (silence_packages := ( bot = TelegramClient('bot', int(os.getenv('TELEGRAM_APP_ID')), os.getenv('TELEGRAM_APP_HASH')).start(bot_token=os.getenv('TELEGRAM_BOT_TOKEN')) -@bot.on(events.NewMessage) +@bot.on(events.NewMessage(pattern='.*')) async def echo(event): user_input = event.text # skip if bot or no one sent a command @@ -116,40 +116,43 @@ async def echo(event): user_name = get_name_from_user(event.message.from_user) record_path = "{}/posts/tweet_{}_{}.json".format(os.getenv('DATA_FOLDER'), chat_id, message_id) - # get text, switch to caption if media is attached - try: - text, media_path = await find_text_and_download_media(bot, reply_to_message or event.message) - except Exception as e: - if os.path.exists(media_progress_file): - os.remove(media_progress_file) - if "Cancelled" in str(e): - logging.debug("Cancelled media download") - return - logging.error(e) - if "File is too big" in str(e): - return await event.reply("Error: file is too big for X") - elif "Video is too long" in str(e): - return await event.reply("Error: video is too long for X") + media_paths = [] + if (reply_to_message and reply_to_message.media) or (event.message and event.message.media): + # get text, switch to caption if media is attached + try: + text, media_paths = await find_text_and_download_media(bot, reply_to_message or event.message) + except Exception as e: + if os.path.exists(media_progress_file): + os.remove(media_progress_file) + if "Cancelled" in str(e): + logging.debug("Cancelled media download") + return + logging.error(e) + if "File is too big" in str(e): + return await event.reply("Error: file is too big for X") + elif "Video is too long" in str(e): + return await event.reply("Error: video is too long for X") + else: + return await event.reply("Error: unknown TG download error") else: - return await event.reply("Error: unknown TG download error") + logging.info("{} ({}) Downloaded file(s): {}".format(user_name, user_id, media_paths)) + media_progress = json.loads(open(media_progress_file, "r").read()) + if media_progress['id']: + await bot.edit_message(channel_id, media_progress['id'], "Uploading to X...") + else: + progress_message = await event.reply("Uploading to X...") + media_progress['id'] = progress_message.id + open(media_progress_file, "w").write(json.dumps(media_progress)) else: - logging.info("{} ({}) Downloaded file: {}".format(user_name, user_id, media_path)) - media_progress = json.loads(open(media_progress_file, "r").read()) - if media_progress['id']: - await bot.edit_message(channel_id, media_progress['id'], "Uploading to X...") - else: - progress_message = await event.reply("Uploading to X...") - media_progress['id'] = progress_message.id - open(media_progress_file, "w").write(json.dumps(media_progress)) - + text = reply_to_message.message or event.message.message text = text.replace(user_input[0], '').strip() if text else '' # check if tweet is empty - if len(user_input) == 1 and not media_path and not text: + if len(user_input) == 1 and not media_paths and not text: return await event.reply("Not enough parameters") # upload media try: - media_id = await upload_media(media_path) if media_path else None + media_ids = await upload_media(media_paths) if media_paths else None except Exception as e: logging.error(e) if "maxFileSizeExceeded" in str(e): @@ -157,19 +160,19 @@ async def echo(event): else: return await event.reply("Error: unknown X upload error") else: - if media_id: - logging.info("{} ({}) Uploaded file: {}".format(user_name, user_id, media_path)) - elif media_id is False: - logging.info("{} ({}) Failed to uploaded file: {}".format(user_name, user_id, media_path)) + if media_ids: + logging.info("{} ({}) Uploaded file(s): {}".format(user_name, user_id, media_paths)) + elif media_ids is False: + logging.info("{} ({}) Failed to uploaded file(s): {}".format(user_name, user_id, media_paths)) finally: if os.path.exists(media_progress_file): media_progress = json.loads(open(media_progress_file, "r").read()) await bot.delete_messages(channel_id, media_progress['id']) os.remove(media_progress_file) - # send tweet try: - post = await create_tweet(text, [media_id] if media_id else None) + # send tweet + post = await create_tweet(text, media_ids) except Exception as e: logging.error(e) if "include either text or media" in str(e): diff --git a/app/sample.env b/app/sample.env index 5180db5..85fbe2e 100644 --- a/app/sample.env +++ b/app/sample.env @@ -10,4 +10,5 @@ X_ACCESS_SECRET= X_CONSUMER_KEY= X_CONSUMER_SECRET= X_CLIENT_ID= -X_CLIENT_SECRET= \ No newline at end of file +X_CLIENT_SECRET= +X_MAX_MEDIA=10 \ No newline at end of file