ddns 4.1.0b1__py2.py3-none-any.whl → 4.1.0b3__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ddns might be problematic. Click here for more details.

@@ -0,0 +1,130 @@
1
+ # coding=utf-8
2
+ """
3
+ AliESA API
4
+ 阿里云边缘安全加速(ESA) DNS 解析操作库
5
+ @author: NewFuture, GitHub Copilot
6
+ """
7
+
8
+ from time import strftime
9
+
10
+ from ._base import TYPE_JSON, join_domain
11
+ from .alidns import AliBaseProvider
12
+
13
+
14
+ class AliesaProvider(AliBaseProvider):
15
+ """阿里云边缘安全加速(ESA) DNS Provider"""
16
+
17
+ endpoint = "https://esa.cn-hangzhou.aliyuncs.com"
18
+ api_version = "2024-09-10" # ESA API版本
19
+ content_type = TYPE_JSON
20
+ remark = "Managed by DDNS %s" % strftime("%Y-%m-%d %H:%M:%S")
21
+
22
+ def _query_zone_id(self, domain):
23
+ # type: (str) -> str | None
24
+ """
25
+ 查询站点ID
26
+ https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-listsites
27
+ """
28
+ res = self._request(method="GET", action="ListSites", SiteName=domain, PageSize=500)
29
+ sites = res.get("Sites", [])
30
+
31
+ for site in sites:
32
+ if site.get("SiteName") == domain:
33
+ site_id = site.get("SiteId")
34
+ self.logger.debug("Found site ID %s for domain %s", site_id, domain)
35
+ return site_id
36
+
37
+ self.logger.error("Site not found for domain: %s", domain)
38
+ return None
39
+
40
+ def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
41
+ # type: (str, str, str, str, str | None, dict) -> dict | None
42
+ """
43
+ 查询DNS记录
44
+ https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-listrecords
45
+ """
46
+ full_domain = join_domain(subdomain, main_domain)
47
+ res = self._request(
48
+ method="GET",
49
+ action="ListRecords",
50
+ SiteId=int(zone_id),
51
+ RecordName=full_domain,
52
+ Type=self._get_type(record_type),
53
+ RecordMatchType="exact", # 精确匹配
54
+ PageSize=100,
55
+ )
56
+
57
+ records = res.get("Records", [])
58
+ if len(records) == 0:
59
+ self.logger.warning("No records found for [%s] with %s <%s>", zone_id, full_domain, record_type)
60
+ return None
61
+
62
+ # 返回第一个匹配的记录
63
+ record = records[0]
64
+ self.logger.debug("Found record: %s", record)
65
+ return record
66
+
67
+ def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
68
+ # type: (str, str, str, str, str, int | str | None, str | None, dict) -> bool
69
+ """
70
+ 创建DNS记录
71
+ https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-createrecord
72
+ """
73
+ full_domain = join_domain(subdomain, main_domain)
74
+ extra["Comment"] = extra.get("Comment", self.remark)
75
+ extra["BizName"] = extra.get("BizName", "web")
76
+ extra["Proxied"] = extra.get("Proxied", True)
77
+ data = self._request(
78
+ method="POST",
79
+ action="CreateRecord",
80
+ SiteId=int(zone_id),
81
+ RecordName=full_domain,
82
+ Type=self._get_type(record_type),
83
+ Data={"Value": value},
84
+ Ttl=ttl or 1,
85
+ **extra
86
+ ) # fmt: skip
87
+
88
+ if data and data.get("RecordId"):
89
+ self.logger.info("Record created: %s", data)
90
+ return True
91
+
92
+ self.logger.error("Failed to create record: %s", data)
93
+ return False
94
+
95
+ def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
96
+ # type: (str, dict, str, str, int | str | None, str | None, dict) -> bool
97
+ """
98
+ 更新DNS记录
99
+ https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-updaterecord
100
+ """
101
+ # 检查是否需要更新
102
+ if (
103
+ old_record.get("Data", {}).get("Value") == value
104
+ and old_record.get("RecordType") == self._get_type(record_type)
105
+ and (not ttl or old_record.get("Ttl") == ttl)
106
+ ):
107
+ self.logger.warning("No changes detected, skipping update for record: %s", old_record.get("RecordName"))
108
+ return True
109
+
110
+ extra["Comment"] = extra.get("Comment", self.remark)
111
+ extra["Proxied"] = extra.get("Proxied", old_record.get("Proxied"))
112
+ data = self._request(
113
+ method="POST",
114
+ action="UpdateRecord",
115
+ RecordId=old_record.get("RecordId"),
116
+ Data={"Value": value},
117
+ Ttl=ttl,
118
+ **extra
119
+ ) # fmt: skip
120
+
121
+ if data and data.get("RecordId"):
122
+ self.logger.info("Record updated: %s", data)
123
+ return True
124
+
125
+ self.logger.error("Failed to update record: %s", data)
126
+ return False
127
+
128
+ def _get_type(self, record_type):
129
+ # type: (str) -> str
130
+ return "A/AAAA" if record_type in ("A", "AAAA") else record_type
ddns/provider/callback.py CHANGED
@@ -5,6 +5,7 @@ Custom Callback API
5
5
 
6
6
  @author: 老周部落, NewFuture
7
7
  """
8
+
8
9
  from ._base import TYPE_JSON, SimpleProvider
9
10
  from time import time
10
11
  from json import loads as jsondecode
@@ -16,7 +17,7 @@ class CallbackProvider(SimpleProvider):
16
17
  Generic custom callback provider, supports GET/POST arbitrary API.
17
18
  """
18
19
 
19
- API = "" # CallbackProvider uses auth_id as URL, no fixed API endpoint
20
+ endpoint = "" # CallbackProvider uses id as URL, no fixed API endpoint
20
21
  content_type = TYPE_JSON
21
22
  decode_response = False # Callback response is not JSON, it's a custom response
22
23
 
@@ -26,9 +27,8 @@ class CallbackProvider(SimpleProvider):
26
27
  Send custom callback request, support GET/POST
27
28
  """
28
29
  self.logger.info("%s => %s(%s)", domain, value, record_type)
29
- url = self.auth_id # 直接用 auth_id 作为 url
30
- token = self.auth_token # auth_token 作为 POST 参数
31
- headers = {"User-Agent": "DDNS/{0} (ddns@newfuture.cc)".format(self.version)}
30
+ url = self.id # 直接用 id 作为 url
31
+ token = self.token # token 作为 POST 参数
32
32
  extra.update(
33
33
  {
34
34
  "__DOMAIN__": domain,
@@ -51,7 +51,7 @@ class CallbackProvider(SimpleProvider):
51
51
  params[k] = self._replace_vars(v, extra)
52
52
 
53
53
  try:
54
- res = self._http(method, url, body=params, headers=headers)
54
+ res = self._http(method, url, body=params)
55
55
  if res is not None:
56
56
  self.logger.info("Callback result: %s", res)
57
57
  return True
@@ -72,8 +72,8 @@ class CallbackProvider(SimpleProvider):
72
72
  return string
73
73
 
74
74
  def _validate(self):
75
- # CallbackProvider uses auth_id as URL, not as regular ID
76
- if not self.auth_id or "://" not in self.auth_id:
77
- self.logger.critical("callback ID 参数[%s] 必须是有效的URL", self.auth_id)
78
- raise ValueError("id must be configured with URL")
79
- # CallbackProvider doesn't need auth_token validation (it can be empty)
75
+ # CallbackProvider uses id as URL, not as regular ID
76
+ if self.endpoint or (not self.id or "://" not in self.id):
77
+ # 如果 endpoint 已经设置,或者 id 不是有效的 URL,则抛出异常
78
+ self.logger.critical("endpoint [%s] or id [%s] 必须是有效的URL", self.endpoint, self.id)
79
+ raise ValueError("endpoint or id must be configured with URL")
@@ -4,30 +4,30 @@ CloudFlare API
4
4
  @author: TongYifan, NewFuture
5
5
  """
6
6
 
7
- from ._base import BaseProvider, TYPE_JSON, join_domain
7
+ from ._base import TYPE_JSON, BaseProvider, join_domain
8
8
 
9
9
 
10
10
  class CloudflareProvider(BaseProvider):
11
- API = "https://api.cloudflare.com"
11
+ endpoint = "https://api.cloudflare.com"
12
12
  content_type = TYPE_JSON
13
13
 
14
14
  def _validate(self):
15
- if not self.auth_token:
15
+ if not self.token:
16
16
  raise ValueError("token must be configured")
17
- if self.auth_id:
17
+ if self.id:
18
18
  # must be email for Cloudflare API v4
19
- if "@" not in self.auth_id:
19
+ if "@" not in self.id:
20
20
  self.logger.critical("ID 必须为空或有效的邮箱地址")
21
21
  raise ValueError("ID must be a valid email or Empty for Cloudflare API v4")
22
22
 
23
23
  def _request(self, method, action, **params):
24
24
  """发送请求数据"""
25
25
  headers = {}
26
- if self.auth_id:
27
- headers["X-Auth-Email"] = self.auth_id
28
- headers["X-Auth-Key"] = self.auth_token
26
+ if self.id:
27
+ headers["X-Auth-Email"] = self.id
28
+ headers["X-Auth-Key"] = self.token
29
29
  else:
30
- headers["Authorization"] = "Bearer " + self.auth_token
30
+ headers["Authorization"] = "Bearer " + self.token
31
31
 
32
32
  params = {k: v for k, v in params.items() if v is not None} # 过滤掉None参数
33
33
  data = self._http(method, "/client/v4/zones" + action, headers=headers, params=params)
@@ -92,7 +92,7 @@ class CloudflareProvider(BaseProvider):
92
92
  content=value,
93
93
  ttl=ttl,
94
94
  **extra
95
- )
95
+ ) # fmt: skip
96
96
  self.logger.debug("Record updated: %s", data)
97
97
  if data:
98
98
  return True
ddns/provider/debug.py CHANGED
@@ -8,7 +8,6 @@ from ._base import SimpleProvider
8
8
 
9
9
 
10
10
  class DebugProvider(SimpleProvider):
11
-
12
11
  def _validate(self):
13
12
  """无需任何验证"""
14
13
  pass
ddns/provider/dnscom.py CHANGED
@@ -5,10 +5,11 @@ www.51dns.com (原dns.com)
5
5
  @author: Bigjin<i@bigjin.com>, NewFuture
6
6
  """
7
7
 
8
- from ._base import BaseProvider, TYPE_FORM
9
8
  from hashlib import md5
10
9
  from time import time
11
10
 
11
+ from ._base import TYPE_FORM, BaseProvider, encode_params
12
+
12
13
 
13
14
  class DnscomProvider(BaseProvider):
14
15
  """
@@ -16,7 +17,7 @@ class DnscomProvider(BaseProvider):
16
17
  https://www.51dns.com/document/api/index.html
17
18
  """
18
19
 
19
- API = "https://www.51dns.com"
20
+ endpoint = "https://www.51dns.com"
20
21
  content_type = TYPE_FORM
21
22
 
22
23
  def _validate(self):
@@ -31,12 +32,12 @@ class DnscomProvider(BaseProvider):
31
32
  params = {k: v for k, v in params.items() if v is not None}
32
33
  params.update(
33
34
  {
34
- "apiKey": self.auth_id,
35
+ "apiKey": self.id,
35
36
  "timestamp": time(), # 时间戳
36
37
  }
37
38
  )
38
- query = self._encode(sorted(params.items()))
39
- sign = md5((query + self.auth_token).encode("utf-8")).hexdigest()
39
+ query = encode_params(params)
40
+ sign = md5((query + self.token).encode("utf-8")).hexdigest()
40
41
  params["hash"] = sign
41
42
  return params
42
43
 
@@ -82,7 +83,7 @@ class DnscomProvider(BaseProvider):
82
83
  TTL=ttl,
83
84
  viewID=line,
84
85
  **extra
85
- )
86
+ ) # fmt: skip
86
87
  if res and res.get("recordID"):
87
88
  self.logger.info("Record created: %s", res)
88
89
  return True
ddns/provider/dnspod.py CHANGED
@@ -14,7 +14,7 @@ class DnspodProvider(BaseProvider):
14
14
  DNSPOD 接口解析操作库
15
15
  """
16
16
 
17
- API = "https://dnsapi.cn"
17
+ endpoint = "https://dnsapi.cn"
18
18
  content_type = TYPE_FORM
19
19
 
20
20
  DefaultLine = "默认"
@@ -36,9 +36,8 @@ class DnspodProvider(BaseProvider):
36
36
  if extra:
37
37
  params.update(extra)
38
38
  params = {k: v for k, v in params.items() if v is not None}
39
- params.update({"login_token": "{0},{1}".format(self.auth_id, self.auth_token), "format": "json"})
40
- headers = {"User-Agent": "DDNS/{0} (ddns@newfuture.cc)".format(self.version)}
41
- data = self._http("POST", "/" + action, headers=headers, body=params)
39
+ params.update({"login_token": "{0},{1}".format(self.id, self.token), "format": "json"})
40
+ data = self._http("POST", "/" + action, body=params)
42
41
  if data and data.get("status", {}).get("code") == "1": # 请求成功
43
42
  return data
44
43
  else: # 请求失败
@@ -102,9 +101,7 @@ class DnspodProvider(BaseProvider):
102
101
  def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
103
102
  # type: (str, str, str, str, str | None, dict) -> dict | None
104
103
  """查询记录 list 然后逐个查找 https://docs.dnspod.cn/api/record-list/"""
105
- res = self._request(
106
- "Record.List", domain_id=zone_id, sub_domain=subdomain, record_type=record_type, line=line
107
- )
104
+ res = self._request("Record.List", domain_id=zone_id, sub_domain=subdomain, record_type=record_type, line=line)
108
105
  # length="3000"
109
106
  records = res.get("records", [])
110
107
  n = len(records)
@@ -14,5 +14,5 @@ class DnspodComProvider(DnspodProvider):
14
14
  This class extends the DnspodProvider to use the global DNSPOD API.
15
15
  """
16
16
 
17
- API = "https://api.dnspod.com"
17
+ endpoint = "https://api.dnspod.com"
18
18
  DefaultLine = "default"
@@ -0,0 +1,83 @@
1
+ # coding=utf-8
2
+ """
3
+ Tencent Cloud EdgeOne API
4
+ 腾讯云 EdgeOne (边缘安全速平台) API
5
+ API Documentation: https://cloud.tencent.com/document/api/1552/80731
6
+ @author: NewFuture
7
+ """
8
+
9
+ from ddns.provider._base import join_domain
10
+ from .tencentcloud import TencentCloudProvider
11
+
12
+
13
+ class EdgeOneProvider(TencentCloudProvider):
14
+ """
15
+ 腾讯云 EdgeOne API 提供商
16
+ Tencent Cloud EdgeOne API Provider
17
+ """
18
+
19
+ endpoint = "https://teo.tencentcloudapi.com"
20
+ # 腾讯云 EdgeOne API 配置
21
+ service = "teo"
22
+ version_date = "2022-09-01"
23
+
24
+ def _query_zone_id(self, domain):
25
+ # type: (str) -> str | None
26
+ """查询域名的加速域名信息获取 ZoneId https://cloud.tencent.com/document/api/1552/80713"""
27
+ # 首先尝试直接查找域名
28
+ filters = [{"Name": "zone-name", "Values": [domain], "Fuzzy": False}] # type: Any
29
+ response = self._request("DescribeZones", Filters=filters)
30
+
31
+ if response and "Zones" in response:
32
+ for zone in response.get("Zones", []):
33
+ if zone.get("ZoneName") == domain:
34
+ zone_id = zone.get("ZoneId")
35
+ if zone_id:
36
+ self.logger.debug("Found acceleration domain %s with Zone ID: %s", domain, zone_id)
37
+ return zone_id
38
+
39
+ self.logger.debug("Acceleration domain not found for: %s", domain)
40
+ return None
41
+
42
+ def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
43
+ # type: (str, str, str, str, str | None, dict) -> dict | None
44
+ """查询加速域名信息 https://cloud.tencent.com/document/api/1552/86336"""
45
+ domain = join_domain(subdomain, main_domain)
46
+ filters = [{"Name": "domain-name", "Values": [domain], "Fuzzy": False}] # type: Any
47
+ response = self._request("DescribeAccelerationDomains", ZoneId=zone_id, Filters=filters)
48
+
49
+ if response and "AccelerationDomains" in response:
50
+ for domain_info in response.get("AccelerationDomains", []):
51
+ if domain_info.get("DomainName") == domain:
52
+ self.logger.debug("Found acceleration domain: %s", domain_info)
53
+ return domain_info
54
+
55
+ self.logger.warning("No acceleration domain found for: %s, response: %s", domain, response)
56
+ return None
57
+
58
+ def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
59
+ # type: (str, str, str, str, str, int, str | None, dict) -> bool
60
+ """创建新的加速域名记录 https://cloud.tencent.com/document/api/1552/86338"""
61
+ domain = join_domain(subdomain, main_domain)
62
+ origin = {"OriginType": "IP_DOMAIN", "Origin": value} # type: Any
63
+ res = self._request("CreateAccelerationDomain", ZoneId=zone_id, DomainName=domain, OriginInfo=origin, **extra)
64
+ if res:
65
+ self.logger.info("Acceleration domain created (%s)", res.get("RequestId"))
66
+ return True
67
+
68
+ self.logger.error("Failed to create acceleration domain, response: %s", res)
69
+ return False
70
+
71
+ def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
72
+ """更新加速域名的源站 IP 地址 https://cloud.tencent.com/document/api/1552/86335"""
73
+ domain = old_record.get("DomainName")
74
+ # 构建源站信息
75
+ backup = old_record.get("OriginDetail", {}).get("BackupOrigin", "")
76
+ origin = {"OriginType": "IP_DOMAIN", "Origin": value, "BackupOrigin": backup} # type: Any
77
+ response = self._request("ModifyAccelerationDomain", ZoneId=zone_id, DomainName=domain, OriginInfo=origin)
78
+
79
+ if response:
80
+ self.logger.info("Acceleration domain updated (%s)", response.get("RequestId"))
81
+ return True
82
+ self.logger.error("Failed to update acceleration domain origin, response: %s", response)
83
+ return False
ddns/provider/he.py CHANGED
@@ -8,7 +8,7 @@ from ._base import SimpleProvider, TYPE_FORM
8
8
 
9
9
 
10
10
  class HeProvider(SimpleProvider):
11
- API = "https://dyn.dns.he.net"
11
+ endpoint = "https://dyn.dns.he.net"
12
12
  content_type = TYPE_FORM
13
13
  accept = None # he.net does not require a specific Accept header
14
14
  decode_response = False # he.net response is plain text, not JSON
@@ -18,9 +18,9 @@ class HeProvider(SimpleProvider):
18
18
  "HE.net 缺少充分的真实环境测试,请及时在 GitHub Issues 中反馈: %s",
19
19
  "https://github.com/NewFuture/DDNS/issues",
20
20
  )
21
- if self.auth_id:
21
+ if self.id:
22
22
  raise ValueError("Hurricane Electric (he.net) does not use `id`, use `token(password)` only.")
23
- if not self.auth_token:
23
+ if not self.token:
24
24
  raise ValueError("Hurricane Electric (he.net) requires `token(password)`.")
25
25
 
26
26
  def set_record(self, domain, value, record_type="A", ttl=None, line=None, **extra):
@@ -32,7 +32,7 @@ class HeProvider(SimpleProvider):
32
32
  params = {
33
33
  "hostname": domain, # he.net requires full domain name
34
34
  "myip": value, # IP address to update
35
- "password": self.auth_token, # Use auth_token as password
35
+ "password": self.token, # Use token as password
36
36
  }
37
37
  try:
38
38
  res = self._http("POST", "/nic/update", body=params)
@@ -5,13 +5,14 @@ HuaweiDNS API
5
5
  @author: NewFuture
6
6
  """
7
7
 
8
- from ._base import BaseProvider, TYPE_JSON, hmac_sha256_authorization, sha256_hash, join_domain
9
- from json import dumps as jsonencode
10
- from time import strftime, gmtime
8
+ from time import gmtime, strftime
9
+
10
+ from ._base import TYPE_JSON, BaseProvider, encode_params, join_domain
11
+ from ._signature import hmac_sha256_authorization, sha256_hash
11
12
 
12
13
 
13
14
  class HuaweiDNSProvider(BaseProvider):
14
- API = "https://dns.myhuaweicloud.com"
15
+ endpoint = "https://dns.myhuaweicloud.com"
15
16
  content_type = TYPE_JSON
16
17
  algorithm = "SDK-HMAC-SHA256"
17
18
 
@@ -32,16 +33,16 @@ class HuaweiDNSProvider(BaseProvider):
32
33
  # type: (str, str, **Any) -> dict
33
34
  params = {k: v for k, v in params.items() if v is not None}
34
35
  if method.upper() == "GET" or method.upper() == "DELETE":
35
- query = self._encode(sorted(params.items()))
36
+ query = encode_params(params)
36
37
  body = ""
37
38
  else:
38
39
  query = ""
39
- body = jsonencode(params)
40
+ body = self._encode_body(params)
40
41
 
41
42
  now = strftime("%Y%m%dT%H%M%SZ", gmtime())
42
43
  headers = {
43
44
  "content-type": self.content_type,
44
- "host": self.API.split("://", 1)[1].strip("/"),
45
+ "host": self.endpoint.split("://", 1)[1].strip("/"),
45
46
  "X-Sdk-Date": now,
46
47
  }
47
48
 
@@ -51,10 +52,10 @@ class HuaweiDNSProvider(BaseProvider):
51
52
  sign_path = path if path.endswith("/") else path + "/"
52
53
  authorization_format = "%s Access=%s, SignedHeaders={SignedHeaders}, Signature={Signature}" % (
53
54
  self.algorithm,
54
- self.auth_id,
55
+ self.id,
55
56
  )
56
57
  authorization = hmac_sha256_authorization(
57
- secret_key=self.auth_token,
58
+ secret_key=self.token,
58
59
  method=method,
59
60
  path=sign_path,
60
61
  query=query,
@@ -114,7 +115,7 @@ class HuaweiDNSProvider(BaseProvider):
114
115
  ttl=ttl,
115
116
  line=line,
116
117
  **extra
117
- )
118
+ ) # fmt: skip
118
119
  if res and res.get("id"):
119
120
  self.logger.info("Record created: %s", res)
120
121
  return True
@@ -134,7 +135,7 @@ class HuaweiDNSProvider(BaseProvider):
134
135
  records=[value],
135
136
  ttl=ttl if ttl is not None else old_record.get("ttl"),
136
137
  **extra
137
- )
138
+ ) # fmt: skip
138
139
  if res and res.get("id"):
139
140
  self.logger.info("Record updated: %s", res)
140
141
  return True
@@ -0,0 +1,159 @@
1
+ # coding=utf-8
2
+ """
3
+ NameSilo API Provider
4
+ DNS provider implementation for NameSilo domain registrar
5
+ @doc: https://www.namesilo.com/api-reference
6
+ @author: NewFuture & Copilot
7
+ """
8
+
9
+ from ._base import BaseProvider, TYPE_JSON
10
+
11
+
12
+ class NamesiloProvider(BaseProvider):
13
+ """
14
+ NameSilo DNS API Provider
15
+
16
+ Supports DNS record management through NameSilo's API including:
17
+ - Domain information retrieval
18
+ - DNS record listing
19
+ - DNS record creation
20
+ - DNS record updating
21
+ """
22
+
23
+ endpoint = "https://www.namesilo.com"
24
+ content_type = TYPE_JSON
25
+
26
+ def _validate(self):
27
+ """Validate authentication credentials"""
28
+ # NameSilo only requires API key (token), not ID
29
+ if not self.token:
30
+ raise ValueError("API key (token) must be configured for NameSilo")
31
+ if not self.endpoint:
32
+ raise ValueError("API endpoint must be defined in {}".format(self.__class__.__name__))
33
+
34
+ # Warn if ID is configured since NameSilo doesn't need it
35
+ if self.id:
36
+ self.logger.warning("NameSilo does not require 'id' configuration - only API key (token) is needed")
37
+
38
+ # Show pending verification warning
39
+ self.logger.warning("NameSilo provider implementation is pending verification - please test thoroughly")
40
+
41
+ def _request(self, operation, **params):
42
+ # type: (str, **(str | int | bytes | bool | None)) -> dict|None
43
+ """
44
+ Send request to NameSilo API
45
+
46
+ Args:
47
+ operation (str): API operation name
48
+ params: API parameters
49
+
50
+ Returns:
51
+ dict: API response data
52
+ """
53
+ # Filter out None parameters
54
+ params = {k: v for k, v in params.items() if v is not None}
55
+
56
+ # Add required authentication and format parameters
57
+ params.update({"version": "1", "type": "json", "key": self.token})
58
+
59
+ # Make API request
60
+ response = self._http("GET", "/api/" + operation, queries=params)
61
+
62
+ # Parse response
63
+ if response and isinstance(response, dict):
64
+ reply = response.get("reply", {})
65
+
66
+ # Check for successful response
67
+ if reply.get("code") == "300": # NameSilo success code
68
+ return reply
69
+ else:
70
+ # Log error details
71
+ error_msg = reply.get("detail", "Unknown error")
72
+ self.logger.warning("NameSilo API error [%s]: %s", reply.get("code", "unknown"), error_msg)
73
+
74
+ return None
75
+
76
+ def _query_zone_id(self, domain):
77
+ # type: (str) -> str | None
78
+ """
79
+ Query domain information to get domain as zone identifier
80
+ @doc: https://www.namesilo.com/api-reference#domains/get-domain-info
81
+ """
82
+ response = self._request("getDomainInfo", domain=domain)
83
+
84
+ if response:
85
+ # Domain exists, return the domain name as zone_id
86
+ domain_info = response.get("domain", {})
87
+ if domain_info:
88
+ self.logger.debug("Domain found: %s", domain)
89
+ return domain
90
+
91
+ self.logger.warning("Domain not found or not accessible: %s", domain)
92
+ return None
93
+
94
+ def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
95
+ # type: (str, str, str, str, str | None, dict) -> dict | None
96
+ """
97
+ Query existing DNS record
98
+ @doc: https://www.namesilo.com/api-reference#dns/list-dns-records
99
+ """
100
+ response = self._request("dnsListRecords", domain=main_domain)
101
+
102
+ if response:
103
+ records = response.get("resource_record", [])
104
+
105
+ # Find matching record
106
+ for record in records:
107
+ if record.get("host") == subdomain and record.get("type") == record_type:
108
+ self.logger.debug("Found existing record: %s", record)
109
+ return record
110
+
111
+ self.logger.debug("No matching record found for %s.%s (%s)", subdomain, main_domain, record_type)
112
+ return None
113
+
114
+ def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
115
+ # type: (str, str, str, str, str, int | str | None, str | None, dict) -> bool
116
+ """
117
+ Create new DNS record
118
+ @doc: https://www.namesilo.com/api-reference#dns/add-dns-record
119
+ """
120
+ response = self._request(
121
+ "dnsAddRecord", domain=main_domain, rrtype=record_type, rrhost=subdomain, rrvalue=value, rrttl=ttl
122
+ )
123
+
124
+ if response:
125
+ record_id = response.get("record_id")
126
+ self.logger.info("DNS record created successfully: %s", record_id)
127
+ return True
128
+ else:
129
+ self.logger.error("Failed to create DNS record")
130
+ return False
131
+
132
+ def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
133
+ # type: (str, dict, str, str, int | str | None, str | None, dict) -> bool
134
+ """
135
+ Update existing DNS record
136
+ @doc: https://www.namesilo.com/api-reference#dns/update-dns-record
137
+ """
138
+ record_id = old_record.get("record_id")
139
+ if not record_id:
140
+ self.logger.error("No record_id found in old_record: %s", old_record)
141
+ return False
142
+
143
+ # In NameSilo, zone_id is the main domain name
144
+ response = self._request(
145
+ "dnsUpdateRecord",
146
+ rrid=record_id,
147
+ domain=zone_id, # zone_id is main_domain in NameSilo
148
+ rrhost=old_record.get("host"), # host field contains subdomain
149
+ rrvalue=value,
150
+ rrtype=record_type,
151
+ rrttl=ttl or old_record.get("ttl"),
152
+ )
153
+
154
+ if response:
155
+ self.logger.info("DNS record updated successfully: %s", record_id)
156
+ return True
157
+ else:
158
+ self.logger.error("Failed to update DNS record")
159
+ return False