Skip to content

Commit

Permalink
Call old button command (#427)
Browse files Browse the repository at this point in the history
* Fetch old button command
  • Loading branch information
twonirwana authored Jan 28, 2024
1 parent 7e83d9d commit 41e1ea3
Show file tree
Hide file tree
Showing 26 changed files with 610 additions and 128 deletions.
16 changes: 13 additions & 3 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ toc::[]

== Quickstart

The bot has list a ready to play list of presets for many RPG systems. Simple select a system out of the list or keep typing to search and filter in the list. All these presets are realised with the user available commands and dice expression and can be adapted and extended. Please let me know if you have a good new preset or an improved version for an existing preset.
The bot has list a ready to play list of presets for many RPG systems.
Simple select a system out of the list or keep typing to search and filter in the list.
All these presets are realised with the user available commands and dice expression and can be adapted and extended.
Please let me know if you have a good new preset or an improved version for an existing preset.

[cols="1,1"]
|===
Expand Down Expand Up @@ -94,7 +97,7 @@ If a message sends the answer to another channel, it will not be moved to the en

=== Language

The bot supports multiple languages (currently English, German, Brazilian Portuguese and French).
The bot supports multiple languages (currently English, German, Brazilian Portuguese and French).
The bot will use the discord client language for the slash command and options.
Each button message has a language configuration and will return its answers always in the language of its configuration.
All existing button messages are default in English.
Expand Down Expand Up @@ -301,6 +304,7 @@ This will provide a button called `Attack` which rolls with a personalized modif
==== Examples

===== Dungeon & Dragons 5e with Dice Images

`/custom_dice start buttons: 2d20k1@D20 Advantage;2d20L1@D20 Disadvantage;D20;;1d4;1d6;1d8;1d10;1d12;1d100;2d4=@2d4;2d6=@2d6;2d8=@2d8;2d10=@2d10;2d12=@2d12;2d20=@2d20 answer_format: without_expression dice_image_style: polyhedral_RdD dice_image_color: default`

===== Powered by the Apocalypse
Expand Down Expand Up @@ -337,8 +341,8 @@ The command uses the `without_expression` as default answer format.
==== Examples

===== Vampire 5ed
`/custom_parameter start expression: val('$r',{regular dice:1\<\=>16}d10 col 'blue') val('$h',{hunger dice:0\<\=>5}d10 col 'purple_dark') val('$s',('$r' + '$h')>=6c) val('$rt','$r'==10c) val('$ht','$h'==10c) val('$ho','$h'==1c) val('$2s',( ( ('$rt' + '$ht'=) ) /2)*2) val('$ts',('$s' + '$2s'=)) concat('successes: ', '$ts', ifE('$ts',0,ifG('$ho',1,' bestial failure' , ''),''), ifE('$rt' mod 2, 1, ifE('$ht' mod 2, 1, ' messy critical', ''), '')) answer_format: without_expression dice_image_style: polyhedral_knots dice_image_color: blue`

`/custom_parameter start expression: val('$r',{regular dice:1\<\=>16}d10 col 'blue') val('$h',{hunger dice:0\<\=>5}d10 col 'purple_dark') val('$s',('$r' + '$h')>=6c) val('$rt','$r'==10c) val('$ht','$h'==10c) val('$ho','$h'==1c) val('$2s',( ( ('$rt' + '$ht'=) ) /2)*2) val('$ts',('$s' + '$2s'=)) concat('successes: ', '$ts', ifE('$ts',0,ifG('$ho',1,' bestial failure' , ''),''), ifE('$rt' mod 2, 1, ifE('$ht' mod 2, 1, ' messy critical', ''), '')) answer_format: without_expression dice_image_style: polyhedral_knots dice_image_color: blue`

===== nWod / Chronicles of Darkness

Expand Down Expand Up @@ -463,6 +467,12 @@ For example: `/channel_config channel_alias multi_save aliases: att:2d20;dmg:2d6
* `delete` removes an alias by its name
* `list` provides a list of all alias

=== Fetch Command

The command moves the last existing button message to the bottom of the channel.
The message must be at least 1min old.
The state of the button message will be lost and reset as if new created.

=== Clear Command

The clear command removes all button configuration in a channel from the bot and deletes the button messages.
Expand Down
4 changes: 3 additions & 1 deletion bot/src/main/java/de/janno/discord/bot/Bot.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@


import com.google.common.base.Strings;
import de.janno.discord.bot.command.FetchCommand;
import de.janno.discord.bot.command.ClearCommand;
import de.janno.discord.bot.command.channelConfig.ChannelConfigCommand;
import de.janno.discord.bot.command.countSuccesses.CountSuccessesCommand;
Expand Down Expand Up @@ -71,7 +72,8 @@ public static void main(final String[] args) throws Exception {
welcomeCommand,
new ClearCommand(persistenceManager),
new QuickstartCommand(rpgSystemCommandPreset),
new HelpCommand()
new HelpCommand(),
new FetchCommand(persistenceManager, customParameterCommand, customDiceCommand, sumCustomSetCommand)
),
List.of(customDiceCommand,
sumCustomSetCommand,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.Collectors;
Expand All @@ -42,8 +41,6 @@ public abstract class AbstractCommand<C extends Config, S extends StateData> imp
private static final String HELP_OPTION_NAME = "help";

private static final int MIN_MS_DELAY_BETWEEN_BUTTON_MESSAGES = io.avaje.config.Config.getInt("command.minDelayBetweenButtonMessagesMs", 1000);
private final static ConcurrentSkipListSet<Long> MESSAGE_STATE_IDS_TO_DELETE = new ConcurrentSkipListSet<>();
private static final Duration DELAY_MESSAGE_DATA_DELETION = Duration.ofMillis(io.avaje.config.Config.getLong("command.delayMessageDataDeletionMs", 10000));
protected final PersistenceManager persistenceManager;

protected AbstractCommand(PersistenceManager persistenceManager) {
Expand Down Expand Up @@ -272,7 +269,7 @@ public Mono<Void> handleComponentInteractEvent(@NonNull ButtonEventAdaptor event
if (newButtonMessage.isPresent() && answerTargetChannelId == null) {
actions.add(Mono.defer(() -> event.createMessageWithoutReference(newButtonMessage.get()))
.doOnNext(newMessageId -> createEmptyMessageData(configUUID, guildId, channelId, newMessageId))
.flatMap(newMessageId -> deleteOldAndConcurrentMessageAndData(newMessageId, configUUID, channelId, event))
.flatMap(newMessageId -> MessageDeletionHelper.deleteOldMessageAndData(persistenceManager, newMessageId, event.getMessageId(), configUUID, channelId, event))
.delaySubscription(calculateDelay(event))
.doOnSuccess(v -> BotMetrics.timerNewButtonMessageMetricCounter(getCommandId(), stopwatch.elapsed()))
.then());
Expand All @@ -283,7 +280,7 @@ public Mono<Void> handleComponentInteractEvent(@NonNull ButtonEventAdaptor event

if (deleteCurrentButtonMessage) {
actions.add(Mono.defer(() -> event.deleteMessageById(messageId)
.then(deleteMessageDataWithDelay(channelId, messageId))));
.then(MessageDeletionHelper.deleteMessageDataWithDelay(persistenceManager, channelId, messageId))));
} else {
//don't update the state data async or there will be racing conditions
updateCurrentMessageStateData(configUUID, guildId, channelId, messageId, config, state);
Expand All @@ -294,7 +291,7 @@ public Mono<Void> handleComponentInteractEvent(@NonNull ButtonEventAdaptor event
.then();
}

protected void addFurtherActions(List<Mono<Void>> actions, ButtonEventAdaptor event, C config, State<S> state){
protected void addFurtherActions(List<Mono<Void>> actions, ButtonEventAdaptor event, C config, State<S> state) {

}

Expand All @@ -313,52 +310,6 @@ private Duration calculateDelay(ButtonEventAdaptor event) {
return Duration.ZERO;
}

private Mono<Void> deleteMessageDataWithDelay(long channelId, long messageId) {
MESSAGE_STATE_IDS_TO_DELETE.add(messageId);
return Mono.defer(() -> Mono.just(0)
.delayElement(DELAY_MESSAGE_DATA_DELETION)
.doOnNext(v -> {
MESSAGE_STATE_IDS_TO_DELETE.remove(messageId);
persistenceManager.deleteStateForMessage(channelId, messageId);
}).ofType(Void.class));
}

protected Mono<Void> deleteOldAndConcurrentMessageAndData(
long newMessageId,
@NonNull UUID configUUID,
long channelId,
@NonNull ButtonEventAdaptor event) {

Set<Long> ids = persistenceManager.getAllMessageIdsForConfig(configUUID).stream()
//this will already delete directly
.filter(id -> id != event.getMessageId())
//we don't want to delete the new message
.filter(id -> id != newMessageId)
//we don't want to check the state of messages where the data is already scheduled to be deleted
.filter(id -> !MESSAGE_STATE_IDS_TO_DELETE.contains(id))
.collect(Collectors.toSet());

if (ids.size() > 5) { //there should be not many old message data
log.warn(String.format("ConfigUUID %s had %d to many messageData persisted", configUUID, ids.size()));
}

if (ids.isEmpty()) {
return Mono.empty();
}

return event.getMessagesState(ids)
.flatMap(ms -> {
if (ms.isCanBeDeleted() && !ms.isPinned() && ms.isExists() && ms.getCreationTime() != null) {
return event.deleteMessageById(ms.getMessageId())
.then(deleteMessageDataWithDelay(channelId, ms.getMessageId()));
} else if (!ms.isExists()) {
return deleteMessageDataWithDelay(channelId, ms.getMessageId());
} else {
return Mono.empty();
}
}).then();
}

@Override
public @NonNull Mono<Void> handleSlashCommandEvent(@NonNull SlashEventAdaptor event, @NonNull Supplier<UUID> uuidSupplier, @NonNull Locale userLocale) {
Optional<String> checkPermissions = event.checkPermissions(userLocale);
Expand Down
93 changes: 93 additions & 0 deletions bot/src/main/java/de/janno/discord/bot/command/FetchCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package de.janno.discord.bot.command;

import de.janno.discord.bot.BotMetrics;
import de.janno.discord.bot.I18n;
import de.janno.discord.bot.command.customDice.CustomDiceCommand;
import de.janno.discord.bot.command.customDice.CustomDiceConfig;
import de.janno.discord.bot.command.customParameter.CustomParameterCommand;
import de.janno.discord.bot.command.customParameter.CustomParameterConfig;
import de.janno.discord.bot.command.sumCustomSet.SumCustomSetCommand;
import de.janno.discord.bot.command.sumCustomSet.SumCustomSetConfig;
import de.janno.discord.bot.persistance.MessageConfigDTO;
import de.janno.discord.bot.persistance.PersistenceManager;
import de.janno.discord.connector.api.SlashCommand;
import de.janno.discord.connector.api.SlashEventAdaptor;
import de.janno.discord.connector.api.message.EmbedOrMessageDefinition;
import de.janno.discord.connector.api.slash.CommandDefinition;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;

@Slf4j
@RequiredArgsConstructor
public class FetchCommand implements SlashCommand {

private final PersistenceManager persistenceManager;
private final CustomParameterCommand customParameterCommand;
private final CustomDiceCommand customDiceCommand;
private final SumCustomSetCommand sumCustomSetCommand;

@Override
public @NonNull String getCommandId() {
return "fetch";
}

@Override
public @NonNull CommandDefinition getCommandDefinition() {
return CommandDefinition.builder()
.name(getCommandId())
.nameLocales(I18n.allNoneEnglishMessagesNames("fetch.name"))
.description(I18n.getMessage("fetch.description", Locale.ENGLISH))
.descriptionLocales(I18n.allNoneEnglishMessagesDescriptions("fetch.description"))
.build();
}

@Override
public @NonNull Mono<Void> handleSlashCommandEvent(@NonNull SlashEventAdaptor event, @NonNull Supplier<UUID> uuidSupplier, @NonNull Locale userLocal) {
BotMetrics.incrementSlashStartMetricCounter(getCommandId(), "[]");
long fetchDelayMs = io.avaje.config.Config.getLong("command.fetch.delayMs", 60_000);
Long oldestMessageIdWaitingToDeleted = MessageDeletionHelper.getMessageWaitingToBeDeleted(event.getChannelId()).stream()
.min(Comparator.comparing(Function.identity()))
.orElse(null);
Optional<MessageConfigDTO> messageConfigDTOOptional = persistenceManager.getLastMessageDataInChannel(event.getChannelId(),
LocalDateTime.now().minus(fetchDelayMs, ChronoUnit.MILLIS),
oldestMessageIdWaitingToDeleted);
if (messageConfigDTOOptional.isPresent()) {
final MessageConfigDTO messageConfigDTO = messageConfigDTOOptional.get();
final UUID configUUID = messageConfigDTO.getConfigUUID();
if (customDiceCommand.getCommandId().equals(messageConfigDTO.getCommandId())) {
CustomDiceConfig config = CustomDiceCommand.deserializeConfig(messageConfigDTO);
return moveButtonMessage(config, customDiceCommand, configUUID, event);
} else if (customParameterCommand.getCommandId().equals(messageConfigDTO.getCommandId())) {
CustomParameterConfig config = CustomParameterCommand.deserializeConfig(messageConfigDTO);
return moveButtonMessage(config, customParameterCommand, configUUID, event);
} else if (sumCustomSetCommand.getCommandId().equals(messageConfigDTO.getCommandId())) {
SumCustomSetConfig config = SumCustomSetCommand.deserializeConfig(messageConfigDTO);
return moveButtonMessage(config, sumCustomSetCommand, configUUID, event);
}
}
return event.reply(I18n.getMessage("fetch.no.message.found", userLocal), true);
}

private <C extends Config> Mono<Void> moveButtonMessage(C config, AbstractCommand<C, ?> command, UUID configUUID, SlashEventAdaptor event) {
EmbedOrMessageDefinition buttonMessage = command.createNewButtonMessage(configUUID, config);
List<Mono<Void>> actions = List.of(Mono.defer(event::acknowledgeAndRemoveSlash),
Mono.defer(() -> event.createMessageWithoutReference(buttonMessage)
.doOnNext(messageId -> command.createEmptyMessageData(configUUID, event.getGuildId(), event.getChannelId(), messageId)))
.flatMap(newMessageId -> MessageDeletionHelper.deleteOldMessageAndData(persistenceManager, newMessageId, null, configUUID, event.getChannelId(), event))
.then());
return Flux.merge(1, actions.toArray(new Mono<?>[0]))
.parallel()
.then();
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package de.janno.discord.bot.command;

import de.janno.discord.bot.persistance.PersistenceManager;
import de.janno.discord.connector.api.DiscordAdapter;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.stream.Collectors;

@Slf4j
public class MessageDeletionHelper {

private final static ConcurrentSkipListSet<MessageIdAndChannelId> MESSAGE_STATE_IDS_TO_DELETE = new ConcurrentSkipListSet<>();
private final static Duration DELAY_MESSAGE_DATA_DELETION = Duration.ofMillis(io.avaje.config.Config.getLong("command.delayMessageDataDeletionMs", 10000));

public static Mono<Void> deleteOldMessageAndData(
PersistenceManager persistenceManager,
long newMessageId,
@Nullable Long currentMessageId,
@NonNull UUID configUUID,
long channelId,
@NonNull DiscordAdapter discordAdapter) {

Set<Long> ids = persistenceManager.getAllMessageIdsForConfig(configUUID).stream()
//this will already delete directly
.filter(id -> !Objects.equals(id, currentMessageId))
//we don't want to delete the new message
.filter(id -> id != newMessageId)
//we don't want to check the state of messages where the data is already scheduled to be deleted
.filter(id -> !MESSAGE_STATE_IDS_TO_DELETE.contains(new MessageIdAndChannelId(id, channelId)))
.collect(Collectors.toSet());

if (ids.size() > 5) { //there should be not many old message data
log.warn(String.format("ConfigUUID %s had %d to many messageData persisted", configUUID, ids.size()));
}

if (ids.isEmpty()) {
return Mono.empty();
}

return discordAdapter.getMessagesState(ids)
.flatMap(ms -> {
if (ms.isCanBeDeleted() && !ms.isPinned() && ms.isExists() && ms.getCreationTime() != null) {
return discordAdapter.deleteMessageById(ms.getMessageId())
.then(deleteMessageDataWithDelay(persistenceManager, channelId, ms.getMessageId()));
} else if (!ms.isExists()) {
return deleteMessageDataWithDelay(persistenceManager, channelId, ms.getMessageId());
} else {
return Mono.empty();
}
}).then();
}

public static Mono<Void> deleteMessageDataWithDelay(PersistenceManager persistenceManager, long channelId, long messageId) {
MessageIdAndChannelId messageIdAndChannelId = new MessageIdAndChannelId(messageId, channelId);
MESSAGE_STATE_IDS_TO_DELETE.add(messageIdAndChannelId);
return Mono.defer(() -> Mono.just(messageIdAndChannelId)
.delayElement(DELAY_MESSAGE_DATA_DELETION)
//add throttle?
.doOnNext(mc -> {
MESSAGE_STATE_IDS_TO_DELETE.remove(mc);
persistenceManager.deleteStateForMessage(mc.channelId(), mc.messageId());
}).ofType(Void.class));
}

public static List<Long> getMessageWaitingToBeDeleted(long channelId) {
return MESSAGE_STATE_IDS_TO_DELETE.stream()
.filter(mc -> mc.channelId() == channelId)
.map(MessageIdAndChannelId::messageId)
.toList();
}

private record MessageIdAndChannelId(long messageId, long channelId) implements Comparable<MessageIdAndChannelId> {
@Override
public int compareTo(@NonNull MessageIdAndChannelId o) {
return this.toString().compareTo(o.toString());
}
}
}
Loading

0 comments on commit 41e1ea3

Please sign in to comment.