Commit
·
8325569
0
Parent(s):
Add social media handle checker application with Quart framework
Browse files- Implement main application logic in app.py
- Create index.html for user interface
- Add requirements.txt for dependencies
- Include .gitignore to exclude __pycache__ directory
- .gitignore +1 -0
- app.py +126 -0
- index.html +141 -0
- requirements.txt +3 -0
.gitignore
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
__pycache__/
|
app.py
ADDED
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import sys
|
2 |
+
import os
|
3 |
+
|
4 |
+
sys.path.append(os.path.abspath("../../../"))
|
5 |
+
|
6 |
+
import inspect
|
7 |
+
from typing import Callable, Literal
|
8 |
+
from quart import Quart, send_from_directory
|
9 |
+
import requests
|
10 |
+
import re
|
11 |
+
import asyncio
|
12 |
+
from typing import Any, Callable, Coroutine
|
13 |
+
from python_utils.get_browser import get_browser_page_async
|
14 |
+
import re
|
15 |
+
|
16 |
+
app = Quart(__name__)
|
17 |
+
|
18 |
+
@app.route('/')
|
19 |
+
async def index():
|
20 |
+
"""Route handler for the home page"""
|
21 |
+
try:
|
22 |
+
return await send_from_directory('.', 'index.html')
|
23 |
+
except Exception as e:
|
24 |
+
return str(e)
|
25 |
+
|
26 |
+
@app.route('/check/<platform>/<username>', methods=['GET'])
|
27 |
+
async def check_social_media_handle(platform: str, username: str):
|
28 |
+
match platform.lower():
|
29 |
+
case "instagram":
|
30 |
+
return await async_availability_status(
|
31 |
+
resolve_instagram_username(username))
|
32 |
+
case "linkedin-user":
|
33 |
+
return await async_availability_status(
|
34 |
+
resolve_linkedin_username(username, "in"))
|
35 |
+
case "linkedin-page":
|
36 |
+
return await async_availability_status(
|
37 |
+
resolve_linkedin_username(username, "company"))
|
38 |
+
return {
|
39 |
+
"message": f'❌ The platform "{platform}" is not supported'
|
40 |
+
}
|
41 |
+
|
42 |
+
def resolve_instagram_username(username: str) -> tuple[str, bool, str] :
|
43 |
+
def get_json_value(page_source, key, value_pattern):
|
44 |
+
pattern = rf'[\'"]?{key}[\'"]?\s*:\s*[\'"]?({value_pattern})[\'"]?'
|
45 |
+
match = re.search(pattern, page_source, flags=re.IGNORECASE)
|
46 |
+
return match.group(1) if match else None
|
47 |
+
def is_valid_instagram_username(username):
|
48 |
+
"""
|
49 |
+
Validates an Instagram username based on their username rules:
|
50 |
+
- 1 to 30 characters long
|
51 |
+
- Can contain letters (a-z), numbers (0-9), and periods/underscores
|
52 |
+
- Cannot start or end with a period
|
53 |
+
- Cannot have consecutive periods
|
54 |
+
- Cannot have periods next to underscores
|
55 |
+
"""
|
56 |
+
# Regex pattern for Instagram username validation
|
57 |
+
pattern = r'^(?!.*\.\.)(?!.*\._)(?!.*_\.)(?![\.])[a-zA-Z0-9](?!.*\.$)[a-zA-Z0-9._]{0,28}[a-zA-Z0-9]$'
|
58 |
+
return re.match(pattern, username) is not None
|
59 |
+
def resolve() -> bool:
|
60 |
+
restricted_usernames = ["username"]
|
61 |
+
if username.lower() in restricted_usernames:
|
62 |
+
raise Exception(f'"{username}" is not allowed')
|
63 |
+
if not is_valid_instagram_username(username):
|
64 |
+
raise Exception(f'"{username}" is not a valid instagram username')
|
65 |
+
response = requests.get("https://www.instagram.com/")
|
66 |
+
x_ig_app_id = get_json_value(response.text, "X-IG-App-ID", "\d+")
|
67 |
+
user_data_response = requests.get(
|
68 |
+
url=f"https://www.instagram.com/api/v1/users/web_profile_info/?username={username}",
|
69 |
+
headers={
|
70 |
+
"x-ig-app-id": x_ig_app_id,
|
71 |
+
})
|
72 |
+
return (
|
73 |
+
username,
|
74 |
+
user_data_response.ok and user_data_response.json().get('status') == 'ok',
|
75 |
+
f"https://www.instagram.com/{username}/")
|
76 |
+
return resolve
|
77 |
+
|
78 |
+
def resolve_linkedin_username(username: str, company_or_user: Literal["company", "in"]) -> tuple[str, bool, str]:
|
79 |
+
async def resolve() -> tuple[str, bool, str]:
|
80 |
+
# can replace "www." with "de.", ".ke", ".ug", etc
|
81 |
+
# inkedin private user => kamau
|
82 |
+
uri: str = f"https://www.linkedin.com/{company_or_user}/{username}"
|
83 |
+
page, close = await get_browser_page_async()
|
84 |
+
response = None
|
85 |
+
async def capture_response(resp):
|
86 |
+
nonlocal response
|
87 |
+
if uri in resp.url:
|
88 |
+
response = resp
|
89 |
+
page.on("response", capture_response)
|
90 |
+
await page.goto("https://www.linkedin.com/")
|
91 |
+
await page.evaluate(f"""
|
92 |
+
fetch("{uri}", {{ "mode": "no-cors", "credentials": "include" }})
|
93 |
+
""")
|
94 |
+
await close()
|
95 |
+
return (username, response.ok, uri)
|
96 |
+
return resolve
|
97 |
+
|
98 |
+
async def async_availability_status(
|
99 |
+
resolve: Callable[[str], Coroutine[Any, Any, bool]], message: str = None):
|
100 |
+
try:
|
101 |
+
username_is_available_uri: tuple[str, bool, str] = await resolve()\
|
102 |
+
if inspect.iscoroutinefunction(resolve) or inspect.isawaitable(resolve)\
|
103 |
+
else await asyncio.to_thread(resolve)
|
104 |
+
username, is_available, uri = username_is_available_uri
|
105 |
+
if is_available == True:
|
106 |
+
return {
|
107 |
+
'available': False,
|
108 |
+
'message': f"{username}: ❌ Taken",
|
109 |
+
'url': uri
|
110 |
+
}
|
111 |
+
if message:
|
112 |
+
return {
|
113 |
+
'available': True,
|
114 |
+
'message': message,
|
115 |
+
'url': uri
|
116 |
+
}
|
117 |
+
else:
|
118 |
+
return {
|
119 |
+
'available': True,
|
120 |
+
'message': f"{username}: ✅ Available",
|
121 |
+
'url': uri
|
122 |
+
}
|
123 |
+
except Exception as e:
|
124 |
+
return {
|
125 |
+
'message': f"❌ {str(e)}"
|
126 |
+
}
|
index.html
ADDED
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
|
4 |
+
<head>
|
5 |
+
<meta charset="UTF-8">
|
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 |
+
|
18 |
+
<body class="bg-light">
|
19 |
+
<div class="container mt-5">
|
20 |
+
<h1 class="text-center mb-4">Social Media Handle Checker</h1>
|
21 |
+
|
22 |
+
<p class="text-center text-body-secondary">
|
23 |
+
<a target="_blank" href="https://toknow.ai/posts/social-media-handle-checker"
|
24 |
+
class="text-decoration-none text-reset">
|
25 |
+
<b><u>ToKnow</u></b>.ai
|
26 |
+
</a>
|
27 |
+
</p>
|
28 |
+
|
29 |
+
<div class="row justify-content-center">
|
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>
|
36 |
+
</div>
|
37 |
+
</form>
|
38 |
+
</div>
|
39 |
+
</div>
|
40 |
+
|
41 |
+
<div class="row justify-content-center">
|
42 |
+
<div class="col-lg-6 col-md-8" id="platform-container">
|
43 |
+
</div>
|
44 |
+
</div>
|
45 |
+
|
46 |
+
<p class="text-center mt-4 text-body-secondary text-lowercase">
|
47 |
+
<a target="_blank" href="https://toknow.ai/posts/social-media-handle-checker" class="text-decoration-none">
|
48 |
+
<i>get help or get more details</i>
|
49 |
+
</a>
|
50 |
+
<img class="rounded mx-auto d-block"
|
51 |
+
src="https://api.visitorbadge.io/api/visitors?path=https://toknow.ai/posts/social-media-handle-checker" />
|
52 |
+
</p>
|
53 |
+
</div>
|
54 |
+
|
55 |
+
<script>
|
56 |
+
const platformContainer = document.querySelector("#platform-container")
|
57 |
+
const platforms = [
|
58 |
+
{
|
59 |
+
id: "instagram",
|
60 |
+
name: "Instagram",
|
61 |
+
img: "https://cdn.jsdelivr.net/npm/simple-icons@v6/icons/instagram.svg"
|
62 |
+
},
|
63 |
+
{
|
64 |
+
id: "x",
|
65 |
+
name: "X (formerly Twitter)",
|
66 |
+
img: "https://cdn.jsdelivr.net/npm/simple-icons@v6/icons/twitter.svg"
|
67 |
+
},
|
68 |
+
{
|
69 |
+
id: "linkedin-user",
|
70 |
+
name: "LinkedIn User",
|
71 |
+
img: "https://cdn.jsdelivr.net/npm/simple-icons@v6/icons/linkedin.svg"
|
72 |
+
},
|
73 |
+
{
|
74 |
+
id: "linkedin-page",
|
75 |
+
name: "LinkedIn Company",
|
76 |
+
img: "https://cdn.jsdelivr.net/npm/simple-icons@v6/icons/linkedin.svg"
|
77 |
+
}
|
78 |
+
];
|
79 |
+
|
80 |
+
for (const platform of platforms) {
|
81 |
+
platformContainer.innerHTML +=
|
82 |
+
`<div class="card mb-2" data-platform="${platform.id}">
|
83 |
+
<div class="card-body d-flex align-items-center">
|
84 |
+
<img src="${platform.img}" alt="${platform.name}" class="platform-logo">
|
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 |
+
}
|
91 |
+
|
92 |
+
document.getElementById('search-form').addEventListener('submit', function (e) {
|
93 |
+
e.preventDefault();
|
94 |
+
const username = document.getElementById('social-handle').value;
|
95 |
+
if ((username || '').trim().length == 0) {
|
96 |
+
return;
|
97 |
+
}
|
98 |
+
|
99 |
+
if (platforms.length == 0) {
|
100 |
+
alert("Not working right now, please try again later...")
|
101 |
+
return;
|
102 |
+
}
|
103 |
+
|
104 |
+
// Simulate API calls
|
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 = ' checking...';
|
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.textContent = message;
|
121 |
+
statusSpan.innerHTML += ` (<a href="${url}" target="_blank">view</a>)`;
|
122 |
+
} else if (available === false) {
|
123 |
+
card.classList.add('bg-danger-subtle');
|
124 |
+
statusSpan.textContent = message;
|
125 |
+
statusSpan.innerHTML += ` (<a href="${url}" target="_blank">view</a>)`;
|
126 |
+
} else {
|
127 |
+
card.classList.add('bg-warning-subtle');
|
128 |
+
statusSpan.textContent = message || 'Unable to check';
|
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 |
+
});
|
138 |
+
</script>
|
139 |
+
</body>
|
140 |
+
|
141 |
+
</html>
|
requirements.txt
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
gunicorn==22.0.0
|
2 |
+
Quart==0.19.9
|
3 |
+
requests==2.32.3
|