Remove giant ActivityDto ternary and add more documentation in some areas

This commit is contained in:
Slendy 2024-03-25 01:03:53 -05:00
commit c2011837d7
No known key found for this signature in database
GPG key ID: 7288D68361B91428
23 changed files with 760 additions and 168 deletions

View file

@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "7.0.13",
"version": "8.0.0",
"commands": [
"dotnet-ef"
]

View file

@ -56,7 +56,7 @@ public class ActivityController : ControllerBase
.ToListAsync();
List<int>? friendIds = UserFriendStore.GetUserFriendData(token.UserId)?.FriendIds;
friendIds ??= new List<int>();
friendIds ??= [];
// This is how lbp3 does its filtering
GameStreamFilter? filter = await this.DeserializeBody<GameStreamFilter>();
@ -89,7 +89,7 @@ public class ActivityController : ControllerBase
predicate = predicate.Or(dto => dto.TargetSlotCreatorId == token.UserId);
}
List<int> includedUserIds = new();
List<int> includedUserIds = [];
if (!excludeFriends)
{
@ -168,7 +168,7 @@ public class ActivityController : ControllerBase
private static DateTime GetOldestTime
(IReadOnlyCollection<IGrouping<ActivityGroup, ActivityDto>> groups, DateTime defaultTimestamp) =>
groups.Any()
groups.Count != 0
? groups.Min(g => g.MinBy(a => a.Activity.Timestamp)?.Activity.Timestamp ?? defaultTimestamp)
: defaultTimestamp;
@ -247,7 +247,7 @@ public class ActivityController : ControllerBase
{
GameTokenEntity token = this.GetToken();
if (token.GameVersion == GameVersion.LittleBigPlanet1) return this.NotFound();
if (token.GameVersion is GameVersion.LittleBigPlanet1 or GameVersion.LittleBigPlanetPSP) return this.NotFound();
IQueryable<ActivityDto> activityEvents = await this.GetFilters(this.database.Activities.ToActivityDto(true),
token,
@ -305,10 +305,11 @@ public class ActivityController : ControllerBase
{
GameTokenEntity token = this.GetToken();
if (token.GameVersion == GameVersion.LittleBigPlanet1) return this.NotFound();
if (token.GameVersion is GameVersion.LittleBigPlanet1 or GameVersion.LittleBigPlanetPSP) return this.NotFound();
if ((SlotHelper.IsTypeInvalid(slotType) || slotId == 0) == (username == null)) return this.BadRequest();
// 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);
@ -343,6 +344,8 @@ public class ActivityController : ControllerBase
List<OuterActivityGroup> outerGroups = groups.ToOuterActivityGroups();
PrintOuterGroups(outerGroups);
long oldestTimestamp = GetOldestTime(groups, times.Start).ToUnixTimeMilliseconds();
await this.CacheEntities(outerGroups);

View file

@ -1,31 +1,345 @@
using System.Collections.Generic;
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 ActivityGroupingTest()
public void ToOuterActivityGroups_ShouldCreateGroupPerObject_WhenGroupedBy_ObjectThenActor()
{
List<ActivityDto> activities = new()
{
new ActivityDto
List<ActivityEntity> activities = [
new LevelActivityEntity
{
TargetPlaylistId = 1,
Activity = new ActivityEntity(),
UserId = 1,
SlotId = 1,
Slot = new SlotEntity
{
GameVersion = GameVersion.LittleBigPlanet2,
},
Timestamp = DateTime.Now,
Type = EventType.PlayLevel,
},
};
List<OuterActivityGroup> groups = activities.AsQueryable().ToActivityGroups().ToList().ToOuterActivityGroups();
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,
},
];
//TODO: fix test
List<OuterActivityGroup> groups = activities.ToActivityDto().AsQueryable().ToActivityGroups().ToList().ToOuterActivityGroups();
Assert.NotNull(groups);
Assert.Single(groups);
OuterActivityGroup groupEntry = groups.First();
Assert.Equal(ActivityGroupType.Playlist, groupEntry.Key.GroupType);
Assert.Equal(1, groupEntry.Key.TargetId);
OuterActivityGroup outerGroup = groups.First();
Assert.Equal(ActivityGroupType.Level, outerGroup.Key.GroupType);
Assert.Equal(1, outerGroup.Key.TargetSlotId);
IGrouping<InnerActivityGroup, ActivityDto>? firstGroup = outerGroup.Groups.First();
IGrouping<InnerActivityGroup, ActivityDto>? secondGroup = outerGroup.Groups.Last();
Assert.NotNull(secondGroup);
Assert.Equal(ActivityGroupType.User, secondGroup.Key.Type);
Assert.Equal(1, secondGroup.Key.TargetId); // user group should have the user id
Assert.Equal(1, secondGroup.ToList()[0].TargetSlotId); // events in user group should have t
Assert.Equal(1, secondGroup.ToList()[1].TargetSlotId);
Assert.NotNull(firstGroup);
Assert.Equal(ActivityGroupType.User, firstGroup.Key.Type);
Assert.Equal(2, firstGroup.Key.TargetId);
Assert.Equal(1, firstGroup.ToList()[0].TargetSlotId);
}
[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);
//TODO: fix test
Assert.Multiple(() =>
{
Assert.NotNull(groups);
Assert.Equal(2, groups.Count);
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.Single(firstUserGroup.Groups);
Assert.Single(secondUserGroup.Groups);
Assert.Equal(2, firstUserGroup.Groups.ToList()[0].Count());
Assert.Single(secondUserGroup.Groups.ToList()[0]);
// Assert.Equal(ActivityGroupType.Level, outerGroup.Key.GroupType);
// Assert.Equal(1, outerGroup.Key.TargetSlotId);
//
// IGrouping<InnerActivityGroup, ActivityDto>? firstGroup = outerGroup.Groups.First();
// IGrouping<InnerActivityGroup, ActivityDto>? secondGroup = outerGroup.Groups.Last();
//
// Assert.NotNull(secondGroup);
// Assert.Equal(ActivityGroupType.User, secondGroup.Key.Type);
// Assert.Equal(1, secondGroup.Key.TargetId); // user group should have the user id
// Assert.Equal(1, secondGroup.ToList()[0].TargetSlotId); // events in user group should have t
// Assert.Equal(1, secondGroup.ToList()[1].TargetSlotId);
//
// Assert.NotNull(firstGroup);
// Assert.Equal(ActivityGroupType.User, firstGroup.Key.Type);
// Assert.Equal(2, firstGroup.Key.TargetId);
// Assert.Equal(1, firstGroup.ToList()[0].TargetSlotId);
});
}
[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();
var sql = db.Activities.ToActivityDto().ToQueryString();
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

@ -93,7 +93,7 @@ public class ActivityInterceptor : SaveChangesInterceptor
{
if (eventData.Context is not DatabaseContext context) return;
HashSet<CustomTrackedEntity> entities = new();
HashSet<CustomTrackedEntity> entities = [];
List<EntityEntry> entries = context.ChangeTracker.Entries().ToList();

View file

@ -109,13 +109,15 @@ public partial class DatabaseContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<LevelActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<PhotoActivityEntity>().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<CommentActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<LevelCommentActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<UserCommentActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<UserActivityEntity>().UseTphMappingStrategy();
modelBuilder.Entity<ReviewActivityEntity>().UseTphMappingStrategy();
base.OnModelCreating(modelBuilder);

View file

@ -2,7 +2,6 @@
using System.Linq;
using LBPUnion.ProjectLighthouse.Types.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Activity;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
namespace LBPUnion.ProjectLighthouse.Extensions;
@ -10,7 +9,7 @@ public static class ActivityQueryExtensions
{
public static List<int> GetIds(this IReadOnlyCollection<OuterActivityGroup> groups, ActivityGroupType type)
{
List<int> ids = new();
List<int> ids = [];
// Add outer group ids
ids.AddRange(groups.Where(g => g.Key.GroupType == type)
.Where(g => g.Key.TargetId != 0)
@ -29,6 +28,12 @@ public static class ActivityQueryExtensions
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 or not 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
@ -59,74 +64,65 @@ public static class ActivityQueryExtensions
Groups = g.OrderByDescending(a => a.Activity.Timestamp)
.GroupBy(gr => new InnerActivityGroup
{
Type = groupByActor ? gr.GroupType : gr.GroupType != ActivityGroupType.News ? ActivityGroupType.User : ActivityGroupType.News,
Type = groupByActor
? gr.GroupType
: gr.GroupType != ActivityGroupType.News
? ActivityGroupType.User
: ActivityGroupType.News,
UserId = gr.Activity.UserId,
TargetId = groupByActor ? gr.TargetId : gr.GroupType != ActivityGroupType.News ? gr.Activity.UserId : gr.TargetNewsId ?? 0,
TargetId = groupByActor
? gr.TargetId
: gr.GroupType != ActivityGroupType.News
? gr.Activity.UserId
: gr.TargetNewsId ?? 0,
})
.ToList(),
})
.ToList();
// WARNING - To the next person who tries to improve this code: As of writing this, it's not possible
// to build a pattern matching switch statement with expression trees. so the only other option
// is to basically rewrite this nested ternary mess with expression trees which isn't much better
// The resulting SQL generated by EntityFramework uses a CASE statement which is probably fine
// TOTAL HOURS WASTED: 3
/// <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 or not the <see cref="ActivityDto.TargetSlotCreatorId"/> field should be included.</param>
/// <param name="includeTeamPick">Whether or not 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 is LevelActivityEntity
? ((LevelActivityEntity)a).SlotId
: a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0
? ((PhotoActivityEntity)a).Photo.SlotId
: a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Level
? ((CommentActivityEntity)a).Comment.TargetSlotId
: a is ScoreActivityEntity
? ((ScoreActivityEntity)a).Score.SlotId
: a is ReviewActivityEntity
? ((ReviewActivityEntity)a).Review.SlotId
: 0,
TargetSlotGameVersion = a is LevelActivityEntity
? ((LevelActivityEntity)a).Slot.GameVersion
: a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0
? ((PhotoActivityEntity)a).Photo.Slot.GameVersion
: a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Level
? ((CommentActivityEntity)a).Comment.TargetSlot.GameVersion
: a is ScoreActivityEntity
? ((ScoreActivityEntity)a).Score.Slot.GameVersion
: a is ReviewActivityEntity
? ((ReviewActivityEntity)a).Review.Slot.GameVersion
: 0,
TargetSlotCreatorId = includeSlotCreator
? a is LevelActivityEntity
? ((LevelActivityEntity)a).Slot.CreatorId
: a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId != 0
? ((PhotoActivityEntity)a).Photo.Slot!.CreatorId
: a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Level
? ((CommentActivityEntity)a).Comment.TargetSlot.CreatorId
: a is ScoreActivityEntity
? ((ScoreActivityEntity)a).Score.Slot.CreatorId
: a is ReviewActivityEntity
? ((ReviewActivityEntity)a).Review.Slot!.CreatorId
: 0
: 0,
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, });
}
TargetUserId = a is UserActivityEntity
? ((UserActivityEntity)a).TargetUserId
: a is CommentActivityEntity && ((CommentActivityEntity)a).Comment.Type == CommentType.Profile
? ((CommentActivityEntity)a).Comment.TargetUserId
: a is PhotoActivityEntity && ((PhotoActivityEntity)a).Photo.SlotId == 0
? ((PhotoActivityEntity)a).Photo.CreatorId
: 0,
TargetPlaylistId = a is PlaylistActivityEntity || a is PlaylistWithSlotActivityEntity
? ((PlaylistActivityEntity)a).PlaylistId
: 0,
TargetNewsId = a is NewsActivityEntity ? ((NewsActivityEntity)a).NewsId : 0,
TargetTeamPickId = includeTeamPick
? a.Type == EventType.MMPickLevel && a is LevelActivityEntity ? ((LevelActivityEntity)a).SlotId : 0
: 0, });
/// <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 or not the <see cref="ActivityDto.TargetSlotCreatorId"/> field should be included.</param>
/// <param name="includeTeamPick">Whether or not 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

@ -6,10 +6,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ProjectLighthouse.Migrations
namespace LBPUnion.ProjectLighthouse.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20240120214525_InitialActivity")]
[Migration("20240325034658_InitialActivity")]
public partial class InitialActivity : Migration
{
/// <inheritdoc />
@ -24,12 +24,12 @@ namespace ProjectLighthouse.Migrations
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: "longtext", nullable: false)
Discriminator = table.Column<string>(type: "varchar(34)", maxLength: 34, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
CommentId = table.Column<int>(type: "int", nullable: true),
SlotId = table.Column<int>(type: "int", nullable: true),
NewsId = 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),

View file

@ -27,7 +27,8 @@ namespace ProjectLighthouse.Migrations
b.Property<string>("Discriminator")
.IsRequired()
.HasColumnType("longtext");
.HasMaxLength(34)
.HasColumnType("varchar(34)");
b.Property<DateTime>("Timestamp")
.HasColumnType("datetime(6)");
@ -1123,18 +1124,6 @@ namespace ProjectLighthouse.Migrations
b.ToTable("WebsiteAnnouncements");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.CommentActivityEntity", b =>
{
b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity");
b.Property<int>("CommentId")
.HasColumnType("int");
b.HasIndex("CommentId");
b.HasDiscriminator().HasValue("CommentActivityEntity");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.LevelActivityEntity", b =>
{
b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity");
@ -1149,6 +1138,46 @@ namespace ProjectLighthouse.Migrations
b.HasDiscriminator().HasValue("LevelActivityEntity");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.LevelCommentActivityEntity", b =>
{
b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity");
b.Property<int>("CommentId")
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("int");
b.Property<int>("SlotId")
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("int")
.HasColumnName("SlotId");
b.HasIndex("CommentId");
b.HasIndex("SlotId");
b.HasDiscriminator().HasValue("LevelCommentActivityEntity");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.LevelPhotoActivity", b =>
{
b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity");
b.Property<int>("PhotoId")
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("int");
b.Property<int>("SlotId")
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("int")
.HasColumnName("SlotId");
b.HasIndex("PhotoId");
b.HasIndex("SlotId");
b.HasDiscriminator().HasValue("LevelPhotoActivity");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.NewsActivityEntity", b =>
{
b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity");
@ -1161,18 +1190,6 @@ namespace ProjectLighthouse.Migrations
b.HasDiscriminator().HasValue("NewsActivityEntity");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.PhotoActivityEntity", b =>
{
b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity");
b.Property<int>("PhotoId")
.HasColumnType("int");
b.HasIndex("PhotoId");
b.HasDiscriminator().HasValue("PhotoActivityEntity");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.PlaylistActivityEntity", b =>
{
b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity");
@ -1213,8 +1230,15 @@ namespace ProjectLighthouse.Migrations
b.Property<int>("ReviewId")
.HasColumnType("int");
b.Property<int>("SlotId")
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("int")
.HasColumnName("SlotId");
b.HasIndex("ReviewId");
b.HasIndex("SlotId");
b.HasDiscriminator().HasValue("ReviewActivityEntity");
});
@ -1225,8 +1249,15 @@ namespace ProjectLighthouse.Migrations
b.Property<int>("ScoreId")
.HasColumnType("int");
b.Property<int>("SlotId")
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("int")
.HasColumnName("SlotId");
b.HasIndex("ScoreId");
b.HasIndex("SlotId");
b.HasDiscriminator().HasValue("ScoreActivityEntity");
});
@ -1235,13 +1266,55 @@ namespace ProjectLighthouse.Migrations
b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity");
b.Property<int>("TargetUserId")
.HasColumnType("int");
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("int")
.HasColumnName("TargetUserId");
b.HasIndex("TargetUserId");
b.HasDiscriminator().HasValue("UserActivityEntity");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.UserCommentActivityEntity", b =>
{
b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity");
b.Property<int>("CommentId")
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("int");
b.Property<int>("TargetUserId")
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("int")
.HasColumnName("TargetUserId");
b.HasIndex("CommentId");
b.HasIndex("TargetUserId");
b.HasDiscriminator().HasValue("UserCommentActivityEntity");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.UserPhotoActivity", b =>
{
b.HasBaseType("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity");
b.Property<int>("PhotoId")
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("int");
b.Property<int>("TargetUserId")
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("int")
.HasColumnName("TargetUserId");
b.HasIndex("PhotoId");
b.HasIndex("TargetUserId");
b.HasDiscriminator().HasValue("UserPhotoActivity");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ActivityEntity", b =>
{
b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User")
@ -1646,17 +1719,6 @@ namespace ProjectLighthouse.Migrations
b.Navigation("Publisher");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.CommentActivityEntity", b =>
{
b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.CommentEntity", "Comment")
.WithMany()
.HasForeignKey("CommentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Comment");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.LevelActivityEntity", b =>
{
b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "Slot")
@ -1668,6 +1730,44 @@ namespace ProjectLighthouse.Migrations
b.Navigation("Slot");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.LevelCommentActivityEntity", b =>
{
b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.CommentEntity", "Comment")
.WithMany()
.HasForeignKey("CommentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "Slot")
.WithMany()
.HasForeignKey("SlotId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Comment");
b.Navigation("Slot");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.LevelPhotoActivity", b =>
{
b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoEntity", "Photo")
.WithMany()
.HasForeignKey("PhotoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "Slot")
.WithMany()
.HasForeignKey("SlotId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Photo");
b.Navigation("Slot");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.NewsActivityEntity", b =>
{
b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Website.WebsiteAnnouncementEntity", "News")
@ -1679,17 +1779,6 @@ namespace ProjectLighthouse.Migrations
b.Navigation("News");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.PhotoActivityEntity", b =>
{
b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoEntity", "Photo")
.WithMany()
.HasForeignKey("PhotoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Photo");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.PlaylistActivityEntity", b =>
{
b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.PlaylistEntity", "Playlist")
@ -1720,7 +1809,15 @@ namespace ProjectLighthouse.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "Slot")
.WithMany()
.HasForeignKey("SlotId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Review");
b.Navigation("Slot");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.ScoreActivityEntity", b =>
@ -1731,7 +1828,15 @@ namespace ProjectLighthouse.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "Slot")
.WithMany()
.HasForeignKey("SlotId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Score");
b.Navigation("Slot");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.UserActivityEntity", b =>
@ -1745,6 +1850,44 @@ namespace ProjectLighthouse.Migrations
b.Navigation("TargetUser");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.UserCommentActivityEntity", b =>
{
b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.CommentEntity", "Comment")
.WithMany()
.HasForeignKey("CommentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "TargetUser")
.WithMany()
.HasForeignKey("TargetUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Comment");
b.Navigation("TargetUser");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Activity.UserPhotoActivity", b =>
{
b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoEntity", "Photo")
.WithMany()
.HasForeignKey("PhotoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "TargetUser")
.WithMany()
.HasForeignKey("TargetUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Photo");
b.Navigation("TargetUser");
});
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoEntity", b =>
{
b.Navigation("PhotoSubjects");

View file

@ -25,11 +25,11 @@ public class ActivityDto
};
public ActivityGroupType GroupType =>
this.TargetSlotId != 0
this.TargetSlotId != null
? ActivityGroupType.Level
: this.TargetUserId != 0
: this.TargetUserId != null
? ActivityGroupType.User
: this.TargetPlaylistId != 0
: this.TargetPlaylistId != null
? ActivityGroupType.Playlist
: ActivityGroupType.News;
}

View file

@ -41,40 +41,44 @@ public class ActivityEntityEventHandler : IEntityEventHandler
{
CommentType.Level => comment.TargetSlot?.Type switch
{
SlotType.User => new CommentActivityEntity
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 CommentActivityEntity
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 => photo.SlotId switch
{
// Photos without levels
null => new PhotoActivityEntity
null => new UserPhotoActivity
{
Type = EventType.UploadPhoto,
PhotoId = photo.PhotoId,
UserId = photo.CreatorId,
TargetUserId = photo.CreatorId,
},
_ => photo.Slot?.Type switch
{
SlotType.Developer => null,
// Non-story levels (moon, pod, etc)
_ => new PhotoActivityEntity
_ => new LevelPhotoActivity
{
Type = EventType.UploadPhoto,
PhotoId = photo.PhotoId,
UserId = photo.CreatorId,
SlotId = photo.SlotId ?? throw new NullReferenceException("SlotId in Photo is null"),
},
},
},
@ -86,6 +90,7 @@ public class ActivityEntityEventHandler : IEntityEventHandler
Type = EventType.Score,
ScoreId = score.ScoreId,
UserId = score.UserId,
SlotId = score.SlotId,
},
_ => null,
},
@ -122,6 +127,7 @@ public class ActivityEntityEventHandler : IEntityEventHandler
Type = EventType.ReviewLevel,
ReviewId = review.ReviewId,
UserId = review.ReviewerId,
SlotId = review.SlotId,
},
RatedLevelEntity ratedLevel => new LevelActivityEntity
{

View file

@ -3,7 +3,12 @@
namespace LBPUnion.ProjectLighthouse.Types.Activity;
/// <summary>
/// UnheartLevel, UnheartUser, DeleteLevelComment, and UnpublishLevel don't actually do anything
/// 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
{
@ -61,12 +66,21 @@ public enum EventType
[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

@ -11,12 +11,21 @@ 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

@ -1,15 +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: CommentOnUser, CommentOnLevel, DeleteLevelComment
/// 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

@ -1,13 +1,18 @@
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: play_level, heart_level, publish_level, unheart_level, dpad_rate_level, rate_level, tag_level, mm_pick_level
/// 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; }

View file

@ -1,13 +1,22 @@
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: NewsPost
/// 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))]

View file

@ -1,16 +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: UploadPhoto
/// 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

@ -1,13 +1,17 @@
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: CreatePlaylist, HeartPlaylist
/// 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; }
@ -16,15 +20,19 @@ public class PlaylistActivityEntity : ActivityEntity
}
/// <summary>
/// Supported event types: AddLevelToPlaylist
/// 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 conditional reuse of columns from other ActivityEntity's
///
/// </para>
/// 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; }
@ -33,7 +41,11 @@ public class PlaylistWithSlotActivityEntity : ActivityEntity
/// <summary>
/// This reuses the SlotId column of <see cref="LevelActivityEntity"/> but has no ForeignKey definition so that it can be null
/// <para>It effectively serves as extra storage for PlaylistActivityEntity to use for the AddLevelToPlaylistEvent</para>
/// <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

@ -1,15 +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: DpadRateLevel, ReviewLevel, RateLevel, TagLevel
/// 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

@ -1,15 +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: Score
/// 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; }
}

View file

@ -1,13 +1,18 @@
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: HeartUser, UnheartUser
/// 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))]

View file

@ -58,7 +58,7 @@ public class GameEvent : ILbpSerializable, INeedsPreparationForSerialization
public static IEnumerable<GameEvent> CreateFromActivities(IEnumerable<ActivityDto> activities)
{
List<GameEvent> events = new();
List<GameEvent> events = [];
List<IGrouping<EventType, ActivityDto>> typeGroups = activities.GroupBy(g => g.Activity.Type).ToList();
foreach (IGrouping<EventType, ActivityDto> typeGroup in typeGroups)
{

View file

@ -26,21 +26,34 @@ namespace LBPUnion.ProjectLighthouse.Types.Serialization.Activity;
[XmlRoot("stream")]
public class GameStream : ILbpSerializable, INeedsPreparationForSerialization
{
/// <summary>
/// A list of <see cref="SlotEntity.SlotId"/> that should be included in the root
/// of the stream object. These will be loaded into <see cref="Slots"/>
/// </summary>
[XmlIgnore]
public List<int> SlotIds { get; set; }
/// <summary>
/// A list of <see cref="UserEntity.UserId"/> that should be included in the root
/// of the stream object. These will be loaded into <see cref="Users"/>
/// </summary>
[XmlIgnore]
public List<int> UserIds { get; set; }
/// <summary>
/// A list of <see cref="PlaylistEntity.PlaylistId"/> that should be included in the root
/// of the stream object. These will be loaded into <see cref="Playlists"/>
/// </summary>
[XmlIgnore]
public List<int> PlaylistIds { get; set; }
/// <summary>
/// A list of <see cref="WebsiteAnnouncementEntity.AnnouncementId"/> that should be included in the root
/// of the stream object. These will be loaded into <see cref="News"/>
/// </summary>
[XmlIgnore]
public List<int> NewsIds { get; set; }
[XmlIgnore]
private int TargetUserId { get; set; }
[XmlIgnore]
private GameVersion TargetGame { get; set; }
@ -77,7 +90,7 @@ public class GameStream : ILbpSerializable, INeedsPreparationForSerialization
public async Task PrepareSerialization(DatabaseContext database)
{
this.Slots = await LoadEntities<SlotEntity, SlotBase>(this.SlotIds, slot => SlotBase.CreateFromEntity(slot, this.TargetGame, this.TargetUserId), s => s.Type == SlotType.User);
this.Slots = await LoadEntities<SlotEntity, SlotBase>(this.SlotIds, slot => SlotBase.CreateFromEntity(slot, this.TargetGame, 0), s => s.Type == SlotType.User);
this.Users = await LoadEntities<UserEntity, GameUser>(this.UserIds, user => GameUser.CreateFromEntity(user, this.TargetGame));
this.Playlists = await LoadEntities<PlaylistEntity, GamePlaylist>(this.PlaylistIds, GamePlaylist.CreateFromEntity);
this.News = await LoadEntities<WebsiteAnnouncementEntity, GameNewsObject>(this.NewsIds, a => GameNewsObject.CreateFromEntity(a, this.TargetGame));
@ -86,16 +99,16 @@ public class GameStream : ILbpSerializable, INeedsPreparationForSerialization
async Task<List<TResult>> LoadEntities<TFrom, TResult>(List<int> ids, Func<TFrom, TResult> transformation, Func<TFrom, bool> predicate = null)
where TFrom : class
{
List<TResult> results = new();
List<TResult> results = [];
if (ids.Count <= 0) return null;
foreach (int id in ids)
{
TFrom entity = await database.Set<TFrom>().FindAsync(id);
if (predicate != null && !predicate(entity)) continue;
if (entity == null) continue;
if (predicate != null && !predicate(entity)) continue;
results.Add(transformation(entity));
}
@ -108,7 +121,6 @@ public class GameStream : ILbpSerializable, INeedsPreparationForSerialization
{
GameStream gameStream = new()
{
TargetUserId = token.UserId,
TargetGame = token.GameVersion,
StartTimestamp = startTimestamp,
EndTimestamp = endTimestamp,

View file

@ -37,8 +37,6 @@ public class GameStreamGroup : ILbpSerializable
[XmlArray("events")]
[XmlArrayItem("event")]
[DefaultValue(null)]
// ReSharper disable once MemberCanBePrivate.Global
// (the serializer can't see this if it's private)
public List<GameEvent> Events { get; set; }
public static GameStreamGroup CreateFromGroup(OuterActivityGroup group)
@ -56,8 +54,7 @@ public class GameStreamGroup : ILbpSerializable
g.Key.TargetId,
streamGroup =>
{
streamGroup.Timestamp =
g.MaxBy(a => a.Activity.Timestamp).Activity.Timestamp.ToUnixTimeMilliseconds();
streamGroup.Timestamp = g.Max(a => a.Activity.Timestamp).ToUnixTimeMilliseconds();
streamGroup.Events = GameEvent.CreateFromActivities(g).ToList();
}))
.ToList());