mirror of
https://github.com/Sheldan/Sissi.git
synced 2026-01-08 18:34:00 +00:00
[SIS-xxx] changing the setup for donation tracking
This commit is contained in:
@@ -34,6 +34,11 @@
|
|||||||
<artifactId>rssreader</artifactId>
|
<artifactId>rssreader</artifactId>
|
||||||
<version>${rssreader.version}</version>
|
<version>${rssreader.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jsoup</groupId>
|
||||||
|
<artifactId>jsoup</artifactId>
|
||||||
|
<version>${jsoup.version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,11 @@
|
|||||||
<groupId>com.google.code.gson</groupId>
|
<groupId>com.google.code.gson</groupId>
|
||||||
<artifactId>gson</artifactId>
|
<artifactId>gson</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.sheldan.abstracto.scheduling</groupId>
|
||||||
|
<artifactId>scheduling-int</artifactId>
|
||||||
|
<version>${abstracto.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
@@ -28,6 +33,10 @@
|
|||||||
<groupId>org.springframework</groupId>
|
<groupId>org.springframework</groupId>
|
||||||
<artifactId>spring-context-support</artifactId>
|
<artifactId>spring-context-support</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jsoup</groupId>
|
||||||
|
<artifactId>jsoup</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static dev.sheldan.sissi.module.debra.config.DebraFeatureConfig.DEBRA_DONATION_NOTIFICATION_SERVER_ID_ENV_NAME;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping(value = "/debra")
|
@RequestMapping(value = "/debra")
|
||||||
public class DebraDonationStatusController {
|
public class DebraDonationStatusController {
|
||||||
@@ -20,50 +18,38 @@ public class DebraDonationStatusController {
|
|||||||
|
|
||||||
@GetMapping(value = "/latestDonations", produces = "application/json")
|
@GetMapping(value = "/latestDonations", produces = "application/json")
|
||||||
public DonationStats getLatestDonations() {
|
public DonationStats getLatestDonations() {
|
||||||
Long serverId = Long.parseLong(System.getenv(DEBRA_DONATION_NOTIFICATION_SERVER_ID_ENV_NAME));
|
DonationsResponse donationResponse = donationService.getSynchronizedCachedDonationAmount();
|
||||||
DonationsResponse donationResponse = donationService.getSynchronizedCachedDonationAmount(serverId);
|
|
||||||
List<DonationInfo> donations = donationService.getLatestDonations(donationResponse, Integer.MAX_VALUE)
|
List<DonationInfo> donations = donationService.getLatestDonations(donationResponse, Integer.MAX_VALUE)
|
||||||
.stream()
|
.stream()
|
||||||
.map(DonationInfo::fromDonationItemModel)
|
.map(DonationInfo::fromDonationItemModel)
|
||||||
.toList();
|
.toList();
|
||||||
return DonationStats
|
return DonationStats
|
||||||
.builder()
|
.builder()
|
||||||
.totalAmount(donationResponse.getPage().getCollected())
|
.totalAmount(donationResponse.getCurrentDonationAmount())
|
||||||
.donations(donations)
|
.donations(donations)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(value = "/highestDonations", produces = "application/json")
|
@GetMapping(value = "/highestDonations", produces = "application/json")
|
||||||
public DonationStats getHighestDonations() {
|
public DonationStats getHighestDonations() {
|
||||||
Long serverId = Long.parseLong(System.getenv(DEBRA_DONATION_NOTIFICATION_SERVER_ID_ENV_NAME));
|
DonationsResponse donationResponse = donationService.getSynchronizedCachedDonationAmount();
|
||||||
DonationsResponse donationResponse = donationService.getSynchronizedCachedDonationAmount(serverId);
|
|
||||||
List<DonationInfo> donations = donationService.getHighestDonations(donationResponse, Integer.MAX_VALUE)
|
List<DonationInfo> donations = donationService.getHighestDonations(donationResponse, Integer.MAX_VALUE)
|
||||||
.stream()
|
.stream()
|
||||||
.map(DonationInfo::fromDonationItemModel)
|
.map(DonationInfo::fromDonationItemModel)
|
||||||
.toList();
|
.toList();
|
||||||
return DonationStats
|
return DonationStats
|
||||||
.builder()
|
.builder()
|
||||||
.totalAmount(donationResponse.getPage().getCollected())
|
.totalAmount(donationResponse.getCurrentDonationAmount())
|
||||||
.donations(donations)
|
.donations(donations)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(value = "/campaignInfo", produces = "application/json")
|
@GetMapping(value = "/campaignInfo", produces = "application/json")
|
||||||
public CampaignInfo getCampaignInfo() {
|
public CampaignInfo getCampaignInfo() {
|
||||||
Long serverId = Long.parseLong(System.getenv(DEBRA_DONATION_NOTIFICATION_SERVER_ID_ENV_NAME));
|
DonationsResponse donationResponse = donationService.getSynchronizedCachedDonationAmount();
|
||||||
DonationsResponse donationResponse = donationService.getSynchronizedCachedDonationAmount(serverId);
|
|
||||||
|
|
||||||
Description pageObject = donationResponse.getPage();
|
|
||||||
return CampaignInfo
|
return CampaignInfo
|
||||||
.builder()
|
.builder()
|
||||||
.collected(pageObject.getCollected())
|
|
||||||
.collectedNet(pageObject.getCollectedNet())
|
|
||||||
.donationCount(donationResponse.getDonationCount())
|
|
||||||
.currency(pageObject.getCurrency())
|
|
||||||
.percent(pageObject.getPercent())
|
|
||||||
.displayName(pageObject.getDisplayName())
|
|
||||||
.slug(pageObject.getSlug())
|
|
||||||
.target(pageObject.getTarget())
|
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ public class EndlessStreamController {
|
|||||||
public EndlessStreamInfo getLatestDonations(@PathVariable("id") Long id) {
|
public EndlessStreamInfo getLatestDonations(@PathVariable("id") Long id) {
|
||||||
Long serverId = Long.parseLong(System.getenv(DEBRA_DONATION_NOTIFICATION_SERVER_ID_ENV_NAME));
|
Long serverId = Long.parseLong(System.getenv(DEBRA_DONATION_NOTIFICATION_SERVER_ID_ENV_NAME));
|
||||||
EndlessStream endlessStream = endlessStreamManagementServiceBean.getEndlessStream(id);
|
EndlessStream endlessStream = endlessStreamManagementServiceBean.getEndlessStream(id);
|
||||||
DonationsResponse donationInfo = donationService.getSynchronizedCachedDonationAmount(serverId);
|
DonationsResponse donationInfo = donationService.getSynchronizedCachedDonationAmount();
|
||||||
BigDecimal collectedAmount = donationInfo.getPage().getCollected();
|
BigDecimal collectedAmount = donationInfo.getCurrentDonationAmount();
|
||||||
Long minuteRate = configService.getLongValueOrConfigDefault(DebraFeatureConfig.ENDLESS_STREAM_MINUTE_RATE, serverId);
|
Long minuteRate = configService.getLongValueOrConfigDefault(DebraFeatureConfig.ENDLESS_STREAM_MINUTE_RATE, serverId);
|
||||||
Instant endDate = endlessStream.getStartTime().plus(collectedAmount.multiply(new BigDecimal(minuteRate)).toBigInteger().longValue(), ChronoUnit.MINUTES);
|
Instant endDate = endlessStream.getStartTime().plus(collectedAmount.multiply(new BigDecimal(minuteRate)).toBigInteger().longValue(), ChronoUnit.MINUTES);
|
||||||
return EndlessStreamInfo
|
return EndlessStreamInfo
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import dev.sheldan.abstracto.core.command.config.CommandConfiguration;
|
|||||||
import dev.sheldan.abstracto.core.command.config.HelpInfo;
|
import dev.sheldan.abstracto.core.command.config.HelpInfo;
|
||||||
import dev.sheldan.abstracto.core.command.config.Parameter;
|
import dev.sheldan.abstracto.core.command.config.Parameter;
|
||||||
import dev.sheldan.abstracto.core.command.execution.CommandContext;
|
import dev.sheldan.abstracto.core.command.execution.CommandContext;
|
||||||
|
import dev.sheldan.abstracto.core.command.execution.CommandParameterKey;
|
||||||
import dev.sheldan.abstracto.core.command.execution.CommandResult;
|
import dev.sheldan.abstracto.core.command.execution.CommandResult;
|
||||||
import dev.sheldan.abstracto.core.config.FeatureDefinition;
|
import dev.sheldan.abstracto.core.config.FeatureDefinition;
|
||||||
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
|
|
||||||
import dev.sheldan.abstracto.core.interaction.InteractionService;
|
import dev.sheldan.abstracto.core.interaction.InteractionService;
|
||||||
import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig;
|
import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig;
|
||||||
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService;
|
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService;
|
||||||
@@ -26,7 +26,6 @@ import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEve
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -70,11 +69,12 @@ public class Donations extends AbstractConditionableCommand {
|
|||||||
Integer selectionValue = (Integer) parameters.get(1);
|
Integer selectionValue = (Integer) parameters.get(1);
|
||||||
Integer top = null;
|
Integer top = null;
|
||||||
Integer latest = null;
|
Integer latest = null;
|
||||||
switch (type) {
|
if(type != null) {
|
||||||
case "top": top = selectionValue; break;
|
DonationsTypeParameterKey typeKey = CommandParameterKey.getEnumFromKey(DonationsTypeParameterKey.class, type);
|
||||||
default:
|
switch (typeKey) {
|
||||||
case "latest" :
|
case LATEST -> latest = selectionValue;
|
||||||
latest = selectionValue; break;
|
case TOP -> top = selectionValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
messageToSend = getDonationMessageToSend(commandContext.getGuild().getIdLong(), top, latest);
|
messageToSend = getDonationMessageToSend(commandContext.getGuild().getIdLong(), top, latest);
|
||||||
}
|
}
|
||||||
@@ -98,11 +98,10 @@ public class Donations extends AbstractConditionableCommand {
|
|||||||
Integer top = null;
|
Integer top = null;
|
||||||
Integer latest = null;
|
Integer latest = null;
|
||||||
if(selectionType != null) {
|
if(selectionType != null) {
|
||||||
switch (selectionType) {
|
DonationsTypeParameterKey typeKey = CommandParameterKey.getEnumFromKey(DonationsTypeParameterKey.class, selectionType);
|
||||||
case "top": top = selectionValue; break;
|
switch (typeKey) {
|
||||||
default:
|
case LATEST -> latest = selectionValue;
|
||||||
case "latest" :
|
case TOP -> top = selectionValue;
|
||||||
latest = selectionValue; break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +112,7 @@ public class Donations extends AbstractConditionableCommand {
|
|||||||
|
|
||||||
private MessageToSend getDonationMessageToSend(Long serverId, Integer top, Integer latest) {
|
private MessageToSend getDonationMessageToSend(Long serverId, Integer top, Integer latest) {
|
||||||
DonationsModel donationModel;
|
DonationsModel donationModel;
|
||||||
DonationsResponse donationResponse = donationService.fetchCurrentDonationAmount(serverId);
|
DonationsResponse donationResponse = donationService.fetchCurrentDonations();
|
||||||
donationModel = donationConverter.convertDonationResponse(donationResponse);
|
donationModel = donationConverter.convertDonationResponse(donationResponse);
|
||||||
if(top != null) {
|
if(top != null) {
|
||||||
donationModel.setDonations(donationService.getHighestDonations(donationResponse, top));
|
donationModel.setDonations(donationService.getHighestDonations(donationResponse, top));
|
||||||
@@ -146,7 +145,7 @@ public class Donations extends AbstractConditionableCommand {
|
|||||||
.templated(true)
|
.templated(true)
|
||||||
.name(SELECTION_PARAMETER)
|
.name(SELECTION_PARAMETER)
|
||||||
.optional(true)
|
.optional(true)
|
||||||
.type(String.class)
|
.type(DonationsTypeParameterKey.class)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package dev.sheldan.sissi.module.debra.commands;
|
||||||
|
|
||||||
|
import dev.sheldan.abstracto.core.command.execution.CommandParameterKey;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum DonationsTypeParameterKey implements CommandParameterKey {
|
||||||
|
TOP("top"), LATEST("latest");
|
||||||
|
|
||||||
|
private String key;
|
||||||
|
}
|
||||||
@@ -13,7 +13,6 @@ public class DebraFeatureConfig implements FeatureConfig {
|
|||||||
|
|
||||||
public static final String DEBRA_DONATION_NOTIFICATION_DELAY_CONFIG_KEY = "debraDonationNotificationDelayMillis";
|
public static final String DEBRA_DONATION_NOTIFICATION_DELAY_CONFIG_KEY = "debraDonationNotificationDelayMillis";
|
||||||
public static final String ENDLESS_STREAM_MINUTE_RATE = "endlessStreamMinuteRate";
|
public static final String ENDLESS_STREAM_MINUTE_RATE = "endlessStreamMinuteRate";
|
||||||
public static final String DEBRA_DONATION_API_FETCH_SIZE_KEY = "debraDonationApiFetchSize";
|
|
||||||
public static final String DEBRA_DONATION_NOTIFICATION_SERVER_ID_ENV_NAME = "DEBRA_DONATION_NOTIFICATION_SERVER_ID";
|
public static final String DEBRA_DONATION_NOTIFICATION_SERVER_ID_ENV_NAME = "DEBRA_DONATION_NOTIFICATION_SERVER_ID";
|
||||||
@Override
|
@Override
|
||||||
public FeatureDefinition getFeature() {
|
public FeatureDefinition getFeature() {
|
||||||
@@ -27,6 +26,6 @@ public class DebraFeatureConfig implements FeatureConfig {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> getRequiredSystemConfigKeys() {
|
public List<String> getRequiredSystemConfigKeys() {
|
||||||
return Arrays.asList(DEBRA_DONATION_NOTIFICATION_DELAY_CONFIG_KEY, DEBRA_DONATION_API_FETCH_SIZE_KEY, ENDLESS_STREAM_MINUTE_RATE);
|
return Arrays.asList(DEBRA_DONATION_NOTIFICATION_DELAY_CONFIG_KEY, ENDLESS_STREAM_MINUTE_RATE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,5 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
@Setter
|
@Setter
|
||||||
@ConfigurationProperties(prefix = "sissi.debra")
|
@ConfigurationProperties(prefix = "sissi.debra")
|
||||||
public class DebraProperties {
|
public class DebraProperties {
|
||||||
private String websocketURL;
|
private String donationPageUrl;
|
||||||
private String donationAPIUrl;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package dev.sheldan.sissi.module.debra.converter;
|
package dev.sheldan.sissi.module.debra.converter;
|
||||||
|
|
||||||
import dev.sheldan.sissi.module.debra.model.api.Donation;
|
import dev.sheldan.sissi.module.debra.model.api.DonationDto;
|
||||||
import dev.sheldan.sissi.module.debra.model.api.DonationsResponse;
|
import dev.sheldan.sissi.module.debra.model.api.DonationsResponse;
|
||||||
import dev.sheldan.sissi.module.debra.model.commands.DonationItemModel;
|
import dev.sheldan.sissi.module.debra.model.commands.DonationItemModel;
|
||||||
import dev.sheldan.sissi.module.debra.model.commands.DonationsModel;
|
import dev.sheldan.sissi.module.debra.model.commands.DonationsModel;
|
||||||
@@ -9,20 +9,19 @@ import org.springframework.stereotype.Component;
|
|||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class DonationConverter {
|
public class DonationConverter {
|
||||||
public DonationItemModel convertDonation(Donation donation) {
|
public DonationItemModel convertDonation(DonationDto donation) {
|
||||||
return DonationItemModel
|
return DonationItemModel
|
||||||
.builder()
|
.builder()
|
||||||
.donationAmount(donation.getAmount())
|
.donationAmount(donation.getAmount())
|
||||||
.firstName(donation.getFirstname())
|
.name(donation.getName())
|
||||||
.lastName(donation.getLastname())
|
.anonymous(BooleanUtils.toBoolean(donation.getAnonymous()))
|
||||||
.anonymous(BooleanUtils.toBoolean(donation.getAnonym()))
|
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public DonationsModel convertDonationResponse(DonationsResponse response) {
|
public DonationsModel convertDonationResponse(DonationsResponse response) {
|
||||||
return DonationsModel
|
return DonationsModel
|
||||||
.builder()
|
.builder()
|
||||||
.totalAmount(response.getPage().getCollected())
|
.totalAmount(response.getCurrentDonationAmount())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package dev.sheldan.sissi.module.debra.job;
|
||||||
|
|
||||||
|
import dev.sheldan.sissi.module.debra.service.DonationService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.quartz.DisallowConcurrentExecution;
|
||||||
|
import org.quartz.JobExecutionContext;
|
||||||
|
import org.quartz.JobExecutionException;
|
||||||
|
import org.quartz.PersistJobDataAfterExecution;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.scheduling.quartz.QuartzJobBean;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@DisallowConcurrentExecution
|
||||||
|
@Component
|
||||||
|
@PersistJobDataAfterExecution
|
||||||
|
public class DonationFetchJob extends QuartzJobBean {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DonationService donationService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
|
||||||
|
try {
|
||||||
|
log.info("Checking for new donations.");
|
||||||
|
donationService.checkForNewDonations();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to check for new donations.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package dev.sheldan.sissi.module.debra.listener;
|
|
||||||
|
|
||||||
import dev.sheldan.abstracto.core.listener.AsyncStartupListener;
|
|
||||||
import dev.sheldan.abstracto.core.service.ConfigService;
|
|
||||||
import dev.sheldan.sissi.module.debra.config.DebraProperties;
|
|
||||||
import dev.sheldan.sissi.module.debra.model.listener.DonationResponseModel;
|
|
||||||
import dev.sheldan.sissi.module.debra.service.DonationService;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import okhttp3.*;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import static dev.sheldan.sissi.module.debra.config.DebraFeatureConfig.DEBRA_DONATION_NOTIFICATION_DELAY_CONFIG_KEY;
|
|
||||||
import static dev.sheldan.sissi.module.debra.config.DebraFeatureConfig.DEBRA_DONATION_NOTIFICATION_SERVER_ID_ENV_NAME;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
|
||||||
public class WebsocketListener extends WebSocketListener implements AsyncStartupListener {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private DonationService donationService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private DebraProperties debraProperties;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private ConfigService configService;
|
|
||||||
|
|
||||||
private WebSocket webSocketObj;
|
|
||||||
private OkHttpClient clientObj;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOpen(WebSocket webSocket, Response response) {
|
|
||||||
log.info("Connected to donation websocket.");
|
|
||||||
super.onOpen(webSocket, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMessage(WebSocket webSocket, String text) {
|
|
||||||
CompletableFuture.runAsync(() -> {
|
|
||||||
log.info("Handling received message on websocket.");
|
|
||||||
try {
|
|
||||||
Long targetServerId = Long.parseLong(System.getenv(DEBRA_DONATION_NOTIFICATION_SERVER_ID_ENV_NAME));
|
|
||||||
Long delayMillis = configService.getLongValueOrConfigDefault(DEBRA_DONATION_NOTIFICATION_DELAY_CONFIG_KEY, targetServerId);
|
|
||||||
log.info("Waiting {} milli seconds to send notification.", delayMillis);
|
|
||||||
Thread.sleep(delayMillis);
|
|
||||||
log.info("Loading new donation amount and sending notification.");
|
|
||||||
DonationResponseModel donation = donationService.parseDonationFromMessage(text);
|
|
||||||
donationService.sendDonationNotification(donation).thenAccept(unused -> {
|
|
||||||
log.info("Successfully notified about donation.");
|
|
||||||
}).exceptionally(throwable -> {
|
|
||||||
log.error("Failed to notify about donation.", throwable);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
} catch (Exception exception) {
|
|
||||||
log.error("Failed to handle websocket message.", exception);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(WebSocket webSocket, Throwable t, @Nullable Response response) {
|
|
||||||
log.warn("Websocket connection failed...", t);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClosing(WebSocket webSocket, int code, String reason) {
|
|
||||||
log.info("Closing websocket connection. It was closed with code {} and reason {}.", code, reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute() {
|
|
||||||
if(clientObj != null) {
|
|
||||||
clientObj.connectionPool().evictAll();
|
|
||||||
clientObj.dispatcher().executorService().shutdownNow();
|
|
||||||
}
|
|
||||||
clientObj = new OkHttpClient.Builder()
|
|
||||||
.readTimeout(0, TimeUnit.MILLISECONDS)
|
|
||||||
.retryOnConnectionFailure(true)
|
|
||||||
.build();
|
|
||||||
startConnection(clientObj);
|
|
||||||
clientObj.dispatcher().executorService().shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startConnection(OkHttpClient client) {
|
|
||||||
log.info("Starting websocket connection.");
|
|
||||||
Request request = new Request.Builder()
|
|
||||||
.url(debraProperties.getWebsocketURL())
|
|
||||||
.build();
|
|
||||||
this.webSocketObj = client.newWebSocket(request, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package dev.sheldan.sissi.module.debra.model.api;
|
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@Builder
|
|
||||||
public class Description {
|
|
||||||
@SerializedName("collected")
|
|
||||||
private BigDecimal collected;
|
|
||||||
@SerializedName("target")
|
|
||||||
private BigDecimal target;
|
|
||||||
@SerializedName("currency")
|
|
||||||
private String currency;
|
|
||||||
@SerializedName("slug")
|
|
||||||
private String slug;
|
|
||||||
@SerializedName("displayname")
|
|
||||||
private String displayName;
|
|
||||||
@SerializedName("collectednet")
|
|
||||||
private BigDecimal collectedNet;
|
|
||||||
@SerializedName("percent")
|
|
||||||
private BigDecimal percent;
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package dev.sheldan.sissi.module.debra.model.api;
|
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@Builder
|
|
||||||
public class Donation {
|
|
||||||
@SerializedName("amount")
|
|
||||||
private BigDecimal amount;
|
|
||||||
@SerializedName("currency")
|
|
||||||
private String currency;
|
|
||||||
@SerializedName("text")
|
|
||||||
private String text;
|
|
||||||
@SerializedName("anonym")
|
|
||||||
private Integer anonym;
|
|
||||||
@SerializedName("firstname")
|
|
||||||
private String firstname;
|
|
||||||
@SerializedName("lastname")
|
|
||||||
private String lastname;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package dev.sheldan.sissi.module.debra.model.api;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Builder
|
||||||
|
@ToString
|
||||||
|
public class DonationDto {
|
||||||
|
private BigDecimal amount;
|
||||||
|
private String currency;
|
||||||
|
private String text;
|
||||||
|
private Boolean anonymous;
|
||||||
|
private String name;
|
||||||
|
private LocalDate date;
|
||||||
|
|
||||||
|
public String stringRepresentation() {
|
||||||
|
return String.format("%s %s %s %s %s", name, amount, text, anonymous, date.format(DateTimeFormatter.ISO_DATE));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import java.math.BigDecimal;
|
|||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
public class DonationInfo {
|
public class DonationInfo {
|
||||||
private String firstName;
|
private String name;
|
||||||
private BigDecimal donationAmount;
|
private BigDecimal donationAmount;
|
||||||
private Boolean anonymous;
|
private Boolean anonymous;
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ public class DonationInfo {
|
|||||||
.builder()
|
.builder()
|
||||||
.donationAmount(donationItemModel.getDonationAmount())
|
.donationAmount(donationItemModel.getDonationAmount())
|
||||||
.anonymous(donationItemModel.getAnonymous())
|
.anonymous(donationItemModel.getAnonymous())
|
||||||
.firstName(donationItemModel.getFirstName())
|
.name(donationItemModel.getName())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
package dev.sheldan.sissi.module.debra.model.api;
|
package dev.sheldan.sissi.module.debra.model.api;
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName;
|
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
public class DonationsResponse {
|
public class DonationsResponse {
|
||||||
@SerializedName("page")
|
private BigDecimal currentDonationAmount;
|
||||||
private Description page;
|
private BigDecimal donationAmountGoal;
|
||||||
@SerializedName("donation_count")
|
private int donationCount;
|
||||||
private BigInteger donationCount;
|
private List<DonationDto> donations;
|
||||||
@SerializedName("donations")
|
|
||||||
private List<Donation> donations;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import lombok.Getter;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
public class DonationItemModel {
|
public class DonationItemModel {
|
||||||
private String firstName;
|
private String name;
|
||||||
private String lastName;
|
private LocalDate date;
|
||||||
private BigDecimal donationAmount;
|
private BigDecimal donationAmount;
|
||||||
private Boolean anonymous;
|
private Boolean anonymous;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package dev.sheldan.sissi.module.debra.model.database;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
@Entity
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Table(name = "donation")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@EqualsAndHashCode
|
||||||
|
public class Donation {
|
||||||
|
@Id
|
||||||
|
@Column(name = "id", nullable = false)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
// we cant be sure about duplicates
|
||||||
|
@Column(name = "count")
|
||||||
|
private Integer count;
|
||||||
|
|
||||||
|
@Column(name = "created")
|
||||||
|
private Instant created;
|
||||||
|
|
||||||
|
@Column(name = "updated")
|
||||||
|
private Instant updated;
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import java.math.BigDecimal;
|
|||||||
@ToString
|
@ToString
|
||||||
public class DonationResponseModel {
|
public class DonationResponseModel {
|
||||||
private String donatorName;
|
private String donatorName;
|
||||||
|
private Boolean anonymous;
|
||||||
private BigDecimal amount;
|
private BigDecimal amount;
|
||||||
private String message;
|
private String message;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package dev.sheldan.sissi.module.debra.repository;
|
||||||
|
|
||||||
|
import dev.sheldan.sissi.module.debra.model.database.Donation;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface DonationRepository extends JpaRepository<Donation, String> {
|
||||||
|
}
|
||||||
@@ -1,52 +1,52 @@
|
|||||||
package dev.sheldan.sissi.module.debra.service;
|
package dev.sheldan.sissi.module.debra.service;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.GsonBuilder;
|
|
||||||
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
|
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
|
||||||
import dev.sheldan.abstracto.core.interaction.ComponentPayloadService;
|
import dev.sheldan.abstracto.core.interaction.ComponentPayloadService;
|
||||||
import dev.sheldan.abstracto.core.interaction.ComponentService;
|
import dev.sheldan.abstracto.core.interaction.ComponentService;
|
||||||
import dev.sheldan.abstracto.core.models.database.AServer;
|
import dev.sheldan.abstracto.core.models.database.AServer;
|
||||||
import dev.sheldan.abstracto.core.service.ChannelService;
|
import dev.sheldan.abstracto.core.service.ChannelService;
|
||||||
import dev.sheldan.abstracto.core.service.ConfigService;
|
import dev.sheldan.abstracto.core.service.HashService;
|
||||||
import dev.sheldan.abstracto.core.service.PostTargetService;
|
import dev.sheldan.abstracto.core.service.PostTargetService;
|
||||||
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
|
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
|
||||||
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
|
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
|
||||||
import dev.sheldan.abstracto.core.templating.service.TemplateService;
|
import dev.sheldan.abstracto.core.templating.service.TemplateService;
|
||||||
|
import dev.sheldan.abstracto.core.utils.CompletableFutureList;
|
||||||
import dev.sheldan.abstracto.core.utils.FutureUtils;
|
import dev.sheldan.abstracto.core.utils.FutureUtils;
|
||||||
import dev.sheldan.sissi.module.debra.exception.DonationAmountNotFoundException;
|
|
||||||
import dev.sheldan.sissi.module.debra.config.DebraPostTarget;
|
import dev.sheldan.sissi.module.debra.config.DebraPostTarget;
|
||||||
import dev.sheldan.sissi.module.debra.config.DebraProperties;
|
import dev.sheldan.sissi.module.debra.config.DebraProperties;
|
||||||
import dev.sheldan.sissi.module.debra.converter.DonationConverter;
|
import dev.sheldan.sissi.module.debra.converter.DonationConverter;
|
||||||
import dev.sheldan.sissi.module.debra.model.api.Donation;
|
import dev.sheldan.sissi.module.debra.model.api.DonationDto;
|
||||||
import dev.sheldan.sissi.module.debra.model.api.DonationsResponse;
|
import dev.sheldan.sissi.module.debra.model.api.DonationsResponse;
|
||||||
import dev.sheldan.sissi.module.debra.model.commands.DebraInfoButtonPayload;
|
import dev.sheldan.sissi.module.debra.model.commands.DebraInfoButtonPayload;
|
||||||
import dev.sheldan.sissi.module.debra.model.commands.DebraInfoModel;
|
import dev.sheldan.sissi.module.debra.model.commands.DebraInfoModel;
|
||||||
import dev.sheldan.sissi.module.debra.model.commands.DonationItemModel;
|
import dev.sheldan.sissi.module.debra.model.commands.DonationItemModel;
|
||||||
import dev.sheldan.sissi.module.debra.model.commands.DonationsModel;
|
import dev.sheldan.sissi.module.debra.model.commands.DonationsModel;
|
||||||
|
import dev.sheldan.sissi.module.debra.model.database.Donation;
|
||||||
import dev.sheldan.sissi.module.debra.model.listener.DonationResponseModel;
|
import dev.sheldan.sissi.module.debra.model.listener.DonationResponseModel;
|
||||||
import dev.sheldan.sissi.module.debra.model.listener.DonationNotificationModel;
|
import dev.sheldan.sissi.module.debra.model.listener.DonationNotificationModel;
|
||||||
|
import dev.sheldan.sissi.module.debra.service.management.DonationManagementServiceBean;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import net.dv8tion.jda.api.entities.Message;
|
import net.dv8tion.jda.api.entities.Message;
|
||||||
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
|
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
|
||||||
import okhttp3.OkHttpClient;
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
import okhttp3.Request;
|
import org.jsoup.Jsoup;
|
||||||
import okhttp3.Response;
|
import org.jsoup.nodes.Document;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.jsoup.select.Elements;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.cache.annotation.Cacheable;
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Comparator;
|
import java.text.DecimalFormat;
|
||||||
import java.util.List;
|
import java.text.DecimalFormatSymbols;
|
||||||
import java.util.Optional;
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.*;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static dev.sheldan.sissi.module.debra.config.DebraFeatureConfig.DEBRA_DONATION_API_FETCH_SIZE_KEY;
|
|
||||||
import static dev.sheldan.sissi.module.debra.config.DebraFeatureConfig.DEBRA_DONATION_NOTIFICATION_SERVER_ID_ENV_NAME;
|
import static dev.sheldan.sissi.module.debra.config.DebraFeatureConfig.DEBRA_DONATION_NOTIFICATION_SERVER_ID_ENV_NAME;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -62,15 +62,9 @@ public class DonationService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private TemplateService templateService;
|
private TemplateService templateService;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private OkHttpClient okHttpClient;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private DonationConverter donationConverter;
|
private DonationConverter donationConverter;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private ConfigService configService;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ChannelService channelService;
|
private ChannelService channelService;
|
||||||
|
|
||||||
@@ -83,100 +77,209 @@ public class DonationService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ServerManagementService serverManagementService;
|
private ServerManagementService serverManagementService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DonationManagementServiceBean donationManagementServiceBean;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private HashService hashService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private DonationService self;
|
private DonationService self;
|
||||||
|
|
||||||
private static final String DEBRA_DONATION_NOTIFICATION_TEMPLATE_KEY = "debra_donation_notification";
|
private static final String DEBRA_DONATION_NOTIFICATION_TEMPLATE_KEY = "debra_donation_notification";
|
||||||
|
|
||||||
private static final Pattern MESSAGE_PATTERN = Pattern.compile("(.*) hat (\\d{1,9},\\d{2}) Euro gespendet!<br \\/>Vielen Dank!<br \\/>Nachricht:<br \\/>(.*)");
|
|
||||||
|
|
||||||
private static final String DEBRA_INFO_BUTTON_MESSAGE_TEMPLATE_KEY = "debraInfoButton";
|
private static final String DEBRA_INFO_BUTTON_MESSAGE_TEMPLATE_KEY = "debraInfoButton";
|
||||||
public static final String DEBRA_INFO_BUTTON_ORIGIN = "DEBRA_INFO_BUTTON";
|
public static final String DEBRA_INFO_BUTTON_ORIGIN = "DEBRA_INFO_BUTTON";
|
||||||
|
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("d.M.y");
|
||||||
public DonationResponseModel parseDonationFromMessage(String message) {
|
|
||||||
Matcher matcher = MESSAGE_PATTERN.matcher(message);
|
|
||||||
if (matcher.find()) {
|
|
||||||
String donatorName = matcher.group(1);
|
|
||||||
String amountString = matcher.group(2);
|
|
||||||
BigDecimal amount = new BigDecimal(amountString.replace(',', '.'));
|
|
||||||
String donationMessage = Optional.ofNullable(matcher.group(3)).map(msg -> msg.replaceAll("(<br>)+", " ")).map(String::trim).orElse("");
|
|
||||||
return DonationResponseModel
|
|
||||||
.builder()
|
|
||||||
.message(donationMessage)
|
|
||||||
.donatorName(donatorName)
|
|
||||||
.amount(amount)
|
|
||||||
.build();
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("String in wrong format");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<DonationItemModel> getHighestDonations(DonationsResponse response, Integer maxCount) {
|
public List<DonationItemModel> getHighestDonations(DonationsResponse response, Integer maxCount) {
|
||||||
List<Donation> topDonations = response
|
return response
|
||||||
.getDonations()
|
.getDonations()
|
||||||
.stream()
|
.stream()
|
||||||
.sorted(Comparator.comparing(Donation::getAmount)
|
.sorted(Comparator.comparing(DonationDto::getAmount)
|
||||||
.reversed())
|
.reversed())
|
||||||
.collect(Collectors.toList());
|
|
||||||
return topDonations
|
|
||||||
.stream()
|
|
||||||
.limit(maxCount)
|
.limit(maxCount)
|
||||||
.map(donation -> donationConverter.convertDonation(donation))
|
.map(donation -> donationConverter.convertDonation(donation))
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<DonationItemModel> getLatestDonations(DonationsResponse response, Integer maxCount) {
|
public List<DonationItemModel> getLatestDonations(DonationsResponse response, Integer maxCount) {
|
||||||
return response
|
return response
|
||||||
.getDonations()
|
.getDonations()
|
||||||
.stream()
|
.stream()
|
||||||
|
.sorted(Comparator.comparing(DonationDto::getDate).reversed())
|
||||||
.limit(maxCount)
|
.limit(maxCount)
|
||||||
.map(donation -> donationConverter.convertDonation(donation))
|
.map(donation -> donationConverter.convertDonation(donation))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized DonationsResponse getSynchronizedCachedDonationAmount(Long serverId) {
|
public synchronized DonationsResponse getSynchronizedCachedDonationAmount() {
|
||||||
return self.getCachedDonationAmount(serverId);
|
return self.getCachedDonationAmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Cacheable(value = "donation-cache")
|
@Cacheable(value = "donation-cache")
|
||||||
public synchronized DonationsResponse getCachedDonationAmount(Long serverId) {
|
public synchronized DonationsResponse getCachedDonationAmount() {
|
||||||
return self.fetchCurrentDonationAmount(serverId);
|
return self.fetchCurrentDonations();
|
||||||
}
|
}
|
||||||
|
|
||||||
public DonationsResponse fetchCurrentDonationAmount(Long serverId) {
|
public DonationsResponse fetchCurrentDonations() {
|
||||||
try {
|
try {
|
||||||
Long fetchSize = configService.getLongValueOrConfigDefault(DEBRA_DONATION_API_FETCH_SIZE_KEY, serverId);
|
Document donationPage = Jsoup.connect(debraProperties.getDonationPageUrl()).get();
|
||||||
Request request = new Request.Builder()
|
DecimalFormat decimalFormat = getDecimalFormat();
|
||||||
.url(String.format(debraProperties.getDonationAPIUrl(), fetchSize))
|
Element endValueElement = donationPage.getElementById("end-value");
|
||||||
.get()
|
String endValueString = endValueElement.text();
|
||||||
.build();
|
Elements currentValueElement = donationPage.getElementsByClass("current_amount").get(0).getElementsByClass("value");
|
||||||
Response response = okHttpClient.newCall(request).execute();
|
String[] valueArray = currentValueElement.text().split(" ");
|
||||||
if(!response.isSuccessful()) {
|
String currentValueString = valueArray[0];
|
||||||
log.error("Failed to retrieve donation response. Response had code {} with body {} and headers {}.",
|
String currency = valueArray[1];
|
||||||
response.code(), response.body().string(), response.headers());
|
BigDecimal currentValue = (BigDecimal) decimalFormat.parse(currentValueString);
|
||||||
throw new DonationAmountNotFoundException();
|
BigDecimal endValue = (BigDecimal) decimalFormat.parse(endValueString);
|
||||||
|
Element list = donationPage.getElementsByClass("donor-list").first();
|
||||||
|
Elements donationElements = list.getElementsByClass("list-item");
|
||||||
|
List<DonationDto> donations = new ArrayList<>();
|
||||||
|
for (Element donationMainElement : donationElements.asList()) {
|
||||||
|
Elements nameElement = donationMainElement.getElementsByClass("donor-list-name");
|
||||||
|
Elements dateElement = donationMainElement.getElementsByClass("donor-list-date");
|
||||||
|
Elements amountElement = donationMainElement.getElementsByClass("donor-list-amount");
|
||||||
|
Elements textElement = donationMainElement.getElementsByClass("donor-list-amount-text");
|
||||||
|
LocalDate dateValue;
|
||||||
|
if (dateElement.hasText()) {
|
||||||
|
dateValue = LocalDate.parse(dateElement.text(), DATE_FORMAT);
|
||||||
|
} else {
|
||||||
|
dateValue = null;
|
||||||
}
|
}
|
||||||
Gson gson = getGson();
|
BigDecimal amount;
|
||||||
return gson.fromJson(response.body().string(), DonationsResponse.class);
|
if (amountElement.hasText()) {
|
||||||
|
String amountText = amountElement.text().split(" ")[0];
|
||||||
|
amount = (BigDecimal) decimalFormat.parse(amountText);
|
||||||
|
} else {
|
||||||
|
amount = null;
|
||||||
|
}
|
||||||
|
String additionalText = textElement.text();
|
||||||
|
String name = nameElement.text();
|
||||||
|
boolean anonymous = name.isBlank();
|
||||||
|
donations.add(DonationDto
|
||||||
|
.builder()
|
||||||
|
.anonymous(anonymous)
|
||||||
|
.name(nameElement.text())
|
||||||
|
.amount(amount)
|
||||||
|
.currency(currency)
|
||||||
|
.name(name)
|
||||||
|
.text(additionalText)
|
||||||
|
.date(dateValue)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
return DonationsResponse
|
||||||
|
.builder()
|
||||||
|
.donations(donations)
|
||||||
|
.currentDonationAmount(currentValue)
|
||||||
|
.donationAmountGoal(endValue)
|
||||||
|
.donationCount(donations.size())
|
||||||
|
.build();
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
throw new AbstractoRunTimeException(exception);
|
throw new AbstractoRunTimeException(exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Gson getGson() {
|
private DecimalFormat getDecimalFormat() {
|
||||||
return new GsonBuilder()
|
DecimalFormatSymbols symbols = new DecimalFormatSymbols();
|
||||||
.registerTypeAdapter(BigDecimal.class, new BigDecimalGsonAdapter())
|
symbols.setGroupingSeparator('.');
|
||||||
.create();
|
symbols.setDecimalSeparator(',');
|
||||||
|
String pattern = "#,##0.0#";
|
||||||
|
|
||||||
|
DecimalFormat decimalFormat = new DecimalFormat(pattern, symbols);
|
||||||
|
decimalFormat.setParseBigDecimal(true);
|
||||||
|
return decimalFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
private DonationsModel getDonationInfoModel(Long serverId) {
|
private DonationsModel getDonationInfoModel() {
|
||||||
return donationConverter.convertDonationResponse(fetchCurrentDonationAmount(serverId));
|
return donationConverter.convertDonationResponse(fetchCurrentDonations());
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<Void> sendDonationNotification(DonationResponseModel donation) throws IOException {
|
|
||||||
|
private String hashDonation(DonationDto donation) {
|
||||||
|
return hashService.sha256HashString(donation.stringRepresentation());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void checkForNewDonations() {
|
||||||
|
List<Donation> allDonations = donationManagementServiceBean.getAllDonations();
|
||||||
|
Map<String, Integer> existingHashes = allDonations
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toMap(Donation::getId, Donation::getCount));
|
||||||
|
DonationsResponse donationResponse = fetchCurrentDonations();
|
||||||
|
Map<String, Pair<Integer, DonationDto>> donationFromPageHashes = new HashMap<>();
|
||||||
|
donationResponse.getDonations().forEach(donationDto -> {
|
||||||
|
String thisHash = hashDonation(donationDto);
|
||||||
|
if(donationFromPageHashes.containsKey(thisHash)) {
|
||||||
|
donationFromPageHashes.put(thisHash, Pair.of(donationFromPageHashes.get(thisHash).getLeft() + 1, donationDto));
|
||||||
|
} else {
|
||||||
|
donationFromPageHashes.put(thisHash, Pair.of(1, donationDto));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Set<String> pageHashesToRemove = new HashSet<>();
|
||||||
|
donationFromPageHashes.entrySet().forEach(pageHash -> {
|
||||||
|
if(existingHashes.containsKey(pageHash.getKey())) {
|
||||||
|
Integer existingDonation = existingHashes.get(pageHash.getKey());
|
||||||
|
int amountDifference = pageHash.getValue().getKey() - existingDonation;
|
||||||
|
if(amountDifference == 0) {
|
||||||
|
pageHashesToRemove.add(pageHash.getKey()); // it matches 1:1, we know about all of them already
|
||||||
|
} if(amountDifference < 0) {
|
||||||
|
pageHashesToRemove.add(pageHash.getKey());
|
||||||
|
log.warn("We have more donations than on the page of hash {}:{}.", pageHash.getKey(), amountDifference);
|
||||||
|
} else {
|
||||||
|
pageHash.setValue(Pair.of(amountDifference, pageHash.getValue().getRight()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pageHashesToRemove.forEach(donationFromPageHashes::remove);
|
||||||
|
if(donationFromPageHashes.isEmpty()) {
|
||||||
|
log.info("No new donations - ending search.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CompletableFuture<Void>> notificationFutures = new ArrayList<>();
|
||||||
|
donationFromPageHashes.values().forEach(donationInfo -> {
|
||||||
|
for (int i = 0; i < donationInfo.getLeft(); i++) {
|
||||||
|
DonationDto donationDto = donationInfo.getRight();
|
||||||
|
DonationResponseModel model = DonationResponseModel
|
||||||
|
.builder()
|
||||||
|
.message(donationDto.getText())
|
||||||
|
.donatorName(donationDto.getName())
|
||||||
|
.amount(donationDto.getAmount())
|
||||||
|
.anonymous(donationDto.getAnonymous())
|
||||||
|
.build();
|
||||||
|
notificationFutures.add(sendDonationNotification(model));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
new CompletableFutureList<>(notificationFutures).getMainFuture().thenAccept(unused -> {
|
||||||
|
log.info("All {} notifications send.", notificationFutures.size());
|
||||||
|
}).exceptionally(throwable -> {
|
||||||
|
log.warn("Failed to send notifications about {} new donations.", notificationFutures.size(), throwable);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
log.info("Creating/updating {} donation entries.", donationFromPageHashes.size());
|
||||||
|
allDonations.forEach(donation -> {
|
||||||
|
Set<String> donationsToRemoveBecauseUpdate = new HashSet<>();
|
||||||
|
donationFromPageHashes.forEach((key, value) -> {
|
||||||
|
// its assumed that donationFromPageHashes only contains donations that need to be created
|
||||||
|
if (donation.getId().equals(key)) {
|
||||||
|
donation.setCount(value.getLeft() + donation.getCount());
|
||||||
|
donationManagementServiceBean.updateDonation(donation);
|
||||||
|
donationsToRemoveBecauseUpdate.add(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
donationsToRemoveBecauseUpdate.forEach(donationFromPageHashes::remove);
|
||||||
|
});
|
||||||
|
donationFromPageHashes.forEach((key, value) ->
|
||||||
|
donationManagementServiceBean.saveDonation(key, value.getLeft()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Void> sendDonationNotification(DonationResponseModel donation) {
|
||||||
Long targetServerId = Long.parseLong(System.getenv(DEBRA_DONATION_NOTIFICATION_SERVER_ID_ENV_NAME));
|
Long targetServerId = Long.parseLong(System.getenv(DEBRA_DONATION_NOTIFICATION_SERVER_ID_ENV_NAME));
|
||||||
DonationsModel donationInfoModel = getDonationInfoModel(targetServerId);
|
DonationsModel donationInfoModel = getDonationInfoModel();
|
||||||
DonationNotificationModel model = DonationNotificationModel
|
DonationNotificationModel model = DonationNotificationModel
|
||||||
.builder()
|
.builder()
|
||||||
.donation(donation)
|
.donation(donation)
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package dev.sheldan.sissi.module.debra.service.management;
|
||||||
|
|
||||||
|
import dev.sheldan.sissi.module.debra.model.database.Donation;
|
||||||
|
import dev.sheldan.sissi.module.debra.repository.DonationRepository;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class DonationManagementServiceBean {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DonationRepository donationRepository;
|
||||||
|
|
||||||
|
public List<Donation> getAllDonations() {
|
||||||
|
return donationRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateDonation(Donation donation) {
|
||||||
|
donationRepository.save(donation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Donation saveDonation(String hash, Integer count) {
|
||||||
|
Donation donation = Donation
|
||||||
|
.builder()
|
||||||
|
.id(hash)
|
||||||
|
.count(count)
|
||||||
|
.build();
|
||||||
|
return donationRepository.save(donation);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,8 +4,7 @@ abstracto.featureFlags.debra.enabled=false
|
|||||||
abstracto.postTargets.debraDonationNotification.name=debraDonationNotification
|
abstracto.postTargets.debraDonationNotification.name=debraDonationNotification
|
||||||
abstracto.postTargets.debraDonationNotification2.name=debraDonationNotification2
|
abstracto.postTargets.debraDonationNotification2.name=debraDonationNotification2
|
||||||
|
|
||||||
sissi.debra.websocketURL=ws://spenden.baba.fm:8765/
|
sissi.debra.donationPageUrl=https://secure.sicherhelfen.org/campaigns/07a3baf6-5cdc-4300-854b-ea2b36b0b218/show
|
||||||
sissi.debra.donationAPIUrl=https://www.altruja.de/api/page/discord-schmetterlingsaktion-2024?details=1&num=%s&ort=0
|
|
||||||
|
|
||||||
abstracto.systemConfigs.debraDonationNotificationDelayMillis.name=debraDonationNotificationDelayMillis
|
abstracto.systemConfigs.debraDonationNotificationDelayMillis.name=debraDonationNotificationDelayMillis
|
||||||
abstracto.systemConfigs.debraDonationNotificationDelayMillis.longValue=60000
|
abstracto.systemConfigs.debraDonationNotificationDelayMillis.longValue=60000
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
|
||||||
|
<include file="seedData/data.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="tables/tables.xml" relativeToChangelogFile="true"/>
|
||||||
|
</databaseChangeLog>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
|
||||||
|
<include file="donation_fetch_job.xml" relativeToChangelogFile="true"/>
|
||||||
|
</databaseChangeLog>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
|
||||||
|
<changeSet author="Sheldan" id="donation_fetch_job-insert">
|
||||||
|
<insert tableName="scheduler_job">
|
||||||
|
<column name="name" value="donationFetchJob"/>
|
||||||
|
<column name="group_name" value="debra"/>
|
||||||
|
<column name="clazz" value="dev.sheldan.sissi.module.debra.job.DonationFetchJob"/>
|
||||||
|
<column name="active" value="true"/>
|
||||||
|
<column name="cron_expression" value="0 */2 * * * ?"/>
|
||||||
|
<column name="recovery" value="false"/>
|
||||||
|
</insert>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
|
||||||
|
<changeSet author="Sheldan" id="donation-table">
|
||||||
|
<createTable tableName="donation">
|
||||||
|
<column name="id" type="TEXT">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="count" type="NUMERIC">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="created" type="TIMESTAMP WITHOUT TIME ZONE">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column name="updated" type="TIMESTAMP WITHOUT TIME ZONE"/>
|
||||||
|
</createTable>
|
||||||
|
<sql>
|
||||||
|
DROP TRIGGER IF EXISTS donation_update_trigger ON donation;
|
||||||
|
CREATE TRIGGER donation_update_trigger BEFORE UPDATE ON donation FOR EACH ROW EXECUTE PROCEDURE update_trigger_procedure();
|
||||||
|
</sql>
|
||||||
|
<sql>
|
||||||
|
DROP TRIGGER IF EXISTS donation_insert_trigger ON donation;
|
||||||
|
CREATE TRIGGER donation_insert_trigger BEFORE INSERT ON donation FOR EACH ROW EXECUTE PROCEDURE insert_trigger_procedure();
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.26.xsd" >
|
||||||
|
<include file="donation.xml" relativeToChangelogFile="true"/>
|
||||||
|
</databaseChangeLog>
|
||||||
@@ -5,4 +5,5 @@
|
|||||||
<include file="1.3.6/collection.xml" relativeToChangelogFile="true"/>
|
<include file="1.3.6/collection.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="1.4.21/collection.xml" relativeToChangelogFile="true"/>
|
<include file="1.4.21/collection.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="1.4.29/collection.xml" relativeToChangelogFile="true"/>
|
<include file="1.4.29/collection.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="1.5.16/collection.xml" relativeToChangelogFile="true"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
@@ -60,7 +60,7 @@ spec:
|
|||||||
- name: DB_SCHEMA
|
- name: DB_SCHEMA
|
||||||
value: {{ .Values.dbCredentials.schema }}
|
value: {{ .Values.dbCredentials.schema }}
|
||||||
- name: DEBRA_DONATION_NOTIFICATION_SERVER_ID
|
- name: DEBRA_DONATION_NOTIFICATION_SERVER_ID
|
||||||
value: "297910194841583616"
|
value: "{{ .Values.bot.config.debraNotificationServerId }}"
|
||||||
- name: WEEKLY_TEXT_SERVER_ID
|
- name: WEEKLY_TEXT_SERVER_ID
|
||||||
value: "{{ .Values.bot.config.weeklyTextServerId }}"
|
value: "{{ .Values.bot.config.weeklyTextServerId }}"
|
||||||
- name: TOKEN
|
- name: TOKEN
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ bot:
|
|||||||
host: null
|
host: null
|
||||||
config:
|
config:
|
||||||
weeklyTextServerId: null
|
weeklyTextServerId: null
|
||||||
|
debraNotificationServerId: null
|
||||||
restApi:
|
restApi:
|
||||||
enabled: true
|
enabled: true
|
||||||
repository: harbor.sheldan.dev/sissi
|
repository: harbor.sheldan.dev/sissi
|
||||||
|
|||||||
5
pom.xml
5
pom.xml
@@ -18,10 +18,11 @@
|
|||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.target>17</maven.compiler.target>
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
<maven.compiler.source>17</maven.compiler.source>
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
<abstracto.version>1.6.17-SNAPSHOT</abstracto.version>
|
<abstracto.version>1.6.17</abstracto.version>
|
||||||
<abstracto.templates.version>1.4.63-SNAPSHOT</abstracto.templates.version>
|
<abstracto.templates.version>1.4.63</abstracto.templates.version>
|
||||||
<apache-jena.version>4.9.0</apache-jena.version>
|
<apache-jena.version>4.9.0</apache-jena.version>
|
||||||
<rssreader.version>3.5.0</rssreader.version>
|
<rssreader.version>3.5.0</rssreader.version>
|
||||||
|
<jsoup.version>1.21.2</jsoup.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
,"fields": [
|
,"fields": [
|
||||||
<#list donations as donation>
|
<#list donations as donation>
|
||||||
{
|
{
|
||||||
"name": "<#if donation.anonymous><#include "donations_response_anonymous"><#else>${donation.firstName}</#if>",
|
"name": "<#if donation.anonymous><#include "donations_response_anonymous"><#else>${donation.name}</#if>",
|
||||||
"value": "${donation.donationAmount}€",
|
"value": "${donation.donationAmount}€",
|
||||||
"inline": true
|
"inline": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
"embeds": [
|
"embeds": [
|
||||||
{
|
{
|
||||||
"title": {
|
"title": {
|
||||||
<#assign donatorName=donation.donatorName>
|
<#assign donatorName><#if donation.anonymous><#include "donations_response_anonymous"><#else>${donation.donatorName}</#if></#assign>
|
||||||
<#assign donationAmount=donation.amount>
|
<#assign donationAmount=donation.amount>
|
||||||
"title": "<@safe_include "debra_donation_notification_embed_title"/>"
|
"title": "<@safe_include "debra_donation_notification_embed_title"/>"
|
||||||
},
|
},
|
||||||
|
<#if donation.message != 'gespendet'>
|
||||||
<#assign donationMessage=donation.message>
|
<#assign donationMessage=donation.message>
|
||||||
"description": "${donationMessage?json_string}",
|
"description": "${donationMessage?json_string}",
|
||||||
|
</#if>
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
<#assign totalDonationAmount=totalDonationAmount>
|
<#assign totalDonationAmount=totalDonationAmount>
|
||||||
@@ -24,7 +26,7 @@
|
|||||||
"buttons": [
|
"buttons": [
|
||||||
{
|
{
|
||||||
"label": "<@safe_include "debra_donation_notification_link_button_label"/>",
|
"label": "<@safe_include "debra_donation_notification_link_button_label"/>",
|
||||||
"url": "http://tiny.cc/schmetterling2024",
|
"url": "https://tinyurl.com/debra25",
|
||||||
"buttonStyle": "link",
|
"buttonStyle": "link",
|
||||||
"metaConfig": {
|
"metaConfig": {
|
||||||
"persistCallback": false
|
"persistCallback": false
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Aktuell wurden **${donationAmount} Euro** für Debra Austria gespendet. Spende auch du unter https://tiny.cc/schmetterling2024.
|
Aktuell wurden **${donationAmount} Euro** für Debra Austria gespendet. Spende auch du unter https://tinyurl.com/debra25.
|
||||||
@@ -10,7 +10,7 @@ Alle Grafiken von diesem und den letzten Jahren findest du in diesem Ordner. All
|
|||||||
|
|
||||||
Spendenlink
|
Spendenlink
|
||||||
Wir empfehlen neben dem Einrichten einer 'Kachel' auch, dass ihr einen !spenden Befehl bei eurem Bot (nightbot, moobot, self hosted etc.) hinzufügt.
|
Wir empfehlen neben dem Einrichten einer 'Kachel' auch, dass ihr einen !spenden Befehl bei eurem Bot (nightbot, moobot, self hosted etc.) hinzufügt.
|
||||||
Bitte verlinke dabei auf <https://tiny.cc/schmetterling2024>
|
Bitte verlinke dabei auf <https://tinyurl.com/debra25>
|
||||||
|
|
||||||
Live-ping am Discord
|
Live-ping am Discord
|
||||||
Damit wir deinen Account zur Going Live Liste hinzufügen können, musst du uns nach der Einrichtung des Streams bescheid geben.
|
Damit wir deinen Account zur Going Live Liste hinzufügen können, musst du uns nach der Einrichtung des Streams bescheid geben.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Spende auch du für Debra Austria unter http://tiny.cc/schmetterling2024.
|
Spende auch du für Debra Austria unter https://tinyurl.com/debra25.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
The latest donations
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
The highest donations
|
||||||
@@ -1 +1 @@
|
|||||||
Also donate for Debra austria via http://tiny.cc/schmetterling2024.
|
Also donate for Debra austria via https://tinyurl.com/debra25.
|
||||||
Reference in New Issue
Block a user