From 3bb5c767aa98721ddba361de8d6256092be68310 Mon Sep 17 00:00:00 2001 From: 0xZensh Date: Sat, 21 Oct 2023 13:27:59 +0800 Subject: [PATCH] feat: implement collection and payment --- .github/workflows/build.yml | 20 +- .github/workflows/ci.yml | 28 +-- Makefile | 2 +- cmd/keys/main.go | 49 ++++ cmd/tiktoken/main.go | 48 ++++ config/default.toml | 4 + go.mod | 7 +- go.sum | 24 +- keys/aesgcm.key | 1 + keys/hmac.key | 1 + src/api/collection.go | 384 +++++++++++++++++++++++++++++++ src/api/creation.go | 46 +++- src/api/group.go | 2 +- src/api/jarvis.go | 4 +- src/api/message.go | 33 ++- src/api/payment.go | 315 +++++++++++++++++++++++++ src/api/publication.go | 100 ++++---- src/api/router.go | 26 ++- src/bll/common.go | 104 ++++++++- src/bll/jarvis.go | 2 +- src/bll/logbase.go | 1 + src/bll/userbase.go | 18 ++ src/bll/walletbase.go | 18 +- src/bll/writing.go | 2 +- src/bll/writing_bookmark.go | 45 ++-- src/bll/writing_collection.go | 309 +++++++++++++++++++++++++ src/bll/writing_creation.go | 68 ++++-- src/bll/writing_creation_test.go | 2 +- src/bll/writing_message.go | 1 + src/bll/writing_publication.go | 93 ++++---- src/conf/config.go | 41 ++++ src/service/oss.go | 3 + src/util/cose.go | 69 ++++++ src/util/cose_test.go | 76 ++++++ src/util/id.go | 17 ++ src/util/id_test.go | 4 + 36 files changed, 1732 insertions(+), 235 deletions(-) create mode 100644 cmd/keys/main.go create mode 100644 cmd/tiktoken/main.go create mode 100644 keys/aesgcm.key create mode 100644 keys/hmac.key create mode 100644 src/api/collection.go create mode 100644 src/api/payment.go create mode 100644 src/bll/writing_collection.go create mode 100644 src/util/cose.go create mode 100644 src/util/cose_test.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e413706..b2f2899 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,13 +9,13 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: Swatinem/rust-cache@v2 - - name: Build the Docker image - run: | - docker run --rm --privileged multiarch/qemu-user-static --reset -p yes - docker buildx create --use - docker login --username ${{ secrets.CR_USERNAME }} --password ${{ secrets.CR_PASSWORD }} ${{ secrets.CR_REGISTRY }} - IMAGE_TAG="${{ secrets.CR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}" - TAGS="-t ${IMAGE_TAG}" - docker buildx build --platform='linux/amd64,linux/arm64' $TAGS --push . + - uses: actions/checkout@v3 + - uses: Swatinem/rust-cache@v2 + - name: Build the Docker image + run: | + docker run --rm --privileged multiarch/qemu-user-static --reset -p yes + docker buildx create --use + docker login --username ${{ secrets.CR_USERNAME }} --password ${{ secrets.CR_PASSWORD }} ${{ secrets.CR_REGISTRY }} + IMAGE_TAG="${{ secrets.CR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}" + TAGS="-t ${IMAGE_TAG}" + docker buildx build --platform='linux/amd64,linux/arm64' $TAGS --push . diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2416c4d..6daee3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,21 +14,21 @@ jobs: go-version: ['1.21.x'] steps: - - name: Install Go - uses: actions/setup-go@v4 - with: - go-version: ${{ matrix.go-version }} + - name: Install Go + uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go-version }} - - name: Checkout code - uses: actions/checkout@v3 - with: - fetch-depth: 1 + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 1 - - name: Print Go version - run: go version + - name: Print Go version + run: go version - - name: Get dependencies - run: go get -v -t -d ./... + - name: Get dependencies + run: go get -v -t -d ./... - - name: Run tests - run: go test -v -failfast -tags=test -timeout="3m" -race ./... + - name: Run tests + run: make test diff --git a/Makefile b/Makefile index 79d8122..593694f 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ run-dev: @CONFIG_FILE_PATH=${PWD}/config.toml APP_ENV=dev go run main.go test: - @CONFIG_FILE_PATH=${PWD}/config/default.toml APP_ENV=test go test ./... + @EXEC_DIR_PATH=${PWD} CONFIG_FILE_PATH=${PWD}/config/default.toml APP_ENV=test go test -v -failfast -tags=test -timeout="3m" -race ./... lint: @hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ diff --git a/cmd/keys/main.go b/cmd/keys/main.go new file mode 100644 index 0000000..8cd492a --- /dev/null +++ b/cmd/keys/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "encoding/base64" + "flag" + "os" + + "github.com/fxamacker/cbor/v2" + "github.com/ldclabs/cose/iana" + "github.com/ldclabs/cose/key" + "github.com/ldclabs/cose/key/aesgcm" + "github.com/ldclabs/cose/key/hmac" +) + +var kind = flag.String("kind", "state", "generate key for kind") +var out = flag.String("out", "./keys/out.key", "write key to a file") + +func main() { + flag.Parse() + + var err error + var k key.Key + var data []byte + + switch *kind { + case "hmac": + k, err = hmac.GenerateKey(iana.AlgorithmHMAC_256_64) + case "aesgcm": + k, err = aesgcm.GenerateKey(iana.AlgorithmA256GCM) + default: + panic("unsupported kind") + } + + if err == nil { + // data, err = k.MarshalCBOR() + data, err = cbor.Marshal(cbor.Tag{ + Number: 55799, // self described CBOR Tag + Content: k, + }) + } + + if err == nil { + err = os.WriteFile(*out, []byte(base64.RawURLEncoding.EncodeToString(data)), 0644) + } + + if err != nil { + panic(err) + } +} diff --git a/cmd/tiktoken/main.go b/cmd/tiktoken/main.go new file mode 100644 index 0000000..6c5b8f1 --- /dev/null +++ b/cmd/tiktoken/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "flag" + "fmt" + "os" + "unicode/utf8" + + "github.com/pkoukk/tiktoken-go" + tiktoken_loader "github.com/pkoukk/tiktoken-go-loader" +) + +var help = flag.Bool("help", false, "show help info") +var version = flag.Bool("version", false, "show version info") + +func main() { + flag.Parse() + if *help || *version { + fmt.Println("tiktoken example.txt") + os.Exit(0) + } + + args := flag.Args() + if len(args) == 0 { + fmt.Println("tiktoken example.txt") + os.Exit(0) + } + file, err := os.ReadFile(args[0]) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + text := string(file) + if !utf8.ValidString(text) { + fmt.Println("invalid utf8 text") + os.Exit(1) + } + + tiktoken.SetBpeLoader(tiktoken_loader.NewOfflineLoader()) + tk, err := tiktoken.GetEncoding("cl100k_base") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + fmt.Printf("%s %d tokens\n", args[0], len(tk.Encode(text, nil, nil))) + os.Exit(0) +} diff --git a/config/default.toml b/config/default.toml index cbca2a1..b8eb52d 100644 --- a/config/default.toml +++ b/config/default.toml @@ -10,6 +10,10 @@ addr = ":8080" # The maximum number of seconds to wait for graceful shutdown. graceful_shutdown = 10 +[keys] +hmac = "./keys/hmac.key" +aesgcm = "./keys/aesgcm.key" + [redis] prefix = "YWAPI:" node = "127.0.0.1:6379" diff --git a/go.mod b/go.mod index 6fc1931..bae3f4b 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,12 @@ require ( github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible github.com/bsm/redislock v0.9.4 github.com/fxamacker/cbor/v2 v2.5.0 - github.com/gabriel-vasile/mimetype v1.4.2 + github.com/gabriel-vasile/mimetype v1.4.3 github.com/go-playground/validator/v10 v10.15.5 github.com/google/uuid v1.3.1 github.com/jaevor/go-nanoid v1.3.0 - github.com/klauspost/compress v1.17.0 + github.com/klauspost/compress v1.17.1 + github.com/ldclabs/cose v1.1.2 github.com/pkoukk/tiktoken-go v0.1.6 github.com/pkoukk/tiktoken-go-loader v0.0.1 github.com/redis/go-redis/v9 v9.2.1 @@ -19,7 +20,7 @@ require ( github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d github.com/stretchr/testify v1.8.4 github.com/teambition/gear v1.27.3 - go.uber.org/dig v1.17.0 + go.uber.org/dig v1.17.1 golang.org/x/text v0.13.0 ) diff --git a/go.sum b/go.sum index afe3e0c..ad758a9 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/go-http-utils/cookie v1.3.1 h1:GCdTeqVV5vDcjP7LrgYpH8pbt3dOYKS+Wrs7Jo3/k/w= github.com/go-http-utils/cookie v1.3.1/go.mod h1:ATl4rfG3bEemjiVa+8WIfgNcBUWdYBTasfXKjJ3Avt8= github.com/go-http-utils/negotiator v1.0.0 h1:Qp1zofD6Nw7KXApXa3pAjehP06Js0ILguEBCnHhZeVA= @@ -39,10 +39,14 @@ github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/jaevor/go-nanoid v1.3.0 h1:nD+iepesZS6pr3uOVf20vR9GdGgJW1HPaR46gtrxzkg= github.com/jaevor/go-nanoid v1.3.0/go.mod h1:SI+jFaPuddYkqkVQoNGHs81navCtH388TcrH0RqFKgY= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= -github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.1 h1:NE3C767s2ak2bweCZo3+rdP4U/HoyVXLv/X9f2gPS5g= +github.com/klauspost/compress v1.17.1/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/ldclabs/cose v1.1.2 h1:kq8IkpCiTM2jcynmPbEUH4dPQ4tM8+qQewKMvuC/ljo= +github.com/ldclabs/cose v1.1.2/go.mod h1:M52HratClumnAkI1icUIUljX4fWfZL7kF80hh6ijGrQ= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw= github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= github.com/pkoukk/tiktoken-go-loader v0.0.1 h1:aOB2gRFzZTCCPi3YsOQXJO771P/5876JAsdebMyazig= @@ -70,20 +74,12 @@ github.com/teambition/trie-mux v1.5.2 h1:ALTagFwKZXkn1vfSRlODlmoZg+NMeWAm4dyBPQI github.com/teambition/trie-mux v1.5.2/go.mod h1:0Woh4KOHSN9bkJ66eWmLs8ltrEKw+fnZbFaHFfbMrtc= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -go.uber.org/dig v1.17.0 h1:5Chju+tUvcC+N7N6EV08BJz41UZuO3BmHcN4A287ZLI= -go.uber.org/dig v1.17.0/go.mod h1:rTxpf7l5I0eBTlE6/9RL+lDybC7WFwY2QH55ZSjy1mU= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= +go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= -golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= diff --git a/keys/aesgcm.key b/keys/aesgcm.key new file mode 100644 index 0000000..d3e245f --- /dev/null +++ b/keys/aesgcm.key @@ -0,0 +1 @@ +2dn3pAEEAlRkHwywt4RHc4QPkRFC1Tzcuov7CgMDIFggtG5vgaB8sZJqQ1HDpQ78QBQLYO7HHUnXutsovGCAArI \ No newline at end of file diff --git a/keys/hmac.key b/keys/hmac.key new file mode 100644 index 0000000..e2e2370 --- /dev/null +++ b/keys/hmac.key @@ -0,0 +1 @@ +2dn3pAEEAlScZRWIAJlBA18cw7mHo3bB03xW9gMEIFggBQ13neTam4zLiMMLn2Hk6J5jT3TQlS5eHegh4AO_Qh0 \ No newline at end of file diff --git a/src/api/collection.go b/src/api/collection.go new file mode 100644 index 0000000..f0615c7 --- /dev/null +++ b/src/api/collection.go @@ -0,0 +1,384 @@ +package api + +import ( + "github.com/teambition/gear" + + "github.com/yiwen-ai/yiwen-api/src/bll" + "github.com/yiwen-ai/yiwen-api/src/middleware" + "github.com/yiwen-ai/yiwen-api/src/service" + "github.com/yiwen-ai/yiwen-api/src/util" +) + +type Collection struct { + blls *bll.Blls +} + +func (a *Collection) Get(ctx *gear.Context) error { + input := &bll.QueryGidID{} + if err := ctx.ParseURL(input); err != nil { + return err + } + role, _ := a.checkReadPermission(ctx, input.GID) + status := int8(2) + switch role { + case 2, 1: + status = -1 + case 0: + status = 0 + case -1: + status = 1 + } + + output, err := a.blls.Writing.GetCollection(ctx, input, status) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + if output.Subscription != nil { + subtoken, err := util.EncodeMac0(a.blls.MACer, SubscriptionToken{ + Kind: 2, + ExpireAt: output.Subscription.ExpireAt, + UID: output.Subscription.UID, + CID: output.Subscription.CID, + GID: output.Subscription.GID, + }, []byte("SubscriptionToken")) + if err == nil { + output.SubToken = &subtoken + } + } + result := bll.CollectionOutputs{*output} + result.LoadGroups(func(ids ...util.ID) []bll.GroupInfo { + return a.blls.Userbase.LoadGroupInfo(ctx, ids...) + }) + + return ctx.OkSend(bll.SuccessResponse[*bll.CollectionOutput]{Result: &result[0]}) +} + +func (a *Collection) ListByChild(ctx *gear.Context) error { + input := &bll.QueryGidCid{} + if err := ctx.ParseURL(input); err != nil { + return err + } + role, _ := a.checkReadPermission(ctx, input.GID) + input.Status = int8(2) + switch role { + case 2, 1, 0: + input.Status = 0 + case -1: + input.Status = 1 + } + + output, err := a.blls.Writing.ListCollectionByChild(ctx, input) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + for i := range output.Result { + if s := output.Result[i].Subscription; s != nil { + subtoken, err := util.EncodeMac0(a.blls.MACer, SubscriptionToken{ + Kind: 2, + ExpireAt: s.ExpireAt, + UID: s.UID, + CID: s.CID, + GID: s.GID, + }, []byte("SubscriptionToken")) + if err == nil { + output.Result[i].SubToken = &subtoken + } + } + } + + output.Result.LoadGroups(func(ids ...util.ID) []bll.GroupInfo { + return a.blls.Userbase.LoadGroupInfo(ctx, ids...) + }) + + return ctx.OkSend(output) +} + +func (a *Collection) ListChildren(ctx *gear.Context) error { + input := &bll.IDGIDPagination{} + if err := ctx.ParseBody(input); err != nil { + return err + } + + role, _ := a.checkReadPermission(ctx, input.GID) + input.Status = util.Ptr(int8(2)) + switch role { + case 2, 1, 0: + input.Status = util.Ptr(int8(0)) + case -1: + input.Status = util.Ptr(int8(1)) + } + + output, err := a.blls.Writing.ListCollectionChildren(ctx, input) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + + output.Result.LoadGroups(func(ids ...util.ID) []bll.GroupInfo { + return a.blls.Userbase.LoadGroupInfo(ctx, ids...) + }) + + return ctx.OkSend(output) +} + +func (a *Collection) List(ctx *gear.Context) error { + input := &bll.GIDPagination{} + if err := ctx.ParseBody(input); err != nil { + return err + } + + role, _ := a.checkReadPermission(ctx, input.GID) + input.Status = util.Ptr(int8(2)) + switch role { + case 2, 1, 0: + input.Status = util.Ptr(int8(0)) + case -1: + input.Status = util.Ptr(int8(1)) + } + + output, err := a.blls.Writing.ListCollection(ctx, input) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + + output.Result.LoadGroups(func(ids ...util.ID) []bll.GroupInfo { + return a.blls.Userbase.LoadGroupInfo(ctx, ids...) + }) + + return ctx.OkSend(output) +} + +func (a *Collection) ListArchived(ctx *gear.Context) error { + input := &bll.GIDPagination{} + if err := ctx.ParseBody(input); err != nil { + return err + } + + if err := a.checkWritePermission(ctx, input.GID); err != nil { + return err + } + + input.Status = util.Ptr(int8(-1)) + output, err := a.blls.Writing.ListCollection(ctx, input) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + + output.Result.LoadGroups(func(ids ...util.ID) []bll.GroupInfo { + return a.blls.Userbase.LoadGroupInfo(ctx, ids...) + }) + + return ctx.OkSend(output) +} + +func (a *Collection) Create(ctx *gear.Context) error { + input := &bll.CreateCollectionInput{} + if err := ctx.ParseBody(input); err != nil { + return err + } + + if err := a.checkWritePermission(ctx, input.GID); err != nil { + return err + } + + output, err := a.blls.Writing.CreateCollection(ctx, input) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + return ctx.OkSend(bll.SuccessResponse[*bll.CollectionOutput]{Result: output}) +} + +func (a *Collection) Update(ctx *gear.Context) error { + input := &bll.UpdateCollectionInput{} + if err := ctx.ParseBody(input); err != nil { + return err + } + + if err := a.checkWritePermission(ctx, input.GID); err != nil { + return err + } + + output, err := a.blls.Writing.UpdateCollection(ctx, input) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + return ctx.OkSend(bll.SuccessResponse[*bll.CollectionOutput]{Result: output}) +} + +func (a *Collection) Delete(ctx *gear.Context) error { + input := &bll.QueryGidID{} + if err := ctx.ParseURL(input); err != nil { + return err + } + + if err := a.checkWritePermission(ctx, input.GID); err != nil { + return err + } + + output, err := a.blls.Writing.DeleteCollection(ctx, input) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + return ctx.OkSend(bll.SuccessResponse[bool]{Result: output}) +} + +func (a *Collection) GetInfo(ctx *gear.Context) error { + input := &bll.QueryGidID{} + if err := ctx.ParseURL(input); err != nil { + return err + } + + if err := a.checkWritePermission(ctx, input.GID); err != nil { + return err + } + + output, err := a.blls.Writing.GetCollectionInfo(ctx, input) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + return ctx.OkSend(bll.SuccessResponse[*bll.MessageOutput]{Result: output}) +} + +func (a *Collection) UpdateInfo(ctx *gear.Context) error { + input := &bll.UpdateMessageInput{} + if err := ctx.ParseBody(input); err != nil { + return err + } + + if err := a.checkWritePermission(ctx, input.GID); err != nil { + return err + } + + output, err := a.blls.Writing.UpdateCollectionInfo(ctx, input) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + return ctx.OkSend(bll.SuccessResponse[*bll.MessageOutput]{Result: output}) +} + +func (a *Collection) UpdateStatus(ctx *gear.Context) error { + input := &bll.UpdateStatusInput{} + if err := ctx.ParseBody(input); err != nil { + return err + } + + if err := a.checkWritePermission(ctx, input.GID); err != nil { + return err + } + + output, err := a.blls.Writing.UpdateCollectionStatus(ctx, input) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + + return ctx.OkSend(bll.SuccessResponse[*bll.CollectionOutput]{Result: output}) +} + +func (a *Collection) AddChildren(ctx *gear.Context) error { + input := &bll.AddCollectionChildrenInput{} + if err := ctx.ParseBody(input); err != nil { + return err + } + + if err := a.checkWritePermission(ctx, input.GID); err != nil { + return err + } + + output, err := a.blls.Writing.AddCollectionChildren(ctx, input) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + return ctx.OkSend(bll.SuccessResponse[[]util.ID]{Result: output}) +} + +func (a *Collection) UpdateChild(ctx *gear.Context) error { + input := &bll.UpdateCollectionChildInput{} + if err := ctx.ParseBody(input); err != nil { + return err + } + + if err := a.checkWritePermission(ctx, input.GID); err != nil { + return err + } + + output, err := a.blls.Writing.UpdateCollectionChild(ctx, input) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + return ctx.OkSend(bll.SuccessResponse[bool]{Result: output}) +} + +func (a *Collection) RemoveChild(ctx *gear.Context) error { + input := &bll.QueryGidIdCid{} + if err := ctx.ParseURL(input); err != nil { + return err + } + + if err := a.checkWritePermission(ctx, input.GID); err != nil { + return err + } + + output, err := a.blls.Writing.RemoveCollectionChild(ctx, input) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + return ctx.OkSend(bll.SuccessResponse[bool]{Result: output}) +} + +func (a *Collection) UploadFile(ctx *gear.Context) error { + input := &bll.QueryGidID{} + if err := ctx.ParseURL(input); err != nil { + return err + } + + err := a.checkWritePermission(ctx, input.GID) + if err != nil { + return err + } + + input.Fields = "gid,status" + doc, err := a.blls.Writing.GetCollection(ctx, input, -1) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + + if *doc.Status < 0 { + return gear.ErrBadRequest.WithMsg("collection archived") + } + + output := a.blls.Writing.SignPostPolicy(doc.GID, doc.ID, "", 0) + return ctx.OkSend(bll.SuccessResponse[service.PostFilePolicy]{Result: output}) +} + +func (a *Collection) checkReadPermission(ctx *gear.Context, gid util.ID) (int8, error) { + sess := gear.CtxValue[middleware.Session](ctx) + if sess == nil || sess.UserID.Compare(util.MinID) <= 0 { + return -2, gear.ErrForbidden.WithMsg("no permission") + } + + role, err := a.blls.Userbase.UserGroupRole(ctx, sess.UserID, gid) + if err != nil { + return -2, gear.ErrNotFound.From(err) + } + if role < -1 { + return role, gear.ErrForbidden.WithMsg("no permission") + } + + return role, nil +} + +func (a *Collection) checkWritePermission(ctx *gear.Context, gid util.ID) error { + sess := gear.CtxValue[middleware.Session](ctx) + if sess == nil || sess.UserID.Compare(util.MinID) <= 0 { + return gear.ErrForbidden.WithMsg("no permission") + } + + role, err := a.blls.Userbase.UserGroupRole(ctx, sess.UserID, gid) + if err != nil { + return gear.ErrNotFound.From(err) + } + if role <= 0 { + return gear.ErrForbidden.WithMsg("no permission") + } + + return nil +} diff --git a/src/api/creation.go b/src/api/creation.go index 4d7fbd1..0c2b697 100644 --- a/src/api/creation.go +++ b/src/api/creation.go @@ -79,7 +79,7 @@ func (a *Creation) Create(ctx *gear.Context) error { } func (a *Creation) Get(ctx *gear.Context) error { - input := &bll.QueryCreation{} + input := &bll.QueryGidID{} if err := ctx.ParseURL(input); err != nil { return err } @@ -137,7 +137,7 @@ func (a *Creation) Update(ctx *gear.Context) error { } func (a *Creation) Delete(ctx *gear.Context) error { - input := &bll.QueryCreation{} + input := &bll.QueryGidID{} if err := ctx.ParseURL(input); err != nil { return err } @@ -218,7 +218,7 @@ func (a *Creation) ListArchived(ctx *gear.Context) error { } func (a *Creation) Archive(ctx *gear.Context) error { - input := &bll.UpdateCreationStatusInput{} + input := &bll.UpdateStatusInput{} if err := ctx.ParseBody(input); err != nil { return err } @@ -246,7 +246,7 @@ func (a *Creation) Archive(ctx *gear.Context) error { } func (a *Creation) Redraft(ctx *gear.Context) error { - input := &bll.UpdateCreationStatusInput{} + input := &bll.UpdateStatusInput{} if err := ctx.ParseBody(input); err != nil { return err } @@ -274,7 +274,7 @@ func (a *Creation) Redraft(ctx *gear.Context) error { } func (a *Creation) checkTokens(ctx *gear.Context, gid, cid util.ID) error { - src, err := a.blls.Writing.GetCreation(ctx, &bll.QueryCreation{ + src, err := a.blls.Writing.GetCreation(ctx, &bll.QueryGidID{ GID: gid, ID: cid, Fields: "content", @@ -399,7 +399,7 @@ func (a *Creation) release(gctx context.Context, creation *bll.CreationOutput, a } // 用户私有 group 自动提升 status,无需 review 和 approve - statusInput := &bll.UpdateCreationStatusInput{ + statusInput := &bll.UpdateStatusInput{ GID: creation.GID, ID: creation.ID, } @@ -487,7 +487,7 @@ func (a *Creation) UpdateContent(ctx *gear.Context) error { } func (a *Creation) UploadFile(ctx *gear.Context) error { - input := &bll.QueryCreation{} + input := &bll.QueryGidID{} if err := ctx.ParseBody(input); err != nil { return err } @@ -505,6 +505,34 @@ func (a *Creation) UploadFile(ctx *gear.Context) error { return ctx.OkSend(bll.SuccessResponse[service.PostFilePolicy]{Result: output}) } +func (a *Creation) UpdatePrice(ctx *gear.Context) error { + input := &bll.UpdateCreationPriceInput{} + if err := ctx.ParseBody(input); err != nil { + return err + } + + doc, err := a.checkWritePermission(ctx, input.GID, input.ID) + if err != nil { + return err + } + + _, err = a.blls.Writing.UpdateCreationPrice(ctx, input) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + + if _, err = a.blls.Logbase.Log(ctx, bll.LogActionCreationUpdate, 1, input.GID, &bll.LogPayload{ + GID: input.GID, + CID: input.ID, + Price: &input.Price, + }); err != nil { + logging.SetTo(ctx, "writeLogError", err.Error()) + } + + doc.Price = &input.Price + return ctx.OkSend(bll.SuccessResponse[*bll.CreationOutput]{Result: doc}) +} + func (a *Creation) checkReadPermission(ctx *gear.Context, gid util.ID) error { sess := gear.CtxValue[middleware.Session](ctx) role, err := a.blls.Userbase.UserGroupRole(ctx, sess.UserID, gid) @@ -541,7 +569,7 @@ func (a *Creation) checkWritePermission(ctx *gear.Context, gid, cid util.ID) (*b return nil, gear.ErrForbidden.WithMsg("no permission") } - creation, err := a.blls.Writing.GetCreation(ctx, &bll.QueryCreation{ + creation, err := a.blls.Writing.GetCreation(ctx, &bll.QueryGidID{ GID: gid, ID: cid, Fields: "status,creator,updated_at,language,version", @@ -563,7 +591,7 @@ func (a *Creation) checkWritePermission(ctx *gear.Context, gid, cid util.ID) (*b // summarize and embedding when updating status from 1 to 2 func (a *Creation) summarize(gctx context.Context, gid, cid util.ID, auditLog *bll.UpdateLog) (*bll.CreationOutput, error) { - creation, err := a.blls.Writing.GetCreation(gctx, &bll.QueryCreation{ + creation, err := a.blls.Writing.GetCreation(gctx, &bll.QueryGidID{ GID: gid, ID: cid, Fields: "status,creator,updated_at,language,version,keywords,summary,content", diff --git a/src/api/group.go b/src/api/group.go index 06bb76e..6229d5a 100644 --- a/src/api/group.go +++ b/src/api/group.go @@ -83,7 +83,7 @@ func (a *Group) GetInfo(ctx *gear.Context) error { return ctx.OkSend(bll.SuccessResponse[*bll.GroupInfo]{Result: res}) } -func (a *Group) UploadLogo(ctx *gear.Context) error { +func (a *Group) UploadPicture(ctx *gear.Context) error { input := &bll.QueryIdCn{} err := ctx.ParseURL(input) if err != nil { diff --git a/src/api/jarvis.go b/src/api/jarvis.go index 56f5410..4598649 100644 --- a/src/api/jarvis.go +++ b/src/api/jarvis.go @@ -126,7 +126,7 @@ func (a *Jarvis) Search(ctx *gear.Context) error { CID: item.CID, Language: item.Language, Fields: "status,updated_at,title,summary", - }); err == nil && *doc.Status == 2 { + }, nil); err == nil && *doc.Status == 2 { v := bll.SearchDocument{ GID: doc.GID, CID: doc.CID, @@ -251,7 +251,7 @@ func (a *Jarvis) GroupSearch(ctx *gear.Context) error { CID: item.CID, Language: item.Language, Fields: "updated_at,title,summary", - }); err == nil { + }, nil); err == nil { v := bll.SearchDocument{ GID: doc.GID, CID: doc.CID, diff --git a/src/api/message.go b/src/api/message.go index 61b6f96..5665711 100644 --- a/src/api/message.go +++ b/src/api/message.go @@ -65,17 +65,7 @@ func (a *Message) Update(ctx *gear.Context) error { } } - msg, err := a.blls.Writing.GetMessage(ctx, &bll.QueryID{ - ID: input.ID, Fields: "version,attach_to", - }) - if err != nil { - return gear.ErrInternalServerError.From(err) - } - if *msg.Version != input.Version { - return gear.ErrBadRequest.WithMsg("version mismatch") - } - - if err := a.checkWritePermission(ctx, *msg.AttachTo); err != nil { + if err := a.checkWritePermission(ctx, input.GID); err != nil { return err } @@ -84,9 +74,9 @@ func (a *Message) Update(ctx *gear.Context) error { return gear.ErrInternalServerError.From(err) } - if _, err = a.blls.Logbase.Log(ctx, bll.LogActionMessageUpdate, 1, *msg.AttachTo, &bll.LogMessage{ + if _, err = a.blls.Logbase.Log(ctx, bll.LogActionMessageUpdate, 1, input.GID, &bll.LogMessage{ ID: input.ID, - AttachTo: *msg.AttachTo, + AttachTo: input.GID, Language: input.Language, Version: &input.Version, }); err != nil { @@ -106,6 +96,10 @@ func (a *Message) UpdateI18n(ctx *gear.Context) error { return gear.ErrBadRequest.WithMsg("invalid language") } + if err := a.checkWritePermission(ctx, input.GID); err != nil { + return err + } + lang := *input.Language msg, err := a.blls.Writing.GetMessage(ctx, &bll.QueryID{ ID: input.ID, Fields: "version,language,attach_to,context,message," + lang, @@ -119,9 +113,8 @@ func (a *Message) UpdateI18n(ctx *gear.Context) error { if *msg.Language == *input.Language { return gear.ErrBadRequest.WithMsg("language is the same") } - - if err := a.checkWritePermission(ctx, *msg.AttachTo); err != nil { - return err + if *msg.AttachTo != input.GID { + return gear.ErrForbidden.WithMsg("no permission") } var kv bll.KVMessage @@ -139,6 +132,10 @@ func (a *Message) UpdateI18n(ctx *gear.Context) error { } } + if len(kv) == 0 { + return gear.ErrBadRequest.WithMsg("no need to translate") + } + sess := gear.CtxValue[middleware.Session](ctx) wallet, err := a.blls.Walletbase.Get(ctx, sess.UserID) if err != nil { @@ -244,11 +241,11 @@ func (a *Message) UpdateI18n(ctx *gear.Context) error { } if err == nil { - err = a.blls.Walletbase.CommitExpending(gctx, txn) + err = a.blls.Walletbase.CommitTxn(gctx, txn) } if err != nil { - _ = a.blls.Walletbase.CancelExpending(gctx, txn) + _ = a.blls.Walletbase.CancelTxn(gctx, txn) } } } diff --git a/src/api/payment.go b/src/api/payment.go new file mode 100644 index 0000000..b6d0849 --- /dev/null +++ b/src/api/payment.go @@ -0,0 +1,315 @@ +package api + +import ( + "time" + + "github.com/teambition/gear" + + "github.com/yiwen-ai/yiwen-api/src/bll" + "github.com/yiwen-ai/yiwen-api/src/middleware" + "github.com/yiwen-ai/yiwen-api/src/util" +) + +type Payment struct { + blls *bll.Blls +} + +type PaymentCode struct { + Kind int8 `cbor:"1,keyasint"` // 0: subscribe creation; 2: subscribe collection + ExpireAt int64 `cbor:"2,keyasint"` // code 的失效时间,unix 秒 + Payee util.ID `cbor:"3,keyasint"` // 收款人 id + SubPayee *util.ID `cbor:"4,keyasint,omitempty"` // 分成收款人 id + Amount int64 `cbor:"5,keyasint"` // 花费的亿文币数量 + GID util.ID `cbor:"6,keyasint"` // 订阅对象所属 group + UID util.ID `cbor:"7,keyasint"` // 受益人 id + CID util.ID `cbor:"8,keyasint"` // 订阅对象 id + Duration int64 `cbor:"9,keyasint"` // 增加的订阅时长,单位秒 +} + +type SubscriptionToken struct { + Kind int8 `cbor:"1,keyasint"` // 2: collection subscription + ExpireAt int64 `cbor:"2,keyasint"` // 订阅失效时间,unix 秒 + GID util.ID `cbor:"3,keyasint"` // 订阅对象所属 group + UID util.ID `cbor:"4,keyasint"` // 受益人 id + CID util.ID `cbor:"5,keyasint"` // 订阅对象 id +} + +type QueryPaymentCode struct { + Kind int8 `json:"kind" cbor:"kind" query:"kind" validate:"required"` + CID util.ID `json:"cid" cbor:"cid" query:"cid" validate:"required"` + // 触发支付的 group,如果不是订阅对象所属 group,则分享收益给该 group + GID util.ID `json:"gid" cbor:"gid" query:"gid" validate:"required"` +} + +func (i *QueryPaymentCode) Validate() error { + if err := util.Validator.Struct(i); err != nil { + return gear.ErrBadRequest.From(err) + } + + return nil +} + +type PaymentCodeOutput struct { + Kind int8 `json:"kind" cbor:"kind"` + Title string `json:"title" cbor:"title"` + Duration int64 `json:"duration" cbor:"duration"` + Amount int64 `json:"amount" cbor:"amount"` + Code string `json:"code" cbor:"code"` + ExpireAt int64 `json:"expire_at" cbor:"expire_at"` + GroupInfo *bll.GroupInfo `json:"group_info" cbor:"group_info"` +} + +func (a *Payment) GetCode(ctx *gear.Context) error { + input := &QueryPaymentCode{} + if err := ctx.ParseURL(input); err != nil { + return err + } + + sess := gear.CtxValue[middleware.Session](ctx) + code := &PaymentCode{ + Kind: input.Kind, + ExpireAt: time.Now().Add(time.Hour).Unix(), + GID: input.GID, + UID: sess.UserID, + CID: input.CID, + Duration: 3600 * 24 * 365 * 3, + } + output := &PaymentCodeOutput{ + Kind: code.Kind, + Duration: code.Duration, + ExpireAt: code.ExpireAt, + } + + language := "" + version := uint16(1) + PayeeGID := input.GID + var SubPayeeGID *util.ID + + switch input.Kind { + default: + return gear.ErrBadRequest.WithMsg("invalid kind") + case 0: + doc, err := a.blls.Writing.ImplicitGetPublication(ctx, &bll.ImplicitQueryPublication{ + CID: input.CID, + GID: &input.GID, + Fields: "title", + }, nil) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + if doc.Title == nil || doc.Price == nil || doc.FromGID == nil { + return gear.ErrInternalServerError.WithMsg("title or price or from_gid is nil") + } + + if *doc.FromGID != input.GID { + PayeeGID = *doc.FromGID + SubPayeeGID = &input.GID + } + + code.Amount = *doc.Price + if code.Amount <= 0 { + return gear.ErrBadRequest.WithMsg("creation is free") + } + output.Amount = code.Amount + output.Title = *doc.Title + language = doc.Language + version = doc.Version + case 2: + doc, err := a.blls.Writing.GetCollection(ctx, &bll.QueryGidID{ + GID: input.GID, + ID: input.CID, + Fields: "gid,info", + }, 2) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + if doc.Info == nil || doc.Price == nil { + return gear.ErrInternalServerError.WithMsg("title or price is nil") + } + if doc.GID != input.GID { + PayeeGID = doc.GID + SubPayeeGID = &input.GID + } + + code.Amount = *doc.Price + if code.Amount <= 0 { + return gear.ErrBadRequest.WithMsg("collection is free") + } + output.Amount = code.Amount + output.Title = doc.Info.Title + language = *doc.Language + version = *doc.Version + if len(doc.I18nInfo) > 0 { + for k, v := range doc.I18nInfo { + language = k + output.Title = v.Title + break + } + } + } + + if SubPayeeGID != nil { + group, err := a.blls.Userbase.GetGroup(ctx, *SubPayeeGID, "uid,status") + if err == nil && group.UID != nil && group.Status != nil && *group.Status >= 0 { + code.SubPayee = group.UID + } + } + + group, err := a.blls.Userbase.GetGroup(ctx, PayeeGID, "uid,cn,name,logo,status,slogan") + if err != nil { + return gear.ErrInternalServerError.From(err) + } + if group.Status == nil || group.UID == nil || *group.Status <= 0 { + return gear.ErrBadRequest.WithMsg("group is not active") + } + code.Payee = *group.UID + + output.GroupInfo = &bll.GroupInfo{ + ID: *group.ID, + CN: group.CN, + Name: group.Name, + Logo: *group.Logo, + Slogan: *group.Slogan, + Status: *group.Status, + } + + output.Code, err = util.EncodeEncrypt0(a.blls.Encryptor, code, []byte("PaymentCode")) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + + _, _ = a.blls.Writing.CreateBookmark(ctx, &bll.CreateBookmarkInput{ + GID: code.GID, + CID: code.CID, + Language: language, + Version: version, + Kind: output.Kind, + Title: output.Title, + }) + + return ctx.OkSend(bll.SuccessResponse[[]*PaymentCodeOutput]{Result: []*PaymentCodeOutput{output}}) +} + +type PaymentInput struct { + Code string `json:"code" cbor:"code" query:"code" validate:"required"` +} + +func (i *PaymentInput) Validate() error { + if err := util.Validator.Struct(i); err != nil { + return gear.ErrBadRequest.From(err) + } + + return nil +} + +func (a *Payment) PayByCode(ctx *gear.Context) error { + input := &PaymentInput{} + if err := ctx.ParseBody(input); err != nil { + return err + } + + code, err := util.DecodeEncrypt0[PaymentCode](a.blls.Encryptor, input.Code, []byte("PaymentCode")) + if err != nil { + return gear.ErrBadRequest.From(err) + } + now := time.Now().Unix() + if code.ExpireAt < now { + return gear.ErrBadRequest.WithMsg("code expired") + } + + sess := gear.CtxValue[middleware.Session](ctx) + wallet, err := a.blls.Walletbase.Get(ctx, sess.UserID) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + + if wallet.Balance() < code.Amount { + return gear.ErrPaymentRequired.WithMsg("insufficient balance") + } + + var subscription *bll.SubscriptionOutput + var logAction string + switch code.Kind { + default: + return gear.ErrBadRequest.WithMsg("invalid kind") + case 0: + logAction = bll.LogActionCreationSubscribe + subscription, _ = a.blls.Writing.InternalGetCreationSubscription(ctx, code.CID) + case 2: + logAction = bll.LogActionCollectionSubscribe + subscription, _ = a.blls.Writing.InternalGetCollectionSubscription(ctx, code.CID) + } + if subscription != nil && subscription.ExpireAt > (now+code.Duration/2) { + return gear.ErrBadRequest.WithMsg("already subscribed") + } + + subscriptionInput := &bll.SubscriptionInput{ + UID: code.UID, + CID: code.CID, + ExpireAt: now + code.Duration, + } + if subscription != nil { + subscriptionInput.UpdatedAt = subscription.UpdatedAt + if subscription.ExpireAt > now { + subscriptionInput.ExpireAt = subscription.ExpireAt + code.Duration + } + } + + payload, err := util.Marshal(code) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + + log, err := a.blls.Logbase.Log(ctx, logAction, 0, code.GID, code) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + + auditLog := &bll.UpdateLog{ + UID: log.UID, + ID: log.ID, + } + + wallet, err = a.blls.Walletbase.Subscribe(ctx, &bll.SpendInput{ + UID: sess.UserID, + Amount: code.Amount, + Payee: &code.Payee, + SubPayee: code.SubPayee, + Description: logAction, + Payload: payload, + }) + if err != nil { + return gear.ErrInternalServerError.From(err) + } + + subscriptionInput.Txn = wallet.Txn + txn := &bll.TransactionPK{ + UID: sess.UserID, + ID: wallet.Txn, + } + + switch code.Kind { + default: + err = gear.ErrBadRequest.WithMsg("invalid kind") + case 0: + subscription, err = a.blls.Writing.InternalUpdateCreationSubscription(ctx, subscriptionInput) + case 2: + subscription, err = a.blls.Writing.InternalUpdateCollectionSubscription(ctx, subscriptionInput) + } + + if err == nil { + auditLog.Status = 1 + err = a.blls.Walletbase.CommitTxn(ctx, txn) + } + + if err != nil { + auditLog.Status = -1 + auditLog.Error = util.Ptr(err.Error()) + _ = a.blls.Walletbase.CancelTxn(ctx, txn) + a.blls.Logbase.Update(ctx, auditLog) + return gear.ErrInternalServerError.From(err) + } + + a.blls.Logbase.Update(ctx, auditLog) + return ctx.OkSend(bll.SuccessResponse[*bll.SubscriptionOutput]{Result: subscription}) +} diff --git a/src/api/publication.go b/src/api/publication.go index 7491696..f6c07c4 100644 --- a/src/api/publication.go +++ b/src/api/publication.go @@ -110,11 +110,6 @@ func (a *Publication) Create(ctx *gear.Context) error { return gear.ErrForbidden.From(err) } - _, err := a.tryReadOne(ctx, input.GID, input.CID, input.Language, input.Version, false) - if err != nil { - return gear.ErrForbidden.From(err) - } - sess := gear.CtxValue[middleware.Session](ctx) wallet, err := a.blls.Walletbase.Get(ctx, sess.UserID) if err != nil { @@ -129,36 +124,9 @@ func (a *Publication) Create(ctx *gear.Context) error { return gear.ErrBadRequest.WithMsgf("model %q is not allowed for user level < 2", input.Model) } - dst, _ := a.blls.Writing.GetPublication(ctx, &bll.QueryPublication{ - GID: *input.ToGID, - CID: input.CID, - Language: *input.ToLanguage, - Version: input.Version, - Fields: "status,creator,updated_at", - }) - if dst != nil && dst.Status != nil && *dst.Status >= 0 { - return gear.ErrConflict.WithMsgf("%s publication already exists", *input.ToLanguage) - } - - if dst != nil { - a.blls.Writing.DeletePublication(ctx, &bll.QueryPublication{ - GID: dst.GID, - CID: dst.CID, - Language: *input.ToLanguage, - Version: dst.Version, - }) - } - - src, err := a.blls.Writing.GetPublication(ctx, &bll.QueryPublication{ - GID: input.GID, - CID: input.CID, - Language: input.Language, - Version: input.Version, - Fields: "", - }) - + src, err := a.tryReadOne(ctx, input.GID, input.CID, input.Language, input.Version, true) if err != nil { - return gear.ErrInternalServerError.From(err) + return gear.ErrForbidden.From(err) } teContents, err := src.ToTEContents() @@ -190,6 +158,26 @@ func (a *Publication) Create(ctx *gear.Context) error { input.Context = util.Ptr(fmt.Sprintf("The text is part or all of the %q", *src.Title)) } + dst, _ := a.blls.Writing.GetPublication(ctx, &bll.QueryPublication{ + GID: *input.ToGID, + CID: input.CID, + Language: *input.ToLanguage, + Version: input.Version, + Fields: "status,creator,updated_at", + }) + if dst != nil && dst.Status != nil && *dst.Status >= 0 { + return gear.ErrConflict.WithMsgf("%s publication already exists", *input.ToLanguage) + } + + if dst != nil { + a.blls.Writing.DeletePublication(ctx, &bll.QueryPublication{ + GID: dst.GID, + CID: dst.CID, + Language: *input.ToLanguage, + Version: dst.Version, + }) + } + gctx := middleware.WithGlobalCtx(ctx) key := fmt.Sprintf("CP:%s:%s:%s:%d", input.ToGID.String(), input.CID.String(), *input.ToLanguage, input.Version) locker, err := a.blls.Locker.Lock(gctx, key, 20*60*time.Second) @@ -265,12 +253,12 @@ func (a *Publication) Create(ctx *gear.Context) error { }) if err == nil { - err = a.blls.Walletbase.CommitExpending(gctx, txn) + err = a.blls.Walletbase.CommitTxn(gctx, txn) } } if err != nil { - _ = a.blls.Walletbase.CancelExpending(gctx, txn) + _ = a.blls.Walletbase.CancelTxn(gctx, txn) } } } @@ -323,22 +311,23 @@ func (a *Publication) Get(ctx *gear.Context) error { return err } - var err error - var output *bll.PublicationOutput - if input.GID != nil && input.Language != "" && input.Version > 0 { - output, err = a.tryReadOne(ctx, *input.GID, input.CID, input.Language, input.Version, true) - } else { - output, err = a.blls.Writing.ImplicitGetPublication(ctx, input) + subscription_in := util.ZeroID + subtoken, err := util.DecodeMac0[SubscriptionToken](a.blls.MACer, input.SubToken, []byte("SubscriptionToken")) + if err == nil { + // fast API calling with subtoken + subscription_in = subtoken.GID + if input.Parent != nil && *input.Parent != subtoken.CID { + return gear.ErrBadRequest.WithMsg("invalid parent") + } + input.Parent = &subtoken.CID } + output, err := a.blls.Writing.ImplicitGetPublication(ctx, input, &subscription_in) if err != nil { return gear.ErrBadRequest.From(err) } result := bll.PublicationOutputs{*output} - // result.LoadCreators(func(ids ...util.ID) []bll.UserInfo { - // return a.blls.Userbase.LoadUserInfo(ctx, ids...) - // }) result.LoadGroups(func(ids ...util.ID) []bll.GroupInfo { return a.blls.Userbase.LoadGroupInfo(ctx, ids...) }) @@ -438,9 +427,6 @@ func (a *Publication) GetByJob(ctx *gear.Context) error { } result := bll.PublicationOutputs{*output} - // result.LoadCreators(func(ids ...util.ID) []bll.UserInfo { - // return a.blls.Userbase.LoadUserInfo(ctx, ids...) - // }) result.LoadGroups(func(ids ...util.ID) []bll.GroupInfo { return a.blls.Userbase.LoadGroupInfo(ctx, ids...) }) @@ -722,7 +708,7 @@ func (a *Publication) ListArchived(ctx *gear.Context) error { } func (a *Publication) GetPublishList(ctx *gear.Context) error { - input := &bll.GidCidInput{} + input := &bll.QueryGidCid{} if err := ctx.ParseURL(input); err != nil { return err } @@ -911,6 +897,7 @@ func (a *Publication) Bookmark(ctx *gear.Context) error { return gear.ErrForbidden.From(err) } + input.Kind = 1 output, err := a.blls.Writing.CreateBookmark(ctx, input) if err != nil { return gear.ErrInternalServerError.From(err) @@ -949,7 +936,7 @@ func (a *Publication) UploadFile(ctx *gear.Context) error { func (a *Publication) checkReadPermission(ctx *gear.Context, gid util.ID) (int8, error) { sess := gear.CtxValue[middleware.Session](ctx) - if sess == nil || sess.UserID == util.ANON { + if sess == nil || sess.UserID.Compare(util.MinID) <= 0 { return -2, gear.ErrForbidden.WithMsg("no permission") } @@ -966,7 +953,7 @@ func (a *Publication) checkReadPermission(ctx *gear.Context, gid util.ID) (int8, func (a *Publication) checkCreatePermission(ctx *gear.Context, gid util.ID) error { sess := gear.CtxValue[middleware.Session](ctx) - if sess == nil || sess.UserID == util.ANON { + if sess == nil || sess.UserID.Compare(util.MinID) <= 0 { return gear.ErrForbidden.WithMsg("no permission") } @@ -983,7 +970,7 @@ func (a *Publication) checkCreatePermission(ctx *gear.Context, gid util.ID) erro func (a *Publication) checkWritePermission(ctx *gear.Context, gid, cid util.ID, language string, version uint16) (*bll.PublicationOutput, error) { sess := gear.CtxValue[middleware.Session](ctx) - if sess == nil || sess.UserID == util.ANON { + if sess == nil || sess.UserID.Compare(util.MinID) <= 0 { return nil, gear.ErrForbidden.WithMsg("no permission") } @@ -1018,13 +1005,12 @@ func (a *Publication) checkWritePermission(ctx *gear.Context, gid, cid util.ID, } func (a *Publication) tryReadOne(ctx *gear.Context, gid, cid util.ID, language string, version uint16, full bool) (*bll.PublicationOutput, error) { - var err error - var role int8 = -2 - - if sess := gear.CtxValue[middleware.Session](ctx); sess != nil && sess.UserID != util.ANON { - role, _ = a.blls.Userbase.UserGroupRole(ctx, sess.UserID, gid) + sess := gear.CtxValue[middleware.Session](ctx) + if sess == nil || sess.UserID.Compare(util.MinID) <= 0 { + return nil, gear.ErrForbidden.WithMsg("no permission") } + role, _ := a.blls.Userbase.UserGroupRole(ctx, sess.UserID, gid) input := &bll.QueryPublication{ GID: gid, CID: cid, diff --git a/src/api/router.go b/src/api/router.go index b772294..0137c9e 100644 --- a/src/api/router.go +++ b/src/api/router.go @@ -19,11 +19,13 @@ func init() { type APIs struct { Healthz *Healthz Bookmark *Bookmark + Collection *Collection Creation *Creation Group *Group Jarvis *Jarvis Log *Log Message *Message + Payment *Payment Publication *Publication Scraping *Scraping Wechat *Wechat @@ -33,11 +35,13 @@ func newAPIs(blls *bll.Blls) *APIs { return &APIs{ Healthz: &Healthz{blls}, Bookmark: &Bookmark{blls}, + Collection: &Collection{blls}, Creation: &Creation{blls}, Group: &Group{blls}, Jarvis: &Jarvis{blls}, Log: &Log{blls}, Message: &Message{blls}, + Payment: &Payment{blls}, Publication: &Publication{blls}, Scraping: &Scraping{blls}, Wechat: &Wechat{blls}, @@ -76,6 +80,10 @@ func newRouters(apis *APIs) []*gear.Router { router.Get("/v1/publication/publish", middleware.AuthAllowAnon.Auth, apis.Publication.GetPublishList) router.Post("/v1/publication/list_published", middleware.AuthAllowAnon.Auth, apis.Publication.ListPublished) router.Post("/v1/publication/list", middleware.AuthAllowAnon.Auth, apis.Publication.List) // 匿名时等价于 list_published + router.Get("/v1/collection", middleware.AuthAllowAnon.Auth, apis.Collection.Get) + router.Get("/v1/collection/list_by_child", middleware.AuthAllowAnon.Auth, apis.Collection.ListByChild) + router.Post("/v1/collection/list", middleware.AuthAllowAnon.Auth, apis.Collection.List) + router.Post("/v1/collection/list_children", middleware.AuthAllowAnon.Auth, apis.Collection.ListChildren) router.Get("/v1/group/info", middleware.AuthAllowAnon.Auth, apis.Group.GetInfo) router.Get("/v1/group/statistic", middleware.AuthAllowAnon.Auth, apis.Group.GetStatistic) @@ -104,6 +112,7 @@ func newRouters(apis *APIs) []*gear.Router { router.Patch("/v1/creation/update_content", middleware.AuthToken.Auth, middleware.CheckUserStatus(0), todo) // 暂不实现 router.Post("/v1/creation/assist", middleware.AuthToken.Auth, middleware.CheckUserStatus(0), todo) // 暂不实现 router.Post("/v1/creation/upload", middleware.AuthToken.Auth, middleware.CheckUserStatus(0), apis.Creation.UploadFile) + router.Patch("/v1/creation/price", middleware.AuthToken.Auth, middleware.CheckUserStatus(0), apis.Creation.UpdatePrice) router.Post("/v1/publication", middleware.AuthToken.Auth, middleware.CheckUserStatus(0), apis.Publication.Create) router.Post("/v1/publication/estimate", middleware.AuthToken.Auth, middleware.CheckUserStatus(0), apis.Publication.Estimate) @@ -122,6 +131,18 @@ func newRouters(apis *APIs) []*gear.Router { router.Post("/v1/publication/bookmark", middleware.AuthToken.Auth, apis.Publication.Bookmark) router.Post("/v1/publication/upload", middleware.AuthToken.Auth, middleware.CheckUserStatus(0), apis.Publication.UploadFile) + router.Post("/v1/collection/list_archived", middleware.AuthToken.Auth, apis.Collection.ListArchived) + router.Post("/v1/collection", middleware.AuthToken.Auth, middleware.CheckUserStatus(0), apis.Collection.Create) + router.Patch("/v1/collection", middleware.AuthToken.Auth, middleware.CheckUserStatus(0), apis.Collection.Update) + router.Delete("/v1/collection", middleware.AuthToken.Auth, middleware.CheckUserStatus(0), apis.Collection.Delete) + router.Get("/v1/collection/info", middleware.AuthToken.Auth, apis.Collection.GetInfo) + router.Patch("/v1/collection/info", middleware.AuthToken.Auth, middleware.CheckUserStatus(0), apis.Collection.UpdateInfo) + router.Patch("/v1/collection/status", middleware.AuthToken.Auth, middleware.CheckUserStatus(0), apis.Collection.UpdateStatus) + router.Post("/v1/collection/child", middleware.AuthToken.Auth, middleware.CheckUserStatus(0), apis.Collection.AddChildren) + router.Patch("/v1/collection/child", middleware.AuthToken.Auth, middleware.CheckUserStatus(0), apis.Collection.UpdateChild) + router.Delete("/v1/collection/child", middleware.AuthToken.Auth, middleware.CheckUserStatus(0), apis.Collection.RemoveChild) + router.Get("/v1/collection/upload", middleware.AuthToken.Auth, middleware.CheckUserStatus(0), apis.Collection.UploadFile) + router.Post("/v1/message", middleware.AuthToken.Auth, apis.Message.Create) router.Patch("/v1/message", middleware.AuthToken.Auth, apis.Message.Update) router.Post("/v1/message/translate", middleware.AuthToken.Auth, apis.Message.UpdateI18n) @@ -139,7 +160,10 @@ func newRouters(apis *APIs) []*gear.Router { router.Post("/v1/group/list_following", middleware.AuthToken.Auth, apis.Group.ListFollowing) router.Post("/v1/group/list_subscribing", middleware.AuthToken.Auth, todo) // 暂不实现 router.Patch("/v1/group", middleware.AuthToken.Auth, middleware.CheckUserStatus(0), apis.Group.UpdateInfo) - router.Get("/v1/group/upload_logo", middleware.AuthToken.Auth, middleware.CheckUserStatus(0), apis.Group.UploadLogo) + router.Get("/v1/group/upload_logo", middleware.AuthToken.Auth, middleware.CheckUserStatus(0), apis.Group.UploadPicture) + + router.Get("/v1/payment/code", middleware.AuthToken.Auth, apis.Payment.GetCode) + router.Post("/v1/payment/code", middleware.AuthToken.Auth, apis.Payment.PayByCode) router.Get("/v1/log/list_recently", middleware.AuthToken.Auth, apis.Log.ListRecently) diff --git a/src/bll/common.go b/src/bll/common.go index de80d07..b7e2e48 100644 --- a/src/bll/common.go +++ b/src/bll/common.go @@ -3,6 +3,9 @@ package bll import ( "context" + "github.com/ldclabs/cose/key" + _ "github.com/ldclabs/cose/key/aesgcm" + _ "github.com/ldclabs/cose/key/hmac" "github.com/teambition/gear" "github.com/yiwen-ai/yiwen-api/src/conf" @@ -16,6 +19,8 @@ func init() { // Blls ... type Blls struct { + MACer key.MACer + Encryptor key.Encryptor Locker *service.Locker Jarvis *Jarvis Logbase *Logbase @@ -30,7 +35,18 @@ type Blls struct { // NewBlls ... func NewBlls(oss *service.OSS, redis *service.Redis, locker *service.Locker) *Blls { cfg := conf.Config.Base + macer, err := conf.Config.COSEKeys.Hmac.MACer() + if err != nil { + panic(err) + } + encryptor, err := conf.Config.COSEKeys.Aesgcm.Encryptor() + if err != nil { + panic(err) + } + return &Blls{ + MACer: macer, + Encryptor: encryptor, Locker: locker, Jarvis: &Jarvis{svc: service.APIHost(cfg.Jarvis)}, Logbase: &Logbase{svc: service.APIHost(cfg.Logbase)}, @@ -91,6 +107,23 @@ func (i *Pagination) Validate() error { return nil } +type IDGIDPagination struct { + ID util.ID `json:"id" cbor:"id" validate:"required"` + GID util.ID `json:"gid" cbor:"gid" validate:"required"` + PageToken *util.Bytes `json:"page_token,omitempty" cbor:"page_token,omitempty"` + PageSize *uint16 `json:"page_size,omitempty" cbor:"page_size,omitempty" validate:"omitempty,gte=5,lte=100"` + Status *int8 `json:"status,omitempty" cbor:"status,omitempty"` + Fields *[]string `json:"fields,omitempty" cbor:"fields,omitempty"` +} + +func (i *IDGIDPagination) Validate() error { + if err := util.Validator.Struct(i); err != nil { + return gear.ErrBadRequest.From(err) + } + + return nil +} + type GIDPagination struct { GID util.ID `json:"gid" cbor:"gid" validate:"required"` PageToken *util.Bytes `json:"page_token,omitempty" cbor:"page_token,omitempty"` @@ -120,12 +153,28 @@ func (i *QueryIdCn) Validate() error { return nil } -type GidCidInput struct { +type QueryGidIdCid struct { GID util.ID `json:"gid" cbor:"gid" query:"gid" validate:"required"` + ID util.ID `json:"id" cbor:"id" query:"id" validate:"required"` CID util.ID `json:"cid" cbor:"cid" query:"cid" validate:"required"` } -func (i *GidCidInput) Validate() error { +func (i *QueryGidIdCid) Validate() error { + if err := util.Validator.Struct(i); err != nil { + return gear.ErrBadRequest.From(err) + } + + return nil +} + +type QueryGidCid struct { + GID util.ID `json:"gid" cbor:"gid" query:"gid" validate:"required"` + CID util.ID `json:"cid" cbor:"cid" query:"cid" validate:"required"` + Status int8 `json:"status,omitempty" cbor:"status,omitempty"` + Fields string `json:"fields,omitempty" cbor:"fields,omitempty"` +} + +func (i *QueryGidCid) Validate() error { if err := util.Validator.Struct(i); err != nil { return gear.ErrBadRequest.From(err) } @@ -144,3 +193,54 @@ func (i *QueryID) Validate() error { } return nil } + +type QueryGidID struct { + GID util.ID `json:"gid" cbor:"gid" query:"gid" validate:"required"` + ID util.ID `json:"id" cbor:"id" query:"id" validate:"required"` + Fields string `json:"fields" cbor:"fields" query:"fields"` +} + +func (i *QueryGidID) Validate() error { + if err := util.Validator.Struct(i); err != nil { + return gear.ErrBadRequest.From(err) + } + return nil +} + +type SubscriptionInput struct { + UID util.ID `json:"uid" cbor:"uid"` + CID util.ID `json:"cid" cbor:"cid"` + Txn util.ID `json:"txn" cbor:"txn"` + ExpireAt int64 `json:"expire_at" cbor:"expire_at"` + UpdatedAt int64 `json:"updated_at" cbor:"updated_at"` +} + +type SubscriptionOutput struct { + UID util.ID `json:"uid" cbor:"uid"` + CID util.ID `json:"cid" cbor:"cid"` + GID util.ID `json:"gid" cbor:"gid"` + Txn util.ID `json:"txn" cbor:"txn"` + ExpireAt int64 `json:"expire_at" cbor:"expire_at"` + UpdatedAt int64 `json:"updated_at" cbor:"updated_at"` +} + +// Request for Payment +type RFP struct { + Creation uint64 `json:"creation,omitempty" cbor:"creation,omitempty"` + Collection uint64 `json:"collection,omitempty" cbor:"collection,omitempty"` +} + +type UpdateStatusInput struct { + GID util.ID `json:"gid" cbor:"gid" validate:"required"` + ID util.ID `json:"id" cbor:"id" validate:"required"` + UpdatedAt int64 `json:"updated_at" cbor:"updated_at" validate:"required"` + Status int8 `json:"status" cbor:"status" validate:"gte=-2,lte=2"` +} + +func (i *UpdateStatusInput) Validate() error { + if err := util.Validator.Struct(i); err != nil { + return gear.ErrBadRequest.From(err) + } + + return nil +} diff --git a/src/bll/jarvis.go b/src/bll/jarvis.go index 150b345..58c302f 100644 --- a/src/bll/jarvis.go +++ b/src/bll/jarvis.go @@ -43,7 +43,7 @@ func (b *Jarvis) getTokensRate(lang string) float32 { } func (b *Jarvis) EstimateTranslatingTokens(text, srcLang, dstLang string) uint32 { - tokens := util.Tiktokens(text) + tokens := util.Tiktokens(text) + 100 return tokens + uint32(float32(tokens)*b.getTokensRate(dstLang)/b.getTokensRate(srcLang)) } diff --git a/src/bll/logbase.go b/src/bll/logbase.go index 9748ff1..92c554a 100644 --- a/src/bll/logbase.go +++ b/src/bll/logbase.go @@ -114,6 +114,7 @@ type LogPayload struct { Language *string `json:"language,omitempty" cbor:"language,omitempty"` Status *int8 `json:"status,omitempty" cbor:"status,omitempty"` Rating *int8 `json:"rating,omitempty" cbor:"rating,omitempty"` + Price *int64 `json:"price,omitempty" cbor:"price,omitempty"` } type LogMessage struct { diff --git a/src/bll/userbase.go b/src/bll/userbase.go index e4ed026..156832d 100644 --- a/src/bll/userbase.go +++ b/src/bll/userbase.go @@ -22,6 +22,9 @@ func (b *Userbase) UserGroupRole(ctx context.Context, uid, gid util.ID) (int8, e if uid == gid { return 2, nil } + if gid.Compare(util.MinID) <= 0 { + return -2, gear.ErrBadRequest.WithMsg("invalid group id") + } output := SuccessResponse[GroupInfo]{} api := fmt.Sprintf("/v1/group/get_by_user?id=%s&fields=cn,status", gid.String()) @@ -197,6 +200,21 @@ func (b *Userbase) GroupInfo(ctx context.Context, input *QueryIdCn) (*GroupInfo, return &output.Result, nil } +func (b *Userbase) GetGroup(ctx context.Context, id util.ID, fields string) (*Group, error) { + output := SuccessResponse[Group]{} + + query := url.Values{} + query.Add("id", id.String()) + query.Add("fields", fields) + + err := b.svc.Get(ctx, "/v1/group?"+query.Encode(), &output) + if err != nil { + return nil, err + } + + return &output.Result, nil +} + type UpdateGroupInfoInput struct { ID util.ID `json:"id" cbor:"id" validate:"required"` Name *string `json:"name,omitempty" cbor:"name,omitempty" validate:"omitempty,gte=2,lte=16"` diff --git a/src/bll/walletbase.go b/src/bll/walletbase.go index f5e3d8c..536cc0e 100644 --- a/src/bll/walletbase.go +++ b/src/bll/walletbase.go @@ -55,6 +55,8 @@ type Walletbase struct { type SpendInput struct { UID util.ID `json:"uid" cbor:"uid"` Amount int64 `json:"amount" cbor:"amount"` + Payee *util.ID `json:"payee,omitempty" cbor:"payee,omitempty"` + SubPayee *util.ID `json:"sub_payee,omitempty" cbor:"sub_payee,omitempty"` Description string `json:"description,omitempty" cbor:"description,omitempty"` Payload util.Bytes `json:"payload,omitempty" cbor:"payload,omitempty"` } @@ -100,6 +102,7 @@ func (b *Walletbase) Get(ctx context.Context, uid util.ID) (*WalletOutput, error return &output.Result, nil } +// should call CommitTxn or CancelTxn to confirm the transaction func (b *Walletbase) Spend(ctx context.Context, uid util.ID, input *SpendPayload) (*WalletOutput, error) { data, err := cbor.Marshal(input) if err != nil { @@ -124,6 +127,17 @@ func (b *Walletbase) Spend(ctx context.Context, uid util.ID, input *SpendPayload return &output.Result, nil } +// should call CommitTxn or CancelTxn to confirm the transaction +func (b *Walletbase) Subscribe(ctx context.Context, input *SpendInput) (*WalletOutput, error) { + output := SuccessResponse[WalletOutput]{} + if err := b.svc.Post(ctx, "/v1/wallet/subscribe", input, &output); err != nil { + return nil, err + } + + output.Result.SetLevel() + return &output.Result, nil +} + type TransactionPK struct { UID util.ID `json:"uid" cbor:"uid" query:"uid" validate:"required"` ID util.ID `json:"id" cbor:"id" query:"id" validate:"required"` @@ -143,7 +157,7 @@ type TransactionOutput struct { Payload *util.Bytes `json:"payload,omitempty" cbor:"payload,omitempty"` } -func (b *Walletbase) CommitExpending(ctx context.Context, input *TransactionPK) error { +func (b *Walletbase) CommitTxn(ctx context.Context, input *TransactionPK) error { output := SuccessResponse[TransactionOutput]{} if err := b.svc.Post(ctx, "/v1/transaction/commit", input, &output); err != nil { return err @@ -152,7 +166,7 @@ func (b *Walletbase) CommitExpending(ctx context.Context, input *TransactionPK) return nil } -func (b *Walletbase) CancelExpending(ctx context.Context, input *TransactionPK) error { +func (b *Walletbase) CancelTxn(ctx context.Context, input *TransactionPK) error { output := SuccessResponse[TransactionOutput]{} if err := b.svc.Post(ctx, "/v1/transaction/cancel", input, &output); err != nil { return err diff --git a/src/bll/writing.go b/src/bll/writing.go index 54658c6..f2c2b50 100644 --- a/src/bll/writing.go +++ b/src/bll/writing.go @@ -30,7 +30,7 @@ type SearchDocument struct { Language string `json:"language" cbor:"language"` Version uint16 `json:"version" cbor:"version"` UpdatedAt int64 `json:"updated_at" cbor:"updated_at"` - Kind int8 `json:"kind" cbor:"kind"` + Kind int8 `json:"kind" cbor:"kind"` // 0: creation, 1: publication, 2: collection Title string `json:"title" cbor:"title"` Summary string `json:"summary" cbor:"summary"` GroupInfo *GroupInfo `json:"group_info,omitempty" cbor:"group_info,omitempty"` diff --git a/src/bll/writing_bookmark.go b/src/bll/writing_bookmark.go index eaa6734..13af420 100644 --- a/src/bll/writing_bookmark.go +++ b/src/bll/writing_bookmark.go @@ -10,12 +10,14 @@ import ( // TODO: more validation type CreateBookmarkInput struct { - GID util.ID `json:"gid" cbor:"gid" validate:"required"` - CID util.ID `json:"cid" cbor:"cid" validate:"required"` - Language string `json:"language" cbor:"language" validate:"required"` - Version uint16 `json:"version" cbor:"version" validate:"gte=1,lte=10000"` - Title string `json:"title" cbor:"title" validate:"gte=4,lte=256"` - Labels *[]string `json:"labels,omitempty" cbor:"labels,omitempty" validate:"omitempty,gte=0,lte=5"` + GID util.ID `json:"gid" cbor:"gid" validate:"required"` + CID util.ID `json:"cid" cbor:"cid" validate:"required"` + Language string `json:"language" cbor:"language" validate:"required"` + Kind int8 `json:"kind" cbor:"kind" validate:"gte=0,lte=2"` + Version uint16 `json:"version" cbor:"version" validate:"gte=1,lte=10000"` + Title string `json:"title" cbor:"title" validate:"gte=4,lte=256"` + Labels *[]string `json:"labels,omitempty" cbor:"labels,omitempty" validate:"omitempty,gte=0,lte=5"` + Payload *util.Bytes `json:"payload,omitempty" cbor:"payload,omitempty"` } func (i *CreateBookmarkInput) Validate() error { @@ -27,15 +29,17 @@ func (i *CreateBookmarkInput) Validate() error { } type BookmarkOutput struct { - ID util.ID `json:"id" cbor:"id"` - GID util.ID `json:"gid" cbor:"gid"` - CID util.ID `json:"cid" cbor:"cid"` - Language string `json:"language" cbor:"language"` - Version uint16 `json:"version" cbor:"version"` - UpdatedAt *int64 `json:"updated_at,omitempty" cbor:"updated_at,omitempty"` - Title *string `json:"title,omitempty" cbor:"title,omitempty"` - Labels *[]string `json:"labels,omitempty" cbor:"labels,omitempty"` - GroupInfo *GroupInfo `json:"group_info,omitempty" cbor:"group_info,omitempty"` + ID util.ID `json:"id" cbor:"id"` + GID util.ID `json:"gid" cbor:"gid"` + CID util.ID `json:"cid" cbor:"cid"` + Language string `json:"language" cbor:"language"` + Kind int8 `json:"kind" cbor:"kind"` + Version uint16 `json:"version" cbor:"version"` + UpdatedAt *int64 `json:"updated_at,omitempty" cbor:"updated_at,omitempty"` + Title *string `json:"title,omitempty" cbor:"title,omitempty"` + Labels *[]string `json:"labels,omitempty" cbor:"labels,omitempty"` + Payload *util.Bytes `json:"payload,omitempty" cbor:"payload,omitempty"` + GroupInfo *GroupInfo `json:"group_info,omitempty" cbor:"group_info,omitempty"` } type BookmarkOutputs []BookmarkOutput @@ -76,11 +80,12 @@ func (b *Writing) CreateBookmark(ctx context.Context, input *CreateBookmarkInput // TODO: more validation type UpdateBookmarkInput struct { - ID util.ID `json:"id" cbor:"id" validate:"required"` - UpdatedAt int64 `json:"updated_at" cbor:"updated_at" validate:"required"` - Version *uint16 `json:"version" cbor:"version" validate:"omitempty,gte=1,lte=10000"` - Title *string `json:"title,omitempty" cbor:"title,omitempty" validate:"omitempty,gte=4,lte=256"` - Labels *[]string `json:"labels,omitempty" cbor:"labels,omitempty" validate:"omitempty,gte=0,lte=5"` + ID util.ID `json:"id" cbor:"id" validate:"required"` + UpdatedAt int64 `json:"updated_at" cbor:"updated_at" validate:"required"` + Version *uint16 `json:"version" cbor:"version" validate:"omitempty,gte=1,lte=10000"` + Title *string `json:"title,omitempty" cbor:"title,omitempty" validate:"omitempty,gte=4,lte=256"` + Labels *[]string `json:"labels,omitempty" cbor:"labels,omitempty" validate:"omitempty,gte=0,lte=5"` + Payload *util.Bytes `json:"payload,omitempty" cbor:"payload,omitempty"` } func (i *UpdateBookmarkInput) Validate() error { diff --git a/src/bll/writing_collection.go b/src/bll/writing_collection.go new file mode 100644 index 0000000..d78ea9d --- /dev/null +++ b/src/bll/writing_collection.go @@ -0,0 +1,309 @@ +package bll + +import ( + "context" + "net/url" + "strconv" + + "github.com/teambition/gear" + "github.com/yiwen-ai/yiwen-api/src/util" +) + +type CreateCollectionInput struct { + GID util.ID `json:"gid" cbor:"gid" validate:"required"` + Language string `json:"language" cbor:"language" validate:"required"` + Context string `json:"context" cbor:"context" validate:"gte=0,lte=1024"` + Info CollectionInfo `json:"info" cbor:"info" validate:"required"` + Cover *string `json:"cover" cbor:"cover" validate:"omitempty,http_url"` + Price *int64 `json:"price" cbor:"price" validate:"omitempty,gte=-1,lte=1000000"` + CreationPrice *int64 `json:"creation_price" cbor:"creation_price" validate:"omitempty,gte=-1,lte=100000"` + Parent *util.ID `json:"parent,omitempty" cbor:"parent,omitempty"` +} + +type CollectionInfo struct { + Title string `json:"title" cbor:"title" validate:"gte=1,lte=256"` + Summary *string `json:"summary,omitempty" cbor:"summary,omitempty" validate:"omitempty,gte=1,lte=2048"` + Keywords *[]string `json:"keywords,omitempty" cbor:"keywords,omitempty" validate:"omitempty,gte=0,lte=10"` + Authors *[]string `json:"authors,omitempty" cbor:"authors,omitempty" validate:"omitempty,gte=0,lte=10"` +} + +func (i *CreateCollectionInput) Validate() error { + if err := util.Validator.Struct(i); err != nil { + return gear.ErrBadRequest.From(err) + } + + return nil +} + +type CollectionOutput struct { + ID util.ID `json:"id" cbor:"id"` + GID util.ID `json:"gid" cbor:"gid"` + Status *int8 `json:"status,omitempty" cbor:"status,omitempty"` + Rating *int8 `json:"rating,omitempty" cbor:"rating,omitempty"` + UpdatedAt *int64 `json:"updated_at,omitempty" cbor:"updated_at,omitempty"` + Cover *string `json:"cover,omitempty" cbor:"cover,omitempty"` + Price *int64 `json:"price,omitempty" cbor:"price,omitempty"` + CreationPrice *int64 `json:"creation_price,omitempty" cbor:"creation_price,omitempty"` + Language *string `json:"language,omitempty" cbor:"language,omitempty"` + Version *uint16 `json:"version,omitempty" cbor:"version,omitempty"` + Info *CollectionInfo `json:"info,omitempty" cbor:"info,omitempty"` + I18nInfo map[string]CollectionInfo `json:"i18n_info,omitempty" cbor:"i18n_info,omitempty"` + Subscription *SubscriptionOutput `json:"subscription,omitempty" cbor:"subscription,omitempty"` + RFP *RFP `json:"rfp,omitempty" cbor:"rfp,omitempty"` + SubToken *string `json:"subtoken,omitempty" cbor:"subtoken,omitempty"` + GroupInfo *GroupInfo `json:"group_info,omitempty" cbor:"group_info,omitempty"` +} + +type CollectionOutputs []CollectionOutput + +func (list *CollectionOutputs) LoadGroups(loader func(ids ...util.ID) []GroupInfo) { + if len(*list) == 0 { + return + } + + ids := make([]util.ID, 0, len(*list)) + for _, v := range *list { + ids = append(ids, v.GID) + } + + groups := loader(ids...) + if len(groups) == 0 { + return + } + + infoMap := make(map[util.ID]*GroupInfo, len(groups)) + for i := range groups { + infoMap[groups[i].ID] = &groups[i] + } + + for i := range *list { + (*list)[i].GroupInfo = infoMap[(*list)[i].GID] + } +} + +func (b *Writing) CreateCollection(ctx context.Context, input *CreateCollectionInput) (*CollectionOutput, error) { + output := SuccessResponse[CollectionOutput]{} + if err := b.svc.Post(ctx, "/v1/collection", input, &output); err != nil { + return nil, err + } + + return &output.Result, nil +} + +func (b *Writing) GetCollection(ctx context.Context, input *QueryGidID, status int8) (*CollectionOutput, error) { + output := SuccessResponse[CollectionOutput]{} + query := url.Values{} + query.Add("id", input.ID.String()) + query.Add("gid", input.GID.String()) + query.Add("status", strconv.Itoa(int(status))) + if input.Fields != "" { + query.Add("fields", input.Fields) + } + if err := b.svc.Get(ctx, "/v1/collection?"+query.Encode(), &output); err != nil { + return nil, err + } + + return &output.Result, nil +} + +type UpdateCollectionInput struct { + ID util.ID `json:"id" cbor:"id" validate:"required"` + GID util.ID `json:"gid" cbor:"gid" validate:"required"` + UpdatedAt int64 `json:"updated_at" cbor:"updated_at" validate:"gte=1"` + Cover *string `json:"cover" cbor:"cover" validate:"omitempty,http_url"` + Price *int64 `json:"price" cbor:"price" validate:"omitempty,gte=-1,lte=1000000"` + CreationPrice *int64 `json:"creation_price" cbor:"creation_price" validate:"omitempty,gte=-1,lte=100000"` +} + +func (i *UpdateCollectionInput) Validate() error { + if err := util.Validator.Struct(i); err != nil { + return gear.ErrBadRequest.From(err) + } + + return nil +} + +func (b *Writing) UpdateCollection(ctx context.Context, input *UpdateCollectionInput) (*CollectionOutput, error) { + output := SuccessResponse[CollectionOutput]{} + if err := b.svc.Patch(ctx, "/v1/collection", input, &output); err != nil { + return nil, err + } + + return &output.Result, nil +} + +func (b *Writing) DeleteCollection(ctx context.Context, input *QueryGidID) (bool, error) { + output := SuccessResponse[bool]{} + + query := url.Values{} + query.Add("id", input.ID.String()) + query.Add("gid", input.GID.String()) + if err := b.svc.Delete(ctx, "/v1/collection?"+query.Encode(), &output); err != nil { + return false, err + } + + return output.Result, nil +} + +func (b *Writing) GetCollectionInfo(ctx context.Context, input *QueryGidID) (*MessageOutput, error) { + output := SuccessResponse[MessageOutput]{} + query := url.Values{} + query.Add("id", input.ID.String()) + query.Add("gid", input.GID.String()) + if input.Fields != "" { + query.Add("fields", input.Fields) + } + if err := b.svc.Get(ctx, "/v1/collection/info?"+query.Encode(), &output); err != nil { + return nil, err + } + + return &output.Result, nil +} + +func (b *Writing) UpdateCollectionInfo(ctx context.Context, input *UpdateMessageInput) (*MessageOutput, error) { + output := SuccessResponse[MessageOutput]{} + if err := b.svc.Patch(ctx, "/v1/collection/info", input, &output); err != nil { + return nil, err + } + + return &output.Result, nil +} + +func (b *Writing) UpdateCollectionStatus(ctx context.Context, input *UpdateStatusInput) (*CollectionOutput, error) { + output := SuccessResponse[CollectionOutput]{} + if err := b.svc.Patch(ctx, "/v1/collection/update_status", input, &output); err != nil { + return nil, err + } + + return &output.Result, nil +} + +type AddCollectionChildrenInput struct { + ID util.ID `json:"id" cbor:"id" validate:"required"` + GID util.ID `json:"gid" cbor:"gid" validate:"required"` + Cids []util.ID `json:"cids" cbor:"cids" validate:"gte=1,lte=100"` + Kind int8 `json:"kind" cbor:"kind" validate:"gte=0,lte=2"` +} + +func (i *AddCollectionChildrenInput) Validate() error { + if err := util.Validator.Struct(i); err != nil { + return gear.ErrBadRequest.From(err) + } + + return nil +} + +func (b *Writing) AddCollectionChildren(ctx context.Context, input *AddCollectionChildrenInput) ([]util.ID, error) { + output := SuccessResponse[[]util.ID]{} + if err := b.svc.Post(ctx, "/v1/collection/child", input, &output); err != nil { + return nil, err + } + + return output.Result, nil +} + +type UpdateCollectionChildInput struct { + ID util.ID `json:"id" cbor:"id" validate:"required"` + GID util.ID `json:"gid" cbor:"gid" validate:"required"` + CID util.ID `json:"cid" cbor:"cid" validate:"required"` + Ord float64 `json:"ord" cbor:"ord" validate:"gte=0"` +} + +func (i *UpdateCollectionChildInput) Validate() error { + if err := util.Validator.Struct(i); err != nil { + return gear.ErrBadRequest.From(err) + } + + return nil +} + +func (b *Writing) UpdateCollectionChild(ctx context.Context, input *UpdateCollectionChildInput) (bool, error) { + output := SuccessResponse[bool]{} + if err := b.svc.Patch(ctx, "/v1/collection/child", input, &output); err != nil { + return false, err + } + + return output.Result, nil +} + +func (b *Writing) RemoveCollectionChild(ctx context.Context, input *QueryGidIdCid) (bool, error) { + output := SuccessResponse[bool]{} + + query := url.Values{} + query.Add("id", input.ID.String()) + query.Add("gid", input.GID.String()) + query.Add("cid", input.CID.String()) + if err := b.svc.Delete(ctx, "/v1/collection/child?"+query.Encode(), &output); err != nil { + return false, err + } + + return output.Result, nil +} + +func (b *Writing) InternalGetCollectionSubscription(ctx context.Context, id util.ID) (*SubscriptionOutput, error) { + output := SuccessResponse[SubscriptionOutput]{} + query := url.Values{} + query.Add("id", id.String()) + if err := b.svc.Get(ctx, "/v1/collection/subscription?"+query.Encode(), &output); err != nil { + return nil, err + } + + return &output.Result, nil +} + +func (b *Writing) InternalUpdateCollectionSubscription(ctx context.Context, input *SubscriptionInput) (*SubscriptionOutput, error) { + output := SuccessResponse[SubscriptionOutput]{} + if err := b.svc.Put(ctx, "/v1/collection/subscription", input, &output); err != nil { + return nil, err + } + + return &output.Result, nil +} + +func (b *Writing) ListCollection(ctx context.Context, input *GIDPagination) (*SuccessResponse[CollectionOutputs], error) { + output := SuccessResponse[CollectionOutputs]{} + if err := b.svc.Post(ctx, "/v1/collection/list", input, &output); err != nil { + return nil, err + } + + return &output, nil +} + +type CollectionChildrenOutput struct { + Parent util.ID `json:"parent" cbor:"parent"` + GID util.ID `json:"gid" cbor:"gid"` + CID util.ID `json:"cid" cbor:"cid"` + Kind int8 `json:"kind" cbor:"kind"` + Ord float64 `json:"ord" cbor:"ord"` + Status int8 `json:"status" cbor:"status"` + Rating int8 `json:"rating" cbor:"rating"` + UpdatedAt int64 `json:"updated_at" cbor:"updated_at"` + Cover string `json:"cover" cbor:"cover"` + Price int64 `json:"price" cbor:"price"` + Language string `json:"language" cbor:"language"` + Title string `json:"title" cbor:"title"` + Summary string `json:"summary" cbor:"summary"` +} + +func (b *Writing) ListCollectionChildren(ctx context.Context, input *IDGIDPagination) (*SuccessResponse[CollectionOutputs], error) { + output := SuccessResponse[CollectionOutputs]{} + if err := b.svc.Post(ctx, "/v1/collection/list_children", input, &output); err != nil { + return nil, err + } + + return &output, nil +} + +func (b *Writing) ListCollectionByChild(ctx context.Context, input *QueryGidCid) (*SuccessResponse[CollectionOutputs], error) { + output := SuccessResponse[CollectionOutputs]{} + query := url.Values{} + query.Add("gid", input.GID.String()) + query.Add("cid", input.CID.String()) + query.Add("status", strconv.Itoa(int(input.Status))) + query.Add("fields", input.Fields) + if err := b.svc.Get(ctx, "/v1/collection/list_by_child?"+query.Encode(), &output); err != nil { + return nil, err + } + + return &output, nil +} diff --git a/src/bll/writing_creation.go b/src/bll/writing_creation.go index e6aea04..dd11d7a 100644 --- a/src/bll/writing_creation.go +++ b/src/bll/writing_creation.go @@ -20,7 +20,9 @@ type CreateCreationInput struct { Keywords *[]string `json:"keywords,omitempty" cbor:"keywords,omitempty" validate:"omitempty,gte=0,lte=5"` Labels *[]string `json:"labels,omitempty" cbor:"labels,omitempty" validate:"omitempty,gte=0,lte=5"` Authors *[]string `json:"authors,omitempty" cbor:"authors,omitempty" validate:"omitempty,gte=0,lte=10"` + Summary *string `json:"summary,omitempty" cbor:"summary,omitempty" validate:"omitempty,gte=4,lte=2048"` License *string `json:"license,omitempty" cbor:"license,omitempty"` + Parent *util.ID `json:"parent,omitempty" cbor:"parent,omitempty"` } func (i *CreateCreationInput) Validate() error { @@ -36,6 +38,7 @@ type CreationOutput struct { GID util.ID `json:"gid" cbor:"gid"` Status *int8 `json:"status,omitempty" cbor:"status,omitempty"` Rating *int8 `json:"rating,omitempty" cbor:"rating,omitempty"` + Price *int64 `json:"price,omitempty" cbor:"price,omitempty"` Version *uint16 `json:"version,omitempty" cbor:"version,omitempty"` Language *string `json:"language,omitempty" cbor:"language,omitempty"` Creator *util.ID `json:"creator,omitempty" cbor:"creator,omitempty"` @@ -121,17 +124,7 @@ func (b *Writing) CreateCreation(ctx context.Context, input *CreateCreationInput return &output.Result, nil } -type QueryCreation struct { - GID util.ID `json:"gid" cbor:"gid" query:"gid" validate:"required"` - ID util.ID `json:"id" cbor:"id" query:"id" validate:"required"` - Fields string `json:"fields" cbor:"fields" query:"fields"` -} - -func (i *QueryCreation) Validate() error { - return nil -} - -func (b *Writing) GetCreation(ctx context.Context, input *QueryCreation) (*CreationOutput, error) { +func (b *Writing) GetCreation(ctx context.Context, input *QueryGidID) (*CreationOutput, error) { output := SuccessResponse[CreationOutput]{} query := url.Values{} @@ -178,7 +171,7 @@ func (b *Writing) UpdateCreation(ctx context.Context, input *UpdateCreationInput return &output.Result, nil } -func (b *Writing) DeleteCreation(ctx context.Context, input *QueryCreation) (bool, error) { +func (b *Writing) DeleteCreation(ctx context.Context, input *QueryGidID) (bool, error) { output := SuccessResponse[bool]{} query := url.Values{} @@ -200,15 +193,22 @@ func (b *Writing) ListCreation(ctx context.Context, input *GIDPagination) (*Succ return &output, nil } -// TODO: more validation -type UpdateCreationStatusInput struct { - GID util.ID `json:"gid" cbor:"gid" validate:"required"` - ID util.ID `json:"id" cbor:"id" validate:"required"` - UpdatedAt int64 `json:"updated_at" cbor:"updated_at" validate:"required"` - Status int8 `json:"status" cbor:"status" validate:"gte=-2,lte=2"` +func (b *Writing) UpdateCreationStatus(ctx context.Context, input *UpdateStatusInput) (*CreationOutput, error) { + output := SuccessResponse[CreationOutput]{} + if err := b.svc.Patch(ctx, "/v1/creation/update_status", input, &output); err != nil { + return nil, err + } + + return &output.Result, nil +} + +type UpdateCreationPriceInput struct { + GID util.ID `json:"gid" cbor:"gid" validate:"required"` + ID util.ID `json:"id" cbor:"id" validate:"required"` + Price int64 `json:"price" cbor:"price" validate:"gte=-1,lte=100000"` } -func (i *UpdateCreationStatusInput) Validate() error { +func (i *UpdateCreationPriceInput) Validate() error { if err := util.Validator.Struct(i); err != nil { return gear.ErrBadRequest.From(err) } @@ -216,13 +216,13 @@ func (i *UpdateCreationStatusInput) Validate() error { return nil } -func (b *Writing) UpdateCreationStatus(ctx context.Context, input *UpdateCreationStatusInput) (*CreationOutput, error) { - output := SuccessResponse[CreationOutput]{} - if err := b.svc.Patch(ctx, "/v1/creation/update_status", input, &output); err != nil { - return nil, err +func (b *Writing) UpdateCreationPrice(ctx context.Context, input *UpdateCreationPriceInput) (bool, error) { + output := SuccessResponse[bool]{} + if err := b.svc.Patch(ctx, "/v1/creation/update_price", input, &output); err != nil { + return false, err } - return &output.Result, nil + return output.Result, nil } // TODO: more validation @@ -250,3 +250,23 @@ func (b *Writing) UpdateCreationContent(ctx context.Context, input *UpdateCreati return &output.Result, nil } + +func (b *Writing) InternalGetCreationSubscription(ctx context.Context, id util.ID) (*SubscriptionOutput, error) { + output := SuccessResponse[SubscriptionOutput]{} + query := url.Values{} + query.Add("id", id.String()) + if err := b.svc.Get(ctx, "/v1/creation/subscription?"+query.Encode(), &output); err != nil { + return nil, err + } + + return &output.Result, nil +} + +func (b *Writing) InternalUpdateCreationSubscription(ctx context.Context, input *SubscriptionInput) (*SubscriptionOutput, error) { + output := SuccessResponse[SubscriptionOutput]{} + if err := b.svc.Put(ctx, "/v1/creation/subscription", input, &output); err != nil { + return nil, err + } + + return &output.Result, nil +} diff --git a/src/bll/writing_creation_test.go b/src/bll/writing_creation_test.go index 164fcda..af28570 100644 --- a/src/bll/writing_creation_test.go +++ b/src/bll/writing_creation_test.go @@ -23,7 +23,7 @@ func TestCreateCreationInput(t *testing.T) { assert.NoError(obj.Validate()) str := `{"gid":"0000000000000jarvis0","id":"0000000000000jarvis0","updated_at":123,"status":0}` - var input UpdateCreationStatusInput + var input UpdateStatusInput err = json.Unmarshal([]byte(str), &input) fmt.Println(input) require.NoError(t, err) diff --git a/src/bll/writing_message.go b/src/bll/writing_message.go index 8d926b0..9987549 100644 --- a/src/bll/writing_message.go +++ b/src/bll/writing_message.go @@ -84,6 +84,7 @@ func (b *Writing) CreateMessage(ctx context.Context, input *CreateMessageInput) type UpdateMessageInput struct { ID util.ID `json:"id" cbor:"id" validate:"required"` + GID util.ID `json:"gid" cbor:"gid" validate:"required"` Version uint16 `json:"version" cbor:"version" validate:"gte=1,lte=32767"` Context *string `json:"context,omitempty" cbor:"context,omitempty" validate:"omitempty,gte=4,lte=1024"` Language *string `json:"language,omitempty" cbor:"language,omitempty"` diff --git a/src/bll/writing_publication.go b/src/bll/writing_publication.go index f3c59fd..8a350f9 100644 --- a/src/bll/writing_publication.go +++ b/src/bll/writing_publication.go @@ -52,61 +52,35 @@ type PublicationDraft struct { } type PublicationOutput struct { - GID util.ID `json:"gid" cbor:"gid"` - CID util.ID `json:"cid" cbor:"cid"` - Language string `json:"language" cbor:"language"` - Version uint16 `json:"version" cbor:"version"` - Rating *int8 `json:"rating,omitempty" cbor:"rating,omitempty"` - Status *int8 `json:"status,omitempty" cbor:"status,omitempty"` - Creator *util.ID `json:"creator,omitempty" cbor:"creator,omitempty"` - CreatedAt *int64 `json:"created_at,omitempty" cbor:"created_at,omitempty"` - UpdatedAt *int64 `json:"updated_at,omitempty" cbor:"updated_at,omitempty"` - Model *string `json:"model,omitempty" cbor:"model,omitempty"` - OriginalUrl *string `json:"original_url,omitempty" cbor:"original_url,omitempty"` - FromLanguage *string `json:"from_language,omitempty" cbor:"from_language,omitempty"` - Genre *[]string `json:"genre,omitempty" cbor:"genre,omitempty"` - Title *string `json:"title,omitempty" cbor:"title,omitempty"` - Cover *string `json:"cover,omitempty" cbor:"cover,omitempty"` - Keywords *[]string `json:"keywords,omitempty" cbor:"keywords,omitempty"` - Authors *[]string `json:"authors,omitempty" cbor:"authors,omitempty"` - Summary *string `json:"summary,omitempty" cbor:"summary,omitempty"` - Content *util.Bytes `json:"content,omitempty" cbor:"content,omitempty"` - License *string `json:"license,omitempty" cbor:"license,omitempty"` - CreatorInfo *UserInfo `json:"creator_info,omitempty" cbor:"creator_info,omitempty"` - GroupInfo *GroupInfo `json:"group_info,omitempty" cbor:"group_info,omitempty"` + GID util.ID `json:"gid" cbor:"gid"` + CID util.ID `json:"cid" cbor:"cid"` + Language string `json:"language" cbor:"language"` + Version uint16 `json:"version" cbor:"version"` + Rating *int8 `json:"rating,omitempty" cbor:"rating,omitempty"` + Price *int64 `json:"price,omitempty" cbor:"price,omitempty"` + Status *int8 `json:"status,omitempty" cbor:"status,omitempty"` + Creator *util.ID `json:"creator,omitempty" cbor:"creator,omitempty"` + CreatedAt *int64 `json:"created_at,omitempty" cbor:"created_at,omitempty"` + UpdatedAt *int64 `json:"updated_at,omitempty" cbor:"updated_at,omitempty"` + Model *string `json:"model,omitempty" cbor:"model,omitempty"` + OriginalUrl *string `json:"original_url,omitempty" cbor:"original_url,omitempty"` + FromLanguage *string `json:"from_language,omitempty" cbor:"from_language,omitempty"` + Genre *[]string `json:"genre,omitempty" cbor:"genre,omitempty"` + Title *string `json:"title,omitempty" cbor:"title,omitempty"` + Cover *string `json:"cover,omitempty" cbor:"cover,omitempty"` + Keywords *[]string `json:"keywords,omitempty" cbor:"keywords,omitempty"` + Authors *[]string `json:"authors,omitempty" cbor:"authors,omitempty"` + Summary *string `json:"summary,omitempty" cbor:"summary,omitempty"` + Content *util.Bytes `json:"content,omitempty" cbor:"content,omitempty"` + License *string `json:"license,omitempty" cbor:"license,omitempty"` + Subscription *SubscriptionOutput `json:"subscription,omitempty" cbor:"subscription,omitempty"` + RFP *RFP `json:"rfp,omitempty" cbor:"rfp,omitempty"` + FromGID *util.ID `json:"from_gid,omitempty" cbor:"from_gid,omitempty"` + GroupInfo *GroupInfo `json:"group_info,omitempty" cbor:"group_info,omitempty"` } type PublicationOutputs []PublicationOutput -func (list *PublicationOutputs) LoadCreators(loader func(ids ...util.ID) []UserInfo) { - if len(*list) == 0 { - return - } - - ids := make([]util.ID, 0, len(*list)) - for _, v := range *list { - if v.Creator != nil { - ids = append(ids, *v.Creator) - } - } - - users := loader(ids...) - if len(users) == 0 { - return - } - - infoMap := make(map[util.ID]*UserInfo, len(users)) - for i := range users { - infoMap[*users[i].ID] = &users[i] - infoMap[*users[i].ID].ID = nil - } - - for i := range *list { - (*list)[i].CreatorInfo = infoMap[*(*list)[i].Creator] - (*list)[i].Creator = nil - } -} - func (list *PublicationOutputs) LoadGroups(loader func(ids ...util.ID) []GroupInfo) { if len(*list) == 0 { return @@ -232,7 +206,7 @@ func (i *PublicationOutput) IntoPublicationDraft(gid util.ID, language, model st func (b *Writing) InitApp(ctx context.Context, _ *gear.App) error { for _, v := range conf.Config.Recommendations { - res, err := b.GetPublicationList(ctx, 2, &GidCidInput{ + res, err := b.GetPublicationList(ctx, 2, &QueryGidCid{ GID: v.GID, CID: v.CID, }) @@ -303,9 +277,11 @@ func (b *Writing) GetPublication(ctx context.Context, input *QueryPublication) ( type ImplicitQueryPublication struct { CID util.ID `json:"cid" cbor:"cid" query:"cid" validate:"required"` GID *util.ID `json:"gid" cbor:"gid" query:"gid"` + Parent *util.ID `json:"parent" cbor:"parent" query:"parent"` Language string `json:"language" cbor:"language" query:"language"` Version uint16 `json:"version" cbor:"version" query:"version" validate:"omitempty,gte=0,lte=10000"` Fields string `json:"fields" cbor:"fields" query:"fields"` + SubToken string `json:"subtoken" cbor:"subtoken" query:"subtoken"` } func (i *ImplicitQueryPublication) Validate() error { @@ -316,7 +292,10 @@ func (i *ImplicitQueryPublication) Validate() error { return nil } -func (b *Writing) ImplicitGetPublication(ctx context.Context, input *ImplicitQueryPublication) (*PublicationOutput, error) { +// ImplicitGetPublication is used to get a publication. +// It will check the subscription if subscription_in privided. (ignore checking if nil) +func (b *Writing) ImplicitGetPublication(ctx context.Context, input *ImplicitQueryPublication, + subscription_in *util.ID) (*PublicationOutput, error) { output := SuccessResponse[PublicationOutput]{} query := url.Values{} @@ -324,6 +303,12 @@ func (b *Writing) ImplicitGetPublication(ctx context.Context, input *ImplicitQue if input.GID != nil { query.Add("gid", input.GID.String()) } + if input.Parent != nil { + query.Add("parent", input.Parent.String()) + } + if subscription_in != nil { + query.Add("subscription_in", subscription_in.String()) + } if input.Language != "" { query.Add("language", input.Language) } @@ -434,7 +419,7 @@ func (b *Writing) ListLatestPublications(ctx context.Context, input *Pagination) return &output, nil } -func (b *Writing) GetPublicationList(ctx context.Context, from_status int8, input *GidCidInput) (*SuccessResponse[PublicationOutputs], error) { +func (b *Writing) GetPublicationList(ctx context.Context, from_status int8, input *QueryGidCid) (*SuccessResponse[PublicationOutputs], error) { output := SuccessResponse[PublicationOutputs]{} query := url.Values{} query.Add("gid", input.GID.String()) diff --git a/src/conf/config.go b/src/conf/config.go index 63c37a4..e20af42 100644 --- a/src/conf/config.go +++ b/src/conf/config.go @@ -2,14 +2,18 @@ package conf import ( "context" + "encoding/base64" "fmt" "math/rand" "os" + "path/filepath" "sync" "sync/atomic" "time" "github.com/BurntSushi/toml" + "github.com/fxamacker/cbor/v2" + "github.com/ldclabs/cose/key" "github.com/teambition/gear" "github.com/yiwen-ai/yiwen-api/src/util" ) @@ -50,6 +54,11 @@ type Server struct { GracefulShutdown uint `json:"graceful_shutdown" toml:"graceful_shutdown"` } +type Keys struct { + Hmac string `json:"hmac" toml:"hmac"` + Aesgcm string `json:"aesgcm" toml:"aesgcm"` +} + type Redis struct { Prefix string `json:"prefix" toml:"prefix"` Node string `json:"node" toml:"node"` @@ -92,6 +101,7 @@ type ConfigTpl struct { Env string `json:"env" toml:"env"` Logger Logger `json:"log" toml:"log"` Server Server `json:"server" toml:"server"` + Keys Keys `json:"keys" toml:"keys"` Redis Redis `json:"redis" toml:"redis"` Base Base `json:"base" toml:"base"` OSS OSS `json:"oss" toml:"oss"` @@ -99,11 +109,28 @@ type ConfigTpl struct { Wechat Wechat `json:"wechat" toml:"wechat"` TokensRate map[string]float32 `json:"tokens_rate" toml:"tokens_rate"` Recommendations []Recommendation `json:"recommendations" toml:"recommendations"` + COSEKeys struct { + Hmac key.Key + Aesgcm key.Key + } globalJobs int64 // global async jobs counter for graceful shutdown } func (c *ConfigTpl) Validate() error { + var err error + execDir := os.Getenv("EXEC_DIR_PATH") + if execDir != "" { + c.Keys.Hmac = filepath.Join(execDir, c.Keys.Hmac) + c.Keys.Aesgcm = filepath.Join(execDir, c.Keys.Aesgcm) + } + + if c.COSEKeys.Hmac, err = readKey(c.Keys.Hmac); err != nil { + return err + } + if c.COSEKeys.Aesgcm, err = readKey(c.Keys.Aesgcm); err != nil { + return err + } return nil } @@ -119,6 +146,20 @@ func (c *ConfigTpl) JobsIdle() bool { return atomic.LoadInt64(&c.globalJobs) <= 0 } +func readKey(filePath string) (k key.Key, err error) { + var data []byte + data, err = os.ReadFile(filePath) + if err != nil { + return + } + data, err = base64.RawURLEncoding.DecodeString(string(data)) + if err != nil { + return + } + err = cbor.Unmarshal(data, &k) + return +} + func readConfig(v interface{}, path ...string) { once.Do(func() { filePath, err := getConfigFilePath(path...) diff --git a/src/service/oss.go b/src/service/oss.go index b240f8a..ed5279f 100644 --- a/src/service/oss.go +++ b/src/service/oss.go @@ -82,6 +82,9 @@ func (s *OSS) SignPostPolicy(gid, cid, lang string, version uint) PostFilePolicy // https://help.aliyun.com/zh/oss/use-cases/oss-performance-and-scalability-best-practices // 反转打散分区,避免热点 dir := fmt.Sprintf("%s/%s/%d/%s/", util.Reverse(cid), gid, version, lang) + if lang == "" { + dir = fmt.Sprintf("%s/%s/%d/", util.Reverse(cid), gid, version) + } data, _ := json.Marshal(map[string]any{ "expiration": expiration, diff --git a/src/util/cose.go b/src/util/cose.go new file mode 100644 index 0000000..c5f05e9 --- /dev/null +++ b/src/util/cose.go @@ -0,0 +1,69 @@ +package util + +import ( + "encoding/base64" + "errors" + + "github.com/ldclabs/cose/cose" + "github.com/ldclabs/cose/key" +) + +func EncodeMac0[T any](macer key.MACer, obj T, externalData []byte) (string, error) { + m := &cose.Mac0Message[T]{ + Protected: cose.Headers{}, + Unprotected: cose.Headers{}, + Payload: obj, + } + data, err := m.ComputeAndEncode(macer, externalData) + if err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(data), nil +} + +func DecodeMac0[T any](macer key.MACer, input string, externalData []byte) (*T, error) { + if input == "" { + return nil, errors.New("empty input") + } + data, err := base64.RawURLEncoding.DecodeString(input) + if err != nil { + return nil, err + } + + obj, err := cose.VerifyMac0Message[T](macer, data, externalData) + if err != nil { + return nil, err + } + + return &obj.Payload, nil +} + +func EncodeEncrypt0[T any](encryptor key.Encryptor, obj T, externalData []byte) (string, error) { + m := &cose.Encrypt0Message[T]{ + Protected: cose.Headers{}, + Unprotected: cose.Headers{}, + Payload: obj, + } + data, err := m.EncryptAndEncode(encryptor, externalData) + if err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(data), nil +} + +func DecodeEncrypt0[T any](encryptor key.Encryptor, input string, externalData []byte) (*T, error) { + if input == "" { + return nil, errors.New("empty input") + } + data, err := base64.RawURLEncoding.DecodeString(input) + if err != nil { + return nil, err + } + + obj, err := cose.DecryptEncrypt0Message[T](encryptor, data, externalData) + if err != nil { + return nil, err + } + + return &obj.Payload, nil +} diff --git a/src/util/cose_test.go b/src/util/cose_test.go new file mode 100644 index 0000000..82b8fe5 --- /dev/null +++ b/src/util/cose_test.go @@ -0,0 +1,76 @@ +package util + +import ( + "fmt" + "testing" + "time" + + "github.com/ldclabs/cose/iana" + "github.com/ldclabs/cose/key/aesgcm" + "github.com/ldclabs/cose/key/hmac" + "github.com/stretchr/testify/assert" +) + +type PaymentCode struct { + Kind int8 `cbor:"1,keyasint"` // 0: subscribe creation; 2: subscribe collection + ExpireAt uint64 `cbor:"2,keyasint"` // code 的失效时间,unix 秒 + Payee ID `cbor:"3,keyasint"` // 收款人 id + Amount uint64 `cbor:"4,keyasint"` // 花费的亿文币数量 + UID ID `cbor:"5,keyasint"` // 受益人 id + CID ID `cbor:"6,keyasint"` // 订阅对象 id + Duration uint64 `cbor:"7,keyasint"` // 增加的订阅时长,单位秒 +} + +func TestMac0(t *testing.T) { + assert := assert.New(t) + + k, err := hmac.GenerateKey(iana.AlgorithmHMAC_256_64) + assert.NoError(err) + macer, err := k.MACer() + assert.NoError(err) + + obj := PaymentCode{ + Kind: 2, + ExpireAt: uint64(time.Now().Add(time.Hour).Unix()), + Payee: NewID(), + Amount: 100000, + UID: NewID(), + CID: NewID(), + Duration: 3600 * 24 * 7, + } + + text, err := EncodeMac0(macer, obj, []byte("PaymentCode")) + assert.NoError(err) + fmt.Println(len(text), text) + + obj2, err := DecodeMac0[PaymentCode](macer, text, []byte("PaymentCode")) + assert.NoError(err) + assert.Equal(obj, *obj2) +} + +func TestEncrypt0(t *testing.T) { + assert := assert.New(t) + + k, err := aesgcm.GenerateKey(iana.AlgorithmA256GCM) + assert.NoError(err) + encryptor, err := k.Encryptor() + assert.NoError(err) + + obj := PaymentCode{ + Kind: 2, + ExpireAt: uint64(time.Now().Add(time.Hour).Unix()), + Payee: NewID(), + Amount: 100000, + UID: NewID(), + CID: NewID(), + Duration: 3600 * 24 * 7, + } + + text, err := EncodeEncrypt0(encryptor, obj, []byte("PaymentCode")) + assert.NoError(err) + fmt.Println(len(text), text) + + obj2, err := DecodeEncrypt0[PaymentCode](encryptor, text, []byte("PaymentCode")) + assert.NoError(err) + assert.Equal(obj, *obj2) +} diff --git a/src/util/id.go b/src/util/id.go index a915a66..60fd1c5 100644 --- a/src/util/id.go +++ b/src/util/id.go @@ -14,6 +14,7 @@ import ( var ZeroID ID var JARVIS ID = mustParseID("0000000000000jarvis0") // system user var ANON ID = mustParseID("000000000000000anon0") // anonymous user +var MinID ID = ID(xid.ID([12]byte{0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255})) func NewID() ID { return ID(xid.New()) @@ -45,6 +46,10 @@ func (id *ID) String() string { return xid.ID(*id).String() } +func (id ID) Compare(other ID) int { + return xid.ID(id).Compare(xid.ID(other)) +} + func (id ID) MarshalCBOR() ([]byte, error) { return cbor.Marshal(xid.ID(id).Bytes()) } @@ -195,3 +200,15 @@ func Unmarshal[T any](b *Bytes) (*T, error) { } return &v, nil } + +func Marshal[T any](v *T) (Bytes, error) { + if v == nil { + return nil, errors.New("nil value") + } + + b, err := cbor.Marshal(v) + if err != nil { + return nil, err + } + return Bytes(b), nil +} diff --git a/src/util/id_test.go b/src/util/id_test.go index 8cb74ec..1287840 100644 --- a/src/util/id_test.go +++ b/src/util/id_test.go @@ -18,6 +18,10 @@ func TestID(t *testing.T) { t.Run("CBOR", func(t *testing.T) { assert := assert.New(t) + assert.True(MinID.Compare(ZeroID) > 0) + assert.True(MinID.Compare(JARVIS) > 0) + assert.True(MinID.Compare(ANON) > 0) + data, err := cbor.Marshal(JARVIS) assert.NoError(err) var id ID