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