Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: import from other apps #56

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
367 changes: 362 additions & 5 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ include_dir = "0.7.3"
listenfd = "1.0.1"
mime_guess = "2.0.4"
rand = { version = "0.8.5", default-features = false }
reqwest = { version = "0.12.4", features = ["json"] }
serde = "1.0.199"
serde-aux = { version = "4.5.0", default-features = false }
serde_json = "1.0.116"
Expand Down
6 changes: 6 additions & 0 deletions src/forms/import.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub struct ImportFromOmnivore {
pub api_token: String,
}
1 change: 1 addition & 0 deletions src/forms/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod bookmarks;
pub mod import;
pub mod links;
pub mod lists;
pub mod users;
1 change: 1 addition & 0 deletions src/import/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod omnivore;
154 changes: 154 additions & 0 deletions src/import/omnivore.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
use reqwest::Client;
use serde_json::json;

pub struct OmnivoreImport {
client: Client,
graphql_endpoint_url: String,
}

impl OmnivoreImport {
pub fn new(api_token: String, graphql_endpoint_url: String) -> Self {
let client = Client::builder()
.default_headers({
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("Content-Type", "application/json".parse().unwrap());
headers.insert("Authorization", api_token.parse().unwrap());
headers
})
.build()
.unwrap();

Self {
client,
graphql_endpoint_url,
}
}

pub async fn get_articles(
&self,
limit: Option<i32>,
cursor: Option<String>,
format: String,
query: String,
include_content: bool,
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
let request_body = json!({
"query": "
query Search($after: String, $first: Int, $query: String, $format: String, $includeContent: Boolean) {
search(after: $after, first: $first, query: $query, format: $format, includeContent: $includeContent) {
... on SearchSuccess {
edges {
cursor
node {
id
title
slug
url
pageType
contentReader
createdAt
isArchived
readingProgressPercent
readingProgressTopPercent
readingProgressAnchorIndex
author
image
description
publishedAt
ownedByViewer
originalArticleUrl
uploadFileId
labels {
id
name
color
}
pageId
shortId
quote
annotation
state
siteName
subscription
readAt
savedAt
wordsCount
recommendations {
id
name
note
user {
userId
name
username
profileImageURL
}
recommendedAt
}
highlights {
...HighlightFields
}
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
totalCount
}
}
... on SearchError {
errorCodes
}
}
}

fragment HighlightFields on Highlight {
id
type
shortId
quote
prefix
suffix
patch
annotation
createdByMe
createdAt
updatedAt
sharedAt
highlightPositionPercent
highlightPositionAnchorIndex
labels {
id
name
color
createdAt
}
}
",
"variables": {
"first": limit,
"after": cursor,
"query": query,
"format": format,
"includeContent": include_content,
}
});

let response = self
.client
.post(&self.graphql_endpoint_url)
.json(&request_body)
.send()
.await?;

let response_body: serde_json::Value = response.json().await?;

if let Some(errors) = response_body.get("errors") {
return Err(format!("GraphQL Error: {:?}", errors).into());
}

Ok(response_body)
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod db;
mod extract;
mod form_errors;
mod forms;
mod import;
mod response_error;
mod routes;
pub mod server;
Expand Down
77 changes: 75 additions & 2 deletions src/routes/bookmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ use crate::{
authentication::AuthUser,
db::{self, bookmarks::InsertBookmark},
extract::{self, qs_form::QsForm},
forms::{bookmarks::CreateBookmark, links::CreateLink, lists::CreateList},
forms::{
bookmarks::CreateBookmark, import::ImportFromOmnivore, links::CreateLink, lists::CreateList,
},
import::omnivore::OmnivoreImport,
response_error::ResponseResult,
server::AppState,
views::{
self,
bookmarks::{CreateBookmarkTemplate, UnsortedBookmarksTemplate},
bookmarks::{
CreateBookmarkTemplate, ImportFromOmnivoreTemplate, UnsortedBookmarksTemplate,
},
layout::LayoutTemplate,
},
};
Expand All @@ -30,6 +35,10 @@ pub fn router() -> Router<AppState> {
.route("/bookmarks/create", get(get_create).post(post_create))
.route("/bookmarks/unsorted", get(get_unsorted))
.route("/bookmarks/:id", delete(delete_by_id))
.route(
"/bookmarks/import_from_omnivore",
get(get_import_from_omnivore).post(post_import_from_omnivore),
)
}

async fn post_create(
Expand Down Expand Up @@ -169,3 +178,67 @@ async fn delete_by_id(

Ok(headers)
}

async fn post_import_from_omnivore(
extract::Tx(mut tx): extract::Tx,
auth_user: AuthUser,
QsForm(import): QsForm<ImportFromOmnivore>,
) -> ResponseResult<Response> {
let api_token = import.api_token;
let omnivore_graphql_endpoint_url = "https://api-prod.omnivore.app/api/graphql".to_string();

let client = OmnivoreImport::new(api_token, omnivore_graphql_endpoint_url);
let result = client
.get_articles(
Some(1000),
None,
"markdown".to_string(),
"in:inbox".to_string(),
false,
)
.await
.expect("Failed to get articles");
let parent = db::lists::insert(
&mut tx,
auth_user.user_id,
CreateList {
title: "Omnivore".to_string(),
content: Some("Imported from Omnivore".to_string()),
},
)
.await?;
for article in result["data"]["search"]["edges"].as_array().unwrap() {
let title = article["node"]["title"].as_str().unwrap();
let url = article["node"]["originalArticleUrl"].as_str().unwrap();
let bookmark = db::bookmarks::insert(
&mut tx,
auth_user.user_id,
InsertBookmark {
url: url.to_string(),
title: title.to_string(),
},
)
.await?;
db::links::insert(
&mut tx,
auth_user.user_id,
CreateLink {
src: parent.id,
dest: bookmark.id,
},
)
.await?;
}
tx.commit().await?;

Ok("Imported from Omnivore".into_response())
}

async fn get_import_from_omnivore(
extract::Tx(mut tx): extract::Tx,
auth_user: AuthUser,
) -> ResponseResult<ImportFromOmnivoreTemplate> {
let layout = LayoutTemplate::from_db(&mut tx, &auth_user).await?;

Ok(ImportFromOmnivoreTemplate { layout })
}
7 changes: 7 additions & 0 deletions src/views/bookmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,10 @@ pub struct UnsortedBookmarksTemplate {
pub layout: LayoutTemplate,
pub bookmarks: Vec<db::Bookmark>,
}

#[derive(Template)]
#[template(path = "import_from_omnivore.html")]

pub struct ImportFromOmnivoreTemplate {
pub layout: LayoutTemplate,
}
13 changes: 13 additions & 0 deletions templates/import_from_omnivore.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% import "_form.html" as form %}
{% import "_content.html" as content %}
{% extends "_layout.html" %}

{% block content %}
<form action="/bookmarks/import_from_omnivore" method="post" hx-post="/bookmarks/import_from_omnivore" hx-target="main"
hx-select="main"
hx-push-url="true">
<label for="api_token">API Token:</label>
<input type="text" id="api_token" name="api_token" required>
<button type="submit">Import from Omnivore</button>
</form>
{% endblock %}