Rewrite it in rust
This commit is contained in:
parent
bd149a64a7
commit
763288866c
36 changed files with 2506 additions and 9087 deletions
18
.github/workflows/deploy.yml
vendored
18
.github/workflows/deploy.yml
vendored
|
|
@ -1,5 +1,6 @@
|
|||
name: Push To Yandex Cloud CR
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
|
|
@ -29,6 +30,9 @@ jobs:
|
|||
- name: Deploy Serverless Container
|
||||
id: deploy-sls-container
|
||||
uses: yc-actions/yc-sls-container-deploy@v2
|
||||
env:
|
||||
secret: e6qb3muh98m0as2r1r4b
|
||||
revision: e6q2tsf58jo5qtpjqkf5
|
||||
with:
|
||||
yc-sa-json-credentials: ${{ secrets.YC_SA_JSON_CREDENTIALS }}
|
||||
container-name: mtg-price-bot
|
||||
|
|
@ -40,14 +44,14 @@ jobs:
|
|||
revision-concurrency: 1
|
||||
revision-image-url: cr.yandex/crp7gp50nf1pitkpk3hq/mtg-price-bot:${{ github.sha }}
|
||||
revision-execution-timeout: 10
|
||||
revision-env: |
|
||||
YDB_CONNECTION_STRING=grpcs://ydb.serverless.yandexcloud.net:2135/ru-central1/b1gofbt3bqdgf7fash84/etnp6sprstj3c6k8n34s
|
||||
revision-secrets: |
|
||||
TG_TOKEN=e6qb3muh98m0as2r1r4b/e6q6lnhd66itrki6ao0s/TG_TOKEN
|
||||
VK_SECRET_KEY=e6qb3muh98m0as2r1r4b/e6q6lnhd66itrki6ao0s/VK_SECRET_KEY
|
||||
VK_GROUP_ID=e6qb3muh98m0as2r1r4b/e6q6lnhd66itrki6ao0s/VK_GROUP_ID
|
||||
VK_TOKEN=e6qb3muh98m0as2r1r4b/e6q6lnhd66itrki6ao0s/VK_TOKEN
|
||||
VK_CONFIRMATION_STRING=e6qb3muh98m0as2r1r4b/e6q6lnhd66itrki6ao0s/VK_CONFIRMATION_STRING
|
||||
TELOXIDE_TOKEN=$secret/$revision/TELOXIDE_TOKEN
|
||||
VK_TOKEN=$secret/$revision/VK_TOKEN
|
||||
VK_GROUP_ID=$secret/$revision/VK_GROUP_ID
|
||||
VK_SECRET=$secret/$revision/VK_SECRET
|
||||
VK_CONFIRMATION_STRING=$secret/$revision/VK_CONFIRMATION_STRING
|
||||
TG_SECRET=$secret/$revision/TG_SECRET
|
||||
SCG_CLIENT_GUID=$secret/$revision/SCG_CLIENT_GUID
|
||||
|
||||
- name: Deploy API Gateway
|
||||
id: deploy-gateway
|
||||
|
|
|
|||
11
.gitignore
vendored
11
.gitignore
vendored
|
|
@ -1,9 +1,2 @@
|
|||
vendor
|
||||
*.swp
|
||||
hosts
|
||||
.idea
|
||||
.venv
|
||||
coverage.out
|
||||
.envrc
|
||||
/my_token
|
||||
/bot
|
||||
/target
|
||||
.env
|
||||
|
|
|
|||
1985
Cargo.lock
generated
Normal file
1985
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "mtg-price-bot"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.79"
|
||||
axum = "0.7.3"
|
||||
regex = "1.10.2"
|
||||
reqwest = "0.11.23"
|
||||
scryfall = "0.14.2"
|
||||
serde = { version = "1.0.194", features = ["derive"] }
|
||||
serde_json = "1.0.111"
|
||||
teloxide = "0.12.2"
|
||||
tokio = { version = "1.35.1", features = ["full"] }
|
||||
30
Dockerfile
30
Dockerfile
|
|
@ -1,11 +1,21 @@
|
|||
FROM golang:1.20.4-alpine3.18
|
||||
COPY . /go/src/go-mtg-vk
|
||||
WORKDIR /go/src/go-mtg-vk
|
||||
RUN go build ./cmd/bot
|
||||
|
||||
FROM alpine:3.18
|
||||
RUN mkdir /app
|
||||
FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef
|
||||
WORKDIR /app
|
||||
COPY --from=0 /go/src/go-mtg-vk/bot .
|
||||
ENV GIN_MODE=release
|
||||
ENTRYPOINT ./bot
|
||||
|
||||
FROM chef AS planner
|
||||
COPY . .
|
||||
RUN cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
FROM chef AS builder
|
||||
COPY --from=planner /app/recipe.json recipe.json
|
||||
# Build dependencies - this is the caching Docker layer!
|
||||
RUN cargo chef cook --release --recipe-path recipe.json
|
||||
# Build application
|
||||
COPY . .
|
||||
RUN cargo build --release --bin mtg-price-bot
|
||||
|
||||
# We do not need the Rust toolchain to run the binary!
|
||||
FROM debian:bookworm-slim AS runtime
|
||||
RUN apt-get update && apt-get -y install libssl3 ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/target/release/mtg-price-bot /usr/local/bin
|
||||
ENTRYPOINT ["/usr/local/bin/mtg-price-bot"]
|
||||
|
|
|
|||
142
cmd/bot/main.go
142
cmd/bot/main.go
|
|
@ -1,142 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gitlab.com/flygrounder/go-mtg-vk/internal/caching"
|
||||
"gitlab.com/flygrounder/go-mtg-vk/internal/cardsinfo"
|
||||
"gitlab.com/flygrounder/go-mtg-vk/internal/scenario"
|
||||
"gitlab.com/flygrounder/go-mtg-vk/internal/telegram"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
|
||||
environ "github.com/ydb-platform/ydb-go-sdk-auth-environ"
|
||||
"github.com/ydb-platform/ydb-go-sdk/v3"
|
||||
"gitlab.com/flygrounder/go-mtg-vk/internal/vk"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
tgToken string
|
||||
vkGroupId int64
|
||||
vkSecretKey string
|
||||
vkToken string
|
||||
vkConfirmationString string
|
||||
ydbConnectionString string
|
||||
port string
|
||||
}
|
||||
|
||||
func getConfig() *config {
|
||||
var cfg config
|
||||
var exists bool
|
||||
var err error
|
||||
|
||||
cfg.tgToken, exists = os.LookupEnv("TG_TOKEN")
|
||||
if !exists {
|
||||
panic("TG_TOKEN environment variable not defined")
|
||||
}
|
||||
|
||||
vkGroupId, exists := os.LookupEnv("VK_GROUP_ID")
|
||||
if !exists {
|
||||
panic("VK_GROUP_ID environment variable not defined")
|
||||
}
|
||||
cfg.vkGroupId, err = strconv.ParseInt(vkGroupId, 10, 64)
|
||||
if err != nil {
|
||||
panic("VK_GROUP_ID is not a number")
|
||||
}
|
||||
|
||||
cfg.vkSecretKey, exists = os.LookupEnv("VK_SECRET_KEY")
|
||||
if !exists {
|
||||
panic("VK_SECRET_KEY environment variable not defined")
|
||||
}
|
||||
|
||||
cfg.vkToken, exists = os.LookupEnv("VK_TOKEN")
|
||||
if !exists {
|
||||
panic("VK_TOKEN environment variable not defined")
|
||||
}
|
||||
|
||||
cfg.vkConfirmationString, exists = os.LookupEnv("VK_CONFIRMATION_STRING")
|
||||
if !exists {
|
||||
panic("VK_CONFIRMATION_STRING environment variable not defined")
|
||||
}
|
||||
|
||||
cfg.ydbConnectionString, exists = os.LookupEnv("YDB_CONNECTION_STRING")
|
||||
if !exists {
|
||||
panic("YDB_CONNECTION_STRING environment variable not defined")
|
||||
}
|
||||
|
||||
cfg.port, exists = os.LookupEnv("PORT")
|
||||
if !exists {
|
||||
panic("PORT environment variable not defined")
|
||||
}
|
||||
|
||||
return &cfg
|
||||
}
|
||||
|
||||
func main() {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
|
||||
cfg := getConfig()
|
||||
|
||||
bot, _ := tgbotapi.NewBotAPI(cfg.tgToken)
|
||||
r := gin.Default()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
db, err := ydb.Open(ctx,
|
||||
cfg.ydbConnectionString,
|
||||
environ.WithEnvironCredentials(ctx),
|
||||
)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("connect error: %w", err))
|
||||
}
|
||||
defer func() { _ = db.Close(ctx) }()
|
||||
|
||||
cache := &caching.CacheClient{
|
||||
Storage: db.Table(),
|
||||
Expiration: 12 * time.Hour,
|
||||
Prefix: db.Name(),
|
||||
}
|
||||
err = cache.Init(context.Background())
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("init error: %w", err))
|
||||
}
|
||||
|
||||
logger := log.New(os.Stdout, "", 0)
|
||||
fetcher := &cardsinfo.Fetcher{}
|
||||
handler := vk.Handler{
|
||||
Scenario: &scenario.Scenario{
|
||||
Sender: &vk.ApiSender{
|
||||
Token: cfg.vkToken,
|
||||
Logger: logger,
|
||||
},
|
||||
Logger: logger,
|
||||
InfoFetcher: fetcher,
|
||||
Cache: cache,
|
||||
},
|
||||
SecretKey: cfg.vkSecretKey,
|
||||
GroupId: cfg.vkGroupId,
|
||||
ConfirmationString: cfg.vkConfirmationString,
|
||||
}
|
||||
|
||||
tgHandler := telegram.Handler{
|
||||
Scenario: &scenario.Scenario{
|
||||
Sender: &telegram.Sender{
|
||||
API: bot,
|
||||
},
|
||||
Logger: logger,
|
||||
InfoFetcher: fetcher,
|
||||
Cache: cache,
|
||||
},
|
||||
}
|
||||
|
||||
r.POST("vk", handler.HandleMessage)
|
||||
r.POST("tg", tgHandler.HandleMessage)
|
||||
_ = r.Run(":" + cfg.port)
|
||||
}
|
||||
24
go.mod
24
go.mod
|
|
@ -1,24 +0,0 @@
|
|||
module gitlab.com/flygrounder/go-mtg-vk
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/antchfx/htmlquery v1.2.3
|
||||
github.com/gin-gonic/gin v1.4.0
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rogpeppe/go-internal v1.9.0 // indirect
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
github.com/ydb-platform/ydb-go-sdk-auth-environ v0.1.3
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.47.2
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/h2non/gock.v1 v1.0.16
|
||||
gopkg.in/yaml.v2 v2.2.4 // indirect
|
||||
)
|
||||
280
go.sum
280
go.sum
|
|
@ -1,280 +0,0 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/antchfx/htmlquery v1.2.3 h1:sP3NFDneHx2stfNXCKbhHFo8XgNjCACnU/4AO5gWz6M=
|
||||
github.com/antchfx/htmlquery v1.2.3/go.mod h1:B0ABL+F5irhhMWg54ymEZinzMSi0Kt3I2if0BLYa3V0=
|
||||
github.com/antchfx/xpath v1.1.6 h1:6sVh6hB5T6phw1pFpHRQ+C4bd8sNI+O58flqtg7h0R0=
|
||||
github.com/antchfx/xpath v1.1.6/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ=
|
||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.1 h1:pC5DB52sCeK48Wlb9oPcdhnjkz1TKt1D/P7WKJ0kUcQ=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg=
|
||||
github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rekby/fixenv v0.3.2/go.mod h1:/b5LRc06BYJtslRtHKxsPWFT/ySpHV+rWvzTg+XWk4c=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
|
||||
github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20211115083454-9ca41db5ed9e h1:9LPdmD1vqadsDQUva6t2O9MbnyvoOgo8nFNPaOIH5U8=
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20211115083454-9ca41db5ed9e/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
|
||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20220203104745-929cf9c248bc/go.mod h1:cc138nptTn9eKptCQl/grxP6pBKpo/bnXDiOxuVZtps=
|
||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20221215182650-986f9d10542f h1:BBczNIM1MJHT7XkIUA8pThXWxJvxoBjcWvne3xwe2RI=
|
||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20221215182650-986f9d10542f/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I=
|
||||
github.com/ydb-platform/ydb-go-sdk-auth-environ v0.1.3 h1:+DrFgi0hjjLarFcWHwI6WPk28hcWr6N7ga+OHAxtemI=
|
||||
github.com/ydb-platform/ydb-go-sdk-auth-environ v0.1.3/go.mod h1:K5wHHoLBfmWc7zyiETOA1Spx7DN+4skQ7YeWy5snXKo=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.25.3/go.mod h1:PFizF/vJsdAgEwjK3DVSBD52kdmRkWfSIS2q2pA+e88=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.47.1/go.mod h1:oSLwnuilwIpaF5bJJMAofnGgzPJusoI3zWMNb8I+GnM=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.47.2 h1:nBPXs27hwve26x2DIec45IO0EnxZiB63SwTIPanre/4=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.47.2/go.mod h1:oSLwnuilwIpaF5bJJMAofnGgzPJusoI3zWMNb8I+GnM=
|
||||
github.com/ydb-platform/ydb-go-yc v0.10.2 h1:RAHy6g7ncxk1y0N4oS2MwYXLATqRqKBI6DYXuxpV2wo=
|
||||
github.com/ydb-platform/ydb-go-yc v0.10.2/go.mod h1:U1dX3LJy6zADId2DciCXlgrU/vphK1+CQzaefKq21dQ=
|
||||
github.com/ydb-platform/ydb-go-yc-metadata v0.5.2 h1:nMtixUijP0Z7iHJNT9fOL+dbmEzZxqU6Xk87ll7hqXg=
|
||||
github.com/ydb-platform/ydb-go-yc-metadata v0.5.2/go.mod h1:82SQ4L3PewiEmFW4oTMc1sfPjODasIYxD/SKGsbK74s=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191021144547-ec77196f6094/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3 h1:SeX3QUcBj3fciwnfPT9kt5gBhFy/FCZtYZ+I/RB8agc=
|
||||
google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
|
||||
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw=
|
||||
google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/h2non/gentleman.v1 v1.0.4/go.mod h1:JYuHVdFzS4MKOXe0o+chKJ4hCe6tqKKw9XH9YP6WFrg=
|
||||
gopkg.in/h2non/gock.v1 v1.0.16 h1:F11k+OafeuFENsjei5t2vMTSTs9L62AdyTe4E1cgdG8=
|
||||
gopkg.in/h2non/gock.v1 v1.0.16/go.mod h1:XVuDAssexPLwgxCLMvDTWNU5eqklsydR6I5phZ9oPB8=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
package caching
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/ydb-platform/ydb-go-sdk/v3/table"
|
||||
"github.com/ydb-platform/ydb-go-sdk/v3/table/options"
|
||||
"github.com/ydb-platform/ydb-go-sdk/v3/table/result"
|
||||
"github.com/ydb-platform/ydb-go-sdk/v3/table/result/named"
|
||||
"github.com/ydb-platform/ydb-go-sdk/v3/table/types"
|
||||
"gitlab.com/flygrounder/go-mtg-vk/internal/cardsinfo"
|
||||
)
|
||||
|
||||
type CacheClient struct {
|
||||
Storage table.Client
|
||||
Expiration time.Duration
|
||||
Prefix string
|
||||
}
|
||||
|
||||
func (client *CacheClient) Init(ctx context.Context) error {
|
||||
return client.Storage.Do(ctx, func(ctx context.Context, s table.Session) error {
|
||||
return s.CreateTable(
|
||||
ctx,
|
||||
path.Join(client.Prefix, "cache"),
|
||||
options.WithColumn("card", types.TypeString),
|
||||
options.WithColumn("prices", types.Optional(types.TypeJSON)),
|
||||
options.WithColumn("created_at", types.Optional(types.TypeTimestamp)),
|
||||
options.WithTimeToLiveSettings(
|
||||
options.NewTTLSettings().ColumnDateType("created_at").ExpireAfter(client.Expiration),
|
||||
),
|
||||
options.WithPrimaryKeyColumn("card"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (client *CacheClient) Set(ctx context.Context, key string, prices []cardsinfo.ScgCardPrice) error {
|
||||
const query = `
|
||||
DECLARE $cacheData AS List<Struct<
|
||||
card: String,
|
||||
prices: Json,
|
||||
created_at: Timestamp>>;
|
||||
|
||||
INSERT INTO cache SELECT cd.card AS card, cd.prices AS prices, cd.created_at AS created_at FROM AS_TABLE($cacheData) cd LEFT OUTER JOIN cache c ON cd.card = c.card WHERE c.card IS NULL`
|
||||
value, _ := json.Marshal(prices)
|
||||
return client.Storage.Do(ctx, func(ctx context.Context, s table.Session) (err error) {
|
||||
_, _, err = s.Execute(ctx, writeTx(), query, table.NewQueryParameters(
|
||||
table.ValueParam("$cacheData", types.ListValue(
|
||||
types.StructValue(
|
||||
types.StructFieldValue("card", types.StringValueFromString(key)),
|
||||
types.StructFieldValue("prices", types.JSONValueFromBytes(value)),
|
||||
types.StructFieldValue("created_at", types.TimestampValueFromTime(time.Now())),
|
||||
))),
|
||||
))
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func (client *CacheClient) Get(ctx context.Context, key string) ([]cardsinfo.ScgCardPrice, error) {
|
||||
const query = `
|
||||
DECLARE $card AS String;
|
||||
|
||||
SELECT UNWRAP(prices) AS prices FROM cache WHERE card = $card`
|
||||
var pricesStr string
|
||||
var res result.Result
|
||||
err := client.Storage.Do(ctx, func(ctx context.Context, s table.Session) (err error) {
|
||||
_, res, err = s.Execute(ctx, readTx(), query, table.NewQueryParameters(
|
||||
table.ValueParam("$card", types.StringValueFromString(key)),
|
||||
))
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to get key")
|
||||
}
|
||||
ok := res.NextResultSet(ctx)
|
||||
if !ok {
|
||||
return nil, errors.New("no key")
|
||||
}
|
||||
ok = res.NextRow()
|
||||
if !ok {
|
||||
return nil, errors.New("no key")
|
||||
}
|
||||
err = res.ScanNamed(
|
||||
named.Required("prices", &pricesStr),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan prices: %w", err)
|
||||
}
|
||||
var prices []cardsinfo.ScgCardPrice
|
||||
_ = json.Unmarshal([]byte(pricesStr), &prices)
|
||||
return prices, nil
|
||||
}
|
||||
|
||||
func writeTx() *table.TransactionControl {
|
||||
return table.TxControl(table.BeginTx(
|
||||
table.WithSerializableReadWrite(),
|
||||
), table.CommitTx())
|
||||
}
|
||||
|
||||
func readTx() *table.TransactionControl {
|
||||
return table.TxControl(table.BeginTx(
|
||||
table.WithOnlineReadOnly(),
|
||||
), table.CommitTx())
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
package cardsinfo
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ScgCardPrice struct {
|
||||
Price string
|
||||
Edition string
|
||||
Link string
|
||||
}
|
||||
|
||||
type card struct {
|
||||
Name string `json:"name"`
|
||||
Layout string `json:"layout"`
|
||||
}
|
||||
|
||||
func (c *card) getName() string {
|
||||
if c.Layout == "transform" {
|
||||
return strings.Replace(c.Name, "//", "|", 1)
|
||||
}
|
||||
return c.Name
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
package cardsinfo
|
||||
|
||||
type Fetcher struct {
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
package cardsinfo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const scryfallUrl = "https://api.scryfall.com"
|
||||
|
||||
func (f *Fetcher) GetNameByCardId(set string, number string) string {
|
||||
/*
|
||||
Note: number is string because some cards contain letters in their numbers.
|
||||
*/
|
||||
path := scryfallUrl + "/cards/" + strings.ToLower(set) + "/" + number
|
||||
return getCardByUrl(path)
|
||||
}
|
||||
|
||||
func (f *Fetcher) GetOriginalName(name string) string {
|
||||
path := scryfallUrl + "/cards/named?fuzzy=" + applyFilters(name)
|
||||
result := getCardByUrl(path)
|
||||
return result
|
||||
}
|
||||
|
||||
func applyFilters(name string) string {
|
||||
/*
|
||||
Despite of the rules of Russian language, letter ё is replaced with e on cards
|
||||
Sometimes it leads to wrong search results
|
||||
*/
|
||||
name = strings.ReplaceAll(name, "ё", "е")
|
||||
return url.QueryEscape(name)
|
||||
}
|
||||
|
||||
func getCardByUrl(path string) string {
|
||||
response, err := http.Get(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer func() {
|
||||
_ = response.Body.Close()
|
||||
}()
|
||||
data, _ := ioutil.ReadAll(response.Body)
|
||||
var v card
|
||||
err = json.Unmarshal(data, &v)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return v.getName()
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
package cardsinfo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestGetNameByCardId(t *testing.T) {
|
||||
defer gock.Off()
|
||||
|
||||
gock.New(scryfallUrl + "/set/1").Reply(http.StatusOK).JSON(card{
|
||||
Name: "card",
|
||||
})
|
||||
f := &Fetcher{}
|
||||
name := f.GetNameByCardId("set", "1")
|
||||
assert.Equal(t, "card", name)
|
||||
}
|
||||
|
||||
func TestGetOriginalName_Scryfall(t *testing.T) {
|
||||
defer gock.Off()
|
||||
|
||||
gock.New(scryfallUrl + "/cards/named?fuzzy=card").Reply(http.StatusOK).JSON(card{
|
||||
Name: "Result Card",
|
||||
})
|
||||
f := &Fetcher{}
|
||||
name := f.GetOriginalName("card")
|
||||
assert.Equal(t, "Result Card", name)
|
||||
}
|
||||
|
||||
func TestGetOriginalName_BadJson(t *testing.T) {
|
||||
defer gock.Off()
|
||||
|
||||
gock.New(scryfallUrl + "/cards/named?fuzzy=card").Reply(http.StatusOK).BodyString("}")
|
||||
f := &Fetcher{}
|
||||
name := f.GetOriginalName("card")
|
||||
assert.Equal(t, "", name)
|
||||
}
|
||||
|
||||
func TestGetOriginalName_DoubleSide(t *testing.T) {
|
||||
defer gock.Off()
|
||||
|
||||
gock.New(scryfallUrl + "/cards/named?fuzzy=card").Reply(http.StatusOK).JSON(card{
|
||||
Name: "Legion's Landing // Adanto, the First Fort",
|
||||
Layout: "transform",
|
||||
})
|
||||
f := &Fetcher{}
|
||||
name := f.GetOriginalName("card")
|
||||
assert.Equal(t, "Legion's Landing | Adanto, the First Fort", name)
|
||||
}
|
||||
|
||||
func TestGetOriginalName_Error(t *testing.T) {
|
||||
defer gock.Off()
|
||||
|
||||
gock.New(scryfallUrl + "/cards/named?fuzzy=card").ReplyError(errors.New("internal server error"))
|
||||
f := &Fetcher{}
|
||||
name := f.GetOriginalName("card")
|
||||
assert.Equal(t, "", name)
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
package cardsinfo
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/antchfx/htmlquery"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const scgDomain = "https://starcitygames.com"
|
||||
const scgSearchUrlTemplate = "https://starcitygames.hawksearch.com/sites/starcitygames/?tournament_legality=Legal&search_query="
|
||||
|
||||
func (f *Fetcher) GetPrices(name string) ([]ScgCardPrice, error) {
|
||||
prices, err := getPricesScg(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(prices) > 5 {
|
||||
return prices[:5], nil
|
||||
}
|
||||
return prices, nil
|
||||
}
|
||||
|
||||
func getPricesScg(name string) ([]ScgCardPrice, error) {
|
||||
escapedName := url.QueryEscape(name)
|
||||
searchUrl := scgSearchUrlTemplate + escapedName
|
||||
node, err := htmlquery.LoadURL(searchUrl)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot load url")
|
||||
}
|
||||
blocks := htmlquery.Find(node, "//div[@class=\"hawk-results-item\"]")
|
||||
var results []ScgCardPrice
|
||||
for _, block := range blocks {
|
||||
price := ScgCardPrice{}
|
||||
linkNode := htmlquery.FindOne(block, "//h2/a")
|
||||
price.Link = scgDomain + htmlquery.SelectAttr(linkNode, "href")
|
||||
editionNode := htmlquery.FindOne(block, "//p[@class=\"hawk-results-item__category\"]/a")
|
||||
if !strings.HasPrefix(htmlquery.SelectAttr(editionNode, "href"), "/shop/singles/") {
|
||||
continue
|
||||
}
|
||||
price.Edition = editionNode.FirstChild.Data
|
||||
priceNode := htmlquery.FindOne(block, "//span[@class='hawk-old-price']|//div[contains(concat(' ',normalize-space(@class),' '),' hawk-results-item__options-table-cell--price ')]")
|
||||
price.Price = priceNode.FirstChild.Data
|
||||
results = append(results, price)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
package cardsinfo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetPrices_Ok(t *testing.T) {
|
||||
defer gock.Off()
|
||||
|
||||
file, _ := os.Open("test_data/AcademyRuinsTest.html")
|
||||
gock.New(scgSearchUrlTemplate + "card").Reply(http.StatusOK).Body(file)
|
||||
f := &Fetcher{}
|
||||
prices, err := f.GetPrices("card")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []ScgCardPrice{
|
||||
{
|
||||
Price: "$6.99",
|
||||
Edition: "Double Masters",
|
||||
Link: "https://starcitygames.com/academy-ruins-sgl-mtg-2xm-309-enn/?sku=SGL-MTG-2XM-309-ENN1",
|
||||
},
|
||||
{
|
||||
Price: "$9.99",
|
||||
Edition: "Double Masters (Foil)",
|
||||
Link: "https://starcitygames.com/academy-ruins-sgl-mtg-2xm-309-enf/?sku=SGL-MTG-2XM-309-ENF1",
|
||||
},
|
||||
{
|
||||
Price: "$11.99",
|
||||
Edition: "Double Masters - Variants",
|
||||
Link: "https://starcitygames.com/academy-ruins-sgl-mtg-2xm2-369-enn/?sku=SGL-MTG-2XM2-369-ENN1",
|
||||
},
|
||||
{
|
||||
Price: "$14.99",
|
||||
Edition: "Double Masters - Variants (Foil)",
|
||||
Link: "https://starcitygames.com/academy-ruins-sgl-mtg-2xm2-369-enf/?sku=SGL-MTG-2XM2-369-ENF1",
|
||||
},
|
||||
{
|
||||
Price: "$7.99",
|
||||
Edition: "Modern Masters: 2013 Edition",
|
||||
Link: "https://starcitygames.com/academy-ruins-sgl-mtg-mma-219-enn/?sku=SGL-MTG-MMA-219-ENN1",
|
||||
},
|
||||
}, prices)
|
||||
}
|
||||
|
||||
func TestGetPrices_Unavailable(t *testing.T) {
|
||||
defer gock.Off()
|
||||
|
||||
gock.New(scgSearchUrlTemplate + "card").Reply(http.StatusBadGateway)
|
||||
f := &Fetcher{}
|
||||
_, err := f.GetPrices("card")
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestGetPrices_Empty(t *testing.T) {
|
||||
defer gock.Off()
|
||||
|
||||
file, _ := os.Open("test_data/EmptyTest.html")
|
||||
gock.New(scgSearchUrlTemplate + "card").Reply(http.StatusOK).Body(file)
|
||||
f := &Fetcher{}
|
||||
prices, err := f.GetPrices("card")
|
||||
assert.Nil(t, err)
|
||||
assert.Nil(t, prices)
|
||||
}
|
||||
|
||||
func TestGetPrices_FilterNonCards(t *testing.T) {
|
||||
defer gock.Off()
|
||||
|
||||
file, _ := os.Open("test_data/NonCards.html")
|
||||
gock.New(scgSearchUrlTemplate + "card").Reply(http.StatusOK).Body(file)
|
||||
f := &Fetcher{}
|
||||
prices, err := f.GetPrices("card")
|
||||
assert.Nil(t, err)
|
||||
expected := []ScgCardPrice{
|
||||
{
|
||||
Price: "$72.99",
|
||||
Edition: "3rd Edition - Black Border",
|
||||
Link: "https://starcitygames.com/sol-ring-sgl-mtg-3bb-274-frn/?sku=SGL-MTG-3BB-274-FRN3",
|
||||
},
|
||||
{
|
||||
Price: "$24.99",
|
||||
Edition: "3rd Edition/Revised",
|
||||
Link: "https://starcitygames.com/sol-ring-sgl-mtg-3ed-274-enn/?sku=SGL-MTG-3ED-274-ENN1",
|
||||
},
|
||||
{
|
||||
Price: "$1,999.99",
|
||||
Edition: "Alpha",
|
||||
Link: "https://starcitygames.com/sol-ring-sgl-mtg-lea-269-enn/?sku=SGL-MTG-LEA-269-ENN1",
|
||||
},
|
||||
{
|
||||
Price: "$1,199.99",
|
||||
Edition: "Beta",
|
||||
Link: "https://starcitygames.com/sol-ring-sgl-mtg-leb-270-enn/?sku=SGL-MTG-LEB-270-ENN1",
|
||||
},
|
||||
{
|
||||
Price: "$99.99",
|
||||
Edition: "Collectors' Edition",
|
||||
Link: "https://starcitygames.com/sol-ring-sgl-mtg-ced-270-enn/?sku=SGL-MTG-CED-270-ENN1",
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expected, prices)
|
||||
}
|
||||
3005
internal/cardsinfo/test_data/AcademyRuinsTest.html
vendored
3005
internal/cardsinfo/test_data/AcademyRuinsTest.html
vendored
File diff suppressed because it is too large
Load diff
356
internal/cardsinfo/test_data/EmptyTest.html
vendored
356
internal/cardsinfo/test_data/EmptyTest.html
vendored
|
|
@ -1,356 +0,0 @@
|
|||
<div>
|
||||
|
||||
<div id="hawkmetarobots"><meta name="robots" content="noindex,nofollow" /></div>
|
||||
<div id="hawkheadertitle"><title>Nosuchcardever at Star City Games</title></div>
|
||||
<div id="hawkmetadescription"></div>
|
||||
<div id="hawkmetakeywords"></div>
|
||||
<div id="hawkrelcanonical"></div>
|
||||
<div id="hawklandingpagecontent"></div>
|
||||
|
||||
<div id="hawktoptext">
|
||||
<div id="hawk-backToTop">
|
||||
<img src="//manage.hawksearch.com/sites/shared/images/top.png" />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div id="hawktoppager">
|
||||
|
||||
|
||||
<div class="clearfix">
|
||||
|
||||
<div class="hawk-searchView">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div id="hawkitemlist">
|
||||
|
||||
|
||||
<div style="padding-top:15px;" class="hawk-">
|
||||
Search was unable to find any results for <b>"nosuchcardever"</b>, you may have typed your word incorrectly, or are being too specific. Try using a broader search phrase.
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div id="hawkbottompager">
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div id="hawkexplain">
|
||||
|
||||
</div>
|
||||
|
||||
<div id="hawksmartbug">
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div id="hawkrelated">
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div id="hawkfacets">
|
||||
<input type="hidden" id="hdnhawkcompare" value="" />
|
||||
<input type="hidden" id="hdnhawkprv" value="" />
|
||||
<input type="hidden" id="hdnhawklp" value="" />
|
||||
<input type="hidden" id="hdnhawkadv" value="" />
|
||||
<input type="hidden" id="hdnhawkit" value="" />
|
||||
<input type="hidden" id="hdnhawkoperator" value="" />
|
||||
<input type="hidden" id="hdnhawkexpand" value="" />
|
||||
<input type="hidden" id="hdnhawkkeyword" value="nosuchcardever" />
|
||||
<input type="hidden" id="hdnhawkpg" value="" />
|
||||
<input type="hidden" id="hdnhawkmpp" value="24" />
|
||||
<input type="hidden" id="hdnhawksortby" value="" />
|
||||
<input type="hidden" id="hdnhawkb" value="" />
|
||||
<input type="hidden" id="hdnhawkcustom" value="" />
|
||||
<input type="hidden" id="hdnhawkdefaultmpp" value="24" />
|
||||
<input type="hidden" id="hdnhawkkeywordfield" value="search_query" />
|
||||
<input type="hidden" id="hdnhawktrackingid" value="54966a4b-5aca-48a6-bdc6-634cac18c27b" />
|
||||
<input type="hidden" id="hdnhawkflags" value="" />
|
||||
<input type="hidden" id="hdnhawktrackingversion" value="v2" />
|
||||
<input type="hidden" id="hdnhawkaid" value="" />
|
||||
<input type="hidden" id="hdnhawkp" value="" />
|
||||
<input type="hidden" id="hdnhawkssfid" value="35158" />
|
||||
<input type="hidden" id="hdnhawkislpc" value="0" />
|
||||
<input type="hidden" id="hdnhawkislpcip" value="0" />
|
||||
<input type="hidden" id="hdnhawksearchable" value="" />
|
||||
<input type="hidden" id="hdnhawksearchablefacets" value="" />
|
||||
<div class="hawk-railNavHeading">
|
||||
Narrow Results
|
||||
</div>
|
||||
|
||||
<div class="hawkRailNav">
|
||||
<div class="hawk-guidedNavWrapper">
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clear"></div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="hawkbannertop" class="hawk-bannerTop">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div id="hawkbannertopmobile" class="bannerTopMobile">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="hawkbannerlefttop" class="hawk-bannerLeftTop">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div id="hawkbannerlefttopMobile" class="bannerLeftTopMobile">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="hawkbannerleftbottom" class="hawk-bannerLeftBottom">
|
||||
|
||||
|
||||
<div>
|
||||
<div class="TagCloud">
|
||||
|
||||
<span class="TagWeight3" title="Count: 10534"><a href="/search?keyword=blood moon">blood moon</a></span>
|
||||
<span class="TagWeight5" title="Count: 8885"><a href="/search?keyword=chrome mox">chrome mox</a></span>
|
||||
<span class="TagWeight4" title="Count: 9488"><a href="/search?keyword=demonic tutor">demonic tutor</a></span>
|
||||
<span class="TagWeight1" title="Count: 47974"><a href="/search?keyword=force of will">force of will</a></span>
|
||||
<span class="TagWeight5" title="Count: 8719"><a href="/search?keyword=grim tutor">grim tutor</a></span>
|
||||
<span class="TagWeight4" title="Count: 9751"><a href="/search?keyword=jeweled lotus">jeweled lotus</a></span>
|
||||
<span class="TagWeight5" title="Count: 8370"><a href="/search?keyword=lightning bolt">lightning bolt</a></span>
|
||||
<span class="TagWeight5" title="Count: 8885"><a href="/search?keyword=lotus cobra">lotus cobra</a></span>
|
||||
<span class="TagWeight2" title="Count: 16372"><a href="/search?keyword=mana crypt">mana crypt</a></span>
|
||||
<span class="TagWeight3" title="Count: 12372"><a href="/search?keyword=mana drain">mana drain</a></span>
|
||||
<span class="TagWeight4" title="Count: 9300"><a href="/search?keyword=mox opal">mox opal</a></span>
|
||||
<span class="TagWeight2" title="Count: 14914"><a href="/search?keyword=sol ring">sol ring</a></span>
|
||||
<span class="TagWeight3" title="Count: 12017"><a href="/search?keyword=thoughtseize">thoughtseize</a></span>
|
||||
<span class="TagWeight5" title="Count: 7886"><a href="/search?keyword=uro">uro</a></span>
|
||||
<span class="TagWeight4" title="Count: 10162"><a href="/search?keyword=vampiric tutor">vampiric tutor</a></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div id="hawkbannerleftbottommobile" class="bannerLeftBottomMobile">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="hawkbannerbottom" class="hawk-bannerBottom">
|
||||
|
||||
|
||||
|
||||
<p><span style="background-color:#ffff00;"></span></p><span style="font-size:14px;"> - Please double check your spelling.</span><br style="font-size:14px;" /><span style="font-size:14px;"> - Try searching for an item that is less specific.</span><br style="font-size:14px;" /><span style="font-size:14px;"> - You can always narrow your search results later. </span><span style="background-color:#ffff00;"></span><span style="background-color:#ffff00;"><br /></span><p><span style="background-color:#ffff00;"></span><span style="background-color:#ffff00;"><br /></span><br /></p>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div id="hawkbannerbottomMobile" class="bannerBottomMobile">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="hawkbannerbottom2" class="hawk-bannerBottom2"><div id="ctl00_BannerBottom2_FeaturedBottom2_titleDiv" class="hawk-featured-title">Popular Items</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="grid_3 " >
|
||||
<div class="itemWrapper hawk-itemWrapper">
|
||||
|
||||
<a href="/rathi-dragon-sgl-mtg-9ed-210-jaf/" id="ctl00_BannerBottom2_FeaturedBottom2_lvItems_ctrl0_ctl00_item_lnk1" class="itemLink" onclick="return HawkSearch.link(event,'54966a4b-5aca-48a6-bdc6-634cac18c27b',1,'99998',0)">
|
||||
<img class="itemImage hawk-itemImage" src="" alt="" style="height:190px;" />
|
||||
</a>
|
||||
<h3 class="itemTitle">
|
||||
<em style="display: block;"></em>
|
||||
<a href="/rathi-dragon-sgl-mtg-9ed-210-jaf/" id="ctl00_BannerBottom2_FeaturedBottom2_lvItems_ctrl0_ctl00_item_lnk2" onclick="return HawkSearch.link(event,'54966a4b-5aca-48a6-bdc6-634cac18c27b',1,'99998',0)">Rathi Dragon</a>
|
||||
</h3>
|
||||
<span class="itemSku">SGL-MTG-9ED-210-JAF</span>
|
||||
<p class="itemDesc"></p>
|
||||
<p class="itemPrice">
|
||||
$19.99
|
||||
|
||||
</p>
|
||||
<div class="clearfix">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="clearfix">
|
||||
<div class="itemButtons clearfix">
|
||||
<a href="/rathi-dragon-sgl-mtg-9ed-210-jaf/" id="ctl00_BannerBottom2_FeaturedBottom2_lvItems_ctrl0_ctl00_item_lnk3" class="btnWrapper" onclick="return HawkSearch.link(event,'54966a4b-5aca-48a6-bdc6-634cac18c27b',1,'99998',0)"><span class="btn">View Details</span></a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="grid_3 " >
|
||||
<div class="itemWrapper hawk-itemWrapper">
|
||||
|
||||
<a href="/rathi-dragon-sgl-mtg-9ed-210-zsf/" id="ctl00_BannerBottom2_FeaturedBottom2_lvItems_ctrl0_ctl01_item_lnk1" class="itemLink" onclick="return HawkSearch.link(event,'54966a4b-5aca-48a6-bdc6-634cac18c27b',2,'99996',0)">
|
||||
<img class="itemImage hawk-itemImage" src="" alt="" style="height:190px;" />
|
||||
</a>
|
||||
<h3 class="itemTitle">
|
||||
<em style="display: block;"></em>
|
||||
<a href="/rathi-dragon-sgl-mtg-9ed-210-zsf/" id="ctl00_BannerBottom2_FeaturedBottom2_lvItems_ctrl0_ctl01_item_lnk2" onclick="return HawkSearch.link(event,'54966a4b-5aca-48a6-bdc6-634cac18c27b',2,'99996',0)">Rathi Dragon</a>
|
||||
</h3>
|
||||
<span class="itemSku">SGL-MTG-9ED-210-ZSF</span>
|
||||
<p class="itemDesc"></p>
|
||||
<p class="itemPrice">
|
||||
$14.99
|
||||
|
||||
</p>
|
||||
<div class="clearfix">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="clearfix">
|
||||
<div class="itemButtons clearfix">
|
||||
<a href="/rathi-dragon-sgl-mtg-9ed-210-zsf/" id="ctl00_BannerBottom2_FeaturedBottom2_lvItems_ctrl0_ctl01_item_lnk3" class="btnWrapper" onclick="return HawkSearch.link(event,'54966a4b-5aca-48a6-bdc6-634cac18c27b',2,'99996',0)"><span class="btn">View Details</span></a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="grid_3 " >
|
||||
<div class="itemWrapper hawk-itemWrapper">
|
||||
|
||||
<a href="/rathi-dragon-sgl-mtg-9ed-210-enn/" id="ctl00_BannerBottom2_FeaturedBottom2_lvItems_ctrl0_ctl02_item_lnk1" class="itemLink" onclick="return HawkSearch.link(event,'54966a4b-5aca-48a6-bdc6-634cac18c27b',3,'99995',0)">
|
||||
<img class="itemImage hawk-itemImage" src="https://cdn11.bigcommerce.com/s-3b5vpig99v/products/99995/images/402488/RathiDragon__87752.1590611958.386.513.jpg?c=2" alt="" style="height:190px;" />
|
||||
</a>
|
||||
<h3 class="itemTitle">
|
||||
<em style="display: block;"></em>
|
||||
<a href="/rathi-dragon-sgl-mtg-9ed-210-enn/" id="ctl00_BannerBottom2_FeaturedBottom2_lvItems_ctrl0_ctl02_item_lnk2" onclick="return HawkSearch.link(event,'54966a4b-5aca-48a6-bdc6-634cac18c27b',3,'99995',0)">Rathi Dragon</a>
|
||||
</h3>
|
||||
<span class="itemSku">SGL-MTG-9ED-210-ENN</span>
|
||||
<p class="itemDesc"></p>
|
||||
<p class="itemPrice">
|
||||
$0.49
|
||||
|
||||
</p>
|
||||
<div class="clearfix">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="clearfix">
|
||||
<div class="itemButtons clearfix">
|
||||
<a href="/rathi-dragon-sgl-mtg-9ed-210-enn/" id="ctl00_BannerBottom2_FeaturedBottom2_lvItems_ctrl0_ctl02_item_lnk3" class="btnWrapper" onclick="return HawkSearch.link(event,'54966a4b-5aca-48a6-bdc6-634cac18c27b',3,'99995',0)"><span class="btn">View Details</span></a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div id="hawkbannerbottom2Mobile" class="bannerBottom2Mobile">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="hawkbreadcrumb"><div class="breadcrumbs"><div class="brmbwrpr"><span><a href="https://starcitygames.com/search">Home</a></span> > <span><a href="https://starcitygames.com/search?keyword=nosuchcardever"><b>nosuchcardever</b></a></span></div></div></div>
|
||||
|
||||
|
||||
<div id="hawktitle">
|
||||
<h1 class="hdng" id="pageHeading" >Search Results for Nosuchcardever</h1>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
4139
internal/cardsinfo/test_data/NonCards.html
vendored
4139
internal/cardsinfo/test_data/NonCards.html
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -1,91 +0,0 @@
|
|||
package scenario
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"gitlab.com/flygrounder/go-mtg-vk/internal/cardsinfo"
|
||||
)
|
||||
|
||||
const (
|
||||
incorrectMessage = "Некорректная команда"
|
||||
cardNotFoundMessage = "Карта не найдена"
|
||||
pricesUnavailableMessage = "Цены временно недоступны, попробуйте позже"
|
||||
)
|
||||
|
||||
type Scenario struct {
|
||||
Sender Sender
|
||||
Logger *log.Logger
|
||||
InfoFetcher CardInfoFetcher
|
||||
Cache CardCache
|
||||
}
|
||||
|
||||
type UserMessage struct {
|
||||
Body string
|
||||
UserId int64
|
||||
}
|
||||
|
||||
type CardCache interface {
|
||||
Init(ctx context.Context) error
|
||||
Get(ctx context.Context, cardName string) ([]cardsinfo.ScgCardPrice, error)
|
||||
Set(ctx context.Context, cardName string, prices []cardsinfo.ScgCardPrice) error
|
||||
}
|
||||
|
||||
type CardInfoFetcher interface {
|
||||
GetNameByCardId(set string, number string) string
|
||||
GetOriginalName(name string) string
|
||||
GetPrices(name string) ([]cardsinfo.ScgCardPrice, error)
|
||||
}
|
||||
|
||||
type Sender interface {
|
||||
Send(userId int64, message string)
|
||||
SendPrices(userId int64, cardName string, prices []cardsinfo.ScgCardPrice)
|
||||
}
|
||||
|
||||
func (s *Scenario) HandleSearch(ctx context.Context, msg *UserMessage) {
|
||||
cardName, err := s.getCardNameByCommand(msg.Body)
|
||||
if err != nil {
|
||||
s.Sender.Send(msg.UserId, incorrectMessage)
|
||||
s.Logger.Printf("[info] Not correct command. Message: %s user input: %s", err.Error(), msg.Body)
|
||||
} else if cardName == "" {
|
||||
s.Sender.Send(msg.UserId, cardNotFoundMessage)
|
||||
s.Logger.Printf("[info] Could not find card. User input: %s", msg.Body)
|
||||
} else {
|
||||
prices, err := s.Cache.Get(ctx, cardName)
|
||||
if err == nil {
|
||||
s.Sender.SendPrices(msg.UserId, cardName, prices)
|
||||
return
|
||||
}
|
||||
prices, err = s.InfoFetcher.GetPrices(cardName)
|
||||
if err != nil {
|
||||
s.Sender.Send(msg.UserId, pricesUnavailableMessage)
|
||||
s.Logger.Printf("[error] Could not find SCG prices. Message: %s card name: %s", err.Error(), cardName)
|
||||
return
|
||||
}
|
||||
err = s.Cache.Set(ctx, cardName, prices)
|
||||
if err != nil {
|
||||
s.Logger.Println(fmt.Errorf("failed add entry in cache: %w", err))
|
||||
}
|
||||
s.Sender.SendPrices(msg.UserId, cardName, prices)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scenario) getCardNameByCommand(command string) (string, error) {
|
||||
var name string
|
||||
switch {
|
||||
case strings.HasPrefix(command, "!s"):
|
||||
split := strings.Split(command, " ")
|
||||
if len(split) < 3 {
|
||||
return "", errors.New("wrong command")
|
||||
}
|
||||
set := split[1]
|
||||
number := split[2]
|
||||
name = s.InfoFetcher.GetNameByCardId(set, number)
|
||||
default:
|
||||
name = s.InfoFetcher.GetOriginalName(command)
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
package scenario
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestScenario_HandleSearch_BadCommand(t *testing.T) {
|
||||
testCtx := GetTestScenarioCtx()
|
||||
testCtx.Scenario.HandleSearch(context.Background(), &UserMessage{
|
||||
Body: "!s",
|
||||
UserId: 1,
|
||||
})
|
||||
assert.Equal(t, []testMessage{
|
||||
{
|
||||
userId: 1,
|
||||
message: incorrectMessage,
|
||||
},
|
||||
}, testCtx.Sender.sent)
|
||||
assert.True(t, strings.Contains(testCtx.LogBuf.String(), "[info]"))
|
||||
}
|
||||
|
||||
func TestScenario_HandleSearch_GoodCommand(t *testing.T) {
|
||||
testCtx := GetTestScenarioCtx()
|
||||
testCtx.Scenario.HandleSearch(context.Background(), &UserMessage{
|
||||
Body: "!s grn 228",
|
||||
UserId: 1,
|
||||
})
|
||||
assert.Equal(t, []testMessage{
|
||||
{
|
||||
userId: 1,
|
||||
message: "good",
|
||||
},
|
||||
}, testCtx.Sender.sent)
|
||||
}
|
||||
|
||||
func TestScenario_HandleSearch_NotFoundCard(t *testing.T) {
|
||||
testCtx := GetTestScenarioCtx()
|
||||
testCtx.Scenario.HandleSearch(context.Background(), &UserMessage{
|
||||
Body: "absolutely_random_card",
|
||||
UserId: 1,
|
||||
})
|
||||
assert.Equal(t, []testMessage{
|
||||
{
|
||||
userId: 1,
|
||||
message: cardNotFoundMessage,
|
||||
},
|
||||
}, testCtx.Sender.sent)
|
||||
assert.True(t, strings.Contains(testCtx.LogBuf.String(), "[info]"))
|
||||
}
|
||||
|
||||
func TestScenario_HandleSearch_BadCard(t *testing.T) {
|
||||
testCtx := GetTestScenarioCtx()
|
||||
testCtx.Scenario.HandleSearch(context.Background(), &UserMessage{
|
||||
Body: "bad",
|
||||
UserId: 1,
|
||||
})
|
||||
assert.Equal(t, []testMessage{
|
||||
{
|
||||
userId: 1,
|
||||
message: pricesUnavailableMessage,
|
||||
},
|
||||
}, testCtx.Sender.sent)
|
||||
assert.True(t, strings.Contains(testCtx.LogBuf.String(), "[error]"))
|
||||
}
|
||||
func TestScenario_HandleSearch_Uncached(t *testing.T) {
|
||||
testCtx := GetTestScenarioCtx()
|
||||
testCtx.Scenario.HandleSearch(context.Background(), &UserMessage{
|
||||
Body: "uncached",
|
||||
UserId: 1,
|
||||
})
|
||||
assert.Equal(t, []testMessage{
|
||||
{
|
||||
userId: 1,
|
||||
message: "uncached",
|
||||
},
|
||||
}, testCtx.Sender.sent)
|
||||
_, err := testCtx.Scenario.Cache.Get(context.Background(), "uncached")
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package scenario
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"gitlab.com/flygrounder/go-mtg-vk/internal/cardsinfo"
|
||||
)
|
||||
|
||||
type testCache struct {
|
||||
table map[string][]cardsinfo.ScgCardPrice
|
||||
}
|
||||
|
||||
func (t *testCache) Init(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *testCache) Get(ctx context.Context, cardName string) ([]cardsinfo.ScgCardPrice, error) {
|
||||
msg, ok := t.table[cardName]
|
||||
if !ok {
|
||||
return nil, errors.New("test")
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func (t *testCache) Set(ctx context.Context, cardName string, prices []cardsinfo.ScgCardPrice) error {
|
||||
t.table[cardName] = prices
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
package scenario
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
|
||||
"gitlab.com/flygrounder/go-mtg-vk/internal/cardsinfo"
|
||||
)
|
||||
|
||||
type TestScenarioCtx struct {
|
||||
Scenario *Scenario
|
||||
Sender *testSender
|
||||
LogBuf *bytes.Buffer
|
||||
}
|
||||
|
||||
func GetTestScenarioCtx() TestScenarioCtx {
|
||||
sender := &testSender{}
|
||||
buf := &bytes.Buffer{}
|
||||
return TestScenarioCtx{
|
||||
LogBuf: buf,
|
||||
Scenario: &Scenario{
|
||||
Sender: sender,
|
||||
Logger: log.New(buf, "", 0),
|
||||
InfoFetcher: &testInfoFetcher{},
|
||||
Cache: &testCache{
|
||||
table: map[string][]cardsinfo.ScgCardPrice{
|
||||
"good": {
|
||||
{
|
||||
Price: "1",
|
||||
Edition: "alpha",
|
||||
Link: "scg",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Sender: sender,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
package scenario
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"gitlab.com/flygrounder/go-mtg-vk/internal/cardsinfo"
|
||||
)
|
||||
|
||||
type testInfoFetcher struct{}
|
||||
|
||||
func (t *testInfoFetcher) GetPrices(name string) ([]cardsinfo.ScgCardPrice, error) {
|
||||
if name == "good" || name == "uncached" {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.New("test")
|
||||
}
|
||||
|
||||
func (t *testInfoFetcher) GetNameByCardId(_ string, _ string) string {
|
||||
return "good"
|
||||
}
|
||||
|
||||
func (t *testInfoFetcher) GetOriginalName(name string) string {
|
||||
if name == "good" || name == "bad" || name == "uncached" {
|
||||
return name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
package scenario
|
||||
|
||||
import "gitlab.com/flygrounder/go-mtg-vk/internal/cardsinfo"
|
||||
|
||||
type testSender struct {
|
||||
sent []testMessage
|
||||
}
|
||||
|
||||
func (s *testSender) SendPrices(userId int64, cardName string, prices []cardsinfo.ScgCardPrice) {
|
||||
s.sent = append(s.sent, testMessage{
|
||||
userId: userId,
|
||||
message: cardName,
|
||||
})
|
||||
}
|
||||
|
||||
type testMessage struct {
|
||||
userId int64
|
||||
message string
|
||||
}
|
||||
|
||||
func (s *testSender) Send(userId int64, message string) {
|
||||
s.sent = append(s.sent, testMessage{
|
||||
userId: userId,
|
||||
message: message,
|
||||
})
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package telegram
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
|
||||
"gitlab.com/flygrounder/go-mtg-vk/internal/scenario"
|
||||
)
|
||||
|
||||
const welcomeMessage = "Здравствуйте, вас приветствует бот для поиска цен на карты MTG, введите название карты, которая вас интересует."
|
||||
|
||||
type Handler struct {
|
||||
Scenario *scenario.Scenario
|
||||
}
|
||||
|
||||
func (h *Handler) HandleMessage(c *gin.Context) {
|
||||
var upd tgbotapi.Update
|
||||
err := c.Bind(&upd)
|
||||
if err != nil || upd.Message == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if upd.Message.Text == "/start" {
|
||||
h.Scenario.Sender.Send(upd.Message.Chat.ID, welcomeMessage)
|
||||
return
|
||||
}
|
||||
|
||||
h.Scenario.HandleSearch(context.Background(), &scenario.UserMessage{
|
||||
Body: upd.Message.Text,
|
||||
UserId: upd.Message.Chat.ID,
|
||||
})
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
package telegram
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
|
||||
"gitlab.com/flygrounder/go-mtg-vk/internal/cardsinfo"
|
||||
)
|
||||
|
||||
type Sender struct {
|
||||
API *tgbotapi.BotAPI
|
||||
}
|
||||
|
||||
func (s *Sender) SendPrices(userId int64, cardName string, prices []cardsinfo.ScgCardPrice) {
|
||||
msg := formatCardPrices(cardName, prices)
|
||||
s.Send(userId, msg)
|
||||
}
|
||||
|
||||
func (h *Sender) Send(userId int64, message string) {
|
||||
msg := tgbotapi.NewMessage(userId, message)
|
||||
msg.DisableWebPagePreview = true
|
||||
msg.ParseMode = tgbotapi.ModeMarkdown
|
||||
h.API.Send(msg)
|
||||
}
|
||||
|
||||
func formatCardPrices(name string, prices []cardsinfo.ScgCardPrice) string {
|
||||
escapedName := strings.ReplaceAll(name, "_", "\\_")
|
||||
message := fmt.Sprintf("Оригинальное название: %v\n\n", escapedName)
|
||||
for i, v := range prices {
|
||||
message += fmt.Sprintf("%v. %v", i+1, formatPrice(v))
|
||||
}
|
||||
if len(prices) == 0 {
|
||||
message += "Цен не найдено\n"
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func formatPrice(s cardsinfo.ScgCardPrice) string {
|
||||
return fmt.Sprintf("[%v](%v): %v\n", s.Edition, s.Link, s.Price)
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package telegram
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gitlab.com/flygrounder/go-mtg-vk/internal/cardsinfo"
|
||||
)
|
||||
|
||||
func Test_formatCardPrices(t *testing.T) {
|
||||
prices := []cardsinfo.ScgCardPrice{
|
||||
{
|
||||
Price: "1",
|
||||
Edition: "Alpha",
|
||||
Link: "scg1",
|
||||
},
|
||||
{
|
||||
Price: "2",
|
||||
Edition: "Beta",
|
||||
Link: "scg2",
|
||||
},
|
||||
}
|
||||
result := formatCardPrices("card", prices)
|
||||
assert.Equal(t, "Оригинальное название: card\n\n1. [Alpha](scg1): 1\n2. [Beta](scg2): 2\n", result)
|
||||
}
|
||||
|
||||
func Test_formatCardPricesEscapeUnderscore(t *testing.T) {
|
||||
prices := []cardsinfo.ScgCardPrice{}
|
||||
result := formatCardPrices("_____", prices)
|
||||
assert.Equal(t, "Оригинальное название: \\_\\_\\_\\_\\_\n\nЦен не найдено\n", result)
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package vk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitlab.com/flygrounder/go-mtg-vk/internal/cardsinfo"
|
||||
)
|
||||
|
||||
func formatCardPrices(name string, prices []cardsinfo.ScgCardPrice) string {
|
||||
message := fmt.Sprintf("Оригинальное название: %v\n\n", name)
|
||||
for i, v := range prices {
|
||||
message += fmt.Sprintf("%v. %v", i+1, formatPrice(v))
|
||||
}
|
||||
if len(prices) == 0 {
|
||||
message += "Цен не найдено\n"
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func formatPrice(s cardsinfo.ScgCardPrice) string {
|
||||
return fmt.Sprintf("%v: %v\n%v\n", s.Edition, s.Price, s.Link)
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package vk
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gitlab.com/flygrounder/go-mtg-vk/internal/cardsinfo"
|
||||
)
|
||||
|
||||
func TestFormatCardPrices(t *testing.T) {
|
||||
formatted := formatCardPrices("card", []cardsinfo.ScgCardPrice{
|
||||
{
|
||||
Price: "1.5$",
|
||||
Edition: "ED",
|
||||
Link: "scg.com",
|
||||
},
|
||||
})
|
||||
assert.Equal(t, "Оригинальное название: card\n\n1. ED: 1.5$\nscg.com\n", formatted)
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
package vk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gitlab.com/flygrounder/go-mtg-vk/internal/scenario"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
Scenario *scenario.Scenario
|
||||
SecretKey string
|
||||
GroupId int64
|
||||
ConfirmationString string
|
||||
DictPath string
|
||||
}
|
||||
|
||||
type cardInfoFetcher interface {
|
||||
GetFormattedCardPrices(name string) (string, error)
|
||||
GetNameByCardId(set string, number string) string
|
||||
GetOriginalName(name string) string
|
||||
}
|
||||
|
||||
type cardCache interface {
|
||||
Get(cardName string) (string, error)
|
||||
Set(cardName string, message string)
|
||||
}
|
||||
|
||||
type messageRequest struct {
|
||||
Type string `json:"type"`
|
||||
GroupId int64 `json:"group_id"`
|
||||
Object userMessage `json:"object"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
type userMessage struct {
|
||||
Body string `json:"text"`
|
||||
UserId int64 `json:"peer_id"`
|
||||
}
|
||||
|
||||
const (
|
||||
incorrectMessage = "Некорректная команда"
|
||||
cardNotFoundMessage = "Карта не найдена"
|
||||
pricesUnavailableMessage = "Цены временно недоступны, попробуйте позже"
|
||||
)
|
||||
|
||||
func (h *Handler) HandleMessage(c *gin.Context) {
|
||||
var req messageRequest
|
||||
_ = c.BindJSON(&req)
|
||||
if req.Secret != h.SecretKey {
|
||||
return
|
||||
}
|
||||
switch req.Type {
|
||||
case "confirmation":
|
||||
h.handleConfirmation(c, &req)
|
||||
case "message_new":
|
||||
h.Scenario.HandleSearch(context.Background(), &scenario.UserMessage{
|
||||
Body: req.Object.Body,
|
||||
UserId: req.Object.UserId,
|
||||
})
|
||||
c.String(http.StatusOK, "ok")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) handleConfirmation(c *gin.Context, req *messageRequest) {
|
||||
if (req.Type == "confirmation") && (req.GroupId == h.GroupId) {
|
||||
c.String(http.StatusOK, h.ConfirmationString)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
package vk
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHandler_HandleMessage_Confirm(t *testing.T) {
|
||||
testCtx := getTestHandlerCtx()
|
||||
ctx := getTestRequestCtx(&messageRequest{
|
||||
Type: "confirmation",
|
||||
GroupId: testCtx.handler.GroupId,
|
||||
Secret: testCtx.handler.SecretKey,
|
||||
}, testCtx.recorder)
|
||||
testCtx.handler.HandleMessage(ctx)
|
||||
assert.Equal(t, testCtx.handler.ConfirmationString, testCtx.recorder.Body.String())
|
||||
}
|
||||
|
||||
func TestHandler_HandleMessage_Message(t *testing.T) {
|
||||
testCtx := getTestHandlerCtx()
|
||||
ctx := getTestRequestCtx(&messageRequest{
|
||||
Type: "message_new",
|
||||
Secret: testCtx.handler.SecretKey,
|
||||
}, testCtx.recorder)
|
||||
testCtx.handler.HandleMessage(ctx)
|
||||
assert.Equal(t, "ok", testCtx.recorder.Body.String())
|
||||
}
|
||||
|
||||
func TestHandler_HandleMessage_NoSecretKey(t *testing.T) {
|
||||
testCtx := getTestHandlerCtx()
|
||||
ctx := getTestRequestCtx(&messageRequest{
|
||||
Type: "message_new",
|
||||
}, testCtx.recorder)
|
||||
testCtx.handler.HandleMessage(ctx)
|
||||
assert.Equal(t, "", testCtx.recorder.Body.String())
|
||||
}
|
||||
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
package vk
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitlab.com/flygrounder/go-mtg-vk/internal/cardsinfo"
|
||||
)
|
||||
|
||||
const sendMessageUrl = "https://api.vk.com/method/messages.send"
|
||||
|
||||
type sender interface {
|
||||
send(userId int64, message string)
|
||||
}
|
||||
|
||||
type ApiSender struct {
|
||||
Token string
|
||||
Logger *log.Logger
|
||||
}
|
||||
|
||||
func (s *ApiSender) SendPrices(userId int64, cardName string, prices []cardsinfo.ScgCardPrice) {
|
||||
msg := formatCardPrices(cardName, prices)
|
||||
s.Send(userId, msg)
|
||||
}
|
||||
|
||||
type sendMessageResponse struct {
|
||||
Error errorResponse `json:"error"`
|
||||
}
|
||||
|
||||
type errorResponse struct {
|
||||
ErrorCode int `json:"error_code"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
}
|
||||
|
||||
func (s *ApiSender) Send(userId int64, message string) {
|
||||
randomId := rand.Int63()
|
||||
params := []string{
|
||||
"access_token=" + s.Token,
|
||||
"peer_id=" + strconv.FormatInt(userId, 10),
|
||||
"message=" + url.QueryEscape(message),
|
||||
"v=5.95",
|
||||
"random_id=" + strconv.FormatInt(randomId, 10),
|
||||
"dont_parse_links=1",
|
||||
}
|
||||
joined := strings.Join(params, "&")
|
||||
reqUrl := sendMessageUrl + "?" + joined
|
||||
resp, err := http.Get(reqUrl)
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
s.Logger.Printf("[error] Could not Send message. User: %d", userId)
|
||||
return
|
||||
}
|
||||
respContent, _ := ioutil.ReadAll(resp.Body)
|
||||
var unmarshalled sendMessageResponse
|
||||
_ = json.Unmarshal(respContent, &unmarshalled)
|
||||
if unmarshalled.Error.ErrorCode != 0 {
|
||||
s.Logger.Printf("[error] Message was not sent. User: %d error message: %s", userId, unmarshalled.Error.ErrorMsg)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
package vk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestApiSender_Send_OK(t *testing.T) {
|
||||
defer gock.Off()
|
||||
|
||||
gock.New(sendMessageUrl).MatchParams(
|
||||
map[string]string{
|
||||
"access_token": "token",
|
||||
"peer_id": "1",
|
||||
"message": "msg",
|
||||
"v": "5.95",
|
||||
"dont_parse_links": "1",
|
||||
},
|
||||
).ParamPresent("random_id").Reply(http.StatusOK)
|
||||
|
||||
sender := ApiSender{Token: "token"}
|
||||
sender.Send(1, "msg")
|
||||
assert.False(t, gock.HasUnmatchedRequest())
|
||||
}
|
||||
|
||||
func TestApiSender_Send_NotOK(t *testing.T) {
|
||||
defer gock.Off()
|
||||
|
||||
gock.New(sendMessageUrl).Reply(http.StatusInternalServerError)
|
||||
|
||||
b := &bytes.Buffer{}
|
||||
sender := ApiSender{
|
||||
Token: "token",
|
||||
Logger: log.New(b, "", 0),
|
||||
}
|
||||
sender.Send(1, "msg")
|
||||
assert.True(t, strings.Contains(b.String(), "[error]"))
|
||||
}
|
||||
|
||||
func TestApiSender_Send_ErrorCode(t *testing.T) {
|
||||
defer gock.Off()
|
||||
|
||||
gock.New(sendMessageUrl).Reply(http.StatusOK).JSON(
|
||||
map[string]interface{}{
|
||||
"error": map[string]interface{}{
|
||||
"error_code": 100,
|
||||
"error_msg": "bad user",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
b := &bytes.Buffer{}
|
||||
sender := ApiSender{
|
||||
Token: "token",
|
||||
Logger: log.New(b, "", 0),
|
||||
}
|
||||
sender.Send(1, "msg")
|
||||
assert.True(t, strings.Contains(b.String(), "[error]"))
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
package vk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gitlab.com/flygrounder/go-mtg-vk/internal/scenario"
|
||||
)
|
||||
|
||||
type testCtx struct {
|
||||
handler *Handler
|
||||
recorder *httptest.ResponseRecorder
|
||||
}
|
||||
|
||||
func getTestHandlerCtx() testCtx {
|
||||
s := scenario.GetTestScenarioCtx()
|
||||
return testCtx{
|
||||
handler: &Handler{
|
||||
SecretKey: "sec",
|
||||
GroupId: 10,
|
||||
ConfirmationString: "con",
|
||||
Scenario: s.Scenario,
|
||||
},
|
||||
recorder: httptest.NewRecorder(),
|
||||
}
|
||||
}
|
||||
|
||||
func getTestRequestCtx(msgReq *messageRequest, recorder *httptest.ResponseRecorder) *gin.Context {
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
body, _ := json.Marshal(msgReq)
|
||||
ctx.Request = httptest.NewRequest("POST", "/", bytes.NewReader(body))
|
||||
return ctx
|
||||
}
|
||||
4
justfile
Normal file
4
justfile
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
set dotenv-load
|
||||
|
||||
run:
|
||||
cargo watch -x run
|
||||
467
src/main.rs
Normal file
467
src/main.rs
Normal file
|
|
@ -0,0 +1,467 @@
|
|||
use anyhow::{Context, Result};
|
||||
use axum::extract::State;
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::routing::post;
|
||||
use axum::{Json, Router};
|
||||
use regex::Regex;
|
||||
use scryfall::card::Card;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use std::fmt::Display;
|
||||
use std::sync::Arc;
|
||||
use teloxide::payloads::SendMessage;
|
||||
use teloxide::prelude::*;
|
||||
use teloxide::requests::JsonRequest;
|
||||
use teloxide::types::ParseMode;
|
||||
use teloxide::utils::markdown::{escape, escape_link_url};
|
||||
use teloxide::Bot;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
vk_client: Arc<VkClient>,
|
||||
telegram_client: Arc<TelegramClient>,
|
||||
price_fetcher: Arc<PriceFetcher>,
|
||||
}
|
||||
|
||||
async fn get_card_name(query: &str) -> Result<String> {
|
||||
let number_search_regex = Regex::new(r#"^!s (?<set>\w{3}) (?<number>\d+)$"#)
|
||||
.context("failed to compile number search regex")?;
|
||||
let name = if let Some(captures) = number_search_regex.captures(query) {
|
||||
let set = captures
|
||||
.name("set")
|
||||
.context("failed to get 'set' value from capture")?
|
||||
.as_str()
|
||||
.to_string();
|
||||
let number: usize = captures
|
||||
.name("number")
|
||||
.context("failed to get 'number' from capture")?
|
||||
.as_str()
|
||||
.parse()
|
||||
.context("failed to parse collector number")?;
|
||||
Card::set_and_number(&set, number)
|
||||
.await
|
||||
.map(|card| card.name)
|
||||
.context("failed to get card by set and number")?
|
||||
} else {
|
||||
Card::named_fuzzy(query)
|
||||
.await
|
||||
.map(|card| card.name)
|
||||
.context("failed to find card by it's fuzzy name")?
|
||||
};
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct StarCityRequestPayload {
|
||||
keyword: String,
|
||||
#[serde(rename = "ClientGuid")]
|
||||
client_guid: String,
|
||||
#[serde(rename = "SortBy")]
|
||||
sort_by: String,
|
||||
#[serde(rename = "FacetSelections")]
|
||||
facet_selections: StarCityFacetSelection,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct StarCityFacetSelection {
|
||||
product_type: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct StarCityResponse {
|
||||
#[serde(rename = "Results")]
|
||||
results: Vec<StarCityResponseResult>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct StarCityResponseResult {
|
||||
#[serde(rename = "Document")]
|
||||
document: StarCityResponseDocument,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct StarCityResponseDocument {
|
||||
set: Vec<String>,
|
||||
hawk_child_attributes: Vec<StarCityResponseAttributesVariants>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum StarCityResponseAttributesVariants {
|
||||
Available(StarCityResponseAttributes),
|
||||
Unavailable {},
|
||||
}
|
||||
|
||||
impl StarCityResponseAttributesVariants {
|
||||
fn get_card_info(&self, set: &str) -> Option<CardInfo> {
|
||||
if let StarCityResponseAttributesVariants::Available(res) = self {
|
||||
let condition = res.condition.first().cloned()?;
|
||||
let price = res.price.first().cloned()?.parse().ok()?;
|
||||
let url = res.url.first().cloned()?;
|
||||
if condition == "Near Mint" {
|
||||
Some(CardInfo {
|
||||
set: set.to_string(),
|
||||
price,
|
||||
url: format!("https://starcitygames.com{url}"),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct StarCityResponseAttributes {
|
||||
price: Vec<String>,
|
||||
condition: Vec<String>,
|
||||
url: Vec<String>,
|
||||
}
|
||||
|
||||
struct CardInfo {
|
||||
set: String,
|
||||
price: f32,
|
||||
url: String,
|
||||
}
|
||||
|
||||
struct PriceFetcher {
|
||||
client_guid: String,
|
||||
}
|
||||
|
||||
impl PriceFetcher {
|
||||
fn from_env() -> Result<Self> {
|
||||
let client_guid =
|
||||
env::var("SCG_CLIENT_GUID").context("SCG_CLIENT_GUID env variable is not set")?;
|
||||
Ok(Self { client_guid })
|
||||
}
|
||||
|
||||
async fn get_card_prices(&self, name: &str) -> Result<Vec<CardInfo>> {
|
||||
let client = reqwest::ClientBuilder::new().build()?;
|
||||
let resp = client
|
||||
.post("https://essearchapi-na.hawksearch.com/api/v2/search")
|
||||
.json(&StarCityRequestPayload {
|
||||
keyword: name.to_string(),
|
||||
client_guid: self.client_guid.clone(),
|
||||
sort_by: "score".into(),
|
||||
facet_selections: StarCityFacetSelection {
|
||||
product_type: vec!["Singles".into()],
|
||||
},
|
||||
})
|
||||
.send()
|
||||
.await
|
||||
.context("request to SCG failed")?;
|
||||
|
||||
let response: StarCityResponse = resp.json().await.context("SCG returned invalid json")?;
|
||||
let res = response
|
||||
.results
|
||||
.iter()
|
||||
.flat_map(|result| {
|
||||
let set = result.document.set.first().cloned()?;
|
||||
let info = result
|
||||
.document
|
||||
.hawk_child_attributes
|
||||
.iter()
|
||||
.flat_map(|res| res.get_card_info(&set))
|
||||
.collect::<Vec<_>>();
|
||||
Some(info)
|
||||
})
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
struct TelegramClient {
|
||||
bot: Bot,
|
||||
secret: String,
|
||||
}
|
||||
|
||||
impl TelegramClient {
|
||||
fn from_env() -> Result<Self> {
|
||||
let secret = env::var("TG_SECRET").context("failed to get TG_SECRET env var")?;
|
||||
Ok(Self {
|
||||
bot: Bot::from_env(),
|
||||
secret,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TelegramUpdate {
|
||||
message: TelegramMessage,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TelegramMessage {
|
||||
text: String,
|
||||
chat: TelegramChat,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TelegramChat {
|
||||
id: i64,
|
||||
}
|
||||
|
||||
async fn telegram(
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<TelegramUpdate>,
|
||||
) -> Result<&'static str, StatusCode> {
|
||||
let secret = headers
|
||||
.get("X-Telegram-Bot-Api-Secret-Token")
|
||||
.ok_or(StatusCode::FORBIDDEN)?
|
||||
.to_str()
|
||||
.map_err(|_| StatusCode::FORBIDDEN)?;
|
||||
if secret != state.telegram_client.secret {
|
||||
Err(StatusCode::FORBIDDEN)?;
|
||||
}
|
||||
let chat_id = payload.message.chat.id;
|
||||
let text = &payload.message.text;
|
||||
let res = handle_telegram_message(&state, chat_id, text).await;
|
||||
if let Err(err) = res {
|
||||
report_error(state.telegram_client.as_ref(), chat_id, err).await;
|
||||
}
|
||||
Ok("OK")
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ErrorContext {
|
||||
Scryfall,
|
||||
Scg,
|
||||
Telegram,
|
||||
Vk,
|
||||
}
|
||||
|
||||
impl Display for ErrorContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let text = match self {
|
||||
Self::Scryfall => "failed to get card from scryfall",
|
||||
Self::Scg => "failed to get card from SCG",
|
||||
Self::Telegram => "failed to send message through Telergam",
|
||||
Self::Vk => "failed to send message through VK",
|
||||
};
|
||||
f.write_str(text)
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_telegram_message(state: &AppState, chat_id: i64, message: &str) -> Result<()> {
|
||||
let name = get_card_name(message)
|
||||
.await
|
||||
.context(ErrorContext::Scryfall)?;
|
||||
let prices = state
|
||||
.price_fetcher
|
||||
.get_card_prices(&name)
|
||||
.await
|
||||
.context(ErrorContext::Scg)?;
|
||||
let content = prices
|
||||
.iter()
|
||||
.take(5)
|
||||
.enumerate()
|
||||
.map(|(i, info)| {
|
||||
format!(
|
||||
"{}\\. [{}]({}): {}",
|
||||
i + 1,
|
||||
escape(&info.set),
|
||||
escape_link_url(&info.url),
|
||||
escape(&format!("${}", info.price))
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let header = escape(&format!("Оригинальное название: {}", name));
|
||||
let response = format!("{header}\n\n{content}");
|
||||
let request = SendMessage::new(ChatId(chat_id), response)
|
||||
.parse_mode(ParseMode::MarkdownV2)
|
||||
.disable_web_page_preview(true);
|
||||
JsonRequest::new(state.telegram_client.bot.clone(), request)
|
||||
.send()
|
||||
.await
|
||||
.context(ErrorContext::Telegram)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct VkClient {
|
||||
client: reqwest::Client,
|
||||
token: String,
|
||||
group_id: i64,
|
||||
confirmation: String,
|
||||
secret: String,
|
||||
}
|
||||
|
||||
impl VkClient {
|
||||
fn from_env() -> Result<Self> {
|
||||
let client = reqwest::Client::new();
|
||||
let token = env::var("VK_TOKEN").context("failed to get VK_TOKEN")?;
|
||||
let group_id = env::var("VK_GROUP_ID")
|
||||
.context("failed to get VK_GROUP_ID")
|
||||
.and_then(|x| x.parse().context("failed to parse VK_GROUP_ID as a number"))?;
|
||||
let confirmation =
|
||||
env::var("VK_CONFIRMATION_STRING").context("failed to get VK_CONFIRMATION_STRING")?;
|
||||
let secret = env::var("VK_SECRET").context("failed to get VK_SECRET")?;
|
||||
Ok(Self {
|
||||
client,
|
||||
token,
|
||||
group_id,
|
||||
confirmation,
|
||||
secret,
|
||||
})
|
||||
}
|
||||
|
||||
async fn send(&self, user_id: i64, message: &str) -> Result<()> {
|
||||
self.client
|
||||
.get("https://api.vk.com/method/messages.send")
|
||||
.query(&[
|
||||
("user_id", user_id.to_string().as_str()),
|
||||
("v", "5.131"),
|
||||
("access_token", &self.token),
|
||||
("random_id", "0"),
|
||||
("message", message),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
.context(ErrorContext::Vk)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
enum VkRequest {
|
||||
#[serde(rename = "message_new")]
|
||||
Message(VkMessageRequest),
|
||||
#[serde(rename = "confirmation")]
|
||||
Confirmation(VkConfirmationRequest),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct VkMessageRequest {
|
||||
object: VkMessageObject,
|
||||
secret: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct VkMessageObject {
|
||||
from_id: i64,
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct VkConfirmationRequest {
|
||||
group_id: i64,
|
||||
}
|
||||
|
||||
async fn vk(State(state): State<AppState>, Json(request): Json<VkRequest>) -> (StatusCode, String) {
|
||||
match request {
|
||||
VkRequest::Message(payload) => {
|
||||
if payload.secret != state.vk_client.secret {
|
||||
return (StatusCode::FORBIDDEN, "Access denied".into());
|
||||
}
|
||||
let user_id = payload.object.from_id;
|
||||
let message = &payload.object.text;
|
||||
let res = handle_vk_message(&state, user_id, message).await;
|
||||
if let Err(err) = res {
|
||||
report_error(state.vk_client.as_ref(), user_id, err).await;
|
||||
}
|
||||
}
|
||||
VkRequest::Confirmation(confirmation) => {
|
||||
if confirmation.group_id != state.vk_client.group_id {
|
||||
return (StatusCode::FORBIDDEN, "Access denied".into());
|
||||
}
|
||||
return (StatusCode::OK, state.vk_client.confirmation.clone());
|
||||
}
|
||||
}
|
||||
(StatusCode::OK, "OK".into())
|
||||
}
|
||||
|
||||
async fn handle_vk_message(state: &AppState, user_id: i64, message: &str) -> Result<()> {
|
||||
let name = get_card_name(message)
|
||||
.await
|
||||
.context(ErrorContext::Scryfall)?;
|
||||
let prices = state
|
||||
.price_fetcher
|
||||
.get_card_prices(&name)
|
||||
.await
|
||||
.context(ErrorContext::Scg)?;
|
||||
let header = escape(&format!("Оригинальное название: {}", name));
|
||||
let content = prices
|
||||
.iter()
|
||||
.take(5)
|
||||
.enumerate()
|
||||
.map(|(i, info)| format!("{}. {}: ${}\n{}", i + 1, info.set, info.price, info.url))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let response = format!("{header}\n\n{content}");
|
||||
state.vk_client.send(user_id, &response).await
|
||||
}
|
||||
|
||||
trait MessageSender {
|
||||
async fn send(&self, user_id: i64, message: &str) -> Result<()>;
|
||||
}
|
||||
|
||||
impl MessageSender for VkClient {
|
||||
async fn send(&self, user_id: i64, message: &str) -> Result<()> {
|
||||
self.send(user_id, message).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageSender for TelegramClient {
|
||||
async fn send(&self, user_id: i64, message: &str) -> Result<()> {
|
||||
self.bot.send_message(ChatId(user_id), message).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn report_error<T: MessageSender>(sender: &T, chat_id: i64, err: anyhow::Error) {
|
||||
if !matches!(
|
||||
err.downcast_ref::<scryfall::Error>(),
|
||||
Some(scryfall::Error::ScryfallError(_))
|
||||
) {
|
||||
println!("error: {:#}", err);
|
||||
}
|
||||
let sent = match err.downcast_ref::<ErrorContext>() {
|
||||
Some(ErrorContext::Scryfall) => sender
|
||||
.send(chat_id, "Карта не найдена")
|
||||
.await
|
||||
.map(|_| ())
|
||||
.context("failed to send error message"),
|
||||
Some(ErrorContext::Scg) => sender
|
||||
.send(chat_id, "Цены не найдены")
|
||||
.await
|
||||
.map(|_| ())
|
||||
.context("failed to send error message"),
|
||||
_ => Ok(()),
|
||||
};
|
||||
if let Err(err) = sent {
|
||||
println!("error: {:#}", err);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let port = env::var("PORT")
|
||||
.ok()
|
||||
.and_then(|x| x.parse().ok())
|
||||
.unwrap_or(3000);
|
||||
let addr: (&str, u16) = ("0.0.0.0", port);
|
||||
let listener = TcpListener::bind(addr)
|
||||
.await
|
||||
.context("failed to create tcp listener")?;
|
||||
let vk_client = VkClient::from_env().context("failed to init vk client")?;
|
||||
let telegram_client = TelegramClient::from_env().context("failed to init telegram client")?;
|
||||
let price_fetcher = PriceFetcher::from_env().context("failed to init price fetcher")?;
|
||||
let state = AppState {
|
||||
vk_client: Arc::new(vk_client),
|
||||
telegram_client: Arc::new(telegram_client),
|
||||
price_fetcher: Arc::new(price_fetcher),
|
||||
};
|
||||
let app = Router::new()
|
||||
.route("/tg", post(telegram))
|
||||
.route("/vk", post(vk))
|
||||
.with_state(state);
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue