Added first version.

This commit is contained in:
Thelie 2023-05-29 16:08:07 +02:00
parent af9344f0d2
commit 7870100cbe
Signed by: Thelie
GPG key ID: C1C21A32DB006A37
4 changed files with 1573 additions and 0 deletions

1099
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

15
Cargo.toml Normal file
View 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
View 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
View 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(())
}