diff --git a/Cargo.lock b/Cargo.lock index 0c6d9570..c459f9c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2714,7 +2714,7 @@ dependencies = [ [[package]] name = "revolt-bonfire" -version = "1.0.6-patch.2" +version = "0.5.5" dependencies = [ "async-std", "async-tungstenite", @@ -2730,7 +2730,7 @@ dependencies = [ [[package]] name = "revolt-delta" -version = "0.5.4" +version = "0.5.5" dependencies = [ "async-channel", "async-std", diff --git a/crates/bonfire/Cargo.toml b/crates/bonfire/Cargo.toml index 3e7f7852..ba684e55 100644 --- a/crates/bonfire/Cargo.toml +++ b/crates/bonfire/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-bonfire" -version = "1.0.6-patch.2" +version = "0.5.5" license = "AGPL-3.0-or-later" edition = "2021" diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index 4a7ef196..f07167b3 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-delta" -version = "0.5.4" +version = "0.5.5" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] edition = "2018" diff --git a/crates/delta/src/routes/channels/message_clear_reactions.rs b/crates/delta/src/routes/channels/message_clear_reactions.rs new file mode 100644 index 00000000..683a225d --- /dev/null +++ b/crates/delta/src/routes/channels/message_clear_reactions.rs @@ -0,0 +1,34 @@ +use revolt_quark::{ + models::{message::PartialMessage, User}, + perms, Db, EmptyResponse, Permission, Ref, Result, +}; + +/// # Remove All Reactions from Message +/// +/// Remove your own, someone else's or all of a given reaction. +/// +/// Requires `ManageMessages` permission. +#[openapi(tag = "Interactions")] +#[delete("//messages//reactions")] +pub async fn clear_reactions(db: &Db, user: User, target: Ref, msg: Ref) -> Result { + let channel = target.as_channel(db).await?; + perms(&user) + .channel(&channel) + .throw_permission_and_view_channel(db, Permission::ManageMessages) + .await?; + + // Fetch relevant message + let mut message = msg.as_message_in(db, channel.id()).await?; + + // Clear reactions + message + .update( + db, + PartialMessage { + reactions: Some(Default::default()), + ..Default::default() + }, + ) + .await + .map(|_| EmptyResponse) +} diff --git a/crates/delta/src/routes/channels/message_react.rs b/crates/delta/src/routes/channels/message_react.rs new file mode 100644 index 00000000..4c76adcc --- /dev/null +++ b/crates/delta/src/routes/channels/message_react.rs @@ -0,0 +1,29 @@ +use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Result}; + +/// # Add Reaction to Message +/// +/// React to a given message. +#[openapi(tag = "Interactions")] +#[put("//messages//reactions/")] +pub async fn react_message( + db: &Db, + user: User, + target: Ref, + msg: Ref, + emoji: Ref, +) -> Result { + let channel = target.as_channel(db).await?; + perms(&user) + .channel(&channel) + .throw_permission_and_view_channel(db, Permission::React) + .await?; + + // Fetch relevant message + let message = msg.as_message_in(db, channel.id()).await?; + + // Add the reaction + message + .add_reaction(db, &user, &emoji.id) + .await + .map(|_| EmptyResponse) +} diff --git a/crates/delta/src/routes/channels/message_send.rs b/crates/delta/src/routes/channels/message_send.rs index 554d64ea..399e56b3 100644 --- a/crates/delta/src/routes/channels/message_send.rs +++ b/crates/delta/src/routes/channels/message_send.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; use revolt_quark::{ models::{ - message::{Masquerade, Reply, SendableEmbed}, + message::{Interactions, Masquerade, Reply, SendableEmbed}, Message, User, }, perms, @@ -40,6 +40,8 @@ pub struct DataMessageSend { /// Masquerade to apply to this message #[validate] masquerade: Option, + /// Information about how this message should be interacted with + interactions: Option, } lazy_static! { @@ -86,6 +88,7 @@ pub async fn message_send( channel: channel.id().to_string(), author: user.id.clone(), masquerade: data.masquerade, + interactions: data.interactions.unwrap_or_default(), ..Default::default() }; @@ -112,7 +115,10 @@ pub async fn message_send( } } - // 3. Verify replies are valid. + // 3. Ensure interactions information is correct + message.interactions.validate(db).await?; + + // 4. Verify replies are valid. let mut replies = HashSet::new(); if let Some(entries) = data.replies { if entries.len() > 5 { @@ -145,7 +151,7 @@ pub async fn message_send( .replace(replies.into_iter().collect::>()); } - // 4. Process included embeds. + // 5. Process included embeds. let mut embeds = vec![]; if let Some(sendable_embeds) = data.embeds { for sendable_embed in sendable_embeds { @@ -157,7 +163,7 @@ pub async fn message_send( message.embeds.replace(embeds); } - // 5. Add attachments to message. + // 6. Add attachments to message. let mut attachments = vec![]; if let Some(ids) = &data.attachments { if !ids.is_empty() { @@ -183,10 +189,10 @@ pub async fn message_send( message.attachments.replace(attachments); } - // 6. Set content + // 7. Set content message.content = data.content; - // 7. Pass-through nonce value for clients + // 8. Pass-through nonce value for clients message.nonce = Some(idempotency.into_key()); message.create(db, &channel, Some(&user)).await?; diff --git a/crates/delta/src/routes/channels/message_unreact.rs b/crates/delta/src/routes/channels/message_unreact.rs new file mode 100644 index 00000000..7a5d4dc0 --- /dev/null +++ b/crates/delta/src/routes/channels/message_unreact.rs @@ -0,0 +1,58 @@ +use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Result}; +use serde::{Deserialize, Serialize}; + +/// # Query Parameters +#[derive(Serialize, Deserialize, JsonSchema, FromForm)] +pub struct OptionsUnreact { + /// Remove a specific user's reaction + user_id: Option, + /// Remove all reactions + remove_all: Option, +} + +/// # Remove Reaction(s) to Message +/// +/// Remove your own, someone else's or all of a given reaction. +/// +/// Requires `ManageMessages` if changing others' reactions. +#[openapi(tag = "Interactions")] +#[delete("//messages//reactions/?")] +pub async fn unreact_message( + db: &Db, + user: User, + target: Ref, + msg: Ref, + emoji: Ref, + options: OptionsUnreact, +) -> Result { + let channel = target.as_channel(db).await?; + let mut permissions = perms(&user).channel(&channel); + permissions + .throw_permission_and_view_channel(db, Permission::React) + .await?; + + // Check if we need to escalate permissions + let remove_all = options.remove_all.unwrap_or_default(); + if options.user_id.is_some() || remove_all { + permissions + .throw_permission(db, Permission::ManageMessages) + .await?; + } + + // Fetch relevant message + let message = msg.as_message_in(db, channel.id()).await?; + + // Check if we should wipe all of this reaction + if remove_all { + return message + .clear_reaction(db, &emoji.id) + .await + .map(|_| EmptyResponse); + } + + // Remove the reaction + message + .remove_reaction(db, options.user_id.as_ref().unwrap_or(&user.id), &emoji.id) + .await + .map(|_| EmptyResponse) +} diff --git a/crates/delta/src/routes/channels/mod.rs b/crates/delta/src/routes/channels/mod.rs index 4e5fc8df..255660d4 100644 --- a/crates/delta/src/routes/channels/mod.rs +++ b/crates/delta/src/routes/channels/mod.rs @@ -11,13 +11,16 @@ mod group_remove_member; mod invite_create; mod members_fetch; mod message_bulk_delete; +mod message_clear_reactions; mod message_delete; mod message_edit; mod message_fetch; mod message_query; mod message_query_stale; +mod message_react; mod message_search; mod message_send; +mod message_unreact; mod permissions_set; mod permissions_set_default; mod voice_join; @@ -44,5 +47,8 @@ pub fn routes() -> (Vec, OpenApi) { voice_join::req, permissions_set::req, permissions_set_default::req, + message_react::react_message, + message_unreact::unreact_message, + message_clear_reactions::clear_reactions ] } diff --git a/crates/delta/src/routes/mod.rs b/crates/delta/src/routes/mod.rs index 716b8051..f50ef91f 100644 --- a/crates/delta/src/routes/mod.rs +++ b/crates/delta/src/routes/mod.rs @@ -80,6 +80,7 @@ fn custom_openapi_spec() -> OpenApi { "Channel Invites", "Channel Permissions", "Messaging", + "Interactions", "Groups", "Voice" ] diff --git a/crates/quark/assets/emojis.txt b/crates/quark/assets/emojis.txt new file mode 100644 index 00000000..bc8036e3 --- /dev/null +++ b/crates/quark/assets/emojis.txt @@ -0,0 +1,1850 @@ +๐Ÿ’ฏ +๐Ÿ”ข +๐Ÿ˜€ +๐Ÿ˜ƒ +๐Ÿ˜„ +๐Ÿ˜ +๐Ÿ˜† +๐Ÿ˜† +๐Ÿ˜… +๐Ÿคฃ +๐Ÿ˜‚ +๐Ÿ™‚ +๐Ÿ™ƒ +๐Ÿ˜‰ +๐Ÿ˜Š +๐Ÿ˜‡ +๐Ÿฅฐ +๐Ÿ˜ +๐Ÿคฉ +๐Ÿ˜˜ +๐Ÿ˜— +โ˜บ๏ธ +๐Ÿ˜š +๐Ÿ˜™ +๐Ÿฅฒ +๐Ÿ˜‹ +๐Ÿ˜› +๐Ÿ˜œ +๐Ÿคช +๐Ÿ˜ +๐Ÿค‘ +๐Ÿค— +๐Ÿคญ +๐Ÿคซ +๐Ÿค” +๐Ÿค +๐Ÿคจ +๐Ÿ˜ +๐Ÿ˜‘ +๐Ÿ˜ถ +๐Ÿ˜ +๐Ÿ˜’ +๐Ÿ™„ +๐Ÿ˜ฌ +๐Ÿคฅ +๐Ÿ˜Œ +๐Ÿ˜” +๐Ÿ˜ช +๐Ÿคค +๐Ÿ˜ด +๐Ÿ˜ท +๐Ÿค’ +๐Ÿค• +๐Ÿคข +๐Ÿคฎ +๐Ÿคง +๐Ÿฅต +๐Ÿฅถ +๐Ÿฅด +๐Ÿ˜ต +๐Ÿคฏ +๐Ÿค  +๐Ÿฅณ +๐Ÿฅธ +๐Ÿ˜Ž +๐Ÿค“ +๐Ÿง +๐Ÿ˜• +๐Ÿ˜Ÿ +๐Ÿ™ +โ˜น๏ธ +๐Ÿ˜ฎ +๐Ÿ˜ฏ +๐Ÿ˜ฒ +๐Ÿ˜ณ +๐Ÿฅบ +๐Ÿ˜ฆ +๐Ÿ˜ง +๐Ÿ˜จ +๐Ÿ˜ฐ +๐Ÿ˜ฅ +๐Ÿ˜ข +๐Ÿ˜ญ +๐Ÿ˜ฑ +๐Ÿ˜– +๐Ÿ˜ฃ +๐Ÿ˜ž +๐Ÿ˜“ +๐Ÿ˜ฉ +๐Ÿ˜ซ +๐Ÿฅฑ +๐Ÿ˜ค +๐Ÿ˜ก +๐Ÿ˜ก +๐Ÿ˜  +๐Ÿคฌ +๐Ÿ˜ˆ +๐Ÿ‘ฟ +๐Ÿ’€ +โ˜ ๏ธ +๐Ÿ’ฉ +๐Ÿ’ฉ +๐Ÿ’ฉ +๐Ÿคก +๐Ÿ‘น +๐Ÿ‘บ +๐Ÿ‘ป +๐Ÿ‘ฝ +๐Ÿ‘พ +๐Ÿค– +๐Ÿ˜บ +๐Ÿ˜ธ +๐Ÿ˜น +๐Ÿ˜ป +๐Ÿ˜ผ +๐Ÿ˜ฝ +๐Ÿ™€ +๐Ÿ˜ฟ +๐Ÿ˜พ +๐Ÿ™ˆ +๐Ÿ™‰ +๐Ÿ™Š +๐Ÿ’‹ +๐Ÿ’Œ +๐Ÿ’˜ +๐Ÿ’ +๐Ÿ’– +๐Ÿ’— +๐Ÿ’“ +๐Ÿ’ž +๐Ÿ’• +๐Ÿ’Ÿ +โฃ๏ธ +๐Ÿ’” +โค๏ธ +๐Ÿงก +๐Ÿ’› +๐Ÿ’š +๐Ÿ’™ +๐Ÿ’œ +๐ŸคŽ +๐Ÿ–ค +๐Ÿค +๐Ÿ’ข +๐Ÿ’ฅ +๐Ÿ’ฅ +๐Ÿ’ซ +๐Ÿ’ฆ +๐Ÿ’จ +๐Ÿ•ณ๏ธ +๐Ÿ’ฃ +๐Ÿ’ฌ +๐Ÿ‘๏ธโ€๐Ÿ—จ๏ธ +๐Ÿ—จ๏ธ +๐Ÿ—ฏ๏ธ +๐Ÿ’ญ +๐Ÿ’ค +๐Ÿ‘‹ +๐Ÿคš +๐Ÿ–๏ธ +โœ‹ +โœ‹ +๐Ÿ–– +๐Ÿ‘Œ +๐ŸคŒ +๐Ÿค +โœŒ๏ธ +๐Ÿคž +๐ŸคŸ +๐Ÿค˜ +๐Ÿค™ +๐Ÿ‘ˆ +๐Ÿ‘‰ +๐Ÿ‘† +๐Ÿ–• +๐Ÿ–• +๐Ÿ‘‡ +โ˜๏ธ +๐Ÿ‘ +๐Ÿ‘ +๐Ÿ‘Ž +๐Ÿ‘Ž +โœŠ +โœŠ +๐Ÿ‘Š +๐Ÿ‘Š +๐Ÿ‘Š +๐Ÿค› +๐Ÿคœ +๐Ÿ‘ +๐Ÿ™Œ +๐Ÿ‘ +๐Ÿคฒ +๐Ÿค +๐Ÿ™ +โœ๏ธ +๐Ÿ’… +๐Ÿคณ +๐Ÿ’ช +๐Ÿฆพ +๐Ÿฆฟ +๐Ÿฆต +๐Ÿฆถ +๐Ÿ‘‚ +๐Ÿฆป +๐Ÿ‘ƒ +๐Ÿง  +๐Ÿซ€ +๐Ÿซ +๐Ÿฆท +๐Ÿฆด +๐Ÿ‘€ +๐Ÿ‘๏ธ +๐Ÿ‘… +๐Ÿ‘„ +๐Ÿ‘ถ +๐Ÿง’ +๐Ÿ‘ฆ +๐Ÿ‘ง +๐Ÿง‘ +๐Ÿ‘ฑ +๐Ÿ‘จ +๐Ÿง” +๐Ÿ‘จโ€๐Ÿฆฐ +๐Ÿ‘จโ€๐Ÿฆฑ +๐Ÿ‘จโ€๐Ÿฆณ +๐Ÿ‘จโ€๐Ÿฆฒ +๐Ÿ‘ฉ +๐Ÿ‘ฉโ€๐Ÿฆฐ +๐Ÿง‘โ€๐Ÿฆฐ +๐Ÿ‘ฉโ€๐Ÿฆฑ +๐Ÿง‘โ€๐Ÿฆฑ +๐Ÿ‘ฉโ€๐Ÿฆณ +๐Ÿง‘โ€๐Ÿฆณ +๐Ÿ‘ฉโ€๐Ÿฆฒ +๐Ÿง‘โ€๐Ÿฆฒ +๐Ÿ‘ฑโ€โ™€๏ธ +๐Ÿ‘ฑโ€โ™€๏ธ +๐Ÿ‘ฑโ€โ™‚๏ธ +๐Ÿง“ +๐Ÿ‘ด +๐Ÿ‘ต +๐Ÿ™ +๐Ÿ™โ€โ™‚๏ธ +๐Ÿ™โ€โ™€๏ธ +๐Ÿ™Ž +๐Ÿ™Žโ€โ™‚๏ธ +๐Ÿ™Žโ€โ™€๏ธ +๐Ÿ™… +๐Ÿ™…โ€โ™‚๏ธ +๐Ÿ™…โ€โ™‚๏ธ +๐Ÿ™…โ€โ™€๏ธ +๐Ÿ™…โ€โ™€๏ธ +๐Ÿ™† +๐Ÿ™†โ€โ™‚๏ธ +๐Ÿ™†โ€โ™€๏ธ +๐Ÿ’ +๐Ÿ’ +๐Ÿ’โ€โ™‚๏ธ +๐Ÿ’โ€โ™‚๏ธ +๐Ÿ’โ€โ™€๏ธ +๐Ÿ’โ€โ™€๏ธ +๐Ÿ™‹ +๐Ÿ™‹โ€โ™‚๏ธ +๐Ÿ™‹โ€โ™€๏ธ +๐Ÿง +๐Ÿงโ€โ™‚๏ธ +๐Ÿงโ€โ™€๏ธ +๐Ÿ™‡ +๐Ÿ™‡โ€โ™‚๏ธ +๐Ÿ™‡โ€โ™€๏ธ +๐Ÿคฆ +๐Ÿคฆโ€โ™‚๏ธ +๐Ÿคฆโ€โ™€๏ธ +๐Ÿคท +๐Ÿคทโ€โ™‚๏ธ +๐Ÿคทโ€โ™€๏ธ +๐Ÿง‘โ€โš•๏ธ +๐Ÿ‘จโ€โš•๏ธ +๐Ÿ‘ฉโ€โš•๏ธ +๐Ÿง‘โ€๐ŸŽ“ +๐Ÿ‘จโ€๐ŸŽ“ +๐Ÿ‘ฉโ€๐ŸŽ“ +๐Ÿง‘โ€๐Ÿซ +๐Ÿ‘จโ€๐Ÿซ +๐Ÿ‘ฉโ€๐Ÿซ +๐Ÿง‘โ€โš–๏ธ +๐Ÿ‘จโ€โš–๏ธ +๐Ÿ‘ฉโ€โš–๏ธ +๐Ÿง‘โ€๐ŸŒพ +๐Ÿ‘จโ€๐ŸŒพ +๐Ÿ‘ฉโ€๐ŸŒพ +๐Ÿง‘โ€๐Ÿณ +๐Ÿ‘จโ€๐Ÿณ +๐Ÿ‘ฉโ€๐Ÿณ +๐Ÿง‘โ€๐Ÿ”ง +๐Ÿ‘จโ€๐Ÿ”ง +๐Ÿ‘ฉโ€๐Ÿ”ง +๐Ÿง‘โ€๐Ÿญ +๐Ÿ‘จโ€๐Ÿญ +๐Ÿ‘ฉโ€๐Ÿญ +๐Ÿง‘โ€๐Ÿ’ผ +๐Ÿ‘จโ€๐Ÿ’ผ +๐Ÿ‘ฉโ€๐Ÿ’ผ +๐Ÿง‘โ€๐Ÿ”ฌ +๐Ÿ‘จโ€๐Ÿ”ฌ +๐Ÿ‘ฉโ€๐Ÿ”ฌ +๐Ÿง‘โ€๐Ÿ’ป +๐Ÿ‘จโ€๐Ÿ’ป +๐Ÿ‘ฉโ€๐Ÿ’ป +๐Ÿง‘โ€๐ŸŽค +๐Ÿ‘จโ€๐ŸŽค +๐Ÿ‘ฉโ€๐ŸŽค +๐Ÿง‘โ€๐ŸŽจ +๐Ÿ‘จโ€๐ŸŽจ +๐Ÿ‘ฉโ€๐ŸŽจ +๐Ÿง‘โ€โœˆ๏ธ +๐Ÿ‘จโ€โœˆ๏ธ +๐Ÿ‘ฉโ€โœˆ๏ธ +๐Ÿง‘โ€๐Ÿš€ +๐Ÿ‘จโ€๐Ÿš€ +๐Ÿ‘ฉโ€๐Ÿš€ +๐Ÿง‘โ€๐Ÿš’ +๐Ÿ‘จโ€๐Ÿš’ +๐Ÿ‘ฉโ€๐Ÿš’ +๐Ÿ‘ฎ +๐Ÿ‘ฎ +๐Ÿ‘ฎโ€โ™‚๏ธ +๐Ÿ‘ฎโ€โ™€๏ธ +๐Ÿ•ต๏ธ +๐Ÿ•ต๏ธโ€โ™‚๏ธ +๐Ÿ•ต๏ธโ€โ™€๏ธ +๐Ÿ’‚ +๐Ÿ’‚โ€โ™‚๏ธ +๐Ÿ’‚โ€โ™€๏ธ +๐Ÿฅท +๐Ÿ‘ท +๐Ÿ‘ทโ€โ™‚๏ธ +๐Ÿ‘ทโ€โ™€๏ธ +๐Ÿคด +๐Ÿ‘ธ +๐Ÿ‘ณ +๐Ÿ‘ณโ€โ™‚๏ธ +๐Ÿ‘ณโ€โ™€๏ธ +๐Ÿ‘ฒ +๐Ÿง• +๐Ÿคต +๐Ÿคตโ€โ™‚๏ธ +๐Ÿคตโ€โ™€๏ธ +๐Ÿ‘ฐ +๐Ÿ‘ฐโ€โ™‚๏ธ +๐Ÿ‘ฐโ€โ™€๏ธ +๐Ÿ‘ฐโ€โ™€๏ธ +๐Ÿคฐ +๐Ÿคฑ +๐Ÿ‘ฉโ€๐Ÿผ +๐Ÿ‘จโ€๐Ÿผ +๐Ÿง‘โ€๐Ÿผ +๐Ÿ‘ผ +๐ŸŽ… +๐Ÿคถ +๐Ÿง‘โ€๐ŸŽ„ +๐Ÿฆธ +๐Ÿฆธโ€โ™‚๏ธ +๐Ÿฆธโ€โ™€๏ธ +๐Ÿฆน +๐Ÿฆนโ€โ™‚๏ธ +๐Ÿฆนโ€โ™€๏ธ +๐Ÿง™ +๐Ÿง™โ€โ™‚๏ธ +๐Ÿง™โ€โ™€๏ธ +๐Ÿงš +๐Ÿงšโ€โ™‚๏ธ +๐Ÿงšโ€โ™€๏ธ +๐Ÿง› +๐Ÿง›โ€โ™‚๏ธ +๐Ÿง›โ€โ™€๏ธ +๐Ÿงœ +๐Ÿงœโ€โ™‚๏ธ +๐Ÿงœโ€โ™€๏ธ +๐Ÿง +๐Ÿงโ€โ™‚๏ธ +๐Ÿงโ€โ™€๏ธ +๐Ÿงž +๐Ÿงžโ€โ™‚๏ธ +๐Ÿงžโ€โ™€๏ธ +๐ŸงŸ +๐ŸงŸโ€โ™‚๏ธ +๐ŸงŸโ€โ™€๏ธ +๐Ÿ’† +๐Ÿ’†โ€โ™‚๏ธ +๐Ÿ’†โ€โ™€๏ธ +๐Ÿ’‡ +๐Ÿ’‡โ€โ™‚๏ธ +๐Ÿ’‡โ€โ™€๏ธ +๐Ÿšถ +๐Ÿšถโ€โ™‚๏ธ +๐Ÿšถโ€โ™€๏ธ +๐Ÿง +๐Ÿงโ€โ™‚๏ธ +๐Ÿงโ€โ™€๏ธ +๐ŸงŽ +๐ŸงŽโ€โ™‚๏ธ +๐ŸงŽโ€โ™€๏ธ +๐Ÿง‘โ€๐Ÿฆฏ +๐Ÿ‘จโ€๐Ÿฆฏ +๐Ÿ‘ฉโ€๐Ÿฆฏ +๐Ÿง‘โ€๐Ÿฆผ +๐Ÿ‘จโ€๐Ÿฆผ +๐Ÿ‘ฉโ€๐Ÿฆผ +๐Ÿง‘โ€๐Ÿฆฝ +๐Ÿ‘จโ€๐Ÿฆฝ +๐Ÿ‘ฉโ€๐Ÿฆฝ +๐Ÿƒ +๐Ÿƒ +๐Ÿƒโ€โ™‚๏ธ +๐Ÿƒโ€โ™€๏ธ +๐Ÿ’ƒ +๐Ÿ’ƒ +๐Ÿ•บ +๐Ÿ•ด๏ธ +๐Ÿ‘ฏ +๐Ÿ‘ฏโ€โ™‚๏ธ +๐Ÿ‘ฏโ€โ™€๏ธ +๐Ÿง– +๐Ÿง–โ€โ™‚๏ธ +๐Ÿง–โ€โ™€๏ธ +๐Ÿง— +๐Ÿง—โ€โ™‚๏ธ +๐Ÿง—โ€โ™€๏ธ +๐Ÿคบ +๐Ÿ‡ +โ›ท๏ธ +๐Ÿ‚ +๐ŸŒ๏ธ +๐ŸŒ๏ธโ€โ™‚๏ธ +๐ŸŒ๏ธโ€โ™€๏ธ +๐Ÿ„ +๐Ÿ„โ€โ™‚๏ธ +๐Ÿ„โ€โ™€๏ธ +๐Ÿšฃ +๐Ÿšฃโ€โ™‚๏ธ +๐Ÿšฃโ€โ™€๏ธ +๐ŸŠ +๐ŸŠโ€โ™‚๏ธ +๐ŸŠโ€โ™€๏ธ +โ›น๏ธ +โ›น๏ธโ€โ™‚๏ธ +โ›น๏ธโ€โ™‚๏ธ +โ›น๏ธโ€โ™€๏ธ +โ›น๏ธโ€โ™€๏ธ +๐Ÿ‹๏ธ +๐Ÿ‹๏ธโ€โ™‚๏ธ +๐Ÿ‹๏ธโ€โ™€๏ธ +๐Ÿšด +๐Ÿšดโ€โ™‚๏ธ +๐Ÿšดโ€โ™€๏ธ +๐Ÿšต +๐Ÿšตโ€โ™‚๏ธ +๐Ÿšตโ€โ™€๏ธ +๐Ÿคธ +๐Ÿคธโ€โ™‚๏ธ +๐Ÿคธโ€โ™€๏ธ +๐Ÿคผ +๐Ÿคผโ€โ™‚๏ธ +๐Ÿคผโ€โ™€๏ธ +๐Ÿคฝ +๐Ÿคฝโ€โ™‚๏ธ +๐Ÿคฝโ€โ™€๏ธ +๐Ÿคพ +๐Ÿคพโ€โ™‚๏ธ +๐Ÿคพโ€โ™€๏ธ +๐Ÿคน +๐Ÿคนโ€โ™‚๏ธ +๐Ÿคนโ€โ™€๏ธ +๐Ÿง˜ +๐Ÿง˜โ€โ™‚๏ธ +๐Ÿง˜โ€โ™€๏ธ +๐Ÿ›€ +๐Ÿ›Œ +๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘ +๐Ÿ‘ญ +๐Ÿ‘ซ +๐Ÿ‘ฌ +๐Ÿ’ +๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ +๐Ÿ‘จโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ +๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ +๐Ÿ’‘ +๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ‘จ +๐Ÿ‘จโ€โค๏ธโ€๐Ÿ‘จ +๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ‘ฉ +๐Ÿ‘ช +๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ +๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง +๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ +๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ +๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง +๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ฆ +๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ง +๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ +๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ +๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ง +๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ +๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ง +๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ +๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ +๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง +๐Ÿ‘จโ€๐Ÿ‘ฆ +๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ +๐Ÿ‘จโ€๐Ÿ‘ง +๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ +๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ง +๐Ÿ‘ฉโ€๐Ÿ‘ฆ +๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ +๐Ÿ‘ฉโ€๐Ÿ‘ง +๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ +๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง +๐Ÿ—ฃ๏ธ +๐Ÿ‘ค +๐Ÿ‘ฅ +๐Ÿซ‚ +๐Ÿ‘ฃ +๐Ÿต +๐Ÿ’ +๐Ÿฆ +๐Ÿฆง +๐Ÿถ +๐Ÿ• +๐Ÿฆฎ +๐Ÿ•โ€๐Ÿฆบ +๐Ÿฉ +๐Ÿบ +๐ŸฆŠ +๐Ÿฆ +๐Ÿฑ +๐Ÿˆ +๐Ÿˆโ€โฌ› +๐Ÿฆ +๐Ÿฏ +๐Ÿ… +๐Ÿ† +๐Ÿด +๐ŸŽ +๐Ÿฆ„ +๐Ÿฆ“ +๐ŸฆŒ +๐Ÿฆฌ +๐Ÿฎ +๐Ÿ‚ +๐Ÿƒ +๐Ÿ„ +๐Ÿท +๐Ÿ– +๐Ÿ— +๐Ÿฝ +๐Ÿ +๐Ÿ‘ +๐Ÿ +๐Ÿช +๐Ÿซ +๐Ÿฆ™ +๐Ÿฆ’ +๐Ÿ˜ +๐Ÿฆฃ +๐Ÿฆ +๐Ÿฆ› +๐Ÿญ +๐Ÿ +๐Ÿ€ +๐Ÿน +๐Ÿฐ +๐Ÿ‡ +๐Ÿฟ๏ธ +๐Ÿฆซ +๐Ÿฆ” +๐Ÿฆ‡ +๐Ÿป +๐Ÿปโ€โ„๏ธ +๐Ÿจ +๐Ÿผ +๐Ÿฆฅ +๐Ÿฆฆ +๐Ÿฆจ +๐Ÿฆ˜ +๐Ÿฆก +๐Ÿพ +๐Ÿพ +๐Ÿฆƒ +๐Ÿ” +๐Ÿ“ +๐Ÿฃ +๐Ÿค +๐Ÿฅ +๐Ÿฆ +๐Ÿง +๐Ÿ•Š๏ธ +๐Ÿฆ… +๐Ÿฆ† +๐Ÿฆข +๐Ÿฆ‰ +๐Ÿฆค +๐Ÿชถ +๐Ÿฆฉ +๐Ÿฆš +๐Ÿฆœ +๐Ÿธ +๐ŸŠ +๐Ÿข +๐ŸฆŽ +๐Ÿ +๐Ÿฒ +๐Ÿ‰ +๐Ÿฆ• +๐Ÿฆ– +๐Ÿณ +๐Ÿ‹ +๐Ÿฌ +๐Ÿฌ +๐Ÿฆญ +๐ŸŸ +๐Ÿ  +๐Ÿก +๐Ÿฆˆ +๐Ÿ™ +๐Ÿš +๐ŸŒ +๐Ÿฆ‹ +๐Ÿ› +๐Ÿœ +๐Ÿ +๐Ÿ +๐Ÿชฒ +๐Ÿž +๐Ÿฆ— +๐Ÿชณ +๐Ÿ•ท๏ธ +๐Ÿ•ธ๏ธ +๐Ÿฆ‚ +๐ŸฆŸ +๐Ÿชฐ +๐Ÿชฑ +๐Ÿฆ  +๐Ÿ’ +๐ŸŒธ +๐Ÿ’ฎ +๐Ÿต๏ธ +๐ŸŒน +๐Ÿฅ€ +๐ŸŒบ +๐ŸŒป +๐ŸŒผ +๐ŸŒท +๐ŸŒฑ +๐Ÿชด +๐ŸŒฒ +๐ŸŒณ +๐ŸŒด +๐ŸŒต +๐ŸŒพ +๐ŸŒฟ +โ˜˜๏ธ +๐Ÿ€ +๐Ÿ +๐Ÿ‚ +๐Ÿƒ +๐Ÿ‡ +๐Ÿˆ +๐Ÿ‰ +๐ŸŠ +๐ŸŠ +๐ŸŠ +๐Ÿ‹ +๐ŸŒ +๐ŸŒ +๐Ÿ +๐Ÿฅญ +๐ŸŽ +๐Ÿ +๐Ÿ +๐Ÿ‘ +๐Ÿ’ +๐Ÿ“ +๐Ÿซ +๐Ÿฅ +๐Ÿ… +๐Ÿซ’ +๐Ÿฅฅ +๐Ÿฅ‘ +๐Ÿ† +๐Ÿฅ” +๐Ÿฅ• +๐ŸŒฝ +๐ŸŒถ๏ธ +๐Ÿซ‘ +๐Ÿฅ’ +๐Ÿฅฌ +๐Ÿฅฆ +๐Ÿง„ +๐Ÿง… +๐Ÿ„ +๐Ÿฅœ +๐ŸŒฐ +๐Ÿž +๐Ÿฅ +๐Ÿฅ– +๐Ÿซ“ +๐Ÿฅจ +๐Ÿฅฏ +๐Ÿฅž +๐Ÿง‡ +๐Ÿง€ +๐Ÿ– +๐Ÿ— +๐Ÿฅฉ +๐Ÿฅ“ +๐Ÿ” +๐ŸŸ +๐Ÿ• +๐ŸŒญ +๐Ÿฅช +๐ŸŒฎ +๐ŸŒฏ +๐Ÿซ” +๐Ÿฅ™ +๐Ÿง† +๐Ÿฅš +๐Ÿณ +๐Ÿฅ˜ +๐Ÿฒ +๐Ÿซ• +๐Ÿฅฃ +๐Ÿฅ— +๐Ÿฟ +๐Ÿงˆ +๐Ÿง‚ +๐Ÿฅซ +๐Ÿฑ +๐Ÿ˜ +๐Ÿ™ +๐Ÿš +๐Ÿ› +๐Ÿœ +๐Ÿ +๐Ÿ  +๐Ÿข +๐Ÿฃ +๐Ÿค +๐Ÿฅ +๐Ÿฅฎ +๐Ÿก +๐ŸฅŸ +๐Ÿฅ  +๐Ÿฅก +๐Ÿฆ€ +๐Ÿฆž +๐Ÿฆ +๐Ÿฆ‘ +๐Ÿฆช +๐Ÿฆ +๐Ÿง +๐Ÿจ +๐Ÿฉ +๐Ÿช +๐ŸŽ‚ +๐Ÿฐ +๐Ÿง +๐Ÿฅง +๐Ÿซ +๐Ÿฌ +๐Ÿญ +๐Ÿฎ +๐Ÿฏ +๐Ÿผ +๐Ÿฅ› +โ˜• +๐Ÿซ– +๐Ÿต +๐Ÿถ +๐Ÿพ +๐Ÿท +๐Ÿธ +๐Ÿน +๐Ÿบ +๐Ÿป +๐Ÿฅ‚ +๐Ÿฅƒ +๐Ÿฅค +๐Ÿง‹ +๐Ÿงƒ +๐Ÿง‰ +๐ŸงŠ +๐Ÿฅข +๐Ÿฝ๏ธ +๐Ÿด +๐Ÿฅ„ +๐Ÿ”ช +๐Ÿ”ช +๐Ÿบ +๐ŸŒ +๐ŸŒŽ +๐ŸŒ +๐ŸŒ +๐Ÿ—บ๏ธ +๐Ÿ—พ +๐Ÿงญ +๐Ÿ”๏ธ +โ›ฐ๏ธ +๐ŸŒ‹ +๐Ÿ—ป +๐Ÿ•๏ธ +๐Ÿ–๏ธ +๐Ÿœ๏ธ +๐Ÿ๏ธ +๐Ÿž๏ธ +๐ŸŸ๏ธ +๐Ÿ›๏ธ +๐Ÿ—๏ธ +๐Ÿงฑ +๐Ÿชจ +๐Ÿชต +๐Ÿ›– +๐Ÿ˜๏ธ +๐Ÿš๏ธ +๐Ÿ  +๐Ÿก +๐Ÿข +๐Ÿฃ +๐Ÿค +๐Ÿฅ +๐Ÿฆ +๐Ÿจ +๐Ÿฉ +๐Ÿช +๐Ÿซ +๐Ÿฌ +๐Ÿญ +๐Ÿฏ +๐Ÿฐ +๐Ÿ’’ +๐Ÿ—ผ +๐Ÿ—ฝ +โ›ช +๐Ÿ•Œ +๐Ÿ›• +๐Ÿ• +โ›ฉ๏ธ +๐Ÿ•‹ +โ›ฒ +โ›บ +๐ŸŒ +๐ŸŒƒ +๐Ÿ™๏ธ +๐ŸŒ„ +๐ŸŒ… +๐ŸŒ† +๐ŸŒ‡ +๐ŸŒ‰ +โ™จ๏ธ +๐ŸŽ  +๐ŸŽก +๐ŸŽข +๐Ÿ’ˆ +๐ŸŽช +๐Ÿš‚ +๐Ÿšƒ +๐Ÿš„ +๐Ÿš… +๐Ÿš† +๐Ÿš‡ +๐Ÿšˆ +๐Ÿš‰ +๐ŸšŠ +๐Ÿš +๐Ÿšž +๐Ÿš‹ +๐ŸšŒ +๐Ÿš +๐ŸšŽ +๐ŸšŽ +๐Ÿš +๐Ÿš‘ +๐Ÿš’ +๐Ÿš“ +๐Ÿš” +๐Ÿš• +๐Ÿš– +๐Ÿš— +๐Ÿš— +๐Ÿš˜ +๐Ÿš™ +๐Ÿ›ป +๐Ÿšš +๐Ÿš› +๐Ÿšœ +๐ŸŽ๏ธ +๐Ÿ๏ธ +๐Ÿ›ต +๐Ÿฆฝ +๐Ÿฆผ +๐Ÿ›บ +๐Ÿšฒ +๐Ÿ›ด +๐Ÿ›น +๐Ÿ›ผ +๐Ÿš +๐Ÿ›ฃ๏ธ +๐Ÿ›ค๏ธ +๐Ÿ›ข๏ธ +โ›ฝ +๐Ÿšจ +๐Ÿšฅ +๐Ÿšฆ +๐Ÿ›‘ +๐Ÿšง +โš“ +โ›ต +โ›ต +๐Ÿ›ถ +๐Ÿšค +๐Ÿ›ณ๏ธ +โ›ด๏ธ +๐Ÿ›ฅ๏ธ +๐Ÿšข +โœˆ๏ธ +๐Ÿ›ฉ๏ธ +๐Ÿ›ซ +๐Ÿ›ฌ +๐Ÿช‚ +๐Ÿ’บ +๐Ÿš +๐ŸšŸ +๐Ÿš  +๐Ÿšก +๐Ÿ›ฐ๏ธ +๐Ÿš€ +๐Ÿ›ธ +๐Ÿ›Ž๏ธ +๐Ÿงณ +โŒ› +โณ +โŒš +โฐ +โฑ๏ธ +โฒ๏ธ +๐Ÿ•ฐ๏ธ +๐Ÿ•› +๐Ÿ•ง +๐Ÿ• +๐Ÿ•œ +๐Ÿ•‘ +๐Ÿ• +๐Ÿ•’ +๐Ÿ•ž +๐Ÿ•“ +๐Ÿ•Ÿ +๐Ÿ•” +๐Ÿ•  +๐Ÿ•• +๐Ÿ•ก +๐Ÿ•– +๐Ÿ•ข +๐Ÿ•— +๐Ÿ•ฃ +๐Ÿ•˜ +๐Ÿ•ค +๐Ÿ•™ +๐Ÿ•ฅ +๐Ÿ•š +๐Ÿ•ฆ +๐ŸŒ‘ +๐ŸŒ’ +๐ŸŒ“ +๐ŸŒ” +๐ŸŒ” +๐ŸŒ• +๐ŸŒ– +๐ŸŒ— +๐ŸŒ˜ +๐ŸŒ™ +๐ŸŒš +๐ŸŒ› +๐ŸŒœ +๐ŸŒก๏ธ +โ˜€๏ธ +๐ŸŒ +๐ŸŒž +๐Ÿช +โญ +๐ŸŒŸ +๐ŸŒ  +๐ŸŒŒ +โ˜๏ธ +โ›… +โ›ˆ๏ธ +๐ŸŒค๏ธ +๐ŸŒฅ๏ธ +๐ŸŒฆ๏ธ +๐ŸŒง๏ธ +๐ŸŒจ๏ธ +๐ŸŒฉ๏ธ +๐ŸŒช๏ธ +๐ŸŒซ๏ธ +๐ŸŒฌ๏ธ +๐ŸŒ€ +๐ŸŒˆ +๐ŸŒ‚ +โ˜‚๏ธ +โ˜” +โ›ฑ๏ธ +โšก +โ„๏ธ +โ˜ƒ๏ธ +โ›„ +โ˜„๏ธ +๐Ÿ”ฅ +๐Ÿ’ง +๐ŸŒŠ +๐ŸŽƒ +๐ŸŽ„ +๐ŸŽ† +๐ŸŽ‡ +๐Ÿงจ +โœจ +๐ŸŽˆ +๐ŸŽ‰ +๐ŸŽŠ +๐ŸŽ‹ +๐ŸŽ +๐ŸŽŽ +๐ŸŽ +๐ŸŽ +๐ŸŽ‘ +๐Ÿงง +๐ŸŽ€ +๐ŸŽ +๐ŸŽ—๏ธ +๐ŸŽŸ๏ธ +๐ŸŽซ +๐ŸŽ–๏ธ +๐Ÿ† +๐Ÿ… +๐Ÿฅ‡ +๐Ÿฅˆ +๐Ÿฅ‰ +โšฝ +โšพ +๐ŸฅŽ +๐Ÿ€ +๐Ÿ +๐Ÿˆ +๐Ÿ‰ +๐ŸŽพ +๐Ÿฅ +๐ŸŽณ +๐Ÿ +๐Ÿ‘ +๐Ÿ’ +๐Ÿฅ +๐Ÿ“ +๐Ÿธ +๐ŸฅŠ +๐Ÿฅ‹ +๐Ÿฅ… +โ›ณ +โ›ธ๏ธ +๐ŸŽฃ +๐Ÿคฟ +๐ŸŽฝ +๐ŸŽฟ +๐Ÿ›ท +๐ŸฅŒ +๐ŸŽฏ +๐Ÿช€ +๐Ÿช +๐ŸŽฑ +๐Ÿ”ฎ +๐Ÿช„ +๐Ÿงฟ +๐ŸŽฎ +๐Ÿ•น๏ธ +๐ŸŽฐ +๐ŸŽฒ +๐Ÿงฉ +๐Ÿงธ +๐Ÿช… +๐Ÿช† +โ™ ๏ธ +โ™ฅ๏ธ +โ™ฆ๏ธ +โ™ฃ๏ธ +โ™Ÿ๏ธ +๐Ÿƒ +๐Ÿ€„ +๐ŸŽด +๐ŸŽญ +๐Ÿ–ผ๏ธ +๐ŸŽจ +๐Ÿงต +๐Ÿชก +๐Ÿงถ +๐Ÿชข +๐Ÿ‘“ +๐Ÿ•ถ๏ธ +๐Ÿฅฝ +๐Ÿฅผ +๐Ÿฆบ +๐Ÿ‘” +๐Ÿ‘• +๐Ÿ‘• +๐Ÿ‘– +๐Ÿงฃ +๐Ÿงค +๐Ÿงฅ +๐Ÿงฆ +๐Ÿ‘— +๐Ÿ‘˜ +๐Ÿฅป +๐Ÿฉฑ +๐Ÿฉฒ +๐Ÿฉณ +๐Ÿ‘™ +๐Ÿ‘š +๐Ÿ‘› +๐Ÿ‘œ +๐Ÿ‘ +๐Ÿ›๏ธ +๐ŸŽ’ +๐Ÿฉด +๐Ÿ‘ž +๐Ÿ‘ž +๐Ÿ‘Ÿ +๐Ÿฅพ +๐Ÿฅฟ +๐Ÿ‘  +๐Ÿ‘ก +๐Ÿฉฐ +๐Ÿ‘ข +๐Ÿ‘‘ +๐Ÿ‘’ +๐ŸŽฉ +๐ŸŽ“ +๐Ÿงข +๐Ÿช– +โ›‘๏ธ +๐Ÿ“ฟ +๐Ÿ’„ +๐Ÿ’ +๐Ÿ’Ž +๐Ÿ”‡ +๐Ÿ”ˆ +๐Ÿ”‰ +๐Ÿ”Š +๐Ÿ“ข +๐Ÿ“ฃ +๐Ÿ“ฏ +๐Ÿ”” +๐Ÿ”• +๐ŸŽผ +๐ŸŽต +๐ŸŽถ +๐ŸŽ™๏ธ +๐ŸŽš๏ธ +๐ŸŽ›๏ธ +๐ŸŽค +๐ŸŽง +๐Ÿ“ป +๐ŸŽท +๐Ÿช— +๐ŸŽธ +๐ŸŽน +๐ŸŽบ +๐ŸŽป +๐Ÿช• +๐Ÿฅ +๐Ÿช˜ +๐Ÿ“ฑ +๐Ÿ“ฒ +โ˜Ž๏ธ +โ˜Ž๏ธ +๐Ÿ“ž +๐Ÿ“Ÿ +๐Ÿ“  +๐Ÿ”‹ +๐Ÿ”Œ +๐Ÿ’ป +๐Ÿ–ฅ๏ธ +๐Ÿ–จ๏ธ +โŒจ๏ธ +๐Ÿ–ฑ๏ธ +๐Ÿ–ฒ๏ธ +๐Ÿ’ฝ +๐Ÿ’พ +๐Ÿ’ฟ +๐Ÿ“€ +๐Ÿงฎ +๐ŸŽฅ +๐ŸŽž๏ธ +๐Ÿ“ฝ๏ธ +๐ŸŽฌ +๐Ÿ“บ +๐Ÿ“ท +๐Ÿ“ธ +๐Ÿ“น +๐Ÿ“ผ +๐Ÿ” +๐Ÿ”Ž +๐Ÿ•ฏ๏ธ +๐Ÿ’ก +๐Ÿ”ฆ +๐Ÿฎ +๐Ÿฎ +๐Ÿช” +๐Ÿ“” +๐Ÿ“• +๐Ÿ“– +๐Ÿ“– +๐Ÿ“— +๐Ÿ“˜ +๐Ÿ“™ +๐Ÿ“š +๐Ÿ““ +๐Ÿ“’ +๐Ÿ“ƒ +๐Ÿ“œ +๐Ÿ“„ +๐Ÿ“ฐ +๐Ÿ—ž๏ธ +๐Ÿ“‘ +๐Ÿ”– +๐Ÿท๏ธ +๐Ÿ’ฐ +๐Ÿช™ +๐Ÿ’ด +๐Ÿ’ต +๐Ÿ’ถ +๐Ÿ’ท +๐Ÿ’ธ +๐Ÿ’ณ +๐Ÿงพ +๐Ÿ’น +โœ‰๏ธ +๐Ÿ“ง +๐Ÿ“ง +๐Ÿ“จ +๐Ÿ“ฉ +๐Ÿ“ค +๐Ÿ“ฅ +๐Ÿ“ฆ +๐Ÿ“ซ +๐Ÿ“ช +๐Ÿ“ฌ +๐Ÿ“ญ +๐Ÿ“ฎ +๐Ÿ—ณ๏ธ +โœ๏ธ +โœ’๏ธ +๐Ÿ–‹๏ธ +๐Ÿ–Š๏ธ +๐Ÿ–Œ๏ธ +๐Ÿ–๏ธ +๐Ÿ“ +๐Ÿ“ +๐Ÿ’ผ +๐Ÿ“ +๐Ÿ“‚ +๐Ÿ—‚๏ธ +๐Ÿ“… +๐Ÿ“† +๐Ÿ—’๏ธ +๐Ÿ—“๏ธ +๐Ÿ“‡ +๐Ÿ“ˆ +๐Ÿ“‰ +๐Ÿ“Š +๐Ÿ“‹ +๐Ÿ“Œ +๐Ÿ“ +๐Ÿ“Ž +๐Ÿ–‡๏ธ +๐Ÿ“ +๐Ÿ“ +โœ‚๏ธ +๐Ÿ—ƒ๏ธ +๐Ÿ—„๏ธ +๐Ÿ—‘๏ธ +๐Ÿ”’ +๐Ÿ”“ +๐Ÿ” +๐Ÿ” +๐Ÿ”‘ +๐Ÿ—๏ธ +๐Ÿ”จ +๐Ÿช“ +โ›๏ธ +โš’๏ธ +๐Ÿ› ๏ธ +๐Ÿ—ก๏ธ +โš”๏ธ +๐Ÿ”ซ +๐Ÿชƒ +๐Ÿน +๐Ÿ›ก๏ธ +๐Ÿชš +๐Ÿ”ง +๐Ÿช› +๐Ÿ”ฉ +โš™๏ธ +๐Ÿ—œ๏ธ +โš–๏ธ +๐Ÿฆฏ +๐Ÿ”— +โ›“๏ธ +๐Ÿช +๐Ÿงฐ +๐Ÿงฒ +๐Ÿชœ +โš—๏ธ +๐Ÿงช +๐Ÿงซ +๐Ÿงฌ +๐Ÿ”ฌ +๐Ÿ”ญ +๐Ÿ“ก +๐Ÿ’‰ +๐Ÿฉธ +๐Ÿ’Š +๐Ÿฉน +๐Ÿฉบ +๐Ÿšช +๐Ÿ›— +๐Ÿชž +๐ŸชŸ +๐Ÿ›๏ธ +๐Ÿ›‹๏ธ +๐Ÿช‘ +๐Ÿšฝ +๐Ÿช  +๐Ÿšฟ +๐Ÿ› +๐Ÿชค +๐Ÿช’ +๐Ÿงด +๐Ÿงท +๐Ÿงน +๐Ÿงบ +๐Ÿงป +๐Ÿชฃ +๐Ÿงผ +๐Ÿชฅ +๐Ÿงฝ +๐Ÿงฏ +๐Ÿ›’ +๐Ÿšฌ +โšฐ๏ธ +๐Ÿชฆ +โšฑ๏ธ +๐Ÿ—ฟ +๐Ÿชง +๐Ÿง +๐Ÿšฎ +๐Ÿšฐ +โ™ฟ +๐Ÿšน +๐Ÿšบ +๐Ÿšป +๐Ÿšผ +๐Ÿšพ +๐Ÿ›‚ +๐Ÿ›ƒ +๐Ÿ›„ +๐Ÿ›… +โš ๏ธ +๐Ÿšธ +โ›” +๐Ÿšซ +๐Ÿšณ +๐Ÿšญ +๐Ÿšฏ +๐Ÿšฑ +๐Ÿšท +๐Ÿ“ต +๐Ÿ”ž +โ˜ข๏ธ +โ˜ฃ๏ธ +โฌ†๏ธ +โ†—๏ธ +โžก๏ธ +โ†˜๏ธ +โฌ‡๏ธ +โ†™๏ธ +โฌ…๏ธ +โ†–๏ธ +โ†•๏ธ +โ†”๏ธ +โ†ฉ๏ธ +โ†ช๏ธ +โคด๏ธ +โคต๏ธ +๐Ÿ”ƒ +๐Ÿ”„ +๐Ÿ”™ +๐Ÿ”š +๐Ÿ”› +๐Ÿ”œ +๐Ÿ” +๐Ÿ› +โš›๏ธ +๐Ÿ•‰๏ธ +โœก๏ธ +โ˜ธ๏ธ +โ˜ฏ๏ธ +โœ๏ธ +โ˜ฆ๏ธ +โ˜ช๏ธ +โ˜ฎ๏ธ +๐Ÿ•Ž +๐Ÿ”ฏ +โ™ˆ +โ™‰ +โ™Š +โ™‹ +โ™Œ +โ™ +โ™Ž +โ™ +โ™ +โ™‘ +โ™’ +โ™“ +โ›Ž +๐Ÿ”€ +๐Ÿ” +๐Ÿ”‚ +โ–ถ๏ธ +โฉ +โญ๏ธ +โฏ๏ธ +โ—€๏ธ +โช +โฎ๏ธ +๐Ÿ”ผ +โซ +๐Ÿ”ฝ +โฌ +โธ๏ธ +โน๏ธ +โบ๏ธ +โ๏ธ +๐ŸŽฆ +๐Ÿ”… +๐Ÿ”† +๐Ÿ“ถ +๐Ÿ“ณ +๐Ÿ“ด +โ™€๏ธ +โ™‚๏ธ +โšง๏ธ +โœ–๏ธ +โž• +โž– +โž— +โ™พ๏ธ +โ€ผ๏ธ +โ‰๏ธ +โ“ +โ” +โ• +โ— +โ— +ใ€ฐ๏ธ +๐Ÿ’ฑ +๐Ÿ’ฒ +โš•๏ธ +โ™ป๏ธ +โšœ๏ธ +๐Ÿ”ฑ +๐Ÿ“› +๐Ÿ”ฐ +โญ• +โœ… +โ˜‘๏ธ +โœ”๏ธ +โŒ +โŽ +โžฐ +โžฟ +ใ€ฝ๏ธ +โœณ๏ธ +โœด๏ธ +โ‡๏ธ +ยฉ๏ธ +ยฎ๏ธ +โ„ข๏ธ +#๏ธโƒฃ +*๏ธโƒฃ +0๏ธโƒฃ +1๏ธโƒฃ +2๏ธโƒฃ +3๏ธโƒฃ +4๏ธโƒฃ +5๏ธโƒฃ +6๏ธโƒฃ +7๏ธโƒฃ +8๏ธโƒฃ +9๏ธโƒฃ +๐Ÿ”Ÿ +๐Ÿ”  +๐Ÿ”ก +๐Ÿ”ฃ +๐Ÿ”ค +๐Ÿ…ฐ๏ธ +๐Ÿ†Ž +๐Ÿ…ฑ๏ธ +๐Ÿ†‘ +๐Ÿ†’ +๐Ÿ†“ +โ„น๏ธ +๐Ÿ†” +โ“‚๏ธ +๐Ÿ†• +๐Ÿ†– +๐Ÿ…พ๏ธ +๐Ÿ†— +๐Ÿ…ฟ๏ธ +๐Ÿ†˜ +๐Ÿ†™ +๐Ÿ†š +๐Ÿˆ +๐Ÿˆ‚๏ธ +๐Ÿˆท๏ธ +๐Ÿˆถ +๐Ÿˆฏ +๐Ÿ‰ +๐Ÿˆน +๐Ÿˆš +๐Ÿˆฒ +๐Ÿ‰‘ +๐Ÿˆธ +๐Ÿˆด +๐Ÿˆณ +ใŠ—๏ธ +ใŠ™๏ธ +๐Ÿˆบ +๐Ÿˆต +๐Ÿ”ด +๐ŸŸ  +๐ŸŸก +๐ŸŸข +๐Ÿ”ต +๐ŸŸฃ +๐ŸŸค +โšซ +โšช +๐ŸŸฅ +๐ŸŸง +๐ŸŸจ +๐ŸŸฉ +๐ŸŸฆ +๐ŸŸช +๐ŸŸซ +โฌ› +โฌœ +โ—ผ๏ธ +โ—ป๏ธ +โ—พ +โ—ฝ +โ–ช๏ธ +โ–ซ๏ธ +๐Ÿ”ถ +๐Ÿ”ท +๐Ÿ”ธ +๐Ÿ”น +๐Ÿ”บ +๐Ÿ”ป +๐Ÿ’  +๐Ÿ”˜ +๐Ÿ”ณ +๐Ÿ”ฒ +๐Ÿ +๐Ÿšฉ +๐ŸŽŒ +๐Ÿด +๐Ÿณ๏ธ +๐Ÿณ๏ธโ€๐ŸŒˆ +๐Ÿณ๏ธโ€โšง๏ธ +๐Ÿดโ€โ˜ ๏ธ +๐Ÿ‡ฆ๐Ÿ‡จ +๐Ÿ‡ฆ๐Ÿ‡ฉ +๐Ÿ‡ฆ๐Ÿ‡ช +๐Ÿ‡ฆ๐Ÿ‡ซ +๐Ÿ‡ฆ๐Ÿ‡ฌ +๐Ÿ‡ฆ๐Ÿ‡ฎ +๐Ÿ‡ฆ๐Ÿ‡ฑ +๐Ÿ‡ฆ๐Ÿ‡ฒ +๐Ÿ‡ฆ๐Ÿ‡ด +๐Ÿ‡ฆ๐Ÿ‡ถ +๐Ÿ‡ฆ๐Ÿ‡ท +๐Ÿ‡ฆ๐Ÿ‡ธ +๐Ÿ‡ฆ๐Ÿ‡น +๐Ÿ‡ฆ๐Ÿ‡บ +๐Ÿ‡ฆ๐Ÿ‡ผ +๐Ÿ‡ฆ๐Ÿ‡ฝ +๐Ÿ‡ฆ๐Ÿ‡ฟ +๐Ÿ‡ง๐Ÿ‡ฆ +๐Ÿ‡ง๐Ÿ‡ง +๐Ÿ‡ง๐Ÿ‡ฉ +๐Ÿ‡ง๐Ÿ‡ช +๐Ÿ‡ง๐Ÿ‡ซ +๐Ÿ‡ง๐Ÿ‡ฌ +๐Ÿ‡ง๐Ÿ‡ญ +๐Ÿ‡ง๐Ÿ‡ฎ +๐Ÿ‡ง๐Ÿ‡ฏ +๐Ÿ‡ง๐Ÿ‡ฑ +๐Ÿ‡ง๐Ÿ‡ฒ +๐Ÿ‡ง๐Ÿ‡ณ +๐Ÿ‡ง๐Ÿ‡ด +๐Ÿ‡ง๐Ÿ‡ถ +๐Ÿ‡ง๐Ÿ‡ท +๐Ÿ‡ง๐Ÿ‡ธ +๐Ÿ‡ง๐Ÿ‡น +๐Ÿ‡ง๐Ÿ‡ป +๐Ÿ‡ง๐Ÿ‡ผ +๐Ÿ‡ง๐Ÿ‡พ +๐Ÿ‡ง๐Ÿ‡ฟ +๐Ÿ‡จ๐Ÿ‡ฆ +๐Ÿ‡จ๐Ÿ‡จ +๐Ÿ‡จ๐Ÿ‡ฉ +๐Ÿ‡จ๐Ÿ‡ซ +๐Ÿ‡จ๐Ÿ‡ฌ +๐Ÿ‡จ๐Ÿ‡ญ +๐Ÿ‡จ๐Ÿ‡ฎ +๐Ÿ‡จ๐Ÿ‡ฐ +๐Ÿ‡จ๐Ÿ‡ฑ +๐Ÿ‡จ๐Ÿ‡ฒ +๐Ÿ‡จ๐Ÿ‡ณ +๐Ÿ‡จ๐Ÿ‡ด +๐Ÿ‡จ๐Ÿ‡ต +๐Ÿ‡จ๐Ÿ‡ท +๐Ÿ‡จ๐Ÿ‡บ +๐Ÿ‡จ๐Ÿ‡ป +๐Ÿ‡จ๐Ÿ‡ผ +๐Ÿ‡จ๐Ÿ‡ฝ +๐Ÿ‡จ๐Ÿ‡พ +๐Ÿ‡จ๐Ÿ‡ฟ +๐Ÿ‡ฉ๐Ÿ‡ช +๐Ÿ‡ฉ๐Ÿ‡ฌ +๐Ÿ‡ฉ๐Ÿ‡ฏ +๐Ÿ‡ฉ๐Ÿ‡ฐ +๐Ÿ‡ฉ๐Ÿ‡ฒ +๐Ÿ‡ฉ๐Ÿ‡ด +๐Ÿ‡ฉ๐Ÿ‡ฟ +๐Ÿ‡ช๐Ÿ‡ฆ +๐Ÿ‡ช๐Ÿ‡จ +๐Ÿ‡ช๐Ÿ‡ช +๐Ÿ‡ช๐Ÿ‡ฌ +๐Ÿ‡ช๐Ÿ‡ญ +๐Ÿ‡ช๐Ÿ‡ท +๐Ÿ‡ช๐Ÿ‡ธ +๐Ÿ‡ช๐Ÿ‡น +๐Ÿ‡ช๐Ÿ‡บ +๐Ÿ‡ช๐Ÿ‡บ +๐Ÿ‡ซ๐Ÿ‡ฎ +๐Ÿ‡ซ๐Ÿ‡ฏ +๐Ÿ‡ซ๐Ÿ‡ฐ +๐Ÿ‡ซ๐Ÿ‡ฒ +๐Ÿ‡ซ๐Ÿ‡ด +๐Ÿ‡ซ๐Ÿ‡ท +๐Ÿ‡ฌ๐Ÿ‡ฆ +๐Ÿ‡ฌ๐Ÿ‡ง +๐Ÿ‡ฌ๐Ÿ‡ง +๐Ÿ‡ฌ๐Ÿ‡ฉ +๐Ÿ‡ฌ๐Ÿ‡ช +๐Ÿ‡ฌ๐Ÿ‡ซ +๐Ÿ‡ฌ๐Ÿ‡ฌ +๐Ÿ‡ฌ๐Ÿ‡ญ +๐Ÿ‡ฌ๐Ÿ‡ฎ +๐Ÿ‡ฌ๐Ÿ‡ฑ +๐Ÿ‡ฌ๐Ÿ‡ฒ +๐Ÿ‡ฌ๐Ÿ‡ณ +๐Ÿ‡ฌ๐Ÿ‡ต +๐Ÿ‡ฌ๐Ÿ‡ถ +๐Ÿ‡ฌ๐Ÿ‡ท +๐Ÿ‡ฌ๐Ÿ‡ธ +๐Ÿ‡ฌ๐Ÿ‡น +๐Ÿ‡ฌ๐Ÿ‡บ +๐Ÿ‡ฌ๐Ÿ‡ผ +๐Ÿ‡ฌ๐Ÿ‡พ +๐Ÿ‡ญ๐Ÿ‡ฐ +๐Ÿ‡ญ๐Ÿ‡ฒ +๐Ÿ‡ญ๐Ÿ‡ณ +๐Ÿ‡ญ๐Ÿ‡ท +๐Ÿ‡ญ๐Ÿ‡น +๐Ÿ‡ญ๐Ÿ‡บ +๐Ÿ‡ฎ๐Ÿ‡จ +๐Ÿ‡ฎ๐Ÿ‡ฉ +๐Ÿ‡ฎ๐Ÿ‡ช +๐Ÿ‡ฎ๐Ÿ‡ฑ +๐Ÿ‡ฎ๐Ÿ‡ฒ +๐Ÿ‡ฎ๐Ÿ‡ณ +๐Ÿ‡ฎ๐Ÿ‡ด +๐Ÿ‡ฎ๐Ÿ‡ถ +๐Ÿ‡ฎ๐Ÿ‡ท +๐Ÿ‡ฎ๐Ÿ‡ธ +๐Ÿ‡ฎ๐Ÿ‡น +๐Ÿ‡ฏ๐Ÿ‡ช +๐Ÿ‡ฏ๐Ÿ‡ฒ +๐Ÿ‡ฏ๐Ÿ‡ด +๐Ÿ‡ฏ๐Ÿ‡ต +๐Ÿ‡ฐ๐Ÿ‡ช +๐Ÿ‡ฐ๐Ÿ‡ฌ +๐Ÿ‡ฐ๐Ÿ‡ญ +๐Ÿ‡ฐ๐Ÿ‡ฎ +๐Ÿ‡ฐ๐Ÿ‡ฒ +๐Ÿ‡ฐ๐Ÿ‡ณ +๐Ÿ‡ฐ๐Ÿ‡ต +๐Ÿ‡ฐ๐Ÿ‡ท +๐Ÿ‡ฐ๐Ÿ‡ผ +๐Ÿ‡ฐ๐Ÿ‡พ +๐Ÿ‡ฐ๐Ÿ‡ฟ +๐Ÿ‡ฑ๐Ÿ‡ฆ +๐Ÿ‡ฑ๐Ÿ‡ง +๐Ÿ‡ฑ๐Ÿ‡จ +๐Ÿ‡ฑ๐Ÿ‡ฎ +๐Ÿ‡ฑ๐Ÿ‡ฐ +๐Ÿ‡ฑ๐Ÿ‡ท +๐Ÿ‡ฑ๐Ÿ‡ธ +๐Ÿ‡ฑ๐Ÿ‡น +๐Ÿ‡ฑ๐Ÿ‡บ +๐Ÿ‡ฑ๐Ÿ‡ป +๐Ÿ‡ฑ๐Ÿ‡พ +๐Ÿ‡ฒ๐Ÿ‡ฆ +๐Ÿ‡ฒ๐Ÿ‡จ +๐Ÿ‡ฒ๐Ÿ‡ฉ +๐Ÿ‡ฒ๐Ÿ‡ช +๐Ÿ‡ฒ๐Ÿ‡ซ +๐Ÿ‡ฒ๐Ÿ‡ฌ +๐Ÿ‡ฒ๐Ÿ‡ญ +๐Ÿ‡ฒ๐Ÿ‡ฐ +๐Ÿ‡ฒ๐Ÿ‡ฑ +๐Ÿ‡ฒ๐Ÿ‡ฒ +๐Ÿ‡ฒ๐Ÿ‡ณ +๐Ÿ‡ฒ๐Ÿ‡ด +๐Ÿ‡ฒ๐Ÿ‡ต +๐Ÿ‡ฒ๐Ÿ‡ถ +๐Ÿ‡ฒ๐Ÿ‡ท +๐Ÿ‡ฒ๐Ÿ‡ธ +๐Ÿ‡ฒ๐Ÿ‡น +๐Ÿ‡ฒ๐Ÿ‡บ +๐Ÿ‡ฒ๐Ÿ‡ป +๐Ÿ‡ฒ๐Ÿ‡ผ +๐Ÿ‡ฒ๐Ÿ‡ฝ +๐Ÿ‡ฒ๐Ÿ‡พ +๐Ÿ‡ฒ๐Ÿ‡ฟ +๐Ÿ‡ณ๐Ÿ‡ฆ +๐Ÿ‡ณ๐Ÿ‡จ +๐Ÿ‡ณ๐Ÿ‡ช +๐Ÿ‡ณ๐Ÿ‡ซ +๐Ÿ‡ณ๐Ÿ‡ฌ +๐Ÿ‡ณ๐Ÿ‡ฎ +๐Ÿ‡ณ๐Ÿ‡ฑ +๐Ÿ‡ณ๐Ÿ‡ด +๐Ÿ‡ณ๐Ÿ‡ต +๐Ÿ‡ณ๐Ÿ‡ท +๐Ÿ‡ณ๐Ÿ‡บ +๐Ÿ‡ณ๐Ÿ‡ฟ +๐Ÿ‡ด๐Ÿ‡ฒ +๐Ÿ‡ต๐Ÿ‡ฆ +๐Ÿ‡ต๐Ÿ‡ช +๐Ÿ‡ต๐Ÿ‡ซ +๐Ÿ‡ต๐Ÿ‡ฌ +๐Ÿ‡ต๐Ÿ‡ญ +๐Ÿ‡ต๐Ÿ‡ฐ +๐Ÿ‡ต๐Ÿ‡ฑ +๐Ÿ‡ต๐Ÿ‡ฒ +๐Ÿ‡ต๐Ÿ‡ณ +๐Ÿ‡ต๐Ÿ‡ท +๐Ÿ‡ต๐Ÿ‡ธ +๐Ÿ‡ต๐Ÿ‡น +๐Ÿ‡ต๐Ÿ‡ผ +๐Ÿ‡ต๐Ÿ‡พ +๐Ÿ‡ถ๐Ÿ‡ฆ +๐Ÿ‡ท๐Ÿ‡ช +๐Ÿ‡ท๐Ÿ‡ด +๐Ÿ‡ท๐Ÿ‡ธ +๐Ÿ‡ท๐Ÿ‡บ +๐Ÿ‡ท๐Ÿ‡ผ +๐Ÿ‡ธ๐Ÿ‡ฆ +๐Ÿ‡ธ๐Ÿ‡ง +๐Ÿ‡ธ๐Ÿ‡จ +๐Ÿ‡ธ๐Ÿ‡ฉ +๐Ÿ‡ธ๐Ÿ‡ช +๐Ÿ‡ธ๐Ÿ‡ฌ +๐Ÿ‡ธ๐Ÿ‡ญ +๐Ÿ‡ธ๐Ÿ‡ฎ +๐Ÿ‡ธ๐Ÿ‡ฏ +๐Ÿ‡ธ๐Ÿ‡ฐ +๐Ÿ‡ธ๐Ÿ‡ฑ +๐Ÿ‡ธ๐Ÿ‡ฒ +๐Ÿ‡ธ๐Ÿ‡ณ +๐Ÿ‡ธ๐Ÿ‡ด +๐Ÿ‡ธ๐Ÿ‡ท +๐Ÿ‡ธ๐Ÿ‡ธ +๐Ÿ‡ธ๐Ÿ‡น +๐Ÿ‡ธ๐Ÿ‡ป +๐Ÿ‡ธ๐Ÿ‡ฝ +๐Ÿ‡ธ๐Ÿ‡พ +๐Ÿ‡ธ๐Ÿ‡ฟ +๐Ÿ‡น๐Ÿ‡ฆ +๐Ÿ‡น๐Ÿ‡จ +๐Ÿ‡น๐Ÿ‡ฉ +๐Ÿ‡น๐Ÿ‡ซ +๐Ÿ‡น๐Ÿ‡ฌ +๐Ÿ‡น๐Ÿ‡ญ +๐Ÿ‡น๐Ÿ‡ฏ +๐Ÿ‡น๐Ÿ‡ฐ +๐Ÿ‡น๐Ÿ‡ฑ +๐Ÿ‡น๐Ÿ‡ฒ +๐Ÿ‡น๐Ÿ‡ณ +๐Ÿ‡น๐Ÿ‡ด +๐Ÿ‡น๐Ÿ‡ท +๐Ÿ‡น๐Ÿ‡น +๐Ÿ‡น๐Ÿ‡ป +๐Ÿ‡น๐Ÿ‡ผ +๐Ÿ‡น๐Ÿ‡ฟ +๐Ÿ‡บ๐Ÿ‡ฆ +๐Ÿ‡บ๐Ÿ‡ฌ +๐Ÿ‡บ๐Ÿ‡ฒ +๐Ÿ‡บ๐Ÿ‡ณ +๐Ÿ‡บ๐Ÿ‡ธ +๐Ÿ‡บ๐Ÿ‡พ +๐Ÿ‡บ๐Ÿ‡ฟ +๐Ÿ‡ป๐Ÿ‡ฆ +๐Ÿ‡ป๐Ÿ‡จ +๐Ÿ‡ป๐Ÿ‡ช +๐Ÿ‡ป๐Ÿ‡ฌ +๐Ÿ‡ป๐Ÿ‡ฎ +๐Ÿ‡ป๐Ÿ‡ณ +๐Ÿ‡ป๐Ÿ‡บ +๐Ÿ‡ผ๐Ÿ‡ซ +๐Ÿ‡ผ๐Ÿ‡ธ +๐Ÿ‡ฝ๐Ÿ‡ฐ +๐Ÿ‡พ๐Ÿ‡ช +๐Ÿ‡พ๐Ÿ‡น +๐Ÿ‡ฟ๐Ÿ‡ฆ +๐Ÿ‡ฟ๐Ÿ‡ฒ +๐Ÿ‡ฟ๐Ÿ‡ผ +๐Ÿด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ +๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ +๐Ÿด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ \ No newline at end of file diff --git a/crates/quark/src/events/client.rs b/crates/quark/src/events/client.rs index a46d9411..28f7eb6a 100644 --- a/crates/quark/src/events/client.rs +++ b/crates/quark/src/events/client.rs @@ -78,6 +78,29 @@ pub enum EventV1 { /// Delete message MessageDelete { id: String, channel: String }, + /// New reaction to a message + MessageReact { + id: String, + channel_id: String, + user_id: String, + emoji_id: String, + }, + + /// Remove user's reaction from message + MessageUnreact { + id: String, + channel_id: String, + user_id: String, + emoji_id: String, + }, + + /// Remove a reaction from message + MessageRemoveReaction { + id: String, + channel_id: String, + emoji_id: String, + }, + /// Bulk delete messages BulkMessageDelete { channel: String, ids: Vec }, diff --git a/crates/quark/src/impl/dummy/channels/message.rs b/crates/quark/src/impl/dummy/channels/message.rs index efee2c36..a01a32a1 100644 --- a/crates/quark/src/impl/dummy/channels/message.rs +++ b/crates/quark/src/impl/dummy/channels/message.rs @@ -64,4 +64,22 @@ impl AbstractMessage for DummyDb { ) -> Result> { Ok(vec![self.fetch_message(channel).await.unwrap()]) } + + /// Add a new reaction to a message + async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> { + info!("Add to {id} with {emoji} and {user}"); + Ok(()) + } + + /// Remove a reaction from a message + async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> { + info!("Remove {emoji} from {id} for {user}"); + Ok(()) + } + + /// Remove reaction from a message + async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> { + info!("Clear {emoji} on {id}"); + Ok(()) + } } diff --git a/crates/quark/src/impl/generic/channels/message.rs b/crates/quark/src/impl/generic/channels/message.rs index 2c7ead71..1352568a 100644 --- a/crates/quark/src/impl/generic/channels/message.rs +++ b/crates/quark/src/impl/generic/channels/message.rs @@ -8,9 +8,10 @@ use crate::{ events::client::EventV1, models::{ message::{ - AppendMessage, BulkMessageResponse, PartialMessage, SendableEmbed, SystemMessage, + AppendMessage, BulkMessageResponse, Interactions, PartialMessage, SendableEmbed, + SystemMessage, }, - Channel, Message, User, + Channel, Emoji, Message, User, }, presence::presence_filter_online, tasks::ack::AckEvent, @@ -191,6 +192,79 @@ impl Message { Err(Error::PayloadTooLarge) } } + + /// Add a reaction to a message + pub async fn add_reaction(&self, db: &Database, user: &User, emoji: &str) -> Result<()> { + // Check if the emoji is whitelisted + if !self.interactions.can_use(emoji) { + return Err(Error::InvalidOperation); + } + + // Check if the emoji is usable by us + if !Emoji::can_use(db, emoji).await? { + return Err(Error::InvalidOperation); + } + + // Send reaction event + EventV1::MessageReact { + id: self.id.to_string(), + channel_id: self.channel.to_string(), + user_id: user.id.to_string(), + emoji_id: emoji.to_string(), + } + .p(self.channel.to_string()) + .await; + + // Add emoji + db.add_reaction(&self.id, emoji, &user.id).await + } + + /// Remove a reaction from a message + pub async fn remove_reaction(&self, db: &Database, user: &str, emoji: &str) -> Result<()> { + // Check if it actually exists + let empty = if let Some(users) = self.reactions.get(emoji) { + if !users.contains(user) { + return Err(Error::NotFound); + } + + users.len() == 1 + } else { + return Err(Error::NotFound); + }; + + // Send reaction event + EventV1::MessageUnreact { + id: self.id.to_string(), + channel_id: self.channel.to_string(), + user_id: user.to_string(), + emoji_id: emoji.to_string(), + } + .p(self.channel.to_string()) + .await; + + if empty { + // If empty, remove the reaction entirely + db.clear_reaction(&self.id, emoji).await + } else { + // Otherwise only remove that one reaction + db.remove_reaction(&self.id, emoji, user).await + } + } + + /// Remove a reaction from a message + pub async fn clear_reaction(&self, db: &Database, emoji: &str) -> Result<()> { + // Send reaction event + EventV1::MessageRemoveReaction { + id: self.id.to_string(), + channel_id: self.channel.to_string(), + emoji_id: emoji.to_string(), + } + .p(self.channel.to_string()) + .await; + + // Write to database + db.clear_reaction(&self.id, emoji).await + } } pub trait IntoUsers { @@ -324,3 +398,40 @@ impl BulkMessageResponse { } } } + +impl Interactions { + /// Validate interactions info is correct + pub async fn validate(&self, db: &Database) -> Result<()> { + if let Some(reactions) = &self.reactions { + if reactions.len() > 20 { + return Err(Error::InvalidOperation); + } + + for reaction in reactions { + if !Emoji::can_use(db, reaction).await? { + return Err(Error::InvalidOperation); + } + } + } + + Ok(()) + } + + /// Check if we can use a given emoji to react + pub fn can_use(&self, emoji: &str) -> bool { + if self.restrict_reactions { + if let Some(reactions) = &self.reactions { + reactions.contains(emoji) + } else { + false + } + } else { + true + } + } + + /// Check if default initialisation of fields + pub fn is_default(&self) -> bool { + !self.restrict_reactions && self.reactions.is_none() + } +} diff --git a/crates/quark/src/impl/generic/media/emoji.rs b/crates/quark/src/impl/generic/media/emoji.rs index 145b7047..6c58b38a 100644 --- a/crates/quark/src/impl/generic/media/emoji.rs +++ b/crates/quark/src/impl/generic/media/emoji.rs @@ -1,9 +1,21 @@ +use std::{collections::HashSet, str::FromStr}; + +use ulid::Ulid; + use crate::{ events::client::EventV1, models::{emoji::EmojiParent, Emoji}, Database, Result, }; +lazy_static! { + /// Permissible emojis + static ref PERMISSIBLE_EMOJIS: HashSet = include_str!(crate::asset!("emojis.txt")) + .split('\n') + .map(|x| x.into()) + .collect(); +} + impl Emoji { /// Get parent id fn parent(&self) -> &str { @@ -33,4 +45,14 @@ impl Emoji { db.detach_emoji(&self).await } + + /// Check whether we can use a given emoji + pub async fn can_use(db: &Database, emoji: &str) -> Result { + if Ulid::from_str(emoji).is_ok() { + db.fetch_emoji(emoji).await?; + Ok(true) + } else { + Ok(PERMISSIBLE_EMOJIS.contains(emoji)) + } + } } diff --git a/crates/quark/src/impl/mongo/channels/message.rs b/crates/quark/src/impl/mongo/channels/message.rs index c411131b..0a1db531 100644 --- a/crates/quark/src/impl/mongo/channels/message.rs +++ b/crates/quark/src/impl/mongo/channels/message.rs @@ -279,4 +279,70 @@ impl AbstractMessage for MongoDb { ) .await } + + /// Add a new reaction to a message + async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> { + self.col::(COL) + .update_one( + doc! { + "_id": id + }, + doc! { + "$addToSet": { + format!("reactions.{emoji}"): user + } + }, + None, + ) + .await + .map(|_| ()) + .map_err(|_| Error::DatabaseError { + operation: "update_one", + with: "message", + }) + } + + /// Remove a reaction from a message + async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> { + self.col::(COL) + .update_one( + doc! { + "_id": id + }, + doc! { + "$pull": { + format!("reactions.{emoji}"): user + } + }, + None, + ) + .await + .map(|_| ()) + .map_err(|_| Error::DatabaseError { + operation: "update_one", + with: "message", + }) + } + + /// Remove reaction from a message + async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> { + self.col::(COL) + .update_one( + doc! { + "_id": id + }, + doc! { + "$unset": { + format!("reactions.{emoji}"): 1 + } + }, + None, + ) + .await + .map(|_| ()) + .map_err(|_| Error::DatabaseError { + operation: "update_one", + with: "message", + }) + } } diff --git a/crates/quark/src/models/channels/message.rs b/crates/quark/src/models/channels/message.rs index b3e094df..3630004f 100644 --- a/crates/quark/src/models/channels/message.rs +++ b/crates/quark/src/models/channels/message.rs @@ -1,4 +1,5 @@ use crate::util::regex::RE_COLOUR; +use std::collections::{HashMap, HashSet}; use serde::{Deserialize, Serialize}; use validator::Validate; @@ -13,6 +14,11 @@ use crate::{ types::january::Embed, }; +/// Utility function to check if a boolean value is false +pub fn if_false(t: &bool) -> bool { + !t +} + /// # Reply /// /// Representation of a message reply before it is sent. @@ -88,6 +94,17 @@ pub struct Masquerade { pub colour: Option, } +/// Information to guide interactions on this message +#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Validate, Default)] +pub struct Interactions { + /// Reactions which should always appear and be distinct + #[serde(skip_serializing_if = "Option::is_none", default)] + pub reactions: Option>, + /// Whether reactions should be restricted to the given list + #[serde(skip_serializing_if = "if_false", default)] + pub restrict_reactions: bool, +} + /// Representation of a Message on Revolt #[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, OptionalStruct, Default)] #[optional_derive(Serialize, Deserialize, JsonSchema, Debug, Default, Clone)] @@ -127,6 +144,12 @@ pub struct Message { /// Array of message ids this message is replying to #[serde(skip_serializing_if = "Option::is_none")] pub replies: Option>, + /// Hashmap of emoji IDs to array of user IDs + #[serde(skip_serializing_if = "HashMap::is_empty", default)] + pub reactions: HashMap>, + /// Information about how this message should be interacted with + #[serde(skip_serializing_if = "Interactions::is_default", default)] + pub interactions: Interactions, /// Name and / or avatar overrides for this message #[serde(skip_serializing_if = "Option::is_none")] pub masquerade: Option, diff --git a/crates/quark/src/permissions/defn/permission.rs b/crates/quark/src/permissions/defn/permission.rs index 86d74199..215710ad 100644 --- a/crates/quark/src/permissions/defn/permission.rs +++ b/crates/quark/src/permissions/defn/permission.rs @@ -65,8 +65,8 @@ pub enum Permission { UploadFiles = 1 << 27, /// Masquerade messages using custom nickname and avatar Masquerade = 1 << 28, - - // % 1 bits reserved + /// React to messages with emojis + React = 1 << 29, // * Voice permissions /// Connect to a voice channel diff --git a/crates/quark/src/traits/channels/message.rs b/crates/quark/src/traits/channels/message.rs index 0e4c8110..6f3f4f96 100644 --- a/crates/quark/src/traits/channels/message.rs +++ b/crates/quark/src/traits/channels/message.rs @@ -42,4 +42,13 @@ pub trait AbstractMessage: Sync + Send { after: Option, sort: MessageSort, ) -> Result>; + + /// Add a new reaction to a message + async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>; + + /// Remove a reaction from a message + async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>; + + /// Remove reaction from a message + async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()>; } diff --git a/crates/quark/src/util/ref.rs b/crates/quark/src/util/ref.rs index a6709d04..d010b4c1 100644 --- a/crates/quark/src/util/ref.rs +++ b/crates/quark/src/util/ref.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::models::{Bot, Channel, Emoji, Invite, Member, Message, Server, ServerBan, User}; use crate::presence::presence_is_online; -use crate::{Database, Result}; +use crate::{Database, Error, Result}; /// Reference to some object in the database #[derive(Serialize, Deserialize)] @@ -44,6 +44,16 @@ impl Ref { db.fetch_message(&self.id).await } + /// Fetch message in channel from Ref + pub async fn as_message_in(&self, db: &Database, channel: &str) -> Result { + let message = self.as_message(db).await?; + if message.channel != channel { + return Err(Error::NotFound); + } + + Ok(message) + } + /// Fetch bot from Ref pub async fn as_bot(&self, db: &Database) -> Result { db.fetch_bot(&self.id).await