From aad52717a9057fa261d7df0749f65467b5001fb0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 18:43:52 +0000 Subject: [PATCH 01/12] Bump golang from 1.20.7-alpine to 1.22.2-alpine in /docker/tuning Bumps golang from 1.20.7-alpine to 1.22.2-alpine. --- updated-dependencies: - dependency-name: golang dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- docker/tuning/Dockerfile.server | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/tuning/Dockerfile.server b/docker/tuning/Dockerfile.server index 38e5f11e..eed7c56e 100644 --- a/docker/tuning/Dockerfile.server +++ b/docker/tuning/Dockerfile.server @@ -1,4 +1,4 @@ -FROM golang:1.20.7-alpine as build-step +FROM golang:1.22.2-alpine as build-step RUN apk add --update --no-cache ca-certificates git WORKDIR /go/src/github.com/traPtitech/anke-to From f5bebd3c75a1425b40df40ffb9c06422d2a7ff42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 18:43:57 +0000 Subject: [PATCH 02/12] Bump golang from 1.20.7-alpine to 1.22.2-alpine Bumps golang from 1.20.7-alpine to 1.22.2-alpine. --- updated-dependencies: - dependency-name: golang dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 82a44dec..7f125e40 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # syntax = docker/dockerfile:1.3.0 # build backend -FROM golang:1.20.7-alpine as server-build +FROM golang:1.22.2-alpine as server-build RUN --mount=type=cache,target=/var/cache/apk \ apk add --update git From ab1014d8ec50843a9d3eb8ef8ead167acee0c0c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 18:53:02 +0000 Subject: [PATCH 03/12] Bump golang from 1.20.7-alpine to 1.22.2-alpine in /docker/staging Bumps golang from 1.20.7-alpine to 1.22.2-alpine. --- updated-dependencies: - dependency-name: golang dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- docker/staging/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/staging/Dockerfile b/docker/staging/Dockerfile index 2478edd7..4743501d 100644 --- a/docker/staging/Dockerfile +++ b/docker/staging/Dockerfile @@ -1,7 +1,7 @@ # syntax = docker/dockerfile:1.3.0 # build backend -FROM golang:1.20.7-alpine as server-build +FROM golang:1.22.2-alpine as server-build RUN --mount=type=cache,target=/var/cache/apk \ apk add --update git From 5f3efb6a09bbd7f6dc0edfa524481b21dc023f88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 18:57:49 +0000 Subject: [PATCH 04/12] Bump golang from 1.20.7-alpine to 1.22.2-alpine in /docker/dev Bumps golang from 1.20.7-alpine to 1.22.2-alpine. --- updated-dependencies: - dependency-name: golang dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- docker/dev/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index 24f74ca2..7e452281 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.20.7-alpine as build-step +FROM golang:1.22.2-alpine as build-step RUN apk add --update --no-cache ca-certificates git WORKDIR /go/src/github.com/traPtitech/anke-to From e54cb5d2827cb8e75de1011ab7af7b056ff552fe Mon Sep 17 00:00:00 2001 From: kaitoyama <45167401+kaitoyama@users.noreply.github.com> Date: Thu, 25 Apr 2024 14:47:22 +0900 Subject: [PATCH 05/12] Add CancelTargets method to ITarget interface and implement it in TargetsImpl --- model/targets.go | 1 + model/targets_impl.go | 22 +++++++- model/targets_test.go | 118 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 1 deletion(-) diff --git a/model/targets.go b/model/targets.go index e6557b12..a51abd1c 100644 --- a/model/targets.go +++ b/model/targets.go @@ -9,4 +9,5 @@ type ITarget interface { InsertTargets(ctx context.Context, questionnaireID int, targets []string) error DeleteTargets(ctx context.Context, questionnaireID int) error GetTargets(ctx context.Context, questionnaireIDs []int) ([]Targets, error) + CancelTargets(ctx context.Context, questionnaireID int, targets []string) error } diff --git a/model/targets_impl.go b/model/targets_impl.go index c3d73dd8..1abac702 100644 --- a/model/targets_impl.go +++ b/model/targets_impl.go @@ -13,10 +13,11 @@ func NewTarget() *Target { return new(Target) } -//Targets targetsテーブルの構造体 +// Targets targetsテーブルの構造体 type Targets struct { QuestionnaireID int `gorm:"type:int(11) AUTO_INCREMENT;not null;primaryKey"` UserTraqid string `gorm:"type:varchar(32);size:32;not null;primaryKey"` + IsCanceled bool `gorm:"type:tinyint(1);not null;default:0"` } // InsertTargets アンケートの対象を追加 @@ -35,6 +36,7 @@ func (*Target) InsertTargets(ctx context.Context, questionnaireID int, targets [ dbTargets = append(dbTargets, Targets{ QuestionnaireID: questionnaireID, UserTraqid: target, + IsCanceled: false, }) } @@ -80,3 +82,21 @@ func (*Target) GetTargets(ctx context.Context, questionnaireIDs []int) ([]Target return targets, nil } + +// CancelTarget アンケートの対象をキャンセル +func (*Target) CancelTargets(ctx context.Context, questionnaireID int, targets []string) error { + db, err := getTx(ctx) + if err != nil { + return fmt.Errorf("failed to get transaction: %w", err) + } + + err = db. + Model(&Targets{}). + Where("questionnaire_id = ? AND user_traqid IN (?)", questionnaireID, targets). + Update("is_canceled", true).Error + if err != nil { + return fmt.Errorf("failed to cancel targets: %w", err) + } + + return nil +} diff --git a/model/targets_test.go b/model/targets_test.go index a00d7487..7de9e25e 100644 --- a/model/targets_test.go +++ b/model/targets_test.go @@ -308,3 +308,121 @@ func TestGetTargets(t *testing.T) { }) } } + +func TestCancelTargets(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + type test struct { + description string + beforeValidTargets []string + beforeInvalidTargets []string + afterValidTargets []string + afterInvalidTargets []string + argCancelTargets []string + isErr bool + err error + } + + testCases := []test{ + { + description: "キャンセルするtargetが1人でもエラーなし", + beforeValidTargets: []string{"a"}, + beforeInvalidTargets: []string{}, + afterValidTargets: []string{}, + afterInvalidTargets: []string{"a"}, + argCancelTargets: []string{"a"}, + }, + { + description: "キャンセルするtargetが複数でもエラーなし", + beforeValidTargets: []string{"a", "b"}, + beforeInvalidTargets: []string{}, + afterValidTargets: []string{}, + afterInvalidTargets: []string{"a", "b"}, + argCancelTargets: []string{"a", "b"}, + }, + { + description: "キャンセルするtargetがなくてもエラーなし", + beforeValidTargets: []string{"a"}, + beforeInvalidTargets: []string{}, + afterValidTargets: []string{"a"}, + afterInvalidTargets: []string{}, + argCancelTargets: []string{}, + }, + { + description: "キャンセルするtargetが見つからない場合はエラー", + beforeValidTargets: []string{"a"}, + beforeInvalidTargets: []string{}, + afterValidTargets: []string{"a"}, + afterInvalidTargets: []string{}, + argCancelTargets: []string{"b"}, + isErr: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.description, func(t *testing.T) { + targets := make([]Targets, 0, len(testCase.beforeValidTargets)+len(testCase.beforeInvalidTargets)) + for _, target := range testCase.beforeValidTargets { + targets = append(targets, Targets{ + UserTraqid: target, + IsCanceled: false, + }) + } + for _, target := range testCase.beforeInvalidTargets { + targets = append(targets, Targets{ + UserTraqid: target, + IsCanceled: true, + }) + } + questionnaire := Questionnaires{ + Targets: targets, + } + err := db. + Session(&gorm.Session{}). + Create(&questionnaire).Error + if err != nil { + t.Errorf("failed to create questionnaire: %v", err) + } + + err = targetImpl.CancelTargets(ctx, questionnaire.ID, testCase.argCancelTargets) + if err != nil { + if !testCase.isErr { + t.Errorf("unexpected error: %v", err) + } else if !errors.Is(err, testCase.err) { + t.Errorf("invalid error: expected: %v, actual: %v", testCase.err, err) + } + return + } + + afterTargets := make([]Targets, 0, len(testCase.afterValidTargets)+len(testCase.afterInvalidTargets)) + for _, afterTarget := range testCase.afterInvalidTargets { + afterTargets = append(afterTargets, Targets{ + QuestionnaireID: questionnaire.ID, + UserTraqid: afterTarget, + IsCanceled: true, + }) + } + for _, afterTarget := range testCase.afterValidTargets { + afterTargets = append(afterTargets, Targets{ + QuestionnaireID: questionnaire.ID, + UserTraqid: afterTarget, + IsCanceled: false, + }) + } + + actualTargets := make([]Targets, 0, len(testCase.afterValidTargets)+len(testCase.afterInvalidTargets)) + err = db. + Session(&gorm.Session{}). + Model(&Targets{}). + Where("questionnaire_id = ?", questionnaire.ID). + Find(&actualTargets).Error + if err != nil { + t.Errorf("failed to get targets: %v", err) + } + + assert.ElementsMatchf(t, afterTargets, actualTargets, "targets") + }) + } +} From cf48bb06cf9ca06203eb9b7c02b0f0c236139386 Mon Sep 17 00:00:00 2001 From: kaitoyama <45167401+kaitoyama@users.noreply.github.com> Date: Sat, 27 Apr 2024 13:21:41 +0900 Subject: [PATCH 06/12] upgrade golang version --- docker/test/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/test/Dockerfile b/docker/test/Dockerfile index 71351a61..92a5ef39 100644 --- a/docker/test/Dockerfile +++ b/docker/test/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.20.7-alpine as builder +FROM golang:1.22.2-alpine as builder RUN apk add --update --no-cache ca-certificates git \ && apk add --no-cache gcc libc-dev \ && apk add --no-cache openssl From a91115c30d95de2c7af7e64d19a54ae25e90bfe5 Mon Sep 17 00:00:00 2001 From: kaitoyama <45167401+kaitoyama@users.noreply.github.com> Date: Thu, 25 Apr 2024 14:47:22 +0900 Subject: [PATCH 07/12] Add CancelTargets method to ITarget interface and implement it in TargetsImpl --- model/targets.go | 1 + model/targets_impl.go | 22 +++++++- model/targets_test.go | 118 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 1 deletion(-) diff --git a/model/targets.go b/model/targets.go index e6557b12..a51abd1c 100644 --- a/model/targets.go +++ b/model/targets.go @@ -9,4 +9,5 @@ type ITarget interface { InsertTargets(ctx context.Context, questionnaireID int, targets []string) error DeleteTargets(ctx context.Context, questionnaireID int) error GetTargets(ctx context.Context, questionnaireIDs []int) ([]Targets, error) + CancelTargets(ctx context.Context, questionnaireID int, targets []string) error } diff --git a/model/targets_impl.go b/model/targets_impl.go index c3d73dd8..1abac702 100644 --- a/model/targets_impl.go +++ b/model/targets_impl.go @@ -13,10 +13,11 @@ func NewTarget() *Target { return new(Target) } -//Targets targetsテーブルの構造体 +// Targets targetsテーブルの構造体 type Targets struct { QuestionnaireID int `gorm:"type:int(11) AUTO_INCREMENT;not null;primaryKey"` UserTraqid string `gorm:"type:varchar(32);size:32;not null;primaryKey"` + IsCanceled bool `gorm:"type:tinyint(1);not null;default:0"` } // InsertTargets アンケートの対象を追加 @@ -35,6 +36,7 @@ func (*Target) InsertTargets(ctx context.Context, questionnaireID int, targets [ dbTargets = append(dbTargets, Targets{ QuestionnaireID: questionnaireID, UserTraqid: target, + IsCanceled: false, }) } @@ -80,3 +82,21 @@ func (*Target) GetTargets(ctx context.Context, questionnaireIDs []int) ([]Target return targets, nil } + +// CancelTarget アンケートの対象をキャンセル +func (*Target) CancelTargets(ctx context.Context, questionnaireID int, targets []string) error { + db, err := getTx(ctx) + if err != nil { + return fmt.Errorf("failed to get transaction: %w", err) + } + + err = db. + Model(&Targets{}). + Where("questionnaire_id = ? AND user_traqid IN (?)", questionnaireID, targets). + Update("is_canceled", true).Error + if err != nil { + return fmt.Errorf("failed to cancel targets: %w", err) + } + + return nil +} diff --git a/model/targets_test.go b/model/targets_test.go index a00d7487..7de9e25e 100644 --- a/model/targets_test.go +++ b/model/targets_test.go @@ -308,3 +308,121 @@ func TestGetTargets(t *testing.T) { }) } } + +func TestCancelTargets(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + type test struct { + description string + beforeValidTargets []string + beforeInvalidTargets []string + afterValidTargets []string + afterInvalidTargets []string + argCancelTargets []string + isErr bool + err error + } + + testCases := []test{ + { + description: "キャンセルするtargetが1人でもエラーなし", + beforeValidTargets: []string{"a"}, + beforeInvalidTargets: []string{}, + afterValidTargets: []string{}, + afterInvalidTargets: []string{"a"}, + argCancelTargets: []string{"a"}, + }, + { + description: "キャンセルするtargetが複数でもエラーなし", + beforeValidTargets: []string{"a", "b"}, + beforeInvalidTargets: []string{}, + afterValidTargets: []string{}, + afterInvalidTargets: []string{"a", "b"}, + argCancelTargets: []string{"a", "b"}, + }, + { + description: "キャンセルするtargetがなくてもエラーなし", + beforeValidTargets: []string{"a"}, + beforeInvalidTargets: []string{}, + afterValidTargets: []string{"a"}, + afterInvalidTargets: []string{}, + argCancelTargets: []string{}, + }, + { + description: "キャンセルするtargetが見つからない場合はエラー", + beforeValidTargets: []string{"a"}, + beforeInvalidTargets: []string{}, + afterValidTargets: []string{"a"}, + afterInvalidTargets: []string{}, + argCancelTargets: []string{"b"}, + isErr: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.description, func(t *testing.T) { + targets := make([]Targets, 0, len(testCase.beforeValidTargets)+len(testCase.beforeInvalidTargets)) + for _, target := range testCase.beforeValidTargets { + targets = append(targets, Targets{ + UserTraqid: target, + IsCanceled: false, + }) + } + for _, target := range testCase.beforeInvalidTargets { + targets = append(targets, Targets{ + UserTraqid: target, + IsCanceled: true, + }) + } + questionnaire := Questionnaires{ + Targets: targets, + } + err := db. + Session(&gorm.Session{}). + Create(&questionnaire).Error + if err != nil { + t.Errorf("failed to create questionnaire: %v", err) + } + + err = targetImpl.CancelTargets(ctx, questionnaire.ID, testCase.argCancelTargets) + if err != nil { + if !testCase.isErr { + t.Errorf("unexpected error: %v", err) + } else if !errors.Is(err, testCase.err) { + t.Errorf("invalid error: expected: %v, actual: %v", testCase.err, err) + } + return + } + + afterTargets := make([]Targets, 0, len(testCase.afterValidTargets)+len(testCase.afterInvalidTargets)) + for _, afterTarget := range testCase.afterInvalidTargets { + afterTargets = append(afterTargets, Targets{ + QuestionnaireID: questionnaire.ID, + UserTraqid: afterTarget, + IsCanceled: true, + }) + } + for _, afterTarget := range testCase.afterValidTargets { + afterTargets = append(afterTargets, Targets{ + QuestionnaireID: questionnaire.ID, + UserTraqid: afterTarget, + IsCanceled: false, + }) + } + + actualTargets := make([]Targets, 0, len(testCase.afterValidTargets)+len(testCase.afterInvalidTargets)) + err = db. + Session(&gorm.Session{}). + Model(&Targets{}). + Where("questionnaire_id = ?", questionnaire.ID). + Find(&actualTargets).Error + if err != nil { + t.Errorf("failed to get targets: %v", err) + } + + assert.ElementsMatchf(t, afterTargets, actualTargets, "targets") + }) + } +} From 9c6415a992f199c4969bcf9a2f50a9d901af3c32 Mon Sep 17 00:00:00 2001 From: kaitoyama <45167401+kaitoyama@users.noreply.github.com> Date: Sat, 27 Apr 2024 13:27:18 +0900 Subject: [PATCH 08/12] rough implmentation --- go.mod | 28 +++++--- go.sum | 26 +++++++ main.go | 16 ++++- model/questionnaires_impl.go | 18 +++++ router/questionnaires.go | 53 ++++++++++++++ router/reminder.go | 133 +++++++++++++++++++++++++++++++++++ 6 files changed, 264 insertions(+), 10 deletions(-) create mode 100644 router/reminder.go diff --git a/go.mod b/go.mod index 5358018d..94b9e3d3 100644 --- a/go.mod +++ b/go.mod @@ -9,15 +9,15 @@ require ( github.com/google/wire v0.5.0 github.com/labstack/echo/v4 v4.11.1 github.com/mattn/go-isatty v0.0.19 // indirect - github.com/stretchr/testify v1.8.1 - golang.org/x/crypto v0.11.0 // indirect - golang.org/x/mod v0.8.0 // indirect - golang.org/x/net v0.12.0 // indirect + github.com/stretchr/testify v1.9.0 + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.19.0 // indirect golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f - golang.org/x/sync v0.1.0 - golang.org/x/sys v0.10.0 // indirect - golang.org/x/text v0.11.0 // indirect - golang.org/x/tools v0.6.0 // indirect + golang.org/x/sync v0.5.0 + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.16.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.27.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -65,4 +65,14 @@ require ( gorm.io/plugin/prometheus v0.0.0-20210820101226-2a49866f83ee ) -require github.com/go-gormigrate/gormigrate/v2 v2.1.1 // indirect +require ( + github.com/go-co-op/gocron/v2 v2.2.9 + github.com/go-gormigrate/gormigrate/v2 v2.1.1 +) + +require ( + github.com/google/uuid v1.6.0 // indirect + github.com/jonboulle/clockwork v0.4.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 // indirect +) diff --git a/go.sum b/go.sum index 83e0cc26..c0190a98 100644 --- a/go.sum +++ b/go.sum @@ -82,6 +82,8 @@ github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-co-op/gocron/v2 v2.2.9 h1:aoKosYWSSdXFLecjFWX1i8+R6V7XdZb8sB2ZKAY5Yis= +github.com/go-co-op/gocron/v2 v2.2.9/go.mod h1:mZx3gMSlFnb97k3hRqX3+GdlG3+DUwTh6B8fnsTScXg= 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= @@ -169,6 +171,8 @@ github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3 github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -197,6 +201,8 @@ github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= +github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -295,6 +301,8 @@ github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0 github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rabbitmq/amqp091-go v1.1.0/go.mod h1:ogQDLSOACsLPsIq0NpbtiifNZi2YOz0VTJ0kHRghqbM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 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= @@ -320,6 +328,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ 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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= @@ -358,6 +368,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -371,6 +383,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE= +golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -396,6 +410,8 @@ 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/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -439,6 +455,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= 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= @@ -461,6 +479,8 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ 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/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -523,6 +543,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -540,6 +562,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +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= @@ -594,6 +618,8 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= +golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go index f5457edc..6b54dd62 100644 --- a/main.go +++ b/main.go @@ -8,10 +8,12 @@ import ( "runtime" "github.com/traPtitech/anke-to/model" + "github.com/traPtitech/anke-to/router" "github.com/traPtitech/anke-to/tuning" ) func main() { + env, ok := os.LookupEnv("ANKE-TO_ENV") if !ok { env = "production" @@ -51,5 +53,17 @@ func main() { panic("no PORT") } - SetRouting(port) + router.Wg.Add(1) + go func() { + SetRouting(port) + router.Wg.Done() + }() + + router.Wg.Add(1) + go func() { + router.ReminderWorker() + router.Wg.Done() + }() + + router.Wg.Wait() } diff --git a/model/questionnaires_impl.go b/model/questionnaires_impl.go index e7756bc7..4d2b3437 100755 --- a/model/questionnaires_impl.go +++ b/model/questionnaires_impl.go @@ -362,6 +362,24 @@ func (*Questionnaire) GetTargettedQuestionnaires(ctx context.Context, userID str return questionnaires, nil } +// GetQuestionnaireInfoByLestTime アンケートの詳細な情報を回答期限が3日以内のものだけ取得 +func (*Questionnaire) GetQuestionnairesByLestTime(ctx context.Context) ([]Questionnaires, error) { + db, err := getTx(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get tx: %w", err) + } + + questionnaires := []Questionnaires{} + err = db. + Where("res_time_limit > ? AND res_time_limit < ?", time.Now(), time.Now().AddDate(0, 0, 3)). + Find(&questionnaires).Error + if err != nil { + return nil, fmt.Errorf("failed to get a questionnaire: %w", err) + } + + return questionnaires, nil +} + // GetQuestionnaireLimit アンケートの回答期限の取得 func (*Questionnaire) GetQuestionnaireLimit(ctx context.Context, questionnaireID int) (null.Time, error) { db, err := getTx(ctx) diff --git a/router/questionnaires.go b/router/questionnaires.go index b33db7f4..0cc76ea7 100644 --- a/router/questionnaires.go +++ b/router/questionnaires.go @@ -214,6 +214,12 @@ func (q *Questionnaire) PostQuestionnaire(c echo.Context) error { return echo.NewHTTPError(http.StatusInternalServerError, "failed to post message to traQ") } + err = Q.PushReminder(questionnaireID) + if err != nil { + c.Logger().Errorf("failed to push reminder: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to push reminder") + } + return nil }) if err != nil { @@ -439,6 +445,18 @@ func (q *Questionnaire) EditQuestionnaire(c echo.Context) error { return err } + err = Q.DeleteReminder(questionnaireID) + if err != nil { + c.Logger().Errorf("failed to delete reminder: %+v", err) + return err + } + + err = Q.PushReminder(questionnaireID) + if err != nil { + c.Logger().Errorf("failed to push reminder: %+v", err) + return err + } + return nil }) if err != nil { @@ -481,6 +499,12 @@ func (q *Questionnaire) DeleteQuestionnaire(c echo.Context) error { return err } + err = Q.DeleteReminder(questionnaireID) + if err != nil { + c.Logger().Errorf("failed to delete reminder: %+v", err) + return err + } + return nil }) if err != nil { @@ -664,3 +688,32 @@ https://anke-to.trap.jp/responses/new/%d`, questionnaireID, ) } + +func createReminderMessage(questionnaireID int, title string, description string, administrators []string, resTimeLimit time.Time, targets []string, lestTimeString string) string { + var resTimeLimitText = resTimeLimit.Local().Format("2006/01/02 15:04") + + var targetsMentionText = "@" + strings.Join(targets, " @") + + return fmt.Sprintf( + `### アンケート『[%s](https://anke-to.trap.jp/questionnaires/%d)』の回答締め切りが迫っています! +==残り%sです!== +#### 管理者 +%s +#### 説明 +%s +#### 回答期限 +%s +#### 対象者 +%s +#### 回答リンク +https://anke-to.trap.jp/responses/new/%d`, + title, + questionnaireID, + lestTimeString, + strings.Join(administrators, ","), + description, + resTimeLimitText, + targetsMentionText, + questionnaireID, + ) +} diff --git a/router/reminder.go b/router/reminder.go new file mode 100644 index 00000000..39707fa7 --- /dev/null +++ b/router/reminder.go @@ -0,0 +1,133 @@ +package router + +import ( + "context" + "slices" + "sort" + "sync" + "time" + + "github.com/traPtitech/anke-to/model" + "github.com/traPtitech/anke-to/traq" + "golang.org/x/sync/semaphore" +) + +type Job struct { + Timestamp time.Time + QuestionnaireID string + Action func() +} + +type JobQueue struct { + jobs []*Job + mu sync.Mutex +} + +var ( + sem = semaphore.NewWeighted(1) + Q = &JobQueue{} + Wg = &sync.WaitGroup{} + reiminderTimingMinutes = []int{5, 30, 60, 1440, 10080} +) + +func (q *JobQueue) Push(j *Job) { + q.mu.Lock() + defer q.mu.Unlock() + q.jobs = append(q.jobs, j) + sort.Slice(q.jobs, func(i, j int) bool { + return q.jobs[i].Timestamp.Before(q.jobs[j].Timestamp) + }) +} + +func (q *JobQueue) Pop() *Job { + q.mu.Lock() + defer q.mu.Unlock() + if len(q.jobs) == 0 { + return nil + } + job := q.jobs[0] + q.jobs = q.jobs[1:] + return job +} + +func (q *JobQueue) PushReminder(questionnaireID int) error { + ctx := context.Background() + questionnaire := model.Questionnaire{} + limit, err := questionnaire.GetQuestionnaireLimit(ctx, questionnaireID) + if err != nil { + return err + } + if !limit.Valid { + return nil + } + for _, timing := range reiminderTimingMinutes { + remindTimeStamp := limit.Time.Add(-time.Duration(timing) * time.Minute) + if remindTimeStamp.After(time.Now()) { + Q.Push(&Job{ + Timestamp: remindTimeStamp, + QuestionnaireID: string(questionnaireID), + Action: func() { + reminderAction(questionnaireID, time.Until(limit.Time).String()) + }, + }) + + } + } + return nil +} + +func (q *JobQueue) DeleteReminder(questionnaireID int) error { + q.mu.Lock() + defer q.mu.Unlock() + for i, job := range q.jobs { + if job.QuestionnaireID == string(questionnaireID) { + q.jobs = append(q.jobs[:i], q.jobs[i+1:]...) + } + } + return nil +} + +func reminderAction(questionnaireID int, lestTimeString string) error { + ctx := context.Background() + questionnaire := model.Questionnaire{} + questionnaires, administorators, _, respondents, err := questionnaire.GetQuestionnaireInfo(ctx, questionnaireID) + if err != nil { + return err + } + + var remindeTargets []string + for _, target := range questionnaires.Targets { + if !target.IsCanceled { + if !slices.Contains(respondents, target.UserTraqid) { + remindeTargets = append(remindeTargets, target.UserTraqid) + } + } + } + + reminderMessage := createReminderMessage(questionnaireID, questionnaires.Title, questionnaires.Description, administorators, questionnaires.ResTimeLimit.Time, remindeTargets, lestTimeString) + wh := traq.NewWebhook() + err = wh.PostMessage(reminderMessage) + if err != nil { + return err + } + + return nil +} + +func ReminderWorker() { + for { + job := Q.Pop() + if job == nil { + time.Sleep(1 * time.Minute) + continue + } + if time.Until(job.Timestamp) > 0 { + time.Sleep(time.Until(job.Timestamp)) + } + Wg.Add(1) + go func() { + defer Wg.Done() + job.Action() + }() + } +} From 7947c16c791236ab804f884b19cb0d3be226c44b Mon Sep 17 00:00:00 2001 From: kaitoyama <45167401+kaitoyama@users.noreply.github.com> Date: Sat, 27 Apr 2024 13:29:52 +0900 Subject: [PATCH 09/12] Update Go version to 1.22 in GitHub Actions workflow --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5c8d7c8e..d865424e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -68,7 +68,7 @@ jobs: steps: - uses: actions/setup-go@v4 with: - go-version: '1.20' + go-version: '1.22' - uses: actions/checkout@v4 - uses: actions/cache@v3.3.2 with: From 621d698afc0a4d05a56e49acadf52350343193cf Mon Sep 17 00:00:00 2001 From: kaitoyama <45167401+kaitoyama@users.noreply.github.com> Date: Sat, 27 Apr 2024 13:30:34 +0900 Subject: [PATCH 10/12] Update Go version to 1.22 in GitHub Actions workflow --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d865424e..a7e2fd04 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/setup-go@v4 with: - go-version: '1.20' + go-version: '1.22' - uses: actions/checkout@v4 - uses: actions/cache@v3.3.2 with: @@ -33,7 +33,7 @@ jobs: steps: - uses: actions/setup-go@v4 with: - go-version: '1.20' + go-version: '1.22' - uses: actions/checkout@v4 - uses: actions/cache@v3.3.2 with: @@ -99,7 +99,7 @@ jobs: steps: - uses: actions/setup-go@v4 with: - go-version: '1.20' + go-version: '1.22' - uses: actions/checkout@v4 - name: golangci-lint uses: reviewdog/action-golangci-lint@v2.3 From 9d250c505df82c33c64ab1226de525b25a2e5eaa Mon Sep 17 00:00:00 2001 From: kaitoyama <45167401+kaitoyama@users.noreply.github.com> Date: Tue, 30 Apr 2024 12:49:10 +0900 Subject: [PATCH 11/12] Update Migrations function in current.go to include v3 migration Update main.go to add ReminderInit function call Update db_schema.md to add is_cancelled column Update IQuestionnaire interface in questionnaires.go to include GetQuestionnairesForReminder method Add v3.go file with v3 migration implementation Update GetQuestionnaireInfo function in questionnaires_impl.go to preload Targets Update PostQuestionnaire function in questionnaires.go to call PushReminder with res_time_limit Update EditQuestionnaire function in questionnaires.go to call PushReminder with res_time_limit Add ReminderInit function to questionnaires.go to push reminders for questionnaires with res_time_limit within 7 days --- docs/db_schema.md | 1 + main.go | 8 ++++++++ model/current.go | 4 +++- model/questionnaires.go | 1 + model/questionnaires_impl.go | 19 +++++++++++++++++++ model/v3.go | 28 ++++++++++++++++++++++++++++ router/questionnaires.go | 18 ++++++++++++++++-- router/reminder.go | 34 ++++++++++++++++++++-------------- 8 files changed, 96 insertions(+), 17 deletions(-) create mode 100644 model/v3.go diff --git a/docs/db_schema.md b/docs/db_schema.md index 45f30206..2db029b0 100644 --- a/docs/db_schema.md +++ b/docs/db_schema.md @@ -107,3 +107,4 @@ | ---------------- | -------- | ---- | --- | ------- | ----- | -------- | | questionnaire_id | int(11) | NO | PRI | _NULL_ | | user_traqid | char(32) | NO | PRI | _NULL_ | +| is_cancelled | boolean | NO | | false | | アンケートの対象者がキャンセルしたかどうか | diff --git a/main.go b/main.go index 6b54dd62..8652b915 100644 --- a/main.go +++ b/main.go @@ -59,6 +59,14 @@ func main() { router.Wg.Done() }() + router.Wg.Add(1) + go func() { + log.Println("ReminderInit") + router.ReminderInit() + log.Println("ReminderInitEnd") + router.Wg.Done() + }() + router.Wg.Add(1) go func() { router.ReminderWorker() diff --git a/model/current.go b/model/current.go index 9b3fea3e..973a7fda 100644 --- a/model/current.go +++ b/model/current.go @@ -6,7 +6,9 @@ import ( // Migrations is all db migrations func Migrations() []*gormigrate.Migration { - return []*gormigrate.Migration{} + return []*gormigrate.Migration{ + v3(), + } } func AllTables() []interface{} { diff --git a/model/questionnaires.go b/model/questionnaires.go index ec759d2e..36d596b4 100644 --- a/model/questionnaires.go +++ b/model/questionnaires.go @@ -21,4 +21,5 @@ type IQuestionnaire interface { GetQuestionnaireLimitByResponseID(ctx context.Context, responseID int) (null.Time, error) GetResponseReadPrivilegeInfoByResponseID(ctx context.Context, userID string, responseID int) (*ResponseReadPrivilegeInfo, error) GetResponseReadPrivilegeInfoByQuestionnaireID(ctx context.Context, userID string, questionnaireID int) (*ResponseReadPrivilegeInfo, error) + GetQuestionnairesForReminder(ctx context.Context) ([]Questionnaires, error) } diff --git a/model/questionnaires_impl.go b/model/questionnaires_impl.go index 4d2b3437..4e268d90 100755 --- a/model/questionnaires_impl.go +++ b/model/questionnaires_impl.go @@ -280,6 +280,7 @@ func (*Questionnaire) GetQuestionnaireInfo(ctx context.Context, questionnaireID err = db. Where("questionnaires.id = ?", questionnaireID). + Preload("Targets"). First(&questionnaire).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, nil, nil, ErrRecordNotFound @@ -380,6 +381,24 @@ func (*Questionnaire) GetQuestionnairesByLestTime(ctx context.Context) ([]Questi return questionnaires, nil } +// GetQuestionnaireInfoForReminder アンケートの詳細な情報を回答期限が7日以内のものだけ取得 +func (*Questionnaire) GetQuestionnairesForReminder(ctx context.Context) ([]Questionnaires, error) { + db, err := getTx(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get tx: %w", err) + } + + questionnaires := []Questionnaires{} + err = db. + Where("res_time_limit > ? AND res_time_limit < ?", time.Now(), time.Now().AddDate(0, 0, 7)). + Find(&questionnaires).Error + if err != nil { + return nil, fmt.Errorf("failed to get a questionnaire: %w", err) + } + + return questionnaires, nil +} + // GetQuestionnaireLimit アンケートの回答期限の取得 func (*Questionnaire) GetQuestionnaireLimit(ctx context.Context, questionnaireID int) (null.Time, error) { db, err := getTx(ctx) diff --git a/model/v3.go b/model/v3.go new file mode 100644 index 00000000..d11b9077 --- /dev/null +++ b/model/v3.go @@ -0,0 +1,28 @@ +package model + +import ( + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" +) + +func v3() *gormigrate.Migration { + return &gormigrate.Migration{ + ID: "3", + Migrate: func(tx *gorm.DB) error { + if err := tx.AutoMigrate(&v3Targets{}); err != nil { + return err + } + return nil + }, + } +} + +type v3Targets struct { + QuestionnaireID int `gorm:"type:int(11) AUTO_INCREMENT;not null;primaryKey"` + UserTraqid string `gorm:"type:varchar(32);size:32;not null;primaryKey"` + IsCanceled bool `gorm:"type:tinyint(1);not null;default:0"` +} + +func (*v3Targets) TableName() string { + return "targets" +} diff --git a/router/questionnaires.go b/router/questionnaires.go index 0cc76ea7..663f41e4 100644 --- a/router/questionnaires.go +++ b/router/questionnaires.go @@ -214,7 +214,7 @@ func (q *Questionnaire) PostQuestionnaire(c echo.Context) error { return echo.NewHTTPError(http.StatusInternalServerError, "failed to post message to traQ") } - err = Q.PushReminder(questionnaireID) + err = Q.PushReminder(questionnaireID, req.ResTimeLimit) if err != nil { c.Logger().Errorf("failed to push reminder: %+v", err) return echo.NewHTTPError(http.StatusInternalServerError, "failed to push reminder") @@ -451,7 +451,7 @@ func (q *Questionnaire) EditQuestionnaire(c echo.Context) error { return err } - err = Q.PushReminder(questionnaireID) + err = Q.PushReminder(questionnaireID, req.ResTimeLimit) if err != nil { c.Logger().Errorf("failed to push reminder: %+v", err) return err @@ -717,3 +717,17 @@ https://anke-to.trap.jp/responses/new/%d`, questionnaireID, ) } + +// DB中のquestionnaireをもとにリマインダーを設定する +func ReminderInit() { + questionnaires, err := model.NewQuestionnaire().GetQuestionnairesForReminder(context.Background()) + if err != nil { + panic(err) + } + for _, questionnaire := range questionnaires { + err := Q.PushReminder(questionnaire.ID, questionnaire.ResTimeLimit) + if err != nil { + panic(err) + } + } +} diff --git a/router/reminder.go b/router/reminder.go index 39707fa7..cc113078 100644 --- a/router/reminder.go +++ b/router/reminder.go @@ -2,6 +2,7 @@ package router import ( "context" + "log" "slices" "sort" "sync" @@ -10,6 +11,7 @@ import ( "github.com/traPtitech/anke-to/model" "github.com/traPtitech/anke-to/traq" "golang.org/x/sync/semaphore" + "gopkg.in/guregu/null.v4" ) type Job struct { @@ -24,12 +26,14 @@ type JobQueue struct { } var ( - sem = semaphore.NewWeighted(1) - Q = &JobQueue{} - Wg = &sync.WaitGroup{} - reiminderTimingMinutes = []int{5, 30, 60, 1440, 10080} + sem = semaphore.NewWeighted(1) + Q = &JobQueue{} + Wg = &sync.WaitGroup{} + reminderTimingMinutes = []int{5, 30, 60, 1440, 10080} + reminderTimingStrings = []string{"5分", "30分", "1時間", "1日", "1週間"} ) +// jobQueueにjobを追加する func (q *JobQueue) Push(j *Job) { q.mu.Lock() defer q.mu.Unlock() @@ -39,6 +43,7 @@ func (q *JobQueue) Push(j *Job) { }) } +// jobQueueからjobを取り出す func (q *JobQueue) Pop() *Job { q.mu.Lock() defer q.mu.Unlock() @@ -50,24 +55,20 @@ func (q *JobQueue) Pop() *Job { return job } -func (q *JobQueue) PushReminder(questionnaireID int) error { - ctx := context.Background() - questionnaire := model.Questionnaire{} - limit, err := questionnaire.GetQuestionnaireLimit(ctx, questionnaireID) - if err != nil { - return err - } +// jobQueueにquestionnaireIDに対応するアンケートのリマインダーを追加する +func (q *JobQueue) PushReminder(questionnaireID int, limit null.Time) error { if !limit.Valid { return nil } - for _, timing := range reiminderTimingMinutes { + log.Printf("[DEBUG] PushReminder: questionnaireID=%d, limit=%s\n", questionnaireID, limit.Time.String()) + for i, timing := range reminderTimingMinutes { remindTimeStamp := limit.Time.Add(-time.Duration(timing) * time.Minute) if remindTimeStamp.After(time.Now()) { Q.Push(&Job{ Timestamp: remindTimeStamp, QuestionnaireID: string(questionnaireID), Action: func() { - reminderAction(questionnaireID, time.Until(limit.Time).String()) + reminderAction(questionnaireID, reminderTimingStrings[i]) }, }) @@ -76,6 +77,7 @@ func (q *JobQueue) PushReminder(questionnaireID int) error { return nil } +// jobQueueからquestionnaireIDに対応するアンケートのリマインダーを削除する func (q *JobQueue) DeleteReminder(questionnaireID int) error { q.mu.Lock() defer q.mu.Unlock() @@ -87,10 +89,11 @@ func (q *JobQueue) DeleteReminder(questionnaireID int) error { return nil } +// リマインダーのメッセージを送信する func reminderAction(questionnaireID int, lestTimeString string) error { ctx := context.Background() questionnaire := model.Questionnaire{} - questionnaires, administorators, _, respondents, err := questionnaire.GetQuestionnaireInfo(ctx, questionnaireID) + questionnaires, _, administorators, respondents, err := questionnaire.GetQuestionnaireInfo(ctx, questionnaireID) if err != nil { return err } @@ -103,6 +106,8 @@ func reminderAction(questionnaireID int, lestTimeString string) error { } } } + log.Printf("[DEBUG] questionnaires.Targets=%v", questionnaires.Targets) + log.Printf("[DEBUG] reminderAction: questionnaireID=%d, title=%s, description=%s, administorators=%v, resTimeLimit=%s, remindeTargets=%v, lestTimeString=%s\n", questionnaireID, questionnaires.Title, questionnaires.Description, administorators, questionnaires.ResTimeLimit.Time.String(), remindeTargets, lestTimeString) reminderMessage := createReminderMessage(questionnaireID, questionnaires.Title, questionnaires.Description, administorators, questionnaires.ResTimeLimit.Time, remindeTargets, lestTimeString) wh := traq.NewWebhook() @@ -114,6 +119,7 @@ func reminderAction(questionnaireID int, lestTimeString string) error { return nil } +// リマインダーのメッセージを作成する func ReminderWorker() { for { job := Q.Pop() From fccac5c235801f6f6d7a97cc4af5979c97cc4ce9 Mon Sep 17 00:00:00 2001 From: kaitoyama <45167401+kaitoyama@users.noreply.github.com> Date: Tue, 30 Apr 2024 12:55:50 +0900 Subject: [PATCH 12/12] Fix DeleteReminder method in reminder.go to handle the case when there is only one job --- router/reminder.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/router/reminder.go b/router/reminder.go index cc113078..d0bb81a6 100644 --- a/router/reminder.go +++ b/router/reminder.go @@ -82,8 +82,12 @@ func (q *JobQueue) DeleteReminder(questionnaireID int) error { q.mu.Lock() defer q.mu.Unlock() for i, job := range q.jobs { - if job.QuestionnaireID == string(questionnaireID) { - q.jobs = append(q.jobs[:i], q.jobs[i+1:]...) + if len(q.jobs) == 1 { + q.jobs = []*Job{} + } else { + if job.QuestionnaireID == string(questionnaireID) { + q.jobs = append(q.jobs[:i], q.jobs[i+1:]...) + } } } return nil