♻️ No idea, but errors all gone

This commit is contained in:
2025-07-08 23:55:31 +08:00
parent 2c67472894
commit 63b2b989ba
74 changed files with 1551 additions and 1100 deletions

View File

@@ -0,0 +1,78 @@
using MailKit.Net.Smtp;
using MimeKit;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
using System.Collections.Generic;
namespace DysonNetwork.Shared.Services;
public class EmailService
{
private readonly IConfiguration _configuration;
private readonly ILogger<EmailService> _logger;
public EmailService(IConfiguration configuration, ILogger<EmailService> logger)
{
_configuration = configuration;
_logger = logger;
}
public async Task SendEmailAsync(
string toName,
string toEmail,
string subject,
string body,
Dictionary<string, string>? headers = null
)
{
var message = new MimeMessage();
message.From.Add(new MailboxAddress(
_configuration["Email:SenderName"],
_configuration["Email:SenderEmail"]
));
message.To.Add(new MailboxAddress(toName, toEmail));
message.Subject = subject;
var bodyBuilder = new BodyBuilder { HtmlBody = body };
message.Body = bodyBuilder.ToMessageBody();
if (headers != null)
{
foreach (var header in headers)
{
message.Headers.Add(header.Key, header.Value);
}
}
using var client = new SmtpClient();
try
{
await client.ConnectAsync(
_configuration["Email:SmtpHost"],
int.Parse(_configuration["Email:SmtpPort"]),
MailKit.Security.SecureSocketOptions.StartTls
);
await client.AuthenticateAsync(
_configuration["Email:SmtpUser"],
_configuration["Email:SmtpPass"]
);
await client.SendAsync(message);
_logger.LogInformation("Email sent to {ToEmail}", toEmail);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send email to {ToEmail}", toEmail);
throw;
}
finally
{
await client.DisconnectAsync(true);
}
}
public async Task SendTemplatedEmailAsync<T>(string toName, string toEmail, string subject, string htmlBody)
{
await SendEmailAsync(toName, toEmail, subject, htmlBody);
}
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using DysonNetwork.Shared.Models;
using MagicOnion;
using NodaTime;
@@ -10,17 +11,22 @@ public interface IAccountEventService : IService<IAccountEventService>
/// Purges the status cache for a user
/// </summary>
void PurgeStatusCache(Guid userId);
/// <summary>
/// Gets the status of a user
/// </summary>
Task<Status> GetStatus(Guid userId);
/// <summary>
/// Gets the statuses of a list of users
/// </summary>
Task<Dictionary<Guid, Status>> GetStatuses(List<Guid> userIds);
/// <summary>
/// Performs a daily check-in for a user
/// </summary>
Task<CheckInResult> CheckInDaily(Account user);
/// <summary>
/// Gets the check-in streak for a user
/// </summary>

View File

@@ -0,0 +1,54 @@
using DysonNetwork.Shared.Models;
using MagicOnion;
using System;
using System.Threading.Tasks;
namespace DysonNetwork.Shared.Services
{
public interface IAccountProfileService : IService<IAccountProfileService>
{
/// <summary>
/// Gets an account profile by account ID.
/// </summary>
/// <param name="accountId">The ID of the account.</param>
/// <returns>The account profile if found, otherwise null.</returns>
Task<Profile?> GetAccountProfileByIdAsync(Guid accountId);
/// <summary>
/// Updates the StellarMembership of an account.
/// </summary>
/// <param name="accountId">The ID of the account.</param>
/// <param name="subscription">The subscription to set as the StellarMembership.</param>
/// <returns>The updated account profile.</returns>
Task<Profile> UpdateStellarMembershipAsync(Guid accountId, SubscriptionReferenceObject? subscription);
/// <summary>
/// Gets all account profiles that have a non-null StellarMembership.
/// </summary>
/// <returns>A list of account profiles with StellarMembership.</returns>
Task<List<Profile>> GetAccountsWithStellarMembershipAsync();
/// <summary>
/// Clears the StellarMembership for a list of account IDs.
/// </summary>
/// <param name="accountIds">The list of account IDs for which to clear the StellarMembership.</param>
/// <returns>The number of accounts updated.</returns>
Task<int> ClearStellarMembershipsAsync(List<Guid> accountIds);
/// <summary>
/// Updates the profile picture of an account.
/// </summary>
/// <param name="accountId">The ID of the account.</param>
/// <param name="picture">The new profile picture reference object.</param>
/// <returns>The updated profile.</returns>
Task<Profile> UpdateProfilePictureAsync(Guid accountId, CloudFileReferenceObject? picture);
/// <summary>
/// Updates the profile background of an account.
/// </summary>
/// <param name="accountId">The ID of the account.</param>
/// <param name="background">The new profile background reference object.</param>
/// <returns>The updated profile.</returns>
Task<Profile> UpdateProfileBackgroundAsync(Guid accountId, CloudFileReferenceObject? background);
}
}

View File

@@ -1,5 +1,8 @@
using DysonNetwork.Shared.Models;
using MagicOnion;
using System.Collections.Generic;
using System.Threading.Tasks;
using System;
namespace DysonNetwork.Shared.Services;
@@ -59,4 +62,247 @@ public interface IAccountService : IService<IAccountService>
/// <param name="userInfo">The OpenID Connect user information</param>
/// <returns>The newly created account</returns>
Task<Account> CreateAccount(OidcUserInfo userInfo);
}
/// <summary>
/// Gets an account by its ID.
/// </summary>
/// <param name="accountId">The ID of the account.</param>
/// <param name="withProfile">Join the profile table or not.</param>
/// <returns>The account if found, otherwise null.</returns>
Task<Account?> GetAccountById(Guid accountId, bool withProfile = false);
/// <summary>
/// Gets an account profile by account ID.
/// </summary>
/// <param name="accountId">The ID of the account.</param>
/// <returns>The account profile if found, otherwise null.</returns>
Task<Profile?> GetAccountProfile(Guid accountId);
/// <summary>
/// Gets an authentication challenge by its ID.
/// </summary>
/// <param name="challengeId">The ID of the challenge.</param>
/// <returns>The authentication challenge if found, otherwise null.</returns>
Task<Challenge?> GetAuthChallenge(Guid challengeId);
/// <summary>
/// Gets an authentication challenge by account ID, IP address, and user agent.
/// </summary>
/// <param name="accountId">The ID of the account.</param>
/// <param name="ipAddress">The IP address.</param>
/// <param name="userAgent">The user agent.</param>
/// <param name="now">The current instant.</param>
/// <returns>The authentication challenge if found, otherwise null.</returns>
Task<Challenge?> GetAuthChallenge(Guid accountId, string? ipAddress, string? userAgent, NodaTime.Instant now);
/// <summary>
/// Creates a new authentication challenge.
/// </summary>
/// <param name="challenge">The challenge to create.</param>
/// <returns>The created challenge.</returns>
Task<Challenge> CreateAuthChallenge(Challenge challenge);
/// <summary>
/// Gets an account authentication factor by its ID and account ID.
/// </summary>
/// <param name="factorId">The ID of the factor.</param>
/// <param name="accountId">The ID of the account.</param>
/// <returns>The account authentication factor if found, otherwise null.</returns>
Task<AccountAuthFactor?> GetAccountAuthFactor(Guid factorId, Guid accountId);
/// <summary>
/// Gets a list of account authentication factors for a given account ID.
/// </summary>
/// <param name="accountId">The ID of the account.</param>
/// <returns>A list of account authentication factors.</returns>
Task<List<AccountAuthFactor>> GetAccountAuthFactors(Guid accountId);
/// <summary>
/// Gets an authentication session by its ID.
/// </summary>
/// <param name="sessionId">The ID of the session.</param>
/// <returns>The authentication session if found, otherwise null.</returns>
Task<Session?> GetAuthSession(Guid sessionId);
/// <summary>
/// Gets a magic spell by its ID.
/// </summary>
/// <param name="spellId">The ID of the magic spell.</param>
/// <returns>The magic spell if found, otherwise null.</returns>
Task<MagicSpell?> GetMagicSpell(Guid spellId);
/// <summary>
/// Gets an abuse report by its ID.
/// </summary>
/// <param name="reportId">The ID of the abuse report.</param>
/// <returns>The abuse report if found, otherwise null.</returns>
Task<AbuseReport?> GetAbuseReport(Guid reportId);
/// <summary>
/// Creates a new abuse report.
/// </summary>
/// <param name="resourceIdentifier">The identifier of the resource being reported.</param>
/// <param name="type">The type of abuse report.</param>
/// <param name="reason">The reason for the report.</param>
/// <param name="accountId">The ID of the account making the report.</param>
/// <returns>The created abuse report.</returns>
Task<AbuseReport> CreateAbuseReport(string resourceIdentifier, AbuseReportType type, string reason, Guid accountId);
/// <summary>
/// Counts abuse reports.
/// </summary>
/// <param name="includeResolved">Whether to include resolved reports.</param>
/// <returns>The count of abuse reports.</returns>
Task<int> CountAbuseReports(bool includeResolved = false);
/// <summary>
/// Counts abuse reports by a specific user.
/// </summary>
/// <param name="accountId">The ID of the account.</param>
/// <param name="includeResolved">Whether to include resolved reports.</param>
/// <returns>The count of abuse reports by the user.</returns>
Task<int> CountUserAbuseReports(Guid accountId, bool includeResolved = false);
/// <summary>
/// Gets a list of abuse reports.
/// </summary>
/// <param name="skip">Number of reports to skip.</param>
/// <param name="take">Number of reports to take.</param>
/// <param name="includeResolved">Whether to include resolved reports.</param>
/// <returns>A list of abuse reports.</returns>
Task<List<AbuseReport>> GetAbuseReports(int skip = 0, int take = 20, bool includeResolved = false);
/// <summary>
/// Gets a list of abuse reports by a specific user.
/// </summary>
/// <param name="accountId">The ID of the account.</param>
/// <param name="skip">Number of reports to skip.</param>
/// <param name="take">Number of reports to take.</param>
/// <param name="includeResolved">Whether to include resolved reports.</param>
/// <returns>A list of abuse reports by the user.</returns>
Task<List<AbuseReport>> GetUserAbuseReports(Guid accountId, int skip = 0, int take = 20, bool includeResolved = false);
/// <summary>
/// Resolves an abuse report.
/// </summary>
/// <param name="id">The ID of the report to resolve.</param>
/// <param name="resolution">The resolution message.</param>
/// <returns>The resolved abuse report.</returns>
Task<AbuseReport> ResolveAbuseReport(Guid id, string resolution);
/// <summary>
/// Gets the count of pending abuse reports.
/// </summary>
/// <returns>The count of pending abuse reports.</returns>
Task<int> GetPendingAbuseReportsCount();
/// <summary>
/// Checks if a relationship with a specific status exists between two accounts.
/// </summary>
/// <param name="accountId1">The ID of the first account.</param>
/// <param name="accountId2">The ID of the second account.</param>
/// <param name="status">The relationship status to check for.</param>
/// <returns>True if the relationship exists, otherwise false.</returns>
Task<bool> HasRelationshipWithStatus(Guid accountId1, Guid accountId2, RelationshipStatus status);
/// <summary>
/// Gets the statuses for a list of account IDs.
/// </summary>
/// <param name="accountIds">A list of account IDs.</param>
/// <returns>A dictionary where the key is the account ID and the value is the status.</returns>
Task<Dictionary<Guid, Status>> GetStatuses(List<Guid> accountIds);
/// <summary>
/// Sends a notification to an account.
/// </summary>
/// <param name="account">The target account.</param>
/// <param name="topic">The notification topic.</param>
/// <param name="title">The notification title.</param>
/// <param name="subtitle">The notification subtitle.</param>
/// <param name="body">The notification body.</param>
/// <param name="actionUri">The action URI for the notification.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task SendNotification(Account account, string topic, string title, string? subtitle, string body, string? actionUri = null);
/// <summary>
/// Lists the friends of an account.
/// </summary>
/// <param name="account">The account.</param>
/// <returns>A list of friend accounts.</returns>
Task<List<Account>> ListAccountFriends(Account account);
/// <summary>
/// Verifies an authentication factor code.
/// </summary>
/// <param name="factor">The authentication factor.</param>
/// <param name="code">The code to verify.</param>
/// <returns>True if the code is valid, otherwise false.</returns>
Task<bool> VerifyFactorCode(AccountAuthFactor factor, string code);
/// <summary>
/// Send the auth factor verification code to users, for factors like in-app code and email.
/// Sometimes it requires a hint, like a part of the user's email address to ensure the user is who own the account.
/// </summary>
/// <param name="account">The owner of the auth factor</param>
/// <param name="factor">The auth factor needed to send code</param>
/// <param name="hint">The part of the contact method for verification</param>
Task SendFactorCode(Account account, AccountAuthFactor factor, string? hint = null);
/// <summary>
/// Creates an action log entry.
/// </summary>
/// <param name="type">The type of action log.</param>
/// <param name="meta">Additional metadata for the action log.</param>
/// <param name="request">The HTTP request.</param>
/// <param name="account">The account associated with the action.</param>
/// <returns>The created action log.</returns>
Task<ActionLog> CreateActionLogFromRequest(string type, Dictionary<string, object> meta, string? ipAddress, string? userAgent, Account? account = null);
/// <summary>
/// Creates a new session.
/// </summary>
/// <param name="lastGrantedAt">The last granted instant.</param>
/// <param name="expiredAt">The expiration instant.</param>
/// <param name="account">The associated account.</param>
/// <param name="challenge">The associated challenge.</param>
/// <returns>The created session.</returns>
Task<Session> CreateSession(NodaTime.Instant lastGrantedAt, NodaTime.Instant expiredAt, Account account, Challenge challenge);
/// <summary>
/// Updates the LastGrantedAt for a session.
/// </summary>
/// <param name="sessionId">The ID of the session.</param>
/// <param name="lastGrantedAt">The new LastGrantedAt instant.</param>
Task UpdateSessionLastGrantedAt(Guid sessionId, NodaTime.Instant lastGrantedAt);
/// <summary>
/// Updates the LastSeenAt for an account profile.
/// </summary>
/// <param name="accountId">The ID of the account.</param>
/// <param name="lastSeenAt">The new LastSeenAt instant.</param>
Task UpdateAccountProfileLastSeenAt(Guid accountId, NodaTime.Instant lastSeenAt);
/// <summary>
/// Creates a token for a session.
/// </summary>
/// <param name="session">The session.</param>
/// <returns>The token string.</returns>
string CreateToken(Session session);
/// <summary>
/// Gets the AuthConstants.CookieTokenName.
/// </summary>
/// <returns>The cookie token name.</returns>
string GetAuthCookieTokenName();
/// <summary>
/// Searches for accounts by a search term.
/// </summary>
/// <param name="searchTerm">The term to search for.</param>
/// <returns>A list of matching accounts.</returns>
Task<List<Account>> SearchAccountsAsync(string searchTerm);
}

View File

@@ -1,7 +1,6 @@
using System.Collections.Generic;
using DysonNetwork.Shared.Models;
using MagicOnion;
using Microsoft.AspNetCore.Http;
namespace DysonNetwork.Shared.Services;
@@ -15,10 +14,11 @@ public interface IActionLogService : IService<IActionLogService>
/// <summary>
/// Creates an action log entry from an HTTP request
/// </summary>
void CreateActionLogFromRequest(
string action,
Task<ActionLog> CreateActionLogFromRequest(
string type,
Dictionary<string, object> meta,
HttpRequest request,
string? ipAddress,
string? userAgent,
Account? account = null
);
}
}

View File

@@ -0,0 +1,13 @@
using MagicOnion;
using DysonNetwork.Shared.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace DysonNetwork.Shared.Services;
public interface ICustomAppService : IService<ICustomAppService>
{
Task<CustomApp?> FindClientByIdAsync(Guid clientId);
Task<int> CountCustomAppsByPublisherId(Guid publisherId);
}

View File

@@ -22,7 +22,21 @@ public interface IMagicSpellService : IService<IMagicSpellService>
/// Gets a magic spell by its token
/// </summary>
Task<MagicSpell?> GetMagicSpellAsync(string token);
/// <summary>
/// Gets a magic spell by its ID.
/// </summary>
/// <param name="spellId">The ID of the magic spell.</param>
/// <returns>The magic spell if found, otherwise null.</returns>
Task<MagicSpell?> GetMagicSpellByIdAsync(Guid spellId);
/// <summary>
/// Applies a password reset magic spell.
/// </summary>
/// <param name="spell">The magic spell object.</param>
/// <param name="newPassword">The new password.</param>
Task ApplyPasswordReset(MagicSpell spell, string newPassword);
/// <summary>
/// Consumes a magic spell
/// </summary>

View File

@@ -1,5 +1,7 @@
using DysonNetwork.Shared.Models;
using MagicOnion;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace DysonNetwork.Shared.Services;
@@ -20,4 +22,25 @@ public interface INotificationService : IService<INotificationService>
string deviceId,
string deviceToken
);
Task<Notification> SendNotification(
Account account,
string topic,
string? title = null,
string? subtitle = null,
string? content = null,
Dictionary<string, object>? meta = null,
string? actionUri = null,
bool isSilent = false,
bool save = true
);
Task DeliveryNotification(Notification notification);
Task MarkNotificationsViewed(ICollection<Notification> notifications);
Task BroadcastNotification(Notification notification, bool save = false);
Task SendNotificationBatch(Notification notification, List<Account> accounts,
bool save = false);
}

View File

@@ -0,0 +1,15 @@
using MagicOnion;
using DysonNetwork.Shared.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace DysonNetwork.Shared.Services;
public interface IPublisherService : IService<IPublisherService>
{
Task<Publisher?> GetPublisherByName(string name);
Task<List<Publisher>> GetUserPublishers(Guid accountId);
Task<bool> IsMemberWithRole(Guid publisherId, Guid accountId, PublisherMemberRole role);
Task<List<PublisherFeature>> GetPublisherFeatures(Guid publisherId);
}

View File

@@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using DysonNetwork.Shared.Models;
using MagicOnion;
@@ -9,7 +12,7 @@ public interface IRelationshipService : IService<IRelationshipService>
/// Checks if a relationship exists between two accounts
/// </summary>
Task<bool> HasExistingRelationship(Guid accountId, Guid relatedId);
/// <summary>
/// Gets a relationship between two accounts
/// </summary>
@@ -19,9 +22,58 @@ public interface IRelationshipService : IService<IRelationshipService>
RelationshipStatus? status = null,
bool ignoreExpired = false
);
/// <summary>
/// Creates a new relationship between two accounts
/// </summary>
Task<Relationship> CreateRelationship(Account sender, Account target, RelationshipStatus status);
/// <summary>
/// Blocks a user
/// </summary>
Task<Relationship> BlockAccount(Account sender, Account target);
/// <summary>
/// Unblocks a user
/// </summary>
Task<Relationship> UnblockAccount(Account sender, Account target);
/// <summary>
/// Sends a friend request to a user
/// </summary>
Task<Relationship> SendFriendRequest(Account sender, Account target);
/// <summary>
/// Deletes a friend request
/// </summary>
Task DeleteFriendRequest(Guid accountId, Guid relatedId);
/// <summary>
/// Accepts a friend request
/// </summary>
Task<Relationship> AcceptFriendRelationship(
Relationship relationship,
RelationshipStatus status = RelationshipStatus.Friends
);
/// <summary>
/// Updates a relationship between two users
/// </summary>
Task<Relationship> UpdateRelationship(Guid accountId, Guid relatedId, RelationshipStatus status);
/// <summary>
/// Lists all friends of an account
/// </summary>
Task<List<Account>> ListAccountFriends(Account account);
/// <summary>
/// Lists all blocked users of an account
/// </summary>
Task<List<Guid>> ListAccountBlocked(Account account);
/// <summary>
/// Checks if a relationship with a specific status exists between two accounts
/// </summary>
Task<bool> HasRelationshipWithStatus(Guid accountId, Guid relatedId,
RelationshipStatus status = RelationshipStatus.Friends);
}