├── .codecov.yml ├── .github ├── dependabot.yml ├── dependency-review-config.yml └── workflows │ ├── atlas.yml │ ├── commitlint.yml │ ├── dep-review.yml │ ├── release.yml │ └── x.yml ├── .gitignore ├── .k8s ├── .crd │ ├── README.md │ ├── cnp.cilium.io.yml │ └── monitoring.coreos.com_servicemonitors.yaml ├── deployment.yml ├── kubeconfig.sh ├── service.yaml └── servicemonitor.yml ├── .ogen.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── _data └── metrics.dump.txt ├── _oas └── openapi.yaml ├── atlas.hcl ├── cmd └── bot │ └── main.go ├── gen.go ├── go.coverage.sh ├── go.mod ├── go.sum ├── go.test.sh ├── internal ├── action │ ├── action.go │ ├── action_test.go │ ├── entity_str_gen.go │ └── type_str_gen.go ├── api │ ├── api.go │ ├── badge.go │ └── status.go ├── app │ ├── file_id.go │ ├── middleware.go │ ├── middleware_options.go │ ├── stats.go │ └── version.go ├── botapi │ ├── client.go │ ├── message.go │ └── options.go ├── cmd │ ├── cmd.go │ └── internal │ │ ├── job │ │ ├── cmd.go │ │ └── commits │ │ │ └── cmd.go │ │ └── server │ │ ├── app.go │ │ ├── bot.go │ │ ├── button.go │ │ ├── cmd.go │ │ ├── events.go │ │ ├── gh_pat.go │ │ ├── ghtest.go │ │ └── github.go ├── dispatch │ ├── base_event.go │ ├── bot.go │ ├── button.go │ ├── handle_button.go │ ├── handle_inline.go │ ├── handle_message.go │ ├── inline.go │ ├── logged.go │ ├── message.go │ ├── message_mux.go │ └── message_mux_test.go ├── ent │ ├── check.go │ ├── check │ │ ├── check.go │ │ └── where.go │ ├── check_create.go │ ├── check_delete.go │ ├── check_query.go │ ├── check_update.go │ ├── client.go │ ├── ent.go │ ├── entc.go │ ├── enttest │ │ └── enttest.go │ ├── generate.go │ ├── gitcommit.go │ ├── gitcommit │ │ ├── gitcommit.go │ │ └── where.go │ ├── gitcommit_create.go │ ├── gitcommit_delete.go │ ├── gitcommit_query.go │ ├── gitcommit_update.go │ ├── gptdialog.go │ ├── gptdialog │ │ ├── gptdialog.go │ │ └── where.go │ ├── gptdialog_create.go │ ├── gptdialog_delete.go │ ├── gptdialog_query.go │ ├── gptdialog_update.go │ ├── hook │ │ └── hook.go │ ├── intercept │ │ └── intercept.go │ ├── lastchannelmessage.go │ ├── lastchannelmessage │ │ ├── lastchannelmessage.go │ │ └── where.go │ ├── lastchannelmessage_create.go │ ├── lastchannelmessage_delete.go │ ├── lastchannelmessage_query.go │ ├── lastchannelmessage_update.go │ ├── migrate │ │ ├── migrate.go │ │ └── schema.go │ ├── mutation.go │ ├── organization.go │ ├── organization │ │ ├── organization.go │ │ └── where.go │ ├── organization_create.go │ ├── organization_delete.go │ ├── organization_query.go │ ├── organization_update.go │ ├── predicate │ │ └── predicate.go │ ├── prnotification.go │ ├── prnotification │ │ ├── prnotification.go │ │ └── where.go │ ├── prnotification_create.go │ ├── prnotification_delete.go │ ├── prnotification_query.go │ ├── prnotification_update.go │ ├── repository.go │ ├── repository │ │ ├── repository.go │ │ └── where.go │ ├── repository_create.go │ ├── repository_delete.go │ ├── repository_query.go │ ├── repository_update.go │ ├── runtime.go │ ├── runtime │ │ └── runtime.go │ ├── schema │ │ ├── check.go │ │ ├── commit.go │ │ ├── gpt_dialog.go │ │ ├── organization.go │ │ ├── repository.go │ │ ├── session.go │ │ ├── state.go │ │ ├── telegram_state.go │ │ └── user.go │ ├── telegramchannelstate.go │ ├── telegramchannelstate │ │ ├── telegramchannelstate.go │ │ └── where.go │ ├── telegramchannelstate_create.go │ ├── telegramchannelstate_delete.go │ ├── telegramchannelstate_query.go │ ├── telegramchannelstate_update.go │ ├── telegramsession.go │ ├── telegramsession │ │ ├── telegramsession.go │ │ └── where.go │ ├── telegramsession_create.go │ ├── telegramsession_delete.go │ ├── telegramsession_query.go │ ├── telegramsession_update.go │ ├── telegramuserstate.go │ ├── telegramuserstate │ │ ├── telegramuserstate.go │ │ └── where.go │ ├── telegramuserstate_create.go │ ├── telegramuserstate_delete.go │ ├── telegramuserstate_query.go │ ├── telegramuserstate_update.go │ ├── tx.go │ ├── user.go │ ├── user │ │ ├── user.go │ │ └── where.go │ ├── user_create.go │ ├── user_delete.go │ ├── user_query.go │ └── user_update.go ├── entdb │ └── ebtdb.go ├── entgaps │ └── entgaps.go ├── entsession │ └── storage.go ├── gh │ ├── _golden │ │ └── event_wh.json │ ├── _testdata │ │ ├── event.check.run.completed.json │ │ ├── event.json │ │ ├── event.status.json │ │ ├── event.workflow.job.completed.json │ │ └── event.workflow.run.json │ ├── client.go │ ├── db.go │ ├── doc.go │ ├── extract_meta.go │ ├── extract_meta_test.go │ ├── handle_check_run.go │ ├── handle_check_suite.go │ ├── handle_discussion.go │ ├── handle_issue.go │ ├── handle_pr.go │ ├── handle_pr_test.go │ ├── handle_release.go │ ├── handle_repo.go │ ├── handle_star.go │ ├── handle_status.go │ ├── handle_workflow_job.go │ ├── handle_workflow_run.go │ ├── main_test.go │ ├── transform.go │ ├── transform_test.go │ ├── updater.go │ └── webhook.go ├── gpt │ ├── doc.go │ ├── gpt.go │ ├── model.go │ ├── model_test.go │ ├── rate_limit.go │ ├── system_prompt.go │ └── system_prompt_test.go ├── oas │ ├── oas_cfg_gen.go │ ├── oas_client_gen.go │ ├── oas_faker_gen.go │ ├── oas_handlers_gen.go │ ├── oas_json_gen.go │ ├── oas_labeler_gen.go │ ├── oas_middleware_gen.go │ ├── oas_operations_gen.go │ ├── oas_parameters_gen.go │ ├── oas_request_decoders_gen.go │ ├── oas_request_encoders_gen.go │ ├── oas_response_decoders_gen.go │ ├── oas_response_encoders_gen.go │ ├── oas_router_gen.go │ ├── oas_schemas_gen.go │ ├── oas_server_gen.go │ ├── oas_test_examples_gen_test.go │ ├── oas_unimplemented_gen.go │ └── oas_validators_gen.go ├── otelredis │ └── otelredis.go └── stat │ ├── commits.go │ └── stat.go ├── migrate.Dockerfile ├── migrations ├── 20230411163934_init.sql ├── 20230412000613_state.sql ├── 20230412002223_session.sql ├── 20230412184602_gpt_dialog.sql ├── 20230413073523_user-token.sql ├── 20230414100629_checks.sql ├── 20230415000746_check_upd.sql ├── 20230415150924_gaps.sql ├── 20230415175734_gaps.sql ├── 20230417145105_pr_title.sql ├── 20230430144340_repository.sql ├── 20230430150308_organization.sql ├── 20230430162510_commit.sql └── atlas.sum └── tools.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: off 4 | project: 5 | default: 6 | threshold: 5% 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | groups: 8 | opentelemetry: 9 | patterns: 10 | - "go.opentelemetry.io/*" 11 | golang: 12 | patterns: 13 | - "golang.org/x/*" 14 | - package-ecosystem: github-actions 15 | directory: "/" 16 | schedule: 17 | interval: daily 18 | -------------------------------------------------------------------------------- /.github/dependency-review-config.yml: -------------------------------------------------------------------------------- 1 | fail_on_severity: 'low' 2 | allow_licenses: 3 | - 'MIT' 4 | - 'ISC' 5 | - 'MPL-2.0' 6 | - 'BSD-2-Clause' 7 | - 'BSD-3-Clause' 8 | - 'Apache-2.0' 9 | -------------------------------------------------------------------------------- /.github/workflows/atlas.yml: -------------------------------------------------------------------------------- 1 | name: atlas 2 | on: 3 | # push: 4 | # branches: 5 | # - main 6 | # pull_request: 7 | # paths: 8 | # - 'migrations/*' 9 | workflow_dispatch: 10 | jobs: 11 | lint: 12 | services: 13 | postgres15: 14 | image: postgres:15 15 | env: 16 | POSTGRES_DB: test 17 | POSTGRES_PASSWORD: pass 18 | ports: 19 | - 5430:5432 20 | options: >- 21 | --health-cmd pg_isready 22 | --health-interval 10s 23 | --health-timeout 5s 24 | --health-retries 5 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | with: 29 | fetch-depth: 0 # Mandatory unless "latest" is set below. 30 | - uses: ariga/atlas-action@v1 31 | with: 32 | dir: migrations 33 | dir-format: atlas 34 | dev-url: postgres://postgres:pass@localhost:5430/test?sslmode=disable -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | name: Commit linter 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | # Common Go workflows from go faster 8 | # See https://github.com/go-faster/x 9 | jobs: 10 | check: 11 | uses: go-faster/x/.github/workflows/commit.yml@main 12 | -------------------------------------------------------------------------------- /.github/workflows/dep-review.yml: -------------------------------------------------------------------------------- 1 | name: 'Dependency Review' 2 | on: [pull_request] 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | dependency-review: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: 'Checkout Repository' 12 | uses: actions/checkout@v4 13 | - name: 'Dependency Review' 14 | uses: actions/dependency-review-action@v4 15 | with: 16 | config-file: './.github/dependency-review-config.yml' 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | IMAGE_TAG: ghcr.io/${{ github.repository }} 11 | KUBECTL_VERSION: v1.29.7 12 | 13 | jobs: 14 | deploy: 15 | environment: prod 16 | runs-on: [deploy] 17 | permissions: 18 | contents: read 19 | packages: write 20 | env: 21 | KUBECONFIG: /tmp/kubeconfig 22 | 23 | # Skip deploy commit message contains #skip 24 | if: ${{ !contains(github.event.head_commit.message, '!skip') }} 25 | 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | 30 | - uses: docker/setup-buildx-action@v3 31 | 32 | - name: Install Go 33 | uses: actions/setup-go@v5 34 | with: 35 | go-version: "1.24.x" 36 | cache: false 37 | 38 | - name: Get Go environment 39 | id: go-env 40 | run: | 41 | echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV 42 | echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV 43 | echo "goversion=$(go env GOVERSION)" >> $GITHUB_ENV 44 | 45 | # - name: Set up cache 46 | # uses: actions/cache@v4 47 | # with: 48 | # path: | 49 | # ${{ env.cache }} 50 | # ${{ env.modcache }} 51 | # key: release-${{ runner.os }}-go-${{ env.goversion }}-${{ hashFiles('**/go.sum') }} 52 | # restore-keys: | 53 | # release-${{ runner.os }}-go-${{ env.goversion }} 54 | 55 | - name: Docker log in 56 | uses: docker/login-action@v3 57 | with: 58 | registry: ${{ env.REGISTRY }} 59 | username: ${{ github.actor }} 60 | password: ${{ secrets.GITHUB_TOKEN }} 61 | 62 | - name: Get short commit SHA 63 | id: var 64 | shell: bash 65 | run: | 66 | echo "sha=$(git rev-parse --short HEAD)" >> $GITHUB_ENV 67 | 68 | - name: Go build 69 | env: 70 | CGO_ENABLED: 0 71 | run: go build -v ./cmd/bot 72 | 73 | - name: Application image 74 | uses: docker/build-push-action@v6 75 | with: 76 | context: . 77 | push: true 78 | tags: "${{ env.IMAGE_TAG }}:${{ env.sha }}" 79 | cache-from: type=gha 80 | cache-to: type=gha,mode=max 81 | 82 | - name: Migrate image 83 | uses: docker/build-push-action@v6 84 | with: 85 | file: migrate.Dockerfile 86 | push: true 87 | tags: "${{ env.IMAGE_TAG }}/migrate:${{ env.sha }}" 88 | cache-from: type=gha 89 | cache-to: type=gha,mode=max 90 | 91 | - name: Generate deployment with SHA version 92 | run: sed 's/:main/:${{ env.sha }}/g' .k8s/deployment.yml > .k8s/deployment.release.yml 93 | 94 | - name: Set up kubectl cache 95 | uses: actions/cache@v4 96 | with: 97 | path: /tmp/kubectl 98 | key: kubectl-${{ env.KUBECTL_VERSION }} 99 | 100 | - name: Check kubectl 101 | id: "kubectl" 102 | uses: andstor/file-existence-action@v3 103 | with: 104 | files: /tmp/kubectl 105 | 106 | - name: Download kubectl 107 | if: steps.kubectl.outputs.files_exists != 'true' 108 | run: | 109 | wget -O /tmp/kubectl "https://dl.k8s.io/release/${{ env.KUBECTL_VERSION }}/bin/linux/amd64/kubectl" 110 | chmod +x /tmp/kubectl 111 | 112 | - name: Setup kubeconfig 113 | env: 114 | KUBE: ${{ secrets.KUBE }} 115 | run: .k8s/kubeconfig.sh 116 | 117 | - name: Deploy 118 | run: | 119 | /tmp/kubectl apply -f .k8s/deployment.release.yml -f .k8s/service.yaml -f .k8s/servicemonitor.yml 120 | /tmp/kubectl -n faster rollout status deployment/bot --timeout=1m 121 | -------------------------------------------------------------------------------- /.github/workflows/x.yml: -------------------------------------------------------------------------------- 1 | name: x 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | # Common Go workflows from go faster 10 | # See https://github.com/go-faster/x 11 | jobs: 12 | test: 13 | uses: go-faster/x/.github/workflows/test.yml@main 14 | with: 15 | enable-386: false 16 | cover: 17 | uses: go-faster/x/.github/workflows/cover.yml@main 18 | lint: 19 | uses: go-faster/x/.github/workflows/lint.yml@main 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /bot 3 | cmd/bot/bot 4 | 5 | _deploy/kubeconfig 6 | 7 | secret.yaml 8 | 9 | kubectl -------------------------------------------------------------------------------- /.k8s/.crd/README.md: -------------------------------------------------------------------------------- 1 | # crd 2 | 3 | Imported CRDs for autocompletion in IDE. 4 | 5 | ``` 6 | kubectl get CustomResourceDefinition ciliumnetworkpolicies.cilium.io -o yaml > cnp.cilium.io.yml 7 | ``` -------------------------------------------------------------------------------- /.k8s/kubeconfig.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "${KUBE}" | base64 -d > /tmp/kubeconfig 4 | -------------------------------------------------------------------------------- /.k8s/service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | namespace: faster 6 | name: bot 7 | labels: 8 | app.kubernetes.io/name: bot 9 | spec: 10 | ports: 11 | - port: 8080 12 | protocol: TCP 13 | targetPort: 8080 14 | name: http-bot 15 | - port: 8081 16 | protocol: TCP 17 | targetPort: 8081 18 | name: http-api 19 | - port: 8090 20 | protocol: TCP 21 | targetPort: 8090 22 | name: metrics 23 | selector: 24 | app.kubernetes.io/name: bot 25 | sessionAffinity: None 26 | --- 27 | apiVersion: networking.k8s.io/v1 28 | kind: Ingress 29 | metadata: 30 | name: bot 31 | namespace: faster 32 | labels: 33 | app.kubernetes.io/name: bot 34 | spec: 35 | ingressClassName: cilium 36 | rules: 37 | - host: bot.go-faster.org 38 | http: 39 | paths: 40 | - path: / 41 | pathType: Prefix 42 | backend: 43 | service: 44 | name: bot 45 | port: 46 | name: http-bot 47 | --- 48 | apiVersion: networking.k8s.io/v1 49 | kind: Ingress 50 | metadata: 51 | name: api 52 | namespace: faster 53 | labels: 54 | app.kubernetes.io/name: bot 55 | spec: 56 | ingressClassName: cilium 57 | rules: 58 | - host: api.go-faster.org 59 | http: 60 | paths: 61 | - path: / 62 | pathType: Prefix 63 | backend: 64 | service: 65 | name: bot 66 | port: 67 | name: http-api 68 | --- 69 | apiVersion: gateway.networking.k8s.io/v1 70 | kind: HTTPRoute 71 | metadata: 72 | name: http-api 73 | namespace: faster 74 | spec: 75 | parentRefs: 76 | - name: gateway 77 | namespace: cloudflare-gateway 78 | hostnames: 79 | - api.go-faster.org 80 | rules: 81 | - backendRefs: 82 | - name: bot 83 | port: 8081 84 | --- 85 | apiVersion: gateway.networking.k8s.io/v1 86 | kind: HTTPRoute 87 | metadata: 88 | name: http-bot 89 | namespace: faster 90 | spec: 91 | parentRefs: 92 | - name: gateway 93 | namespace: cloudflare-gateway 94 | hostnames: 95 | - bot.go-faster.org 96 | rules: 97 | - backendRefs: 98 | - name: bot 99 | port: 8080 100 | -------------------------------------------------------------------------------- /.k8s/servicemonitor.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | name: bot 6 | namespace: faster 7 | spec: 8 | selector: 9 | matchLabels: 10 | app.kubernetes.io/name: bot 11 | endpoints: 12 | - port: metrics 13 | -------------------------------------------------------------------------------- /.ogen.yml: -------------------------------------------------------------------------------- 1 | generator: 2 | features: 3 | enable: 4 | - "ogen/otel" 5 | # Enables example tests generation 6 | - 'debug/example_tests' 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gcr.io/distroless/static 2 | 3 | ADD bot /usr/local/bin/bot 4 | 5 | ENTRYPOINT ["bot"] 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: generate test build 2 | 3 | test: 4 | @./go.test.sh 5 | 6 | coverage: 7 | @./go.coverage.sh 8 | 9 | generate: 10 | go generate 11 | go generate ./... 12 | 13 | build: 14 | CGO_ENABLED=0 go build ./cmd/bot 15 | 16 | check_generated: generate 17 | git diff --exit-code 18 | 19 | forward_psql: 20 | kubectl -n faster port-forward svc/postgresql 15432:5432 21 | 22 | .PHONY: check_generated coverage test generate build forward_psql 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bot 2 | 3 | Bot for go-faster chats and channels, based on [gotd/td](https://github.com/gotd/td). 4 | 5 | ## Skip deploy 6 | 7 | Add `!skip` to commit message. 8 | 9 | ## Migrations 10 | 11 | ### Add migration 12 | 13 | To add migration named `some-migration-name`: 14 | 15 | ```console 16 | atlas migrate --env dev diff some-migration-name 17 | ``` 18 | 19 | ## Golden files 20 | 21 | In package directory: 22 | 23 | ```console 24 | go test -update 25 | ``` -------------------------------------------------------------------------------- /atlas.hcl: -------------------------------------------------------------------------------- 1 | // Define an environment named "local" 2 | env "dev" { 3 | // Define the URL of the Dev Database for this environment 4 | // See: https://atlasgo.io/concepts/dev-database 5 | dev = "docker://postgres/15/test?search_path=public" 6 | 7 | # use at least atlas version v0.10.2-7425aae-canary 8 | src = "ent://internal/ent/schema" 9 | } 10 | -------------------------------------------------------------------------------- /cmd/bot/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/go-faster/bot/internal/cmd" 5 | ) 6 | 7 | func main() { 8 | _ = cmd.Root().Execute() 9 | } 10 | -------------------------------------------------------------------------------- /gen.go: -------------------------------------------------------------------------------- 1 | package gha 2 | 3 | //go:generate go run github.com/ogen-go/ogen/cmd/ogen --target internal/oas -package oas --clean _oas/openapi.yaml 4 | -------------------------------------------------------------------------------- /go.coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | go test -v -coverpkg=./... -coverprofile=profile.out ./... 6 | go tool cover -func profile.out 7 | -------------------------------------------------------------------------------- /go.test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # test with -race 6 | go test --timeout 5m -race ./... 7 | -------------------------------------------------------------------------------- /internal/action/action.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/binary" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/go-faster/errors" 10 | ) 11 | 12 | //go:generate go run github.com/dmarkham/enumer -type Entity -output entity_str_gen.go 13 | 14 | type Entity uint8 15 | 16 | const ( 17 | Unknown Entity = iota 18 | PullRequest 19 | Issue 20 | ) 21 | 22 | //go:generate go run github.com/dmarkham/enumer -type Type -output type_str_gen.go 23 | 24 | type Type uint8 25 | 26 | const ( 27 | UnknownType Type = iota 28 | Merge 29 | Close 30 | ) 31 | 32 | func (a Action) Is(t Type, e Entity) bool { 33 | return a.Type == t && a.Entity == e 34 | } 35 | 36 | type Action struct { 37 | Entity Entity 38 | Type Type 39 | ID int 40 | RepositoryID int64 41 | } 42 | 43 | func (a Action) String() string { 44 | var b strings.Builder 45 | b.WriteString(fmt.Sprintf("%s(%s=%d)", a.Type, a.Entity, a.ID)) 46 | if a.RepositoryID != 0 { 47 | b.WriteString(fmt.Sprintf(" r=%d", a.RepositoryID)) 48 | } 49 | return b.String() 50 | } 51 | 52 | func (a Action) MarshalText() ([]byte, error) { 53 | data, err := a.MarshalBinary() 54 | if err != nil { 55 | return nil, errors.Wrap(err, "marshal") 56 | } 57 | return []byte(base64.RawStdEncoding.EncodeToString(data)), nil 58 | } 59 | 60 | func (a *Action) UnmarshalText(data []byte) error { 61 | b, err := base64.RawStdEncoding.DecodeString(string(data)) 62 | if err != nil { 63 | return errors.Wrap(err, "decode") 64 | } 65 | return a.UnmarshalBinary(b) 66 | } 67 | 68 | const binarySize = 18 69 | 70 | func (a *Action) UnmarshalBinary(data []byte) error { 71 | // Inverse of MarshalBinary. 72 | if len(data) != binarySize { 73 | return errors.New("invalid data length") 74 | } 75 | a.Entity = Entity(data[0]) 76 | a.Type = Type(data[1]) 77 | a.ID = int(binary.BigEndian.Uint64(data[2:10])) 78 | a.RepositoryID = int64(binary.BigEndian.Uint64(data[10:18])) 79 | return nil 80 | } 81 | 82 | func (a Action) MarshalBinary() ([]byte, error) { 83 | var out [binarySize]byte 84 | out[0] = byte(a.Entity) 85 | out[1] = byte(a.Type) 86 | binary.BigEndian.PutUint64(out[2:10], uint64(a.ID)) 87 | binary.BigEndian.PutUint64(out[10:18], uint64(a.RepositoryID)) 88 | return out[:], nil 89 | } 90 | 91 | func Marshal(a Action) []byte { 92 | data, _ := a.MarshalText() 93 | return data 94 | } 95 | -------------------------------------------------------------------------------- /internal/action/action_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestAction(t *testing.T) { 10 | a := Action{ 11 | Entity: PullRequest, 12 | ID: 1, 13 | RepositoryID: 2, 14 | Type: Merge, 15 | } 16 | 17 | data, err := a.MarshalText() 18 | require.NoError(t, err) 19 | require.Less(t, len(data), 50) 20 | t.Logf("data=%s [%s]", data, a.String()) 21 | 22 | for _, buf := range [][]byte{ 23 | data, 24 | Marshal(a), 25 | } { 26 | var out Action 27 | require.NoError(t, out.UnmarshalText(buf)) 28 | require.Equal(t, a, out) 29 | require.True(t, out.Is(Merge, PullRequest)) 30 | require.True(t, out.Is(a.Type, a.Entity)) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/action/entity_str_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by "enumer -type Entity -output entity_str_gen.go"; DO NOT EDIT. 2 | 3 | package action 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | const _EntityName = "UnknownPullRequestIssue" 11 | 12 | var _EntityIndex = [...]uint8{0, 7, 18, 23} 13 | 14 | const _EntityLowerName = "unknownpullrequestissue" 15 | 16 | func (i Entity) String() string { 17 | if i >= Entity(len(_EntityIndex)-1) { 18 | return fmt.Sprintf("Entity(%d)", i) 19 | } 20 | return _EntityName[_EntityIndex[i]:_EntityIndex[i+1]] 21 | } 22 | 23 | // An "invalid array index" compiler error signifies that the constant values have changed. 24 | // Re-run the stringer command to generate them again. 25 | func _EntityNoOp() { 26 | var x [1]struct{} 27 | _ = x[Unknown-(0)] 28 | _ = x[PullRequest-(1)] 29 | _ = x[Issue-(2)] 30 | } 31 | 32 | var _EntityValues = []Entity{Unknown, PullRequest, Issue} 33 | 34 | var _EntityNameToValueMap = map[string]Entity{ 35 | _EntityName[0:7]: Unknown, 36 | _EntityLowerName[0:7]: Unknown, 37 | _EntityName[7:18]: PullRequest, 38 | _EntityLowerName[7:18]: PullRequest, 39 | _EntityName[18:23]: Issue, 40 | _EntityLowerName[18:23]: Issue, 41 | } 42 | 43 | var _EntityNames = []string{ 44 | _EntityName[0:7], 45 | _EntityName[7:18], 46 | _EntityName[18:23], 47 | } 48 | 49 | // EntityString retrieves an enum value from the enum constants string name. 50 | // Throws an error if the param is not part of the enum. 51 | func EntityString(s string) (Entity, error) { 52 | if val, ok := _EntityNameToValueMap[s]; ok { 53 | return val, nil 54 | } 55 | 56 | if val, ok := _EntityNameToValueMap[strings.ToLower(s)]; ok { 57 | return val, nil 58 | } 59 | return 0, fmt.Errorf("%s does not belong to Entity values", s) 60 | } 61 | 62 | // EntityValues returns all values of the enum 63 | func EntityValues() []Entity { 64 | return _EntityValues 65 | } 66 | 67 | // EntityStrings returns a slice of all String values of the enum 68 | func EntityStrings() []string { 69 | strs := make([]string, len(_EntityNames)) 70 | copy(strs, _EntityNames) 71 | return strs 72 | } 73 | 74 | // IsAEntity returns "true" if the value is listed in the enum definition. "false" otherwise 75 | func (i Entity) IsAEntity() bool { 76 | for _, v := range _EntityValues { 77 | if i == v { 78 | return true 79 | } 80 | } 81 | return false 82 | } 83 | -------------------------------------------------------------------------------- /internal/action/type_str_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by "enumer -type Type -output type_str_gen.go"; DO NOT EDIT. 2 | 3 | package action 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | const _TypeName = "UnknownTypeMergeClose" 11 | 12 | var _TypeIndex = [...]uint8{0, 11, 16, 21} 13 | 14 | const _TypeLowerName = "unknowntypemergeclose" 15 | 16 | func (i Type) String() string { 17 | if i >= Type(len(_TypeIndex)-1) { 18 | return fmt.Sprintf("Type(%d)", i) 19 | } 20 | return _TypeName[_TypeIndex[i]:_TypeIndex[i+1]] 21 | } 22 | 23 | // An "invalid array index" compiler error signifies that the constant values have changed. 24 | // Re-run the stringer command to generate them again. 25 | func _TypeNoOp() { 26 | var x [1]struct{} 27 | _ = x[UnknownType-(0)] 28 | _ = x[Merge-(1)] 29 | _ = x[Close-(2)] 30 | } 31 | 32 | var _TypeValues = []Type{UnknownType, Merge, Close} 33 | 34 | var _TypeNameToValueMap = map[string]Type{ 35 | _TypeName[0:11]: UnknownType, 36 | _TypeLowerName[0:11]: UnknownType, 37 | _TypeName[11:16]: Merge, 38 | _TypeLowerName[11:16]: Merge, 39 | _TypeName[16:21]: Close, 40 | _TypeLowerName[16:21]: Close, 41 | } 42 | 43 | var _TypeNames = []string{ 44 | _TypeName[0:11], 45 | _TypeName[11:16], 46 | _TypeName[16:21], 47 | } 48 | 49 | // TypeString retrieves an enum value from the enum constants string name. 50 | // Throws an error if the param is not part of the enum. 51 | func TypeString(s string) (Type, error) { 52 | if val, ok := _TypeNameToValueMap[s]; ok { 53 | return val, nil 54 | } 55 | 56 | if val, ok := _TypeNameToValueMap[strings.ToLower(s)]; ok { 57 | return val, nil 58 | } 59 | return 0, fmt.Errorf("%s does not belong to Type values", s) 60 | } 61 | 62 | // TypeValues returns all values of the enum 63 | func TypeValues() []Type { 64 | return _TypeValues 65 | } 66 | 67 | // TypeStrings returns a slice of all String values of the enum 68 | func TypeStrings() []string { 69 | strs := make([]string, len(_TypeNames)) 70 | copy(strs, _TypeNames) 71 | return strs 72 | } 73 | 74 | // IsAType returns "true" if the value is listed in the enum definition. "false" otherwise 75 | func (i Type) IsAType() bool { 76 | for _, v := range _TypeValues { 77 | if i == v { 78 | return true 79 | } 80 | } 81 | return false 82 | } 83 | -------------------------------------------------------------------------------- /internal/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/errors" 7 | "github.com/gotd/td/telegram" 8 | "github.com/gotd/td/telegram/message/peer" 9 | "github.com/ogen-go/ogen/http" 10 | "go.uber.org/zap" 11 | 12 | "github.com/go-faster/bot/internal/ent" 13 | "github.com/go-faster/bot/internal/oas" 14 | ) 15 | 16 | func NewServer(lg *zap.Logger, db *ent.Client, tg *telegram.Client, resolver peer.Resolver, ht http.Client) *Server { 17 | return &Server{ 18 | lg: lg, 19 | db: db, 20 | tg: tg, 21 | ht: ht, 22 | resolver: resolver, 23 | } 24 | } 25 | 26 | type Server struct { 27 | db *ent.Client 28 | tg *telegram.Client 29 | ht http.Client 30 | resolver peer.Resolver 31 | lg *zap.Logger 32 | } 33 | 34 | func (s Server) GithubStatus(ctx context.Context, req oas.StatusNotification, params oas.GithubStatusParams) error { 35 | if params.Secret.Value == "" { 36 | return errors.New("not authenticated") 37 | } 38 | 39 | switch req.Type { 40 | case oas.StatusNotificationComponentUpdateStatusNotification: 41 | s.lg.Info("Github status: component update") 42 | case oas.StatusNotificationIncidentUpdateStatusNotification: 43 | s.lg.Info("Github status: incident update") 44 | default: 45 | return nil 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func (s Server) NewError(ctx context.Context, err error) *oas.ErrorStatusCode { 52 | return &oas.ErrorStatusCode{ 53 | StatusCode: 500, 54 | Response: oas.Error{ 55 | Message: err.Error(), 56 | }, 57 | } 58 | } 59 | 60 | var _ oas.Handler = (*Server)(nil) 61 | -------------------------------------------------------------------------------- /internal/api/badge.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "hash/crc32" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/go-faster/errors" 15 | "github.com/gotd/td/tg" 16 | "go.uber.org/zap" 17 | 18 | "github.com/go-faster/bot/internal/oas" 19 | ) 20 | 21 | func toBadgeStr(v string) string { 22 | v = strings.ReplaceAll(v, " ", "_") 23 | return v 24 | } 25 | 26 | func generateBadgePath(name, text, style string) string { 27 | return "/badge/" + strings.Join([]string{ 28 | toBadgeStr(name), 29 | toBadgeStr(text), 30 | style, 31 | }, "-") 32 | } 33 | 34 | func etag(name string, data []byte) string { 35 | crc := crc32.ChecksumIEEE(data) 36 | return fmt.Sprintf(`W/"%s-%d-%08X"`, name, len(data), crc) 37 | } 38 | 39 | func (s Server) download(ctx context.Context, u *url.URL) (*oas.SVGHeaders, error) { 40 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), http.NoBody) 41 | if err != nil { 42 | return nil, errors.Wrap(err, "create request") 43 | } 44 | res, err := s.ht.Do(req) 45 | if err != nil { 46 | return nil, errors.Wrap(err, "send request") 47 | } 48 | defer func() { _ = res.Body.Close() }() 49 | data, err := io.ReadAll(res.Body) 50 | if err != nil { 51 | return nil, errors.Wrap(err, "read body") 52 | } 53 | 54 | return &oas.SVGHeaders{ 55 | CacheControl: oas.NewOptString("no-cache"), 56 | ETag: oas.NewOptString(etag("svg", data)), 57 | Response: oas.SVG{ 58 | Data: bytes.NewReader(data), 59 | }, 60 | }, nil 61 | } 62 | 63 | func (s Server) fetchChannel(ctx context.Context, name string) (*tg.ChannelFull, error) { 64 | peer, err := s.resolver.ResolveDomain(ctx, name) 65 | if err != nil { 66 | return nil, errors.Wrap(err, "resolve domain") 67 | } 68 | var inputChannel tg.InputChannel 69 | inputChannel.FillFrom(peer.(*tg.InputPeerChannel)) 70 | full, err := s.tg.API().ChannelsGetFullChannel(ctx, &inputChannel) 71 | if err != nil { 72 | return nil, errors.Wrap(err, "get chat") 73 | } 74 | v := full.FullChat.(*tg.ChannelFull) 75 | s.lg.Info("Got chat", 76 | zap.String("name", name), 77 | zap.Int64("id", v.ID), 78 | zap.Int("participants", v.ParticipantsCount), 79 | zap.Int("online", v.OnlineCount), 80 | ) 81 | return v, nil 82 | } 83 | 84 | func (s Server) GetTelegramOnlineBadge(ctx context.Context, params oas.GetTelegramOnlineBadgeParams) (*oas.SVGHeaders, error) { 85 | var count int 86 | for _, name := range params.Groups { 87 | full, err := s.fetchChannel(ctx, name) 88 | if err != nil { 89 | return nil, errors.Wrap(err, "get chat") 90 | } 91 | count += full.OnlineCount 92 | } 93 | var ( 94 | text = strconv.Itoa(count) 95 | u = &url.URL{ 96 | Scheme: "https", 97 | Host: "img.shields.io", 98 | Path: generateBadgePath("online", text, "green"), 99 | } 100 | ) 101 | { 102 | q := u.Query() 103 | q.Set("logo", "telegram") 104 | u.RawQuery = q.Encode() 105 | } 106 | return s.download(ctx, u) 107 | } 108 | 109 | func (s Server) GetTelegramBadge(ctx context.Context, params oas.GetTelegramBadgeParams) (*oas.SVGHeaders, error) { 110 | channel, err := s.fetchChannel(ctx, params.GroupName) 111 | if err != nil { 112 | return nil, errors.Wrap(err, "get chat") 113 | } 114 | var ( 115 | title = params.Title.Or(params.GroupName) 116 | text = strconv.Itoa(channel.ParticipantsCount) 117 | u = &url.URL{ 118 | Scheme: "https", 119 | Host: "img.shields.io", 120 | Path: generateBadgePath(title, text, "179cde"), 121 | } 122 | ) 123 | { 124 | q := u.Query() 125 | q.Set("logo", "telegram") 126 | u.RawQuery = q.Encode() 127 | } 128 | return s.download(ctx, u) 129 | } 130 | -------------------------------------------------------------------------------- /internal/api/status.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/go-faster/errors" 8 | 9 | "github.com/go-faster/bot/internal/ent/gitcommit" 10 | "github.com/go-faster/bot/internal/oas" 11 | ) 12 | 13 | func (s *Server) Status(ctx context.Context) (*oas.Status, error) { 14 | // Last week. 15 | until := time.Now().AddDate(0, 0, -7) 16 | totalCommits, err := s.db.GitCommit.Query(). 17 | Where(gitcommit.DateGTE(until)). 18 | Count(ctx) 19 | if err != nil { 20 | return nil, errors.Wrap(err, "count commits") 21 | } 22 | return &oas.Status{ 23 | Message: "Weekly stats:", 24 | Stat: oas.Statistics{ 25 | TotalCommits: totalCommits, 26 | }, 27 | }, nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/app/file_id.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/errors" 7 | "github.com/gotd/td/fileid" 8 | "github.com/gotd/td/tg" 9 | 10 | "github.com/go-faster/bot/internal/botapi" 11 | ) 12 | 13 | // checkOurFileID generates file_id and tries to use it in BotAPI. 14 | func (m Middleware) checkOurFileID(ctx context.Context, id fileid.FileID) error { 15 | if m.client == nil { 16 | return nil 17 | } 18 | 19 | encoded, err := fileid.EncodeFileID(id) 20 | if err != nil { 21 | return errors.Wrap(err, "encode") 22 | } 23 | 24 | if err := m.client.GetFile(ctx, encoded); err != nil { 25 | return errors.Wrap(err, "check file_id") 26 | } 27 | 28 | return nil 29 | } 30 | 31 | // tryGetFileID decodes file_id from BotAPI and tries to map into Telegram API file location. 32 | func (m Middleware) tryGetFileID(ctx context.Context, msgID int) (tg.InputFileLocationClass, string, error) { 33 | botAPIMsg, err := m.client.GetBotAPIMessage(ctx, msgID) 34 | if err != nil { 35 | return nil, "", errors.Wrap(err, "get message") 36 | } 37 | 38 | encoded, ok := botapi.GetFileIDFromMessage(botAPIMsg) 39 | if !ok { 40 | return nil, "", errors.New("no media in message") 41 | } 42 | 43 | fileID, err := fileid.DecodeFileID(encoded) 44 | if err != nil { 45 | return nil, encoded, errors.Wrap(err, "decode file_id") 46 | } 47 | 48 | loc, ok := fileID.AsInputFileLocation() 49 | if !ok { 50 | return nil, encoded, errors.New("can't map to location") 51 | } 52 | 53 | return loc, encoded, nil 54 | } 55 | -------------------------------------------------------------------------------- /internal/app/middleware_options.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | 6 | "github.com/go-faster/bot/internal/botapi" 7 | ) 8 | 9 | // MiddlewareOptions is middleware options. 10 | type MiddlewareOptions struct { 11 | BotAPI *botapi.Client 12 | Logger *zap.Logger 13 | } 14 | 15 | func (m *MiddlewareOptions) setDefaults() { 16 | if m.Logger == nil { 17 | m.Logger = zap.NewNop() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/app/stats.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/go-faster/sdk/app" 9 | "github.com/gotd/contrib/oteltg" 10 | "github.com/gotd/td/tg" 11 | 12 | "github.com/go-faster/bot/internal/dispatch" 13 | ) 14 | 15 | // Handler implements stats request handler. 16 | type Handler struct { 17 | Middleware *oteltg.Middleware 18 | 19 | m *app.Telemetry 20 | } 21 | 22 | func NewHandler(m *app.Telemetry) Handler { 23 | return Handler{m: m} 24 | } 25 | 26 | func (h Handler) stats() string { 27 | var w strings.Builder 28 | fmt.Fprintf(&w, "Statistics:\n\n") 29 | fmt.Fprintln(&w, "TL Layer version:", tg.Layer) 30 | if v := GetGotdVersion(); v != "" { 31 | fmt.Fprintln(&w, "Version:", v) 32 | } 33 | 34 | return w.String() 35 | } 36 | 37 | // OnMessage implements dispatch.MessageHandler. 38 | func (h Handler) OnMessage(ctx context.Context, e dispatch.MessageEvent) error { 39 | _, err := e.Reply().Text(ctx, h.stats()) 40 | return err 41 | } 42 | -------------------------------------------------------------------------------- /internal/app/version.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "runtime/debug" 5 | "strings" 6 | ) 7 | 8 | // GetGotdVersion optimistically gets current gotd version. 9 | // 10 | // Does not handle replace directives. 11 | func GetGotdVersion() string { 12 | info, ok := debug.ReadBuildInfo() 13 | if !ok { 14 | return "" 15 | } 16 | const pkg = "github.com/gotd/td" 17 | for _, d := range info.Deps { 18 | if strings.HasPrefix(d.Path, pkg) { 19 | return d.Version 20 | } 21 | } 22 | return "" 23 | } 24 | -------------------------------------------------------------------------------- /internal/botapi/client.go: -------------------------------------------------------------------------------- 1 | package botapi 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | 10 | "github.com/go-faster/errors" 11 | "go.uber.org/multierr" 12 | ) 13 | 14 | // Client is simplified Telegram BotAPI client. 15 | type Client struct { 16 | httpClient *http.Client 17 | token string 18 | } 19 | 20 | // NewClient creates new Client. 21 | func NewClient(token string, opts Options) *Client { 22 | opts.setDefaults() 23 | return &Client{ 24 | httpClient: opts.HTTPClient, 25 | token: token, 26 | } 27 | } 28 | 29 | func (m *Client) sendBotAPI(ctx context.Context, u string, result interface{}) (rErr error) { 30 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, http.NoBody) 31 | if err != nil { 32 | return errors.Wrap(err, "create request") 33 | } 34 | 35 | resp, err := m.httpClient.Do(req) 36 | if err != nil { 37 | return errors.Wrap(err, "send request") 38 | } 39 | defer multierr.AppendInvoke(&rErr, multierr.Close(resp.Body)) 40 | 41 | if err := json.NewDecoder(resp.Body).Decode(result); err != nil { 42 | return errors.Wrap(err, "decode json") 43 | } 44 | 45 | return nil 46 | } 47 | 48 | // GetFile sends getFile request to BotAPI. 49 | func (m *Client) GetFile(ctx context.Context, id string) (rErr error) { 50 | u := fmt.Sprintf("https://api.telegram.org/bot%s/getFile?file_id=%s", m.token, url.QueryEscape(id)) 51 | var result struct { 52 | OK bool `json:"ok"` 53 | ErrorCode int `json:"error_code"` 54 | Description string `json:"description"` 55 | } 56 | if err := m.sendBotAPI(ctx, u, &result); err != nil { 57 | return errors.Wrap(err, "send") 58 | } 59 | if !result.OK { 60 | return errors.Errorf("API error %d: %s", result.ErrorCode, result.Description) 61 | } 62 | 63 | return nil 64 | } 65 | 66 | type Update struct { 67 | UpdateID int `json:"update_id"` 68 | Message Message `json:"message"` 69 | } 70 | 71 | // GetBotAPIMessage sends getUpdates request to BotAPI and finds message by msg_id. 72 | // 73 | // NB: it can find only recently received messages. 74 | func (m *Client) GetBotAPIMessage(ctx context.Context, msgID int) (Message, error) { 75 | u := fmt.Sprintf(`https://api.telegram.org/bot%s/getUpdates?allowed_updates="message"`, m.token) 76 | var resp struct { 77 | OK bool `json:"ok"` 78 | ErrorCode int `json:"error_code"` 79 | Description string `json:"description"` 80 | Result []Update `json:"result"` 81 | } 82 | if err := m.sendBotAPI(ctx, u, &resp); err != nil { 83 | return Message{}, errors.Wrap(err, "send") 84 | } 85 | if !resp.OK { 86 | return Message{}, errors.Errorf("API error %d: %s", resp.ErrorCode, resp.Description) 87 | } 88 | 89 | for _, update := range resp.Result { 90 | if update.Message.MessageID == msgID { 91 | return update.Message, nil 92 | } 93 | } 94 | 95 | return Message{}, errors.Errorf("message %d not found", msgID) 96 | } 97 | 98 | // GetFileIDFromMessage finds file_id in message attachments. 99 | func GetFileIDFromMessage(msg Message) (string, bool) { 100 | if len(msg.Photo) > 0 { 101 | return msg.Photo[0].FileID, true 102 | } 103 | switch { 104 | case msg.Animation.FileID != "": 105 | return msg.Animation.FileID, true 106 | case msg.Audio.FileID != "": 107 | return msg.Audio.FileID, true 108 | case msg.Document.FileID != "": 109 | return msg.Document.FileID, true 110 | case msg.Sticker.FileID != "": 111 | return msg.Sticker.FileID, true 112 | case msg.Video.FileID != "": 113 | return msg.Video.FileID, true 114 | case msg.VideoNote.FileID != "": 115 | return msg.VideoNote.FileID, true 116 | case msg.Voice.FileID != "": 117 | return msg.Voice.FileID, true 118 | } 119 | 120 | return "", false 121 | } 122 | -------------------------------------------------------------------------------- /internal/botapi/options.go: -------------------------------------------------------------------------------- 1 | package botapi 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // Options is Client options. 8 | type Options struct { 9 | HTTPClient *http.Client 10 | } 11 | 12 | func (m *Options) setDefaults() { 13 | if m.HTTPClient == nil { 14 | m.HTTPClient = http.DefaultClient 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internal/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/go-faster/bot/internal/cmd/internal/job" 7 | "github.com/go-faster/bot/internal/cmd/internal/server" 8 | ) 9 | 10 | func Root() *cobra.Command { 11 | cmd := &cobra.Command{ 12 | Use: "bot", 13 | Short: "The go-faster automation around GitHub and Telegram", 14 | } 15 | cmd.AddCommand( 16 | job.Root(), 17 | server.Root(), 18 | ) 19 | return cmd 20 | } 21 | -------------------------------------------------------------------------------- /internal/cmd/internal/job/cmd.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/go-faster/bot/internal/cmd/internal/job/commits" 7 | ) 8 | 9 | func Root() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "job", 12 | Short: "Run a job", 13 | } 14 | cmd.AddCommand( 15 | commits.Root(), 16 | ) 17 | return cmd 18 | } 19 | -------------------------------------------------------------------------------- /internal/cmd/internal/job/commits/cmd.go: -------------------------------------------------------------------------------- 1 | package commits 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "net/http" 7 | "os" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/bradleyfalzon/ghinstallation/v2" 12 | "github.com/go-faster/errors" 13 | "github.com/google/go-github/v52/github" 14 | "github.com/redis/go-redis/v9" 15 | "github.com/spf13/cobra" 16 | "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 17 | "go.uber.org/zap" 18 | 19 | "github.com/go-faster/sdk/app" 20 | 21 | "github.com/go-faster/bot/internal/entdb" 22 | "github.com/go-faster/bot/internal/otelredis" 23 | "github.com/go-faster/bot/internal/stat" 24 | ) 25 | 26 | func setupGithubInstallation(httpTransport http.RoundTripper) (*github.Client, error) { 27 | ghAppID, err := strconv.ParseInt(os.Getenv("GITHUB_APP_ID"), 10, 64) 28 | if err != nil { 29 | return nil, errors.Wrap(err, "GITHUB_APP_ID is invalid") 30 | } 31 | key, err := base64.StdEncoding.DecodeString(os.Getenv("GITHUB_PRIVATE_KEY")) 32 | if err != nil { 33 | return nil, errors.Wrap(err, "GITHUB_PRIVATE_KEY is invalid") 34 | } 35 | ghTransport, err := ghinstallation.NewAppsTransport(httpTransport, ghAppID, key) 36 | if err != nil { 37 | return nil, errors.Wrap(err, "create ghInstallation transport") 38 | } 39 | return github.NewClient(&http.Client{ 40 | Transport: ghTransport, 41 | }), nil 42 | } 43 | 44 | func Root() *cobra.Command { 45 | cmd := &cobra.Command{ 46 | Use: "commits", 47 | Short: "Gather commit information and save to database", 48 | Run: func(cmd *cobra.Command, args []string) { 49 | app.Run(func(ctx context.Context, logger *zap.Logger, m *app.Telemetry) error { 50 | start := time.Now() 51 | 52 | tracer := m.TracerProvider().Tracer("command") 53 | 54 | ctx, span := tracer.Start(ctx, "job.commits") 55 | defer span.End() 56 | 57 | httpTransport := otelhttp.NewTransport(http.DefaultTransport, 58 | otelhttp.WithTracerProvider(m.TracerProvider()), 59 | otelhttp.WithMeterProvider(m.MeterProvider()), 60 | ) 61 | 62 | r := redis.NewClient(&redis.Options{ 63 | Addr: "redis:6379", 64 | }) 65 | r.AddHook(otelredis.NewHook(m.TracerProvider())) 66 | 67 | ghInstallationClient, err := setupGithubInstallation(httpTransport) 68 | if err != nil { 69 | return errors.Wrap(err, "setup github installation") 70 | } 71 | ghInstallationID, err := strconv.ParseInt(os.Getenv("GITHUB_INSTALLATION_ID"), 10, 64) 72 | if err != nil { 73 | return errors.Wrap(err, "GITHUB_INSTALLATION_ID") 74 | } 75 | db, err := entdb.Open(os.Getenv("DATABASE_URL")) 76 | if err != nil { 77 | return errors.Wrap(err, "open database") 78 | } 79 | 80 | c := stat.NewCommit(db, r, ghInstallationClient, ghInstallationID, m.MeterProvider(), m.TracerProvider()) 81 | if err := c.Update(ctx); err != nil { 82 | return errors.Wrap(err, "update commits") 83 | } 84 | 85 | logger.Info("Done", 86 | zap.Duration("duration", time.Since(start)), 87 | zap.Stringer("duration_human", time.Since(start)), 88 | ) 89 | 90 | return nil 91 | }) 92 | }, 93 | } 94 | return cmd 95 | } 96 | -------------------------------------------------------------------------------- /internal/cmd/internal/server/bot.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/errors" 7 | 8 | "github.com/go-faster/bot/internal/app" 9 | "github.com/go-faster/bot/internal/dispatch" 10 | "github.com/go-faster/bot/internal/gpt" 11 | ) 12 | 13 | func setupBot(a *App) error { 14 | a.mux.HandleFunc("/bot", "Ping bot", func(ctx context.Context, e dispatch.MessageEvent) error { 15 | _, err := e.Reply().Text(ctx, "What?") 16 | return err 17 | }) 18 | a.mux.Handle("/stat", "Metrics and version", app.NewHandler(a.m)) 19 | a.mux.HandleFunc("/events", "GitHub events", a.HandleEvents) 20 | a.mux.HandleFunc("/gh_pat", "Set GitHub personal token", a.HandleGitHubPersonalToken) 21 | a.mux.HandleFunc("/gh_test", "GitHub test", a.HandleGitHubTest) 22 | { 23 | var limitCfg gpt.LimitConfig 24 | if err := limitCfg.ParseEnv(); err != nil { 25 | return errors.Wrap(err, "parse GPT limit config") 26 | } 27 | hgpt := gpt.New(a.openai, a.db, a.m.TracerProvider()). 28 | WithLimitConfig(limitCfg) 29 | a.mux.HandleFunc("/gpt", "ChatGPT", hgpt.OnCommand) 30 | a.mux.SetFallbackFunc(hgpt.OnReply) 31 | } 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /internal/cmd/internal/server/button.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/go-faster/errors" 8 | "github.com/go-faster/sdk/zctx" 9 | "github.com/google/go-github/v52/github" 10 | "github.com/gotd/td/tg" 11 | "go.opentelemetry.io/otel/attribute" 12 | "go.opentelemetry.io/otel/codes" 13 | "go.uber.org/multierr" 14 | "go.uber.org/zap" 15 | 16 | "github.com/go-faster/bot/internal/action" 17 | "github.com/go-faster/bot/internal/dispatch" 18 | "github.com/go-faster/bot/internal/ent/user" 19 | ) 20 | 21 | func (a *App) OnButton(ctx context.Context, e dispatch.Button) (rerr error) { 22 | ctx, span := a.tracer.Start(ctx, "OnBotCallbackQuery") 23 | defer span.End() 24 | if e.Input == nil { 25 | span.SetStatus(codes.Ok, "Ignored") 26 | zctx.From(ctx).Info("OnButton: no user") 27 | return nil 28 | } 29 | zctx.From(ctx).Info("OnButton", 30 | zap.String("user", e.Input.String()), 31 | ) 32 | rpc := e.RPC() 33 | defer func() { 34 | if rerr != nil { 35 | span.SetStatus(codes.Error, rerr.Error()) 36 | answer := &tg.MessagesSetBotCallbackAnswerRequest{ 37 | QueryID: e.QueryID, 38 | Message: fmt.Sprintf("Error: %s", rerr), 39 | Alert: true, 40 | } 41 | if _, err := rpc.MessagesSetBotCallbackAnswer(ctx, answer); err != nil { 42 | rerr = multierr.Append(rerr, err) 43 | } 44 | } 45 | }() 46 | 47 | var act action.Action 48 | if err := act.UnmarshalText(e.Data); err != nil { 49 | return errors.Wrap(err, "unmarshal") 50 | } 51 | span.SetAttributes( 52 | attribute.Int("action.id", act.ID), 53 | attribute.Int64("action.repository_id", act.RepositoryID), 54 | attribute.Stringer("action.entity", act.Entity), 55 | attribute.Stringer("action.type", act.Type), 56 | ) 57 | 58 | var token string 59 | { 60 | users, err := a.db.User.Query().Where( 61 | user.ID(e.User.ID), 62 | ).All(ctx) 63 | if err != nil { 64 | return errors.Wrap(err, "query user") 65 | } 66 | for _, u := range users { 67 | if u.GithubToken != "" { 68 | token = u.GithubToken 69 | break 70 | } 71 | } 72 | } 73 | if token == "" { 74 | return errors.New("no PAT token found for user") 75 | } 76 | 77 | switch { 78 | case act.Is(action.Merge, action.PullRequest): 79 | api := a.clientWithToken(ctx, token) 80 | repo, _, err := api.Repositories.GetByID(ctx, act.RepositoryID) 81 | if err != nil { 82 | return errors.Wrap(err, "get repo") 83 | } 84 | var ( 85 | owner = repo.GetOwner().GetLogin() 86 | repoName = repo.GetName() 87 | message = "" // use default message 88 | options = &github.PullRequestOptions{ 89 | MergeMethod: "merge", 90 | } 91 | ) 92 | if _, _, err := api.PullRequests.Merge(ctx, owner, repoName, act.ID, message, options); err != nil { 93 | return errors.Wrap(err, "merge") 94 | } 95 | if _, err := rpc.MessagesSetBotCallbackAnswer(ctx, &tg.MessagesSetBotCallbackAnswerRequest{ 96 | QueryID: e.QueryID, 97 | Message: "Pull request merged", 98 | }); err != nil { 99 | return errors.Wrap(err, "answer") 100 | } 101 | return nil 102 | default: 103 | return errors.Errorf("unknown action %q(%q)", act.Type, act.Entity) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /internal/cmd/internal/server/cmd.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | "go.uber.org/zap" 8 | 9 | "github.com/go-faster/sdk/app" 10 | ) 11 | 12 | func Root() *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "server", 15 | Short: "Run a go-faster bot server", 16 | Run: func(cmd *cobra.Command, args []string) { 17 | app.Run(func(ctx context.Context, lg *zap.Logger, t *app.Telemetry) error { 18 | return runBot(ctx, t, lg.Named("bot")) 19 | }) 20 | }, 21 | } 22 | return cmd 23 | } 24 | -------------------------------------------------------------------------------- /internal/cmd/internal/server/gh_pat.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "entgo.io/ent/dialect/sql" 8 | "github.com/go-faster/errors" 9 | 10 | "github.com/go-faster/bot/internal/dispatch" 11 | ) 12 | 13 | func (a *App) HandleGitHubPersonalToken(ctx context.Context, e dispatch.MessageEvent) error { 14 | u, ok := e.User() 15 | if !ok { 16 | if _, err := e.Reply().Text(ctx, "Only for users"); err != nil { 17 | return errors.Wrap(err, "reply") 18 | } 19 | return nil 20 | } 21 | 22 | tok := e.Message.Message 23 | tok = strings.TrimPrefix(tok, "/gh_pat") 24 | tok = strings.TrimSpace(tok) 25 | 26 | if len(tok) == 0 { 27 | if _, err := e.Reply().Text(ctx, "Please, provide GitHub personal token"); err != nil { 28 | return errors.Wrap(err, "reply") 29 | } 30 | return nil 31 | } 32 | 33 | if err := a.db.User.Create(). 34 | SetID(u.ID). 35 | SetUsername(u.Username). 36 | SetFirstName(u.FirstName). 37 | SetLastName(u.LastName). 38 | SetGithubToken(tok).OnConflict( 39 | sql.ConflictColumns("id"), 40 | sql.ResolveWithNewValues(), 41 | ).UpdateGithubToken().Exec(ctx); err != nil { 42 | if _, err := e.Reply().Text(ctx, "500: Failed\nSorry, internal server error."); err != nil { 43 | return errors.Wrap(err, "reply") 44 | } 45 | } 46 | 47 | if _, err := e.Reply().Text(ctx, "✔️ Token set up"); err != nil { 48 | return errors.Wrap(err, "reply") 49 | } 50 | 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /internal/cmd/internal/server/ghtest.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/bot/internal/dispatch" 7 | ) 8 | 9 | func (a *App) HandleGitHubTest(ctx context.Context, e dispatch.MessageEvent) error { 10 | client, err := a.wh.Client(ctx) 11 | if err != nil { 12 | if _, err := e.Reply().Textf(ctx, "Error: %v", err); err != nil { 13 | return err 14 | } 15 | return nil 16 | } 17 | repo, _, err := client.Repositories.Get(ctx, "go-faster", "bot") 18 | if err != nil { 19 | if _, err := e.Reply().Textf(ctx, "Error: %v", err); err != nil { 20 | return err 21 | } 22 | } 23 | if _, err := e.Reply().Textf(ctx, "Repo id: %d", repo.GetID()); err != nil { 24 | return err 25 | } 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /internal/cmd/internal/server/github.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "net/http" 7 | "os" 8 | "strconv" 9 | 10 | "github.com/bradleyfalzon/ghinstallation/v2" 11 | "github.com/go-faster/errors" 12 | "github.com/google/go-github/v52/github" 13 | "golang.org/x/oauth2" 14 | ) 15 | 16 | func setupGithubInstallation(httpTransport http.RoundTripper) (*github.Client, error) { 17 | ghAppID, err := strconv.ParseInt(os.Getenv("GITHUB_APP_ID"), 10, 64) 18 | if err != nil { 19 | return nil, errors.Wrap(err, "GITHUB_APP_ID is invalid") 20 | } 21 | key, err := base64.StdEncoding.DecodeString(os.Getenv("GITHUB_PRIVATE_KEY")) 22 | if err != nil { 23 | return nil, errors.Wrap(err, "GITHUB_PRIVATE_KEY is invalid") 24 | } 25 | ghTransport, err := ghinstallation.NewAppsTransport(httpTransport, ghAppID, key) 26 | if err != nil { 27 | return nil, errors.Wrap(err, "create ghInstallation transport") 28 | } 29 | return github.NewClient(&http.Client{ 30 | Transport: ghTransport, 31 | }), nil 32 | } 33 | 34 | func (a *App) clientWithToken(ctx context.Context, token string) *github.Client { 35 | ts := oauth2.StaticTokenSource( 36 | &oauth2.Token{AccessToken: token}, 37 | ) 38 | tc := oauth2.NewClient(ctx, ts) 39 | return github.NewClient(tc) 40 | } 41 | -------------------------------------------------------------------------------- /internal/dispatch/base_event.go: -------------------------------------------------------------------------------- 1 | package dispatch 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/go-faster/errors" 8 | "github.com/go-faster/sdk/zctx" 9 | "go.uber.org/zap" 10 | 11 | "github.com/gotd/td/telegram/message" 12 | "github.com/gotd/td/tg" 13 | ) 14 | 15 | func (b *Bot) baseEvent(ctx context.Context) baseEvent { 16 | return baseEvent{ 17 | sender: b.sender, 18 | lg: zctx.From(ctx), 19 | rpc: b.rpc, 20 | rand: b.rand, 21 | } 22 | } 23 | 24 | type baseEvent struct { 25 | sender *message.Sender 26 | lg *zap.Logger 27 | rpc *tg.Client 28 | rand io.Reader 29 | } 30 | 31 | // Logger returns associated lg. 32 | func (e baseEvent) Logger() *zap.Logger { 33 | return e.lg 34 | } 35 | 36 | // RPC returns Telegram RPC client. 37 | func (e baseEvent) RPC() *tg.Client { 38 | return e.rpc 39 | } 40 | 41 | // Sender returns *message.Sender 42 | func (e baseEvent) Sender() *message.Sender { 43 | return e.sender 44 | } 45 | 46 | func findMessage(r tg.MessagesMessagesClass, msgID int) (*tg.Message, error) { 47 | slice, ok := r.(interface{ GetMessages() []tg.MessageClass }) 48 | if !ok { 49 | return nil, errors.Errorf("unexpected type %T", r) 50 | } 51 | 52 | msgs := slice.GetMessages() 53 | for _, m := range msgs { 54 | msg, ok := m.(*tg.Message) 55 | if !ok || msg.ID != msgID { 56 | continue 57 | } 58 | 59 | return msg, nil 60 | } 61 | 62 | return nil, errors.Errorf("message %d not found in response %+v", msgID, msgs) 63 | } 64 | 65 | func (e baseEvent) getMessage(ctx context.Context, msgID int) (*tg.Message, error) { 66 | r, err := e.rpc.MessagesGetMessages(ctx, []tg.InputMessageClass{&tg.InputMessageID{ID: msgID}}) 67 | if err != nil { 68 | return nil, errors.Wrap(err, "get message") 69 | } 70 | 71 | return findMessage(r, msgID) 72 | } 73 | 74 | func (e baseEvent) getChannelMessage(ctx context.Context, channel *tg.InputChannel, msgID int) (*tg.Message, error) { 75 | r, err := e.rpc.ChannelsGetMessages(ctx, &tg.ChannelsGetMessagesRequest{ 76 | Channel: channel, 77 | ID: []tg.InputMessageClass{&tg.InputMessageID{ID: msgID}}, 78 | }) 79 | if err != nil { 80 | return nil, errors.Wrap(err, "get message") 81 | } 82 | 83 | return findMessage(r, msgID) 84 | } 85 | -------------------------------------------------------------------------------- /internal/dispatch/bot.go: -------------------------------------------------------------------------------- 1 | package dispatch 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "io" 7 | 8 | "github.com/google/go-github/v52/github" 9 | "github.com/gotd/td/telegram/downloader" 10 | "github.com/gotd/td/telegram/message" 11 | "github.com/gotd/td/tg" 12 | "go.opentelemetry.io/otel/trace" 13 | "go.opentelemetry.io/otel/trace/noop" 14 | ) 15 | 16 | // Bot represents generic Telegram bot state and event dispatcher. 17 | type Bot struct { 18 | onMessage MessageHandler 19 | onInline InlineHandler 20 | onButton ButtonHandler 21 | 22 | rpc *tg.Client 23 | sender *message.Sender 24 | downloader *downloader.Downloader 25 | github *github.Client 26 | 27 | rand io.Reader 28 | tracer trace.Tracer 29 | } 30 | 31 | const botInstrumentationName = "bot" 32 | 33 | // NewBot creates new bot. 34 | func NewBot(raw *tg.Client) *Bot { 35 | return &Bot{ 36 | onMessage: MessageHandlerFunc(func(context.Context, MessageEvent) error { 37 | return nil 38 | }), 39 | onInline: InlineHandlerFunc(func(context.Context, InlineQuery) error { 40 | return nil 41 | }), 42 | onButton: ButtonHandlerFunc(func(context.Context, Button) error { 43 | return nil 44 | }), 45 | rpc: raw, 46 | sender: message.NewSender(raw), 47 | downloader: downloader.NewDownloader(), 48 | rand: rand.Reader, 49 | tracer: noop.NewTracerProvider().Tracer(botInstrumentationName), 50 | } 51 | } 52 | 53 | // OnMessage sets message handler. 54 | func (b *Bot) OnMessage(handler MessageHandler) *Bot { 55 | b.onMessage = handler 56 | return b 57 | } 58 | 59 | func (b *Bot) WithGitHub(client *github.Client) *Bot { 60 | b.github = client 61 | return b 62 | } 63 | 64 | // OnInline sets inline query handler. 65 | func (b *Bot) OnInline(handler InlineHandler) *Bot { 66 | b.onInline = handler 67 | return b 68 | } 69 | 70 | // OnButton sets button handler. 71 | func (b *Bot) OnButton(handler ButtonHandler) *Bot { 72 | b.onButton = handler 73 | return b 74 | } 75 | 76 | // WithSender sets message sender to use. 77 | func (b *Bot) WithSender(sender *message.Sender) *Bot { 78 | b.sender = sender 79 | return b 80 | } 81 | 82 | func (b *Bot) WithTracerProvider(provider trace.TracerProvider) *Bot { 83 | b.tracer = provider.Tracer(botInstrumentationName) 84 | return b 85 | } 86 | 87 | // Register sets handlers using given dispatcher. 88 | func (b *Bot) Register(dispatcher tg.UpdateDispatcher) *Bot { 89 | dispatcher.OnNewMessage(b.OnNewMessage) 90 | dispatcher.OnNewChannelMessage(b.OnNewChannelMessage) 91 | dispatcher.OnBotInlineQuery(b.OnBotInlineQuery) 92 | dispatcher.OnBotCallbackQuery(b.OnBotCallbackQuery) 93 | return b 94 | } 95 | -------------------------------------------------------------------------------- /internal/dispatch/button.go: -------------------------------------------------------------------------------- 1 | package dispatch 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gotd/td/tg" 7 | ) 8 | 9 | type Button struct { 10 | QueryID int64 11 | Input *tg.InputUser 12 | Data []byte 13 | 14 | User *tg.User 15 | baseEvent 16 | } 17 | 18 | type ButtonHandler interface { 19 | OnButton(ctx context.Context, e Button) error 20 | } 21 | 22 | // ButtonHandlerFunc is a functional adapter for Handler. 23 | type ButtonHandlerFunc func(ctx context.Context, e Button) error 24 | 25 | // OnButton implements ButtonHandler. 26 | func (h ButtonHandlerFunc) OnButton(ctx context.Context, e Button) error { 27 | return h(ctx, e) 28 | } 29 | -------------------------------------------------------------------------------- /internal/dispatch/handle_button.go: -------------------------------------------------------------------------------- 1 | package dispatch 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/errors" 7 | "github.com/go-faster/sdk/zctx" 8 | "github.com/gotd/td/tg" 9 | "go.opentelemetry.io/otel/attribute" 10 | ) 11 | 12 | func (b *Bot) OnBotCallbackQuery(ctx context.Context, e tg.Entities, u *tg.UpdateBotCallbackQuery) error { 13 | ctx, span := b.tracer.Start(ctx, "OnBotCallbackQuery") 14 | defer span.End() 15 | 16 | zctx.From(ctx).Info("Got callback query") 17 | 18 | user, ok := e.Users[u.UserID] 19 | if !ok { 20 | return errors.Errorf("unknown user ID %d", u.UserID) 21 | } 22 | 23 | span.SetAttributes( 24 | attribute.Int64("user.id", user.ID), 25 | attribute.String("user.username", user.Username), 26 | attribute.String("user.first_name", user.FirstName), 27 | attribute.String("user.last_name", user.LastName), 28 | ) 29 | 30 | if err := b.onButton.OnButton(ctx, Button{ 31 | QueryID: u.QueryID, 32 | Input: user.AsInput(), 33 | Data: u.Data, 34 | User: user, 35 | 36 | baseEvent: b.baseEvent(ctx), 37 | }); err != nil { 38 | return errors.Wrap(err, "handle onButton") 39 | } 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /internal/dispatch/handle_inline.go: -------------------------------------------------------------------------------- 1 | package dispatch 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/errors" 7 | "github.com/go-faster/sdk/zctx" 8 | "go.uber.org/zap" 9 | 10 | "github.com/gotd/td/tg" 11 | ) 12 | 13 | func (b *Bot) OnBotInlineQuery(ctx context.Context, e tg.Entities, u *tg.UpdateBotInlineQuery) error { 14 | ctx, span := b.tracer.Start(ctx, "OnBotInlineQuery") 15 | defer span.End() 16 | 17 | zctx.From(ctx).Info("Got inline query", 18 | zap.String("query", u.Query), 19 | zap.String("offset", u.Offset), 20 | ) 21 | 22 | user, ok := e.Users[u.UserID] 23 | if !ok { 24 | return errors.Errorf("unknown user ID %d", u.UserID) 25 | } 26 | 27 | var geo *tg.GeoPoint 28 | if u.Geo != nil { 29 | geo, _ = u.Geo.AsNotEmpty() 30 | } 31 | if err := b.onInline.OnInline(ctx, InlineQuery{ 32 | QueryID: u.QueryID, 33 | Query: u.Query, 34 | Offset: u.Offset, 35 | Enquirer: user.AsInput(), 36 | geo: geo, 37 | user: user, 38 | baseEvent: b.baseEvent(ctx), 39 | }); err != nil { 40 | return errors.Wrap(err, "handle inline") 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/dispatch/inline.go: -------------------------------------------------------------------------------- 1 | package dispatch 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gotd/td/telegram/message/inline" 7 | "github.com/gotd/td/tg" 8 | ) 9 | 10 | // InlineQuery represents inline query event. 11 | type InlineQuery struct { 12 | QueryID int64 13 | Query string 14 | Offset string 15 | Enquirer *tg.InputUser 16 | 17 | geo *tg.GeoPoint 18 | user *tg.User 19 | 20 | baseEvent 21 | } 22 | 23 | // Reply returns result builder. 24 | func (e InlineQuery) Reply() *inline.ResultBuilder { 25 | return inline.New(e.rpc, e.rand, e.QueryID) 26 | } 27 | 28 | // User returns User object if available. 29 | // False and nil otherwise. 30 | func (e InlineQuery) User() (*tg.User, bool) { 31 | return e.user, e.user != nil 32 | } 33 | 34 | // Geo returns GeoPoint object and true if query has attached geo point. 35 | // False and nil otherwise. 36 | func (e InlineQuery) Geo() (*tg.GeoPoint, bool) { 37 | return e.geo, e.geo != nil 38 | } 39 | 40 | // InlineHandler is a simple inline query event handler. 41 | type InlineHandler interface { 42 | OnInline(ctx context.Context, e InlineQuery) error 43 | } 44 | 45 | // InlineHandlerFunc is a functional adapter for Handler. 46 | type InlineHandlerFunc func(ctx context.Context, e InlineQuery) error 47 | 48 | // OnInline implements InlineHandler. 49 | func (h InlineHandlerFunc) OnInline(ctx context.Context, e InlineQuery) error { 50 | return h(ctx, e) 51 | } 52 | -------------------------------------------------------------------------------- /internal/dispatch/logged.go: -------------------------------------------------------------------------------- 1 | package dispatch 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "go.opentelemetry.io/otel/trace" 8 | "go.uber.org/zap" 9 | 10 | "github.com/gotd/td/telegram" 11 | "github.com/gotd/td/tg" 12 | ) 13 | 14 | // LoggedDispatcher is update logging middleware. 15 | type LoggedDispatcher struct { 16 | handler telegram.UpdateHandler 17 | log *zap.Logger 18 | tracer trace.Tracer 19 | } 20 | 21 | // NewLoggedDispatcher creates new update logging middleware. 22 | func NewLoggedDispatcher(next telegram.UpdateHandler, log *zap.Logger, traceProvider trace.TracerProvider) LoggedDispatcher { 23 | return LoggedDispatcher{ 24 | handler: next, 25 | log: log, 26 | tracer: traceProvider.Tracer("td.dispatch.logged"), 27 | } 28 | } 29 | 30 | // Handle implements telegram.UpdateHandler. 31 | func (d LoggedDispatcher) Handle(ctx context.Context, u tg.UpdatesClass) error { 32 | d.log.Debug("Update", 33 | zap.String("t", fmt.Sprintf("%T", u)), 34 | ) 35 | ctx, span := d.tracer.Start(ctx, "handle: "+u.TypeName(), 36 | trace.WithSpanKind(trace.SpanKindServer), 37 | trace.WithAttributes(), 38 | ) 39 | defer span.End() 40 | return d.handler.Handle(ctx, u) 41 | } 42 | -------------------------------------------------------------------------------- /internal/dispatch/message.go: -------------------------------------------------------------------------------- 1 | package dispatch 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/errors" 7 | "go.uber.org/zap" 8 | 9 | "github.com/gotd/td/telegram/message" 10 | "github.com/gotd/td/tg" 11 | ) 12 | 13 | // MessageEvent represents message event. 14 | type MessageEvent struct { 15 | Peer tg.InputPeerClass 16 | Message *tg.Message 17 | 18 | user *tg.User 19 | chat *tg.Chat 20 | channel *tg.Channel 21 | msgSender *tg.User 22 | 23 | baseEvent 24 | } 25 | 26 | // MessageFrom returns user whom send the messag and true, if `from` field was present in event. 27 | func (e MessageEvent) MessageFrom() (u *tg.User, _ bool) { 28 | u = e.msgSender 29 | return u, u != nil 30 | } 31 | 32 | // User returns User object and true if message got from user. 33 | // False and nil otherwise. 34 | func (e MessageEvent) User() (*tg.User, bool) { 35 | return e.user, e.user != nil 36 | } 37 | 38 | // Chat returns Chat object and true if message got from chat. 39 | // False and nil otherwise. 40 | func (e MessageEvent) Chat() (*tg.Chat, bool) { 41 | return e.chat, e.chat != nil 42 | } 43 | 44 | // Channel returns Channel object and true if message got from channel. 45 | // False and nil otherwise. 46 | func (e MessageEvent) Channel() (*tg.Channel, bool) { 47 | return e.channel, e.channel != nil 48 | } 49 | 50 | // WithReply calls given callback if current message event is a reply message. 51 | func (e MessageEvent) WithReply(ctx context.Context, cb func(reply *tg.Message) error) error { 52 | h, ok := e.Message.GetReplyTo() 53 | if !ok { 54 | if _, err := e.Reply().Text(ctx, "Message must be a reply"); err != nil { 55 | return errors.Wrap(err, "send") 56 | } 57 | return nil 58 | } 59 | 60 | var ( 61 | msg *tg.Message 62 | err error 63 | log = e.lg.With( 64 | zap.Int("msg_id", e.Message.ID), 65 | zap.Int("reply_to_msg_id", h.(*tg.MessageReplyHeader).ReplyToMsgID), 66 | ) 67 | ) 68 | switch p := e.Peer.(type) { 69 | case *tg.InputPeerChannel: 70 | log.Info("Fetching message", zap.Int64("channel_id", p.ChannelID)) 71 | 72 | msg, err = e.getChannelMessage(ctx, &tg.InputChannel{ 73 | ChannelID: p.ChannelID, 74 | AccessHash: p.AccessHash, 75 | }, h.(*tg.MessageReplyHeader).ReplyToMsgID) 76 | case *tg.InputPeerChat: 77 | log.Info("Fetching message", zap.Int64("chat_id", p.ChatID)) 78 | 79 | msg, err = e.getMessage(ctx, h.(*tg.MessageReplyHeader).ReplyToMsgID) 80 | case *tg.InputPeerUser: 81 | log.Info("Fetching message", zap.Int64("user_id", p.UserID)) 82 | 83 | msg, err = e.getMessage(ctx, h.(*tg.MessageReplyHeader).ReplyToMsgID) 84 | } 85 | if err != nil { 86 | log.Warn("Fetch message", zap.Error(err)) 87 | if _, err := e.Reply().Textf(ctx, "Message %d not found", h.(*tg.MessageReplyHeader).ReplyToMsgID); err != nil { 88 | return errors.Wrap(err, "send") 89 | } 90 | return nil 91 | } 92 | 93 | return cb(msg) 94 | } 95 | 96 | // Reply creates new message builder to reply. 97 | func (e MessageEvent) Reply() *message.Builder { 98 | return e.sender.To(e.Peer).ReplyMsg(e.Message) 99 | } 100 | 101 | func (e MessageEvent) TypingAction() *message.TypingActionBuilder { 102 | return e.sender.To(e.Peer).TypingAction() 103 | } 104 | 105 | // MessageHandler is a simple message event handler. 106 | type MessageHandler interface { 107 | OnMessage(ctx context.Context, e MessageEvent) error 108 | } 109 | 110 | // MessageHandlerFunc is a functional adapter for Handler. 111 | type MessageHandlerFunc func(ctx context.Context, e MessageEvent) error 112 | 113 | // OnMessage implements MessageHandler. 114 | func (h MessageHandlerFunc) OnMessage(ctx context.Context, e MessageEvent) error { 115 | return h(ctx, e) 116 | } 117 | -------------------------------------------------------------------------------- /internal/dispatch/message_mux.go: -------------------------------------------------------------------------------- 1 | package dispatch 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/go-faster/errors" 8 | "github.com/go-faster/sdk/zctx" 9 | "go.opentelemetry.io/otel/trace" 10 | "go.opentelemetry.io/otel/trace/noop" 11 | "go.uber.org/zap" 12 | 13 | "github.com/gotd/td/tg" 14 | ) 15 | 16 | type handle struct { 17 | MessageHandler 18 | description string 19 | } 20 | 21 | // MessageMux is message event router. 22 | type MessageMux struct { 23 | prefixes map[string]handle 24 | fallback MessageHandler 25 | tracer trace.Tracer 26 | } 27 | 28 | // NewMessageMux creates new MessageMux. 29 | func NewMessageMux() *MessageMux { 30 | return &MessageMux{prefixes: map[string]handle{}, tracer: noop.NewTracerProvider().Tracer("nop")} 31 | } 32 | 33 | func (m *MessageMux) WithTracerProvider(provider trace.TracerProvider) *MessageMux { 34 | m.tracer = provider.Tracer("td.dispatch.message_mux") 35 | return m 36 | } 37 | 38 | // Handle adds given prefix and handler to the mux. 39 | func (m *MessageMux) Handle(prefix, description string, handler MessageHandler) { 40 | m.prefixes[prefix] = handle{ 41 | MessageHandler: handler, 42 | description: description, 43 | } 44 | } 45 | 46 | // HandleFunc adds given prefix and handler to the mux. 47 | func (m *MessageMux) HandleFunc(prefix, description string, handler func(ctx context.Context, e MessageEvent) error) { 48 | m.Handle(prefix, description, MessageHandlerFunc(handler)) 49 | } 50 | 51 | // OnMessage implements MessageHandler. 52 | func (m *MessageMux) OnMessage(ctx context.Context, e MessageEvent) error { 53 | ctx, span := m.tracer.Start(ctx, "OnMessage") 54 | defer span.End() 55 | 56 | lg := zctx.From(ctx) 57 | 58 | for prefix, handler := range m.prefixes { 59 | if strings.HasPrefix(e.Message.Message, prefix) { 60 | lg.Debug("Found handler", zap.String("prefix", prefix)) 61 | if err := handler.OnMessage(ctx, e); err != nil { 62 | return errors.Wrapf(err, "handle %q", prefix) 63 | } 64 | return nil 65 | } 66 | } 67 | 68 | if h := m.fallback; h != nil { 69 | lg.Debug("Using fallback") 70 | return h.OnMessage(ctx, e) 71 | } 72 | 73 | lg.Debug("No handler found") 74 | 75 | return nil 76 | } 77 | 78 | // SetFallback sets fallback handler, if mux is unable to find a command handler. 79 | func (m *MessageMux) SetFallback(h MessageHandler) { 80 | m.fallback = h 81 | } 82 | 83 | // SetFallbackFunc sets fallback handler, if mux is unable to find a command handler. 84 | func (m *MessageMux) SetFallbackFunc(h func(ctx context.Context, e MessageEvent) error) { 85 | m.SetFallback(MessageHandlerFunc(h)) 86 | } 87 | 88 | // RegisterCommands registers all mux commands using https://core.telegram.org/method/bots.setBotCommands. 89 | func (m *MessageMux) RegisterCommands(ctx context.Context, raw *tg.Client) error { 90 | commands := make([]tg.BotCommand, 0, len(m.prefixes)) 91 | for prefix, handler := range m.prefixes { 92 | if handler.description == "" { 93 | continue 94 | } 95 | commands = append(commands, tg.BotCommand{ 96 | Command: strings.TrimPrefix(prefix, "/"), 97 | Description: handler.description, 98 | }) 99 | } 100 | 101 | if _, err := raw.BotsSetBotCommands(ctx, &tg.BotsSetBotCommandsRequest{ 102 | Scope: &tg.BotCommandScopeDefault{}, 103 | LangCode: "en", 104 | Commands: commands, 105 | }); err != nil { 106 | return errors.Wrap(err, "set commands") 107 | } 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /internal/dispatch/message_mux_test.go: -------------------------------------------------------------------------------- 1 | package dispatch 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/gotd/td/tg" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestMessageMux_OnMessage(t *testing.T) { 12 | a := require.New(t) 13 | ctx := context.Background() 14 | mux := NewMessageMux() 15 | 16 | cmdCalls := 0 17 | mux.HandleFunc("/github", "test", func(ctx context.Context, e MessageEvent) error { 18 | cmdCalls++ 19 | return nil 20 | }) 21 | 22 | fallbackCalls := 0 23 | mux.SetFallbackFunc(func(ctx context.Context, e MessageEvent) error { 24 | fallbackCalls++ 25 | return nil 26 | }) 27 | 28 | send := func(text string) { 29 | a.NoError(mux.OnMessage(ctx, MessageEvent{ 30 | Message: &tg.Message{ 31 | Message: text, 32 | }, 33 | })) 34 | } 35 | send("github/") 36 | send("github/gotd") 37 | send("github/gotd/td") 38 | a.Zero(cmdCalls) 39 | a.Equal(fallbackCalls, 3) 40 | 41 | send("/github") 42 | a.Equal(1, cmdCalls) 43 | a.Equal(fallbackCalls, 3) 44 | } 45 | -------------------------------------------------------------------------------- /internal/ent/check/check.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package check 4 | 5 | import ( 6 | "entgo.io/ent/dialect/sql" 7 | ) 8 | 9 | const ( 10 | // Label holds the string label denoting the check type in the database. 11 | Label = "check" 12 | // FieldID holds the string denoting the id field in the database. 13 | FieldID = "id" 14 | // FieldRepoID holds the string denoting the repo_id field in the database. 15 | FieldRepoID = "repo_id" 16 | // FieldPullRequestID holds the string denoting the pull_request_id field in the database. 17 | FieldPullRequestID = "pull_request_id" 18 | // FieldName holds the string denoting the name field in the database. 19 | FieldName = "name" 20 | // FieldStatus holds the string denoting the status field in the database. 21 | FieldStatus = "status" 22 | // FieldConclusion holds the string denoting the conclusion field in the database. 23 | FieldConclusion = "conclusion" 24 | // Table holds the table name of the check in the database. 25 | Table = "checks" 26 | ) 27 | 28 | // Columns holds all SQL columns for check fields. 29 | var Columns = []string{ 30 | FieldID, 31 | FieldRepoID, 32 | FieldPullRequestID, 33 | FieldName, 34 | FieldStatus, 35 | FieldConclusion, 36 | } 37 | 38 | // ValidColumn reports if the column name is valid (part of the table columns). 39 | func ValidColumn(column string) bool { 40 | for i := range Columns { 41 | if column == Columns[i] { 42 | return true 43 | } 44 | } 45 | return false 46 | } 47 | 48 | // OrderOption defines the ordering options for the Check queries. 49 | type OrderOption func(*sql.Selector) 50 | 51 | // ByID orders the results by the id field. 52 | func ByID(opts ...sql.OrderTermOption) OrderOption { 53 | return sql.OrderByField(FieldID, opts...).ToFunc() 54 | } 55 | 56 | // ByRepoID orders the results by the repo_id field. 57 | func ByRepoID(opts ...sql.OrderTermOption) OrderOption { 58 | return sql.OrderByField(FieldRepoID, opts...).ToFunc() 59 | } 60 | 61 | // ByPullRequestID orders the results by the pull_request_id field. 62 | func ByPullRequestID(opts ...sql.OrderTermOption) OrderOption { 63 | return sql.OrderByField(FieldPullRequestID, opts...).ToFunc() 64 | } 65 | 66 | // ByName orders the results by the name field. 67 | func ByName(opts ...sql.OrderTermOption) OrderOption { 68 | return sql.OrderByField(FieldName, opts...).ToFunc() 69 | } 70 | 71 | // ByStatus orders the results by the status field. 72 | func ByStatus(opts ...sql.OrderTermOption) OrderOption { 73 | return sql.OrderByField(FieldStatus, opts...).ToFunc() 74 | } 75 | 76 | // ByConclusion orders the results by the conclusion field. 77 | func ByConclusion(opts ...sql.OrderTermOption) OrderOption { 78 | return sql.OrderByField(FieldConclusion, opts...).ToFunc() 79 | } 80 | -------------------------------------------------------------------------------- /internal/ent/check_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/go-faster/bot/internal/ent/check" 12 | "github.com/go-faster/bot/internal/ent/predicate" 13 | ) 14 | 15 | // CheckDelete is the builder for deleting a Check entity. 16 | type CheckDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *CheckMutation 20 | } 21 | 22 | // Where appends a list predicates to the CheckDelete builder. 23 | func (cd *CheckDelete) Where(ps ...predicate.Check) *CheckDelete { 24 | cd.mutation.Where(ps...) 25 | return cd 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (cd *CheckDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, cd.sqlExec, cd.mutation, cd.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (cd *CheckDelete) ExecX(ctx context.Context) int { 35 | n, err := cd.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (cd *CheckDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(check.Table, sqlgraph.NewFieldSpec(check.FieldID, field.TypeInt64)) 44 | if ps := cd.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, cd.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | cd.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // CheckDeleteOne is the builder for deleting a single Check entity. 60 | type CheckDeleteOne struct { 61 | cd *CheckDelete 62 | } 63 | 64 | // Where appends a list predicates to the CheckDelete builder. 65 | func (cdo *CheckDeleteOne) Where(ps ...predicate.Check) *CheckDeleteOne { 66 | cdo.cd.mutation.Where(ps...) 67 | return cdo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (cdo *CheckDeleteOne) Exec(ctx context.Context) error { 72 | n, err := cdo.cd.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{check.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (cdo *CheckDeleteOne) ExecX(ctx context.Context) { 85 | if err := cdo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/ent/entc.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import ( 7 | "log" 8 | 9 | "entgo.io/ent/entc" 10 | "entgo.io/ent/entc/gen" 11 | "github.com/go-faster/errors" 12 | ) 13 | 14 | func main() { 15 | if err := run(); err != nil { 16 | log.Fatalf("error: %v", err) 17 | } 18 | } 19 | 20 | func run() error { 21 | if err := entc.Generate("./schema", &gen.Config{ 22 | Features: []gen.Feature{ 23 | gen.FeatureUpsert, 24 | gen.FeatureVersionedMigration, 25 | gen.FeatureIntercept, 26 | gen.FeatureNamedEdges, 27 | }, 28 | }); err != nil { 29 | return errors.Wrap(err, "ent codegen") 30 | } 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /internal/ent/enttest/enttest.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package enttest 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/go-faster/bot/internal/ent" 9 | // required by schema hooks. 10 | _ "github.com/go-faster/bot/internal/ent/runtime" 11 | 12 | "entgo.io/ent/dialect/sql/schema" 13 | "github.com/go-faster/bot/internal/ent/migrate" 14 | ) 15 | 16 | type ( 17 | // TestingT is the interface that is shared between 18 | // testing.T and testing.B and used by enttest. 19 | TestingT interface { 20 | FailNow() 21 | Error(...any) 22 | } 23 | 24 | // Option configures client creation. 25 | Option func(*options) 26 | 27 | options struct { 28 | opts []ent.Option 29 | migrateOpts []schema.MigrateOption 30 | } 31 | ) 32 | 33 | // WithOptions forwards options to client creation. 34 | func WithOptions(opts ...ent.Option) Option { 35 | return func(o *options) { 36 | o.opts = append(o.opts, opts...) 37 | } 38 | } 39 | 40 | // WithMigrateOptions forwards options to auto migration. 41 | func WithMigrateOptions(opts ...schema.MigrateOption) Option { 42 | return func(o *options) { 43 | o.migrateOpts = append(o.migrateOpts, opts...) 44 | } 45 | } 46 | 47 | func newOptions(opts []Option) *options { 48 | o := &options{} 49 | for _, opt := range opts { 50 | opt(o) 51 | } 52 | return o 53 | } 54 | 55 | // Open calls ent.Open and auto-run migration. 56 | func Open(t TestingT, driverName, dataSourceName string, opts ...Option) *ent.Client { 57 | o := newOptions(opts) 58 | c, err := ent.Open(driverName, dataSourceName, o.opts...) 59 | if err != nil { 60 | t.Error(err) 61 | t.FailNow() 62 | } 63 | migrateSchema(t, c, o) 64 | return c 65 | } 66 | 67 | // NewClient calls ent.NewClient and auto-run migration. 68 | func NewClient(t TestingT, opts ...Option) *ent.Client { 69 | o := newOptions(opts) 70 | c := ent.NewClient(o.opts...) 71 | migrateSchema(t, c, o) 72 | return c 73 | } 74 | func migrateSchema(t TestingT, c *ent.Client, o *options) { 75 | tables, err := schema.CopyTables(migrate.Tables) 76 | if err != nil { 77 | t.Error(err) 78 | t.FailNow() 79 | } 80 | if err := migrate.Create(context.Background(), c.Schema, tables, o.migrateOpts...); err != nil { 81 | t.Error(err) 82 | t.FailNow() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /internal/ent/generate.go: -------------------------------------------------------------------------------- 1 | package ent 2 | 3 | //go:generate go run ./entc.go 4 | -------------------------------------------------------------------------------- /internal/ent/gitcommit_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/go-faster/bot/internal/ent/gitcommit" 12 | "github.com/go-faster/bot/internal/ent/predicate" 13 | ) 14 | 15 | // GitCommitDelete is the builder for deleting a GitCommit entity. 16 | type GitCommitDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *GitCommitMutation 20 | } 21 | 22 | // Where appends a list predicates to the GitCommitDelete builder. 23 | func (gcd *GitCommitDelete) Where(ps ...predicate.GitCommit) *GitCommitDelete { 24 | gcd.mutation.Where(ps...) 25 | return gcd 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (gcd *GitCommitDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, gcd.sqlExec, gcd.mutation, gcd.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (gcd *GitCommitDelete) ExecX(ctx context.Context) int { 35 | n, err := gcd.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (gcd *GitCommitDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(gitcommit.Table, sqlgraph.NewFieldSpec(gitcommit.FieldID, field.TypeString)) 44 | if ps := gcd.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, gcd.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | gcd.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // GitCommitDeleteOne is the builder for deleting a single GitCommit entity. 60 | type GitCommitDeleteOne struct { 61 | gcd *GitCommitDelete 62 | } 63 | 64 | // Where appends a list predicates to the GitCommitDelete builder. 65 | func (gcdo *GitCommitDeleteOne) Where(ps ...predicate.GitCommit) *GitCommitDeleteOne { 66 | gcdo.gcd.mutation.Where(ps...) 67 | return gcdo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (gcdo *GitCommitDeleteOne) Exec(ctx context.Context) error { 72 | n, err := gcdo.gcd.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{gitcommit.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (gcdo *GitCommitDeleteOne) ExecX(ctx context.Context) { 85 | if err := gcdo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/ent/gptdialog/gptdialog.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package gptdialog 4 | 5 | import ( 6 | "time" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | ) 10 | 11 | const ( 12 | // Label holds the string label denoting the gptdialog type in the database. 13 | Label = "gpt_dialog" 14 | // FieldID holds the string denoting the id field in the database. 15 | FieldID = "id" 16 | // FieldPeerID holds the string denoting the peer_id field in the database. 17 | FieldPeerID = "peer_id" 18 | // FieldPromptMsgID holds the string denoting the prompt_msg_id field in the database. 19 | FieldPromptMsgID = "prompt_msg_id" 20 | // FieldPromptMsg holds the string denoting the prompt_msg field in the database. 21 | FieldPromptMsg = "prompt_msg" 22 | // FieldGptMsgID holds the string denoting the gpt_msg_id field in the database. 23 | FieldGptMsgID = "gpt_msg_id" 24 | // FieldGptMsg holds the string denoting the gpt_msg field in the database. 25 | FieldGptMsg = "gpt_msg" 26 | // FieldThreadTopMsgID holds the string denoting the thread_top_msg_id field in the database. 27 | FieldThreadTopMsgID = "thread_top_msg_id" 28 | // FieldCreatedAt holds the string denoting the created_at field in the database. 29 | FieldCreatedAt = "created_at" 30 | // Table holds the table name of the gptdialog in the database. 31 | Table = "gpt_dialogs" 32 | ) 33 | 34 | // Columns holds all SQL columns for gptdialog fields. 35 | var Columns = []string{ 36 | FieldID, 37 | FieldPeerID, 38 | FieldPromptMsgID, 39 | FieldPromptMsg, 40 | FieldGptMsgID, 41 | FieldGptMsg, 42 | FieldThreadTopMsgID, 43 | FieldCreatedAt, 44 | } 45 | 46 | // ValidColumn reports if the column name is valid (part of the table columns). 47 | func ValidColumn(column string) bool { 48 | for i := range Columns { 49 | if column == Columns[i] { 50 | return true 51 | } 52 | } 53 | return false 54 | } 55 | 56 | var ( 57 | // DefaultCreatedAt holds the default value on creation for the "created_at" field. 58 | DefaultCreatedAt func() time.Time 59 | ) 60 | 61 | // OrderOption defines the ordering options for the GPTDialog queries. 62 | type OrderOption func(*sql.Selector) 63 | 64 | // ByID orders the results by the id field. 65 | func ByID(opts ...sql.OrderTermOption) OrderOption { 66 | return sql.OrderByField(FieldID, opts...).ToFunc() 67 | } 68 | 69 | // ByPeerID orders the results by the peer_id field. 70 | func ByPeerID(opts ...sql.OrderTermOption) OrderOption { 71 | return sql.OrderByField(FieldPeerID, opts...).ToFunc() 72 | } 73 | 74 | // ByPromptMsgID orders the results by the prompt_msg_id field. 75 | func ByPromptMsgID(opts ...sql.OrderTermOption) OrderOption { 76 | return sql.OrderByField(FieldPromptMsgID, opts...).ToFunc() 77 | } 78 | 79 | // ByPromptMsg orders the results by the prompt_msg field. 80 | func ByPromptMsg(opts ...sql.OrderTermOption) OrderOption { 81 | return sql.OrderByField(FieldPromptMsg, opts...).ToFunc() 82 | } 83 | 84 | // ByGptMsgID orders the results by the gpt_msg_id field. 85 | func ByGptMsgID(opts ...sql.OrderTermOption) OrderOption { 86 | return sql.OrderByField(FieldGptMsgID, opts...).ToFunc() 87 | } 88 | 89 | // ByGptMsg orders the results by the gpt_msg field. 90 | func ByGptMsg(opts ...sql.OrderTermOption) OrderOption { 91 | return sql.OrderByField(FieldGptMsg, opts...).ToFunc() 92 | } 93 | 94 | // ByThreadTopMsgID orders the results by the thread_top_msg_id field. 95 | func ByThreadTopMsgID(opts ...sql.OrderTermOption) OrderOption { 96 | return sql.OrderByField(FieldThreadTopMsgID, opts...).ToFunc() 97 | } 98 | 99 | // ByCreatedAt orders the results by the created_at field. 100 | func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption { 101 | return sql.OrderByField(FieldCreatedAt, opts...).ToFunc() 102 | } 103 | -------------------------------------------------------------------------------- /internal/ent/gptdialog_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/go-faster/bot/internal/ent/gptdialog" 12 | "github.com/go-faster/bot/internal/ent/predicate" 13 | ) 14 | 15 | // GPTDialogDelete is the builder for deleting a GPTDialog entity. 16 | type GPTDialogDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *GPTDialogMutation 20 | } 21 | 22 | // Where appends a list predicates to the GPTDialogDelete builder. 23 | func (gdd *GPTDialogDelete) Where(ps ...predicate.GPTDialog) *GPTDialogDelete { 24 | gdd.mutation.Where(ps...) 25 | return gdd 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (gdd *GPTDialogDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, gdd.sqlExec, gdd.mutation, gdd.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (gdd *GPTDialogDelete) ExecX(ctx context.Context) int { 35 | n, err := gdd.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (gdd *GPTDialogDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(gptdialog.Table, sqlgraph.NewFieldSpec(gptdialog.FieldID, field.TypeInt)) 44 | if ps := gdd.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, gdd.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | gdd.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // GPTDialogDeleteOne is the builder for deleting a single GPTDialog entity. 60 | type GPTDialogDeleteOne struct { 61 | gdd *GPTDialogDelete 62 | } 63 | 64 | // Where appends a list predicates to the GPTDialogDelete builder. 65 | func (gddo *GPTDialogDeleteOne) Where(ps ...predicate.GPTDialog) *GPTDialogDeleteOne { 66 | gddo.gdd.mutation.Where(ps...) 67 | return gddo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (gddo *GPTDialogDeleteOne) Exec(ctx context.Context) error { 72 | n, err := gddo.gdd.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{gptdialog.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (gddo *GPTDialogDeleteOne) ExecX(ctx context.Context) { 85 | if err := gddo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/ent/lastchannelmessage.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | 9 | "entgo.io/ent" 10 | "entgo.io/ent/dialect/sql" 11 | "github.com/go-faster/bot/internal/ent/lastchannelmessage" 12 | ) 13 | 14 | // LastChannelMessage is the model entity for the LastChannelMessage schema. 15 | type LastChannelMessage struct { 16 | config `json:"-"` 17 | // ID of the ent. 18 | // Telegram channel ID. 19 | ID int64 `json:"id,omitempty"` 20 | // Telegram message ID of last observed message in channel. 21 | MessageID int `json:"message_id,omitempty"` 22 | selectValues sql.SelectValues 23 | } 24 | 25 | // scanValues returns the types for scanning values from sql.Rows. 26 | func (*LastChannelMessage) scanValues(columns []string) ([]any, error) { 27 | values := make([]any, len(columns)) 28 | for i := range columns { 29 | switch columns[i] { 30 | case lastchannelmessage.FieldID, lastchannelmessage.FieldMessageID: 31 | values[i] = new(sql.NullInt64) 32 | default: 33 | values[i] = new(sql.UnknownType) 34 | } 35 | } 36 | return values, nil 37 | } 38 | 39 | // assignValues assigns the values that were returned from sql.Rows (after scanning) 40 | // to the LastChannelMessage fields. 41 | func (lcm *LastChannelMessage) assignValues(columns []string, values []any) error { 42 | if m, n := len(values), len(columns); m < n { 43 | return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) 44 | } 45 | for i := range columns { 46 | switch columns[i] { 47 | case lastchannelmessage.FieldID: 48 | value, ok := values[i].(*sql.NullInt64) 49 | if !ok { 50 | return fmt.Errorf("unexpected type %T for field id", value) 51 | } 52 | lcm.ID = int64(value.Int64) 53 | case lastchannelmessage.FieldMessageID: 54 | if value, ok := values[i].(*sql.NullInt64); !ok { 55 | return fmt.Errorf("unexpected type %T for field message_id", values[i]) 56 | } else if value.Valid { 57 | lcm.MessageID = int(value.Int64) 58 | } 59 | default: 60 | lcm.selectValues.Set(columns[i], values[i]) 61 | } 62 | } 63 | return nil 64 | } 65 | 66 | // Value returns the ent.Value that was dynamically selected and assigned to the LastChannelMessage. 67 | // This includes values selected through modifiers, order, etc. 68 | func (lcm *LastChannelMessage) Value(name string) (ent.Value, error) { 69 | return lcm.selectValues.Get(name) 70 | } 71 | 72 | // Update returns a builder for updating this LastChannelMessage. 73 | // Note that you need to call LastChannelMessage.Unwrap() before calling this method if this LastChannelMessage 74 | // was returned from a transaction, and the transaction was committed or rolled back. 75 | func (lcm *LastChannelMessage) Update() *LastChannelMessageUpdateOne { 76 | return NewLastChannelMessageClient(lcm.config).UpdateOne(lcm) 77 | } 78 | 79 | // Unwrap unwraps the LastChannelMessage entity that was returned from a transaction after it was closed, 80 | // so that all future queries will be executed through the driver which created the transaction. 81 | func (lcm *LastChannelMessage) Unwrap() *LastChannelMessage { 82 | _tx, ok := lcm.config.driver.(*txDriver) 83 | if !ok { 84 | panic("ent: LastChannelMessage is not a transactional entity") 85 | } 86 | lcm.config.driver = _tx.drv 87 | return lcm 88 | } 89 | 90 | // String implements the fmt.Stringer. 91 | func (lcm *LastChannelMessage) String() string { 92 | var builder strings.Builder 93 | builder.WriteString("LastChannelMessage(") 94 | builder.WriteString(fmt.Sprintf("id=%v, ", lcm.ID)) 95 | builder.WriteString("message_id=") 96 | builder.WriteString(fmt.Sprintf("%v", lcm.MessageID)) 97 | builder.WriteByte(')') 98 | return builder.String() 99 | } 100 | 101 | // LastChannelMessages is a parsable slice of LastChannelMessage. 102 | type LastChannelMessages []*LastChannelMessage 103 | -------------------------------------------------------------------------------- /internal/ent/lastchannelmessage/lastchannelmessage.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package lastchannelmessage 4 | 5 | import ( 6 | "entgo.io/ent/dialect/sql" 7 | ) 8 | 9 | const ( 10 | // Label holds the string label denoting the lastchannelmessage type in the database. 11 | Label = "last_channel_message" 12 | // FieldID holds the string denoting the id field in the database. 13 | FieldID = "id" 14 | // FieldMessageID holds the string denoting the message_id field in the database. 15 | FieldMessageID = "message_id" 16 | // Table holds the table name of the lastchannelmessage in the database. 17 | Table = "last_channel_messages" 18 | ) 19 | 20 | // Columns holds all SQL columns for lastchannelmessage fields. 21 | var Columns = []string{ 22 | FieldID, 23 | FieldMessageID, 24 | } 25 | 26 | // ValidColumn reports if the column name is valid (part of the table columns). 27 | func ValidColumn(column string) bool { 28 | for i := range Columns { 29 | if column == Columns[i] { 30 | return true 31 | } 32 | } 33 | return false 34 | } 35 | 36 | // OrderOption defines the ordering options for the LastChannelMessage queries. 37 | type OrderOption func(*sql.Selector) 38 | 39 | // ByID orders the results by the id field. 40 | func ByID(opts ...sql.OrderTermOption) OrderOption { 41 | return sql.OrderByField(FieldID, opts...).ToFunc() 42 | } 43 | 44 | // ByMessageID orders the results by the message_id field. 45 | func ByMessageID(opts ...sql.OrderTermOption) OrderOption { 46 | return sql.OrderByField(FieldMessageID, opts...).ToFunc() 47 | } 48 | -------------------------------------------------------------------------------- /internal/ent/lastchannelmessage_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/go-faster/bot/internal/ent/lastchannelmessage" 12 | "github.com/go-faster/bot/internal/ent/predicate" 13 | ) 14 | 15 | // LastChannelMessageDelete is the builder for deleting a LastChannelMessage entity. 16 | type LastChannelMessageDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *LastChannelMessageMutation 20 | } 21 | 22 | // Where appends a list predicates to the LastChannelMessageDelete builder. 23 | func (lcmd *LastChannelMessageDelete) Where(ps ...predicate.LastChannelMessage) *LastChannelMessageDelete { 24 | lcmd.mutation.Where(ps...) 25 | return lcmd 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (lcmd *LastChannelMessageDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, lcmd.sqlExec, lcmd.mutation, lcmd.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (lcmd *LastChannelMessageDelete) ExecX(ctx context.Context) int { 35 | n, err := lcmd.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (lcmd *LastChannelMessageDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(lastchannelmessage.Table, sqlgraph.NewFieldSpec(lastchannelmessage.FieldID, field.TypeInt64)) 44 | if ps := lcmd.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, lcmd.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | lcmd.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // LastChannelMessageDeleteOne is the builder for deleting a single LastChannelMessage entity. 60 | type LastChannelMessageDeleteOne struct { 61 | lcmd *LastChannelMessageDelete 62 | } 63 | 64 | // Where appends a list predicates to the LastChannelMessageDelete builder. 65 | func (lcmdo *LastChannelMessageDeleteOne) Where(ps ...predicate.LastChannelMessage) *LastChannelMessageDeleteOne { 66 | lcmdo.lcmd.mutation.Where(ps...) 67 | return lcmdo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (lcmdo *LastChannelMessageDeleteOne) Exec(ctx context.Context) error { 72 | n, err := lcmdo.lcmd.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{lastchannelmessage.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (lcmdo *LastChannelMessageDeleteOne) ExecX(ctx context.Context) { 85 | if err := lcmdo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/ent/migrate/migrate.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package migrate 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "io" 9 | 10 | "entgo.io/ent/dialect" 11 | "entgo.io/ent/dialect/sql/schema" 12 | ) 13 | 14 | var ( 15 | // WithGlobalUniqueID sets the universal ids options to the migration. 16 | // If this option is enabled, ent migration will allocate a 1<<32 range 17 | // for the ids of each entity (table). 18 | // Note that this option cannot be applied on tables that already exist. 19 | WithGlobalUniqueID = schema.WithGlobalUniqueID 20 | // WithDropColumn sets the drop column option to the migration. 21 | // If this option is enabled, ent migration will drop old columns 22 | // that were used for both fields and edges. This defaults to false. 23 | WithDropColumn = schema.WithDropColumn 24 | // WithDropIndex sets the drop index option to the migration. 25 | // If this option is enabled, ent migration will drop old indexes 26 | // that were defined in the schema. This defaults to false. 27 | // Note that unique constraints are defined using `UNIQUE INDEX`, 28 | // and therefore, it's recommended to enable this option to get more 29 | // flexibility in the schema changes. 30 | WithDropIndex = schema.WithDropIndex 31 | // WithForeignKeys enables creating foreign-key in schema DDL. This defaults to true. 32 | WithForeignKeys = schema.WithForeignKeys 33 | ) 34 | 35 | // Schema is the API for creating, migrating and dropping a schema. 36 | type Schema struct { 37 | drv dialect.Driver 38 | } 39 | 40 | // NewSchema creates a new schema client. 41 | func NewSchema(drv dialect.Driver) *Schema { return &Schema{drv: drv} } 42 | 43 | // Create creates all schema resources. 44 | func (s *Schema) Create(ctx context.Context, opts ...schema.MigrateOption) error { 45 | return Create(ctx, s, Tables, opts...) 46 | } 47 | 48 | // Create creates all table resources using the given schema driver. 49 | func Create(ctx context.Context, s *Schema, tables []*schema.Table, opts ...schema.MigrateOption) error { 50 | migrate, err := schema.NewMigrate(s.drv, opts...) 51 | if err != nil { 52 | return fmt.Errorf("ent/migrate: %w", err) 53 | } 54 | return migrate.Create(ctx, tables...) 55 | } 56 | 57 | // Diff compares the state read from a database connection or migration directory with 58 | // the state defined by the Ent schema. Changes will be written to new migration files. 59 | func Diff(ctx context.Context, url string, opts ...schema.MigrateOption) error { 60 | return NamedDiff(ctx, url, "changes", opts...) 61 | } 62 | 63 | // NamedDiff compares the state read from a database connection or migration directory with 64 | // the state defined by the Ent schema. Changes will be written to new named migration files. 65 | func NamedDiff(ctx context.Context, url, name string, opts ...schema.MigrateOption) error { 66 | return schema.Diff(ctx, url, name, Tables, opts...) 67 | } 68 | 69 | // Diff creates a migration file containing the statements to resolve the diff 70 | // between the Ent schema and the connected database. 71 | func (s *Schema) Diff(ctx context.Context, opts ...schema.MigrateOption) error { 72 | migrate, err := schema.NewMigrate(s.drv, opts...) 73 | if err != nil { 74 | return fmt.Errorf("ent/migrate: %w", err) 75 | } 76 | return migrate.Diff(ctx, Tables...) 77 | } 78 | 79 | // NamedDiff creates a named migration file containing the statements to resolve the diff 80 | // between the Ent schema and the connected database. 81 | func (s *Schema) NamedDiff(ctx context.Context, name string, opts ...schema.MigrateOption) error { 82 | migrate, err := schema.NewMigrate(s.drv, opts...) 83 | if err != nil { 84 | return fmt.Errorf("ent/migrate: %w", err) 85 | } 86 | return migrate.NamedDiff(ctx, name, Tables...) 87 | } 88 | 89 | // WriteTo writes the schema changes to w instead of running them against the database. 90 | // 91 | // if err := client.Schema.WriteTo(context.Background(), os.Stdout); err != nil { 92 | // log.Fatal(err) 93 | // } 94 | func (s *Schema) WriteTo(ctx context.Context, w io.Writer, opts ...schema.MigrateOption) error { 95 | return Create(ctx, &Schema{drv: &schema.WriteDriver{Writer: w, Driver: s.drv}}, Tables, opts...) 96 | } 97 | -------------------------------------------------------------------------------- /internal/ent/organization/organization.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package organization 4 | 5 | import ( 6 | "entgo.io/ent/dialect/sql" 7 | "entgo.io/ent/dialect/sql/sqlgraph" 8 | ) 9 | 10 | const ( 11 | // Label holds the string label denoting the organization type in the database. 12 | Label = "organization" 13 | // FieldID holds the string denoting the id field in the database. 14 | FieldID = "id" 15 | // FieldName holds the string denoting the name field in the database. 16 | FieldName = "name" 17 | // FieldHTMLURL holds the string denoting the html_url field in the database. 18 | FieldHTMLURL = "html_url" 19 | // EdgeRepositories holds the string denoting the repositories edge name in mutations. 20 | EdgeRepositories = "repositories" 21 | // Table holds the table name of the organization in the database. 22 | Table = "organizations" 23 | // RepositoriesTable is the table that holds the repositories relation/edge. 24 | RepositoriesTable = "repositories" 25 | // RepositoriesInverseTable is the table name for the Repository entity. 26 | // It exists in this package in order to avoid circular dependency with the "repository" package. 27 | RepositoriesInverseTable = "repositories" 28 | // RepositoriesColumn is the table column denoting the repositories relation/edge. 29 | RepositoriesColumn = "organization_repositories" 30 | ) 31 | 32 | // Columns holds all SQL columns for organization fields. 33 | var Columns = []string{ 34 | FieldID, 35 | FieldName, 36 | FieldHTMLURL, 37 | } 38 | 39 | // ValidColumn reports if the column name is valid (part of the table columns). 40 | func ValidColumn(column string) bool { 41 | for i := range Columns { 42 | if column == Columns[i] { 43 | return true 44 | } 45 | } 46 | return false 47 | } 48 | 49 | // OrderOption defines the ordering options for the Organization queries. 50 | type OrderOption func(*sql.Selector) 51 | 52 | // ByID orders the results by the id field. 53 | func ByID(opts ...sql.OrderTermOption) OrderOption { 54 | return sql.OrderByField(FieldID, opts...).ToFunc() 55 | } 56 | 57 | // ByName orders the results by the name field. 58 | func ByName(opts ...sql.OrderTermOption) OrderOption { 59 | return sql.OrderByField(FieldName, opts...).ToFunc() 60 | } 61 | 62 | // ByHTMLURL orders the results by the html_url field. 63 | func ByHTMLURL(opts ...sql.OrderTermOption) OrderOption { 64 | return sql.OrderByField(FieldHTMLURL, opts...).ToFunc() 65 | } 66 | 67 | // ByRepositoriesCount orders the results by repositories count. 68 | func ByRepositoriesCount(opts ...sql.OrderTermOption) OrderOption { 69 | return func(s *sql.Selector) { 70 | sqlgraph.OrderByNeighborsCount(s, newRepositoriesStep(), opts...) 71 | } 72 | } 73 | 74 | // ByRepositories orders the results by repositories terms. 75 | func ByRepositories(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption { 76 | return func(s *sql.Selector) { 77 | sqlgraph.OrderByNeighborTerms(s, newRepositoriesStep(), append([]sql.OrderTerm{term}, terms...)...) 78 | } 79 | } 80 | func newRepositoriesStep() *sqlgraph.Step { 81 | return sqlgraph.NewStep( 82 | sqlgraph.From(Table, FieldID), 83 | sqlgraph.To(RepositoriesInverseTable, FieldID), 84 | sqlgraph.Edge(sqlgraph.O2M, false, RepositoriesTable, RepositoriesColumn), 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /internal/ent/organization_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/go-faster/bot/internal/ent/organization" 12 | "github.com/go-faster/bot/internal/ent/predicate" 13 | ) 14 | 15 | // OrganizationDelete is the builder for deleting a Organization entity. 16 | type OrganizationDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *OrganizationMutation 20 | } 21 | 22 | // Where appends a list predicates to the OrganizationDelete builder. 23 | func (od *OrganizationDelete) Where(ps ...predicate.Organization) *OrganizationDelete { 24 | od.mutation.Where(ps...) 25 | return od 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (od *OrganizationDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, od.sqlExec, od.mutation, od.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (od *OrganizationDelete) ExecX(ctx context.Context) int { 35 | n, err := od.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (od *OrganizationDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(organization.Table, sqlgraph.NewFieldSpec(organization.FieldID, field.TypeInt64)) 44 | if ps := od.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, od.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | od.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // OrganizationDeleteOne is the builder for deleting a single Organization entity. 60 | type OrganizationDeleteOne struct { 61 | od *OrganizationDelete 62 | } 63 | 64 | // Where appends a list predicates to the OrganizationDelete builder. 65 | func (odo *OrganizationDeleteOne) Where(ps ...predicate.Organization) *OrganizationDeleteOne { 66 | odo.od.mutation.Where(ps...) 67 | return odo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (odo *OrganizationDeleteOne) Exec(ctx context.Context) error { 72 | n, err := odo.od.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{organization.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (odo *OrganizationDeleteOne) ExecX(ctx context.Context) { 85 | if err := odo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/ent/predicate/predicate.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package predicate 4 | 5 | import ( 6 | "entgo.io/ent/dialect/sql" 7 | ) 8 | 9 | // Check is the predicate function for check builders. 10 | type Check func(*sql.Selector) 11 | 12 | // GPTDialog is the predicate function for gptdialog builders. 13 | type GPTDialog func(*sql.Selector) 14 | 15 | // GitCommit is the predicate function for gitcommit builders. 16 | type GitCommit func(*sql.Selector) 17 | 18 | // LastChannelMessage is the predicate function for lastchannelmessage builders. 19 | type LastChannelMessage func(*sql.Selector) 20 | 21 | // Organization is the predicate function for organization builders. 22 | type Organization func(*sql.Selector) 23 | 24 | // PRNotification is the predicate function for prnotification builders. 25 | type PRNotification func(*sql.Selector) 26 | 27 | // Repository is the predicate function for repository builders. 28 | type Repository func(*sql.Selector) 29 | 30 | // TelegramChannelState is the predicate function for telegramchannelstate builders. 31 | type TelegramChannelState func(*sql.Selector) 32 | 33 | // TelegramSession is the predicate function for telegramsession builders. 34 | type TelegramSession func(*sql.Selector) 35 | 36 | // TelegramUserState is the predicate function for telegramuserstate builders. 37 | type TelegramUserState func(*sql.Selector) 38 | 39 | // User is the predicate function for user builders. 40 | type User func(*sql.Selector) 41 | -------------------------------------------------------------------------------- /internal/ent/prnotification/prnotification.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package prnotification 4 | 5 | import ( 6 | "entgo.io/ent/dialect/sql" 7 | ) 8 | 9 | const ( 10 | // Label holds the string label denoting the prnotification type in the database. 11 | Label = "pr_notification" 12 | // FieldID holds the string denoting the id field in the database. 13 | FieldID = "id" 14 | // FieldRepoID holds the string denoting the repo_id field in the database. 15 | FieldRepoID = "repo_id" 16 | // FieldPullRequestID holds the string denoting the pull_request_id field in the database. 17 | FieldPullRequestID = "pull_request_id" 18 | // FieldPullRequestTitle holds the string denoting the pull_request_title field in the database. 19 | FieldPullRequestTitle = "pull_request_title" 20 | // FieldPullRequestBody holds the string denoting the pull_request_body field in the database. 21 | FieldPullRequestBody = "pull_request_body" 22 | // FieldPullRequestAuthorLogin holds the string denoting the pull_request_author_login field in the database. 23 | FieldPullRequestAuthorLogin = "pull_request_author_login" 24 | // FieldMessageID holds the string denoting the message_id field in the database. 25 | FieldMessageID = "message_id" 26 | // Table holds the table name of the prnotification in the database. 27 | Table = "pr_notifications" 28 | ) 29 | 30 | // Columns holds all SQL columns for prnotification fields. 31 | var Columns = []string{ 32 | FieldID, 33 | FieldRepoID, 34 | FieldPullRequestID, 35 | FieldPullRequestTitle, 36 | FieldPullRequestBody, 37 | FieldPullRequestAuthorLogin, 38 | FieldMessageID, 39 | } 40 | 41 | // ValidColumn reports if the column name is valid (part of the table columns). 42 | func ValidColumn(column string) bool { 43 | for i := range Columns { 44 | if column == Columns[i] { 45 | return true 46 | } 47 | } 48 | return false 49 | } 50 | 51 | var ( 52 | // DefaultPullRequestTitle holds the default value on creation for the "pull_request_title" field. 53 | DefaultPullRequestTitle string 54 | // DefaultPullRequestBody holds the default value on creation for the "pull_request_body" field. 55 | DefaultPullRequestBody string 56 | // DefaultPullRequestAuthorLogin holds the default value on creation for the "pull_request_author_login" field. 57 | DefaultPullRequestAuthorLogin string 58 | ) 59 | 60 | // OrderOption defines the ordering options for the PRNotification queries. 61 | type OrderOption func(*sql.Selector) 62 | 63 | // ByID orders the results by the id field. 64 | func ByID(opts ...sql.OrderTermOption) OrderOption { 65 | return sql.OrderByField(FieldID, opts...).ToFunc() 66 | } 67 | 68 | // ByRepoID orders the results by the repo_id field. 69 | func ByRepoID(opts ...sql.OrderTermOption) OrderOption { 70 | return sql.OrderByField(FieldRepoID, opts...).ToFunc() 71 | } 72 | 73 | // ByPullRequestID orders the results by the pull_request_id field. 74 | func ByPullRequestID(opts ...sql.OrderTermOption) OrderOption { 75 | return sql.OrderByField(FieldPullRequestID, opts...).ToFunc() 76 | } 77 | 78 | // ByPullRequestTitle orders the results by the pull_request_title field. 79 | func ByPullRequestTitle(opts ...sql.OrderTermOption) OrderOption { 80 | return sql.OrderByField(FieldPullRequestTitle, opts...).ToFunc() 81 | } 82 | 83 | // ByPullRequestBody orders the results by the pull_request_body field. 84 | func ByPullRequestBody(opts ...sql.OrderTermOption) OrderOption { 85 | return sql.OrderByField(FieldPullRequestBody, opts...).ToFunc() 86 | } 87 | 88 | // ByPullRequestAuthorLogin orders the results by the pull_request_author_login field. 89 | func ByPullRequestAuthorLogin(opts ...sql.OrderTermOption) OrderOption { 90 | return sql.OrderByField(FieldPullRequestAuthorLogin, opts...).ToFunc() 91 | } 92 | 93 | // ByMessageID orders the results by the message_id field. 94 | func ByMessageID(opts ...sql.OrderTermOption) OrderOption { 95 | return sql.OrderByField(FieldMessageID, opts...).ToFunc() 96 | } 97 | -------------------------------------------------------------------------------- /internal/ent/prnotification_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/go-faster/bot/internal/ent/predicate" 12 | "github.com/go-faster/bot/internal/ent/prnotification" 13 | ) 14 | 15 | // PRNotificationDelete is the builder for deleting a PRNotification entity. 16 | type PRNotificationDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *PRNotificationMutation 20 | } 21 | 22 | // Where appends a list predicates to the PRNotificationDelete builder. 23 | func (pnd *PRNotificationDelete) Where(ps ...predicate.PRNotification) *PRNotificationDelete { 24 | pnd.mutation.Where(ps...) 25 | return pnd 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (pnd *PRNotificationDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, pnd.sqlExec, pnd.mutation, pnd.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (pnd *PRNotificationDelete) ExecX(ctx context.Context) int { 35 | n, err := pnd.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (pnd *PRNotificationDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(prnotification.Table, sqlgraph.NewFieldSpec(prnotification.FieldID, field.TypeInt)) 44 | if ps := pnd.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, pnd.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | pnd.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // PRNotificationDeleteOne is the builder for deleting a single PRNotification entity. 60 | type PRNotificationDeleteOne struct { 61 | pnd *PRNotificationDelete 62 | } 63 | 64 | // Where appends a list predicates to the PRNotificationDelete builder. 65 | func (pndo *PRNotificationDeleteOne) Where(ps ...predicate.PRNotification) *PRNotificationDeleteOne { 66 | pndo.pnd.mutation.Where(ps...) 67 | return pndo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (pndo *PRNotificationDeleteOne) Exec(ctx context.Context) error { 72 | n, err := pndo.pnd.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{prnotification.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (pndo *PRNotificationDeleteOne) ExecX(ctx context.Context) { 85 | if err := pndo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/ent/repository_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/go-faster/bot/internal/ent/predicate" 12 | "github.com/go-faster/bot/internal/ent/repository" 13 | ) 14 | 15 | // RepositoryDelete is the builder for deleting a Repository entity. 16 | type RepositoryDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *RepositoryMutation 20 | } 21 | 22 | // Where appends a list predicates to the RepositoryDelete builder. 23 | func (rd *RepositoryDelete) Where(ps ...predicate.Repository) *RepositoryDelete { 24 | rd.mutation.Where(ps...) 25 | return rd 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (rd *RepositoryDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, rd.sqlExec, rd.mutation, rd.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (rd *RepositoryDelete) ExecX(ctx context.Context) int { 35 | n, err := rd.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (rd *RepositoryDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(repository.Table, sqlgraph.NewFieldSpec(repository.FieldID, field.TypeInt64)) 44 | if ps := rd.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, rd.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | rd.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // RepositoryDeleteOne is the builder for deleting a single Repository entity. 60 | type RepositoryDeleteOne struct { 61 | rd *RepositoryDelete 62 | } 63 | 64 | // Where appends a list predicates to the RepositoryDelete builder. 65 | func (rdo *RepositoryDeleteOne) Where(ps ...predicate.Repository) *RepositoryDeleteOne { 66 | rdo.rd.mutation.Where(ps...) 67 | return rdo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (rdo *RepositoryDeleteOne) Exec(ctx context.Context) error { 72 | n, err := rdo.rd.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{repository.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (rdo *RepositoryDeleteOne) ExecX(ctx context.Context) { 85 | if err := rdo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/ent/runtime/runtime.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package runtime 4 | 5 | // The schema-stitching logic is generated in github.com/go-faster/bot/internal/ent/runtime.go 6 | 7 | const ( 8 | Version = "v0.14.4" // Version of ent codegen. 9 | Sum = "h1:/DhDraSLXIkBhyiVoJeSshr4ZYi7femzhj6/TckzZuI=" // Sum of ent codegen. 10 | ) 11 | -------------------------------------------------------------------------------- /internal/ent/schema/check.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/field" 6 | "entgo.io/ent/schema/index" 7 | ) 8 | 9 | // Check for workflow. 10 | // 11 | // https://docs.github.com/webhooks-and-events/webhooks/webhook-events-and-payloads#check_run 12 | type Check struct { 13 | ent.Schema 14 | } 15 | 16 | func (Check) Fields() []ent.Field { 17 | return []ent.Field{ 18 | field.Int64("id").Comment("Value of check_run.id"), 19 | field.Int64("repo_id").Comment("Repository id"), 20 | field.Int("pull_request_id").Comment("Pull request id"), 21 | field.String("name").Comment("Name of check_run"), 22 | field.String("status").Comment(`The phase of the lifecycle that the check is currently in. Can be one of: queued, in_progress, completed, pending`), 23 | field.String("conclusion").Optional().Comment(`The final conclusion of the check. Can be one of: waiting, pending, startup_failure, stale, success, failure, neutral, cancelled, skipped, timed_out, action_required, null`), 24 | } 25 | } 26 | 27 | func (Check) Indexes() []ent.Index { 28 | return []ent.Index{ 29 | index.Fields("repo_id", "pull_request_id", "id"). 30 | Unique(), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/ent/schema/commit.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/edge" 6 | "entgo.io/ent/schema/field" 7 | ) 8 | 9 | type GitCommit struct { 10 | ent.Schema 11 | } 12 | 13 | // Fields of the GitCommit. 14 | func (GitCommit) Fields() []ent.Field { 15 | return []ent.Field{ 16 | field.String("id").StorageKey("sha").Immutable().Unique().Comment("GitCommit SHA."), 17 | field.String("message").Comment("GitCommit message."), 18 | field.String("author_login").Comment("GitCommit author."), 19 | field.Int64("author_id").Comment("GitCommit author ID."), 20 | field.Time("date").Comment("GitCommit date."), 21 | } 22 | } 23 | 24 | func (GitCommit) Edges() []ent.Edge { 25 | return []ent.Edge{ 26 | edge.From("repository", Repository.Type).Ref("commits").Unique().Comment("GitHub Repository."), 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/ent/schema/gpt_dialog.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "time" 5 | 6 | "entgo.io/ent" 7 | "entgo.io/ent/schema/field" 8 | "entgo.io/ent/schema/index" 9 | ) 10 | 11 | type GPTDialog struct { 12 | ent.Schema 13 | } 14 | 15 | func (GPTDialog) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.String("peer_id").Immutable(). 18 | Comment("Peer ID"), 19 | field.Int("prompt_msg_id"). 20 | Comment("Telegram message id of prompt message."), 21 | field.String("prompt_msg"). 22 | Comment("Prompt message."), 23 | field.Int("gpt_msg_id"). 24 | Comment("Telegram message id of sent message."), 25 | field.String("gpt_msg"). 26 | Comment("AI-generated message. Does not include prompt."), 27 | field.Int("thread_top_msg_id").Optional(). 28 | Comment("Telegram thread's top message id."), 29 | field.Time("created_at").Default(time.Now).Immutable(). 30 | Comment("Message generation time. To simplify cleanup."), 31 | } 32 | } 33 | 34 | func (GPTDialog) Indexes() []ent.Index { 35 | return []ent.Index{ 36 | // In order to find all thread messages. 37 | index.Fields("peer_id", "thread_top_msg_id"), 38 | } 39 | } 40 | 41 | func (GPTDialog) Edges() []ent.Edge { 42 | return []ent.Edge{} 43 | } 44 | -------------------------------------------------------------------------------- /internal/ent/schema/organization.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/edge" 6 | "entgo.io/ent/schema/field" 7 | ) 8 | 9 | type Organization struct { 10 | ent.Schema 11 | } 12 | 13 | func (Organization) Fields() []ent.Field { 14 | return []ent.Field{ 15 | field.Int64("id").Unique().Immutable().Comment("GitHub organization ID."), 16 | field.String("name").Comment("GitHub organization name."), 17 | field.String("html_url").Comment("GitHub organization URL.").Optional(), 18 | } 19 | } 20 | 21 | func (Organization) Edges() []ent.Edge { 22 | return []ent.Edge{ 23 | edge.To("repositories", Repository.Type).Comment("GitHub repositories."), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/ent/schema/repository.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/edge" 6 | "entgo.io/ent/schema/field" 7 | ) 8 | 9 | type Repository struct { 10 | ent.Schema 11 | } 12 | 13 | func (Repository) Fields() []ent.Field { 14 | return []ent.Field{ 15 | field.Int64("id").Unique().Immutable().Comment("GitHub repository ID."), 16 | field.String("name").Comment("GitHub repository name."), 17 | field.String("full_name").Comment("GitHub repository full name.").Unique(), 18 | field.String("html_url").Comment("GitHub repository URL.").Optional(), 19 | field.String("description").Default("").Comment("GitHub repository description."), 20 | field.Time("last_pushed_at").Optional(), 21 | field.Time("last_event_at").Optional(), 22 | } 23 | } 24 | 25 | func (Repository) Indexes() []ent.Index { 26 | return []ent.Index{} 27 | } 28 | 29 | func (Repository) Edges() []ent.Edge { 30 | return []ent.Edge{ 31 | edge.From("organization", Organization.Type).Ref("repositories").Unique().Comment("GitHub organization."), 32 | edge.To("commits", GitCommit.Type).Comment("Commits."), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/ent/schema/session.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/field" 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type TelegramSession struct { 10 | ent.Schema 11 | } 12 | 13 | func (TelegramSession) Fields() []ent.Field { 14 | return []ent.Field{ 15 | field.UUID("id", uuid.New()), 16 | field.Bytes("data"), 17 | } 18 | } 19 | 20 | func (TelegramSession) Edges() []ent.Edge { 21 | return []ent.Edge{} 22 | } 23 | -------------------------------------------------------------------------------- /internal/ent/schema/state.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/field" 6 | "entgo.io/ent/schema/index" 7 | ) 8 | 9 | // LastChannelMessage holds the last message ID of Telegram channel. 10 | // 11 | // We use it to compute how many messages were sent between PR event notification 12 | // and last messsage, since Telegram does not allow to bots to query messages in a channel. 13 | // 14 | // The number of messages is used to find out if old event notification is out of context 15 | // and we should send a new message for a new event instead of editing. 16 | type LastChannelMessage struct { 17 | ent.Schema 18 | } 19 | 20 | func (LastChannelMessage) Fields() []ent.Field { 21 | return []ent.Field{ 22 | field.Int64("id").Unique().Immutable().Comment("Telegram channel ID."), 23 | field.Int("message_id").Comment("Telegram message ID of last observed message in channel."), 24 | } 25 | } 26 | 27 | func (LastChannelMessage) Edges() []ent.Edge { 28 | return []ent.Edge{} 29 | } 30 | 31 | type PRNotification struct { 32 | ent.Schema 33 | } 34 | 35 | func (PRNotification) Fields() []ent.Field { 36 | return []ent.Field{ 37 | field.Int64("repo_id").Comment("Github repository ID."), 38 | field.Int("pull_request_id").Comment("Pull request number."), 39 | field.String("pull_request_title").Default("").Comment("Pull request title."), 40 | field.String("pull_request_body").Default("").Comment("Pull request body."), 41 | field.String("pull_request_author_login").Default("").Comment("Pull request author's login."), 42 | // TODO(tdakkota): store notify peer_id. 43 | field.Int("message_id").Comment("Telegram message ID. Belongs to notify channel."), 44 | } 45 | } 46 | 47 | func (PRNotification) Indexes() []ent.Index { 48 | return []ent.Index{ 49 | index.Fields("repo_id", "pull_request_id"). 50 | Unique(), 51 | } 52 | } 53 | 54 | func (PRNotification) Edges() []ent.Edge { 55 | return []ent.Edge{} 56 | } 57 | -------------------------------------------------------------------------------- /internal/ent/schema/telegram_state.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/edge" 6 | "entgo.io/ent/schema/field" 7 | "entgo.io/ent/schema/index" 8 | ) 9 | 10 | type TelegramUserState struct { 11 | ent.Schema 12 | } 13 | 14 | func (TelegramUserState) Fields() []ent.Field { 15 | return []ent.Field{ 16 | field.Int64("id").Unique().Comment("User ID"), 17 | field.Int("qts").Default(0), 18 | field.Int("pts").Default(0), 19 | field.Int("date").Default(0), 20 | field.Int("seq").Default(0), 21 | } 22 | } 23 | 24 | func (TelegramUserState) Edges() []ent.Edge { 25 | return []ent.Edge{ 26 | edge.To("channels", TelegramChannelState.Type), 27 | } 28 | } 29 | 30 | type TelegramChannelState struct { 31 | ent.Schema 32 | } 33 | 34 | func (TelegramChannelState) Fields() []ent.Field { 35 | return []ent.Field{ 36 | field.Int64("channel_id").Comment("Channel id"), 37 | field.Int64("user_id").Comment("User id"), 38 | field.Int("pts").Default(0), 39 | } 40 | } 41 | 42 | func (TelegramChannelState) Indexes() []ent.Index { 43 | return []ent.Index{ 44 | index.Fields("user_id", "channel_id").Unique(), 45 | } 46 | } 47 | 48 | func (TelegramChannelState) Edges() []ent.Edge { 49 | return []ent.Edge{ 50 | edge.From("user", TelegramUserState.Type). 51 | Ref("channels"). 52 | Field("user_id"). 53 | Unique(). 54 | Required(), 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/ent/schema/user.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/field" 6 | ) 7 | 8 | // User holds the schema definition for the User entity. 9 | type User struct { 10 | ent.Schema 11 | } 12 | 13 | // Fields of the User. 14 | func (User) Fields() []ent.Field { 15 | return []ent.Field{ 16 | field.Int64("id").Unique().Immutable(), 17 | field.String("username"), 18 | field.String("first_name"), 19 | field.String("last_name"), 20 | field.String("github_token").Optional().Comment("PAT"), 21 | } 22 | } 23 | 24 | // Edges of the User. 25 | func (User) Edges() []ent.Edge { 26 | return []ent.Edge{} 27 | } 28 | -------------------------------------------------------------------------------- /internal/ent/telegramchannelstate/telegramchannelstate.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package telegramchannelstate 4 | 5 | import ( 6 | "entgo.io/ent/dialect/sql" 7 | "entgo.io/ent/dialect/sql/sqlgraph" 8 | ) 9 | 10 | const ( 11 | // Label holds the string label denoting the telegramchannelstate type in the database. 12 | Label = "telegram_channel_state" 13 | // FieldID holds the string denoting the id field in the database. 14 | FieldID = "id" 15 | // FieldChannelID holds the string denoting the channel_id field in the database. 16 | FieldChannelID = "channel_id" 17 | // FieldUserID holds the string denoting the user_id field in the database. 18 | FieldUserID = "user_id" 19 | // FieldPts holds the string denoting the pts field in the database. 20 | FieldPts = "pts" 21 | // EdgeUser holds the string denoting the user edge name in mutations. 22 | EdgeUser = "user" 23 | // Table holds the table name of the telegramchannelstate in the database. 24 | Table = "telegram_channel_states" 25 | // UserTable is the table that holds the user relation/edge. 26 | UserTable = "telegram_channel_states" 27 | // UserInverseTable is the table name for the TelegramUserState entity. 28 | // It exists in this package in order to avoid circular dependency with the "telegramuserstate" package. 29 | UserInverseTable = "telegram_user_states" 30 | // UserColumn is the table column denoting the user relation/edge. 31 | UserColumn = "user_id" 32 | ) 33 | 34 | // Columns holds all SQL columns for telegramchannelstate fields. 35 | var Columns = []string{ 36 | FieldID, 37 | FieldChannelID, 38 | FieldUserID, 39 | FieldPts, 40 | } 41 | 42 | // ValidColumn reports if the column name is valid (part of the table columns). 43 | func ValidColumn(column string) bool { 44 | for i := range Columns { 45 | if column == Columns[i] { 46 | return true 47 | } 48 | } 49 | return false 50 | } 51 | 52 | var ( 53 | // DefaultPts holds the default value on creation for the "pts" field. 54 | DefaultPts int 55 | ) 56 | 57 | // OrderOption defines the ordering options for the TelegramChannelState queries. 58 | type OrderOption func(*sql.Selector) 59 | 60 | // ByID orders the results by the id field. 61 | func ByID(opts ...sql.OrderTermOption) OrderOption { 62 | return sql.OrderByField(FieldID, opts...).ToFunc() 63 | } 64 | 65 | // ByChannelID orders the results by the channel_id field. 66 | func ByChannelID(opts ...sql.OrderTermOption) OrderOption { 67 | return sql.OrderByField(FieldChannelID, opts...).ToFunc() 68 | } 69 | 70 | // ByUserID orders the results by the user_id field. 71 | func ByUserID(opts ...sql.OrderTermOption) OrderOption { 72 | return sql.OrderByField(FieldUserID, opts...).ToFunc() 73 | } 74 | 75 | // ByPts orders the results by the pts field. 76 | func ByPts(opts ...sql.OrderTermOption) OrderOption { 77 | return sql.OrderByField(FieldPts, opts...).ToFunc() 78 | } 79 | 80 | // ByUserField orders the results by user field. 81 | func ByUserField(field string, opts ...sql.OrderTermOption) OrderOption { 82 | return func(s *sql.Selector) { 83 | sqlgraph.OrderByNeighborTerms(s, newUserStep(), sql.OrderByField(field, opts...)) 84 | } 85 | } 86 | func newUserStep() *sqlgraph.Step { 87 | return sqlgraph.NewStep( 88 | sqlgraph.From(Table, FieldID), 89 | sqlgraph.To(UserInverseTable, FieldID), 90 | sqlgraph.Edge(sqlgraph.M2O, true, UserTable, UserColumn), 91 | ) 92 | } 93 | -------------------------------------------------------------------------------- /internal/ent/telegramchannelstate_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/go-faster/bot/internal/ent/predicate" 12 | "github.com/go-faster/bot/internal/ent/telegramchannelstate" 13 | ) 14 | 15 | // TelegramChannelStateDelete is the builder for deleting a TelegramChannelState entity. 16 | type TelegramChannelStateDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *TelegramChannelStateMutation 20 | } 21 | 22 | // Where appends a list predicates to the TelegramChannelStateDelete builder. 23 | func (tcsd *TelegramChannelStateDelete) Where(ps ...predicate.TelegramChannelState) *TelegramChannelStateDelete { 24 | tcsd.mutation.Where(ps...) 25 | return tcsd 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (tcsd *TelegramChannelStateDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, tcsd.sqlExec, tcsd.mutation, tcsd.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (tcsd *TelegramChannelStateDelete) ExecX(ctx context.Context) int { 35 | n, err := tcsd.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (tcsd *TelegramChannelStateDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(telegramchannelstate.Table, sqlgraph.NewFieldSpec(telegramchannelstate.FieldID, field.TypeInt)) 44 | if ps := tcsd.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, tcsd.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | tcsd.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // TelegramChannelStateDeleteOne is the builder for deleting a single TelegramChannelState entity. 60 | type TelegramChannelStateDeleteOne struct { 61 | tcsd *TelegramChannelStateDelete 62 | } 63 | 64 | // Where appends a list predicates to the TelegramChannelStateDelete builder. 65 | func (tcsdo *TelegramChannelStateDeleteOne) Where(ps ...predicate.TelegramChannelState) *TelegramChannelStateDeleteOne { 66 | tcsdo.tcsd.mutation.Where(ps...) 67 | return tcsdo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (tcsdo *TelegramChannelStateDeleteOne) Exec(ctx context.Context) error { 72 | n, err := tcsdo.tcsd.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{telegramchannelstate.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (tcsdo *TelegramChannelStateDeleteOne) ExecX(ctx context.Context) { 85 | if err := tcsdo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/ent/telegramsession.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | 9 | "entgo.io/ent" 10 | "entgo.io/ent/dialect/sql" 11 | "github.com/go-faster/bot/internal/ent/telegramsession" 12 | "github.com/google/uuid" 13 | ) 14 | 15 | // TelegramSession is the model entity for the TelegramSession schema. 16 | type TelegramSession struct { 17 | config `json:"-"` 18 | // ID of the ent. 19 | ID uuid.UUID `json:"id,omitempty"` 20 | // Data holds the value of the "data" field. 21 | Data []byte `json:"data,omitempty"` 22 | selectValues sql.SelectValues 23 | } 24 | 25 | // scanValues returns the types for scanning values from sql.Rows. 26 | func (*TelegramSession) scanValues(columns []string) ([]any, error) { 27 | values := make([]any, len(columns)) 28 | for i := range columns { 29 | switch columns[i] { 30 | case telegramsession.FieldData: 31 | values[i] = new([]byte) 32 | case telegramsession.FieldID: 33 | values[i] = new(uuid.UUID) 34 | default: 35 | values[i] = new(sql.UnknownType) 36 | } 37 | } 38 | return values, nil 39 | } 40 | 41 | // assignValues assigns the values that were returned from sql.Rows (after scanning) 42 | // to the TelegramSession fields. 43 | func (ts *TelegramSession) assignValues(columns []string, values []any) error { 44 | if m, n := len(values), len(columns); m < n { 45 | return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) 46 | } 47 | for i := range columns { 48 | switch columns[i] { 49 | case telegramsession.FieldID: 50 | if value, ok := values[i].(*uuid.UUID); !ok { 51 | return fmt.Errorf("unexpected type %T for field id", values[i]) 52 | } else if value != nil { 53 | ts.ID = *value 54 | } 55 | case telegramsession.FieldData: 56 | if value, ok := values[i].(*[]byte); !ok { 57 | return fmt.Errorf("unexpected type %T for field data", values[i]) 58 | } else if value != nil { 59 | ts.Data = *value 60 | } 61 | default: 62 | ts.selectValues.Set(columns[i], values[i]) 63 | } 64 | } 65 | return nil 66 | } 67 | 68 | // Value returns the ent.Value that was dynamically selected and assigned to the TelegramSession. 69 | // This includes values selected through modifiers, order, etc. 70 | func (ts *TelegramSession) Value(name string) (ent.Value, error) { 71 | return ts.selectValues.Get(name) 72 | } 73 | 74 | // Update returns a builder for updating this TelegramSession. 75 | // Note that you need to call TelegramSession.Unwrap() before calling this method if this TelegramSession 76 | // was returned from a transaction, and the transaction was committed or rolled back. 77 | func (ts *TelegramSession) Update() *TelegramSessionUpdateOne { 78 | return NewTelegramSessionClient(ts.config).UpdateOne(ts) 79 | } 80 | 81 | // Unwrap unwraps the TelegramSession entity that was returned from a transaction after it was closed, 82 | // so that all future queries will be executed through the driver which created the transaction. 83 | func (ts *TelegramSession) Unwrap() *TelegramSession { 84 | _tx, ok := ts.config.driver.(*txDriver) 85 | if !ok { 86 | panic("ent: TelegramSession is not a transactional entity") 87 | } 88 | ts.config.driver = _tx.drv 89 | return ts 90 | } 91 | 92 | // String implements the fmt.Stringer. 93 | func (ts *TelegramSession) String() string { 94 | var builder strings.Builder 95 | builder.WriteString("TelegramSession(") 96 | builder.WriteString(fmt.Sprintf("id=%v, ", ts.ID)) 97 | builder.WriteString("data=") 98 | builder.WriteString(fmt.Sprintf("%v", ts.Data)) 99 | builder.WriteByte(')') 100 | return builder.String() 101 | } 102 | 103 | // TelegramSessions is a parsable slice of TelegramSession. 104 | type TelegramSessions []*TelegramSession 105 | -------------------------------------------------------------------------------- /internal/ent/telegramsession/telegramsession.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package telegramsession 4 | 5 | import ( 6 | "entgo.io/ent/dialect/sql" 7 | ) 8 | 9 | const ( 10 | // Label holds the string label denoting the telegramsession type in the database. 11 | Label = "telegram_session" 12 | // FieldID holds the string denoting the id field in the database. 13 | FieldID = "id" 14 | // FieldData holds the string denoting the data field in the database. 15 | FieldData = "data" 16 | // Table holds the table name of the telegramsession in the database. 17 | Table = "telegram_sessions" 18 | ) 19 | 20 | // Columns holds all SQL columns for telegramsession fields. 21 | var Columns = []string{ 22 | FieldID, 23 | FieldData, 24 | } 25 | 26 | // ValidColumn reports if the column name is valid (part of the table columns). 27 | func ValidColumn(column string) bool { 28 | for i := range Columns { 29 | if column == Columns[i] { 30 | return true 31 | } 32 | } 33 | return false 34 | } 35 | 36 | // OrderOption defines the ordering options for the TelegramSession queries. 37 | type OrderOption func(*sql.Selector) 38 | 39 | // ByID orders the results by the id field. 40 | func ByID(opts ...sql.OrderTermOption) OrderOption { 41 | return sql.OrderByField(FieldID, opts...).ToFunc() 42 | } 43 | -------------------------------------------------------------------------------- /internal/ent/telegramsession_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/go-faster/bot/internal/ent/predicate" 12 | "github.com/go-faster/bot/internal/ent/telegramsession" 13 | ) 14 | 15 | // TelegramSessionDelete is the builder for deleting a TelegramSession entity. 16 | type TelegramSessionDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *TelegramSessionMutation 20 | } 21 | 22 | // Where appends a list predicates to the TelegramSessionDelete builder. 23 | func (tsd *TelegramSessionDelete) Where(ps ...predicate.TelegramSession) *TelegramSessionDelete { 24 | tsd.mutation.Where(ps...) 25 | return tsd 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (tsd *TelegramSessionDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, tsd.sqlExec, tsd.mutation, tsd.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (tsd *TelegramSessionDelete) ExecX(ctx context.Context) int { 35 | n, err := tsd.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (tsd *TelegramSessionDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(telegramsession.Table, sqlgraph.NewFieldSpec(telegramsession.FieldID, field.TypeUUID)) 44 | if ps := tsd.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, tsd.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | tsd.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // TelegramSessionDeleteOne is the builder for deleting a single TelegramSession entity. 60 | type TelegramSessionDeleteOne struct { 61 | tsd *TelegramSessionDelete 62 | } 63 | 64 | // Where appends a list predicates to the TelegramSessionDelete builder. 65 | func (tsdo *TelegramSessionDeleteOne) Where(ps ...predicate.TelegramSession) *TelegramSessionDeleteOne { 66 | tsdo.tsd.mutation.Where(ps...) 67 | return tsdo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (tsdo *TelegramSessionDeleteOne) Exec(ctx context.Context) error { 72 | n, err := tsdo.tsd.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{telegramsession.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (tsdo *TelegramSessionDeleteOne) ExecX(ctx context.Context) { 85 | if err := tsdo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/ent/telegramuserstate_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/go-faster/bot/internal/ent/predicate" 12 | "github.com/go-faster/bot/internal/ent/telegramuserstate" 13 | ) 14 | 15 | // TelegramUserStateDelete is the builder for deleting a TelegramUserState entity. 16 | type TelegramUserStateDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *TelegramUserStateMutation 20 | } 21 | 22 | // Where appends a list predicates to the TelegramUserStateDelete builder. 23 | func (tusd *TelegramUserStateDelete) Where(ps ...predicate.TelegramUserState) *TelegramUserStateDelete { 24 | tusd.mutation.Where(ps...) 25 | return tusd 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (tusd *TelegramUserStateDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, tusd.sqlExec, tusd.mutation, tusd.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (tusd *TelegramUserStateDelete) ExecX(ctx context.Context) int { 35 | n, err := tusd.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (tusd *TelegramUserStateDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(telegramuserstate.Table, sqlgraph.NewFieldSpec(telegramuserstate.FieldID, field.TypeInt64)) 44 | if ps := tusd.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, tusd.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | tusd.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // TelegramUserStateDeleteOne is the builder for deleting a single TelegramUserState entity. 60 | type TelegramUserStateDeleteOne struct { 61 | tusd *TelegramUserStateDelete 62 | } 63 | 64 | // Where appends a list predicates to the TelegramUserStateDelete builder. 65 | func (tusdo *TelegramUserStateDeleteOne) Where(ps ...predicate.TelegramUserState) *TelegramUserStateDeleteOne { 66 | tusdo.tusd.mutation.Where(ps...) 67 | return tusdo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (tusdo *TelegramUserStateDeleteOne) Exec(ctx context.Context) error { 72 | n, err := tusdo.tusd.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{telegramuserstate.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (tusdo *TelegramUserStateDeleteOne) ExecX(ctx context.Context) { 85 | if err := tusdo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/ent/user/user.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package user 4 | 5 | import ( 6 | "entgo.io/ent/dialect/sql" 7 | ) 8 | 9 | const ( 10 | // Label holds the string label denoting the user type in the database. 11 | Label = "user" 12 | // FieldID holds the string denoting the id field in the database. 13 | FieldID = "id" 14 | // FieldUsername holds the string denoting the username field in the database. 15 | FieldUsername = "username" 16 | // FieldFirstName holds the string denoting the first_name field in the database. 17 | FieldFirstName = "first_name" 18 | // FieldLastName holds the string denoting the last_name field in the database. 19 | FieldLastName = "last_name" 20 | // FieldGithubToken holds the string denoting the github_token field in the database. 21 | FieldGithubToken = "github_token" 22 | // Table holds the table name of the user in the database. 23 | Table = "users" 24 | ) 25 | 26 | // Columns holds all SQL columns for user fields. 27 | var Columns = []string{ 28 | FieldID, 29 | FieldUsername, 30 | FieldFirstName, 31 | FieldLastName, 32 | FieldGithubToken, 33 | } 34 | 35 | // ValidColumn reports if the column name is valid (part of the table columns). 36 | func ValidColumn(column string) bool { 37 | for i := range Columns { 38 | if column == Columns[i] { 39 | return true 40 | } 41 | } 42 | return false 43 | } 44 | 45 | // OrderOption defines the ordering options for the User queries. 46 | type OrderOption func(*sql.Selector) 47 | 48 | // ByID orders the results by the id field. 49 | func ByID(opts ...sql.OrderTermOption) OrderOption { 50 | return sql.OrderByField(FieldID, opts...).ToFunc() 51 | } 52 | 53 | // ByUsername orders the results by the username field. 54 | func ByUsername(opts ...sql.OrderTermOption) OrderOption { 55 | return sql.OrderByField(FieldUsername, opts...).ToFunc() 56 | } 57 | 58 | // ByFirstName orders the results by the first_name field. 59 | func ByFirstName(opts ...sql.OrderTermOption) OrderOption { 60 | return sql.OrderByField(FieldFirstName, opts...).ToFunc() 61 | } 62 | 63 | // ByLastName orders the results by the last_name field. 64 | func ByLastName(opts ...sql.OrderTermOption) OrderOption { 65 | return sql.OrderByField(FieldLastName, opts...).ToFunc() 66 | } 67 | 68 | // ByGithubToken orders the results by the github_token field. 69 | func ByGithubToken(opts ...sql.OrderTermOption) OrderOption { 70 | return sql.OrderByField(FieldGithubToken, opts...).ToFunc() 71 | } 72 | -------------------------------------------------------------------------------- /internal/ent/user_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/go-faster/bot/internal/ent/predicate" 12 | "github.com/go-faster/bot/internal/ent/user" 13 | ) 14 | 15 | // UserDelete is the builder for deleting a User entity. 16 | type UserDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *UserMutation 20 | } 21 | 22 | // Where appends a list predicates to the UserDelete builder. 23 | func (ud *UserDelete) Where(ps ...predicate.User) *UserDelete { 24 | ud.mutation.Where(ps...) 25 | return ud 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (ud *UserDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, ud.sqlExec, ud.mutation, ud.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (ud *UserDelete) ExecX(ctx context.Context) int { 35 | n, err := ud.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (ud *UserDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(user.Table, sqlgraph.NewFieldSpec(user.FieldID, field.TypeInt64)) 44 | if ps := ud.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, ud.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | ud.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // UserDeleteOne is the builder for deleting a single User entity. 60 | type UserDeleteOne struct { 61 | ud *UserDelete 62 | } 63 | 64 | // Where appends a list predicates to the UserDelete builder. 65 | func (udo *UserDeleteOne) Where(ps ...predicate.User) *UserDeleteOne { 66 | udo.ud.mutation.Where(ps...) 67 | return udo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (udo *UserDeleteOne) Exec(ctx context.Context) error { 72 | n, err := udo.ud.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{user.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (udo *UserDeleteOne) ExecX(ctx context.Context) { 85 | if err := udo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/entdb/ebtdb.go: -------------------------------------------------------------------------------- 1 | package entdb 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "entgo.io/ent/dialect" 7 | entsql "entgo.io/ent/dialect/sql" 8 | "github.com/go-faster/errors" 9 | _ "github.com/jackc/pgx/v5/stdlib" 10 | 11 | "github.com/go-faster/bot/internal/ent" 12 | ) 13 | 14 | // Open new connection 15 | func Open(uri string) (*ent.Client, error) { 16 | db, err := sql.Open("pgx", uri) 17 | if err != nil { 18 | return nil, errors.Wrap(err, "sql.Open") 19 | } 20 | 21 | // Create an ent.Driver from `db`. 22 | drv := entsql.OpenDB(dialect.Postgres, db) 23 | return ent.NewClient(ent.Driver(drv)), nil 24 | } 25 | -------------------------------------------------------------------------------- /internal/entsession/storage.go: -------------------------------------------------------------------------------- 1 | package entsession 2 | 3 | import ( 4 | "context" 5 | 6 | "entgo.io/ent/dialect/sql" 7 | "github.com/go-faster/errors" 8 | "github.com/google/uuid" 9 | "github.com/gotd/td/session" 10 | "go.opentelemetry.io/otel/trace" 11 | 12 | "github.com/go-faster/bot/internal/ent" 13 | "github.com/go-faster/bot/internal/ent/telegramsession" 14 | ) 15 | 16 | type Storage struct { 17 | UUID uuid.UUID 18 | Database *ent.Client 19 | Tracer trace.Tracer 20 | } 21 | 22 | func (s Storage) LoadSession(ctx context.Context) ([]byte, error) { 23 | ctx, span := s.Tracer.Start(ctx, "LoadSession") 24 | defer span.End() 25 | 26 | list, err := s.Database.TelegramSession.Query(). 27 | Where(telegramsession.ID(s.UUID)). 28 | All(ctx) 29 | if err != nil { 30 | return nil, err 31 | } 32 | for _, v := range list { 33 | return v.Data, nil 34 | } 35 | return nil, session.ErrNotFound 36 | } 37 | 38 | func (s Storage) StoreSession(ctx context.Context, data []byte) error { 39 | ctx, span := s.Tracer.Start(ctx, "StoreSession") 40 | defer span.End() 41 | 42 | if err := s.Database.TelegramSession.Create(). 43 | SetID(s.UUID). 44 | SetData(data). 45 | OnConflict( 46 | sql.ConflictColumns("id"), 47 | sql.ResolveWithNewValues(), 48 | ).UpdateData().Exec(ctx); err != nil { 49 | return errors.Wrap(err, "store session") 50 | } 51 | return nil 52 | } 53 | 54 | var _ session.Storage = (*Storage)(nil) 55 | -------------------------------------------------------------------------------- /internal/gh/_golden/event_wh.json: -------------------------------------------------------------------------------- 1 | { 2 | "sender": { 3 | "id": 866677, 4 | "login": "ernado", 5 | "display_login": "ernado", 6 | "gravatar_id": "", 7 | "url": "https://api.github.com/users/ernado", 8 | "html_url": "https://github.com/ernado", 9 | "avatar_url": "https://avatars.githubusercontent.com/u/866677?" 10 | }, 11 | "action": "opened", 12 | "issue": { 13 | "url": "https://api.github.com/repos/ernado/oss-estimator/issues/14", 14 | "repository_url": "https://api.github.com/repos/ernado/oss-estimator", 15 | "labels_url": "https://api.github.com/repos/ernado/oss-estimator/issues/14/labels{/name}", 16 | "comments_url": "https://api.github.com/repos/ernado/oss-estimator/issues/14/comments", 17 | "events_url": "https://api.github.com/repos/ernado/oss-estimator/issues/14/events", 18 | "html_url": "https://github.com/ernado/oss-estimator/issues/14", 19 | "id": 1637789355, 20 | "node_id": "I_kwDOJGfUlc5hnq6r", 21 | "number": 14, 22 | "title": "test4", 23 | "user": { 24 | "login": "ernado", 25 | "id": 866677, 26 | "node_id": "MDQ6VXNlcjg2NjY3Nw==", 27 | "avatar_url": "https://avatars.githubusercontent.com/u/866677?v=4", 28 | "gravatar_id": "", 29 | "url": "https://api.github.com/users/ernado", 30 | "html_url": "https://github.com/ernado", 31 | "followers_url": "https://api.github.com/users/ernado/followers", 32 | "following_url": "https://api.github.com/users/ernado/following{/other_user}", 33 | "gists_url": "https://api.github.com/users/ernado/gists{/gist_id}", 34 | "starred_url": "https://api.github.com/users/ernado/starred{/owner}{/repo}", 35 | "subscriptions_url": "https://api.github.com/users/ernado/subscriptions", 36 | "organizations_url": "https://api.github.com/users/ernado/orgs", 37 | "repos_url": "https://api.github.com/users/ernado/repos", 38 | "events_url": "https://api.github.com/users/ernado/events{/privacy}", 39 | "received_events_url": "https://api.github.com/users/ernado/received_events", 40 | "type": "User", 41 | "site_admin": false 42 | }, 43 | "labels": [], 44 | "state": "open", 45 | "locked": false, 46 | "assignee": null, 47 | "assignees": [], 48 | "milestone": null, 49 | "comments": 0, 50 | "created_at": "2023-03-23T15:41:09Z", 51 | "updated_at": "2023-03-23T15:41:09Z", 52 | "closed_at": null, 53 | "author_association": "OWNER", 54 | "active_lock_reason": null, 55 | "body": null, 56 | "reactions": { 57 | "url": "https://api.github.com/repos/ernado/oss-estimator/issues/14/reactions", 58 | "total_count": 0, 59 | "+1": 0, 60 | "-1": 0, 61 | "laugh": 0, 62 | "hooray": 0, 63 | "confused": 0, 64 | "heart": 0, 65 | "rocket": 0, 66 | "eyes": 0 67 | }, 68 | "timeline_url": "https://api.github.com/repos/ernado/oss-estimator/issues/14/timeline", 69 | "performed_via_github_app": null, 70 | "state_reason": null 71 | }, 72 | "repository": { 73 | "id": 610784405, 74 | "full_name": "ernado/oss-estimator", 75 | "url": "https://api.github.com/repos/ernado/oss-estimator", 76 | "html_url": "https://github.com/ernado/oss-estimator", 77 | "name": "oss-estimator", 78 | "owner": { 79 | "login": "ernado" 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /internal/gh/_testdata/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "27933275010", 3 | "type": "IssuesEvent", 4 | "actor": { 5 | "id": 866677, 6 | "login": "ernado", 7 | "display_login": "ernado", 8 | "gravatar_id": "", 9 | "url": "https://api.github.com/users/ernado", 10 | "avatar_url": "https://avatars.githubusercontent.com/u/866677?" 11 | }, 12 | "repo": { 13 | "id": 610784405, 14 | "name": "ernado/oss-estimator", 15 | "url": "https://api.github.com/repos/ernado/oss-estimator" 16 | }, 17 | "payload": { 18 | "action": "opened", 19 | "issue": { 20 | "url": "https://api.github.com/repos/ernado/oss-estimator/issues/14", 21 | "repository_url": "https://api.github.com/repos/ernado/oss-estimator", 22 | "labels_url": "https://api.github.com/repos/ernado/oss-estimator/issues/14/labels{/name}", 23 | "comments_url": "https://api.github.com/repos/ernado/oss-estimator/issues/14/comments", 24 | "events_url": "https://api.github.com/repos/ernado/oss-estimator/issues/14/events", 25 | "html_url": "https://github.com/ernado/oss-estimator/issues/14", 26 | "id": 1637789355, 27 | "node_id": "I_kwDOJGfUlc5hnq6r", 28 | "number": 14, 29 | "title": "test4", 30 | "user": { 31 | "login": "ernado", 32 | "id": 866677, 33 | "node_id": "MDQ6VXNlcjg2NjY3Nw==", 34 | "avatar_url": "https://avatars.githubusercontent.com/u/866677?v=4", 35 | "gravatar_id": "", 36 | "url": "https://api.github.com/users/ernado", 37 | "html_url": "https://github.com/ernado", 38 | "followers_url": "https://api.github.com/users/ernado/followers", 39 | "following_url": "https://api.github.com/users/ernado/following{/other_user}", 40 | "gists_url": "https://api.github.com/users/ernado/gists{/gist_id}", 41 | "starred_url": "https://api.github.com/users/ernado/starred{/owner}{/repo}", 42 | "subscriptions_url": "https://api.github.com/users/ernado/subscriptions", 43 | "organizations_url": "https://api.github.com/users/ernado/orgs", 44 | "repos_url": "https://api.github.com/users/ernado/repos", 45 | "events_url": "https://api.github.com/users/ernado/events{/privacy}", 46 | "received_events_url": "https://api.github.com/users/ernado/received_events", 47 | "type": "User", 48 | "site_admin": false 49 | }, 50 | "labels": [], 51 | "state": "open", 52 | "locked": false, 53 | "assignee": null, 54 | "assignees": [], 55 | "milestone": null, 56 | "comments": 0, 57 | "created_at": "2023-03-23T15:41:09Z", 58 | "updated_at": "2023-03-23T15:41:09Z", 59 | "closed_at": null, 60 | "author_association": "OWNER", 61 | "active_lock_reason": null, 62 | "body": null, 63 | "reactions": { 64 | "url": "https://api.github.com/repos/ernado/oss-estimator/issues/14/reactions", 65 | "total_count": 0, 66 | "+1": 0, 67 | "-1": 0, 68 | "laugh": 0, 69 | "hooray": 0, 70 | "confused": 0, 71 | "heart": 0, 72 | "rocket": 0, 73 | "eyes": 0 74 | }, 75 | "timeline_url": "https://api.github.com/repos/ernado/oss-estimator/issues/14/timeline", 76 | "performed_via_github_app": null, 77 | "state_reason": null 78 | } 79 | }, 80 | "public": true, 81 | "created_at": "2023-03-23T15:41:10Z" 82 | } -------------------------------------------------------------------------------- /internal/gh/client.go: -------------------------------------------------------------------------------- 1 | package gh 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/go-faster/errors" 10 | "github.com/go-faster/sdk/zctx" 11 | "github.com/google/go-github/v52/github" 12 | "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 13 | "go.opentelemetry.io/otel/metric" 14 | "go.opentelemetry.io/otel/trace" 15 | "go.uber.org/zap" 16 | "golang.org/x/oauth2" 17 | "golang.org/x/time/rate" 18 | ) 19 | 20 | func (w *Webhook) clientWithToken(token string) *github.Client { 21 | return NewTokenClient(token, w.meterProvider, w.tracerProvider) 22 | } 23 | 24 | type rateLimitedTransport struct { 25 | limiter *rate.Limiter 26 | base http.RoundTripper 27 | } 28 | 29 | func (t *rateLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) { 30 | if err := t.limiter.Wait(req.Context()); err != nil { 31 | return nil, err 32 | } 33 | return t.base.RoundTrip(req) 34 | } 35 | 36 | // NewTokenClient returns new instrumented GitHub client with token and rate limiter. 37 | func NewTokenClient(token string, mp metric.MeterProvider, tp trace.TracerProvider) *github.Client { 38 | ts := oauth2.StaticTokenSource( 39 | &oauth2.Token{AccessToken: token}, 40 | ) 41 | tc := oauth2.NewClient(context.Background(), ts) 42 | tc.Transport = otelhttp.NewTransport(&rateLimitedTransport{ 43 | limiter: rate.NewLimiter(rate.Every(1*time.Second), 3), 44 | base: tc.Transport, 45 | }, 46 | otelhttp.WithTracerProvider(tp), 47 | otelhttp.WithMeterProvider(mp), 48 | ) 49 | return github.NewClient(tc) 50 | } 51 | 52 | // Client returns GitHub client for installation. 53 | func (w *Webhook) Client(ctx context.Context) (*github.Client, error) { 54 | ctx, cancel := context.WithTimeout(ctx, 5*time.Second) 55 | defer cancel() 56 | 57 | tokenKey := fmt.Sprintf("gh:installation:%d:token", w.ghID) 58 | key, err := w.cache.Get(ctx, tokenKey).Result() 59 | if err == nil { 60 | return w.clientWithToken(key), nil 61 | } 62 | 63 | tok, _, err := w.ghClient.Apps.CreateInstallationToken(ctx, w.ghID, &github.InstallationTokenOptions{}) 64 | if err != nil { 65 | return nil, errors.Wrap(err, "create token") 66 | } 67 | 68 | expiration := time.Until(tok.GetExpiresAt().Time) 69 | zctx.From(ctx).Info("Token expires in", 70 | zap.Duration("d", expiration), 71 | ) 72 | offset := time.Minute * 10 73 | if expiration > offset { 74 | // Just to make sure that we will not get expired token. 75 | expiration -= offset 76 | } 77 | if _, err := w.cache.Set(ctx, tokenKey, tok.GetToken(), expiration).Result(); err != nil { 78 | return nil, errors.Wrap(err, "set token") 79 | } 80 | 81 | return w.clientWithToken(tok.GetToken()), nil 82 | } 83 | -------------------------------------------------------------------------------- /internal/gh/doc.go: -------------------------------------------------------------------------------- 1 | // Package gh implements bot command to create Github workflow dispatch events. 2 | package gh 3 | -------------------------------------------------------------------------------- /internal/gh/extract_meta.go: -------------------------------------------------------------------------------- 1 | package gh 2 | 3 | import ( 4 | "github.com/go-faster/jx" 5 | "go.opentelemetry.io/otel/attribute" 6 | "go.uber.org/zap" 7 | ) 8 | 9 | type eventMeta struct { 10 | Organization string // go-faster 11 | OrganizationID int64 // 93744681 12 | 13 | Repository string // bot 14 | RepositoryID int64 // 512150878 15 | RepositoryFullName string // go-faster/bot 16 | } 17 | 18 | func (m *eventMeta) Fields() []zap.Field { 19 | if m == nil { 20 | return nil 21 | } 22 | return []zap.Field{ 23 | zap.String("repo", m.RepositoryFullName), 24 | } 25 | } 26 | 27 | func (m *eventMeta) Attributes() []attribute.KeyValue { 28 | if m == nil { 29 | return nil 30 | } 31 | return []attribute.KeyValue{ 32 | attribute.String("org.name", m.Organization), 33 | attribute.Int64("org.id", m.OrganizationID), 34 | attribute.String("repo.name", m.Repository), 35 | attribute.Int64("repo.id", m.RepositoryID), 36 | attribute.String("repo", m.RepositoryFullName), 37 | } 38 | } 39 | 40 | func extractEventMeta(raw []byte) (*eventMeta, error) { 41 | d := jx.GetDecoder() 42 | defer jx.PutDecoder(d) 43 | 44 | d.ResetBytes(raw) 45 | 46 | var m eventMeta 47 | parseOrg := func(d *jx.Decoder, key []byte) error { 48 | switch string(key) { 49 | case "login": 50 | v, err := d.Str() 51 | if err != nil { 52 | return err 53 | } 54 | m.Organization = v 55 | return nil 56 | case "id": 57 | v, err := d.Int64() 58 | if err != nil { 59 | return err 60 | } 61 | m.OrganizationID = v 62 | return nil 63 | default: 64 | return d.Skip() 65 | } 66 | } 67 | parseRepo := func(d *jx.Decoder, key []byte) error { 68 | switch string(key) { 69 | case "name": 70 | v, err := d.Str() 71 | if err != nil { 72 | return err 73 | } 74 | m.Repository = v 75 | return nil 76 | case "full_name": 77 | v, err := d.Str() 78 | if err != nil { 79 | return err 80 | } 81 | m.RepositoryFullName = v 82 | return nil 83 | case "id": 84 | v, err := d.Int64() 85 | if err != nil { 86 | return err 87 | } 88 | m.RepositoryID = v 89 | return nil 90 | default: 91 | return d.Skip() 92 | } 93 | } 94 | if err := d.ObjBytes(func(d *jx.Decoder, key []byte) error { 95 | switch string(key) { 96 | case "organization": 97 | return d.ObjBytes(parseOrg) 98 | case "repository": 99 | return d.ObjBytes(parseRepo) 100 | default: 101 | return d.Skip() 102 | } 103 | }); err != nil { 104 | return nil, err 105 | } 106 | 107 | return &m, nil 108 | } 109 | -------------------------------------------------------------------------------- /internal/gh/extract_meta_test.go: -------------------------------------------------------------------------------- 1 | package gh 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestExtractMeta(t *testing.T) { 12 | _, _ = extractEventMeta([]byte(`{}`)) 13 | type testCase struct { 14 | name string 15 | output eventMeta 16 | } 17 | for _, tc := range []testCase{ 18 | { 19 | name: "event.status.json", 20 | output: eventMeta{ 21 | Organization: "go-faster", 22 | OrganizationID: 93744681, 23 | Repository: "yaml", 24 | RepositoryID: 512150878, 25 | RepositoryFullName: "go-faster/yaml", 26 | }, 27 | }, 28 | { 29 | name: "event.workflow.job.completed.json", 30 | output: eventMeta{ 31 | Organization: "go-faster", 32 | OrganizationID: 93744681, 33 | Repository: "yaml", 34 | RepositoryID: 512150878, 35 | RepositoryFullName: "go-faster/yaml", 36 | }, 37 | }, 38 | { 39 | name: "event.workflow.run.json", 40 | output: eventMeta{ 41 | Organization: "go-faster", 42 | OrganizationID: 93744681, 43 | Repository: "yaml", 44 | RepositoryID: 512150878, 45 | RepositoryFullName: "go-faster/yaml", 46 | }, 47 | }, 48 | { 49 | name: "event.check.run.completed.json", 50 | output: eventMeta{ 51 | Organization: "go-faster", 52 | OrganizationID: 93744681, 53 | Repository: "yaml", 54 | RepositoryID: 512150878, 55 | RepositoryFullName: "go-faster/yaml", 56 | }, 57 | }, 58 | } { 59 | t.Run(tc.name, func(t *testing.T) { 60 | data, err := os.ReadFile(filepath.Join("_testdata", tc.name)) 61 | require.NoError(t, err, "no file") 62 | v, err := extractEventMeta(data) 63 | require.NoError(t, err, "no error") 64 | require.Equal(t, tc.output, *v, "output") 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /internal/gh/handle_check_run.go: -------------------------------------------------------------------------------- 1 | package gh 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/errors" 7 | "github.com/go-faster/sdk/zctx" 8 | "github.com/google/go-github/v52/github" 9 | "go.opentelemetry.io/otel/attribute" 10 | "go.opentelemetry.io/otel/trace" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | func (w *Webhook) handleCheckRun(ctx context.Context, e *github.CheckRunEvent) error { 15 | _, span := w.tracer.Start(ctx, "handleCheckRun", 16 | trace.WithSpanKind(trace.SpanKindServer), 17 | ) 18 | defer span.End() 19 | 20 | run := e.GetCheckRun() 21 | span.AddEvent("CheckRunEvent", 22 | trace.WithStackTrace(true), 23 | trace.WithAttributes( 24 | attribute.String("action", e.GetAction()), 25 | attribute.String("check_run.name", run.GetName()), 26 | attribute.String("check_run.status", run.GetStatus()), 27 | attribute.String("check_run.conclusion", run.GetConclusion()), 28 | attribute.String("check_run.head_sha", run.GetHeadSHA()), 29 | 30 | attribute.Int64("organization.id", e.GetOrg().GetID()), 31 | attribute.String("organization.login", e.GetOrg().GetLogin()), 32 | attribute.String("repository.full_name", e.GetRepo().GetFullName()), 33 | attribute.Int64("repository.id", e.GetRepo().GetID()), 34 | ), 35 | ) 36 | 37 | ctx = zctx.With(ctx, 38 | zap.String("action", e.GetAction()), 39 | zap.Int64("check_run.id", run.GetID()), 40 | zap.String("check_run.name", run.GetName()), 41 | zap.String("head_sha", run.GetHeadSHA()), 42 | ) 43 | lg := zctx.From(ctx) 44 | 45 | pr, err := w.upsertCheck(ctx, e) 46 | if err != nil { 47 | return errors.Wrap(err, "upsert check") 48 | } 49 | if pr == nil { 50 | // No PR - no update. 51 | lg.Debug("Ignore event: no PR info") 52 | return nil 53 | } 54 | lg.Debug("Emit check_update", 55 | zap.Int("pr.number", pr.GetNumber()), 56 | zap.String("pr.head_sha", pr.GetHead().GetSHA()), 57 | ) 58 | 59 | return w.updater.Emit(PullRequestUpdate{ 60 | Event: "check_update", 61 | Action: "", 62 | Repo: e.GetRepo(), 63 | PR: pr, 64 | Checks: nil, 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /internal/gh/handle_check_suite.go: -------------------------------------------------------------------------------- 1 | package gh 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/sdk/zctx" 7 | "github.com/google/go-github/v52/github" 8 | "go.opentelemetry.io/otel/attribute" 9 | "go.opentelemetry.io/otel/trace" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | func (w *Webhook) handleCheckSuite(ctx context.Context, e *github.CheckSuiteEvent) error { 14 | _, span := w.tracer.Start(ctx, "handleCheckSuite", 15 | trace.WithSpanKind(trace.SpanKindServer), 16 | ) 17 | defer span.End() 18 | 19 | suite := e.GetCheckSuite() 20 | span.AddEvent("CheckSuiteEvent", 21 | trace.WithStackTrace(true), 22 | trace.WithAttributes( 23 | attribute.String("action", e.GetAction()), 24 | attribute.String("check_suite.status", suite.GetStatus()), 25 | attribute.String("check_suite.conclusion", suite.GetConclusion()), 26 | attribute.String("check_suite.head_sha", suite.GetHeadSHA()), 27 | 28 | attribute.Int64("organization.id", e.GetOrg().GetID()), 29 | attribute.String("organization.login", e.GetOrg().GetLogin()), 30 | attribute.String("repository.full_name", e.GetRepo().GetFullName()), 31 | attribute.Int64("repository.id", e.GetRepo().GetID()), 32 | ), 33 | ) 34 | 35 | ctx = zctx.With(ctx, 36 | zap.String("action", e.GetAction()), 37 | zap.Int64("check_suite.id", suite.GetID()), 38 | zap.String("head_sha", suite.GetHeadSHA()), 39 | ) 40 | lg := zctx.From(ctx) 41 | 42 | var pr *github.PullRequest 43 | for _, pr = range suite.PullRequests { 44 | break 45 | } 46 | if pr == nil { 47 | // No PR - no update. 48 | lg.Debug("Ignore event: no PR info") 49 | return nil 50 | } 51 | lg.Debug("Emit check_update", 52 | zap.Int("pr.number", pr.GetNumber()), 53 | zap.String("pr.head_sha", pr.GetHead().GetSHA()), 54 | ) 55 | 56 | return w.updater.Emit(PullRequestUpdate{ 57 | Event: "check_update", 58 | Action: "", 59 | Repo: e.GetRepo(), 60 | PR: pr, 61 | Checks: nil, 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /internal/gh/handle_discussion.go: -------------------------------------------------------------------------------- 1 | package gh 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/go-faster/errors" 8 | "github.com/google/go-github/v52/github" 9 | "github.com/gotd/td/telegram/message" 10 | "github.com/gotd/td/telegram/message/entity" 11 | "github.com/gotd/td/telegram/message/styling" 12 | "go.opentelemetry.io/otel/trace" 13 | ) 14 | 15 | func getDiscussionType(d *github.Discussion) string { 16 | cat := d.GetDiscussionCategory() 17 | emoji := cat.GetEmoji() 18 | if emoji == "" { 19 | return cat.GetName() 20 | } 21 | return cat.GetName() + " " + cat.GetEmoji() 22 | } 23 | 24 | func formatDiscussion(e *github.DiscussionEvent) message.StyledTextOption { 25 | discussion := e.GetDiscussion() 26 | sender := e.GetSender() 27 | formatter := func(eb *entity.Builder) error { 28 | eb.Plain("New ") 29 | eb.Plain(getDiscussionType(discussion)) 30 | eb.Plain(" discussion") 31 | 32 | urlName := fmt.Sprintf(" %s#%d", 33 | e.GetRepo().GetFullName(), 34 | discussion.GetNumber(), 35 | ) 36 | eb.TextURL(urlName, discussion.GetHTMLURL()) 37 | eb.Plain(" by ") 38 | eb.TextURL(sender.GetLogin(), sender.GetHTMLURL()) 39 | eb.Plain("\n\n") 40 | 41 | eb.Italic(discussion.GetTitle()) 42 | eb.Plain("\n\n") 43 | 44 | return nil 45 | } 46 | 47 | return styling.Custom(formatter) 48 | } 49 | 50 | func (w *Webhook) handleDiscussion(ctx context.Context, e *github.DiscussionEvent) error { 51 | ctx, span := w.tracer.Start(ctx, "handleDiscussion", 52 | trace.WithSpanKind(trace.SpanKindServer), 53 | ) 54 | defer span.End() 55 | 56 | if e.GetAction() != "created" { 57 | return nil 58 | } 59 | 60 | p, err := w.notifyPeer(ctx) 61 | if err != nil { 62 | return errors.Wrap(err, "peer") 63 | } 64 | 65 | if _, err := w.sender.To(p).NoWebpage().StyledText(ctx, formatDiscussion(e)); err != nil { 66 | return errors.Wrap(err, "send") 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/gh/handle_issue.go: -------------------------------------------------------------------------------- 1 | package gh 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/go-faster/errors" 8 | "github.com/go-faster/sdk/zctx" 9 | "github.com/google/go-github/v52/github" 10 | "github.com/gotd/td/telegram/message" 11 | "github.com/gotd/td/telegram/message/entity" 12 | "github.com/gotd/td/telegram/message/styling" 13 | "go.opentelemetry.io/otel/trace" 14 | ) 15 | 16 | type issueType string 17 | 18 | const ( 19 | featureRequest issueType = "feature request" 20 | bugReport issueType = "bug report" 21 | plain issueType = "issue" 22 | ) 23 | 24 | func getIssueType(issue *github.Issue) issueType { 25 | for _, label := range issue.Labels { 26 | switch label.GetName() { 27 | case "enhancement": 28 | return featureRequest 29 | case "bug": 30 | return bugReport 31 | } 32 | } 33 | 34 | return plain 35 | } 36 | 37 | func formatIssue(e *github.IssuesEvent) message.StyledTextOption { 38 | issue := e.GetIssue() 39 | user := issue.GetUser() 40 | formatter := func(eb *entity.Builder) error { 41 | eb.Plain("New ") 42 | eb.Plain(string(getIssueType(issue))) 43 | 44 | urlName := fmt.Sprintf(" %s#%d", 45 | e.GetRepo().GetFullName(), 46 | issue.GetNumber(), 47 | ) 48 | eb.TextURL(urlName, issue.GetHTMLURL()) 49 | eb.Plain(" by ") 50 | eb.TextURL(user.GetLogin(), user.GetHTMLURL()) 51 | eb.Plain("\n\n") 52 | 53 | eb.Italic(issue.GetTitle()) 54 | eb.Plain("\n\n") 55 | 56 | length := len(issue.Labels) 57 | if length > 0 { 58 | eb.Italic("Labels: ") 59 | 60 | for idx, label := range issue.Labels { 61 | switch label.GetName() { 62 | case "": 63 | continue 64 | case "bug": 65 | eb.Bold(label.GetName()) 66 | default: 67 | eb.Italic(label.GetName()) 68 | } 69 | 70 | if idx != length-1 { 71 | eb.Plain(", ") 72 | } 73 | } 74 | } 75 | return nil 76 | } 77 | 78 | return styling.Custom(formatter) 79 | } 80 | 81 | func (w *Webhook) handleIssue(ctx context.Context, e *github.IssuesEvent) error { 82 | ctx, span := w.tracer.Start(ctx, "handleIssue", 83 | trace.WithSpanKind(trace.SpanKindServer), 84 | ) 85 | defer span.End() 86 | 87 | if e.GetAction() != "opened" { 88 | zctx.From(ctx).Info("Ignoring non-opened issue") 89 | return nil 90 | } 91 | 92 | p, err := w.notifyPeer(ctx) 93 | if err != nil { 94 | return errors.Wrap(err, "peer") 95 | } 96 | 97 | if _, err := w.sender.To(p).NoWebpage(). 98 | StyledText(ctx, formatIssue(e)); err != nil { 99 | return errors.Wrap(err, "send") 100 | } 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /internal/gh/handle_pr_test.go: -------------------------------------------------------------------------------- 1 | package gh 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_generateChecksStatus(t *testing.T) { 11 | tests := []struct { 12 | checks []Check 13 | want string 14 | }{ 15 | {nil, "Checks▶"}, 16 | {[]Check{}, "Checks▶"}, 17 | 18 | { 19 | []Check{ 20 | {Status: "created"}, 21 | {Status: "created"}, 22 | {Status: "created"}, 23 | {Status: "completed", Conclusion: "success"}, 24 | }, 25 | "Checks⏳", 26 | }, 27 | { 28 | []Check{ 29 | {Status: "completed", Conclusion: "failure"}, 30 | {Status: "completed", Conclusion: "timed_out"}, 31 | {Status: "completed", Conclusion: "cancelled"}, 32 | {Status: "completed", Conclusion: "success"}, 33 | }, 34 | "Checks❌", 35 | }, 36 | { 37 | []Check{ 38 | {Status: "completed", Conclusion: "success"}, 39 | {Status: "completed", Conclusion: "success"}, 40 | {Status: "completed", Conclusion: "success"}, 41 | }, 42 | "Checks✅", 43 | }, 44 | } 45 | for i, tt := range tests { 46 | tt := tt 47 | t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { 48 | require.Equal(t, tt.want, generateChecksStatus(tt.checks)) 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/gh/handle_release.go: -------------------------------------------------------------------------------- 1 | package gh 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/go-faster/errors" 8 | "github.com/google/go-github/v52/github" 9 | "go.opentelemetry.io/otel/trace" 10 | 11 | "github.com/gotd/td/telegram/message/styling" 12 | ) 13 | 14 | func (w *Webhook) handleRelease(ctx context.Context, e *github.ReleaseEvent) error { 15 | ctx, span := w.tracer.Start(ctx, "handleRelease", 16 | trace.WithSpanKind(trace.SpanKindServer), 17 | ) 18 | defer span.End() 19 | 20 | if e.GetAction() != "published" { 21 | return nil 22 | } 23 | 24 | p, err := w.notifyPeer(ctx) 25 | if err != nil { 26 | return errors.Wrap(err, "peer") 27 | } 28 | 29 | if _, err := w.sender.To(p).StyledText(ctx, 30 | styling.Plain("New release: "), 31 | styling.TextURL(e.GetRelease().GetTagName(), e.GetRelease().GetHTMLURL()), 32 | styling.Plain(fmt.Sprintf(" for %s", e.GetRepo().GetFullName())), 33 | ); err != nil { 34 | return errors.Wrap(err, "send") 35 | } 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /internal/gh/handle_repo.go: -------------------------------------------------------------------------------- 1 | package gh 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/errors" 7 | "github.com/go-faster/sdk/zctx" 8 | "github.com/google/go-github/v52/github" 9 | "go.opentelemetry.io/otel/trace" 10 | "go.uber.org/zap" 11 | 12 | "github.com/gotd/td/telegram/message/styling" 13 | ) 14 | 15 | func (w *Webhook) handleRepo(ctx context.Context, e *github.RepositoryEvent) error { 16 | ctx, span := w.tracer.Start(ctx, "handleRepo", 17 | trace.WithSpanKind(trace.SpanKindServer), 18 | ) 19 | defer span.End() 20 | 21 | if e.GetRepo().GetPrivate() { 22 | zctx.From(ctx).Info("Private repository", zap.String("repo", e.GetRepo().GetFullName())) 23 | return nil 24 | } 25 | 26 | switch e.GetAction() { 27 | case "created", "publicized": 28 | p, err := w.notifyPeer(ctx) 29 | if err != nil { 30 | return errors.Wrap(err, "peer") 31 | } 32 | 33 | if _, err := w.sender.To(p).StyledText(ctx, 34 | styling.Plain("New repository "), 35 | styling.TextURL(e.GetRepo().GetFullName(), e.GetRepo().GetHTMLURL()), 36 | ); err != nil { 37 | return errors.Wrap(err, "send") 38 | } 39 | 40 | return nil 41 | default: 42 | zctx.From(ctx).Info("Type ignored", zap.String("action", e.GetAction())) 43 | 44 | return nil 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/gh/handle_star.go: -------------------------------------------------------------------------------- 1 | package gh 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/go-faster/errors" 8 | "github.com/go-faster/sdk/zctx" 9 | "github.com/google/go-github/v52/github" 10 | "github.com/gotd/td/telegram/message/styling" 11 | "go.opentelemetry.io/otel/trace" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | func (w *Webhook) handleStar(ctx context.Context, e *github.StarEvent) error { 16 | ctx, span := w.tracer.Start(ctx, "handleStar", 17 | trace.WithSpanKind(trace.SpanKindServer), 18 | ) 19 | defer span.End() 20 | 21 | if a := e.GetAction(); a != "created" { 22 | zctx.From(ctx).Debug("Skipping action", zap.String("action", a)) 23 | return nil 24 | } 25 | p, err := w.notifyPeer(ctx) 26 | if err != nil { 27 | return errors.Wrap(err, "peer") 28 | } 29 | 30 | var options []styling.StyledTextOption 31 | repo := e.GetRepo() 32 | sender := e.GetSender() 33 | options = append(options, 34 | styling.Plain("⭐ "), 35 | styling.TextURL(repo.GetFullName(), repo.GetHTMLURL()), 36 | styling.Bold(fmt.Sprintf(" %d ", repo.GetStargazersCount())), 37 | styling.Plain("by "), 38 | ) 39 | options = append(options, styling.TextURL(sender.GetLogin(), sender.GetHTMLURL())) 40 | if name := sender.GetName(); name != "" { 41 | options = append(options, styling.Plain(fmt.Sprintf(" (%s)", name))) 42 | } 43 | if _, err := w.sender.To(p).NoWebpage().StyledText(ctx, options...); err != nil { 44 | return errors.Wrap(err, "send") 45 | } 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/gh/handle_status.go: -------------------------------------------------------------------------------- 1 | package gh 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/go-faster/errors" 8 | "github.com/go-faster/sdk/zctx" 9 | "github.com/gotd/td/telegram/message" 10 | "github.com/gotd/td/telegram/message/entity" 11 | "github.com/gotd/td/telegram/message/styling" 12 | "github.com/labstack/echo/v4" 13 | "go.opentelemetry.io/otel/attribute" 14 | "go.opentelemetry.io/otel/codes" 15 | "go.opentelemetry.io/otel/trace" 16 | ) 17 | 18 | type StatusWebhook struct { 19 | Meta StatusMeta `json:"meta"` 20 | Page StatusPage `json:"page"` 21 | ComponentUpdate ComponentUpdate `json:"component_update"` 22 | Component StatusComponent `json:"component"` 23 | } 24 | 25 | type StatusMeta struct { 26 | Unsubscribe string `json:"unsubscribe"` 27 | Documentation string `json:"documentation"` 28 | } 29 | 30 | type StatusPage struct { 31 | ID string `json:"id"` 32 | StatusIndicator string `json:"status_indicator"` 33 | StatusDescription string `json:"status_description"` 34 | } 35 | 36 | type ComponentUpdate struct { 37 | CreatedAt time.Time `json:"created_at"` 38 | NewStatus string `json:"new_status"` 39 | OldStatus string `json:"old_status"` 40 | ID string `json:"id"` 41 | ComponentID string `json:"component_id"` 42 | } 43 | 44 | type StatusComponent struct { 45 | CreatedAt time.Time `json:"created_at"` 46 | ID string `json:"id"` 47 | Name string `json:"name"` 48 | Status string `json:"status"` 49 | } 50 | 51 | func formatStatus(s StatusWebhook) message.StyledTextOption { 52 | formatter := func(eb *entity.Builder) error { 53 | eb.Plain("GitHub ") 54 | eb.Bold(s.Component.Name) 55 | eb.Plain(" status changed to ") 56 | eb.Bold(s.Component.Status) 57 | return nil 58 | } 59 | 60 | return styling.Custom(formatter) 61 | } 62 | 63 | func (w *Webhook) handleStatus(c echo.Context) error { 64 | // Handle Atlassian status webhook. 65 | // 66 | // See https://support.atlassian.com/statuspage/docs/enable-webhook-notifications/ 67 | // GitHub status page: https://www.githubstatus.com 68 | 69 | ctx := c.Request().Context() 70 | ctx, span := w.tracer.Start(ctx, "github.status") 71 | defer span.End() 72 | 73 | var s StatusWebhook 74 | if err := c.Bind(&s); err != nil { 75 | span.SetStatus(codes.Error, err.Error()) 76 | span.RecordError(err) 77 | return errors.Wrap(err, "bind") 78 | } 79 | 80 | if s.Component.Name == "" { 81 | // Incident update, ignoring. 82 | zctx.From(ctx).Debug("Ignoring incident update") 83 | return c.String(http.StatusOK, "ok") 84 | } 85 | 86 | span.AddEvent("StatusWebhook", 87 | trace.WithAttributes( 88 | attribute.String("component_name", s.Component.Name), 89 | attribute.String("component_status", s.Component.Status), 90 | ), 91 | ) 92 | 93 | // Notify telegram group. 94 | p, err := w.notifyPeer(ctx) 95 | if err != nil { 96 | return errors.Wrap(err, "peer") 97 | } 98 | if _, err := w.sender.To(p). 99 | NoWebpage(). 100 | StyledText(ctx, formatStatus(s)); err != nil { 101 | return errors.Wrap(err, "send") 102 | } 103 | 104 | return c.String(http.StatusOK, "ok") 105 | } 106 | -------------------------------------------------------------------------------- /internal/gh/handle_workflow_job.go: -------------------------------------------------------------------------------- 1 | package gh 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/google/go-github/v52/github" 7 | "go.opentelemetry.io/otel/attribute" 8 | "go.opentelemetry.io/otel/trace" 9 | ) 10 | 11 | func (w *Webhook) handleWorkflowJob(ctx context.Context, e *github.WorkflowJobEvent) error { 12 | _, span := w.tracer.Start(ctx, "handleWorkflowJob", 13 | trace.WithSpanKind(trace.SpanKindServer), 14 | ) 15 | defer span.End() 16 | 17 | j := e.GetWorkflowJob() 18 | 19 | span.AddEvent("WorkflowJob", 20 | trace.WithStackTrace(true), 21 | trace.WithAttributes( 22 | attribute.String("workflow.name", j.GetWorkflowName()), 23 | attribute.String("name", j.GetName()), 24 | ), 25 | ) 26 | 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/gh/handle_workflow_run.go: -------------------------------------------------------------------------------- 1 | package gh 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/sdk/zctx" 7 | "github.com/google/go-github/v52/github" 8 | "go.opentelemetry.io/otel/attribute" 9 | "go.opentelemetry.io/otel/trace" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | func (w *Webhook) handleWorkflowRun(ctx context.Context, e *github.WorkflowRunEvent) error { 14 | _, span := w.tracer.Start(ctx, "handleWorkflowRun", 15 | trace.WithSpanKind(trace.SpanKindServer), 16 | ) 17 | defer span.End() 18 | 19 | run := e.GetWorkflowRun() 20 | span.AddEvent("WorkflowRun", 21 | trace.WithStackTrace(true), 22 | trace.WithAttributes( 23 | attribute.String("name", run.GetName()), 24 | attribute.String("status", run.GetStatus()), 25 | attribute.String("conclusion", run.GetConclusion()), 26 | attribute.String("head_sha", run.GetHeadSHA()), 27 | attribute.String("event", run.GetEvent()), 28 | 29 | attribute.Int64("organization.id", e.GetOrg().GetID()), 30 | attribute.String("organization.login", e.GetOrg().GetLogin()), 31 | attribute.Int64("repository.id", e.GetRepo().GetID()), 32 | attribute.String("repository.full_name", e.GetRepo().GetFullName()), 33 | ), 34 | ) 35 | 36 | ctx = zctx.With(ctx, 37 | zap.String("action", e.GetAction()), 38 | zap.Int64("workflow_run.id", run.GetID()), 39 | zap.String("workflow_run.name", run.GetName()), 40 | zap.String("head_sha", run.GetHeadSHA()), 41 | ) 42 | lg := zctx.From(ctx) 43 | 44 | var pr *github.PullRequest 45 | for _, pr = range e.GetWorkflowRun().PullRequests { 46 | break 47 | } 48 | if pr == nil { 49 | // No PR - no update. 50 | lg.Debug("Ignore event: no PR info") 51 | return nil 52 | } 53 | lg.Debug("Emit check_update", 54 | zap.Int("pr.number", pr.GetNumber()), 55 | zap.String("pr.head_sha", pr.GetHead().GetSHA()), 56 | ) 57 | 58 | return w.updater.Emit(PullRequestUpdate{ 59 | Event: "check_update", 60 | Action: "", 61 | Repo: e.GetRepo(), 62 | PR: pr, 63 | Checks: nil, 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /internal/gh/main_test.go: -------------------------------------------------------------------------------- 1 | package gh 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/go-faster/sdk/gold" 8 | ) 9 | 10 | func TestMain(m *testing.M) { 11 | // Explicitly registering flags for golden files. 12 | gold.Init() 13 | 14 | os.Exit(m.Run()) 15 | } 16 | -------------------------------------------------------------------------------- /internal/gh/transform.go: -------------------------------------------------------------------------------- 1 | package gh 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | 7 | "github.com/go-faster/errors" 8 | "github.com/go-faster/jx" 9 | ) 10 | 11 | type Event struct { 12 | Type string 13 | RepoName string 14 | RepoID int64 15 | } 16 | 17 | func htmlURL(s string) string { 18 | u, err := url.Parse(s) 19 | if err != nil { 20 | return s 21 | } 22 | u.Host = "github.com" 23 | secondSlash := strings.Index(u.Path[1:], "/") 24 | if secondSlash == -1 { 25 | return s 26 | } 27 | u.Path = u.Path[secondSlash+1:] 28 | return u.String() 29 | } 30 | 31 | func Transform(d *jx.Decoder, e *jx.Encoder) (*Event, error) { 32 | var ( 33 | repoID int64 34 | fullRepoName string 35 | repoURL string 36 | evType string 37 | ) 38 | e.ObjStart() 39 | if err := d.ObjBytes(func(d *jx.Decoder, key []byte) error { 40 | var err error 41 | switch string(key) { 42 | case "actor": 43 | e.FieldStart("sender") 44 | e.ObjStart() 45 | if err := d.ObjBytes(func(d *jx.Decoder, key []byte) error { 46 | switch string(key) { 47 | case "url": 48 | s, err := d.Str() 49 | if err != nil { 50 | return errors.Wrap(err, "url") 51 | } 52 | e.Field("url", func(e *jx.Encoder) { 53 | e.Str(s) 54 | }) 55 | e.Field("html_url", func(e *jx.Encoder) { 56 | e.Str(htmlURL(s)) 57 | }) 58 | return nil 59 | default: 60 | v, err := d.Raw() 61 | if err != nil { 62 | return errors.Wrap(err, "actor") 63 | } 64 | e.Field(string(key), func(e *jx.Encoder) { 65 | e.Raw(v) 66 | }) 67 | return nil 68 | } 69 | }); err != nil { 70 | return err 71 | } 72 | e.ObjEnd() 73 | return nil 74 | case "payload": 75 | return d.ObjBytes(func(d *jx.Decoder, key []byte) error { 76 | v, err := d.Raw() 77 | if err != nil { 78 | return errors.Wrap(err, "payload") 79 | } 80 | e.Field(string(key), func(e *jx.Encoder) { 81 | e.Raw(v) 82 | }) 83 | return nil 84 | }) 85 | case "type": 86 | if evType, err = d.Str(); err != nil { 87 | return errors.Wrap(err, "type") 88 | } 89 | return nil 90 | case "repo": 91 | return d.ObjBytes(func(d *jx.Decoder, key []byte) error { 92 | switch string(key) { 93 | case "id": 94 | if repoID, err = d.Int64(); err != nil { 95 | return errors.Wrap(err, "id") 96 | } 97 | return nil 98 | case "name": 99 | if fullRepoName, err = d.Str(); err != nil { 100 | return errors.Wrap(err, "name") 101 | } 102 | return nil 103 | case "url": 104 | if repoURL, err = d.Str(); err != nil { 105 | return errors.Wrap(err, "url") 106 | } 107 | return nil 108 | default: 109 | return d.Skip() 110 | } 111 | }) 112 | default: 113 | return d.Skip() 114 | } 115 | }); err != nil { 116 | return nil, errors.Wrap(err, "decode") 117 | } 118 | e.Field("repository", func(e *jx.Encoder) { 119 | e.Obj(func(e *jx.Encoder) { 120 | e.Field("id", func(e *jx.Encoder) { 121 | e.Int64(repoID) 122 | }) 123 | 124 | e.Field("full_name", func(e *jx.Encoder) { 125 | e.Str(fullRepoName) 126 | }) 127 | e.Field("url", func(e *jx.Encoder) { 128 | e.Str(repoURL) 129 | }) 130 | e.Field("html_url", func(e *jx.Encoder) { 131 | e.Str(htmlURL(repoURL)) 132 | }) 133 | owner, name, ok := strings.Cut(fullRepoName, "/") 134 | if ok { 135 | e.Field("name", func(e *jx.Encoder) { 136 | e.Str(name) 137 | }) 138 | 139 | e.Field("owner", func(e *jx.Encoder) { 140 | e.Obj(func(e *jx.Encoder) { 141 | e.Field("login", func(e *jx.Encoder) { 142 | e.Str(owner) 143 | }) 144 | }) 145 | }) 146 | } 147 | }) 148 | }) 149 | e.ObjEnd() 150 | d.ResetBytes(e.Bytes()) 151 | 152 | return &Event{ 153 | Type: evType, 154 | RepoName: fullRepoName, 155 | RepoID: repoID, 156 | }, nil 157 | } 158 | -------------------------------------------------------------------------------- /internal/gh/transform_test.go: -------------------------------------------------------------------------------- 1 | package gh 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/go-faster/jx" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/go-faster/sdk/gold" 13 | ) 14 | 15 | func TestTransform(t *testing.T) { 16 | data, err := os.ReadFile(filepath.Join("_testdata", "event.json")) 17 | require.NoErrorf(t, err, "read event.json") 18 | 19 | var ( 20 | e = jx.GetEncoder() 21 | d = jx.GetDecoder() 22 | ) 23 | 24 | e.Reset() 25 | e.SetIdent(2) 26 | d.ResetBytes(data) 27 | v, err := Transform(d, e) 28 | require.NoErrorf(t, err, "transform") 29 | assert.Equal(t, &Event{Type: "IssuesEvent", RepoName: "ernado/oss-estimator", RepoID: 610784405}, v) 30 | 31 | gold.Str(t, gold.NormalizeNewlines(e.String()), "event_wh.json") 32 | } 33 | -------------------------------------------------------------------------------- /internal/gh/updater.go: -------------------------------------------------------------------------------- 1 | package gh 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | 8 | "github.com/go-faster/errors" 9 | "github.com/go-faster/sdk/zctx" 10 | "go.uber.org/zap" 11 | "go.uber.org/zap/zapcore" 12 | 13 | "github.com/go-faster/bot/internal/ent" 14 | ) 15 | 16 | type prKey struct { 17 | // repo is full repo name. 18 | repo string 19 | // number is pr number. 20 | number int 21 | } 22 | 23 | func (k prKey) MarshalLogObject(e zapcore.ObjectEncoder) error { 24 | e.AddString("repo", k.repo) 25 | e.AddInt("pr", k.number) 26 | return nil 27 | } 28 | 29 | type queuedUpdate struct { 30 | Update PullRequestUpdate 31 | Tries int 32 | } 33 | 34 | type updater struct { 35 | w *Webhook 36 | 37 | tick time.Duration 38 | prUpdates map[prKey]*queuedUpdate 39 | checkUpdates map[prKey]*queuedUpdate 40 | updatesMux sync.Mutex 41 | } 42 | 43 | func newUpdater(w *Webhook, tick time.Duration) *updater { 44 | return &updater{ 45 | w: w, 46 | tick: tick, 47 | // TODO(tdakkota): store queue in DB? 48 | prUpdates: map[prKey]*queuedUpdate{}, 49 | checkUpdates: map[prKey]*queuedUpdate{}, 50 | } 51 | } 52 | 53 | var errNoNotificationYet = errors.New("no PR notification message yet") 54 | 55 | func (u *updater) updateOne(ctx context.Context, update PullRequestUpdate) error { 56 | if update.Event == "check_update" { 57 | switch err := u.w.fillPRState(ctx, u.w.db.PRNotification, update.Repo, update.PR); { 58 | case err == nil: 59 | case ent.IsNotFound(err): 60 | return errNoNotificationYet 61 | default: 62 | return errors.Wrap(err, "query cached pr fields") 63 | } 64 | } 65 | 66 | // Do not query checks if PR was merged: we won't send status anyway. 67 | if !update.ActionIn("merged", "closed") && update.Checks == nil { 68 | checks, err := u.w.queryChecks(ctx, update.Repo, update.PR) 69 | if err != nil { 70 | return errors.Wrap(err, "query checks") 71 | } 72 | update.Checks = checks 73 | } 74 | 75 | return u.w.updatePR(ctx, update) 76 | } 77 | 78 | func (u *updater) doUpdate(ctx context.Context) { 79 | u.updatesMux.Lock() 80 | defer u.updatesMux.Unlock() 81 | 82 | applyUpdates := func(updates map[prKey]*queuedUpdate) { 83 | for key, qu := range updates { 84 | ctx := zctx.With(ctx, 85 | zap.String("event", qu.Update.Event), 86 | zap.Inline(key), 87 | ) 88 | 89 | if err := u.updateOne(ctx, qu.Update); err != nil { 90 | lg := zctx.From(ctx) 91 | if !errors.Is(err, errNoNotificationYet) { 92 | lg.Error("PR Update failed", zap.Error(err)) 93 | } else { 94 | lg.Debug("Update checks later: no PR yet") 95 | } 96 | 97 | if qu.Tries < 5 { 98 | qu.Tries++ 99 | continue 100 | } 101 | } 102 | delete(updates, key) 103 | } 104 | } 105 | applyUpdates(u.prUpdates) 106 | applyUpdates(u.checkUpdates) 107 | } 108 | 109 | // Emit enqueues update. 110 | func (u *updater) Emit(update PullRequestUpdate) error { 111 | u.updatesMux.Lock() 112 | defer u.updatesMux.Unlock() 113 | 114 | key := prKey{ 115 | update.Repo.GetFullName(), 116 | update.PR.GetNumber(), 117 | } 118 | emit := &queuedUpdate{ 119 | Update: update, 120 | } 121 | 122 | switch e := update.Event; e { 123 | case "pr_update": 124 | u.prUpdates[key] = emit 125 | return nil 126 | case "check_update": 127 | u.checkUpdates[key] = emit 128 | return nil 129 | default: 130 | return errors.Errorf("unexpected event type %q", e) 131 | } 132 | } 133 | 134 | // Run setups update worker. 135 | func (u *updater) Run(ctx context.Context) error { 136 | t := time.NewTicker(u.tick) 137 | defer t.Stop() 138 | 139 | for { 140 | select { 141 | case <-ctx.Done(): 142 | return ctx.Err() 143 | case <-t.C: 144 | u.doUpdate(ctx) 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /internal/gpt/doc.go: -------------------------------------------------------------------------------- 1 | // Package gpt contains bot command handler which sends text generated by GPT. 2 | package gpt 3 | -------------------------------------------------------------------------------- /internal/gpt/model.go: -------------------------------------------------------------------------------- 1 | package gpt 2 | 3 | import ( 4 | "github.com/go-faster/errors" 5 | "github.com/sashabaranov/go-openai" 6 | "github.com/tiktoken-go/tokenizer" 7 | ) 8 | 9 | const ( 10 | model = openai.GPT4 11 | // Model token limit. 12 | modelTokenLimit = 8_192 13 | // Reserve tokens for model to answer. 14 | modelAnswerReserve = 1_000 15 | tokenizerModel = tokenizer.GPT4 16 | ) 17 | 18 | func cutDialog(tokenizer tokenizer.Codec, limit int, dialog []openai.ChatCompletionMessage) (_ []openai.ChatCompletionMessage, tokens int, _ error) { 19 | for i := len(dialog) - 1; i >= 0; i-- { 20 | msg := dialog[i] 21 | // FIXME(tdakkota): dramatically inefficient. 22 | // Probably we should fork it and optimize it. 23 | _, tks, err := tokenizer.Encode(msg.Content) 24 | if err != nil { 25 | return nil, 0, errors.Wrap(err, "tokenizer error") 26 | } 27 | msgTokens := len(tks) 28 | 29 | if tokens+msgTokens >= limit { 30 | dialog = dialog[i+1:] 31 | break 32 | } 33 | tokens += msgTokens 34 | } 35 | return dialog, tokens, nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/gpt/model_test.go: -------------------------------------------------------------------------------- 1 | package gpt 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/sashabaranov/go-openai" 8 | "github.com/stretchr/testify/require" 9 | "github.com/tiktoken-go/tokenizer" 10 | ) 11 | 12 | func Test_cutDialog(t *testing.T) { 13 | codec, err := tokenizer.ForModel(tokenizerModel) 14 | require.NoError(t, err) 15 | 16 | type dialog = []openai.ChatCompletionMessage 17 | msg := func(content string) (m openai.ChatCompletionMessage) { 18 | m.Content = content 19 | return m 20 | } 21 | 22 | tests := []struct { 23 | limit int 24 | msgs dialog 25 | expect dialog 26 | }{ 27 | { 28 | 10, 29 | dialog{ 30 | msg("Hello. How are you?"), // 6 31 | }, 32 | dialog{ 33 | msg("Hello. How are you?"), 34 | }, 35 | }, 36 | { 37 | 10, 38 | dialog{ 39 | msg("Hello. How are you?"), // 6 40 | msg("Oh, I am fine. And you?"), // 9 41 | msg("Same."), // 2 42 | msg("Something."), // 2 43 | msg("Goodbye."), // 2 44 | }, 45 | dialog{ 46 | msg("Same."), 47 | msg("Something."), 48 | msg("Goodbye."), 49 | }, 50 | }, 51 | } 52 | for i, tt := range tests { 53 | tt := tt 54 | t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { 55 | a := require.New(t) 56 | 57 | got, tokens, err := cutDialog(codec, tt.limit, tt.msgs) 58 | a.NoError(err) 59 | 60 | a.Greater(tt.limit, tokens) 61 | a.Equal(tt.expect, got) 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/gpt/rate_limit.go: -------------------------------------------------------------------------------- 1 | package gpt 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "golang.org/x/time/rate" 8 | ) 9 | 10 | type limiterMap[K comparable] struct { 11 | limiters map[K]*rate.Limiter 12 | create func(K) *rate.Limiter 13 | mux sync.Mutex 14 | } 15 | 16 | func newLimiterMap[K comparable](create func(K) *rate.Limiter) *limiterMap[K] { 17 | return &limiterMap[K]{ 18 | limiters: map[K]*rate.Limiter{}, 19 | create: create, 20 | } 21 | } 22 | 23 | func (m *limiterMap[K]) Allow(k K) (time.Duration, bool) { 24 | // Limit disabled. 25 | if m == nil { 26 | return 0, true 27 | } 28 | 29 | m.mux.Lock() 30 | defer m.mux.Unlock() 31 | 32 | if m.limiters == nil { 33 | m.limiters = map[K]*rate.Limiter{} 34 | } 35 | 36 | limiter, ok := m.limiters[k] 37 | if !ok { 38 | limiter = m.create(k) 39 | m.limiters[k] = limiter 40 | } 41 | 42 | r := limiter.Reserve() 43 | return r.Delay(), r.OK() 44 | } 45 | -------------------------------------------------------------------------------- /internal/gpt/system_prompt.go: -------------------------------------------------------------------------------- 1 | package gpt 2 | 3 | import ( 4 | "text/template" 5 | 6 | "github.com/go-faster/bot/internal/dispatch" 7 | ) 8 | 9 | // PromptUser defines context prompt data of user asking the question. 10 | type PromptUser struct { 11 | Username string 12 | FirstName string 13 | } 14 | 15 | // ContextPromptData is a data structure passed to context prompt template. 16 | type ContextPromptData struct { 17 | Prompter PromptUser 18 | // ChatTitle is a chat title where prompt was generated. 19 | ChatTitle string 20 | } 21 | 22 | var defaultContextPrompt = template.Must(template.New("context_prompt").Parse(`Chat title is: {{ printf "%q" .ChatTitle }} 23 | User's nickname is: {{ printf "%q" .Prompter.Username }} 24 | User's name is: {{ printf "%q" .Prompter.FirstName }} 25 | `)) 26 | 27 | func generateContextPromptData(e dispatch.MessageEvent) (data ContextPromptData) { 28 | if from, ok := e.MessageFrom(); ok { 29 | data.Prompter = PromptUser{ 30 | Username: from.Username, 31 | FirstName: from.Username, 32 | } 33 | } 34 | 35 | if ch, ok := e.Channel(); ok { 36 | data.ChatTitle = ch.Title 37 | } else if ch, ok := e.Chat(); ok { 38 | data.ChatTitle = ch.Title 39 | } else if u, ok := e.User(); ok { 40 | data.ChatTitle = u.FirstName 41 | if last := u.LastName; last != "" { 42 | data.ChatTitle += " " + last 43 | } 44 | } 45 | return data 46 | } 47 | -------------------------------------------------------------------------------- /internal/gpt/system_prompt_test.go: -------------------------------------------------------------------------------- 1 | package gpt 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestDefaultContextPrompt(t *testing.T) { 11 | var sb strings.Builder 12 | require.NoError(t, defaultContextPrompt.Execute(&sb, ContextPromptData{ 13 | Prompter: PromptUser{ 14 | Username: "catent", 15 | FirstName: "Aleksandr", 16 | }, 17 | ChatTitle: "go faster chat", 18 | })) 19 | require.Equal(t, `Chat title is: "go faster chat" 20 | User's nickname is: "catent" 21 | User's name is: "Aleksandr" 22 | `, sb.String()) 23 | } 24 | -------------------------------------------------------------------------------- /internal/oas/oas_labeler_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "context" 7 | 8 | "go.opentelemetry.io/otel/attribute" 9 | ) 10 | 11 | // Labeler is used to allow adding custom attributes to the server request metrics. 12 | type Labeler struct { 13 | attrs []attribute.KeyValue 14 | } 15 | 16 | // Add attributes to the Labeler. 17 | func (l *Labeler) Add(attrs ...attribute.KeyValue) { 18 | l.attrs = append(l.attrs, attrs...) 19 | } 20 | 21 | // AttributeSet returns the attributes added to the Labeler as an attribute.Set. 22 | func (l *Labeler) AttributeSet() attribute.Set { 23 | return attribute.NewSet(l.attrs...) 24 | } 25 | 26 | type labelerContextKey struct{} 27 | 28 | // LabelerFromContext retrieves the Labeler from the provided context, if present. 29 | // 30 | // If no Labeler was found in the provided context a new, empty Labeler is returned and the second 31 | // return value is false. In this case it is safe to use the Labeler but any attributes added to 32 | // it will not be used. 33 | func LabelerFromContext(ctx context.Context) (*Labeler, bool) { 34 | if l, ok := ctx.Value(labelerContextKey{}).(*Labeler); ok { 35 | return l, true 36 | } 37 | return &Labeler{}, false 38 | } 39 | 40 | func contextWithLabeler(ctx context.Context, l *Labeler) context.Context { 41 | return context.WithValue(ctx, labelerContextKey{}, l) 42 | } 43 | -------------------------------------------------------------------------------- /internal/oas/oas_middleware_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "github.com/ogen-go/ogen/middleware" 7 | ) 8 | 9 | // Middleware is middleware type. 10 | type Middleware = middleware.Middleware 11 | -------------------------------------------------------------------------------- /internal/oas/oas_operations_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | // OperationName is the ogen operation name 6 | type OperationName = string 7 | 8 | const ( 9 | GetTelegramBadgeOperation OperationName = "GetTelegramBadge" 10 | GetTelegramOnlineBadgeOperation OperationName = "GetTelegramOnlineBadge" 11 | GithubStatusOperation OperationName = "GithubStatus" 12 | StatusOperation OperationName = "Status" 13 | ) 14 | -------------------------------------------------------------------------------- /internal/oas/oas_request_decoders_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "io" 7 | "mime" 8 | "net/http" 9 | 10 | "github.com/go-faster/errors" 11 | "github.com/go-faster/jx" 12 | 13 | "github.com/ogen-go/ogen/ogenerrors" 14 | "github.com/ogen-go/ogen/validate" 15 | ) 16 | 17 | func (s *Server) decodeGithubStatusRequest(r *http.Request) ( 18 | req StatusNotification, 19 | close func() error, 20 | rerr error, 21 | ) { 22 | var closers []func() error 23 | close = func() error { 24 | var merr error 25 | // Close in reverse order, to match defer behavior. 26 | for i := len(closers) - 1; i >= 0; i-- { 27 | c := closers[i] 28 | merr = errors.Join(merr, c()) 29 | } 30 | return merr 31 | } 32 | defer func() { 33 | if rerr != nil { 34 | rerr = errors.Join(rerr, close()) 35 | } 36 | }() 37 | ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) 38 | if err != nil { 39 | return req, close, errors.Wrap(err, "parse media type") 40 | } 41 | switch { 42 | case ct == "application/json": 43 | if r.ContentLength == 0 { 44 | return req, close, validate.ErrBodyRequired 45 | } 46 | buf, err := io.ReadAll(r.Body) 47 | if err != nil { 48 | return req, close, err 49 | } 50 | 51 | if len(buf) == 0 { 52 | return req, close, validate.ErrBodyRequired 53 | } 54 | 55 | d := jx.DecodeBytes(buf) 56 | 57 | var request StatusNotification 58 | if err := func() error { 59 | if err := request.Decode(d); err != nil { 60 | return err 61 | } 62 | if err := d.Skip(); err != io.EOF { 63 | return errors.New("unexpected trailing data") 64 | } 65 | return nil 66 | }(); err != nil { 67 | err = &ogenerrors.DecodeBodyError{ 68 | ContentType: ct, 69 | Body: buf, 70 | Err: err, 71 | } 72 | return req, close, err 73 | } 74 | if err := func() error { 75 | if err := request.Validate(); err != nil { 76 | return err 77 | } 78 | return nil 79 | }(); err != nil { 80 | return req, close, errors.Wrap(err, "validate") 81 | } 82 | return request, close, nil 83 | default: 84 | return req, close, validate.InvalidContentType(ct) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /internal/oas/oas_request_encoders_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "bytes" 7 | "net/http" 8 | 9 | "github.com/go-faster/jx" 10 | 11 | ht "github.com/ogen-go/ogen/http" 12 | ) 13 | 14 | func encodeGithubStatusRequest( 15 | req StatusNotification, 16 | r *http.Request, 17 | ) error { 18 | const contentType = "application/json" 19 | e := new(jx.Encoder) 20 | { 21 | req.Encode(e) 22 | } 23 | encoded := e.Bytes() 24 | ht.SetBody(r, bytes.NewReader(encoded), contentType) 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /internal/oas/oas_server_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // Handler handles operations described by OpenAPI v3 specification. 10 | type Handler interface { 11 | // GetTelegramBadge implements getTelegramBadge operation. 12 | // 13 | // Get svg badge for telegram group. 14 | // 15 | // GET /badge/telegram/{group_name} 16 | GetTelegramBadge(ctx context.Context, params GetTelegramBadgeParams) (*SVGHeaders, error) 17 | // GetTelegramOnlineBadge implements getTelegramOnlineBadge operation. 18 | // 19 | // GET /badge/telegram/online 20 | GetTelegramOnlineBadge(ctx context.Context, params GetTelegramOnlineBadgeParams) (*SVGHeaders, error) 21 | // GithubStatus implements githubStatus operation. 22 | // 23 | // Https://www.githubstatus.com/ webhook. 24 | // 25 | // POST /github/status 26 | GithubStatus(ctx context.Context, req StatusNotification, params GithubStatusParams) error 27 | // Status implements status operation. 28 | // 29 | // Get status. 30 | // 31 | // GET /status 32 | Status(ctx context.Context) (*Status, error) 33 | // NewError creates *ErrorStatusCode from error returned by handler. 34 | // 35 | // Used for common default response. 36 | NewError(ctx context.Context, err error) *ErrorStatusCode 37 | } 38 | 39 | // Server implements http server based on OpenAPI v3 specification and 40 | // calls Handler to handle requests. 41 | type Server struct { 42 | h Handler 43 | baseServer 44 | } 45 | 46 | // NewServer creates new Server. 47 | func NewServer(h Handler, opts ...ServerOption) (*Server, error) { 48 | s, err := newServerConfig(opts...).baseServer() 49 | if err != nil { 50 | return nil, err 51 | } 52 | return &Server{ 53 | h: h, 54 | baseServer: s, 55 | }, nil 56 | } 57 | -------------------------------------------------------------------------------- /internal/oas/oas_unimplemented_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "context" 7 | 8 | ht "github.com/ogen-go/ogen/http" 9 | ) 10 | 11 | // UnimplementedHandler is no-op Handler which returns http.ErrNotImplemented. 12 | type UnimplementedHandler struct{} 13 | 14 | var _ Handler = UnimplementedHandler{} 15 | 16 | // GetTelegramBadge implements getTelegramBadge operation. 17 | // 18 | // Get svg badge for telegram group. 19 | // 20 | // GET /badge/telegram/{group_name} 21 | func (UnimplementedHandler) GetTelegramBadge(ctx context.Context, params GetTelegramBadgeParams) (r *SVGHeaders, _ error) { 22 | return r, ht.ErrNotImplemented 23 | } 24 | 25 | // GetTelegramOnlineBadge implements getTelegramOnlineBadge operation. 26 | // 27 | // GET /badge/telegram/online 28 | func (UnimplementedHandler) GetTelegramOnlineBadge(ctx context.Context, params GetTelegramOnlineBadgeParams) (r *SVGHeaders, _ error) { 29 | return r, ht.ErrNotImplemented 30 | } 31 | 32 | // GithubStatus implements githubStatus operation. 33 | // 34 | // Https://www.githubstatus.com/ webhook. 35 | // 36 | // POST /github/status 37 | func (UnimplementedHandler) GithubStatus(ctx context.Context, req StatusNotification, params GithubStatusParams) error { 38 | return ht.ErrNotImplemented 39 | } 40 | 41 | // Status implements status operation. 42 | // 43 | // Get status. 44 | // 45 | // GET /status 46 | func (UnimplementedHandler) Status(ctx context.Context) (r *Status, _ error) { 47 | return r, ht.ErrNotImplemented 48 | } 49 | 50 | // NewError creates *ErrorStatusCode from error returned by handler. 51 | // 52 | // Used for common default response. 53 | func (UnimplementedHandler) NewError(ctx context.Context, err error) (r *ErrorStatusCode) { 54 | r = new(ErrorStatusCode) 55 | return r 56 | } 57 | -------------------------------------------------------------------------------- /internal/oas/oas_validators_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "github.com/go-faster/errors" 7 | 8 | "github.com/ogen-go/ogen/validate" 9 | ) 10 | 11 | func (s *Statistics) Validate() error { 12 | if s == nil { 13 | return validate.ErrNilPointer 14 | } 15 | 16 | var failures []validate.FieldError 17 | if err := func() error { 18 | if s.TopUsers == nil { 19 | return errors.New("nil is invalid value") 20 | } 21 | return nil 22 | }(); err != nil { 23 | failures = append(failures, validate.FieldError{ 24 | Name: "top_users", 25 | Error: err, 26 | }) 27 | } 28 | if len(failures) > 0 { 29 | return &validate.Error{Fields: failures} 30 | } 31 | return nil 32 | } 33 | 34 | func (s *Status) Validate() error { 35 | if s == nil { 36 | return validate.ErrNilPointer 37 | } 38 | 39 | var failures []validate.FieldError 40 | if err := func() error { 41 | if err := s.Stat.Validate(); err != nil { 42 | return err 43 | } 44 | return nil 45 | }(); err != nil { 46 | failures = append(failures, validate.FieldError{ 47 | Name: "stat", 48 | Error: err, 49 | }) 50 | } 51 | if len(failures) > 0 { 52 | return &validate.Error{Fields: failures} 53 | } 54 | return nil 55 | } 56 | 57 | func (s StatusNotification) Validate() error { 58 | switch s.Type { 59 | case StatusNotificationIncidentUpdateStatusNotification: 60 | if err := s.StatusNotificationIncidentUpdate.Validate(); err != nil { 61 | return err 62 | } 63 | return nil 64 | case StatusNotificationComponentUpdateStatusNotification: 65 | return nil // no validation needed 66 | default: 67 | return errors.Errorf("invalid type %q", s.Type) 68 | } 69 | } 70 | 71 | func (s *StatusNotificationIncident) Validate() error { 72 | if s == nil { 73 | return validate.ErrNilPointer 74 | } 75 | 76 | var failures []validate.FieldError 77 | if err := func() error { 78 | if s.IncidentUpdates == nil { 79 | return errors.New("nil is invalid value") 80 | } 81 | return nil 82 | }(); err != nil { 83 | failures = append(failures, validate.FieldError{ 84 | Name: "incident_updates", 85 | Error: err, 86 | }) 87 | } 88 | if len(failures) > 0 { 89 | return &validate.Error{Fields: failures} 90 | } 91 | return nil 92 | } 93 | 94 | func (s *StatusNotificationIncidentUpdate) Validate() error { 95 | if s == nil { 96 | return validate.ErrNilPointer 97 | } 98 | 99 | var failures []validate.FieldError 100 | if err := func() error { 101 | if err := s.Incident.Validate(); err != nil { 102 | return err 103 | } 104 | return nil 105 | }(); err != nil { 106 | failures = append(failures, validate.FieldError{ 107 | Name: "incident", 108 | Error: err, 109 | }) 110 | } 111 | if len(failures) > 0 { 112 | return &validate.Error{Fields: failures} 113 | } 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /internal/otelredis/otelredis.go: -------------------------------------------------------------------------------- 1 | package otelredis 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "github.com/redis/go-redis/v9" 8 | "go.opentelemetry.io/otel/trace" 9 | ) 10 | 11 | type Hook struct { 12 | tracer trace.Tracer 13 | } 14 | 15 | func NewHook(tp trace.TracerProvider) *Hook { 16 | return &Hook{ 17 | tracer: tp.Tracer("redis"), 18 | } 19 | } 20 | 21 | func (h Hook) DialHook(next redis.DialHook) redis.DialHook { 22 | return func(ctx context.Context, network, addr string) (net.Conn, error) { 23 | ctx, span := h.tracer.Start(ctx, "redis: Dial", 24 | trace.WithSpanKind(trace.SpanKindClient), 25 | ) 26 | defer span.End() 27 | return next(ctx, network, addr) 28 | } 29 | } 30 | 31 | func (h Hook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { 32 | return func(ctx context.Context, cmd redis.Cmder) error { 33 | ctx, span := h.tracer.Start(ctx, "redis: "+cmd.Name(), 34 | trace.WithSpanKind(trace.SpanKindClient), 35 | ) 36 | defer span.End() 37 | return next(ctx, cmd) 38 | } 39 | } 40 | 41 | func (h Hook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook { 42 | return func(ctx context.Context, cmds []redis.Cmder) error { 43 | ctx, span := h.tracer.Start(ctx, "redis: Pipeline", 44 | trace.WithSpanKind(trace.SpanKindClient), 45 | ) 46 | defer span.End() 47 | return next(ctx, cmds) 48 | } 49 | } 50 | 51 | var _ redis.Hook = (*Hook)(nil) 52 | -------------------------------------------------------------------------------- /internal/stat/stat.go: -------------------------------------------------------------------------------- 1 | package stat 2 | -------------------------------------------------------------------------------- /migrate.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM arigaio/atlas:latest 2 | 3 | COPY migrations migrations -------------------------------------------------------------------------------- /migrations/20230411163934_init.sql: -------------------------------------------------------------------------------- 1 | -- Create "users" table 2 | CREATE TABLE "users" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "name" character varying NOT NULL, PRIMARY KEY ("id")); 3 | -------------------------------------------------------------------------------- /migrations/20230412000613_state.sql: -------------------------------------------------------------------------------- 1 | -- Modify "users" table 2 | ALTER TABLE "users" DROP COLUMN "name", ADD COLUMN "username" character varying NOT NULL, ADD COLUMN "first_name" character varying NOT NULL; 3 | -- Create "last_channel_messages" table 4 | CREATE TABLE "last_channel_messages" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "message_id" bigint NOT NULL, PRIMARY KEY ("id")); 5 | -- Create "pr_notifications" table 6 | CREATE TABLE "pr_notifications" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "repo_id" bigint NOT NULL, "pull_request_id" bigint NOT NULL, "message_id" bigint NOT NULL, PRIMARY KEY ("id")); 7 | -- Create index "prnotification_repo_id_pull_request_id" to table: "pr_notifications" 8 | CREATE UNIQUE INDEX "prnotification_repo_id_pull_request_id" ON "pr_notifications" ("repo_id", "pull_request_id"); 9 | -------------------------------------------------------------------------------- /migrations/20230412002223_session.sql: -------------------------------------------------------------------------------- 1 | -- Create "telegram_sessions" table 2 | CREATE TABLE "telegram_sessions" ("id" uuid NOT NULL, "data" bytea NOT NULL, PRIMARY KEY ("id")); 3 | -------------------------------------------------------------------------------- /migrations/20230412184602_gpt_dialog.sql: -------------------------------------------------------------------------------- 1 | -- Create "gpt_dialogs" table 2 | CREATE TABLE "gpt_dialogs" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "peer_id" character varying NOT NULL, "prompt_msg_id" bigint NOT NULL, "prompt_msg" character varying NOT NULL, "gpt_msg_id" bigint NOT NULL, "gpt_msg" character varying NOT NULL, "thread_top_msg_id" bigint NULL, "created_at" timestamptz NOT NULL, PRIMARY KEY ("id")); 3 | -- Create index "gptdialog_peer_id_thread_top_msg_id" to table: "gpt_dialogs" 4 | CREATE INDEX "gptdialog_peer_id_thread_top_msg_id" ON "gpt_dialogs" ("peer_id", "thread_top_msg_id"); 5 | -------------------------------------------------------------------------------- /migrations/20230413073523_user-token.sql: -------------------------------------------------------------------------------- 1 | -- Modify "users" table 2 | ALTER TABLE "users" ADD COLUMN "last_name" character varying NOT NULL, ADD COLUMN "github_token" character varying NULL; 3 | -------------------------------------------------------------------------------- /migrations/20230414100629_checks.sql: -------------------------------------------------------------------------------- 1 | -- Create "checks" table 2 | CREATE TABLE "checks" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "repo_id" bigint NOT NULL, "name" character varying NOT NULL, "status" character varying NOT NULL, "conclusion" character varying NULL, PRIMARY KEY ("id")); 3 | -- Create index "check_repo_id_id" to table: "checks" 4 | CREATE UNIQUE INDEX "check_repo_id_id" ON "checks" ("repo_id", "id"); 5 | -------------------------------------------------------------------------------- /migrations/20230415000746_check_upd.sql: -------------------------------------------------------------------------------- 1 | -- Drop index "check_repo_id_id" from table: "checks" 2 | DROP INDEX "check_repo_id_id"; 3 | -- Modify "checks" table 4 | ALTER TABLE "checks" ADD COLUMN "pull_request_id" bigint NOT NULL; 5 | -- Create index "check_repo_id_pull_request_id_id" to table: "checks" 6 | CREATE UNIQUE INDEX "check_repo_id_pull_request_id_id" ON "checks" ("repo_id", "pull_request_id", "id"); 7 | -------------------------------------------------------------------------------- /migrations/20230415150924_gaps.sql: -------------------------------------------------------------------------------- 1 | -- Create "telegram_user_states" table 2 | CREATE TABLE "telegram_user_states" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "qts" bigint NOT NULL, "pts" bigint NOT NULL, "date" bigint NOT NULL, "seq" bigint NOT NULL, PRIMARY KEY ("id")); 3 | -- Create "telegram_channel_states" table 4 | CREATE TABLE "telegram_channel_states" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "channel_id" bigint NOT NULL, "pts" bigint NOT NULL, "user_id" bigint NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "telegram_channel_states_telegram_user_states_channels" FOREIGN KEY ("user_id") REFERENCES "telegram_user_states" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION); 5 | -- Create index "telegramchannelstate_user_id_channel_id" to table: "telegram_channel_states" 6 | CREATE UNIQUE INDEX "telegramchannelstate_user_id_channel_id" ON "telegram_channel_states" ("user_id", "channel_id"); 7 | -------------------------------------------------------------------------------- /migrations/20230415175734_gaps.sql: -------------------------------------------------------------------------------- 1 | -- Modify "telegram_channel_states" table 2 | ALTER TABLE "telegram_channel_states" ALTER COLUMN "pts" SET DEFAULT 0; 3 | -- Modify "telegram_user_states" table 4 | ALTER TABLE "telegram_user_states" ALTER COLUMN "qts" SET DEFAULT 0, ALTER COLUMN "pts" SET DEFAULT 0, ALTER COLUMN "date" SET DEFAULT 0, ALTER COLUMN "seq" SET DEFAULT 0; 5 | -------------------------------------------------------------------------------- /migrations/20230417145105_pr_title.sql: -------------------------------------------------------------------------------- 1 | -- Modify "pr_notifications" table 2 | ALTER TABLE "pr_notifications" ADD COLUMN "pull_request_title" character varying NOT NULL DEFAULT '', ADD COLUMN "pull_request_body" character varying NOT NULL DEFAULT '', ADD COLUMN "pull_request_author_login" character varying NOT NULL DEFAULT ''; 3 | -------------------------------------------------------------------------------- /migrations/20230430144340_repository.sql: -------------------------------------------------------------------------------- 1 | -- Create "repositories" table 2 | CREATE TABLE "repositories" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "owner" character varying NOT NULL, "name" character varying NOT NULL, "full_name" character varying NOT NULL, "html_url" character varying NOT NULL, "description" character varying NOT NULL DEFAULT '', "last_pushed_at" timestamptz NULL, "last_event_at" timestamptz NULL, PRIMARY KEY ("id")); 3 | -------------------------------------------------------------------------------- /migrations/20230430150308_organization.sql: -------------------------------------------------------------------------------- 1 | -- Create "organizations" table 2 | CREATE TABLE "organizations" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "name" character varying NOT NULL, "html_url" character varying NULL, PRIMARY KEY ("id")); 3 | -- Modify "repositories" table 4 | ALTER TABLE "repositories" DROP COLUMN "owner", ALTER COLUMN "html_url" DROP NOT NULL, ADD COLUMN "organization_repositories" bigint NULL, ADD CONSTRAINT "repositories_organizations_repositories" FOREIGN KEY ("organization_repositories") REFERENCES "organizations" ("id") ON UPDATE NO ACTION ON DELETE SET NULL; 5 | -- Create index "repositories_full_name_key" to table: "repositories" 6 | CREATE UNIQUE INDEX "repositories_full_name_key" ON "repositories" ("full_name"); 7 | -------------------------------------------------------------------------------- /migrations/20230430162510_commit.sql: -------------------------------------------------------------------------------- 1 | -- Create "git_commits" table 2 | CREATE TABLE "git_commits" ("sha" character varying NOT NULL, "message" character varying NOT NULL, "author_login" character varying NOT NULL, "author_id" bigint NOT NULL, "date" timestamptz NOT NULL, "repository_commits" bigint NULL, PRIMARY KEY ("sha"), CONSTRAINT "git_commits_repositories_commits" FOREIGN KEY ("repository_commits") REFERENCES "repositories" ("id") ON UPDATE NO ACTION ON DELETE SET NULL); 3 | -------------------------------------------------------------------------------- /migrations/atlas.sum: -------------------------------------------------------------------------------- 1 | h1:B1u4Mq2KyrjxlMYElBaIHAdIqXQjYwdEav6zJ82Op1w= 2 | 20230411163934_init.sql h1:Tbl5s0g/laXKKiLW5BfG9FXwdHcHaAg/9T+/7X1aSOA= 3 | 20230412000613_state.sql h1:cHHFpA3UmxU2elLmr4JKe8cZMtfAmJMLPjGC3MSe2tA= 4 | 20230412002223_session.sql h1:WS8XdtkA5b9BwpIZ2kKBdzoFLeqkGXqFrSSkXCaybG8= 5 | 20230412184602_gpt_dialog.sql h1:CCuanyC3MwL4VUDPZsLxbLhXoiWy1k/KdjJfVupua6A= 6 | 20230413073523_user-token.sql h1:4etUolEllnf7ISfvo/nUy20BN6IcvbLcqsKauxkrkJU= 7 | 20230414100629_checks.sql h1:bgRJlMquFf3DVD7hvT3pa4gxVOGM5RyiJpHDjTGNvFo= 8 | 20230415000746_check_upd.sql h1:XNYAv3xhA1Zcp/KHFBgbHuq8udRlbyKspJkVnFJUsL0= 9 | 20230415150924_gaps.sql h1:eqSM6QHDVifIUyju+pRh30h1//b2jkT7of3l23HS39o= 10 | 20230415175734_gaps.sql h1:fd0W2qP+3MwSRWY3OAUKBY70TqVwIkFiT5yMRVKM94k= 11 | 20230417145105_pr_title.sql h1:anMWDs/OLqwFWPSd6BTLXhoBho35LILoPHTTeupZpd0= 12 | 20230430144340_repository.sql h1:NwZ7wqUqMiCUc4vioR9HrIoRL4rn/wKzyWbRPo4Qn8Q= 13 | 20230430150308_organization.sql h1:O+KAnDJQjRZ/lsELIr8gFf+0j+xF17FQ20+D9ysgfNw= 14 | 20230430162510_commit.sql h1:h5cxOBBqUDzFVlj1Uc5z/qTBdnsUIxhBdHY3V6xx09w= 15 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | package bot 4 | 5 | import ( 6 | _ "entgo.io/ent/cmd/ent" 7 | _ "github.com/dmarkham/enumer" 8 | ) 9 | --------------------------------------------------------------------------------