Skip to content

Commit 271070b

Browse files
authored
Merge pull request #430 from danthe1st/1984
staff activity tracking
2 parents c9d6cc7 + 16970d4 commit 271070b

File tree

7 files changed

+218
-0
lines changed

7 files changed

+218
-0
lines changed

src/main/java/net/javadiscord/javabot/data/config/guild/ModerationConfig.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ public class ModerationConfig extends GuildConfigItem {
3434
* The threshold for deleting a message in #share-knowledge. Note that this should be strictly < 0.
3535
*/
3636
private int shareKnowledgeMessageDeleteThreshold;
37+
38+
/**
39+
* ID of the channel storing staff activity information.
40+
*/
41+
private long staffActivityChannelId = 0;
3742

3843
/**
3944
* The threshold for deleting a message in #looking-for-programmer.
@@ -105,6 +110,10 @@ public ForumChannel getProjectChannel() {
105110
public ForumChannel getJobChannel() {
106111
return this.getGuild().getForumChannelById(this.jobChannelId);
107112
}
113+
114+
public TextChannel getStaffActivityChannel() {
115+
return this.getGuild().getTextChannelById(this.staffActivityChannelId);
116+
}
108117

109118
public ForumChannel getShareKnowledgeChannel() {
110119
return this.getGuild().getForumChannelById(this.shareKnowledgeChannelId);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package net.javadiscord.javabot.systems.staff_activity;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import net.dv8tion.jda.api.entities.Member;
5+
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
6+
import net.dv8tion.jda.api.hooks.ListenerAdapter;
7+
import net.javadiscord.javabot.data.config.BotConfig;
8+
import net.javadiscord.javabot.data.config.guild.ModerationConfig;
9+
10+
/**
11+
* Listener for tracking staff activity.
12+
* Each time the staff member sends a message, the staff activity message is updated.
13+
* @see StaffActivityService
14+
*/
15+
@RequiredArgsConstructor
16+
public class StaffActivityListener extends ListenerAdapter {
17+
18+
private final BotConfig botConfig;
19+
private final StaffActivityService service;
20+
21+
@Override
22+
public void onMessageReceived(MessageReceivedEvent event) {
23+
if (!event.isFromGuild()) {
24+
return;
25+
}
26+
if (event.getAuthor().isBot() || event.getAuthor().isSystem()) {
27+
return;
28+
}
29+
ModerationConfig moderationConfig = botConfig.get(event.getGuild()).getModerationConfig();
30+
Member member = event.getMember();
31+
if (!member.getRoles().contains(moderationConfig.getStaffRole())) {
32+
return;
33+
}
34+
35+
service.updateStaffActivity(StaffActivityType.LAST_MESSAGE, event.getMessage().getTimeCreated(), member);
36+
}
37+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package net.javadiscord.javabot.systems.staff_activity;
2+
3+
import java.time.temporal.TemporalAccessor;
4+
import java.util.Iterator;
5+
import java.util.List;
6+
7+
import org.springframework.stereotype.Service;
8+
9+
import lombok.RequiredArgsConstructor;
10+
import net.dv8tion.jda.api.EmbedBuilder;
11+
import net.dv8tion.jda.api.entities.Member;
12+
import net.dv8tion.jda.api.entities.Message;
13+
import net.dv8tion.jda.api.entities.MessageEmbed;
14+
import net.dv8tion.jda.api.entities.MessageEmbed.Field;
15+
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
16+
import net.dv8tion.jda.api.utils.TimeFormat;
17+
import net.javadiscord.javabot.data.config.BotConfig;
18+
import net.javadiscord.javabot.systems.staff_activity.dao.StaffActivityMessageRepository;
19+
import net.javadiscord.javabot.systems.staff_activity.model.StaffActivityMessage;
20+
import net.javadiscord.javabot.util.ExceptionLogger;
21+
22+
/**
23+
* Responsible for staff activity tracking.
24+
* This class maintains a message for each staff member in each channel.
25+
* This message is updated when a staff activity action occurs.
26+
*/
27+
@Service
28+
@RequiredArgsConstructor
29+
public class StaffActivityService {
30+
private final BotConfig botConfig;
31+
private final StaffActivityMessageRepository repository;
32+
33+
/**
34+
* Updates the staff activity message or creates it if necessary.
35+
* Called when a tracked staff activity is executed.
36+
* If no channel is configured for staff activity, this method doesn't do anything.
37+
* @param type The type of the occured staff activity
38+
* @param timestamp The timestamp when the activity occured
39+
* @param member The saff member
40+
*/
41+
public void updateStaffActivity(StaffActivityType type, TemporalAccessor timestamp, Member member) {
42+
TextChannel staffActivityChannel = botConfig.get(member.getGuild()).getModerationConfig().getStaffActivityChannel();
43+
if (staffActivityChannel == null) {
44+
return;
45+
}
46+
Long msgId = repository.getMessageId(staffActivityChannel.getGuild().getIdLong(), member.getIdLong());
47+
if (msgId != null) {
48+
staffActivityChannel
49+
.retrieveMessageById(msgId)
50+
.queue(
51+
activityMessage -> replaceActivityMessage(type, timestamp, activityMessage, member),
52+
notFound -> createNewMessage(staffActivityChannel, member, type, timestamp));
53+
} else {
54+
createNewMessage(staffActivityChannel, member, type, timestamp);
55+
}
56+
}
57+
58+
private void replaceActivityMessage(StaffActivityType type, TemporalAccessor timestamp, Message activityMessage, Member member) {
59+
List<MessageEmbed> embeds = activityMessage.getEmbeds();
60+
MessageEmbed embed;
61+
if(embeds.isEmpty()) {
62+
embed = createStaffActivityEmbedWithEntry(member, type, timestamp);
63+
}else {
64+
embed = replaceActivityEmbedField(embeds.get(0), type, timestamp);
65+
}
66+
activityMessage.editMessageEmbeds(embed).queue();
67+
}
68+
69+
private MessageEmbed replaceActivityEmbedField(MessageEmbed embed, StaffActivityType type, TemporalAccessor timestamp) {
70+
EmbedBuilder eb = new EmbedBuilder(embed);
71+
for (Iterator<Field> it = eb.getFields().iterator(); it.hasNext();) {
72+
Field field = it.next();
73+
if(type.getTitle().equals(field.getName())) {
74+
it.remove();
75+
}
76+
}
77+
eb.addField(type.getTitle(), TimeFormat.RELATIVE.format(timestamp), false);
78+
return eb.build();
79+
}
80+
81+
void createNewMessage(TextChannel staffActivityChannel, Member member, StaffActivityType type, TemporalAccessor timestamp) {
82+
MessageEmbed embed = createStaffActivityEmbedWithEntry(member, type, timestamp);
83+
staffActivityChannel.sendMessageEmbeds(embed).queue(success ->
84+
repository.insertOrReplace(new StaffActivityMessage(member.getGuild().getIdLong(), member.getIdLong(), success.getIdLong())),
85+
error -> ExceptionLogger.capture(error, "Cannot create new staff activity message")
86+
);
87+
}
88+
89+
private MessageEmbed createStaffActivityEmbedWithEntry(Member member, StaffActivityType type, TemporalAccessor timestamp) {
90+
return replaceActivityEmbedField(createEmptyStaffActivityEmbed(member), type, timestamp);
91+
}
92+
93+
private MessageEmbed createEmptyStaffActivityEmbed(Member member) {
94+
return new EmbedBuilder()
95+
.setAuthor(member.getEffectiveName(), null, member.getEffectiveAvatarUrl())
96+
.setFooter(member.getId())
97+
.build();
98+
}
99+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package net.javadiscord.javabot.systems.staff_activity;
2+
3+
import lombok.Getter;
4+
import lombok.RequiredArgsConstructor;
5+
6+
/**
7+
* Types of recorded staff activities.
8+
*/
9+
@RequiredArgsConstructor
10+
public enum StaffActivityType {
11+
/**
12+
* The last message sent by the staff member.
13+
*/
14+
LAST_MESSAGE("Last message sent");
15+
16+
@Getter
17+
private final String title;
18+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package net.javadiscord.javabot.systems.staff_activity.dao;
2+
3+
import org.springframework.jdbc.core.JdbcTemplate;
4+
import org.springframework.stereotype.Repository;
5+
6+
import lombok.RequiredArgsConstructor;
7+
import net.javadiscord.javabot.systems.staff_activity.model.StaffActivityMessage;
8+
9+
/**
10+
* Repository for storing staff activity message locations.
11+
*/
12+
@RequiredArgsConstructor
13+
@Repository
14+
public class StaffActivityMessageRepository {
15+
private final JdbcTemplate jdbcTemplate;
16+
17+
/**
18+
* Inserts a new {@link StaffActivityMessage} or replaces an old one.
19+
* @param msg the {@link StaffActivityMessage} to store
20+
*/
21+
public void insertOrReplace(StaffActivityMessage msg) {
22+
jdbcTemplate.update("""
23+
MERGE INTO staff_activity_messages
24+
(guild_id, user_id, message_id)
25+
KEY (guild_id, user_id)
26+
VALUES
27+
(?,?,?)
28+
""", msg.guildId(), msg.userId(), msg.messageId());
29+
}
30+
31+
/**
32+
* gets the ID of the activity message of a specific staff member.
33+
* @param guildId the ID of the relevant guild
34+
* @param userId the ID of the staff member
35+
* @return the message ID of the activity message
36+
*/
37+
public Long getMessageId(long guildId, long userId) {
38+
return jdbcTemplate.query("SELECT message_id FROM staff_activity_messages WHERE guild_id=? AND user_id=?", rs-> rs.next() ? (Long)rs.getLong(1) : null, guildId, userId);
39+
}
40+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package net.javadiscord.javabot.systems.staff_activity.model;
2+
3+
/**
4+
* Represents metadata of a message where activity information of a staff member is stored.
5+
* @param guildId the ID of the guild
6+
* @param userId the ID of the staff member
7+
* @param messageId the ID of the message storing activity information about the staff member
8+
*/
9+
public record StaffActivityMessage(long guildId, long userId, long messageId) {}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
CREATE TABLE staff_activity_messages (
2+
guild_id BIGINT NOT NULL,
3+
user_id BIGINT NOT NULL,
4+
message_id BIGINT NOT NULL,
5+
PRIMARY KEY(guild_id, user_id)
6+
)

0 commit comments

Comments
 (0)