This commit is contained in:
Josh 2024-11-08 11:19:04 -05:00 committed by GitHub
commit a8b070fb1e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
162 changed files with 5438 additions and 138 deletions

View file

@ -1,6 +1,6 @@
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Filter;
using LBPUnion.ProjectLighthouse.Filter.Filters;
using LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Servers.API.Responses;
using LBPUnion.ProjectLighthouse.Types.Users;

View file

@ -28,11 +28,7 @@ public class ApiStartup
}
);
services.AddDbContext<DatabaseContext>(builder =>
{
builder.UseMySql(ServerConfiguration.Instance.DbConnectionString,
MySqlServerVersion.LatestSupportedServerVersion);
});
services.AddDbContext<DatabaseContext>(DatabaseContext.ConfigureBuilder());
services.AddSwaggerGen
(

View file

@ -0,0 +1,372 @@
using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Filter.Filters.Activity;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.StorableLists.Stores;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Logging;
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;
}
private class ActivityFilterOptions
{
public bool ExcludeNews { get; init; }
public bool ExcludeMyLevels { get; init; }
public bool ExcludeFriends { get; init; }
public bool ExcludeFavouriteUsers { get; init; }
public bool ExcludeMyself { get; init; }
public bool ExcludeMyPlaylists { get; init; } = true;
}
private async Task<IQueryable<ActivityDto>> GetFilters
(
IQueryable<ActivityDto> dtoQuery,
GameTokenEntity token,
ActivityFilterOptions options
)
{
dtoQuery = token.GameVersion == GameVersion.LittleBigPlanetVita
? dtoQuery.Where(dto => dto.TargetSlotGameVersion == null || dto.TargetSlotGameVersion == token.GameVersion)
: dtoQuery.Where(dto => dto.TargetSlotGameVersion == null || dto.TargetSlotGameVersion <= token.GameVersion);
Expression<Func<ActivityDto, bool>> predicate = PredicateExtensions.False<ActivityDto>();
List<int> favouriteUsers = await this.database.HeartedProfiles.Where(hp => hp.UserId == token.UserId)
.Select(hp => hp.HeartedUserId)
.ToListAsync();
List<int>? friendIds = UserFriendStore.GetUserFriendData(token.UserId)?.FriendIds;
friendIds ??= [];
// This is how lbp3 does its filtering
GameStreamFilter? filter = await this.DeserializeBody<GameStreamFilter>();
if (filter?.Sources != null)
{
foreach (GameStreamFilterEventSource filterSource in filter.Sources.Where(filterSource =>
filterSource.SourceType != null && filterSource.Types?.Count != 0))
{
EventType[] types = filterSource.Types?.ToArray() ?? Array.Empty<EventType>();
EventTypeFilter eventFilter = new(types);
predicate = filterSource.SourceType switch
{
"MyLevels" => predicate.Or(new MyLevelActivityFilter(token.UserId, eventFilter).GetPredicate()),
"FavouriteUsers" => predicate.Or(
new IncludeUserIdFilter(favouriteUsers, eventFilter).GetPredicate()),
"Friends" => predicate.Or(new IncludeUserIdFilter(friendIds, eventFilter).GetPredicate()),
_ => predicate,
};
}
}
Expression<Func<ActivityDto, bool>> newsPredicate = !options.ExcludeNews
? new IncludeNewsFilter().GetPredicate()
: new ExcludeNewsFilter().GetPredicate();
predicate = predicate.Or(newsPredicate);
if (!options.ExcludeMyLevels)
{
predicate = predicate.Or(dto => dto.TargetSlotCreatorId == token.UserId);
}
List<int> includedUserIds = [];
if (!options.ExcludeFriends)
{
includedUserIds.AddRange(friendIds);
}
if (!options.ExcludeFavouriteUsers)
{
includedUserIds.AddRange(favouriteUsers);
}
if (!options.ExcludeMyself)
{
includedUserIds.Add(token.UserId);
}
predicate = predicate.Or(dto => includedUserIds.Contains(dto.Activity.UserId));
if (!options.ExcludeMyPlaylists && !options.ExcludeMyself && token.GameVersion == GameVersion.LittleBigPlanet3)
{
List<int> creatorPlaylists = await this.database.Playlists.Where(p => p.CreatorId == token.UserId)
.Select(p => p.PlaylistId)
.ToListAsync();
predicate = predicate.Or(new PlaylistActivityFilter(creatorPlaylists).GetPredicate());
}
else
{
predicate = predicate.And(dto =>
dto.Activity.Type != EventType.CreatePlaylist &&
dto.Activity.Type != EventType.HeartPlaylist &&
dto.Activity.Type != EventType.AddLevelToPlaylist);
}
dtoQuery = dtoQuery.Where(predicate);
return dtoQuery;
}
public Task<DateTime> GetMostRecentEventTime(IQueryable<ActivityDto> activity, DateTime upperBound)
{
return activity.OrderByDescending(a => a.Activity.Timestamp)
.Where(a => a.Activity.Timestamp < upperBound)
.Select(a => a.Activity.Timestamp)
.FirstOrDefaultAsync();
}
private async Task<(DateTime Start, DateTime End)> GetTimeBounds
(IQueryable<ActivityDto> activityQuery, long? startTime, long? endTime)
{
if (startTime is null or 0) startTime = TimeHelper.TimestampMillis;
DateTime start = DateTimeExtensions.FromUnixTimeMilliseconds(startTime.Value);
DateTime end;
if (endTime == null)
{
end = await this.GetMostRecentEventTime(activityQuery, start);
// If there is no recent event then set it to the the start
if (end == DateTime.MinValue) end = start;
end = end.Subtract(TimeSpan.FromDays(7));
}
else
{
end = DateTimeExtensions.FromUnixTimeMilliseconds(endTime.Value);
// Don't allow more than 7 days worth of activity in a single page
if (start.Subtract(end).TotalDays > 7)
{
end = start.Subtract(TimeSpan.FromDays(7));
}
}
return (start, end);
}
private static DateTime GetOldestTime
(IReadOnlyCollection<IGrouping<ActivityGroup, ActivityDto>> groups, DateTime defaultTimestamp) =>
groups.Count != 0
? groups.Min(g => g.MinBy(a => a.Activity.Timestamp)?.Activity.Timestamp ?? defaultTimestamp)
: defaultTimestamp;
/// <summary>
/// Speeds up serialization because many nested entities need to find Slots by id
/// and since they use the Find() method they can benefit from having the entities
/// already tracked by the context
/// </summary>
private async Task CacheEntities(IReadOnlyCollection<OuterActivityGroup> groups)
{
List<int> slotIds = groups.GetIds(ActivityGroupType.Level);
List<int> userIds = groups.GetIds(ActivityGroupType.User);
List<int> playlistIds = groups.GetIds(ActivityGroupType.Playlist);
List<int> newsIds = groups.GetIds(ActivityGroupType.News);
// Cache target levels and users within DbContext
if (slotIds.Count > 0) await this.database.Slots.Where(s => slotIds.Contains(s.SlotId)).LoadAsync();
if (userIds.Count > 0) await this.database.Users.Where(u => userIds.Contains(u.UserId)).LoadAsync();
if (playlistIds.Count > 0)
await this.database.Playlists.Where(p => playlistIds.Contains(p.PlaylistId)).LoadAsync();
if (newsIds.Count > 0)
await this.database.WebsiteAnnouncements.Where(a => newsIds.Contains(a.AnnouncementId)).LoadAsync();
}
/// <summary>
/// LBP3 uses a different grouping format that wants the actor to be the top level group and the events should be the subgroups
/// </summary>
[HttpPost]
public async Task<IActionResult> GlobalActivityLBP3
(long timestamp, bool excludeMyPlaylists, bool excludeNews, bool excludeMyself)
{
GameTokenEntity token = this.GetToken();
if (token.GameVersion != GameVersion.LittleBigPlanet3) return this.NotFound();
IQueryable<ActivityDto> activityEvents = await this.GetFilters(
this.database.Activities.ToActivityDto(true, true), token, new ActivityFilterOptions()
{
ExcludeNews = excludeNews,
ExcludeMyLevels = true,
ExcludeFriends = true,
ExcludeFavouriteUsers = true,
ExcludeMyself = excludeMyself,
ExcludeMyPlaylists = excludeMyPlaylists,
});
(DateTime Start, DateTime End) times = await this.GetTimeBounds(activityEvents, timestamp, null);
// LBP3 is grouped by actorThenObject meaning it wants all events by a user grouped together rather than
// all user events for a level or profile grouped together
List<IGrouping<ActivityGroup, ActivityDto>> groups = await activityEvents
.Where(dto => dto.Activity.Timestamp < times.Start && dto.Activity.Timestamp > times.End)
.ToActivityGroups(true)
.ToListAsync();
List<OuterActivityGroup> outerGroups = groups.ToOuterActivityGroups(true);
long oldestTimestamp = GetOldestTime(groups, times.Start).ToUnixTimeMilliseconds();
return this.Ok(GameStream.CreateFromGroups(token,
outerGroups,
times.Start.ToUnixTimeMilliseconds(),
oldestTimestamp));
}
[HttpGet]
public async Task<IActionResult> GlobalActivity
(
long timestamp,
long endTimestamp,
bool excludeNews,
bool excludeMyLevels,
bool excludeFriends,
bool excludeFavouriteUsers,
bool excludeMyself
)
{
GameTokenEntity token = this.GetToken();
if (token.GameVersion is GameVersion.LittleBigPlanet1 or GameVersion.LittleBigPlanetPSP) return this.NotFound();
IQueryable<ActivityDto> activityEvents = await this.GetFilters(this.database.Activities.ToActivityDto(true),
token,
new ActivityFilterOptions
{
ExcludeNews = excludeNews,
ExcludeMyLevels = excludeMyLevels,
ExcludeFriends = excludeFriends,
ExcludeFavouriteUsers = excludeFavouriteUsers,
ExcludeMyself = excludeMyself,
});
(DateTime Start, DateTime End) times = await this.GetTimeBounds(activityEvents, timestamp, endTimestamp);
List<IGrouping<ActivityGroup, ActivityDto>> groups = await activityEvents
.Where(dto => dto.Activity.Timestamp < times.Start && dto.Activity.Timestamp > times.End)
.ToActivityGroups()
.ToListAsync();
List<OuterActivityGroup> outerGroups = groups.ToOuterActivityGroups();
long oldestTimestamp = GetOldestTime(groups, times.Start).ToUnixTimeMilliseconds();
await this.CacheEntities(outerGroups);
GameStream? gameStream = GameStream.CreateFromGroups(token,
outerGroups,
times.Start.ToUnixTimeMilliseconds(),
oldestTimestamp);
return this.Ok(gameStream);
}
#if DEBUG
private static void PrintOuterGroups(List<OuterActivityGroup> outerGroups)
{
foreach (OuterActivityGroup outer in outerGroups)
{
Logger.Debug(@$"Outer group key: {outer.Key}", LogArea.Activity);
List<IGrouping<InnerActivityGroup, ActivityDto>> itemGroup = outer.Groups;
foreach (IGrouping<InnerActivityGroup, ActivityDto> item in itemGroup)
{
Logger.Debug(
@$" Inner group key: TargetId={item.Key.TargetId}, UserId={item.Key.UserId}, Type={item.Key.Type}",
LogArea.Activity);
foreach (ActivityDto activity in item)
{
Logger.Debug(
@$" Activity: {activity.GroupType}, Timestamp: {activity.Activity.Timestamp}, UserId: {activity.Activity.UserId}, EventType: {activity.Activity.Type}, TargetId: {activity.TargetId}",
LogArea.Activity);
}
}
}
}
#endif
[HttpGet("slot/{slotType}/{slotId:int}")]
[HttpGet("user2/{username}")]
public async Task<IActionResult> LocalActivity(string? slotType, int slotId, string? username, long? timestamp)
{
GameTokenEntity token = this.GetToken();
if (token.GameVersion is GameVersion.LittleBigPlanet1 or GameVersion.LittleBigPlanetPSP) return this.NotFound();
if ((SlotHelper.IsTypeInvalid(slotType) || slotId == 0) == (username == null)) return this.BadRequest();
bool isLevelActivity = username == null;
bool groupByActor = !isLevelActivity && token.GameVersion == GameVersion.LittleBigPlanet3;
// User and Level activity will never contain news posts or MM pick events.
IQueryable<ActivityDto> activityQuery = this.database.Activities.ToActivityDto()
.Where(a => a.Activity.Type != EventType.NewsPost && a.Activity.Type != EventType.MMPickLevel);
if (token.GameVersion != GameVersion.LittleBigPlanet3)
{
activityQuery = activityQuery.Where(a =>
a.Activity.Type != EventType.CreatePlaylist &&
a.Activity.Type != EventType.HeartPlaylist &&
a.Activity.Type != EventType.AddLevelToPlaylist);
}
// Slot activity
if (isLevelActivity)
{
if (slotType == "developer")
slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer);
if (!await this.database.Slots.AnyAsync(s => s.SlotId == slotId)) return this.NotFound();
activityQuery = activityQuery.Where(dto => dto.TargetSlotId == slotId);
}
// User activity
else
{
int userId = await this.database.Users.Where(u => u.Username == username)
.Select(u => u.UserId)
.FirstOrDefaultAsync();
if (userId == 0) return this.NotFound();
activityQuery = activityQuery.Where(dto => dto.Activity.UserId == userId);
}
(DateTime Start, DateTime End) times = await this.GetTimeBounds(activityQuery, timestamp, null);
activityQuery = activityQuery.Where(dto =>
dto.Activity.Timestamp < times.Start && dto.Activity.Timestamp > times.End);
List<IGrouping<ActivityGroup, ActivityDto>> groups = await activityQuery.ToActivityGroups(groupByActor).ToListAsync();
List<OuterActivityGroup> outerGroups = groups.ToOuterActivityGroups(groupByActor);
long oldestTimestamp = GetOldestTime(groups, times.Start).ToUnixTimeMilliseconds();
await this.CacheEntities(outerGroups);
return this.Ok(GameStream.CreateFromGroups(token,
outerGroups,
times.Start.ToUnixTimeMilliseconds(),
oldestTimestamp,
isLevelActivity));
}
}

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

@ -0,0 +1,31 @@
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Types.Entities.Website;
using LBPUnion.ProjectLighthouse.Types.Serialization.News;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers;
[ApiController]
[Authorize]
[Route("LITTLEBIGPLANETPS3_XML/")]
[Produces("text/xml")]
public class NewsController : ControllerBase
{
private readonly DatabaseContext database;
public NewsController(DatabaseContext database)
{
this.database = database;
}
[HttpGet("news")]
public async Task<IActionResult> GetNews()
{
List<WebsiteAnnouncementEntity> websiteAnnouncements =
await this.database.WebsiteAnnouncements.OrderByDescending(a => a.AnnouncementId).ToListAsync();
return this.Ok(GameNews.CreateFromEntity(websiteAnnouncements));
}
}

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

@ -7,7 +7,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;
@ -145,6 +145,22 @@ public class ReviewController : ControllerBase
return this.Ok();
}
[HttpGet("review/user/{slotId:int}/{reviewerName}")]
public async Task<IActionResult> GetReview(int slotId, string reviewerName)
{
GameTokenEntity token = this.GetToken();
int reviewerId = await this.database.Users.Where(u => u.Username == reviewerName)
.Select(s => s.UserId)
.FirstOrDefaultAsync();
if (reviewerId == 0) return this.NotFound();
ReviewEntity? review = await this.database.Reviews.FirstOrDefaultAsync(r => r.ReviewerId == reviewerId && r.SlotId == slotId);
if (review == null) return this.NotFound();
return this.Ok(GameReview.CreateFromEntity(review, token));
}
[HttpGet("reviewsFor/user/{slotId:int}")]
public async Task<IActionResult> ReviewsFor(int slotId)
{

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;
@ -131,7 +131,8 @@ public class ScoreController : ControllerBase
await this.database.SaveChangesAsync();
ScoreEntity? existingScore = await this.database.Scores.Where(s => s.SlotId == slot.SlotId)
ScoreEntity? existingScore = await this.database.Scores
.Where(s => s.SlotId == slot.SlotId)
.Where(s => s.ChildSlotId == 0 || s.ChildSlotId == childId)
.Where(s => s.UserId == token.UserId)
.Where(s => s.Type == score.Type)

View file

@ -2,13 +2,13 @@
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Filter;
using LBPUnion.ProjectLighthouse.Filter.Filters;
using LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
using LBPUnion.ProjectLighthouse.Filter.Sorts;
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

@ -4,6 +4,7 @@ using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Filter;
using LBPUnion.ProjectLighthouse.Filter.Filters;
using LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
using LBPUnion.ProjectLighthouse.Filter.Sorts;
using LBPUnion.ProjectLighthouse.Filter.Sorts.Metadata;
using LBPUnion.ProjectLighthouse.Helpers;
@ -13,7 +14,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

@ -4,9 +4,9 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Filter;
using LBPUnion.ProjectLighthouse.Filter.Filters;
using LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
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

@ -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.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

@ -1,5 +1,5 @@
using LBPUnion.ProjectLighthouse.Filter;
using LBPUnion.ProjectLighthouse.Filter.Filters;
using LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Users;

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

@ -54,11 +54,7 @@ public class GameServerStartup
}
);
services.AddDbContext<DatabaseContext>(builder =>
{
builder.UseMySql(ServerConfiguration.Instance.DbConnectionString,
MySqlServerVersion.LatestSupportedServerVersion);
});
services.AddDbContext<DatabaseContext>(DatabaseContext.ConfigureBuilder());
IMailService mailService = ServerConfiguration.Instance.Mail.MailEnabled
? new MailQueueService(new SmtpMailSender())

View file

@ -1,7 +1,7 @@
#nullable enable
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Filter;
using LBPUnion.ProjectLighthouse.Filter.Filters;
using LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;

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

@ -2,7 +2,7 @@
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Filter;
using LBPUnion.ProjectLighthouse.Filter.Filters;
using LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
using LBPUnion.ProjectLighthouse.Filter.Sorts;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;

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

@ -0,0 +1,85 @@
@page "/debug/activity"
@using System.Globalization
@using LBPUnion.ProjectLighthouse.Types.Activity
@using LBPUnion.ProjectLighthouse.Types.Entities.Activity
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Debug.ActivityTestPage
@{
Layout = "Layouts/BaseLayout";
Model.Title = "Debug - Activity Test";
}
<a href="?groupByActor=false">
<div class="ui @(!Model.GroupByActor ? "blue" : "") button">Group By Activity</div>
</a>
<a href="?groupByActor=true">
<div class="ui @(Model.GroupByActor ? "blue" : "") button">Group By Actor</div>
</a>
<div class="ui divider"></div>
@foreach (OuterActivityGroup activity in Model.ActivityGroups)
{
<h4 class="ui top attached header">@activity.Key.GroupType, Timestamp: @activity.Key.Timestamp.ToString(CultureInfo.InvariantCulture)</h4>
<div class="ui attached segment">
@if (activity.Key.UserId != -1)
{
<p>UserId: @activity.Key.UserId</p>
}
@if ((activity.Key.TargetNewsId ?? -1) != -1)
{
<p>TargetNewsId?: @activity.Key.TargetNewsId (targetId=@activity.Key.TargetId)</p>
}
@if ((activity.Key.TargetPlaylistId ?? -1) != -1)
{
<p>TargetPlaylistId?: @activity.Key.TargetPlaylistId (targetId=@activity.Key.TargetId)</p>
}
@if ((activity.Key.TargetSlotId ?? -1) != -1)
{
<p>TargetSlotId?: @activity.Key.TargetSlotId (targetId=@activity.Key.TargetId)</p>
}
@if ((activity.Key.TargetTeamPickSlotId ?? -1) != -1)
{
<p>TargetTeamPickSlot?: @activity.Key.TargetTeamPickSlotId (targetId=@activity.Key.TargetId)</p>
}
@if ((activity.Key.TargetUserId ?? -1) != -1)
{
<p>TargetUserId?: @activity.Key.TargetUserId (targetId=@activity.Key.TargetId)</p>
}
<div class="ui segments">
@foreach (IGrouping<InnerActivityGroup, ActivityDto>? eventGroup in activity.Groups)
{
<div class="ui segment">
<h5>Nested Group Type: @eventGroup.Key.Type</h5>
@foreach (ActivityDto gameEvent in eventGroup.ToList())
{
<h5 class="ui top attached header" style="text-align: start">
@gameEvent.Activity.Type, Event Id: @gameEvent.Activity.ActivityId
</h5>
<div class="ui attached segment">
<p>Event Group Type: @gameEvent.GroupType</p>
<p>Event Target ID: @gameEvent.TargetId</p>
@if (gameEvent.Activity is LevelActivityEntity level)
{
<p>SlotId: @level.SlotId</p>
<p>SlotVersion: @gameEvent.TargetSlotGameVersion</p>
}
@if (gameEvent.Activity is ScoreActivityEntity score)
{
<p>ScoreId: @score.ScoreId</p>
<p>SlotId: @score.SlotId</p>
<p>SlotVersion: @gameEvent.TargetSlotGameVersion</p>
}
</div>
}
</div>
}
</div>
</div>
<div class="ui bottom attached segment">
<p>Total events: @activity.Groups.Sum(g => g.ToList().Count)</p>
</div>
<div class="ui massive divider" style="background-color: #0e91f5"></div>
}

View file

@ -0,0 +1,34 @@
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types.Activity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Debug;
public class ActivityTestPage : BaseLayout
{
public ActivityTestPage(DatabaseContext database) : base(database)
{ }
public List<OuterActivityGroup> ActivityGroups = [];
public bool GroupByActor { get; set; }
public async Task<IActionResult> OnGet(bool groupByActor = false)
{
Console.WriteLine(groupByActor);
List<OuterActivityGroup>? events = (await this.Database.Activities.ToActivityDto(true).ToActivityGroups(groupByActor).ToListAsync())
.ToOuterActivityGroups(groupByActor);
if (events == null) return this.Page();
this.GroupByActor = groupByActor;
this.ActivityGroups = events;
return this.Page();
}
}

View file

@ -64,7 +64,7 @@ public class CompleteEmailVerificationPage : BaseLayout
webToken.UserToken,
new CookieOptions
{
Expires = DateTimeOffset.Now.AddDays(7),
Expires = DateTimeOffset.UtcNow.AddDays(7),
});
return this.Redirect("/passwordReset");
}

View file

@ -14,6 +14,7 @@
bool isMobile = Request.IsMobile();
string language = Model.GetLanguage();
string timeZone = Model.GetTimeZone();
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
}
<h1 class="lighthouse-welcome lighthouse-title">
@Model.Translate(LandingPageStrings.Welcome, ServerConfiguration.Instance.Customization.ServerName)
@ -82,6 +83,7 @@
<a style="color: black" href="~/user/@Model.LatestAnnouncement.Publisher.UserId">
@Model.LatestAnnouncement.Publisher.Username
</a>
at @TimeZoneInfo.ConvertTime(Model.LatestAnnouncement.PublishedAt, TimeZoneInfo.Utc, timeZoneInfo).ToString("M/d/yyyy h:mm:ss tt")
</div>
}
</div>

View file

@ -38,7 +38,7 @@
<div class="column">
<h2 class="ui black image header centered">
<img src="~/@(ServerConfiguration.Instance.WebsiteConfiguration.PrideEventEnabled && DateTime.Now.Month == 6 ? "logo-pride.png" : "logo-color.png")"
<img src="~/@(ServerConfiguration.Instance.WebsiteConfiguration.PrideEventEnabled && DateTime.UtcNow.Month == 6 ? "logo-pride.png" : "logo-color.png")"
alt="Instance logo"
class="image"
style="width: 128px;"/>

View file

@ -100,7 +100,7 @@ public class LoginForm : BaseLayout
webToken.UserToken,
new CookieOptions
{
Expires = DateTimeOffset.Now.AddDays(7),
Expires = DateTimeOffset.UtcNow.AddDays(7),
}
);

View file

@ -2,13 +2,14 @@
@using LBPUnion.ProjectLighthouse.Localization.StringLists
@using LBPUnion.ProjectLighthouse.Types.Entities.Notifications
@using LBPUnion.ProjectLighthouse.Types.Entities.Website
@using LBPUnion.ProjectLighthouse.Types.Notifications
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.NotificationsPage
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = "Layouts/BaseLayout";
Model.Title = Model.Translate(GeneralStrings.Notifications);
string timeZone = Model.GetTimeZone();
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
}
@if (Model.User != null && Model.User.IsAdmin)
@ -51,6 +52,7 @@
<a style="color: black" href="~/user/@announcement.Publisher.UserId">
@announcement.Publisher.Username
</a>
at @TimeZoneInfo.ConvertTime(announcement.PublishedAt, TimeZoneInfo.Utc, timeZoneInfo).ToString("M/d/yyyy h:mm:ss tt")
</div>
}
</div>

View file

@ -54,6 +54,7 @@ public class NotificationsPage : BaseLayout
Title = title.Trim(),
Content = content.Trim(),
PublisherId = user.UserId,
PublishedAt = DateTime.UtcNow,
};
this.Database.WebsiteAnnouncements.Add(announcement);

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,8 @@
@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;
bool canDelete = (bool?)ViewData["CanDelete"] ?? false;

View file

@ -13,7 +13,6 @@ using LBPUnion.ProjectLighthouse.Services;
using LBPUnion.ProjectLighthouse.Types.Mail;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Localization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.FileProviders;
#if !DEBUG
@ -39,11 +38,7 @@ public class WebsiteStartup
services.AddControllers();
services.AddRazorPages().WithRazorPagesAtContentRoot();
services.AddDbContext<DatabaseContext>(builder =>
{
builder.UseMySql(ServerConfiguration.Instance.DbConnectionString,
MySqlServerVersion.LatestSupportedServerVersion);
});
services.AddDbContext<DatabaseContext>(DatabaseContext.ConfigureBuilder());
IMailService mailService = ServerConfiguration.Instance.Mail.MailEnabled
? new MailQueueService(new SmtpMailSender())

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

@ -0,0 +1,969 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Tests.Helpers;
using LBPUnion.ProjectLighthouse.Types.Activity;
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 Xunit;
namespace ProjectLighthouse.Tests.GameApiTests.Unit.Activity;
[Trait("Category", "Unit")]
public class ActivityEventHandlerTests
{
#region Entity Inserts
[Fact]
public async Task Level_Insert_ShouldCreatePublishActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
SlotEntity slot = new()
{
CreatorId = 1,
SlotId = 1,
};
database.Slots.Add(slot);
await database.SaveChangesAsync();
eventHandler.OnEntityInserted(database, slot);
Assert.NotNull(database.Activities.OfType<LevelActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.PublishLevel && a.SlotId == 1));
}
[Fact]
public async Task LevelComment_Insert_ShouldCreateCommentActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
SlotEntity slot = new()
{
SlotId = 1,
CreatorId = 1,
};
database.Slots.Add(slot);
CommentEntity comment = new()
{
CommentId = 1,
PosterUserId = 1,
TargetSlotId = 1,
Type = CommentType.Level,
};
database.Comments.Add(comment);
await database.SaveChangesAsync();
eventHandler.OnEntityInserted(database, comment);
Assert.NotNull(database.Activities.ToList().OfType<CommentActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.CommentOnLevel && a.CommentId == 1));
}
[Fact]
public async Task ProfileComment_Insert_ShouldCreateCommentActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
CommentEntity comment = new()
{
CommentId = 1,
PosterUserId = 1,
TargetUserId = 1,
Type = CommentType.Profile,
};
database.Comments.Add(comment);
await database.SaveChangesAsync();
eventHandler.OnEntityInserted(database, comment);
Assert.NotNull(database.Activities.ToList().OfType<CommentActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.CommentOnUser && a.CommentId == 1));
}
[Fact]
public async Task Photo_Insert_ShouldCreatePhotoActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
},
new List<SlotEntity>
{
new()
{
SlotId = 1,
CreatorId = 1,
},
});
PhotoEntity photo = new()
{
PhotoId = 1,
CreatorId = 1,
SlotId = 1,
};
database.Photos.Add(photo);
await database.SaveChangesAsync();
eventHandler.OnEntityInserted(database, photo);
Assert.NotNull(database.Activities.ToList().OfType<PhotoActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.UploadPhoto && a.PhotoId == 1));
}
[Fact]
public async Task Score_Insert_ShouldCreateScoreActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
SlotEntity slot = new()
{
SlotId = 1,
CreatorId = 1,
};
database.Slots.Add(slot);
ScoreEntity score = new()
{
ScoreId = 1,
SlotId = 1,
UserId = 1,
};
database.Scores.Add(score);
await database.SaveChangesAsync();
eventHandler.OnEntityInserted(database, score);
Assert.NotNull(database.Activities.OfType<ScoreActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.Score && a.ScoreId == 1));
}
[Fact]
public async Task HeartedLevel_Insert_ShouldCreateLevelActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
SlotEntity slot = new()
{
SlotId = 1,
CreatorId = 1,
};
database.Slots.Add(slot);
await database.SaveChangesAsync();
HeartedLevelEntity heartedLevel = new()
{
HeartedLevelId = 1,
UserId = 1,
SlotId = 1,
Slot = slot,
};
eventHandler.OnEntityInserted(database, heartedLevel);
Assert.NotNull(database.Activities.OfType<LevelActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.HeartLevel && a.SlotId == 1));
}
[Fact]
public async Task HeartedLevel_InsertDuplicate_ShouldRemoveOldActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
SlotEntity slot = new()
{
SlotId = 1,
CreatorId = 1,
};
database.Slots.Add(slot);
LevelActivityEntity levelActivity = new()
{
UserId = 1,
SlotId = 1,
Type = EventType.HeartLevel,
Timestamp = DateTime.MinValue,
};
database.Activities.Add(levelActivity);
await database.SaveChangesAsync();
HeartedLevelEntity heartedLevel = new()
{
HeartedLevelId = 1,
UserId = 1,
SlotId = 1,
Slot = slot,
};
Assert.NotNull(database.Activities.OfType<LevelActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.HeartLevel && a.SlotId == 1 && a.Timestamp == DateTime.MinValue));
eventHandler.OnEntityInserted(database, heartedLevel);
Assert.NotNull(database.Activities.OfType<LevelActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.HeartLevel && a.SlotId == 1 && a.Timestamp != DateTime.MinValue));
}
[Fact]
public async Task HeartedProfile_Insert_ShouldCreateUserActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
HeartedProfileEntity heartedProfile = new()
{
HeartedProfileId = 1,
UserId = 1,
HeartedUserId = 1,
};
eventHandler.OnEntityInserted(database, heartedProfile);
Assert.NotNull(database.Activities.OfType<UserActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.HeartUser && a.TargetUserId == 1));
}
[Fact]
public async Task HeartedProfile_InsertDuplicate_ShouldRemoveOldActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
UserActivityEntity userActivity = new()
{
UserId = 1,
TargetUserId = 1,
Type = EventType.HeartUser,
Timestamp = DateTime.MinValue,
};
database.Activities.Add(userActivity);
await database.SaveChangesAsync();
Assert.NotNull(database.Activities.OfType<UserActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.HeartUser && a.TargetUserId == 1 && a.Timestamp == DateTime.MinValue));
HeartedProfileEntity heartedProfile = new()
{
HeartedProfileId = 1,
UserId = 1,
HeartedUserId = 1,
};
eventHandler.OnEntityInserted(database, heartedProfile);
Assert.NotNull(database.Activities.OfType<UserActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.HeartUser && a.TargetUserId == 1 && a.Timestamp != DateTime.MinValue));
}
[Fact]
public async Task HeartedPlaylist_Insert_ShouldCreatePlaylistActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
PlaylistEntity playlist = new()
{
PlaylistId = 1,
CreatorId = 1,
};
database.Playlists.Add(playlist);
await database.SaveChangesAsync();
HeartedPlaylistEntity heartedPlaylist = new()
{
HeartedPlaylistId = 1,
UserId = 1,
PlaylistId = 1,
};
eventHandler.OnEntityInserted(database, heartedPlaylist);
Assert.NotNull(database.Activities.OfType<PlaylistActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.HeartPlaylist && a.PlaylistId == 1));
}
[Fact]
public async Task HeartedPlaylist_InsertDuplicate_ShouldCreatePlaylistActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
PlaylistEntity playlist = new()
{
PlaylistId = 1,
CreatorId = 1,
};
database.Playlists.Add(playlist);
PlaylistActivityEntity playlistActivity = new()
{
UserId = 1,
PlaylistId = 1,
Type = EventType.HeartPlaylist,
Timestamp = DateTime.MinValue,
};
database.Activities.Add(playlistActivity);
await database.SaveChangesAsync();
HeartedPlaylistEntity heartedPlaylist = new()
{
HeartedPlaylistId = 1,
UserId = 1,
PlaylistId = 1,
};
Assert.NotNull(database.Activities.OfType<PlaylistActivityEntity>()
.FirstOrDefault(a =>
a.Type == EventType.HeartPlaylist && a.PlaylistId == 1 && a.Timestamp == DateTime.MinValue));
eventHandler.OnEntityInserted(database, heartedPlaylist);
Assert.NotNull(database.Activities.OfType<PlaylistActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.HeartPlaylist && a.PlaylistId == 1 && a.Timestamp != DateTime.MinValue));
}
[Fact]
public async Task VisitedLevel_Insert_ShouldCreateLevelActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
SlotEntity slot = new()
{
SlotId = 1,
CreatorId = 1,
};
database.Slots.Add(slot);
await database.SaveChangesAsync();
VisitedLevelEntity visitedLevel = new()
{
VisitedLevelId = 1,
UserId = 1,
SlotId = 1,
};
eventHandler.OnEntityInserted(database, visitedLevel);
Assert.NotNull(database.Activities.OfType<LevelActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.PlayLevel && a.SlotId == 1));
}
[Fact]
public async Task Review_Insert_ShouldCreateReviewActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
SlotEntity slot = new()
{
SlotId = 1,
CreatorId = 1,
};
database.Slots.Add(slot);
ReviewEntity review = new()
{
ReviewId = 1,
ReviewerId = 1,
SlotId = 1,
};
database.Reviews.Add(review);
await database.SaveChangesAsync();
eventHandler.OnEntityInserted(database, review);
Assert.NotNull(database.Activities.OfType<ReviewActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.ReviewLevel && a.ReviewId == 1));
}
[Fact]
public async Task RatedLevel_WithRatingInsert_ShouldCreateLevelActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
SlotEntity slot = new()
{
SlotId = 1,
CreatorId = 1,
};
database.Slots.Add(slot);
await database.SaveChangesAsync();
RatedLevelEntity ratedLevel = new()
{
RatedLevelId = 1,
UserId = 1,
SlotId = 1,
Rating = 1,
};
eventHandler.OnEntityInserted(database, ratedLevel);
Assert.NotNull(database.Activities.OfType<LevelActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.DpadRateLevel && a.SlotId == 1));
}
[Fact]
public async Task RatedLevel_WithLBP1RatingInsert_ShouldCreateLevelActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
SlotEntity slot = new()
{
SlotId = 1,
CreatorId = 1,
};
database.Slots.Add(slot);
await database.SaveChangesAsync();
RatedLevelEntity ratedLevel = new()
{
RatedLevelId = 1,
UserId = 1,
SlotId = 1,
RatingLBP1 = 5,
};
eventHandler.OnEntityInserted(database, ratedLevel);
Assert.NotNull(database.Activities.OfType<LevelActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.RateLevel && a.SlotId == 1));
}
[Fact]
public async Task Playlist_Insert_ShouldCreateLevelActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
PlaylistEntity playlist = new()
{
PlaylistId = 1,
CreatorId = 1,
};
database.Playlists.Add(playlist);
await database.SaveChangesAsync();
eventHandler.OnEntityInserted(database, playlist);
Assert.NotNull(database.Activities.OfType<PlaylistActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.CreatePlaylist && a.PlaylistId == 1));
}
#endregion
#region Entity changes
[Fact]
public async Task VisitedLevel_WithNoChange_ShouldNotCreateLevelActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
SlotEntity slot = new()
{
SlotId = 1,
CreatorId = 1,
};
database.Slots.Add(slot);
await database.SaveChangesAsync();
VisitedLevelEntity visitedLevel = new()
{
VisitedLevelId = 1,
UserId = 1,
SlotId = 1,
};
eventHandler.OnEntityChanged(database, visitedLevel, visitedLevel);
Assert.Null(database.Activities.OfType<LevelActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.PlayLevel && a.SlotId == 1));
}
[Fact]
public async Task VisitedLevel_WithChange_ShouldCreateLevelActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
SlotEntity slot = new()
{
SlotId = 1,
CreatorId = 1,
};
database.Slots.Add(slot);
await database.SaveChangesAsync();
VisitedLevelEntity oldVisitedLevel = new()
{
VisitedLevelId = 1,
UserId = 1,
SlotId = 1,
PlaysLBP2 = 1,
};
VisitedLevelEntity newVisitedLevel = new()
{
VisitedLevelId = 1,
UserId = 1,
SlotId = 1,
PlaysLBP2 = 2,
};
eventHandler.OnEntityChanged(database, oldVisitedLevel, newVisitedLevel);
Assert.NotNull(database.Activities.OfType<LevelActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.PlayLevel && a.SlotId == 1));
}
[Fact]
public async Task Slot_WithTeamPickChange_ShouldCreateLevelActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
SlotEntity oldSlot = new()
{
SlotId = 1,
CreatorId = 1,
};
database.Slots.Add(oldSlot);
await database.SaveChangesAsync();
SlotEntity newSlot = new()
{
SlotId = 1,
CreatorId = 1,
TeamPickTime = 1,
};
eventHandler.OnEntityChanged(database, oldSlot, newSlot);
Assert.NotNull(database.Activities.ToList().OfType<LevelActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.MMPickLevel && a.SlotId == 1));
}
[Fact]
public async Task Slot_WithRepublish_ShouldCreateLevelActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
SlotEntity oldSlot = new()
{
SlotId = 1,
CreatorId = 1,
};
database.Slots.Add(oldSlot);
await database.SaveChangesAsync();
SlotEntity newSlot = new()
{
SlotId = 1,
CreatorId = 1,
LastUpdated = 1,
};
eventHandler.OnEntityChanged(database, oldSlot, newSlot);
Assert.NotNull(database.Activities.ToList().OfType<LevelActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.PublishLevel && a.SlotId == 1));
}
[Fact]
public async Task Comment_WithDeletion_ShouldCreateCommentActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
CommentEntity oldComment = new()
{
CommentId = 1,
PosterUserId = 1,
Type = CommentType.Level,
};
database.Comments.Add(oldComment);
await database.SaveChangesAsync();
CommentEntity newComment = new()
{
CommentId = 1,
PosterUserId = 1,
Type = CommentType.Level,
Deleted = true,
};
eventHandler.OnEntityChanged(database, oldComment, newComment);
Assert.NotNull(database.Activities.ToList()
.FirstOrDefault(a => a.Type == EventType.DeleteLevelComment && ((CommentActivityEntity)a).CommentId == 1));
}
[Fact]
public async Task Playlist_WithSlotsChanged_ShouldCreatePlaylistWithSlotActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
SlotEntity slot = new()
{
SlotId = 1,
CreatorId = 1,
};
database.Slots.Add(slot);
PlaylistEntity oldPlaylist = new()
{
PlaylistId = 1,
CreatorId = 1,
};
database.Playlists.Add(oldPlaylist);
await database.SaveChangesAsync();
PlaylistEntity newPlaylist = new()
{
PlaylistId = 1,
CreatorId = 1,
SlotCollection = "1",
};
eventHandler.OnEntityChanged(database, oldPlaylist, newPlaylist);
Assert.NotNull(database.Activities.OfType<PlaylistWithSlotActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.AddLevelToPlaylist && a.PlaylistId == 1 && a.SlotId == 1));
}
#endregion
#region Entity deletion
[Fact]
public async Task HeartedLevel_Delete_ShouldCreateLevelActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
SlotEntity slot = new()
{
SlotId = 1,
CreatorId = 1,
};
database.Slots.Add(slot);
HeartedLevelEntity heartedLevel = new()
{
HeartedLevelId = 1,
UserId = 1,
SlotId = 1,
};
database.HeartedLevels.Add(heartedLevel);
await database.SaveChangesAsync();
eventHandler.OnEntityDeleted(database, heartedLevel);
Assert.NotNull(database.Activities.OfType<LevelActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.UnheartLevel && a.SlotId == 1));
}
[Fact]
public async Task HeartedLevel_DeleteDuplicate_ShouldRemoveOldActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
SlotEntity slot = new()
{
SlotId = 1,
CreatorId = 1,
};
database.Slots.Add(slot);
LevelActivityEntity levelActivity = new()
{
UserId = 1,
SlotId = 1,
Type = EventType.UnheartLevel,
Timestamp = DateTime.MinValue,
};
database.Activities.Add(levelActivity);
HeartedLevelEntity heartedLevel = new()
{
HeartedLevelId = 1,
UserId = 1,
SlotId = 1,
};
database.HeartedLevels.Add(heartedLevel);
await database.SaveChangesAsync();
eventHandler.OnEntityDeleted(database, heartedLevel);
Assert.NotNull(database.Activities.OfType<LevelActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.UnheartLevel && a.SlotId == 1 && a.Timestamp != DateTime.MinValue));
}
[Fact]
public async Task HeartedProfile_Delete_ShouldCreateLevelActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
SlotEntity slot = new()
{
SlotId = 1,
CreatorId = 1,
};
database.Slots.Add(slot);
HeartedProfileEntity heartedProfile = new()
{
HeartedProfileId = 1,
UserId = 1,
HeartedUserId = 1,
};
database.HeartedProfiles.Add(heartedProfile);
await database.SaveChangesAsync();
eventHandler.OnEntityDeleted(database, heartedProfile);
Assert.NotNull(database.Activities.OfType<UserActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.UnheartUser && a.UserId == 1));
}
[Fact]
public async Task HeartedProfile_DeleteDuplicate_ShouldCreateLevelActivity()
{
ActivityEntityEventHandler eventHandler = new();
DatabaseContext database = await MockHelper.GetTestDatabase(new List<UserEntity>
{
new()
{
Username = "test",
UserId = 1,
},
});
UserActivityEntity userActivity = new()
{
UserId = 1,
TargetUserId = 1,
Type = EventType.UnheartUser,
Timestamp = DateTime.MinValue,
};
database.Activities.Add(userActivity);
HeartedProfileEntity heartedProfile = new()
{
HeartedProfileId = 1,
UserId = 1,
HeartedUserId = 1,
};
database.HeartedProfiles.Add(heartedProfile);
await database.SaveChangesAsync();
Assert.NotNull(database.Activities.OfType<UserActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.UnheartUser && a.UserId == 1 && a.Timestamp == DateTime.MinValue));
eventHandler.OnEntityDeleted(database, heartedProfile);
Assert.NotNull(database.Activities.OfType<UserActivityEntity>()
.FirstOrDefault(a => a.Type == EventType.UnheartUser && a.UserId == 1 && a.Timestamp != DateTime.MinValue));
}
#endregion
}

View file

@ -0,0 +1,318 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Tests.Helpers;
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.Website;
using LBPUnion.ProjectLighthouse.Types.Users;
using Microsoft.EntityFrameworkCore;
using Xunit;
namespace ProjectLighthouse.Tests.GameApiTests.Unit.Activity;
[Trait("Category", "Unit")]
public class ActivityGroupingTests
{
[Fact]
public void ToOuterActivityGroups_ShouldCreateGroupPerObject_WhenGroupedBy_ObjectThenActor()
{
List<ActivityEntity> activities = [
new LevelActivityEntity
{
UserId = 1,
SlotId = 1,
Slot = new SlotEntity
{
GameVersion = GameVersion.LittleBigPlanet2,
},
Timestamp = DateTime.Now,
Type = EventType.PlayLevel,
},
new LevelActivityEntity
{
UserId = 1,
SlotId = 1,
Slot = new SlotEntity
{
GameVersion = GameVersion.LittleBigPlanet2,
},
Timestamp = DateTime.Now,
Type = EventType.ReviewLevel,
},
new LevelActivityEntity
{
UserId = 2,
SlotId = 1,
Slot = new SlotEntity
{
GameVersion = GameVersion.LittleBigPlanet2,
},
Timestamp = DateTime.Now,
Type = EventType.PlayLevel,
},
new UserActivityEntity
{
TargetUserId = 2,
UserId = 1,
Type = EventType.HeartUser,
Timestamp = DateTime.Now,
},
new UserActivityEntity
{
TargetUserId = 2,
UserId = 1,
Type = EventType.CommentOnUser,
Timestamp = DateTime.Now,
},
new UserActivityEntity
{
TargetUserId = 1,
UserId = 2,
Type = EventType.HeartUser,
Timestamp = DateTime.Now,
},
new UserActivityEntity
{
TargetUserId = 1,
UserId = 2,
Type = EventType.CommentOnUser,
Timestamp = DateTime.Now,
},
];
List<OuterActivityGroup> groups = activities.ToActivityDto().AsQueryable().ToActivityGroups().ToList().ToOuterActivityGroups();
Assert.NotNull(groups);
Assert.Equal(3, groups.Count);
Assert.Equal(ActivityGroupType.User, groups.ElementAt(0).Key.GroupType);
Assert.Equal(ActivityGroupType.User, groups.ElementAt(1).Key.GroupType);
Assert.Equal(ActivityGroupType.Level, groups.ElementAt(2).Key.GroupType);
Assert.Equal(1, groups.ElementAt(0).Key.TargetUserId);
Assert.Equal(2, groups.ElementAt(1).Key.TargetUserId);
Assert.Equal(1, groups.ElementAt(2).Key.TargetSlotId);
Assert.Single(groups.ElementAt(0).Groups);
Assert.Single(groups.ElementAt(1).Groups);
Assert.Equal(2, groups.ElementAt(2).Groups.Count);
}
[Fact]
public void ToOuterActivityGroups_ShouldCreateGroupPerObject_WhenGroupedBy_ActorThenObject()
{
List<ActivityEntity> activities = [
new LevelActivityEntity
{
UserId = 1,
SlotId = 1,
Slot = new SlotEntity
{
GameVersion = GameVersion.LittleBigPlanet2,
},
Timestamp = DateTime.Now,
Type = EventType.PlayLevel,
},
new LevelActivityEntity
{
UserId = 1,
SlotId = 1,
Slot = new SlotEntity
{
GameVersion = GameVersion.LittleBigPlanet2,
},
Timestamp = DateTime.Now,
Type = EventType.ReviewLevel,
},
new LevelActivityEntity
{
UserId = 2,
SlotId = 1,
Slot = new SlotEntity
{
GameVersion = GameVersion.LittleBigPlanet2,
},
Timestamp = DateTime.Now,
Type = EventType.PlayLevel,
},
new UserActivityEntity
{
TargetUserId = 2,
UserId = 1,
Type = EventType.HeartUser,
Timestamp = DateTime.Now,
},
new UserActivityEntity
{
TargetUserId = 2,
UserId = 1,
Type = EventType.CommentOnUser,
Timestamp = DateTime.Now,
},
new UserActivityEntity
{
TargetUserId = 1,
UserId = 2,
Type = EventType.HeartUser,
Timestamp = DateTime.Now,
},
new UserActivityEntity
{
TargetUserId = 1,
UserId = 2,
Type = EventType.CommentOnUser,
Timestamp = DateTime.Now,
},
];
List<OuterActivityGroup> groups = activities.ToActivityDto()
.AsQueryable()
.ToActivityGroups(true)
.ToList()
.ToOuterActivityGroups(true);
Assert.Multiple(() =>
{
Assert.NotNull(groups);
Assert.Equal(2, groups.Count);
Assert.Equal(1, groups.Count(g => g.Key.UserId == 1));
Assert.Equal(1, groups.Count(g => g.Key.UserId == 2));
OuterActivityGroup firstUserGroup = groups.FirstOrDefault(g => g.Key.UserId == 1);
OuterActivityGroup secondUserGroup = groups.FirstOrDefault(g => g.Key.UserId == 2);
Assert.NotNull(firstUserGroup.Groups);
Assert.NotNull(secondUserGroup.Groups);
Assert.Equal(ActivityGroupType.User, firstUserGroup.Key.GroupType);
Assert.Equal(ActivityGroupType.User, secondUserGroup.Key.GroupType);
Assert.True(firstUserGroup.Groups.All(g => g.Key.UserId == 1));
Assert.True(secondUserGroup.Groups.All(g => g.Key.UserId == 2));
});
}
[Fact]
public async Task ToActivityDtoTest()
{
DatabaseContext db = await MockHelper.GetTestDatabase();
db.Slots.Add(new SlotEntity
{
SlotId = 1,
CreatorId = 1,
GameVersion = GameVersion.LittleBigPlanet2,
});
db.Slots.Add(new SlotEntity
{
SlotId = 2,
CreatorId = 1,
GameVersion = GameVersion.LittleBigPlanet2,
TeamPickTime = 1,
});
db.Reviews.Add(new ReviewEntity
{
Timestamp = DateTime.Now.ToUnixTimeMilliseconds(),
SlotId = 1,
ReviewerId = 1,
ReviewId = 1,
});
db.Comments.Add(new CommentEntity
{
TargetSlotId = 1,
PosterUserId = 1,
Message = "comment on level test",
CommentId = 1,
});
db.Comments.Add(new CommentEntity
{
TargetUserId = 1,
PosterUserId = 1,
Message = "comment on user test",
CommentId = 2,
});
db.WebsiteAnnouncements.Add(new WebsiteAnnouncementEntity
{
PublisherId = 1,
AnnouncementId = 1,
});
db.Playlists.Add(new PlaylistEntity
{
PlaylistId = 1,
CreatorId = 1,
});
db.Activities.Add(new LevelActivityEntity
{
Timestamp = DateTime.Now,
SlotId = 1,
Type = EventType.PlayLevel,
UserId = 1,
});
db.Activities.Add(new ReviewActivityEntity
{
Timestamp = DateTime.Now,
SlotId = 1,
Type = EventType.ReviewLevel,
ReviewId = 1,
UserId = 1,
});
db.Activities.Add(new UserCommentActivityEntity
{
Timestamp = DateTime.Now,
Type = EventType.CommentOnUser,
UserId = 1,
TargetUserId = 1,
CommentId = 2,
});
db.Activities.Add(new LevelCommentActivityEntity
{
Timestamp = DateTime.Now,
Type = EventType.CommentOnLevel,
UserId = 1,
SlotId = 1,
CommentId = 1,
});
db.Activities.Add(new NewsActivityEntity
{
Type = EventType.NewsPost,
NewsId = 1,
UserId = 1,
});
db.Activities.Add(new PlaylistActivityEntity
{
Type = EventType.CreatePlaylist,
PlaylistId = 1,
UserId = 1,
});
db.Activities.Add(new LevelActivityEntity
{
Type = EventType.MMPickLevel,
SlotId = 2,
UserId = 1,
});
await db.SaveChangesAsync();
List<ActivityDto> resultDto = await db.Activities.ToActivityDto(includeSlotCreator: true, includeTeamPick: true).ToListAsync();
Assert.Equal(2, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.MMPickLevel)?.TargetTeamPickId);
Assert.Equal(2, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.MMPickLevel)?.TargetSlotId);
Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.MMPickLevel)?.TargetSlotCreatorId);
Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.CreatePlaylist)?.TargetPlaylistId);
Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.NewsPost)?.TargetNewsId);
Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.CommentOnUser)?.TargetUserId);
Assert.Null(resultDto.FirstOrDefault(a => a.Activity.Type == EventType.CommentOnLevel)?.TargetTeamPickId);
Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.CommentOnLevel)?.TargetSlotId);
Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.CommentOnLevel)?.TargetSlotCreatorId);
Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.ReviewLevel)?.TargetSlotId);
Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.ReviewLevel)?.TargetSlotCreatorId);
Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.PlayLevel)?.TargetSlotId);
Assert.Equal(1, resultDto.FirstOrDefault(a => a.Activity.Type == EventType.PlayLevel)?.TargetSlotCreatorId);
}
}

View file

@ -0,0 +1,83 @@
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Tests.Helpers;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using Microsoft.EntityFrameworkCore;
using Moq;
using Xunit;
namespace ProjectLighthouse.Tests.GameApiTests.Unit.Activity;
[Trait("Category", "Unit")]
public class ActivityInterceptorTests
{
private static async Task<DatabaseContext> GetTestDatabase(IMock<IEntityEventHandler> eventHandlerMock)
{
DbContextOptionsBuilder<DatabaseContext> optionsBuilder = await MockHelper.GetInMemoryDbOptions();
optionsBuilder.AddInterceptors(new ActivityInterceptor(eventHandlerMock.Object));
DatabaseContext database = new(optionsBuilder.Options);
await database.Database.EnsureCreatedAsync();
return database;
}
[Fact]
public async Task SaveChangesWithNewEntity_ShouldCallEntityInserted()
{
Mock<IEntityEventHandler> eventHandlerMock = new();
DatabaseContext database = await GetTestDatabase(eventHandlerMock);
database.Users.Add(new UserEntity
{
UserId = 1,
Username = "test",
});
await database.SaveChangesAsync();
eventHandlerMock.Verify(x => x.OnEntityInserted(It.IsAny<DatabaseContext>(), It.Is<object>(user => user is UserEntity)), Times.Once);
}
[Fact]
public async Task SaveChangesWithModifiedEntity_ShouldCallEntityChanged()
{
Mock<IEntityEventHandler> eventHandlerMock = new();
DatabaseContext database = await GetTestDatabase(eventHandlerMock);
UserEntity user = new()
{
Username = "test",
};
database.Users.Add(user);
await database.SaveChangesAsync();
user.Username = "test2";
await database.SaveChangesAsync();
eventHandlerMock.Verify(x => x.OnEntityChanged(It.IsAny<DatabaseContext>(),
It.Is<object>(u => u is UserEntity && ((UserEntity)u).Username == "test"),
It.Is<object>(u => u is UserEntity && ((UserEntity)u).Username == "test2")),
Times.Once);
}
[Fact]
public async Task SaveChangesWithModifiedEntity_ShouldCallEntityDeleted()
{
Mock<IEntityEventHandler> eventHandlerMock = new();
DatabaseContext database = await GetTestDatabase(eventHandlerMock);
UserEntity user = new()
{
Username = "test",
};
database.Users.Add(user);
await database.SaveChangesAsync();
database.Users.Remove(user);
await database.SaveChangesAsync();
eventHandlerMock.Verify(x => x.OnEntityDeleted(It.IsAny<DatabaseContext>(), It.Is<object>(u => u is UserEntity)), Times.Once);
}
}

View file

@ -0,0 +1,39 @@
using System.Threading.Tasks;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers;
using LBPUnion.ProjectLighthouse.Tests.Helpers;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Serialization.Activity;
using LBPUnion.ProjectLighthouse.Types.Users;
using Microsoft.AspNetCore.Mvc;
using Xunit;
namespace ProjectLighthouse.Tests.GameApiTests.Unit.Controllers;
[Trait("Category", "Unit")]
public class ActivityControllerTests
{
private static void SetupToken(ControllerBase controller, GameVersion version)
{
GameTokenEntity token = MockHelper.GetUnitTestToken();
token.GameVersion = version;
controller.SetupTestController(token);
}
[Fact]
public async Task LBP2GlobalActivity_ShouldReturnNothing_WhenEmpty()
{
DatabaseContext database = await MockHelper.GetTestDatabase();
ActivityController activityController = new(database);
SetupToken(activityController, GameVersion.LittleBigPlanet2);
long timestamp = TimeHelper.TimestampMillis;
IActionResult response = await activityController.GlobalActivity(timestamp, 0, false, false, false, false, false);
GameStream stream = response.CastTo<OkObjectResult, GameStream>();
Assert.Null(stream.Groups);
Assert.Equal(timestamp, stream.StartTimestamp);
}
//TODO write activity controller tests
}

View file

@ -1,5 +1,5 @@
using LBPUnion.ProjectLighthouse.Filter;
using LBPUnion.ProjectLighthouse.Filter.Filters;
using LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Slots;
using LBPUnion.ProjectLighthouse.Servers.GameServer.Extensions;
using LBPUnion.ProjectLighthouse.Tests.Helpers;

View file

@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Slots;
using LBPUnion.ProjectLighthouse.Tests.Helpers;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Serialization;
using LBPUnion.ProjectLighthouse.Types.Serialization.Review;
using LBPUnion.ProjectLighthouse.Types.Users;
using Microsoft.AspNetCore.Mvc;
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

@ -50,7 +50,7 @@ public static class MockHelper
return finalResult;
}
private static async Task<DbContextOptionsBuilder<DatabaseContext>> GetInMemoryDbOptions()
public static async Task<DbContextOptionsBuilder<DatabaseContext>> GetInMemoryDbOptions()
{
DbConnection connection = new SqliteConnection("DataSource=:memory:");
await connection.OpenAsync();

View file

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using LBPUnion.ProjectLighthouse.Filter;
using LBPUnion.ProjectLighthouse.Filter.Filters;
using LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Levels;

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,151 @@
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 struct TrackedEntityKey
{
public Type Type { get; set; }
public int HashCode { get; set; }
public Guid ContextId { get; set; }
}
private readonly ConcurrentDictionary<TrackedEntityKey, CustomTrackedEntity> unsavedEntities;
private readonly IEntityEventHandler eventHandler;
public ActivityInterceptor(IEntityEventHandler eventHandler)
{
this.eventHandler = eventHandler;
this.unsavedEntities = new ConcurrentDictionary<TrackedEntityKey, 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(new TrackedEntityKey
{
ContextId = context.ContextId.InstanceId,
Type = entry.Entity.GetType(),
HashCode = 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 = [];
List<EntityEntry> entries = context.ChangeTracker.Entries().ToList();
foreach (KeyValuePair<TrackedEntityKey, 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,7 @@
using System;
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;
using LBPUnion.ProjectLighthouse.Types.Entities.Maintenance;
@ -26,6 +29,10 @@ public partial class DatabaseContext : DbContext
public DbSet<WebTokenEntity> WebTokens { get; set; }
#endregion
#region Activity
public DbSet<ActivityEntity> Activities { get; set; }
#endregion
#region Users
public DbSet<CommentEntity> Comments { get; set; }
public DbSet<LastContactEntity> LastContacts { get; set; }
@ -84,8 +91,36 @@ public partial class DatabaseContext : DbContext
public static DatabaseContext CreateNewInstance()
{
DbContextOptionsBuilder<DatabaseContext> builder = new();
builder.UseMySql(ServerConfiguration.Instance.DbConnectionString,
MySqlServerVersion.LatestSupportedServerVersion);
ConfigureBuilder()(builder);
return new DatabaseContext(builder.Options);
}
public static Action<DbContextOptionsBuilder> ConfigureBuilder()
{
return builder =>
{
builder.UseMySql(ServerConfiguration.Instance.DbConnectionString,
MySqlServerVersion.LatestSupportedServerVersion);
builder.AddInterceptors(new ActivityInterceptor(new ActivityEntityEventHandler()));
};
}
#region Activity
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<LevelActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<UserPhotoActivity>().UseTphMappingStrategy();
modelBuilder.Entity<LevelPhotoActivity>().UseTphMappingStrategy();
modelBuilder.Entity<PlaylistActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<PlaylistWithSlotActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<ScoreActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<UserActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<NewsActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<LevelCommentActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<UserCommentActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<UserActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<ReviewActivityEntity>().UseTphMappingStrategy();
base.OnModelCreating(modelBuilder);
}
#endregion
}

View file

@ -0,0 +1,129 @@
using System.Collections.Generic;
using System.Linq;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Activity;
namespace LBPUnion.ProjectLighthouse.Extensions;
public static class ActivityQueryExtensions
{
public static List<int> GetIds(this IReadOnlyCollection<OuterActivityGroup> groups, ActivityGroupType type)
{
List<int> ids = [];
// Add outer group ids
ids.AddRange(groups.Where(g => g.Key.GroupType == type)
.Where(g => g.Key.TargetId != 0)
.Select(g => g.Key.TargetId)
.ToList());
// Add specific event ids
ids.AddRange(groups.SelectMany(g =>
g.Groups.SelectMany(gr => gr.Where(a => a.GroupType == type).Select(a => a.TargetId))));
if (type == ActivityGroupType.User)
{
ids.AddRange(groups.Where(g => g.Key.GroupType is not ActivityGroupType.News)
.SelectMany(g => g.Groups.Select(a => a.Key.UserId)));
}
return ids.Distinct().ToList();
}
/// <summary>
/// Turns a list of <see cref="ActivityDto"/> into a group based on its timestamp
/// </summary>
/// <param name="activityQuery">An <see cref="IQueryable{ActivityDto}"/> to group</param>
/// <param name="groupByActor">Whether the groups should be created based on the initiator of the event or the target of the event</param>
/// <returns>The transformed query containing groups of <see cref="ActivityDto"/></returns>
public static IQueryable<IGrouping<ActivityGroup, ActivityDto>> ToActivityGroups
(this IQueryable<ActivityDto> activityQuery, bool groupByActor = false) =>
groupByActor
? activityQuery.GroupBy(dto => new ActivityGroup
{
Timestamp = dto.Activity.Timestamp.Date,
UserId = dto.Activity.UserId,
TargetNewsId = dto.TargetNewsId ?? -1,
TargetTeamPickSlotId = dto.TargetTeamPickId ?? -1,
})
: activityQuery.GroupBy(dto => new ActivityGroup
{
Timestamp = dto.Activity.Timestamp.Date,
UserId = -1,
TargetUserId = dto.TargetUserId ?? -1,
TargetSlotId = dto.TargetSlotId ?? -1,
TargetPlaylistId = dto.TargetPlaylistId ?? -1,
TargetNewsId = dto.TargetNewsId ?? -1,
});
public static List<OuterActivityGroup> ToOuterActivityGroups
(this IEnumerable<IGrouping<ActivityGroup, ActivityDto>> activityGroups, bool groupByActor = false) =>
// Pin news posts to the top
activityGroups.OrderByDescending(g => g.Key.GroupType == ActivityGroupType.News ? 1 : 0)
.ThenByDescending(g => g.MaxBy(a => a.Activity.Timestamp)?.Activity.Timestamp ?? g.Key.Timestamp)
.Select(g => new OuterActivityGroup
{
Key = g.Key,
Groups = g.OrderByDescending(a => a.Activity.Timestamp)
.GroupBy(gr => new InnerActivityGroup
{
Type = groupByActor
? gr.GroupType
: gr.GroupType == ActivityGroupType.News
? ActivityGroupType.News
: ActivityGroupType.User,
UserId = gr.Activity.UserId,
TargetId = groupByActor
? gr.TargetId
: gr.GroupType == ActivityGroupType.News
? gr.TargetNewsId ?? 0
: gr.Activity.UserId,
})
.ToList(),
})
.ToList();
/// <summary>
/// Converts an <see cref="IQueryable"/>&lt;<see cref="ActivityEntity"/>&gt; into an <see cref="IQueryable"/>&lt;<see cref="ActivityDto"/>&gt; for grouping.
/// </summary>
/// <param name="activityQuery">The activity query to be converted.</param>
/// <param name="includeSlotCreator">Whether the <see cref="ActivityDto.TargetSlotCreatorId"/> field should be included.</param>
/// <param name="includeTeamPick">Whether the <see cref="ActivityDto.TargetTeamPickId"/> field should be included.</param>
/// <returns>The converted <see cref="IQueryable"/>&lt;<see cref="ActivityDto"/>&gt;</returns>
public static IQueryable<ActivityDto> ToActivityDto
(this IQueryable<ActivityEntity> activityQuery, bool includeSlotCreator = false, bool includeTeamPick = false)
{
return activityQuery.Select(a => new ActivityDto
{
Activity = a,
TargetSlotId = (a as LevelActivityEntity).SlotId,
TargetSlotGameVersion = (a as LevelActivityEntity).Slot.GameVersion,
TargetSlotCreatorId = includeSlotCreator ? (a as LevelActivityEntity).Slot.CreatorId : null,
TargetUserId = (a as UserActivityEntity).TargetUserId,
TargetNewsId = (a as NewsActivityEntity).NewsId,
TargetPlaylistId = (a as PlaylistActivityEntity).PlaylistId,
TargetTeamPickId =
includeTeamPick && a.Type == EventType.MMPickLevel ? (a as LevelActivityEntity).SlotId : null, });
}
/// <summary>
/// Converts an IEnumerable&lt;<see cref="ActivityEntity"/>&gt; into an IEnumerable&lt;<see cref="ActivityDto"/>&gt; for grouping.
/// </summary>
/// <param name="activityEnumerable">The activity query to be converted.</param>
/// <param name="includeSlotCreator">Whether the <see cref="ActivityDto.TargetSlotCreatorId"/> field should be included.</param>
/// <param name="includeTeamPick">Whether the <see cref="ActivityDto.TargetTeamPickId"/> field should be included.</param>
/// <returns>The converted IEnumerable&lt;<see cref="ActivityDto"/>&gt;</returns>
public static IEnumerable<ActivityDto> ToActivityDto
(this IEnumerable<ActivityEntity> activityEnumerable, bool includeSlotCreator = false, bool includeTeamPick = false)
{
return activityEnumerable.Select(a => new ActivityDto
{
Activity = a,
TargetSlotId = (a as LevelActivityEntity)?.SlotId,
TargetSlotGameVersion = (a as LevelActivityEntity)?.Slot.GameVersion,
TargetSlotCreatorId = includeSlotCreator ? (a as LevelActivityEntity)?.Slot.CreatorId : null,
TargetUserId = (a as UserActivityEntity)?.TargetUserId,
TargetNewsId = (a as NewsActivityEntity)?.NewsId,
TargetPlaylistId = (a as PlaylistActivityEntity)?.PlaylistId,
TargetTeamPickId =
includeTeamPick && a.Type == EventType.MMPickLevel ? (a as LevelActivityEntity)?.SlotId : null, });
}
}

View file

@ -44,6 +44,7 @@ public static partial class ControllerExtensions
public static async Task<T?> DeserializeBody<T>(this ControllerBase controller, params string[] rootElements)
{
string bodyString = await controller.ReadBodyAsync();
if (bodyString.Length == 0) return default;
try
{
// Prevent unescaped ampersands from causing deserialization to fail

View file

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

View file

@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter;
public class ActivityQueryBuilder : IQueryBuilder<ActivityDto>
{
private readonly List<IActivityFilter> filters;
public ActivityQueryBuilder()
{
this.filters = new List<IActivityFilter>();
}
public Expression<Func<ActivityDto, bool>> Build()
{
Expression<Func<ActivityDto, bool>> predicate = PredicateExtensions.True<ActivityDto>();
predicate = this.filters.Aggregate(predicate, (current, filter) => current.And(filter.GetPredicate()));
return predicate;
}
public ActivityQueryBuilder RemoveFilter(Type type)
{
this.filters.RemoveAll(f => f.GetType() == type);
return this;
}
#nullable enable
public IEnumerable<IActivityFilter> GetFilters(Type type) => this.filters.Where(f => f.GetType() == type).ToList();
#nullable disable
public ActivityQueryBuilder AddFilter(int index, IActivityFilter filter)
{
this.filters.Insert(index, filter);
return this;
}
public ActivityQueryBuilder Clone()
{
ActivityQueryBuilder clone = new();
clone.filters.AddRange(this.filters);
return clone;
}
public ActivityQueryBuilder AddFilter(IActivityFilter filter)
{
this.filters.Add(filter);
return this;
}
}

View file

@ -0,0 +1,27 @@
#nullable enable
using System;
using System.Linq;
using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Activity;
public class EventTypeFilter : IActivityFilter
{
private readonly EventType[] events;
public EventTypeFilter(params EventType[] events)
{
this.events = events;
}
public Expression<Func<ActivityDto, bool>> GetPredicate()
{
Expression<Func<ActivityDto, bool>> predicate = PredicateExtensions.False<ActivityDto>();
predicate = this.events.Aggregate(predicate,
(current, eventType) => current.Or(a => a.Activity.Type == eventType));
return predicate;
}
}

View file

@ -0,0 +1,12 @@
using System;
using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Activity;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Activity;
public class ExcludeNewsFilter : IActivityFilter
{
public Expression<Func<ActivityDto, bool>> GetPredicate() => a => a.Activity is NewsActivityEntity && a.Activity.Type != EventType.NewsPost;
}

View file

@ -0,0 +1,14 @@
using System;
using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Activity;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Activity;
public class IncludeNewsFilter : IActivityFilter
{
public Expression<Func<ActivityDto, bool>> GetPredicate() =>
a => (a.Activity is NewsActivityEntity && a.Activity.Type == EventType.NewsPost) ||
a.Activity.Type == EventType.MMPickLevel;
}

View file

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Activity;
public class IncludeUserIdFilter : IActivityFilter
{
private readonly IEnumerable<int> userIds;
private readonly EventTypeFilter eventFilter;
public IncludeUserIdFilter(IEnumerable<int> userIds, EventTypeFilter eventFilter = null)
{
this.userIds = userIds;
this.eventFilter = eventFilter;
}
public Expression<Func<ActivityDto, bool>> GetPredicate()
{
Expression<Func<ActivityDto, bool>> predicate = PredicateExtensions.False<ActivityDto>();
predicate = this.userIds.Aggregate(predicate, (current, friendId) => current.Or(a => a.Activity.UserId == friendId));
if (this.eventFilter != null) predicate = predicate.And(this.eventFilter.GetPredicate());
return predicate;
}
}

View file

@ -0,0 +1,27 @@
using System;
using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Activity;
public class MyLevelActivityFilter : IActivityFilter
{
private readonly int userId;
private readonly EventTypeFilter eventFilter;
public MyLevelActivityFilter(int userId, EventTypeFilter eventFilter = null)
{
this.userId = userId;
this.eventFilter = eventFilter;
}
public Expression<Func<ActivityDto, bool>> GetPredicate()
{
Expression<Func<ActivityDto, bool>> predicate = PredicateExtensions.False<ActivityDto>();
predicate = predicate.Or(a => a.TargetSlotCreatorId == this.userId);
if (this.eventFilter != null) predicate = predicate.And(this.eventFilter.GetPredicate());
return predicate;
}
}

View file

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Activity;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Activity;
public class PlaylistActivityFilter : IActivityFilter
{
private readonly List<int> playlistIds;
private readonly EventTypeFilter eventFilter;
public PlaylistActivityFilter(List<int> playlistIds, EventTypeFilter eventFilter = null)
{
this.playlistIds = playlistIds;
this.eventFilter = eventFilter;
}
public Expression<Func<ActivityDto, bool>> GetPredicate()
{
Expression<Func<ActivityDto, bool>> predicate = PredicateExtensions.False<ActivityDto>();
predicate = this.playlistIds.Aggregate(predicate, (current, playlistId) => current.Or(a => (a.Activity is PlaylistActivityEntity || a.Activity is PlaylistWithSlotActivityEntity) && a.TargetPlaylistId == playlistId));
if (this.eventFilter != null) predicate = predicate.And(this.eventFilter.GetPredicate());
return predicate;
}
}

View file

@ -3,7 +3,7 @@ using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
public class AdventureFilter : ISlotFilter
{

View file

@ -5,7 +5,7 @@ using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
public class AuthorLabelFilter : ISlotFilter
{
@ -20,7 +20,7 @@ public class AuthorLabelFilter : ISlotFilter
{
Expression<Func<SlotEntity, bool>> predicate = PredicateExtensions.True<SlotEntity>();
predicate = this.labels.Aggregate(predicate,
(current, label) => current.And(s => s.AuthorLabels.Contains(label)));
(current, label) => PredicateExtensions.And<SlotEntity>(current, s => s.AuthorLabels.Contains(label)));
return predicate;
}
}

View file

@ -3,7 +3,7 @@ using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
public class CreatorFilter : ISlotFilter
{

View file

@ -3,7 +3,7 @@ using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
public class CrossControlFilter : ISlotFilter
{

View file

@ -3,7 +3,7 @@ using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
public class ExcludeAdventureFilter : ISlotFilter
{

View file

@ -3,7 +3,7 @@ using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
public class ExcludeCrossControlFilter : ISlotFilter
{

View file

@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Filter;
using LBPUnion.ProjectLighthouse.Types.Users;
namespace LBPUnion.ProjectLighthouse.Filter.Filters;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
public class ExcludeLBP1OnlyFilter : ISlotFilter
{

View file

@ -3,7 +3,7 @@ using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
public class ExcludeMovePackFilter : ISlotFilter
{

View file

@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
public class FirstUploadedFilter : ISlotFilter
{

View file

@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Filter;
using LBPUnion.ProjectLighthouse.Types.Users;
namespace LBPUnion.ProjectLighthouse.Filter.Filters;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
public class GameVersionFilter : ISlotFilter
{

View file

@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Filter;
using LBPUnion.ProjectLighthouse.Types.Users;
namespace LBPUnion.ProjectLighthouse.Filter.Filters;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
public class GameVersionListFilter : ISlotFilter
{
@ -19,5 +19,5 @@ public class GameVersionListFilter : ISlotFilter
public Expression<Func<SlotEntity, bool>> GetPredicate() =>
this.versions.Aggregate(PredicateExtensions.False<SlotEntity>(),
(current, version) => current.Or(s => s.GameVersion == version));
(current, version) => PredicateExtensions.Or<SlotEntity>(current, s => s.GameVersion == version));
}

View file

@ -3,7 +3,7 @@ using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
public class HiddenSlotFilter : ISlotFilter
{

View file

@ -3,7 +3,7 @@ using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
public class MovePackFilter : ISlotFilter
{

View file

@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
public class PlayerCountFilter : ISlotFilter
{

View file

@ -4,7 +4,7 @@ using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
public class ResultTypeFilter : ISlotFilter
{

View file

@ -6,7 +6,7 @@ using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
public class SlotIdFilter : ISlotFilter
{
@ -20,7 +20,7 @@ public class SlotIdFilter : ISlotFilter
public Expression<Func<SlotEntity, bool>> GetPredicate()
{
Expression<Func<SlotEntity, bool>> predicate = PredicateExtensions.False<SlotEntity>();
predicate = this.slotIds.Aggregate(predicate, (current, slotId) => current.Or(s => s.SlotId == slotId));
predicate = this.slotIds.Aggregate(predicate, (current, slotId) => PredicateExtensions.Or<SlotEntity>(current, s => s.SlotId == slotId));
return predicate;
}
}

View file

@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Filter;
using LBPUnion.ProjectLighthouse.Types.Levels;
namespace LBPUnion.ProjectLighthouse.Filter.Filters;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
public class SlotTypeFilter : ISlotFilter
{

View file

@ -3,7 +3,7 @@ using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
public class SubLevelFilter : ISlotFilter
{

View file

@ -3,7 +3,7 @@ using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
public class TeamPickFilter : ISlotFilter
{

View file

@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Filter;
namespace LBPUnion.ProjectLighthouse.Filter.Filters;
namespace LBPUnion.ProjectLighthouse.Filter.Filters.Slot;
public class TextFilter : ISlotFilter
{

View file

@ -0,0 +1,692 @@
using System;
using LBPUnion.ProjectLighthouse.Database;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LBPUnion.ProjectLighthouse.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20240514032512_AddRecentActivity")]
public partial class AddRecentActivity : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<int>(
name: "TokenId",
table: "WebTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "AnnouncementId",
table: "WebsiteAnnouncements",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "VisitedLevelId",
table: "VisitedLevels",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "UserId",
table: "Users",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "SlotId",
table: "Slots",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "ScoreId",
table: "Scores",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "ReviewId",
table: "Reviews",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "ReportId",
table: "Reports",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "TokenId",
table: "RegistrationTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "RatedReviewId",
table: "RatedReviews",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "RatedLevelId",
table: "RatedLevels",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "RatingId",
table: "RatedComments",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "QueuedLevelId",
table: "QueuedLevels",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "PlaylistId",
table: "Playlists",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "PlatformLinkAttemptId",
table: "PlatformLinkAttempts",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "PhotoSubjectId",
table: "PhotoSubjects",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "PhotoId",
table: "Photos",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "TokenId",
table: "PasswordResetTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "Id",
table: "Notifications",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "HeartedProfileId",
table: "HeartedProfiles",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "HeartedPlaylistId",
table: "HeartedPlaylists",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "HeartedLevelId",
table: "HeartedLevels",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "TokenId",
table: "GameTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "EmailVerificationTokenId",
table: "EmailVerificationTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "EmailSetTokenId",
table: "EmailSetTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "CategoryId",
table: "CustomCategories",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "CommentId",
table: "Comments",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "CaseId",
table: "Cases",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "BlockedProfileId",
table: "BlockedProfiles",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "Id",
table: "APIKeys",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.CreateTable(
name: "Activities",
columns: table => new
{
ActivityId = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Timestamp = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UserId = table.Column<int>(type: "int", nullable: false),
Type = table.Column<int>(type: "int", nullable: false),
Discriminator = table.Column<string>(type: "varchar(34)", maxLength: 34, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
SlotId = table.Column<int>(type: "int", nullable: true),
CommentId = table.Column<int>(type: "int", nullable: true),
PhotoId = table.Column<int>(type: "int", nullable: true),
NewsId = table.Column<int>(type: "int", nullable: true),
PlaylistId = table.Column<int>(type: "int", nullable: true),
ReviewId = table.Column<int>(type: "int", nullable: true),
ScoreId = table.Column<int>(type: "int", nullable: true),
Points = table.Column<int>(type: "int", nullable: true),
TargetUserId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Activities", x => x.ActivityId);
table.ForeignKey(
name: "FK_Activities_Comments_CommentId",
column: x => x.CommentId,
principalTable: "Comments",
principalColumn: "CommentId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Activities_Photos_PhotoId",
column: x => x.PhotoId,
principalTable: "Photos",
principalColumn: "PhotoId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Activities_Playlists_PlaylistId",
column: x => x.PlaylistId,
principalTable: "Playlists",
principalColumn: "PlaylistId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Activities_Reviews_ReviewId",
column: x => x.ReviewId,
principalTable: "Reviews",
principalColumn: "ReviewId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Activities_Scores_ScoreId",
column: x => x.ScoreId,
principalTable: "Scores",
principalColumn: "ScoreId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Activities_Slots_SlotId",
column: x => x.SlotId,
principalTable: "Slots",
principalColumn: "SlotId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Activities_Users_TargetUserId",
column: x => x.TargetUserId,
principalTable: "Users",
principalColumn: "UserId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Activities_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "UserId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Activities_WebsiteAnnouncements_NewsId",
column: x => x.NewsId,
principalTable: "WebsiteAnnouncements",
principalColumn: "AnnouncementId",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_Activities_CommentId",
table: "Activities",
column: "CommentId");
migrationBuilder.CreateIndex(
name: "IX_Activities_NewsId",
table: "Activities",
column: "NewsId");
migrationBuilder.CreateIndex(
name: "IX_Activities_PhotoId",
table: "Activities",
column: "PhotoId");
migrationBuilder.CreateIndex(
name: "IX_Activities_PlaylistId",
table: "Activities",
column: "PlaylistId");
migrationBuilder.CreateIndex(
name: "IX_Activities_ReviewId",
table: "Activities",
column: "ReviewId");
migrationBuilder.CreateIndex(
name: "IX_Activities_ScoreId",
table: "Activities",
column: "ScoreId");
migrationBuilder.CreateIndex(
name: "IX_Activities_SlotId",
table: "Activities",
column: "SlotId");
migrationBuilder.CreateIndex(
name: "IX_Activities_TargetUserId",
table: "Activities",
column: "TargetUserId");
migrationBuilder.CreateIndex(
name: "IX_Activities_UserId",
table: "Activities",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Activities");
migrationBuilder.AlterColumn<int>(
name: "TokenId",
table: "WebTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "AnnouncementId",
table: "WebsiteAnnouncements",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "VisitedLevelId",
table: "VisitedLevels",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "UserId",
table: "Users",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "SlotId",
table: "Slots",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "ScoreId",
table: "Scores",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "ReviewId",
table: "Reviews",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "ReportId",
table: "Reports",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "TokenId",
table: "RegistrationTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "RatedReviewId",
table: "RatedReviews",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "RatedLevelId",
table: "RatedLevels",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "RatingId",
table: "RatedComments",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "QueuedLevelId",
table: "QueuedLevels",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "PlaylistId",
table: "Playlists",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "PlatformLinkAttemptId",
table: "PlatformLinkAttempts",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "PhotoSubjectId",
table: "PhotoSubjects",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "PhotoId",
table: "Photos",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "TokenId",
table: "PasswordResetTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "Id",
table: "Notifications",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "HeartedProfileId",
table: "HeartedProfiles",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "HeartedPlaylistId",
table: "HeartedPlaylists",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "HeartedLevelId",
table: "HeartedLevels",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "TokenId",
table: "GameTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "EmailVerificationTokenId",
table: "EmailVerificationTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "EmailSetTokenId",
table: "EmailSetTokens",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "CategoryId",
table: "CustomCategories",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "CommentId",
table: "Comments",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "CaseId",
table: "Cases",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "BlockedProfileId",
table: "BlockedProfiles",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
migrationBuilder.AlterColumn<int>(
name: "Id",
table: "APIKeys",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.OldAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
}
}
}

View file

@ -0,0 +1,33 @@
using System;
using LBPUnion.ProjectLighthouse.Database;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LBPUnion.ProjectLighthouse.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20240514032620_AddPublishedAtToWebAnnouncement")]
public partial class AddPublishedAtToWebAnnouncement : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "PublishedAt",
table: "WebsiteAnnouncements",
type: "datetime(6)",
nullable: false,
defaultValue: DateTime.UtcNow);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PublishedAt",
table: "WebsiteAnnouncements");
}
}
}

View file

@ -1,5 +1,6 @@
#nullable enable
using System.Collections.Generic;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Types.Matchmaking.Rooms;
namespace LBPUnion.ProjectLighthouse.StorableLists.Stores;
@ -10,7 +11,7 @@ public static class RoomStore
public static StorableList<Room> GetRooms()
{
if (RedisDatabase.Initialized)
if (!ServerStatics.IsUnitTesting && RedisDatabase.Initialized)
{
return new RedisStorableList<Room>(RedisDatabase.GetRooms());
}

View file

@ -0,0 +1,35 @@
using LBPUnion.ProjectLighthouse.Types.Entities.Activity;
using LBPUnion.ProjectLighthouse.Types.Users;
namespace LBPUnion.ProjectLighthouse.Types.Activity;
public class ActivityDto
{
public required ActivityEntity Activity { get; set; }
public int? TargetSlotId { get; set; }
public int? TargetSlotCreatorId { get; set; }
public GameVersion? TargetSlotGameVersion { get; set; }
public int? TargetUserId { get; set; }
public int? TargetPlaylistId { get; set; }
public int? TargetNewsId { get; set; }
public int? TargetTeamPickId { get; set; }
public int TargetId =>
this.GroupType switch
{
ActivityGroupType.User => this.TargetUserId ?? -1,
ActivityGroupType.Level => this.TargetSlotId ?? -1,
ActivityGroupType.Playlist => this.TargetPlaylistId ?? -1,
ActivityGroupType.News => this.TargetNewsId ?? -1,
_ => this.Activity.UserId,
};
public ActivityGroupType GroupType =>
this.TargetPlaylistId != null
? ActivityGroupType.Playlist
: this.TargetNewsId != null
? ActivityGroupType.News
: this.TargetSlotId != null
? ActivityGroupType.Level
: ActivityGroupType.User;
}

View file

@ -0,0 +1,385 @@
#nullable enable
using System;
using System.Linq;
using System.Linq.Expressions;
using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Logging;
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.Entities.Website;
using LBPUnion.ProjectLighthouse.Types.Levels;
using LBPUnion.ProjectLighthouse.Types.Logging;
using Microsoft.EntityFrameworkCore;
namespace LBPUnion.ProjectLighthouse.Types.Activity;
public class ActivityEntityEventHandler : IEntityEventHandler
{
public void OnEntityInserted<T>(DatabaseContext database, T entity) where T : class
{
ActivityEntity? activity = entity switch
{
SlotEntity slot => slot.Type switch
{
SlotType.User => new LevelActivityEntity
{
Type = EventType.PublishLevel,
SlotId = slot.SlotId,
UserId = slot.CreatorId,
},
_ => null,
},
CommentEntity comment => comment.Type switch
{
CommentType.Level => database.Slots.Where(s => s.SlotId == comment.TargetSlotId)
.Select(s => s.Type)
.FirstOrDefault() switch
{
SlotType.User => new LevelCommentActivityEntity
{
Type = EventType.CommentOnLevel,
CommentId = comment.CommentId,
UserId = comment.PosterUserId,
SlotId = comment.TargetSlotId ?? throw new NullReferenceException("SlotId in Level comment is null, this shouldn't happen."),
},
_ => null,
},
CommentType.Profile => new UserCommentActivityEntity
{
Type = EventType.CommentOnUser,
CommentId = comment.CommentId,
UserId = comment.PosterUserId,
TargetUserId = comment.TargetUserId ?? throw new NullReferenceException("TargetUserId in User comment is null, this shouldn't happen."),
},
_ => null,
},
PhotoEntity photo => database.Slots.Where(s => s.SlotId == photo.SlotId)
.Select(s => s.Type)
.FirstOrDefault() switch
{
SlotType.User => new LevelPhotoActivity
{
Type = EventType.UploadPhoto,
PhotoId = photo.PhotoId,
UserId = photo.CreatorId,
SlotId = photo.SlotId ?? throw new NullReferenceException("SlotId in Photo is null"),
},
// All other photos (story, moon, pod, etc.)
_ => null,
},
ScoreEntity score => database.Slots.Where(s => s.SlotId == score.SlotId)
.Select(s => s.Type)
.FirstOrDefault() switch
{
// Don't add story scores or versus scores
SlotType.User when score.Type != 7 => new ScoreActivityEntity
{
Type = EventType.Score,
ScoreId = score.ScoreId,
UserId = score.UserId,
SlotId = score.SlotId,
Points = score.Points,
},
_ => null,
},
HeartedLevelEntity heartedLevel => database.Slots.Where(s => s.SlotId == heartedLevel.SlotId)
.Select(s => s.Type)
.FirstOrDefault() switch
{
SlotType.User => new LevelActivityEntity
{
Type = EventType.HeartLevel,
SlotId = heartedLevel.SlotId,
UserId = heartedLevel.UserId,
},
_ => null,
},
HeartedProfileEntity heartedProfile => new UserActivityEntity
{
Type = EventType.HeartUser,
TargetUserId = heartedProfile.HeartedUserId,
UserId = heartedProfile.UserId,
},
HeartedPlaylistEntity heartedPlaylist => new PlaylistActivityEntity
{
Type = EventType.HeartPlaylist,
PlaylistId = heartedPlaylist.PlaylistId,
UserId = heartedPlaylist.UserId,
},
VisitedLevelEntity visitedLevel => new LevelActivityEntity
{
Type = EventType.PlayLevel,
SlotId = visitedLevel.SlotId,
UserId = visitedLevel.UserId,
},
ReviewEntity review => new ReviewActivityEntity
{
Type = EventType.ReviewLevel,
ReviewId = review.ReviewId,
UserId = review.ReviewerId,
SlotId = review.SlotId,
},
RatedLevelEntity ratedLevel => new LevelActivityEntity
{
Type = ratedLevel.Rating != 0 ? EventType.DpadRateLevel : EventType.RateLevel,
SlotId = ratedLevel.SlotId,
UserId = ratedLevel.UserId,
},
PlaylistEntity playlist => new PlaylistActivityEntity
{
Type = EventType.CreatePlaylist,
PlaylistId = playlist.PlaylistId,
UserId = playlist.CreatorId,
},
WebsiteAnnouncementEntity announcement => new NewsActivityEntity
{
Type = EventType.NewsPost,
UserId = announcement.PublisherId ?? 0,
NewsId = announcement.AnnouncementId,
},
_ => null,
};
InsertActivity(database, activity);
}
private static void RemoveDuplicateEvents(DatabaseContext database, ActivityEntity activity)
{
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
switch (activity.Type)
{
case EventType.HeartLevel:
case EventType.UnheartLevel:
{
if (activity is not LevelActivityEntity levelActivity) break;
DeleteActivity(a => a.TargetSlotId == levelActivity.SlotId);
break;
}
case EventType.HeartUser:
case EventType.UnheartUser:
{
if (activity is not UserActivityEntity userActivity) break;
DeleteActivity(a => a.TargetUserId == userActivity.TargetUserId);
break;
}
case EventType.HeartPlaylist:
{
if (activity is not PlaylistActivityEntity playlistActivity) break;
DeleteActivity(a => a.TargetPlaylistId == playlistActivity.PlaylistId);
break;
}
}
return;
void DeleteActivity(Expression<Func<ActivityDto, bool>> predicate)
{
database.Activities.ToActivityDto()
.Where(a => a.Activity.UserId == activity.UserId)
.Where(a => a.Activity.Type == activity.Type)
.Where(predicate)
.Select(a => a.Activity)
.ExecuteDelete();
}
}
private static void InsertActivity(DatabaseContext database, ActivityEntity? activity)
{
if (activity == null) return;
if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode) return;
Logger.Debug("Inserting activity: " + activity.GetType().Name, LogArea.Activity);
RemoveDuplicateEvents(database, activity);
activity.Timestamp = DateTime.UtcNow;
database.Activities.Add(activity);
database.SaveChanges();
}
public void OnEntityChanged<T>(DatabaseContext database, T origEntity, T currentEntity) where T : class
{
ActivityEntity? activity = null;
switch (currentEntity)
{
case VisitedLevelEntity visitedLevel:
{
if (origEntity is not VisitedLevelEntity oldVisitedLevel) break;
if (Plays(oldVisitedLevel) >= Plays(visitedLevel)) break;
activity = new LevelActivityEntity
{
Type = EventType.PlayLevel,
SlotId = visitedLevel.SlotId,
UserId = visitedLevel.UserId,
};
break;
int Plays(VisitedLevelEntity entity) => entity.PlaysLBP1 + entity.PlaysLBP2 + entity.PlaysLBP3;
}
case ScoreEntity score:
{
if (origEntity is not ScoreEntity oldScore) break;
// don't track versus levels
if (oldScore.Type == 7) break;
SlotType slotType = database.Slots.Where(s => s.SlotId == score.SlotId)
.Select(s => s.Type)
.FirstOrDefault();
if (slotType != SlotType.User) break;
if (oldScore.Points > score.Points) break;
activity = new ScoreActivityEntity
{
Type = EventType.Score,
ScoreId = score.ScoreId,
SlotId = score.SlotId,
UserId = score.UserId,
Points = score.Points,
};
break;
}
case SlotEntity slotEntity:
{
if (origEntity is not SlotEntity oldSlotEntity) break;
bool oldIsTeamPick = oldSlotEntity.TeamPickTime != 0;
bool newIsTeamPick = slotEntity.TeamPickTime != 0;
switch (oldIsTeamPick)
{
// When a level is team picked
case false when newIsTeamPick:
activity = new LevelActivityEntity
{
Type = EventType.MMPickLevel,
SlotId = slotEntity.SlotId,
UserId = slotEntity.CreatorId,
};
break;
// When a level has its team pick removed then remove the corresponding activity
case true when !newIsTeamPick:
database.Activities.OfType<LevelActivityEntity>()
.Where(a => a.Type == EventType.MMPickLevel)
.Where(a => a.SlotId == slotEntity.SlotId)
.ExecuteDelete();
break;
default:
{
if (oldSlotEntity.SlotId == slotEntity.SlotId &&
slotEntity.Type == SlotType.User &&
oldSlotEntity.LastUpdated != slotEntity.LastUpdated)
{
activity = new LevelActivityEntity
{
Type = EventType.PublishLevel,
SlotId = slotEntity.SlotId,
UserId = slotEntity.CreatorId,
};
}
break;
}
}
break;
}
case CommentEntity comment:
{
if (origEntity is not CommentEntity oldComment) break;
if (comment.TargetSlotId != null)
{
SlotType slotType = database.Slots.Where(s => s.SlotId == comment.TargetSlotId)
.Select(s => s.Type)
.FirstOrDefault();
if (slotType != SlotType.User) break;
}
if (oldComment.Deleted || !comment.Deleted) break;
if (comment.Type != CommentType.Level) break;
activity = new CommentActivityEntity
{
Type = EventType.DeleteLevelComment,
CommentId = comment.CommentId,
UserId = comment.PosterUserId,
};
break;
}
case PlaylistEntity playlist:
{
if (origEntity is not PlaylistEntity oldPlaylist) break;
int[] newSlots = playlist.SlotIds;
int[] oldSlots = oldPlaylist.SlotIds;
Logger.Debug($@"Old playlist slots: {string.Join(",", oldSlots)}", LogArea.Activity);
Logger.Debug($@"New playlist slots: {string.Join(",", newSlots)}", LogArea.Activity);
int[] addedSlots = newSlots.Except(oldSlots).ToArray();
Logger.Debug($@"Added playlist slots: {string.Join(",", addedSlots)}", LogArea.Activity);
// If no new level have been added
if (addedSlots.Length == 0) break;
// Normally events only need 1 resulting ActivityEntity but here
// we need multiple, so we have to do the inserting ourselves.
foreach (int slotId in addedSlots)
{
ActivityEntity entity = new PlaylistWithSlotActivityEntity
{
Type = EventType.AddLevelToPlaylist,
PlaylistId = playlist.PlaylistId,
SlotId = slotId,
UserId = playlist.CreatorId,
};
InsertActivity(database, entity);
}
break;
}
}
InsertActivity(database, activity);
}
public void OnEntityDeleted<T>(DatabaseContext database, T entity) where T : class
{
ActivityEntity? activity = entity switch
{
HeartedLevelEntity heartedLevel => database.Slots.Where(s => s.SlotId == heartedLevel.SlotId)
.Select(s => s.Type)
.FirstOrDefault() switch
{
SlotType.User => new LevelActivityEntity
{
Type = EventType.UnheartLevel,
SlotId = heartedLevel.SlotId,
UserId = heartedLevel.UserId,
},
_ => null,
},
HeartedProfileEntity heartedProfile => new UserActivityEntity
{
Type = EventType.UnheartUser,
TargetUserId = heartedProfile.HeartedUserId,
UserId = heartedProfile.UserId,
},
_ => null,
};
InsertActivity(database, activity);
}
}

View file

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Activity;
public struct 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? TargetNewsId { get; set; }
public int? TargetTeamPickSlotId { get; set; }
public int TargetId =>
this.GroupType switch
{
ActivityGroupType.User => this.TargetUserId ?? this.UserId,
ActivityGroupType.Level => this.TargetSlotId ?? -1,
ActivityGroupType.TeamPick => this.TargetTeamPickSlotId ?? -1,
ActivityGroupType.Playlist => this.TargetPlaylistId ?? -1,
ActivityGroupType.News => this.TargetNewsId ?? -1,
_ => this.UserId,
};
public ActivityGroupType GroupType =>
(this.TargetPlaylistId ?? -1) != -1
? ActivityGroupType.Playlist
: (this.TargetNewsId ?? -1) != -1
? ActivityGroupType.News
: (this.TargetTeamPickSlotId ?? -1) != -1
? ActivityGroupType.TeamPick
: (this.TargetSlotId ?? -1) != -1
? ActivityGroupType.Level
: ActivityGroupType.User;
public override string ToString() =>
$@"{this.GroupType} Group: Timestamp: {this.Timestamp}, UserId: {this.UserId}, TargetId: {this.TargetId}";
}
public struct OuterActivityGroup
{
public ActivityGroup Key { get; set; }
public List<IGrouping<InnerActivityGroup, ActivityDto>> Groups { get; set; }
}
public struct InnerActivityGroup
{
public ActivityGroupType Type { get; set; }
public int UserId { get; set; }
public int TargetId { get; set; }
}
public enum ActivityGroupType
{
[XmlEnum("user")]
User,
[XmlEnum("slot")]
Level,
[XmlEnum("playlist")]
Playlist,
[XmlEnum("news")]
News,
[XmlEnum("slot")]
TeamPick,
}

View file

@ -0,0 +1,86 @@
using System.Xml.Serialization;
namespace LBPUnion.ProjectLighthouse.Types.Activity;
/// <summary>
/// An enum of all possible event types that LBP recognizes in Recent Activity
/// <remarks>
/// <para>
/// <see cref="UnheartLevel"/>, <see cref="UnheartUser"/>, <see cref="DeleteLevelComment"/>, <see cref="UnpublishLevel"/> are ignored by the game
/// </para>
/// </remarks>
/// </summary>
public enum EventType
{
[XmlEnum("heart_level")]
HeartLevel = 0,
[XmlEnum("unheart_level")]
UnheartLevel = 1,
[XmlEnum("heart_user")]
HeartUser = 2,
[XmlEnum("unheart_user")]
UnheartUser = 3,
[XmlEnum("play_level")]
PlayLevel = 4,
[XmlEnum("rate_level")]
RateLevel = 5,
[XmlEnum("tag_level")]
TagLevel = 6,
[XmlEnum("comment_on_level")]
CommentOnLevel = 7,
[XmlEnum("delete_level_comment")]
DeleteLevelComment = 8,
[XmlEnum("upload_photo")]
UploadPhoto = 9,
[XmlEnum("publish_level")]
PublishLevel = 10,
[XmlEnum("unpublish_level")]
UnpublishLevel = 11,
[XmlEnum("score")]
Score = 12,
[XmlEnum("news_post")]
NewsPost = 13,
[XmlEnum("mm_pick_level")]
MMPickLevel = 14,
[XmlEnum("dpad_rate_level")]
DpadRateLevel = 15,
[XmlEnum("review_level")]
ReviewLevel = 16,
[XmlEnum("comment_on_user")]
CommentOnUser = 17,
/// <remarks>
/// This event is only used in LBP3
/// </remarks>>
[XmlEnum("create_playlist")]
CreatePlaylist = 18,
/// <remarks>
/// This event is only used in LBP3
/// </remarks>>
[XmlEnum("heart_playlist")]
HeartPlaylist = 19,
/// <remarks>
/// This event is only used in LBP3
/// </remarks>>
[XmlEnum("add_level_to_playlist")]
AddLevelToPlaylist = 20,
}

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

@ -0,0 +1,31 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity;
public class ActivityEntity
{
[Key]
public int ActivityId { get; set; }
/// <summary>
/// The time that this event took place.
/// </summary>
public DateTime Timestamp { get; set; }
/// <summary>
/// The <see cref="UserEntity.UserId"/> of the <see cref="UserEntity"/> that triggered this event.
/// </summary>
public int UserId { get; set; }
[ForeignKey(nameof(UserId))]
public UserEntity User { get; set; }
/// <summary>
/// The type of this event.
/// </summary>
public EventType Type { get; set; }
}

View file

@ -0,0 +1,38 @@
using System.ComponentModel.DataAnnotations.Schema;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity;
/// <summary>
/// Supported event types: <see cref="EventType.CommentOnUser"/>, <see cref="EventType.CommentOnLevel"/>, and <see cref="EventType.DeleteLevelComment"/>.
/// </summary>
public class CommentActivityEntity : ActivityEntity
{
/// <summary>
/// The <see cref="CommentEntity.CommentId"/> of the <see cref="CommentEntity"/> that this event refers to.
/// </summary>
public int CommentId { get; set; }
[ForeignKey(nameof(CommentId))]
public CommentEntity Comment { get; set; }
}
public class LevelCommentActivityEntity : CommentActivityEntity
{
[Column("SlotId")]
public int SlotId { get; set; }
[ForeignKey(nameof(SlotId))]
public SlotEntity Slot { get; set; }
}
public class UserCommentActivityEntity : CommentActivityEntity
{
[Column("TargetUserId")]
public int TargetUserId { get; set; }
[ForeignKey(nameof(TargetUserId))]
public UserEntity TargetUser { get; set; }
}

View file

@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations.Schema;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity;
/// <summary>
/// Supported event types: <see cref="EventType.PlayLevel"/>, <see cref="EventType.HeartLevel"/>, <see cref="EventType.PublishLevel"/>,
/// <see cref="EventType.UnheartLevel"/>, and <see cref="EventType.MMPickLevel"/>.
/// </summary>
public class LevelActivityEntity : ActivityEntity
{
/// <summary>
/// The <see cref="SlotEntity.SlotId"/> of the <see cref="SlotEntity"/> that this event refers to.
/// </summary>
[Column("SlotId")]
public int SlotId { get; set; }
[ForeignKey(nameof(SlotId))]
public SlotEntity Slot { get; set; }
}

View file

@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations.Schema;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Website;
namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity;
/// <summary>
/// Supported event types: <see cref="EventType.NewsPost"/>.
/// <remarks>
/// <para>
/// This event type can only be grouped with other <see cref="NewsActivityEntity"/>.
/// </para>
/// </remarks>
/// </summary>
public class NewsActivityEntity : ActivityEntity
{
/// <summary>
/// The <see cref="WebsiteAnnouncementEntity.AnnouncementId"/> of the <see cref="WebsiteAnnouncementEntity"/> that this event refers to.
/// </summary>
public int NewsId { get; set; }
[ForeignKey(nameof(NewsId))]
public WebsiteAnnouncementEntity News { get; set; }
}

View file

@ -0,0 +1,38 @@
using System.ComponentModel.DataAnnotations.Schema;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity;
/// <summary>
/// Supported event types: <see cref="EventType.UploadPhoto"/>.
/// </summary>
public class PhotoActivityEntity : ActivityEntity
{
/// <summary>
/// The <see cref="PhotoEntity.PhotoId"/> of the <see cref="PhotoEntity"/> that this event refers to.
/// </summary>
public int PhotoId { get; set; }
[ForeignKey(nameof(PhotoId))]
public PhotoEntity Photo { get; set; }
}
public class LevelPhotoActivity : PhotoActivityEntity
{
[Column("SlotId")]
public int SlotId { get; set; }
[ForeignKey(nameof(SlotId))]
public SlotEntity Slot { get; set; }
}
public class UserPhotoActivity : PhotoActivityEntity
{
[Column("TargetUserId")]
public int TargetUserId { get; set; }
[ForeignKey(nameof(TargetUserId))]
public UserEntity TargetUser { get; set; }
}

View file

@ -0,0 +1,52 @@
using System.ComponentModel.DataAnnotations.Schema;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity;
/// <summary>
/// Supported event types: <see cref="EventType.CreatePlaylist"/> and <see cref="EventType.HeartPlaylist"/>.
/// </summary>
public class PlaylistActivityEntity : ActivityEntity
{
/// <summary>
/// The <see cref="PlaylistEntity.PlaylistId"/> of the <see cref="PlaylistEntity"/> that this event refers to.
/// </summary>
[Column("PlaylistId")]
public int PlaylistId { get; set; }
[ForeignKey(nameof(PlaylistId))]
public PlaylistEntity Playlist { get; set; }
}
/// <summary>
/// Supported event types: <see cref="EventType.AddLevelToPlaylist"/>.
/// <remarks>
/// <para>
/// The relationship between <see cref="PlaylistActivityEntity"/> and <see cref="PlaylistWithSlotActivityEntity"/>
/// is slightly hacky but it allows us to reuse columns that would normally only be user with other <see cref="ActivityEntity"/> types.
/// </para>
/// </remarks>
/// </summary>
public class PlaylistWithSlotActivityEntity : ActivityEntity
{
/// <summary>
/// The <see cref="PlaylistEntity.PlaylistId"/> of the <see cref="PlaylistEntity"/> that this event refers to.
/// </summary>
[Column("PlaylistId")]
public int PlaylistId { get; set; }
[ForeignKey(nameof(PlaylistId))]
public PlaylistEntity Playlist { get; set; }
/// <summary>
/// This reuses the SlotId column of <see cref="LevelActivityEntity"/> but has no ForeignKey definition so that it can be null
/// <remarks>
/// <para>
/// It effectively serves as extra storage for PlaylistActivityEntity to use for the AddLevelToPlaylistEvent
/// </para>
/// </remarks>
/// </summary>
[Column("SlotId")]
public int SlotId { get; set; }
}

View file

@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations.Schema;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity;
/// <summary>
/// Supported event types: <see cref="EventType.DpadRateLevel"/>, <see cref="EventType.ReviewLevel"/>, <see cref="EventType.RateLevel"/>, and <see cref="EventType.TagLevel"/>.
/// </summary>
public class ReviewActivityEntity : ActivityEntity
{
/// <summary>
/// The <see cref="ReviewEntity.ReviewId"/> of the <see cref="ReviewEntity"/> that this event refers to.
/// </summary>
public int ReviewId { get; set; }
[ForeignKey(nameof(ReviewId))]
public ReviewEntity Review { get; set; }
[Column("SlotId")]
public int SlotId { get; set; }
[ForeignKey(nameof(SlotId))]
public SlotEntity Slot { get; set; }
}

View file

@ -0,0 +1,27 @@
using System.ComponentModel.DataAnnotations.Schema;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity;
/// <summary>
/// Supported event types: <see cref="EventType.Score"/>.
/// </summary>
public class ScoreActivityEntity : ActivityEntity
{
/// <summary>
/// The <see cref="ScoreEntity.ScoreId"/> of the <see cref="ScoreEntity"/> that this event refers to.
/// </summary>
public int ScoreId { get; set; }
[ForeignKey(nameof(ScoreId))]
public ScoreEntity Score { get; set; }
[Column("SlotId")]
public int SlotId { get; set; }
[ForeignKey(nameof(SlotId))]
public SlotEntity Slot { get; set; }
public int Points { get; set; }
}

View file

@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations.Schema;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
namespace LBPUnion.ProjectLighthouse.Types.Entities.Activity;
/// <summary>
/// Supported event types: <see cref="EventType.HeartUser"/> and <see cref="EventType.UnheartUser"/>.
/// </summary>
public class UserActivityEntity : ActivityEntity
{
/// <summary>
/// The <see cref="UserEntity.UserId"/> of the <see cref="UserEntity"/> that this event refers to.
/// </summary>
[Column("TargetUserId")]
public int TargetUserId { get; set; }
[ForeignKey(nameof(TargetUserId))]
public UserEntity TargetUser { get; set; }
}

Some files were not shown because too many files have changed in this diff Show more