discord4j.core.ExampleBot.java Source code

Java tutorial

Introduction

Here is the source code for discord4j.core.ExampleBot.java

Source

/*
 * This file is part of Discord4J.
 *
 * Discord4J is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Discord4J is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Discord4J. If not, see <http://www.gnu.org/licenses/>.
 */

package discord4j.core;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import discord4j.core.event.domain.lifecycle.ReadyEvent;
import discord4j.core.event.domain.lifecycle.ResumeEvent;
import discord4j.core.event.domain.message.MessageCreateEvent;
import discord4j.core.object.entity.*;
import discord4j.core.object.reaction.ReactionEmoji;
import discord4j.core.object.util.Image;
import discord4j.core.object.util.Snowflake;
import discord4j.rest.http.client.ClientException;
import org.apache.commons.io.IOUtils;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.slf4j.LoggerFactory;
import reactor.core.Exceptions;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;
import reactor.util.Logger;
import reactor.util.Loggers;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class ExampleBot {

    private static final Logger log = Loggers.getLogger(ExampleBot.class);

    private static String token;
    private static String testRole;

    @BeforeClass
    public static void initialize() {
        token = System.getenv("token");
        testRole = System.getenv("testRole");
    }

    @Test
    @Ignore("Example code excluded from CI")
    public void testCommandBot() {
        DiscordClient client = new DiscordClientBuilder(token).build();

        // Get the bot owner ID to filter commands
        AtomicLong ownerId = new AtomicLong();
        Flux.first(client.getEventDispatcher().on(ReadyEvent.class),
                client.getEventDispatcher().on(ResumeEvent.class)).next()
                .flatMap(evt -> client.getApplicationInfo()).map(ApplicationInfo::getOwnerId).map(Snowflake::asLong)
                .subscribe(ownerId::set);

        // Create our event handlers
        List<EventHandler> eventHandlers = new ArrayList<>();
        eventHandlers.add(new AddRole());
        eventHandlers.add(new Echo());
        eventHandlers.add(new UserInfo());
        eventHandlers.add(new LogLevelChange());
        eventHandlers.add(new BlockingEcho());
        eventHandlers.add(new Reactor());
        eventHandlers.add(new ChangeAvatar());

        // Build a safe event-processing pipeline
        client.getEventDispatcher().on(MessageCreateEvent.class)
                .filter(event -> event.getMessage().getAuthor().map(User::getId).map(Snowflake::asLong)
                        .filter(id -> ownerId.get() == id).isPresent())
                .flatMap(event -> Mono.whenDelayError(eventHandlers.stream()
                        .map(handler -> handler.onMessageCreate(event)).collect(Collectors.toList())))
                .onErrorContinue((t, o) -> log.error("Error while processing event", t)).subscribe();

        client.login().block();
    }

    public static class AddRole extends EventHandler {

        @Override
        public Mono<Void> onMessageCreate(MessageCreateEvent event) {
            Message message = event.getMessage();
            return message.getContent().filter(content -> content.startsWith("!addrole")).map(Mono::just)
                    .orElseGet(Mono::empty).flatMap(content -> message.getAuthorAsMember())
                    .flatMap(member -> member.addRole(Snowflake.of(testRole), null));
            // if "testRole" is null, the bot will keep processing events despite throwing an error
        }
    }

    public static class Echo extends EventHandler {

        @Override
        public Mono<Void> onMessageCreate(MessageCreateEvent event) {
            Message message = event.getMessage();
            return message.getContent().filter(content -> content.startsWith("!echo ")).map(Mono::just)
                    .orElseGet(Mono::empty).map(content -> content.substring("!echo ".length()))
                    .flatMap(source -> message.getChannel().flatMap(channel -> channel.createMessage(source)))
                    .then();
        }
    }

    public static class UserInfo extends EventHandler {

        @Override
        public Mono<Void> onMessageCreate(MessageCreateEvent event) {
            Message message = event.getMessage();
            if (message.getContent().filter(content -> content.startsWith("!user ")).isPresent()) {
                return Mono.justOrEmpty(message.getContent()).map(content -> content.split(" ", 2))
                        .flatMap(tokens -> message.getClient().getUserById(Snowflake.of(tokens[1])))
                        .flatMap(user -> message.getChannel().flatMap(channel -> channel.createMessage(
                                msg -> msg.setEmbed(embed -> embed.addField("Name", user.getUsername(), false)
                                        .addField("Avatar URL", user.getAvatarUrl(), false)
                                        .setImage(user.getAvatarUrl())))))
                        .then();
            }
            return Mono.empty();
        }
    }

    public static class LogLevelChange extends EventHandler {

        private final LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();

        @Override
        public Mono<Void> onMessageCreate(MessageCreateEvent event) {
            Message message = event.getMessage();
            if (message.getContent().filter(content -> content.startsWith("!log ")).isPresent()) {
                return Mono.justOrEmpty(message.getContent()).map(content -> content.split(" ", 3))
                        .doOnNext(tokens -> {
                            String level = tokens[1];
                            String name = tokens[2];
                            Level logLevel = Level.valueOf(level);
                            context.getLoggerList().stream().filter(logger -> logger.getName().startsWith(name))
                                    .forEach(logger -> {
                                        log.info("Changing {} to {}", logger, logLevel);
                                        logger.setLevel(logLevel);
                                    });
                        }).then();
            }
            return Mono.empty();
        }
    }

    public static class BlockingEcho extends EventHandler {

        @Override
        public Mono<Void> onMessageCreate(MessageCreateEvent event) {
            Message message = event.getMessage();
            message.getContent().filter(content -> content.startsWith("!echos ")).ifPresent(content -> {
                String source = content.substring("!echos ".length());
                MessageChannel channel = message.getChannel().block();
                channel.createMessage(source).block();
            });
            return Mono.empty();
        }
    }

    public static class Reactor extends EventHandler {

        private final Random random = new Random();
        private final List<String> emoji = new ArrayList<>(Arrays.asList("", "", "", "", "",
                "", "", "", "", "", "", "", "", "", "", "", "",
                "", "", "", "", "", "", "", "", "", "", "", "",
                "", "", "", "", "", "", "", "", "", "?", "", "?",
                "", "", "", "", "", "?", "", "?", "?", "", "?", "", "",
                "?", "", "", "", "", "?", "", "", "", "", "", "",
                "", "", "", "", "", "", "", "", "", "", "", "",
                "", "?", "", "", "", "", "", "", "?", "", "", "",
                "", "?", "", "", "", "????", "?",
                "?????", "?????", "??",
                "??", "", "", "", "", "", "", "", "", "",
                "", "", "", "", "", "", "", "", "", "", "", "",
                "?", "", "", "", "", "?", "", "", "?", "?", "?", "?",
                "?", "?", "?", "?", "?", "?", "?", "?", "", "", "?", "?",
                "?", "?", "?", "", "??", "?", "?", "", "", "", "?", "?",
                "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?",
                "?", "?", "??", "?", "?", "??", "?", "", "?", "?", "?", "?",
                "?", "?", "?", "?", "", "", "", "", "", "", "", "??",
                "?", "?", "", "?", "?", "", "", "", "", "", "", "?",
                "?", "", "?", "", "?", "?", "", "", "", "", "", "",
                "", "", "", "?", "", "", "", "?", "", "", "", "",
                "", "", "", "", "", "?", "", "", "", "", "", "", "",
                "", "", "", "??", "?", "??", "?", "?", "?", "?", "", "?",
                "??", "?", "?", "?", "", "?", "?", "?", "?", "", "?", "?",
                "", "", "??", "?", "", "?", "?", "?", "?", "?", "?", "?",
                "?", "?", "?", "?", "", "?", "?", "?", "?", "?", "?", "?",
                "?", "?", "?", "?", "?", "?", "?", "?", "", "?", "?", "?",
                "?", "?", "?", "?", "?", "", "?", "?", "", "", "??", "?",
                "", "", "??", "?", "?", "?", "", "", "?", "", "?", "", "",
                "??", "?", "?", "", "?", "", "", "?", "", "", "", "?",
                "", "", "?", "", "", "", "", "", "", "", "", "",
                "", "", "", "", "", "", "", "", "", "", "", "",
                "", "", "?", "", "", "?", "", "", "??", "", "", "",
                "", "", "", "", "", "?", "", "", "?", "", "", "",
                "", "", "", "", "", "", "", "?", "", "", "", "",
                "", "", "", "", "", "", "", "??", "", "", "?", "",
                "", "", "", "", "", "?", "?", "", "?", "", "", "?", "",
                "", "", "?", "", "", "?", "", "", "", "", "?", "?",
                "??", "", "", "", "", "", "?", "", "", "", "", "?",
                "?", "?", "?", "", "?", "?", "?", "?", "?", "?", "?", "?",
                "?", "", "?", "", "?", "?", "?", "?", "?", "", "", "",
                "", "", "", "", "", "", "", "", "?", "", "", "",
                "", "", "?", "?", "", "?", "", "", "", "", "", "", "",
                "", "", "", "", "", "", "", "", "", "", "", "",
                "", "", "", "", "", "", "", "", "", "", "?", "",
                "?", "", "?", "", "", "", "", "", "?", "", "?", "",
                "", "", "", "", "", "", "", "", "", "", "", "",
                "", "", "", "", "", "?", "", "?", "", "?", "", "", "",
                "", "", "?", "", ""));

        @Override
        public Mono<Void> onMessageCreate(MessageCreateEvent event) {
            Message message = event.getMessage();
            return message.getContent().filter(content -> content.startsWith("!react")).map(content -> {
                String rawCount = content.substring("!react".length());
                int count = 1;
                if (!rawCount.isEmpty()) {
                    try {
                        count = Math.max(1, Math.min(20, Integer.parseInt(rawCount.trim())));
                    } catch (NumberFormatException e) {
                        throw Exceptions.propagate(e);
                    }
                }
                return Flux.fromIterable(fetch(count))
                        .flatMap(emoji -> message.addReaction(ReactionEmoji.unicode(emoji)).onErrorContinue(
                                isStatusCode(400), (t, o) -> log.info("Dropping {} due to {}", t.toString())))
                        .then();
            }).orElseGet(Mono::empty);
        }

        private List<String> fetch(int count) {
            List<String> reactions = new ArrayList<>();
            for (int i = 0; i < count; i++) {
                int index = random.nextInt(emoji.size());
                reactions.add(emoji.get(index));
                emoji.remove(index);
            }
            return reactions;
        }
    }

    public static class ChangeAvatar extends EventHandler {

        @Override
        public Mono<Void> onMessageCreate(MessageCreateEvent event) {
            Message message = event.getMessage();
            if (message.getContent().filter(content -> content.equals("!avatar")).isPresent()) {
                for (Attachment attachment : message.getAttachments()) {
                    // This code is very optimistic as it does not check for status codes or file types
                    return HttpClient.create().get().uri(attachment.getUrl())
                            .responseSingle((res, mono) -> mono.asInputStream())
                            .flatMap(input -> message.getClient().edit(spec -> {
                                try {
                                    spec.setAvatar(Image.ofRaw(IOUtils.toByteArray(input), Image.Format.PNG));
                                } catch (IOException e) {
                                    throw Exceptions.propagate(e);
                                }
                            })).then();
                }
            }
            return Mono.empty();
        }
    }

    public static abstract class EventHandler {

        public abstract Mono<Void> onMessageCreate(MessageCreateEvent event);
    }

    private static Predicate<Throwable> isStatusCode(int code) {
        return t -> {
            if (t instanceof ClientException) {
                ClientException e = (ClientException) t;
                return e.getStatus().code() == code;
            }
            return false;
        };
    }
}