ScheduleManager.java

        package com.niklasarndt.discordbutler.scheduler;

import com.niklasarndt.discordbutler.Butler;
import com.niklasarndt.discordbutler.util.ButlerUtils;
import com.niklasarndt.discordbutler.util.Emojis;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * Created by Niklas on 2020/08/01.
 * <p>
 * WARNING: Automating a task without explicit user consent is considered API abuse.
 * A message should only be scheduled if the user to you to do so. (e.g. via the remind command)
 * <p>
 * Quote: "You may not post messages, trigger notifications, or play audio on behalf of a Discord
 * user except in response to such Discord user expressly opting-in to each instance of such action"
 * <p>
 * https://discord.com/developers/docs/policy (1st August 2020)
 */
public class ScheduleManager {

    public static final String MESSAGE_REMINDER_NAME = "Scheduled Message";

    private final Logger logger = LoggerFactory.getLogger(getClass());
    private final ScheduledExecutorService executorService = Executors
            .newScheduledThreadPool(1, runnable -> new Thread(null, runnable,
                    "ScheduleThread-" + System.currentTimeMillis()));
    private final Butler butler;
    private final List<ScheduledTask> failedTasks = new ArrayList<>();
    private List<ScheduledTask> tasks = new ArrayList<>();
    private final AtomicInteger index = new AtomicInteger();

    public ScheduleManager(Butler butler) {
        this.butler = butler;
    }

    private ScheduledTask schedule(ScheduledTask task) {
        tasks.add(task);
        executorService.schedule(() -> {
            if (!hasTask(task.getId())) {
                logger.debug("Skipping cancelled task with id {}.", task.getId());
                return;
            }
            try {
                task.execute();
            } catch (Exception e) {
                logger.error("Failed to run scheduled task", e);
                failedTasks.add(task);
            }
        }, task.getWaitTimeInMs(), TimeUnit.MILLISECONDS);
        return task;
    }

    public ScheduledTask schedule(String name, Runnable runnable, long waitTimeInMs) {
        return schedule(new ScheduledTask(index.incrementAndGet(), name, runnable, waitTimeInMs));
    }

    public void scheduleMessage(String message, long waitTimeInMs) {
        schedule(MESSAGE_REMINDER_NAME, () -> {
            String duration = ButlerUtils.prettyPrintTime(waitTimeInMs);

            String intro = String.format("Hey there %s Here's what you " +
                            "asked me to **remind** you of **%s ago**!", Emojis.WAVE,
                    duration);

            MessageEmbed embed = new EmbedBuilder()
                    .addField("Your Reminder", message, false)
                    .setFooter(String.format("Requested %s ago", duration)).build();

            butler.getJda().retrieveUserById(butler.getOwnerId())
                    .flatMap(User::openPrivateChannel)
                    .flatMap(channel -> channel.sendMessage(intro).embed(embed))
                    .flatMap(sent -> sent.addReaction(Emojis.HOURGLASS))
                    .queue();
        }, waitTimeInMs);
    }

    public void shutdown() {
        executorService.shutdown();
        try {
            if (!executorService.awaitTermination(1, TimeUnit.SECONDS)) {
                executorService.shutdownNow();
            }
        } catch (InterruptedException e) {
            logger.warn("Executor service did not shut down automatically, forcing shutdown", e);
            executorService.shutdownNow();
        }
    }

    public boolean isShutdown() {
        return executorService.isShutdown();
    }

    public List<ScheduledTask> getFailedTasks(boolean clearAfterwards) {
        List<ScheduledTask> result = Collections.unmodifiableList(
                clearAfterwards ? List.copyOf(failedTasks) : failedTasks);
        if (clearAfterwards) failedTasks.clear();
        return result;
    }

    public List<ScheduledTask> getScheduledTasks() {
        tasks = tasks.stream().filter(item -> !item.shouldBeExecuted())
                .collect(Collectors.toList());
        return Collections.unmodifiableList(tasks);
    }

    public boolean hasTask(int id) {
        return tasks.stream().anyMatch(i -> i.getId() == id);
    }

    public boolean cancel(int id) {
        Optional<ScheduledTask> task = tasks.stream().filter(i -> i.getId() == id).findFirst();

        task.ifPresent(i -> tasks.remove(i));

        return task.isPresent();
    }

    public int cancel(int... ids) {
        AtomicInteger result = new AtomicInteger();
        for (int id : ids) {
            if (cancel(id)) result.getAndIncrement();
        }
        return result.get();
    }

    public int cancel(List<Integer> taskIds) {
        AtomicInteger result = new AtomicInteger();
        taskIds.forEach(i -> {
            if (cancel(i)) result.getAndIncrement();
        });
        return result.get();
    }
}