Compare commits
No commits in common. "8c03ac337d6d1d3b41f5cb39d4fab325b9fbb63e" and "6f71ab08f698adfdc55a81ace290c24d8196fe3a" have entirely different histories.
8c03ac337d
...
6f71ab08f6
11 changed files with 15 additions and 227 deletions
65
README.md
65
README.md
|
@ -1,65 +0,0 @@
|
||||||
# Fingerlink
|
|
||||||
General purpose WebFinger daemon and utilities for managing its served resources written in Rust.
|
|
||||||
## Basic usage
|
|
||||||
The program is split into two halves: the server daemon and the editing tools.
|
|
||||||
|
|
||||||
There are lots of command line parameters built in, so always check the subcommand's
|
|
||||||
help page with `fingerlink help [subcommand]` before using.
|
|
||||||
|
|
||||||
Every command that can alter the database defaults to dry-run mode where the actual database
|
|
||||||
isn't altered. To actually save the result of a command, supply the `--save` flag. The program
|
|
||||||
also always keeps one backup of the previous database state in case something goes wrong during
|
|
||||||
writing. The editing processes can also try to reload the running server process when
|
|
||||||
the `--reload-server` flag is supplied.
|
|
||||||
## Subcommands
|
|
||||||
### `serve`
|
|
||||||
Starts serving the database. By default, it binds to 127.0.0.1:8080, but these values
|
|
||||||
can be changed by using the `--bind-ip` and `--bind-port` parameters.
|
|
||||||
|
|
||||||
The server keeps a PID file around to enable the optional auto-refresh functionality of
|
|
||||||
the editing tools and to prevent accidentally running multiple instances of the server
|
|
||||||
at once.
|
|
||||||
|
|
||||||
### `fetch`
|
|
||||||
The simplest way to get new records into the database. By supplying a list of
|
|
||||||
Fediverse-compatible handles, their resource data can be quickly and easily imported
|
|
||||||
into the database.
|
|
||||||
|
|
||||||
The command tries to prevent multiple resources from taking the same query strings,
|
|
||||||
and to help with this, there are multiple implemented strategies to handle collisions.
|
|
||||||
- `terminate` simply stops the import process if a collision is detected. The resources
|
|
||||||
processed up to the collision are still inserted.
|
|
||||||
- `skip` skips over every resource that would collide with another resource already
|
|
||||||
in the database
|
|
||||||
- `overwrite-single-skip-multiple` overwrites the old resource with the new one, but only
|
|
||||||
if there's just a single already saved resource that the new resource will collide with.
|
|
||||||
Otherwise, it skips. This is the default, as it's the most useful for updating old records.
|
|
||||||
- `overwrite-multiple` overwrites the old resource or resources that the new record collides
|
|
||||||
with. Can be highly destructive, be careful!
|
|
||||||
|
|
||||||
In case you want to batch-import handles from a file, you can use the `--handles-are-files` flag.
|
|
||||||
When it's enabled, the command opens the provided handles as files and reads their contents.
|
|
||||||
One line, one query.
|
|
||||||
|
|
||||||
It can be very useful to replace the old domain in the `subject` field with the one that the
|
|
||||||
service will run on. In this case you can supply the `--new-domain` parameter. It runs a very
|
|
||||||
simple process
|
|
||||||
1. Takes the `subject` field of the returned resource and cuts off everything after the last @ sign.
|
|
||||||
2. Appends the string provided with the `--new-domain` parameter.
|
|
||||||
3. Puts it in the `subject` field instead of the old subject
|
|
||||||
4. Appends the old subject to the `aliases` array
|
|
||||||
|
|
||||||
### `query`
|
|
||||||
A simple command to get the response JSON to a query without `curl`. It pretty-prints the JSON.
|
|
||||||
|
|
||||||
### `editor`
|
|
||||||
Uses the editor defined in `$EDITOR` (or `vi` if the environment variable isn't set) to edit
|
|
||||||
the JSON record returned to the provided query. Like every editor command, it runs in dry-run
|
|
||||||
mode and requires the `--save` flag to be present to actually modify the database.
|
|
||||||
|
|
||||||
If the `--create` flag is present, then if the query doesn't return a resource, it creates a
|
|
||||||
new blank resource to fill out and insert into the database.
|
|
||||||
|
|
||||||
### `init`
|
|
||||||
Creates a new blank database file. If one is already present, it requires the `--force` flag
|
|
||||||
to be present otherwise it refuses to delete the database.
|
|
|
@ -66,7 +66,7 @@ pub struct SaveSettings {
|
||||||
pub save: bool,
|
pub save: bool,
|
||||||
|
|
||||||
/// Save behaviour when encountering a collision
|
/// Save behaviour when encountering a collision
|
||||||
#[arg(long, default_value = "overwrite-single-skip-multiple")]
|
#[arg(long, short = 'c', default_value = "overwrite-single-skip-multiple")]
|
||||||
pub collision_handling: CollisionHandling,
|
pub collision_handling: CollisionHandling,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,9 +100,6 @@ pub enum Command {
|
||||||
save: SaveSettings,
|
save: SaveSettings,
|
||||||
/// The resource to query for and edit
|
/// The resource to query for and edit
|
||||||
resource: String,
|
resource: String,
|
||||||
/// Create a blank resource with RESOURCE as the subject if the query fails
|
|
||||||
#[arg(long, short = 'c')]
|
|
||||||
create: bool,
|
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
server_reload: ServerReloadOptions,
|
server_reload: ServerReloadOptions,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,31 +1,18 @@
|
||||||
use crate::args_parser::SaveSettings;
|
use crate::args_parser::SaveSettings;
|
||||||
use crate::editor::open_in_editor::open_resource_in_editor;
|
use crate::editor::open_in_editor::open_resource_in_editor;
|
||||||
use crate::schema::lookup_handler::LookupHandler;
|
use crate::schema::lookup_handler::LookupHandler;
|
||||||
use crate::schema::resource::Resource;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tracing::{error, info};
|
use tracing::info;
|
||||||
|
|
||||||
pub fn editor(database_path: PathBuf, save_settings: SaveSettings, resource: String, create: bool) {
|
pub fn editor(database_path: PathBuf, save_settings: SaveSettings, resource: String) {
|
||||||
let resources = LookupHandler::load(&database_path).unwrap();
|
let resources = LookupHandler::load(&database_path).unwrap();
|
||||||
let (index, resource) = match resources.lookup_with_index(resource.as_str()) {
|
let (index, resource) = resources
|
||||||
None => {
|
.lookup_with_index(resource.as_str())
|
||||||
if create {
|
.expect("Couldn't find a resource for that query");
|
||||||
(None, Resource::new(resource))
|
|
||||||
} else {
|
|
||||||
error!("Couldn't find a resource for the query \"{resource}\"");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some((index, resource)) => (Some(index), resource.clone()),
|
|
||||||
};
|
|
||||||
let resource = open_resource_in_editor(resource).unwrap();
|
let resource = open_resource_in_editor(resource).unwrap();
|
||||||
if save_settings.save {
|
if save_settings.save {
|
||||||
let mut resources = resources.into_inner();
|
let mut resources = resources.into_inner();
|
||||||
if let Some(index) = index {
|
resources.0[index] = resource;
|
||||||
resources.0[index] = resource;
|
|
||||||
} else {
|
|
||||||
resources.0.push(resource);
|
|
||||||
}
|
|
||||||
resources.save(database_path).unwrap();
|
resources.save(database_path).unwrap();
|
||||||
} else {
|
} else {
|
||||||
info!("To save edits, run this command with the -s flag")
|
info!("To save edits, run this command with the -s flag")
|
||||||
|
|
|
@ -4,7 +4,7 @@ use std::path::PathBuf;
|
||||||
|
|
||||||
pub fn query(database_path: PathBuf, handle: String) {
|
pub fn query(database_path: PathBuf, handle: String) {
|
||||||
let data = LookupHandler::load(database_path).unwrap();
|
let data = LookupHandler::load(database_path).unwrap();
|
||||||
let resource = data.lookup(handle.trim().to_lowercase().as_str()).unwrap();
|
let resource = data.lookup(handle.trim()).unwrap();
|
||||||
serde_json::to_writer_pretty(stdout(), resource).unwrap();
|
serde_json::to_writer_pretty(stdout(), resource).unwrap();
|
||||||
println!()
|
println!()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::schema::resource::Resource;
|
use crate::schema::resource::Resource;
|
||||||
use crate::schema::resource_complete_serialisation::ResourceComplete;
|
|
||||||
use std::fmt::{Debug, Display};
|
use std::fmt::{Debug, Display};
|
||||||
use std::io::{Read, Seek, SeekFrom, Write};
|
use std::io::{Read, Seek, SeekFrom, Write};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
@ -81,11 +80,9 @@ pub enum ResourceEditingError {
|
||||||
|
|
||||||
/// Opens the provided `resource` in the system editor and returns the edited version or an error if something goes wrong.
|
/// Opens the provided `resource` in the system editor and returns the edited version or an error if something goes wrong.
|
||||||
#[instrument(skip(resource))]
|
#[instrument(skip(resource))]
|
||||||
pub fn open_resource_in_editor(resource: Resource) -> Result<Resource, ResourceEditingError> {
|
pub fn open_resource_in_editor(resource: &Resource) -> Result<Resource, ResourceEditingError> {
|
||||||
use ResourceEditingError::*;
|
use ResourceEditingError::*;
|
||||||
let resource = ResourceComplete::from_resource(resource);
|
let printed = serde_json::to_string_pretty(resource).map_err(PrettyPrintFailed)?;
|
||||||
let printed = serde_json::to_string_pretty(&resource).map_err(PrettyPrintFailed)?;
|
|
||||||
let edited = spawn_editor(printed)?;
|
let edited = spawn_editor(printed)?;
|
||||||
let parsed: ResourceComplete = serde_json::from_str(edited.as_str()).map_err(ParseFailed)?;
|
serde_json::from_str(edited.as_str()).map_err(ParseFailed)
|
||||||
Ok(parsed.into_resource())
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,10 +57,9 @@ fn main() {
|
||||||
Command::Editor {
|
Command::Editor {
|
||||||
save,
|
save,
|
||||||
resource,
|
resource,
|
||||||
create,
|
|
||||||
server_reload,
|
server_reload,
|
||||||
} => {
|
} => {
|
||||||
editor(data_paths.database_path, save, resource, create);
|
editor(data_paths.database_path, save, resource);
|
||||||
|
|
||||||
reload(data_paths.pid_file_path, server_reload);
|
reload(data_paths.pid_file_path, server_reload);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
pub mod lookup_handler;
|
pub mod lookup_handler;
|
||||||
pub mod resource;
|
pub mod resource;
|
||||||
pub mod resource_complete_serialisation;
|
|
||||||
pub mod resource_list;
|
pub mod resource_list;
|
||||||
|
|
|
@ -23,15 +23,6 @@ impl Display for Resource {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Resource {
|
impl Resource {
|
||||||
pub fn new(subject: String) -> Self {
|
|
||||||
Self {
|
|
||||||
subject,
|
|
||||||
aliases: None,
|
|
||||||
properties: None,
|
|
||||||
links: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the aliases of the given record. If the `aliases` field is
|
/// Returns the aliases of the given record. If the `aliases` field is
|
||||||
/// entirely missing, returns &[].
|
/// entirely missing, returns &[].
|
||||||
pub fn keys(&self) -> impl Iterator<Item = &String> {
|
pub fn keys(&self) -> impl Iterator<Item = &String> {
|
||||||
|
|
|
@ -1,112 +0,0 @@
|
||||||
use crate::schema::resource::{Link, Resource};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// Same as `Resource`, but doesn't have any of the skip_serializing_if fields and options set so the structure is more friendly to editing through the `editor` command
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct ResourceComplete {
|
|
||||||
subject: String,
|
|
||||||
aliases: Vec<String>,
|
|
||||||
properties: HashMap<String, Option<String>>,
|
|
||||||
links: Vec<LinkComplete>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResourceComplete {
|
|
||||||
pub fn from_resource(value: Resource) -> Self {
|
|
||||||
let Resource {
|
|
||||||
subject,
|
|
||||||
aliases,
|
|
||||||
properties,
|
|
||||||
links,
|
|
||||||
} = value;
|
|
||||||
Self {
|
|
||||||
subject,
|
|
||||||
aliases: aliases.unwrap_or_default(),
|
|
||||||
properties: properties.unwrap_or_default(),
|
|
||||||
links: links
|
|
||||||
.map(|vec| vec.into_iter().map(LinkComplete::from_link).collect())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn into_resource(self) -> Resource {
|
|
||||||
let ResourceComplete {
|
|
||||||
subject,
|
|
||||||
aliases,
|
|
||||||
properties,
|
|
||||||
links,
|
|
||||||
} = self;
|
|
||||||
Resource {
|
|
||||||
subject,
|
|
||||||
aliases: if aliases.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(aliases)
|
|
||||||
},
|
|
||||||
properties: if properties.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(properties)
|
|
||||||
},
|
|
||||||
links: if links.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(links.into_iter().map(LinkComplete::into_link).collect())
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Same as `Link`, but doesn't have any of the skip_serializing_if fields set so the structure is more friendly to editing through the `editor` command
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct LinkComplete {
|
|
||||||
rel: String,
|
|
||||||
media_type: Option<String>,
|
|
||||||
href: Option<String>,
|
|
||||||
titles: HashMap<String, String>,
|
|
||||||
properties: HashMap<String, Option<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LinkComplete {
|
|
||||||
fn from_link(value: Link) -> Self {
|
|
||||||
let Link {
|
|
||||||
rel,
|
|
||||||
media_type,
|
|
||||||
href,
|
|
||||||
titles,
|
|
||||||
properties,
|
|
||||||
} = value;
|
|
||||||
Self {
|
|
||||||
rel,
|
|
||||||
media_type,
|
|
||||||
href,
|
|
||||||
titles: titles.unwrap_or_default(),
|
|
||||||
properties: properties.unwrap_or_default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_link(self) -> Link {
|
|
||||||
let LinkComplete {
|
|
||||||
rel,
|
|
||||||
media_type,
|
|
||||||
href,
|
|
||||||
titles,
|
|
||||||
properties,
|
|
||||||
} = self;
|
|
||||||
Link {
|
|
||||||
rel,
|
|
||||||
media_type,
|
|
||||||
href,
|
|
||||||
titles: if titles.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(titles)
|
|
||||||
},
|
|
||||||
properties: if properties.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(properties)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -50,8 +50,7 @@ impl ResourceList {
|
||||||
pub fn save(&self, path: impl AsRef<Path> + Debug) -> Result<(), ResourceSaveError> {
|
pub fn save(&self, path: impl AsRef<Path> + Debug) -> Result<(), ResourceSaveError> {
|
||||||
info!("Creating backup before writing...");
|
info!("Creating backup before writing...");
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
std::fs::rename(path, path.with_extension("bak"))
|
std::fs::copy(path, path.with_extension("bak")).map_err(ResourceSaveError::BackupFailed)?;
|
||||||
.map_err(ResourceSaveError::BackupFailed)?;
|
|
||||||
info!("Writing data to {path:?}...");
|
info!("Writing data to {path:?}...");
|
||||||
let file = std::fs::File::create(path).map_err(ResourceSaveError::FileOpen)?;
|
let file = std::fs::File::create(path).map_err(ResourceSaveError::FileOpen)?;
|
||||||
self.save_to_writer(file)
|
self.save_to_writer(file)
|
||||||
|
|
|
@ -81,11 +81,7 @@ async fn run_webfinger_query(
|
||||||
info!("Received query with {uri}");
|
info!("Received query with {uri}");
|
||||||
let query = uri.query().ok_or(StatusCode::BAD_REQUEST)?;
|
let query = uri.query().ok_or(StatusCode::BAD_REQUEST)?;
|
||||||
debug!("Query string is {query}");
|
debug!("Query string is {query}");
|
||||||
let query = urlencoding::decode(query).map_err(|e| {
|
let query = urlencoding::decode(query).map_err(|e| StatusCode::BAD_REQUEST)?;
|
||||||
debug!("Failed to decode query string with error {e}");
|
|
||||||
StatusCode::BAD_REQUEST
|
|
||||||
})?;
|
|
||||||
debug!("Decoded query string is {query}");
|
|
||||||
let params = query
|
let params = query
|
||||||
.split('&')
|
.split('&')
|
||||||
.filter_map(|query_part| {
|
.filter_map(|query_part| {
|
||||||
|
@ -111,7 +107,7 @@ async fn run_webfinger_query(
|
||||||
let resource = datastore
|
let resource = datastore
|
||||||
.read()
|
.read()
|
||||||
.await
|
.await
|
||||||
.lookup(resource_query.to_lowercase().as_str())
|
.lookup(resource_query)
|
||||||
.ok_or(StatusCode::NOT_FOUND)?
|
.ok_or(StatusCode::NOT_FOUND)?
|
||||||
.clone();
|
.clone();
|
||||||
debug!("Found resource: {resource:?}");
|
debug!("Found resource: {resource:?}");
|
||||||
|
|
Loading…
Reference in a new issue