mirror of
https://github.com/versia-pub/activitypub.git
synced 2025-12-06 06:38:20 +01:00
Compare commits
No commits in common. "main" and "0.1.0" have entirely different histories.
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/universal:2",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/rust:latest": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
DATABASE_URL="sqlite:///home/aprl/Documents/versia-ap-layer/db.sqlite?mode=rwc"
|
||||
LYSAND_DOMAIN="versia.social"
|
||||
API_DOMAIN="ap.versia.social"
|
||||
RUST_LOG="debug"
|
||||
72
.github/workflows/docker-publish.yml
vendored
72
.github/workflows/docker-publish.yml
vendored
|
|
@ -1,72 +0,0 @@
|
|||
name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
# Publish semver tags as releases.
|
||||
tags: [ 'v*.*.*' ]
|
||||
|
||||
env:
|
||||
# Use docker.io for Docker Hub if empty
|
||||
REGISTRY: ghcr.io
|
||||
# github.repository as <account>/<repo>
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
# This is used to complete the identity challenge
|
||||
# with sigstore/fulcio when running outside of PRs.
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3 # v3.0.0
|
||||
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3 # v3.0.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5 # v5.0.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
extra-conf: accept-flake-config = true
|
||||
- uses: DeterminateSystems/flakehub-cache-action@main
|
||||
- uses: DeterminateSystems/flake-checker-action@main
|
||||
- name: Build docker package
|
||||
run: nix build .#ociImage
|
||||
- name: Load Docker image
|
||||
run: docker load < result
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@0.28.0
|
||||
with:
|
||||
image-ref: 'ghcr.io/${{ env.IMAGE_NAME }}:main'
|
||||
format: 'table'
|
||||
exit-code: '1'
|
||||
ignore-unfixed: true
|
||||
vuln-type: 'os,library'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
- name: Push image to registry
|
||||
if: github.event_name != 'pull_request'
|
||||
run: docker push ghcr.io/$IMAGE_NAME -a
|
||||
15
.github/workflows/nix-flake.yml
vendored
15
.github/workflows/nix-flake.yml
vendored
|
|
@ -10,21 +10,14 @@ on:
|
|||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
id-token: "write"
|
||||
contents: "read"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
extra-conf: accept-flake-config = true
|
||||
- uses: DeterminateSystems/flakehub-cache-action@main
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
- uses: DeterminateSystems/flake-checker-action@main
|
||||
- name: Build default package
|
||||
- name: Run `nix build`
|
||||
run: nix build .
|
||||
- name: Check flakes
|
||||
run: nix flake check
|
||||
- name: Build migrations
|
||||
run: nix build .#ls-ap-migration
|
||||
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -4,6 +4,3 @@
|
|||
/result
|
||||
/result-lib
|
||||
.direnv
|
||||
migration/target
|
||||
db.sqlite
|
||||
.env
|
||||
2328
Cargo.lock
generated
2328
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
46
Cargo.toml
46
Cargo.toml
|
|
@ -1,63 +1,31 @@
|
|||
[package]
|
||||
name = "versia-ap-layer"
|
||||
name = "lysand-ap-layer"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
authors = ["April John <aprl@acab.dev>"]
|
||||
license = "AGPL-3.0-or-later"
|
||||
repository = "https://github.com/versia-pub/versia-ap-layer"
|
||||
description = "A compatibility layer between versias official server and activitypub"
|
||||
repository = "https://github.com/lysand-org/lysand-ap-layer"
|
||||
description = "A compatibility layer between lysands official server and activitypub"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.20.0", features = ["rt", "macros"] }
|
||||
serde = { version = "1.0.130", features = ["derive", "rc"] }
|
||||
serde = { version = "1.0.130", features = ["derive"] }
|
||||
actix-web = "4"
|
||||
env_logger = "0.11.0"
|
||||
clap = { version = "4.3.14", features = ["derive"] }
|
||||
activitypub_federation = "0.5.8"
|
||||
activitypub_federation = "0.5.2"
|
||||
anyhow = "1.0.81"
|
||||
url = "2.5.0"
|
||||
rand = "0.8.5"
|
||||
tracing = "0.1.40"
|
||||
async-trait = "0.1.79"
|
||||
enum_delegate = "0.2.0"
|
||||
chrono = "0.4.37"
|
||||
activitystreams-kinds = "0.3.0"
|
||||
thiserror = "1.0.58"
|
||||
num_cpus = "1.16.0"
|
||||
actix-web-prom = { version = "0.8.0", features = ["process"] }
|
||||
serde_json = "1.0.115"
|
||||
chrono = "0.4.38"
|
||||
lazy_static = "1.4.0"
|
||||
async_once = "0.2.6"
|
||||
reqwest = { version = "0.12.4", features = ["blocking", "json", "multipart"] }
|
||||
time = { version = "0.3.36", features = ["serde"] }
|
||||
serde_derive = "1.0.201"
|
||||
dotenv = "0.15.0"
|
||||
async-recursion = "1.1.1"
|
||||
base64-url = "3.0.0"
|
||||
webfinger = "0.5.1"
|
||||
regex = "1.10.6"
|
||||
once_cell = "1.19.0"
|
||||
actix-files = "0.6.6"
|
||||
|
||||
[dependencies.sea-orm]
|
||||
version = "0.12.0"
|
||||
features = [
|
||||
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
|
||||
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
|
||||
# e.g.
|
||||
"runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
|
||||
"sqlx-postgres", # `DATABASE_DRIVER` feature
|
||||
"sqlx-sqlite","sqlx-mysql","with-chrono"
|
||||
]
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "1.8.0"
|
||||
features = [
|
||||
"v4",
|
||||
"v7",
|
||||
"fast-rng", # Use a faster (but still sufficiently random) RNG
|
||||
"serde",
|
||||
]
|
||||
|
||||
[build-dependencies]
|
||||
vcpkg = "0.2.15"
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
## 2024-04-14, Version 0.1.0
|
||||
### Commits
|
||||
- [[`51a708a1c3`](https://github.com/versia-pub/versia-ap-layer/commit/51a708a1c3d1aa974deb148156c07dfe7e775a8c)] feat! AGPL3 + Contributor Covenant (April John)
|
||||
- [[`cc663ffc9b`](https://github.com/versia-pub/versia-ap-layer/commit/cc663ffc9b56b4b1a93ceaa2e488fc554e597790)] arbeiter (aprilthepink)
|
||||
- [[`1e6c005698`](https://github.com/versia-pub/versia-ap-layer/commit/1e6c005698df8c781c60abfb1bab94d0b968c4d9)] enable ttp sign (aprilthepink)
|
||||
- [[`0060d8baa8`](https://github.com/versia-pub/versia-ap-layer/commit/0060d8baa8b356d61d0aabfcd6b6f5143a47e8fa)] boop (aprilthepink)
|
||||
- [[`522ecabd07`](https://github.com/versia-pub/versia-ap-layer/commit/522ecabd078b7b35276f2aa4f255f967f0b958bf)] aaaa (aprilthepink)
|
||||
- [[`e75b523bb3`](https://github.com/versia-pub/versia-ap-layer/commit/e75b523bb3d74bcf696313a320c35caa6f78e225)] awa (aprilthepink)
|
||||
- [[`76d20cb2ef`](https://github.com/versia-pub/versia-ap-layer/commit/76d20cb2ef8b3ef3ebe68e5a58c6f7588597ad4e)] awa (aprilthepink)
|
||||
- [[`4d3657132d`](https://github.com/versia-pub/versia-ap-layer/commit/4d3657132d764e71f70878de1ee1a284427eb7bb)] mew (aprilthepink)
|
||||
- [[`643dc59b4b`](https://github.com/versia-pub/versia-ap-layer/commit/643dc59b4bb0d7da13daf8d6ef47f3e0f31d1500)] Use Action badge on readme (aprilthepink)
|
||||
- [[`3f4618d4d2`](https://github.com/versia-pub/versia-ap-layer/commit/3f4618d4d2291e16369fbb4ecddd35d6a841833e)] [feat] GH nix builds (aprilthepink)
|
||||
- [[`9bc640aadc`](https://github.com/versia-pub/versia-ap-layer/commit/9bc640aadcd2c02374fa195aed09452b4a3bb3ea)] [fix] format files (aprilthepink)
|
||||
- [[`b90a332b3c`](https://github.com/versia-pub/versia-ap-layer/commit/b90a332b3c1e23198d1fad3caddcc8b69dee6a4b)] basic AP (aprilthepink)
|
||||
- [[`1c09eb793d`](https://github.com/versia-pub/versia-ap-layer/commit/1c09eb793db0e9b29f3d45b044e269311420ed8b)] Rework readme (aprilthepink)
|
||||
- [[`091b8efe8e`](https://github.com/versia-pub/versia-ap-layer/commit/091b8efe8e0578304a81722d11e12db906c4edbc)] [feat]: basic nix dev enviroment (aprilthepink)
|
||||
- [[`51c7a6d6a2`](https://github.com/versia-pub/versia-ap-layer/commit/51c7a6d6a2054f6dafbbac70c6b41239e56e2fe9)] Removed mod entities from main.rs (Helba)
|
||||
- [[`9a021e768d`](https://github.com/versia-pub/versia-ap-layer/commit/9a021e768d62d4355324f9137e63ae279358ebed)] Create LICENSE (Helba)
|
||||
- [[`9609c7ab83`](https://github.com/versia-pub/versia-ap-layer/commit/9609c7ab83251ca31ded4f0589a7dac04ceca874)] initial commit (Helba)
|
||||
- [[`51a708a1c3`](https://github.com/lysand-org/lysand-ap-layer/commit/51a708a1c3d1aa974deb148156c07dfe7e775a8c)] feat! AGPL3 + Contributor Covenant (April John)
|
||||
- [[`cc663ffc9b`](https://github.com/lysand-org/lysand-ap-layer/commit/cc663ffc9b56b4b1a93ceaa2e488fc554e597790)] arbeiter (aprilthepink)
|
||||
- [[`1e6c005698`](https://github.com/lysand-org/lysand-ap-layer/commit/1e6c005698df8c781c60abfb1bab94d0b968c4d9)] enable ttp sign (aprilthepink)
|
||||
- [[`0060d8baa8`](https://github.com/lysand-org/lysand-ap-layer/commit/0060d8baa8b356d61d0aabfcd6b6f5143a47e8fa)] boop (aprilthepink)
|
||||
- [[`522ecabd07`](https://github.com/lysand-org/lysand-ap-layer/commit/522ecabd078b7b35276f2aa4f255f967f0b958bf)] aaaa (aprilthepink)
|
||||
- [[`e75b523bb3`](https://github.com/lysand-org/lysand-ap-layer/commit/e75b523bb3d74bcf696313a320c35caa6f78e225)] awa (aprilthepink)
|
||||
- [[`76d20cb2ef`](https://github.com/lysand-org/lysand-ap-layer/commit/76d20cb2ef8b3ef3ebe68e5a58c6f7588597ad4e)] awa (aprilthepink)
|
||||
- [[`4d3657132d`](https://github.com/lysand-org/lysand-ap-layer/commit/4d3657132d764e71f70878de1ee1a284427eb7bb)] mew (aprilthepink)
|
||||
- [[`643dc59b4b`](https://github.com/lysand-org/lysand-ap-layer/commit/643dc59b4bb0d7da13daf8d6ef47f3e0f31d1500)] Use Action badge on readme (aprilthepink)
|
||||
- [[`3f4618d4d2`](https://github.com/lysand-org/lysand-ap-layer/commit/3f4618d4d2291e16369fbb4ecddd35d6a841833e)] [feat] GH nix builds (aprilthepink)
|
||||
- [[`9bc640aadc`](https://github.com/lysand-org/lysand-ap-layer/commit/9bc640aadcd2c02374fa195aed09452b4a3bb3ea)] [fix] format files (aprilthepink)
|
||||
- [[`b90a332b3c`](https://github.com/lysand-org/lysand-ap-layer/commit/b90a332b3c1e23198d1fad3caddcc8b69dee6a4b)] basic AP (aprilthepink)
|
||||
- [[`1c09eb793d`](https://github.com/lysand-org/lysand-ap-layer/commit/1c09eb793db0e9b29f3d45b044e269311420ed8b)] Rework readme (aprilthepink)
|
||||
- [[`091b8efe8e`](https://github.com/lysand-org/lysand-ap-layer/commit/091b8efe8e0578304a81722d11e12db906c4edbc)] [feat]: basic nix dev enviroment (aprilthepink)
|
||||
- [[`51c7a6d6a2`](https://github.com/lysand-org/lysand-ap-layer/commit/51c7a6d6a2054f6dafbbac70c6b41239e56e2fe9)] Removed mod entities from main.rs (Helba)
|
||||
- [[`9a021e768d`](https://github.com/lysand-org/lysand-ap-layer/commit/9a021e768d62d4355324f9137e63ae279358ebed)] Create LICENSE (Helba)
|
||||
- [[`9609c7ab83`](https://github.com/lysand-org/lysand-ap-layer/commit/9609c7ab83251ca31ded4f0589a7dac04ceca874)] initial commit (Helba)
|
||||
|
||||
### Stats
|
||||
```diff
|
||||
13
Dockerfile
Normal file
13
Dockerfile
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
FROM rust:slim as builder
|
||||
RUN apt-get update && apt-get install -y libpq-dev libssl-dev pkg-config musl-tools perl make && rm -rf /var/lib/apt/lists/*
|
||||
RUN rustup target add x86_64-unknown-linux-musl
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
RUN cargo build --release --target x86_64-unknown-linux-musl
|
||||
RUN strip /app/target/x86_64-unknown-linux-musl/release/microservice
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/microservice /microservice
|
||||
WORKDIR /
|
||||
CMD ["/microservice"]
|
||||
32
README.MD
32
README.MD
|
|
@ -1,15 +1,7 @@
|
|||
|
||||
<p align="center">
|
||||
<a href="https://versia.pub"><img src="https://cdn.versia.pub/branding/logo-dark.svg" alt="Versia Logo" height="110"></a>
|
||||
</p>
|
||||
|
||||
## Versia ActivityPub Bridge
|
||||
|
||||
[](https://github.com/versia-pub/activitypub/actions/workflows/nix-flake.yml)
|
||||
|
||||
**ActivityPub/ActivityStreams** compatibility layer for [**Versia Server**](https://github.com/versia-pub/versia).
|
||||
|
||||
Designed as a microservice, runs as its own process and communicates with the main server via HTTP.
|
||||
## Lysand ActivityPub Layer
|
||||
[](https://github.com/lysand-org/lysand-ap-layer/actions/workflows/nix-flake.yml)
|
||||
A simple activitypub compatibility layer ("bridge"), to make Lysand compatible with the ActivityPub and ActivityStreams protocol.
|
||||
The layer is realised in a microservice format.
|
||||
|
||||
## Development (Flakes)
|
||||
|
||||
|
|
@ -28,22 +20,16 @@ nix build
|
|||
|
||||
We also provide a [`justfile`](https://just.systems/) for Makefile'esque commands.
|
||||
|
||||
## Building
|
||||
### Building and running the docker image
|
||||
|
||||
### Docker/Podman
|
||||
|
||||
To build the Docker image, run the following command:
|
||||
To build the docker image, run the following command:
|
||||
|
||||
```bash
|
||||
docker build -t activitypub:latest .
|
||||
> docker build -t f:latest .
|
||||
```
|
||||
|
||||
To run the docker image, use the [`docker-compose.yml`](./docker-compose.yml) file:
|
||||
To run the docker image, run the following command:
|
||||
|
||||
```bash
|
||||
wget https://raw.githubusercontent.com/versia-pub/activitypub/main/docker-compose.yml
|
||||
|
||||
docker-compose up -d
|
||||
docker run -i -e RUST_LOG="debug" -e DATABASE_URL="postgresql://postgres:postgres@host.docker.internal:5432/database" -e LISTEN="0.0.0.0:8080" -p 8080:8080 f:latest
|
||||
```
|
||||
|
||||
If you are building from source, make sure to replace the image name in the `docker-compose.yml` file.
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://donotsta.re/schemas/litepub-0.1.jsonld",
|
||||
{
|
||||
"@language": "und"
|
||||
}
|
||||
],
|
||||
"alsoKnownAs": [
|
||||
"https://catcatnya.com/users/riedler"
|
||||
],
|
||||
"attachment": [
|
||||
{
|
||||
"name": "Languages",
|
||||
"type": "PropertyValue",
|
||||
"value": "Austrian (native), English (fluent), Standard German (fluent)"
|
||||
},
|
||||
{
|
||||
"name": "Website",
|
||||
"type": "PropertyValue",
|
||||
"value": "<a href=\"https://riedler.wien/\" rel=\"ugc\">https://riedler.wien/</a>"
|
||||
},
|
||||
{
|
||||
"name": "programming (order: familiarity)",
|
||||
"type": "PropertyValue",
|
||||
"value": "Python, PHP, JS, C99, Java"
|
||||
},
|
||||
{
|
||||
"name": "webdev (order: random)",
|
||||
"type": "PropertyValue",
|
||||
"value": "HTML5, CSS3, SVG"
|
||||
},
|
||||
{
|
||||
"name": "pronounciation (approx)",
|
||||
"type": "PropertyValue",
|
||||
"value": "reedluh"
|
||||
},
|
||||
{
|
||||
"name": "pronounciation (IPA)",
|
||||
"type": "PropertyValue",
|
||||
"value": "ʁiːdlä"
|
||||
},
|
||||
{
|
||||
"name": "personal info :3",
|
||||
"type": "PropertyValue",
|
||||
"value": "<a href=\"https://pronouns.cc/@Riedler\" rel=\"ugc\">https://pronouns.cc/@Riedler</a>"
|
||||
},
|
||||
{
|
||||
"name": "age",
|
||||
"type": "PropertyValue",
|
||||
"value": "adult"
|
||||
}
|
||||
],
|
||||
"capabilities": {},
|
||||
"discoverable": true,
|
||||
"endpoints": {
|
||||
"oauthAuthorizationEndpoint": "https://donotsta.re/oauth/authorize",
|
||||
"oauthRegistrationEndpoint": "https://donotsta.re/api/v1/apps",
|
||||
"oauthTokenEndpoint": "https://donotsta.re/oauth/token",
|
||||
"sharedInbox": "https://donotsta.re/inbox"
|
||||
},
|
||||
"featured": "https://donotsta.re/users/Riedler/collections/featured",
|
||||
"followers": "https://donotsta.re/users/Riedler/followers",
|
||||
"following": "https://donotsta.re/users/Riedler/following",
|
||||
"icon": {
|
||||
"type": "Image",
|
||||
"url": "https://asdf.donotsta.re/media/c5671c1d2d6eec83124b00dc2eb010cef7b4d9734074a27feff0cd2b9eac91dc.png"
|
||||
},
|
||||
"id": "https://donotsta.re/users/Riedler",
|
||||
"image": {
|
||||
"type": "Image",
|
||||
"url": "https://asdf.donotsta.re/media/397f19d2f61853b9cba4f63aa7d4235225a4744a188279f6095b3cfbdf19a1eb.png"
|
||||
},
|
||||
"inbox": "https://donotsta.re/users/Riedler/inbox",
|
||||
"manuallyApprovesFollowers": true,
|
||||
"name": "Riedler (2004) [E] [!]",
|
||||
"outbox": "https://donotsta.re/users/Riedler/outbox",
|
||||
"preferredUsername": "Riedler",
|
||||
"publicKey": {
|
||||
"id": "https://donotsta.re/users/Riedler#main-key",
|
||||
"owner": "https://donotsta.re/users/Riedler",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4rUBqCAMMSDjuiepr+3R\nZMLZ1qF4ybsAI1iOcMU+l7YLkZao8KpfYb7NI5AoXUiR8OhCaFFLgbnQQOBuXzzT\nmMa5KOMEiCUwTCSN7nHrAcULWuT9RPA8Uo/itin2Q5rLkGQHWu04IuBZSrLyql4E\nvnRIybaFaNx/uYIsVD6UI/9Gmp1HSYn1hfzSVywZy1umeo7dimRN8joaAy4VTBgl\nyA5TgFsEWKF//4JUtdPCIRMnRgUnihwNPk0dkWa7fPt4syFWEg1smjulFKJ4hSB6\nJ9oqQJ0Wj0i+K+x+hS68PG38Ofj+BTlF0os2MmEcSXG9859W7hAhA0TzoQQsEXDM\nHwIDAQAB\n-----END PUBLIC KEY-----\n\n"
|
||||
},
|
||||
"summary": "Austrian Musician and Programmer. From Linz, living in Vienna.<br/><br/>Several people have told me that not all of this bio federates to other software properly, so please open my profile externally, to read the whole thing. Thanks, and sorry for the inconvenience!<br/><br/>In uwus with <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AcLGFPIEBPczHLP2Lw\" href=\"https://tech.lgbt/@Autumn_Maxime\" rel=\"ugc\">@<span>Autumn_Maxime@tech.lgbt</span></a></span>, <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AAcLsLRX2zm0dbbXHM\" href=\"https://donotsta.re/users/domi\" rel=\"ugc\">@<span>domi@donotsta.re</span></a></span>, she who bangs outside of fedi, <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"Ae0GjjJMdsUKeCS8jQ\" href=\"https://not.an.evilcyberhacker.net/@phos\" rel=\"ugc\">@<span>phos@not.an.evilcyberhacker.net</span></a></span>, <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AXcm6z3VImlLMSmYwi\" href=\"https://not.an.evilcyberhacker.net/@darkphoenix\" rel=\"ugc\">@<span>darkphoenix@not.an.evilcyberhacker.net</span></a></span> and <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AQMoXdVLtxjDMiK0SO\" href=\"https://donotsta.re/users/Lili\" rel=\"ugc\">@<span>lili@donotsta.re</span></a></span> :3<br/><br/>Professional Reviews™:<br/> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AQkD6XWhWPJnGeN2MS\" href=\"https://catcatnya.com/@benaryorg\" rel=\"ugc\">@<span>benaryorg@catcatnya.com</span></a></span> - "suspiciously knowledgeable tech person with inspiring Thoughts™ and Opinions™"<br/> shebang - "bsdhdkshhejeeh riedler whaaa eirurjrjrkeikwwbebrufyfirjrhjwnedj"<br/> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AXv31NdX2IJbWIgyUy\" href=\"https://plush.city/@heatherhorns_lite\" rel=\"ugc\">@<span>heatherhorns_lite@plush.city</span></a></span> - "Riedler's got the moves"<br/> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AdVvWtdO2ct6eq1Z56\" href=\"https://tech.lgbt/@keyboardsmash\" rel=\"ugc\">@<span>keyboardsmash@tech.lgbt</span></a></span> - "i know them from the internet and they are nice. also dad jokes."<br/> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AiWaF6eoRSyY7HVttQ\" href=\"https://donotsta.re/users/Ak1ra\" rel=\"ugc\">@<span>Ak1ra</span></a></span> - "You are very caring Riedler"<br/> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AAcLsLRX2zm0dbbXHM\" href=\"https://donotsta.re/users/domi\" rel=\"ugc\">@<span>domi</span></a></span> - "God's biggest eeper"<br/> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AWFInQhcYuy288f9bk\" href=\"https://donotsta.re/users/april\" rel=\"ugc\">@<span>april</span></a></span> - "I meowed at you, why u don respond🥺"<br/> <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AQMoXdVLtxjDMiK0SO\" href=\"https://donotsta.re/users/Lili\" rel=\"ugc\">@<span>Lili</span></a></span> - "someday I'll think about something"<br/><br/>I accept follow requests if you pass the vibe check. Try it out :meowmlem: To check that you read my bio, tell me your favourite food.<br/><br/>Profile picture is a very stylized version of a capital R. It's a pink-white-blue sine wave with a softened nail thing poking through the left side. Yes, trans colors.<br/>Header is a very stylized version of "Riedler". The shape of the Rs are similar to my profile picture, just without the nail thing. Various shapes have lines exending throughout the rest of the image, creating a nice pattern between the pink, white and blue.",
|
||||
"tag": [
|
||||
{
|
||||
"icon": {
|
||||
"type": "Image",
|
||||
"url": "https://donotsta.re/emoji/meow/meowmlem.png"
|
||||
},
|
||||
"id": "https://donotsta.re/emoji/meow/meowmlem.png",
|
||||
"name": ":meowmlem:",
|
||||
"type": "Emoji",
|
||||
"updated": "1970-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"type": "Person",
|
||||
"url": "https://donotsta.re/users/Riedler"
|
||||
}
|
||||
11
build.rs
Normal file
11
build.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#[cfg(target_os = "windows")]
|
||||
fn main() {
|
||||
vcpkg::Config::new()
|
||||
.emit_includes(true)
|
||||
.copy_dlls(true)
|
||||
.find_package("libpq")
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn main() {}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
git pull
|
||||
nix run .#ls-ap-migration
|
||||
RUST_DEBUG=1 cargo run
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
services:
|
||||
activitypub:
|
||||
environment:
|
||||
- RUST_LOG=debug
|
||||
- DATABASE_URL=postgresql://postgres:postgres@host.docker.internal:5432/database
|
||||
- LISTEN=0.0.0.0:8080
|
||||
ports:
|
||||
- 8080:8080
|
||||
image: ghcr.io/versia-pub/activitypub:main
|
||||
96
flake.lock
generated
96
flake.lock
generated
|
|
@ -1,29 +1,15 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-compat": {
|
||||
"locked": {
|
||||
"lastModified": 1733328505,
|
||||
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
|
||||
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
|
||||
"revCount": 69,
|
||||
"type": "tarball",
|
||||
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz?rev=ff81ac966bb2cae68946d5ed5fc4994f96d0ffec&revCount=69"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
"url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
|
||||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1738453229,
|
||||
"narHash": "sha256-7H9XgNiGLKN1G1CgRh0vUL4AheZSYzPm+zmZ7vxbJdo=",
|
||||
"lastModified": 1709336216,
|
||||
"narHash": "sha256-Dt/wOWeW6Sqm11Yh+2+t0dfEWxoMxGBvv3JpIocFl9E=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "32ea77a06711b758da0ad9bd6a844c5740a87abd",
|
||||
"rev": "f7b3c975cf067e56e7cda6cb098ebe3fb4d74ca2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -32,55 +18,13 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"naersk": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1736429655,
|
||||
"narHash": "sha256-BwMekRuVlSB9C0QgwKMICiJ5EVbLGjfe4qyueyNQyGI=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "0621e47bd95542b8e1ce2ee2d65d6a1f887a13ce",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 0,
|
||||
"narHash": "sha256-8Eo/jRAgT3CbAloyqOj6uPN1EqBvLI/Tv2g+RxHjkhU=",
|
||||
"path": "/nix/store/vg3rs6imxilxn66gf6vb8m98d7ib35f8-source",
|
||||
"type": "path"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1738452942,
|
||||
"narHash": "sha256-vJzFZGaCpnmo7I6i416HaBLpC+hvcURh/BQwROcGIp8=",
|
||||
"type": "tarball",
|
||||
"url": "https://github.com/NixOS/nixpkgs/archive/072a6db25e947df2f31aab9eccd0ab75d5b2da11.tar.gz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
"url": "https://github.com/NixOS/nixpkgs/archive/072a6db25e947df2f31aab9eccd0ab75d5b2da11.tar.gz"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1739020877,
|
||||
"narHash": "sha256-mIvECo/NNdJJ/bXjNqIh8yeoSjVLAuDuTUzAo7dzs8Y=",
|
||||
"lastModified": 1710806803,
|
||||
"narHash": "sha256-qrxvLS888pNJFwJdK+hf1wpRCSQcqA6W5+Ox202NDa0=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a79cfe0ebd24952b580b1cf08cd906354996d547",
|
||||
"rev": "b06025f1533a1e07b6db3e75151caa155d1c7eb3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -90,12 +34,28 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"dir": "lib",
|
||||
"lastModified": 1709237383,
|
||||
"narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"dir": "lib",
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-parts": "flake-parts",
|
||||
"naersk": "naersk",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"systems": "systems",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
}
|
||||
|
|
@ -122,11 +82,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1738953846,
|
||||
"narHash": "sha256-yrK3Hjcr8F7qS/j2F+r7C7o010eVWWlm4T1PrbKBOxQ=",
|
||||
"lastModified": 1710781103,
|
||||
"narHash": "sha256-nehQK/XTFxfa6rYKtbi8M1w+IU1v5twYhiyA4dg1vpg=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "4f09b473c936d41582dd744e19f34ec27592c5fd",
|
||||
"rev": "7ee5aaac63c30d3c97a8c56efe89f3b2aa9ae564",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
63
flake.nix
63
flake.nix
|
|
@ -5,44 +5,24 @@
|
|||
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||
systems.url = "github:nix-systems/default";
|
||||
|
||||
naersk.url = "github:nix-community/naersk";
|
||||
flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz";
|
||||
|
||||
# Dev tools
|
||||
treefmt-nix.url = "github:numtide/treefmt-nix";
|
||||
treefmt-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
nixConfig = {
|
||||
extra-substituters = [
|
||||
"https://cache.kyouma.net"
|
||||
];
|
||||
extra-trusted-public-keys = [
|
||||
"cache.kyouma.net:Frjwu4q1rnwE/MnSTmX9yx86GNA/z3p/oElGvucLiZg="
|
||||
];
|
||||
};
|
||||
|
||||
outputs = inputs@{ flake-parts, self, ... }:
|
||||
inputs.flake-parts.lib.mkFlake { inherit inputs self; } {
|
||||
outputs = inputs@{ flake-parts, ... }:
|
||||
inputs.flake-parts.lib.mkFlake { inherit inputs; } {
|
||||
systems = import inputs.systems;
|
||||
flake = {
|
||||
hydraJobs = inputs.nixpkgs.lib.genAttrs [ "packages" "checks" "devShells" ] (attrs: {
|
||||
inherit (self.${attrs}) x86_64-linux aarch64-linux;
|
||||
});
|
||||
};
|
||||
imports = [
|
||||
inputs.treefmt-nix.flakeModule
|
||||
inputs.flake-parts.flakeModules.easyOverlay
|
||||
];
|
||||
perSystem = { config, self', pkgs, lib, system, ... }:
|
||||
let
|
||||
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
|
||||
cargoMigToml = builtins.fromTOML (builtins.readFile ./migration/Cargo.toml);
|
||||
nonRustDeps = with pkgs; [
|
||||
libiconv
|
||||
openssl
|
||||
];
|
||||
naersk' = pkgs.callPackage inputs.naersk { };
|
||||
rust-toolchain = pkgs.symlinkJoin {
|
||||
name = "rust-toolchain";
|
||||
paths = [ pkgs.rustc pkgs.cargo pkgs.cargo-watch pkgs.rust-analyzer pkgs.rustPlatform.rustcSrc ];
|
||||
|
|
@ -52,51 +32,17 @@
|
|||
};
|
||||
in
|
||||
{
|
||||
overlayAttrs = {
|
||||
inherit (config.packages) versia-ap-layer ls-ap-migration;
|
||||
};
|
||||
# Rust package
|
||||
packages.default = config.packages.versia-ap-layer;
|
||||
packages.versia-ap-layer = naersk'.buildPackage {
|
||||
packages.default = pkgs.rustPlatform.buildRustPackage {
|
||||
inherit (cargoToml.package) name version;
|
||||
src = ./.;
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
buildInputs = nonRustDeps;
|
||||
nativeBuildInputs = with pkgs; [
|
||||
rust-toolchain
|
||||
pkg-config
|
||||
];
|
||||
};
|
||||
packages.ls-ap-migration = naersk'.buildPackage {
|
||||
inherit (cargoMigToml.package) name version;
|
||||
src = ./migration;
|
||||
buildInputs = nonRustDeps;
|
||||
nativeBuildInputs = with pkgs; [
|
||||
rust-toolchain
|
||||
pkg-config
|
||||
];
|
||||
};
|
||||
packages.ociImage = pkgs.dockerTools.buildLayeredImage
|
||||
{
|
||||
name = "ghcr.io/versia-pub/activitypub";
|
||||
tag = "main";
|
||||
contents = [
|
||||
config.packages.versia-ap-layer
|
||||
config.packages.ls-ap-migration
|
||||
pkgs.bash
|
||||
];
|
||||
config = {
|
||||
Cmd = [
|
||||
"${pkgs.bash}/bin/bash"
|
||||
"${config.packages.ls-ap-migration}/bin/ls-ap-migration"
|
||||
"up"
|
||||
"&&"
|
||||
"${config.packages.versia-ap-layer}/bin/versia-ap-layer"
|
||||
];
|
||||
ExposedPorts = {
|
||||
"8080/tcp" = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Rust dev environment
|
||||
devShells.default = pkgs.mkShell {
|
||||
|
|
@ -116,7 +62,6 @@
|
|||
just
|
||||
rust-toolchain
|
||||
pkg-config
|
||||
sea-orm-cli
|
||||
];
|
||||
RUST_BACKTRACE = 1;
|
||||
};
|
||||
|
|
|
|||
2765
migration/Cargo.lock
generated
2765
migration/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,25 +0,0 @@
|
|||
[package]
|
||||
name = "ls-ap-migration"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "migration"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
async-std = { version = "1", features = ["attributes", "tokio1"] }
|
||||
chrono = "0.4.38"
|
||||
dotenv = "0.15.0"
|
||||
|
||||
[dependencies.sea-orm-migration]
|
||||
version = "0.12.0"
|
||||
features = [
|
||||
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
|
||||
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
|
||||
# e.g.
|
||||
"runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
|
||||
"sqlx-postgres", # `DATABASE_DRIVER` feature
|
||||
"sqlx-sqlite","sqlx-mysql","with-chrono"
|
||||
]
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
# Running Migrator CLI
|
||||
|
||||
- Generate a new migration file
|
||||
```sh
|
||||
cargo run -- generate MIGRATION_NAME
|
||||
```
|
||||
- Apply all pending migrations
|
||||
```sh
|
||||
cargo run
|
||||
```
|
||||
```sh
|
||||
cargo run -- up
|
||||
```
|
||||
- Apply first 10 pending migrations
|
||||
```sh
|
||||
cargo run -- up -n 10
|
||||
```
|
||||
- Rollback last applied migrations
|
||||
```sh
|
||||
cargo run -- down
|
||||
```
|
||||
- Rollback last 10 applied migrations
|
||||
```sh
|
||||
cargo run -- down -n 10
|
||||
```
|
||||
- Drop all tables from the database, then reapply all migrations
|
||||
```sh
|
||||
cargo run -- fresh
|
||||
```
|
||||
- Rollback all applied migrations, then reapply all migrations
|
||||
```sh
|
||||
cargo run -- refresh
|
||||
```
|
||||
- Rollback all applied migrations
|
||||
```sh
|
||||
cargo run -- reset
|
||||
```
|
||||
- Check the status of all migrations
|
||||
```sh
|
||||
cargo run -- status
|
||||
```
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
pub use sea_orm_migration::prelude::*;
|
||||
|
||||
mod m20220101_000001_post_table;
|
||||
mod m20240417_230111_user_table;
|
||||
mod m20240417_233430_post_user_keys;
|
||||
mod m20240505_002524_user_follow_relation;
|
||||
mod m20240626_030922_store_ap_json_in_posts;
|
||||
mod m20240719_235452_user_ap_column;
|
||||
mod m20240725_120932_follow_table_two_point_zero;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![
|
||||
Box::new(m20220101_000001_post_table::Migration),
|
||||
Box::new(m20240417_230111_user_table::Migration),
|
||||
Box::new(m20240417_233430_post_user_keys::Migration),
|
||||
Box::new(m20240505_002524_user_follow_relation::Migration),
|
||||
Box::new(m20240626_030922_store_ap_json_in_posts::Migration),
|
||||
Box::new(m20240719_235452_user_ap_column::Migration),
|
||||
Box::new(m20240725_120932_follow_table_two_point_zero::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Post::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(Post::Id).string().not_null().primary_key())
|
||||
.col(ColumnDef::new(Post::Title).string())
|
||||
.col(ColumnDef::new(Post::Content).string().not_null())
|
||||
.col(ColumnDef::new(Post::Local).boolean().not_null())
|
||||
.col(ColumnDef::new(Post::CreatedAt).timestamp().not_null())
|
||||
.col(ColumnDef::new(Post::UpdatedAt).timestamp())
|
||||
.col(ColumnDef::new(Post::ReblogId).string())
|
||||
.col(ColumnDef::new(Post::ContentType).string().not_null())
|
||||
.col(ColumnDef::new(Post::Visibility).string().not_null())
|
||||
.col(ColumnDef::new(Post::ReplyId).string())
|
||||
.col(ColumnDef::new(Post::QuotingId).string())
|
||||
.col(ColumnDef::new(Post::Sensitive).boolean().not_null())
|
||||
.col(ColumnDef::new(Post::SpoilerText).string())
|
||||
.col(ColumnDef::new(Post::Creator).string().not_null())
|
||||
.col(ColumnDef::new(Post::Url).string().not_null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Post::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
pub enum Post {
|
||||
Table,
|
||||
Id,
|
||||
Url,
|
||||
Creator,
|
||||
Title,
|
||||
Content,
|
||||
Local,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
ReblogId,
|
||||
ContentType,
|
||||
Visibility,
|
||||
ReplyId,
|
||||
QuotingId,
|
||||
Sensitive,
|
||||
SpoilerText,
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(User::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(User::Id).string().not_null().primary_key())
|
||||
.col(ColumnDef::new(User::Username).string().not_null())
|
||||
.col(ColumnDef::new(User::Name).string().not_null())
|
||||
.col(ColumnDef::new(User::Summary).string())
|
||||
.col(ColumnDef::new(User::Url).string().not_null())
|
||||
.col(ColumnDef::new(User::PublicKey).string().not_null())
|
||||
.col(ColumnDef::new(User::PrivateKey).string())
|
||||
.col(ColumnDef::new(User::LastRefreshedAt).timestamp().not_null())
|
||||
.col(ColumnDef::new(User::Local).boolean().not_null())
|
||||
.col(ColumnDef::new(User::FollowerCount).integer().not_null())
|
||||
.col(ColumnDef::new(User::FollowingCount).integer().not_null())
|
||||
.col(ColumnDef::new(User::CreatedAt).timestamp().not_null())
|
||||
.col(ColumnDef::new(User::UpdatedAt).timestamp())
|
||||
.col(ColumnDef::new(User::Following).string())
|
||||
.col(ColumnDef::new(User::Followers).string())
|
||||
.col(ColumnDef::new(User::Inbox).string().not_null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(User::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
pub enum User {
|
||||
Table,
|
||||
Id,
|
||||
Username,
|
||||
Name,
|
||||
Summary,
|
||||
Url,
|
||||
PublicKey,
|
||||
PrivateKey,
|
||||
LastRefreshedAt,
|
||||
Local,
|
||||
FollowerCount,
|
||||
FollowingCount,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
Following,
|
||||
Followers,
|
||||
Inbox,
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
use crate::{m20220101_000001_post_table::Post, m20240417_230111_user_table::User};
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Post::Table).to_owned())
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Post::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(Post::Id).string().not_null().primary_key())
|
||||
.col(ColumnDef::new(Post::Title).string())
|
||||
.col(ColumnDef::new(Post::Content).string().not_null())
|
||||
.col(ColumnDef::new(Post::Local).boolean().not_null())
|
||||
.col(ColumnDef::new(Post::CreatedAt).timestamp().not_null())
|
||||
.col(ColumnDef::new(Post::UpdatedAt).timestamp())
|
||||
.col(ColumnDef::new(Post::ReblogId).string())
|
||||
.col(ColumnDef::new(Post::ContentType).string().not_null())
|
||||
.col(ColumnDef::new(Post::Visibility).string().not_null())
|
||||
.col(ColumnDef::new(Post::ReplyId).string())
|
||||
.col(ColumnDef::new(Post::QuotingId).string())
|
||||
.col(ColumnDef::new(Post::Sensitive).boolean().not_null())
|
||||
.col(ColumnDef::new(Post::SpoilerText).string())
|
||||
.col(ColumnDef::new(Post::Creator).string().not_null())
|
||||
.col(ColumnDef::new(Post::Url).string().not_null())
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_post_creator_user_id")
|
||||
.from(Post::Table, Post::Creator)
|
||||
.to(User::Table, User::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_post_reblog_id")
|
||||
.from(Post::Table, Post::ReblogId)
|
||||
.to(Post::Table, Post::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_post_reply_id")
|
||||
.from(Post::Table, Post::ReplyId)
|
||||
.to(Post::Table, Post::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_post_quoting_id")
|
||||
.from(Post::Table, Post::QuotingId)
|
||||
.to(Post::Table, Post::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_foreign_key(
|
||||
ForeignKey::drop()
|
||||
.name("fk_post_creator_user_id")
|
||||
.table(Post::Table)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
use crate::m20240417_230111_user_table::User;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(FollowRelation::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(FollowRelation::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(FollowRelation::FolloweeId)
|
||||
.string()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(FollowRelation::FollowerId)
|
||||
.string()
|
||||
.not_null(),
|
||||
)
|
||||
.col(ColumnDef::new(FollowRelation::FolloweeHost).string())
|
||||
.col(ColumnDef::new(FollowRelation::FollowerHost).string())
|
||||
.col(ColumnDef::new(FollowRelation::FolloweeInbox).string())
|
||||
.col(ColumnDef::new(FollowRelation::FollowerInbox).string())
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_follow_relation_followee_id")
|
||||
.from(FollowRelation::Table, FollowRelation::FolloweeId)
|
||||
.to(User::Table, User::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_follow_relation_follower_id")
|
||||
.from(FollowRelation::Table, FollowRelation::FollowerId)
|
||||
.to(User::Table, User::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(FollowRelation::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum FollowRelation {
|
||||
Table,
|
||||
Id,
|
||||
FolloweeId,
|
||||
FollowerId,
|
||||
FolloweeHost,
|
||||
FollowerHost,
|
||||
FolloweeInbox,
|
||||
FollowerInbox,
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Post::Table)
|
||||
.add_column_if_not_exists(ColumnDef::new(Post::ApJson).string())
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Post::Table)
|
||||
.drop_column(Post::ApJson)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
pub enum Post {
|
||||
Table,
|
||||
ApJson,
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(User::Table)
|
||||
.add_column_if_not_exists(ColumnDef::new(User::ApJson).string())
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(User::Table)
|
||||
.drop_column(User::ApJson)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
pub enum User {
|
||||
Table,
|
||||
ApJson,
|
||||
}
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
use crate::m20240417_230111_user_table::User;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let _ = manager
|
||||
.drop_table(Table::drop().table(FollowRelation::Table).to_owned())
|
||||
.await;
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(FollowRelation::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(FollowRelation::Id)
|
||||
.string()
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(FollowRelation::FolloweeId)
|
||||
.string()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(FollowRelation::FollowerId)
|
||||
.string()
|
||||
.not_null(),
|
||||
)
|
||||
.col(ColumnDef::new(FollowRelation::FolloweeHost).string())
|
||||
.col(ColumnDef::new(FollowRelation::FollowerHost).string())
|
||||
.col(ColumnDef::new(FollowRelation::FolloweeInbox).string())
|
||||
.col(ColumnDef::new(FollowRelation::FollowerInbox).string())
|
||||
.col(ColumnDef::new(FollowRelation::AcceptId).string())
|
||||
.col(ColumnDef::new(FollowRelation::ApId).string())
|
||||
.col(ColumnDef::new(FollowRelation::ApAcceptId).string())
|
||||
.col(ColumnDef::new(FollowRelation::Remote).boolean().not_null())
|
||||
.col(ColumnDef::new(FollowRelation::ApJson).string().not_null())
|
||||
.col(ColumnDef::new(FollowRelation::ApAcceptJson).string())
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_follow_relation_followee_id")
|
||||
.from(FollowRelation::Table, FollowRelation::FolloweeId)
|
||||
.to(User::Table, User::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_follow_relation_follower_id")
|
||||
.from(FollowRelation::Table, FollowRelation::FollowerId)
|
||||
.to(User::Table, User::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(FollowRelation::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum FollowRelation {
|
||||
Table,
|
||||
Id,
|
||||
AcceptId,
|
||||
Remote, // true if initial Follow came from Remote
|
||||
ApId,
|
||||
ApAcceptId,
|
||||
FolloweeId,
|
||||
FollowerId,
|
||||
FolloweeHost,
|
||||
FollowerHost,
|
||||
FolloweeInbox,
|
||||
FollowerInbox,
|
||||
ApJson,
|
||||
ApAcceptJson,
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
use dotenv::dotenv;
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
dotenv().ok();
|
||||
cli::run_cli(migration::Migrator).await;
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
nix run .#ls-ap-migration
|
||||
nix run .#versia-ap-layer
|
||||
23
shell.nix
23
shell.nix
|
|
@ -1,10 +1,13 @@
|
|||
(import
|
||||
(
|
||||
let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in
|
||||
fetchTarball {
|
||||
url = lock.nodes.flake-compat.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
|
||||
sha256 = lock.nodes.flake-compat.locked.narHash;
|
||||
}
|
||||
)
|
||||
{ src = ./.; }
|
||||
).shellNix
|
||||
{ pkgs ? import <nixpkgs> { } }:
|
||||
|
||||
pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
rustc
|
||||
cargo
|
||||
rustfmt
|
||||
rust-analyzer
|
||||
clippy
|
||||
];
|
||||
|
||||
RUST_BACKTRACE = 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,9 @@
|
|||
use crate::{
|
||||
database::StateHandle,
|
||||
entities::{self, post, prelude, user},
|
||||
database::DatabaseHandle,
|
||||
error::Error,
|
||||
objects::{
|
||||
person::DbUser,
|
||||
post::{DbPost, Note},
|
||||
},
|
||||
utils::{base_url_encode, generate_create_id, generate_random_object_id},
|
||||
versia::{
|
||||
conversion::{versia_post_from_db, versia_user_from_db},
|
||||
objects::SortAlphabetically,
|
||||
superx::request_client,
|
||||
},
|
||||
API_DOMAIN, AUTH, DB,
|
||||
objects::post::DbPost,
|
||||
objects::{person::DbUser, post::Note},
|
||||
utils::generate_object_id,
|
||||
};
|
||||
use activitypub_federation::{
|
||||
activity_sending::SendActivityTask,
|
||||
|
|
@ -22,16 +13,13 @@ use activitypub_federation::{
|
|||
protocol::{context::WithContext, helpers::deserialize_one_or_many},
|
||||
traits::{ActivityHandler, Object},
|
||||
};
|
||||
use reqwest::RequestBuilder;
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{info, warn};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreatePost {
|
||||
pub(crate) actor: ObjectId<user::Model>,
|
||||
pub(crate) actor: ObjectId<DbUser>,
|
||||
#[serde(deserialize_with = "deserialize_one_or_many")]
|
||||
pub(crate) to: Vec<Url>,
|
||||
pub(crate) object: Note,
|
||||
|
|
@ -41,52 +29,18 @@ pub struct CreatePost {
|
|||
}
|
||||
|
||||
impl CreatePost {
|
||||
pub async fn send(
|
||||
note: Note,
|
||||
db_entry: post::Model,
|
||||
inbox: Url,
|
||||
data: &Data<StateHandle>,
|
||||
) -> Result<(), Error> {
|
||||
pub async fn send(note: Note, inbox: Url, data: &Data<DatabaseHandle>) -> Result<(), Error> {
|
||||
print!("Sending reply to {}", ¬e.attributed_to);
|
||||
let encoded_url = base_url_encode(¬e.id.clone().into());
|
||||
let create = CreatePost {
|
||||
actor: note.attributed_to.clone(),
|
||||
to: note.to.clone(),
|
||||
object: note,
|
||||
kind: CreateType::Create,
|
||||
id: generate_create_id(data.domain(), &db_entry.id, &encoded_url)?,
|
||||
};
|
||||
let create_with_context = WithContext::new_default(create);
|
||||
let sends = SendActivityTask::prepare(
|
||||
&create_with_context,
|
||||
&data.local_user().await?,
|
||||
vec![inbox],
|
||||
data,
|
||||
)
|
||||
.await?;
|
||||
for send in sends {
|
||||
send.sign_and_send(data).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub async fn sends(
|
||||
note: Note,
|
||||
db_entry: post::Model,
|
||||
inbox: Vec<Url>,
|
||||
data: &Data<StateHandle>,
|
||||
) -> Result<(), Error> {
|
||||
print!("Sending reply to {}", ¬e.attributed_to);
|
||||
let encoded_url = base_url_encode(¬e.id.clone().into());
|
||||
let create = CreatePost {
|
||||
actor: note.attributed_to.clone(),
|
||||
to: note.to.clone(),
|
||||
object: note,
|
||||
kind: CreateType::Create,
|
||||
id: generate_create_id(data.domain(), &db_entry.id, &encoded_url)?,
|
||||
id: generate_object_id(data.domain())?,
|
||||
};
|
||||
let create_with_context = WithContext::new_default(create);
|
||||
let sends =
|
||||
SendActivityTask::prepare(&create_with_context, &data.local_user().await?, inbox, data)
|
||||
SendActivityTask::prepare(&create_with_context, &data.local_user(), vec![inbox], data)
|
||||
.await?;
|
||||
for send in sends {
|
||||
send.sign_and_send(data).await?;
|
||||
|
|
@ -97,7 +51,7 @@ impl CreatePost {
|
|||
|
||||
#[async_trait::async_trait]
|
||||
impl ActivityHandler for CreatePost {
|
||||
type DataType = StateHandle;
|
||||
type DataType = DatabaseHandle;
|
||||
type Error = crate::error::Error;
|
||||
|
||||
fn id(&self) -> &Url {
|
||||
|
|
@ -109,73 +63,12 @@ impl ActivityHandler for CreatePost {
|
|||
}
|
||||
|
||||
async fn verify(&self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
post::Model::verify(&self.object, &self.id, data).await?;
|
||||
DbPost::verify(&self.object, &self.id, data).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
let note = post::Model::from_json(self.object, data).await?;
|
||||
federate_inbox(note).await?;
|
||||
DbPost::from_json(self.object, data).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn federate_inbox(note: crate::entities::post::Model) -> anyhow::Result<()> {
|
||||
let versia_post = versia_post_from_db(note.clone()).await?;
|
||||
|
||||
let mut array;
|
||||
if versia_post.mentions.is_some() {
|
||||
info!("good");
|
||||
array = versia_post.mentions.clone().unwrap();
|
||||
info!("{:#?}", versia_post.mentions.clone().unwrap());
|
||||
} else {
|
||||
info!("fake");
|
||||
array = Vec::new();
|
||||
}
|
||||
|
||||
let db = DB.get().unwrap();
|
||||
|
||||
let list_model = entities::prelude::FollowRelation::find()
|
||||
.filter(entities::follow_relation::Column::FolloweeId.eq(note.creator.to_string()))
|
||||
.all(db)
|
||||
.await?;
|
||||
|
||||
let mut list_url = Vec::new();
|
||||
|
||||
for model in list_model {
|
||||
let url = Url::parse(&model.follower_inbox.unwrap())?;
|
||||
list_url.push(url);
|
||||
}
|
||||
|
||||
array.append(&mut list_url);
|
||||
|
||||
array.sort();
|
||||
array.dedup();
|
||||
|
||||
let req_client = request_client();
|
||||
let model = prelude::User::find()
|
||||
.filter(user::Column::Id.eq(note.creator.as_str()))
|
||||
.one(db)
|
||||
.await?
|
||||
.unwrap();
|
||||
for inbox in array {
|
||||
let push = req_client
|
||||
.post(inbox.clone())
|
||||
.bearer_auth(AUTH.to_string())
|
||||
.json(&SortAlphabetically(&versia_post));
|
||||
warn!("{}", inbox.to_string());
|
||||
tokio::spawn(push_to_inbox(push));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn push_to_inbox(req: RequestBuilder) -> anyhow::Result<()> {
|
||||
let req_client = request_client();
|
||||
let response = req_client.execute(req.build()?).await?;
|
||||
|
||||
info!("{}", response.status());
|
||||
info!("{:?}", response.text().await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,203 +0,0 @@
|
|||
use activitypub_federation::{
|
||||
activity_sending::SendActivityTask,
|
||||
config::Data,
|
||||
fetch::object_id::ObjectId,
|
||||
protocol::context::WithContext,
|
||||
traits::{ActivityHandler, Actor, Object},
|
||||
};
|
||||
use activitystreams_kinds::activity::{AcceptType, FollowType};
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityOrSelect, EntityTrait, QueryFilter, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
database::StateHandle,
|
||||
entities::{
|
||||
follow_relation::{self, Entity},
|
||||
post, prelude, user,
|
||||
},
|
||||
error,
|
||||
utils::{generate_follow_accept_id, generate_random_object_id},
|
||||
versia::funcs::send_follow_accept_to_versia,
|
||||
DB,
|
||||
};
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct Follow {
|
||||
pub actor: ObjectId<user::Model>,
|
||||
pub object: ObjectId<user::Model>,
|
||||
#[serde(rename = "type")]
|
||||
pub kind: FollowType,
|
||||
pub id: Url,
|
||||
}
|
||||
|
||||
impl Follow {
|
||||
pub async fn send(
|
||||
local_user: ObjectId<user::Model>,
|
||||
followee: ObjectId<user::Model>,
|
||||
inbox: Url,
|
||||
data: &Data<StateHandle>,
|
||||
) -> Result<(), error::Error> {
|
||||
print!("Sending follow request to {}", &followee);
|
||||
let create = Follow {
|
||||
actor: local_user.clone(),
|
||||
object: followee.clone(),
|
||||
kind: FollowType::Follow,
|
||||
id: generate_random_object_id(data.domain())?,
|
||||
};
|
||||
let create_with_context = WithContext::new_default(create);
|
||||
let sends = SendActivityTask::prepare(
|
||||
&create_with_context,
|
||||
&data.local_user().await?,
|
||||
vec![inbox],
|
||||
data,
|
||||
)
|
||||
.await?;
|
||||
for send in sends {
|
||||
send.sign_and_send(data).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct Accept {
|
||||
actor: ObjectId<user::Model>,
|
||||
object: Follow,
|
||||
#[serde(rename = "type")]
|
||||
kind: AcceptType,
|
||||
id: Url,
|
||||
}
|
||||
|
||||
impl Accept {
|
||||
pub async fn send(
|
||||
follow_relation: follow_relation::Model,
|
||||
follow_req: Follow,
|
||||
inbox: Url,
|
||||
data: &Data<StateHandle>,
|
||||
) -> Result<(), error::Error> {
|
||||
print!("Sending accept to {}", &follow_relation.follower_id);
|
||||
let create = Accept {
|
||||
actor: follow_req.object.clone(),
|
||||
object: follow_req,
|
||||
kind: AcceptType::Accept,
|
||||
id: generate_follow_accept_id(data.domain(), follow_relation.id.to_string().as_str())?,
|
||||
};
|
||||
let create_with_context = WithContext::new_default(create);
|
||||
let sends = SendActivityTask::prepare(
|
||||
&create_with_context,
|
||||
&data.local_user().await?,
|
||||
vec![inbox],
|
||||
data,
|
||||
)
|
||||
.await?;
|
||||
for send in sends {
|
||||
send.sign_and_send(data).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActivityHandler for Follow {
|
||||
type DataType = StateHandle;
|
||||
type Error = crate::error::Error;
|
||||
|
||||
fn id(&self) -> &Url {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn actor(&self) -> &Url {
|
||||
self.actor.inner()
|
||||
}
|
||||
|
||||
async fn verify(&self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
//accept_follow(self, data).await?; TODO replace w/ versia forward
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ActivityHandler for Accept {
|
||||
type DataType = StateHandle;
|
||||
type Error = crate::error::Error;
|
||||
|
||||
fn id(&self) -> &Url {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn actor(&self) -> &Url {
|
||||
self.actor.inner()
|
||||
}
|
||||
|
||||
async fn verify(&self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
let user = self.actor.dereference(data).await?;
|
||||
let follower_id;
|
||||
let follower_bridge_url = self.object.actor.clone().to_string();
|
||||
let split = follower_bridge_url.split("/").collect::<Vec<&str>>();
|
||||
if split[split.len() - 1].is_empty() {
|
||||
follower_id = split[split.len() - 2];
|
||||
} else {
|
||||
follower_id = split[split.len() - 1];
|
||||
}
|
||||
let follower = prelude::User::find()
|
||||
.filter(user::Column::Id.eq(follower_id))
|
||||
.one(data.database_connection.as_ref())
|
||||
.await?;
|
||||
save_accept_follow(user, follower.unwrap(), self).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
async fn accept_follow(
|
||||
follow_req: Follow,
|
||||
data: &Data<StateHandle>,
|
||||
) -> Result<(), crate::error::Error> {
|
||||
let local_user = follow_req.actor.dereference(data).await?;
|
||||
let follower = follow_req.object.dereference(data).await?;
|
||||
let follow_relation = save_follow(local_user, follower.clone()).await?;
|
||||
Accept::send(follow_relation, follow_req, follower.inbox().clone(), data).await?;
|
||||
Ok(())
|
||||
}
|
||||
*/
|
||||
|
||||
async fn save_accept_follow(
|
||||
followee: user::Model,
|
||||
follower: user::Model,
|
||||
accept_activity: Accept,
|
||||
) -> Result<follow_relation::Model, crate::error::Error> {
|
||||
let db = DB.get().unwrap();
|
||||
let query = prelude::FollowRelation::find()
|
||||
.filter(follow_relation::Column::FollowerId.eq(follower.id.as_str()))
|
||||
.filter(follow_relation::Column::FolloweeId.eq(followee.id.as_str()))
|
||||
.one(db)
|
||||
.await?;
|
||||
if query.is_none() {
|
||||
return Err(crate::error::Error(anyhow::anyhow!("oopsie woopise")));
|
||||
}
|
||||
let versia_accept_id = uuid::Uuid::now_v7().to_string();
|
||||
// all values in the ActiveModel that are set, except the id, will be updated
|
||||
let active_query = follow_relation::ActiveModel {
|
||||
id: Set(query.unwrap().id),
|
||||
ap_accept_id: Set(Some(accept_activity.id.to_string())),
|
||||
ap_accept_json: Set(Some(serde_json::to_string(&accept_activity).unwrap())),
|
||||
accept_id: Set(Some(versia_accept_id)),
|
||||
..Default::default()
|
||||
};
|
||||
// modify db entry
|
||||
let res = prelude::FollowRelation::update(active_query);
|
||||
let model = res.exec(db).await?;
|
||||
|
||||
let _ = send_follow_accept_to_versia(model.clone()).await?;
|
||||
|
||||
Ok(model)
|
||||
}
|
||||
|
|
@ -1,2 +1 @@
|
|||
pub mod create_post;
|
||||
pub mod follow;
|
||||
|
|
|
|||
|
|
@ -1,22 +1,8 @@
|
|||
use super::entities::prelude::User;
|
||||
use crate::{entities::user, error::Error, objects::person::DbUser, LOCAL_USER_NAME};
|
||||
use crate::{error::Error, objects::person::DbUser};
|
||||
use anyhow::anyhow;
|
||||
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
env,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State {
|
||||
pub database_connection: Arc<DatabaseConnection>,
|
||||
}
|
||||
|
||||
pub type StateHandle = State;
|
||||
pub type DatabaseHandle = Arc<Database>;
|
||||
|
||||
/// Our "database" which contains all known users (local and federated)
|
||||
#[derive(Debug)]
|
||||
|
|
@ -24,25 +10,18 @@ pub struct Database {
|
|||
pub users: Mutex<Vec<DbUser>>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub async fn local_user(&self) -> Result<user::Model, Error> {
|
||||
let user = User::find()
|
||||
.filter(
|
||||
user::Column::Username
|
||||
.eq(env::var("LOCAL_USER_NAME").unwrap_or(LOCAL_USER_NAME.to_string())),
|
||||
)
|
||||
.one(self.database_connection.as_ref())
|
||||
.await?
|
||||
.unwrap();
|
||||
Ok(user.clone())
|
||||
impl Database {
|
||||
pub fn local_user(&self) -> DbUser {
|
||||
let lock = self.users.lock().unwrap();
|
||||
lock.first().unwrap().clone()
|
||||
}
|
||||
|
||||
pub async fn read_user(&self, name: &str) -> Result<user::Model, Error> {
|
||||
let db_user = self.local_user().await?;
|
||||
if name == db_user.username {
|
||||
pub fn read_user(&self, name: &str) -> Result<DbUser, Error> {
|
||||
let db_user = self.local_user();
|
||||
if name == db_user.name {
|
||||
Ok(db_user)
|
||||
} else {
|
||||
Err(anyhow!("Invalid user {name} // {0}", db_user.username).into())
|
||||
Err(anyhow!("Invalid user {name}").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "follow_relation")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
pub followee_id: String,
|
||||
pub follower_id: String,
|
||||
pub followee_host: Option<String>,
|
||||
pub follower_host: Option<String>,
|
||||
pub followee_inbox: Option<String>,
|
||||
pub follower_inbox: Option<String>,
|
||||
pub accept_id: Option<String>,
|
||||
pub ap_id: Option<String>,
|
||||
pub ap_accept_id: Option<String>,
|
||||
pub remote: bool,
|
||||
pub ap_json: String,
|
||||
pub ap_accept_json: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::FollowerId",
|
||||
to = "super::user::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
User2,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::FolloweeId",
|
||||
to = "super::user::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
User1,
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
|
||||
|
||||
pub mod prelude;
|
||||
|
||||
pub mod follow_relation;
|
||||
pub mod post;
|
||||
pub mod user;
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
|
||||
|
||||
use chrono::Utc;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "post")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
pub title: Option<String>,
|
||||
pub content: String,
|
||||
pub local: bool,
|
||||
#[sea_orm(column_type = "Timestamp")]
|
||||
pub created_at: chrono::DateTime<Utc>,
|
||||
#[sea_orm(column_type = "Timestamp")]
|
||||
pub updated_at: Option<chrono::DateTime<Utc>>,
|
||||
pub reblog_id: Option<String>,
|
||||
pub content_type: String,
|
||||
pub visibility: String,
|
||||
pub reply_id: Option<String>,
|
||||
pub quoting_id: Option<String>,
|
||||
pub sensitive: bool,
|
||||
pub spoiler_text: Option<String>,
|
||||
pub creator: String,
|
||||
pub url: String,
|
||||
pub ap_json: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "Entity",
|
||||
from = "Column::QuotingId",
|
||||
to = "Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
SelfRef3,
|
||||
#[sea_orm(
|
||||
belongs_to = "Entity",
|
||||
from = "Column::ReplyId",
|
||||
to = "Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
SelfRef2,
|
||||
#[sea_orm(
|
||||
belongs_to = "Entity",
|
||||
from = "Column::ReblogId",
|
||||
to = "Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
SelfRef1,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::Creator",
|
||||
to = "super::user::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
User,
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
|
||||
|
||||
pub use super::follow_relation::Entity as FollowRelation;
|
||||
pub use super::post::Entity as Post;
|
||||
pub use super::user::Entity as User;
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
|
||||
|
||||
use chrono::Utc;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "user")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
pub username: String,
|
||||
pub name: String,
|
||||
pub summary: Option<String>,
|
||||
pub url: String,
|
||||
pub public_key: String,
|
||||
pub private_key: Option<String>,
|
||||
#[sea_orm(column_type = "Timestamp")]
|
||||
pub last_refreshed_at: chrono::DateTime<Utc>,
|
||||
pub local: bool,
|
||||
pub follower_count: i32,
|
||||
pub following_count: i32,
|
||||
#[sea_orm(column_type = "Timestamp")]
|
||||
pub created_at: chrono::DateTime<Utc>,
|
||||
#[sea_orm(column_type = "Timestamp")]
|
||||
pub updated_at: Option<chrono::DateTime<Utc>>,
|
||||
pub following: Option<String>,
|
||||
pub followers: Option<String>,
|
||||
pub inbox: String,
|
||||
pub ap_json: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::post::Entity")]
|
||||
Post,
|
||||
}
|
||||
|
||||
impl Related<super::post::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Post.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
50
src/http.rs
50
src/http.rs
|
|
@ -1,33 +1,22 @@
|
|||
use crate::{
|
||||
database::StateHandle,
|
||||
entities::user,
|
||||
database::DatabaseHandle,
|
||||
error::Error,
|
||||
objects::person::{DbUser, PersonAcceptedActivities},
|
||||
utils::generate_user_id,
|
||||
versia::{
|
||||
self,
|
||||
conversion::{db_user_from_url, local_db_user_from_name, receive_versia_note},
|
||||
},
|
||||
API_DOMAIN, LYSAND_DOMAIN,
|
||||
};
|
||||
use activitypub_federation::{
|
||||
actix_web::{inbox::receive_activity, signing_actor},
|
||||
config::{Data, FederationConfig, FederationMiddleware},
|
||||
fetch::webfinger::{build_webfinger_response, extract_webfinger_name, WebFingerError},
|
||||
fetch::webfinger::{build_webfinger_response, extract_webfinger_name},
|
||||
protocol::context::WithContext,
|
||||
traits::{Actor, Object},
|
||||
FEDERATION_CONTENT_TYPE,
|
||||
};
|
||||
use actix_web::{web, web::Bytes, App, HttpRequest, HttpResponse, HttpServer};
|
||||
use anyhow::anyhow;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use tracing::info;
|
||||
use url::Url;
|
||||
use webfinger::resolve;
|
||||
|
||||
pub fn listen(config: &FederationConfig<StateHandle>) -> Result<(), Error> {
|
||||
pub fn listen(config: &FederationConfig<DatabaseHandle>) -> Result<(), Error> {
|
||||
let hostname = config.domain();
|
||||
info!("Listening with actix-web on {hostname}");
|
||||
let config = config.clone();
|
||||
|
|
@ -45,15 +34,6 @@ pub fn listen(config: &FederationConfig<StateHandle>) -> Result<(), Error> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn versia_inbox(
|
||||
note: web::Json<versia::objects::Note>,
|
||||
id: web::Path<String>,
|
||||
data: Data<StateHandle>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
tokio::spawn(receive_versia_note(note.into_inner(), id.into_inner()));
|
||||
Ok(HttpResponse::Created().finish())
|
||||
}
|
||||
|
||||
/// Handles requests to fetch system user json over HTTP
|
||||
/*pub async fn http_get_system_user(data: Data<DatabaseHandle>) -> Result<HttpResponse, Error> {
|
||||
let json_user = data.system_user.clone().into_json(&data).await?;
|
||||
|
|
@ -66,7 +46,7 @@ pub fn versia_inbox(
|
|||
pub async fn http_get_user(
|
||||
request: HttpRequest,
|
||||
user_name: web::Path<String>,
|
||||
data: Data<StateHandle>,
|
||||
data: Data<DatabaseHandle>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
//let signed_by = signing_actor::<DbUser>(&request, None, &data).await?;
|
||||
// here, checks can be made on the actor or the domain to which
|
||||
|
|
@ -76,8 +56,8 @@ pub async fn http_get_user(
|
|||
// signed_by.id()
|
||||
//);
|
||||
|
||||
let db_user = data.local_user().await?;
|
||||
if user_name.into_inner() == db_user.username {
|
||||
let db_user = data.local_user();
|
||||
if user_name.into_inner() == db_user.name {
|
||||
let json_user = db_user.into_json(&data).await?;
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type(FEDERATION_CONTENT_TYPE)
|
||||
|
|
@ -91,9 +71,9 @@ pub async fn http_get_user(
|
|||
pub async fn http_post_user_inbox(
|
||||
request: HttpRequest,
|
||||
body: Bytes,
|
||||
data: Data<StateHandle>,
|
||||
data: Data<DatabaseHandle>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
receive_activity::<WithContext<PersonAcceptedActivities>, user::Model, StateHandle>(
|
||||
receive_activity::<WithContext<PersonAcceptedActivities>, DbUser, DatabaseHandle>(
|
||||
request, body, &data,
|
||||
)
|
||||
.await
|
||||
|
|
@ -106,18 +86,12 @@ pub struct WebfingerQuery {
|
|||
|
||||
pub async fn webfinger(
|
||||
query: web::Query<WebfingerQuery>,
|
||||
data: Data<StateHandle>,
|
||||
data: Data<DatabaseHandle>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
static WEBFINGER_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^acct:([\p{L}0-9_\.\-]+)@(.*)$").expect("compile regex"));
|
||||
let captures = WEBFINGER_REGEX
|
||||
.captures(&query.resource)
|
||||
.ok_or(WebFingerError::WrongFormat)?;
|
||||
let account_name = captures.get(1).ok_or(WebFingerError::WrongFormat)?;
|
||||
let name = account_name.as_str();
|
||||
let user = local_db_user_from_name(name.to_string()).await?;
|
||||
let name = extract_webfinger_name(&query.resource, &data)?;
|
||||
let db_user = data.read_user(name)?;
|
||||
Ok(HttpResponse::Ok().json(build_webfinger_response(
|
||||
query.resource.clone(),
|
||||
generate_user_id(&API_DOMAIN, &user.id)?,
|
||||
db_user.ap_id.into_inner(),
|
||||
)))
|
||||
}
|
||||
|
|
|
|||
283
src/main.rs
283
src/main.rs
|
|
@ -1,55 +1,30 @@
|
|||
use activitypub_federation::{
|
||||
config::{Data, FederationConfig, FederationMiddleware},
|
||||
fetch::{object_id::ObjectId, webfinger::webfinger_resolve_actor},
|
||||
http_signatures::generate_actor_keypair,
|
||||
traits::Actor,
|
||||
};
|
||||
use activitystreams_kinds::public;
|
||||
use actix_web::{
|
||||
get, http::KeepAlive, middleware, post, web, App, Error, HttpResponse, HttpServer,
|
||||
};
|
||||
use activitypub_federation::config::{FederationConfig, FederationMiddleware};
|
||||
use actix_web::{get, http::KeepAlive, middleware, web, App, Error, HttpResponse, HttpServer};
|
||||
use actix_web_prom::PrometheusMetricsBuilder;
|
||||
use async_once::AsyncOnce;
|
||||
use chrono::{DateTime, Utc};
|
||||
use clap::Parser;
|
||||
use database::Database;
|
||||
use entities::post;
|
||||
use http::{http_get_user, http_post_user_inbox, webfinger};
|
||||
use objects::person::{DbUser, Person};
|
||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, Set};
|
||||
use objects::person::DbUser;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env,
|
||||
net::ToSocketAddrs,
|
||||
sync::{Arc, Mutex, OnceLock},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tokio::signal;
|
||||
use tracing::{info, instrument::WithSubscriber};
|
||||
use url::Url;
|
||||
use utils::generate_object_id;
|
||||
use uuid::Uuid;
|
||||
use versia::http::{
|
||||
create_activity, fetch_post, fetch_user, fetch_versia_post, query_post, versia_inbox,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
activities::create_post::CreatePost,
|
||||
database::{Config, State},
|
||||
objects::post::{Mention, Note},
|
||||
};
|
||||
use crate::{activities::follow::Follow, entities::user};
|
||||
use dotenv::dotenv;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
mod activities;
|
||||
mod database;
|
||||
mod entities;
|
||||
mod error;
|
||||
mod http;
|
||||
mod objects;
|
||||
mod utils;
|
||||
mod versia;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct State {
|
||||
database: Arc<Database>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Response {
|
||||
|
|
@ -74,214 +49,52 @@ async fn index(_: web::Data<State>) -> actix_web::Result<HttpResponse, Error> {
|
|||
Ok(HttpResponse::Ok().json(Response { health: true }))
|
||||
}
|
||||
|
||||
#[get("/test/postmanually/{user}/{post}")]
|
||||
async fn post_manually(
|
||||
path: web::Path<(String, String)>,
|
||||
state: web::Data<State>,
|
||||
) -> actix_web::Result<HttpResponse, error::Error> {
|
||||
let local_user = state.local_user().await?;
|
||||
let data = FEDERATION_CONFIG.get().unwrap();
|
||||
let target =
|
||||
webfinger_resolve_actor::<State, user::Model>(path.0.as_str(), &data.to_request_data())
|
||||
.await?;
|
||||
|
||||
let mention = Mention {
|
||||
href: Url::parse(&target.id)?,
|
||||
kind: Default::default(),
|
||||
};
|
||||
// TODO change
|
||||
let uuid = uuid::Uuid::now_v7().to_string();
|
||||
let id: ObjectId<post::Model> = generate_object_id(data.domain(), &uuid)?.into();
|
||||
let note = Note {
|
||||
kind: Default::default(),
|
||||
id: id.clone(),
|
||||
sensitive: Some(false),
|
||||
attributed_to: Url::parse(&local_user.id).unwrap().into(),
|
||||
to: vec![public(), mention.href.clone()],
|
||||
content: format!("{} {}", path.1, target.name),
|
||||
tag: vec![mention],
|
||||
in_reply_to: None,
|
||||
cc: vec![].into(),
|
||||
};
|
||||
|
||||
let post = entities::post::ActiveModel {
|
||||
id: Set(uuid),
|
||||
creator: Set(local_user.id.clone()),
|
||||
content: Set(note.content.clone()),
|
||||
sensitive: Set(false),
|
||||
created_at: Set(Utc::now()),
|
||||
local: Set(true),
|
||||
updated_at: Set(Some(Utc::now())),
|
||||
content_type: Set("Note".to_string()),
|
||||
visibility: Set("public".to_string()),
|
||||
url: Set(id.to_string()),
|
||||
ap_json: Set(Some(serde_json::to_string(¬e).unwrap())),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let post = post.insert(DB.get().unwrap()).await?;
|
||||
|
||||
CreatePost::send(
|
||||
note,
|
||||
post,
|
||||
target.shared_inbox_or_inbox(),
|
||||
&data.to_request_data(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(Response { health: true }))
|
||||
}
|
||||
|
||||
#[get("/favicon")]
|
||||
async fn favicon() -> actix_web::Result<actix_files::NamedFile> {
|
||||
Ok(actix_files::NamedFile::open("static/favicon.ico")?)
|
||||
}
|
||||
|
||||
#[get("/test/follow/{user}")]
|
||||
async fn follow_manually(
|
||||
path: web::Path<String>,
|
||||
state: web::Data<State>,
|
||||
) -> actix_web::Result<HttpResponse, error::Error> {
|
||||
let local_user = state.local_user().await?;
|
||||
let data = FEDERATION_CONFIG.get().unwrap();
|
||||
let followee =
|
||||
webfinger_resolve_actor::<State, user::Model>(path.as_str(), &data.to_request_data())
|
||||
.await?;
|
||||
|
||||
let followee_object: ObjectId<user::Model> = Url::parse(&followee.url)?.into();
|
||||
let localuser_object: ObjectId<user::Model> = Url::parse(&local_user.url)?.into();
|
||||
|
||||
Follow::send(
|
||||
localuser_object,
|
||||
followee_object,
|
||||
followee.shared_inbox_or_inbox(),
|
||||
&data.to_request_data(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(Response { health: true }))
|
||||
}
|
||||
|
||||
const DOMAIN_DEF: &str = "versia.social";
|
||||
const LOCAL_USER_NAME: &str = "apservice";
|
||||
|
||||
lazy_static! {
|
||||
static ref SERVER_URL: String = env::var("LISTEN").unwrap_or("0.0.0.0:8080".to_string());
|
||||
static ref DATABASE_URL: String = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||
static ref USERNAME: String =
|
||||
env::var("LOCAL_USER_NAME").unwrap_or(LOCAL_USER_NAME.to_string());
|
||||
static ref API_DOMAIN: String = env::var("API_DOMAIN").expect("not set API_DOMAIN");
|
||||
static ref AUTH: String = env::var("AUTH").expect("not set AUTH");
|
||||
static ref LYSAND_DOMAIN: String = env::var("LYSAND_DOMAIN").expect("not set LYSAND_DOMAIN");
|
||||
static ref FEDERATED_DOMAIN: String =
|
||||
env::var("FEDERATED_DOMAIN").unwrap_or(API_DOMAIN.to_string());
|
||||
}
|
||||
|
||||
static DB: OnceLock<DatabaseConnection> = OnceLock::new();
|
||||
static FEDERATION_CONFIG: OnceLock<FederationConfig<State>> = OnceLock::new();
|
||||
const DOMAIN: &str = "example.com";
|
||||
const LOCAL_USER_NAME: &str = "example";
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> actix_web::Result<(), anyhow::Error> {
|
||||
dotenv().ok();
|
||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||
|
||||
let uuid = Uuid::parse_str("019116ea-3bf6-7ba3-b437-2cd7aaf40f80")?;
|
||||
let server_url = env::var("LISTEN").unwrap_or("127.0.0.1:8080".to_string());
|
||||
|
||||
let ap_id = Url::parse(&format!(
|
||||
"https://{}/apbridge/user/{}",
|
||||
API_DOMAIN.to_string(),
|
||||
&uuid.to_string()
|
||||
))?;
|
||||
let inbox = Url::parse(&format!(
|
||||
"https://{}/{}/inbox",
|
||||
API_DOMAIN.to_string(),
|
||||
&USERNAME.to_string()
|
||||
))?;
|
||||
let keypair = generate_actor_keypair()?;
|
||||
let local_user = DbUser::new(
|
||||
env::var("FEDERATED_DOMAIN")
|
||||
.unwrap_or(DOMAIN.to_string())
|
||||
.as_str(),
|
||||
env::var("LOCAL_USER_NAME")
|
||||
.unwrap_or(LOCAL_USER_NAME.to_string())
|
||||
.as_str(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let ap_json = Person {
|
||||
id: ap_id.clone().into(),
|
||||
preferred_username: USERNAME.to_string(),
|
||||
name: "Test account <3".to_string(),
|
||||
inbox: inbox.clone(),
|
||||
public_key: activitypub_federation::protocol::public_key::PublicKey {
|
||||
owner: ap_id.clone(),
|
||||
public_key_pem: keypair.public_key.clone(),
|
||||
id: format!("{}#main-key", ap_id.clone()),
|
||||
},
|
||||
summary: Some("Test account <3".to_string()),
|
||||
url: ap_id.clone(),
|
||||
kind: Default::default(),
|
||||
indexable: Some(false),
|
||||
discoverable: Some(false),
|
||||
icon: None,
|
||||
image: None,
|
||||
attachment: None,
|
||||
tag: None,
|
||||
endpoints: None,
|
||||
followers: None,
|
||||
following: None,
|
||||
featured: None,
|
||||
outbox: None,
|
||||
also_known_as: None,
|
||||
featured_tags: None,
|
||||
manually_approves_followers: Some(false),
|
||||
};
|
||||
let database = Arc::new(Database {
|
||||
users: Mutex::new(vec![local_user]),
|
||||
});
|
||||
|
||||
let user = entities::user::ActiveModel {
|
||||
id: Set(uuid.to_string()),
|
||||
username: Set(USERNAME.to_string()),
|
||||
name: Set("Test account <3".to_string()),
|
||||
inbox: Set(inbox.to_string()),
|
||||
public_key: Set(keypair.public_key.clone()),
|
||||
private_key: Set(Some(keypair.private_key.clone())),
|
||||
last_refreshed_at: Set(Utc::now()),
|
||||
follower_count: Set(0),
|
||||
following_count: Set(0),
|
||||
url: Set(ap_id.to_string()),
|
||||
local: Set(true),
|
||||
created_at: Set(Utc::now()),
|
||||
ap_json: Set(Some(serde_json::to_string(&ap_json).unwrap())),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let db = sea_orm::Database::connect(DATABASE_URL.to_string()).await?;
|
||||
|
||||
info!("Connected to database: {:?}", db);
|
||||
|
||||
DB.set(db)
|
||||
.expect("We were not able to save the DB conn into memory");
|
||||
|
||||
let db = DB.get().unwrap();
|
||||
|
||||
let user = user.insert(db).await;
|
||||
|
||||
if let Err(err) = user {
|
||||
eprintln!("Error inserting user: {:?}", err);
|
||||
} else {
|
||||
info!("User inserted: {:?}", user.unwrap());
|
||||
}
|
||||
|
||||
let state: State = State {
|
||||
database_connection: Arc::new(db.clone()),
|
||||
};
|
||||
let state = State { database };
|
||||
|
||||
let data = FederationConfig::builder()
|
||||
.domain(FEDERATED_DOMAIN.to_string())
|
||||
.app_data(state.clone())
|
||||
.http_signature_compat(true)
|
||||
.signed_fetch_actor(&state.local_user().await.unwrap())
|
||||
.domain(env::var("FEDERATED_DOMAIN").expect("FEDERATED_DOMAIN must be set"))
|
||||
.app_data(state.clone().database)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let _ = FEDERATION_CONFIG.set(data.clone());
|
||||
|
||||
let mut labels = HashMap::new();
|
||||
labels.insert("domain".to_string(), FEDERATED_DOMAIN.to_string());
|
||||
labels.insert("name".to_string(), USERNAME.to_string());
|
||||
labels.insert("api_domain".to_string(), API_DOMAIN.to_string());
|
||||
labels.insert(
|
||||
"domain".to_string(),
|
||||
env::var("FEDERATED_DOMAIN")
|
||||
.expect("FEDERATED_DOMAIN must be set")
|
||||
.to_string(),
|
||||
);
|
||||
labels.insert(
|
||||
"name".to_string(),
|
||||
env::var("LOCAL_USER_NAME")
|
||||
.expect("LOCAL_USER_NAME must be set")
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
let prometheus = PrometheusMetricsBuilder::new("activitypub_bridge")
|
||||
let prometheus = PrometheusMetricsBuilder::new("api")
|
||||
.endpoint("/metrics")
|
||||
.const_labels(labels)
|
||||
.build()
|
||||
|
|
@ -293,24 +106,12 @@ async fn main() -> actix_web::Result<(), anyhow::Error> {
|
|||
.wrap(middleware::Logger::default()) // enable logger
|
||||
.wrap(prometheus.clone())
|
||||
.wrap(FederationMiddleware::new(data.clone()))
|
||||
.service(post_manually)
|
||||
.service(versia_inbox)
|
||||
.service(follow_manually)
|
||||
.route("/{user}", web::get().to(http_get_user))
|
||||
.route("/{user}/inbox", web::post().to(http_post_user_inbox))
|
||||
.route(
|
||||
"/apbridge/{user}/inbox",
|
||||
web::post().to(http_post_user_inbox),
|
||||
)
|
||||
.route("/.well-known/webfinger", web::get().to(webfinger))
|
||||
.service(index)
|
||||
.service(fetch_post)
|
||||
.service(fetch_user)
|
||||
.service(create_activity)
|
||||
.service(query_post)
|
||||
.service(fetch_versia_post)
|
||||
})
|
||||
.bind(SERVER_URL.to_string())?
|
||||
.bind(&server_url)?
|
||||
.workers(num_cpus::get())
|
||||
.shutdown_timeout(20)
|
||||
.keep_alive(KeepAlive::Os)
|
||||
|
|
@ -326,7 +127,5 @@ async fn main() -> actix_web::Result<(), anyhow::Error> {
|
|||
}
|
||||
}
|
||||
|
||||
info!("Main thread shutdown..");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,4 @@
|
|||
use crate::{
|
||||
activities::{
|
||||
create_post::CreatePost,
|
||||
follow::{self, Follow},
|
||||
},
|
||||
database::{State, StateHandle},
|
||||
entities::{self, user},
|
||||
error::Error,
|
||||
API_DOMAIN,
|
||||
};
|
||||
use crate::{activities::create_post::CreatePost, database::DatabaseHandle, error::Error};
|
||||
use activitypub_federation::{
|
||||
config::Data,
|
||||
fetch::object_id::ObjectId,
|
||||
|
|
@ -16,20 +7,15 @@ use activitypub_federation::{
|
|||
protocol::{public_key::PublicKey, verification::verify_domains_match},
|
||||
traits::{ActivityHandler, Actor, Object},
|
||||
};
|
||||
use actix_web::http::header::Accept;
|
||||
use chrono::{prelude, DateTime, Utc};
|
||||
use entities::prelude::User;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Debug;
|
||||
use tracing::info;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DbUser {
|
||||
pub name: String,
|
||||
pub ap_id: ObjectId<user::Model>,
|
||||
pub ap_id: ObjectId<DbUser>,
|
||||
pub inbox: Url,
|
||||
// exists for all users (necessary to verify http signatures)
|
||||
pub public_key: String,
|
||||
|
|
@ -46,8 +32,6 @@ pub struct DbUser {
|
|||
#[enum_delegate::implement(ActivityHandler)]
|
||||
pub enum PersonAcceptedActivities {
|
||||
CreateNote(CreatePost),
|
||||
Follow(Follow),
|
||||
Accept(follow::Accept),
|
||||
}
|
||||
|
||||
impl DbUser {
|
||||
|
|
@ -72,85 +56,16 @@ impl DbUser {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Person {
|
||||
#[serde(rename = "type")]
|
||||
pub kind: PersonType,
|
||||
pub preferred_username: String,
|
||||
pub name: String,
|
||||
pub summary: Option<String>,
|
||||
pub url: Url,
|
||||
pub id: ObjectId<user::Model>,
|
||||
pub inbox: Url,
|
||||
pub public_key: PublicKey,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub indexable: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub discoverable: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub manually_approves_followers: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub followers: Option<Url>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub following: Option<Url>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub featured: Option<Url>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub endpoints: Option<EndpointType>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub outbox: Option<Url>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub featured_tags: Option<Url>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tag: Option<Vec<TagType>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon: Option<IconType>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image: Option<IconType>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub attachment: Option<Vec<AttachmentType>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub also_known_as: Option<Vec<Url>>,
|
||||
}
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct TagType {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<Url>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub href: Option<Url>,
|
||||
pub name: String,
|
||||
#[serde(rename = "type")]
|
||||
pub type_: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub updated: Option<DateTime<Utc>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon: Option<IconType>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EndpointType {
|
||||
pub shared_inbox: Url,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IconType {
|
||||
#[serde(rename = "type")]
|
||||
pub type_: String, //Always "Image"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub media_type: Option<String>,
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct AttachmentType {
|
||||
#[serde(rename = "type")]
|
||||
pub type_: String, //Always "PropertyValue"
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
kind: PersonType,
|
||||
preferred_username: String,
|
||||
id: ObjectId<DbUser>,
|
||||
inbox: Url,
|
||||
public_key: PublicKey,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Object for user::Model {
|
||||
type DataType = StateHandle;
|
||||
impl Object for DbUser {
|
||||
type DataType = DatabaseHandle;
|
||||
type Kind = Person;
|
||||
type Error = Error;
|
||||
|
||||
|
|
@ -162,21 +77,22 @@ impl Object for user::Model {
|
|||
object_id: Url,
|
||||
data: &Data<Self::DataType>,
|
||||
) -> Result<Option<Self>, Self::Error> {
|
||||
println!("!!!!!!!!Reading user from id!!!!!!!!!!!: {}", object_id);
|
||||
let res = entities::prelude::User::find()
|
||||
.filter(entities::user::Column::Url.eq(object_id.to_string()))
|
||||
.one(data.database_connection.as_ref())
|
||||
.await?;
|
||||
println!(
|
||||
"!!!!!!!!Reading user from id!!!!!!!!!!!: {}",
|
||||
res.clone().is_some()
|
||||
);
|
||||
let users = data.users.lock().unwrap();
|
||||
let res = users
|
||||
.clone()
|
||||
.into_iter()
|
||||
.find(|u| u.ap_id.inner() == &object_id);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
|
||||
let serialized = serde_json::from_str(self.ap_json.as_ref().unwrap().as_str())?;
|
||||
Ok(serialized)
|
||||
Ok(Person {
|
||||
preferred_username: self.name.clone(),
|
||||
kind: Default::default(),
|
||||
id: self.ap_id.clone(),
|
||||
inbox: self.inbox.clone(),
|
||||
public_key: self.public_key(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn verify(
|
||||
|
|
@ -188,50 +104,26 @@ impl Object for user::Model {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> {
|
||||
let query = User::find()
|
||||
.filter(user::Column::Url.eq(json.id.inner().as_str()))
|
||||
.one(data.database_connection.as_ref())
|
||||
.await?;
|
||||
if let Some(user) = query {
|
||||
return Ok(user);
|
||||
}
|
||||
let copied_json = json.clone();
|
||||
let model = user::ActiveModel {
|
||||
id: Set(Uuid::now_v7().to_string()),
|
||||
username: Set(json.preferred_username),
|
||||
name: Set(json.name),
|
||||
inbox: Set(json.inbox.to_string()),
|
||||
public_key: Set(json.public_key.public_key_pem),
|
||||
local: Set(false),
|
||||
summary: Set(json.summary),
|
||||
url: Set(json.id.to_string()),
|
||||
follower_count: Set(0),
|
||||
following_count: Set(0),
|
||||
created_at: Set(Utc::now()),
|
||||
last_refreshed_at: Set(Utc::now()),
|
||||
ap_json: Set(Some(serde_json::to_string(&copied_json).unwrap())),
|
||||
..Default::default()
|
||||
};
|
||||
let model = model.insert(data.database_connection.as_ref()).await;
|
||||
if let Err(err) = model {
|
||||
eprintln!("Error inserting user: {:?}", err);
|
||||
Err(err.into())
|
||||
} else {
|
||||
info!("User inserted: {:?}", model.as_ref().unwrap());
|
||||
Ok(model.unwrap())
|
||||
}
|
||||
async fn from_json(
|
||||
json: Self::Kind,
|
||||
_data: &Data<Self::DataType>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(DbUser {
|
||||
name: json.preferred_username,
|
||||
ap_id: json.id,
|
||||
inbox: json.inbox,
|
||||
public_key: json.public_key.public_key_pem,
|
||||
private_key: None,
|
||||
last_refreshed_at: Utc::now(),
|
||||
followers: vec![],
|
||||
local: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for user::Model {
|
||||
impl Actor for DbUser {
|
||||
fn id(&self) -> Url {
|
||||
Url::parse(&format!(
|
||||
"https://{}/apbridge/user/{}",
|
||||
API_DOMAIN.to_string(),
|
||||
&self.id
|
||||
))
|
||||
.unwrap()
|
||||
self.ap_id.inner().clone()
|
||||
}
|
||||
|
||||
fn public_key_pem(&self) -> &str {
|
||||
|
|
@ -243,11 +135,6 @@ impl Actor for user::Model {
|
|||
}
|
||||
|
||||
fn inbox(&self) -> Url {
|
||||
Url::parse(&self.inbox).unwrap()
|
||||
}
|
||||
|
||||
//TODO: Differenciate shared inbox
|
||||
fn shared_inbox(&self) -> Option<Url> {
|
||||
None
|
||||
self.inbox.clone()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
use crate::{
|
||||
activities::create_post::CreatePost,
|
||||
database::StateHandle,
|
||||
entities::{post, prelude::Post, user},
|
||||
error::Error,
|
||||
objects::person::DbUser,
|
||||
utils::generate_object_id,
|
||||
versia::conversion::db_user_from_url,
|
||||
activities::create_post::CreatePost, database::DatabaseHandle, error::Error,
|
||||
objects::person::DbUser, utils::generate_object_id,
|
||||
};
|
||||
use activitypub_federation::{
|
||||
config::Data,
|
||||
|
|
@ -15,43 +10,29 @@ use activitypub_federation::{
|
|||
traits::{Actor, Object},
|
||||
};
|
||||
use activitystreams_kinds::link::MentionType;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::info;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DbPost {
|
||||
pub text: String,
|
||||
pub ap_id: ObjectId<post::Model>,
|
||||
pub creator: ObjectId<user::Model>,
|
||||
pub ap_id: ObjectId<DbPost>,
|
||||
pub creator: ObjectId<DbUser>,
|
||||
pub local: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Note {
|
||||
#[serde(rename = "type")]
|
||||
pub(crate) kind: NoteType,
|
||||
pub(crate) id: ObjectId<post::Model>,
|
||||
pub(crate) attributed_to: ObjectId<user::Model>,
|
||||
kind: NoteType,
|
||||
id: ObjectId<DbPost>,
|
||||
pub(crate) attributed_to: ObjectId<DbUser>,
|
||||
#[serde(deserialize_with = "deserialize_one_or_many")]
|
||||
pub(crate) to: Vec<Url>,
|
||||
pub(crate) content: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) in_reply_to: Option<ObjectId<post::Model>>,
|
||||
pub(crate) tag: Vec<Mention>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) sensitive: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) cc: Option<Vec<Url>>,
|
||||
}
|
||||
|
||||
impl Note {
|
||||
pub fn from_db(post: &post::Model) -> Self {
|
||||
serde_json::from_str(&post.ap_json.as_ref().unwrap()).unwrap()
|
||||
}
|
||||
content: String,
|
||||
in_reply_to: Option<ObjectId<DbPost>>,
|
||||
tag: Vec<Mention>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
|
|
@ -62,48 +43,20 @@ pub struct Mention {
|
|||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Object for post::Model {
|
||||
type DataType = StateHandle;
|
||||
impl Object for DbPost {
|
||||
type DataType = DatabaseHandle;
|
||||
type Kind = Note;
|
||||
type Error = Error;
|
||||
|
||||
async fn read_from_id(
|
||||
object_id: Url,
|
||||
data: &Data<Self::DataType>,
|
||||
_object_id: Url,
|
||||
_data: &Data<Self::DataType>,
|
||||
) -> Result<Option<Self>, Self::Error> {
|
||||
let post = crate::entities::prelude::Post::find()
|
||||
.filter(post::Column::Id.eq(object_id.to_string()))
|
||||
.one(data.app_data().database_connection.clone().as_ref())
|
||||
.await;
|
||||
Ok(post.unwrap())
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
|
||||
let creator = db_user_from_url(Url::parse(self.creator.as_str()).unwrap()).await?;
|
||||
let to = match self.visibility.as_str() {
|
||||
"public" => vec![
|
||||
public(),
|
||||
Url::parse(creator.followers.unwrap().as_str()).unwrap(),
|
||||
],
|
||||
"followers" => vec![Url::parse(creator.followers.unwrap().as_str()).unwrap()],
|
||||
"direct" => vec![], //TODO: implement this
|
||||
"unlisted" => vec![
|
||||
Url::parse(creator.followers.unwrap().as_str()).unwrap(),
|
||||
public(),
|
||||
],
|
||||
_ => vec![public()],
|
||||
};
|
||||
Ok(Note {
|
||||
kind: Default::default(),
|
||||
id: Url::parse(self.url.as_str()).unwrap().into(),
|
||||
attributed_to: Url::parse(self.creator.as_str()).unwrap().into(),
|
||||
to: to.clone(),
|
||||
content: self.content,
|
||||
in_reply_to: None,
|
||||
tag: vec![],
|
||||
sensitive: Some(self.sensitive),
|
||||
cc: Some(to),
|
||||
})
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn verify(
|
||||
|
|
@ -120,37 +73,29 @@ impl Object for post::Model {
|
|||
"Received post with content {} and id {}",
|
||||
&json.content, &json.id
|
||||
);
|
||||
let query = Post::find()
|
||||
.filter(post::Column::Url.eq(json.id.inner().as_str()))
|
||||
.one(data.database_connection.as_ref())
|
||||
.await?;
|
||||
if let Some(post) = query {
|
||||
return Ok(post);
|
||||
}
|
||||
let creator = json.attributed_to.dereference(data).await?;
|
||||
let post: post::ActiveModel = post::ActiveModel {
|
||||
content: Set(json.content.clone()),
|
||||
id: Set(Uuid::now_v7().to_string()),
|
||||
creator: Set(creator.id.to_string()),
|
||||
created_at: Set(chrono::Utc::now()), //TODO: make this use the real timestamp
|
||||
content_type: Set("text/plain".to_string()), // TODO: make this use the real content type
|
||||
local: Set(false),
|
||||
visibility: Set("public".to_string()), // TODO: make this use the real visibility
|
||||
sensitive: Set(json.sensitive.clone().unwrap_or_default()),
|
||||
url: Set(json.id.clone().to_string()),
|
||||
ap_json: Set(Some(serde_json::to_string(&json).unwrap())),
|
||||
..Default::default()
|
||||
let post = DbPost {
|
||||
text: json.content,
|
||||
ap_id: json.id.clone(),
|
||||
creator: json.attributed_to.clone(),
|
||||
local: false,
|
||||
};
|
||||
let post = post
|
||||
.insert(data.app_data().database_connection.clone().as_ref())
|
||||
.await;
|
||||
|
||||
if let Err(err) = post {
|
||||
eprintln!("Error inserting post: {:?}", err);
|
||||
return Err(err.into());
|
||||
}
|
||||
info!("Post inserted: {:?}", post.as_ref().unwrap());
|
||||
let mention = Mention {
|
||||
href: creator.ap_id.clone().into_inner(),
|
||||
kind: Default::default(),
|
||||
};
|
||||
let note = Note {
|
||||
kind: Default::default(),
|
||||
id: generate_object_id(data.domain())?.into(),
|
||||
attributed_to: data.local_user().ap_id,
|
||||
to: vec![public()],
|
||||
content: format!("Hello {}", creator.name),
|
||||
in_reply_to: Some(json.id.clone()),
|
||||
tag: vec![mention],
|
||||
};
|
||||
CreatePost::send(note, creator.shared_inbox_or_inbox(), data).await?;
|
||||
|
||||
Ok(post.unwrap())
|
||||
Ok(post)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
62
src/utils.rs
62
src/utils.rs
|
|
@ -1,55 +1,13 @@
|
|||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use url::{ParseError, Url};
|
||||
|
||||
pub fn generate_object_id(domain: &str, uuid: &str) -> Result<Url, ParseError> {
|
||||
Url::parse(&format!("https://{}/apbridge/object/{}", domain, uuid))
|
||||
}
|
||||
|
||||
pub fn generate_user_id(domain: &str, uuid: &str) -> Result<Url, ParseError> {
|
||||
Url::parse(&format!("https://{}/apbridge/user/{}", domain, uuid))
|
||||
}
|
||||
|
||||
pub fn generate_random_object_id(domain: &str) -> Result<Url, ParseError> {
|
||||
let id: String = uuid::Uuid::new_v4().to_string();
|
||||
generate_object_id(domain, &id)
|
||||
}
|
||||
|
||||
/// Generate a follow accept id
|
||||
pub fn generate_follow_accept_id(domain: &str, db_id: &str) -> Result<Url, ParseError> {
|
||||
Url::parse(&format!("https://{}/apbridge/follow/{}", domain, db_id))
|
||||
}
|
||||
|
||||
pub fn generate_follow_req_id(domain: &str, db_id: &str) -> Result<Url, ParseError> {
|
||||
Url::parse(&format!("https://{}/apbridge/followreq/{}", domain, db_id))
|
||||
}
|
||||
|
||||
pub fn generate_versia_post_url(domain: &str, db_id: &str) -> Result<Url, ParseError> {
|
||||
Url::parse(&format!(
|
||||
"https://{}/apbridge/versia/object/{}",
|
||||
domain, db_id
|
||||
))
|
||||
}
|
||||
|
||||
// TODO for later aprl: needs to be base64url!!!
|
||||
pub fn generate_create_id(
|
||||
domain: &str,
|
||||
create_db_id: &str,
|
||||
basesixfour_url: &str,
|
||||
) -> Result<Url, ParseError> {
|
||||
Url::parse(&format!(
|
||||
"https://{}/apbridge/create/{}/{}",
|
||||
domain, create_db_id, basesixfour_url
|
||||
))
|
||||
}
|
||||
|
||||
pub fn generate_random_create_id(domain: &str, basesixfour_url: &str) -> Result<Url, ParseError> {
|
||||
let id: String = uuid::Uuid::new_v4().to_string();
|
||||
generate_create_id(domain, &id, basesixfour_url)
|
||||
}
|
||||
|
||||
pub fn base_url_encode(url: &Url) -> String {
|
||||
base64_url::encode(&url.to_string())
|
||||
}
|
||||
|
||||
pub fn base_url_decode(encoded: &str) -> String {
|
||||
String::from_utf8(base64_url::decode(encoded).unwrap()).unwrap()
|
||||
/// Just generate random url as object id. In a real project, you probably want to use
|
||||
/// an url which contains the database id for easy retrieval (or store the random id in db).
|
||||
pub fn generate_object_id(domain: &str) -> Result<Url, ParseError> {
|
||||
let id: String = thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(7)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
Url::parse(&format!("https://{}/objects/{}", domain, id))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,678 +0,0 @@
|
|||
use std::fmt::format;
|
||||
|
||||
use activitypub_federation::{
|
||||
fetch::object_id::ObjectId, http_signatures::generate_actor_keypair, traits::Object,
|
||||
};
|
||||
use activitystreams_kinds::public;
|
||||
use anyhow::{anyhow, Ok};
|
||||
use async_recursion::async_recursion;
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use reqwest::header::{self, CONTENT_TYPE};
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::to_string;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::info;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
database::State,
|
||||
entities::{self, post, prelude, user},
|
||||
objects::{
|
||||
self,
|
||||
person::{AttachmentType, EndpointType, IconType, Person, TagType},
|
||||
post::Mention,
|
||||
},
|
||||
utils::{generate_object_id, generate_user_id, generate_versia_post_url},
|
||||
API_DOMAIN, DB, FEDERATION_CONFIG, LOCAL_USER_NAME, LYSAND_DOMAIN, USERNAME,
|
||||
};
|
||||
|
||||
use super::{
|
||||
objects::{CategoryType, ContentEntry, ContentFormat, Note, PublicKey, UserCollections},
|
||||
superx::request_client,
|
||||
};
|
||||
|
||||
pub async fn fetch_user_from_url(url: Url) -> anyhow::Result<super::objects::User> {
|
||||
let req_client = request_client();
|
||||
let request = req_client.get(url).send().await?;
|
||||
Ok(request.json::<super::objects::User>().await?)
|
||||
}
|
||||
|
||||
pub async fn versia_post_from_db(
|
||||
post: entities::post::Model,
|
||||
) -> anyhow::Result<super::objects::Note> {
|
||||
let data = FEDERATION_CONFIG.get().unwrap();
|
||||
let domain = data.domain();
|
||||
let url = generate_versia_post_url(domain, &post.id)?;
|
||||
let creator = prelude::User::find()
|
||||
.filter(entities::user::Column::Id.eq(post.creator.clone()))
|
||||
.one(DB.get().unwrap())
|
||||
.await?;
|
||||
let author = Url::parse(&creator.unwrap().url)?;
|
||||
let group = match post.visibility.as_str() {
|
||||
"public" => Some("public".to_string()),
|
||||
"followers" => Some("followers".to_string()),
|
||||
"direct" => None,
|
||||
//"unlisted" => super::objects::VisibilityType::Unlisted,
|
||||
_ => Some("public".to_string()),
|
||||
};
|
||||
|
||||
let mut mentions = Vec::new();
|
||||
let ap_obj =
|
||||
serde_json::from_str::<crate::objects::post::Note>(post.ap_json.unwrap().as_str())?;
|
||||
let req_data = data.to_request_data();
|
||||
for obj in ap_obj.tag.clone() {
|
||||
info!("Url: {}", obj.href);
|
||||
let option = user::Model::read_from_id(obj.href.clone(), &req_data)
|
||||
.await
|
||||
.unwrap();
|
||||
if let Some(model) = option {
|
||||
info!("Model: {:?}", model);
|
||||
let user = versia_user_from_db(model).await?;
|
||||
let domain = user.inbox.domain();
|
||||
//if domain.is_none() || domain.is_some_and(|domain| LYSAND_DOMAIN.as_str() != domain) {
|
||||
// continue;
|
||||
//} TODO
|
||||
mentions.push(user.inbox);
|
||||
} else if let Some(model) = entities::prelude::User::find()
|
||||
.filter(
|
||||
entities::user::Column::Id.eq(obj.href.path_segments().unwrap().last().unwrap()),
|
||||
)
|
||||
.one(data.database_connection.as_ref())
|
||||
.await?
|
||||
{
|
||||
info!("Model: {:?}", model);
|
||||
let user = versia_user_from_db(model).await?;
|
||||
let domain = user.inbox.domain();
|
||||
//if domain.is_none() || domain.is_some_and(|domain| LYSAND_DOMAIN.as_str() != domain) {
|
||||
// continue;
|
||||
//} TODO
|
||||
mentions.push(user.inbox);
|
||||
}
|
||||
}
|
||||
|
||||
let mut content = ContentFormat::default();
|
||||
content.x.insert(
|
||||
"text/html".to_string(),
|
||||
ContentEntry::from_string(post.content),
|
||||
);
|
||||
let note = super::objects::Note {
|
||||
rtype: "Note".to_string(),
|
||||
id: uuid::Uuid::parse_str(&post.id)?,
|
||||
author: author.clone(),
|
||||
uri: url.clone(),
|
||||
created_at: OffsetDateTime::from_unix_timestamp(post.created_at.timestamp()).unwrap(),
|
||||
content: Some(content),
|
||||
mentions: Some(mentions),
|
||||
category: Some(CategoryType::Microblog),
|
||||
device: None,
|
||||
previews: None,
|
||||
replies_to: None,
|
||||
quotes: None,
|
||||
group,
|
||||
attachments: None,
|
||||
subject: post.title,
|
||||
is_sensitive: Some(post.sensitive),
|
||||
};
|
||||
Ok(note)
|
||||
}
|
||||
|
||||
pub async fn versia_user_from_db(
|
||||
user: entities::user::Model,
|
||||
) -> anyhow::Result<super::objects::User> {
|
||||
let url = Url::parse(&user.url)?;
|
||||
let ap = user.ap_json.unwrap();
|
||||
let serialized_ap: crate::objects::person::Person = serde_json::from_str(&ap)?;
|
||||
let inbox_url;
|
||||
let outbox_url = Url::parse(
|
||||
("https://".to_string() + &API_DOMAIN + "/apbridge/versia/outbox/" + &user.id).as_str(),
|
||||
)?;
|
||||
let followers_url;
|
||||
let following_url;
|
||||
let featured_url = Url::parse(
|
||||
("https://".to_string() + &API_DOMAIN + "/apbridge/versia/featured/" + &user.id).as_str(),
|
||||
)?;
|
||||
let likes_url = Url::parse(
|
||||
("https://".to_string() + &API_DOMAIN + "/apbridge/versia/likes/" + &user.id).as_str(),
|
||||
)?;
|
||||
let dislikes_url = Url::parse(
|
||||
("https://".to_string() + &API_DOMAIN + "/apbridge/versia/dislikes/" + &user.id).as_str(),
|
||||
)?;
|
||||
|
||||
if user.local {
|
||||
inbox_url = Url::parse(&user.inbox)?;
|
||||
followers_url = Url::parse(&user.followers.unwrap())?;
|
||||
following_url = Url::parse(&user.following.unwrap())?;
|
||||
} else {
|
||||
inbox_url = Url::parse(&("https://".to_string() + &API_DOMAIN + "/apbridge/versia/inbox"))?;
|
||||
followers_url = Url::parse(
|
||||
("https://".to_string() + &API_DOMAIN + "/apbridge/versia/followers/" + &user.id)
|
||||
.as_str(),
|
||||
)?;
|
||||
following_url = Url::parse(
|
||||
("https://".to_string() + &API_DOMAIN + "/apbridge/versia/following/" + &user.id)
|
||||
.as_str(),
|
||||
)?;
|
||||
}
|
||||
|
||||
let og_displayname_ref = user.name.clone();
|
||||
let og_username_ref = user.username.clone();
|
||||
let empty = "".to_owned();
|
||||
// linter was having a stroke
|
||||
let display_name = match og_displayname_ref {
|
||||
og_username_ref => None,
|
||||
empty => None,
|
||||
_ => Some(user.name),
|
||||
};
|
||||
let mut bio = ContentFormat::default();
|
||||
bio.x.insert(
|
||||
"text/html".to_string(),
|
||||
ContentEntry::from_string(user.summary.unwrap_or_default()),
|
||||
);
|
||||
let avatar = match serialized_ap.icon {
|
||||
Some(icon) => {
|
||||
let mut content_format = ContentFormat::default();
|
||||
let content_entry = ContentEntry::from_string(icon.url.to_string());
|
||||
let media_type = icon.media_type.unwrap_or({
|
||||
let req = request_client().get(icon.url.clone()).build()?;
|
||||
let res = request_client().execute(req).await?;
|
||||
let headers = res.headers();
|
||||
let content_type_header = headers.get(CONTENT_TYPE);
|
||||
content_type_header.unwrap().to_str().unwrap().to_string()
|
||||
});
|
||||
content_format.x.insert(media_type, content_entry);
|
||||
Some(content_format)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let header = match serialized_ap.image {
|
||||
Some(image) => {
|
||||
let mut content_format = ContentFormat::default();
|
||||
let content_entry = ContentEntry::from_string(image.url.to_string());
|
||||
let media_type = image.media_type.unwrap_or({
|
||||
let req = request_client().get(image.url.clone()).build()?;
|
||||
let res = request_client().execute(req).await?;
|
||||
let headers = res.headers();
|
||||
let content_type_header = headers.get(CONTENT_TYPE);
|
||||
content_type_header.unwrap().to_str().unwrap().to_string()
|
||||
});
|
||||
content_format.x.insert(media_type, content_entry);
|
||||
Some(content_format)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let mut fields = Vec::new();
|
||||
if let Some(attachments) = serialized_ap.attachment {
|
||||
for attachment in attachments {
|
||||
let mut key = ContentFormat::default();
|
||||
let mut value = ContentFormat::default();
|
||||
key.x.insert(
|
||||
"text/html".to_string(),
|
||||
ContentEntry::from_string(attachment.name),
|
||||
);
|
||||
value.x.insert(
|
||||
"text/html".to_string(),
|
||||
ContentEntry::from_string(attachment.value),
|
||||
);
|
||||
fields.push(super::objects::FieldKV { key, value });
|
||||
}
|
||||
}
|
||||
let emojis = match serialized_ap.tag {
|
||||
Some(tags) => {
|
||||
let mut emojis = Vec::new();
|
||||
for tag in tags {
|
||||
let mut content_format = ContentFormat::default();
|
||||
if tag.icon.is_none() {
|
||||
continue;
|
||||
}
|
||||
let content_entry =
|
||||
ContentEntry::from_string(tag.icon.clone().unwrap().url.to_string());
|
||||
let icon = tag.icon.unwrap();
|
||||
let media_type = icon.media_type.unwrap_or({
|
||||
let req = request_client().get(icon.url.clone()).build()?;
|
||||
let res = request_client().execute(req).await?;
|
||||
let headers = res.headers();
|
||||
let content_type_header = headers.get(CONTENT_TYPE);
|
||||
if content_type_header.is_none() {
|
||||
continue;
|
||||
}
|
||||
content_type_header.unwrap().to_str().unwrap().to_string()
|
||||
});
|
||||
content_format.x.insert(media_type, content_entry);
|
||||
let name = tag.name;
|
||||
emojis.push(super::objects::CustomEmoji {
|
||||
name,
|
||||
url: content_format,
|
||||
});
|
||||
}
|
||||
Some(super::objects::CustomEmojis { emojis })
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let extensions = super::objects::ExtensionSpecs {
|
||||
custom_emojis: emojis,
|
||||
};
|
||||
let collections = UserCollections {
|
||||
outbox: outbox_url,
|
||||
followers: followers_url,
|
||||
following: following_url,
|
||||
featured: featured_url,
|
||||
likes: Some(likes_url),
|
||||
dislikes: Some(dislikes_url),
|
||||
};
|
||||
let user = super::objects::User {
|
||||
rtype: "User".to_string(),
|
||||
id: uuid::Uuid::parse_str(&user.id)?,
|
||||
uri: url.clone(),
|
||||
username: user.username,
|
||||
display_name,
|
||||
inbox: inbox_url,
|
||||
bio: Some(bio),
|
||||
collections,
|
||||
avatar,
|
||||
header,
|
||||
fields: Some(fields),
|
||||
indexable: false,
|
||||
created_at: OffsetDateTime::from_unix_timestamp(user.created_at.timestamp()).unwrap(),
|
||||
public_key: PublicKey {
|
||||
actor: url.clone(),
|
||||
key: "AAAAC3NzaC1lZDI1NTE5AAAAIMxsX+lEWkHZt9NOvn9yYFP0Z++186LY4b97C4mwj/f2".to_string(), // dummy key
|
||||
algorithm: "ed25519".to_string(),
|
||||
},
|
||||
extensions: Some(extensions),
|
||||
manually_approves_followers: false,
|
||||
};
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn option_content_format_text(opt: Option<ContentFormat>) -> Option<String> {
|
||||
if let Some(format) = opt {
|
||||
return Some(format.select_rich_text().await.unwrap());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
#[async_recursion]
|
||||
pub async fn db_post_from_url(url: Url) -> anyhow::Result<entities::post::Model> {
|
||||
if !url.domain().eq(&Some(LYSAND_DOMAIN.as_str())) {
|
||||
return Err(anyhow!("not versias domain"));
|
||||
}
|
||||
let str_url = url.to_string();
|
||||
let post_res: Option<post::Model> = prelude::Post::find()
|
||||
.filter(entities::post::Column::Url.eq(str_url.clone()))
|
||||
.one(DB.get().unwrap())
|
||||
.await?;
|
||||
|
||||
if let Some(post) = post_res {
|
||||
Ok(post)
|
||||
} else {
|
||||
let post = fetch_note_from_url(url.clone()).await?;
|
||||
let res =
|
||||
receive_versia_note(post, "https://".to_string() + &API_DOMAIN + "/example").await?; // TODO: Replace user id with actual user id
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ApiUser {
|
||||
uri: Url,
|
||||
}
|
||||
|
||||
pub async fn local_db_user_from_name(name: String) -> anyhow::Result<entities::user::Model> {
|
||||
let user_res: Option<user::Model> = prelude::User::find()
|
||||
.filter(entities::user::Column::Username.eq(name.clone()))
|
||||
.filter(entities::user::Column::Local.eq(true))
|
||||
.one(DB.get().unwrap())
|
||||
.await?;
|
||||
if let Some(user) = user_res {
|
||||
Ok(user)
|
||||
} else {
|
||||
let client = request_client();
|
||||
let api_url = Url::parse(&format!(
|
||||
"https://{}/api/v1/accounts/id?username={}",
|
||||
LYSAND_DOMAIN.to_string(),
|
||||
name
|
||||
))?;
|
||||
let request = client.get(api_url).send().await?;
|
||||
let user_json = request.json::<ApiUser>().await?;
|
||||
Ok(db_user_from_url(user_json.uri).await?)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn db_user_from_url(url: Url) -> anyhow::Result<entities::user::Model> {
|
||||
println!("Fetching user from domain: {}", url.domain().unwrap());
|
||||
if !url.domain().eq(&Some(LYSAND_DOMAIN.as_str()))
|
||||
&& !url.domain().eq(&Some(API_DOMAIN.as_str()))
|
||||
{
|
||||
return Err(anyhow!("not versias domain"));
|
||||
}
|
||||
let user_res: Option<user::Model> = prelude::User::find()
|
||||
.filter(entities::user::Column::Url.eq(url.to_string()))
|
||||
.one(DB.get().unwrap())
|
||||
.await?;
|
||||
|
||||
if let Some(user) = user_res {
|
||||
Ok(user)
|
||||
} else {
|
||||
let ls_user = fetch_user_from_url(url).await?;
|
||||
let keypair = generate_actor_keypair()?;
|
||||
let bridge_user_url = generate_user_id(&API_DOMAIN, &ls_user.id.to_string())?;
|
||||
let inbox = Url::parse(&format!(
|
||||
"https://{}/{}/inbox",
|
||||
API_DOMAIN.to_string(),
|
||||
ls_user.username.clone()
|
||||
))?;
|
||||
let icon = if let Some(avatar) = ls_user.avatar {
|
||||
let avatar_url = avatar.select_rich_img_touple().await?;
|
||||
Some(IconType {
|
||||
type_: "Image".to_string(),
|
||||
media_type: Some(avatar_url.0),
|
||||
url: Url::parse(&avatar_url.1).unwrap(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let image = if let Some(header) = ls_user.header {
|
||||
let header_url = header.select_rich_img_touple().await?;
|
||||
Some(IconType {
|
||||
type_: "Image".to_string(),
|
||||
media_type: Some(header_url.0),
|
||||
url: Url::parse(&header_url.1).unwrap(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut attachments: Vec<AttachmentType> = Vec::new();
|
||||
if let Some(fields) = ls_user.fields {
|
||||
for attachment in fields {
|
||||
attachments.push(AttachmentType {
|
||||
type_: "PropertyValue".to_string(),
|
||||
name: attachment.key.select_rich_text().await?,
|
||||
value: attachment.value.select_rich_text().await?,
|
||||
});
|
||||
}
|
||||
}
|
||||
let mut tags: Vec<TagType> = Vec::new();
|
||||
if let Some(extensions) = ls_user.extensions {
|
||||
if let Some(custom_emojis) = extensions.custom_emojis {
|
||||
for emoji in custom_emojis.emojis {
|
||||
let touple = emoji.url.select_rich_img_touple().await?;
|
||||
tags.push(TagType {
|
||||
id: Some(Url::parse(&touple.1).unwrap()),
|
||||
name: emoji.name,
|
||||
type_: "Emoji".to_string(),
|
||||
updated: Some(Utc::now()),
|
||||
href: None,
|
||||
icon: Some(IconType {
|
||||
type_: "Image".to_string(),
|
||||
media_type: Some(touple.0),
|
||||
url: Url::parse(&touple.1).unwrap(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let ap_json = Person {
|
||||
kind: Default::default(),
|
||||
id: bridge_user_url.clone().into(),
|
||||
preferred_username: ls_user.username.clone(),
|
||||
inbox,
|
||||
public_key: activitypub_federation::protocol::public_key::PublicKey {
|
||||
owner: bridge_user_url.clone(),
|
||||
public_key_pem: keypair.public_key.clone(),
|
||||
id: format!("{}#main-key", bridge_user_url.clone()),
|
||||
},
|
||||
name: ls_user
|
||||
.display_name
|
||||
.clone()
|
||||
.unwrap_or(ls_user.username.clone()),
|
||||
summary: option_content_format_text(ls_user.bio.clone()).await,
|
||||
url: ls_user.uri.clone(),
|
||||
indexable: Some(ls_user.indexable),
|
||||
discoverable: Some(true),
|
||||
manually_approves_followers: Some(false),
|
||||
followers: None,
|
||||
following: None,
|
||||
featured: None,
|
||||
featured_tags: None,
|
||||
also_known_as: None,
|
||||
outbox: None,
|
||||
endpoints: Some(EndpointType {
|
||||
shared_inbox: Url::parse(
|
||||
&format!(
|
||||
"https://{}/{}/inbox",
|
||||
API_DOMAIN.to_string(),
|
||||
&USERNAME.to_string()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.unwrap(),
|
||||
}),
|
||||
icon,
|
||||
image,
|
||||
attachment: Some(attachments),
|
||||
tag: Some(tags),
|
||||
};
|
||||
let user = entities::user::ActiveModel {
|
||||
id: Set(ls_user.id.to_string()),
|
||||
username: Set(ls_user.username.clone()),
|
||||
name: Set(ls_user.display_name.unwrap_or(ls_user.username)),
|
||||
inbox: Set(ls_user.inbox.to_string()),
|
||||
public_key: Set(keypair.public_key.clone()),
|
||||
private_key: Set(Some(keypair.private_key.clone())),
|
||||
last_refreshed_at: Set(Utc::now()),
|
||||
follower_count: Set(0),
|
||||
following_count: Set(0),
|
||||
url: Set(ls_user.uri.to_string()),
|
||||
local: Set(true),
|
||||
created_at: Set(
|
||||
DateTime::from_timestamp(ls_user.created_at.unix_timestamp(), 0).unwrap(),
|
||||
),
|
||||
summary: Set(option_content_format_text(ls_user.bio).await),
|
||||
updated_at: Set(Some(Utc::now())),
|
||||
followers: Set(Some(ls_user.collections.followers.to_string())),
|
||||
following: Set(Some(ls_user.collections.following.to_string())),
|
||||
ap_json: Set(Some(serde_json::to_string(&ap_json).unwrap())),
|
||||
..Default::default()
|
||||
};
|
||||
let db = DB.get().unwrap();
|
||||
Ok(user.insert(db).await?)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_note_from_url(url: Url) -> anyhow::Result<super::objects::Note> {
|
||||
let req_client = request_client();
|
||||
let request = req_client.get(url).send().await?;
|
||||
Ok(request.json::<super::objects::Note>().await?)
|
||||
}
|
||||
#[async_recursion]
|
||||
pub async fn receive_versia_note(
|
||||
note: Note,
|
||||
db_id: String,
|
||||
) -> anyhow::Result<entities::post::Model> {
|
||||
let post_res: Option<post::Model> = prelude::Post::find()
|
||||
.filter(entities::post::Column::Id.eq(note.id.to_string()))
|
||||
.one(DB.get().unwrap())
|
||||
.await?;
|
||||
|
||||
if let Some(post) = post_res {
|
||||
return Ok(post);
|
||||
}
|
||||
|
||||
let versia_author: entities::user::Model = db_user_from_url(note.author.clone()).await?;
|
||||
let user_res = prelude::User::find_by_id(db_id)
|
||||
.one(DB.get().unwrap())
|
||||
.await;
|
||||
if user_res.is_err() {
|
||||
println!("{}", user_res.as_ref().unwrap_err());
|
||||
return Err(user_res.err().unwrap().into());
|
||||
}
|
||||
if let Some(target) = user_res? {
|
||||
let data = FEDERATION_CONFIG.get().unwrap();
|
||||
let id: ObjectId<post::Model> =
|
||||
generate_object_id(data.domain(), ¬e.id.to_string())?.into();
|
||||
let user_id = generate_user_id(data.domain(), &target.id.to_string())?;
|
||||
println!("{}", note.author.clone());
|
||||
let user = fetch_user_from_url(note.author.clone()).await?;
|
||||
let mut tag: Vec<Mention> = Vec::new();
|
||||
let domain = API_DOMAIN.as_str();
|
||||
for l_tag in note.mentions.clone().unwrap_or_default() {
|
||||
if l_tag.clone().to_string().contains("apbridge/user") {
|
||||
println!("{}", l_tag.clone().to_string().contains("apbridge/user"));
|
||||
tag.push(Mention {
|
||||
href: l_tag,
|
||||
kind: Default::default(),
|
||||
});
|
||||
continue;
|
||||
} else if !(l_tag.clone().to_string().contains(LYSAND_DOMAIN.as_str())
|
||||
|| l_tag.clone().to_string().contains(domain))
|
||||
{
|
||||
println!(
|
||||
"{}",
|
||||
l_tag.clone().to_string().contains(LYSAND_DOMAIN.as_str())
|
||||
);
|
||||
println!("{}", l_tag.clone().to_string().contains(domain));
|
||||
println!(
|
||||
"-------------- {} -----------------a",
|
||||
l_tag.clone().to_string()
|
||||
);
|
||||
tag.push(Mention {
|
||||
href: l_tag,
|
||||
kind: Default::default(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
println!("+++++++ --------- ++++++++++");
|
||||
let user = db_user_from_url(l_tag).await?;
|
||||
let ap_url =
|
||||
Url::parse(&format!("https://{}/apbridge/user/{}", domain, user.id).to_string())?;
|
||||
tag.push(Mention {
|
||||
href: ap_url,
|
||||
kind: Default::default(),
|
||||
});
|
||||
}
|
||||
let mut mentions = Vec::new();
|
||||
for obj in tag.clone() {
|
||||
mentions.push(obj.href.clone());
|
||||
}
|
||||
let to = match note.group.clone().unwrap_or("nothing".to_string()).as_str() {
|
||||
"public" => {
|
||||
let mut vec = vec![
|
||||
public(),
|
||||
Url::parse(&user.collections.followers.to_string().as_str())?,
|
||||
];
|
||||
vec.append(&mut mentions.clone());
|
||||
vec
|
||||
}
|
||||
"unlisted" => {
|
||||
let mut vec = vec![Url::parse(
|
||||
&user.collections.followers.to_string().as_str(),
|
||||
)?];
|
||||
vec.append(&mut mentions.clone());
|
||||
vec
|
||||
}
|
||||
"followers" => {
|
||||
let mut vec = vec![Url::parse(
|
||||
&user.collections.followers.to_string().as_str(),
|
||||
)?];
|
||||
vec.append(&mut mentions.clone());
|
||||
vec
|
||||
}
|
||||
_ => mentions.clone(),
|
||||
};
|
||||
let cc = match note.group.clone().unwrap_or("nothing".to_string()).as_str() {
|
||||
"unlisted" => Some(vec![public()]),
|
||||
_ => None,
|
||||
};
|
||||
let reply: Option<ObjectId<entities::post::Model>> =
|
||||
if let Some(rep) = note.replies_to.clone() {
|
||||
let note = fetch_note_from_url(rep).await?;
|
||||
let fake_rep_url = Url::parse(&format!(
|
||||
"https://{}/apbridge/object/{}",
|
||||
API_DOMAIN.to_string(),
|
||||
¬e.id.to_string()
|
||||
))?;
|
||||
Some(fake_rep_url.into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let quote: Option<ObjectId<entities::post::Model>> = if let Some(rep) = note.quotes.clone()
|
||||
{
|
||||
let note = fetch_note_from_url(rep).await?;
|
||||
let fake_rep_url = Url::parse(&format!(
|
||||
"https://{}/apbridge/object/{}",
|
||||
API_DOMAIN.to_string(),
|
||||
¬e.id.to_string()
|
||||
))?;
|
||||
Some(fake_rep_url.into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let reply_uuid: Option<String> = if let Some(rep) = note.replies_to.clone() {
|
||||
Some(db_post_from_url(rep).await?.id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let quote_uuid: Option<String> = if let Some(rep) = note.quotes.clone() {
|
||||
Some(db_post_from_url(rep).await?.id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let ap_note = crate::objects::post::Note {
|
||||
kind: Default::default(),
|
||||
id,
|
||||
sensitive: Some(note.is_sensitive.unwrap_or(false)),
|
||||
cc,
|
||||
to,
|
||||
tag,
|
||||
attributed_to: {
|
||||
let user = db_user_from_url(Url::parse(user.uri.clone().as_str()).unwrap()).await?;
|
||||
let ap_url = Url::parse(
|
||||
&format!("https://{}/apbridge/user/{}", domain, user.id).to_string(),
|
||||
)?;
|
||||
ap_url.into()
|
||||
},
|
||||
content: option_content_format_text(note.content)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
in_reply_to: reply.clone(),
|
||||
};
|
||||
|
||||
let visibility = match note.group.clone().unwrap_or("nothing".to_string()).as_str() {
|
||||
"public" => "public",
|
||||
"followers" => "followers",
|
||||
"unlisted" => "unlisted",
|
||||
_ => "direct",
|
||||
};
|
||||
if let Some(obj) = note.replies_to {
|
||||
println!("Quoting: {}", db_post_from_url(obj).await?.url);
|
||||
}
|
||||
if let Some(obj) = note.quotes {
|
||||
println!("Replying to: {}", db_post_from_url(obj).await?.url);
|
||||
}
|
||||
let post = entities::post::ActiveModel {
|
||||
id: Set(note.id.to_string()),
|
||||
creator: Set(versia_author.id.clone()),
|
||||
content: Set(ap_note.content.clone()),
|
||||
sensitive: Set(ap_note.sensitive.unwrap_or_default()),
|
||||
created_at: Set(Utc
|
||||
.timestamp_micros(note.created_at.unix_timestamp())
|
||||
.unwrap()),
|
||||
local: Set(true),
|
||||
updated_at: Set(Some(Utc::now())),
|
||||
content_type: Set("Note".to_string()),
|
||||
visibility: Set(visibility.to_string()),
|
||||
title: Set(note.subject.clone()),
|
||||
url: Set(note.uri.clone().to_string()),
|
||||
reply_id: Set(reply_uuid),
|
||||
quoting_id: Set(quote_uuid),
|
||||
spoiler_text: Set(note.subject),
|
||||
ap_json: Set(Some(serde_json::to_string(&ap_note).unwrap())),
|
||||
..Default::default()
|
||||
};
|
||||
let res = post.insert(DB.get().unwrap()).await?;
|
||||
Ok(res)
|
||||
} else {
|
||||
Err(anyhow!("User not found"))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
entities::{follow_relation, prelude, user},
|
||||
utils::generate_follow_accept_id,
|
||||
API_DOMAIN, DB,
|
||||
};
|
||||
|
||||
use super::{
|
||||
conversion::{fetch_user_from_url, versia_user_from_db},
|
||||
objects::FollowResult,
|
||||
superx::request_client,
|
||||
};
|
||||
|
||||
pub async fn send_follow_accept_to_versia(model: follow_relation::Model) -> anyhow::Result<()> {
|
||||
let request_client = request_client();
|
||||
let db = DB.get().unwrap();
|
||||
|
||||
let id_raw = model.accept_id.unwrap();
|
||||
let id = uuid::Uuid::parse_str(&id_raw)?;
|
||||
let uri = generate_follow_accept_id(API_DOMAIN.as_str(), &id_raw)?;
|
||||
|
||||
let follower_model = prelude::User::find()
|
||||
.filter(user::Column::Id.eq(model.follower_id))
|
||||
.one(db)
|
||||
.await?
|
||||
.unwrap();
|
||||
let versia_follower = fetch_user_from_url(Url::parse(&follower_model.url)?).await?;
|
||||
|
||||
let followee_model = prelude::User::find()
|
||||
.filter(user::Column::Id.eq(model.followee_id))
|
||||
.one(db)
|
||||
.await?
|
||||
.unwrap();
|
||||
let versia_followee = versia_user_from_db(followee_model).await?;
|
||||
|
||||
let entity = FollowResult {
|
||||
rtype: "FollowAccept".to_string(),
|
||||
id,
|
||||
uri,
|
||||
created_at: OffsetDateTime::now_utc(),
|
||||
author: versia_followee.uri,
|
||||
follower: versia_follower.uri,
|
||||
};
|
||||
|
||||
let request = request_client
|
||||
.post(versia_follower.inbox.as_str())
|
||||
.header("Content-Type", "application/json; charset=utf-8")
|
||||
.header("Accept", "application/json")
|
||||
.header("Date", entity.created_at.clone().to_string())
|
||||
.json(&entity);
|
||||
|
||||
let response = request.send().await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Failed to send follow accept to Versia"))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,269 +0,0 @@
|
|||
use activitypub_federation::{
|
||||
fetch::{object_id::ObjectId, webfinger::webfinger_resolve_actor},
|
||||
protocol::{context::WithContext, public_key::PublicKey},
|
||||
traits::Object,
|
||||
FEDERATION_CONTENT_TYPE,
|
||||
};
|
||||
use activitystreams_kinds::{activity::CreateType, object};
|
||||
use actix_web::{get, post, web, HttpResponse};
|
||||
use sea_orm::{query, ColumnTrait, EntityTrait, QueryFilter};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
database::State,
|
||||
entities::{
|
||||
post::{self, Entity},
|
||||
prelude, user,
|
||||
},
|
||||
error,
|
||||
objects::{self, person::Person},
|
||||
utils::{base_url_decode, generate_create_id, generate_user_id},
|
||||
versia::{
|
||||
conversion::{versia_post_from_db, versia_user_from_db},
|
||||
inbox::inbox_entry,
|
||||
},
|
||||
Response, API_DOMAIN, DB, FEDERATION_CONFIG,
|
||||
};
|
||||
|
||||
use super::conversion::db_user_from_url;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct VersiaQuery {
|
||||
// Post url
|
||||
url: Option<Url>,
|
||||
// User handle
|
||||
user: Option<String>,
|
||||
// User URL
|
||||
user_url: Option<Url>,
|
||||
}
|
||||
|
||||
#[get("/apbridge/versia/query")]
|
||||
async fn query_post(
|
||||
query: web::Query<VersiaQuery>,
|
||||
state: web::Data<State>,
|
||||
) -> actix_web::Result<HttpResponse, error::Error> {
|
||||
if query.url.is_none() && query.user.is_none() && query.user_url.is_none() {
|
||||
return Ok(
|
||||
HttpResponse::BadRequest().body("Bad Request. Error code: mrrrmrrrmrrawwawwawwa")
|
||||
);
|
||||
}
|
||||
|
||||
let db = DB.get().unwrap();
|
||||
let data = FEDERATION_CONFIG.get().unwrap();
|
||||
|
||||
if let Some(user) = query.user.clone() {
|
||||
let target =
|
||||
webfinger_resolve_actor::<State, user::Model>(user.as_str(), &data.to_request_data())
|
||||
.await?;
|
||||
println!("!!!!!!! DB USER GOT");
|
||||
let versia_user = versia_user_from_db(target).await?;
|
||||
|
||||
return Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.json(versia_user));
|
||||
}
|
||||
|
||||
if let Some(user) = query.user_url.clone() {
|
||||
let versia_user = versia_url_to_user(user).await?;
|
||||
|
||||
return Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.json(versia_user));
|
||||
}
|
||||
|
||||
let opt_model = prelude::Post::find()
|
||||
.filter(post::Column::Url.eq(query.url.clone().unwrap().as_str()))
|
||||
.one(db)
|
||||
.await?;
|
||||
let target;
|
||||
if let Some(model) = opt_model {
|
||||
target = model;
|
||||
} else {
|
||||
target = ObjectId::<post::Model>::from(Url::parse(query.url.clone().unwrap().as_str())?)
|
||||
.dereference(&data.to_request_data())
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.json(versia_post_from_db(target).await?))
|
||||
}
|
||||
|
||||
#[post("/apbridge/versia/inbox")]
|
||||
async fn versia_inbox(
|
||||
body: web::Bytes,
|
||||
state: web::Data<State>,
|
||||
) -> actix_web::Result<HttpResponse, error::Error> {
|
||||
let string = String::from_utf8(body.to_vec())?;
|
||||
inbox_entry(&string).await?;
|
||||
Ok(HttpResponse::Created().finish())
|
||||
}
|
||||
|
||||
#[get("/apbridge/object/{post}")]
|
||||
async fn fetch_post(
|
||||
path: web::Path<String>,
|
||||
state: web::Data<State>,
|
||||
) -> actix_web::Result<HttpResponse, error::Error> {
|
||||
let db = DB.get().unwrap();
|
||||
|
||||
let post = prelude::Post::find()
|
||||
.filter(post::Column::Id.eq(path.as_str()))
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
let post = match post {
|
||||
Some(post) => post,
|
||||
None => return Ok(HttpResponse::NotFound().finish()),
|
||||
};
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type(FEDERATION_CONTENT_TYPE)
|
||||
.json(crate::objects::post::Note::from_db(&post)))
|
||||
}
|
||||
|
||||
#[get("/apbridge/user/{user}")]
|
||||
async fn fetch_user(
|
||||
path: web::Path<String>,
|
||||
state: web::Data<State>,
|
||||
) -> actix_web::Result<HttpResponse, error::Error> {
|
||||
let db = DB.get().unwrap();
|
||||
|
||||
let user = prelude::User::find()
|
||||
.filter(user::Column::Id.eq(path.as_str()))
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
let user = match user {
|
||||
Some(user) => user,
|
||||
None => return Ok(HttpResponse::NotFound().finish()),
|
||||
};
|
||||
|
||||
let deserialized_user: Person = serde_json::from_str(user.ap_json.as_ref().unwrap().as_str())?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type(FEDERATION_CONTENT_TYPE)
|
||||
.json(WithContext::new_default(deserialized_user)))
|
||||
}
|
||||
|
||||
#[get("/apbridge/versia/object/{post}")]
|
||||
async fn fetch_versia_post(
|
||||
path: web::Path<String>,
|
||||
state: web::Data<State>,
|
||||
) -> actix_web::Result<HttpResponse, error::Error> {
|
||||
let db = DB.get().unwrap();
|
||||
|
||||
let post = prelude::Post::find()
|
||||
.filter(post::Column::Id.eq(path.as_str()))
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
let post = match post {
|
||||
Some(post) => post,
|
||||
None => return Ok(HttpResponse::NotFound().finish()),
|
||||
};
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.json(versia_post_from_db(post).await?))
|
||||
}
|
||||
|
||||
#[get("/apbridge/create/{id}/{base64url}")]
|
||||
async fn create_activity(
|
||||
path: web::Path<(String, String)>,
|
||||
state: web::Data<State>,
|
||||
) -> actix_web::Result<HttpResponse, error::Error> {
|
||||
let db = DB.get().unwrap();
|
||||
|
||||
let url = base_url_decode(path.1.as_str());
|
||||
|
||||
let post = prelude::Post::find()
|
||||
.filter(post::Column::Id.eq(path.0.as_str()))
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
let post = match post {
|
||||
Some(post) => post,
|
||||
None => return Ok(HttpResponse::NotFound().finish()),
|
||||
};
|
||||
|
||||
let ap_post = crate::objects::post::Note::from_db(&post);
|
||||
|
||||
let data = FEDERATION_CONFIG.get().unwrap();
|
||||
|
||||
let create = crate::activities::create_post::CreatePost {
|
||||
actor: ap_post.attributed_to.clone(),
|
||||
to: ap_post.to.clone(),
|
||||
object: ap_post,
|
||||
kind: CreateType::Create,
|
||||
id: generate_create_id(&data.to_request_data().domain(), &path.0, &path.1)?,
|
||||
};
|
||||
let create_with_context = WithContext::new_default(create);
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type(FEDERATION_CONTENT_TYPE)
|
||||
.json(create_with_context))
|
||||
}
|
||||
|
||||
pub async fn versia_url_to_user(url: Url) -> anyhow::Result<super::objects::User> {
|
||||
let db = DB.get().unwrap();
|
||||
let data = FEDERATION_CONFIG.get().unwrap();
|
||||
|
||||
let opt_model = prelude::User::find()
|
||||
.filter(user::Column::Url.eq(url.as_str()))
|
||||
.one(db)
|
||||
.await?;
|
||||
let target;
|
||||
if let Some(model) = opt_model {
|
||||
target = model;
|
||||
} else {
|
||||
target = ObjectId::<user::Model>::from(url)
|
||||
.dereference(&data.to_request_data())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
Ok(versia_user_from_db(target).await?)
|
||||
}
|
||||
|
||||
pub async fn versia_url_to_user_and_model(
|
||||
url: Url,
|
||||
) -> anyhow::Result<(super::objects::User, user::Model)> {
|
||||
let db = DB.get().unwrap();
|
||||
let data = FEDERATION_CONFIG.get().unwrap();
|
||||
|
||||
let opt_model = prelude::User::find()
|
||||
.filter(user::Column::Url.eq(url.to_string()))
|
||||
.one(db)
|
||||
.await?;
|
||||
let target;
|
||||
if let Some(model) = opt_model {
|
||||
target = model;
|
||||
} else {
|
||||
target = ObjectId::<user::Model>::from(url)
|
||||
.dereference(&data.to_request_data())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
Ok((versia_user_from_db(target.clone()).await?, target))
|
||||
}
|
||||
|
||||
pub async fn main_versia_url_to_user_and_model(
|
||||
url: Url,
|
||||
) -> anyhow::Result<(super::objects::User, user::Model)> {
|
||||
let db = DB.get().unwrap();
|
||||
let data = FEDERATION_CONFIG.get().unwrap();
|
||||
|
||||
let opt_model = prelude::User::find()
|
||||
.filter(user::Column::Url.eq(url.as_str()))
|
||||
.one(db)
|
||||
.await?;
|
||||
let target;
|
||||
if let Some(model) = opt_model {
|
||||
target = model;
|
||||
} else {
|
||||
target = db_user_from_url(url.clone()).await?;
|
||||
}
|
||||
|
||||
Ok((versia_user_from_db(target.clone()).await?, target))
|
||||
}
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
use crate::{
|
||||
activities::{create_post::CreatePost, follow::Follow},
|
||||
entities::{
|
||||
self, follow_relation,
|
||||
prelude::{self, FollowRelation},
|
||||
user,
|
||||
},
|
||||
utils::generate_follow_req_id,
|
||||
versia::http::main_versia_url_to_user_and_model,
|
||||
API_DOMAIN, DB, FEDERATION_CONFIG,
|
||||
};
|
||||
use activitypub_federation::{
|
||||
activity_sending::SendActivityTask, fetch::object_id::ObjectId, protocol::context::WithContext,
|
||||
};
|
||||
use activitystreams_kinds::{activity::FollowType, public};
|
||||
use anyhow::Result;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityOrSelect, EntityTrait, QueryFilter, Set};
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
use super::{
|
||||
conversion::{db_user_from_url, fetch_user_from_url, receive_versia_note, versia_user_from_db},
|
||||
http::{versia_url_to_user, versia_url_to_user_and_model},
|
||||
};
|
||||
|
||||
pub async fn inbox_entry(json: &str) -> Result<()> {
|
||||
// Deserialize the JSON string into a dynamic value
|
||||
let value: serde_json::Value = serde_json::from_str(json).unwrap();
|
||||
|
||||
// Extract the "type" field from the JSON
|
||||
if let Some(json_type) = value.get("type") {
|
||||
// Match the "type" field with the corresponding VersiaType
|
||||
match json_type.as_str() {
|
||||
Some("Note") => {
|
||||
let note: super::objects::Note = serde_json::from_str(json)?;
|
||||
federate_inbox(note).await?;
|
||||
}
|
||||
Some("Follow") => {
|
||||
let follow_req: super::objects::Follow = serde_json::from_str(json)?;
|
||||
follow_request(follow_req).await?;
|
||||
}
|
||||
Some("FollowAccept") => {
|
||||
let follow_accept: super::objects::FollowResult = serde_json::from_str(json)?;
|
||||
}
|
||||
Some("FollowReject") => {
|
||||
let follow_rej: super::objects::FollowResult = serde_json::from_str(json)?;
|
||||
}
|
||||
Some("Unfollow") => {
|
||||
let unfollow: super::objects::Unfollow = serde_json::from_str(json)?;
|
||||
}
|
||||
// Add more cases for other types as needed
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Unknown 'type' field in JSON, it is {}",
|
||||
json_type
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Missing 'type' field in JSON"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn follow_request(follow: super::objects::Follow) -> Result<()> {
|
||||
// Check if the user is already following the requester
|
||||
let db = DB.get().unwrap();
|
||||
let query = FollowRelation::find()
|
||||
.filter(follow_relation::Column::FollowerId.eq(follow.author.to_string().as_str()))
|
||||
.filter(follow_relation::Column::FolloweeId.eq(follow.followee.to_string().as_str()))
|
||||
.one(db)
|
||||
.await?;
|
||||
if query.is_some() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"User is already follow requesting / following the followee"
|
||||
));
|
||||
}
|
||||
let data = FEDERATION_CONFIG.get().unwrap();
|
||||
let author = main_versia_url_to_user_and_model(follow.author.into()).await?;
|
||||
println!("Followee URL: {}", &follow.followee.to_string());
|
||||
let followee = versia_url_to_user_and_model(follow.followee.into()).await?;
|
||||
let serial_ap_author = serde_json::from_str::<crate::objects::person::Person>(
|
||||
&(author.1.ap_json.clone()).unwrap(),
|
||||
)?;
|
||||
let serial_ap_followee = serde_json::from_str::<crate::objects::person::Person>(
|
||||
&(followee.1.ap_json.clone()).unwrap(),
|
||||
)?;
|
||||
|
||||
let id = uuid::Uuid::now_v7().to_string();
|
||||
|
||||
let followee_object: ObjectId<user::Model> = serial_ap_followee.id;
|
||||
let localuser_object: ObjectId<user::Model> = serial_ap_author.id;
|
||||
|
||||
println!(
|
||||
"Sending follow request to {}",
|
||||
&followee.0.display_name.unwrap_or(followee.0.username)
|
||||
);
|
||||
let create = Follow {
|
||||
actor: localuser_object.clone(),
|
||||
object: followee_object.clone(),
|
||||
kind: FollowType::Follow,
|
||||
id: generate_follow_req_id(&API_DOMAIN.to_string(), id.clone().as_str())?,
|
||||
};
|
||||
|
||||
let ap_json = serde_json::to_string(&create)?;
|
||||
|
||||
let create_with_context = WithContext::new_default(create);
|
||||
|
||||
let follow_db_entry = follow_relation::ActiveModel {
|
||||
id: Set(id.clone()),
|
||||
followee_id: Set(followee.0.id.to_string()),
|
||||
follower_id: Set(author.0.id.to_string()),
|
||||
ap_id: Set(Some(id.clone())),
|
||||
ap_json: Set(ap_json),
|
||||
remote: Set(false),
|
||||
..Default::default()
|
||||
};
|
||||
follow_db_entry.insert(db).await?;
|
||||
|
||||
let sends = SendActivityTask::prepare(
|
||||
&create_with_context,
|
||||
&author.1,
|
||||
vec![serial_ap_followee.inbox],
|
||||
&data.to_request_data(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for send in sends {
|
||||
send.sign_and_send(&data.to_request_data()).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn federate_inbox(note: super::objects::Note) -> Result<()> {
|
||||
let db_user = db_user_from_url(note.author.clone()).await?;
|
||||
let note = receive_versia_note(note, db_user.id).await?;
|
||||
|
||||
let ap_str = note.ap_json.clone().unwrap();
|
||||
let ap_note = serde_json::from_str::<crate::objects::post::Note>(&ap_str)?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
let conf = FEDERATION_CONFIG.get().unwrap();
|
||||
let inbox = get_inbox_vec(&ap_note).await;
|
||||
|
||||
let res = CreatePost::sends(ap_note, note, inbox, &conf.to_request_data()).await;
|
||||
if let Err(e) = res {
|
||||
panic!("Problem federating: {e:?}");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_inbox_vec(ap_note: &crate::objects::post::Note) -> Vec<Url> {
|
||||
let mut inbox_users: Vec<Url> = Vec::new();
|
||||
let mut inbox: Vec<Url> = Vec::new();
|
||||
|
||||
let entry = ap_note.to.get(0).unwrap();
|
||||
if entry
|
||||
.to_string()
|
||||
.eq_ignore_ascii_case(public().to_string().as_str())
|
||||
{
|
||||
let (_, mentions) = ap_note.to.split_at(2);
|
||||
inbox_users.append(&mut mentions.to_vec());
|
||||
} else {
|
||||
let (_, mentions) = ap_note.to.split_at(1);
|
||||
inbox_users.append(&mut mentions.to_vec());
|
||||
}
|
||||
|
||||
inbox_users.dedup();
|
||||
|
||||
let conf = FEDERATION_CONFIG.get().unwrap();
|
||||
let data = &conf.to_request_data();
|
||||
|
||||
for user in inbox_users {
|
||||
let ap_user = ObjectId::<user::Model>::from(user)
|
||||
.dereference(data)
|
||||
.await
|
||||
.unwrap();
|
||||
inbox.push(Url::parse(&ap_user.inbox).unwrap());
|
||||
}
|
||||
|
||||
inbox.dedup();
|
||||
|
||||
inbox
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
pub mod conversion;
|
||||
pub mod funcs;
|
||||
pub mod http;
|
||||
pub mod inbox;
|
||||
pub mod objects;
|
||||
pub mod superx;
|
||||
pub mod test;
|
||||
|
|
@ -1,405 +0,0 @@
|
|||
extern crate serde; // 1.0.68
|
||||
extern crate serde_derive; // 1.0.68
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::{Display, Formatter},
|
||||
};
|
||||
|
||||
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use time::{
|
||||
format_description::well_known::{iso8601, Iso8601},
|
||||
OffsetDateTime,
|
||||
};
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
const FORMAT: Iso8601<6651332276412969266533270467398074368> = Iso8601::<
|
||||
{
|
||||
iso8601::Config::DEFAULT
|
||||
.set_year_is_six_digits(false)
|
||||
.encode()
|
||||
},
|
||||
>;
|
||||
time::serde::format_description!(iso_versia, OffsetDateTime, FORMAT);
|
||||
|
||||
fn sort_alphabetically<T: Serialize, S: serde::Serializer>(
|
||||
value: &T,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
let value = serde_json::to_value(value).map_err(serde::ser::Error::custom)?;
|
||||
value.serialize(serializer)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SortAlphabetically<T: Serialize>(#[serde(serialize_with = "sort_alphabetically")] pub T);
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum CategoryType {
|
||||
Microblog,
|
||||
Forum,
|
||||
Blog,
|
||||
Image,
|
||||
Video,
|
||||
Audio,
|
||||
Messaging,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub enum VersiaExtensions {
|
||||
#[serde(rename = "pub.versia:share/Share")]
|
||||
Share,
|
||||
#[serde(rename = "pub.versia:custom_emojis")]
|
||||
CustomEmojis,
|
||||
#[serde(rename = "pub.versia:reactions/Reaction")]
|
||||
Reaction,
|
||||
#[serde(rename = "pub.versia:reactions")]
|
||||
Reactions,
|
||||
#[serde(rename = "pub.versia:polls")]
|
||||
Polls,
|
||||
#[serde(rename = "pub.versia:is_cat")]
|
||||
IsCat,
|
||||
#[serde(rename = "pub.versia:server_endorsement/Endorsement")]
|
||||
Endorsement,
|
||||
#[serde(rename = "pub.versia:server_endorsement")]
|
||||
EndorsementCollection,
|
||||
#[serde(rename = "pub.versia:reports/Report")]
|
||||
Report,
|
||||
#[serde(rename = "pub.versia:vanity")]
|
||||
Vanity,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct PublicKey {
|
||||
pub key: String,
|
||||
pub actor: Url,
|
||||
pub algorithm: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ContentHash {
|
||||
md5: Option<String>,
|
||||
sha1: Option<String>,
|
||||
sha256: Option<String>,
|
||||
sha512: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ContentFormat {
|
||||
pub x: HashMap<String, ContentEntry>,
|
||||
}
|
||||
|
||||
impl ContentFormat {
|
||||
pub async fn select_rich_text(&self) -> anyhow::Result<String> {
|
||||
if let Some(entry) = self.x.get("text/x.misskeymarkdown") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
if let Some(entry) = self.x.get("text/html") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
if let Some(entry) = self.x.get("text/markdown") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
if let Some(entry) = self.x.get("text/plain") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
|
||||
Ok(self.x.clone().values().next().unwrap().content.clone())
|
||||
}
|
||||
|
||||
pub async fn select_rich_img(&self) -> anyhow::Result<String> {
|
||||
if let Some(entry) = self.x.get("image/webp") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/png") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/avif") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/jxl") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/jpeg") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/gif") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/bmp") {
|
||||
return Ok(entry.content.clone());
|
||||
}
|
||||
|
||||
Ok(self.x.clone().values().next().unwrap().content.clone())
|
||||
}
|
||||
|
||||
pub async fn select_rich_img_touple(&self) -> anyhow::Result<(String, String)> {
|
||||
if let Some(entry) = self.x.get("image/webp") {
|
||||
return Ok(("image/webp".to_string(), entry.content.clone()));
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/png") {
|
||||
return Ok(("image/png".to_string(), entry.content.clone()));
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/avif") {
|
||||
return Ok(("image/avif".to_string(), entry.content.clone()));
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/jxl") {
|
||||
return Ok(("image/jxl".to_string(), entry.content.clone()));
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/jpeg") {
|
||||
return Ok(("image/jpeg".to_string(), entry.content.clone()));
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/gif") {
|
||||
return Ok(("image/gif".to_string(), entry.content.clone()));
|
||||
}
|
||||
if let Some(entry) = self.x.get("image/bmp") {
|
||||
return Ok(("image/bmp".to_string(), entry.content.clone()));
|
||||
}
|
||||
|
||||
let touple = self.x.iter().next().unwrap();
|
||||
|
||||
Ok((touple.0.clone(), touple.1.content.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ContentFormat {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_map(Some(self.x.len()))?;
|
||||
for (k, v) in &self.x {
|
||||
seq.serialize_entry(&k.to_string(), &v)?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
impl<'de> Deserialize<'de> for ContentFormat {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let map = HashMap::deserialize(deserializer)?;
|
||||
Ok(ContentFormat { x: map })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct FieldKV {
|
||||
pub key: ContentFormat,
|
||||
pub value: ContentFormat,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ContentEntry {
|
||||
content: String,
|
||||
remote: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
size: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
hash: Option<ContentHash>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
blurhash: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
fps: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
width: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
height: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
duration: Option<u64>,
|
||||
}
|
||||
impl ContentEntry {
|
||||
pub fn from_string(string: String) -> ContentEntry {
|
||||
ContentEntry {
|
||||
content: string,
|
||||
remote: false,
|
||||
description: None,
|
||||
size: None,
|
||||
hash: None,
|
||||
blurhash: None,
|
||||
fps: None,
|
||||
width: None,
|
||||
height: None,
|
||||
duration: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct User {
|
||||
pub public_key: PublicKey,
|
||||
#[serde(rename = "type")]
|
||||
pub rtype: String,
|
||||
pub id: Uuid,
|
||||
pub uri: Url,
|
||||
#[serde(with = "iso_versia")]
|
||||
pub created_at: OffsetDateTime,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub display_name: Option<String>,
|
||||
pub collections: UserCollections,
|
||||
pub inbox: Url,
|
||||
pub username: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bio: Option<ContentFormat>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub avatar: Option<ContentFormat>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub header: Option<ContentFormat>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub fields: Option<Vec<FieldKV>>,
|
||||
pub indexable: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extensions: Option<ExtensionSpecs>,
|
||||
pub manually_approves_followers: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UserCollections {
|
||||
pub outbox: Url,
|
||||
pub featured: Url,
|
||||
pub followers: Url,
|
||||
pub following: Url,
|
||||
#[serde(
|
||||
skip_serializing_if = "Option::is_none",
|
||||
rename = "pub.versia:likes/Likes"
|
||||
)]
|
||||
pub likes: Option<Url>,
|
||||
#[serde(
|
||||
skip_serializing_if = "Option::is_none",
|
||||
rename = "pub.versia:likes/Dislikes"
|
||||
)]
|
||||
pub dislikes: Option<Url>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ExtensionSpecs {
|
||||
#[serde(rename = "pub.versia:custom_emojis")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub custom_emojis: Option<CustomEmojis>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct CustomEmojis {
|
||||
pub emojis: Vec<CustomEmoji>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct CustomEmoji {
|
||||
pub name: String,
|
||||
pub url: ContentFormat,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct DeviceInfo {
|
||||
name: String,
|
||||
version: String,
|
||||
url: Url,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct LinkPreview {
|
||||
description: String,
|
||||
title: String,
|
||||
link: Url,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
image: Option<Url>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
icon: Option<Url>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Note {
|
||||
#[serde(rename = "type")]
|
||||
pub rtype: String,
|
||||
pub id: Uuid,
|
||||
pub uri: Url,
|
||||
pub author: Url,
|
||||
#[serde(with = "iso_versia")]
|
||||
pub created_at: OffsetDateTime,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub category: Option<CategoryType>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<ContentFormat>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub device: Option<DeviceInfo>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub previews: Option<Vec<LinkPreview>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub group: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub attachments: Option<Vec<ContentFormat>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub replies_to: Option<Url>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub quotes: Option<Url>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mentions: Option<Vec<Url>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub subject: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_sensitive: Option<bool>,
|
||||
//TODO extensions
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Outbox {
|
||||
pub first: Url,
|
||||
pub last: Url,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next: Option<Url>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub previous: Option<Url>,
|
||||
pub items: Vec<Note>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Follow {
|
||||
#[serde(rename = "type")]
|
||||
pub rtype: String,
|
||||
pub id: Uuid,
|
||||
pub uri: Url,
|
||||
pub author: Url,
|
||||
#[serde(with = "iso_versia")]
|
||||
pub created_at: OffsetDateTime,
|
||||
pub followee: Url,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct FollowResult {
|
||||
#[serde(rename = "type")]
|
||||
pub rtype: String,
|
||||
pub id: Uuid,
|
||||
pub uri: Url,
|
||||
pub author: Url,
|
||||
#[serde(with = "iso_versia")]
|
||||
pub created_at: OffsetDateTime,
|
||||
pub follower: Url,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Unfollow {
|
||||
#[serde(rename = "type")]
|
||||
pub rtype: String,
|
||||
pub id: Uuid,
|
||||
pub author: Url,
|
||||
#[serde(with = "iso_versia")]
|
||||
pub created_at: OffsetDateTime,
|
||||
pub followee: Url,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Delete {
|
||||
#[serde(rename = "type")]
|
||||
pub rtype: String,
|
||||
pub id: Uuid,
|
||||
pub author: Option<Url>,
|
||||
#[serde(with = "iso_versia")]
|
||||
pub created_at: OffsetDateTime,
|
||||
pub deleted_type: String,
|
||||
pub deleted: Url,
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
use super::objects::SortAlphabetically;
|
||||
|
||||
pub async fn deserialize_user(data: String) -> anyhow::Result<super::objects::User> {
|
||||
let user: super::objects::User = serde_json::from_str(&data)?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn serialize_user(user: super::objects::User) -> anyhow::Result<String> {
|
||||
let data = serde_json::to_string(&SortAlphabetically(&user))?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub async fn deserialize_versia_type(data: String) -> anyhow::Result<String> {
|
||||
let versia_type: String = serde_json::from_str(&data)?;
|
||||
Ok(versia_type)
|
||||
}
|
||||
|
||||
pub async fn serialize_versia_type(versia_type: String) -> anyhow::Result<String> {
|
||||
let data = serde_json::to_string(&versia_type)?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub async fn deserialize_note(data: String) -> anyhow::Result<super::objects::Note> {
|
||||
let post: super::objects::Note = serde_json::from_str(&data)?;
|
||||
Ok(post)
|
||||
}
|
||||
|
||||
pub async fn serialize_note(post: super::objects::Note) -> anyhow::Result<String> {
|
||||
let data = serde_json::to_string(&SortAlphabetically(&post))?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub async fn deserialize_outbox(data: String) -> anyhow::Result<super::objects::Outbox> {
|
||||
let outbox: super::objects::Outbox = serde_json::from_str(&data)?;
|
||||
Ok(outbox)
|
||||
}
|
||||
|
||||
pub async fn serialize_outbox(outbox: super::objects::Outbox) -> anyhow::Result<String> {
|
||||
let data = serde_json::to_string(&SortAlphabetically(&outbox))?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn request_client() -> reqwest::Client {
|
||||
reqwest::Client::builder()
|
||||
.user_agent(concat!(
|
||||
env!("CARGO_PKG_NAME"),
|
||||
"/",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
))
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
use crate::versia::objects::SortAlphabetically;
|
||||
|
||||
use super::superx::request_client;
|
||||
|
||||
#[actix_web::test]
|
||||
async fn test_user_serial() {
|
||||
let client = request_client();
|
||||
let response = client
|
||||
.get("https://versia.social/users/018ec082-0ae1-761c-b2c5-22275a611771")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let user = super::superx::deserialize_user(response.text().await.unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let response_outbox = client
|
||||
.get(user.collections.outbox.as_str())
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let outbox = super::superx::deserialize_outbox(response_outbox.text().await.unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(outbox.items.len() > 0);
|
||||
}
|
||||
|
||||
pub async fn main() -> anyhow::Result<()> {
|
||||
let client = request_client();
|
||||
|
||||
println!("Requesting user");
|
||||
let response = client
|
||||
.get("https://versia.social/users/018ec082-0ae1-761c-b2c5-22275a611771")
|
||||
.send()
|
||||
.await?;
|
||||
println!("Response: {:?}", response);
|
||||
let user_json = response.text().await?;
|
||||
println!("User JSON: {:?}", user_json);
|
||||
let user = super::superx::deserialize_user(user_json).await?;
|
||||
|
||||
println!("\n\n\nUser: ");
|
||||
print!("{:#?}", user);
|
||||
|
||||
println!("\n\n\nas JSON:");
|
||||
let user_json = serde_json::to_string_pretty(&SortAlphabetically(&user))?;
|
||||
println!("{}", user_json);
|
||||
|
||||
let response_outbox = client.get(user.collections.outbox.as_str()).send().await?;
|
||||
|
||||
let outbox_json = response_outbox.text().await?;
|
||||
let outbox = super::superx::deserialize_outbox(outbox_json).await?;
|
||||
|
||||
println!("\n\n\nOutbox: ");
|
||||
print!("{:#?}", outbox);
|
||||
|
||||
println!("\n\n\nas AP:");
|
||||
for item in outbox.items {
|
||||
let ap_item = super::conversion::receive_versia_note(
|
||||
item,
|
||||
"https://ap.versia.social/example".to_string(),
|
||||
)
|
||||
.await?;
|
||||
println!("{:#?}", ap_item);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB |
Loading…
Reference in a new issue