just throwin there

This commit is contained in:
spushk 2026-05-15 22:27:28 +05:00 • committed by spushk
commit 278d7cf05d
Signed by: spushk
GPG key ID: 7797B761E105E8AA
52 changed files with 5712 additions and 0 deletions

12
.cargo/config.toml Normal file
View file

@ -0,0 +1,12 @@
[build]
rustflags = ["--cfg=has_std", "-Zthreads=6"]
[target.x86_64-unknown-linux-gnu]
rustflags = ["-Csplit-debuginfo=unpacked", "-Clink-arg=-fuse-ld=mold"]
[profile.release.build-override]
inherits = "release"
[registries.Technologygeek]
index = "sparse+https://source.technologygeek.ru/api/packages/Technologygeek/cargo/"
credential-provider = ["cargo:token"]

View file

@ -0,0 +1,3 @@
[build]
rustflags = ["--cfg=has_std", "-Zthreads=6", "-Zlocation-detail=none", "-Cpanic=abort"]
target = ["x86_64-unknown-linux-musl"]

8
.containerignore Normal file
View file

@ -0,0 +1,8 @@
Containerfile
.envrc
target
Cargo.lock
dev-volumes
adsbot.conf.d.example
adsbot.log
adsbot.db

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
/target
/dev-volumes
.envrc
adsbot.db
adsbot.log
adsbot.conf

3066
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

69
Cargo.toml Normal file
View file

@ -0,0 +1,69 @@
[package]
name = "adsbot"
version = "0.1.0"
rust-version = "1.95"
edition = "2024"
license = "MIT"
license-file = "LICENSE"
authors = ["Svyatoslav Pushkarev <spushk@spushk.ru>"]
publish = false
[dependencies]
## Rt
tokio.version = "^1"
tokio.features = ["full"]
deadpool.version = "^0.13"
deadpool.features = ["rt_tokio_1", "serde"]
async-trait.version = "^0.1"
## Logging
tracing.version = "^0.1"
tracing.features = ["max_level_debug", "release_max_level_info"]
tracing-subscriber.version = "^0.3"
tracing-subscriber.features = ["chrono"]
## Database
diesel.version = "^2"
diesel.features = ["chrono", "sqlite"]
diesel_migrations.version = "^2"
diesel_migrations.features = ["sqlite"]
libsqlite3-sys.version = "^0"
libsqlite3-sys.features = ["bundled"]
## Marshal
serde.version = "^1"
serde.features = ["derive"]
serde_json.version = "^1"
toml.version = "^1"
serde_urlencoded.version = "^0.7"
urlencoding.version = "^2"
## Net
teloxide.version = "^0.17"
teloxide.default-features = false
teloxide.features = ["tracing", "nightly", "ctrlc_handler", "rustls", "macros"]
oauth2.version = "^5"
reqwest.version = "^0.12"
reqwest.default-features = false
reqwest.features = ["charset", "http2", "rustls-tls", "json"]
## Util
chrono.version = "^0.4"
regex.version = "^1"
## Codegen
thiserror.version = "^2"
const-str.version = "^1"
[profile.dev]
opt-level = 0
lto = "off"
codegen-units = 256
panic = "unwind"
debug = "full"
[profile.dev.package."*"]
opt-level = 3
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
panic = "abort"
debug = "none"
strip = "symbols"

65
Containerfile Normal file
View file

@ -0,0 +1,65 @@
FROM source.spushk.ru/rustyworks/rust-alpine-mimalloc:1.95 as build
WORKDIR /build
ENV OPENSSL_DIR=/usr
ENV TARGET_CC=x86_64-linux-musl-clang
ENV CARGO_BUILD_TARGET=x86_64-unknown-linux-musl
COPY .cargo .cargo
COPY rust-toolchain.toml rust-toolchain.toml
RUN --mount=type=secret,id=cargo-token \
--mount=type=cache,target=/usr/local/rustup,id=rustup_cache \
cat /run/secrets/cargo-token | cargo login --quiet --registry Technologygeek
FROM build as planner
WORKDIR /build
COPY . .
RUN --mount=type=cache,target=/usr/local/cargo/registry,id=cargo_registry \
--mount=type=cache,target=/usr/local/rustup,id=rustup_cache \
cargo chef prepare --recipe-path=recipe.json
FROM build as build-debug
WORKDIR /build
COPY --from=planner /build/recipe.json recipe.json
RUN --mount=type=cache,target=/usr/local/cargo/registry,id=cargo_registry \
--mount=type=cache,target=/usr/local/rustup,id=rustup_cache \
cargo --config=.cargo/container_debug.toml \
chef cook --recipe-path=recipe.json
COPY . .
RUN --mount=type=cache,target=/usr/local/cargo/registry,id=cargo_registry \
--mount=type=cache,target=/usr/local/rustup,id=rustup_cache \
cargo --config=.cargo/container_debug.toml \
test
RUN --mount=type=cache,target=/usr/local/cargo/registry,id=cargo_registry \
--mount=type=cache,target=/usr/local/rustup,id=rustup_cache \
cargo --config=.cargo/container_debug.toml \
build
FROM build as build-release
WORKDIR /build
COPY --from=planner /build/recipe.json recipe.json
RUN --mount=type=cache,target=/usr/local/cargo/registry,id=cargo_registry \
--mount=type=cache,target=/usr/local/rustup,id=rustup_cache \
cargo --config=.cargo/container_release.toml \
chef cook --release --recipe-path=recipe.json
COPY . .
RUN --mount=type=cache,target=/usr/local/cargo/registry,id=cargo_registry \
--mount=type=cache,target=/usr/local/rustup,id=rustup_cache \
cargo --config=.cargo/container_release.toml \
build --release
FROM scratch as app
WORKDIR /
VOLUME /adsbot.db
VOLUME /adsbot.log
EXPOSE 3000
FROM app as app-debug
WORKDIR /
COPY --from=build-debug /build/target/x86_64-unknown-linux-musl/debug/adsbot /adsbot
ENV RUST_LOG="debug"
ENTRYPOINT [ "/adsbot" ]
FROM app as app-release
WORKDIR /
COPY --from=build-release /build/target/x86_64-unknown-linux-musl/release/adsbot /adsbot
ENV RUST_LOG="info"
ENTRYPOINT [ "/adsbot" ]

52
Makefile.toml Normal file
View file

@ -0,0 +1,52 @@
[env]
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
[env.development]
PROD = false
DEV = true
_MK_IMAGE_TARGET = 'app-debug'
_MK_IMAGE = 'source.spushk.ru/rustyworks/adsbot:debug'
[env.production]
PROD = true
DEV = false
_MK_IMAGE_TARGET = 'app-release'
_MK_IMAGE = 'source.spushk.ru/rustyworks/adsbot:v0.1'
[tasks.rustfmt]
command = "cargo"
args = ["fmt", "--", "--emit=files"]
[tasks.format]
dependencies = ["rustfmt"]
[tasks.build]
workspace = false
command = "cargo"
args = ["build"]
[tasks.image]
workspace = false
condition.env_set = ["_MK_IMAGE_TARGET", "_MK_IMAGE"]
command = "podman"
args = [
"build",
"--target", "${_MK_IMAGE_TARGET}",
"-t", "${_MK_IMAGE}",
"."
]
[tasks.image-view]
workspace = false
condition.env_set = ["_MK_IMAGE"]
condition.files_exist = ["${CARGO_MAKE_WORKING_DIRECTORY}/dev-volumes"]
command = "podman"
args = [
"run",
"--rm",
"-p3000:3000",
"-v./dev-volumes/adsbot.conf:/adsbot.conf",
"-v./dev-volumes/adsbot.log:/adsbot.log",
"-v./dev-volumes/adsbot.db:/adsbot.db",
"${_MK_IMAGE}"
]

232
README.md Normal file
View file

@ -0,0 +1,232 @@
Very simple bot which will monitor VK ADS form leads and pass them to telegram upstream. Just throwin it here coz friend asked.
## VK ADS API lead forms
See the [configuration example](./adsbot.conf.example)
## Arbitrary POST forms (TBD)
The arbitrary POST form is created with `adsbot.conf/leadbot/features/formlead.d` TOML
configuration. The format is explained in the example.
POST request head shall contain `Authorization` header with one of the configured secret values.
POST request raw body size in bytes is limited by 4KiB.
Request raw body may be:
- Either valid UTF-8 JSON string with `Content-Type: application/json`
- Or a messagepack encoded message with `Content-type: application/msgpack`
- Or a raw text, which is reported as-is without parsing with `Content-type: text/plain`
### Response
Depending on the request criteria and server state
### form
Parsed request body shall contain the field `form`, which is an array limited
by 128 entries.
Excessive entries shall not be reported, form entries are reported in the order of presense.
Each of entries must follow the schema:
```json
{
type: "object",
"properties": {
"t": {
"type": "string",
"enum": ["text","tel","personal","yn"]
},
"d": {
"type": "object"
}
}
}
```
Any form entry, which does not conform to the specified schema, will not be processed.
#### `t`/`d` fields
**"text"** type:
With the `"t": "text"` field set on the POST data, the `d` field is expected to
follow the schema:
```json
{
"type": "object",
"properties": {
"q": {
"type": "string",
"description": "The form question/field name to display in report"
},
"a": {
"type": "string",
"description": "The user answer/value to display in report"
}
}
}
```
The "text" entry shall be included into report as-is.
**"tel"** type:
With the `"t": "tel"` field set on the POST data, the `d` field is expected to
follow the schema:
```json
{
"type": "object",
"properties": {
"q": {
"type": "string",
"description": "The form question/field name to display in report"
},
"a": {
"type": "string",
"description": "The value, which will be treated as personal phone number",
}
}
}
```
When possible the "tel" entry shall be included into report as `tel:` anchor/link,
and (when possible) shall be hidden under protective block, which does not grant visual access to the phone number without manual interaction or authorization.
**"personal"** type:
With the `"t": "personal"` field set on the POST data, the `d` field is expected to
follow the schema:
```json
{
"type": "object",
"properties": {
"q": {
"type": "string",
"description": "The form question/field name to display in report"
},
"a": {
"type": "string",
"description": "The value, which will be treated as personal information"
}
}
}
```
When possible shall be hidden under protective block, which does not grant visual access to the phone number without manual interaction or authorization.
**"yesno"** type:
With the `"t": "yn"` field set on the POST data, the `d` field is expected to
follow the schema:
```json
{
"type": "object",
"properties": {
"q": {
"type": "string",
"description": "The form question/field name to display in report"
},
"a": {
"type": "boolean",
"description": "The value, which will be treated as yes/no answer"
}
}
}
```
#### example
The following shall not be parsed and will be reported as-is
```
-- HEADERS --
Authorization: pqo8jdp9:p183ycnpdo1
Content-Type: text/plain
-- BODY --
I am a text. I wanna MANY alpacas. I demand:
- At least 1 black alpaca
- At most 4 white alpacas
- All alpacas to be either black or white
- Exactly 666 alpacas
```
The following shall be parsed as JSON
```
-- HEADERS --
Authorization: 0192mu:odump298yfcniudo8c3mo
Content-type: application/json
-- BODY --
{
"form": [
{
"t": "personal",
"d": {
"q": "First name",
"a": "Danis"
}
},
{
"t": "text",
"d": {
"q": "How much aplacas do you have?",
"a": "120"
}
},
{
"t": "tel",
"d": {
"q": "Phone",
"a": "88005553535"
}
},
{
"t": "yn",
"d": {
"q": "Do you want another aplaca?",
"a": true
}
}
]
}
```
## Development
**Software requirements:**
- `cargo-make`
- `diesel-cli`
- `podman`
**Prerequistes:**
- Create the `dev-volumes/{adsbot.log,adsbot.db,adsbot.conf}` directories,
and configure the `dev-volumes/adsbot.conf` by example provided in `adsbot.conf.example`
**Altering database schema**:
Dont forget to create appropriate migrations by
```sh
diesel migration generate --diff-schema=<SCHEMA.RS> <MIGRATION_NAME>
```
## Building from source
`cargo make build` for static binary build
`cargo make image` for container image
Please, note, that by default on each program startup the license and authors information from Cargo.toml are printed into the log.
If you want to disable it - set the `LOG_CREDENTIALS="none"` env variable during the build

View file

@ -0,0 +1,10 @@
[telegram.bot]
# Telegram bot api token
token = "TOKEN"
[[telegram.admin]]
user = 111111111
[[logging.telegram]]
Chat ID, which will receive logs. For private chat with user - go to the bot and ask him /echo id
chat = 22222222222

View file

@ -0,0 +1,2 @@
info="Keep this directory existing to activate echoing features"

View file

@ -0,0 +1,17 @@
# Unique (for the formlead feature) identifier. Must be valid URL subpath
id = "unique_id_url_safe"
[[form.post]]
# The display name for the form report
name = "Display name"
# within the current example POST request is accepted at /unique_id_url_safe/subpath_under_id
path = "subpath_under_id"
# The request sender shall pass "AUTHORIZATION"
# "xqoi83nyra0s98d:4e7b44d9a7b220e2b4bd8ba60085f0aa"
clients = [
{id="xqoi83nyra0s98d", secret="4e7b44d9a7b220e2b4bd8ba60085f0aa"}
]
# The following destinations will receive form reports
reports = [
{telegram = {chat = -10000000000000}}
]

View file

@ -0,0 +1,23 @@
# The unique (for the vkadslead feature) id. Must be valid URL subpath
id = "unique_id_url_safe"
[api]
# Client ID received from VK ADS api
client_id = "VKADS API CLIENT ID"
# Client secret receiver from VK ADS api
client_secret = "VKADS API CLIENT SECRET"
[[telegram.admin]]
user = 111111111
[[form]]
# Display name for the form report
name = "Display name"
# Form identifier as shown by VK ADS
id = "VKADS form id"
# I forgot what this does
anonymous = false
# The following destinations shall receive form reports
reports = [
{telegram = {chat = -10000000000000}}
]

13
diesel.toml Normal file
View file

@ -0,0 +1,13 @@
# For documentation on how to configure this file,
# see https://diesel.rs/guides/configuring-diesel-cli
[print_schema.feature_vkadslead]
file = "src/features/vkadslead/db/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
[print_schema.integration_telegram]
file = "src/telegram/db/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
[migrations_directory]
dir = "migrations"

0
migrations/.keep Normal file
View file

View file

@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
DROP TABLE IF EXISTS `features_vkadslead_clientauth`;
DROP TABLE IF EXISTS `features_vkadslead_formsync`;

View file

@ -0,0 +1,19 @@
-- Your SQL goes here
CREATE TABLE IF NOT EXISTS `features_vkadslead_clientauth` (
`instance` TEXT NOT NULL PRIMARY KEY,
`client_id` TEXT NOT NULL,
`access_token` TEXT NOT NULL,
`ctime` TIMESTAMP NOT NULL,
`expires_in` INTEGER,
`refresh_token` TEXT,
`scope` TEXT,
`token_type` TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS `features_vkadslead_formsync` (
`instance_id` TEXT NOT NULL,
`form_id` TEXT NOT NULL,
`stime` TIMESTAMP NOT NULL,
PRIMARY KEY (`instance_id`, `form_id`),
FOREIGN KEY (`instance_id`) REFERENCES `features_vkadslead_clientauth` (`instance`)
);

View file

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE IF EXISTS `integrations_telegram_auth`;

View file

@ -0,0 +1,5 @@
-- Your SQL goes here
CREATE TABLE IF NOT EXISTS `integrations_telegram_auth` (
`id` INTEGER NOT NULL PRIMARY KEY,
`token` TEXT NOT NULL
);

3
rust-toolchain.toml Normal file
View file

@ -0,0 +1,3 @@
[toolchain]
channel = "nightly-2026-05-10"
components = ["rust-src"]

Some files were not shown because too many files have changed in this diff Show more