Compare commits

..

No commits in common. "main" and "v1.14.0" have entirely different histories.

317 changed files with 12587 additions and 10364 deletions
.dockerignore.gitignore.woodpecker.ymlCHANGELOG.mdCargo.lockCargo.tomlFEDERATION.mdREADME.md
build
config.yaml.example
contracts
contrib
docs
fedimovies-cli/src
fedimovies-config/src
fedimovies-models
justfile
migrations
mitra-cli
mitra-config

View file

@ -1,21 +0,0 @@
# 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,9 +1,5 @@
.env.local
config.yaml
/secret/*
/files/*
!/files/.gitkeep
/build/*
!/build/.gitkeep
/target
fly.toml

View file

@ -1,63 +0,0 @@
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,209 +6,6 @@ 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

2362
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,39 +1,34 @@
[package]
name = "fedimovies"
version = "1.22.0"
description = "Movies reviews and ratings for the fediverse"
name = "mitra"
version = "1.14.0"
description = "Federated micro-blogging platform and content subscription service"
license = "AGPL-3.0"
edition = "2021"
rust-version = "1.68"
rust-version = "1.56"
publish = false
default-run = "fedimovies"
default-run = "mitra"
[workspace]
members = [
".",
"fedimovies-cli",
"fedimovies-config",
"fedimovies-models",
"fedimovies-utils",
"mitra-cli",
"mitra-config",
"mitra-utils",
]
default-members = [
".",
"fedimovies-cli",
"fedimovies-config",
"fedimovies-models",
"fedimovies-utils",
"mitra-cli",
]
[dependencies]
fedimovies-config = { path = "fedimovies-config" }
fedimovies-models = { path = "fedimovies-models" }
fedimovies-utils = { path = "fedimovies-utils" }
mitra-config = { path = "mitra-config" }
mitra-utils = { path = "mitra-utils" }
# Used to handle incoming HTTP requests
actix-cors = "0.6.4"
actix-cors = "0.6.2"
actix-files = "0.6.2"
actix-web = "4.3.1"
actix-web = "4.1.0"
actix-web-httpauth = "0.8.0"
# Used for catching errors
anyhow = "1.0.58"
@ -41,6 +36,9 @@ 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
@ -48,16 +46,22 @@ 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.3"
ed25519 = "1.5.2"
blake2 = "0.10.5"
# Used to query Monero node
monero-rpc = "0.3.2"
# Used to determine the number of CPUs on the system
num_cpus = "1.13.0"
# Used for working with regular expressions
regex = "1.6.0"
# Used for managing database migrations
refinery = { version = "0.8.4", features = ["tokio-postgres"] }
# Used for making async HTTP requests
reqwest = { version = "0.11.13", features = ["json", "multipart", "socks"] }
reqwest = { version = "0.11.10", 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"
@ -68,18 +72,25 @@ siwe = "0.4.0"
# Used for creating error types
thiserror = "1.0.37"
# Async runtime
tokio = { version = "=1.20.4", features = ["macros"] }
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" }
# 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]
fedimovies-config = { path = "fedimovies-config", features = ["test-utils"] }
fedimovies-models = { path = "fedimovies-models", features = ["test-utils"] }
fedimovies-utils = { path = "fedimovies-utils", features = ["test-utils"] }
mitra-config = { path = "mitra-config", features = ["test-utils"] }
mitra-utils = { path = "mitra-utils", features = ["test-utils"] }
serial_test = "0.7.0"
[features]
production = ["fedimovies-config/production"]
production = ["mitra-config/production"]

View file

@ -86,10 +86,6 @@ 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,38 +1,47 @@
# FediMovies
[![status-badge](https://ci.caric.io/api/badges/FediMovies/fedimovies/status.svg)](https://ci.caric.io/FediMovies/fedimovies)
# Mitra
Lively federated movies reviews platform.
Federated micro-blogging platform and content subscription service.
Built on [ActivityPub](https://www.w3.org/TR/activitypub/) protocol, self-hosted, lightweight. Part of the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
Features:
Subscriptions provide a way to receive monthly payments from subscribers and to publish private content made exclusively for them.
- 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.
Supported payment methods:
## Instances
- [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).
- [FediList](http://demo.fedilist.com/instance?software=fedimovies)
- [Fediverse Observer](https://fedimovies.fediverse.observer/list)
Other features:
Demo instance: https://nullpointer.social/ ([invite-only](https://nullpointer.social/about))
- [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))
## Code
Server: https://code.caric.io/reef/reef (this repo)
Server: https://codeberg.org/silverpill/mitra (this repo)
Web client:
Web client: https://codeberg.org/silverpill/mitra-web
Ethereum contracts: https://codeberg.org/silverpill/mitra-contracts
## Requirements
- Rust 1.57+ (when building from source)
- Rust 1.56+ (when building from source)
- PostgreSQL 12+
Optional:
- Monero node and Monero wallet service
- Ethereum node
- IPFS node (see [guide](./docs/ipfs.md))
## Installation
@ -45,57 +54,71 @@ Run:
cargo build --release --features production
```
This command will produce two binaries in `target/release` directory, `fedimovies` and `fedimoviesctl`.
This command will produce two binaries in `target/release` directory, `mitra` and `mitractl`.
Install PostgreSQL and create the database:
```sql
CREATE USER fedimovies WITH PASSWORD 'fedimovies';
CREATE DATABASE fedimovies OWNER fedimovies;
CREATE USER mitra WITH PASSWORD 'mitra';
CREATE DATABASE mitra OWNER mitra;
```
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.
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.
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.
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.
Start Fedimovies:
Start Mitra:
```shell
./fedimovies
./mitra
```
An HTTP server will be needed to handle HTTPS requests. See the example of [nginx configuration file](./contrib/fedimovies.nginx).
An HTTP server will be needed to handle HTTPS requests. See the example of [nginx configuration file](./contrib/mitra.nginx).
To run Fedimovies as a systemd service, check out the [systemd unit file example](./contrib/fedimovies.service).
To run Mitra as a systemd service, check out the [systemd unit file example](./contrib/mitra.service).
### Debian package
Download and install Fedimovies package:
Download and install Mitra package:
```shell
dpkg -i fedimovies.deb
dpkg -i mitra.deb
```
Install PostgreSQL and create the database:
```sql
CREATE USER fedimovies WITH PASSWORD 'fedimovies';
CREATE DATABASE fedimovies OWNER fedimovies;
CREATE USER mitra WITH PASSWORD 'mitra';
CREATE DATABASE mitra OWNER mitra;
```
Open configuration file `/etc/fedimovies/config.yaml` and configure the instance.
Open configuration file `/etc/mitra/config.yaml` and configure the instance.
Start Fedimovies:
Start Mitra:
```shell
systemctl start fedimovies
systemctl start mitra
```
An HTTP server will be needed to handle HTTPS requests. See the example of [nginx configuration file](./contrib/fedimovies.nginx).
An HTTP server will be needed to handle HTTPS requests. See the example of [nginx configuration file](./contrib/mitra.nginx).
### Tor federation
### Monero
See [guide](./docs/onion.md).
Install Monero node or choose a [public one](https://monero.fail/).
Configure and start [monero-wallet-rpc](https://monerodocs.org/interacting/monero-wallet-rpc-reference/) daemon. Add `disable-rpc-login=1` to your `monero-wallet-rpc` config (currently RPC auth is not supported in Mitra).
Create a wallet for your instance.
Add blockchain configuration to `blockchains` array in your configuration file.
### Ethereum
Install Ethereum client or choose a JSON-RPC API provider.
Deploy contracts on the blockchain. Instructions can be found at https://codeberg.org/silverpill/mitra-contracts.
Add blockchain configuration to `blockchains` array in your configuration file.
## Development
@ -110,7 +133,15 @@ docker-compose up -d
Test connection:
```shell
psql -h localhost -p 55432 -U fedimovies fedimovies
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
```
### Run web service
@ -130,7 +161,7 @@ cargo run
### Run CLI
```shell
cargo run --bin fedimoviesctl
cargo run --bin mitractl
```
### Run linter
@ -151,16 +182,20 @@ See [FEDERATION.md](./FEDERATION.md)
## Client API
Most methods are similar to Mastodon API, but Fedimovies is not fully compatible.
Most methods are similar to Mastodon API, but Mitra is not fully compatible.
[OpenAPI spec](./docs/openapi.yaml)
## CLI
`fedimoviesctl` is a command-line tool for performing instance maintenance.
`mitractl` is a command-line tool for performing instance maintenance.
[Documentation](./docs/fedimoviesctl.md)
[Documentation](./docs/mitractl.md)
## License
[AGPL-3.0](./LICENSE)
## Support
Monero: 8Ahza5RM4JQgtdqvpcF1U628NN5Q87eryXQad3Fy581YWTZU8o3EMbtScuioQZSkyNNEEE1Lkj2cSbG4VnVYCW5L1N4os5p

View file

View file

@ -14,5 +14,28 @@ 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'

30
contracts/IERC165.json Normal file
View file

@ -0,0 +1,30 @@
{
"_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

@ -0,0 +1,233 @@
{
"_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

@ -0,0 +1,341 @@
{
"_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": {}
}

30
contracts/IGate.json Normal file
View file

@ -0,0 +1,30 @@
{
"_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": {}
}

57
contracts/IMinter.json Normal file
View file

@ -0,0 +1,57 @@
{
"_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

@ -0,0 +1,87 @@
{
"_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

@ -0,0 +1,137 @@
{
"_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": {}
}

View file

@ -1,16 +0,0 @@
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 40M;
client_max_body_size 10M;
location / {
# Frontend

View file

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

View file

@ -20,19 +20,18 @@ instance_uri: https://example.tld
instance_title: example
instance_short_description: my instance
# Long description can contain markdown syntax
instance_description: |
# My instance
Welcome!
instance_description: my instance
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:
@ -41,21 +40,38 @@ registration:
# character_limit: 2000
# Data retention parameters
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'
#retention:
# extraneous_posts: 50
# empty_profiles: 150
# 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 (note is optional):
Generate invite code:
```shell
mitractl generate-invite-code <note>
mitractl generate-invite-code
```
List generated invites:
@ -32,19 +32,13 @@ 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 (admin, user or read_only_user).
Change user's role:
```shell
mitractl set-role <user-id> <role-name>
@ -86,12 +80,6 @@ Delete empty remote profiles:
mitractl delete-empty-profiles 100
```
Delete unused remote emojis:
```shell
mitractl prune-remote-emojis
```
Import custom emoji from another instance:
```shell

View file

@ -1,42 +0,0 @@
# 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,12 +352,6 @@ 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.
@ -596,37 +590,6 @@ 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.
@ -780,29 +743,6 @@ 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.
@ -826,28 +766,6 @@ 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
@ -959,10 +877,6 @@ 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
@ -1502,11 +1416,6 @@ 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
@ -1531,10 +1440,6 @@ 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:
@ -1543,19 +1448,6 @@ 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:
@ -1583,7 +1475,6 @@ components:
- unknown
- image
- video
- audio
url:
description: The location of the original full-size attachment.
type: string
@ -1868,8 +1759,6 @@ components:
enum:
- create_follow_request
- create_post
- delete_any_post
- delete_any_profile
- manage_subscription_options
Signature:
type: object
@ -1911,13 +1800,6 @@ 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,76 +0,0 @@
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,38 +0,0 @@
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,66 +0,0 @@
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

@ -1,46 +0,0 @@
[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

@ -1,2 +0,0 @@
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

@ -1,5 +0,0 @@
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

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

View file

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

View file

@ -1,18 +0,0 @@
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

@ -1,2 +0,0 @@
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

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

View file

@ -1,5 +0,0 @@
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

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

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

View file

@ -1,2 +0,0 @@
ALTER TABLE post ADD COLUMN is_sensitive BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE post ALTER COLUMN is_sensitive DROP DEFAULT;

View file

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

View file

@ -1,41 +0,0 @@
use crate::database::{DatabaseClient, DatabaseError};
pub async fn create_instance(
db_client: &impl DatabaseClient,
hostname: &str,
) -> Result<(), DatabaseError> {
db_client
.execute(
"
INSERT INTO instance VALUES ($1)
ON CONFLICT DO NOTHING
",
&[&hostname],
)
.await?;
Ok(())
}
pub async fn get_peers(db_client: &impl DatabaseClient) -> Result<Vec<String>, DatabaseError> {
let rows = db_client
.query(
"
SELECT instance.hostname FROM instance
",
&[],
)
.await?;
let peers = rows
.iter()
.map(|row| row.try_get("hostname"))
.collect::<Result<_, _>>()?;
Ok(peers)
}
pub async fn get_peer_count(db_client: &impl DatabaseClient) -> Result<i64, DatabaseError> {
let row = db_client
.query_one("SELECT count(instance) FROM instance", &[])
.await?;
let count = row.try_get("count")?;
Ok(count)
}

View file

@ -1,265 +0,0 @@
use uuid::Uuid;
use crate::database::{DatabaseClient, DatabaseError};
use crate::posts::{
helpers::{add_related_posts, add_user_actions},
queries::{RELATED_ATTACHMENTS, RELATED_EMOJIS, RELATED_LINKS, RELATED_MENTIONS, RELATED_TAGS},
};
use super::types::{EventType, Notification};
async fn create_notification(
db_client: &impl DatabaseClient,
sender_id: &Uuid,
recipient_id: &Uuid,
post_id: Option<&Uuid>,
event_type: EventType,
) -> Result<(), DatabaseError> {
db_client
.execute(
"
INSERT INTO notification (
sender_id,
recipient_id,
post_id,
event_type
)
VALUES ($1, $2, $3, $4)
",
&[&sender_id, &recipient_id, &post_id, &event_type],
)
.await?;
Ok(())
}
pub async fn delete_notification(
db_client: &impl DatabaseClient,
notification_id: i32,
) -> Result<(), DatabaseError> {
db_client
.execute(
"
DELETE FROM notification
WHERE id = $1
",
&[&notification_id],
)
.await?;
Ok(())
}
pub async fn create_follow_notification(
db_client: &impl DatabaseClient,
sender_id: &Uuid,
recipient_id: &Uuid,
) -> Result<(), DatabaseError> {
create_notification(db_client, sender_id, recipient_id, None, EventType::Follow).await
}
pub async fn create_reply_notification(
db_client: &impl DatabaseClient,
sender_id: &Uuid,
recipient_id: &Uuid,
post_id: &Uuid,
) -> Result<(), DatabaseError> {
create_notification(
db_client,
sender_id,
recipient_id,
Some(post_id),
EventType::Reply,
)
.await
}
pub async fn create_reaction_notification(
db_client: &impl DatabaseClient,
sender_id: &Uuid,
recipient_id: &Uuid,
post_id: &Uuid,
) -> Result<(), DatabaseError> {
create_notification(
db_client,
sender_id,
recipient_id,
Some(post_id),
EventType::Reaction,
)
.await
}
pub async fn create_mention_notification(
db_client: &impl DatabaseClient,
sender_id: &Uuid,
recipient_id: &Uuid,
post_id: &Uuid,
) -> Result<(), DatabaseError> {
create_notification(
db_client,
sender_id,
recipient_id,
Some(post_id),
EventType::Mention,
)
.await
}
pub async fn create_repost_notification(
db_client: &impl DatabaseClient,
sender_id: &Uuid,
recipient_id: &Uuid,
post_id: &Uuid,
) -> Result<(), DatabaseError> {
create_notification(
db_client,
sender_id,
recipient_id,
Some(post_id),
EventType::Repost,
)
.await
}
pub async fn create_subscription_notification(
db_client: &impl DatabaseClient,
sender_id: &Uuid,
recipient_id: &Uuid,
) -> Result<(), DatabaseError> {
create_notification(
db_client,
sender_id,
recipient_id,
None,
EventType::Subscription,
)
.await
}
pub async fn create_subscription_expiration_notification(
db_client: &impl DatabaseClient,
sender_id: &Uuid,
recipient_id: &Uuid,
) -> Result<(), DatabaseError> {
create_notification(
db_client,
sender_id,
recipient_id,
None,
EventType::SubscriptionExpiration,
)
.await
}
pub async fn create_move_notification(
db_client: &impl DatabaseClient,
sender_id: &Uuid,
recipient_id: &Uuid,
) -> Result<(), DatabaseError> {
create_notification(db_client, sender_id, recipient_id, None, EventType::Move).await
}
pub async fn get_notifications(
db_client: &impl DatabaseClient,
recipient_id: &Uuid,
max_id: Option<i32>,
limit: u16,
) -> Result<Vec<Notification>, DatabaseError> {
let statement = format!(
"
SELECT
notification, sender, post, post_author, recipient,
{related_attachments},
{related_mentions},
{related_tags},
{related_links},
{related_emojis}
FROM notification
JOIN actor_profile AS sender
ON notification.sender_id = sender.id
LEFT JOIN post
ON notification.post_id = post.id
LEFT JOIN actor_profile AS post_author
ON post.author_id = post_author.id
LEFT JOIN actor_profile AS recipient
ON notification.recipient_id = recipient.id
WHERE
recipient_id = $1
AND ($2::integer IS NULL OR notification.id < $2)
ORDER BY notification.id DESC
LIMIT $3
",
related_attachments = RELATED_ATTACHMENTS,
related_mentions = RELATED_MENTIONS,
related_tags = RELATED_TAGS,
related_links = RELATED_LINKS,
related_emojis = RELATED_EMOJIS,
);
let rows = db_client
.query(&statement, &[&recipient_id, &max_id, &i64::from(limit)])
.await?;
let mut notifications: Vec<Notification> = rows
.iter()
.map(Notification::try_from)
.collect::<Result<_, _>>()?;
add_related_posts(
db_client,
notifications
.iter_mut()
.filter_map(|item| item.post.as_mut())
.collect(),
)
.await?;
add_user_actions(
db_client,
recipient_id,
notifications
.iter_mut()
.filter_map(|item| item.post.as_mut())
.collect(),
)
.await?;
Ok(notifications)
}
pub async fn get_mention_notifications(
db_client: &impl DatabaseClient,
limit: u16,
) -> Result<Vec<Notification>, DatabaseError> {
let statement = format!(
"
SELECT
notification, sender, post, post_author, recipient,
{related_attachments},
{related_mentions},
{related_tags},
{related_links},
{related_emojis}
FROM notification
JOIN actor_profile AS sender
ON notification.sender_id = sender.id
LEFT JOIN post
ON notification.post_id = post.id
LEFT JOIN actor_profile AS post_author
ON post.author_id = post_author.id
LEFT JOIN actor_profile AS recipient
ON notification.recipient_id = recipient.id
WHERE
event_type = $1
ORDER BY notification.id DESC
LIMIT $2
",
related_attachments = RELATED_ATTACHMENTS,
related_mentions = RELATED_MENTIONS,
related_tags = RELATED_TAGS,
related_links = RELATED_LINKS,
related_emojis = RELATED_EMOJIS,
);
let rows = db_client
.query(&statement, &[&EventType::Mention, &i64::from(limit)])
.await?;
let notifications: Vec<Notification> = rows
.iter()
.map(Notification::try_from)
.collect::<Result<_, _>>()?;
Ok(notifications)
}

View file

@ -1,38 +0,0 @@
use crate::database::{DatabaseClient, DatabaseError};
use super::queries::{get_profile_by_remote_actor_id, search_profiles_by_did_only};
use super::types::DbActorProfile;
pub async fn find_declared_aliases(
db_client: &impl DatabaseClient,
profile: &DbActorProfile,
) -> Result<Vec<DbActorProfile>, DatabaseError> {
let mut results = vec![];
for actor_id in profile.aliases.clone().into_actor_ids() {
let alias = match get_profile_by_remote_actor_id(db_client, &actor_id).await {
Ok(profile) => profile,
// Ignore unknown profiles
Err(DatabaseError::NotFound(_)) => continue,
Err(other_error) => return Err(other_error),
};
results.push(alias);
}
Ok(results)
}
pub async fn find_verified_aliases(
db_client: &impl DatabaseClient,
profile: &DbActorProfile,
) -> Result<Vec<DbActorProfile>, DatabaseError> {
let mut results = vec![];
for identity_proof in profile.identity_proofs.inner() {
let aliases = search_profiles_by_did_only(db_client, &identity_proof.issuer).await?;
for alias in aliases {
if alias.id == profile.id {
continue;
};
results.push(alias);
}
}
Ok(results)
}

View file

@ -1,3 +0,0 @@
pub mod helpers;
pub mod queries;
pub mod types;

View file

@ -1,72 +0,0 @@
use serde::{de::DeserializeOwned, Serialize};
use serde_json::Value as JsonValue;
use crate::database::{DatabaseClient, DatabaseError, DatabaseTypeError};
pub async fn set_internal_property(
db_client: &impl DatabaseClient,
name: &str,
value: &impl Serialize,
) -> Result<(), DatabaseError> {
let value_json = serde_json::to_value(value).map_err(|_| DatabaseTypeError)?;
db_client
.execute(
"
INSERT INTO internal_property (property_name, property_value)
VALUES ($1, $2)
ON CONFLICT (property_name) DO UPDATE
SET property_value = $2
",
&[&name, &value_json],
)
.await?;
Ok(())
}
pub async fn get_internal_property<T: DeserializeOwned>(
db_client: &impl DatabaseClient,
name: &str,
) -> Result<Option<T>, DatabaseError> {
let maybe_row = db_client
.query_opt(
"
SELECT property_value
FROM internal_property
WHERE property_name = $1
",
&[&name],
)
.await?;
let maybe_value = match maybe_row {
Some(row) => {
let value_json: JsonValue = row.try_get("property_value")?;
let value: T = serde_json::from_value(value_json).map_err(|_| DatabaseTypeError)?;
Some(value)
}
None => None,
};
Ok(maybe_value)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::database::test_utils::create_test_database;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn test_set_internal_property() {
let db_client = &create_test_database().await;
let name = "myproperty";
let value = 100;
set_internal_property(db_client, name, &value)
.await
.unwrap();
let db_value: u32 = get_internal_property(db_client, name)
.await
.unwrap()
.unwrap_or_default();
assert_eq!(db_value, value);
}
}

View file

@ -1 +0,0 @@
pub mod queries;

View file

@ -1,9 +0,0 @@
#!/usr/bin/env -S just --justfile
build-release:
cargo build --release --target x86_64-unknown-linux-gnu
cp target/x86_64-unknown-linux-gnu/release/fedimovies build/fedimovies
cp target/x86_64-unknown-linux-gnu/release/fedimoviesctl build/fedimoviesctl
deploy: build-release
fly deploy

View file

@ -1,8 +1,3 @@
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,
@ -26,18 +21,15 @@ 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(2000) UNIQUE GENERATED ALWAYS AS (actor_json ->> 'id') STORED,
actor_id VARCHAR(200) 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,
@ -46,9 +38,7 @@ CREATE TABLE actor_profile (
CREATE TABLE user_invite_code (
code VARCHAR(100) PRIMARY KEY,
used BOOLEAN NOT NULL DEFAULT FALSE,
note VARCHAR(200),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
used BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE TABLE user_account (
@ -58,7 +48,6 @@ 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()
);
@ -67,7 +56,7 @@ CREATE TABLE oauth_application (
app_name VARCHAR(100) NOT NULL,
website VARCHAR(100),
scopes VARCHAR(200) NOT NULL,
redirect_uri VARCHAR(2000) NOT NULL,
redirect_uri VARCHAR(200) 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
@ -96,18 +85,16 @@ 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),
CHECK (source_id != target_id)
UNIQUE (source_id, target_id, relationship_type)
);
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(2000) UNIQUE,
activity_id VARCHAR(250) UNIQUE,
request_status SMALLINT NOT NULL,
UNIQUE (source_id, target_id),
CHECK (source_id != target_id)
UNIQUE (source_id, target_id)
);
CREATE TABLE post (
@ -116,12 +103,11 @@ 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,
visibility SMALLINT NOT NULL,
is_sensitive BOOLEAN NOT NULL,
visilibity SMALLINT 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(2000) UNIQUE,
object_id VARCHAR(200) UNIQUE,
ipfs_cid VARCHAR(200),
token_id INTEGER,
token_tx_id VARCHAR(200),
@ -134,7 +120,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(2000) UNIQUE,
activity_id VARCHAR(250) UNIQUE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
UNIQUE (author_id, post_id)
);
@ -170,8 +156,7 @@ 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),
CHECK (source_id != target_id)
PRIMARY KEY (source_id, target_id)
);
CREATE TABLE emoji (
@ -179,7 +164,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(2000) UNIQUE,
object_id VARCHAR(250) UNIQUE,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
UNIQUE (emoji_name, hostname),
CHECK ((hostname IS NULL) = (object_id IS NULL))
@ -191,12 +176,6 @@ 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,
@ -224,8 +203,7 @@ 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),
CHECK (sender_id != recipient_id)
UNIQUE (chain_id, payment_address)
);
CREATE TABLE subscription (
@ -236,6 +214,5 @@ 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),
CHECK (sender_id != recipient_id)
UNIQUE (sender_id, recipient_id)
);

View file

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

View file

@ -1,39 +1,58 @@
use anyhow::Error;
use anyhow::{anyhow, Error};
use clap::Parser;
use uuid::Uuid;
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::activitypub::{
actors::helpers::update_remote_profile,
builders::delete_note::prepare_delete_note,
builders::delete_person::prepare_delete_person,
fetcher::fetchers::fetch_actor,
};
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::{
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::{
attachments::queries::delete_unused_attachments,
cleanup::find_orphaned_files,
database::DatabaseClient,
emojis::helpers::get_emoji_by_name,
emojis::queries::{
create_emoji, delete_emoji, find_unused_remote_emojis, get_emoji_by_name_and_hostname,
create_emoji,
delete_emoji,
get_emoji_by_name_and_hostname,
},
emojis::validators::EMOJI_LOCAL_MAX_SIZE,
oauth::queries::delete_oauth_tokens,
posts::queries::{delete_post, find_extraneous_posts, get_post_by_id},
profiles::queries::{
delete_profile, find_empty_profiles, find_unreachable, get_profile_by_id,
delete_profile,
find_empty_profiles,
get_profile_by_id,
get_profile_by_remote_actor_id,
},
subscriptions::queries::reset_subscriptions,
users::queries::{
create_invite_code, create_user, get_invite_codes, get_user_by_id, set_user_password,
create_invite_code,
get_invite_codes,
get_user_by_id,
set_user_password,
set_user_role,
},
users::types::UserCreateData,
users::types::Role,
};
use fedimovies_utils::{
crypto_rsa::{generate_rsa_key, serialize_private_key},
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,
},
datetime::{days_before_now, get_min_datetime},
passwords::hash_password,
};
@ -52,11 +71,9 @@ pub enum SubCommand {
GenerateInviteCode(GenerateInviteCode),
ListInviteCodes(ListInviteCodes),
CreateUser(CreateUser),
SetPassword(SetPassword),
SetRole(SetRole),
RefetchActor(RefetchActor),
ReadOutbox(ReadOutbox),
DeleteProfile(DeleteProfile),
DeletePost(DeletePost),
DeleteEmoji(DeleteEmoji),
@ -64,8 +81,6 @@ pub enum SubCommand {
DeleteUnusedAttachments(DeleteUnusedAttachments),
DeleteOrphanedFiles(DeleteOrphanedFiles),
DeleteEmptyProfiles(DeleteEmptyProfiles),
PruneRemoteEmojis(PruneRemoteEmojis),
ListUnreachableActors(ListUnreachableActors),
ImportEmoji(ImportEmoji),
UpdateCurrentBlock(UpdateCurrentBlock),
ResetSubscriptions(ResetSubscriptions),
@ -91,19 +106,25 @@ pub struct GenerateEthereumAddress;
impl GenerateEthereumAddress {
pub fn execute(&self) -> () {
println!("dummy");
let private_key = generate_ecdsa_key();
let address = key_to_ethereum_address(&private_key);
println!(
"address {:?}; private key {}",
address, private_key.display_secret(),
);
}
}
/// Generate invite code
#[derive(Parser)]
pub struct GenerateInviteCode {
note: Option<String>,
}
pub struct GenerateInviteCode;
impl GenerateInviteCode {
pub async fn execute(&self, db_client: &impl DatabaseClient) -> Result<(), Error> {
let invite_code = create_invite_code(db_client, self.note.as_deref()).await?;
pub async fn execute(
&self,
db_client: &impl DatabaseClient,
) -> Result<(), Error> {
let invite_code = create_invite_code(db_client).await?;
println!("generated invite code: {}", invite_code);
Ok(())
}
@ -114,49 +135,18 @@ 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 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,
for code in invite_codes {
println!("{}", code);
};
create_user(db_client, user_data).await?;
println!("user created");
Ok(())
}
}
@ -169,7 +159,10 @@ 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
@ -183,13 +176,15 @@ 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_str(&self.role)?;
pub async fn execute(
&self,
db_client: &impl DatabaseClient,
) -> Result<(), Error> {
let role = Role::from_name(&self.role)?;
set_user_role(db_client, &self.id, role).await?;
println!("role changed");
Ok(())
@ -206,42 +201,25 @@ impl RefetchActor {
pub async fn execute(
&self,
config: &Config,
db_client: &mut impl DatabaseClient,
db_client: &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(),
&MediaStorage::from(config),
&config.media_dir(),
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 {
@ -258,14 +236,15 @@ 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?;
remove_media(config, deletion_queue).await;
deletion_queue.process(config).await;
// Send Delete(Person) activities
if let Some(activity) = maybe_delete_person {
activity.enqueue(db_client).await?;
activity.deliver().await?;
};
println!("profile deleted");
Ok(())
@ -288,15 +267,19 @@ 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?;
remove_media(config, deletion_queue).await;
deletion_queue.process(config).await;
// Send Delete(Note) activity
if let Some(activity) = maybe_delete_note {
activity.enqueue(db_client).await?;
activity.deliver().await?;
};
println!("post deleted");
Ok(())
@ -316,10 +299,13 @@ 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?;
remove_media(config, deletion_queue).await;
deletion_queue.process(config).await;
println!("emoji deleted");
Ok(())
}
@ -341,9 +327,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?;
remove_media(config, deletion_queue).await;
deletion_queue.process(config).await;
println!("post {} deleted", post_id);
}
};
Ok(())
}
}
@ -361,8 +347,11 @@ 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?;
remove_media(config, deletion_queue).await;
let deletion_queue = delete_unused_attachments(
db_client,
&created_before,
).await?;
deletion_queue.process(config).await;
println!("unused attachments deleted");
Ok(())
}
@ -381,9 +370,10 @@ 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() {
@ -411,59 +401,9 @@ 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?;
remove_media(config, deletion_queue).await;
deletion_queue.process(config).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(())
}
}
@ -481,8 +421,11 @@ 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(());
@ -494,8 +437,7 @@ impl ImportEmoji {
emoji.image,
None,
&get_min_datetime(),
)
.await?;
).await?;
println!("added emoji to local collection");
Ok(())
}
@ -510,9 +452,10 @@ 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(())
}
@ -547,7 +490,18 @@ pub struct CreateMoneroWallet {
}
impl CreateMoneroWallet {
pub async fn execute(&self, _config: &Config) -> Result<(), Error> {
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?;
println!("wallet created");
Ok(())
}
@ -562,9 +516,17 @@ 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(())
}
}

53
mitra-cli/src/main.rs Normal file
View file

@ -0,0 +1,53 @@
use clap::Parser;
use mitra::database::create_database_client;
use mitra::database::migrate::apply_migrations;
use mitra::logger::configure_logger;
use mitra_config::parse_config;
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).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::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::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::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(),
_ => panic!(),
};
},
};
}

View file

@ -1,12 +1,12 @@
[package]
name = "fedimovies-config"
version = "1.22.0"
name = "mitra-config"
version = "1.14.0"
license = "AGPL-3.0"
edition = "2021"
rust-version = "1.68"
rust-version = "1.56"
[dependencies]
fedimovies-utils = { path = "../fedimovies-utils" }
mitra-utils = { path = "../mitra-utils" }
# Used to read .env files
dotenv = "0.15.0"

View file

@ -0,0 +1,87 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use mitra_utils::caip2::{ChainId, ChainIdError};
fn default_chain_sync_step() -> u64 { 1000 }
fn default_chain_reorg_max_depth() -> u64 { 10 }
#[derive(Clone, Deserialize, Serialize)]
pub struct EthereumChainMetadata {
pub chain_name: String,
pub currency_name: String,
pub currency_symbol: String,
pub currency_decimals: u8,
pub public_api_url: String,
// Block explorer base URL (should be compatible with https://eips.ethereum.org/EIPS/eip-3091)
pub explorer_url: Option<String>,
}
#[derive(Clone, Deserialize)]
pub struct EthereumConfig {
// CAIP-2 chain ID
pub chain_id: ChainId,
// Additional information for clients
// https://github.com/ethereum-lists/chains
pub chain_metadata: Option<EthereumChainMetadata>,
pub contract_address: String,
pub contract_dir: PathBuf,
pub api_url: String,
// Instance private key
pub signing_key: String,
#[serde(default = "default_chain_sync_step")]
pub chain_sync_step: u64,
#[serde(default = "default_chain_reorg_max_depth")]
pub chain_reorg_max_depth: u64,
}
impl EthereumConfig {
pub fn try_ethereum_chain_id(&self) -> Result<u32, ChainIdError> {
self.chain_id.ethereum_chain_id()
}
pub fn ethereum_chain_id(&self) -> u32 {
self.try_ethereum_chain_id().unwrap()
}
}
#[derive(Clone, Deserialize)]
pub struct MoneroConfig {
pub chain_id: ChainId,
#[serde(alias = "daemon_url")]
pub node_url: String,
pub wallet_url: String,
// Wallet name and password are required when
// monero-wallet-rpc is running with --wallet-dir option
pub wallet_name: Option<String>,
pub wallet_password: Option<String>,
}
#[derive(Clone, Deserialize)]
#[serde(untagged)]
pub enum BlockchainConfig {
Ethereum(EthereumConfig),
Monero(MoneroConfig),
}
impl BlockchainConfig {
pub fn ethereum_config(&self) -> Option<&EthereumConfig> {
if let Self::Ethereum(ethereum_config) = self {
Some(ethereum_config)
} else {
None
}
}
pub fn monero_config(&self) -> Option<&MoneroConfig> {
if let Self::Monero(monero_config) = self {
Some(monero_config)
} else {
None
}
}
}

View file

@ -1,27 +1,59 @@
use std::path::PathBuf;
use log::Level as LogLevel;
use log::{Level as LogLevel};
use rsa::RsaPrivateKey;
use serde::Deserialize;
use serde::{
Deserialize,
Deserializer,
de::Error as DeserializerError,
};
use url::Url;
use fedimovies_utils::urls::normalize_url;
use mitra_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::REEF_VERSION;
use super::MITRA_VERSION;
fn default_log_level() -> LogLevel {
LogLevel::Info
#[derive(Clone, PartialEq)]
pub enum RegistrationType {
Open,
Invite,
}
fn default_login_message() -> String {
"What?!".to_string()
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, 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
@ -33,8 +65,6 @@ 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>,
@ -55,11 +85,6 @@ 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>,
@ -74,20 +99,23 @@ 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>,
@ -102,14 +130,8 @@ impl Config {
Instance {
_url: self.try_instance_url().unwrap(),
actor_key: self.instance_rsa_key.clone().unwrap(),
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,
proxy_url: self.proxy_url.clone(),
is_private: matches!(self.environment, Environment::Development),
}
}
@ -120,6 +142,18 @@ 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)]
@ -129,12 +163,8 @@ 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 {
@ -148,9 +178,9 @@ impl Instance {
pub fn agent(&self) -> String {
format!(
"Reef {version}; {instance_url}",
version = REEF_VERSION,
instance_url = self.url(),
"Mitra {version}; {instance_url}",
version=MITRA_VERSION,
instance_url=self.url(),
)
}
}
@ -158,24 +188,20 @@ impl Instance {
#[cfg(feature = "test-utils")]
impl Instance {
pub fn for_test(url: &str) -> Self {
use fedimovies_utils::crypto_rsa::generate_weak_rsa_key;
use mitra_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() {
@ -185,18 +211,14 @@ 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", REEF_VERSION),
format!("Mitra {}; https://example.com", MITRA_VERSION),
);
}
@ -208,11 +230,7 @@ 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,13 +10,9 @@ 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

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

View file

@ -1,17 +1,19 @@
use super::ConfigError;
use regex::Regex;
use serde::{de::Error as DeserializerError, Deserialize, Deserializer};
use serde::{
Deserialize,
Deserializer,
de::{Error as DeserializerError},
};
use super::ConfigError;
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() {
@ -24,49 +26,37 @@ 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_emoji_size_limit() -> usize {
500_000
} // 500 kB
const fn default_file_size_limit() -> usize { 20_000_000 } // 20 MB
#[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 {

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