Skip to content

Commit 68491a1

Browse files
authored
feature: add new dating category for username modules, add 5 Estonian dating site modules (#621)
* feature: add new dating category for username modules, add 5 Estonian dating site modules * fix: alternative missing user checks, bot detection error messages * fix: sanity check Neanky profile username * fix: sanity check Neanky profile username
1 parent db4f40b commit 68491a1

7 files changed

Lines changed: 560 additions & 0 deletions

File tree

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import html
2+
import re
3+
from urllib.parse import quote
4+
5+
import httpx
6+
7+
from user_scanner.core.orchestrator import generic_validate
8+
from user_scanner.core.result import Result
9+
10+
FIELD_NAMES = {
11+
"Konto tüüp": "account_type",
12+
"Nimi": "name",
13+
"Sünniaeg": "birth_date",
14+
"Riik": "country",
15+
"Elukoht": "location",
16+
"Kodulinn": "hometown",
17+
"Kasv": "height_cm",
18+
"Kaal": "weight_kg",
19+
"Kehaehitus": "body_type",
20+
"Juuste värv": "hair_color",
21+
"Silmade värv": "eye_color",
22+
"Orientatsioon": "orientation",
23+
"Soovin tutvuda": "looking_for",
24+
"Soovitud vanus": "desired_age",
25+
"Tutvumise eesmärk": "intent",
26+
"Mina (me) oleme": "relationship_status",
27+
"Kas sa oled abielus?": "marital_status",
28+
"Kas sul on lapsi?": "children",
29+
"Keeled": "languages",
30+
"Haridus": "education",
31+
"Kool/Ülikool kus õppisin": "school",
32+
"Hobid": "hobbies",
33+
"Avalik e-maili aadress": "public_email",
34+
"Kas sa suitsetad?": "smoking",
35+
"Suhe alkoholi": "alcohol",
36+
"Suhe narkootikumidesse": "drug_use",
37+
"Sissetulek": "income",
38+
"Minu roll seksis": "sexual_role",
39+
"Kui tihti soovin seksida": "sexual_frequency",
40+
"Mind erutab": "turn_ons",
41+
"Kas sulle meeldib masturbeerida": "masturbation",
42+
"BDSM": "bdsm",
43+
"Liiklusvahend": "vehicle",
44+
"Mind jahutab maha": "turn_offs",
45+
"Muusika, mis meeldib": "favorite_music",
46+
"Minu magamistoast leiad": "bedroom_items",
47+
"Mis muudab minu jaoks seksi kvaliteetseks?": "sexual_preferences",
48+
"Lemmikraamat": "favorite_book",
49+
"Lemmikfilm": "favorite_movie",
50+
"Ametikoht": "occupation",
51+
"Minu ettekujutus ideaalsest esimesest kohtingust": "ideal_first_date",
52+
"Mida õppisin oma viimasest suhtest": "relationship_lessons",
53+
}
54+
55+
56+
def validate_iha(user: str) -> Result:
57+
username = user.strip()
58+
url = f"https://www.iha.ee/users/{quote(username, safe='')}"
59+
60+
def process(response: httpx.Response) -> Result:
61+
if response.status_code != 200:
62+
return Result.error(f"Unexpected response status: {response.status_code}")
63+
64+
match = re.search(
65+
r'<div class="member_username"[^>]*>\s*([^<]+)', response.text
66+
)
67+
if match:
68+
profile_user = html.unescape(match.group(1)).strip()
69+
if profile_user.casefold() != username.casefold():
70+
return Result.error(
71+
"Profile response does not match the requested handle"
72+
)
73+
user_id = re.search(r'<form[^>]+(?:name|id)="_(\d+)"', response.text)
74+
extra = _fields(response.text)
75+
if user_id:
76+
extra["user_id"] = int(user_id.group(1))
77+
return Result.taken(
78+
extra=extra,
79+
media=_media(response.text, user_id.group(1) if user_id else None),
80+
)
81+
82+
if re.search(
83+
r"<title>\s*Iha\.ee - Seksikate inimeste kohtumispaik\s*</title>",
84+
response.text,
85+
) and (
86+
'id="online_list"' in response.text
87+
or "Sellist kasutajat ei leitud" in response.text
88+
):
89+
return Result.available()
90+
91+
return Result.error("Could not verify profile page")
92+
93+
return generic_validate(url, process, show_url=url)
94+
95+
96+
def _fields(page: str) -> dict[str, object]:
97+
pairs = re.findall(
98+
r'<div class="member_form_opt1">\s*<div[^>]*>(.*?)</div>\s*</div>\s*'
99+
r'<div class="member_form_opt2">\s*<div[^>]*>(.*?)</div>\s*</div>',
100+
page,
101+
re.DOTALL,
102+
)
103+
fields: dict[str, object] = {}
104+
for raw_label, raw_value in pairs:
105+
label = _text(raw_label)
106+
value = _text(raw_value)
107+
if label not in FIELD_NAMES or not value:
108+
continue
109+
field = FIELD_NAMES[label]
110+
if field == "birth_date":
111+
value = re.sub(r"\s+\(\d+\)$", "", value)
112+
fields[field] = value
113+
return fields
114+
115+
116+
def _media(page: str, user_id: str | None) -> dict[str, str]:
117+
if not user_id:
118+
return {}
119+
photos = list(
120+
dict.fromkeys(
121+
html.unescape(url)
122+
for url in re.findall(r"https://img2\.iha\.ee/[^\"'? )]+", page)
123+
if f"/{user_id}e" in url
124+
)
125+
)
126+
if not photos:
127+
return {}
128+
return {
129+
"avatar": photos[0],
130+
**{f"photo_{index}": url for index, url in enumerate(photos[1:], 1)},
131+
}
132+
133+
134+
def _text(value: str) -> str:
135+
return re.sub(r"\s+", " ", html.unescape(re.sub(r"<[^>]+>", " ", value))).strip()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import html
2+
import re
3+
from urllib.parse import quote
4+
5+
import httpx
6+
7+
from user_scanner.core.orchestrator import generic_validate
8+
from user_scanner.core.result import Result
9+
10+
GENDERS = {"mehega": "male", "naisega": "female"}
11+
12+
13+
def validate_amoremi(user: str) -> Result:
14+
username = user.strip()
15+
url = f"https://www.amoremi.ee/user/{quote(username, safe='')}"
16+
17+
def process(response: httpx.Response) -> Result:
18+
if response.status_code == 404 and (
19+
"<title>Lehekülge ei leitud</title>" in response.text
20+
or f'Kasutajat nimega "{html.escape(username)}" ei eksisteeri.'
21+
in response.text
22+
):
23+
return Result.available()
24+
if response.status_code != 200:
25+
return Result.error(f"Unexpected response status: {response.status_code}")
26+
27+
match = re.search(
28+
r'<div class="profile-title text-center">\s*'
29+
r'<div class=(?:"page-title"|page-title)>\s*([^<]+)',
30+
response.text,
31+
)
32+
if not match:
33+
return Result.error("Could not verify profile page")
34+
35+
profile_user = html.unescape(match.group(1)).strip()
36+
if profile_user.casefold() != username.casefold():
37+
return Result.error("Profile response does not match the requested handle")
38+
39+
extra: dict[str, object] = {}
40+
breadcrumb = re.search(
41+
r'<li class="breadcrumb-item active">\s*[^<]*?\s+(\d+)\s+a\.',
42+
response.text,
43+
)
44+
if breadcrumb:
45+
extra["age"] = int(breadcrumb.group(1))
46+
if profile_type := re.search(
47+
r'<a href="/tutvus/([^"/]+)">\s*([^<]+)</a>', response.text
48+
):
49+
extra["profile_type"] = html.unescape(profile_type.group(2)).strip()
50+
if profile_gender := GENDERS.get(profile_type.group(1)):
51+
extra["gender"] = profile_gender
52+
if description := re.search(
53+
r"<div class=profile-description>.*?"
54+
r'<div class="short-desc">(.*?)</div>',
55+
response.text,
56+
re.DOTALL,
57+
):
58+
extra["bio"] = _text(description.group(1))
59+
if seeking := re.search(
60+
r"Tutvun\s+(mehega|naisega)\s+(\d+)-(\d+)\s+a\.", response.text
61+
):
62+
extra["looking_for"] = GENDERS[seeking.group(1)]
63+
extra["desired_age"] = f"{seeking.group(2)}-{seeking.group(3)}"
64+
if title := re.search(r"<title>(.*?)</title>", response.text, re.DOTALL):
65+
extra["headline"] = (
66+
html.unescape(title.group(1)).strip().removesuffix(f" - {profile_user}")
67+
)
68+
69+
return Result.taken(extra=extra)
70+
71+
return generic_validate(url, process, show_url=url)
72+
73+
74+
def _text(value: str) -> str:
75+
return re.sub(r"\s+", " ", html.unescape(re.sub(r"<[^>]+>", " ", value))).strip()
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import html
2+
import re
3+
from urllib.parse import quote, urljoin
4+
5+
import httpx
6+
7+
from user_scanner.core.orchestrator import generic_validate, make_request
8+
from user_scanner.core.result import Result
9+
10+
11+
def validate_neanky(user: str) -> Result:
12+
username = user.strip()
13+
url = f"https://neanky.ee/user/{quote(username, safe='')}"
14+
15+
def process_profile(response: httpx.Response) -> Result:
16+
if (
17+
response.status_code == 404
18+
and "<title>Kasutajat ei leitud | Neanky" in response.text
19+
):
20+
return Result.available()
21+
if response.status_code != 200:
22+
return Result.error(f"Unexpected response status: {response.status_code}")
23+
24+
if pending := re.search(
25+
r"Soovitud kasutaja ei ole hetkel veel aktiveeritud!<br>\s*"
26+
r"Konto registreeritud:\s*([^<]+)<br>",
27+
response.text,
28+
):
29+
return Result.taken(
30+
extra={
31+
"account_status": "pending_activation",
32+
"registered_at": pending.group(1).strip(),
33+
}
34+
)
35+
36+
match = re.search(
37+
r'<h1 class="u-box-name-txt-b text-white">(.*?)</h1>',
38+
response.text,
39+
re.DOTALL,
40+
)
41+
if not match:
42+
return Result.taken()
43+
44+
profile_user = _text(match.group(1))
45+
if profile_user.casefold() != username.casefold():
46+
return Result.error("Profile response does not match the requested handle")
47+
48+
extra: dict[str, object] = {}
49+
if name := re.search(r"Nimi:\s*<strong>(.*?)</strong>", response.text):
50+
extra["name"] = _text(name.group(1))
51+
if gender := re.search(r"Sugu:\s*<img[^>]*>\s*([^<]+)<br", response.text):
52+
extra["gender"] = html.unescape(gender.group(1)).strip()
53+
if user_id := re.search(r'\bvar vsID="(\d+)"', response.text):
54+
extra["user_id"] = int(user_id.group(1))
55+
if online_status := re.search(
56+
r'<span[^>]+title="(online|offline)"', match.group(1)
57+
):
58+
extra["online_status"] = online_status.group(1)
59+
media: dict[str, str] = {}
60+
avatar = re.search(
61+
r"u-box-avatar-Pic[^>]+background-image:url\([\'\"]?([^\'\")]+)",
62+
response.text,
63+
)
64+
if avatar and not avatar.group(1).endswith("/images/avatar.svg"):
65+
media["avatar"] = urljoin("https://neanky.ee", avatar.group(1))
66+
return Result.taken(extra=extra, media=media)
67+
68+
def process_availability(response: httpx.Response) -> Result:
69+
if response.status_code != 200:
70+
return Result.error(f"Unexpected response status: {response.status_code}")
71+
data = response.json()
72+
if data.get("user", {}).get("username", "").casefold() != username.casefold():
73+
return Result.error(
74+
"Availability response does not match the requested handle"
75+
)
76+
if data.get("state") == 1:
77+
return Result.available()
78+
if data.get("state") != 0:
79+
return Result.error("Could not verify username availability")
80+
try:
81+
return process_profile(make_request(url))
82+
except httpx.HTTPError as exc:
83+
return Result.error(exc)
84+
85+
return generic_validate(
86+
"https://neanky.ee/p/register",
87+
process_availability,
88+
method="POST",
89+
data={"username": username, "type": "validateusername"},
90+
show_url=url,
91+
)
92+
93+
94+
def _text(value: str) -> str:
95+
return re.sub(r"\s+", " ", html.unescape(re.sub(r"<[^>]+>", " ", value))).strip()

0 commit comments

Comments
 (0)