Add API methods for creating timeline markers
This commit is contained in:
parent
64dddf3f7e
commit
fbb0bc01cd
14 changed files with 254 additions and 2 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1723,6 +1723,7 @@ dependencies = [
|
|||
"mime-sniffer",
|
||||
"mime_guess",
|
||||
"num_cpus",
|
||||
"postgres-protocol",
|
||||
"postgres-types",
|
||||
"rand 0.8.3",
|
||||
"refinery",
|
||||
|
|
|
@ -39,8 +39,6 @@ mime_guess = "2.0.3"
|
|||
mime-sniffer = "0.1.2"
|
||||
# Used to determine the number of CPUs on the system
|
||||
num_cpus = "1.13.0"
|
||||
# Used to map postgres types to rust types
|
||||
postgres-types = { version = "0.1.2", features = ["derive", "with-chrono-0_4", "with-uuid-0_8", "with-serde_json-1"] }
|
||||
# Used for working with regular expressions
|
||||
regex = "1.5.4"
|
||||
# Used to generate random numbers
|
||||
|
@ -69,6 +67,8 @@ thiserror = "1.0.24"
|
|||
tokio = { version = "0.2.25", features = ["macros"] }
|
||||
# Used for working with Postgresql database (compatible with tokio 0.2)
|
||||
tokio-postgres = { version = "0.5.5", features = ["with-chrono-0_4", "with-uuid-0_8", "with-serde_json-1"] }
|
||||
postgres-types = { version = "0.1.2", features = ["derive", "with-chrono-0_4", "with-uuid-0_8", "with-serde_json-1"] }
|
||||
postgres-protocol = "0.5.3"
|
||||
# Used to work with URLs
|
||||
url = "2.2.2"
|
||||
# Used to work with UUIDs
|
||||
|
|
8
migrations/V0010__timeline_marker.sql
Normal file
8
migrations/V0010__timeline_marker.sql
Normal file
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE timeline_marker (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES user_account (id) ON DELETE CASCADE,
|
||||
timeline SMALLINT NOT NULL,
|
||||
last_read_id VARCHAR(100) NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
UNIQUE (user_id, timeline)
|
||||
);
|
|
@ -91,3 +91,12 @@ CREATE TABLE notification (
|
|||
event_type SMALLINT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE timeline_marker (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES user_account (id) ON DELETE CASCADE,
|
||||
timeline SMALLINT NOT NULL,
|
||||
last_read_id VARCHAR(100) NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
UNIQUE (user_id, timeline)
|
||||
);
|
||||
|
|
|
@ -13,6 +13,7 @@ use mitra::logger::configure_logger;
|
|||
use mitra::mastodon_api::accounts::views::account_api_scope;
|
||||
use mitra::mastodon_api::directory::views::profile_directory;
|
||||
use mitra::mastodon_api::instance::views as instance_api;
|
||||
use mitra::mastodon_api::markers::views::marker_api_scope;
|
||||
use mitra::mastodon_api::media::views::media_api_scope;
|
||||
use mitra::mastodon_api::notifications::views::notification_api_scope;
|
||||
use mitra::mastodon_api::oauth::auth::create_auth_error_handler;
|
||||
|
@ -77,6 +78,7 @@ async fn main() -> std::io::Result<()> {
|
|||
.service(oauth_api_scope())
|
||||
.service(profile_directory)
|
||||
.service(account_api_scope())
|
||||
.service(marker_api_scope())
|
||||
.service(media_api_scope())
|
||||
.service(notification_api_scope())
|
||||
.service(status_api_scope())
|
||||
|
|
2
src/mastodon_api/markers/mod.rs
Normal file
2
src/mastodon_api/markers/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
mod types;
|
||||
pub mod views;
|
53
src/mastodon_api/markers/types.rs
Normal file
53
src/mastodon_api/markers/types.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::errors::ValidationError;
|
||||
use crate::models::markers::types::{DbTimelineMarker, Timeline};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct MarkerQueryParams {
|
||||
#[serde(rename = "timeline[]")]
|
||||
pub timeline: String,
|
||||
}
|
||||
|
||||
impl MarkerQueryParams {
|
||||
pub fn to_timeline(&self) -> Result<Timeline, ValidationError> {
|
||||
let timeline = match self.timeline.as_ref() {
|
||||
"home" => Timeline::Home,
|
||||
"notifications" => Timeline::Notifications,
|
||||
_ => return Err(ValidationError("invalid timeline name")),
|
||||
};
|
||||
Ok(timeline)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct MarkerCreateData {
|
||||
#[serde(rename = "notifications[last_read_id]")]
|
||||
pub notifications: String,
|
||||
}
|
||||
|
||||
/// https://docs.joinmastodon.org/entities/marker/
|
||||
#[derive(Serialize)]
|
||||
pub struct Marker {
|
||||
last_read_id: String,
|
||||
version: i32,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Markers {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub notifications: Option<Marker>,
|
||||
}
|
||||
|
||||
impl From<DbTimelineMarker> for Marker {
|
||||
|
||||
fn from(value: DbTimelineMarker) -> Self {
|
||||
Self {
|
||||
last_read_id: value.last_read_id,
|
||||
version: 0,
|
||||
updated_at: value.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
53
src/mastodon_api/markers/views.rs
Normal file
53
src/mastodon_api/markers/views.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
use actix_web::{get, post, web, HttpResponse, Scope};
|
||||
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||
|
||||
use crate::database::{Pool, get_database_client};
|
||||
use crate::errors::HttpError;
|
||||
use crate::mastodon_api::oauth::auth::get_current_user;
|
||||
use crate::models::markers::queries::{
|
||||
create_or_update_marker,
|
||||
get_marker_opt,
|
||||
};
|
||||
use crate::models::markers::types::Timeline;
|
||||
use super::types::{MarkerQueryParams, MarkerCreateData, Markers};
|
||||
|
||||
/// https://docs.joinmastodon.org/methods/timelines/markers/
|
||||
#[get("")]
|
||||
async fn get_marker_view(
|
||||
auth: BearerAuth,
|
||||
db_pool: web::Data<Pool>,
|
||||
query_params: web::Query<MarkerQueryParams>,
|
||||
) -> Result<HttpResponse, HttpError> {
|
||||
let db_client = &**get_database_client(&db_pool).await?;
|
||||
let current_user = get_current_user(db_client, auth.token()).await?;
|
||||
let timeline = query_params.to_timeline()?;
|
||||
let maybe_db_marker = get_marker_opt(db_client, ¤t_user.id, timeline).await?;
|
||||
let markers = Markers {
|
||||
notifications: maybe_db_marker.map(|db_marker| db_marker.into()),
|
||||
};
|
||||
Ok(HttpResponse::Ok().json(markers))
|
||||
}
|
||||
|
||||
#[post("")]
|
||||
async fn update_marker_view(
|
||||
auth: BearerAuth,
|
||||
db_pool: web::Data<Pool>,
|
||||
data: web::Json<MarkerCreateData>,
|
||||
) -> Result<HttpResponse, HttpError> {
|
||||
let db_client = &**get_database_client(&db_pool).await?;
|
||||
let current_user = get_current_user(db_client, auth.token()).await?;
|
||||
let db_marker = create_or_update_marker(
|
||||
db_client,
|
||||
¤t_user.id,
|
||||
Timeline::Notifications,
|
||||
data.into_inner().notifications,
|
||||
).await?;
|
||||
let markers = Markers { notifications: Some(db_marker.into()) };
|
||||
Ok(HttpResponse::Ok().json(markers))
|
||||
}
|
||||
|
||||
pub fn marker_api_scope() -> Scope {
|
||||
web::scope("/api/v1/markers")
|
||||
.service(get_marker_view)
|
||||
.service(update_marker_view)
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
pub mod accounts;
|
||||
pub mod directory;
|
||||
pub mod instance;
|
||||
pub mod markers;
|
||||
pub mod media;
|
||||
pub mod notifications;
|
||||
pub mod oauth;
|
||||
|
|
|
@ -9,6 +9,7 @@ use crate::mastodon_api::statuses::types::Status;
|
|||
use crate::models::posts::helpers::get_actions_for_posts;
|
||||
use crate::models::posts::queries::get_posts;
|
||||
|
||||
/// https://docs.joinmastodon.org/methods/timelines/
|
||||
#[get("/api/v1/timelines/home")]
|
||||
pub async fn home_timeline(
|
||||
auth: BearerAuth,
|
||||
|
|
2
src/models/markers/mod.rs
Normal file
2
src/models/markers/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod queries;
|
||||
pub mod types;
|
45
src/models/markers/queries.rs
Normal file
45
src/models/markers/queries.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
use tokio_postgres::GenericClient;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::errors::DatabaseError;
|
||||
use super::types::{DbTimelineMarker, Timeline};
|
||||
|
||||
pub async fn create_or_update_marker(
|
||||
db_client: &impl GenericClient,
|
||||
user_id: &Uuid,
|
||||
timeline: Timeline,
|
||||
last_read_id: String,
|
||||
) -> Result<DbTimelineMarker, DatabaseError> {
|
||||
let row = db_client.query_one(
|
||||
"
|
||||
INSERT INTO timeline_marker (user_id, timeline, last_read_id)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (user_id, timeline) DO UPDATE
|
||||
SET last_read_id = $3, updated_at = now()
|
||||
RETURNING timeline_marker
|
||||
",
|
||||
&[&user_id, &timeline, &last_read_id],
|
||||
).await?;
|
||||
let marker = row.try_get("timeline_marker")?;
|
||||
Ok(marker)
|
||||
}
|
||||
|
||||
pub async fn get_marker_opt(
|
||||
db_client: &impl GenericClient,
|
||||
user_id: &Uuid,
|
||||
timeline: Timeline,
|
||||
) -> Result<Option<DbTimelineMarker>, DatabaseError> {
|
||||
let maybe_row = db_client.query_opt(
|
||||
"
|
||||
SELECT timeline_marker
|
||||
FROM timeline_marker
|
||||
WHERE user_id = $1 AND timeline = $2
|
||||
",
|
||||
&[&user_id, &timeline],
|
||||
).await?;
|
||||
let maybe_marker = match maybe_row {
|
||||
Some(row) => row.try_get("timeline_marker")?,
|
||||
None => None,
|
||||
};
|
||||
Ok(maybe_marker)
|
||||
}
|
74
src/models/markers/types.rs
Normal file
74
src/models/markers/types.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
use std::convert::TryFrom;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use postgres_protocol::types::{int2_from_sql, int2_to_sql};
|
||||
use postgres_types::{
|
||||
FromSql, ToSql, IsNull, Type,
|
||||
accepts, to_sql_checked,
|
||||
private::BytesMut,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::errors::ConversionError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Timeline {
|
||||
Home,
|
||||
Notifications,
|
||||
}
|
||||
|
||||
impl From<&Timeline> for i16 {
|
||||
fn from(value: &Timeline) -> i16 {
|
||||
match value {
|
||||
Timeline::Home => 1,
|
||||
Timeline::Notifications => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<i16> for Timeline {
|
||||
type Error = ConversionError;
|
||||
|
||||
fn try_from(value: i16) -> Result<Self, Self::Error> {
|
||||
let timeline = match value {
|
||||
1 => Self::Home,
|
||||
2 => Self::Notifications,
|
||||
_ => return Err(ConversionError),
|
||||
};
|
||||
Ok(timeline)
|
||||
}
|
||||
}
|
||||
|
||||
type SqlError = Box<dyn std::error::Error + Sync + Send>;
|
||||
|
||||
impl<'a> FromSql<'a> for Timeline {
|
||||
fn from_sql(_: &Type, raw: &'a [u8]) -> Result<Timeline, SqlError> {
|
||||
let int_value = int2_from_sql(raw)?;
|
||||
let timeline = Timeline::try_from(int_value)?;
|
||||
Ok(timeline)
|
||||
}
|
||||
|
||||
accepts!(INT2);
|
||||
}
|
||||
|
||||
impl ToSql for Timeline {
|
||||
fn to_sql(&self, _: &Type, out: &mut BytesMut) -> Result<IsNull, SqlError> {
|
||||
let int_value: i16 = self.into();
|
||||
int2_to_sql(int_value, out);
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
|
||||
accepts!(INT2);
|
||||
to_sql_checked!();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(FromSql)]
|
||||
#[postgres(name = "timeline_marker")]
|
||||
pub struct DbTimelineMarker {
|
||||
id: i32,
|
||||
user_id: Uuid,
|
||||
pub timeline: Timeline,
|
||||
pub last_read_id: String,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
pub mod attachments;
|
||||
mod cleanup;
|
||||
pub mod markers;
|
||||
pub mod notifications;
|
||||
pub mod oauth;
|
||||
pub mod posts;
|
||||
|
|
Loading…
Reference in a new issue