Compare commits

...

133 commits

Author SHA1 Message Date
Rafael Caricio d2075dcbd2
Apply clippy suggestions
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-28 00:15:44 +02:00
Rafael Caricio 7281340423
Apply clippy suggestions
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-28 00:01:24 +02:00
Rafael Caricio 406781570c
Fix fmt
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-27 23:35:08 +02:00
Rafael Caricio 205ea8c5b8
Fix fmt
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-27 23:30:41 +02:00
Rafael Caricio 9944095959
Fix CI
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-27 23:29:27 +02:00
Rafael Caricio 13d6fa25bc
Fix formatting
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-27 23:28:32 +02:00
Rafael Caricio 456d0789fb
Better CI
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-27 23:27:00 +02:00
Rafael Caricio f73a05439d
Remove println
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-27 23:24:50 +02:00
Rafael Caricio 4df257d024
Noticed the branch was wrong.. 🤦
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-27 23:24:08 +02:00
Rafael Caricio 44004de576
OK, run CI always
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-27 23:23:03 +02:00
Rafael Caricio 93be6f8559
Fix imports
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-27 23:20:59 +02:00
Rafael Caricio 3860b4780d
CI fix loop
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-27 22:46:23 +02:00
Rafael Caricio de6072026b
Fix missing import in tests
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-27 22:45:30 +02:00
Rafael Caricio 2b44c8e20d
Run test on Rust file changes
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-27 22:41:40 +02:00
Rafael Caricio d8bea2c868
Fix test db setup
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-27 22:40:28 +02:00
Rafael Caricio 5d0b03c2a3
Only run cargo when Rust code is changed
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-27 22:13:05 +02:00
Rafael Caricio a2bc297f0e
Update readme 2023-04-27 22:00:02 +02:00
Rafael Caricio fe8380e359
Test CI
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-27 21:27:05 +02:00
Rafael Caricio 22883798b3
Don't notify when muted 2023-04-27 21:26:18 +02:00
Rafael Caricio cb61f4a86b
Allow mute accounts 2023-04-27 13:38:49 +02:00
Rafael Caricio bb28bf800d
Make API response compatible with what IceCubes iOS client expects 2023-04-26 22:10:34 +02:00
Rafael Caricio 60a27b5b11
Make generic errors carry more details 2023-04-26 12:55:42 +02:00
Rafael Caricio 0d77557ad6
Simplify deployment workflow 2023-04-26 12:54:48 +02:00
Rafael Caricio 1e40a42524
Support database connection via SSL
This is required to use managed Postgres databases. It is necessary to
use SSL connection to the remote host as the connection goes through the
open internet.
2023-04-26 12:07:36 +02:00
Rafael Caricio b7fafe6458
Rename to Fedimovies 2023-04-25 15:49:35 +02:00
Rafael Caricio 17d8c11726
Retoot reviews 2023-04-25 13:19:04 +02:00
Rafael Caricio b049b75873
TMDB API integration 2023-04-25 11:09:30 +02:00
Rafael Caricio 272d06897a
In this server all accounts are bots 2023-04-24 18:57:48 +02:00
Rafael Caricio 47529ff703
Apply cargo fmt 2023-04-24 17:35:32 +02:00
silverpill b4ff7abbc1
Bump version 2023-04-24 16:59:37 +02:00
silverpill 5906185154
Add federation.i2p_proxy_url configuration parameter 2023-04-24 16:59:26 +02:00
silverpill b3b62a9c7f
Make onion_proxy_url override proxy_url setting if request target is onion 2023-04-24 16:59:13 +02:00
silverpill b77d4a9bdf
Add replies and reposts to outbox collection 2023-04-24 16:59:01 +02:00
silverpill b6e7fa5d13
Support integrity proofs with DataIntegrityProof type 2023-04-24 16:58:05 +02:00
silverpill 05eeb5ae2a
Deserialize actor attachments into Value array 2023-04-24 16:57:14 +02:00
silverpill f41b205084
Add support for content warnings 2023-04-24 16:56:59 +02:00
silverpill 1302611731
Update actix 2023-04-24 16:56:47 +02:00
silverpill 469a5484a1
Update openssl crate 2023-04-24 16:55:20 +02:00
silverpill 7471c03ed1
Make /api/v1/accounts/{account_id}/follow work with form-data 2023-04-24 16:53:18 +02:00
silverpill e2ea58d33a
Make activity limit in outbox fetcher adjustable 2023-04-24 16:51:51 +02:00
silverpill a3f44cf678
Ignore errors when importing activities from outbox 2023-04-24 16:50:36 +02:00
silverpill 69caf0b5bc
Bump version 2023-04-24 16:50:02 +02:00
silverpill 01cefa6ea1
Disable spam detection when importing activities from outbox 2023-04-24 16:49:52 +02:00
silverpill 1092319f6e
Fix database query error in Create activity handler 2023-04-24 16:49:39 +02:00
silverpill f8df50934c
Explain database client errors 2023-04-24 16:49:22 +02:00
silverpill 7f6ebb89c0
Add read-outbox command 2023-04-24 16:49:08 +02:00
silverpill c022e0d320
Add actor validation to Update(Note) and Undo(Follow) handlers 2023-04-24 16:46:49 +02:00
silverpill b6abcf252a
Remove unused query_params parameter from send_request 2023-04-24 16:46:37 +02:00
silverpill 55c0b1eb6b
Re-fetch object if attributedTo value doesn't match actor of Create activity 2023-04-24 16:46:22 +02:00
silverpill 8daf566eb2
Add create-user command 2023-04-24 16:26:31 +02:00
silverpill 533ef48393
Check mention and link counts when creating post 2023-04-24 16:15:45 +02:00
silverpill 8533a892bf
Add emoji count check to profile data validator 2023-04-24 16:15:21 +02:00
Rafael Caricio ad3ea0e7ca
When running locally with .example.com domain use http 2023-04-24 16:10:50 +02:00
Rafael Caricio 83286b7522
No blockchain support 2023-04-24 16:10:25 +02:00
Rafael Caricio c5dbb0257f
No eth support 2023-04-24 16:10:08 +02:00
Rafael Caricio c0049e6d49
Delete some more 2023-04-09 01:34:07 +02:00
Rafael Caricio e5be1326de
Nop 2023-04-08 21:30:21 +02:00
Rafael Caricio 89e3f93592
Rm web3 lolz 2023-04-08 21:29:03 +02:00
Rafael Caricio 5ef024d923
Fuck blockchain 2023-04-08 21:20:12 +02:00
silverpill cdb728a70a Bump version 2023-04-06 23:54:11 +00:00
silverpill 8708abd9cd Reject unsolicited public posts 2023-04-06 23:00:08 +00:00
silverpill fc82c83421 Create API endpoint for managing client configurations 2023-04-06 21:59:57 +00:00
silverpill 01494f1770 Increase fetcher timeout to 15 seconds when processing search queries 2023-04-06 16:49:32 +00:00
silverpill 278950252e Refactor get_object_visibility function 2023-04-06 16:14:35 +00:00
silverpill 7c38c0a4d6 Increase object ID size limit to 2000 chars 2023-04-06 16:04:09 +00:00
silverpill e950189086 Validate emoji name length before saving to database 2023-04-06 15:54:26 +00:00
silverpill 970071a9f0 Validate object ID length before saving post to database 2023-04-06 15:54:21 +00:00
silverpill 20080333d0 Add missing CHECK constraints to database tables 2023-04-05 19:50:26 +00:00
silverpill b9fdb1ccf4 Allow custom emojis with image/webp media type 2023-04-05 19:38:00 +00:00
silverpill 9768fc6228 Send Update(Person) after adding alias 2023-04-05 19:38:00 +00:00
silverpill 9e5672929b Process incoming Move() activities in background 2023-04-05 19:37:59 +00:00
silverpill dcaa2227d2 Support account migration from Mastodon 2023-04-05 19:33:58 +00:00
silverpill b0bf3cf594 Populate alsoKnownAs property on actor object with declared aliases 2023-04-05 00:08:06 +00:00
silverpill 99f6c08e9a Create API endpoint for adding aliases 2023-04-04 23:58:36 +00:00
silverpill 13df9e0478 Create /api/v1/accounts/aliases/all API endpoint 2023-04-04 23:56:38 +00:00
silverpill 59e5f12016 Support calling /api/v1/accounts/search with "resolve" parameter 2023-04-02 22:23:19 +00:00
silverpill edebae0dc6 Validate actor aliases before saving into database 2023-04-02 21:58:44 +00:00
silverpill ebbde534af Update comrak crate 2023-04-02 21:58:40 +00:00
silverpill 300d2ef6f8 Increase maximum number of custom emojis per post to 50 2023-04-01 23:24:52 +00:00
silverpill 3c2d2d124b Bump version 2023-03-31 19:26:17 +00:00
silverpill dbca8183bb Order attachments by creation date when new post is created 2023-03-31 19:23:20 +00:00
silverpill 9a32fb9c80 Remove activity from queue if handler times out 2023-03-31 17:56:13 +00:00
silverpill 779d4e7287 Process queued background jobs before re-trying stalled 2023-03-31 17:43:13 +00:00
silverpill 6604ea8a2b Limit number of mentions and links in remote posts 2023-03-31 17:05:41 +00:00
silverpill 95daa94a97 Move contents of database and models modules to mitra-models crate 2023-03-31 00:20:19 +00:00
silverpill 19780c3b8a Bump version 2023-03-30 13:56:27 +00:00
silverpill 00ca54f9b4 Update comrak to v0.17.1 2023-03-30 13:44:22 +00:00
silverpill 006665f6fb Save Monero wallet after generating addresses or sending transactions 2023-03-30 13:22:25 +00:00
silverpill 348149bbaa Set fetcher timeout to 5 seconds when processing search queries 2023-03-28 07:57:28 +00:00
silverpill dd0c53c5e9 Add federation.fetcher_timeout and federation.deliverer_timeout configuration parameters 2023-03-28 11:27:14 +04:00
silverpill 378d94e7b8 Don't reopen monero wallet on each subscription monitor run 2023-03-28 00:11:48 +00:00
silverpill 8cfb2318a2 Order attachments by creation date 2023-03-27 20:50:48 +00:00
silverpill 462da87e9b Create DbActor type and use it to represent actor_profile.actor_json column value 2023-03-27 17:43:01 +00:00
silverpill 4f9a99e6f2 Use "aliases" property in Move() activity handler 2023-03-27 02:17:59 +04:00
silverpill eb1f815548 Use manually_approves_followers field in Account::from_profile 2023-03-27 02:17:59 +04:00
silverpill b85a0fb7ac Refactor import_post function 2023-03-26 01:12:03 +00:00
silverpill 5e1f441e8b Add limits.media.emoji_size_limit configuration parameter 2023-03-25 23:11:11 +00:00
silverpill 0521f1f731 Restart stalled background jobs 2023-03-25 20:23:19 +00:00
silverpill 5ba8b8d6ae Move microsyntax parsers to mastodon_api::statuses::microsyntax module 2023-03-25 18:19:10 +00:00
silverpill ac6491d030 Set database connection pool size in mitra::main 2023-03-25 17:34:50 +00:00
silverpill 08d7482f32 Increase remote emoji size limit to 500 kB 2023-03-25 12:24:33 +00:00
silverpill 5450ba8871 Set log size limit in monero-wallet-rpc config example 2023-03-25 12:18:22 +00:00
silverpill 399a632a88 Prune remote emojis in background 2023-03-25 12:18:22 +00:00
silverpill ef852d781e Move profile data cleaning code to validators::profiles module 2023-03-25 11:28:48 +00:00
silverpill 441850dd21 Allow emoji names containing hyphens 2023-03-25 11:28:48 +00:00
silverpill 73b576c643 Move normalize_hashtag function to activitypub::handlers::create 2023-03-25 11:28:48 +00:00
silverpill f5dd0a17c9 Move all validators to validators module 2023-03-25 11:28:48 +00:00
silverpill 37ab3dc456 Add prune-remote-emojis command 2023-03-25 11:26:52 +00:00
silverpill 76e85a3b7b Fix error in emoji update SQL query 2023-03-24 22:54:51 +00:00
silverpill 521c2cbe41 Move mention_to_address function to webfinger::types module 2023-03-23 19:19:17 +00:00
silverpill 3b5c8a4131 Remove Role::from_name function 2023-03-23 00:29:32 +00:00
silverpill 21135d7704 Move get_post_by_object_id to activitypub::fetcher::helpers module 2023-03-22 23:13:31 +00:00
silverpill dae9be1388 Bump version 2023-03-21 18:45:50 +00:00
silverpill 39ab6bbb13 Don't allow migration if user doesn't have identity proofs 2023-03-21 18:14:24 +00:00
silverpill cdb304a8b7 Update profile page URL template to match mitra-web 2023-03-21 16:48:14 +00:00
silverpill 848a0685de Add configuration option that disables federation 2023-03-21 16:17:44 +00:00
silverpill 608ec096cd Add /api/v1/instance/peers API endpoint 2023-03-20 17:11:00 +00:00
silverpill 28be8dbb31 Update Monero configuration guide 2023-03-19 20:31:34 +00:00
silverpill e3ee144889 Grant delete_any_post and delete_any_profile permissions to admin role 2023-03-19 19:21:31 +00:00
silverpill b80b827fde Document valid role names for set-role command 2023-03-19 19:10:51 +00:00
silverpill fcf63ff317 Revert "Use "git" protocol to access crates.io"
This reverts commit 133e1349cf.
2023-03-19 13:54:04 +04:00
silverpill 9a513c928f Add account_index parameter to Monero configuration 2023-03-18 19:02:54 +00:00
silverpill 7640598431 Replace DbActorProfile::actor_address with ActorAddress::from_profile 2023-03-18 18:29:56 +00:00
silverpill f76438b6f8 Move DbActorProfile::actor_id function to activitypub::identifiers 2023-03-18 18:29:45 +00:00
silverpill 306fd7b75b Move DbActorProfile::actor_url function to activitypub::identifiers 2023-03-18 18:29:34 +00:00
silverpill a515af1111 Move Post::object_id function to activitypub::identifiers 2023-03-18 18:29:15 +00:00
silverpill f27b2e13eb Add webclient redirection rule for /@username routes 2023-03-18 14:09:27 +00:00
silverpill a07d7ce34a Make webclient-to-object redirects work for remote profiles and posts 2023-03-18 13:38:46 +00:00
silverpill f037a4d58c Move media deletion helper to media module 2023-03-18 11:27:16 +00:00
silverpill b56e11e81d Add "aliases" column to actor profile table
It is used to store unverified aliases,
and potentially can be used for verified aliases too.
2023-03-17 20:27:50 +00:00
silverpill c80bfccd6a Add "manually_approves_followers" column to actor_profile table 2023-03-17 19:20:42 +00:00
silverpill 0b65e7473e Move DID types to mitra-utils crate 2023-03-16 17:59:45 +00:00
silverpill 1637e38ee4 Add fep-e232 feature flag 2023-03-15 20:05:21 +00:00
310 changed files with 8713 additions and 12240 deletions

View file

@ -1,6 +1,3 @@
[registries.crates-io]
protocol = "git"
# https://github.com/rust-lang/cargo/issues/5034#issuecomment-927105016
[target.'cfg(feature = "cargo-clippy")']
rustflags = [

21
.dockerignore Normal file
View 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
View file

@ -1,5 +1,9 @@
.env.local
config.yaml
/secret/*
/files/*
!/files/.gitkeep
/build/*
!/build/.gitkeep
/target
fly.toml

63
.woodpecker.yml Normal file
View 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

View file

@ -6,6 +6,130 @@ 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

2157
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,36 +1,39 @@
[package]
name = "mitra"
version = "1.17.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",
"mitra-config",
"mitra-utils",
"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"
@ -38,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
@ -50,20 +50,14 @@ env_logger = { version = "0.9.0", default-features = false }
ed25519-dalek = "1.0.1"
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.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"
@ -74,28 +68,18 @@ siwe = "0.4.0"
# Used for creating error types
thiserror = "1.0.37"
# Async runtime
tokio = { version = "1.20.4", 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]
ethereum-extras = []
production = ["mitra-config/production"]
production = ["fedimovies-config/production"]

102
README.md
View file

@ -1,6 +1,7 @@
# Mitra
# FediMovies
[![status-badge](https://ci.caric.io/api/badges/FediMovies/fedimovies/status.svg)](https://ci.caric.io/FediMovies/fedimovies)
Federated micro-blogging platform.
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).
@ -8,41 +9,30 @@ Features:
- Micro-blogging service (includes support for quote posts, custom emojis and more).
- Mastodon API.
- Content subscription service. Subscriptions provide a way to receive monthly payments from subscribers and to publish private content made exclusively for them.
- Supported payment methods: [Monero](https://www.getmonero.org/get-started/what-is-monero/) and [ERC-20](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/) tokens (on Ethereum and other EVM-compatible blockchains).
- [Sign-in with a wallet](https://eips.ethereum.org/EIPS/eip-4361).
- Donation buttons.
- Account migrations (from one server to another). Identity can be detached from the server.
- Federation over Tor.
Follow: [@mitra@mitra.social](https://mitra.social/@mitra)
Matrix chat: [#mitra:halogen.city](https://matrix.to/#/#mitra:halogen.city)
## Instances
- [FediList](http://demo.fedilist.com/instance?software=mitra)
- [Fediverse Observer](https://mitra.fediverse.observer/list)
- [FediList](http://demo.fedilist.com/instance?software=fedimovies)
- [Fediverse Observer](https://fedimovies.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
@ -55,76 +45,58 @@ 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).
### Tor federation
See [guide](./docs/onion.md).
### Monero
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.
## Development
See [CONTRIBUTING.md](./CONTRIBUTING.md)
@ -138,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
@ -166,7 +130,7 @@ cargo run
### Run CLI
```shell
cargo run --bin mitractl
cargo run --bin fedimoviesctl
```
### Run linter
@ -187,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
View file

View 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'

View file

@ -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": {}
}

View file

@ -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": {}
}

View file

@ -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": {}
}

View file

@ -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": {}
}

View file

@ -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": {}
}

View file

@ -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": {}
}

View file

@ -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
View 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"]

View file

@ -47,6 +47,7 @@ retention:
# Federation parameters
#federation:
# enabled: true
# # Proxy for outgoing requests
# #proxy_url: 'socks5h://127.0.0.1:9050'
# # Proxy for outgoing requests to .onion targets
@ -55,31 +56,6 @@ retention:
# 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)

View file

@ -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

View file

@ -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.
@ -544,23 +550,6 @@ paths:
$ref: '#/components/schemas/Subscription'
404:
description: Profile not found
/api/v1/accounts/{account_id}/aliases:
get:
summary: Get actor's 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}/follow:
post:
summary: Follow the given actor.
@ -607,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.
@ -760,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.
@ -783,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
@ -894,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
@ -1462,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:
@ -1470,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:
@ -1782,6 +1868,8 @@ components:
enum:
- create_follow_request
- create_post
- delete_any_post
- delete_any_profile
- manage_subscription_options
Signature:
type: object
@ -1823,6 +1911,10 @@ 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

View file

@ -1,18 +1,19 @@
[package]
name = "mitra-cli"
version = "1.17.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"

View file

@ -1,59 +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,
find_unreachable,
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,
};
@ -72,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),
@ -82,6 +64,7 @@ pub enum SubCommand {
DeleteUnusedAttachments(DeleteUnusedAttachments),
DeleteOrphanedFiles(DeleteOrphanedFiles),
DeleteEmptyProfiles(DeleteEmptyProfiles),
PruneRemoteEmojis(PruneRemoteEmojis),
ListUnreachableActors(ListUnreachableActors),
ImportEmoji(ImportEmoji),
UpdateCurrentBlock(UpdateCurrentBlock),
@ -108,12 +91,7 @@ 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");
}
}
@ -124,14 +102,8 @@ pub struct GenerateInviteCode {
}
impl GenerateInviteCode {
pub async fn execute(
&self,
db_client: &impl DatabaseClient,
) -> Result<(), Error> {
let invite_code = create_invite_code(
db_client,
self.note.as_deref(),
).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(())
}
@ -142,10 +114,7 @@ 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");
@ -157,7 +126,37 @@ impl ListInviteCodes {
} 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(())
}
}
@ -170,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
@ -187,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(())
@ -214,23 +208,40 @@ impl RefetchActor {
config: &Config,
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 {
@ -247,12 +258,11 @@ 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.enqueue(db_client).await?;
@ -278,16 +288,12 @@ 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.enqueue(db_client).await?;
@ -310,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(())
}
@ -338,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(())
}
}
@ -358,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(())
}
@ -381,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() {
@ -412,9 +411,29 @@ 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(())
}
}
@ -444,7 +463,7 @@ impl ListUnreachableActors {
profile.unreachable_since.unwrap().to_string(),
profile.updated_at.to_string(),
);
};
}
Ok(())
}
}
@ -462,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(());
@ -478,7 +494,8 @@ impl ImportEmoji {
emoji.image,
None,
&get_min_datetime(),
).await?;
)
.await?;
println!("added emoji to local collection");
Ok(())
}
@ -494,9 +511,8 @@ impl UpdateCurrentBlock {
pub async fn execute(
&self,
_config: &Config,
db_client: &impl DatabaseClient,
_db_client: &impl DatabaseClient,
) -> Result<(), Error> {
save_current_block_number(db_client, self.number).await?;
println!("current block updated");
Ok(())
}
@ -531,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(())
}
@ -557,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(())
}
}

View 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!(),
};
}
};
}

View file

@ -1,12 +1,12 @@
[package]
name = "mitra-config"
version = "1.17.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"

View file

@ -1,23 +1,26 @@
use std::path::PathBuf;
use log::{Level as LogLevel};
use log::Level as LogLevel;
use rsa::RsaPrivateKey;
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;
fn default_log_level() -> LogLevel { LogLevel::Info }
fn default_log_level() -> LogLevel {
LogLevel::Info
}
fn default_login_message() -> String { "Do not sign this message on other sites!".to_string() }
fn default_login_message() -> String {
"What?!".to_string()
}
#[derive(Clone, Deserialize)]
pub struct Config {
@ -30,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>,
@ -50,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>,
@ -73,17 +83,11 @@ pub struct Config {
pub(super) proxy_url: Option<String>,
#[serde(default)]
pub(super) federation: FederationConfig,
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>,
@ -100,7 +104,12 @@ impl Config {
actor_key: self.instance_rsa_key.clone().unwrap(),
proxy_url: self.federation.proxy_url.clone(),
onion_proxy_url: self.federation.onion_proxy_url.clone(),
is_private: matches!(self.environment, Environment::Development),
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,
}
}
@ -111,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)]
@ -133,8 +130,11 @@ pub struct Instance {
// 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 {
@ -148,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(),
)
}
}
@ -158,21 +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() {
@ -183,14 +186,17 @@ mod tests {
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),
);
}
@ -203,7 +209,10 @@ mod tests {
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");

View file

@ -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 {

View 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,
}
}
}

View file

@ -1,4 +1,3 @@
mod blockchain;
mod config;
mod environment;
mod federation;
@ -7,17 +6,12 @@ mod loader;
mod registration;
mod retention;
pub use blockchain::{
BlockchainConfig,
EthereumConfig,
MoneroConfig,
};
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}")]

View file

@ -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 {

View file

@ -4,12 +4,8 @@ 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},
};
@ -23,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,
@ -45,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 {
@ -63,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)
@ -83,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
@ -102,14 +95,6 @@ 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");
};
@ -117,7 +102,8 @@ pub fn parse_config() -> (Config, Vec<&'static str>) {
// 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 {

View file

@ -1,8 +1,4 @@
use serde::{
Deserialize,
Deserializer,
de::Error as DeserializerError,
};
use serde::{de::Error as DeserializerError, Deserialize, Deserializer};
#[derive(Clone, PartialEq)]
pub enum RegistrationType {
@ -11,12 +7,15 @@ pub enum RegistrationType {
}
impl Default for RegistrationType {
fn default() -> Self { Self::Invite }
fn default() -> Self {
Self::Invite
}
}
impl<'de> Deserialize<'de> for RegistrationType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: Deserializer<'de>
where
D: Deserializer<'de>,
{
let registration_type_str = String::deserialize(deserializer)?;
let registration_type = match registration_type_str.as_str() {
@ -35,12 +34,15 @@ pub enum DefaultRole {
}
impl Default for DefaultRole {
fn default() -> Self { Self::NormalUser }
fn default() -> Self {
Self::NormalUser
}
}
impl<'de> Deserialize<'de> for DefaultRole {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: Deserializer<'de>
where
D: Deserializer<'de>,
{
let role_str = String::deserialize(deserializer)?;
let role = match role_str.as_str() {

View 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 = []

View file

@ -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;

View file

@ -0,0 +1 @@
ALTER TABLE actor_profile ADD COLUMN aliases JSONB NOT NULL DEFAULT '[]';

View file

@ -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);

View file

@ -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);

View file

@ -0,0 +1 @@
ALTER TABLE user_account ADD COLUMN client_config JSONB NOT NULL DEFAULT '{}';

View file

@ -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;

View file

@ -26,16 +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,
@ -56,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()
);
@ -64,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
@ -93,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 (
@ -111,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),
@ -128,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)
);
@ -164,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 (
@ -172,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))
@ -217,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 (
@ -228,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)
);

View file

@ -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,13 +97,10 @@ 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]
@ -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());
}
}

View file

@ -35,7 +35,7 @@ impl AttachmentType {
} else {
Self::Unknown
}
},
}
None => Self::Unknown,
}
}

View file

@ -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,8 +23,9 @@ 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(())
}
@ -31,9 +33,13 @@ pub async fn get_job_batch(
db_client: &impl DatabaseClient,
job_type: &JobType,
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,
&i64::from(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);
}
}

View file

@ -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::{

View file

@ -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)

View file

@ -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};

View file

@ -14,7 +14,7 @@ macro_rules! json_from_sql {
postgres_types::accepts!(JSON, JSONB);
}
}
};
}
/// Implements ToSql trait for any serializable type
@ -33,7 +33,7 @@ macro_rules! json_to_sql {
postgres_types::accepts!(JSON, JSONB);
postgres_types::to_sql_checked!();
}
}
};
}
pub(crate) use {json_from_sql, json_to_sql};

View file

@ -8,7 +8,8 @@ mod embedded {
pub async fn apply_migrations(db_client: &mut Client) {
let migration_report = embedded::migrations::runner()
.run_async(db_client)
.await.unwrap();
.await
.unwrap();
for migration in migration_report.applied_migrations() {
log::info!(

View file

@ -0,0 +1,113 @@
use openssl::ssl::{SslConnector, SslMethod};
use postgres_openssl::MakeTlsConnector;
use std::path::Path;
use tokio_postgres::config::Config as DatabaseConfig;
use tokio_postgres::error::{Error as PgError, SqlState};
pub mod int_enum;
pub mod json_macro;
pub mod migrate;
pub mod query_macro;
#[cfg(feature = "test-utils")]
pub mod test_utils;
pub type DbPool = deadpool_postgres::Pool;
pub use tokio_postgres::GenericClient as DatabaseClient;
#[derive(thiserror::Error, Debug)]
#[error("database type error")]
pub struct DatabaseTypeError;
#[derive(thiserror::Error, Debug)]
pub enum DatabaseError {
#[error("database pool error")]
DatabasePoolError(#[from] deadpool_postgres::PoolError),
#[error("database query error")]
DatabaseQueryError(#[from] postgres_query::Error),
#[error("database client error")]
DatabaseClientError(#[from] tokio_postgres::Error),
#[error(transparent)]
DatabaseTypeError(#[from] DatabaseTypeError),
#[error("{0} not found")]
NotFound(&'static str), // object type
#[error("{0} already exists")]
AlreadyExists(&'static str), // object type
}
pub async fn create_database_client(
db_config: &DatabaseConfig,
ca_file_path: Option<&Path>,
) -> tokio_postgres::Client {
let client = if let Some(ca_file_path) = ca_file_path {
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
log::debug!("Using TLS CA file: {}", ca_file_path.display());
builder.set_ca_file(ca_file_path).unwrap();
let connector = MakeTlsConnector::new(builder.build());
let (client, connection) = db_config.connect(connector).await.unwrap();
tokio::spawn(async move {
if let Err(err) = connection.await {
log::error!("connection with tls error: {}", err);
};
});
client
} else {
let (client, connection) = db_config.connect(tokio_postgres::NoTls).await.unwrap();
tokio::spawn(async move {
if let Err(err) = connection.await {
log::error!("connection error: {}", err);
};
});
client
};
client
}
pub fn create_pool(database_url: &str, ca_file_path: Option<&Path>, pool_size: usize) -> DbPool {
let manager = if let Some(ca_file_path) = ca_file_path {
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
log::info!("Using TLS CA file: {}", ca_file_path.display());
builder.set_ca_file(ca_file_path).unwrap();
let connector = MakeTlsConnector::new(builder.build());
deadpool_postgres::Manager::new(
database_url.parse().expect("invalid database URL"),
connector,
)
} else {
deadpool_postgres::Manager::new(
database_url.parse().expect("invalid database URL"),
tokio_postgres::NoTls,
)
};
DbPool::builder(manager)
.max_size(pool_size)
.build()
.unwrap()
}
pub async fn get_database_client(
db_pool: &DbPool,
) -> Result<deadpool_postgres::Client, DatabaseError> {
// Returns wrapped client
// https://github.com/bikeshedder/deadpool/issues/56
let client = db_pool.get().await?;
Ok(client)
}
pub fn catch_unique_violation(object_type: &'static str) -> impl Fn(PgError) -> DatabaseError {
move |err| {
if let Some(code) = err.code() {
if code == &SqlState::UNIQUE_VIOLATION {
return DatabaseError::AlreadyExists(object_type);
};
};
err.into()
}
}

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