├── tests ├── __init__.py └── test_explicit.py ├── README.md ├── .gitignore ├── requirements.txt ├── dev_requirements.txt ├── languages.py ├── misc.py ├── config.example.py ├── LICENSE ├── engine.py ├── help.py ├── antiflood.py ├── bot.py ├── moderator.py └── explicit.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TrueModer 2 | Telegram moderator bot 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.py 2 | draft.py 3 | .pytest_cache/ 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiogram 2 | aiosocksy 3 | aiohttp 4 | aioChatbase 5 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | 3 | aresponses 4 | pytest-asyncio 5 | -------------------------------------------------------------------------------- /languages.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(f'TrueModer.{__name__}') 4 | 5 | 6 | def underscore(text: str, **kwargs): 7 | return text.format(**kwargs) 8 | 9 | # todo add multilingual support 10 | -------------------------------------------------------------------------------- /misc.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import config 3 | import sys 4 | from aiogram.types import Chat, User 5 | 6 | logger = logging.getLogger(f'TrueModer.{__name__}') 7 | 8 | 9 | def setup_logger(): 10 | logging.basicConfig(format='%(asctime)s | %(name)s:%(lineno)d | %(levelname)s | %(message)s', 11 | level=config.LOGGING_LEVEL, stream=sys.stdout) 12 | 13 | logging.getLogger('aiohttp').setLevel(logging.WARNING) 14 | logging.getLogger('chatbase').setLevel(logging.INFO) 15 | 16 | return logging.getLogger('TrueModer') 17 | 18 | 19 | def log_repr(o): 20 | """ 21 | Represents object to log view 22 | :param o: 23 | :type o: Chat or User 24 | :return: 25 | :rtype: str 26 | """ 27 | if isinstance(o, Chat): 28 | return f'{o.full_name} ({o.id})' 29 | 30 | if isinstance(o, User): 31 | return f'{o.full_name} ({o.id})' 32 | 33 | return None 34 | -------------------------------------------------------------------------------- /config.example.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | # telegram 4 | TELEGRAM_TOKEN = '4234423:AAEswagGNsfuREYdagrCPkgLvda1C_5frsZsBYrvLefdc' # DevTester 5 | 6 | # webhook settings 7 | WEBHOOK = False 8 | WEBHOOK_HOST = 'https://google.com' 9 | WEBHOOK_PATH = '/api/' 10 | WEBHOOK_URL = f'{WEBHOOK_HOST}{WEBHOOK_PATH}' 11 | 12 | # webserver settings 13 | WEBAPP_HOST = 'localhost' 14 | WEBAPP_PORT = 3111 15 | 16 | # proxy settings 17 | PROXY_URL = 'socks5://8.8.8.8:1080' # or '' to disable 18 | PROXY_LOGIN = 'proxy_login' # or '' to disable 19 | PROXY_PASSWORD = 'proxy_password' # or '' to disable 20 | 21 | # analytics 22 | CHATBASE_KEY = 'agGNsfuREYdagrCPkgLvda1C_5frsZsBYrv' 23 | CHATBASE_POOL_SIZE = 20 24 | 25 | # vars 26 | super_admins = [12345678, ] 27 | 28 | # content 29 | BOT_NAME = '@TrueModerBot' 30 | FAQ_LINK = 'http://telegra.ph/True-Moder-07-31' 31 | 32 | # db mode 33 | DB_MODE = False 34 | 35 | # logging 36 | LOGGING_LEVEL = logging.INFO 37 | -------------------------------------------------------------------------------- /tests/test_explicit.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pytest 4 | 5 | from explicit import find_explicit 6 | 7 | logging.basicConfig(level=logging.INFO) 8 | logger = logging.getLogger('TrueModerTest') 9 | pytestmark = pytest.mark.asyncio 10 | 11 | GOOD_WORDS = 'сабля', 'Употреблять', 'рубля', 'злоупотреблять', 'не психуй' 12 | BAD_WORDS = 'Хуй', 'хуйло', 'бля', 'пиздец' 13 | 14 | 15 | @pytest.fixture(params=GOOD_WORDS) 16 | def good_word(request): 17 | return request.param 18 | 19 | 20 | @pytest.fixture(params=BAD_WORDS) 21 | def bad_word(request): 22 | return request.param 23 | 24 | 25 | async def test_non_explicit(good_word): 26 | """ huy test """ 27 | txt = f'Какое-то предложение и {good_word} среди него' 28 | result = await find_explicit(txt) 29 | assert result is False 30 | 31 | 32 | async def test_explicit(bad_word): 33 | """ huy test """ 34 | txt = f'Какое-то предложение и {bad_word} среди него' 35 | result = await find_explicit(txt) 36 | assert result is True 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Oleg A. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /engine.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | import config 4 | from aiogram import Bot, Dispatcher, types 5 | from aiogram.contrib.fsm_storage.memory import MemoryStorage 6 | from aiogram.utils import context 7 | from aiochatbase import Chatbase 8 | from moderator import Moderator 9 | 10 | logger = logging.getLogger(f'TrueModer.{__name__}') 11 | loop = asyncio.get_event_loop() 12 | loop.set_task_factory(context.task_factory) 13 | 14 | 15 | def get_proxy_data(): 16 | if config.PROXY_URL and config.PROXY_LOGIN and config.PROXY_PASSWORD: 17 | proxy = config.PROXY_URL 18 | if proxy.startswith('socks5'): 19 | import aiosocksy 20 | logger.info('Socks5 proxy enabled.') 21 | proxy_auth = aiosocksy.Socks5Auth(login=config.PROXY_LOGIN, password=config.PROXY_PASSWORD) 22 | else: 23 | import aiohttp 24 | logger.info('HTTP proxy enabled.') 25 | proxy_auth = aiohttp.BasicAuth(login=config.PROXY_LOGIN, password=config.PROXY_PASSWORD) 26 | else: 27 | logger.info('Proxy disabled.') 28 | proxy = None 29 | proxy_auth = None 30 | 31 | return proxy, proxy_auth 32 | 33 | 34 | # vars and instances 35 | url, auth = get_proxy_data() 36 | bot = Bot(token=config.TELEGRAM_TOKEN, loop=loop, proxy=url, proxy_auth=auth, parse_mode=types.ParseMode.HTML) 37 | dp = Dispatcher(bot, storage=MemoryStorage(), run_tasks_by_default=True) 38 | cb = Chatbase(api_key=config.CHATBASE_KEY, loop=loop, platform='Telegram', task_mode=True, 39 | pool_size=config.CHATBASE_POOL_SIZE) 40 | moder = Moderator(bot, cb) 41 | -------------------------------------------------------------------------------- /help.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from languages import underscore as _ 3 | from engine import moder 4 | import config 5 | import logging 6 | 7 | logger = logging.getLogger(f'TrueModer.{__name__}') 8 | 9 | 10 | async def welcome(message: types.Message): 11 | """ 12 | Welcomes self and say HELP 13 | Said welcome message if enabled 14 | 15 | """ 16 | chat = message.chat 17 | new_users = message.new_chat_members 18 | 19 | # a little delay before welcome 20 | await types.ChatActions.typing(sleep=2) 21 | 22 | # say help when bot added to new chat 23 | if await moder.me in new_users: 24 | logger.info(f'TrueModer added to chat {chat.full_name} ({chat.id})') 25 | markup = types.InlineKeyboardMarkup() 26 | markup.add(types.InlineKeyboardButton(text=f'ℹ️ Описание', url=config.FAQ_LINK)) 27 | text = _(f"Привет! Я бот-модератор! \n\n" 28 | f"Чтобы я смог следить за этой группой, мне нужно дать следующие права администратора: \n" 29 | f"- удалять сообщения; \n" 30 | f"- блокировать пользователей; \n" 31 | f"- закреплять сообщения. \n\n" 32 | f"Подробности в описании:") 33 | await moder.say(chat.id, text, reply_markup=markup) 34 | 35 | 36 | async def welcome_group(message: types.Message): 37 | """ Said that bot can works only in super groups """ 38 | 39 | chat = message.chat 40 | new_users = message.new_chat_members 41 | 42 | # a little delay before welcome 43 | await types.ChatActions.typing(sleep=2) 44 | 45 | if await moder.me in new_users: 46 | logger.info(f'Bot added to group chat {chat.full_name} ({chat.id})') 47 | 48 | text = (f"Привет! Я бот-модератор! \n\n" 49 | f"Я умею работать только в супергруппах \n" 50 | f"Преобразовать эту группу в супергруппу можно в настройках группы.") 51 | await moder.say(chat.id, text) 52 | 53 | 54 | async def group_migrates_to_supergroup(message: types.Message): 55 | chat = message.chat 56 | 57 | logger.info(f'Group {message.migrate_from_chat_id} migrated to supergroup {chat.id}') 58 | 59 | markup = types.InlineKeyboardMarkup() 60 | markup.add(types.InlineKeyboardButton(text=f'ℹ️ Инструкция', url=config.FAQ_LINK)) 61 | text = _(f"Отлично! \n\n" 62 | f"Теперь для начала работы требуется дать мне следующие права администратора: \n" 63 | f"- удалять сообщения; \n" 64 | f"- блокировать пользователей \n" 65 | f"- закреплять сообщения. \n\n" 66 | f"Подробности в разделе Установка:") 67 | await moder.say(chat.id, text, reply_markup=markup) 68 | -------------------------------------------------------------------------------- /antiflood.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from aiogram import types 4 | from aiogram.dispatcher import CancelHandler, DEFAULT_RATE_LIMIT, ctx 5 | from aiogram.dispatcher.middlewares import BaseMiddleware 6 | from aiogram.utils import context 7 | from aiogram.utils.exceptions import Throttled 8 | from languages import underscore as _ 9 | 10 | 11 | FLOOD_LOCK_MESSAGE = _('Не надо флудить!') 12 | FLOOD_MUTE_TIME = 120 # seconds 13 | 14 | 15 | def rate_limit(limit: float, key=None): 16 | """ 17 | Decorator for configuring rate limit and key in different functions. 18 | 19 | :param limit: 20 | :param key: 21 | :return: 22 | """ 23 | 24 | def decorator(func): 25 | setattr(func, 'throttling_rate_limit', limit) 26 | if key: 27 | setattr(func, 'throttling_key', key) 28 | return func 29 | 30 | return decorator 31 | 32 | 33 | class ThrottlingMiddleware(BaseMiddleware): 34 | """ 35 | Simple middleware 36 | """ 37 | 38 | def __init__(self, limit=DEFAULT_RATE_LIMIT, key_prefix='antiflood_'): 39 | self.rate_limit = limit 40 | self.prefix = key_prefix 41 | super(ThrottlingMiddleware, self).__init__() 42 | 43 | async def on_process_message(self, message: types.Message): 44 | """ 45 | This handler is called when dispatcher receives a message 46 | 47 | :param message: 48 | """ 49 | # Get current handler 50 | handler = context.get_value('handler') 51 | 52 | # Get dispatcher from context 53 | dispatcher = ctx.get_dispatcher() 54 | 55 | # If handler was configured, get rate limit and key from handler 56 | if handler: 57 | limit = getattr(handler, 'throttling_rate_limit', self.rate_limit) 58 | key = getattr(handler, 'throttling_key', f"{self.prefix}_{handler.__name__}") 59 | else: 60 | limit = self.rate_limit 61 | key = f"{self.prefix}_message" 62 | 63 | # Use Dispatcher.throttle method. 64 | try: 65 | await dispatcher.throttle(key, rate=limit) 66 | except Throttled as t: 67 | # Execute action 68 | await self.message_throttled(message, t) 69 | 70 | # Cancel current handler 71 | raise CancelHandler() 72 | 73 | async def message_throttled(self, message: types.Message, throttled: Throttled): 74 | """ 75 | Notify user only on first exceed and notify about unlocking only on last exceed 76 | 77 | :param message: 78 | :param throttled: 79 | """ 80 | from engine import moder 81 | 82 | chat = message.chat 83 | user = message.from_user 84 | handler = context.get_value('handler') 85 | dispatcher = ctx.get_dispatcher() 86 | 87 | if handler: 88 | key = getattr(handler, 'throttling_key', f"{self.prefix}_{handler.__name__}") 89 | else: 90 | key = f"{self.prefix}_message" 91 | 92 | # Calculate how many time is left till the block ends 93 | delta = throttled.rate - throttled.delta 94 | 95 | # Prevent flooding 96 | if throttled.exceeded_count <= 2: 97 | await moder.restrict_user(chat.id, user.id, FLOOD_MUTE_TIME) 98 | await message.reply(FLOOD_LOCK_MESSAGE) 99 | 100 | # Sleep. 101 | await asyncio.sleep(delta) 102 | 103 | # Check lock status 104 | thr = await dispatcher.check_key(key) 105 | 106 | # # If current message is not last with current key - do not send message 107 | # if thr.exceeded_count == throttled.exceeded_count: 108 | # await message.reply('Unlocked.') 109 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from aiogram import types 4 | from aiogram.utils.executor import start_polling, start_webhook 5 | 6 | import config 7 | import help 8 | from antiflood import ThrottlingMiddleware 9 | from engine import dp, moder, bot, cb 10 | from languages import underscore as _ 11 | from misc import setup_logger 12 | 13 | logger = setup_logger() 14 | loop = asyncio.get_event_loop() 15 | 16 | 17 | @dp.errors_handler() 18 | async def errors_handler(dispatcher, update, exception): 19 | """ 20 | Exceptions handler. Catches all exceptions within task factory tasks. 21 | 22 | :param dispatcher: 23 | :param update: 24 | :param exception: 25 | :return: stdout logging 26 | """ 27 | from aiogram.utils.exceptions import Unauthorized, InvalidQueryID, TelegramAPIError, \ 28 | CantDemoteChatCreator, MessageNotModified, MessageToDeleteNotFound 29 | 30 | if isinstance(exception, CantDemoteChatCreator): 31 | logger.debug("Can't demote chat creator") 32 | return 33 | 34 | if isinstance(exception, MessageNotModified): 35 | logger.debug('Message is not modified') 36 | return 37 | 38 | if isinstance(exception, MessageToDeleteNotFound): 39 | logger.debug('Message to delete not found') 40 | return 41 | 42 | if isinstance(exception, Unauthorized): 43 | logger.info(f'Unauthorized: {exception}') 44 | return 45 | 46 | if isinstance(exception, InvalidQueryID): 47 | logger.exception(f'InvalidQueryID: {exception} \nUpdate: {update}') 48 | return 49 | 50 | if isinstance(exception, TelegramAPIError): 51 | logger.exception(f'TelegramAPIError: {exception} \nUpdate: {update}') 52 | return 53 | 54 | logger.exception(f'Update: {update} \n{exception}') 55 | 56 | 57 | @dp.message_handler(types.ChatType.is_private, commands=['start', 'help']) 58 | async def start_private(message: types.Message): 59 | """ 60 | Handle start and help commands in private chat 61 | 62 | :param message: 63 | :return: 64 | """ 65 | text = _('Привет, я бот-модератор! \n' 66 | 'Добавь меня в чат, чтобы навести там порядок') 67 | 68 | markup = types.InlineKeyboardMarkup() 69 | add_group = f'https://telegram.me/{config.BOT_NAME[1:]}?startgroup=true' 70 | markup.add(types.InlineKeyboardButton(text=f'Добавить модератора в чат', url=add_group)) 71 | await message.reply(text, reply_markup=markup, reply=False) 72 | 73 | 74 | TYPES_TO_DELETE = types.ContentType.STICKER + types.ContentType.VIDEO_NOTE + types.ContentType.VIDEO + \ 75 | types.ContentType.DOCUMENT + types.ContentType.CONTACT + types.ContentType.PHOTO + \ 76 | types.ContentType.GAME + types.ContentType.ANIMATION 77 | 78 | 79 | @dp.message_handler(types.ChatType.is_super_group, content_types=TYPES_TO_DELETE) 80 | async def delete_media(message: types.Message): 81 | user = message.from_user 82 | chat = message.chat 83 | 84 | if user.id in config.super_admins: 85 | return 86 | 87 | await message.delete() 88 | logger.info(f'Deleted message from {user.full_name} ({user.id}) in {chat.full_name} ({chat.id})') 89 | 90 | 91 | async def register_handlers(): 92 | """ 93 | Function-container with registering external (non-decorated) handlers 94 | Don't forget about order of registered handlers! It really matters! 95 | :return: None 96 | """ 97 | # bot join chat handlers 98 | dp.register_message_handler(help.welcome, custom_filters=[types.ChatType.is_super_group], 99 | content_types=types.ContentType.NEW_CHAT_MEMBERS) 100 | 101 | dp.register_message_handler(help.welcome_group, custom_filters=[types.ChatType.is_group], 102 | content_types=types.ContentType.NEW_CHAT_MEMBERS) 103 | 104 | dp.register_message_handler(help.group_migrates_to_supergroup, 105 | content_types=types.ContentType.MIGRATE_FROM_CHAT_ID) 106 | 107 | # moderator commands 108 | # dp.register_message_handler(moder.ban, regexp=r'^!.*бан.*', content_types=types.ContentType.TEXT) 109 | # dp.register_message_handler(moder.mute, regexp=r'^!.*мол[ч,к].*', content_types=types.ContentType.TEXT) 110 | 111 | # text filter 112 | dp.register_message_handler(moder.check_text, custom_filters=[types.ChatType.is_super_group], 113 | content_types=types.ContentType.TEXT) 114 | 115 | 116 | async def on_startup(dispatcher): 117 | """ 118 | Auto exec function on startup of your app 119 | 120 | :param dispatcher: dispatcher 121 | :return: None 122 | """ 123 | await register_handlers() 124 | dispatcher.middleware.setup(ThrottlingMiddleware(limit=0)) 125 | 126 | if config.WEBHOOK: 127 | await bot.set_webhook(config.WEBHOOK_URL) 128 | 129 | 130 | async def on_shutdown(_): 131 | """ 132 | Auto exec function on shutdown of your app 133 | 134 | :param _: dispatcher 135 | :return: None 136 | """ 137 | await bot.close() 138 | await cb.close() 139 | await asyncio.sleep(0.250) 140 | 141 | 142 | if __name__ == '__main__': 143 | if config.WEBHOOK: 144 | start_webhook(dp, webhook_path=config.WEBHOOK_PATH, loop=loop, skip_updates=True, on_startup=on_startup, 145 | on_shutdown=on_shutdown, host=config.WEBAPP_HOST, port=config.WEBAPP_PORT) 146 | else: 147 | start_polling(dp, loop=loop, skip_updates=True, on_startup=on_startup, on_shutdown=on_shutdown) 148 | -------------------------------------------------------------------------------- /moderator.py: -------------------------------------------------------------------------------- 1 | import asyncio as aio 2 | import logging 3 | import random 4 | import re 5 | from datetime import datetime, timedelta 6 | 7 | from aiogram import types, Bot 8 | from aiogram.utils import markdown as md 9 | from aiogram.utils.exceptions import * 10 | 11 | from aiochatbase import Chatbase 12 | from antiflood import rate_limit 13 | from languages import underscore as _ 14 | from misc import log_repr 15 | 16 | logger = logging.getLogger(f'TrueModer.{__name__}') 17 | 18 | TEXT = 'text' 19 | ANSWER = 'answer' 20 | TIME = 'time' 21 | 22 | jail = {} 23 | 24 | 25 | class Moderator: 26 | def __init__(self, bot, cb): 27 | self._bot: Bot = bot 28 | self.cb: Chatbase = cb 29 | 30 | @property 31 | async def me(self): 32 | return await self._bot.me 33 | 34 | async def say(self, chat_id, text, reply_markup=None, disable_web_page_preview=None): 35 | """ 36 | Overrides bot.send_message and catches exceptions 37 | 38 | :param chat_id: 39 | :param text: 40 | :param reply_markup: 41 | :param disable_web_page_preview: 42 | :return: message 43 | :rtype: Message or None 44 | """ 45 | try: 46 | msg = await self._bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup, 47 | disable_web_page_preview=disable_web_page_preview) 48 | 49 | except BadRequest: 50 | pass 51 | 52 | except Unauthorized: 53 | pass 54 | 55 | else: 56 | return msg 57 | 58 | @staticmethod 59 | async def check_admin(user, chat): 60 | """ 61 | Check user is admin of chat 62 | 63 | :param user: administrator's user object 64 | :type user: types.User 65 | 66 | :param chat: chat object 67 | :type chat: types.Chat 68 | 69 | :return: True if user is admin of chat, else False 70 | :rtype: bool 71 | """ 72 | from config import super_admins 73 | 74 | if not isinstance(user, types.User): 75 | logger.error("There's no User to check rights") 76 | return False 77 | 78 | if user.id in super_admins: 79 | return True 80 | 81 | if not isinstance(chat, types.Chat): 82 | logger.error("There's no Chat to check rights") 83 | return False 84 | 85 | member = await chat.get_member(user.id) 86 | if not isinstance(member, types.ChatMember): 87 | return False 88 | 89 | if member.is_admin(): 90 | return True 91 | 92 | return False 93 | 94 | @staticmethod 95 | async def get_time(message): 96 | """ 97 | Parse time from message 98 | 99 | :param message: 100 | :type message: types.Message 101 | :return: dict with keys: 'time' and 'text' 102 | :rtype: dict 103 | """ 104 | from datetime import timedelta 105 | 106 | result = {} 107 | time = re.search(r'(\d+)', message.text) # в сообщении есть числа 108 | time = time.group() if time else None 109 | 110 | minutes = re.search(r'^мин|мин[^ ]+', message.text) 111 | hours = re.search(r'^час|час[^ ]+', message.text) 112 | days = re.search(r'дн[^ ]|день|сутки|суток', message.text) 113 | weeks = re.search(r'недел', message.text) 114 | 115 | if not time: 116 | if re.search(r'пару', message.text): 117 | time = 2 118 | elif re.search(r'несколько', message.text): 119 | time = random.randint(3, 9) 120 | else: 121 | time = 1 122 | 123 | half = re.search(r'\s?пол.*', message.text) 124 | time = int(time) / 2 if half else int(time) 125 | 126 | if time and minutes: 127 | result[TEXT] = f'{str(time)} {minutes.group()}' 128 | elif time and hours: 129 | result[TEXT] = f'{str(time)} {hours.group()}' 130 | elif time and days: 131 | result[TEXT] = f'{str(time)} {days.group()}' 132 | elif time and weeks: 133 | result[TEXT] = f'{str(time)} {weeks.group()}' 134 | else: 135 | result[TEXT] = f'{str(time)} час.' 136 | 137 | if minutes: 138 | result[TIME] = timedelta(minutes=float(time)) 139 | elif hours: 140 | result[TIME] = timedelta(hours=float(time)) 141 | elif days: 142 | result[TIME] = timedelta(days=float(time)) 143 | elif weeks: 144 | result[TIME] = timedelta(weeks=float(time)) 145 | else: 146 | result[TIME] = timedelta(hours=float(time)) 147 | 148 | return result 149 | 150 | @staticmethod 151 | async def check_delete(message): 152 | """ 153 | Parse delete command from message 154 | 155 | :param message: 156 | :type message: types.Message 157 | :return: True if delete command 158 | :rtype: bool 159 | """ 160 | delete = re.search(r'[ ]-|-[ ]', message.text) 161 | return True if delete else False 162 | 163 | async def kick(self, chat_id, user_id, seconds): 164 | until = int((datetime.now() + timedelta(seconds=seconds)).timestamp()) 165 | 166 | try: 167 | await self._bot.kick_chat_member(chat_id, user_id, until) 168 | 169 | except BadRequest as error: 170 | 171 | if 'not enough rights' in str(error): 172 | logger.debug('Не хватает прав на совершение действия') 173 | text = _('Я бы с удовольствием произвёл блокировку, но мне не хватает администраторских прав') 174 | await self.say(chat_id, text) 175 | 176 | elif 'an administrator of the chat' in str(error): 177 | logger.debug(f'Зачем-то пытается ограничить админа :)') 178 | text = _('Я не могу заблокировать админа') 179 | await self.say(chat_id, text) 180 | 181 | else: 182 | logger.exception(f'BadRequest: {error}', exc_info=True) 183 | text = _('Не шмогла :(') 184 | await self.say(chat_id, text) 185 | 186 | async def ban(self, message): 187 | """ 188 | Executing ban 189 | 190 | :param message: 191 | :type message: types.Message 192 | :return: None 193 | """ 194 | if not isinstance(message, types.Message): 195 | logger.error("There's no Message with ban request ") 196 | return 197 | 198 | admin = message.from_user 199 | chat = message.chat 200 | 201 | logger.info(f'moderator.ban received from {log_repr(admin)} in {log_repr(chat)}') 202 | 203 | # check admin rights 204 | if not await self.check_admin(admin, chat): 205 | await message.delete() 206 | await self.restrict_user(chat_id=chat.id, user_id=admin.id, seconds=30 * 60) 207 | return 208 | 209 | # check reply to forward 210 | if not message.reply_to_message: 211 | await message.reply(f'Эту команду нужно использовать в ответ на чьё-то сообщение') 212 | return 213 | 214 | abuser = message.reply_to_message.from_user 215 | if chat and abuser: 216 | how_long = await self.get_time(message) 217 | ban_before = int((datetime.now() + how_long.get(TIME)).timestamp()) 218 | need_delete = await self.check_delete(message) 219 | 220 | try: 221 | await self._bot.kick_chat_member(chat.id, abuser.id, ban_before) 222 | 223 | except BadRequest as error: 224 | 225 | if 'not enough rights' in str(error): 226 | logger.debug('Не хватает прав на совершение действия') 227 | text = _('Я бы с удовольствием произвёл блокировку, но мне не хватает администраторских прав') 228 | await self.say(chat.id, text) 229 | 230 | elif 'an administrator of the chat' in str(error): 231 | logger.debug(f'Зачем-то пытается ограничить админа :)') 232 | text = _('Я не могу заблокировать админа') 233 | await self.say(chat.id, text) 234 | 235 | else: 236 | logger.exception(f'BadRequest: {error}', exc_info=True) 237 | text = _('Я не могу заблокировать админа') 238 | await self.say(chat.id, text) 239 | 240 | else: 241 | await self._bot.send_message(chat.id, 'Готово! :)') 242 | logger.info(f"{admin.full_name} ({admin.id}) " 243 | f"ban {abuser.full_name} ({abuser.id}) " 244 | f"in {chat.full_name} ({chat.id}) for {how_long.get(TEXT)}") 245 | 246 | if need_delete: 247 | await self._bot.delete_message(chat.id, message.reply_to_message.message_id) 248 | 249 | else: 250 | logger.info(f"{admin.first_name} ({admin.id}) " 251 | f"хотел кого-то забанить, но не получилось :(") 252 | 253 | async def mute(self, message): 254 | """ 255 | Executing mute command 256 | 257 | :param message: 258 | :type message: types.Message 259 | :return: None 260 | """ 261 | if not isinstance(message, types.Message): 262 | logger.error("There's no Message with mute request ") 263 | return 264 | 265 | admin = message.from_user 266 | chat = message.chat 267 | logger.info(f'moderator.mute received from {log_repr(admin)} in {log_repr(chat)}') 268 | 269 | # check admin rights 270 | if not await self.check_admin(admin, chat): 271 | await message.delete() 272 | await self.restrict_user(chat.id, admin.id, seconds=61) 273 | return 274 | 275 | # check reply to forward 276 | if not message.reply_to_message: 277 | return await message.reply(f'Эту команду нужно использовать в ответ на чьё-то сообщение') 278 | 279 | abuser = message.reply_to_message.from_user 280 | if chat and abuser: 281 | how_long = await self.get_time(message) 282 | restrict_before = int((datetime.now() + how_long.get(TIME)).timestamp()) 283 | need_delete = await self.check_delete(message) 284 | 285 | try: 286 | await self._bot.restrict_chat_member(chat_id=chat.id, 287 | user_id=abuser.id, 288 | until_date=restrict_before, 289 | can_send_messages=False) 290 | 291 | except BadRequest as error: 292 | if 'not enough rights' in str(error): 293 | logger.debug(f'Не хватает прав на совершение действия: {error}') 294 | 295 | elif 'an administrator of the chat' in str(error): 296 | logger.debug(f'Зачем-то пытается ограничить админа. {error}') 297 | 298 | else: 299 | logger.exception(f'BadRequest: {error}', exc_info=True) 300 | 301 | else: 302 | await self._bot.send_message(chat.id, 'Готово! :)') 303 | logger.info(f"{admin.full_name} ({admin.id}) " 304 | f"mute {abuser.full_name} ({abuser.id}) " 305 | f"in {chat.title} ({chat.id}) at {how_long.get(TEXT)}") 306 | 307 | if need_delete: 308 | await self._bot.delete_message(chat.id, message.reply_to_message.message_id) 309 | 310 | else: 311 | logger.info(f"{admin.first_name} ({admin.id}) " 312 | f"хотел кого-то заткнуть, но не получилось :(") 313 | 314 | async def restrict_user(self, chat_id, user_id, seconds=61): 315 | """ 316 | Restriction method with try 317 | 318 | :param chat_id: 319 | :param user_id: 320 | :type user_id: int 321 | :param seconds: int 322 | :return: 323 | """ 324 | until = int((datetime.now() + timedelta(seconds=seconds)).timestamp()) 325 | 326 | try: 327 | await self._bot.restrict_chat_member(chat_id, user_id, 328 | can_send_messages=False, 329 | can_send_other_messages=False, 330 | can_add_web_page_previews=False, 331 | can_send_media_messages=False, 332 | until_date=until) 333 | 334 | except BadRequest as e: 335 | if "Can't demote chat creator" in str(e) or "can't demote chat creator" in str(e): 336 | logger.debug(f"Restriction: can't demote chat creator at {chat_id}") 337 | text = _('Не могу я создателя блочить!') 338 | await self.say(chat_id, text) 339 | 340 | elif "is an administrator of the chat" in str(e): 341 | logger.debug(f"Restriction: can't demote chat admin at {chat_id}") 342 | text = _('Не могу я админа блочить!') 343 | await self.say(chat_id, text) 344 | 345 | elif "Not enough rights to restrict/unrestrict chat member" in str(e): 346 | logger.warning(f"Not enough rights to restrict/unrestrict chat member at {chat_id}") 347 | text = _('Я бы с удовольствием произвёл блокировку, но мне не хватает администраторских прав') 348 | await self.say(chat_id, text) 349 | 350 | else: 351 | logger.exception(f'Error: \n{e}', exc_info=True) 352 | text = _('Не шмогла :(') 353 | await self.say(chat_id, text) 354 | 355 | except RetryAfter: 356 | logging.error(f'Message limit reached! {RetryAfter}') 357 | 358 | except Unauthorized as e: 359 | logger.exception(f'Error: \n{e}', exc_info=True) 360 | 361 | except TelegramAPIError as e: 362 | logger.error(f'Error: \n{e}') 363 | 364 | else: 365 | return True 366 | 367 | @staticmethod 368 | async def delete_message(message: types.Message): 369 | chat = message.chat 370 | 371 | try: 372 | await message.delete() 373 | 374 | except MessageError as e: 375 | logger.info(f"Can't delete message in {chat.full_name} ({chat.id}), cause: {e}") 376 | 377 | except TelegramAPIError as e: 378 | logger.error(f'TelegramAPIError: {e}') 379 | 380 | else: 381 | return True 382 | 383 | @rate_limit(0.5, 'text') 384 | async def check_text(self, message: types.Message): 385 | logger.debug(f'Checking received text: {message.text}') 386 | await self.check_explicit(message) 387 | await self.check_link(message) 388 | 389 | @rate_limit(0.5, 'text') 390 | async def check_explicit(self, message: types.Message): 391 | from explicit import find_explicit 392 | 393 | text = message.text 394 | chat = message.chat 395 | user = message.from_user 396 | 397 | # message without text skip 398 | if not text: 399 | return 400 | 401 | # is explicit found? 402 | result = await find_explicit(text) 403 | if not result: 404 | await self.cb.register_message(user_id=user.id, intent='normal message') 405 | return 406 | logger.info(f'Found explicit in message: {text}') 407 | await self.cb.register_message(user_id=user.id, intent='explicit message') 408 | 409 | # let's delete bad message 410 | await self.delete_message(message) 411 | 412 | # notify user 413 | try: 414 | jail[user.id] += 1 415 | except KeyError: 416 | jail[user.id] = 1 417 | 418 | user_link = md.hlink(user.full_name, f'tg://user?id={user.id}') 419 | 420 | if jail.get(user.id) <= 2: 421 | text = _('Ай-ай-ай, {user_link}!', user_link=user_link) 422 | await self.say(chat.id, text) 423 | return 424 | 425 | if 2 < jail.get(user.id) < 5: 426 | text = _('{user_link}, я же тебя предупреждал... Иди молчать.', user_link=user_link) 427 | await self.say(chat.id, text) 428 | await aio.sleep(1) 429 | await self.restrict_user(chat.id, user.id, 5 * 60 * jail.get(user.id)) 430 | return 431 | 432 | if jail.get(user.id) >= 5: 433 | text = _('{user_link}, я же тебя предупреждал... Иди в бан.', user_link=user_link) 434 | await self.say(chat.id, text) 435 | await aio.sleep(1) 436 | await self.kick(chat.id, user.id, 24 * 60 * 60) 437 | jail[user.id] = 3 438 | return 439 | 440 | @rate_limit(0.5, 'link') 441 | async def check_link(self, message: types.Message): 442 | """ Find links and @group mentions """ 443 | 444 | entities = message.entities 445 | text = message.text 446 | chat = message.chat 447 | user = message.from_user 448 | bot = message.bot 449 | 450 | for entity in entities: 451 | logger.debug(f'Checking entity with {entity.type}') 452 | if entity.type == types.MessageEntityType.URL: 453 | logger.info('Url found. Deleting. Restricting.') 454 | await message.delete() 455 | await self.restrict_user(chat_id=chat.id, user_id=user.id, seconds=65) 456 | return 457 | 458 | if entity.type == types.MessageEntityType.MENTION: 459 | name = entity.get_text(text) 460 | logger.debug(f'Received mention: {name}. Checking...') 461 | 462 | try: 463 | mentioned_chat = await bot.get_chat(name) 464 | 465 | except Unauthorized as e: 466 | logger.info('@-mention of group found. Deleting. Restricting.') 467 | await message.delete() 468 | await self.restrict_user(chat_id=chat.id, user_id=user.id, seconds=65) 469 | return 470 | 471 | except ChatNotFound: 472 | logger.debug('@-mention is user. Nothing to do.') 473 | 474 | else: 475 | logger.info('@-mention of group found. Deleting. Restricting.') 476 | if types.ChatType.is_group_or_super_group(mentioned_chat): 477 | await message.delete() 478 | await self.restrict_user(chat_id=chat.id, user_id=user.id, seconds=65) 479 | return 480 | -------------------------------------------------------------------------------- /explicit.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | 4 | logger = logging.getLogger(f'TrueModer.{__name__}') 5 | 6 | my_patterns = ( 7 | r'^(\w*бля\w*)$', 8 | r'([нпз]?[?аео]?[хк]у[йеё]\w*)', 9 | r'\w*п[иi]зд\w*' 10 | 11 | ) 12 | 13 | external_patterns = ( 14 | # основной фильтр 15 | # '(?:\w*(?:[хxh](?:[уyu][иiu])|[пnp][еиeiu](?:[з3z][дd]|[дd](?:[еаоeoa0][рpr]|[рpr]))|[бb6][лl]я[дd]|[оo0][хxh][уyu]' 16 | # '[еe]|[мm][уyu][дd][еоиаeioau0]|[дd][еe][рpr][ьb]|[гrg][аоoa0][вbv][нhn]|[уyu][еёe][бb6])|[хxh][\W_]*(?:[уyu][\W_]*' 17 | # '[йиеёяeiju])|[пnp][\W_]*[еиeiu][\W_]*(?:[з3z][\W_]*[дd]|[дd][\W_]*(?:[еаоeoa0][\W_]*[рpr]|[рpr]))|[бb6][\W_]*[лl]' 18 | # '[\W_]*я[\W_]*[дd]|[оo0][\W_]*[хxh][\W_]*[уyu][\W_]*[еe]|[мm][\W_]*[уyu][\W_]*[дd][\W_]*[еоиаeioau0]|[дd][\W_]*[еe]' 19 | # '[\W_]*[рpr][\W_]*[ьb]|[гrg][\W_]*[аоoa0][\W_]*[вbv][\W_]*[нhn]|[уyu][\W_]*[еёe][\W_]*[бb6]|[ёеe][бb6])\w+', 20 | 21 | # жоп* 22 | 'ж[\W_]*(?:[оo0][\W_]*[пnp][\W_]*(?:[аa]|[oо0](?:[\W_]*[хxh])?|[уеыeyiuё]|[оo0][\W_]*[йj])\w*)', 23 | 24 | # дерьмо 25 | '[дd][\W_]*[еe][\W_]*[рpr][\W_]*(?:[ьb][\W_]*)?[мm][\W_]*[оуеаeoya0u](?:[\W_]*[мm])?', 26 | 27 | # чмо, чмырь 28 | '[чc][\W_]*[мm][\W_]*(?:[оo0]|[ыi][\W_]*[рpr][\W_]*[еиьяeibu])', 29 | 30 | # сука, сучка 31 | '[сsc][\W_]*[уuy][\W_]*(?:(?:[чc][\W_]*)?[кk][\W_]*[ауиiyau](?:[\W_]*[нhn](?:[\W_]*[оo0][\W_]*[йj]|[\W_]*[уаыyiau])' 32 | '?)?|[чc][\W_]*(?:(?:[ьb][\W_]*)?(?:[еёяиeiu]|[еиeiu][\W_]*[йj])|[аa][\W_]*[рpr][\W_]*[ыауеeyiau]))', 33 | 34 | # гондон 35 | '[гrg][\W_]*(?:[аоoa0][\W_]*(?:[нhn][\W_]*[дd][\W_]*[аоoa0][\W_]*[нhn](?:[\W_]*[ыуyiu])?|[вbv][\W_]*[нhn][\W_]*' 36 | '[оаoa0](?:[\W_]*(?:[мm]|[еe][\W_]*[дd](?:[\W_]*[ыуаеeyiau]|[\W_]*[оаoa0][\W_]*[мm](?:[\W_]*[иiu])?)?))?)|[нhn]' 37 | '[\W_]*(?:[иiu][\W_]*[дd][\W_]*(?:[ыуеаeyiau]|[оo0][\W_]*[йj])|[уyu][сsc](?:[\W_]*[аыуyiau]|[\W_]*[оаoa0][\W_]*' 38 | '[мm](?:[\W_]*[иiu])?)?))', 39 | 40 | # *еб* 41 | '(?:[нhn][\W_]*[еe][\W_]*)?(?:(?:[з3z][\W_]*[аa]|[оo0][тt]|[пnp][\W_]*[оo0]|[пnp][\W_]*(?:[еe][\W_]*[рpr][\W_]*[еe]|[рpr][\W_]*[оеиeiou0]|[иiu][\W_]*[з3z][\W_]*[дd][\W_]*[оo0])|[нhn][\W_]*[аa]|[иiu][\W_]*[з3z]|[дd][\W_]*[оo0]|[вbv][\W_]*[ыi]|[уyu]|[рpr][\W_]*[аa][\W_]*[з3z]|[з3z][\W_]*[лl][\W_]*[оo0]|[тt][\W_]*[рpr][\W_]*[оo0]|[уyu])[\W_]*)?(?:[вbv][\W_]*[ыi][\W_]*)?(?:[ъьb][\W_]*)?(?:[еёe][\W_]*[бb6](?:(?:[\W_]*[оеёаиуeioyau0])?(?:[\W_]*[нhn](?:[\W_]*[нhn])?[\W_]*[яуаиьiybau]?)?(?:[\W_]*[вbv][\W_]*[аa])?(?:(?:[\W_]*(?:[иеeiu]ш[\W_]*[ьb][\W_]*[сsc][\W_]*я|[тt][\W_]*(?:(?:[ьb][\W_]*)?[сsc][\W_]*я|[ьb]|[еe][\W_]*[сsc][\W_]*[ьb]|[еe]|[оo0]|[иiu][\W_]*[нhn][\W_]*[уыеаeyiau])|(?:щ[\W_]*(?:[иiu][\W_]*[йj]|[аa][\W_]*я|[иеeiu][\W_]*[еe]|[еe][\W_]*[гrg][\W_]*[оo0])|ю[\W_]*[тt])(?:[\W_]*[сsc][\W_]*я)?|[еe][\W_]*[мтmt]|[кk](?:[\W_]*[иаiau])?|[аa][\W_]*[лl](?:[\W_]*[сsc][\W_]*я)?|[лl][\W_]*(?:[аa][\W_]*[нhn]|[оаoa0](?:[\W_]*[мm])?|(?:[иiu][\W_]*)?[сsc][\W_]*[ьяb]|[иiu]|[аa][\W_]*[сsc][\W_]*[ьb])|[рpr][\W_]*[ьb]|[сsc][\W_]*[яьb]|[нhn][\W_]*[оo0]|[чc][\W_]*(?:[иiu][\W_]*[хxh]|[еe][\W_]*[сsc][\W_]*[тt][\W_]*[ьиibu](?:[\W_]*ю)?)|(?:[тt][\W_]*[еe][\W_]*[лl][\W_]*[ьb][\W_]*[сsc][\W_]*[кk][\W_]*|[сsc][\W_]*[тt][\W_]*|[лl][\W_]*[иiu][\W_]*[вbv][\W_]*|[чтtc][\W_]*)?(?:[аa][\W_]*я|[оo0][\W_]*[йемejm]|[ыi][\W_]*[хйеejxh]|[ыi][\W_]*[мm](?:[\W_]*[иiu])?|[уyu][\W_]*ю|[иiu][\W_]*[еe]|[оo0][\W_]*[мm][\W_]*[уyu]|[иiu][\W_]*[йj]|[еe][\W_]*[вbv]|[иiu][\W_]*[мm](?:[\W_]*[иiu])?)|[чтыйилijltcu]))?)|[\W_]*[ыi](?:(?:[\W_]*[вbv][\W_]*[аa]|[\W_]*[нhn](?:[\W_]*[нhn])?)(?:(?:[\W_]*(?:[иеeiu]ш[\W_]*[ьb][\W_]*[сsc][\W_]*я|[тt][\W_]*(?:[ьb][\W_]*[сsc][\W_]*я|[ьb]|[еe][\W_]*[сsc][\W_]*[ьb]|[еe]|[иiu][\W_]*[нhn][\W_]*[уыеаeyiau])|(?:щ[\W_]*(?:[иiu][\W_]*[йj]|[аa][\W_]*я|[иеeiu][\W_]*[еe]|[еe][\W_]*[гrg][\W_]*[оo0])|ю[\W_]*[тt])(?:[\W_]*[сsc][\W_]*я)?|[еe][\W_]*[мтmt]|[лl][\W_]*(?:(?:[иiu][\W_]*)?[сsc][\W_]*[ьяb]|[иiu]|[аa][\W_]*[сsc][\W_]*[ьb])|(?:[сsc][\W_]*[тt][\W_]*|[лl][\W_]*[иiu][\W_]*[вbv][\W_]*|[чтtc][\W_]*)?(?:[аa][\W_]*я|[оo0][\W_]*[йемejm]|[ыi][\W_]*[хйеejxh]|[ыi][\W_]*[мm](?:[\W_]*[иiu])?|[уyu][\W_]*ю|[иiu][\W_]*[еe]|[оo0][\W_]*[мm][\W_]*[уyu]|[иiu][\W_]*[йj]|[еe][\W_]*[вbv]|[иiu][\W_]*[мm](?:[\W_]*[иiu])?))))|[рpr][\W_]*[ьb]))|я[\W_]*[бb6](?:[\W_]*[оеёаиуeioyau0])?(?:(?:[\W_]*[нhn](?:[\W_]*[нhn])?[\W_]*[яуаиьiybau]?)?(?:(?:[\W_]*(?:[иеeiu]ш[\W_]*[ьb][\W_]*[сsc][\W_]*я|[тt][\W_]*(?:[ьb][\W_]*[сsc][\W_]*я|[ьb]|[еe][\W_]*[сsc][\W_]*[ьb]|[еe]|[иiu][\W_]*[нhn][\W_]*[уыеаeyiau])|(?:щ[\W_]*(?:[иiu][\W_]*[йj]|[аa][\W_]*я|[иеeiu][\W_]*[еe]|[еe][\W_]*[гrg][\W_]*[оo0])|ю[\W_]*[тt])(?:[\W_]*[сsc][\W_]*я)?|[еe][\W_]*[мтmt]|[кk](?:[\W_]*[иаiau])?|[аa][\W_]*[лl](?:[\W_]*[сsc][\W_]*я)?|[лl][\W_]*(?:[аa][\W_]*[нhn]|[оаoa0](?:[\W_]*[мm])?|(?:[иiu][\W_]*)?[сsc][\W_]*[ьяb]|[иiu])|[рpr][\W_]*[ьb]|[сsc][\W_]*[яьb]|[нhn][\W_]*[оo0]|[чc][\W_]*(?:[иiu][\W_]*[хxh]|[еe][\W_]*[сsc][\W_]*[тt][\W_]*[ьиibu](?:[\W_]*ю)?)|(?:[тt][\W_]*[еe][\W_]*[лl][\W_]*[ьb][\W_]*[сsc][\W_]*[кk][\W_]*|[сsc][\W_]*[тt][\W_]*|[лl][\W_]*[иiu][\W_]*[вbv][\W_]*|[чтtc][\W_]*)?(?:[аa][\W_]*я|[оo0][\W_]*[йемejm]|[ыi][\W_]*[хйеejxh]|[ыi][\W_]*[мm](?:[\W_]*[иiu])?|[уyu][\W_]*ю|[иiu][\W_]*[еe]|[оo0][\W_]*[мm][\W_]*[уyu]|[иiu][\W_]*[йj]|[еe][\W_]*[вbv]|[иiu][\W_]*[мm](?:[\W_]*[иiu])?)|[чмйилijlmcu]))|(?:[\W_]*[нhn](?:[\W_]*[нhn])?[\W_]*[яуаиьiybau]?)))|я[\W_]*[бb6][\W_]*(?:[еёаиуeiyau][\W_]*)?(?:[нhn][\W_]*(?:[нhn][\W_]*)?(?:[яуаиьiybau][\W_]*)?)?[тt])' 42 | 43 | # *выебан* 44 | '\w*[вbv][\W_]*([ыиiu]+[\W_]*[еёe]+[\W_]*[бb6][\W_]*[аоoa0][\W_]*[нhn])\w*', 45 | 46 | # (с,об)ьеб* 47 | '([сsc]|[оo0][\W_]*[бb6])[\W_]*[ьъb][\W_]*[еяёe][\W_]*[бb6][\W_]*(?:([уyu]|[оo0][\W_]*[сз3szc])|(?:[еиёауeiyau](?:[\W_]*[лl](?:[\W_]*[иоаioau0])?|[\W_]*ш[\W_]*[ьb]|[\W_]*[тt][\W_]*[еe])?(?:[\W_]*[сsc][\W_]*[ьяb])?))', 48 | 49 | # еб* 50 | '[еe][\W_]*(?:[бb6][\W_]*(?:[уyu][\W_]*[кk][\W_]*[еe][\W_]*[нhn][\W_]*[тt][\W_]*[иiu][\W_]*[йj]|[еe][\W_]*[нhn][\W_]*(?:[ьb]|я(?:[\W_]*[мm])?)|[иiu][\W_]*(?:[цc][\W_]*[кk][\W_]*[аa][\W_]*я|[чc][\W_]*[еe][\W_]*[сsc][\W_]*[кk][\W_]*[аa][\W_]*я)|[лl][\W_]*[иiu][\W_]*щ[\W_]*[еe]|[аa][\W_]*(?:[лl][\W_]*[ьb][\W_]*[нhn][\W_]*[иiu][\W_]*[кk](?:[\W_]*[иаiau])?|[тt][\W_]*[оo0][\W_]*[рpr][\W_]*[иiu][\W_]*[йj]|[нhn][\W_]*(?:[тt][\W_]*[рpr][\W_]*[оo0][\W_]*[пnp]|[аa][\W_]*[тt][\W_]*[иiu][\W_]*(?:[кk]|[чc][\W_]*[еe][\W_]*[сsc][\W_]*[кk][\W_]*[иiu][\W_]*[йj]))))|[дd][\W_]*[рpr][\W_]*[иiu][\W_]*[тt])', 51 | 52 | # невротъебательск* 53 | '[нhn][\W_]*[еe][\W_]*[вbv][\W_]*[рpr][\W_]*[оo0][\W_]*[тt][\W_]*ъ[\W_]*[еe][\W_]*[бb6][\W_]*[аa][\W_]*[тt][\W_]*[еe][\W_]*[лl][\W_]*[ьb][\W_]*[сsc][\W_]*[кk][\W_]*[иiu][\W_]*(?:[ыиiu][\W_]*[йj]|[аa][\W_]*я|[оo0][\W_]*[ейej]|[ыi][\W_]*[хxh]|[ыi][\W_]*[еe]|[ыi][\W_]*[мm](?:[\W_]*[иiu])?|[уyu][\W_]*ю|[оo0][\W_]*[мm][\W_]*[уyu])', 54 | 55 | # уёбищ* 56 | '[уyu][\W_]*(?:[ёеe][\W_]*[бb6][\W_]*(?:[иiu][\W_]*щ[\W_]*[еаea]|[аa][\W_]*[нhn](?:[\W_]*[тt][\W_]*[уyu][\W_]*[сsc])?(?:[\W_]*[аоoa0][\W_]*[вмbmv]|[\W_]*[ыуеаeyiau])?)|[рpr][\W_]*[оo0][\W_]*[дd](?:[\W_]*[аоoa0][\W_]*[вмbmv]|[\W_]*[ыуеаeyiau])?|[бb6][\W_]*[лl][\W_]*ю[\W_]*[дd][\W_]*(?:[оo0][\W_]*[кk]|[кk][\W_]*(?:[аоoa0][\W_]*[вмbmv](?:[\W_]*[иiu])?|[иуеаeiyau])?))', 57 | 58 | # муд* 59 | '[мm][\W_]*(?:[уyu][\W_]*[дd][\W_]*(?:[оo0][\W_]*[хxh][\W_]*[аa][\W_]*(?:[тt][\W_]*[ьb][\W_]*[сsc][\W_]*я|ю[\W_]*[сsc][\W_]*[ьb]|[еe][\W_]*ш[\W_]*[ьb][\W_]*[сsc][\W_]*я)|[аa][\W_]*(?:[кk](?:[\W_]*[иаiau]|[оo0][мвbmv])?|[чc][\W_]*(?:[ьb][\W_]*[еёe]|[иiu][\W_]*[нhn][\W_]*[уыаyiau]|[кk][\W_]*(?:[аиеуeiyau]|[оo0][\W_]*[йj])))|[еe][\W_]*[нhn][\W_]*[ьb]|[иiu][\W_]*[лl](?:[\W_]*[аеоыeoia0]?))|[аa][\W_]*[нhn][\W_]*[дd][\W_]*[уаyau]|[лl][\W_]*(?:[иiu][\W_]*[нhn]|я))', 60 | 61 | # (мозг/долб/скот/...)(хуй|еб)|ство 62 | '(?:[мm][\W_]*(?:[оo0][\W_]*[з3z][\W_]*[гrg]|[уyu][\W_]*[дd])|[дd][\W_]*(?:[оo0][\W_]*[лl][\W_]*[бb6]|[уyu][\W_]*[рpr])|[сsc][\W_]*[кk][\W_]*[оo0][\W_]*[тt])[\W_]*[аоoa0][\W_]*(?:[хxh][\W_]*[уyu][\W_]*[ийяiju]|[ёеe][\W_]*[бb6](?:[\W_]*[еоeo0][\W_]*[вbv]|[\W_]*[ыаia]|[\W_]*[сsc][\W_]*[тt][\W_]*[вbv][\W_]*[оуoy0u](?:[\W_]*[мm])?|[иiu][\W_]*[з3z][\W_]*[мm])?)', 63 | 64 | # *пизд/еб* 65 | '(?:[нhn][\W_]*[еe][\W_]*|[з3z][\W_]*[аa][\W_]*|[оo0][\W_]*[тt][\W_]*|[пnp][\W_]*[оo0][\W_]*|[нhn][\W_]*[аa][\W_]*|[рpr][\W_]*[аa][\W_]*[сз3szc][\W_]*)?(?:[пnp][\W_]*[иiu][\W_]*[з3z][\W_]*[дd][\W_]*[ияеeiu]|(?:ъ)?[еёe][\W_]*[бb6][\W_]*[аa])[\W_]*(?:(?:([тt][\W_]*[ьb]|[лl])[\W_]*[сsc][\W_]*я|[тt][\W_]*[ьb]|[лl][\W_]*[иiu]|[аa][\W_]*[лl]|[лl]|c[\W_]*[ьb]|[иiu][\W_]*[тt]|[иiu]|[тt][\W_]*[еe]|[чc][\W_]*[уyu]|ш[\W_]*[ьb])|(?:[йяиiju]|[иеeiu][\W_]*[мm](?:[\W_]*[иiu])?|[йj][\W_]*[сsc][\W_]*(?:[кk][\W_]*(?:[ыиiu][\W_]*[йеej]|[аa][\W_]*я|[оo0][\W_]*[еe]|[ыi][\W_]*[хxh]|[ыi][\W_]*[мm](?:[\W_]*[иiu])?|[уyu][\W_]*ю|[оo0][\W_]*[мm][\W_]*[уyu])|[тt][\W_]*[вbv][\W_]*[оуаoya0u](?:[\W_]*[мm])?)))', 66 | 67 | # пидор* 68 | '[пnp][\W_]*[еиыeiu][\W_]*[дd][\W_]*[аеэоeoa0][\W_]*[рpr](?:(?:[\W_]*[аa][\W_]*[сз3szc](?:(?:[\W_]*[тt])?(?:[\W_]*[ыi]|[\W_]*[оаoa0][\W_]*[мm](?:[\W_]*[иiu])?|[\W_]*[кk][\W_]*[аиiau])?|(?:[\W_]*[ыуаеeyiau]|[\W_]*[оаoa0][\W_]*[мm](?:[\W_]*[иiu])?|[\W_]*[оo0][\W_]*[вbv])))|[\W_]*(?:[ыуаеeyiau]|[оаoa0][\W_]*[мm](?:[\W_]*[иiu])?|[оo0][\W_]*[вbv]|[нhn][\W_]*я))?', 69 | 70 | # пизд* 71 | '[пnp][\W_]*[иiu][\W_]*[з3z][\W_]*(?:[ьb][\W_]*)?[дd][\W_]*(?:[ёеe][\W_]*(?:[нhn][\W_]*[ыi][\W_]*ш(?:[\W_]*[ьb])?|' 72 | '[шнжhn](?:[\W_]*[ьb])?)|[уyu][\W_]*(?:[йj](?:[\W_]*[тt][\W_]*[еe])?|[нhn](?:[\W_]*[ыi])?)|ю[\W_]*(?:[кk](?:[\W_]*' 73 | '(?:[аеуиeiyau]|[оo0][\W_]*[вbv]|[аa][\W_]*[мm](?:[\W_]*[иiu])?))?|[лl](?:[ьиibu]|[еe][\W_]*[йj]|я[\W_]*[хмmxh]))|' 74 | '[еe][\W_]*[цc]|[аоoa0][\W_]*(?:[нhn][\W_]*[уyu][\W_]*)?[тt][\W_]*(?:[иiu][\W_]*[йj]|[аa][\W_]*я|[оo0](?:[\W_]*' 75 | '[ейej])?|[ыi][\W_]*[ейхejxh]|[ыi][\W_]*[мm](?:[\W_]*[иiu])?|[уyu][\W_]*ю|[оo0][\W_]*[мm][\W_]*[уyu]|[еe][\W_]*' 76 | '[еe]|[ауьеыeyibau])|[аa][\W_]*[нhn][\W_]*[уyu][\W_]*[лl](?:[\W_]*[аиiau])?|[ыеуиаeiyau]|[оаoa0][\W_]*(?:[йj]|' 77 | '[хxh][\W_]*[уyu][\W_]*[йj]|[еёe][\W_]*[бb6]|(?:[рpr][\W_]*[оo0][\W_]*[тt]|[гrg][\W_]*[оo0][\W_]*[лl][\W_]*[оo0]' 78 | '[\W_]*[вbv])[\W_]*(?:[ыиiu][\W_]*[йj]|[аa][\W_]*я|[оo0][\W_]*[ейej]|[ыi][\W_]*[хxh]|[ыi][\W_]*[еe]|[ыi][\W_]*[мm]' 79 | '(?:[\W_]*[иiu])?|[уyu][\W_]*ю|[оo0][\W_]*[мm][\W_]*[уyu])|[бb6][\W_]*(?:[рpr][\W_]*[аa][\W_]*[тt][\W_]*[иiu][\W_]' 80 | '*я|[оo0][\W_]*[лl](?:[\W_]*[аыуyiau])?)))', 81 | 82 | # падл* 83 | '[пnp][\W_]*(?:[аa][\W_]*[дd][\W_]*[лl][\W_]*[аоыoia0]|[оаoa0][\W_]*[сsc][\W_]*[кk][\W_]*[уyu][\W_]*[дd][\W_]*(?:[ыуаеeyiau]|[оаoa0][\W_]*[мm](?:[\W_]*[иiu])?)|[иеeiu][\W_]*[дd][\W_]*(?:[иiu][\W_]*[кk]|[рpr][\W_]*[иiu][\W_]*[лl](?:[\W_]*[лl])?)(?:[\W_]*[оаoa0][\W_]*[мвbmv]|[\W_]*[иуеоыаeioyau0])?|[рpr][\W_]*[оo0][\W_]*[бb6][\W_]*[лl][\W_]*я[\W_]*[дd][\W_]*[оo0][\W_]*[мm])', 84 | 85 | # *срать* 86 | '(?:[з3z][\W_]*[аa][\W_]*|[оo0][\W_]*[тt][\W_]*|[нhn][\W_]*[аa][\W_]*)?[сsc][\W_]*[рpr][\W_]*(?:[аa][\W_]*[тt][\W_]*[ьb]|[аa][\W_]*[лl](?:[\W_]*[иiu])?|[eуиiyu])', 87 | 88 | # срак* 89 | '[сsc][\W_]*[рpr][\W_]*[аa][\W_]*(?:[кk][\W_]*(?:[аеиуeiyau]|[оo0][\W_]*[йj])|[нhn](?:[\W_]*[нhn])?(?:[ьb]|(?:[\W_]*[ыi][\W_]*[йеej]|[\W_]*[аa][\W_]*я|[\W_]*[оo0][\W_]*[еe]))|[лl][\W_]*[ьb][\W_]*[нhn][\W_]*[иiu][\W_]*[кk](?:[\W_]*[иiu]|[\W_]*[оаoa0][\W_]*[мm])?)', 90 | 91 | # *трах* 92 | '(?:[з3z][\W_]*[аa][\W_]*)?[тt][\W_]*[рpr][\W_]*[аa][\W_]*[хxh][\W_]*(?:[нhn][\W_]*(?:[уyu](?:[\W_]*[тt][\W_]*[ьb](?:[\W_]*[сsc][\W_]*я)?|[\W_]*[сsc][\W_]*[ьb]|[\W_]*[лl](?:[\W_]*[аиiau])?)?|[еиeiu][\W_]*ш[\W_]*[ьb][\W_]*[сsc][\W_]*я)|[аa][\W_]*(?:[лl](?:[\W_]*[аоиioau0])?|[тt][\W_]*[ьb](?:[\W_]*[сsc][\W_]*я)?|[нhn][\W_]*(?:[нhn][\W_]*)?(?:[ыиiu][\W_]*[йj]|[аa][\W_]*я|[оo0][\W_]*[йеej]|[ыi][\W_]*[хxh]|[ыi][\W_]*[еe]|[ыi][\W_]*[мm](?:[\W_]*[иiu])?|[уyu][\W_]*ю|[оo0][\W_]*[мm][\W_]*[уyu])))', 93 | 94 | # *хуй* 95 | '(?:[нhn][\W_]*[иеeiu][\W_]*|[пnp][\W_]*[оo0][\W_]*|[нhn][\W_]*[аa][\W_]*|[оаoa0][\W_]*(?:[тt][\W_]*)?|[дd][\W_]*[аоoa0][\W_]*|[з3z][\W_]*[аa][\W_]*)?(?:(?:[хxh][\W_]*(?:[еиeiu][\W_]*(?:[йj][\W_]*)?[рpr]|[уyu](?:[\W_]*[йj])?))(?:[\W_]*[еоёeo0][\W_]*[вbv](?:[\W_]*[аa][\W_]*ю[\W_]*щ|[\W_]*ш)?)?(?:[\W_]*[аиеeiau][\W_]*[лнlhn])?(?:[нhn])?(?:[\W_]*(?:[иаоёяыеeioau0][юяиевмйbeijmvu]|я[\W_]*(?:[мm](?:[\W_]*[иiu])?|[рpr][\W_]*(?:ю|[иiu][\W_]*(?:[тt](?:[\W_]*[ьеeb][\W_]*[сsc][\W_]*[яьb])?|[лl](?:[\W_]*[иоаioau0])?))|[чc][\W_]*(?:[аиiau][\W_]*[тt](?:[\W_]*[сsc][\W_]*я)|[иiu][\W_]*[лl](?:[\W_]*[иоаioau0])?)|[чc](?:[\W_]*[ьb])?)|[еe][\W_]*(?:[тt][\W_]*(?:[оo0][\W_]*[йj]|[аьуybau])|[еe][\W_]*(?:[тt][\W_]*[еe]|ш[\W_]*[ьb]))|[аыоуяюйиijoyau0]|[лl][\W_]*[иоiou0]|[чc][\W_]*[уyu])))', 96 | 97 | # хуй 98 | '(?:[хxh][\W_]*(?:[еиeiu][\W_]*(?:[йj][\W_]*)?[рpr]|[уyu][\W_]*[йj]))', 99 | 100 | # хуеc* 101 | '[хxh][\W_]*[уyu][\W_]*(?:[еёиeiu][\W_]*(?:[сsc][\W_]*[оo0][\W_]*[сsc]|[пnp][\W_]*[лl][\W_]*[еe][\W_]*[тt]|[нhn][\W_]*[ыi][\W_]*ш)(?:[\W_]*[аыуyiau]|[\W_]*[оаoa0][\W_]*[мm](?:[\W_]*[иiu])?|[нhn][\W_]*(?:[ыиiu][\W_]*[йj]|[аa][\W_]*я|[оo0][\W_]*[йеej]|[ыi][\W_]*[хxh]|[ыi][\W_]*[еe]|[ыi][\W_]*[мm](?:[\W_]*[иiu])?|[уyu][\W_]*ю|[оo0][\W_]*[мm][\W_]*[уyu]))?|[дd][\W_]*[оo0][\W_]*ё[\W_]*[бb6][\W_]*[иiu][\W_]*[нhn][\W_]*(?:[оo0][\W_]*[йj]|[аеыуeyiau]))', 102 | 103 | # бляд* 104 | '(\w*[бb6][\W_]*[лl][\W_]*(я|[еe][аa])(?:[\W_]*[дтdt][ьъ]?[\W_]*(?:[ьb]|[иiu]|[кk][\W_]*[иiu]|[сsc][\W_]*[тt][\W_]*[вbv][\W_]*[оo0]|[сsc][\W_]*[кk][\W_]*(?:[оo0][\W_]*[ейej]|[иiu][\W_]*[еe]|[аa][\W_]*я|[иiu][\W_]*[йj]|[оo0][\W_]*[гrg][\W_]*[оo0])))?)', 105 | 106 | # выбляд* 107 | '[вbv][\W_]*[ыi][\W_]*[бb6][\W_]*[лl][\W_]*я[\W_]*[дd][\W_]*(?:[оo0][\W_]*[кk]|[кk][\W_]*(?:[иуаеeiyau]|[аa][\W_]*[мm](?:[\W_]*[иiu])?))', 108 | 109 | # западл* 110 | '(?:[з3z][\W_]*[аоoa0][\W_]*)(?:[пnp][\W_]*[аоoa0][\W_]*[дd][\W_]*[лl][\W_]*[оыаoia0]|[лl][\W_]*[уyu][\W_]*[пnp][\W_]*(?:[оo0][\W_]*[йj]|[аеыуeyiau]))', 111 | 112 | # шлюх* 113 | 'ш[\W_]*[лl][\W_]*ю[\W_]*[хxh][\W_]*(?:[ауеиeiyau]|[оo0][\W_]*[йj])', 114 | 115 | # анус* 116 | '[аa][\W_]*[нhn][\W_]*[уyu][сsc](?:[\W_]*[еаыуeyiau]|[\W_]*[оo0][\W_]*[мm])?', 117 | ) 118 | 119 | explicit_list = ( 120 | "нехуй", "мудило", 121 | "6ля", "6лядь", "6лять", "b3ъeб", "cock", "cunt", "e6aль", "ebal", "eblan", "eбaл", "eбaть", "eбyч", "eбать", 122 | "eбёт", "eблантий", "fuck", "fucker", "fucking", "xyёв", "xyй", "xyя", "xуе", "xуй", "xую", "zaeb", "zaebal", 123 | "zaebali", "zaebat", "архипиздрит", "ахуел", "ахуеть", "бздение", "бздеть", "бздех", "бздецы", "бздит", 124 | "бздицы", 125 | "бздло", "бзднуть", "бздун", "бздунья", "бздюха", "бздюшка", "бздюшко", r"бля", "блябу", "блябуду", "бляд", 126 | "бляди", 127 | "блядина", "блядище", "блядки", "блядовать", "блядство", "блядун", "блядуны", "блядунья", "блядь", "блядюга", 128 | "блять", "бляtь", "вафел", "вафлёр", "взъебка", "взьебка", "взьебывать", "въеб", "въебался", "въебенн", "въебусь", 129 | "въебывать", "выблядок", "выблядыш", "выеб", "выебать", "выебен", "выебнулся", "выебон", "выебываться", 130 | "выпердеть", 131 | "высраться", "выссаться", "вьебен", "гавно", "гавнюк", "гавнючка", "гамно", "гандон", "гнид", "гнида", "гниды", 132 | "говенка", "говенный", "говешка", "говназия", "говнецо", "говнище", "говно", "говноед", "говнолинк", 133 | "говночист", 134 | "говнюк", "говнюха", "говнядина", "говняк", "говняный", "говнять", "гондон", "доебываться", "долбоеб", 135 | "долбоёб", 136 | "долбоящер", "дрисня", "дрист", "дристануть", "дристать", "дристун", "дристуха", "дрочелло", "дрочена", 137 | "дрочила", 138 | "дрочилка", "дрочистый", "дрочить", "дрочка", "дрочун", "е6ал", "е6ут", "еб твою мать", "ёб твою мать", "ёбaн", 139 | "ебaть", "ебyч", "ебал", "ебало", "ебальник", "ебан", "ебанамать", "ебанат", "ебаная", "ёбаная", "ебанический", 140 | "ебанный", "ебанныйврот", "ебаное", "ебануть", "ебануться", "ёбаную", "ебаный", "ебанько", "ебарь", "ебат", 141 | "ёбат", 142 | "ебатория", "ебать", "ебать-копать", "ебаться", "ебашить", "ебёна", "ебет", "ебёт", "ебец", "ебик", "ебин", 143 | "ебись", 144 | "ебическая", "ебки", "ебла", "еблан", "ебливый", "еблище", "ебло", "еблыст", "ебля", "ёбн", "ебнуть", 145 | "ебнуться", 146 | "ебня", "ебошить", "ебская", "ебский", "ебтвоюмать", "ебун", "ебут", "ебуч", "ебуче", "ебучее", "ебучий", 147 | "ебучим", 148 | "ебущ", "ебырь", "елда", "елдак", "елдачить", "жопа", "жопу", "заговнять", "задрачивать", "задристать", 149 | "задрота", 150 | "зае6", "заё6", "заеб", "заёб", "заеба", "заебал", "заебанец", "заебастая", "заебастый", "заебать", "заебаться", 151 | "заебашить", "заебистое", "заёбистое", "заебистые", "заёбистые", "заебистый", "заёбистый", "заебись", 152 | "заебошить", 153 | "заебываться", "залуп", "залупа", "залупаться", "залупить", "залупиться", "замудохаться", "запиздячить", 154 | "засерать", 155 | "засерун", "засеря", "засирать", "засрун", "захуячить", "заябестая", "злоеб", "злоебучая", "злоебучее", 156 | "злоебучий", 157 | "ибанамат", "ибонех", "изговнять", "изговняться", "изъебнуться", "ипать", "ипаться", "ипаццо", 158 | "Какдвапальцаобоссать", "конча", "курва", "курвятник", "лох", "лошарa", "лошара", "лошары", "лошок", "лярва", 159 | "малафья", "манда", "мандавошек", "мандавошка", "мандавошки", "мандей", "мандень", "мандеть", "мандища", 160 | "мандой", 161 | "манду", "мандюк", "минет", "минетчик", "минетчица", "млять", "мокрощелка", "мокрощёлка", "мразь", "мудak", 162 | "мудaк", 163 | "мудаг", "мудак", "муде", "мудель", "мудеть", "муди", "мудил", "мудила", "мудистый", "мудня", "мудоеб", 164 | "мудозвон", 165 | "мудоклюй", "на хер", "на хуй", "набздел", "набздеть", "наговнять", "надристать", "надрочить", "наебать", 166 | "наебет", 167 | "наебнуть", "наебнуться", "наебывать", "напиздел", "напиздели", "напиздело", "напиздили", "насрать", 168 | "настопиздить", 169 | "нахер", "нахрен", "нахуй", "haxуй", "нахуйник", "не ебет", "не ебёт", "невротебучий", "невъебенно", "нехира", "нехрен", 170 | "Нехуй", "нехуйственно", "ниибацо", "ниипацца", "ниипаццо", "ниипет", "никуя", "нихера", "нихуя", 171 | "обдристаться", 172 | "обосранец", "обосрать", "обосцать", "обосцаться", "обсирать", "объебос", "обьебать", "обьебос", "однохуйственно", 173 | "опездал", "опизде", "опизденивающе", "остоебенить", "остопиздеть", "отмудохать", "отпиздить", "отпиздячить", 174 | "отпороть", "отъебись", "охуевательский", "охуевать", "охуевающий", "охуел", "охуенно", "охуеньчик", "охуеть", 175 | "охуительно", "охуительный", "охуяньчик", "охуячивать", "охуячить", "очкун", "падла", "падонки", "падонок", 176 | "паскуда", "педерас", "педик", "педрик", "педрила", "педрилло", "педрило", "педрилы", "пездень", "пездит", 177 | "пездишь", "пездо", "пездят", "пердануть", "пердеж", "пердение", "пердеть", "пердильник", "перднуть", 178 | "пёрднуть", 179 | "пердун", "пердунец", "пердунина", "пердунья", "пердуха", "пердь", "переёбок", "пернуть", "пёрнуть", "пи3д", 180 | "пи3де", "пи3ду", "пиzдец", "пидар", "пидарaс", "пидарас", "пидарасы", "пидары", "пидор", "пидорасы", "пидорка", 181 | "пидорок", "пидоры", "пидрас", "пизда", "пиздануть", "пиздануться", "пиздарваньчик", "пиздато", "пиздатое", 182 | "пиздатый", "пизденка", "пизденыш", "пиздёныш", "пиздеть", "пиздец", "пиздит", "пиздить", "пиздиться", 183 | "пиздишь", "пидр", 184 | "пиздища", "пиздище", "пиздобол", "пиздоболы", "пиздобратия", "пиздоватая", "пиздоватый", "пиздолиз", 185 | "пиздонутые", 186 | "пиздорванец", "пиздорванка", "пиздострадатель", "пизду", "пиздуй", "пиздун", "пиздунья", "пизды", "пиздюга", 187 | "пиздюк", "пиздюлина", "пиздюля", "пиздят", "пиздячить", "писбшки", "писька", "писькострадатель", "писюн", 188 | "писюшка", "по хуй", "по хую", "подговнять", "подонки", "подонок", "подъебнуть", "подъебнуться", "поебать", 189 | "поебень", "поёбываает", "поскуда", "посрать", "потаскуха", "потаскушка", "похер", "похерил", "похерила", 190 | "похерили", "похеру", "похрен", "похрену", "похуй", "похуист", "похуистка", "похую", "придурок", "приебаться", 191 | "припиздень", "припизднутый", "припиздюлина", "пробзделся", "проблядь", "проеб", "проебанка", "проебать", 192 | "промандеть", "промудеть", "пропизделся", "пропиздеть", "пропиздячить", "раздолбай", "разхуячить", "разъеб", 193 | "разъеба", "разъебай", "разъебать", "распиздай", "распиздеться", "распиздяй", "распиздяйство", "распроеть", 194 | "сволота", "сволочь", "сговнять", "секель", "серун", "серька", "сестроеб", "сикель", "сирать", 195 | "сирывать", 196 | "соси", "спиздел", "спиздеть", "спиздил", "спиздила", "спиздили", "спиздит", "спиздить", "срака", "сраку", 197 | "сраный", 198 | "сранье", "срать", "срун", "ссака", "ссышь", "стерва", "страхопиздище", "сука", "суки", "суходрочка", "сучара", 199 | "сучий", "сучка", "сучко", "сучонок", "сучье", "сцание", "сцать", "сцука", "сцуки", "сцуконах", "сцуль", 200 | "сцыха", 201 | "сцышь", "съебаться", "сыкун", "трахае6", "трахаеб", "трахаёб", "трахатель", "ублюдок", "уебать", "уёбища", 202 | "уебище", "уёбище", "уебищное", "уёбищное", "уебк", "уебки", "уёбки", "уебок", "уёбок", "урюк", "усраться", 203 | "ушлепок", "х_у_я_р_а", "хyё", "хyй", "хyйня", "хамло", "хер", "херня", "херовато", "херовина", "херовый", 204 | "хитровыебанный", "хитрожопый", "хуeм", "хуе", "хуё", "хуевато", "хуёвенький", "хуевина", "хуево", "хуевый", 205 | "хуёвый", "хуек", "хуёк", "хуел", "хуем", "хуенч", "хуеныш", "хуенький", "хуеплет", "хуеплёт", 206 | "хуепромышленник", 207 | "хуерик", "хуерыло", "хуесос", "хуесоска", "хуета", "хуетень", "хуею", "хуи", "хуй", "хуйком", "хуйло", "хуйня", 208 | "хуйрик", "хуище", "хуля", "хую", "хуюл", "хуя", "хуяк", "хуякать", "хуякнуть", "хуяра", "хуясе", "хуячить", 209 | "целка", "чмо", "чмошник", "чмырь", "шалава", "шалавой", "шараёбиться", "шлюха", "шлюхой", "шлюшка", "ябывает", 210 | 'пiзда', 211 | ) 212 | 213 | exclude_list = ( 214 | 'сабля', 'употреблять', 'рубля', 'злоупотреблять', 'психуй', 215 | ) 216 | 217 | 218 | async def find_explicit(text: str): 219 | patterns = my_patterns 220 | 221 | for pattern in patterns: 222 | to_find = re.compile(pattern, flags=re.IGNORECASE) 223 | result = re.match(to_find, text) 224 | 225 | if result: 226 | word = result.group() 227 | 228 | if word.lower() in exclude_list: 229 | logger.info(f'{word} in exclude list') 230 | continue 231 | 232 | logger.info(f'{word} - {pattern}') 233 | return True 234 | 235 | for word in text.lower().split(): 236 | if word in exclude_list: 237 | logger.info(f'{word} in exclude list') 238 | continue 239 | 240 | if word in explicit_list: 241 | logger.info(f'{word}') 242 | return True 243 | 244 | return False 245 | --------------------------------------------------------------------------------