package classes import ( "context" "errors" "strconv" "strings" "time" "git.golem15.com/golem15/sm-seo-plugin/models" "gorm.io/gorm" "gorm.io/gorm/clause" ) // RobotsRow returns robots row 1 inside db, creating it when the // migration's row is gone (first or create, safe against a concurrent // first request). func RobotsRow(db *gorm.DB) (models.Robots, error) { var rows []models.Robots if err := db.Where("id = ?", 1).Limit(1).Find(&rows).Error; err != nil { return models.Robots{}, err } if len(rows) > 0 { return rows[0], nil } row := models.Robots{ID: 1, Bots: models.BotSwitches{}} if err := db.Clauses(clause.OnConflict{DoNothing: true}).Create(&row).Error; err != nil { return models.Robots{}, err } if err := db.Where("id = ?", 1).Limit(1).Find(&rows).Error; err != nil { return models.Robots{}, err } if len(rows) == 0 { return models.Robots{}, errors.New("golem15.seo: robots row is missing") } return rows[0], nil } // RobotsValues are the robots row's values for history: the editor's text // and every listed bot's switch. func RobotsValues(r models.Robots) map[string]any { bots := map[string]any{} for _, b := range Bots { bots[b.Token] = r.Bots.Allowed(b.Token) } return map[string]any{"content": r.Content, "bots": bots, "llms_enabled": r.LlmsEnabled, "llms_intro": r.LlmsIntro} } // RobotsWrite is one change of robots.txt: the editor's text when Content // is set, and the switches of the bots listed in Bots (by token). type RobotsWrite struct { Content *string Bots map[string]bool LlmsEnabled *bool LlmsIntro *string } // SaveRobots writes robots.txt inside tx and records it in history // (D-22, D-25). A text over MaxRobotsBytes and a token that is not a // listed AI bot are refused with a *ProblemsError. It returns the stored // row and the noindex addresses Googlebot may no longer fetch under it // (the robots.blocks_noindex warning). func (s *Service) SaveRobots(ctx context.Context, tx *gorm.DB, in RobotsWrite) (models.Robots, []string, error) { row, err := RobotsRow(tx) if err != nil { return models.Robots{}, nil, err } problems := map[string][]models.Problem{} if in.Content != nil && len(*in.Content) > MaxRobotsBytes { problems["content"] = append(problems["content"], models.Problem{Key: "golem15.seo::lang.robots.too_large"}) } for token := range in.Bots { if _, ok := BotByToken(token); !ok { problems["bots."+token] = append(problems["bots."+token], models.Problem{Key: "golem15.seo::lang.api.bot_unknown"}) } } if len(problems) > 0 { return models.Robots{}, nil, &ProblemsError{Problems: problems} } before := RobotsValues(row) if in.Content != nil { row.Content = *in.Content } bots := models.BotSwitches{} for token, on := range row.Bots { bots[token] = on } for token, on := range in.Bots { b, _ := BotByToken(token) bots[b.Token] = on } row.Bots = bots if in.LlmsEnabled != nil { row.LlmsEnabled = *in.LlmsEnabled } if in.LlmsIntro != nil { row.LlmsIntro = strings.TrimSpace(*in.LlmsIntro) } now := time.Now().UTC() row.UpdatedAt = &now if err := tx.Model(&models.Robots{}).Where("id = ?", row.ID).Updates(map[string]any{ "content": row.Content, "bots": row.Bots, "llms_enabled": row.LlmsEnabled, "llms_intro": row.LlmsIntro, "updated_at": now, }).Error; err != nil { return models.Robots{}, nil, err } if err := s.Record(ctx, tx, Change{ Kind: models.SubjectRobots, SubjectID: strconv.FormatUint(uint64(row.ID), 10), What: s.Phrase("golem15.seo::lang.history.what_robots", nil), Before: before, After: RobotsValues(row), }); err != nil { return models.Robots{}, nil, err } blocked, err := s.BlockedNoindex(tx, row.Content, row.Bots) if err != nil { return models.Robots{}, nil, err } return row, blocked, nil }