|
10 | 10 | import platform |
11 | 11 | import sys |
12 | 12 | import time |
13 | | -from functools import lru_cache |
14 | 13 | import psutil |
15 | 14 | import requests |
16 | 15 |
|
@@ -164,279 +163,61 @@ def track_event(event_name: str): |
164 | 163 | ) |
165 | 164 |
|
166 | 165 |
|
167 | | -def get_geoip_properties_zh_cn(timeout_seconds: float = 1.5) -> dict: |
168 | | - providers = ( |
169 | | - ("系统位置信息", _get_system_location_properties_zh_cn), |
170 | | - ("IP33 位置信息", _get_ip33_location_properties), |
171 | | - ) |
172 | | - for provider_name, provider in providers: |
173 | | - properties = provider(timeout_seconds=timeout_seconds) |
174 | | - if properties: |
175 | | - logger.debug(f"获取到{provider_name}: {properties}") |
176 | | - return properties |
177 | | - return {} |
178 | | - |
179 | | - |
180 | | -_GEOIP_COUNTRY_KEY = "$geoip_country_name" |
181 | | -_GEOIP_CITY_KEY = "$geoip_city_name" |
182 | | -_COUNTRY_NAME_ZH_CN_MAP = { |
183 | | - "China": "中国", |
184 | | - "People's Republic of China": "中国", |
185 | | - "PRC": "中国", |
186 | | - "中华人民共和国": "中国", |
187 | | -} |
188 | | -_COUNTRY_REGION_ZH_CN_MAP = { |
189 | | - "CN": "中国", |
190 | | - "HK": "中国香港", |
191 | | - "MO": "中国澳门", |
192 | | - "TW": "中国台湾", |
193 | | -} |
194 | | -_IP33_AREA_CITY_RE = re.compile( |
195 | | - r"^(?P<prefix>.+?(?:省|自治区|特别行政区|市))(?P<rest>.*)$" |
196 | | -) |
197 | | - |
198 | | - |
199 | | -def _build_geoip_properties(country: str = "", city: str = "") -> dict: |
200 | | - country = (country or "").strip() |
201 | | - city = (city or "").strip() |
202 | | - |
203 | | - country = _COUNTRY_NAME_ZH_CN_MAP.get(country, country) |
204 | | - |
205 | | - properties = {} |
206 | | - if country: |
207 | | - properties[_GEOIP_COUNTRY_KEY] = country |
208 | | - if city: |
209 | | - properties[_GEOIP_CITY_KEY] = city |
210 | | - return properties |
211 | | - |
212 | | - |
213 | | -@lru_cache(maxsize=1) |
214 | | -def _map_country_region_to_zh_cn(country_region: str) -> str: |
215 | | - country_region = (country_region or "").strip().upper() |
216 | | - if not country_region: |
217 | | - return "" |
218 | | - |
219 | | - return _COUNTRY_REGION_ZH_CN_MAP.get(country_region, country_region) |
220 | | - |
221 | | - |
222 | | -def _get_windows_geo_country_region() -> tuple[str, str]: |
223 | | - try: |
224 | | - import ctypes |
225 | | - from ctypes import wintypes |
226 | | - |
227 | | - kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) |
228 | | - |
229 | | - GEOCLASS_NATION = 16 |
230 | | - GEOID_NOT_AVAILABLE = 0 |
231 | | - GEO_ISO2 = 4 |
232 | | - GEO_FRIENDLYNAME = 8 |
233 | | - |
234 | | - kernel32.GetUserGeoID.argtypes = [wintypes.DWORD] |
235 | | - kernel32.GetUserGeoID.restype = wintypes.INT |
236 | | - kernel32.GetGeoInfoW.argtypes = [ |
237 | | - wintypes.INT, |
238 | | - wintypes.DWORD, |
239 | | - wintypes.LPWSTR, |
240 | | - wintypes.INT, |
241 | | - wintypes.LANGID, |
242 | | - ] |
243 | | - kernel32.GetGeoInfoW.restype = wintypes.INT |
244 | | - |
245 | | - geo_id = int(kernel32.GetUserGeoID(GEOCLASS_NATION)) |
246 | | - if geo_id in (GEOID_NOT_AVAILABLE, -1): |
247 | | - return "", "" |
248 | | - |
249 | | - def _get_geoinfo(geo_type: int) -> str: |
250 | | - buf = ctypes.create_unicode_buffer(256) |
251 | | - size = int(kernel32.GetGeoInfoW(geo_id, geo_type, buf, len(buf), 0)) |
252 | | - if size <= 0: |
253 | | - return "" |
254 | | - return (buf.value or "").strip() |
255 | | - |
256 | | - iso2 = _get_geoinfo(GEO_ISO2).upper() |
257 | | - friendly = _get_geoinfo(GEO_FRIENDLYNAME) |
258 | | - return iso2, friendly |
259 | | - except Exception: |
260 | | - return "", "" |
261 | | - |
| 166 | +_GEOIP_COUNTRY_NAME_KEY = "$geoip_country_name" |
| 167 | +_GEOIP_SUBDIVISION_1_NAME_KEY = "$geoip_subdivision_1_name" |
| 168 | +_GEOIP_CITY_NAME_KEY = "$geoip_city_name" |
| 169 | +_GEOIP_IP_KEY = "$geoip_ip" |
| 170 | +_GEOIP_ISP_KEY = "$geoip_isp" |
262 | 171 |
|
263 | | -def _get_windows_location_properties_via_winrt_zh_cn(timeout_seconds: float) -> dict: |
264 | | - try: |
265 | | - import concurrent.futures |
266 | | - import asyncio |
267 | | - |
268 | | - import winrt.windows.devices.geolocation as geolocation |
269 | | - except Exception: |
270 | | - return {} |
271 | 172 |
|
| 173 | +def get_geoip_properties_zh_cn(timeout_seconds: float = 1.5) -> dict: |
| 174 | + headers = {"User-Agent": f"SecRandom/{SPECIAL_VERSION}"} |
272 | 175 | timeout_seconds = max(0.2, float(timeout_seconds or 0)) |
273 | 176 |
|
274 | | - def _worker() -> dict: |
275 | | - async def _get() -> dict: |
276 | | - locator = geolocation.Geolocator() |
277 | | - position = await asyncio.wait_for( |
278 | | - locator.get_geoposition_async(), timeout_seconds |
279 | | - ) |
280 | | - address = getattr(position, "civic_address", None) |
281 | | - if address is None: |
282 | | - return {} |
283 | | - |
284 | | - city = (getattr(address, "city", "") or "").strip() |
285 | | - state_province = (getattr(address, "state", "") or "").strip() |
286 | | - if not state_province: |
287 | | - state_province = (getattr(address, "state_province", "") or "").strip() |
288 | | - |
289 | | - country_region = ( |
290 | | - (getattr(address, "country_code", "") or "").strip().upper() |
291 | | - ) |
292 | | - country = (getattr(address, "country", "") or "").strip() |
293 | | - |
294 | | - if not country and country_region: |
295 | | - country = _map_country_region_to_zh_cn(country_region) |
296 | | - else: |
297 | | - country = _COUNTRY_NAME_ZH_CN_MAP.get(country, country) |
298 | | - |
299 | | - if not city: |
300 | | - city = state_province |
301 | | - |
302 | | - return _build_geoip_properties(country=country, city=city) |
303 | | - |
304 | | - return asyncio.run(_get()) |
305 | | - |
306 | | - with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: |
307 | | - future = executor.submit(_worker) |
308 | | - try: |
309 | | - return future.result(timeout=timeout_seconds + 0.3) |
310 | | - except Exception: |
311 | | - return {} |
312 | | - |
313 | | - |
314 | | -def _get_windows_timezone_key_name() -> str: |
315 | 177 | try: |
316 | | - import ctypes |
317 | | - from ctypes import wintypes |
318 | | - |
319 | | - class _SYSTEMTIME(ctypes.Structure): |
320 | | - _fields_ = [ |
321 | | - ("wYear", wintypes.WORD), |
322 | | - ("wMonth", wintypes.WORD), |
323 | | - ("wDayOfWeek", wintypes.WORD), |
324 | | - ("wDay", wintypes.WORD), |
325 | | - ("wHour", wintypes.WORD), |
326 | | - ("wMinute", wintypes.WORD), |
327 | | - ("wSecond", wintypes.WORD), |
328 | | - ("wMilliseconds", wintypes.WORD), |
329 | | - ] |
330 | | - |
331 | | - class _TIME_DYNAMIC_ZONE_INFORMATION(ctypes.Structure): |
332 | | - _fields_ = [ |
333 | | - ("Bias", wintypes.LONG), |
334 | | - ("StandardName", wintypes.WCHAR * 32), |
335 | | - ("StandardDate", _SYSTEMTIME), |
336 | | - ("StandardBias", wintypes.LONG), |
337 | | - ("DaylightName", wintypes.WCHAR * 32), |
338 | | - ("DaylightDate", _SYSTEMTIME), |
339 | | - ("DaylightBias", wintypes.LONG), |
340 | | - ("TimeZoneKeyName", wintypes.WCHAR * 128), |
341 | | - ("DynamicDaylightTimeDisabled", wintypes.BOOL), |
342 | | - ] |
343 | | - |
344 | | - kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) |
345 | | - kernel32.GetDynamicTimeZoneInformation.argtypes = [ |
346 | | - ctypes.POINTER(_TIME_DYNAMIC_ZONE_INFORMATION) |
347 | | - ] |
348 | | - kernel32.GetDynamicTimeZoneInformation.restype = wintypes.DWORD |
349 | | - |
350 | | - info = _TIME_DYNAMIC_ZONE_INFORMATION() |
351 | | - kernel32.GetDynamicTimeZoneInformation(ctypes.byref(info)) |
352 | | - return (info.TimeZoneKeyName or "").strip() |
| 178 | + response = requests.get( |
| 179 | + "https://ip9.com.cn/get", |
| 180 | + headers=headers, |
| 181 | + timeout=timeout_seconds, |
| 182 | + ) |
| 183 | + response.raise_for_status() |
| 184 | + data = response.json() |
353 | 185 | except Exception: |
354 | | - return "" |
355 | | - |
356 | | - |
357 | | -def _guess_city_from_timezone(country_region: str, tz_key_name: str) -> str: |
358 | | - country_region = (country_region or "").strip().upper() |
359 | | - tz_key_name = (tz_key_name or "").strip() |
360 | | - if not country_region or not tz_key_name: |
361 | | - return "" |
362 | | - |
363 | | - if country_region == "TW" and tz_key_name == "Taipei Standard Time": |
364 | | - return "台北" |
365 | | - |
366 | | - if country_region in {"CN", "HK", "MO"} and tz_key_name == "China Standard Time": |
367 | | - return "北京" |
368 | | - |
369 | | - return "" |
370 | | - |
371 | | - |
372 | | -def _get_system_location_properties_zh_cn(timeout_seconds: float = 1.5) -> dict: |
373 | | - if sys.platform != "win32": |
374 | 186 | return {} |
375 | 187 |
|
376 | | - properties = _get_windows_location_properties_via_winrt_zh_cn( |
377 | | - timeout_seconds=timeout_seconds |
378 | | - ) |
379 | | - if properties: |
380 | | - return properties |
381 | | - |
382 | | - country_region, friendly = _get_windows_geo_country_region() |
383 | | - if not country_region and not friendly: |
| 188 | + if not isinstance(data, dict) or data.get("ret") != 200: |
384 | 189 | return {} |
385 | 190 |
|
386 | | - country = () |
387 | | - if not country: |
388 | | - country = friendly |
389 | | - |
390 | | - city = "" |
391 | | - if country_region: |
392 | | - city = _guess_city_from_timezone( |
393 | | - country_region, _get_windows_timezone_key_name() |
394 | | - ) |
395 | | - |
396 | | - return _build_geoip_properties(country=country, city=city) |
397 | | - |
| 191 | + geo_data = data.get("data") |
| 192 | + if not isinstance(geo_data, dict): |
| 193 | + return {} |
398 | 194 |
|
399 | | -def _get_ip33_location_properties(timeout_seconds: float = 1.5) -> dict: |
400 | | - headers = {"User-Agent": f"SecRandom/{SPECIAL_VERSION}"} |
| 195 | + properties = {} |
401 | 196 |
|
402 | | - timeout_seconds = max(0.2, float(timeout_seconds or 0)) |
403 | | - for url in ("https://api.ip33.com/ip/search", "http://api.ip33.com/ip/search"): |
404 | | - try: |
405 | | - response = requests.get(url, headers=headers, timeout=timeout_seconds) |
406 | | - response.raise_for_status() |
407 | | - data = response.json() |
408 | | - properties = _parse_ip33_response_to_properties(data) |
409 | | - if properties: |
410 | | - return properties |
411 | | - except Exception: |
412 | | - continue |
413 | | - return {} |
| 197 | + country = (geo_data.get("country") or "").strip() |
| 198 | + if country: |
| 199 | + properties[_GEOIP_COUNTRY_NAME_KEY] = country |
414 | 200 |
|
| 201 | + prov = (geo_data.get("prov") or "").strip() |
| 202 | + if prov: |
| 203 | + properties[_GEOIP_SUBDIVISION_1_NAME_KEY] = prov |
415 | 204 |
|
416 | | -def _parse_ip33_response_to_properties(data: dict) -> dict: |
417 | | - if not isinstance(data, dict): |
418 | | - return {} |
| 205 | + city = (geo_data.get("city") or "").strip() |
| 206 | + if city: |
| 207 | + properties[_GEOIP_CITY_NAME_KEY] = city |
419 | 208 |
|
420 | | - area = (data.get("area") or "").strip() |
421 | | - if not area: |
422 | | - return {} |
| 209 | + ip = (geo_data.get("ip") or "").strip() |
| 210 | + if ip: |
| 211 | + properties[_GEOIP_IP_KEY] = ip |
423 | 212 |
|
424 | | - area_main = area.split()[0].strip() |
425 | | - if not area_main: |
426 | | - return {} |
| 213 | + isp = (geo_data.get("isp") or "").strip() |
| 214 | + if isp: |
| 215 | + properties[_GEOIP_ISP_KEY] = isp |
427 | 216 |
|
428 | | - city = "" |
429 | | - m = _IP33_AREA_CITY_RE.match(area_main) |
430 | | - if m: |
431 | | - rest = (m.group("rest") or "").strip() |
432 | | - if rest: |
433 | | - city = rest |
434 | | - else: |
435 | | - city = (m.group("prefix") or "").strip() |
436 | | - else: |
437 | | - city = area_main |
| 217 | + if properties: |
| 218 | + logger.debug(f"获取到地理位置信息: {properties}") |
438 | 219 |
|
439 | | - return _build_geoip_properties(country="中国", city=city) |
| 220 | + return properties |
440 | 221 |
|
441 | 222 |
|
442 | 223 | # ==================== 通知模块 ==================== |
|
0 commit comments