package api import ( gitea "code.gitea.io/sdk/gitea" "encoding/json" "fmt" "git.lumeweb.com/LumeWeb/gitea-github-proxy/config" "github.com/google/go-github/v59/github" "github.com/gorilla/mux" "go.uber.org/zap" "net/http" "strconv" ) type restApi struct { config *config.Config logger *zap.Logger } func newRestApi(cfg *config.Config, logger *zap.Logger) *restApi { return &restApi{config: cfg, logger: logger} } func (r restApi) handlerGetPullRequestFiles(w http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) owner := vars["owner"] repo := vars["repo"] pullNumber := vars["pull_number"] client := r.getClientOrError(w) if client == nil { return } parsedPullNumber, err := strconv.ParseInt(pullNumber, 10, 64) if err != nil { http.Error(w, "Failed to parse pull number", http.StatusBadRequest) r.logger.Error("Failed to parse pull number", zap.Error(err)) return } files, r2, err := client.ListPullRequestFiles(owner, repo, parsedPullNumber, gitea.ListPullRequestFilesOptions{ ListOptions: r.getPagingOptions(request), }) if err != nil { http.Error(w, "Failed to get pull request files", http.StatusInternalServerError) r.logger.Error("Failed to get pull request files", zap.Error(err)) return } githubFiles := make([]*github.CommitFile, len(files)) for i, file := range files { githubFiles[i] = convertCommitFile(file) } r.sendPagingHeaders(w, r2) r.respond(w, http.StatusOK, githubFiles) } func (r restApi) getPagingOptions(request *http.Request) gitea.ListOptions { page, _ := strconv.Atoi(request.URL.Query().Get("page")) perPage, _ := strconv.Atoi(request.URL.Query().Get("per_page")) return gitea.ListOptions{ Page: page, PageSize: perPage, } } func (r restApi) sendPagingHeaders(w http.ResponseWriter, apiResponse *gitea.Response) { links := []string{} baseURL := fmt.Sprintf("https://%s", r.config.Domain) joinStrings := func(elements []string, separator string) string { if len(elements) == 0 { return "" } result := elements[0] for i := 1; i < len(elements); i++ { result += separator + elements[i] } return result } joinLinks := func(links []string) string { return fmt.Sprintf("%s", joinStrings(links, ", ")) } if apiResponse.FirstPage > 0 { links = append(links, fmt.Sprintf(`<%s?page=%d>; rel="first"`, baseURL, apiResponse.FirstPage)) } if apiResponse.PrevPage > 0 { links = append(links, fmt.Sprintf(`<%s?page=%d>; rel="prev"`, baseURL, apiResponse.PrevPage)) } if apiResponse.NextPage > 0 { links = append(links, fmt.Sprintf(`<%s?page=%d>; rel="next"`, baseURL, apiResponse.NextPage)) } if apiResponse.LastPage > 0 { links = append(links, fmt.Sprintf(`<%s?page=%d>; rel="last"`, baseURL, apiResponse.LastPage)) } if len(links) > 0 { w.Header().Add("Link", fmt.Sprintf("%s", joinLinks(links))) } } func (r restApi) respond(w http.ResponseWriter, status int, data interface{}) { jsonData, err := json.Marshal(data) if err != nil { http.Error(w, "Failed to marshal response", http.StatusInternalServerError) r.logger.Error("Failed to marshal response", zap.Error(err)) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if data != nil { _, _ = w.Write(jsonData) } } func (r restApi) getClientOrError(w http.ResponseWriter) *gitea.Client { client, err := getClient(ClientParams{ Config: r.config, }) if err != nil { http.Error(w, "Failed to get Gitea client", http.StatusInternalServerError) r.logger.Error("Failed to get Gitea client", zap.Error(err)) return nil } return client } func (r restApi) handlerGetTree(w http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) owner := vars["owner"] repo := vars["repo"] treeSha := vars["tree_sha"] recursive := false if request.URL.Query().Has("recursive") { recursive = true } client := r.getClientOrError(w) if client == nil { return } tree, r2, err := client.GetTrees(owner, repo, treeSha, recursive) if err != nil { http.Error(w, "Failed to get tree", http.StatusInternalServerError) r.logger.Error("Failed to get tree", zap.Error(err)) return } treeResponse := convertGitTree(tree) r.sendPagingHeaders(w, r2) r.respond(w, http.StatusOK, treeResponse) } func (r restApi) handleGetIssueComments(w http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) owner := vars["owner"] repo := vars["repo"] issueNumber := vars["issue_number"] client := r.getClientOrError(w) if client == nil { return } issueNumberInt, err := strconv.Atoi(issueNumber) if err != nil { http.Error(w, "Failed to parse issue number", http.StatusBadRequest) r.logger.Error("Failed to parse issue number", zap.Error(err)) return } comments, r2, err := client.ListIssueComments(owner, repo, int64(issueNumberInt), gitea.ListIssueCommentOptions{ListOptions: r.getPagingOptions(request)}) if err != nil { http.Error(w, "Failed to get issue comments", http.StatusInternalServerError) r.logger.Error("Failed to get issue comments", zap.Error(err)) return } ghComments := make([]*github.IssueComment, len(comments)) for i, comment := range comments { reactions, _, _ := client.GetIssueCommentReactions(owner, repo, comment.ID) ghComments[i] = convertIssueComment(comment, reactions) } r.sendPagingHeaders(w, r2) r.respond(w, http.StatusOK, ghComments) } func (r restApi) handlerCreateIssueComment(w http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) owner := vars["owner"] repo := vars["repo"] issueNumber := vars["issue_number"] client := r.getClientOrError(w) if client == nil { return } ghComment := github.IssueComment{} if err := r.parseJsonBody(request, &ghComment); err != nil { http.Error(w, "Failed to parse request body", http.StatusBadRequest) r.logger.Error("Failed to parse request body", zap.Error(err)) } issueNumberInt, err := strconv.Atoi(issueNumber) if err != nil { http.Error(w, "Failed to parse issue number", http.StatusBadRequest) r.logger.Error("Failed to parse issue number", zap.Error(err)) return } comment := gitea.CreateIssueCommentOption{ Body: ghComment.GetBody(), } commentResponse, _, err := client.CreateIssueComment(owner, repo, int64(issueNumberInt), comment) if err != nil { http.Error(w, "Failed to create comment", http.StatusInternalServerError) r.logger.Error("Failed to create comment", zap.Error(err)) return } r.respond(w, http.StatusCreated, commentResponse) } func (r restApi) handlerUpdateIssueComment(w http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) owner := vars["owner"] repo := vars["repo"] commentID := vars["comment_id"] client := r.getClientOrError(w) if client == nil { return } ghComment := github.IssueComment{} if err := r.parseJsonBody(request, &ghComment); err != nil { http.Error(w, "Failed to parse request body", http.StatusBadRequest) r.logger.Error("Failed to parse request body", zap.Error(err)) } commentIDInt, err := strconv.Atoi(commentID) if err != nil { http.Error(w, "Failed to parse comment ID", http.StatusBadRequest) r.logger.Error("Failed to parse comment ID", zap.Error(err)) return } comment := gitea.EditIssueCommentOption{ Body: ghComment.GetBody(), } commentResponse, _, err := client.EditIssueComment(owner, repo, int64(commentIDInt), comment) if err != nil { http.Error(w, "Failed to update comment", http.StatusInternalServerError) r.logger.Error("Failed to update comment", zap.Error(err)) return } r.respond(w, http.StatusOK, commentResponse) } func (r restApi) handlerCreateRelease(w http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) owner := vars["owner"] repo := vars["repo"] client := r.getClientOrError(w) if client == nil { return } ghRelease := github.RepositoryRelease{} if err := r.parseJsonBody(request, &ghRelease); err != nil { http.Error(w, "Failed to parse request body", http.StatusBadRequest) r.logger.Error("Failed to parse request body", zap.Error(err)) } release := gitea.CreateReleaseOption{ TagName: ghRelease.GetTagName(), Target: ghRelease.GetTargetCommitish(), Title: ghRelease.GetName(), Note: ghRelease.GetBody(), IsDraft: ghRelease.GetDraft(), IsPrerelease: ghRelease.GetPrerelease(), } releaseResponse, r2, err := client.CreateRelease(owner, repo, release) if err != nil { http.Error(w, "Failed to create release", http.StatusInternalServerError) r.logger.Error("Failed to create release", zap.Error(err)) return } r.sendPagingHeaders(w, r2) r.respond(w, http.StatusCreated, releaseResponse) } func (r restApi) handlerCreatePullRequest(w http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) owner := vars["owner"] repo := vars["repo"] client := r.getClientOrError(w) if client == nil { return } ghPullRequest := github.NewPullRequest{} if err := r.parseJsonBody(request, &ghPullRequest); err != nil { http.Error(w, "Failed to parse request body", http.StatusBadRequest) r.logger.Error("Failed to parse request body", zap.Error(err)) } pullRequest := gitea.CreatePullRequestOption{ Title: ghPullRequest.GetTitle(), Head: ghPullRequest.GetHead(), Base: ghPullRequest.GetBase(), Body: ghPullRequest.GetBody(), } pullRequestResponse, r2, err := client.CreatePullRequest(owner, repo, pullRequest) if err != nil { http.Error(w, "Failed to create pull request", http.StatusInternalServerError) r.logger.Error("Failed to create pull request", zap.Error(err)) return } r.sendPagingHeaders(w, r2) r.respond(w, http.StatusCreated, pullRequestResponse) } func (r restApi) parseJsonBody(request *http.Request, obj interface{}) error { if obj == nil { obj = make(map[string]interface{}) } decoder := json.NewDecoder(request.Body) return decoder.Decode(obj) } func setupRestRoutes(params RouteParams) { logger := params.Logger cfg := params.Config r := params.R restApi := newRestApi(cfg, logger) setupRoutes := func(r *mux.Router) { r.HandleFunc("/repos/{owner}/{repo}/pulls/{pull_number}/files", restApi.handlerGetPullRequestFiles).Methods("GET") r.HandleFunc("/repos/{owner}/{repo}/git/trees/{tree_sha}", restApi.handlerGetTree).Methods("GET") // Comment routes r.HandleFunc("/repos/{owner}/{repo}/issues/{issue_number}/comments", restApi.handleGetIssueComments).Methods("GET") r.HandleFunc("/repos/{owner}/{repo}/issues/{issue_number}/comments", restApi.handlerCreateIssueComment).Methods("POST") r.HandleFunc("/repos/{owner}/{repo}/issues/comments/{comment_id}", restApi.handlerUpdateIssueComment).Methods("PATCH") // Repo Release routes r.HandleFunc("/repos/{owner}/{repo}/releases", restApi.handlerCreateRelease).Methods("POST") // Pull Request routes r.HandleFunc("/repos/{owner}/{repo}/pulls", restApi.handlerCreatePullRequest).Methods("POST") } restRouter := r.PathPrefix("/api").Subrouter() restRouter.Use(githubRestVerifyMiddleware(params.Db)) restRouter.Use(githubRestRequireAuthMiddleware(params.Config)) setupRoutes(restRouter) setupRoutes(restRouter.PathPrefix("/v3").Subrouter()) }