using MailApi.Models; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using MongoDB.Driver; namespace MailApi.Services; public class MailStore { private readonly IMongoCollection _messages; private readonly IMongoCollection _characters; public MailStore(IConfiguration cfg) { var cs = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017"; var dbName = cfg["MongoDB:DatabaseName"] ?? "promiscuity"; var client = new MongoClient(cs); var db = client.GetDatabase(dbName); _messages = db.GetCollection("MailMessages"); _characters = db.GetCollection("Characters"); _messages.Indexes.CreateOne(new CreateIndexModel( Builders.IndexKeys.Ascending(m => m.RecipientCharacterId).Descending(m => m.CreatedUtc))); _messages.Indexes.CreateOne(new CreateIndexModel( Builders.IndexKeys.Ascending(m => m.SenderCharacterId).Descending(m => m.CreatedUtc))); } public async Task ResolveCharacterAsync(string characterId, string userId, bool allowAnyOwner) { var character = await _characters.Find(c => c.Id == characterId).FirstOrDefaultAsync(); if (character is null) return new CharacterAccessResult { Exists = false }; return new CharacterAccessResult { Exists = true, IsAuthorized = allowAnyOwner || character.OwnerUserId == userId, CharacterId = character.Id ?? string.Empty, CharacterName = character.Name, OwnerUserId = character.OwnerUserId }; } public async Task> GetInboxAsync(string characterId) => await _messages.Find(m => m.RecipientCharacterId == characterId) .SortByDescending(m => m.CreatedUtc) .ToListAsync(); public async Task> GetSentAsync(string characterId) => await _messages.Find(m => m.SenderCharacterId == characterId) .SortByDescending(m => m.CreatedUtc) .ToListAsync(); public async Task SendAsync(string senderCharacterId, string recipientCharacterName, string subject, string body) { var sender = await _characters.Find(c => c.Id == senderCharacterId).FirstOrDefaultAsync(); if (sender is null) return new SendMailResult { Status = SendMailStatus.SenderNotFound }; var normalizedRecipientName = recipientCharacterName.Trim(); var recipients = await _characters.Find(c => c.Name == normalizedRecipientName).ToListAsync(); if (recipients.Count == 0) return new SendMailResult { Status = SendMailStatus.RecipientNotFound }; if (recipients.Count > 1) return new SendMailResult { Status = SendMailStatus.RecipientAmbiguous }; var recipient = recipients[0]; if (recipient.Id == sender.Id) return new SendMailResult { Status = SendMailStatus.Invalid }; var message = new MailMessage { SenderCharacterId = sender.Id ?? string.Empty, SenderCharacterName = sender.Name, RecipientCharacterId = recipient.Id ?? string.Empty, RecipientCharacterName = recipient.Name, Subject = subject.Trim(), Body = body.Trim(), CreatedUtc = DateTime.UtcNow }; await _messages.InsertOneAsync(message); return new SendMailResult { Status = SendMailStatus.Ok, Message = message }; } public async Task MarkReadAsync(string characterId, string messageId) { var filter = Builders.Filter.And( Builders.Filter.Eq(m => m.Id, messageId), Builders.Filter.Eq(m => m.RecipientCharacterId, characterId) ); var update = Builders.Update.Set(m => m.ReadUtc, DateTime.UtcNow); var options = new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }; return await _messages.FindOneAndUpdateAsync(filter, update, options); } public class CharacterAccessResult { public bool Exists { get; set; } public bool IsAuthorized { get; set; } public string CharacterId { get; set; } = string.Empty; public string CharacterName { get; set; } = string.Empty; public string OwnerUserId { get; set; } = string.Empty; } public class SendMailResult { public SendMailStatus Status { get; set; } public MailMessage? Message { get; set; } } public enum SendMailStatus { Ok, SenderNotFound, RecipientNotFound, RecipientAmbiguous, Invalid } [BsonIgnoreExtraElements] private class CharacterDocument { [BsonId] [BsonRepresentation(BsonType.ObjectId)] public string? Id { get; set; } public string OwnerUserId { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; } }