From 2f15769147b79f78a5ab33734ca6f1b134d74e39 Mon Sep 17 00:00:00 2001 From: Kirill Shaprunov Date: Sun, 12 May 2024 04:20:18 +0300 Subject: [PATCH 01/14] [nmt/internal] Add gitea mode --- internal/config/config.go | 35 ++- internal/database/database.go | 114 +++++++- internal/database/database_proxy.go | 111 ++++++++ internal/gitlab/pipelines.go | 205 --------------- internal/logfield/logfield.go | 10 + internal/models/flag.go | 1 + internal/models/overrides.go | 1 + internal/models/user.go | 8 +- internal/platform/base/base_client.go | 56 ++++ internal/platform/base/base_pipelines.go | 55 ++++ internal/platform/base/base_projects.go | 26 ++ internal/platform/client_fabric.go | 49 ++++ internal/platform/gitea/gitea_client.go | 157 +++++++++++ internal/platform/gitea/gitea_pipelines.go | 243 ++++++++++++++++++ internal/platform/gitea/gitea_projects.go | 97 +++++++ .../gitlab/gitlab_client.go} | 118 +++------ internal/platform/gitlab/gitlab_pipelines.go | 180 +++++++++++++ .../gitlab/gitlab_projects.go} | 60 ++--- internal/{gitlab => platform}/oauth.go | 26 +- internal/platform/pipelines_fabric.go | 47 ++++ internal/platform/projects_fabric.go | 49 ++++ internal/scorer/scorer.go | 10 +- internal/web/login.go | 157 ++++++++--- internal/web/oauth.go | 16 +- internal/web/render.go | 34 ++- internal/web/run.go | 21 +- internal/web/server.go | 30 ++- 27 files changed, 1513 insertions(+), 403 deletions(-) create mode 100644 internal/database/database_proxy.go delete mode 100644 internal/gitlab/pipelines.go create mode 100644 internal/platform/base/base_client.go create mode 100644 internal/platform/base/base_pipelines.go create mode 100644 internal/platform/base/base_projects.go create mode 100644 internal/platform/client_fabric.go create mode 100644 internal/platform/gitea/gitea_client.go create mode 100644 internal/platform/gitea/gitea_pipelines.go create mode 100644 internal/platform/gitea/gitea_projects.go rename internal/{gitlab/gitlab.go => platform/gitlab/gitlab_client.go} (50%) create mode 100644 internal/platform/gitlab/gitlab_pipelines.go rename internal/{gitlab/projects.go => platform/gitlab/gitlab_projects.go} (50%) rename internal/{gitlab => platform}/oauth.go (51%) create mode 100644 internal/platform/pipelines_fabric.go create mode 100644 internal/platform/projects_fabric.go diff --git a/internal/config/config.go b/internal/config/config.go index e06f888..1e33f81 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -8,6 +8,11 @@ import ( "github.com/pkg/errors" ) +const ( + GitlabMode = "gitlab" + GiteaMode = "gitea" +) + type GitLabConfig struct { BaseURL string Group struct { @@ -27,6 +32,25 @@ type GitLabConfig struct { CIConfigPath string } +type GiteaConfig struct { + BaseURL string + Organization struct { + Name string + ID int + } + DefaultReadme string + TaskUrlPrefix string + + Application struct { + ClientID string + Secret string + } + Api struct { + Token string + } + CIConfigPath string +} + type EndpointsConfig struct { HostName string Home string @@ -100,9 +124,16 @@ type TelegramBotConfig struct { BotToken string } +type PlatformConfig struct { + GitLab GitLabConfig + Gitea GiteaConfig + Mode string +} + type Config struct { - Log log.Config - GitLab GitLabConfig + Log log.Config + + Platform PlatformConfig Endpoints EndpointsConfig Server ServerConfig DataBase DataBaseConfig diff --git a/internal/database/database.go b/internal/database/database.go index b05dbb3..22a9888 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -104,6 +104,24 @@ func (db *DataBase) FindUserByGitlabID(id int) (*models.User, error) { return &user, nil } +func (db *DataBase) FindUserByGiteaLogin(login string) (*models.User, error) { + var user models.User + err := db.First(&user, "gitea_login = ?", login).Error + if err != nil { + return nil, err + } + return &user, nil +} + +func (db *DataBase) FindUserByGiteaID(id int64) (*models.User, error) { + var user models.User + err := db.First(&user, "gitea_id = ?", id).Error + if err != nil { + return nil, err + } + return &user, nil +} + func (db *DataBase) FindUserByTelegramID(id int64) (*models.User, error) { var user models.User err := db.First(&user, "telegram_id = ?", id).Error @@ -116,7 +134,7 @@ func (db *DataBase) FindUserByTelegramID(id int64) (*models.User, error) { return &user, nil } -func (db *DataBase) ListUsersWithoutRepos() ([]*models.User, error) { +func (db *DataBase) ListGitlabUsersWithoutRepos() ([]*models.User, error) { var users []*models.User err := db.Find(&users, "repository IS NULL AND gitlab_id IS NOT NULL AND gitlab_login IS NOT NULL").Error if err != nil { @@ -125,6 +143,15 @@ func (db *DataBase) ListUsersWithoutRepos() ([]*models.User, error) { return users, nil } +func (db *DataBase) ListGiteaUsersWithoutRepos() ([]*models.User, error) { + var users []*models.User + err := db.Find(&users, "repository IS NULL AND gitea_id IS NOT NULL AND gitea_login IS NOT NULL").Error + if err != nil { + return nil, err + } + return users, nil +} + func (db *DataBase) ListGroupUsers(groupName string) ([]*models.User, error) { var users []*models.User err := db.Find(&users, "repository IS NOT NULL AND group_name = ?", groupName).Order("created_at").Error @@ -155,6 +182,27 @@ func (db *DataBase) SetUserGitlabAccount(uid uint, user *models.GitlabUser) erro return nil } +func (db *DataBase) SetUserGiteaAccount(uid uint, user *models.GiteaUser) error { + res := db.Model(&models.User{}). + Where("id = ? AND (gitea_id IS NULL OR gitea_login IS NULL)", uid). + Updates(map[string]interface{}{ + "gitea_id": user.GiteaID, + "gitea_login": user.GiteaLogin, + }) + + if res.Error != nil { + if isUnqiueViolation(res.Error) { + return &DuplicateKey{res.Error} + } + return res.Error + } + + if res.RowsAffected < 1 { + return fmt.Errorf("unknown user %d", uid) + } + return nil +} + func (db *DataBase) SetUserRepository(user *models.User) error { res := db.Model(user).Update("repository", user.Repository) if res.Error != nil { @@ -262,7 +310,7 @@ func (db *DataBase) CreateFlag(task string) (*models.Flag, error) { return flag, nil } -func (db *DataBase) SubmitFlag(id, gitlabLogin string) error { +func (db *DataBase) SubmitFlagGitlab(id, gitlabLogin string) error { result := db.Model(&models.Flag{}).Where("id = ? AND gitlab_login IS NULL", id).Update("gitlab_login", gitlabLogin) if errors.Is(result.Error, gorm.ErrRecordNotFound) { return fmt.Errorf("unknown flag") @@ -276,7 +324,21 @@ func (db *DataBase) SubmitFlag(id, gitlabLogin string) error { return nil } -func (db *DataBase) ListUserFlags(gitlabLogin string) (flags []models.Flag, err error) { +func (db *DataBase) SubmitFlagGitea(id, giteaLogin string) error { + result := db.Model(&models.Flag{}).Where("id = ? AND gitea_login IS NULL", id).Update("gitea_login", giteaLogin) + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return fmt.Errorf("unknown flag") + } + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return fmt.Errorf("unknown flag") + } + return nil +} + +func (db *DataBase) ListUserFlagsGitlab(gitlabLogin string) (flags []models.Flag, err error) { flags = make([]models.Flag, 0) err = db.Find(&flags, "gitlab_login = ?", gitlabLogin).Error if err != nil { @@ -285,16 +347,25 @@ func (db *DataBase) ListUserFlags(gitlabLogin string) (flags []models.Flag, err return } +func (db *DataBase) ListUserFlagsGitea(giteaLogin string) (flags []models.Flag, err error) { + flags = make([]models.Flag, 0) + err = db.Find(&flags, "gitea_login = ?", giteaLogin).Error + if err != nil { + flags = nil + } + return +} + func (db *DataBase) ListSubmittedFlags() (flags []models.Flag, err error) { flags = make([]models.Flag, 0) - err = db.Find(&flags, "gitlab_login IS NOT NULL").Error + err = db.Find(&flags, "gitlab_login IS NOT NULL OR gitea_login IS NOT NULL").Error if err != nil { flags = nil } return } -func (db *DataBase) ListUserOverrides(login string) (overrides []models.OverriddenScore, err error) { +func (db *DataBase) ListUserOverridesGitlab(login string) (overrides []models.OverriddenScore, err error) { overrides = make([]models.OverriddenScore, 0) err = db.Find(&overrides, "gitlab_login = ?", login).Error if err != nil { @@ -303,6 +374,15 @@ func (db *DataBase) ListUserOverrides(login string) (overrides []models.Overridd return } +func (db *DataBase) ListUserOverridesGitea(login string) (overrides []models.OverriddenScore, err error) { + overrides = make([]models.OverriddenScore, 0) + err = db.Find(&overrides, "gitea_login = ?", login).Error + if err != nil { + overrides = nil + } + return +} + func (db *DataBase) ListOverrides() (overrides []models.OverriddenScore, err error) { overrides = make([]models.OverriddenScore, 0) err = db.Find(&overrides).Error @@ -312,7 +392,7 @@ func (db *DataBase) ListOverrides() (overrides []models.OverriddenScore, err err return } -func (db *DataBase) AddOverride(gitlabLogin, task string, score int, status models.PipelineStatus) error { +func (db *DataBase) AddOverrideGitlab(gitlabLogin, task string, score int, status models.PipelineStatus) error { overridenScore := &models.OverriddenScore{ GitlabLogin: gitlabLogin, Task: task, @@ -325,9 +405,29 @@ func (db *DataBase) AddOverride(gitlabLogin, task string, score int, status mode }).Create(overridenScore).Error } -func (db *DataBase) RemoveOverride(gitlabLogin, task string) error { +func (db *DataBase) AddOverrideGitea(giteaLogin, task string, score int, status models.PipelineStatus) error { + overridenScore := &models.OverriddenScore{ + GiteaLogin: giteaLogin, + Task: task, + Score: score, + Status: status, + } + return db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "gitea_login"}, {Name: "task"}}, + DoUpdates: clause.AssignmentColumns([]string{"score", "status"}), + }).Create(overridenScore).Error +} + +func (db *DataBase) RemoveOverrideGitlab(gitlabLogin, task string) error { return db. Where("gitlab_login = ? AND task = ?", gitlabLogin, task). Delete(models.OverriddenScore{}). Error } + +func (db *DataBase) RemoveOverrideGitea(giteaLogin, task string) error { + return db. + Where("gitea_login = ? AND task = ?", giteaLogin, task). + Delete(models.OverriddenScore{}). + Error +} diff --git a/internal/database/database_proxy.go b/internal/database/database_proxy.go new file mode 100644 index 0000000..3200902 --- /dev/null +++ b/internal/database/database_proxy.go @@ -0,0 +1,111 @@ +package database + +import ( + "github.com/bigredeye/notmanytask/internal/config" + "github.com/bigredeye/notmanytask/internal/models" + "github.com/pkg/errors" +) + +type DataBaseProxy struct { + *DataBase + Conf *config.Config +} + +func (db *DataBaseProxy) UserID(user *models.User) int { + switch db.Conf.Platform.Mode { + case config.GitlabMode: + return *user.GitlabID + case config.GiteaMode: + return int(*user.GiteaID) + default: + return -1 + } +} + +func (db *DataBaseProxy) UserLogin(user *models.User) *string { + switch db.Conf.Platform.Mode { + case config.GitlabMode: + return user.GitlabLogin + case config.GiteaMode: + return user.GiteaLogin + default: + return nil + } +} + +func (db *DataBaseProxy) FindUserByLogin(login string) (*models.User, error) { + switch db.Conf.Platform.Mode { + case config.GitlabMode: + return db.FindUserByGitlabLogin(login) + case config.GiteaMode: + return db.FindUserByGiteaLogin(login) + default: + return nil, errors.New("Unknown platform mode") + } +} + +func (db *DataBaseProxy) FindUserByID(id int) (*models.User, error) { + switch db.Conf.Platform.Mode { + case config.GitlabMode: + return db.FindUserByGitlabID(id) + case config.GiteaMode: + return db.FindUserByGiteaID(int64(id)) + default: + return nil, errors.New("Unknown platform mode") + } +} + +func (db *DataBaseProxy) ListUsersWithoutRepos() ([]*models.User, error) { + switch db.Conf.Platform.Mode { + case config.GitlabMode: + return db.ListGitlabUsersWithoutRepos() + case config.GiteaMode: + return db.ListGiteaUsersWithoutRepos() + default: + return nil, errors.New("Unknown platform mode") + } +} + +func (db *DataBaseProxy) ListUserFlags(login string) (flags []models.Flag, err error) { + switch db.Conf.Platform.Mode { + case config.GitlabMode: + return db.ListUserFlagsGitlab(login) + case config.GiteaMode: + return db.ListUserFlagsGitea(login) + default: + return nil, errors.New("Unknown platform mode") + } +} + +func (db *DataBaseProxy) ListUserOverrides(login string) (overrides []models.OverriddenScore, err error) { + switch db.Conf.Platform.Mode { + case config.GitlabMode: + return db.ListUserOverridesGitlab(login) + case config.GiteaMode: + return db.ListUserOverridesGitea(login) + default: + return nil, errors.New("Unknown platform mode") + } +} + +func (db *DataBaseProxy) AddOverride(login, task string, score int, status models.PipelineStatus) error { + switch db.Conf.Platform.Mode { + case config.GitlabMode: + return db.AddOverrideGitlab(login, task, score, status) + case config.GiteaMode: + return db.AddOverrideGitea(login, task, score, status) + default: + return nil + } +} + +func (db *DataBaseProxy) RemoveOverride(login, task string) error { + switch db.Conf.Platform.Mode { + case config.GitlabMode: + return db.RemoveOverrideGitlab(login, task) + case config.GiteaMode: + return db.RemoveOverrideGitea(login, task) + default: + return nil + } +} diff --git a/internal/gitlab/pipelines.go b/internal/gitlab/pipelines.go deleted file mode 100644 index 3561559..0000000 --- a/internal/gitlab/pipelines.go +++ /dev/null @@ -1,205 +0,0 @@ -package gitlab - -import ( - "context" - "strings" - "sync" - "time" - - "github.com/pkg/errors" - "github.com/xanzy/go-gitlab" - "go.uber.org/zap" - - "github.com/bigredeye/notmanytask/internal/database" - lf "github.com/bigredeye/notmanytask/internal/logfield" - "github.com/bigredeye/notmanytask/internal/models" -) - -type PipelinesFetcher struct { - *Client - - logger *zap.Logger - db *database.DataBase - - fresh sync.Map -} - -func NewPipelinesFetcher(client *Client, db *database.DataBase) (*PipelinesFetcher, error) { - return &PipelinesFetcher{ - Client: client, - logger: client.logger.Named("pipelines"), - db: db, - }, nil -} - -func (p *PipelinesFetcher) Run(ctx context.Context) { - interval := p.config.PullIntervals.Pipelines - if interval == nil { - return - } - - tick := time.NewTicker(*interval) - - for { - select { - case <-tick.C: - p.fetchAllPipelines() - case <-ctx.Done(): - p.logger.Info("Stopping pipelines fetcher") - return - } - } -} - -func (p *PipelinesFetcher) RunFresh(ctx context.Context) { - tick := time.NewTicker(time.Second) - - for { - select { - case <-tick.C: - p.fetchFreshPipelines() - case <-ctx.Done(): - p.logger.Info("Stopping fresh pipelines fetcher") - return - } - } -} - -type qualifiedPipelineID struct { - project string - id int -} - -func (p *PipelinesFetcher) AddFresh(id int, project string) error { - p.logger.Info("Added fresh pipeline", lf.ProjectName(project), lf.PipelineID(id)) - p.fresh.Store(&qualifiedPipelineID{project, id}, true) - return nil -} - -func (p *PipelinesFetcher) fetch(id int, project string) (*gitlab.PipelineInfo, error) { - log := p.logger.With( - lf.PipelineID(id), - lf.ProjectName(project), - ) - - log.Debug("Fetching pipeline") - - pipeline, _, err := p.gitlab.Pipelines.GetPipeline(p.Client.MakeProjectWithNamespace(project), id) - if err != nil { - log.Error("Failed to fetch pipeline", zap.Error(err)) - return nil, errors.Wrap(err, "Failed to fetch pipeline") - } - - info := &gitlab.PipelineInfo{ - ID: pipeline.ID, - Ref: pipeline.Ref, - Status: pipeline.Status, - CreatedAt: pipeline.CreatedAt, - ProjectID: pipeline.ProjectID, - } - return info, p.addPipeline(project, info) -} - -func (p *PipelinesFetcher) addPipeline(projectName string, pipeline *gitlab.PipelineInfo) error { - return p.db.AddPipeline(&models.Pipeline{ - ID: pipeline.ID, - Task: ParseTaskFromBranch(pipeline.Ref), - Status: pipeline.Status, - Project: projectName, - StartedAt: *pipeline.CreatedAt, - }) -} - -func (p *PipelinesFetcher) fetchAllPipelines() { - p.logger.Debug("Start pipelines fetcher iteration") - defer p.logger.Debug("Finish pipelines fetcher iteration") - - err := p.forEachProject(func(project *gitlab.Project) error { - p.logger.Debug("Found project", lf.ProjectName(project.Name)) - options := &gitlab.ListProjectPipelinesOptions{} - for { - pipelines, resp, err := p.gitlab.Pipelines.ListProjectPipelines(project.ID, options) - if err != nil { - p.logger.Error("Failed to list projects", zap.Error(err)) - return err - } - - for _, pipeline := range pipelines { - p.logger.Debug("Found pipeline", lf.ProjectName(project.Name), lf.PipelineID(pipeline.ID), lf.PipelineStatus(pipeline.Status)) - if err = p.addPipeline(project.Name, pipeline); err != nil { - p.logger.Error("Failed to add pipeline", zap.Error(err), lf.ProjectName(project.Name), lf.PipelineID(pipeline.ID)) - } - } - - if resp.CurrentPage >= resp.TotalPages { - break - } - options.Page = resp.NextPage - } - - return nil - }) - - if err == nil { - p.logger.Debug("Successfully fetched pipelines") - } else { - p.logger.Error("Failed to fetch pipelines", zap.Error(err)) - } -} - -func (p *PipelinesFetcher) forEachProject(callback func(project *gitlab.Project) error) error { - options := gitlab.ListGroupProjectsOptions{} - - for { - projects, resp, err := p.gitlab.Groups.ListGroupProjects(p.config.GitLab.Group.ID, &options) - if err != nil { - p.logger.Error("Failed to list projects", zap.Error(err)) - return err - } - - for _, project := range projects { - if err = callback(project); err != nil { - p.logger.Error("Project callback failed", zap.Error(err)) - return err - } - } - - if resp.CurrentPage >= resp.TotalPages { - break - } - options.Page = resp.NextPage - } - - return nil -} - -func (p *PipelinesFetcher) fetchFreshPipelines() { - removed := make([]interface{}, 0) - p.fresh.Range(func(key, _ interface{}) bool { - id := key.(*qualifiedPipelineID) - info, err := p.fetch(id.id, id.project) - if err != nil { - p.logger.Error("Failed to fetch pipeline", zap.Error(err)) - } else if info.Status != models.PipelineStatusRunning { - p.logger.Info("Fetched fresh pipeline", lf.ProjectName(id.project), lf.PipelineID(id.id), lf.PipelineStatus(info.Status)) - removed = append(removed, id) - } - return true - }) - - for _, id := range removed { - p.fresh.Delete(id) - } -} - -const ( - branchPrefix = "submits/" -) - -func ParseTaskFromBranch(task string) string { - return strings.TrimPrefix(task, branchPrefix) -} - -func MakeBranchForTask(task string) string { - return branchPrefix + task -} diff --git a/internal/logfield/logfield.go b/internal/logfield/logfield.go index e5a853c..d8ef48a 100644 --- a/internal/logfield/logfield.go +++ b/internal/logfield/logfield.go @@ -7,6 +7,8 @@ const ( FieldUserID = "user_id" FieldGitlabID = "gitlab_id" FieldGitlabLogin = "gitlab_login" + FieldGiteaID = "gitea_id" + FieldGiteaLogin = "gitea_login" FieldProjectName = "project_name" FieldProjectID = "project_id" FieldPipelineID = "pipeline_id" @@ -29,6 +31,14 @@ func GitlabLogin(login string) zap.Field { return zap.String(FieldGitlabLogin, login) } +func GiteaID(id int64) zap.Field { + return zap.Int64(FieldGiteaID, id) +} + +func GiteaLogin(login string) zap.Field { + return zap.String(FieldGiteaLogin, login) +} + func ProjectName(name string) zap.Field { return zap.String(FieldProjectName, name) } diff --git a/internal/models/flag.go b/internal/models/flag.go index d6d1b5c..6883ae9 100644 --- a/internal/models/flag.go +++ b/internal/models/flag.go @@ -6,5 +6,6 @@ type Flag struct { ID string `gorm:"primaryKey"` Task string `gorm:"index"` GitlabLogin *string `gorm:"index"` + GiteaLogin *string `gorm:"index"` CreatedAt time.Time } diff --git a/internal/models/overrides.go b/internal/models/overrides.go index 7615c9b..092cf74 100644 --- a/internal/models/overrides.go +++ b/internal/models/overrides.go @@ -8,6 +8,7 @@ type OverriddenScore struct { gorm.Model GitlabLogin string `gorm:"uniqueIndex:idx_overrides"` + GiteaLogin string `gorm:"uniqueIndex:idx_overrides"` Task string `gorm:"uniqueIndex:idx_overrides"` Score int diff --git a/internal/models/user.go b/internal/models/user.go index c216f8a..0e22b96 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -7,13 +7,19 @@ import ( type GitlabUser struct { GitlabID *int `gorm:"uniqueIndex"` GitlabLogin *string `gorm:"uniqueIndex"` - Repository *string +} + +type GiteaUser struct { + GiteaID *int64 `gorm:"uniqueIndex"` + GiteaLogin *string `gorm:"uniqueIndex"` } type User struct { gorm.Model GitlabUser + GiteaUser + Repository *string FirstName string `gorm:"uniqueIndex:idx_name"` LastName string `gorm:"uniqueIndex:idx_name"` diff --git a/internal/platform/base/base_client.go b/internal/platform/base/base_client.go new file mode 100644 index 0000000..2c26fe8 --- /dev/null +++ b/internal/platform/base/base_client.go @@ -0,0 +1,56 @@ +package base + +import ( + "fmt" + "strings" + + "github.com/alexsergivan/transliterator" + "go.uber.org/zap" + + "github.com/bigredeye/notmanytask/internal/config" + "github.com/bigredeye/notmanytask/internal/models" +) + +func Main() { + fmt.Println("vim-go") +} + +type ClientInterface interface { + InitializeProject(user *models.User) error + CleanupName(name string) string + CleanupLogin(login string) string + MakeProjectName(user *models.User) string + MakeProjectURL(user *models.User) string + MakeProjectSubmitsURL(user *models.User) string + MakeProjectWithNamespace(project string) string + MakePipelineURL(user *models.User, pipeline *models.Pipeline) string + MakeBranchURL(user *models.User, pipeline *models.Pipeline) string + MakeTaskURL(task string) string +} + +type ClientBase struct { + Config *config.Config + Logger *zap.Logger + Translit *transliterator.Transliterator +} + +const ( + Master = "master" +) + +func (c *ClientBase) CleanupName(name string) string { + transliteratedName := c.Translit.Transliterate(name, "en") + return strings.Map(func(ch rune) rune { + switch ch { + case '-': + return -1 + case '\'': + return -1 + } + return ch + }, transliteratedName) +} + +func (c *ClientBase) CleanupLogin(login string) string { + return strings.ReplaceAll(login, "__", "") +} diff --git a/internal/platform/base/base_pipelines.go b/internal/platform/base/base_pipelines.go new file mode 100644 index 0000000..ca33e87 --- /dev/null +++ b/internal/platform/base/base_pipelines.go @@ -0,0 +1,55 @@ +package base + +import ( + "context" + "strings" + "sync" + + "github.com/bigredeye/notmanytask/internal/database" + lf "github.com/bigredeye/notmanytask/internal/logfield" + "go.uber.org/zap" +) + +type PipelinesFetcherBase struct { + Logger *zap.Logger + Db *database.DataBase + + Fresh sync.Map +} + +type PipelineInfo interface{} +type Project interface{} + +type PipelinesFetcherInterface interface { + Run(ctx context.Context) + AddFresh(id int, project string) error + RunFresh(ctx context.Context) + AddPipeline(projectName string, pipeline PipelineInfo) error + Fetch(id int, project string) (PipelineInfo, error) + FetchAllPipelines() + FetchFreshPipelines() + ForEachProject(callback func(project Project) error) error +} + +type QualifiedPipelineID struct { + Project string + Id int +} + +func (p *PipelinesFetcherBase) AddFresh(id int, project string) error { + p.Logger.Info("Added fresh pipeline", lf.ProjectName(project), lf.PipelineID(id)) + p.Fresh.Store(&QualifiedPipelineID{project, id}, true) + return nil +} + +const ( + BranchPrefix = "submits/" +) + +func ParseTaskFromBranch(task string) string { + return strings.TrimPrefix(task, BranchPrefix) +} + +func MakeBranchForTask(task string) string { + return BranchPrefix + task +} diff --git a/internal/platform/base/base_projects.go b/internal/platform/base/base_projects.go new file mode 100644 index 0000000..06aad42 --- /dev/null +++ b/internal/platform/base/base_projects.go @@ -0,0 +1,26 @@ +package base + +import ( + "context" + + "github.com/bigredeye/notmanytask/internal/database" + "github.com/bigredeye/notmanytask/internal/models" + "go.uber.org/zap" +) + +type ProjectsMakerBase struct { + Logger *zap.Logger + Db *database.DataBaseProxy + Users chan *models.User +} + +type ProjectsMakerInterface interface { + AsyncPrepareProject(user *models.User) + Run(ctx context.Context) + InitializeMissingProjects() + MaybeInitializeProject(user *models.User) bool +} + +func (p ProjectsMakerBase) AsyncPrepareProject(user *models.User) { + p.Users <- user +} diff --git a/internal/platform/client_fabric.go b/internal/platform/client_fabric.go new file mode 100644 index 0000000..f8a8da5 --- /dev/null +++ b/internal/platform/client_fabric.go @@ -0,0 +1,49 @@ +package platform + +import ( + "fmt" + + gitea "code.gitea.io/sdk/gitea" + "github.com/alexsergivan/transliterator" + "github.com/bigredeye/notmanytask/internal/config" + "github.com/bigredeye/notmanytask/internal/platform/base" + gitea_client "github.com/bigredeye/notmanytask/internal/platform/gitea" + gitlab_client "github.com/bigredeye/notmanytask/internal/platform/gitlab" + "github.com/pkg/errors" + gitlab "github.com/xanzy/go-gitlab" + + "go.uber.org/zap" +) + +func NewClient(conf *config.Config, logger *zap.Logger) (base.ClientInterface, error) { + switch conf.Platform.Mode { + case "gitlab": + client, err := gitlab.NewClient(conf.Platform.GitLab.Api.Token, gitlab.WithBaseURL(conf.Platform.GitLab.BaseURL)) + if err != nil { + return nil, errors.Wrap(err, "Failed to create gitlab client") + } + return &gitlab_client.ClientGitlab{ + ClientBase: &base.ClientBase{ + Config: conf, + Logger: logger, + Translit: transliterator.NewTransliterator(nil), + }, + Gitlab: client, + }, nil + case "gitea": + client, err := gitea.NewClient(conf.Platform.GitLab.BaseURL, gitea.SetToken(conf.Platform.Gitea.Api.Token)) + if err != nil { + return nil, errors.Wrap(err, "Failed to create gitea client") + } + return &gitea_client.ClientGitea{ + ClientBase: &base.ClientBase{ + Config: conf, + Logger: logger, + Translit: transliterator.NewTransliterator(nil), + }, + Gitea: client, + }, nil + default: + return nil, errors.Wrap(errors.Errorf("Unknown platform mode: %s", conf.Platform.Mode), fmt.Sprintf("Failed to create client for platform %s", conf.Platform.Mode)) + } +} diff --git a/internal/platform/gitea/gitea_client.go b/internal/platform/gitea/gitea_client.go new file mode 100644 index 0000000..0a38b96 --- /dev/null +++ b/internal/platform/gitea/gitea_client.go @@ -0,0 +1,157 @@ +package gitea + +import ( + "fmt" + "net/http" + + "code.gitea.io/sdk/gitea" + "github.com/pkg/errors" + "go.uber.org/zap" + + lf "github.com/bigredeye/notmanytask/internal/logfield" + "github.com/bigredeye/notmanytask/internal/models" + "github.com/bigredeye/notmanytask/internal/platform/base" +) + +type ClientGitea struct { + *base.ClientBase + Gitea *gitea.Client +} + +func (c *ClientGitea) InitializeProject(user *models.User) error { + if user.GiteaID == nil || user.GiteaLogin == nil { + c.Logger.Error("Empty gitea user", zap.Uint("uid", user.ID)) + return errors.New("Empty gitea user") + } + + log := c.Logger.With(zap.Stringp("gitea_login", user.GiteaLogin), zap.Int64p("gitea_id", user.GiteaID), zap.Uint("user_id", user.ID)) + log.Info("Going to initialize project") + + projectName := c.MakeProjectName(user) + log = log.With(lf.ProjectName(projectName)) + + // Try to find existing repository + repo, resp, err := c.Gitea.GetRepo(c.Config.Platform.Gitea.Organization.Name, projectName) + if err != nil && resp == nil { + log.Error("Failed to get repository", zap.String("escaped_project", fmt.Sprintf("%s/%s", c.Config.Platform.Gitea.Organization.Name, projectName)), zap.Error(err)) + return errors.Wrap(err, "Failed to get repository") + } else if resp.StatusCode == http.StatusNotFound { + log.Info("Repository was not found", zap.String("escaped_project", fmt.Sprintf("%s/%s", c.Config.Platform.Gitea.Organization.Name, projectName))) + // Create repository + + opts := gitea.CreateRepoOption{ + Name: projectName, + Description: fmt.Sprintf("%s private repository", *user.GiteaLogin), + DefaultBranch: base.Master, + Private: true, + AutoInit: true, + Readme: c.Config.Platform.Gitea.DefaultReadme, + } + repo, _, err = c.Gitea.CreateOrgRepo(c.Config.Platform.Gitea.Organization.Name, opts) + if err != nil { + log.Error("Failed to create repository", zap.Error(err)) + return errors.Wrap(err, "Failed to create repository") + } + log = log.With(zap.Int64("repository_id", repo.ID)) + log.Info("Created repository") + } else if err != nil { + log.Error("Failed to find repository", zap.Error(err)) + return errors.Wrap(err, "Failed to find repository") + } else { + log = log.With(zap.Int64("repository_id", repo.ID)) + log.Info("Found existing repository") + } + + // Protect master branch from unintended commits + + branch_protection_opts := gitea.CreateBranchProtectionOption{ + BranchName: base.Master, + EnablePush: false, + EnableMergeWhitelist: false, + } + _, _, err = c.Gitea.CreateBranchProtection(c.Config.Platform.Gitea.Organization.Name, repo.Name, branch_protection_opts) + if err != nil { + log.Error("Failed to protect master branch", zap.Error(err)) + return errors.Wrap(err, "Failed to protect master branch") + } + log.Info("Protected master branch") + + // Check if user is already in project + foundUser := false + options := gitea.ListCollaboratorsOptions{} + for { + members, resp, err := c.Gitea.ListCollaborators(c.Config.Platform.Gitea.Organization.Name, repo.Name, options) + if err != nil { + log.Error("Failed to list repository members", zap.Error(err)) + return errors.Wrap(err, "Failed to list repository members") + } + + for _, member := range members { + if member.ID == *user.GiteaID { + foundUser = true + break + } + } + + if foundUser { + break + } + + if resp.FirstPage >= resp.LastPage { + break + } + options.Page = resp.NextPage + } + + if foundUser { + log.Info("User is already in the repository") + } else { + // Add our dear user to the project + permission := gitea.AccessModeWrite + col_opts := gitea.AddCollaboratorOption{ + Permission: &permission, + } + _, err = c.Gitea.AddCollaborator(c.Config.Platform.Gitea.Organization.Name, repo.Name, *user.GiteaLogin, col_opts) + if err != nil { + log.Error("Failed to add user to the repository", zap.Error(err)) + return errors.Wrap(err, "Failed to add user to the repository") + } + log.Info("Added user to the repository") + } + + return nil +} + +func (c *ClientGitea) MakeProjectName(user *models.User) string { + return fmt.Sprintf("%s-%s-%s-%s", user.GroupName, c.CleanupName(user.FirstName), c.CleanupName(user.LastName), c.CleanupLogin(*user.GiteaLogin)) +} + +func (c *ClientGitea) MakeProjectURL(user *models.User) string { + name := c.MakeProjectName(user) + return fmt.Sprintf("%s/%s/%s", c.Config.Platform.Gitea.BaseURL, c.Config.Platform.Gitea.Organization.Name, name) +} + +func (c *ClientGitea) MakeProjectSubmitsURL(user *models.User) string { + url := c.MakeProjectURL(user) + return fmt.Sprintf("%s/actions", url) +} + +func (c *ClientGitea) MakeProjectWithNamespace(project string) string { + return fmt.Sprintf("%s/%s", c.Config.Platform.Gitea.Organization.Name, project) +} + +func (c *ClientGitea) MakePipelineURL(user *models.User, pipeline *models.Pipeline) string { + name := c.MakeProjectName(user) + return fmt.Sprintf("%s/%s/%s/actions/runs/%d", c.Config.Platform.Gitea.BaseURL, c.Config.Platform.Gitea.Organization.Name, name, pipeline.ID) +} + +func (c *ClientGitea) MakeBranchURL(user *models.User, pipeline *models.Pipeline) string { + name := c.MakeProjectName(user) + return fmt.Sprintf("%s/%s/%s/src/branch/%s", c.Config.Platform.Gitea.BaseURL, c.Config.Platform.Gitea.Organization.Name, name, pipeline.Task) +} + +func (c *ClientGitea) MakeTaskURL(task string) string { + return fmt.Sprintf("%s/%s", c.Config.Platform.Gitea.TaskUrlPrefix, task) +} + +var _ base.ClientInterface = &ClientGitea{} diff --git a/internal/platform/gitea/gitea_pipelines.go b/internal/platform/gitea/gitea_pipelines.go new file mode 100644 index 0000000..5983acd --- /dev/null +++ b/internal/platform/gitea/gitea_pipelines.go @@ -0,0 +1,243 @@ +package gitea + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "code.gitea.io/sdk/gitea" + "github.com/pkg/errors" + "github.com/xanzy/go-gitlab" + "go.uber.org/zap" + + lf "github.com/bigredeye/notmanytask/internal/logfield" + "github.com/bigredeye/notmanytask/internal/models" + "github.com/bigredeye/notmanytask/internal/platform/base" +) + +type PipelinesInfoResponse struct { + TotalCount int64 `json:"total_count"` + WorkflowRuns []PipelineInfoGitea `json:"workflow_runs"` +} + +type PipelineInfoGitea struct { + ID int `json:"id"` + Name string `json:"name"` + HeadBranch string `json:"head_branch"` + HeadSHA string `json:"head_sha"` + RunNumber int `json:"run_number"` + Event string `json:"event"` + DisplayTitle string `json:"display_title"` + Status string `json:"status"` + WorkflowID string `json:"workflow_id"` + URL string `json:"url"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + RunStartedAt *time.Time `json:"run_started_at"` +} + +type PipelinesFetcherGitea struct { + *base.PipelinesFetcherBase + *ClientGitea +} + +func (p *PipelinesFetcherGitea) Run(ctx context.Context) { + interval := p.Config.PullIntervals.Pipelines + if interval == nil { + return + } + + tick := time.NewTicker(*interval) + + for { + select { + case <-tick.C: + p.FetchAllPipelines() + case <-ctx.Done(): + p.Logger.Info("Stopping pipelines fetcher") + return + } + } +} + +func (p *PipelinesFetcherGitea) RunFresh(ctx context.Context) { + tick := time.NewTicker(time.Second) + + for { + select { + case <-tick.C: + p.FetchFreshPipelines() + case <-ctx.Done(): + p.Logger.Info("Stopping fresh pipelines fetcher") + return + } + } +} + +func (p *PipelinesFetcherGitea) AddPipeline(projectName string, pipeline base.PipelineInfo) error { + pipeline_gitea, cast := pipeline.(*PipelineInfoGitea) + if !cast { + return errors.New("Failed to cast pipeline to gitlab pipeline") + } + return p.Db.AddPipeline(&models.Pipeline{ + ID: pipeline_gitea.ID, + Task: base.ParseTaskFromBranch(pipeline_gitea.HeadBranch), + Status: pipeline_gitea.Status, + Project: projectName, + StartedAt: *pipeline_gitea.CreatedAt, + }) +} + +// So dirty hack FIXME(shaprunovk) +// But there is no analogue of https://try.gitea.io/api/swagger#/repository/ListActionTasks in gitea go-sdk yet +func (p *PipelinesFetcherGitea) GetPipelines(project string) ([]PipelineInfoGitea, error) { + apiURL := fmt.Sprintf("https://gitea.com/api/v1/repos/%s/%s/actions/tasks", p.Config.Platform.Gitea.Organization.Name, project) + + req, err := http.NewRequest("GET", apiURL, nil) + if err != nil { + p.Logger.Error("Failed to request pipelines via API", zap.Error(err)) + return nil, errors.Wrap(err, "Failed to make pipelines fetch request itself") + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "token "+p.Config.Platform.Gitea.Api.Token) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + p.Logger.Error("Failed to request pipelines via API", zap.Error(err)) + return nil, errors.Wrap(err, "Failed to request pipelines via API") + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + var response PipelinesInfoResponse + if err := json.Unmarshal(body, &response); err != nil { + p.Logger.Error("Failed to unmarshal response", zap.Error(err)) + return nil, errors.Wrap(err, "Failed to unmarshal response") + } + + p.Logger.Info(fmt.Sprintf("Total Workflow Runs: %d\n", response.TotalCount), lf.ProjectName(project)) + return response.WorkflowRuns, nil +} + +func (p *PipelinesFetcherGitea) Fetch(id int, project string) (base.PipelineInfo, error) { + log := p.Logger.With( + lf.PipelineID(id), + lf.ProjectName(project), + ) + log.Debug("Fetching pipeline") + + pipelines, err := p.GetPipelines(project) + if err != nil { + log.Error("Failed to fetch pipeline", zap.Error(err)) + return nil, errors.Wrap(err, "Failed to fetch pipeline") + } + + found := false + var pipelineInfo PipelineInfoGitea + for _, pipeline := range pipelines { + if pipeline.ID == id { + found = true + pipelineInfo = pipeline + break + } + } + if !found { + log.Error("Failed to fetch pipeline", zap.Error(err)) + return nil, errors.Wrap(err, fmt.Sprintf("Failed to fetch pipeline with id:%d", id)) + } + info := &PipelineInfoGitea{ + ID: pipelineInfo.ID, + HeadBranch: pipelineInfo.HeadBranch, + Status: pipelineInfo.Status, + CreatedAt: pipelineInfo.CreatedAt, + } + return info, p.AddPipeline(project, info) +} + +func (p *PipelinesFetcherGitea) FetchAllPipelines() { + p.Logger.Debug("Start pipelines fetcher iteration") + defer p.Logger.Debug("Finish pipelines fetcher iteration") + + err := p.ForEachProject(func(project base.Project) error { + project_gitea, cast := project.(*gitea.Repository) + if !cast { + return errors.New("Failed to cast project to gitlab project") + } + p.Logger.Debug("Found project", lf.ProjectName(project_gitea.Name)) + pipelines, err := p.GetPipelines(project_gitea.Name) + if err != nil { + p.Logger.Error("Failed to list projects", zap.Error(err)) + return err + } + + for _, pipeline := range pipelines { + p.Logger.Debug("Found pipeline", lf.ProjectName(project_gitea.Name), lf.PipelineID(pipeline.ID), lf.PipelineStatus(pipeline.Status)) + if err = p.AddPipeline(project_gitea.Name, &pipeline); err != nil { + p.Logger.Error("Failed to add pipeline", zap.Error(err), lf.ProjectName(project_gitea.Name), lf.PipelineID(pipeline.ID)) + } + } + + return nil + }) + + if err == nil { + p.Logger.Debug("Successfully fetched pipelines") + } else { + p.Logger.Error("Failed to fetch pipelines", zap.Error(err)) + } +} + +func (p *PipelinesFetcherGitea) ForEachProject(callback func(project base.Project) error) error { + options := gitlab.ListGroupProjectsOptions{} + + for { + opts := gitea.ListOrgReposOptions{} + projects, resp, err := p.Gitea.ListOrgRepos(p.Config.Platform.Gitea.Organization.Name, opts) + if err != nil { + p.Logger.Error("Failed to list projects", zap.Error(err)) + return err + } + + for _, project := range projects { + if err = callback(project); err != nil { + p.Logger.Error("Project callback failed", zap.Error(err)) + return err + } + } + + if resp.FirstPage >= resp.LastPage { + break + } + options.Page = resp.NextPage + } + + return nil +} + +func (p *PipelinesFetcherGitea) FetchFreshPipelines() { + removed := make([]interface{}, 0) + p.Fresh.Range(func(key, _ interface{}) bool { + id := key.(*base.QualifiedPipelineID) + info, err := p.Fetch(id.Id, id.Project) + info_gitea, cast := info.(*PipelineInfoGitea) + if !cast { + p.Logger.Error("Failed to cast pipeline info to gitlab pipeline info", lf.ProjectName(id.Project), lf.PipelineID(id.Id)) + } + if err != nil { + p.Logger.Error("Failed to fetch pipeline", zap.Error(err)) + } else if info_gitea.Status != models.PipelineStatusRunning { + p.Logger.Info("Fetched fresh pipeline", lf.ProjectName(id.Project), lf.PipelineID(id.Id), lf.PipelineStatus(info_gitea.Status)) + removed = append(removed, id) + } + return true + }) + + for _, id := range removed { + p.Fresh.Delete(id) + } +} diff --git a/internal/platform/gitea/gitea_projects.go b/internal/platform/gitea/gitea_projects.go new file mode 100644 index 0000000..c203551 --- /dev/null +++ b/internal/platform/gitea/gitea_projects.go @@ -0,0 +1,97 @@ +package gitea + +import ( + "context" + "time" + + "github.com/bigredeye/notmanytask/internal/models" + "github.com/bigredeye/notmanytask/internal/platform/base" + + "go.uber.org/zap" +) + +type ProjectsMakerGitea struct { + *base.ProjectsMakerBase + *ClientGitea +} + +func (p ProjectsMakerGitea) Run(ctx context.Context) { + if p.Config.PullIntervals.Projects == nil { + return + } + + p.InitializeMissingProjects() + + tick := time.NewTimer(*p.Config.PullIntervals.Projects) + for { + select { + case user := <-p.Users: + p.Logger.Info("Got user from in-proc channel", + zap.Int64p("gitea_id", user.GiteaID), + zap.Stringp("gitea_login", user.GiteaLogin), + ) + if !p.MaybeInitializeProject(user) { + p.Users <- user + } + case <-tick.C: + p.InitializeMissingProjects() + case <-ctx.Done(): + p.Logger.Info("Stopping projects maker") + return + } + } +} + +func (p ProjectsMakerGitea) InitializeMissingProjects() { + p.Logger.Debug("Start projectsMaker iteration") + numProjectsInitialized := 0 + defer p.Logger.Debug("Finish projectsMaker iteration", zap.Int("num_projects_initialized", numProjectsInitialized)) + + users, err := p.Db.ListUsersWithoutRepos() + if err != nil { + p.Logger.Error("Failed to list users without repos", zap.Error(err)) + return + } + + for _, user := range users { + p.Logger.Info("Got user without repo from database", + zap.Int64p("gitea_id", user.GiteaID), + zap.Stringp("gitea_login", user.GiteaLogin), + ) + ok := p.MaybeInitializeProject(user) + if ok { + numProjectsInitialized++ + } + } +} + +func (p ProjectsMakerGitea) MaybeInitializeProject(user *models.User) bool { + log := p.Logger + if user.GitlabID == nil || user.GitlabLogin == nil { + log.Error("Trying to initialize repo for user without login, aborting", zap.Uint("user_id", user.ID)) + return false + } + + log = log.With(zap.Int64p("gitea_id", user.GiteaID), zap.Stringp("gitea_login", user.GiteaLogin)) + + err := p.InitializeProject(user) + if err != nil { + log.Error("Failed to initialize project", zap.Error(err)) + // TODO(BigRedEye): nice backoff + time.Sleep(time.Second * 1) + return false + } + + project := p.MakeProjectURL(user) + log = log.With(zap.String("project", project)) + + user.Repository = &project + err = p.Db.SetUserRepository(user) + if err != nil { + log.Error("Failed to set user repo", zap.Error(err)) + return false + } + + log.Info("Successfully set user repo") + return true +} diff --git a/internal/gitlab/gitlab.go b/internal/platform/gitlab/gitlab_client.go similarity index 50% rename from internal/gitlab/gitlab.go rename to internal/platform/gitlab/gitlab_client.go index 1913b6e..77753b7 100644 --- a/internal/gitlab/gitlab.go +++ b/internal/platform/gitlab/gitlab_client.go @@ -4,73 +4,48 @@ import ( goerrors "errors" "fmt" "net/http" - "strings" - "github.com/alexsergivan/transliterator" "github.com/pkg/errors" "github.com/xanzy/go-gitlab" "go.uber.org/zap" - "github.com/bigredeye/notmanytask/internal/config" lf "github.com/bigredeye/notmanytask/internal/logfield" "github.com/bigredeye/notmanytask/internal/models" + base "github.com/bigredeye/notmanytask/internal/platform/base" ) -func Main() { - fmt.Println("vim-go") +type ClientGitlab struct { + *base.ClientBase + Gitlab *gitlab.Client } -type Client struct { - config *config.Config - gitlab *gitlab.Client - logger *zap.Logger - translit *transliterator.Transliterator -} - -func NewClient(conf *config.Config, logger *zap.Logger) (*Client, error) { - client, err := gitlab.NewClient(conf.GitLab.Api.Token, gitlab.WithBaseURL(conf.GitLab.BaseURL)) - if err != nil { - return nil, errors.Wrap(err, "Failed to create gitlab client") - } - return &Client{ - config: conf, - gitlab: client, - logger: logger, - translit: transliterator.NewTransliterator(nil), - }, nil -} - -const ( - master = "master" -) - -func (c Client) InitializeProject(user *models.User) error { +func (c *ClientGitlab) InitializeProject(user *models.User) error { if user.GitlabID == nil || user.GitlabLogin == nil { - c.logger.Error("Empty gitlab user", zap.Uint("uid", user.ID)) + c.Logger.Error("Empty gitlab user", zap.Uint("uid", user.ID)) return errors.New("Empty gitlab user") } - log := c.logger.With(zap.Stringp("gitlab_login", user.GitlabLogin), zap.Intp("gitlab_id", user.GitlabID), zap.Uint("user_id", user.ID)) + log := c.Logger.With(zap.Stringp("gitlab_login", user.GitlabLogin), zap.Intp("gitlab_id", user.GitlabID), zap.Uint("user_id", user.ID)) log.Info("Going to initialize project") projectName := c.MakeProjectName(user) log = log.With(lf.ProjectName(projectName)) // Try to find existing project - project, resp, err := c.gitlab.Projects.GetProject(fmt.Sprintf("%s/%s", c.config.GitLab.Group.Name, projectName), &gitlab.GetProjectOptions{}) + project, resp, err := c.Gitlab.Projects.GetProject(fmt.Sprintf("%s/%s", c.Config.Platform.GitLab.Group.Name, projectName), &gitlab.GetProjectOptions{}) if err != nil && resp == nil { - log.Error("Failed to get project", zap.String("escaped_project", fmt.Sprintf("%s/%s", c.config.GitLab.Group.Name, projectName)), zap.Error(err)) + log.Error("Failed to get project", zap.String("escaped_project", fmt.Sprintf("%s/%s", c.Config.Platform.GitLab.Group.Name, projectName)), zap.Error(err)) return errors.Wrap(err, "Failed to get project") } else if resp.StatusCode == http.StatusNotFound { - log.Info("Project was not found", zap.String("escaped_project", fmt.Sprintf("%s/%s", c.config.GitLab.Group.Name, projectName))) + log.Info("Project was not found", zap.String("escaped_project", fmt.Sprintf("%s/%s", c.Config.Platform.GitLab.Group.Name, projectName))) // Create project - project, _, err = c.gitlab.Projects.CreateProject(&gitlab.CreateProjectOptions{ + project, _, err = c.Gitlab.Projects.CreateProject(&gitlab.CreateProjectOptions{ Name: &projectName, - NamespaceID: &c.config.GitLab.Group.ID, - DefaultBranch: gitlab.String(master), + NamespaceID: &c.Config.Platform.GitLab.Group.ID, + DefaultBranch: gitlab.String(base.Master), Visibility: gitlab.Visibility(gitlab.PrivateVisibility), SharedRunnersEnabled: gitlab.Bool(false), - CIConfigPath: &c.config.GitLab.CIConfigPath, + CIConfigPath: &c.Config.Platform.GitLab.CIConfigPath, }) if err != nil { log.Error("Failed to create project", zap.Error(err)) @@ -87,15 +62,15 @@ func (c Client) InitializeProject(user *models.User) error { } // Prepare README.md with basic info - _, _, err = c.gitlab.Commits.CreateCommit(project.ID, &gitlab.CreateCommitOptions{ - Branch: gitlab.String(master), + _, _, err = c.Gitlab.Commits.CreateCommit(project.ID, &gitlab.CreateCommitOptions{ + Branch: gitlab.String(base.Master), CommitMessage: gitlab.String("Initialize repo"), AuthorName: gitlab.String("notmanytask"), AuthorEmail: gitlab.String("mail@notmanytask.org"), Actions: []*gitlab.CommitActionOptions{{ Action: gitlab.FileAction(gitlab.FileCreate), FilePath: gitlab.String("README.md"), - Content: gitlab.String(c.config.GitLab.DefaultReadme), + Content: gitlab.String(c.Config.Platform.GitLab.DefaultReadme), }}, }) @@ -112,22 +87,22 @@ func (c Client) InitializeProject(user *models.User) error { log.Info("Created README") } - // Protect master branch from unintended commits - _, _, err = c.gitlab.Branches.ProtectBranch(project.ID, master, &gitlab.ProtectBranchOptions{ + // Protect base.Master branch from unintended commits + _, _, err = c.Gitlab.Branches.ProtectBranch(project.ID, base.Master, &gitlab.ProtectBranchOptions{ DevelopersCanPush: gitlab.Bool(false), DevelopersCanMerge: gitlab.Bool(false), }) if err != nil { - log.Error("Failed to protect master branch", zap.Error(err)) - return errors.Wrap(err, "Failed to protect master branch") + log.Error("Failed to protect base.Master branch", zap.Error(err)) + return errors.Wrap(err, "Failed to protect base.Master branch") } - log.Info("Protected master branch") + log.Info("Protected base.Master branch") // Check if user is alreay in project foundUser := false options := gitlab.ListProjectMembersOptions{} for { - members, resp, err := c.gitlab.ProjectMembers.ListAllProjectMembers(project.ID, &options) + members, resp, err := c.Gitlab.ProjectMembers.ListAllProjectMembers(project.ID, &options) if err != nil { log.Error("Failed to list project members", zap.Error(err)) return errors.Wrap(err, "Failed to list project members") @@ -154,7 +129,7 @@ func (c Client) InitializeProject(user *models.User) error { log.Info("User is already in the project") } else { // Add our dear user to the project - _, _, err = c.gitlab.ProjectMembers.AddProjectMember(project.ID, &gitlab.AddProjectMemberOptions{ + _, _, err = c.Gitlab.ProjectMembers.AddProjectMember(project.ID, &gitlab.AddProjectMemberOptions{ UserID: *user.GitlabID, AccessLevel: gitlab.AccessLevel(gitlab.DeveloperPermissions), }) @@ -168,51 +143,36 @@ func (c Client) InitializeProject(user *models.User) error { return nil } -func (c Client) cleanupName(name string) string { - transliteratedName := c.translit.Transliterate(name, "en") - return strings.Map(func(ch rune) rune { - switch ch { - case '-': - return -1 - case '\'': - return -1 - } - return ch - }, transliteratedName) -} - -func (c Client) cleanupLogin(login string) string { - return strings.ReplaceAll(login, "__", "") +func (c *ClientGitlab) MakeProjectName(user *models.User) string { + return fmt.Sprintf("%s-%s-%s-%s", user.GroupName, c.CleanupName(user.FirstName), c.CleanupName(user.LastName), c.CleanupLogin(*user.GitlabLogin)) } -func (c Client) MakeProjectName(user *models.User) string { - return fmt.Sprintf("%s-%s-%s-%s", user.GroupName, c.cleanupName(user.FirstName), c.cleanupName(user.LastName), c.cleanupLogin(*user.GitlabLogin)) -} - -func (c Client) MakeProjectURL(user *models.User) string { +func (c *ClientGitlab) MakeProjectURL(user *models.User) string { name := c.MakeProjectName(user) - return fmt.Sprintf("%s/%s/%s", c.config.GitLab.BaseURL, c.config.GitLab.Group.Name, name) + return fmt.Sprintf("%s/%s/%s", c.Config.Platform.GitLab.BaseURL, c.Config.Platform.GitLab.Group.Name, name) } -func (c Client) MakeProjectSubmitsURL(user *models.User) string { +func (c *ClientGitlab) MakeProjectSubmitsURL(user *models.User) string { url := c.MakeProjectURL(user) return fmt.Sprintf("%s/-/jobs", url) } -func (c Client) MakeProjectWithNamespace(project string) string { - return fmt.Sprintf("%s/%s", c.config.GitLab.Group.Name, project) +func (c *ClientGitlab) MakeProjectWithNamespace(project string) string { + return fmt.Sprintf("%s/%s", c.Config.Platform.GitLab.Group.Name, project) } -func (c Client) MakePipelineURL(user *models.User, pipeline *models.Pipeline) string { +func (c *ClientGitlab) MakePipelineURL(user *models.User, pipeline *models.Pipeline) string { name := c.MakeProjectName(user) - return fmt.Sprintf("%s/%s/%s/-/pipelines/%d", c.config.GitLab.BaseURL, c.config.GitLab.Group.Name, name, pipeline.ID) + return fmt.Sprintf("%s/%s/%s/-/pipelines/%d", c.Config.Platform.GitLab.BaseURL, c.Config.Platform.GitLab.Group.Name, name, pipeline.ID) } -func (c Client) MakeBranchURL(user *models.User, pipeline *models.Pipeline) string { +func (c *ClientGitlab) MakeBranchURL(user *models.User, pipeline *models.Pipeline) string { name := c.MakeProjectName(user) - return fmt.Sprintf("%s/%s/%s/-/tree/submits/%s", c.config.GitLab.BaseURL, c.config.GitLab.Group.Name, name, pipeline.Task) + return fmt.Sprintf("%s/%s/%s/-/tree/submits/%s", c.Config.Platform.GitLab.BaseURL, c.Config.Platform.GitLab.Group.Name, name, pipeline.Task) } -func (c Client) MakeTaskURL(task string) string { - return fmt.Sprintf("%s/%s", c.config.GitLab.TaskUrlPrefix, task) +func (c *ClientGitlab) MakeTaskURL(task string) string { + return fmt.Sprintf("%s/%s", c.Config.Platform.GitLab.TaskUrlPrefix, task) } + +var _ base.ClientInterface = &ClientGitlab{} diff --git a/internal/platform/gitlab/gitlab_pipelines.go b/internal/platform/gitlab/gitlab_pipelines.go new file mode 100644 index 0000000..63a9026 --- /dev/null +++ b/internal/platform/gitlab/gitlab_pipelines.go @@ -0,0 +1,180 @@ +package gitlab + +import ( + "context" + "time" + + "github.com/pkg/errors" + "github.com/xanzy/go-gitlab" + "go.uber.org/zap" + + lf "github.com/bigredeye/notmanytask/internal/logfield" + "github.com/bigredeye/notmanytask/internal/models" + "github.com/bigredeye/notmanytask/internal/platform/base" +) + +type PipelinesFetcherGitlab struct { + *base.PipelinesFetcherBase + *ClientGitlab +} + +func (p *PipelinesFetcherGitlab) Run(ctx context.Context) { + interval := p.Config.PullIntervals.Pipelines + if interval == nil { + return + } + + tick := time.NewTicker(*interval) + + for { + select { + case <-tick.C: + p.FetchAllPipelines() + case <-ctx.Done(): + p.Logger.Info("Stopping pipelines fetcher") + return + } + } +} + +func (p *PipelinesFetcherGitlab) RunFresh(ctx context.Context) { + tick := time.NewTicker(time.Second) + + for { + select { + case <-tick.C: + p.FetchFreshPipelines() + case <-ctx.Done(): + p.Logger.Info("Stopping fresh pipelines fetcher") + return + } + } +} + +func (p *PipelinesFetcherGitlab) AddPipeline(projectName string, pipeline base.PipelineInfo) error { + pipeline_gitlab, cast := pipeline.(*gitlab.PipelineInfo) + if !cast { + return errors.New("Failed to cast pipeline to gitlab pipeline") + } + return p.Db.AddPipeline(&models.Pipeline{ + ID: pipeline_gitlab.ID, + Task: base.ParseTaskFromBranch(pipeline_gitlab.Ref), + Status: pipeline_gitlab.Status, + Project: projectName, + StartedAt: *pipeline_gitlab.CreatedAt, + }) +} + +func (p *PipelinesFetcherGitlab) Fetch(id int, project string) (base.PipelineInfo, error) { + log := p.Logger.With( + lf.PipelineID(id), + lf.ProjectName(project), + ) + + log.Debug("Fetching pipeline") + + pipeline, _, err := p.Gitlab.Pipelines.GetPipeline(p.ClientGitlab.MakeProjectWithNamespace(project), id) + if err != nil { + log.Error("Failed to fetch pipeline", zap.Error(err)) + return nil, errors.Wrap(err, "Failed to fetch pipeline") + } + + info := &gitlab.PipelineInfo{ + ID: pipeline.ID, + Ref: pipeline.Ref, + Status: pipeline.Status, + CreatedAt: pipeline.CreatedAt, + ProjectID: pipeline.ProjectID, + } + return info, p.AddPipeline(project, info) +} + +func (p *PipelinesFetcherGitlab) FetchAllPipelines() { + p.Logger.Debug("Start pipelines fetcher iteration") + defer p.Logger.Debug("Finish pipelines fetcher iteration") + + err := p.ForEachProject(func(project base.Project) error { + project_gitlab, cast := project.(*gitlab.Project) + if !cast { + return errors.New("Failed to cast project to gitlab project") + } + p.Logger.Debug("Found project", lf.ProjectName(project_gitlab.Name)) + options := &gitlab.ListProjectPipelinesOptions{} + for { + pipelines, resp, err := p.Gitlab.Pipelines.ListProjectPipelines(project_gitlab.ID, options) + if err != nil { + p.Logger.Error("Failed to list projects", zap.Error(err)) + return err + } + + for _, pipeline := range pipelines { + p.Logger.Debug("Found pipeline", lf.ProjectName(project_gitlab.Name), lf.PipelineID(pipeline.ID), lf.PipelineStatus(pipeline.Status)) + if err = p.AddPipeline(project_gitlab.Name, pipeline); err != nil { + p.Logger.Error("Failed to add pipeline", zap.Error(err), lf.ProjectName(project_gitlab.Name), lf.PipelineID(pipeline.ID)) + } + } + + if resp.CurrentPage >= resp.TotalPages { + break + } + options.Page = resp.NextPage + } + + return nil + }) + + if err == nil { + p.Logger.Debug("Successfully fetched pipelines") + } else { + p.Logger.Error("Failed to fetch pipelines", zap.Error(err)) + } +} + +func (p *PipelinesFetcherGitlab) ForEachProject(callback func(project base.Project) error) error { + options := gitlab.ListGroupProjectsOptions{} + + for { + projects, resp, err := p.Gitlab.Groups.ListGroupProjects(p.Config.Platform.GitLab.Group.ID, &options) + if err != nil { + p.Logger.Error("Failed to list projects", zap.Error(err)) + return err + } + + for _, project := range projects { + if err = callback(project); err != nil { + p.Logger.Error("Project callback failed", zap.Error(err)) + return err + } + } + + if resp.CurrentPage >= resp.TotalPages { + break + } + options.Page = resp.NextPage + } + + return nil +} + +func (p *PipelinesFetcherGitlab) FetchFreshPipelines() { + removed := make([]interface{}, 0) + p.Fresh.Range(func(key, _ interface{}) bool { + id := key.(*base.QualifiedPipelineID) + info, err := p.Fetch(id.Id, id.Project) + info_gitlab, cast := info.(*gitlab.PipelineInfo) + if !cast { + p.Logger.Error("Failed to cast pipeline info to gitlab pipeline info", lf.ProjectName(id.Project), lf.PipelineID(id.Id)) + } + if err != nil { + p.Logger.Error("Failed to fetch pipeline", zap.Error(err)) + } else if info_gitlab.Status != models.PipelineStatusRunning { + p.Logger.Info("Fetched fresh pipeline", lf.ProjectName(id.Project), lf.PipelineID(id.Id), lf.PipelineStatus(info_gitlab.Status)) + removed = append(removed, id) + } + return true + }) + + for _, id := range removed { + p.Fresh.Delete(id) + } +} diff --git a/internal/gitlab/projects.go b/internal/platform/gitlab/gitlab_projects.go similarity index 50% rename from internal/gitlab/projects.go rename to internal/platform/gitlab/gitlab_projects.go index cdae994..204cbb9 100644 --- a/internal/gitlab/projects.go +++ b/internal/platform/gitlab/gitlab_projects.go @@ -4,79 +4,69 @@ import ( "context" "time" - "github.com/bigredeye/notmanytask/internal/database" "github.com/bigredeye/notmanytask/internal/models" + "github.com/bigredeye/notmanytask/internal/platform/base" + "go.uber.org/zap" ) -type ProjectsMaker struct { - *Client - - logger *zap.Logger - db *database.DataBase - users chan *models.User -} - -func NewProjectsMaker(client *Client, db *database.DataBase) (*ProjectsMaker, error) { - return &ProjectsMaker{client, client.logger.Named("projects"), db, make(chan *models.User, 4)}, nil -} - -func (p ProjectsMaker) AsyncPrepareProject(user *models.User) { - p.users <- user +type ProjectsMakerGitlab struct { + *base.ProjectsMakerBase + *ClientGitlab } -func (p ProjectsMaker) Run(ctx context.Context) { - if p.config.PullIntervals.Projects == nil { +func (p ProjectsMakerGitlab) Run(ctx context.Context) { + if p.Config.PullIntervals.Projects == nil { return } - p.initializeMissingProjects() + p.InitializeMissingProjects() - tick := time.NewTimer(*p.config.PullIntervals.Projects) + tick := time.NewTimer(*p.Config.PullIntervals.Projects) for { select { - case user := <-p.users: - p.logger.Info("Got user from in-proc channel", + case user := <-p.Users: + p.Logger.Info("Got user from in-proc channel", zap.Intp("gitlab_id", user.GitlabID), zap.Stringp("gitlab_login", user.GitlabLogin), ) - if !p.maybeInitializeProject(user) { - p.users <- user + if !p.MaybeInitializeProject(user) { + p.Users <- user } case <-tick.C: - p.initializeMissingProjects() + p.InitializeMissingProjects() case <-ctx.Done(): - p.logger.Info("Stopping projects maker") + p.Logger.Info("Stopping projects maker") return } } } -func (p ProjectsMaker) initializeMissingProjects() { - p.logger.Debug("Start projectsMaker iteration") +func (p ProjectsMakerGitlab) InitializeMissingProjects() { + p.Logger.Debug("Start projectsMaker iteration") numProjectsInitialized := 0 - defer p.logger.Debug("Finish projectsMaker iteration", zap.Int("num_projects_initialized", numProjectsInitialized)) + defer p.Logger.Debug("Finish projectsMaker iteration", zap.Int("num_projects_initialized", numProjectsInitialized)) - users, err := p.db.ListUsersWithoutRepos() + users, err := p.Db.ListUsersWithoutRepos() if err != nil { - p.logger.Error("Failed to list users without repos", zap.Error(err)) + p.Logger.Error("Failed to list users without repos", zap.Error(err)) return } for _, user := range users { - p.logger.Info("Got user without repo from database", + p.Logger.Info("Got user without repo from database", zap.Intp("gitlab_id", user.GitlabID), zap.Stringp("gitlab_login", user.GitlabLogin), ) - ok := p.maybeInitializeProject(user) + ok := p.MaybeInitializeProject(user) if ok { numProjectsInitialized++ } } } -func (p ProjectsMaker) maybeInitializeProject(user *models.User) bool { - log := p.logger +func (p ProjectsMakerGitlab) MaybeInitializeProject(user *models.User) bool { + log := p.Logger if user.GitlabID == nil || user.GitlabLogin == nil { log.Error("Trying to initialize repo for user without login, aborting", zap.Uint("user_id", user.ID)) return false @@ -96,7 +86,7 @@ func (p ProjectsMaker) maybeInitializeProject(user *models.User) bool { log = log.With(zap.String("project", project)) user.Repository = &project - err = p.db.SetUserRepository(user) + err = p.Db.SetUserRepository(user) if err != nil { log.Error("Failed to set user repo", zap.Error(err)) return false diff --git a/internal/gitlab/oauth.go b/internal/platform/oauth.go similarity index 51% rename from internal/gitlab/oauth.go rename to internal/platform/oauth.go index c9e75d5..b4ec89b 100644 --- a/internal/gitlab/oauth.go +++ b/internal/platform/oauth.go @@ -1,8 +1,10 @@ -package gitlab +package platform import ( "net/http" + gitea "code.gitea.io/sdk/gitea" + "github.com/bigredeye/notmanytask/internal/config" "github.com/pkg/errors" "github.com/xanzy/go-gitlab" ) @@ -12,6 +14,11 @@ type User struct { Login string } +type UserGitea struct { + ID int64 + Login string +} + func GetOAuthGitLabUser(token string) (*User, error) { client, err := gitlab.NewOAuthClient(token) if err != nil { @@ -31,3 +38,20 @@ func GetOAuthGitLabUser(token string) (*User, error) { Login: user.Username, }, nil } + +func GetOAuthGiteaUser(conf *config.Config, token string) (*UserGitea, error) { + client, err := gitea.NewClient(conf.Platform.Gitea.BaseURL, gitea.SetToken(token)) + if err != nil { + return nil, errors.Wrap(err, "failed to create gitea client") + } + + user, _, err := client.GetMyUserInfo() + if err != nil { + return nil, errors.Wrap(err, "failed to get current user") + } + + return &UserGitea{ + ID: user.ID, + Login: user.UserName, + }, nil +} diff --git a/internal/platform/pipelines_fabric.go b/internal/platform/pipelines_fabric.go new file mode 100644 index 0000000..dd704b7 --- /dev/null +++ b/internal/platform/pipelines_fabric.go @@ -0,0 +1,47 @@ +package platform + +import ( + "fmt" + + "github.com/bigredeye/notmanytask/internal/config" + "github.com/bigredeye/notmanytask/internal/database" + "github.com/bigredeye/notmanytask/internal/platform/base" + gitea_client "github.com/bigredeye/notmanytask/internal/platform/gitea" + gitlab_client "github.com/bigredeye/notmanytask/internal/platform/gitlab" + + "github.com/pkg/errors" +) + +func NewPipelinesFetcher(conf *config.Config, client base.ClientInterface, db *database.DataBase) (base.PipelinesFetcherInterface, error) { + switch conf.Platform.Mode { + case "gitlab": + client_gitlab, done := client.(*gitlab_client.ClientGitlab) + if !done { + return nil, errors.Wrap(nil, "failed to cast client to gitlab") + } + + return &gitlab_client.PipelinesFetcherGitlab{ + ClientGitlab: client_gitlab, + PipelinesFetcherBase: &base.PipelinesFetcherBase{ + Logger: client_gitlab.Logger.Named("pipelines"), + Db: db, + }, + }, nil + case "gitea": + client_gitea, done := client.(*gitea_client.ClientGitea) + if !done { + return nil, errors.Wrap(nil, "failed to cast client to gitlab") + } + + return &gitea_client.PipelinesFetcherGitea{ + ClientGitea: client_gitea, + PipelinesFetcherBase: &base.PipelinesFetcherBase{ + Logger: client_gitea.Logger.Named("pipelines"), + Db: db, + }, + }, nil + default: + return nil, errors.Wrap(errors.Errorf("Unknown platform mode: %s", conf.Platform.Mode), fmt.Sprintf("Failed to create pipeline fetcher for platform %s", conf.Platform.Mode)) + } + +} diff --git a/internal/platform/projects_fabric.go b/internal/platform/projects_fabric.go new file mode 100644 index 0000000..2eecc2a --- /dev/null +++ b/internal/platform/projects_fabric.go @@ -0,0 +1,49 @@ +package platform + +import ( + "fmt" + + "github.com/bigredeye/notmanytask/internal/config" + "github.com/bigredeye/notmanytask/internal/database" + "github.com/bigredeye/notmanytask/internal/models" + "github.com/bigredeye/notmanytask/internal/platform/base" + gitea_client "github.com/bigredeye/notmanytask/internal/platform/gitea" + gitlab_client "github.com/bigredeye/notmanytask/internal/platform/gitlab" + "github.com/pkg/errors" +) + +func NewProjectsMaker(conf *config.Config, client base.ClientInterface, db *database.DataBaseProxy) (base.ProjectsMakerInterface, error) { + switch conf.Platform.Mode { + case config.GitlabMode: + client_gitlab, done := client.(*gitlab_client.ClientGitlab) + if !done { + return nil, errors.Wrap(nil, "Projects maker: client is not gitlab") + } + + return &gitlab_client.ProjectsMakerGitlab{ + ClientGitlab: client_gitlab, + ProjectsMakerBase: &base.ProjectsMakerBase{ + Logger: client_gitlab.Logger.Named("projects"), + Db: db, + Users: make(chan *models.User, 4), + }, + }, nil + case config.GiteaMode: + client_gitea, done := client.(*gitea_client.ClientGitea) + if !done { + return nil, errors.Wrap(nil, "Projects maker: client is not gitea") + } + + return &gitea_client.ProjectsMakerGitea{ + ClientGitea: client_gitea, + ProjectsMakerBase: &base.ProjectsMakerBase{ + Logger: client_gitea.Logger.Named("projects"), + Db: db, + Users: make(chan *models.User, 4), + }, + }, nil + default: + return nil, errors.Wrap(errors.Errorf("Unknown platform mode: %s", conf.Platform.Mode), fmt.Sprintf("Failed to create projects maker for platform %s", conf.Platform.Mode)) + } + +} diff --git a/internal/scorer/scorer.go b/internal/scorer/scorer.go index 6c10a2b..4e40fed 100644 --- a/internal/scorer/scorer.go +++ b/internal/scorer/scorer.go @@ -7,6 +7,7 @@ import ( "sort" "strings" + "github.com/bigredeye/notmanytask/internal/config" "github.com/bigredeye/notmanytask/internal/database" "github.com/bigredeye/notmanytask/internal/deadlines" "github.com/bigredeye/notmanytask/internal/models" @@ -22,13 +23,14 @@ type ProjectNameFactory interface { } type Scorer struct { + conf *config.Config deadlines *deadlines.Fetcher - db *database.DataBase + db *database.DataBaseProxy projects ProjectNameFactory } -func NewScorer(db *database.DataBase, deadlines *deadlines.Fetcher, projects ProjectNameFactory) *Scorer { - return &Scorer{deadlines, db, projects} +func NewScorer(conf *config.Config, db *database.DataBaseProxy, deadlines *deadlines.Fetcher, projects ProjectNameFactory) *Scorer { + return &Scorer{conf, deadlines, db, projects} } const ( @@ -228,7 +230,7 @@ func (s Scorer) CalcUserScores(user *models.User) (*UserScores, error) { return nil, fmt.Errorf("no deadlines found") } - overrides, err := s.db.ListUserOverrides(*user.GitlabLogin) + overrides, err := s.db.ListUserOverrides(*s.db.UserLogin(user)) if err != nil { return nil, fmt.Errorf("failed to list user overrides: %w", err) } diff --git a/internal/web/login.go b/internal/web/login.go index f629d43..9bb289e 100644 --- a/internal/web/login.go +++ b/internal/web/login.go @@ -19,11 +19,13 @@ import ( "go.uber.org/zap" "golang.org/x/exp/maps" "golang.org/x/exp/slices" + "golang.org/x/oauth2" + "github.com/bigredeye/notmanytask/internal/config" "github.com/bigredeye/notmanytask/internal/database" - "github.com/bigredeye/notmanytask/internal/gitlab" lf "github.com/bigredeye/notmanytask/internal/logfield" "github.com/bigredeye/notmanytask/internal/models" + "github.com/bigredeye/notmanytask/internal/platform" ) type loginService struct { @@ -115,14 +117,26 @@ func (s loginService) signupForm(c *gin.Context) { } return } - if user.GitlabID != nil || user.GitlabLogin != nil { - log.Warn("User is already registered", - zap.Error(err), - zap.Intp("gitlab_id", user.GitlabID), - zap.Stringp("gitlab_login", user.GitlabLogin), - ) - s.RedirectToSignup(c, "User is already registered") - return + if s.config.Platform.Mode == config.GitlabMode { + if user.GitlabID != nil || user.GitlabLogin != nil { + log.Warn("User is already registered", + zap.Error(err), + zap.Intp("gitlab_id", user.GitlabID), + zap.Stringp("gitlab_login", user.GitlabLogin), + ) + s.RedirectToSignup(c, "User is already registered") + return + } + } else if s.config.Platform.Mode == config.GiteaMode { + if user.GiteaID != nil || user.GiteaLogin != nil { + log.Warn("User is already registered", + zap.Error(err), + zap.Int64p("gitea_id", user.GiteaID), + zap.Stringp("gitea_login", user.GiteaLogin), + ) + s.RedirectToSignup(c, "User is already registered") + return + } } if err = s.fillSessionForUser(c, user); err != nil { @@ -213,53 +227,73 @@ func (s loginService) login(c *gin.Context) { c.Redirect(http.StatusFound, s.server.auth.LoginURL(oauthState)) } -func (s loginService) oauth(c *gin.Context) { - // Compare oauth state in query and cookie - oauthState := c.Query("state") - storage := sessions.Default(c) - if v := storage.Get(sessionKeyOAuth); v == nil || v != oauthState { - if v == nil { - s.log.Info("No oauth state found") - } else { - s.log.Info("Mismatched oauth state", zap.String("query", oauthState), zap.String("cookie", v.(string))) +func (s loginService) giteaOauth(token *oauth2.Token, user *models.User, c *gin.Context) error { + giteaUser, err := platform.GetOAuthGiteaUser(s.config, token.AccessToken) + if err != nil { + s.log.Error("Failed to get gitea user", zap.Error(err)) + s.RedirectToSignup(c, "Gitea authentication failed, try again") + return err + } + s.log.Info("Fetched gitea user", zap.String("gitea_login", giteaUser.Login), zap.Int64("gitea_id", giteaUser.ID)) + + // user == nil if the token was not provided + // This may happen after /logout and /login + if user == nil { + user, err = s.server.db.FindUserByGiteaID(giteaUser.ID) + if err != nil { + s.log.Error("Unknown user", zap.Error(err), zap.Int64("gitea_id", giteaUser.ID)) + s.RedirectToSignup(c, "You are not registered, please try to register first") + return err } - s.RedirectToSignup(c, "GitLab authentication failed, try again") - return } - // Get users and session from database - user, _, err := s.server.tryFindUserByToken(c) - if err != nil { - s.log.Error("Failed to find user session", zap.Error(err)) - s.RedirectToSignup(c, "You are not registered, try again") - return + if user.GiteaLogin != nil && user.GiteaID != nil { + if err = s.fillSessionForUser(c, user); err != nil { + s.log.Error("Failed to create session", zap.Error(err), zap.Int64("gitea_id", giteaUser.ID)) + s.RedirectToSignup(c, "Internal server error, try again later") + return err + } + s.log.Info("Filled session for existing user", lf.UserID(user.ID), lf.GiteaLogin(giteaUser.Login), lf.GiteaID(giteaUser.ID)) + c.Redirect(http.StatusFound, s.config.Endpoints.Home) + return nil } - // Resolve gitlab user - ctx, cancel := context.WithTimeout(c, time.Second*10) - defer cancel() - token, err := s.server.auth.Exchange(ctx, c.Query("code")) + user.GiteaUser = models.GiteaUser{ + GiteaID: &giteaUser.ID, + GiteaLogin: &giteaUser.Login, + } + + err = s.server.db.SetUserGiteaAccount(user.ID, &user.GiteaUser) if err != nil { - s.log.Error("Failed to exchange tokens", zap.Error(err)) - s.RedirectToSignup(c, "GitLab authentication failed, try again") - return + if database.IsDuplicateKey(err) { + s.log.Error("Duplicate gitlab account", zap.Error(err)) + s.RedirectToSignup(c, "Gitlab account is already registered") + } else { + s.log.Error("Failed to set user gitlab account", zap.Error(err)) + s.RedirectToSignup(c, "Internal server error, try again later") + } + return err } - gitlabUser, err := gitlab.GetOAuthGitLabUser(token.AccessToken) + return nil +} + +func (s loginService) gitlabOauth(token *oauth2.Token, user *models.User, c *gin.Context) error { + gitlabUser, err := platform.GetOAuthGitLabUser(token.AccessToken) if err != nil { s.log.Error("Failed to get gitlab user", zap.Error(err)) s.RedirectToSignup(c, "GitLab authentication failed, try again") - return + return err } s.log.Info("Fetched gitlab user", zap.String("gitlab_login", gitlabUser.Login), zap.Int("gitlab_id", gitlabUser.ID)) - // user == nil iff the token was not provided + // user == nil if the token was not provided // This may happen after /logout and /login if user == nil { user, err = s.server.db.FindUserByGitlabID(gitlabUser.ID) if err != nil { s.log.Error("Unknown user", zap.Error(err), zap.Int("gitlab_id", gitlabUser.ID)) s.RedirectToSignup(c, "You are not registered, please try to register first") - return + return err } } @@ -267,11 +301,11 @@ func (s loginService) oauth(c *gin.Context) { if err = s.fillSessionForUser(c, user); err != nil { s.log.Error("Failed to create session", zap.Error(err), zap.Int("gitlab_id", gitlabUser.ID)) s.RedirectToSignup(c, "Internal server error, try again later") - return + return err } s.log.Info("Filled session for existing user", lf.UserID(user.ID), lf.GitlabLogin(gitlabUser.Login), lf.GitlabID(gitlabUser.ID)) c.Redirect(http.StatusFound, s.config.Endpoints.Home) - return + return nil } user.GitlabUser = models.GitlabUser{ @@ -288,6 +322,51 @@ func (s loginService) oauth(c *gin.Context) { s.log.Error("Failed to set user gitlab account", zap.Error(err)) s.RedirectToSignup(c, "Internal server error, try again later") } + return err + } + return nil +} + +func (s loginService) oauth(c *gin.Context) { + // Compare oauth state in query and cookie + oauthState := c.Query("state") + storage := sessions.Default(c) + if v := storage.Get(sessionKeyOAuth); v == nil || v != oauthState { + if v == nil { + s.log.Info("No oauth state found") + } else { + s.log.Info("Mismatched oauth state", zap.String("query", oauthState), zap.String("cookie", v.(string))) + } + s.RedirectToSignup(c, "Provider authentication failed, try again") + return + } + + // Get users and session from database + user, _, err := s.server.tryFindUserByToken(c) + if err != nil { + s.log.Error("Failed to find user session", zap.Error(err)) + s.RedirectToSignup(c, "You are not registered, try again") + return + } + + // Resolve gitlab user + ctx, cancel := context.WithTimeout(c, time.Second*10) + defer cancel() + token, err := s.server.auth.Exchange(ctx, c.Query("code")) + if err != nil { + s.log.Error("Failed to exchange tokens", zap.Error(err)) + s.RedirectToSignup(c, "GitLab authentication failed, try again") + return + } + + switch s.config.Platform.Mode { + case config.GitlabMode: + s.gitlabOauth(token, user, c) + case config.GiteaMode: + s.giteaOauth(token, user, c) + default: + s.log.Error("Unknown platform mode", zap.String("mode", s.config.Platform.Mode)) + s.RedirectToSignup(c, "Internal server error, try again later") return } diff --git a/internal/web/oauth.go b/internal/web/oauth.go index 99a2444..ce475b9 100644 --- a/internal/web/oauth.go +++ b/internal/web/oauth.go @@ -4,13 +4,17 @@ import ( "context" "net/http" + "github.com/bigredeye/notmanytask/internal/config" + gitlab "github.com/markbates/goth/providers/gitlab" "github.com/pkg/errors" "golang.org/x/oauth2" - "golang.org/x/oauth2/gitlab" - - "github.com/bigredeye/notmanytask/internal/config" ) +var Endpoint = oauth2.Endpoint{ + AuthURL: gitlab.AuthURL, + TokenURL: gitlab.TokenURL, +} + type AuthClient struct { conf *oauth2.Config } @@ -18,10 +22,10 @@ type AuthClient struct { func NewAuthClient(conf *config.Config) *AuthClient { return &AuthClient{ conf: &oauth2.Config{ - ClientID: conf.GitLab.Application.ClientID, - ClientSecret: conf.GitLab.Application.Secret, + ClientID: conf.Platform.GitLab.Application.ClientID, + ClientSecret: conf.Platform.GitLab.Application.Secret, Scopes: []string{"read_user"}, - Endpoint: gitlab.Endpoint, + Endpoint: Endpoint, RedirectURL: conf.Endpoints.HostName + conf.Endpoints.OauthCallback, }, } diff --git a/internal/web/render.go b/internal/web/render.go index 635d87f..a8ad617 100644 --- a/internal/web/render.go +++ b/internal/web/render.go @@ -48,7 +48,7 @@ func (s *server) RenderSubmitFlagPageDetails(c *gin.Context, err, success string var flagRe = regexp.MustCompile(`^\{FLAG(-[a-z0-9_/]+)+(-[0-9a-f]+)+\}$`) -func (s *server) handleFlagSubmit(c *gin.Context) { +func (s *server) handleFlagSubmitGitlab(c *gin.Context) { user := c.MustGet("user").(*models.User) if user.GitlabLogin == nil { s.logger.Error("User without gitlab login!", lf.UserID(user.ID)) @@ -63,7 +63,31 @@ func (s *server) handleFlagSubmit(c *gin.Context) { return } - err := s.db.SubmitFlag(flag, *user.GitlabLogin) + err := s.db.SubmitFlagGitlab(flag, *user.GitlabLogin) + if err != nil { + s.RenderSubmitFlagPageDetails(c, "Unknown flag", "") + return + } + + s.RenderSubmitFlagPageDetails(c, "", "The matrix has you...") +} + +func (s *server) handleFlagSubmitGitea(c *gin.Context) { + user := c.MustGet("user").(*models.User) + if user.GiteaLogin == nil { + s.logger.Error("User without gitea login!", lf.UserID(user.ID)) + c.Redirect(http.StatusFound, s.config.Endpoints.Signup) + return + } + + flag := c.PostForm("flag") + if !flagRe.MatchString(flag) { + s.logger.Warn("Invalid flag", zap.String("flag", flag), lf.UserID(user.ID), lf.GiteaLogin(*user.GiteaLogin)) + s.RenderSubmitFlagPageDetails(c, "Invalid flag", "") + return + } + + err := s.db.SubmitFlagGitea(flag, *user.GiteaLogin) if err != nil { s.RenderSubmitFlagPageDetails(c, "Unknown flag", "") return @@ -94,9 +118,9 @@ func (s *server) makeLinks(user *models.User) *Links { return &Links{ Deadlines: s.config.Endpoints.Home, Standings: s.config.Endpoints.Standings, - TasksRepository: s.config.GitLab.TaskUrlPrefix, - Repository: s.gitlab.MakeProjectURL(user), - Submits: s.gitlab.MakeProjectSubmitsURL(user), + TasksRepository: s.config.Platform.GitLab.TaskUrlPrefix, + Repository: s.client.MakeProjectURL(user), + Submits: s.client.MakeProjectSubmitsURL(user), Logout: s.config.Endpoints.Logout, SubmitFlag: s.config.Endpoints.Flag, } diff --git a/internal/web/run.go b/internal/web/run.go index 4b4bd72..adbacf3 100644 --- a/internal/web/run.go +++ b/internal/web/run.go @@ -10,7 +10,7 @@ import ( "github.com/bigredeye/notmanytask/internal/config" "github.com/bigredeye/notmanytask/internal/database" "github.com/bigredeye/notmanytask/internal/deadlines" - "github.com/bigredeye/notmanytask/internal/gitlab" + "github.com/bigredeye/notmanytask/internal/platform" "github.com/bigredeye/notmanytask/internal/scorer" "github.com/bigredeye/notmanytask/internal/tgbot" zlog "github.com/bigredeye/notmanytask/pkg/log" @@ -45,6 +45,11 @@ func Run() error { config.DataBase.Port, config.DataBase.Name, )) + + db_proxy := database.DataBaseProxy{ + DataBase: db, + Conf: config, + } if err != nil { return errors.Wrap(err, "Failed to open database") } @@ -59,22 +64,22 @@ func Run() error { return errors.Wrap(err, "Failed to create deadlines fetcher") } - git, err := gitlab.NewClient(config, logger.Named("gitlab")) + git, err := platform.NewClient(config, logger.Named(config.Platform.Mode)) if err != nil { - return errors.Wrap(err, "Failed to create gitlab client") + return errors.Wrap(err, fmt.Sprintf("Failed to create %s client", config.Platform.Mode)) } - projects, err := gitlab.NewProjectsMaker(git, db) + projects, err := platform.NewProjectsMaker(config, git, &db_proxy) if err != nil { return errors.Wrap(err, "Failed to create projects maker") } - pipelines, err := gitlab.NewPipelinesFetcher(git, db) + pipelines, err := platform.NewPipelinesFetcher(config, git, db) if err != nil { - return errors.Wrap(err, "Failed to create projects maker") + return errors.Wrap(err, "Failed to create pipeline fetcher") } - scorer := scorer.NewScorer(db, deadlines, git) + scorer := scorer.NewScorer(config, &db_proxy, deadlines, git) wg.Add(5) go func() { @@ -98,7 +103,7 @@ func Run() error { bot.Run(ctx) }() - s := newServer(config, logger.Named("server"), db, deadlines, projects, pipelines, scorer, git) + s := newServer(config, logger.Named("server"), &db_proxy, deadlines, projects, pipelines, scorer, git) return errors.Wrap(s.run(), "Server failed") } diff --git a/internal/web/server.go b/internal/web/server.go index 4c4a2fb..04ef6ad 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -16,7 +16,7 @@ import ( "github.com/bigredeye/notmanytask/internal/config" "github.com/bigredeye/notmanytask/internal/database" "github.com/bigredeye/notmanytask/internal/deadlines" - "github.com/bigredeye/notmanytask/internal/gitlab" + "github.com/bigredeye/notmanytask/internal/platform/base" "github.com/bigredeye/notmanytask/internal/scorer" "github.com/bigredeye/notmanytask/web" ) @@ -26,12 +26,12 @@ type server struct { logger *zap.Logger auth *AuthClient - db *database.DataBase + db *database.DataBaseProxy deadlines *deadlines.Fetcher - projects *gitlab.ProjectsMaker - pipelines *gitlab.PipelinesFetcher + projects base.ProjectsMakerInterface + pipelines base.PipelinesFetcherInterface scorer *scorer.Scorer - gitlab *gitlab.Client + client base.ClientInterface cache *ccache.Cache } @@ -39,12 +39,12 @@ type server struct { func newServer( config *config.Config, logger *zap.Logger, - db *database.DataBase, + db *database.DataBaseProxy, deadlines *deadlines.Fetcher, - projects *gitlab.ProjectsMaker, - pipelines *gitlab.PipelinesFetcher, + projects base.ProjectsMakerInterface, + pipelines base.PipelinesFetcherInterface, scorer *scorer.Scorer, - gitlab *gitlab.Client, + client base.ClientInterface, ) *server { return &server{ config: config, @@ -55,7 +55,7 @@ func newServer( projects: projects, pipelines: pipelines, scorer: scorer, - gitlab: gitlab, + client: client, cache: ccache.New(ccache.Configure()), } } @@ -104,10 +104,18 @@ func (s *server) run() error { c.String(http.StatusOK, "pong "+fmt.Sprint(time.Now().Unix())) }) + var handleFlagSubmit func(c *gin.Context) + switch s.config.Platform.Mode { + case config.GitlabMode: + handleFlagSubmit = s.handleFlagSubmitGitlab + case config.GiteaMode: + handleFlagSubmit = s.handleFlagSubmitGitea + } + r.GET(s.config.Endpoints.Home, s.validateSession(true), s.RenderHomePage) r.GET(s.config.Endpoints.Flag, s.validateSession(true), s.RenderSubmitFlagPage) r.GET(s.config.Endpoints.Retakes, s.validateSession(true), s.RenderRetakesPage) - r.POST(s.config.Endpoints.Flag, s.validateSession(true), s.handleFlagSubmit) + r.POST(s.config.Endpoints.Flag, s.validateSession(true), handleFlagSubmit) r.GET(s.config.Endpoints.Standings /* no need to validate session */, s.RenderStandingsPage) r.GET("/private/solutions/:group/:task", s.handleChuckNorris) From 9149c219fd2f92063be536fdbc68aa54185f29ac Mon Sep 17 00:00:00 2001 From: Kirill Shaprunov Date: Sun, 12 May 2024 05:52:49 +0300 Subject: [PATCH 02/14] [nmt/internal] Fix oauth endpoints for Gitea --- internal/platform/client_fabric.go | 2 +- internal/platform/gitea/gitea_projects.go | 3 +- internal/web/login.go | 2 +- internal/web/oauth.go | 38 +++++++++++++++++------ 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/internal/platform/client_fabric.go b/internal/platform/client_fabric.go index f8a8da5..34f89cf 100644 --- a/internal/platform/client_fabric.go +++ b/internal/platform/client_fabric.go @@ -31,7 +31,7 @@ func NewClient(conf *config.Config, logger *zap.Logger) (base.ClientInterface, e Gitlab: client, }, nil case "gitea": - client, err := gitea.NewClient(conf.Platform.GitLab.BaseURL, gitea.SetToken(conf.Platform.Gitea.Api.Token)) + client, err := gitea.NewClient(conf.Platform.Gitea.BaseURL, gitea.SetToken(conf.Platform.Gitea.Api.Token)) if err != nil { return nil, errors.Wrap(err, "Failed to create gitea client") } diff --git a/internal/platform/gitea/gitea_projects.go b/internal/platform/gitea/gitea_projects.go index c203551..7f275b9 100644 --- a/internal/platform/gitea/gitea_projects.go +++ b/internal/platform/gitea/gitea_projects.go @@ -67,7 +67,8 @@ func (p ProjectsMakerGitea) InitializeMissingProjects() { func (p ProjectsMakerGitea) MaybeInitializeProject(user *models.User) bool { log := p.Logger - if user.GitlabID == nil || user.GitlabLogin == nil { + if user.GiteaID == nil || user.GiteaLogin == nil { + time.Sleep(time.Second * 5) log.Error("Trying to initialize repo for user without login, aborting", zap.Uint("user_id", user.ID)) return false } diff --git a/internal/web/login.go b/internal/web/login.go index 9bb289e..57060f6 100644 --- a/internal/web/login.go +++ b/internal/web/login.go @@ -355,7 +355,7 @@ func (s loginService) oauth(c *gin.Context) { token, err := s.server.auth.Exchange(ctx, c.Query("code")) if err != nil { s.log.Error("Failed to exchange tokens", zap.Error(err)) - s.RedirectToSignup(c, "GitLab authentication failed, try again") + s.RedirectToSignup(c, "Provider authentication failed, try again") return } diff --git a/internal/web/oauth.go b/internal/web/oauth.go index ce475b9..df1480e 100644 --- a/internal/web/oauth.go +++ b/internal/web/oauth.go @@ -10,25 +10,45 @@ import ( "golang.org/x/oauth2" ) -var Endpoint = oauth2.Endpoint{ +var EndpointGitlab = oauth2.Endpoint{ AuthURL: gitlab.AuthURL, TokenURL: gitlab.TokenURL, } +var EndpointGitea = oauth2.Endpoint{ + AuthURL: "/login/oauth/authorize", + TokenURL: "/login/oauth/access_token", +} + type AuthClient struct { conf *oauth2.Config } func NewAuthClient(conf *config.Config) *AuthClient { - return &AuthClient{ - conf: &oauth2.Config{ - ClientID: conf.Platform.GitLab.Application.ClientID, - ClientSecret: conf.Platform.GitLab.Application.Secret, - Scopes: []string{"read_user"}, - Endpoint: Endpoint, - RedirectURL: conf.Endpoints.HostName + conf.Endpoints.OauthCallback, - }, + var authClient AuthClient + switch conf.Platform.Mode { + case config.GitlabMode: + authClient = AuthClient{ + conf: &oauth2.Config{ + ClientID: conf.Platform.GitLab.Application.ClientID, + ClientSecret: conf.Platform.GitLab.Application.Secret, + Scopes: []string{"read_user"}, + Endpoint: EndpointGitlab, + RedirectURL: conf.Endpoints.HostName + conf.Endpoints.OauthCallback, + }, + } + case config.GiteaMode: + authClient = AuthClient{ + conf: &oauth2.Config{ + ClientID: conf.Platform.Gitea.Application.ClientID, + ClientSecret: conf.Platform.Gitea.Application.Secret, + Scopes: []string{"read_user"}, + Endpoint: EndpointGitea, + RedirectURL: conf.Endpoints.HostName + conf.Endpoints.OauthCallback, + }, + } } + return &authClient } func (c *AuthClient) LoginURL(state string) string { From f768fa3b940e6806e24d4032f20552cc8526ce6c Mon Sep 17 00:00:00 2001 From: Kirill Shaprunov Date: Sun, 12 May 2024 05:56:19 +0300 Subject: [PATCH 03/14] [internal] go.mod, go.sum upd --- go.mod | 23 +++++++++++++--------- go.sum | 61 ++++++++++++++++++++++++++++++++++++++++------------------ 2 files changed, 56 insertions(+), 28 deletions(-) diff --git a/go.mod b/go.mod index fc32edb..d5bcce2 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/jackc/pgconn v1.13.0 github.com/joho/godotenv v1.4.0 github.com/karlseguin/ccache/v2 v2.0.8 + github.com/markbates/goth v1.79.0 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.6.1 github.com/spf13/viper v1.14.0 @@ -23,7 +24,7 @@ require ( go.uber.org/atomic v1.10.0 go.uber.org/zap v1.24.0 golang.org/x/exp v0.0.0-20221227203929-1b447090c38c - golang.org/x/oauth2 v0.3.0 + golang.org/x/oauth2 v0.17.0 golang.org/x/sync v0.1.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v2 v2.4.0 @@ -33,19 +34,23 @@ require ( ) require ( + code.gitea.io/sdk/gitea v0.18.0 // indirect + github.com/davidmz/go-pageant v1.0.2 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/validator/v10 v10.11.1 // indirect - github.com/goccy/go-json v0.10.0 // indirect - github.com/golang/protobuf v1.5.2 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/sessions v1.2.1 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.1 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect @@ -75,13 +80,13 @@ require ( go.opentelemetry.io/otel v1.11.2 // indirect go.opentelemetry.io/otel/trace v1.11.2 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.4.0 // indirect - golang.org/x/net v0.4.0 // indirect - golang.org/x/sys v0.3.0 // indirect - golang.org/x/text v0.5.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.3.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.28.1 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/protobuf v1.32.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index dc61590..f9e8eb9 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +code.gitea.io/sdk/gitea v0.18.0 h1:+zZrwVmujIrgobt6wVBWCqITz6bn1aBjnCUHmpZrerI= +code.gitea.io/sdk/gitea v0.18.0/go.mod h1:IG9xZJoltDNeDSW0qiF2Vqx5orMWa7OhVWrjvrd5NpI= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -65,6 +67,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= +github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -86,6 +90,8 @@ github.com/gin-contrib/zap v0.1.0/go.mod h1:hvnZaPs478H1PGvRP8w89ZZbyJUiyip4ddiI github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY= github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398= +github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= +github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -109,8 +115,8 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= -github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -139,8 +145,9 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -192,6 +199,8 @@ github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrj github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ= github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -271,8 +280,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxv github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -287,6 +296,8 @@ github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/markbates/goth v1.79.0 h1:fUYi9R6VubVEK2bpmXvIUp7xRcxA68i8ovfUQx/i5Qc= +github.com/markbates/goth v1.79.0/go.mod h1:RBD+tcFnXul2NnYuODhnIweOcuVPkBohLfEvutPekcU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= @@ -318,8 +329,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -355,8 +366,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= @@ -372,6 +383,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -418,13 +430,15 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= -golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -461,6 +475,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -496,8 +511,9 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= -golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -507,8 +523,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8= -golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk= +golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= +golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -520,6 +536,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -568,12 +585,15 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -582,8 +602,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -642,6 +663,7 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -673,8 +695,9 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -740,8 +763,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= From a2ffa9c5d3356b762be09b7e9cc56e98697e1b81 Mon Sep 17 00:00:00 2001 From: Kirill Shaprunov Date: Sun, 12 May 2024 06:01:46 +0300 Subject: [PATCH 04/14] [nmt/internal] Add base url prefix to oauth endpoints --- internal/web/oauth.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/web/oauth.go b/internal/web/oauth.go index df1480e..ac2030c 100644 --- a/internal/web/oauth.go +++ b/internal/web/oauth.go @@ -16,8 +16,8 @@ var EndpointGitlab = oauth2.Endpoint{ } var EndpointGitea = oauth2.Endpoint{ - AuthURL: "/login/oauth/authorize", - TokenURL: "/login/oauth/access_token", + AuthURL: "https://gitea.com/login/oauth/authorize", + TokenURL: "https://gitea.com/login/oauth/access_token", } type AuthClient struct { From 93da6edf09f30299d89cfb06d94cfaad80ce3cdf Mon Sep 17 00:00:00 2001 From: Kirill Shaprunov Date: Sun, 12 May 2024 06:42:48 +0300 Subject: [PATCH 05/14] [nmt/internal] Fix validate session --- internal/web/login.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/web/login.go b/internal/web/login.go index 57060f6..a7e874f 100644 --- a/internal/web/login.go +++ b/internal/web/login.go @@ -457,10 +457,12 @@ func (s *server) validateSession(verifyTelegram bool) func(c *gin.Context) { zap.Uint("user_id", user.ID), zap.Stringp("gitlab_login", user.GitlabLogin), zap.Intp("gitlab_id", user.GitlabID), + zap.Stringp("gitea_login", user.GiteaLogin), + zap.Int64p("gitea_id", user.GiteaID), ) - if user.GitlabID == nil || user.GitlabLogin == nil { - s.logger.Warn("Found user without gitlab account, redirecting to /login", + if (user.GitlabID == nil || user.GitlabLogin == nil) && (user.GiteaID == nil || user.GiteaLogin == nil) { + s.logger.Warn("Found user without provider account, redirecting to /login", zap.String("token", session.Token), zap.Uint("user_id", user.ID), ) From a2a81d78080b6887fc347aca6e502e61bfe22be0 Mon Sep 17 00:00:00 2001 From: Kirill Shaprunov Date: Sun, 12 May 2024 07:09:35 +0300 Subject: [PATCH 06/14] [nmt/internal] Change README.md in repo --- internal/platform/gitea/gitea_client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/platform/gitea/gitea_client.go b/internal/platform/gitea/gitea_client.go index 0a38b96..236c414 100644 --- a/internal/platform/gitea/gitea_client.go +++ b/internal/platform/gitea/gitea_client.go @@ -41,11 +41,11 @@ func (c *ClientGitea) InitializeProject(user *models.User) error { opts := gitea.CreateRepoOption{ Name: projectName, - Description: fmt.Sprintf("%s private repository", *user.GiteaLogin), + Description: c.Config.Platform.Gitea.DefaultReadme, DefaultBranch: base.Master, Private: true, AutoInit: true, - Readme: c.Config.Platform.Gitea.DefaultReadme, + Template: true, } repo, _, err = c.Gitea.CreateOrgRepo(c.Config.Platform.Gitea.Organization.Name, opts) if err != nil { From 0bdb87bf4d8bbecc965127de904d66b64f606d33 Mon Sep 17 00:00:00 2001 From: Kirill Shaprunov Date: Sun, 12 May 2024 07:44:57 +0300 Subject: [PATCH 07/14] [nmt/internal] Scorer User model change to common --- internal/scorer/results.go | 8 ++++---- internal/scorer/scorer.go | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/scorer/results.go b/internal/scorer/results.go index 38b900c..cf25c24 100644 --- a/internal/scorer/results.go +++ b/internal/scorer/results.go @@ -59,10 +59,10 @@ type ScoredTaskGroup struct { } type User struct { - FirstName string - LastName string - GitlabLogin string - GitlabProject string + FirstName string + LastName string + Login string + Project string } func (u User) FullName() string { diff --git a/internal/scorer/scorer.go b/internal/scorer/scorer.go index 4e40fed..df72bcb 100644 --- a/internal/scorer/scorer.go +++ b/internal/scorer/scorer.go @@ -273,10 +273,10 @@ func (s Scorer) calcUserScoresImpl(currentDeadlines *deadlines.Deadlines, user * MaxScore: 0, FinalMark: 0.0, User: User{ - FirstName: user.FirstName, - LastName: user.LastName, - GitlabLogin: *user.GitlabLogin, - GitlabProject: s.projects.MakeProjectName(user), + FirstName: user.FirstName, + LastName: user.LastName, + Login: *s.db.UserLogin(user), + Project: s.projects.MakeProjectName(user), }, } From 016240dca5634cb68f4189fd4055388cb176e407 Mon Sep 17 00:00:00 2001 From: Kirill Shaprunov Date: Sun, 12 May 2024 07:58:08 +0300 Subject: [PATCH 08/14] [nmt/internal] Redirect no async prepare project for existing user --- internal/web/login.go | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/internal/web/login.go b/internal/web/login.go index a7e874f..082438e 100644 --- a/internal/web/login.go +++ b/internal/web/login.go @@ -227,12 +227,12 @@ func (s loginService) login(c *gin.Context) { c.Redirect(http.StatusFound, s.server.auth.LoginURL(oauthState)) } -func (s loginService) giteaOauth(token *oauth2.Token, user *models.User, c *gin.Context) error { +func (s loginService) giteaOauth(token *oauth2.Token, user *models.User, c *gin.Context) (bool, error) { giteaUser, err := platform.GetOAuthGiteaUser(s.config, token.AccessToken) if err != nil { s.log.Error("Failed to get gitea user", zap.Error(err)) s.RedirectToSignup(c, "Gitea authentication failed, try again") - return err + return false, err } s.log.Info("Fetched gitea user", zap.String("gitea_login", giteaUser.Login), zap.Int64("gitea_id", giteaUser.ID)) @@ -243,7 +243,7 @@ func (s loginService) giteaOauth(token *oauth2.Token, user *models.User, c *gin. if err != nil { s.log.Error("Unknown user", zap.Error(err), zap.Int64("gitea_id", giteaUser.ID)) s.RedirectToSignup(c, "You are not registered, please try to register first") - return err + return false, err } } @@ -251,11 +251,11 @@ func (s loginService) giteaOauth(token *oauth2.Token, user *models.User, c *gin. if err = s.fillSessionForUser(c, user); err != nil { s.log.Error("Failed to create session", zap.Error(err), zap.Int64("gitea_id", giteaUser.ID)) s.RedirectToSignup(c, "Internal server error, try again later") - return err + return false, err } s.log.Info("Filled session for existing user", lf.UserID(user.ID), lf.GiteaLogin(giteaUser.Login), lf.GiteaID(giteaUser.ID)) c.Redirect(http.StatusFound, s.config.Endpoints.Home) - return nil + return true, nil } user.GiteaUser = models.GiteaUser{ @@ -272,17 +272,17 @@ func (s loginService) giteaOauth(token *oauth2.Token, user *models.User, c *gin. s.log.Error("Failed to set user gitlab account", zap.Error(err)) s.RedirectToSignup(c, "Internal server error, try again later") } - return err + return false, err } - return nil + return false, nil } -func (s loginService) gitlabOauth(token *oauth2.Token, user *models.User, c *gin.Context) error { +func (s loginService) gitlabOauth(token *oauth2.Token, user *models.User, c *gin.Context) (bool, error) { gitlabUser, err := platform.GetOAuthGitLabUser(token.AccessToken) if err != nil { s.log.Error("Failed to get gitlab user", zap.Error(err)) s.RedirectToSignup(c, "GitLab authentication failed, try again") - return err + return false, err } s.log.Info("Fetched gitlab user", zap.String("gitlab_login", gitlabUser.Login), zap.Int("gitlab_id", gitlabUser.ID)) @@ -293,7 +293,7 @@ func (s loginService) gitlabOauth(token *oauth2.Token, user *models.User, c *gin if err != nil { s.log.Error("Unknown user", zap.Error(err), zap.Int("gitlab_id", gitlabUser.ID)) s.RedirectToSignup(c, "You are not registered, please try to register first") - return err + return false, err } } @@ -301,11 +301,11 @@ func (s loginService) gitlabOauth(token *oauth2.Token, user *models.User, c *gin if err = s.fillSessionForUser(c, user); err != nil { s.log.Error("Failed to create session", zap.Error(err), zap.Int("gitlab_id", gitlabUser.ID)) s.RedirectToSignup(c, "Internal server error, try again later") - return err + return false, err } s.log.Info("Filled session for existing user", lf.UserID(user.ID), lf.GitlabLogin(gitlabUser.Login), lf.GitlabID(gitlabUser.ID)) c.Redirect(http.StatusFound, s.config.Endpoints.Home) - return nil + return true, nil } user.GitlabUser = models.GitlabUser{ @@ -322,9 +322,9 @@ func (s loginService) gitlabOauth(token *oauth2.Token, user *models.User, c *gin s.log.Error("Failed to set user gitlab account", zap.Error(err)) s.RedirectToSignup(c, "Internal server error, try again later") } - return err + return false, err } - return nil + return false, nil } func (s loginService) oauth(c *gin.Context) { @@ -361,9 +361,15 @@ func (s loginService) oauth(c *gin.Context) { switch s.config.Platform.Mode { case config.GitlabMode: - s.gitlabOauth(token, user, c) + success, err := s.gitlabOauth(token, user, c) + if err != nil || success { + return + } case config.GiteaMode: - s.giteaOauth(token, user, c) + success, err := s.giteaOauth(token, user, c) + if err != nil || success { + return + } default: s.log.Error("Unknown platform mode", zap.String("mode", s.config.Platform.Mode)) s.RedirectToSignup(c, "Internal server error, try again later") From 287c7d6f17e85096488b8f6239c00d12e4e9d087 Mon Sep 17 00:00:00 2001 From: Kirill Shaprunov Date: Sun, 12 May 2024 08:05:07 +0300 Subject: [PATCH 09/14] [nmt/internal] Platform common login --- internal/database/database.go | 16 ++++++++-------- internal/models/flag.go | 9 ++++----- internal/models/overrides.go | 5 ++--- internal/scorer/scorer.go | 16 ++++++++-------- 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/internal/database/database.go b/internal/database/database.go index 22a9888..cb2aa80 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -394,10 +394,10 @@ func (db *DataBase) ListOverrides() (overrides []models.OverriddenScore, err err func (db *DataBase) AddOverrideGitlab(gitlabLogin, task string, score int, status models.PipelineStatus) error { overridenScore := &models.OverriddenScore{ - GitlabLogin: gitlabLogin, - Task: task, - Score: score, - Status: status, + Login: gitlabLogin, + Task: task, + Score: score, + Status: status, } return db.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "gitlab_login"}, {Name: "task"}}, @@ -407,10 +407,10 @@ func (db *DataBase) AddOverrideGitlab(gitlabLogin, task string, score int, statu func (db *DataBase) AddOverrideGitea(giteaLogin, task string, score int, status models.PipelineStatus) error { overridenScore := &models.OverriddenScore{ - GiteaLogin: giteaLogin, - Task: task, - Score: score, - Status: status, + Login: giteaLogin, + Task: task, + Score: score, + Status: status, } return db.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "gitea_login"}, {Name: "task"}}, diff --git a/internal/models/flag.go b/internal/models/flag.go index 6883ae9..deed0ea 100644 --- a/internal/models/flag.go +++ b/internal/models/flag.go @@ -3,9 +3,8 @@ package models import "time" type Flag struct { - ID string `gorm:"primaryKey"` - Task string `gorm:"index"` - GitlabLogin *string `gorm:"index"` - GiteaLogin *string `gorm:"index"` - CreatedAt time.Time + ID string `gorm:"primaryKey"` + Task string `gorm:"index"` + Login *string `gorm:"index"` + CreatedAt time.Time } diff --git a/internal/models/overrides.go b/internal/models/overrides.go index 092cf74..8af3af0 100644 --- a/internal/models/overrides.go +++ b/internal/models/overrides.go @@ -7,9 +7,8 @@ import ( type OverriddenScore struct { gorm.Model - GitlabLogin string `gorm:"uniqueIndex:idx_overrides"` - GiteaLogin string `gorm:"uniqueIndex:idx_overrides"` - Task string `gorm:"uniqueIndex:idx_overrides"` + Login string `gorm:"uniqueIndex:idx_overrides"` + Task string `gorm:"uniqueIndex:idx_overrides"` Score int Status PipelineStatus diff --git a/internal/scorer/scorer.go b/internal/scorer/scorer.go index df72bcb..47dedce 100644 --- a/internal/scorer/scorer.go +++ b/internal/scorer/scorer.go @@ -75,7 +75,7 @@ type pipelinesMap map[string]*models.Pipeline type flagsMap map[string]*models.Flag type pipelinesProvider = func(project string) (pipelines []models.Pipeline, err error) -type flagsProvider = func(gitlabLogin string) (flags []models.Flag, err error) +type flagsProvider = func(login string) (flags []models.Flag, err error) func (s Scorer) loadUserPipelines(user *models.User, provider pipelinesProvider) (pipelinesMap, error) { pipelines, err := provider(s.projects.MakeProjectName(user)) @@ -96,7 +96,7 @@ func (s Scorer) loadUserPipelines(user *models.User, provider pipelinesProvider) } func (s Scorer) loadUserFlags(user *models.User, provider flagsProvider) (flagsMap, error) { - flags, err := provider(*user.GitlabLogin) + flags, err := provider(*s.db.UserLogin(user)) if err != nil { return nil, errors.Wrap(err, "Failed to list user flags") } @@ -211,16 +211,16 @@ func (s Scorer) makeCachedFlagsProvider() (flagsProvider, error) { flagsMap := make(map[string][]models.Flag) for _, flag := range flags { - prev, found := flagsMap[*flag.GitlabLogin] + prev, found := flagsMap[*flag.Login] if !found { prev = make([]models.Flag, 0, 1) } prev = append(prev, flag) - flagsMap[*flag.GitlabLogin] = prev + flagsMap[*flag.Login] = prev } - return func(gitlabLogin string) (flags []models.Flag, err error) { - return flagsMap[gitlabLogin], nil + return func(login string) (flags []models.Flag, err error) { + return flagsMap[login], nil }, nil } @@ -247,7 +247,7 @@ func parseOverrides(overrides []models.OverriddenScore) (result map[overrideKey] result = make(map[overrideKey]*models.OverriddenScore) for i := range overrides { result[overrideKey{ - login: overrides[i].GitlabLogin, + login: overrides[i].Login, task: overrides[i].Task, }] = &overrides[i] } @@ -319,7 +319,7 @@ func (s Scorer) calcUserScoresImpl(currentDeadlines *deadlines.Deadlines, user * } } - override, found := overrides[overrideKey{login: *user.GitlabLogin, task: task.Task}] + override, found := overrides[overrideKey{login: *s.db.UserLogin(user), task: task.Task}] if found { tasks[i].Score = override.Score tasks[i].Status = ClassifyPipelineStatus(override.Status) From b34cdad4943e92f74bc3968c862eadd5e3933dae Mon Sep 17 00:00:00 2001 From: Kirill Shaprunov Date: Sun, 12 May 2024 08:19:25 +0300 Subject: [PATCH 10/14] [nmt/internal] TelegramBot Gitea support | render signup button with platform --- internal/config/config.go | 4 ++-- internal/platform/client_fabric.go | 4 ++-- internal/platform/pipelines_fabric.go | 4 ++-- internal/tgbot/bot.go | 11 ++++++++--- internal/web/render.go | 1 + internal/web/run.go | 2 +- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 1e33f81..47d058d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -9,8 +9,8 @@ import ( ) const ( - GitlabMode = "gitlab" - GiteaMode = "gitea" + GitlabMode = "GitLab" + GiteaMode = "Gitea" ) type GitLabConfig struct { diff --git a/internal/platform/client_fabric.go b/internal/platform/client_fabric.go index 34f89cf..c3ec7bb 100644 --- a/internal/platform/client_fabric.go +++ b/internal/platform/client_fabric.go @@ -17,7 +17,7 @@ import ( func NewClient(conf *config.Config, logger *zap.Logger) (base.ClientInterface, error) { switch conf.Platform.Mode { - case "gitlab": + case config.GitlabMode: client, err := gitlab.NewClient(conf.Platform.GitLab.Api.Token, gitlab.WithBaseURL(conf.Platform.GitLab.BaseURL)) if err != nil { return nil, errors.Wrap(err, "Failed to create gitlab client") @@ -30,7 +30,7 @@ func NewClient(conf *config.Config, logger *zap.Logger) (base.ClientInterface, e }, Gitlab: client, }, nil - case "gitea": + case config.GiteaMode: client, err := gitea.NewClient(conf.Platform.Gitea.BaseURL, gitea.SetToken(conf.Platform.Gitea.Api.Token)) if err != nil { return nil, errors.Wrap(err, "Failed to create gitea client") diff --git a/internal/platform/pipelines_fabric.go b/internal/platform/pipelines_fabric.go index dd704b7..ad779ee 100644 --- a/internal/platform/pipelines_fabric.go +++ b/internal/platform/pipelines_fabric.go @@ -14,7 +14,7 @@ import ( func NewPipelinesFetcher(conf *config.Config, client base.ClientInterface, db *database.DataBase) (base.PipelinesFetcherInterface, error) { switch conf.Platform.Mode { - case "gitlab": + case config.GitlabMode: client_gitlab, done := client.(*gitlab_client.ClientGitlab) if !done { return nil, errors.Wrap(nil, "failed to cast client to gitlab") @@ -27,7 +27,7 @@ func NewPipelinesFetcher(conf *config.Config, client base.ClientInterface, db *d Db: db, }, }, nil - case "gitea": + case config.GiteaMode: client_gitea, done := client.(*gitea_client.ClientGitea) if !done { return nil, errors.Wrap(nil, "failed to cast client to gitlab") diff --git a/internal/tgbot/bot.go b/internal/tgbot/bot.go index d7f1ca5..82010cb 100644 --- a/internal/tgbot/bot.go +++ b/internal/tgbot/bot.go @@ -16,10 +16,10 @@ import ( type Bot struct { bot *tgbotapi.BotAPI log *zap.Logger - db *database.DataBase + db *database.DataBaseProxy } -func NewBot(conf *config.Config, log *zap.Logger, db *database.DataBase) (*Bot, error) { +func NewBot(conf *config.Config, log *zap.Logger, db *database.DataBaseProxy) (*Bot, error) { bot, err := tgbotapi.NewBotAPI(conf.Telegram.BotToken) if err != nil { return nil, err @@ -99,11 +99,16 @@ func (b *Bot) handleWhois(update tgbotapi.Update) error { var err error switch system { - case "gitlab": + case config.GitlabMode: user, err = b.db.FindUserByGitlabLogin(name) if err != nil { return err } + case config.GiteaMode: + user, err = b.db.FindUserByGiteaLogin(name) + if err != nil { + return err + } default: return b.ReplyTo(update, fmt.Sprintf("Unknown system %s", system)) } diff --git a/internal/web/render.go b/internal/web/render.go index a8ad617..7b5c7a2 100644 --- a/internal/web/render.go +++ b/internal/web/render.go @@ -20,6 +20,7 @@ func (s *server) RenderSignupPage(c *gin.Context, err string) { "CourseName": "HSE Advanced C++", "Config": s.config, "ErrorMessage": err, + "Platform": s.config.Platform.Mode, }) } diff --git a/internal/web/run.go b/internal/web/run.go index adbacf3..f936e96 100644 --- a/internal/web/run.go +++ b/internal/web/run.go @@ -54,7 +54,7 @@ func Run() error { return errors.Wrap(err, "Failed to open database") } - bot, err := tgbot.NewBot(config, logger.Named("tgbot"), db) + bot, err := tgbot.NewBot(config, logger.Named("tgbot"), &db_proxy) if err != nil { return errors.Wrap(err, "failed to create telegram bot") } From 8e63e4d4901cb5e026df11cdb34a274ae047849a Mon Sep 17 00:00:00 2001 From: Kirill Shaprunov Date: Sun, 12 May 2024 08:24:02 +0300 Subject: [PATCH 11/14] [nmt/internal] Signup page --- web/signup.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/signup.tmpl b/web/signup.tmpl index 5c961d7..89b80cd 100644 --- a/web/signup.tmpl +++ b/web/signup.tmpl @@ -57,12 +57,12 @@ {{ end }}
- +
From 293d6613ccd0c22ab956d365e801c3916d67c11a Mon Sep 17 00:00:00 2001 From: Kirill Shaprunov Date: Sun, 12 May 2024 08:42:40 +0300 Subject: [PATCH 12/14] [nmt/internal] Erase fields of platforms --- internal/config/config.go | 18 ++++++++---------- internal/platform/gitea/gitea_client.go | 4 ++-- internal/platform/gitlab/gitlab_client.go | 4 ++-- internal/web/render.go | 2 +- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 47d058d..19313fc 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,8 +19,6 @@ type GitLabConfig struct { Name string ID int } - DefaultReadme string - TaskUrlPrefix string Application struct { ClientID string @@ -38,8 +36,6 @@ type GiteaConfig struct { Name string ID int } - DefaultReadme string - TaskUrlPrefix string Application struct { ClientID string @@ -51,6 +47,14 @@ type GiteaConfig struct { CIConfigPath string } +type PlatformConfig struct { + GitLab GitLabConfig + Gitea GiteaConfig + Mode string + TaskUrlPrefix string + DefaultReadme string +} + type EndpointsConfig struct { HostName string Home string @@ -124,12 +128,6 @@ type TelegramBotConfig struct { BotToken string } -type PlatformConfig struct { - GitLab GitLabConfig - Gitea GiteaConfig - Mode string -} - type Config struct { Log log.Config diff --git a/internal/platform/gitea/gitea_client.go b/internal/platform/gitea/gitea_client.go index 236c414..89305c0 100644 --- a/internal/platform/gitea/gitea_client.go +++ b/internal/platform/gitea/gitea_client.go @@ -41,7 +41,7 @@ func (c *ClientGitea) InitializeProject(user *models.User) error { opts := gitea.CreateRepoOption{ Name: projectName, - Description: c.Config.Platform.Gitea.DefaultReadme, + Description: c.Config.Platform.DefaultReadme, DefaultBranch: base.Master, Private: true, AutoInit: true, @@ -151,7 +151,7 @@ func (c *ClientGitea) MakeBranchURL(user *models.User, pipeline *models.Pipeline } func (c *ClientGitea) MakeTaskURL(task string) string { - return fmt.Sprintf("%s/%s", c.Config.Platform.Gitea.TaskUrlPrefix, task) + return fmt.Sprintf("%s/%s", c.Config.Platform.TaskUrlPrefix, task) } var _ base.ClientInterface = &ClientGitea{} diff --git a/internal/platform/gitlab/gitlab_client.go b/internal/platform/gitlab/gitlab_client.go index 77753b7..60d4094 100644 --- a/internal/platform/gitlab/gitlab_client.go +++ b/internal/platform/gitlab/gitlab_client.go @@ -70,7 +70,7 @@ func (c *ClientGitlab) InitializeProject(user *models.User) error { Actions: []*gitlab.CommitActionOptions{{ Action: gitlab.FileAction(gitlab.FileCreate), FilePath: gitlab.String("README.md"), - Content: gitlab.String(c.Config.Platform.GitLab.DefaultReadme), + Content: gitlab.String(c.Config.Platform.DefaultReadme), }}, }) @@ -172,7 +172,7 @@ func (c *ClientGitlab) MakeBranchURL(user *models.User, pipeline *models.Pipelin } func (c *ClientGitlab) MakeTaskURL(task string) string { - return fmt.Sprintf("%s/%s", c.Config.Platform.GitLab.TaskUrlPrefix, task) + return fmt.Sprintf("%s/%s", c.Config.Platform.TaskUrlPrefix, task) } var _ base.ClientInterface = &ClientGitlab{} diff --git a/internal/web/render.go b/internal/web/render.go index 7b5c7a2..3dc6343 100644 --- a/internal/web/render.go +++ b/internal/web/render.go @@ -119,7 +119,7 @@ func (s *server) makeLinks(user *models.User) *Links { return &Links{ Deadlines: s.config.Endpoints.Home, Standings: s.config.Endpoints.Standings, - TasksRepository: s.config.Platform.GitLab.TaskUrlPrefix, + TasksRepository: s.config.Platform.TaskUrlPrefix, Repository: s.client.MakeProjectURL(user), Submits: s.client.MakeProjectSubmitsURL(user), Logout: s.config.Endpoints.Logout, From 9f755e9fc5c7f3c966478d45a22a11b643d48705 Mon Sep 17 00:00:00 2001 From: Kirill Shaprunov Date: Wed, 15 May 2024 04:17:27 +0300 Subject: [PATCH 13/14] [nmt/internal] Create action.yml after user repository | some fixes --- internal/config/config.go | 1 + internal/platform/gitea/gitea_client.go | 25 ++++++++++++++++++++-- internal/platform/gitea/gitea_pipelines.go | 7 +++--- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 19313fc..49842f2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -44,6 +44,7 @@ type GiteaConfig struct { Api struct { Token string } + CIConfig string CIConfigPath string } diff --git a/internal/platform/gitea/gitea_client.go b/internal/platform/gitea/gitea_client.go index 89305c0..03ecd5b 100644 --- a/internal/platform/gitea/gitea_client.go +++ b/internal/platform/gitea/gitea_client.go @@ -1,6 +1,7 @@ package gitea import ( + "encoding/base64" "fmt" "net/http" @@ -45,7 +46,6 @@ func (c *ClientGitea) InitializeProject(user *models.User) error { DefaultBranch: base.Master, Private: true, AutoInit: true, - Template: true, } repo, _, err = c.Gitea.CreateOrgRepo(c.Config.Platform.Gitea.Organization.Name, opts) if err != nil { @@ -62,8 +62,29 @@ func (c *ClientGitea) InitializeProject(user *models.User) error { log.Info("Found existing repository") } - // Protect master branch from unintended commits + // Prepare action.yml with CI configuration + create_file_opts := gitea.CreateFileOptions{ + FileOptions: gitea.FileOptions{ + Message: "Initialize repo", + BranchName: base.Master, + Author: gitea.Identity{ + Name: "notmanytask", + Email: "mail@notmanytask.org", + }, + Committer: gitea.Identity{ + Name: "notmanytask", + Email: "mail@notmanytask.org", + }, + }, + Content: base64.StdEncoding.EncodeToString([]byte(c.Config.Platform.Gitea.CIConfig)), + } + _, _, err = c.Gitea.CreateFile(c.Config.Platform.Gitea.Organization.Name, repo.Name, c.Config.Platform.Gitea.CIConfigPath, create_file_opts) + if err != nil { + log.Error("Failed to create CI configuration", zap.Error(err)) + return errors.Wrap(err, "Failed to create CI configuration") + } + // Protect master branch from unintended commits branch_protection_opts := gitea.CreateBranchProtectionOption{ BranchName: base.Master, EnablePush: false, diff --git a/internal/platform/gitea/gitea_pipelines.go b/internal/platform/gitea/gitea_pipelines.go index 5983acd..c062e99 100644 --- a/internal/platform/gitea/gitea_pipelines.go +++ b/internal/platform/gitea/gitea_pipelines.go @@ -101,9 +101,10 @@ func (p *PipelinesFetcherGitea) GetPipelines(project string) ([]PipelineInfoGite p.Logger.Error("Failed to request pipelines via API", zap.Error(err)) return nil, errors.Wrap(err, "Failed to make pipelines fetch request itself") } - + // Make page iteration TODO(shaprunovk) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "token "+p.Config.Platform.Gitea.Api.Token) + // req.SetPathValue("page", fmt.Sprint(1)) client := &http.Client{} resp, err := client.Do(req) @@ -166,7 +167,7 @@ func (p *PipelinesFetcherGitea) FetchAllPipelines() { err := p.ForEachProject(func(project base.Project) error { project_gitea, cast := project.(*gitea.Repository) if !cast { - return errors.New("Failed to cast project to gitlab project") + return errors.New("Failed to cast project to gitea project") } p.Logger.Debug("Found project", lf.ProjectName(project_gitea.Name)) pipelines, err := p.GetPipelines(project_gitea.Name) @@ -226,7 +227,7 @@ func (p *PipelinesFetcherGitea) FetchFreshPipelines() { info, err := p.Fetch(id.Id, id.Project) info_gitea, cast := info.(*PipelineInfoGitea) if !cast { - p.Logger.Error("Failed to cast pipeline info to gitlab pipeline info", lf.ProjectName(id.Project), lf.PipelineID(id.Id)) + p.Logger.Error("Failed to cast pipeline info to gitea pipeline info", lf.ProjectName(id.Project), lf.PipelineID(id.Id)) } if err != nil { p.Logger.Error("Failed to fetch pipeline", zap.Error(err)) From 99fdb1d782eb79cf54ef1ffc8c7aafe12f26e36c Mon Sep 17 00:00:00 2001 From: Kirill Shaprunov Date: Sun, 19 May 2024 15:07:34 +0300 Subject: [PATCH 14/14] [nmt/internal] Some fixes --- internal/web/api.go | 8 ++++---- internal/web/login.go | 2 +- internal/web/render.go | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/web/api.go b/internal/web/api.go index 6563625..a39a6a7 100644 --- a/internal/web/api.go +++ b/internal/web/api.go @@ -86,7 +86,7 @@ func (s apiService) report(c *gin.Context) { } func (s apiService) createFlag(c *gin.Context) { - s.log.Info("Handling crasme flag request") + s.log.Info("Handling crashme flag request") onError := func(code int, err error) { s.log.Warn("Failed to create flag for crasme", zap.Error(err)) c.JSON(code, &api.FlagResponse{ @@ -160,7 +160,7 @@ func (s apiService) override(c *gin.Context) { zap.String("status", req.Status), ) - _, err := s.server.db.FindUserByGitlabLogin(req.Login) + _, err := s.server.db.FindUserByLogin(req.Login) if err != nil { s.log.Error("Failed to get user by login", lf.GitlabLogin(req.Login)) onError(http.StatusNotFound, fmt.Errorf("not found user")) @@ -217,7 +217,7 @@ func (s apiService) changeGroup(c *gin.Context) { return } - user, err := s.server.db.FindUserByGitlabLogin(req.Login) + user, err := s.server.db.FindUserByLogin(req.Login) if err != nil { s.log.Error("Failed to get user by login", lf.GitlabLogin(req.Login)) onError(http.StatusNotFound, fmt.Errorf("not found user")) @@ -270,7 +270,7 @@ func (s apiService) userScores(c *gin.Context) { return } - user, err := s.server.db.FindUserByGitlabLogin(req.Login) + user, err := s.server.db.FindUserByLogin(req.Login) if err != nil { s.log.Error("Failed to get user by login", lf.GitlabLogin(req.Login)) onError(http.StatusNotFound, fmt.Errorf("not found user")) diff --git a/internal/web/login.go b/internal/web/login.go index 082438e..bf5c61c 100644 --- a/internal/web/login.go +++ b/internal/web/login.go @@ -483,7 +483,7 @@ func (s *server) validateSession(verifyTelegram bool) func(c *gin.Context) { func (s *server) fillUserFromQuery(c *gin.Context) { login := c.Query("login") - user, err := s.db.FindUserByGitlabLogin(login) + user, err := s.db.FindUserByLogin(login) if err != nil { s.logger.Warn("Failed to find user", lf.GitlabLogin(login)) c.Redirect(http.StatusTemporaryRedirect, s.config.Endpoints.Signup) diff --git a/internal/web/render.go b/internal/web/render.go index 3dc6343..c58ddec 100644 --- a/internal/web/render.go +++ b/internal/web/render.go @@ -144,7 +144,7 @@ func (s *server) RenderHomePage(c *gin.Context) { } func (s *server) RenderCheaterPage(c *gin.Context) { - user, err := s.db.FindUserByGitlabLogin(c.Query("login")) + user, err := s.db.FindUserByLogin(c.Query("login")) var scores *scorer.UserScores if err == nil { scores, err = s.scorer.CalcUserScores(user) @@ -206,7 +206,7 @@ func (s *server) RenderRetakesPage(c *gin.Context) { func (s *server) RenderStandingsCheaterPage(c *gin.Context) { group := "hse" - user, _ := s.db.FindUserByGitlabLogin(c.Query("login")) + user, _ := s.db.FindUserByLogin(c.Query("login")) scores, err := s.scorer.CalcScoreboard(group) reverseScoreboardGroups(scores) c.HTML(http.StatusOK, "standings.tmpl", gin.H{