add name history tracking

This commit is contained in:
Lee 2024-06-30 03:36:00 +01:00
parent 91ecc9882c
commit 50391e5344
17 changed files with 537 additions and 10 deletions

@ -2,12 +2,14 @@ package cc.fascinated.bat.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public abstract class Profile {
@ -16,9 +18,6 @@ public abstract class Profile {
*/
private String profileKey;
public Profile() {
}
/**
* Resets the profile
*/

@ -8,10 +8,12 @@ import cc.fascinated.bat.model.token.beatsaber.scoresaber.ScoreSaberScoreToken;
import lombok.NonNull;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent;
import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameEvent;
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateGlobalNameEvent;
/**
* @author Fascinated (fascinated7)
@ -81,4 +83,25 @@ public interface EventListener {
*/
default void onModalInteraction(BatGuild guild, @NonNull BatUser user, @NonNull ModalInteractionEvent event) {
}
/**
* Called when a user updates their global name
*
* @param user the user that updated their global name
* @param oldName the old global name
* @param newName the new global name
*/
default void onUserUpdateGlobalName(@NonNull BatUser user, String oldName, String newName, @NonNull UserUpdateGlobalNameEvent event) {
}
/**
* Called when a user updates their nickname in a guild
*
* @param guild the guild that the user updated their nickname in
* @param user the user that updated their nickname
* @param oldName the old nickname
* @param newName the new nickname
*/
default void onGuildMemberUpdateNickname(@NonNull BatGuild guild, @NonNull BatUser user, String oldName, String newName, @NonNull GuildMemberUpdateNicknameEvent event) {
}
}

@ -17,10 +17,6 @@ import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author Fascinated (fascinated7)

@ -7,7 +7,6 @@ import cc.fascinated.bat.features.birthday.UserBirthday;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import cc.fascinated.bat.service.UserService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;

@ -0,0 +1,17 @@
package cc.fascinated.bat.features.namehistory;
import cc.fascinated.bat.command.Category;
import cc.fascinated.bat.features.Feature;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class NameHistoryFeature extends Feature {
public static final int NAME_HISTORY_SIZE = 25;
public NameHistoryFeature() {
super("Name History", Category.UTILITY);
}
}

@ -0,0 +1,43 @@
package cc.fascinated.bat.features.namehistory;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.features.namehistory.profile.user.NameHistoryProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.GuildService;
import cc.fascinated.bat.service.UserService;
import lombok.NonNull;
import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateGlobalNameEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class NameHistoryListener implements EventListener {
private final UserService userService;
private final GuildService guildService;
@Autowired
public NameHistoryListener(@NonNull UserService userService, @NonNull GuildService guildService) {
this.userService = userService;
this.guildService = guildService;
}
@Override
public void onUserUpdateGlobalName(@NonNull BatUser user, String oldName, String newName, @NonNull UserUpdateGlobalNameEvent event) {
NameHistoryProfile profile = user.getNameHistoryProfile();
profile.addName(newName);
userService.saveUser(user);
}
@Override
public void onGuildMemberUpdateNickname(@NonNull BatGuild guild, @NonNull BatUser user, String oldName, String newName,
@NonNull GuildMemberUpdateNicknameEvent event) {
cc.fascinated.bat.features.namehistory.profile.guild.NameHistoryProfile profile = guild.getNameHistoryProfile();
profile.addName(user, newName);
guildService.saveGuild(guild);
}
}

@ -0,0 +1,22 @@
package cc.fascinated.bat.features.namehistory;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Date;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor @Getter
public class TrackedName {
/**
* The new name of the user
*/
private final String name;
/**
* The date the name was changed
*/
private final Date changedDate;
}

@ -0,0 +1,62 @@
package cc.fascinated.bat.features.namehistory.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.namehistory.TrackedName;
import cc.fascinated.bat.features.namehistory.profile.guild.NameHistoryProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.UserService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("namehistory:guild.sub")
@CommandInfo(name = "guild", description = "View the guild nickname history of a user")
public class GuildSubCommand extends BatSubCommand {
private final UserService userService;
@Autowired
public GuildSubCommand(@NonNull UserService userService) {
this.userService = userService;
super.addOption(OptionType.USER, "user", "The user to view the name history of", false);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
OptionMapping userOption = interaction.getOption("user");
BatUser target = userOption == null ? user : userService.getUser(userOption.getAsUser().getId());
if (target == null) {
channel.sendMessage("User not found").queue();
return;
}
NameHistoryProfile profile = guild.getNameHistoryProfile();
StringBuilder builder = new StringBuilder();
if (profile.getNameHistory(target).isEmpty()) {
builder.append("%s has no name history".formatted(target.getDiscordUser().getAsMention()));
} else {
for (TrackedName trackedName : profile.getNameHistorySorted(target)) {
builder.append("%s - <t:%s>\n".formatted(
trackedName.getName() == null ? "Removed Nickname" : "`%s`".formatted(trackedName.getName()),
trackedName.getChangedDate().toInstant().toEpochMilli()/1000
));
}
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("%s's Nickname History in %s".formatted(target.getName(), guild.getName()))
.setDescription(builder.toString())
.build()
).queue();
}
}

@ -0,0 +1,21 @@
package cc.fascinated.bat.features.namehistory.command;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(name = "namehistory", description = "View the name history of a user")
public class NameHistoryCommand extends BatCommand {
@Autowired
public NameHistoryCommand(@NonNull ApplicationContext context) {
super.addSubCommand(context.getBean(UserSubCommand.class));
super.addSubCommand(context.getBean(GuildSubCommand.class));
}
}

@ -0,0 +1,59 @@
package cc.fascinated.bat.features.namehistory.command;
import cc.fascinated.bat.command.BatSubCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.namehistory.TrackedName;
import cc.fascinated.bat.features.namehistory.profile.user.NameHistoryProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.UserService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("namehistory:user.sub")
@CommandInfo(name = "user", description = "View the global name history of a user", guildOnly = false)
public class UserSubCommand extends BatSubCommand {
private final UserService userService;
@Autowired
public UserSubCommand(@NonNull UserService userService) {
this.userService = userService;
super.addOption(OptionType.USER, "user", "The user to view the name history of", false);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) {
OptionMapping userOption = interaction.getOption("user");
BatUser target = userOption == null ? user : userService.getUser(userOption.getAsUser().getId());
if (target == null) {
channel.sendMessage("User not found").queue();
return;
}
NameHistoryProfile profile = target.getNameHistoryProfile();
StringBuilder builder = new StringBuilder();
if (profile.getNameHistory().isEmpty()) {
builder.append("%s has no name history".formatted(target.getDiscordUser().getAsMention()));
} else {
for (TrackedName trackedName : profile.getNameHistorySorted()) {
builder.append("`%s` - <t:%s>\n".formatted(trackedName.getName(), trackedName.getChangedDate().toInstant().toEpochMilli()/1000));
}
}
interaction.replyEmbeds(EmbedUtils.genericEmbed()
.setAuthor("%s's Global Name History".formatted(target.getName()))
.setDescription(builder.toString())
.build()
).queue();
}
}

@ -0,0 +1,78 @@
package cc.fascinated.bat.features.namehistory.profile.guild;
import cc.fascinated.bat.common.Profile;
import cc.fascinated.bat.features.namehistory.NameHistoryFeature;
import cc.fascinated.bat.features.namehistory.TrackedName;
import cc.fascinated.bat.model.BatUser;
import java.util.*;
/**
* @author Fascinated (fascinated7)
*/
public class NameHistoryProfile extends Profile {
private Map<String, List<TrackedName>> nameHistory;
public NameHistoryProfile() {
super("name-history");
}
/**
* Gets the name history of the user
*
* @param user the user to get the name history of
* @return the name history of the user
*/
public List<TrackedName> getNameHistory(BatUser user) {
if (this.nameHistory == null) {
this.nameHistory = new HashMap<>();
}
if (!this.nameHistory.containsKey(user.getId())) {
this.nameHistory.put(user.getId(), new LinkedList<>());
}
return this.nameHistory.get(user.getId());
}
/**
* Gets the name history of the user sorted
*
* @param user the user to get the name history of
* @return the name history of the user sorted
*/
public List<TrackedName> getNameHistorySorted(BatUser user) {
return getNameHistory(user).stream().sorted((o1, o2) -> o2.getChangedDate().compareTo(o1.getChangedDate())).toList();
}
/**
* Adds a name to the name history
*
* @param user the user to add the name to
* @param name the name to add
*/
public void addName(BatUser user, String name) {
getNameHistory(user).add(new TrackedName(name, new Date()));
cleanup();
}
/**
* Cleans up the name history
* <p>
* This will remove any names that are not within
* the size limit of the name history
* </p>
*/
private void cleanup() {
for (String userId : this.nameHistory.keySet()) {
List<TrackedName> trackedNames = this.nameHistory.get(userId);
if (trackedNames.size() > NameHistoryFeature.NAME_HISTORY_SIZE) {
trackedNames.sort((o1, o2) -> o2.getChangedDate().compareTo(o1.getChangedDate()));
trackedNames.subList(NameHistoryFeature.NAME_HISTORY_SIZE, trackedNames.size()).clear();
}
}
}
@Override
public void reset() {
this.nameHistory = null;
}
}

@ -0,0 +1,75 @@
package cc.fascinated.bat.features.namehistory.profile.user;
import cc.fascinated.bat.common.Profile;
import cc.fascinated.bat.features.namehistory.NameHistoryFeature;
import cc.fascinated.bat.features.namehistory.TrackedName;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
/**
* @author Fascinated (fascinated7)
*/
public class NameHistoryProfile extends Profile {
private List<TrackedName> nameHistory;
public NameHistoryProfile() {
super("name-history");
}
/**
* Gets the name history of the user
*
* @return the name history of the user
*/
public List<TrackedName> getNameHistory() {
if (this.nameHistory == null) {
this.nameHistory = new LinkedList<>();
}
return nameHistory;
}
/**
* Gets the name history of the user sorted
*
* @return the name history of the user sorted
*/
public List<TrackedName> getNameHistorySorted() {
return getNameHistory().stream().sorted((o1, o2) -> o2.getChangedDate().compareTo(o1.getChangedDate())).toList();
}
/**
* Adds a name to the name history
*
* @param name the name to add
*/
public void addName(String name) {
if (this.nameHistory == null) {
this.nameHistory = new LinkedList<>();
}
this.nameHistory.add(new TrackedName(name, new Date()));
cleanup();
}
/**
* Cleans up the name history
* <p>
* This will remove any names that are not within
* the size limit of the name history
* </p>
*/
private void cleanup() {
if (this.nameHistory.size() > NameHistoryFeature.NAME_HISTORY_SIZE) {
List<TrackedName> trackedNames = new ArrayList<>(this.nameHistory);
trackedNames.sort((o1, o2) -> o2.getChangedDate().compareTo(o1.getChangedDate()));
this.nameHistory = trackedNames.subList(0, NameHistoryFeature.NAME_HISTORY_SIZE);
}
}
@Override
public void reset() {
this.nameHistory = null;
}
}

@ -1,4 +1,4 @@
package cc.fascinated.bat.migrations;
package cc.fascinated.bat.migrations.changelogs;
import com.mongodb.client.FindIterable;
import io.mongock.api.annotations.ChangeUnit;

@ -0,0 +1,86 @@
package cc.fascinated.bat.migrations.changelogs;
import com.mongodb.client.FindIterable;
import io.mongock.api.annotations.ChangeUnit;
import io.mongock.api.annotations.Execution;
import io.mongock.api.annotations.RollbackExecution;
import org.bson.Document;
import org.springframework.data.mongodb.core.MongoTemplate;
/**
* @author Fascinated (fascinated7)
*/
@ChangeUnit(id = "scoresaber-profile-rename-package-changelog", order = "001", author = "fascinated7")
public class ScoresaberProfileRenamePackageChangelog {
private final MongoTemplate mongoTemplate;
public ScoresaberProfileRenamePackageChangelog(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
@Execution
public void changeSet() {
FindIterable<Document> guilds = mongoTemplate.getCollection("guilds").find();
guilds.forEach(guild -> {
Document profiles = guild.get("profiles", Document.class);
if (profiles == null) {
return;
}
// NumberOneScoreFeedProfile
Document numberOneScoreFeedProfile = profiles.get("scoresaber-number-one-score-feed", Document.class);
if (numberOneScoreFeedProfile == null) {
return;
}
numberOneScoreFeedProfile.put("_class", "cc.fascinated.bat.features.scoresaber.profile.guild.NumberOneScoreFeedProfile");
profiles.put("scoresaber-number-one-score-feed", numberOneScoreFeedProfile);
guild.put("profiles", profiles);
mongoTemplate.getCollection("guilds").replaceOne(new Document("_id", guild.get("_id")), guild);
});
guilds.forEach(guild -> {
Document profiles = guild.get("profiles", Document.class);
if (profiles == null) {
return;
}
// UserScoreFeedProfile
Document userScoreFeedProfile = profiles.get("scoresaber-number-one-score-feed", Document.class);
if (userScoreFeedProfile == null) {
return;
}
userScoreFeedProfile.put("_class", "cc.fascinated.bat.features.scoresaber.profile.guild.UserScoreFeedProfile");
profiles.put("scoresaber-user-score-feed", userScoreFeedProfile);
guild.put("profiles", profiles);
mongoTemplate.getCollection("guilds").replaceOne(new Document("_id", guild.get("_id")), guild);
});
FindIterable<Document> users = mongoTemplate.getCollection("users").find();
users.forEach(guild -> {
Document profiles = guild.get("profiles", Document.class);
if (profiles == null) {
return;
}
// ScoreSaberProfile
Document userScoreFeedProfile = profiles.get("scoresaber", Document.class);
if (userScoreFeedProfile == null) {
return;
}
userScoreFeedProfile.put("_class", "cc.fascinated.bat.features.scoresaber.profile.user.ScoreSaberProfile");
profiles.put("scoresaber", userScoreFeedProfile);
guild.put("profiles", profiles);
mongoTemplate.getCollection("users").replaceOne(new Document("_id", guild.get("_id")), guild);
});
}
@RollbackExecution
public void rollback() {
// DO NOTHING
}
}

@ -1,6 +1,7 @@
package cc.fascinated.bat.model;
import cc.fascinated.bat.common.ProfileHolder;
import cc.fascinated.bat.features.namehistory.profile.guild.NameHistoryProfile;
import cc.fascinated.bat.service.DiscordService;
import lombok.*;
import net.dv8tion.jda.api.entities.Guild;
@ -65,6 +66,15 @@ public class BatGuild extends ProfileHolder {
return DiscordService.JDA.getGuildById(id);
}
/**
* Gets the user's name history profile
*
* @return the user's name history profile
*/
public NameHistoryProfile getNameHistoryProfile() {
return getProfile(NameHistoryProfile.class);
}
@AllArgsConstructor
@Getter
@Setter

@ -1,6 +1,7 @@
package cc.fascinated.bat.model;
import cc.fascinated.bat.common.ProfileHolder;
import cc.fascinated.bat.features.namehistory.profile.user.NameHistoryProfile;
import cc.fascinated.bat.features.scoresaber.profile.user.ScoreSaberProfile;
import cc.fascinated.bat.service.DiscordService;
import lombok.Getter;
@ -37,7 +38,7 @@ public class BatUser extends ProfileHolder {
* The name of the user
*/
public String getName() {
return getDiscordUser().getName();
return getDiscordUser().getEffectiveName();
}
/**
@ -57,4 +58,13 @@ public class BatUser extends ProfileHolder {
public ScoreSaberProfile getScoreSaberProfile() {
return getProfile(ScoreSaberProfile.class);
}
/**
* Gets the user's name history profile
*
* @return the user's name history profile
*/
public NameHistoryProfile getNameHistoryProfile() {
return getProfile(NameHistoryProfile.class);
}
}

@ -7,10 +7,12 @@ import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent;
import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameEvent;
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.events.user.update.UserUpdateGlobalNameEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
@ -131,4 +133,29 @@ public class EventService extends ListenerAdapter {
listener.onModalInteraction(guild, user, event);
}
}
@Override
public void onUserUpdateGlobalName(@NotNull UserUpdateGlobalNameEvent event) {
if (event.getUser().isBot()) {
return;
}
BatUser user = userService.getUser(event.getUser().getId());
for (EventListener listener : LISTENERS) {
listener.onUserUpdateGlobalName(user, event.getOldGlobalName(), event.getNewGlobalName(), event);
}
}
@Override
public void onGuildMemberUpdateNickname(@NotNull GuildMemberUpdateNicknameEvent event) {
if (event.getUser().isBot()) {
return;
}
BatGuild guild = guildService.getGuild(event.getGuild().getId());
BatUser user = userService.getUser(event.getUser().getId());
for (EventListener listener : LISTENERS) {
listener.onGuildMemberUpdateNickname(guild, user, event.getOldNickname(), event.getNewNickname(), event);
}
}
}