mirror of
https://github.com/go-acme/lego.git
synced 2024-12-23 09:15:11 +02:00
chore: improve internal release command (#2315)
This commit is contained in:
parent
d0708fc64e
commit
4809501817
@ -14,9 +14,9 @@ builds:
|
||||
- -s -w -X main.version={{.Version}}
|
||||
|
||||
goos:
|
||||
- windows
|
||||
- darwin
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
- freebsd
|
||||
- openbsd
|
||||
- solaris
|
||||
|
8
Makefile
8
Makefile
@ -39,16 +39,16 @@ checks:
|
||||
.PHONY: patch minor major detach
|
||||
|
||||
patch:
|
||||
go run ./internal/useragent/ release -m patch
|
||||
go run ./internal/releaser/ release -m patch
|
||||
|
||||
minor:
|
||||
go run ./internal/useragent/ release -m minor
|
||||
go run ./internal/releaser/ release -m minor
|
||||
|
||||
major:
|
||||
go run ./internal/useragent/ release -m major
|
||||
go run ./internal/releaser/ release -m major
|
||||
|
||||
detach:
|
||||
go run ./internal/useragent/ detach
|
||||
go run ./internal/releaser/ detach
|
||||
|
||||
# Docs
|
||||
.PHONY: docs-build docs-serve docs-themes
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Code generated by 'internal/useragent'; DO NOT EDIT.
|
||||
// Code generated by 'internal/releaser'; DO NOT EDIT.
|
||||
|
||||
package sender
|
||||
|
||||
|
@ -13,8 +13,6 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "lego"
|
||||
@ -22,7 +20,7 @@ func main() {
|
||||
app.Usage = "Let's Encrypt client written in Go"
|
||||
app.EnableBashCompletion = true
|
||||
|
||||
app.Version = version
|
||||
app.Version = getVersion()
|
||||
cli.VersionPrinter = func(c *cli.Context) {
|
||||
fmt.Printf("lego version %s %s/%s\n", c.App.Version, runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
|
15
cmd/lego/zz_gen_version.go
Normal file
15
cmd/lego/zz_gen_version.go
Normal file
@ -0,0 +1,15 @@
|
||||
// Code generated by 'internal/releaser'; DO NOT EDIT.
|
||||
|
||||
package main
|
||||
|
||||
const defaultVersion = "v4.19.2+dev-detach"
|
||||
|
||||
var version = ""
|
||||
|
||||
func getVersion() string {
|
||||
if version == "" {
|
||||
return defaultVersion
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
1
go.mod
1
go.mod
@ -36,6 +36,7 @@ require (
|
||||
github.com/gophercloud/gophercloud v1.14.0
|
||||
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7
|
||||
github.com/hashicorp/go-version v1.7.0
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114
|
||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df
|
||||
github.com/infobloxopen/infoblox-go-client v1.1.1
|
||||
|
2
go.sum
2
go.sum
@ -454,6 +454,8 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
|
84
internal/releaser/generator.go
Normal file
84
internal/releaser/generator.go
Normal file
@ -0,0 +1,84 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
const (
|
||||
dnsTemplate = "templates/dns.go.tmpl"
|
||||
dnsTargetFile = "./providers/dns/internal/useragent/useragent.go"
|
||||
)
|
||||
|
||||
const (
|
||||
senderTemplate = "templates/sender.go.tmpl"
|
||||
senderTargetFile = "./acme/api/internal/sender/useragent.go"
|
||||
)
|
||||
|
||||
const (
|
||||
versionTemplate = "templates/version.go.tmpl"
|
||||
versionTargetFile = "./cmd/lego/zz_gen_version.go"
|
||||
)
|
||||
|
||||
//go:embed templates
|
||||
var templateFS embed.FS
|
||||
|
||||
type Generator struct {
|
||||
templatePath string
|
||||
targetFile string
|
||||
}
|
||||
|
||||
func NewGenerator(templatePath string, targetFile string) *Generator {
|
||||
return &Generator{templatePath: templatePath, targetFile: targetFile}
|
||||
}
|
||||
|
||||
func (g *Generator) Generate(version, comment string) error {
|
||||
tmpl, err := template.New(filepath.Base(g.templatePath)).ParseFS(templateFS, g.templatePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing template (%s): %w", g.templatePath, err)
|
||||
}
|
||||
|
||||
b := &bytes.Buffer{}
|
||||
|
||||
err = tmpl.Execute(b, map[string]string{
|
||||
"version": version,
|
||||
"comment": comment,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("execute template (%s): %w", g.templatePath, err)
|
||||
}
|
||||
|
||||
source, err := format.Source(b.Bytes())
|
||||
if err != nil {
|
||||
return fmt.Errorf("format generated content (%s): %w", g.targetFile, err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(g.targetFile, source, 0o644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write file (%s): %w", g.targetFile, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generate(targetVersion, comment string) error {
|
||||
generators := []*Generator{
|
||||
NewGenerator(dnsTemplate, dnsTargetFile),
|
||||
NewGenerator(senderTemplate, senderTargetFile),
|
||||
NewGenerator(versionTemplate, versionTargetFile),
|
||||
}
|
||||
|
||||
for _, generator := range generators {
|
||||
err := generator.Generate(targetVersion, comment)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate file(s): %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
183
internal/releaser/releaser.go
Normal file
183
internal/releaser/releaser.go
Normal file
@ -0,0 +1,183 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
hcversion "github.com/hashicorp/go-version"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const flgMode = "mode"
|
||||
|
||||
const (
|
||||
modePatch = "patch"
|
||||
modeMinor = "minor"
|
||||
modeMajor = "major"
|
||||
)
|
||||
|
||||
const versionSourceFile = "./cmd/lego/zz_gen_version.go"
|
||||
|
||||
const (
|
||||
commentRelease = "release"
|
||||
commentDetach = "detach"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "lego-releaser"
|
||||
app.Usage = "Lego releaser"
|
||||
app.HelpName = "releaser"
|
||||
app.Commands = []*cli.Command{
|
||||
{
|
||||
Name: "release",
|
||||
Usage: "Update file for a release",
|
||||
Action: release,
|
||||
Before: func(ctx *cli.Context) error {
|
||||
mode := ctx.String("mode")
|
||||
switch mode {
|
||||
case modePatch, modeMinor, modeMajor:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid mode: %s", mode)
|
||||
}
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: flgMode,
|
||||
Aliases: []string{"m"},
|
||||
Value: modePatch,
|
||||
Usage: fmt.Sprintf("The release mode: %s|%s|%s", modePatch, modeMinor, modeMajor),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "detach",
|
||||
Usage: "Update file post release",
|
||||
Action: detach,
|
||||
},
|
||||
}
|
||||
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func release(ctx *cli.Context) error {
|
||||
mode := ctx.String(flgMode)
|
||||
|
||||
currentVersion, err := readCurrentVersion(versionSourceFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read current version: %w", err)
|
||||
}
|
||||
|
||||
nextVersion, err := bumpVersion(mode, currentVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bump version: %w", err)
|
||||
}
|
||||
|
||||
err = generate(nextVersion, commentRelease)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func detach(_ *cli.Context) error {
|
||||
currentVersion, err := readCurrentVersion(versionSourceFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read current version: %w", err)
|
||||
}
|
||||
|
||||
v := currentVersion.Core().String()
|
||||
|
||||
err = generate(v, commentDetach)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readCurrentVersion(filename string) (*hcversion.Version, error) {
|
||||
fset := token.NewFileSet()
|
||||
file, err := parser.ParseFile(fset, filename, nil, parser.AllErrors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := visitor{data: make(map[string]string)}
|
||||
ast.Walk(v, file)
|
||||
|
||||
current, err := hcversion.NewSemver(v.data["defaultVersion"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return current, nil
|
||||
}
|
||||
|
||||
type visitor struct {
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func (v visitor) Visit(n ast.Node) ast.Visitor {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch d := n.(type) {
|
||||
case *ast.GenDecl:
|
||||
if d.Tok == token.CONST {
|
||||
for _, spec := range d.Specs {
|
||||
valueSpec, ok := spec.(*ast.ValueSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if len(valueSpec.Names) != 1 || len(valueSpec.Values) != 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
va, ok := valueSpec.Values[0].(*ast.BasicLit)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if va.Kind != token.STRING {
|
||||
continue
|
||||
}
|
||||
|
||||
s, err := strconv.Unquote(va.Value)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
v.data[valueSpec.Names[0].String()] = s
|
||||
}
|
||||
}
|
||||
default:
|
||||
// noop
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func bumpVersion(mode string, v *hcversion.Version) (string, error) {
|
||||
segments := v.Segments()
|
||||
|
||||
switch mode {
|
||||
case modePatch:
|
||||
return fmt.Sprintf("%d.%d.%d", segments[0], segments[1], segments[2]+1), nil
|
||||
case modeMinor:
|
||||
return fmt.Sprintf("%d.%d.0", segments[0], segments[1]+1), nil
|
||||
case modeMajor:
|
||||
return fmt.Sprintf("%d.0.0", segments[0]+1), nil
|
||||
default:
|
||||
return "", fmt.Errorf("invalid mode: %s", mode)
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
// Code generated by 'internal/useragent'; DO NOT EDIT.
|
||||
// Code generated by 'internal/releaser'; DO NOT EDIT.
|
||||
|
||||
package useragent
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Code generated by 'internal/useragent'; DO NOT EDIT.
|
||||
// Code generated by 'internal/releaser'; DO NOT EDIT.
|
||||
|
||||
package sender
|
||||
|
15
internal/releaser/templates/version.go.tmpl
Normal file
15
internal/releaser/templates/version.go.tmpl
Normal file
@ -0,0 +1,15 @@
|
||||
// Code generated by 'internal/releaser'; DO NOT EDIT.
|
||||
|
||||
package main
|
||||
|
||||
const defaultVersion = "v{{ .version }}+dev{{ if .comment }}-{{ .comment }}{{end}}"
|
||||
|
||||
var version = ""
|
||||
|
||||
func getVersion() string {
|
||||
if version == "" {
|
||||
return defaultVersion
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
@ -1,175 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
//go:embed templates
|
||||
var templateFS embed.FS
|
||||
|
||||
type Generator struct {
|
||||
baseUserAgent string
|
||||
templatePath string
|
||||
sourcePath string
|
||||
}
|
||||
|
||||
func NewGenerator(baseUserAgent string, templatePath string, sourcePath string) *Generator {
|
||||
return &Generator{baseUserAgent: baseUserAgent, templatePath: templatePath, sourcePath: sourcePath}
|
||||
}
|
||||
|
||||
func (g *Generator) Release(mode string) error {
|
||||
// Read file
|
||||
data, err := readUserAgentFile(g.sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Bump version
|
||||
newVersion, err := g.bumpVersion(data["ourUserAgent"], mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write file
|
||||
comment := "release" // detach|release
|
||||
|
||||
return g.writeUserAgentFile(g.sourcePath, newVersion, comment)
|
||||
}
|
||||
|
||||
func (g *Generator) Detach() error {
|
||||
// Read file
|
||||
data, err := readUserAgentFile(g.sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write file
|
||||
version := strings.TrimPrefix(data["ourUserAgent"], g.baseUserAgent)
|
||||
comment := "detach"
|
||||
|
||||
return g.writeUserAgentFile(g.sourcePath, version, comment)
|
||||
}
|
||||
|
||||
func (g *Generator) writeUserAgentFile(filename, version, comment string) error {
|
||||
tmpl, err := template.New(filepath.Base(g.templatePath)).ParseFS(templateFS, g.templatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b := &bytes.Buffer{}
|
||||
err = tmpl.Execute(b, map[string]string{
|
||||
"version": version,
|
||||
"comment": comment,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
source, err := format.Source(b.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(filename, source, 0o644)
|
||||
}
|
||||
|
||||
func (g *Generator) bumpVersion(userAgent, mode string) (string, error) {
|
||||
prevVersion := strings.TrimPrefix(userAgent, g.baseUserAgent)
|
||||
|
||||
allString := regexp.MustCompile(`(\d+)\.(\d+)\.(\d+)`).FindStringSubmatch(prevVersion)
|
||||
|
||||
if len(allString) != 4 {
|
||||
return "", fmt.Errorf("invalid version format: %s", prevVersion)
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case "patch":
|
||||
patch, err := strconv.Atoi(allString[3])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s.%s.%d", allString[1], allString[2], patch+1), nil
|
||||
case "minor":
|
||||
minor, err := strconv.Atoi(allString[2])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s.%d.0", allString[1], minor+1), nil
|
||||
case "major":
|
||||
major, err := strconv.Atoi(allString[1])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%d.0.0", major+1), nil
|
||||
default:
|
||||
return "", fmt.Errorf("invalid mode: %s", mode)
|
||||
}
|
||||
}
|
||||
|
||||
func readUserAgentFile(filename string) (map[string]string, error) {
|
||||
fset := token.NewFileSet()
|
||||
file, err := parser.ParseFile(fset, filename, nil, parser.AllErrors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := visitor{data: make(map[string]string)}
|
||||
ast.Walk(v, file)
|
||||
|
||||
return v.data, nil
|
||||
}
|
||||
|
||||
type visitor struct {
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func (v visitor) Visit(n ast.Node) ast.Visitor {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch d := n.(type) {
|
||||
case *ast.GenDecl:
|
||||
if d.Tok == token.CONST {
|
||||
for _, spec := range d.Specs {
|
||||
valueSpec, ok := spec.(*ast.ValueSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if len(valueSpec.Names) != 1 || len(valueSpec.Values) != 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
va, ok := valueSpec.Values[0].(*ast.BasicLit)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if va.Kind != token.STRING {
|
||||
continue
|
||||
}
|
||||
|
||||
s, err := strconv.Unquote(va.Value)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
v.data[valueSpec.Names[0].String()] = s
|
||||
}
|
||||
}
|
||||
default:
|
||||
// noop
|
||||
}
|
||||
return v
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
dnsBaseUserAgent = "goacme-lego/"
|
||||
dnsSourceFile = "./providers/dns/internal/useragent/useragent.go"
|
||||
dnsTemplate = "templates/dns.go.tmpl"
|
||||
)
|
||||
|
||||
const (
|
||||
senderBaseUserAgent = "xenolf-acme/"
|
||||
senderSourceFile = "./acme/api/internal/sender/useragent.go"
|
||||
senderTemplate = "templates/sender.go.tmpl"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "lego-releaser"
|
||||
app.Usage = "Lego releaser"
|
||||
app.HelpName = "releaser"
|
||||
app.Commands = []*cli.Command{
|
||||
{
|
||||
Name: "release",
|
||||
Usage: "Update file for a release",
|
||||
Action: release,
|
||||
Before: func(ctx *cli.Context) error {
|
||||
mode := ctx.String("mode")
|
||||
switch mode {
|
||||
case "patch", "minor", "major":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid mode: %s", mode)
|
||||
}
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "mode",
|
||||
Aliases: []string{"m"},
|
||||
Value: "patch",
|
||||
Usage: "The release mode: patch|minor|major",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "detach",
|
||||
Usage: "Update file post release",
|
||||
Action: detach,
|
||||
},
|
||||
}
|
||||
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func release(ctx *cli.Context) error {
|
||||
mode := ctx.String("mode")
|
||||
|
||||
generators := []*Generator{
|
||||
NewGenerator(senderBaseUserAgent, senderTemplate, senderSourceFile),
|
||||
NewGenerator(dnsBaseUserAgent, dnsTemplate, dnsSourceFile),
|
||||
}
|
||||
|
||||
for _, generator := range generators {
|
||||
err := generator.Release(mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func detach(_ *cli.Context) error {
|
||||
generators := []*Generator{
|
||||
NewGenerator(senderBaseUserAgent, senderTemplate, senderSourceFile),
|
||||
NewGenerator(dnsBaseUserAgent, dnsTemplate, dnsSourceFile),
|
||||
}
|
||||
|
||||
for _, generator := range generators {
|
||||
err := generator.Detach()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
// Code generated by 'internal/useragent'; DO NOT EDIT.
|
||||
// Code generated by 'internal/releaser'; DO NOT EDIT.
|
||||
|
||||
package useragent
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user