Base application setup
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 36s
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 36s
Signed-off-by: Jan Tytgat <jan.tytgat@corelayer.eu>
This commit is contained in:
44
examples/simple/main.go
Normal file
44
examples/simple/main.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"git.flexabyte.io/flexabyte/go-kit/pkg/application"
|
||||||
|
"git.flexabyte.io/flexabyte/go-kit/pkg/slogd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var err error
|
||||||
|
slogd.Init(slogd.LevelTrace, true)
|
||||||
|
|
||||||
|
config := application.Config{
|
||||||
|
Name: "main",
|
||||||
|
Title: "Main Test",
|
||||||
|
Banner: "",
|
||||||
|
Version: "0.1.0-alpha.0+metadata.20101112",
|
||||||
|
EnableGracefulShutdown: false,
|
||||||
|
OverrideRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
fmt.Println("overrideRunE")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
PersistentPreRunE: nil,
|
||||||
|
PersistentPostRunE: nil,
|
||||||
|
ShutdownSignals: nil,
|
||||||
|
ShutdownTimeout: 0,
|
||||||
|
SubCommands: nil,
|
||||||
|
SubCommandInitializeFunc: nil,
|
||||||
|
ValidArgs: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
var app application.Application
|
||||||
|
if app, err = application.New(config); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = app.Start(context.Background()); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func New() *App {
|
|
||||||
return &App{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type App struct{}
|
|
||||||
|
|
||||||
func (a *App) Execute() {
|
|
||||||
fmt.Println("Hello World")
|
|
||||||
}
|
|
38
pkg/application/application.go
Normal file
38
pkg/application/application.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Application interface {
|
||||||
|
Start(ctx context.Context) error
|
||||||
|
Shutdown() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(c Config) (Application, error) {
|
||||||
|
var cmd *cobra.Command
|
||||||
|
var err error
|
||||||
|
if cmd, err = c.getRootCommand(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &application{
|
||||||
|
cmd: cmd,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type application struct {
|
||||||
|
cmd *cobra.Command
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *application) Start(ctx context.Context) error {
|
||||||
|
return a.cmd.ExecuteContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *application) Shutdown() error {
|
||||||
|
fmt.Println("Shutdown")
|
||||||
|
return nil
|
||||||
|
}
|
1
pkg/application/application_test.go
Normal file
1
pkg/application/application_test.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package application
|
28
pkg/application/commander.go
Normal file
28
pkg/application/commander.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import "github.com/spf13/cobra"
|
||||||
|
|
||||||
|
type Commander interface {
|
||||||
|
Initialize(f func(c *cobra.Command)) *cobra.Command
|
||||||
|
}
|
||||||
|
|
||||||
|
type Command struct {
|
||||||
|
Command *cobra.Command
|
||||||
|
SubCommands []Commander
|
||||||
|
Configure func(c *cobra.Command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Command) Initialize(f func(c *cobra.Command)) *cobra.Command {
|
||||||
|
if f != nil {
|
||||||
|
f(c.Command)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Configure != nil {
|
||||||
|
c.Configure(c.Command)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sub := range c.SubCommands {
|
||||||
|
c.Command.AddCommand(sub.Initialize(f))
|
||||||
|
}
|
||||||
|
return c.Command
|
||||||
|
}
|
110
pkg/application/config.go
Normal file
110
pkg/application/config.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"git.flexabyte.io/flexabyte/go-kit/pkg/semver"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Name string
|
||||||
|
Title string
|
||||||
|
Banner string
|
||||||
|
Version string
|
||||||
|
EnableGracefulShutdown bool
|
||||||
|
OverrideRunE func(cmd *cobra.Command, args []string) error
|
||||||
|
PersistentPreRunE []func(cmd *cobra.Command, args []string) error // collection of PreRunE functions
|
||||||
|
PersistentPostRunE []func(cmd *cobra.Command, args []string) error // collection of PostRunE functions
|
||||||
|
ShutdownSignals []os.Signal
|
||||||
|
ShutdownTimeout time.Duration
|
||||||
|
SubCommands []Command
|
||||||
|
SubCommandInitializeFunc func(cmd *cobra.Command)
|
||||||
|
ValidArgs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) getRootCommand() (*cobra.Command, error) {
|
||||||
|
var err error
|
||||||
|
if err = c.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var long string
|
||||||
|
if c.Banner != "" {
|
||||||
|
long = c.Banner + "\n" + c.Title
|
||||||
|
} else {
|
||||||
|
long = c.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: c.Name,
|
||||||
|
Short: c.Title,
|
||||||
|
Long: long,
|
||||||
|
PersistentPreRunE: persistentPreRunFuncE,
|
||||||
|
PersistentPostRunE: persistentPostRunFuncE,
|
||||||
|
RunE: runFuncE,
|
||||||
|
SilenceErrors: true,
|
||||||
|
SilenceUsage: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.OverrideRunE != nil {
|
||||||
|
cmd.RunE = c.OverrideRunE
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, subcommand := range c.SubCommands {
|
||||||
|
cmd.AddCommand(subcommand.Initialize(c.SubCommandInitializeFunc))
|
||||||
|
}
|
||||||
|
|
||||||
|
var v semver.Version
|
||||||
|
if v, err = c.ParseVersion(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
configureVersionFlag(cmd, v) // Configure app for version information
|
||||||
|
configureOutputFlags(cmd) // Configure verbosity
|
||||||
|
configureLoggingFlags(cmd) // Configure logging
|
||||||
|
cmd.PersistentFlags().SetNormalizeFunc(normalizeFunc) // normalize persistent flags
|
||||||
|
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) ParseVersion() (semver.Version, error) {
|
||||||
|
return semver.Parse(c.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) RegisterCommand(cmd Commander, f func(*cobra.Command)) {
|
||||||
|
appCmd.AddCommand(cmd.Initialize(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) RegisterCommands(cmds []Commander, f func(*cobra.Command)) {
|
||||||
|
for _, cmd := range cmds {
|
||||||
|
appCmd.AddCommand(cmd.Initialize(f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) RegisterPersistentPreRunE(f func(cmd *cobra.Command, args []string) error) {
|
||||||
|
persistentPreRunE = append(persistentPreRunE, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) RegisterPersistentPostRunE(f func(cmd *cobra.Command, args []string) error) {
|
||||||
|
persistentPostRunE = append(persistentPostRunE, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) Validate() error {
|
||||||
|
if c.Name == "" {
|
||||||
|
return errors.New("name is required")
|
||||||
|
}
|
||||||
|
if c.Title == "" {
|
||||||
|
return errors.New("title is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if _, err = semver.Parse(c.Version); err != nil {
|
||||||
|
return fmt.Errorf("invalid version: %s", c.Version)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
22
pkg/application/config_test.go
Normal file
22
pkg/application/config_test.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig_Validate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config Config
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := tt.config.Validate(); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
105
pkg/application/globals.go
Normal file
105
pkg/application/globals.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"git.flexabyte.io/flexabyte/go-kit/pkg/slogd"
|
||||||
|
"git.flexabyte.io/flexabyte/go-kit/pkg/slogd_colored"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
appName string
|
||||||
|
appCmd *cobra.Command
|
||||||
|
persistentPreRunE []func(cmd *cobra.Command, args []string) error // collection of PreRunE functions
|
||||||
|
persistentPostRunE []func(cmd *cobra.Command, args []string) error // collection of PostRunE functions
|
||||||
|
outWriter io.Writer = os.Stdout
|
||||||
|
// version semver.Version
|
||||||
|
)
|
||||||
|
|
||||||
|
func helpFuncE(cmd *cobra.Command, args []string) error {
|
||||||
|
return cmd.Help()
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeFunc(f *pflag.FlagSet, name string) pflag.NormalizedName {
|
||||||
|
// switch name {
|
||||||
|
// case "no-color":
|
||||||
|
// name = "log-type"
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
return pflag.NormalizedName(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func persistentPreRunFuncE(cmd *cobra.Command, args []string) error {
|
||||||
|
slogd.SetLevel(slogd.Level(logLevelFlag))
|
||||||
|
if slogd.ActiveHandler() == slogd_colored.HandlerColor && noColorFlag {
|
||||||
|
slogd.UseHandler(slogd.HandlerText)
|
||||||
|
cmd.SetContext(slogd.WithContext(cmd.Context()))
|
||||||
|
}
|
||||||
|
|
||||||
|
slogd.FromContext(cmd.Context()).Log(cmd.Context(), slogd.LevelTrace, "starting application", slog.String("command", cmd.CommandPath()))
|
||||||
|
slogd.FromContext(cmd.Context()).Log(cmd.Context(), slogd.LevelTrace, "executing PersistentPreRun")
|
||||||
|
|
||||||
|
// Make sure we can always get the version
|
||||||
|
if versionFlag || cmd.Use == versionName {
|
||||||
|
slogd.FromContext(cmd.Context()).LogAttrs(cmd.Context(), slogd.LevelTrace, "overriding command", slog.String("old_function", runtime.FuncForPC(reflect.ValueOf(cmd.RunE).Pointer()).Name()), slog.String("new_function", runtime.FuncForPC(reflect.ValueOf(versionRunFuncE).Pointer()).Name()))
|
||||||
|
cmd.RunE = versionRunFuncE
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that we show the app help if no commands or flags are passed
|
||||||
|
if cmd.CalledAs() == appName && runtime.FuncForPC(reflect.ValueOf(cmd.RunE).Pointer()).Name() == runtime.FuncForPC(reflect.ValueOf(runFuncE).Pointer()).Name() {
|
||||||
|
slogd.FromContext(cmd.Context()).LogAttrs(cmd.Context(), slogd.LevelTrace, "overriding command", slog.String("old_function", runtime.FuncForPC(reflect.ValueOf(cmd.RunE).Pointer()).Name()), slog.String("new_function", runtime.FuncForPC(reflect.ValueOf(helpFuncE).Pointer()).Name()))
|
||||||
|
|
||||||
|
cmd.RunE = helpFuncE
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO move to front??
|
||||||
|
if quietFlag {
|
||||||
|
slogd.FromContext(cmd.Context()).LogAttrs(cmd.Context(), slogd.LevelDebug, "activating quiet mode")
|
||||||
|
outWriter = io.Discard
|
||||||
|
}
|
||||||
|
|
||||||
|
if persistentPreRunE == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
for _, preRun := range persistentPreRunE {
|
||||||
|
slogd.FromContext(cmd.Context()).Log(cmd.Context(), slogd.LevelTrace, "executing PersistentPreRun function", slog.String("function", runtime.FuncForPC(reflect.ValueOf(preRun).Pointer()).Name()))
|
||||||
|
if err = preRun(cmd, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func persistentPostRunFuncE(cmd *cobra.Command, args []string) error {
|
||||||
|
defer slogd.FromContext(cmd.Context()).Log(cmd.Context(), slogd.LevelTrace, "stopping application", slog.String("command", cmd.CommandPath()))
|
||||||
|
slogd.FromContext(cmd.Context()).Log(cmd.Context(), slogd.LevelTrace, "executing PersistentPostRunE")
|
||||||
|
|
||||||
|
if persistentPostRunE == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
for _, postRun := range persistentPostRunE {
|
||||||
|
slogd.FromContext(cmd.Context()).Log(cmd.Context(), slogd.LevelTrace, "executing PersistentPostRun function", slog.String("function", runtime.FuncForPC(reflect.ValueOf(postRun).Pointer()).Name()))
|
||||||
|
if err = postRun(cmd, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// appRunE is an empty catch function to allow overrides through persistentPreRunE
|
||||||
|
func runFuncE(cmd *cobra.Command, args []string) error {
|
||||||
|
return nil
|
||||||
|
}
|
35
pkg/application/logging.go
Normal file
35
pkg/application/logging.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LogOutputStdOut = "stdout"
|
||||||
|
LogOutputStdErr = "stderr"
|
||||||
|
LogOutputFile = "file"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logLevelFlag string
|
||||||
|
var logOutputFlag string
|
||||||
|
var logTypeFlag string
|
||||||
|
|
||||||
|
func addLogLevelFlag(cmd *cobra.Command) {
|
||||||
|
cmd.PersistentFlags().StringVarP(&logLevelFlag, "log-level", "", "info", "Set log level (trace, debug, info, warn, error, fatal)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func addLogOutputFlag(cmd *cobra.Command) {
|
||||||
|
cmd.PersistentFlags().StringVarP(&logOutputFlag, "log-outWriter", "", "stderr", "Set log outWriter (stdout, stderr, file)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func addLogTypeFlag(cmd *cobra.Command) {
|
||||||
|
cmd.PersistentFlags().StringVarP(&logTypeFlag, "log-type", "", "text", "Set log type (text, json, color)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func configureLoggingFlags(cmd *cobra.Command) {
|
||||||
|
addLogLevelFlag(cmd)
|
||||||
|
addLogOutputFlag(cmd)
|
||||||
|
addLogTypeFlag(cmd)
|
||||||
|
|
||||||
|
cmd.MarkFlagsMutuallyExclusive("no-color", "log-type")
|
||||||
|
}
|
36
pkg/application/output.go
Normal file
36
pkg/application/output.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import "github.com/spf13/cobra"
|
||||||
|
|
||||||
|
var jsonOutputFlag bool
|
||||||
|
var noColorFlag bool
|
||||||
|
var quietFlag bool
|
||||||
|
var verboseFlag bool
|
||||||
|
|
||||||
|
func addJsonOutputFlag(cmd *cobra.Command) {
|
||||||
|
cmd.PersistentFlags().BoolVarP(&jsonOutputFlag, "json", "", false, "Enable JSON outWriter")
|
||||||
|
}
|
||||||
|
|
||||||
|
func addNoColorFlag(cmd *cobra.Command) {
|
||||||
|
cmd.PersistentFlags().BoolVarP(&noColorFlag, "no-color", "", false, "Disable color outWriter")
|
||||||
|
}
|
||||||
|
|
||||||
|
func addQuietFlag(cmd *cobra.Command) {
|
||||||
|
cmd.PersistentFlags().BoolVarP(&quietFlag, "quiet", "q", false, "Enable quiet mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
func addVerboseFlag(cmd *cobra.Command) {
|
||||||
|
cmd.PersistentFlags().BoolVarP(&verboseFlag, "verbose", "v", false, "Enable verbose outWriter")
|
||||||
|
}
|
||||||
|
|
||||||
|
func configureOutputFlags(cmd *cobra.Command) {
|
||||||
|
addJsonOutputFlag(cmd)
|
||||||
|
addNoColorFlag(cmd)
|
||||||
|
addVerboseFlag(cmd)
|
||||||
|
addQuietFlag(cmd)
|
||||||
|
|
||||||
|
cmd.MarkFlagsMutuallyExclusive("verbose", "quiet", "json")
|
||||||
|
cmd.MarkFlagsMutuallyExclusive("json", "no-color")
|
||||||
|
cmd.MarkFlagsMutuallyExclusive("quiet", "no-color")
|
||||||
|
|
||||||
|
}
|
69
pkg/application/version.go
Normal file
69
pkg/application/version.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"git.flexabyte.io/flexabyte/go-kit/pkg/semver"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
versionName = "version"
|
||||||
|
versionShortHand = "V"
|
||||||
|
versionUsage = "Show version information"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
version semver.Version
|
||||||
|
versionFlag bool
|
||||||
|
versionCmd = &cobra.Command{
|
||||||
|
Use: versionName,
|
||||||
|
Short: versionUsage,
|
||||||
|
RunE: versionRunFuncE,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func addVersionFlag(cmd *cobra.Command) {
|
||||||
|
cmd.PersistentFlags().BoolVarP(&versionFlag, versionName, versionShortHand, false, versionUsage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func configureVersionFlag(cmd *cobra.Command, v semver.Version) {
|
||||||
|
version = v
|
||||||
|
cmd.AddCommand(versionCmd)
|
||||||
|
addVersionFlag(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printVersion(v semver.Version) string {
|
||||||
|
var output string
|
||||||
|
if !verboseFlag {
|
||||||
|
output = v.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonOutputFlag {
|
||||||
|
var b []byte
|
||||||
|
b, _ = json.Marshal(v)
|
||||||
|
output = string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if output != "" {
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"Full: %s\nVersion: %s\nChannel: %s\nCommit: %s\nDate: %s",
|
||||||
|
v.String(),
|
||||||
|
v.Number(),
|
||||||
|
v.Release(),
|
||||||
|
v.Commit(),
|
||||||
|
v.Date(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func versionRunFuncE(cmd *cobra.Command, args []string) error {
|
||||||
|
if _, err := fmt.Fprintln(outWriter, printVersion(version)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
Reference in New Issue
Block a user