diff --git a/.gitignore b/.gitignore index 37fd804..997e1d9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,4 @@ config.json .vs/ obj/ -bin/ *.user \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 76f74d5..40fe2a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,15 @@ description = "Export your FFXIV character in portable, generic formats" [lib] crate-type = ["cdylib", "rlib"] + +[[bin]] +name = "cli" +required-features = ["cli"] + +[[bin]] +name = "ui" +required-features = ["ui"] + [features] # Builds the Qt UI for Auracite ui = []# ["cxx-qt-build", "cxx-qt", "cxx-qt-lib", "cxx-kde-frameworks"] diff --git a/README.md b/README.md index e2819e2..3401afa 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ you can display in your browser. ## Usage +### CLI + Provide a character name via `--name`: ```shell diff --git a/index.html b/index.html new file mode 100644 index 0000000..6e14ac2 --- /dev/null +++ b/index.html @@ -0,0 +1,58 @@ + + + + + Auracite + + + +

Auracite

+

This tool allows you to export your FFXIV character into portable, generic formats.

+
+

Auracite uses the Lodestone for collecting data about your character. We use a proxy and that's unfortunately necessary, due to CORS compliance and Lodestone doing annoying browser compatibility checks we need to inject cookies for.

+ +

The entire process happens locally, and does not give me any personally identifiable data about you. We only collect what is already publicly available on the Lodestone.

+ + How does this work? + +
+
+

Here is the list of requests we make to the Lodestone:

+

It makes one request to search for your character by it's name.

+

It makes one request to request your character's main page. The other pages are not yet considered.

+

It makes two requests to download images. This is currently your avatar and the full body portrait images.

+ + + What kind of requests does Auracite make? + +
+
+

Auracite can only collect as much data about your character as they make publicly available on the Lodestone.

+

To work around this, I created a Dalamud plugin to collect even more information. It's available in my personal Dalamud plugin repository.

+

The plugin needs to start a local HTTP server in order to communicate with Auracite. To prevent this from running all the time, you must type /auracite begin before clicking the "Download" button. Once the process is complete, the server is shutdown automatically. It's always safe to disable the plugin when you're not using Auracite.

+

The website connects to your game client locally, and it does not use my server to proxy any data. No data leaves your device.

+ + What is the "Connect to Dalamud Plugin" option? + +
+ + + + + +Source Code + + diff --git a/src/bin/cli.rs b/src/bin/cli.rs new file mode 100644 index 0000000..6f1e9a9 --- /dev/null +++ b/src/bin/cli.rs @@ -0,0 +1,33 @@ +use auracite::downloader::download; +use auracite::html::write_html; +use auracite::parser::{parse_lodestone, parse_search}; +use clap::Parser; +use serde::Deserialize; +use std::convert::Infallible; +use std::fs::{read, write}; +use std::path::Path; +use std::sync::{Arc, Mutex}; +use touche::server::Service; +use touche::{Body, HttpBody, Request, Response, Server, StatusCode}; +use auracite::archive_character; + +const LODESTONE_HOST: &str = "https://na.finalfantasyxiv.com"; + +#[derive(Parser, Debug)] +#[command(version, about)] +struct Args { + #[arg(short, long, help = "The character's name.")] + name: String, + + #[arg(short, long, help = "Whether to import more data from the Auracite Dalamud plugin.")] + dalamud: bool, +} + +#[tokio::main] +async fn main() { + let args = Args::parse(); + + println!("Downloading character data for {}...", args.name); + + archive_character(&args.name, args.dalamud).await; +} diff --git a/src/bin/ui/Main.qml b/src/bin/ui/Main.qml new file mode 100644 index 0000000..5f802a0 --- /dev/null +++ b/src/bin/ui/Main.qml @@ -0,0 +1,25 @@ +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import zone.xiv.auracite + +Kirigami.ApplicationWindow { + id: root + + title: "Auracite" + + readonly property Backend backend: Backend {} + + ColumnLayout { + QQC2.TextField { + id: characterNameField + + placeholderText: "Full name of the character" + } + + QQC2.Button { + text: "Archive" + onClicked: root.backend.archiveCharacter(characterNameField.text, false) + } + } +} \ No newline at end of file diff --git a/src/bin/ui/bridge.rs b/src/bin/ui/bridge.rs new file mode 100644 index 0000000..852312d --- /dev/null +++ b/src/bin/ui/bridge.rs @@ -0,0 +1,39 @@ +#[cxx_qt::bridge] +pub mod bridge { + unsafe extern "C++" { + include!("cxx-qt-lib/qstring.h"); + type QString = cxx_qt_lib::QString; + } + + unsafe extern "RustQt" { + #[qobject] + #[qml_element] + type Backend = super::BackendRust; + } + + unsafe extern "RustQt" { + #[qinvokable] + #[cxx_name = "archiveCharacter"] + fn archive_character(self: &Backend, character_name: &QString, use_dalamud: bool); + } +} + +use std::fs::write; +use cxx_qt_lib::QString; +use auracite::archive_character; + +#[derive(Default)] +pub struct BackendRust { +} + +impl bridge::Backend { + pub fn archive_character(&self, character_name: &QString, use_dalamud: bool) { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + let inner = rt.block_on(archive_character(&character_name.to_string(), use_dalamud)); + write("/home/josh/test.zip", inner); + } +} \ No newline at end of file diff --git a/src/bin/ui/main.rs b/src/bin/ui/main.rs new file mode 100644 index 0000000..00cd25b --- /dev/null +++ b/src/bin/ui/main.rs @@ -0,0 +1,24 @@ +use cxx_kde_frameworks::ki18n::{KLocalizedContext, KLocalizedString}; +use cxx_qt_lib::{QByteArray, QGuiApplication, QQmlApplicationEngine, QQuickStyle, QString, QUrl}; + +#[cfg(feature = "ui")] +pub mod bridge; + +fn main() { + QQuickStyle::set_style(&QString::from("org.kde.desktop")); + + let mut app = QGuiApplication::new(); + let mut engine = QQmlApplicationEngine::new(); + + KLocalizedString::set_application_domain(&QByteArray::from("mgedit")); + + if let Some(mut engine) = engine.as_mut() { + KLocalizedContext::initialize_engine(engine.as_mut().as_qqmlengine()); + // TODO: replace with loadModule (requires cxx-qt changes) + engine.load(&QUrl::from("qrc:/qt/qml/zone/xiv/auracite/src/bin/ui/Main.qml")); + } + + if let Some(app) = app.as_mut() { + app.exec(); + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 360d4e2..0000000 --- a/src/main.rs +++ /dev/null @@ -1,72 +0,0 @@ -use auracite::downloader::download; -use auracite::html::write_html; -use auracite::parser::{parse_lodestone, parse_search}; -use clap::Parser; -use serde::Deserialize; -use std::convert::Infallible; -use std::fs::{read, write}; -use std::path::Path; -use std::sync::{Arc, Mutex}; -use touche::server::Service; -use touche::{Body, HttpBody, Request, Response, Server, StatusCode}; -use auracite::archive_character; - -const LODESTONE_HOST: &str = "https://na.finalfantasyxiv.com"; - -#[derive(Parser, Debug)] -#[command(version, about)] -struct Args { - #[arg(short, long, help = "The character's name.")] - name: String, - - #[arg(short, long, help = "Whether to import more data from the Auracite Dalamud plugin.")] - dalamud: bool, -} - -#[derive(Default, Deserialize, Clone)] -struct Package { - playtime: String, - height: i32, - bust_size: i32, - gil: u32, - is_battle_mentor: bool, - is_trade_mentor: bool, - is_novice: bool, - is_returner: bool, - player_commendations: i32, -} - -#[derive(Clone)] -struct PackageService<'a> { - wants_stop: Arc>, // TODO: THIS IS TERRIBLE STOP STOP STOP - package: &'a Arc>, -} - -impl Service for PackageService<'_> { - type Body = &'static str; - type Error = Infallible; - - fn call(&self, req: Request) -> Result, Self::Error> { - *self.package.lock().unwrap() = serde_json::from_str(&String::from_utf8(req.into_body().into_bytes().unwrap()).unwrap()).unwrap(); - - *self.wants_stop.lock().unwrap() = true; - - Ok(Response::builder() - .status(StatusCode::OK) - .body("") - .unwrap()) - } - - // TODO: NO NO NO NO - fn wants_stop(&self) -> bool { - *self.wants_stop.lock().unwrap() - } -} - -fn main() { - let args = Args::parse(); - - println!("Downloading character data for {}...", args.name); - - archive_character(&args.name, args.dalamud); -}