mirror of
https://github.com/qdm12/ddns-updater.git
synced 2026-03-31 06:23:54 -04:00
feat(providers): support name.com (#474)
This commit is contained in:
@@ -57,6 +57,7 @@ Light container updating DNS A and/or AAAA records periodically for multiple DNS
|
||||
- INWX
|
||||
- Linode
|
||||
- LuaDNS
|
||||
- Name.com
|
||||
- Namecheap
|
||||
- Netcup
|
||||
- NoIP
|
||||
@@ -184,6 +185,7 @@ Check the documentation for your DNS provider:
|
||||
- [INWX](https://github.com/qdm12/ddns-updater/blob/master/docs/inwx.md)
|
||||
- [Linode](https://github.com/qdm12/ddns-updater/blob/master/docs/linode.md)
|
||||
- [LuaDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/luadns.md)
|
||||
- [Name.com](https://github.com/qdm12/ddns-updater/blob/master/docs/name.com.md)
|
||||
- [Namecheap](https://github.com/qdm12/ddns-updater/blob/master/docs/namecheap.md)
|
||||
- [Netcup](https://github.com/qdm12/ddns-updater/blob/master/docs/netcup.md)
|
||||
- [NoIP](https://github.com/qdm12/ddns-updater/blob/master/docs/noip.md)
|
||||
|
||||
33
docs/name.com.md
Normal file
33
docs/name.com.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Name.com
|
||||
|
||||
<a href="https://www.name.com"><img src="../readme/name.svg" alt="drawing" width="25%"/></a>
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "name.com",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"username": "username",
|
||||
"token": "token"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"username"` is your account username
|
||||
- `"token"` which you can obtain from [www.name.com/account/settings/api](https://www.name.com/account/settings/api)
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ttl"` is the time this record can be cached for in seconds. Name.com allows a minimum TTL of 300, or 5 minutes. Name.com defaults to 300 if not provided.
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
@@ -30,6 +30,7 @@ const (
|
||||
Linode models.Provider = "linode"
|
||||
LuaDNS models.Provider = "luadns"
|
||||
Namecheap models.Provider = "namecheap"
|
||||
NameCom models.Provider = "name.com"
|
||||
Netcup models.Provider = "netcup"
|
||||
Njalla models.Provider = "njalla"
|
||||
NoIP models.Provider = "noip"
|
||||
@@ -72,6 +73,7 @@ func ProviderChoices() []models.Provider {
|
||||
Linode,
|
||||
LuaDNS,
|
||||
Namecheap,
|
||||
NameCom,
|
||||
Njalla,
|
||||
NoIP,
|
||||
OpenDNS,
|
||||
|
||||
@@ -12,6 +12,7 @@ var (
|
||||
ErrConflictingRecord = errors.New("conflicting record")
|
||||
ErrDNSServerSide = errors.New("server side DNS error")
|
||||
ErrDomainDisabled = errors.New("record disabled")
|
||||
ErrDomainNotFound = errors.New("domain not found")
|
||||
ErrDomainIDNotFound = errors.New("ID not found in domain record")
|
||||
ErrFeatureUnavailable = errors.New("feature is not available to the user")
|
||||
ErrHostnameNotExists = errors.New("hostname does not exist")
|
||||
|
||||
@@ -18,6 +18,7 @@ var (
|
||||
ErrEmptyAccessKeyID = errors.New("empty access key id")
|
||||
ErrEmptyAccessKeySecret = errors.New("empty key secret")
|
||||
ErrEmptyTTL = errors.New("TTL is not set")
|
||||
ErrTTLTooLow = errors.New("TTL is too low")
|
||||
ErrEmptyUsername = errors.New("empty username")
|
||||
ErrEmptyZoneIdentifier = errors.New("empty zone identifier")
|
||||
ErrEmptyHost = errors.New("host cannot be empty")
|
||||
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/linode"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/luadns"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/namecheap"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/namecom"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/netcup"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/njalla"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/noip"
|
||||
@@ -120,6 +121,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, host string
|
||||
return luadns.New(data, domain, host, ipVersion)
|
||||
case constants.Namecheap:
|
||||
return namecheap.New(data, domain, host, ipVersion)
|
||||
case constants.NameCom:
|
||||
return namecom.New(data, domain, host, ipVersion)
|
||||
case constants.Netcup:
|
||||
return netcup.New(data, domain, host, ipVersion)
|
||||
case constants.Njalla:
|
||||
|
||||
65
internal/provider/providers/namecom/createrecord.go
Normal file
65
internal/provider/providers/namecom/createrecord.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package namecom
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/provider/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/errors"
|
||||
)
|
||||
|
||||
func (p *Provider) createRecord(ctx context.Context, client *http.Client,
|
||||
ip netip.Addr) (err error) {
|
||||
recordType := constants.A
|
||||
if ip.Is6() {
|
||||
recordType = constants.AAAA
|
||||
}
|
||||
|
||||
u := &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.name.com",
|
||||
Path: fmt.Sprintf("/v4/domains/%s/records", p.domain),
|
||||
User: url.UserPassword(p.username, p.token),
|
||||
}
|
||||
|
||||
postRecordsParams := struct {
|
||||
Host string `json:"host"`
|
||||
Type string `json:"type"`
|
||||
Answer string `json:"answer"`
|
||||
TTL *uint32 `json:"ttl,omitempty"`
|
||||
}{
|
||||
Host: p.host,
|
||||
Type: recordType,
|
||||
Answer: ip.String(),
|
||||
TTL: p.ttl,
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(postRecordsParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", errors.ErrRequestMarshal, err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewBuffer(bodyBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", errors.ErrBadRequest, err)
|
||||
}
|
||||
setHeaders(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("doing HTTP request: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
switch response.StatusCode {
|
||||
case http.StatusOK, http.StatusCreated:
|
||||
return verifySuccessResponseBody(response.Body, ip)
|
||||
default:
|
||||
return parseErrorResponse(response)
|
||||
}
|
||||
}
|
||||
64
internal/provider/providers/namecom/getrecord.go
Normal file
64
internal/provider/providers/namecom/getrecord.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package namecom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/provider/errors"
|
||||
)
|
||||
|
||||
func (p *Provider) getRecordID(ctx context.Context, client *http.Client,
|
||||
recordType string) (recordID int, err error) {
|
||||
u := &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.name.com",
|
||||
Path: fmt.Sprintf("/v4/domains/%s/records", p.domain),
|
||||
User: url.UserPassword(p.username, p.token),
|
||||
}
|
||||
|
||||
// by default GET request will return 1000 records.
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%w: %w", errors.ErrBadRequest, err)
|
||||
}
|
||||
setHeaders(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
switch response.StatusCode {
|
||||
case http.StatusOK:
|
||||
case http.StatusNotFound:
|
||||
return 0, fmt.Errorf("%w", errors.ErrDomainNotFound)
|
||||
default:
|
||||
return 0, parseErrorResponse(response)
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
var data struct {
|
||||
Records []struct {
|
||||
RecordID int `json:"id"`
|
||||
Host string `json:"host"`
|
||||
Type string `json:"type"`
|
||||
} `json:"records"`
|
||||
}
|
||||
err = decoder.Decode(&data)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
|
||||
}
|
||||
|
||||
for _, record := range data.Records {
|
||||
if record.Host == p.host && record.Type == recordType {
|
||||
return record.RecordID, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("%w: in %d record(s)",
|
||||
errors.ErrRecordNotFound, len(data.Records))
|
||||
}
|
||||
13
internal/provider/providers/namecom/headers.go
Normal file
13
internal/provider/providers/namecom/headers.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package namecom
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/provider/headers"
|
||||
)
|
||||
|
||||
func setHeaders(request *http.Request) {
|
||||
headers.SetContentType(request, "application/json")
|
||||
headers.SetAccept(request, "application/json")
|
||||
headers.SetUserAgent(request)
|
||||
}
|
||||
119
internal/provider/providers/namecom/provider.go
Normal file
119
internal/provider/providers/namecom/provider.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package namecom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/errors"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/utils"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion ipversion.IPVersion
|
||||
username string
|
||||
token string
|
||||
ttl *uint32
|
||||
}
|
||||
|
||||
func New(data json.RawMessage, domain, host string,
|
||||
ipVersion ipversion.IPVersion) (p *Provider, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
Token string `json:"token"`
|
||||
TTL *uint32 `json:"ttl,omitempty"`
|
||||
}{}
|
||||
err = json.Unmarshal(data, &extraSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
const minTTL = 300
|
||||
switch {
|
||||
case extraSettings.Username == "":
|
||||
return nil, fmt.Errorf("%w", errors.ErrEmptyUsername)
|
||||
case extraSettings.Token == "":
|
||||
return nil, fmt.Errorf("%w", errors.ErrEmptyPassword)
|
||||
case extraSettings.TTL != nil && *extraSettings.TTL < minTTL:
|
||||
return nil, fmt.Errorf("%w: %d must be at least %d",
|
||||
errors.ErrTTLTooLow, *extraSettings.TTL, minTTL)
|
||||
}
|
||||
|
||||
return &Provider{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
username: extraSettings.Username,
|
||||
token: extraSettings.Token,
|
||||
ttl: extraSettings.TTL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Provider) String() string {
|
||||
return utils.ToString(p.domain, p.host, constants.NameCom, p.ipVersion)
|
||||
}
|
||||
|
||||
func (p *Provider) Domain() string {
|
||||
return p.domain
|
||||
}
|
||||
|
||||
func (p *Provider) Host() string {
|
||||
return p.host
|
||||
}
|
||||
|
||||
func (p *Provider) IPVersion() ipversion.IPVersion {
|
||||
return p.ipVersion
|
||||
}
|
||||
|
||||
func (p *Provider) Proxied() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Provider) BuildDomainName() string {
|
||||
return utils.BuildDomainName(p.host, p.domain)
|
||||
}
|
||||
|
||||
func (p *Provider) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", p.BuildDomainName(), p.BuildDomainName())),
|
||||
Host: models.HTML(p.Host()),
|
||||
Provider: "<a href=\"https://name.com\">Name.com</a>",
|
||||
IPVersion: models.HTML(p.ipVersion.String()),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
|
||||
// Documentation at https://www.name.com/api-docs
|
||||
recordType := constants.A
|
||||
if ip.Is6() {
|
||||
recordType = constants.AAAA
|
||||
}
|
||||
|
||||
recordID, err := p.getRecordID(ctx, client, recordType)
|
||||
|
||||
if stderrors.Is(err, errors.ErrRecordNotFound) {
|
||||
err = p.createRecord(ctx, client, ip)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrCreateRecord, err)
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
} else if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrGetRecordID, err)
|
||||
}
|
||||
|
||||
err = p.updateRecord(ctx, client, recordID, ip)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUpdateRecord, err)
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
}
|
||||
63
internal/provider/providers/namecom/response.go
Normal file
63
internal/provider/providers/namecom/response.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package namecom
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/provider/errors"
|
||||
)
|
||||
|
||||
func parseErrorResponse(response *http.Response) (err error) {
|
||||
var errorResponse struct {
|
||||
Message string `json:"message"`
|
||||
Details string `json:"details"`
|
||||
}
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
err = decoder.Decode(&errorResponse)
|
||||
if err != nil {
|
||||
return fmt.Errorf("for error response: %w: %w", errors.ErrUnmarshalResponse, err)
|
||||
}
|
||||
|
||||
switch strings.ToLower(errorResponse.Message) {
|
||||
case "not found":
|
||||
return wrapErrorAndDetails(errors.ErrRecordNotFound, errorResponse.Details)
|
||||
case "permission denied", "unauthenticated":
|
||||
return wrapErrorAndDetails(errors.ErrAuth, errorResponse.Details)
|
||||
case "invalid argument":
|
||||
return wrapErrorAndDetails(errors.ErrBadRequest, errorResponse.Details)
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: %s: %s (status code %d)", errors.ErrUnknownResponse,
|
||||
errorResponse.Message, errorResponse.Details, response.StatusCode)
|
||||
}
|
||||
|
||||
func verifySuccessResponseBody(responseBody io.ReadCloser, sentIP netip.Addr) (err error) {
|
||||
decoder := json.NewDecoder(responseBody)
|
||||
var responseData struct {
|
||||
Answer string `json:"answer"`
|
||||
}
|
||||
err = decoder.Decode(&responseData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
|
||||
}
|
||||
|
||||
receivedIP, err := netip.ParseAddr(responseData.Answer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", errors.ErrIPReceivedMalformed, responseData.Answer)
|
||||
} else if sentIP.Compare(receivedIP) != 0 {
|
||||
return fmt.Errorf("%w: sent ip %s to update but received %s",
|
||||
errors.ErrIPReceivedMismatch, sentIP, receivedIP)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func wrapErrorAndDetails(sentinelErr error, details string) (wrappedErr error) {
|
||||
if details == "" {
|
||||
return fmt.Errorf("%w", sentinelErr)
|
||||
}
|
||||
return fmt.Errorf("%w: %s", sentinelErr, details)
|
||||
}
|
||||
65
internal/provider/providers/namecom/updaterecord.go
Normal file
65
internal/provider/providers/namecom/updaterecord.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package namecom
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/provider/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/errors"
|
||||
)
|
||||
|
||||
func (p *Provider) updateRecord(ctx context.Context, client *http.Client,
|
||||
recordID int, ip netip.Addr) (err error) {
|
||||
recordType := constants.A
|
||||
if ip.Is6() {
|
||||
recordType = constants.AAAA
|
||||
}
|
||||
|
||||
u := &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.name.com",
|
||||
Path: fmt.Sprintf("/v4/domains/%s/records/%d", p.domain, recordID),
|
||||
User: url.UserPassword(p.username, p.token),
|
||||
}
|
||||
|
||||
postRecordsParams := struct {
|
||||
Host string `json:"host"`
|
||||
Type string `json:"type"`
|
||||
Answer string `json:"answer"`
|
||||
TTL *uint32 `json:"ttl,omitempty"`
|
||||
}{
|
||||
Host: p.host,
|
||||
Type: recordType,
|
||||
Answer: ip.String(),
|
||||
TTL: p.ttl,
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(postRecordsParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", errors.ErrRequestMarshal, err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPut, u.String(), bytes.NewBuffer(bodyBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", errors.ErrBadRequest, err)
|
||||
}
|
||||
setHeaders(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("doing HTTP request: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
switch response.StatusCode {
|
||||
case http.StatusOK, http.StatusCreated:
|
||||
return verifySuccessResponseBody(response.Body, ip)
|
||||
default:
|
||||
return parseErrorResponse(response)
|
||||
}
|
||||
}
|
||||
1
readme/name.svg
Normal file
1
readme/name.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg enable-background="new 0 0 1510.1 240.3" viewBox="0 0 1510.1 240.3" xmlns="http://www.w3.org/2000/svg"><path d="m203.2 233.3-143.7-151.2v95.7c0 24.7 11.5 37.2 34.5 37.2v14.2h-84.3v-14.2c18.6 0 28.4-12.5 28.4-37.2v-146.8c0-7.1-11.5-11.2-33.1-11.2v-14.8h63.9l126.8 136v-87c0-23-11.8-34.2-34.2-34.2v-14.8h83.9v14.9c-20.3 0-30.8 11.2-30.8 34.2v176.2zm189.7-21c-3.7 13.2-19.3 20.6-34.5 20.6-13.2 0-27.7-5.4-32.1-19.3-12.4 13.2-25 19.3-43.3 19.3-23 0-41.3-10.5-41.3-34.8 0-30.1 28.4-46 81.5-55.1v-10.8c0-22.7-3.3-29-18.6-30.1-9.5-.7-24.7 2.4-24.7 14.9 1.7 2.4 3 6.1 3 10.5 0 9.5-6.4 14.6-17.3 14.5-9.7-.1-15.6-5.4-15.6-16.2 0-22.3 19.3-40.6 58.9-40.6 33.1 0 58.5 12.9 58.5 46v71c0 6.4 3 10.1 8.5 10.1 3.4 0 6.4-1.7 10.5-4.1zm-68.7-54.1c-25.7 4.1-37.9 11.5-37.9 35.5 0 16.2 5.7 20.3 16.2 20.3 23 0 21.6-19.7 21.6-32.1.1-12.4.1-23.7.1-23.7zm272.3 71.1v-14.3c14.2 0 19.6-4.7 19.6-14.2v-59.5c0-22.3-6.8-34.5-23.7-34.5-13.5 0-22.7 7.8-28.8 26v62.9c0 12.5 5.7 19.3 19.6 19.3v14.2h-82.2v-14.2c13.2 0 18.3-7.1 18.3-20.3v-45c0-29.8-6.8-43-25-43s-27.7 13.3-27.7 38.3v54.8c0 9.8 6.1 15.2 19.6 15.2v14.2h-82.5v-14.2c12.5 0 18.6-4.7 18.6-14.2v-83.5c0-5.1-7.4-7.8-22.3-7.8v-12.8l62.9-11.8c2 2 2.7 4.4 2.7 9.1v12.2c10.8-12.9 27.1-20.6 44.6-20.6 17.3 0 32.8 7.4 41.9 24 13.2-16.9 30.8-24 51.4-24 26.7 0 55.8 11.8 55.8 49.4v66c0 9.5 7.1 14.2 21.3 14.2v14.2h-84.1zm230.1-35.2c-13.2 26.4-31.1 41.3-62.6 41.3-41.6 0-72.4-26-72.4-73.7 0-48.7 31.8-76.4 74.1-76.4 41.3 0 63.9 26 63.9 61.2 0 2.7 0 6.1-.3 9.1h-87.6c-2.4 0-3.7 1-3.7 2.7 0 30.8 10.8 55.1 36.5 55.1 18.3 0 30.8-12.5 41.3-27.1zm-52.6-52.5c8.1 0 11.5-4.1 11.5-13.9 0-11.5-5.6-27.1-22.9-27.1-18.9 0-24.7 19.6-24.7 38.9 0 1.4 1.4 2 5.1 2h31zm273.8 57.2c-10.5 26.7-37.2 36.5-60.5 36.5-37.9 0-68-25.7-68-72.7 0-49 32.8-77.5 76.4-77.5 36.9 0 49 20.3 49 32.8 0 15.3-7.8 19.2-18.4 19.2s-18.3-6.1-17.1-17.2c.2-1.7 1.4-7.1 1.4-10.1 0-7.1-8.8-10.5-16.9-10.5-23.3 0-27.4 29.1-27.4 53.8 0 32.1 6.8 59.2 34.8 59.2 14.2 0 25.4-6.8 34.2-20.3zm88.8 36.5c-40.3 0-76.1-25.7-76.1-76.1 0-52.4 38.9-74.1 77.5-74.1 42.3 0 77.1 26 77.1 74.8-.1 51.8-39.6 75.4-78.5 75.4zm31.4-66.6c0-25-.7-68-31.1-68-28.1 0-28.8 35.9-28.8 47.7 0 28.8 0 71.7 31.5 71.7 28.1 0 28.4-33.8 28.4-51.4zm252.9 60.6v-14.3c14.2 0 19.6-4.7 19.6-14.2v-59.5c0-22.3-6.8-34.5-23.7-34.5-13.5 0-22.7 7.8-28.8 26v62.9c0 12.5 5.8 19.3 19.6 19.3v14.2h-82.2v-14.2c13.2 0 18.3-7.1 18.3-20.3v-45c0-29.8-6.8-43-25-43-18.3 0-27.7 13.2-27.7 38.2v54.8c0 9.8 6.1 15.2 19.6 15.2v14.2h-82.5v-14.1c12.5 0 18.6-4.7 18.6-14.2v-83.5c0-5.1-7.4-7.8-22.3-7.8v-12.8l62.9-11.8c2 2 2.7 4.4 2.7 9.1v12.2c10.8-12.9 27.1-20.6 44.6-20.6 17.3 0 32.8 7.4 41.9 24 13.2-16.9 30.8-24 51.4-24 26.7 0 55.8 11.8 55.8 49.4v66c0 9.5 7.1 14.2 21.3 14.2v14.2h-84.1z" fill="#fff"/><circle cx="873.9" cy="208.7" fill="#6eda78" r="25.3"/></svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
Reference in New Issue
Block a user