Initial implementation of recent activity

DB migrations intentionally left out since they aren't finalized
This commit is contained in:
Slendy 2023-07-20 19:38:37 -05:00
commit 23cb1bef1c
No known key found for this signature in database
GPG key ID: 7288D68361B91428
86 changed files with 1542 additions and 93 deletions

View file

@ -0,0 +1,286 @@
using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.StorableLists.Stores;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Serialization.Activity;
using LBPUnion.ProjectLighthouse.Types.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers;
[ApiController]
[Authorize]
[Route("LITTLEBIGPLANETPS3_XML/stream")]
[Produces("text/xml")]
public class ActivityController : ControllerBase
{
private readonly DatabaseContext database;
public ActivityController(DatabaseContext database)
{
this.database = database;
}
public class ActivityDto
{
public required ActivityEntity Activity { get; set; }
public int? TargetSlotId { get; set; }
public int? TargetUserId { get; set; }
public int? TargetPlaylistId { get; set; }
public int? SlotCreatorId { get; set; }
}
//TODO refactor this mess into a separate db file or something
private static Expression<Func<ActivityEntity, ActivityDto>> ActivityToDto()
{
return a => new ActivityDto
{
Activity = a,
TargetSlotId = a is LevelActivityEntity
? ((LevelActivityEntity)a).SlotId
: a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.PhotoId != 0
? ((PhotoActivityEntity)a).Photo.SlotId
: a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Level
? ((CommentActivityEntity)a).Comment.TargetId
: a is ScoreActivityEntity
? ((ScoreActivityEntity)a).Score.SlotId
: 0,
TargetUserId = a is UserActivityEntity
? ((UserActivityEntity)a).TargetUserId
: a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Profile
? ((CommentActivityEntity)a).Comment.TargetId
: a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0
? ((PhotoActivityEntity)a).Photo.CreatorId
: 0,
TargetPlaylistId = a is PlaylistActivityEntity ? ((PlaylistActivityEntity)a).PlaylistId : 0,
};
}
private static IQueryable<IGrouping<ActivityGroup, ActivityEntity>> GroupActivities
(IQueryable<ActivityEntity> activityQuery)
{
return activityQuery.Select(ActivityToDto())
.GroupBy(dto => new ActivityGroup
{
Timestamp = dto.Activity.Timestamp.Date,
UserId = dto.Activity.UserId,
TargetUserId = dto.TargetUserId,
TargetSlotId = dto.TargetSlotId,
TargetPlaylistId = dto.TargetPlaylistId,
},
dto => dto.Activity);
}
private static IQueryable<IGrouping<ActivityGroup, ActivityEntity>> GroupActivities
(IQueryable<ActivityDto> activityQuery)
{
return activityQuery.GroupBy(dto => new ActivityGroup
{
Timestamp = dto.Activity.Timestamp.Date,
UserId = dto.Activity.UserId,
TargetUserId = dto.TargetUserId,
TargetSlotId = dto.TargetSlotId,
TargetPlaylistId = dto.TargetPlaylistId,
},
dto => dto.Activity);
}
// TODO this is kinda ass, can maybe improve once comment migration is merged
private async Task<IQueryable<ActivityEntity>> GetFilters
(
GameTokenEntity token,
bool excludeNews,
bool excludeMyLevels,
bool excludeFriends,
bool excludeFavouriteUsers,
bool excludeMyself
)
{
IQueryable<ActivityEntity> query = this.database.Activities.AsQueryable();
if (excludeNews) query = query.Where(a => a.Type != EventType.NewsPost);
IQueryable<ActivityDto> dtoQuery = query.Select(a => new ActivityDto
{
Activity = a,
SlotCreatorId = a is LevelActivityEntity
? ((LevelActivityEntity)a).Slot.CreatorId
: a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0
? ((PhotoActivityEntity)a).Photo.Slot!.CreatorId
: a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Level
? ((CommentActivityEntity)a).Comment.TargetId
: a is ScoreActivityEntity
? ((ScoreActivityEntity)a).Score.Slot.CreatorId
: 0,
});
Expression<Func<ActivityDto, bool>> predicate = PredicateExtensions.False<ActivityDto>();
predicate = predicate.Or(a => a.SlotCreatorId == 0 || excludeMyLevels
? a.SlotCreatorId != token.UserId
: a.SlotCreatorId == token.UserId);
List<int>? friendIds = UserFriendStore.GetUserFriendData(token.UserId)?.FriendIds;
if (friendIds != null)
{
predicate = excludeFriends
? predicate.Or(a => !friendIds.Contains(a.Activity.UserId))
: predicate.Or(a => friendIds.Contains(a.Activity.UserId));
}
List<int> favouriteUsers = await this.database.HeartedProfiles.Where(hp => hp.UserId == token.UserId)
.Select(hp => hp.HeartedUserId)
.ToListAsync();
predicate = excludeFavouriteUsers
? predicate.Or(a => !favouriteUsers.Contains(a.Activity.UserId))
: predicate.Or(a => favouriteUsers.Contains(a.Activity.UserId));
predicate = excludeMyself
? predicate.Or(a => a.Activity.UserId != token.UserId)
: predicate.Or(a => a.Activity.UserId == token.UserId);
query = dtoQuery.Where(predicate).Select(dto => dto.Activity);
return query.OrderByDescending(a => a.Timestamp);
}
public Task<DateTime> GetMostRecentEventTime(GameTokenEntity token, DateTime upperBound)
{
return this.database.Activities.Where(a => a.UserId == token.UserId)
.Where(a => a.Timestamp < upperBound)
.OrderByDescending(a => a.Timestamp)
.Select(a => a.Timestamp)
.FirstOrDefaultAsync();
}
[HttpGet]
public async Task<IActionResult> GlobalActivity
(
long timestamp,
bool excludeNews,
bool excludeMyLevels,
bool excludeFriends,
bool excludeFavouriteUsers,
bool excludeMyself
)
{
GameTokenEntity token = this.GetToken();
if (token.GameVersion == GameVersion.LittleBigPlanet1) return this.BadRequest();
if (timestamp > TimeHelper.TimestampMillis || timestamp <= 0) timestamp = TimeHelper.TimestampMillis;
DateTime start = DateTimeExtensions.FromUnixTimeMilliseconds(timestamp);
DateTime soonestTime = await this.GetMostRecentEventTime(token, start);
Console.WriteLine(@"Most recent event occurred at " + soonestTime);
soonestTime = soonestTime.Subtract(TimeSpan.FromDays(1));
long soonestTimestamp = soonestTime.ToUnixTimeMilliseconds();
long endTimestamp = soonestTimestamp - 86_400_000;
Console.WriteLine(@$"soonestTime: {soonestTimestamp}, endTime: {endTimestamp}");
IQueryable<ActivityEntity> activityEvents = await this.GetFilters(token,
excludeNews,
excludeMyLevels,
excludeFriends,
excludeFavouriteUsers,
excludeMyself);
DateTime end = DateTimeExtensions.FromUnixTimeMilliseconds(endTimestamp);
activityEvents = activityEvents.Where(a => a.Timestamp < start && a.Timestamp > end);
Console.WriteLine($@"start: {start}, end: {end}");
List<IGrouping<ActivityGroup, ActivityEntity>> groups = await GroupActivities(activityEvents).ToListAsync();
foreach (IGrouping<ActivityGroup, ActivityEntity> group in groups)
{
ActivityGroup key = group.Key;
Console.WriteLine(
$@"{key.GroupType}: Timestamp: {key.Timestamp}, UserId: {key.UserId}, TargetSlotId: {key.TargetSlotId}, " +
@$"TargetUserId: {key.TargetUserId}, TargetPlaylistId: {key.TargetPlaylistId}");
foreach (ActivityEntity activity in group)
{
Console.WriteLine($@" {activity.Type}: Timestamp: {activity.Timestamp}");
}
}
DateTime oldestTime = groups.Any() ? groups.Min(g => g.Any() ? g.Min(a => a.Timestamp) : end) : end;
long oldestTimestamp = oldestTime.ToUnixTimeMilliseconds();
return this.Ok(await GameStream.CreateFromEntityResult(this.database, token, groups, timestamp, oldestTimestamp));
}
[HttpGet("slot/{slotType}/{slotId:int}")]
public async Task<IActionResult> SlotActivity(string slotType, int slotId, long timestamp)
{
GameTokenEntity token = this.GetToken();
if (token.GameVersion == GameVersion.LittleBigPlanet1) return this.BadRequest();
if (timestamp > TimeHelper.TimestampMillis || timestamp <= 0) timestamp = TimeHelper.TimestampMillis;
long endTimestamp = timestamp - 864_000;
if (slotType is not ("developer" or "user")) return this.BadRequest();
if (slotType == "developer")
slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer);
IQueryable<ActivityDto> slotActivity = this.database.Activities.Select(ActivityToDto())
.Where(a => a.TargetSlotId == slotId);
DateTime start = DateTimeOffset.FromUnixTimeMilliseconds(timestamp).DateTime;
DateTime end = DateTimeOffset.FromUnixTimeMilliseconds(endTimestamp).DateTime;
slotActivity = slotActivity.Where(a => a.Activity.Timestamp < start && a.Activity.Timestamp > end);
List<IGrouping<ActivityGroup, ActivityEntity>> groups = await GroupActivities(slotActivity).ToListAsync();
DateTime oldestTime = groups.Max(g => g.Max(a => a.Timestamp));
long oldestTimestamp = new DateTimeOffset(oldestTime).ToUnixTimeMilliseconds();
return this.Ok(await GameStream.CreateFromEntityResult(this.database, token, groups, timestamp, oldestTimestamp));
}
[HttpGet("user2/{userId:int}/")]
public async Task<IActionResult> UserActivity(int userId, long timestamp)
{
GameTokenEntity token = this.GetToken();
if (token.GameVersion == GameVersion.LittleBigPlanet1) return this.BadRequest();
if (timestamp > TimeHelper.TimestampMillis || timestamp <= 0) timestamp = TimeHelper.TimestampMillis;
long endTimestamp = timestamp - 864_000;
IQueryable<ActivityDto> userActivity = this.database.Activities.Select(ActivityToDto())
.Where(a => a.TargetUserId == userId);
DateTime start = DateTimeOffset.FromUnixTimeMilliseconds(timestamp).DateTime;
DateTime end = DateTimeOffset.FromUnixTimeMilliseconds(endTimestamp).DateTime;
userActivity = userActivity.Where(a => a.Activity.Timestamp < start && a.Activity.Timestamp > end);
List<IGrouping<ActivityGroup, ActivityEntity>> groups = await GroupActivities(userActivity).ToListAsync();
DateTime oldestTime = groups.Max(g => g.Max(a => a.Timestamp));
long oldestTimestamp = new DateTimeOffset(oldestTime).ToUnixTimeMilliseconds();
return this.Ok(
await GameStream.CreateFromEntityResult(this.database, token, groups, timestamp, oldestTimestamp));
}
}

View file

@ -9,7 +9,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Filter;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Logging;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Comment;
using LBPUnion.ProjectLighthouse.Types.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -44,6 +44,20 @@ public class CommentController : ControllerBase
return this.Ok();
}
[HttpGet("userComment/{username}")]
[HttpGet("comment/{slotType}/{slotId:int}")]
public async Task<IActionResult> GetSingleComment(string? username, string? slotType, int? slotId, int commentId)
{
GameTokenEntity token = this.GetToken();
if (username == null == (SlotHelper.IsTypeInvalid(slotType) || slotId == null)) return this.BadRequest();
CommentEntity? comment = await this.database.Comments.FindAsync(commentId);
if (comment == null) return this.NotFound();
return this.Ok(GameComment.CreateFromEntity(comment, token.UserId));
}
[HttpGet("comments/{slotType}/{slotId:int}")]
[HttpGet("userComments/{username}")]
public async Task<IActionResult> GetComments(string? username, string? slotType, int slotId)

View file

@ -5,7 +5,7 @@ using LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Users;
using LBPUnion.ProjectLighthouse.StorableLists.Stores;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.User;
using LBPUnion.ProjectLighthouse.Types.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

View file

@ -12,7 +12,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Filter;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Logging;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Photo;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

View file

@ -13,6 +13,9 @@ using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Logging;
using LBPUnion.ProjectLighthouse.Types.Misc;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Playlist;
using LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
using LBPUnion.ProjectLighthouse.Types.Serialization.User;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

View file

@ -9,7 +9,9 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Filter;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Playlist;
using LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
using LBPUnion.ProjectLighthouse.Types.Serialization.User;
using LBPUnion.ProjectLighthouse.Types.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

View file

@ -4,7 +4,8 @@ using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Playlist;
using LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

View file

@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Logging;
using LBPUnion.ProjectLighthouse.Types.Resources;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
using LBPUnion.ProjectLighthouse.Types.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

View file

@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Interaction;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Filter;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Review;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

View file

@ -8,7 +8,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Logging;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Score;
using LBPUnion.ProjectLighthouse.Types.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

View file

@ -8,7 +8,7 @@ using LBPUnion.ProjectLighthouse.Servers.GameServer.Extensions;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Filter;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

View file

@ -13,7 +13,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Filter;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Misc;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
using LBPUnion.ProjectLighthouse.Types.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

View file

@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Filter;
using LBPUnion.ProjectLighthouse.Filter.Filters;
using LBPUnion.ProjectLighthouse.Servers.GameServer.Extensions;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers;

View file

@ -10,7 +10,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Logging;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.User;
using LBPUnion.ProjectLighthouse.Types.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

View file

@ -7,7 +7,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Filter;
using LBPUnion.ProjectLighthouse.Types.Filter.Sorts;
using LBPUnion.ProjectLighthouse.Types.Misc;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Extensions;

View file

@ -5,6 +5,8 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Playlist;
using LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories;

View file

@ -5,6 +5,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories;

View file

@ -5,6 +5,8 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
using LBPUnion.ProjectLighthouse.Types.Serialization.User;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Categories;

View file

@ -1,7 +1,7 @@
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Review;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

View file

@ -3,7 +3,7 @@
@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions
@using LBPUnion.ProjectLighthouse.Types.Entities.Profile
@using LBPUnion.ProjectLighthouse.Types.Levels
@using LBPUnion.ProjectLighthouse.Types.Serialization
@using LBPUnion.ProjectLighthouse.Types.Serialization.Photo
@model LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoEntity
@{

View file

@ -3,7 +3,7 @@
@using LBPUnion.ProjectLighthouse.Files
@using LBPUnion.ProjectLighthouse.Helpers
@using LBPUnion.ProjectLighthouse.Types.Entities.Level
@using LBPUnion.ProjectLighthouse.Types.Serialization
@using LBPUnion.ProjectLighthouse.Types.Serialization.Review
@{
bool isMobile = (bool?)ViewData["IsMobile"] ?? false;

View file

@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Tests.Helpers;
using LBPUnion.ProjectLighthouse.Tests.Integration;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
using LBPUnion.ProjectLighthouse.Types.Users;
using Xunit;

View file

@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Matchmaking.Rooms;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
using LBPUnion.ProjectLighthouse.Types.Users;
using Microsoft.AspNetCore.Mvc;
using Xunit;

View file

@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers;
using LBPUnion.ProjectLighthouse.Tests.Helpers;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
using LBPUnion.ProjectLighthouse.Types.Users;
using Microsoft.AspNetCore.Mvc;
using Xunit;

View file

@ -5,7 +5,7 @@ using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers;
using LBPUnion.ProjectLighthouse.Tests.Helpers;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.User;
using Microsoft.AspNetCore.Mvc;
using Xunit;

View file

@ -3,7 +3,7 @@ using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Misc;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
using Xunit;
namespace LBPUnion.ProjectLighthouse.Tests.Unit;

View file

@ -3,7 +3,7 @@ using System.Linq;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Types.Filter;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.User;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Xunit;

View file

@ -0,0 +1,140 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace LBPUnion.ProjectLighthouse.Database;
public class ActivityInterceptor : SaveChangesInterceptor
{
private class CustomTrackedEntity
{
public required EntityState State { get; init; }
public required object Entity { get; init; }
public required object OldEntity { get; init; }
}
private readonly ConcurrentDictionary<(Type Type, int HashCode), CustomTrackedEntity> unsavedEntities;
private readonly IEntityEventHandler eventHandler;
public ActivityInterceptor(IEntityEventHandler eventHandler)
{
this.eventHandler = eventHandler;
this.unsavedEntities = new ConcurrentDictionary<(Type Type, int HashCode), CustomTrackedEntity>();
}
#region Hooking stuff
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
this.SaveNewEntities(eventData);
return base.SavingChanges(eventData, result);
}
public override ValueTask<InterceptionResult<int>> SavingChangesAsync
(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = new())
{
this.SaveNewEntities(eventData);
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
public override int SavedChanges(SaveChangesCompletedEventData eventData, int result)
{
this.ParseInsertedEntities(eventData);
return base.SavedChanges(eventData, result);
}
public override ValueTask<int> SavedChangesAsync
(SaveChangesCompletedEventData eventData, int result, CancellationToken cancellationToken = new())
{
this.ParseInsertedEntities(eventData);
return base.SavedChangesAsync(eventData, result, cancellationToken);
}
#endregion
private void SaveNewEntities(DbContextEventData eventData)
{
if (eventData.Context == null) return;
DbContext context = eventData.Context;
this.unsavedEntities.Clear();
foreach (EntityEntry entry in context.ChangeTracker.Entries())
{
// Ignore activities
if (entry.Metadata.BaseType?.ClrType == typeof(ActivityEntity) || entry.Metadata.ClrType == typeof(LastContactEntity)) continue;
// Ignore tokens
if (entry.Metadata.Name.Contains("Token")) continue;
if (entry.State is not (EntityState.Added or EntityState.Deleted or EntityState.Modified)) continue;
this.unsavedEntities.TryAdd((entry.Entity.GetType(), entry.Entity.GetHashCode()),
new CustomTrackedEntity
{
State = entry.State,
Entity = entry.Entity,
OldEntity = entry.OriginalValues.ToObject(),
});
}
}
private void ParseInsertedEntities(DbContextEventData eventData)
{
if (eventData.Context is not DatabaseContext context) return;
HashSet<CustomTrackedEntity> entities = new();
List<EntityEntry> entries = context.ChangeTracker.Entries().ToList();
foreach (KeyValuePair<(Type Type, int HashCode), CustomTrackedEntity> kvp in this.unsavedEntities)
{
EntityEntry entry = entries.FirstOrDefault(e =>
e.Metadata.ClrType == kvp.Key.Type && e.Entity.GetHashCode() == kvp.Key.HashCode);
switch (kvp.Value.State)
{
case EntityState.Added:
case EntityState.Modified:
if (entry != null) entities.Add(kvp.Value);
break;
case EntityState.Deleted:
if (entry == null) entities.Add(kvp.Value);
break;
case EntityState.Detached:
case EntityState.Unchanged:
default:
break;
}
}
foreach (CustomTrackedEntity entity in entities)
{
switch (entity.State)
{
case EntityState.Added:
this.eventHandler.OnEntityInserted(context, entity.Entity);
break;
case EntityState.Deleted:
this.eventHandler.OnEntityDeleted(context, entity.Entity);
break;
case EntityState.Modified:
this.eventHandler.OnEntityChanged(context, entity.OldEntity, entity.Entity);
break;
case EntityState.Detached:
case EntityState.Unchanged:
default:
continue;
}
}
}
}

View file

@ -1,4 +1,5 @@
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Interaction;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
@ -86,16 +87,6 @@ public partial class DatabaseContext : DbContext
public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options)
{ }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<LevelActivityEntity>().UseTpcMappingStrategy();
modelBuilder.Entity<PhotoActivityEntity>().UseTpcMappingStrategy();
modelBuilder.Entity<PlaylistActivityEntity>().UseTpcMappingStrategy();
modelBuilder.Entity<ScoreActivityEntity>().UseTpcMappingStrategy();
modelBuilder.Entity<UserActivityEntity>().UseTpcMappingStrategy();
base.OnModelCreating(modelBuilder);
}
public static DatabaseContext CreateNewInstance()
{
DbContextOptionsBuilder<DatabaseContext> builder = new();
@ -103,4 +94,26 @@ public partial class DatabaseContext : DbContext
MySqlServerVersion.LatestSupportedServerVersion);
return new DatabaseContext(builder.Options);
}
#region Activity
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//TODO implement reviews
modelBuilder.Entity<LevelActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<PhotoActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<PlaylistActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<ScoreActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<UserActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<NewsActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<CommentActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<UserActivityEntity>().UseTphMappingStrategy();
base.OnModelCreating(modelBuilder);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.AddInterceptors(new ActivityInterceptor(new ActivityEntityEventHandler()));
base.OnConfiguring(optionsBuilder);
}
#endregion
}

View file

@ -0,0 +1,12 @@
using System;
namespace LBPUnion.ProjectLighthouse.Extensions;
public static class DateTimeExtensions
{
public static long ToUnixTimeMilliseconds(this DateTime dateTime) =>
new DateTimeOffset(dateTime).ToUniversalTime().ToUnixTimeMilliseconds();
public static DateTime FromUnixTimeMilliseconds(long timestamp) =>
DateTimeOffset.FromUnixTimeMilliseconds(timestamp).ToUniversalTime().DateTime;
}

View file

@ -0,0 +1,179 @@
#nullable enable
using System;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Reflection;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Types.Entities.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Interaction;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Levels;
namespace LBPUnion.ProjectLighthouse.Types.Activity;
//TODO implement missing event triggers
public class ActivityEntityEventHandler : IEntityEventHandler
{
public void OnEntityInserted<T>(DatabaseContext database, T entity) where T : class
{
Console.WriteLine($@"OnEntityInserted: {entity.GetType().Name}");
ActivityEntity? activity = entity switch
{
SlotEntity slot => new LevelActivityEntity
{
Type = EventType.PublishLevel,
SlotId = slot.SlotId,
UserId = slot.CreatorId,
},
CommentEntity comment => new CommentActivityEntity
{
Type = comment.Type == CommentType.Level ? EventType.CommentOnLevel : EventType.CommentOnUser,
CommentId = comment.CommentId,
UserId = comment.PosterUserId,
},
PhotoEntity photo => new PhotoActivityEntity
{
Type = EventType.UploadPhoto,
PhotoId = photo.PhotoId,
UserId = photo.CreatorId,
},
ScoreEntity score => new ScoreActivityEntity
{
Type = EventType.Score,
ScoreId = score.ScoreId,
//TODO merge score migration
// UserId = int.Parse(score.PlayerIds[0]),
},
HeartedLevelEntity heartedLevel => new LevelActivityEntity
{
Type = EventType.HeartLevel,
SlotId = heartedLevel.SlotId,
UserId = heartedLevel.UserId,
},
HeartedProfileEntity heartedProfile => new UserActivityEntity
{
Type = EventType.HeartUser,
TargetUserId = heartedProfile.HeartedUserId,
UserId = heartedProfile.UserId,
},
VisitedLevelEntity visitedLevel => new LevelActivityEntity
{
Type = EventType.PlayLevel,
SlotId = visitedLevel.SlotId,
UserId = visitedLevel.UserId,
},
_ => null,
};
InsertActivity(database, activity);
}
private static void InsertActivity(DatabaseContext database, ActivityEntity? activity)
{
if (activity == null) return;
Console.WriteLine("Inserting activity: " + activity.GetType().Name);
activity.Timestamp = DateTime.UtcNow;
database.Activities.Add(activity);
database.SaveChanges();
}
public void OnEntityChanged<T>(DatabaseContext database, T origEntity, T currentEntity) where T : class
{
foreach (PropertyInfo propInfo in currentEntity.GetType().GetProperties())
{
if (!propInfo.CanRead || !propInfo.CanWrite) continue;
if (propInfo.CustomAttributes.Any(c => c.AttributeType == typeof(NotMappedAttribute))) continue;
object? origVal = propInfo.GetValue(origEntity);
object? newVal = propInfo.GetValue(currentEntity);
if ((origVal == null && newVal == null) || (origVal != null && newVal != null && origVal.Equals(newVal)))
continue;
Console.WriteLine($@"Value for {propInfo.Name} changed");
Console.WriteLine($@"Orig val: {origVal?.ToString() ?? "null"}");
Console.WriteLine($@"New val: {newVal?.ToString() ?? "null"}");
}
Console.WriteLine($@"OnEntityChanged: {currentEntity.GetType().Name}");
ActivityEntity? activity = null;
switch (currentEntity)
{
case VisitedLevelEntity visitedLevel:
{
if (origEntity is not VisitedLevelEntity) break;
activity = new LevelActivityEntity
{
Type = EventType.PlayLevel,
SlotId = visitedLevel.SlotId,
UserId = visitedLevel.UserId,
};
break;
}
case SlotEntity slotEntity:
{
if (origEntity is not SlotEntity oldSlotEntity) break;
if (!oldSlotEntity.TeamPick && slotEntity.TeamPick)
{
activity = new LevelActivityEntity
{
Type = EventType.MMPickLevel,
SlotId = slotEntity.SlotId,
UserId = SlotHelper.GetPlaceholderUserId(database).Result,
};
}
else if (oldSlotEntity.SlotId == slotEntity.SlotId && slotEntity.Type == SlotType.User)
{
activity = new LevelActivityEntity
{
Type = EventType.PublishLevel,
SlotId = slotEntity.SlotId,
UserId = slotEntity.CreatorId,
};
}
break;
}
}
InsertActivity(database, activity);
}
public void OnEntityDeleted<T>(DatabaseContext database, T entity) where T : class
{
Console.WriteLine($@"OnEntityDeleted: {entity.GetType().Name}");
ActivityEntity? activity = entity switch
{
//TODO move this to EntityModified and use CommentEntity.Deleted
CommentEntity comment => comment.Type switch
{
CommentType.Level => new CommentActivityEntity
{
Type = EventType.DeleteLevelComment,
CommentId = comment.CommentId,
UserId = comment.PosterUserId,
},
_ => null,
},
HeartedLevelEntity heartedLevel => new LevelActivityEntity
{
Type = EventType.UnheartLevel,
SlotId = heartedLevel.SlotId,
UserId = heartedLevel.UserId,
},
HeartedProfileEntity heartedProfile => new UserActivityEntity
{
Type = EventType.UnheartUser,
TargetUserId = heartedProfile.HeartedUserId,
UserId = heartedProfile.UserId,
},
_ => null,
};
InsertActivity(database, activity);
}
}

View file

@ -0,0 +1,41 @@
using System;
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Activity;
public class ActivityGroup
{
public DateTime Timestamp { get; set; }
public int UserId { get; set; }
public int? TargetSlotId { get; set; }
public int? TargetUserId { get; set; }
public int? TargetPlaylistId { get; set; }
public int TargetId =>
this.GroupType switch
{
ActivityGroupType.User => this.TargetUserId ?? 0,
ActivityGroupType.Level => this.TargetSlotId ?? 0,
ActivityGroupType.Playlist => this.TargetPlaylistId ?? 0,
_ => this.UserId,
};
public ActivityGroupType GroupType =>
this.TargetSlotId != 0
? ActivityGroupType.Level
: this.TargetUserId != 0
? ActivityGroupType.User
: ActivityGroupType.Playlist;
}
public enum ActivityGroupType
{
[XmlEnum("user")]
User,
[XmlEnum("slot")]
Level,
[XmlEnum("playlist")]
Playlist,
}

View file

@ -1,26 +1,69 @@
namespace LBPUnion.ProjectLighthouse.Types.Activity;
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Activity;
public enum EventType
{
[XmlEnum("heart_level")]
HeartLevel,
[XmlEnum("unheart_level")]
UnheartLevel,
[XmlEnum("heart_user")]
HeartUser,
[XmlEnum("unheart_user")]
UnheartUser,
[XmlEnum("play_level")]
PlayLevel,
[XmlEnum("rate_level")]
RateLevel,
[XmlEnum("tag_level")]
TagLevel,
[XmlEnum("comment_on_level")]
CommentOnLevel,
[XmlEnum("delete_level_comment")]
DeleteLevelComment,
[XmlEnum("upload_photo")]
UploadPhoto,
[XmlEnum("publish_level")]
PublishLevel,
[XmlEnum("unpublish_level")]
UnpublishLevel,
[XmlEnum("score")]
Score,
[XmlEnum("news_post")]
NewsPost,
[XmlEnum("mm_pick_level")]
MMPickLevel,
[XmlEnum("dpad_rate_level")]
DpadRateLevel,
[XmlEnum("review_level")]
ReviewLevel,
[XmlEnum("comment_on_user")]
CommentOnUser,
[XmlEnum("create_playlist")]
CreatePlaylist,
[XmlEnum("heart_playlist")]
HeartPlaylist,
[XmlEnum("add_level_to_playlist")]
AddLevelToPlaylist,
}

View file

@ -0,0 +1,10 @@
using LBPUnion.ProjectLighthouse.Database;
namespace LBPUnion.ProjectLighthouse.Types.Activity;
public interface IEntityEventHandler
{
public void OnEntityInserted<T>(DatabaseContext database, T entity) where T : class;
public void OnEntityChanged<T>(DatabaseContext database, T origEntity, T currentEntity) where T : class;
public void OnEntityDeleted<T>(DatabaseContext database, T entity) where T : class;
}

View file

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
@ -10,7 +11,7 @@ public class ActivityEntity
[Key]
public int ActivityId { get; set; }
public long Timestamp { get; set; }
public DateTime Timestamp { get; set; }
public int UserId { get; set; }

View file

@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations.Schema;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity;
/// <summary>
/// Supported event types: CommentOnUser, CommentOnLevel, DeleteLevelComment
/// </summary>
public class CommentActivityEntity : ActivityEntity
{
public int CommentId { get; set; }
[ForeignKey(nameof(CommentId))]
public CommentEntity Comment { get; set; }
}

View file

@ -1,9 +0,0 @@
namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity;
/// <summary>
/// Supported event types: CommentOnUser, CommentOnLevel, DeleteLevelComment
/// </summary>
public class CommentActivityEntry
{
}

View file

@ -3,6 +3,9 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level;
namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity;
/// <summary>
/// Supported event types: CreatePlaylist, HeartPlaylist, AddLevelToPlaylist
/// </summary>
public class PlaylistActivityEntity : ActivityEntity
{
public int PlaylistId { get; set; }

View file

@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations.Schema;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity;
public class ReviewActivityEntity : ActivityEntity
{
public int ReviewId { get; set; }
[ForeignKey(nameof(ReviewId))]
public ReviewEntity Review { get; set; }
// TODO review_modified?
}

View file

@ -1,9 +1,15 @@
namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity;
using System.ComponentModel.DataAnnotations.Schema;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity;
/// <summary>
/// Supported event types: HeartUser, UnheartUser
/// </summary>
public class UserActivityEntity : ActivityEntity
{
public int TargetUserId { get; set; }
[ForeignKey(nameof(TargetUserId))]
public UserEntity TargetUser { get; set; }
}

View file

@ -3,7 +3,7 @@ using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Review;
namespace LBPUnion.ProjectLighthouse.Types.Entities.Level;

View file

@ -5,6 +5,7 @@ using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Filter;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
namespace LBPUnion.ProjectLighthouse.Types.Levels;

View file

@ -0,0 +1,55 @@
using System.Threading.Tasks;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Serialization.Review;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events;
[XmlInclude(typeof(GameUserCommentEvent))]
[XmlInclude(typeof(GameSlotCommentEvent))]
public class GameCommentEvent : GameEvent
{
[XmlElement("comment_id")]
public int CommentId { get; set; }
}
public class GameUserCommentEvent : GameCommentEvent
{
[XmlElement("object_user")]
public string TargetUsername { get; set; }
public new async Task PrepareSerialization(DatabaseContext database)
{
await base.PrepareSerialization(database);
CommentEntity comment = await database.Comments.FindAsync(this.CommentId);
if (comment == null) return;
UserEntity user = await database.Users.FindAsync(comment.TargetId);
if (user == null) return;
this.TargetUsername = user.Username;
}
}
public class GameSlotCommentEvent : GameCommentEvent
{
[XmlElement("object_slot_id")]
public ReviewSlot TargetSlot { get; set; }
public new async Task PrepareSerialization(DatabaseContext database)
{
await base.PrepareSerialization(database);
CommentEntity comment = await database.Comments.FindAsync(this.CommentId);
if (comment == null) return;
SlotEntity slot = await database.Slots.FindAsync(comment.TargetId);
if (slot == null) return;
this.TargetSlot = ReviewSlot.CreateFromEntity(slot);
}
}

View file

@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Serialization.Review;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events;
[XmlInclude(typeof(GameCommentEvent))]
[XmlInclude(typeof(GamePhotoUploadEvent))]
[XmlInclude(typeof(GamePlayLevelEvent))]
[XmlInclude(typeof(GameReviewEvent))]
[XmlInclude(typeof(GameScoreEvent))]
[XmlInclude(typeof(GameHeartLevelEvent))]
[XmlInclude(typeof(GameHeartUserEvent))]
public class GameEvent : ILbpSerializable, INeedsPreparationForSerialization
{
[XmlIgnore]
private int UserId { get; set; }
[XmlAttribute("type")]
public EventType Type { get; set; }
[XmlElement("timestamp")]
public long Timestamp { get; set; }
[XmlElement("actor")]
public string Username { get; set; }
protected async Task PrepareSerialization(DatabaseContext database)
{
Console.WriteLine($@"SERIALIZATION!! {this.UserId} - {this.GetHashCode()}");
UserEntity user = await database.Users.FindAsync(this.UserId);
if (user == null) return;
this.Username = user.Username;
}
public static IEnumerable<GameEvent> CreateFromActivityGroups(IGrouping<EventType, ActivityEntity> group)
{
List<GameEvent> events = new();
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
// Events with Count need special treatment
switch (group.Key)
{
case EventType.PlayLevel:
{
if (group.First() is not LevelActivityEntity levelActivity) break;
events.Add(new GamePlayLevelEvent
{
Slot = new ReviewSlot
{
SlotId = levelActivity.SlotId,
},
Count = group.Count(),
UserId = levelActivity.UserId,
Timestamp = levelActivity.Timestamp.ToUnixTimeMilliseconds(),
Type = levelActivity.Type,
});
break;
}
case EventType.PublishLevel:
{
if (group.First() is not LevelActivityEntity levelActivity) break;
events.Add(new GamePublishLevelEvent
{
Slot = new ReviewSlot
{
SlotId = levelActivity.SlotId,
},
Count = group.Count(),
UserId = levelActivity.UserId,
Timestamp = levelActivity.Timestamp.ToUnixTimeMilliseconds(),
Type = levelActivity.Type,
});
break;
}
// Everything else can be handled as normal
default: events.AddRange(group.Select(CreateFromActivity));
break;
}
return events.AsEnumerable();
}
private static GameEvent CreateFromActivity(ActivityEntity activity)
{
GameEvent gameEvent = activity.Type switch
{
EventType.PlayLevel => new GamePlayLevelEvent
{
Slot = new ReviewSlot
{
SlotId = ((LevelActivityEntity)activity).SlotId,
},
},
EventType.CommentOnLevel => new GameSlotCommentEvent
{
CommentId = ((CommentActivityEntity)activity).CommentId,
},
EventType.CommentOnUser => new GameUserCommentEvent
{
CommentId = ((CommentActivityEntity)activity).CommentId,
},
EventType.HeartUser or EventType.UnheartUser => new GameHeartUserEvent
{
TargetUserId = ((UserActivityEntity)activity).TargetUserId,
},
EventType.HeartLevel or EventType.UnheartLevel => new GameHeartLevelEvent
{
TargetSlot = new ReviewSlot
{
SlotId = ((LevelActivityEntity)activity).SlotId,
},
},
_ => new GameEvent(),
};
gameEvent.UserId = activity.UserId;
gameEvent.Type = activity.Type;
gameEvent.Timestamp = activity.Timestamp.ToUnixTimeMilliseconds();
return gameEvent;
}
}

View file

@ -0,0 +1,43 @@
using System.Threading.Tasks;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Serialization.Review;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events;
public class GameHeartUserEvent : GameEvent
{
[XmlIgnore]
public int TargetUserId { get; set; }
[XmlElement("object_user")]
public string TargetUsername { get; set; }
public new async Task PrepareSerialization(DatabaseContext database)
{
await base.PrepareSerialization(database);
UserEntity targetUser = await database.Users.FindAsync(this.TargetUserId);
if (targetUser == null) return;
this.TargetUsername = targetUser.Username;
}
}
public class GameHeartLevelEvent : GameEvent
{
[XmlElement("object_slot_id")]
public ReviewSlot TargetSlot { get; set; }
public new async Task PrepareSerialization(DatabaseContext database)
{
await base.PrepareSerialization(database);
SlotEntity slot = await database.Slots.FindAsync(this.TargetSlot.SlotId);
if (slot == null) return;
this.TargetSlot = ReviewSlot.CreateFromEntity(slot);
}
}

View file

@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Serialization.Review;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events;
public class GamePhotoUploadEvent : GameEvent
{
[XmlElement("photo_id")]
public int PhotoId { get; set; }
[XmlElement("object_slot_id")]
[DefaultValue(null)]
public ReviewSlot SlotId { get; set; }
[XmlElement("user_in_photo")]
public List<string> PhotoParticipants { get; set; }
public new async Task PrepareSerialization(DatabaseContext database)
{
await base.PrepareSerialization(database);
PhotoEntity photo = await database.Photos.Where(p => p.PhotoId == this.PhotoId)
.Include(p => p.PhotoSubjects)
.ThenInclude(ps => ps.User)
.FirstOrDefaultAsync();
if (photo == null) return;
this.PhotoParticipants = photo.PhotoSubjects.Select(ps => ps.User.Username).ToList();
if (photo.SlotId == null) return;
SlotEntity slot = await database.Slots.FindAsync(photo.SlotId);
if (slot == null) return;
this.SlotId = ReviewSlot.CreateFromEntity(slot);
}
}

View file

@ -0,0 +1,26 @@
using System.Threading.Tasks;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Serialization.Review;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events;
public class GamePlayLevelEvent : GameEvent
{
[XmlElement("object_slot_id")]
public ReviewSlot Slot { get; set; }
[XmlElement("count")]
public int Count { get; set; } = 1;
public new async Task PrepareSerialization(DatabaseContext database)
{
await base.PrepareSerialization(database);
SlotEntity slot = await database.Slots.FindAsync(this.Slot.SlotId);
if (slot == null) return;
this.Slot = ReviewSlot.CreateFromEntity(slot);
}
}

View file

@ -0,0 +1,31 @@
using System.Threading.Tasks;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Serialization.Review;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events;
public class GamePublishLevelEvent : GameEvent
{
[XmlElement("object_slot_id")]
public ReviewSlot Slot { get; set; }
[XmlElement("republish")]
public bool IsRepublish { get; set; }
[XmlElement("count")]
public int Count { get; set; }
public new async Task PrepareSerialization(DatabaseContext database)
{
await base.PrepareSerialization(database);
SlotEntity slot = await database.Slots.FindAsync(this.Slot.SlotId);
if (slot == null) return;
this.Slot = ReviewSlot.CreateFromEntity(slot);
// TODO does this work?
this.IsRepublish = slot.LastUpdated == slot.FirstUploaded;
}
}

View file

@ -0,0 +1,32 @@
using System.ComponentModel;
using System.Threading.Tasks;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Serialization.Review;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events;
public class GameReviewEvent : GameEvent
{
[XmlElement("slot_id")]
public ReviewSlot Slot { get; set; }
[XmlElement("review_id")]
public int ReviewId { get; set; }
[XmlElement("review_modified")]
[DefaultValue(0)]
public long ReviewTimestamp { get; set; }
public new async Task PrepareSerialization(DatabaseContext database)
{
ReviewEntity review = await database.Reviews.FindAsync(this.ReviewId);
if (review == null) return;
SlotEntity slot = await database.Slots.FindAsync(review.SlotId);
if (slot == null) return;
this.Slot = ReviewSlot.CreateFromEntity(slot);
}
}

View file

@ -0,0 +1,39 @@
using System.Threading.Tasks;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Serialization.Review;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events;
public class GameScoreEvent : GameEvent
{
[XmlIgnore]
public int ScoreId { get; set; }
[XmlElement("object_slot_id")]
public ReviewSlot Slot { get; set; }
[XmlElement("score")]
public int Score { get; set; }
[XmlElement("user_count")]
public int UserCount { get; set; }
public new async Task PrepareSerialization(DatabaseContext database)
{
await base.PrepareSerialization(database);
ScoreEntity score = await database.Scores.FindAsync(this.ScoreId);
if (score == null) return;
SlotEntity slot = await database.Slots.FindAsync(score.SlotId);
if (slot == null) return;
this.Score = score.Points;
//TODO is this correct?
this.UserCount = score.Type;
this.Slot = ReviewSlot.CreateFromEntity(slot);
}
}

View file

@ -0,0 +1,21 @@
using System.Threading.Tasks;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Serialization.Review;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity;
public class GameSlotStreamGroup : GameStreamGroup, INeedsPreparationForSerialization
{
[XmlElement("slot_id")]
public ReviewSlot Slot { get; set; }
public async Task PrepareSerialization(DatabaseContext database)
{
SlotEntity slot = await database.Slots.FindAsync(this.Slot.SlotId);
if (slot == null) return;
this.Slot = ReviewSlot.CreateFromEntity(slot);
}
}

View file

@ -0,0 +1,127 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
using LBPUnion.ProjectLighthouse.Types.Serialization.User;
using LBPUnion.ProjectLighthouse.Types.Users;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity;
/// <summary>
/// The global stream object, contains all
/// </summary>
[XmlRoot("stream")]
public class GameStream : ILbpSerializable, INeedsPreparationForSerialization
{
[XmlIgnore]
private List<int> SlotIds { get; set; }
[XmlIgnore]
private List<int> UserIds { get; set; }
[XmlIgnore]
private int TargetUserId { get; set; }
[XmlIgnore]
private GameVersion TargetGame { get; set; }
[XmlElement("start_timestamp")]
public long StartTimestamp { get; set; }
[XmlElement("end_timestamp")]
public long EndTimestamp { get; set; }
[XmlArray("groups")]
[XmlArrayItem("group")]
public List<GameStreamGroup> Groups { get; set; }
[XmlArray("slots")]
[XmlArrayItem("slot")]
public List<SlotBase> Slots { get; set; }
[XmlArray("users")]
[XmlArrayItem("user")]
public List<GameUser> Users { get; set; }
[XmlArray("news")]
[XmlArrayItem("item")]
public List<object> News { get; set; }
//TODO implement lbp1 and lbp2 news objects
public async Task PrepareSerialization(DatabaseContext database)
{
if (this.SlotIds.Count > 0)
{
this.Slots = new List<SlotBase>();
foreach (int slotId in this.SlotIds)
{
SlotEntity slot = await database.Slots.FindAsync(slotId);
if (slot == null) continue;
this.Slots.Add(SlotBase.CreateFromEntity(slot, this.TargetGame, this.TargetUserId));
}
}
if (this.UserIds.Count > 0)
{
this.Users = new List<GameUser>();
foreach (int userId in this.UserIds)
{
UserEntity user = await database.Users.FindAsync(userId);
if (user == null) continue;
this.Users.Add(GameUser.CreateFromEntity(user, this.TargetGame));
}
}
}
public static async Task<GameStream> CreateFromEntityResult
(
DatabaseContext database,
GameTokenEntity token,
List<IGrouping<ActivityGroup, ActivityEntity>> results,
long startTimestamp,
long endTimestamp
)
{
List<int> slotIds = results.Where(g => g.Key.TargetSlotId != null && g.Key.TargetSlotId.Value != 0)
.Select(g => g.Key.TargetSlotId.Value)
.ToList();
Console.WriteLine($@"slotIds: {string.Join(",", slotIds)}");
List<int> userIds = results.Where(g => g.Key.TargetUserId != null && g.Key.TargetUserId.Value != 0)
.Select(g => g.Key.TargetUserId.Value)
.Distinct()
.Union(results.Select(g => g.Key.UserId))
.ToList();
// Cache target levels and users within DbContext
await database.Slots.Where(s => slotIds.Contains(s.SlotId)).LoadAsync();
await database.Users.Where(u => userIds.Contains(u.UserId)).LoadAsync();
Console.WriteLine($@"userIds: {string.Join(",", userIds)}");
Console.WriteLine($@"Stream contains {slotIds.Count} slots and {userIds.Count} users");
GameStream gameStream = new()
{
TargetUserId = token.UserId,
TargetGame = token.GameVersion,
StartTimestamp = startTimestamp,
EndTimestamp = endTimestamp,
SlotIds = slotIds,
UserIds = userIds,
Groups = new List<GameStreamGroup>(),
};
foreach (IGrouping<ActivityGroup, ActivityEntity> group in results)
{
gameStream.Groups.Add(GameStreamGroup.CreateFromGrouping(group));
}
return gameStream;
}
}

View file

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Activity;
using LBPUnion.ProjectLighthouse.Types.Serialization.Activity.Events;
using LBPUnion.ProjectLighthouse.Types.Serialization.Review;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity;
/// <summary>
/// Top level groups generally contain all events for a given level or user
/// <para>
/// The sub-groups are always <see cref="GameUserStreamGroup"/> and contain all activities from a single user
/// for the top level group entity
/// </para>
/// </summary>
[XmlInclude(typeof(GameUserStreamGroup))]
[XmlInclude(typeof(GameSlotStreamGroup))]
public class GameStreamGroup : ILbpSerializable
{
[XmlAttribute("type")]
public ActivityGroupType Type { get; set; }
[XmlElement("timestamp")]
public long Timestamp { get; set; }
[XmlArray("subgroups")]
[XmlArrayItem("group")]
[DefaultValue(null)]
public List<GameStreamGroup> Groups { get; set; }
[XmlArray("events")]
[XmlArrayItem("event")]
[DefaultValue(null)]
public List<GameEvent> Events { get; set; }
public static GameStreamGroup CreateFromGrouping(IGrouping<ActivityGroup, ActivityEntity> group)
{
ActivityGroupType type = group.Key.GroupType;
GameStreamGroup gameGroup = type switch
{
ActivityGroupType.Level => new GameSlotStreamGroup
{
Slot = new ReviewSlot
{
SlotId = group.Key.TargetId,
},
},
ActivityGroupType.User => new GameUserStreamGroup
{
UserId = group.Key.TargetId,
},
_ => new GameStreamGroup(),
};
gameGroup.Timestamp = new DateTimeOffset(group.Select(a => a.Timestamp).MaxBy(a => a)).ToUnixTimeMilliseconds();
gameGroup.Type = type;
List<IGrouping<EventType, ActivityEntity>> eventGroups = group.OrderByDescending(a => a.Timestamp).GroupBy(g => g.Type).ToList();
//TODO removeme debug
foreach (IGrouping<EventType, ActivityEntity> bruh in eventGroups)
{
Console.WriteLine($@"group key: {bruh.Key}, count={bruh.Count()}");
}
gameGroup.Groups = new List<GameStreamGroup>
{
new GameUserStreamGroup
{
UserId = group.Key.UserId,
Type = ActivityGroupType.User,
Timestamp = gameGroup.Timestamp,
Events = eventGroups.SelectMany(GameEvent.CreateFromActivityGroups).ToList(),
},
};
return gameGroup;
}
}

View file

@ -0,0 +1,29 @@
using System.Threading.Tasks;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity;
public class GameUserStreamGroup : GameStreamGroup, INeedsPreparationForSerialization
{
[XmlIgnore]
public int UserId { get; set; }
[XmlElement("user_id")]
public string Username { get; set; }
public async Task PrepareSerialization(DatabaseContext database)
{
UserEntity user = await database.Users.FindAsync(this.UserId);
if (user == null) return;
this.Username = user.Username;
}
public static GameUserStreamGroup Create(int userId) =>
new()
{
UserId = userId,
};
}

View file

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Comment;
[XmlRoot("comments")]
public struct CommentListResponse : ILbpSerializable

View file

@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Comment;
[XmlRoot("comment")]
[XmlType("comment")]

View file

@ -9,7 +9,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Levels;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Photo;
[XmlRoot("photo")]
[XmlType("photo")]

View file

@ -1,7 +1,7 @@
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Photo;
[XmlType("subject")]
[XmlRoot("subject")]

View file

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Photo;
[XmlRoot("photos")]
public struct PhotoListResponse : ILbpSerializable

View file

@ -3,7 +3,7 @@ using System.ComponentModel;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Types.Levels;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Photo;
[XmlRoot("slot")]
public class PhotoSlot : ILbpSerializable

View file

@ -1,6 +1,6 @@
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Playlist;
[XmlRoot("author")]
public struct Author : ILbpSerializable

View file

@ -10,7 +10,7 @@ using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Playlist;
[XmlRoot("playlist")]
public class GamePlaylist : ILbpSerializable, INeedsPreparationForSerialization

View file

@ -2,7 +2,7 @@
using System.ComponentModel;
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Playlist;
public struct GenericPlaylistResponse<T> : ILbpSerializable, IHasCustomRoot where T : ILbpSerializable
{

View file

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Playlist;
public struct IconList : ILbpSerializable
{

View file

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Playlist;
[XmlRoot("playlists")]
public struct PlaylistResponse : ILbpSerializable

View file

@ -8,7 +8,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Levels;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Review;
[XmlRoot("deleted_by")]
public enum DeletedBy

View file

@ -2,7 +2,7 @@
using System.ComponentModel;
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Review;
[XmlRoot("reviews")]
public struct ReviewResponse : ILbpSerializable

View file

@ -0,0 +1,22 @@
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Levels;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Review;
[XmlRoot("slot")]
public class ReviewSlot : ILbpSerializable
{
[XmlAttribute("type")]
public SlotType SlotType { get; set; }
[XmlText]
public int SlotId { get; set; }
public static ReviewSlot CreateFromEntity(SlotEntity slot) =>
new()
{
SlotType = slot.Type,
SlotId = slot.Type == SlotType.User ? slot.SlotId : slot.InternalSlotId,
};
}

View file

@ -1,14 +0,0 @@
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Types.Levels;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
[XmlRoot("slot")]
public class ReviewSlot : ILbpSerializable
{
[XmlAttribute("type")]
public SlotType SlotType { get; set; }
[XmlText]
public int SlotId { get; set; }
}

View file

@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Score;
[XmlRoot("playRecord")]
[XmlType("playRecord")]

View file

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Score;
[XmlRoot("scoreboards")]
public class MultiScoreboardResponse : ILbpSerializable

View file

@ -2,7 +2,7 @@
using System.ComponentModel;
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Score;
public struct ScoreboardResponse: ILbpSerializable, IHasCustomRoot
{

View file

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
[XmlRoot("categories")]
public class CategoryListResponse : ILbpSerializable

View file

@ -2,7 +2,7 @@
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Types.Levels;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
[XmlRoot("category")]
public class GameCategory : ILbpSerializable

View file

@ -7,7 +7,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Levels;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
[XmlRoot("slot")]
public class GameDeveloperSlot : SlotBase, INeedsPreparationForSerialization

View file

@ -13,10 +13,12 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Interaction;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Misc;
using LBPUnion.ProjectLighthouse.Types.Serialization.Review;
using LBPUnion.ProjectLighthouse.Types.Serialization.User;
using LBPUnion.ProjectLighthouse.Types.Users;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
[XmlRoot("slot")]
public class GameUserSlot : SlotBase, INeedsPreparationForSerialization

View file

@ -3,7 +3,7 @@ using System.ComponentModel;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
public struct GenericSlotResponse : ILbpSerializable, IHasCustomRoot
{

View file

@ -1,6 +1,6 @@
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
[XmlRoot("planetStats")]
public class PlanetStatsResponse : ILbpSerializable

View file

@ -3,9 +3,10 @@ using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Serialization.User;
using LBPUnion.ProjectLighthouse.Types.Users;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
[XmlInclude(typeof(GameUserSlot))]
[XmlInclude(typeof(GameDeveloperSlot))]
@ -49,7 +50,7 @@ public abstract class SlotBase : ILbpSerializable
public static SlotBase CreateFromEntity(SlotEntity slot, GameTokenEntity token)
=> CreateFromEntity(slot, token.GameVersion, token.UserId);
private static SlotBase CreateFromEntity(SlotEntity slot, GameVersion targetGame, int targetUserId)
public static SlotBase CreateFromEntity(SlotEntity slot, GameVersion targetGame, int targetUserId)
{
if (slot == null)
{

View file

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.Slot;
[XmlRoot("slot")]
public struct SlotResourceResponse : ILbpSerializable

View file

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.User;
[XmlRoot("npdata")]
public struct FriendResponse : ILbpSerializable

View file

@ -11,7 +11,7 @@ using LBPUnion.ProjectLighthouse.Types.Misc;
using LBPUnion.ProjectLighthouse.Types.Users;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.User;
[XmlRoot("user")]
public class GameUser : ILbpSerializable, INeedsPreparationForSerialization

View file

@ -3,7 +3,7 @@ using System.ComponentModel;
using System.Xml.Serialization;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.User;
public struct GenericUserResponse<T> : ILbpSerializable, IHasCustomRoot where T : ILbpSerializable
{

View file

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.User;
[XmlRoot("users")]
public struct MinimalUserListResponse : ILbpSerializable

View file

@ -1,7 +1,7 @@
using System.ComponentModel;
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.User;
public class NpHandle : ILbpSerializable
{

View file

@ -2,7 +2,7 @@
using System.ComponentModel;
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Serialization.User;
public struct UserListResponse : ILbpSerializable, IHasCustomRoot
{
@ -23,7 +23,7 @@ public struct UserListResponse : ILbpSerializable, IHasCustomRoot
}
[XmlIgnore]
public string RootTag { get; set; }
private string RootTag { get; set; }
[XmlElement("user")]
public List<GameUser> Users { get; set; }