refactor: extract userbot to standalone repo, add as git submodule
Deploy to Server / deploy (push) Has been cancelled

userbot now lives at ssh://git@gitssh.forust.xyz:2221/forust/userbot.git
and is included in homelab as a submodule at userbot/
This commit is contained in:
2026-06-19 15:39:00 +02:00
parent 227e5fda27
commit 3c383db9a7
134 changed files with 4 additions and 15257 deletions
-110
View File
@@ -1,110 +0,0 @@
import random
import re
from io import BytesIO
import requests
from bs4 import BeautifulSoup as BS
from pyrogram import Client, filters
from pyrogram.errors import RPCError
from pyrogram.types import InputMediaPhoto, Message
from utils.misc import modules_help, prefix
@Client.on_message(filters.command('icon', prefix) & filters.me)
async def search_icon(_, message: Message):
if len(message.command) != 2:
return await message.edit_text('Please provide some text to search icons from Flaticon.com.')
query = message.text.split(maxsplit=1)[1]
await message.edit_text('Searching for icons...')
search_query = query.replace(' ', '%20')
url = f'https://www.flaticon.com/search?word={search_query}'
try:
html_content = requests.get(url, timeout=10).text
soup = BS(html_content, 'html.parser')
results = soup.find_all(
'img',
src=re.compile(r'https://cdn-icons-png.flaticon.com/128/[0-9]+/[0-9]+.png'),
)
if not results:
return await message.edit('No results found.')
random.shuffle(results)
icons = []
for i in range(5):
icons.append(results[i]['src'].replace('128', '512'))
for icon in icons:
await message.reply_document(icon)
return await message.delete()
except Exception as e:
await message.edit(f'An error occurred: {e}')
print(f'Error: {e}')
@Client.on_message(filters.command('freepik', prefix) & filters.me)
async def freepik_search(client: Client, message: Message):
parts = message.text.split(' ', 1)
if len(parts) < 2:
await message.edit_text('Please provide a search query!')
return
query = parts[1]
limit = 5
if ' ; ' in query:
match, limit_str = query.split(' ; ', 1)
try:
limit = int(limit_str)
except ValueError:
await message.edit_text('Invalid limit! Using the default value of 5.')
else:
match = query
match = match.replace(' ', '%20')
await message.edit_text('Searching Freepik...')
try:
url = f'https://www.freepik.com/api/regular/search?locale=en&term={match}'
json_content = requests.get(url, timeout=10).json()
results = []
for i in json_content['items']:
results.append(i['preview']['url'])
if results is None:
return await message.edit_text('No results found.')
random.shuffle(results)
img_urls = results[:limit]
media_group = []
for img_url in img_urls:
icon = requests.get(img_url, timeout=10)
if icon.status_code == 200:
media_group.append(InputMediaPhoto(media=BytesIO(icon.content)))
if not media_group:
await message.edit_text('No images could be downloaded.')
return
try:
await client.send_media_group(chat_id=message.chat.id, media=media_group)
except RPCError:
await message.edit_text('Failed to send some images. Retrying individually...')
for media in media_group:
try:
await message.reply_photo(photo=media.media)
except Exception as e:
await message.edit_text(f'Error sending image: {e}')
except Exception as e:
await message.edit_text(f'Failed to fetch data: {e}')
print(f'Error: {e}')
modules_help['icons'] = {
'icon [query]': 'Search for icons on Flaticon.',
'freepik [query] [limit]': 'Search for images on Freepik. Limit is optional and defaults to 5.',
}
-47
View File
@@ -1,47 +0,0 @@
import base64
import requests
from pyrogram import Client, filters
from pyrogram.types import Message
from utils.misc import modules_help, prefix
@Client.on_message(filters.command(['imgur'], prefix) & filters.me)
async def imgur(_, message: Message):
# Check if a reply exists
msg = await message.edit_text('🎉 Please wait. trying to upload...')
if message.reply_to_message and message.reply_to_message.photo:
# Download the photo
photo_path = await message.reply_to_message.download()
# Read the photo file and encode as base64
with open(photo_path, 'rb') as file:
data = file.read()
base64_data = base64.b64encode(data)
# Set API endpoint and headers for image upload
url = 'https://api.imgur.com/3/image'
headers = {'Authorization': 'Client-ID a10ad04550b0648'}
# Upload image to Imgur and get URL
response = requests.post(url, headers=headers, data={'image': base64_data}, timeout=10)
result = response.json()
await msg.edit_text(result['data']['link'])
elif message.reply_to_message and message.reply_to_message.animation:
# Download the animation (GIF)
animation_path = await message.reply_to_message.download()
# Read the animation file and encode as base64
with open(animation_path, 'rb') as file:
data = file.read()
base64_data = base64.b64encode(data)
# Set API endpoint and headers for animation upload
url = 'https://api.imgur.com/3/image'
headers = {'Authorization': 'Client-ID a10ad04550b0648'}
# Upload animation to Imgur and get URL
response = requests.post(url, headers=headers, data={'image': base64_data}, timeout=10)
result = response.json()
await msg.edit_text(result['data']['link'])
else:
await msg.edit_text('Please reply to a photo or animation (GIF) to upload to Imgur.')
modules_help['imgur'] = {
'imgur [img]*': 'upload a photo or animation (GIF) to imgur',
}
-47
View File
@@ -1,47 +0,0 @@
import os
import pygments
from pygments.formatters import ImageFormatter
from pygments.lexers import Python3Lexer
from pyrogram import Client, filters
from pyrogram.errors import MessageNotModified
from pyrogram.types import Message
from utils.misc import modules_help, prefix
@Client.on_message(filters.command('ncode', prefix) & filters.me)
async def coder_print(client, message: Message):
if message.reply_to_message:
reply_message = message.reply_to_message
if reply_message.media:
download_path = await client.download_media(reply_message)
with open(download_path) as file:
code = file.read()
if os.path.exists(download_path):
os.remove(download_path)
pygments.highlight(
f'{code}',
Python3Lexer(),
ImageFormatter(font_name='DejaVu Sans Mono', line_numbers=True),
'result.png',
)
try:
sent_message = await message.edit_text('Pasting this code on my page...')
await client.send_document(
chat_id=message.chat.id,
document='result.png',
caption='Code highlighted by Pygments',
reply_to_message_id=message.id,
)
except MessageNotModified:
pass
await sent_message.delete()
if os.path.exists('result.png'):
os.remove('result.png')
else:
return await message.reply_text('Please reply to a text or a file.')
else:
return await message.reply_text('Please reply to a text or a file.')
modules_help['ncode'] = {'ncode': 'Highlight the code using Pygments and send it as an image.'}
-91
View File
@@ -1,91 +0,0 @@
import asyncio
from io import BytesIO
import requests
from PIL import Image
from pyrogram import Client, enums, filters
from pyrogram.types import InputMediaPhoto, Message
from utils.misc import modules_help, prefix
# Pinterest API URL
API_URL = 'https://bk9.fun/pinterest/search?q='
def resize_image(image_bytes):
try:
with Image.open(image_bytes) as img:
max_size = (1280, 1280)
if img.size > max_size:
img.thumbnail(max_size)
output = BytesIO()
img.save(output, format='JPEG')
output.seek(0)
return output
image_bytes.seek(0) # Reset pointer if not resized
return image_bytes
except Exception as e:
print(f'Error resizing image: {e}')
return image_bytes
async def download_image(url):
try:
response = requests.get(url, timeout=10)
if response.status_code == 200:
img_bytes = BytesIO(response.content)
return resize_image(img_bytes)
except Exception as e:
print(f'Error downloading image: {e}')
return None
@Client.on_message(filters.command('pinterest', prefix) & filters.me)
async def pinterest_search(client: Client, message: Message):
if len(message.command) < 2:
await message.edit('Usage: `pinterest [number] <query>`', parse_mode=enums.ParseMode.MARKDOWN)
return
num_pics = int(message.command[1]) if message.command[1].isdigit() else 10
query = ' '.join(message.command[2:])
# Update status
status_message = await message.edit('Searching for images...', parse_mode=enums.ParseMode.MARKDOWN)
url = f'{API_URL}{query}'
response = requests.get(url, timeout=10)
if response.status_code == 200:
data = response.json()
if data.get('status'):
urls = [item['images_url'] for item in data.get('BK9', [])[:num_pics]]
images = [download_image(img_url) for img_url in urls]
# Download images
downloaded_images = await asyncio.gather(*images)
media = [InputMediaPhoto(media=img_bytes) for img_bytes in downloaded_images if img_bytes]
if media:
await status_message.edit('Uploading pictures...', parse_mode=enums.ParseMode.MARKDOWN)
while media:
batch = media[:10]
media = media[10:]
await message.reply_media_group(batch)
await status_message.delete() # Delete status message after uploading
else:
await status_message.edit('No valid images found.', parse_mode=enums.ParseMode.MARKDOWN)
else:
await status_message.edit(
'No images found for the given query.',
parse_mode=enums.ParseMode.MARKDOWN,
)
else:
await status_message.edit(
'An error occurred, please try again later.',
parse_mode=enums.ParseMode.MARKDOWN,
)
modules_help['pinterest'] = {
'pinterest [number]* [query]': 'Get images from Pinterest. Default number of images is 10',
}
-100
View File
@@ -1,100 +0,0 @@
import os
import requests
from modules.url import generate_screenshot
from pyrogram import Client, filters
from pyrogram.types import Message
from utils.misc import modules_help, prefix
# API endpoints for reverse image search engines
SEARCH_ENGINES = {
'lens': 'https://lens.google.com/uploadbyurl?url={image}',
'reverse': 'https://www.google.com/searchbyimage?sbisrc=4chanx&image_url={image}&safe=off',
'tineye': 'https://www.tineye.com/search?url={image}',
'bing': 'https://www.bing.com/images/search?view=detailv2&iss=sbi&form=SBIVSP&sbisrc=UrlPaste&q=imgurl:{image}',
'yandex': 'https://yandex.com/images/search?source=collections&&url={image}&rpt=imageview',
'saucenao': 'https://saucenao.com/search.php?db=999&url={image}',
}
@Client.on_message(filters.command('risearch', prefix) & filters.reply)
async def reverse_image_search(client: Client, message: Message):
if not message.reply_to_message or not message.reply_to_message.photo:
await message.reply_text(
f'Please reply to an image with <code>{prefix}risearch [engine]</code> or <code>{prefix}risearch</code>.'
)
return
command_parts = message.text.split(maxsplit=1)
engines_to_use = (
[command_parts[1].strip().lower()]
if len(command_parts) > 1 and command_parts[1].strip()
else list(SEARCH_ENGINES.keys())
)
invalid_engines = [engine for engine in engines_to_use if engine not in SEARCH_ENGINES]
if invalid_engines:
await message.reply_text(
f'Invalid engine(s): {", ".join(invalid_engines)}. Available: {", ".join(SEARCH_ENGINES.keys())}'
)
return
processing_message = await message.edit_text('Processing the image...')
try:
# Download and upload the image
photo_path = await message.reply_to_message.download()
img_url = upload_image(photo_path)
print(img_url)
if not img_url:
await processing_message.edit('Error: Could not upload the image.')
return
# Perform searches for the selected engines
for engine in engines_to_use:
search_url = SEARCH_ENGINES[engine].format(image=img_url)
await send_screenshot(client, message, search_url, engine)
except Exception as e:
await processing_message.edit(f'An error occurred: {e}')
finally:
if photo_path and os.path.exists(photo_path):
os.remove(photo_path)
def upload_image(photo_path):
"""Uploads an image to tmpfiles.org and returns the direct download URL."""
try:
with open(photo_path, 'rb') as image_file:
response = requests.post('https://tmpfiles.org/api/v1/upload', files={'file': image_file}, timeout=10)
if response.status_code == 200:
data = response.json()
url = data['data']['url']
pic_url = url.split('/')[-2] + '/' + url.split('/')[-1]
direct_download_url = url.replace(f'/{pic_url}', f'/dl/{pic_url}')
print(direct_download_url)
return direct_download_url
else:
return None
except Exception:
return None
async def send_screenshot(client, message, url, engine_name):
"""Takes a screenshot of the URL and sends it to the chat."""
screenshot_data = generate_screenshot(url)
if screenshot_data:
await client.send_photo(
message.chat.id,
screenshot_data,
caption=f'<b>{engine_name.capitalize()} Result</b>\nURL: <code>{url}</code>',
reply_to_message_id=message.id,
)
else:
await message.reply(f'Failed to take screenshot for {engine_name.capitalize()}.')
# Add module details to help
modules_help['risearch'] = {
'risearch': f'Reply to a photo with `{prefix}risearch [engine]` (e.g., `{prefix}risearch lens`, `{prefix}risearch bing`) '
f'\nor use `{prefix}risearch` to analyze the image with all engines.',
}
-85
View File
@@ -1,85 +0,0 @@
import asyncio
import os
import shutil
import aiohttp
import requests
from pyrogram import Client, enums, filters
from pyrogram.types import Message
from utils.misc import modules_help, prefix
class AioHttp:
async def get_json(self, link):
headers = {
'accept': '*/*',
'accept-language': 'en-US',
'cache-control': 'no-cache',
'client-geo-region': 'global',
'dnt': '1',
'pragma': 'no-cache',
'priority': 'u=1, i',
'sec-ch-ua': '"Chromium";v="130", "Microsoft Edge";v="130", "Not?A_Brand";v="99"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0',
}
async with aiohttp.ClientSession() as session, session.get(link, headers=headers) as resp:
return await resp.json()
@Client.on_message(filters.command('unsplash', prefix) & filters.me)
async def unsplash(client: Client, message: Message):
if len(message.command) > 1 and isinstance(message.command[1], str):
keyword = message.command[1]
unsplash_dir = 'downloads/unsplash/'
if not os.path.exists(unsplash_dir):
os.makedirs(unsplash_dir)
if len(message.command) > 2 and 2 <= int(message.command[2]) <= 10:
await message.edit('<b>Getting Pictures</b>', parse_mode=enums.ParseMode.HTML)
count = int(message.command[2])
images = []
data = await AioHttp().get_json(
f'https://unsplash.com/napi/search/photos?page=1&per_page={count}&query={keyword}'
)
while len(images) < count:
for ia in range(len(images), count):
img = data['results'][ia]['urls']['raw']
if img.startswith('https://images.unsplash.com/photo'):
image_content = requests.get(img, timeout=10).content
with open(f'{unsplash_dir}/unsplash_{ia}.jpg', 'wb') as f:
f.write(image_content)
imgr = f'{unsplash_dir}/unsplash_{ia}.jpg'
images.append(imgr)
else:
images.append(img)
if len(images) == count:
break
for img in images:
await client.send_document(message.chat.id, img)
await message.delete()
shutil.rmtree(unsplash_dir)
return
else:
await message.edit('<b>Getting Picture</b>', parse_mode=enums.ParseMode.HTML)
data = await AioHttp().get_json(
f'https://unsplash.com/napi/search/photos?page=1&per_page=1&query={keyword}'
)
img = data['results'][0]['urls']['raw']
await asyncio.gather(message.delete(), client.send_document(message.chat.id, str(img)))
modules_help['unsplash'] = {
'unsplash': '[keyword]* [number of results you want]*\n'
'Makes a request to <code>unsplash.com</code> and sends the image with the keyword you provided.\n\n'
'<b>Note:</b>\n1. The number of results you can get is limited to 10.\n'
'2. Keyword is required and should be of one word only!.\n'
'3. Images are sent as document to maintain quality.',
}
-92
View File
@@ -1,92 +0,0 @@
import asyncio
from io import BytesIO
import requests
from PIL import Image
from pyrogram import Client, enums, filters
from pyrogram.types import InputMediaPhoto, Message
from utils.misc import modules_help, prefix
API_URL = 'https://bk9.fun/search/unsplash?q='
def resize_image(image_bytes):
try:
with Image.open(image_bytes) as img:
max_size = (1280, 1280)
if img.size > max_size:
img.thumbnail(max_size)
output = BytesIO()
img.save(output, format='JPEG')
output.seek(0)
return output
image_bytes.seek(0) # Reset pointer if not resized
return image_bytes
except Exception as e:
print(f'Error resizing image: {e}')
return image_bytes
async def download_image(url):
try:
response = requests.get(url, timeout=10)
if response.status_code == 200:
img_bytes = BytesIO(response.content)
resized_img_bytes = resize_image(img_bytes)
return resized_img_bytes
except Exception as e:
print(f'Error downloading image: {e}')
return None
@Client.on_message(filters.command(['unsplash2', 'usp2'], prefix) & filters.me)
async def imgsearch(client: Client, message: Message):
if len(message.command) < 2:
await message.edit('Usage: `img [number] <query>`', parse_mode=enums.ParseMode.MARKDOWN)
return
num_pics = int(message.command[1]) if message.command[1].isdigit() else 10
query = ' '.join(message.command[2:])
# Update status
status_message = await message.edit('Searching for images...', parse_mode=enums.ParseMode.MARKDOWN)
url = f'{API_URL}{query}'
response = requests.get(url, timeout=10)
if response.status_code == 200:
data = response.json()
if data.get('status'):
urls = data.get('BK9', [])[:num_pics]
images = [download_image(img_url) for img_url in urls]
# Download images
downloaded_images = await asyncio.gather(*images)
media = [InputMediaPhoto(media=img_bytes) for img_bytes in downloaded_images if img_bytes]
if media:
await status_message.edit('Uploading pictures...', parse_mode=enums.ParseMode.MARKDOWN)
while media:
batch = media[:10]
media = media[10:]
await message.reply_media_group(batch)
await status_message.delete() # Delete status message after uploading
else:
await status_message.edit('No valid images found.', parse_mode=enums.ParseMode.MARKDOWN)
else:
await status_message.edit(
'No images found for the given query.',
parse_mode=enums.ParseMode.MARKDOWN,
)
else:
await status_message.edit(
'An error occurred, please try again later.',
parse_mode=enums.ParseMode.MARKDOWN,
)
modules_help['unsplash2'] = {
'unsplash2 [number]* [query]': 'Get HD images. Default number of images is 10',
'usp2 [number]* [query]': 'Get HD images. Default number of images is 10',
}