mckabue commited on
Commit
c8826b9
Β·
1 Parent(s): 488a688

Refactor Instagram and LinkedIn username resolution to enhance logging, improve response handling, and update UI feedback

Browse files
Files changed (2) hide show
  1. app.py +39 -30
  2. 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(username: str, logger: Callable[[str, str], None]) -> tuple[str, bool, str] :
 
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
- response = requests.get(profile_uri, allow_redirects = False)
81
- _username = get_json_value(response.text, "username", "\w+") or ""
82
- logger("user_data_response:_username", _username)
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 _username.lower().strip() == username.lower().strip():
86
  return _return_result(True)
87
- x_ig_app_id = get_json_value(response.text, "X-IG-App-ID", "\d+")
88
- user_data_response = requests.get(
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("user_data_response:status", user_data_response.status_code)
95
  # if status is 404, then the account doesnt exist!
96
- is_html = re.match(r'.*(\w+)/html', user_data_response.headers.get("Content-Type"))
97
- if user_data_response.status_code == 404 and is_html:
98
  return _return_result(False)
99
  # if status is 200, check status of the json
100
- is_json = re.match(r'.*(\w+)/json', user_data_response.headers.get("Content-Type"))
101
- json_status = (user_data_response.json() or {}).get('status') == 'ok' if is_json else False
102
- return _return_result(user_data_response.status_code == 200 and json_status)
103
  return resolve
104
 
105
- def resolve_linkedin_username(username: str, company_or_user: Literal["company", "in"]) -> tuple[str, bool, str]:
 
 
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]], message: str = None):
 
 
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-6 col-md-8" id="platform-container">
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 = ' 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
  });
 
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
  });