rosu_v2/model/
comments.rs

1use std::fmt;
2
3use serde::{Deserialize, Serializer};
4use time::OffsetDateTime;
5
6use crate::{prelude::Username, request::GetUser, Osu, OsuResult};
7
8use super::{serde_util, user::User, CacheUserFn, ContainedUsers};
9
10/// Represents an single comment.
11#[derive(Clone, Debug, Deserialize)]
12#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
13pub struct Comment {
14    /// the ID of the comment
15    #[serde(rename = "id")]
16    pub comment_id: u32,
17    /// ID of the object the comment is attached to
18    pub commentable_id: u32,
19    /// type of object the comment is attached to
20    pub commentable_type: String,
21    /// ISO 8601 date
22    #[serde(with = "serde_util::datetime")]
23    pub created_at: OffsetDateTime,
24    /// ISO 8601 date if the comment was deleted; `None`, otherwise
25    #[serde(
26        default,
27        skip_serializing_if = "Option::is_none",
28        with = "serde_util::option_datetime"
29    )]
30    pub deleted_at: Option<OffsetDateTime>,
31    /// ISO 8601 date if the comment was edited; `None`, otherwise
32    #[serde(
33        default,
34        skip_serializing_if = "Option::is_none",
35        with = "serde_util::option_datetime"
36    )]
37    pub edited_at: Option<OffsetDateTime>,
38    /// user id of the user that edited the post; `None`, otherwise
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub edited_by_id: Option<u32>,
41    /// username displayed on legacy comments
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub legacy_name: Option<Username>,
44    /// markdown of the comment's content
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub message: Option<String>,
47    /// html version of the comment's content
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub message_html: Option<String>,
50    /// ID of the comment's parent
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub parent_id: Option<u32>,
53    /// Pin status of the comment
54    pub pinned: bool,
55    /// number of replies to the comment
56    pub replies_count: u32,
57    /// ISO 8601 date
58    #[serde(with = "serde_util::datetime")]
59    pub updated_at: OffsetDateTime,
60    /// user ID of the poster
61    pub user_id: Option<u32>,
62    /// number of votes
63    pub votes_count: u32,
64}
65
66impl Comment {
67    /// Request the [`UserExtended`](crate::model::user::UserExtended) of a comment.
68    ///
69    /// Only works if `user_id` is Some.
70    #[inline]
71    pub fn get_user<'o>(&self, osu: &'o Osu) -> Option<GetUser<'o>> {
72        self.user_id.map(|id| osu.user(id))
73    }
74}
75
76impl PartialEq for Comment {
77    #[inline]
78    fn eq(&self, other: &Self) -> bool {
79        self.comment_id == other.comment_id && self.user_id == other.user_id
80    }
81}
82
83impl Eq for Comment {}
84
85/// Comments and related data.
86#[derive(Clone, Debug, Deserialize, PartialEq)]
87#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
88pub struct CommentBundle {
89    /// ID of the object the comment is attached to
90    pub commentable_meta: Vec<CommentableMeta>,
91    /// List of comments ordered according to `sort`
92    pub comments: Vec<Comment>,
93    #[serde(
94        default,
95        rename = "cursor_string",
96        skip_serializing_if = "Option::is_none"
97    )]
98    pub(crate) cursor: Option<Box<str>>,
99    /// If there are more comments or replies available
100    pub(crate) has_more: bool,
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub has_more_id: Option<u32>,
103    /// Related comments; e.g. parent comments and nested replies
104    pub included_comments: Vec<Comment>,
105    /// Pinned comments
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub pinned_comments: Option<Vec<Comment>>,
108    /// order of comments
109    pub sort: CommentSort,
110    /// Number of comments at the top level. Not returned for replies.
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub top_level_count: Option<u32>,
113    /// Total number of comments. Not retuned for replies.
114    #[serde(default, skip_serializing_if = "Option::is_none")]
115    pub total: Option<u32>,
116    /// is the current user watching the comment thread?
117    pub user_follow: bool,
118    /// IDs of the comments in the bundle the current user has upvoted
119    pub user_votes: Vec<u32>,
120    /// List of users related to the comments
121    pub users: Vec<User>,
122}
123
124impl CommentBundle {
125    /// Returns whether there is a next page of comments,
126    /// retrievable via [`get_next`](CommentBundle::get_next).
127    #[inline]
128    pub const fn has_more(&self) -> bool {
129        self.has_more
130    }
131
132    /// If [`has_more`](CommentBundle::has_more) is true, the API can provide
133    /// the next set of comments and this method will request them. Otherwise,
134    /// this method returns `None`.
135    #[inline]
136    pub async fn get_next(&self, osu: &Osu) -> Option<OsuResult<CommentBundle>> {
137        debug_assert!(self.has_more == self.cursor.is_some());
138
139        Some(osu.comments().cursor(self.cursor.as_deref()?).await)
140    }
141}
142
143impl ContainedUsers for CommentBundle {
144    fn apply_to_users(&self, f: impl CacheUserFn) {
145        self.users.apply_to_users(f);
146    }
147}
148
149/// Available orders for comments
150#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
151#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
152pub enum CommentSort {
153    /// Sort by date, newest first
154    #[serde(rename = "new")]
155    New,
156    /// Sort by date, oldest first
157    #[serde(rename = "old")]
158    Old,
159    /// Sort by vote count
160    #[serde(rename = "top")]
161    Top,
162}
163
164impl CommentSort {
165    pub const fn as_str(self) -> &'static str {
166        match self {
167            Self::New => "new",
168            Self::Old => "old",
169            Self::Top => "top",
170        }
171    }
172
173    #[allow(clippy::trivially_copy_pass_by_ref)]
174    pub(crate) fn serialize_as_query<S: Serializer>(
175        &self,
176        serializer: S,
177    ) -> Result<S::Ok, S::Error> {
178        serializer.serialize_str(self.as_str())
179    }
180}
181
182impl fmt::Display for CommentSort {
183    #[inline]
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        f.write_str(self.as_str())
186    }
187}
188
189/// Metadata of the object that a comment is attached to.
190#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
191#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
192#[serde(untagged)]
193pub enum CommentableMeta {
194    Full {
195        /// the ID of the object
196        id: u32,
197        /// the type of the object
198        #[serde(rename = "type")]
199        kind: String,
200        owner_id: u32,
201        owner_title: String,
202        /// display title
203        title: String,
204        /// url of the object
205        url: String,
206    },
207    Title {
208        /// display title
209        title: String,
210    },
211}