Added first version.
This commit is contained in:
parent
af9344f0d2
commit
7870100cbe
4 changed files with 1573 additions and 0 deletions
1099
Cargo.lock
generated
Normal file
1099
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
15
Cargo.toml
Normal file
15
Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "mk-to-atom"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
atom_syndication = { version = "0.11", features = ["with-serde"] }
|
||||||
|
hyper = { version = "0.14", features = ["server", "client", "http1", "tcp"] }
|
||||||
|
hyper-tls = "0.5"
|
||||||
|
tokio = { version = "1.0", features = ["rt", "macros", "rt-multi-thread"] }
|
||||||
|
chrono = { version = "0.4.24", features = ["std"] }
|
209
src/#main.rs#
Normal file
209
src/#main.rs#
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
extern crate hyper;
|
||||||
|
|
||||||
|
use core::fmt;
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use atom_syndication::{Feed, Entry, FeedBuilder, LinkBuilder, TextBuilder, EntryBuilder, PersonBuilder, ContentBuilder};
|
||||||
|
use hyper::client::HttpConnector;
|
||||||
|
use hyper::{Body, Method, Request, Response, StatusCode, Server, Client};
|
||||||
|
use hyper::service::{service_fn, make_service_fn};
|
||||||
|
use hyper_tls::HttpsConnector;
|
||||||
|
use serde_json::{json, Value, Map};
|
||||||
|
use chrono::{DateTime, FixedOffset};
|
||||||
|
|
||||||
|
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct FediToFeedError{
|
||||||
|
error_string: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FediToFeedError {
|
||||||
|
fn new(error_string: String) -> Self {
|
||||||
|
Self{error_string}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for FediToFeedError {}
|
||||||
|
|
||||||
|
impl fmt::Display for FediToFeedError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Got the following error while trying to get the Feed: {}",
|
||||||
|
self.error_string
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stringify_value(val: Value) -> String {
|
||||||
|
val.as_str().get_or_insert("").to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn stringtrunkify_value(val: Value, new_len: usize) -> String {
|
||||||
|
let mut long_string = stringify_value(val);
|
||||||
|
long_string.truncate(new_len);
|
||||||
|
long_string
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MkFeedGenerator{
|
||||||
|
client: Client<HttpsConnector<HttpConnector>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MkFeedGenerator {
|
||||||
|
async fn get_mk_data(&self, domain: &str, token: &str) -> Result<Value> {
|
||||||
|
let uri = String::from("https://") + domain + "/api/notes/timeline";
|
||||||
|
let req_json = json!({
|
||||||
|
"i" : token,
|
||||||
|
"limit": 10 as usize,
|
||||||
|
"sinceDate": 0 as usize,
|
||||||
|
"untilDate": 0 as usize,
|
||||||
|
"includeMyRenotes": false,
|
||||||
|
"includeRenotedMyNotes": false,
|
||||||
|
"includeLocalRenotes": true,
|
||||||
|
"withFiles": false
|
||||||
|
});
|
||||||
|
|
||||||
|
let req = Request::builder()
|
||||||
|
.method(Method::POST)
|
||||||
|
.uri(uri.clone())
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(Body::from(req_json.to_string()))?;
|
||||||
|
|
||||||
|
let resp = self.client.request(req).await?;
|
||||||
|
|
||||||
|
if resp.status() == StatusCode::OK {
|
||||||
|
return Ok(serde_json::from_slice(
|
||||||
|
&hyper::body::to_bytes(resp.into_body()).await?
|
||||||
|
)?)
|
||||||
|
} else {
|
||||||
|
return Err(Box::new(FediToFeedError::new(format!("Reqiest to {} failed", uri))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
let https = HttpsConnector::new();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
client: Client::builder().build::<_, hyper::Body>(https)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_feed(&self, domain: &str, token: &str) -> Result<Feed> {
|
||||||
|
let mut builder = FeedBuilder::default();
|
||||||
|
|
||||||
|
builder.id(String::from("https://") + domain)
|
||||||
|
.title(String::from(domain) + " Atom feed")
|
||||||
|
.link(LinkBuilder::default()
|
||||||
|
.href(String::from("https://") + domain)
|
||||||
|
.rel("alternate")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.subtitle(TextBuilder::default()
|
||||||
|
.value("Fedi2Feed -- Bringing the fediverse into your feed reader!")
|
||||||
|
.lang(Some(String::from("en")))
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
let data = self.get_mk_data(domain, token).await?;
|
||||||
|
|
||||||
|
let mut entries: Vec<Entry> = vec![];
|
||||||
|
if let Value::Array(notes) = data { for note_val in notes { if let Value::Object(note) = note_val {
|
||||||
|
let mut e_builder = EntryBuilder::default();
|
||||||
|
|
||||||
|
e_builder.id(stringify_value(note["id"].clone()))
|
||||||
|
.title(TextBuilder::default()
|
||||||
|
.value(stringtrunkify_value(note["text"].clone(), 20))
|
||||||
|
.lang(Some(String::from("en")))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.author(PersonBuilder::default()
|
||||||
|
.name(stringify_value(note["user"].as_object().get_or_insert(&Map::new())["name"].clone()))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.updated(DateTime::<FixedOffset>::parse_from_rfc3339(&stringify_value(note["createdAt"].clone()))?)
|
||||||
|
.content(ContentBuilder::default()
|
||||||
|
.base(domain.to_owned())
|
||||||
|
.value(stringify_value(note["text"].clone()))
|
||||||
|
.src(stringify_value(note["uri"].clone()))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.link(LinkBuilder::default()
|
||||||
|
.href(stringify_value(note["uri"].clone()))
|
||||||
|
.rel("alternate")
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Value::Array(files) = note["files"].clone() { for file_val in files { if let Value::Object(file) = file_val {
|
||||||
|
e_builder.link(LinkBuilder::default()
|
||||||
|
.href(stringify_value(file["url"].clone()))
|
||||||
|
.rel("enclosure")
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}}}
|
||||||
|
|
||||||
|
entries.push(e_builder.build());
|
||||||
|
} else {
|
||||||
|
return Err(Box::new(FediToFeedError::new(String::from("Note does not have the expected structure!"))))
|
||||||
|
}};
|
||||||
|
} else {
|
||||||
|
return Err(Box::new(FediToFeedError::new(String::from("Could not get a list of posts from API response!"))))
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.entries(entries);
|
||||||
|
Ok(builder.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_feed(res: Response<Body>, domain: &str, token: &str) -> Result<Response<Body>> {
|
||||||
|
let generator = MkFeedGenerator::new();
|
||||||
|
|
||||||
|
let feed = generator.get_feed(domain, token).await?;
|
||||||
|
|
||||||
|
let mut res = res;
|
||||||
|
*res.body_mut() = Body::from(feed.to_string());
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn echo(req: Request<Body>) -> Result<Response<Body>> {
|
||||||
|
let mut response = Response::new(Body::empty());
|
||||||
|
|
||||||
|
match (req.method(), req.uri().path()) {
|
||||||
|
(&Method::GET, path) => {
|
||||||
|
let split_path = path.split('/').collect::<Vec<&str>>();
|
||||||
|
|
||||||
|
// The first item in split_path is always an empty string.
|
||||||
|
if split_path.len() >= 3 {
|
||||||
|
let domain = split_path[1];
|
||||||
|
let token = split_path[2];
|
||||||
|
|
||||||
|
response = generate_feed(response, domain, token).await?;
|
||||||
|
} else {
|
||||||
|
*response.body_mut() = Body::from("<h1>You need to go to `/domain/token!`</h1>");
|
||||||
|
}
|
||||||
|
*response.status_mut() = StatusCode::OK;
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
*response.status_mut() = StatusCode::NOT_FOUND;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
let addr = ([127, 0, 0, 1], 3000).into();
|
||||||
|
|
||||||
|
let service = make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(echo)) });
|
||||||
|
|
||||||
|
let server = Server::bind(&addr).serve(service);
|
||||||
|
|
||||||
|
println!("Listening on http://{}", addr);
|
||||||
|
|
||||||
|
server.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
250
src/main.rs
Normal file
250
src/main.rs
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
extern crate hyper;
|
||||||
|
|
||||||
|
use core::fmt;
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use atom_syndication::{Feed, Entry, FeedBuilder, LinkBuilder, TextBuilder, EntryBuilder, PersonBuilder, ContentBuilder};
|
||||||
|
use hyper::client::HttpConnector;
|
||||||
|
use hyper::{Body, Method, Request, Response, StatusCode, Server, Client};
|
||||||
|
use hyper::service::{service_fn, make_service_fn};
|
||||||
|
use hyper_tls::HttpsConnector;
|
||||||
|
use serde_json::{json, Value, Map};
|
||||||
|
use chrono::{DateTime, FixedOffset};
|
||||||
|
|
||||||
|
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct FediToFeedError{
|
||||||
|
error_string: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FediToFeedError {
|
||||||
|
fn new(error_string: String) -> Self {
|
||||||
|
Self{error_string}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for FediToFeedError {}
|
||||||
|
|
||||||
|
impl fmt::Display for FediToFeedError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Got the following error while trying to get the Feed: {}",
|
||||||
|
self.error_string
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stringify_value(val: Value) -> String {
|
||||||
|
val.as_str().get_or_insert("").to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stringtrunkify_value(val: Value, new_len: usize) -> String {
|
||||||
|
let mut long_string = stringify_value(val);
|
||||||
|
long_string.truncate(new_len);
|
||||||
|
long_string
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MkFeedGenerator{
|
||||||
|
client: Client<HttpsConnector<HttpConnector>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MkFeedGenerator {
|
||||||
|
async fn get_mk_data(&self, domain: &str, token: &str) -> Result<Value> {
|
||||||
|
let uri = String::from("https://") + domain + "/api/notes/timeline";
|
||||||
|
let req_json = json!({
|
||||||
|
"i" : token,
|
||||||
|
"limit": 10 as usize,
|
||||||
|
"sinceDate": 0 as usize,
|
||||||
|
"untilDate": 0 as usize,
|
||||||
|
"includeMyRenotes": false,
|
||||||
|
"includeRenotedMyNotes": false,
|
||||||
|
"includeLocalRenotes": true,
|
||||||
|
"withFiles": false
|
||||||
|
});
|
||||||
|
|
||||||
|
let req = Request::builder()
|
||||||
|
.method(Method::POST)
|
||||||
|
.uri(uri.clone())
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(Body::from(req_json.to_string()))?;
|
||||||
|
|
||||||
|
let resp = self.client.request(req).await?;
|
||||||
|
|
||||||
|
if resp.status() == StatusCode::OK {
|
||||||
|
return Ok(serde_json::from_slice(
|
||||||
|
&hyper::body::to_bytes(resp.into_body()).await?
|
||||||
|
)?)
|
||||||
|
} else {
|
||||||
|
return Err(Box::new(FediToFeedError::new(format!("Reqiest to {} failed", uri))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
let https = HttpsConnector::new();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
client: Client::builder().build::<_, hyper::Body>(https)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
def generate_feed(mk_data, domain):
|
||||||
|
feed = FeedGenerator()
|
||||||
|
feed.id("https://" + domain)
|
||||||
|
feed.title(domain + " Atom feed")
|
||||||
|
feed.link( href="https://" + domain, rel="alternate" )
|
||||||
|
feed.subtitle("Bringing the fediverse into your feed reader!")
|
||||||
|
|
||||||
|
for note in mk_data:
|
||||||
|
if not note["text"]:
|
||||||
|
note["text"] = "Misskey Post"
|
||||||
|
|
||||||
|
entry = feed.add_entry()
|
||||||
|
entry.id(note["id"])
|
||||||
|
entry.title(note["text"][:20])
|
||||||
|
entry.author({"name" : note["user"]["name"]})
|
||||||
|
entry.updated(note["createdAt"])
|
||||||
|
entry.content(note["text"])
|
||||||
|
if note["files"]:
|
||||||
|
for f in note["files"]:
|
||||||
|
entry.link(href=f["url"], rel="enclosure")
|
||||||
|
|
||||||
|
entry.link(href=note["uri"], rel="alternate")
|
||||||
|
|
||||||
|
return feed.atom_str(pretty=False)
|
||||||
|
*/
|
||||||
|
async fn get_feed(&self, domain: &str, token: &str) -> Result<Feed> {
|
||||||
|
let mut builder = FeedBuilder::default();
|
||||||
|
|
||||||
|
builder.id(String::from("https://") + domain)
|
||||||
|
.title(String::from(domain) + " Atom feed")
|
||||||
|
.link(LinkBuilder::default()
|
||||||
|
.href(String::from("https://") + domain)
|
||||||
|
.rel("alternate")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.subtitle(TextBuilder::default()
|
||||||
|
.value("Fedi2Feed -- Bringing the fediverse into your feed reader!")
|
||||||
|
.lang(Some(String::from("en")))
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
let data = self.get_mk_data(domain, token).await?;
|
||||||
|
|
||||||
|
let mut entries: Vec<Entry> = vec![];
|
||||||
|
if let Value::Array(notes) = data { for note_val in notes { if let Value::Object(note) = note_val {
|
||||||
|
let mut e_builder = EntryBuilder::default();
|
||||||
|
|
||||||
|
e_builder.id(stringify_value(note["id"].clone()))
|
||||||
|
.title(TextBuilder::default()
|
||||||
|
.value(stringtrunkify_value(note["text"].clone(), 20))
|
||||||
|
.lang(Some(String::from("en")))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.author(PersonBuilder::default()
|
||||||
|
.name(stringify_value(note["user"].as_object().get_or_insert(&Map::new())["name"].clone()))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.updated(DateTime::<FixedOffset>::parse_from_rfc3339(&stringify_value(note["createdAt"].clone()))?)
|
||||||
|
.content(ContentBuilder::default()
|
||||||
|
.base(domain.to_owned())
|
||||||
|
.value(stringify_value(note["text"].clone()))
|
||||||
|
.src(stringify_value(note["uri"].clone()))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.link(LinkBuilder::default()
|
||||||
|
.href(stringify_value(note["uri"].clone()))
|
||||||
|
.rel("alternate")
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Value::Array(files) = note["files"].clone() { for file_val in files { if let Value::Object(file) = file_val {
|
||||||
|
e_builder.link(LinkBuilder::default()
|
||||||
|
.href(stringify_value(file["url"].clone()))
|
||||||
|
.rel("enclosure")
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}}}
|
||||||
|
|
||||||
|
entries.push(e_builder.build());
|
||||||
|
} else {
|
||||||
|
return Err(Box::new(FediToFeedError::new(String::from("Note does not have the expected structure!"))))
|
||||||
|
}};
|
||||||
|
} else {
|
||||||
|
return Err(Box::new(FediToFeedError::new(String::from("Could not get a list of posts from API response!"))))
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.entries(entries);
|
||||||
|
Ok(builder.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_feed(res: Response<Body>, domain: &str, token: &str) -> Result<Response<Body>> {
|
||||||
|
let generator = MkFeedGenerator::new();
|
||||||
|
|
||||||
|
let feed = generator.get_feed(domain, token).await?;
|
||||||
|
|
||||||
|
let mut res = res;
|
||||||
|
*res.body_mut() = Body::from(feed.to_string());
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
app = Flask(__name__)
|
||||||
|
@app.route('/<path:path>', methods=['GET'])
|
||||||
|
def result(path):
|
||||||
|
args = path.split("/")
|
||||||
|
|
||||||
|
if len(args) >= 2:
|
||||||
|
domain = args[0]
|
||||||
|
token = args[1]
|
||||||
|
|
||||||
|
return generate_feed(get_mk_data(domain, token), domain)
|
||||||
|
else:
|
||||||
|
return '<h1>You need to go to /domain/token !</h1>'
|
||||||
|
*/
|
||||||
|
|
||||||
|
async fn echo(req: Request<Body>) -> Result<Response<Body>> {
|
||||||
|
let mut response = Response::new(Body::empty());
|
||||||
|
|
||||||
|
println!("Got a connection!");
|
||||||
|
|
||||||
|
match (req.method(), req.uri().path()) {
|
||||||
|
(&Method::GET, path) => {
|
||||||
|
let split_path = path.split('/').collect::<Vec<&str>>();
|
||||||
|
|
||||||
|
if split_path.len() >= 3 {
|
||||||
|
let domain = split_path[1];
|
||||||
|
let token = split_path[2];
|
||||||
|
|
||||||
|
response = generate_feed(response, domain, token).await?;
|
||||||
|
} else {
|
||||||
|
*response.body_mut() = Body::from("<h1>You need to go to `/domain/token!`</h1>");
|
||||||
|
}
|
||||||
|
*response.status_mut() = StatusCode::OK;
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
*response.status_mut() = StatusCode::NOT_FOUND;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
let addr = ([127, 0, 0, 1], 3000).into();
|
||||||
|
|
||||||
|
let service = make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(echo)) });
|
||||||
|
|
||||||
|
let server = Server::bind(&addr).serve(service);
|
||||||
|
|
||||||
|
println!("Listening on http://{}", addr);
|
||||||
|
|
||||||
|
server.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in a new issue