Compare commits
193 commits
Author | SHA1 | Date | |
---|---|---|---|
d2075dcbd2 | |||
7281340423 | |||
406781570c | |||
205ea8c5b8 | |||
9944095959 | |||
13d6fa25bc | |||
456d0789fb | |||
f73a05439d | |||
4df257d024 | |||
44004de576 | |||
93be6f8559 | |||
3860b4780d | |||
de6072026b | |||
2b44c8e20d | |||
d8bea2c868 | |||
5d0b03c2a3 | |||
a2bc297f0e | |||
fe8380e359 | |||
22883798b3 | |||
cb61f4a86b | |||
bb28bf800d | |||
60a27b5b11 | |||
0d77557ad6 | |||
1e40a42524 | |||
b7fafe6458 | |||
17d8c11726 | |||
b049b75873 | |||
272d06897a | |||
47529ff703 | |||
|
b4ff7abbc1 | ||
|
5906185154 | ||
|
b3b62a9c7f | ||
|
b77d4a9bdf | ||
|
b6e7fa5d13 | ||
|
05eeb5ae2a | ||
|
f41b205084 | ||
|
1302611731 | ||
|
469a5484a1 | ||
|
7471c03ed1 | ||
|
e2ea58d33a | ||
|
a3f44cf678 | ||
|
69caf0b5bc | ||
|
01cefa6ea1 | ||
|
1092319f6e | ||
|
f8df50934c | ||
|
7f6ebb89c0 | ||
|
c022e0d320 | ||
|
b6abcf252a | ||
|
55c0b1eb6b | ||
|
8daf566eb2 | ||
|
533ef48393 | ||
|
8533a892bf | ||
ad3ea0e7ca | |||
83286b7522 | |||
c5dbb0257f | |||
c0049e6d49 | |||
e5be1326de | |||
89e3f93592 | |||
5ef024d923 | |||
|
cdb728a70a | ||
|
8708abd9cd | ||
|
fc82c83421 | ||
|
01494f1770 | ||
|
278950252e | ||
|
7c38c0a4d6 | ||
|
e950189086 | ||
|
970071a9f0 | ||
|
20080333d0 | ||
|
b9fdb1ccf4 | ||
|
9768fc6228 | ||
|
9e5672929b | ||
|
dcaa2227d2 | ||
|
b0bf3cf594 | ||
|
99f6c08e9a | ||
|
13df9e0478 | ||
|
59e5f12016 | ||
|
edebae0dc6 | ||
|
ebbde534af | ||
|
300d2ef6f8 | ||
|
3c2d2d124b | ||
|
dbca8183bb | ||
|
9a32fb9c80 | ||
|
779d4e7287 | ||
|
6604ea8a2b | ||
|
95daa94a97 | ||
|
19780c3b8a | ||
|
00ca54f9b4 | ||
|
006665f6fb | ||
|
348149bbaa | ||
|
dd0c53c5e9 | ||
|
378d94e7b8 | ||
|
8cfb2318a2 | ||
|
462da87e9b | ||
|
4f9a99e6f2 | ||
|
eb1f815548 | ||
|
b85a0fb7ac | ||
|
5e1f441e8b | ||
|
0521f1f731 | ||
|
5ba8b8d6ae | ||
|
ac6491d030 | ||
|
08d7482f32 | ||
|
5450ba8871 | ||
|
399a632a88 | ||
|
ef852d781e | ||
|
441850dd21 | ||
|
73b576c643 | ||
|
f5dd0a17c9 | ||
|
37ab3dc456 | ||
|
76e85a3b7b | ||
|
521c2cbe41 | ||
|
3b5c8a4131 | ||
|
21135d7704 | ||
|
dae9be1388 | ||
|
39ab6bbb13 | ||
|
cdb304a8b7 | ||
|
848a0685de | ||
|
608ec096cd | ||
|
28be8dbb31 | ||
|
e3ee144889 | ||
|
b80b827fde | ||
|
fcf63ff317 | ||
|
9a513c928f | ||
|
7640598431 | ||
|
f76438b6f8 | ||
|
306fd7b75b | ||
|
a515af1111 | ||
|
f27b2e13eb | ||
|
a07d7ce34a | ||
|
f037a4d58c | ||
|
b56e11e81d | ||
|
c80bfccd6a | ||
|
0b65e7473e | ||
|
1637e38ee4 | ||
|
773c36f3df | ||
|
43b56be722 | ||
|
1d4bb200d2 | ||
|
8b8f1bb678 | ||
|
a3a4579e03 | ||
|
c7fd3ddc83 | ||
|
c87c5da17c | ||
|
d4f701332f | ||
|
8400241ab4 | ||
|
43d084de8d | ||
|
cb7bc04b64 | ||
|
2b6de17fb9 | ||
|
0e9879bacb | ||
|
138c1e915e | ||
|
4f64b2c615 | ||
|
133e1349cf | ||
|
268707a78a | ||
|
b5365099a4 | ||
|
a300a822ec | ||
|
161d29e00e | ||
|
6d1ed4571b | ||
|
50f31e96fc | ||
|
94a5f3a3cd | ||
|
0817177282 | ||
|
f17c9d9f76 | ||
|
522fd5bafa | ||
|
452de34780 | ||
|
ba1c694294 | ||
|
70c2d2aa25 | ||
|
2787efc83f | ||
|
bd53e147ca | ||
|
4204350375 | ||
|
6335e216a9 | ||
|
0995186bc8 | ||
|
d93c2ca23d | ||
|
1b1e2a1521 | ||
|
721238d897 | ||
|
849b201ab9 | ||
|
8d479ac15d | ||
|
cc51c2c647 | ||
|
82b6c4e7cf | ||
|
baec22272d | ||
|
49b0011a9c | ||
|
e02ebebe02 | ||
|
62069dc011 | ||
|
c201f3ea2b | ||
|
e4254e7a3d | ||
|
971b541826 | ||
|
ca2e541ff5 | ||
|
0f3c247069 | ||
|
49f51f44d8 | ||
|
f5c012769f | ||
|
f66e0b812f | ||
|
56e0ed8f5d | ||
|
bacb8c8380 | ||
|
59a5f7b4fc | ||
|
21ea50279e | ||
|
0245eb59a2 | ||
|
872fe8fef3 | ||
|
d0b4d77519 |
317 changed files with 10390 additions and 12613 deletions
21
.dockerignore
Normal file
21
.dockerignore
Normal file
|
@ -0,0 +1,21 @@
|
|||
# flyctl launch added from .gitignore
|
||||
**/.env.local
|
||||
**/config.yaml
|
||||
target
|
||||
|
||||
# other things
|
||||
docs/*
|
||||
fedimovies-*
|
||||
scripts/*
|
||||
src/*
|
||||
|
||||
# flyctl launch added from .idea/.gitignore
|
||||
# Default ignored files
|
||||
.idea/shelf
|
||||
.idea/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
.idea/httpRequests
|
||||
# Datasource local storage ignored files
|
||||
.idea/dataSources
|
||||
.idea/dataSources.local.xml
|
||||
fly.toml
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,5 +1,9 @@
|
|||
.env.local
|
||||
config.yaml
|
||||
/secret/*
|
||||
/files/*
|
||||
!/files/.gitkeep
|
||||
/build/*
|
||||
!/build/.gitkeep
|
||||
/target
|
||||
fly.toml
|
||||
|
|
63
.woodpecker.yml
Normal file
63
.woodpecker.yml
Normal file
|
@ -0,0 +1,63 @@
|
|||
matrix:
|
||||
RUST: [stable]
|
||||
|
||||
pipeline:
|
||||
|
||||
check-formatting:
|
||||
image: rust
|
||||
when:
|
||||
branch: [ main ]
|
||||
path:
|
||||
include:
|
||||
- .woodpecker.yml
|
||||
- src/**/*.rs
|
||||
- fedimovies-cli/**/*.rs
|
||||
- fedimovies-config/**/*.rs
|
||||
- fedimovies-models/**/*.rs
|
||||
- fedimovies-utils/**/*.rs
|
||||
environment:
|
||||
- CARGO_TERM_COLOR=always
|
||||
- CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
|
||||
commands:
|
||||
- rustup default $RUST
|
||||
- rustup component add rustfmt
|
||||
- cargo fmt --all -- --check
|
||||
|
||||
check-style:
|
||||
image: rust
|
||||
when:
|
||||
branch: [ main ]
|
||||
path:
|
||||
include:
|
||||
- .woodpecker.yml
|
||||
- src/**/*.rs
|
||||
- fedimovies-cli/**/*.rs
|
||||
- fedimovies-config/**/*.rs
|
||||
- fedimovies-models/**/*.rs
|
||||
- fedimovies-utils/**/*.rs
|
||||
environment:
|
||||
- CARGO_TERM_COLOR=always
|
||||
- CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
|
||||
commands:
|
||||
- rustup default $RUST
|
||||
- rustup component add clippy
|
||||
- cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
run-tests:
|
||||
image: rust
|
||||
when:
|
||||
branch: [ main ]
|
||||
path:
|
||||
include:
|
||||
- .woodpecker.yml
|
||||
- src/**/*.rs
|
||||
- fedimovies-cli/**/*.rs
|
||||
- fedimovies-config/**/*.rs
|
||||
- fedimovies-models/**/*.rs
|
||||
- fedimovies-utils/**/*.rs
|
||||
environment:
|
||||
- CARGO_TERM_COLOR=always
|
||||
- CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
|
||||
commands:
|
||||
- rustup default $RUST
|
||||
- cargo test --all -- --nocapture
|
203
CHANGELOG.md
203
CHANGELOG.md
|
@ -6,6 +6,209 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.22.0] - 2023-04-22
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for content warnings.
|
||||
- Support integrity proofs with `DataIntegrityProof` type.
|
||||
- Add `federation.i2p_proxy_url` configuration parameter.
|
||||
|
||||
### Changed
|
||||
|
||||
- Ignore errors when importing activities from outbox.
|
||||
- Make activity limit in outbox fetcher adjustable.
|
||||
- Updated actix to latest version. MSRV changed to 1.57.
|
||||
- Add replies and reposts to outbox collection.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Make `/api/v1/accounts/{account_id}/follow` work with form-data.
|
||||
- Make `onion_proxy_url` override `proxy_url` setting if request target is onion.
|
||||
|
||||
## [1.21.0] - 2023-04-12
|
||||
|
||||
### Added
|
||||
|
||||
- Added `create-user` command.
|
||||
- Added `read-outbox` command.
|
||||
|
||||
### Changed
|
||||
|
||||
- Added emoji count check to profile data validator.
|
||||
- Check mention and link counts when creating post.
|
||||
- Re-fetch object if `attributedTo` value doesn't match `actor` of `Create` activity.
|
||||
- Added actor validation to `Update(Note)` and `Undo(Follow)` handlers.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed database query error in `Create` activity handler.
|
||||
|
||||
## [1.20.0] - 2023-04-07
|
||||
|
||||
### Added
|
||||
|
||||
- Support calling `/api/v1/accounts/search` with `resolve` parameter.
|
||||
- Created `/api/v1/accounts/aliases/all` API endpoint.
|
||||
- Created API endpoint for adding aliases.
|
||||
- Populate `alsoKnownAs` property on actor object with declared aliases.
|
||||
- Support account migration from Mastodon.
|
||||
- Created API endpoint for managing client configurations.
|
||||
- Reject unsolicited public posts.
|
||||
|
||||
### Changed
|
||||
|
||||
- Increase maximum number of custom emojis per post to 50.
|
||||
- Validate actor aliases before saving into database.
|
||||
- Process incoming `Move()` activities in background.
|
||||
- Allow custom emojis with `image/webp` media type.
|
||||
- Increase object ID size limit to 2000 chars.
|
||||
- Increase fetcher timeout to 15 seconds when processing search queries.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Added missing `CHECK` constraints to database tables.
|
||||
- Validate object ID length before saving post to database.
|
||||
- Validate emoji name length before saving to database.
|
||||
|
||||
## [1.19.1] - 2023-03-31
|
||||
|
||||
### Changed
|
||||
|
||||
- Limit number of mentions and links in remote posts.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Process queued background jobs before re-trying stalled.
|
||||
- Remove activity from queue if handler times out.
|
||||
- Order attachments by creation date when new post is created.
|
||||
|
||||
## [1.19.0] - 2023-03-30
|
||||
|
||||
### Added
|
||||
|
||||
- Added `prune-remote-emojis` command.
|
||||
- Prune remote emojis in background.
|
||||
- Added `limits.media.emoji_size_limit` configuration parameter.
|
||||
- Added `federation.fetcher_timeout` and `federation.deliverer_timeout` configuration parameters.
|
||||
|
||||
### Changed
|
||||
|
||||
- Allow emoji names containing hyphens.
|
||||
- Increased remote emoji size limit to 500 kB.
|
||||
- Set fetcher timeout to 5 seconds when processing search queries.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed error in emoji update SQL query.
|
||||
- Restart stalled background jobs.
|
||||
- Order attachments by creation date.
|
||||
- Don't reopen monero wallet on each subscription monitor run.
|
||||
|
||||
### Security
|
||||
|
||||
- Updated markdown parser to latest version.
|
||||
|
||||
## [1.18.0] - 2023-03-21
|
||||
|
||||
### Added
|
||||
|
||||
- Added `fep-e232` feature flag (disabled by default).
|
||||
- Added `account_index` parameter to Monero configuration.
|
||||
- Added `/api/v1/instance/peers` API endpoint.
|
||||
- Added `federation.enabled` configuration parameter that can be used to disable federation.
|
||||
|
||||
### Changed
|
||||
|
||||
- Documented valid role names for `set-role` command.
|
||||
- Granted `delete_any_post` and `delete_any_profile` permissions to admin role.
|
||||
- Updated profile page URL template to match mitra-web.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Make webclient-to-object redirects work for remote profiles and posts.
|
||||
- Added webclient redirection rule for `/@username` routes.
|
||||
- Don't allow migration if user doesn't have identity proofs.
|
||||
|
||||
## [1.17.0] - 2023-03-15
|
||||
|
||||
### Added
|
||||
|
||||
- Enabled audio and video uploads.
|
||||
- Added `audio/ogg` and `audio/x-wav` to the list of supported media types.
|
||||
|
||||
### Changed
|
||||
|
||||
- Save latest ethereum block number to database instead of file.
|
||||
- Removed hardcoded upload size limit.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Reading ethereum block number from `current_block` file.
|
||||
|
||||
### Removed
|
||||
|
||||
- Disabled post tokenization (can be re-enabled with `ethereum-extras` feature).
|
||||
- Removed ability to switch from Ethereum devnet to another chain without resetting subscriptions.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Allow `!` after hashtags and mentions.
|
||||
- Ignore emojis with non-unique names in remote posts.
|
||||
|
||||
## [1.16.0] - 2023-03-08
|
||||
|
||||
### Added
|
||||
|
||||
- Allow to add notes to generated invite codes.
|
||||
- Added `registration.default_role` configuration option.
|
||||
- Save emojis attached to actor objects.
|
||||
- Added `emojis` field to Mastodon API Account entity.
|
||||
- Support audio attachments.
|
||||
- Added CLI command for viewing unreachable actors.
|
||||
- Implemented NodeInfo 2.1.
|
||||
- Added `federation.onion_proxy_url` configuration parameter (enables proxy for requests to `.onion` domains).
|
||||
|
||||
### Changed
|
||||
|
||||
- Use .jpg extension for files with image/jpeg media type.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Deprecated `default_role_read_only_user` configuration option (replaced by `registration.default_role`).
|
||||
|
||||
## [1.15.0] - 2023-02-27
|
||||
|
||||
### Added
|
||||
|
||||
- Set fetcher timeout to 3 minutes.
|
||||
- Set deliverer timeout to 30 seconds.
|
||||
- Added `federation` parameter group to configuration.
|
||||
- Add empty `spoiler_text` property to Mastodon API Status object.
|
||||
- Added `error` and `error_description` fields to Mastodon API error responses.
|
||||
- Store information about failed activity deliveries in database.
|
||||
- Added `/api/v1/accounts/{account_id}/aliases` API endpoint.
|
||||
|
||||
### Changed
|
||||
|
||||
- Put activities generated by CLI commands in a queue instead of immediately sending them.
|
||||
- Changed path of user's Atom feed to `/feeds/users/{username}`.
|
||||
- Increase number of delivery attempts and increase intervals between them.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Deprecated `proxy_url` configuration parameter (replaced by `federation.proxy_url`).
|
||||
- Deprecated Atom feeds at `/feeds/{username}`.
|
||||
- Deprecated `message` field in Mastodon API error response.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Prevent `delete-extraneous-posts` command from removing locally-linked posts.
|
||||
- Make webfinger response compatible with GNU Social account lookup.
|
||||
- Prefer `Group` actor when doing webfinger query on Lemmy server.
|
||||
- Fetch missing profiles before doing follower migration.
|
||||
- Follow FEP-e232 links when importing post.
|
||||
|
||||
## [1.14.0] - 2023-02-22
|
||||
|
||||
### Added
|
||||
|
|
2356
Cargo.lock
generated
2356
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
63
Cargo.toml
63
Cargo.toml
|
@ -1,34 +1,39 @@
|
|||
[package]
|
||||
name = "mitra"
|
||||
version = "1.14.0"
|
||||
description = "Federated micro-blogging platform and content subscription service"
|
||||
name = "fedimovies"
|
||||
version = "1.22.0"
|
||||
description = "Movies reviews and ratings for the fediverse"
|
||||
license = "AGPL-3.0"
|
||||
|
||||
edition = "2021"
|
||||
rust-version = "1.56"
|
||||
rust-version = "1.68"
|
||||
publish = false
|
||||
default-run = "mitra"
|
||||
default-run = "fedimovies"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
".",
|
||||
"mitra-cli",
|
||||
"mitra-config",
|
||||
"mitra-utils",
|
||||
"fedimovies-cli",
|
||||
"fedimovies-config",
|
||||
"fedimovies-models",
|
||||
"fedimovies-utils",
|
||||
]
|
||||
default-members = [
|
||||
".",
|
||||
"mitra-cli",
|
||||
"fedimovies-cli",
|
||||
"fedimovies-config",
|
||||
"fedimovies-models",
|
||||
"fedimovies-utils",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
mitra-config = { path = "mitra-config" }
|
||||
mitra-utils = { path = "mitra-utils" }
|
||||
fedimovies-config = { path = "fedimovies-config" }
|
||||
fedimovies-models = { path = "fedimovies-models" }
|
||||
fedimovies-utils = { path = "fedimovies-utils" }
|
||||
|
||||
# Used to handle incoming HTTP requests
|
||||
actix-cors = "0.6.2"
|
||||
actix-cors = "0.6.4"
|
||||
actix-files = "0.6.2"
|
||||
actix-web = "4.1.0"
|
||||
actix-web = "4.3.1"
|
||||
actix-web-httpauth = "0.8.0"
|
||||
# Used for catching errors
|
||||
anyhow = "1.0.58"
|
||||
|
@ -36,9 +41,6 @@ anyhow = "1.0.58"
|
|||
base64 = "0.13.0"
|
||||
# Used for working with dates
|
||||
chrono = { version = "0.4.23", default-features = false, features = ["std", "serde"] }
|
||||
# Used for pooling database connections
|
||||
deadpool = "0.9.2"
|
||||
deadpool-postgres = { version = "0.10.2", default-features = false }
|
||||
# Used to work with hexadecimal strings
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
# Used for logging
|
||||
|
@ -46,22 +48,16 @@ log = "0.4.14"
|
|||
env_logger = { version = "0.9.0", default-features = false }
|
||||
# Used to verify minisign signatures
|
||||
ed25519-dalek = "1.0.1"
|
||||
ed25519 = "1.5.2"
|
||||
ed25519 = "1.5.3"
|
||||
blake2 = "0.10.5"
|
||||
# Used to query Monero node
|
||||
monero-rpc = "0.3.2"
|
||||
# Used to determine the number of CPUs on the system
|
||||
num_cpus = "1.13.0"
|
||||
# Used for working with regular expressions
|
||||
regex = "1.6.0"
|
||||
# Used for managing database migrations
|
||||
refinery = { version = "0.8.4", features = ["tokio-postgres"] }
|
||||
# Used for making async HTTP requests
|
||||
reqwest = { version = "0.11.10", features = ["json", "multipart", "socks"] }
|
||||
reqwest = { version = "0.11.13", features = ["json", "multipart", "socks"] }
|
||||
# Used for working with RSA keys
|
||||
rsa = "0.5.0"
|
||||
# Used for working with ethereum keys
|
||||
secp256k1 = { version = "0.21.3", features = ["rand", "rand-std"] }
|
||||
# Used for serialization/deserialization
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
|
@ -72,25 +68,18 @@ siwe = "0.4.0"
|
|||
# Used for creating error types
|
||||
thiserror = "1.0.37"
|
||||
# Async runtime
|
||||
tokio = { version = "1.17.0", features = ["macros"] }
|
||||
# Used for working with Postgresql database
|
||||
tokio-postgres = { version = "0.7.6", features = ["with-chrono-0_4", "with-uuid-1", "with-serde_json-1"] }
|
||||
postgres-types = { version = "0.2.3", features = ["derive", "with-chrono-0_4", "with-uuid-1", "with-serde_json-1"] }
|
||||
postgres-protocol = "0.6.4"
|
||||
# Used to construct PostgreSQL queries
|
||||
postgres_query = { git = "https://github.com/nolanderc/rust-postgres-query", rev = "b4422051c8a31fbba4a35f88004c1cefb1878dd5" }
|
||||
postgres_query_macro = { git = "https://github.com/nolanderc/rust-postgres-query", rev = "b4422051c8a31fbba4a35f88004c1cefb1878dd5" }
|
||||
tokio = { version = "=1.20.4", features = ["macros"] }
|
||||
# Used to work with URLs
|
||||
url = "2.2.2"
|
||||
# Used to work with UUIDs
|
||||
uuid = { version = "1.1.2", features = ["serde", "v4"] }
|
||||
# Used to query ethereum node
|
||||
web3 = { version = "0.18.0", default-features = false, features = ["http", "http-tls", "signing"] }
|
||||
|
||||
[dev-dependencies]
|
||||
mitra-config = { path = "mitra-config", features = ["test-utils"] }
|
||||
mitra-utils = { path = "mitra-utils", features = ["test-utils"] }
|
||||
fedimovies-config = { path = "fedimovies-config", features = ["test-utils"] }
|
||||
fedimovies-models = { path = "fedimovies-models", features = ["test-utils"] }
|
||||
fedimovies-utils = { path = "fedimovies-utils", features = ["test-utils"] }
|
||||
|
||||
serial_test = "0.7.0"
|
||||
|
||||
[features]
|
||||
production = ["mitra-config/production"]
|
||||
production = ["fedimovies-config/production"]
|
||||
|
|
|
@ -86,6 +86,10 @@ Canonicalization algorithm: JCS
|
|||
Hashing algorithm: BLAKE2b-512
|
||||
Signature algorithm: EdDSA
|
||||
|
||||
## Custom emojis
|
||||
|
||||
Custom emojis are implemented as described in Mastodon documentation: https://docs.joinmastodon.org/spec/activitypub/#emoji.
|
||||
|
||||
## Profile extensions
|
||||
|
||||
### Cryptocurrency addresses
|
||||
|
|
113
README.md
113
README.md
|
@ -1,47 +1,38 @@
|
|||
# Mitra
|
||||
# FediMovies
|
||||
[](https://ci.caric.io/FediMovies/fedimovies)
|
||||
|
||||
Federated micro-blogging platform and content subscription service.
|
||||
Lively federated movies reviews platform.
|
||||
|
||||
Built on [ActivityPub](https://www.w3.org/TR/activitypub/) protocol, self-hosted, lightweight. Part of the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
|
||||
|
||||
Subscriptions provide a way to receive monthly payments from subscribers and to publish private content made exclusively for them.
|
||||
Features:
|
||||
|
||||
Supported payment methods:
|
||||
- Micro-blogging service (includes support for quote posts, custom emojis and more).
|
||||
- Mastodon API.
|
||||
- Account migrations (from one server to another). Identity can be detached from the server.
|
||||
- Federation over Tor.
|
||||
|
||||
- [Monero](https://www.getmonero.org/get-started/what-is-monero/).
|
||||
- [ERC-20](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/) tokens (on Ethereum and other EVM-compatible blockchains).
|
||||
## Instances
|
||||
|
||||
Other features:
|
||||
- [FediList](http://demo.fedilist.com/instance?software=fedimovies)
|
||||
- [Fediverse Observer](https://fedimovies.fediverse.observer/list)
|
||||
|
||||
- [Sign-in with a wallet](https://eips.ethereum.org/EIPS/eip-4361).
|
||||
- Account migrations (from one server to another).
|
||||
- Donation buttons.
|
||||
|
||||
Follow: [@mitra@mitra.social](https://mitra.social/@mitra)
|
||||
|
||||
Matrix chat: [#mitra:halogen.city](https://matrix.to/#/#mitra:halogen.city)
|
||||
|
||||
Network stats: [FediList](http://demo.fedilist.com/instance?software=mitra) / [Fediverse Observer](https://mitra.fediverse.observer/list)
|
||||
|
||||
Demo instance: https://public.mitra.social/ ([invite-only](https://public.mitra.social/about))
|
||||
Demo instance: https://nullpointer.social/ ([invite-only](https://nullpointer.social/about))
|
||||
|
||||
## Code
|
||||
|
||||
Server: https://codeberg.org/silverpill/mitra (this repo)
|
||||
Server: https://code.caric.io/reef/reef (this repo)
|
||||
|
||||
Web client: https://codeberg.org/silverpill/mitra-web
|
||||
Web client:
|
||||
|
||||
Ethereum contracts: https://codeberg.org/silverpill/mitra-contracts
|
||||
|
||||
## Requirements
|
||||
|
||||
- Rust 1.56+ (when building from source)
|
||||
- Rust 1.57+ (when building from source)
|
||||
- PostgreSQL 12+
|
||||
|
||||
Optional:
|
||||
|
||||
- Monero node and Monero wallet service
|
||||
- Ethereum node
|
||||
- IPFS node (see [guide](./docs/ipfs.md))
|
||||
|
||||
## Installation
|
||||
|
@ -54,71 +45,57 @@ Run:
|
|||
cargo build --release --features production
|
||||
```
|
||||
|
||||
This command will produce two binaries in `target/release` directory, `mitra` and `mitractl`.
|
||||
This command will produce two binaries in `target/release` directory, `fedimovies` and `fedimoviesctl`.
|
||||
|
||||
Install PostgreSQL and create the database:
|
||||
|
||||
```sql
|
||||
CREATE USER mitra WITH PASSWORD 'mitra';
|
||||
CREATE DATABASE mitra OWNER mitra;
|
||||
CREATE USER fedimovies WITH PASSWORD 'fedimovies';
|
||||
CREATE DATABASE fedimovies OWNER fedimovies;
|
||||
```
|
||||
|
||||
Create configuration file by copying `contrib/mitra_config.yaml` and configure the instance. Default config file path is `/etc/mitra/config.yaml`, but it can be changed using `CONFIG_PATH` environment variable.
|
||||
Create configuration file by copying `contrib/fedimovies_config.yaml` and configure the instance. Default config file path is `/etc/fedimovies/config.yaml`, but it can be changed using `CONFIG_PATH` environment variable.
|
||||
|
||||
Put any static files into the directory specified in configuration file. Building instructions for `mitra-web` frontend can be found at https://codeberg.org/silverpill/mitra-web#project-setup.
|
||||
Put any static files into the directory specified in configuration file. Building instructions for `fedimovies-web` frontend can be found at https://code.caric.io/FediMovies/fedimovies#project-setup.
|
||||
|
||||
Start Mitra:
|
||||
Start Fedimovies:
|
||||
|
||||
```shell
|
||||
./mitra
|
||||
./fedimovies
|
||||
```
|
||||
|
||||
An HTTP server will be needed to handle HTTPS requests. See the example of [nginx configuration file](./contrib/mitra.nginx).
|
||||
An HTTP server will be needed to handle HTTPS requests. See the example of [nginx configuration file](./contrib/fedimovies.nginx).
|
||||
|
||||
To run Mitra as a systemd service, check out the [systemd unit file example](./contrib/mitra.service).
|
||||
To run Fedimovies as a systemd service, check out the [systemd unit file example](./contrib/fedimovies.service).
|
||||
|
||||
### Debian package
|
||||
|
||||
Download and install Mitra package:
|
||||
Download and install Fedimovies package:
|
||||
|
||||
```shell
|
||||
dpkg -i mitra.deb
|
||||
dpkg -i fedimovies.deb
|
||||
```
|
||||
|
||||
Install PostgreSQL and create the database:
|
||||
|
||||
```sql
|
||||
CREATE USER mitra WITH PASSWORD 'mitra';
|
||||
CREATE DATABASE mitra OWNER mitra;
|
||||
CREATE USER fedimovies WITH PASSWORD 'fedimovies';
|
||||
CREATE DATABASE fedimovies OWNER fedimovies;
|
||||
```
|
||||
|
||||
Open configuration file `/etc/mitra/config.yaml` and configure the instance.
|
||||
Open configuration file `/etc/fedimovies/config.yaml` and configure the instance.
|
||||
|
||||
Start Mitra:
|
||||
Start Fedimovies:
|
||||
|
||||
```shell
|
||||
systemctl start mitra
|
||||
systemctl start fedimovies
|
||||
```
|
||||
|
||||
An HTTP server will be needed to handle HTTPS requests. See the example of [nginx configuration file](./contrib/mitra.nginx).
|
||||
An HTTP server will be needed to handle HTTPS requests. See the example of [nginx configuration file](./contrib/fedimovies.nginx).
|
||||
|
||||
### Monero
|
||||
### Tor federation
|
||||
|
||||
Install Monero node or choose a [public one](https://monero.fail/).
|
||||
|
||||
Configure and start [monero-wallet-rpc](https://monerodocs.org/interacting/monero-wallet-rpc-reference/) daemon. Add `disable-rpc-login=1` to your `monero-wallet-rpc` config (currently RPC auth is not supported in Mitra).
|
||||
|
||||
Create a wallet for your instance.
|
||||
|
||||
Add blockchain configuration to `blockchains` array in your configuration file.
|
||||
|
||||
### Ethereum
|
||||
|
||||
Install Ethereum client or choose a JSON-RPC API provider.
|
||||
|
||||
Deploy contracts on the blockchain. Instructions can be found at https://codeberg.org/silverpill/mitra-contracts.
|
||||
|
||||
Add blockchain configuration to `blockchains` array in your configuration file.
|
||||
See [guide](./docs/onion.md).
|
||||
|
||||
## Development
|
||||
|
||||
|
@ -133,15 +110,7 @@ docker-compose up -d
|
|||
Test connection:
|
||||
|
||||
```shell
|
||||
psql -h localhost -p 55432 -U mitra mitra
|
||||
```
|
||||
|
||||
### Start Monero node and wallet server
|
||||
|
||||
(this step is optional)
|
||||
|
||||
```shell
|
||||
docker-compose --profile monero up -d
|
||||
psql -h localhost -p 55432 -U fedimovies fedimovies
|
||||
```
|
||||
|
||||
### Run web service
|
||||
|
@ -161,7 +130,7 @@ cargo run
|
|||
### Run CLI
|
||||
|
||||
```shell
|
||||
cargo run --bin mitractl
|
||||
cargo run --bin fedimoviesctl
|
||||
```
|
||||
|
||||
### Run linter
|
||||
|
@ -182,20 +151,16 @@ See [FEDERATION.md](./FEDERATION.md)
|
|||
|
||||
## Client API
|
||||
|
||||
Most methods are similar to Mastodon API, but Mitra is not fully compatible.
|
||||
Most methods are similar to Mastodon API, but Fedimovies is not fully compatible.
|
||||
|
||||
[OpenAPI spec](./docs/openapi.yaml)
|
||||
|
||||
## CLI
|
||||
|
||||
`mitractl` is a command-line tool for performing instance maintenance.
|
||||
`fedimoviesctl` is a command-line tool for performing instance maintenance.
|
||||
|
||||
[Documentation](./docs/mitractl.md)
|
||||
[Documentation](./docs/fedimoviesctl.md)
|
||||
|
||||
## License
|
||||
|
||||
[AGPL-3.0](./LICENSE)
|
||||
|
||||
## Support
|
||||
|
||||
Monero: 8Ahza5RM4JQgtdqvpcF1U628NN5Q87eryXQad3Fy581YWTZU8o3EMbtScuioQZSkyNNEEE1Lkj2cSbG4VnVYCW5L1N4os5p
|
||||
|
|
0
build/.gitkeep
Normal file
0
build/.gitkeep
Normal file
|
@ -14,28 +14,5 @@ instance_description: My instance
|
|||
registration:
|
||||
type: open
|
||||
|
||||
blockchains:
|
||||
# Parameters for hardhat local node
|
||||
- chain_id: eip155:31337
|
||||
chain_metadata:
|
||||
chain_name: localhost
|
||||
currency_name: ETH
|
||||
currency_symbol: ETH
|
||||
currency_decimals: 18
|
||||
public_api_url: 'http://127.0.0.1:8546'
|
||||
explorer_url: null
|
||||
contract_address: '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9'
|
||||
contract_dir: contracts
|
||||
api_url: 'http://127.0.0.1:8546'
|
||||
signing_key: 'ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
|
||||
chain_sync_step: 100
|
||||
chain_reorg_max_depth: 0
|
||||
# # Parameters for local Monero node
|
||||
# - chain_id: monero:regtest
|
||||
# node_url: 'http://127.0.0.1:58081'
|
||||
# wallet_url: 'http://127.0.0.1:58083'
|
||||
# wallet_name: test
|
||||
# wallet_password: test
|
||||
|
||||
ipfs_api_url: 'http://127.0.0.1:5001'
|
||||
ipfs_gateway_url: 'http://127.0.0.1:8001'
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"_format": "hh-sol-artifact-1",
|
||||
"contractName": "IERC165",
|
||||
"sourceName": "@openzeppelin/contracts/utils/introspection/IERC165.sol",
|
||||
"abi": [
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes4",
|
||||
"name": "interfaceId",
|
||||
"type": "bytes4"
|
||||
}
|
||||
],
|
||||
"name": "supportsInterface",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"bytecode": "0x",
|
||||
"deployedBytecode": "0x",
|
||||
"linkReferences": {},
|
||||
"deployedLinkReferences": {}
|
||||
}
|
|
@ -1,233 +0,0 @@
|
|||
{
|
||||
"_format": "hh-sol-artifact-1",
|
||||
"contractName": "IERC20Metadata",
|
||||
"sourceName": "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol",
|
||||
"abi": [
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Approval",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "allowance",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "approve",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "account",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "balanceOf",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "decimals",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint8",
|
||||
"name": "",
|
||||
"type": "uint8"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "name",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "symbol",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "totalSupply",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transfer",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "sender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transferFrom",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"bytecode": "0x",
|
||||
"deployedBytecode": "0x",
|
||||
"linkReferences": {},
|
||||
"deployedLinkReferences": {}
|
||||
}
|
|
@ -1,341 +0,0 @@
|
|||
{
|
||||
"_format": "hh-sol-artifact-1",
|
||||
"contractName": "IERC721Metadata",
|
||||
"sourceName": "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol",
|
||||
"abi": [
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "approved",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "uint256",
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Approval",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "operator",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bool",
|
||||
"name": "approved",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"name": "ApprovalForAll",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "uint256",
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "approve",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "balanceOf",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "balance",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "getApproved",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "operator",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "operator",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "isApprovedForAll",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "name",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "ownerOf",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "safeTransferFrom",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "data",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "safeTransferFrom",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "operator",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "_approved",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"name": "setApprovalForAll",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes4",
|
||||
"name": "interfaceId",
|
||||
"type": "bytes4"
|
||||
}
|
||||
],
|
||||
"name": "supportsInterface",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "symbol",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "tokenURI",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transferFrom",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"bytecode": "0x",
|
||||
"deployedBytecode": "0x",
|
||||
"linkReferences": {},
|
||||
"deployedLinkReferences": {}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"_format": "hh-sol-artifact-1",
|
||||
"contractName": "IGate",
|
||||
"sourceName": "contracts/interfaces/IGate.sol",
|
||||
"abi": [
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "user",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "isAllowedUser",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"bytecode": "0x",
|
||||
"deployedBytecode": "0x",
|
||||
"linkReferences": {},
|
||||
"deployedLinkReferences": {}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
{
|
||||
"_format": "hh-sol-artifact-1",
|
||||
"contractName": "IMinter",
|
||||
"sourceName": "contracts/interfaces/IMinter.sol",
|
||||
"abi": [
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "collectible",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "contract IERC721Metadata",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "user",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "tokenURI",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"internalType": "uint8",
|
||||
"name": "v",
|
||||
"type": "uint8"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "r",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "s",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "mint",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"bytecode": "0x",
|
||||
"deployedBytecode": "0x",
|
||||
"linkReferences": {},
|
||||
"deployedLinkReferences": {}
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
{
|
||||
"_format": "hh-sol-artifact-1",
|
||||
"contractName": "ISubscription",
|
||||
"sourceName": "contracts/interfaces/ISubscription.sol",
|
||||
"abi": [
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "sender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "expires_at",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "UpdateSubscription",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "cancel",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "send",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "sender",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "withdrawReceived",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "withdrawReceivedAll",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"bytecode": "0x",
|
||||
"deployedBytecode": "0x",
|
||||
"linkReferences": {},
|
||||
"deployedLinkReferences": {}
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
{
|
||||
"_format": "hh-sol-artifact-1",
|
||||
"contractName": "ISubscriptionAdapter",
|
||||
"sourceName": "contracts/interfaces/ISubscriptionAdapter.sol",
|
||||
"abi": [
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "price",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "uint8",
|
||||
"name": "v",
|
||||
"type": "uint8"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "r",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "s",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "configureSubscription",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "getSubscriptionPrice",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "sender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "getSubscriptionState",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "isSubscriptionConfigured",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "subscription",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "contract ISubscription",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "subscriptionToken",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "contract IERC20Metadata",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"bytecode": "0x",
|
||||
"deployedBytecode": "0x",
|
||||
"linkReferences": {},
|
||||
"deployedLinkReferences": {}
|
||||
}
|
16
contrib/Dockerfile
Normal file
16
contrib/Dockerfile
Normal file
|
@ -0,0 +1,16 @@
|
|||
FROM ubuntu:23.04
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
wget \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /var/lib/data
|
||||
|
||||
COPY build/fedimovies /usr/local/bin
|
||||
COPY build/fedimoviesctl /usr/local/bin
|
||||
COPY secret/fedimovies.conf /etc/fedimovies.conf
|
||||
COPY files /www/frontend/
|
||||
|
||||
CMD ["/usr/local/bin/fedimovies"]
|
|
@ -36,7 +36,7 @@ server {
|
|||
add_header Content-Security-Policy "default-src 'none'; connect-src 'self'; img-src 'self' data:; media-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'nonce-oauth-authorization'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'";
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
|
||||
client_max_body_size 10M;
|
||||
client_max_body_size 40M;
|
||||
|
||||
location / {
|
||||
# Frontend
|
||||
|
|
|
@ -32,7 +32,7 @@ server {
|
|||
|
||||
add_header Strict-Transport-Security "max-age=63072000" always;
|
||||
|
||||
client_max_body_size 10M;
|
||||
client_max_body_size 40M;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8383;
|
||||
|
|
|
@ -20,18 +20,19 @@ instance_uri: https://example.tld
|
|||
instance_title: example
|
||||
instance_short_description: my instance
|
||||
# Long description can contain markdown syntax
|
||||
instance_description: my instance
|
||||
instance_description: |
|
||||
# My instance
|
||||
Welcome!
|
||||
|
||||
registration:
|
||||
# Possible values: open, invite
|
||||
type: invite
|
||||
# Possible values: user, read_only_user
|
||||
default_role: user
|
||||
|
||||
# EIP-4361 login message
|
||||
#login_message: 'Do not sign this message on other sites!'
|
||||
|
||||
# Proxy for outgoing requests
|
||||
#proxy_url: 'socks5h://127.0.0.1:9050'
|
||||
|
||||
# Limits
|
||||
#limits:
|
||||
# media:
|
||||
|
@ -40,38 +41,21 @@ registration:
|
|||
# character_limit: 2000
|
||||
|
||||
# Data retention parameters
|
||||
#retention:
|
||||
# extraneous_posts: 50
|
||||
# empty_profiles: 150
|
||||
retention:
|
||||
extraneous_posts: 50
|
||||
empty_profiles: 150
|
||||
|
||||
# Federation parameters
|
||||
#federation:
|
||||
# enabled: true
|
||||
# # Proxy for outgoing requests
|
||||
# #proxy_url: 'socks5h://127.0.0.1:9050'
|
||||
# # Proxy for outgoing requests to .onion targets
|
||||
# #onion_proxy_url: 'socks5h://127.0.0.1:9050'
|
||||
|
||||
# List of blocked domains
|
||||
#blocked_instances: []
|
||||
|
||||
# Blockchain integrations
|
||||
# Multiple configuration are currently not allowed.
|
||||
# Chain metadata for EVM chains can be found at https://github.com/ethereum-lists/chains
|
||||
# Signing key for ethereum integration can be generated with `mitractl generate-ethereum-address`
|
||||
#blockchains:
|
||||
# - chain_id: monero:mainnet
|
||||
# node_url: 'http://opennode.xmr-tw.org:18089'
|
||||
# wallet_url: 'http://127.0.0.1:18083'
|
||||
# wallet_name: null
|
||||
# wallet_password: null
|
||||
# - chain_id: eip155:31337
|
||||
# chain_metadata:
|
||||
# chain_name: localhost
|
||||
# currency_name: ETH
|
||||
# currency_symbol: ETH
|
||||
# currency_decimals: 18
|
||||
# public_api_url: 'http://127.0.0.1:8545'
|
||||
# explorer_url: null
|
||||
# contract_address: '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9'
|
||||
# contract_dir: /usr/share/mitra/contracts
|
||||
# api_url: 'http://127.0.0.1:8545'
|
||||
# signing_key: null
|
||||
# chain_sync_step: 1000
|
||||
# chain_reorg_max_depth: 10
|
||||
|
||||
# IPFS integration
|
||||
#ipfs_api_url: 'http://127.0.0.1:5001'
|
||||
# IPFS gateway (for clients)
|
||||
|
|
|
@ -20,10 +20,10 @@ Generate RSA private key:
|
|||
mitractl generate-rsa-key
|
||||
```
|
||||
|
||||
Generate invite code:
|
||||
Generate invite code (note is optional):
|
||||
|
||||
```shell
|
||||
mitractl generate-invite-code
|
||||
mitractl generate-invite-code <note>
|
||||
```
|
||||
|
||||
List generated invites:
|
||||
|
@ -32,13 +32,19 @@ List generated invites:
|
|||
mitractl list-invite-codes
|
||||
```
|
||||
|
||||
Create user:
|
||||
|
||||
```shell
|
||||
mitractl create-user <username> <password> <role-name>
|
||||
```
|
||||
|
||||
Set or change password:
|
||||
|
||||
```shell
|
||||
mitractl set-password <user-id> <password>
|
||||
```
|
||||
|
||||
Change user's role:
|
||||
Change user's role (admin, user or read_only_user).
|
||||
|
||||
```shell
|
||||
mitractl set-role <user-id> <role-name>
|
||||
|
@ -80,6 +86,12 @@ Delete empty remote profiles:
|
|||
mitractl delete-empty-profiles 100
|
||||
```
|
||||
|
||||
Delete unused remote emojis:
|
||||
|
||||
```shell
|
||||
mitractl prune-remote-emojis
|
||||
```
|
||||
|
||||
Import custom emoji from another instance:
|
||||
|
||||
```shell
|
||||
|
|
42
docs/onion.md
Normal file
42
docs/onion.md
Normal file
|
@ -0,0 +1,42 @@
|
|||
# Tor federation
|
||||
|
||||
## Tor-only instance
|
||||
|
||||
Install Tor.
|
||||
|
||||
Install Mitra. Uncomment or add the following block to Mitra configuration file:
|
||||
|
||||
```yaml
|
||||
federation:
|
||||
proxy_url: 'socks5h://127.0.0.1:9050'
|
||||
```
|
||||
|
||||
Where `127.0.0.1:9050` is the address and the port where Tor proxy is listening.
|
||||
|
||||
Configure the onion service by adding these lines to `torrc` configuration file:
|
||||
|
||||
```
|
||||
HiddenServiceDir /var/lib/tor/mitra/
|
||||
HiddenServicePort 80 127.0.0.1:8383
|
||||
```
|
||||
|
||||
Where `8383` should correspond to `http_port` setting in Mitra configuration file.
|
||||
|
||||
Restart the Tor service. Inside the `HiddenServiceDir` directory find the `hostname` file. This file contains the hostname of your onion service. Change the value of `instance_uri` parameter in Mitra configuration file to that hostname (it should end with `.onion`).
|
||||
|
||||
Start Mitra.
|
||||
|
||||
For more information about running onion services, visit https://community.torproject.org/onion-services/setup/
|
||||
|
||||
## Clearnet + Tor
|
||||
|
||||
Clearnet instances can federate with Tor-only instances.
|
||||
|
||||
Add the following block to Mitra configuration file:
|
||||
|
||||
```yaml
|
||||
federation:
|
||||
onion_proxy_url: 'socks5h://127.0.0.1:9050'
|
||||
```
|
||||
|
||||
Where `127.0.0.1:9050` is the address and the port where Tor proxy is listening.
|
|
@ -352,6 +352,12 @@ paths:
|
|||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: resolve
|
||||
in: query
|
||||
description: Attempt WebFinger lookup.
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
- name: limit
|
||||
in: query
|
||||
description: Maximum number of results. Defaults to 40.
|
||||
|
@ -590,6 +596,37 @@ paths:
|
|||
$ref: '#/components/schemas/Relationship'
|
||||
404:
|
||||
description: Profile not found
|
||||
/api/v1/accounts/{account_id}/aliases:
|
||||
get:
|
||||
summary: Get actor's verified aliases.
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/account_id'
|
||||
responses:
|
||||
200:
|
||||
description: Successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
description: Profile list
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Account'
|
||||
404:
|
||||
description: Profile not found
|
||||
/api/v1/accounts/{account_id}/aliases/all:
|
||||
get:
|
||||
summary: Get actor's aliases.
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/account_id'
|
||||
responses:
|
||||
200:
|
||||
description: Successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Aliases'
|
||||
404:
|
||||
description: Profile not found
|
||||
/api/v1/apps:
|
||||
post:
|
||||
summary: Create a new application to obtain OAuth2 credentials.
|
||||
|
@ -743,6 +780,29 @@ paths:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Notification'
|
||||
/api/v1/settings/client_config:
|
||||
post:
|
||||
summary: Update client configuration.
|
||||
security:
|
||||
- tokenAuth: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
description: |
|
||||
Client configuration.
|
||||
Should contain a single key identifying type of client.
|
||||
type: object
|
||||
example: {"mitra-web":{"theme":"dark"}}
|
||||
responses:
|
||||
200:
|
||||
description: Successful operation.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CredentialAccount'
|
||||
400:
|
||||
description: Invalid request data.
|
||||
/api/v1/settings/change_password:
|
||||
post:
|
||||
summary: Set or change user's password.
|
||||
|
@ -766,6 +826,28 @@ paths:
|
|||
$ref: '#/components/schemas/CredentialAccount'
|
||||
400:
|
||||
description: Invalid request data.
|
||||
/api/v1/settings/aliases:
|
||||
post:
|
||||
summary: Add alias (not verified).
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
acct:
|
||||
description: Actor address.
|
||||
type: string
|
||||
example: user@example.com
|
||||
responses:
|
||||
200:
|
||||
description: Successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Aliases'
|
||||
404:
|
||||
description: Profile not found.
|
||||
/api/v1/settings/export_followers:
|
||||
get:
|
||||
summary: Export followers to CSV file
|
||||
|
@ -877,6 +959,10 @@ paths:
|
|||
visibility:
|
||||
description: Visibility of the post.
|
||||
$ref: '#/components/schemas/Visibility'
|
||||
sensitiive:
|
||||
description: Mark post and attached media as sensitive?
|
||||
type: boolean
|
||||
default: false
|
||||
mentions:
|
||||
description: Array of profile IDs to be mentioned
|
||||
type: array
|
||||
|
@ -1416,6 +1502,11 @@ components:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Field'
|
||||
emojis:
|
||||
description: Custom emoji entities to be used when rendering the profile.
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/CustomEmoji'
|
||||
followers_count:
|
||||
description: The reported followers of this profile.
|
||||
type: number
|
||||
|
@ -1440,6 +1531,10 @@ components:
|
|||
role:
|
||||
description: The role assigned to the currently authorized user.
|
||||
$ref: '#/components/schemas/Role'
|
||||
client_config:
|
||||
description: Client configurations.
|
||||
type: object
|
||||
example: {"mitra-web":{"theme":"dark"}}
|
||||
ActivityParameters:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -1448,6 +1543,19 @@ components:
|
|||
type: string
|
||||
enum:
|
||||
- update
|
||||
Aliases:
|
||||
type: object
|
||||
properties:
|
||||
declared:
|
||||
description: Aliases declared by user.
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Account'
|
||||
verified:
|
||||
description: Cryptographically verified aliases.
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Account'
|
||||
Application:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -1475,6 +1583,7 @@ components:
|
|||
- unknown
|
||||
- image
|
||||
- video
|
||||
- audio
|
||||
url:
|
||||
description: The location of the original full-size attachment.
|
||||
type: string
|
||||
|
@ -1759,6 +1868,8 @@ components:
|
|||
enum:
|
||||
- create_follow_request
|
||||
- create_post
|
||||
- delete_any_post
|
||||
- delete_any_profile
|
||||
- manage_subscription_options
|
||||
Signature:
|
||||
type: object
|
||||
|
@ -1800,6 +1911,13 @@ components:
|
|||
visibility:
|
||||
description: Visibility of this post.
|
||||
$ref: '#/components/schemas/Visibility'
|
||||
sensitiive:
|
||||
description: Is this post marked as sensitive content?
|
||||
type: boolean
|
||||
example: false
|
||||
spoiler_text:
|
||||
description: Subject or summary line, below which post content is collapsed until expanded.
|
||||
type: string
|
||||
media_attachments:
|
||||
description: Media that is attached to this post.
|
||||
type: array
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
[package]
|
||||
name = "mitra-cli"
|
||||
version = "1.14.0"
|
||||
name = "fedimovies-cli"
|
||||
version = "1.22.0"
|
||||
license = "AGPL-3.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.56"
|
||||
rust-version = "1.68"
|
||||
|
||||
[[bin]]
|
||||
name = "mitractl"
|
||||
name = "fedimoviesctl"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
mitra-config = { path = "../mitra-config" }
|
||||
mitra-utils = { path = "../mitra-utils" }
|
||||
mitra = { path = ".." }
|
||||
fedimovies-config = { path = "../fedimovies-config" }
|
||||
fedimovies-models = { path = "../fedimovies-models" }
|
||||
fedimovies-utils = { path = "../fedimovies-utils" }
|
||||
fedimovies = { path = ".." }
|
||||
|
||||
# Used for catching errors
|
||||
anyhow = "1.0.58"
|
||||
|
@ -21,6 +22,6 @@ clap = { version = "3.2.18", default-features = false, features = ["std", "deriv
|
|||
# Used for logging
|
||||
log = "0.4.14"
|
||||
# Async runtime
|
||||
tokio = { version = "1.17.0", features = ["macros"] }
|
||||
tokio = { version = "1.20.4", features = ["macros"] }
|
||||
# Used to work with UUIDs
|
||||
uuid = "1.1.2"
|
|
@ -1,58 +1,39 @@
|
|||
use anyhow::{anyhow, Error};
|
||||
use anyhow::Error;
|
||||
use clap::Parser;
|
||||
use uuid::Uuid;
|
||||
|
||||
use mitra::activitypub::{
|
||||
actors::helpers::update_remote_profile,
|
||||
builders::delete_note::prepare_delete_note,
|
||||
builders::delete_person::prepare_delete_person,
|
||||
fetcher::fetchers::fetch_actor,
|
||||
use fedimovies::activitypub::{
|
||||
actors::helpers::update_remote_profile, builders::delete_note::prepare_delete_note,
|
||||
builders::delete_person::prepare_delete_person, fetcher::fetchers::fetch_actor,
|
||||
fetcher::helpers::import_from_outbox,
|
||||
};
|
||||
use mitra::database::DatabaseClient;
|
||||
use mitra::ethereum::{
|
||||
signatures::generate_ecdsa_key,
|
||||
sync::save_current_block_number,
|
||||
utils::key_to_ethereum_address,
|
||||
};
|
||||
use mitra::media::remove_files;
|
||||
use mitra::models::{
|
||||
use fedimovies::admin::roles::{role_from_str, ALLOWED_ROLES};
|
||||
use fedimovies::media::{remove_files, remove_media, MediaStorage};
|
||||
use fedimovies::validators::{emojis::EMOJI_LOCAL_MAX_SIZE, users::validate_local_username};
|
||||
use fedimovies_config::Config;
|
||||
use fedimovies_models::{
|
||||
attachments::queries::delete_unused_attachments,
|
||||
cleanup::find_orphaned_files,
|
||||
database::DatabaseClient,
|
||||
emojis::helpers::get_emoji_by_name,
|
||||
emojis::queries::{
|
||||
create_emoji,
|
||||
delete_emoji,
|
||||
get_emoji_by_name_and_hostname,
|
||||
create_emoji, delete_emoji, find_unused_remote_emojis, get_emoji_by_name_and_hostname,
|
||||
},
|
||||
emojis::validators::EMOJI_LOCAL_MAX_SIZE,
|
||||
oauth::queries::delete_oauth_tokens,
|
||||
posts::queries::{delete_post, find_extraneous_posts, get_post_by_id},
|
||||
profiles::queries::{
|
||||
delete_profile,
|
||||
find_empty_profiles,
|
||||
get_profile_by_id,
|
||||
delete_profile, find_empty_profiles, find_unreachable, get_profile_by_id,
|
||||
get_profile_by_remote_actor_id,
|
||||
},
|
||||
subscriptions::queries::reset_subscriptions,
|
||||
users::queries::{
|
||||
create_invite_code,
|
||||
get_invite_codes,
|
||||
get_user_by_id,
|
||||
set_user_password,
|
||||
create_invite_code, create_user, get_invite_codes, get_user_by_id, set_user_password,
|
||||
set_user_role,
|
||||
},
|
||||
users::types::Role,
|
||||
users::types::UserCreateData,
|
||||
};
|
||||
use mitra::monero::{
|
||||
helpers::check_expired_invoice,
|
||||
wallet::create_monero_wallet,
|
||||
};
|
||||
use mitra_config::Config;
|
||||
use mitra_utils::{
|
||||
crypto_rsa::{
|
||||
generate_rsa_key,
|
||||
serialize_private_key,
|
||||
},
|
||||
use fedimovies_utils::{
|
||||
crypto_rsa::{generate_rsa_key, serialize_private_key},
|
||||
datetime::{days_before_now, get_min_datetime},
|
||||
passwords::hash_password,
|
||||
};
|
||||
|
@ -71,9 +52,11 @@ pub enum SubCommand {
|
|||
|
||||
GenerateInviteCode(GenerateInviteCode),
|
||||
ListInviteCodes(ListInviteCodes),
|
||||
CreateUser(CreateUser),
|
||||
SetPassword(SetPassword),
|
||||
SetRole(SetRole),
|
||||
RefetchActor(RefetchActor),
|
||||
ReadOutbox(ReadOutbox),
|
||||
DeleteProfile(DeleteProfile),
|
||||
DeletePost(DeletePost),
|
||||
DeleteEmoji(DeleteEmoji),
|
||||
|
@ -81,6 +64,8 @@ pub enum SubCommand {
|
|||
DeleteUnusedAttachments(DeleteUnusedAttachments),
|
||||
DeleteOrphanedFiles(DeleteOrphanedFiles),
|
||||
DeleteEmptyProfiles(DeleteEmptyProfiles),
|
||||
PruneRemoteEmojis(PruneRemoteEmojis),
|
||||
ListUnreachableActors(ListUnreachableActors),
|
||||
ImportEmoji(ImportEmoji),
|
||||
UpdateCurrentBlock(UpdateCurrentBlock),
|
||||
ResetSubscriptions(ResetSubscriptions),
|
||||
|
@ -106,25 +91,19 @@ pub struct GenerateEthereumAddress;
|
|||
|
||||
impl GenerateEthereumAddress {
|
||||
pub fn execute(&self) -> () {
|
||||
let private_key = generate_ecdsa_key();
|
||||
let address = key_to_ethereum_address(&private_key);
|
||||
println!(
|
||||
"address {:?}; private key {}",
|
||||
address, private_key.display_secret(),
|
||||
);
|
||||
println!("dummy");
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate invite code
|
||||
#[derive(Parser)]
|
||||
pub struct GenerateInviteCode;
|
||||
pub struct GenerateInviteCode {
|
||||
note: Option<String>,
|
||||
}
|
||||
|
||||
impl GenerateInviteCode {
|
||||
pub async fn execute(
|
||||
&self,
|
||||
db_client: &impl DatabaseClient,
|
||||
) -> Result<(), Error> {
|
||||
let invite_code = create_invite_code(db_client).await?;
|
||||
pub async fn execute(&self, db_client: &impl DatabaseClient) -> Result<(), Error> {
|
||||
let invite_code = create_invite_code(db_client, self.note.as_deref()).await?;
|
||||
println!("generated invite code: {}", invite_code);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -135,18 +114,49 @@ impl GenerateInviteCode {
|
|||
pub struct ListInviteCodes;
|
||||
|
||||
impl ListInviteCodes {
|
||||
pub async fn execute(
|
||||
&self,
|
||||
db_client: &impl DatabaseClient,
|
||||
) -> Result<(), Error> {
|
||||
pub async fn execute(&self, db_client: &impl DatabaseClient) -> Result<(), Error> {
|
||||
let invite_codes = get_invite_codes(db_client).await?;
|
||||
if invite_codes.is_empty() {
|
||||
println!("no invite codes found");
|
||||
return Ok(());
|
||||
};
|
||||
for code in invite_codes {
|
||||
println!("{}", code);
|
||||
for invite_code in invite_codes {
|
||||
if let Some(note) = invite_code.note {
|
||||
println!("{} ({})", invite_code.code, note);
|
||||
} else {
|
||||
println!("{}", invite_code.code);
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new user
|
||||
#[derive(Parser)]
|
||||
pub struct CreateUser {
|
||||
username: String,
|
||||
password: String,
|
||||
#[clap(value_parser = ALLOWED_ROLES)]
|
||||
role: String,
|
||||
}
|
||||
|
||||
impl CreateUser {
|
||||
pub async fn execute(&self, db_client: &mut impl DatabaseClient) -> Result<(), Error> {
|
||||
validate_local_username(&self.username)?;
|
||||
let password_hash = hash_password(&self.password)?;
|
||||
let private_key = generate_rsa_key()?;
|
||||
let private_key_pem = serialize_private_key(&private_key)?;
|
||||
let role = role_from_str(&self.role)?;
|
||||
let user_data = UserCreateData {
|
||||
username: self.username.clone(),
|
||||
password_hash: Some(password_hash),
|
||||
private_key_pem,
|
||||
wallet_address: None,
|
||||
invite_code: None,
|
||||
role,
|
||||
};
|
||||
create_user(db_client, user_data).await?;
|
||||
println!("user created");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -159,10 +169,7 @@ pub struct SetPassword {
|
|||
}
|
||||
|
||||
impl SetPassword {
|
||||
pub async fn execute(
|
||||
&self,
|
||||
db_client: &impl DatabaseClient,
|
||||
) -> Result<(), Error> {
|
||||
pub async fn execute(&self, db_client: &impl DatabaseClient) -> Result<(), Error> {
|
||||
let password_hash = hash_password(&self.password)?;
|
||||
set_user_password(db_client, &self.id, password_hash).await?;
|
||||
// Revoke all sessions
|
||||
|
@ -176,15 +183,13 @@ impl SetPassword {
|
|||
#[derive(Parser)]
|
||||
pub struct SetRole {
|
||||
id: Uuid,
|
||||
#[clap(value_parser = ALLOWED_ROLES)]
|
||||
role: String,
|
||||
}
|
||||
|
||||
impl SetRole {
|
||||
pub async fn execute(
|
||||
&self,
|
||||
db_client: &impl DatabaseClient,
|
||||
) -> Result<(), Error> {
|
||||
let role = Role::from_name(&self.role)?;
|
||||
pub async fn execute(&self, db_client: &impl DatabaseClient) -> Result<(), Error> {
|
||||
let role = role_from_str(&self.role)?;
|
||||
set_user_role(db_client, &self.id, role).await?;
|
||||
println!("role changed");
|
||||
Ok(())
|
||||
|
@ -201,25 +206,42 @@ impl RefetchActor {
|
|||
pub async fn execute(
|
||||
&self,
|
||||
config: &Config,
|
||||
db_client: &impl DatabaseClient,
|
||||
db_client: &mut impl DatabaseClient,
|
||||
) -> Result<(), Error> {
|
||||
let profile = get_profile_by_remote_actor_id(
|
||||
db_client,
|
||||
&self.id,
|
||||
).await?;
|
||||
let profile = get_profile_by_remote_actor_id(db_client, &self.id).await?;
|
||||
let actor = fetch_actor(&config.instance(), &self.id).await?;
|
||||
update_remote_profile(
|
||||
db_client,
|
||||
&config.instance(),
|
||||
&config.media_dir(),
|
||||
&MediaStorage::from(config),
|
||||
profile,
|
||||
actor,
|
||||
).await?;
|
||||
)
|
||||
.await?;
|
||||
println!("profile updated");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Pull activities from actor's outbox
|
||||
#[derive(Parser)]
|
||||
pub struct ReadOutbox {
|
||||
actor_id: String,
|
||||
#[clap(long, default_value_t = 5)]
|
||||
limit: usize,
|
||||
}
|
||||
|
||||
impl ReadOutbox {
|
||||
pub async fn execute(
|
||||
&self,
|
||||
config: &Config,
|
||||
db_client: &mut impl DatabaseClient,
|
||||
) -> Result<(), Error> {
|
||||
import_from_outbox(config, db_client, &self.actor_id, self.limit).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete profile
|
||||
#[derive(Parser)]
|
||||
pub struct DeleteProfile {
|
||||
|
@ -236,15 +258,14 @@ impl DeleteProfile {
|
|||
let mut maybe_delete_person = None;
|
||||
if profile.is_local() {
|
||||
let user = get_user_by_id(db_client, &profile.id).await?;
|
||||
let activity =
|
||||
prepare_delete_person(db_client, &config.instance(), &user).await?;
|
||||
let activity = prepare_delete_person(db_client, &config.instance(), &user).await?;
|
||||
maybe_delete_person = Some(activity);
|
||||
};
|
||||
let deletion_queue = delete_profile(db_client, &profile.id).await?;
|
||||
deletion_queue.process(config).await;
|
||||
remove_media(config, deletion_queue).await;
|
||||
// Send Delete(Person) activities
|
||||
if let Some(activity) = maybe_delete_person {
|
||||
activity.deliver().await?;
|
||||
activity.enqueue(db_client).await?;
|
||||
};
|
||||
println!("profile deleted");
|
||||
Ok(())
|
||||
|
@ -267,19 +288,15 @@ impl DeletePost {
|
|||
let mut maybe_delete_note = None;
|
||||
if post.author.is_local() {
|
||||
let author = get_user_by_id(db_client, &post.author.id).await?;
|
||||
let activity = prepare_delete_note(
|
||||
db_client,
|
||||
&config.instance(),
|
||||
&author,
|
||||
&post,
|
||||
).await?;
|
||||
let activity =
|
||||
prepare_delete_note(db_client, &config.instance(), &author, &post).await?;
|
||||
maybe_delete_note = Some(activity);
|
||||
};
|
||||
let deletion_queue = delete_post(db_client, &post.id).await?;
|
||||
deletion_queue.process(config).await;
|
||||
remove_media(config, deletion_queue).await;
|
||||
// Send Delete(Note) activity
|
||||
if let Some(activity) = maybe_delete_note {
|
||||
activity.deliver().await?;
|
||||
activity.enqueue(db_client).await?;
|
||||
};
|
||||
println!("post deleted");
|
||||
Ok(())
|
||||
|
@ -299,13 +316,10 @@ impl DeleteEmoji {
|
|||
config: &Config,
|
||||
db_client: &impl DatabaseClient,
|
||||
) -> Result<(), Error> {
|
||||
let emoji = get_emoji_by_name(
|
||||
db_client,
|
||||
&self.emoji_name,
|
||||
self.hostname.as_deref(),
|
||||
).await?;
|
||||
let emoji =
|
||||
get_emoji_by_name(db_client, &self.emoji_name, self.hostname.as_deref()).await?;
|
||||
let deletion_queue = delete_emoji(db_client, &emoji.id).await?;
|
||||
deletion_queue.process(config).await;
|
||||
remove_media(config, deletion_queue).await;
|
||||
println!("emoji deleted");
|
||||
Ok(())
|
||||
}
|
||||
|
@ -327,9 +341,9 @@ impl DeleteExtraneousPosts {
|
|||
let posts = find_extraneous_posts(db_client, &updated_before).await?;
|
||||
for post_id in posts {
|
||||
let deletion_queue = delete_post(db_client, &post_id).await?;
|
||||
deletion_queue.process(config).await;
|
||||
remove_media(config, deletion_queue).await;
|
||||
println!("post {} deleted", post_id);
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -347,11 +361,8 @@ impl DeleteUnusedAttachments {
|
|||
db_client: &impl DatabaseClient,
|
||||
) -> Result<(), Error> {
|
||||
let created_before = days_before_now(self.days);
|
||||
let deletion_queue = delete_unused_attachments(
|
||||
db_client,
|
||||
&created_before,
|
||||
).await?;
|
||||
deletion_queue.process(config).await;
|
||||
let deletion_queue = delete_unused_attachments(db_client, &created_before).await?;
|
||||
remove_media(config, deletion_queue).await;
|
||||
println!("unused attachments deleted");
|
||||
Ok(())
|
||||
}
|
||||
|
@ -370,10 +381,9 @@ impl DeleteOrphanedFiles {
|
|||
let media_dir = config.media_dir();
|
||||
let mut files = vec![];
|
||||
for maybe_path in std::fs::read_dir(&media_dir)? {
|
||||
let file_name = maybe_path?.file_name()
|
||||
.to_string_lossy().to_string();
|
||||
let file_name = maybe_path?.file_name().to_string_lossy().to_string();
|
||||
files.push(file_name);
|
||||
};
|
||||
}
|
||||
println!("found {} files", files.len());
|
||||
let orphaned = find_orphaned_files(db_client, files).await?;
|
||||
if !orphaned.is_empty() {
|
||||
|
@ -401,9 +411,59 @@ impl DeleteEmptyProfiles {
|
|||
for profile_id in profiles {
|
||||
let profile = get_profile_by_id(db_client, &profile_id).await?;
|
||||
let deletion_queue = delete_profile(db_client, &profile.id).await?;
|
||||
deletion_queue.process(config).await;
|
||||
remove_media(config, deletion_queue).await;
|
||||
println!("profile {} deleted", profile.acct);
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete unused remote emojis
|
||||
#[derive(Parser)]
|
||||
pub struct PruneRemoteEmojis;
|
||||
|
||||
impl PruneRemoteEmojis {
|
||||
pub async fn execute(
|
||||
&self,
|
||||
config: &Config,
|
||||
db_client: &mut impl DatabaseClient,
|
||||
) -> Result<(), Error> {
|
||||
let emojis = find_unused_remote_emojis(db_client).await?;
|
||||
for emoji_id in emojis {
|
||||
let deletion_queue = delete_emoji(db_client, &emoji_id).await?;
|
||||
remove_media(config, deletion_queue).await;
|
||||
println!("emoji {} deleted", emoji_id);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// List unreachable actors
|
||||
#[derive(Parser)]
|
||||
pub struct ListUnreachableActors {
|
||||
days: u32,
|
||||
}
|
||||
|
||||
impl ListUnreachableActors {
|
||||
pub async fn execute(
|
||||
&self,
|
||||
_config: &Config,
|
||||
db_client: &impl DatabaseClient,
|
||||
) -> Result<(), Error> {
|
||||
let unreachable_since = days_before_now(self.days);
|
||||
let profiles = find_unreachable(db_client, &unreachable_since).await?;
|
||||
println!(
|
||||
"{0: <60} | {1: <35} | {2: <35}",
|
||||
"ID", "unreachable since", "updated at",
|
||||
);
|
||||
for profile in profiles {
|
||||
println!(
|
||||
"{0: <60} | {1: <35} | {2: <35}",
|
||||
profile.actor_id.unwrap(),
|
||||
profile.unreachable_since.unwrap().to_string(),
|
||||
profile.updated_at.to_string(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -421,11 +481,8 @@ impl ImportEmoji {
|
|||
_config: &Config,
|
||||
db_client: &impl DatabaseClient,
|
||||
) -> Result<(), Error> {
|
||||
let emoji = get_emoji_by_name_and_hostname(
|
||||
db_client,
|
||||
&self.emoji_name,
|
||||
&self.hostname,
|
||||
).await?;
|
||||
let emoji =
|
||||
get_emoji_by_name_and_hostname(db_client, &self.emoji_name, &self.hostname).await?;
|
||||
if emoji.image.file_size > EMOJI_LOCAL_MAX_SIZE {
|
||||
println!("emoji is too big");
|
||||
return Ok(());
|
||||
|
@ -437,7 +494,8 @@ impl ImportEmoji {
|
|||
emoji.image,
|
||||
None,
|
||||
&get_min_datetime(),
|
||||
).await?;
|
||||
)
|
||||
.await?;
|
||||
println!("added emoji to local collection");
|
||||
Ok(())
|
||||
}
|
||||
|
@ -452,10 +510,9 @@ pub struct UpdateCurrentBlock {
|
|||
impl UpdateCurrentBlock {
|
||||
pub async fn execute(
|
||||
&self,
|
||||
config: &Config,
|
||||
_config: &Config,
|
||||
_db_client: &impl DatabaseClient,
|
||||
) -> Result<(), Error> {
|
||||
save_current_block_number(&config.storage_dir, self.number)?;
|
||||
println!("current block updated");
|
||||
Ok(())
|
||||
}
|
||||
|
@ -490,18 +547,7 @@ pub struct CreateMoneroWallet {
|
|||
}
|
||||
|
||||
impl CreateMoneroWallet {
|
||||
pub async fn execute(
|
||||
&self,
|
||||
config: &Config,
|
||||
) -> Result<(), Error> {
|
||||
let monero_config = config.blockchain()
|
||||
.and_then(|conf| conf.monero_config())
|
||||
.ok_or(anyhow!("monero configuration not found"))?;
|
||||
create_monero_wallet(
|
||||
monero_config,
|
||||
self.name.clone(),
|
||||
self.password.clone(),
|
||||
).await?;
|
||||
pub async fn execute(&self, _config: &Config) -> Result<(), Error> {
|
||||
println!("wallet created");
|
||||
Ok(())
|
||||
}
|
||||
|
@ -516,17 +562,9 @@ pub struct CheckExpiredInvoice {
|
|||
impl CheckExpiredInvoice {
|
||||
pub async fn execute(
|
||||
&self,
|
||||
config: &Config,
|
||||
db_client: &impl DatabaseClient,
|
||||
_config: &Config,
|
||||
_db_client: &impl DatabaseClient,
|
||||
) -> Result<(), Error> {
|
||||
let monero_config = config.blockchain()
|
||||
.and_then(|conf| conf.monero_config())
|
||||
.ok_or(anyhow!("monero configuration not found"))?;
|
||||
check_expired_invoice(
|
||||
monero_config,
|
||||
db_client,
|
||||
&self.id,
|
||||
).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
76
fedimovies-cli/src/main.rs
Normal file
76
fedimovies-cli/src/main.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
use clap::Parser;
|
||||
|
||||
use fedimovies::logger::configure_logger;
|
||||
use fedimovies_config::parse_config;
|
||||
use fedimovies_models::database::create_database_client;
|
||||
use fedimovies_models::database::migrate::apply_migrations;
|
||||
|
||||
mod cli;
|
||||
use cli::{Opts, SubCommand};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let opts: Opts = Opts::parse();
|
||||
|
||||
match opts.subcmd {
|
||||
SubCommand::GenerateRsaKey(cmd) => cmd.execute(),
|
||||
SubCommand::GenerateEthereumAddress(cmd) => cmd.execute(),
|
||||
subcmd => {
|
||||
// Other commands require initialized app
|
||||
let (config, config_warnings) = parse_config();
|
||||
configure_logger(config.log_level);
|
||||
log::info!("config loaded from {}", config.config_path);
|
||||
for warning in config_warnings {
|
||||
log::warn!("{}", warning);
|
||||
}
|
||||
|
||||
let db_config = config.database_url.parse().unwrap();
|
||||
let db_client =
|
||||
&mut create_database_client(&db_config, config.tls_ca_file.as_deref()).await;
|
||||
apply_migrations(db_client).await;
|
||||
|
||||
match subcmd {
|
||||
SubCommand::GenerateInviteCode(cmd) => cmd.execute(db_client).await.unwrap(),
|
||||
SubCommand::ListInviteCodes(cmd) => cmd.execute(db_client).await.unwrap(),
|
||||
SubCommand::CreateUser(cmd) => cmd.execute(db_client).await.unwrap(),
|
||||
SubCommand::SetPassword(cmd) => cmd.execute(db_client).await.unwrap(),
|
||||
SubCommand::SetRole(cmd) => cmd.execute(db_client).await.unwrap(),
|
||||
SubCommand::RefetchActor(cmd) => cmd.execute(&config, db_client).await.unwrap(),
|
||||
SubCommand::ReadOutbox(cmd) => cmd.execute(&config, db_client).await.unwrap(),
|
||||
SubCommand::DeleteProfile(cmd) => cmd.execute(&config, db_client).await.unwrap(),
|
||||
SubCommand::DeletePost(cmd) => cmd.execute(&config, db_client).await.unwrap(),
|
||||
SubCommand::DeleteEmoji(cmd) => cmd.execute(&config, db_client).await.unwrap(),
|
||||
SubCommand::DeleteExtraneousPosts(cmd) => {
|
||||
cmd.execute(&config, db_client).await.unwrap()
|
||||
}
|
||||
SubCommand::DeleteUnusedAttachments(cmd) => {
|
||||
cmd.execute(&config, db_client).await.unwrap()
|
||||
}
|
||||
SubCommand::DeleteOrphanedFiles(cmd) => {
|
||||
cmd.execute(&config, db_client).await.unwrap()
|
||||
}
|
||||
SubCommand::DeleteEmptyProfiles(cmd) => {
|
||||
cmd.execute(&config, db_client).await.unwrap()
|
||||
}
|
||||
SubCommand::PruneRemoteEmojis(cmd) => {
|
||||
cmd.execute(&config, db_client).await.unwrap()
|
||||
}
|
||||
SubCommand::ListUnreachableActors(cmd) => {
|
||||
cmd.execute(&config, db_client).await.unwrap()
|
||||
}
|
||||
SubCommand::ImportEmoji(cmd) => cmd.execute(&config, db_client).await.unwrap(),
|
||||
SubCommand::UpdateCurrentBlock(cmd) => {
|
||||
cmd.execute(&config, db_client).await.unwrap()
|
||||
}
|
||||
SubCommand::ResetSubscriptions(cmd) => {
|
||||
cmd.execute(&config, db_client).await.unwrap()
|
||||
}
|
||||
SubCommand::CreateMoneroWallet(cmd) => cmd.execute(&config).await.unwrap(),
|
||||
SubCommand::CheckExpiredInvoice(cmd) => {
|
||||
cmd.execute(&config, db_client).await.unwrap()
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
[package]
|
||||
name = "mitra-config"
|
||||
version = "1.14.0"
|
||||
name = "fedimovies-config"
|
||||
version = "1.22.0"
|
||||
license = "AGPL-3.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.56"
|
||||
rust-version = "1.68"
|
||||
|
||||
[dependencies]
|
||||
mitra-utils = { path = "../mitra-utils" }
|
||||
fedimovies-utils = { path = "../fedimovies-utils" }
|
||||
|
||||
# Used to read .env files
|
||||
dotenv = "0.15.0"
|
|
@ -1,59 +1,27 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use log::{Level as LogLevel};
|
||||
use log::Level as LogLevel;
|
||||
use rsa::RsaPrivateKey;
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Deserializer,
|
||||
de::Error as DeserializerError,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
use mitra_utils::urls::normalize_url;
|
||||
use fedimovies_utils::urls::normalize_url;
|
||||
|
||||
use super::blockchain::BlockchainConfig;
|
||||
use super::environment::Environment;
|
||||
use super::federation::FederationConfig;
|
||||
use super::limits::Limits;
|
||||
use super::registration::RegistrationConfig;
|
||||
use super::retention::RetentionConfig;
|
||||
use super::MITRA_VERSION;
|
||||
use super::REEF_VERSION;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum RegistrationType {
|
||||
Open,
|
||||
Invite,
|
||||
fn default_log_level() -> LogLevel {
|
||||
LogLevel::Info
|
||||
}
|
||||
|
||||
impl Default for RegistrationType {
|
||||
fn default() -> Self { Self::Invite }
|
||||
fn default_login_message() -> String {
|
||||
"What?!".to_string()
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for RegistrationType {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where D: Deserializer<'de>
|
||||
{
|
||||
let registration_type_str = String::deserialize(deserializer)?;
|
||||
let registration_type = match registration_type_str.as_str() {
|
||||
"open" => Self::Open,
|
||||
"invite" => Self::Invite,
|
||||
_ => return Err(DeserializerError::custom("unknown registration type")),
|
||||
};
|
||||
Ok(registration_type)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize)]
|
||||
pub struct RegistrationConfig {
|
||||
#[serde(rename = "type")]
|
||||
pub registration_type: RegistrationType,
|
||||
|
||||
#[serde(default)]
|
||||
pub default_role_read_only_user: bool, // default is false
|
||||
}
|
||||
|
||||
fn default_log_level() -> LogLevel { LogLevel::Info }
|
||||
|
||||
fn default_login_message() -> String { "Do not sign this message on other sites!".to_string() }
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct Config {
|
||||
// Properties auto-populated from the environment
|
||||
|
@ -65,6 +33,8 @@ pub struct Config {
|
|||
|
||||
// Core settings
|
||||
pub database_url: String,
|
||||
#[serde(default)]
|
||||
pub tls_ca_file: Option<PathBuf>,
|
||||
pub storage_dir: PathBuf,
|
||||
pub web_client_dir: Option<PathBuf>,
|
||||
|
||||
|
@ -85,6 +55,11 @@ pub struct Config {
|
|||
pub instance_short_description: String,
|
||||
pub instance_description: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub tmdb_api_key: Option<String>,
|
||||
#[serde(default)]
|
||||
pub movie_user_password: Option<String>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub(super) instance_rsa_key: Option<RsaPrivateKey>,
|
||||
|
||||
|
@ -99,23 +74,20 @@ pub struct Config {
|
|||
|
||||
pub(super) post_character_limit: Option<usize>, // deprecated
|
||||
|
||||
proxy_url: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub limits: Limits,
|
||||
|
||||
#[serde(default)]
|
||||
pub retention: RetentionConfig,
|
||||
|
||||
pub(super) proxy_url: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub federation: FederationConfig,
|
||||
|
||||
#[serde(default)]
|
||||
pub blocked_instances: Vec<String>,
|
||||
|
||||
// Blockchain integrations
|
||||
#[serde(rename = "blockchain")]
|
||||
_blockchain: Option<BlockchainConfig>, // deprecated
|
||||
#[serde(default)]
|
||||
blockchains: Vec<BlockchainConfig>,
|
||||
|
||||
// IPFS
|
||||
pub ipfs_api_url: Option<String>,
|
||||
pub ipfs_gateway_url: Option<String>,
|
||||
|
@ -130,8 +102,14 @@ impl Config {
|
|||
Instance {
|
||||
_url: self.try_instance_url().unwrap(),
|
||||
actor_key: self.instance_rsa_key.clone().unwrap(),
|
||||
proxy_url: self.proxy_url.clone(),
|
||||
is_private: matches!(self.environment, Environment::Development),
|
||||
proxy_url: self.federation.proxy_url.clone(),
|
||||
onion_proxy_url: self.federation.onion_proxy_url.clone(),
|
||||
i2p_proxy_url: self.federation.i2p_proxy_url.clone(),
|
||||
// Private instance doesn't send activities and sign requests
|
||||
is_private: !self.federation.enabled,
|
||||
// || matches!(self.environment, Environment::Development),
|
||||
fetcher_timeout: self.federation.fetcher_timeout,
|
||||
deliverer_timeout: self.federation.deliverer_timeout,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -142,18 +120,6 @@ impl Config {
|
|||
pub fn media_dir(&self) -> PathBuf {
|
||||
self.storage_dir.join("media")
|
||||
}
|
||||
|
||||
pub fn blockchain(&self) -> Option<&BlockchainConfig> {
|
||||
if let Some(ref _blockchain_config) = self._blockchain {
|
||||
panic!("'blockchain' setting is not supported anymore, use 'blockchains' instead");
|
||||
} else {
|
||||
match &self.blockchains[..] {
|
||||
[blockchain_config] => Some(blockchain_config),
|
||||
[] => None,
|
||||
_ => panic!("multichain deployments are not supported"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -163,8 +129,12 @@ pub struct Instance {
|
|||
pub actor_key: RsaPrivateKey,
|
||||
// Proxy for outgoing requests
|
||||
pub proxy_url: Option<String>,
|
||||
pub onion_proxy_url: Option<String>,
|
||||
pub i2p_proxy_url: Option<String>,
|
||||
// Private instance won't send signed HTTP requests
|
||||
pub is_private: bool,
|
||||
pub fetcher_timeout: u64,
|
||||
pub deliverer_timeout: u64,
|
||||
}
|
||||
|
||||
impl Instance {
|
||||
|
@ -178,9 +148,9 @@ impl Instance {
|
|||
|
||||
pub fn agent(&self) -> String {
|
||||
format!(
|
||||
"Mitra {version}; {instance_url}",
|
||||
version=MITRA_VERSION,
|
||||
instance_url=self.url(),
|
||||
"Reef {version}; {instance_url}",
|
||||
version = REEF_VERSION,
|
||||
instance_url = self.url(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -188,20 +158,24 @@ impl Instance {
|
|||
#[cfg(feature = "test-utils")]
|
||||
impl Instance {
|
||||
pub fn for_test(url: &str) -> Self {
|
||||
use mitra_utils::crypto_rsa::generate_weak_rsa_key;
|
||||
use fedimovies_utils::crypto_rsa::generate_weak_rsa_key;
|
||||
Self {
|
||||
_url: Url::parse(url).unwrap(),
|
||||
actor_key: generate_weak_rsa_key().unwrap(),
|
||||
proxy_url: None,
|
||||
onion_proxy_url: None,
|
||||
i2p_proxy_url: None,
|
||||
is_private: true,
|
||||
fetcher_timeout: 0,
|
||||
deliverer_timeout: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use mitra_utils::crypto_rsa::generate_weak_rsa_key;
|
||||
use super::*;
|
||||
use fedimovies_utils::crypto_rsa::generate_weak_rsa_key;
|
||||
|
||||
#[test]
|
||||
fn test_instance_url_https_dns() {
|
||||
|
@ -211,14 +185,18 @@ mod tests {
|
|||
_url: instance_url,
|
||||
actor_key: instance_rsa_key,
|
||||
proxy_url: None,
|
||||
onion_proxy_url: None,
|
||||
i2p_proxy_url: None,
|
||||
is_private: true,
|
||||
fetcher_timeout: 0,
|
||||
deliverer_timeout: 0,
|
||||
};
|
||||
|
||||
assert_eq!(instance.url(), "https://example.com");
|
||||
assert_eq!(instance.hostname(), "example.com");
|
||||
assert_eq!(
|
||||
instance.agent(),
|
||||
format!("Mitra {}; https://example.com", MITRA_VERSION),
|
||||
format!("Mitra {}; https://example.com", REEF_VERSION),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -230,7 +208,11 @@ mod tests {
|
|||
_url: instance_url,
|
||||
actor_key: instance_rsa_key,
|
||||
proxy_url: None,
|
||||
onion_proxy_url: None,
|
||||
i2p_proxy_url: None,
|
||||
is_private: true,
|
||||
fetcher_timeout: 0,
|
||||
deliverer_timeout: 0,
|
||||
};
|
||||
|
||||
assert_eq!(instance.url(), "http://1.2.3.4:3777");
|
|
@ -10,9 +10,13 @@ pub enum Environment {
|
|||
|
||||
impl Default for Environment {
|
||||
#[cfg(feature = "production")]
|
||||
fn default() -> Self { Self::Production }
|
||||
fn default() -> Self {
|
||||
Self::Production
|
||||
}
|
||||
#[cfg(not(feature = "production"))]
|
||||
fn default() -> Self { Self::Development }
|
||||
fn default() -> Self {
|
||||
Self::Development
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Environment {
|
38
fedimovies-config/src/federation.rs
Normal file
38
fedimovies-config/src/federation.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
fn default_federation_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
const fn default_fetcher_timeout() -> u64 {
|
||||
300
|
||||
}
|
||||
const fn default_deliverer_timeout() -> u64 {
|
||||
30
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct FederationConfig {
|
||||
#[serde(default = "default_federation_enabled")]
|
||||
pub enabled: bool,
|
||||
#[serde(default = "default_fetcher_timeout")]
|
||||
pub(super) fetcher_timeout: u64,
|
||||
#[serde(default = "default_deliverer_timeout")]
|
||||
pub(super) deliverer_timeout: u64,
|
||||
pub(super) proxy_url: Option<String>,
|
||||
pub(super) onion_proxy_url: Option<String>,
|
||||
pub(super) i2p_proxy_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for FederationConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: default_federation_enabled(),
|
||||
fetcher_timeout: default_fetcher_timeout(),
|
||||
deliverer_timeout: default_deliverer_timeout(),
|
||||
proxy_url: None,
|
||||
onion_proxy_url: None,
|
||||
i2p_proxy_url: None,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,20 +1,17 @@
|
|||
mod blockchain;
|
||||
mod config;
|
||||
mod environment;
|
||||
mod federation;
|
||||
mod limits;
|
||||
mod loader;
|
||||
mod registration;
|
||||
mod retention;
|
||||
|
||||
pub use blockchain::{
|
||||
BlockchainConfig,
|
||||
EthereumConfig,
|
||||
MoneroConfig,
|
||||
};
|
||||
pub use config::{Config, Instance, RegistrationType};
|
||||
pub use config::{Config, Instance};
|
||||
pub use environment::Environment;
|
||||
pub use loader::parse_config;
|
||||
pub use registration::{DefaultRole, RegistrationType};
|
||||
|
||||
pub const MITRA_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub const REEF_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("{0}")]
|
|
@ -1,19 +1,17 @@
|
|||
use regex::Regex;
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Deserializer,
|
||||
de::{Error as DeserializerError},
|
||||
};
|
||||
use super::ConfigError;
|
||||
use regex::Regex;
|
||||
use serde::{de::Error as DeserializerError, Deserialize, Deserializer};
|
||||
|
||||
const FILE_SIZE_RE: &str = r#"^(?i)(?P<size>\d+)(?P<unit>[kmg]?)b?$"#;
|
||||
|
||||
fn parse_file_size(value: &str) -> Result<usize, ConfigError> {
|
||||
let file_size_re = Regex::new(FILE_SIZE_RE)
|
||||
.expect("regexp should be valid");
|
||||
let caps = file_size_re.captures(value)
|
||||
let file_size_re = Regex::new(FILE_SIZE_RE).expect("regexp should be valid");
|
||||
let caps = file_size_re
|
||||
.captures(value)
|
||||
.ok_or(ConfigError("invalid file size"))?;
|
||||
let size: usize = caps["size"].to_string().parse()
|
||||
let size: usize = caps["size"]
|
||||
.to_string()
|
||||
.parse()
|
||||
.map_err(|_| ConfigError("invalid file size"))?;
|
||||
let unit = caps["unit"].to_string().to_lowercase();
|
||||
let multiplier = match unit.as_str() {
|
||||
|
@ -26,37 +24,49 @@ fn parse_file_size(value: &str) -> Result<usize, ConfigError> {
|
|||
Ok(size * multiplier)
|
||||
}
|
||||
|
||||
fn deserialize_file_size<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<usize, D::Error>
|
||||
where D: Deserializer<'de>
|
||||
fn deserialize_file_size<'de, D>(deserializer: D) -> Result<usize, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let file_size_str = String::deserialize(deserializer)?;
|
||||
let file_size = parse_file_size(&file_size_str)
|
||||
.map_err(DeserializerError::custom)?;
|
||||
let file_size = parse_file_size(&file_size_str).map_err(DeserializerError::custom)?;
|
||||
Ok(file_size)
|
||||
}
|
||||
|
||||
const fn default_file_size_limit() -> usize { 20_000_000 } // 20 MB
|
||||
const fn default_file_size_limit() -> usize {
|
||||
20_000_000
|
||||
} // 20 MB
|
||||
const fn default_emoji_size_limit() -> usize {
|
||||
500_000
|
||||
} // 500 kB
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct MediaLimits {
|
||||
#[serde(
|
||||
default = "default_file_size_limit",
|
||||
deserialize_with = "deserialize_file_size",
|
||||
deserialize_with = "deserialize_file_size"
|
||||
)]
|
||||
pub file_size_limit: usize,
|
||||
|
||||
#[serde(
|
||||
default = "default_emoji_size_limit",
|
||||
deserialize_with = "deserialize_file_size"
|
||||
)]
|
||||
pub emoji_size_limit: usize,
|
||||
}
|
||||
|
||||
impl Default for MediaLimits {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
file_size_limit: default_file_size_limit(),
|
||||
emoji_size_limit: default_emoji_size_limit(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn default_post_character_limit() -> usize { 2000 }
|
||||
const fn default_post_character_limit() -> usize {
|
||||
2000
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct PostLimits {
|
|
@ -4,17 +4,14 @@ use std::str::FromStr;
|
|||
|
||||
use rsa::RsaPrivateKey;
|
||||
|
||||
use mitra_utils::{
|
||||
crypto_rsa::{
|
||||
deserialize_private_key,
|
||||
generate_rsa_key,
|
||||
serialize_private_key,
|
||||
},
|
||||
use fedimovies_utils::{
|
||||
crypto_rsa::{deserialize_private_key, generate_rsa_key, serialize_private_key},
|
||||
files::{set_file_permissions, write_file},
|
||||
};
|
||||
|
||||
use super::config::{Config, RegistrationType};
|
||||
use super::config::Config;
|
||||
use super::environment::Environment;
|
||||
use super::registration::{DefaultRole, RegistrationType};
|
||||
|
||||
struct EnvConfig {
|
||||
config_path: String,
|
||||
|
@ -22,16 +19,16 @@ struct EnvConfig {
|
|||
}
|
||||
|
||||
#[cfg(feature = "production")]
|
||||
const DEFAULT_CONFIG_PATH: &str = "/etc/mitra/config.yaml";
|
||||
const DEFAULT_CONFIG_PATH: &str = "/etc/fedimovies/config.yaml";
|
||||
#[cfg(not(feature = "production"))]
|
||||
const DEFAULT_CONFIG_PATH: &str = "config.yaml";
|
||||
|
||||
fn parse_env() -> EnvConfig {
|
||||
dotenv::from_filename(".env.local").ok();
|
||||
dotenv::dotenv().ok();
|
||||
let config_path = std::env::var("CONFIG_PATH")
|
||||
.unwrap_or(DEFAULT_CONFIG_PATH.to_string());
|
||||
let environment = std::env::var("ENVIRONMENT").ok()
|
||||
let config_path = std::env::var("CONFIG_PATH").unwrap_or(DEFAULT_CONFIG_PATH.to_string());
|
||||
let environment = std::env::var("ENVIRONMENT")
|
||||
.ok()
|
||||
.map(|val| Environment::from_str(&val).expect("invalid environment type"));
|
||||
EnvConfig {
|
||||
config_path,
|
||||
|
@ -44,8 +41,7 @@ extern "C" {
|
|||
}
|
||||
|
||||
fn check_directory_owner(path: &Path) -> () {
|
||||
let metadata = std::fs::metadata(path)
|
||||
.expect("can't read file metadata");
|
||||
let metadata = std::fs::metadata(path).expect("can't read file metadata");
|
||||
let owner_uid = metadata.uid();
|
||||
let current_uid = unsafe { geteuid() };
|
||||
if owner_uid != current_uid {
|
||||
|
@ -62,16 +58,15 @@ fn check_directory_owner(path: &Path) -> () {
|
|||
fn read_instance_rsa_key(storage_dir: &Path) -> RsaPrivateKey {
|
||||
let private_key_path = storage_dir.join("instance_rsa_key");
|
||||
if private_key_path.exists() {
|
||||
let private_key_str = std::fs::read_to_string(&private_key_path)
|
||||
.expect("failed to read instance RSA key");
|
||||
let private_key = deserialize_private_key(&private_key_str)
|
||||
.expect("failed to read instance RSA key");
|
||||
let private_key_str =
|
||||
std::fs::read_to_string(&private_key_path).expect("failed to read instance RSA key");
|
||||
let private_key =
|
||||
deserialize_private_key(&private_key_str).expect("failed to read instance RSA key");
|
||||
private_key
|
||||
} else {
|
||||
let private_key = generate_rsa_key()
|
||||
.expect("failed to generate RSA key");
|
||||
let private_key_str = serialize_private_key(&private_key)
|
||||
.expect("failed to serialize RSA key");
|
||||
let private_key = generate_rsa_key().expect("failed to generate RSA key");
|
||||
let private_key_str =
|
||||
serialize_private_key(&private_key).expect("failed to serialize RSA key");
|
||||
write_file(private_key_str.as_bytes(), &private_key_path)
|
||||
.expect("failed to write instance RSA key");
|
||||
set_file_permissions(&private_key_path, 0o600)
|
||||
|
@ -82,10 +77,9 @@ fn read_instance_rsa_key(storage_dir: &Path) -> RsaPrivateKey {
|
|||
|
||||
pub fn parse_config() -> (Config, Vec<&'static str>) {
|
||||
let env = parse_env();
|
||||
let config_yaml = std::fs::read_to_string(&env.config_path)
|
||||
.expect("failed to load config file");
|
||||
let mut config = serde_yaml::from_str::<Config>(&config_yaml)
|
||||
.expect("invalid yaml data");
|
||||
let config_yaml =
|
||||
std::fs::read_to_string(&env.config_path).expect("failed to load config file");
|
||||
let mut config = serde_yaml::from_str::<Config>(&config_yaml).expect("invalid yaml data");
|
||||
let mut warnings = vec![];
|
||||
|
||||
// Set parameters from environment
|
||||
|
@ -101,32 +95,37 @@ pub fn parse_config() -> (Config, Vec<&'static str>) {
|
|||
};
|
||||
check_directory_owner(&config.storage_dir);
|
||||
config.try_instance_url().expect("invalid instance URI");
|
||||
if let Some(blockchain_config) = config.blockchain() {
|
||||
if let Some(ethereum_config) = blockchain_config.ethereum_config() {
|
||||
ethereum_config.try_ethereum_chain_id().unwrap();
|
||||
if !ethereum_config.contract_dir.exists() {
|
||||
panic!("contract directory does not exist");
|
||||
};
|
||||
};
|
||||
};
|
||||
if config.ipfs_api_url.is_some() != config.ipfs_gateway_url.is_some() {
|
||||
panic!("both ipfs_api_url and ipfs_gateway_url must be set");
|
||||
};
|
||||
|
||||
// Migrations
|
||||
if let Some(registrations_open) = config.registrations_open {
|
||||
// Change type if 'registrations_open' parameter is used
|
||||
warnings.push("'registrations_open' setting is deprecated, use 'registration.type' instead");
|
||||
warnings
|
||||
.push("'registrations_open' setting is deprecated, use 'registration.type' instead");
|
||||
if registrations_open {
|
||||
config.registration.registration_type = RegistrationType::Open;
|
||||
} else {
|
||||
config.registration.registration_type = RegistrationType::Invite;
|
||||
};
|
||||
};
|
||||
|
||||
if let Some(read_only_user) = config.registration.default_role_read_only_user {
|
||||
warnings.push("'default_role_read_only_user' setting is deprecated, use 'registration.default_role' instead");
|
||||
if read_only_user {
|
||||
config.registration.default_role = DefaultRole::ReadOnlyUser;
|
||||
} else {
|
||||
config.registration.default_role = DefaultRole::NormalUser;
|
||||
};
|
||||
};
|
||||
if let Some(post_character_limit) = config.post_character_limit {
|
||||
warnings.push("'post_character_limit' setting is deprecated, use 'limits.posts.character_limit' instead");
|
||||
config.limits.posts.character_limit = post_character_limit;
|
||||
};
|
||||
if let Some(ref proxy_url) = config.proxy_url {
|
||||
warnings.push("'proxy_url' setting is deprecated, use 'federation.proxy_url' instead");
|
||||
config.federation.proxy_url = Some(proxy_url.to_string());
|
||||
};
|
||||
|
||||
// Insert instance RSA key
|
||||
config.instance_rsa_key = Some(read_instance_rsa_key(&config.storage_dir));
|
66
fedimovies-config/src/registration.rs
Normal file
66
fedimovies-config/src/registration.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use serde::{de::Error as DeserializerError, Deserialize, Deserializer};
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum RegistrationType {
|
||||
Open,
|
||||
Invite,
|
||||
}
|
||||
|
||||
impl Default for RegistrationType {
|
||||
fn default() -> Self {
|
||||
Self::Invite
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for RegistrationType {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let registration_type_str = String::deserialize(deserializer)?;
|
||||
let registration_type = match registration_type_str.as_str() {
|
||||
"open" => Self::Open,
|
||||
"invite" => Self::Invite,
|
||||
_ => return Err(DeserializerError::custom("unknown registration type")),
|
||||
};
|
||||
Ok(registration_type)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum DefaultRole {
|
||||
NormalUser,
|
||||
ReadOnlyUser,
|
||||
}
|
||||
|
||||
impl Default for DefaultRole {
|
||||
fn default() -> Self {
|
||||
Self::NormalUser
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for DefaultRole {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let role_str = String::deserialize(deserializer)?;
|
||||
let role = match role_str.as_str() {
|
||||
"user" => Self::NormalUser,
|
||||
"read_only_user" => Self::ReadOnlyUser,
|
||||
_ => return Err(DeserializerError::custom("unknown role name")),
|
||||
};
|
||||
Ok(role)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize)]
|
||||
pub struct RegistrationConfig {
|
||||
#[serde(rename = "type")]
|
||||
pub registration_type: RegistrationType,
|
||||
|
||||
pub(super) default_role_read_only_user: Option<bool>, // deprecated
|
||||
|
||||
#[serde(default)]
|
||||
pub default_role: DefaultRole,
|
||||
}
|
46
fedimovies-models/Cargo.toml
Normal file
46
fedimovies-models/Cargo.toml
Normal file
|
@ -0,0 +1,46 @@
|
|||
[package]
|
||||
name = "fedimovies-models"
|
||||
version = "1.22.0"
|
||||
license = "AGPL-3.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.68"
|
||||
|
||||
[dependencies]
|
||||
fedimovies-utils = { path = "../fedimovies-utils" }
|
||||
|
||||
# Used for working with dates
|
||||
chrono = { version = "0.4.23", default-features = false, features = ["std", "serde"] }
|
||||
# Used for pooling database connections
|
||||
deadpool = "0.9.2"
|
||||
deadpool-postgres = { version = "0.10.2", default-features = false }
|
||||
# Used to work with hexadecimal strings
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
# Used for logging
|
||||
log = "0.4.14"
|
||||
# Used for managing database migrations
|
||||
refinery = { version = "0.8.4", features = ["tokio-postgres"] }
|
||||
# Used for serialization/deserialization
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
# Used for creating error types
|
||||
thiserror = "1.0.37"
|
||||
# Async runtime
|
||||
tokio = { version = "1.20.4", features = [] }
|
||||
# Used for working with Postgresql database
|
||||
openssl = { version = "0.10", features = ["vendored"] }
|
||||
postgres-openssl = "0.5.0"
|
||||
tokio-postgres = { version = "0.7.6", features = ["with-chrono-0_4", "with-uuid-1", "with-serde_json-1"] }
|
||||
postgres-types = { version = "0.2.3", features = ["derive", "with-chrono-0_4", "with-uuid-1", "with-serde_json-1"] }
|
||||
postgres-protocol = "0.6.4"
|
||||
# Used to construct PostgreSQL queries
|
||||
postgres_query = { git = "https://github.com/nolanderc/rust-postgres-query", rev = "b4422051c8a31fbba4a35f88004c1cefb1878dd5" }
|
||||
postgres_query_macro = { git = "https://github.com/nolanderc/rust-postgres-query", rev = "b4422051c8a31fbba4a35f88004c1cefb1878dd5" }
|
||||
# Used to work with UUIDs
|
||||
uuid = { version = "1.1.2", features = ["serde", "v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
fedimovies-utils = { path = "../fedimovies-utils", features = ["test-utils"] }
|
||||
serial_test = "0.7.0"
|
||||
|
||||
[features]
|
||||
test-utils = []
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE user_invite_code ADD COLUMN note VARCHAR(200);
|
||||
ALTER TABLE user_invite_code ADD COLUMN created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP;
|
5
fedimovies-models/migrations/V0045__profile_emoji.sql
Normal file
5
fedimovies-models/migrations/V0045__profile_emoji.sql
Normal file
|
@ -0,0 +1,5 @@
|
|||
CREATE TABLE profile_emoji (
|
||||
profile_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
|
||||
emoji_id UUID NOT NULL REFERENCES emoji (id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (profile_id, emoji_id)
|
||||
);
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE actor_profile ADD COLUMN emojis JSONB NOT NULL DEFAULT '[]';
|
|
@ -0,0 +1,4 @@
|
|||
CREATE TABLE internal_property (
|
||||
property_name VARCHAR(100) PRIMARY KEY,
|
||||
property_value JSONB NOT NULL
|
||||
);
|
|
@ -0,0 +1,18 @@
|
|||
UPDATE actor_profile
|
||||
SET identity_proofs = replaced.identity_proofs
|
||||
FROM (
|
||||
SELECT
|
||||
actor_profile.id,
|
||||
jsonb_agg(
|
||||
CASE
|
||||
WHEN identity_proof ->> 'proof_type' = 'ethereum-eip191-00'
|
||||
THEN jsonb_set(identity_proof, '{proof_type}', '1')
|
||||
WHEN identity_proof ->> 'proof_type' = 'MitraMinisignSignature2022A'
|
||||
THEN jsonb_set(identity_proof, '{proof_type}', '2')
|
||||
END
|
||||
) AS identity_proofs
|
||||
FROM actor_profile
|
||||
CROSS JOIN jsonb_array_elements(actor_profile.identity_proofs) AS identity_proof
|
||||
GROUP BY actor_profile.id
|
||||
) AS replaced
|
||||
WHERE actor_profile.id = replaced.id;
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE actor_profile ADD COLUMN manually_approves_followers BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE actor_profile ALTER COLUMN manually_approves_followers DROP DEFAULT;
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE actor_profile ADD COLUMN aliases JSONB NOT NULL DEFAULT '[]';
|
|
@ -0,0 +1,5 @@
|
|||
ALTER TABLE relationship ADD CONSTRAINT relationship_source_id_target_id_check CHECK (source_id != target_id);
|
||||
ALTER TABLE follow_request ADD CONSTRAINT follow_request_source_id_target_id_check CHECK (source_id != target_id);
|
||||
ALTER TABLE post_link ADD CONSTRAINT post_link_source_id_target_id_check CHECK (source_id != target_id);
|
||||
ALTER TABLE invoice ADD CONSTRAINT invoice_sender_id_recipient_id_check CHECK (sender_id != recipient_id);
|
||||
ALTER TABLE subscription ADD CONSTRAINT subscription_sender_id_recipient_id_check CHECK (sender_id != recipient_id);
|
|
@ -0,0 +1,6 @@
|
|||
ALTER TABLE actor_profile ALTER COLUMN actor_id TYPE VARCHAR(2000);
|
||||
ALTER TABLE oauth_application ALTER COLUMN redirect_uri TYPE VARCHAR(2000);
|
||||
ALTER TABLE follow_request ALTER COLUMN activity_id TYPE VARCHAR(2000);
|
||||
ALTER TABLE post ALTER COLUMN object_id TYPE VARCHAR(2000);
|
||||
ALTER TABLE post_reaction ALTER COLUMN activity_id TYPE VARCHAR(2000);
|
||||
ALTER TABLE emoji ALTER COLUMN object_id TYPE VARCHAR(2000);
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE user_account ADD COLUMN client_config JSONB NOT NULL DEFAULT '{}';
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE post ADD COLUMN is_sensitive BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE post ALTER COLUMN is_sensitive DROP DEFAULT;
|
|
@ -1,3 +1,8 @@
|
|||
CREATE TABLE internal_property (
|
||||
property_name VARCHAR(100) PRIMARY KEY,
|
||||
property_value JSONB NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE background_job (
|
||||
id UUID PRIMARY KEY,
|
||||
job_type SMALLINT NOT NULL,
|
||||
|
@ -21,15 +26,18 @@ CREATE TABLE actor_profile (
|
|||
bio_source TEXT,
|
||||
avatar JSONB,
|
||||
banner JSONB,
|
||||
manually_approves_followers BOOLEAN NOT NULL,
|
||||
identity_proofs JSONB NOT NULL DEFAULT '[]',
|
||||
payment_options JSONB NOT NULL DEFAULT '[]',
|
||||
extra_fields JSONB NOT NULL DEFAULT '[]',
|
||||
aliases JSONB NOT NULL DEFAULT '[]',
|
||||
follower_count INTEGER NOT NULL CHECK (follower_count >= 0) DEFAULT 0,
|
||||
following_count INTEGER NOT NULL CHECK (following_count >= 0) DEFAULT 0,
|
||||
subscriber_count INTEGER NOT NULL CHECK (subscriber_count >= 0) DEFAULT 0,
|
||||
post_count INTEGER NOT NULL CHECK (post_count >= 0) DEFAULT 0,
|
||||
emojis JSONB NOT NULL DEFAULT '[]',
|
||||
actor_json JSONB,
|
||||
actor_id VARCHAR(200) UNIQUE GENERATED ALWAYS AS (actor_json ->> 'id') STORED,
|
||||
actor_id VARCHAR(2000) UNIQUE GENERATED ALWAYS AS (actor_json ->> 'id') STORED,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
unreachable_since TIMESTAMP WITH TIME ZONE,
|
||||
|
@ -38,7 +46,9 @@ CREATE TABLE actor_profile (
|
|||
|
||||
CREATE TABLE user_invite_code (
|
||||
code VARCHAR(100) PRIMARY KEY,
|
||||
used BOOLEAN NOT NULL DEFAULT FALSE
|
||||
used BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
note VARCHAR(200),
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE user_account (
|
||||
|
@ -48,6 +58,7 @@ CREATE TABLE user_account (
|
|||
private_key TEXT NOT NULL,
|
||||
invite_code VARCHAR(100) UNIQUE REFERENCES user_invite_code (code) ON DELETE SET NULL,
|
||||
user_role SMALLINT NOT NULL,
|
||||
client_config JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
|
@ -56,7 +67,7 @@ CREATE TABLE oauth_application (
|
|||
app_name VARCHAR(100) NOT NULL,
|
||||
website VARCHAR(100),
|
||||
scopes VARCHAR(200) NOT NULL,
|
||||
redirect_uri VARCHAR(200) NOT NULL,
|
||||
redirect_uri VARCHAR(2000) NOT NULL,
|
||||
client_id UUID UNIQUE NOT NULL,
|
||||
client_secret VARCHAR(100) NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
|
@ -85,16 +96,18 @@ CREATE TABLE relationship (
|
|||
source_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
|
||||
target_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
|
||||
relationship_type SMALLINT NOT NULL,
|
||||
UNIQUE (source_id, target_id, relationship_type)
|
||||
UNIQUE (source_id, target_id, relationship_type),
|
||||
CHECK (source_id != target_id)
|
||||
);
|
||||
|
||||
CREATE TABLE follow_request (
|
||||
id UUID PRIMARY KEY,
|
||||
source_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
|
||||
target_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
|
||||
activity_id VARCHAR(250) UNIQUE,
|
||||
activity_id VARCHAR(2000) UNIQUE,
|
||||
request_status SMALLINT NOT NULL,
|
||||
UNIQUE (source_id, target_id)
|
||||
UNIQUE (source_id, target_id),
|
||||
CHECK (source_id != target_id)
|
||||
);
|
||||
|
||||
CREATE TABLE post (
|
||||
|
@ -103,11 +116,12 @@ CREATE TABLE post (
|
|||
content TEXT NOT NULL,
|
||||
in_reply_to_id UUID REFERENCES post (id) ON DELETE CASCADE,
|
||||
repost_of_id UUID REFERENCES post (id) ON DELETE CASCADE,
|
||||
visilibity SMALLINT NOT NULL,
|
||||
visibility SMALLINT NOT NULL,
|
||||
is_sensitive BOOLEAN NOT NULL,
|
||||
reply_count INTEGER NOT NULL CHECK (reply_count >= 0) DEFAULT 0,
|
||||
reaction_count INTEGER NOT NULL CHECK (reaction_count >= 0) DEFAULT 0,
|
||||
repost_count INTEGER NOT NULL CHECK (repost_count >= 0) DEFAULT 0,
|
||||
object_id VARCHAR(200) UNIQUE,
|
||||
object_id VARCHAR(2000) UNIQUE,
|
||||
ipfs_cid VARCHAR(200),
|
||||
token_id INTEGER,
|
||||
token_tx_id VARCHAR(200),
|
||||
|
@ -120,7 +134,7 @@ CREATE TABLE post_reaction (
|
|||
id UUID PRIMARY KEY,
|
||||
author_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
|
||||
post_id UUID NOT NULL REFERENCES post (id) ON DELETE CASCADE,
|
||||
activity_id VARCHAR(250) UNIQUE,
|
||||
activity_id VARCHAR(2000) UNIQUE,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
UNIQUE (author_id, post_id)
|
||||
);
|
||||
|
@ -156,7 +170,8 @@ CREATE TABLE post_tag (
|
|||
CREATE TABLE post_link (
|
||||
source_id UUID NOT NULL REFERENCES post (id) ON DELETE CASCADE,
|
||||
target_id UUID NOT NULL REFERENCES post (id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (source_id, target_id)
|
||||
PRIMARY KEY (source_id, target_id),
|
||||
CHECK (source_id != target_id)
|
||||
);
|
||||
|
||||
CREATE TABLE emoji (
|
||||
|
@ -164,7 +179,7 @@ CREATE TABLE emoji (
|
|||
emoji_name VARCHAR(100) NOT NULL,
|
||||
hostname VARCHAR(100) REFERENCES instance (hostname) ON DELETE RESTRICT,
|
||||
image JSONB NOT NULL,
|
||||
object_id VARCHAR(250) UNIQUE,
|
||||
object_id VARCHAR(2000) UNIQUE,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
UNIQUE (emoji_name, hostname),
|
||||
CHECK ((hostname IS NULL) = (object_id IS NULL))
|
||||
|
@ -176,6 +191,12 @@ CREATE TABLE post_emoji (
|
|||
PRIMARY KEY (post_id, emoji_id)
|
||||
);
|
||||
|
||||
CREATE TABLE profile_emoji (
|
||||
profile_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
|
||||
emoji_id UUID NOT NULL REFERENCES emoji (id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (profile_id, emoji_id)
|
||||
);
|
||||
|
||||
CREATE TABLE notification (
|
||||
id SERIAL PRIMARY KEY,
|
||||
sender_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
|
||||
|
@ -203,7 +224,8 @@ CREATE TABLE invoice (
|
|||
amount BIGINT NOT NULL CHECK (amount >= 0),
|
||||
invoice_status SMALLINT NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (chain_id, payment_address)
|
||||
UNIQUE (chain_id, payment_address),
|
||||
CHECK (sender_id != recipient_id)
|
||||
);
|
||||
|
||||
CREATE TABLE subscription (
|
||||
|
@ -214,5 +236,6 @@ CREATE TABLE subscription (
|
|||
chain_id VARCHAR(50) NOT NULL,
|
||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
UNIQUE (sender_id, recipient_id)
|
||||
UNIQUE (sender_id, recipient_id),
|
||||
CHECK (sender_id != recipient_id)
|
||||
);
|
|
@ -1,14 +1,11 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
use mitra_utils::id::generate_ulid;
|
||||
use fedimovies_utils::id::generate_ulid;
|
||||
|
||||
use crate::cleanup::{find_orphaned_files, find_orphaned_ipfs_objects, DeletionQueue};
|
||||
use crate::database::{DatabaseClient, DatabaseError};
|
||||
use crate::models::cleanup::{
|
||||
find_orphaned_files,
|
||||
find_orphaned_ipfs_objects,
|
||||
DeletionQueue,
|
||||
};
|
||||
|
||||
use super::types::DbMediaAttachment;
|
||||
|
||||
pub async fn create_attachment(
|
||||
|
@ -19,10 +16,10 @@ pub async fn create_attachment(
|
|||
media_type: Option<String>,
|
||||
) -> Result<DbMediaAttachment, DatabaseError> {
|
||||
let attachment_id = generate_ulid();
|
||||
let file_size: i32 = file_size.try_into()
|
||||
.expect("value should be within bounds");
|
||||
let inserted_row = db_client.query_one(
|
||||
"
|
||||
let file_size: i32 = file_size.try_into().expect("value should be within bounds");
|
||||
let inserted_row = db_client
|
||||
.query_one(
|
||||
"
|
||||
INSERT INTO media_attachment (
|
||||
id,
|
||||
owner_id,
|
||||
|
@ -33,14 +30,15 @@ pub async fn create_attachment(
|
|||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING media_attachment
|
||||
",
|
||||
&[
|
||||
&attachment_id,
|
||||
&owner_id,
|
||||
&file_name,
|
||||
&file_size,
|
||||
&media_type,
|
||||
],
|
||||
).await?;
|
||||
&[
|
||||
&attachment_id,
|
||||
&owner_id,
|
||||
&file_name,
|
||||
&file_size,
|
||||
&media_type,
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
let db_attachment: DbMediaAttachment = inserted_row.try_get("media_attachment")?;
|
||||
Ok(db_attachment)
|
||||
}
|
||||
|
@ -50,15 +48,17 @@ pub async fn set_attachment_ipfs_cid(
|
|||
attachment_id: &Uuid,
|
||||
ipfs_cid: &str,
|
||||
) -> Result<DbMediaAttachment, DatabaseError> {
|
||||
let maybe_row = db_client.query_opt(
|
||||
"
|
||||
let maybe_row = db_client
|
||||
.query_opt(
|
||||
"
|
||||
UPDATE media_attachment
|
||||
SET ipfs_cid = $1
|
||||
WHERE id = $2 AND ipfs_cid IS NULL
|
||||
RETURNING media_attachment
|
||||
",
|
||||
&[&ipfs_cid, &attachment_id],
|
||||
).await?;
|
||||
&[&ipfs_cid, &attachment_id],
|
||||
)
|
||||
.await?;
|
||||
let row = maybe_row.ok_or(DatabaseError::NotFound("attachment"))?;
|
||||
let db_attachment = row.try_get("media_attachment")?;
|
||||
Ok(db_attachment)
|
||||
|
@ -68,14 +68,16 @@ pub async fn delete_unused_attachments(
|
|||
db_client: &impl DatabaseClient,
|
||||
created_before: &DateTime<Utc>,
|
||||
) -> Result<DeletionQueue, DatabaseError> {
|
||||
let rows = db_client.query(
|
||||
"
|
||||
let rows = db_client
|
||||
.query(
|
||||
"
|
||||
DELETE FROM media_attachment
|
||||
WHERE post_id IS NULL AND created_at < $1
|
||||
RETURNING file_name, ipfs_cid
|
||||
",
|
||||
&[&created_before],
|
||||
).await?;
|
||||
&[&created_before],
|
||||
)
|
||||
.await?;
|
||||
let mut files = vec![];
|
||||
let mut ipfs_objects = vec![];
|
||||
for row in rows {
|
||||
|
@ -84,7 +86,7 @@ pub async fn delete_unused_attachments(
|
|||
if let Some(ipfs_cid) = row.try_get("ipfs_cid")? {
|
||||
ipfs_objects.push(ipfs_cid);
|
||||
};
|
||||
};
|
||||
}
|
||||
let orphaned_files = find_orphaned_files(db_client, files).await?;
|
||||
let orphaned_ipfs_objects = find_orphaned_ipfs_objects(db_client, ipfs_objects).await?;
|
||||
Ok(DeletionQueue {
|
||||
|
@ -95,18 +97,15 @@ pub async fn delete_unused_attachments(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serial_test::serial;
|
||||
use crate::database::test_utils::create_test_database;
|
||||
use crate::models::{
|
||||
profiles::types::ProfileCreateData,
|
||||
profiles::queries::create_profile,
|
||||
};
|
||||
use super::*;
|
||||
use crate::database::test_utils::create_test_database;
|
||||
use crate::profiles::{queries::create_profile, types::ProfileCreateData};
|
||||
use serial_test::serial;
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_create_attachment() {
|
||||
let db_client = &create_test_database().await;
|
||||
let db_client = &mut create_test_database().await;
|
||||
let profile_data = ProfileCreateData {
|
||||
username: "test".to_string(),
|
||||
..Default::default()
|
||||
|
@ -121,11 +120,13 @@ mod tests {
|
|||
file_name.to_string(),
|
||||
file_size,
|
||||
Some(media_type.to_string()),
|
||||
).await.unwrap();
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(attachment.owner_id, profile.id);
|
||||
assert_eq!(attachment.file_name, file_name);
|
||||
assert_eq!(attachment.file_size.unwrap(), file_size as i32);
|
||||
assert_eq!(attachment.media_type.unwrap(), media_type);
|
||||
assert_eq!(attachment.post_id.is_none(), true);
|
||||
assert!(attachment.post_id.is_none());
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ pub enum AttachmentType {
|
|||
Unknown,
|
||||
Image,
|
||||
Video,
|
||||
Audio,
|
||||
}
|
||||
|
||||
impl AttachmentType {
|
||||
|
@ -29,10 +30,12 @@ impl AttachmentType {
|
|||
Self::Image
|
||||
} else if media_type.starts_with("video/") {
|
||||
Self::Video
|
||||
} else if media_type.starts_with("audio/") {
|
||||
Self::Audio
|
||||
} else {
|
||||
Self::Unknown
|
||||
}
|
||||
},
|
||||
}
|
||||
None => Self::Unknown,
|
||||
}
|
||||
}
|
|
@ -2,8 +2,8 @@ use chrono::{DateTime, Utc};
|
|||
use serde_json::Value;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::{DatabaseClient, DatabaseError};
|
||||
use super::types::{DbBackgroundJob, JobStatus, JobType};
|
||||
use crate::database::{DatabaseClient, DatabaseError};
|
||||
|
||||
pub async fn enqueue_job(
|
||||
db_client: &impl DatabaseClient,
|
||||
|
@ -12,8 +12,9 @@ pub async fn enqueue_job(
|
|||
scheduled_for: &DateTime<Utc>,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let job_id = Uuid::new_v4();
|
||||
db_client.execute(
|
||||
"
|
||||
db_client
|
||||
.execute(
|
||||
"
|
||||
INSERT INTO background_job (
|
||||
id,
|
||||
job_type,
|
||||
|
@ -22,18 +23,23 @@ pub async fn enqueue_job(
|
|||
)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
",
|
||||
&[&job_id, &job_type, &job_data, &scheduled_for],
|
||||
).await?;
|
||||
&[&job_id, &job_type, &job_data, &scheduled_for],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_job_batch(
|
||||
db_client: &impl DatabaseClient,
|
||||
job_type: &JobType,
|
||||
batch_size: i64,
|
||||
batch_size: u32,
|
||||
job_timeout: u32,
|
||||
) -> Result<Vec<DbBackgroundJob>, DatabaseError> {
|
||||
let rows = db_client.query(
|
||||
"
|
||||
// https://github.com/sfackler/rust-postgres/issues/60
|
||||
let job_timeout_pg = format!("{}S", job_timeout); // interval
|
||||
let rows = db_client
|
||||
.query(
|
||||
"
|
||||
UPDATE background_job
|
||||
SET
|
||||
job_status = $1,
|
||||
|
@ -43,21 +49,33 @@ pub async fn get_job_batch(
|
|||
FROM background_job
|
||||
WHERE
|
||||
job_type = $2
|
||||
AND job_status = $3
|
||||
AND scheduled_for < CURRENT_TIMESTAMP
|
||||
ORDER BY scheduled_for ASC
|
||||
AND (
|
||||
-- queued
|
||||
job_status = $3
|
||||
-- running
|
||||
OR job_status = $1
|
||||
AND updated_at < CURRENT_TIMESTAMP - $5::text::interval
|
||||
)
|
||||
ORDER BY
|
||||
-- queued jobs first
|
||||
job_status ASC,
|
||||
scheduled_for ASC
|
||||
LIMIT $4
|
||||
)
|
||||
RETURNING background_job
|
||||
",
|
||||
&[
|
||||
&JobStatus::Running,
|
||||
&job_type,
|
||||
&JobStatus::Queued,
|
||||
&batch_size,
|
||||
],
|
||||
).await?;
|
||||
let jobs = rows.iter()
|
||||
&[
|
||||
&JobStatus::Running,
|
||||
&job_type,
|
||||
&JobStatus::Queued,
|
||||
&i64::from(batch_size),
|
||||
&job_timeout_pg,
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
let jobs = rows
|
||||
.iter()
|
||||
.map(|row| row.try_get("background_job"))
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(jobs)
|
||||
|
@ -67,13 +85,15 @@ pub async fn delete_job_from_queue(
|
|||
db_client: &impl DatabaseClient,
|
||||
job_id: &Uuid,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let deleted_count = db_client.execute(
|
||||
"
|
||||
let deleted_count = db_client
|
||||
.execute(
|
||||
"
|
||||
DELETE FROM background_job
|
||||
WHERE id = $1
|
||||
",
|
||||
&[&job_id],
|
||||
).await?;
|
||||
&[&job_id],
|
||||
)
|
||||
.await?;
|
||||
if deleted_count == 0 {
|
||||
return Err(DatabaseError::NotFound("background job"));
|
||||
};
|
||||
|
@ -82,10 +102,10 @@ pub async fn delete_job_from_queue(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::database::test_utils::create_test_database;
|
||||
use serde_json::json;
|
||||
use serial_test::serial;
|
||||
use crate::database::test_utils::create_test_database;
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
|
@ -98,20 +118,22 @@ mod tests {
|
|||
"failure_count": 0,
|
||||
});
|
||||
let scheduled_for = Utc::now();
|
||||
enqueue_job(db_client, &job_type, &job_data, &scheduled_for).await.unwrap();
|
||||
enqueue_job(db_client, &job_type, &job_data, &scheduled_for)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let batch_1 = get_job_batch(db_client, &job_type, 10).await.unwrap();
|
||||
let batch_1 = get_job_batch(db_client, &job_type, 10, 3600).await.unwrap();
|
||||
assert_eq!(batch_1.len(), 1);
|
||||
let job = &batch_1[0];
|
||||
assert_eq!(job.job_type, job_type);
|
||||
assert_eq!(job.job_data, job_data);
|
||||
assert_eq!(job.job_status, JobStatus::Running);
|
||||
|
||||
let batch_2 = get_job_batch(db_client, &job_type, 10).await.unwrap();
|
||||
let batch_2 = get_job_batch(db_client, &job_type, 10, 3600).await.unwrap();
|
||||
assert_eq!(batch_2.len(), 0);
|
||||
|
||||
delete_job_from_queue(db_client, &job.id).await.unwrap();
|
||||
let batch_3 = get_job_batch(db_client, &job_type, 10).await.unwrap();
|
||||
let batch_3 = get_job_batch(db_client, &job_type, 10, 3600).await.unwrap();
|
||||
assert_eq!(batch_3.len(), 0);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde_json::Value;
|
||||
use postgres_types::FromSql;
|
||||
use serde_json::Value;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::{
|
|
@ -1,40 +1,17 @@
|
|||
use mitra_config::Config;
|
||||
|
||||
use crate::database::{DatabaseClient, DatabaseError};
|
||||
use crate::ipfs::store as ipfs_store;
|
||||
use crate::media::remove_files;
|
||||
|
||||
pub struct DeletionQueue {
|
||||
pub files: Vec<String>,
|
||||
pub ipfs_objects: Vec<String>,
|
||||
}
|
||||
|
||||
impl DeletionQueue {
|
||||
pub async fn process(self, config: &Config) -> () {
|
||||
remove_files(self.files, &config.media_dir());
|
||||
if !self.ipfs_objects.is_empty() {
|
||||
match &config.ipfs_api_url {
|
||||
Some(ipfs_api_url) => {
|
||||
ipfs_store::remove(ipfs_api_url, self.ipfs_objects).await
|
||||
.unwrap_or_else(|err| log::error!("{}", err));
|
||||
},
|
||||
None => {
|
||||
log::error!(
|
||||
"can not remove objects because IPFS API URL is not set: {:?}",
|
||||
self.ipfs_objects,
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_orphaned_files(
|
||||
db_client: &impl DatabaseClient,
|
||||
files: Vec<String>,
|
||||
) -> Result<Vec<String>, DatabaseError> {
|
||||
let rows = db_client.query(
|
||||
"
|
||||
let rows = db_client
|
||||
.query(
|
||||
"
|
||||
SELECT DISTINCT fname
|
||||
FROM unnest($1::text[]) AS fname
|
||||
WHERE
|
||||
|
@ -51,9 +28,11 @@ pub async fn find_orphaned_files(
|
|||
WHERE image ->> 'file_name' = fname
|
||||
)
|
||||
",
|
||||
&[&files],
|
||||
).await?;
|
||||
let orphaned_files = rows.iter()
|
||||
&[&files],
|
||||
)
|
||||
.await?;
|
||||
let orphaned_files = rows
|
||||
.iter()
|
||||
.map(|row| row.try_get("fname"))
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(orphaned_files)
|
||||
|
@ -63,8 +42,9 @@ pub async fn find_orphaned_ipfs_objects(
|
|||
db_client: &impl DatabaseClient,
|
||||
ipfs_objects: Vec<String>,
|
||||
) -> Result<Vec<String>, DatabaseError> {
|
||||
let rows = db_client.query(
|
||||
"
|
||||
let rows = db_client
|
||||
.query(
|
||||
"
|
||||
SELECT DISTINCT cid
|
||||
FROM unnest($1::text[]) AS cid
|
||||
WHERE
|
||||
|
@ -75,9 +55,11 @@ pub async fn find_orphaned_ipfs_objects(
|
|||
SELECT 1 FROM post WHERE ipfs_cid = cid
|
||||
)
|
||||
",
|
||||
&[&ipfs_objects],
|
||||
).await?;
|
||||
let orphaned_ipfs_objects = rows.iter()
|
||||
&[&ipfs_objects],
|
||||
)
|
||||
.await?;
|
||||
let orphaned_ipfs_objects = rows
|
||||
.iter()
|
||||
.map(|row| row.try_get("cid"))
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(orphaned_ipfs_objects)
|
|
@ -12,7 +12,7 @@ macro_rules! int_enum_from_sql {
|
|||
|
||||
postgres_types::accepts!(INT2);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! int_enum_to_sql {
|
||||
|
@ -31,7 +31,7 @@ macro_rules! int_enum_to_sql {
|
|||
postgres_types::accepts!(INT2);
|
||||
postgres_types::to_sql_checked!();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use {int_enum_from_sql, int_enum_to_sql};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue