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; public class AccountEventService( AppDatabase db, ActivityService act, WebSocketService ws, IMemoryCache cache, IStringLocalizer localizer ) { private static readonly Random Random = new(); private const string StatusCacheKey = "account_status_"; public async Task GetStatus(long userId) { var cacheKey = $"{StatusCacheKey}{userId}"; if (cache.TryGetValue(cacheKey, out Status? cachedStatus)) return cachedStatus!; var now = SystemClock.Instance.GetCurrentInstant(); var status = await db.AccountStatuses .Where(e => e.AccountId == userId) .Where(e => e.ClearedAt == null || e.ClearedAt > now) .OrderByDescending(e => e.CreatedAt) .FirstOrDefaultAsync(); if (status is not null) { cache.Set(cacheKey, status, TimeSpan.FromMinutes(5)); return status; } var isOnline = ws.GetAccountIsConnected(userId); if (isOnline) { return new Status { Attitude = StatusAttitude.Neutral, IsOnline = true, Label = "Online", AccountId = userId, }; } return new Status { Attitude = StatusAttitude.Neutral, IsOnline = false, Label = "Offline", AccountId = userId, }; } public async Task CreateStatus(Account user, Status status) { var now = SystemClock.Instance.GetCurrentInstant(); await db.AccountStatuses .Where(x => x.AccountId == user.Id && (x.ClearedAt == null || x.ClearedAt > now)) .ExecuteUpdateAsync(s => s.SetProperty(x => x.ClearedAt, now)); db.AccountStatuses.Add(status); await db.SaveChangesAsync(); await act.CreateActivity( user, "accounts.status", $"account.statuses/{status.Id}", ActivityVisibility.Friends ); return status; } public async Task ClearStatus(Account user, Status status) { status.ClearedAt = SystemClock.Instance.GetCurrentInstant(); db.Update(status); await db.SaveChangesAsync(); } private const int FortuneTipCount = 7; // This will be the max index for each type (positive/negative) private const string CaptchaCacheKey = "checkin_captcha_"; private const int CaptchaProbabilityPercent = 20; public bool CheckInDailyDoAskCaptcha(Account user) { var cacheKey = $"{CaptchaCacheKey}{user.Id}"; if (cache.TryGetValue(cacheKey, out bool? needsCaptcha)) return needsCaptcha!.Value; var result = Random.Next(100) < CaptchaProbabilityPercent; cache.Set(cacheKey, result, TimeSpan.FromHours(24)); return result; } public async Task CheckInDailyIsAvailable(Account user) { var now = SystemClock.Instance.GetCurrentInstant(); var lastCheckIn = await db.AccountCheckInResults .Where(x => x.AccountId == user.Id) .OrderByDescending(x => x.CreatedAt) .FirstOrDefaultAsync(); if (lastCheckIn == null) return true; var lastDate = lastCheckIn.CreatedAt.InUtc().Date; var currentDate = now.InUtc().Date; return lastDate < currentDate; } public async Task CheckInDaily(Account user) { var cultureInfo = new CultureInfo(user.Language, false); CultureInfo.CurrentCulture = cultureInfo; CultureInfo.CurrentUICulture = cultureInfo; // Generate 2 positive tips var positiveIndices = Enumerable.Range(1, FortuneTipCount) .OrderBy(_ => Random.Next()) .Take(2) .ToList(); var tips = positiveIndices.Select(index => new FortuneTip { IsPositive = true, Title = localizer[$"FortuneTipPositiveTitle_{index}"].Value, Content = localizer[$"FortuneTipPositiveContent_{index}"].Value }).ToList(); // Generate 2 negative tips var negativeIndices = Enumerable.Range(1, FortuneTipCount) .Except(positiveIndices) .OrderBy(_ => Random.Next()) .Take(2) .ToList(); tips.AddRange(negativeIndices.Select(index => new FortuneTip { IsPositive = false, Title = localizer[$"FortuneTipNegativeTitle_{index}"].Value, Content = localizer[$"FortuneTipNegativeContent_{index}"].Value })); var result = new CheckInResult { Tips = tips, Level = (CheckInResultLevel)Random.Next(Enum.GetValues().Length), AccountId = user.Id }; db.AccountCheckInResults.Add(result); await db.SaveChangesAsync(); await act.CreateActivity( user, "accounts.check-in", $"account.check-in/{result.Id}", ActivityVisibility.Friends ); 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 => { var utcDate = date.InUtc().Date; return new DailyEventResponse { Date = date, CheckInResult = checkInByDate.GetValueOrDefault(utcDate), Statuses = statusesByDate.GetValueOrDefault(utcDate, new List()) }; }).ToList(); } }