From 9bc56666a081f95d180ebd8bfc0249a2e6826241 Mon Sep 17 00:00:00 2001 From: Gitea Date: Tue, 21 Nov 2023 15:07:54 +0300 Subject: [PATCH] initial commit --- .gitignore | 1 + Taskfile.yml | 20 ++ cmd/cli/main.go | 5 + cmd/dev/sravnicli/main.go | 40 ++++ go.mod | 8 + go.sum | 45 +++++ internal/app/courses/client.go | 1 + internal/domain/error.go | 11 ++ .../courses/sravni/client.go | 172 ++++++++++++++++++ .../interfaceadapters/services.go | 8 + kurious.go | 38 ++++ 11 files changed, 349 insertions(+) create mode 100644 .gitignore create mode 100644 Taskfile.yml create mode 100644 cmd/cli/main.go create mode 100644 cmd/dev/sravnicli/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/app/courses/client.go create mode 100644 internal/domain/error.go create mode 100644 internal/infrastructure/interfaceadapters/courses/sravni/client.go create mode 100644 internal/infrastructure/interfaceadapters/services.go create mode 100644 kurious.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba077a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bin diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..952171e --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,20 @@ +version: '3' + +env: + CGO_ENABLED: 0 + GOBIN: "{{.USER_WORKING_DIR}}/bin" + PROJECT: "git.loyso.art/frx/kurious" + +tasks: + install_tools: + cmds: + - go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 + check: + cmds: + - "$GOBIN/golangci-lint run ./..." + test: + cmds: + - go test --count=1 ./internal/... + build: + cmds: + - go build -o $GOBIN/sravnicli -v -ldflags "-X '$PROJECT.version=ohwell'" cmd/dev/sravnicli/main.go diff --git a/cmd/cli/main.go b/cmd/cli/main.go new file mode 100644 index 0000000..4f5f4e5 --- /dev/null +++ b/cmd/cli/main.go @@ -0,0 +1,5 @@ +package main + +func main() { + println("oh well") +} diff --git a/cmd/dev/sravnicli/main.go b/cmd/dev/sravnicli/main.go new file mode 100644 index 0000000..268dcf1 --- /dev/null +++ b/cmd/dev/sravnicli/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "context" + "encoding/json" + "log/slog" + "os" + "os/signal" + + "git.loyso.art/frx/kurious" + "git.loyso.art/frx/kurious/internal/infrastructure/interfaceadapters/courses/sravni" +) + +func main() { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{})) + + version, commit, bt := kurious.Version(), kurious.Commit(), kurious.BuildTime() + pid := os.Getpid() + + log.InfoContext( + ctx, "running app", + slog.Int("pid", pid), + slog.String("version", version), + slog.String("commit", commit), + slog.Time("build_time", bt), + ) + + client := sravni.NewClient(log, true) + meta, err := client.GetMetaInfo(ctx) + if err != nil { + log.ErrorContext(ctx, "unable to get meta info", slog.Any("error", err)) + } + + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + _ = enc.Encode(meta) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1757e3a --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module git.loyso.art/frx/kurious + +go 1.21 + +require ( + github.com/go-resty/resty/v2 v2.10.0 + golang.org/x/net v0.18.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d8e6445 --- /dev/null +++ b/go.sum @@ -0,0 +1,45 @@ +github.com/go-resty/resty/v2 v2.10.0 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+CkiCo= +github.com/go-resty/resty/v2 v2.10.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/app/courses/client.go b/internal/app/courses/client.go new file mode 100644 index 0000000..01e398d --- /dev/null +++ b/internal/app/courses/client.go @@ -0,0 +1 @@ +package courses diff --git a/internal/domain/error.go b/internal/domain/error.go new file mode 100644 index 0000000..3250582 --- /dev/null +++ b/internal/domain/error.go @@ -0,0 +1,11 @@ +package domain + +const ( + UnexpectedStatusError SimpleError = "unexpected status" +) + +type SimpleError string + +func (err SimpleError) Error() string { + return string(err) +} diff --git a/internal/infrastructure/interfaceadapters/courses/sravni/client.go b/internal/infrastructure/interfaceadapters/courses/sravni/client.go new file mode 100644 index 0000000..0039678 --- /dev/null +++ b/internal/infrastructure/interfaceadapters/courses/sravni/client.go @@ -0,0 +1,172 @@ +package sravni + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "log/slog" + "strings" + + "git.loyso.art/frx/kurious/internal/domain" + + "github.com/go-resty/resty/v2" + "golang.org/x/net/html" + "golang.org/x/net/html/atom" +) + +const ( + baseURL = "https://www.sravni.ru/kursy" +) + +func NewClient(log *slog.Logger, debug bool) *client { + return &client{ + log: log.With(slog.String("client", "sravni")), + http: resty.New(). + SetBaseURL(baseURL). + SetDebug(debug), + } +} + +type client struct { + log *slog.Logger + http *resty.Client +} + +type MetaInfoRuntimeConfig struct { + BrandingURL string `json:"brandingUrl"` + Release string `json:"release"` + Environment string `json:"environment"` + Gateway string `json:"gatewayUrl"` + APIGatewayURL string `json:"apiGatewayUrl"` + EducationURL string `json:"educationUrl"` + PhoneVerifierURL string `json:"phoneVerifierUrl"` + WebPath string `json:"webPath"` + ServiceName string `json:"serviceName"` + OrgnazationURL string `json:"organizationsUrl"` +} + +type MetaInfoReduxState struct { + Categories struct { + Data map[string]int `json:"data"` + } `json:"categories"` +} + +type MetaInfoProps struct { + InitialReduxState MetaInfoReduxState `json:"initialReduxState"` +} + +type MetaInfo struct { + Page string `json:"page"` + Query map[string]string `json:"query"` + BuildID string `json:"buildId"` + RuntimeConfig MetaInfoRuntimeConfig `json:"runtimeConfig"` + Props MetaInfoProps `json:"props"` +} + +func (c *client) GetMetaInfo(ctx context.Context) (*MetaInfo, error) { + ctxLogger := restyCtxLogger{ + ctx: ctx, + log: c.log, + } + + req := c.http.R(). + SetContext(ctx). + SetLogger(ctxLogger). + EnableTrace() + + resp, err := req.Get("/") + if err != nil { + return nil, fmt.Errorf("getting request: %w", err) + } + + if resp.IsError() { + c.log.ErrorContext(ctx, "unable to proceed request", slog.String("body", string(resp.Body()))) + return nil, fmt.Errorf("got %d, but expected success: %w", resp.StatusCode(), domain.UnexpectedStatusError) + } + + traceInfo := resp.Request.TraceInfo() + c.log.InfoContext(ctx, "request proceeded", slog.Any("trace", traceInfo)) + + r := bytes.NewReader(resp.Body()) + nodes, err := html.Parse(r) + if err != nil { + return nil, fmt.Errorf("parsing html body: %w", err) + } + + c.log.InfoContext(ctx, "inspecting node", slog.Any("node", nodes)) + + htmlNode := func() *html.Node { + for child := nodes.FirstChild; child != nil; child = child.NextSibling { + c.log.InfoContext(ctx, "inspecting node", slog.Any("node", child)) + if child.Type == html.ElementNode { + return child + } + } + + return nil + }() + if htmlNode == nil { + c.log.WarnContext(ctx, "no html node found") + return nil, nil + } + + var bodyNode *html.Node + for child := htmlNode.FirstChild; child != nil; child = child.NextSibling { + c.log.InfoContext(ctx, "inspecting html node", slog.Any("node", child)) + if child.DataAtom == atom.Body { + c.log.InfoContext(ctx, "found body node") + bodyNode = child + break + } + } + + var nextData *html.Node + for child := bodyNode.FirstChild; child != nil; child = child.NextSibling { + c.log.InfoContext(ctx, "inspecting body node", slog.Any("node", child)) + if child.DataAtom == atom.Script { + c.log.InfoContext(ctx, "found script node") + for _, attr := range child.Attr { + if attr.Key == "id" && attr.Val == "__NEXT_DATA__" { + c.log.InfoContext(ctx, "found metadata container") + nextData = child.FirstChild + break + } + } + } + } + + if nextData == nil { + c.log.WarnContext(ctx, "no metadata container found") + return nil, nil + } + + var out MetaInfo + dataReader := strings.NewReader(nextData.Data) + err = json.NewDecoder(dataReader).Decode(&out) + if err != nil { + return nil, fmt.Errorf("unmarshalling data: %w", err) + } + + return &out, nil +} + +type restyCtxLogger struct { + ctx context.Context + log *slog.Logger +} + +func (l restyCtxLogger) Debugf(format string, v ...any) { + msg := fmt.Sprintf(format, v...) + l.log.DebugContext(l.ctx, msg) +} + +func (l restyCtxLogger) Warnf(format string, v ...any) { + msg := fmt.Sprintf(format, v...) + l.log.WarnContext(l.ctx, msg) +} + +func (l restyCtxLogger) Errorf(format string, v ...any) { + msg := fmt.Sprintf(format, v...) + l.log.ErrorContext(l.ctx, msg) +} diff --git a/internal/infrastructure/interfaceadapters/services.go b/internal/infrastructure/interfaceadapters/services.go new file mode 100644 index 0000000..90910a7 --- /dev/null +++ b/internal/infrastructure/interfaceadapters/services.go @@ -0,0 +1,8 @@ +// Package adapters aggregates all external services and it's implementations. +package adapters + +type Services struct{} + +func NewServices() Services { + return Services{} +} diff --git a/kurious.go b/kurious.go new file mode 100644 index 0000000..ddd416f --- /dev/null +++ b/kurious.go @@ -0,0 +1,38 @@ +package kurious + +import ( + "sync" + "time" +) + +var ( + version = "unknown" + commit = "unknown" + buildTime = "" + buildTimeParsed = time.Time{} +) + +func Version() string { + return version +} + +func Commit() string { + return commit +} + +var buildTimeParseOnce sync.Once +func BuildTime() time.Time { + if buildTime == "" { + return time.Time{} + } + + buildTimeParseOnce.Do(func() { + var err error + buildTimeParsed, err = time.Parse(buildTime, time.RFC3339) + if err != nil { + panic(err.Error()) + } + }) + + return buildTimeParsed +}