init
This commit is contained in:
commit
03a529fc7b
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
.idea/
|
3580
Cargo.lock
generated
Normal file
3580
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
Cargo.toml
Normal file
28
Cargo.toml
Normal file
@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "nap"
|
||||
description = "Nostr App Publisher"
|
||||
license = "MIT"
|
||||
repository = "https://git.v0l.io/Kieran/nap"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "nap"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.95"
|
||||
clap = { version = "4.5.28", features = ["derive"] }
|
||||
config = { version = "0.15.7", features = ["yaml"] }
|
||||
log = "0.4.25"
|
||||
nostr-sdk = "0.39.0"
|
||||
reqwest = { version = "0.12.12", features = ["json"] }
|
||||
tokio = { version = "1.43.0", features = ["fs", "rt", "macros", "rt-multi-thread"] }
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
async-trait = "0.1.86"
|
||||
apk = "0.4.0"
|
||||
zip = "2.2.2"
|
||||
semver = "1.0.25"
|
||||
indicatif = "0.17.11"
|
||||
dialoguer = "0.11.0"
|
||||
env_logger = "0.11.6"
|
26
nap.yaml
Normal file
26
nap.yaml
Normal file
@ -0,0 +1,26 @@
|
||||
# Unique app id
|
||||
id: "io.nostrlabs.freeflow"
|
||||
|
||||
# Display name of the app
|
||||
name: "Freeflow"
|
||||
|
||||
# Human-readable long description
|
||||
description: "Live in the moment"
|
||||
|
||||
# Application icon
|
||||
icon: "https://freeflow.app/icon.png"
|
||||
|
||||
# Banner / Preview of the app
|
||||
images:
|
||||
- "https://freeflow.app/banner.jpg"
|
||||
|
||||
# Public code repo or project website
|
||||
repository: "https://github.com/nostrlabs-io/freeflow"
|
||||
|
||||
# SPDX code license
|
||||
license: "MIT"
|
||||
|
||||
# Descriptive app tags
|
||||
tags:
|
||||
- "tiktok"
|
||||
- "shorts"
|
59
src/main.rs
Normal file
59
src/main.rs
Normal file
@ -0,0 +1,59 @@
|
||||
mod manifest;
|
||||
mod repo;
|
||||
|
||||
use crate::manifest::Manifest;
|
||||
use crate::repo::Repo;
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::Parser;
|
||||
use config::{Config, File, FileSourceFile};
|
||||
use log::info;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
#[command(version, about)]
|
||||
struct Args {
|
||||
/// User specified config path
|
||||
#[arg(long, short)]
|
||||
pub config: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
// Set default log level to info
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
}
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
let manifest: Manifest = Config::builder()
|
||||
.add_source(File::from(args.config.unwrap_or(PathBuf::from("nap.yaml"))))
|
||||
.build()
|
||||
.map_err(|e| anyhow!("Failed to load config: {}", e))?
|
||||
.try_deserialize()?;
|
||||
|
||||
let repo: Box<dyn Repo> = (&manifest).try_into()?;
|
||||
|
||||
let releases = repo.get_releases().await?;
|
||||
|
||||
info!("Found {} release(s)", releases.len());
|
||||
|
||||
if let Some(release) = releases.first() {
|
||||
info!("Starting publish of release {}", release.version);
|
||||
info!("Artifacts: ");
|
||||
for a in &release.artifacts {
|
||||
info!(" - {}", a.name);
|
||||
}
|
||||
if !dialoguer::Confirm::new()
|
||||
.default(false)
|
||||
.with_prompt(format!("Publish v{}?", release.version))
|
||||
.interact()?
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
52
src/manifest.rs
Normal file
52
src/manifest.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Manifest {
|
||||
/// App ID, must be unique
|
||||
pub id: String,
|
||||
|
||||
/// Application display name
|
||||
pub name: String,
|
||||
|
||||
/// Long app description / release notes
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Repo URL
|
||||
pub repository: Option<String>,
|
||||
|
||||
/// App icon
|
||||
pub icon: Option<String>,
|
||||
|
||||
/// App preview images
|
||||
pub images: Vec<String>,
|
||||
|
||||
/// Tags (category / purpose)
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub enum Platform {
|
||||
Android {
|
||||
arch: Architecture
|
||||
},
|
||||
IOS,
|
||||
MacOS {
|
||||
arch: Architecture
|
||||
},
|
||||
Windows {
|
||||
arch: Architecture
|
||||
},
|
||||
Linux {
|
||||
arch: Architecture
|
||||
},
|
||||
Web
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub enum Architecture {
|
||||
ARMv7,
|
||||
ARMv8,
|
||||
X86,
|
||||
AMD64,
|
||||
ARM64
|
||||
}
|
128
src/repo/github.rs
Normal file
128
src/repo/github.rs
Normal file
@ -0,0 +1,128 @@
|
||||
use crate::repo::{Repo, RepoArtifact, RepoRelease, RepoResource};
|
||||
use anyhow::{anyhow, Result};
|
||||
use log::{info, warn};
|
||||
use nostr_sdk::Url;
|
||||
use reqwest::header::{HeaderMap, ACCEPT, USER_AGENT};
|
||||
use reqwest::Client;
|
||||
use semver::Version;
|
||||
use serde::Deserialize;
|
||||
|
||||
pub struct GithubRepo {
|
||||
client: Client,
|
||||
owner: String,
|
||||
repo: String,
|
||||
}
|
||||
|
||||
impl GithubRepo {
|
||||
pub fn new(owner: String, repo: String) -> GithubRepo {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(ACCEPT, "application/vnd.github+json".parse().unwrap());
|
||||
headers.insert(
|
||||
USER_AGENT,
|
||||
"nap/1.0 (https://github.com/v0l/nap)".parse().unwrap(),
|
||||
);
|
||||
|
||||
let client = Client::builder().default_headers(headers).build().unwrap();
|
||||
|
||||
GithubRepo {
|
||||
owner,
|
||||
repo,
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_url(url: &str) -> Result<GithubRepo> {
|
||||
let u: Url = url.parse()?;
|
||||
let mut segs = u.path_segments().ok_or(anyhow::anyhow!("Invalid URL"))?;
|
||||
Ok(GithubRepo::new(
|
||||
segs.next().ok_or(anyhow!("Invalid URL"))?.to_string(),
|
||||
segs.next().ok_or(anyhow!("Invalid URL"))?.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GithubRelease {
|
||||
pub tag_name: String,
|
||||
pub name: String,
|
||||
pub draft: bool,
|
||||
#[serde(rename = "prerelease")]
|
||||
pub pre_release: bool,
|
||||
pub body: String,
|
||||
pub assets: Vec<GithubReleaseArtifact>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GithubReleaseArtifact {
|
||||
pub name: String,
|
||||
pub size: u64,
|
||||
pub content_type: String,
|
||||
pub browser_download_url: String,
|
||||
}
|
||||
|
||||
impl TryFrom<&GithubRelease> for RepoRelease {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &GithubRelease) -> std::result::Result<Self, Self::Error> {
|
||||
Ok(RepoRelease {
|
||||
version: Version::parse(if value.tag_name.starts_with("v") {
|
||||
&value.tag_name[1..]
|
||||
} else {
|
||||
&value.tag_name
|
||||
})?,
|
||||
artifacts: value
|
||||
.assets
|
||||
.iter()
|
||||
.filter_map(|v| match RepoArtifact::try_from(v) {
|
||||
Ok(art) => Some(art),
|
||||
Err(e) => {
|
||||
warn!("Failed to parse artifact {}: {}", &v.name, e);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&GithubReleaseArtifact> for RepoArtifact {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &GithubReleaseArtifact) -> std::result::Result<Self, Self::Error> {
|
||||
Ok(RepoArtifact {
|
||||
name: value.name.clone(),
|
||||
size: value.size,
|
||||
content_type: value.content_type.clone(),
|
||||
location: RepoResource::Remote(value.browser_download_url.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Repo for GithubRepo {
|
||||
async fn get_releases(&self) -> Result<Vec<RepoRelease>> {
|
||||
info!(
|
||||
"Fetching release from: github.com/{}/{}",
|
||||
self.owner, self.repo
|
||||
);
|
||||
let req = self
|
||||
.client
|
||||
.get(format!(
|
||||
"https://api.github.com/repos/{}/{}/releases",
|
||||
self.owner, self.repo
|
||||
))
|
||||
.build()?;
|
||||
|
||||
let rsp: Vec<GithubRelease> = self.client.execute(req).await?.json().await?;
|
||||
Ok(rsp
|
||||
.into_iter()
|
||||
.filter_map(|v| match RepoRelease::try_from(&v) {
|
||||
Ok(r) => Some(r),
|
||||
Err(e) => {
|
||||
warn!("Failed to parse release: {} {}", v.tag_name, e);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
51
src/repo/mod.rs
Normal file
51
src/repo/mod.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use std::path::PathBuf;
|
||||
use crate::manifest::Manifest;
|
||||
use crate::repo::github::GithubRepo;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use semver::Version;
|
||||
|
||||
mod github;
|
||||
|
||||
/// Since artifact binary / image
|
||||
pub struct RepoArtifact {
|
||||
pub name: String,
|
||||
pub size: u64,
|
||||
pub location: RepoResource,
|
||||
pub content_type: String,
|
||||
}
|
||||
|
||||
/// A local/remote location where the artifact is located
|
||||
pub enum RepoResource {
|
||||
Remote(String),
|
||||
Local(PathBuf),
|
||||
}
|
||||
|
||||
/// A single release with one or more artifacts
|
||||
pub struct RepoRelease {
|
||||
pub version: Version,
|
||||
pub artifacts: Vec<RepoArtifact>,
|
||||
}
|
||||
|
||||
/// Generic artifact repository
|
||||
#[async_trait::async_trait]
|
||||
pub trait Repo {
|
||||
/// Get a list of release artifacts
|
||||
async fn get_releases(&self) -> Result<Vec<RepoRelease>>;
|
||||
}
|
||||
|
||||
impl TryInto<Box<dyn Repo>> for &Manifest {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_into(self) -> std::result::Result<Box<dyn Repo>, Self::Error> {
|
||||
let repo = self
|
||||
.repository
|
||||
.as_ref()
|
||||
.ok_or(anyhow!("repository not found"))?;
|
||||
|
||||
if !repo.starts_with("https://github.com/") {
|
||||
bail!("Only github repos are supported");
|
||||
}
|
||||
|
||||
Ok(Box::new(GithubRepo::from_url(repo)?))
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user