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) }