about summary refs log tree commit diff
path: root/util/src/micropub.rs
blob: 6127079546fe544c2f073ff28b5305cf0131c2a8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
//! Common protocol types for Micropub.
//!
//! Check out the [`kittybox-indieauth`] crate that gives similar treatment to IndieAuth.
use std::collections::HashMap;

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "kebab-case")]
/// Query types supported by a Micropub server (as in ?q=<type>).
pub enum QueryType {
    /// Query source URL or recent posts (subject to extension implementation)
    Source,
    /// Query config (mandatory)
    Config,
    /// Query available channels
    Channel,
    /// Query available syndication destinations
    SyndicateTo,
    /// Query known categories/tags
    Category,
    /// Unsupported query type
    // TODO: make this take a lifetime parameter for zero-copy deserialization if possible?
    Unknown(std::borrow::Cow<'static, str>)
}

/// Data structure representing a Micropub channel in the ?q=channels output.
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
pub struct Channel {
    /// The channel's UID, opaque to the client.
    pub uid: String,
    /// A human-friendly name.
    pub name: String,
}

#[derive(Serialize, Deserialize, PartialEq, Debug)]
/// A destination place to syndicate a post to.
///
/// Commonly seen as part of [`?q=syndicate-to`][QueryType::SyndicateTo].
pub struct SyndicationDestination {
    /// The syndication destination's UID, opaque to the client.
    pub uid: String,
    /// A human-friendly name.
    pub name: String
}

fn default_q_list() -> Vec<QueryType> {
    vec![QueryType::Config]
}
/// ?q=config output of a Micropub endpoint.
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct Config {
    /// Query types supported by this server.
    #[serde(default = "default_q_list")]
    pub q: Vec<QueryType>,
    /// List of channels this server provides.
    /// If [`None`][Option::None], you may want to consult [`?q=channel`][QueryType::Channel].
    #[serde(skip_serializing_if = "Option::is_none")]
    pub channels: Option<Vec<Channel>>,
    /// List of available syndication destinations.
    /// If [`None`][Option::None], you may want to consult [`?q=channel`][QueryType::Channel].
    #[serde(skip_serializing_if = "Option::is_none")]
    pub syndicate_to: Option<Vec<SyndicationDestination>>,
    /// URL for a Media Endpoint, if any.
    pub media_endpoint: Option<url::Url>,
    /// Other unspecified keys, sometimes implementation-defined.
    #[serde(flatten)]
    pub other: HashMap<String, serde_json::Value>
}

#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
#[serde(rename_all = "snake_case")]
/// Kinds of errors that can happen within a Micropub operation.
pub enum ErrorKind {
    /// An erroneous attempt to create something that already exists.
    AlreadyExists,
    /// Current user is expressly forbidden from performing this action.
    Forbidden,
    /// The Micropub server experienced an internal error.
    InternalServerError,
    /// The request was invalid or malformed.
    InvalidRequest,
    /// The provided OAuth2 scopes were insufficient to allow performing this action.
    InvalidScope,
    /// There was no token or other means of authorization in the request.
    NotAuthorized,
    /// Whatever was requested was not found.
    NotFound,
    /// The request payload was of a type unsupported by the Micropub endpoint.
    UnsupportedMediaType,
}

/// Representation of the Micropub API error.
#[derive(Serialize, Deserialize, Debug)]
pub struct Error {
    /// General kind of an error that occured.
    pub error: ErrorKind,
    /// A human-readable error description intended for application developers.
    // TODO use Cow<'static, str> to save on heap allocations
    pub error_description: String,
}

impl std::error::Error for Error {}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let s = if let serde_json::Value::String(s) = serde_json::to_value(&self.error).unwrap() {
            s
        } else {
            unreachable!()
        };

        f.write_str(&s)?;
        f.write_str(" (")?;
        f.write_str(&self.error_description)?;
        f.write_str(")")
    }
}

impl From<serde_json::Error> for Error {
    fn from(err: serde_json::Error) -> Self {
        use ErrorKind::*;
        Self {
            error: InvalidRequest,
            error_description: err.to_string(),
        }
    }
}

impl Error {
    /// Create a new Micropub error.
    pub fn new(error: ErrorKind, error_description: &str) -> Self {
        Self {
            error,
            error_description: error_description.to_owned(),
        }
    }
}

#[cfg(feature = "http")]
impl From<&Error> for http::StatusCode {
    fn from(err: &Error) -> Self {
        use ErrorKind::*;
        match err.error {
            AlreadyExists => http::StatusCode::CONFLICT,
            Forbidden => http::StatusCode::FORBIDDEN,
            InternalServerError => http::StatusCode::INTERNAL_SERVER_ERROR,
            InvalidRequest => http::StatusCode::BAD_REQUEST,
            InvalidScope => http::StatusCode::UNAUTHORIZED,
            NotAuthorized => http::StatusCode::UNAUTHORIZED,
            NotFound => http::StatusCode::NOT_FOUND,
            UnsupportedMediaType => http::StatusCode::UNSUPPORTED_MEDIA_TYPE,
        }
    }
}

#[cfg(feature = "http")]
impl From<Error> for http::StatusCode {
    fn from(err: Error) -> Self {
        (&err).into()
    }
}

#[cfg(feature = "axum")]
impl axum_core::response::IntoResponse for Error {
    fn into_response(self) -> axum_core::response::Response {
        axum_core::response::IntoResponse::into_response((
            http::StatusCode::from(&self),
            [("Content-Type", "application/json")],
            serde_json::to_string(&self).unwrap(),
        ))
    }
}