11import requests
22import json
33import logging
4+ from typing import Optional , Dict , List , Union
45
5-
6+ # Configuration
67TIMEOUT = 10
78NEXT_DNS_API = "https://api.nextdns.io"
89API_PROFILE_ROUTE = "profiles"
910API_KEY = "REMOVED"
1011
1112PROFILE_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+ ]))
2523TLD_BAN_LIST_DICT = [{'id' : tld } for tld in TLD_BAN_LIST ]
2624TLD_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
5047logger = 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
192171keys_to_sync = ["allowlist" , "denylist" , "parentalControl" , "security" , "privacy" ]
193172sync_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