117 lines
4.2 KiB
Python
117 lines
4.2 KiB
Python
from typing import Callable
|
||
from dataclasses import dataclass
|
||
from exception import UrlRemoverNotImplementedException
|
||
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
||
import re
|
||
from telebot.types import User
|
||
|
||
|
||
@dataclass(init=True, eq=True)
|
||
class TrackerRemovalProcessorMessage:
|
||
fromUser: User
|
||
text: str
|
||
|
||
|
||
@dataclass(frozen=True, init=True, eq=True)
|
||
class TrackerRemovalResult:
|
||
needsToReply: bool
|
||
text: str
|
||
|
||
|
||
class TrackerRemovalMsgProcessor:
|
||
def __init__(self, msg: TrackerRemovalProcessorMessage):
|
||
self.__msg = msg
|
||
|
||
def process(self) -> TrackerRemovalResult:
|
||
if not self.__remove_trackers_from_msg_urls():
|
||
return TrackerRemovalResult(
|
||
needsToReply=False, text=""
|
||
) # дальнейшие трансформации смысла не имеют
|
||
self.__emplace_sender_into_msg_text()
|
||
return TrackerRemovalResult(
|
||
needsToReply=True, text=self.__msg.text
|
||
) # сообщение было изменено, нужно ответ отослать
|
||
|
||
def __remove_trackers_from_msg_urls(self) -> bool:
|
||
trackers_extracted = False
|
||
|
||
# todo: вообще мы работаем с http и это юзкейс обскьюрный
|
||
# но ссылка может быть и без указания схемы, телега может распарсить
|
||
# просто строку через точки и в конце какой то домен верхнего уровня как ссылку
|
||
def is_url(url: str) -> bool:
|
||
SCHEMES = ["http://", "https://"]
|
||
return len([s for s in SCHEMES if url.startswith(s)]) != 0
|
||
|
||
SEPARATOR_CHARS = [" ", "\n"]
|
||
separator_regex = "(" + "|".join(SEPARATOR_CHARS) + ")"
|
||
lexems = re.split(separator_regex, self.__msg.text)
|
||
for i, l in enumerate(lexems):
|
||
if not is_url(l):
|
||
continue
|
||
|
||
removed_trackers_url = self.__remove_tracker(l)
|
||
if l == removed_trackers_url: # изменений урла не было
|
||
continue
|
||
|
||
trackers_extracted = True
|
||
lexems[i] = removed_trackers_url
|
||
|
||
self.__msg.text = "".join(lexems)
|
||
return trackers_extracted
|
||
|
||
@staticmethod
|
||
def __remove_tracker(url: str) -> str:
|
||
try:
|
||
parsed_url = urlparse(url)
|
||
except Exception:
|
||
return url
|
||
if parsed_url.hostname is None:
|
||
return url
|
||
hostname = str(parsed_url.hostname)
|
||
try:
|
||
return TrackerRemoverFactory.make_remover(hostname)(url)
|
||
except UrlRemoverNotImplementedException:
|
||
return url
|
||
|
||
def __emplace_sender_into_msg_text(self):
|
||
self.__msg.text = f'Message from <a href="tg://user?id={self.__msg.fromUser.id}">{self.__msg.fromUser.first_name}</a>:\n\n{self.__msg.text}'
|
||
|
||
|
||
class TrackerRemoverFactory:
|
||
TrackerRemover = Callable[[str], str]
|
||
|
||
@staticmethod
|
||
def make_remover(domain: str) -> TrackerRemover:
|
||
@dataclass(frozen=True, init=True)
|
||
class RemoverIdentifyer:
|
||
domains: list[str]
|
||
remover: TrackerRemoverFactory.TrackerRemover
|
||
|
||
removers_by_domain = [
|
||
RemoverIdentifyer(
|
||
domains=["youtube.com", "youtu.be"],
|
||
remover=TrackerRemoverFactory.remove_yt_trackers,
|
||
)
|
||
]
|
||
remover_one = [
|
||
r
|
||
for r in removers_by_domain
|
||
if len([d for d in r.domains if domain.endswith(d)]) != 0
|
||
]
|
||
if len(remover_one) == 0:
|
||
raise UrlRemoverNotImplementedException(domain)
|
||
return remover_one[0].remover
|
||
|
||
@staticmethod
|
||
def remove_yt_trackers(url: str) -> str:
|
||
# todo: подумать как обобщить, мб билдер стратегии поиска трекера
|
||
# но эт сильно на потом
|
||
QUERY_PARAMS_TRACKER = "si"
|
||
parsed_url = urlparse(url)
|
||
query_params = parse_qs(parsed_url.query)
|
||
if QUERY_PARAMS_TRACKER in query_params:
|
||
del query_params[QUERY_PARAMS_TRACKER]
|
||
return urlunparse(
|
||
parsed_url._replace(query=urlencode(query_params, doseq=True))
|
||
)
|