commit 8b053a7adb8f873b95b59c3f32b7fdb6da82b65a94fae99258335821f4ca021a Author: unknown Date: Wed May 13 23:38:18 2026 +0200 Initial commit diff --git a/arch/bot_old.py b/arch/bot_old.py new file mode 100644 index 0000000..4a344da --- /dev/null +++ b/arch/bot_old.py @@ -0,0 +1,132 @@ +import asyncio +from aiogram import Bot, Dispatcher, types, F +from aiogram.types import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton +from aiogram.filters import CommandStart +from aiogram.fsm.storage.memory import MemoryStorage +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import StatesGroup, State + +TOKEN = "8517883174:AAFA-dHF5Xm6q1mPQJA_eSBcyqDsaO0nPS4" +CHANNEL_ID = -1003749575740 +REVIEW_ID = -1003911723791 + +bot = Bot(token=TOKEN) +dp = Dispatcher(storage=MemoryStorage()) + +submissions = {} +counter = 0 + +class Upload(StatesGroup): + waiting_media = State() + confirm = State() + +start_kb = ReplyKeyboardMarkup( + keyboard=[ + [KeyboardButton(text="Upload Photo")], + [KeyboardButton(text="Upload Video")] + ], + resize_keyboard=True +) + +confirm_kb = InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="Submit", callback_data="submit")], + [InlineKeyboardButton(text="Cancel", callback_data="cancel")] + ] +) + +def admin_kb(sub_id): + return InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="Approve", callback_data="a|" + str(sub_id))], + [InlineKeyboardButton(text="Reject", callback_data="r|" + str(sub_id))] + ] + ) + +@dp.message(CommandStart()) +async def start(message: types.Message, state: FSMContext): + await message.answer("The other videos/images can be found in the channel. Channel invite link: https://t.me/+engKAbYjPgNmYTQ1 here is where your stuff will be posted too") + args = message.text.split() + if len(args) > 1 and args[1] == "submit": + await message.answer("Send your photo or video", reply_markup=start_kb) + else: + await message.answer("Choose:", reply_markup=start_kb) + +@dp.message(F.text == "Upload Photo") +async def photo_btn(message: types.Message, state: FSMContext): + await state.set_state(Upload.waiting_media) + await state.update_data(type="photo") + await message.answer("Send photo") + +@dp.message(F.text == "Upload Video") +async def video_btn(message: types.Message, state: FSMContext): + await state.set_state(Upload.waiting_media) + await state.update_data(type="video") + await message.answer("Send video") + +@dp.message(Upload.waiting_media, F.photo | F.video) +async def handle_media(message: types.Message, state: FSMContext): + if message.photo: + file_id = message.photo[-1].file_id + file_type = "photo" + else: + file_id = message.video.file_id + file_type = "video" + await state.update_data(file_id=file_id, file_type=file_type) + await state.set_state(Upload.confirm) + await message.answer("Confirm submission", reply_markup=confirm_kb) + +@dp.callback_query(F.data == "submit") +async def submit(cb: types.CallbackQuery, state: FSMContext): + global counter + data = await state.get_data() + file_id = data["file_id"] + file_type = data["file_type"] + user = cb.from_user + counter += 1 + submissions[counter] = (user.id, file_id, file_type) + caption = "New submission from @" + (user.username if user.username else user.full_name) + if file_type == "photo": + await bot.send_photo(REVIEW_ID, file_id, caption=caption, reply_markup=admin_kb(counter)) + else: + await bot.send_video(REVIEW_ID, file_id, caption=caption, reply_markup=admin_kb(counter)) + await cb.message.answer("Submitted") + await state.clear() + +@dp.callback_query(F.data == "cancel") +async def cancel(cb: types.CallbackQuery, state: FSMContext): + await state.clear() + await cb.message.answer("Cancelled") + +@dp.callback_query(F.data.startswith("a|")) +async def approve(cb: types.CallbackQuery): + sub_id = int(cb.data.split("|")[1]) + user_id, file_id, file_type = submissions[sub_id] + if file_type == "photo": + await bot.send_photo(CHANNEL_ID, file_id, caption="Approved submission, submit your own self-harm media using https://t.me/Selfharmmeowbot?start=submit") + else: + await bot.send_video(CHANNEL_ID, file_id, caption="Approved submission, submit your own self-harm media using https://t.me/Selfharmmeowbot?start=submit") + await bot.send_message(user_id, "Your submission was approved") + await cb.message.edit_reply_markup() + +@dp.callback_query(F.data.startswith("r|")) +async def reject(cb: types.CallbackQuery): + sub_id = int(cb.data.split("|")[1]) + user_id, _, _ = submissions[sub_id] + await bot.send_message(user_id, "Your submission was rejected") + await cb.message.edit_reply_markup() + +@dp.message(F.new_chat_members) +async def welcome(message: types.Message): + for user in message.new_chat_members: + text = f"{user.first_name} welcome to the channel. if you want to submit media of SH please use this link to our bot: https://t.me/selfharmmeowbot?start=submit If you want to view existing self-harm media in the channel, please click on it and go to the media section." + msg = await message.answer(text, parse_mode="HTML") + await asyncio.sleep(1800) + await msg.delete() + + +async def main(): + await dp.start_polling(bot) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/arch/sh-bot.rar b/arch/sh-bot.rar new file mode 100644 index 0000000..ed37b02 Binary files /dev/null and b/arch/sh-bot.rar differ diff --git a/bot_state.py b/bot_state.py new file mode 100644 index 0000000..02cb3bf --- /dev/null +++ b/bot_state.py @@ -0,0 +1,19 @@ +"""Shared in-memory state. Import from here; never create a second copy.""" +import asyncio + +submissions: dict = {} +counter: int = 0 +daily_submissions: dict = {} +welcome_messages: dict = {} +welcome_lock: asyncio.Lock = asyncio.Lock() +chat_sessions: dict = {} +chat_message_map: dict = {} +banned_chat_users: set = set() +upload_prompt_tasks: dict = {} +upload_prompt_msg_ids: dict = {} # user_id -> last prompt message_id +submitting_users: set = set() +chatroom_semipublic_group_messages: dict = {} +known_usernames: dict[int, str | None] = {} +backup_hashes: dict[str, str] = {} # file_unique_id -> sha256 +confirmed_users: set[int] = set() +blacklisted_words: list[str] = [] \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..b7a49c2 --- /dev/null +++ b/config.py @@ -0,0 +1,20 @@ +TOKEN = "xxx" + +CHATROOM_SEMIPUBLIC_GROUP_ID = -1003857747500 # SUPERGROUP CHATROOM SEMI-PUBLIC +MAIN_PUBLIC_CHANNEL_ID = -1003903492201 # BROADCAST CHANNEL, ONLY FOR MEDIA WITHOUT DISCUSSIONS, PUBLIC +CHAT_CONTACT_ADMIN_GROUP_ID = -1003926588340 # ADMIN DISCUSSION/CONTACT GROUP, PRIVATE +REVIEW_ADMIN_CHATROOM_SEMIPUBLIC_GROUP_ID = -1003917414873 # REVIEW GROUP, PRIVATE, ONLY FOR APPROVING/REJECTING SUBMISSIONS, NOT FOR DISCUSSIONS, ADMIN ONLY +CHATROOM_PRIVATE_BACKUP_GROUP_ID = -1003747166652 # PRIVATE BACKUP GROUP, NOT PUBLIC, ONLY FOR BACKUP PURPOSES, NOT FOR DISCUSSIONS, ADMIN ONLY + +BLACKLISTED_WORDS_FILE = "./rsrcs/blacklist.txt" +BACKUP_IDS_FILE = "./rsrcs/backup_ids.json" +CONFIRMED_USERS_FILE = "./rsrcs/confirmed_users.json" + +DAILY_SUBMISSION_LIMIT = 5 +PURGE_INTERVAL_HOURS = 14 # 12 for complex high-risk groups, 18-24 for general groups, 36 for less known, secretive groups ect. + +BOT_USERNAME = "harmfulmeowbot" # WITHOUT @, for captions and other purposes +INVITELINK_ARCH = "t.me/+8lo1VJuwVRwxOTk0" # INVITE LINK FOR ARCHIVE CHANNEL, NOT FOR DISCUSSIONS, PUBLIC +INVITELINK_CHAT = "https://t.me/+UMULzzHutGhhMmQ8" # INVITE LINK FOR CHATROOM, DISCUSSION SUPER GROUP, PUBLIC + +BLACKLIST_MODE = 0 # 0 = delete message, 1 = delete original message and bot sends censored version \ No newline at end of file diff --git a/docs/requirements.md b/docs/requirements.md new file mode 100644 index 0000000..65563a9 --- /dev/null +++ b/docs/requirements.md @@ -0,0 +1,5 @@ +- python 3.12+ +- pip with venv/pipx +- shell +- screen +- telegram - 4 groups + 1 channel or 5 groups recommended diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..eaa47a0 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,90 @@ +(- for each new submission, if the content is bigger than 35 media OR there has been over 20 messages in the CHATROOM_SEMIPUBLIC_GROUP_ID inbetween last submission and the new one, add one of the following messages randomly after submitting media and pin that msg: + +- […], then delete the old warning and resend the following warning again immediately and pin it: + "" + +- add https://pypi.org/project/lexicont/ to monitor all new messages within 15 minute cycles, ML analyze and detect purge score of that convo blob ect.) + +01/05/2026: + +- first time warning does not work + +- reindex command shall pull all OLD submission from the backup CHATROOM_PRIVATE_BACKUP_GROUP_ID as well and also, save the hashes in a json, also for new submissions + +- formatting is not proper the way i expressed/want it, i.e. welcome, submitted ect. + +- fix: blacklist displays censorship too complex ☑️ + +- links get censored instead of just deleted, even for admins ☑️ + +- add softban, permanent ban, softmute, mute, kick commands: + /sban @ or id, duration & time unit, reason (optional) -> bans user for certain time (save in db) + /smute @ or id, duration & time unit, reason (optional) -> mutes user for certain time + /mute @ or id, reason (optional) -> mutes user forever + /pban @ or id, reason (optional) -> bans user forever + /rmute @ or id -> revokes mute + /rban @ or id -> revokes ban + /kick @ or id, reason (optional) -> kicks a user once + +- add better log to save every little action & command executed in functional db + +- add sqlite3 integration + +- make user bans when /ban is executed by admin be saved in the json, so even after restart they cannot contact admins again + +- rework the button 'New Chat' to say/into 'Contact Administrators' ☑️ + +- fix/add auto delete leave messages basically delete the system messages like '... left the group' that telegram shows ☑️ + +- make when new 'Media submitted msg' is sent, delete the prior one (if user sends large portions of data) ☑️ + +- if a message contains a link and blacklisted word, do not send censored message with link, just delete old message as now someone could bypass the link blocker by just adding a slur word into their msg + +- add a command to reload the config file that can only be executed by admins in any of the groups (except backup CHATROOM_PRIVATE_BACKUP_GROUP_ID), also make the config a dynamic json and add much more configuration to it, such as instead of harcoding what messages should look like etc. or invite links, make it fully configurable + +- add multi group/channel/branch management via admin commands that let you change & view & reload the config within telegram + +30/04/2026: + +- purge chat CHATROOM_SEMIPUBLIC_GROUP_ID every 36hours, except for all messages from the bots + +- refine messages, especially welcome & submission accepted into the following - new welcome message: + "@username (links to profile, or simply id if user has no username set) welcome to the official s3lfharm archive. + +You can [view media](UNDERLINE) by either checking [pinned messages](BOLD) or the [media section](BOLD) if you click on the channel. + +[Feel free](UNDERLINE) to share your own [s3lfharm imagery](SPOILER + ITALIC) using this bot ('this bot' shall hyperclickable link to https://t.me/selfharmmeowbot?start=submit). + +[💋 Join the public archive + +https://t.me/+8lo1VJuwVRwxOTk0 (S3LF HARM)](QUOTE)" - new submission accepted message/caption: +"This is an [anonymous](UNDERLINE) submission reviewed by admins. + +You can apply having [self-harm imagery](SPOILER + ITALIC) posted using this bot. ('this bot' hyperlinks to https://t.me/selfharmmeowbot?start=submit)" + +- monitor ALL changes of usernames of ALL users and send them in chat CHATROOM_SEMIPUBLIC_GROUP_ID, CHAT_CONTACT_ADMIN_GROUP_ID like: + "@old_username (clickable hyperlink to profile via id tho) changed username to @new_username (clickable hyperlink to profile via id again) at EXACT_TIME." + +- turn bot actions in private chats into clickable style buttons directly under the message, not "official" telegram buttons in the message prompt + +- use any kind of library etc. if possible to avoid duplicate sending of data, check entirety of CHATROOM_PRIVATE_BACKUP_GROUP_ID if that same video/picture submitted already exists, if it does and the submission is approved/rejected say additionally i.e.: + "Submitted content from @.../USERID contains x duplicates. Skipping those." + +- on first time using bot user has to confirm: + "Hey! + +This bot is affiliated with services offering extreme contents and services involving topics/ touching on topics such as [political controversies, gore, self-injury](ITALIC + SPOILER] ect. + +[If](BOLD) you [acknowledge that](BOLD) and want to [proceed](BOLD), please tap '[Yes](BOLD + UNDERLINE)'. + +This is solely a trigger warning that will only show up [once](UNDERLINE)." + +- add a blacklist system working the following: + if a message contains any word from BLACKLISTED_WORDS_LIST, first apply REPLACEMENT_CHARSET on ALL blacklisted words in that messages, for those blacklisted words for the rest of the characters that have not been replaced by REPLACEMENT_CHARSET please make it apply either CHARTSET_1, CHARTSET_2, CHARTSET_3 or CHARTSET_4, delete the original message immediately and respond like this: + "Censored text from @.../USERID -> QUOTE 'i love 🅁 @ 🄿 🄴'" + or if user has no username set + "Censored text from USERID (which links to their profile) -> QUOTE 'i love 🅁 @ 🄿 🄴'" + +and inside the quote make each blacklisted censored word a spoiler + between each letter of each blacklisted word must be two spaces + +- also, delete ANY kind of LINK that is sent immediately, even the sneaky ones like google[.]com or t .me/ ect., except for admins, those are still allowed to send diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..9097bf2 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,3 @@ +Usage + +Execute `/bot/bin/python bot.py` (if /bot/ is your venv, otherwise just python/python3) diff --git a/filters.py b/filters.py new file mode 100644 index 0000000..b395a53 --- /dev/null +++ b/filters.py @@ -0,0 +1,94 @@ +"""Charset helpers, blacklist censoring, link detection.""" +import html +import random +import re +import bot_state as state + +_CHARSET_1 = "🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉" +_CHARSET_2 = "🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩" +_CHARSET_3 = "ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏ" +_CHARSET_4 = "🇦🇧🇨🇩🇪🇫🇬🇭🇮🇯🇰🇱🇲🇳🇴🇵🇶🇷🇸🇹🇺🇻🇼🇽🇾🇿" +_CHARSETS = [_CHARSET_1, _CHARSET_2, _CHARSET_3, _CHARSET_4] + +_REPLACEMENT: dict[str, str] = { + "a": "@", "e": "3", + "i": "1", "o": "0", +} + +LINK_PATTERNS: list[re.Pattern] = [ + re.compile(r"https?://", re.IGNORECASE), + re.compile(r"www\s*\.", re.IGNORECASE), + re.compile(r"t\s*[\[\(\.]\s*me\s*/", re.IGNORECASE), + re.compile(r"t\s+\.\s*me", re.IGNORECASE), + re.compile(r"\w+\s*\[\s*\.\s*\]\s*\w+", re.IGNORECASE), + re.compile(r"\w+\s*\(\s*\.\s*\)\s*\w+", re.IGNORECASE), + re.compile(r"\w+\s*\(\s*dot\s*\)\s*\w+", re.IGNORECASE), + re.compile( + r"\.(?:com|net|org|info|biz|name|pro|xyz|online|site|website|space|store|shop|blog|tech|dev|app|cloud|" + r"digital|solutions|systems|services|agency|group|company|center|world|global|today|live|life|news|media|" + r"network|social|community|zone|one|link|io|ai|co|ly|me|gg|tv|to|sh|fm|ws|cc|so|vc|it|page|software|tools|" + r"design|studio|lab|labs|build|engineering|data|systems|academy|care|finance|capital|fund|money|loan|loans|" + r"credit|insurance|investments|tax|accountants|law|legal|attorney|consulting|partners|ventures|holdings|" + r"management|marketing|media|press|events|productions|photos|photography|pictures|video|film|music|audio|" + r"games|game|play|fun|chat|dating|love|fans|family|kids|school|education|college|university|training|" + r"courses|institute|health|clinic|hospital|doctor|dentist|fitness|gym|yoga|diet|food|restaurant|cafe|" + r"coffee|bar|beer|wine|recipes|kitchen|cooking|fashion|style|clothing|shoes|jewelry|beauty|hair|makeup|" + r"salon|travel|trips|tours|vacations|holiday|flights|tickets|hotel|hostel|rentals|cars|car|auto|" + r"motorcycles|bike|bikes|taxi|delivery|express|logistics|shipping|realty|realestate|homes|house|rent|" + r"apartments|property|construction|builders|contractors|repair|cleaning|security|energy|solar|green|eco|" + r"farm|garden|flowers|pets|pet|dog|cat|animals|science|research|space|earth|energy|finance|bank|exchange|" + r"trade|trading|market|markets|crypto|bitcoin|eth|nft|art|gallery|design|graphics|print|books|library|" + r"wiki|guide|help|support|tools|download|software|app|cloud|host|hosting|server|email|mail|tech|network|" + r"systems|solutions|world|global|international|express|plus|pro|max|now|top|best|cool|fun|zone|land|city|" + r"place|town|country|uk|us|ca|au|de|fr|ru|cn|jp|kr|in|br|za|es|it|nl|se|no|fi|dk|pl|ch|be|at|ie|nz|mx|" + r"ar|cl|co|pe|pt|gr|tr|ae|sa|il|sg|hk|id|my|th|vn|ph|pk|bd|ng|ke|gh)\b", + re.IGNORECASE, + ), +] + + +def censor_word(word: str) -> str: + charset = random.choice(_CHARSETS) + chars = [] + for char in word: + lower = char.lower() + if lower in _REPLACEMENT: + chars.append(_REPLACEMENT[lower]) + elif lower.isalpha(): + idx = ord(lower) - ord("a") + chars.append(charset[idx] if 0 <= idx < len(charset) else char) + else: + chars.append(char) + chunks = ["".join(chars[i:i + 3]) for i in range(0, len(chars), 3)] + return " ".join(chunks) + + +def process_blacklisted_message(text: str) -> tuple[str, bool]: + if not state.blacklisted_words or not text: + return html.escape(text or ""), False + matches: list[tuple[int, int, str]] = [] + for word in state.blacklisted_words: + for m in re.finditer(re.escape(word), text, re.IGNORECASE): + matches.append((m.start(), m.end(), m.group())) + if not matches: + return html.escape(text), False + matches.sort(key=lambda x: (x[0], -(x[1] - x[0]))) + filtered, last_end = [], 0 + for start, end, w in matches: + if start >= last_end: + filtered.append((start, end, w)) + last_end = end + parts, pos = [], 0 + for start, end, w in filtered: + parts.append(html.escape(text[pos:start])) + censored_words = [censor_word(tok) for tok in w.split()] + parts.append(f"{' '.join(censored_words)}") + pos = end + parts.append(html.escape(text[pos:])) + return "".join(parts), True + + +def contains_link(text: str) -> bool: + if not text: + return False + return any(p.search(text) for p in LINK_PATTERNS) \ No newline at end of file diff --git a/hashing.py b/hashing.py new file mode 100644 index 0000000..126de4f --- /dev/null +++ b/hashing.py @@ -0,0 +1,71 @@ +""" +Media-hash helpers. + +Strategy +-------- +We never download files just to hash them. Instead we use Telegram's +file_unique_id as a stable, server-side content identifier – two files +that are byte-for-byte identical always share the same file_unique_id, +regardless of who uploaded them or when. + +The cache (backup_ids.json) stores: +{ "": "" } +""" + +import hashlib +import logging + +from aiogram import Bot + +import bot_state as state +from persistence import save_backup_ids + +logger = logging.getLogger(__name__) + + +def _hash(file_unique_id: str) -> str: + return hashlib.sha256(file_unique_id.encode()).hexdigest() + + +def register_file(file_unique_id: str) -> bool: + """Add a file to the in-memory cache and persist it. + + Returns True if the file was new, False if it was already known. + """ + if file_unique_id in state.backup_hashes: + return False + state.backup_hashes[file_unique_id] = _hash(file_unique_id) + save_backup_ids() + return True + + +def is_duplicate(file_unique_id: str) -> bool: + return file_unique_id in state.backup_hashes + + +def check_media_list(media: list[dict]) -> tuple[list[dict], list[dict]]: + """Split a media list into (unique, duplicates).""" + unique, dupes = [], [] + for item in media: + if is_duplicate(item.get("file_unique_id", "")): + dupes.append(item) + else: + unique.append(item) + return unique, dupes + + +async def preload_backup_hashes(bot: Bot, chat_id: int) -> None: + """ + Since the standard Bot API does not expose a bulk message history + endpoint for groups, we: + 1. Load whatever is already in backup_ids.json (done by load_backup_ids). + 2. On each new message in the backup group, register via register_file. + + To do a full historical reindex, admins forward all older media back + into the backup group — the bot registers each file automatically. + """ + logger.info( + "Backup hash cache loaded from disk: %d known files. " + "New files arriving in the backup group will be indexed automatically.", + len(state.backup_hashes), + ) \ No newline at end of file diff --git a/keyboards.py b/keyboards.py new file mode 100644 index 0000000..64bf329 --- /dev/null +++ b/keyboards.py @@ -0,0 +1,47 @@ +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup + +confirm_tos_kb = InlineKeyboardMarkup( + inline_keyboard=[[InlineKeyboardButton(text="Yes", callback_data="tos_confirm")]] +) + +anonymous_choice_kb = InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="Yes", callback_data="menu_anon_yes")], + [InlineKeyboardButton(text="No", callback_data="menu_anon_no")], + ] +) + +confirm_kb = InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="Submit", callback_data="submit")], + [InlineKeyboardButton(text="Cancel", callback_data="cancel")], + ] +) + + +def menu_kb() -> InlineKeyboardMarkup: + return InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="📤 Upload media", callback_data="menu_upload")], + [InlineKeyboardButton(text="📩 Contact Administrators", callback_data="menu_chat")], + ] + ) + + +def admin_kb(sub_id: int) -> InlineKeyboardMarkup: + return InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="Approve", callback_data=f"a|{sub_id}")], + [InlineKeyboardButton(text="Reject", callback_data=f"r|{sub_id}")], + ] + ) + + +def publish_kb(sub_id: int) -> InlineKeyboardMarkup: + return InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="Broadcast", callback_data=f"p|b|{sub_id}")], + [InlineKeyboardButton(text="Send in discussion", callback_data=f"p|d|{sub_id}")], + [InlineKeyboardButton(text="Send in both", callback_data=f"p|both|{sub_id}")], + ] + ) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..349ed7b --- /dev/null +++ b/main.py @@ -0,0 +1,51 @@ +"""Entry point — wires everything together.""" +import asyncio +import logging + +from aiogram import Bot, Dispatcher +from aiogram.fsm.storage.memory import MemoryStorage + +from config import TOKEN +from hashing import preload_backup_hashes +from middlewares import TosMiddleware, UsernameTrackerMiddleware +from persistence import load_backup_ids, load_blacklist, load_confirmed_users +from routers import admin, group, private + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", +) +logger = logging.getLogger(__name__) + + +async def main() -> None: + # 1. Load persisted data + load_backup_ids() + load_confirmed_users() + load_blacklist() + + bot = Bot(token=TOKEN) + dp = Dispatcher(storage=MemoryStorage()) + + # 2. Log cached hashes + import bot_state as state + logger.info("Loaded %d file hashes from cache.", len(state.backup_hashes)) + + # 3. Middlewares + dp.update.outer_middleware(UsernameTrackerMiddleware()) + dp.update.outer_middleware(TosMiddleware()) + + # 4. Routers + dp.include_router(private.router) + dp.include_router(admin.router) + dp.include_router(group.router) + + # 5. Background tasks + asyncio.create_task(group.purge_chatroom_semipublic_group(bot)) + + logger.info("Bot starting…") + await dp.start_polling(bot) + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/middlewares.py b/middlewares.py new file mode 100644 index 0000000..6b89499 --- /dev/null +++ b/middlewares.py @@ -0,0 +1,75 @@ +"""Outer middlewares: username tracker + TOS gate.""" +import logging +from datetime import datetime, timezone +from aiogram import BaseMiddleware, Bot, types +import bot_state as state +from config import CHAT_CONTACT_ADMIN_GROUP_ID, CHATROOM_SEMIPUBLIC_GROUP_ID +from keyboards import confirm_tos_kb +from persistence import save_confirmed_users + +logger = logging.getLogger(__name__) + +TOS_TEXT = ( + "Hey! \n\n" + "This bot is affiliated with services offering extreme contents and services involving " + "topics / touching on topics such as " + "political controversies, gore, self-injury etc. \n\n" + "If you acknowledge that and want to proceed, " + "please tap 'Yes'.\n\n" + "This is solely a trigger warning that will only show up once." +) + + +class UsernameTrackerMiddleware(BaseMiddleware): + async def __call__(self, handler, event, data): + user: types.User | None = data.get("event_from_user") + if user: + old = state.known_usernames.get(user.id, "UNSET") + if old != "UNSET" and old != user.username: + old_m = f'@{old}' if old else str(user.id) + new_m = f'@{user.username}' if user.username else str(user.id) + now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") + text = f"{old_m} changed username to {new_m} at {now}." + bot: Bot = data["bot"] + for chat_id in (CHATROOM_SEMIPUBLIC_GROUP_ID, CHAT_CONTACT_ADMIN_GROUP_ID): + try: + await bot.send_message(chat_id, text, parse_mode="HTML") + except Exception: + logger.warning("Failed to broadcast username change to %s", chat_id) + state.known_usernames[user.id] = user.username + return await handler(event, data) + + +class TosMiddleware(BaseMiddleware): + """Block all private interactions until the user confirms the TOS.""" + + async def __call__(self, handler, event, data): + user: types.User | None = data.get("event_from_user") + if not user: + return await handler(event, data) + + is_private = ( + (isinstance(event, types.Message) and event.chat.type == "private") + or (isinstance(event, types.CallbackQuery) and event.message and event.message.chat.type == "private") + ) + if not is_private: + return await handler(event, data) + + # Always let the TOS confirmation callback through FIRST + if isinstance(event, types.CallbackQuery) and event.data == "tos_confirm": + return await handler(event, data) + + if user.id not in state.confirmed_users: + bot: Bot = data["bot"] + try: + await bot.send_message(user.id, TOS_TEXT, parse_mode="HTML", reply_markup=confirm_tos_kb) + except Exception: + logger.warning("Failed to send TOS to user %s", user.id) + if isinstance(event, types.CallbackQuery): + try: + await event.answer() + except Exception: + pass + return # gate: do NOT call handler + + return await handler(event, data) \ No newline at end of file diff --git a/persistence.py b/persistence.py new file mode 100644 index 0000000..90eba2b --- /dev/null +++ b/persistence.py @@ -0,0 +1,51 @@ +import json +import os +import bot_state as state +from config import BACKUP_IDS_FILE, BLACKLISTED_WORDS_FILE, CONFIRMED_USERS_FILE + + +def load_backup_ids() -> None: + if not os.path.exists(BACKUP_IDS_FILE): + return + try: + with open(BACKUP_IDS_FILE) as f: + data = json.load(f) + state.backup_hashes = data if isinstance(data, dict) else {} + except Exception: + state.backup_hashes = {} + + +def save_backup_ids() -> None: + try: + with open(BACKUP_IDS_FILE, "w") as f: + json.dump(state.backup_hashes, f) + except Exception: + pass + + +def load_confirmed_users() -> None: + if not os.path.exists(CONFIRMED_USERS_FILE): + return + try: + with open(CONFIRMED_USERS_FILE) as f: + state.confirmed_users = set(json.load(f)) + except Exception: + state.confirmed_users = set() + + +def save_confirmed_users() -> None: + try: + with open(CONFIRMED_USERS_FILE, "w") as f: + json.dump(list(state.confirmed_users), f) + except Exception: + pass + + +def load_blacklist() -> None: + if not os.path.exists(BLACKLISTED_WORDS_FILE): + return + try: + with open(BLACKLISTED_WORDS_FILE, encoding="utf-8") as f: + state.blacklisted_words = [ln.strip().lower() for ln in f if ln.strip()] + except Exception: + state.blacklisted_words = [] \ No newline at end of file diff --git a/routers/__init__.py b/routers/__init__.py new file mode 100644 index 0000000..c64dd19 --- /dev/null +++ b/routers/__init__.py @@ -0,0 +1 @@ +from routers import admin, group, private \ No newline at end of file diff --git a/routers/admin.py b/routers/admin.py new file mode 100644 index 0000000..0347142 --- /dev/null +++ b/routers/admin.py @@ -0,0 +1,190 @@ +"""Handlers for admin review/contact groups.""" +import logging + +from aiogram import F, Router, types +from aiogram.filters import Command + +import bot_state as state_store +from config import ( + BOT_USERNAME, + CHAT_CONTACT_ADMIN_GROUP_ID, + CHATROOM_PRIVATE_BACKUP_GROUP_ID, + CHATROOM_SEMIPUBLIC_GROUP_ID, + MAIN_PUBLIC_CHANNEL_ID, + REVIEW_ADMIN_CHATROOM_SEMIPUBLIC_GROUP_ID, +) +from hashing import register_file +from keyboards import publish_kb +from utils import build_quote, get_admin_display_name, send_media_items + +logger = logging.getLogger(__name__) +router = Router(name="admin") + +# ── Submission approve / reject ─────────────────────────────────────────────── + +@router.callback_query(F.data.startswith("a|")) +async def approve(cb: types.CallbackQuery): + sub_id = int(cb.data.split("|")[1]) + if sub_id not in state_store.submissions: + await cb.answer("Submission not found.", show_alert=True) + return + await cb.message.edit_reply_markup(reply_markup=publish_kb(sub_id)) + await cb.answer() + + +@router.callback_query(F.data.startswith("p|")) +async def publish(cb: types.CallbackQuery): + parts = cb.data.split("|") + target = parts[1] + sub_id = int(parts[2]) + submission = state_store.submissions.get(sub_id) + if not submission: + await cb.answer("Submission not found.", show_alert=True) + return + + user_id = submission["user_id"] + media = submission["media"] + + duplicate_media = [m for m in media if m.get("file_unique_id", "") in state_store.backup_hashes] + unique_media = [m for m in media if m.get("file_unique_id", "") not in state_store.backup_hashes] + + if duplicate_media: + try: + user_info = await cb.bot.get_chat(user_id) + user_ref = ( + f'@{user_info.username}' + if getattr(user_info, "username", None) + else str(user_id) + ) + except Exception: + user_ref = str(user_id) + try: + await cb.bot.send_message( + REVIEW_ADMIN_CHATROOM_SEMIPUBLIC_GROUP_ID, + f"⚠️ Content from {user_ref} contains {len(duplicate_media)} duplicate(s). " + "Skipping those.", + parse_mode="HTML", + ) + except Exception: + logger.warning("Could not send duplicate notice") + + caption = ( + "This is an anonymous submission reviewed by admins.\n\n" + "You can apply having self-harm imagery posted using " + f'this bot.' + ) + + if unique_media: + await send_media_items(cb.bot, CHATROOM_PRIVATE_BACKUP_GROUP_ID, unique_media) + for item in unique_media: + register_file(item["file_unique_id"]) + + if target in {"d", "both"}: + await send_media_items(cb.bot, CHATROOM_SEMIPUBLIC_GROUP_ID, unique_media, caption=caption) + if target in {"b", "both"}: + await send_media_items(cb.bot, MAIN_PUBLIC_CHANNEL_ID, unique_media, caption=caption) + + try: + await cb.bot.send_message(user_id, "Your submission was approved ✅") + except Exception: + logger.warning("Could not notify user %s of approval", user_id) + + state_store.submissions.pop(sub_id, None) + try: + await cb.message.edit_reply_markup() + except Exception: + pass + await cb.answer() + + +@router.callback_query(F.data.startswith("r|")) +async def reject(cb: types.CallbackQuery): + sub_id = int(cb.data.split("|")[1]) + submission = state_store.submissions.get(sub_id) + if not submission: + await cb.answer("Submission not found.", show_alert=True) + return + try: + await cb.bot.send_message(submission["user_id"], "Your submission was rejected ❌") + except Exception: + logger.warning("Could not notify user %s of rejection", submission["user_id"]) + state_store.submissions.pop(sub_id, None) + try: + await cb.message.edit_reply_markup() + except Exception: + pass + await cb.answer() + +# ── Admin contact-group reply commands ──────────────────────────────────────── + +@router.message(F.chat.id == CHAT_CONTACT_ADMIN_GROUP_ID, F.reply_to_message, Command("stop")) +async def admin_stop_chat(message: types.Message): + route = state_store.chat_message_map.get(message.reply_to_message.message_id) + if not route: + return + user_id = route["user_id"] + if state_store.chat_sessions.pop(user_id, None): + try: + await message.bot.send_message(user_id, "An admin has closed this chat.") + except Exception: + pass + + +@router.message(F.chat.id == CHAT_CONTACT_ADMIN_GROUP_ID, F.reply_to_message, Command("ban")) +async def admin_ban_user(message: types.Message): + route = state_store.chat_message_map.get(message.reply_to_message.message_id) + if not route: + return + user_id = route["user_id"] + state_store.banned_chat_users.add(user_id) + if state_store.chat_sessions.pop(user_id, None): + try: + await message.bot.send_message(user_id, "An admin has closed this chat.") + except Exception: + pass + + +@router.message(F.chat.id == CHAT_CONTACT_ADMIN_GROUP_ID, F.reply_to_message, Command("private")) +async def admin_private_chat(message: types.Message): + route = state_store.chat_message_map.get(message.reply_to_message.message_id) + if not route: + return + user_id = route["user_id"] + username = ( + f"@{message.from_user.username}" + if message.from_user.username + else message.from_user.full_name + ) + try: + await message.bot.send_message( + user_id, + f"An admin wants to continue this chat privately. Feel free to text them at {username}, " + "or open a new chat here. This chat is now closed.", + ) + except Exception: + pass + state_store.chat_sessions.pop(user_id, None) + + +@router.message(F.chat.id == CHAT_CONTACT_ADMIN_GROUP_ID, F.reply_to_message) +async def admin_reply(message: types.Message): + if message.text in {"/stop", "/ban", "/private"}: + return + route = state_store.chat_message_map.get(message.reply_to_message.message_id) + if not route: + return + user_id = route["user_id"] + if user_id not in state_store.chat_sessions: + return + quote = build_quote(route["quote"]) + admin_name = get_admin_display_name(message.from_user) + try: + if message.text: + await message.bot.send_message(user_id, f"{admin_name}\n{quote}\n\n{message.text}") + else: + await message.bot.send_message(user_id, f"{admin_name}\n{quote}") + await message.bot.copy_message( + user_id, CHAT_CONTACT_ADMIN_GROUP_ID, message.message_id + ) + except Exception: + logger.exception("Failed to relay admin reply to user %s", user_id) \ No newline at end of file diff --git a/routers/group.py b/routers/group.py new file mode 100644 index 0000000..28ea33e --- /dev/null +++ b/routers/group.py @@ -0,0 +1,186 @@ +"""Handlers for the semipublic chatroom and backup group.""" +import asyncio +import logging + +from aiogram import F, Router, types +from aiogram.filters import Command + +import bot_state as state_store +from config import ( + BLACKLIST_MODE, + CHATROOM_PRIVATE_BACKUP_GROUP_ID, + CHATROOM_SEMIPUBLIC_GROUP_ID, + PURGE_INTERVAL_HOURS, + INVITELINK_ARCH, + INVITELINK_CHAT, + BOT_USERNAME, +) +from filters import contains_link, process_blacklisted_message +from hashing import register_file +from utils import is_group_admin + +logger = logging.getLogger(__name__) +router = Router(name="group") + +# ── Background purge task ───────────────────────────────────────────────────── + +async def purge_chatroom_semipublic_group(bot) -> None: + while True: + await asyncio.sleep(PURGE_INTERVAL_HOURS * 3600) + for message_id, is_bot_msg in list(state_store.chatroom_semipublic_group_messages.items()): + if is_bot_msg: + continue + try: + await bot.delete_message(CHATROOM_SEMIPUBLIC_GROUP_ID, message_id) + except Exception: + pass + state_store.chatroom_semipublic_group_messages.pop(message_id, None) + +# ── Backup-group tracker ────────────────────────────────────────────────────── + +@router.message(F.chat.id == CHATROOM_PRIVATE_BACKUP_GROUP_ID) +async def track_backup_group(message: types.Message): + if message.photo: + register_file(message.photo[-1].file_unique_id) + elif message.video: + register_file(message.video.file_unique_id) + elif message.document: + register_file(message.document.file_unique_id) + + +@router.message( + F.chat.id == CHATROOM_PRIVATE_BACKUP_GROUP_ID, + Command("reindex"), +) +async def reindex_backup(message: types.Message): + if not await is_group_admin(message.bot, CHATROOM_PRIVATE_BACKUP_GROUP_ID, message.from_user.id): + return + count = len(state_store.backup_hashes) + await message.reply( + f"ℹ️ Currently {count} files indexed in the hash cache.\n\n" + "The Bot API does not expose a bulk message history endpoint. " + "To do a full historical reindex, forward all older media back into " + "this group — the bot will register each file automatically as it arrives.", + parse_mode="HTML", + ) + +# ── Welcome / join request ──────────────────────────────────────────────────── + +@router.message(F.new_chat_members) +async def welcome(message: types.Message): + async with state_store.welcome_lock: + try: + await message.delete() + except Exception: + pass + for mid in state_store.welcome_messages.get(message.chat.id, []): + try: + await message.bot.delete_message(message.chat.id, mid) + except Exception: + pass + sent = [] + for user in message.new_chat_members: + mention = ( + f'@{user.username}' if user.username else str(user.id) + ) + text = ( + f"{mention} welcome to the official s3lfharm archive.\n\n" + "You can view media by checking pinned messages or the media section.\n\n" + "Feel free to share your own s3lfharm imagery using " + f'this bot.\n\n' + f'\U0001f48b Join the public archive\n\n' + f'
S3LF HARM
' + ) + try: + msg = await message.bot.send_message( + message.chat.id, + text, + parse_mode="HTML", + disable_web_page_preview=True, + ) + sent.append(msg.message_id) + except Exception: + logger.exception("Failed to send welcome in %s", message.chat.id) + state_store.welcome_messages[message.chat.id] = sent + + +@router.chat_join_request() +async def auto_approve_join_request(request: types.ChatJoinRequest): + try: + await request.bot.approve_chat_join_request(request.chat.id, request.from_user.id) + logger.info("Approved user %s to join %s", request.from_user.id, request.chat.id) + except Exception: + logger.exception("Failed to approve join request from %s", request.from_user.id) + +# ── Auto-delete Telegram service/system messages in semipublic group ────────── + +@router.message( + F.chat.id == CHATROOM_SEMIPUBLIC_GROUP_ID, + F.content_type.in_({ + types.ContentType.NEW_CHAT_MEMBERS, + types.ContentType.LEFT_CHAT_MEMBER, + types.ContentType.NEW_CHAT_TITLE, + types.ContentType.NEW_CHAT_PHOTO, + types.ContentType.DELETE_CHAT_PHOTO, + types.ContentType.GROUP_CHAT_CREATED, + types.ContentType.SUPERGROUP_CHAT_CREATED, + types.ContentType.MESSAGE_AUTO_DELETE_TIMER_CHANGED, + types.ContentType.PINNED_MESSAGE, + }), +) +async def delete_service_messages(message: types.Message): + try: + await message.delete() + except Exception: + pass + +# ── Semipublic group moderation ─────────────────────────────────────────────── + +@router.message(F.chat.id == CHATROOM_SEMIPUBLIC_GROUP_ID) +async def handle_semipublic_message(message: types.Message): + state_store.chatroom_semipublic_group_messages[message.message_id] = bool( + message.from_user and message.from_user.is_bot + ) + + if not message.from_user or message.from_user.is_bot: + return + + # ── Define text first ──────────────────────────────────────────────────── + text = message.text or message.caption or "" + + # ── Blacklist check (all users including admins) ────────────────────────── + censored_text, was_censored = process_blacklisted_message(text) + if was_censored: + try: + await message.delete() + except Exception: + pass + sender = ( + f'@{message.from_user.username}' + if message.from_user.username + else str(message.from_user.id) + ) + if BLACKLIST_MODE == 1: + try: + await message.bot.send_message( + CHATROOM_SEMIPUBLIC_GROUP_ID, + f"Censored text from {sender} →\n
{censored_text}
", + parse_mode="HTML", + ) + except Exception: + logger.exception("Failed to send censor notice") + return + + # ── Link check — admins are fully exempt ───────────────────────────────── + if await is_group_admin(message.bot, CHATROOM_SEMIPUBLIC_GROUP_ID, message.from_user.id): + return # admin: keep message as-is, no further checks + + has_link_entity = any( + e.type in ("url", "text_link") + for e in (message.entities or []) + (message.caption_entities or []) + ) + if has_link_entity or contains_link(text): + try: + await message.delete() + except Exception: + pass \ No newline at end of file diff --git a/routers/private.py b/routers/private.py new file mode 100644 index 0000000..2c87644 --- /dev/null +++ b/routers/private.py @@ -0,0 +1,240 @@ +"""Handlers for private (DM) interactions: TOS, menu, upload FSM, chat.""" +import logging +import time + +from aiogram import F, Router, types +from aiogram.filters import Command, CommandStart +from aiogram.fsm.context import FSMContext + +import bot_state as state_store +from config import ( + CHAT_CONTACT_ADMIN_GROUP_ID, + DAILY_SUBMISSION_LIMIT, + REVIEW_ADMIN_CHATROOM_SEMIPUBLIC_GROUP_ID, + INVITELINK_ARCH, + INVITELINK_CHAT, + BOT_USERNAME, +) +from keyboards import admin_kb, anonymous_choice_kb, confirm_kb, menu_kb +from persistence import save_confirmed_users +from states import ChatSetup, Upload +from utils import ( + build_message_preview, + build_quote, + cancel_upload_prompt, + get_admin_display_name, + schedule_upload_prompt, + send_media_items, + start_chat_session, +) + +logger = logging.getLogger(__name__) +router = Router(name="private") + +# ── TOS confirm ─────────────────────────────────────────────────────────────── + +@router.callback_query(F.data == "tos_confirm") +async def tos_confirm_cb(cb: types.CallbackQuery): + state_store.confirmed_users.add(cb.from_user.id) + save_confirmed_users() + try: + await cb.message.edit_reply_markup(reply_markup=None) + except Exception: + pass + await cb.message.answer("Choose:", reply_markup=menu_kb()) + await cb.answer() + +# ── /start ──────────────────────────────────────────────────────────────────── + +@router.message(CommandStart()) +async def cmd_start(message: types.Message, state: FSMContext): + await message.answer( + "The other videos/images can be found in the channel.\n" + "Channel invite links:\n" + f"[Archive]({INVITELINK_ARCH}) | [Chat]({INVITELINK_CHAT})", + parse_mode="Markdown", + ) + await message.answer("Choose:", reply_markup=menu_kb()) + +# ── Menu buttons ────────────────────────────────────────────────────────────── + +@router.callback_query(F.data == "menu_upload") +async def menu_upload_cb(cb: types.CallbackQuery, state: FSMContext): + if cb.from_user.id in state_store.chat_sessions: + await cb.answer( + "Your chat is active. Send /stop to return to the submission menu.", + show_alert=True, + ) + return + cancel_upload_prompt(cb.from_user.id) + await state.set_state(Upload.waiting_media) + await state.update_data(media=[]) + await cb.message.answer("Send photos or videos. You can send multiple, then press Submit.") + await cb.answer() + + +@router.callback_query(F.data == "menu_chat") +async def menu_chat_cb(cb: types.CallbackQuery, state: FSMContext): + if cb.from_user.id in state_store.banned_chat_users: + await cb.answer("You are not allowed to contact the administrators.", show_alert=True) + return + if cb.from_user.id in state_store.chat_sessions: + await cb.answer( + "Your chat is already active. Send /stop to close it.", show_alert=True + ) + return + cancel_upload_prompt(cb.from_user.id) + await state.clear() + await state.set_state(ChatSetup.waiting_anonymous_choice) + await cb.message.answer("Do you want to remain anonymous?", reply_markup=anonymous_choice_kb) + await cb.answer() + +# ── Anonymous choice ────────────────────────────────────────────────────────── + +@router.callback_query(F.data == "menu_anon_yes") +async def menu_anon_yes_cb(cb: types.CallbackQuery, state: FSMContext): + await state.set_state(ChatSetup.waiting_anonymous_name) + await cb.message.answer("What name do you want to use?") + await cb.answer() + + +@router.callback_query(F.data == "menu_anon_no") +async def menu_anon_no_cb(cb: types.CallbackQuery, state: FSMContext): + await start_chat_session(cb.bot, cb.from_user, cb.message.answer, state) + await cb.answer() + + +@router.message(ChatSetup.waiting_anonymous_name, F.text) +async def anonymous_name(message: types.Message, state: FSMContext): + name = message.text.strip() + if not name: + await message.answer("Please send a valid name.") + return + await start_chat_session(message.bot, message.from_user, message.answer, state, anonymous_name=name) + +# ── /stop ───────────────────────────────────────────────────────────────────── + +@router.message(Command("stop"), F.chat.type == "private") +async def cmd_stop(message: types.Message, state: FSMContext): + cancel_upload_prompt(message.from_user.id) + state_store.upload_prompt_msg_ids.pop(message.from_user.id, None) + await state.clear() + if state_store.chat_sessions.pop(message.from_user.id, None): + await message.answer("Chat stopped.", reply_markup=menu_kb()) + else: + await message.answer("Choose:", reply_markup=menu_kb()) + +# ── Media upload FSM ────────────────────────────────────────────────────────── + +@router.message(Upload.waiting_media, F.photo | F.video) +async def handle_media(message: types.Message, state: FSMContext): + if message.photo: + file_id, file_unique_id, file_type = ( + message.photo[-1].file_id, + message.photo[-1].file_unique_id, + "photo", + ) + else: + file_id, file_unique_id, file_type = ( + message.video.file_id, + message.video.file_unique_id, + "video", + ) + data = await state.get_data() + media = data.get("media", []) + media.append({"file_id": file_id, "file_unique_id": file_unique_id, "type": file_type}) + await state.update_data(media=media) + await schedule_upload_prompt(message.bot, message.chat.id, message.from_user.id) + + +@router.callback_query(F.data == "submit") +async def cb_submit(cb: types.CallbackQuery, state: FSMContext): + if cb.from_user.id in state_store.submitting_users: + await cb.answer() + return + state_store.submitting_users.add(cb.from_user.id) + try: + cancel_upload_prompt(cb.from_user.id) + state_store.upload_prompt_msg_ids.pop(cb.from_user.id, None) + + data = await state.get_data() + media = list(data.get("media", [])) + if not media: + await cb.answer("Send at least one photo or video first.", show_alert=True) + return + + user = cb.from_user + current_day = int(time.time() // 86400) + user_limit = state_store.daily_submissions.get(user.id) + if not user_limit or user_limit["day"] != current_day: + user_limit = {"day": current_day, "count": 0} + state_store.daily_submissions[user.id] = user_limit + if user_limit["count"] >= DAILY_SUBMISSION_LIMIT: + await cb.message.answer( + f"You reached your daily limit of {DAILY_SUBMISSION_LIMIT} submissions. " + "Please try again tomorrow." + ) + await cb.answer() + return + + state_store.counter += 1 + sub_id = state_store.counter + state_store.submissions[sub_id] = {"user_id": user.id, "media": media} + caption = "New submission from @" + (user.username or user.full_name) + try: + await send_media_items(cb.bot, REVIEW_ADMIN_CHATROOM_SEMIPUBLIC_GROUP_ID, media) + await cb.bot.send_message( + REVIEW_ADMIN_CHATROOM_SEMIPUBLIC_GROUP_ID, + caption, + reply_markup=admin_kb(sub_id), + ) + except Exception: + logger.exception("Failed to forward submission %s to review group", sub_id) + + user_limit["count"] += 1 + await state.clear() + await cb.message.answer("Submitted ✅", reply_markup=menu_kb()) + await cb.answer() + finally: + state_store.submitting_users.discard(cb.from_user.id) + + +@router.callback_query(F.data == "cancel") +async def cb_cancel(cb: types.CallbackQuery, state: FSMContext): + cancel_upload_prompt(cb.from_user.id) + state_store.upload_prompt_msg_ids.pop(cb.from_user.id, None) + await state.clear() + await cb.message.answer("Cancelled.", reply_markup=menu_kb()) + await cb.answer() + +# ── Active chat (DM → admin group) ─────────────────────────────────────────── + +@router.message(F.chat.type == "private") +async def active_chat_router(message: types.Message, state: FSMContext): + session = state_store.chat_sessions.get(message.from_user.id) + if not session: + return + preview = build_message_preview(message) + header = f"From: {session['display_name']}\n\n{preview}" + try: + if message.text: + sent = await message.bot.send_message(CHAT_CONTACT_ADMIN_GROUP_ID, header) + state_store.chat_message_map[sent.message_id] = { + "user_id": message.from_user.id, + "quote": preview, + } + else: + info_msg = await message.bot.send_message(CHAT_CONTACT_ADMIN_GROUP_ID, header) + copied_msg = await message.bot.copy_message( + CHAT_CONTACT_ADMIN_GROUP_ID, message.chat.id, message.message_id + ) + for mid in (info_msg.message_id, copied_msg.message_id): + state_store.chat_message_map[mid] = { + "user_id": message.from_user.id, + "quote": preview, + } + except Exception: + logger.exception( + "Failed to forward chat message from user %s to admin group", + message.from_user.id, + ) \ No newline at end of file diff --git a/rsrcs/blacklist.txt b/rsrcs/blacklist.txt new file mode 100644 index 0000000..293f264 --- /dev/null +++ b/rsrcs/blacklist.txt @@ -0,0 +1,5220 @@ +1 man 1 jar +1m1j +1man1jar +2 girls 1 cup +2g1c +2girls1cup +acrotomophile +acrotomophilia +alabama hot pocket +alabama tuna melt +alaskan pipeline +algophile +algophilia +anal +anal assassin +anal astronaut +anilingus +anus +ape shit +ape-shit +apeshit +apotemnophile +apotemnophilia +arse +arse bandit +arsehole +ass +ass bandit +asshole +auto erotic +autoerotic +babeland +baby batter +baby gravy +baby juice +ball batter +ball gag +ball gravy +ball kicking +ball licking +ball sack +ball sucking +ball-gag +ball-kicking +ball-licking +ball-sucking +ballcuzi +ballgag +bang bros +bang bus +bangbros +bangbus +bareback +barely legal +bastard +bastinado +batty boi +batty boy +battyboi +battyboy +bdsm +bean flicker +bean queen +bean-flicker +beaner +beaners +beanflicker +beastiality +beaver cleaver +beaver lips +beestiality +bellend +bellesa +bestiality +bicon +big boobs +big breasts +big cock +big knockers +big tits +birdlock +bitch +bitches +black cock +bloody +blow job +blow your load +blow-job +blowjob +blue waffle +bluewaffle +blumpkin +bollocks +bone smuggler +bone-smuggler +boner +bonesmuggler +boob +booty buffer +booty call +booty-buffer +boston george +breasts +brown piper +brown shower +brown showers +brown-piper +brownie king +brownie queen +brownpiper +buddha head +buddha-head +buddhahead +bufter +bufty +bugger +bukkake +bull shit +bull-shit +bulldyke +bullet vibe +bullet vibrator +bullshit +bum boy +bum chum +bum driller +bum pilot +bum pirate +bum rider +bum robber +bum rustler +bum-boy +bum-chum +bum-driller +bum-pirate +bum-robber +bumboy +bumchum +bumdriller +bumhole engineer +bumrider +bumrobber +butt boy +butt pilot +butt pirate +butt rider +butt robber +butt rustler +butt-boy +butt-pirate +butt-robber +buttboy +butthole engineer +buttrider +buttrobber +camel jockey +camel jockies +camel toe +cameljockey +cameljockies +canadian porch swing +carpet muncher +carpetmuncher +cheese eating surrender monkey +cheese-eating surrender monkey +chi chi man +chi-chi man +chicken queen +china man +china men +chinaman +chinamen +ching chong +ching-chong +chink +chinks +chinky +chocolate rosebud +chocolate rosebuds +cholerophile +cholerophilia +christ +cialis +circle-jerk +circlejerk +cishet +cissie +cissy +claustrophile +claustrophilia +cleveland accordion +cleveland hot waffle +cleveland steamer +clit +clitoris +clover clamp +clover clamps +clunge +cluster fuck +cluster-fuck +clusterfuck +cock +cockpipe cosmonaut +cockstruction worker +coimetrophile +coimetrophilia +collared +collaring +coon +coons +coprolagnia +coprophile +coprophilia +cornhole +crafty butcher +crap +cream-pie +creampie +cum +cum shot +cum shots +cumming +cumshot +cumshots +cunnilingus +cunt +cunt boy +cunt-boy +cuntboy +cunts +curry muncher +curry-muncher +currymuncher +damn +darkey +darkie +darkies +darky +date rape +daterape +ddlg +deep throat +deep-throat +deepthroat +dendrophile +dendrophilia +dick +dick girl +dick-girl +dickgirl +dildo +dildos +dingleberries +dingleberry +dipsea +dirty pillows +dirty sanchez +dishabiliophile +dishabiliophilia +dog shit +dog style +dog-shit +doggie style +doggie-style +doggiestyle +doggy style +doggy-style +doggystyle +dogshit +dolcett +domination +dominatrix +domme +dommes +donkey punch +donut muncher +donut puncher +doon coon +dooncoon +double penetration +dp action +dry hump +dune coon +dune-coon +dutch rudder +dyke +dystychiphile +dystychiphilia +edge play +edgeplay +ejaculate +ejaculated +ejaculating +ejaculation +electro-play +electroplay +emetophile +emetophilia +enby +eskimo trebuchet +eye-tie +eyetie +fag +fag bomb +fag-bomb +fagbomb +faggot +fagot +felch +felching +fellating +fellatio +female squirting +figging +finger bang +fingerbang +fingerbanging +fingered +fingering +finocchio +finoccio +finochio +fisted +fisting +foot job +foot-job +footjob +french rudder +frog eater +frog-eater +frogeater +frolic me +frolicme +frottage +frotting +fuck +fuck-wit +fucken +fucker +fuckers +fuckhead +fuckheads +fuckin +fucking +fucks +fucktard +fucktards +fuckwad +fuckwads +fuckwhit +fuckwit +fuckwits +fudge packer +fudge-packer +fudgepacker +futanari +g-spot +gang bang +gangbang +gay sex +gaysian +genitals +genitorture +gerontophile +gerontophilia +giant cock +gin jockey +gin jocky +girl on top +go-kun +goatcx +goatse +god damn +god damned +god-damn +god-damned +goddamn +goddamned +gokkun +golden shower +golden showers +golliwog +gollywog +gook +gook-eye +gookie +gooks +gooky +goregasm +gray queen +greaseball +groom +groomer +grooming +y&t +young and tight +grey queen +grope +group sex +gym bunny +gymbunny +hadji +haji +hajji +hand job +hand-job +handjob +heimie +hell +hermie +hickory switch +hippophile +hippophilia +homoerotic +honkey +honkeys +honkies +honky +horny +horse shit +horse-shit +horseshit +hot carl +hot richard +huge cock +humping +hymie +impact play +impact-play +incest +intercourse +jack off +jack-off +jail bait +jailbait +jap +jelly donut +jerk mate +jerk off +jerk-off +jerkmate +jigaboo +jiggerboo +jizz +juggs +jungle bunny +junglebunny +kennebunkport surprise +kentucky klondike +kentucky tractor puller +kike +kinbaku +kitty puncher +kitty-puncher +kittypuncher +knobbing +kraut +krauts +kunt +kunts +kynophile +kynophilia +lady boy +lady-boy +ladyboy +leather restraint +leather straight jacket +lemon party +lemonparty +leningrad steamer +lesbo +leso +lezzie +lezzies +light in the fedora +light in the loafers +light in the pants +limp wristed +limp-wristed +literotica +lovemaking +male squirting +male-squirting +massive cock +masterb8 +masterbate +masturb8 +masturbate +masturbating +masturbation +mayonnaise monkey +mayonnaise monkies +mdlb +meat masseuse +meat spin +meatspin +menage a trois +menage-a-trois +menages a trois +menages-a-trois +menophile +menophilia +mexican pancake +milwaukee blizzard +missionary position +mississippi birdbath +mound of venus +mr hands +mr. hands +mrhands +muff diver +muff diver +muff diving +muff-diver +muffdiver +muffdiver +muffdiving +muscle mary +mvtube +nambla +necrophile +necrophilia +negro +neo nazi +neo-nazi +neonazi +nig nog +nigerian hurricane +nigga +nigger +niggs +nignog +nimpho +nimphomania +nimphomaniac +nipple +nipple clamp +nipple clamps +nipples +nude +nudity +nutten +nympho +nymphomania +nymphomaniac +octopussy +oklahomo +omorashi +one cup two girls +one jar one man +one man one jar +only fans +onlyfans +orgasm +orgasmic +orgasms +paedo bear +paedobear +paedophile +paedophilia +pain slut +painslut +paki +panamanian petting zoo +pansy +panties +parthenophile +parthenophilia +pedo bear +pedobear +pedophile +pedophilia +pegging +penis +peter puffer +peter-puffer +peterpuffer +petrol sniffer +petrol-sniffer +petrolsniffer +phagophile +phagophilia +piece of shit +pieces of shit +pikey +pikeys +piss off +piss pig +piss pig +pissed off +pissing +pisspig +pisspig +playboy +pleasure chest +pnigerophile +pnigerophilia +pnigophile +pnigophilia +poinephile +poinephilia +pony boy +pony girl +pony-boy +pony-girl +pony-play +ponyboy +ponygirl +ponyplay +poof +poon +poontang +poop chute +poopchute +porn +porn hub +pornhub +porno +pornographic +pornography +pornos +potato queen +prince albert piercing +proctophile +proctophilia +pubes +punani +punany +pussy +pussy puncher +pussy-puncher +pussypuncher +queaf +queef +quim +rag head +rag heads +raghead +ragheads +raging boner +ramen yarmulke +rape +raping +rapist +rectum +retard +retarded +reverse cowgirl +rhabdophile +rhabdophilia +rhypophile +rhypophilia +rice queen +rimjob +rimming +ring raider +ringraider +rusty trombone +sand nigger +sand-nigger +sandnigger +santorum +scatophile +scatophilia +schlong +scissoring +semen +seplophile +seplophilia +sex +shaved beaver +shaved pussy +she male +she-male +sheep shagger +sheepshagger +shemale +shibari +shit +shit head +shithead +shitty +shlong +shota +shrimping +sissy +skeet +skittle harvest +skittles harvest +slant eye +slant-eye +slanteye +snatch +snowballing +sod off +sodding +sodomise +sodomist +sodomize +sodomy +spastic +spearchucker +spic +spick +spicks +spics +spicy gringo +splooge +splooge moose +spooge +spunk +strap on +strap-on +strap-on +strapon +strappado +suastika +svastika +swamp guinea +swamp-guinea +swastika +switch hitter +t-girl +taphephile +taphephilia +tea bagged +tea bagging +tea-bagged +tea-bagging +tgirl +thanatophile +thanatophilia +threesome +throating +throbbing boner +throbbing cock +thumbzilla +timber nigger +timber-nigger +timbernigger +tits +titties +titty +topless +tosser +towel head +towel-head +towelhead +trannie +tranny +transbian +traumatophile +traumatophilia +tribadism +tribbing +tub girl +tubgirl +twat +twink +two girls one cup +urethra play +urophile +urophilia +vagina +venus mound +viagra +vibrator +violet wand +vorarephile +vorarephilia +voyeurweb +wagon burner +wagon-burner +wank +wanker +wax play +wax-play +wet back +wet dream +wet-back +wetback +whigger +white power +white-power +whitepower +whore +wigga +wigger +wiitwd +wog +wogs +wolfbagging +worldsex +wrapping men +wrinkled starfish +xhamster +xnxx +xtube +xvideos +xxx +xyrophile +xyrophilia +yellow shower +yellow showers +zipper head +zipper-head +zipperhead +zippo cat +zippo-cat +zippocat +zoophile +zoophilia +1 man 1 jar +1m1j +1man1jar +2 girls 1 cup +2g1c +2girls1cup +acrotomophile +acrotomophilia +alabama hot pocket +alabama tuna melt +alaskan pipeline +algophile +algophilia +anal +anal assassin +anal astronaut +anilingus +anus +ape shit +ape-shit +apeshit +apotemnophile +apotemnophilia +arse +arse bandit +arsehole +ass +ass bandit +asshole +auto erotic +autoerotic +babeland +baby batter +baby gravy +baby juice +ball batter +ball gag +ball gravy +ball kicking +ball licking +ball sack +ball sucking +ball-gag +ball-kicking +ball-licking +ball-sucking +ballcuzi +ballgag +bang bros +bang bus +bangbros +bangbus +bareback +barely legal +bastard +bastinado +batty boi +batty boy +battyboi +battyboy +bdsm +bean flicker +bean queen +bean-flicker +beaner +beaners +beanflicker +beastiality +beaver cleaver +beaver lips +beestiality +bellend +bellesa +bestiality +bicon +big boobs +big breasts +big cock +big knockers +big tits +birdlock +bitch +bitches +black cock +bloody +blow job +blow your load +blow-job +blowjob +blue waffle +bluewaffle +blumpkin +bollocks +bone smuggler +bone-smuggler +boner +bonesmuggler +boob +booty buffer +booty call +booty-buffer +boston george +breasts +brown piper +brown shower +brown showers +brown-piper +brownie king +brownie queen +brownpiper +buddha head +buddha-head +buddhahead +bufter +bufty +bugger +bukkake +bull shit +bull-shit +bulldyke +bullet vibe +bullet vibrator +bullshit +bum boy +bum chum +bum driller +bum pilot +bum pirate +bum rider +bum robber +bum rustler +bum-boy +bum-chum +bum-driller +bum-pirate +bum-robber +bumboy +bumchum +bumdriller +bumhole engineer +bumrider +bumrobber +butt boy +butt pilot +butt pirate +butt rider +butt robber +butt rustler +butt-boy +butt-pirate +butt-robber +buttboy +butthole engineer +buttrider +buttrobber +camel jockey +camel jockies +camel toe +cameljockey +cameljockies +canadian porch swing +carpet muncher +carpetmuncher +cheese eating surrender monkey +cheese-eating surrender monkey +chi chi man +chi-chi man +chicken queen +china man +china men +chinaman +chinamen +ching chong +ching-chong +chink +chinks +chinky +chocolate rosebud +chocolate rosebuds +cholerophile +cholerophilia +christ +cialis +circle-jerk +circlejerk +cishet +cissie +cissy +claustrophile +claustrophilia +cleveland accordion +cleveland hot waffle +cleveland steamer +clit +clitoris +clover clamp +clover clamps +clunge +cluster fuck +cluster-fuck +clusterfuck +cock +cockpipe cosmonaut +cockstruction worker +coimetrophile +coimetrophilia +collared +collaring +coon +coons +coprolagnia +coprophile +coprophilia +cornhole +crafty butcher +crap +cream-pie +creampie +cum +cum shot +cum shots +cumming +cumshot +cumshots +cunnilingus +cunt +cunt boy +cunt-boy +cuntboy +cunts +curry muncher +curry-muncher +currymuncher +damn +darkey +darkie +darkies +darky +date rape +daterape +ddlg +deep throat +deep-throat +deepthroat +dendrophile +dendrophilia +dick +dick girl +dick-girl +dickgirl +dildo +dildos +dingleberries +dingleberry +dipsea +dirty pillows +dirty sanchez +dishabiliophile +dishabiliophilia +dog shit +dog style +dog-shit +doggie style +doggie-style +doggiestyle +doggy style +doggy-style +doggystyle +dogshit +dolcett +domination +dominatrix +domme +dommes +donkey punch +donut muncher +donut puncher +doon coon +dooncoon +double penetration +dp action +dry hump +dune coon +dune-coon +dutch rudder +dyke +dystychiphile +dystychiphilia +edge play +edgeplay +ejaculate +ejaculated +ejaculating +ejaculation +electro-play +electroplay +emetophile +emetophilia +enby +eskimo trebuchet +eye-tie +eyetie +fag +fag bomb +fag-bomb +fagbomb +faggot +fagot +felch +felching +fellating +fellatio +female squirting +figging +finger bang +fingerbang +fingerbanging +fingered +fingering +finocchio +finoccio +finochio +fisted +fisting +foot job +foot-job +footjob +french rudder +frog eater +frog-eater +frogeater +frolic me +frolicme +frottage +frotting +fuck +fuck-wit +fucken +fucker +fuckers +fuckhead +fuckheads +fuckin +fucking +fucks +fucktard +fucktards +fuckwad +fuckwads +fuckwhit +fuckwit +fuckwits +fudge packer +fudge-packer +fudgepacker +futanari +g-spot +gang bang +gangbang +gay sex +gaysian +genitals +genitorture +gerontophile +gerontophilia +giant cock +girl on top +go-kun +goatcx +goatse +god damn +god damned +god-damn +god-damned +goddamn +goddamned +gokkun +golden shower +golden showers +golliwog +gollywog +gook +gook-eye +gookie +gooks +gooky +goregasm +gray queen +greaseball +grey queen +grope +group sex +gym bunny +gymbunny +hadji +haji +hajji +hand job +hand-job +handjob +heimie +hell +hermie +hickory switch +hippophile +hippophilia +homoerotic +honkey +honkeys +honkies +honky +horny +horse shit +horse-shit +horseshit +hot carl +hot richard +huge cock +humping +hymie +impact play +impact-play +incest +intercourse +jack off +jack-off +jail bait +jailbait +jap +jelly donut +jerk mate +jerk off +jerk-off +jerkmate +jesus +jesus christ +jigaboo +jiggerboo +jizz +juggs +jungle bunny +junglebunny +kennebunkport surprise +kentucky klondike +kentucky tractor puller +kike +kinbaku +kitty puncher +kitty-puncher +kittypuncher +knobbing +kraut +krauts +kunt +kunts +kynophile +kynophilia +lady boy +lady-boy +ladyboy +leather restraint +leather straight jacket +lemon party +lemonparty +leningrad steamer +lesbo +leso +lezzie +lezzies +light in the fedora +light in the loafers +light in the pants +limp wristed +limp-wristed +literotica +lovemaking +male squirting +male-squirting +massive cock +masterb8 +masterbate +masturb8 +masturbate +masturbating +masturbation +mayonnaise monkey +mayonnaise monkies +mdlb +meat masseuse +meat spin +meatspin +menage a trois +menage-a-trois +menages a trois +menages-a-trois +menophile +menophilia +mexican pancake +milwaukee blizzard +missionary position +mississippi birdbath +mound of venus +mr hands +mr. hands +mrhands +muff diver +muff diver +muff diving +muff-diver +muffdiver +muffdiver +muffdiving +muscle mary +mvtube +nambla +necrophile +necrophilia +negro +neo nazi +neo-nazi +neonazi +nig nog +nigerian hurricane +nigga +nigger +niggs +nignog +nimpho +nimphomania +nimphomaniac +nipple +nipple clamp +nipple clamps +nipples +nude +nudity +nutten +nympho +nymphomania +nymphomaniac +octopussy +oklahomo +omorashi +one cup two girls +one jar one man +one man one jar +only fans +onlyfans +orgasm +orgasmic +orgasms +paedo bear +paedobear +paedophile +paedophilia +pain slut +painslut +paki +panamanian petting zoo +pansy +panties +parthenophile +parthenophilia +pedo bear +pedobear +pedophile +pedophilia +pegging +penis +peter puffer +peter-puffer +peterpuffer +petrol sniffer +petrol-sniffer +petrolsniffer +phagophile +phagophilia +piece of shit +pieces of shit +pikey +pikeys +piss off +piss pig +piss pig +pissed off +pissing +pisspig +pisspig +playboy +pleasure chest +pnigerophile +pnigerophilia +pnigophile +pnigophilia +poinephile +poinephilia +pony boy +pony girl +pony-boy +pony-girl +pony-play +ponyboy +ponygirl +ponyplay +poof +poon +poontang +poop chute +poopchute +porn +porn hub +porno +pornographic +pornography +pornos +potato queen +prince albert piercing +proctophile +proctophilia +pubes +punani +punany +pussy +pussy puncher +pussy-puncher +pussypuncher +queaf +queef +quim +rag head +rag heads +raghead +ragheads +raging boner +ramen yarmulke +rape +raping +rapist +rectum +retard +retarded +reverse cowgirl +rhabdophile +rhabdophilia +rhypophile +rhypophilia +rice queen +rimjob +rimming +ring raider +ringraider +rusty trombone +sand nigger +sand-nigger +sandnigger +santorum +scatophile +scatophilia +schlong +scissoring +semen +seplophile +seplophilia +sex +shaved beaver +shaved pussy +she male +she-male +sheep shagger +sheepshagger +shemale +shibari +shit +shit head +shithead +shitty +shlong +shota +shrimping +sissy +skeet +skittle harvest +skittles harvest +slant eye +slant-eye +slanteye +snatch +snowballing +sod off +sodding +sodomise +sodomist +sodomize +sodomy +spastic +spearchucker +spic +spick +spicks +spics +spicy gringo +splooge +splooge moose +spooge +spunk +strap on +strap-on +strap-on +strapon +strappado +suastika +svastika +swamp guinea +swamp-guinea +swastika +switch hitter +t-girl +taphephile +taphephilia +tea bagged +tea bagging +tea-bagged +tea-bagging +tgirl +thanatophile +thanatophilia +threesome +throating +throbbing boner +throbbing cock +thumbzilla +timber nigger +timber-nigger +timbernigger +tits +titties +titty +topless +tosser +towel head +towel-head +towelhead +trannie +tranny +transbian +traumatophile +traumatophilia +tribadism +tribbing +tub girl +tubgirl +twat +twink +two girls one cup +urethra play +urophile +urophilia +vagina +venus mound +viagra +vibrator +violet wand +vorarephile +vorarephilia +voyeurweb +wagon burner +wagon-burner +wank +wanker +wax play +wax-play +wet back +wet dream +wet-back +wetback +whigger +white power +white-power +whitepower +whore +wigga +wigger +wiitwd +wog +wogs +wolfbagging +worldsex +wrapping men +wrinkled starfish +xhamster +xnxx +xtube +xvideos +xxx +xyrophile +xyrophilia +yellow shower +yellow showers +zipper head +zipper-head +zipperhead +zippo cat +zippo-cat +zippocat +zoophile +zoophilia +@$$ +AssMonkey +Assface +Biatch +BlowJob +CarpetMuncher +Clit +Cock +CockSucker +Ekrem +Ekto +Felcher +Flikker +Fotze +Fu +FudgePacker +Fukah +Fuken +Fukin +Fukk +Fukkah +Fukken +Fukker +Fukkin +Goddamned +Huevon +Kurac +Lesbian +Lezzian +Lipshits +Lipshitz +MothaFucker +MothaFuker +MothaFukkah +MothaFukker +MotherFucker +MotherFukah +MotherFuker +MotherFukkah +MotherFukker +MuthaFucker +MuthaFukah +MuthaFuker +MuthaFukkah +MuthaFukker +Phuc +Phuck +Phuk +Phuker +Phukker +Poonani +Shitty +Shity +Sht +Shyt +Shyte +Shytty +Skanky +Slutty +ahole +amcik +andskota +anus +arschloch +arse +ash0le +ash0les +asholes +ass +assh0le +assh0lez +asshole +assholes +assholz +assrammer +asswipe +ayir +azzhole +b00bs +b17ch +b1tch +bassterds +bastard +bastards +bastardz +basterds +basterdz +bch +bi7ch +bich +bitch +bitches +blowjob +boffing +boiolas +bollock +boobs +breasts +btch +buceta +bullshit +butthole +buttpirate +buttwipe +c0ck +c0cks +c0k +cabron +cawk +cawks +cazzo +chink +chraa +chuj +cipa +clit +clits +cnts +cntz +cock +cockhead +cocks +cocksucker +crap +cum +cunt +cunts +cuntz +d4mn +damn +daygo +dego +dick +dike +dild0 +dild0s +dildo +dildos +dilld0 +dilld0s +dirsa +dominatricks +dominatrics +dominatrix +dupa +dyke +dziwka +ejackulate +ejakulate +enculer +enema +faen +fag +fag1t +faget +fagg1t +faggit +faggot +fagit +fags +fagz +faig +faigs +fanculo +fanny +fart +fatass +fcuk +feces +feg +ficken +fitt +flipping +foreskin +fuchah +fuck +fucka +fucker +fuckin +fucking +fucks +fuk +fukah +fuker +fukka +fukkah +fukker +futkretzn +fux0r +g00k +gay +gaybor +gayboy +gaygirl +gays +gayz +gook +guiena +h00r +h0ar +h0r +h0re +h4x0r +hell +hells +helvete +hoar +hoer +honkey +hoor +hoore +hore +hui +injun +jackoff +jap +japs +jerkoff +jisim +jism +jiss +jizm +jizz +kanker +kawk +kike +klootzak +knob +knobs +knobz +knulle +kraut +kuk +kuksuger +kunt +kunts +kuntz +kurwa +kusi +kyrpa +l3i+ch +l3itch +lesbian +lesbo +mamhoon +masochist +masokist +massterbait +masstrbait +masstrbate +masterbaiter +masterbat +masterbat3 +masterbate +masterbates +masturbat +masturbate +merd +mibun +mofo +monkleigh +motha +motherfucker +mouliewop +muie +mulkku +muschi +mutha +n1gr +nastt +nasty +nazi +nazis +nepesaurio +nigga +niggas +nigger +nigur +niiger +niigr +nutsack +orafis +orgasim +orgasm +orgasum +oriface +orifice +orifiss +orospu +p0rn +packi +packie +packy +paki +pakie +paky +paska +pecker +peeenus +peeenusss +peenus +peinus +pen1s +penas +penis +penisbreath +penus +penuus +perse +phuck +picka +pierdol +pillu +pimmel +pimpis +piss +pizda +polac +polack +polak +poontsee +poop +porn +pr0n +pr1c +pr1ck +pr1k +preteen +pula +pule +pusse +pussee +pussy +puta +puto +puuke +puuker +qahbeh +queef +queer +queers +queerz +qweers +qweerz +qweir +rautenberg +recktum +rectum +retard +s.o.b. +sadist +scank +schaffer +scheiss +schlampe +schlong +schmuck +screw +screwing +scrotum +semen +sex +sexx +sexxx +sexy +sh1t +sh1ter +sh1ts +sh1tter +sh1tz +sharmuta +sharmute +self-harm +selfharm +self harm +self injury +nssi +shemale +shi+ +shipal +shit +shits +shitt +shitter +shitz +shiz +skanck +skank +skankee +skankey +skanks +skrib +slut +sluts +slutz +smut +sonofabitch +sx +teets +teez +testical +testicle +tit +tits +titt +turd +va1jina +vag1na +vagiina +vagina +vaj1na +vajina +vullva +vulva +w00se +w0p +wank +wh00r +wh0re +whoar +whore +xrated +xxx +三级片 +下三烂 +下贱 +个老子的 +九游 +乳 +乳交 +乳头 +乳房 +乳波臀浪 +交配 +仆街 +他奶奶 +他奶奶的 +他奶娘的 +他妈 +他妈ㄉ王八蛋 +他妈地 +他妈的 +他娘 +他马的 +你个傻比 +你他马的 +你全家 +你奶奶的 +你她马的 +你妈 +你妈的 +你娘 +你娘卡好 +你娘咧 +你它妈的 +你它马的 +你是鸡 +你是鸭 +你马的 +做爱 +傻比 +傻逼 +册那 +军妓 +几八 +几叭 +几巴 +几芭 +刚度 +刚瘪三 +包皮 +十三点 +卖B +卖比 +卖淫 +卵 +卵子 +双峰微颤 +口交 +口肯 +叫床 +吃屎 +后庭 +吹箫 +塞你公 +塞你娘 +塞你母 +塞你爸 +塞你老师 +塞你老母 +处女 +外阴 +大卵子 +大卵泡 +大鸡巴 +奶 +奶奶的熊 +奶子 +奸 +奸你 +她妈地 +她妈的 +她马的 +妈B +妈个B +妈个比 +妈个老比 +妈妈的 +妈比 +妈的 +妈的B +妈逼 +妓 +妓女 +妓院 +妳她妈的 +妳妈的 +妳娘的 +妳老母的 +妳马的 +姘头 +姣西 +姦 +娘个比 +娘的 +婊子 +婊子养的 +嫖娼 +嫖客 +它妈地 +它妈的 +密洞 +射你 +射精 +小乳头 +小卵子 +小卵泡 +小瘪三 +小肉粒 +小骚比 +小骚货 +小鸡巴 +小鸡鸡 +屁眼 +屁股 +屄 +屌 +巨乳 +干x娘 +干七八 +干你 +干你妈 +干你娘 +干你老母 +干你良 +干妳妈 +干妳娘 +干妳老母 +干妳马 +干您娘 +干机掰 +干死CS +干死GM +干死你 +干死客服 +幹 +强奸 +强奸你 +性 +性交 +性器 +性无能 +性爱 +情色 +想上你 +懆您妈 +懆您娘 +懒8 +懒八 +懒叫 +懒教 +成人 +我操你祖宗十八代 +扒光 +打炮 +打飞机 +抽插 +招妓 +插你 +插死你 +撒尿 +操你 +操你全家 +操你奶奶 +操你妈 +操你娘 +操你祖宗 +操你老妈 +操你老母 +操妳 +操妳全家 +操妳妈 +操妳娘 +操妳祖宗 +操机掰 +操比 +操逼 +放荡 +日他娘 +日你 +日你妈 +日你老娘 +日你老母 +日批 +月经 +机八 +机巴 +机机歪歪 +杂种 +浪叫 +淫 +淫乱 +淫妇 +淫棍 +淫水 +淫秽 +淫荡 +淫西 +湿透的内裤 +激情 +灨你娘 +烂货 +烂逼 +爛 +狗屁 +狗日 +狗狼养的 +玉杵 +王八蛋 +瓜娃子 +瓜婆娘 +瓜批 +瘪三 +白烂 +白痴 +白癡 +祖宗 +私服 +笨蛋 +精子 +老二 +老味 +老母 +老瘪三 +老骚比 +老骚货 +肉壁 +肉棍子 +肉棒 +肉缝 +肏 +肛交 +肥西 +色情 +花柳 +荡妇 +賤 +贝肉 +贱B +贱人 +贱货 +贼你妈 +赛你老母 +赛妳阿母 +赣您娘 +轮奸 +迷药 +逼 +逼样 +野鸡 +阳具 +阳萎 +阴唇 +阴户 +阴核 +阴毛 +阴茎 +阴道 +阴部 +雞巴 +靠北 +靠母 +靠爸 +靠背 +靠腰 +驶你公 +驶你娘 +驶你母 +驶你爸 +驶你老师 +驶你老母 +骚比 +骚货 +骚逼 +鬼公 +鸡8 +鸡八 +鸡叭 +鸡吧 +鸡奸 +鸡巴 +鸡芭 +鸡鸡 +龟儿子 +龟头 +กระดอ +กระเด้า +กระหรี่ +กะปิ +กู +ขี้ +ควย +จิ๋ม +จู๋ +เจ๊ก +เจี๊ยว +ดอกทอง +ตอแหล +ตูด +น้ําแตก +มึง +แม่ง +เย็ด +รูตูด +ล้างตู้เย็น +ส้นตีน +สัด +เสือก +หญิงชาติชั่ว +หลั่ง +ห่า +หํา +หี +เหี้ย +อมนกเขา +ไอ้ควาย +Asesinato +asno +bastardo +Bollera +Cabron +Cabrón +Caca +Chupada +Chupapollas +Chupetón +concha +Concha de tu madre +Coño +Coprofagía +Culo +Drogas +Esperma +Fiesta de salchichas +Follador +Follar +Gilipichis +Gilipollas +Hacer una paja +Haciendo el amor +Heroína +Hija de puta +Hijaputa +Hijo de puta +Hijoputa +Idiota +Imbécil +infierno +Jilipollas +Kapullo +Lameculos +Maciza +Macizorra +maldito +Mamada +Marica +Maricón +Mariconazo +martillo +Mierda +Nazi +Orina +Pedo +Pervertido +Pezón +Pinche +Pis +Prostituta +Puta +Racista +Ramera +Sádico +Semen +Sexo +Sexo oral +Soplagaitas +Soplapollas +Tetas grandes +Tía buena +Travesti +Trio +Verga +vete a la mierda +Vulva +baiser +bander +bigornette +bite +bitte +bloblos +bordel +bosser +bourré +bourrée +brackmard +branlage +branler +branlette +branleur +branleuse +brouter le cresson +caca +cailler +chatte +chiasse +chier +chiottes +clito +clitoris +connard +connasse +conne +couilles +cramouille +déconne +déconner +drague +emmerdant +emmerder +emmerdeur +emmerdeuse +enculé +enculée +enculeur +enculeurs +enfoiré +enfoirée +étron +fille de pute +fils de pute +folle +foutre +gerbe +gerber +gouine +grande folle +grogniasse +gueule +jouir +la putain de ta mère +MALPT +ménage à trois +merde +merdeuse +merdeux +meuf +nègre +nique ta mère +nique ta race +palucher +pédale +pédé +péter +pipi +pisser +pouffiasse +pousse-crotte +putain +pute +ramoner +sac à merde +salaud +salope +suce +tapette +teuf +tringler +trique +trou du cul +turlute +veuve +zigounette +zizi +bychara +byk +chernozhopyi +dolboy'eb +ebalnik +ebalo +ebalom sch'elkat +gol +mudack +opizdenet +osto'eblo +ostokhuitel'no +ot'ebis +otmudohat +otpizdit +otsosi +padlo +pedik +perdet +petuh +pidar gnoinyj +pizda +pizdato +pizdatyi +piz'det +pizdetc +pizdoi nakryt'sja +pizd'uk +piz`dyulina +podi ku'evo +poeben +po'imat' na konchik +po'iti posrat +po khuy +poluchit pizdy +pososi moyu konfetku +prissat +proebat +promudobl'adsksya pizdopro'ebina +propezdoloch +prosrat +raspeezdeyi +raspizdatyi +raz'yebuy +raz'yoba +s'ebat'sya +shalava +styervo +sukin syn +svodit posrat +svoloch +trakhat'sya +trimandoblydskiy pizdoproyob +ubl'yudok +uboy +u'ebitsche +vafl'a +vafli lovit +v pizdu +vyperdysh +vzdrochennyi +yeb vas +za'ebat +zaebis +zalupa +zalupat +zasranetc +zassat +zlo'ebuchy +бардак +бздёнок +блядки +блядовать +блядство +блядь +бугор +во пизду +встать раком +выёбываться +гандон +говно +говнюк +голый +дать пизды +дерьмо +дрочить +другой дразнится +ёбарь +ебать +ебать-копать +ебло +ебнуть +ёб твою мать +жопа +жополиз +играть на кожаной флейте +измудохать +каждый дрочит как он хочет +какая разница +как два пальца обоссать +курите мою трубку +лысого в кулаке гонять +малофя +манда +мандавошка +мент +муда +мудило +мудозмон +наебать +наебениться +наебнуться +на фиг +на хуй +на хую вертеть +на хуя +нахуячиться +невебенный +не ебет +ни за хуй собачу +ни хуя +обнаженный +обоссаться можно +один ебётся +опесдол +офигеть +охуеть +охуйтельно +половое сношение +секс +сиски +спиздить +срать +ссать +траxать +ты мне ваньку не валяй +фига +хапать +хер с ней +хер с ним +хохол +хрен +хуёво +хуёвый +хуем груши околачивать +хуеплет +хуило +хуиней страдать +хуиня +хуй +хуйнуть +хуй пинать +analritter +arsch +arschficker +arschlecker +arschloch +bimbo +bratze +bumsen +bonze +dödel +fick +ficken +flittchen +fotze +fratze +hackfresse +hure +hurensohn +ische +kackbratze +kacke +kacken +kackwurst +kampflesbe +kanake +kimme +lümmel +MILF +möpse +morgenlatte +möse +mufti +muschi +nackt +neger +nigger +nippel +nutte +onanieren +orgasmus +pimmel +pimpern +pinkeln +pissen +pisser +popel +poppen +porno +reudig +rosette +schabracke +schlampe +scheiße +scheisser +schiesser +schnackeln +schwanzlutscher +schwuchtel +tittchen +titten +vögeln +vollpfosten +wichse +wichsen +wichser +2g1c +2 girls 1 cup +acrotomophilia +alabama hot pocket +alaskan pipeline +anal +anilingus +anus +apeshit +arsehole +ass +asshole +assmunch +auto erotic +autoerotic +babeland +baby batter +baby juice +ball gag +ball gravy +ball kicking +ball licking +ball sack +ball sucking +bangbros +bareback +barely legal +barenaked +bastard +bastardo +bastinado +bbw +bdsm +beaner +beaners +beaver cleaver +beaver lips +bestiality +big black +big breasts +big knockers +big tits +bimbos +birdlock +bitch +bitches +black cock +blonde action +blonde on blonde action +blowjob +blow job +blow your load +blue waffle +blumpkin +bollocks +bondage +boner +boob +boobs +booty call +brown showers +brunette action +bukkake +bulldyke +bullet vibe +bullshit +bung hole +bunghole +butt +buttcheeks +butthole +camel toe +camgirl +camslut +camwhore +carpet muncher +carpetmuncher +chocolate rosebuds +circlejerk +cleveland steamer +clit +clitoris +clover clamps +clusterfuck +cock +cocks +coprolagnia +coprophilia +cornhole +coon +coons +creampie +cum +cumming +cunnilingus +cunt +darkie +date rape +daterape +deep throat +deepthroat +dendrophilia +dick +dildo +dingleberry +dingleberries +dirty pillows +dirty sanchez +doggie style +doggiestyle +doggy style +doggystyle +dog style +dolcett +domination +dominatrix +dommes +donkey punch +double dong +double penetration +dp action +dry hump +dvda +eat my ass +ecchi +ejaculation +erotic +erotism +escort +eunuch +faggot +fecal +felch +fellatio +feltch +female squirting +femdom +figging +fingerbang +fingering +fisting +foot fetish +footjob +frotting +fuck +fuck buttons +fuckin +fucking +fucktards +fudge packer +fudgepacker +futanari +gang bang +gay sex +genitals +giant cock +girl on +girl on top +girls gone wild +goatcx +goatse +god damn +gokkun +golden shower +goodpoop +goo girl +goregasm +gore +grope +group sex +g-spot +guro +hand job +handjob +hard core +hardcore +hentai +homoerotic +honkey +hooker +hot carl +hot chick +how to kill +how to murder +huge fat +humping +incest +intercourse +jack off +jail bait +jailbait +jelly donut +jerk off +jigaboo +jiggaboo +jiggerboo +jizz +juggs +kys +kike +kinbaku +kinkster +kinky +knobbing +leather restraint +leather straight jacket +lemon party +lolita +lovemaking +make me come +male squirting +masturbate +menage a trois +milf +missionary position +motherfucker +mound of venus +mr hands +muff diver +muffdiving +nambla +nawashi +negro +neonazi +nigga +nigger +nig nog +nimphomania +nipple +nipples +nsfw images +nude +nudity +nympho +nymphomania +octopussy +omorashi +one cup two girls +one guy one jar +orgasm +orgy +paedophile +paki +panties +panty +pedobear +pedophile +pegging +penis +phone sex +piece of shit +pissing +piss pig +pisspig +playboy +pleasure chest +pole smoker +ponyplay +poof +poon +poontang +punany +poop chute +poopchute +porn +porno +pornography +prince albert piercing +pthc +pubes +pussy +queaf +queef +quim +raghead +raging boner +rape +raping +rapist +rectum +reverse cowgirl +rimjob +rimming +rosy palm +rosy palm and her 5 sisters +rusty trombone +sadism +santorum +scat +schlong +scissoring +semen +sex +sexo +sexy +shaved beaver +shaved pussy +shemale +shibari +shit +shitblimp +shitty +shota +shrimping +skeet +slanteye +slut +s&m +smut +snatch +snowballing +sodomize +sodomy +spic +splooge +splooge moose +spooge +spread legs +spunk +strap on +strapon +strappado +strip club +style doggy +suck +sucks +suicide girls +sultry women +swastika +swinger +tainted love +taste my +tea bagging +threesome +throating +tied up +tight white +tit +tits +titties +titty +tongue in a +topless +tosser +towelhead +tranny +tribadism +tub girl +tubgirl +tushy +twat +twink +twinkie +two girls one cup +undressing +upskirt +urethra play +urophilia +vagina +venus mound +vibrator +violet wand +vorarephilia +voyeur +vulva +wank +wetback +wet dream +white power +wrapping men +wrinkled starfish +xx +xxx +yaoi +yellow showers +yiffy +zoophilia +abbo +abortion +abuse +addict +addicts +adult +africa +african +alla +allah +alligatorbait +amateur +anal +analannie +analsex +angie +angry +anus +arab +arabs +areola +argie +aroused +arse +arsehole +asian +ass +assassin +assassinate +assassination +assault +assbagger +assblaster +assclown +asscowboy +asses +assfuck +assfucker +asshat +asshole +assholes +asshore +assjockey +asskiss +asskisser +assklown +asslick +asslicker +asslover +assman +assmonkey +assmunch +assmuncher +asspacker +asspirate +asspuppies +assranger +asswhore +asswipe +athletesfoot +attack +australian +babe +babies +backdoor +backdoorman +backseat +badfuck +balllicker +balls +ballsack +banging +baptist +barelylegal +barf +barface +barfface +bast +bastard +bazongas +bazooms +beaner +beast +beastality +beastial +beastiality +beatoff +beat-off +beatyourmeat +beaver +bestial +bestiality +biatch +bible +bicurious +bigass +bigbastard +bigbutt +bigger +bisexual +bi-sexual +bitch +bitcher +bitches +bitchez +bitchin +bitching +bitchslap +bitchy +biteme +black +blackman +blackout +blacks +blind +blow +blowjob +boang +bogan +bohunk +bollick +bollock +bomb +bombers +bombing +bombs +bomd +bondage +boner +bong +boob +boobies +boobs +booby +boody +boom +boong +boonga +boonie +booty +bootycall +bountybar +bra +brea5t +breast +breastjob +breastlover +breastman +brothel +bugger +buggered +buggery +bullcrap +bulldike +bulldyke +bullshit +bumblefuck +bumfuck +bunga +bunghole +buried +burn +butchbabes +butchdike +butchdyke +butt +buttbang +butt-bang +buttface +buttfuck +butt-fuck +buttfucker +butt-fucker +buttfuckers +butt-fuckers +butthead +buttman +buttmunch +buttmuncher +buttpirate +buttplug +buttstain +byatch +cacker +cameljockey +cameltoe +canadian +cancer +carpetmuncher +carruth +catholic +catholics +cemetery +chav +cherrypopper +chickslick +children's +chinaman +chinamen +chinese +chink +chinky +choad +chode +christ +christian +church +cigarette +cigs +clamdigger +clamdiver +clit +clitoris +clogwog +cocaine +cock +cockblock +cockblocker +cockcowboy +cockfight +cockhead +cockknob +cocklicker +cocklover +cocknob +cockqueen +cockrider +cocksman +cocksmith +cocksmoker +cocksucer +cocksuck +cocksucked +cocksucker +cocksucking +cocktail +cocktease +cocky +cohee +coitus +color +colored +coloured +commie +communist +condom +conservative +conspiracy +coolie +cooly +coon +coondog +copulate +cornhole +corruption +cra5h +crabs +crack +crackpipe +crackwhore +crack-whore +crap +crapola +crapper +crappy +crash +creamy +crime +crimes +criminal +criminals +crotch +crotchjockey +crotchmonkey +crotchrot +cum +cumbubble +cumfest +cumjockey +cumm +cummer +cumming +cumquat +cumqueen +cumshot +cunilingus +cunillingus +cunn +cunnilingus +cunntt +cunt +cunteyed +cuntfuck +cuntfucker +cuntlick +cuntlicker +cuntlicking +cuntsucker +cybersex +cyberslimer +dago +dahmer +dammit +damn +damnation +damnit +darkie +darky +datnigga +dead +deapthroat +death +deepthroat +defecate +dego +demon +deposit +desire +destroy +deth +devil +devilworshipper +dick +dickbrain +dickforbrains +dickhead +dickless +dicklick +dicklicker +dickman +dickwad +dickweed +diddle +die +died +dies +dike +dildo +dingleberry +dink +dipshit +dipstick +dirty +disease +diseases +disturbed +dive +dix +dixiedike +dixiedyke +doggiestyle +doggystyle +dong +doodoo +doo-doo +doom +dope +dragqueen +dragqween +dripdick +drug +drunk +drunken +dumb +dumbass +dumbbitch +dumbfuck +dyefly +dyke +easyslut +eatballs +eatme +eatpussy +ecstacy +ejaculate +ejaculated +ejaculating +ejaculation +enema +enemy +erect +erection +ero +escort +ethiopian +ethnic +exchange +excrement +execute +executed +execution +executioner +explosion +facefucker +faeces +fag +fagging +faggot +fagot +failed +failure +fairies +fairy +faith +fannyfucker +fart +farted +farting +farty +fastfuck +fat +fatah +fatass +fatfuck +fatfucker +fatso +fckcum +feces +felatio +felch +felcher +felching +fellatio +feltch +feltcher +feltching +fetish +fight +filipina +filipino +fingerfood +fingerfuck +fingerfucked +fingerfucker +fingerfuckers +fingerfucking +firing +fister +fistfuck +fistfucked +fistfucker +fistfucking +fisting +flange +flasher +flatulence +flydie +flydye +fok +fondle +footaction +footfuck +footfucker +footlicker +footstar +fore +foreskin +forni +fornicate +foursome +fourtwenty +fraud +freakfuck +freakyfucker +freefuck +fubar +fucck +fuck +fucka +fuckable +fuckbag +fuckbuddy +fucked +fuckedup +fucker +fuckers +fuckface +fuckfest +fuckfreak +fuckfriend +fuckhead +fuckher +fuckin +fuckina +fucking +fuckingbitch +fuckinnuts +fuckinright +fuckit +fuckknob +fuckme +fuckmehard +fuckmonkey +fuckoff +fuckpig +fucks +fucktard +fuckwhore +fuckyou +fudgepacker +fugly +fuk +fuks +funeral +funfuck +fungus +fuuck +gangbang +gangbanged +gangbanger +gangsta +gatorbait +gay +gaymuthafuckinwhore +gaysex +geez +geezer +geni +genital +german +getiton +ginzo +gipp +girls +givehead +glazeddonut +gob +godammit +goddamit +goddammit +goddamn +goddamned +goddamnes +goddamnit +goddamnmuthafucker +goldenshower +gonorrehea +gonzagas +gook +gotohell +goy +goyim +greaseball +gringo +groe +gross +grostulation +gubba +gummer +gun +gyp +gypo +gypp +gyppie +gyppo +gyppy +hamas +handjob +hapa +harder +hardon +harem +headfuck +headlights +hebe +heeb +hell +henhouse +heroin +herpes +heterosexual +hijack +hijacker +hijacking +hillbillies +hindoo +hiscock +hitler +hitlerism +hitlerist +hobo +hodgie +hoes +hole +holestuffer +homicide +homo +homobangers +homosexual +honger +honk +honkers +honkey +honky +hook +hooker +hookers +hooters +hore +hork +horney +horniest +horny +horseshit +hosejob +hoser +hostage +hotdamn +hotpussy +hottotrot +hummer +husky +hussy +hustler +hymen +hymie +iblowu +idiot +ikey +illegal +incest +insest +intercourse +interracial +intheass +inthebuff +israel +israeli +israel's +italiano +jackass +jackoff +jackshit +jacktheripper +jade +jap +japanese +japcrap +jebus +jerkoff +jesus +jesuschrist +jew +jewish +jiga +jigaboo +jigg +jigga +jiggabo +jigger +jiggy +jihad +jijjiboo +jimfish +jism +jiz +jizim +jizjuice +jizm +jizz +jizzim +jizzum +joint +juggalo +jugs +junglebunny +kill yourself +kaffer +kaffir +kaffre +kafir +kanake +kid +kigger +kike +kill +killed +killer +killing +kills +kink +kinky +kissass +kkk +knife +knockers +kock +kondum +koon +kotex +krap +krappy +kraut +kum +kumbubble +kumbullbe +kummer +kumming +kumquat +kums +kunilingus +kunnilingus +kunt +kyke +lactate +laid +lapdance +latin +lesbain +lesbayn +lesbian +lesbin +lesbo +lez +lezbe +lezbefriends +lezbo +lezz +lezzo +liberal +libido +lickme +limey +limpdick +limy +lingerie +liquor +livesex +loadedgun +lolita +looser +loser +lotion +lovebone +lovegoo +lovegun +lovejuice +lovemuscle +lovepistol +loverocket +lowlife +lsd +lubejob +lucifer +luckycammeltoe +lugan +lynch +macaca +mafia +magicwand +mams +manhater +manpaste +marijuana +mastabate +mastabater +masterbate +masterblaster +mastrabator +masturbate +masturbating +mattressprincess +meatbeatter +meatrack +mexican +mgger +mggor +mickeyfinn +mideast +milf +minority +mockey +mockie +mocky +mofo +moky +moles +molest +molestation +molester +molestor +moneyshot +mooncricket +mormon +moron +moslem +mosshead +mothafuck +mothafucka +mothafuckaz +mothafucked +mothafucker +mothafuckin +mothafucking +mothafuckings +motherfuck +motherfucked +motherfucker +motherfuckin +motherfucking +motherfuckings +motherlovebone +muff +muffdive +muffdiver +muffindiver +mufflikcer +mulatto +muncher +munt +murder +murderer +muslim +naked +narcotic +nasty +nastybitch +nastyho +nastyslut +nastywhore +nazi +necro +negro +negroes +negroid +negro's +nig +niger +nigerian +nigerians +nigg +nigga +niggah +niggaracci +niggard +niggarded +niggarding +niggardliness +niggardliness's +niggardly +niggards +niggard's +niggaz +nigger +niggerhead +niggerhole +niggers +nigger's +niggle +niggled +niggles +niggling +nigglings +niggor +niggur +niglet +nignog +nigr +nigra +nigre +nipple +nipplering +nittit +nlgger +nlggor +nofuckingway +nook +nookey +nookie +noonan +nooner +nude +nudger +nuke +nutfucker +nymph +ontherag +oral +orga +orgasim +orgasm +orgies +orgy +osama +paki +palesimian +palestinian +pansies +pansy +panti +panties +payo +pearlnecklace +peck +pecker +peckerwood +pee +peehole +pee-pee +peepshow +peepshpw +pendy +penetration +peni5 +penile +penis +penises +penthouse +period +perv +phonesex +phuk +phuked +phuking +phukked +phukking +phungky +phuq +pi55 +picaninny +piccaninny +pickaninny +piker +pikey +piky +pimp +pimped +pimper +pimpjuic +pimpjuice +pimpsimp +pindick +piss +pissed +pisser +pisses +pisshead +pissin +pissing +pissoff +pistol +pixie +pixy +playboy +playgirl +pocha +pocho +pocketpool +pohm +polack +pom +pommie +pommy +poon +poontang +poop +pooper +pooperscooper +pooping +poorwhitetrash +popimp +porchmonkey +porn +pornflick +pornking +porno +pornography +pornprincess +pot +poverty +premature +pric +prick +prickhead +primetime +propaganda +pros +prostitute +protestant +pu55i +pu55y +pube +pubic +pubiclice +pud +pudboy +pudd +puddboy +puke +puntang +purinapricness +puss +pussie +pussies +pussy +pussycat +pussyeater +pussyfucker +pussylicker +pussylips +pussylover +pussypounder +pusy +quashie +queef +queer +quickie +quim +ra8s +rabbi +racial +racist +radical +radicals +raghead +randy +rape +raped +raper +rapist +rearend +rearentry +rectum +redlight +redneck +reefer +reestie +refugee +reject +remains +rentafuck +republican +rere +retard +retarded +ribbed +rigger +rimjob +rimming +roach +robber +roundeye +rump +russki +russkie +sadis +sadom +samckdaddy +sandm +sandnigger +satan +scag +scallywag +scat +schlong +screw +screwyou +scrotum +scum +semen +seppo +servant +sex +sexed +sexfarm +sexhound +sexhouse +sexing +sexkitten +sexpot +sexslave +sextogo +sextoy +sextoys +sexual +sexually +sexwhore +sexy +sexymoma +sexy-slim +shag +shaggin +shagging +shat +shav +shawtypimp +ext +extort +extorting +extortation +larp +extortionist +extorter +skid +sheeney +shhit +shinola +shit +shitcan +shitdick +shite +shiteater +shited +shitface +shitfaced +shitfit +shitforbrains +shitfuck +shitfucker +shitfull +shithapens +shithappens +shithead +shithouse +shiting +shitlist +shitola +shitoutofluck +shits +shitstain +shitted +shitter +shitting +shitty +shoot +shooting +shortfuck +showtime +sick +sissy +sixsixsix +sixtynine +sixtyniner +skank +skankbitch +skankfuck +skankwhore +skanky +skankybitch +skankywhore +skinflute +skum +skumbag +slant +slanteye +slapper +slaughter +slav +slave +slavedriver +sleezebag +sleezeball +slideitin +slime +slimeball +slimebucket +slopehead +slopey +slopy +slut +sluts +slutt +slutting +slutty +slutwear +slutwhore +smack +smackthemonkey +smut +snatchpatch +snigger +sniggered +sniggering +sniggers +snigger's +snowback +snownigger +sodom +sodomise +sodomite +sodomize +sodomy +sonofabitch +sonofbitch +sooty +soviet +spaghettibender +spaghettinigger +spank +spankthemonkey +sperm +spermacide +spermbag +spermhearder +spermherder +spic +spick +spig +spigotty +spik +spit +spitter +splittail +spooge +spreadeagle +spunk +spunky +squaw +stagg +stiffy +strapon +stringer +stripclub +stroke +stroking +stupid +stupidfuck +stupidfucker +suck +suckdick +sucker +suckme +suckmyass +suckmydick +suckmytit +suckoff +suicide +swallow +swallower +swalow +swastika +sweetness +syphilis +taboo +tampon +tang +tantra +tarbaby +tard +teat +terror +terrorist +teste +testicle +testicles +thicklips +thirdeye +thirdleg +threesome +threeway +timbernigger +tinkle +tit +titbitnipply +titfuck +titfucker +titfuckin +titjob +titlicker +titlover +tits +tittie +titties +titty +tnt +toilet +tongethruster +tongue +tonguethrust +tonguetramp +tortur +torture +tosser +towelhead +trailertrash +tramp +trannie +tranny +transexual +transsexual +transvestite +triplex +trisexual +trojan +trots +tuckahoe +tunneloflove +turd +turnon +twat +twink +twinkie +twobitwhore +unfuckable +upskirt +uptheass +upthebutt +urinary +urinate +urine +usama +uterus +vagina +vaginal +vatican +vibr +vibrater +vibrator +vietcong +violence +virgin +virginbreaker +vomit +vulva +wab +wank +wanker +wanking +waysted +weapon +weenie +weewee +welcher +welfare +wetb +wetback +wetspot +whacker +whash +whigger +whiskey +whiskeydick +whiskydick +whit +whitenigger +whites +whitetrash +whitey +whiz +whop +whore +whorefucker +whorehouse +wigger +willie +williewanker +willy +wog +women's +wtf +wuss +wuzzie +xtc +xxx +yankee +yellowman +zigabo +zipperhead +2g1c +4chan +baccarat +betting +betting odds +betting line +blackjack +casino +craps +gamble +gambling +online gambling +poker +roulette +slot machine +texas hold'em +a2m +acrotomophilia +adult +amateur +anal +anilingus +annie sprinkle +anus +apple bong howto +are idiots +arsehole +aryan +asexual +asian babe +ass +asshole +assmunch +auto erotic +autoerotic +babes in toyland +babeland +baby batter +baby chowder +ball gravy +ball sack +ball gag +ball kicking +ball licking +ball sucking +bangbros +bareback +barely legal +barenaked ladies +bastard +bastardo +bastinado +bbw +bdsm +beaner +bearded clam +beaver cleaver +beaver lips +behind the green door +bestiality +betty dodson +bianca beauchamp +big black +big knockers +big tits +bimbo +bimbos +birdlock +bisexual +bitch +black cock +blonde action +blonde on blonde action +blood play +blow +blow j +blow your l +bludgeon +blue waffle +blumpkin +bollocks +bondage +boner +boob +booty call +breast +brown showers +brunette action +bugger +bukkake +bulldyke +bullet vibe +bumming +bung hole +bunghole +busty +butt +butthole +buttcheeks +buttcrack +buttocks +camel toe +camgirl +camslut +camwhore +carnage +carol queen +carpet muncher +carpetmuncher +chastity belt +chink +chocolate rosebuds +chrissie wunna +church of satan +circlejerk +cleveland steamer +clit +clitoral +clitoris +clitoritis +clover clamps +clusterfuck +cocaine +cock +cocks +commie +consensual intercourse +coprolagnia +coprophilia +coprophagia +cornhole +cory silverberg +courtney trouble +crack +cream pie +creampie +crossdresser +cuckold +cum +cumming +cunt +cunnilingus +dago +darkie +date rape +daterape +deep throat +deepthroat +dick +diddy ride +dildo +dirty pillows +dirty sanchez +disembowel +dismember +dog style +doggie style +doggiestyle +doggy style +doggystyle +dolcett +domination +dominatrix +dommes +donkey punch +dothead +double dong +double penetration +dp action +ducky doolittle +dutch oven +dyke +eat my ass +ecchi +ecstasy +ejaculation +electrotorture +erection +erotic +erotism +escort +ethical slut +eunuch +excrement +extreme elvis +fag +faggot +faggot +fantasies +fapserver +fascist +fecal +felch +fellatio +feltch +femdom +female desperation +female squirting +feminazi +fetish +figging +filthy sanchez +fingering +fisting +five knuckle shuffle +flick the bean +foot fetish +footjob +foreplay +fornicate +foursome +freeones +frotting +fuck +fuck buttons +malware +fudge packer +fudgepacker +futanari +g-spot +g-string +gang bang +gay boy +gay dog +gay man +gay men +gay sex +genitals +get my sister +giant cock +ginger lynne +girl on +girl on top +girls gone wild +goat.se +goatcx +goatse +gokkun +golden shower +golliwog +goo girl +good poop +goodpoop +goodvibes +google is evil +gook +goregasm +got my sister +greaser +gringo +grope +group sex +gtfo +guro +hairy +hand job +handjob +happy slapping video +hard core +hardcore +hate +hebe +hedop +hentai +hermaphrodite +heroin +heroin +heterosexual +hickey +holdem +homoerotic +homosexual +honkey +honky +hookup +hooker +hot chick +hottie +how to commit genocide +how to commit suicide +how to kill +how to murder +how to torture +huge fat +humping +hustler +i hate +impalement +incest +injun +insertions +interracial +jack off +jackie strano +jacobs ladder piercing +jail bait +jailbait +jenna jameson +jerk off +jesse jane +jizz +jigaboo +jiggaboo +jiggerboo +john holmes +jordan capri +juggs +kama +kamasutra +kike +kinbaku +kinky +kinkster +kinsey institute +kkk +knobbing +ku klux klan +labia +latina +leather restraint +leather straight jacket +lemon party +lemonparty +lesbian +licked +linda lovelace +lindsay lohan +lingerie +lolita +lovemaking +lovers +lsd +madison young +make me come +making love +male squirting +marijuana +marijuana +masturbate +mature +mdma +meats +menage a trois +merkin +miki sawaguchi +milf +missionary +missionary position +money shot +motherfucker +mound of venus +mr hands +muff diver +muffdiving +murder +murder +naked +nambla +naughty +nawashi +nazi +necrophilia +negro +neonazi +new pornographers +nicky hilton +nig nog +nigga +nigger +nimphomania +nina hartley +nipple +nipples +nonconsent +nsfw images +nude +nut butter +nympho +nymphomania +octopussy +octopussoir +omorashi +one cup two girls +one guy one jar +oral +oral sex +orgy +orgasm +outercourse +paedophile +pamela anderson +pansy +panty +panties +paris hilton +paris whitney hilton +pecker +pedobear +pedophile +peep show +pegging +penetration +penis +penthouse +pervert +philip kindred dick +phone sex +pickaninny +piece of shit +pinko +piss +piss pig +pissing +pisspig +playboy +pleasure chest +pole smoker +ponyplay +poof +poofter +poop chute +poopchute +porn +pr0n +pre teen +preteen +prince albert piercing +prolapsed +prostitute +pthc +pubes +pubic +puppy play +pussy +queaf +queef +quickie +quim +r@ygold +raghead +raging boner +rape +raping +rapist +rapping women +rectum +redtube +redskin +renob +retard +reverse cowgirl +rimjob +rimming +roman shower +ron jeremy +rosy palm +rosy palm and her 5 sisters +rule 34 +russian brides +rusty trombone +s and m +s&m +sadie lune +sadism +sadist +sambo +sasha grey +sausage party +savage love +scat +schlong +schoolgirl +schoolboy +scissoring +seduced +seductive +semen +servitude +serviture +sex +sexo +sexy +sexual reproduction +shanna katz +shar rednaur +shauna grant +shaved beaver +shaved pussy +shaved fish +shay lauren +shemale +shibari +shit +shocker +shota +shrimping +sissy +size queen +sjambok +skank +slanteye +sleazy d +slit +slut +smells like teen spirit +smut +snatch +snowballing +sodomize +sodomy +spank +speculum +sperm +spic +spooge +spook +spread legs +spunky teens +squirt +stab +stickam girl +stileproject +stormfront +strap on +strapon +strappado +strip club +style doggy +submissive +submission +suck +sucks +suicide girls +sultry men +sultry women +susie bright +swastika +swinger +taboo 2 +tainted love +taste my +tea bagging +teen +tentacle +testicle +thong +threesome +throating +tied up +tight white +tit +titties +titty +tongue in a +tosser +towelhead +traci lords +tranny +transexual +transgender +tribadism +trisexual +tub girl +tubgirl +turd +tushy +twat +twink +twinkie +two girls +two girls one cup +undressing +upskirt +urethra play +urophilia +vagina +vagisil +vanilla rosebuds +venus mound +vibrator +violet blue +violet ray +violet wand +vivid +vorarephilia +voyeur +vulva +wank +wartenberg pinwheel +wartenberg wheel +watersports +webcam +wet dream +wetback +white power +whore +wigger +women rapping +wop +wrapping men +wrapping women +wrinkled starfish +wtf +yaoi +yellow showers +yiffy +zoophilia +amcığa +amcığı +amcığın +amcık +amcıklar +amcıklara +amcıklarda +amcıklardan +amcıkları +amcıkların +amcıkta +amcıktan +amlar +çingene +Çingenede +Çingeneden +Çingeneler +Çingenelerde +Çingenelerden +Çingenelere +Çingeneleri +Çingenelerin +Çingenenin +Çingeneye +Çingeneyi +göt +göte +götler +götlerde +götlerden +götlere +götleri +götlerin +götte +götten +götü +götün +götveren +götverende +götverenden +götverene +götvereni +götverenin +götverenler +götverenlerde +götverenlerden +götverenlere +götverenleri +götverenlerin +kaltağa +kaltağı +kaltağın +kaltak +kaltaklar +kaltaklara +kaltaklarda +kaltaklardan +kaltakları +kaltakların +kaltakta +kaltaktan +orospu +orospuda +orospudan +orospular +orospulara +orospularda +orospulardan +orospuları +orospuların +orospunun +orospuya +orospuyu +otuz birci +otuz bircide +otuz birciden +otuz birciler +otuz bircilerde +otuz bircilerden +otuz bircilere +otuz bircileri +otuz bircilerin +otuz bircinin +otuz birciye +otuz birciyi +saksocu +saksocuda +saksocudan +saksocular +saksoculara +saksocularda +saksoculardan +saksocuları +saksocuların +saksocunun +saksocuya +saksocuyu +sıçmak +sik +sike +siker sikmez +siki +sikilir sikilmez +sikin +sikler +siklerde +siklerden +siklere +sikleri +siklerin +sikmek +sikmemek +sikte +sikten +siktir +siktirir siktirmez +taşağa +taşağı +taşağın +taşak +taşaklar +taşaklara +taşaklarda +taşaklardan +taşakları +taşakların +taşakta +taşaktan +yarağa +yarağı +yarağın +yarak +yaraklar +yaraklara +yaraklarda +yaraklardan +yarakları +yarakların +yarakta +yaraktan \ No newline at end of file diff --git a/rsrcs/requirements.txt b/rsrcs/requirements.txt new file mode 100644 index 0000000..a8929ab --- /dev/null +++ b/rsrcs/requirements.txt @@ -0,0 +1 @@ +aiogram==3.27.0 \ No newline at end of file diff --git a/states.py b/states.py new file mode 100644 index 0000000..32dc2cb --- /dev/null +++ b/states.py @@ -0,0 +1,11 @@ +from aiogram.fsm.state import State, StatesGroup + + +class Upload(StatesGroup): + waiting_media = State() + confirm = State() + + +class ChatSetup(StatesGroup): + waiting_anonymous_choice = State() + waiting_anonymous_name = State() \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..694031b --- /dev/null +++ b/utils.py @@ -0,0 +1,135 @@ +"""Reusable async helpers shared across routers.""" +import asyncio +import logging +from aiogram import Bot, types +from aiogram.types import InputMediaPhoto, InputMediaVideo +import bot_state as state + +logger = logging.getLogger(__name__) + + +def get_chat_display_name(user: types.User, anonymous_name: str | None = None) -> str: + if anonymous_name: + return anonymous_name + if user.username: + return f"{user.full_name} (@{user.username}, {user.id})" + return f"{user.full_name} ({user.id})" + + +def get_admin_display_name(user: types.User) -> str: + if user.username: + return f"Admin: {user.full_name} (@{user.username})" + return f"Admin: {user.full_name}" + + +def build_message_preview(message: types.Message) -> str: + if message.text: return message.text + if message.photo: return f"[Photo]\n{message.caption}" if message.caption else "[Photo]" + if message.video: return f"[Video]\n{message.caption}" if message.caption else "[Video]" + if message.document: return f"[Document]\n{message.caption}" if message.caption else "[Document]" + if message.voice: return "[Voice message]" + if message.audio: return f"[Audio]\n{message.caption}" if message.caption else "[Audio]" + if message.sticker: return f"[Sticker] {message.sticker.emoji or ''}".strip() + if message.animation: return f"[Animation]\n{message.caption}" if message.caption else "[Animation]" + return "[Unsupported message]" + + +def build_quote(text: str | None) -> str: + return "\n".join("> " + line for line in (text or "[No text]").splitlines()) + + +async def is_group_admin(bot: Bot, chat_id: int, user_id: int) -> bool: + try: + member = await bot.get_chat_member(chat_id, user_id) + return member.status in ("administrator", "creator") + except Exception: + logger.warning("Failed to check admin status for user %s in chat %s", user_id, chat_id) + return False + + +async def send_media_items( + bot: Bot, chat_id: int, media: list[dict], caption: str | None = None +) -> None: + if len(media) == 1: + item = media[0] + pm = "HTML" if caption else None + try: + if item["type"] == "photo": + await bot.send_photo(chat_id, item["file_id"], caption=caption, parse_mode=pm) + else: + await bot.send_video(chat_id, item["file_id"], caption=caption, parse_mode=pm) + except Exception: + logger.exception("Failed to send single media to %s", chat_id) + return + for i in range(0, len(media), 10): + chunk = media[i:i + 10] + group = [] + for j, item in enumerate(chunk): + ic = caption if i == 0 and j == 0 else None + if item["type"] == "photo": + group.append(InputMediaPhoto(media=item["file_id"], caption=ic, parse_mode="HTML" if ic else None)) + else: + group.append(InputMediaVideo(media=item["file_id"], caption=ic, parse_mode="HTML" if ic else None)) + try: + await bot.send_media_group(chat_id, media=group) + except Exception: + logger.exception("Failed to send media group to %s", chat_id) + + +def cancel_upload_prompt(user_id: int) -> None: + task = state.upload_prompt_tasks.pop(user_id, None) + if task: + task.cancel() + + +async def schedule_upload_prompt(bot: Bot, chat_id: int, user_id: int) -> None: + cancel_upload_prompt(user_id) + + async def _delayed() -> None: + try: + await asyncio.sleep(0.8) + from keyboards import confirm_kb + prev_msg_id = state.upload_prompt_msg_ids.pop(user_id, None) + if prev_msg_id: + try: + await bot.delete_message(chat_id, prev_msg_id) + except Exception: + pass + sent = await bot.send_message( + chat_id, + "Media added. Send more or confirm submission.", + reply_markup=confirm_kb, + ) + state.upload_prompt_msg_ids[user_id] = sent.message_id + except asyncio.CancelledError: + pass + except Exception: + logger.exception("Failed to send upload prompt to %s", chat_id) + finally: + if state.upload_prompt_tasks.get(user_id) is task: + state.upload_prompt_tasks.pop(user_id, None) + + task = asyncio.create_task(_delayed()) + state.upload_prompt_tasks[user_id] = task + + +async def start_chat_session( + bot: Bot, + user: types.User, + send_func, + fsm_state, + anonymous_name: str | None = None, +) -> None: + await fsm_state.clear() + cancel_upload_prompt(user.id) + state.chat_sessions[user.id] = { + "display_name": get_chat_display_name(user, anonymous_name), + "anonymous": bool(anonymous_name), + } + try: + await send_func( + "Your chat has started. Every message you send will be forwarded to the admins. " + "Send /stop to return to the menu." + ) + except Exception: + logger.exception("Failed to send chat-start message to %s", user.id) \ No newline at end of file