Compare commits

...

193 commits

Author SHA1 Message Date
d2075dcbd2
Apply clippy suggestions
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-28 00:15:44 +02:00
7281340423
Apply clippy suggestions
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-28 00:01:24 +02:00
406781570c
Fix fmt
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-27 23:35:08 +02:00
205ea8c5b8
Fix fmt
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-27 23:30:41 +02:00
9944095959
Fix CI
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-27 23:29:27 +02:00
13d6fa25bc
Fix formatting
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-27 23:28:32 +02:00
456d0789fb
Better CI
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-27 23:27:00 +02:00
f73a05439d
Remove println
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-04-27 23:24:50 +02:00
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
44004de576
OK, run CI always
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-27 23:23:03 +02:00
93be6f8559
Fix imports
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-27 23:20:59 +02:00
3860b4780d
CI fix loop
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-27 22:46:23 +02:00
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
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
d8bea2c868
Fix test db setup
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-27 22:40:28 +02:00
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
a2bc297f0e
Update readme 2023-04-27 22:00:02 +02:00
fe8380e359
Test CI
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-04-27 21:27:05 +02:00
22883798b3
Don't notify when muted 2023-04-27 21:26:18 +02:00
cb61f4a86b
Allow mute accounts 2023-04-27 13:38:49 +02:00
bb28bf800d
Make API response compatible with what IceCubes iOS client expects 2023-04-26 22:10:34 +02:00
60a27b5b11
Make generic errors carry more details 2023-04-26 12:55:42 +02:00
0d77557ad6
Simplify deployment workflow 2023-04-26 12:54:48 +02:00
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
b7fafe6458
Rename to Fedimovies 2023-04-25 15:49:35 +02:00
17d8c11726
Retoot reviews 2023-04-25 13:19:04 +02:00
b049b75873
TMDB API integration 2023-04-25 11:09:30 +02:00
272d06897a
In this server all accounts are bots 2023-04-24 18:57:48 +02:00
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
ad3ea0e7ca
When running locally with .example.com domain use http 2023-04-24 16:10:50 +02:00
83286b7522
No blockchain support 2023-04-24 16:10:25 +02:00
c5dbb0257f
No eth support 2023-04-24 16:10:08 +02:00
c0049e6d49
Delete some more 2023-04-09 01:34:07 +02:00
e5be1326de
Nop 2023-04-08 21:30:21 +02:00
89e3f93592
Rm web3 lolz 2023-04-08 21:29:03 +02:00
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
silverpill
773c36f3df Bump version 2023-03-15 12:03:06 +00:00
silverpill
43b56be722 Remove verify_eip191_identity_proof and verify_ed25519_signature functions 2023-03-14 23:28:12 +00:00
silverpill
1d4bb200d2 Move identity::signatures to json_signatures module 2023-03-14 20:53:25 +00:00
silverpill
8b8f1bb678 Ignore emojis with non-unique names in remote posts 2023-03-14 18:48:36 +00:00
silverpill
a3a4579e03 Log invalid emoji names 2023-03-14 18:42:21 +00:00
silverpill
c7fd3ddc83 Remove hardcoded upload size limit 2023-03-14 15:17:56 +00:00
silverpill
c87c5da17c Rename SignatureType to ProofType 2023-03-13 21:29:48 +00:00
silverpill
d4f701332f Use numbers to represent identity proof type in database 2023-03-13 21:29:40 +00:00
silverpill
8400241ab4 Add audio/ogg and audio/x-wav to the list of supported media types 2023-03-12 20:14:58 +00:00
silverpill
43d084de8d Enable audio and video uploads 2023-03-12 20:13:53 +00:00
silverpill
cb7bc04b64 Change instance_description value in config example to multiline string 2023-03-12 18:45:08 +00:00
silverpill
2b6de17fb9 Save latest ethereum block number to database instead of file 2023-03-12 17:13:38 +00:00
silverpill
0e9879bacb Remove ability to switch from Ethereum devnet to another chain without resetting subscriptions
Assuming migration is over (migration started at 2eb7ec2f64).
2023-03-12 15:53:40 +00:00
silverpill
138c1e915e Allow ! after hashtags and mentions 2023-03-11 17:12:37 +00:00
silverpill
4f64b2c615 Update Tor federation guide 2023-03-11 17:09:02 +00:00
silverpill
133e1349cf Use "git" protocol to access crates.io 2023-03-11 16:53:21 +00:00
silverpill
268707a78a Disable post tokenization feature 2023-03-11 14:40:23 +00:00
silverpill
b5365099a4 Store NFT monitor state in database 2023-03-11 10:33:52 +00:00
silverpill
a300a822ec Create internal_property database table 2023-03-11 10:33:48 +00:00
silverpill
161d29e00e Bump version 2023-03-08 01:42:38 +00:00
silverpill
6d1ed4571b Remove crates with vulnerabilities 2023-03-07 23:57:00 +00:00
silverpill
50f31e96fc Add federation.onion_proxy_url configuration parameter 2023-03-07 14:53:24 +00:00
silverpill
94a5f3a3cd Implement NodeInfo 2.1 2023-03-06 19:50:12 +00:00
silverpill
0817177282 Move federation client builders to activitypub::http_client module 2023-03-06 18:50:55 +00:00
silverpill
f17c9d9f76 Add CLI command for viewing unreachable actors 2023-03-06 00:53:57 +00:00
silverpill
522fd5bafa Refresh emoji caches when emoji is deleted 2023-03-05 19:42:46 +00:00
silverpill
452de34780 Support audio attachments 2023-03-05 19:29:29 +00:00
silverpill
ba1c694294 Add "emojis" field to Mastodon API Account entity 2023-03-05 19:09:58 +00:00
silverpill
70c2d2aa25 Create profile_emoji database table 2023-03-04 21:12:02 +00:00
silverpill
2787efc83f Use .jpg extension for files with image/jpeg media type 2023-03-04 12:40:13 +00:00
silverpill
bd53e147ca Save emojis attached to actor objects 2023-03-03 23:22:04 +00:00
silverpill
4204350375 Deserialize actor tag to Vec<Value> 2023-03-03 22:11:41 +00:00
silverpill
6335e216a9 Add registration.default_role configuration option 2023-03-03 18:14:02 +00:00
silverpill
0995186bc8 Move RegistrationConfig type to mitra-config::registration module 2023-03-03 17:25:22 +00:00
silverpill
d93c2ca23d Add custom emojis section to FEDERATION.md 2023-03-02 18:39:54 +00:00
silverpill
1b1e2a1521 Allow to add notes to generated invite codes 2023-03-02 17:24:32 +00:00
silverpill
721238d897 Deserialize object tag to Vec<Value> 2023-02-28 17:02:29 +00:00
silverpill
849b201ab9 Deserialize actor attachments into Vec instead of Option<Vec> 2023-02-28 00:34:23 +00:00
silverpill
8d479ac15d Update README.md 2023-02-27 13:28:34 +00:00
silverpill
cc51c2c647 Bump version 2023-02-27 01:00:44 +00:00
silverpill
82b6c4e7cf Add /api/v1/accounts/{account_id}/aliases API endpoint 2023-02-26 23:18:45 +00:00
silverpill
baec22272d Follow FEP-e232 links when importing post 2023-02-26 23:04:56 +00:00
silverpill
49b0011a9c Fetch missing profiles before doing follower migration 2023-02-26 20:21:21 +00:00
silverpill
e02ebebe02 Process follower migration request in background 2023-02-26 20:16:43 +00:00
silverpill
62069dc011 Increase number of delivery attempts and increase intervals between them 2023-02-26 20:16:39 +00:00
silverpill
c201f3ea2b Store information about failed activity deliveries in database 2023-02-26 14:49:01 +00:00
silverpill
e4254e7a3d Add "error" and "error_description" fields to Mastodon API error responses 2023-02-25 23:38:42 +00:00
silverpill
971b541826 Change path of user's Atom feed to /feeds/users/{username} 2023-02-25 21:25:25 +00:00
silverpill
ca2e541ff5 Store delivery statuses in delivery job data
Preparing for migration to a new delivery queue mechanism.
2023-02-25 19:28:23 +00:00
silverpill
0f3c247069 Put activities generated by CLI commands in a queue 2023-02-25 19:28:23 +00:00
silverpill
49f51f44d8 Replace magic numbers in activitypub::queues module with constants 2023-02-25 19:28:19 +00:00
silverpill
f5c012769f Prefer Group actor when doing webfinger query on Lemmy server 2023-02-24 13:12:38 +00:00
silverpill
f66e0b812f Make webfinger response compatible with GNU Social account lookup 2023-02-24 11:15:44 +00:00
silverpill
56e0ed8f5d Add empty spoiler_text property to Mastodon API Status object 2023-02-24 00:26:39 +00:00
silverpill
bacb8c8380 Prevent delete-extraneous-posts command from removing locally-linked posts 2023-02-23 21:43:37 +00:00
silverpill
59a5f7b4fc Add Tor instance installation doc 2023-02-23 19:49:04 +00:00
silverpill
21ea50279e Rename proxy_url config parameter to federation.proxy_url 2023-02-23 18:02:58 +00:00
silverpill
0245eb59a2 Set deliverer timeout to 30 seconds 2023-02-22 21:54:08 +00:00
silverpill
872fe8fef3 Set fetcher timeout to 3 minutes 2023-02-22 21:48:19 +00:00
silverpill
d0b4d77519 Enable data pruning in default config 2023-02-22 20:10:29 +00:00
317 changed files with 10390 additions and 12613 deletions

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,209 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
## [1.22.0] - 2023-04-22
### Added
- Added support for content warnings.
- Support integrity proofs with `DataIntegrityProof` type.
- Add `federation.i2p_proxy_url` configuration parameter.
### Changed
- Ignore errors when importing activities from outbox.
- Make activity limit in outbox fetcher adjustable.
- Updated actix to latest version. MSRV changed to 1.57.
- Add replies and reposts to outbox collection.
### Fixed
- Make `/api/v1/accounts/{account_id}/follow` work with form-data.
- Make `onion_proxy_url` override `proxy_url` setting if request target is onion.
## [1.21.0] - 2023-04-12
### Added
- Added `create-user` command.
- Added `read-outbox` command.
### Changed
- Added emoji count check to profile data validator.
- Check mention and link counts when creating post.
- Re-fetch object if `attributedTo` value doesn't match `actor` of `Create` activity.
- Added actor validation to `Update(Note)` and `Undo(Follow)` handlers.
### Fixed
- Fixed database query error in `Create` activity handler.
## [1.20.0] - 2023-04-07
### Added
- Support calling `/api/v1/accounts/search` with `resolve` parameter.
- Created `/api/v1/accounts/aliases/all` API endpoint.
- Created API endpoint for adding aliases.
- Populate `alsoKnownAs` property on actor object with declared aliases.
- Support account migration from Mastodon.
- Created API endpoint for managing client configurations.
- Reject unsolicited public posts.
### Changed
- Increase maximum number of custom emojis per post to 50.
- Validate actor aliases before saving into database.
- Process incoming `Move()` activities in background.
- Allow custom emojis with `image/webp` media type.
- Increase object ID size limit to 2000 chars.
- Increase fetcher timeout to 15 seconds when processing search queries.
### Fixed
- Added missing `CHECK` constraints to database tables.
- Validate object ID length before saving post to database.
- Validate emoji name length before saving to database.
## [1.19.1] - 2023-03-31
### Changed
- Limit number of mentions and links in remote posts.
### Fixed
- Process queued background jobs before re-trying stalled.
- Remove activity from queue if handler times out.
- Order attachments by creation date when new post is created.
## [1.19.0] - 2023-03-30
### Added
- Added `prune-remote-emojis` command.
- Prune remote emojis in background.
- Added `limits.media.emoji_size_limit` configuration parameter.
- Added `federation.fetcher_timeout` and `federation.deliverer_timeout` configuration parameters.
### Changed
- Allow emoji names containing hyphens.
- Increased remote emoji size limit to 500 kB.
- Set fetcher timeout to 5 seconds when processing search queries.
### Fixed
- Fixed error in emoji update SQL query.
- Restart stalled background jobs.
- Order attachments by creation date.
- Don't reopen monero wallet on each subscription monitor run.
### Security
- Updated markdown parser to latest version.
## [1.18.0] - 2023-03-21
### Added
- Added `fep-e232` feature flag (disabled by default).
- Added `account_index` parameter to Monero configuration.
- Added `/api/v1/instance/peers` API endpoint.
- Added `federation.enabled` configuration parameter that can be used to disable federation.
### Changed
- Documented valid role names for `set-role` command.
- Granted `delete_any_post` and `delete_any_profile` permissions to admin role.
- Updated profile page URL template to match mitra-web.
### Fixed
- Make webclient-to-object redirects work for remote profiles and posts.
- Added webclient redirection rule for `/@username` routes.
- Don't allow migration if user doesn't have identity proofs.
## [1.17.0] - 2023-03-15
### Added
- Enabled audio and video uploads.
- Added `audio/ogg` and `audio/x-wav` to the list of supported media types.
### Changed
- Save latest ethereum block number to database instead of file.
- Removed hardcoded upload size limit.
### Deprecated
- Reading ethereum block number from `current_block` file.
### Removed
- Disabled post tokenization (can be re-enabled with `ethereum-extras` feature).
- Removed ability to switch from Ethereum devnet to another chain without resetting subscriptions.
### Fixed
- Allow `!` after hashtags and mentions.
- Ignore emojis with non-unique names in remote posts.
## [1.16.0] - 2023-03-08
### Added
- Allow to add notes to generated invite codes.
- Added `registration.default_role` configuration option.
- Save emojis attached to actor objects.
- Added `emojis` field to Mastodon API Account entity.
- Support audio attachments.
- Added CLI command for viewing unreachable actors.
- Implemented NodeInfo 2.1.
- Added `federation.onion_proxy_url` configuration parameter (enables proxy for requests to `.onion` domains).
### Changed
- Use .jpg extension for files with image/jpeg media type.
### Deprecated
- Deprecated `default_role_read_only_user` configuration option (replaced by `registration.default_role`).
## [1.15.0] - 2023-02-27
### Added
- Set fetcher timeout to 3 minutes.
- Set deliverer timeout to 30 seconds.
- Added `federation` parameter group to configuration.
- Add empty `spoiler_text` property to Mastodon API Status object.
- Added `error` and `error_description` fields to Mastodon API error responses.
- Store information about failed activity deliveries in database.
- Added `/api/v1/accounts/{account_id}/aliases` API endpoint.
### Changed
- Put activities generated by CLI commands in a queue instead of immediately sending them.
- Changed path of user's Atom feed to `/feeds/users/{username}`.
- Increase number of delivery attempts and increase intervals between them.
### Deprecated
- Deprecated `proxy_url` configuration parameter (replaced by `federation.proxy_url`).
- Deprecated Atom feeds at `/feeds/{username}`.
- Deprecated `message` field in Mastodon API error response.
### Fixed
- Prevent `delete-extraneous-posts` command from removing locally-linked posts.
- Make webfinger response compatible with GNU Social account lookup.
- Prefer `Group` actor when doing webfinger query on Lemmy server.
- Fetch missing profiles before doing follower migration.
- Follow FEP-e232 links when importing post.
## [1.14.0] - 2023-02-22
### Added

2356
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,34 +1,39 @@
[package]
name = "mitra"
version = "1.14.0"
description = "Federated micro-blogging platform and content subscription service"
name = "fedimovies"
version = "1.22.0"
description = "Movies reviews and ratings for the fediverse"
license = "AGPL-3.0"
edition = "2021"
rust-version = "1.56"
rust-version = "1.68"
publish = false
default-run = "mitra"
default-run = "fedimovies"
[workspace]
members = [
".",
"mitra-cli",
"mitra-config",
"mitra-utils",
"fedimovies-cli",
"fedimovies-config",
"fedimovies-models",
"fedimovies-utils",
]
default-members = [
".",
"mitra-cli",
"fedimovies-cli",
"fedimovies-config",
"fedimovies-models",
"fedimovies-utils",
]
[dependencies]
mitra-config = { path = "mitra-config" }
mitra-utils = { path = "mitra-utils" }
fedimovies-config = { path = "fedimovies-config" }
fedimovies-models = { path = "fedimovies-models" }
fedimovies-utils = { path = "fedimovies-utils" }
# Used to handle incoming HTTP requests
actix-cors = "0.6.2"
actix-cors = "0.6.4"
actix-files = "0.6.2"
actix-web = "4.1.0"
actix-web = "4.3.1"
actix-web-httpauth = "0.8.0"
# Used for catching errors
anyhow = "1.0.58"
@ -36,9 +41,6 @@ anyhow = "1.0.58"
base64 = "0.13.0"
# Used for working with dates
chrono = { version = "0.4.23", default-features = false, features = ["std", "serde"] }
# Used for pooling database connections
deadpool = "0.9.2"
deadpool-postgres = { version = "0.10.2", default-features = false }
# Used to work with hexadecimal strings
hex = { version = "0.4.3", features = ["serde"] }
# Used for logging
@ -46,22 +48,16 @@ log = "0.4.14"
env_logger = { version = "0.9.0", default-features = false }
# Used to verify minisign signatures
ed25519-dalek = "1.0.1"
ed25519 = "1.5.2"
ed25519 = "1.5.3"
blake2 = "0.10.5"
# Used to query Monero node
monero-rpc = "0.3.2"
# Used to determine the number of CPUs on the system
num_cpus = "1.13.0"
# Used for working with regular expressions
regex = "1.6.0"
# Used for managing database migrations
refinery = { version = "0.8.4", features = ["tokio-postgres"] }
# Used for making async HTTP requests
reqwest = { version = "0.11.10", features = ["json", "multipart", "socks"] }
reqwest = { version = "0.11.13", features = ["json", "multipart", "socks"] }
# Used for working with RSA keys
rsa = "0.5.0"
# Used for working with ethereum keys
secp256k1 = { version = "0.21.3", features = ["rand", "rand-std"] }
# Used for serialization/deserialization
serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0.89"
@ -72,25 +68,18 @@ siwe = "0.4.0"
# Used for creating error types
thiserror = "1.0.37"
# Async runtime
tokio = { version = "1.17.0", features = ["macros"] }
# Used for working with Postgresql database
tokio-postgres = { version = "0.7.6", features = ["with-chrono-0_4", "with-uuid-1", "with-serde_json-1"] }
postgres-types = { version = "0.2.3", features = ["derive", "with-chrono-0_4", "with-uuid-1", "with-serde_json-1"] }
postgres-protocol = "0.6.4"
# Used to construct PostgreSQL queries
postgres_query = { git = "https://github.com/nolanderc/rust-postgres-query", rev = "b4422051c8a31fbba4a35f88004c1cefb1878dd5" }
postgres_query_macro = { git = "https://github.com/nolanderc/rust-postgres-query", rev = "b4422051c8a31fbba4a35f88004c1cefb1878dd5" }
tokio = { version = "=1.20.4", features = ["macros"] }
# Used to work with URLs
url = "2.2.2"
# Used to work with UUIDs
uuid = { version = "1.1.2", features = ["serde", "v4"] }
# Used to query ethereum node
web3 = { version = "0.18.0", default-features = false, features = ["http", "http-tls", "signing"] }
[dev-dependencies]
mitra-config = { path = "mitra-config", features = ["test-utils"] }
mitra-utils = { path = "mitra-utils", features = ["test-utils"] }
fedimovies-config = { path = "fedimovies-config", features = ["test-utils"] }
fedimovies-models = { path = "fedimovies-models", features = ["test-utils"] }
fedimovies-utils = { path = "fedimovies-utils", features = ["test-utils"] }
serial_test = "0.7.0"
[features]
production = ["mitra-config/production"]
production = ["fedimovies-config/production"]

View file

@ -86,6 +86,10 @@ Canonicalization algorithm: JCS
Hashing algorithm: BLAKE2b-512
Signature algorithm: EdDSA
## Custom emojis
Custom emojis are implemented as described in Mastodon documentation: https://docs.joinmastodon.org/spec/activitypub/#emoji.
## Profile extensions
### Cryptocurrency addresses

113
README.md
View file

@ -1,47 +1,38 @@
# Mitra
# FediMovies
[![status-badge](https://ci.caric.io/api/badges/FediMovies/fedimovies/status.svg)](https://ci.caric.io/FediMovies/fedimovies)
Federated micro-blogging platform and content subscription service.
Lively federated movies reviews platform.
Built on [ActivityPub](https://www.w3.org/TR/activitypub/) protocol, self-hosted, lightweight. Part of the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
Subscriptions provide a way to receive monthly payments from subscribers and to publish private content made exclusively for them.
Features:
Supported payment methods:
- Micro-blogging service (includes support for quote posts, custom emojis and more).
- Mastodon API.
- Account migrations (from one server to another). Identity can be detached from the server.
- Federation over Tor.
- [Monero](https://www.getmonero.org/get-started/what-is-monero/).
- [ERC-20](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/) tokens (on Ethereum and other EVM-compatible blockchains).
## Instances
Other features:
- [FediList](http://demo.fedilist.com/instance?software=fedimovies)
- [Fediverse Observer](https://fedimovies.fediverse.observer/list)
- [Sign-in with a wallet](https://eips.ethereum.org/EIPS/eip-4361).
- Account migrations (from one server to another).
- Donation buttons.
Follow: [@mitra@mitra.social](https://mitra.social/@mitra)
Matrix chat: [#mitra:halogen.city](https://matrix.to/#/#mitra:halogen.city)
Network stats: [FediList](http://demo.fedilist.com/instance?software=mitra) / [Fediverse Observer](https://mitra.fediverse.observer/list)
Demo instance: https://public.mitra.social/ ([invite-only](https://public.mitra.social/about))
Demo instance: https://nullpointer.social/ ([invite-only](https://nullpointer.social/about))
## Code
Server: https://codeberg.org/silverpill/mitra (this repo)
Server: https://code.caric.io/reef/reef (this repo)
Web client: https://codeberg.org/silverpill/mitra-web
Web client:
Ethereum contracts: https://codeberg.org/silverpill/mitra-contracts
## Requirements
- Rust 1.56+ (when building from source)
- Rust 1.57+ (when building from source)
- PostgreSQL 12+
Optional:
- Monero node and Monero wallet service
- Ethereum node
- IPFS node (see [guide](./docs/ipfs.md))
## Installation
@ -54,71 +45,57 @@ Run:
cargo build --release --features production
```
This command will produce two binaries in `target/release` directory, `mitra` and `mitractl`.
This command will produce two binaries in `target/release` directory, `fedimovies` and `fedimoviesctl`.
Install PostgreSQL and create the database:
```sql
CREATE USER mitra WITH PASSWORD 'mitra';
CREATE DATABASE mitra OWNER mitra;
CREATE USER fedimovies WITH PASSWORD 'fedimovies';
CREATE DATABASE fedimovies OWNER fedimovies;
```
Create configuration file by copying `contrib/mitra_config.yaml` and configure the instance. Default config file path is `/etc/mitra/config.yaml`, but it can be changed using `CONFIG_PATH` environment variable.
Create configuration file by copying `contrib/fedimovies_config.yaml` and configure the instance. Default config file path is `/etc/fedimovies/config.yaml`, but it can be changed using `CONFIG_PATH` environment variable.
Put any static files into the directory specified in configuration file. Building instructions for `mitra-web` frontend can be found at https://codeberg.org/silverpill/mitra-web#project-setup.
Put any static files into the directory specified in configuration file. Building instructions for `fedimovies-web` frontend can be found at https://code.caric.io/FediMovies/fedimovies#project-setup.
Start Mitra:
Start Fedimovies:
```shell
./mitra
./fedimovies
```
An HTTP server will be needed to handle HTTPS requests. See the example of [nginx configuration file](./contrib/mitra.nginx).
An HTTP server will be needed to handle HTTPS requests. See the example of [nginx configuration file](./contrib/fedimovies.nginx).
To run Mitra as a systemd service, check out the [systemd unit file example](./contrib/mitra.service).
To run Fedimovies as a systemd service, check out the [systemd unit file example](./contrib/fedimovies.service).
### Debian package
Download and install Mitra package:
Download and install Fedimovies package:
```shell
dpkg -i mitra.deb
dpkg -i fedimovies.deb
```
Install PostgreSQL and create the database:
```sql
CREATE USER mitra WITH PASSWORD 'mitra';
CREATE DATABASE mitra OWNER mitra;
CREATE USER fedimovies WITH PASSWORD 'fedimovies';
CREATE DATABASE fedimovies OWNER fedimovies;
```
Open configuration file `/etc/mitra/config.yaml` and configure the instance.
Open configuration file `/etc/fedimovies/config.yaml` and configure the instance.
Start Mitra:
Start Fedimovies:
```shell
systemctl start mitra
systemctl start fedimovies
```
An HTTP server will be needed to handle HTTPS requests. See the example of [nginx configuration file](./contrib/mitra.nginx).
An HTTP server will be needed to handle HTTPS requests. See the example of [nginx configuration file](./contrib/fedimovies.nginx).
### Monero
### Tor federation
Install Monero node or choose a [public one](https://monero.fail/).
Configure and start [monero-wallet-rpc](https://monerodocs.org/interacting/monero-wallet-rpc-reference/) daemon. Add `disable-rpc-login=1` to your `monero-wallet-rpc` config (currently RPC auth is not supported in Mitra).
Create a wallet for your instance.
Add blockchain configuration to `blockchains` array in your configuration file.
### Ethereum
Install Ethereum client or choose a JSON-RPC API provider.
Deploy contracts on the blockchain. Instructions can be found at https://codeberg.org/silverpill/mitra-contracts.
Add blockchain configuration to `blockchains` array in your configuration file.
See [guide](./docs/onion.md).
## Development
@ -133,15 +110,7 @@ docker-compose up -d
Test connection:
```shell
psql -h localhost -p 55432 -U mitra mitra
```
### Start Monero node and wallet server
(this step is optional)
```shell
docker-compose --profile monero up -d
psql -h localhost -p 55432 -U fedimovies fedimovies
```
### Run web service
@ -161,7 +130,7 @@ cargo run
### Run CLI
```shell
cargo run --bin mitractl
cargo run --bin fedimoviesctl
```
### Run linter
@ -182,20 +151,16 @@ See [FEDERATION.md](./FEDERATION.md)
## Client API
Most methods are similar to Mastodon API, but Mitra is not fully compatible.
Most methods are similar to Mastodon API, but Fedimovies is not fully compatible.
[OpenAPI spec](./docs/openapi.yaml)
## CLI
`mitractl` is a command-line tool for performing instance maintenance.
`fedimoviesctl` is a command-line tool for performing instance maintenance.
[Documentation](./docs/mitractl.md)
[Documentation](./docs/fedimoviesctl.md)
## License
[AGPL-3.0](./LICENSE)
## Support
Monero: 8Ahza5RM4JQgtdqvpcF1U628NN5Q87eryXQad3Fy581YWTZU8o3EMbtScuioQZSkyNNEEE1Lkj2cSbG4VnVYCW5L1N4os5p

0
build/.gitkeep Normal file
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

@ -36,7 +36,7 @@ server {
add_header Content-Security-Policy "default-src 'none'; connect-src 'self'; img-src 'self' data:; media-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'nonce-oauth-authorization'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'";
add_header X-Content-Type-Options "nosniff";
client_max_body_size 10M;
client_max_body_size 40M;
location / {
# Frontend

View file

@ -32,7 +32,7 @@ server {
add_header Strict-Transport-Security "max-age=63072000" always;
client_max_body_size 10M;
client_max_body_size 40M;
location / {
proxy_pass http://127.0.0.1:8383;

View file

@ -20,18 +20,19 @@ instance_uri: https://example.tld
instance_title: example
instance_short_description: my instance
# Long description can contain markdown syntax
instance_description: my instance
instance_description: |
# My instance
Welcome!
registration:
# Possible values: open, invite
type: invite
# Possible values: user, read_only_user
default_role: user
# EIP-4361 login message
#login_message: 'Do not sign this message on other sites!'
# Proxy for outgoing requests
#proxy_url: 'socks5h://127.0.0.1:9050'
# Limits
#limits:
# media:
@ -40,38 +41,21 @@ registration:
# character_limit: 2000
# Data retention parameters
#retention:
# extraneous_posts: 50
# empty_profiles: 150
retention:
extraneous_posts: 50
empty_profiles: 150
# Federation parameters
#federation:
# enabled: true
# # Proxy for outgoing requests
# #proxy_url: 'socks5h://127.0.0.1:9050'
# # Proxy for outgoing requests to .onion targets
# #onion_proxy_url: 'socks5h://127.0.0.1:9050'
# List of blocked domains
#blocked_instances: []
# Blockchain integrations
# Multiple configuration are currently not allowed.
# Chain metadata for EVM chains can be found at https://github.com/ethereum-lists/chains
# Signing key for ethereum integration can be generated with `mitractl generate-ethereum-address`
#blockchains:
# - chain_id: monero:mainnet
# node_url: 'http://opennode.xmr-tw.org:18089'
# wallet_url: 'http://127.0.0.1:18083'
# wallet_name: null
# wallet_password: null
# - chain_id: eip155:31337
# chain_metadata:
# chain_name: localhost
# currency_name: ETH
# currency_symbol: ETH
# currency_decimals: 18
# public_api_url: 'http://127.0.0.1:8545'
# explorer_url: null
# contract_address: '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9'
# contract_dir: /usr/share/mitra/contracts
# api_url: 'http://127.0.0.1:8545'
# signing_key: null
# chain_sync_step: 1000
# chain_reorg_max_depth: 10
# IPFS integration
#ipfs_api_url: 'http://127.0.0.1:5001'
# IPFS gateway (for clients)

View file

@ -20,10 +20,10 @@ Generate RSA private key:
mitractl generate-rsa-key
```
Generate invite code:
Generate invite code (note is optional):
```shell
mitractl generate-invite-code
mitractl generate-invite-code <note>
```
List generated invites:
@ -32,13 +32,19 @@ List generated invites:
mitractl list-invite-codes
```
Create user:
```shell
mitractl create-user <username> <password> <role-name>
```
Set or change password:
```shell
mitractl set-password <user-id> <password>
```
Change user's role:
Change user's role (admin, user or read_only_user).
```shell
mitractl set-role <user-id> <role-name>
@ -80,6 +86,12 @@ Delete empty remote profiles:
mitractl delete-empty-profiles 100
```
Delete unused remote emojis:
```shell
mitractl prune-remote-emojis
```
Import custom emoji from another instance:
```shell

42
docs/onion.md Normal file
View file

@ -0,0 +1,42 @@
# Tor federation
## Tor-only instance
Install Tor.
Install Mitra. Uncomment or add the following block to Mitra configuration file:
```yaml
federation:
proxy_url: 'socks5h://127.0.0.1:9050'
```
Where `127.0.0.1:9050` is the address and the port where Tor proxy is listening.
Configure the onion service by adding these lines to `torrc` configuration file:
```
HiddenServiceDir /var/lib/tor/mitra/
HiddenServicePort 80 127.0.0.1:8383
```
Where `8383` should correspond to `http_port` setting in Mitra configuration file.
Restart the Tor service. Inside the `HiddenServiceDir` directory find the `hostname` file. This file contains the hostname of your onion service. Change the value of `instance_uri` parameter in Mitra configuration file to that hostname (it should end with `.onion`).
Start Mitra.
For more information about running onion services, visit https://community.torproject.org/onion-services/setup/
## Clearnet + Tor
Clearnet instances can federate with Tor-only instances.
Add the following block to Mitra configuration file:
```yaml
federation:
onion_proxy_url: 'socks5h://127.0.0.1:9050'
```
Where `127.0.0.1:9050` is the address and the port where Tor proxy is listening.

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.
@ -590,6 +596,37 @@ paths:
$ref: '#/components/schemas/Relationship'
404:
description: Profile not found
/api/v1/accounts/{account_id}/aliases:
get:
summary: Get actor's verified aliases.
parameters:
- $ref: '#/components/parameters/account_id'
responses:
200:
description: Successful operation
content:
application/json:
schema:
description: Profile list
type: array
items:
$ref: '#/components/schemas/Account'
404:
description: Profile not found
/api/v1/accounts/{account_id}/aliases/all:
get:
summary: Get actor's aliases.
parameters:
- $ref: '#/components/parameters/account_id'
responses:
200:
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Aliases'
404:
description: Profile not found
/api/v1/apps:
post:
summary: Create a new application to obtain OAuth2 credentials.
@ -743,6 +780,29 @@ paths:
type: array
items:
$ref: '#/components/schemas/Notification'
/api/v1/settings/client_config:
post:
summary: Update client configuration.
security:
- tokenAuth: []
requestBody:
content:
application/json:
schema:
description: |
Client configuration.
Should contain a single key identifying type of client.
type: object
example: {"mitra-web":{"theme":"dark"}}
responses:
200:
description: Successful operation.
content:
application/json:
schema:
$ref: '#/components/schemas/CredentialAccount'
400:
description: Invalid request data.
/api/v1/settings/change_password:
post:
summary: Set or change user's password.
@ -766,6 +826,28 @@ paths:
$ref: '#/components/schemas/CredentialAccount'
400:
description: Invalid request data.
/api/v1/settings/aliases:
post:
summary: Add alias (not verified).
requestBody:
content:
application/json:
schema:
type: object
properties:
acct:
description: Actor address.
type: string
example: user@example.com
responses:
200:
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Aliases'
404:
description: Profile not found.
/api/v1/settings/export_followers:
get:
summary: Export followers to CSV file
@ -877,6 +959,10 @@ paths:
visibility:
description: Visibility of the post.
$ref: '#/components/schemas/Visibility'
sensitiive:
description: Mark post and attached media as sensitive?
type: boolean
default: false
mentions:
description: Array of profile IDs to be mentioned
type: array
@ -1416,6 +1502,11 @@ components:
type: array
items:
$ref: '#/components/schemas/Field'
emojis:
description: Custom emoji entities to be used when rendering the profile.
type: array
items:
$ref: '#/components/schemas/CustomEmoji'
followers_count:
description: The reported followers of this profile.
type: number
@ -1440,6 +1531,10 @@ components:
role:
description: The role assigned to the currently authorized user.
$ref: '#/components/schemas/Role'
client_config:
description: Client configurations.
type: object
example: {"mitra-web":{"theme":"dark"}}
ActivityParameters:
type: object
properties:
@ -1448,6 +1543,19 @@ components:
type: string
enum:
- update
Aliases:
type: object
properties:
declared:
description: Aliases declared by user.
type: array
items:
$ref: '#/components/schemas/Account'
verified:
description: Cryptographically verified aliases.
type: array
items:
$ref: '#/components/schemas/Account'
Application:
type: object
properties:
@ -1475,6 +1583,7 @@ components:
- unknown
- image
- video
- audio
url:
description: The location of the original full-size attachment.
type: string
@ -1759,6 +1868,8 @@ components:
enum:
- create_follow_request
- create_post
- delete_any_post
- delete_any_profile
- manage_subscription_options
Signature:
type: object
@ -1800,6 +1911,13 @@ components:
visibility:
description: Visibility of this post.
$ref: '#/components/schemas/Visibility'
sensitiive:
description: Is this post marked as sensitive content?
type: boolean
example: false
spoiler_text:
description: Subject or summary line, below which post content is collapsed until expanded.
type: string
media_attachments:
description: Media that is attached to this post.
type: array

View file

@ -1,18 +1,19 @@
[package]
name = "mitra-cli"
version = "1.14.0"
name = "fedimovies-cli"
version = "1.22.0"
license = "AGPL-3.0"
edition = "2021"
rust-version = "1.56"
rust-version = "1.68"
[[bin]]
name = "mitractl"
name = "fedimoviesctl"
path = "src/main.rs"
[dependencies]
mitra-config = { path = "../mitra-config" }
mitra-utils = { path = "../mitra-utils" }
mitra = { path = ".." }
fedimovies-config = { path = "../fedimovies-config" }
fedimovies-models = { path = "../fedimovies-models" }
fedimovies-utils = { path = "../fedimovies-utils" }
fedimovies = { path = ".." }
# Used for catching errors
anyhow = "1.0.58"
@ -21,6 +22,6 @@ clap = { version = "3.2.18", default-features = false, features = ["std", "deriv
# Used for logging
log = "0.4.14"
# Async runtime
tokio = { version = "1.17.0", features = ["macros"] }
tokio = { version = "1.20.4", features = ["macros"] }
# Used to work with UUIDs
uuid = "1.1.2"

View file

@ -1,58 +1,39 @@
use anyhow::{anyhow, Error};
use anyhow::Error;
use clap::Parser;
use uuid::Uuid;
use mitra::activitypub::{
actors::helpers::update_remote_profile,
builders::delete_note::prepare_delete_note,
builders::delete_person::prepare_delete_person,
fetcher::fetchers::fetch_actor,
use fedimovies::activitypub::{
actors::helpers::update_remote_profile, builders::delete_note::prepare_delete_note,
builders::delete_person::prepare_delete_person, fetcher::fetchers::fetch_actor,
fetcher::helpers::import_from_outbox,
};
use mitra::database::DatabaseClient;
use mitra::ethereum::{
signatures::generate_ecdsa_key,
sync::save_current_block_number,
utils::key_to_ethereum_address,
};
use mitra::media::remove_files;
use mitra::models::{
use fedimovies::admin::roles::{role_from_str, ALLOWED_ROLES};
use fedimovies::media::{remove_files, remove_media, MediaStorage};
use fedimovies::validators::{emojis::EMOJI_LOCAL_MAX_SIZE, users::validate_local_username};
use fedimovies_config::Config;
use fedimovies_models::{
attachments::queries::delete_unused_attachments,
cleanup::find_orphaned_files,
database::DatabaseClient,
emojis::helpers::get_emoji_by_name,
emojis::queries::{
create_emoji,
delete_emoji,
get_emoji_by_name_and_hostname,
create_emoji, delete_emoji, find_unused_remote_emojis, get_emoji_by_name_and_hostname,
},
emojis::validators::EMOJI_LOCAL_MAX_SIZE,
oauth::queries::delete_oauth_tokens,
posts::queries::{delete_post, find_extraneous_posts, get_post_by_id},
profiles::queries::{
delete_profile,
find_empty_profiles,
get_profile_by_id,
delete_profile, find_empty_profiles, find_unreachable, get_profile_by_id,
get_profile_by_remote_actor_id,
},
subscriptions::queries::reset_subscriptions,
users::queries::{
create_invite_code,
get_invite_codes,
get_user_by_id,
set_user_password,
create_invite_code, create_user, get_invite_codes, get_user_by_id, set_user_password,
set_user_role,
},
users::types::Role,
users::types::UserCreateData,
};
use mitra::monero::{
helpers::check_expired_invoice,
wallet::create_monero_wallet,
};
use mitra_config::Config;
use mitra_utils::{
crypto_rsa::{
generate_rsa_key,
serialize_private_key,
},
use fedimovies_utils::{
crypto_rsa::{generate_rsa_key, serialize_private_key},
datetime::{days_before_now, get_min_datetime},
passwords::hash_password,
};
@ -71,9 +52,11 @@ pub enum SubCommand {
GenerateInviteCode(GenerateInviteCode),
ListInviteCodes(ListInviteCodes),
CreateUser(CreateUser),
SetPassword(SetPassword),
SetRole(SetRole),
RefetchActor(RefetchActor),
ReadOutbox(ReadOutbox),
DeleteProfile(DeleteProfile),
DeletePost(DeletePost),
DeleteEmoji(DeleteEmoji),
@ -81,6 +64,8 @@ pub enum SubCommand {
DeleteUnusedAttachments(DeleteUnusedAttachments),
DeleteOrphanedFiles(DeleteOrphanedFiles),
DeleteEmptyProfiles(DeleteEmptyProfiles),
PruneRemoteEmojis(PruneRemoteEmojis),
ListUnreachableActors(ListUnreachableActors),
ImportEmoji(ImportEmoji),
UpdateCurrentBlock(UpdateCurrentBlock),
ResetSubscriptions(ResetSubscriptions),
@ -106,25 +91,19 @@ pub struct GenerateEthereumAddress;
impl GenerateEthereumAddress {
pub fn execute(&self) -> () {
let private_key = generate_ecdsa_key();
let address = key_to_ethereum_address(&private_key);
println!(
"address {:?}; private key {}",
address, private_key.display_secret(),
);
println!("dummy");
}
}
/// Generate invite code
#[derive(Parser)]
pub struct GenerateInviteCode;
pub struct GenerateInviteCode {
note: Option<String>,
}
impl GenerateInviteCode {
pub async fn execute(
&self,
db_client: &impl DatabaseClient,
) -> Result<(), Error> {
let invite_code = create_invite_code(db_client).await?;
pub async fn execute(&self, db_client: &impl DatabaseClient) -> Result<(), Error> {
let invite_code = create_invite_code(db_client, self.note.as_deref()).await?;
println!("generated invite code: {}", invite_code);
Ok(())
}
@ -135,18 +114,49 @@ impl GenerateInviteCode {
pub struct ListInviteCodes;
impl ListInviteCodes {
pub async fn execute(
&self,
db_client: &impl DatabaseClient,
) -> Result<(), Error> {
pub async fn execute(&self, db_client: &impl DatabaseClient) -> Result<(), Error> {
let invite_codes = get_invite_codes(db_client).await?;
if invite_codes.is_empty() {
println!("no invite codes found");
return Ok(());
};
for code in invite_codes {
println!("{}", code);
for invite_code in invite_codes {
if let Some(note) = invite_code.note {
println!("{} ({})", invite_code.code, note);
} else {
println!("{}", invite_code.code);
};
}
Ok(())
}
}
/// Create new user
#[derive(Parser)]
pub struct CreateUser {
username: String,
password: String,
#[clap(value_parser = ALLOWED_ROLES)]
role: String,
}
impl CreateUser {
pub async fn execute(&self, db_client: &mut impl DatabaseClient) -> Result<(), Error> {
validate_local_username(&self.username)?;
let password_hash = hash_password(&self.password)?;
let private_key = generate_rsa_key()?;
let private_key_pem = serialize_private_key(&private_key)?;
let role = role_from_str(&self.role)?;
let user_data = UserCreateData {
username: self.username.clone(),
password_hash: Some(password_hash),
private_key_pem,
wallet_address: None,
invite_code: None,
role,
};
create_user(db_client, user_data).await?;
println!("user created");
Ok(())
}
}
@ -159,10 +169,7 @@ pub struct SetPassword {
}
impl SetPassword {
pub async fn execute(
&self,
db_client: &impl DatabaseClient,
) -> Result<(), Error> {
pub async fn execute(&self, db_client: &impl DatabaseClient) -> Result<(), Error> {
let password_hash = hash_password(&self.password)?;
set_user_password(db_client, &self.id, password_hash).await?;
// Revoke all sessions
@ -176,15 +183,13 @@ impl SetPassword {
#[derive(Parser)]
pub struct SetRole {
id: Uuid,
#[clap(value_parser = ALLOWED_ROLES)]
role: String,
}
impl SetRole {
pub async fn execute(
&self,
db_client: &impl DatabaseClient,
) -> Result<(), Error> {
let role = Role::from_name(&self.role)?;
pub async fn execute(&self, db_client: &impl DatabaseClient) -> Result<(), Error> {
let role = role_from_str(&self.role)?;
set_user_role(db_client, &self.id, role).await?;
println!("role changed");
Ok(())
@ -201,25 +206,42 @@ impl RefetchActor {
pub async fn execute(
&self,
config: &Config,
db_client: &impl DatabaseClient,
db_client: &mut impl DatabaseClient,
) -> Result<(), Error> {
let profile = get_profile_by_remote_actor_id(
db_client,
&self.id,
).await?;
let profile = get_profile_by_remote_actor_id(db_client, &self.id).await?;
let actor = fetch_actor(&config.instance(), &self.id).await?;
update_remote_profile(
db_client,
&config.instance(),
&config.media_dir(),
&MediaStorage::from(config),
profile,
actor,
).await?;
)
.await?;
println!("profile updated");
Ok(())
}
}
/// Pull activities from actor's outbox
#[derive(Parser)]
pub struct ReadOutbox {
actor_id: String,
#[clap(long, default_value_t = 5)]
limit: usize,
}
impl ReadOutbox {
pub async fn execute(
&self,
config: &Config,
db_client: &mut impl DatabaseClient,
) -> Result<(), Error> {
import_from_outbox(config, db_client, &self.actor_id, self.limit).await?;
Ok(())
}
}
/// Delete profile
#[derive(Parser)]
pub struct DeleteProfile {
@ -236,15 +258,14 @@ impl DeleteProfile {
let mut maybe_delete_person = None;
if profile.is_local() {
let user = get_user_by_id(db_client, &profile.id).await?;
let activity =
prepare_delete_person(db_client, &config.instance(), &user).await?;
let activity = prepare_delete_person(db_client, &config.instance(), &user).await?;
maybe_delete_person = Some(activity);
};
let deletion_queue = delete_profile(db_client, &profile.id).await?;
deletion_queue.process(config).await;
remove_media(config, deletion_queue).await;
// Send Delete(Person) activities
if let Some(activity) = maybe_delete_person {
activity.deliver().await?;
activity.enqueue(db_client).await?;
};
println!("profile deleted");
Ok(())
@ -267,19 +288,15 @@ impl DeletePost {
let mut maybe_delete_note = None;
if post.author.is_local() {
let author = get_user_by_id(db_client, &post.author.id).await?;
let activity = prepare_delete_note(
db_client,
&config.instance(),
&author,
&post,
).await?;
let activity =
prepare_delete_note(db_client, &config.instance(), &author, &post).await?;
maybe_delete_note = Some(activity);
};
let deletion_queue = delete_post(db_client, &post.id).await?;
deletion_queue.process(config).await;
remove_media(config, deletion_queue).await;
// Send Delete(Note) activity
if let Some(activity) = maybe_delete_note {
activity.deliver().await?;
activity.enqueue(db_client).await?;
};
println!("post deleted");
Ok(())
@ -299,13 +316,10 @@ impl DeleteEmoji {
config: &Config,
db_client: &impl DatabaseClient,
) -> Result<(), Error> {
let emoji = get_emoji_by_name(
db_client,
&self.emoji_name,
self.hostname.as_deref(),
).await?;
let emoji =
get_emoji_by_name(db_client, &self.emoji_name, self.hostname.as_deref()).await?;
let deletion_queue = delete_emoji(db_client, &emoji.id).await?;
deletion_queue.process(config).await;
remove_media(config, deletion_queue).await;
println!("emoji deleted");
Ok(())
}
@ -327,9 +341,9 @@ impl DeleteExtraneousPosts {
let posts = find_extraneous_posts(db_client, &updated_before).await?;
for post_id in posts {
let deletion_queue = delete_post(db_client, &post_id).await?;
deletion_queue.process(config).await;
remove_media(config, deletion_queue).await;
println!("post {} deleted", post_id);
};
}
Ok(())
}
}
@ -347,11 +361,8 @@ impl DeleteUnusedAttachments {
db_client: &impl DatabaseClient,
) -> Result<(), Error> {
let created_before = days_before_now(self.days);
let deletion_queue = delete_unused_attachments(
db_client,
&created_before,
).await?;
deletion_queue.process(config).await;
let deletion_queue = delete_unused_attachments(db_client, &created_before).await?;
remove_media(config, deletion_queue).await;
println!("unused attachments deleted");
Ok(())
}
@ -370,10 +381,9 @@ impl DeleteOrphanedFiles {
let media_dir = config.media_dir();
let mut files = vec![];
for maybe_path in std::fs::read_dir(&media_dir)? {
let file_name = maybe_path?.file_name()
.to_string_lossy().to_string();
let file_name = maybe_path?.file_name().to_string_lossy().to_string();
files.push(file_name);
};
}
println!("found {} files", files.len());
let orphaned = find_orphaned_files(db_client, files).await?;
if !orphaned.is_empty() {
@ -401,9 +411,59 @@ impl DeleteEmptyProfiles {
for profile_id in profiles {
let profile = get_profile_by_id(db_client, &profile_id).await?;
let deletion_queue = delete_profile(db_client, &profile.id).await?;
deletion_queue.process(config).await;
remove_media(config, deletion_queue).await;
println!("profile {} deleted", profile.acct);
};
}
Ok(())
}
}
/// Delete unused remote emojis
#[derive(Parser)]
pub struct PruneRemoteEmojis;
impl PruneRemoteEmojis {
pub async fn execute(
&self,
config: &Config,
db_client: &mut impl DatabaseClient,
) -> Result<(), Error> {
let emojis = find_unused_remote_emojis(db_client).await?;
for emoji_id in emojis {
let deletion_queue = delete_emoji(db_client, &emoji_id).await?;
remove_media(config, deletion_queue).await;
println!("emoji {} deleted", emoji_id);
}
Ok(())
}
}
/// List unreachable actors
#[derive(Parser)]
pub struct ListUnreachableActors {
days: u32,
}
impl ListUnreachableActors {
pub async fn execute(
&self,
_config: &Config,
db_client: &impl DatabaseClient,
) -> Result<(), Error> {
let unreachable_since = days_before_now(self.days);
let profiles = find_unreachable(db_client, &unreachable_since).await?;
println!(
"{0: <60} | {1: <35} | {2: <35}",
"ID", "unreachable since", "updated at",
);
for profile in profiles {
println!(
"{0: <60} | {1: <35} | {2: <35}",
profile.actor_id.unwrap(),
profile.unreachable_since.unwrap().to_string(),
profile.updated_at.to_string(),
);
}
Ok(())
}
}
@ -421,11 +481,8 @@ impl ImportEmoji {
_config: &Config,
db_client: &impl DatabaseClient,
) -> Result<(), Error> {
let emoji = get_emoji_by_name_and_hostname(
db_client,
&self.emoji_name,
&self.hostname,
).await?;
let emoji =
get_emoji_by_name_and_hostname(db_client, &self.emoji_name, &self.hostname).await?;
if emoji.image.file_size > EMOJI_LOCAL_MAX_SIZE {
println!("emoji is too big");
return Ok(());
@ -437,7 +494,8 @@ impl ImportEmoji {
emoji.image,
None,
&get_min_datetime(),
).await?;
)
.await?;
println!("added emoji to local collection");
Ok(())
}
@ -452,10 +510,9 @@ pub struct UpdateCurrentBlock {
impl UpdateCurrentBlock {
pub async fn execute(
&self,
config: &Config,
_config: &Config,
_db_client: &impl DatabaseClient,
) -> Result<(), Error> {
save_current_block_number(&config.storage_dir, self.number)?;
println!("current block updated");
Ok(())
}
@ -490,18 +547,7 @@ pub struct CreateMoneroWallet {
}
impl CreateMoneroWallet {
pub async fn execute(
&self,
config: &Config,
) -> Result<(), Error> {
let monero_config = config.blockchain()
.and_then(|conf| conf.monero_config())
.ok_or(anyhow!("monero configuration not found"))?;
create_monero_wallet(
monero_config,
self.name.clone(),
self.password.clone(),
).await?;
pub async fn execute(&self, _config: &Config) -> Result<(), Error> {
println!("wallet created");
Ok(())
}
@ -516,17 +562,9 @@ pub struct CheckExpiredInvoice {
impl CheckExpiredInvoice {
pub async fn execute(
&self,
config: &Config,
db_client: &impl DatabaseClient,
_config: &Config,
_db_client: &impl DatabaseClient,
) -> Result<(), Error> {
let monero_config = config.blockchain()
.and_then(|conf| conf.monero_config())
.ok_or(anyhow!("monero configuration not found"))?;
check_expired_invoice(
monero_config,
db_client,
&self.id,
).await?;
Ok(())
}
}

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.14.0"
name = "fedimovies-config"
version = "1.22.0"
license = "AGPL-3.0"
edition = "2021"
rust-version = "1.56"
rust-version = "1.68"
[dependencies]
mitra-utils = { path = "../mitra-utils" }
fedimovies-utils = { path = "../fedimovies-utils" }
# Used to read .env files
dotenv = "0.15.0"

View file

@ -1,59 +1,27 @@
use std::path::PathBuf;
use log::{Level as LogLevel};
use log::Level as LogLevel;
use rsa::RsaPrivateKey;
use serde::{
Deserialize,
Deserializer,
de::Error as DeserializerError,
};
use serde::Deserialize;
use url::Url;
use mitra_utils::urls::normalize_url;
use fedimovies_utils::urls::normalize_url;
use super::blockchain::BlockchainConfig;
use super::environment::Environment;
use super::federation::FederationConfig;
use super::limits::Limits;
use super::registration::RegistrationConfig;
use super::retention::RetentionConfig;
use super::MITRA_VERSION;
use super::REEF_VERSION;
#[derive(Clone, PartialEq)]
pub enum RegistrationType {
Open,
Invite,
fn default_log_level() -> LogLevel {
LogLevel::Info
}
impl Default for RegistrationType {
fn default() -> Self { Self::Invite }
fn default_login_message() -> String {
"What?!".to_string()
}
impl<'de> Deserialize<'de> for RegistrationType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: Deserializer<'de>
{
let registration_type_str = String::deserialize(deserializer)?;
let registration_type = match registration_type_str.as_str() {
"open" => Self::Open,
"invite" => Self::Invite,
_ => return Err(DeserializerError::custom("unknown registration type")),
};
Ok(registration_type)
}
}
#[derive(Clone, Default, Deserialize)]
pub struct RegistrationConfig {
#[serde(rename = "type")]
pub registration_type: RegistrationType,
#[serde(default)]
pub default_role_read_only_user: bool, // default is false
}
fn default_log_level() -> LogLevel { LogLevel::Info }
fn default_login_message() -> String { "Do not sign this message on other sites!".to_string() }
#[derive(Clone, Deserialize)]
pub struct Config {
// Properties auto-populated from the environment
@ -65,6 +33,8 @@ pub struct Config {
// Core settings
pub database_url: String,
#[serde(default)]
pub tls_ca_file: Option<PathBuf>,
pub storage_dir: PathBuf,
pub web_client_dir: Option<PathBuf>,
@ -85,6 +55,11 @@ pub struct Config {
pub instance_short_description: String,
pub instance_description: String,
#[serde(default)]
pub tmdb_api_key: Option<String>,
#[serde(default)]
pub movie_user_password: Option<String>,
#[serde(skip)]
pub(super) instance_rsa_key: Option<RsaPrivateKey>,
@ -99,23 +74,20 @@ pub struct Config {
pub(super) post_character_limit: Option<usize>, // deprecated
proxy_url: Option<String>,
#[serde(default)]
pub limits: Limits,
#[serde(default)]
pub retention: RetentionConfig,
pub(super) proxy_url: Option<String>,
#[serde(default)]
pub federation: FederationConfig,
#[serde(default)]
pub blocked_instances: Vec<String>,
// Blockchain integrations
#[serde(rename = "blockchain")]
_blockchain: Option<BlockchainConfig>, // deprecated
#[serde(default)]
blockchains: Vec<BlockchainConfig>,
// IPFS
pub ipfs_api_url: Option<String>,
pub ipfs_gateway_url: Option<String>,
@ -130,8 +102,14 @@ impl Config {
Instance {
_url: self.try_instance_url().unwrap(),
actor_key: self.instance_rsa_key.clone().unwrap(),
proxy_url: self.proxy_url.clone(),
is_private: matches!(self.environment, Environment::Development),
proxy_url: self.federation.proxy_url.clone(),
onion_proxy_url: self.federation.onion_proxy_url.clone(),
i2p_proxy_url: self.federation.i2p_proxy_url.clone(),
// Private instance doesn't send activities and sign requests
is_private: !self.federation.enabled,
// || matches!(self.environment, Environment::Development),
fetcher_timeout: self.federation.fetcher_timeout,
deliverer_timeout: self.federation.deliverer_timeout,
}
}
@ -142,18 +120,6 @@ impl Config {
pub fn media_dir(&self) -> PathBuf {
self.storage_dir.join("media")
}
pub fn blockchain(&self) -> Option<&BlockchainConfig> {
if let Some(ref _blockchain_config) = self._blockchain {
panic!("'blockchain' setting is not supported anymore, use 'blockchains' instead");
} else {
match &self.blockchains[..] {
[blockchain_config] => Some(blockchain_config),
[] => None,
_ => panic!("multichain deployments are not supported"),
}
}
}
}
#[derive(Clone)]
@ -163,8 +129,12 @@ pub struct Instance {
pub actor_key: RsaPrivateKey,
// Proxy for outgoing requests
pub proxy_url: Option<String>,
pub onion_proxy_url: Option<String>,
pub i2p_proxy_url: Option<String>,
// Private instance won't send signed HTTP requests
pub is_private: bool,
pub fetcher_timeout: u64,
pub deliverer_timeout: u64,
}
impl Instance {
@ -178,9 +148,9 @@ impl Instance {
pub fn agent(&self) -> String {
format!(
"Mitra {version}; {instance_url}",
version=MITRA_VERSION,
instance_url=self.url(),
"Reef {version}; {instance_url}",
version = REEF_VERSION,
instance_url = self.url(),
)
}
}
@ -188,20 +158,24 @@ impl Instance {
#[cfg(feature = "test-utils")]
impl Instance {
pub fn for_test(url: &str) -> Self {
use mitra_utils::crypto_rsa::generate_weak_rsa_key;
use fedimovies_utils::crypto_rsa::generate_weak_rsa_key;
Self {
_url: Url::parse(url).unwrap(),
actor_key: generate_weak_rsa_key().unwrap(),
proxy_url: None,
onion_proxy_url: None,
i2p_proxy_url: None,
is_private: true,
fetcher_timeout: 0,
deliverer_timeout: 0,
}
}
}
#[cfg(test)]
mod tests {
use mitra_utils::crypto_rsa::generate_weak_rsa_key;
use super::*;
use fedimovies_utils::crypto_rsa::generate_weak_rsa_key;
#[test]
fn test_instance_url_https_dns() {
@ -211,14 +185,18 @@ mod tests {
_url: instance_url,
actor_key: instance_rsa_key,
proxy_url: None,
onion_proxy_url: None,
i2p_proxy_url: None,
is_private: true,
fetcher_timeout: 0,
deliverer_timeout: 0,
};
assert_eq!(instance.url(), "https://example.com");
assert_eq!(instance.hostname(), "example.com");
assert_eq!(
instance.agent(),
format!("Mitra {}; https://example.com", MITRA_VERSION),
format!("Mitra {}; https://example.com", REEF_VERSION),
);
}
@ -230,7 +208,11 @@ mod tests {
_url: instance_url,
actor_key: instance_rsa_key,
proxy_url: None,
onion_proxy_url: None,
i2p_proxy_url: None,
is_private: true,
fetcher_timeout: 0,
deliverer_timeout: 0,
};
assert_eq!(instance.url(), "http://1.2.3.4:3777");

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,20 +1,17 @@
mod blockchain;
mod config;
mod environment;
mod federation;
mod limits;
mod loader;
mod registration;
mod retention;
pub use blockchain::{
BlockchainConfig,
EthereumConfig,
MoneroConfig,
};
pub use config::{Config, Instance, RegistrationType};
pub use config::{Config, Instance};
pub use environment::Environment;
pub use loader::parse_config;
pub use registration::{DefaultRole, RegistrationType};
pub const MITRA_VERSION: &str = env!("CARGO_PKG_VERSION");
pub const REEF_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(thiserror::Error, Debug)]
#[error("{0}")]

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,17 +4,14 @@ use std::str::FromStr;
use rsa::RsaPrivateKey;
use mitra_utils::{
crypto_rsa::{
deserialize_private_key,
generate_rsa_key,
serialize_private_key,
},
use fedimovies_utils::{
crypto_rsa::{deserialize_private_key, generate_rsa_key, serialize_private_key},
files::{set_file_permissions, write_file},
};
use super::config::{Config, RegistrationType};
use super::config::Config;
use super::environment::Environment;
use super::registration::{DefaultRole, RegistrationType};
struct EnvConfig {
config_path: String,
@ -22,16 +19,16 @@ struct EnvConfig {
}
#[cfg(feature = "production")]
const DEFAULT_CONFIG_PATH: &str = "/etc/mitra/config.yaml";
const DEFAULT_CONFIG_PATH: &str = "/etc/fedimovies/config.yaml";
#[cfg(not(feature = "production"))]
const DEFAULT_CONFIG_PATH: &str = "config.yaml";
fn parse_env() -> EnvConfig {
dotenv::from_filename(".env.local").ok();
dotenv::dotenv().ok();
let config_path = std::env::var("CONFIG_PATH")
.unwrap_or(DEFAULT_CONFIG_PATH.to_string());
let environment = std::env::var("ENVIRONMENT").ok()
let config_path = std::env::var("CONFIG_PATH").unwrap_or(DEFAULT_CONFIG_PATH.to_string());
let environment = std::env::var("ENVIRONMENT")
.ok()
.map(|val| Environment::from_str(&val).expect("invalid environment type"));
EnvConfig {
config_path,
@ -44,8 +41,7 @@ extern "C" {
}
fn check_directory_owner(path: &Path) -> () {
let metadata = std::fs::metadata(path)
.expect("can't read file metadata");
let metadata = std::fs::metadata(path).expect("can't read file metadata");
let owner_uid = metadata.uid();
let current_uid = unsafe { geteuid() };
if owner_uid != current_uid {
@ -62,16 +58,15 @@ fn check_directory_owner(path: &Path) -> () {
fn read_instance_rsa_key(storage_dir: &Path) -> RsaPrivateKey {
let private_key_path = storage_dir.join("instance_rsa_key");
if private_key_path.exists() {
let private_key_str = std::fs::read_to_string(&private_key_path)
.expect("failed to read instance RSA key");
let private_key = deserialize_private_key(&private_key_str)
.expect("failed to read instance RSA key");
let private_key_str =
std::fs::read_to_string(&private_key_path).expect("failed to read instance RSA key");
let private_key =
deserialize_private_key(&private_key_str).expect("failed to read instance RSA key");
private_key
} else {
let private_key = generate_rsa_key()
.expect("failed to generate RSA key");
let private_key_str = serialize_private_key(&private_key)
.expect("failed to serialize RSA key");
let private_key = generate_rsa_key().expect("failed to generate RSA key");
let private_key_str =
serialize_private_key(&private_key).expect("failed to serialize RSA key");
write_file(private_key_str.as_bytes(), &private_key_path)
.expect("failed to write instance RSA key");
set_file_permissions(&private_key_path, 0o600)
@ -82,10 +77,9 @@ fn read_instance_rsa_key(storage_dir: &Path) -> RsaPrivateKey {
pub fn parse_config() -> (Config, Vec<&'static str>) {
let env = parse_env();
let config_yaml = std::fs::read_to_string(&env.config_path)
.expect("failed to load config file");
let mut config = serde_yaml::from_str::<Config>(&config_yaml)
.expect("invalid yaml data");
let config_yaml =
std::fs::read_to_string(&env.config_path).expect("failed to load config file");
let mut config = serde_yaml::from_str::<Config>(&config_yaml).expect("invalid yaml data");
let mut warnings = vec![];
// Set parameters from environment
@ -101,32 +95,37 @@ pub fn parse_config() -> (Config, Vec<&'static str>) {
};
check_directory_owner(&config.storage_dir);
config.try_instance_url().expect("invalid instance URI");
if let Some(blockchain_config) = config.blockchain() {
if let Some(ethereum_config) = blockchain_config.ethereum_config() {
ethereum_config.try_ethereum_chain_id().unwrap();
if !ethereum_config.contract_dir.exists() {
panic!("contract directory does not exist");
};
};
};
if config.ipfs_api_url.is_some() != config.ipfs_gateway_url.is_some() {
panic!("both ipfs_api_url and ipfs_gateway_url must be set");
};
// Migrations
if let Some(registrations_open) = config.registrations_open {
// Change type if 'registrations_open' parameter is used
warnings.push("'registrations_open' setting is deprecated, use 'registration.type' instead");
warnings
.push("'registrations_open' setting is deprecated, use 'registration.type' instead");
if registrations_open {
config.registration.registration_type = RegistrationType::Open;
} else {
config.registration.registration_type = RegistrationType::Invite;
};
};
if let Some(read_only_user) = config.registration.default_role_read_only_user {
warnings.push("'default_role_read_only_user' setting is deprecated, use 'registration.default_role' instead");
if read_only_user {
config.registration.default_role = DefaultRole::ReadOnlyUser;
} else {
config.registration.default_role = DefaultRole::NormalUser;
};
};
if let Some(post_character_limit) = config.post_character_limit {
warnings.push("'post_character_limit' setting is deprecated, use 'limits.posts.character_limit' instead");
config.limits.posts.character_limit = post_character_limit;
};
if let Some(ref proxy_url) = config.proxy_url {
warnings.push("'proxy_url' setting is deprecated, use 'federation.proxy_url' instead");
config.federation.proxy_url = Some(proxy_url.to_string());
};
// Insert instance RSA key
config.instance_rsa_key = Some(read_instance_rsa_key(&config.storage_dir));

View file

@ -0,0 +1,66 @@
use serde::{de::Error as DeserializerError, Deserialize, Deserializer};
#[derive(Clone, PartialEq)]
pub enum RegistrationType {
Open,
Invite,
}
impl Default for RegistrationType {
fn default() -> Self {
Self::Invite
}
}
impl<'de> Deserialize<'de> for RegistrationType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let registration_type_str = String::deserialize(deserializer)?;
let registration_type = match registration_type_str.as_str() {
"open" => Self::Open,
"invite" => Self::Invite,
_ => return Err(DeserializerError::custom("unknown registration type")),
};
Ok(registration_type)
}
}
#[derive(Clone)]
pub enum DefaultRole {
NormalUser,
ReadOnlyUser,
}
impl Default for DefaultRole {
fn default() -> Self {
Self::NormalUser
}
}
impl<'de> Deserialize<'de> for DefaultRole {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let role_str = String::deserialize(deserializer)?;
let role = match role_str.as_str() {
"user" => Self::NormalUser,
"read_only_user" => Self::ReadOnlyUser,
_ => return Err(DeserializerError::custom("unknown role name")),
};
Ok(role)
}
}
#[derive(Clone, Default, Deserialize)]
pub struct RegistrationConfig {
#[serde(rename = "type")]
pub registration_type: RegistrationType,
pub(super) default_role_read_only_user: Option<bool>, // deprecated
#[serde(default)]
pub default_role: DefaultRole,
}

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 user_invite_code ADD COLUMN note VARCHAR(200);
ALTER TABLE user_invite_code ADD COLUMN created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP;

View file

@ -0,0 +1,5 @@
CREATE TABLE profile_emoji (
profile_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
emoji_id UUID NOT NULL REFERENCES emoji (id) ON DELETE CASCADE,
PRIMARY KEY (profile_id, emoji_id)
);

View file

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

View file

@ -0,0 +1,4 @@
CREATE TABLE internal_property (
property_name VARCHAR(100) PRIMARY KEY,
property_value JSONB NOT NULL
);

View file

@ -0,0 +1,18 @@
UPDATE actor_profile
SET identity_proofs = replaced.identity_proofs
FROM (
SELECT
actor_profile.id,
jsonb_agg(
CASE
WHEN identity_proof ->> 'proof_type' = 'ethereum-eip191-00'
THEN jsonb_set(identity_proof, '{proof_type}', '1')
WHEN identity_proof ->> 'proof_type' = 'MitraMinisignSignature2022A'
THEN jsonb_set(identity_proof, '{proof_type}', '2')
END
) AS identity_proofs
FROM actor_profile
CROSS JOIN jsonb_array_elements(actor_profile.identity_proofs) AS identity_proof
GROUP BY actor_profile.id
) AS replaced
WHERE actor_profile.id = replaced.id;

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

@ -1,3 +1,8 @@
CREATE TABLE internal_property (
property_name VARCHAR(100) PRIMARY KEY,
property_value JSONB NOT NULL
);
CREATE TABLE background_job (
id UUID PRIMARY KEY,
job_type SMALLINT NOT NULL,
@ -21,15 +26,18 @@ CREATE TABLE actor_profile (
bio_source TEXT,
avatar JSONB,
banner JSONB,
manually_approves_followers BOOLEAN NOT NULL,
identity_proofs JSONB NOT NULL DEFAULT '[]',
payment_options JSONB NOT NULL DEFAULT '[]',
extra_fields JSONB NOT NULL DEFAULT '[]',
aliases JSONB NOT NULL DEFAULT '[]',
follower_count INTEGER NOT NULL CHECK (follower_count >= 0) DEFAULT 0,
following_count INTEGER NOT NULL CHECK (following_count >= 0) DEFAULT 0,
subscriber_count INTEGER NOT NULL CHECK (subscriber_count >= 0) DEFAULT 0,
post_count INTEGER NOT NULL CHECK (post_count >= 0) DEFAULT 0,
emojis JSONB NOT NULL DEFAULT '[]',
actor_json JSONB,
actor_id VARCHAR(200) UNIQUE GENERATED ALWAYS AS (actor_json ->> 'id') STORED,
actor_id VARCHAR(2000) UNIQUE GENERATED ALWAYS AS (actor_json ->> 'id') STORED,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
unreachable_since TIMESTAMP WITH TIME ZONE,
@ -38,7 +46,9 @@ CREATE TABLE actor_profile (
CREATE TABLE user_invite_code (
code VARCHAR(100) PRIMARY KEY,
used BOOLEAN NOT NULL DEFAULT FALSE
used BOOLEAN NOT NULL DEFAULT FALSE,
note VARCHAR(200),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE user_account (
@ -48,6 +58,7 @@ CREATE TABLE user_account (
private_key TEXT NOT NULL,
invite_code VARCHAR(100) UNIQUE REFERENCES user_invite_code (code) ON DELETE SET NULL,
user_role SMALLINT NOT NULL,
client_config JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
@ -56,7 +67,7 @@ CREATE TABLE oauth_application (
app_name VARCHAR(100) NOT NULL,
website VARCHAR(100),
scopes VARCHAR(200) NOT NULL,
redirect_uri VARCHAR(200) NOT NULL,
redirect_uri VARCHAR(2000) NOT NULL,
client_id UUID UNIQUE NOT NULL,
client_secret VARCHAR(100) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
@ -85,16 +96,18 @@ CREATE TABLE relationship (
source_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
target_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
relationship_type SMALLINT NOT NULL,
UNIQUE (source_id, target_id, relationship_type)
UNIQUE (source_id, target_id, relationship_type),
CHECK (source_id != target_id)
);
CREATE TABLE follow_request (
id UUID PRIMARY KEY,
source_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
target_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
activity_id VARCHAR(250) UNIQUE,
activity_id VARCHAR(2000) UNIQUE,
request_status SMALLINT NOT NULL,
UNIQUE (source_id, target_id)
UNIQUE (source_id, target_id),
CHECK (source_id != target_id)
);
CREATE TABLE post (
@ -103,11 +116,12 @@ CREATE TABLE post (
content TEXT NOT NULL,
in_reply_to_id UUID REFERENCES post (id) ON DELETE CASCADE,
repost_of_id UUID REFERENCES post (id) ON DELETE CASCADE,
visilibity SMALLINT NOT NULL,
visibility SMALLINT NOT NULL,
is_sensitive BOOLEAN NOT NULL,
reply_count INTEGER NOT NULL CHECK (reply_count >= 0) DEFAULT 0,
reaction_count INTEGER NOT NULL CHECK (reaction_count >= 0) DEFAULT 0,
repost_count INTEGER NOT NULL CHECK (repost_count >= 0) DEFAULT 0,
object_id VARCHAR(200) UNIQUE,
object_id VARCHAR(2000) UNIQUE,
ipfs_cid VARCHAR(200),
token_id INTEGER,
token_tx_id VARCHAR(200),
@ -120,7 +134,7 @@ CREATE TABLE post_reaction (
id UUID PRIMARY KEY,
author_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
post_id UUID NOT NULL REFERENCES post (id) ON DELETE CASCADE,
activity_id VARCHAR(250) UNIQUE,
activity_id VARCHAR(2000) UNIQUE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
UNIQUE (author_id, post_id)
);
@ -156,7 +170,8 @@ CREATE TABLE post_tag (
CREATE TABLE post_link (
source_id UUID NOT NULL REFERENCES post (id) ON DELETE CASCADE,
target_id UUID NOT NULL REFERENCES post (id) ON DELETE CASCADE,
PRIMARY KEY (source_id, target_id)
PRIMARY KEY (source_id, target_id),
CHECK (source_id != target_id)
);
CREATE TABLE emoji (
@ -164,7 +179,7 @@ CREATE TABLE emoji (
emoji_name VARCHAR(100) NOT NULL,
hostname VARCHAR(100) REFERENCES instance (hostname) ON DELETE RESTRICT,
image JSONB NOT NULL,
object_id VARCHAR(250) UNIQUE,
object_id VARCHAR(2000) UNIQUE,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
UNIQUE (emoji_name, hostname),
CHECK ((hostname IS NULL) = (object_id IS NULL))
@ -176,6 +191,12 @@ CREATE TABLE post_emoji (
PRIMARY KEY (post_id, emoji_id)
);
CREATE TABLE profile_emoji (
profile_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
emoji_id UUID NOT NULL REFERENCES emoji (id) ON DELETE CASCADE,
PRIMARY KEY (profile_id, emoji_id)
);
CREATE TABLE notification (
id SERIAL PRIMARY KEY,
sender_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE,
@ -203,7 +224,8 @@ CREATE TABLE invoice (
amount BIGINT NOT NULL CHECK (amount >= 0),
invoice_status SMALLINT NOT NULL DEFAULT 1,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (chain_id, payment_address)
UNIQUE (chain_id, payment_address),
CHECK (sender_id != recipient_id)
);
CREATE TABLE subscription (
@ -214,5 +236,6 @@ CREATE TABLE subscription (
chain_id VARCHAR(50) NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
UNIQUE (sender_id, recipient_id)
UNIQUE (sender_id, recipient_id),
CHECK (sender_id != recipient_id)
);

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,18 +97,15 @@ pub async fn delete_unused_attachments(
#[cfg(test)]
mod tests {
use serial_test::serial;
use crate::database::test_utils::create_test_database;
use crate::models::{
profiles::types::ProfileCreateData,
profiles::queries::create_profile,
};
use super::*;
use crate::database::test_utils::create_test_database;
use crate::profiles::{queries::create_profile, types::ProfileCreateData};
use serial_test::serial;
#[tokio::test]
#[serial]
async fn test_create_attachment() {
let db_client = &create_test_database().await;
let db_client = &mut create_test_database().await;
let profile_data = ProfileCreateData {
username: "test".to_string(),
..Default::default()
@ -121,11 +120,13 @@ mod tests {
file_name.to_string(),
file_size,
Some(media_type.to_string()),
).await.unwrap();
)
.await
.unwrap();
assert_eq!(attachment.owner_id, profile.id);
assert_eq!(attachment.file_name, file_name);
assert_eq!(attachment.file_size.unwrap(), file_size as i32);
assert_eq!(attachment.media_type.unwrap(), media_type);
assert_eq!(attachment.post_id.is_none(), true);
assert!(attachment.post_id.is_none());
}
}

View file

@ -19,6 +19,7 @@ pub enum AttachmentType {
Unknown,
Image,
Video,
Audio,
}
impl AttachmentType {
@ -29,10 +30,12 @@ impl AttachmentType {
Self::Image
} else if media_type.starts_with("video/") {
Self::Video
} else if media_type.starts_with("audio/") {
Self::Audio
} else {
Self::Unknown
}
},
}
None => Self::Unknown,
}
}

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,18 +23,23 @@ pub async fn enqueue_job(
)
VALUES ($1, $2, $3, $4)
",
&[&job_id, &job_type, &job_data, &scheduled_for],
).await?;
&[&job_id, &job_type, &job_data, &scheduled_for],
)
.await?;
Ok(())
}
pub async fn get_job_batch(
db_client: &impl DatabaseClient,
job_type: &JobType,
batch_size: i64,
batch_size: u32,
job_timeout: u32,
) -> Result<Vec<DbBackgroundJob>, DatabaseError> {
let rows = db_client.query(
"
// https://github.com/sfackler/rust-postgres/issues/60
let job_timeout_pg = format!("{}S", job_timeout); // interval
let rows = db_client
.query(
"
UPDATE background_job
SET
job_status = $1,
@ -43,21 +49,33 @@ pub async fn get_job_batch(
FROM background_job
WHERE
job_type = $2
AND job_status = $3
AND scheduled_for < CURRENT_TIMESTAMP
ORDER BY scheduled_for ASC
AND (
-- queued
job_status = $3
-- running
OR job_status = $1
AND updated_at < CURRENT_TIMESTAMP - $5::text::interval
)
ORDER BY
-- queued jobs first
job_status ASC,
scheduled_for ASC
LIMIT $4
)
RETURNING background_job
",
&[
&JobStatus::Running,
&job_type,
&JobStatus::Queued,
&batch_size,
],
).await?;
let jobs = rows.iter()
&[
&JobStatus::Running,
&job_type,
&JobStatus::Queued,
&i64::from(batch_size),
&job_timeout_pg,
],
)
.await?;
let jobs = rows
.iter()
.map(|row| row.try_get("background_job"))
.collect::<Result<_, _>>()?;
Ok(jobs)
@ -67,13 +85,15 @@ pub async fn delete_job_from_queue(
db_client: &impl DatabaseClient,
job_id: &Uuid,
) -> Result<(), DatabaseError> {
let deleted_count = db_client.execute(
"
let deleted_count = db_client
.execute(
"
DELETE FROM background_job
WHERE id = $1
",
&[&job_id],
).await?;
&[&job_id],
)
.await?;
if deleted_count == 0 {
return Err(DatabaseError::NotFound("background job"));
};
@ -82,10 +102,10 @@ pub async fn delete_job_from_queue(
#[cfg(test)]
mod tests {
use super::*;
use crate::database::test_utils::create_test_database;
use serde_json::json;
use serial_test::serial;
use crate::database::test_utils::create_test_database;
use super::*;
#[tokio::test]
#[serial]
@ -98,20 +118,22 @@ mod tests {
"failure_count": 0,
});
let scheduled_for = Utc::now();
enqueue_job(db_client, &job_type, &job_data, &scheduled_for).await.unwrap();
enqueue_job(db_client, &job_type, &job_data, &scheduled_for)
.await
.unwrap();
let batch_1 = get_job_batch(db_client, &job_type, 10).await.unwrap();
let batch_1 = get_job_batch(db_client, &job_type, 10, 3600).await.unwrap();
assert_eq!(batch_1.len(), 1);
let job = &batch_1[0];
assert_eq!(job.job_type, job_type);
assert_eq!(job.job_data, job_data);
assert_eq!(job.job_status, JobStatus::Running);
let batch_2 = get_job_batch(db_client, &job_type, 10).await.unwrap();
let batch_2 = get_job_batch(db_client, &job_type, 10, 3600).await.unwrap();
assert_eq!(batch_2.len(), 0);
delete_job_from_queue(db_client, &job.id).await.unwrap();
let batch_3 = get_job_batch(db_client, &job_type, 10).await.unwrap();
let batch_3 = get_job_batch(db_client, &job_type, 10, 3600).await.unwrap();
assert_eq!(batch_3.len(), 0);
}
}

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

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