From 6f256d80bd0503f49c1d3b3c0e3535d8ef31f635 Mon Sep 17 00:00:00 2001 From: Clas Wen Date: Sat, 15 Feb 2025 18:27:11 +0800 Subject: [PATCH 1/8] sync to github --- go.mod | 14 +++-- go.sum | 17 ++++++ migrations/000002_add_github_issue.down.sql | 23 ++++++++ migrations/000002_add_github_issue.up.sql | 24 +++++++++ model/event.go | 21 ++++++++ repo/event.go | 16 ++++-- service/event.go | 58 +++++++++++++++++++++ 7 files changed, 163 insertions(+), 10 deletions(-) create mode 100644 migrations/000002_add_github_issue.down.sql create mode 100644 migrations/000002_add_github_issue.up.sql diff --git a/go.mod b/go.mod index 5a236c4..b34d742 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/nbtca/saturday -go 1.21 +go 1.22.0 -toolchain go1.22.2 +toolchain go1.23.4 require ( github.com/Masterminds/squirrel v1.5.4 @@ -14,9 +14,11 @@ require ( github.com/go-playground/validator/v10 v10.18.0 github.com/go-sql-driver/mysql v1.7.1 github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/google/go-github/v69 v69.1.0 github.com/jmoiron/sqlx v1.3.5 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 + github.com/nao1215/markdown v0.7.0 github.com/nsqio/go-nsq v1.1.0 github.com/ory/dockertest/v3 v3.10.0 github.com/qustavo/sqlhooks/v2 v2.1.0 @@ -31,13 +33,15 @@ require ( github.com/distribution/reference v0.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/karrick/godirwalk v1.17.0 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/kr/text v0.1.0 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect go.opentelemetry.io/otel/trace v1.26.0 // indirect diff --git a/go.sum b/go.sum index d328446..f1750f3 100644 --- a/go.sum +++ b/go.sum @@ -86,9 +86,14 @@ github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v69 v69.1.0 h1:ljzwzEsHsc4qUqyHEJCNA1dMqvoTK3YX2NAaK6iprDg= +github.com/google/go-github/v69 v69.1.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= @@ -105,6 +110,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/karrick/godirwalk v1.17.0 h1:b4kY7nqDdioR/6qnbHQyDvmA17u5G1cZ6J+CZXwSWoI= +github.com/karrick/godirwalk v1.17.0/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -127,6 +134,9 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= @@ -142,8 +152,12 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/nao1215/markdown v0.7.0 h1:SCQkvdQXQuKJW8KaCsvBob8Afy4T4iJUAYIefWpHojE= +github.com/nao1215/markdown v0.7.0/go.mod h1:ObBhnNduWwPN+bu4dtv4JoLRt57ONla7l//03iHIVhY= github.com/nsqio/go-nsq v1.1.0 h1:PQg+xxiUjA7V+TLdXw7nVrJ5Jbl3sN86EhGCQj4+FYE= github.com/nsqio/go-nsq v1.1.0/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= @@ -161,6 +175,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/qustavo/sqlhooks/v2 v2.1.0 h1:54yBemHnGHp/7xgT+pxwmIlMSDNYKx5JW5dfRAiCZi0= github.com/qustavo/sqlhooks/v2 v2.1.0/go.mod h1:aMREyKo7fOKTwiLuWPsaHRXEmtqG4yREztO0idF83AU= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= diff --git a/migrations/000002_add_github_issue.down.sql b/migrations/000002_add_github_issue.down.sql new file mode 100644 index 0000000..4609b9f --- /dev/null +++ b/migrations/000002_add_github_issue.down.sql @@ -0,0 +1,23 @@ +ALTER TABLE public.event +DROP COLUMN github_issue_id; + +ALTER TABLE public.event +DROP COLUMN github_issue_number; + +DROP VIEW public.event_view; +CREATE OR REPLACE VIEW public.event_view AS + SELECT event.event_id, + event.client_id, + event.model, + event.phone, + event.qq, + event.contact_preference, + event.problem, + event.member_id, + event.closed_by, + event.gmt_create, + event.gmt_modified, + COALESCE(event_status.status, ''::character varying) AS status + FROM ((public.event + LEFT JOIN public.event_event_status_relation ON ((event.event_id = event_event_status_relation.event_id))) + LEFT JOIN public.event_status ON ((event_event_status_relation.event_status_id = event_status.event_status_id))); \ No newline at end of file diff --git a/migrations/000002_add_github_issue.up.sql b/migrations/000002_add_github_issue.up.sql new file mode 100644 index 0000000..b79fc31 --- /dev/null +++ b/migrations/000002_add_github_issue.up.sql @@ -0,0 +1,24 @@ +ALTER TABLE public.event +ADD COLUMN github_issue_id BIGINT; + +ALTER TABLE public.event +ADD COLUMN github_issue_number BIGINT; + +CREATE OR REPLACE VIEW public.event_view AS + SELECT event.event_id, + event.client_id, + event.model, + event.phone, + event.qq, + event.contact_preference, + event.problem, + event.member_id, + event.closed_by, + event.gmt_create, + event.gmt_modified, + COALESCE(event_status.status, ''::character varying) AS status, + event.github_issue_id, + event.github_issue_number + FROM ((public.event + LEFT JOIN public.event_event_status_relation ON ((event.event_id = event_event_status_relation.event_id))) + LEFT JOIN public.event_status ON ((event_event_status_relation.event_status_id = event_status.event_status_id))); \ No newline at end of file diff --git a/model/event.go b/model/event.go index ede5f72..ad9c8d3 100644 --- a/model/event.go +++ b/model/event.go @@ -1,8 +1,18 @@ package model +import ( + "bytes" + "database/sql" + "fmt" + + md "github.com/nao1215/markdown" +) + type Event struct { EventId int64 `json:"eventId" db:"event_id"` ClientId int64 `json:"clientId" db:"client_id"` + GithubIssueId sql.NullInt64 `json:"githubIssueId" db:"github_issue_id"` + GithubIssueNumber sql.NullInt64 `json:"githubIssueNumber" db:"github_issue_number"` Model string `json:"model"` Phone string `json:"phone"` QQ string `json:"qq"` @@ -18,6 +28,17 @@ type Event struct { GmtModified string `json:"gmtModified" db:"gmt_modified"` } +func (e Event) ToMarkdownString() string { + buf := new(bytes.Buffer) + markdown := md.NewMarkdown(buf).H2("Description") + markdown.PlainText(e.Problem) + if e.Model != "" { + markdown.BulletList(fmt.Sprintf("Model: %s", e.Model)) + } + markdown.Blockquote("footer") + return markdown.String() +} + type Status struct { StatusId int64 `json:"status_id"` Status string `json:"status"` diff --git a/repo/event.go b/repo/event.go index 2ec19ee..ab5211a 100644 --- a/repo/event.go +++ b/repo/event.go @@ -11,7 +11,7 @@ import ( ) var eventFields = []string{"event_id", "client_id", "model", "phone", "qq", "contact_preference", - "problem", "member_id", "closed_by", "status", "gmt_create", "gmt_modified", "status"} + "problem", "member_id", "closed_by", "status", "gmt_create", "gmt_modified", "status", "github_issue_id", "github_issue_number"} var EventLogFields = []string{"event_log_id", "description", "gmt_create", "member_id", "action"} @@ -120,16 +120,22 @@ func GetClientEvents(f EventFilter, clientId string) ([]model.Event, error) { } func UpdateEvent(event *model.Event, eventLog *model.EventLog) error { - sql, args, _ := sq.Update("event"). + builder := sq.Update("event"). Set("model", event.Model). Set("phone", event.Phone). Set("qq", event.QQ). Set("contact_preference", event.ContactPreference). Set("problem", event.Problem). Set("member_id", event.MemberId). - Set("closed_by", event.ClosedBy). - // Set("gmt_modified", event.GmtModified). - Where(squirrel.Eq{"event_id": event.EventId}).ToSql() + Set("closed_by", event.ClosedBy) + if event.GithubIssueId.Valid { + builder = builder.Set("github_issue_id", event.GithubIssueId.Int64) + } + if event.GithubIssueNumber.Valid { + builder = builder.Set("github_issue_number", event.GithubIssueNumber.Int64) + } + + sql, args, _ := builder.Where(squirrel.Eq{"event_id": event.EventId}).ToSql() conn, err := db.Beginx() if err != nil { return err diff --git a/service/event.go b/service/event.go index c88688a..04d6768 100644 --- a/service/event.go +++ b/service/event.go @@ -1,11 +1,16 @@ package service import ( + "bytes" + "database/sql" "fmt" "net/http" "net/rpc" "os" + "github.com/google/go-github/v69/github" + md "github.com/nao1215/markdown" + "github.com/nbtca/saturday/model" "github.com/nbtca/saturday/repo" "github.com/nbtca/saturday/util" @@ -145,6 +150,53 @@ func (service EventService) SendActionNotifyViaMail(event *model.Event, subject return nil } +func syncEventActionToGithubIssue(event *model.Event, eventLog model.EventLog, identity model.Identity) error { + if util.Action(eventLog.Action) == util.Create { + body := event.ToMarkdownString() + issue, _, err := util.CreateIssue(&github.IssueRequest{ + Title: &event.Problem, + Body: &body, + Labels: &[]string{"ticket"}, + }) + if err != nil { + return err + } + event.GithubIssueId = sql.NullInt64{ + Valid: true, + Int64: int64(*issue.ID), + } + event.GithubIssueNumber = sql.NullInt64{ + Valid: true, + Int64: int64(*issue.Number), + } + return nil + } + if !event.GithubIssueId.Valid { + return fmt.Errorf("event.GithubIssueId is not valid") + } + + buf := new(bytes.Buffer) + description := md.NewMarkdown(buf). + H2(eventLog.Action). + PlainText(eventLog.Description). + PlainText(fmt.Sprintf("By %s", identity.Member.Alias)).String() + _, _, err := util.CreateIssueComment(int(event.GithubIssueNumber.Int64), &github.IssueComment{ + Body: &description, + }) + if err != nil { + return err + } + + if util.Action(eventLog.Action) == util.Close || util.Action(eventLog.Action) == util.Cancel { + _, _, err := util.CloseIssue(int(event.GithubIssueNumber.Int64)) + if err != nil { + return err + } + } + + return nil +} + /* this function validates the action and then perform action to the event. it also persists the event and event log. @@ -159,6 +211,12 @@ func (service EventService) Act(event *model.Event, identity model.Identity, act } log := handler.Handle() + + err := syncEventActionToGithubIssue(event, log, identity) + if err != nil { + util.Logger.Error(err) + } + // persist event if err := repo.UpdateEvent(event, &log); err != nil { return err From abcc2680f973bda3593227f3fbbb5dde47727e9e Mon Sep 17 00:00:00 2001 From: Clas Wen Date: Sat, 15 Feb 2025 18:28:39 +0800 Subject: [PATCH 2/8] update readme --- readme.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 7b96cbd..d21d49d 100644 --- a/readme.md +++ b/readme.md @@ -41,8 +41,15 @@ LOG_TOPIC= RPC_PORT= + + LOGTO_APPID= + LOGTO_APP_SECRET= + LOGTO_ENDPOINT= + + GITHUB_OWNER= + GITHUB_REPO= + GITHUB_TOKEN= ``` -5. 启动服务 在项目根目录下运行 ``` sh go run main.go From 8219068c56a854841acef1acf7e891ac6a5b2b6b Mon Sep 17 00:00:00 2001 From: Clas Wen Date: Sat, 15 Feb 2025 18:29:15 +0800 Subject: [PATCH 3/8] add github util --- util/github.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 util/github.go diff --git a/util/github.go b/util/github.go new file mode 100644 index 0000000..64192db --- /dev/null +++ b/util/github.go @@ -0,0 +1,34 @@ +package util + +import ( + "context" + "os" + + "github.com/google/go-github/v69/github" +) + +var ghClient *github.Client + +var owner string +var repo string + +func init() { + ghClient = github.NewClient(nil).WithAuthToken(os.Getenv("GITHUB_TOKEN")) + owner = os.Getenv("GITHUB_OWNER") + repo = os.Getenv("GITHUB_REPO") +} + +func CreateIssue(issue *github.IssueRequest) (*github.Issue, *github.Response, error) { + return ghClient.Issues.Create(context.Background(), owner, repo, issue) +} + +func CreateIssueComment(number int, issueComment *github.IssueComment) (*github.IssueComment, *github.Response, error) { + return ghClient.Issues.CreateComment(context.Background(), owner, repo, number, issueComment) +} + +func CloseIssue(number int) (*github.Issue, *github.Response, error) { + state := "closed" + return ghClient.Issues.Edit(context.Background(), owner, repo, number, &github.IssueRequest{ + State: &state, + }) +} From 2c83d8c4dc7529cdd4c06aaf03c697106d372e56 Mon Sep 17 00:00:00 2001 From: Clas Wen Date: Sat, 22 Feb 2025 15:07:06 +0800 Subject: [PATCH 4/8] fix sql --- ...add_github_issue.down.sql => 000003_add_github_issue.down.sql} | 0 ...002_add_github_issue.up.sql => 000003_add_github_issue.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename migrations/{000002_add_github_issue.down.sql => 000003_add_github_issue.down.sql} (100%) rename migrations/{000002_add_github_issue.up.sql => 000003_add_github_issue.up.sql} (100%) diff --git a/migrations/000002_add_github_issue.down.sql b/migrations/000003_add_github_issue.down.sql similarity index 100% rename from migrations/000002_add_github_issue.down.sql rename to migrations/000003_add_github_issue.down.sql diff --git a/migrations/000002_add_github_issue.up.sql b/migrations/000003_add_github_issue.up.sql similarity index 100% rename from migrations/000002_add_github_issue.up.sql rename to migrations/000003_add_github_issue.up.sql From 975530bd6713f391bd6e58a013ea8f1b9526bf9d Mon Sep 17 00:00:00 2001 From: Clas Wen Date: Fri, 28 Feb 2025 22:07:47 +0800 Subject: [PATCH 5/8] fix sql script --- migrations/000001_init.up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/000001_init.up.sql b/migrations/000001_init.up.sql index 88c4b86..9332b07 100644 --- a/migrations/000001_init.up.sql +++ b/migrations/000001_init.up.sql @@ -390,7 +390,7 @@ INSERT INTO public.role (role_id,role) VALUES (0,'member_inactive'), (1,'admin_inactive'), (2,'member'), -(4,'admin'); +(3,'admin'); -- -- Name: client client_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres From e1b31ae74454808f0ebc66563040e869e79d610b Mon Sep 17 00:00:00 2001 From: Clas Wen Date: Fri, 28 Feb 2025 22:12:17 +0800 Subject: [PATCH 6/8] remove unused import --- repo/role.go | 1 - 1 file changed, 1 deletion(-) diff --git a/repo/role.go b/repo/role.go index 436049b..f22880a 100644 --- a/repo/role.go +++ b/repo/role.go @@ -1,7 +1,6 @@ package repo import ( - _ "github.com/go-sql-driver/mysql" "github.com/jmoiron/sqlx" ) From c43bdd4215ac5d2f2b0910e41e3d86d9737a5667 Mon Sep 17 00:00:00 2001 From: Clas Wen Date: Mon, 7 Apr 2025 21:55:45 +0800 Subject: [PATCH 7/8] save --- service/webhook.go | 109 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 service/webhook.go diff --git a/service/webhook.go b/service/webhook.go new file mode 100644 index 0000000..1e7c826 --- /dev/null +++ b/service/webhook.go @@ -0,0 +1,109 @@ +package service + +import ( + "log" + "net/http" + + "github.com/go-playground/webhooks/v6/github" + "github.com/nbtca/saturday/model" + "github.com/nbtca/saturday/repo" + "github.com/nbtca/saturday/util" +) + +type GithubWebHook struct { + hook *github.Webhook +} + +func MakeGithubWebHook(secret string) (*GithubWebHook, error) { + hook, err := github.New(github.Options.Secret(secret)) + if err != nil { + return nil, err + } + return &GithubWebHook{hook: hook}, nil +} + +// - accept repair event when some one is assigned to the issue +// - close repair whe issue when rep +func (gh *GithubWebHook) Handle(request *http.Request) error { + payload, err := gh.hook.Parse( + request, + github.ReleaseEvent, + github.PullRequestEvent, + github.IssueCommentEvent, + github.IssuesEvent, + ) + if err != nil { + return err + } + switch payload.(type) { + case github.IssuesPayload: + issue := payload.(github.IssuesPayload) + event, err := repo.GetEventByIssueId(issue.Issue.ID) + if err != nil { + return err + } + if event.EventId == 0 { + return nil + } + log.Printf("event found %v", event) + + if issue.Action == "assigned" { + return gh.onAssign(issue, event) + } + if issue.Action == "unassigned" { + log.Printf("issue unassigned %v", issue) + } + if issue.Action == "closed" { + log.Printf("issue closed %v", issue) + } + case github.IssueCommentPayload: + comment := payload.(github.IssueCommentPayload) + log.Printf("issue comment %+v", comment) + } + return nil +} + +// assignee Id -> github user email -> logto user -> member +func (gh *GithubWebHook) onAssign(issue github.IssuesPayload, event model.Event) error { + log.Printf("issue assigned %v", issue.Issue.ID) + + assigneeId := issue.Assignee.ID + if assigneeId == 0 { + return nil + } + assignee, _, _ := util.GetUserById(assigneeId) + if assignee == nil { + return nil + } + log.Printf("assignee %v", assignee) + users, _ := LogtoServiceApp.FetchUsers(FetchLogtoUsersRequest{ + PageSize: 1, + SearchParams: map[string]interface{}{ + "search.primaryEmail": *assignee.Email, + }, + }) + if len(users) == 0 { + return nil + } + user := users[0] + member, err := MemberServiceApp.GetMemberByLogtoId(user.Id) + if err != nil { + return err + } + if member.MemberId == "" { + return nil + } + + token, _ := LogtoServiceApp.getToken() + roles, _ := LogtoServiceApp.FetchUserRole(user.Id, token) + log.Printf("member %v", member) + err = EventServiceApp.Accept(&event, model.Identity{ + Id: member.MemberId, + Member: member, + Role: MemberServiceApp.MapLogtoUserRole(roles), + }) + if err != nil { + return err + } + return nil +} From cda28605187aecff577cc89b47c6b6ece2102904 Mon Sep 17 00:00:00 2001 From: Clas Wen Date: Sat, 12 Apr 2025 18:05:00 +0800 Subject: [PATCH 8/8] save --- go.mod | 2 + go.sum | 4 + middleware/auth.go | 6 +- migrations/000004_add_github_id.down.sql | 24 +++++ migrations/000004_add_github_id.up.sql | 28 ++++++ model/event.go | 1 - model/member.go | 2 + readme.md | 36 ++++++- repo/event.go | 26 ++++++ router/event.go | 60 +----------- router/main.go | 11 +++ service/event.go | 67 +++++-------- service/event_test.go | 22 ----- service/logto.go | 114 +++++++++++++++++++++++ service/logto_test.go | 14 +++ service/webhook.go | 7 +- util/event-action.go | 3 +- util/github.go | 9 +- 18 files changed, 300 insertions(+), 136 deletions(-) create mode 100644 migrations/000004_add_github_id.down.sql create mode 100644 migrations/000004_add_github_id.up.sql diff --git a/go.mod b/go.mod index 93cdd23..d6d09f4 100644 --- a/go.mod +++ b/go.mod @@ -12,12 +12,14 @@ require ( github.com/gin-contrib/cors v1.6.0 github.com/gin-gonic/gin v1.10.0 github.com/go-playground/validator/v10 v10.22.1 + github.com/go-playground/webhooks/v6 v6.4.0 github.com/go-sql-driver/mysql v1.7.1 github.com/golang-jwt/jwt/v4 v4.5.0 github.com/google/go-github/v69 v69.1.0 github.com/jmoiron/sqlx v1.3.5 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 + github.com/mostafa/go-api-client v0.1.2 github.com/nao1215/markdown v0.7.0 github.com/nsqio/go-nsq v1.1.0 github.com/ory/dockertest/v3 v3.10.0 diff --git a/go.sum b/go.sum index c593e8a..bfba4c2 100644 --- a/go.sum +++ b/go.sum @@ -66,6 +66,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-playground/webhooks/v6 v6.4.0 h1:KLa6y7bD19N48rxJDHM0DpE3T4grV7GxMy1b/aHMWPY= +github.com/go-playground/webhooks/v6 v6.4.0/go.mod h1:5lBxopx+cAJiBI4+kyRbuHrEi+hYRDdRHuRR4Ya5Ums= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= @@ -148,6 +150,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mostafa/go-api-client v0.1.2 h1:TIkkVQarZKNRc5j3cC/XOPYZ6Z8+oMu54rA0A2L4wGc= +github.com/mostafa/go-api-client v0.1.2/go.mod h1:QmHlkiEHNjh98j8wzRsXdXw/PyU8LQ9KNmjqGnU1xOQ= github.com/nao1215/markdown v0.7.0 h1:SCQkvdQXQuKJW8KaCsvBob8Afy4T4iJUAYIefWpHojE= github.com/nao1215/markdown v0.7.0/go.mod h1:ObBhnNduWwPN+bu4dtv4JoLRt57ONla7l//03iHIVhY= github.com/nsqio/go-nsq v1.1.0 h1:PQg+xxiUjA7V+TLdXw7nVrJ5Jbl3sN86EhGCQj4+FYE= diff --git a/middleware/auth.go b/middleware/auth.go index 79899fe..a918962 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -28,7 +28,7 @@ func Auth(acceptableRoles ...Role) func(c *gin.Context) { token := c.GetHeader("Authorization") if token == "" { c.AbortWithStatusJSON(util.MakeServiceError(http.StatusUnauthorized). - SetMessage("not authorized"). + SetMessage("not authorized, missing token"). Build()) return } @@ -38,7 +38,7 @@ func Auth(acceptableRoles ...Role) func(c *gin.Context) { tokenParsed, claims, err := util.ParseToken(token) if err != nil || !tokenParsed.Valid { c.AbortWithStatusJSON(util.MakeServiceError(http.StatusUnauthorized). - SetMessage("not authorized"). + SetMessage("not authorized, token not valid."). Build()) return } @@ -67,7 +67,7 @@ func Auth(acceptableRoles ...Role) func(c *gin.Context) { userinfo, err := service.LogtoServiceApp.FetchUserInfo(token) if err != nil { c.AbortWithStatusJSON(util.MakeServiceError(http.StatusUnauthorized). - SetMessage("not authorized"). + SetMessage("not authorized" + err.Error()). Build()) return } diff --git a/migrations/000004_add_github_id.down.sql b/migrations/000004_add_github_id.down.sql new file mode 100644 index 0000000..2b89d88 --- /dev/null +++ b/migrations/000004_add_github_id.down.sql @@ -0,0 +1,24 @@ +-- Remove the 'github_id' column from the 'member' table +ALTER TABLE public.member + DROP COLUMN github_id; + +-- Revert the 'member_view' to exclude the 'github_id' column +DROP VIEW public.member_view; +CREATE VIEW public.member_view AS + SELECT member.member_id, + member.alias, + member.password, + member.name, + member.section, + member.profile, + member.phone, + member.qq, + member.avatar, + member.created_by, + member.gmt_create, + member.gmt_modified, + COALESCE(role.role, ''::character varying) AS role, + member.logto_id + FROM ((public.member + LEFT JOIN public.member_role_relation ON ((member.member_id = (member_role_relation.member_id)::bpchar))) + LEFT JOIN public.role ON ((member_role_relation.role_id = role.role_id))); \ No newline at end of file diff --git a/migrations/000004_add_github_id.up.sql b/migrations/000004_add_github_id.up.sql new file mode 100644 index 0000000..e95fa09 --- /dev/null +++ b/migrations/000004_add_github_id.up.sql @@ -0,0 +1,28 @@ +-- Add a new column 'github_id' to the 'member' table +ALTER TABLE public.member + ADD COLUMN github_id character varying(50) DEFAULT ''::character varying; + +-- Optionally, add a comment for the new column +COMMENT ON COLUMN public.member.github_id IS 'GitHub User ID'; + +-- Update the 'member_view' to include the new 'github_id' column +DROP VIEW public.member_view; +CREATE VIEW public.member_view AS + SELECT member.member_id, + member.alias, + member.password, + member.name, + member.section, + member.profile, + member.phone, + member.qq, + member.avatar, + member.created_by, + member.gmt_create, + member.gmt_modified, + COALESCE(role.role, ''::character varying) AS role, + member.logto_id, + member.github_id -- Include the new column + FROM ((public.member + LEFT JOIN public.member_role_relation ON ((member.member_id = (member_role_relation.member_id)::bpchar))) + LEFT JOIN public.role ON ((member_role_relation.role_id = role.role_id))); \ No newline at end of file diff --git a/model/event.go b/model/event.go index ad9c8d3..1a53459 100644 --- a/model/event.go +++ b/model/event.go @@ -35,7 +35,6 @@ func (e Event) ToMarkdownString() string { if e.Model != "" { markdown.BulletList(fmt.Sprintf("Model: %s", e.Model)) } - markdown.Blockquote("footer") return markdown.String() } diff --git a/model/member.go b/model/member.go index 6a23d04..6075bd9 100644 --- a/model/member.go +++ b/model/member.go @@ -15,6 +15,7 @@ type Member struct { QQ string `json:"qq" ` Avatar string `json:"avatar"` CreatedBy string `json:"createdBy" db:"created_by"` + GithubId string `json:"githubId" db:"github_id"` GmtCreate string `json:"gmtCreate" db:"gmt_create"` GmtModified string `json:"gmtModified" db:"gmt_modified"` } @@ -30,6 +31,7 @@ type NullMember struct { Phone sql.NullString `json:"phone" ` QQ sql.NullString `json:"qq" ` Avatar sql.NullString `json:"avatar"` + GithubId string `json:"githubId" db:"github_id"` CreatedBy sql.NullString `json:"createdBy" db:"created_by"` GmtCreate sql.NullString `json:"gmtCreate" db:"gmt_create"` GmtModified sql.NullString `json:"gmtModified" db:"gmt_modified"` diff --git a/readme.md b/readme.md index d21d49d..15327c7 100644 --- a/readme.md +++ b/readme.md @@ -1,27 +1,31 @@ # Saturday -> still relaxing ## 简介 + + 使用 Golang,gin 搭建的维修队后端 + [API文档](https://nbtca.github.io/Saturday/api) + 使用此后端服务的项目 - + [Sunday](https://github.com/nbtca/Sunday) (管理系统) - + [Hawaii](https://github.com/nbtca/Hawaii) (维修小程序) + + [Sunday](https://github.com/nbtca/Sunday) (管理系统) + + [Hawaii](https://github.com/nbtca/Hawaii) (维修小程序) ## 如何运行 + 1. 安装 `Golang` , `Mysql` 2. 安装项目依赖 在项目根目录下运行 + ``` sh go get ``` + 3. 导入数据库 在`Mysql`中新建数据库,并将`assets/saturday.sql`导入 4. 添加配置文件 在项目根目录下新建`.env`文件,添加配置 + ``` DB_URL=:@(
:)/ @@ -50,16 +54,40 @@ GITHUB_REPO= GITHUB_TOKEN= ``` + 在项目根目录下运行 + ``` sh go run main.go ``` -6. 服务运行在`8080`端口 + +5. 服务运行在`8080`端口 ## 测试 + 1. 安装 `Docker` 2. 运行测试 ```sh go test ``` + +## Syncing with Github Issue + +The aim is to achieve a two-way sync between Saturday and Github Issues. The following table outlines the actions taken in Saturday and their corresponding actions in Github. + +### From Saturday to Github + +| Event Action | Github Action | Comment | +| --- | --- | ---| +| Create | Create Github Issue | | +| Cancel | Close Github Issue as not planned | | +| Accept | Assign member to github issue | Not implemented | +| Commit | Add comment in github issue | | +| Drop | Add comment in github issue | | +| Approve | Add comment in github issue and close issue | | + +### From Github to Saturday + +| Event Action | Github Action | Description | +| --- | --- | ---| diff --git a/repo/event.go b/repo/event.go index ca749d5..1a718e2 100644 --- a/repo/event.go +++ b/repo/event.go @@ -79,6 +79,32 @@ func GetEventById(id int64) (model.Event, error) { return event, nil } +func GetEventByIssueId(issueId int64) (model.Event, error) { + getEventSql, getEventArgs, _ := getEventStatement().Where(squirrel.Eq{"github_issue_id": issueId}).ToSql() + conn, err := db.Beginx() + if err != nil { + return model.Event{}, err + } + joinEvent := JoinEvent{} + if err := conn.Get(&joinEvent, getEventSql, getEventArgs...); err != nil { + if err == sql.ErrNoRows { + return model.Event{}, nil + } + conn.Rollback() + return model.Event{}, err + } + getLogSql, getLogArgs, _ := getLogStatement().Where(squirrel.Eq{"event_id": joinEvent.Event.EventId}).ToSql() + defer util.RollbackOnErr(err, conn) + event := joinEvent.ToEvent() + if err = conn.Select(&event.Logs, getLogSql, getLogArgs...); err != nil { + return model.Event{}, err + } + if err = conn.Commit(); err != nil { + return model.Event{}, err + } + return event, nil +} + type EventFilter struct { Offset uint64 Limit uint64 diff --git a/router/event.go b/router/event.go index 4823a63..99e5063 100644 --- a/router/event.go +++ b/router/event.go @@ -93,19 +93,9 @@ func (EventRouter) Accept(c *gin.Context) { rawEvent, _ := c.Get("event") event := rawEvent.(model.Event) identity := util.GetIdentity(c) - if err := service.EventServiceApp.Act(&event, identity, util.Accept); util.CheckError(c, err) { + if err := service.EventServiceApp.Accept(&event, identity); util.CheckError(c, err) { return } - go func() { - if err := service.EventServiceApp.SendActionNotifyViaRPC(&model.EventActionNotifyRequest{ - Subject: "接受维修", - ActorAlias: identity.Member.Alias, - Model: event.Model, - Problem: event.Problem, - }); err != nil { - util.Logger.Error(err) - } - }() c.JSON(200, event) } @@ -130,16 +120,6 @@ func (EventRouter) Commit(c *gin.Context) { if err := service.EventServiceApp.Act(&event, identity, util.Commit, req.Content); util.CheckError(c, err) { return } - go func() { - if err := service.EventServiceApp.SendActionNotifyViaRPC(&model.EventActionNotifyRequest{ - Subject: "维修完成", - ActorAlias: identity.Member.Alias, - Model: event.Model, - Problem: event.Problem, - }); err != nil { - util.Logger.Error(err) - } - }() c.JSON(200, event) } @@ -164,16 +144,6 @@ func (EventRouter) RejectCommit(c *gin.Context) { if err := service.EventServiceApp.Act(&event, identity, util.Reject); util.CheckError(c, err) { return } - go func() { - if err := service.EventServiceApp.SendActionNotifyViaRPC(&model.EventActionNotifyRequest{ - Subject: "退回", - ActorAlias: identity.Member.Alias, - Model: event.Model, - Problem: event.Problem, - }); err != nil { - util.Logger.Error(err) - } - }() c.JSON(200, event) } @@ -184,16 +154,6 @@ func (EventRouter) Close(c *gin.Context) { if err := service.EventServiceApp.Act(&event, identity, util.Close); util.CheckError(c, err) { return } - go func() { - if err := service.EventServiceApp.SendActionNotifyViaRPC(&model.EventActionNotifyRequest{ - Subject: "关闭维修", - ActorAlias: identity.Member.Alias, - Model: event.Model, - Problem: event.Problem, - }); err != nil { - util.Logger.Error(err) - } - }() c.JSON(200, event) } @@ -237,15 +197,6 @@ func (EventRouter) Create(c *gin.Context) { if util.CheckError(c, err) { return } - go func() { - if err := service.EventServiceApp.SendActionNotifyViaRPC(&model.EventActionNotifyRequest{ - Subject: "新的维修", - Model: event.Model, - Problem: event.Problem, - }); err != nil { - util.Logger.Error(err) - } - }() c.JSON(200, event) } @@ -280,15 +231,6 @@ func (EventRouter) Cancel(c *gin.Context) { if err := service.EventServiceApp.Act(&event, identity, util.Cancel); util.CheckError(c, err) { return } - go func() { - if err := service.EventServiceApp.SendActionNotifyViaRPC(&model.EventActionNotifyRequest{ - Subject: "取消维修", - Model: event.Model, - Problem: event.Problem, - }); err != nil { - util.Logger.Error(err) - } - }() c.JSON(200, event) } diff --git a/router/main.go b/router/main.go index 4585c0a..40ad9b6 100644 --- a/router/main.go +++ b/router/main.go @@ -10,6 +10,7 @@ import ( "github.com/danielgtaylor/huma/v2/adapters/humagin" "github.com/gin-contrib/cors" "github.com/nbtca/saturday/middleware" + "github.com/nbtca/saturday/service" "github.com/nbtca/saturday/util" "github.com/gin-gonic/gin" @@ -38,6 +39,16 @@ func SetupRouter() *gin.Engine { MaxAge: 12 * time.Hour, })) + hook, _ := service.MakeGithubWebHook("terepanhakkan") + Router.Handle("POST", "/webhook", func(ctx *gin.Context) { + err := hook.Handle(ctx.Request) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "error": err.Error(), + }) + } + }) + api := humagin.New(Router, huma.DefaultConfig("Saturday API", "1.0.0")) huma.Register(api, huma.Operation{ diff --git a/service/event.go b/service/event.go index 04d6768..2200d72 100644 --- a/service/event.go +++ b/service/event.go @@ -77,45 +77,18 @@ func (service EventService) CreateEvent(event *model.Event) error { return nil } -func (service EventService) SendActionNotify(event *model.Event, subject string) error { - if event == nil { - return util.MakeInternalServerError() +func (service EventService) Accept(event *model.Event, identity model.Identity) error { + if err := service.Act(event, identity, util.Accept); err != nil { + return err } - service.SendActionNotifyViaRPC(&model.EventActionNotifyRequest{ - Subject: subject, - Model: event.Model, - Problem: event.Problem, - Link: "A Link to Sunday", - GmtCreate: event.GmtCreate, - }) - service.SendActionNotifyViaMail(event, subject) return nil } -func (service EventService) SendActionNotifyViaRPC(req *model.EventActionNotifyRequest) error { - address := os.Getenv("RPC_ADDRESS") - if address == "" { - return fmt.Errorf("RPC_ADDRESS is not set") - } - conn, err := rpc.DialHTTP("tcp", address) - if err != nil { - return err - } - // req := model.EventActionNotifyRequest{ - // Subject: subject, - // Model: event.Model, - // Problem: event.Problem, - // Link: "A Link to Sunday", - // GmtCreate: event.GmtCreate, - // } - res := model.EventActionNotifyResponse{} - if err = conn.Call("Notify.EventActionNotify", req, &res); err != nil { - util.Logger.Error(err) - return err - } - if !res.Success { - return fmt.Errorf("failed to send action notify via rpc") +func (service EventService) SendActionNotify(event *model.Event, subject string) error { + if event == nil { + return util.MakeInternalServerError() } + service.SendActionNotifyViaMail(event, subject) return nil } @@ -153,8 +126,9 @@ func (service EventService) SendActionNotifyViaMail(event *model.Event, subject func syncEventActionToGithubIssue(event *model.Event, eventLog model.EventLog, identity model.Identity) error { if util.Action(eventLog.Action) == util.Create { body := event.ToMarkdownString() + title := fmt.Sprintf("%s(#%v)", event.Problem, event.EventId) issue, _, err := util.CreateIssue(&github.IssueRequest{ - Title: &event.Problem, + Title: &title, Body: &body, Labels: &[]string{"ticket"}, }) @@ -178,22 +152,30 @@ func syncEventActionToGithubIssue(event *model.Event, eventLog model.EventLog, i buf := new(bytes.Buffer) description := md.NewMarkdown(buf). H2(eventLog.Action). - PlainText(eventLog.Description). - PlainText(fmt.Sprintf("By %s", identity.Member.Alias)).String() + PlainText(eventLog.Description) + if util.Action(eventLog.Action) == util.Cancel { + description = description.PlainText("Cancelled by client") + } else { + description = description.PlainText(fmt.Sprintf("By %s", identity.Member.Alias)) + } + commentBody := description.String() + _, _, err := util.CreateIssueComment(int(event.GithubIssueNumber.Int64), &github.IssueComment{ - Body: &description, + Body: &commentBody, }) if err != nil { return err } - if util.Action(eventLog.Action) == util.Close || util.Action(eventLog.Action) == util.Cancel { - _, _, err := util.CloseIssue(int(event.GithubIssueNumber.Int64)) - if err != nil { + if util.Action(eventLog.Action) == util.Close { + if _, _, err := util.CloseIssue(int(event.GithubIssueNumber.Int64), "complete"); err != nil { + return err + } + } else if util.Action(eventLog.Action) == util.Cancel { + if _, _, err := util.CloseIssue(int(event.GithubIssueNumber.Int64), "not_planned"); err != nil { return err } } - return nil } @@ -204,6 +186,7 @@ it also persists the event and event log. func (service EventService) Act(event *model.Event, identity model.Identity, action util.Action, description ...string) error { handler := util.MakeEventActionHandler(action, event, identity) if err := handler.ValidateAction(); err != nil { + util.Logger.Error("validate action failed", err) return err } for _, d := range description { diff --git a/service/event_test.go b/service/event_test.go index 36e2468..1ccaf1d 100644 --- a/service/event_test.go +++ b/service/event_test.go @@ -1,23 +1 @@ package service_test - -import ( - "log" - "os" - "testing" - - "github.com/nbtca/saturday/model" - "github.com/nbtca/saturday/service" -) - -func TestSendActionNotifyViaRPC(t *testing.T) { - os.Setenv("RPC_ADDRESS", ":8000") - service := service.EventService{} - err := service.SendActionNotifyViaRPC(&model.EventActionNotifyRequest{ - Model: "model", - Problem: "problem", - GmtCreate: "gmtCreate", - }) - if err != nil { - log.Print(err) - } -} diff --git a/service/logto.go b/service/logto.go index 277eb31..60597cf 100644 --- a/service/logto.go +++ b/service/logto.go @@ -5,11 +5,14 @@ import ( "encoding/json" "fmt" "io" + "log" "net/http" "net/url" "os" "strings" + "time" + "github.com/golang-jwt/jwt/v4" "github.com/nbtca/saturday/model/dto" "github.com/nbtca/saturday/util" ) @@ -18,6 +21,45 @@ var DefaultLogtoResource = "https://default.logto.app/api" type LogtoService struct { BaseURL string + token string +} + +func (l LogtoService) getToken() (string, error) { + + validate := func(token string) bool { + if token == "" { + return false + } + parsedToken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { + return []byte(os.Getenv("LOGTO_APP_SECRET")), nil + }, jwt.WithoutClaimsValidation()) + if err != nil { + return false + } + // Check if the token is valid and not expired + if claims, ok := parsedToken.Claims.(jwt.MapClaims); ok { + // Check for expiration claim ('exp') + if exp, ok := claims["exp"].(float64); ok { + // Convert 'exp' to time + expirationTime := time.Unix(int64(exp), 0) + // Check if the token is expired + if time.Now().After(expirationTime) { + return false + } + } + } + return true + } + + if !validate(l.token) { + res, err := l.FetchLogtoToken(DefaultLogtoResource, "all") + if err != nil { + return "", err + } + l.token = res["access_token"].(string) + return l.token, nil + } + return l.token, nil } func (l LogtoService) FetchLogtoToken(resource string, scope string) (map[string]interface{}, error) { @@ -58,6 +100,74 @@ func (l LogtoService) FetchLogtoToken(resource string, scope string) (map[string return body, nil } +type FetchLogtoUsersRequest struct { + Page int32 + PageSize int32 + SearchParams map[string]interface{} +} +type LogtoUserIdentities struct { + UserId string `json:"userId"` + Details map[string]interface{} `json:"details"` +} + +type FetchLogtoUsersResponse struct { + Id string `json:"id"` + UserName string `json:"username"` + PrimaryEmail string `json:"primaryEmail"` + PrimaryPhone string `json:"primaryPhone"` + Name string `json:"name"` + Avatar string `json:"avatar"` + Identities map[string]LogtoUserIdentities `json:"identities"` + CustomerData map[string]interface{} `json:"customData"` + SSOIdentities []map[string]string `json:"ssoIdentities"` +} + +func (l LogtoService) FetchUsers(request FetchLogtoUsersRequest) ([]FetchLogtoUsersResponse, error) { + if request.Page < 1 { + request.Page = 1 + } + if request.PageSize < 1 { + request.PageSize = 10 + } + query := url.Values{ + "page": {fmt.Sprint(request.Page)}, + "page_size": {fmt.Sprint(request.PageSize)}, + } + for key, value := range request.SearchParams { + query.Add(key, fmt.Sprint(value)) + } + requestURL, _ := url.JoinPath(l.BaseURL, "/api/users") + requestURL = requestURL + "?" + query.Encode() + log.Println(requestURL) + req, _ := http.NewRequest("GET", requestURL, nil) + token, err := l.getToken() + if err != nil { + return nil, err + } + req.Header.Add("Authorization", "Bearer "+token) + res, err := http.DefaultClient.Do(req) + + if err != nil { + return nil, err + } + defer res.Body.Close() + rawBody, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + var body []FetchLogtoUsersResponse + if err := json.Unmarshal(rawBody, &body); err != nil { + return nil, err + } + + if res.Status != "200 OK" { + return nil, fmt.Errorf(string(rawBody)) + } + return body, nil + +} + func (l LogtoService) FetchUserById(userId string, token string) (map[string]interface{}, error) { requestURL, _ := url.JoinPath(l.BaseURL, "/api/users/", userId) req, _ := http.NewRequest("GET", requestURL, nil) @@ -243,3 +353,7 @@ func (l LogtoService) FetchUserInfo(accessToken string) (FetchUserInfoResponse, } var LogtoServiceApp LogtoService + +func init() { + LogtoServiceApp = MakeLogtoService(os.Getenv("LOGTO_ENDPOINT")) +} diff --git a/service/logto_test.go b/service/logto_test.go index 74c2f3f..2d7b075 100644 --- a/service/logto_test.go +++ b/service/logto_test.go @@ -42,3 +42,17 @@ func TestFetchLogtoUser(t *testing.T) { } t.Log(user) } + +func TestFetchLogtoUsers(t *testing.T) { + service.LogtoServiceApp = service.MakeLogtoService(os.Getenv("LOGTO_ENDPOINT")) + // userId := os.Getenv("LOGTO_TEST_USER_ID") + user, err := service.LogtoServiceApp.FetchUsers(service.FetchLogtoUsersRequest{ + Page: 1, + PageSize: 10, + SearchParams: map[string]interface{}{"search.primaryEmail": "clas.wen@icloud.com"}, + }) + if err != nil { + t.Fatal(err) + } + t.Log(user[0].Identities["github"].UserId) +} diff --git a/service/webhook.go b/service/webhook.go index 1e7c826..5427777 100644 --- a/service/webhook.go +++ b/service/webhook.go @@ -51,7 +51,7 @@ func (gh *GithubWebHook) Handle(request *http.Request) error { return gh.onAssign(issue, event) } if issue.Action == "unassigned" { - log.Printf("issue unassigned %v", issue) + return gh.onUnassign(issue, event) } if issue.Action == "closed" { log.Printf("issue closed %v", issue) @@ -107,3 +107,8 @@ func (gh *GithubWebHook) onAssign(issue github.IssuesPayload, event model.Event) } return nil } + +func (gh *GithubWebHook) onUnassign(issue github.IssuesPayload, event model.Event) error { + // TODO + return nil +} diff --git a/util/event-action.go b/util/event-action.go index cc01bd9..720c3c0 100644 --- a/util/event-action.go +++ b/util/event-action.go @@ -65,7 +65,7 @@ var eventActionMap map[Action]eventActionHandler = map[Action]eventActionHandler }, Accept: { action: Accept, - role: []string{"member"}, + role: []string{"member", "admin"}, prevStatus: Open, nextStatus: Accepted, customLog: func(eh *eventActionHandler) model.EventLog { @@ -223,4 +223,3 @@ func (eh *eventActionHandler) Handle() model.EventLog { return eventLog } - diff --git a/util/github.go b/util/github.go index 64192db..d09cb99 100644 --- a/util/github.go +++ b/util/github.go @@ -26,9 +26,14 @@ func CreateIssueComment(number int, issueComment *github.IssueComment) (*github. return ghClient.Issues.CreateComment(context.Background(), owner, repo, number, issueComment) } -func CloseIssue(number int) (*github.Issue, *github.Response, error) { +func CloseIssue(number int, stateReason string) (*github.Issue, *github.Response, error) { state := "closed" return ghClient.Issues.Edit(context.Background(), owner, repo, number, &github.IssueRequest{ - State: &state, + State: &state, + StateReason: &stateReason, }) } + +func GetUserById(id int64) (*github.User, *github.Response, error) { + return ghClient.Users.GetByID(context.Background(), id) +}