Created
June 27, 2024 21:15
-
-
Save Jaakkonen/6dfdf86679d4baf82cdc69c99413cda1 to your computer and use it in GitHub Desktop.
Clash of clans clan search
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/python | |
| import re | |
| import statistics | |
| from typing import Any, Callable | |
| import requests | |
| import os | |
| from urllib.parse import quote | |
| from cachecontrol import CacheControl | |
| from cachecontrol.caches.file_cache import FileCache | |
| # Get API key from https://developer.clashofclans.com/#/account | |
| # and specify it here. Note that this uses IP whitelisting so for | |
| # dynamic IP addresses keys need to be created each time the IP changes. | |
| os.environ["COC_APIKEY"] | |
| s = CacheControl(requests.Session(), cache=FileCache(".clashofclans_request_cache")) | |
| s.headers.update( | |
| { | |
| "Authorization": "Bearer " + COCAPIKEY, | |
| } | |
| ) | |
| locations = s.get("https://api.clashofclans.com/v1/locations").json() | |
| if locations.get("reason") == "accessDenied.invalidIp": | |
| raise ValueError("API key IP not whitelisted") | |
| assert len(locations["paging"]["cursors"]) == 0 | |
| clanlabels_req = s.get("https://api.clashofclans.com/v1/labels/clans").json() | |
| assert len(clanlabels_req["paging"]["cursors"]) == 0 | |
| clanlabels = {label["name"]: label["id"] for label in clanlabels_req["items"]} | |
| assert set(clanlabels.keys()) == { | |
| "Trophy Pushing", | |
| "Clan War League", | |
| "Friendly", | |
| "Farming", | |
| "Underdog", | |
| "Base Designing", | |
| "Clan Wars", | |
| "Donations", | |
| "International", | |
| "Builder Base", | |
| "Competitive", | |
| "Friendly Wars", | |
| "Clan Games", | |
| "Talkative", | |
| "Newbie Friendly", | |
| "Clan Capital", | |
| "Relaxed", | |
| } | |
| finland_code = next( | |
| location["id"] for location in locations["items"] if location["name"] == "Finland" | |
| ) | |
| # Search clans | |
| finclans = s.get( | |
| "https://api.clashofclans.com/v1/clans", | |
| params={ | |
| "locationId": finland_code, | |
| }, | |
| ).json() | |
| """ | |
| Example clan: | |
| {'tag': '#28RVGJJPU', | |
| 'name': 'Tule klaaniin', | |
| 'type': 'open', | |
| 'location': {'id': 32000086, | |
| 'name': 'Finland', | |
| 'isCountry': True, | |
| 'countryCode': 'FI'}, | |
| 'isFamilyFriendly': False, | |
| 'badgeUrls': {'small': 'https://api-assets.clashofclans.com/badges/70/R5mkb7JcbtE2IwUVrgpQlbpDaN3PYsHKRrx7VVmzm68.png', | |
| 'large': 'https://api-assets.clashofclans.com/badges/512/R5mkb7JcbtE2IwUVrgpQlbpDaN3PYsHKRrx7VVmzm68.png', | |
| 'medium': 'https://api-assets.clashofclans.com/badges/200/R5mkb7JcbtE2IwUVrgpQlbpDaN3PYsHKRrx7VVmzm68.png'}, | |
| 'clanLevel': 1, | |
| 'clanPoints': 389, | |
| 'clanBuilderBasePoints': 69, | |
| 'clanCapitalPoints': 0, | |
| 'capitalLeague': {'id': 85000000, 'name': 'Unranked'}, | |
| 'requiredTrophies': 0, | |
| 'warFrequency': 'always', | |
| 'warWinStreak': 0, | |
| 'warWins': 0, | |
| 'warTies': 0, | |
| 'warLosses': 0, | |
| 'isWarLogPublic': True, | |
| 'warLeague': {'id': 48000000, 'name': 'Unranked'}, | |
| 'members': 3, | |
| 'labels': [], | |
| 'requiredBuilderBaseTrophies': 0, | |
| 'requiredTownhallLevel': 1}, | |
| """ | |
| SearchFilter = tuple[int, int] | str | None | set | Callable[[Any], bool] | |
| def do_filter(filter: SearchFilter, value: Any) -> bool: | |
| if filter is None: | |
| return True | |
| if isinstance(filter, int): | |
| return value == filter | |
| if isinstance(filter, str): | |
| # Regex match | |
| return re.match(filter, value) is not None | |
| if isinstance(filter, tuple): | |
| return filter[0] <= value <= filter[1] | |
| if isinstance(filter, set): | |
| return value in filter | |
| if callable(filter): | |
| return filter(value) | |
| raise ValueError(f"Invalid filter: {filter}") | |
| # inclusive filter ranges | |
| search_ranges: dict["str", SearchFilter] = { | |
| "members": (lambda x: 35 <= x <= 49), | |
| "requiredTownhallLevel": (0, 10), | |
| # "requiredTrophies": (1000, 1800), | |
| "type": "open", | |
| "chatLanguage": (lambda x: x is None or x["languageCode"] == 'FI'), | |
| # "isFamilyFriendly": (lambda x: x is False) | |
| } | |
| filtered_clans = [ | |
| clan | |
| for clan in finclans["items"] | |
| if all( | |
| do_filter(filt, clan.get(key)) | |
| for key, filt in search_ranges.items() | |
| ) | |
| ] | |
| # Request for descriptions | |
| full_clans = [ | |
| s.get(f"https://api.clashofclans.com/v1/clans/{quote(clan['tag'])}").json() | |
| for clan in filtered_clans | |
| ] | |
| def reformat_clan(clan: dict[str, Any]) -> dict[str, Any]: | |
| # shallow copy clan | |
| clan = dict(clan) | |
| clan['labels'] = [label['name'] for label in clan['labels']] | |
| del clan['badgeUrls'] | |
| clan['location'] = clan['location']['name'] | |
| clan['warLeague'] = clan['warLeague']['name'] | |
| clan['capitalLeague'] = clan['capitalLeague']['name'] | |
| clan['chatLanguage'] = clan.get('chatLanguage', {}).get('name') | |
| if 'clanCapital' in clan: | |
| clan['clanCapitalLevels'] = { | |
| district['name']: district['districtHallLevel'] | |
| for district in clan['clanCapital']['districts'] | |
| } | |
| del clan['clanCapital'] | |
| clan['trophies'] = { | |
| 'quantiles': statistics.quantiles([member['trophies'] for member in clan['memberList']], n=4), | |
| 'quantilesBuilderBase': statistics.quantiles([member['builderBaseTrophies'] for member in clan['memberList']], n=4), | |
| } | |
| # hide list of members | |
| del clan['memberList'] | |
| return clan | |
| from devtools import debug | |
| format_clans = [reformat_clan(clan) for clan in full_clans] | |
| # Sort clans by lowest quantile | |
| format_clans.sort(key=lambda x: -x['trophies']['quantiles'][0]) | |
| # Take only clans where 50% quantile is between 1700 and 2100 | |
| format_clans = [ | |
| clan | |
| for clan in format_clans | |
| if 1700 <= clan['trophies']['quantiles'][1] <= 2100 | |
| ] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment