Java tutorial
/* * MIT License * * Copyright (c) 2017 Frederik Ar. Mikkelsen * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * */ package fredboat; import com.mashape.unirest.http.HttpResponse; import com.mashape.unirest.http.JsonNode; import com.mashape.unirest.http.Unirest; import com.mashape.unirest.http.exceptions.UnirestException; import fredboat.agent.CarbonitexAgent; import fredboat.agent.ShardWatchdogAgent; import fredboat.api.API; import fredboat.api.OAuthManager; import fredboat.audio.GuildPlayer; import fredboat.audio.MusicPersistenceHandler; import fredboat.audio.PlayerRegistry; import fredboat.commandmeta.CommandRegistry; import fredboat.commandmeta.init.MainCommandInitializer; import fredboat.commandmeta.init.MusicCommandInitializer; import fredboat.db.DatabaseManager; import fredboat.event.EventListenerBoat; import fredboat.event.EventListenerSelf; import fredboat.event.ShardWatchdogListener; import fredboat.feature.I18n; import fredboat.util.DistributionEnum; import fredboat.util.log.SimpleLogToSLF4JAdapter; import frederikam.jca.JCA; import frederikam.jca.JCABuilder; import net.dv8tion.jda.core.AccountType; import net.dv8tion.jda.core.JDA; import net.dv8tion.jda.core.JDAInfo; import net.dv8tion.jda.core.entities.Guild; import net.dv8tion.jda.core.entities.TextChannel; import net.dv8tion.jda.core.entities.User; import net.dv8tion.jda.core.entities.VoiceChannel; import net.dv8tion.jda.core.events.ReadyEvent; import net.dv8tion.jda.core.hooks.EventListener; import net.dv8tion.jda.core.managers.AudioManager; import net.dv8tion.jda.core.utils.SimpleLog; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.security.auth.login.LoginException; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; public abstract class FredBoat { private static final Logger log = LoggerFactory.getLogger(FredBoat.class); static final int SHARD_CREATION_SLEEP_INTERVAL = 5100; private static final ArrayList<FredBoat> shards = new ArrayList<>(); public static JCA jca; public static final long START_TIME = System.currentTimeMillis(); public static final int UNKNOWN_SHUTDOWN_CODE = -991023; public static int shutdownCode = UNKNOWN_SHUTDOWN_CODE;//Used when specifying the intended code for shutdown hooks static EventListenerBoat listenerBot; static EventListenerSelf listenerSelf; ShardWatchdogListener shardWatchdogListener = null; private static AtomicInteger numShardsReady = new AtomicInteger(0); //For when we need to join a revived shard with it's old GuildPlayers final ArrayList<String> channelsToRejoin = new ArrayList<>(); //unlimited threads = http://i.imgur.com/H3b7H1S.gif //use this executor for various small async tasks public final static ExecutorService executor = Executors.newCachedThreadPool(); JDA jda; private static FredBoatClient fbClient; private boolean hasReadiedOnce = false; public static void main(String[] args) throws LoginException, IllegalArgumentException, InterruptedException, IOException, UnirestException { Runtime.getRuntime().addShutdownHook(new Thread(ON_SHUTDOWN)); log.info("\n\n" + " ______ _ ____ _ \n" + " | ____| | | _ \\ | | \n" + " | |__ _ __ ___ __| | |_) | ___ __ _| |_ \n" + " | __| '__/ _ \\/ _` | _ < / _ \\ / _` | __|\n" + " | | | | | __/ (_| | |_) | (_) | (_| | |_ \n" + " |_| |_| \\___|\\__,_|____/ \\___/ \\__,_|\\__|\n\n"); I18n.start(); //Attach log adapter SimpleLog.addListener(new SimpleLogToSLF4JAdapter()); //Make JDA not print to console, we have Logback for that SimpleLog.LEVEL = SimpleLog.Level.OFF; int scope; try { scope = Integer.parseInt(args[0]); } catch (NumberFormatException | ArrayIndexOutOfBoundsException ignored) { log.info("Invalid scope, defaulting to scopes 0x111"); scope = 0x111; } log.info("Starting with scopes:" + "\n\tMain: " + ((scope & 0x100) == 0x100) + "\n\tMusic: " + ((scope & 0x010) == 0x010) + "\n\tSelf: " + ((scope & 0x001) == 0x001)); log.info("JDA version:\t" + JDAInfo.VERSION); Config.loadDefaultConfig(scope); try { API.start(); } catch (Exception e) { log.info("Failed to ignite Spark, FredBoat API unavailable", e); } try { if (!Config.CONFIG.getJdbcUrl().equals("") && !Config.CONFIG.getOauthSecret().equals("")) { DatabaseManager.startup(Config.CONFIG.getJdbcUrl(), null, Config.CONFIG.getHikariPoolSize()); OAuthManager.start(Config.CONFIG.getBotToken(), Config.CONFIG.getOauthSecret()); } else { log.warn("No JDBC URL and/or secret found, skipped database connection and OAuth2 client"); log.warn("Falling back to internal SQLite db"); DatabaseManager.startup("jdbc:sqlite:fredboat.db", "org.hibernate.dialect.SQLiteDialect", Config.CONFIG.getHikariPoolSize()); } } catch (Exception e) { log.info("Failed to start DatabaseManager and OAuth2 client", e); } //Initialise event listeners listenerBot = new EventListenerBoat(); listenerSelf = new EventListenerSelf(); //Commands if (Config.CONFIG.getDistribution() == DistributionEnum.DEVELOPMENT || Config.CONFIG.getDistribution() == DistributionEnum.MAIN) MainCommandInitializer.initCommands(); if (Config.CONFIG.getDistribution() == DistributionEnum.DEVELOPMENT || Config.CONFIG.getDistribution() == DistributionEnum.MUSIC || Config.CONFIG.getDistribution() == DistributionEnum.PATRON) MusicCommandInitializer.initCommands(); log.info("Loaded commands, registry size is " + CommandRegistry.getSize()); //Check MAL creds executor.submit(FredBoat::hasValidMALLogin); //Check imgur creds executor.submit(FredBoat::hasValidImgurCredentials); //Initialise JCA executor.submit(FredBoat::loadJCA); /* Init JDA */ if ((Config.CONFIG.getScope() & 0x110) != 0) { initBotShards(listenerBot); } if ((Config.CONFIG.getScope() & 0x001) != 0) { log.error("Selfbot support has been removed."); //fbClient = new FredBoatClient(); } if (Config.CONFIG.getDistribution() == DistributionEnum.MUSIC && Config.CONFIG.getCarbonKey() != null) { CarbonitexAgent carbonitexAgent = new CarbonitexAgent(Config.CONFIG.getCarbonKey()); carbonitexAgent.setDaemon(true); carbonitexAgent.start(); } ShardWatchdogAgent shardWatchdogAgent = new ShardWatchdogAgent(); shardWatchdogAgent.setDaemon(true); shardWatchdogAgent.start(); } private static boolean loadJCA() { boolean result = true; try { if (!Config.CONFIG.getCbUser().equals("") && !Config.CONFIG.getCbKey().equals("")) { log.info("Starting CleverBot"); jca = new JCABuilder().setKey(Config.CONFIG.getCbKey()).setUser(Config.CONFIG.getCbUser()) .buildBlocking(); } else { log.warn("Credentials not found for cleverbot authentication. Skipping..."); result = false; } } catch (Exception e) { log.error("Error when starting JCA", e); result = false; } return result; } private static boolean hasValidMALLogin() { if ("".equals(Config.CONFIG.getMalUser()) || "".equals(Config.CONFIG.getMalPassword())) { log.info("MAL credentials not found. MAL related commands will not be available."); return false; } try { HttpResponse<String> response = Unirest .get("https://myanimelist.net/api/account/verify_credentials.xml") .basicAuth(Config.CONFIG.getMalUser(), Config.CONFIG.getMalPassword()).asString(); int responseStatus = response.getStatus(); if (responseStatus == 200) { log.info("MAL login successful"); return true; } else { log.warn("MAL login failed with " + responseStatus + ": " + response.getBody()); } } catch (UnirestException e) { log.warn("MAL login failed, it seems to be down.", e); } return false; } private static boolean hasValidImgurCredentials() { if ("".equals(Config.CONFIG.getImgurClientId())) { log.info("Imgur credentials not found. Commands relying on Imgur will not work properly."); return false; } try { HttpResponse<JsonNode> response = Unirest.get("https://api.imgur.com/3/credits") .header("Authorization", "Client-ID " + Config.CONFIG.getImgurClientId()).asJson(); int responseStatus = response.getStatus(); if (responseStatus == 200) { JSONObject data = response.getBody().getObject().getJSONObject("data"); //https://api.imgur.com/#limits //at the time of the introduction of this code imgur offers daily 12500 and hourly 500 GET requests for open source software //hitting the daily limit 5 times in a month will blacklist the app for the rest of the month //we use 3 requests per hour (and per restart of the bot), so there should be no problems with imgur's rate limit int hourlyLimit = data.getInt("UserLimit"); int hourlyLeft = data.getInt("UserRemaining"); long seconds = data.getLong("UserReset") - (System.currentTimeMillis() / 1000); String timeTillReset = String.format("%d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, (seconds % 60)); int dailyLimit = data.getInt("ClientLimit"); int dailyLeft = data.getInt("ClientRemaining"); log.info("Imgur credentials are valid. " + hourlyLeft + "/" + hourlyLimit + " requests remaining this hour, resetting in " + timeTillReset + ", " + dailyLeft + "/" + dailyLimit + " requests remaining today."); return true; } else { log.warn("Imgur login failed with " + responseStatus + ": " + response.getBody()); } } catch (UnirestException e) { log.warn("Imgur login failed, it seems to be down.", e); } return false; } private static void initBotShards(EventListener listener) { for (int i = 0; i < Config.CONFIG.getNumShards(); i++) { try { shards.add(i, new FredBoatBot(i, listener)); } catch (Exception e) { log.error("Caught an exception while starting shard " + i + "!", e); numShardsReady.getAndIncrement(); } try { Thread.sleep(SHARD_CREATION_SLEEP_INTERVAL); } catch (InterruptedException e) { throw new RuntimeException("Got interrupted while setting up bot shards!", e); } } log.info(shards.size() + " shards have been constructed"); } public void onInit(ReadyEvent readyEvent) { if (!hasReadiedOnce) { numShardsReady.incrementAndGet(); hasReadiedOnce = false; } log.info("Received ready event for " + FredBoat.getInstance(readyEvent.getJDA()).getShardInfo().getShardString()); int ready = numShardsReady.get(); if (ready == Config.CONFIG.getNumShards()) { log.info("All " + ready + " shards are ready."); MusicPersistenceHandler.reloadPlaylists(); } //Rejoin old channels if revived channelsToRejoin.forEach(vcid -> { VoiceChannel channel = jda.getVoiceChannelById(vcid); if (channel == null) return; GuildPlayer player = PlayerRegistry.get(channel.getGuild()); if (player == null) return; AudioManager am = channel.getGuild().getAudioManager(); am.openAudioConnection(channel); am.setSendingHandler(player); }); channelsToRejoin.clear(); } //Shutdown hook private static final Runnable ON_SHUTDOWN = () -> { int code = shutdownCode != UNKNOWN_SHUTDOWN_CODE ? shutdownCode : -1; try { MusicPersistenceHandler.handlePreShutdown(code); } catch (Exception e) { log.error("Critical error while handling music persistence.", e); } for (FredBoat fb : shards) { fb.getJda().shutdown(false); } try { Unirest.shutdown(); } catch (IOException ignored) { } executor.shutdown(); DatabaseManager.shutdown(); }; public static void shutdown(int code) { log.info("Shutting down with exit code " + code); shutdownCode = code; System.exit(code); } public static EventListenerBoat getListenerBot() { return listenerBot; } public static EventListenerSelf getListenerSelf() { return listenerSelf; } /* Sharding */ public JDA getJda() { return jda; } public static List<FredBoat> getShards() { return shards; } public static List<Guild> getAllGuilds() { ArrayList<Guild> list = new ArrayList<>(); for (FredBoat fb : shards) { list.addAll(fb.getJda().getGuilds()); } return list; } public static Map<String, User> getAllUsersAsMap() { HashMap<String, User> map = new HashMap<>(); for (FredBoat fb : shards) { for (User usr : fb.getJda().getUsers()) { map.put(usr.getId(), usr); } } return map; } public static TextChannel getTextChannelById(String id) { for (FredBoat fb : shards) { for (TextChannel channel : fb.getJda().getTextChannels()) { if (channel.getId().equals(id)) return channel; } } return null; } public static VoiceChannel getVoiceChannelById(String id) { for (FredBoat fb : shards) { for (VoiceChannel channel : fb.getJda().getVoiceChannels()) { if (channel.getId().equals(id)) return channel; } } return null; } public static FredBoatClient getClient() { return fbClient; } public static FredBoat getInstance(JDA jda) { if (jda.getAccountType() == AccountType.CLIENT) { return fbClient; } else { int sId = jda.getShardInfo() == null ? 0 : jda.getShardInfo().getShardId(); for (FredBoat fb : shards) { if (((FredBoatBot) fb).getShardId() == sId) { return fb; } } } throw new IllegalStateException("Attempted to get instance for JDA shard that is not indexed"); } public static FredBoat getInstance(int id) { return shards.get(id); } public static JDA getFirstJDA() { return shards.get(0).getJda(); } public ShardInfo getShardInfo() { int sId = jda.getShardInfo() == null ? 0 : jda.getShardInfo().getShardId(); if (jda.getAccountType() == AccountType.CLIENT) { return new ShardInfo(0, 1); } else { return new ShardInfo(sId, Config.CONFIG.getNumShards()); } } public abstract void revive(); public ShardWatchdogListener getShardWatchdogListener() { return shardWatchdogListener; } @SuppressWarnings("WeakerAccess") public class ShardInfo { int shardId; int shardTotal; ShardInfo(int shardId, int shardTotal) { this.shardId = shardId; this.shardTotal = shardTotal; } public int getShardId() { return this.shardId; } public int getShardTotal() { return this.shardTotal; } public String getShardString() { return String.format("[%02d / %02d]", this.shardId, this.shardTotal); } @Override public String toString() { return getShardString(); } } }