Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add word filters and blacklist #98

Merged
merged 3 commits into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <[email protected]>"]
license = "GNU General Public License v3.0"
Expand Down