[AB-302] refactoring assignable roles to use buttons instead of reactions

moved button related services to component service instead of message service
adding component type to component payload
renamed async role service methods
ignoring bot reactions in starboard
fixing rank not showing correct experience to next level for other members
This commit is contained in:
Sheldan
2021-07-09 02:00:33 +02:00
parent c08134a150
commit 7e7591a4b3
117 changed files with 1886 additions and 2470 deletions

View File

@@ -22,7 +22,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.task.TaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
@@ -118,11 +117,6 @@ public class SyncButtonClickedListenerBean extends ListenerAdapter {
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void executeFeatureListenerInTransaction(ButtonClickedListener listener, ButtonClickedListenerModel model) {
listener.execute(model);
}
private Optional<ButtonClickedListener> findListener(List<ButtonClickedListener> featureAwareListeners, ButtonClickedListenerModel model) {
return featureAwareListeners.stream().filter(asyncButtonClickedListener -> asyncButtonClickedListener.handlesEvent(model)).findFirst();

View File

@@ -17,6 +17,7 @@ import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.interactions.components.ActionRow;
import net.dv8tion.jda.api.requests.RestAction;
import net.dv8tion.jda.api.requests.restaction.MessageAction;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -229,13 +230,20 @@ public class ChannelServiceBean implements ChannelService {
List<ActionRow> actionRows = messageToSend.getActionRows();
if(!actionRows.isEmpty() && textChannel instanceof GuildChannel) {
GuildChannel channel = (GuildChannel) textChannel;
List<List<ActionRow>> groupedActionRows = ListUtils.partition(actionRows, ComponentService.MAX_BUTTONS_PER_ROW);
for (int i = 0; i < allMessageActions.size(); i++) {
allMessageActions.set(i, allMessageActions.get(i).setActionRows(groupedActionRows.get(i)));
}
for (int i = allMessageActions.size(); i < groupedActionRows.size(); i++) {
// TODO maybe possible nicer
allMessageActions.add(textChannel.sendMessage(".").setActionRows(groupedActionRows.get(i)));
}
AServer server = serverManagementService.loadServer(channel.getGuild());
allMessageActions.set(0, allMessageActions.get(0).setActionRows(actionRows));
actionRows.forEach(components -> components.forEach(component -> {
String id = component.getId();
MessageToSend.ComponentConfig payload = messageToSend.getComponentPayloads().get(id);
if(payload.getPersistCallback()) {
componentPayloadManagementService.createPayload(id, payload.getPayload(), payload.getPayloadType(), payload.getComponentOrigin(), server);
if(payload != null && payload.getPersistCallback()) {
componentPayloadManagementService.createPayload(id, payload.getPayload(), payload.getPayloadType(), payload.getComponentOrigin(), server, payload.getComponentType());
}
}));
}

View File

@@ -0,0 +1,34 @@
package dev.sheldan.abstracto.core.service;
import com.google.gson.Gson;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.ComponentPayload;
import dev.sheldan.abstracto.core.models.database.ComponentType;
import dev.sheldan.abstracto.core.models.template.button.ButtonPayload;
import dev.sheldan.abstracto.core.service.management.ComponentPayloadManagementService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class ComponentPayloadServiceBean implements ComponentPayloadService{
@Autowired
private ComponentPayloadManagementService componentPayloadManagementService;
@Autowired
private Gson gson;
@Override
public ComponentPayload createButtonPayload(String componentId, ButtonPayload payload, String origin, AServer server) {
String json = gson.toJson(payload);
return componentPayloadManagementService.createPayload(componentId, json, payload.getClass(), origin, server, ComponentType.BUTTON);
}
@Override
public ComponentPayload createSelectionPayload(String componentId, ButtonPayload payload, String origin, AServer server) {
String json = gson.toJson(payload);
return componentPayloadManagementService.createPayload(componentId, json, payload.getClass(), origin, server, ComponentType.SELECTION);
}
}

View File

@@ -1,11 +1,29 @@
package dev.sheldan.abstracto.core.service;
import net.dv8tion.jda.api.entities.Emoji;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.interactions.components.ActionRow;
import net.dv8tion.jda.api.interactions.components.Button;
import net.dv8tion.jda.api.interactions.components.ButtonStyle;
import org.apache.commons.collections4.ListUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@Component
public class ComponentServiceBean implements ComponentService {
@Autowired
private MessageService messageService;
@Autowired
private ChannelService channelService;
@Override
public String generateComponentId(Long serverId) {
return generateComponentId();
@@ -15,4 +33,99 @@ public class ComponentServiceBean implements ComponentService {
public String generateComponentId() {
return UUID.randomUUID().toString();
}
@Override
public CompletableFuture<Message> addButtonToMessage(Long messageId, TextChannel textChannel, String buttonId, String description, String emoteMarkdown, ButtonStyle style) {
return channelService.retrieveMessageInChannel(textChannel, messageId).thenCompose(message -> {
Button button = Button.of(style, buttonId, description);
if(emoteMarkdown != null) {
button = button.withEmoji(Emoji.fromMarkdown(emoteMarkdown));
}
List<ActionRow> actionRows;
if(message.getActionRows().isEmpty()) {
actionRows = Arrays.asList(ActionRow.of(button));
} else {
ActionRow lastRow = message.getActionRows().get(message.getActionRows().size() - 1);
if(lastRow.getComponents().size() < MAX_BUTTONS_PER_ROW) {
lastRow.getComponents().add(button);
actionRows = message.getActionRows();
} else {
List<ActionRow> currentActionRows = new ArrayList<>(message.getActionRows());
currentActionRows.add(ActionRow.of(button));
actionRows = currentActionRows;
}
}
return messageService.editMessageWithActionRowsMessage(message, actionRows);
});
}
@Override
public CompletableFuture<Void> clearButtons(Message message) {
return messageService.editMessageWithActionRows(message, new ArrayList<>());
}
@Override
public CompletableFuture<Void> disableAllButtons(Message message) {
return setAllButtonStatesTo(message, true);
}
@Override
public CompletableFuture<Void> enableAllButtons(Message message) {
return setAllButtonStatesTo(message, false);
}
@Override
public CompletableFuture<Void> removeComponentWithId(Message message, String componentId) {
return removeComponentWithId(message, componentId, false);
}
@Override
public CompletableFuture<Void> removeComponentWithId(Message message, String componentId, Boolean rearrange) {
List<ActionRow> actionRows = new ArrayList<>();
if(Boolean.TRUE.equals(rearrange)) {
List<net.dv8tion.jda.api.interactions.components.Component> components = new ArrayList<>();
message.getActionRows().forEach(row ->
row
.getComponents()
.stream()
.filter(component -> component.getId() == null || !component.getId().equals(componentId))
.forEach(components::add));
actionRows = splitIntoActionRowsMax(components);
} else {
for (ActionRow row : message.getActionRows()) {
actionRows.add(ActionRow.of(
row
.getComponents()
.stream()
.filter(component -> component.getId() == null || !component.getId().equals(componentId))
.collect(Collectors.toList())));
}
}
return messageService.editMessageWithActionRows(message, actionRows);
}
@Override
public List<ActionRow> splitIntoActionRowsMax(List<net.dv8tion.jda.api.interactions.components.Component> allComponents) {
List<List<net.dv8tion.jda.api.interactions.components.Component>> actionRows = ListUtils.partition(allComponents, MAX_BUTTONS_PER_ROW);
return actionRows.stream().map(ActionRow::of).collect(Collectors.toList());
}
private CompletableFuture<Void> setAllButtonStatesTo(Message message, Boolean disabled) {
List<ActionRow> actionRows = new ArrayList<>();
message.getActionRows().forEach(row -> {
List<net.dv8tion.jda.api.interactions.components.Component> newComponents = new ArrayList<>();
row.getComponents().forEach(component -> {
if(component.getType().equals(net.dv8tion.jda.api.interactions.components.Component.Type.BUTTON)) {
Button button = ((Button) component).withDisabled(disabled);
newComponents.add(button);
} else {
newComponents.add(component);
}
});
actionRows.add(ActionRow.of(newComponents));
});
return messageService.editMessageWithActionRows(message, actionRows);
}
}

View File

@@ -7,8 +7,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Component
@@ -38,7 +38,7 @@ public class ConditionServiceBean implements ConditionService {
private void verifyConditionContext(ConditionContextInstance contextInstance, SystemCondition condition) {
for (ConditionContextVariable conditionContextVariable : condition.getExpectedContext().getRequiredVariables()) {
HashMap<String, Object> providedParameters = contextInstance.getParameters();
Map<String, Object> providedParameters = contextInstance.getParameters();
if(!providedParameters.containsKey(conditionContextVariable.getName())) {
throw new InvalidConditionParametersException(String.format("Variable %s was not present", conditionContextVariable.getName()));
}

View File

@@ -89,7 +89,7 @@ public class InteractionServiceBean implements InteractionService {
String id = component.getId();
MessageToSend.ComponentConfig payload = messageToSend.getComponentPayloads().get(id);
if(payload.getPersistCallback()) {
componentPayloadManagementService.createPayload(id, payload.getPayload(), payload.getPayloadType(), payload.getComponentOrigin(), server);
componentPayloadManagementService.createPayload(id, payload.getPayload(), payload.getPayloadType(), payload.getComponentOrigin(), server, payload.getComponentType());
}
}));
}

View File

@@ -12,6 +12,7 @@ import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.interactions.components.ActionRow;
import net.dv8tion.jda.api.requests.restaction.AuditableRestAction;
import net.dv8tion.jda.api.requests.restaction.MessageAction;
import org.jetbrains.annotations.NotNull;
@@ -235,8 +236,14 @@ public class MessageServiceBean implements MessageService {
}
@Override
public CompletableFuture<Void> clearButtons(Message message) {
return message.editMessage(message).setActionRows().submit().thenApply(message1 -> null);
public CompletableFuture<Void> editMessageWithActionRows(Message message, List<ActionRow> rows) {
return editMessageWithActionRowsMessage(message, rows).thenApply(message1 -> null);
}
@Override
public CompletableFuture<Message> editMessageWithActionRowsMessage(Message message, List<ActionRow> rows) {
metricService.incrementCounter(MESSAGE_EDIT_METRIC);
return message.editMessage(message).setActionRows(rows).submit();
}
@PostConstruct

View File

@@ -55,43 +55,48 @@ public class RoleServiceBean implements RoleService {
.build();
@Override
public void addRoleToUser(AUserInAServer aUserInAServer, ARole role) {
public void addRoleToMember(AUserInAServer aUserInAServer, ARole role) {
Optional<Guild> guildById = guildService.getGuildByIdOptional(aUserInAServer.getServerReference().getId());
if(guildById.isPresent()) {
addRoleToUser(guildById.get(), role, aUserInAServer.getUserReference().getId());
addRoleToMember(guildById.get(), role, aUserInAServer.getUserReference().getId());
} else {
throw new GuildNotFoundException(aUserInAServer.getServerReference().getId());
}
}
@Override
public CompletableFuture<Void> addRoleToUserFuture(AUserInAServer aUserInAServer, ARole role) {
public CompletableFuture<Void> addRoleToUserAsync(AUserInAServer aUserInAServer, ARole role) {
Optional<Guild> guildById = guildService.getGuildByIdOptional(aUserInAServer.getServerReference().getId());
if(guildById.isPresent()) {
return addRoleToUserFuture(guildById.get(), aUserInAServer.getUserReference().getId(), role);
return addRoleToUserAsync(guildById.get(), aUserInAServer.getUserReference().getId(), role);
} else {
throw new GuildNotFoundException(aUserInAServer.getServerReference().getId());
}
}
@Override
public CompletableFuture<Void> addRoleToMemberFuture(Member member, Long roleId) {
public CompletableFuture<Void> addRoleToMemberAsync(Member member, Long roleId) {
Role role = member.getGuild().getRoleById(roleId);
if(role == null) {
throw new RoleNotFoundInGuildException(roleId, member.getGuild().getIdLong());
}
return addRoleToUser(member.getGuild(), member.getIdLong(), role);
return addRoleToMemberAsync(member.getGuild(), member.getIdLong(), role);
}
@Override
public CompletableFuture<Void> addRoleToMemberAsync(Member member, Role role) {
return addRoleToMemberAsync(member.getGuild(), member.getIdLong(), role);
}
@Override
public void addRoleToMember(Member member, ARole role) {
addRoleToMemberFuture(member, role.getId());
addRoleToMemberAsync(member, role.getId());
}
@Override
public CompletableFuture<Void> addRoleToMemberFuture(Member member, ARole role) {
return addRoleToMemberFuture(member, role.getId());
public CompletableFuture<Void> addRoleToMemberAsync(Member member, ARole role) {
return addRoleToMemberAsync(member, role.getId());
}
@Override
@@ -114,7 +119,7 @@ public class RoleServiceBean implements RoleService {
return member.getGuild().removeRoleFromMember(member, role).submit();
}
private CompletableFuture<Void> addRoleToUserFuture(Guild guild, Long userId, ARole role) {
private CompletableFuture<Void> addRoleToUserAsync(Guild guild, Long userId, ARole role) {
if(role.getDeleted()) {
log.warn("Not possible to add role to user. Role {} was marked as deleted.", role.getId());
throw new RoleDeletedException(role);
@@ -122,27 +127,27 @@ public class RoleServiceBean implements RoleService {
Role roleById = guild.getRoleById(role.getId());
if(roleById != null) {
log.info("Adding role {} to user {} in server {}.", role.getId(), userId, guild.getId());
return addRoleToUser(guild, userId, roleById);
return addRoleToMemberAsync(guild, userId, roleById);
} else {
throw new RoleNotFoundInGuildException(role.getId(), guild.getIdLong());
}
}
@Override
public CompletableFuture<Void> addRoleToUser(Guild guild, Long userId, Role roleById) {
public CompletableFuture<Void> addRoleToMemberAsync(Guild guild, Long userId, Role roleById) {
metricService.incrementCounter(ROLE_ASSIGNED_METRIC);
return guild.addRoleToMember(userId, roleById).submit();
}
@Override
public CompletableFuture<Void> removeRoleFromUser(Guild guild, Long userId, Role roleById) {
public CompletableFuture<Void> removeRoleFromUserAsync(Guild guild, Long userId, Role roleById) {
metricService.incrementCounter(ROLE_REMOVED_METRIC);
return guild.removeRoleFromMember(userId, roleById).submit();
}
private void addRoleToUser(Guild guild, ARole role, Long userId) {
addRoleToUserFuture(guild, userId, role);
private void addRoleToMember(Guild guild, ARole role, Long userId) {
addRoleToUserAsync(guild, userId, role);
}
private CompletableFuture<Void> removeRoleFromUserFuture(Guild guild, ARole role, Long userId) {
@@ -153,7 +158,7 @@ public class RoleServiceBean implements RoleService {
Role roleById = guild.getRoleById(role.getId());
if(roleById != null) {
log.info("Removing role {} from user {} in server {}.", role.getId(), userId, guild.getId());
return removeRoleFromUser(guild, userId, roleById);
return removeRoleFromUserAsync(guild, userId, roleById);
} else {
throw new RoleNotFoundInGuildException(role.getId(), guild.getIdLong());
}
@@ -175,7 +180,7 @@ public class RoleServiceBean implements RoleService {
}
@Override
public CompletableFuture<Void> removeRoleFromUserFuture(AUserInAServer aUserInAServer, ARole role) {
public CompletableFuture<Void> removeRoleFromUserAsync(AUserInAServer aUserInAServer, ARole role) {
Optional<Guild> guildById = guildService.getGuildByIdOptional(aUserInAServer.getServerReference().getId());
if(guildById.isPresent()) {
return removeRoleFromUserFuture(guildById.get(), role, aUserInAServer.getUserReference().getId());
@@ -184,6 +189,11 @@ public class RoleServiceBean implements RoleService {
}
}
@Override
public CompletableFuture<Void> removeRoleFromUserAsync(Member member, Role role) {
return removeRoleFromUserAsync(member.getGuild(), member.getIdLong(), role);
}
@Override
public void markDeleted(Role role, AServer server) {
markDeleted(role.getIdLong(), server);
@@ -249,8 +259,13 @@ public class RoleServiceBean implements RoleService {
@Override
public boolean canBotInteractWithRole(ARole role) {
Role jdaRole = getRoleFromGuild(role);
Member selfMember = jdaRole.getGuild().getSelfMember();
return selfMember.canInteract(jdaRole);
return canBotInteractWithRole(jdaRole);
}
@Override
public boolean canBotInteractWithRole(Role role) {
Member selfMember = role.getGuild().getSelfMember();
return selfMember.canInteract(role);
}
@Override

View File

@@ -3,6 +3,7 @@ package dev.sheldan.abstracto.core.service.management;
import com.google.gson.Gson;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.ComponentPayload;
import dev.sheldan.abstracto.core.models.database.ComponentType;
import dev.sheldan.abstracto.core.models.template.button.ButtonConfigModel;
import dev.sheldan.abstracto.core.repository.ComponentPayloadRepository;
import lombok.extern.slf4j.Slf4j;
@@ -23,7 +24,7 @@ public class ComponentPayloadManagementServiceBean implements ComponentPayloadMa
private Gson gson;
@Override
public ComponentPayload createPayload(String id, String payload, Class payloadType, String buttonOrigin, AServer server) {
public ComponentPayload createPayload(String id, String payload, Class payloadType, String buttonOrigin, AServer server, ComponentType componentType) {
ComponentPayload componentPayload = ComponentPayload
.builder()
.origin(buttonOrigin)
@@ -31,6 +32,7 @@ public class ComponentPayloadManagementServiceBean implements ComponentPayloadMa
.payload(payload)
.payloadType(payloadType.getTypeName())
.server(server)
.type(componentType)
.build();
return repository.save(componentPayload);
}
@@ -38,7 +40,7 @@ public class ComponentPayloadManagementServiceBean implements ComponentPayloadMa
@Override
public ComponentPayload createPayload(ButtonConfigModel buttonConfigModel, AServer server) {
String payload = gson.toJson(buttonConfigModel.getButtonPayload());
return createPayload(buttonConfigModel.getButtonId(), payload, buttonConfigModel.getPayloadType(), buttonConfigModel.getOrigin(), server);
return createPayload(buttonConfigModel.getButtonId(), payload, buttonConfigModel.getPayloadType(), buttonConfigModel.getOrigin(), server, ComponentType.BUTTON);
}
@Override
@@ -55,4 +57,9 @@ public class ComponentPayloadManagementServiceBean implements ComponentPayloadMa
public void deletePayload(String id) {
repository.deleteById(id);
}
@Override
public void deletePayload(ComponentPayload payload) {
repository.delete(payload);
}
}

View File

@@ -4,6 +4,8 @@ import com.google.gson.Gson;
import dev.sheldan.abstracto.core.command.config.features.CoreFeatureConfig;
import dev.sheldan.abstracto.core.config.ServerContext;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.models.database.ComponentType;
import dev.sheldan.abstracto.core.service.ComponentServiceBean;
import dev.sheldan.abstracto.core.service.ConfigService;
import dev.sheldan.abstracto.core.templating.Templatable;
import dev.sheldan.abstracto.core.templating.exception.TemplatingException;
@@ -113,6 +115,7 @@ public class TemplateServiceBean implements TemplateService {
componentConfig = MessageToSend.ComponentConfig
.builder()
.componentOrigin(componentOrigin)
.componentType(ComponentType.BUTTON)
.persistCallback(metaConfig != null && Boolean.TRUE.equals(metaConfig.getPersistCallback()))
.payload(buttonConfig.getButtonPayload())
.payloadType(buttonConfig.getPayloadType() != null ? Class.forName(buttonConfig.getPayloadType()) : null)
@@ -139,7 +142,7 @@ public class TemplateServiceBean implements TemplateService {
metaConfig != null &&
Boolean.TRUE.equals(metaConfig.getForceNewRow())
)
|| currentRow.getComponents().size() == 5) {
|| currentRow.getComponents().size() == ComponentServiceBean.MAX_BUTTONS_PER_ROW) {
buttons.add(currentRow);
currentRow = ActionRow.of(createdButton);
} else {

View File

@@ -0,0 +1,10 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="tables/tables.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,14 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<changeSet author="Sheldan" id="component_payload-addType">
<addColumn tableName="component_payload">
<column name="type" type="VARCHAR(100)" />
</addColumn>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,10 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="component_payload.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -17,4 +17,5 @@
<include file="1.2.11-core/collection.xml" relativeToChangelogFile="true"/>
<include file="1.2.12/collection.xml" relativeToChangelogFile="true"/>
<include file="1.3.0/collection.xml" relativeToChangelogFile="true"/>
<include file="1.3.1/collection.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>