-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathheuristics.py
More file actions
156 lines (128 loc) · 4.82 KB
/
Copy pathheuristics.py
File metadata and controls
156 lines (128 loc) · 4.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
"""Pure spam-scoring heuristics for the Telegram spam sweeper.
No Telethon or I/O here — everything is a pure function so it can be
unit-tested and tuned from the sweeper.log without touching the wiring.
"""
import re
from dataclasses import dataclass, field
SPAM_THRESHOLD = 3
URL_RE = re.compile(
r"(https?://\S+|www\.\S+|t\.me/\S+|telegram\.me/\S+)", re.IGNORECASE
)
PHONE_RE = re.compile(r"\+?\d[\d\s().-]{7,}\d")
KEYWORD_CATEGORIES = {
"keyword:crypto-investment": [
"usdt", "crypto", "bitcoin", "btc", "eth ", "forex", "trading signal",
"passive income", "guaranteed profit", "daily profit", "invest",
"investment", "returns", "profit", "earn money", "make money",
"financial freedom", "binary option", "stock tips", "airdrop",
],
"keyword:premium-gift-bait": [
"telegram premium", "premium gift", "free premium", "gift subscription",
"you have received a gift", "claim your", "you won", "congratulations",
"lucky winner", "prize",
],
"keyword:platform-redirect": [
"whatsapp", "signal app", "text me on", "message me on", "add me on",
"reach me on",
],
"keyword:generic-spam": [
"dear friend", "hello dear", "job offer", "work from home",
"part-time job", "salary per day", "casino", "betting", "18+",
"adult content", "hot photos",
],
}
_EMOJI_RE = re.compile(
"[\U0001F300-\U0001FAFF\U00002600-\U000027BF\U0001F900-\U0001F9FF]"
)
@dataclass
class Verdict:
is_spam: bool
score: int
reasons: list = field(default_factory=list)
def score_text(text):
"""Score one message body. Returns (score, reasons)."""
if not text:
return 0, []
score = 0
reasons = []
lower = text.lower()
if URL_RE.search(text):
score += 2
reasons.append("url/invite-link")
for category, keywords in KEYWORD_CATEGORIES.items():
hits = [kw for kw in keywords if kw in lower]
if hits:
score += 2
reasons.append(f"{category} ({', '.join(hits[:3])})")
if PHONE_RE.search(text):
score += 1
reasons.append("phone-number")
emoji_count = len(_EMOJI_RE.findall(text))
if emoji_count >= 5:
score += 1
reasons.append(f"emoji-blast ({emoji_count})")
return score, reasons
def _score_texts(texts):
"""Combined score for a chat: union of signals across its messages."""
total = 0
all_reasons = []
seen = set()
for text in texts or []:
score, reasons = score_text(text)
for reason in reasons:
key = reason.split(" ")[0]
if key not in seen:
seen.add(key)
all_reasons.append(reason)
total = max(total, score)
# Re-derive the union score from distinct signal kinds so spam split
# across several short messages scores like one combined blast.
union_score = 0
for reason in all_reasons:
if reason.startswith("url") or reason.startswith("keyword"):
union_score += 2
else:
union_score += 1
return max(total, union_score), all_reasons
def classify_dm(
is_contact,
has_outgoing,
texts,
sender_verified=False,
sender_has_username=True,
sender_name=None,
media_only=False,
):
"""Classify a private chat with a sender. Trusted senders short-circuit.
Policy: a non-contact, unverified sender with no reply history is spam
on its own — the stranger baseline alone meets the threshold. Content
and profile signals still accumulate so the log shows how spammy a
flagged chat looked beyond mere strangerhood. `sender_name` is the
display name; pass None when unknown to skip the name checks (empty
string means an actually blank name).
"""
if is_contact or has_outgoing or sender_verified:
return Verdict(False, 0, ["trusted: contact/mutual-history/verified"])
score, reasons = _score_texts(texts)
score += SPAM_THRESHOLD
reasons.append("stranger (not a contact, unverified, no history)")
if not sender_has_username:
score += 1
reasons.append("no-username")
if sender_name is not None:
if not sender_name.strip():
score += 1
reasons.append("empty name")
elif _EMOJI_RE.search(sender_name):
score += 1
reasons.append("emoji-decorated name")
if media_only:
score += 1
reasons.append("media-only opener")
return Verdict(score >= SPAM_THRESHOLD, score, reasons)
def classify_group(title, texts, inviter_is_contact, i_am_creator):
"""Classify a group/channel the account is in."""
if i_am_creator or inviter_is_contact:
return Verdict(False, 0, ["trusted: own-group/contact-invite"])
score, reasons = _score_texts([title] + list(texts or []))
return Verdict(score >= SPAM_THRESHOLD, score, reasons)