Skip to content

Commit

Permalink
feat: add word filters and blacklist (#98)
Browse files Browse the repository at this point in the history
This update introduce two config on chat_list.json

filters (Optional) - An array of strings to filter words. If the message containes any of the strings in the array, it WILL BE forwarded.

blacklist (Optional) - An array of strings to blacklist words. If the message containes any of the string in the array, it will NOT BE forwarded.
  • Loading branch information
MrMissx authored May 18, 2024
1 parent 3a7a806 commit 6ba7685
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 41 deletions.
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,21 @@ This file contains the list of chats to forward messages from and to. The bot ex
"destination": [-10011111111, "-10022222222#123456"]
},
{
"source": "-10087654321#000000", // Topic/Forum group
"destination": ["-10033333333#654321"]
"source": "-10087654321#000000", // Topic/Forum group
"destination": ["-10033333333#654321"],
"filters": ["word1", "word2"] // message that contain this word will be forwarded
},
{
"source": -10087654321,
"destination": [-10033333333],
"blacklist": ["word3", "word4"] // message that contain this word will not be forwarded
},
{
"source": -10087654321,
"destination": [-10033333333],
"filters": ["word5"],
"blacklist": ["word6"]
// message must contain word5 and must not contain word6 to be forwarded
}
]
```
Expand All @@ -62,8 +75,13 @@ This file contains the list of chats to forward messages from and to. The bot ex
> If the source chat is a Topic groups, you **MUST** explicitly specify the topic ID. The bot will ignore incoming message from topic group if the topic ID is not specified.
- `destination` - An array of chat IDs to forward messages to. It can be a group or a channel.

> Destenation supports Topics chat. You can use `#topicID` string to forward to specific topic. Example: `[-10011111111, "-10022222222#123456"]`. With this config it will forward to chat `-10022222222` with topic `123456` and to chat `-10011111111` .
- `filters` (Optional) - An array of strings to filter words. If the message containes any of the strings in the array, it **WILL BE** forwarded.

- `blacklist` (Optional) - An array of strings to blacklist words. If the message containes any of the string in the array, it will **NOT BE** forwarded.

You may add as many objects as you want. The bot will forward messages from all the chats in the `source` field to all the chats in the `destination` field. Duplicates are allowed as it already handled by the bot.

### Python dependencies
Expand Down
14 changes: 13 additions & 1 deletion chat_list.sample.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@
},
{
"source": -10098765432,
"destination": [-10012345678, "-10012345678#98765"]
"destination": [-10012345678, "-10012345678#98765"],
"filters": ["word1", "word2"]
},
{
"source": -10087654321,
"destination": [-10033333333],
"blacklist": ["word3", "word4"]
},
{
"source": -10087654321,
"destination": [-10033333333],
"filters": ["word5"],
"blacklist": ["word6"]
}
]
44 changes: 28 additions & 16 deletions forwarder/modules/forward.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from telegram.ext import MessageHandler, filters, ContextTypes

from forwarder import bot, REMOVE_TAG, LOGGER
from forwarder.utils import get_source, get_destenation
from forwarder.utils import get_destination, get_config, predicate_text


async def send_message(
Expand All @@ -24,24 +24,36 @@ async def forwarder(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None:
if not message or not source:
return

for chat in get_destenation(message.chat_id, message.message_thread_id):
try:
await send_message(message, chat["chat_id"], thread_id=chat["thread_id"])
except RetryAfter as err:
LOGGER.warning(f"Rate limited, retrying in {err.retry_after} seconds")
await asyncio.sleep(err.retry_after + 0.2)
await send_message(message, chat["chat_id"], thread_id=chat["thread_id"])
except ChatMigrated as err:
await send_message(message, err.new_chat_id)
LOGGER.warning(
f"Chat {chat} has been migrated to {err.new_chat_id}!! Edit the config file!!"
)
except Exception as err:
LOGGER.warning(f"Failed to forward message from {source.id} to {chat} due to {err}")
dest = get_destination(source.id, message.message_thread_id)

for config in dest:

if config.filters:
if not predicate_text(config.filters, message.text or ""):
return
if config.blacklist:
if predicate_text(config.blacklist, message.text or ""):
return

for chat in config.destination:
LOGGER.debug(f"Forwarding message {source.id} to {chat}")
try:
await send_message(message, chat.get_id(), chat.get_topic())
except RetryAfter as err:
LOGGER.warning(f"Rate limited, retrying in {err.retry_after} seconds")
await asyncio.sleep(err.retry_after + 0.2)
await send_message(message, chat.get_id(), thread_id=chat.get_topic())
except ChatMigrated as err:
await send_message(message, err.new_chat_id)
LOGGER.warning(
f"Chat {chat} has been migrated to {err.new_chat_id}!! Edit the config file!!"
)
except Exception as err:
LOGGER.error(f"Failed to forward message from {source.id} to {chat} due to {err}")


FORWARD_HANDLER = MessageHandler(
filters.Chat([source["chat_id"] for source in get_source()])
filters.Chat([config.source.get_id() for config in get_config()])
& ~filters.COMMAND
& ~filters.StatusUpdate.ALL,
forwarder,
Expand Down
1 change: 1 addition & 0 deletions forwarder/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .chat import *
from .message import *
91 changes: 70 additions & 21 deletions forwarder/utils/chat.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,90 @@
from typing import List, List, Union, TypedDict, Optional
from typing import List, List, Union, Optional

from forwarder import CONFIG


class ChatConfig(TypedDict):
chat_id: int
thread_id: Optional[int]
PARSED_CONFIG = []


def parse_topic(chat_id: Union[str, int]) -> ChatConfig:
if isinstance(chat_id, str):
raw = chat_id.split("#")
if len(raw) == 2:
return {"chat_id": int(raw[0]), "thread_id": int(raw[1])}
return {"chat_id": int(raw[0]), "thread_id": None}
class ChatConfig:
__chat: Union[str, int]

return {"chat_id": chat_id, "thread_id": None}
def __init__(self, chat_id: Union[str, int]):
self.__chat = chat_id

def __repr__(self) -> str:
if self.is_topic:
return f"{self.get_id()}#{self.get_topic()}"
return str(self.get_id())

def get_source() -> List[ChatConfig]:
return [parse_topic(chat["source"]) for chat in CONFIG]
@property
def is_topic(self) -> bool:
if isinstance(self.__chat, str) and len(self.__chat.split("#")) == 2:
return True
return False

def get_topic(self) -> Optional[int]:
if not self.is_topic:
return None

def get_destenation(chat_id: int, topic_id: Optional[int] = None) -> List[ChatConfig]:
if isinstance(self.__chat, int):
return None

return int(self.__chat.split("#")[1])

def get_id(self) -> int:
if isinstance(self.__chat, int):
return self.__chat
return int(self.__chat.split("#")[0])


class ForwardConfig:
source: ChatConfig
destination: List[ChatConfig]
filters: Optional[List[str]]
blacklist: Optional[List[str]]

def __init__(
self,
source: Union[str, int],
destination: List[Union[str, int]],
filters: Optional[List[str]] = None,
blacklist: Optional[List[str]] = None,
):
self.source = ChatConfig(source)
self.destination = [ChatConfig(item) for item in destination]
self.filters = filters
self.blacklist = blacklist


def get_config() -> List[ForwardConfig]:
global PARSED_CONFIG
if PARSED_CONFIG:
return PARSED_CONFIG

PARSED_CONFIG = [
ForwardConfig(
source=chat["source"],
destination=chat["destination"],
filters=chat.get("filters"),
blacklist=chat.get("blacklist"),
)
for chat in CONFIG
]
return PARSED_CONFIG


def get_destination(chat_id: int, topic_id: Optional[int] = None) -> List[ForwardConfig]:
"""Get destination from a specific source chat
Args:
chat_id (`int`): source chat id
topic_id (`Optional[int]`): source topic id. Defaults to None.
"""

dest: List[ChatConfig] = []

for chat in CONFIG:
parsed = parse_topic(chat["source"])

if parsed["chat_id"] == chat_id and parsed["thread_id"] == topic_id:
dest.extend([parse_topic(item) for item in chat["destination"]])
dest: List[ForwardConfig] = []

for chat in get_config():
if chat.source.get_id() == chat_id and chat.source.get_topic() == topic_id:
dest.append(chat)
return dest
13 changes: 13 additions & 0 deletions forwarder/utils/message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import re

from typing import List


def predicate_text(filters: List[str], text: str) -> bool:
"""Check if the text contains any of the filters"""
for i in filters:
pattern = r"( |^|[^\w])" + re.escape(i) + r"( |$|[^\w])"
if re.search(pattern, text, flags=re.IGNORECASE):
return True

return False
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "telegram-forwarder"
version = "2.2.1"
version = "2.3.0"
description = ""
authors = ["mrmissx <hi@mrmiss.my.id>"]
license = "GNU General Public License v3.0"
Expand Down

0 comments on commit 6ba7685

Please sign in to comment.