├── .cargo
└── config
├── .dockerignore
├── .drone.yml
├── .env
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── Dockerfile
├── Jenkinsfile
├── LICENSE
├── README.md
├── docker-compose.yml.example
├── docker
├── drone
│ ├── Dockerfile
│ └── manifest.tmpl
└── prod
│ └── docker-compose.yml
├── flake.lock
├── flake.nix
├── relay.nix
├── scss
└── index.scss
├── src
├── admin.rs
├── admin
│ ├── client.rs
│ └── routes.rs
├── apub.rs
├── args.rs
├── build.rs
├── collector.rs
├── config.rs
├── data.rs
├── data
│ ├── actor.rs
│ ├── last_online.rs
│ ├── media.rs
│ ├── node.rs
│ └── state.rs
├── db.rs
├── error.rs
├── extractors.rs
├── future.rs
├── jobs.rs
├── jobs
│ ├── apub.rs
│ ├── apub
│ │ ├── announce.rs
│ │ ├── follow.rs
│ │ ├── forward.rs
│ │ ├── reject.rs
│ │ └── undo.rs
│ ├── contact.rs
│ ├── deliver.rs
│ ├── deliver_many.rs
│ ├── instance.rs
│ ├── nodeinfo.rs
│ ├── process_listeners.rs
│ └── record_last_online.rs
├── main.rs
├── middleware.rs
├── middleware
│ ├── payload.rs
│ ├── timings.rs
│ ├── verifier.rs
│ └── webfinger.rs
├── requests.rs
├── routes.rs
├── routes
│ ├── actor.rs
│ ├── healthz.rs
│ ├── inbox.rs
│ ├── index.rs
│ ├── media.rs
│ ├── nodeinfo.rs
│ └── statics.rs
├── spawner.rs
└── telegram.rs
├── systemd
├── example-relay.service
├── example-relay.service.env
└── example-relay.socket
└── templates
├── admin.rs.html
├── index.rs.html
├── info.rs.html
└── instance.rs.html
/.cargo/config:
--------------------------------------------------------------------------------
1 | [build]
2 | rustflags = ["--cfg", "tokio_unstable"]
3 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | /target
2 | /artifacts
3 | /sled
4 | .dockerignore
5 | Dockerfile
6 | Jenkinsfile
7 |
--------------------------------------------------------------------------------
/.drone.yml:
--------------------------------------------------------------------------------
1 | kind: pipeline
2 | type: docker
3 | name: clippy
4 |
5 | platform:
6 | arch: amd64
7 |
8 | clone:
9 | disable: true
10 |
11 | steps:
12 | - name: clone
13 | image: alpine/git:latest
14 | user: root
15 | commands:
16 | - git clone $DRONE_GIT_HTTP_URL .
17 | - git checkout $DRONE_COMMIT
18 | - chown -R 991:991 .
19 |
20 | - name: clippy
21 | image: asonix/rust-builder:latest-linux-amd64
22 | pull: always
23 | commands:
24 | - rustup component add clippy
25 | - cargo clippy --no-deps -- -D warnings
26 |
27 | trigger:
28 | event:
29 | - push
30 | - pull_request
31 | - tag
32 |
33 | ---
34 |
35 | kind: pipeline
36 | type: docker
37 | name: tests
38 |
39 | platform:
40 | arch: amd64
41 |
42 | clone:
43 | disable: true
44 |
45 | steps:
46 | - name: clone
47 | image: alpine/git:latest
48 | user: root
49 | commands:
50 | - git clone $DRONE_GIT_HTTP_URL .
51 | - git checkout $DRONE_COMMIT
52 | - chown -R 991:991 .
53 |
54 | - name: tests
55 | image: asonix/rust-builder:latest-linux-amd64
56 | pull: always
57 | commands:
58 | - cargo test
59 |
60 | trigger:
61 | event:
62 | - push
63 | - pull_request
64 | - tag
65 |
66 | ---
67 |
68 | kind: pipeline
69 | type: docker
70 | name: check-amd64
71 |
72 | platform:
73 | arch: amd64
74 |
75 | clone:
76 | disable: true
77 |
78 | steps:
79 | - name: clone
80 | image: alpine/git:latest
81 | user: root
82 | commands:
83 | - git clone $DRONE_GIT_HTTP_URL .
84 | - git checkout $DRONE_COMMIT
85 | - chown -R 991:991 .
86 |
87 | - name: check
88 | image: asonix/rust-builder:latest-linux-amd64
89 | pull: always
90 | commands:
91 | - cargo check --target=$TARGET
92 |
93 | trigger:
94 | event:
95 | - push
96 | - pull_request
97 |
98 | ---
99 |
100 | kind: pipeline
101 | type: docker
102 | name: build-amd64
103 |
104 | platform:
105 | arch: amd64
106 |
107 | clone:
108 | disable: true
109 |
110 | steps:
111 | - name: clone
112 | image: alpine/git:latest
113 | user: root
114 | commands:
115 | - git clone $DRONE_GIT_HTTP_URL .
116 | - git checkout $DRONE_COMMIT
117 | - chown -R 991:991 .
118 |
119 | - name: build
120 | image: asonix/rust-builder:latest-linux-amd64
121 | pull: always
122 | commands:
123 | - cargo build --target=$TARGET --release
124 | - $TOOL-strip target/$TARGET/release/relay
125 | - cp target/$TARGET/release/relay .
126 | - cp relay relay-linux-amd64
127 |
128 | - name: push
129 | image: plugins/docker:20
130 | settings:
131 | username: asonix
132 | password:
133 | from_secret: dockerhub_token
134 | repo: asonix/relay
135 | dockerfile: docker/drone/Dockerfile
136 | auto_tag: true
137 | auto_tag_suffix: linux-amd64
138 | build_args:
139 | - REPO_ARCH=amd64
140 |
141 | - name: publish
142 | image: plugins/gitea-release:1
143 | settings:
144 | api_key:
145 | from_secret: gitea_token
146 | base_url: https://git.asonix.dog
147 | files:
148 | - relay-linux-amd64
149 |
150 | depends_on:
151 | - clippy
152 | - tests
153 |
154 | trigger:
155 | event:
156 | - tag
157 |
158 | ---
159 |
160 | kind: pipeline
161 | type: docker
162 | name: check-arm64v8
163 |
164 | platform:
165 | arch: amd64
166 |
167 | clone:
168 | disable: true
169 |
170 | steps:
171 | - name: clone
172 | image: alpine/git:latest
173 | user: root
174 | commands:
175 | - git clone $DRONE_GIT_HTTP_URL .
176 | - git checkout $DRONE_COMMIT
177 | - chown -R 991:991 .
178 |
179 | - name: check
180 | image: asonix/rust-builder:latest-linux-arm64v8
181 | pull: always
182 | commands:
183 | - cargo check --target=$TARGET
184 |
185 | trigger:
186 | event:
187 | - push
188 | - pull_request
189 |
190 | ---
191 |
192 | kind: pipeline
193 | type: docker
194 | name: build-arm64v8
195 |
196 | platform:
197 | arch: amd64
198 |
199 | clone:
200 | disable: true
201 |
202 | steps:
203 | - name: clone
204 | image: alpine/git:latest
205 | user: root
206 | commands:
207 | - git clone $DRONE_GIT_HTTP_URL .
208 | - git checkout $DRONE_COMMIT
209 | - chown -R 991:991 .
210 |
211 | - name: build
212 | image: asonix/rust-builder:latest-linux-arm64v8
213 | pull: always
214 | commands:
215 | - cargo build --target=$TARGET --release
216 | - $TOOL-strip target/$TARGET/release/relay
217 | - cp target/$TARGET/release/relay .
218 | - cp relay relay-linux-arm64v8
219 |
220 | - name: push
221 | image: plugins/docker:20
222 | settings:
223 | username: asonix
224 | password:
225 | from_secret: dockerhub_token
226 | repo: asonix/relay
227 | dockerfile: docker/drone/Dockerfile
228 | auto_tag: true
229 | auto_tag_suffix: linux-arm64v8
230 | build_args:
231 | - REPO_ARCH=arm64v8
232 |
233 | - name: publish
234 | image: plugins/gitea-release:1
235 | settings:
236 | api_key:
237 | from_secret: gitea_token
238 | base_url: https://git.asonix.dog
239 | files:
240 | - relay-linux-arm64v8
241 |
242 | depends_on:
243 | - clippy
244 | - tests
245 |
246 | trigger:
247 | event:
248 | - tag
249 |
250 | ---
251 |
252 | kind: pipeline
253 | type: docker
254 | name: check-arm32v7
255 |
256 | platform:
257 | arch: amd64
258 |
259 | clone:
260 | disable: true
261 |
262 | steps:
263 | - name: clone
264 | image: alpine/git:latest
265 | user: root
266 | commands:
267 | - git clone $DRONE_GIT_HTTP_URL .
268 | - git checkout $DRONE_COMMIT
269 | - chown -R 991:991 .
270 |
271 | - name: check
272 | image: asonix/rust-builder:latest-linux-arm32v7
273 | pull: always
274 | commands:
275 | - cargo check --target=$TARGET
276 |
277 | trigger:
278 | event:
279 | - push
280 | - pull_request
281 |
282 | ---
283 |
284 | kind: pipeline
285 | type: docker
286 | name: build-arm32v7
287 |
288 | platform:
289 | arch: amd64
290 |
291 | clone:
292 | disable: true
293 |
294 | steps:
295 | - name: clone
296 | image: alpine/git:latest
297 | user: root
298 | commands:
299 | - git clone $DRONE_GIT_HTTP_URL .
300 | - git checkout $DRONE_COMMIT
301 | - chown -R 991:991 .
302 |
303 | - name: build
304 | image: asonix/rust-builder:latest-linux-arm32v7
305 | pull: always
306 | commands:
307 | - cargo build --target=$TARGET --release
308 | - $TOOL-strip target/$TARGET/release/relay
309 | - cp target/$TARGET/release/relay .
310 | - cp relay relay-linux-arm32v7
311 |
312 | - name: push
313 | image: plugins/docker:20
314 | settings:
315 | username: asonix
316 | password:
317 | from_secret: dockerhub_token
318 | repo: asonix/relay
319 | dockerfile: docker/drone/Dockerfile
320 | auto_tag: true
321 | auto_tag_suffix: linux-arm32v7
322 | build_args:
323 | - REPO_ARCH=arm32v7
324 |
325 | - name: publish
326 | image: plugins/gitea-release:1
327 | settings:
328 | api_key:
329 | from_secret: gitea_token
330 | base_url: https://git.asonix.dog
331 | files:
332 | - relay-linux-arm32v7
333 |
334 | depends_on:
335 | - clippy
336 | - tests
337 |
338 | trigger:
339 | event:
340 | - tag
341 |
342 | ---
343 |
344 | kind: pipeline
345 | type: docker
346 | name: manifest
347 |
348 | platform:
349 | arch: amd64
350 |
351 | clone:
352 | disable: true
353 |
354 | steps:
355 | - name: clone
356 | image: alpine/git:latest
357 | user: root
358 | commands:
359 | - git clone $DRONE_GIT_HTTP_URL .
360 | - git checkout $DRONE_COMMIT
361 | - chown -R 991:991 .
362 |
363 | - name: manifest
364 | image: plugins/manifest:1
365 | settings:
366 | username: asonix
367 | password:
368 | from_secret: dockerhub_token
369 | dump: true
370 | auto_tag: true
371 | ignore_missing: true
372 | spec: docker/drone/manifest.tmpl
373 |
374 |
375 | depends_on:
376 | - build-amd64
377 | - build-arm64v8
378 | - build-arm32v7
379 |
380 | trigger:
381 | event:
382 | - tag
383 |
384 | ---
385 |
386 | kind: pipeline
387 | type: docker
388 | name: publish-crate
389 |
390 | platform:
391 | arch: amd64
392 |
393 | clone:
394 | disable: true
395 |
396 | steps:
397 | - name: clone
398 | image: alpine/git:latest
399 | user: root
400 | commands:
401 | - git clone $DRONE_GIT_HTTP_URL .
402 | - git checkout $DRONE_COMMIT
403 | - chown -R 991:991 .
404 |
405 | - name: publish
406 | image: asonix/rust-builder:latest-linux-amd64
407 | pull: always
408 | environment:
409 | CRATES_IO_TOKEN:
410 | from_secret: crates_io_token
411 | commands:
412 | - cargo publish --token $CRATES_IO_TOKEN
413 |
414 | depends_on:
415 | - build-amd64
416 | - build-arm64v8
417 | - build-arm32v7
418 |
419 | trigger:
420 | event:
421 | - tag
422 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | HOSTNAME=localhost:8079
2 | PORT=8079
3 | HTTPS=false
4 | DEBUG=true
5 | RESTRICTED_MODE=true
6 | VALIDATE_SIGNATURES=false
7 | API_TOKEN=somesecretpassword
8 | FOOTER_BLURB="Contact @asonix for inquiries"
9 | LOCAL_DOMAINS="masto.asonix.dog"
10 | LOCAL_BLURB="
Welcome to my cool relay where I have cool relay things happening. I hope you enjoy your stay!
"
11 | # OPENTELEMETRY_URL=http://localhost:4317
12 | PROMETHEUS_ADDR=127.0.0.1
13 | PROMETHEUS_PORT=9000
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /artifacts
3 | /sled
4 | /.direnv
5 | /.envrc
6 | /result
7 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "ap-relay"
3 | description = "A simple activitypub relay"
4 | version = "0.3.106"
5 | authors = ["asonix "]
6 | license = "AGPL-3.0"
7 | readme = "README.md"
8 | repository = "https://git.asonix.dog/asonix/ap-relay"
9 | keywords = ["activitypub", "relay"]
10 | edition = "2021"
11 | build = "src/build.rs"
12 |
13 | [[bin]]
14 | name = "relay"
15 | path = "src/main.rs"
16 |
17 | [features]
18 | console = ["dep:console-subscriber"]
19 | default = []
20 |
21 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
22 |
23 | [dependencies]
24 | anyhow = "1.0"
25 | actix-web = { version = "4.4.0", default-features = false, features = ["compress-brotli", "compress-gzip", "rustls-0_21"] }
26 | actix-webfinger = { version = "0.5.0", default-features = false }
27 | activitystreams = "0.7.0-alpha.25"
28 | activitystreams-ext = "0.1.0-alpha.3"
29 | ammonia = "3.1.0"
30 | async-cpupool = "0.2.0"
31 | bcrypt = "0.15"
32 | base64 = "0.21"
33 | clap = { version = "4.0.0", features = ["derive"] }
34 | config = "0.13.0"
35 | console-subscriber = { version = "0.2", optional = true }
36 | dashmap = "5.1.0"
37 | dotenv = "0.15.0"
38 | flume = "0.11.0"
39 | lru = "0.12.0"
40 | metrics = "0.22.0"
41 | metrics-exporter-prometheus = { version = "0.13.0", default-features = false, features = [
42 | "http-listener",
43 | ] }
44 | metrics-util = "0.16.0"
45 | mime = "0.3.16"
46 | minify-html = "0.15.0"
47 | opentelemetry = "0.21"
48 | opentelemetry_sdk = { version = "0.21", features = ["rt-tokio"] }
49 | opentelemetry-otlp = "0.14"
50 | pin-project-lite = "0.2.9"
51 | # pinned to metrics-util
52 | quanta = "0.12.0"
53 | rand = "0.8"
54 | reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "stream"]}
55 | reqwest-middleware = "0.2"
56 | reqwest-tracing = "0.4.5"
57 | ring = "0.17.5"
58 | rsa = { version = "0.9" }
59 | rsa-magic-public-key = "0.8.0"
60 | rustls = "0.21.0"
61 | rustls-pemfile = "1.0.1"
62 | serde = { version = "1.0", features = ["derive"] }
63 | serde_json = "1.0"
64 | sled = "0.34.7"
65 | teloxide = { version = "0.12.0", default-features = false, features = [
66 | "ctrlc_handler",
67 | "macros",
68 | "rustls",
69 | ] }
70 | thiserror = "1.0"
71 | time = { version = "0.3.17", features = ["serde"] }
72 | tracing = "0.1"
73 | tracing-error = "0.2"
74 | tracing-log = "0.2"
75 | tracing-opentelemetry = "0.22"
76 | tracing-subscriber = { version = "0.3", features = [
77 | "ansi",
78 | "env-filter",
79 | "fmt",
80 | ] }
81 | tokio = { version = "1", features = ["full", "tracing"] }
82 | uuid = { version = "1", features = ["v4", "serde"] }
83 | streem = "0.2.0"
84 |
85 | [dependencies.background-jobs]
86 | version = "0.17.0"
87 | default-features = false
88 | features = ["error-logging", "metrics", "tokio"]
89 |
90 | [dependencies.http-signature-normalization-actix]
91 | version = "0.11.0"
92 | default-features = false
93 | features = ["server", "ring"]
94 |
95 | [dependencies.http-signature-normalization-reqwest]
96 | version = "0.11.0"
97 | default-features = false
98 | features = ["middleware", "ring"]
99 |
100 | [dependencies.tracing-actix-web]
101 | version = "0.7.9"
102 |
103 | [build-dependencies]
104 | anyhow = "1.0"
105 | dotenv = "0.15.0"
106 | ructe = { version = "0.17.0", features = ["sass", "mime03"] }
107 | toml = "0.8.0"
108 |
109 | [profile.dev.package.rsa]
110 | opt-level = 3
111 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1.4
2 | FROM alpine:3.19 AS alpine
3 | ARG TARGETPLATFORM
4 |
5 | RUN \
6 | --mount=type=cache,id=$TARGETPLATFORM-alpine,target=/var/cache/apk,sharing=locked \
7 | set -eux; \
8 | apk add -U libgcc;
9 |
10 | ################################################################################
11 |
12 | FROM alpine AS alpine-dev
13 | ARG TARGETPLATFORM
14 |
15 | RUN \
16 | --mount=type=cache,id=$TARGETPLATFORM-alpine,target=/var/cache/apk,sharing=locked \
17 | set -eux; \
18 | apk add -U musl-dev;
19 |
20 | ################################################################################
21 |
22 | FROM --platform=$BUILDPLATFORM rust:1 AS builder
23 | ARG BUILDPLATFORM
24 | ARG TARGETPLATFORM
25 |
26 | RUN \
27 | --mount=type=cache,id=$BUILDPLATFORM-debian,target=/var/cache,sharing=locked \
28 | --mount=type=cache,id=$BUILDPLATFORM-debian,target=/var/lib/apt,sharing=locked \
29 | set -eux; \
30 | case "${TARGETPLATFORM}" in \
31 | linux/i386) \
32 | dpkgArch='i386'; \
33 | ;; \
34 | linux/amd64) \
35 | dpkgArch='amd64'; \
36 | ;; \
37 | linux/arm64) \
38 | dpkgArch='arm64'; \
39 | ;; \
40 | *) echo "unsupported architecture"; exit 1 ;; \
41 | esac; \
42 | dpkg --add-architecture $dpkgArch; \
43 | apt-get update; \
44 | apt-get install -y --no-install-recommends \
45 | musl-dev:$dpkgArch \
46 | musl-tools:$dpkgArch \
47 | ;
48 |
49 | WORKDIR /opt/aode-relay
50 |
51 | RUN set -eux; \
52 | case "${TARGETPLATFORM}" in \
53 | linux/i386) arch='i686';; \
54 | linux/amd64) arch='x86_64';; \
55 | linux/arm64) arch='aarch64';; \
56 | *) echo "unsupported architecture"; exit 1 ;; \
57 | esac; \
58 | rustup target add "${arch}-unknown-linux-musl";
59 |
60 | ADD Cargo.lock Cargo.toml /opt/aode-relay/
61 | RUN cargo fetch;
62 |
63 | ADD . /opt/aode-relay
64 | COPY --link --from=alpine-dev / /opt/alpine/
65 |
66 | RUN set -eux; \
67 | case "${TARGETPLATFORM}" in \
68 | linux/i386) arch='i686';; \
69 | linux/amd64) arch='x86_64';; \
70 | linux/arm64) arch='aarch64';; \
71 | *) echo "unsupported architecture"; exit 1 ;; \
72 | esac; \
73 | ln -s "target/${arch}-unknown-linux-musl/release/relay" "aode-relay"; \
74 | export RUSTFLAGS="-C target-cpu=generic -C linker=${arch}-linux-musl-gcc -C target-feature=-crt-static -C link-self-contained=no -L /opt/alpine/lib -L /opt/alpine/usr/lib"; \
75 | cargo build --frozen --release --target="${arch}-unknown-linux-musl";
76 |
77 | ################################################################################
78 |
79 | FROM alpine
80 | ARG TARGETPLATFORM
81 |
82 | RUN \
83 | --mount=type=cache,id=$TARGETPLATFORM-alpine,target=/var/cache/apk,sharing=locked \
84 | set -eux; \
85 | apk add -U ca-certificates curl tini;
86 |
87 | COPY --link --from=builder /opt/aode-relay/aode-relay /usr/local/bin/aode-relay
88 |
89 | # Smoke test
90 | RUN /usr/local/bin/aode-relay --help
91 |
92 | # Some base env configuration
93 | ENV ADDR 0.0.0.0
94 | ENV PORT 8080
95 | ENV DEBUG false
96 | ENV VALIDATE_SIGNATURES true
97 | ENV HTTPS false
98 | ENV PRETTY_LOG false
99 | ENV PUBLISH_BLOCKS true
100 | ENV SLED_PATH "/var/lib/aode-relay/sled/db-0.34"
101 | ENV RUST_LOG warn
102 |
103 | VOLUME "/var/lib/aode-relay"
104 |
105 | ENTRYPOINT ["/sbin/tini", "--"]
106 |
107 | CMD ["/usr/local/bin/aode-relay"]
108 |
109 | EXPOSE 8080
110 |
111 | HEALTHCHECK CMD curl -sSf "localhost:$PORT/healthz" > /dev/null || exit 1
112 |
--------------------------------------------------------------------------------
/Jenkinsfile:
--------------------------------------------------------------------------------
1 | pipeline {
2 | agent none
3 | stages {
4 | stage('Test') {
5 | agent {
6 | docker {
7 | image 'ghcr.io/cleanc-lab/rust:1.72.0-slim-bookworm'
8 | args '--privileged --net=host -v /var/run/docker.sock:/var/run/docker.sock'
9 | }
10 | }
11 | stages {
12 | stage('Build') {
13 | steps {
14 | sh 'cargo build'
15 | }
16 | }
17 | stage('Nextest') {
18 | steps {
19 | sh '/usr/local/cargo/bin/cargo-nextest nextest run'
20 | }
21 | }
22 | }
23 | }
24 | stage('Docker') {
25 | agent {
26 | docker {
27 | image 'docker:24-cli'
28 | args '--privileged -v /var/run/docker.sock:/var/run/docker.sock'
29 | }
30 | }
31 | when {
32 | anyOf {
33 | branch 'interstellar-next';
34 | branch 'interstellar-dev';
35 | buildingTag();
36 | }
37 | }
38 | environment {
39 | DOCKER_REGISTRY = 'ghcr.io'
40 | GITHUB_ORG = 'interstellar-relay-community'
41 | DOCKER_IMAGE = "${env.DOCKER_REGISTRY}/${env.GITHUB_ORG}/aode-relay"
42 | GHCR_TOKEN = credentials('siliconforest-jenkins-github-pat-package-rw')
43 | }
44 | stages {
45 | stage('Prepare') {
46 | steps {
47 | script {
48 | if (env.BRANCH_NAME == 'interstellar-next') {
49 | env.DOCKER_TAG = 'latest'
50 | } else if (env.BRANCH_NAME == 'interstellar-dev') {
51 | env.DOCKER_TAG = 'develop'
52 | } else {
53 | env.DOCKER_TAG = env.TAG_NAME
54 | }
55 | }
56 | }
57 | }
58 | stage('Docker login') {
59 | steps {
60 | sh 'echo $GHCR_TOKEN_PSW | docker login ghcr.io -u $GHCR_TOKEN_USR --password-stdin'
61 | }
62 | }
63 | stage('Build') {
64 | matrix {
65 | axes {
66 | axis {
67 | name 'TARGET'
68 | values 'amd64', 'arm64'
69 | }
70 | }
71 | stages {
72 | stage('Build platform specific image') {
73 | steps {
74 | sh "docker buildx create --name container-${TARGET} --driver=docker-container"
75 | sh "docker buildx build --builder container-${TARGET} -t $DOCKER_IMAGE:$DOCKER_TAG-${TARGET} --platform linux/${TARGET} ."
76 | }
77 | }
78 | stage('Push platform specific image') {
79 | steps {
80 | sh "docker push $DOCKER_IMAGE:$DOCKER_TAG-${TARGET}"
81 | }
82 | }
83 | }
84 | }
85 | }
86 | stage('Docker manifest') {
87 | steps {
88 | sh "docker manifest create $DOCKER_IMAGE:$DOCKER_TAG --amend $DOCKER_IMAGE:$DOCKER_TAG-amd64 --amend $DOCKER_IMAGE:$DOCKER_TAG-arm64"
89 | }
90 | }
91 | stage('Docker push') {
92 | steps {
93 | sh "docker manifest push $DOCKER_IMAGE:$DOCKER_TAG"
94 | }
95 | }
96 | }
97 | post {
98 | always {
99 | sh 'docker logout "$DOCKER_REGISTRY"'
100 | }
101 | }
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AodeRelay
2 | _A simple and efficient activitypub relay_
3 |
4 | Forked from original [Aode Relay](https://git.asonix.dog/asonix/relay), applied some customization on it.
5 |
6 | ### Installation
7 | #### Docker
8 | If running docker, you can start the relay with the following command:
9 | ```
10 | $ sudo docker run --rm -it \
11 | -v "$(pwd):/mnt/" \
12 | -e ADDR=0.0.0.0 \
13 | -e SLED_PATH=/mnt/sled/db-0.34 \
14 | -p 8080:8080 \
15 | asonix/relay:0.3.85
16 | ```
17 | This will launch the relay with the database stored in "./sled/db-0.34" and listening on port 8080
18 | #### Cargo
19 | With cargo installed, the relay can be installed to your cargo bin directory with the following command
20 | ```
21 | $ cargo install ap-relay
22 | ```
23 | Then it can be run with this:
24 | ```
25 | $ ADDR=0.0.0.0 relay
26 | ```
27 | This will launch the relay with the database stored in "./sled/db-0.34" and listening on port 8080
28 | #### Source
29 | The relay can be launched directly from this git repository with the following commands:
30 | ```
31 | $ git clone https://git.asonix.dog/asonix/relay
32 | $ ADDR=0.0.0.0 cargo run --release
33 | ```
34 |
35 | ### Usage
36 | To simply run the server, the command is as follows
37 | ```bash
38 | $ ./relay
39 | ```
40 |
41 | #### Administration
42 | > **NOTE:** The server _must be running_ in order to update the lists with the following commands
43 |
44 | To learn about any other tasks, the `--help` flag can be passed
45 | ```bash
46 | An activitypub relay
47 |
48 | Usage: relay [OPTIONS]
49 |
50 | Options:
51 | -b A list of domains that should be blocked
52 | -a A list of domains that should be allowed
53 | -u, --undo Undo allowing or blocking domains
54 | -h, --help Print help information
55 | ```
56 |
57 | To add domains to the blocklist, use the `-b` flag and pass a list of domains
58 | ```bash
59 | $ ./relay -b asonix.dog blimps.xyz
60 | ```
61 | To remove domains from the blocklist, simply pass the `-u` flag along with `-b`
62 | ```bash
63 | $ ./relay -ub asonix.dog blimps.xyz
64 | ```
65 | The same rules apply for allowing domains, although domains are allowed with the `-a` flag
66 | ```bash
67 | $ ./relay -a asonix.dog blimps.xyz
68 | $ ./relay -ua asonix.dog blimps.xyz
69 | ```
70 |
71 | ### Configuration
72 | By default, all these values are set to development values. These are read from the environment, or
73 | from the `.env` file in the working directory.
74 | ```env
75 | HOSTNAME=localhost:8080
76 | ADDR=127.0.0.1
77 | PORT=8080
78 | DEBUG=true
79 | RESTRICTED_MODE=false
80 | VALIDATE_SIGNATURES=false
81 | HTTPS=false
82 | PRETTY_LOG=true
83 | PUBLISH_BLOCKS=false
84 | SLED_PATH=./sled/db-0.34
85 | ```
86 | To run this server in production, you'll likely want to set most of them
87 | ```env
88 | HOSTNAME=relay.my.tld
89 | ADDR=0.0.0.0
90 | PORT=8080
91 | DEBUG=false
92 | RESTRICTED_MODE=false
93 | VALIDATE_SIGNATURES=true
94 | HTTPS=true
95 | PRETTY_LOG=false
96 | PUBLISH_BLOCKS=true
97 | SLED_PATH=./sled/db-0.34
98 | RUST_LOG=warn
99 | API_TOKEN=somepasswordishtoken
100 | OPENTELEMETRY_URL=localhost:4317
101 | TELEGRAM_TOKEN=secret
102 | TELEGRAM_ADMIN_HANDLE=your_handle
103 | TLS_KEY=/path/to/key
104 | TLS_CERT=/path/to/cert
105 | FOOTER_BLURB="Contact @asonix for inquiries"
106 | LOCAL_DOMAINS=masto.asonix.dog
107 | LOCAL_BLURB="Welcome to my cool relay where I have cool relay things happening. I hope you enjoy your stay!
"
108 | PROMETHEUS_ADDR=0.0.0.0
109 | PROMETHEUS_PORT=9000
110 | CLIENT_TIMEOUT=10
111 | DELIVER_CONCURRENCY=8
112 | SIGNATURE_THREADS=2
113 | ```
114 |
115 | #### Descriptions
116 | ##### `HOSTNAME`
117 | The domain or IP address the relay is hosted on. If you launch the relay on `example.com`, that would be your HOSTNAME. The default is `localhost:8080`
118 | ##### `ADDR`
119 | The address the server binds to. By default, this is `127.0.0.1`, so for production cases it should be set to `0.0.0.0` or another public address.
120 | ##### `PORT`
121 | The port the server binds to, this is `8080` by default but can be changed if needed.
122 | ##### `DEBUG`
123 | Whether to print incoming activities to the console when requests hit the /inbox route. This defaults to `true`, but should be set to `false` in production cases. Since every activity sent to the relay is public anyway, this doesn't represent a security risk.
124 | ##### `RESTRICTED_MODE`
125 | This setting enables an 'allowlist' setup where only servers that have been explicitly enabled through the `relay -a` command can join the relay. This is `false` by default. If `RESTRICTED_MODE` is not enabled, then manually allowing domains with `relay -a` has no effect.
126 | ##### `VALIDATE_SIGNATURES`
127 | This setting enforces checking HTTP signatures on incoming activities. It defaults to `true`
128 | ##### `HTTPS`
129 | Whether the current server is running on an HTTPS port or not. This is used for generating URLs to the current running relay. By default it is set to `true`
130 | ##### `PUBLISH_BLOCKS`
131 | Whether or not to publish a list of blocked domains in the `nodeinfo` metadata for the server. It defaults to `false`.
132 | ##### `SLED_PATH`
133 | Where to store the on-disk database of connected servers. This defaults to `./sled/db-0.34`.
134 | ##### `RUST_LOG`
135 | The log level to print. Available levels are `ERROR`, `WARN`, `INFO`, `DEBUG`, and `TRACE`. You can also specify module paths to enable some logs but not others, such as `RUST_LOG=warn,tracing_actix_web=info,relay=info`. This defaults to `warn`
136 | ##### `SOURCE_REPO`
137 | The URL to the source code for the relay. This defaults to `https://git.asonix.dog/asonix/relay`, but should be changed if you're running a fork hosted elsewhere.
138 | ##### `REPOSITORY_COMMIT_BASE`
139 | The base path of the repository commit hash reference. For example, `/src/commit/` for Gitea, `/tree/` for GitLab.
140 | ##### `API_TOKEN`
141 | The Secret token used to access the admin APIs. This must be set for the commandline to function
142 | ##### `OPENTELEMETRY_URL`
143 | A URL for exporting opentelemetry spans. This is mostly useful for debugging. There is no default, since most people probably don't run an opentelemetry collector.
144 | ##### `TELEGRAM_TOKEN`
145 | A Telegram Bot Token for running the relay administration bot. There is no default.
146 | ##### `TELEGRAM_ADMIN_HANDLE`
147 | The handle of the telegram user allowed to administer the relay. There is no default.
148 | ##### `TLS_KEY`
149 | Optional - This is specified if you are running the relay directly on the internet and have a TLS key to provide HTTPS for your relay
150 | ##### `TLS_CERT`
151 | Optional - This is specified if you are running the relay directly on the internet and have a TLS certificate chain to provide HTTPS for your relay
152 | ##### `FOOTER_BLURB`
153 | Optional - Add custom notes in the footer of the page
154 | ##### `LOCAL_DOMAINS`
155 | Optional - domains of mastodon servers run by the same admin as the relay
156 | ##### `LOCAL_BLURB`
157 | Optional - description for the relay
158 | ##### `PROMETHEUS_ADDR`
159 | Optional - Address to bind to for serving the prometheus scrape endpoint
160 | ##### `PROMETHEUS_PORT`
161 | Optional - Port to bind to for serving the prometheus scrape endpoint
162 | ##### `CLIENT_TIMEOUT`
163 | Optional - How long the relay will hold open a connection (in seconds) to a remote server during
164 | fetches and deliveries. This defaults to 10
165 | ##### `DELIVER_CONCURRENCY`
166 | Optional - How many deliver requests the relay should allow to be in-flight per thread. the default
167 | is 8
168 | ##### `SIGNATURE_THREADS`
169 | Optional - Override number of threads used for signing and verifying requests. Default is
170 | `std::thread::available_parallelism()` (It tries to detect how many cores you have). If it cannot
171 | detect the correct number of cores, it falls back to 1.
172 | ##### 'PROXY_URL'
173 | Optional - URL of an HTTP proxy to forward outbound requests through
174 | ##### 'PROXY_USERNAME'
175 | Optional - username to provide to the HTTP proxy set with `PROXY_URL` through HTTP Basic Auth
176 | ##### 'PROXY_PASSWORD'
177 | Optional - password to provide to the HTTP proxy set with `PROXY_URL` through HTTP Basic Auth
178 |
179 | ### Subscribing
180 | Mastodon admins can subscribe to this relay by adding the `/inbox` route to their relay settings.
181 | For example, if the server is `https://relay.my.tld`, the correct URL would be
182 | `https://relay.my.tld/inbox`.
183 |
184 | Pleroma admins can subscribe to this relay by adding the `/actor` route to their relay settings. For
185 | example, if the server is `https://relay.my.tld`, the correct URL would be
186 | `https://relay.my.tld/actor`.
187 |
188 | ### Supported Activities
189 | - Accept Follow {remote-actor}, this is a no-op
190 | - Reject Follow {remote-actor}, an Undo Follow is sent to {remote-actor}
191 | - Announce {anything}, {anything} is Announced to listening servers
192 | - Create {anything}, {anything} is Announced to listening servers
193 | - Follow {self-actor}, become a listener of the relay, a Follow will be sent back
194 | - Follow Public, become a listener of the relay
195 | - Undo Follow {self-actor}, stop listening on the relay, an Undo Follow will be sent back
196 | - Undo Follow Public, stop listening on the relay
197 | - Delete {anything}, the Delete {anything} is relayed verbatim to listening servers.
198 | Note that this activity will likely be rejected by the listening servers unless it has been
199 | signed with a JSON-LD signature
200 | - Update {anything}, the Update {anything} is relayed verbatim to listening servers.
201 | Note that this activity will likely be rejected by the listening servers unless it has been
202 | signed with a JSON-LD signature
203 | - Add {anything}, the Add {anything} is relayed verbatim to listening servers.
204 | Note that this activity will likely be rejected by the listening servers unless it has been
205 | signed with a JSON-LD signature
206 | - Remove {anything}, the Remove {anything} is relayed verbatim to listening servers.
207 | Note that this activity will likely be rejected by the listening servers unless it has been
208 | signed with a JSON-LD signature
209 |
210 | ### Supported Discovery Protocols
211 | - Webfinger
212 | - NodeInfo
213 |
214 | ### Known issues
215 | Pleroma and Akkoma do not support validating JSON-LD signatures, meaning many activities such as Delete, Update, Add, and Remove will be rejected with a message similar to `WARN: Response from https://example.com/inbox, "Invalid HTTP Signature"`. This is normal and not an issue with the relay.
216 |
217 | ### Contributing
218 | Feel free to open issues for anything you find an issue with. Please note that any contributed code will be licensed under the AGPLv3.
219 |
220 | ### License
221 | Copyright © 2022 Riley Trautman
222 |
223 | AodeRelay is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
224 |
225 | AodeRelay is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. This file is part of AodeRelay.
226 |
227 | You should have received a copy of the GNU General Public License along with AodeRelay. If not, see [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/).
228 |
--------------------------------------------------------------------------------
/docker-compose.yml.example:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 |
3 | services:
4 | relay:
5 | #image: interstellarflights/aode-relay:edge
6 | image: ghcr.io/interstellar-relay-community/aode-relay:edge
7 | ports:
8 | - "8080:8080"
9 | - "8081:8081"
10 | restart: always
11 | volumes:
12 | - './relay:/var/lib/aode-relay'
13 | environment:
14 | - HOSTNAME=ap.example.com
15 | - DEBUG=true
16 | - RESTRICTED_MODE=true
17 | - VALIDATE_SIGNATURES=true
18 | - HTTPS=true
19 | - PRETTY_LOG=false
20 | - PUBLISH_BLOCKS=true
21 | - SOURCE_REPO=https://github.com/Interstellar-Relay-Community/aode-relay
22 | - REPOSITORY_COMMIT_BASE=/tree/
23 | - PROMETHEUS_ADDR=0.0.0.0
24 | - PROMETHEUS_PORT=8081
25 | - API_TOKEN=[REDACTED]
26 |
--------------------------------------------------------------------------------
/docker/drone/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG REPO_ARCH
2 |
3 | FROM asonix/rust-runner:latest-linux-$REPO_ARCH
4 |
5 | COPY relay /usr/local/bin/relay
6 |
7 | USER app
8 | EXPOSE 8080
9 | VOLUME /mnt
10 | ENTRYPOINT ["/sbin/tini", "--"]
11 | CMD ["/usr/local/bin/relay"]
12 |
--------------------------------------------------------------------------------
/docker/drone/manifest.tmpl:
--------------------------------------------------------------------------------
1 | image: asonix/relay:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
2 | {{#if build.tags}}
3 | tags:
4 | {{#each build.tags}}
5 | - {{this}}
6 | {{/each}}
7 | {{/if}}
8 | manifests:
9 | -
10 | image: asonix/relay:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
11 | platform:
12 | architecture: amd64
13 | os: linux
14 | -
15 | image: asonix/relay:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64v8
16 | platform:
17 | architecture: arm64
18 | os: linux
19 | variant: v8
20 | -
21 | image: asonix/relay:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm32v7
22 | platform:
23 | architecture: arm
24 | os: linux
25 | variant: v7
26 |
--------------------------------------------------------------------------------
/docker/prod/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 |
3 | services:
4 | relay:
5 | image: asonix/relay:0.3.85
6 | ports:
7 | - "8079:8079"
8 | restart: always
9 | environment:
10 | - HOSTNAME=relay.my.tld
11 | - ADDR=0.0.0.0
12 | - PORT=8080
13 | - DEBUG=false
14 | - RESTRICTED_MODE=false
15 | - VALIDATE_SIGNATURES=true
16 | - HTTPS=true
17 | - SLED_PATH=/mnt/sled/db-0.34
18 | - PRETTY_LOG=false
19 | - PUBLISH_BLOCKS=true
20 | - API_TOKEN=somepasswordishtoken
21 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-utils": {
4 | "inputs": {
5 | "systems": "systems"
6 | },
7 | "locked": {
8 | "lastModified": 1701680307,
9 | "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
10 | "owner": "numtide",
11 | "repo": "flake-utils",
12 | "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
13 | "type": "github"
14 | },
15 | "original": {
16 | "owner": "numtide",
17 | "repo": "flake-utils",
18 | "type": "github"
19 | }
20 | },
21 | "nixpkgs": {
22 | "locked": {
23 | "lastModified": 1705133751,
24 | "narHash": "sha256-rCIsyE80jgiOU78gCWN3A0wE0tR2GI5nH6MlS+HaaSQ=",
25 | "owner": "NixOS",
26 | "repo": "nixpkgs",
27 | "rev": "9b19f5e77dd906cb52dade0b7bd280339d2a1f3d",
28 | "type": "github"
29 | },
30 | "original": {
31 | "owner": "NixOS",
32 | "ref": "nixos-unstable",
33 | "repo": "nixpkgs",
34 | "type": "github"
35 | }
36 | },
37 | "root": {
38 | "inputs": {
39 | "flake-utils": "flake-utils",
40 | "nixpkgs": "nixpkgs"
41 | }
42 | },
43 | "systems": {
44 | "locked": {
45 | "lastModified": 1681028828,
46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
47 | "owner": "nix-systems",
48 | "repo": "default",
49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
50 | "type": "github"
51 | },
52 | "original": {
53 | "owner": "nix-systems",
54 | "repo": "default",
55 | "type": "github"
56 | }
57 | }
58 | },
59 | "root": "root",
60 | "version": 7
61 | }
62 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "relay";
3 |
4 | inputs = {
5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6 | flake-utils.url = "github:numtide/flake-utils";
7 | };
8 |
9 | outputs = { self, nixpkgs, flake-utils }:
10 | flake-utils.lib.eachDefaultSystem (system:
11 | let
12 | pkgs = import nixpkgs {
13 | inherit system;
14 | };
15 | in
16 | {
17 | packages = rec {
18 | relay = pkgs.callPackage ./relay.nix { };
19 |
20 | default = relay;
21 | };
22 |
23 | apps = rec {
24 | dev = flake-utils.lib.mkApp { drv = self.packages.${system}.pict-rs-proxy; };
25 | default = dev;
26 | };
27 |
28 | devShell = with pkgs; mkShell {
29 | nativeBuildInputs = [ cargo cargo-outdated cargo-zigbuild clippy gcc protobuf rust-analyzer rustc rustfmt ];
30 |
31 | RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
32 | };
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/relay.nix:
--------------------------------------------------------------------------------
1 | { lib
2 | , nixosTests
3 | , rustPlatform
4 | }:
5 |
6 | rustPlatform.buildRustPackage {
7 | pname = "relay";
8 | version = "0.3.106";
9 | src = ./.;
10 | cargoLock.lockFile = ./Cargo.lock;
11 |
12 | RUSTFLAGS = "--cfg tokio_unstable";
13 |
14 | nativeBuildInputs = [ ];
15 |
16 | passthru.tests = { inherit (nixosTests) relay; };
17 |
18 | meta = with lib; {
19 | description = "An ActivityPub relay";
20 | homepage = "https://git.asonix.dog/asonix/relay";
21 | license = with licenses; [ agpl3Plus ];
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/scss/index.scss:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #333;
3 | color: #f5f5f5;
4 | font-family: sans-serif;
5 | margin: 0;
6 | position: relative;
7 | min-height: 100vh;
8 | padding-bottom: 96px;
9 | }
10 |
11 | ul {
12 | margin: 0;
13 | padding: 0;
14 | list-style: none;
15 | }
16 |
17 | body,
18 | body * {
19 | box-sizing: border-box;
20 | }
21 |
22 | header {
23 | .header-text {
24 | max-width: 700px;
25 | margin: auto;
26 | padding: 24px 0;
27 | }
28 |
29 | h1 {
30 | margin: 0px;
31 | .smaller {
32 | font-size: 14px;
33 | font-weight: 400;
34 | }
35 | }
36 |
37 | p {
38 | margin: 0;
39 | margin-top: 8px;
40 | font-style: italic;
41 | }
42 | }
43 |
44 | article {
45 | background-color: #fff;
46 | color: #333;
47 | border: 1px solid #e5e5e5;
48 | box-shadow: 0 0 3px rgba(0, 0, 0, 0.1);
49 | border-radius: 3px;
50 | margin: 32px auto 0;
51 | max-width: 700px;
52 | padding-bottom: 32px;
53 |
54 | section {
55 | border-bottom: 1px solid #e5e5e5;
56 |
57 | > h4:first-child,
58 | > p:first-child {
59 | margin-top: 0;
60 | }
61 | > p:last-child {
62 | margin-bottom: 0;
63 | }
64 | }
65 |
66 | h3 {
67 | padding: 24px;
68 | margin: 0px;
69 | border-bottom: 1px solid #e5e5e5;
70 | }
71 |
72 | .info {
73 | padding-bottom: 36px;
74 | }
75 |
76 | li {
77 | padding-top: 36px;
78 | }
79 |
80 | .padded {
81 | padding: 0 24px;
82 | }
83 |
84 | .local-explainer,
85 | .joining {
86 | padding: 24px;
87 | }
88 |
89 | a {
90 | transition: color .2s cubic-bezier(.3,0,.5,1);
91 |
92 | &,
93 | &:focus,
94 | &:active {
95 | color: #c92a60;
96 | }
97 |
98 | &:hover {
99 | color: #9d2a60;
100 | }
101 | }
102 | }
103 |
104 | pre {
105 | border: 1px solid #e5e5e5;
106 | border-radius: 3px;
107 | background-color: #f5f5f5;
108 | padding: 8px;
109 | padding-left: 32px;
110 | padding-top: 10px;
111 | position: relative;
112 |
113 | &:before {
114 | content: ' ';
115 | display: block;
116 | position: absolute;
117 | top: 0;
118 | left: 0;
119 | bottom: 0;
120 | width: 24px;
121 | background-color: #e5e5e5;
122 | }
123 | }
124 |
125 | a {
126 | &,
127 | &:focus,
128 | &:active {
129 | color: #f9a6c2;
130 | }
131 |
132 | &:hover {
133 | color: #f2739f;
134 | }
135 | }
136 |
137 | footer {
138 | background-color: #333;
139 | color: #f5f5f5;
140 | position: absolute;
141 | padding: 16px 8px;
142 | bottom: 0;
143 | left: 0;
144 | right: 0;
145 | text-align: center;
146 |
147 | p {
148 | margin: 0;
149 | }
150 | }
151 |
152 | .instance,
153 | .info {
154 | h4 {
155 | font-size: 20px;
156 | margin: 0;
157 | }
158 |
159 | .instance-info {
160 | padding: 24px;
161 | padding-bottom: 36px;
162 | border-top: 1px solid #e5e5e5;
163 | background-color: #f5f5f5;
164 |
165 | .instance-description {
166 | margin: 0;
167 | margin-bottom: 24px;
168 | }
169 | .instance-admin {
170 | margin: 24px 0;
171 | }
172 |
173 | .description .please-stay {
174 | h3 {
175 | padding: 0;
176 | margin: 0;
177 | border-bottom: none;
178 | }
179 | ul {
180 | list-style: disc;
181 | padding-left: 24px;
182 |
183 | li {
184 | padding: 0;
185 | }
186 | }
187 | article section {
188 | border-bottom: none;
189 | }
190 | }
191 | }
192 |
193 | a {
194 | text-decoration: none;
195 | }
196 | }
197 |
198 | .admin {
199 | margin-top: 32px;
200 | display: flex;
201 | align-items: center;
202 | background-color: #fff;
203 | border: 1px solid #e5e5e5;
204 | border-radius: 3px;
205 | box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1);
206 |
207 | .display-name {
208 | font-weight: 600;
209 | font-size: 16px;
210 | margin: 0;
211 | }
212 |
213 | .username {
214 | font-size: 14px;
215 | color: #777;
216 | margin: 0;
217 | margin-top: 8px;
218 | }
219 | }
220 |
221 | .avatar {
222 | width: 80px;
223 | height: 80px;
224 |
225 | img {
226 | width: 100%;
227 | border-radius: 40px;
228 | border: 1px solid #333;
229 | background-color: #f5f5f5;
230 | box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.1);
231 | }
232 | }
233 |
234 | @media(max-width: 700px) {
235 | header .header-text {
236 | padding: 24px;
237 | }
238 |
239 | article {
240 | border-left: none;
241 | border-right: none;
242 | border-radius: 0;
243 | }
244 | }
245 |
246 | @media(max-width: 500px) {
247 | .avatar {
248 | width: 60px;
249 | height: 60px;
250 | margin: 16px 24px;
251 |
252 | img {
253 | border-radius: 30px;
254 | }
255 | }
256 | }
257 |
258 | @media(max-width: 400px) {
259 | .avatar {
260 | width: 50px;
261 | height: 50px;
262 | margin: 16px 20px;
263 |
264 | img {
265 | border-radius: 25px;
266 | }
267 | }
268 | }
269 |
270 | @media(max-width: 360px) {
271 | .admin {
272 | flex-direction: column;
273 | }
274 |
275 | .right {
276 | margin: 16px;
277 | margin-top: 0;
278 | }
279 | }
280 |
--------------------------------------------------------------------------------
/src/admin.rs:
--------------------------------------------------------------------------------
1 | use activitystreams::iri_string::types::IriString;
2 | use std::collections::{BTreeMap, BTreeSet};
3 | use time::OffsetDateTime;
4 |
5 | pub mod client;
6 | pub mod routes;
7 |
8 | #[derive(serde::Deserialize, serde::Serialize)]
9 | pub(crate) struct Domains {
10 | domains: Vec,
11 | }
12 |
13 | #[derive(serde::Deserialize, serde::Serialize)]
14 | pub(crate) struct AllowedDomains {
15 | pub(crate) allowed_domains: Vec,
16 | }
17 |
18 | #[derive(serde::Deserialize, serde::Serialize)]
19 | pub(crate) struct BlockedDomains {
20 | pub(crate) blocked_domains: Vec,
21 | }
22 |
23 | #[derive(serde::Deserialize, serde::Serialize)]
24 | pub(crate) struct ConnectedActors {
25 | pub(crate) connected_actors: Vec,
26 | }
27 |
28 | #[derive(serde::Deserialize, serde::Serialize)]
29 | pub(crate) struct LastSeen {
30 | pub(crate) last_seen: BTreeMap>,
31 | pub(crate) never: Vec,
32 | }
33 |
--------------------------------------------------------------------------------
/src/admin/client.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | admin::{AllowedDomains, BlockedDomains, ConnectedActors, Domains, LastSeen},
3 | collector::Snapshot,
4 | config::{AdminUrlKind, Config},
5 | error::{Error, ErrorKind},
6 | extractors::XApiToken,
7 | };
8 | use actix_web::http::header::Header;
9 | use reqwest_middleware::ClientWithMiddleware;
10 | use serde::de::DeserializeOwned;
11 |
12 | pub(crate) async fn allow(
13 | client: &ClientWithMiddleware,
14 | config: &Config,
15 | domains: Vec,
16 | ) -> Result<(), Error> {
17 | post_domains(client, config, domains, AdminUrlKind::Allow).await
18 | }
19 |
20 | pub(crate) async fn disallow(
21 | client: &ClientWithMiddleware,
22 | config: &Config,
23 | domains: Vec,
24 | ) -> Result<(), Error> {
25 | post_domains(client, config, domains, AdminUrlKind::Disallow).await
26 | }
27 |
28 | pub(crate) async fn block(
29 | client: &ClientWithMiddleware,
30 | config: &Config,
31 | domains: Vec,
32 | ) -> Result<(), Error> {
33 | post_domains(client, config, domains, AdminUrlKind::Block).await
34 | }
35 |
36 | pub(crate) async fn unblock(
37 | client: &ClientWithMiddleware,
38 | config: &Config,
39 | domains: Vec,
40 | ) -> Result<(), Error> {
41 | post_domains(client, config, domains, AdminUrlKind::Unblock).await
42 | }
43 |
44 | pub(crate) async fn allowed(
45 | client: &ClientWithMiddleware,
46 | config: &Config,
47 | ) -> Result {
48 | get_results(client, config, AdminUrlKind::Allowed).await
49 | }
50 |
51 | pub(crate) async fn blocked(
52 | client: &ClientWithMiddleware,
53 | config: &Config,
54 | ) -> Result {
55 | get_results(client, config, AdminUrlKind::Blocked).await
56 | }
57 |
58 | pub(crate) async fn connected(
59 | client: &ClientWithMiddleware,
60 | config: &Config,
61 | ) -> Result {
62 | get_results(client, config, AdminUrlKind::Connected).await
63 | }
64 |
65 | pub(crate) async fn stats(
66 | client: &ClientWithMiddleware,
67 | config: &Config,
68 | ) -> Result {
69 | get_results(client, config, AdminUrlKind::Stats).await
70 | }
71 |
72 | pub(crate) async fn last_seen(
73 | client: &ClientWithMiddleware,
74 | config: &Config,
75 | ) -> Result {
76 | get_results(client, config, AdminUrlKind::LastSeen).await
77 | }
78 |
79 | async fn get_results(
80 | client: &ClientWithMiddleware,
81 | config: &Config,
82 | url_kind: AdminUrlKind,
83 | ) -> Result {
84 | let x_api_token = config.x_api_token().ok_or(ErrorKind::MissingApiToken)?;
85 |
86 | let iri = config.generate_admin_url(url_kind);
87 |
88 | let res = client
89 | .get(iri.as_str())
90 | .header(XApiToken::name(), x_api_token.to_string())
91 | .send()
92 | .await
93 | .map_err(|e| ErrorKind::SendRequest(iri.to_string(), e.to_string()))?;
94 |
95 | if !res.status().is_success() {
96 | return Err(ErrorKind::Status(iri.to_string(), res.status()).into());
97 | }
98 |
99 | let t = res
100 | .json()
101 | .await
102 | .map_err(|e| ErrorKind::ReceiveResponse(iri.to_string(), e.to_string()))?;
103 |
104 | Ok(t)
105 | }
106 |
107 | async fn post_domains(
108 | client: &ClientWithMiddleware,
109 | config: &Config,
110 | domains: Vec,
111 | url_kind: AdminUrlKind,
112 | ) -> Result<(), Error> {
113 | let x_api_token = config.x_api_token().ok_or(ErrorKind::MissingApiToken)?;
114 |
115 | let iri = config.generate_admin_url(url_kind);
116 |
117 | let res = client
118 | .post(iri.as_str())
119 | .header(XApiToken::name(), x_api_token.to_string())
120 | .json(&Domains { domains })
121 | .send()
122 | .await
123 | .map_err(|e| ErrorKind::SendRequest(iri.to_string(), e.to_string()))?;
124 |
125 | if !res.status().is_success() {
126 | tracing::warn!("Failed to allow domains");
127 | }
128 |
129 | Ok(())
130 | }
131 |
--------------------------------------------------------------------------------
/src/admin/routes.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | admin::{AllowedDomains, BlockedDomains, ConnectedActors, Domains, LastSeen},
3 | collector::{MemoryCollector, Snapshot},
4 | error::Error,
5 | extractors::Admin,
6 | };
7 | use actix_web::{
8 | web::{self, Data, Json},
9 | HttpResponse,
10 | };
11 | use std::collections::{BTreeMap, BTreeSet};
12 | use time::OffsetDateTime;
13 |
14 | pub(crate) async fn allow(
15 | admin: Admin,
16 | Json(Domains { domains }): Json,
17 | ) -> Result {
18 | admin.db_ref().add_allows(domains).await?;
19 |
20 | Ok(HttpResponse::NoContent().finish())
21 | }
22 |
23 | pub(crate) async fn disallow(
24 | admin: Admin,
25 | Json(Domains { domains }): Json,
26 | ) -> Result {
27 | admin.db_ref().remove_allows(domains).await?;
28 |
29 | Ok(HttpResponse::NoContent().finish())
30 | }
31 |
32 | pub(crate) async fn block(
33 | admin: Admin,
34 | Json(Domains { domains }): Json,
35 | ) -> Result {
36 | admin.db_ref().add_blocks(domains).await?;
37 |
38 | Ok(HttpResponse::NoContent().finish())
39 | }
40 |
41 | pub(crate) async fn unblock(
42 | admin: Admin,
43 | Json(Domains { domains }): Json,
44 | ) -> Result {
45 | admin.db_ref().remove_blocks(domains).await?;
46 |
47 | Ok(HttpResponse::NoContent().finish())
48 | }
49 |
50 | pub(crate) async fn allowed(admin: Admin) -> Result, Error> {
51 | let allowed_domains = admin.db_ref().allows().await?;
52 |
53 | Ok(Json(AllowedDomains { allowed_domains }))
54 | }
55 |
56 | pub(crate) async fn blocked(admin: Admin) -> Result, Error> {
57 | let blocked_domains = admin.db_ref().blocks().await?;
58 |
59 | Ok(Json(BlockedDomains { blocked_domains }))
60 | }
61 |
62 | pub(crate) async fn connected(admin: Admin) -> Result, Error> {
63 | let connected_actors = admin.db_ref().connected_ids().await?;
64 |
65 | Ok(Json(ConnectedActors { connected_actors }))
66 | }
67 |
68 | pub(crate) async fn stats(
69 | _admin: Admin,
70 | collector: Data,
71 | ) -> Result, Error> {
72 | Ok(Json(collector.snapshot()))
73 | }
74 |
75 | pub(crate) async fn last_seen(admin: Admin) -> Result, Error> {
76 | let nodes = admin.db_ref().last_seen().await?;
77 |
78 | let mut last_seen: BTreeMap> = BTreeMap::new();
79 | let mut never = Vec::new();
80 |
81 | for (domain, datetime) in nodes {
82 | if let Some(datetime) = datetime {
83 | last_seen.entry(datetime).or_default().insert(domain);
84 | } else {
85 | never.push(domain);
86 | }
87 | }
88 |
89 | Ok(Json(LastSeen { last_seen, never }))
90 | }
91 |
92 | pub(crate) async fn get_authority_cfg(
93 | _admin: Admin,
94 | state: Data,
95 | domain: web::Path,
96 | ) -> Result, Error> {
97 | if let Some(cfg) = state.get_authority_cfg(&domain).await {
98 | Ok(Json(cfg))
99 | } else {
100 | Err(crate::error::ErrorKind::NotFound.into())
101 | }
102 | }
103 |
104 | pub(crate) async fn get_all_authority_cfg(
105 | _admin: Admin,
106 | state: Data,
107 | ) -> Result>, Error> {
108 | let cfg = state.get_all_authority_cfg().await;
109 |
110 | Ok(Json(cfg))
111 | }
112 |
113 | pub(crate) async fn set_authority_cfg(
114 | _admin: Admin,
115 | state: Data,
116 | domain: web::Path,
117 | Json(cfg): Json,
118 | ) -> Result {
119 | state.set_authority_cfg(&domain, cfg).await;
120 |
121 | Ok(HttpResponse::NoContent().finish())
122 | }
123 |
124 | pub(crate) async fn clear_authority_cfg(
125 | _admin: Admin,
126 | state: Data,
127 | domain: web::Path,
128 | ) -> Result {
129 | state.clear_authority_cfg(&domain).await;
130 |
131 | Ok(HttpResponse::NoContent().finish())
132 | }
133 |
--------------------------------------------------------------------------------
/src/apub.rs:
--------------------------------------------------------------------------------
1 | use activitystreams::{
2 | activity::ActorAndObject,
3 | actor::{Actor, ApActor},
4 | iri_string::types::IriString,
5 | unparsed::UnparsedMutExt,
6 | };
7 | use activitystreams_ext::{Ext1, UnparsedExtension};
8 |
9 | #[derive(Clone, serde::Deserialize, serde::Serialize)]
10 | #[serde(rename_all = "camelCase")]
11 | pub struct PublicKeyInner {
12 | pub id: IriString,
13 | pub owner: IriString,
14 | pub public_key_pem: String,
15 | }
16 |
17 | impl std::fmt::Debug for PublicKeyInner {
18 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 | f.debug_struct("PublicKeyInner")
20 | .field("id", &self.id.to_string())
21 | .field("owner", &self.owner.to_string())
22 | .field("public_key_pem", &self.public_key_pem)
23 | .finish()
24 | }
25 | }
26 |
27 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
28 | #[serde(rename_all = "camelCase")]
29 | pub struct PublicKey {
30 | pub public_key: PublicKeyInner,
31 | }
32 |
33 | #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize)]
34 | #[serde(rename_all = "PascalCase")]
35 | pub enum ValidTypes {
36 | Accept,
37 | Add,
38 | Announce,
39 | Create,
40 | Delete,
41 | Follow,
42 | Reject,
43 | Remove,
44 | Undo,
45 | Update,
46 | Move,
47 | }
48 |
49 | #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize)]
50 | #[serde(rename_all = "PascalCase")]
51 | pub enum UndoTypes {
52 | Follow,
53 | Announce,
54 | Create,
55 | }
56 |
57 | pub type AcceptedUndoObjects = ActorAndObject;
58 | pub type AcceptedActivities = ActorAndObject;
59 | pub type AcceptedActors = Ext1>, PublicKey>;
60 |
61 | impl UnparsedExtension for PublicKey
62 | where
63 | U: UnparsedMutExt,
64 | {
65 | type Error = serde_json::Error;
66 |
67 | fn try_from_unparsed(unparsed_mut: &mut U) -> Result {
68 | Ok(PublicKey {
69 | public_key: unparsed_mut.remove("publicKey")?,
70 | })
71 | }
72 |
73 | fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
74 | unparsed_mut.insert("publicKey", self.public_key)?;
75 | Ok(())
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/args.rs:
--------------------------------------------------------------------------------
1 | use clap::Parser;
2 |
3 | #[derive(Debug, Parser)]
4 | #[structopt(name = "relay", about = "An activitypub relay")]
5 | pub(crate) struct Args {
6 | #[arg(short, help = "A list of domains that should be blocked")]
7 | blocks: Vec,
8 |
9 | #[arg(short, help = "A list of domains that should be allowed")]
10 | allowed: Vec,
11 |
12 | #[arg(short, long, help = "Undo allowing or blocking domains")]
13 | undo: bool,
14 |
15 | #[arg(short, long, help = "List allowed and blocked domains")]
16 | list: bool,
17 |
18 | #[arg(short, long, help = "Get statistics from the server")]
19 | stats: bool,
20 |
21 | #[arg(
22 | short,
23 | long,
24 | help = "List domains by when they were last succesfully contacted"
25 | )]
26 | contacted: bool,
27 | }
28 |
29 | impl Args {
30 | pub(crate) fn any(&self) -> bool {
31 | !self.blocks.is_empty()
32 | || !self.allowed.is_empty()
33 | || self.list
34 | || self.stats
35 | || self.contacted
36 | }
37 |
38 | pub(crate) fn new() -> Self {
39 | Self::parse()
40 | }
41 |
42 | pub(crate) fn blocks(&self) -> &[String] {
43 | &self.blocks
44 | }
45 |
46 | pub(crate) fn allowed(&self) -> &[String] {
47 | &self.allowed
48 | }
49 |
50 | pub(crate) fn undo(&self) -> bool {
51 | self.undo
52 | }
53 |
54 | pub(crate) fn list(&self) -> bool {
55 | self.list
56 | }
57 |
58 | pub(crate) fn stats(&self) -> bool {
59 | self.stats
60 | }
61 |
62 | pub(crate) fn contacted(&self) -> bool {
63 | self.contacted
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/build.rs:
--------------------------------------------------------------------------------
1 | use ructe::Ructe;
2 | use std::{fs::File, io::Read, path::Path, process::Command};
3 |
4 | fn git_info() {
5 | if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() {
6 | if output.status.success() {
7 | let git_hash = String::from_utf8_lossy(&output.stdout);
8 | println!("cargo:rustc-env=GIT_HASH={git_hash}");
9 | println!("cargo:rustc-env=GIT_SHORT_HASH={}", &git_hash[..8])
10 | }
11 | }
12 |
13 | if let Ok(output) = Command::new("git")
14 | .args(["rev-parse", "--abbrev-ref", "HEAD"])
15 | .output()
16 | {
17 | if output.status.success() {
18 | let git_branch = String::from_utf8_lossy(&output.stdout);
19 | println!("cargo:rustc-env=GIT_BRANCH={git_branch}");
20 | }
21 | }
22 | }
23 |
24 | fn version_info() -> Result<(), anyhow::Error> {
25 | let cargo_toml = Path::new(&std::env::var("CARGO_MANIFEST_DIR")?).join("Cargo.toml");
26 |
27 | let mut file = File::open(cargo_toml)?;
28 |
29 | let mut cargo_data = String::new();
30 | file.read_to_string(&mut cargo_data)?;
31 |
32 | let data: toml::Value = toml::from_str(&cargo_data)?;
33 |
34 | if let Some(version) = data["package"]["version"].as_str() {
35 | println!("cargo:rustc-env=PKG_VERSION={version}");
36 | }
37 |
38 | if let Some(name) = data["package"]["name"].as_str() {
39 | println!("cargo:rustc-env=PKG_NAME={name}");
40 | }
41 |
42 | Ok(())
43 | }
44 |
45 | fn main() -> Result<(), anyhow::Error> {
46 | dotenv::dotenv().ok();
47 |
48 | git_info();
49 | version_info()?;
50 |
51 | let mut ructe = Ructe::from_env()?;
52 | let mut statics = ructe.statics()?;
53 | statics.add_sass_file("scss/index.scss")?;
54 | ructe.compile_templates("templates")?;
55 |
56 | Ok(())
57 | }
58 |
--------------------------------------------------------------------------------
/src/collector.rs:
--------------------------------------------------------------------------------
1 | use metrics::{Key, Metadata, Recorder, SetRecorderError};
2 | use metrics_util::{
3 | registry::{AtomicStorage, GenerationalStorage, Recency, Registry},
4 | MetricKindMask, Summary,
5 | };
6 | use quanta::Clock;
7 | use std::{
8 | collections::{BTreeMap, HashMap},
9 | sync::{atomic::Ordering, Arc, RwLock},
10 | time::Duration,
11 | };
12 |
13 | const SECONDS: u64 = 1;
14 | const MINUTES: u64 = 60 * SECONDS;
15 | const HOURS: u64 = 60 * MINUTES;
16 | const DAYS: u64 = 24 * HOURS;
17 |
18 | type DistributionMap = BTreeMap, Summary>;
19 |
20 | #[derive(Clone)]
21 | pub struct MemoryCollector {
22 | inner: Arc,
23 | }
24 |
25 | struct Inner {
26 | descriptions: RwLock>,
27 | distributions: RwLock>,
28 | recency: Recency,
29 | registry: Registry>,
30 | }
31 |
32 | #[derive(Debug, serde::Deserialize, serde::Serialize)]
33 | struct Counter {
34 | labels: BTreeMap,
35 | value: u64,
36 | }
37 |
38 | impl std::fmt::Display for Counter {
39 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 | let labels = self
41 | .labels
42 | .iter()
43 | .map(|(k, v)| format!("{k}: {v}"))
44 | .collect::>()
45 | .join(", ");
46 |
47 | write!(f, "{labels} - {}", self.value)
48 | }
49 | }
50 |
51 | #[derive(Debug, serde::Deserialize, serde::Serialize)]
52 | struct Gauge {
53 | labels: BTreeMap,
54 | value: f64,
55 | }
56 |
57 | impl std::fmt::Display for Gauge {
58 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 | let labels = self
60 | .labels
61 | .iter()
62 | .map(|(k, v)| format!("{k}: {v}"))
63 | .collect::>()
64 | .join(", ");
65 |
66 | write!(f, "{labels} - {}", self.value)
67 | }
68 | }
69 |
70 | #[derive(Debug, serde::Deserialize, serde::Serialize)]
71 | struct Histogram {
72 | labels: BTreeMap,
73 | value: Vec<(f64, Option)>,
74 | }
75 |
76 | impl std::fmt::Display for Histogram {
77 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 | let labels = self
79 | .labels
80 | .iter()
81 | .map(|(k, v)| format!("{k}: {v}"))
82 | .collect::>()
83 | .join(", ");
84 |
85 | let value = self
86 | .value
87 | .iter()
88 | .map(|(k, v)| {
89 | if let Some(v) = v {
90 | format!("{k}: {v:.6}")
91 | } else {
92 | format!("{k}: None,")
93 | }
94 | })
95 | .collect::>()
96 | .join(", ");
97 |
98 | write!(f, "{labels} - {value}")
99 | }
100 | }
101 |
102 | #[derive(Debug, serde::Deserialize, serde::Serialize)]
103 | pub(crate) struct Snapshot {
104 | counters: HashMap>,
105 | gauges: HashMap>,
106 | histograms: HashMap>,
107 | }
108 |
109 | const PAIRS: [((&str, &str), &str); 2] = [
110 | (
111 | (
112 | "background-jobs.worker.started",
113 | "background-jobs.worker.finished",
114 | ),
115 | "background-jobs.worker.running",
116 | ),
117 | (
118 | (
119 | "background-jobs.job.started",
120 | "background-jobs.job.finished",
121 | ),
122 | "background-jobs.job.running",
123 | ),
124 | ];
125 |
126 | #[derive(Default)]
127 | struct MergeCounter {
128 | start: Option,
129 | finish: Option,
130 | }
131 |
132 | impl MergeCounter {
133 | fn merge(self) -> Option {
134 | match (self.start, self.finish) {
135 | (Some(start), Some(end)) => Some(Counter {
136 | labels: start.labels,
137 | value: start.value.saturating_sub(end.value),
138 | }),
139 | (Some(only), None) => Some(only),
140 | (None, Some(only)) => Some(Counter {
141 | labels: only.labels,
142 | value: 0,
143 | }),
144 | (None, None) => None,
145 | }
146 | }
147 | }
148 |
149 | impl Snapshot {
150 | pub(crate) fn present(self) {
151 | if !self.counters.is_empty() {
152 | println!("Counters");
153 | let mut merging = HashMap::new();
154 | for (key, counters) in self.counters {
155 | if let Some(((start, _), name)) = PAIRS
156 | .iter()
157 | .find(|((start, finish), _)| *start == key || *finish == key)
158 | {
159 | let entry = merging.entry(name).or_insert_with(HashMap::new);
160 |
161 | for counter in counters {
162 | let merge_counter = entry
163 | .entry(counter.labels.clone())
164 | .or_insert_with(MergeCounter::default);
165 | if key == *start {
166 | merge_counter.start = Some(counter);
167 | } else {
168 | merge_counter.finish = Some(counter);
169 | }
170 | }
171 |
172 | continue;
173 | }
174 |
175 | println!("\t{key}");
176 | for counter in counters {
177 | println!("\t\t{counter}");
178 | }
179 | }
180 |
181 | for (key, counters) in merging {
182 | println!("\t{key}");
183 |
184 | for (_, counter) in counters {
185 | if let Some(counter) = counter.merge() {
186 | println!("\t\t{counter}");
187 | }
188 | }
189 | }
190 | }
191 |
192 | if !self.gauges.is_empty() {
193 | println!("Gauges");
194 | for (key, gauges) in self.gauges {
195 | println!("\t{key}");
196 |
197 | for gauge in gauges {
198 | println!("\t\t{gauge}");
199 | }
200 | }
201 | }
202 |
203 | if !self.histograms.is_empty() {
204 | println!("Histograms");
205 | for (key, histograms) in self.histograms {
206 | println!("\t{key}");
207 |
208 | for histogram in histograms {
209 | println!("\t\t{histogram}");
210 | }
211 | }
212 | }
213 | }
214 | }
215 |
216 | fn key_to_parts(key: &Key) -> (String, Vec<(String, String)>) {
217 | let labels = key
218 | .labels()
219 | .map(|label| (label.key().to_string(), label.value().to_string()))
220 | .collect();
221 | let name = key.name().to_string();
222 | (name, labels)
223 | }
224 |
225 | impl Inner {
226 | fn snapshot_counters(&self) -> HashMap> {
227 | let mut counters = HashMap::new();
228 |
229 | for (key, counter) in self.registry.get_counter_handles() {
230 | let gen = counter.get_generation();
231 | if !self.recency.should_store_counter(&key, gen, &self.registry) {
232 | continue;
233 | }
234 |
235 | let (name, labels) = key_to_parts(&key);
236 | let value = counter.get_inner().load(Ordering::Acquire);
237 | counters.entry(name).or_insert_with(Vec::new).push(Counter {
238 | labels: labels.into_iter().collect(),
239 | value,
240 | });
241 | }
242 |
243 | counters
244 | }
245 |
246 | fn snapshot_gauges(&self) -> HashMap> {
247 | let mut gauges = HashMap::new();
248 |
249 | for (key, gauge) in self.registry.get_gauge_handles() {
250 | let gen = gauge.get_generation();
251 | if !self.recency.should_store_gauge(&key, gen, &self.registry) {
252 | continue;
253 | }
254 |
255 | let (name, labels) = key_to_parts(&key);
256 | let value = f64::from_bits(gauge.get_inner().load(Ordering::Acquire));
257 | gauges.entry(name).or_insert_with(Vec::new).push(Gauge {
258 | labels: labels.into_iter().collect(),
259 | value,
260 | })
261 | }
262 |
263 | gauges
264 | }
265 |
266 | fn snapshot_histograms(&self) -> HashMap> {
267 | for (key, histogram) in self.registry.get_histogram_handles() {
268 | let gen = histogram.get_generation();
269 | let (name, labels) = key_to_parts(&key);
270 |
271 | if !self
272 | .recency
273 | .should_store_histogram(&key, gen, &self.registry)
274 | {
275 | let mut d = self.distributions.write().unwrap();
276 | let delete_by_name = if let Some(by_name) = d.get_mut(&name) {
277 | by_name.remove(&labels);
278 | by_name.is_empty()
279 | } else {
280 | false
281 | };
282 | drop(d);
283 |
284 | if delete_by_name {
285 | self.descriptions.write().unwrap().remove(&name);
286 | }
287 |
288 | continue;
289 | }
290 |
291 | let mut d = self.distributions.write().unwrap();
292 | let outer_entry = d.entry(name.clone()).or_default();
293 |
294 | let entry = outer_entry
295 | .entry(labels)
296 | .or_insert_with(Summary::with_defaults);
297 |
298 | histogram.get_inner().clear_with(|samples| {
299 | for sample in samples {
300 | entry.add(*sample);
301 | }
302 | })
303 | }
304 |
305 | let d = self.distributions.read().unwrap().clone();
306 | d.into_iter()
307 | .map(|(key, value)| {
308 | (
309 | key,
310 | value
311 | .into_iter()
312 | .map(|(labels, summary)| Histogram {
313 | labels: labels.into_iter().collect(),
314 | value: [0.001, 0.01, 0.05, 0.1, 0.5, 0.9, 0.99, 1.0]
315 | .into_iter()
316 | .map(|q| (q, summary.quantile(q)))
317 | .collect(),
318 | })
319 | .collect(),
320 | )
321 | })
322 | .collect()
323 | }
324 |
325 | fn snapshot(&self) -> Snapshot {
326 | Snapshot {
327 | counters: self.snapshot_counters(),
328 | gauges: self.snapshot_gauges(),
329 | histograms: self.snapshot_histograms(),
330 | }
331 | }
332 | }
333 |
334 | impl MemoryCollector {
335 | pub(crate) fn new() -> Self {
336 | MemoryCollector {
337 | inner: Arc::new(Inner {
338 | descriptions: Default::default(),
339 | distributions: Default::default(),
340 | recency: Recency::new(
341 | Clock::new(),
342 | MetricKindMask::ALL,
343 | Some(Duration::from_secs(5 * DAYS)),
344 | ),
345 | registry: Registry::new(GenerationalStorage::atomic()),
346 | }),
347 | }
348 | }
349 |
350 | pub(crate) fn snapshot(&self) -> Snapshot {
351 | self.inner.snapshot()
352 | }
353 |
354 | fn add_description_if_missing(
355 | &self,
356 | key: &metrics::KeyName,
357 | description: metrics::SharedString,
358 | ) {
359 | let mut d = self.inner.descriptions.write().unwrap();
360 | d.entry(key.as_str().to_owned()).or_insert(description);
361 | }
362 |
363 | pub(crate) fn install(&self) -> Result<(), SetRecorderError> {
364 | metrics::set_global_recorder(self.clone())
365 | }
366 | }
367 |
368 | impl Recorder for MemoryCollector {
369 | fn describe_counter(
370 | &self,
371 | key: metrics::KeyName,
372 | _: Option,
373 | description: metrics::SharedString,
374 | ) {
375 | self.add_description_if_missing(&key, description)
376 | }
377 |
378 | fn describe_gauge(
379 | &self,
380 | key: metrics::KeyName,
381 | _: Option,
382 | description: metrics::SharedString,
383 | ) {
384 | self.add_description_if_missing(&key, description)
385 | }
386 |
387 | fn describe_histogram(
388 | &self,
389 | key: metrics::KeyName,
390 | _: Option,
391 | description: metrics::SharedString,
392 | ) {
393 | self.add_description_if_missing(&key, description)
394 | }
395 |
396 | fn register_counter(&self, key: &Key, _: &Metadata<'_>) -> metrics::Counter {
397 | self.inner
398 | .registry
399 | .get_or_create_counter(key, |c| c.clone().into())
400 | }
401 |
402 | fn register_gauge(&self, key: &Key, _: &Metadata<'_>) -> metrics::Gauge {
403 | self.inner
404 | .registry
405 | .get_or_create_gauge(key, |c| c.clone().into())
406 | }
407 |
408 | fn register_histogram(&self, key: &Key, _: &Metadata<'_>) -> metrics::Histogram {
409 | self.inner
410 | .registry
411 | .get_or_create_histogram(key, |c| c.clone().into())
412 | }
413 | }
414 |
--------------------------------------------------------------------------------
/src/data.rs:
--------------------------------------------------------------------------------
1 | mod actor;
2 | mod last_online;
3 | mod media;
4 | mod node;
5 | mod state;
6 |
7 | pub(crate) use actor::ActorCache;
8 | pub(crate) use last_online::LastOnline;
9 | pub(crate) use media::MediaCache;
10 | pub(crate) use node::{Node, NodeCache, NodeConfig};
11 | pub(crate) use state::State;
12 |
--------------------------------------------------------------------------------
/src/data/actor.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | apub::AcceptedActors,
3 | db::{Actor, Db},
4 | error::{Error, ErrorKind},
5 | requests::{BreakerStrategy, Requests},
6 | };
7 | use activitystreams::{iri_string::types::IriString, prelude::*};
8 | use std::time::{Duration, SystemTime};
9 |
10 | const REFETCH_DURATION: Duration = Duration::from_secs(60 * 30);
11 |
12 | #[derive(Debug)]
13 | pub enum MaybeCached {
14 | Cached(T),
15 | Fetched(T),
16 | }
17 |
18 | impl MaybeCached {
19 | pub(crate) fn is_cached(&self) -> bool {
20 | matches!(self, MaybeCached::Cached(_))
21 | }
22 |
23 | pub(crate) fn into_inner(self) -> T {
24 | match self {
25 | MaybeCached::Cached(t) | MaybeCached::Fetched(t) => t,
26 | }
27 | }
28 | }
29 |
30 | #[derive(Clone, Debug)]
31 | pub struct ActorCache {
32 | db: Db,
33 | }
34 |
35 | impl ActorCache {
36 | pub(crate) fn new(db: Db) -> Self {
37 | ActorCache { db }
38 | }
39 |
40 | #[tracing::instrument(level = "debug" name = "Get Actor", skip_all, fields(id = id.to_string().as_str()))]
41 | pub(crate) async fn get(
42 | &self,
43 | id: &IriString,
44 | requests: &Requests,
45 | ) -> Result, Error> {
46 | if let Some(actor) = self.db.actor(id.clone()).await? {
47 | if actor.saved_at + REFETCH_DURATION > SystemTime::now() {
48 | return Ok(MaybeCached::Cached(actor));
49 | }
50 | }
51 |
52 | self.get_no_cache(id, requests)
53 | .await
54 | .map(MaybeCached::Fetched)
55 | }
56 |
57 | #[tracing::instrument(level = "debug", name = "Add Connection", skip(self))]
58 | pub(crate) async fn add_connection(&self, actor: Actor) -> Result<(), Error> {
59 | self.db.add_connection(actor.id.clone()).await?;
60 | self.db.save_actor(actor).await
61 | }
62 |
63 | #[tracing::instrument(level = "debug", name = "Remove Connection", skip(self))]
64 | pub(crate) async fn remove_connection(&self, actor: &Actor) -> Result<(), Error> {
65 | self.db.remove_connection(actor.id.clone()).await
66 | }
67 |
68 | #[tracing::instrument(level = "debug", name = "Fetch remote actor", skip_all, fields(id = id.to_string().as_str()))]
69 | pub(crate) async fn get_no_cache(
70 | &self,
71 | id: &IriString,
72 | requests: &Requests,
73 | ) -> Result {
74 | let accepted_actor = requests
75 | .fetch::(id, BreakerStrategy::Require2XX)
76 | .await?;
77 |
78 | let input_authority = id.authority_components().ok_or(ErrorKind::MissingDomain)?;
79 | let accepted_actor_id = accepted_actor
80 | .id(input_authority.host(), input_authority.port())?
81 | .ok_or(ErrorKind::MissingId)?;
82 |
83 | let inbox = get_inbox(&accepted_actor)?.clone();
84 |
85 | let actor = Actor {
86 | id: accepted_actor_id.clone(),
87 | public_key: accepted_actor.ext_one.public_key.public_key_pem,
88 | public_key_id: accepted_actor.ext_one.public_key.id,
89 | inbox,
90 | saved_at: SystemTime::now(),
91 | };
92 |
93 | self.db.save_actor(actor.clone()).await?;
94 |
95 | Ok(actor)
96 | }
97 | }
98 |
99 | fn get_inbox(actor: &AcceptedActors) -> Result<&IriString, Error> {
100 | Ok(actor
101 | .endpoints()?
102 | .and_then(|e| e.shared_inbox.as_ref())
103 | .unwrap_or(actor.inbox()?))
104 | }
105 |
--------------------------------------------------------------------------------
/src/data/last_online.rs:
--------------------------------------------------------------------------------
1 | use activitystreams::iri_string::types::IriStr;
2 | use std::{collections::HashMap, sync::Mutex};
3 | use time::OffsetDateTime;
4 |
5 | pub(crate) struct LastOnline {
6 | domains: Mutex>,
7 | }
8 |
9 | impl LastOnline {
10 | pub(crate) fn mark_seen(&self, iri: &IriStr) {
11 | if let Some(authority) = iri.authority_str() {
12 | self.domains
13 | .lock()
14 | .unwrap()
15 | .insert(authority.to_string(), OffsetDateTime::now_utc());
16 | }
17 | }
18 |
19 | pub(crate) fn take(&self) -> HashMap {
20 | std::mem::take(&mut *self.domains.lock().unwrap())
21 | }
22 |
23 | pub(crate) fn empty() -> Self {
24 | Self {
25 | domains: Mutex::new(HashMap::default()),
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/data/media.rs:
--------------------------------------------------------------------------------
1 | use crate::{db::Db, error::Error};
2 | use activitystreams::iri_string::types::IriString;
3 | use uuid::Uuid;
4 |
5 | #[derive(Clone, Debug)]
6 | pub struct MediaCache {
7 | db: Db,
8 | }
9 |
10 | impl MediaCache {
11 | pub(crate) fn new(db: Db) -> Self {
12 | MediaCache { db }
13 | }
14 |
15 | #[tracing::instrument(level = "debug", name = "Get media uuid", skip_all, fields(url = url.to_string().as_str()))]
16 | pub(crate) async fn get_uuid(&self, url: IriString) -> Result