├── 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 |
--------------------------------------------------------------------------------