add a new app! kino_updates, serves a widget

This commit is contained in:
dr-frmr 2024-05-06 17:22:08 -06:00
parent 8ad86b0183
commit 78aab0004a
No known key found for this signature in database
7 changed files with 232 additions and 0 deletions

13
Cargo.lock generated
View File

@ -6528,6 +6528,19 @@ version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]]
name = "widget"
version = "0.1.0"
dependencies = [
"anyhow",
"bincode",
"kinode_process_lib 0.7.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.7.0)",
"serde",
"serde_json",
"url",
"wit-bindgen",
]
[[package]]
name = "wiggle"
version = "19.0.2"

View File

@ -18,6 +18,7 @@ members = [
"kinode/packages/app_store/download", "kinode/packages/app_store/install", "kinode/packages/app_store/uninstall",
"kinode/packages/chess/chess",
"kinode/packages/homepage/homepage",
"kinode/packages/kino_updates/widget",
"kinode/packages/kns_indexer/kns_indexer", "kinode/packages/kns_indexer/get_block", "kinode/packages/kns_indexer/state",
"kinode/packages/settings/settings",
"kinode/packages/terminal/terminal",

View File

@ -0,0 +1,16 @@
{
"name": "kino_updates",
"description": "Updates from Kinode DAO",
"image": "https://kinode.org/static/media/kinode.a60d538cb0ae88c56b07.png",
"properties": {
"package_name": "kino_updates",
"current_version": "0.1.0",
"publisher": "sys",
"mirrors": [],
"code_hashes": {
"0.1.0": ""
}
},
"external_url": "https://kinode.org",
"animation_url": ""
}

View File

@ -0,0 +1,18 @@
[
{
"process_name": "widget",
"process_wasm_path": "/widget.wasm",
"on_exit": "Restart",
"request_networking": false,
"request_capabilities": [
"homepage:homepage:sys",
"http_client:distro:sys",
"http_server:distro:sys"
],
"grant_capabilities": [
"http_client:distro:sys",
"timer:distro:sys"
],
"public": false
}
]

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,19 @@
[package]
name = "widget"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0"
bincode = "1.3.3"
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", tag = "v0.7.0" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
url = "2.5.0"
wit-bindgen = "0.24.0"
[lib]
crate-type = ["cdylib"]
[package.metadata.component]
package = "kinode:process"

View File

@ -0,0 +1,164 @@
use kinode_process_lib::{call_init, http, timer, Address, Request};
use serde::{Deserialize, Serialize};
wit_bindgen::generate!({
path: "wit",
world: "process",
});
/// Fetching OS version from main package.. LMK if there's a better way...
const CARGO_TOML: &str = include_str!("../../../../Cargo.toml");
/// A static message to display on the homepage.
const MOTD: &str = "Welcome to Kinode!";
/// 20 minutes
const REFRESH_INTERVAL: u64 = 20 * 60 * 1000;
#[derive(Serialize, Deserialize)]
struct KinodeBlogPost {
slug: String,
content: String,
title: String,
date: u64,
#[serde(rename = "thumbnailImage")]
thumbnail_image: String,
#[serde(flatten)]
extra: std::collections::HashMap<String, serde_json::Value>,
}
call_init!(init);
fn init(_our: Address) {
// updates: given a static message, the current version of the system,
// and the kinode.org website, produce a widget for our homepage which
// presents this information in a visually appealing way.
loop {
// add ourselves to the homepage
Request::to(("our", "homepage", "homepage", "sys"))
.body(
serde_json::json!({
"Add": {
"label": "KinoUpdates",
"widget": create_widget(fetch_three_most_recent_blog_posts()),
}
})
.to_string(),
)
.send()
.unwrap();
timer::set_and_await_timer(REFRESH_INTERVAL).expect("Failed to produce a timer!");
}
}
fn create_widget(posts: Vec<KinodeBlogPost>) -> String {
return format!(
r#"<html>
<head>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.post {{
width: 100%;
}}
.post-image {{
background-size: cover;
background-repeat: no-repeat;
background-position: center;
width: 100px;
height: 100px;
border-radius: 16px;
}}
.post-info {{
max-width: 67%
}}
@media screen and (min-width: 500px) {{
.post {{
width: 49%;
}}
}}
</style>
</head>
<body class="text-white overflow-hidden">
<p>Kinode {}: {}</p>
<p>Recent posts from kinode.org:</p>
<div
id="latest-blog-posts"
class="flex flex-col p-2 gap-2 backdrop-brightness-125 rounded-xl shadow-lg h-screen w-screen overflow-y-auto"
style="
scrollbar-color: transparent transparent;
scrollbar-width: none;
"
>
{}
</div>
</body>
</html>"#,
version_from_cargo_toml(),
MOTD,
posts
.into_iter()
.map(post_to_html_string)
.collect::<String>()
);
}
fn version_from_cargo_toml() -> String {
let version = CARGO_TOML
.lines()
.find(|line| line.starts_with("version = "))
.expect("Failed to find version in Cargo.toml");
version
.split('=')
.last()
.expect("Failed to parse version from Cargo.toml")
.trim()
.trim_matches('"')
.to_string()
}
fn fetch_three_most_recent_blog_posts() -> Vec<KinodeBlogPost> {
let blog_posts = match http::send_request_await_response(
http::Method::GET,
url::Url::parse("https://kinode.org/api/blog/posts").unwrap(),
None,
60,
vec![],
) {
Ok(response) => serde_json::from_slice::<Vec<KinodeBlogPost>>(response.body())
.expect("Invalid UTF-8 from kinode.org"),
Err(e) => panic!("Failed to fetch blog posts: {:?}", e),
};
blog_posts.into_iter().rev().take(3).collect()
}
/// Take first 100 chars of a blog post and append "..." to the end
fn trim_content(content: &str) -> String {
if content.len() > 100 {
format!("{}...", &content[..100])
} else {
content.to_string()
}
}
fn post_to_html_string(post: KinodeBlogPost) -> String {
format!(
r#"<div class="post p-2 grow self-stretch flex items-stretch rounded-lg shadow bg-white/10 font-sans w-full">
<div
class="post-image rounded mr-2 grow"
style="background-image: url('https://kinode.org{}');"
></div>
<div class="post-info flex flex-col grow">
<h2 class="font-bold">{}</h2>
<p>{}</p>
<a href="https://kinode.org/blog/post/{}" class="text-blue-500" target="_blank" rel="noopener noreferrer">Read more</a>
</div>
</div>"#,
post.thumbnail_image,
post.title,
trim_content(&post.content),
post.slug,
)
}