diff --git a/DysonNetwork.Sphere/Account/AccountController.cs b/DysonNetwork.Sphere/Account/AccountController.cs index 7d80774..3c3b280 100644 --- a/DysonNetwork.Sphere/Account/AccountController.cs +++ b/DysonNetwork.Sphere/Account/AccountController.cs @@ -224,6 +224,44 @@ public class AccountController( return Ok(status); } + [HttpGet("{name}/statuses")] + public async Task> GetOtherStatus(string name) + { + var account = await db.Accounts.FirstOrDefaultAsync(a => a.Name == name); + if (account is null) return BadRequest(); + var status = await events.GetStatus(account.Id); + status.IsInvisible = false; // Keep the invisible field not available for other users + return Ok(status); + } + + [HttpPatch("me/statuses")] + [Authorize] + [RequiredPermission("global", "accounts.statuses.update")] + public async Task> UpdateStatus([FromBody] StatusRequest request) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + + var now = SystemClock.Instance.GetCurrentInstant(); + var status = await db.AccountStatuses + .Where(e => e.AccountId == currentUser.Id) + .Where(e => e.ClearedAt == null || e.ClearedAt > now) + .OrderByDescending(e => e.CreatedAt) + .FirstOrDefaultAsync(); + if (status is null) return NotFound(); + + status.Attitude = request.Attitude; + status.IsInvisible = request.IsInvisible; + status.IsNotDisturb = request.IsNotDisturb; + status.Label = request.Label; + status.ClearedAt = request.ClearedAt; + + db.Update(status); + await db.SaveChangesAsync(); + events.PurgeStatusCache(currentUser.Id); + + return status; + } + [HttpPost("me/statuses")] [Authorize] [RequiredPermission("global", "accounts.statuses.create")] @@ -305,7 +343,8 @@ public class AccountController( [HttpGet("me/calendar")] [Authorize] - public async Task>> GetEventCalendar([FromQuery] int? month, [FromQuery] int? year) + public async Task>> GetEventCalendar([FromQuery] int? month, + [FromQuery] int? year) { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); @@ -320,6 +359,27 @@ public class AccountController( return Ok(calendar); } + [HttpGet("{name}/calendar")] + public async Task>> GetOtherEventCalendar( + string name, + [FromQuery] int? month, + [FromQuery] int? year + ) + { + var currentDate = SystemClock.Instance.GetCurrentInstant().InUtc().Date; + month ??= currentDate.Month; + year ??= currentDate.Year; + + if (month is < 1 or > 12) return BadRequest("Invalid month."); + if (year < 1) return BadRequest("Invalid year."); + + var account = await db.Accounts.FirstOrDefaultAsync(a => a.Name == name); + if (account is null) return BadRequest(); + + var calendar = await events.GetEventCalendar(account, month.Value, year.Value, replaceInvisible: true); + return Ok(calendar); + } + [HttpGet("search")] public async Task> Search([FromQuery] string query, [FromQuery] int take = 20) { diff --git a/DysonNetwork.Sphere/Account/AccountEventService.cs b/DysonNetwork.Sphere/Account/AccountEventService.cs index 3cccaad..6629d2e 100644 --- a/DysonNetwork.Sphere/Account/AccountEventService.cs +++ b/DysonNetwork.Sphere/Account/AccountEventService.cs @@ -1,12 +1,10 @@ using System.Globalization; using DysonNetwork.Sphere.Activity; using DysonNetwork.Sphere.Connection; -using DysonNetwork.Sphere.Resources; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Localization; using NodaTime; -using NodaTime; namespace DysonNetwork.Sphere.Account; @@ -21,11 +19,20 @@ public class AccountEventService( private static readonly Random Random = new(); private const string StatusCacheKey = "account_status_"; + public void PurgeStatusCache(long userId) + { + var cacheKey = $"{StatusCacheKey}{userId}"; + cache.Remove(cacheKey); + } + public async Task GetStatus(long userId) { var cacheKey = $"{StatusCacheKey}{userId}"; if (cache.TryGetValue(cacheKey, out Status? cachedStatus)) - return cachedStatus!; + { + cachedStatus!.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId); + return cachedStatus; + } var now = SystemClock.Instance.GetCurrentInstant(); var status = await db.AccountStatuses @@ -33,19 +40,21 @@ public class AccountEventService( .Where(e => e.ClearedAt == null || e.ClearedAt > now) .OrderByDescending(e => e.CreatedAt) .FirstOrDefaultAsync(); + var isOnline = ws.GetAccountIsConnected(userId); if (status is not null) { + status.IsOnline = !status.IsInvisible && isOnline; cache.Set(cacheKey, status, TimeSpan.FromMinutes(5)); return status; } - var isOnline = ws.GetAccountIsConnected(userId); if (isOnline) { return new Status { Attitude = StatusAttitude.Neutral, IsOnline = true, + IsCustomized = false, Label = "Online", AccountId = userId, }; @@ -55,6 +64,7 @@ public class AccountEventService( { Attitude = StatusAttitude.Neutral, IsOnline = false, + IsCustomized = false, Label = "Offline", AccountId = userId, }; @@ -85,6 +95,7 @@ public class AccountEventService( status.ClearedAt = SystemClock.Instance.GetCurrentInstant(); db.Update(status); await db.SaveChangesAsync(); + PurgeStatusCache(user.Id); } private const int FortuneTipCount = 7; // This will be the max index for each type (positive/negative) @@ -123,8 +134,8 @@ public class AccountEventService( { var cultureInfo = new CultureInfo(user.Language, false); CultureInfo.CurrentCulture = cultureInfo; - CultureInfo.CurrentUICulture = cultureInfo; - + CultureInfo.CurrentUICulture = cultureInfo; + // Generate 2 positive tips var positiveIndices = Enumerable.Range(1, FortuneTipCount) .OrderBy(_ => Random.Next()) @@ -168,44 +179,60 @@ public class AccountEventService( return result; } -public async Task> GetEventCalendar(Account user, int month, int year = 0) -{ - if (year == 0) - year = SystemClock.Instance.GetCurrentInstant().InUtc().Date.Year; - - // Create start and end dates for the specified month - var startOfMonth = new LocalDate(year, month, 1).AtStartOfDayInZone(DateTimeZone.Utc).ToInstant(); - var endOfMonth = startOfMonth.Plus(Duration.FromDays(DateTime.DaysInMonth(year, month))); - - var statuses = await db.AccountStatuses - .Where(x => x.AccountId == user.Id && x.CreatedAt >= startOfMonth && x.CreatedAt < endOfMonth) - .OrderBy(x => x.CreatedAt) - .ToListAsync(); - - var checkIn = await db.AccountCheckInResults - .Where(x => x.AccountId == user.Id && x.CreatedAt >= startOfMonth && x.CreatedAt < endOfMonth) - .ToListAsync(); - - var dates = Enumerable.Range(1, DateTime.DaysInMonth(year, month)) - .Select(day => new LocalDate(year, month, day).AtStartOfDayInZone(DateTimeZone.Utc).ToInstant()) - .ToList(); - - var statusesByDate = statuses - .GroupBy(s => s.CreatedAt.InUtc().Date) - .ToDictionary(g => g.Key, g => g.ToList()); - - var checkInByDate = checkIn - .ToDictionary(c => c.CreatedAt.InUtc().Date); - - return dates.Select(date => + public async Task> GetEventCalendar(Account user, int month, int year = 0, + bool replaceInvisible = false) { - var utcDate = date.InUtc().Date; - return new DailyEventResponse + if (year == 0) + year = SystemClock.Instance.GetCurrentInstant().InUtc().Date.Year; + + // Create start and end dates for the specified month + var startOfMonth = new LocalDate(year, month, 1).AtStartOfDayInZone(DateTimeZone.Utc).ToInstant(); + var endOfMonth = startOfMonth.Plus(Duration.FromDays(DateTime.DaysInMonth(year, month))); + + var statuses = await db.AccountStatuses + .AsNoTracking() + .TagWith("GetEventCalendar_Statuses") + .Where(x => x.AccountId == user.Id && x.CreatedAt >= startOfMonth && x.CreatedAt < endOfMonth) + .Select(x => new Status + { + Id = x.Id, + Attitude = x.Attitude, + IsInvisible = !replaceInvisible && x.IsInvisible, + IsNotDisturb = x.IsNotDisturb, + Label = x.Label, + ClearedAt = x.ClearedAt, + AccountId = x.AccountId, + CreatedAt = x.CreatedAt + }) + .OrderBy(x => x.CreatedAt) + .ToListAsync(); + + var checkIn = await db.AccountCheckInResults + .AsNoTracking() + .TagWith("GetEventCalendar_CheckIn") + .Where(x => x.AccountId == user.Id && x.CreatedAt >= startOfMonth && x.CreatedAt < endOfMonth) + .ToListAsync(); + + var dates = Enumerable.Range(1, DateTime.DaysInMonth(year, month)) + .Select(day => new LocalDate(year, month, day).AtStartOfDayInZone(DateTimeZone.Utc).ToInstant()) + .ToList(); + + var statusesByDate = statuses + .GroupBy(s => s.CreatedAt.InUtc().Date) + .ToDictionary(g => g.Key, g => g.ToList()); + + var checkInByDate = checkIn + .ToDictionary(c => c.CreatedAt.InUtc().Date); + + return dates.Select(date => { - Date = date, - CheckInResult = checkInByDate.GetValueOrDefault(utcDate), - Statuses = statusesByDate.GetValueOrDefault(utcDate, new List()) - }; - }).ToList(); -} + var utcDate = date.InUtc().Date; + return new DailyEventResponse + { + Date = date, + CheckInResult = checkInByDate.GetValueOrDefault(utcDate), + Statuses = statusesByDate.GetValueOrDefault(utcDate, new List()) + }; + }).ToList(); + } } \ No newline at end of file diff --git a/DysonNetwork.Sphere/Account/Event.cs b/DysonNetwork.Sphere/Account/Event.cs index 9031a59..b7aa935 100644 --- a/DysonNetwork.Sphere/Account/Event.cs +++ b/DysonNetwork.Sphere/Account/Event.cs @@ -16,6 +16,7 @@ public class Status : ModelBase public Guid Id { get; set; } = Guid.NewGuid(); public StatusAttitude Attitude { get; set; } [NotMapped] public bool IsOnline { get; set; } + [NotMapped] public bool IsCustomized { get; set; } = true; public bool IsInvisible { get; set; } public bool IsNotDisturb { get; set; } [MaxLength(1024)] public string? Label { get; set; } diff --git a/DysonNetwork.Sphere/AppDatabase.cs b/DysonNetwork.Sphere/AppDatabase.cs index 7abf022..dec9d3f 100644 --- a/DysonNetwork.Sphere/AppDatabase.cs +++ b/DysonNetwork.Sphere/AppDatabase.cs @@ -90,7 +90,8 @@ public class AppDatabase( PermissionService.NewPermissionNode("group:default", "global", "chat.create", true), PermissionService.NewPermissionNode("group:default", "global", "chat.messages.create", true), PermissionService.NewPermissionNode("group:default", "global", "chat.realtime.create", true), - PermissionService.NewPermissionNode("group:default", "global", "accounts.statuses.create", true) + PermissionService.NewPermissionNode("group:default", "global", "accounts.statuses.create", true), + PermissionService.NewPermissionNode("group:default", "global", "accounts.statuses.update", true) } }); await context.SaveChangesAsync(cancellationToken);