Refactor Instagram and LinkedIn username resolution to enhance logging, improve response handling, and update UI feedback
Browse files- app.py +39 -30
- index.html +29 -9
app.py
CHANGED
@@ -31,20 +31,35 @@ async def check_social_media_handle(platform: str, username: str):
|
|
31 |
match platform.lower():
|
32 |
case "instagram":
|
33 |
response = await async_availability_status(
|
34 |
-
resolve_instagram_username(username, logger)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
35 |
case "linkedin-user":
|
36 |
response = await async_availability_status(
|
37 |
-
resolve_linkedin_username(username, "in")
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
case "linkedin-page":
|
39 |
response = await async_availability_status(
|
40 |
-
resolve_linkedin_username(username, "company")
|
|
|
41 |
case _:
|
42 |
response = {
|
43 |
"message": f'β The platform "{platform}" is not supported'
|
44 |
}
|
45 |
return {**response, "logs": logs}
|
46 |
|
47 |
-
def resolve_instagram_username(
|
|
|
48 |
def get_json_value(page_source, key, value_pattern):
|
49 |
pattern = rf'[\'"]?{key}[\'"]?\s*:\s*[\'"]?({value_pattern})[\'"]?'
|
50 |
match = re.search(pattern, page_source, flags=re.IGNORECASE)
|
@@ -62,47 +77,37 @@ def resolve_instagram_username(username: str, logger: Callable[[str, str], None]
|
|
62 |
pattern = r'^(?!.*\.\.)(?!.*\._)(?!.*_\.)(?![\.])[a-zA-Z0-9](?!.*\.$)[a-zA-Z0-9._]{0,28}[a-zA-Z0-9]$'
|
63 |
return re.match(pattern, username) is not None
|
64 |
def resolve() -> bool:
|
65 |
-
# await fetch("https://www.instagram.com/ajax/bulk-route-definitions/", {
|
66 |
-
# "headers": {
|
67 |
-
# "content-type": "application/x-www-form-urlencoded",
|
68 |
-
# },
|
69 |
-
# "body": 'route_urls[0]=/us/&lsd=AVr8bTpwtk4', // "lsd" or 'token'
|
70 |
-
# "method": "POST",
|
71 |
-
# }).then(resp => resp.text()).then(text => JSON.parse(text.replace('for (;;);', ''))?.payload?.payloads)
|
72 |
-
restricted_usernames = [
|
73 |
-
# "username", "we", "instagram"
|
74 |
-
]
|
75 |
-
if username.lower() in restricted_usernames:
|
76 |
-
raise Exception(f'"{username}" is not allowed')
|
77 |
if not is_valid_instagram_username(username):
|
78 |
raise Exception(f'"{username}" is not a valid instagram username')
|
79 |
profile_uri = f"https://www.instagram.com/{username}/"
|
80 |
-
|
81 |
-
|
82 |
-
logger("
|
83 |
_return_result = lambda is_available: (username, is_available, profile_uri)
|
84 |
# if there is a username in the page, then this is likely an existing account
|
85 |
-
if
|
86 |
return _return_result(True)
|
87 |
-
x_ig_app_id = get_json_value(
|
88 |
-
|
89 |
url=f"https://www.instagram.com/api/v1/users/web_profile_info/?username={username}",
|
90 |
headers={
|
91 |
"x-ig-app-id": x_ig_app_id,
|
92 |
},
|
93 |
allow_redirects = False)
|
94 |
-
logger("
|
95 |
# if status is 404, then the account doesnt exist!
|
96 |
-
is_html = re.match(r'.*(\w+)/html',
|
97 |
-
if
|
98 |
return _return_result(False)
|
99 |
# if status is 200, check status of the json
|
100 |
-
is_json = re.match(r'.*(\w+)/json',
|
101 |
-
json_status = (
|
102 |
-
return _return_result(
|
103 |
return resolve
|
104 |
|
105 |
-
def resolve_linkedin_username(
|
|
|
|
|
106 |
async def resolve() -> tuple[str, bool, str]:
|
107 |
# can replace "www." with "de.", ".ke", ".ug", etc
|
108 |
# inkedin private user => kamau
|
@@ -123,11 +128,14 @@ def resolve_linkedin_username(username: str, company_or_user: Literal["company",
|
|
123 |
return resolve
|
124 |
|
125 |
async def async_availability_status(
|
126 |
-
resolve: Callable[[str], Coroutine[Any, Any, bool]],
|
|
|
|
|
127 |
try:
|
128 |
username_is_available_uri: tuple[str, bool, str] = await resolve()\
|
129 |
if inspect.iscoroutinefunction(resolve) or inspect.isawaitable(resolve)\
|
130 |
else await asyncio.to_thread(resolve)
|
|
|
131 |
username, is_available, uri = username_is_available_uri
|
132 |
if is_available == True:
|
133 |
return {
|
@@ -148,4 +156,5 @@ async def async_availability_status(
|
|
148 |
'url': uri
|
149 |
}
|
150 |
except Exception as e:
|
|
|
151 |
return { 'message': f"β {str(e)}" }
|
|
|
31 |
match platform.lower():
|
32 |
case "instagram":
|
33 |
response = await async_availability_status(
|
34 |
+
resolve = resolve_instagram_username(username, logger),
|
35 |
+
logger = logger,
|
36 |
+
message = (
|
37 |
+
f'username <b>"{username}"</b> is β
Available.'
|
38 |
+
' However, usernames from disabled or deleted accounts may also appear'
|
39 |
+
' available but you can\'t choose them, eg: usernames like <b>"we"</b>, <b>"us"</b>, and <b>"the"</b>.'
|
40 |
+
' Go to <a target="_blank" href="https://accountscenter.instagram.com/">accounts center</a>'
|
41 |
+
" and try changing the username of an existing account and see if it it's available"))
|
42 |
case "linkedin-user":
|
43 |
response = await async_availability_status(
|
44 |
+
resolve = resolve_linkedin_username(username, "in", logger),
|
45 |
+
logger = logger,
|
46 |
+
message = (
|
47 |
+
f'username <b>"{username}"</b> is β
Available.'
|
48 |
+
' However, usernames from private or deleted accounts will appear'
|
49 |
+
" available, login into LinkedIn and go to"
|
50 |
+
f" \"https://www.linkedin.com/in/{username}\" and see if it it's available"))
|
51 |
case "linkedin-page":
|
52 |
response = await async_availability_status(
|
53 |
+
resolve = resolve_linkedin_username(username, "company", logger),
|
54 |
+
logger = logger)
|
55 |
case _:
|
56 |
response = {
|
57 |
"message": f'β The platform "{platform}" is not supported'
|
58 |
}
|
59 |
return {**response, "logs": logs}
|
60 |
|
61 |
+
def resolve_instagram_username(
|
62 |
+
username: str, logger: Callable[[str, str], None]) -> tuple[str, bool, str]:
|
63 |
def get_json_value(page_source, key, value_pattern):
|
64 |
pattern = rf'[\'"]?{key}[\'"]?\s*:\s*[\'"]?({value_pattern})[\'"]?'
|
65 |
match = re.search(pattern, page_source, flags=re.IGNORECASE)
|
|
|
77 |
pattern = r'^(?!.*\.\.)(?!.*\._)(?!.*_\.)(?![\.])[a-zA-Z0-9](?!.*\.$)[a-zA-Z0-9._]{0,28}[a-zA-Z0-9]$'
|
78 |
return re.match(pattern, username) is not None
|
79 |
def resolve() -> bool:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
80 |
if not is_valid_instagram_username(username):
|
81 |
raise Exception(f'"{username}" is not a valid instagram username')
|
82 |
profile_uri = f"https://www.instagram.com/{username}/"
|
83 |
+
profile_response = requests.get(profile_uri, allow_redirects = False)
|
84 |
+
profile_response_username = get_json_value(profile_response.text, "username", "\w+") or ""
|
85 |
+
logger("profile_response_username", profile_response_username)
|
86 |
_return_result = lambda is_available: (username, is_available, profile_uri)
|
87 |
# if there is a username in the page, then this is likely an existing account
|
88 |
+
if profile_response_username.lower().strip() == username.lower().strip():
|
89 |
return _return_result(True)
|
90 |
+
x_ig_app_id = get_json_value(profile_response.text, "X-IG-App-ID", "\d+")
|
91 |
+
web_profile_response = requests.get(
|
92 |
url=f"https://www.instagram.com/api/v1/users/web_profile_info/?username={username}",
|
93 |
headers={
|
94 |
"x-ig-app-id": x_ig_app_id,
|
95 |
},
|
96 |
allow_redirects = False)
|
97 |
+
logger("web_profile_response.status_code", web_profile_response.status_code)
|
98 |
# if status is 404, then the account doesnt exist!
|
99 |
+
is_html = re.match(r'.*(\w+)/html', web_profile_response.headers.get("Content-Type"))
|
100 |
+
if web_profile_response.status_code == 404 and is_html:
|
101 |
return _return_result(False)
|
102 |
# if status is 200, check status of the json
|
103 |
+
is_json = re.match(r'.*(\w+)/json', web_profile_response.headers.get("Content-Type"))
|
104 |
+
json_status = (web_profile_response.json() or {}).get('status') == 'ok' if is_json else False
|
105 |
+
return _return_result(web_profile_response.status_code == 200 and json_status)
|
106 |
return resolve
|
107 |
|
108 |
+
def resolve_linkedin_username(
|
109 |
+
username: str, company_or_user: Literal["company", "in"],
|
110 |
+
logger: Callable[[str, str], None],) -> tuple[str, bool, str]:
|
111 |
async def resolve() -> tuple[str, bool, str]:
|
112 |
# can replace "www." with "de.", ".ke", ".ug", etc
|
113 |
# inkedin private user => kamau
|
|
|
128 |
return resolve
|
129 |
|
130 |
async def async_availability_status(
|
131 |
+
resolve: Callable[[str], Coroutine[Any, Any, bool]],
|
132 |
+
logger: Callable[[str, str], None],
|
133 |
+
message: str = None):
|
134 |
try:
|
135 |
username_is_available_uri: tuple[str, bool, str] = await resolve()\
|
136 |
if inspect.iscoroutinefunction(resolve) or inspect.isawaitable(resolve)\
|
137 |
else await asyncio.to_thread(resolve)
|
138 |
+
logger("username_is_available_uri", username_is_available_uri)
|
139 |
username, is_available, uri = username_is_available_uri
|
140 |
if is_available == True:
|
141 |
return {
|
|
|
156 |
'url': uri
|
157 |
}
|
158 |
except Exception as e:
|
159 |
+
logger(f"{async_availability_status.__name__}:Exception", str(e))
|
160 |
return { 'message': f"β {str(e)}" }
|
index.html
CHANGED
@@ -6,12 +6,17 @@
|
|
6 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
7 |
<title>Social Media Handle Checker | ToKnow.ai</title>
|
8 |
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
|
9 |
<style>
|
10 |
.platform-logo {
|
11 |
width: 30px;
|
12 |
height: 30px;
|
13 |
margin-right: 10px;
|
14 |
}
|
|
|
|
|
|
|
|
|
15 |
</style>
|
16 |
</head>
|
17 |
|
@@ -30,6 +35,7 @@
|
|
30 |
<div class="col-lg-6 col-md-8">
|
31 |
<form id="search-form" class="mb-3 mt-4">
|
32 |
<div class="input-group">
|
|
|
33 |
<input type="text" id="social-handle" class="form-control" placeholder="Enter username"
|
34 |
aria-label="Username" required />
|
35 |
<button class="btn btn-primary" type="submit">Check</button>
|
@@ -39,7 +45,7 @@
|
|
39 |
</div>
|
40 |
|
41 |
<div class="row justify-content-center">
|
42 |
-
<div class="col-lg-
|
43 |
</div>
|
44 |
</div>
|
45 |
|
@@ -52,6 +58,8 @@
|
|
52 |
</p>
|
53 |
</div>
|
54 |
|
|
|
|
|
55 |
<script>
|
56 |
const platformContainer = document.querySelector("#platform-container")
|
57 |
const platforms = [
|
@@ -85,6 +93,11 @@
|
|
85 |
<span class="me-auto">${platform.name}</span>
|
86 |
<div class="spinner-border text-primary d-none me-2"></div>
|
87 |
<span class="status"></span>
|
|
|
|
|
|
|
|
|
|
|
88 |
</div>
|
89 |
</div>`
|
90 |
}
|
@@ -105,33 +118,40 @@
|
|
105 |
platforms.forEach(platform => {
|
106 |
const card = document.querySelector(`[data-platform="${platform.id}"]`);
|
107 |
const statusSpan = card.querySelector('.status');
|
|
|
108 |
const spinner = card.querySelector('.spinner-border');
|
|
|
109 |
|
110 |
card.classList.remove('bg-success-subtle', 'bg-danger-subtle', 'bg-warning-subtle');
|
111 |
spinner.classList.remove('d-none');
|
112 |
-
statusSpan.textContent = '
|
113 |
|
114 |
fetch(`/check/${platform.id}/${username}`)
|
115 |
.then(response => response.json())
|
116 |
.then(({ available, message, url }) => {
|
117 |
-
spinner.classList.add('d-none');
|
118 |
if (available === true) {
|
119 |
card.classList.add('bg-success-subtle');
|
120 |
-
statusSpan.
|
121 |
-
statusSpan.innerHTML += ` (<a href="${url}" target="_blank">view</a>)`;
|
122 |
} else if (available === false) {
|
123 |
card.classList.add('bg-danger-subtle');
|
124 |
-
statusSpan.
|
125 |
-
statusSpan.innerHTML += ` (<a href="${url}" target="_blank">view</a>)`;
|
126 |
} else {
|
127 |
card.classList.add('bg-warning-subtle');
|
128 |
-
statusSpan.textContent =
|
129 |
}
|
|
|
130 |
})
|
131 |
.catch(error => {
|
132 |
-
spinner.classList.add('d-none');
|
133 |
card.classList.add('bg-warning-subtle');
|
134 |
statusSpan.textContent = `Error checking availability`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
135 |
});
|
136 |
});
|
137 |
});
|
|
|
6 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
7 |
<title>Social Media Handle Checker | ToKnow.ai</title>
|
8 |
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
9 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css">
|
10 |
<style>
|
11 |
.platform-logo {
|
12 |
width: 30px;
|
13 |
height: 30px;
|
14 |
margin-right: 10px;
|
15 |
}
|
16 |
+
i[data-bs-toggle] {
|
17 |
+
cursor: pointer;
|
18 |
+
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
19 |
+
}
|
20 |
</style>
|
21 |
</head>
|
22 |
|
|
|
35 |
<div class="col-lg-6 col-md-8">
|
36 |
<form id="search-form" class="mb-3 mt-4">
|
37 |
<div class="input-group">
|
38 |
+
<span class="input-group-text">@</span>
|
39 |
<input type="text" id="social-handle" class="form-control" placeholder="Enter username"
|
40 |
aria-label="Username" required />
|
41 |
<button class="btn btn-primary" type="submit">Check</button>
|
|
|
45 |
</div>
|
46 |
|
47 |
<div class="row justify-content-center">
|
48 |
+
<div class="col-lg-8 col-md-10" id="platform-container">
|
49 |
</div>
|
50 |
</div>
|
51 |
|
|
|
58 |
</p>
|
59 |
</div>
|
60 |
|
61 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
62 |
+
|
63 |
<script>
|
64 |
const platformContainer = document.querySelector("#platform-container")
|
65 |
const platforms = [
|
|
|
93 |
<span class="me-auto">${platform.name}</span>
|
94 |
<div class="spinner-border text-primary d-none me-2"></div>
|
95 |
<span class="status"></span>
|
96 |
+
<i class="ms-2 d-none bi bi-info-circle-fill" data-bs-toggle="collapse" data-bs-target="#collapse-${platform.id}" aria-expanded="false" aria-controls="collapse-${platform.id}"></i>
|
97 |
+
</div>
|
98 |
+
<div class="collapse p-3 pt-0" id="collapse-${platform.id}">
|
99 |
+
<hr class="mt-0" />
|
100 |
+
<div></div>
|
101 |
</div>
|
102 |
</div>`
|
103 |
}
|
|
|
118 |
platforms.forEach(platform => {
|
119 |
const card = document.querySelector(`[data-platform="${platform.id}"]`);
|
120 |
const statusSpan = card.querySelector('.status');
|
121 |
+
const collapse = card.querySelector('.collapse > div');
|
122 |
const spinner = card.querySelector('.spinner-border');
|
123 |
+
const i_toggle = card.querySelector('i[data-bs-toggle]');
|
124 |
|
125 |
card.classList.remove('bg-success-subtle', 'bg-danger-subtle', 'bg-warning-subtle');
|
126 |
spinner.classList.remove('d-none');
|
127 |
+
statusSpan.textContent = 'Checking...';
|
128 |
|
129 |
fetch(`/check/${platform.id}/${username}`)
|
130 |
.then(response => response.json())
|
131 |
.then(({ available, message, url }) => {
|
|
|
132 |
if (available === true) {
|
133 |
card.classList.add('bg-success-subtle');
|
134 |
+
statusSpan.innerHTML = `<a href="${url}" target="_blank">${username}</a>: β
Available`;
|
|
|
135 |
} else if (available === false) {
|
136 |
card.classList.add('bg-danger-subtle');
|
137 |
+
statusSpan.innerHTML = `<a href="${url}" target="_blank">${username}</a>: β Taken`;
|
|
|
138 |
} else {
|
139 |
card.classList.add('bg-warning-subtle');
|
140 |
+
statusSpan.textContent = `${username}: π Unable to check`;
|
141 |
}
|
142 |
+
collapse.innerHTML = message;
|
143 |
})
|
144 |
.catch(error => {
|
|
|
145 |
card.classList.add('bg-warning-subtle');
|
146 |
statusSpan.textContent = `Error checking availability`;
|
147 |
+
collapse.innerHTML = error;
|
148 |
+
}).finally(() => {
|
149 |
+
spinner.classList.add('d-none');
|
150 |
+
if (collapse.textContent.trim().length > 0) {
|
151 |
+
i_toggle.classList.remove('d-none');
|
152 |
+
} else {
|
153 |
+
i_toggle.classList.add('d-none');
|
154 |
+
}
|
155 |
});
|
156 |
});
|
157 |
});
|