diff --git a/README.md b/README.md index 5d56a85..aa1211f 100755 --- a/README.md +++ b/README.md @@ -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 } ] ``` @@ -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 diff --git a/chat_list.sample.json b/chat_list.sample.json index 0c9ce16..f820638 100644 --- a/chat_list.sample.json +++ b/chat_list.sample.json @@ -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"] } ] diff --git a/forwarder/modules/forward.py b/forwarder/modules/forward.py index 955118e..3eaf38f 100644 --- a/forwarder/modules/forward.py +++ b/forwarder/modules/forward.py @@ -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( @@ -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, diff --git a/forwarder/utils/__init__.py b/forwarder/utils/__init__.py index b680661..5c763af 100644 --- a/forwarder/utils/__init__.py +++ b/forwarder/utils/__init__.py @@ -1 +1,2 @@ from .chat import * +from .message import * diff --git a/forwarder/utils/chat.py b/forwarder/utils/chat.py index 17ddcfc..59335f9 100644 --- a/forwarder/utils/chat.py +++ b/forwarder/utils/chat.py @@ -1,28 +1,80 @@ -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: @@ -30,12 +82,9 @@ def get_destenation(chat_id: int, topic_id: Optional[int] = None) -> List[ChatCo 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 diff --git a/forwarder/utils/message.py b/forwarder/utils/message.py new file mode 100644 index 0000000..fdb3ca4 --- /dev/null +++ b/forwarder/utils/message.py @@ -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 diff --git a/pyproject.toml b/pyproject.toml index c81831b..671b729 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "telegram-forwarder" -version = "2.2.1" +version = "2.3.0" description = "" authors = ["mrmissx "] license = "GNU General Public License v3.0"