Initial Matrix implementation
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
fbca4b7c06
commit
a50d157394
10 changed files with 204 additions and 30 deletions
|
@ -1 +1,3 @@
|
||||||
- Return API error response on missing query parameter
|
- Initial Matrix implementation
|
||||||
|
- Allows bots to know, when a new meme was uploaded
|
||||||
|
- First step towards decentralization
|
|
@ -7,14 +7,14 @@ use axum::{
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::ipfs::error::IPFSError;
|
use crate::error::ServiceError;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum CDNError {
|
pub enum CDNError {
|
||||||
#[error("SQL error: {0}")]
|
#[error("SQL error: {0}")]
|
||||||
Sql(#[from] sqlx::Error),
|
Sql(#[from] sqlx::Error),
|
||||||
#[error("IPFS error: {0}")]
|
#[error("JMService error: {0}")]
|
||||||
Ipfs(#[from] IPFSError),
|
Service(#[from] ServiceError),
|
||||||
#[error("Decode error: {0}")]
|
#[error("Decode error: {0}")]
|
||||||
Decode(#[from] FromUtf8Error),
|
Decode(#[from] FromUtf8Error),
|
||||||
#[error("Internal server error")]
|
#[error("Internal server error")]
|
||||||
|
|
|
@ -10,6 +10,9 @@ pub struct Config {
|
||||||
pub database: String,
|
pub database: String,
|
||||||
pub cdn: String,
|
pub cdn: String,
|
||||||
pub ipfs_api: Url,
|
pub ipfs_api: Url,
|
||||||
|
pub matrix_url: Url,
|
||||||
|
pub matrix_token: String,
|
||||||
|
pub matrix_domain: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
@ -19,6 +22,9 @@ impl Config {
|
||||||
client,
|
client,
|
||||||
ipfs_url: self.ipfs_api.clone(),
|
ipfs_url: self.ipfs_api.clone(),
|
||||||
cdn_url: self.cdn.clone(),
|
cdn_url: self.cdn.clone(),
|
||||||
|
matrix_url: self.matrix_url.clone(),
|
||||||
|
matrix_token: self.matrix_token.clone(),
|
||||||
|
matrix_domain: self.matrix_domain.clone(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
12
src/error.rs
12
src/error.rs
|
@ -1,4 +1,6 @@
|
||||||
|
use hyper::StatusCode;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use url::ParseError;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum JMError {
|
pub enum JMError {
|
||||||
|
@ -13,3 +15,13 @@ pub enum JMError {
|
||||||
#[error("Reqwest error: {0}")]
|
#[error("Reqwest error: {0}")]
|
||||||
Reqwest(#[from] reqwest::Error),
|
Reqwest(#[from] reqwest::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum ServiceError {
|
||||||
|
#[error("Reqwest error: {0}")]
|
||||||
|
Reqwest(#[from] reqwest::Error),
|
||||||
|
#[error("URL parse error: {0}")]
|
||||||
|
Url(#[from] ParseError),
|
||||||
|
#[error("Invalid response code: {0}")]
|
||||||
|
InvalidResponse(StatusCode),
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
use thiserror::Error;
|
|
||||||
use url::ParseError;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum IPFSError {
|
|
||||||
#[error("Reqwest error: {0}")]
|
|
||||||
Reqwest(#[from] reqwest::Error),
|
|
||||||
#[error("URL parse error: {0}")]
|
|
||||||
Url(#[from] ParseError),
|
|
||||||
}
|
|
|
@ -7,11 +7,7 @@ use reqwest::{
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::JMServiceInner;
|
use crate::{error::ServiceError, JMServiceInner};
|
||||||
|
|
||||||
use self::error::IPFSError;
|
|
||||||
|
|
||||||
pub(crate) mod error;
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct IPFSFile {
|
pub struct IPFSFile {
|
||||||
|
@ -39,7 +35,7 @@ pub struct PinQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JMServiceInner {
|
impl JMServiceInner {
|
||||||
pub async fn cat(&self, cid: String) -> Result<Response, IPFSError> {
|
pub async fn cat(&self, cid: String) -> Result<Response, ServiceError> {
|
||||||
let request = self
|
let request = self
|
||||||
.client
|
.client
|
||||||
.post(self.ipfs_url.join("/api/v0/cat")?)
|
.post(self.ipfs_url.join("/api/v0/cat")?)
|
||||||
|
@ -47,7 +43,7 @@ impl JMServiceInner {
|
||||||
Ok(request.send().await?)
|
Ok(request.send().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add(&self, file: Bytes, filename: String) -> Result<IPFSFile, IPFSError> {
|
pub async fn add(&self, file: Bytes, filename: String) -> Result<IPFSFile, ServiceError> {
|
||||||
let request = self
|
let request = self
|
||||||
.client
|
.client
|
||||||
.post(self.ipfs_url.join("/api/v0/add")?)
|
.post(self.ipfs_url.join("/api/v0/add")?)
|
||||||
|
@ -58,7 +54,7 @@ impl JMServiceInner {
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn pin(&self, cid: String) -> Result<(), IPFSError> {
|
pub async fn pin(&self, cid: String) -> Result<(), ServiceError> {
|
||||||
let request = self
|
let request = self
|
||||||
.client
|
.client
|
||||||
.post(self.ipfs_url.join("/api/v0/pin/add")?)
|
.post(self.ipfs_url.join("/api/v0/pin/add")?)
|
||||||
|
|
|
@ -16,6 +16,7 @@ mod config;
|
||||||
mod error;
|
mod error;
|
||||||
mod ipfs;
|
mod ipfs;
|
||||||
mod lib;
|
mod lib;
|
||||||
|
mod matrix;
|
||||||
mod models;
|
mod models;
|
||||||
mod sql;
|
mod sql;
|
||||||
mod v1;
|
mod v1;
|
||||||
|
@ -35,6 +36,9 @@ pub struct JMServiceInner {
|
||||||
client: Client,
|
client: Client,
|
||||||
ipfs_url: Url,
|
ipfs_url: Url,
|
||||||
cdn_url: String,
|
cdn_url: String,
|
||||||
|
matrix_url: Url,
|
||||||
|
matrix_token: String,
|
||||||
|
matrix_domain: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type JMService = Arc<JMServiceInner>;
|
pub type JMService = Arc<JMServiceInner>;
|
||||||
|
|
158
src/matrix/mod.rs
Normal file
158
src/matrix/mod.rs
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{error::ServiceError, JMServiceInner};
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Meme {
|
||||||
|
pub category: String,
|
||||||
|
pub filename: String,
|
||||||
|
pub cid: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct UserID {
|
||||||
|
pub user_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct RoomID {
|
||||||
|
pub room_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct EventID {
|
||||||
|
pub event_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct RegisterRequest {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub reg_type: String,
|
||||||
|
pub username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JMServiceInner {
|
||||||
|
pub async fn add_meme(
|
||||||
|
&self,
|
||||||
|
category: String,
|
||||||
|
filename: String,
|
||||||
|
cid: String,
|
||||||
|
user: String,
|
||||||
|
id: u64,
|
||||||
|
) -> Result<(), ServiceError> {
|
||||||
|
let meme = Meme {
|
||||||
|
category,
|
||||||
|
filename,
|
||||||
|
cid,
|
||||||
|
};
|
||||||
|
let txid = meme.calc_txid(user.clone());
|
||||||
|
let usr = self.check_user(user).await?;
|
||||||
|
let room_id = self.join_room(&usr).await?;
|
||||||
|
let path = format!(
|
||||||
|
"/_matrix/client/r0/rooms/{}/send/es.jensmem.meme/{}",
|
||||||
|
&room_id, txid
|
||||||
|
);
|
||||||
|
let url = self.matrix_url.join(path.as_str())?;
|
||||||
|
let req = self
|
||||||
|
.client
|
||||||
|
.put(url)
|
||||||
|
.bearer_auth(self.matrix_token.clone())
|
||||||
|
.query(&usr)
|
||||||
|
.json(&meme);
|
||||||
|
let res = req.send().await?;
|
||||||
|
if res.status().is_success() {
|
||||||
|
let event: EventID = res.json().await?;
|
||||||
|
let path = format!(
|
||||||
|
"/_matrix/client/r0/rooms/{}/state/es.jensmem.index/{}",
|
||||||
|
&room_id, id
|
||||||
|
);
|
||||||
|
let req = self
|
||||||
|
.client
|
||||||
|
.put(self.matrix_url.join(path.as_str())?)
|
||||||
|
.bearer_auth(self.matrix_token.clone())
|
||||||
|
.json(&event);
|
||||||
|
let res = req.send().await?;
|
||||||
|
if res.status().is_success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ServiceError::InvalidResponse(res.status()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(ServiceError::InvalidResponse(res.status()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_user(&self, user: String) -> Result<UserID, ServiceError> {
|
||||||
|
let username = format!("jm_{}", user);
|
||||||
|
let user = self.get_mxid(username.clone());
|
||||||
|
let req = self
|
||||||
|
.client
|
||||||
|
.get(self.matrix_url.join("/_matrix/client/r0/account/whoami")?)
|
||||||
|
.bearer_auth(self.matrix_token.clone())
|
||||||
|
.query(&user);
|
||||||
|
let res = req.send().await?;
|
||||||
|
if res.status().is_success() {
|
||||||
|
let mxid: UserID = res.json().await?;
|
||||||
|
Ok(mxid)
|
||||||
|
} else {
|
||||||
|
let mxid = self.register_user(username).await?;
|
||||||
|
Ok(mxid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn register_user(&self, username: String) -> Result<UserID, ServiceError> {
|
||||||
|
let req = self
|
||||||
|
.client
|
||||||
|
.post(self.matrix_url.join("/_matrix/client/r0/register")?)
|
||||||
|
.bearer_auth(self.matrix_token.clone())
|
||||||
|
.json(&RegisterRequest::new(username));
|
||||||
|
let res = req.send().await?;
|
||||||
|
if res.status().is_success() {
|
||||||
|
let user: UserID = res.json().await?;
|
||||||
|
Ok(user)
|
||||||
|
} else {
|
||||||
|
Err(ServiceError::InvalidResponse(res.status()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn join_room(&self, user: &UserID) -> Result<String, ServiceError> {
|
||||||
|
let req = self
|
||||||
|
.client
|
||||||
|
.post(
|
||||||
|
self.matrix_url
|
||||||
|
.join("/_matrix/client/r0/join/%23memes%3Atilera.org")?,
|
||||||
|
)
|
||||||
|
.bearer_auth(self.matrix_token.clone())
|
||||||
|
.query(user);
|
||||||
|
|
||||||
|
let res = req.send().await?;
|
||||||
|
if res.status().is_success() {
|
||||||
|
let room: RoomID = res.json().await?;
|
||||||
|
Ok(room.room_id)
|
||||||
|
} else {
|
||||||
|
Err(ServiceError::InvalidResponse(res.status()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_mxid(&self, username: String) -> UserID {
|
||||||
|
UserID {
|
||||||
|
user_id: format!("@{}:{}", username, self.matrix_domain.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegisterRequest {
|
||||||
|
pub fn new(username: String) -> Self {
|
||||||
|
Self {
|
||||||
|
reg_type: "m.login.application_service".to_string(),
|
||||||
|
username,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Meme {
|
||||||
|
pub fn calc_txid(&self, user: String) -> String {
|
||||||
|
let txid = format!("{}/{}/{}/{}", user, self.category, self.filename, self.cid);
|
||||||
|
urlencoding::encode(txid.as_str()).into_owned()
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ use reqwest::StatusCode;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use super::models::ErrorResponse;
|
use super::models::ErrorResponse;
|
||||||
use crate::ipfs::error::IPFSError;
|
use crate::error::ServiceError;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum APIError {
|
pub enum APIError {
|
||||||
|
@ -28,8 +28,8 @@ pub enum APIError {
|
||||||
NotFound(String),
|
NotFound(String),
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Internal(String),
|
Internal(String),
|
||||||
#[error("IPFS error: {0}")]
|
#[error("JMService error: {0}")]
|
||||||
Ipfs(#[from] IPFSError),
|
Service(#[from] ServiceError),
|
||||||
#[error("Query rejection: {0}")]
|
#[error("Query rejection: {0}")]
|
||||||
Query(#[from] QueryRejection),
|
Query(#[from] QueryRejection),
|
||||||
}
|
}
|
||||||
|
@ -60,10 +60,8 @@ impl IntoResponse for APIError {
|
||||||
APIError::Unauthorized(err) => ErrorResponse::new(StatusCode::UNAUTHORIZED, Some(err)),
|
APIError::Unauthorized(err) => ErrorResponse::new(StatusCode::UNAUTHORIZED, Some(err)),
|
||||||
APIError::Forbidden(err) => ErrorResponse::new(StatusCode::FORBIDDEN, Some(err)),
|
APIError::Forbidden(err) => ErrorResponse::new(StatusCode::FORBIDDEN, Some(err)),
|
||||||
APIError::NotFound(err) => ErrorResponse::new(StatusCode::NOT_FOUND, Some(err)),
|
APIError::NotFound(err) => ErrorResponse::new(StatusCode::NOT_FOUND, Some(err)),
|
||||||
APIError::Internal(err) => {
|
APIError::Internal(err) => ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, Some(err)),
|
||||||
ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, Some(err))
|
APIError::Service(_) => ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, None),
|
||||||
}
|
|
||||||
APIError::Ipfs(_) => ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, None),
|
|
||||||
APIError::Query(_) => ErrorResponse::new(StatusCode::BAD_REQUEST, None),
|
APIError::Query(_) => ErrorResponse::new(StatusCode::BAD_REQUEST, None),
|
||||||
};
|
};
|
||||||
let status = res.status;
|
let status = res.status;
|
||||||
|
|
|
@ -166,7 +166,15 @@ async fn upload(
|
||||||
if res == 0 {
|
if res == 0 {
|
||||||
return Err(APIError::Internal("Database insertion error".to_string()));
|
return Err(APIError::Internal("Database insertion error".to_string()));
|
||||||
}
|
}
|
||||||
|
service
|
||||||
|
.add_meme(
|
||||||
|
cat.id.clone(),
|
||||||
|
f.name.clone(),
|
||||||
|
f.hash.clone(),
|
||||||
|
user.id.clone(),
|
||||||
|
res,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
service.pin(f.hash).await?;
|
service.pin(f.hash).await?;
|
||||||
links.push(format!(
|
links.push(format!(
|
||||||
"{}/{}/{}",
|
"{}/{}/{}",
|
||||||
|
|
Loading…
Reference in a new issue