From 6504339a03e73e9f8ee3ae8562557b1f67af8a17 Mon Sep 17 00:00:00 2001 From: "xiaobing.wang" Date: Wed, 29 Nov 2023 10:40:47 +0800 Subject: [PATCH] feat: add star count --- Dockerfile | 14 ++--- backend/docs/docs.go | 89 +++++++++++++++++++++++++++- backend/docs/swagger.json | 89 +++++++++++++++++++++++++++- backend/docs/swagger.yaml | 59 +++++++++++++++++- backend/internal/handler/github.go | 17 ++++++ backend/internal/handler/safeline.go | 36 +++++++++++ backend/internal/service/github.go | 38 ++++++++++++ backend/internal/service/safeline.go | 56 +++++++++++++++++ backend/main.go | 16 +++-- 9 files changed, 394 insertions(+), 20 deletions(-) create mode 100644 backend/internal/handler/safeline.go create mode 100644 backend/internal/service/safeline.go diff --git a/Dockerfile b/Dockerfile index 467152a..d659c6e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,15 @@ FROM golang:1.21 as go-builder WORKDIR /work -COPY backend . ENV GOPROXY=https://goproxy.cn,direct -RUN go mod tidy +COPY backend/go.mod . +COPY backend/go.sum . +RUN go mod download +COPY backend . RUN CGO_ENABLED=0 go build -a -v -ldflags="-w" -o server . FROM node:20.5-alpine -ARG telemetry - RUN apk update RUN apk add nginx supervisor curl @@ -17,12 +17,6 @@ RUN echo -e " server { \n\ listen 80; \n\ \n\ - location /api/count { \n\ - proxy_pass $telemetry; \n\ - } \n\ - location /api/exist { \n\ - proxy_pass $telemetry; \n\ - } \n\ location /api/ { \n\ proxy_pass http://localhost:8080; \n\ } \n\ diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 77e3707..9896712 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -49,6 +49,29 @@ const docTemplate = `{ } } }, + "/repos/info": { + "get": { + "description": "get repo info from GitHub", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "GitHub" + ], + "summary": "get repo info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.Repo" + } + } + } + } + }, "/repos/issues": { "get": { "description": "get issues from GitHub", @@ -82,17 +105,57 @@ const docTemplate = `{ } } } + }, + "/safeline/count": { + "get": { + "description": "get installer count", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Safeline" + ], + "summary": "get installer count", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.InstallerCount" + } + } + } + } } }, "definitions": { + "service.Category": { + "type": "object", + "properties": { + "emoji": { + "type": "string" + }, + "emoji_html": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "service.Discussion": { "type": "object", "properties": { "author": { "$ref": "#/definitions/service.User" }, - "category_name": { - "type": "string" + "category": { + "$ref": "#/definitions/service.Category" }, "comment_count": { "type": "integer" @@ -126,6 +189,17 @@ const docTemplate = `{ }, "upvote_count": { "type": "integer" + }, + "url": { + "type": "string" + } + } + }, + "service.InstallerCount": { + "type": "object", + "properties": { + "total": { + "type": "integer" } } }, @@ -172,6 +246,17 @@ const docTemplate = `{ } } }, + "service.Repo": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "star_count": { + "type": "integer" + } + } + }, "service.User": { "type": "object", "properties": { diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index d31450e..4097e09 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -38,6 +38,29 @@ } } }, + "/repos/info": { + "get": { + "description": "get repo info from GitHub", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "GitHub" + ], + "summary": "get repo info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.Repo" + } + } + } + } + }, "/repos/issues": { "get": { "description": "get issues from GitHub", @@ -71,17 +94,57 @@ } } } + }, + "/safeline/count": { + "get": { + "description": "get installer count", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Safeline" + ], + "summary": "get installer count", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.InstallerCount" + } + } + } + } } }, "definitions": { + "service.Category": { + "type": "object", + "properties": { + "emoji": { + "type": "string" + }, + "emoji_html": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "service.Discussion": { "type": "object", "properties": { "author": { "$ref": "#/definitions/service.User" }, - "category_name": { - "type": "string" + "category": { + "$ref": "#/definitions/service.Category" }, "comment_count": { "type": "integer" @@ -115,6 +178,17 @@ }, "upvote_count": { "type": "integer" + }, + "url": { + "type": "string" + } + } + }, + "service.InstallerCount": { + "type": "object", + "properties": { + "total": { + "type": "integer" } } }, @@ -161,6 +235,17 @@ } } }, + "service.Repo": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "star_count": { + "type": "integer" + } + } + }, "service.User": { "type": "object", "properties": { diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 7b603ec..96f5a9b 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -1,10 +1,21 @@ definitions: + service.Category: + properties: + emoji: + type: string + emoji_html: + type: string + id: + type: string + name: + type: string + type: object service.Discussion: properties: author: $ref: '#/definitions/service.User' - category_name: - type: string + category: + $ref: '#/definitions/service.Category' comment_count: type: integer comment_users: @@ -27,6 +38,13 @@ definitions: type: string upvote_count: type: integer + url: + type: string + type: object + service.InstallerCount: + properties: + total: + type: integer type: object service.Issue: properties: @@ -56,6 +74,13 @@ definitions: name: type: string type: object + service.Repo: + properties: + id: + type: string + star_count: + type: integer + type: object service.User: properties: avatar_url: @@ -88,6 +113,21 @@ paths: summary: get discussions tags: - GitHub + /repos/info: + get: + consumes: + - application/json + description: get repo info from GitHub + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service.Repo' + summary: get repo info + tags: + - GitHub /repos/issues: get: consumes: @@ -110,4 +150,19 @@ paths: summary: get issues tags: - GitHub + /safeline/count: + get: + consumes: + - application/json + description: get installer count + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service.InstallerCount' + summary: get installer count + tags: + - Safeline swagger: "2.0" diff --git a/backend/internal/handler/github.go b/backend/internal/handler/github.go index da1183f..1f3885b 100644 --- a/backend/internal/handler/github.go +++ b/backend/internal/handler/github.go @@ -60,3 +60,20 @@ func (h *GitHubHandler) GetDiscussions(c *gin.Context) { c.JSON(http.StatusOK, discussions) } + +// GetRepo handles GET requests for fetching GitHub repo info. +// @Summary get repo info +// @Description get repo info from GitHub +// @Tags GitHub +// @Accept json +// @Produce json +// @Success 200 {object} service.Repo +// @Router /repos/info [get] +func (h *GitHubHandler) GetRepo(c *gin.Context) { + repo, err := h.gitHubService.GetRepo(c) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, repo) +} diff --git a/backend/internal/handler/safeline.go b/backend/internal/handler/safeline.go new file mode 100644 index 0000000..6711e79 --- /dev/null +++ b/backend/internal/handler/safeline.go @@ -0,0 +1,36 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/chaitin/SafeLine/internal/service" +) + +type SafelineHandler struct { + safelineService *service.SafelineService +} + +func NewSafelineHandler(safelineService *service.SafelineService) *SafelineHandler { + return &SafelineHandler{ + safelineService: safelineService, + } +} + +// GetInstallerCount +// @Summary get installer count +// @Description get installer count +// @Tags Safeline +// @Accept json +// @Produce json +// @Success 200 {object} service.InstallerCount +// @Router /safeline/count [get] +func (h *SafelineHandler) GetInstallerCount(c *gin.Context) { + count, err := h.safelineService.GetInstallerCount(c) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(200, count) +} diff --git a/backend/internal/service/github.go b/backend/internal/service/github.go index 9a4e460..cc8466b 100644 --- a/backend/internal/service/github.go +++ b/backend/internal/service/github.go @@ -58,6 +58,11 @@ type Category struct { EmojiHTML string `json:"emoji_html" graphql:"emojiHTML"` } +type Repo struct { + ID string `json:"id"` + StarCount int `json:"star_count"` +} + type GitHubAPI interface { Query(ctx context.Context, q interface{}, variables map[string]interface{}) error } @@ -374,3 +379,36 @@ func (s *GitHubService) fetchDiscussions(ctx context.Context, afterCursor *githu return discussions, nil } + +func (s *GitHubService) GetRepo(ctx context.Context) (*Repo, error) { + if cachedData, found := s.cache.Load("repo"); found { + return cachedData.(*Repo), nil + } + + repo, err := s.fetchRepo(ctx) + if err != nil { + return nil, err + } + + s.cache.Store("repo", repo) + + return repo, nil +} + +func (s *GitHubService) fetchRepo(ctx context.Context) (*Repo, error) { + var query struct { + Repository struct { + ID string + StargazerCount int + } `graphql:"repository(owner: $owner, name: $name)"` + } + variables := map[string]interface{}{ + "owner": githubv4.String(s.owner), + "name": githubv4.String(s.repo), + } + err := s.request(ctx, &query, variables) + if err != nil { + return nil, err + } + return &Repo{ID: query.Repository.ID, StarCount: query.Repository.StargazerCount}, nil +} diff --git a/backend/internal/service/safeline.go b/backend/internal/service/safeline.go new file mode 100644 index 0000000..c9e93bb --- /dev/null +++ b/backend/internal/service/safeline.go @@ -0,0 +1,56 @@ +package service + +import ( + "context" + "crypto/tls" + "encoding/json" + "net/http" + "time" +) + +var cacheCount InstallerCount + +type InstallerCount struct { + Total int `json:"total"` +} + +type SafelineService struct { + client *http.Client + APIHost string +} + +func NewSafelineService(host string) *SafelineService { + return &SafelineService{ + APIHost: host, + client: &http.Client{ + Timeout: 10 * time.Second, + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + }, + } +} + +func (s *SafelineService) GetInstallerCount(ctx context.Context) (InstallerCount, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.APIHost+"/api/v1/public/safeline/count", nil) + if err != nil { + return cacheCount, err + } + res, err := s.client.Do(req) + if err != nil { + return cacheCount, err + } + defer res.Body.Close() + var r map[string]interface{} + if err := json.NewDecoder(res.Body).Decode(&r); err != nil { + return cacheCount, err + } + if r["code"].(float64) != 0 { + return cacheCount, nil + } + cacheCount = InstallerCount{ + Total: int(r["data"].(map[string]interface{})["total"].(float64)), + } + return cacheCount, nil +} diff --git a/backend/main.go b/backend/main.go index c8903eb..56086e8 100644 --- a/backend/main.go +++ b/backend/main.go @@ -16,10 +16,6 @@ import ( func main() { viper.AutomaticEnv() - // variables that must be set - viper.SetDefault("GITHUB_TOKEN", "") - - // optional variables to set viper.SetDefault("GITHUB_CACHE_TTL", 10) // cache timeout in minutes viper.SetDefault("LISTEN_ADDR", ":8080") // api server addr @@ -27,6 +23,12 @@ func main() { if githubToken == "" { log.Fatal("GITHUB_TOKEN must be set") } + + telemetryHost := viper.GetString("TELEMETRY_HOST") + if telemetryHost == "" { + log.Fatal("TELEMETRY_HOST must be set") + } + listenAddr := viper.GetString("LISTEN_ADDR") r := gin.Default() @@ -45,6 +47,12 @@ func main() { v1 := r.Group("/api") v1.GET("/repos/issues", gitHubHandler.GetIssues) v1.GET("/repos/discussions", gitHubHandler.GetDiscussions) + v1.GET("/repos/info", gitHubHandler.GetRepo) + + // Initialize the SafelineService. + safelineService := service.NewSafelineService(telemetryHost) + safelineHandler := handler.NewSafelineHandler(safelineService) + v1.GET("/safeline/count", safelineHandler.GetInstallerCount) docs.SwaggerInfo.BasePath = v1.BasePath() r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))