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