├── .codecov.yml ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── commitlint.yml │ ├── e2e.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 ├── .ogen.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── _oas └── openapi.yaml ├── atlas.hcl ├── cmd └── bot │ ├── app.go │ ├── bot.go │ ├── github.go │ ├── main.go │ ├── search.go │ └── util.go ├── deployment.yml ├── generate.go ├── go.coverage.sh ├── go.mod ├── go.sum ├── go.test.sh ├── integration └── integration_test.go ├── internal ├── api │ └── handler.go ├── app │ ├── file_id.go │ ├── metrics.go │ ├── middleware.go │ ├── middleware_options.go │ ├── stats.go │ └── version.go ├── botapi │ ├── client.go │ ├── message.go │ └── options.go ├── dispatch │ ├── base_event.go │ ├── bot.go │ ├── handle_inline.go │ ├── handle_message.go │ ├── inline.go │ ├── logged.go │ ├── message.go │ ├── message_mux.go │ └── message_mux_test.go ├── docs │ ├── docs.go │ └── storage.go ├── ent │ ├── client.go │ ├── ent.go │ ├── entc.go │ ├── enttest │ │ └── enttest.go │ ├── generate.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 │ ├── predicate │ │ └── predicate.go │ ├── prnotification.go │ ├── prnotification │ │ ├── prnotification.go │ │ └── where.go │ ├── prnotification_create.go │ ├── prnotification_delete.go │ ├── prnotification_query.go │ ├── prnotification_update.go │ ├── runtime.go │ ├── runtime │ │ └── runtime.go │ ├── schema │ │ ├── session.go │ │ ├── state.go │ │ ├── telegram_account.go │ │ └── telegram_state.go │ ├── telegramaccount.go │ ├── telegramaccount │ │ ├── telegramaccount.go │ │ └── where.go │ ├── telegramaccount_create.go │ ├── telegramaccount_delete.go │ ├── telegramaccount_query.go │ ├── telegramaccount_update.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 ├── entdb │ └── ebtdb.go ├── inspect │ ├── doc.go │ ├── formatter.go │ └── inspect.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_security_gen.go │ ├── oas_server_gen.go │ ├── oas_test_examples_gen_test.go │ ├── oas_unimplemented_gen.go │ └── oas_validators_gen.go ├── storage │ ├── hook.go │ └── msg_id.go └── tgmanager │ ├── account.go │ ├── account_test.go │ ├── manager.go │ └── storage.go ├── kubeconfig.gpg ├── kubeconfig.sh ├── migrate.Dockerfile ├── migrations ├── 20241202075819_init.sql ├── 20241208073032_telegram_account.sql ├── 20241208082152_telegram_acc_session.sql ├── 20241208112252_telegram_acc_nillable.sql ├── 20241208112922_telegram_acc_nillable.sql ├── 20241208113242_telegram_acc_rename.sql └── atlas.sum ├── role.yml ├── service.yaml └── tools.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: off 4 | project: 5 | default: 6 | threshold: 5% 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | end_of_line = lf 10 | 11 | [{*.go, go.mod}] 12 | indent_style = tab 13 | indent_size = 4 14 | 15 | [{*.yml,*.yaml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.py] 20 | indent_style = space 21 | indent_size = 4 22 | 23 | # Makefiles always use tabs for indentation 24 | [{Makefile}] 25 | indent_style = tab 26 | -------------------------------------------------------------------------------- /.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/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | name: Commit linter 2 | 3 | on: 4 | pull_request: 5 | 6 | # Common Go workflows from go faster 7 | # See https://github.com/go-faster/x 8 | jobs: 9 | check: 10 | uses: go-faster/x/.github/workflows/commit.yml@main 11 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: e2e 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | with: 16 | submodules: true 17 | 18 | - name: Install Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: stable 22 | cache: false 23 | 24 | - name: Get Go environment 25 | id: go-env 26 | shell: bash 27 | run: | 28 | echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV 29 | echo "modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV 30 | 31 | - name: Set up cache 32 | uses: actions/cache@v4 33 | with: 34 | path: | 35 | ${{ env.cache }} 36 | ${{ env.modcache }} 37 | key: ${{ runner.os }}-${{ runner.arch }}-go-${{ hashFiles('**/go.sum') }} 38 | restore-keys: | 39 | ${{ runner.os }}-${{ runner.arch }}-go- 40 | 41 | - name: Run test 42 | env: 43 | E2E: "1" 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | GITHUB_JOB_ID: ${{ github.job }} 46 | GITHUB_RUN_ID: ${{ github.run_id }} 47 | GITHUB_RUN_ATTEMPT: ${{ github.run_attempt }} 48 | run: go test -timeout 15m -v -run TestIntegration ./... 49 | -------------------------------------------------------------------------------- /.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: ubuntu-latest 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.23.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 120 | /tmp/kubectl -n gotd rollout status deployment/bot --timeout=10m 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 | lint: 15 | uses: go-faster/x/.github/workflows/lint.yml@main 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | echobot 2 | .idea 3 | /bot 4 | cmd/bot/bot 5 | 6 | kubeconfig 7 | 8 | secret.yaml 9 | -------------------------------------------------------------------------------- /.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/deployment.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | namespace: gotd 6 | name: bot 7 | labels: 8 | app.kubernetes.io/name: bot 9 | spec: 10 | strategy: 11 | type: Recreate 12 | replicas: 1 13 | selector: 14 | matchLabels: 15 | app.kubernetes.io/name: bot 16 | template: 17 | metadata: 18 | labels: 19 | app.kubernetes.io/name: bot 20 | service.opentelemetry.io/name: gotd.bot 21 | spec: 22 | volumes: 23 | - name: atlas 24 | secret: 25 | secretName: atlas 26 | initContainers: 27 | - name: migrate 28 | image: ghcr.io/gotd/bot/migrate:main 29 | volumeMounts: 30 | - mountPath: "/root/.config/" 31 | name: atlas 32 | readOnly: true 33 | args: 34 | - --config 35 | - file://root/.config/atlas.hcl 36 | - --env 37 | - prod 38 | - migrate 39 | - apply 40 | resources: 41 | requests: 42 | cpu: 100m 43 | memory: 64M 44 | limits: 45 | cpu: 500m 46 | memory: 128M 47 | containers: 48 | - name: bot 49 | image: ghcr.io/gotd/bot:main 50 | args: 51 | - server 52 | startupProbe: 53 | httpGet: 54 | path: /probe/startup 55 | port: http-bot 56 | initialDelaySeconds: 3 57 | periodSeconds: 5 58 | readinessProbe: 59 | httpGet: 60 | path: /probe/ready 61 | port: http-bot 62 | initialDelaySeconds: 3 63 | periodSeconds: 5 64 | ports: 65 | - containerPort: 8090 66 | protocol: TCP 67 | name: metrics 68 | - containerPort: 8080 69 | protocol: TCP 70 | name: http-bot 71 | - containerPort: 8081 72 | protocol: TCP 73 | name: http-api 74 | resources: 75 | requests: 76 | cpu: 500m 77 | memory: 256M 78 | limits: 79 | cpu: "3" 80 | memory: 2G 81 | env: 82 | - name: GOMEMLIMIT 83 | value: "512MiB" 84 | - name: GOMAXPROCS 85 | value: "3" 86 | - name: OTEL_EXPORTER_OTLP_PROTOCOL 87 | value: "grpc" 88 | - name: OTEL_RESOURCE_ATTRIBUTES 89 | value: "service.name=gotd.bot" 90 | - name: OTEL_LOG_LEVEL 91 | value: "DEBUG" 92 | - name: OTEL_EXPORTER_OTLP_ENDPOINT 93 | value: "http://otel-collector.monitoring.svc.cluster.local:4317" 94 | - name: HOME 95 | value: /cache 96 | - name: HTTP_ADDR 97 | value: 0.0.0.0:8080 98 | - name: TG_NOTIFY_GROUP 99 | value: gotd_deploys 100 | - name: TG_DEPLOY_NOTIFY_GROUP 101 | value: gotd_deploys 102 | - name: BOT_TOKEN 103 | valueFrom: 104 | secretKeyRef: 105 | name: bot 106 | key: BOT_TOKEN 107 | - name: APP_ID 108 | valueFrom: 109 | secretKeyRef: 110 | name: bot 111 | key: APP_ID 112 | - name: APP_HASH 113 | valueFrom: 114 | secretKeyRef: 115 | name: bot 116 | key: APP_HASH 117 | - name: GITHUB_PRIVATE_KEY 118 | valueFrom: 119 | secretKeyRef: 120 | name: bot 121 | key: GITHUB_PRIVATE_KEY 122 | - name: GITHUB_SECRET 123 | valueFrom: 124 | secretKeyRef: 125 | name: bot 126 | key: GITHUB_SECRET 127 | - name: GITHUB_APP_ID 128 | valueFrom: 129 | secretKeyRef: 130 | name: bot 131 | key: GITHUB_APP_ID 132 | - name: GITHUB_CLIENT_ID 133 | valueFrom: 134 | secretKeyRef: 135 | name: bot 136 | key: GITHUB_CLIENT_ID 137 | - name: GITHUB_INSTALLATION_ID 138 | value: "26766968" 139 | - name: DATABASE_URL 140 | valueFrom: 141 | secretKeyRef: 142 | name: bot 143 | key: DATABASE_URL 144 | -------------------------------------------------------------------------------- /.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: gotd 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: gotd 32 | labels: 33 | app.kubernetes.io/name: bot 34 | spec: 35 | ingressClassName: nginx 36 | rules: 37 | - host: bot.gotd.dev 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: gotd 53 | labels: 54 | app.kubernetes.io/name: bot 55 | spec: 56 | ingressClassName: nginx 57 | rules: 58 | - host: api.gotd.dev 59 | http: 60 | paths: 61 | - path: / 62 | pathType: Prefix 63 | backend: 64 | service: 65 | name: bot 66 | port: 67 | name: http-api 68 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Aleksandr Razumov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @./go.test.sh 3 | .PHONY: test 4 | 5 | coverage: 6 | @./go.coverage.sh 7 | .PHONY: coverage 8 | 9 | generate: 10 | go generate 11 | go generate ./... 12 | .PHONY: generate 13 | 14 | build: 15 | CGO_ENABLED=0 go build ./cmd/bot 16 | 17 | check_generated: generate 18 | git diff --exit-code 19 | .PHONY: check_generated 20 | 21 | forward_psql: 22 | kubectl -n faster port-forward svc/postgresql 15432:5432 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bot 2 | 3 | Bot for gotd chats based on [gotd/td](https://github.com/gotd/td). 4 | 5 | ## Goals 6 | 7 | This bot is like a canary node: platform for experiments, testing 8 | and ensuring stability. 9 | 10 | ## Commands 11 | 12 | * `/bot` - answers "What?" 13 | * `/json` - inspects replied message 14 | * `/dice` - sends dice 15 | * `/stat` - prints metrics 16 | 17 | ## Skip deploy 18 | 19 | Add `!skip` to commit message. 20 | 21 | ## Migrations 22 | 23 | ### Add migration 24 | 25 | To add migration named `some-migration-name`: 26 | 27 | ```console 28 | atlas migrate --env dev diff some-migration-name 29 | ``` 30 | 31 | ## Golden files 32 | 33 | In package directory: 34 | 35 | ```console 36 | go test -update 37 | ``` 38 | -------------------------------------------------------------------------------- /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/bot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gotd/bot/internal/app" 7 | "github.com/gotd/bot/internal/dispatch" 8 | "github.com/gotd/bot/internal/inspect" 9 | ) 10 | 11 | func setupBot(a *App) error { 12 | a.mux.HandleFunc("/bot", "Ping bot", func(ctx context.Context, e dispatch.MessageEvent) error { 13 | _, err := e.Reply().Text(ctx, "What?") 14 | return err 15 | }) 16 | a.mux.HandleFunc("/dice", "Send dice", 17 | func(ctx context.Context, e dispatch.MessageEvent) error { 18 | _, err := e.Reply().Dice(ctx) 19 | return err 20 | }) 21 | a.mux.HandleFunc("/darts", "Send darts", 22 | func(ctx context.Context, e dispatch.MessageEvent) error { 23 | _, err := e.Reply().Darts(ctx) 24 | return err 25 | }) 26 | a.mux.HandleFunc("/basketball", "Send basketball", 27 | func(ctx context.Context, e dispatch.MessageEvent) error { 28 | _, err := e.Reply().Basketball(ctx) 29 | return err 30 | }) 31 | a.mux.HandleFunc("/football", "Send football", 32 | func(ctx context.Context, e dispatch.MessageEvent) error { 33 | _, err := e.Reply().Football(ctx) 34 | return err 35 | }) 36 | a.mux.HandleFunc("/casino", "Send casino", 37 | func(ctx context.Context, e dispatch.MessageEvent) error { 38 | _, err := e.Reply().Casino(ctx) 39 | return err 40 | }) 41 | a.mux.HandleFunc("/bowling", "Send bowling", 42 | func(ctx context.Context, e dispatch.MessageEvent) error { 43 | _, err := e.Reply().Bowling(ctx) 44 | return err 45 | }) 46 | 47 | a.mux.Handle("/pp", "Pretty print replied message", inspect.Pretty()) 48 | a.mux.Handle("/json", "Print JSON of replied message", inspect.JSON()) 49 | a.mux.Handle("/stat", "Version", app.NewHandler()) 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /cmd/bot/github.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "net/http" 6 | "os" 7 | "strconv" 8 | 9 | "github.com/bradleyfalzon/ghinstallation" 10 | "github.com/go-faster/errors" 11 | "github.com/google/go-github/v42/github" 12 | ) 13 | 14 | func setupGithub(appID string, httpTransport http.RoundTripper) (*github.Client, error) { 15 | ghAppID, err := strconv.ParseInt(appID, 10, 64) 16 | if err != nil { 17 | return nil, errors.Wrap(err, "GITHUB_APP_ID is invalid") 18 | } 19 | key, err := base64.StdEncoding.DecodeString(os.Getenv("GITHUB_PRIVATE_KEY")) 20 | if err != nil { 21 | return nil, errors.Wrap(err, "GITHUB_PRIVATE_KEY is invalid") 22 | } 23 | ghTransport, err := ghinstallation.NewAppsTransport(httpTransport, ghAppID, key) 24 | if err != nil { 25 | return nil, errors.Wrap(err, "create github transport") 26 | } 27 | return github.NewClient(&http.Client{ 28 | Transport: ghTransport, 29 | }), nil 30 | } 31 | -------------------------------------------------------------------------------- /cmd/bot/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "runtime/debug" 6 | "time" 7 | 8 | "github.com/go-faster/errors" 9 | "github.com/go-faster/sdk/app" 10 | "go.uber.org/zap" 11 | 12 | iapp "github.com/gotd/bot/internal/app" 13 | ) 14 | 15 | func main() { 16 | app.Run(func(ctx context.Context, lg *zap.Logger, m *app.Telemetry) error { 17 | defer func() { 18 | if r := recover(); r != nil { 19 | lg.Error("panic", zap.Any("recover", r)) 20 | debug.PrintStack() 21 | } 22 | lg.Info("Stopping") 23 | <-time.After(time.Second) 24 | }() 25 | mx := &iapp.Metrics{} 26 | { 27 | var err error 28 | meter := m.MeterProvider().Meter("gotd.bot") 29 | if mx.Bytes, err = meter.Int64Counter("gotd.bot.bytes"); err != nil { 30 | return errors.Wrap(err, "bytes") 31 | } 32 | if mx.Messages, err = meter.Int64Counter("gotd.bot.messages"); err != nil { 33 | return errors.Wrap(err, "messages") 34 | } 35 | if mx.Responses, err = meter.Int64Counter("gotd.bot.responses"); err != nil { 36 | return errors.Wrap(err, "responses") 37 | } 38 | } 39 | return runBot(ctx, m, mx, lg.Named("bot")) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /cmd/bot/search.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/blevesearch/bleve/v2" 8 | "github.com/go-faster/errors" 9 | "go.uber.org/multierr" 10 | 11 | "github.com/gotd/getdoc" 12 | "github.com/gotd/tl" 13 | 14 | "github.com/gotd/bot/internal/docs" 15 | ) 16 | 17 | func setupIndex(sessionDir, schemaPath string) (_ *docs.Search, rerr error) { 18 | f, err := os.Open(schemaPath) 19 | if err != nil { 20 | return nil, errors.Wrap(err, "open") 21 | } 22 | defer func() { _ = f.Close() }() 23 | 24 | sch, err := tl.Parse(f) 25 | if err != nil { 26 | return nil, errors.Wrap(err, "parse") 27 | } 28 | 29 | indexPath := filepath.Join(sessionDir, "docs.index") 30 | index, err := bleve.Open(indexPath) 31 | switch { 32 | case errors.Is(err, bleve.ErrorIndexPathDoesNotExist): 33 | index, err = bleve.New(indexPath, bleve.NewIndexMapping()) 34 | if err != nil { 35 | return nil, errors.Wrap(err, "create indexer") 36 | } 37 | case err != nil: 38 | return nil, errors.Wrap(err, "open index") 39 | } 40 | defer func() { 41 | if rerr != nil { 42 | multierr.AppendInto(&rerr, index.Close()) 43 | } 44 | }() 45 | 46 | doc, err := getdoc.Load(getdoc.LayerLatest) 47 | if err != nil { 48 | return nil, errors.Wrap(err, "load docs") 49 | } 50 | 51 | search, err := docs.IndexSchema(index, sch, doc) 52 | if err != nil { 53 | return nil, errors.Wrap(err, "index schema") 54 | } 55 | 56 | return search, nil 57 | } 58 | -------------------------------------------------------------------------------- /cmd/bot/util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | ) 7 | 8 | func tokHash(token string) string { 9 | h := md5.Sum([]byte(token + "gotd-token-salt")) // #nosec 10 | return fmt.Sprintf("%x", h[:5]) 11 | } 12 | -------------------------------------------------------------------------------- /deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | namespace: gotd 5 | name: bot 6 | labels: 7 | app: bot 8 | spec: 9 | strategy: 10 | type: Recreate 11 | replicas: 1 12 | selector: 13 | matchLabels: 14 | app: bot 15 | template: 16 | metadata: 17 | labels: 18 | app: bot 19 | annotations: 20 | prometheus.io/scrape: 'true' 21 | prometheus.io/port: '8090' 22 | spec: 23 | volumes: 24 | - name: cache 25 | emptyDir: {} 26 | containers: 27 | - name: bot 28 | image: ghcr.io/gotd/bot:main 29 | resources: 30 | requests: 31 | cpu: 500m 32 | memory: 256M 33 | limits: 34 | cpu: "2" 35 | memory: 512M 36 | env: 37 | - name: OTEL_EXPORTER_JAEGER_AGENT_HOST 38 | value: jaeger.faster-monitoring.svc.cluster.local 39 | - name: HOME 40 | value: /cache 41 | - name: HTTP_ADDR 42 | value: 0.0.0.0:8080 43 | - name: METRICS_ADDR 44 | value: 0.0.0.0:8090 45 | - name: TG_NOTIFY_GROUP 46 | value: gotd_ru 47 | - name: TG_DEPLOY_NOTIFY_GROUP 48 | value: gotd_test 49 | - name: BOT_TOKEN 50 | valueFrom: 51 | secretKeyRef: 52 | name: bot 53 | key: BOT_TOKEN 54 | - name: APP_ID 55 | valueFrom: 56 | secretKeyRef: 57 | name: bot 58 | key: APP_ID 59 | - name: APP_HASH 60 | valueFrom: 61 | secretKeyRef: 62 | name: bot 63 | key: APP_HASH 64 | - name: GITHUB_PRIVATE_KEY 65 | valueFrom: 66 | secretKeyRef: 67 | name: bot 68 | key: GITHUB_PRIVATE_KEY 69 | - name: GITHUB_SECRET 70 | valueFrom: 71 | secretKeyRef: 72 | name: bot 73 | key: GITHUB_SECRET 74 | - name: GITHUB_APP_ID 75 | valueFrom: 76 | secretKeyRef: 77 | name: bot 78 | key: GITHUB_APP_ID 79 | - name: GITHUB_CLIENT_ID 80 | valueFrom: 81 | secretKeyRef: 82 | name: bot 83 | key: GITHUB_CLIENT_ID 84 | volumeMounts: 85 | - mountPath: /cache 86 | name: cache 87 | -------------------------------------------------------------------------------- /generate.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | //go:generate go run -mod=mod 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 | -------------------------------------------------------------------------------- /integration/integration_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "strconv" 7 | "testing" 8 | "time" 9 | 10 | "github.com/cenkalti/backoff/v4" 11 | "github.com/go-faster/errors" 12 | "github.com/google/uuid" 13 | "github.com/gotd/td/telegram" 14 | "github.com/gotd/td/telegram/auth" 15 | "github.com/gotd/td/telegram/dcs" 16 | "github.com/gotd/td/tg" 17 | "github.com/stretchr/testify/require" 18 | "go.uber.org/zap/zaptest" 19 | 20 | "github.com/gotd/bot/internal/oas" 21 | ) 22 | 23 | type securitySource struct{} 24 | 25 | func (s securitySource) TokenAuth(ctx context.Context, operationName oas.OperationName) (oas.TokenAuth, error) { 26 | return oas.TokenAuth{ 27 | APIKey: os.Getenv("GITHUB_TOKEN"), 28 | }, nil 29 | } 30 | 31 | // terminalAuth implements auth.UserAuthenticator prompting the terminal for 32 | // input. 33 | type codeAuth struct { 34 | phone string 35 | token uuid.UUID 36 | client *oas.Client 37 | } 38 | 39 | func (codeAuth) SignUp(ctx context.Context) (auth.UserInfo, error) { 40 | return auth.UserInfo{}, errors.New("not implemented") 41 | } 42 | 43 | func (codeAuth) AcceptTermsOfService(ctx context.Context, tos tg.HelpTermsOfService) error { 44 | return &auth.SignUpRequired{TermsOfService: tos} 45 | } 46 | 47 | func (a codeAuth) Code(ctx context.Context, sentCode *tg.AuthSentCode) (string, error) { 48 | bo := backoff.NewExponentialBackOff() 49 | bo.MaxElapsedTime = time.Minute 50 | bo.MaxInterval = time.Second 51 | 52 | return backoff.RetryWithData(func() (string, error) { 53 | res, err := a.client.ReceiveTelegramCode(ctx, oas.ReceiveTelegramCodeParams{ 54 | Token: a.token, 55 | }) 56 | if err != nil { 57 | return "", err 58 | } 59 | if res.Code.Value == "" { 60 | return "", errors.New("no code") 61 | } 62 | return res.Code.Value, err 63 | }, bo) 64 | } 65 | 66 | func (a codeAuth) Phone(_ context.Context) (string, error) { 67 | return a.phone, nil 68 | } 69 | 70 | func (codeAuth) Password(_ context.Context) (string, error) { 71 | return "", errors.New("password not supported") 72 | } 73 | 74 | func TestIntegration(t *testing.T) { 75 | // Integration tests should be explicitly enabled, 76 | // also should be in GitHub actions with token. 77 | if _, ok := os.LookupEnv("GITHUB_TOKEN"); !ok { 78 | t.Skip("no token") 79 | } 80 | if ok, _ := strconv.ParseBool(os.Getenv("E2E")); !ok { 81 | t.Skip("E2E=1 not set") 82 | } 83 | 84 | jobID := os.Getenv("GITHUB_JOB_ID") 85 | runID, _ := strconv.ParseInt(os.Getenv("GITHUB_RUN_ID"), 10, 64) 86 | attempt, _ := strconv.Atoi(os.Getenv("GITHUB_RUN_ATTEMPT")) 87 | 88 | ctx := context.Background() 89 | client, err := oas.NewClient("https://bot.gotd.dev", securitySource{}) 90 | require.NoError(t, err) 91 | 92 | bo := backoff.NewExponentialBackOff() 93 | bo.MaxElapsedTime = time.Minute 94 | bo.MaxInterval = time.Second 95 | 96 | res, err := backoff.RetryNotifyWithData(func() (*oas.AcquireTelegramAccountOK, error) { 97 | return client.AcquireTelegramAccount(ctx, &oas.AcquireTelegramAccountReq{ 98 | RepoOwner: "gotd", 99 | RepoName: "bot", 100 | RunID: runID, 101 | Job: jobID, 102 | RunAttempt: attempt, 103 | }) 104 | }, bo, func(err error, duration time.Duration) { 105 | t.Logf("Error: %v, retrying in %v", err, duration) 106 | }) 107 | require.NoError(t, err) 108 | 109 | t.Logf("Acquired account: %v", res.AccountID) 110 | t.Cleanup(func() { 111 | _ = client.HeartbeatTelegramAccount(ctx, oas.HeartbeatTelegramAccountParams{ 112 | Token: res.Token, 113 | Forget: oas.NewOptBool(true), 114 | }) 115 | }) 116 | 117 | lg := zaptest.NewLogger(t) 118 | au := codeAuth{ 119 | phone: string(res.AccountID), 120 | token: res.Token, 121 | client: client, 122 | } 123 | tgc := telegram.NewClient(17349, "344583e45741c457fe1862106095a5eb", telegram.Options{ 124 | DCList: dcs.Test(), 125 | Logger: lg.Named("client"), 126 | }) 127 | require.NoError(t, tgc.Run(ctx, func(ctx context.Context) error { 128 | t.Log("Auth") 129 | if err := tgc.Auth().IfNecessary(ctx, auth.NewFlow(au, auth.SendCodeOptions{})); err != nil { 130 | return errors.Wrap(err, "auth") 131 | } 132 | t.Log("Auth ok") 133 | return nil 134 | })) 135 | } 136 | -------------------------------------------------------------------------------- /internal/api/handler.go: -------------------------------------------------------------------------------- 1 | package api 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/v42/github" 9 | "go.uber.org/zap" 10 | "golang.org/x/oauth2" 11 | 12 | "github.com/gotd/bot/internal/oas" 13 | "github.com/gotd/bot/internal/tgmanager" 14 | ) 15 | 16 | func NewHandler(manager *tgmanager.Manager) *Handler { 17 | return &Handler{manager: manager} 18 | } 19 | 20 | type Handler struct { 21 | manager *tgmanager.Manager 22 | } 23 | 24 | func (h Handler) AcquireTelegramAccount(ctx context.Context, req *oas.AcquireTelegramAccountReq) (*oas.AcquireTelegramAccountOK, error) { 25 | client, ok := ctx.Value(ghClient{}).(*github.Client) 26 | if !ok { 27 | return nil, errors.New("github client not found") 28 | } 29 | if req.RepoOwner != "gotd" { 30 | return nil, errors.New("unsupported repo owner") 31 | } 32 | repo, _, err := client.Repositories.Get(ctx, req.RepoOwner, req.RepoName) 33 | if err != nil { 34 | return nil, errors.Wrap(err, "get repo") 35 | } 36 | wr, _, err := client.Actions.GetWorkflowRunByID(ctx, req.RepoOwner, req.RepoName, req.RunID) 37 | if err != nil { 38 | return nil, errors.Wrap(err, "get job") 39 | } 40 | zctx.From(ctx).Info("AcquireTelegramAccount", 41 | zap.String("repo", repo.GetFullName()), 42 | zap.String("run", wr.GetName()), 43 | ) 44 | 45 | lease, err := h.manager.Acquire() 46 | if err != nil { 47 | return nil, errors.Wrap(err, "acquire") 48 | } 49 | 50 | return &oas.AcquireTelegramAccountOK{ 51 | AccountID: oas.TelegramAccountID(lease.Account), 52 | Token: lease.Token, 53 | }, nil 54 | } 55 | 56 | type ghClient struct{} 57 | 58 | func (h Handler) HandleTokenAuth(ctx context.Context, operationName oas.OperationName, t oas.TokenAuth) (context.Context, error) { 59 | ts := oauth2.StaticTokenSource( 60 | &oauth2.Token{AccessToken: t.APIKey}, 61 | ) 62 | tc := oauth2.NewClient(ctx, ts) 63 | client := github.NewClient(tc) 64 | ctx = context.WithValue(ctx, ghClient{}, client) 65 | return ctx, nil 66 | } 67 | 68 | func (h Handler) GetHealth(ctx context.Context) (*oas.Health, error) { 69 | return &oas.Health{ 70 | Status: "ok", 71 | }, nil 72 | } 73 | 74 | func (h Handler) HeartbeatTelegramAccount(ctx context.Context, params oas.HeartbeatTelegramAccountParams) error { 75 | if params.Forget.Value { 76 | h.manager.Forget(params.Token) 77 | return nil 78 | } 79 | return h.manager.Heartbeat(params.Token) 80 | } 81 | 82 | func (h Handler) ReceiveTelegramCode(ctx context.Context, params oas.ReceiveTelegramCodeParams) (*oas.ReceiveTelegramCodeOK, error) { 83 | code, err := h.manager.LeaseCode(ctx, params.Token) 84 | if err != nil { 85 | return nil, errors.Wrap(err, "lease code") 86 | } 87 | var rc oas.OptString 88 | if code != "" { 89 | rc.SetTo(code) 90 | } 91 | return &oas.ReceiveTelegramCodeOK{Code: rc}, nil 92 | } 93 | 94 | func (h Handler) NewError(ctx context.Context, err error) *oas.ErrorStatusCode { 95 | return &oas.ErrorStatusCode{ 96 | StatusCode: 500, 97 | Response: oas.Error{ 98 | ErrorMessage: err.Error(), 99 | }, 100 | } 101 | } 102 | 103 | var _ oas.Handler = &Handler{} 104 | var _ oas.SecurityHandler = &Handler{} 105 | -------------------------------------------------------------------------------- /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/gotd/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/metrics.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | type metricWriter struct { 4 | Increase func(n int64) int64 5 | Bytes int64 6 | } 7 | 8 | func (m *metricWriter) Write(p []byte) (n int, err error) { 9 | delta := int64(len(p)) 10 | 11 | m.Increase(delta) 12 | m.Bytes += delta 13 | 14 | return len(p), nil 15 | } 16 | -------------------------------------------------------------------------------- /internal/app/middleware.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/go-faster/errors" 10 | "github.com/gotd/td/fileid" 11 | "github.com/gotd/td/telegram" 12 | "go.opentelemetry.io/otel/metric" 13 | "go.uber.org/zap" 14 | 15 | "github.com/gotd/td/telegram/downloader" 16 | "github.com/gotd/td/tg" 17 | 18 | "github.com/gotd/bot/internal/botapi" 19 | "github.com/gotd/bot/internal/dispatch" 20 | ) 21 | 22 | type Metrics struct { 23 | Messages metric.Int64Counter 24 | Responses metric.Int64Counter 25 | Bytes metric.Int64Counter 26 | Middleware telegram.Middleware 27 | } 28 | 29 | type Middleware struct { 30 | next dispatch.MessageHandler 31 | downloader *downloader.Downloader 32 | client *botapi.Client 33 | metrics *Metrics 34 | 35 | logger *zap.Logger 36 | } 37 | 38 | // NewMiddleware creates new metrics middleware 39 | func NewMiddleware( 40 | next dispatch.MessageHandler, 41 | d *downloader.Downloader, 42 | metrics *Metrics, 43 | opts MiddlewareOptions, 44 | ) Middleware { 45 | opts.setDefaults() 46 | return Middleware{ 47 | next: next, 48 | downloader: d, 49 | client: opts.BotAPI, 50 | metrics: metrics, 51 | logger: opts.Logger, 52 | } 53 | } 54 | 55 | func maxSize(sizes []tg.PhotoSizeClass) string { 56 | var ( 57 | maxSize string 58 | maxH int 59 | ) 60 | 61 | for _, size := range sizes { 62 | if s, ok := size.(interface { 63 | GetH() int 64 | GetType() string 65 | }); ok && s.GetH() > maxH { 66 | maxH = s.GetH() 67 | maxSize = s.GetType() 68 | } 69 | } 70 | 71 | return maxSize 72 | } 73 | 74 | func (m Middleware) downloadMedia(ctx context.Context, rpc *tg.Client, loc tg.InputFileLocationClass) error { 75 | h := sha256.New() 76 | w := &metricWriter{ 77 | Increase: func(n int64) int64 { 78 | m.metrics.Bytes.Add(ctx, n) 79 | return n 80 | }, 81 | } 82 | 83 | if _, err := m.downloader.Download(rpc, loc). 84 | Stream(ctx, io.MultiWriter(h, w)); err != nil { 85 | return errors.Wrap(err, "stream") 86 | } 87 | 88 | m.logger.Info("Downloaded media", 89 | zap.Int64("bytes", w.Bytes), 90 | zap.String("sha256", fmt.Sprintf("%x", h.Sum(nil))), 91 | ) 92 | 93 | return nil 94 | } 95 | 96 | func (m Middleware) handleMedia(ctx context.Context, rpc *tg.Client, msg *tg.Message) error { 97 | log := m.logger.With(zap.Int("msg_id", msg.ID), zap.Stringer("peer_id", msg.PeerID)) 98 | 99 | switch media := msg.Media.(type) { 100 | case *tg.MessageMediaDocument: 101 | doc, ok := media.Document.AsNotEmpty() 102 | if !ok { 103 | return nil 104 | } 105 | if err := m.downloadMedia(ctx, rpc, &tg.InputDocumentFileLocation{ 106 | ID: doc.ID, 107 | AccessHash: doc.AccessHash, 108 | FileReference: doc.FileReference, 109 | }); err != nil { 110 | return errors.Wrap(err, "download") 111 | } 112 | 113 | if err := m.checkOurFileID(ctx, fileid.FromDocument(doc)); err != nil { 114 | log.Warn("Test document FileID", zap.Error(err)) 115 | } 116 | 117 | case *tg.MessageMediaPhoto: 118 | p, ok := media.Photo.AsNotEmpty() 119 | if !ok { 120 | return nil 121 | } 122 | size := maxSize(p.Sizes) 123 | if err := m.downloadMedia(ctx, rpc, &tg.InputPhotoFileLocation{ 124 | ID: p.ID, 125 | AccessHash: p.AccessHash, 126 | FileReference: p.FileReference, 127 | ThumbSize: size, 128 | }); err != nil { 129 | return errors.Wrap(err, "download") 130 | } 131 | 132 | thumbType := 'x' 133 | if len(size) >= 1 { 134 | thumbType = rune(size[0]) 135 | } 136 | 137 | if err := m.checkOurFileID(ctx, fileid.FromPhoto(p, thumbType)); err != nil { 138 | log.Warn("Test photo FileID", zap.Error(err)) 139 | } 140 | default: 141 | // Do not try to get file_id from messages without attachments. 142 | return nil 143 | } 144 | 145 | loc, fileID, err := m.tryGetFileID(ctx, msg.ID) 146 | if err != nil { 147 | log.Warn("Parse file_id", 148 | zap.String("file_id", fileID), 149 | zap.Error(err), 150 | ) 151 | } else { 152 | if _, err := m.downloader.Download(rpc, loc).Stream(ctx, io.Discard); err != nil { 153 | log.Warn("Download file_id", 154 | zap.String("file_id", fileID), 155 | zap.Error(err), 156 | ) 157 | } 158 | log.Info("Successfully downloaded file_id", zap.String("file_id", fileID)) 159 | } 160 | 161 | return nil 162 | } 163 | 164 | // OnMessage implements dispatch.MessageHandler. 165 | func (m Middleware) OnMessage(ctx context.Context, e dispatch.MessageEvent) error { 166 | m.metrics.Messages.Add(ctx, 1) 167 | 168 | if err := m.next.OnMessage(ctx, e); err != nil { 169 | return err 170 | } 171 | 172 | if err := m.handleMedia(ctx, e.RPC(), e.Message); err != nil { 173 | return errors.Wrap(err, "handle media") 174 | } 175 | 176 | m.metrics.Responses.Add(ctx, 1) 177 | return nil 178 | } 179 | -------------------------------------------------------------------------------- /internal/app/middleware_options.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | 6 | "github.com/gotd/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/gotd/td/tg" 9 | 10 | "github.com/gotd/bot/internal/dispatch" 11 | ) 12 | 13 | // Handler implements stats request handler. 14 | type Handler struct { 15 | } 16 | 17 | // NewHandler creates new Handler. 18 | func NewHandler() Handler { 19 | return Handler{} 20 | } 21 | 22 | func (h Handler) stats() string { 23 | var w strings.Builder 24 | fmt.Fprintf(&w, "Statistics:\n\n") 25 | fmt.Fprintln(&w, "TL Layer version:", tg.Layer) 26 | if v := GetVersion(); v != "" { 27 | fmt.Fprintln(&w, "Version:", v) 28 | } 29 | 30 | return w.String() 31 | } 32 | 33 | // OnMessage implements dispatch.MessageHandler. 34 | func (h Handler) OnMessage(ctx context.Context, e dispatch.MessageEvent) error { 35 | _, err := e.Reply().Text(ctx, h.stats()) 36 | return err 37 | } 38 | -------------------------------------------------------------------------------- /internal/app/version.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "runtime/debug" 5 | "strings" 6 | ) 7 | 8 | // GetVersion optimistically gets current client version. 9 | // 10 | // Does not handle replace directives. 11 | func GetVersion() 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/message.go: -------------------------------------------------------------------------------- 1 | package botapi 2 | 3 | type Animation struct { 4 | FileID string `json:"file_id"` 5 | FileUniqueID string `json:"file_unique_id"` 6 | Width int `json:"width"` 7 | Height int `json:"height"` 8 | Duration int `json:"duration"` 9 | Thumb PhotoSize `json:"thumb"` 10 | FileName string `json:"file_name"` 11 | MimeType string `json:"mime_type"` 12 | FileSize int `json:"file_size"` 13 | } 14 | 15 | type Audio struct { 16 | FileID string `json:"file_id"` 17 | FileUniqueID string `json:"file_unique_id"` 18 | Duration int `json:"duration"` 19 | Performer string `json:"performer"` 20 | Title string `json:"title"` 21 | Thumb PhotoSize `json:"thumb"` 22 | FileName string `json:"file_name"` 23 | MimeType string `json:"mime_type"` 24 | FileSize int `json:"file_size"` 25 | } 26 | 27 | type Document struct { 28 | FileID string `json:"file_id"` 29 | FileUniqueID string `json:"file_unique_id"` 30 | Thumb PhotoSize `json:"thumb"` 31 | FileName string `json:"file_name"` 32 | MimeType string `json:"mime_type"` 33 | FileSize int `json:"file_size"` 34 | } 35 | 36 | type Sticker struct { 37 | FileID string `json:"file_id"` 38 | FileUniqueID string `json:"file_unique_id"` 39 | Width int `json:"width"` 40 | Height int `json:"height"` 41 | IsAnimated bool `json:"is_animated"` 42 | Thumb PhotoSize `json:"thumb"` 43 | Emoji string `json:"emoji"` 44 | SetName string `json:"set_name"` 45 | MaskPosition MaskPosition `json:"mask_position"` 46 | FileSize int `json:"file_size"` 47 | } 48 | 49 | type Video struct { 50 | FileID string `json:"file_id"` 51 | FileUniqueID string `json:"file_unique_id"` 52 | Width int `json:"width"` 53 | Height int `json:"height"` 54 | Duration int `json:"duration"` 55 | Thumb PhotoSize `json:"thumb"` 56 | FileName string `json:"file_name"` 57 | MimeType string `json:"mime_type"` 58 | FileSize int `json:"file_size"` 59 | } 60 | 61 | type VideoNote struct { 62 | FileID string `json:"file_id"` 63 | FileUniqueID string `json:"file_unique_id"` 64 | Length int `json:"length"` 65 | Duration int `json:"duration"` 66 | Thumb PhotoSize `json:"thumb"` 67 | FileSize int `json:"file_size"` 68 | } 69 | 70 | type Voice struct { 71 | FileID string `json:"file_id"` 72 | FileUniqueID string `json:"file_unique_id"` 73 | Duration int `json:"duration"` 74 | MimeType string `json:"mime_type"` 75 | FileSize int `json:"file_size"` 76 | } 77 | 78 | type Chat struct { 79 | ID int `json:"id"` 80 | Type string `json:"type"` 81 | Title string `json:"title"` 82 | Username string `json:"username"` 83 | FirstName string `json:"first_name"` 84 | LastName string `json:"last_name"` 85 | Bio string `json:"bio"` 86 | Description string `json:"description"` 87 | InviteLink string `json:"invite_link"` 88 | PinnedMessage *Message `json:"pinned_message"` 89 | } 90 | 91 | type Message struct { 92 | MessageID int `json:"message_id"` 93 | Chat Chat `json:"chat"` 94 | Animation Animation `json:"animation"` 95 | Audio Audio `json:"audio"` 96 | Document Document `json:"document"` 97 | Photo []PhotoSize `json:"photo"` 98 | Sticker Sticker `json:"sticker"` 99 | Video Video `json:"video"` 100 | VideoNote VideoNote `json:"video_note"` 101 | Voice Voice `json:"voice"` 102 | } 103 | 104 | type PhotoSize struct { 105 | FileID string `json:"file_id"` 106 | FileUniqueID string `json:"file_unique_id"` 107 | Width int `json:"width"` 108 | Height int `json:"height"` 109 | FileSize int `json:"file_size"` 110 | } 111 | 112 | type MaskPosition struct { 113 | Point string `json:"point"` 114 | XShift float64 `json:"x_shift"` 115 | YShift float64 `json:"y_shift"` 116 | Scale float64 `json:"scale"` 117 | } 118 | -------------------------------------------------------------------------------- /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/dispatch/base_event.go: -------------------------------------------------------------------------------- 1 | package dispatch 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/go-faster/errors" 8 | "go.uber.org/zap" 9 | 10 | "github.com/gotd/td/telegram/message" 11 | "github.com/gotd/td/tg" 12 | ) 13 | 14 | func (b *Bot) baseEvent() baseEvent { 15 | return baseEvent{ 16 | sender: b.sender, 17 | logger: b.logger, 18 | rpc: b.rpc, 19 | rand: b.rand, 20 | } 21 | } 22 | 23 | type baseEvent struct { 24 | sender *message.Sender 25 | logger *zap.Logger 26 | rpc *tg.Client 27 | rand io.Reader 28 | } 29 | 30 | // Logger returns associated logger. 31 | func (e baseEvent) Logger() *zap.Logger { 32 | return e.logger 33 | } 34 | 35 | // RPC returns Telegram RPC client. 36 | func (e baseEvent) RPC() *tg.Client { 37 | return e.rpc 38 | } 39 | 40 | // Sender returns *message.Sender 41 | func (e baseEvent) Sender() *message.Sender { 42 | return e.sender 43 | } 44 | 45 | func findMessage(r tg.MessagesMessagesClass, msgID int) (*tg.Message, error) { 46 | slice, ok := r.(interface{ GetMessages() []tg.MessageClass }) 47 | if !ok { 48 | return nil, errors.Errorf("unexpected type %T", r) 49 | } 50 | 51 | msgs := slice.GetMessages() 52 | for _, m := range msgs { 53 | msg, ok := m.(*tg.Message) 54 | if !ok || msg.ID != msgID { 55 | continue 56 | } 57 | 58 | return msg, nil 59 | } 60 | 61 | return nil, errors.Errorf("message %d not found in response %+v", msgID, msgs) 62 | } 63 | 64 | func (e baseEvent) getMessage(ctx context.Context, msgID int) (*tg.Message, error) { 65 | r, err := e.rpc.MessagesGetMessages(ctx, []tg.InputMessageClass{&tg.InputMessageID{ID: msgID}}) 66 | if err != nil { 67 | return nil, errors.Wrap(err, "get message") 68 | } 69 | 70 | return findMessage(r, msgID) 71 | } 72 | 73 | func (e baseEvent) getChannelMessage(ctx context.Context, channel *tg.InputChannel, msgID int) (*tg.Message, error) { 74 | r, err := e.rpc.ChannelsGetMessages(ctx, &tg.ChannelsGetMessagesRequest{ 75 | Channel: channel, 76 | ID: []tg.InputMessageClass{&tg.InputMessageID{ID: msgID}}, 77 | }) 78 | if err != nil { 79 | return nil, errors.Wrap(err, "get message") 80 | } 81 | 82 | return findMessage(r, msgID) 83 | } 84 | -------------------------------------------------------------------------------- /internal/dispatch/bot.go: -------------------------------------------------------------------------------- 1 | package dispatch 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "io" 7 | 8 | "go.uber.org/zap" 9 | 10 | "github.com/gotd/td/telegram/downloader" 11 | "github.com/gotd/td/telegram/message" 12 | "github.com/gotd/td/tg" 13 | ) 14 | 15 | // Bot represents generic Telegram bot state and event dispatcher. 16 | type Bot struct { 17 | onMessage MessageHandler 18 | onInline InlineHandler 19 | 20 | rpc *tg.Client 21 | sender *message.Sender 22 | downloader *downloader.Downloader 23 | 24 | logger *zap.Logger 25 | rand io.Reader 26 | } 27 | 28 | // NewBot creates new bot. 29 | func NewBot(raw *tg.Client) *Bot { 30 | return &Bot{ 31 | onMessage: MessageHandlerFunc(func(context.Context, MessageEvent) error { 32 | return nil 33 | }), 34 | onInline: InlineHandlerFunc(func(context.Context, InlineQuery) error { 35 | return nil 36 | }), 37 | rpc: raw, 38 | sender: message.NewSender(raw), 39 | downloader: downloader.NewDownloader(), 40 | logger: zap.NewNop(), 41 | rand: rand.Reader, 42 | } 43 | } 44 | 45 | // OnMessage sets message handler. 46 | func (b *Bot) OnMessage(handler MessageHandler) *Bot { 47 | b.onMessage = handler 48 | return b 49 | } 50 | 51 | // OnInline sets inline query handler. 52 | func (b *Bot) OnInline(handler InlineHandler) *Bot { 53 | b.onInline = handler 54 | return b 55 | } 56 | 57 | // WithSender sets message sender to use. 58 | func (b *Bot) WithSender(sender *message.Sender) *Bot { 59 | b.sender = sender 60 | return b 61 | } 62 | 63 | // WithLogger sets logger. 64 | func (b *Bot) WithLogger(logger *zap.Logger) *Bot { 65 | b.logger = logger 66 | return b 67 | } 68 | 69 | // Register sets handlers using given dispatcher. 70 | func (b *Bot) Register(dispatcher tg.UpdateDispatcher) *Bot { 71 | dispatcher.OnNewMessage(b.OnNewMessage) 72 | dispatcher.OnNewChannelMessage(b.OnNewChannelMessage) 73 | dispatcher.OnBotInlineQuery(b.OnBotInlineQuery) 74 | return b 75 | } 76 | -------------------------------------------------------------------------------- /internal/dispatch/handle_inline.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/tg" 10 | ) 11 | 12 | func (b *Bot) OnBotInlineQuery(ctx context.Context, e tg.Entities, u *tg.UpdateBotInlineQuery) error { 13 | b.logger.Info("Got inline query", 14 | zap.String("query", u.Query), 15 | zap.String("offset", u.Offset), 16 | ) 17 | 18 | user, ok := e.Users[u.UserID] 19 | if !ok { 20 | return errors.Errorf("unknown user ID %d", u.UserID) 21 | } 22 | 23 | var geo *tg.GeoPoint 24 | if u.Geo != nil { 25 | geo, _ = u.Geo.AsNotEmpty() 26 | } 27 | if err := b.onInline.OnInline(ctx, InlineQuery{ 28 | QueryID: u.QueryID, 29 | Query: u.Query, 30 | Offset: u.Offset, 31 | Enquirer: user.AsInput(), 32 | geo: geo, 33 | user: user, 34 | baseEvent: b.baseEvent(), 35 | }); err != nil { 36 | return errors.Wrap(err, "handle inline") 37 | } 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/dispatch/handle_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/tg" 10 | ) 11 | 12 | func (b *Bot) handleUser(ctx context.Context, user *tg.User, m *tg.Message) error { 13 | b.logger.Info("Got message", 14 | zap.String("text", m.Message), 15 | zap.Int64("user_id", user.ID), 16 | zap.String("user_first_name", user.FirstName), 17 | zap.String("username", user.Username), 18 | ) 19 | 20 | return b.onMessage.OnMessage(ctx, MessageEvent{ 21 | Peer: user.AsInputPeer(), 22 | user: user, 23 | Message: m, 24 | baseEvent: b.baseEvent(), 25 | }) 26 | } 27 | 28 | func (b *Bot) handleChat(ctx context.Context, chat *tg.Chat, m *tg.Message) error { 29 | b.logger.Info("Got message from chat", 30 | zap.String("text", m.Message), 31 | zap.Int64("chat_id", chat.ID), 32 | ) 33 | 34 | return b.onMessage.OnMessage(ctx, MessageEvent{ 35 | Peer: chat.AsInputPeer(), 36 | chat: chat, 37 | Message: m, 38 | baseEvent: b.baseEvent(), 39 | }) 40 | } 41 | 42 | func (b *Bot) handleChannel(ctx context.Context, channel *tg.Channel, m *tg.Message) error { 43 | b.logger.Info("Got message from channel", 44 | zap.String("text", m.Message), 45 | zap.String("username", channel.Username), 46 | zap.Int64("channel_id", channel.ID), 47 | ) 48 | 49 | return b.onMessage.OnMessage(ctx, MessageEvent{ 50 | Peer: channel.AsInputPeer(), 51 | channel: channel, 52 | Message: m, 53 | baseEvent: b.baseEvent(), 54 | }) 55 | } 56 | 57 | func (b *Bot) handleMessage(ctx context.Context, e tg.Entities, msg tg.MessageClass) error { 58 | switch m := msg.(type) { 59 | case *tg.Message: 60 | if m.Out { 61 | return nil 62 | } 63 | 64 | switch p := m.PeerID.(type) { 65 | case *tg.PeerUser: 66 | user, ok := e.Users[p.UserID] 67 | if !ok { 68 | return errors.Errorf("unknown user ID %d", p.UserID) 69 | } 70 | return b.handleUser(ctx, user, m) 71 | case *tg.PeerChat: 72 | chat, ok := e.Chats[p.ChatID] 73 | if !ok { 74 | return errors.Errorf("unknown chat ID %d", p.ChatID) 75 | } 76 | return b.handleChat(ctx, chat, m) 77 | case *tg.PeerChannel: 78 | channel, ok := e.Channels[p.ChannelID] 79 | if !ok { 80 | return errors.Errorf("unknown channel ID %d", p.ChannelID) 81 | } 82 | return b.handleChannel(ctx, channel, m) 83 | } 84 | } 85 | 86 | return nil 87 | } 88 | 89 | func (b *Bot) OnNewMessage(ctx context.Context, e tg.Entities, u *tg.UpdateNewMessage) error { 90 | if err := b.handleMessage(ctx, e, u.Message); err != nil { 91 | if !tg.IsUserBlocked(err) { 92 | return errors.Wrapf(err, "handle message %d", u.Message.GetID()) 93 | } 94 | 95 | b.logger.Debug("Bot is blocked by user") 96 | } 97 | return nil 98 | } 99 | 100 | func (b *Bot) OnNewChannelMessage(ctx context.Context, e tg.Entities, u *tg.UpdateNewChannelMessage) error { 101 | if err := b.handleMessage(ctx, e, u.Message); err != nil { 102 | return errors.Wrap(err, "handle") 103 | } 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /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.uber.org/zap" 8 | 9 | "github.com/gotd/td/telegram" 10 | "github.com/gotd/td/tg" 11 | ) 12 | 13 | // LoggedDispatcher is update logging middleware. 14 | type LoggedDispatcher struct { 15 | handler telegram.UpdateHandler 16 | log *zap.Logger 17 | } 18 | 19 | // NewLoggedDispatcher creates new update logging middleware. 20 | func NewLoggedDispatcher(log *zap.Logger) LoggedDispatcher { 21 | return LoggedDispatcher{ 22 | log: log, 23 | } 24 | } 25 | 26 | // Handle implements telegram.UpdateHandler. 27 | func (d LoggedDispatcher) Handle(ctx context.Context, u tg.UpdatesClass) error { 28 | d.log.Debug("Update", 29 | zap.String("t", fmt.Sprintf("%T", u)), 30 | ) 31 | return d.handler.Handle(ctx, u) 32 | } 33 | -------------------------------------------------------------------------------- /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 | 22 | baseEvent 23 | } 24 | 25 | // User returns User object and true if message got from user. 26 | // False and nil otherwise. 27 | func (e MessageEvent) User() (*tg.User, bool) { 28 | return e.user, e.user != nil 29 | } 30 | 31 | // Chat returns Chat object and true if message got from chat. 32 | // False and nil otherwise. 33 | func (e MessageEvent) Chat() (*tg.Chat, bool) { 34 | return e.chat, e.chat != nil 35 | } 36 | 37 | // Channel returns Channel object and true if message got from channel. 38 | // False and nil otherwise. 39 | func (e MessageEvent) Channel() (*tg.Channel, bool) { 40 | return e.channel, e.channel != nil 41 | } 42 | 43 | // WithReply calls given callback if current message event is a reply message. 44 | func (e MessageEvent) WithReply(ctx context.Context, cb func(reply *tg.Message) error) error { 45 | h, ok := e.Message.GetReplyTo() 46 | if !ok { 47 | if _, err := e.Reply().Text(ctx, "Message must be a reply"); err != nil { 48 | return errors.Wrap(err, "send") 49 | } 50 | return nil 51 | } 52 | 53 | var ( 54 | msg *tg.Message 55 | err error 56 | log = e.logger.With( 57 | zap.Int("msg_id", e.Message.ID), 58 | zap.Int("reply_to_msg_id", h.(*tg.MessageReplyHeader).ReplyToMsgID), 59 | ) 60 | ) 61 | switch p := e.Peer.(type) { 62 | case *tg.InputPeerChannel: 63 | log.Info("Fetching message", zap.Int64("channel_id", p.ChannelID)) 64 | 65 | msg, err = e.getChannelMessage(ctx, &tg.InputChannel{ 66 | ChannelID: p.ChannelID, 67 | AccessHash: p.AccessHash, 68 | }, h.(*tg.MessageReplyHeader).ReplyToMsgID) 69 | case *tg.InputPeerChat: 70 | log.Info("Fetching message", zap.Int64("chat_id", p.ChatID)) 71 | 72 | msg, err = e.getMessage(ctx, h.(*tg.MessageReplyHeader).ReplyToMsgID) 73 | case *tg.InputPeerUser: 74 | log.Info("Fetching message", zap.Int64("user_id", p.UserID)) 75 | 76 | msg, err = e.getMessage(ctx, h.(*tg.MessageReplyHeader).ReplyToMsgID) 77 | } 78 | if err != nil { 79 | log.Warn("Fetch message", zap.Error(err)) 80 | if _, err := e.Reply().Textf(ctx, "Message %d not found", h.(*tg.MessageReplyHeader).ReplyToMsgID); err != nil { 81 | return errors.Wrap(err, "send") 82 | } 83 | return nil 84 | } 85 | 86 | return cb(msg) 87 | } 88 | 89 | // Reply creates new message builder to reply. 90 | func (e MessageEvent) Reply() *message.Builder { 91 | return e.sender.To(e.Peer).ReplyMsg(e.Message) 92 | } 93 | 94 | func (e MessageEvent) TypingAction() *message.TypingActionBuilder { 95 | return e.sender.To(e.Peer).TypingAction() 96 | } 97 | 98 | // MessageHandler is a simple message event handler. 99 | type MessageHandler interface { 100 | OnMessage(ctx context.Context, e MessageEvent) error 101 | } 102 | 103 | // MessageHandlerFunc is a functional adapter for Handler. 104 | type MessageHandlerFunc func(ctx context.Context, e MessageEvent) error 105 | 106 | // OnMessage implements MessageHandler. 107 | func (h MessageHandlerFunc) OnMessage(ctx context.Context, e MessageEvent) error { 108 | return h(ctx, e) 109 | } 110 | -------------------------------------------------------------------------------- /internal/dispatch/message_mux.go: -------------------------------------------------------------------------------- 1 | package dispatch 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/go-faster/errors" 8 | 9 | "github.com/gotd/td/tg" 10 | ) 11 | 12 | type handle struct { 13 | MessageHandler 14 | description string 15 | } 16 | 17 | // MessageMux is message event router. 18 | type MessageMux struct { 19 | prefixes map[string]handle 20 | } 21 | 22 | // NewMessageMux creates new MessageMux. 23 | func NewMessageMux() MessageMux { 24 | return MessageMux{prefixes: map[string]handle{}} 25 | } 26 | 27 | // Handle adds given prefix and handler to the mux. 28 | func (m MessageMux) Handle(prefix, description string, handler MessageHandler) { 29 | m.prefixes[prefix] = handle{ 30 | MessageHandler: handler, 31 | description: description, 32 | } 33 | } 34 | 35 | // HandleFunc adds given prefix and handler to the mux. 36 | func (m MessageMux) HandleFunc(prefix, description string, handler func(ctx context.Context, e MessageEvent) error) { 37 | m.Handle(prefix, description, MessageHandlerFunc(handler)) 38 | } 39 | 40 | // OnMessage implements MessageHandler. 41 | func (m MessageMux) OnMessage(ctx context.Context, e MessageEvent) error { 42 | for prefix, handler := range m.prefixes { 43 | if strings.HasPrefix(e.Message.Message, prefix) { 44 | if err := handler.OnMessage(ctx, e); err != nil { 45 | return errors.Wrapf(err, "handle %q", prefix) 46 | } 47 | return nil 48 | } 49 | } 50 | 51 | return nil 52 | } 53 | 54 | // RegisterCommands registers all mux commands using https://core.telegram.org/method/bots.setBotCommands. 55 | func (m MessageMux) RegisterCommands(ctx context.Context, raw *tg.Client) error { 56 | commands := make([]tg.BotCommand, 0, len(m.prefixes)) 57 | for prefix, handler := range m.prefixes { 58 | if handler.description == "" { 59 | continue 60 | } 61 | commands = append(commands, tg.BotCommand{ 62 | Command: strings.TrimPrefix(prefix, "/"), 63 | Description: handler.description, 64 | }) 65 | } 66 | 67 | if _, err := raw.BotsSetBotCommands(ctx, &tg.BotsSetBotCommandsRequest{ 68 | Scope: &tg.BotCommandScopeDefault{}, 69 | LangCode: "en", 70 | Commands: commands, 71 | }); err != nil { 72 | return errors.Wrap(err, "set commands") 73 | } 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /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 | calls := 0 17 | mux.HandleFunc("/github", "test", func(ctx context.Context, e MessageEvent) error { 18 | calls++ 19 | return nil 20 | }) 21 | 22 | send := func(text string) { 23 | a.NoError(mux.OnMessage(ctx, MessageEvent{ 24 | Message: &tg.Message{ 25 | Message: text, 26 | }, 27 | })) 28 | } 29 | send("github/") 30 | send("github/gotd") 31 | send("github/gotd/td") 32 | a.Zero(calls) 33 | send("/github") 34 | a.Equal(1, calls) 35 | } 36 | -------------------------------------------------------------------------------- /internal/docs/docs.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | escapehtml "html" 7 | "strings" 8 | 9 | "github.com/go-faster/errors" 10 | "go.uber.org/multierr" 11 | 12 | "github.com/gotd/getdoc" 13 | "github.com/gotd/td/telegram/message/entity" 14 | "github.com/gotd/td/telegram/message/html" 15 | "github.com/gotd/td/telegram/message/inline" 16 | "github.com/gotd/td/telegram/message/markup" 17 | "github.com/gotd/td/telegram/message/styling" 18 | "github.com/gotd/tl" 19 | 20 | "github.com/gotd/bot/internal/dispatch" 21 | ) 22 | 23 | // Handler implements docs inline query handler. 24 | type Handler struct { 25 | search *Search 26 | } 27 | 28 | // New creates new Handler. 29 | func New(search *Search) Handler { 30 | return Handler{search: search} 31 | } 32 | 33 | func writeType(w *strings.Builder, typ tl.Type, namespace []string, text string) { 34 | if typ.Bare || typ.GenericRef || typ.GenericArg != nil { 35 | w.WriteString(text) 36 | return 37 | } 38 | 39 | w.WriteString("") 40 | w.WriteString(fmt.Sprintf( 41 | ``, 42 | escapehtml.EscapeString(namespacedName(typ.Name, namespace)), 43 | )) 44 | w.WriteString(text) 45 | w.WriteString("") 46 | w.WriteString("
") 47 | } 48 | 49 | func formatDefinition(d tl.Definition) styling.StyledTextOption { 50 | var b strings.Builder 51 | 52 | b.WriteString("") 53 | for _, ns := range d.Namespace { 54 | b.WriteString(ns) 55 | b.WriteRune('.') 56 | } 57 | b.WriteString(fmt.Sprintf("%s#%x", d.Name, d.ID)) 58 | for _, param := range d.GenericParams { 59 | b.WriteString(" {") 60 | b.WriteString(param) 61 | b.WriteString(":Type}") 62 | } 63 | for _, param := range d.Params { 64 | b.WriteRune(' ') 65 | escaped := escapehtml.EscapeString(param.String()) 66 | if param.Flags { 67 | b.WriteString(escaped) 68 | continue 69 | } 70 | writeType(&b, param.Type, nil, escaped) 71 | } 72 | if d.Base { 73 | b.WriteString(" ?") 74 | } 75 | b.WriteString(" = ") 76 | writeType(&b, d.Type, d.Namespace, escapehtml.EscapeString(d.Type.String())) 77 | b.WriteString("") 78 | 79 | return html.String(nil, b.String()) 80 | } 81 | 82 | // OnInline implements dispatch.InlineHandler. 83 | func (h Handler) OnInline(ctx context.Context, e dispatch.InlineQuery) error { 84 | reply := e.Reply() 85 | 86 | results, err := h.search.Match(e.Query) 87 | if err != nil { 88 | _, setErr := reply.Set(ctx) 89 | return multierr.Append(errors.Wrapf(setErr, "search"), err) 90 | } 91 | 92 | var options []inline.ResultOption 93 | for _, result := range results { 94 | def := result.Definition 95 | title := fmt.Sprintf("%s %s#%x", result.Category.String(), def.Name, def.ID) 96 | goDoc := fmt.Sprintf("https://ref.gotd.dev/use/github.com/gotd/td/tg..%s.html", result.GoName) 97 | 98 | var ( 99 | desc []string 100 | docURL string 101 | fields map[string]getdoc.ParamDescription 102 | botCanUse bool 103 | ) 104 | switch result.Category { 105 | case tl.CategoryType: 106 | desc = result.Constructor.Description 107 | docURL = fmt.Sprintf("https://core.telegram.org/constructor/%s", result.NamespacedName) 108 | fields = result.Constructor.Fields 109 | case tl.CategoryFunction: 110 | desc = result.Method.Description 111 | docURL = fmt.Sprintf("https://core.telegram.org/method/%s", result.NamespacedName) 112 | fields = result.Method.Parameters 113 | } 114 | description := strings.Join(desc, " ") 115 | 116 | msg := inline.MessageStyledText( 117 | formatDefinition(def), 118 | styling.Custom(func(eb *entity.Builder) error { 119 | eb.Plain("\n\n") 120 | eb.Italic(description) 121 | eb.Plain("\n\n") 122 | 123 | if botCanUse { 124 | eb.Plain("\n\n") 125 | eb.Plain("Bot can use this method") 126 | eb.Plain("\n\n") 127 | } 128 | 129 | for _, field := range fields { 130 | eb.Plain("-") 131 | eb.Bold(field.Name) 132 | eb.Plain(" ") 133 | eb.Italic(field.Description) 134 | eb.Plain("\n") 135 | } 136 | eb.Plain("\n") 137 | 138 | return nil 139 | }), 140 | ).Row( 141 | markup.URL("Telegram docs", docURL), 142 | markup.URL("gotd docs", goDoc), 143 | ).NoWebpage() 144 | 145 | options = append(options, inline.Article(title, msg).Description(description)) 146 | } 147 | _, err = e.Reply().Set(ctx, options...) 148 | return err 149 | } 150 | -------------------------------------------------------------------------------- /internal/docs/storage.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/blevesearch/bleve/v2" 9 | "github.com/go-faster/errors" 10 | 11 | "github.com/gotd/getdoc" 12 | "github.com/gotd/td/bin" 13 | "github.com/gotd/td/tg" 14 | "github.com/gotd/tl" 15 | ) 16 | 17 | func namespacedName(name string, namespace []string) string { 18 | if len(namespace) == 0 { 19 | return name 20 | } 21 | return fmt.Sprintf("%s.%s", strings.Join(namespace, "."), name) 22 | } 23 | 24 | func definitionType(d tl.Definition) string { 25 | return namespacedName(d.Name, d.Namespace) 26 | } 27 | 28 | // Search is a abstraction for searching docs. 29 | type Search struct { 30 | idx bleve.Index 31 | data map[string]tl.SchemaDefinition 32 | docs *getdoc.Doc 33 | goNames map[uint32]func() bin.Object 34 | } 35 | 36 | // Close closes underlying index. 37 | func (s *Search) Close() error { 38 | return s.idx.Close() 39 | } 40 | 41 | // IndexSchema creates new Search object. 42 | func IndexSchema(indexer bleve.Index, schema *tl.Schema, docs *getdoc.Doc) (*Search, error) { 43 | type Alias tl.SchemaDefinition 44 | 45 | s := &Search{ 46 | idx: indexer, 47 | data: make(map[string]tl.SchemaDefinition, len(schema.Definitions)), 48 | docs: docs, 49 | goNames: tg.TypesConstructorMap(), 50 | } 51 | 52 | for _, def := range schema.Definitions { 53 | id := fmt.Sprintf("%x", def.Definition.ID) 54 | 55 | doc, err := indexer.Document(id) 56 | if err != nil { 57 | return nil, errors.Wrapf(err, "try find %q", id) 58 | } 59 | s.data[id] = def 60 | if doc != nil { 61 | continue 62 | } 63 | 64 | if err := indexer.Index(id, map[string]interface{}{ 65 | "id": id, 66 | "idx": "0x" + id, 67 | "definition": Alias(def), 68 | "name": def.Definition.Name, 69 | "namespace": def.Definition.Namespace, 70 | "fullName": definitionType(def.Definition), 71 | "goName": s.goName(def.Definition.ID), 72 | "category": def.Category.String(), 73 | }); err != nil { 74 | return nil, errors.Wrapf(err, "index %s", id) 75 | } 76 | } 77 | 78 | return s, nil 79 | } 80 | 81 | type SearchResult struct { 82 | tl.SchemaDefinition 83 | NamespacedName string 84 | GoName string 85 | Constructor getdoc.Constructor 86 | Method getdoc.Method 87 | } 88 | 89 | func getType(v interface{}) string { 90 | if t := reflect.TypeOf(v); t.Kind() == reflect.Ptr { 91 | return t.Elem().Name() 92 | } else { 93 | return t.Name() 94 | } 95 | } 96 | 97 | func (s *Search) goName(id uint32) string { 98 | v, ok := s.goNames[id] 99 | if !ok { 100 | return "" 101 | } 102 | return getType(v()) 103 | } 104 | 105 | // Match searches docs using given text query. 106 | func (s *Search) Match(q string) ([]SearchResult, error) { 107 | query := bleve.NewQueryStringQuery(q) 108 | req := bleve.NewSearchRequest(query) 109 | searchResult, err := s.idx.Search(req) 110 | if err != nil { 111 | return nil, errors.Wrapf(err, "query index %q", q) 112 | } 113 | 114 | result := make([]SearchResult, 0, len(searchResult.Hits)) 115 | for _, hit := range searchResult.Hits { 116 | def, ok := s.data[hit.ID] 117 | if !ok { 118 | return nil, errors.Errorf("%s not found", hit.ID) 119 | } 120 | 121 | typeKey := definitionType(def.Definition) 122 | constructorDoc := s.docs.Constructors[typeKey] 123 | methodDoc := s.docs.Methods[typeKey] 124 | 125 | result = append(result, SearchResult{ 126 | SchemaDefinition: def, 127 | GoName: s.goName(def.Definition.ID), 128 | NamespacedName: typeKey, 129 | Constructor: constructorDoc, 130 | Method: methodDoc, 131 | }) 132 | } 133 | return result, nil 134 | } 135 | -------------------------------------------------------------------------------- /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/gotd/bot/internal/ent" 9 | // required by schema hooks. 10 | _ "github.com/gotd/bot/internal/ent/runtime" 11 | 12 | "entgo.io/ent/dialect/sql/schema" 13 | "github.com/gotd/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/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/gotd/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/where.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package lastchannelmessage 4 | 5 | import ( 6 | "entgo.io/ent/dialect/sql" 7 | "github.com/gotd/bot/internal/ent/predicate" 8 | ) 9 | 10 | // ID filters vertices based on their ID field. 11 | func ID(id int64) predicate.LastChannelMessage { 12 | return predicate.LastChannelMessage(sql.FieldEQ(FieldID, id)) 13 | } 14 | 15 | // IDEQ applies the EQ predicate on the ID field. 16 | func IDEQ(id int64) predicate.LastChannelMessage { 17 | return predicate.LastChannelMessage(sql.FieldEQ(FieldID, id)) 18 | } 19 | 20 | // IDNEQ applies the NEQ predicate on the ID field. 21 | func IDNEQ(id int64) predicate.LastChannelMessage { 22 | return predicate.LastChannelMessage(sql.FieldNEQ(FieldID, id)) 23 | } 24 | 25 | // IDIn applies the In predicate on the ID field. 26 | func IDIn(ids ...int64) predicate.LastChannelMessage { 27 | return predicate.LastChannelMessage(sql.FieldIn(FieldID, ids...)) 28 | } 29 | 30 | // IDNotIn applies the NotIn predicate on the ID field. 31 | func IDNotIn(ids ...int64) predicate.LastChannelMessage { 32 | return predicate.LastChannelMessage(sql.FieldNotIn(FieldID, ids...)) 33 | } 34 | 35 | // IDGT applies the GT predicate on the ID field. 36 | func IDGT(id int64) predicate.LastChannelMessage { 37 | return predicate.LastChannelMessage(sql.FieldGT(FieldID, id)) 38 | } 39 | 40 | // IDGTE applies the GTE predicate on the ID field. 41 | func IDGTE(id int64) predicate.LastChannelMessage { 42 | return predicate.LastChannelMessage(sql.FieldGTE(FieldID, id)) 43 | } 44 | 45 | // IDLT applies the LT predicate on the ID field. 46 | func IDLT(id int64) predicate.LastChannelMessage { 47 | return predicate.LastChannelMessage(sql.FieldLT(FieldID, id)) 48 | } 49 | 50 | // IDLTE applies the LTE predicate on the ID field. 51 | func IDLTE(id int64) predicate.LastChannelMessage { 52 | return predicate.LastChannelMessage(sql.FieldLTE(FieldID, id)) 53 | } 54 | 55 | // MessageID applies equality check predicate on the "message_id" field. It's identical to MessageIDEQ. 56 | func MessageID(v int) predicate.LastChannelMessage { 57 | return predicate.LastChannelMessage(sql.FieldEQ(FieldMessageID, v)) 58 | } 59 | 60 | // MessageIDEQ applies the EQ predicate on the "message_id" field. 61 | func MessageIDEQ(v int) predicate.LastChannelMessage { 62 | return predicate.LastChannelMessage(sql.FieldEQ(FieldMessageID, v)) 63 | } 64 | 65 | // MessageIDNEQ applies the NEQ predicate on the "message_id" field. 66 | func MessageIDNEQ(v int) predicate.LastChannelMessage { 67 | return predicate.LastChannelMessage(sql.FieldNEQ(FieldMessageID, v)) 68 | } 69 | 70 | // MessageIDIn applies the In predicate on the "message_id" field. 71 | func MessageIDIn(vs ...int) predicate.LastChannelMessage { 72 | return predicate.LastChannelMessage(sql.FieldIn(FieldMessageID, vs...)) 73 | } 74 | 75 | // MessageIDNotIn applies the NotIn predicate on the "message_id" field. 76 | func MessageIDNotIn(vs ...int) predicate.LastChannelMessage { 77 | return predicate.LastChannelMessage(sql.FieldNotIn(FieldMessageID, vs...)) 78 | } 79 | 80 | // MessageIDGT applies the GT predicate on the "message_id" field. 81 | func MessageIDGT(v int) predicate.LastChannelMessage { 82 | return predicate.LastChannelMessage(sql.FieldGT(FieldMessageID, v)) 83 | } 84 | 85 | // MessageIDGTE applies the GTE predicate on the "message_id" field. 86 | func MessageIDGTE(v int) predicate.LastChannelMessage { 87 | return predicate.LastChannelMessage(sql.FieldGTE(FieldMessageID, v)) 88 | } 89 | 90 | // MessageIDLT applies the LT predicate on the "message_id" field. 91 | func MessageIDLT(v int) predicate.LastChannelMessage { 92 | return predicate.LastChannelMessage(sql.FieldLT(FieldMessageID, v)) 93 | } 94 | 95 | // MessageIDLTE applies the LTE predicate on the "message_id" field. 96 | func MessageIDLTE(v int) predicate.LastChannelMessage { 97 | return predicate.LastChannelMessage(sql.FieldLTE(FieldMessageID, v)) 98 | } 99 | 100 | // And groups predicates with the AND operator between them. 101 | func And(predicates ...predicate.LastChannelMessage) predicate.LastChannelMessage { 102 | return predicate.LastChannelMessage(sql.AndPredicates(predicates...)) 103 | } 104 | 105 | // Or groups predicates with the OR operator between them. 106 | func Or(predicates ...predicate.LastChannelMessage) predicate.LastChannelMessage { 107 | return predicate.LastChannelMessage(sql.OrPredicates(predicates...)) 108 | } 109 | 110 | // Not applies the not operator on the given predicate. 111 | func Not(p predicate.LastChannelMessage) predicate.LastChannelMessage { 112 | return predicate.LastChannelMessage(sql.NotPredicates(p)) 113 | } 114 | -------------------------------------------------------------------------------- /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/gotd/bot/internal/ent/lastchannelmessage" 12 | "github.com/gotd/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/migrate/schema.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package migrate 4 | 5 | import ( 6 | "entgo.io/ent/dialect/sql/schema" 7 | "entgo.io/ent/schema/field" 8 | ) 9 | 10 | var ( 11 | // LastChannelMessagesColumns holds the columns for the "last_channel_messages" table. 12 | LastChannelMessagesColumns = []*schema.Column{ 13 | {Name: "id", Type: field.TypeInt64, Increment: true}, 14 | {Name: "message_id", Type: field.TypeInt}, 15 | } 16 | // LastChannelMessagesTable holds the schema information for the "last_channel_messages" table. 17 | LastChannelMessagesTable = &schema.Table{ 18 | Name: "last_channel_messages", 19 | Columns: LastChannelMessagesColumns, 20 | PrimaryKey: []*schema.Column{LastChannelMessagesColumns[0]}, 21 | } 22 | // PrNotificationsColumns holds the columns for the "pr_notifications" table. 23 | PrNotificationsColumns = []*schema.Column{ 24 | {Name: "id", Type: field.TypeInt, Increment: true}, 25 | {Name: "repo_id", Type: field.TypeInt64}, 26 | {Name: "pull_request_id", Type: field.TypeInt}, 27 | {Name: "pull_request_title", Type: field.TypeString, Default: ""}, 28 | {Name: "pull_request_body", Type: field.TypeString, Default: ""}, 29 | {Name: "pull_request_author_login", Type: field.TypeString, Default: ""}, 30 | {Name: "message_id", Type: field.TypeInt}, 31 | } 32 | // PrNotificationsTable holds the schema information for the "pr_notifications" table. 33 | PrNotificationsTable = &schema.Table{ 34 | Name: "pr_notifications", 35 | Columns: PrNotificationsColumns, 36 | PrimaryKey: []*schema.Column{PrNotificationsColumns[0]}, 37 | Indexes: []*schema.Index{ 38 | { 39 | Name: "prnotification_repo_id_pull_request_id", 40 | Unique: true, 41 | Columns: []*schema.Column{PrNotificationsColumns[1], PrNotificationsColumns[2]}, 42 | }, 43 | }, 44 | } 45 | // TelegramAccountsColumns holds the columns for the "telegram_accounts" table. 46 | TelegramAccountsColumns = []*schema.Column{ 47 | {Name: "id", Type: field.TypeString}, 48 | {Name: "code", Type: field.TypeString, Nullable: true}, 49 | {Name: "code_at", Type: field.TypeTime, Nullable: true}, 50 | {Name: "state", Type: field.TypeEnum, Enums: []string{"New", "CodeSent", "Active", "Error"}, Default: "New"}, 51 | {Name: "status", Type: field.TypeString}, 52 | {Name: "session_data", Type: field.TypeBytes, Nullable: true}, 53 | } 54 | // TelegramAccountsTable holds the schema information for the "telegram_accounts" table. 55 | TelegramAccountsTable = &schema.Table{ 56 | Name: "telegram_accounts", 57 | Columns: TelegramAccountsColumns, 58 | PrimaryKey: []*schema.Column{TelegramAccountsColumns[0]}, 59 | } 60 | // TelegramChannelStatesColumns holds the columns for the "telegram_channel_states" table. 61 | TelegramChannelStatesColumns = []*schema.Column{ 62 | {Name: "id", Type: field.TypeInt, Increment: true}, 63 | {Name: "channel_id", Type: field.TypeInt64}, 64 | {Name: "pts", Type: field.TypeInt, Default: 0}, 65 | {Name: "user_id", Type: field.TypeInt64}, 66 | } 67 | // TelegramChannelStatesTable holds the schema information for the "telegram_channel_states" table. 68 | TelegramChannelStatesTable = &schema.Table{ 69 | Name: "telegram_channel_states", 70 | Columns: TelegramChannelStatesColumns, 71 | PrimaryKey: []*schema.Column{TelegramChannelStatesColumns[0]}, 72 | ForeignKeys: []*schema.ForeignKey{ 73 | { 74 | Symbol: "telegram_channel_states_telegram_user_states_channels", 75 | Columns: []*schema.Column{TelegramChannelStatesColumns[3]}, 76 | RefColumns: []*schema.Column{TelegramUserStatesColumns[0]}, 77 | OnDelete: schema.NoAction, 78 | }, 79 | }, 80 | Indexes: []*schema.Index{ 81 | { 82 | Name: "telegramchannelstate_user_id_channel_id", 83 | Unique: true, 84 | Columns: []*schema.Column{TelegramChannelStatesColumns[3], TelegramChannelStatesColumns[1]}, 85 | }, 86 | }, 87 | } 88 | // TelegramSessionsColumns holds the columns for the "telegram_sessions" table. 89 | TelegramSessionsColumns = []*schema.Column{ 90 | {Name: "id", Type: field.TypeUUID}, 91 | {Name: "data", Type: field.TypeBytes}, 92 | } 93 | // TelegramSessionsTable holds the schema information for the "telegram_sessions" table. 94 | TelegramSessionsTable = &schema.Table{ 95 | Name: "telegram_sessions", 96 | Columns: TelegramSessionsColumns, 97 | PrimaryKey: []*schema.Column{TelegramSessionsColumns[0]}, 98 | } 99 | // TelegramUserStatesColumns holds the columns for the "telegram_user_states" table. 100 | TelegramUserStatesColumns = []*schema.Column{ 101 | {Name: "id", Type: field.TypeInt64, Increment: true}, 102 | {Name: "qts", Type: field.TypeInt, Default: 0}, 103 | {Name: "pts", Type: field.TypeInt, Default: 0}, 104 | {Name: "date", Type: field.TypeInt, Default: 0}, 105 | {Name: "seq", Type: field.TypeInt, Default: 0}, 106 | } 107 | // TelegramUserStatesTable holds the schema information for the "telegram_user_states" table. 108 | TelegramUserStatesTable = &schema.Table{ 109 | Name: "telegram_user_states", 110 | Columns: TelegramUserStatesColumns, 111 | PrimaryKey: []*schema.Column{TelegramUserStatesColumns[0]}, 112 | } 113 | // Tables holds all the tables in the schema. 114 | Tables = []*schema.Table{ 115 | LastChannelMessagesTable, 116 | PrNotificationsTable, 117 | TelegramAccountsTable, 118 | TelegramChannelStatesTable, 119 | TelegramSessionsTable, 120 | TelegramUserStatesTable, 121 | } 122 | ) 123 | 124 | func init() { 125 | TelegramChannelStatesTable.ForeignKeys[0].RefTable = TelegramUserStatesTable 126 | } 127 | -------------------------------------------------------------------------------- /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 | // LastChannelMessage is the predicate function for lastchannelmessage builders. 10 | type LastChannelMessage func(*sql.Selector) 11 | 12 | // PRNotification is the predicate function for prnotification builders. 13 | type PRNotification func(*sql.Selector) 14 | 15 | // TelegramAccount is the predicate function for telegramaccount builders. 16 | type TelegramAccount func(*sql.Selector) 17 | 18 | // TelegramChannelState is the predicate function for telegramchannelstate builders. 19 | type TelegramChannelState func(*sql.Selector) 20 | 21 | // TelegramSession is the predicate function for telegramsession builders. 22 | type TelegramSession func(*sql.Selector) 23 | 24 | // TelegramUserState is the predicate function for telegramuserstate builders. 25 | type TelegramUserState func(*sql.Selector) 26 | -------------------------------------------------------------------------------- /internal/ent/prnotification.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/gotd/bot/internal/ent/prnotification" 12 | ) 13 | 14 | // PRNotification is the model entity for the PRNotification schema. 15 | type PRNotification struct { 16 | config `json:"-"` 17 | // ID of the ent. 18 | ID int `json:"id,omitempty"` 19 | // Github repository ID. 20 | RepoID int64 `json:"repo_id,omitempty"` 21 | // Pull request number. 22 | PullRequestID int `json:"pull_request_id,omitempty"` 23 | // Pull request title. 24 | PullRequestTitle string `json:"pull_request_title,omitempty"` 25 | // Pull request body. 26 | PullRequestBody string `json:"pull_request_body,omitempty"` 27 | // Pull request author's login. 28 | PullRequestAuthorLogin string `json:"pull_request_author_login,omitempty"` 29 | // Telegram message ID. Belongs to notify channel. 30 | MessageID int `json:"message_id,omitempty"` 31 | selectValues sql.SelectValues 32 | } 33 | 34 | // scanValues returns the types for scanning values from sql.Rows. 35 | func (*PRNotification) scanValues(columns []string) ([]any, error) { 36 | values := make([]any, len(columns)) 37 | for i := range columns { 38 | switch columns[i] { 39 | case prnotification.FieldID, prnotification.FieldRepoID, prnotification.FieldPullRequestID, prnotification.FieldMessageID: 40 | values[i] = new(sql.NullInt64) 41 | case prnotification.FieldPullRequestTitle, prnotification.FieldPullRequestBody, prnotification.FieldPullRequestAuthorLogin: 42 | values[i] = new(sql.NullString) 43 | default: 44 | values[i] = new(sql.UnknownType) 45 | } 46 | } 47 | return values, nil 48 | } 49 | 50 | // assignValues assigns the values that were returned from sql.Rows (after scanning) 51 | // to the PRNotification fields. 52 | func (pn *PRNotification) assignValues(columns []string, values []any) error { 53 | if m, n := len(values), len(columns); m < n { 54 | return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) 55 | } 56 | for i := range columns { 57 | switch columns[i] { 58 | case prnotification.FieldID: 59 | value, ok := values[i].(*sql.NullInt64) 60 | if !ok { 61 | return fmt.Errorf("unexpected type %T for field id", value) 62 | } 63 | pn.ID = int(value.Int64) 64 | case prnotification.FieldRepoID: 65 | if value, ok := values[i].(*sql.NullInt64); !ok { 66 | return fmt.Errorf("unexpected type %T for field repo_id", values[i]) 67 | } else if value.Valid { 68 | pn.RepoID = value.Int64 69 | } 70 | case prnotification.FieldPullRequestID: 71 | if value, ok := values[i].(*sql.NullInt64); !ok { 72 | return fmt.Errorf("unexpected type %T for field pull_request_id", values[i]) 73 | } else if value.Valid { 74 | pn.PullRequestID = int(value.Int64) 75 | } 76 | case prnotification.FieldPullRequestTitle: 77 | if value, ok := values[i].(*sql.NullString); !ok { 78 | return fmt.Errorf("unexpected type %T for field pull_request_title", values[i]) 79 | } else if value.Valid { 80 | pn.PullRequestTitle = value.String 81 | } 82 | case prnotification.FieldPullRequestBody: 83 | if value, ok := values[i].(*sql.NullString); !ok { 84 | return fmt.Errorf("unexpected type %T for field pull_request_body", values[i]) 85 | } else if value.Valid { 86 | pn.PullRequestBody = value.String 87 | } 88 | case prnotification.FieldPullRequestAuthorLogin: 89 | if value, ok := values[i].(*sql.NullString); !ok { 90 | return fmt.Errorf("unexpected type %T for field pull_request_author_login", values[i]) 91 | } else if value.Valid { 92 | pn.PullRequestAuthorLogin = value.String 93 | } 94 | case prnotification.FieldMessageID: 95 | if value, ok := values[i].(*sql.NullInt64); !ok { 96 | return fmt.Errorf("unexpected type %T for field message_id", values[i]) 97 | } else if value.Valid { 98 | pn.MessageID = int(value.Int64) 99 | } 100 | default: 101 | pn.selectValues.Set(columns[i], values[i]) 102 | } 103 | } 104 | return nil 105 | } 106 | 107 | // Value returns the ent.Value that was dynamically selected and assigned to the PRNotification. 108 | // This includes values selected through modifiers, order, etc. 109 | func (pn *PRNotification) Value(name string) (ent.Value, error) { 110 | return pn.selectValues.Get(name) 111 | } 112 | 113 | // Update returns a builder for updating this PRNotification. 114 | // Note that you need to call PRNotification.Unwrap() before calling this method if this PRNotification 115 | // was returned from a transaction, and the transaction was committed or rolled back. 116 | func (pn *PRNotification) Update() *PRNotificationUpdateOne { 117 | return NewPRNotificationClient(pn.config).UpdateOne(pn) 118 | } 119 | 120 | // Unwrap unwraps the PRNotification entity that was returned from a transaction after it was closed, 121 | // so that all future queries will be executed through the driver which created the transaction. 122 | func (pn *PRNotification) Unwrap() *PRNotification { 123 | _tx, ok := pn.config.driver.(*txDriver) 124 | if !ok { 125 | panic("ent: PRNotification is not a transactional entity") 126 | } 127 | pn.config.driver = _tx.drv 128 | return pn 129 | } 130 | 131 | // String implements the fmt.Stringer. 132 | func (pn *PRNotification) String() string { 133 | var builder strings.Builder 134 | builder.WriteString("PRNotification(") 135 | builder.WriteString(fmt.Sprintf("id=%v, ", pn.ID)) 136 | builder.WriteString("repo_id=") 137 | builder.WriteString(fmt.Sprintf("%v", pn.RepoID)) 138 | builder.WriteString(", ") 139 | builder.WriteString("pull_request_id=") 140 | builder.WriteString(fmt.Sprintf("%v", pn.PullRequestID)) 141 | builder.WriteString(", ") 142 | builder.WriteString("pull_request_title=") 143 | builder.WriteString(pn.PullRequestTitle) 144 | builder.WriteString(", ") 145 | builder.WriteString("pull_request_body=") 146 | builder.WriteString(pn.PullRequestBody) 147 | builder.WriteString(", ") 148 | builder.WriteString("pull_request_author_login=") 149 | builder.WriteString(pn.PullRequestAuthorLogin) 150 | builder.WriteString(", ") 151 | builder.WriteString("message_id=") 152 | builder.WriteString(fmt.Sprintf("%v", pn.MessageID)) 153 | builder.WriteByte(')') 154 | return builder.String() 155 | } 156 | 157 | // PRNotifications is a parsable slice of PRNotification. 158 | type PRNotifications []*PRNotification 159 | -------------------------------------------------------------------------------- /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/gotd/bot/internal/ent/predicate" 12 | "github.com/gotd/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/runtime.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "github.com/gotd/bot/internal/ent/prnotification" 7 | "github.com/gotd/bot/internal/ent/schema" 8 | "github.com/gotd/bot/internal/ent/telegramchannelstate" 9 | "github.com/gotd/bot/internal/ent/telegramuserstate" 10 | ) 11 | 12 | // The init function reads all schema descriptors with runtime code 13 | // (default values, validators, hooks and policies) and stitches it 14 | // to their package variables. 15 | func init() { 16 | prnotificationFields := schema.PRNotification{}.Fields() 17 | _ = prnotificationFields 18 | // prnotificationDescPullRequestTitle is the schema descriptor for pull_request_title field. 19 | prnotificationDescPullRequestTitle := prnotificationFields[2].Descriptor() 20 | // prnotification.DefaultPullRequestTitle holds the default value on creation for the pull_request_title field. 21 | prnotification.DefaultPullRequestTitle = prnotificationDescPullRequestTitle.Default.(string) 22 | // prnotificationDescPullRequestBody is the schema descriptor for pull_request_body field. 23 | prnotificationDescPullRequestBody := prnotificationFields[3].Descriptor() 24 | // prnotification.DefaultPullRequestBody holds the default value on creation for the pull_request_body field. 25 | prnotification.DefaultPullRequestBody = prnotificationDescPullRequestBody.Default.(string) 26 | // prnotificationDescPullRequestAuthorLogin is the schema descriptor for pull_request_author_login field. 27 | prnotificationDescPullRequestAuthorLogin := prnotificationFields[4].Descriptor() 28 | // prnotification.DefaultPullRequestAuthorLogin holds the default value on creation for the pull_request_author_login field. 29 | prnotification.DefaultPullRequestAuthorLogin = prnotificationDescPullRequestAuthorLogin.Default.(string) 30 | telegramaccountFields := schema.TelegramAccount{}.Fields() 31 | _ = telegramaccountFields 32 | telegramchannelstateFields := schema.TelegramChannelState{}.Fields() 33 | _ = telegramchannelstateFields 34 | // telegramchannelstateDescPts is the schema descriptor for pts field. 35 | telegramchannelstateDescPts := telegramchannelstateFields[2].Descriptor() 36 | // telegramchannelstate.DefaultPts holds the default value on creation for the pts field. 37 | telegramchannelstate.DefaultPts = telegramchannelstateDescPts.Default.(int) 38 | telegramuserstateFields := schema.TelegramUserState{}.Fields() 39 | _ = telegramuserstateFields 40 | // telegramuserstateDescQts is the schema descriptor for qts field. 41 | telegramuserstateDescQts := telegramuserstateFields[1].Descriptor() 42 | // telegramuserstate.DefaultQts holds the default value on creation for the qts field. 43 | telegramuserstate.DefaultQts = telegramuserstateDescQts.Default.(int) 44 | // telegramuserstateDescPts is the schema descriptor for pts field. 45 | telegramuserstateDescPts := telegramuserstateFields[2].Descriptor() 46 | // telegramuserstate.DefaultPts holds the default value on creation for the pts field. 47 | telegramuserstate.DefaultPts = telegramuserstateDescPts.Default.(int) 48 | // telegramuserstateDescDate is the schema descriptor for date field. 49 | telegramuserstateDescDate := telegramuserstateFields[3].Descriptor() 50 | // telegramuserstate.DefaultDate holds the default value on creation for the date field. 51 | telegramuserstate.DefaultDate = telegramuserstateDescDate.Default.(int) 52 | // telegramuserstateDescSeq is the schema descriptor for seq field. 53 | telegramuserstateDescSeq := telegramuserstateFields[4].Descriptor() 54 | // telegramuserstate.DefaultSeq holds the default value on creation for the seq field. 55 | telegramuserstate.DefaultSeq = telegramuserstateDescSeq.Default.(int) 56 | } 57 | -------------------------------------------------------------------------------- /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/gotd/bot/internal/ent/runtime.go 6 | 7 | const ( 8 | Version = "v0.14.2" // Version of ent codegen. 9 | Sum = "h1:ywld/j2Rx4EmnIKs8eZ29cbFA1zpB+DA9TLL5l3rlq0=" // Sum of ent codegen. 10 | ) 11 | -------------------------------------------------------------------------------- /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_account.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/field" 6 | ) 7 | 8 | type TelegramAccount struct { 9 | ent.Schema 10 | } 11 | 12 | func (TelegramAccount) Fields() []ent.Field { 13 | return []ent.Field{ 14 | field.String("id").Comment("Phone number without +"), 15 | field.String("code"). 16 | Optional(). 17 | Nillable(), 18 | field.Time("code_at"). 19 | Optional(). 20 | Nillable(), 21 | field.Enum("state"). 22 | Values("New", "CodeSent", "Active", "Error").Default("New"), 23 | field.String("status"), 24 | field.Bytes("session_data"). 25 | Optional(). 26 | Nillable(), 27 | } 28 | } 29 | 30 | func (TelegramAccount) Edges() []ent.Edge { 31 | return []ent.Edge{} 32 | } 33 | -------------------------------------------------------------------------------- /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/telegramaccount.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | "entgo.io/ent" 11 | "entgo.io/ent/dialect/sql" 12 | "github.com/gotd/bot/internal/ent/telegramaccount" 13 | ) 14 | 15 | // TelegramAccount is the model entity for the TelegramAccount schema. 16 | type TelegramAccount struct { 17 | config `json:"-"` 18 | // ID of the ent. 19 | // Phone number without + 20 | ID string `json:"id,omitempty"` 21 | // Code holds the value of the "code" field. 22 | Code *string `json:"code,omitempty"` 23 | // CodeAt holds the value of the "code_at" field. 24 | CodeAt *time.Time `json:"code_at,omitempty"` 25 | // State holds the value of the "state" field. 26 | State telegramaccount.State `json:"state,omitempty"` 27 | // Status holds the value of the "status" field. 28 | Status string `json:"status,omitempty"` 29 | // SessionData holds the value of the "session_data" field. 30 | SessionData *[]byte `json:"session_data,omitempty"` 31 | selectValues sql.SelectValues 32 | } 33 | 34 | // scanValues returns the types for scanning values from sql.Rows. 35 | func (*TelegramAccount) scanValues(columns []string) ([]any, error) { 36 | values := make([]any, len(columns)) 37 | for i := range columns { 38 | switch columns[i] { 39 | case telegramaccount.FieldSessionData: 40 | values[i] = new([]byte) 41 | case telegramaccount.FieldID, telegramaccount.FieldCode, telegramaccount.FieldState, telegramaccount.FieldStatus: 42 | values[i] = new(sql.NullString) 43 | case telegramaccount.FieldCodeAt: 44 | values[i] = new(sql.NullTime) 45 | default: 46 | values[i] = new(sql.UnknownType) 47 | } 48 | } 49 | return values, nil 50 | } 51 | 52 | // assignValues assigns the values that were returned from sql.Rows (after scanning) 53 | // to the TelegramAccount fields. 54 | func (ta *TelegramAccount) assignValues(columns []string, values []any) error { 55 | if m, n := len(values), len(columns); m < n { 56 | return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) 57 | } 58 | for i := range columns { 59 | switch columns[i] { 60 | case telegramaccount.FieldID: 61 | if value, ok := values[i].(*sql.NullString); !ok { 62 | return fmt.Errorf("unexpected type %T for field id", values[i]) 63 | } else if value.Valid { 64 | ta.ID = value.String 65 | } 66 | case telegramaccount.FieldCode: 67 | if value, ok := values[i].(*sql.NullString); !ok { 68 | return fmt.Errorf("unexpected type %T for field code", values[i]) 69 | } else if value.Valid { 70 | ta.Code = new(string) 71 | *ta.Code = value.String 72 | } 73 | case telegramaccount.FieldCodeAt: 74 | if value, ok := values[i].(*sql.NullTime); !ok { 75 | return fmt.Errorf("unexpected type %T for field code_at", values[i]) 76 | } else if value.Valid { 77 | ta.CodeAt = new(time.Time) 78 | *ta.CodeAt = value.Time 79 | } 80 | case telegramaccount.FieldState: 81 | if value, ok := values[i].(*sql.NullString); !ok { 82 | return fmt.Errorf("unexpected type %T for field state", values[i]) 83 | } else if value.Valid { 84 | ta.State = telegramaccount.State(value.String) 85 | } 86 | case telegramaccount.FieldStatus: 87 | if value, ok := values[i].(*sql.NullString); !ok { 88 | return fmt.Errorf("unexpected type %T for field status", values[i]) 89 | } else if value.Valid { 90 | ta.Status = value.String 91 | } 92 | case telegramaccount.FieldSessionData: 93 | if value, ok := values[i].(*[]byte); !ok { 94 | return fmt.Errorf("unexpected type %T for field session_data", values[i]) 95 | } else if value != nil { 96 | ta.SessionData = value 97 | } 98 | default: 99 | ta.selectValues.Set(columns[i], values[i]) 100 | } 101 | } 102 | return nil 103 | } 104 | 105 | // Value returns the ent.Value that was dynamically selected and assigned to the TelegramAccount. 106 | // This includes values selected through modifiers, order, etc. 107 | func (ta *TelegramAccount) Value(name string) (ent.Value, error) { 108 | return ta.selectValues.Get(name) 109 | } 110 | 111 | // Update returns a builder for updating this TelegramAccount. 112 | // Note that you need to call TelegramAccount.Unwrap() before calling this method if this TelegramAccount 113 | // was returned from a transaction, and the transaction was committed or rolled back. 114 | func (ta *TelegramAccount) Update() *TelegramAccountUpdateOne { 115 | return NewTelegramAccountClient(ta.config).UpdateOne(ta) 116 | } 117 | 118 | // Unwrap unwraps the TelegramAccount entity that was returned from a transaction after it was closed, 119 | // so that all future queries will be executed through the driver which created the transaction. 120 | func (ta *TelegramAccount) Unwrap() *TelegramAccount { 121 | _tx, ok := ta.config.driver.(*txDriver) 122 | if !ok { 123 | panic("ent: TelegramAccount is not a transactional entity") 124 | } 125 | ta.config.driver = _tx.drv 126 | return ta 127 | } 128 | 129 | // String implements the fmt.Stringer. 130 | func (ta *TelegramAccount) String() string { 131 | var builder strings.Builder 132 | builder.WriteString("TelegramAccount(") 133 | builder.WriteString(fmt.Sprintf("id=%v, ", ta.ID)) 134 | if v := ta.Code; v != nil { 135 | builder.WriteString("code=") 136 | builder.WriteString(*v) 137 | } 138 | builder.WriteString(", ") 139 | if v := ta.CodeAt; v != nil { 140 | builder.WriteString("code_at=") 141 | builder.WriteString(v.Format(time.ANSIC)) 142 | } 143 | builder.WriteString(", ") 144 | builder.WriteString("state=") 145 | builder.WriteString(fmt.Sprintf("%v", ta.State)) 146 | builder.WriteString(", ") 147 | builder.WriteString("status=") 148 | builder.WriteString(ta.Status) 149 | builder.WriteString(", ") 150 | if v := ta.SessionData; v != nil { 151 | builder.WriteString("session_data=") 152 | builder.WriteString(fmt.Sprintf("%v", *v)) 153 | } 154 | builder.WriteByte(')') 155 | return builder.String() 156 | } 157 | 158 | // TelegramAccounts is a parsable slice of TelegramAccount. 159 | type TelegramAccounts []*TelegramAccount 160 | -------------------------------------------------------------------------------- /internal/ent/telegramaccount/telegramaccount.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package telegramaccount 4 | 5 | import ( 6 | "fmt" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | ) 10 | 11 | const ( 12 | // Label holds the string label denoting the telegramaccount type in the database. 13 | Label = "telegram_account" 14 | // FieldID holds the string denoting the id field in the database. 15 | FieldID = "id" 16 | // FieldCode holds the string denoting the code field in the database. 17 | FieldCode = "code" 18 | // FieldCodeAt holds the string denoting the code_at field in the database. 19 | FieldCodeAt = "code_at" 20 | // FieldState holds the string denoting the state field in the database. 21 | FieldState = "state" 22 | // FieldStatus holds the string denoting the status field in the database. 23 | FieldStatus = "status" 24 | // FieldSessionData holds the string denoting the session_data field in the database. 25 | FieldSessionData = "session_data" 26 | // Table holds the table name of the telegramaccount in the database. 27 | Table = "telegram_accounts" 28 | ) 29 | 30 | // Columns holds all SQL columns for telegramaccount fields. 31 | var Columns = []string{ 32 | FieldID, 33 | FieldCode, 34 | FieldCodeAt, 35 | FieldState, 36 | FieldStatus, 37 | FieldSessionData, 38 | } 39 | 40 | // ValidColumn reports if the column name is valid (part of the table columns). 41 | func ValidColumn(column string) bool { 42 | for i := range Columns { 43 | if column == Columns[i] { 44 | return true 45 | } 46 | } 47 | return false 48 | } 49 | 50 | // State defines the type for the "state" enum field. 51 | type State string 52 | 53 | // StateNew is the default value of the State enum. 54 | const DefaultState = StateNew 55 | 56 | // State values. 57 | const ( 58 | StateNew State = "New" 59 | StateCodeSent State = "CodeSent" 60 | StateActive State = "Active" 61 | StateError State = "Error" 62 | ) 63 | 64 | func (s State) String() string { 65 | return string(s) 66 | } 67 | 68 | // StateValidator is a validator for the "state" field enum values. It is called by the builders before save. 69 | func StateValidator(s State) error { 70 | switch s { 71 | case StateNew, StateCodeSent, StateActive, StateError: 72 | return nil 73 | default: 74 | return fmt.Errorf("telegramaccount: invalid enum value for state field: %q", s) 75 | } 76 | } 77 | 78 | // OrderOption defines the ordering options for the TelegramAccount queries. 79 | type OrderOption func(*sql.Selector) 80 | 81 | // ByID orders the results by the id field. 82 | func ByID(opts ...sql.OrderTermOption) OrderOption { 83 | return sql.OrderByField(FieldID, opts...).ToFunc() 84 | } 85 | 86 | // ByCode orders the results by the code field. 87 | func ByCode(opts ...sql.OrderTermOption) OrderOption { 88 | return sql.OrderByField(FieldCode, opts...).ToFunc() 89 | } 90 | 91 | // ByCodeAt orders the results by the code_at field. 92 | func ByCodeAt(opts ...sql.OrderTermOption) OrderOption { 93 | return sql.OrderByField(FieldCodeAt, opts...).ToFunc() 94 | } 95 | 96 | // ByState orders the results by the state field. 97 | func ByState(opts ...sql.OrderTermOption) OrderOption { 98 | return sql.OrderByField(FieldState, opts...).ToFunc() 99 | } 100 | 101 | // ByStatus orders the results by the status field. 102 | func ByStatus(opts ...sql.OrderTermOption) OrderOption { 103 | return sql.OrderByField(FieldStatus, opts...).ToFunc() 104 | } 105 | -------------------------------------------------------------------------------- /internal/ent/telegramaccount_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/gotd/bot/internal/ent/predicate" 12 | "github.com/gotd/bot/internal/ent/telegramaccount" 13 | ) 14 | 15 | // TelegramAccountDelete is the builder for deleting a TelegramAccount entity. 16 | type TelegramAccountDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *TelegramAccountMutation 20 | } 21 | 22 | // Where appends a list predicates to the TelegramAccountDelete builder. 23 | func (tad *TelegramAccountDelete) Where(ps ...predicate.TelegramAccount) *TelegramAccountDelete { 24 | tad.mutation.Where(ps...) 25 | return tad 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (tad *TelegramAccountDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, tad.sqlExec, tad.mutation, tad.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (tad *TelegramAccountDelete) ExecX(ctx context.Context) int { 35 | n, err := tad.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (tad *TelegramAccountDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(telegramaccount.Table, sqlgraph.NewFieldSpec(telegramaccount.FieldID, field.TypeString)) 44 | if ps := tad.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, tad.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | tad.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // TelegramAccountDeleteOne is the builder for deleting a single TelegramAccount entity. 60 | type TelegramAccountDeleteOne struct { 61 | tad *TelegramAccountDelete 62 | } 63 | 64 | // Where appends a list predicates to the TelegramAccountDelete builder. 65 | func (tado *TelegramAccountDeleteOne) Where(ps ...predicate.TelegramAccount) *TelegramAccountDeleteOne { 66 | tado.tad.mutation.Where(ps...) 67 | return tado 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (tado *TelegramAccountDeleteOne) Exec(ctx context.Context) error { 72 | n, err := tado.tad.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{telegramaccount.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (tado *TelegramAccountDeleteOne) ExecX(ctx context.Context) { 85 | if err := tado.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/ent/telegramchannelstate.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/gotd/bot/internal/ent/telegramchannelstate" 12 | "github.com/gotd/bot/internal/ent/telegramuserstate" 13 | ) 14 | 15 | // TelegramChannelState is the model entity for the TelegramChannelState schema. 16 | type TelegramChannelState struct { 17 | config `json:"-"` 18 | // ID of the ent. 19 | ID int `json:"id,omitempty"` 20 | // Channel id 21 | ChannelID int64 `json:"channel_id,omitempty"` 22 | // User id 23 | UserID int64 `json:"user_id,omitempty"` 24 | // Pts holds the value of the "pts" field. 25 | Pts int `json:"pts,omitempty"` 26 | // Edges holds the relations/edges for other nodes in the graph. 27 | // The values are being populated by the TelegramChannelStateQuery when eager-loading is set. 28 | Edges TelegramChannelStateEdges `json:"edges"` 29 | selectValues sql.SelectValues 30 | } 31 | 32 | // TelegramChannelStateEdges holds the relations/edges for other nodes in the graph. 33 | type TelegramChannelStateEdges struct { 34 | // User holds the value of the user edge. 35 | User *TelegramUserState `json:"user,omitempty"` 36 | // loadedTypes holds the information for reporting if a 37 | // type was loaded (or requested) in eager-loading or not. 38 | loadedTypes [1]bool 39 | } 40 | 41 | // UserOrErr returns the User value or an error if the edge 42 | // was not loaded in eager-loading, or loaded but was not found. 43 | func (e TelegramChannelStateEdges) UserOrErr() (*TelegramUserState, error) { 44 | if e.User != nil { 45 | return e.User, nil 46 | } else if e.loadedTypes[0] { 47 | return nil, &NotFoundError{label: telegramuserstate.Label} 48 | } 49 | return nil, &NotLoadedError{edge: "user"} 50 | } 51 | 52 | // scanValues returns the types for scanning values from sql.Rows. 53 | func (*TelegramChannelState) scanValues(columns []string) ([]any, error) { 54 | values := make([]any, len(columns)) 55 | for i := range columns { 56 | switch columns[i] { 57 | case telegramchannelstate.FieldID, telegramchannelstate.FieldChannelID, telegramchannelstate.FieldUserID, telegramchannelstate.FieldPts: 58 | values[i] = new(sql.NullInt64) 59 | default: 60 | values[i] = new(sql.UnknownType) 61 | } 62 | } 63 | return values, nil 64 | } 65 | 66 | // assignValues assigns the values that were returned from sql.Rows (after scanning) 67 | // to the TelegramChannelState fields. 68 | func (tcs *TelegramChannelState) assignValues(columns []string, values []any) error { 69 | if m, n := len(values), len(columns); m < n { 70 | return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) 71 | } 72 | for i := range columns { 73 | switch columns[i] { 74 | case telegramchannelstate.FieldID: 75 | value, ok := values[i].(*sql.NullInt64) 76 | if !ok { 77 | return fmt.Errorf("unexpected type %T for field id", value) 78 | } 79 | tcs.ID = int(value.Int64) 80 | case telegramchannelstate.FieldChannelID: 81 | if value, ok := values[i].(*sql.NullInt64); !ok { 82 | return fmt.Errorf("unexpected type %T for field channel_id", values[i]) 83 | } else if value.Valid { 84 | tcs.ChannelID = value.Int64 85 | } 86 | case telegramchannelstate.FieldUserID: 87 | if value, ok := values[i].(*sql.NullInt64); !ok { 88 | return fmt.Errorf("unexpected type %T for field user_id", values[i]) 89 | } else if value.Valid { 90 | tcs.UserID = value.Int64 91 | } 92 | case telegramchannelstate.FieldPts: 93 | if value, ok := values[i].(*sql.NullInt64); !ok { 94 | return fmt.Errorf("unexpected type %T for field pts", values[i]) 95 | } else if value.Valid { 96 | tcs.Pts = int(value.Int64) 97 | } 98 | default: 99 | tcs.selectValues.Set(columns[i], values[i]) 100 | } 101 | } 102 | return nil 103 | } 104 | 105 | // Value returns the ent.Value that was dynamically selected and assigned to the TelegramChannelState. 106 | // This includes values selected through modifiers, order, etc. 107 | func (tcs *TelegramChannelState) Value(name string) (ent.Value, error) { 108 | return tcs.selectValues.Get(name) 109 | } 110 | 111 | // QueryUser queries the "user" edge of the TelegramChannelState entity. 112 | func (tcs *TelegramChannelState) QueryUser() *TelegramUserStateQuery { 113 | return NewTelegramChannelStateClient(tcs.config).QueryUser(tcs) 114 | } 115 | 116 | // Update returns a builder for updating this TelegramChannelState. 117 | // Note that you need to call TelegramChannelState.Unwrap() before calling this method if this TelegramChannelState 118 | // was returned from a transaction, and the transaction was committed or rolled back. 119 | func (tcs *TelegramChannelState) Update() *TelegramChannelStateUpdateOne { 120 | return NewTelegramChannelStateClient(tcs.config).UpdateOne(tcs) 121 | } 122 | 123 | // Unwrap unwraps the TelegramChannelState entity that was returned from a transaction after it was closed, 124 | // so that all future queries will be executed through the driver which created the transaction. 125 | func (tcs *TelegramChannelState) Unwrap() *TelegramChannelState { 126 | _tx, ok := tcs.config.driver.(*txDriver) 127 | if !ok { 128 | panic("ent: TelegramChannelState is not a transactional entity") 129 | } 130 | tcs.config.driver = _tx.drv 131 | return tcs 132 | } 133 | 134 | // String implements the fmt.Stringer. 135 | func (tcs *TelegramChannelState) String() string { 136 | var builder strings.Builder 137 | builder.WriteString("TelegramChannelState(") 138 | builder.WriteString(fmt.Sprintf("id=%v, ", tcs.ID)) 139 | builder.WriteString("channel_id=") 140 | builder.WriteString(fmt.Sprintf("%v", tcs.ChannelID)) 141 | builder.WriteString(", ") 142 | builder.WriteString("user_id=") 143 | builder.WriteString(fmt.Sprintf("%v", tcs.UserID)) 144 | builder.WriteString(", ") 145 | builder.WriteString("pts=") 146 | builder.WriteString(fmt.Sprintf("%v", tcs.Pts)) 147 | builder.WriteByte(')') 148 | return builder.String() 149 | } 150 | 151 | // TelegramChannelStates is a parsable slice of TelegramChannelState. 152 | type TelegramChannelStates []*TelegramChannelState 153 | -------------------------------------------------------------------------------- /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/gotd/bot/internal/ent/predicate" 12 | "github.com/gotd/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/google/uuid" 12 | "github.com/gotd/bot/internal/ent/telegramsession" 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/where.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package telegramsession 4 | 5 | import ( 6 | "entgo.io/ent/dialect/sql" 7 | "github.com/google/uuid" 8 | "github.com/gotd/bot/internal/ent/predicate" 9 | ) 10 | 11 | // ID filters vertices based on their ID field. 12 | func ID(id uuid.UUID) predicate.TelegramSession { 13 | return predicate.TelegramSession(sql.FieldEQ(FieldID, id)) 14 | } 15 | 16 | // IDEQ applies the EQ predicate on the ID field. 17 | func IDEQ(id uuid.UUID) predicate.TelegramSession { 18 | return predicate.TelegramSession(sql.FieldEQ(FieldID, id)) 19 | } 20 | 21 | // IDNEQ applies the NEQ predicate on the ID field. 22 | func IDNEQ(id uuid.UUID) predicate.TelegramSession { 23 | return predicate.TelegramSession(sql.FieldNEQ(FieldID, id)) 24 | } 25 | 26 | // IDIn applies the In predicate on the ID field. 27 | func IDIn(ids ...uuid.UUID) predicate.TelegramSession { 28 | return predicate.TelegramSession(sql.FieldIn(FieldID, ids...)) 29 | } 30 | 31 | // IDNotIn applies the NotIn predicate on the ID field. 32 | func IDNotIn(ids ...uuid.UUID) predicate.TelegramSession { 33 | return predicate.TelegramSession(sql.FieldNotIn(FieldID, ids...)) 34 | } 35 | 36 | // IDGT applies the GT predicate on the ID field. 37 | func IDGT(id uuid.UUID) predicate.TelegramSession { 38 | return predicate.TelegramSession(sql.FieldGT(FieldID, id)) 39 | } 40 | 41 | // IDGTE applies the GTE predicate on the ID field. 42 | func IDGTE(id uuid.UUID) predicate.TelegramSession { 43 | return predicate.TelegramSession(sql.FieldGTE(FieldID, id)) 44 | } 45 | 46 | // IDLT applies the LT predicate on the ID field. 47 | func IDLT(id uuid.UUID) predicate.TelegramSession { 48 | return predicate.TelegramSession(sql.FieldLT(FieldID, id)) 49 | } 50 | 51 | // IDLTE applies the LTE predicate on the ID field. 52 | func IDLTE(id uuid.UUID) predicate.TelegramSession { 53 | return predicate.TelegramSession(sql.FieldLTE(FieldID, id)) 54 | } 55 | 56 | // Data applies equality check predicate on the "data" field. It's identical to DataEQ. 57 | func Data(v []byte) predicate.TelegramSession { 58 | return predicate.TelegramSession(sql.FieldEQ(FieldData, v)) 59 | } 60 | 61 | // DataEQ applies the EQ predicate on the "data" field. 62 | func DataEQ(v []byte) predicate.TelegramSession { 63 | return predicate.TelegramSession(sql.FieldEQ(FieldData, v)) 64 | } 65 | 66 | // DataNEQ applies the NEQ predicate on the "data" field. 67 | func DataNEQ(v []byte) predicate.TelegramSession { 68 | return predicate.TelegramSession(sql.FieldNEQ(FieldData, v)) 69 | } 70 | 71 | // DataIn applies the In predicate on the "data" field. 72 | func DataIn(vs ...[]byte) predicate.TelegramSession { 73 | return predicate.TelegramSession(sql.FieldIn(FieldData, vs...)) 74 | } 75 | 76 | // DataNotIn applies the NotIn predicate on the "data" field. 77 | func DataNotIn(vs ...[]byte) predicate.TelegramSession { 78 | return predicate.TelegramSession(sql.FieldNotIn(FieldData, vs...)) 79 | } 80 | 81 | // DataGT applies the GT predicate on the "data" field. 82 | func DataGT(v []byte) predicate.TelegramSession { 83 | return predicate.TelegramSession(sql.FieldGT(FieldData, v)) 84 | } 85 | 86 | // DataGTE applies the GTE predicate on the "data" field. 87 | func DataGTE(v []byte) predicate.TelegramSession { 88 | return predicate.TelegramSession(sql.FieldGTE(FieldData, v)) 89 | } 90 | 91 | // DataLT applies the LT predicate on the "data" field. 92 | func DataLT(v []byte) predicate.TelegramSession { 93 | return predicate.TelegramSession(sql.FieldLT(FieldData, v)) 94 | } 95 | 96 | // DataLTE applies the LTE predicate on the "data" field. 97 | func DataLTE(v []byte) predicate.TelegramSession { 98 | return predicate.TelegramSession(sql.FieldLTE(FieldData, v)) 99 | } 100 | 101 | // And groups predicates with the AND operator between them. 102 | func And(predicates ...predicate.TelegramSession) predicate.TelegramSession { 103 | return predicate.TelegramSession(sql.AndPredicates(predicates...)) 104 | } 105 | 106 | // Or groups predicates with the OR operator between them. 107 | func Or(predicates ...predicate.TelegramSession) predicate.TelegramSession { 108 | return predicate.TelegramSession(sql.OrPredicates(predicates...)) 109 | } 110 | 111 | // Not applies the not operator on the given predicate. 112 | func Not(p predicate.TelegramSession) predicate.TelegramSession { 113 | return predicate.TelegramSession(sql.NotPredicates(p)) 114 | } 115 | -------------------------------------------------------------------------------- /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/gotd/bot/internal/ent/predicate" 12 | "github.com/gotd/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/telegramuserstate.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package telegramuserstate 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 telegramuserstate type in the database. 12 | Label = "telegram_user_state" 13 | // FieldID holds the string denoting the id field in the database. 14 | FieldID = "id" 15 | // FieldQts holds the string denoting the qts field in the database. 16 | FieldQts = "qts" 17 | // FieldPts holds the string denoting the pts field in the database. 18 | FieldPts = "pts" 19 | // FieldDate holds the string denoting the date field in the database. 20 | FieldDate = "date" 21 | // FieldSeq holds the string denoting the seq field in the database. 22 | FieldSeq = "seq" 23 | // EdgeChannels holds the string denoting the channels edge name in mutations. 24 | EdgeChannels = "channels" 25 | // Table holds the table name of the telegramuserstate in the database. 26 | Table = "telegram_user_states" 27 | // ChannelsTable is the table that holds the channels relation/edge. 28 | ChannelsTable = "telegram_channel_states" 29 | // ChannelsInverseTable is the table name for the TelegramChannelState entity. 30 | // It exists in this package in order to avoid circular dependency with the "telegramchannelstate" package. 31 | ChannelsInverseTable = "telegram_channel_states" 32 | // ChannelsColumn is the table column denoting the channels relation/edge. 33 | ChannelsColumn = "user_id" 34 | ) 35 | 36 | // Columns holds all SQL columns for telegramuserstate fields. 37 | var Columns = []string{ 38 | FieldID, 39 | FieldQts, 40 | FieldPts, 41 | FieldDate, 42 | FieldSeq, 43 | } 44 | 45 | // ValidColumn reports if the column name is valid (part of the table columns). 46 | func ValidColumn(column string) bool { 47 | for i := range Columns { 48 | if column == Columns[i] { 49 | return true 50 | } 51 | } 52 | return false 53 | } 54 | 55 | var ( 56 | // DefaultQts holds the default value on creation for the "qts" field. 57 | DefaultQts int 58 | // DefaultPts holds the default value on creation for the "pts" field. 59 | DefaultPts int 60 | // DefaultDate holds the default value on creation for the "date" field. 61 | DefaultDate int 62 | // DefaultSeq holds the default value on creation for the "seq" field. 63 | DefaultSeq int 64 | ) 65 | 66 | // OrderOption defines the ordering options for the TelegramUserState queries. 67 | type OrderOption func(*sql.Selector) 68 | 69 | // ByID orders the results by the id field. 70 | func ByID(opts ...sql.OrderTermOption) OrderOption { 71 | return sql.OrderByField(FieldID, opts...).ToFunc() 72 | } 73 | 74 | // ByQts orders the results by the qts field. 75 | func ByQts(opts ...sql.OrderTermOption) OrderOption { 76 | return sql.OrderByField(FieldQts, opts...).ToFunc() 77 | } 78 | 79 | // ByPts orders the results by the pts field. 80 | func ByPts(opts ...sql.OrderTermOption) OrderOption { 81 | return sql.OrderByField(FieldPts, opts...).ToFunc() 82 | } 83 | 84 | // ByDate orders the results by the date field. 85 | func ByDate(opts ...sql.OrderTermOption) OrderOption { 86 | return sql.OrderByField(FieldDate, opts...).ToFunc() 87 | } 88 | 89 | // BySeq orders the results by the seq field. 90 | func BySeq(opts ...sql.OrderTermOption) OrderOption { 91 | return sql.OrderByField(FieldSeq, opts...).ToFunc() 92 | } 93 | 94 | // ByChannelsCount orders the results by channels count. 95 | func ByChannelsCount(opts ...sql.OrderTermOption) OrderOption { 96 | return func(s *sql.Selector) { 97 | sqlgraph.OrderByNeighborsCount(s, newChannelsStep(), opts...) 98 | } 99 | } 100 | 101 | // ByChannels orders the results by channels terms. 102 | func ByChannels(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption { 103 | return func(s *sql.Selector) { 104 | sqlgraph.OrderByNeighborTerms(s, newChannelsStep(), append([]sql.OrderTerm{term}, terms...)...) 105 | } 106 | } 107 | func newChannelsStep() *sqlgraph.Step { 108 | return sqlgraph.NewStep( 109 | sqlgraph.From(Table, FieldID), 110 | sqlgraph.To(ChannelsInverseTable, FieldID), 111 | sqlgraph.Edge(sqlgraph.O2M, false, ChannelsTable, ChannelsColumn), 112 | ) 113 | } 114 | -------------------------------------------------------------------------------- /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/gotd/bot/internal/ent/predicate" 12 | "github.com/gotd/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/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/gotd/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/inspect/doc.go: -------------------------------------------------------------------------------- 1 | // Package inspect contains bot command handler which inspects given message and returns inspection result 2 | // using given formatter. 3 | package inspect 4 | -------------------------------------------------------------------------------- /internal/inspect/formatter.go: -------------------------------------------------------------------------------- 1 | package inspect 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | 7 | "github.com/gotd/td/tdp" 8 | "github.com/gotd/td/tg" 9 | ) 10 | 11 | // Formatter is a message formatter. 12 | type Formatter func(io.Writer, *tg.Message) error 13 | 14 | // JSON returns JSON inspect handler. 15 | func JSON() Handler { 16 | return New(func(w io.Writer, m *tg.Message) error { 17 | encoder := json.NewEncoder(w) 18 | encoder.SetIndent("", " ") 19 | return encoder.Encode(m) 20 | }) 21 | } 22 | 23 | // Pretty returns tdp-based inspect handler. 24 | func Pretty() Handler { 25 | return New(func(w io.Writer, m *tg.Message) error { 26 | if _, err := io.WriteString(w, tdp.Format(m, tdp.WithTypeID)); err != nil { 27 | return err 28 | } 29 | 30 | return nil 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /internal/inspect/inspect.go: -------------------------------------------------------------------------------- 1 | package inspect 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/go-faster/errors" 8 | 9 | "github.com/gotd/td/telegram/message/styling" 10 | "github.com/gotd/td/tg" 11 | 12 | "github.com/gotd/bot/internal/dispatch" 13 | ) 14 | 15 | // Handler implements inspect request handler. 16 | type Handler struct { 17 | fmt Formatter 18 | } 19 | 20 | // New creates new Handler. 21 | func New(fmt Formatter) Handler { 22 | return Handler{fmt: fmt} 23 | } 24 | 25 | // OnMessage implements dispatch.MessageHandler. 26 | func (h Handler) OnMessage(ctx context.Context, e dispatch.MessageEvent) error { 27 | return e.WithReply(ctx, func(reply *tg.Message) error { 28 | var w strings.Builder 29 | if err := h.fmt(&w, reply); err != nil { 30 | return errors.Wrapf(err, "encode message %d", reply.ID) 31 | } 32 | 33 | if _, err := e.Reply().StyledText(ctx, styling.Code(w.String())); err != nil { 34 | return errors.Wrap(err, "send") 35 | } 36 | 37 | return nil 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /internal/oas/oas_faker_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "time" 7 | 8 | "github.com/google/uuid" 9 | ) 10 | 11 | // SetFake set fake values. 12 | func (s *AcquireTelegramAccountOK) SetFake() { 13 | { 14 | { 15 | s.AccountID.SetFake() 16 | } 17 | } 18 | { 19 | { 20 | s.Token = uuid.New() 21 | } 22 | } 23 | } 24 | 25 | // SetFake set fake values. 26 | func (s *AcquireTelegramAccountReq) SetFake() { 27 | { 28 | { 29 | s.RepoOwner = "string" 30 | } 31 | } 32 | { 33 | { 34 | s.RepoName = "string" 35 | } 36 | } 37 | { 38 | { 39 | s.Job = "string" 40 | } 41 | } 42 | { 43 | { 44 | s.RunID = int64(0) 45 | } 46 | } 47 | { 48 | { 49 | s.RunAttempt = int(0) 50 | } 51 | } 52 | } 53 | 54 | // SetFake set fake values. 55 | func (s *Error) SetFake() { 56 | { 57 | { 58 | s.ErrorMessage = "string" 59 | } 60 | } 61 | { 62 | { 63 | s.TraceID.SetFake() 64 | } 65 | } 66 | { 67 | { 68 | s.SpanID.SetFake() 69 | } 70 | } 71 | } 72 | 73 | // SetFake set fake values. 74 | func (s *Health) SetFake() { 75 | { 76 | { 77 | s.Status = "string" 78 | } 79 | } 80 | { 81 | { 82 | s.Version = "string" 83 | } 84 | } 85 | { 86 | { 87 | s.Commit = "string" 88 | } 89 | } 90 | { 91 | { 92 | s.BuildDate = time.Now() 93 | } 94 | } 95 | } 96 | 97 | // SetFake set fake values. 98 | func (s *OptSpanID) SetFake() { 99 | var elem SpanID 100 | { 101 | elem.SetFake() 102 | } 103 | s.SetTo(elem) 104 | } 105 | 106 | // SetFake set fake values. 107 | func (s *OptString) SetFake() { 108 | var elem string 109 | { 110 | elem = "string" 111 | } 112 | s.SetTo(elem) 113 | } 114 | 115 | // SetFake set fake values. 116 | func (s *OptTraceID) SetFake() { 117 | var elem TraceID 118 | { 119 | elem.SetFake() 120 | } 121 | s.SetTo(elem) 122 | } 123 | 124 | // SetFake set fake values. 125 | func (s *ReceiveTelegramCodeOK) SetFake() { 126 | { 127 | { 128 | s.Code.SetFake() 129 | } 130 | } 131 | } 132 | 133 | // SetFake set fake values. 134 | func (s *SpanID) SetFake() { 135 | var unwrapped string 136 | { 137 | unwrapped = "string" 138 | } 139 | *s = SpanID(unwrapped) 140 | } 141 | 142 | // SetFake set fake values. 143 | func (s *TelegramAccountID) SetFake() { 144 | var unwrapped string 145 | { 146 | unwrapped = "string" 147 | } 148 | *s = TelegramAccountID(unwrapped) 149 | } 150 | 151 | // SetFake set fake values. 152 | func (s *TraceID) SetFake() { 153 | var unwrapped string 154 | { 155 | unwrapped = "string" 156 | } 157 | *s = TraceID(unwrapped) 158 | } 159 | -------------------------------------------------------------------------------- /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 | AcquireTelegramAccountOperation OperationName = "AcquireTelegramAccount" 10 | GetHealthOperation OperationName = "GetHealth" 11 | HeartbeatTelegramAccountOperation OperationName = "HeartbeatTelegramAccount" 12 | ReceiveTelegramCodeOperation OperationName = "ReceiveTelegramCode" 13 | ) 14 | -------------------------------------------------------------------------------- /internal/oas/oas_parameters_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "net/http" 7 | "net/url" 8 | 9 | "github.com/go-faster/errors" 10 | "github.com/google/uuid" 11 | 12 | "github.com/ogen-go/ogen/conv" 13 | "github.com/ogen-go/ogen/middleware" 14 | "github.com/ogen-go/ogen/ogenerrors" 15 | "github.com/ogen-go/ogen/uri" 16 | "github.com/ogen-go/ogen/validate" 17 | ) 18 | 19 | // HeartbeatTelegramAccountParams is parameters of heartbeatTelegramAccount operation. 20 | type HeartbeatTelegramAccountParams struct { 21 | Token uuid.UUID 22 | Forget OptBool 23 | } 24 | 25 | func unpackHeartbeatTelegramAccountParams(packed middleware.Parameters) (params HeartbeatTelegramAccountParams) { 26 | { 27 | key := middleware.ParameterKey{ 28 | Name: "token", 29 | In: "path", 30 | } 31 | params.Token = packed[key].(uuid.UUID) 32 | } 33 | { 34 | key := middleware.ParameterKey{ 35 | Name: "forget", 36 | In: "query", 37 | } 38 | if v, ok := packed[key]; ok { 39 | params.Forget = v.(OptBool) 40 | } 41 | } 42 | return params 43 | } 44 | 45 | func decodeHeartbeatTelegramAccountParams(args [1]string, argsEscaped bool, r *http.Request) (params HeartbeatTelegramAccountParams, _ error) { 46 | q := uri.NewQueryDecoder(r.URL.Query()) 47 | // Decode path: token. 48 | if err := func() error { 49 | param := args[0] 50 | if argsEscaped { 51 | unescaped, err := url.PathUnescape(args[0]) 52 | if err != nil { 53 | return errors.Wrap(err, "unescape path") 54 | } 55 | param = unescaped 56 | } 57 | if len(param) > 0 { 58 | d := uri.NewPathDecoder(uri.PathDecoderConfig{ 59 | Param: "token", 60 | Value: param, 61 | Style: uri.PathStyleSimple, 62 | Explode: false, 63 | }) 64 | 65 | if err := func() error { 66 | val, err := d.DecodeValue() 67 | if err != nil { 68 | return err 69 | } 70 | 71 | c, err := conv.ToUUID(val) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | params.Token = c 77 | return nil 78 | }(); err != nil { 79 | return err 80 | } 81 | } else { 82 | return validate.ErrFieldRequired 83 | } 84 | return nil 85 | }(); err != nil { 86 | return params, &ogenerrors.DecodeParamError{ 87 | Name: "token", 88 | In: "path", 89 | Err: err, 90 | } 91 | } 92 | // Decode query: forget. 93 | if err := func() error { 94 | cfg := uri.QueryParameterDecodingConfig{ 95 | Name: "forget", 96 | Style: uri.QueryStyleForm, 97 | Explode: true, 98 | } 99 | 100 | if err := q.HasParam(cfg); err == nil { 101 | if err := q.DecodeParam(cfg, func(d uri.Decoder) error { 102 | var paramsDotForgetVal bool 103 | if err := func() error { 104 | val, err := d.DecodeValue() 105 | if err != nil { 106 | return err 107 | } 108 | 109 | c, err := conv.ToBool(val) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | paramsDotForgetVal = c 115 | return nil 116 | }(); err != nil { 117 | return err 118 | } 119 | params.Forget.SetTo(paramsDotForgetVal) 120 | return nil 121 | }); err != nil { 122 | return err 123 | } 124 | } 125 | return nil 126 | }(); err != nil { 127 | return params, &ogenerrors.DecodeParamError{ 128 | Name: "forget", 129 | In: "query", 130 | Err: err, 131 | } 132 | } 133 | return params, nil 134 | } 135 | 136 | // ReceiveTelegramCodeParams is parameters of receiveTelegramCode operation. 137 | type ReceiveTelegramCodeParams struct { 138 | Token uuid.UUID 139 | } 140 | 141 | func unpackReceiveTelegramCodeParams(packed middleware.Parameters) (params ReceiveTelegramCodeParams) { 142 | { 143 | key := middleware.ParameterKey{ 144 | Name: "token", 145 | In: "path", 146 | } 147 | params.Token = packed[key].(uuid.UUID) 148 | } 149 | return params 150 | } 151 | 152 | func decodeReceiveTelegramCodeParams(args [1]string, argsEscaped bool, r *http.Request) (params ReceiveTelegramCodeParams, _ error) { 153 | // Decode path: token. 154 | if err := func() error { 155 | param := args[0] 156 | if argsEscaped { 157 | unescaped, err := url.PathUnescape(args[0]) 158 | if err != nil { 159 | return errors.Wrap(err, "unescape path") 160 | } 161 | param = unescaped 162 | } 163 | if len(param) > 0 { 164 | d := uri.NewPathDecoder(uri.PathDecoderConfig{ 165 | Param: "token", 166 | Value: param, 167 | Style: uri.PathStyleSimple, 168 | Explode: false, 169 | }) 170 | 171 | if err := func() error { 172 | val, err := d.DecodeValue() 173 | if err != nil { 174 | return err 175 | } 176 | 177 | c, err := conv.ToUUID(val) 178 | if err != nil { 179 | return err 180 | } 181 | 182 | params.Token = c 183 | return nil 184 | }(); err != nil { 185 | return err 186 | } 187 | } else { 188 | return validate.ErrFieldRequired 189 | } 190 | return nil 191 | }(); err != nil { 192 | return params, &ogenerrors.DecodeParamError{ 193 | Name: "token", 194 | In: "path", 195 | Err: err, 196 | } 197 | } 198 | return params, nil 199 | } 200 | -------------------------------------------------------------------------------- /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 | "go.uber.org/multierr" 13 | 14 | "github.com/ogen-go/ogen/ogenerrors" 15 | "github.com/ogen-go/ogen/validate" 16 | ) 17 | 18 | func (s *Server) decodeAcquireTelegramAccountRequest(r *http.Request) ( 19 | req *AcquireTelegramAccountReq, 20 | close func() error, 21 | rerr error, 22 | ) { 23 | var closers []func() error 24 | close = func() error { 25 | var merr error 26 | // Close in reverse order, to match defer behavior. 27 | for i := len(closers) - 1; i >= 0; i-- { 28 | c := closers[i] 29 | merr = multierr.Append(merr, c()) 30 | } 31 | return merr 32 | } 33 | defer func() { 34 | if rerr != nil { 35 | rerr = multierr.Append(rerr, close()) 36 | } 37 | }() 38 | ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) 39 | if err != nil { 40 | return req, close, errors.Wrap(err, "parse media type") 41 | } 42 | switch { 43 | case ct == "application/json": 44 | if r.ContentLength == 0 { 45 | return req, close, validate.ErrBodyRequired 46 | } 47 | buf, err := io.ReadAll(r.Body) 48 | if err != nil { 49 | return req, close, err 50 | } 51 | 52 | if len(buf) == 0 { 53 | return req, close, validate.ErrBodyRequired 54 | } 55 | 56 | d := jx.DecodeBytes(buf) 57 | 58 | var request AcquireTelegramAccountReq 59 | if err := func() error { 60 | if err := request.Decode(d); err != nil { 61 | return err 62 | } 63 | if err := d.Skip(); err != io.EOF { 64 | return errors.New("unexpected trailing data") 65 | } 66 | return nil 67 | }(); err != nil { 68 | err = &ogenerrors.DecodeBodyError{ 69 | ContentType: ct, 70 | Body: buf, 71 | Err: err, 72 | } 73 | return req, close, err 74 | } 75 | return &request, close, nil 76 | default: 77 | return req, close, validate.InvalidContentType(ct) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /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 encodeAcquireTelegramAccountRequest( 15 | req *AcquireTelegramAccountReq, 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_response_encoders_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "net/http" 7 | 8 | "github.com/go-faster/errors" 9 | "github.com/go-faster/jx" 10 | "go.opentelemetry.io/otel/codes" 11 | "go.opentelemetry.io/otel/trace" 12 | 13 | ht "github.com/ogen-go/ogen/http" 14 | ) 15 | 16 | func encodeAcquireTelegramAccountResponse(response *AcquireTelegramAccountOK, w http.ResponseWriter, span trace.Span) error { 17 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 18 | w.WriteHeader(200) 19 | span.SetStatus(codes.Ok, http.StatusText(200)) 20 | 21 | e := new(jx.Encoder) 22 | response.Encode(e) 23 | if _, err := e.WriteTo(w); err != nil { 24 | return errors.Wrap(err, "write") 25 | } 26 | 27 | return nil 28 | } 29 | 30 | func encodeGetHealthResponse(response *Health, w http.ResponseWriter, span trace.Span) error { 31 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 32 | w.WriteHeader(200) 33 | span.SetStatus(codes.Ok, http.StatusText(200)) 34 | 35 | e := new(jx.Encoder) 36 | response.Encode(e) 37 | if _, err := e.WriteTo(w); err != nil { 38 | return errors.Wrap(err, "write") 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func encodeHeartbeatTelegramAccountResponse(response *HeartbeatTelegramAccountOK, w http.ResponseWriter, span trace.Span) error { 45 | w.WriteHeader(200) 46 | span.SetStatus(codes.Ok, http.StatusText(200)) 47 | 48 | return nil 49 | } 50 | 51 | func encodeReceiveTelegramCodeResponse(response *ReceiveTelegramCodeOK, w http.ResponseWriter, span trace.Span) error { 52 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 53 | w.WriteHeader(200) 54 | span.SetStatus(codes.Ok, http.StatusText(200)) 55 | 56 | e := new(jx.Encoder) 57 | response.Encode(e) 58 | if _, err := e.WriteTo(w); err != nil { 59 | return errors.Wrap(err, "write") 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func encodeErrorResponse(response *ErrorStatusCode, w http.ResponseWriter, span trace.Span) error { 66 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 67 | code := response.StatusCode 68 | if code == 0 { 69 | // Set default status code. 70 | code = http.StatusOK 71 | } 72 | w.WriteHeader(code) 73 | if st := http.StatusText(code); code >= http.StatusBadRequest { 74 | span.SetStatus(codes.Error, st) 75 | } else { 76 | span.SetStatus(codes.Ok, st) 77 | } 78 | 79 | e := new(jx.Encoder) 80 | response.Response.Encode(e) 81 | if _, err := e.WriteTo(w); err != nil { 82 | return errors.Wrap(err, "write") 83 | } 84 | 85 | if code >= http.StatusInternalServerError { 86 | return errors.Wrapf(ht.ErrInternalServerErrorResponse, "code: %d, message: %s", code, http.StatusText(code)) 87 | } 88 | return nil 89 | 90 | } 91 | -------------------------------------------------------------------------------- /internal/oas/oas_security_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/go-faster/errors" 11 | 12 | "github.com/ogen-go/ogen/ogenerrors" 13 | ) 14 | 15 | // SecurityHandler is handler for security parameters. 16 | type SecurityHandler interface { 17 | // HandleTokenAuth handles tokenAuth security. 18 | HandleTokenAuth(ctx context.Context, operationName OperationName, t TokenAuth) (context.Context, error) 19 | } 20 | 21 | func findAuthorization(h http.Header, prefix string) (string, bool) { 22 | v, ok := h["Authorization"] 23 | if !ok { 24 | return "", false 25 | } 26 | for _, vv := range v { 27 | scheme, value, ok := strings.Cut(vv, " ") 28 | if !ok || !strings.EqualFold(scheme, prefix) { 29 | continue 30 | } 31 | return value, true 32 | } 33 | return "", false 34 | } 35 | 36 | func (s *Server) securityTokenAuth(ctx context.Context, operationName OperationName, req *http.Request) (context.Context, bool, error) { 37 | var t TokenAuth 38 | const parameterName = "Token" 39 | value := req.Header.Get(parameterName) 40 | if value == "" { 41 | return ctx, false, nil 42 | } 43 | t.APIKey = value 44 | rctx, err := s.sec.HandleTokenAuth(ctx, operationName, t) 45 | if errors.Is(err, ogenerrors.ErrSkipServerSecurity) { 46 | return nil, false, nil 47 | } else if err != nil { 48 | return nil, false, err 49 | } 50 | return rctx, true, err 51 | } 52 | 53 | // SecuritySource is provider of security values (tokens, passwords, etc.). 54 | type SecuritySource interface { 55 | // TokenAuth provides tokenAuth security value. 56 | TokenAuth(ctx context.Context, operationName OperationName) (TokenAuth, error) 57 | } 58 | 59 | func (s *Client) securityTokenAuth(ctx context.Context, operationName OperationName, req *http.Request) error { 60 | t, err := s.sec.TokenAuth(ctx, operationName) 61 | if err != nil { 62 | return errors.Wrap(err, "security source \"TokenAuth\"") 63 | } 64 | req.Header.Set("Token", t.APIKey) 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /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 | // AcquireTelegramAccount implements acquireTelegramAccount operation. 12 | // 13 | // Acquire telegram account. 14 | // 15 | // POST /api/telegram/account/acquire 16 | AcquireTelegramAccount(ctx context.Context, req *AcquireTelegramAccountReq) (*AcquireTelegramAccountOK, error) 17 | // GetHealth implements getHealth operation. 18 | // 19 | // Get health. 20 | // 21 | // GET /api/health 22 | GetHealth(ctx context.Context) (*Health, error) 23 | // HeartbeatTelegramAccount implements heartbeatTelegramAccount operation. 24 | // 25 | // Heartbeat telegram account. 26 | // 27 | // GET /api/telegram/account/heartbeat/{token} 28 | HeartbeatTelegramAccount(ctx context.Context, params HeartbeatTelegramAccountParams) error 29 | // ReceiveTelegramCode implements receiveTelegramCode operation. 30 | // 31 | // Receive telegram code. 32 | // 33 | // GET /api/telegram/code/receive/{token} 34 | ReceiveTelegramCode(ctx context.Context, params ReceiveTelegramCodeParams) (*ReceiveTelegramCodeOK, error) 35 | // NewError creates *ErrorStatusCode from error returned by handler. 36 | // 37 | // Used for common default response. 38 | NewError(ctx context.Context, err error) *ErrorStatusCode 39 | } 40 | 41 | // Server implements http server based on OpenAPI v3 specification and 42 | // calls Handler to handle requests. 43 | type Server struct { 44 | h Handler 45 | sec SecurityHandler 46 | baseServer 47 | } 48 | 49 | // NewServer creates new Server. 50 | func NewServer(h Handler, sec SecurityHandler, opts ...ServerOption) (*Server, error) { 51 | s, err := newServerConfig(opts...).baseServer() 52 | if err != nil { 53 | return nil, err 54 | } 55 | return &Server{ 56 | h: h, 57 | sec: sec, 58 | baseServer: s, 59 | }, nil 60 | } 61 | -------------------------------------------------------------------------------- /internal/oas/oas_test_examples_gen_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by ogen, DO NOT EDIT. 2 | 3 | package oas 4 | 5 | import ( 6 | "github.com/go-faster/jx" 7 | 8 | std "encoding/json" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestAcquireTelegramAccountOK_EncodeDecode(t *testing.T) { 15 | var typ AcquireTelegramAccountOK 16 | typ.SetFake() 17 | 18 | e := jx.Encoder{} 19 | typ.Encode(&e) 20 | data := e.Bytes() 21 | require.True(t, std.Valid(data), "Encoded: %s", data) 22 | 23 | var typ2 AcquireTelegramAccountOK 24 | require.NoError(t, typ2.Decode(jx.DecodeBytes(data))) 25 | } 26 | func TestAcquireTelegramAccountReq_EncodeDecode(t *testing.T) { 27 | var typ AcquireTelegramAccountReq 28 | typ.SetFake() 29 | 30 | e := jx.Encoder{} 31 | typ.Encode(&e) 32 | data := e.Bytes() 33 | require.True(t, std.Valid(data), "Encoded: %s", data) 34 | 35 | var typ2 AcquireTelegramAccountReq 36 | require.NoError(t, typ2.Decode(jx.DecodeBytes(data))) 37 | } 38 | func TestError_EncodeDecode(t *testing.T) { 39 | var typ Error 40 | typ.SetFake() 41 | 42 | e := jx.Encoder{} 43 | typ.Encode(&e) 44 | data := e.Bytes() 45 | require.True(t, std.Valid(data), "Encoded: %s", data) 46 | 47 | var typ2 Error 48 | require.NoError(t, typ2.Decode(jx.DecodeBytes(data))) 49 | } 50 | func TestHealth_EncodeDecode(t *testing.T) { 51 | var typ Health 52 | typ.SetFake() 53 | 54 | e := jx.Encoder{} 55 | typ.Encode(&e) 56 | data := e.Bytes() 57 | require.True(t, std.Valid(data), "Encoded: %s", data) 58 | 59 | var typ2 Health 60 | require.NoError(t, typ2.Decode(jx.DecodeBytes(data))) 61 | } 62 | func TestReceiveTelegramCodeOK_EncodeDecode(t *testing.T) { 63 | var typ ReceiveTelegramCodeOK 64 | typ.SetFake() 65 | 66 | e := jx.Encoder{} 67 | typ.Encode(&e) 68 | data := e.Bytes() 69 | require.True(t, std.Valid(data), "Encoded: %s", data) 70 | 71 | var typ2 ReceiveTelegramCodeOK 72 | require.NoError(t, typ2.Decode(jx.DecodeBytes(data))) 73 | } 74 | func TestSpanID_EncodeDecode(t *testing.T) { 75 | var typ SpanID 76 | typ.SetFake() 77 | 78 | e := jx.Encoder{} 79 | typ.Encode(&e) 80 | data := e.Bytes() 81 | require.True(t, std.Valid(data), "Encoded: %s", data) 82 | 83 | var typ2 SpanID 84 | require.NoError(t, typ2.Decode(jx.DecodeBytes(data))) 85 | } 86 | func TestTelegramAccountID_EncodeDecode(t *testing.T) { 87 | var typ TelegramAccountID 88 | typ.SetFake() 89 | 90 | e := jx.Encoder{} 91 | typ.Encode(&e) 92 | data := e.Bytes() 93 | require.True(t, std.Valid(data), "Encoded: %s", data) 94 | 95 | var typ2 TelegramAccountID 96 | require.NoError(t, typ2.Decode(jx.DecodeBytes(data))) 97 | } 98 | func TestTraceID_EncodeDecode(t *testing.T) { 99 | var typ TraceID 100 | typ.SetFake() 101 | 102 | e := jx.Encoder{} 103 | typ.Encode(&e) 104 | data := e.Bytes() 105 | require.True(t, std.Valid(data), "Encoded: %s", data) 106 | 107 | var typ2 TraceID 108 | require.NoError(t, typ2.Decode(jx.DecodeBytes(data))) 109 | } 110 | -------------------------------------------------------------------------------- /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 | // AcquireTelegramAccount implements acquireTelegramAccount operation. 17 | // 18 | // Acquire telegram account. 19 | // 20 | // POST /api/telegram/account/acquire 21 | func (UnimplementedHandler) AcquireTelegramAccount(ctx context.Context, req *AcquireTelegramAccountReq) (r *AcquireTelegramAccountOK, _ error) { 22 | return r, ht.ErrNotImplemented 23 | } 24 | 25 | // GetHealth implements getHealth operation. 26 | // 27 | // Get health. 28 | // 29 | // GET /api/health 30 | func (UnimplementedHandler) GetHealth(ctx context.Context) (r *Health, _ error) { 31 | return r, ht.ErrNotImplemented 32 | } 33 | 34 | // HeartbeatTelegramAccount implements heartbeatTelegramAccount operation. 35 | // 36 | // Heartbeat telegram account. 37 | // 38 | // GET /api/telegram/account/heartbeat/{token} 39 | func (UnimplementedHandler) HeartbeatTelegramAccount(ctx context.Context, params HeartbeatTelegramAccountParams) error { 40 | return ht.ErrNotImplemented 41 | } 42 | 43 | // ReceiveTelegramCode implements receiveTelegramCode operation. 44 | // 45 | // Receive telegram code. 46 | // 47 | // GET /api/telegram/code/receive/{token} 48 | func (UnimplementedHandler) ReceiveTelegramCode(ctx context.Context, params ReceiveTelegramCodeParams) (r *ReceiveTelegramCodeOK, _ error) { 49 | return r, ht.ErrNotImplemented 50 | } 51 | 52 | // NewError creates *ErrorStatusCode from error returned by handler. 53 | // 54 | // Used for common default response. 55 | func (UnimplementedHandler) NewError(ctx context.Context, err error) (r *ErrorStatusCode) { 56 | r = new(ErrorStatusCode) 57 | return r 58 | } 59 | -------------------------------------------------------------------------------- /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 *AcquireTelegramAccountOK) Validate() error { 12 | if s == nil { 13 | return validate.ErrNilPointer 14 | } 15 | 16 | var failures []validate.FieldError 17 | if err := func() error { 18 | if err := s.AccountID.Validate(); err != nil { 19 | return err 20 | } 21 | return nil 22 | }(); err != nil { 23 | failures = append(failures, validate.FieldError{ 24 | Name: "account_id", 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 *Error) Validate() error { 35 | if s == nil { 36 | return validate.ErrNilPointer 37 | } 38 | 39 | var failures []validate.FieldError 40 | if err := func() error { 41 | if value, ok := s.TraceID.Get(); ok { 42 | if err := func() error { 43 | if err := value.Validate(); err != nil { 44 | return err 45 | } 46 | return nil 47 | }(); err != nil { 48 | return err 49 | } 50 | } 51 | return nil 52 | }(); err != nil { 53 | failures = append(failures, validate.FieldError{ 54 | Name: "trace_id", 55 | Error: err, 56 | }) 57 | } 58 | if err := func() error { 59 | if value, ok := s.SpanID.Get(); ok { 60 | if err := func() error { 61 | if err := value.Validate(); err != nil { 62 | return err 63 | } 64 | return nil 65 | }(); err != nil { 66 | return err 67 | } 68 | } 69 | return nil 70 | }(); err != nil { 71 | failures = append(failures, validate.FieldError{ 72 | Name: "span_id", 73 | Error: err, 74 | }) 75 | } 76 | if len(failures) > 0 { 77 | return &validate.Error{Fields: failures} 78 | } 79 | return nil 80 | } 81 | 82 | func (s *ErrorStatusCode) Validate() error { 83 | if s == nil { 84 | return validate.ErrNilPointer 85 | } 86 | 87 | var failures []validate.FieldError 88 | if err := func() error { 89 | if err := s.Response.Validate(); err != nil { 90 | return err 91 | } 92 | return nil 93 | }(); err != nil { 94 | failures = append(failures, validate.FieldError{ 95 | Name: "Response", 96 | Error: err, 97 | }) 98 | } 99 | if len(failures) > 0 { 100 | return &validate.Error{Fields: failures} 101 | } 102 | return nil 103 | } 104 | 105 | func (s *ReceiveTelegramCodeOK) Validate() error { 106 | if s == nil { 107 | return validate.ErrNilPointer 108 | } 109 | 110 | var failures []validate.FieldError 111 | if err := func() error { 112 | if value, ok := s.Code.Get(); ok { 113 | if err := func() error { 114 | if err := (validate.String{ 115 | MinLength: 0, 116 | MinLengthSet: false, 117 | MaxLength: 0, 118 | MaxLengthSet: false, 119 | Email: false, 120 | Hostname: false, 121 | Regex: regexMap["^[0-9]{3,6}$"], 122 | }).Validate(string(value)); err != nil { 123 | return errors.Wrap(err, "string") 124 | } 125 | return nil 126 | }(); err != nil { 127 | return err 128 | } 129 | } 130 | return nil 131 | }(); err != nil { 132 | failures = append(failures, validate.FieldError{ 133 | Name: "code", 134 | Error: err, 135 | }) 136 | } 137 | if len(failures) > 0 { 138 | return &validate.Error{Fields: failures} 139 | } 140 | return nil 141 | } 142 | 143 | func (s SpanID) Validate() error { 144 | alias := (string)(s) 145 | if err := (validate.String{ 146 | MinLength: 0, 147 | MinLengthSet: false, 148 | MaxLength: 0, 149 | MaxLengthSet: false, 150 | Email: false, 151 | Hostname: false, 152 | Regex: regexMap["[[:xdigit:]]{16}"], 153 | }).Validate(string(alias)); err != nil { 154 | return errors.Wrap(err, "string") 155 | } 156 | return nil 157 | } 158 | 159 | func (s TelegramAccountID) Validate() error { 160 | alias := (string)(s) 161 | if err := (validate.String{ 162 | MinLength: 0, 163 | MinLengthSet: false, 164 | MaxLength: 0, 165 | MaxLengthSet: false, 166 | Email: false, 167 | Hostname: false, 168 | Regex: regexMap["^[0-9]{7,15}$"], 169 | }).Validate(string(alias)); err != nil { 170 | return errors.Wrap(err, "string") 171 | } 172 | return nil 173 | } 174 | 175 | func (s TraceID) Validate() error { 176 | alias := (string)(s) 177 | if err := (validate.String{ 178 | MinLength: 0, 179 | MinLengthSet: false, 180 | MaxLength: 0, 181 | MaxLengthSet: false, 182 | Email: false, 183 | Hostname: false, 184 | Regex: regexMap["[[:xdigit:]]{32}"], 185 | }).Validate(string(alias)); err != nil { 186 | return errors.Wrap(err, "string") 187 | } 188 | return nil 189 | } 190 | -------------------------------------------------------------------------------- /internal/storage/hook.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | 6 | "go.uber.org/multierr" 7 | 8 | "github.com/gotd/bot/internal/dispatch" 9 | ) 10 | 11 | // Hook is event handler which saves last message ID of dialog to the Pebble storage. 12 | type Hook struct { 13 | next dispatch.MessageHandler 14 | storage MsgID 15 | } 16 | 17 | // NewHook creates new hook. 18 | func NewHook(next dispatch.MessageHandler, storage MsgID) Hook { 19 | return Hook{next: next, storage: storage} 20 | } 21 | 22 | // OnMessage implements dispatch.MessageHandler. 23 | func (h Hook) OnMessage(ctx context.Context, e dispatch.MessageEvent) error { 24 | ch, ok := e.Channel() 25 | if !ok { 26 | return h.next.OnMessage(ctx, e) 27 | } 28 | 29 | return multierr.Append( 30 | h.storage.UpdateLastMsgID(ch.ID, e.Message.ID), 31 | h.next.OnMessage(ctx, e), 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /internal/storage/msg_id.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/cockroachdb/pebble" 7 | "github.com/go-faster/errors" 8 | "github.com/google/go-github/v42/github" 9 | "go.uber.org/multierr" 10 | ) 11 | 12 | // PRMsgIDKey generates key for given PR. 13 | func PRMsgIDKey(pr *github.PullRequestEvent) []byte { 14 | key := strconv.AppendInt([]byte("pr_"), pr.GetRepo().GetID(), 10) 15 | key = strconv.AppendInt(key, int64(pr.GetPullRequest().GetNumber()), 10) 16 | return key 17 | } 18 | 19 | // LastMsgIDKey generates last message ID key for given channel. 20 | func LastMsgIDKey(channelID int64) []byte { 21 | return strconv.AppendInt([]byte("last_msg_"), channelID, 10) 22 | } 23 | 24 | // MsgID is a simple message ID storage. 25 | type MsgID struct { 26 | db *pebble.DB 27 | } 28 | 29 | // NewMsgID creates new MsgID. 30 | func NewMsgID(db *pebble.DB) MsgID { 31 | return MsgID{db: db} 32 | } 33 | 34 | // UpdateLastMsgID updates last message ID for given channel. 35 | func (m MsgID) UpdateLastMsgID(channelID int64, msgID int) (rerr error) { 36 | key := LastMsgIDKey(channelID) 37 | 38 | b := m.db.NewIndexedBatch() 39 | data, closer, err := b.Get(key) 40 | switch { 41 | case errors.Is(err, pebble.ErrNotFound): 42 | case err != nil: 43 | return err 44 | default: 45 | defer func() { 46 | multierr.AppendInto(&rerr, closer.Close()) 47 | }() 48 | s := string(data) 49 | id, err := strconv.Atoi(s) 50 | if err != nil { 51 | return errors.Wrapf(err, "parse msg id %q", s) 52 | } 53 | 54 | if id > msgID { 55 | return nil 56 | } 57 | } 58 | 59 | if err := b.Set(key, strconv.AppendInt(nil, int64(msgID), 10), pebble.Sync); err != nil { 60 | return errors.Wrapf(err, "set msg_id %d", channelID) 61 | } 62 | 63 | if err := b.Commit(nil); err != nil { 64 | return errors.Wrap(err, "commit") 65 | } 66 | 67 | return nil 68 | } 69 | 70 | // SetPRNotification sets PR notification message ID. 71 | func (m MsgID) SetPRNotification(pr *github.PullRequestEvent, msgID int) error { 72 | return m.db.Set(PRMsgIDKey(pr), strconv.AppendInt(nil, int64(msgID), 10), pebble.Sync) 73 | } 74 | 75 | // FindPRNotification finds PR notification message ID and last message ID for given channel. 76 | // NB: even if last message ID was not found, function returns non-zero msgID. 77 | func (m MsgID) FindPRNotification(channelID int64, pr *github.PullRequestEvent) (msgID, lastMsgID int, rerr error) { 78 | prID := pr.GetPullRequest().GetNumber() 79 | snap := m.db.NewSnapshot() 80 | defer func() { 81 | multierr.AppendInto(&rerr, snap.Close()) 82 | }() 83 | 84 | var err error 85 | msgID, err = findInt(snap, PRMsgIDKey(pr)) 86 | if err != nil { 87 | return 0, 0, errors.Wrapf(err, "find msg ID of PR #%d notification", prID) 88 | } 89 | 90 | lastMsgID, err = findInt(snap, LastMsgIDKey(channelID)) 91 | if err != nil { 92 | return msgID, 0, errors.Wrapf(err, "find last msg ID of channel %d", channelID) 93 | } 94 | 95 | return msgID, lastMsgID, nil 96 | } 97 | 98 | func findInt(snap *pebble.Snapshot, key []byte) (_ int, rerr error) { 99 | data, closer, err := snap.Get(key) 100 | if err != nil { 101 | return 0, err 102 | } 103 | defer func() { 104 | multierr.AppendInto(&rerr, closer.Close()) 105 | }() 106 | 107 | s := string(data) 108 | id, err := strconv.Atoi(s) 109 | if err != nil { 110 | return 0, errors.Wrapf(err, "parse msg id %q", s) 111 | } 112 | 113 | return id, nil 114 | } 115 | -------------------------------------------------------------------------------- /internal/tgmanager/account.go: -------------------------------------------------------------------------------- 1 | package tgmanager 2 | 3 | import ( 4 | "context" 5 | "regexp" 6 | "time" 7 | 8 | "github.com/go-faster/errors" 9 | "github.com/gotd/td/telegram" 10 | "github.com/gotd/td/telegram/auth" 11 | "github.com/gotd/td/telegram/dcs" 12 | "github.com/gotd/td/tg" 13 | "go.opentelemetry.io/otel/trace" 14 | "go.uber.org/zap" 15 | 16 | "github.com/gotd/bot/internal/ent" 17 | "github.com/gotd/bot/internal/ent/telegramaccount" 18 | ) 19 | 20 | type Account struct { 21 | client *telegram.Client 22 | number string 23 | lg *zap.Logger 24 | db *ent.Client 25 | tracer trace.Tracer 26 | } 27 | 28 | // terminalAuth implements auth.UserAuthenticator prompting the terminal for 29 | // input. 30 | type codeAuth struct { 31 | phone string 32 | acc *Account 33 | } 34 | 35 | func (codeAuth) SignUp(ctx context.Context) (auth.UserInfo, error) { 36 | return auth.UserInfo{}, errors.New("not implemented") 37 | } 38 | 39 | func (codeAuth) AcceptTermsOfService(ctx context.Context, tos tg.HelpTermsOfService) error { 40 | return &auth.SignUpRequired{TermsOfService: tos} 41 | } 42 | 43 | func (a codeAuth) Code(ctx context.Context, sentCode *tg.AuthSentCode) (string, error) { 44 | // Waiting for code. 45 | return a.acc.WaitForCode(ctx, sentCode) 46 | } 47 | 48 | func (a codeAuth) Phone(_ context.Context) (string, error) { 49 | return a.phone, nil 50 | } 51 | 52 | func (codeAuth) Password(_ context.Context) (string, error) { 53 | return "", errors.New("password not supported") 54 | } 55 | 56 | func extractCode(message string) string { 57 | // Extract login code by regex. 58 | // Get first 5 to 7 digits number. 59 | r := regexp.MustCompile(`\d{5,7}`) 60 | matches := r.FindStringSubmatch(message) 61 | if len(matches) > 0 { 62 | return matches[0] 63 | } 64 | return "" 65 | } 66 | 67 | func NewAccount(lg *zap.Logger, db *ent.Client, tracer trace.Tracer, number string) *Account { 68 | acc := &Account{ 69 | lg: lg.Named("account"), 70 | number: number, 71 | db: db, 72 | tracer: tracer, 73 | } 74 | 75 | const supportID = 777000 76 | dispatcher := tg.NewUpdateDispatcher() 77 | dispatcher.OnNewMessage(func(ctx context.Context, e tg.Entities, update *tg.UpdateNewMessage) error { 78 | msg, ok := update.Message.(*tg.Message) 79 | if !ok { 80 | return nil 81 | } 82 | if msg.Out { 83 | return nil 84 | } 85 | switch p := msg.PeerID.(type) { 86 | case *tg.PeerUser: 87 | if p.UserID != supportID { 88 | lg.Info("Ignoring message", zap.Any("peer", msg.PeerID)) 89 | return nil 90 | } 91 | lg.Info("Support message", zap.String("message", msg.Message)) 92 | default: 93 | lg.Info("Ignoring message", zap.Any("peer", msg.PeerID)) 94 | return nil 95 | } 96 | 97 | text := msg.Message 98 | code := extractCode(text) 99 | if code == "" { 100 | lg.Info("Code not found") 101 | return nil 102 | } 103 | 104 | if err := db.TelegramAccount.UpdateOneID(number). 105 | SetCode(code). 106 | SetCodeAt(time.Now()). 107 | SetStatus("New code received"). 108 | Exec(ctx); err != nil { 109 | return errors.Wrap(err, "update account") 110 | } 111 | lg.Info("Code updated to", 112 | zap.String("code", code), 113 | ) 114 | 115 | return nil 116 | }) 117 | 118 | // https://github.com/telegramdesktop/tdesktop/blob/dev/docs/api_credentials.md 119 | client := telegram.NewClient(17349, "344583e45741c457fe1862106095a5eb", telegram.Options{ 120 | DCList: dcs.Test(), 121 | Logger: lg.Named("client"), 122 | UpdateHandler: dispatcher, 123 | SessionStorage: &SessionStorage{ 124 | id: number, 125 | db: db, 126 | }, 127 | }) 128 | acc.withClient(client) 129 | 130 | return acc 131 | } 132 | 133 | func (a *Account) withClient(client *telegram.Client) *Account { 134 | a.client = client 135 | return a 136 | } 137 | 138 | func (a *Account) setState(ctx context.Context, state telegramaccount.State) error { 139 | return a.db.TelegramAccount.UpdateOneID(a.number). 140 | SetState(state). 141 | SetStatus(state.String()). 142 | Exec(ctx) 143 | } 144 | 145 | func (a *Account) Run(ctx context.Context) error { 146 | if a.client == nil { 147 | return errors.New("client is not initialized") 148 | } 149 | a.lg.Info("Starting") 150 | flow := auth.NewFlow(&codeAuth{ 151 | phone: a.number, 152 | acc: a, 153 | }, auth.SendCodeOptions{}) 154 | return a.client.Run(ctx, func(ctx context.Context) error { 155 | a.lg.Info("Running") 156 | if err := a.client.Auth().IfNecessary(ctx, flow); err != nil { 157 | return errors.Wrap(err, "auth") 158 | } 159 | a.lg.Info("Auth ok") 160 | if err := a.setState(ctx, telegramaccount.StateActive); err != nil { 161 | return errors.Wrap(err, "update account") 162 | } 163 | <-ctx.Done() 164 | return ctx.Err() 165 | }) 166 | } 167 | 168 | func (a *Account) WaitForCode(ctx context.Context, code *tg.AuthSentCode) (ret string, rerr error) { 169 | // Wait for code to be sent via API. 170 | ctx, span := a.tracer.Start(ctx, "WaitForCode") 171 | defer func() { 172 | if rerr != nil { 173 | span.RecordError(rerr) 174 | } 175 | span.End() 176 | }() 177 | 178 | a.lg.Info("Waiting for code") 179 | if err := a.setState(ctx, telegramaccount.StateCodeSent); err != nil { 180 | return "", errors.Wrap(err, "update account") 181 | } 182 | 183 | start := time.Now() 184 | ticker := time.NewTicker(time.Second) 185 | for { 186 | select { 187 | case <-ctx.Done(): 188 | return "", ctx.Err() 189 | case <-ticker.C: 190 | acc, err := a.db.TelegramAccount.Get(ctx, a.number) 191 | if err != nil { 192 | return "", errors.Wrap(err, "get account") 193 | } 194 | if acc.Code == nil || acc.CodeAt == nil || *acc.Code == "" { 195 | a.lg.Info("Code not received") 196 | continue 197 | } 198 | if acc.CodeAt.Before(start) { 199 | a.lg.Info("Code expired") 200 | continue 201 | } 202 | return *acc.Code, nil 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /internal/tgmanager/account_test.go: -------------------------------------------------------------------------------- 1 | package tgmanager 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_extractCode(t *testing.T) { 10 | for _, tt := range []struct { 11 | Name string 12 | Message string 13 | Result string 14 | }{ 15 | { 16 | Name: "Empty", 17 | Message: "", 18 | Result: "", 19 | }, 20 | { 21 | Name: "English", 22 | Message: `Login code: 70021. Do not give this code to anyone, even if they say they are from Telegram! 23 | 24 | ❗️This code can be used to log in to your Telegram account. We never ask it for anything else. 25 | 26 | If you didn't request this code by trying to log in on another device, simply ignore this message.`, 27 | Result: "70021", 28 | }, 29 | { 30 | Name: "NonCode", 31 | Message: `New login. Dear Foo, we detected a login into your account from a new device on 08/12/2024 at 11:43:00 UTC. 32 | 33 | Device: tdesktop, v0.115.0, go1.23.3, Desktop, linux 34 | Location: Russia 35 | 36 | If this wasn't you, you can terminate that session in Settings > Devices (or Privacy & Security > Active Sessions). 37 | 38 | If you think that somebody logged in to your account against your will, you can enable Two-Step Verification in Privacy and Security settings.`, 39 | Result: "", 40 | }, 41 | } { 42 | t.Run(tt.Name, func(t *testing.T) { 43 | got := extractCode(tt.Message) 44 | require.Equal(t, tt.Result, got) 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/tgmanager/storage.go: -------------------------------------------------------------------------------- 1 | package tgmanager 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-faster/errors" 7 | "github.com/gotd/td/session" 8 | 9 | "github.com/gotd/bot/internal/ent" 10 | ) 11 | 12 | type SessionStorage struct { 13 | db *ent.Client 14 | id string 15 | } 16 | 17 | func (s SessionStorage) LoadSession(ctx context.Context) ([]byte, error) { 18 | acc, err := s.db.TelegramAccount.Get(ctx, s.id) 19 | if err != nil { 20 | return nil, errors.Wrap(err, "get account") 21 | } 22 | if acc.SessionData == nil || len(*acc.SessionData) == 0 { 23 | return nil, session.ErrNotFound 24 | } 25 | return *acc.SessionData, nil 26 | } 27 | 28 | func (s SessionStorage) StoreSession(ctx context.Context, data []byte) error { 29 | _, err := s.db.TelegramAccount.UpdateOneID(s.id). 30 | SetSessionData(data). 31 | Save(ctx) 32 | if err != nil { 33 | return errors.Wrap(err, "update session") 34 | } 35 | return err 36 | } 37 | -------------------------------------------------------------------------------- /kubeconfig.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotd/bot/e62d8cd434f9e00460eb3a499f1039798319e7f3/kubeconfig.gpg -------------------------------------------------------------------------------- /kubeconfig.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | gpg --quiet --batch --yes --decrypt --passphrase="${KUBECONFIG_GPG_PASS}" \ 4 | --output /tmp/kubeconfig kubeconfig.gpg 5 | -------------------------------------------------------------------------------- /migrate.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM arigaio/atlas:latest 2 | 3 | COPY migrations migrations 4 | -------------------------------------------------------------------------------- /migrations/20241202075819_init.sql: -------------------------------------------------------------------------------- 1 | -- Create "last_channel_messages" table 2 | CREATE TABLE "last_channel_messages" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "message_id" bigint NOT NULL, PRIMARY KEY ("id")); 3 | -- Create "pr_notifications" table 4 | CREATE TABLE "pr_notifications" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "repo_id" bigint NOT NULL, "pull_request_id" bigint NOT NULL, "pull_request_title" character varying NOT NULL DEFAULT '', "pull_request_body" character varying NOT NULL DEFAULT '', "pull_request_author_login" character varying NOT NULL DEFAULT '', "message_id" bigint NOT NULL, PRIMARY KEY ("id")); 5 | -- Create index "prnotification_repo_id_pull_request_id" to table: "pr_notifications" 6 | CREATE UNIQUE INDEX "prnotification_repo_id_pull_request_id" ON "pr_notifications" ("repo_id", "pull_request_id"); 7 | -- Create "telegram_sessions" table 8 | CREATE TABLE "telegram_sessions" ("id" uuid NOT NULL, "data" bytea NOT NULL, PRIMARY KEY ("id")); 9 | -- Create "telegram_user_states" table 10 | CREATE TABLE "telegram_user_states" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "qts" bigint NOT NULL DEFAULT 0, "pts" bigint NOT NULL DEFAULT 0, "date" bigint NOT NULL DEFAULT 0, "seq" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("id")); 11 | -- Create "telegram_channel_states" table 12 | CREATE TABLE "telegram_channel_states" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "channel_id" bigint NOT NULL, "pts" bigint NOT NULL DEFAULT 0, "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); 13 | -- Create index "telegramchannelstate_user_id_channel_id" to table: "telegram_channel_states" 14 | CREATE UNIQUE INDEX "telegramchannelstate_user_id_channel_id" ON "telegram_channel_states" ("user_id", "channel_id"); 15 | -------------------------------------------------------------------------------- /migrations/20241208073032_telegram_account.sql: -------------------------------------------------------------------------------- 1 | -- Create "telegram_accounts" table 2 | CREATE TABLE "telegram_accounts" ("id" character varying NOT NULL, "code" character varying NOT NULL, "code_at" timestamptz NOT NULL, "data" bytea NOT NULL, "state" character varying NOT NULL, "status" character varying NOT NULL, PRIMARY KEY ("id")); 3 | -------------------------------------------------------------------------------- /migrations/20241208082152_telegram_acc_session.sql: -------------------------------------------------------------------------------- 1 | -- Modify "telegram_accounts" table 2 | ALTER TABLE "telegram_accounts" ADD COLUMN "session" bytea NOT NULL; 3 | -------------------------------------------------------------------------------- /migrations/20241208112252_telegram_acc_nillable.sql: -------------------------------------------------------------------------------- 1 | -- Modify "telegram_accounts" table 2 | ALTER TABLE "telegram_accounts" DROP COLUMN "data", ALTER COLUMN "state" SET DEFAULT 'New'; 3 | -------------------------------------------------------------------------------- /migrations/20241208112922_telegram_acc_nillable.sql: -------------------------------------------------------------------------------- 1 | -- Modify "telegram_accounts" table 2 | ALTER TABLE "telegram_accounts" ALTER COLUMN "code" DROP NOT NULL, ALTER COLUMN "code_at" DROP NOT NULL, ALTER COLUMN "session" DROP NOT NULL; 3 | -------------------------------------------------------------------------------- /migrations/20241208113242_telegram_acc_rename.sql: -------------------------------------------------------------------------------- 1 | -- Rename a column from "session" to "session_data" 2 | ALTER TABLE "telegram_accounts" RENAME COLUMN "session" TO "session_data"; 3 | -------------------------------------------------------------------------------- /migrations/atlas.sum: -------------------------------------------------------------------------------- 1 | h1:7WUDXEpl/90uHuqyZY4COEjbVLH+7+odj4peoWkTtbU= 2 | 20241202075819_init.sql h1:r0lJLQNwt57c2NIRwSmfUM+3yL/2NOZ4seeGxvzgVj0= 3 | 20241208073032_telegram_account.sql h1:ImERWJTnJnTlfPeZjktBmu+f/jDCVRcnZ9Mhep9W52Y= 4 | 20241208082152_telegram_acc_session.sql h1:7zf4FeSz/FDlB0tknYtu1y4PCDa5G55Q5HckDmwJwVA= 5 | 20241208112252_telegram_acc_nillable.sql h1:bpSNEnZ+iExgRcSmNqBJyFvUtPn9pShWmHSPehlrGfo= 6 | 20241208112922_telegram_acc_nillable.sql h1:iBcHFUbhPdLiEhvJxekB/Z+ijdvj8hdedpR3EI9U3JA= 7 | 20241208113242_telegram_acc_rename.sql h1:wmR7yS7xpOx9Ao7QVeqZ9gCfUgA3l2SECnl0dyO7wqI= 8 | -------------------------------------------------------------------------------- /role.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | namespace: gotd 6 | name: gotd:deploy 7 | rules: 8 | - apiGroups: ["apps"] 9 | resources: ["deployments", "pods"] 10 | verbs: 11 | - get 12 | - list 13 | - watch 14 | - create 15 | - update 16 | - patch 17 | - delete 18 | -------------------------------------------------------------------------------- /service.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | namespace: gotd 5 | name: bot 6 | labels: 7 | app: status 8 | prometheus: "true" 9 | spec: 10 | selector: 11 | app: bot 12 | ports: 13 | - port: 80 14 | protocol: TCP 15 | targetPort: 8080 16 | name: http 17 | - port: 8080 18 | protocol: TCP 19 | targetPort: 8090 20 | name: metrics 21 | --- 22 | apiVersion: networking.k8s.io/v1 23 | kind: Ingress 24 | metadata: 25 | name: bot 26 | namespace: gotd 27 | annotations: 28 | # use the shared ingress-nginx 29 | kubernetes.io/ingress.class: "nginx" 30 | labels: 31 | app: status 32 | spec: 33 | rules: 34 | - host: bot.gotd.dev 35 | http: 36 | paths: 37 | - path: / 38 | pathType: Prefix 39 | backend: 40 | service: 41 | name: bot 42 | port: 43 | name: http 44 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | package bot 4 | 5 | import ( 6 | _ "entgo.io/ent/entc" 7 | _ "entgo.io/ent/entc/gen" 8 | _ "github.com/ogen-go/ent2ogen" 9 | _ "github.com/ogen-go/ogen" 10 | _ "github.com/ogen-go/ogen/conv" 11 | _ "github.com/ogen-go/ogen/gen" 12 | _ "github.com/ogen-go/ogen/gen/genfs" 13 | _ "github.com/ogen-go/ogen/middleware" 14 | _ "github.com/ogen-go/ogen/ogenerrors" 15 | _ "github.com/ogen-go/ogen/otelogen" 16 | _ "go.opentelemetry.io/otel/semconv/v1.19.0" 17 | _ "go.uber.org/mock/mockgen" 18 | _ "go.uber.org/mock/mockgen/model" 19 | ) 20 | --------------------------------------------------------------------------------