Add API methods for creating timeline markers

This commit is contained in:
silverpill 2021-11-03 12:03:56 +00:00
parent 64dddf3f7e
commit fbb0bc01cd
14 changed files with 254 additions and 2 deletions

1
Cargo.lock generated
View file

@ -1723,6 +1723,7 @@ dependencies = [
"mime-sniffer",
"mime_guess",
"num_cpus",
"postgres-protocol",
"postgres-types",
"rand 0.8.3",
"refinery",

View file

@ -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

View 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)
);

View file

@ -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)
);

View file

@ -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())

View file

@ -0,0 +1,2 @@
mod types;
pub mod views;

View 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,
}
}
}

View 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, &current_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,
&current_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)
}

View file

@ -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;

View file

@ -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,

View file

@ -0,0 +1,2 @@
pub mod queries;
pub mod types;

View 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)
}

View 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>,
}

View file

@ -1,5 +1,6 @@
pub mod attachments;
mod cleanup;
pub mod markers;
pub mod notifications;
pub mod oauth;
pub mod posts;