Skip to content

Commit 68116ac

Browse files
authored
Update nextdns_sync.py
1 parent 9056650 commit 68116ac

1 file changed

Lines changed: 64 additions & 86 deletions

File tree

nextdns_sync.py

Lines changed: 64 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,34 @@
11
import requests
22
import json
33
import logging
4+
from typing import Optional, Dict, List, Union
45

5-
6+
# Configuration
67
TIMEOUT = 10
78
NEXT_DNS_API = "https://api.nextdns.io"
89
API_PROFILE_ROUTE = "profiles"
910
API_KEY = "REMOVED"
1011

1112
PROFILE_MAIN = "REMOVED"
12-
PROFILE_RPM = "REMOVED"
13-
PROFILE_BETSY = "REMOVED"
14-
PROFILE_LEONA = "REMOVED"
15-
PROFILE_HURLEY = "REMOVED"
16-
profile_sync_list = [PROFILE_RPM, PROFILE_BETSY, PROFILE_LEONA, PROFILE_HURLEY]
13+
PROFILE_SYNC_LIST = [
14+
"REMOVED", "REMOVED", "REMOVED", "REMOVED"
15+
]
1716

18-
TLD_BAN_LIST = [
17+
TLD_BAN_LIST = sorted(set([
1918
"autos", "best", "bid", "bio", "boats", "boston", "boutique", "charity",
2019
"christmas", "dance", "fishing", "hair", "haus", "loan", "loans", "men",
2120
"mom", "name", "review", "rip", "skin", "support", "tattoo", "tokyo",
2221
"voto", "sbs", "ooo", "gdn", "zip"
23-
]
24-
TLD_BAN_LIST = sorted(set(TLD_BAN_LIST))
22+
]))
2523
TLD_BAN_LIST_DICT = [{'id': tld} for tld in TLD_BAN_LIST]
2624
TLD_BAN_PAYLOAD = {"tlds": TLD_BAN_LIST_DICT}
2725

28-
headers = {
26+
HEADERS = {
2927
"X-Api-Key": API_KEY,
3028
"Content-Type": "application/json"
3129
}
3230

33-
def setup_logger(name):
31+
def setup_logger(name: str) -> logging.Logger:
3432
"""Sets up a logger with a given name."""
3533
logger = logging.getLogger(name)
3634
logger.setLevel(logging.DEBUG) # Set to DEBUG to capture all messages
@@ -46,134 +44,116 @@ def setup_logger(name):
4644

4745
return logger
4846

49-
# Create a logger instance with the current module name
5047
logger = setup_logger(__name__)
5148

52-
def api_request(method, url, headers=None, data=None, json=None, timeout=10):
49+
def api_request(method: str, url: str, headers: Optional[Dict[str, str]] = None,
50+
data: Optional[Union[Dict, str]] = None, json: Optional[Dict] = None,
51+
timeout: int = TIMEOUT) -> requests.Response:
5352
"""
54-
Makes an HTTP request and handles 400 Bad Request errors by throwing the full response.
55-
53+
Makes an HTTP request and handles errors.
54+
5655
Args:
5756
method (str): HTTP method (GET, POST, PATCH, etc.).
5857
url (str): The endpoint URL.
59-
headers (dict): Optional headers to include in the request.
60-
data (dict): Data to send in the body of the request (for POST, PATCH).
61-
json (dict): JSON data to send in the body of the request (for POST, PATCH).
58+
headers (dict, optional): Headers to include in the request.
59+
data (dict, optional): Data to send in the body of the request (for POST, PATCH).
60+
json (dict, optional): JSON data to send in the body of the request (for POST, PATCH).
6261
timeout (int): Timeout for the request.
63-
62+
6463
Returns:
65-
dict: The JSON response if the request is successful.
66-
64+
Response: The HTTP response object.
65+
6766
Raises:
6867
Exception: If the request returns a 400 Bad Request, or any other HTTP error.
6968
"""
7069
logger.info("[API CALL] USING %s", method)
7170
if data is not None:
72-
logger.info("[API CALL] Using payload {json.dumps(data, separators=(',', ':'))}")
73-
response = requests.request(method, url, headers=headers, data=data, json=json, timeout=timeout)
74-
response_code = response.status_code
75-
if response_code == 400:
76-
raise Exception(f"[API CALL] Bad Request: {response.status_code} - {response.text}")
77-
response.raise_for_status()
78-
if response_code not in (200, 204):
79-
logger.info("[API CALL] Response: %s", response)
80-
else:
81-
logger.info("[API CALL] Request succeeded")
82-
return response
83-
84-
# Using make_request in fetch_profile_settings
85-
def fetch_profile_settings(profile_id):
86-
"""Fetches the settings for a given profile ID and returns the allowlist and denylist."""
87-
url = f"{NEXT_DNS_API}/{API_PROFILE_ROUTE}/{profile_id}/"
88-
return api_request("GET", url, headers=headers, timeout=TIMEOUT)
71+
logger.info("[API CALL] Using payload %s", json.dumps(data, separators=(',', ':')))
72+
73+
try:
74+
response = requests.request(method, url, headers=headers, data=data, json=json, timeout=timeout)
75+
response.raise_for_status()
76+
if response.status_code not in (200, 204):
77+
logger.info("[API CALL] Response: %s", response.text)
78+
else:
79+
logger.info("[API CALL] Request succeeded")
80+
return response
81+
except requests.exceptions.RequestException as e:
82+
logger.error("[API CALL] Request failed: %s", e)
83+
raise
8984

85+
def fetch_profile_settings(profile_id: str) -> Dict:
86+
"""Fetches the settings for a given profile ID."""
87+
url = f"{NEXT_DNS_API}/{API_PROFILE_ROUTE}/{profile_id}/"
88+
response = api_request("GET", url, headers=HEADERS)
89+
return response.json()
9090

91-
def filter_blocklists(blocklists):
91+
def filter_blocklists(blocklists: List[Dict[str, str]]) -> List[Dict[str, str]]:
9292
"""Filter blocklists to only include the 'id' field."""
9393
return [{"id": blocklist.get("id")} for blocklist in blocklists if blocklist.get("id")]
9494

95-
96-
def build_payload(data, keys_to_sync):
95+
def build_payload(data: Dict, keys_to_sync: List[str]) -> Dict:
96+
"""Build payload for syncing settings."""
9797
return {key: data.get(key, []) for key in keys_to_sync if data.get(key) is not None}
9898

99-
100-
def alpha_sort_lists(data):
99+
def alpha_sort_lists(data: Dict) -> Dict:
101100
"""Sorts the allowlist and denylist alphabetically by 'id'."""
102101
if "allowlist" in data:
103102
data["allowlist"] = sorted(data["allowlist"], key=lambda x: x['id'])
104103
if "denylist" in data:
105104
data["denylist"] = sorted(data["denylist"], key=lambda x: x['id'])
106105
return data
107106

108-
109-
# Using make_request in update_profile_settings
110-
def update_profile_settings(profile_id, payload, route=None, method="PATCH"):
111-
"""Updates the settings for a given profile ID using the provided allowlist and denylist."""
112-
logger.info(f"[UPDATE-PROFILE] Updating profile settings for profile {profile_id}...")
113-
if route is None:
114-
url = f"{NEXT_DNS_API}/{API_PROFILE_ROUTE}/{profile_id}/settings"
115-
else:
116-
url = f"{NEXT_DNS_API}/{API_PROFILE_ROUTE}/{profile_id}/{route}"
117-
107+
def update_profile_settings(profile_id: str, payload: Dict, route: Optional[str] = None, method: str = "PATCH") -> requests.Response:
108+
"""Updates the settings for a given profile ID."""
109+
logger.info("[UPDATE-PROFILE] Updating profile settings for profile %s...", profile_id)
110+
url = f"{NEXT_DNS_API}/{API_PROFILE_ROUTE}/{profile_id}/{route if route else 'settings'}"
118111
if payload is None:
119112
raise ValueError("[UPDATE-PROFILE] Payload cannot be None. Please provide a valid payload.")
120-
return api_request(method, url, headers=headers, json=payload, timeout=TIMEOUT)
121-
113+
return api_request(method, url, headers=HEADERS, json=payload)
122114

123-
def update_array_settings(profile_id, key, payload, route=None, method="PUT"):
115+
def update_array_settings(profile_id: str, key: str, payload: List[Dict], route: Optional[str] = None, method: str = "PUT") -> requests.Response:
124116
"""Updates array settings like denylist or blocklists."""
125-
logger.info(f"[UPDATE-ARRAY] Updating array settings for profile {profile_id}...")
126-
if route is None:
127-
url = f"{NEXT_DNS_API}/{API_PROFILE_ROUTE}/{profile_id}/{key}"
128-
else:
129-
url = f"{NEXT_DNS_API}/{API_PROFILE_ROUTE}/{profile_id}/{route}/{key}"
130-
117+
logger.info("[UPDATE-ARRAY] Updating array settings for profile %s...", profile_id)
118+
url = f"{NEXT_DNS_API}/{API_PROFILE_ROUTE}/{profile_id}/{route if route else key}"
131119
if not isinstance(payload, list):
132120
raise ValueError("[UPDATE-ARRAY] Payload must be a list for array updates.")
133121
logger.info("[UPDATE-ARRAY] Url for [%s] is %s", profile_id, url)
134-
return api_request(method, url, headers=headers, json=payload, timeout=TIMEOUT)
135-
122+
return api_request(method, url, headers=HEADERS, json=payload)
136123

137-
def update_security_settings(profile_id, tlds_payload, method="PATCH"):
124+
def update_security_settings(profile_id: str, tlds_payload: Dict, method: str = "PATCH") -> requests.Response:
125+
"""Updates security settings for the profile."""
138126
url = f"{NEXT_DNS_API}/{API_PROFILE_ROUTE}/{profile_id}/security"
139-
140-
logger.info(f"[UPDATE-SECURITY] Updating security settings for profile {profile_id}...")
141-
logger.info(f"[UPDATE-SECURITY] url for [{profile_id}] is {url}")
127+
logger.info("[UPDATE-SECURITY] Updating security settings for profile %s...", profile_id)
142128
try:
143-
response = api_request(method, url, headers=headers, json=tlds_payload, timeout=TIMEOUT)
129+
response = api_request(method, url, headers=HEADERS, json=tlds_payload)
144130
logger.info("[UPDATE-SECURITY] Request succeeded")
145131
return response
146132
except Exception as e:
147-
logger.error(f"[UPDATE-SECURITY] Failed to update security settings: {e}")
133+
logger.error("[UPDATE-SECURITY] Failed to update security settings: %s", e)
148134
raise
149135

150-
151-
def sync_profiles(keys_to_sync, payload=None):
152-
"""Syncs the allowlist, denylist, and other settings from the main profile to the target profiles."""
136+
def sync_profiles(keys_to_sync: List[str], payload: Optional[Dict] = None) -> None:
137+
"""Syncs settings from the main profile to the target profiles."""
153138
try:
154-
settings = fetch_profile_settings(PROFILE_MAIN).json()
139+
settings = fetch_profile_settings(PROFILE_MAIN)
155140
data = settings['data']
156-
logger.info("[SYNC] Settings from Profile [PROFILE_MAIN]: %s", PROFILE_MAIN)
141+
logger.info("[SYNC] Settings from Profile %s", PROFILE_MAIN)
157142

158143
if payload is None:
159144
payload = build_payload(data, keys_to_sync)
160-
# Alpha sort allowlist and denylist
145+
161146
payload = alpha_sort_lists(payload)
162-
logger.info("[SYNC] Generated Payload:")
163-
logger.info(f"[SYNC] Using payload {json.dumps(data, separators=(',', ':'))}")
147+
logger.info("[SYNC] Generated Payload: %s", json.dumps(payload, separators=(',', ':')))
164148

165-
for profile_id in profile_sync_list:
149+
for profile_id in PROFILE_SYNC_LIST:
166150
for key in keys_to_sync:
167151
if key in payload:
168152
key_payload = payload[key]
169153
logger.info("[SYNC] Using Key [%s]", key)
170-
logger.debug(f"[SYNC] Payload is {key_payload}")
171154
if "blocklists" in key_payload:
172155
logger.info("[SYNC] [blocklists] to be filtered")
173-
logger.debug("[SYNC] Pre Filter of [privacy][blocklists]")
174156
key_payload["blocklists"] = filter_blocklists(key_payload["blocklists"])
175-
logger.debug("[SYNC] Filtered [blocklists]")
176-
logger.debug(key_payload["blocklists"])
177157
try:
178158
if isinstance(key_payload, list):
179159
update_array_settings(profile_id, key, key_payload)
@@ -187,11 +167,9 @@ def sync_profiles(keys_to_sync, payload=None):
187167
logger.error("[SYNC] Failed to sync profiles: %s", e)
188168
raise
189169

190-
191170
# Run the sync process
192171
keys_to_sync = ["allowlist", "denylist", "parentalControl", "security", "privacy"]
193172
sync_profiles(keys_to_sync)
194173

195174
# Ad Hoc Main Updates
196-
## TLDs
197-
# update_security_settings(PROFILE_MAIN, TLD_BAN_PAYLOAD)
175+
update_security_settings(PROFILE_MAIN, TLD_BAN_PAYLOAD)

0 commit comments

Comments
 (0)