diff --git a/Directory.Build.props b/Directory.Build.props index 0e31fcf..0751e1d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 0.1.24 + 0.1.31 diff --git a/Femto.Api/Auth/SessionAuthenticationHandler.cs b/Femto.Api/Auth/SessionAuthenticationHandler.cs index e71481d..9c44b04 100644 --- a/Femto.Api/Auth/SessionAuthenticationHandler.cs +++ b/Femto.Api/Auth/SessionAuthenticationHandler.cs @@ -3,11 +3,11 @@ using System.Text.Encodings.Web; using Femto.Api.Sessions; using Femto.Common; using Femto.Modules.Auth.Application; -using Femto.Modules.Auth.Application.Interface.ValidateSession; -using Femto.Modules.Auth.Errors; +using Femto.Modules.Auth.Application.Dto; +using Femto.Modules.Auth.Contracts; +using Femto.Modules.Auth.Models; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; -using Microsoft.OpenApi.Extensions; namespace Femto.Api.Auth; @@ -15,48 +15,115 @@ internal class SessionAuthenticationHandler( IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, - IAuthModule authModule, + IAuthService authService, CurrentUserContext currentUserContext ) : AuthenticationHandler(options, logger, encoder) { protected override async Task HandleAuthenticateAsync() { - var sessionId = this.Request.Cookies["session"]; - if (string.IsNullOrWhiteSpace(sessionId)) + Logger.LogDebug("{TraceId} Authenticating session", this.Context.TraceIdentifier); + + var user = await this.TryAuthenticateWithSession(); + + if (user is null) + user = await this.TryAuthenticateWithRememberMeToken(); + + if (user is null) return AuthenticateResult.NoResult(); - try + var claims = new List { - var result = await authModule.Command(new ValidateSessionCommand(sessionId)); + new(ClaimTypes.Name, user.Username), + new("sub", user.Id.ToString()), + new("user_id", user.Id.ToString()), + }; - var claims = new List - { - new(ClaimTypes.Name, result.User.Username), - new("sub", result.User.Id.ToString()), - new("user_id", result.User.Id.ToString()), - }; + claims.AddRange(user.Roles.Select(role => new Claim(ClaimTypes.Role, role.ToString()))); - claims.AddRange( - result.User.Roles.Select(role => new Claim(ClaimTypes.Role, role.ToString())) - ); + var identity = new ClaimsIdentity(claims, this.Scheme.Name); + var principal = new ClaimsPrincipal(identity); + currentUserContext.CurrentUser = new CurrentUser( + user.Id, + user.Username, + user.Roles.Contains(Role.SuperUser) + ); - var identity = new ClaimsIdentity(claims, this.Scheme.Name); - var principal = new ClaimsPrincipal(identity); + return AuthenticateResult.Success(new AuthenticationTicket(principal, this.Scheme.Name)); + } - this.Context.SetSession(result.Session, result.User, Logger); - currentUserContext.CurrentUser = new CurrentUser( - result.User.Id, - result.User.Username, - result.Session.SessionId - ); + private async Task TryAuthenticateWithSession() + { + var sessionId = this.Context.GetSessionId(); - return AuthenticateResult.Success( - new AuthenticationTicket(principal, this.Scheme.Name) - ); - } - catch (InvalidSessionError) + if (sessionId is null) { - return AuthenticateResult.Fail("Invalid session"); + Logger.LogDebug("{TraceId} SessionId was null ", this.Context.TraceIdentifier); + return null; } + + var session = await authService.GetSession(sessionId); + + if (session is null) + { + Logger.LogDebug("{TraceId} Loaded session was null ", this.Context.TraceIdentifier); + return null; + } + + if (session.IsExpired) + { + Logger.LogDebug("{TraceId} Loaded session was expired ", this.Context.TraceIdentifier); + await authService.DeleteSession(sessionId); + this.Context.DeleteSession(); + return null; + } + + var user = await authService.GetUserWithId(session.UserId); + + if (user is null) + { + await authService.DeleteSession(sessionId); + this.Context.DeleteSession(); + return null; + } + + if (session.ExpiresSoon) + { + session = await authService.CreateWeakSession(session.UserId); + this.Context.SetSession(session, user); + } + + return user; + } + + private async Task TryAuthenticateWithRememberMeToken() + { + /* + * load remember me from token + * if it is null, return null + * if it exists, validate it + * if it is valid, create a new weak session, return the user + * if it is almost expired, refresh it + */ + + var rememberMeToken = this.Context.GetRememberMeToken(); + + if (rememberMeToken is null) + return null; + + var (user, newRememberMeToken) = await authService.GetUserWithRememberMeToken( + rememberMeToken + ); + + if (user is null) + return null; + + var session = await authService.CreateWeakSession(user.Id); + + this.Context.SetSession(session, user); + + if (newRememberMeToken is not null) + this.Context.SetRememberMeToken(newRememberMeToken); + + return user; } } diff --git a/Femto.Api/Controllers/Auth/AuthController.cs b/Femto.Api/Controllers/Auth/AuthController.cs index b567d1d..c9108da 100644 --- a/Femto.Api/Controllers/Auth/AuthController.cs +++ b/Femto.Api/Controllers/Auth/AuthController.cs @@ -1,95 +1,174 @@ -using Femto.Api.Auth; using Femto.Api.Sessions; using Femto.Common; using Femto.Modules.Auth.Application; -using Femto.Modules.Auth.Application.Dto; -using Femto.Modules.Auth.Application.Interface.CreateSignupCode; -using Femto.Modules.Auth.Application.Interface.GetSignupCodesQuery; -using Femto.Modules.Auth.Application.Interface.Login; -using Femto.Modules.Auth.Application.Interface.RefreshUserSession; -using Femto.Modules.Auth.Application.Interface.Register; using Femto.Modules.Auth.Contracts; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; namespace Femto.Api.Controllers.Auth; [ApiController] [Route("auth")] -public class AuthController( - IAuthModule authModule, - IOptions cookieSettings, - ICurrentUserContext currentUserContext, - ILogger logger -) : ControllerBase +public class AuthController(ICurrentUserContext currentUserContext, IAuthService authService) + : ControllerBase { [HttpPost("login")] - public async Task> Login([FromBody] LoginRequest request) + public async Task> Login( + [FromBody] LoginRequest request, + CancellationToken cancellationToken + ) { - var result = await authModule.Command(new LoginCommand(request.Username, request.Password)); - - HttpContext.SetSession(result.Session, result.User, logger); - - return new LoginResponse( - result.User.Id, - result.User.Username, - result.User.Roles.Any(r => r == Role.SuperUser) + var result = await authService.AuthenticateUserCredentials( + request.Username, + request.Password, + cancellationToken ); + + if (result is null) + return this.BadRequest(); + + var (user, session) = result; + + HttpContext.SetSession(session, user); + + if (request.RememberMe) + { + var newRememberMeToken = await authService.CreateRememberMeToken(user.Id); + HttpContext.SetRememberMeToken(newRememberMeToken); + } + + return new LoginResponse(user.Id, user.Username, user.Roles.Any(r => r == Role.SuperUser)); } [HttpPost("register")] - public async Task> Register([FromBody] RegisterRequest request) + public async Task> Register( + [FromBody] RegisterRequest request, + CancellationToken cancellationToken + ) { - var result = await authModule.Command( - new RegisterCommand(request.Username, request.Password, request.SignupCode) + var (user, session) = await authService.CreateUserWithCredentials( + request.Username, + request.Password, + request.SignupCode, + cancellationToken ); - HttpContext.SetSession(result.Session, result.User, logger); + HttpContext.SetSession(session, user); + + if (request.RememberMe) + { + var newRememberMeToken = await authService.CreateRememberMeToken(user.Id); + HttpContext.SetRememberMeToken(newRememberMeToken); + } return new RegisterResponse( - result.User.Id, - result.User.Username, - result.User.Roles.Any(r => r == Role.SuperUser) + user.Id, + user.Username, + user.Roles.Any(r => r == Role.SuperUser) ); } + [HttpPost("change-password")] + public async Task ChangePassword( + [FromBody] ChangePasswordRequestBody req, + CancellationToken cancellationToken + ) + { + if (currentUserContext.CurrentUser is not {} user) + return this.BadRequest(); + + // superuser do what superuser want + if (!user.IsSuperUser) + { + if (user.Id != req.UserId) + return this.BadRequest(); + + var session = await authService.GetSession(this.HttpContext.GetSessionId()!); + + // require strong authentication to change password + // the user can re-enter their password + if (session is null || !session.IsStronglyAuthenticated) + return this.BadRequest(); + } + + await authService.ChangePassword(req.UserId, req.NewPassword, cancellationToken); + + // TODO would be better do handle this from inside the auth service. maybe just have it happen in a post-save event handler? + await authService.InvalidateUserSessions(req.UserId, cancellationToken); + + return this.Ok(new {}); + } + + [HttpPost("delete-current-session")] + public async Task DeleteSessionV2() + { + var sessionId = HttpContext.GetSessionId(); + + if (sessionId is not null) + { + await authService.DeleteSession(sessionId); + HttpContext.DeleteSession(); + } + + var rememberMeToken = HttpContext.GetRememberMeToken(); + + if (rememberMeToken is not null) + { + await authService.DeleteRememberMeToken(rememberMeToken); + HttpContext.DeleteRememberMeToken(); + } + + return Ok(new { }); + } + + [Obsolete("use POST /auth/delete-current-session")] [HttpDelete("session")] public async Task DeleteSession() { - HttpContext.DeleteSession(); + var sessionId = HttpContext.GetSessionId(); + + if (sessionId is not null) + { + await authService.DeleteSession(sessionId); + HttpContext.DeleteSession(); + } + + var rememberMeToken = HttpContext.GetRememberMeToken(); + + if (rememberMeToken is not null) + { + await authService.DeleteRememberMeToken(rememberMeToken); + HttpContext.DeleteRememberMeToken(); + } + return Ok(new { }); } [HttpGet("user/{userId}")] [Authorize] - public async Task> RefreshUser( + public async Task> GetUserInfo( Guid userId, CancellationToken cancellationToken ) { - var currentUser = currentUserContext.CurrentUser!; + var currentUser = currentUserContext.CurrentUser; - try - { - var result = await authModule.Command( - new RefreshUserSessionCommand(userId, currentUser), - cancellationToken - ); + if (currentUser is null || currentUser.Id != userId) + return this.BadRequest(); - return new RefreshUserResult( - result.User.Id, - result.User.Username, - result.User.Roles.Any(r => r == Role.SuperUser) - ); - } - catch (Exception) - { - HttpContext.DeleteSession(); - return this.Forbid(); - } + var user = await authService.GetUserWithId(userId, cancellationToken); + + if (user is null) + return this.BadRequest(); + + return new GetUserInfoResult( + user.Id, + user.Username, + user.Roles.Any(r => r == Role.SuperUser) + ); } + [Obsolete("use POST /auth/create-signup-code")] [HttpPost("signup-codes")] [Authorize(Roles = "SuperUser")] public async Task CreateSignupCode( @@ -97,21 +176,51 @@ public class AuthController( CancellationToken cancellationToken ) { - await authModule.Command( - new CreateSignupCodeCommand(request.Code, request.Email, request.Name), - cancellationToken - ); + await authService.AddSignupCode(request.Code, request.Name, cancellationToken); return Ok(new { }); } + [Obsolete("use GET /auth/list-signup-codes")] [HttpGet("signup-codes")] [Authorize(Roles = "SuperUser")] public async Task> ListSignupCodes( CancellationToken cancellationToken ) { - var codes = await authModule.Query(new GetSignupCodesQuery(), cancellationToken); + var codes = await authService.GetSignupCodes(cancellationToken); + + return new ListSignupCodesResult( + codes.Select(c => new SignupCodeDto( + c.Code, + c.Email, + c.Name, + c.RedeemedByUserId, + c.RedeemedByUsername, + c.ExpiresOn + )) + ); + } + + [HttpPost("create-signup-code")] + [Authorize(Roles = "SuperUser")] + public async Task CreateSignupCodeV2( + [FromBody] CreateSignupCodeRequest request, + CancellationToken cancellationToken + ) + { + await authService.AddSignupCode(request.Code, request.Name, cancellationToken); + + return Ok(new { }); + } + + [HttpGet("list-signup-codes")] + [Authorize(Roles = "SuperUser")] + public async Task> ListSignupCodesV2( + CancellationToken cancellationToken + ) + { + var codes = await authService.GetSignupCodes(cancellationToken); return new ListSignupCodesResult( codes.Select(c => new SignupCodeDto( diff --git a/Femto.Api/Controllers/Auth/ChangePasswordRequestBody.cs b/Femto.Api/Controllers/Auth/ChangePasswordRequestBody.cs new file mode 100644 index 0000000..77f1dcd --- /dev/null +++ b/Femto.Api/Controllers/Auth/ChangePasswordRequestBody.cs @@ -0,0 +1,3 @@ +namespace Femto.Api.Controllers.Auth; + +public record ChangePasswordRequestBody(Guid UserId, string NewPassword); \ No newline at end of file diff --git a/Femto.Api/Controllers/Auth/GetUserInfoResult.cs b/Femto.Api/Controllers/Auth/GetUserInfoResult.cs new file mode 100644 index 0000000..0212f32 --- /dev/null +++ b/Femto.Api/Controllers/Auth/GetUserInfoResult.cs @@ -0,0 +1,3 @@ +namespace Femto.Api.Controllers.Auth; + +public record GetUserInfoResult(Guid UserId, string Username, bool IsSuperUser); \ No newline at end of file diff --git a/Femto.Api/Controllers/Auth/LoginRequest.cs b/Femto.Api/Controllers/Auth/LoginRequest.cs index 8366d14..6c09e64 100644 --- a/Femto.Api/Controllers/Auth/LoginRequest.cs +++ b/Femto.Api/Controllers/Auth/LoginRequest.cs @@ -1,3 +1,3 @@ namespace Femto.Api.Controllers.Auth; -public record LoginRequest(string Username, string Password); \ No newline at end of file +public record LoginRequest(string Username, string Password, bool RememberMe); \ No newline at end of file diff --git a/Femto.Api/Controllers/Auth/RefreshUserResult.cs b/Femto.Api/Controllers/Auth/RefreshUserResult.cs deleted file mode 100644 index 8dbdee8..0000000 --- a/Femto.Api/Controllers/Auth/RefreshUserResult.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Femto.Api.Controllers.Auth; - -public record RefreshUserResult(Guid UserId, string Username, bool IsSuperUser); \ No newline at end of file diff --git a/Femto.Api/Controllers/Auth/RegisterRequest.cs b/Femto.Api/Controllers/Auth/RegisterRequest.cs index f386198..ee21297 100644 --- a/Femto.Api/Controllers/Auth/RegisterRequest.cs +++ b/Femto.Api/Controllers/Auth/RegisterRequest.cs @@ -1,3 +1,3 @@ namespace Femto.Api.Controllers.Auth; -public record RegisterRequest(string Username, string Password, string SignupCode, string? Email); \ No newline at end of file +public record RegisterRequest(string Username, string Password, string SignupCode, bool RememberMe); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/AddPostCommentRequest.cs b/Femto.Api/Controllers/Posts/Dto/AddPostCommentRequest.cs new file mode 100644 index 0000000..7546af0 --- /dev/null +++ b/Femto.Api/Controllers/Posts/Dto/AddPostCommentRequest.cs @@ -0,0 +1,3 @@ +namespace Femto.Api.Controllers.Posts.Dto; + +public record AddPostCommentRequest(Guid AuthorId, string Content); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/AddPostReactionRequest.cs b/Femto.Api/Controllers/Posts/Dto/AddPostReactionRequest.cs new file mode 100644 index 0000000..36330cf --- /dev/null +++ b/Femto.Api/Controllers/Posts/Dto/AddPostReactionRequest.cs @@ -0,0 +1,3 @@ +namespace Femto.Api.Controllers.Posts.Dto; + +public record AddPostReactionRequest(string Emoji); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/CreatePostResponse.cs b/Femto.Api/Controllers/Posts/Dto/CreatePostResponse.cs index a03dd93..1cec414 100644 --- a/Femto.Api/Controllers/Posts/Dto/CreatePostResponse.cs +++ b/Femto.Api/Controllers/Posts/Dto/CreatePostResponse.cs @@ -1,3 +1,3 @@ namespace Femto.Api.Controllers.Posts.Dto; -public record CreatePostResponse(Guid PostId); \ No newline at end of file +public record CreatePostResponse(PostDto Post); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/DeletePostReactionRequest.cs b/Femto.Api/Controllers/Posts/Dto/DeletePostReactionRequest.cs new file mode 100644 index 0000000..cb39e0e --- /dev/null +++ b/Femto.Api/Controllers/Posts/Dto/DeletePostReactionRequest.cs @@ -0,0 +1,3 @@ +namespace Femto.Api.Controllers.Posts.Dto; + +public record DeletePostReactionRequest(string Emoji); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/GetPostResponse.cs b/Femto.Api/Controllers/Posts/Dto/GetPostResponse.cs new file mode 100644 index 0000000..b44740c --- /dev/null +++ b/Femto.Api/Controllers/Posts/Dto/GetPostResponse.cs @@ -0,0 +1,3 @@ +namespace Femto.Api.Controllers.Posts.Dto; + +public record GetPostResponse(PostDto Post); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/GetPublicPostsSearchParams.cs b/Femto.Api/Controllers/Posts/Dto/GetPublicPostsSearchParams.cs index e5155f6..3705456 100644 --- a/Femto.Api/Controllers/Posts/Dto/GetPublicPostsSearchParams.cs +++ b/Femto.Api/Controllers/Posts/Dto/GetPublicPostsSearchParams.cs @@ -3,4 +3,4 @@ using JetBrains.Annotations; namespace Femto.Api.Controllers.Posts.Dto; [PublicAPI] -public record GetPublicPostsSearchParams(Guid? From, int? Amount, Guid? AuthorId, string? Author); \ No newline at end of file +public record GetPublicPostsSearchParams(Guid? After, int? Amount, Guid? AuthorId, string? Author); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/GetAllPublicPostsResponse.cs b/Femto.Api/Controllers/Posts/Dto/LoadPostsResponse.cs similarity index 51% rename from Femto.Api/Controllers/Posts/Dto/GetAllPublicPostsResponse.cs rename to Femto.Api/Controllers/Posts/Dto/LoadPostsResponse.cs index ededad1..54b9df7 100644 --- a/Femto.Api/Controllers/Posts/Dto/GetAllPublicPostsResponse.cs +++ b/Femto.Api/Controllers/Posts/Dto/LoadPostsResponse.cs @@ -3,4 +3,4 @@ using JetBrains.Annotations; namespace Femto.Api.Controllers.Posts.Dto; [PublicAPI] -public record GetAllPublicPostsResponse(IEnumerable Posts, Guid? Next); \ No newline at end of file +public record LoadPostsResponse(IEnumerable Posts); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/PostCommentDto.cs b/Femto.Api/Controllers/Posts/Dto/PostCommentDto.cs new file mode 100644 index 0000000..04e180a --- /dev/null +++ b/Femto.Api/Controllers/Posts/Dto/PostCommentDto.cs @@ -0,0 +1,3 @@ +namespace Femto.Api.Controllers.Posts.Dto; + +public record PostCommentDto(string Author, string Content, DateTimeOffset PostedOn); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/PostDto.cs b/Femto.Api/Controllers/Posts/Dto/PostDto.cs index c48014a..c9af7c6 100644 --- a/Femto.Api/Controllers/Posts/Dto/PostDto.cs +++ b/Femto.Api/Controllers/Posts/Dto/PostDto.cs @@ -8,5 +8,21 @@ public record PostDto( Guid PostId, string Content, IEnumerable Media, - DateTimeOffset CreatedAt -); \ No newline at end of file + IEnumerable Reactions, + DateTimeOffset CreatedAt, + IEnumerable PossibleReactions, + IEnumerable Comments +) +{ + public static PostDto FromModel(Modules.Blog.Application.Queries.GetPosts.Dto.PostDto post) => + new( + new PostAuthorDto(post.Author.AuthorId, post.Author.Username), + post.PostId, + post.Text, + post.Media.Select(m => new PostMediaDto(m.Url, m.Width, m.Height)), + post.Reactions.Select(r => new PostReactionDto(r.Emoji, r.AuthorName, r.ReactedOn)), + post.CreatedAt, + post.PossibleReactions, + post.Comments.Select(c => new PostCommentDto(c.Author, c.Content, c.PostedOn)) + ); +} \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/PostReactionDto.cs b/Femto.Api/Controllers/Posts/Dto/PostReactionDto.cs new file mode 100644 index 0000000..f9934c6 --- /dev/null +++ b/Femto.Api/Controllers/Posts/Dto/PostReactionDto.cs @@ -0,0 +1,3 @@ +namespace Femto.Api.Controllers.Posts.Dto; + +public record PostReactionDto(string Emoji, string AuthorName, DateTimeOffset ReactedOn); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/PostsController.cs b/Femto.Api/Controllers/Posts/PostsController.cs index 2b1e830..ed882f7 100644 --- a/Femto.Api/Controllers/Posts/PostsController.cs +++ b/Femto.Api/Controllers/Posts/PostsController.cs @@ -1,6 +1,9 @@ using Femto.Api.Controllers.Posts.Dto; using Femto.Common; using Femto.Modules.Blog.Application; +using Femto.Modules.Blog.Application.Commands.AddPostComment; +using Femto.Modules.Blog.Application.Commands.AddPostReaction; +using Femto.Modules.Blog.Application.Commands.ClearPostReaction; using Femto.Modules.Blog.Application.Commands.CreatePost; using Femto.Modules.Blog.Application.Commands.DeletePost; using Femto.Modules.Blog.Application.Queries.GetPosts; @@ -11,10 +14,11 @@ namespace Femto.Api.Controllers.Posts; [ApiController] [Route("posts")] -public class PostsController(IBlogModule blogModule, ICurrentUserContext currentUserContext) : ControllerBase +public class PostsController(IBlogModule blogModule, ICurrentUserContext currentUserContext, IAuthorizationService auth) + : ControllerBase { [HttpGet] - public async Task> LoadPosts( + public async Task> LoadPosts( [FromQuery] GetPublicPostsSearchParams searchParams, CancellationToken cancellationToken ) @@ -22,7 +26,7 @@ public class PostsController(IBlogModule blogModule, ICurrentUserContext current var res = await blogModule.Query( new GetPostsQuery(currentUserContext.CurrentUser?.Id) { - From = searchParams.From, + After = searchParams.After, Amount = searchParams.Amount ?? 20, AuthorId = searchParams.AuthorId, Author = searchParams.Author, @@ -30,16 +34,7 @@ public class PostsController(IBlogModule blogModule, ICurrentUserContext current cancellationToken ); - return new GetAllPublicPostsResponse( - res.Posts.Select(p => new PostDto( - new PostAuthorDto(p.Author.AuthorId, p.Author.Username), - p.PostId, - p.Text, - p.Media.Select(m => new PostMediaDto(m.Url, m.Width, m.Height)), - p.CreatedAt - )), - res.Next - ); + return new LoadPostsResponse(res.Posts.Select(PostDto.FromModel)); } [HttpPost] @@ -49,7 +44,7 @@ public class PostsController(IBlogModule blogModule, ICurrentUserContext current CancellationToken cancellationToken ) { - var guid = await blogModule.Command( + var post = await blogModule.Command( new CreatePostCommand( req.AuthorId, req.Content, @@ -64,18 +59,96 @@ public class PostsController(IBlogModule blogModule, ICurrentUserContext current media.Height ) ), - req.IsPublic + req.IsPublic, + currentUserContext.CurrentUser! ), cancellationToken ); - return new CreatePostResponse(guid); + return new CreatePostResponse(PostDto.FromModel(post)); + } + + [HttpGet("{postId}")] + public async Task> GetPost( + Guid postId, + CancellationToken cancellationToken + ) + { + var result = await blogModule.Query( + new GetPostsQuery(postId, currentUserContext.CurrentUser?.Id), + cancellationToken + ); + + var post = result.Posts.SingleOrDefault(); + + if (post is null) + return NotFound(); + + return new GetPostResponse(PostDto.FromModel(post)); } [HttpDelete("{postId}")] [Authorize] public async Task DeletePost(Guid postId, CancellationToken cancellationToken) { - await blogModule.Command(new DeletePostCommand(postId, currentUserContext.CurrentUser!.Id), cancellationToken); + await blogModule.Command( + new DeletePostCommand(postId, currentUserContext.CurrentUser!.Id), + cancellationToken + ); + } + + [HttpPost("{postId}/reactions")] + [Authorize] + public async Task AddPostReaction( + Guid postId, + [FromBody] AddPostReactionRequest request, + CancellationToken cancellationToken + ) + { + var currentUser = currentUserContext.CurrentUser!; + + await blogModule.Command( + new AddPostReactionCommand(postId, request.Emoji, currentUser.Id), + cancellationToken + ); + + return this.Ok(); + } + + [HttpDelete("{postId}/reactions")] + [Authorize] + public async Task DeletePostReaction( + Guid postId, + [FromBody] DeletePostReactionRequest request, + CancellationToken cancellationToken + ) + { + var currentUser = currentUserContext.CurrentUser!; + + await blogModule.Command( + new ClearPostReactionCommand(postId, request.Emoji, currentUser.Id), + cancellationToken + ); + + return this.Ok(); + } + + [HttpPost("{postId}/comments")] + [Authorize] + public async Task AddPostComment( + Guid postId, + [FromBody] AddPostCommentRequest request, + CancellationToken cancellationToken + ) + { + if (currentUserContext.CurrentUser?.Id != request.AuthorId) + return this.BadRequest(); + + await blogModule.Command( + new AddPostCommentCommand(postId, request.AuthorId, request.Content), + cancellationToken + ); + + return this.Ok(); } } diff --git a/Femto.Api/Program.cs b/Femto.Api/Program.cs index 08a40b8..ebb81fd 100644 --- a/Femto.Api/Program.cs +++ b/Femto.Api/Program.cs @@ -46,7 +46,7 @@ builder.Services.AddHostedService(_ => eventBus); builder.Services.InitializeBlogModule(connectionString, eventBus, loggerFactory); builder.Services.InitializeMediaModule(connectionString, blobStorageRoot); -builder.Services.InitializeAuthenticationModule(connectionString, eventBus, loggerFactory); +builder.Services.InitializeAuthenticationModule(connectionString, eventBus, loggerFactory, TimeProvider.System); builder.Services.AddScoped(); builder.Services.AddScoped(s => s.GetRequiredService()); diff --git a/Femto.Api/Properties/launchSettings.json b/Femto.Api/Properties/launchSettings.json index 9a9026a..b024278 100644 --- a/Femto.Api/Properties/launchSettings.json +++ b/Femto.Api/Properties/launchSettings.json @@ -14,7 +14,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "https://stinkpad:7269;http://0.0.0.0:5181", + "applicationUrl": "https://localhost:7269", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/Femto.Api/Sessions/HttpContextSessionExtensions.cs b/Femto.Api/Sessions/HttpContextSessionExtensions.cs index f5e5d25..2b8ee96 100644 --- a/Femto.Api/Sessions/HttpContextSessionExtensions.cs +++ b/Femto.Api/Sessions/HttpContextSessionExtensions.cs @@ -1,60 +1,52 @@ using System.Text.Json; -using System.Text.Json.Serialization; using Femto.Api.Auth; using Femto.Modules.Auth.Application.Dto; +using Femto.Modules.Auth.Models; using Microsoft.Extensions.Options; namespace Femto.Api.Sessions; +internal record SessionInfo(string? SessionId, Guid? UserId); + internal static class HttpContextSessionExtensions { - public static void SetSession(this HttpContext httpContext, Session session, UserInfo user, ILogger logger) + private static readonly JsonSerializerOptions JsonOptions = new() { - var cookieSettings = httpContext.RequestServices.GetService>(); - - var secure = cookieSettings?.Value.Secure ?? true; - var sameSite = cookieSettings?.Value.SameSite ?? SameSiteMode.Strict; - var domain = cookieSettings?.Value.Domain; - var expires = session.Expires; + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; - logger.LogInformation( - "cookie settings: Secure={Secure}, SameSite={SameSite}, domain={Domain}, Expires={Expires}", - secure, - sameSite, - domain, - expires - ); + public static string? GetSessionId(this HttpContext httpContext) => + httpContext.Request.Cookies["sid"]; - httpContext.Response.Cookies.Append( - "session", - session.SessionId, + public static void SetSession(this HttpContext context, Session session, UserInfo user) + { + var cookieSettings = context.RequestServices.GetRequiredService>(); + + context.Response.Cookies.Append( + "sid", + session.Id, new CookieOptions { + Path = "/", IsEssential = true, - Domain = domain, + Domain = cookieSettings.Value.Domain, HttpOnly = true, - Secure = secure, - SameSite = sameSite, - Expires = expires, + Secure = cookieSettings.Value.Secure, + SameSite = cookieSettings.Value.SameSite, + Expires = session.Expires, } ); - httpContext.Response.Cookies.Append( + context.Response.Cookies.Append( "user", - JsonSerializer.Serialize( - user, - new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - Converters = { new JsonStringEnumConverter() }, - } - ), + JsonSerializer.Serialize(user, JsonOptions), new CookieOptions { - Domain = domain, + Path = "/", + Domain = cookieSettings.Value.Domain, IsEssential = true, - Secure = secure, - SameSite = sameSite, + Secure = cookieSettings.Value.Secure, + SameSite = cookieSettings.Value.SameSite, Expires = session.Expires, } ); @@ -62,28 +54,78 @@ internal static class HttpContextSessionExtensions public static void DeleteSession(this HttpContext httpContext) { - var cookieSettings = httpContext.RequestServices.GetService>(); - - var secure = cookieSettings?.Value.Secure ?? true; - var sameSite = secure ? SameSiteMode.None : SameSiteMode.Unspecified; - var domain = cookieSettings?.Value.Domain; - - httpContext.Response.Cookies.Delete("session", new CookieOptions - { - HttpOnly = true, - Domain = domain, - IsEssential = true, - Secure = secure, - SameSite = sameSite, - Expires = DateTimeOffset.UtcNow.AddDays(-1), - }); - httpContext.Response.Cookies.Delete("user", new CookieOptions - { - Domain = domain, - IsEssential = true, - Secure = secure, - SameSite = sameSite, - Expires = DateTimeOffset.UtcNow.AddDays(-1), - }); + var cookieSettings = httpContext.RequestServices.GetRequiredService< + IOptions + >(); + + httpContext.Response.Cookies.Delete( + "sid", + new CookieOptions + { + Path = "/", + HttpOnly = true, + Domain = cookieSettings.Value.Domain, + IsEssential = true, + Secure = cookieSettings.Value.Secure, + SameSite = cookieSettings.Value.SameSite, + Expires = DateTimeOffset.UtcNow.AddDays(-1), + } + ); + + httpContext.Response.Cookies.Delete( + "user", + new CookieOptions + { + Path = "/", + Domain = cookieSettings.Value.Domain, + IsEssential = true, + Secure = cookieSettings.Value.Secure, + SameSite = cookieSettings.Value.SameSite, + Expires = DateTimeOffset.UtcNow.AddDays(-1), + } + ); + } + + + public static RememberMeToken? GetRememberMeToken(this HttpContext httpContext) => + httpContext.Request.Cookies["rid"] is { } code ? RememberMeToken.FromCode(code) : null; + + public static void SetRememberMeToken(this HttpContext context, NewRememberMeToken token) + { + var cookieSettings = context.RequestServices.GetRequiredService>(); + + context.Response.Cookies.Append( + "rid", + token.Code, + new CookieOptions + { + Path = "/", + IsEssential = true, + Domain = cookieSettings.Value.Domain, + HttpOnly = true, + Secure = cookieSettings.Value.Secure, + SameSite = cookieSettings.Value.SameSite, + Expires = token.Expires, + } + ); + } + + public static void DeleteRememberMeToken(this HttpContext context) + { + var cookieSettings = context.RequestServices.GetRequiredService>(); + + context.Response.Cookies.Delete( + "rid", + new CookieOptions + { + Path = "/", + HttpOnly = true, + Domain = cookieSettings.Value.Domain, + IsEssential = true, + Secure = cookieSettings.Value.Secure, + SameSite = cookieSettings.Value.SameSite, + Expires = DateTimeOffset.UtcNow.AddDays(-1), + } + ); } } diff --git a/Femto.Common/ICurrentUserContext.cs b/Femto.Common/ICurrentUserContext.cs index a7233e0..629b2d2 100644 --- a/Femto.Common/ICurrentUserContext.cs +++ b/Femto.Common/ICurrentUserContext.cs @@ -5,4 +5,4 @@ public interface ICurrentUserContext CurrentUser? CurrentUser { get; } } -public record CurrentUser(Guid Id, string Username, string SessionId); +public record CurrentUser(Guid Id, string Username, bool IsSuperUser); \ No newline at end of file diff --git a/Femto.Common/Infrastructure/SaveChangesPipelineBehaviour.cs b/Femto.Common/Infrastructure/DDDPipelineBehaviour.cs similarity index 69% rename from Femto.Common/Infrastructure/SaveChangesPipelineBehaviour.cs rename to Femto.Common/Infrastructure/DDDPipelineBehaviour.cs index d9aaf03..e5f338f 100644 --- a/Femto.Common/Infrastructure/SaveChangesPipelineBehaviour.cs +++ b/Femto.Common/Infrastructure/DDDPipelineBehaviour.cs @@ -5,10 +5,10 @@ using Microsoft.Extensions.Logging; namespace Femto.Common.Infrastructure; -public class SaveChangesPipelineBehaviour( +public class DDDPipelineBehaviour( DbContext context, IPublisher publisher, - ILogger> logger + ILogger> logger ) : IPipelineBehavior where TRequest : notnull { @@ -18,7 +18,12 @@ public class SaveChangesPipelineBehaviour( CancellationToken cancellationToken ) { + logger.LogDebug("handling request {Type}", typeof(TRequest).Name); var response = await next(cancellationToken); + + var hasChanges = context.ChangeTracker.HasChanges(); + logger.LogDebug("request handled. Changes? {HasChanges}", hasChanges); + if (context.ChangeTracker.HasChanges()) { await context.EmitDomainEvents(logger, publisher, cancellationToken); diff --git a/Femto.Common/Infrastructure/DomainServiceExtensions.cs b/Femto.Common/Infrastructure/DomainServiceExtensions.cs index e83469e..9812c93 100644 --- a/Femto.Common/Infrastructure/DomainServiceExtensions.cs +++ b/Femto.Common/Infrastructure/DomainServiceExtensions.cs @@ -12,7 +12,7 @@ public static class DomainServiceExtensions services.AddScoped(s => s.GetRequiredService()); services.AddTransient( typeof(IPipelineBehavior<,>), - typeof(SaveChangesPipelineBehaviour<,>) + typeof(DDDPipelineBehaviour<,>) ); } diff --git a/Femto.Common/ScopeBinding.cs b/Femto.Common/ScopeBinding.cs index c78408e..da58589 100644 --- a/Femto.Common/ScopeBinding.cs +++ b/Femto.Common/ScopeBinding.cs @@ -3,19 +3,24 @@ using Microsoft.Extensions.Logging; namespace Femto.Common; + /// /// We use this to bind a scope to the request scope in the composition root /// Any scoped services provided by this subcontainer should be accessed via a ScopeBinding injected in the host /// /// -public class ScopeBinding(IServiceScope scope) : IDisposable - where T : notnull +public class ScopeBinding(IServiceScope scope) : IDisposable { - public T GetService() { - return scope.ServiceProvider.GetRequiredService(); + private IServiceScope Scope { get; } = scope; + + public T GetService() + where T : notnull + { + return this.Scope.ServiceProvider.GetRequiredService(); } - public void Dispose() { - scope.Dispose(); + public virtual void Dispose() + { + this.Scope.Dispose(); } } diff --git a/Femto.Database/Migrations/20250518193113_AddUserRole.sql b/Femto.Database/Migrations/20250518193113_AddUserRole.sql index 8199a10..9febe66 100644 --- a/Femto.Database/Migrations/20250518193113_AddUserRole.sql +++ b/Femto.Database/Migrations/20250518193113_AddUserRole.sql @@ -6,4 +6,4 @@ CREATE TABLE authn.user_role user_id uuid REFERENCES authn.user_identity(id), role int, primary key (user_id, role) -); \ No newline at end of file +) \ No newline at end of file diff --git a/Femto.Database/Migrations/20250526220032_AddReactions.sql b/Femto.Database/Migrations/20250526220032_AddReactions.sql new file mode 100644 index 0000000..a99bbae --- /dev/null +++ b/Femto.Database/Migrations/20250526220032_AddReactions.sql @@ -0,0 +1,13 @@ +-- Migration: AddReactions +-- Created at: 26/05/2025 22:00:32 + +ALTER TABLE blog.post +ADD COLUMN possible_reactions TEXT; + +CREATE TABLE blog.post_reaction +( + post_id uuid REFERENCES blog.post(id), + author_id uuid REFERENCES blog.author(id), + emoji text not null, + primary key (post_id, author_id, emoji) +); \ No newline at end of file diff --git a/Femto.Database/Migrations/20250529101346_SessionsRework.sql b/Femto.Database/Migrations/20250529101346_SessionsRework.sql new file mode 100644 index 0000000..11cb84e --- /dev/null +++ b/Femto.Database/Migrations/20250529101346_SessionsRework.sql @@ -0,0 +1,13 @@ +-- Migration: addLongTermSessions +-- Created at: 29/05/2025 10:13:46 + +DROP TABLE authn.user_session; + +CREATE TABLE authn.long_term_session +( + id serial PRIMARY KEY, + selector varchar(16) NOT NULL, + hashed_verifier bytea NOT NULL, + expires timestamptz not null, + user_id uuid REFERENCES authn.user_identity (id) +); \ No newline at end of file diff --git a/Femto.Database/Migrations/20250719104200_AddInvalidateToLongTermSession.sql b/Femto.Database/Migrations/20250719104200_AddInvalidateToLongTermSession.sql new file mode 100644 index 0000000..15d0323 --- /dev/null +++ b/Femto.Database/Migrations/20250719104200_AddInvalidateToLongTermSession.sql @@ -0,0 +1,5 @@ +-- Migration: AddInvalidateToLongTermSession +-- Created at: 19/07/2025 10:42:00 + +ALTER TABLE authn.long_term_session +ADD COLUMN is_invalidated BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/Femto.Database/Migrations/20250810152132_AddTimestampToReaction.sql b/Femto.Database/Migrations/20250810152132_AddTimestampToReaction.sql new file mode 100644 index 0000000..4557156 --- /dev/null +++ b/Femto.Database/Migrations/20250810152132_AddTimestampToReaction.sql @@ -0,0 +1,4 @@ +-- Migration: AddTimestampToReaction +-- Created at: 10/08/2025 15:21:32 +alter table blog.post_reaction +add column created_at timestamptz; \ No newline at end of file diff --git a/Femto.Database/Migrations/20250810172242_AddCommentToPost.sql b/Femto.Database/Migrations/20250810172242_AddCommentToPost.sql new file mode 100644 index 0000000..44e0086 --- /dev/null +++ b/Femto.Database/Migrations/20250810172242_AddCommentToPost.sql @@ -0,0 +1,11 @@ +-- Migration: AddCommentToPost +-- Created at: 10/08/2025 17:22:42 + +CREATE TABLE blog.post_comment +( + id uuid PRIMARY KEY, + post_id uuid REFERENCES blog.post(id), + author_id uuid REFERENCES blog.author(id), + content TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +) \ No newline at end of file diff --git a/Femto.Database/Seed/TestDataSeeder.cs b/Femto.Database/Seed/TestDataSeeder.cs index 47d22aa..2c8efcc 100644 --- a/Femto.Database/Seed/TestDataSeeder.cs +++ b/Femto.Database/Seed/TestDataSeeder.cs @@ -35,14 +35,15 @@ public static class TestDataSeeder ; INSERT INTO blog.post - (id, author_id, content) + (id, author_id, possible_reactions, content) VALUES - ('019691a0-48ed-7eba-b8d3-608e25e07d4b', @id, 'However, authors often misinterpret the zoology as a smothered advantage, when in actuality it feels more like a blindfold accordion. They were lost without the chastest puppy that composed their Santa.'), - ('019691a0-4ace-7bb5-a8f3-e3362920eba0', @id, 'Extending this logic, a swim can hardly be considered a seasick duckling without also being a tornado. Some posit the whity voyage to be less than dippy.'), - ('019691a0-4c3e-726f-b8f6-bcbaabe789ae', @id,'Few can name a springless sun that isn''t a thudding Vietnam. The burn of a competitor becomes a frosted target.'), - ('019691a0-4dd3-7e89-909e-94a6fd19a05e', @id,'Some unwitched marbles are thought of simply as currencies. A boundary sees a nepal as a chordal railway.') + ('019691a0-48ed-7eba-b8d3-608e25e07d4b', @id, '["๐Ÿ†", "๐Ÿงข", "๐Ÿง‘๐Ÿพโ€๐ŸŽ“", "๐Ÿฅ•", "๐Ÿ•—"]', 'However, authors often misinterpret the zoology as a smothered advantage, when in actuality it feels more like a blindfold accordion. They were lost without the chastest puppy that composed their Santa.'), + ('019691a0-4ace-7bb5-a8f3-e3362920eba0', @id, '["๐Ÿ†", "๐Ÿงข", "๐Ÿง‘๐Ÿพโ€๐ŸŽ“", "๐Ÿฅ•", "๐Ÿ•—"]', 'Extending this logic, a swim can hardly be considered a seasick duckling without also being a tornado. Some posit the whity voyage to be less than dippy.'), + ('019691a0-4c3e-726f-b8f6-bcbaabe789ae', @id, '["๐Ÿ†", "๐Ÿงข", "๐Ÿง‘๐Ÿพโ€๐ŸŽ“", "๐Ÿฅ•", "๐Ÿ•—"]', 'Few can name a springless sun that isn''t a thudding Vietnam. The burn of a competitor becomes a frosted target.'), + ('019691a0-4dd3-7e89-909e-94a6fd19a05e', @id, '["๐Ÿ†", "๐Ÿงข", "๐Ÿง‘๐Ÿพโ€๐ŸŽ“", "๐Ÿฅ•", "๐Ÿ•—"]', 'Some unwitched marbles are thought of simply as currencies. A boundary sees a nepal as a chordal railway.') ; + INSERT INTO blog.post_media (id, post_id, url, ordering) VALUES @@ -54,6 +55,21 @@ public static class TestDataSeeder ('019691b6-2608-7088-8110-f0f6e35fa633', '019691a0-4dd3-7e89-909e-94a6fd19a05e', 'https://www.pinclipart.com/picdir/big/535-5356059_big-transparent-chungus-png-background-big-chungus-clipart.png', 0) ; + INSERT INTO blog.post_reaction + (post_id, author_id, emoji) + VALUES + ('019691a0-48ed-7eba-b8d3-608e25e07d4b', @id, '๐Ÿ†'), + ('019691a0-4ace-7bb5-a8f3-e3362920eba0', @id, '๐Ÿ†'), + ('019691a0-4c3e-726f-b8f6-bcbaabe789ae', @id, '๐Ÿง‘๐Ÿพโ€'), + ('019691a0-4c3e-726f-b8f6-bcbaabe789ae', @id, '๐Ÿ•—') + ; + + INSERT INTO blog.post_comment + (id, post_id, author_id, content) + VALUES + ('9116da05-49eb-4053-9199-57f54f92e73a', '019691a0-48ed-7eba-b8d3-608e25e07d4b', @id, 'this is a comment!') + ; + INSERT INTO authn.user_identity (id, username, password_hash, password_salt) VALUES diff --git a/Femto.Docs/Design/Auth/RememberMe.md b/Femto.Docs/Design/Auth/RememberMe.md new file mode 100644 index 0000000..0ff9ec2 --- /dev/null +++ b/Femto.Docs/Design/Auth/RememberMe.md @@ -0,0 +1,27 @@ +# Remember me + +We want to implement long lived sessions + +we will do this with a remember me cookie + +this should be implemented as so: + + +logging or registering and including a "rememberMe" flag with the request will generate a new remember me token, which can be stored as a cookie . + +the remember me token should live until: +* the user changes password anywhere +* the user logs out on that device +* the user logs in with an expired session, in which case the remember me token will be used to refresh the session, and then it will be swapped out for a new one + +that means we need to implement three spots: +- [ ] login +- [ ] register +- [ ] validate session + +we will implement it as described [here](https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence) + +we will only check the remember me token in "validate session". + +"refresh session" is only called with valid sessions so we do not need to check it here, as the session should already have been validated + diff --git a/Femto.Docs/Design/Auth/strong_vs_weak_session.md b/Femto.Docs/Design/Auth/strong_vs_weak_session.md new file mode 100644 index 0000000..5a45a7d --- /dev/null +++ b/Femto.Docs/Design/Auth/strong_vs_weak_session.md @@ -0,0 +1,16 @@ +# Strong vs weak sessions + +a **strong** session is one that should have the power to do account level admin tasks like change password + + +a **weak** session has strictly fewer privileges than a strong session + +## where to get a strong session + +a strong session is created when a user provides a username and a password. a session remains strong until it is refreshed, at which point it becomes weak. + +## where to get a weak session + +A weak session is any session that has not been directly created by user credentials, i.e.: +* short-term session refresh +* long-term session refresh \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/AuthModule.cs b/Femto.Modules.Auth/Application/AuthModule.cs deleted file mode 100644 index d289d9e..0000000 --- a/Femto.Modules.Auth/Application/AuthModule.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Femto.Common.Domain; -using MediatR; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace Femto.Modules.Auth.Application; - -internal class AuthModule(IMediator mediator) : IAuthModule -{ - public async Task Command(ICommand command, CancellationToken cancellationToken = default) => - await mediator.Send(command, cancellationToken); - - public async Task Command( - ICommand command, - CancellationToken cancellationToken = default - ) => await mediator.Send(command, cancellationToken); - - public async Task Query( - IQuery query, - CancellationToken cancellationToken = default - ) => await mediator.Send(query, cancellationToken); -} diff --git a/Femto.Modules.Auth/Application/AuthService.cs b/Femto.Modules.Auth/Application/AuthService.cs new file mode 100644 index 0000000..eb6215f --- /dev/null +++ b/Femto.Modules.Auth/Application/AuthService.cs @@ -0,0 +1,258 @@ +using Dapper; +using Femto.Common.Domain; +using Femto.Common.Infrastructure.DbConnection; +using Femto.Modules.Auth.Application.Dto; +using Femto.Modules.Auth.Data; +using Femto.Modules.Auth.Infrastructure; +using Femto.Modules.Auth.Models; +using Microsoft.EntityFrameworkCore; + +namespace Femto.Modules.Auth.Application; + +internal class AuthService( + AuthContext context, + SessionStorage sessionStorage, + IDbConnectionFactory connectionFactory +) : IAuthService +{ + public async Task AuthenticateUserCredentials( + string username, + string password, + CancellationToken cancellationToken = default + ) + { + var user = await context + .Users.Where(u => u.Username == username) + .SingleOrDefaultAsync(cancellationToken); + + if (user is null) + return null; + + if (!user.HasPassword(password)) + return null; + + var session = new Session(user.Id, true); + + await sessionStorage.AddSession(session); + + return new( + new UserInfo(user.Id, user.Username, user.Roles.Select(r => r.Role).ToList()), + session + ); + } + + public Task GetUserWithId(Guid? userId, CancellationToken cancellationToken) + { + return context + .Users.Where(u => u.Id == userId) + .Select(u => new UserInfo(u.Id, u.Username, u.Roles.Select(r => r.Role).ToList())) + .SingleOrDefaultAsync(cancellationToken); + } + + public async Task CreateNewSession(Guid userId) + { + var session = new Session(userId, true); + + await sessionStorage.AddSession(session); + + return session; + } + + public async Task CreateWeakSession(Guid userId) + { + var session = new Session(userId, false); + + await sessionStorage.AddSession(session); + + return session; + } + + public Task GetSession(string sessionId) + { + return sessionStorage.GetSession(sessionId); + } + + public async Task DeleteSession(string sessionId) + { + await sessionStorage.DeleteSession(sessionId); + } + + public async Task CreateUserWithCredentials( + string username, + string password, + string signupCode, + CancellationToken cancellationToken = default + ) + { + var now = DateTimeOffset.UtcNow; + + var code = await context + .SignupCodes.Where(c => c.Code == signupCode) + .Where(c => c.ExpiresAt == null || c.ExpiresAt > now) + .Where(c => c.RedeemingUserId == null) + .SingleOrDefaultAsync(cancellationToken); + + if (code is null) + throw new DomainError("invalid signup code"); + + var usernameTaken = await context.Users.AnyAsync( + u => u.Username == username, + cancellationToken + ); + + if (usernameTaken) + throw new DomainError("username taken"); + + var user = new UserIdentity(username); + + await context.AddAsync(user, cancellationToken); + + user.SetPassword(password); + + code.Redeem(user.Id); + + var session = new Session(user.Id, true); + + await sessionStorage.AddSession(session); + + await context.SaveChangesAsync(cancellationToken); + + return new(new UserInfo(user), session); + } + + public async Task AddSignupCode( + string code, + string recipientName, + CancellationToken cancellationToken + ) + { + await context.SignupCodes.AddAsync( + new SignupCode("", recipientName, code), + cancellationToken + ); + + await context.SaveChangesAsync(cancellationToken); + } + + public async Task> GetSignupCodes( + CancellationToken cancellationToken = default + ) + { + using var conn = connectionFactory.GetConnection(); + + // lang=sql + const string sql = """ + SELECT + sc.code as Code, + sc.recipient_email as Email, + sc.recipient_name as Name, + sc.redeeming_user_id as RedeemedByUserId, + u.username as RedeemedByUsername, + sc.expires_at as ExpiresOn + FROM authn.signup_code sc + LEFT JOIN authn.user_identity u ON u.id = sc.redeeming_user_id + ORDER BY sc.created_at DESC + """; + + var result = await conn.QueryAsync(sql, cancellationToken); + + return result + .Select(row => new SignupCodeDto( + row.Code, + row.Email, + row.Name, + row.RedeemedByUserId, + row.RedeemedByUsername, + row.ExpiresOn + )) + .ToList(); + } + + public async Task CreateRememberMeToken(Guid userId) + { + var (rememberMeToken, verifier) = LongTermSession.Create(userId); + + await context.AddAsync(rememberMeToken); + await context.SaveChangesAsync(); + + return new(rememberMeToken.Selector, verifier, rememberMeToken.Expires); + } + + public async Task<(UserInfo?, NewRememberMeToken?)> GetUserWithRememberMeToken( + RememberMeToken rememberMeToken + ) + { + var token = await context.LongTermSessions.SingleOrDefaultAsync(t => + t.Selector == rememberMeToken.Selector + ); + + if (token is null) + return (null, null); + + if (!token.CheckVerifier(rememberMeToken.Verifier)) + return (null, null); + + var user = await context.Users.SingleOrDefaultAsync(u => u.Id == token.UserId); + + if (user is null) + return (null, null); + + if (token.ExpiresSoon) + { + var (newToken, verifier) = LongTermSession.Create(user.Id); + await context.AddAsync(newToken); + await context.SaveChangesAsync(); + + return (new(user), new(newToken.Selector, verifier, newToken.Expires)); + } + + return (new(user), null); + } + + public async Task DeleteRememberMeToken(RememberMeToken rememberMeToken) + { + var session = await context.LongTermSessions.SingleOrDefaultAsync(s => + s.Selector == rememberMeToken.Selector + ); + + if (session is null) + return; + + if (!session.CheckVerifier(rememberMeToken.Verifier)) + return; + + context.Remove(session); + await context.SaveChangesAsync(); + } + + public async Task ChangePassword(Guid userId, string password, CancellationToken cancellationToken) + { + // change the password + // invalidate long term sessions + // invalidate sessions + + var user = await context.Users.SingleOrDefaultAsync(u => u.Id == userId,cancellationToken); + + if (user is null) + throw new DomainError("invalid user"); + + user.SetPassword(password); + + await context.SaveChangesAsync(cancellationToken); + } + + public async Task InvalidateUserSessions(Guid userId, CancellationToken cancellationToken) + { + await sessionStorage.InvalidateUserSessions(userId); + } + + private class GetSignupCodesQueryResultRow + { + public string Code { get; set; } + public string Email { get; set; } + public string Name { get; set; } + public Guid? RedeemedByUserId { get; set; } + public string? RedeemedByUsername { get; set; } + public DateTimeOffset? ExpiresOn { get; set; } + } +} diff --git a/Femto.Modules.Auth/Application/AuthStartup.cs b/Femto.Modules.Auth/Application/AuthStartup.cs index b9e6132..362e600 100644 --- a/Femto.Modules.Auth/Application/AuthStartup.cs +++ b/Femto.Modules.Auth/Application/AuthStartup.cs @@ -20,20 +20,29 @@ public static class AuthStartup this IServiceCollection rootContainer, string connectionString, IEventBus eventBus, - ILoggerFactory loggerFactory + ILoggerFactory loggerFactory, + TimeProvider timeProvider ) { var hostBuilder = Host.CreateDefaultBuilder(); + hostBuilder.ConfigureServices(services => - ConfigureServices(services, connectionString, eventBus, loggerFactory) + ConfigureServices(services, connectionString, eventBus, loggerFactory, timeProvider) ); + var host = hostBuilder.Build(); - rootContainer.AddScoped(_ => new ScopeBinding(host.Services.CreateScope())); - rootContainer.AddScoped(services => - services.GetRequiredService>().GetService() + rootContainer.AddKeyedScoped( + "AuthServiceScope", + (s, o) => + { + var scope = host.Services.CreateScope(); + return new ScopeBinding(scope); + } ); + rootContainer.ExposeScopedService(); + rootContainer.AddHostedService(services => new AuthApplication(host)); eventBus.Subscribe( (evt, cancellationToken) => EventSubscriber(evt, host.Services, cancellationToken) @@ -44,9 +53,12 @@ public static class AuthStartup IServiceCollection services, string connectionString, IEventPublisher publisher, - ILoggerFactory loggerFactory + ILoggerFactory loggerFactory, + TimeProvider timeProvider ) { + services.AddSingleton(timeProvider); + services.AddTransient(_ => new DbConnectionFactory(connectionString)); services.AddDbContext(builder => @@ -66,7 +78,7 @@ public static class AuthStartup { options.WaitForJobsToComplete = true; }); - + // #endif services.AddOutbox(); services.AddMediatR(c => c.RegisterServicesFromAssembly(typeof(AuthStartup).Assembly)); @@ -74,8 +86,11 @@ public static class AuthStartup services.ConfigureDomainServices(); services.AddSingleton(publisher); + services.AddSingleton(); - services.AddScoped(); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(SaveChangesPipelineBehaviour<,>)); + + services.AddScoped(); } private static async Task EventSubscriber( @@ -107,3 +122,14 @@ public static class AuthStartup } } } + +internal static class AuthServiceCollectionExtensions +{ + public static void ExposeScopedService(this IServiceCollection container) + where T : class + { + container.AddScoped(services => + services.GetRequiredKeyedService("AuthServiceScope").GetService() + ); + } +} diff --git a/Femto.Modules.Auth/Application/Dto/LoginResult.cs b/Femto.Modules.Auth/Application/Dto/LoginResult.cs index 1405a28..c9048ad 100644 --- a/Femto.Modules.Auth/Application/Dto/LoginResult.cs +++ b/Femto.Modules.Auth/Application/Dto/LoginResult.cs @@ -1,3 +1,3 @@ namespace Femto.Modules.Auth.Application.Dto; -public record LoginResult(Session Session, UserInfo User); \ No newline at end of file +public record LoginResult(SessionDto SessionDto, UserInfo User); \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Dto/RefreshUserSessionResult.cs b/Femto.Modules.Auth/Application/Dto/RefreshUserSessionResult.cs index ac1bbc3..19f1d17 100644 --- a/Femto.Modules.Auth/Application/Dto/RefreshUserSessionResult.cs +++ b/Femto.Modules.Auth/Application/Dto/RefreshUserSessionResult.cs @@ -1,3 +1,3 @@ namespace Femto.Modules.Auth.Application.Dto; -public record RefreshUserSessionResult(Session Session, UserInfo User); \ No newline at end of file +public record RefreshUserSessionResult(SessionDto SessionDto, UserInfo User); \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Dto/RegisterResult.cs b/Femto.Modules.Auth/Application/Dto/RegisterResult.cs index 13e1d12..e0a1243 100644 --- a/Femto.Modules.Auth/Application/Dto/RegisterResult.cs +++ b/Femto.Modules.Auth/Application/Dto/RegisterResult.cs @@ -1,3 +1,3 @@ namespace Femto.Modules.Auth.Application.Dto; -public record RegisterResult(Session Session, UserInfo User); \ No newline at end of file +public record RegisterResult(SessionDto SessionDto, UserInfo User); \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Dto/RememberMeToken.cs b/Femto.Modules.Auth/Application/Dto/RememberMeToken.cs new file mode 100644 index 0000000..5750b8d --- /dev/null +++ b/Femto.Modules.Auth/Application/Dto/RememberMeToken.cs @@ -0,0 +1,18 @@ +using Femto.Modules.Auth.Models; + +namespace Femto.Modules.Auth.Application.Dto; + +public record RememberMeToken(string Selector, string Verifier) +{ + public static RememberMeToken FromCode(string code) + { + var parts = code.Split('.'); + return new RememberMeToken(parts[0], parts[1]); + } + +}; + +public record NewRememberMeToken(string Selector, string Verifier, DateTimeOffset Expires) +{ + public string Code => $"{Selector}.{Verifier}"; +} diff --git a/Femto.Modules.Auth/Application/Dto/Session.cs b/Femto.Modules.Auth/Application/Dto/Session.cs index 9e87ca8..7f422eb 100644 --- a/Femto.Modules.Auth/Application/Dto/Session.cs +++ b/Femto.Modules.Auth/Application/Dto/Session.cs @@ -2,9 +2,16 @@ using Femto.Modules.Auth.Models; namespace Femto.Modules.Auth.Application.Dto; -public record Session(string SessionId, DateTimeOffset Expires) +public record SessionDto( + string SessionId, + DateTimeOffset Expires, + bool Weak, + string? RememberMe = null +) { - internal Session(UserSession session) : this(session.Id, session.Expires) - { - } -} \ No newline at end of file + internal SessionDto(Session session) + : this(session.Id, session.Expires, !session.IsStronglyAuthenticated) { } + + internal SessionDto(Session session, string? rememberMe) + : this(session.Id, session.Expires, !session.IsStronglyAuthenticated, rememberMe) { } +} diff --git a/Femto.Modules.Auth/Application/Dto/ValidateSessionResult.cs b/Femto.Modules.Auth/Application/Dto/ValidateSessionResult.cs index 7fb022f..e29c84a 100644 --- a/Femto.Modules.Auth/Application/Dto/ValidateSessionResult.cs +++ b/Femto.Modules.Auth/Application/Dto/ValidateSessionResult.cs @@ -1,3 +1,3 @@ namespace Femto.Modules.Auth.Application.Dto; -public record ValidateSessionResult(Session Session, UserInfo User); \ No newline at end of file +public record ValidateSessionResult(SessionDto SessionDto); \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/IAuthModule.cs b/Femto.Modules.Auth/Application/IAuthModule.cs deleted file mode 100644 index 4559161..0000000 --- a/Femto.Modules.Auth/Application/IAuthModule.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Femto.Common.Domain; - -namespace Femto.Modules.Auth.Application; - -public interface IAuthModule -{ - Task Command(ICommand command, CancellationToken cancellationToken = default); - Task Command(ICommand command, CancellationToken cancellationToken = default); - Task Query(IQuery query, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/IAuthService.cs b/Femto.Modules.Auth/Application/IAuthService.cs new file mode 100644 index 0000000..f656d95 --- /dev/null +++ b/Femto.Modules.Auth/Application/IAuthService.cs @@ -0,0 +1,45 @@ +using Femto.Modules.Auth.Application.Dto; +using Femto.Modules.Auth.Models; + +namespace Femto.Modules.Auth.Application; + +public interface IAuthService +{ + public Task AuthenticateUserCredentials( + string username, + string password, + CancellationToken cancellationToken = default + ); + public Task GetUserWithId( + Guid? userId, + CancellationToken cancellationToken = default + ); + public Task CreateNewSession(Guid userId); + public Task CreateWeakSession(Guid userId); + public Task GetSession(string sessionId); + public Task DeleteSession(string sessionId); + + public Task CreateUserWithCredentials(string username, + string password, + string signupCode, + CancellationToken cancellationToken = default); + + public Task AddSignupCode( + string code, + string recipientName, + CancellationToken cancellationToken = default + ); + + public Task> GetSignupCodes( + CancellationToken cancellationToken = default + ); + + Task CreateRememberMeToken(Guid userId); + Task<(UserInfo?, NewRememberMeToken?)> GetUserWithRememberMeToken(RememberMeToken rememberMeToken); + Task DeleteRememberMeToken(RememberMeToken rememberMeToken); + + Task ChangePassword(Guid userId, string password, CancellationToken cancellationToken = default); + Task InvalidateUserSessions(Guid userId, CancellationToken cancellationToken = default); +} + +public record UserAndSession(UserInfo User, Session Session); \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Interface/CreateSignupCode/CreateSignupCodeCommand.cs b/Femto.Modules.Auth/Application/Interface/CreateSignupCode/CreateSignupCodeCommand.cs deleted file mode 100644 index be24aa9..0000000 --- a/Femto.Modules.Auth/Application/Interface/CreateSignupCode/CreateSignupCodeCommand.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Femto.Common.Domain; - -namespace Femto.Modules.Auth.Application.Interface.CreateSignupCode; - -public record CreateSignupCodeCommand(string Code, string RecipientEmail, string RecipientName): ICommand; \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Interface/CreateSignupCode/CreateSignupCodeCommandHandler.cs b/Femto.Modules.Auth/Application/Interface/CreateSignupCode/CreateSignupCodeCommandHandler.cs deleted file mode 100644 index cfbb44a..0000000 --- a/Femto.Modules.Auth/Application/Interface/CreateSignupCode/CreateSignupCodeCommandHandler.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Femto.Common.Domain; -using Femto.Modules.Auth.Data; -using Femto.Modules.Auth.Models; - -namespace Femto.Modules.Auth.Application.Interface.CreateSignupCode; - -internal class CreateSignupCodeCommandHandler(AuthContext context) : ICommandHandler -{ - public async Task Handle(CreateSignupCodeCommand command, CancellationToken cancellationToken) - { - var code = new SignupCode(command.RecipientEmail, command.RecipientName, command.Code); - - await context.SignupCodes.AddAsync(code, cancellationToken); - } -} diff --git a/Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQuery.cs b/Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQuery.cs deleted file mode 100644 index 422a09d..0000000 --- a/Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQuery.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Femto.Common.Domain; -using Femto.Modules.Auth.Application.Dto; - -namespace Femto.Modules.Auth.Application.Interface.GetSignupCodesQuery; - -public record GetSignupCodesQuery: IQuery>; \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQueryHandler.cs b/Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQueryHandler.cs deleted file mode 100644 index 201fdce..0000000 --- a/Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQueryHandler.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Dapper; -using Femto.Common.Domain; -using Femto.Common.Infrastructure.DbConnection; -using Femto.Modules.Auth.Application.Dto; - -namespace Femto.Modules.Auth.Application.Interface.GetSignupCodesQuery; - -public class GetSignupCodesQueryHandler(IDbConnectionFactory connectionFactory) - : IQueryHandler> -{ - public async Task> Handle( - GetSignupCodesQuery request, - CancellationToken cancellationToken - ) - { - using var conn = connectionFactory.GetConnection(); - - // lang=sql - const string sql = """ - SELECT - sc.code as Code, - sc.recipient_email as Email, - sc.recipient_name as Name, - sc.redeeming_user_id as RedeemedByUserId, - u.username as RedeemedByUsername, - sc.expires_at as ExpiresOn - FROM authn.signup_code sc - LEFT JOIN authn.user_identity u ON u.id = sc.redeeming_user_id - ORDER BY sc.created_at DESC - """; - - var result = await conn.QueryAsync(sql); - - return result - .Select(row => new SignupCodeDto( - row.Code, - row.Email, - row.Name, - row.RedeemedByUserId, - row.RedeemedByUsername, - row.ExpiresOn - )) - .ToList(); - } - - private class QueryResultRow - { - public string Code { get; set; } - public string Email { get; set; } - public string Name { get; set; } - public Guid? RedeemedByUserId { get; set; } - public string? RedeemedByUsername { get; set; } - public DateTimeOffset? ExpiresOn { get; set; } - } -} diff --git a/Femto.Modules.Auth/Application/Interface/Login/LoginCommand.cs b/Femto.Modules.Auth/Application/Interface/Login/LoginCommand.cs deleted file mode 100644 index 8252e2e..0000000 --- a/Femto.Modules.Auth/Application/Interface/Login/LoginCommand.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Femto.Common.Domain; -using Femto.Modules.Auth.Application.Dto; - -namespace Femto.Modules.Auth.Application.Interface.Login; - -public record LoginCommand(string Username, string Password) : ICommand; \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Interface/Login/LoginCommandHandler.cs b/Femto.Modules.Auth/Application/Interface/Login/LoginCommandHandler.cs deleted file mode 100644 index 45b1ae4..0000000 --- a/Femto.Modules.Auth/Application/Interface/Login/LoginCommandHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Femto.Common.Domain; -using Femto.Modules.Auth.Application.Dto; -using Femto.Modules.Auth.Data; -using Microsoft.EntityFrameworkCore; - -namespace Femto.Modules.Auth.Application.Interface.Login; - -internal class LoginCommandHandler(AuthContext context) - : ICommandHandler -{ - public async Task Handle(LoginCommand request, CancellationToken cancellationToken) - { - var user = await context.Users.SingleOrDefaultAsync( - u => u.Username == request.Username, - cancellationToken - ); - - if (user is null) - throw new DomainError("invalid credentials"); - - if (!user.HasPassword(request.Password)) - throw new DomainError("invalid credentials"); - - var session = user.StartNewSession(); - - return new(new Session(session.Id, session.Expires), new UserInfo(user)); - } -} diff --git a/Femto.Modules.Auth/Application/Interface/RefreshUserSession/RefreshUserSessionCommand.cs b/Femto.Modules.Auth/Application/Interface/RefreshUserSession/RefreshUserSessionCommand.cs deleted file mode 100644 index f04fa82..0000000 --- a/Femto.Modules.Auth/Application/Interface/RefreshUserSession/RefreshUserSessionCommand.cs +++ /dev/null @@ -1,7 +0,0 @@ -using Femto.Common; -using Femto.Common.Domain; -using Femto.Modules.Auth.Application.Dto; - -namespace Femto.Modules.Auth.Application.Interface.RefreshUserSession; - -public record RefreshUserSessionCommand(Guid ForUser, CurrentUser CurrentUser) : ICommand; \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Interface/RefreshUserSession/RefreshUserSessionCommandHandler.cs b/Femto.Modules.Auth/Application/Interface/RefreshUserSession/RefreshUserSessionCommandHandler.cs deleted file mode 100644 index f0c6dc1..0000000 --- a/Femto.Modules.Auth/Application/Interface/RefreshUserSession/RefreshUserSessionCommandHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Femto.Common.Domain; -using Femto.Common.Infrastructure.DbConnection; -using Femto.Modules.Auth.Application.Dto; -using Femto.Modules.Auth.Data; -using Microsoft.EntityFrameworkCore; - -namespace Femto.Modules.Auth.Application.Interface.RefreshUserSession; - -internal class RefreshUserSessionCommandHandler(AuthContext context) - : ICommandHandler -{ - public async Task Handle( - RefreshUserSessionCommand request, - CancellationToken cancellationToken - ) - { - if (request.CurrentUser.Id != request.ForUser) - throw new DomainError("invalid request"); - - var user = await context.Users.SingleOrDefaultAsync( - u => u.Id == request.ForUser, - cancellationToken - ); - - if (user is null) - throw new DomainError("invalid request"); - - var session = user.PossiblyRefreshSession(request.CurrentUser.SessionId); - - return new(new Session(session), new UserInfo(user)); - } -} diff --git a/Femto.Modules.Auth/Application/Interface/Register/RegisterCommand.cs b/Femto.Modules.Auth/Application/Interface/Register/RegisterCommand.cs deleted file mode 100644 index dd3c186..0000000 --- a/Femto.Modules.Auth/Application/Interface/Register/RegisterCommand.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Femto.Common.Domain; -using Femto.Modules.Auth.Application.Dto; - -namespace Femto.Modules.Auth.Application.Interface.Register; - -public record RegisterCommand(string Username, string Password, string SignupCode) : ICommand; \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Interface/Register/RegisterCommandHandler.cs b/Femto.Modules.Auth/Application/Interface/Register/RegisterCommandHandler.cs deleted file mode 100644 index 9e29be6..0000000 --- a/Femto.Modules.Auth/Application/Interface/Register/RegisterCommandHandler.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Femto.Common.Domain; -using Femto.Modules.Auth.Application.Dto; -using Femto.Modules.Auth.Data; -using Femto.Modules.Auth.Models; -using Microsoft.EntityFrameworkCore; - -namespace Femto.Modules.Auth.Application.Interface.Register; - -internal class RegisterCommandHandler(AuthContext context) : ICommandHandler -{ - public async Task Handle(RegisterCommand request, CancellationToken cancellationToken) - { - var now = DateTimeOffset.UtcNow; - var code = await context.SignupCodes - .Where(c => c.Code == request.SignupCode) - .Where(c => c.ExpiresAt == null || c.ExpiresAt > now) - .Where(c => c.RedeemingUserId == null) - .SingleOrDefaultAsync(cancellationToken); - - if (code is null) - throw new DomainError("invalid signup code"); - - var user = new UserIdentity(request.Username); - - user.SetPassword(request.Password); - - var session = user.StartNewSession(); - - await context.AddAsync(user, cancellationToken); - - code.Redeem(user.Id); - - return new(new Session(session.Id, session.Expires), new UserInfo(user)); - } -} \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Interface/ValidateSession/ValidateSessionCommand.cs b/Femto.Modules.Auth/Application/Interface/ValidateSession/ValidateSessionCommand.cs deleted file mode 100644 index 40d5417..0000000 --- a/Femto.Modules.Auth/Application/Interface/ValidateSession/ValidateSessionCommand.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Femto.Common.Domain; -using Femto.Modules.Auth.Application.Dto; - -namespace Femto.Modules.Auth.Application.Interface.ValidateSession; - -/// -/// Validate an existing session, and then return either the current session, or a new one in case the expiry is further in the future -/// -/// -public record ValidateSessionCommand(string SessionId) : ICommand; diff --git a/Femto.Modules.Auth/Application/Interface/ValidateSession/ValidateSessionCommandHandler.cs b/Femto.Modules.Auth/Application/Interface/ValidateSession/ValidateSessionCommandHandler.cs deleted file mode 100644 index f79552c..0000000 --- a/Femto.Modules.Auth/Application/Interface/ValidateSession/ValidateSessionCommandHandler.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Femto.Common.Domain; -using Femto.Modules.Auth.Application.Dto; -using Femto.Modules.Auth.Data; -using Femto.Modules.Auth.Errors; -using Microsoft.EntityFrameworkCore; - -namespace Femto.Modules.Auth.Application.Interface.ValidateSession; - -internal class ValidateSessionCommandHandler(AuthContext context) - : ICommandHandler -{ - public async Task Handle( - ValidateSessionCommand request, - CancellationToken cancellationToken - ) - { - var now = DateTimeOffset.UtcNow; - - var user = await context.Users.SingleOrDefaultAsync( - u => u.Sessions.Any(s => s.Id == request.SessionId && s.Expires > now), - cancellationToken - ); - - if (user is null) - throw new InvalidSessionError(); - - var session = user.PossiblyRefreshSession(request.SessionId); - - return new ValidateSessionResult( - new Session(session.Id, session.Expires), - new UserInfo(user) - ); - } -} diff --git a/Femto.Modules.Auth/Data/AuthContext.cs b/Femto.Modules.Auth/Data/AuthContext.cs index e850eb8..ac395ba 100644 --- a/Femto.Modules.Auth/Data/AuthContext.cs +++ b/Femto.Modules.Auth/Data/AuthContext.cs @@ -1,6 +1,10 @@ +using Femto.Common.Domain; using Femto.Common.Infrastructure.Outbox; using Femto.Modules.Auth.Models; +using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.Logging; namespace Femto.Modules.Auth.Data; @@ -8,6 +12,7 @@ internal class AuthContext(DbContextOptions options) : DbContext(op { public virtual DbSet Users { get; set; } public virtual DbSet SignupCodes { get; set; } + public virtual DbSet LongTermSessions { get; set; } public virtual DbSet Outbox { get; set; } protected override void OnModelCreating(ModelBuilder builder) @@ -16,4 +21,43 @@ internal class AuthContext(DbContextOptions options) : DbContext(op builder.HasDefaultSchema("authn"); builder.ApplyConfigurationsFromAssembly(typeof(AuthContext).Assembly); } + + public override int SaveChanges() + { + throw new InvalidOperationException("Use SaveChangesAsync instead"); + } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + await EmitDomainEvents(cancellationToken); + + return await base.SaveChangesAsync(cancellationToken); + } + + private async Task EmitDomainEvents(CancellationToken cancellationToken) + { + var logger = this.GetService>(); + var publisher = this.GetService(); + var domainEvents = this + .ChangeTracker.Entries() + .SelectMany(e => + { + var events = e.Entity.DomainEvents; + e.Entity.ClearDomainEvents(); + return events; + }) + .ToList(); + + logger.LogTrace("loaded {Count} domain events", domainEvents.Count); + + foreach (var domainEvent in domainEvents) + { + logger.LogTrace( + "publishing {Type} domain event {Id}", + domainEvent.GetType().Name, + domainEvent.EventId + ); + await publisher.Publish(domainEvent, cancellationToken); + } + } } \ No newline at end of file diff --git a/Femto.Modules.Auth/Data/Configurations/LongTermSessionConfiguration.cs b/Femto.Modules.Auth/Data/Configurations/LongTermSessionConfiguration.cs new file mode 100644 index 0000000..00f2a13 --- /dev/null +++ b/Femto.Modules.Auth/Data/Configurations/LongTermSessionConfiguration.cs @@ -0,0 +1,13 @@ +using Femto.Modules.Auth.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Femto.Modules.Auth.Data.Configurations; + +public class LongTermSessionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("long_term_session"); + } +} \ No newline at end of file diff --git a/Femto.Modules.Auth/Data/Configurations/UserIdentityTypeConfiguration.cs b/Femto.Modules.Auth/Data/Configurations/UserIdentityTypeConfiguration.cs index 2e5086b..1921451 100644 --- a/Femto.Modules.Auth/Data/Configurations/UserIdentityTypeConfiguration.cs +++ b/Femto.Modules.Auth/Data/Configurations/UserIdentityTypeConfiguration.cs @@ -19,8 +19,6 @@ internal class UserIdentityTypeConfiguration : IEntityTypeConfiguration u.Sessions).WithOwner().HasForeignKey("user_id"); - builder .OwnsMany(u => u.Roles, entity => { diff --git a/Femto.Modules.Auth/Infrastructure/SaveChangesPipelineBehaviour.cs b/Femto.Modules.Auth/Infrastructure/SaveChangesPipelineBehaviour.cs new file mode 100644 index 0000000..cc4f983 --- /dev/null +++ b/Femto.Modules.Auth/Infrastructure/SaveChangesPipelineBehaviour.cs @@ -0,0 +1,23 @@ +using Femto.Modules.Auth.Data; +using MediatR; + +namespace Femto.Modules.Auth.Infrastructure; + +internal class SaveChangesPipelineBehaviour(AuthContext context) + : IPipelineBehavior + where TRequest : notnull +{ + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken + ) + { + var response = await next(cancellationToken); + + if (context.ChangeTracker.HasChanges()) + await context.SaveChangesAsync(cancellationToken); + + return response; + } +} diff --git a/Femto.Modules.Auth/Infrastructure/SessionStorage.cs b/Femto.Modules.Auth/Infrastructure/SessionStorage.cs new file mode 100644 index 0000000..e331396 --- /dev/null +++ b/Femto.Modules.Auth/Infrastructure/SessionStorage.cs @@ -0,0 +1,56 @@ +using System.Collections; +using System.Collections.Concurrent; +using Femto.Modules.Auth.Models; +using Microsoft.Extensions.Caching.Memory; + +namespace Femto.Modules.Auth.Infrastructure; + +internal class SessionStorage(TimeProvider timeProvider) +{ + private readonly IMemoryCache _storage = new MemoryCache(new MemoryCacheOptions()); + + public async Task GetSession(string id) + { + var session = this._storage.Get($"session:{id}"); + + if (session is null) + return null; + + var invalidUntil = this._storage.Get( + $"user:invalid_until:{session.UserId}" + ); + + if (invalidUntil is not null && invalidUntil > session.Expires) + return null; + + return session; + } + + public Task AddSession(Session session) + { + using var sessionEntry = this._storage.CreateEntry($"session:{session.Id}"); + sessionEntry.Value = session; + sessionEntry.SetAbsoluteExpiration(session.Expires); + + return Task.CompletedTask; + } + + public Task DeleteSession(string id) + { + this._storage.Remove($"session:{id}"); + + return Task.CompletedTask; + } + + public Task InvalidateUserSessions(Guid userId) + { + var invalidUntil = timeProvider.GetUtcNow() + Session.ValidityPeriod; + + // invalidate sessions who are currently valid + // any sessions created after this will have a validity period that extends past invalid_until + // this cache entry doesn't need to live longer than that point in time + this._storage.Set($"user:invalid_until:{userId}", invalidUntil, invalidUntil); + + return Task.CompletedTask; + } +} diff --git a/Femto.Modules.Auth/Models/DomainEventHandlers/UserPasswordChangedHandler.cs b/Femto.Modules.Auth/Models/DomainEventHandlers/UserPasswordChangedHandler.cs new file mode 100644 index 0000000..e09c66e --- /dev/null +++ b/Femto.Modules.Auth/Models/DomainEventHandlers/UserPasswordChangedHandler.cs @@ -0,0 +1,22 @@ +using Femto.Modules.Auth.Data; +using Femto.Modules.Auth.Models.Events; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Femto.Modules.Auth.Models.DomainEventHandlers; + +internal class UserPasswordChangedHandler(AuthContext context) + : INotificationHandler +{ + public async Task Handle(UserWasCreatedEvent notification, CancellationToken cancellationToken) + { + var longTermSessions = await context + .LongTermSessions.Where(s => s.UserId == notification.User.Id) + .ToListAsync(cancellationToken); + + foreach (var session in longTermSessions) + { + session.Invalidate(); + } + } +} diff --git a/Femto.Modules.Auth/Models/Events/UserPasswordChangedDomainEvent.cs b/Femto.Modules.Auth/Models/Events/UserPasswordChangedDomainEvent.cs new file mode 100644 index 0000000..70c9a73 --- /dev/null +++ b/Femto.Modules.Auth/Models/Events/UserPasswordChangedDomainEvent.cs @@ -0,0 +1,5 @@ +using Femto.Common.Domain; + +namespace Femto.Modules.Auth.Models.Events; + +internal record UserPasswordChangedDomainEvent(UserIdentity User) : DomainEvent; \ No newline at end of file diff --git a/Femto.Modules.Auth/Models/LongTermSession.cs b/Femto.Modules.Auth/Models/LongTermSession.cs new file mode 100644 index 0000000..eba3d9d --- /dev/null +++ b/Femto.Modules.Auth/Models/LongTermSession.cs @@ -0,0 +1,73 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.Text; +using static System.Security.Cryptography.RandomNumberGenerator; + +namespace Femto.Modules.Auth.Models; + +public class LongTermSession +{ + private static TimeSpan TokenTimeout { get; } = TimeSpan.FromDays(90); + private static TimeSpan RefreshBuffer { get; } = TimeSpan.FromDays(5); + + public int Id { get; private set; } + + public string Selector { get; private set; } + + public byte[] HashedVerifier { get; private set; } + + public DateTimeOffset Expires { get; private set; } + + public Guid UserId { get; private set; } + + public bool IsInvalidated { get; private set; } + + [NotMapped] + public bool ExpiresSoon => this.Expires < DateTimeOffset.UtcNow + RefreshBuffer; + + private LongTermSession() { } + + public static (LongTermSession, string) Create(Guid userId) + { + var selector = GetString( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + 12 + ); + + var verifier = GetString("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 32); + + using var sha256 = System.Security.Cryptography.SHA256.Create(); + + var longTermSession = new LongTermSession + { + Selector = selector, + HashedVerifier = ComputeHash(verifier), + UserId = userId, + Expires = DateTimeOffset.UtcNow + TokenTimeout, + }; + + return (longTermSession, verifier); + } + + public bool CheckVerifier(string verifier) + { + if (this.IsInvalidated) + return false; + + if (this.Expires < DateTimeOffset.UtcNow) + return false; + + return ComputeHash(verifier).SequenceEqual(this.HashedVerifier); + } + + private static byte[] ComputeHash(string verifier) + { + using var sha256 = System.Security.Cryptography.SHA256.Create(); + var hashedVerifier = sha256.ComputeHash(Encoding.UTF8.GetBytes(verifier)); + return hashedVerifier; + } + + public void Invalidate() + { + this.IsInvalidated = true; + } +} diff --git a/Femto.Modules.Auth/Models/Session.cs b/Femto.Modules.Auth/Models/Session.cs new file mode 100644 index 0000000..e641a61 --- /dev/null +++ b/Femto.Modules.Auth/Models/Session.cs @@ -0,0 +1,16 @@ +using static System.Security.Cryptography.RandomNumberGenerator; + +namespace Femto.Modules.Auth.Models; + +public class Session(Guid userId, bool isStrong) +{ + public static readonly TimeSpan ValidityPeriod = TimeSpan.FromMinutes(15); + private static readonly TimeSpan RefreshBuffer = TimeSpan.FromMinutes(5); + public string Id { get; } = Convert.ToBase64String(GetBytes(32)); + public Guid UserId { get; } = userId; + public DateTimeOffset Expires { get; } = DateTimeOffset.UtcNow + ValidityPeriod; + + public bool ExpiresSoon => this.Expires < DateTimeOffset.UtcNow + RefreshBuffer; + public bool IsStronglyAuthenticated { get; } = isStrong; + public bool IsExpired => this.Expires < DateTimeOffset.UtcNow; +} diff --git a/Femto.Modules.Auth/Models/UserIdentity.cs b/Femto.Modules.Auth/Models/UserIdentity.cs index 756be41..bd0288f 100644 --- a/Femto.Modules.Auth/Models/UserIdentity.cs +++ b/Femto.Modules.Auth/Models/UserIdentity.cs @@ -1,9 +1,6 @@ -using System.Text; -using System.Text.Unicode; using Femto.Common.Domain; using Femto.Modules.Auth.Contracts; using Femto.Modules.Auth.Models.Events; -using Geralt; namespace Femto.Modules.Auth.Models; @@ -15,8 +12,6 @@ internal class UserIdentity : Entity public Password? Password { get; private set; } - public ICollection Sessions { get; private set; } = []; - public ICollection Roles { get; private set; } = []; private UserIdentity() { } @@ -31,14 +26,10 @@ internal class UserIdentity : Entity this.AddDomainEvent(new UserWasCreatedEvent(this)); } - public UserIdentity WithPassword(string password) - { - this.SetPassword(password); - return this; - } - public void SetPassword(string password) { + if (this.Password is not null) + this.AddDomainEvent(new UserPasswordChangedDomainEvent(this)); this.Password = new Password(password); } @@ -51,25 +42,6 @@ internal class UserIdentity : Entity return this.Password.Check(requestPassword); } - - public UserSession PossiblyRefreshSession(string sessionId) - { - var session = this.Sessions.Single(s => s.Id == sessionId); - - if (session.ExpiresSoon) - return this.StartNewSession(); - - return session; - } - - public UserSession StartNewSession() - { - var session = UserSession.Create(); - - this.Sessions.Add(session); - - return session; - } } public class SetPasswordError(string message, Exception inner) : DomainError(message, inner); diff --git a/Femto.Modules.Auth/Models/UserSession.cs b/Femto.Modules.Auth/Models/UserSession.cs deleted file mode 100644 index 7deb251..0000000 --- a/Femto.Modules.Auth/Models/UserSession.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Femto.Modules.Auth.Models; - -internal class UserSession -{ - private static TimeSpan SessionTimeout { get; } = TimeSpan.FromMinutes(30); - private static TimeSpan ExpiryBuffer { get; } = TimeSpan.FromMinutes(5); - public string Id { get; private set; } - public DateTimeOffset Expires { get; private set; } - public bool ExpiresSoon => Expires < DateTimeOffset.UtcNow + ExpiryBuffer; - - private UserSession() {} - - public static UserSession Create() - { - return new() - { - Id = Convert.ToBase64String(System.Security.Cryptography.RandomNumberGenerator.GetBytes(32)), - Expires = DateTimeOffset.UtcNow + SessionTimeout - }; - } -} \ No newline at end of file diff --git a/Femto.Modules.Blog.Data/Class1.cs b/Femto.Modules.Blog.Data/Class1.cs deleted file mode 100644 index 3be8b2a..0000000 --- a/Femto.Modules.Blog.Data/Class1.cs +++ /dev/null @@ -1,5 +0,0 @@ -๏ปฟnamespace Femto.Modules.Blog.Data; - -public class Class1 -{ -} \ No newline at end of file diff --git a/Femto.Modules.Blog.Data/Femto.Modules.Blog.Data.csproj b/Femto.Modules.Blog.Data/Femto.Modules.Blog.Data.csproj deleted file mode 100644 index 17b910f..0000000 --- a/Femto.Modules.Blog.Data/Femto.Modules.Blog.Data.csproj +++ /dev/null @@ -1,9 +0,0 @@ -๏ปฟ - - - net9.0 - enable - enable - - - diff --git a/Femto.Modules.Blog.Domain/Femto.Modules.Blog.Domain.csproj b/Femto.Modules.Blog.Domain/Femto.Modules.Blog.Domain.csproj deleted file mode 100644 index 6ae6742..0000000 --- a/Femto.Modules.Blog.Domain/Femto.Modules.Blog.Domain.csproj +++ /dev/null @@ -1,24 +0,0 @@ -๏ปฟ - - - net9.0 - enable - enable - - - - - - - - - - - - - - ..\..\..\..\.nuget\packages\microsoft.entityframeworkcore\9.0.4\lib\net8.0\Microsoft.EntityFrameworkCore.dll - - - - diff --git a/Femto.Modules.Blog/Application/BlogStartup.cs b/Femto.Modules.Blog/Application/BlogStartup.cs index b134f4c..afd18b5 100644 --- a/Femto.Modules.Blog/Application/BlogStartup.cs +++ b/Femto.Modules.Blog/Application/BlogStartup.cs @@ -35,9 +35,13 @@ public static class BlogStartup rootContainer.AddHostedService(_ => new BlogApplication(host)); - rootContainer.AddScoped(_ => new ScopeBinding(host.Services.CreateScope())); + rootContainer.AddKeyedScoped( + "BlogService", + (_, o) => new ScopeBinding(host.Services.CreateScope()) + ); + rootContainer.AddScoped(services => - services.GetRequiredService>().GetService() + services.GetRequiredKeyedService("BlogService").GetService() ); bus.Subscribe( diff --git a/Femto.Modules.Blog/Application/Commands/AddPostComment/AddPostCommentCommand.cs b/Femto.Modules.Blog/Application/Commands/AddPostComment/AddPostCommentCommand.cs new file mode 100644 index 0000000..445c59e --- /dev/null +++ b/Femto.Modules.Blog/Application/Commands/AddPostComment/AddPostCommentCommand.cs @@ -0,0 +1,5 @@ +using Femto.Common.Domain; + +namespace Femto.Modules.Blog.Application.Commands.AddPostComment; + +public record AddPostCommentCommand(Guid PostId, Guid AuthorId, string Content) : ICommand; \ No newline at end of file diff --git a/Femto.Modules.Blog/Application/Commands/AddPostComment/AddPostCommentCommandHandler.cs b/Femto.Modules.Blog/Application/Commands/AddPostComment/AddPostCommentCommandHandler.cs new file mode 100644 index 0000000..6e52877 --- /dev/null +++ b/Femto.Modules.Blog/Application/Commands/AddPostComment/AddPostCommentCommandHandler.cs @@ -0,0 +1,20 @@ +using Femto.Common.Domain; +using Microsoft.EntityFrameworkCore; + +namespace Femto.Modules.Blog.Application.Commands.AddPostComment; + +internal class AddPostCommentCommandHandler(BlogContext context) : ICommandHandler +{ + public async Task Handle(AddPostCommentCommand request, CancellationToken cancellationToken) + { + var post = await context.Posts.SingleOrDefaultAsync( + p => p.Id == request.PostId, + cancellationToken + ); + + if (post is null) + return; + + post.AddComment(request.AuthorId, request.Content); + } +} \ No newline at end of file diff --git a/Femto.Modules.Blog/Application/Commands/AddPostReaction/AddPostReactionCommand.cs b/Femto.Modules.Blog/Application/Commands/AddPostReaction/AddPostReactionCommand.cs new file mode 100644 index 0000000..9096687 --- /dev/null +++ b/Femto.Modules.Blog/Application/Commands/AddPostReaction/AddPostReactionCommand.cs @@ -0,0 +1,6 @@ +using Femto.Common; +using Femto.Common.Domain; + +namespace Femto.Modules.Blog.Application.Commands.AddPostReaction; + +public record AddPostReactionCommand(Guid PostId, string Emoji, Guid ReactorId) : ICommand; \ No newline at end of file diff --git a/Femto.Modules.Blog/Application/Commands/AddPostReaction/AddPostReactionCommandHandler.cs b/Femto.Modules.Blog/Application/Commands/AddPostReaction/AddPostReactionCommandHandler.cs new file mode 100644 index 0000000..e2c3f8a --- /dev/null +++ b/Femto.Modules.Blog/Application/Commands/AddPostReaction/AddPostReactionCommandHandler.cs @@ -0,0 +1,21 @@ +using Femto.Common.Domain; +using Microsoft.EntityFrameworkCore; + +namespace Femto.Modules.Blog.Application.Commands.AddPostReaction; + +internal class AddPostReactionCommandHandler(BlogContext context) + : ICommandHandler +{ + public async Task Handle(AddPostReactionCommand request, CancellationToken cancellationToken) + { + var post = await context.Posts.SingleOrDefaultAsync( + p => p.Id == request.PostId, + cancellationToken + ); + + if (post is null) + return; + + post.AddReaction(request.ReactorId, request.Emoji); + } +} diff --git a/Femto.Modules.Blog/Application/Commands/ClearPostReaction/ClearPostReactionCommand.cs b/Femto.Modules.Blog/Application/Commands/ClearPostReaction/ClearPostReactionCommand.cs new file mode 100644 index 0000000..618305f --- /dev/null +++ b/Femto.Modules.Blog/Application/Commands/ClearPostReaction/ClearPostReactionCommand.cs @@ -0,0 +1,6 @@ +using Femto.Common; +using Femto.Common.Domain; + +namespace Femto.Modules.Blog.Application.Commands.ClearPostReaction; + +public record ClearPostReactionCommand(Guid PostId, string Emoji, Guid ReactorId): ICommand; \ No newline at end of file diff --git a/Femto.Modules.Blog/Application/Commands/ClearPostReaction/ClearPostReactionCommandHandler.cs b/Femto.Modules.Blog/Application/Commands/ClearPostReaction/ClearPostReactionCommandHandler.cs new file mode 100644 index 0000000..e4d344e --- /dev/null +++ b/Femto.Modules.Blog/Application/Commands/ClearPostReaction/ClearPostReactionCommandHandler.cs @@ -0,0 +1,22 @@ +using Femto.Common.Domain; +using Femto.Modules.Blog.Application.Commands.AddPostReaction; +using Microsoft.EntityFrameworkCore; + +namespace Femto.Modules.Blog.Application.Commands.ClearPostReaction; + +internal class ClearPostReactionCommandHandler(BlogContext context) + : ICommandHandler +{ + public async Task Handle(ClearPostReactionCommand request, CancellationToken cancellationToken) + { + var post = await context.Posts.SingleOrDefaultAsync( + p => p.Id == request.PostId, + cancellationToken + ); + + if (post is null) + return; + + post.RemoveReaction(request.ReactorId, request.Emoji); + } +} diff --git a/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommand.cs b/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommand.cs index d10c496..30cc602 100644 --- a/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommand.cs +++ b/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommand.cs @@ -1,8 +1,22 @@ +using Femto.Common; using Femto.Common.Domain; +using Femto.Modules.Blog.Application.Queries.GetPosts.Dto; namespace Femto.Modules.Blog.Application.Commands.CreatePost; -public record CreatePostCommand(Guid AuthorId, string Content, IEnumerable Media, bool? IsPublic) - : ICommand; +public record CreatePostCommand( + Guid AuthorId, + string Content, + IEnumerable Media, + bool? IsPublic, + CurrentUser CurrentUser +) : ICommand; -public record CreatePostMedia(Guid MediaId, Uri Url, string? Type, int Order, int? Width, int? Height); +public record CreatePostMedia( + Guid MediaId, + Uri Url, + string? Type, + int Order, + int? Width, + int? Height +); diff --git a/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommandHandler.cs b/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommandHandler.cs index cda4b2d..25bab45 100644 --- a/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommandHandler.cs +++ b/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommandHandler.cs @@ -1,12 +1,16 @@ +using Femto.Modules.Blog.Application.Queries.GetPosts.Dto; using Femto.Modules.Blog.Domain.Posts; using MediatR; namespace Femto.Modules.Blog.Application.Commands.CreatePost; internal class CreatePostCommandHandler(BlogContext context) - : IRequestHandler + : IRequestHandler { - public async Task Handle(CreatePostCommand request, CancellationToken cancellationToken) + public async Task Handle( + CreatePostCommand request, + CancellationToken cancellationToken + ) { var post = new Post( request.AuthorId, @@ -20,13 +24,21 @@ internal class CreatePostCommandHandler(BlogContext context) media.Width, media.Height )) - .ToList() + .ToList(), + request.IsPublic is true ); - - post.IsPublic = request.IsPublic is true; await context.AddAsync(post, cancellationToken); - return post.Id; + return new PostDto( + post.Id, + post.Content, + post.Media.Select(m => new PostMediaDto(m.Url, m.Width, m.Height)).ToList(), + post.PostedOn, + new PostAuthorDto(post.AuthorId, request.CurrentUser.Username), + [], + post.PossibleReactions, + [] + ); } } diff --git a/Femto.Modules.Blog/Application/Configurations/PostConfiguration.cs b/Femto.Modules.Blog/Application/Configurations/PostConfiguration.cs index 8cb2a64..630cbe2 100644 --- a/Femto.Modules.Blog/Application/Configurations/PostConfiguration.cs +++ b/Femto.Modules.Blog/Application/Configurations/PostConfiguration.cs @@ -10,5 +10,24 @@ internal class PostConfiguration : IEntityTypeConfiguration { table.ToTable("post"); table.OwnsMany(post => post.Media).WithOwner(); + table.OwnsMany( + post => post.Reactions, + reactions => + { + reactions.WithOwner().HasForeignKey(r => r.PostId); + reactions.HasKey(r => new + { + r.PostId, + r.AuthorId, + r.Emoji, + }); + } + ); + + table.OwnsMany(p => p.Comments).WithOwner(); + + table.Property("PossibleReactionsJson").HasColumnName("possible_reactions"); + + table.Ignore(e => e.PossibleReactions); } } diff --git a/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/GetPostsQueryResult.cs b/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/GetPostsQueryResult.cs index be8157a..8b75d6e 100644 --- a/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/GetPostsQueryResult.cs +++ b/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/GetPostsQueryResult.cs @@ -1,3 +1,3 @@ namespace Femto.Modules.Blog.Application.Queries.GetPosts.Dto; -public record GetPostsQueryResult(IList Posts, Guid? Next); \ No newline at end of file +public record GetPostsQueryResult(IList Posts); \ No newline at end of file diff --git a/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostCommentDto.cs b/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostCommentDto.cs new file mode 100644 index 0000000..55ea5e8 --- /dev/null +++ b/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostCommentDto.cs @@ -0,0 +1,3 @@ +namespace Femto.Modules.Blog.Application.Queries.GetPosts.Dto; + +public record PostCommentDto(string Author, string Content, DateTimeOffset PostedOn); \ No newline at end of file diff --git a/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostDto.cs b/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostDto.cs index 584d1f5..63efede 100644 --- a/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostDto.cs +++ b/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostDto.cs @@ -1,3 +1,12 @@ namespace Femto.Modules.Blog.Application.Queries.GetPosts.Dto; -public record PostDto(Guid PostId, string Text, IList Media, DateTimeOffset CreatedAt, PostAuthorDto Author); \ No newline at end of file +public record PostDto( + Guid PostId, + string Text, + IList Media, + DateTimeOffset CreatedAt, + PostAuthorDto Author, + IList Reactions, + IEnumerable PossibleReactions, + IList Comments +); \ No newline at end of file diff --git a/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostReactionDto.cs b/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostReactionDto.cs new file mode 100644 index 0000000..60349b9 --- /dev/null +++ b/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostReactionDto.cs @@ -0,0 +1,3 @@ +namespace Femto.Modules.Blog.Application.Queries.GetPosts.Dto; + +public record PostReactionDto(string Emoji, string AuthorName, DateTimeOffset ReactedOn); diff --git a/Femto.Modules.Blog/Application/Queries/GetPosts/GetPostsQuery.cs b/Femto.Modules.Blog/Application/Queries/GetPosts/GetPostsQuery.cs index f8af9d2..1bb1d4c 100644 --- a/Femto.Modules.Blog/Application/Queries/GetPosts/GetPostsQuery.cs +++ b/Femto.Modules.Blog/Application/Queries/GetPosts/GetPostsQuery.cs @@ -3,22 +3,27 @@ using Femto.Modules.Blog.Application.Queries.GetPosts.Dto; namespace Femto.Modules.Blog.Application.Queries.GetPosts; +/// +/// Get posts in reverse chronological order +/// +/// public record GetPostsQuery(Guid? CurrentUserId) : IQuery { - public Guid? From { get; init; } + /// + /// Id of the specific post to load. If specified, After and Amount are ignored + /// + public Guid? PostId { get; } + + /// + /// If specified, loads posts from after the given Id. Used for paging + /// + public Guid? After { get; init; } public int Amount { get; init; } = 20; public Guid? AuthorId { get; init; } public string? Author { get; init; } - /// - /// Default is to load in reverse chronological order - /// TODO this is not exposed on the client as it probably wouldn't work that well - /// - public GetPostsDirection Direction { get; init; } = GetPostsDirection.Backward; -} - -public enum GetPostsDirection -{ - Forward, - Backward, -} + public GetPostsQuery(Guid postId, Guid? currentUserId) : this(currentUserId) + { + this.PostId = postId; + } +} \ No newline at end of file diff --git a/Femto.Modules.Blog/Application/Queries/GetPosts/GetPostsQueryHandler.cs b/Femto.Modules.Blog/Application/Queries/GetPosts/GetPostsQueryHandler.cs index 7ace8a6..c57627f 100644 --- a/Femto.Modules.Blog/Application/Queries/GetPosts/GetPostsQueryHandler.cs +++ b/Femto.Modules.Blog/Application/Queries/GetPosts/GetPostsQueryHandler.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Dapper; using Femto.Common.Infrastructure.DbConnection; using Femto.Modules.Blog.Application.Queries.GetPosts.Dto; @@ -15,101 +16,158 @@ public class GetPostsQueryHandler(IDbConnectionFactory connectionFactory) { using var conn = connectionFactory.GetConnection(); - var orderBy = query.Direction is GetPostsDirection.Backward ? "desc" : "asc"; - var pageFilter = query.Direction is GetPostsDirection.Backward ? "<=" : ">="; var username = query.Author; var authorGuid = query.AuthorId; - var cursor = query.From; + var cursor = query.After; var showPrivate = query.CurrentUserId is not null; - // lang=sql - var sql = $$""" - with page as ( - select blog.post.*, blog.author.username as Username, blog.author.id as AuthorId - from blog.post - inner join blog.author on blog.author.id = blog.post.author_id - where (@username is null or blog.author.username = @username) - and (@showPrivate or blog.post.is_public = true) - and (@authorGuid is null or blog.author.id = @authorGuid) - and (@cursor is null or blog.post.id {{pageFilter}} @cursor) - order by blog.post.id {{orderBy}} - limit @amount - ) - select - page.id as PostId, - page.content as Content, - blog.post_media.url as MediaUrl, - blog.post_media.width as MediaWidth, - blog.post_media.height as MediaHeight, - page.posted_on as PostedOn, - page.Username, - page.AuthorId - from page - left join blog.post_media on blog.post_media.post_id = page.id - order by page.id {{orderBy}} - """; - - var result = await conn.QueryAsync( - sql, + var loadPostsResult = await conn.QueryAsync( + """ + select + blog.post.id as PostId, + blog.post.content as Content, + blog.post.posted_on as PostedOn, + blog.author.username as Username, + blog.author.id as AuthorId, + blog.post.possible_reactions as PossibleReactions + from blog.post + inner join blog.author on blog.author.id = blog.post.author_id + where (@username is null or blog.author.username = @username) + and (@postId is null or blog.post.id = @postId) + and (@showPrivate or blog.post.is_public = true) + and (@authorGuid is null or blog.author.id = @authorGuid) + and (@cursor is null or blog.post.id < @cursor) + order by blog.post.id desc + limit @amount + """, new { username, authorGuid, cursor, - // load an extra one to take for the cursor - amount = query.Amount + 1, + amount = query.PostId is not null ? 1 : query.Amount, showPrivate, + postId = query.PostId, } ); - var rows = result.ToList(); + var posts = loadPostsResult.ToList(); - var posts = rows.GroupBy(row => row.PostId) - .Select(group => - { - var postId = group.Key; - var post = group.First(); - var media = group - .Select(row => - { - if (row.MediaUrl is not null) - { - return new PostMediaDto( - new Uri(row.MediaUrl), - row.MediaHeight, - row.MediaHeight - ); - } - else - return null; - }) - .OfType() - .ToList(); - return new PostDto( - postId, - post.Content, - media, - post.PostedOn, - new PostAuthorDto(post.AuthorId, post.Username) - ); - }) - .ToList(); + var postIds = posts.Select(p => p.PostId).ToList(); - var next = rows.Count >= query.Amount ? rows.LastOrDefault()?.PostId : null; + var loadMediaResult = await conn.QueryAsync( + """ + select + pm.url as MediaUrl, + pm.type as MediaType, + pm.width as MediaWidth, + pm.height as MediaHeight, + pm.post_id as PostId + from blog.post_media pm where pm.post_id = ANY (@postIds) + order by pm.ordering + """, + new { postIds } + ); - return new GetPostsQueryResult(posts, next); + var media = loadMediaResult.ToList(); + + var loadReactionsResult = await conn.QueryAsync( + """ + select + pr.post_id as PostId, + a.username as AuthorName, + pr.emoji as Emoji, + pr.created_at as CreatedOn + from blog.post_reaction pr + join blog.author a on a.id = pr.author_id + where pr.post_id = ANY (@postIds) + """, + new { postIds } + ); + + var reactions = loadReactionsResult.ToList(); + + var loadCommentsResult = await conn.QueryAsync( + """ + select + pc.id as CommentId, + pc.post_id as PostId, + pc.content as Content, + pc.created_at as PostedOn, + a.username as AuthorName + from blog.post_comment pc + join blog.author a on pc.author_id = a.id + where pc.post_id = ANY (@postIds) + """, + new { postIds } + ); + + var comments = loadCommentsResult.ToList(); + + return new GetPostsQueryResult( + posts + .Select(p => new PostDto( + p.PostId, + p.Content, + media + .Where(m => m.PostId == p.PostId) + .Select(m => new PostMediaDto( + new Uri(m.MediaUrl), + m.MediaWidth, + m.MediaHeight + )) + .ToList(), + p.PostedOn, + new PostAuthorDto(p.AuthorId, p.Username), + reactions + .Where(r => r.PostId == p.PostId) + .Select(r => new PostReactionDto(r.Emoji, r.AuthorName, r.CreatedAt)) + .ToList(), + !string.IsNullOrEmpty(p.PossibleReactions) + ? JsonSerializer.Deserialize>(p.PossibleReactions)! + : [], + comments + .Where(c => c.PostId == p.PostId) + .Select(c => new PostCommentDto(c.AuthorName, c.Content, c.PostedOn)) + .ToList() + )) + .ToList() + ); } - internal class QueryResult + internal record LoadPostRow { - public Guid PostId { get; set; } - public string Content { get; set; } - public string? MediaUrl { get; set; } - public string? MediaType { get; set; } - public int? MediaWidth { get; set; } - public int? MediaHeight { get; set; } - public DateTimeOffset PostedOn { get; set; } - public Guid AuthorId { get; set; } - public string Username { get; set; } + public Guid PostId { get; init; } + public string Content { get; init; } + public DateTimeOffset PostedOn { get; init; } + public string Username { get; init; } + public Guid AuthorId { get; init; } + public string? PossibleReactions { get; init; } + } + + internal record LoadMediaRow + { + public string MediaUrl { get; init; } + public string? MediaType { get; init; } + public int? MediaWidth { get; init; } + public int? MediaHeight { get; init; } + public Guid PostId { get; init; } + } + + internal record LoadReactionRow + { + public Guid PostId { get; init; } + public string AuthorName { get; init; } + public string Emoji { get; init; } + public DateTimeOffset CreatedAt { get; init; } + } + + internal record LoadCommentRow + { + public Guid CommentId { get; init; } + public Guid PostId { get; init; } + public string Content { get; init; } + public DateTimeOffset PostedOn { get; init; } + public string AuthorName { get; init; } } } diff --git a/Femto.Modules.Blog/Domain/Posts/Post.cs b/Femto.Modules.Blog/Domain/Posts/Post.cs index 65f90d6..b5a9b2d 100644 --- a/Femto.Modules.Blog/Domain/Posts/Post.cs +++ b/Femto.Modules.Blog/Domain/Posts/Post.cs @@ -1,5 +1,7 @@ +using System.Text.Json; using Femto.Common.Domain; using Femto.Modules.Blog.Domain.Posts.Events; +using Femto.Modules.Blog.Emoji; namespace Femto.Modules.Blog.Domain.Posts; @@ -9,17 +11,62 @@ internal class Post : Entity public Guid AuthorId { get; private set; } public string Content { get; private set; } = null!; public IList Media { get; private set; } - public bool IsPublic { get; set; } + + public IList Reactions { get; private set; } = []; + + public IList Comments { get; private set; } = []; + public bool IsPublic { get; private set; } + + public DateTimeOffset PostedOn { get; private set; } + + private string PossibleReactionsJson { get; set; } = null!; + + public IEnumerable PossibleReactions + { + get => JsonSerializer.Deserialize>(this.PossibleReactionsJson)!; + init => PossibleReactionsJson = JsonSerializer.Serialize(value); + } private Post() { } - public Post(Guid authorId, string content, IList media) + public Post(Guid authorId, string content, IList media, bool isPublic) { this.Id = Guid.CreateVersion7(); this.AuthorId = authorId; this.Content = content; this.Media = media; + this.PossibleReactions = AllEmoji.GetRandomEmoji(5); + this.PostedOn = DateTimeOffset.UtcNow; + this.IsPublic = isPublic; this.AddDomainEvent(new PostCreated(this)); } + + public void AddReaction(Guid reactorId, string emoji) + { + if (!this.PossibleReactions.Contains(emoji)) + return; + + if (this.Reactions.Any(r => r.AuthorId == reactorId && r.Emoji == emoji)) + return; + + this.Reactions.Add(new PostReaction(reactorId, this.Id, emoji)); + } + + public void RemoveReaction(Guid reactorId, string emoji) + { + this.Reactions = this + .Reactions.Where(r => r.AuthorId != reactorId || r.Emoji != emoji) + .ToList(); + } + + public void AddComment(Guid authorId, string content) + { + // XXX just ignore empty comments for now. we may want to upgrade this to an error + // but it is probably suitable to just consider it a no-op + if (string.IsNullOrWhiteSpace(content)) + return; + + this.Comments.Add(new PostComment(authorId, content)); + } } diff --git a/Femto.Modules.Blog/Domain/Posts/PostComment.cs b/Femto.Modules.Blog/Domain/Posts/PostComment.cs new file mode 100644 index 0000000..6f658a8 --- /dev/null +++ b/Femto.Modules.Blog/Domain/Posts/PostComment.cs @@ -0,0 +1,19 @@ +namespace Femto.Modules.Blog.Domain.Posts; + +internal class PostComment +{ + public Guid Id { get; private set; } + public Guid AuthorId { get; private set; } + public DateTimeOffset CreatedAt { get; private set; } + public string Content { get; private set; } + + private PostComment() {} + + public PostComment(Guid authorId, string content) + { + this.Id = Guid.CreateVersion7(); + this.AuthorId = authorId; + this.Content = content; + this.CreatedAt = TimeProvider.System.GetUtcNow(); + } +} \ No newline at end of file diff --git a/Femto.Modules.Blog/Domain/Posts/PostReaction.cs b/Femto.Modules.Blog/Domain/Posts/PostReaction.cs new file mode 100644 index 0000000..38e33b8 --- /dev/null +++ b/Femto.Modules.Blog/Domain/Posts/PostReaction.cs @@ -0,0 +1,18 @@ +namespace Femto.Modules.Blog.Domain.Posts; + +public class PostReaction +{ + public Guid AuthorId { get; private set; } + public Guid PostId { get; private set; } + public string Emoji { get; private set; } = null!; + public DateTimeOffset CreatedAt { get; private set; } + public PostReaction(Guid authorId, Guid postId, string emoji) + { + this.AuthorId = authorId; + this.PostId = postId; + this.Emoji = emoji; + this.CreatedAt = TimeProvider.System.GetUtcNow(); + } + + private PostReaction() { } +} \ No newline at end of file diff --git a/Femto.Modules.Blog/Emoji/GetRandomEmoji.cs b/Femto.Modules.Blog/Emoji/GetRandomEmoji.cs new file mode 100644 index 0000000..8fa7928 --- /dev/null +++ b/Femto.Modules.Blog/Emoji/GetRandomEmoji.cs @@ -0,0 +1,18 @@ +namespace Femto.Modules.Blog.Emoji; + +internal static partial class AllEmoji +{ + public static IList GetRandomEmoji(int count) => new Random().TakeRandomly(Emojis).Distinct().Take(count).ToList(); +} + +internal static class RandomExtensions +{ + public static IEnumerable TakeRandomly(this Random rand, ICollection collection) + { + while (true) + { + var idx = rand.Next(collection.Count); + yield return collection.ElementAt(idx); + } + } +} diff --git a/Femto.Modules.Blog/Emoji/ListOfEmoji.cs b/Femto.Modules.Blog/Emoji/ListOfEmoji.cs new file mode 100644 index 0000000..f3ad8b5 --- /dev/null +++ b/Femto.Modules.Blog/Emoji/ListOfEmoji.cs @@ -0,0 +1,3789 @@ +namespace Femto.Modules.Blog.Emoji; + +internal static partial class AllEmoji +{ + public static readonly string[] Emojis = + [ + "๐Ÿ˜€", + "๐Ÿ˜ƒ", + "๐Ÿ˜„", + "๐Ÿ˜", + "๐Ÿ˜†", + "๐Ÿ˜…", + "๐Ÿคฃ", + "๐Ÿ˜‚", + "๐Ÿ™‚", + "๐Ÿ™ƒ", + "๐Ÿซ ", + "๐Ÿ˜‰", + "๐Ÿ˜Š", + "๐Ÿ˜‡", + "๐Ÿฅฐ", + "๐Ÿ˜", + "๐Ÿคฉ", + "๐Ÿ˜˜", + "๐Ÿ˜—", + "โ˜บ๏ธ", + "๐Ÿ˜š", + "๐Ÿ˜™", + "๐Ÿฅฒ", + "๐Ÿ˜‹", + "๐Ÿ˜›", + "๐Ÿ˜œ", + "๐Ÿคช", + "๐Ÿ˜", + "๐Ÿค‘", + "๐Ÿค—", + "๐Ÿคญ", + "๐Ÿซข", + "๐Ÿซฃ", + "๐Ÿคซ", + "๐Ÿค”", + "๐Ÿซก", + "๐Ÿค", + "๐Ÿคจ", + "๐Ÿ˜", + "๐Ÿ˜‘", + "๐Ÿ˜ถ", + "๐Ÿซฅ", + "๐Ÿ˜ถโ€๐ŸŒซ๏ธ", + "๐Ÿ˜", + "๐Ÿ˜’", + "๐Ÿ™„", + "๐Ÿ˜ฌ", + "๐Ÿ˜ฎโ€๐Ÿ’จ", + "๐Ÿคฅ", + "๐Ÿซจ", + "๐Ÿ™‚โ€โ†”๏ธ", + "๐Ÿ™‚โ€โ†•๏ธ", + "๐Ÿ˜Œ", + "๐Ÿ˜”", + "๐Ÿ˜ช", + "๐Ÿคค", + "๐Ÿ˜ด", + "๐Ÿซฉ", + "๐Ÿ˜ท", + "๐Ÿค’", + "๐Ÿค•", + "๐Ÿคข", + "๐Ÿคฎ", + "๐Ÿคง", + "๐Ÿฅต", + "๐Ÿฅถ", + "๐Ÿฅด", + "๐Ÿ˜ต", + "๐Ÿ˜ตโ€๐Ÿ’ซ", + "๐Ÿคฏ", + "๐Ÿค ", + "๐Ÿฅณ", + "๐Ÿฅธ", + "๐Ÿ˜Ž", + "๐Ÿค“", + "๐Ÿง", + "๐Ÿ˜•", + "๐Ÿซค", + "๐Ÿ˜Ÿ", + "๐Ÿ™", + "โ˜น๏ธ", + "๐Ÿ˜ฎ", + "๐Ÿ˜ฏ", + "๐Ÿ˜ฒ", + "๐Ÿ˜ณ", + "๐Ÿฅบ", + "๐Ÿฅน", + "๐Ÿ˜ฆ", + "๐Ÿ˜ง", + "๐Ÿ˜จ", + "๐Ÿ˜ฐ", + "๐Ÿ˜ฅ", + "๐Ÿ˜ข", + "๐Ÿ˜ญ", + "๐Ÿ˜ฑ", + "๐Ÿ˜–", + "๐Ÿ˜ฃ", + "๐Ÿ˜ž", + "๐Ÿ˜“", + "๐Ÿ˜ฉ", + "๐Ÿ˜ซ", + "๐Ÿฅฑ", + "๐Ÿ˜ค", + "๐Ÿ˜ก", + "๐Ÿ˜ ", + "๐Ÿคฌ", + "๐Ÿ˜ˆ", + "๐Ÿ‘ฟ", + "๐Ÿ’€", + "โ˜ ๏ธ", + "๐Ÿ’ฉ", + "๐Ÿคก", + "๐Ÿ‘น", + "๐Ÿ‘บ", + "๐Ÿ‘ป", + "๐Ÿ‘ฝ", + "๐Ÿ‘พ", + "๐Ÿค–", + "๐Ÿ˜บ", + "๐Ÿ˜ธ", + "๐Ÿ˜น", + "๐Ÿ˜ป", + "๐Ÿ˜ผ", + "๐Ÿ˜ฝ", + "๐Ÿ™€", + "๐Ÿ˜ฟ", + "๐Ÿ˜พ", + "๐Ÿ™ˆ", + "๐Ÿ™‰", + "๐Ÿ™Š", + "๐Ÿ’Œ", + "๐Ÿ’˜", + "๐Ÿ’", + "๐Ÿ’–", + "๐Ÿ’—", + "๐Ÿ’“", + "๐Ÿ’ž", + "๐Ÿ’•", + "๐Ÿ’Ÿ", + "โฃ๏ธ", + "๐Ÿ’”", + "โค๏ธโ€๐Ÿ”ฅ", + "โค๏ธโ€๐Ÿฉน", + "โค๏ธ", + "๐Ÿฉท", + "๐Ÿงก", + "๐Ÿ’›", + "๐Ÿ’š", + "๐Ÿ’™", + "๐Ÿฉต", + "๐Ÿ’œ", + "๐ŸคŽ", + "๐Ÿ–ค", + "๐Ÿฉถ", + "๐Ÿค", + "๐Ÿ’‹", + "๐Ÿ’ฏ", + "๐Ÿ’ข", + "๐Ÿ’ฅ", + "๐Ÿ’ซ", + "๐Ÿ’ฆ", + "๐Ÿ’จ", + "๐Ÿ•ณ๏ธ", + "๐Ÿ’ฌ", + "๐Ÿ‘๏ธโ€๐Ÿ—จ๏ธ", + "๐Ÿ—จ๏ธ", + "๐Ÿ—ฏ๏ธ", + "๐Ÿ’ญ", + "๐Ÿ’ค", + "๐Ÿ‘‹", + "๐Ÿ‘‹๐Ÿป", + "๐Ÿ‘‹๐Ÿผ", + "๐Ÿ‘‹๐Ÿฝ", + "๐Ÿ‘‹๐Ÿพ", + "๐Ÿ‘‹๐Ÿฟ", + "๐Ÿคš", + "๐Ÿคš๐Ÿป", + "๐Ÿคš๐Ÿผ", + "๐Ÿคš๐Ÿฝ", + "๐Ÿคš๐Ÿพ", + "๐Ÿคš๐Ÿฟ", + "๐Ÿ–๏ธ", + "๐Ÿ–๐Ÿป", + "๐Ÿ–๐Ÿผ", + "๐Ÿ–๐Ÿฝ", + "๐Ÿ–๐Ÿพ", + "๐Ÿ–๐Ÿฟ", + "โœ‹", + "โœ‹๐Ÿป", + "โœ‹๐Ÿผ", + "โœ‹๐Ÿฝ", + "โœ‹๐Ÿพ", + "โœ‹๐Ÿฟ", + "๐Ÿ––", + "๐Ÿ––๐Ÿป", + "๐Ÿ––๐Ÿผ", + "๐Ÿ––๐Ÿฝ", + "๐Ÿ––๐Ÿพ", + "๐Ÿ––๐Ÿฟ", + "๐Ÿซฑ", + "๐Ÿซฑ๐Ÿป", + "๐Ÿซฑ๐Ÿผ", + "๐Ÿซฑ๐Ÿฝ", + "๐Ÿซฑ๐Ÿพ", + "๐Ÿซฑ๐Ÿฟ", + "๐Ÿซฒ", + "๐Ÿซฒ๐Ÿป", + "๐Ÿซฒ๐Ÿผ", + "๐Ÿซฒ๐Ÿฝ", + "๐Ÿซฒ๐Ÿพ", + "๐Ÿซฒ๐Ÿฟ", + "๐Ÿซณ", + "๐Ÿซณ๐Ÿป", + "๐Ÿซณ๐Ÿผ", + "๐Ÿซณ๐Ÿฝ", + "๐Ÿซณ๐Ÿพ", + "๐Ÿซณ๐Ÿฟ", + "๐Ÿซด", + "๐Ÿซด๐Ÿป", + "๐Ÿซด๐Ÿผ", + "๐Ÿซด๐Ÿฝ", + "๐Ÿซด๐Ÿพ", + "๐Ÿซด๐Ÿฟ", + "๐Ÿซท", + "๐Ÿซท๐Ÿป", + "๐Ÿซท๐Ÿผ", + "๐Ÿซท๐Ÿฝ", + "๐Ÿซท๐Ÿพ", + "๐Ÿซท๐Ÿฟ", + "๐Ÿซธ", + "๐Ÿซธ๐Ÿป", + "๐Ÿซธ๐Ÿผ", + "๐Ÿซธ๐Ÿฝ", + "๐Ÿซธ๐Ÿพ", + "๐Ÿซธ๐Ÿฟ", + "๐Ÿ‘Œ", + "๐Ÿ‘Œ๐Ÿป", + "๐Ÿ‘Œ๐Ÿผ", + "๐Ÿ‘Œ๐Ÿฝ", + "๐Ÿ‘Œ๐Ÿพ", + "๐Ÿ‘Œ๐Ÿฟ", + "๐ŸคŒ", + "๐ŸคŒ๐Ÿป", + "๐ŸคŒ๐Ÿผ", + "๐ŸคŒ๐Ÿฝ", + "๐ŸคŒ๐Ÿพ", + "๐ŸคŒ๐Ÿฟ", + "๐Ÿค", + "๐Ÿค๐Ÿป", + "๐Ÿค๐Ÿผ", + "๐Ÿค๐Ÿฝ", + "๐Ÿค๐Ÿพ", + "๐Ÿค๐Ÿฟ", + "โœŒ๏ธ", + "โœŒ๐Ÿป", + "โœŒ๐Ÿผ", + "โœŒ๐Ÿฝ", + "โœŒ๐Ÿพ", + "โœŒ๐Ÿฟ", + "๐Ÿคž", + "๐Ÿคž๐Ÿป", + "๐Ÿคž๐Ÿผ", + "๐Ÿคž๐Ÿฝ", + "๐Ÿคž๐Ÿพ", + "๐Ÿคž๐Ÿฟ", + "๐Ÿซฐ", + "๐Ÿซฐ๐Ÿป", + "๐Ÿซฐ๐Ÿผ", + "๐Ÿซฐ๐Ÿฝ", + "๐Ÿซฐ๐Ÿพ", + "๐Ÿซฐ๐Ÿฟ", + "๐ŸคŸ", + "๐ŸคŸ๐Ÿป", + "๐ŸคŸ๐Ÿผ", + "๐ŸคŸ๐Ÿฝ", + "๐ŸคŸ๐Ÿพ", + "๐ŸคŸ๐Ÿฟ", + "๐Ÿค˜", + "๐Ÿค˜๐Ÿป", + "๐Ÿค˜๐Ÿผ", + "๐Ÿค˜๐Ÿฝ", + "๐Ÿค˜๐Ÿพ", + "๐Ÿค˜๐Ÿฟ", + "๐Ÿค™", + "๐Ÿค™๐Ÿป", + "๐Ÿค™๐Ÿผ", + "๐Ÿค™๐Ÿฝ", + "๐Ÿค™๐Ÿพ", + "๐Ÿค™๐Ÿฟ", + "๐Ÿ‘ˆ", + "๐Ÿ‘ˆ๐Ÿป", + "๐Ÿ‘ˆ๐Ÿผ", + "๐Ÿ‘ˆ๐Ÿฝ", + "๐Ÿ‘ˆ๐Ÿพ", + "๐Ÿ‘ˆ๐Ÿฟ", + "๐Ÿ‘‰", + "๐Ÿ‘‰๐Ÿป", + "๐Ÿ‘‰๐Ÿผ", + "๐Ÿ‘‰๐Ÿฝ", + "๐Ÿ‘‰๐Ÿพ", + "๐Ÿ‘‰๐Ÿฟ", + "๐Ÿ‘†", + "๐Ÿ‘†๐Ÿป", + "๐Ÿ‘†๐Ÿผ", + "๐Ÿ‘†๐Ÿฝ", + "๐Ÿ‘†๐Ÿพ", + "๐Ÿ‘†๐Ÿฟ", + "๐Ÿ–•", + "๐Ÿ–•๐Ÿป", + "๐Ÿ–•๐Ÿผ", + "๐Ÿ–•๐Ÿฝ", + "๐Ÿ–•๐Ÿพ", + "๐Ÿ–•๐Ÿฟ", + "๐Ÿ‘‡", + "๐Ÿ‘‡๐Ÿป", + "๐Ÿ‘‡๐Ÿผ", + "๐Ÿ‘‡๐Ÿฝ", + "๐Ÿ‘‡๐Ÿพ", + "๐Ÿ‘‡๐Ÿฟ", + "โ˜๏ธ", + "โ˜๐Ÿป", + "โ˜๐Ÿผ", + "โ˜๐Ÿฝ", + "โ˜๐Ÿพ", + "โ˜๐Ÿฟ", + "๐Ÿซต", + "๐Ÿซต๐Ÿป", + "๐Ÿซต๐Ÿผ", + "๐Ÿซต๐Ÿฝ", + "๐Ÿซต๐Ÿพ", + "๐Ÿซต๐Ÿฟ", + "๐Ÿ‘", + "๐Ÿ‘๐Ÿป", + "๐Ÿ‘๐Ÿผ", + "๐Ÿ‘๐Ÿฝ", + "๐Ÿ‘๐Ÿพ", + "๐Ÿ‘๐Ÿฟ", + "๐Ÿ‘Ž", + "๐Ÿ‘Ž๐Ÿป", + "๐Ÿ‘Ž๐Ÿผ", + "๐Ÿ‘Ž๐Ÿฝ", + "๐Ÿ‘Ž๐Ÿพ", + "๐Ÿ‘Ž๐Ÿฟ", + "โœŠ", + "โœŠ๐Ÿป", + "โœŠ๐Ÿผ", + "โœŠ๐Ÿฝ", + "โœŠ๐Ÿพ", + "โœŠ๐Ÿฟ", + "๐Ÿ‘Š", + "๐Ÿ‘Š๐Ÿป", + "๐Ÿ‘Š๐Ÿผ", + "๐Ÿ‘Š๐Ÿฝ", + "๐Ÿ‘Š๐Ÿพ", + "๐Ÿ‘Š๐Ÿฟ", + "๐Ÿค›", + "๐Ÿค›๐Ÿป", + "๐Ÿค›๐Ÿผ", + "๐Ÿค›๐Ÿฝ", + "๐Ÿค›๐Ÿพ", + "๐Ÿค›๐Ÿฟ", + "๐Ÿคœ", + "๐Ÿคœ๐Ÿป", + "๐Ÿคœ๐Ÿผ", + "๐Ÿคœ๐Ÿฝ", + "๐Ÿคœ๐Ÿพ", + "๐Ÿคœ๐Ÿฟ", + "๐Ÿ‘", + "๐Ÿ‘๐Ÿป", + "๐Ÿ‘๐Ÿผ", + "๐Ÿ‘๐Ÿฝ", + "๐Ÿ‘๐Ÿพ", + "๐Ÿ‘๐Ÿฟ", + "๐Ÿ™Œ", + "๐Ÿ™Œ๐Ÿป", + "๐Ÿ™Œ๐Ÿผ", + "๐Ÿ™Œ๐Ÿฝ", + "๐Ÿ™Œ๐Ÿพ", + "๐Ÿ™Œ๐Ÿฟ", + "๐Ÿซถ", + "๐Ÿซถ๐Ÿป", + "๐Ÿซถ๐Ÿผ", + "๐Ÿซถ๐Ÿฝ", + "๐Ÿซถ๐Ÿพ", + "๐Ÿซถ๐Ÿฟ", + "๐Ÿ‘", + "๐Ÿ‘๐Ÿป", + "๐Ÿ‘๐Ÿผ", + "๐Ÿ‘๐Ÿฝ", + "๐Ÿ‘๐Ÿพ", + "๐Ÿ‘๐Ÿฟ", + "๐Ÿคฒ", + "๐Ÿคฒ๐Ÿป", + "๐Ÿคฒ๐Ÿผ", + "๐Ÿคฒ๐Ÿฝ", + "๐Ÿคฒ๐Ÿพ", + "๐Ÿคฒ๐Ÿฟ", + "๐Ÿค", + "๐Ÿค๐Ÿป", + "๐Ÿค๐Ÿผ", + "๐Ÿค๐Ÿฝ", + "๐Ÿค๐Ÿพ", + "๐Ÿค๐Ÿฟ", + "๐Ÿซฑ๐Ÿปโ€๐Ÿซฒ๐Ÿผ", + "๐Ÿซฑ๐Ÿปโ€๐Ÿซฒ๐Ÿฝ", + "๐Ÿซฑ๐Ÿปโ€๐Ÿซฒ๐Ÿพ", + "๐Ÿซฑ๐Ÿปโ€๐Ÿซฒ๐Ÿฟ", + "๐Ÿซฑ๐Ÿผโ€๐Ÿซฒ๐Ÿป", + "๐Ÿซฑ๐Ÿผโ€๐Ÿซฒ๐Ÿฝ", + "๐Ÿซฑ๐Ÿผโ€๐Ÿซฒ๐Ÿพ", + "๐Ÿซฑ๐Ÿผโ€๐Ÿซฒ๐Ÿฟ", + "๐Ÿซฑ๐Ÿฝโ€๐Ÿซฒ๐Ÿป", + "๐Ÿซฑ๐Ÿฝโ€๐Ÿซฒ๐Ÿผ", + "๐Ÿซฑ๐Ÿฝโ€๐Ÿซฒ๐Ÿพ", + "๐Ÿซฑ๐Ÿฝโ€๐Ÿซฒ๐Ÿฟ", + "๐Ÿซฑ๐Ÿพโ€๐Ÿซฒ๐Ÿป", + "๐Ÿซฑ๐Ÿพโ€๐Ÿซฒ๐Ÿผ", + "๐Ÿซฑ๐Ÿพโ€๐Ÿซฒ๐Ÿฝ", + "๐Ÿซฑ๐Ÿพโ€๐Ÿซฒ๐Ÿฟ", + "๐Ÿซฑ๐Ÿฟโ€๐Ÿซฒ๐Ÿป", + "๐Ÿซฑ๐Ÿฟโ€๐Ÿซฒ๐Ÿผ", + "๐Ÿซฑ๐Ÿฟโ€๐Ÿซฒ๐Ÿฝ", + "๐Ÿซฑ๐Ÿฟโ€๐Ÿซฒ๐Ÿพ", + "๐Ÿ™", + "๐Ÿ™๐Ÿป", + "๐Ÿ™๐Ÿผ", + "๐Ÿ™๐Ÿฝ", + "๐Ÿ™๐Ÿพ", + "๐Ÿ™๐Ÿฟ", + "โœ๏ธ", + "โœ๐Ÿป", + "โœ๐Ÿผ", + "โœ๐Ÿฝ", + "โœ๐Ÿพ", + "โœ๐Ÿฟ", + "๐Ÿ’…", + "๐Ÿ’…๐Ÿป", + "๐Ÿ’…๐Ÿผ", + "๐Ÿ’…๐Ÿฝ", + "๐Ÿ’…๐Ÿพ", + "๐Ÿ’…๐Ÿฟ", + "๐Ÿคณ", + "๐Ÿคณ๐Ÿป", + "๐Ÿคณ๐Ÿผ", + "๐Ÿคณ๐Ÿฝ", + "๐Ÿคณ๐Ÿพ", + "๐Ÿคณ๐Ÿฟ", + "๐Ÿ’ช", + "๐Ÿ’ช๐Ÿป", + "๐Ÿ’ช๐Ÿผ", + "๐Ÿ’ช๐Ÿฝ", + "๐Ÿ’ช๐Ÿพ", + "๐Ÿ’ช๐Ÿฟ", + "๐Ÿฆพ", + "๐Ÿฆฟ", + "๐Ÿฆต", + "๐Ÿฆต๐Ÿป", + "๐Ÿฆต๐Ÿผ", + "๐Ÿฆต๐Ÿฝ", + "๐Ÿฆต๐Ÿพ", + "๐Ÿฆต๐Ÿฟ", + "๐Ÿฆถ", + "๐Ÿฆถ๐Ÿป", + "๐Ÿฆถ๐Ÿผ", + "๐Ÿฆถ๐Ÿฝ", + "๐Ÿฆถ๐Ÿพ", + "๐Ÿฆถ๐Ÿฟ", + "๐Ÿ‘‚", + "๐Ÿ‘‚๐Ÿป", + "๐Ÿ‘‚๐Ÿผ", + "๐Ÿ‘‚๐Ÿฝ", + "๐Ÿ‘‚๐Ÿพ", + "๐Ÿ‘‚๐Ÿฟ", + "๐Ÿฆป", + "๐Ÿฆป๐Ÿป", + "๐Ÿฆป๐Ÿผ", + "๐Ÿฆป๐Ÿฝ", + "๐Ÿฆป๐Ÿพ", + "๐Ÿฆป๐Ÿฟ", + "๐Ÿ‘ƒ", + "๐Ÿ‘ƒ๐Ÿป", + "๐Ÿ‘ƒ๐Ÿผ", + "๐Ÿ‘ƒ๐Ÿฝ", + "๐Ÿ‘ƒ๐Ÿพ", + "๐Ÿ‘ƒ๐Ÿฟ", + "๐Ÿง ", + "๐Ÿซ€", + "๐Ÿซ", + "๐Ÿฆท", + "๐Ÿฆด", + "๐Ÿ‘€", + "๐Ÿ‘๏ธ", + "๐Ÿ‘…", + "๐Ÿ‘„", + "๐Ÿซฆ", + "๐Ÿ‘ถ", + "๐Ÿ‘ถ๐Ÿป", + "๐Ÿ‘ถ๐Ÿผ", + "๐Ÿ‘ถ๐Ÿฝ", + "๐Ÿ‘ถ๐Ÿพ", + "๐Ÿ‘ถ๐Ÿฟ", + "๐Ÿง’", + "๐Ÿง’๐Ÿป", + "๐Ÿง’๐Ÿผ", + "๐Ÿง’๐Ÿฝ", + "๐Ÿง’๐Ÿพ", + "๐Ÿง’๐Ÿฟ", + "๐Ÿ‘ฆ", + "๐Ÿ‘ฆ๐Ÿป", + "๐Ÿ‘ฆ๐Ÿผ", + "๐Ÿ‘ฆ๐Ÿฝ", + "๐Ÿ‘ฆ๐Ÿพ", + "๐Ÿ‘ฆ๐Ÿฟ", + "๐Ÿ‘ง", + "๐Ÿ‘ง๐Ÿป", + "๐Ÿ‘ง๐Ÿผ", + "๐Ÿ‘ง๐Ÿฝ", + "๐Ÿ‘ง๐Ÿพ", + "๐Ÿ‘ง๐Ÿฟ", + "๐Ÿง‘", + "๐Ÿง‘๐Ÿป", + "๐Ÿง‘๐Ÿผ", + "๐Ÿง‘๐Ÿฝ", + "๐Ÿง‘๐Ÿพ", + "๐Ÿง‘๐Ÿฟ", + "๐Ÿ‘ฑ", + "๐Ÿ‘ฑ๐Ÿป", + "๐Ÿ‘ฑ๐Ÿผ", + "๐Ÿ‘ฑ๐Ÿฝ", + "๐Ÿ‘ฑ๐Ÿพ", + "๐Ÿ‘ฑ๐Ÿฟ", + "๐Ÿ‘จ", + "๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘จ๐Ÿฟ", + "๐Ÿง”", + "๐Ÿง”๐Ÿป", + "๐Ÿง”๐Ÿผ", + "๐Ÿง”๐Ÿฝ", + "๐Ÿง”๐Ÿพ", + "๐Ÿง”๐Ÿฟ", + "๐Ÿง”โ€โ™‚๏ธ", + "๐Ÿง”๐Ÿปโ€โ™‚๏ธ", + "๐Ÿง”๐Ÿผโ€โ™‚๏ธ", + "๐Ÿง”๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿง”๐Ÿพโ€โ™‚๏ธ", + "๐Ÿง”๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿง”โ€โ™€๏ธ", + "๐Ÿง”๐Ÿปโ€โ™€๏ธ", + "๐Ÿง”๐Ÿผโ€โ™€๏ธ", + "๐Ÿง”๐Ÿฝโ€โ™€๏ธ", + "๐Ÿง”๐Ÿพโ€โ™€๏ธ", + "๐Ÿง”๐Ÿฟโ€โ™€๏ธ", + "๐Ÿ‘จโ€๐Ÿฆฐ", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฐ", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿฆฐ", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿฆฐ", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿฆฐ", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆฐ", + "๐Ÿ‘จโ€๐Ÿฆฑ", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฑ", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿฆฑ", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿฆฑ", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿฆฑ", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆฑ", + "๐Ÿ‘จโ€๐Ÿฆณ", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿฆณ", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿฆณ", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿฆณ", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿฆณ", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆณ", + "๐Ÿ‘จโ€๐Ÿฆฒ", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฒ", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿฆฒ", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿฆฒ", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿฆฒ", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆฒ", + "๐Ÿ‘ฉ", + "๐Ÿ‘ฉ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿฟ", + "๐Ÿ‘ฉโ€๐Ÿฆฐ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿฆฐ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿฆฐ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿฆฐ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿฆฐ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿฆฐ", + "๐Ÿง‘โ€๐Ÿฆฐ", + "๐Ÿง‘๐Ÿปโ€๐Ÿฆฐ", + "๐Ÿง‘๐Ÿผโ€๐Ÿฆฐ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿฆฐ", + "๐Ÿง‘๐Ÿพโ€๐Ÿฆฐ", + "๐Ÿง‘๐Ÿฟโ€๐Ÿฆฐ", + "๐Ÿ‘ฉโ€๐Ÿฆฑ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿฆฑ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿฆฑ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿฆฑ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿฆฑ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿฆฑ", + "๐Ÿง‘โ€๐Ÿฆฑ", + "๐Ÿง‘๐Ÿปโ€๐Ÿฆฑ", + "๐Ÿง‘๐Ÿผโ€๐Ÿฆฑ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿฆฑ", + "๐Ÿง‘๐Ÿพโ€๐Ÿฆฑ", + "๐Ÿง‘๐Ÿฟโ€๐Ÿฆฑ", + "๐Ÿ‘ฉโ€๐Ÿฆณ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿฆณ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿฆณ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿฆณ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿฆณ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿฆณ", + "๐Ÿง‘โ€๐Ÿฆณ", + "๐Ÿง‘๐Ÿปโ€๐Ÿฆณ", + "๐Ÿง‘๐Ÿผโ€๐Ÿฆณ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿฆณ", + "๐Ÿง‘๐Ÿพโ€๐Ÿฆณ", + "๐Ÿง‘๐Ÿฟโ€๐Ÿฆณ", + "๐Ÿ‘ฉโ€๐Ÿฆฒ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿฆฒ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿฆฒ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿฆฒ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿฆฒ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿฆฒ", + "๐Ÿง‘โ€๐Ÿฆฒ", + "๐Ÿง‘๐Ÿปโ€๐Ÿฆฒ", + "๐Ÿง‘๐Ÿผโ€๐Ÿฆฒ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿฆฒ", + "๐Ÿง‘๐Ÿพโ€๐Ÿฆฒ", + "๐Ÿง‘๐Ÿฟโ€๐Ÿฆฒ", + "๐Ÿ‘ฑโ€โ™€๏ธ", + "๐Ÿ‘ฑ๐Ÿปโ€โ™€๏ธ", + "๐Ÿ‘ฑ๐Ÿผโ€โ™€๏ธ", + "๐Ÿ‘ฑ๐Ÿฝโ€โ™€๏ธ", + "๐Ÿ‘ฑ๐Ÿพโ€โ™€๏ธ", + "๐Ÿ‘ฑ๐Ÿฟโ€โ™€๏ธ", + "๐Ÿ‘ฑโ€โ™‚๏ธ", + "๐Ÿ‘ฑ๐Ÿปโ€โ™‚๏ธ", + "๐Ÿ‘ฑ๐Ÿผโ€โ™‚๏ธ", + "๐Ÿ‘ฑ๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿ‘ฑ๐Ÿพโ€โ™‚๏ธ", + "๐Ÿ‘ฑ๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿง“", + "๐Ÿง“๐Ÿป", + "๐Ÿง“๐Ÿผ", + "๐Ÿง“๐Ÿฝ", + "๐Ÿง“๐Ÿพ", + "๐Ÿง“๐Ÿฟ", + "๐Ÿ‘ด", + "๐Ÿ‘ด๐Ÿป", + "๐Ÿ‘ด๐Ÿผ", + "๐Ÿ‘ด๐Ÿฝ", + "๐Ÿ‘ด๐Ÿพ", + "๐Ÿ‘ด๐Ÿฟ", + "๐Ÿ‘ต", + "๐Ÿ‘ต๐Ÿป", + "๐Ÿ‘ต๐Ÿผ", + "๐Ÿ‘ต๐Ÿฝ", + "๐Ÿ‘ต๐Ÿพ", + "๐Ÿ‘ต๐Ÿฟ", + "๐Ÿ™", + "๐Ÿ™๐Ÿป", + "๐Ÿ™๐Ÿผ", + "๐Ÿ™๐Ÿฝ", + "๐Ÿ™๐Ÿพ", + "๐Ÿ™๐Ÿฟ", + "๐Ÿ™โ€โ™‚๏ธ", + "๐Ÿ™๐Ÿปโ€โ™‚๏ธ", + "๐Ÿ™๐Ÿผโ€โ™‚๏ธ", + "๐Ÿ™๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿ™๐Ÿพโ€โ™‚๏ธ", + "๐Ÿ™๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿ™โ€โ™€๏ธ", + "๐Ÿ™๐Ÿปโ€โ™€๏ธ", + "๐Ÿ™๐Ÿผโ€โ™€๏ธ", + "๐Ÿ™๐Ÿฝโ€โ™€๏ธ", + "๐Ÿ™๐Ÿพโ€โ™€๏ธ", + "๐Ÿ™๐Ÿฟโ€โ™€๏ธ", + "๐Ÿ™Ž", + "๐Ÿ™Ž๐Ÿป", + "๐Ÿ™Ž๐Ÿผ", + "๐Ÿ™Ž๐Ÿฝ", + "๐Ÿ™Ž๐Ÿพ", + "๐Ÿ™Ž๐Ÿฟ", + "๐Ÿ™Žโ€โ™‚๏ธ", + "๐Ÿ™Ž๐Ÿปโ€โ™‚๏ธ", + "๐Ÿ™Ž๐Ÿผโ€โ™‚๏ธ", + "๐Ÿ™Ž๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿ™Ž๐Ÿพโ€โ™‚๏ธ", + "๐Ÿ™Ž๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿ™Žโ€โ™€๏ธ", + "๐Ÿ™Ž๐Ÿปโ€โ™€๏ธ", + "๐Ÿ™Ž๐Ÿผโ€โ™€๏ธ", + "๐Ÿ™Ž๐Ÿฝโ€โ™€๏ธ", + "๐Ÿ™Ž๐Ÿพโ€โ™€๏ธ", + "๐Ÿ™Ž๐Ÿฟโ€โ™€๏ธ", + "๐Ÿ™…", + "๐Ÿ™…๐Ÿป", + "๐Ÿ™…๐Ÿผ", + "๐Ÿ™…๐Ÿฝ", + "๐Ÿ™…๐Ÿพ", + "๐Ÿ™…๐Ÿฟ", + "๐Ÿ™…โ€โ™‚๏ธ", + "๐Ÿ™…๐Ÿปโ€โ™‚๏ธ", + "๐Ÿ™…๐Ÿผโ€โ™‚๏ธ", + "๐Ÿ™…๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿ™…๐Ÿพโ€โ™‚๏ธ", + "๐Ÿ™…๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿ™…โ€โ™€๏ธ", + "๐Ÿ™…๐Ÿปโ€โ™€๏ธ", + "๐Ÿ™…๐Ÿผโ€โ™€๏ธ", + "๐Ÿ™…๐Ÿฝโ€โ™€๏ธ", + "๐Ÿ™…๐Ÿพโ€โ™€๏ธ", + "๐Ÿ™…๐Ÿฟโ€โ™€๏ธ", + "๐Ÿ™†", + "๐Ÿ™†๐Ÿป", + "๐Ÿ™†๐Ÿผ", + "๐Ÿ™†๐Ÿฝ", + "๐Ÿ™†๐Ÿพ", + "๐Ÿ™†๐Ÿฟ", + "๐Ÿ™†โ€โ™‚๏ธ", + "๐Ÿ™†๐Ÿปโ€โ™‚๏ธ", + "๐Ÿ™†๐Ÿผโ€โ™‚๏ธ", + "๐Ÿ™†๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿ™†๐Ÿพโ€โ™‚๏ธ", + "๐Ÿ™†๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿ™†โ€โ™€๏ธ", + "๐Ÿ™†๐Ÿปโ€โ™€๏ธ", + "๐Ÿ™†๐Ÿผโ€โ™€๏ธ", + "๐Ÿ™†๐Ÿฝโ€โ™€๏ธ", + "๐Ÿ™†๐Ÿพโ€โ™€๏ธ", + "๐Ÿ™†๐Ÿฟโ€โ™€๏ธ", + "๐Ÿ’", + "๐Ÿ’๐Ÿป", + "๐Ÿ’๐Ÿผ", + "๐Ÿ’๐Ÿฝ", + "๐Ÿ’๐Ÿพ", + "๐Ÿ’๐Ÿฟ", + "๐Ÿ’โ€โ™‚๏ธ", + "๐Ÿ’๐Ÿปโ€โ™‚๏ธ", + "๐Ÿ’๐Ÿผโ€โ™‚๏ธ", + "๐Ÿ’๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿ’๐Ÿพโ€โ™‚๏ธ", + "๐Ÿ’๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿ’โ€โ™€๏ธ", + "๐Ÿ’๐Ÿปโ€โ™€๏ธ", + "๐Ÿ’๐Ÿผโ€โ™€๏ธ", + "๐Ÿ’๐Ÿฝโ€โ™€๏ธ", + "๐Ÿ’๐Ÿพโ€โ™€๏ธ", + "๐Ÿ’๐Ÿฟโ€โ™€๏ธ", + "๐Ÿ™‹", + "๐Ÿ™‹๐Ÿป", + "๐Ÿ™‹๐Ÿผ", + "๐Ÿ™‹๐Ÿฝ", + "๐Ÿ™‹๐Ÿพ", + "๐Ÿ™‹๐Ÿฟ", + "๐Ÿ™‹โ€โ™‚๏ธ", + "๐Ÿ™‹๐Ÿปโ€โ™‚๏ธ", + "๐Ÿ™‹๐Ÿผโ€โ™‚๏ธ", + "๐Ÿ™‹๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿ™‹๐Ÿพโ€โ™‚๏ธ", + "๐Ÿ™‹๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿ™‹โ€โ™€๏ธ", + "๐Ÿ™‹๐Ÿปโ€โ™€๏ธ", + "๐Ÿ™‹๐Ÿผโ€โ™€๏ธ", + "๐Ÿ™‹๐Ÿฝโ€โ™€๏ธ", + "๐Ÿ™‹๐Ÿพโ€โ™€๏ธ", + "๐Ÿ™‹๐Ÿฟโ€โ™€๏ธ", + "๐Ÿง", + "๐Ÿง๐Ÿป", + "๐Ÿง๐Ÿผ", + "๐Ÿง๐Ÿฝ", + "๐Ÿง๐Ÿพ", + "๐Ÿง๐Ÿฟ", + "๐Ÿงโ€โ™‚๏ธ", + "๐Ÿง๐Ÿปโ€โ™‚๏ธ", + "๐Ÿง๐Ÿผโ€โ™‚๏ธ", + "๐Ÿง๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿง๐Ÿพโ€โ™‚๏ธ", + "๐Ÿง๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿงโ€โ™€๏ธ", + "๐Ÿง๐Ÿปโ€โ™€๏ธ", + "๐Ÿง๐Ÿผโ€โ™€๏ธ", + "๐Ÿง๐Ÿฝโ€โ™€๏ธ", + "๐Ÿง๐Ÿพโ€โ™€๏ธ", + "๐Ÿง๐Ÿฟโ€โ™€๏ธ", + "๐Ÿ™‡", + "๐Ÿ™‡๐Ÿป", + "๐Ÿ™‡๐Ÿผ", + "๐Ÿ™‡๐Ÿฝ", + "๐Ÿ™‡๐Ÿพ", + "๐Ÿ™‡๐Ÿฟ", + "๐Ÿ™‡โ€โ™‚๏ธ", + "๐Ÿ™‡๐Ÿปโ€โ™‚๏ธ", + "๐Ÿ™‡๐Ÿผโ€โ™‚๏ธ", + "๐Ÿ™‡๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿ™‡๐Ÿพโ€โ™‚๏ธ", + "๐Ÿ™‡๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿ™‡โ€โ™€๏ธ", + "๐Ÿ™‡๐Ÿปโ€โ™€๏ธ", + "๐Ÿ™‡๐Ÿผโ€โ™€๏ธ", + "๐Ÿ™‡๐Ÿฝโ€โ™€๏ธ", + "๐Ÿ™‡๐Ÿพโ€โ™€๏ธ", + "๐Ÿ™‡๐Ÿฟโ€โ™€๏ธ", + "๐Ÿคฆ", + "๐Ÿคฆ๐Ÿป", + "๐Ÿคฆ๐Ÿผ", + "๐Ÿคฆ๐Ÿฝ", + "๐Ÿคฆ๐Ÿพ", + "๐Ÿคฆ๐Ÿฟ", + "๐Ÿคฆโ€โ™‚๏ธ", + "๐Ÿคฆ๐Ÿปโ€โ™‚๏ธ", + "๐Ÿคฆ๐Ÿผโ€โ™‚๏ธ", + "๐Ÿคฆ๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿคฆ๐Ÿพโ€โ™‚๏ธ", + "๐Ÿคฆ๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿคฆโ€โ™€๏ธ", + "๐Ÿคฆ๐Ÿปโ€โ™€๏ธ", + "๐Ÿคฆ๐Ÿผโ€โ™€๏ธ", + "๐Ÿคฆ๐Ÿฝโ€โ™€๏ธ", + "๐Ÿคฆ๐Ÿพโ€โ™€๏ธ", + "๐Ÿคฆ๐Ÿฟโ€โ™€๏ธ", + "๐Ÿคท", + "๐Ÿคท๐Ÿป", + "๐Ÿคท๐Ÿผ", + "๐Ÿคท๐Ÿฝ", + "๐Ÿคท๐Ÿพ", + "๐Ÿคท๐Ÿฟ", + "๐Ÿคทโ€โ™‚๏ธ", + "๐Ÿคท๐Ÿปโ€โ™‚๏ธ", + "๐Ÿคท๐Ÿผโ€โ™‚๏ธ", + "๐Ÿคท๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿคท๐Ÿพโ€โ™‚๏ธ", + "๐Ÿคท๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿคทโ€โ™€๏ธ", + "๐Ÿคท๐Ÿปโ€โ™€๏ธ", + "๐Ÿคท๐Ÿผโ€โ™€๏ธ", + "๐Ÿคท๐Ÿฝโ€โ™€๏ธ", + "๐Ÿคท๐Ÿพโ€โ™€๏ธ", + "๐Ÿคท๐Ÿฟโ€โ™€๏ธ", + "๐Ÿง‘โ€โš•๏ธ", + "๐Ÿง‘๐Ÿปโ€โš•๏ธ", + "๐Ÿง‘๐Ÿผโ€โš•๏ธ", + "๐Ÿง‘๐Ÿฝโ€โš•๏ธ", + "๐Ÿง‘๐Ÿพโ€โš•๏ธ", + "๐Ÿง‘๐Ÿฟโ€โš•๏ธ", + "๐Ÿ‘จโ€โš•๏ธ", + "๐Ÿ‘จ๐Ÿปโ€โš•๏ธ", + "๐Ÿ‘จ๐Ÿผโ€โš•๏ธ", + "๐Ÿ‘จ๐Ÿฝโ€โš•๏ธ", + "๐Ÿ‘จ๐Ÿพโ€โš•๏ธ", + "๐Ÿ‘จ๐Ÿฟโ€โš•๏ธ", + "๐Ÿ‘ฉโ€โš•๏ธ", + "๐Ÿ‘ฉ๐Ÿปโ€โš•๏ธ", + "๐Ÿ‘ฉ๐Ÿผโ€โš•๏ธ", + "๐Ÿ‘ฉ๐Ÿฝโ€โš•๏ธ", + "๐Ÿ‘ฉ๐Ÿพโ€โš•๏ธ", + "๐Ÿ‘ฉ๐Ÿฟโ€โš•๏ธ", + "๐Ÿง‘โ€๐ŸŽ“", + "๐Ÿง‘๐Ÿปโ€๐ŸŽ“", + "๐Ÿง‘๐Ÿผโ€๐ŸŽ“", + "๐Ÿง‘๐Ÿฝโ€๐ŸŽ“", + "๐Ÿง‘๐Ÿพโ€๐ŸŽ“", + "๐Ÿง‘๐Ÿฟโ€๐ŸŽ“", + "๐Ÿ‘จโ€๐ŸŽ“", + "๐Ÿ‘จ๐Ÿปโ€๐ŸŽ“", + "๐Ÿ‘จ๐Ÿผโ€๐ŸŽ“", + "๐Ÿ‘จ๐Ÿฝโ€๐ŸŽ“", + "๐Ÿ‘จ๐Ÿพโ€๐ŸŽ“", + "๐Ÿ‘จ๐Ÿฟโ€๐ŸŽ“", + "๐Ÿ‘ฉโ€๐ŸŽ“", + "๐Ÿ‘ฉ๐Ÿปโ€๐ŸŽ“", + "๐Ÿ‘ฉ๐Ÿผโ€๐ŸŽ“", + "๐Ÿ‘ฉ๐Ÿฝโ€๐ŸŽ“", + "๐Ÿ‘ฉ๐Ÿพโ€๐ŸŽ“", + "๐Ÿ‘ฉ๐Ÿฟโ€๐ŸŽ“", + "๐Ÿง‘โ€๐Ÿซ", + "๐Ÿง‘๐Ÿปโ€๐Ÿซ", + "๐Ÿง‘๐Ÿผโ€๐Ÿซ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿซ", + "๐Ÿง‘๐Ÿพโ€๐Ÿซ", + "๐Ÿง‘๐Ÿฟโ€๐Ÿซ", + "๐Ÿ‘จโ€๐Ÿซ", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿซ", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿซ", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿซ", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿซ", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿซ", + "๐Ÿ‘ฉโ€๐Ÿซ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿซ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿซ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿซ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿซ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿซ", + "๐Ÿง‘โ€โš–๏ธ", + "๐Ÿง‘๐Ÿปโ€โš–๏ธ", + "๐Ÿง‘๐Ÿผโ€โš–๏ธ", + "๐Ÿง‘๐Ÿฝโ€โš–๏ธ", + "๐Ÿง‘๐Ÿพโ€โš–๏ธ", + "๐Ÿง‘๐Ÿฟโ€โš–๏ธ", + "๐Ÿ‘จโ€โš–๏ธ", + "๐Ÿ‘จ๐Ÿปโ€โš–๏ธ", + "๐Ÿ‘จ๐Ÿผโ€โš–๏ธ", + "๐Ÿ‘จ๐Ÿฝโ€โš–๏ธ", + "๐Ÿ‘จ๐Ÿพโ€โš–๏ธ", + "๐Ÿ‘จ๐Ÿฟโ€โš–๏ธ", + "๐Ÿ‘ฉโ€โš–๏ธ", + "๐Ÿ‘ฉ๐Ÿปโ€โš–๏ธ", + "๐Ÿ‘ฉ๐Ÿผโ€โš–๏ธ", + "๐Ÿ‘ฉ๐Ÿฝโ€โš–๏ธ", + "๐Ÿ‘ฉ๐Ÿพโ€โš–๏ธ", + "๐Ÿ‘ฉ๐Ÿฟโ€โš–๏ธ", + "๐Ÿง‘โ€๐ŸŒพ", + "๐Ÿง‘๐Ÿปโ€๐ŸŒพ", + "๐Ÿง‘๐Ÿผโ€๐ŸŒพ", + "๐Ÿง‘๐Ÿฝโ€๐ŸŒพ", + "๐Ÿง‘๐Ÿพโ€๐ŸŒพ", + "๐Ÿง‘๐Ÿฟโ€๐ŸŒพ", + "๐Ÿ‘จโ€๐ŸŒพ", + "๐Ÿ‘จ๐Ÿปโ€๐ŸŒพ", + "๐Ÿ‘จ๐Ÿผโ€๐ŸŒพ", + "๐Ÿ‘จ๐Ÿฝโ€๐ŸŒพ", + "๐Ÿ‘จ๐Ÿพโ€๐ŸŒพ", + "๐Ÿ‘จ๐Ÿฟโ€๐ŸŒพ", + "๐Ÿ‘ฉโ€๐ŸŒพ", + "๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ", + "๐Ÿ‘ฉ๐Ÿผโ€๐ŸŒพ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐ŸŒพ", + "๐Ÿ‘ฉ๐Ÿพโ€๐ŸŒพ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐ŸŒพ", + "๐Ÿง‘โ€๐Ÿณ", + "๐Ÿง‘๐Ÿปโ€๐Ÿณ", + "๐Ÿง‘๐Ÿผโ€๐Ÿณ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿณ", + "๐Ÿง‘๐Ÿพโ€๐Ÿณ", + "๐Ÿง‘๐Ÿฟโ€๐Ÿณ", + "๐Ÿ‘จโ€๐Ÿณ", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿณ", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿณ", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿณ", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿณ", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿณ", + "๐Ÿ‘ฉโ€๐Ÿณ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿณ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿณ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿณ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿณ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿณ", + "๐Ÿง‘โ€๐Ÿ”ง", + "๐Ÿง‘๐Ÿปโ€๐Ÿ”ง", + "๐Ÿง‘๐Ÿผโ€๐Ÿ”ง", + "๐Ÿง‘๐Ÿฝโ€๐Ÿ”ง", + "๐Ÿง‘๐Ÿพโ€๐Ÿ”ง", + "๐Ÿง‘๐Ÿฟโ€๐Ÿ”ง", + "๐Ÿ‘จโ€๐Ÿ”ง", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿ”ง", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿ”ง", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿ”ง", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿ”ง", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿ”ง", + "๐Ÿ‘ฉโ€๐Ÿ”ง", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿ”ง", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿ”ง", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ”ง", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿ”ง", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿ”ง", + "๐Ÿง‘โ€๐Ÿญ", + "๐Ÿง‘๐Ÿปโ€๐Ÿญ", + "๐Ÿง‘๐Ÿผโ€๐Ÿญ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿญ", + "๐Ÿง‘๐Ÿพโ€๐Ÿญ", + "๐Ÿง‘๐Ÿฟโ€๐Ÿญ", + "๐Ÿ‘จโ€๐Ÿญ", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿญ", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿญ", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿญ", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿญ", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿญ", + "๐Ÿ‘ฉโ€๐Ÿญ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿญ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿญ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿญ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿญ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿญ", + "๐Ÿง‘โ€๐Ÿ’ผ", + "๐Ÿง‘๐Ÿปโ€๐Ÿ’ผ", + "๐Ÿง‘๐Ÿผโ€๐Ÿ’ผ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿ’ผ", + "๐Ÿง‘๐Ÿพโ€๐Ÿ’ผ", + "๐Ÿง‘๐Ÿฟโ€๐Ÿ’ผ", + "๐Ÿ‘จโ€๐Ÿ’ผ", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ผ", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿ’ผ", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿ’ผ", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿ’ผ", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿ’ผ", + "๐Ÿ‘ฉโ€๐Ÿ’ผ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿ’ผ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿ’ผ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ’ผ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿ’ผ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿ’ผ", + "๐Ÿง‘โ€๐Ÿ”ฌ", + "๐Ÿง‘๐Ÿปโ€๐Ÿ”ฌ", + "๐Ÿง‘๐Ÿผโ€๐Ÿ”ฌ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿ”ฌ", + "๐Ÿง‘๐Ÿพโ€๐Ÿ”ฌ", + "๐Ÿง‘๐Ÿฟโ€๐Ÿ”ฌ", + "๐Ÿ‘จโ€๐Ÿ”ฌ", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿ”ฌ", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿ”ฌ", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿ”ฌ", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿ”ฌ", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿ”ฌ", + "๐Ÿ‘ฉโ€๐Ÿ”ฌ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿ”ฌ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿ”ฌ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ”ฌ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿ”ฌ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿ”ฌ", + "๐Ÿง‘โ€๐Ÿ’ป", + "๐Ÿง‘๐Ÿปโ€๐Ÿ’ป", + "๐Ÿง‘๐Ÿผโ€๐Ÿ’ป", + "๐Ÿง‘๐Ÿฝโ€๐Ÿ’ป", + "๐Ÿง‘๐Ÿพโ€๐Ÿ’ป", + "๐Ÿง‘๐Ÿฟโ€๐Ÿ’ป", + "๐Ÿ‘จโ€๐Ÿ’ป", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿ’ป", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿ’ป", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿ’ป", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿ’ป", + "๐Ÿ‘ฉโ€๐Ÿ’ป", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿ’ป", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿ’ป", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ’ป", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿ’ป", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿ’ป", + "๐Ÿง‘โ€๐ŸŽค", + "๐Ÿง‘๐Ÿปโ€๐ŸŽค", + "๐Ÿง‘๐Ÿผโ€๐ŸŽค", + "๐Ÿง‘๐Ÿฝโ€๐ŸŽค", + "๐Ÿง‘๐Ÿพโ€๐ŸŽค", + "๐Ÿง‘๐Ÿฟโ€๐ŸŽค", + "๐Ÿ‘จโ€๐ŸŽค", + "๐Ÿ‘จ๐Ÿปโ€๐ŸŽค", + "๐Ÿ‘จ๐Ÿผโ€๐ŸŽค", + "๐Ÿ‘จ๐Ÿฝโ€๐ŸŽค", + "๐Ÿ‘จ๐Ÿพโ€๐ŸŽค", + "๐Ÿ‘จ๐Ÿฟโ€๐ŸŽค", + "๐Ÿ‘ฉโ€๐ŸŽค", + "๐Ÿ‘ฉ๐Ÿปโ€๐ŸŽค", + "๐Ÿ‘ฉ๐Ÿผโ€๐ŸŽค", + "๐Ÿ‘ฉ๐Ÿฝโ€๐ŸŽค", + "๐Ÿ‘ฉ๐Ÿพโ€๐ŸŽค", + "๐Ÿ‘ฉ๐Ÿฟโ€๐ŸŽค", + "๐Ÿง‘โ€๐ŸŽจ", + "๐Ÿง‘๐Ÿปโ€๐ŸŽจ", + "๐Ÿง‘๐Ÿผโ€๐ŸŽจ", + "๐Ÿง‘๐Ÿฝโ€๐ŸŽจ", + "๐Ÿง‘๐Ÿพโ€๐ŸŽจ", + "๐Ÿง‘๐Ÿฟโ€๐ŸŽจ", + "๐Ÿ‘จโ€๐ŸŽจ", + "๐Ÿ‘จ๐Ÿปโ€๐ŸŽจ", + "๐Ÿ‘จ๐Ÿผโ€๐ŸŽจ", + "๐Ÿ‘จ๐Ÿฝโ€๐ŸŽจ", + "๐Ÿ‘จ๐Ÿพโ€๐ŸŽจ", + "๐Ÿ‘จ๐Ÿฟโ€๐ŸŽจ", + "๐Ÿ‘ฉโ€๐ŸŽจ", + "๐Ÿ‘ฉ๐Ÿปโ€๐ŸŽจ", + "๐Ÿ‘ฉ๐Ÿผโ€๐ŸŽจ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐ŸŽจ", + "๐Ÿ‘ฉ๐Ÿพโ€๐ŸŽจ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐ŸŽจ", + "๐Ÿง‘โ€โœˆ๏ธ", + "๐Ÿง‘๐Ÿปโ€โœˆ๏ธ", + "๐Ÿง‘๐Ÿผโ€โœˆ๏ธ", + "๐Ÿง‘๐Ÿฝโ€โœˆ๏ธ", + "๐Ÿง‘๐Ÿพโ€โœˆ๏ธ", + "๐Ÿง‘๐Ÿฟโ€โœˆ๏ธ", + "๐Ÿ‘จโ€โœˆ๏ธ", + "๐Ÿ‘จ๐Ÿปโ€โœˆ๏ธ", + "๐Ÿ‘จ๐Ÿผโ€โœˆ๏ธ", + "๐Ÿ‘จ๐Ÿฝโ€โœˆ๏ธ", + "๐Ÿ‘จ๐Ÿพโ€โœˆ๏ธ", + "๐Ÿ‘จ๐Ÿฟโ€โœˆ๏ธ", + "๐Ÿ‘ฉโ€โœˆ๏ธ", + "๐Ÿ‘ฉ๐Ÿปโ€โœˆ๏ธ", + "๐Ÿ‘ฉ๐Ÿผโ€โœˆ๏ธ", + "๐Ÿ‘ฉ๐Ÿฝโ€โœˆ๏ธ", + "๐Ÿ‘ฉ๐Ÿพโ€โœˆ๏ธ", + "๐Ÿ‘ฉ๐Ÿฟโ€โœˆ๏ธ", + "๐Ÿง‘โ€๐Ÿš€", + "๐Ÿง‘๐Ÿปโ€๐Ÿš€", + "๐Ÿง‘๐Ÿผโ€๐Ÿš€", + "๐Ÿง‘๐Ÿฝโ€๐Ÿš€", + "๐Ÿง‘๐Ÿพโ€๐Ÿš€", + "๐Ÿง‘๐Ÿฟโ€๐Ÿš€", + "๐Ÿ‘จโ€๐Ÿš€", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿš€", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿš€", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿš€", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿš€", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿš€", + "๐Ÿ‘ฉโ€๐Ÿš€", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿš€", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿš€", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿš€", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿš€", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿš€", + "๐Ÿง‘โ€๐Ÿš’", + "๐Ÿง‘๐Ÿปโ€๐Ÿš’", + "๐Ÿง‘๐Ÿผโ€๐Ÿš’", + "๐Ÿง‘๐Ÿฝโ€๐Ÿš’", + "๐Ÿง‘๐Ÿพโ€๐Ÿš’", + "๐Ÿง‘๐Ÿฟโ€๐Ÿš’", + "๐Ÿ‘จโ€๐Ÿš’", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿš’", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿš’", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿš’", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿš’", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿš’", + "๐Ÿ‘ฉโ€๐Ÿš’", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿš’", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿš’", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿš’", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿš’", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿš’", + "๐Ÿ‘ฎ", + "๐Ÿ‘ฎ๐Ÿป", + "๐Ÿ‘ฎ๐Ÿผ", + "๐Ÿ‘ฎ๐Ÿฝ", + "๐Ÿ‘ฎ๐Ÿพ", + "๐Ÿ‘ฎ๐Ÿฟ", + "๐Ÿ‘ฎโ€โ™‚๏ธ", + "๐Ÿ‘ฎ๐Ÿปโ€โ™‚๏ธ", + "๐Ÿ‘ฎ๐Ÿผโ€โ™‚๏ธ", + "๐Ÿ‘ฎ๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿ‘ฎ๐Ÿพโ€โ™‚๏ธ", + "๐Ÿ‘ฎ๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿ‘ฎโ€โ™€๏ธ", + "๐Ÿ‘ฎ๐Ÿปโ€โ™€๏ธ", + "๐Ÿ‘ฎ๐Ÿผโ€โ™€๏ธ", + "๐Ÿ‘ฎ๐Ÿฝโ€โ™€๏ธ", + "๐Ÿ‘ฎ๐Ÿพโ€โ™€๏ธ", + "๐Ÿ‘ฎ๐Ÿฟโ€โ™€๏ธ", + "๐Ÿ•ต๏ธ", + "๐Ÿ•ต๐Ÿป", + "๐Ÿ•ต๐Ÿผ", + "๐Ÿ•ต๐Ÿฝ", + "๐Ÿ•ต๐Ÿพ", + "๐Ÿ•ต๐Ÿฟ", + "๐Ÿ•ต๏ธโ€โ™‚๏ธ", + "๐Ÿ•ต๐Ÿปโ€โ™‚๏ธ", + "๐Ÿ•ต๐Ÿผโ€โ™‚๏ธ", + "๐Ÿ•ต๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿ•ต๐Ÿพโ€โ™‚๏ธ", + "๐Ÿ•ต๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿ•ต๏ธโ€โ™€๏ธ", + "๐Ÿ•ต๐Ÿปโ€โ™€๏ธ", + "๐Ÿ•ต๐Ÿผโ€โ™€๏ธ", + "๐Ÿ•ต๐Ÿฝโ€โ™€๏ธ", + "๐Ÿ•ต๐Ÿพโ€โ™€๏ธ", + "๐Ÿ•ต๐Ÿฟโ€โ™€๏ธ", + "๐Ÿ’‚", + "๐Ÿ’‚๐Ÿป", + "๐Ÿ’‚๐Ÿผ", + "๐Ÿ’‚๐Ÿฝ", + "๐Ÿ’‚๐Ÿพ", + "๐Ÿ’‚๐Ÿฟ", + "๐Ÿ’‚โ€โ™‚๏ธ", + "๐Ÿ’‚๐Ÿปโ€โ™‚๏ธ", + "๐Ÿ’‚๐Ÿผโ€โ™‚๏ธ", + "๐Ÿ’‚๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿ’‚๐Ÿพโ€โ™‚๏ธ", + "๐Ÿ’‚๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿ’‚โ€โ™€๏ธ", + "๐Ÿ’‚๐Ÿปโ€โ™€๏ธ", + "๐Ÿ’‚๐Ÿผโ€โ™€๏ธ", + "๐Ÿ’‚๐Ÿฝโ€โ™€๏ธ", + "๐Ÿ’‚๐Ÿพโ€โ™€๏ธ", + "๐Ÿ’‚๐Ÿฟโ€โ™€๏ธ", + "๐Ÿฅท", + "๐Ÿฅท๐Ÿป", + "๐Ÿฅท๐Ÿผ", + "๐Ÿฅท๐Ÿฝ", + "๐Ÿฅท๐Ÿพ", + "๐Ÿฅท๐Ÿฟ", + "๐Ÿ‘ท", + "๐Ÿ‘ท๐Ÿป", + "๐Ÿ‘ท๐Ÿผ", + "๐Ÿ‘ท๐Ÿฝ", + "๐Ÿ‘ท๐Ÿพ", + "๐Ÿ‘ท๐Ÿฟ", + "๐Ÿ‘ทโ€โ™‚๏ธ", + "๐Ÿ‘ท๐Ÿปโ€โ™‚๏ธ", + "๐Ÿ‘ท๐Ÿผโ€โ™‚๏ธ", + "๐Ÿ‘ท๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿ‘ท๐Ÿพโ€โ™‚๏ธ", + "๐Ÿ‘ท๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿ‘ทโ€โ™€๏ธ", + "๐Ÿ‘ท๐Ÿปโ€โ™€๏ธ", + "๐Ÿ‘ท๐Ÿผโ€โ™€๏ธ", + "๐Ÿ‘ท๐Ÿฝโ€โ™€๏ธ", + "๐Ÿ‘ท๐Ÿพโ€โ™€๏ธ", + "๐Ÿ‘ท๐Ÿฟโ€โ™€๏ธ", + "๐Ÿซ…", + "๐Ÿซ…๐Ÿป", + "๐Ÿซ…๐Ÿผ", + "๐Ÿซ…๐Ÿฝ", + "๐Ÿซ…๐Ÿพ", + "๐Ÿซ…๐Ÿฟ", + "๐Ÿคด", + "๐Ÿคด๐Ÿป", + "๐Ÿคด๐Ÿผ", + "๐Ÿคด๐Ÿฝ", + "๐Ÿคด๐Ÿพ", + "๐Ÿคด๐Ÿฟ", + "๐Ÿ‘ธ", + "๐Ÿ‘ธ๐Ÿป", + "๐Ÿ‘ธ๐Ÿผ", + "๐Ÿ‘ธ๐Ÿฝ", + "๐Ÿ‘ธ๐Ÿพ", + "๐Ÿ‘ธ๐Ÿฟ", + "๐Ÿ‘ณ", + "๐Ÿ‘ณ๐Ÿป", + "๐Ÿ‘ณ๐Ÿผ", + "๐Ÿ‘ณ๐Ÿฝ", + "๐Ÿ‘ณ๐Ÿพ", + "๐Ÿ‘ณ๐Ÿฟ", + "๐Ÿ‘ณโ€โ™‚๏ธ", + "๐Ÿ‘ณ๐Ÿปโ€โ™‚๏ธ", + "๐Ÿ‘ณ๐Ÿผโ€โ™‚๏ธ", + "๐Ÿ‘ณ๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿ‘ณ๐Ÿพโ€โ™‚๏ธ", + "๐Ÿ‘ณ๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿ‘ณโ€โ™€๏ธ", + "๐Ÿ‘ณ๐Ÿปโ€โ™€๏ธ", + "๐Ÿ‘ณ๐Ÿผโ€โ™€๏ธ", + "๐Ÿ‘ณ๐Ÿฝโ€โ™€๏ธ", + "๐Ÿ‘ณ๐Ÿพโ€โ™€๏ธ", + "๐Ÿ‘ณ๐Ÿฟโ€โ™€๏ธ", + "๐Ÿ‘ฒ", + "๐Ÿ‘ฒ๐Ÿป", + "๐Ÿ‘ฒ๐Ÿผ", + "๐Ÿ‘ฒ๐Ÿฝ", + "๐Ÿ‘ฒ๐Ÿพ", + "๐Ÿ‘ฒ๐Ÿฟ", + "๐Ÿง•", + "๐Ÿง•๐Ÿป", + "๐Ÿง•๐Ÿผ", + "๐Ÿง•๐Ÿฝ", + "๐Ÿง•๐Ÿพ", + "๐Ÿง•๐Ÿฟ", + "๐Ÿคต", + "๐Ÿคต๐Ÿป", + "๐Ÿคต๐Ÿผ", + "๐Ÿคต๐Ÿฝ", + "๐Ÿคต๐Ÿพ", + "๐Ÿคต๐Ÿฟ", + "๐Ÿคตโ€โ™‚๏ธ", + "๐Ÿคต๐Ÿปโ€โ™‚๏ธ", + "๐Ÿคต๐Ÿผโ€โ™‚๏ธ", + "๐Ÿคต๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿคต๐Ÿพโ€โ™‚๏ธ", + "๐Ÿคต๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿคตโ€โ™€๏ธ", + "๐Ÿคต๐Ÿปโ€โ™€๏ธ", + "๐Ÿคต๐Ÿผโ€โ™€๏ธ", + "๐Ÿคต๐Ÿฝโ€โ™€๏ธ", + "๐Ÿคต๐Ÿพโ€โ™€๏ธ", + "๐Ÿคต๐Ÿฟโ€โ™€๏ธ", + "๐Ÿ‘ฐ", + "๐Ÿ‘ฐ๐Ÿป", + "๐Ÿ‘ฐ๐Ÿผ", + "๐Ÿ‘ฐ๐Ÿฝ", + "๐Ÿ‘ฐ๐Ÿพ", + "๐Ÿ‘ฐ๐Ÿฟ", + "๐Ÿ‘ฐโ€โ™‚๏ธ", + "๐Ÿ‘ฐ๐Ÿปโ€โ™‚๏ธ", + "๐Ÿ‘ฐ๐Ÿผโ€โ™‚๏ธ", + "๐Ÿ‘ฐ๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿ‘ฐ๐Ÿพโ€โ™‚๏ธ", + "๐Ÿ‘ฐ๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿ‘ฐโ€โ™€๏ธ", + "๐Ÿ‘ฐ๐Ÿปโ€โ™€๏ธ", + "๐Ÿ‘ฐ๐Ÿผโ€โ™€๏ธ", + "๐Ÿ‘ฐ๐Ÿฝโ€โ™€๏ธ", + "๐Ÿ‘ฐ๐Ÿพโ€โ™€๏ธ", + "๐Ÿ‘ฐ๐Ÿฟโ€โ™€๏ธ", + "๐Ÿคฐ", + "๐Ÿคฐ๐Ÿป", + "๐Ÿคฐ๐Ÿผ", + "๐Ÿคฐ๐Ÿฝ", + "๐Ÿคฐ๐Ÿพ", + "๐Ÿคฐ๐Ÿฟ", + "๐Ÿซƒ", + "๐Ÿซƒ๐Ÿป", + "๐Ÿซƒ๐Ÿผ", + "๐Ÿซƒ๐Ÿฝ", + "๐Ÿซƒ๐Ÿพ", + "๐Ÿซƒ๐Ÿฟ", + "๐Ÿซ„", + "๐Ÿซ„๐Ÿป", + "๐Ÿซ„๐Ÿผ", + "๐Ÿซ„๐Ÿฝ", + "๐Ÿซ„๐Ÿพ", + "๐Ÿซ„๐Ÿฟ", + "๐Ÿคฑ", + "๐Ÿคฑ๐Ÿป", + "๐Ÿคฑ๐Ÿผ", + "๐Ÿคฑ๐Ÿฝ", + "๐Ÿคฑ๐Ÿพ", + "๐Ÿคฑ๐Ÿฟ", + "๐Ÿ‘ฉโ€๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿผ", + "๐Ÿ‘จโ€๐Ÿผ", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿผ", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿผ", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿผ", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿผ", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿผ", + "๐Ÿง‘โ€๐Ÿผ", + "๐Ÿง‘๐Ÿปโ€๐Ÿผ", + "๐Ÿง‘๐Ÿผโ€๐Ÿผ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿผ", + "๐Ÿง‘๐Ÿพโ€๐Ÿผ", + "๐Ÿง‘๐Ÿฟโ€๐Ÿผ", + "๐Ÿ‘ผ", + "๐Ÿ‘ผ๐Ÿป", + "๐Ÿ‘ผ๐Ÿผ", + "๐Ÿ‘ผ๐Ÿฝ", + "๐Ÿ‘ผ๐Ÿพ", + "๐Ÿ‘ผ๐Ÿฟ", + "๐ŸŽ…", + "๐ŸŽ…๐Ÿป", + "๐ŸŽ…๐Ÿผ", + "๐ŸŽ…๐Ÿฝ", + "๐ŸŽ…๐Ÿพ", + "๐ŸŽ…๐Ÿฟ", + "๐Ÿคถ", + "๐Ÿคถ๐Ÿป", + "๐Ÿคถ๐Ÿผ", + "๐Ÿคถ๐Ÿฝ", + "๐Ÿคถ๐Ÿพ", + "๐Ÿคถ๐Ÿฟ", + "๐Ÿง‘โ€๐ŸŽ„", + "๐Ÿง‘๐Ÿปโ€๐ŸŽ„", + "๐Ÿง‘๐Ÿผโ€๐ŸŽ„", + "๐Ÿง‘๐Ÿฝโ€๐ŸŽ„", + "๐Ÿง‘๐Ÿพโ€๐ŸŽ„", + "๐Ÿง‘๐Ÿฟโ€๐ŸŽ„", + "๐Ÿฆธ", + "๐Ÿฆธ๐Ÿป", + "๐Ÿฆธ๐Ÿผ", + "๐Ÿฆธ๐Ÿฝ", + "๐Ÿฆธ๐Ÿพ", + "๐Ÿฆธ๐Ÿฟ", + "๐Ÿฆธโ€โ™‚๏ธ", + "๐Ÿฆธ๐Ÿปโ€โ™‚๏ธ", + "๐Ÿฆธ๐Ÿผโ€โ™‚๏ธ", + "๐Ÿฆธ๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿฆธ๐Ÿพโ€โ™‚๏ธ", + "๐Ÿฆธ๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿฆธโ€โ™€๏ธ", + "๐Ÿฆธ๐Ÿปโ€โ™€๏ธ", + "๐Ÿฆธ๐Ÿผโ€โ™€๏ธ", + "๐Ÿฆธ๐Ÿฝโ€โ™€๏ธ", + "๐Ÿฆธ๐Ÿพโ€โ™€๏ธ", + "๐Ÿฆธ๐Ÿฟโ€โ™€๏ธ", + "๐Ÿฆน", + "๐Ÿฆน๐Ÿป", + "๐Ÿฆน๐Ÿผ", + "๐Ÿฆน๐Ÿฝ", + "๐Ÿฆน๐Ÿพ", + "๐Ÿฆน๐Ÿฟ", + "๐Ÿฆนโ€โ™‚๏ธ", + "๐Ÿฆน๐Ÿปโ€โ™‚๏ธ", + "๐Ÿฆน๐Ÿผโ€โ™‚๏ธ", + "๐Ÿฆน๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿฆน๐Ÿพโ€โ™‚๏ธ", + "๐Ÿฆน๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿฆนโ€โ™€๏ธ", + "๐Ÿฆน๐Ÿปโ€โ™€๏ธ", + "๐Ÿฆน๐Ÿผโ€โ™€๏ธ", + "๐Ÿฆน๐Ÿฝโ€โ™€๏ธ", + "๐Ÿฆน๐Ÿพโ€โ™€๏ธ", + "๐Ÿฆน๐Ÿฟโ€โ™€๏ธ", + "๐Ÿง™", + "๐Ÿง™๐Ÿป", + "๐Ÿง™๐Ÿผ", + "๐Ÿง™๐Ÿฝ", + "๐Ÿง™๐Ÿพ", + "๐Ÿง™๐Ÿฟ", + "๐Ÿง™โ€โ™‚๏ธ", + "๐Ÿง™๐Ÿปโ€โ™‚๏ธ", + "๐Ÿง™๐Ÿผโ€โ™‚๏ธ", + "๐Ÿง™๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿง™๐Ÿพโ€โ™‚๏ธ", + "๐Ÿง™๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿง™โ€โ™€๏ธ", + "๐Ÿง™๐Ÿปโ€โ™€๏ธ", + "๐Ÿง™๐Ÿผโ€โ™€๏ธ", + "๐Ÿง™๐Ÿฝโ€โ™€๏ธ", + "๐Ÿง™๐Ÿพโ€โ™€๏ธ", + "๐Ÿง™๐Ÿฟโ€โ™€๏ธ", + "๐Ÿงš", + "๐Ÿงš๐Ÿป", + "๐Ÿงš๐Ÿผ", + "๐Ÿงš๐Ÿฝ", + "๐Ÿงš๐Ÿพ", + "๐Ÿงš๐Ÿฟ", + "๐Ÿงšโ€โ™‚๏ธ", + "๐Ÿงš๐Ÿปโ€โ™‚๏ธ", + "๐Ÿงš๐Ÿผโ€โ™‚๏ธ", + "๐Ÿงš๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿงš๐Ÿพโ€โ™‚๏ธ", + "๐Ÿงš๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿงšโ€โ™€๏ธ", + "๐Ÿงš๐Ÿปโ€โ™€๏ธ", + "๐Ÿงš๐Ÿผโ€โ™€๏ธ", + "๐Ÿงš๐Ÿฝโ€โ™€๏ธ", + "๐Ÿงš๐Ÿพโ€โ™€๏ธ", + "๐Ÿงš๐Ÿฟโ€โ™€๏ธ", + "๐Ÿง›", + "๐Ÿง›๐Ÿป", + "๐Ÿง›๐Ÿผ", + "๐Ÿง›๐Ÿฝ", + "๐Ÿง›๐Ÿพ", + "๐Ÿง›๐Ÿฟ", + "๐Ÿง›โ€โ™‚๏ธ", + "๐Ÿง›๐Ÿปโ€โ™‚๏ธ", + "๐Ÿง›๐Ÿผโ€โ™‚๏ธ", + "๐Ÿง›๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿง›๐Ÿพโ€โ™‚๏ธ", + "๐Ÿง›๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿง›โ€โ™€๏ธ", + "๐Ÿง›๐Ÿปโ€โ™€๏ธ", + "๐Ÿง›๐Ÿผโ€โ™€๏ธ", + "๐Ÿง›๐Ÿฝโ€โ™€๏ธ", + "๐Ÿง›๐Ÿพโ€โ™€๏ธ", + "๐Ÿง›๐Ÿฟโ€โ™€๏ธ", + "๐Ÿงœ", + "๐Ÿงœ๐Ÿป", + "๐Ÿงœ๐Ÿผ", + "๐Ÿงœ๐Ÿฝ", + "๐Ÿงœ๐Ÿพ", + "๐Ÿงœ๐Ÿฟ", + "๐Ÿงœโ€โ™‚๏ธ", + "๐Ÿงœ๐Ÿปโ€โ™‚๏ธ", + "๐Ÿงœ๐Ÿผโ€โ™‚๏ธ", + "๐Ÿงœ๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿงœ๐Ÿพโ€โ™‚๏ธ", + "๐Ÿงœ๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿงœโ€โ™€๏ธ", + "๐Ÿงœ๐Ÿปโ€โ™€๏ธ", + "๐Ÿงœ๐Ÿผโ€โ™€๏ธ", + "๐Ÿงœ๐Ÿฝโ€โ™€๏ธ", + "๐Ÿงœ๐Ÿพโ€โ™€๏ธ", + "๐Ÿงœ๐Ÿฟโ€โ™€๏ธ", + "๐Ÿง", + "๐Ÿง๐Ÿป", + "๐Ÿง๐Ÿผ", + "๐Ÿง๐Ÿฝ", + "๐Ÿง๐Ÿพ", + "๐Ÿง๐Ÿฟ", + "๐Ÿงโ€โ™‚๏ธ", + "๐Ÿง๐Ÿปโ€โ™‚๏ธ", + "๐Ÿง๐Ÿผโ€โ™‚๏ธ", + "๐Ÿง๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿง๐Ÿพโ€โ™‚๏ธ", + "๐Ÿง๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿงโ€โ™€๏ธ", + "๐Ÿง๐Ÿปโ€โ™€๏ธ", + "๐Ÿง๐Ÿผโ€โ™€๏ธ", + "๐Ÿง๐Ÿฝโ€โ™€๏ธ", + "๐Ÿง๐Ÿพโ€โ™€๏ธ", + "๐Ÿง๐Ÿฟโ€โ™€๏ธ", + "๐Ÿงž", + "๐Ÿงžโ€โ™‚๏ธ", + "๐Ÿงžโ€โ™€๏ธ", + "๐ŸงŸ", + "๐ŸงŸโ€โ™‚๏ธ", + "๐ŸงŸโ€โ™€๏ธ", + "๐ŸงŒ", + "๐Ÿ’†", + "๐Ÿ’†๐Ÿป", + "๐Ÿ’†๐Ÿผ", + "๐Ÿ’†๐Ÿฝ", + "๐Ÿ’†๐Ÿพ", + "๐Ÿ’†๐Ÿฟ", + "๐Ÿ’†โ€โ™‚๏ธ", + "๐Ÿ’†๐Ÿปโ€โ™‚๏ธ", + "๐Ÿ’†๐Ÿผโ€โ™‚๏ธ", + "๐Ÿ’†๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿ’†๐Ÿพโ€โ™‚๏ธ", + "๐Ÿ’†๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿ’†โ€โ™€๏ธ", + "๐Ÿ’†๐Ÿปโ€โ™€๏ธ", + "๐Ÿ’†๐Ÿผโ€โ™€๏ธ", + "๐Ÿ’†๐Ÿฝโ€โ™€๏ธ", + "๐Ÿ’†๐Ÿพโ€โ™€๏ธ", + "๐Ÿ’†๐Ÿฟโ€โ™€๏ธ", + "๐Ÿ’‡", + "๐Ÿ’‡๐Ÿป", + "๐Ÿ’‡๐Ÿผ", + "๐Ÿ’‡๐Ÿฝ", + "๐Ÿ’‡๐Ÿพ", + "๐Ÿ’‡๐Ÿฟ", + "๐Ÿ’‡โ€โ™‚๏ธ", + "๐Ÿ’‡๐Ÿปโ€โ™‚๏ธ", + "๐Ÿ’‡๐Ÿผโ€โ™‚๏ธ", + "๐Ÿ’‡๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿ’‡๐Ÿพโ€โ™‚๏ธ", + "๐Ÿ’‡๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿ’‡โ€โ™€๏ธ", + "๐Ÿ’‡๐Ÿปโ€โ™€๏ธ", + "๐Ÿ’‡๐Ÿผโ€โ™€๏ธ", + "๐Ÿ’‡๐Ÿฝโ€โ™€๏ธ", + "๐Ÿ’‡๐Ÿพโ€โ™€๏ธ", + "๐Ÿ’‡๐Ÿฟโ€โ™€๏ธ", + "๐Ÿšถ", + "๐Ÿšถ๐Ÿป", + "๐Ÿšถ๐Ÿผ", + "๐Ÿšถ๐Ÿฝ", + "๐Ÿšถ๐Ÿพ", + "๐Ÿšถ๐Ÿฟ", + "๐Ÿšถโ€โ™‚๏ธ", + "๐Ÿšถ๐Ÿปโ€โ™‚๏ธ", + "๐Ÿšถ๐Ÿผโ€โ™‚๏ธ", + "๐Ÿšถ๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿšถ๐Ÿพโ€โ™‚๏ธ", + "๐Ÿšถ๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿšถโ€โ™€๏ธ", + "๐Ÿšถ๐Ÿปโ€โ™€๏ธ", + "๐Ÿšถ๐Ÿผโ€โ™€๏ธ", + "๐Ÿšถ๐Ÿฝโ€โ™€๏ธ", + "๐Ÿšถ๐Ÿพโ€โ™€๏ธ", + "๐Ÿšถ๐Ÿฟโ€โ™€๏ธ", + "๐Ÿšถโ€โžก๏ธ", + "๐Ÿšถ๐Ÿปโ€โžก๏ธ", + "๐Ÿšถ๐Ÿผโ€โžก๏ธ", + "๐Ÿšถ๐Ÿฝโ€โžก๏ธ", + "๐Ÿšถ๐Ÿพโ€โžก๏ธ", + "๐Ÿšถ๐Ÿฟโ€โžก๏ธ", + "๐Ÿšถโ€โ™€๏ธโ€โžก๏ธ", + "๐Ÿšถ๐Ÿปโ€โ™€๏ธโ€โžก๏ธ", + "๐Ÿšถ๐Ÿผโ€โ™€๏ธโ€โžก๏ธ", + "๐Ÿšถ๐Ÿฝโ€โ™€๏ธโ€โžก๏ธ", + "๐Ÿšถ๐Ÿพโ€โ™€๏ธโ€โžก๏ธ", + "๐Ÿšถ๐Ÿฟโ€โ™€๏ธโ€โžก๏ธ", + "๐Ÿšถโ€โ™‚๏ธโ€โžก๏ธ", + "๐Ÿšถ๐Ÿปโ€โ™‚๏ธโ€โžก๏ธ", + "๐Ÿšถ๐Ÿผโ€โ™‚๏ธโ€โžก๏ธ", + "๐Ÿšถ๐Ÿฝโ€โ™‚๏ธโ€โžก๏ธ", + "๐Ÿšถ๐Ÿพโ€โ™‚๏ธโ€โžก๏ธ", + "๐Ÿšถ๐Ÿฟโ€โ™‚๏ธโ€โžก๏ธ", + "๐Ÿง", + "๐Ÿง๐Ÿป", + "๐Ÿง๐Ÿผ", + "๐Ÿง๐Ÿฝ", + "๐Ÿง๐Ÿพ", + "๐Ÿง๐Ÿฟ", + "๐Ÿงโ€โ™‚๏ธ", + "๐Ÿง๐Ÿปโ€โ™‚๏ธ", + "๐Ÿง๐Ÿผโ€โ™‚๏ธ", + "๐Ÿง๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿง๐Ÿพโ€โ™‚๏ธ", + "๐Ÿง๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿงโ€โ™€๏ธ", + "๐Ÿง๐Ÿปโ€โ™€๏ธ", + "๐Ÿง๐Ÿผโ€โ™€๏ธ", + "๐Ÿง๐Ÿฝโ€โ™€๏ธ", + "๐Ÿง๐Ÿพโ€โ™€๏ธ", + "๐Ÿง๐Ÿฟโ€โ™€๏ธ", + "๐ŸงŽ", + "๐ŸงŽ๐Ÿป", + "๐ŸงŽ๐Ÿผ", + "๐ŸงŽ๐Ÿฝ", + "๐ŸงŽ๐Ÿพ", + "๐ŸงŽ๐Ÿฟ", + "๐ŸงŽโ€โ™‚๏ธ", + "๐ŸงŽ๐Ÿปโ€โ™‚๏ธ", + "๐ŸงŽ๐Ÿผโ€โ™‚๏ธ", + "๐ŸงŽ๐Ÿฝโ€โ™‚๏ธ", + "๐ŸงŽ๐Ÿพโ€โ™‚๏ธ", + "๐ŸงŽ๐Ÿฟโ€โ™‚๏ธ", + "๐ŸงŽโ€โ™€๏ธ", + "๐ŸงŽ๐Ÿปโ€โ™€๏ธ", + "๐ŸงŽ๐Ÿผโ€โ™€๏ธ", + "๐ŸงŽ๐Ÿฝโ€โ™€๏ธ", + "๐ŸงŽ๐Ÿพโ€โ™€๏ธ", + "๐ŸงŽ๐Ÿฟโ€โ™€๏ธ", + "๐ŸงŽโ€โžก๏ธ", + "๐ŸงŽ๐Ÿปโ€โžก๏ธ", + "๐ŸงŽ๐Ÿผโ€โžก๏ธ", + "๐ŸงŽ๐Ÿฝโ€โžก๏ธ", + "๐ŸงŽ๐Ÿพโ€โžก๏ธ", + "๐ŸงŽ๐Ÿฟโ€โžก๏ธ", + "๐ŸงŽโ€โ™€๏ธโ€โžก๏ธ", + "๐ŸงŽ๐Ÿปโ€โ™€๏ธโ€โžก๏ธ", + "๐ŸงŽ๐Ÿผโ€โ™€๏ธโ€โžก๏ธ", + "๐ŸงŽ๐Ÿฝโ€โ™€๏ธโ€โžก๏ธ", + "๐ŸงŽ๐Ÿพโ€โ™€๏ธโ€โžก๏ธ", + "๐ŸงŽ๐Ÿฟโ€โ™€๏ธโ€โžก๏ธ", + "๐ŸงŽโ€โ™‚๏ธโ€โžก๏ธ", + "๐ŸงŽ๐Ÿปโ€โ™‚๏ธโ€โžก๏ธ", + "๐ŸงŽ๐Ÿผโ€โ™‚๏ธโ€โžก๏ธ", + "๐ŸงŽ๐Ÿฝโ€โ™‚๏ธโ€โžก๏ธ", + "๐ŸงŽ๐Ÿพโ€โ™‚๏ธโ€โžก๏ธ", + "๐ŸงŽ๐Ÿฟโ€โ™‚๏ธโ€โžก๏ธ", + "๐Ÿง‘โ€๐Ÿฆฏ", + "๐Ÿง‘๐Ÿปโ€๐Ÿฆฏ", + "๐Ÿง‘๐Ÿผโ€๐Ÿฆฏ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿฆฏ", + "๐Ÿง‘๐Ÿพโ€๐Ÿฆฏ", + "๐Ÿง‘๐Ÿฟโ€๐Ÿฆฏ", + "๐Ÿง‘โ€๐Ÿฆฏโ€โžก๏ธ", + "๐Ÿง‘๐Ÿปโ€๐Ÿฆฏโ€โžก๏ธ", + "๐Ÿง‘๐Ÿผโ€๐Ÿฆฏโ€โžก๏ธ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿฆฏโ€โžก๏ธ", + "๐Ÿง‘๐Ÿพโ€๐Ÿฆฏโ€โžก๏ธ", + "๐Ÿง‘๐Ÿฟโ€๐Ÿฆฏโ€โžก๏ธ", + "๐Ÿ‘จโ€๐Ÿฆฏ", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฏ", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿฆฏ", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿฆฏ", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿฆฏ", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆฏ", + "๐Ÿ‘จโ€๐Ÿฆฏโ€โžก๏ธ", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฏโ€โžก๏ธ", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿฆฏโ€โžก๏ธ", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿฆฏโ€โžก๏ธ", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿฆฏโ€โžก๏ธ", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆฏโ€โžก๏ธ", + "๐Ÿ‘ฉโ€๐Ÿฆฏ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿฆฏ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿฆฏ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿฆฏ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿฆฏ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿฆฏ", + "๐Ÿ‘ฉโ€๐Ÿฆฏโ€โžก๏ธ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿฆฏโ€โžก๏ธ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿฆฏโ€โžก๏ธ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿฆฏโ€โžก๏ธ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿฆฏโ€โžก๏ธ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿฆฏโ€โžก๏ธ", + "๐Ÿง‘โ€๐Ÿฆผ", + "๐Ÿง‘๐Ÿปโ€๐Ÿฆผ", + "๐Ÿง‘๐Ÿผโ€๐Ÿฆผ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿฆผ", + "๐Ÿง‘๐Ÿพโ€๐Ÿฆผ", + "๐Ÿง‘๐Ÿฟโ€๐Ÿฆผ", + "๐Ÿง‘โ€๐Ÿฆผโ€โžก๏ธ", + "๐Ÿง‘๐Ÿปโ€๐Ÿฆผโ€โžก๏ธ", + "๐Ÿง‘๐Ÿผโ€๐Ÿฆผโ€โžก๏ธ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿฆผโ€โžก๏ธ", + "๐Ÿง‘๐Ÿพโ€๐Ÿฆผโ€โžก๏ธ", + "๐Ÿง‘๐Ÿฟโ€๐Ÿฆผโ€โžก๏ธ", + "๐Ÿ‘จโ€๐Ÿฆผ", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿฆผ", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿฆผ", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿฆผ", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿฆผ", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆผ", + "๐Ÿ‘จโ€๐Ÿฆผโ€โžก๏ธ", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿฆผโ€โžก๏ธ", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿฆผโ€โžก๏ธ", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿฆผโ€โžก๏ธ", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿฆผโ€โžก๏ธ", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆผโ€โžก๏ธ", + "๐Ÿ‘ฉโ€๐Ÿฆผ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿฆผ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿฆผ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿฆผ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿฆผ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿฆผ", + "๐Ÿ‘ฉโ€๐Ÿฆผโ€โžก๏ธ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿฆผโ€โžก๏ธ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿฆผโ€โžก๏ธ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿฆผโ€โžก๏ธ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿฆผโ€โžก๏ธ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿฆผโ€โžก๏ธ", + "๐Ÿง‘โ€๐Ÿฆฝ", + "๐Ÿง‘๐Ÿปโ€๐Ÿฆฝ", + "๐Ÿง‘๐Ÿผโ€๐Ÿฆฝ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿฆฝ", + "๐Ÿง‘๐Ÿพโ€๐Ÿฆฝ", + "๐Ÿง‘๐Ÿฟโ€๐Ÿฆฝ", + "๐Ÿง‘โ€๐Ÿฆฝโ€โžก๏ธ", + "๐Ÿง‘๐Ÿปโ€๐Ÿฆฝโ€โžก๏ธ", + "๐Ÿง‘๐Ÿผโ€๐Ÿฆฝโ€โžก๏ธ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿฆฝโ€โžก๏ธ", + "๐Ÿง‘๐Ÿพโ€๐Ÿฆฝโ€โžก๏ธ", + "๐Ÿง‘๐Ÿฟโ€๐Ÿฆฝโ€โžก๏ธ", + "๐Ÿ‘จโ€๐Ÿฆฝ", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฝ", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿฆฝ", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿฆฝ", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿฆฝ", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆฝ", + "๐Ÿ‘จโ€๐Ÿฆฝโ€โžก๏ธ", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฝโ€โžก๏ธ", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿฆฝโ€โžก๏ธ", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿฆฝโ€โžก๏ธ", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿฆฝโ€โžก๏ธ", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆฝโ€โžก๏ธ", + "๐Ÿ‘ฉโ€๐Ÿฆฝ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿฆฝ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿฆฝ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿฆฝ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿฆฝ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿฆฝ", + "๐Ÿ‘ฉโ€๐Ÿฆฝโ€โžก๏ธ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿฆฝโ€โžก๏ธ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿฆฝโ€โžก๏ธ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿฆฝโ€โžก๏ธ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿฆฝโ€โžก๏ธ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿฆฝโ€โžก๏ธ", + "๐Ÿƒ", + "๐Ÿƒ๐Ÿป", + "๐Ÿƒ๐Ÿผ", + "๐Ÿƒ๐Ÿฝ", + "๐Ÿƒ๐Ÿพ", + "๐Ÿƒ๐Ÿฟ", + "๐Ÿƒโ€โ™‚๏ธ", + "๐Ÿƒ๐Ÿปโ€โ™‚๏ธ", + "๐Ÿƒ๐Ÿผโ€โ™‚๏ธ", + "๐Ÿƒ๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿƒ๐Ÿพโ€โ™‚๏ธ", + "๐Ÿƒ๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿƒโ€โ™€๏ธ", + "๐Ÿƒ๐Ÿปโ€โ™€๏ธ", + "๐Ÿƒ๐Ÿผโ€โ™€๏ธ", + "๐Ÿƒ๐Ÿฝโ€โ™€๏ธ", + "๐Ÿƒ๐Ÿพโ€โ™€๏ธ", + "๐Ÿƒ๐Ÿฟโ€โ™€๏ธ", + "๐Ÿƒโ€โžก๏ธ", + "๐Ÿƒ๐Ÿปโ€โžก๏ธ", + "๐Ÿƒ๐Ÿผโ€โžก๏ธ", + "๐Ÿƒ๐Ÿฝโ€โžก๏ธ", + "๐Ÿƒ๐Ÿพโ€โžก๏ธ", + "๐Ÿƒ๐Ÿฟโ€โžก๏ธ", + "๐Ÿƒโ€โ™€๏ธโ€โžก๏ธ", + "๐Ÿƒ๐Ÿปโ€โ™€๏ธโ€โžก๏ธ", + "๐Ÿƒ๐Ÿผโ€โ™€๏ธโ€โžก๏ธ", + "๐Ÿƒ๐Ÿฝโ€โ™€๏ธโ€โžก๏ธ", + "๐Ÿƒ๐Ÿพโ€โ™€๏ธโ€โžก๏ธ", + "๐Ÿƒ๐Ÿฟโ€โ™€๏ธโ€โžก๏ธ", + "๐Ÿƒโ€โ™‚๏ธโ€โžก๏ธ", + "๐Ÿƒ๐Ÿปโ€โ™‚๏ธโ€โžก๏ธ", + "๐Ÿƒ๐Ÿผโ€โ™‚๏ธโ€โžก๏ธ", + "๐Ÿƒ๐Ÿฝโ€โ™‚๏ธโ€โžก๏ธ", + "๐Ÿƒ๐Ÿพโ€โ™‚๏ธโ€โžก๏ธ", + "๐Ÿƒ๐Ÿฟโ€โ™‚๏ธโ€โžก๏ธ", + "๐Ÿ’ƒ", + "๐Ÿ’ƒ๐Ÿป", + "๐Ÿ’ƒ๐Ÿผ", + "๐Ÿ’ƒ๐Ÿฝ", + "๐Ÿ’ƒ๐Ÿพ", + "๐Ÿ’ƒ๐Ÿฟ", + "๐Ÿ•บ", + "๐Ÿ•บ๐Ÿป", + "๐Ÿ•บ๐Ÿผ", + "๐Ÿ•บ๐Ÿฝ", + "๐Ÿ•บ๐Ÿพ", + "๐Ÿ•บ๐Ÿฟ", + "๐Ÿ•ด๏ธ", + "๐Ÿ•ด๐Ÿป", + "๐Ÿ•ด๐Ÿผ", + "๐Ÿ•ด๐Ÿฝ", + "๐Ÿ•ด๐Ÿพ", + "๐Ÿ•ด๐Ÿฟ", + "๐Ÿ‘ฏ", + "๐Ÿ‘ฏโ€โ™‚๏ธ", + "๐Ÿ‘ฏโ€โ™€๏ธ", + "๐Ÿง–", + "๐Ÿง–๐Ÿป", + "๐Ÿง–๐Ÿผ", + "๐Ÿง–๐Ÿฝ", + "๐Ÿง–๐Ÿพ", + "๐Ÿง–๐Ÿฟ", + "๐Ÿง–โ€โ™‚๏ธ", + "๐Ÿง–๐Ÿปโ€โ™‚๏ธ", + "๐Ÿง–๐Ÿผโ€โ™‚๏ธ", + "๐Ÿง–๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿง–๐Ÿพโ€โ™‚๏ธ", + "๐Ÿง–๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿง–โ€โ™€๏ธ", + "๐Ÿง–๐Ÿปโ€โ™€๏ธ", + "๐Ÿง–๐Ÿผโ€โ™€๏ธ", + "๐Ÿง–๐Ÿฝโ€โ™€๏ธ", + "๐Ÿง–๐Ÿพโ€โ™€๏ธ", + "๐Ÿง–๐Ÿฟโ€โ™€๏ธ", + "๐Ÿง—", + "๐Ÿง—๐Ÿป", + "๐Ÿง—๐Ÿผ", + "๐Ÿง—๐Ÿฝ", + "๐Ÿง—๐Ÿพ", + "๐Ÿง—๐Ÿฟ", + "๐Ÿง—โ€โ™‚๏ธ", + "๐Ÿง—๐Ÿปโ€โ™‚๏ธ", + "๐Ÿง—๐Ÿผโ€โ™‚๏ธ", + "๐Ÿง—๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿง—๐Ÿพโ€โ™‚๏ธ", + "๐Ÿง—๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿง—โ€โ™€๏ธ", + "๐Ÿง—๐Ÿปโ€โ™€๏ธ", + "๐Ÿง—๐Ÿผโ€โ™€๏ธ", + "๐Ÿง—๐Ÿฝโ€โ™€๏ธ", + "๐Ÿง—๐Ÿพโ€โ™€๏ธ", + "๐Ÿง—๐Ÿฟโ€โ™€๏ธ", + "๐Ÿคบ", + "๐Ÿ‡", + "๐Ÿ‡๐Ÿป", + "๐Ÿ‡๐Ÿผ", + "๐Ÿ‡๐Ÿฝ", + "๐Ÿ‡๐Ÿพ", + "๐Ÿ‡๐Ÿฟ", + "โ›ท๏ธ", + "๐Ÿ‚", + "๐Ÿ‚๐Ÿป", + "๐Ÿ‚๐Ÿผ", + "๐Ÿ‚๐Ÿฝ", + "๐Ÿ‚๐Ÿพ", + "๐Ÿ‚๐Ÿฟ", + "๐ŸŒ๏ธ", + "๐ŸŒ๐Ÿป", + "๐ŸŒ๐Ÿผ", + "๐ŸŒ๐Ÿฝ", + "๐ŸŒ๐Ÿพ", + "๐ŸŒ๐Ÿฟ", + "๐ŸŒ๏ธโ€โ™‚๏ธ", + "๐ŸŒ๐Ÿปโ€โ™‚๏ธ", + "๐ŸŒ๐Ÿผโ€โ™‚๏ธ", + "๐ŸŒ๐Ÿฝโ€โ™‚๏ธ", + "๐ŸŒ๐Ÿพโ€โ™‚๏ธ", + "๐ŸŒ๐Ÿฟโ€โ™‚๏ธ", + "๐ŸŒ๏ธโ€โ™€๏ธ", + "๐ŸŒ๐Ÿปโ€โ™€๏ธ", + "๐ŸŒ๐Ÿผโ€โ™€๏ธ", + "๐ŸŒ๐Ÿฝโ€โ™€๏ธ", + "๐ŸŒ๐Ÿพโ€โ™€๏ธ", + "๐ŸŒ๐Ÿฟโ€โ™€๏ธ", + "๐Ÿ„", + "๐Ÿ„๐Ÿป", + "๐Ÿ„๐Ÿผ", + "๐Ÿ„๐Ÿฝ", + "๐Ÿ„๐Ÿพ", + "๐Ÿ„๐Ÿฟ", + "๐Ÿ„โ€โ™‚๏ธ", + "๐Ÿ„๐Ÿปโ€โ™‚๏ธ", + "๐Ÿ„๐Ÿผโ€โ™‚๏ธ", + "๐Ÿ„๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿ„๐Ÿพโ€โ™‚๏ธ", + "๐Ÿ„๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿ„โ€โ™€๏ธ", + "๐Ÿ„๐Ÿปโ€โ™€๏ธ", + "๐Ÿ„๐Ÿผโ€โ™€๏ธ", + "๐Ÿ„๐Ÿฝโ€โ™€๏ธ", + "๐Ÿ„๐Ÿพโ€โ™€๏ธ", + "๐Ÿ„๐Ÿฟโ€โ™€๏ธ", + "๐Ÿšฃ", + "๐Ÿšฃ๐Ÿป", + "๐Ÿšฃ๐Ÿผ", + "๐Ÿšฃ๐Ÿฝ", + "๐Ÿšฃ๐Ÿพ", + "๐Ÿšฃ๐Ÿฟ", + "๐Ÿšฃโ€โ™‚๏ธ", + "๐Ÿšฃ๐Ÿปโ€โ™‚๏ธ", + "๐Ÿšฃ๐Ÿผโ€โ™‚๏ธ", + "๐Ÿšฃ๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿšฃ๐Ÿพโ€โ™‚๏ธ", + "๐Ÿšฃ๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿšฃโ€โ™€๏ธ", + "๐Ÿšฃ๐Ÿปโ€โ™€๏ธ", + "๐Ÿšฃ๐Ÿผโ€โ™€๏ธ", + "๐Ÿšฃ๐Ÿฝโ€โ™€๏ธ", + "๐Ÿšฃ๐Ÿพโ€โ™€๏ธ", + "๐Ÿšฃ๐Ÿฟโ€โ™€๏ธ", + "๐ŸŠ", + "๐ŸŠ๐Ÿป", + "๐ŸŠ๐Ÿผ", + "๐ŸŠ๐Ÿฝ", + "๐ŸŠ๐Ÿพ", + "๐ŸŠ๐Ÿฟ", + "๐ŸŠโ€โ™‚๏ธ", + "๐ŸŠ๐Ÿปโ€โ™‚๏ธ", + "๐ŸŠ๐Ÿผโ€โ™‚๏ธ", + "๐ŸŠ๐Ÿฝโ€โ™‚๏ธ", + "๐ŸŠ๐Ÿพโ€โ™‚๏ธ", + "๐ŸŠ๐Ÿฟโ€โ™‚๏ธ", + "๐ŸŠโ€โ™€๏ธ", + "๐ŸŠ๐Ÿปโ€โ™€๏ธ", + "๐ŸŠ๐Ÿผโ€โ™€๏ธ", + "๐ŸŠ๐Ÿฝโ€โ™€๏ธ", + "๐ŸŠ๐Ÿพโ€โ™€๏ธ", + "๐ŸŠ๐Ÿฟโ€โ™€๏ธ", + "โ›น๏ธ", + "โ›น๐Ÿป", + "โ›น๐Ÿผ", + "โ›น๐Ÿฝ", + "โ›น๐Ÿพ", + "โ›น๐Ÿฟ", + "โ›น๏ธโ€โ™‚๏ธ", + "โ›น๐Ÿปโ€โ™‚๏ธ", + "โ›น๐Ÿผโ€โ™‚๏ธ", + "โ›น๐Ÿฝโ€โ™‚๏ธ", + "โ›น๐Ÿพโ€โ™‚๏ธ", + "โ›น๐Ÿฟโ€โ™‚๏ธ", + "โ›น๏ธโ€โ™€๏ธ", + "โ›น๐Ÿปโ€โ™€๏ธ", + "โ›น๐Ÿผโ€โ™€๏ธ", + "โ›น๐Ÿฝโ€โ™€๏ธ", + "โ›น๐Ÿพโ€โ™€๏ธ", + "โ›น๐Ÿฟโ€โ™€๏ธ", + "๐Ÿ‹๏ธ", + "๐Ÿ‹๐Ÿป", + "๐Ÿ‹๐Ÿผ", + "๐Ÿ‹๐Ÿฝ", + "๐Ÿ‹๐Ÿพ", + "๐Ÿ‹๐Ÿฟ", + "๐Ÿ‹๏ธโ€โ™‚๏ธ", + "๐Ÿ‹๐Ÿปโ€โ™‚๏ธ", + "๐Ÿ‹๐Ÿผโ€โ™‚๏ธ", + "๐Ÿ‹๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿ‹๐Ÿพโ€โ™‚๏ธ", + "๐Ÿ‹๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿ‹๏ธโ€โ™€๏ธ", + "๐Ÿ‹๐Ÿปโ€โ™€๏ธ", + "๐Ÿ‹๐Ÿผโ€โ™€๏ธ", + "๐Ÿ‹๐Ÿฝโ€โ™€๏ธ", + "๐Ÿ‹๐Ÿพโ€โ™€๏ธ", + "๐Ÿ‹๐Ÿฟโ€โ™€๏ธ", + "๐Ÿšด", + "๐Ÿšด๐Ÿป", + "๐Ÿšด๐Ÿผ", + "๐Ÿšด๐Ÿฝ", + "๐Ÿšด๐Ÿพ", + "๐Ÿšด๐Ÿฟ", + "๐Ÿšดโ€โ™‚๏ธ", + "๐Ÿšด๐Ÿปโ€โ™‚๏ธ", + "๐Ÿšด๐Ÿผโ€โ™‚๏ธ", + "๐Ÿšด๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿšด๐Ÿพโ€โ™‚๏ธ", + "๐Ÿšด๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿšดโ€โ™€๏ธ", + "๐Ÿšด๐Ÿปโ€โ™€๏ธ", + "๐Ÿšด๐Ÿผโ€โ™€๏ธ", + "๐Ÿšด๐Ÿฝโ€โ™€๏ธ", + "๐Ÿšด๐Ÿพโ€โ™€๏ธ", + "๐Ÿšด๐Ÿฟโ€โ™€๏ธ", + "๐Ÿšต", + "๐Ÿšต๐Ÿป", + "๐Ÿšต๐Ÿผ", + "๐Ÿšต๐Ÿฝ", + "๐Ÿšต๐Ÿพ", + "๐Ÿšต๐Ÿฟ", + "๐Ÿšตโ€โ™‚๏ธ", + "๐Ÿšต๐Ÿปโ€โ™‚๏ธ", + "๐Ÿšต๐Ÿผโ€โ™‚๏ธ", + "๐Ÿšต๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿšต๐Ÿพโ€โ™‚๏ธ", + "๐Ÿšต๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿšตโ€โ™€๏ธ", + "๐Ÿšต๐Ÿปโ€โ™€๏ธ", + "๐Ÿšต๐Ÿผโ€โ™€๏ธ", + "๐Ÿšต๐Ÿฝโ€โ™€๏ธ", + "๐Ÿšต๐Ÿพโ€โ™€๏ธ", + "๐Ÿšต๐Ÿฟโ€โ™€๏ธ", + "๐Ÿคธ", + "๐Ÿคธ๐Ÿป", + "๐Ÿคธ๐Ÿผ", + "๐Ÿคธ๐Ÿฝ", + "๐Ÿคธ๐Ÿพ", + "๐Ÿคธ๐Ÿฟ", + "๐Ÿคธโ€โ™‚๏ธ", + "๐Ÿคธ๐Ÿปโ€โ™‚๏ธ", + "๐Ÿคธ๐Ÿผโ€โ™‚๏ธ", + "๐Ÿคธ๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿคธ๐Ÿพโ€โ™‚๏ธ", + "๐Ÿคธ๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿคธโ€โ™€๏ธ", + "๐Ÿคธ๐Ÿปโ€โ™€๏ธ", + "๐Ÿคธ๐Ÿผโ€โ™€๏ธ", + "๐Ÿคธ๐Ÿฝโ€โ™€๏ธ", + "๐Ÿคธ๐Ÿพโ€โ™€๏ธ", + "๐Ÿคธ๐Ÿฟโ€โ™€๏ธ", + "๐Ÿคผ", + "๐Ÿคผโ€โ™‚๏ธ", + "๐Ÿคผโ€โ™€๏ธ", + "๐Ÿคฝ", + "๐Ÿคฝ๐Ÿป", + "๐Ÿคฝ๐Ÿผ", + "๐Ÿคฝ๐Ÿฝ", + "๐Ÿคฝ๐Ÿพ", + "๐Ÿคฝ๐Ÿฟ", + "๐Ÿคฝโ€โ™‚๏ธ", + "๐Ÿคฝ๐Ÿปโ€โ™‚๏ธ", + "๐Ÿคฝ๐Ÿผโ€โ™‚๏ธ", + "๐Ÿคฝ๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿคฝ๐Ÿพโ€โ™‚๏ธ", + "๐Ÿคฝ๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿคฝโ€โ™€๏ธ", + "๐Ÿคฝ๐Ÿปโ€โ™€๏ธ", + "๐Ÿคฝ๐Ÿผโ€โ™€๏ธ", + "๐Ÿคฝ๐Ÿฝโ€โ™€๏ธ", + "๐Ÿคฝ๐Ÿพโ€โ™€๏ธ", + "๐Ÿคฝ๐Ÿฟโ€โ™€๏ธ", + "๐Ÿคพ", + "๐Ÿคพ๐Ÿป", + "๐Ÿคพ๐Ÿผ", + "๐Ÿคพ๐Ÿฝ", + "๐Ÿคพ๐Ÿพ", + "๐Ÿคพ๐Ÿฟ", + "๐Ÿคพโ€โ™‚๏ธ", + "๐Ÿคพ๐Ÿปโ€โ™‚๏ธ", + "๐Ÿคพ๐Ÿผโ€โ™‚๏ธ", + "๐Ÿคพ๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿคพ๐Ÿพโ€โ™‚๏ธ", + "๐Ÿคพ๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿคพโ€โ™€๏ธ", + "๐Ÿคพ๐Ÿปโ€โ™€๏ธ", + "๐Ÿคพ๐Ÿผโ€โ™€๏ธ", + "๐Ÿคพ๐Ÿฝโ€โ™€๏ธ", + "๐Ÿคพ๐Ÿพโ€โ™€๏ธ", + "๐Ÿคพ๐Ÿฟโ€โ™€๏ธ", + "๐Ÿคน", + "๐Ÿคน๐Ÿป", + "๐Ÿคน๐Ÿผ", + "๐Ÿคน๐Ÿฝ", + "๐Ÿคน๐Ÿพ", + "๐Ÿคน๐Ÿฟ", + "๐Ÿคนโ€โ™‚๏ธ", + "๐Ÿคน๐Ÿปโ€โ™‚๏ธ", + "๐Ÿคน๐Ÿผโ€โ™‚๏ธ", + "๐Ÿคน๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿคน๐Ÿพโ€โ™‚๏ธ", + "๐Ÿคน๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿคนโ€โ™€๏ธ", + "๐Ÿคน๐Ÿปโ€โ™€๏ธ", + "๐Ÿคน๐Ÿผโ€โ™€๏ธ", + "๐Ÿคน๐Ÿฝโ€โ™€๏ธ", + "๐Ÿคน๐Ÿพโ€โ™€๏ธ", + "๐Ÿคน๐Ÿฟโ€โ™€๏ธ", + "๐Ÿง˜", + "๐Ÿง˜๐Ÿป", + "๐Ÿง˜๐Ÿผ", + "๐Ÿง˜๐Ÿฝ", + "๐Ÿง˜๐Ÿพ", + "๐Ÿง˜๐Ÿฟ", + "๐Ÿง˜โ€โ™‚๏ธ", + "๐Ÿง˜๐Ÿปโ€โ™‚๏ธ", + "๐Ÿง˜๐Ÿผโ€โ™‚๏ธ", + "๐Ÿง˜๐Ÿฝโ€โ™‚๏ธ", + "๐Ÿง˜๐Ÿพโ€โ™‚๏ธ", + "๐Ÿง˜๐Ÿฟโ€โ™‚๏ธ", + "๐Ÿง˜โ€โ™€๏ธ", + "๐Ÿง˜๐Ÿปโ€โ™€๏ธ", + "๐Ÿง˜๐Ÿผโ€โ™€๏ธ", + "๐Ÿง˜๐Ÿฝโ€โ™€๏ธ", + "๐Ÿง˜๐Ÿพโ€โ™€๏ธ", + "๐Ÿง˜๐Ÿฟโ€โ™€๏ธ", + "๐Ÿ›€", + "๐Ÿ›€๐Ÿป", + "๐Ÿ›€๐Ÿผ", + "๐Ÿ›€๐Ÿฝ", + "๐Ÿ›€๐Ÿพ", + "๐Ÿ›€๐Ÿฟ", + "๐Ÿ›Œ", + "๐Ÿ›Œ๐Ÿป", + "๐Ÿ›Œ๐Ÿผ", + "๐Ÿ›Œ๐Ÿฝ", + "๐Ÿ›Œ๐Ÿพ", + "๐Ÿ›Œ๐Ÿฟ", + "๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘", + "๐Ÿง‘๐Ÿปโ€๐Ÿคโ€๐Ÿง‘๐Ÿป", + "๐Ÿง‘๐Ÿปโ€๐Ÿคโ€๐Ÿง‘๐Ÿผ", + "๐Ÿง‘๐Ÿปโ€๐Ÿคโ€๐Ÿง‘๐Ÿฝ", + "๐Ÿง‘๐Ÿปโ€๐Ÿคโ€๐Ÿง‘๐Ÿพ", + "๐Ÿง‘๐Ÿปโ€๐Ÿคโ€๐Ÿง‘๐Ÿฟ", + "๐Ÿง‘๐Ÿผโ€๐Ÿคโ€๐Ÿง‘๐Ÿป", + "๐Ÿง‘๐Ÿผโ€๐Ÿคโ€๐Ÿง‘๐Ÿผ", + "๐Ÿง‘๐Ÿผโ€๐Ÿคโ€๐Ÿง‘๐Ÿฝ", + "๐Ÿง‘๐Ÿผโ€๐Ÿคโ€๐Ÿง‘๐Ÿพ", + "๐Ÿง‘๐Ÿผโ€๐Ÿคโ€๐Ÿง‘๐Ÿฟ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿคโ€๐Ÿง‘๐Ÿป", + "๐Ÿง‘๐Ÿฝโ€๐Ÿคโ€๐Ÿง‘๐Ÿผ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿคโ€๐Ÿง‘๐Ÿฝ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿคโ€๐Ÿง‘๐Ÿพ", + "๐Ÿง‘๐Ÿฝโ€๐Ÿคโ€๐Ÿง‘๐Ÿฟ", + "๐Ÿง‘๐Ÿพโ€๐Ÿคโ€๐Ÿง‘๐Ÿป", + "๐Ÿง‘๐Ÿพโ€๐Ÿคโ€๐Ÿง‘๐Ÿผ", + "๐Ÿง‘๐Ÿพโ€๐Ÿคโ€๐Ÿง‘๐Ÿฝ", + "๐Ÿง‘๐Ÿพโ€๐Ÿคโ€๐Ÿง‘๐Ÿพ", + "๐Ÿง‘๐Ÿพโ€๐Ÿคโ€๐Ÿง‘๐Ÿฟ", + "๐Ÿง‘๐Ÿฟโ€๐Ÿคโ€๐Ÿง‘๐Ÿป", + "๐Ÿง‘๐Ÿฟโ€๐Ÿคโ€๐Ÿง‘๐Ÿผ", + "๐Ÿง‘๐Ÿฟโ€๐Ÿคโ€๐Ÿง‘๐Ÿฝ", + "๐Ÿง‘๐Ÿฟโ€๐Ÿคโ€๐Ÿง‘๐Ÿพ", + "๐Ÿง‘๐Ÿฟโ€๐Ÿคโ€๐Ÿง‘๐Ÿฟ", + "๐Ÿ‘ญ", + "๐Ÿ‘ญ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿคโ€๐Ÿ‘ฉ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿคโ€๐Ÿ‘ฉ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿคโ€๐Ÿ‘ฉ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿคโ€๐Ÿ‘ฉ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿคโ€๐Ÿ‘ฉ๐Ÿป", + "๐Ÿ‘ญ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿคโ€๐Ÿ‘ฉ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿคโ€๐Ÿ‘ฉ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿคโ€๐Ÿ‘ฉ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿคโ€๐Ÿ‘ฉ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿคโ€๐Ÿ‘ฉ๐Ÿผ", + "๐Ÿ‘ญ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿคโ€๐Ÿ‘ฉ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿคโ€๐Ÿ‘ฉ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿคโ€๐Ÿ‘ฉ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿคโ€๐Ÿ‘ฉ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿคโ€๐Ÿ‘ฉ๐Ÿฝ", + "๐Ÿ‘ญ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿคโ€๐Ÿ‘ฉ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿคโ€๐Ÿ‘ฉ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿคโ€๐Ÿ‘ฉ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿคโ€๐Ÿ‘ฉ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿคโ€๐Ÿ‘ฉ๐Ÿพ", + "๐Ÿ‘ญ๐Ÿฟ", + "๐Ÿ‘ซ", + "๐Ÿ‘ซ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘ซ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘ซ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘ซ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘ซ๐Ÿฟ", + "๐Ÿ‘ฌ", + "๐Ÿ‘ฌ๐Ÿป", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘จ๐Ÿปโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘ฌ๐Ÿผ", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘จ๐Ÿผโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘ฌ๐Ÿฝ", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘จ๐Ÿฝโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘ฌ๐Ÿพ", + "๐Ÿ‘จ๐Ÿพโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘จ๐Ÿฟโ€๐Ÿคโ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘ฌ๐Ÿฟ", + "๐Ÿ’", + "๐Ÿ’๐Ÿป", + "๐Ÿ’๐Ÿผ", + "๐Ÿ’๐Ÿฝ", + "๐Ÿ’๐Ÿพ", + "๐Ÿ’๐Ÿฟ", + "๐Ÿง‘๐Ÿปโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿง‘๐Ÿผ", + "๐Ÿง‘๐Ÿปโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿง‘๐Ÿฝ", + "๐Ÿง‘๐Ÿปโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿง‘๐Ÿพ", + "๐Ÿง‘๐Ÿปโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿง‘๐Ÿฟ", + "๐Ÿง‘๐Ÿผโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿง‘๐Ÿป", + "๐Ÿง‘๐Ÿผโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿง‘๐Ÿฝ", + "๐Ÿง‘๐Ÿผโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿง‘๐Ÿพ", + "๐Ÿง‘๐Ÿผโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿง‘๐Ÿฟ", + "๐Ÿง‘๐Ÿฝโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿง‘๐Ÿป", + "๐Ÿง‘๐Ÿฝโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿง‘๐Ÿผ", + "๐Ÿง‘๐Ÿฝโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿง‘๐Ÿพ", + "๐Ÿง‘๐Ÿฝโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿง‘๐Ÿฟ", + "๐Ÿง‘๐Ÿพโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿง‘๐Ÿป", + "๐Ÿง‘๐Ÿพโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿง‘๐Ÿผ", + "๐Ÿง‘๐Ÿพโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿง‘๐Ÿฝ", + "๐Ÿง‘๐Ÿพโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿง‘๐Ÿฟ", + "๐Ÿง‘๐Ÿฟโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿง‘๐Ÿป", + "๐Ÿง‘๐Ÿฟโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿง‘๐Ÿผ", + "๐Ÿง‘๐Ÿฟโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿง‘๐Ÿฝ", + "๐Ÿง‘๐Ÿฟโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿง‘๐Ÿพ", + "๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ", + "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘จโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ", + "๐Ÿ‘จ๐Ÿปโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘จ๐Ÿปโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘จ๐Ÿปโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘จ๐Ÿปโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘จ๐Ÿปโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘จ๐Ÿผโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘จ๐Ÿผโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘จ๐Ÿผโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘จ๐Ÿผโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘จ๐Ÿผโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘จ๐Ÿฝโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘จ๐Ÿฝโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘จ๐Ÿฝโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘จ๐Ÿฝโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘จ๐Ÿฝโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘จ๐Ÿพโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘จ๐Ÿพโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘จ๐Ÿพโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘จ๐Ÿพโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘จ๐Ÿพโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘จ๐Ÿฟโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘จ๐Ÿฟโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘จ๐Ÿฟโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘จ๐Ÿฟโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘จ๐Ÿฟโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ", + "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ๐Ÿฟ", + "๐Ÿ’‘", + "๐Ÿ’‘๐Ÿป", + "๐Ÿ’‘๐Ÿผ", + "๐Ÿ’‘๐Ÿฝ", + "๐Ÿ’‘๐Ÿพ", + "๐Ÿ’‘๐Ÿฟ", + "๐Ÿง‘๐Ÿปโ€โค๏ธโ€๐Ÿง‘๐Ÿผ", + "๐Ÿง‘๐Ÿปโ€โค๏ธโ€๐Ÿง‘๐Ÿฝ", + "๐Ÿง‘๐Ÿปโ€โค๏ธโ€๐Ÿง‘๐Ÿพ", + "๐Ÿง‘๐Ÿปโ€โค๏ธโ€๐Ÿง‘๐Ÿฟ", + "๐Ÿง‘๐Ÿผโ€โค๏ธโ€๐Ÿง‘๐Ÿป", + "๐Ÿง‘๐Ÿผโ€โค๏ธโ€๐Ÿง‘๐Ÿฝ", + "๐Ÿง‘๐Ÿผโ€โค๏ธโ€๐Ÿง‘๐Ÿพ", + "๐Ÿง‘๐Ÿผโ€โค๏ธโ€๐Ÿง‘๐Ÿฟ", + "๐Ÿง‘๐Ÿฝโ€โค๏ธโ€๐Ÿง‘๐Ÿป", + "๐Ÿง‘๐Ÿฝโ€โค๏ธโ€๐Ÿง‘๐Ÿผ", + "๐Ÿง‘๐Ÿฝโ€โค๏ธโ€๐Ÿง‘๐Ÿพ", + "๐Ÿง‘๐Ÿฝโ€โค๏ธโ€๐Ÿง‘๐Ÿฟ", + "๐Ÿง‘๐Ÿพโ€โค๏ธโ€๐Ÿง‘๐Ÿป", + "๐Ÿง‘๐Ÿพโ€โค๏ธโ€๐Ÿง‘๐Ÿผ", + "๐Ÿง‘๐Ÿพโ€โค๏ธโ€๐Ÿง‘๐Ÿฝ", + "๐Ÿง‘๐Ÿพโ€โค๏ธโ€๐Ÿง‘๐Ÿฟ", + "๐Ÿง‘๐Ÿฟโ€โค๏ธโ€๐Ÿง‘๐Ÿป", + "๐Ÿง‘๐Ÿฟโ€โค๏ธโ€๐Ÿง‘๐Ÿผ", + "๐Ÿง‘๐Ÿฟโ€โค๏ธโ€๐Ÿง‘๐Ÿฝ", + "๐Ÿง‘๐Ÿฟโ€โค๏ธโ€๐Ÿง‘๐Ÿพ", + "๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ‘จ", + "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘จโ€โค๏ธโ€๐Ÿ‘จ", + "๐Ÿ‘จ๐Ÿปโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘จ๐Ÿปโ€โค๏ธโ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘จ๐Ÿปโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘จ๐Ÿปโ€โค๏ธโ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘จ๐Ÿปโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘จ๐Ÿผโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘จ๐Ÿผโ€โค๏ธโ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘จ๐Ÿผโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘จ๐Ÿผโ€โค๏ธโ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘จ๐Ÿผโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘จ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘จ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘จ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘จ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘จ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘จ๐Ÿพโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘จ๐Ÿพโ€โค๏ธโ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘จ๐Ÿพโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘จ๐Ÿพโ€โค๏ธโ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘จ๐Ÿพโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘จ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป", + "๐Ÿ‘จ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿผ", + "๐Ÿ‘จ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฝ", + "๐Ÿ‘จ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿพ", + "๐Ÿ‘จ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฟ", + "๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ‘ฉ", + "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿฟ", + "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿป", + "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿผ", + "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿฝ", + "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿพ", + "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿฟ", + "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ", + "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง", + "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", + "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ", + "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง", + "๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ฆ", + "๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ง", + "๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", + "๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ", + "๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ง", + "๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ", + "๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ง", + "๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", + "๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ", + "๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง", + "๐Ÿ‘จโ€๐Ÿ‘ฆ", + "๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ", + "๐Ÿ‘จโ€๐Ÿ‘ง", + "๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", + "๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ง", + "๐Ÿ‘ฉโ€๐Ÿ‘ฆ", + "๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ", + "๐Ÿ‘ฉโ€๐Ÿ‘ง", + "๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", + "๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง", + "๐Ÿ—ฃ๏ธ", + "๐Ÿ‘ค", + "๐Ÿ‘ฅ", + "๐Ÿซ‚", + "๐Ÿ‘ช", + "๐Ÿง‘โ€๐Ÿง‘โ€๐Ÿง’", + "๐Ÿง‘โ€๐Ÿง‘โ€๐Ÿง’โ€๐Ÿง’", + "๐Ÿง‘โ€๐Ÿง’", + "๐Ÿง‘โ€๐Ÿง’โ€๐Ÿง’", + "๐Ÿ‘ฃ", + "๐Ÿซ†", + "๐Ÿต", + "๐Ÿ’", + "๐Ÿฆ", + "๐Ÿฆง", + "๐Ÿถ", + "๐Ÿ•", + "๐Ÿฆฎ", + "๐Ÿ•โ€๐Ÿฆบ", + "๐Ÿฉ", + "๐Ÿบ", + "๐ŸฆŠ", + "๐Ÿฆ", + "๐Ÿฑ", + "๐Ÿˆ", + "๐Ÿˆโ€โฌ›", + "๐Ÿฆ", + "๐Ÿฏ", + "๐Ÿ…", + "๐Ÿ†", + "๐Ÿด", + "๐ŸซŽ", + "๐Ÿซ", + "๐ŸŽ", + "๐Ÿฆ„", + "๐Ÿฆ“", + "๐ŸฆŒ", + "๐Ÿฆฌ", + "๐Ÿฎ", + "๐Ÿ‚", + "๐Ÿƒ", + "๐Ÿ„", + "๐Ÿท", + "๐Ÿ–", + "๐Ÿ—", + "๐Ÿฝ", + "๐Ÿ", + "๐Ÿ‘", + "๐Ÿ", + "๐Ÿช", + "๐Ÿซ", + "๐Ÿฆ™", + "๐Ÿฆ’", + "๐Ÿ˜", + "๐Ÿฆฃ", + "๐Ÿฆ", + "๐Ÿฆ›", + "๐Ÿญ", + "๐Ÿ", + "๐Ÿ€", + "๐Ÿน", + "๐Ÿฐ", + "๐Ÿ‡", + "๐Ÿฟ๏ธ", + "๐Ÿฆซ", + "๐Ÿฆ”", + "๐Ÿฆ‡", + "๐Ÿป", + "๐Ÿปโ€โ„๏ธ", + "๐Ÿจ", + "๐Ÿผ", + "๐Ÿฆฅ", + "๐Ÿฆฆ", + "๐Ÿฆจ", + "๐Ÿฆ˜", + "๐Ÿฆก", + "๐Ÿพ", + "๐Ÿฆƒ", + "๐Ÿ”", + "๐Ÿ“", + "๐Ÿฃ", + "๐Ÿค", + "๐Ÿฅ", + "๐Ÿฆ", + "๐Ÿง", + "๐Ÿ•Š๏ธ", + "๐Ÿฆ…", + "๐Ÿฆ†", + "๐Ÿฆข", + "๐Ÿฆ‰", + "๐Ÿฆค", + "๐Ÿชถ", + "๐Ÿฆฉ", + "๐Ÿฆš", + "๐Ÿฆœ", + "๐Ÿชฝ", + "๐Ÿฆโ€โฌ›", + "๐Ÿชฟ", + "๐Ÿฆโ€๐Ÿ”ฅ", + "๐Ÿธ", + "๐ŸŠ", + "๐Ÿข", + "๐ŸฆŽ", + "๐Ÿ", + "๐Ÿฒ", + "๐Ÿ‰", + "๐Ÿฆ•", + "๐Ÿฆ–", + "๐Ÿณ", + "๐Ÿ‹", + "๐Ÿฌ", + "๐Ÿฆญ", + "๐ŸŸ", + "๐Ÿ ", + "๐Ÿก", + "๐Ÿฆˆ", + "๐Ÿ™", + "๐Ÿš", + "๐Ÿชธ", + "๐Ÿชผ", + "๐Ÿฆ€", + "๐Ÿฆž", + "๐Ÿฆ", + "๐Ÿฆ‘", + "๐Ÿฆช", + "๐ŸŒ", + "๐Ÿฆ‹", + "๐Ÿ›", + "๐Ÿœ", + "๐Ÿ", + "๐Ÿชฒ", + "๐Ÿž", + "๐Ÿฆ—", + "๐Ÿชณ", + "๐Ÿ•ท๏ธ", + "๐Ÿ•ธ๏ธ", + "๐Ÿฆ‚", + "๐ŸฆŸ", + "๐Ÿชฐ", + "๐Ÿชฑ", + "๐Ÿฆ ", + "๐Ÿ’", + "๐ŸŒธ", + "๐Ÿ’ฎ", + "๐Ÿชท", + "๐Ÿต๏ธ", + "๐ŸŒน", + "๐Ÿฅ€", + "๐ŸŒบ", + "๐ŸŒป", + "๐ŸŒผ", + "๐ŸŒท", + "๐Ÿชป", + "๐ŸŒฑ", + "๐Ÿชด", + "๐ŸŒฒ", + "๐ŸŒณ", + "๐ŸŒด", + "๐ŸŒต", + "๐ŸŒพ", + "๐ŸŒฟ", + "โ˜˜๏ธ", + "๐Ÿ€", + "๐Ÿ", + "๐Ÿ‚", + "๐Ÿƒ", + "๐Ÿชน", + "๐Ÿชบ", + "๐Ÿ„", + "๐Ÿชพ", + "๐Ÿ‡", + "๐Ÿˆ", + "๐Ÿ‰", + "๐ŸŠ", + "๐Ÿ‹", + "๐Ÿ‹โ€๐ŸŸฉ", + "๐ŸŒ", + "๐Ÿ", + "๐Ÿฅญ", + "๐ŸŽ", + "๐Ÿ", + "๐Ÿ", + "๐Ÿ‘", + "๐Ÿ’", + "๐Ÿ“", + "๐Ÿซ", + "๐Ÿฅ", + "๐Ÿ…", + "๐Ÿซ’", + "๐Ÿฅฅ", + "๐Ÿฅ‘", + "๐Ÿ†", + "๐Ÿฅ”", + "๐Ÿฅ•", + "๐ŸŒฝ", + "๐ŸŒถ๏ธ", + "๐Ÿซ‘", + "๐Ÿฅ’", + "๐Ÿฅฌ", + "๐Ÿฅฆ", + "๐Ÿง„", + "๐Ÿง…", + "๐Ÿฅœ", + "๐Ÿซ˜", + "๐ŸŒฐ", + "๐Ÿซš", + "๐Ÿซ›", + "๐Ÿ„โ€๐ŸŸซ", + "๐Ÿซœ", + "๐Ÿž", + "๐Ÿฅ", + "๐Ÿฅ–", + "๐Ÿซ“", + "๐Ÿฅจ", + "๐Ÿฅฏ", + "๐Ÿฅž", + "๐Ÿง‡", + "๐Ÿง€", + "๐Ÿ–", + "๐Ÿ—", + "๐Ÿฅฉ", + "๐Ÿฅ“", + "๐Ÿ”", + "๐ŸŸ", + "๐Ÿ•", + "๐ŸŒญ", + "๐Ÿฅช", + "๐ŸŒฎ", + "๐ŸŒฏ", + "๐Ÿซ”", + "๐Ÿฅ™", + "๐Ÿง†", + "๐Ÿฅš", + "๐Ÿณ", + "๐Ÿฅ˜", + "๐Ÿฒ", + "๐Ÿซ•", + "๐Ÿฅฃ", + "๐Ÿฅ—", + "๐Ÿฟ", + "๐Ÿงˆ", + "๐Ÿง‚", + "๐Ÿฅซ", + "๐Ÿฑ", + "๐Ÿ˜", + "๐Ÿ™", + "๐Ÿš", + "๐Ÿ›", + "๐Ÿœ", + "๐Ÿ", + "๐Ÿ ", + "๐Ÿข", + "๐Ÿฃ", + "๐Ÿค", + "๐Ÿฅ", + "๐Ÿฅฎ", + "๐Ÿก", + "๐ŸฅŸ", + "๐Ÿฅ ", + "๐Ÿฅก", + "๐Ÿฆ", + "๐Ÿง", + "๐Ÿจ", + "๐Ÿฉ", + "๐Ÿช", + "๐ŸŽ‚", + "๐Ÿฐ", + "๐Ÿง", + "๐Ÿฅง", + "๐Ÿซ", + "๐Ÿฌ", + "๐Ÿญ", + "๐Ÿฎ", + "๐Ÿฏ", + "๐Ÿผ", + "๐Ÿฅ›", + "โ˜•", + "๐Ÿซ–", + "๐Ÿต", + "๐Ÿถ", + "๐Ÿพ", + "๐Ÿท", + "๐Ÿธ", + "๐Ÿน", + "๐Ÿบ", + "๐Ÿป", + "๐Ÿฅ‚", + "๐Ÿฅƒ", + "๐Ÿซ—", + "๐Ÿฅค", + "๐Ÿง‹", + "๐Ÿงƒ", + "๐Ÿง‰", + "๐ŸงŠ", + "๐Ÿฅข", + "๐Ÿฝ๏ธ", + "๐Ÿด", + "๐Ÿฅ„", + "๐Ÿ”ช", + "๐Ÿซ™", + "๐Ÿบ", + "๐ŸŒ", + "๐ŸŒŽ", + "๐ŸŒ", + "๐ŸŒ", + "๐Ÿ—บ๏ธ", + "๐Ÿ—พ", + "๐Ÿงญ", + "๐Ÿ”๏ธ", + "โ›ฐ๏ธ", + "๐ŸŒ‹", + "๐Ÿ—ป", + "๐Ÿ•๏ธ", + "๐Ÿ–๏ธ", + "๐Ÿœ๏ธ", + "๐Ÿ๏ธ", + "๐Ÿž๏ธ", + "๐ŸŸ๏ธ", + "๐Ÿ›๏ธ", + "๐Ÿ—๏ธ", + "๐Ÿงฑ", + "๐Ÿชจ", + "๐Ÿชต", + "๐Ÿ›–", + "๐Ÿ˜๏ธ", + "๐Ÿš๏ธ", + "๐Ÿ ", + "๐Ÿก", + "๐Ÿข", + "๐Ÿฃ", + "๐Ÿค", + "๐Ÿฅ", + "๐Ÿฆ", + "๐Ÿจ", + "๐Ÿฉ", + "๐Ÿช", + "๐Ÿซ", + "๐Ÿฌ", + "๐Ÿญ", + "๐Ÿฏ", + "๐Ÿฐ", + "๐Ÿ’’", + "๐Ÿ—ผ", + "๐Ÿ—ฝ", + "โ›ช", + "๐Ÿ•Œ", + "๐Ÿ›•", + "๐Ÿ•", + "โ›ฉ๏ธ", + "๐Ÿ•‹", + "โ›ฒ", + "โ›บ", + "๐ŸŒ", + "๐ŸŒƒ", + "๐Ÿ™๏ธ", + "๐ŸŒ„", + "๐ŸŒ…", + "๐ŸŒ†", + "๐ŸŒ‡", + "๐ŸŒ‰", + "โ™จ๏ธ", + "๐ŸŽ ", + "๐Ÿ›", + "๐ŸŽก", + "๐ŸŽข", + "๐Ÿ’ˆ", + "๐ŸŽช", + "๐Ÿš‚", + "๐Ÿšƒ", + "๐Ÿš„", + "๐Ÿš…", + "๐Ÿš†", + "๐Ÿš‡", + "๐Ÿšˆ", + "๐Ÿš‰", + "๐ŸšŠ", + "๐Ÿš", + "๐Ÿšž", + "๐Ÿš‹", + "๐ŸšŒ", + "๐Ÿš", + "๐ŸšŽ", + "๐Ÿš", + "๐Ÿš‘", + "๐Ÿš’", + "๐Ÿš“", + "๐Ÿš”", + "๐Ÿš•", + "๐Ÿš–", + "๐Ÿš—", + "๐Ÿš˜", + "๐Ÿš™", + "๐Ÿ›ป", + "๐Ÿšš", + "๐Ÿš›", + "๐Ÿšœ", + "๐ŸŽ๏ธ", + "๐Ÿ๏ธ", + "๐Ÿ›ต", + "๐Ÿฆฝ", + "๐Ÿฆผ", + "๐Ÿ›บ", + "๐Ÿšฒ", + "๐Ÿ›ด", + "๐Ÿ›น", + "๐Ÿ›ผ", + "๐Ÿš", + "๐Ÿ›ฃ๏ธ", + "๐Ÿ›ค๏ธ", + "๐Ÿ›ข๏ธ", + "โ›ฝ", + "๐Ÿ›ž", + "๐Ÿšจ", + "๐Ÿšฅ", + "๐Ÿšฆ", + "๐Ÿ›‘", + "๐Ÿšง", + "โš“", + "๐Ÿ›Ÿ", + "โ›ต", + "๐Ÿ›ถ", + "๐Ÿšค", + "๐Ÿ›ณ๏ธ", + "โ›ด๏ธ", + "๐Ÿ›ฅ๏ธ", + "๐Ÿšข", + "โœˆ๏ธ", + "๐Ÿ›ฉ๏ธ", + "๐Ÿ›ซ", + "๐Ÿ›ฌ", + "๐Ÿช‚", + "๐Ÿ’บ", + "๐Ÿš", + "๐ŸšŸ", + "๐Ÿš ", + "๐Ÿšก", + "๐Ÿ›ฐ๏ธ", + "๐Ÿš€", + "๐Ÿ›ธ", + "๐Ÿ›Ž๏ธ", + "๐Ÿงณ", + "โŒ›", + "โณ", + "โŒš", + "โฐ", + "โฑ๏ธ", + "โฒ๏ธ", + "๐Ÿ•ฐ๏ธ", + "๐Ÿ•›", + "๐Ÿ•ง", + "๐Ÿ•", + "๐Ÿ•œ", + "๐Ÿ•‘", + "๐Ÿ•", + "๐Ÿ•’", + "๐Ÿ•ž", + "๐Ÿ•“", + "๐Ÿ•Ÿ", + "๐Ÿ•”", + "๐Ÿ• ", + "๐Ÿ••", + "๐Ÿ•ก", + "๐Ÿ•–", + "๐Ÿ•ข", + "๐Ÿ•—", + "๐Ÿ•ฃ", + "๐Ÿ•˜", + "๐Ÿ•ค", + "๐Ÿ•™", + "๐Ÿ•ฅ", + "๐Ÿ•š", + "๐Ÿ•ฆ", + "๐ŸŒ‘", + "๐ŸŒ’", + "๐ŸŒ“", + "๐ŸŒ”", + "๐ŸŒ•", + "๐ŸŒ–", + "๐ŸŒ—", + "๐ŸŒ˜", + "๐ŸŒ™", + "๐ŸŒš", + "๐ŸŒ›", + "๐ŸŒœ", + "๐ŸŒก๏ธ", + "โ˜€๏ธ", + "๐ŸŒ", + "๐ŸŒž", + "๐Ÿช", + "โญ", + "๐ŸŒŸ", + "๐ŸŒ ", + "๐ŸŒŒ", + "โ˜๏ธ", + "โ›…", + "โ›ˆ๏ธ", + "๐ŸŒค๏ธ", + "๐ŸŒฅ๏ธ", + "๐ŸŒฆ๏ธ", + "๐ŸŒง๏ธ", + "๐ŸŒจ๏ธ", + "๐ŸŒฉ๏ธ", + "๐ŸŒช๏ธ", + "๐ŸŒซ๏ธ", + "๐ŸŒฌ๏ธ", + "๐ŸŒ€", + "๐ŸŒˆ", + "๐ŸŒ‚", + "โ˜‚๏ธ", + "โ˜”", + "โ›ฑ๏ธ", + "โšก", + "โ„๏ธ", + "โ˜ƒ๏ธ", + "โ›„", + "โ˜„๏ธ", + "๐Ÿ”ฅ", + "๐Ÿ’ง", + "๐ŸŒŠ", + "๐ŸŽƒ", + "๐ŸŽ„", + "๐ŸŽ†", + "๐ŸŽ‡", + "๐Ÿงจ", + "โœจ", + "๐ŸŽˆ", + "๐ŸŽ‰", + "๐ŸŽŠ", + "๐ŸŽ‹", + "๐ŸŽ", + "๐ŸŽŽ", + "๐ŸŽ", + "๐ŸŽ", + "๐ŸŽ‘", + "๐Ÿงง", + "๐ŸŽ€", + "๐ŸŽ", + "๐ŸŽ—๏ธ", + "๐ŸŽŸ๏ธ", + "๐ŸŽซ", + "๐ŸŽ–๏ธ", + "๐Ÿ†", + "๐Ÿ…", + "๐Ÿฅ‡", + "๐Ÿฅˆ", + "๐Ÿฅ‰", + "โšฝ", + "โšพ", + "๐ŸฅŽ", + "๐Ÿ€", + "๐Ÿ", + "๐Ÿˆ", + "๐Ÿ‰", + "๐ŸŽพ", + "๐Ÿฅ", + "๐ŸŽณ", + "๐Ÿ", + "๐Ÿ‘", + "๐Ÿ’", + "๐Ÿฅ", + "๐Ÿ“", + "๐Ÿธ", + "๐ŸฅŠ", + "๐Ÿฅ‹", + "๐Ÿฅ…", + "โ›ณ", + "โ›ธ๏ธ", + "๐ŸŽฃ", + "๐Ÿคฟ", + "๐ŸŽฝ", + "๐ŸŽฟ", + "๐Ÿ›ท", + "๐ŸฅŒ", + "๐ŸŽฏ", + "๐Ÿช€", + "๐Ÿช", + "๐Ÿ”ซ", + "๐ŸŽฑ", + "๐Ÿ”ฎ", + "๐Ÿช„", + "๐ŸŽฎ", + "๐Ÿ•น๏ธ", + "๐ŸŽฐ", + "๐ŸŽฒ", + "๐Ÿงฉ", + "๐Ÿงธ", + "๐Ÿช…", + "๐Ÿชฉ", + "๐Ÿช†", + "โ™ ๏ธ", + "โ™ฅ๏ธ", + "โ™ฆ๏ธ", + "โ™ฃ๏ธ", + "โ™Ÿ๏ธ", + "๐Ÿƒ", + "๐Ÿ€„", + "๐ŸŽด", + "๐ŸŽญ", + "๐Ÿ–ผ๏ธ", + "๐ŸŽจ", + "๐Ÿงต", + "๐Ÿชก", + "๐Ÿงถ", + "๐Ÿชข", + "๐Ÿ‘“", + "๐Ÿ•ถ๏ธ", + "๐Ÿฅฝ", + "๐Ÿฅผ", + "๐Ÿฆบ", + "๐Ÿ‘”", + "๐Ÿ‘•", + "๐Ÿ‘–", + "๐Ÿงฃ", + "๐Ÿงค", + "๐Ÿงฅ", + "๐Ÿงฆ", + "๐Ÿ‘—", + "๐Ÿ‘˜", + "๐Ÿฅป", + "๐Ÿฉฑ", + "๐Ÿฉฒ", + "๐Ÿฉณ", + "๐Ÿ‘™", + "๐Ÿ‘š", + "๐Ÿชญ", + "๐Ÿ‘›", + "๐Ÿ‘œ", + "๐Ÿ‘", + "๐Ÿ›๏ธ", + "๐ŸŽ’", + "๐Ÿฉด", + "๐Ÿ‘ž", + "๐Ÿ‘Ÿ", + "๐Ÿฅพ", + "๐Ÿฅฟ", + "๐Ÿ‘ ", + "๐Ÿ‘ก", + "๐Ÿฉฐ", + "๐Ÿ‘ข", + "๐Ÿชฎ", + "๐Ÿ‘‘", + "๐Ÿ‘’", + "๐ŸŽฉ", + "๐ŸŽ“", + "๐Ÿงข", + "๐Ÿช–", + "โ›‘๏ธ", + "๐Ÿ“ฟ", + "๐Ÿ’„", + "๐Ÿ’", + "๐Ÿ’Ž", + "๐Ÿ”‡", + "๐Ÿ”ˆ", + "๐Ÿ”‰", + "๐Ÿ”Š", + "๐Ÿ“ข", + "๐Ÿ“ฃ", + "๐Ÿ“ฏ", + "๐Ÿ””", + "๐Ÿ”•", + "๐ŸŽผ", + "๐ŸŽต", + "๐ŸŽถ", + "๐ŸŽ™๏ธ", + "๐ŸŽš๏ธ", + "๐ŸŽ›๏ธ", + "๐ŸŽค", + "๐ŸŽง", + "๐Ÿ“ป", + "๐ŸŽท", + "๐Ÿช—", + "๐ŸŽธ", + "๐ŸŽน", + "๐ŸŽบ", + "๐ŸŽป", + "๐Ÿช•", + "๐Ÿฅ", + "๐Ÿช˜", + "๐Ÿช‡", + "๐Ÿชˆ", + "๐Ÿช‰", + "๐Ÿ“ฑ", + "๐Ÿ“ฒ", + "โ˜Ž๏ธ", + "๐Ÿ“ž", + "๐Ÿ“Ÿ", + "๐Ÿ“ ", + "๐Ÿ”‹", + "๐Ÿชซ", + "๐Ÿ”Œ", + "๐Ÿ’ป", + "๐Ÿ–ฅ๏ธ", + "๐Ÿ–จ๏ธ", + "โŒจ๏ธ", + "๐Ÿ–ฑ๏ธ", + "๐Ÿ–ฒ๏ธ", + "๐Ÿ’ฝ", + "๐Ÿ’พ", + "๐Ÿ’ฟ", + "๐Ÿ“€", + "๐Ÿงฎ", + "๐ŸŽฅ", + "๐ŸŽž๏ธ", + "๐Ÿ“ฝ๏ธ", + "๐ŸŽฌ", + "๐Ÿ“บ", + "๐Ÿ“ท", + "๐Ÿ“ธ", + "๐Ÿ“น", + "๐Ÿ“ผ", + "๐Ÿ”", + "๐Ÿ”Ž", + "๐Ÿ•ฏ๏ธ", + "๐Ÿ’ก", + "๐Ÿ”ฆ", + "๐Ÿฎ", + "๐Ÿช”", + "๐Ÿ“”", + "๐Ÿ“•", + "๐Ÿ“–", + "๐Ÿ“—", + "๐Ÿ“˜", + "๐Ÿ“™", + "๐Ÿ“š", + "๐Ÿ““", + "๐Ÿ“’", + "๐Ÿ“ƒ", + "๐Ÿ“œ", + "๐Ÿ“„", + "๐Ÿ“ฐ", + "๐Ÿ—ž๏ธ", + "๐Ÿ“‘", + "๐Ÿ”–", + "๐Ÿท๏ธ", + "๐Ÿ’ฐ", + "๐Ÿช™", + "๐Ÿ’ด", + "๐Ÿ’ต", + "๐Ÿ’ถ", + "๐Ÿ’ท", + "๐Ÿ’ธ", + "๐Ÿ’ณ", + "๐Ÿงพ", + "๐Ÿ’น", + "โœ‰๏ธ", + "๐Ÿ“ง", + "๐Ÿ“จ", + "๐Ÿ“ฉ", + "๐Ÿ“ค", + "๐Ÿ“ฅ", + "๐Ÿ“ฆ", + "๐Ÿ“ซ", + "๐Ÿ“ช", + "๐Ÿ“ฌ", + "๐Ÿ“ญ", + "๐Ÿ“ฎ", + "๐Ÿ—ณ๏ธ", + "โœ๏ธ", + "โœ’๏ธ", + "๐Ÿ–‹๏ธ", + "๐Ÿ–Š๏ธ", + "๐Ÿ–Œ๏ธ", + "๐Ÿ–๏ธ", + "๐Ÿ“", + "๐Ÿ’ผ", + "๐Ÿ“", + "๐Ÿ“‚", + "๐Ÿ—‚๏ธ", + "๐Ÿ“…", + "๐Ÿ“†", + "๐Ÿ—’๏ธ", + "๐Ÿ—“๏ธ", + "๐Ÿ“‡", + "๐Ÿ“ˆ", + "๐Ÿ“‰", + "๐Ÿ“Š", + "๐Ÿ“‹", + "๐Ÿ“Œ", + "๐Ÿ“", + "๐Ÿ“Ž", + "๐Ÿ–‡๏ธ", + "๐Ÿ“", + "๐Ÿ“", + "โœ‚๏ธ", + "๐Ÿ—ƒ๏ธ", + "๐Ÿ—„๏ธ", + "๐Ÿ—‘๏ธ", + "๐Ÿ”’", + "๐Ÿ”“", + "๐Ÿ”", + "๐Ÿ”", + "๐Ÿ”‘", + "๐Ÿ—๏ธ", + "๐Ÿ”จ", + "๐Ÿช“", + "โ›๏ธ", + "โš’๏ธ", + "๐Ÿ› ๏ธ", + "๐Ÿ—ก๏ธ", + "โš”๏ธ", + "๐Ÿ’ฃ", + "๐Ÿชƒ", + "๐Ÿน", + "๐Ÿ›ก๏ธ", + "๐Ÿชš", + "๐Ÿ”ง", + "๐Ÿช›", + "๐Ÿ”ฉ", + "โš™๏ธ", + "๐Ÿ—œ๏ธ", + "โš–๏ธ", + "๐Ÿฆฏ", + "๐Ÿ”—", + "โ›“๏ธโ€๐Ÿ’ฅ", + "โ›“๏ธ", + "๐Ÿช", + "๐Ÿงฐ", + "๐Ÿงฒ", + "๐Ÿชœ", + "๐Ÿช", + "โš—๏ธ", + "๐Ÿงช", + "๐Ÿงซ", + "๐Ÿงฌ", + "๐Ÿ”ฌ", + "๐Ÿ”ญ", + "๐Ÿ“ก", + "๐Ÿ’‰", + "๐Ÿฉธ", + "๐Ÿ’Š", + "๐Ÿฉน", + "๐Ÿฉผ", + "๐Ÿฉบ", + "๐Ÿฉป", + "๐Ÿšช", + "๐Ÿ›—", + "๐Ÿชž", + "๐ŸชŸ", + "๐Ÿ›๏ธ", + "๐Ÿ›‹๏ธ", + "๐Ÿช‘", + "๐Ÿšฝ", + "๐Ÿช ", + "๐Ÿšฟ", + "๐Ÿ›", + "๐Ÿชค", + "๐Ÿช’", + "๐Ÿงด", + "๐Ÿงท", + "๐Ÿงน", + "๐Ÿงบ", + "๐Ÿงป", + "๐Ÿชฃ", + "๐Ÿงผ", + "๐Ÿซง", + "๐Ÿชฅ", + "๐Ÿงฝ", + "๐Ÿงฏ", + "๐Ÿ›’", + "๐Ÿšฌ", + "โšฐ๏ธ", + "๐Ÿชฆ", + "โšฑ๏ธ", + "๐Ÿงฟ", + "๐Ÿชฌ", + "๐Ÿ—ฟ", + "๐Ÿชง", + "๐Ÿชช", + "๐Ÿง", + "๐Ÿšฎ", + "๐Ÿšฐ", + "โ™ฟ", + "๐Ÿšน", + "๐Ÿšบ", + "๐Ÿšป", + "๐Ÿšผ", + "๐Ÿšพ", + "๐Ÿ›‚", + "๐Ÿ›ƒ", + "๐Ÿ›„", + "๐Ÿ›…", + "โš ๏ธ", + "๐Ÿšธ", + "โ›”", + "๐Ÿšซ", + "๐Ÿšณ", + "๐Ÿšญ", + "๐Ÿšฏ", + "๐Ÿšฑ", + "๐Ÿšท", + "๐Ÿ“ต", + "๐Ÿ”ž", + "โ˜ข๏ธ", + "โ˜ฃ๏ธ", + "โฌ†๏ธ", + "โ†—๏ธ", + "โžก๏ธ", + "โ†˜๏ธ", + "โฌ‡๏ธ", + "โ†™๏ธ", + "โฌ…๏ธ", + "โ†–๏ธ", + "โ†•๏ธ", + "โ†”๏ธ", + "โ†ฉ๏ธ", + "โ†ช๏ธ", + "โคด๏ธ", + "โคต๏ธ", + "๐Ÿ”ƒ", + "๐Ÿ”„", + "๐Ÿ”™", + "๐Ÿ”š", + "๐Ÿ”›", + "๐Ÿ”œ", + "๐Ÿ”", + "๐Ÿ›", + "โš›๏ธ", + "๐Ÿ•‰๏ธ", + "โœก๏ธ", + "โ˜ธ๏ธ", + "โ˜ฏ๏ธ", + "โœ๏ธ", + "โ˜ฆ๏ธ", + "โ˜ช๏ธ", + "โ˜ฎ๏ธ", + "๐Ÿ•Ž", + "๐Ÿ”ฏ", + "๐Ÿชฏ", + "โ™ˆ", + "โ™‰", + "โ™Š", + "โ™‹", + "โ™Œ", + "โ™", + "โ™Ž", + "โ™", + "โ™", + "โ™‘", + "โ™’", + "โ™“", + "โ›Ž", + "๐Ÿ”€", + "๐Ÿ”", + "๐Ÿ”‚", + "โ–ถ๏ธ", + "โฉ", + "โญ๏ธ", + "โฏ๏ธ", + "โ—€๏ธ", + "โช", + "โฎ๏ธ", + "๐Ÿ”ผ", + "โซ", + "๐Ÿ”ฝ", + "โฌ", + "โธ๏ธ", + "โน๏ธ", + "โบ๏ธ", + "โ๏ธ", + "๐ŸŽฆ", + "๐Ÿ”…", + "๐Ÿ”†", + "๐Ÿ“ถ", + "๐Ÿ›œ", + "๐Ÿ“ณ", + "๐Ÿ“ด", + "โ™€๏ธ", + "โ™‚๏ธ", + "โšง๏ธ", + "โœ–๏ธ", + "โž•", + "โž–", + "โž—", + "๐ŸŸฐ", + "โ™พ๏ธ", + "โ€ผ๏ธ", + "โ‰๏ธ", + "โ“", + "โ”", + "โ•", + "โ—", + "ใ€ฐ๏ธ", + "๐Ÿ’ฑ", + "๐Ÿ’ฒ", + "โš•๏ธ", + "โ™ป๏ธ", + "โšœ๏ธ", + "๐Ÿ”ฑ", + "๐Ÿ“›", + "๐Ÿ”ฐ", + "โญ•", + "โœ…", + "โ˜‘๏ธ", + "โœ”๏ธ", + "โŒ", + "โŽ", + "โžฐ", + "โžฟ", + "ใ€ฝ๏ธ", + "โœณ๏ธ", + "โœด๏ธ", + "โ‡๏ธ", + "ยฉ๏ธ", + "ยฎ๏ธ", + "โ„ข๏ธ", + "๐ŸซŸ", + "#๏ธโƒฃ", + "*๏ธโƒฃ", + "0๏ธโƒฃ", + "1๏ธโƒฃ", + "2๏ธโƒฃ", + "3๏ธโƒฃ", + "4๏ธโƒฃ", + "5๏ธโƒฃ", + "6๏ธโƒฃ", + "7๏ธโƒฃ", + "8๏ธโƒฃ", + "9๏ธโƒฃ", + "๐Ÿ”Ÿ", + "๐Ÿ” ", + "๐Ÿ”ก", + "๐Ÿ”ข", + "๐Ÿ”ฃ", + "๐Ÿ”ค", + "๐Ÿ…ฐ๏ธ", + "๐Ÿ†Ž", + "๐Ÿ…ฑ๏ธ", + "๐Ÿ†‘", + "๐Ÿ†’", + "๐Ÿ†“", + "โ„น๏ธ", + "๐Ÿ†”", + "โ“‚๏ธ", + "๐Ÿ†•", + "๐Ÿ†–", + "๐Ÿ…พ๏ธ", + "๐Ÿ†—", + "๐Ÿ…ฟ๏ธ", + "๐Ÿ†˜", + "๐Ÿ†™", + "๐Ÿ†š", + "๐Ÿˆ", + "๐Ÿˆ‚๏ธ", + "๐Ÿˆท๏ธ", + "๐Ÿˆถ", + "๐Ÿˆฏ", + "๐Ÿ‰", + "๐Ÿˆน", + "๐Ÿˆš", + "๐Ÿˆฒ", + "๐Ÿ‰‘", + "๐Ÿˆธ", + "๐Ÿˆด", + "๐Ÿˆณ", + "ใŠ—๏ธ", + "ใŠ™๏ธ", + "๐Ÿˆบ", + "๐Ÿˆต", + "๐Ÿ”ด", + "๐ŸŸ ", + "๐ŸŸก", + "๐ŸŸข", + "๐Ÿ”ต", + "๐ŸŸฃ", + "๐ŸŸค", + "โšซ", + "โšช", + "๐ŸŸฅ", + "๐ŸŸง", + "๐ŸŸจ", + "๐ŸŸฉ", + "๐ŸŸฆ", + "๐ŸŸช", + "๐ŸŸซ", + "โฌ›", + "โฌœ", + "โ—ผ๏ธ", + "โ—ป๏ธ", + "โ—พ", + "โ—ฝ", + "โ–ช๏ธ", + "โ–ซ๏ธ", + "๐Ÿ”ถ", + "๐Ÿ”ท", + "๐Ÿ”ธ", + "๐Ÿ”น", + "๐Ÿ”บ", + "๐Ÿ”ป", + "๐Ÿ’ ", + "๐Ÿ”˜", + "๐Ÿ”ณ", + "๐Ÿ”ฒ", + "๐Ÿ", + "๐Ÿšฉ", + "๐ŸŽŒ", + "๐Ÿด", + "๐Ÿณ๏ธ", + "๐Ÿณ๏ธโ€๐ŸŒˆ", + "๐Ÿณ๏ธโ€โšง๏ธ", + "๐Ÿดโ€โ˜ ๏ธ", + "๐Ÿ‡ฆ๐Ÿ‡จ", + "๐Ÿ‡ฆ๐Ÿ‡ฉ", + "๐Ÿ‡ฆ๐Ÿ‡ช", + "๐Ÿ‡ฆ๐Ÿ‡ซ", + "๐Ÿ‡ฆ๐Ÿ‡ฌ", + "๐Ÿ‡ฆ๐Ÿ‡ฎ", + "๐Ÿ‡ฆ๐Ÿ‡ฑ", + "๐Ÿ‡ฆ๐Ÿ‡ฒ", + "๐Ÿ‡ฆ๐Ÿ‡ด", + "๐Ÿ‡ฆ๐Ÿ‡ถ", + "๐Ÿ‡ฆ๐Ÿ‡ท", + "๐Ÿ‡ฆ๐Ÿ‡ธ", + "๐Ÿ‡ฆ๐Ÿ‡น", + "๐Ÿ‡ฆ๐Ÿ‡บ", + "๐Ÿ‡ฆ๐Ÿ‡ผ", + "๐Ÿ‡ฆ๐Ÿ‡ฝ", + "๐Ÿ‡ฆ๐Ÿ‡ฟ", + "๐Ÿ‡ง๐Ÿ‡ฆ", + "๐Ÿ‡ง๐Ÿ‡ง", + "๐Ÿ‡ง๐Ÿ‡ฉ", + "๐Ÿ‡ง๐Ÿ‡ช", + "๐Ÿ‡ง๐Ÿ‡ซ", + "๐Ÿ‡ง๐Ÿ‡ฌ", + "๐Ÿ‡ง๐Ÿ‡ญ", + "๐Ÿ‡ง๐Ÿ‡ฎ", + "๐Ÿ‡ง๐Ÿ‡ฏ", + "๐Ÿ‡ง๐Ÿ‡ฑ", + "๐Ÿ‡ง๐Ÿ‡ฒ", + "๐Ÿ‡ง๐Ÿ‡ณ", + "๐Ÿ‡ง๐Ÿ‡ด", + "๐Ÿ‡ง๐Ÿ‡ถ", + "๐Ÿ‡ง๐Ÿ‡ท", + "๐Ÿ‡ง๐Ÿ‡ธ", + "๐Ÿ‡ง๐Ÿ‡น", + "๐Ÿ‡ง๐Ÿ‡ป", + "๐Ÿ‡ง๐Ÿ‡ผ", + "๐Ÿ‡ง๐Ÿ‡พ", + "๐Ÿ‡ง๐Ÿ‡ฟ", + "๐Ÿ‡จ๐Ÿ‡ฆ", + "๐Ÿ‡จ๐Ÿ‡จ", + "๐Ÿ‡จ๐Ÿ‡ฉ", + "๐Ÿ‡จ๐Ÿ‡ซ", + "๐Ÿ‡จ๐Ÿ‡ฌ", + "๐Ÿ‡จ๐Ÿ‡ญ", + "๐Ÿ‡จ๐Ÿ‡ฎ", + "๐Ÿ‡จ๐Ÿ‡ฐ", + "๐Ÿ‡จ๐Ÿ‡ฑ", + "๐Ÿ‡จ๐Ÿ‡ฒ", + "๐Ÿ‡จ๐Ÿ‡ณ", + "๐Ÿ‡จ๐Ÿ‡ด", + "๐Ÿ‡จ๐Ÿ‡ต", + "๐Ÿ‡จ๐Ÿ‡ถ", + "๐Ÿ‡จ๐Ÿ‡ท", + "๐Ÿ‡จ๐Ÿ‡บ", + "๐Ÿ‡จ๐Ÿ‡ป", + "๐Ÿ‡จ๐Ÿ‡ผ", + "๐Ÿ‡จ๐Ÿ‡ฝ", + "๐Ÿ‡จ๐Ÿ‡พ", + "๐Ÿ‡จ๐Ÿ‡ฟ", + "๐Ÿ‡ฉ๐Ÿ‡ช", + "๐Ÿ‡ฉ๐Ÿ‡ฌ", + "๐Ÿ‡ฉ๐Ÿ‡ฏ", + "๐Ÿ‡ฉ๐Ÿ‡ฐ", + "๐Ÿ‡ฉ๐Ÿ‡ฒ", + "๐Ÿ‡ฉ๐Ÿ‡ด", + "๐Ÿ‡ฉ๐Ÿ‡ฟ", + "๐Ÿ‡ช๐Ÿ‡ฆ", + "๐Ÿ‡ช๐Ÿ‡จ", + "๐Ÿ‡ช๐Ÿ‡ช", + "๐Ÿ‡ช๐Ÿ‡ฌ", + "๐Ÿ‡ช๐Ÿ‡ญ", + "๐Ÿ‡ช๐Ÿ‡ท", + "๐Ÿ‡ช๐Ÿ‡ธ", + "๐Ÿ‡ช๐Ÿ‡น", + "๐Ÿ‡ช๐Ÿ‡บ", + "๐Ÿ‡ซ๐Ÿ‡ฎ", + "๐Ÿ‡ซ๐Ÿ‡ฏ", + "๐Ÿ‡ซ๐Ÿ‡ฐ", + "๐Ÿ‡ซ๐Ÿ‡ฒ", + "๐Ÿ‡ซ๐Ÿ‡ด", + "๐Ÿ‡ซ๐Ÿ‡ท", + "๐Ÿ‡ฌ๐Ÿ‡ฆ", + "๐Ÿ‡ฌ๐Ÿ‡ง", + "๐Ÿ‡ฌ๐Ÿ‡ฉ", + "๐Ÿ‡ฌ๐Ÿ‡ช", + "๐Ÿ‡ฌ๐Ÿ‡ซ", + "๐Ÿ‡ฌ๐Ÿ‡ฌ", + "๐Ÿ‡ฌ๐Ÿ‡ญ", + "๐Ÿ‡ฌ๐Ÿ‡ฎ", + "๐Ÿ‡ฌ๐Ÿ‡ฑ", + "๐Ÿ‡ฌ๐Ÿ‡ฒ", + "๐Ÿ‡ฌ๐Ÿ‡ณ", + "๐Ÿ‡ฌ๐Ÿ‡ต", + "๐Ÿ‡ฌ๐Ÿ‡ถ", + "๐Ÿ‡ฌ๐Ÿ‡ท", + "๐Ÿ‡ฌ๐Ÿ‡ธ", + "๐Ÿ‡ฌ๐Ÿ‡น", + "๐Ÿ‡ฌ๐Ÿ‡บ", + "๐Ÿ‡ฌ๐Ÿ‡ผ", + "๐Ÿ‡ฌ๐Ÿ‡พ", + "๐Ÿ‡ญ๐Ÿ‡ฐ", + "๐Ÿ‡ญ๐Ÿ‡ฒ", + "๐Ÿ‡ญ๐Ÿ‡ณ", + "๐Ÿ‡ญ๐Ÿ‡ท", + "๐Ÿ‡ญ๐Ÿ‡น", + "๐Ÿ‡ญ๐Ÿ‡บ", + "๐Ÿ‡ฎ๐Ÿ‡จ", + "๐Ÿ‡ฎ๐Ÿ‡ฉ", + "๐Ÿ‡ฎ๐Ÿ‡ช", + "๐Ÿ‡ฎ๐Ÿ‡ฑ", + "๐Ÿ‡ฎ๐Ÿ‡ฒ", + "๐Ÿ‡ฎ๐Ÿ‡ณ", + "๐Ÿ‡ฎ๐Ÿ‡ด", + "๐Ÿ‡ฎ๐Ÿ‡ถ", + "๐Ÿ‡ฎ๐Ÿ‡ท", + "๐Ÿ‡ฎ๐Ÿ‡ธ", + "๐Ÿ‡ฎ๐Ÿ‡น", + "๐Ÿ‡ฏ๐Ÿ‡ช", + "๐Ÿ‡ฏ๐Ÿ‡ฒ", + "๐Ÿ‡ฏ๐Ÿ‡ด", + "๐Ÿ‡ฏ๐Ÿ‡ต", + "๐Ÿ‡ฐ๐Ÿ‡ช", + "๐Ÿ‡ฐ๐Ÿ‡ฌ", + "๐Ÿ‡ฐ๐Ÿ‡ญ", + "๐Ÿ‡ฐ๐Ÿ‡ฎ", + "๐Ÿ‡ฐ๐Ÿ‡ฒ", + "๐Ÿ‡ฐ๐Ÿ‡ณ", + "๐Ÿ‡ฐ๐Ÿ‡ต", + "๐Ÿ‡ฐ๐Ÿ‡ท", + "๐Ÿ‡ฐ๐Ÿ‡ผ", + "๐Ÿ‡ฐ๐Ÿ‡พ", + "๐Ÿ‡ฐ๐Ÿ‡ฟ", + "๐Ÿ‡ฑ๐Ÿ‡ฆ", + "๐Ÿ‡ฑ๐Ÿ‡ง", + "๐Ÿ‡ฑ๐Ÿ‡จ", + "๐Ÿ‡ฑ๐Ÿ‡ฎ", + "๐Ÿ‡ฑ๐Ÿ‡ฐ", + "๐Ÿ‡ฑ๐Ÿ‡ท", + "๐Ÿ‡ฑ๐Ÿ‡ธ", + "๐Ÿ‡ฑ๐Ÿ‡น", + "๐Ÿ‡ฑ๐Ÿ‡บ", + "๐Ÿ‡ฑ๐Ÿ‡ป", + "๐Ÿ‡ฑ๐Ÿ‡พ", + "๐Ÿ‡ฒ๐Ÿ‡ฆ", + "๐Ÿ‡ฒ๐Ÿ‡จ", + "๐Ÿ‡ฒ๐Ÿ‡ฉ", + "๐Ÿ‡ฒ๐Ÿ‡ช", + "๐Ÿ‡ฒ๐Ÿ‡ซ", + "๐Ÿ‡ฒ๐Ÿ‡ฌ", + "๐Ÿ‡ฒ๐Ÿ‡ญ", + "๐Ÿ‡ฒ๐Ÿ‡ฐ", + "๐Ÿ‡ฒ๐Ÿ‡ฑ", + "๐Ÿ‡ฒ๐Ÿ‡ฒ", + "๐Ÿ‡ฒ๐Ÿ‡ณ", + "๐Ÿ‡ฒ๐Ÿ‡ด", + "๐Ÿ‡ฒ๐Ÿ‡ต", + "๐Ÿ‡ฒ๐Ÿ‡ถ", + "๐Ÿ‡ฒ๐Ÿ‡ท", + "๐Ÿ‡ฒ๐Ÿ‡ธ", + "๐Ÿ‡ฒ๐Ÿ‡น", + "๐Ÿ‡ฒ๐Ÿ‡บ", + "๐Ÿ‡ฒ๐Ÿ‡ป", + "๐Ÿ‡ฒ๐Ÿ‡ผ", + "๐Ÿ‡ฒ๐Ÿ‡ฝ", + "๐Ÿ‡ฒ๐Ÿ‡พ", + "๐Ÿ‡ฒ๐Ÿ‡ฟ", + "๐Ÿ‡ณ๐Ÿ‡ฆ", + "๐Ÿ‡ณ๐Ÿ‡จ", + "๐Ÿ‡ณ๐Ÿ‡ช", + "๐Ÿ‡ณ๐Ÿ‡ซ", + "๐Ÿ‡ณ๐Ÿ‡ฌ", + "๐Ÿ‡ณ๐Ÿ‡ฎ", + "๐Ÿ‡ณ๐Ÿ‡ฑ", + "๐Ÿ‡ณ๐Ÿ‡ด", + "๐Ÿ‡ณ๐Ÿ‡ต", + "๐Ÿ‡ณ๐Ÿ‡ท", + "๐Ÿ‡ณ๐Ÿ‡บ", + "๐Ÿ‡ณ๐Ÿ‡ฟ", + "๐Ÿ‡ด๐Ÿ‡ฒ", + "๐Ÿ‡ต๐Ÿ‡ฆ", + "๐Ÿ‡ต๐Ÿ‡ช", + "๐Ÿ‡ต๐Ÿ‡ซ", + "๐Ÿ‡ต๐Ÿ‡ฌ", + "๐Ÿ‡ต๐Ÿ‡ญ", + "๐Ÿ‡ต๐Ÿ‡ฐ", + "๐Ÿ‡ต๐Ÿ‡ฑ", + "๐Ÿ‡ต๐Ÿ‡ฒ", + "๐Ÿ‡ต๐Ÿ‡ณ", + "๐Ÿ‡ต๐Ÿ‡ท", + "๐Ÿ‡ต๐Ÿ‡ธ", + "๐Ÿ‡ต๐Ÿ‡น", + "๐Ÿ‡ต๐Ÿ‡ผ", + "๐Ÿ‡ต๐Ÿ‡พ", + "๐Ÿ‡ถ๐Ÿ‡ฆ", + "๐Ÿ‡ท๐Ÿ‡ช", + "๐Ÿ‡ท๐Ÿ‡ด", + "๐Ÿ‡ท๐Ÿ‡ธ", + "๐Ÿ‡ท๐Ÿ‡บ", + "๐Ÿ‡ท๐Ÿ‡ผ", + "๐Ÿ‡ธ๐Ÿ‡ฆ", + "๐Ÿ‡ธ๐Ÿ‡ง", + "๐Ÿ‡ธ๐Ÿ‡จ", + "๐Ÿ‡ธ๐Ÿ‡ฉ", + "๐Ÿ‡ธ๐Ÿ‡ช", + "๐Ÿ‡ธ๐Ÿ‡ฌ", + "๐Ÿ‡ธ๐Ÿ‡ญ", + "๐Ÿ‡ธ๐Ÿ‡ฎ", + "๐Ÿ‡ธ๐Ÿ‡ฏ", + "๐Ÿ‡ธ๐Ÿ‡ฐ", + "๐Ÿ‡ธ๐Ÿ‡ฑ", + "๐Ÿ‡ธ๐Ÿ‡ฒ", + "๐Ÿ‡ธ๐Ÿ‡ณ", + "๐Ÿ‡ธ๐Ÿ‡ด", + "๐Ÿ‡ธ๐Ÿ‡ท", + "๐Ÿ‡ธ๐Ÿ‡ธ", + "๐Ÿ‡ธ๐Ÿ‡น", + "๐Ÿ‡ธ๐Ÿ‡ป", + "๐Ÿ‡ธ๐Ÿ‡ฝ", + "๐Ÿ‡ธ๐Ÿ‡พ", + "๐Ÿ‡ธ๐Ÿ‡ฟ", + "๐Ÿ‡น๐Ÿ‡ฆ", + "๐Ÿ‡น๐Ÿ‡จ", + "๐Ÿ‡น๐Ÿ‡ฉ", + "๐Ÿ‡น๐Ÿ‡ซ", + "๐Ÿ‡น๐Ÿ‡ฌ", + "๐Ÿ‡น๐Ÿ‡ญ", + "๐Ÿ‡น๐Ÿ‡ฏ", + "๐Ÿ‡น๐Ÿ‡ฐ", + "๐Ÿ‡น๐Ÿ‡ฑ", + "๐Ÿ‡น๐Ÿ‡ฒ", + "๐Ÿ‡น๐Ÿ‡ณ", + "๐Ÿ‡น๐Ÿ‡ด", + "๐Ÿ‡น๐Ÿ‡ท", + "๐Ÿ‡น๐Ÿ‡น", + "๐Ÿ‡น๐Ÿ‡ป", + "๐Ÿ‡น๐Ÿ‡ผ", + "๐Ÿ‡น๐Ÿ‡ฟ", + "๐Ÿ‡บ๐Ÿ‡ฆ", + "๐Ÿ‡บ๐Ÿ‡ฌ", + "๐Ÿ‡บ๐Ÿ‡ฒ", + "๐Ÿ‡บ๐Ÿ‡ณ", + "๐Ÿ‡บ๐Ÿ‡ธ", + "๐Ÿ‡บ๐Ÿ‡พ", + "๐Ÿ‡บ๐Ÿ‡ฟ", + "๐Ÿ‡ป๐Ÿ‡ฆ", + "๐Ÿ‡ป๐Ÿ‡จ", + "๐Ÿ‡ป๐Ÿ‡ช", + "๐Ÿ‡ป๐Ÿ‡ฌ", + "๐Ÿ‡ป๐Ÿ‡ฎ", + "๐Ÿ‡ป๐Ÿ‡ณ", + "๐Ÿ‡ป๐Ÿ‡บ", + "๐Ÿ‡ผ๐Ÿ‡ซ", + "๐Ÿ‡ผ๐Ÿ‡ธ", + "๐Ÿ‡ฝ๐Ÿ‡ฐ", + "๐Ÿ‡พ๐Ÿ‡ช", + "๐Ÿ‡พ๐Ÿ‡น", + "๐Ÿ‡ฟ๐Ÿ‡ฆ", + "๐Ÿ‡ฟ๐Ÿ‡ฒ", + "๐Ÿ‡ฟ๐Ÿ‡ผ", + "๐Ÿด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ", + "๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ", + "๐Ÿด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ" + ]; +} \ No newline at end of file diff --git a/Femto.Modules.Files/Domain/Files/File.cs b/Femto.Modules.Files/Domain/Files/File.cs deleted file mode 100644 index 9600ceb..0000000 --- a/Femto.Modules.Files/Domain/Files/File.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Femto.Modules.Files.Domain.Files; - -public class File -{ - Guid Id { get; set; } - - -} \ No newline at end of file diff --git a/Femto.Modules.Files/Femto.Modules.Files.csproj b/Femto.Modules.Files/Femto.Modules.Files.csproj deleted file mode 100644 index 17b910f..0000000 --- a/Femto.Modules.Files/Femto.Modules.Files.csproj +++ /dev/null @@ -1,9 +0,0 @@ -๏ปฟ - - - net9.0 - enable - enable - - - diff --git a/scripts/generate-emojis.js b/scripts/generate-emojis.js new file mode 100644 index 0000000..ba3a3b6 --- /dev/null +++ b/scripts/generate-emojis.js @@ -0,0 +1,38 @@ +try { + console.error('Downloading emoji-test.txt...'); + const response = await fetch('https://unicode.org/Public/emoji/latest/emoji-test.txt'); + const text = await response.text(); + const emojis = extractEmojis(text); + console.error(`Extracted ${emojis.length} fully-qualified emojis.`); + console.log(` + namespace Femto.Modules.Blog.Emoji; + + internal static partial class AllEmoji + { + public static readonly string[] Emojis = [\n${emojis.map(e => `"${e}"`).join(',\n')}\n]; + } + `) +} catch (err) { + console.error('Error:', err); +} + + +function extractEmojis(text) { + const lines = text.split('\n'); + const emojis = []; + + for (const line of lines) { + if (line.startsWith('#') || line.trim() === '') continue; + + const [codePart, descPart] = line.split(';'); + if (!descPart || !descPart.includes('fully-qualified')) continue; + + const match = line.match(/#\s+(.+?)\s+E\d+\.\d+/); + if (match) { + emojis.push(match[1]); + } + } + + return emojis; +} +