Как я боролся со спамом в Telegram и что из этого вышло

В последнее время в Telegram стало много спама, и, вероятно, все, кто управляет относительно большими каналами, сталкивались с этой проблемой. Строго говоря, спам заметен уже при 500 подписчиках, но когда их число достигает 5000+, это становится просто невыносимым. Я долгое время игнорировал эту проблему и был активно против традиционных методов борьбы со спамом, таких как капчи, подтверждения и прочие предварительные фильтры. С моей точки зрения, лучше пропустить спам, чем потерять или обидеть реального пользователя. Это работало несколько лет, и все, что приходилось делать, это изредка банить. Но вот когда спама стало слишком много для ручного отстрела, я решил что-то предпринять.

Первые шаги

Очевидно, что конечный продукт должен быть ботом, который будет как-то понимать, что есть спам, и предпринимать меры. Сразу возникла идея (ее подсказал bobuk) использовать простую эвристику и считать тех, кто постит много эмодзи, спамерами. Этот шаблон в то время был вполне подходящим, так как спамеры часто использовали ненормальное количество эмодзи в своих сообщениях. Так что я написал простого бота, который считал эмодзи в сообщении и банил тех, кто их использовал слишком много.

Конечно, этот подход лучше, чем ничего, но он далеко не идеален и, совершенно точно, принесет только временное облегчение. Но сама идея использования простых правил была неплоха, так что я решил продолжить в том же духе. Мы заметили, что спамеры часто призывают “Зайти в личку” или “Подписаться на канал”, так что я организовал список запрещенных слов и фраз и банил тех, кто их использовал.

Попытка добавить чужие правила

Следующий шаг был добавить поддержку стороннего сервиса, который собирает спамеров и предоставляет их список. Я нашел CAS, который при помощи простого HTTP запроса отвечает на вопрос “Этот пользователь спамер?”. Добавить поддержку этого сервиса было делом нескольких минут, и я был уверен, что это улучшит ситуацию. Но, к сожалению, это не принесло никаких видимых результатов. Моя цель - забанить спамера, как только он появляется, но в CAS они, видимо, появляются слишком поздно. Скорее всего, они собирают информацию о спамере после того, как он уже успел нагадить. В результате, я решил, что CAS стоит оставить, но не особо на него полагаться. Забегая вперед, скажу, что после того, как рабочая версия бота была запущена, проверка CAS отлавливала не более 10% спамеров, и не было ни одного случая, когда только проверка CAS сработала, и никакая другая эвристика или классификатор не смогли бы определить спамера.

UPD: сегодня (8 марта 2024) это произошло, первый раз CAS проверка выловила спам который упустили другие правила.

Попытка добавить немного AI

Идея скормить текст в OpenAI (когда я это начинал, был в основном доступен GPT-3.5) и попросить определить, насколько оно похоже на спам, показалась многообещающей. Технически в этом нет ничего сложного - придумать prompt и отправить его в API вместе с текстом сообщения. С горем пополам удалось заставить его отвечать структурированно, но результаты были не очень. Во-первых, это стоило денег, небольших, но тем не менее. Во-вторых, точность была не очень, процент ложных срабатываний был недопустимо высок. В-третьих, это было медленно. Я отказался от этой идеи, хотя вернулся к ней позже, когда GPT-4 стал доступен. У него результаты были намного лучше, почти идеальные. Если бы не зависимость от стороннего сервиса и его цена, я бы просто использовал его и не заморачивался бы с прочими методами.

Подход с обнаружением сообщений похожих на ранее виденный спам

Эта идея мне показалась перспективной. Скормить несколько сотен примеров спама (для начала), токенизировать его, попутно очищая от шума и коротких/общих слов, и посчитать cosine similarity каждого примера с сообщением, которое мы хотим проверить. Если сходство больше, чем некий порог, то сообщение считается спамом. Это было бы быстро, дешево и, как мне казалось, эффективно. И опять, результат был не особо обнадеживающим. Да, отловленные им похожие на спам сообщения практически всегда были спамом, но когда начали появляться новые спамеры, которые писали совсем (или даже немного) по-другому, этот метод перестал работать. В результате, от этого метода я не отказался, но он стал одним из многих, и далеко не самым эффективным.

То, с чего надо было начать - классификатор с дообучением

Мысль, что для подобной классификации spam/ham отлично подойдет Naive Bayes, пришла мне не сразу. Но когда она пришла, я понял, что это то, с чего надо было начать. Алгоритм простой и относительно быстрый, скармливаем примеры спама и не-спама и просим сказать, на что больше похоже. Когда появляется новый вид спама, который мы до этого не видели - добавляем его в выборку и переобучаем классификатор. Если происходит ложное срабатывание, добавляем сообщение в выборку ham (то, что не спам). Этот подход работал лучше всего, и я пожалел, что не начал с него. Строго говоря, одного этого классификатора было бы достаточно для 95% всех случаев, с которыми мне приходилось сталкиваться, но убрать все остальные методы рука не поднималась.

Последний штрих - обнаружение спамеров по сообщениям с недостаточной информацией

Это в основном про грубый и прямой спам, где куча ссылок, но мало (или совсем нет) текста. Также сообщения без текста, но с картинками. Для таких случаев я добавил пару эвристик, которые срабатывают на подобные сообщения, например, если ссылок больше, чем слов в сообщении, то это спам. Это простой способ, который опционален, не требует обучения и работает быстро. Я его так и не включил, т.к. на практике он не был пока нужен, но он там есть на тот случай, если понадобится.

UPD: 23 марта 2024 обратил внимание, что появился настоящий спам с картинками и без текста. Включение обнаружителя “сообщения без текста, но с картинками”, которое я откладывал до последнего, опасаясь ложных срабатываний, за пару недель работы показало себя очень достойно.

Аспекты реализации

Все это я реализовал на Go, упаковал в Docker-контейнер и запустил на своем сервере. Основной классификатор предполагает, что администраторы могут добавлять сообщения в выборку и переобучать классификатор, так что пришлось интегрировать все это непосредственно в бота. Я старался сделать управление максимально простым: чтобы добавить сообщение в спам, достаточно либо переслать его в отдельный канал для администраторов, либо просто ответить на него командой /spam. Затем все происходит автоматически: сообщение удаляется, отправитель блокируется, и сообщение добавляется в выборку для переобучения.

Если сообщение было определено как спам, оно автоматически отправляется в канал администраторов, где показывается, почему оно было так классифицировано со всеми доступными деталями. В случае ложного срабатывания администратор может одним нажатием кнопки отменить блокировку, добавить сообщение в выборку ham и переобучить классификатор.

Пример управления спамом из Телеграмма

Еще один аспект это оптимизация скорости. Не вижу смысла проверять каждое сообщение пользователя на спам, если он уже отправил 10 сообщений, и все они были классифицированы как не спам. Такие пользователи добавляются в белый список, и их сообщения больше не проверяются. На практике оказалось, что даже 10 сообщений — это слишком много, так как спамеры, отправляющие сначала несколько нейтральных сообщений, а затем спам, встречаются крайне редко.

Чтобы снизить потенциальные затраты на проверку через OpenAI, я сделал ее последним этапом, после всех остальных проверок. Эта проверка может быть как решающей (если все остальные методы пропустили спам), так и подтверждающей (если спам уже был обнаружен другим способом), и в любом случае она вызывается нечасто. Повторюсь, что пока не возникало необходимости активировать эту проверку, но она готова к использованию.

Когда я начал использовать tg-spam и у него появились активные пользователи кроме меня, выяснилось, что базовой функциональности иногда не хватает. В результате, появился режим обучения, когда бот не удаляет сообщения, а лишь маркирует их как спам/не спам, позволяя админам самостоятельно решить, что с ними делать. Также появилась функция добавления пользователей в белый список и их удаления оттуда. Еще я ввел “параноидальный режим”, в котором все сообщения проверяются на спам.

Исходная версия не сохраняла состояние (кроме недавно обученных данных spam/ham), и любая информация, например список проверенных пользователей, терялась при перезагрузке. Хотя это не было реальной проблемой, на практике возникла необходимость в сохранении состояния. Теперь он хранит все данные в SQLite, и при перезапуске все восстанавливается.

Замечу, что написание функциональной части бота было довольно простым, но с реализацией его интерактивной части я намучился. Это был мой первый и, надеюсь, последний опыт работы со всеми этими элементами управления, которые требуют плясок с бубном, чтобы самые простые вещи заработали и, совершенно точно, придуманы какими-то вредителями, чтобы максимально усложнить жизнь разработчикам.

WEB интерфейс

Для удобства администраторов я добавил веб-интерфейс, где можно найти все сообщения, которые были определены как спам, и посмотреть, почему. Там же можно добавить сообщение в выборку spam/ham и переобучить классификатор. Также там можно протестировать сообщение на спам и посмотреть, насколько уверен классификатор в своем решении и какие проверки сработали.

Примеры web интерфейса

Результаты

Это все я назвал tg-spam, и эта штука реально работает, и работает очень достойно. Главная проблема таких систем — это ложные срабатывания, но их у меня было совсем немного. Конечно, иногда спам не определялся, но это была редкость, когда появлялся совсем новый вид спама. Админы на это реагировали быстро, добавляли его в выборку, и после этого все подобные сообщения определялись как спам.

Кроме готового к использованию бота, который настраивается за 5 минут, я подготовил и библиотеку для Go, которая позволяет использовать все эти методы в своем коде, например для определения спама в системах, не связанных с Telegram. Еще у сервиса есть простой HTTP API, и его можно использовать для проверки сообщений на спам, и для добавления сообщений в выборку для переобучения.

Где это взять

Это open source проект и его можно найти на umputun/tg-spam. Можно взять как собранный образ, так и бинарный файл. Код доступен под лицензией MIT, и я буду рад если tg-spam кому-то пригодится.