From ec70f451708515f074b217148456ee19321b2db7 Mon Sep 17 00:00:00 2001 From: goriiin Date: Sun, 7 Dec 2025 16:49:57 +0300 Subject: [PATCH 01/29] bot-72: client --- .../apps/posts_command_consumer/config.go | 2 + internal/apps/posts_command_consumer/init.go | 9 +- .../posts_command_consumer/create_post.go | 62 ++++++ .../posts/posts_command_consumer/init.go | 30 ++- pkg/otvet/config.go | 29 +++ pkg/otvet/create_post.go | 186 ++++++++++++++++++ pkg/otvet/dto.go | 145 ++++++++++++++ pkg/otvet/init.go | 47 +++++ 8 files changed, 499 insertions(+), 11 deletions(-) create mode 100644 pkg/otvet/config.go create mode 100644 pkg/otvet/create_post.go create mode 100644 pkg/otvet/dto.go create mode 100644 pkg/otvet/init.go diff --git a/internal/apps/posts_command_consumer/config.go b/internal/apps/posts_command_consumer/config.go index 5e0ab90..38f8473 100644 --- a/internal/apps/posts_command_consumer/config.go +++ b/internal/apps/posts_command_consumer/config.go @@ -5,6 +5,7 @@ import ( "github.com/goriiin/kotyari-bots_backend/internal/kafka" "github.com/goriiin/kotyari-bots_backend/pkg/config" "github.com/goriiin/kotyari-bots_backend/pkg/grok" + "github.com/goriiin/kotyari-bots_backend/pkg/otvet" "github.com/goriiin/kotyari-bots_backend/pkg/postgres" "github.com/goriiin/kotyari-bots_backend/pkg/proxy" ) @@ -15,6 +16,7 @@ type PostsCommandConsumerConfig struct { Database postgres.Config `mapstructure:"posts_database"` KafkaCons kafka.KafkaConfig `mapstructure:"posts_consumer_request"` KafkaProd kafka.KafkaConfig `mapstructure:"posts_consumer_reply"` + Otvet otvet.OtvetClientConfig `mapstructure:"otvet"` } type LLMConfig struct { diff --git a/internal/apps/posts_command_consumer/init.go b/internal/apps/posts_command_consumer/init.go index 590d819..650d98b 100644 --- a/internal/apps/posts_command_consumer/init.go +++ b/internal/apps/posts_command_consumer/init.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/go-faster/errors" "github.com/goriiin/kotyari-bots_backend/internal/delivery_grpc/posts_consumer_client" "github.com/goriiin/kotyari-bots_backend/internal/delivery_http/posts/posts_command_consumer" "github.com/goriiin/kotyari-bots_backend/internal/kafka" @@ -13,6 +14,7 @@ import ( postsRepoLib "github.com/goriiin/kotyari-bots_backend/internal/repo/posts/posts_command" "github.com/goriiin/kotyari-bots_backend/pkg/evals" "github.com/goriiin/kotyari-bots_backend/pkg/grok" + "github.com/goriiin/kotyari-bots_backend/pkg/otvet" "github.com/goriiin/kotyari-bots_backend/pkg/postgres" "github.com/goriiin/kotyari-bots_backend/pkg/rewriter" ) @@ -72,8 +74,13 @@ func NewPostsCommandConsumer(config *PostsCommandConsumerConfig, llmConfig *LLMC j := evals.NewJudge(cfg, grokClient) + otvetClient, err := otvet.NewOtvetClient(&config.Otvet) + if err != nil { + return nil, errors.Wrap(err, "failed to create otvet client") + } + return &PostsCommandConsumer{ - consumerRunner: posts_command_consumer.NewPostsCommandConsumer(cons, repo, grpc, rw, j), + consumerRunner: posts_command_consumer.NewPostsCommandConsumer(cons, repo, grpc, rw, j, otvetClient), consumer: cons, config: config, }, nil diff --git a/internal/delivery_http/posts/posts_command_consumer/create_post.go b/internal/delivery_http/posts/posts_command_consumer/create_post.go index e5e50df..9140af9 100644 --- a/internal/delivery_http/posts/posts_command_consumer/create_post.go +++ b/internal/delivery_http/posts/posts_command_consumer/create_post.go @@ -10,6 +10,7 @@ import ( "github.com/google/uuid" "github.com/goriiin/kotyari-bots_backend/internal/delivery_http/posts" "github.com/goriiin/kotyari-bots_backend/internal/model" + "github.com/goriiin/kotyari-bots_backend/pkg/otvet" ) func (p *PostsCommandConsumer) CreatePost(ctx context.Context, postsMap map[uuid.UUID]model.Post, req posts.KafkaCreatePostRequest) error { @@ -67,6 +68,40 @@ func (p *PostsCommandConsumer) CreatePost(ctx context.Context, postsMap map[uuid bestPost.Text = bestPostCandidate.Text bestPost.Title = bestPostCandidate.Title + // Publish to otvet.mail.ru if platform is otveti + if req.Platform == model.OtvetiPlatform && p.otvetClient != nil { + topicType := getTopicTypeFromPostType(req.PostType) + + // Predict spaces from title + text + combinedText := bestPostCandidate.Title + " " + bestPostCandidate.Text + spaces := getDefaultSpaces() // fallback to default + + predictResp, err := p.otvetClient.PredictTagsSpaces(ctx, combinedText) + if err != nil { + fmt.Printf("error predicting spaces: %v, using default spaces\n", err) + } else if predictResp != nil && len(*predictResp) > 0 { + // Convert predicted spaces to Space format + predictedSpaces := make([]otvet.Space, 0, len((*predictResp)[0].Spaces)) + for _, spaceID := range (*predictResp)[0].Spaces { + predictedSpaces = append(predictedSpaces, otvet.Space{ + ID: spaceID, + IsPrime: true, // Default value, can be adjusted if needed + }) + } + if len(predictedSpaces) > 0 { + spaces = predictedSpaces + } + } + + otvetResp, err := p.otvetClient.CreatePostSimple(ctx, bestPostCandidate.Title, bestPostCandidate.Text, topicType, spaces) + if err != nil { + fmt.Printf("error publishing post to otvet: %v\n", err) + // Continue anyway, post will be saved without OtvetiID + } else if otvetResp != nil && otvetResp.Result != nil { + bestPost.OtvetiID = uint64(otvetResp.Result.ID) + } + } + postsChan <- bestPost }(profile) } @@ -88,3 +123,30 @@ func (p *PostsCommandConsumer) CreatePost(ctx context.Context, postsMap map[uuid return nil } + +// getTopicTypeFromPostType converts PostType to otvet topic_type +// topic_type: 2 = question (opinion), other values may be used for other types +func getTopicTypeFromPostType(postType model.PostType) int { + switch postType { + case model.OpinionPostType: + return 2 // question + case model.KnowledgePostType: + return 2 // question (can be adjusted if needed) + case model.HistoryPostType: + return 2 // question (can be adjusted if needed) + default: + return 2 // default to question + } +} + +// getDefaultSpaces returns default spaces for otvet posts +// TODO: move to config or get from request +func getDefaultSpaces() []otvet.Space { + // Default space - can be configured later + return []otvet.Space{ + { + ID: 501, // Example space ID from the response + IsPrime: true, + }, + } +} diff --git a/internal/delivery_http/posts/posts_command_consumer/init.go b/internal/delivery_http/posts/posts_command_consumer/init.go index a3f05ac..193dd7f 100644 --- a/internal/delivery_http/posts/posts_command_consumer/init.go +++ b/internal/delivery_http/posts/posts_command_consumer/init.go @@ -7,6 +7,7 @@ import ( postssgen "github.com/goriiin/kotyari-bots_backend/api/protos/posts/gen" kafkaConfig "github.com/goriiin/kotyari-bots_backend/internal/kafka" "github.com/goriiin/kotyari-bots_backend/internal/model" + "github.com/goriiin/kotyari-bots_backend/pkg/otvet" "google.golang.org/grpc" ) @@ -35,12 +36,19 @@ type judge interface { SelectBest(ctx context.Context, userPrompt, profilePrompt, botPrompt string, candidates []model.Candidate) (model.Candidate, error) } +type otvetClient interface { + CreatePost(ctx context.Context, req *otvet.CreatePostRequest) (*otvet.CreatePostResponse, error) + CreatePostSimple(ctx context.Context, title string, contentText string, topicType int, spaces []otvet.Space) (*otvet.CreatePostResponse, error) + PredictTagsSpaces(ctx context.Context, text string) (*otvet.PredictTagsSpacesResponse, error) +} + type PostsCommandConsumer struct { - consumer consumer - repo repo - getter postsGetter - rewriter rewriter - judge judge + consumer consumer + repo repo + getter postsGetter + rewriter rewriter + judge judge + otvetClient otvetClient } func NewPostsCommandConsumer( @@ -49,12 +57,14 @@ func NewPostsCommandConsumer( getter postsGetter, rewriter rewriter, judge judge, + otvetClient otvetClient, ) *PostsCommandConsumer { return &PostsCommandConsumer{ - consumer: consumer, - repo: repo, - getter: getter, - rewriter: rewriter, - judge: judge, + consumer: consumer, + repo: repo, + getter: getter, + rewriter: rewriter, + judge: judge, + otvetClient: otvetClient, } } diff --git a/pkg/otvet/config.go b/pkg/otvet/config.go new file mode 100644 index 0000000..b27fe2f --- /dev/null +++ b/pkg/otvet/config.go @@ -0,0 +1,29 @@ +package otvet + +import ( + "fmt" + "time" + + "github.com/goriiin/kotyari-bots_backend/pkg/config" +) + +const OtvetBaseURL = "https://otvet.mail.ru" + +type OtvetClientConfig struct { + config.ConfigBase + AuthToken string `mapstructure:"auth_token" env:"OTVET_AUTH_TOKEN"` + Timeout time.Duration `mapstructure:"request_timeout"` +} + +func (o *OtvetClientConfig) Validate() error { + if o.AuthToken == "" { + return fmt.Errorf("missing auth token") + } + + if o.Timeout == 0 { + o.Timeout = 30 * time.Second + } + + return nil +} + diff --git a/pkg/otvet/create_post.go b/pkg/otvet/create_post.go new file mode 100644 index 0000000..46b89e4 --- /dev/null +++ b/pkg/otvet/create_post.go @@ -0,0 +1,186 @@ +package otvet + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + + "github.com/go-faster/errors" +) + +const ( + createPostEndpoint = "/api/topic/question" + predictTagsSpacesEndpoint = "/api/tags_spaces/predict" +) + +// CreatePost creates a new post/question on otvet.mail.ru +func (c *OtvetClient) CreatePost(ctx context.Context, req *CreatePostRequest) (*CreatePostResponse, error) { + body, err := json.Marshal(req) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal request") + } + + url := c.baseURL + createPostEndpoint + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body)) + if err != nil { + return nil, errors.Wrap(err, "failed to create request") + } + + // Set headers + httpReq.Header.Set("Content-Type", "application/json") + + // Set authentication cookie + httpReq.AddCookie(&http.Cookie{ + Name: "Auth-Token", + Value: c.config.AuthToken, + }) + + // Perform request + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return nil, errors.Wrap(err, "failed to perform request") + } + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + // Log error if needed + _ = closeErr + } + }() + + // Read response body + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "failed to read response body") + } + + // Check status code + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + return nil, errors.Errorf("otvet.mail.ru returned non-2xx response status: %d, body: %s", resp.StatusCode, string(respBody)) + } + + // Parse response + var createResp CreatePostResponse + if err := json.Unmarshal(respBody, &createResp); err != nil { + // If response is not JSON or doesn't match expected structure, return raw response info + return nil, errors.Wrapf(err, "failed to unmarshal response, status: %d, body: %s", resp.StatusCode, string(respBody)) + } + + return &createResp, nil +} + +// TextContentNode represents a simple text content node +type TextContentNode struct { + Type string `json:"type"` + Text string `json:"text"` +} + +// CreatePostSimple is a helper function to create a simple post with minimal required fields +func (c *OtvetClient) CreatePostSimple(ctx context.Context, title string, contentText string, topicType int, spaces []Space) (*CreatePostResponse, error) { + req := &CreatePostRequest{ + Title: title, + TopicType: topicType, + Version: 0, + VisibleTo: 0, + Content: &Content{ + Type: "doc", + Content: []ContentNode{ + { + Type: "paragraph", + Content: []interface{}{ + TextContentNode{ + Type: "text", + Text: contentText, + }, + }, + }, + }, + }, + Tags: []Tag{}, + Spaces: spaces, + } + + return c.CreatePost(ctx, req) +} + +// NewTextContent creates a simple text content structure +func NewTextContent(text string) *Content { + return &Content{ + Type: "doc", + Content: []ContentNode{ + { + Type: "paragraph", + Content: []interface{}{ + TextContentNode{ + Type: "text", + Text: text, + }, + }, + }, + }, + } +} + +// PredictTagsSpaces predicts tags and spaces for given text +func (c *OtvetClient) PredictTagsSpaces(ctx context.Context, text string) (*PredictTagsSpacesResponse, error) { + req := PredictTagsSpacesRequest{ + Data: []PredictDataItem{ + { + ID: "0", + Text: text, + }, + }, + } + + body, err := json.Marshal(req) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal request") + } + + url := c.baseURL + predictTagsSpacesEndpoint + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body)) + if err != nil { + return nil, errors.Wrap(err, "failed to create request") + } + + // Set headers + httpReq.Header.Set("Content-Type", "application/json") + + // Set authentication cookie + httpReq.AddCookie(&http.Cookie{ + Name: "Auth-Token", + Value: c.config.AuthToken, + }) + + // Perform request + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return nil, errors.Wrap(err, "failed to perform request") + } + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + // Log error if needed + _ = closeErr + } + }() + + // Read response body + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "failed to read response body") + } + + // Check status code + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + return nil, errors.Errorf("otvet.mail.ru returned non-2xx response status: %d, body: %s", resp.StatusCode, string(respBody)) + } + + // Parse response + var predictResp PredictTagsSpacesResponse + if err := json.Unmarshal(respBody, &predictResp); err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal response, status: %d, body: %s", resp.StatusCode, string(respBody)) + } + + return &predictResp, nil +} diff --git a/pkg/otvet/dto.go b/pkg/otvet/dto.go new file mode 100644 index 0000000..7d57ac1 --- /dev/null +++ b/pkg/otvet/dto.go @@ -0,0 +1,145 @@ +package otvet + +// CreatePostRequest represents the request body for creating a post +type CreatePostRequest struct { + Content *Content `json:"content"` + ID *int `json:"id,omitempty"` + Poll *Poll `json:"poll,omitempty"` + Spaces []Space `json:"spaces,omitempty"` + Tags []Tag `json:"tags,omitempty"` + Title string `json:"title"` + TopicType int `json:"topic_type"` + Version int `json:"version"` + VisibleTo int `json:"visible_to"` + AuthorID *int `json:"author_id,omitempty"` +} + +// Content represents the content structure +type Content struct { + Type string `json:"type"` + Content []ContentNode `json:"content"` +} + +// ContentNode represents a node in the content tree +type ContentNode struct { + Attrs map[string]interface{} `json:"attrs,omitempty"` + Content []interface{} `json:"content,omitempty"` + Marks []Mark `json:"marks,omitempty"` + Text string `json:"text,omitempty"` + Type string `json:"type"` +} + +// Mark represents a mark in the content +type Mark struct { + Attrs map[string]interface{} `json:"attrs,omitempty"` + Type string `json:"type"` +} + +// Poll represents a poll structure +type Poll struct { + ID int `json:"id"` + Multiple bool `json:"multiple"` + Polls []PollOption `json:"polls"` + Quiz bool `json:"quiz"` + Title string `json:"title"` +} + +// PollOption represents a poll option +type PollOption struct { + Correct bool `json:"correct"` + ID int `json:"id"` + Title string `json:"title"` + VotedByCurrentUser bool `json:"voted_by_current_user"` + Votes int `json:"votes"` +} + +// Space represents a space structure +type Space struct { + Adult bool `json:"adult"` + Banner string `json:"banner,omitempty"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + ID int `json:"id"` + IsPrime bool `json:"is_prime"` + IsSubscribed bool `json:"is_subscribed"` + OrderSpace int `json:"order_space,omitempty"` + Path string `json:"path,omitempty"` + SpaceCounters *SpaceCounters `json:"space_counters,omitempty"` + Title string `json:"title,omitempty"` +} + +// SpaceCounters represents space counters +type SpaceCounters struct { + SubscriptionCount int `json:"subscription_count"` + TopicCount int `json:"topic_count"` +} + +// Tag represents a tag structure +type Tag struct { + ID int `json:"id"` + Name string `json:"name"` + Title string `json:"title"` +} + +// CreatePostResponse represents the response from creating a post +type CreatePostResponse struct { + Result *PostResult `json:"result"` +} + +// PostResult represents the post data in the response +type PostResult struct { + ID int64 `json:"id"` + Title string `json:"title"` + Content *Content `json:"content"` + Author *Author `json:"author"` + RepliesCount int `json:"replies_count"` + RepliesViewCount int `json:"replies_view_count"` + Tags *[]Tag `json:"tags"` + Spaces []Space `json:"spaces"` + TopicType int `json:"topic_type"` + Poll *Poll `json:"poll,omitempty"` + ReactionCounter []ReactionCounter `json:"reaction_counter"` + CreatedAt string `json:"created_at"` + UpdateAt string `json:"update_at"` + IsBookmarked bool `json:"isBookmarked"` + VisibleTo int `json:"visible_to"` +} + +// Author represents the author information +type Author struct { + ID int64 `json:"id"` + Nick string `json:"nick"` + Avatar string `json:"avatar"` + Username string `json:"username"` + Level int `json:"level"` + CreatedAt string `json:"created_at"` + UserStatus int `json:"user_status"` +} + +// ReactionCounter represents a reaction counter +type ReactionCounter struct { + // Add fields as needed when API documentation is available + // For now, keeping it flexible +} + +// PredictTagsSpacesRequest represents the request for tags/spaces prediction +type PredictTagsSpacesRequest struct { + Data []PredictDataItem `json:"data"` +} + +// PredictDataItem represents a single item in the prediction request +type PredictDataItem struct { + ID string `json:"id"` + Text string `json:"text"` +} + +// PredictTagsSpacesResponse represents the response from tags/spaces prediction +type PredictTagsSpacesResponse []PredictResult + +// PredictResult represents a single prediction result +type PredictResult struct { + ID int `json:"id"` + Tags []string `json:"tags"` + Spaces []int `json:"spaces"` + MajorCat int `json:"major_cat"` +} diff --git a/pkg/otvet/init.go b/pkg/otvet/init.go new file mode 100644 index 0000000..63df869 --- /dev/null +++ b/pkg/otvet/init.go @@ -0,0 +1,47 @@ +package otvet + +import ( + "net/http" +) + +// OtvetClient is the client for otvet.mail.ru API +type OtvetClient struct { + config *OtvetClientConfig + httpClient *http.Client + baseURL string +} + +// OtvetClientOption is a function type for client options +type OtvetClientOption func(*OtvetClient) + +// WithBaseURL sets a custom base URL for the client +func WithBaseURL(baseURL string) OtvetClientOption { + return func(c *OtvetClient) { + if baseURL != "" { + c.baseURL = baseURL + } + } +} + +// NewOtvetClient creates a new OtvetClient instance +func NewOtvetClient(config *OtvetClientConfig, opts ...OtvetClientOption) (*OtvetClient, error) { + if err := config.Validate(); err != nil { + return nil, err + } + + httpClient := &http.Client{ + Timeout: config.Timeout, + } + + client := &OtvetClient{ + config: config, + httpClient: httpClient, + baseURL: OtvetBaseURL, + } + + for _, opt := range opts { + opt(client) + } + + return client, nil +} From d7e241737ab6e11cc3276736e417b1a0182b810b Mon Sep 17 00:00:00 2001 From: goriiin Date: Sun, 7 Dec 2025 16:52:19 +0300 Subject: [PATCH 02/29] bot-72: format --- pkg/otvet/config.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/otvet/config.go b/pkg/otvet/config.go index b27fe2f..16364f7 100644 --- a/pkg/otvet/config.go +++ b/pkg/otvet/config.go @@ -26,4 +26,3 @@ func (o *OtvetClientConfig) Validate() error { return nil } - From 63675a704da96addca747d1697182ed98d99f338 Mon Sep 17 00:00:00 2001 From: goriiin Date: Sun, 7 Dec 2025 16:58:19 +0300 Subject: [PATCH 03/29] bot-74: lint --- .../posts_command_consumer/create_post.go | 222 +++++++++++------- 1 file changed, 137 insertions(+), 85 deletions(-) diff --git a/internal/delivery_http/posts/posts_command_consumer/create_post.go b/internal/delivery_http/posts/posts_command_consumer/create_post.go index 9140af9..cb5b6f2 100644 --- a/internal/delivery_http/posts/posts_command_consumer/create_post.go +++ b/internal/delivery_http/posts/posts_command_consumer/create_post.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "sync" - "time" "github.com/go-faster/errors" "github.com/google/uuid" @@ -16,93 +15,14 @@ import ( func (p *PostsCommandConsumer) CreatePost(ctx context.Context, postsMap map[uuid.UUID]model.Post, req posts.KafkaCreatePostRequest) error { postsChan := make(chan model.Post, len(req.Profiles)) var wg sync.WaitGroup - for _, profile := range req.Profiles { wg.Add(1) - go func(prof posts.CreatePostProfiles) { + go func(profile posts.CreatePostProfiles) { defer wg.Done() - - profileCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) - defer cancel() - - var mutex sync.Mutex - profileWg := sync.WaitGroup{} - profilesPosts := make([]model.Post, 0, 5) - - rewritten, err := p.rewriter.Rewrite(profileCtx, req.UserPrompt, prof.ProfilePrompt, req.BotPrompt) - if err != nil { - fmt.Println("error rewriting prompts:", err) - return - } - - for _, rw := range rewritten { - profileWg.Add(1) - go func(rewrittenText string) { - defer profileWg.Done() - - generatedPostContent, err := p.getter.GetPost(profileCtx, rewrittenText, prof.ProfilePrompt, req.BotPrompt) - if err != nil { - fmt.Println("error getting post:", err) - return - } - - post := postsMap[prof.ProfileID] - post.Title = generatedPostContent.PostTitle - post.Text = generatedPostContent.PostText - - mutex.Lock() - profilesPosts = append(profilesPosts, post) - mutex.Unlock() - }(rw) + post := p.processProfile(ctx, req, profile, postsMap) + if post != nil { + postsChan <- *post } - - profileWg.Wait() - - bestPostCandidate, err := p.judge.SelectBest(profileCtx, req.UserPrompt, prof.ProfilePrompt, req.BotPrompt, posts.PostsToCandidates(profilesPosts)) - if err != nil { - fmt.Println("error getting best post:", err) - return - } - - bestPost := postsMap[prof.ProfileID] - bestPost.Text = bestPostCandidate.Text - bestPost.Title = bestPostCandidate.Title - - // Publish to otvet.mail.ru if platform is otveti - if req.Platform == model.OtvetiPlatform && p.otvetClient != nil { - topicType := getTopicTypeFromPostType(req.PostType) - - // Predict spaces from title + text - combinedText := bestPostCandidate.Title + " " + bestPostCandidate.Text - spaces := getDefaultSpaces() // fallback to default - - predictResp, err := p.otvetClient.PredictTagsSpaces(ctx, combinedText) - if err != nil { - fmt.Printf("error predicting spaces: %v, using default spaces\n", err) - } else if predictResp != nil && len(*predictResp) > 0 { - // Convert predicted spaces to Space format - predictedSpaces := make([]otvet.Space, 0, len((*predictResp)[0].Spaces)) - for _, spaceID := range (*predictResp)[0].Spaces { - predictedSpaces = append(predictedSpaces, otvet.Space{ - ID: spaceID, - IsPrime: true, // Default value, can be adjusted if needed - }) - } - if len(predictedSpaces) > 0 { - spaces = predictedSpaces - } - } - - otvetResp, err := p.otvetClient.CreatePostSimple(ctx, bestPostCandidate.Title, bestPostCandidate.Text, topicType, spaces) - if err != nil { - fmt.Printf("error publishing post to otvet: %v\n", err) - // Continue anyway, post will be saved without OtvetiID - } else if otvetResp != nil && otvetResp.Result != nil { - bestPost.OtvetiID = uint64(otvetResp.Result.ID) - } - } - - postsChan <- bestPost }(profile) } @@ -118,12 +38,144 @@ func (p *PostsCommandConsumer) CreatePost(ctx context.Context, postsMap map[uuid err := p.repo.UpdatePostsBatch(ctx, finalPosts) if err != nil { - return errors.Wrap(err, "failed to update posts") + return errors.Wrap(err, "failed to create posts") } return nil } +// processProfile processes a single profile and returns the best post +func (p *PostsCommandConsumer) processProfile(ctx context.Context, req posts.KafkaCreatePostRequest, profile posts.CreatePostProfiles, postsMap map[uuid.UUID]model.Post) *model.Post { + profilesPosts := p.generatePostsForProfile(ctx, req, profile, postsMap) + if len(profilesPosts) == 0 { + return nil + } + + bestPostCandidate, err := p.judge.SelectBest(ctx, req.UserPrompt, profile.ProfilePrompt, req.BotPrompt, + posts.PostsToCandidates(profilesPosts)) + if err != nil { + fmt.Println("error getting best post ", err) + return nil + } + + bestPost := p.createPostFromCandidate(req, profile, bestPostCandidate) + p.publishToOtvet(ctx, req, bestPostCandidate, bestPost) + + return bestPost +} + +// generatePostsForProfile generates multiple post candidates for a profile +func (p *PostsCommandConsumer) generatePostsForProfile(ctx context.Context, req posts.KafkaCreatePostRequest, profile posts.CreatePostProfiles, postsMap map[uuid.UUID]model.Post) []model.Post { + rewritten, err := p.rewriter.Rewrite(ctx, req.UserPrompt, profile.ProfilePrompt, req.BotPrompt) + if err != nil { + fmt.Println("error rewriting prompts", err) + return nil + } + + var ( + mutex sync.Mutex + profileWg sync.WaitGroup + ) + + profilesPosts := make([]model.Post, 0, len(rewritten)) + + for _, rw := range rewritten { + profileWg.Add(1) + go func(rewrittenPrompt string) { + defer profileWg.Done() + + generatedPostContent, err := p.getter.GetPost(ctx, rewrittenPrompt, profile.ProfilePrompt, req.BotPrompt) + if err != nil { + fmt.Println("error getting post", err) + return + } + + post := postsMap[profile.ProfileID] + post.Title = generatedPostContent.PostTitle + post.Text = generatedPostContent.PostText + + mutex.Lock() + profilesPosts = append(profilesPosts, post) + mutex.Unlock() + }(rw) + } + + profileWg.Wait() + return profilesPosts +} + + +// createPostFromCandidate creates a Post model from a candidate +func (p *PostsCommandConsumer) createPostFromCandidate(req posts.KafkaCreatePostRequest, profile posts.CreatePostProfiles, candidate model.Candidate) *model.Post { + return &model.Post{ + ID: uuid.New(), + OtvetiID: 0, + BotID: req.BotID, + BotName: req.BotName, + ProfileID: profile.ProfileID, + ProfileName: profile.ProfileName, + GroupID: req.GroupID, + Platform: req.Platform, + Type: req.PostType, + UserPrompt: req.UserPrompt, + Title: candidate.Title, + Text: candidate.Text, + } +} + + +// publishToOtvet publishes post to otvet.mail.ru if platform is otveti +func (p *PostsCommandConsumer) publishToOtvet(ctx context.Context, req posts.KafkaCreatePostRequest, candidate model.Candidate, post *model.Post) { + if req.Platform != model.OtvetiPlatform || p.otvetClient == nil { + return + } + + topicType := getTopicTypeFromPostType(req.PostType) + spaces := p.getSpacesForPost(ctx, candidate) + + otvetResp, err := p.otvetClient.CreatePostSimple(ctx, candidate.Title, candidate.Text, topicType, spaces) + if err != nil { + fmt.Printf("error publishing post to otvet: %v\n", err) + return + } + + if otvetResp != nil && otvetResp.Result != nil { + post.OtvetiID = uint64(otvetResp.Result.ID) + } +} + + +// getSpacesForPost predicts spaces for a post or returns default spaces +func (p *PostsCommandConsumer) getSpacesForPost(ctx context.Context, candidate model.Candidate) []otvet.Space { + combinedText := candidate.Title + " " + candidate.Text + spaces := getDefaultSpaces() + + predictResp, err := p.otvetClient.PredictTagsSpaces(ctx, combinedText) + if err != nil { + fmt.Printf("error predicting spaces: %v, using default spaces\n", err) + return spaces + } + + if predictResp == nil || len(*predictResp) == 0 { + return spaces + } + + // Convert predicted spaces to Space format + predictedSpaces := make([]otvet.Space, 0, len((*predictResp)[0].Spaces)) + for _, spaceID := range (*predictResp)[0].Spaces { + predictedSpaces = append(predictedSpaces, otvet.Space{ + ID: spaceID, + IsPrime: true, + }) + } + + if len(predictedSpaces) > 0 { + return predictedSpaces + } + + return spaces +} + // getTopicTypeFromPostType converts PostType to otvet topic_type // topic_type: 2 = question (opinion), other values may be used for other types func getTopicTypeFromPostType(postType model.PostType) int { From cc1d5744a0a0c5fba2194efc72586cfd41c8890b Mon Sep 17 00:00:00 2001 From: goriiin Date: Sun, 7 Dec 2025 17:00:11 +0300 Subject: [PATCH 04/29] bot-74: api --- internal/gen/bots/oas_handlers_gen.go | 4 + internal/gen/bots/oas_router_gen.go | 28 +++- internal/gen/bots/oas_validators_gen.go | 18 ++- .../posts/posts_command/oas_handlers_gen.go | 4 + .../gen/posts/posts_command/oas_router_gen.go | 21 ++- .../gen/posts/posts_query/oas_handlers_gen.go | 4 + .../gen/posts/posts_query/oas_router_gen.go | 131 +++++++----------- internal/gen/profiles/oas_handlers_gen.go | 4 + internal/gen/profiles/oas_router_gen.go | 23 ++- internal/gen/profiles/oas_validators_gen.go | 36 +++-- 10 files changed, 150 insertions(+), 123 deletions(-) diff --git a/internal/gen/bots/oas_handlers_gen.go b/internal/gen/bots/oas_handlers_gen.go index c0ced4f..ce33cd0 100644 --- a/internal/gen/bots/oas_handlers_gen.go +++ b/internal/gen/bots/oas_handlers_gen.go @@ -29,6 +29,10 @@ func (c *codeRecorder) WriteHeader(status int) { c.ResponseWriter.WriteHeader(status) } +func (c *codeRecorder) Unwrap() http.ResponseWriter { + return c.ResponseWriter +} + // handleAddProfileToBotRequest handles AddProfileToBot operation. // // Привязать профиль к боту. diff --git a/internal/gen/bots/oas_router_gen.go b/internal/gen/bots/oas_router_gen.go index cecfd86..61a3c5f 100644 --- a/internal/gen/bots/oas_router_gen.go +++ b/internal/gen/bots/oas_router_gen.go @@ -239,12 +239,13 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Route is route object. type Route struct { - name string - summary string - operationID string - pathPattern string - count int - args [2]string + name string + summary string + operationID string + operationGroup string + pathPattern string + count int + args [2]string } // Name returns ogen operation name. @@ -264,6 +265,11 @@ func (r Route) OperationID() string { return r.operationID } +// OperationGroup returns the x-ogen-operation-group value. +func (r Route) OperationGroup() string { + return r.operationGroup +} + // PathPattern returns OpenAPI path. func (r Route) PathPattern() string { return r.pathPattern @@ -326,6 +332,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = ListBotsOperation r.summary = "Получить список своих ботов" r.operationID = "ListBots" + r.operationGroup = "" r.pathPattern = "/api/v1/bots" r.args = args r.count = 0 @@ -334,6 +341,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = CreateBotOperation r.summary = "Создать нового бота" r.operationID = "CreateBot" + r.operationGroup = "" r.pathPattern = "/api/v1/bots" r.args = args r.count = 0 @@ -382,6 +390,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = SearchBotsOperation r.summary = "Поиск ботов по названию или системному промпту" r.operationID = "SearchBots" + r.operationGroup = "" r.pathPattern = "/api/v1/bots/search" r.args = args r.count = 0 @@ -406,6 +415,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = SummaryBotsOperation r.summary = "Получить сводную информацию по ботам" r.operationID = "SummaryBots" + r.operationGroup = "" r.pathPattern = "/api/v1/bots/summary" r.args = args r.count = 0 @@ -434,6 +444,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = DeleteBotByIdOperation r.summary = "Удалить бота по ID" r.operationID = "DeleteBotById" + r.operationGroup = "" r.pathPattern = "/api/v1/bots/{botId}" r.args = args r.count = 1 @@ -442,6 +453,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = GetBotByIdOperation r.summary = "Получить бота по ID" r.operationID = "GetBotById" + r.operationGroup = "" r.pathPattern = "/api/v1/bots/{botId}" r.args = args r.count = 1 @@ -450,6 +462,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = UpdateBotByIdOperation r.summary = "Полностью обновить бота по ID" r.operationID = "UpdateBotById" + r.operationGroup = "" r.pathPattern = "/api/v1/bots/{botId}" r.args = args r.count = 1 @@ -473,6 +486,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = GetBotProfilesOperation r.summary = "Получить список профилей, привязанных к боту" r.operationID = "GetBotProfiles" + r.operationGroup = "" r.pathPattern = "/api/v1/bots/{botId}/profiles" r.args = args r.count = 1 @@ -506,6 +520,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = RemoveProfileFromBotOperation r.summary = "Отвязать профиль от бота" r.operationID = "RemoveProfileFromBot" + r.operationGroup = "" r.pathPattern = "/api/v1/bots/{botId}/profiles/{profileId}" r.args = args r.count = 2 @@ -514,6 +529,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = AddProfileToBotOperation r.summary = "Привязать профиль к боту" r.operationID = "AddProfileToBot" + r.operationGroup = "" r.pathPattern = "/api/v1/bots/{botId}/profiles/{profileId}" r.args = args r.count = 2 diff --git a/internal/gen/bots/oas_validators_gen.go b/internal/gen/bots/oas_validators_gen.go index 0cc8596..fea0c6b 100644 --- a/internal/gen/bots/oas_validators_gen.go +++ b/internal/gen/bots/oas_validators_gen.go @@ -130,13 +130,17 @@ func (s *Profile) Validate() error { if value, ok := s.Email.Get(); ok { if err := func() error { if err := (validate.String{ - MinLength: 0, - MinLengthSet: false, - MaxLength: 0, - MaxLengthSet: false, - Email: true, - Hostname: false, - Regex: nil, + MinLength: 0, + MinLengthSet: false, + MaxLength: 0, + MaxLengthSet: false, + Email: true, + Hostname: false, + Regex: nil, + MinNumeric: 0, + MinNumericSet: false, + MaxNumeric: 0, + MaxNumericSet: false, }).Validate(string(value)); err != nil { return errors.Wrap(err, "string") } diff --git a/internal/gen/posts/posts_command/oas_handlers_gen.go b/internal/gen/posts/posts_command/oas_handlers_gen.go index 2b3b174..74a5e44 100644 --- a/internal/gen/posts/posts_command/oas_handlers_gen.go +++ b/internal/gen/posts/posts_command/oas_handlers_gen.go @@ -29,6 +29,10 @@ func (c *codeRecorder) WriteHeader(status int) { c.ResponseWriter.WriteHeader(status) } +func (c *codeRecorder) Unwrap() http.ResponseWriter { + return c.ResponseWriter +} + // handleCreatePostRequest handles createPost operation. // // Создать новые посты. diff --git a/internal/gen/posts/posts_command/oas_router_gen.go b/internal/gen/posts/posts_command/oas_router_gen.go index b43abe2..b0577a3 100644 --- a/internal/gen/posts/posts_command/oas_router_gen.go +++ b/internal/gen/posts/posts_command/oas_router_gen.go @@ -138,12 +138,13 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Route is route object. type Route struct { - name string - summary string - operationID string - pathPattern string - count int - args [1]string + name string + summary string + operationID string + operationGroup string + pathPattern string + count int + args [1]string } // Name returns ogen operation name. @@ -163,6 +164,11 @@ func (r Route) OperationID() string { return r.operationID } +// OperationGroup returns the x-ogen-operation-group value. +func (r Route) OperationGroup() string { + return r.operationGroup +} + // PathPattern returns OpenAPI path. func (r Route) PathPattern() string { return r.pathPattern @@ -225,6 +231,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = CreatePostOperation r.summary = "Создать новые посты" r.operationID = "createPost" + r.operationGroup = "" r.pathPattern = "/api/v1/posts" r.args = args r.count = 0 @@ -288,6 +295,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = DeletePostByIdOperation r.summary = "Удалить пост по ID" r.operationID = "deletePostById" + r.operationGroup = "" r.pathPattern = "/api/v1/posts/{postId}" r.args = args r.count = 1 @@ -296,6 +304,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = UpdatePostByIdOperation r.summary = "Обновить пост по ID" r.operationID = "updatePostById" + r.operationGroup = "" r.pathPattern = "/api/v1/posts/{postId}" r.args = args r.count = 1 diff --git a/internal/gen/posts/posts_query/oas_handlers_gen.go b/internal/gen/posts/posts_query/oas_handlers_gen.go index 1925892..d0e2cc4 100644 --- a/internal/gen/posts/posts_query/oas_handlers_gen.go +++ b/internal/gen/posts/posts_query/oas_handlers_gen.go @@ -29,6 +29,10 @@ func (c *codeRecorder) WriteHeader(status int) { c.ResponseWriter.WriteHeader(status) } +func (c *codeRecorder) Unwrap() http.ResponseWriter { + return c.ResponseWriter +} + // handleCheckGroupIdRequest handles checkGroupId operation. // // Проверить статус готовности отпределенного. diff --git a/internal/gen/posts/posts_query/oas_router_gen.go b/internal/gen/posts/posts_query/oas_router_gen.go index e3c4739..3a17b43 100644 --- a/internal/gen/posts/posts_query/oas_router_gen.go +++ b/internal/gen/posts/posts_query/oas_router_gen.go @@ -80,57 +80,36 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { break } switch elem[0] { - case 'c': // Prefix: "check" + case 'c': // Prefix: "check/" origElem := elem - if l := len("check"); len(elem) >= l && elem[0:l] == "check" { + if l := len("check/"); len(elem) >= l && elem[0:l] == "check/" { elem = elem[l:] } else { break } + // Param: "groupId" + // Leaf parameter, slashes are prohibited + idx := strings.IndexByte(elem, '/') + if idx >= 0 { + break + } + args[0] = elem + elem = "" + if len(elem) == 0 { + // Leaf node. switch r.Method { case "GET": - s.handleCheckGroupIdsRequest([0]string{}, elemIsEscaped, w, r) + s.handleCheckGroupIdRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) default: s.notAllowed(w, r, "GET") } return } - switch elem[0] { - case '/': // Prefix: "/" - - if l := len("/"); len(elem) >= l && elem[0:l] == "/" { - elem = elem[l:] - } else { - break - } - - // Param: "groupId" - // Leaf parameter, slashes are prohibited - idx := strings.IndexByte(elem, '/') - if idx >= 0 { - break - } - args[0] = elem - elem = "" - - if len(elem) == 0 { - // Leaf node. - switch r.Method { - case "GET": - s.handleCheckGroupIdRequest([1]string{ - args[0], - }, elemIsEscaped, w, r) - default: - s.notAllowed(w, r, "GET") - } - - return - } - - } elem = origElem } @@ -166,12 +145,13 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Route is route object. type Route struct { - name string - summary string - operationID string - pathPattern string - count int - args [1]string + name string + summary string + operationID string + operationGroup string + pathPattern string + count int + args [1]string } // Name returns ogen operation name. @@ -191,6 +171,11 @@ func (r Route) OperationID() string { return r.operationID } +// OperationGroup returns the x-ogen-operation-group value. +func (r Route) OperationGroup() string { + return r.operationGroup +} + // PathPattern returns OpenAPI path. func (r Route) PathPattern() string { return r.pathPattern @@ -253,6 +238,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = ListPostsOperation r.summary = "Получить список постов" r.operationID = "listPosts" + r.operationGroup = "" r.pathPattern = "/api/v1/posts" r.args = args r.count = 0 @@ -274,63 +260,39 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { break } switch elem[0] { - case 'c': // Prefix: "check" + case 'c': // Prefix: "check/" origElem := elem - if l := len("check"); len(elem) >= l && elem[0:l] == "check" { + if l := len("check/"); len(elem) >= l && elem[0:l] == "check/" { elem = elem[l:] } else { break } + // Param: "groupId" + // Leaf parameter, slashes are prohibited + idx := strings.IndexByte(elem, '/') + if idx >= 0 { + break + } + args[0] = elem + elem = "" + if len(elem) == 0 { + // Leaf node. switch method { case "GET": - r.name = CheckGroupIdsOperation - r.summary = "Проверить статус готовности всех постов" - r.operationID = "checkGroupIds" - r.pathPattern = "/api/v1/posts/check" + r.name = CheckGroupIdOperation + r.summary = "Проверить статус готовности постов" + r.operationID = "checkGroupId" + r.operationGroup = "" + r.pathPattern = "/api/v1/posts/check/{groupId}" r.args = args - r.count = 0 + r.count = 1 return r, true default: return } } - switch elem[0] { - case '/': // Prefix: "/" - - if l := len("/"); len(elem) >= l && elem[0:l] == "/" { - elem = elem[l:] - } else { - break - } - - // Param: "groupId" - // Leaf parameter, slashes are prohibited - idx := strings.IndexByte(elem, '/') - if idx >= 0 { - break - } - args[0] = elem - elem = "" - - if len(elem) == 0 { - // Leaf node. - switch method { - case "GET": - r.name = CheckGroupIdOperation - r.summary = "Проверить статус готовности отпределенного" - r.operationID = "checkGroupId" - r.pathPattern = "/api/v1/posts/check/{groupId}" - r.args = args - r.count = 1 - return r, true - default: - return - } - } - - } elem = origElem } @@ -350,6 +312,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = GetPostByIdOperation r.summary = "Получить пост по ID" r.operationID = "getPostById" + r.operationGroup = "" r.pathPattern = "/api/v1/posts/{postId}" r.args = args r.count = 1 diff --git a/internal/gen/profiles/oas_handlers_gen.go b/internal/gen/profiles/oas_handlers_gen.go index 7fb0eff..9e56de0 100644 --- a/internal/gen/profiles/oas_handlers_gen.go +++ b/internal/gen/profiles/oas_handlers_gen.go @@ -29,6 +29,10 @@ func (c *codeRecorder) WriteHeader(status int) { c.ResponseWriter.WriteHeader(status) } +func (c *codeRecorder) Unwrap() http.ResponseWriter { + return c.ResponseWriter +} + // handleCreateMyProfileRequest handles createMyProfile operation. // // Создает новый профиль и связывает его с текущим diff --git a/internal/gen/profiles/oas_router_gen.go b/internal/gen/profiles/oas_router_gen.go index 29b2527..602a2d8 100644 --- a/internal/gen/profiles/oas_router_gen.go +++ b/internal/gen/profiles/oas_router_gen.go @@ -118,12 +118,13 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Route is route object. type Route struct { - name string - summary string - operationID string - pathPattern string - count int - args [1]string + name string + summary string + operationID string + operationGroup string + pathPattern string + count int + args [1]string } // Name returns ogen operation name. @@ -143,6 +144,11 @@ func (r Route) OperationID() string { return r.operationID } +// OperationGroup returns the x-ogen-operation-group value. +func (r Route) OperationGroup() string { + return r.operationGroup +} + // PathPattern returns OpenAPI path. func (r Route) PathPattern() string { return r.pathPattern @@ -205,6 +211,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = ListMyProfilesOperation r.summary = "Получить список своих профилей" r.operationID = "listMyProfiles" + r.operationGroup = "" r.pathPattern = "/api/v1/profiles" r.args = args r.count = 0 @@ -213,6 +220,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = CreateMyProfileOperation r.summary = "Создать новый профиль" r.operationID = "createMyProfile" + r.operationGroup = "" r.pathPattern = "/api/v1/profiles" r.args = args r.count = 0 @@ -246,6 +254,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = DeleteProfileByIdOperation r.summary = "Удалить профиль по ID" r.operationID = "deleteProfileById" + r.operationGroup = "" r.pathPattern = "/api/v1/profiles/{profileId}" r.args = args r.count = 1 @@ -254,6 +263,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = GetProfileByIdOperation r.summary = "Получить профиль по ID" r.operationID = "getProfileById" + r.operationGroup = "" r.pathPattern = "/api/v1/profiles/{profileId}" r.args = args r.count = 1 @@ -262,6 +272,7 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = UpdateProfileByIdOperation r.summary = "Обновить профиль по ID" r.operationID = "updateProfileById" + r.operationGroup = "" r.pathPattern = "/api/v1/profiles/{profileId}" r.args = args r.count = 1 diff --git a/internal/gen/profiles/oas_validators_gen.go b/internal/gen/profiles/oas_validators_gen.go index 7d10d7d..c2a6124 100644 --- a/internal/gen/profiles/oas_validators_gen.go +++ b/internal/gen/profiles/oas_validators_gen.go @@ -17,13 +17,17 @@ func (s *Profile) Validate() error { var failures []validate.FieldError if err := func() error { if err := (validate.String{ - MinLength: 0, - MinLengthSet: false, - MaxLength: 0, - MaxLengthSet: false, - Email: true, - Hostname: false, - Regex: nil, + MinLength: 0, + MinLengthSet: false, + MaxLength: 0, + MaxLengthSet: false, + Email: true, + Hostname: false, + Regex: nil, + MinNumeric: 0, + MinNumericSet: false, + MaxNumeric: 0, + MaxNumericSet: false, }).Validate(string(s.Email)); err != nil { return errors.Wrap(err, "string") } @@ -48,13 +52,17 @@ func (s *ProfileInput) Validate() error { var failures []validate.FieldError if err := func() error { if err := (validate.String{ - MinLength: 0, - MinLengthSet: false, - MaxLength: 0, - MaxLengthSet: false, - Email: true, - Hostname: false, - Regex: nil, + MinLength: 0, + MinLengthSet: false, + MaxLength: 0, + MaxLengthSet: false, + Email: true, + Hostname: false, + Regex: nil, + MinNumeric: 0, + MinNumericSet: false, + MaxNumeric: 0, + MaxNumericSet: false, }).Validate(string(s.Email)); err != nil { return errors.Wrap(err, "string") } From 2e0e3d5b5859397fcdf26b4f5b5dec04e7c750fd Mon Sep 17 00:00:00 2001 From: goriiin Date: Sun, 7 Dec 2025 17:21:13 +0300 Subject: [PATCH 05/29] bot-74: ogen --- go.mod | 20 +++++----- go.sum | 40 +++++++++---------- internal/gen/bots/oas_handlers_gen.go | 4 -- internal/gen/bots/oas_router_gen.go | 28 +++---------- internal/gen/bots/oas_validators_gen.go | 18 ++++----- .../posts/posts_command/oas_handlers_gen.go | 4 -- .../gen/posts/posts_command/oas_router_gen.go | 21 +++------- .../gen/posts/posts_query/oas_handlers_gen.go | 4 -- .../gen/posts/posts_query/oas_router_gen.go | 21 +++------- internal/gen/profiles/oas_handlers_gen.go | 4 -- internal/gen/profiles/oas_router_gen.go | 23 +++-------- internal/gen/profiles/oas_validators_gen.go | 36 +++++++---------- 12 files changed, 75 insertions(+), 148 deletions(-) diff --git a/go.mod b/go.mod index d8521fe..5f635de 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,13 @@ go 1.24.5 require ( github.com/fsnotify/fsnotify v1.8.0 github.com/go-faster/errors v0.7.1 - github.com/go-faster/jx v1.1.0 + github.com/go-faster/jx v1.2.0 github.com/golang-migrate/migrate/v4 v4.19.0 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.7.5 github.com/joho/godotenv v1.5.1 github.com/json-iterator/go v1.1.12 - github.com/ogen-go/ogen v1.16.0 + github.com/ogen-go/ogen v1.18.0 github.com/pkg/errors v0.9.1 github.com/rs/cors v1.11.1 github.com/rs/zerolog v1.34.0 @@ -20,8 +20,8 @@ require ( go.opentelemetry.io/otel v1.38.0 go.opentelemetry.io/otel/metric v1.38.0 go.opentelemetry.io/otel/trace v1.38.0 - golang.org/x/net v0.44.0 - golang.org/x/sync v0.17.0 + golang.org/x/net v0.47.0 + golang.org/x/sync v0.18.0 google.golang.org/grpc v1.67.3 google.golang.org/protobuf v1.36.1 ) @@ -40,7 +40,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect @@ -57,11 +57,11 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.42.0 // indirect - golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect + go.uber.org/zap v1.27.1 // indirect + golang.org/x/crypto v0.44.0 // indirect + golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index f26dc00..facd33a 100644 --- a/go.sum +++ b/go.sum @@ -34,8 +34,8 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= -github.com/go-faster/jx v1.1.0 h1:ZsW3wD+snOdmTDy9eIVgQdjUpXRRV4rqW8NS3t+20bg= -github.com/go-faster/jx v1.1.0/go.mod h1:vKDNikrKoyUmpzaJ0OkIkRQClNHFX/nF3dnTJZb3skg= +github.com/go-faster/jx v1.2.0 h1:T2YHJPrFaYu21fJtUxC9GzmluKu8rVIFDwwGBKTDseI= +github.com/go-faster/jx v1.2.0/go.mod h1:UWLOVDmMG597a5tBFPLIWJdUxz5/2emOpfsj9Neg0PE= github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I= github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -74,8 +74,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -99,8 +99,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/ogen-go/ogen v1.16.0 h1:fKHEYokW/QrMzVNXId74/6RObRIUs9T2oroGKtR25Iw= -github.com/ogen-go/ogen v1.16.0/go.mod h1:s3nWiMzybSf8fhxckyO+wtto92+QHpEL8FmkPnhL3jI= +github.com/ogen-go/ogen v1.18.0 h1:6RQ7lFBjOeNaUWu4getfqIh4GJbEY4hqKuzDtec/g60= +github.com/ogen-go/ogen v1.18.0/go.mod h1:dHFr2Wf6cA7tSxMI+zPC21UR5hAlDw8ZYUkK3PziURY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -165,23 +165,23 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= -golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= -golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= -golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= -golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY= +golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A= google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8= diff --git a/internal/gen/bots/oas_handlers_gen.go b/internal/gen/bots/oas_handlers_gen.go index ce33cd0..c0ced4f 100644 --- a/internal/gen/bots/oas_handlers_gen.go +++ b/internal/gen/bots/oas_handlers_gen.go @@ -29,10 +29,6 @@ func (c *codeRecorder) WriteHeader(status int) { c.ResponseWriter.WriteHeader(status) } -func (c *codeRecorder) Unwrap() http.ResponseWriter { - return c.ResponseWriter -} - // handleAddProfileToBotRequest handles AddProfileToBot operation. // // Привязать профиль к боту. diff --git a/internal/gen/bots/oas_router_gen.go b/internal/gen/bots/oas_router_gen.go index 61a3c5f..cecfd86 100644 --- a/internal/gen/bots/oas_router_gen.go +++ b/internal/gen/bots/oas_router_gen.go @@ -239,13 +239,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Route is route object. type Route struct { - name string - summary string - operationID string - operationGroup string - pathPattern string - count int - args [2]string + name string + summary string + operationID string + pathPattern string + count int + args [2]string } // Name returns ogen operation name. @@ -265,11 +264,6 @@ func (r Route) OperationID() string { return r.operationID } -// OperationGroup returns the x-ogen-operation-group value. -func (r Route) OperationGroup() string { - return r.operationGroup -} - // PathPattern returns OpenAPI path. func (r Route) PathPattern() string { return r.pathPattern @@ -332,7 +326,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = ListBotsOperation r.summary = "Получить список своих ботов" r.operationID = "ListBots" - r.operationGroup = "" r.pathPattern = "/api/v1/bots" r.args = args r.count = 0 @@ -341,7 +334,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = CreateBotOperation r.summary = "Создать нового бота" r.operationID = "CreateBot" - r.operationGroup = "" r.pathPattern = "/api/v1/bots" r.args = args r.count = 0 @@ -390,7 +382,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = SearchBotsOperation r.summary = "Поиск ботов по названию или системному промпту" r.operationID = "SearchBots" - r.operationGroup = "" r.pathPattern = "/api/v1/bots/search" r.args = args r.count = 0 @@ -415,7 +406,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = SummaryBotsOperation r.summary = "Получить сводную информацию по ботам" r.operationID = "SummaryBots" - r.operationGroup = "" r.pathPattern = "/api/v1/bots/summary" r.args = args r.count = 0 @@ -444,7 +434,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = DeleteBotByIdOperation r.summary = "Удалить бота по ID" r.operationID = "DeleteBotById" - r.operationGroup = "" r.pathPattern = "/api/v1/bots/{botId}" r.args = args r.count = 1 @@ -453,7 +442,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = GetBotByIdOperation r.summary = "Получить бота по ID" r.operationID = "GetBotById" - r.operationGroup = "" r.pathPattern = "/api/v1/bots/{botId}" r.args = args r.count = 1 @@ -462,7 +450,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = UpdateBotByIdOperation r.summary = "Полностью обновить бота по ID" r.operationID = "UpdateBotById" - r.operationGroup = "" r.pathPattern = "/api/v1/bots/{botId}" r.args = args r.count = 1 @@ -486,7 +473,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = GetBotProfilesOperation r.summary = "Получить список профилей, привязанных к боту" r.operationID = "GetBotProfiles" - r.operationGroup = "" r.pathPattern = "/api/v1/bots/{botId}/profiles" r.args = args r.count = 1 @@ -520,7 +506,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = RemoveProfileFromBotOperation r.summary = "Отвязать профиль от бота" r.operationID = "RemoveProfileFromBot" - r.operationGroup = "" r.pathPattern = "/api/v1/bots/{botId}/profiles/{profileId}" r.args = args r.count = 2 @@ -529,7 +514,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = AddProfileToBotOperation r.summary = "Привязать профиль к боту" r.operationID = "AddProfileToBot" - r.operationGroup = "" r.pathPattern = "/api/v1/bots/{botId}/profiles/{profileId}" r.args = args r.count = 2 diff --git a/internal/gen/bots/oas_validators_gen.go b/internal/gen/bots/oas_validators_gen.go index fea0c6b..0cc8596 100644 --- a/internal/gen/bots/oas_validators_gen.go +++ b/internal/gen/bots/oas_validators_gen.go @@ -130,17 +130,13 @@ func (s *Profile) Validate() error { if value, ok := s.Email.Get(); ok { if err := func() error { if err := (validate.String{ - MinLength: 0, - MinLengthSet: false, - MaxLength: 0, - MaxLengthSet: false, - Email: true, - Hostname: false, - Regex: nil, - MinNumeric: 0, - MinNumericSet: false, - MaxNumeric: 0, - MaxNumericSet: false, + MinLength: 0, + MinLengthSet: false, + MaxLength: 0, + MaxLengthSet: false, + Email: true, + Hostname: false, + Regex: nil, }).Validate(string(value)); err != nil { return errors.Wrap(err, "string") } diff --git a/internal/gen/posts/posts_command/oas_handlers_gen.go b/internal/gen/posts/posts_command/oas_handlers_gen.go index 74a5e44..2b3b174 100644 --- a/internal/gen/posts/posts_command/oas_handlers_gen.go +++ b/internal/gen/posts/posts_command/oas_handlers_gen.go @@ -29,10 +29,6 @@ func (c *codeRecorder) WriteHeader(status int) { c.ResponseWriter.WriteHeader(status) } -func (c *codeRecorder) Unwrap() http.ResponseWriter { - return c.ResponseWriter -} - // handleCreatePostRequest handles createPost operation. // // Создать новые посты. diff --git a/internal/gen/posts/posts_command/oas_router_gen.go b/internal/gen/posts/posts_command/oas_router_gen.go index b0577a3..b43abe2 100644 --- a/internal/gen/posts/posts_command/oas_router_gen.go +++ b/internal/gen/posts/posts_command/oas_router_gen.go @@ -138,13 +138,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Route is route object. type Route struct { - name string - summary string - operationID string - operationGroup string - pathPattern string - count int - args [1]string + name string + summary string + operationID string + pathPattern string + count int + args [1]string } // Name returns ogen operation name. @@ -164,11 +163,6 @@ func (r Route) OperationID() string { return r.operationID } -// OperationGroup returns the x-ogen-operation-group value. -func (r Route) OperationGroup() string { - return r.operationGroup -} - // PathPattern returns OpenAPI path. func (r Route) PathPattern() string { return r.pathPattern @@ -231,7 +225,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = CreatePostOperation r.summary = "Создать новые посты" r.operationID = "createPost" - r.operationGroup = "" r.pathPattern = "/api/v1/posts" r.args = args r.count = 0 @@ -295,7 +288,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = DeletePostByIdOperation r.summary = "Удалить пост по ID" r.operationID = "deletePostById" - r.operationGroup = "" r.pathPattern = "/api/v1/posts/{postId}" r.args = args r.count = 1 @@ -304,7 +296,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = UpdatePostByIdOperation r.summary = "Обновить пост по ID" r.operationID = "updatePostById" - r.operationGroup = "" r.pathPattern = "/api/v1/posts/{postId}" r.args = args r.count = 1 diff --git a/internal/gen/posts/posts_query/oas_handlers_gen.go b/internal/gen/posts/posts_query/oas_handlers_gen.go index d0e2cc4..1925892 100644 --- a/internal/gen/posts/posts_query/oas_handlers_gen.go +++ b/internal/gen/posts/posts_query/oas_handlers_gen.go @@ -29,10 +29,6 @@ func (c *codeRecorder) WriteHeader(status int) { c.ResponseWriter.WriteHeader(status) } -func (c *codeRecorder) Unwrap() http.ResponseWriter { - return c.ResponseWriter -} - // handleCheckGroupIdRequest handles checkGroupId operation. // // Проверить статус готовности отпределенного. diff --git a/internal/gen/posts/posts_query/oas_router_gen.go b/internal/gen/posts/posts_query/oas_router_gen.go index 3a17b43..98c82fc 100644 --- a/internal/gen/posts/posts_query/oas_router_gen.go +++ b/internal/gen/posts/posts_query/oas_router_gen.go @@ -145,13 +145,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Route is route object. type Route struct { - name string - summary string - operationID string - operationGroup string - pathPattern string - count int - args [1]string + name string + summary string + operationID string + pathPattern string + count int + args [1]string } // Name returns ogen operation name. @@ -171,11 +170,6 @@ func (r Route) OperationID() string { return r.operationID } -// OperationGroup returns the x-ogen-operation-group value. -func (r Route) OperationGroup() string { - return r.operationGroup -} - // PathPattern returns OpenAPI path. func (r Route) PathPattern() string { return r.pathPattern @@ -238,7 +232,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = ListPostsOperation r.summary = "Получить список постов" r.operationID = "listPosts" - r.operationGroup = "" r.pathPattern = "/api/v1/posts" r.args = args r.count = 0 @@ -284,7 +277,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = CheckGroupIdOperation r.summary = "Проверить статус готовности постов" r.operationID = "checkGroupId" - r.operationGroup = "" r.pathPattern = "/api/v1/posts/check/{groupId}" r.args = args r.count = 1 @@ -312,7 +304,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = GetPostByIdOperation r.summary = "Получить пост по ID" r.operationID = "getPostById" - r.operationGroup = "" r.pathPattern = "/api/v1/posts/{postId}" r.args = args r.count = 1 diff --git a/internal/gen/profiles/oas_handlers_gen.go b/internal/gen/profiles/oas_handlers_gen.go index 9e56de0..7fb0eff 100644 --- a/internal/gen/profiles/oas_handlers_gen.go +++ b/internal/gen/profiles/oas_handlers_gen.go @@ -29,10 +29,6 @@ func (c *codeRecorder) WriteHeader(status int) { c.ResponseWriter.WriteHeader(status) } -func (c *codeRecorder) Unwrap() http.ResponseWriter { - return c.ResponseWriter -} - // handleCreateMyProfileRequest handles createMyProfile operation. // // Создает новый профиль и связывает его с текущим diff --git a/internal/gen/profiles/oas_router_gen.go b/internal/gen/profiles/oas_router_gen.go index 602a2d8..29b2527 100644 --- a/internal/gen/profiles/oas_router_gen.go +++ b/internal/gen/profiles/oas_router_gen.go @@ -118,13 +118,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Route is route object. type Route struct { - name string - summary string - operationID string - operationGroup string - pathPattern string - count int - args [1]string + name string + summary string + operationID string + pathPattern string + count int + args [1]string } // Name returns ogen operation name. @@ -144,11 +143,6 @@ func (r Route) OperationID() string { return r.operationID } -// OperationGroup returns the x-ogen-operation-group value. -func (r Route) OperationGroup() string { - return r.operationGroup -} - // PathPattern returns OpenAPI path. func (r Route) PathPattern() string { return r.pathPattern @@ -211,7 +205,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = ListMyProfilesOperation r.summary = "Получить список своих профилей" r.operationID = "listMyProfiles" - r.operationGroup = "" r.pathPattern = "/api/v1/profiles" r.args = args r.count = 0 @@ -220,7 +213,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = CreateMyProfileOperation r.summary = "Создать новый профиль" r.operationID = "createMyProfile" - r.operationGroup = "" r.pathPattern = "/api/v1/profiles" r.args = args r.count = 0 @@ -254,7 +246,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = DeleteProfileByIdOperation r.summary = "Удалить профиль по ID" r.operationID = "deleteProfileById" - r.operationGroup = "" r.pathPattern = "/api/v1/profiles/{profileId}" r.args = args r.count = 1 @@ -263,7 +254,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = GetProfileByIdOperation r.summary = "Получить профиль по ID" r.operationID = "getProfileById" - r.operationGroup = "" r.pathPattern = "/api/v1/profiles/{profileId}" r.args = args r.count = 1 @@ -272,7 +262,6 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { r.name = UpdateProfileByIdOperation r.summary = "Обновить профиль по ID" r.operationID = "updateProfileById" - r.operationGroup = "" r.pathPattern = "/api/v1/profiles/{profileId}" r.args = args r.count = 1 diff --git a/internal/gen/profiles/oas_validators_gen.go b/internal/gen/profiles/oas_validators_gen.go index c2a6124..7d10d7d 100644 --- a/internal/gen/profiles/oas_validators_gen.go +++ b/internal/gen/profiles/oas_validators_gen.go @@ -17,17 +17,13 @@ func (s *Profile) Validate() error { var failures []validate.FieldError if err := func() error { if err := (validate.String{ - MinLength: 0, - MinLengthSet: false, - MaxLength: 0, - MaxLengthSet: false, - Email: true, - Hostname: false, - Regex: nil, - MinNumeric: 0, - MinNumericSet: false, - MaxNumeric: 0, - MaxNumericSet: false, + MinLength: 0, + MinLengthSet: false, + MaxLength: 0, + MaxLengthSet: false, + Email: true, + Hostname: false, + Regex: nil, }).Validate(string(s.Email)); err != nil { return errors.Wrap(err, "string") } @@ -52,17 +48,13 @@ func (s *ProfileInput) Validate() error { var failures []validate.FieldError if err := func() error { if err := (validate.String{ - MinLength: 0, - MinLengthSet: false, - MaxLength: 0, - MaxLengthSet: false, - Email: true, - Hostname: false, - Regex: nil, - MinNumeric: 0, - MinNumericSet: false, - MaxNumeric: 0, - MaxNumericSet: false, + MinLength: 0, + MinLengthSet: false, + MaxLength: 0, + MaxLengthSet: false, + Email: true, + Hostname: false, + Regex: nil, }).Validate(string(s.Email)); err != nil { return errors.Wrap(err, "string") } From 7f44eac9a8820d1532fb107343510f7a579ab7cd Mon Sep 17 00:00:00 2001 From: goriiin Date: Sun, 7 Dec 2025 18:12:42 +0300 Subject: [PATCH 06/29] bot-72: print log --- .../posts/posts_command_consumer/create_post.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/delivery_http/posts/posts_command_consumer/create_post.go b/internal/delivery_http/posts/posts_command_consumer/create_post.go index cb5b6f2..c6db497 100644 --- a/internal/delivery_http/posts/posts_command_consumer/create_post.go +++ b/internal/delivery_http/posts/posts_command_consumer/create_post.go @@ -3,6 +3,7 @@ package posts_command_consumer import ( "context" "fmt" + "log" "sync" "github.com/go-faster/errors" @@ -133,12 +134,16 @@ func (p *PostsCommandConsumer) publishToOtvet(ctx context.Context, req posts.Kaf topicType := getTopicTypeFromPostType(req.PostType) spaces := p.getSpacesForPost(ctx, candidate) + log.Printf("INFO: topicType: %+v\t spaces: %+v\n", topicType, spaces) + otvetResp, err := p.otvetClient.CreatePostSimple(ctx, candidate.Title, candidate.Text, topicType, spaces) if err != nil { fmt.Printf("error publishing post to otvet: %v\n", err) return } + log.Printf("INFO: published post to otvet: %v\n", otvetResp) + if otvetResp != nil && otvetResp.Result != nil { post.OtvetiID = uint64(otvetResp.Result.ID) } From 73982234f8595b62a9c365d8a18f53bf7233d37e Mon Sep 17 00:00:00 2001 From: goriiin Date: Wed, 10 Dec 2025 02:04:30 +0300 Subject: [PATCH 07/29] bot-72: posting --- api/protos/bots/gen/get_bot.pb.go | 30 ++- api/protos/bots/proto/get_bot.proto | 2 + api/protos/posts/proto/posts.proto | 10 + configs/posts-local.yaml | 11 +- docs/posts/common/schemas.yaml | 28 +- docs/posts/posts_command/openapi.yaml | 30 +++ .../apps/posts_command_consumer/config.go | 9 + internal/apps/posts_command_consumer/init.go | 54 +++- internal/apps/posts_command_producer/init.go | 1 + internal/delivery_http/posts/kafka_dto.go | 14 +- .../posts_command_consumer/create_post.go | 23 +- .../posts/posts_command_consumer/handler.go | 24 +- .../posts/posts_command_consumer/init.go | 12 + .../posts_command_producer/create_post.go | 30 +-- .../posts_command_producer/publish_post.go | 63 +++++ pkg/posting_queue/errors.go | 8 + pkg/posting_queue/queue.go | 239 ++++++++++++++++++ 17 files changed, 538 insertions(+), 50 deletions(-) create mode 100644 internal/delivery_http/posts/posts_command_producer/publish_post.go create mode 100644 pkg/posting_queue/errors.go create mode 100644 pkg/posting_queue/queue.go diff --git a/api/protos/bots/gen/get_bot.pb.go b/api/protos/bots/gen/get_bot.pb.go index 4ce6b62..cf3aee1 100644 --- a/api/protos/bots/gen/get_bot.pb.go +++ b/api/protos/bots/gen/get_bot.pb.go @@ -7,11 +7,12 @@ package gen import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( @@ -22,12 +23,14 @@ const ( ) type Bot struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - BotPrompt string `protobuf:"bytes,2,opt,name=bot_prompt,json=botPrompt,proto3" json:"bot_prompt,omitempty"` - BotName string `protobuf:"bytes,3,opt,name=bot_name,json=botName,proto3" json:"bot_name,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + BotPrompt string `protobuf:"bytes,2,opt,name=bot_prompt,json=botPrompt,proto3" json:"bot_prompt,omitempty"` + BotName string `protobuf:"bytes,3,opt,name=bot_name,json=botName,proto3" json:"bot_name,omitempty"` + // Added moderation flag from proto + ModerationRequired bool `protobuf:"varint,4,opt,name=moderation_required,json=moderationRequired,proto3" json:"moderation_required,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Bot) Reset() { @@ -81,6 +84,14 @@ func (x *Bot) GetBotName() string { return "" } +// GetModerationRequired returns true if moderation is required for this bot +func (x *Bot) GetModerationRequired() bool { + if x != nil { + return x.ModerationRequired + } + return false +} + type GetBotRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` @@ -134,7 +145,8 @@ const file_get_bot_proto_rawDesc = "" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n" + "\n" + "bot_prompt\x18\x02 \x01(\tR\tbotPrompt\x12\x19\n" + - "\bbot_name\x18\x03 \x01(\tR\abotName\"\x1f\n" + + "\bbot_name\x18\x03 \x01(\tR\abotName\x12\x35\n" + + "\x15moderation_required\x18\x04 \x01(\tR\x15moderationRequired\"\x1f\n" + "\rGetBotRequest\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id26\n" + "\n" + diff --git a/api/protos/bots/proto/get_bot.proto b/api/protos/bots/proto/get_bot.proto index 43b3df8..41b1189 100644 --- a/api/protos/bots/proto/get_bot.proto +++ b/api/protos/bots/proto/get_bot.proto @@ -12,6 +12,8 @@ message Bot { string id = 1; string bot_prompt = 2; string bot_name = 3; + // Indicates whether posts created by this bot require moderation + bool moderation_required = 4; } message GetBotRequest { diff --git a/api/protos/posts/proto/posts.proto b/api/protos/posts/proto/posts.proto index 6f75c05..348b925 100644 --- a/api/protos/posts/proto/posts.proto +++ b/api/protos/posts/proto/posts.proto @@ -7,6 +7,7 @@ option go_package = "github.com/goriiin/kotyari-bots_backend/api/protos/posts/ge service PostsService { rpc GetPost(GetPostRequest) returns (GetPostResponse); rpc GetPostsBatch(GetPostsRequest) returns (GetPostsResponse); + rpc ApprovePost(ApprovePostRequest) returns (ApprovePostResponse); } message GetPostRequest { @@ -27,3 +28,12 @@ message GetPostsRequest { message GetPostsResponse { repeated GetPostResponse posts_response = 1; } + +message ApprovePostRequest { + string post_id = 1; +} + +message ApprovePostResponse { + bool success = 1; + string message = 2; +} diff --git a/configs/posts-local.yaml b/configs/posts-local.yaml index d386b85..52b14d8 100644 --- a/configs/posts-local.yaml +++ b/configs/posts-local.yaml @@ -47,4 +47,13 @@ posts_consumer_reply: proxy: proxy_server: host: "xray-proxy" - port: 8200 \ No newline at end of file + port: 8200 + +posting_queue: + moderation_required: false + posting_interval: 30m + processing_interval: 1m + +otvet: + auth_token: "" # Set via LOCAL_OTVET_AUTH_TOKEN env var + request_timeout: 30s \ No newline at end of file diff --git a/docs/posts/common/schemas.yaml b/docs/posts/common/schemas.yaml index 7c73608..bdbc0fc 100644 --- a/docs/posts/common/schemas.yaml +++ b/docs/posts/common/schemas.yaml @@ -262,4 +262,30 @@ Error: text: "Поле не может быть пустым." required: - errorCode - - message \ No newline at end of file + - message + +PublishPostRequest: + type: object + description: "Данные для публикации поста." + properties: + approved: + type: boolean + description: "Одобрен ли пост для публикации" + example: true + required: + - approved + +PublishPostResponse: + type: object + description: "Результат публикации поста." + properties: + success: + type: boolean + description: "Успешно ли опубликован пост" + example: true + message: + type: string + description: "Сообщение о результате" + example: "Пост успешно опубликован" + required: + - success \ No newline at end of file diff --git a/docs/posts/posts_command/openapi.yaml b/docs/posts/posts_command/openapi.yaml index 1a9f937..3283d6f 100644 --- a/docs/posts/posts_command/openapi.yaml +++ b/docs/posts/posts_command/openapi.yaml @@ -122,5 +122,35 @@ paths: $ref: '../common/responses.yaml#/Unauthorized' '404': $ref: '../common/responses.yaml#/NotFound' + '500': + $ref: '../common/responses.yaml#/InternalServerError' + + /api/v1/posts/{postId}/publish: + post: + summary: "Опубликовать пост (для модерации)" + operationId: "publishPost" + tags: ["Posts"] + parameters: + - $ref: '../common/parameters.yaml#/PostID' + requestBody: + required: true + description: "Данные для публикации поста." + content: + application/json: + schema: + $ref: '../common/schemas.yaml#/PublishPostRequest' + responses: + '200': + description: "Пост успешно опубликован." + content: + application/json: + schema: + $ref: '../common/schemas.yaml#/PublishPostResponse' + '400': + $ref: '../common/responses.yaml#/BadRequest' + '401': + $ref: '../common/responses.yaml#/Unauthorized' + '404': + $ref: '../common/responses.yaml#/NotFound' '500': $ref: '../common/responses.yaml#/InternalServerError' \ No newline at end of file diff --git a/internal/apps/posts_command_consumer/config.go b/internal/apps/posts_command_consumer/config.go index 38f8473..16af19c 100644 --- a/internal/apps/posts_command_consumer/config.go +++ b/internal/apps/posts_command_consumer/config.go @@ -1,6 +1,8 @@ package posts_command_consumer import ( + "time" + "github.com/goriiin/kotyari-bots_backend/internal/delivery_grpc/posts_consumer_client" "github.com/goriiin/kotyari-bots_backend/internal/kafka" "github.com/goriiin/kotyari-bots_backend/pkg/config" @@ -17,6 +19,13 @@ type PostsCommandConsumerConfig struct { KafkaCons kafka.KafkaConfig `mapstructure:"posts_consumer_request"` KafkaProd kafka.KafkaConfig `mapstructure:"posts_consumer_reply"` Otvet otvet.OtvetClientConfig `mapstructure:"otvet"` + PostingQueue PostingQueueConfig `mapstructure:"posting_queue"` +} + +type PostingQueueConfig struct { + ModerationRequired bool `mapstructure:"moderation_required"` + PostingInterval time.Duration `mapstructure:"posting_interval"` + ProcessingInterval time.Duration `mapstructure:"processing_interval"` } type LLMConfig struct { diff --git a/internal/apps/posts_command_consumer/init.go b/internal/apps/posts_command_consumer/init.go index 650d98b..7218be0 100644 --- a/internal/apps/posts_command_consumer/init.go +++ b/internal/apps/posts_command_consumer/init.go @@ -15,6 +15,7 @@ import ( "github.com/goriiin/kotyari-bots_backend/pkg/evals" "github.com/goriiin/kotyari-bots_backend/pkg/grok" "github.com/goriiin/kotyari-bots_backend/pkg/otvet" + "github.com/goriiin/kotyari-bots_backend/pkg/posting_queue" "github.com/goriiin/kotyari-bots_backend/pkg/postgres" "github.com/goriiin/kotyari-bots_backend/pkg/rewriter" ) @@ -79,9 +80,60 @@ func NewPostsCommandConsumer(config *PostsCommandConsumerConfig, llmConfig *LLMC return nil, errors.Wrap(err, "failed to create otvet client") } + // Initialize posting queue + postingInterval := config.PostingQueue.PostingInterval + if postingInterval == 0 { + postingInterval = 30 * time.Minute // default + } + + processingInterval := config.PostingQueue.ProcessingInterval + if processingInterval == 0 { + processingInterval = 1 * time.Minute // default + } + + queue := posting_queue.NewQueue( + postingInterval, + processingInterval, + config.PostingQueue.ModerationRequired, + ) + + // Add account to queue (using auth token as account ID for now) + accountID := "default" // Can be extended to support multiple accounts + queue.AddAccount(accountID, config.Otvet.AuthToken, otvetClient) + + // Start queue processing in background + ctx := context.Background() + go queue.StartProcessing(ctx, func(ctx context.Context, account *posting_queue.Account, post *posting_queue.QueuedPost) error { + return publishPostFromQueue(ctx, account, post) + }) + return &PostsCommandConsumer{ - consumerRunner: posts_command_consumer.NewPostsCommandConsumer(cons, repo, grpc, rw, j, otvetClient), + consumerRunner: posts_command_consumer.NewPostsCommandConsumer(cons, repo, grpc, rw, j, otvetClient, queue), consumer: cons, config: config, }, nil } + +// publishPostFromQueue publishes a post from the queue +func publishPostFromQueue(ctx context.Context, account *posting_queue.Account, queuedPost *posting_queue.QueuedPost) error { + if account.Client == nil { + return errors.New("account client is nil") + } + + otvetResp, err := account.Client.CreatePostSimple( + ctx, + queuedPost.Candidate.Title, + queuedPost.Candidate.Text, + queuedPost.Request.TopicType, + queuedPost.Request.Spaces, + ) + if err != nil { + return errors.Wrap(err, "failed to publish post from queue") + } + + if otvetResp != nil && otvetResp.Result != nil { + queuedPost.Post.OtvetiID = uint64(otvetResp.Result.ID) + } + + return nil +} diff --git a/internal/apps/posts_command_producer/init.go b/internal/apps/posts_command_producer/init.go index 0290608..70a9a28 100644 --- a/internal/apps/posts_command_producer/init.go +++ b/internal/apps/posts_command_producer/init.go @@ -18,6 +18,7 @@ type postsCommandHandler interface { CreatePost(ctx context.Context, req *gen.PostInput) (gen.CreatePostRes, error) UpdatePostById(ctx context.Context, req *gen.PostUpdate, params gen.UpdatePostByIdParams) (gen.UpdatePostByIdRes, error) DeletePostById(ctx context.Context, params gen.DeletePostByIdParams) (gen.DeletePostByIdRes, error) + PublishPost(ctx context.Context, req *gen.PublishPostRequest, params gen.PublishPostParams) (gen.PublishPostRes, error) SeenPosts(ctx context.Context, req *gen.PostsSeenRequest) (gen.SeenPostsRes, error) } diff --git a/internal/delivery_http/posts/kafka_dto.go b/internal/delivery_http/posts/kafka_dto.go index 4716eec..0567e89 100644 --- a/internal/delivery_http/posts/kafka_dto.go +++ b/internal/delivery_http/posts/kafka_dto.go @@ -8,9 +8,10 @@ import ( ) const ( - CmdCreate kafkaConfig.Command = "create" - CmdUpdate kafkaConfig.Command = "update" - CmdDelete kafkaConfig.Command = "delete" + CmdCreate kafkaConfig.Command = "create" + CmdUpdate kafkaConfig.Command = "update" + CmdDelete kafkaConfig.Command = "delete" + CmdPublish kafkaConfig.Command = "publish" CmdSeen kafkaConfig.Command = "seen" ) @@ -33,6 +34,8 @@ type KafkaCreatePostRequest struct { Profiles []CreatePostProfiles `json:"profiles"` Platform model.PlatformType `json:"platform_type"` PostType model.PostType `json:"post_type"` + // ModerationRequired indicates whether posts from this bot require moderation before publishing + ModerationRequired bool `json:"moderation_required"` } type CreatePostProfiles struct { @@ -51,6 +54,11 @@ type KafkaSeenPostsRequest struct { PostIDs []uuid.UUID `json:"post_ids"` } +type KafkaPublishPostRequest struct { + PostID uuid.UUID `json:"post_id"` + Approved bool `json:"approved"` +} + func PayloadToEnvelope(command kafkaConfig.Command, entityID string, payload []byte) kafkaConfig.Envelope { return kafkaConfig.Envelope{ Command: command, diff --git a/internal/delivery_http/posts/posts_command_consumer/create_post.go b/internal/delivery_http/posts/posts_command_consumer/create_post.go index c6db497..07e472e 100644 --- a/internal/delivery_http/posts/posts_command_consumer/create_post.go +++ b/internal/delivery_http/posts/posts_command_consumer/create_post.go @@ -11,6 +11,7 @@ import ( "github.com/goriiin/kotyari-bots_backend/internal/delivery_http/posts" "github.com/goriiin/kotyari-bots_backend/internal/model" "github.com/goriiin/kotyari-bots_backend/pkg/otvet" + "github.com/goriiin/kotyari-bots_backend/pkg/posting_queue" ) func (p *PostsCommandConsumer) CreatePost(ctx context.Context, postsMap map[uuid.UUID]model.Post, req posts.KafkaCreatePostRequest) error { @@ -134,7 +135,27 @@ func (p *PostsCommandConsumer) publishToOtvet(ctx context.Context, req posts.Kaf topicType := getTopicTypeFromPostType(req.PostType) spaces := p.getSpacesForPost(ctx, candidate) - log.Printf("INFO: topicType: %+v\t spaces: %+v\n", topicType, spaces) + // If queue is available, add to queue instead of publishing directly + if p.queue != nil { + postRequest := posting_queue.PostRequest{ + Platform: req.Platform, + PostType: req.PostType, + TopicType: topicType, + Spaces: spaces, + // per-request moderation flag from bot + ModerationRequired: req.ModerationRequired, + } + p.queue.Enqueue(post, candidate, postRequest) + return + } + + // Fallback to direct publishing if queue is not available + // Respect bot-level moderation flag: if moderation required, do not publish directly + if req.ModerationRequired { + // Create post in DB but skip publish since moderation is required + fmt.Printf("bot requires moderation, skipping direct publish for post %s\n", post.ID.String()) + return + } otvetResp, err := p.otvetClient.CreatePostSimple(ctx, candidate.Title, candidate.Text, topicType, spaces) if err != nil { diff --git a/internal/delivery_http/posts/posts_command_consumer/handler.go b/internal/delivery_http/posts/posts_command_consumer/handler.go index dd021d9..253f225 100644 --- a/internal/delivery_http/posts/posts_command_consumer/handler.go +++ b/internal/delivery_http/posts/posts_command_consumer/handler.go @@ -32,7 +32,8 @@ func (p *PostsCommandConsumer) HandleCommands() error { err = p.handleCreateCommand(ctx, message, env.Payload) case posts.CmdSeen: err = p.handleSeenCommand(ctx, message, env.Payload) - + case posts.CmdPublish: + err = p.handlePublishCommand(ctx, message, env.Payload) default: err = errors.Errorf("unknown command received: %s", env.Command) } @@ -110,19 +111,28 @@ func (p *PostsCommandConsumer) handleCreateCommand(ctx context.Context, message return nil } -func (p *PostsCommandConsumer) handleSeenCommand(ctx context.Context, message kafkaConfig.CommittableMessage, payload []byte) error { - err := p.SeenPosts(ctx, payload) +func (p *PostsCommandConsumer) handlePublishCommand(ctx context.Context, message kafkaConfig.CommittableMessage, payload []byte) error { + var req posts.KafkaPublishPostRequest + err := jsoniter.Unmarshal(payload, &req) if err != nil { - return sendErrReply(ctx, message, err) + return sendErrReply(ctx, message, errors.Wrap(err, "failed to unmarshal")) } - resp := posts.KafkaResponse{} - rawResp, err := jsoniter.Marshal(resp) + if p.queue == nil { + return sendErrReply(ctx, message, errors.New("queue not available")) + } + + err = p.queue.ApprovePost(req.PostID) + if err != nil { + return sendErrReply(ctx, message, errors.Wrap(err, "failed to approve post")) + } + + resp, err := jsoniter.Marshal(posts.KafkaResponse{}) if err != nil { return errors.Wrap(err, constants.MarshalMsg) } - if err := message.Reply(ctx, rawResp, true); err != nil { + if err := message.Reply(ctx, resp); err != nil { return errors.Wrap(err, failedToSendReplyMsg) } diff --git a/internal/delivery_http/posts/posts_command_consumer/init.go b/internal/delivery_http/posts/posts_command_consumer/init.go index 193dd7f..a32b304 100644 --- a/internal/delivery_http/posts/posts_command_consumer/init.go +++ b/internal/delivery_http/posts/posts_command_consumer/init.go @@ -8,6 +8,7 @@ import ( kafkaConfig "github.com/goriiin/kotyari-bots_backend/internal/kafka" "github.com/goriiin/kotyari-bots_backend/internal/model" "github.com/goriiin/kotyari-bots_backend/pkg/otvet" + "github.com/goriiin/kotyari-bots_backend/pkg/posting_queue" "google.golang.org/grpc" ) @@ -42,6 +43,14 @@ type otvetClient interface { PredictTagsSpaces(ctx context.Context, text string) (*otvet.PredictTagsSpacesResponse, error) } +type postingQueue interface { + Enqueue(post *model.Post, candidate model.Candidate, req posting_queue.PostRequest) *posting_queue.QueuedPost + ApprovePost(postID uuid.UUID) error + GetPostByID(postID uuid.UUID) (*posting_queue.QueuedPost, error) + StartProcessing(ctx context.Context, publishFunc func(ctx context.Context, account *posting_queue.Account, post *posting_queue.QueuedPost) error) + Stop() +} + type PostsCommandConsumer struct { consumer consumer repo repo @@ -49,6 +58,7 @@ type PostsCommandConsumer struct { rewriter rewriter judge judge otvetClient otvetClient + queue postingQueue } func NewPostsCommandConsumer( @@ -58,6 +68,7 @@ func NewPostsCommandConsumer( rewriter rewriter, judge judge, otvetClient otvetClient, + queue postingQueue, ) *PostsCommandConsumer { return &PostsCommandConsumer{ consumer: consumer, @@ -66,5 +77,6 @@ func NewPostsCommandConsumer( rewriter: rewriter, judge: judge, otvetClient: otvetClient, + queue: queue, } } diff --git a/internal/delivery_http/posts/posts_command_producer/create_post.go b/internal/delivery_http/posts/posts_command_producer/create_post.go index 192afae..d7ca0bc 100644 --- a/internal/delivery_http/posts/posts_command_producer/create_post.go +++ b/internal/delivery_http/posts/posts_command_producer/create_post.go @@ -35,33 +35,6 @@ func (p *PostsCommandHandler) CreatePost(ctx context.Context, req *gen.PostInput fmt.Printf("profiles batch: %+v\n", idsString) - // mockedBot := struct { - // Id uuid.UUID - // BotPrompt string - // BotName string - // }{ - // req.BotId, - // "Промт бота", - // "Крутой бот", - // } - // - // mockedProfiles := []struct { - // Id uuid.UUID - // ProfilePrompt string - // ProfileName string - // }{ - // { - // uuid.New(), - // "Крутой промт профиля", - // "Профиль 1", - // }, - // { - // uuid.New(), - // "Супер-пупер промт", - // "Профиль 2", - // }, - // } - postProfiles := make([]posts.CreatePostProfiles, 0, len(idsString)) for _, profile := range profilesBatch.Profiles { profileID, _ := uuid.Parse(profile.Id) @@ -75,6 +48,7 @@ func (p *PostsCommandHandler) CreatePost(ctx context.Context, req *gen.PostInput groupID := uuid.New() botID, _ := uuid.Parse(bot.Id) createPostRequest := posts.KafkaCreatePostRequest{ + PostID: uuid.New(), GroupID: groupID, BotID: botID, BotName: bot.BotName, @@ -83,6 +57,8 @@ func (p *PostsCommandHandler) CreatePost(ctx context.Context, req *gen.PostInput Profiles: postProfiles, Platform: model.PlatformType(req.Platform), PostType: model.PostType(req.PostType.Value), + // pass moderation flag from bot + ModerationRequired: bot.ModerationRequired, } fmt.Printf("%+v\n", createPostRequest) diff --git a/internal/delivery_http/posts/posts_command_producer/publish_post.go b/internal/delivery_http/posts/posts_command_producer/publish_post.go new file mode 100644 index 0000000..fc11194 --- /dev/null +++ b/internal/delivery_http/posts/posts_command_producer/publish_post.go @@ -0,0 +1,63 @@ +package posts_command_producer + +import ( + "context" + "net/http" + "time" + + gen "github.com/goriiin/kotyari-bots_backend/internal/gen/posts/posts_command" + "github.com/goriiin/kotyari-bots_backend/internal/delivery_http/posts" + jsoniter "github.com/json-iterator/go" +) + +func (p *PostsCommandHandler) PublishPost(ctx context.Context, req *gen.PublishPostRequest, params gen.PublishPostParams) (gen.PublishPostRes, error) { + if !req.Approved { + return &gen.PublishPostBadRequest{ + ErrorCode: http.StatusBadRequest, + Message: "post must be approved to publish", + }, nil + } + + publishRequest := posts.KafkaPublishPostRequest{ + PostID: params.PostId, + Approved: req.Approved, + } + + rawReq, err := jsoniter.Marshal(publishRequest) + if err != nil { + return &gen.PublishPostInternalServerError{ + ErrorCode: http.StatusInternalServerError, + Message: err.Error(), + }, nil + } + + rawResp, err := p.producer.Request(ctx, posts.PayloadToEnvelope(posts.CmdPublish, params.PostId.String(), rawReq), 5*time.Second) + if err != nil { + return &gen.PublishPostInternalServerError{ + ErrorCode: http.StatusInternalServerError, + Message: err.Error(), + }, nil + } + + var resp posts.KafkaResponse + err = jsoniter.Unmarshal(rawResp, &resp) + if err != nil { + return &gen.PublishPostInternalServerError{ + ErrorCode: http.StatusInternalServerError, + Message: err.Error(), + }, nil + } + + if resp.Error != "" { + return &gen.PublishPostInternalServerError{ + ErrorCode: http.StatusInternalServerError, + Message: resp.Error, + }, nil + } + + return &gen.PublishPostResponse{ + Success: true, + Message: "Post approved for publishing", + }, nil +} + diff --git a/pkg/posting_queue/errors.go b/pkg/posting_queue/errors.go new file mode 100644 index 0000000..91a66cb --- /dev/null +++ b/pkg/posting_queue/errors.go @@ -0,0 +1,8 @@ +package posting_queue + +import "errors" + +var ( + ErrPostNotFound = errors.New("post not found in queue") +) + diff --git a/pkg/posting_queue/queue.go b/pkg/posting_queue/queue.go new file mode 100644 index 0000000..2d7b499 --- /dev/null +++ b/pkg/posting_queue/queue.go @@ -0,0 +1,239 @@ +package posting_queue + +import ( + "context" + "math/rand" + "sync" + "time" + + "github.com/google/uuid" + "github.com/goriiin/kotyari-bots_backend/internal/model" + "github.com/goriiin/kotyari-bots_backend/pkg/otvet" +) + +// QueuedPost represents a post waiting to be published +type QueuedPost struct { + ID uuid.UUID + Post *model.Post + Candidate model.Candidate + Request PostRequest + RequiresModeration bool + Approved bool + CreatedAt time.Time +} + +// PostRequest contains information needed to publish a post +type PostRequest struct { + Platform model.PlatformType + PostType model.PostType + TopicType int + Spaces []otvet.Space + // ModerationRequired allows per-request override from bot configuration + ModerationRequired bool +} + +// Account represents an account for posting +type Account struct { + ID string + AuthToken string + Client *otvet.OtvetClient + LastPost time.Time +} + +// Queue is an in-memory queue for posts +type Queue struct { + mu sync.RWMutex + posts []*QueuedPost + accounts map[string]*Account + postingInterval time.Duration + processingInterval time.Duration + moderationRequired bool + stopChan chan struct{} +} + +// NewQueue creates a new posting queue +func NewQueue(postingInterval, processingInterval time.Duration, moderationRequired bool) *Queue { + return &Queue{ + posts: make([]*QueuedPost, 0), + accounts: make(map[string]*Account), + postingInterval: postingInterval, + processingInterval: processingInterval, + moderationRequired: moderationRequired, + stopChan: make(chan struct{}), + } +} + +// AddAccount adds an account to the queue +func (q *Queue) AddAccount(id string, authToken string, client *otvet.OtvetClient) { + q.mu.Lock() + defer q.mu.Unlock() + + q.accounts[id] = &Account{ + ID: id, + AuthToken: authToken, + Client: client, + LastPost: time.Time{}, + } +} + +// Enqueue adds a post to the queue +func (q *Queue) Enqueue(post *model.Post, candidate model.Candidate, req PostRequest) *QueuedPost { + q.mu.Lock() + defer q.mu.Unlock() + + // Determine if moderation is required either by queue default or by per-request flag + requiresModeration := q.moderationRequired || req.ModerationRequired + + queuedPost := &QueuedPost{ + ID: uuid.New(), + Post: post, + Candidate: candidate, + Request: req, + RequiresModeration: requiresModeration, + Approved: !requiresModeration, + CreatedAt: time.Now(), + } + + q.posts = append(q.posts, queuedPost) + return queuedPost +} + +// ApprovePost approves a post for publishing by post ID from database +func (q *Queue) ApprovePost(postID uuid.UUID) error { + q.mu.Lock() + defer q.mu.Unlock() + + for _, post := range q.posts { + if post.Post != nil && post.Post.ID == postID { + post.Approved = true + return nil + } + } + return ErrPostNotFound +} + +// GetPostByID returns a post by post ID from database +func (q *Queue) GetPostByID(postID uuid.UUID) (*QueuedPost, error) { + q.mu.RLock() + defer q.mu.RUnlock() + + for _, post := range q.posts { + if post.Post != nil && post.Post.ID == postID { + return post, nil + } + } + return nil, ErrPostNotFound +} + +// StartProcessing starts the queue processing loop +func (q *Queue) StartProcessing(ctx context.Context, publishFunc func(ctx context.Context, account *Account, post *QueuedPost) error) { + ticker := time.NewTicker(q.processingInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-q.stopChan: + return + case <-ticker.C: + q.processQueue(ctx, publishFunc) + } + } +} + +// Stop stops the queue processing +func (q *Queue) Stop() { + close(q.stopChan) +} + +// processQueue processes the queue and publishes approved posts +func (q *Queue) processQueue(ctx context.Context, publishFunc func(ctx context.Context, account *Account, post *QueuedPost) error) { + q.mu.Lock() + defer q.mu.Unlock() + + if len(q.accounts) == 0 { + return + } + + // Get random account + account := q.getRandomAccount() + if account == nil { + return + } + + // Check if account can post (respecting posting interval) + if !q.canPost(account) { + return + } + + // Find first approved post (either doesn't require moderation or is approved) + var postToPublish *QueuedPost + var postIndex int = -1 + + for i, post := range q.posts { + if post.Approved { + postToPublish = post + postIndex = i + break + } + } + + if postToPublish == nil { + return + } + + // Publish post + if err := publishFunc(ctx, account, postToPublish); err != nil { + return + } + + // Update account last post time + account.LastPost = time.Now() + + // Remove post from queue + q.posts = append(q.posts[:postIndex], q.posts[postIndex+1:]...) +} + +// getRandomAccount returns a random account from the map +func (q *Queue) getRandomAccount() *Account { + if len(q.accounts) == 0 { + return nil + } + + accounts := make([]*Account, 0, len(q.accounts)) + for _, acc := range q.accounts { + accounts = append(accounts, acc) + } + + return accounts[rand.Intn(len(accounts))] +} + +// canPost checks if account can post (respecting posting interval) +func (q *Queue) canPost(account *Account) bool { + if account.LastPost.IsZero() { + return true + } + return time.Since(account.LastPost) >= q.postingInterval +} + +// GetQueueSize returns the current queue size +func (q *Queue) GetQueueSize() int { + q.mu.RLock() + defer q.mu.RUnlock() + return len(q.posts) +} + +// GetPendingModerationCount returns count of posts waiting for moderation +func (q *Queue) GetPendingModerationCount() int { + q.mu.RLock() + defer q.mu.RUnlock() + + count := 0 + for _, post := range q.posts { + if post.RequiresModeration && !post.Approved { + count++ + } + } + return count +} From 1aa53a8912298217e86c10fe452de5c6da5670da Mon Sep 17 00:00:00 2001 From: goriiin Date: Wed, 10 Dec 2025 18:11:51 +0300 Subject: [PATCH 08/29] bot-72: lint --- internal/apps/posts_command_consumer/init.go | 8 +-- .../posts_command_producer/publish_post.go | 7 +- .../posts/posts_command/oas_parameters_gen.go | 66 +++++++++++++++++ .../gen/posts/posts_command/oas_router_gen.go | 72 +++++++++++++++---- pkg/posting_queue/queue.go | 2 +- 5 files changed, 133 insertions(+), 22 deletions(-) diff --git a/internal/apps/posts_command_consumer/init.go b/internal/apps/posts_command_consumer/init.go index 7218be0..26aba25 100644 --- a/internal/apps/posts_command_consumer/init.go +++ b/internal/apps/posts_command_consumer/init.go @@ -15,8 +15,8 @@ import ( "github.com/goriiin/kotyari-bots_backend/pkg/evals" "github.com/goriiin/kotyari-bots_backend/pkg/grok" "github.com/goriiin/kotyari-bots_backend/pkg/otvet" - "github.com/goriiin/kotyari-bots_backend/pkg/posting_queue" "github.com/goriiin/kotyari-bots_backend/pkg/postgres" + "github.com/goriiin/kotyari-bots_backend/pkg/posting_queue" "github.com/goriiin/kotyari-bots_backend/pkg/rewriter" ) @@ -85,7 +85,7 @@ func NewPostsCommandConsumer(config *PostsCommandConsumerConfig, llmConfig *LLMC if postingInterval == 0 { postingInterval = 30 * time.Minute // default } - + processingInterval := config.PostingQueue.ProcessingInterval if processingInterval == 0 { processingInterval = 1 * time.Minute // default @@ -103,9 +103,7 @@ func NewPostsCommandConsumer(config *PostsCommandConsumerConfig, llmConfig *LLMC // Start queue processing in background ctx := context.Background() - go queue.StartProcessing(ctx, func(ctx context.Context, account *posting_queue.Account, post *posting_queue.QueuedPost) error { - return publishPostFromQueue(ctx, account, post) - }) + go queue.StartProcessing(ctx, publishPostFromQueue) return &PostsCommandConsumer{ consumerRunner: posts_command_consumer.NewPostsCommandConsumer(cons, repo, grpc, rw, j, otvetClient, queue), diff --git a/internal/delivery_http/posts/posts_command_producer/publish_post.go b/internal/delivery_http/posts/posts_command_producer/publish_post.go index fc11194..39cc509 100644 --- a/internal/delivery_http/posts/posts_command_producer/publish_post.go +++ b/internal/delivery_http/posts/posts_command_producer/publish_post.go @@ -5,8 +5,8 @@ import ( "net/http" "time" - gen "github.com/goriiin/kotyari-bots_backend/internal/gen/posts/posts_command" "github.com/goriiin/kotyari-bots_backend/internal/delivery_http/posts" + gen "github.com/goriiin/kotyari-bots_backend/internal/gen/posts/posts_command" jsoniter "github.com/json-iterator/go" ) @@ -19,7 +19,7 @@ func (p *PostsCommandHandler) PublishPost(ctx context.Context, req *gen.PublishP } publishRequest := posts.KafkaPublishPostRequest{ - PostID: params.PostId, + PostID: params.PostId, Approved: req.Approved, } @@ -57,7 +57,6 @@ func (p *PostsCommandHandler) PublishPost(ctx context.Context, req *gen.PublishP return &gen.PublishPostResponse{ Success: true, - Message: "Post approved for publishing", + Message: gen.NewOptString("Post approved for publishing"), }, nil } - diff --git a/internal/gen/posts/posts_command/oas_parameters_gen.go b/internal/gen/posts/posts_command/oas_parameters_gen.go index 7e9f668..aea8c07 100644 --- a/internal/gen/posts/posts_command/oas_parameters_gen.go +++ b/internal/gen/posts/posts_command/oas_parameters_gen.go @@ -81,6 +81,72 @@ func decodeDeletePostByIdParams(args [1]string, argsEscaped bool, r *http.Reques return params, nil } +// PublishPostParams is parameters of publishPost operation. +type PublishPostParams struct { + // Уникальный идентификатор поста. + PostId uuid.UUID +} + +func unpackPublishPostParams(packed middleware.Parameters) (params PublishPostParams) { + { + key := middleware.ParameterKey{ + Name: "postId", + In: "path", + } + params.PostId = packed[key].(uuid.UUID) + } + return params +} + +func decodePublishPostParams(args [1]string, argsEscaped bool, r *http.Request) (params PublishPostParams, _ error) { + // Decode path: postId. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "postId", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToUUID(val) + if err != nil { + return err + } + + params.PostId = c + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "postId", + In: "path", + Err: err, + } + } + return params, nil +} + // UpdatePostByIdParams is parameters of updatePostById operation. type UpdatePostByIdParams struct { // Уникальный идентификатор поста. diff --git a/internal/gen/posts/posts_command/oas_router_gen.go b/internal/gen/posts/posts_command/oas_router_gen.go index b43abe2..b0a3418 100644 --- a/internal/gen/posts/posts_command/oas_router_gen.go +++ b/internal/gen/posts/posts_command/oas_router_gen.go @@ -103,16 +103,15 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { elem = origElem } // Param: "postId" - // Leaf parameter, slashes are prohibited + // Match until "/" idx := strings.IndexByte(elem, '/') - if idx >= 0 { - break + if idx < 0 { + idx = len(elem) } - args[0] = elem - elem = "" + args[0] = elem[:idx] + elem = elem[idx:] if len(elem) == 0 { - // Leaf node. switch r.Method { case "DELETE": s.handleDeletePostByIdRequest([1]string{ @@ -128,6 +127,30 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + switch elem[0] { + case '/': // Prefix: "/publish" + + if l := len("/publish"); len(elem) >= l && elem[0:l] == "/publish" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "POST": + s.handlePublishPostRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "POST") + } + + return + } + + } } @@ -273,16 +296,15 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { elem = origElem } // Param: "postId" - // Leaf parameter, slashes are prohibited + // Match until "/" idx := strings.IndexByte(elem, '/') - if idx >= 0 { - break + if idx < 0 { + idx = len(elem) } - args[0] = elem - elem = "" + args[0] = elem[:idx] + elem = elem[idx:] if len(elem) == 0 { - // Leaf node. switch method { case "DELETE": r.name = DeletePostByIdOperation @@ -304,6 +326,32 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { return } } + switch elem[0] { + case '/': // Prefix: "/publish" + + if l := len("/publish"); len(elem) >= l && elem[0:l] == "/publish" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "POST": + r.name = PublishPostOperation + r.summary = "Опубликовать пост (для модерации)" + r.operationID = "publishPost" + r.pathPattern = "/api/v1/posts/{postId}/publish" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + + } } diff --git a/pkg/posting_queue/queue.go b/pkg/posting_queue/queue.go index 2d7b499..2119c3f 100644 --- a/pkg/posting_queue/queue.go +++ b/pkg/posting_queue/queue.go @@ -169,7 +169,7 @@ func (q *Queue) processQueue(ctx context.Context, publishFunc func(ctx context.C // Find first approved post (either doesn't require moderation or is approved) var postToPublish *QueuedPost - var postIndex int = -1 + postIndex := -1 for i, post := range q.posts { if post.Approved { From 94db374b2de0b776cfd89916076b44abdb9e0afa Mon Sep 17 00:00:00 2001 From: goriiin Date: Wed, 10 Dec 2025 18:26:35 +0300 Subject: [PATCH 09/29] bot-72: add field --- internal/delivery_grpc/bots/get.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/delivery_grpc/bots/get.go b/internal/delivery_grpc/bots/get.go index fca15fd..7c32aff 100644 --- a/internal/delivery_grpc/bots/get.go +++ b/internal/delivery_grpc/bots/get.go @@ -21,8 +21,9 @@ func (s *Server) GetBot(ctx context.Context, req *botgrpc.GetBotRequest) (*botgr } return &botgrpc.Bot{ - Id: botModel.ID.String(), - BotPrompt: botModel.SystemPrompt, - BotName: botModel.Name, + Id: botModel.ID.String(), + BotPrompt: botModel.SystemPrompt, + BotName: botModel.Name, + ModerationRequired: botModel.ModerationRequired, }, nil } From 3e6530a99406dd263a3f07d43e21b6eccede3c72 Mon Sep 17 00:00:00 2001 From: goriiin Date: Wed, 17 Dec 2025 13:45:14 +0300 Subject: [PATCH 10/29] bot-72: generate --- internal/delivery_http/posts/gen_dto.go | 5 +- .../posts/posts_command_consumer/handler.go | 21 +- .../posts/posts_query/get_by_id.go | 1 - .../gen/posts/posts_command/oas_client_gen.go | 101 +++++ .../posts/posts_command/oas_handlers_gen.go | 156 +++++++ .../posts/posts_command/oas_interfaces_gen.go | 4 + .../gen/posts/posts_command/oas_json_gen.go | 396 ++++++++++++++++++ .../posts/posts_command/oas_operations_gen.go | 1 + .../posts_command/oas_request_decoders_gen.go | 71 ++++ .../posts_command/oas_request_encoders_gen.go | 14 + .../oas_response_decoders_gen.go | 181 ++++++++ .../oas_response_encoders_gen.go | 72 ++++ .../posts/posts_command/oas_schemas_gen.go | 110 +++++ .../gen/posts/posts_command/oas_server_gen.go | 6 + .../posts_command/oas_unimplemented_gen.go | 9 + .../gen/posts/posts_query/oas_router_gen.go | 110 +++-- 16 files changed, 1220 insertions(+), 38 deletions(-) diff --git a/internal/delivery_http/posts/gen_dto.go b/internal/delivery_http/posts/gen_dto.go index 375af3b..3534867 100644 --- a/internal/delivery_http/posts/gen_dto.go +++ b/internal/delivery_http/posts/gen_dto.go @@ -104,12 +104,10 @@ func HttpInputToModel(input genCommand.PostInput) (*model.Post, string) { } func PostsCheckModelToHttp(post model.Post) genQuery.PostsCheckObject { - isReady := !(post.Text == "" || post.Title == "") - return genQuery.PostsCheckObject{ ID: post.ID, GroupID: post.GroupID, - IsReady: isReady, + IsReady: post.Text != "" && post.Title != "", } } @@ -117,7 +115,6 @@ func PostsCheckModelsToHttpSlice(posts []model.Post) *genQuery.PostsCheckList { checkObjects := make([]genQuery.PostsCheckObject, 0, len(posts)) for _, post := range posts { - // TODO: Плакать хочется if post.IsSeen { continue diff --git a/internal/delivery_http/posts/posts_command_consumer/handler.go b/internal/delivery_http/posts/posts_command_consumer/handler.go index 253f225..a3c880d 100644 --- a/internal/delivery_http/posts/posts_command_consumer/handler.go +++ b/internal/delivery_http/posts/posts_command_consumer/handler.go @@ -46,6 +46,25 @@ func (p *PostsCommandConsumer) HandleCommands() error { return nil } +func (p *PostsCommandConsumer) handleSeenCommand(ctx context.Context, message kafkaConfig.CommittableMessage, payload []byte) error { + err := p.SeenPosts(ctx, payload) + if err != nil { + return sendErrReply(ctx, message, err) + } + + resp := posts.KafkaResponse{} + rawResp, err := jsoniter.Marshal(resp) + if err != nil { + return errors.Wrap(err, constants.MarshalMsg) + } + + if err := message.Reply(ctx, rawResp, true); err != nil { + return errors.Wrap(err, failedToSendReplyMsg) + } + + return nil +} + func (p *PostsCommandConsumer) handleUpdateCommand(ctx context.Context, message kafkaConfig.CommittableMessage, payload []byte) error { post, err := p.UpdatePost(ctx, payload) if err != nil { @@ -132,7 +151,7 @@ func (p *PostsCommandConsumer) handlePublishCommand(ctx context.Context, message return errors.Wrap(err, constants.MarshalMsg) } - if err := message.Reply(ctx, resp); err != nil { + if err := message.Reply(ctx, resp, true); err != nil { return errors.Wrap(err, failedToSendReplyMsg) } diff --git a/internal/delivery_http/posts/posts_query/get_by_id.go b/internal/delivery_http/posts/posts_query/get_by_id.go index 0b0b09b..059475d 100644 --- a/internal/delivery_http/posts/posts_query/get_by_id.go +++ b/internal/delivery_http/posts/posts_query/get_by_id.go @@ -15,7 +15,6 @@ func (p *PostsQueryHandler) GetPostById(ctx context.Context, params gen.GetPostB // Пока возвращается пост без категорий post, err := p.repo.GetByID(ctx, params.PostId) if err != nil { - if strings.Contains(err.Error(), constants.NotFoundMsg) { return &gen.GetPostByIdNotFound{ErrorCode: http.StatusNotFound, Message: "post not found"}, nil } diff --git a/internal/gen/posts/posts_command/oas_client_gen.go b/internal/gen/posts/posts_command/oas_client_gen.go index 3405df4..e17df73 100644 --- a/internal/gen/posts/posts_command/oas_client_gen.go +++ b/internal/gen/posts/posts_command/oas_client_gen.go @@ -39,6 +39,12 @@ type Invoker interface { // // DELETE /api/v1/posts/{postId} DeletePostById(ctx context.Context, params DeletePostByIdParams) (DeletePostByIdRes, error) + // PublishPost invokes publishPost operation. + // + // Опубликовать пост (для модерации). + // + // POST /api/v1/posts/{postId}/publish + PublishPost(ctx context.Context, request *PublishPostRequest, params PublishPostParams) (PublishPostRes, error) // SeenPosts invokes seenPosts operation. // // Обновить статус постов, которые пользователь уже @@ -264,6 +270,101 @@ func (c *Client) sendDeletePostById(ctx context.Context, params DeletePostByIdPa return result, nil } +// PublishPost invokes publishPost operation. +// +// Опубликовать пост (для модерации). +// +// POST /api/v1/posts/{postId}/publish +func (c *Client) PublishPost(ctx context.Context, request *PublishPostRequest, params PublishPostParams) (PublishPostRes, error) { + res, err := c.sendPublishPost(ctx, request, params) + return res, err +} + +func (c *Client) sendPublishPost(ctx context.Context, request *PublishPostRequest, params PublishPostParams) (res PublishPostRes, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("publishPost"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.URLTemplateKey.String("/api/v1/posts/{postId}/publish"), + } + otelAttrs = append(otelAttrs, c.cfg.Attributes...) + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, PublishPostOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [3]string + pathParts[0] = "/api/v1/posts/" + { + // Encode "postId" parameter. + e := uri.NewPathEncoder(uri.PathEncoderConfig{ + Param: "postId", + Style: uri.PathStyleSimple, + Explode: false, + }) + if err := func() error { + return e.EncodeValue(conv.UUIDToString(params.PostId)) + }(); err != nil { + return res, errors.Wrap(err, "encode path") + } + encoded, err := e.Result() + if err != nil { + return res, errors.Wrap(err, "encode path") + } + pathParts[1] = encoded + } + pathParts[2] = "/publish" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "POST", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + if err := encodePublishPostRequest(request, r); err != nil { + return res, errors.Wrap(err, "encode request") + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + defer resp.Body.Close() + + stage = "DecodeResponse" + result, err := decodePublishPostResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + // SeenPosts invokes seenPosts operation. // // Обновить статус постов, которые пользователь уже diff --git a/internal/gen/posts/posts_command/oas_handlers_gen.go b/internal/gen/posts/posts_command/oas_handlers_gen.go index 2b3b174..663dd4f 100644 --- a/internal/gen/posts/posts_command/oas_handlers_gen.go +++ b/internal/gen/posts/posts_command/oas_handlers_gen.go @@ -311,6 +311,162 @@ func (s *Server) handleDeletePostByIdRequest(args [1]string, argsEscaped bool, w } } +// handlePublishPostRequest handles publishPost operation. +// +// Опубликовать пост (для модерации). +// +// POST /api/v1/posts/{postId}/publish +func (s *Server) handlePublishPostRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("publishPost"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/api/v1/posts/{postId}/publish"), + } + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), PublishPostOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code < 100 || code >= 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: PublishPostOperation, + ID: "publishPost", + } + ) + params, err := decodePublishPostParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var rawBody []byte + request, rawBody, close, err := s.decodePublishPostRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response PublishPostRes + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: PublishPostOperation, + OperationSummary: "Опубликовать пост (для модерации)", + OperationID: "publishPost", + Body: request, + RawBody: rawBody, + Params: middleware.Parameters{ + { + Name: "postId", + In: "path", + }: params.PostId, + }, + Raw: r, + } + + type ( + Request = *PublishPostRequest + Params = PublishPostParams + Response = PublishPostRes + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackPublishPostParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.PublishPost(ctx, request, params) + return response, err + }, + ) + } else { + response, err = s.h.PublishPost(ctx, request, params) + } + if err != nil { + defer recordError("Internal", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + if err := encodePublishPostResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + // handleSeenPostsRequest handles seenPosts operation. // // Обновить статус постов, которые пользователь уже diff --git a/internal/gen/posts/posts_command/oas_interfaces_gen.go b/internal/gen/posts/posts_command/oas_interfaces_gen.go index f8e132a..a64a87a 100644 --- a/internal/gen/posts/posts_command/oas_interfaces_gen.go +++ b/internal/gen/posts/posts_command/oas_interfaces_gen.go @@ -9,6 +9,10 @@ type DeletePostByIdRes interface { deletePostByIdRes() } +type PublishPostRes interface { + publishPostRes() +} + type SeenPostsRes interface { seenPostsRes() } diff --git a/internal/gen/posts/posts_command/oas_json_gen.go b/internal/gen/posts/posts_command/oas_json_gen.go index 012eb5e..2f87257 100644 --- a/internal/gen/posts/posts_command/oas_json_gen.go +++ b/internal/gen/posts/posts_command/oas_json_gen.go @@ -730,6 +730,41 @@ func (s *OptNilUUIDArray) UnmarshalJSON(data []byte) error { return s.Decode(d) } +// Encode encodes string as json. +func (o OptString) Encode(e *jx.Encoder) { + if !o.Set { + return + } + e.Str(string(o.Value)) +} + +// Decode decodes string from json. +func (o *OptString) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptString to nil") + } + o.Set = true + v, err := d.Str() + if err != nil { + return err + } + o.Value = string(v) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptString) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptString) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + // Encode implements json.Marshaler. func (s *Post) Encode(e *jx.Encoder) { e.ObjStart() @@ -1723,6 +1758,367 @@ func (s *PostsSeenRequest) UnmarshalJSON(data []byte) error { return s.Decode(d) } +// Encode encodes PublishPostBadRequest as json. +func (s *PublishPostBadRequest) Encode(e *jx.Encoder) { + unwrapped := (*Error)(s) + + unwrapped.Encode(e) +} + +// Decode decodes PublishPostBadRequest from json. +func (s *PublishPostBadRequest) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode PublishPostBadRequest to nil") + } + var unwrapped Error + if err := func() error { + if err := unwrapped.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = PublishPostBadRequest(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *PublishPostBadRequest) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *PublishPostBadRequest) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes PublishPostInternalServerError as json. +func (s *PublishPostInternalServerError) Encode(e *jx.Encoder) { + unwrapped := (*Error)(s) + + unwrapped.Encode(e) +} + +// Decode decodes PublishPostInternalServerError from json. +func (s *PublishPostInternalServerError) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode PublishPostInternalServerError to nil") + } + var unwrapped Error + if err := func() error { + if err := unwrapped.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = PublishPostInternalServerError(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *PublishPostInternalServerError) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *PublishPostInternalServerError) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes PublishPostNotFound as json. +func (s *PublishPostNotFound) Encode(e *jx.Encoder) { + unwrapped := (*Error)(s) + + unwrapped.Encode(e) +} + +// Decode decodes PublishPostNotFound from json. +func (s *PublishPostNotFound) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode PublishPostNotFound to nil") + } + var unwrapped Error + if err := func() error { + if err := unwrapped.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = PublishPostNotFound(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *PublishPostNotFound) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *PublishPostNotFound) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *PublishPostRequest) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *PublishPostRequest) encodeFields(e *jx.Encoder) { + { + e.FieldStart("approved") + e.Bool(s.Approved) + } +} + +var jsonFieldsNameOfPublishPostRequest = [1]string{ + 0: "approved", +} + +// Decode decodes PublishPostRequest from json. +func (s *PublishPostRequest) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode PublishPostRequest to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "approved": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Bool() + s.Approved = bool(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"approved\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode PublishPostRequest") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000001, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfPublishPostRequest) { + name = jsonFieldsNameOfPublishPostRequest[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *PublishPostRequest) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *PublishPostRequest) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *PublishPostResponse) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *PublishPostResponse) encodeFields(e *jx.Encoder) { + { + e.FieldStart("success") + e.Bool(s.Success) + } + { + if s.Message.Set { + e.FieldStart("message") + s.Message.Encode(e) + } + } +} + +var jsonFieldsNameOfPublishPostResponse = [2]string{ + 0: "success", + 1: "message", +} + +// Decode decodes PublishPostResponse from json. +func (s *PublishPostResponse) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode PublishPostResponse to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "success": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Bool() + s.Success = bool(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"success\"") + } + case "message": + if err := func() error { + s.Message.Reset() + if err := s.Message.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"message\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode PublishPostResponse") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000001, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfPublishPostResponse) { + name = jsonFieldsNameOfPublishPostResponse[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *PublishPostResponse) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *PublishPostResponse) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes PublishPostUnauthorized as json. +func (s *PublishPostUnauthorized) Encode(e *jx.Encoder) { + unwrapped := (*Error)(s) + + unwrapped.Encode(e) +} + +// Decode decodes PublishPostUnauthorized from json. +func (s *PublishPostUnauthorized) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode PublishPostUnauthorized to nil") + } + var unwrapped Error + if err := func() error { + if err := unwrapped.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = PublishPostUnauthorized(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *PublishPostUnauthorized) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *PublishPostUnauthorized) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + // Encode encodes SeenPostsInternalServerError as json. func (s *SeenPostsInternalServerError) Encode(e *jx.Encoder) { unwrapped := (*Error)(s) diff --git a/internal/gen/posts/posts_command/oas_operations_gen.go b/internal/gen/posts/posts_command/oas_operations_gen.go index 83c72d8..74bdcc2 100644 --- a/internal/gen/posts/posts_command/oas_operations_gen.go +++ b/internal/gen/posts/posts_command/oas_operations_gen.go @@ -8,6 +8,7 @@ type OperationName = string const ( CreatePostOperation OperationName = "CreatePost" DeletePostByIdOperation OperationName = "DeletePostById" + PublishPostOperation OperationName = "PublishPost" SeenPostsOperation OperationName = "SeenPosts" UpdatePostByIdOperation OperationName = "UpdatePostById" ) diff --git a/internal/gen/posts/posts_command/oas_request_decoders_gen.go b/internal/gen/posts/posts_command/oas_request_decoders_gen.go index 1844460..5a65c02 100644 --- a/internal/gen/posts/posts_command/oas_request_decoders_gen.go +++ b/internal/gen/posts/posts_command/oas_request_decoders_gen.go @@ -93,6 +93,77 @@ func (s *Server) decodeCreatePostRequest(r *http.Request) ( } } +func (s *Server) decodePublishPostRequest(r *http.Request) ( + req *PublishPostRequest, + rawBody []byte, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = errors.Join(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = errors.Join(rerr, close()) + } + }() + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, rawBody, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + if r.ContentLength == 0 { + return req, rawBody, close, validate.ErrBodyRequired + } + buf, err := io.ReadAll(r.Body) + defer func() { + _ = r.Body.Close() + }() + if err != nil { + return req, rawBody, close, err + } + + // Reset the body to allow for downstream reading. + r.Body = io.NopCloser(bytes.NewBuffer(buf)) + + if len(buf) == 0 { + return req, rawBody, close, validate.ErrBodyRequired + } + + rawBody = append(rawBody, buf...) + d := jx.DecodeBytes(buf) + + var request PublishPostRequest + if err := func() error { + if err := request.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return req, rawBody, close, err + } + return &request, rawBody, close, nil + default: + return req, rawBody, close, validate.InvalidContentType(ct) + } +} + func (s *Server) decodeSeenPostsRequest(r *http.Request) ( req *PostsSeenRequest, rawBody []byte, diff --git a/internal/gen/posts/posts_command/oas_request_encoders_gen.go b/internal/gen/posts/posts_command/oas_request_encoders_gen.go index ac4c67a..f1ee57a 100644 --- a/internal/gen/posts/posts_command/oas_request_encoders_gen.go +++ b/internal/gen/posts/posts_command/oas_request_encoders_gen.go @@ -24,6 +24,20 @@ func encodeCreatePostRequest( return nil } +func encodePublishPostRequest( + req *PublishPostRequest, + r *http.Request, +) error { + const contentType = "application/json" + e := new(jx.Encoder) + { + req.Encode(e) + } + encoded := e.Bytes() + ht.SetBody(r, bytes.NewReader(encoded), contentType) + return nil +} + func encodeSeenPostsRequest( req *PostsSeenRequest, r *http.Request, diff --git a/internal/gen/posts/posts_command/oas_response_decoders_gen.go b/internal/gen/posts/posts_command/oas_response_decoders_gen.go index 2fdaab6..cc6c5d2 100644 --- a/internal/gen/posts/posts_command/oas_response_decoders_gen.go +++ b/internal/gen/posts/posts_command/oas_response_decoders_gen.go @@ -273,6 +273,187 @@ func decodeDeletePostByIdResponse(resp *http.Response) (res DeletePostByIdRes, _ return res, validate.UnexpectedStatusCodeWithResponse(resp) } +func decodePublishPostResponse(resp *http.Response) (res PublishPostRes, _ error) { + switch resp.StatusCode { + case 200: + // Code 200. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response PublishPostResponse + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + case 400: + // Code 400. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response PublishPostBadRequest + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + case 401: + // Code 401. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response PublishPostUnauthorized + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + case 404: + // Code 404. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response PublishPostNotFound + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + case 500: + // Code 500. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response PublishPostInternalServerError + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + } + return res, validate.UnexpectedStatusCodeWithResponse(resp) +} + func decodeSeenPostsResponse(resp *http.Response) (res SeenPostsRes, _ error) { switch resp.StatusCode { case 204: diff --git a/internal/gen/posts/posts_command/oas_response_encoders_gen.go b/internal/gen/posts/posts_command/oas_response_encoders_gen.go index d47f3d1..276222b 100644 --- a/internal/gen/posts/posts_command/oas_response_encoders_gen.go +++ b/internal/gen/posts/posts_command/oas_response_encoders_gen.go @@ -122,6 +122,78 @@ func encodeDeletePostByIdResponse(response DeletePostByIdRes, w http.ResponseWri } } +func encodePublishPostResponse(response PublishPostRes, w http.ResponseWriter, span trace.Span) error { + switch response := response.(type) { + case *PublishPostResponse: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + span.SetStatus(codes.Ok, http.StatusText(200)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *PublishPostBadRequest: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(400) + span.SetStatus(codes.Error, http.StatusText(400)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *PublishPostUnauthorized: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(401) + span.SetStatus(codes.Error, http.StatusText(401)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *PublishPostNotFound: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(404) + span.SetStatus(codes.Error, http.StatusText(404)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *PublishPostInternalServerError: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(500) + span.SetStatus(codes.Error, http.StatusText(500)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + default: + return errors.Errorf("unexpected response type: %T", response) + } +} + func encodeSeenPostsResponse(response SeenPostsRes, w http.ResponseWriter, span trace.Span) error { switch response := response.(type) { case *NoContent: diff --git a/internal/gen/posts/posts_command/oas_schemas_gen.go b/internal/gen/posts/posts_command/oas_schemas_gen.go index eefc2be..3ce8a33 100644 --- a/internal/gen/posts/posts_command/oas_schemas_gen.go +++ b/internal/gen/posts/posts_command/oas_schemas_gen.go @@ -337,6 +337,52 @@ func (o OptNilUUIDArray) Or(d []uuid.UUID) []uuid.UUID { return d } +// NewOptString returns new OptString with value set to v. +func NewOptString(v string) OptString { + return OptString{ + Value: v, + Set: true, + } +} + +// OptString is optional string. +type OptString struct { + Value string + Set bool +} + +// IsSet returns true if OptString was set. +func (o OptString) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptString) Reset() { + var v string + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptString) SetTo(v string) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptString) Get() (v string, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptString) Or(d string) string { + if v, ok := o.Get(); ok { + return v + } + return d +} + // Пост. // Ref: #/Post type Post struct { @@ -821,6 +867,70 @@ func (s *PostsSeenRequest) SetSeen(val []uuid.UUID) { s.Seen = val } +type PublishPostBadRequest Error + +func (*PublishPostBadRequest) publishPostRes() {} + +type PublishPostInternalServerError Error + +func (*PublishPostInternalServerError) publishPostRes() {} + +type PublishPostNotFound Error + +func (*PublishPostNotFound) publishPostRes() {} + +// Данные для публикации поста. +// Ref: #/PublishPostRequest +type PublishPostRequest struct { + // Одобрен ли пост для публикации. + Approved bool `json:"approved"` +} + +// GetApproved returns the value of Approved. +func (s *PublishPostRequest) GetApproved() bool { + return s.Approved +} + +// SetApproved sets the value of Approved. +func (s *PublishPostRequest) SetApproved(val bool) { + s.Approved = val +} + +// Результат публикации поста. +// Ref: #/PublishPostResponse +type PublishPostResponse struct { + // Успешно ли опубликован пост. + Success bool `json:"success"` + // Сообщение о результате. + Message OptString `json:"message"` +} + +// GetSuccess returns the value of Success. +func (s *PublishPostResponse) GetSuccess() bool { + return s.Success +} + +// GetMessage returns the value of Message. +func (s *PublishPostResponse) GetMessage() OptString { + return s.Message +} + +// SetSuccess sets the value of Success. +func (s *PublishPostResponse) SetSuccess(val bool) { + s.Success = val +} + +// SetMessage sets the value of Message. +func (s *PublishPostResponse) SetMessage(val OptString) { + s.Message = val +} + +func (*PublishPostResponse) publishPostRes() {} + +type PublishPostUnauthorized Error + +func (*PublishPostUnauthorized) publishPostRes() {} + type SeenPostsInternalServerError Error func (*SeenPostsInternalServerError) seenPostsRes() {} diff --git a/internal/gen/posts/posts_command/oas_server_gen.go b/internal/gen/posts/posts_command/oas_server_gen.go index ee3441d..8145bef 100644 --- a/internal/gen/posts/posts_command/oas_server_gen.go +++ b/internal/gen/posts/posts_command/oas_server_gen.go @@ -20,6 +20,12 @@ type Handler interface { // // DELETE /api/v1/posts/{postId} DeletePostById(ctx context.Context, params DeletePostByIdParams) (DeletePostByIdRes, error) + // PublishPost implements publishPost operation. + // + // Опубликовать пост (для модерации). + // + // POST /api/v1/posts/{postId}/publish + PublishPost(ctx context.Context, req *PublishPostRequest, params PublishPostParams) (PublishPostRes, error) // SeenPosts implements seenPosts operation. // // Обновить статус постов, которые пользователь уже diff --git a/internal/gen/posts/posts_command/oas_unimplemented_gen.go b/internal/gen/posts/posts_command/oas_unimplemented_gen.go index ef47672..1ee42ee 100644 --- a/internal/gen/posts/posts_command/oas_unimplemented_gen.go +++ b/internal/gen/posts/posts_command/oas_unimplemented_gen.go @@ -31,6 +31,15 @@ func (UnimplementedHandler) DeletePostById(ctx context.Context, params DeletePos return r, ht.ErrNotImplemented } +// PublishPost implements publishPost operation. +// +// Опубликовать пост (для модерации). +// +// POST /api/v1/posts/{postId}/publish +func (UnimplementedHandler) PublishPost(ctx context.Context, req *PublishPostRequest, params PublishPostParams) (r PublishPostRes, _ error) { + return r, ht.ErrNotImplemented +} + // SeenPosts implements seenPosts operation. // // Обновить статус постов, которые пользователь уже diff --git a/internal/gen/posts/posts_query/oas_router_gen.go b/internal/gen/posts/posts_query/oas_router_gen.go index 98c82fc..e3c4739 100644 --- a/internal/gen/posts/posts_query/oas_router_gen.go +++ b/internal/gen/posts/posts_query/oas_router_gen.go @@ -80,36 +80,57 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { break } switch elem[0] { - case 'c': // Prefix: "check/" + case 'c': // Prefix: "check" origElem := elem - if l := len("check/"); len(elem) >= l && elem[0:l] == "check/" { + if l := len("check"); len(elem) >= l && elem[0:l] == "check" { elem = elem[l:] } else { break } - // Param: "groupId" - // Leaf parameter, slashes are prohibited - idx := strings.IndexByte(elem, '/') - if idx >= 0 { - break - } - args[0] = elem - elem = "" - if len(elem) == 0 { - // Leaf node. switch r.Method { case "GET": - s.handleCheckGroupIdRequest([1]string{ - args[0], - }, elemIsEscaped, w, r) + s.handleCheckGroupIdsRequest([0]string{}, elemIsEscaped, w, r) default: s.notAllowed(w, r, "GET") } return } + switch elem[0] { + case '/': // Prefix: "/" + + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + // Param: "groupId" + // Leaf parameter, slashes are prohibited + idx := strings.IndexByte(elem, '/') + if idx >= 0 { + break + } + args[0] = elem + elem = "" + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "GET": + s.handleCheckGroupIdRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "GET") + } + + return + } + + } elem = origElem } @@ -253,38 +274,63 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { break } switch elem[0] { - case 'c': // Prefix: "check/" + case 'c': // Prefix: "check" origElem := elem - if l := len("check/"); len(elem) >= l && elem[0:l] == "check/" { + if l := len("check"); len(elem) >= l && elem[0:l] == "check" { elem = elem[l:] } else { break } - // Param: "groupId" - // Leaf parameter, slashes are prohibited - idx := strings.IndexByte(elem, '/') - if idx >= 0 { - break - } - args[0] = elem - elem = "" - if len(elem) == 0 { - // Leaf node. switch method { case "GET": - r.name = CheckGroupIdOperation - r.summary = "Проверить статус готовности постов" - r.operationID = "checkGroupId" - r.pathPattern = "/api/v1/posts/check/{groupId}" + r.name = CheckGroupIdsOperation + r.summary = "Проверить статус готовности всех постов" + r.operationID = "checkGroupIds" + r.pathPattern = "/api/v1/posts/check" r.args = args - r.count = 1 + r.count = 0 return r, true default: return } } + switch elem[0] { + case '/': // Prefix: "/" + + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + // Param: "groupId" + // Leaf parameter, slashes are prohibited + idx := strings.IndexByte(elem, '/') + if idx >= 0 { + break + } + args[0] = elem + elem = "" + + if len(elem) == 0 { + // Leaf node. + switch method { + case "GET": + r.name = CheckGroupIdOperation + r.summary = "Проверить статус готовности отпределенного" + r.operationID = "checkGroupId" + r.pathPattern = "/api/v1/posts/check/{groupId}" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + + } elem = origElem } From 421034abc11da0c3bf7506044784c5cba0c3b109 Mon Sep 17 00:00:00 2001 From: goriiin Date: Wed, 17 Dec 2025 13:51:31 +0300 Subject: [PATCH 11/29] bot-72: migrate --- docker-compose.bots.yml | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/docker-compose.bots.yml b/docker-compose.bots.yml index fb11dc2..95f94f8 100644 --- a/docker-compose.bots.yml +++ b/docker-compose.bots.yml @@ -20,20 +20,20 @@ services: retries: 5 restart: unless-stopped - bots_migrate: - build: - context: . - dockerfile: ./cmd/bots/Dockerfile - target: migrator - container_name: bots_migrate - image: bots_migrate_image - env_file: .env - networks: - - bots-internal-network - depends_on: - bots_db: - condition: service_healthy - restart: on-failure +# bots_migrate: +# build: +# context: . +# dockerfile: ./cmd/bots/Dockerfile +# target: migrator +# container_name: bots_migrate +# image: bots_migrate_image +# env_file: .env +# networks: +# - bots-internal-network +# depends_on: +# bots_db: +# condition: service_healthy +# restart: on-failure bots_go: build: @@ -52,8 +52,10 @@ services: - bots-internal-network - public-gateway-network depends_on: - bots_migrate: - condition: service_completed_successfully + bots_db: + condition: service_healthy +# bots_migrate: +# condition: service_completed_successfully restart: unless-stopped networks: From ad43a1c6b0849212d7d3747464a1d5f2392057aa Mon Sep 17 00:00:00 2001 From: goriiin Date: Wed, 17 Dec 2025 13:58:11 +0300 Subject: [PATCH 12/29] bot-72: migrate --- Makefile | 239 ++++++++++++++++++++++++++----------------------------- 1 file changed, 114 insertions(+), 125 deletions(-) diff --git a/Makefile b/Makefile index db6c3ce..082fd16 100644 --- a/Makefile +++ b/Makefile @@ -1,51 +1,134 @@ DOCKER_NETWORK := public-gateway-network +DOMAIN := writehub.space +EMAIL := admin@writehub.space +NGINX_COMPOSE := docker-compose.nginx.yml +FRONTEND_DIR := ../kotyari-bots_frontend -defalut: help - +# Автоматический поиск сервисов для Ogen SERVICES := $(shell find ./docs -mindepth 2 -maxdepth 3 -type f -name 'openapi.yaml' -print \ | sed -e 's|^./docs/||' -e 's|/openapi.yaml$$||' | sort -u) export PATH := $(shell go env GOPATH)/bin:$(PATH) -.PHONY: help up down reboot test +.PHONY: help up down bots-up profiles-up posts-up gateway-up ssl-install frontend-build + +default: help help: @echo '' @echo 'usage: make [target]' @echo '' - @echo 'targets:' - @echo ' download-lint - Downloading linter binary' - @echo ' check-lint - Verify linter version (>= 2)' - @echo ' verify-lint-config - Verifies linter config' - @echo ' lint - running linter' - @echo ' download-gci - Downloading import formatter' - @echo ' install - Download all dev tools (linter, formatter)' - @echo ' format - Format go import statements' - @echo ' format-check - Check go import statements formatting' - @echo ' check - Run all checks (lint, format-check)' - @echo "api - Сгенерировать Go-код из всех openapi.yml файлов." - @echo "install-ogen - Установить или обновить генератор кода ogen." - -# --- Вспомогательные и внутренние команды --- - -.PHONY: setup-network teardown-network copy-env + @echo 'MAIN TARGETS:' + @echo ' up - Поднять весь бэкенд (БД, Kafka, Go-сервисы) без Nginx' + @echo ' down - Остановить весь бэкенд' + @echo ' gateway-up - Поднять Nginx (Gateway) + Certbot' + @echo ' ssl-install - Полная настройка HTTPS (с генерацией сертификатов)' + @echo ' frontend-build - Собрать статику Nuxt и исправить права доступа' + @echo '' + @echo 'DEV TOOLS:' + @echo ' lint - Запустить линтер' + @echo ' format - Отформатировать импорты' + @echo ' api - Сгенерировать Go-код (Ogen) из OpenAPI' + @echo ' proto-build - Сгенерировать gRPC код из .proto' +# --- СЕТЬ И ОКРУЖЕНИЕ --- setup-network: @docker network inspect $(DOCKER_NETWORK) >/dev/null 2>&1 || \ (echo "Создаю общую Docker-сеть: $(DOCKER_NETWORK)..." && docker network create $(DOCKER_NETWORK)) -# Удаляет общую сеть -teardown-network: - @docker network rm $(DOCKER_NETWORK) >/dev/null 2>&1 || true - copy-env: @if [ ! -f .env ]; then \ echo "Создаю .env файл из .env.example..."; \ cp .env.example .env; \ fi -# --- Кодогенерация и статический анализ --- +# --- БЭКЕНД (Docker Compose) --- + +# Параллельный запуск основных сервисов +up: copy-env setup-network + @echo "Starting backend services..." + @$(MAKE) bots-up & \ + $(MAKE) profiles-up & \ + $(MAKE) posts-up & \ + wait + @echo "Backend services are up." + +down: + @echo "Stopping backend services..." + @$(MAKE) bots-down & \ + $(MAKE) profiles-down & \ + $(MAKE) posts-down & \ + wait + @echo "Backend services stopped." + +bots-up: setup-network + docker compose -f docker-compose.bots.yml up -d --build + +bots-down: + docker compose -f docker-compose.bots.yml down + +profiles-up: setup-network + docker compose -f docker-compose.profiles.yml up -d --build + +profiles-down: + docker compose -f docker-compose.profiles.yml down + +posts-up: setup-network + docker compose -f docker-compose.posts.yml up -d --build + +posts-down: + docker compose -f docker-compose.posts.yml down + +# --- FRONTEND --- + +frontend-build: + @echo "Building Frontend Static Site..." + cd $(FRONTEND_DIR) && npm run generate + @echo "Fixing permissions for Nginx..." + chmod -R 755 $(FRONTEND_DIR)/.output/public + @echo "Frontend built successfully." + +# --- GATEWAY & SSL (NGINX) --- + +gateway-up: setup-network + docker compose -f $(NGINX_COMPOSE) up -d + +gateway-down: + docker compose -f $(NGINX_COMPOSE) down + +gateway-restart: + docker compose -f $(NGINX_COMPOSE) restart gateway + +gateway-logs: + docker compose -f $(NGINX_COMPOSE) logs -f + +ssl-install: + @if [ ! -f nginx.conf.http ] || [ ! -f nginx.conf.https ]; then \ + echo "Ошибка: Файлы nginx.conf.http и nginx.conf.https должны существовать."; \ + exit 1; \ + fi + @echo ">>> [1/4] Применяем HTTP конфигурацию (для валидации)..." + cp nginx.conf.http nginx.conf + $(MAKE) gateway-up + @echo ">>> Ожидание запуска Nginx..." + @sleep 5 + @echo ">>> [2/4] Генерация сертификатов через Let's Encrypt..." + docker compose -f $(NGINX_COMPOSE) run --rm --entrypoint certbot certbot certonly --webroot --webroot-path /var/www/certbot \ + -d $(DOMAIN) -d www.$(DOMAIN) \ + --email $(EMAIL) \ + --agree-tos --no-eff-email --force-renewal + @echo ">>> [3/4] Применяем HTTPS конфигурацию (боевую)..." + cp nginx.conf.https nginx.conf + @echo ">>> [4/4] Перезагрузка Nginx..." + docker compose -f $(NGINX_COMPOSE) exec gateway nginx -s reload + @echo ">>> Готово. HTTPS настроен." + +cert-renew: + docker compose -f $(NGINX_COMPOSE) run --rm --entrypoint certbot certbot renew + docker compose -f $(NGINX_COMPOSE) exec gateway nginx -s reload + +# --- CODE GEN & LINTING --- PROTO_DIR := ./api/protos GEN_DIR := gen @@ -64,34 +147,27 @@ $(ENTITIES): --go-grpc_out=$(PROTO_DIR)/$@/$(GEN_DIR) \ --go-grpc_opt=paths=source_relative \ $(PROTO_DIR)/$@/proto/*.proto - @echo "Генерация для $@ завершена." + +install-ogen: + go install github.com/ogen-go/ogen/cmd/ogen@latest api: install-ogen @echo "Начинаю генерацию кода для сервисов: $(SERVICES)" $(foreach service,$(SERVICES),$(call generate-service,$(service))) @echo "Генерация кода успешно завершена." -install-ogen: - go install github.com/ogen-go/ogen/cmd/ogen@v1.16.0 - define generate-service @echo "--- Генерирую код для сервиса: $(1) ---" $(eval INPUT_FILE := ./docs/$(1)/openapi.yaml) $(eval OUTPUT_DIR := ./internal/gen/$(1)) - $(eval PKG := $(notdir $(1))) # e.g., posts_1 + $(eval PKG := $(notdir $(1))) $(eval OGEN_CFG := ./docs/ogen-config.yaml) - @if [ ! -f "$(INPUT_FILE)" ]; then \ - echo "Ошибка: Файл спецификации $(INPUT_FILE) не найден!"; \ - exit 1; \ - fi - @mkdir -p "$(OUTPUT_DIR)" ogen --config "$(OGEN_CFG)" --target "$(OUTPUT_DIR)" --package "$(PKG)" -clean "$(INPUT_FILE)" endef - download-lint: - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.3.1 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.61.0 download-gci: go install github.com/daixiang0/gci@v0.13.4 @@ -104,84 +180,7 @@ lint: format: @gci write . --skip-generated --skip-vendor < /dev/null -format-check: - @gci diff . --skip-generated --skip-vendor < /dev/null - -check: lint format-check - -# параллельно -up: copy-env setup-network - @echo "Starting services in parallel..." - @$(MAKE) bots-up & \ - $(MAKE) profiles-up & \ - $(MAKE) posts-up & \ - wait - @echo "All services are up and running." - -# параллельно -down: - @echo "Shutdown services in parallel..." - @$(MAKE) bots-down & \ - $(MAKE) profiles-down & \ - $(MAKE) posts-down & \ - wait - @echo "All services are up and stopped." - -bots-up: setup-network - @echo "Starting bots service and dependencies..." - @docker compose -f docker-compose.bots.yml up -d --build - -bots-down: - @echo "Stopping bots service and dependencies..." - @docker compose -f docker-compose.bots.yml down - -bots-reboot: - @echo "Rebooting bots service and dependencies..." - $(MAKE) bots-down - $(MAKE) bots-up - -profiles-up: setup-network - @echo "Starting profiles service and dependencies..." - docker compose -f docker-compose.profiles.yml up -d --build - -profiles-down: - @echo "Stopping profiles service and dependencies..." - @docker compose -f docker-compose.profiles.yml down - -profiles-reboot: - @echo "Rebooting profiles service and dependencies..." - $(MAKE) profiles-down - $(MAKE) profiles-up - -posts-up: setup-network - @echo "Starting posts service and dependencies..." - docker compose -f docker-compose.posts.yml up -d --build - -posts-down: - @echo "Stopping posts service and dependencies..." - @docker compose -f docker-compose.posts.yml down - -posts-reboot: - @echo "Rebooting posts service and dependencies..." - $(MAKE) posts-down - $(MAKE) posts-up - - -example-run: - @go run cmd/example/main.go -example-run-local: ## Запустить в local режиме - @go run cmd/example/main.go --env=local --config="./configs/local-config.yaml" - -example-run-prod: - @go run cmd/example/main.go --env=prod - -install-migrate: - @if ! command -v migrate &> /dev/null; then \ - echo "migrate CLI not found. Installing..."; \ - go install -tags 'pgx5' github.com/golang-migrate/migrate/v4/cmd/migrate@latest; \ - fi - -.PHONY: download-lint download-gci lint format format-check check help api +# --- INTRANET (Parsers) --- INTRANET_DIR := ./intranet @@ -191,18 +190,8 @@ intranet-up-dev: intranet-down-dev: $(MAKE) -C $(INTRANET_DIR) down-dev - intranet-up-prod: $(MAKE) -C $(INTRANET_DIR) up-prod intranet-down-prod: - $(MAKE) -C $(INTRANET_DIR) down-prod - -intranet-deps: - $(MAKE) -C $(INTRANET_DIR) deps - -intranet-test: - $(MAKE) -C $(INTRANET_DIR) test-detection-compose - -dzen-url-start: - curl -X POST http://localhost:8090/trigger-parsing + $(MAKE) -C $(INTRANET_DIR) down-prod \ No newline at end of file From 463c5f8c4246f6d7877aac06361fa9396c6caa16 Mon Sep 17 00:00:00 2001 From: goriiin Date: Wed, 17 Dec 2025 14:03:49 +0300 Subject: [PATCH 13/29] bot-72: migrate --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 082fd16..e55f8d8 100644 --- a/Makefile +++ b/Makefile @@ -149,7 +149,7 @@ $(ENTITIES): $(PROTO_DIR)/$@/proto/*.proto install-ogen: - go install github.com/ogen-go/ogen/cmd/ogen@latest + go install github.com/ogen-go/ogen/cmd/ogen@v1.16.0 api: install-ogen @echo "Начинаю генерацию кода для сервисов: $(SERVICES)" From d472c09ae81d7f293553412e44fbdd4ed88920e2 Mon Sep 17 00:00:00 2001 From: goriiin Date: Wed, 17 Dec 2025 14:07:32 +0300 Subject: [PATCH 14/29] bot-72: generate --- api/protos/bot_profile/gen/get_profiles.pb.go | 2 +- .../bot_profile/gen/get_profiles_grpc.pb.go | 8 +- api/protos/bots/gen/get_bot.pb.go | 16 +-- api/protos/bots/gen/get_bot_grpc.pb.go | 6 +- api/protos/posts/gen/posts.pb.go | 134 ++++++++++++++++-- api/protos/posts/gen/posts_grpc.pb.go | 48 ++++++- api/protos/profiles/gen/get_profiles.pb.go | 2 +- .../profiles/gen/get_profiles_grpc.pb.go | 8 +- .../url_fetcher/gen/start_fetching.pb.go | 2 +- .../url_fetcher/gen/start_fetching_grpc.pb.go | 6 +- go.mod | 18 +-- go.sum | 48 ++++--- 12 files changed, 224 insertions(+), 74 deletions(-) diff --git a/api/protos/bot_profile/gen/get_profiles.pb.go b/api/protos/bot_profile/gen/get_profiles.pb.go index 5499d0e..fe1cd3d 100644 --- a/api/protos/bot_profile/gen/get_profiles.pb.go +++ b/api/protos/bot_profile/gen/get_profiles.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.8 +// protoc-gen-go v1.36.11 // protoc v6.32.0 // source: get_profiles.proto diff --git a/api/protos/bot_profile/gen/get_profiles_grpc.pb.go b/api/protos/bot_profile/gen/get_profiles_grpc.pb.go index 94df56a..1259310 100644 --- a/api/protos/bot_profile/gen/get_profiles_grpc.pb.go +++ b/api/protos/bot_profile/gen/get_profiles_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 +// - protoc-gen-go-grpc v1.6.0 // - protoc v6.32.0 // source: get_profiles.proto @@ -84,10 +84,10 @@ type ProfilesServiceServer interface { type UnimplementedProfilesServiceServer struct{} func (UnimplementedProfilesServiceServer) GetProfiles(context.Context, *GetProfilesRequest) (*GetProfilesResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetProfiles not implemented") + return nil, status.Error(codes.Unimplemented, "method GetProfiles not implemented") } func (UnimplementedProfilesServiceServer) ProfilesExist(context.Context, *ProfilesExistRequest) (*ProfilesExistResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method ProfilesExist not implemented") + return nil, status.Error(codes.Unimplemented, "method ProfilesExist not implemented") } func (UnimplementedProfilesServiceServer) mustEmbedUnimplementedProfilesServiceServer() {} func (UnimplementedProfilesServiceServer) testEmbeddedByValue() {} @@ -100,7 +100,7 @@ type UnsafeProfilesServiceServer interface { } func RegisterProfilesServiceServer(s grpc.ServiceRegistrar, srv ProfilesServiceServer) { - // If the following call pancis, it indicates UnimplementedProfilesServiceServer was + // If the following call panics, it indicates UnimplementedProfilesServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/api/protos/bots/gen/get_bot.pb.go b/api/protos/bots/gen/get_bot.pb.go index cf3aee1..1c3fcaf 100644 --- a/api/protos/bots/gen/get_bot.pb.go +++ b/api/protos/bots/gen/get_bot.pb.go @@ -1,18 +1,17 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.8 +// protoc-gen-go v1.36.11 // protoc v6.32.0 // source: get_bot.proto package gen import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" - - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( @@ -27,7 +26,7 @@ type Bot struct { Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` BotPrompt string `protobuf:"bytes,2,opt,name=bot_prompt,json=botPrompt,proto3" json:"bot_prompt,omitempty"` BotName string `protobuf:"bytes,3,opt,name=bot_name,json=botName,proto3" json:"bot_name,omitempty"` - // Added moderation flag from proto + // Indicates whether posts created by this bot require moderation ModerationRequired bool `protobuf:"varint,4,opt,name=moderation_required,json=moderationRequired,proto3" json:"moderation_required,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -84,7 +83,6 @@ func (x *Bot) GetBotName() string { return "" } -// GetModerationRequired returns true if moderation is required for this bot func (x *Bot) GetModerationRequired() bool { if x != nil { return x.ModerationRequired @@ -140,13 +138,13 @@ var File_get_bot_proto protoreflect.FileDescriptor const file_get_bot_proto_rawDesc = "" + "\n" + - "\rget_bot.proto\x12\x04bots\"O\n" + + "\rget_bot.proto\x12\x04bots\"\x80\x01\n" + "\x03Bot\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n" + "\n" + "bot_prompt\x18\x02 \x01(\tR\tbotPrompt\x12\x19\n" + - "\bbot_name\x18\x03 \x01(\tR\abotName\x12\x35\n" + - "\x15moderation_required\x18\x04 \x01(\tR\x15moderationRequired\"\x1f\n" + + "\bbot_name\x18\x03 \x01(\tR\abotName\x12/\n" + + "\x13moderation_required\x18\x04 \x01(\bR\x12moderationRequired\"\x1f\n" + "\rGetBotRequest\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id26\n" + "\n" + diff --git a/api/protos/bots/gen/get_bot_grpc.pb.go b/api/protos/bots/gen/get_bot_grpc.pb.go index 395a161..a697ca6 100644 --- a/api/protos/bots/gen/get_bot_grpc.pb.go +++ b/api/protos/bots/gen/get_bot_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 +// - protoc-gen-go-grpc v1.6.0 // - protoc v6.32.0 // source: get_bot.proto @@ -63,7 +63,7 @@ type BotServiceServer interface { type UnimplementedBotServiceServer struct{} func (UnimplementedBotServiceServer) GetBot(context.Context, *GetBotRequest) (*Bot, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetBot not implemented") + return nil, status.Error(codes.Unimplemented, "method GetBot not implemented") } func (UnimplementedBotServiceServer) mustEmbedUnimplementedBotServiceServer() {} func (UnimplementedBotServiceServer) testEmbeddedByValue() {} @@ -76,7 +76,7 @@ type UnsafeBotServiceServer interface { } func RegisterBotServiceServer(s grpc.ServiceRegistrar, srv BotServiceServer) { - // If the following call pancis, it indicates UnimplementedBotServiceServer was + // If the following call panics, it indicates UnimplementedBotServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/api/protos/posts/gen/posts.pb.go b/api/protos/posts/gen/posts.pb.go index 8b21a56..e546cf7 100644 --- a/api/protos/posts/gen/posts.pb.go +++ b/api/protos/posts/gen/posts.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.9 -// protoc v6.32.1 +// protoc-gen-go v1.36.11 +// protoc v6.32.0 // source: posts.proto package gen @@ -221,6 +221,102 @@ func (x *GetPostsResponse) GetPostsResponse() []*GetPostResponse { return nil } +type ApprovePostRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PostId string `protobuf:"bytes,1,opt,name=post_id,json=postId,proto3" json:"post_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApprovePostRequest) Reset() { + *x = ApprovePostRequest{} + mi := &file_posts_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApprovePostRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApprovePostRequest) ProtoMessage() {} + +func (x *ApprovePostRequest) ProtoReflect() protoreflect.Message { + mi := &file_posts_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApprovePostRequest.ProtoReflect.Descriptor instead. +func (*ApprovePostRequest) Descriptor() ([]byte, []int) { + return file_posts_proto_rawDescGZIP(), []int{4} +} + +func (x *ApprovePostRequest) GetPostId() string { + if x != nil { + return x.PostId + } + return "" +} + +type ApprovePostResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApprovePostResponse) Reset() { + *x = ApprovePostResponse{} + mi := &file_posts_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApprovePostResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApprovePostResponse) ProtoMessage() {} + +func (x *ApprovePostResponse) ProtoReflect() protoreflect.Message { + mi := &file_posts_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApprovePostResponse.ProtoReflect.Descriptor instead. +func (*ApprovePostResponse) Descriptor() ([]byte, []int) { + return file_posts_proto_rawDescGZIP(), []int{5} +} + +func (x *ApprovePostResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *ApprovePostResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + var File_posts_proto protoreflect.FileDescriptor const file_posts_proto_rawDesc = "" + @@ -239,10 +335,16 @@ const file_posts_proto_rawDesc = "" + "\x0fGetPostsRequest\x12:\n" + "\rposts_request\x18\x01 \x03(\v2\x15.posts.GetPostRequestR\fpostsRequest\"Q\n" + "\x10GetPostsResponse\x12=\n" + - "\x0eposts_response\x18\x01 \x03(\v2\x16.posts.GetPostResponseR\rpostsResponse2\x8a\x01\n" + + "\x0eposts_response\x18\x01 \x03(\v2\x16.posts.GetPostResponseR\rpostsResponse\"-\n" + + "\x12ApprovePostRequest\x12\x17\n" + + "\apost_id\x18\x01 \x01(\tR\x06postId\"I\n" + + "\x13ApprovePostResponse\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage2\xd0\x01\n" + "\fPostsService\x128\n" + "\aGetPost\x12\x15.posts.GetPostRequest\x1a\x16.posts.GetPostResponse\x12@\n" + - "\rGetPostsBatch\x12\x16.posts.GetPostsRequest\x1a\x17.posts.GetPostsResponseB>ZZ posts.GetPostRequest 1, // 1: posts.GetPostsResponse.posts_response:type_name -> posts.GetPostResponse 0, // 2: posts.PostsService.GetPost:input_type -> posts.GetPostRequest 2, // 3: posts.PostsService.GetPostsBatch:input_type -> posts.GetPostsRequest - 1, // 4: posts.PostsService.GetPost:output_type -> posts.GetPostResponse - 3, // 5: posts.PostsService.GetPostsBatch:output_type -> posts.GetPostsResponse - 4, // [4:6] is the sub-list for method output_type - 2, // [2:4] is the sub-list for method input_type + 4, // 4: posts.PostsService.ApprovePost:input_type -> posts.ApprovePostRequest + 1, // 5: posts.PostsService.GetPost:output_type -> posts.GetPostResponse + 3, // 6: posts.PostsService.GetPostsBatch:output_type -> posts.GetPostsResponse + 5, // 7: posts.PostsService.ApprovePost:output_type -> posts.ApprovePostResponse + 5, // [5:8] is the sub-list for method output_type + 2, // [2:5] is the sub-list for method input_type 2, // [2:2] is the sub-list for extension type_name 2, // [2:2] is the sub-list for extension extendee 0, // [0:2] is the sub-list for field type_name @@ -288,7 +394,7 @@ func file_posts_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_posts_proto_rawDesc), len(file_posts_proto_rawDesc)), NumEnums: 0, - NumMessages: 4, + NumMessages: 6, NumExtensions: 0, NumServices: 1, }, diff --git a/api/protos/posts/gen/posts_grpc.pb.go b/api/protos/posts/gen/posts_grpc.pb.go index 50fbd53..ab64d8e 100644 --- a/api/protos/posts/gen/posts_grpc.pb.go +++ b/api/protos/posts/gen/posts_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 -// - protoc v6.32.1 +// - protoc-gen-go-grpc v1.6.0 +// - protoc v6.32.0 // source: posts.proto package gen @@ -21,6 +21,7 @@ const _ = grpc.SupportPackageIsVersion9 const ( PostsService_GetPost_FullMethodName = "/posts.PostsService/GetPost" PostsService_GetPostsBatch_FullMethodName = "/posts.PostsService/GetPostsBatch" + PostsService_ApprovePost_FullMethodName = "/posts.PostsService/ApprovePost" ) // PostsServiceClient is the client API for PostsService service. @@ -29,6 +30,7 @@ const ( type PostsServiceClient interface { GetPost(ctx context.Context, in *GetPostRequest, opts ...grpc.CallOption) (*GetPostResponse, error) GetPostsBatch(ctx context.Context, in *GetPostsRequest, opts ...grpc.CallOption) (*GetPostsResponse, error) + ApprovePost(ctx context.Context, in *ApprovePostRequest, opts ...grpc.CallOption) (*ApprovePostResponse, error) } type postsServiceClient struct { @@ -59,12 +61,23 @@ func (c *postsServiceClient) GetPostsBatch(ctx context.Context, in *GetPostsRequ return out, nil } +func (c *postsServiceClient) ApprovePost(ctx context.Context, in *ApprovePostRequest, opts ...grpc.CallOption) (*ApprovePostResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ApprovePostResponse) + err := c.cc.Invoke(ctx, PostsService_ApprovePost_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // PostsServiceServer is the server API for PostsService service. // All implementations must embed UnimplementedPostsServiceServer // for forward compatibility. type PostsServiceServer interface { GetPost(context.Context, *GetPostRequest) (*GetPostResponse, error) GetPostsBatch(context.Context, *GetPostsRequest) (*GetPostsResponse, error) + ApprovePost(context.Context, *ApprovePostRequest) (*ApprovePostResponse, error) mustEmbedUnimplementedPostsServiceServer() } @@ -76,10 +89,13 @@ type PostsServiceServer interface { type UnimplementedPostsServiceServer struct{} func (UnimplementedPostsServiceServer) GetPost(context.Context, *GetPostRequest) (*GetPostResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetPost not implemented") + return nil, status.Error(codes.Unimplemented, "method GetPost not implemented") } func (UnimplementedPostsServiceServer) GetPostsBatch(context.Context, *GetPostsRequest) (*GetPostsResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetPostsBatch not implemented") + return nil, status.Error(codes.Unimplemented, "method GetPostsBatch not implemented") +} +func (UnimplementedPostsServiceServer) ApprovePost(context.Context, *ApprovePostRequest) (*ApprovePostResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ApprovePost not implemented") } func (UnimplementedPostsServiceServer) mustEmbedUnimplementedPostsServiceServer() {} func (UnimplementedPostsServiceServer) testEmbeddedByValue() {} @@ -92,7 +108,7 @@ type UnsafePostsServiceServer interface { } func RegisterPostsServiceServer(s grpc.ServiceRegistrar, srv PostsServiceServer) { - // If the following call pancis, it indicates UnimplementedPostsServiceServer was + // If the following call panics, it indicates UnimplementedPostsServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. @@ -138,6 +154,24 @@ func _PostsService_GetPostsBatch_Handler(srv interface{}, ctx context.Context, d return interceptor(ctx, in, info, handler) } +func _PostsService_ApprovePost_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ApprovePostRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PostsServiceServer).ApprovePost(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PostsService_ApprovePost_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PostsServiceServer).ApprovePost(ctx, req.(*ApprovePostRequest)) + } + return interceptor(ctx, in, info, handler) +} + // PostsService_ServiceDesc is the grpc.ServiceDesc for PostsService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -153,6 +187,10 @@ var PostsService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetPostsBatch", Handler: _PostsService_GetPostsBatch_Handler, }, + { + MethodName: "ApprovePost", + Handler: _PostsService_ApprovePost_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "posts.proto", diff --git a/api/protos/profiles/gen/get_profiles.pb.go b/api/protos/profiles/gen/get_profiles.pb.go index 6a16e2e..a33650a 100644 --- a/api/protos/profiles/gen/get_profiles.pb.go +++ b/api/protos/profiles/gen/get_profiles.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.8 +// protoc-gen-go v1.36.11 // protoc v6.32.0 // source: get_profiles.proto diff --git a/api/protos/profiles/gen/get_profiles_grpc.pb.go b/api/protos/profiles/gen/get_profiles_grpc.pb.go index 01cc88b..7f59dac 100644 --- a/api/protos/profiles/gen/get_profiles_grpc.pb.go +++ b/api/protos/profiles/gen/get_profiles_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 +// - protoc-gen-go-grpc v1.6.0 // - protoc v6.32.0 // source: get_profiles.proto @@ -76,10 +76,10 @@ type ProfileServiceServer interface { type UnimplementedProfileServiceServer struct{} func (UnimplementedProfileServiceServer) GetProfile(context.Context, *GetProfileRequest) (*Profile, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetProfile not implemented") + return nil, status.Error(codes.Unimplemented, "method GetProfile not implemented") } func (UnimplementedProfileServiceServer) BatchGetProfiles(context.Context, *BatchGetProfilesRequest) (*BatchGetProfilesResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method BatchGetProfiles not implemented") + return nil, status.Error(codes.Unimplemented, "method BatchGetProfiles not implemented") } func (UnimplementedProfileServiceServer) mustEmbedUnimplementedProfileServiceServer() {} func (UnimplementedProfileServiceServer) testEmbeddedByValue() {} @@ -92,7 +92,7 @@ type UnsafeProfileServiceServer interface { } func RegisterProfileServiceServer(s grpc.ServiceRegistrar, srv ProfileServiceServer) { - // If the following call pancis, it indicates UnimplementedProfileServiceServer was + // If the following call panics, it indicates UnimplementedProfileServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/api/protos/url_fetcher/gen/start_fetching.pb.go b/api/protos/url_fetcher/gen/start_fetching.pb.go index 0a7fbe6..e544415 100644 --- a/api/protos/url_fetcher/gen/start_fetching.pb.go +++ b/api/protos/url_fetcher/gen/start_fetching.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.8 +// protoc-gen-go v1.36.11 // protoc v6.32.0 // source: start_fetching.proto diff --git a/api/protos/url_fetcher/gen/start_fetching_grpc.pb.go b/api/protos/url_fetcher/gen/start_fetching_grpc.pb.go index 639d244..0c73285 100644 --- a/api/protos/url_fetcher/gen/start_fetching_grpc.pb.go +++ b/api/protos/url_fetcher/gen/start_fetching_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 +// - protoc-gen-go-grpc v1.6.0 // - protoc v6.32.0 // source: start_fetching.proto @@ -64,7 +64,7 @@ type ProfileServiceServer interface { type UnimplementedProfileServiceServer struct{} func (UnimplementedProfileServiceServer) StartFetching(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method StartFetching not implemented") + return nil, status.Error(codes.Unimplemented, "method StartFetching not implemented") } func (UnimplementedProfileServiceServer) mustEmbedUnimplementedProfileServiceServer() {} func (UnimplementedProfileServiceServer) testEmbeddedByValue() {} @@ -77,7 +77,7 @@ type UnsafeProfileServiceServer interface { } func RegisterProfileServiceServer(s grpc.ServiceRegistrar, srv ProfileServiceServer) { - // If the following call pancis, it indicates UnimplementedProfileServiceServer was + // If the following call panics, it indicates UnimplementedProfileServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. diff --git a/go.mod b/go.mod index 5f635de..32094d2 100644 --- a/go.mod +++ b/go.mod @@ -20,10 +20,10 @@ require ( go.opentelemetry.io/otel v1.38.0 go.opentelemetry.io/otel/metric v1.38.0 go.opentelemetry.io/otel/trace v1.38.0 - golang.org/x/net v0.47.0 - golang.org/x/sync v0.18.0 - google.golang.org/grpc v1.67.3 - google.golang.org/protobuf v1.36.1 + golang.org/x/net v0.48.0 + golang.org/x/sync v0.19.0 + google.golang.org/grpc v1.77.0 + google.golang.org/protobuf v1.36.11 ) require ( @@ -55,14 +55,14 @@ require ( github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect - golang.org/x/crypto v0.44.0 // indirect + golang.org/x/crypto v0.46.0 // indirect golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index facd33a..58b8831 100644 --- a/go.sum +++ b/go.sum @@ -50,6 +50,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE= github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -113,8 +115,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= @@ -151,14 +153,18 @@ github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -167,27 +173,29 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY= golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= -google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8= -google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s= -google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From b9c6fc857c9328c194f13fdafb772200ca26957e Mon Sep 17 00:00:00 2001 From: goriiin Date: Wed, 17 Dec 2025 14:13:23 +0300 Subject: [PATCH 15/29] bot-72: generate --- docker-compose.bots.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docker-compose.bots.yml b/docker-compose.bots.yml index 95f94f8..5641eeb 100644 --- a/docker-compose.bots.yml +++ b/docker-compose.bots.yml @@ -20,20 +20,20 @@ services: retries: 5 restart: unless-stopped -# bots_migrate: -# build: -# context: . -# dockerfile: ./cmd/bots/Dockerfile -# target: migrator -# container_name: bots_migrate -# image: bots_migrate_image -# env_file: .env -# networks: -# - bots-internal-network -# depends_on: -# bots_db: -# condition: service_healthy -# restart: on-failure + bots_migrate: + build: + context: . + dockerfile: ./cmd/bots/Dockerfile + target: migrator + container_name: bots_migrate + image: bots_migrate_image + env_file: .env + networks: + - bots-internal-network + depends_on: + bots_db: + condition: service_healthy + restart: on-failure bots_go: build: @@ -54,8 +54,8 @@ services: depends_on: bots_db: condition: service_healthy -# bots_migrate: -# condition: service_completed_successfully + bots_migrate: + condition: service_completed_successfully restart: unless-stopped networks: From 64444061feefeb0f715d904e2b798a0bf5510240 Mon Sep 17 00:00:00 2001 From: goriiin Date: Wed, 17 Dec 2025 14:18:14 +0300 Subject: [PATCH 16/29] bot-72: delete --- internal/repo/bots/delete.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/internal/repo/bots/delete.go b/internal/repo/bots/delete.go index 1781374..883c739 100644 --- a/internal/repo/bots/delete.go +++ b/internal/repo/bots/delete.go @@ -7,9 +7,6 @@ import ( ) func (r *BotsRepository) Delete(ctx context.Context, id uuid.UUID) error { - _, err := r.db.Exec(ctx, `delete from bots where id=$1`, id) - if err != nil { - return err - } - return nil + _, err := r.db.Exec(ctx, `UPDATE bots SET is_deleted = true, updated_at = NOW() WHERE id=$1`, id) + return err } From 4bcce850b8c51c33f4e5317bc705c2174a349796 Mon Sep 17 00:00:00 2001 From: goriiin Date: Wed, 17 Dec 2025 14:34:37 +0300 Subject: [PATCH 17/29] bot-72: delete --- nginx.conf.https | 65 +++++++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/nginx.conf.https b/nginx.conf.https index 918c399..edf7393 100644 --- a/nginx.conf.https +++ b/nginx.conf.https @@ -6,23 +6,29 @@ http { include mime.types; default_type application/octet-stream; - # ... твои upstream (bots, profiles, posts ...) ... - upstream bots_service { server bots_go:8001; } - upstream profiles_service { server profiles_go:8003; } - upstream posts_command_service { server posts_command_prod_go:8088; } - upstream posts_query_service { server posts_query_go:8089; } - - map $request_method $posts_upstream { - GET posts_query_service; - HEAD posts_query_service; - default posts_command_service; + # 1. Включаем резолвер Docker. + # Это критически важно: valid=10s заставляет Nginx обновлять кэш DNS. + resolver 127.0.0.11 valid=10s; + + # 2. Карта для маршрутизации постов (CQRS) + # Здесь мы указываем прямые адреса хостов:портов, а не имена upstream-блоков. + map $request_method $posts_target_host { + GET "posts_query_go:8089"; + HEAD "posts_query_go:8089"; + default "posts_command_prod_go:8088"; } server { listen 80; server_name writehub.space www.writehub.space; - location /.well-known/acme-challenge/ { root /var/www/certbot; } - location / { return 301 https://$host$request_uri; } + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } } server { @@ -32,46 +38,59 @@ http { ssl_certificate /etc/letsencrypt/live/writehub.space/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/writehub.space/privkey.pem; - # SSL настройки (как были)... + # Опциональные настройки SSL (рекомендуемые) + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers off; - # === FRONTEND CONFIGURATION === + # === FRONTEND === root /var/www/frontend; index index.html; - # Главная точка входа location / { - # Для SPA (Single Page Application). - # Если файл не найден, отдать index.html (Nuxt подхватит роутинг) + # Поддержка SPA (Single Page Application) try_files $uri $uri/ /index.html; } - # Кеширование статики (_nuxt, изображения, шрифты) + # Кеширование статики location ~* \.(?:ico|css|js|gif|jpe?g|png|woff2?|svg|ttf|eot)$ { expires 6M; access_log off; add_header Cache-Control "public"; } - # ============================== - # API (Backend) - без изменений + # === API BACKEND (Dynamic Proxy) === + + # BOTS SERVICE location /api/v1/bots { - proxy_pass http://bots_service; + # Использование переменной заставляет Nginx резолвить DNS при каждом запросе + set $upstream_bots "http://bots_go:8001"; + + proxy_pass $upstream_bots; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } + # PROFILES SERVICE location /api/v1/profiles { - proxy_pass http://profiles_service; + set $upstream_profiles "http://profiles_go:8003"; + + proxy_pass $upstream_profiles; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } + # POSTS SERVICE (Command/Query split) location /api/v1/posts { - proxy_pass http://$posts_upstream; + # Берем хост из map выше + set $upstream_posts "http://$posts_target_host"; + + proxy_pass $upstream_posts; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; From dd81f7cc9ed33b667d0eba94331934b8d45f14dc Mon Sep 17 00:00:00 2001 From: goriiin Date: Wed, 17 Dec 2025 14:53:48 +0300 Subject: [PATCH 18/29] bot-72: delete --- internal/delivery_http/bots/delete.go | 3 ++ internal/delivery_http/bots/list.go | 3 ++ nginx.conf.https | 42 +++++++++++++++------------ 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/internal/delivery_http/bots/delete.go b/internal/delivery_http/bots/delete.go index bf41a7d..8df7377 100644 --- a/internal/delivery_http/bots/delete.go +++ b/internal/delivery_http/bots/delete.go @@ -2,14 +2,17 @@ package bots import ( "context" + "log" gen "github.com/goriiin/kotyari-bots_backend/internal/gen/bots" ) func (h *Handler) DeleteBotById(ctx context.Context, params gen.DeleteBotByIdParams) (gen.DeleteBotByIdRes, error) { + log.Println("delete:", params.BotId) err := h.u.Delete(ctx, params.BotId) if err != nil { return nil, err } + return &gen.NoContent{}, nil } diff --git a/internal/delivery_http/bots/list.go b/internal/delivery_http/bots/list.go index 0b4ab92..cefbf0a 100644 --- a/internal/delivery_http/bots/list.go +++ b/internal/delivery_http/bots/list.go @@ -2,6 +2,7 @@ package bots import ( "context" + "log" gen "github.com/goriiin/kotyari-bots_backend/internal/gen/bots" ) @@ -17,6 +18,8 @@ func (h *Handler) ListBots(ctx context.Context) (gen.ListBotsRes, error) { genBots[i] = *modelToDTO(&b.Bot, b.Profiles) } + log.Println("bots list:", len(bots), genBots) + return &gen.BotList{ Data: genBots, NextCursor: gen.OptNilString{}, diff --git a/nginx.conf.https b/nginx.conf.https index edf7393..99057f7 100644 --- a/nginx.conf.https +++ b/nginx.conf.https @@ -6,12 +6,10 @@ http { include mime.types; default_type application/octet-stream; - # 1. Включаем резолвер Docker. - # Это критически важно: valid=10s заставляет Nginx обновлять кэш DNS. + # 1. Резолвер resolver 127.0.0.11 valid=10s; - # 2. Карта для маршрутизации постов (CQRS) - # Здесь мы указываем прямые адреса хостов:портов, а не имена upstream-блоков. + # 2. Карта маршрутизации map $request_method $posts_target_host { GET "posts_query_go:8089"; HEAD "posts_query_go:8089"; @@ -38,46 +36,55 @@ http { ssl_certificate /etc/letsencrypt/live/writehub.space/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/writehub.space/privkey.pem; - # Опциональные настройки SSL (рекомендуемые) ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; + # === ГЛОБАЛЬНОЕ ОТКЛЮЧЕНИЕ КЭША === + # Эти заголовки унаследуются всеми location, где нет своих add_header + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; + add_header Pragma "no-cache" always; + add_header Expires "0" always; + expires -1; + etag off; + # === FRONTEND === root /var/www/frontend; index index.html; location / { - # Поддержка SPA (Single Page Application) try_files $uri $uri/ /index.html; + # Для index.html SPA критически важно отсутствие кэша + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; } - # Кеширование статики + # Статика (JS, CSS, IMG) - ТЕПЕРЬ БЕЗ КЭША location ~* \.(?:ico|css|js|gif|jpe?g|png|woff2?|svg|ttf|eot)$ { - expires 6M; access_log off; - add_header Cache-Control "public"; + # Принудительно считаем устаревшим сразу + expires -1; + # Явно переписываем заголовки для этого блока + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; + add_header Pragma "no-cache" always; } - # === API BACKEND (Dynamic Proxy) === + # === API BACKEND === - # BOTS SERVICE location /api/v1/bots { - # Использование переменной заставляет Nginx резолвить DNS при каждом запросе set $upstream_bots "http://bots_go:8001"; - proxy_pass $upstream_bots; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + + # API тоже не кэшируем (наследуется от server, но можно убедиться явно, + # если бэкенд не шлет свои заголовки Cache-Control) } - # PROFILES SERVICE location /api/v1/profiles { set $upstream_profiles "http://profiles_go:8003"; - proxy_pass $upstream_profiles; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -85,11 +92,8 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } - # POSTS SERVICE (Command/Query split) location /api/v1/posts { - # Берем хост из map выше set $upstream_posts "http://$posts_target_host"; - proxy_pass $upstream_posts; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -97,4 +101,4 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } } -} \ No newline at end of file +} From 30cd0a808cc34530fbc19a224b8b6a80239ea4bf Mon Sep 17 00:00:00 2001 From: goriiin Date: Wed, 17 Dec 2025 14:58:24 +0300 Subject: [PATCH 19/29] bot-72: delete --- internal/delivery_http/bots/list.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/delivery_http/bots/list.go b/internal/delivery_http/bots/list.go index cefbf0a..8e982cb 100644 --- a/internal/delivery_http/bots/list.go +++ b/internal/delivery_http/bots/list.go @@ -8,11 +8,14 @@ import ( ) func (h *Handler) ListBots(ctx context.Context) (gen.ListBotsRes, error) { + log.Println("ListBots") bots, err := h.u.List(ctx) if err != nil { + log.Println(err) return nil, err } + log.Println("ListBots", bots) genBots := make([]gen.Bot, len(bots)) for i, b := range bots { genBots[i] = *modelToDTO(&b.Bot, b.Profiles) From 6ef2efe651137cb1baf9241067b3264326047d35 Mon Sep 17 00:00:00 2001 From: goriiin Date: Wed, 17 Dec 2025 15:06:40 +0300 Subject: [PATCH 20/29] bot-72: delete --- nginx.conf.https | 59 +++++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/nginx.conf.https b/nginx.conf.https index 99057f7..383ac4e 100644 --- a/nginx.conf.https +++ b/nginx.conf.https @@ -6,16 +6,24 @@ http { include mime.types; default_type application/octet-stream; - # 1. Резолвер + # 1. Резолвер для динамического upstream resolver 127.0.0.11 valid=10s; - # 2. Карта маршрутизации + # 2. Карта маршрутизации для бэкенда постов map $request_method $posts_target_host { GET "posts_query_go:8089"; HEAD "posts_query_go:8089"; default "posts_command_prod_go:8088"; } + # 3. Upstream для Nuxt SSR приложения + upstream nuxt_app { + # Предполагается, что Nuxt-сервер запущен локально на порту 3000 + server 127.0.0.1:3000; + keepalive 64; + } + + # Сервер для редиректа с HTTP на HTTPS server { listen 80; server_name writehub.space www.writehub.space; @@ -29,47 +37,39 @@ http { } } + # Основной сервер с SSL server { - listen 443 ssl; + listen 443 ssl http2; server_name writehub.space www.writehub.space; + # SSL сертификаты ssl_certificate /etc/letsencrypt/live/writehub.space/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/writehub.space/privkey.pem; + # Настройки SSL ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; - # === ГЛОБАЛЬНОЕ ОТКЛЮЧЕНИЕ КЭША === - # Эти заголовки унаследуются всеми location, где нет своих add_header - add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; - add_header Pragma "no-cache" always; - add_header Expires "0" always; - expires -1; - etag off; - - # === FRONTEND === - root /var/www/frontend; - index index.html; - + # === FRONTEND (Nuxt SSR) === + # Проксируем все запросы, не попавшие в другие location, на Nuxt-сервер location / { - try_files $uri $uri/ /index.html; - # Для index.html SPA критически важно отсутствие кэша - add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; - } - - # Статика (JS, CSS, IMG) - ТЕПЕРЬ БЕЗ КЭША - location ~* \.(?:ico|css|js|gif|jpe?g|png|woff2?|svg|ttf|eot)$ { - access_log off; - # Принудительно считаем устаревшим сразу - expires -1; - # Явно переписываем заголовки для этого блока - add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; - add_header Pragma "no-cache" always; + proxy_pass http://nuxt_app; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 1m; + proxy_connect_timeout 1m; + proxy_cache_bypass $http_upgrade; # Важно для WebSocket (HMR в dev режиме) } # === API BACKEND === + # Эти блоки остаются без изменений location /api/v1/bots { set $upstream_bots "http://bots_go:8001"; @@ -78,9 +78,6 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - - # API тоже не кэшируем (наследуется от server, но можно убедиться явно, - # если бэкенд не шлет свои заголовки Cache-Control) } location /api/v1/profiles { From 2284d3a783a4a58bc5474d3946d2fc01e6510c67 Mon Sep 17 00:00:00 2001 From: goriiin Date: Wed, 17 Dec 2025 15:16:35 +0300 Subject: [PATCH 21/29] bot-72: delete --- nginx.conf.https | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/nginx.conf.https b/nginx.conf.https index 383ac4e..f57b353 100644 --- a/nginx.conf.https +++ b/nginx.conf.https @@ -37,25 +37,20 @@ http { } } - # Основной сервер с SSL server { - listen 443 ssl http2; + listen 443 ssl; server_name writehub.space www.writehub.space; - # SSL сертификаты ssl_certificate /etc/letsencrypt/live/writehub.space/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/writehub.space/privkey.pem; - # Настройки SSL ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; - # === FRONTEND (Nuxt SSR) === - # Проксируем все запросы, не попавшие в другие location, на Nuxt-сервер location / { - proxy_pass http://nuxt_app; + proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; @@ -63,14 +58,9 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 1m; - proxy_connect_timeout 1m; - proxy_cache_bypass $http_upgrade; # Важно для WebSocket (HMR в dev режиме) + proxy_cache_bypass $http_upgrade; } - # === API BACKEND === - # Эти блоки остаются без изменений - location /api/v1/bots { set $upstream_bots "http://bots_go:8001"; proxy_pass $upstream_bots; @@ -99,3 +89,4 @@ http { } } } + From 0ab0562d78ea12ff4cae8a5427dcd8c1f4d8f987 Mon Sep 17 00:00:00 2001 From: goriiin Date: Wed, 17 Dec 2025 15:24:25 +0300 Subject: [PATCH 22/29] bot-72: delete --- docker-compose.nginx.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.nginx.yml b/docker-compose.nginx.yml index 938f006..1aa8e36 100644 --- a/docker-compose.nginx.yml +++ b/docker-compose.nginx.yml @@ -5,6 +5,8 @@ services: ports: - "80:80" - "443:443" + extra_hosts: + - "host.docker.internal:host-gateway" volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro - ./certbot/conf:/etc/letsencrypt From 4d1971285e67d2683a683b1a2ec7278ba2bbf5bb Mon Sep 17 00:00:00 2001 From: goriiin Date: Wed, 17 Dec 2025 15:30:17 +0300 Subject: [PATCH 23/29] bot-72: delete --- nginx.conf.https | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/nginx.conf.https b/nginx.conf.https index f57b353..008e0ea 100644 --- a/nginx.conf.https +++ b/nginx.conf.https @@ -18,8 +18,8 @@ http { # 3. Upstream для Nuxt SSR приложения upstream nuxt_app { - # Предполагается, что Nuxt-сервер запущен локально на порту 3000 - server 127.0.0.1:3000; + # host.docker.internal теперь указывает на ваш хост-сервер (где запущен Nuxt) + server host.docker.internal:3000; keepalive 64; } @@ -50,7 +50,7 @@ http { ssl_prefer_server_ciphers off; location / { - proxy_pass http://127.0.0.1:3000; + proxy_pass http://nuxt_app; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; @@ -58,7 +58,6 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache_bypass $http_upgrade; } location /api/v1/bots { From 3b4ee656f8edb6b3395f42bcba348c95fae960aa Mon Sep 17 00:00:00 2001 From: jojosoderqvist Date: Wed, 17 Dec 2025 16:25:04 +0300 Subject: [PATCH 24/29] added task field --- docs/posts/common/schemas.yaml | 4 ++ internal/delivery_http/posts/gen_dto.go | 2 + internal/delivery_http/posts/kafka_dto.go | 28 +++++++------ .../posts_command_producer/update_post.go | 2 +- .../gen/posts/posts_command/oas_json_gen.go | 39 +++++++++++++------ .../posts/posts_command/oas_schemas_gen.go | 12 ++++++ .../gen/posts/posts_query/oas_json_gen.go | 39 +++++++++++++------ .../gen/posts/posts_query/oas_schemas_gen.go | 12 ++++++ internal/repo/posts/dto.go | 1 + .../repo/posts/posts_command/update_post.go | 3 +- 10 files changed, 106 insertions(+), 36 deletions(-) diff --git a/docs/posts/common/schemas.yaml b/docs/posts/common/schemas.yaml index bdbc0fc..0242d5b 100644 --- a/docs/posts/common/schemas.yaml +++ b/docs/posts/common/schemas.yaml @@ -57,6 +57,9 @@ Post: - "knowledge" - "history" description: "Тип поста" + task: + type: string + description: "Задание на генерацию от пользователя" title: type: string description: "Название поста" @@ -85,6 +88,7 @@ Post: - profileId - profileName - platform + - task - title - text - createdAt diff --git a/internal/delivery_http/posts/gen_dto.go b/internal/delivery_http/posts/gen_dto.go index 3534867..fa1ac07 100644 --- a/internal/delivery_http/posts/gen_dto.go +++ b/internal/delivery_http/posts/gen_dto.go @@ -24,6 +24,7 @@ func QueryModelToHttp(post model.Post) *genQuery.Post { ProfileName: post.ProfileName, Platform: genQuery.PostPlatform(post.Platform), PostType: postType, + Task: post.UserPrompt, Title: post.Title, Text: post.Text, Categories: nil, @@ -61,6 +62,7 @@ func ModelToHttp(post model.Post) *genCommand.Post { ProfileName: post.ProfileName, Platform: genCommand.PostPlatform(post.Platform), PostType: postType, + Task: post.UserPrompt, Title: post.Title, Text: post.Text, Categories: nil, diff --git a/internal/delivery_http/posts/kafka_dto.go b/internal/delivery_http/posts/kafka_dto.go index 0567e89..44670e2 100644 --- a/internal/delivery_http/posts/kafka_dto.go +++ b/internal/delivery_http/posts/kafka_dto.go @@ -12,7 +12,7 @@ const ( CmdUpdate kafkaConfig.Command = "update" CmdDelete kafkaConfig.Command = "delete" CmdPublish kafkaConfig.Command = "publish" - CmdSeen kafkaConfig.Command = "seen" + CmdSeen kafkaConfig.Command = "seen" ) // KafkaResponse TODO: model.Post -> []model.Post? @@ -74,17 +74,21 @@ func (r KafkaResponse) PostCommandToGen() *gen.Post { } return &gen.Post{ - ID: r.Post.ID, - OtvetiId: r.Post.OtvetiID, - BotId: r.Post.BotID, - ProfileId: r.Post.ProfileID, - Platform: gen.PostPlatform(r.Post.Platform), - PostType: postType, - Title: r.Post.Title, - Text: r.Post.Text, - Categories: nil, // TODO: ?? - CreatedAt: r.Post.CreatedAt, - UpdatedAt: r.Post.UpdatedAt, + ID: r.Post.ID, + OtvetiId: r.Post.OtvetiID, + BotId: r.Post.BotID, + BotName: r.Post.BotName, + ProfileId: r.Post.ProfileID, + ProfileName: r.Post.ProfileName, + GroupId: r.Post.GroupID, + Platform: gen.PostPlatform(r.Post.Platform), + PostType: postType, + Task: r.Post.UserPrompt, + Title: r.Post.Title, + Text: r.Post.Text, + Categories: nil, // TODO: ?? + CreatedAt: r.Post.CreatedAt, + UpdatedAt: r.Post.UpdatedAt, } } diff --git a/internal/delivery_http/posts/posts_command_producer/update_post.go b/internal/delivery_http/posts/posts_command_producer/update_post.go index 31a3614..afa281a 100644 --- a/internal/delivery_http/posts/posts_command_producer/update_post.go +++ b/internal/delivery_http/posts/posts_command_producer/update_post.go @@ -49,7 +49,7 @@ func (p *PostsCommandHandler) UpdatePostById(ctx context.Context, req *gen.PostU case strings.Contains(resp.Error, constants.InternalMsg): return &gen.UpdatePostByIdInternalServerError{ ErrorCode: http.StatusInternalServerError, - Message: constants.InternalMsg, + Message: resp.Error, }, nil case strings.Contains(resp.Error, constants.NotFoundMsg): diff --git a/internal/gen/posts/posts_command/oas_json_gen.go b/internal/gen/posts/posts_command/oas_json_gen.go index 2f87257..3cfc226 100644 --- a/internal/gen/posts/posts_command/oas_json_gen.go +++ b/internal/gen/posts/posts_command/oas_json_gen.go @@ -812,6 +812,10 @@ func (s *Post) encodeFields(e *jx.Encoder) { s.PostType.Encode(e) } } + { + e.FieldStart("task") + e.Str(s.Task) + } { e.FieldStart("title") e.Str(s.Title) @@ -840,7 +844,7 @@ func (s *Post) encodeFields(e *jx.Encoder) { } } -var jsonFieldsNameOfPost = [14]string{ +var jsonFieldsNameOfPost = [15]string{ 0: "id", 1: "otvetiId", 2: "groupId", @@ -850,11 +854,12 @@ var jsonFieldsNameOfPost = [14]string{ 6: "profileName", 7: "platform", 8: "postType", - 9: "title", - 10: "text", - 11: "categories", - 12: "createdAt", - 13: "updatedAt", + 9: "task", + 10: "title", + 11: "text", + 12: "categories", + 13: "createdAt", + 14: "updatedAt", } // Decode decodes Post from json. @@ -970,8 +975,20 @@ func (s *Post) Decode(d *jx.Decoder) error { }(); err != nil { return errors.Wrap(err, "decode field \"postType\"") } - case "title": + case "task": requiredBitSet[1] |= 1 << 1 + if err := func() error { + v, err := d.Str() + s.Task = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"task\"") + } + case "title": + requiredBitSet[1] |= 1 << 2 if err := func() error { v, err := d.Str() s.Title = string(v) @@ -983,7 +1000,7 @@ func (s *Post) Decode(d *jx.Decoder) error { return errors.Wrap(err, "decode field \"title\"") } case "text": - requiredBitSet[1] |= 1 << 2 + requiredBitSet[1] |= 1 << 3 if err := func() error { v, err := d.Str() s.Text = string(v) @@ -1012,7 +1029,7 @@ func (s *Post) Decode(d *jx.Decoder) error { return errors.Wrap(err, "decode field \"categories\"") } case "createdAt": - requiredBitSet[1] |= 1 << 4 + requiredBitSet[1] |= 1 << 5 if err := func() error { v, err := json.DecodeDateTime(d) s.CreatedAt = v @@ -1024,7 +1041,7 @@ func (s *Post) Decode(d *jx.Decoder) error { return errors.Wrap(err, "decode field \"createdAt\"") } case "updatedAt": - requiredBitSet[1] |= 1 << 5 + requiredBitSet[1] |= 1 << 6 if err := func() error { v, err := json.DecodeDateTime(d) s.UpdatedAt = v @@ -1046,7 +1063,7 @@ func (s *Post) Decode(d *jx.Decoder) error { var failures []validate.FieldError for i, mask := range [2]uint8{ 0b11111111, - 0b00110110, + 0b01101110, } { if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { // Mask only required fields and check equality to mask using XOR. diff --git a/internal/gen/posts/posts_command/oas_schemas_gen.go b/internal/gen/posts/posts_command/oas_schemas_gen.go index 3ce8a33..0582e44 100644 --- a/internal/gen/posts/posts_command/oas_schemas_gen.go +++ b/internal/gen/posts/posts_command/oas_schemas_gen.go @@ -404,6 +404,8 @@ type Post struct { Platform PostPlatform `json:"platform"` // Тип поста. PostType OptNilPostPostType `json:"postType"` + // Задание на генерацию от пользователя. + Task string `json:"task"` // Название поста. Title string `json:"title"` // Текстовое содержимое поста. @@ -459,6 +461,11 @@ func (s *Post) GetPostType() OptNilPostPostType { return s.PostType } +// GetTask returns the value of Task. +func (s *Post) GetTask() string { + return s.Task +} + // GetTitle returns the value of Title. func (s *Post) GetTitle() string { return s.Title @@ -529,6 +536,11 @@ func (s *Post) SetPostType(val OptNilPostPostType) { s.PostType = val } +// SetTask sets the value of Task. +func (s *Post) SetTask(val string) { + s.Task = val +} + // SetTitle sets the value of Title. func (s *Post) SetTitle(val string) { s.Title = val diff --git a/internal/gen/posts/posts_query/oas_json_gen.go b/internal/gen/posts/posts_query/oas_json_gen.go index 866257d..4f9f538 100644 --- a/internal/gen/posts/posts_query/oas_json_gen.go +++ b/internal/gen/posts/posts_query/oas_json_gen.go @@ -859,6 +859,10 @@ func (s *Post) encodeFields(e *jx.Encoder) { s.PostType.Encode(e) } } + { + e.FieldStart("task") + e.Str(s.Task) + } { e.FieldStart("title") e.Str(s.Title) @@ -887,7 +891,7 @@ func (s *Post) encodeFields(e *jx.Encoder) { } } -var jsonFieldsNameOfPost = [14]string{ +var jsonFieldsNameOfPost = [15]string{ 0: "id", 1: "otvetiId", 2: "groupId", @@ -897,11 +901,12 @@ var jsonFieldsNameOfPost = [14]string{ 6: "profileName", 7: "platform", 8: "postType", - 9: "title", - 10: "text", - 11: "categories", - 12: "createdAt", - 13: "updatedAt", + 9: "task", + 10: "title", + 11: "text", + 12: "categories", + 13: "createdAt", + 14: "updatedAt", } // Decode decodes Post from json. @@ -1017,8 +1022,20 @@ func (s *Post) Decode(d *jx.Decoder) error { }(); err != nil { return errors.Wrap(err, "decode field \"postType\"") } - case "title": + case "task": requiredBitSet[1] |= 1 << 1 + if err := func() error { + v, err := d.Str() + s.Task = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"task\"") + } + case "title": + requiredBitSet[1] |= 1 << 2 if err := func() error { v, err := d.Str() s.Title = string(v) @@ -1030,7 +1047,7 @@ func (s *Post) Decode(d *jx.Decoder) error { return errors.Wrap(err, "decode field \"title\"") } case "text": - requiredBitSet[1] |= 1 << 2 + requiredBitSet[1] |= 1 << 3 if err := func() error { v, err := d.Str() s.Text = string(v) @@ -1059,7 +1076,7 @@ func (s *Post) Decode(d *jx.Decoder) error { return errors.Wrap(err, "decode field \"categories\"") } case "createdAt": - requiredBitSet[1] |= 1 << 4 + requiredBitSet[1] |= 1 << 5 if err := func() error { v, err := json.DecodeDateTime(d) s.CreatedAt = v @@ -1071,7 +1088,7 @@ func (s *Post) Decode(d *jx.Decoder) error { return errors.Wrap(err, "decode field \"createdAt\"") } case "updatedAt": - requiredBitSet[1] |= 1 << 5 + requiredBitSet[1] |= 1 << 6 if err := func() error { v, err := json.DecodeDateTime(d) s.UpdatedAt = v @@ -1093,7 +1110,7 @@ func (s *Post) Decode(d *jx.Decoder) error { var failures []validate.FieldError for i, mask := range [2]uint8{ 0b11111111, - 0b00110110, + 0b01101110, } { if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { // Mask only required fields and check equality to mask using XOR. diff --git a/internal/gen/posts/posts_query/oas_schemas_gen.go b/internal/gen/posts/posts_query/oas_schemas_gen.go index 8d4694d..bf6baeb 100644 --- a/internal/gen/posts/posts_query/oas_schemas_gen.go +++ b/internal/gen/posts/posts_query/oas_schemas_gen.go @@ -264,6 +264,8 @@ type Post struct { Platform PostPlatform `json:"platform"` // Тип поста. PostType OptNilPostPostType `json:"postType"` + // Задание на генерацию от пользователя. + Task string `json:"task"` // Название поста. Title string `json:"title"` // Текстовое содержимое поста. @@ -319,6 +321,11 @@ func (s *Post) GetPostType() OptNilPostPostType { return s.PostType } +// GetTask returns the value of Task. +func (s *Post) GetTask() string { + return s.Task +} + // GetTitle returns the value of Title. func (s *Post) GetTitle() string { return s.Title @@ -389,6 +396,11 @@ func (s *Post) SetPostType(val OptNilPostPostType) { s.PostType = val } +// SetTask sets the value of Task. +func (s *Post) SetTask(val string) { + s.Task = val +} + // SetTitle sets the value of Title. func (s *Post) SetTitle(val string) { s.Title = val diff --git a/internal/repo/posts/dto.go b/internal/repo/posts/dto.go index cc39c8e..43f6e13 100644 --- a/internal/repo/posts/dto.go +++ b/internal/repo/posts/dto.go @@ -68,6 +68,7 @@ func (d PostDTO) ToModel() model.Post { ProfileName: d.ProfileName, Platform: model.PlatformType(d.Platform), Type: postType, + UserPrompt: d.UserPrompt, Title: d.Title, Text: d.Text, CreatedAt: d.CreatedAt, diff --git a/internal/repo/posts/posts_command/update_post.go b/internal/repo/posts/posts_command/update_post.go index 4bfa660..0599e43 100644 --- a/internal/repo/posts/posts_command/update_post.go +++ b/internal/repo/posts/posts_command/update_post.go @@ -15,7 +15,7 @@ func (p *PostsCommandRepo) UpdatePost(ctx context.Context, post model.Post) (mod UPDATE posts SET post_title=$1, post_text=$2, updated_at=NOW() WHERE id=$3 - RETURNING id, otveti_id, bot_id, profile_id, platform_type, post_type, post_title, post_text, created_at, updated_at + RETURNING id, otveti_id, bot_id, bot_name, profile_id, profile_name, group_id, platform_type, user_prompt, post_type, post_title, post_text, created_at, updated_at ` rows, err := p.db.Query(ctx, query, post.Title, post.Text, post.ID) @@ -31,5 +31,6 @@ func (p *PostsCommandRepo) UpdatePost(ctx context.Context, post model.Post) (mod return model.Post{}, constants.ErrInternal } + return modifiedPost.ToModel(), nil } From c7cfb434eefb3a9b0ccac95af5afb9ab8efb2505 Mon Sep 17 00:00:00 2001 From: jojosoderqvist Date: Wed, 17 Dec 2025 17:04:12 +0300 Subject: [PATCH 25/29] fixed posts generation --- .../posts_command_consumer/create_post.go | 31 +++++-------------- 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/internal/delivery_http/posts/posts_command_consumer/create_post.go b/internal/delivery_http/posts/posts_command_consumer/create_post.go index 07e472e..c9d6aab 100644 --- a/internal/delivery_http/posts/posts_command_consumer/create_post.go +++ b/internal/delivery_http/posts/posts_command_consumer/create_post.go @@ -60,10 +60,14 @@ func (p *PostsCommandConsumer) processProfile(ctx context.Context, req posts.Kaf return nil } - bestPost := p.createPostFromCandidate(req, profile, bestPostCandidate) - p.publishToOtvet(ctx, req, bestPostCandidate, bestPost) + bestPost := postsMap[profile.ProfileID] + bestPost.Title = bestPostCandidate.Title + bestPost.Text = bestPostCandidate.Text - return bestPost + //bestPost := p.createPostFromCandidate(req, profile, bestPostCandidate) + p.publishToOtvet(ctx, req, bestPostCandidate, &bestPost) + + return &bestPost } // generatePostsForProfile generates multiple post candidates for a profile @@ -106,26 +110,6 @@ func (p *PostsCommandConsumer) generatePostsForProfile(ctx context.Context, req return profilesPosts } - -// createPostFromCandidate creates a Post model from a candidate -func (p *PostsCommandConsumer) createPostFromCandidate(req posts.KafkaCreatePostRequest, profile posts.CreatePostProfiles, candidate model.Candidate) *model.Post { - return &model.Post{ - ID: uuid.New(), - OtvetiID: 0, - BotID: req.BotID, - BotName: req.BotName, - ProfileID: profile.ProfileID, - ProfileName: profile.ProfileName, - GroupID: req.GroupID, - Platform: req.Platform, - Type: req.PostType, - UserPrompt: req.UserPrompt, - Title: candidate.Title, - Text: candidate.Text, - } -} - - // publishToOtvet publishes post to otvet.mail.ru if platform is otveti func (p *PostsCommandConsumer) publishToOtvet(ctx context.Context, req posts.KafkaCreatePostRequest, candidate model.Candidate, post *model.Post) { if req.Platform != model.OtvetiPlatform || p.otvetClient == nil { @@ -170,7 +154,6 @@ func (p *PostsCommandConsumer) publishToOtvet(ctx context.Context, req posts.Kaf } } - // getSpacesForPost predicts spaces for a post or returns default spaces func (p *PostsCommandConsumer) getSpacesForPost(ctx context.Context, candidate model.Candidate) []otvet.Space { combinedText := candidate.Title + " " + candidate.Text From b1ad1f28fe26f3f6158e4b1801ee1ac00d5d937c Mon Sep 17 00:00:00 2001 From: jojosoderqvist Date: Wed, 17 Dec 2025 18:04:57 +0300 Subject: [PATCH 26/29] added network to docker compose --- configs/posts-local.yaml | 2 +- docker-compose.posts.yml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/configs/posts-local.yaml b/configs/posts-local.yaml index 52b14d8..03e32a5 100644 --- a/configs/posts-local.yaml +++ b/configs/posts-local.yaml @@ -28,7 +28,7 @@ posts_producer_reply: kind: consumer posts_consumer_grpc: - posts_addr: "host.docker.internal:50051" # Создать общий network между docker-compose.yml + posts_addr: "analytics:50051" # Создать общий network между docker-compose.yml dial_timeout: 10 posts_consumer_request: diff --git a/docker-compose.posts.yml b/docker-compose.posts.yml index b03634e..7f4f78f 100644 --- a/docker-compose.posts.yml +++ b/docker-compose.posts.yml @@ -62,6 +62,7 @@ services: networks: - posts-internal-network - public-gateway-network + - common-network depends_on: kafka: condition: service_healthy @@ -128,6 +129,8 @@ networks: driver: bridge public-gateway-network: external: true + common-network: + external: true volumes: posts_db_data: From 1f38f03685a6183d64d50efa87f590e31bea3d4c Mon Sep 17 00:00:00 2001 From: goriiin Date: Fri, 9 Jan 2026 15:36:54 +0300 Subject: [PATCH 27/29] bot-72: fix create + add logger --- internal/apps/posts_command_consumer/init.go | 5 +++- .../posts/posts_command_consumer/handler.go | 25 +++++++++++-------- .../posts/posts_command_consumer/init.go | 4 +++ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/internal/apps/posts_command_consumer/init.go b/internal/apps/posts_command_consumer/init.go index 26aba25..f899ac9 100644 --- a/internal/apps/posts_command_consumer/init.go +++ b/internal/apps/posts_command_consumer/init.go @@ -11,6 +11,7 @@ import ( "github.com/goriiin/kotyari-bots_backend/internal/kafka" "github.com/goriiin/kotyari-bots_backend/internal/kafka/consumer" "github.com/goriiin/kotyari-bots_backend/internal/kafka/producer" + "github.com/goriiin/kotyari-bots_backend/internal/logger" postsRepoLib "github.com/goriiin/kotyari-bots_backend/internal/repo/posts/posts_command" "github.com/goriiin/kotyari-bots_backend/pkg/evals" "github.com/goriiin/kotyari-bots_backend/pkg/grok" @@ -36,6 +37,8 @@ type PostsCommandConsumer struct { } func NewPostsCommandConsumer(config *PostsCommandConsumerConfig, llmConfig *LLMConfig) (*PostsCommandConsumer, error) { + log := logger.NewLogger("posts-command-consumer", &config.ConfigBase) + pool, err := postgres.GetPool(context.Background(), config.Database) if err != nil { return nil, err @@ -106,7 +109,7 @@ func NewPostsCommandConsumer(config *PostsCommandConsumerConfig, llmConfig *LLMC go queue.StartProcessing(ctx, publishPostFromQueue) return &PostsCommandConsumer{ - consumerRunner: posts_command_consumer.NewPostsCommandConsumer(cons, repo, grpc, rw, j, otvetClient, queue), + consumerRunner: posts_command_consumer.NewPostsCommandConsumer(cons, repo, grpc, rw, j, otvetClient, queue, log), consumer: cons, config: config, }, nil diff --git a/internal/delivery_http/posts/posts_command_consumer/handler.go b/internal/delivery_http/posts/posts_command_consumer/handler.go index a3c880d..e34f990 100644 --- a/internal/delivery_http/posts/posts_command_consumer/handler.go +++ b/internal/delivery_http/posts/posts_command_consumer/handler.go @@ -18,7 +18,8 @@ func (p *PostsCommandConsumer) HandleCommands() error { for message := range p.consumer.Start(ctx) { var env kafkaConfig.Envelope if err := jsoniter.Unmarshal(message.Msg.Value, &env); err != nil { - fmt.Printf("%s: %v\n", constants.ErrUnmarshal, err) + p.log.Error(err, false, constants.ErrUnmarshal.Error()) + _ = message.Ack(ctx) continue } @@ -39,7 +40,7 @@ func (p *PostsCommandConsumer) HandleCommands() error { } if err != nil { - fmt.Printf("failed to handle command '%s': %v\n", env.Command, err) + p.log.Error(err, false, fmt.Sprintf("failed to handle command '%s'", env.Command)) } } @@ -115,18 +116,20 @@ func (p *PostsCommandConsumer) handleCreateCommand(ctx context.Context, message return errors.Wrap(err, "failed to ACK posts creation") } - err = p.CreatePost(ctx, postsMapping, req) - if err != nil { - // TODO: LOG - fmt.Printf("failed to create post: %s", err.Error()) - - return message.Nack(ctx, err) - } - if err = message.Ack(ctx); err != nil { - return errors.Wrap(err, "failed to ACK posts creation") + return errors.Wrap(err, "failed to commit offset") } + // Запускаем генерацию в фоне + go func() { + bgCtx := context.Background() + if err := p.CreatePost(bgCtx, postsMapping, req); err != nil { + p.log.Error(err, false, fmt.Sprintf("Async post generation failed for GroupID %s", req.GroupID)) + } else { + p.log.Info(fmt.Sprintf("Async post generation finished for GroupID %s", req.GroupID)) + } + }() + return nil } diff --git a/internal/delivery_http/posts/posts_command_consumer/init.go b/internal/delivery_http/posts/posts_command_consumer/init.go index a32b304..df762dd 100644 --- a/internal/delivery_http/posts/posts_command_consumer/init.go +++ b/internal/delivery_http/posts/posts_command_consumer/init.go @@ -6,6 +6,7 @@ import ( "github.com/google/uuid" postssgen "github.com/goriiin/kotyari-bots_backend/api/protos/posts/gen" kafkaConfig "github.com/goriiin/kotyari-bots_backend/internal/kafka" + "github.com/goriiin/kotyari-bots_backend/internal/logger" "github.com/goriiin/kotyari-bots_backend/internal/model" "github.com/goriiin/kotyari-bots_backend/pkg/otvet" "github.com/goriiin/kotyari-bots_backend/pkg/posting_queue" @@ -59,6 +60,7 @@ type PostsCommandConsumer struct { judge judge otvetClient otvetClient queue postingQueue + log *logger.Logger } func NewPostsCommandConsumer( @@ -69,6 +71,7 @@ func NewPostsCommandConsumer( judge judge, otvetClient otvetClient, queue postingQueue, + log *logger.Logger, ) *PostsCommandConsumer { return &PostsCommandConsumer{ consumer: consumer, @@ -78,5 +81,6 @@ func NewPostsCommandConsumer( judge: judge, otvetClient: otvetClient, queue: queue, + log: log, } } From e6486258b3bd61adf8b62b323ebc6c233a962382 Mon Sep 17 00:00:00 2001 From: goriiin Date: Fri, 9 Jan 2026 15:39:55 +0300 Subject: [PATCH 28/29] bot-72: fix lint --- .../delivery_http/posts/posts_command_consumer/create_post.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/delivery_http/posts/posts_command_consumer/create_post.go b/internal/delivery_http/posts/posts_command_consumer/create_post.go index c9d6aab..2c72f0e 100644 --- a/internal/delivery_http/posts/posts_command_consumer/create_post.go +++ b/internal/delivery_http/posts/posts_command_consumer/create_post.go @@ -64,7 +64,7 @@ func (p *PostsCommandConsumer) processProfile(ctx context.Context, req posts.Kaf bestPost.Title = bestPostCandidate.Title bestPost.Text = bestPostCandidate.Text - //bestPost := p.createPostFromCandidate(req, profile, bestPostCandidate) + // bestPost := p.createPostFromCandidate(req, profile, bestPostCandidate) p.publishToOtvet(ctx, req, bestPostCandidate, &bestPost) return &bestPost From fe6dda413cf2414e5dfbed5f777e466bbd6b15e7 Mon Sep 17 00:00:00 2001 From: goriiin Date: Fri, 9 Jan 2026 15:43:17 +0300 Subject: [PATCH 29/29] bot-72: fix --- Makefile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Makefile b/Makefile index e55f8d8..829b364 100644 --- a/Makefile +++ b/Makefile @@ -180,6 +180,11 @@ lint: format: @gci write . --skip-generated --skip-vendor < /dev/null +format-check: + @gci diff . --skip-generated --skip-vendor < /dev/null + +check: lint format-check + # --- INTRANET (Parsers) --- INTRANET_DIR := ./intranet