mckabue commited on
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

Files changed (4) hide show
  1. .gitignore +1 -0
  2. app.py +126 -0
  3. index.html +141 -0
  4. 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