""" MIT License Copyright (c) 2024 TheHamkerCat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import asyncio import ipaddress import socket from asyncio import gather from datetime import datetime, timedelta from io import BytesIO from math import atan2, cos, radians, sin, sqrt from os import execvp from random import randint from re import findall from re import sub as re_sub from sys import executable from typing import Dict from urllib.parse import urlparse import aiofiles import speedtest from PIL import Image, ImageDraw, ImageFilter, ImageFont from pyrogram import errors from pyrogram.enums import MessageEntityType from pyrogram.types import Message from wbb import aiohttpsession as aiosession from wbb.utils.dbfunctions import start_restart_stage from wbb.utils.http import get, post async def restart(m: Message): if m: await start_restart_stage(m.chat.id, m.id) execvp(executable, [executable, "-m", "wbb"]) def generate_captcha(): # Generate one letter def gen_letter(): return chr(randint(65, 90)) def rndColor(): return (randint(64, 255), randint(64, 255), randint(64, 255)) def rndColor2(): return (randint(32, 127), randint(32, 127), randint(32, 127)) wrong_answers = ["".join(gen_letter() for _ in range(4)) for _ in range(8)] width, height = 320, 100 correct_answer = "" font = ImageFont.truetype("assets/arial.ttf", 55) # White background image = Image.new("RGB", (width, height), (255, 255, 255)) draw = ImageDraw.Draw(image) # Noise (draw random lines instead of painting every pixel) for _ in range(120): x1, y1 = randint(0, width), randint(0, height) x2, y2 = randint(0, width), randint(0, height) draw.line((x1, y1, x2, y2), fill=rndColor(), width=1) # Text for t in range(4): letter = gen_letter() correct_answer += letter draw.text((60 * t + 50, 15), letter, font=font, fill=rndColor2()) image = image.filter(ImageFilter.BLUR) buf = BytesIO() # on memory image.save(buf, "JPEG") buf.name = "captcha.jpg" buf.seek(0) return [buf, correct_answer, wrong_answers] def test_speedtest(): def speed_convert(size): power = 2**10 zero = 0 units = {0: "", 1: "Kb/s", 2: "Mb/s", 3: "Gb/s", 4: "Tb/s"} while size > power: size /= power zero += 1 return f"{round(size, 2)} {units[zero]}" speed = speedtest.Speedtest() info = speed.get_best_server() download = speed.download() upload = speed.upload() return [speed_convert(download), speed_convert(upload), info] async def get_http_status_code(url: str) -> int: async with aiosession.head(url) as resp: return resp.status async def make_carbon(code): url = "https://carbonara.solopov.dev/api/cook" async with aiosession.post(url, json={"code": code}) as resp: image = BytesIO(await resp.read()) image.name = "carbon.png" return image async def transfer_sh(file_or_message): if isinstance(file_or_message, Message): file_or_message = await file_or_message.download() file = file_or_message async with aiofiles.open(file, "rb") as f: params = {file: await f.read()} resp = await post("https://transfer.sh/", data=params) url = resp.strip() return url async def calc_distance_from_ip(ip1: str, ip2: str) -> float: Radius_Earth = 6371.0088 data1, data2 = await gather( get(f"http://ipinfo.io/{ip1}"), get(f"http://ipinfo.io/{ip2}"), ) lat1, lon1 = data1["loc"].split(",") lat2, lon2 = data2["loc"].split(",") lat1, lon1 = radians(float(lat1)), radians(float(lon1)) lat2, lon2 = radians(float(lat2)), radians(float(lon2)) dlon = lon2 - lon1 dlat = lat2 - lat1 a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2 c = 2 * atan2(sqrt(a), sqrt(1 - a)) distance = Radius_Earth * c return distance def is_safe_url(url: str) -> bool: """ Check if a URL is safe to fetch (prevents SSRF). It ensures the URL uses http/https and that all resolved IPs are public. """ try: parsed = urlparse(url.strip()) if parsed.scheme not in {"http", "https"}: return False hostname = parsed.hostname if not hostname: return False port = parsed.port if port is None: port = 443 if parsed.scheme == "https" else 80 # Resolve all addresses (IPv4 + IPv6). URL is safe only if all are public. address_info = socket.getaddrinfo(hostname, port, type=socket.SOCK_STREAM) resolved_ips = {item[4][0] for item in address_info} if not resolved_ips: return False for ip_addr in resolved_ips: ip = ipaddress.ip_address(ip_addr) if not ip.is_global: return False return True except (ValueError, OSError): return False def get_urls_from_text(text: str) -> bool: regex = r"""(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-] [.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|( \([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\ ()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".strip() return [x[0] for x in findall(regex, str(text))] async def time_converter(message: Message, time_value: str) -> datetime: unit = ["m", "h", "d"] # m == minutes | h == hours | d == days check_unit = "".join(list(filter(time_value[-1].lower().endswith, unit))) currunt_time = datetime.now() time_digit = time_value[:-1] if not time_digit.isdigit(): return await message.reply_text("时间格式不正确,请使用 10m、2h 或 1d。") if check_unit == "m": temp_time = currunt_time + timedelta(minutes=int(time_digit)) elif check_unit == "h": temp_time = currunt_time + timedelta(hours=int(time_digit)) elif check_unit == "d": temp_time = currunt_time + timedelta(days=int(time_digit)) else: return await message.reply_text("时间格式不正确,请使用 10m、2h 或 1d。") return temp_time async def extract_userid(message, text: str): """ NOT TO BE USED OUTSIDE THIS FILE """ def is_int(text: str): try: int(text) except ValueError: return False return True text = text.strip() if is_int(text): return int(text) entities = message.entities app = message._client if len(entities) < 2: return (await app.get_users(text)).id entity = entities[1] if entity.type == MessageEntityType.MENTION: return (await app.get_users(text)).id if entity.type == MessageEntityType.TEXT_MENTION: return entity.user.id return None async def extract_user_and_reason(message, sender_chat=False): args = message.text.strip().split() text = message.text user = None reason = None try: if message.reply_to_message: reply = message.reply_to_message # if reply to a message and no reason is given if not reply.from_user: if ( reply.sender_chat and reply.sender_chat != message.chat.id and sender_chat ): id_ = reply.sender_chat.id else: return None, None else: id_ = reply.from_user.id if len(args) < 2: reason = None else: reason = text.split(None, 1)[1] return id_, reason # if not reply to a message and no reason is given if len(args) == 2: user = text.split(None, 1)[1] return await extract_userid(message, user), None # if reason is given if len(args) > 2: user, reason = text.split(None, 2)[1:] return await extract_userid(message, user), reason return user, reason except errors.UsernameInvalid: return "", "" async def extract_user(message): return (await extract_user_and_reason(message))[0] def get_file_id_from_message( message, max_file_size=3145728, mime_types=["image/png", "image/jpeg"], ): file_id = None if message.document: if int(message.document.file_size) > max_file_size: return mime_type = message.document.mime_type if mime_types and mime_type not in mime_types: return file_id = message.document.file_id if message.sticker: if message.sticker.is_animated: if not message.sticker.thumbs: return file_id = message.sticker.thumbs[0].file_id else: file_id = message.sticker.file_id if message.photo: file_id = message.photo.file_id if message.animation: if not message.animation.thumbs: return file_id = message.animation.thumbs[0].file_id if message.video: if not message.video.thumbs: return file_id = message.video.thumbs[0].file_id return file_id def extract_text_and_keyb(ikb, text: str, row_width: int = 2): keyboard = {} try: text = text.strip() if text.startswith("`"): text = text[1:] if text.endswith("`"): text = text[:-1] if "~~" in text: text = text.replace("~~", "¤¤") text, keyb = text.split("~") if "¤¤" in text: text = text.replace("¤¤", "~~") keyb = findall(r"\[.+\,.+\]", keyb) for btn_str in keyb: btn_str = re_sub(r"[\[\]]", "", btn_str) btn_str = btn_str.split(",") btn_txt, btn_url = btn_str[0], btn_str[1].strip() if not get_urls_from_text(btn_url): continue keyboard[btn_txt] = btn_url keyboard = ikb(keyboard, row_width) except Exception: return return text, keyboard async def check_format(ikb, raw_text: str): keyb = findall(r"\[.+\,.+\]", raw_text) if keyb and not "~" in raw_text: raw_text = raw_text.replace("button=", "\n~\nbutton=") return raw_text if "~" in raw_text and keyb: if not extract_text_and_keyb(ikb, raw_text): return "" else: return raw_text else: return raw_text async def get_data_and_name(replied_message, message): text = message.text.markdown if message.text else message.caption.markdown name = text.split(None, 1)[1].strip() text = name.split(" ", 1) if len(text) > 1: name = text[0] data = text[1].strip() if replied_message and (replied_message.sticker or replied_message.video_note): data = None else: if replied_message and (replied_message.sticker or replied_message.video_note): data = None elif ( replied_message and not replied_message.text and not replied_message.caption ): data = None else: data = ( replied_message.text.markdown if replied_message.text else replied_message.caption.markdown ) command = message.command[0] match = f"/{command} " + name if not message.reply_to_message and message.text: if match == data: data = "error" elif not message.reply_to_message and not message.text: if match == data: data = None return data, name async def get_specific_usernames(client, user_ids: list) -> Dict[int, str]: def _fetch_users(): ids_str = ",".join(str(uid) for uid in user_ids) with client.storage.conn: query = f""" SELECT usernames.id, usernames.username FROM usernames WHERE usernames.id IN ({ids_str}) AND username IS NOT NULL """ result = client.storage.conn.execute(query).fetchall() users_ = {} for row in result: users_[row[0]] = row[1] return users_ try: users = await asyncio.to_thread(_fetch_users) return users except Exception as e: print(f"Error fetching users: {e}") return {}