From 8b053a7adb8f873b95b59c3f32b7fdb6da82b65a94fae99258335821f4ca021a Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 13 May 2026 23:38:18 +0200 Subject: [PATCH] Initial commit --- arch/bot_old.py | 132 + arch/sh-bot.rar | Bin 0 -> 38255 bytes bot_state.py | 19 + config.py | 20 + docs/requirements.md | 5 + docs/roadmap.md | 90 + docs/usage.md | 3 + filters.py | 94 + hashing.py | 71 + keyboards.py | 47 + main.py | 51 + middlewares.py | 75 + persistence.py | 51 + routers/__init__.py | 1 + routers/admin.py | 190 ++ routers/group.py | 186 ++ routers/private.py | 240 ++ rsrcs/blacklist.txt | 5220 ++++++++++++++++++++++++++++++++++++++++ rsrcs/requirements.txt | 1 + states.py | 11 + utils.py | 135 ++ 21 files changed, 6642 insertions(+) create mode 100644 arch/bot_old.py create mode 100644 arch/sh-bot.rar create mode 100644 bot_state.py create mode 100644 config.py create mode 100644 docs/requirements.md create mode 100644 docs/roadmap.md create mode 100644 docs/usage.md create mode 100644 filters.py create mode 100644 hashing.py create mode 100644 keyboards.py create mode 100644 main.py create mode 100644 middlewares.py create mode 100644 persistence.py create mode 100644 routers/__init__.py create mode 100644 routers/admin.py create mode 100644 routers/group.py create mode 100644 routers/private.py create mode 100644 rsrcs/blacklist.txt create mode 100644 rsrcs/requirements.txt create mode 100644 states.py create mode 100644 utils.py 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 0000000000000000000000000000000000000000000000000000000000000000..ed37b0204d154ebab94f1c9c6f59f880ddbb1c0e179e472f05c753f7ceabaf10 GIT binary patch literal 38255 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≻vuz5$ebN z^?>g>bwO@JkQodnmgl&m{-Z9D9sv88eV+X6E-mtQm4RquEN?i(bN4d^8_yjuc(9t* zg1}?4?bgH3j3Kfi;HLHoY35f~c}blZ>b?`JtJ6s@PHUNFe39 z1=F&uoET}lh}0lv_WEaYl!hSSV!Cn~ztY#1JDI$ zq7FEqXQJDHK|b?t-kY|&p*#XTjG@1hg$h{DSnYm?727FnsHc8Jvzpooed8F)AyO>s zPqGXrbF0&BWz*R>gD8D6f>^#JhjQ@Q(hR>{`TRnXSNTzQN!yu4?Gwxn;Wp;v`RhAh@mN#G7BDU7 zaZNla(Xn_8CwMJ$o#V+?YD`XtssY6TX<_g6^+W9JgD|ZbckH4L?ZTa^w!0c>CFP?c z4(?C-Hg_MO2r8p%hkHOWu8M>|-!XNq?d?X}K_3$4XvY zN`lvKE6hcH@BuklnFz8>Fj0jLstGlI1(J% ztd`hBJu5mktc|@ z{f5gt`k+mqPiwkru&Wss9{>xxtxo^0fEQNYZ-0gGZ+inxYjNraUG_Wv$ZaIKA(Enn z*t_a36X*)dCHB87n!gl&55^tDUzX_DF!n}Ny-H#A(2~{!vYBV~hfmu)yz~+Mbb}Et z%D%9UW=L|#dP7e7K{xPQ3#zcGM1t8QnLr#(pg0A+(lHjU8+0@0B6v8C&3H;Txo0Tj z+oN+f+no-Zy!+E34sV$}da4kcvk$>(km$Htaf%*4u^Hc99iQw$jaRq1*0mp34cGtd zus4SV3+KLgNgPKsp=ENzW#R91rkLBqTe)=+i*?Jt*+1Mk;lU*%Ou`PKWFM6_kPIwT z+ML?b?=9I6ulGI@u*WaGF5MA7^$}3}2=gZ1eyFZmVcK>DDk-r|2l;>;+(I36AnByQcJ=kYv>&<61m@o$P6*j8^{+zllx5k=}r3BXymNaqP&O&AYF zt9Yc5aHkh9=uceOW!;6-+^5`h$uRl(8`rq@EkCCCD&TkFRD~j7XkM0xGov6+7o=}l zd{bnoF=;UISGZ4Pm`!Mfj>XPPGODg^eiY+AFk7mQi^aYc3Can5o;l$WPOo^FAgJ%eA z$alDhkv>%29gU`=o6w3*{uQH;VWgo6;b;?d5y{3$Qre&I@pB3?;`GZPEnQJ$I-uX` z>8JF>3!(Y4C!CFhCzH@$F(kR6RmpZ4S^zxhsSSu|tae8vvYon#CPNb5gi#Z3?QuS39 zW7x<;k!-Q~d;T|yZuTV!h|_Et=xqaOgj!a+9B8;~TF({z^b%5O0A;`kH;+xQ#70cb z>3q)$+GoUwbz^(wA*(h$+ct246kd!?Ece=~8DT3b2r8-(8M{fMF3Sz!Ma-8p_&nWV zy~>Ibv@$}@arYcQaQCj#yKa2q5>&*O>_w8Nw2@tGCRe_)Yz=f4aM^;&9v_e`Lh3!s z_T6AK;;MTlL03lJF%9)MUv62HVemgBYMLmcy2}`=8omqH9vJVgm9?`o80z<1*|hHd zdI#xO$+pU&?d?2KS90f6qT?!Cg^1Bmq}tF-abHNNX{ei-23*A+(=p3Otwl>PH=d2+ zQQfsN)xE}i%bqQat$%#SW%PjCML?n`>4(87T5K*a*X;G=jLYWUNnP!m{D|@4g6q51 z{aY#WJD5|2uf%DV1P`b+J~FynTQhrl4Ac2#Ay$7;?(wdmQwt#as@f_;HgZ5>duV$8 za&orrEvL>T#+!^DCb_I{P!FhjEq3?o#-VbIMwy3Ee9Q)Xh1^z_!F1H7$#!lxg@k*~ zUfbncKmO#5*xb$Nnls!Q6zQXvJMznMygrq@dFK*>2Km5EekHJxVUK4Hw0Q3Xr}hkZ zSry`0e5^Pv2q`}&9mt5mIuZS zdgAtOV_F%lzn=KuIdL;NNeUHZCe5MNFNPm7>1Yr(Xh2k1<~S Q{}Q38KkB~5#Kr>r2lK)lR{#J2 literal 0 HcmV?d00001 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