diff --git a/src/me/despawningbone/discordbot/command/info/CityInfo.java b/src/me/despawningbone/discordbot/command/info/CityInfo.java index c0a7cd3..8a831d8 100644 --- a/src/me/despawningbone/discordbot/command/info/CityInfo.java +++ b/src/me/despawningbone/discordbot/command/info/CityInfo.java @@ -1,283 +1,272 @@ package me.despawningbone.discordbot.command.info; import java.awt.Color; import java.io.IOException; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; import java.text.DecimalFormat; import java.text.NumberFormat; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Locale; import java.util.TimeZone; import org.apache.commons.lang3.exception.ExceptionUtils; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; import me.despawningbone.discordbot.command.Command; import me.despawningbone.discordbot.command.CommandResult; import me.despawningbone.discordbot.command.CommandResult.CommandResultType; import me.despawningbone.discordbot.utils.MiscUtils; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.exceptions.InsufficientPermissionException; public class CityInfo extends Command { public CityInfo() { this.alias = Arrays.asList("ci", "weather"); this.desc = "Search for info about a city!"; //"Search for info about the city the address is in!"; this.usage = "
"; for (String country : Locale.getISOCountries()) { Locale locale = new Locale("en", country); countryCodes.put(locale.getDisplayCountry(Locale.ENGLISH), locale.getCountry()); } this.examples = Arrays.asList("hong kong", "tokyo"); //"HK", "akihabara"); } HashMap countryCodes = new HashMap<>(); NumberFormat formatter = new DecimalFormat("#0.00"); //private final String flickrAPI = DiscordBot.tokens.getProperty("flickr"); @Override public CommandResult execute(TextChannel channel, User author, Message msg, String[] args) { if(args.length < 1) { return new CommandResult(CommandResultType.INVALIDARGS, "Please input a city name."); //or a address."); } else { channel.sendTyping().queue(); String sword = String.join(" ", args); try { /*JSONTokener georesult = null; InputStream geostream = null; String search = URLEncoder.encode(sword, "UTF-8"); URL geocode = null; try { geocode = new URL("https://maps.googleapis.com/maps/api/geocode/json?address=" + search + "&key=" + AudioPlayer.GAPI + "&language=en"); geostream = geocode.openStream(); } catch (IOException e) { e.printStackTrace(); } georesult = new JSONTokener(geostream); JSONObject geomain = new JSONObject(georesult); JSONArray resultList = geomain.getJSONArray("results"); if(resultList.isNull(0)) { channel.sendMessage("Unfortunately there is no results :cry:").queue(); return new CommandResult(CommandResultType.NORESULT); } JSONObject firstResult = resultList.getJSONObject(0); JSONObject loc = firstResult.getJSONObject("geometry").getJSONObject("location"); //String formattedAddr = firstResult.getString("formatted_address"); JSONArray addrComponents = firstResult.getJSONArray("address_components"); String formattedAddr = "", addr = firstResult.getString("formatted_address"); String countryShort = null; String region = null; String locality = null; String country = null; String colarea = null; boolean stop = false; for(int i = 0; i < addrComponents.length(); i++) { JSONObject component = addrComponents.getJSONObject(i); String compname = component.getString("long_name"); if(!stop) { if(i == addrComponents.length() - 1) { formattedAddr += compname; } else { formattedAddr += compname + ", "; } } List types = component.getJSONArray("types").toList(); if(types.contains("country")) { countryShort = component.getString("short_name"); country = compname; if(i == 0) { channel.sendMessage("You need to specify which part of the country you want to get the info from.").queue(); return new CommandResult(CommandResultType.FAILURE, "Address is a country"); } } else if(types.contains("continent")) { if(i == 0) { channel.sendMessage("You need to specify which part of the continent you want to get the info from.").queue(); return new CommandResult(CommandResultType.FAILURE, "Address is a continent"); } } else if(types.contains("postal_code")) { if(i == 0) { formattedAddr = addr; stop = true; } } else if(types.contains("administrative_area_level_1")) { region = compname; } else if(types.contains("locality")) { locality = compname; } else if(types.contains("colloquial_area")) { colarea = compname; } else if(types.contains("natural_feature") && addrComponents.length() == 1) { channel.sendMessage("Search civilized locations please :joy:").queue(); return new CommandResult(CommandResultType.FAILURE, "Address is natural"); } } if(region == null) { if(stop) { region = country; } else { if(locality.equals("Singapore")) { formattedAddr = addr; } region = colarea; if(locality != null) region = locality; } } double lat = loc.getDouble("lat"); double lng = loc.getDouble("lng"); JSONTokener timeresult = null; InputStream timestream = null; URL timezone = null; Timestamp timestamp = new Timestamp(System.currentTimeMillis()); long sec = Math.round(timestamp.getTime() / 1000.0); try { //can deprecate this since the new weather scrape has local time, but there wont be a name for the timezone anymore timezone = new URL("https://maps.googleapis.com/maps/api/timezone/json?location=" + lat + "," + lng + "×tamp=" + sec + "&key=" + AudioPlayer.GAPI + "&language=en"); timestream = timezone.openStream(); } catch (IOException e) { e.printStackTrace(); } timeresult = new JSONTokener(timestream); JSONObject timemain = new JSONObject(timeresult); String timeZoneName = timemain.getString("timeZoneName"); int rawOffset = timemain.getInt("rawOffset"); int dstOffset = timemain.getInt("dstOffset"); ZonedDateTime zone = ZonedDateTime.now(ZoneOffset.ofTotalSeconds(rawOffset + dstOffset)); int hours = (rawOffset + dstOffset) / 60 / 60;*/ boolean hasWeather = true; JSONObject info = null; String wQualifiedName = "", woeid = "", lng = "", lat = "", region = "", countryShort = ""; TimeZone timezone = null; /*try { URLConnection con = new URL("https://api.flickr.com/services/rest/?method=flickr.places.find&api_key=" + flickrAPI + "&query=" + URLEncoder.encode(sword, "UTF-8") + "&format=json&nojsoncallback=1").openConnection(); con.setRequestProperty("Accept-Language", "en"); JSONArray warray = new JSONObject(new JSONTokener(con.getInputStream())).getJSONObject("places").getJSONArray("place"); int index; for(index = 0; index < warray.length(); index++) { if(warray.getJSONObject(index).has("timezone")) { break; } } JSONObject wsearch = warray.getJSONObject(index); woeid = wsearch.getString("woeid"); //flickr api, using generated api key (will it expire?) //highest accuracy so far //wQualifiedName = wsearch.getString("_content"); //too short ArrayList pSplit = new ArrayList<>(Arrays.asList(URLDecoder.decode(wsearch.getString("place_url"), "UTF-8").substring(1).split("/"))); if(!pSplit.get(pSplit.size() - 1).equals(wsearch.getString("woe_name"))) { pSplit.add(wsearch.getString("woe_name")); } Collections.reverse(pSplit); wQualifiedName = String.join(", ", pSplit); timezone = TimeZone.getTimeZone(wsearch.getString("timezone")); lat = wsearch.getString("latitude"); lng = wsearch.getString("longitude"); String[] rSplit = wQualifiedName.split(", "); region = rSplit.length > 1 ? rSplit[rSplit.length - 2] : rSplit[0]; countryShort = countryCodes.get(rSplit[rSplit.length - 1].trim()); } catch(IOException e) { return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } catch (JSONException e) { //e.printStackTrace(); return new CommandResult(CommandResultType.NORESULT); }*/ //FLICKR DED try { URLConnection sCon = new URL("https://www.yahoo.com/news/_tdnews/api/resource/WeatherSearch;text=" + URLEncoder.encode(sword, "UTF-8") + "?returnMeta=true").openConnection(); sCon.addRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0"); JSONObject wsearch = new JSONObject(new JSONTokener(sCon.getInputStream())).getJSONArray("data").getJSONObject(0); woeid = String.valueOf(wsearch.getInt("woeid")); //yahoo scrape lat = String.valueOf(wsearch.getDouble("lat")); lng = String.valueOf(wsearch.getDouble("lon")); wQualifiedName = wsearch.getString("qualifiedName"); countryShort = countryCodes.get(wsearch.getString("country")); region = wsearch.getString("city"); timezone = TimeZone.getTimeZone(new JSONObject(new JSONTokener(new URL("https://api.internal.teleport.org/api/locations/" + lat + "," + lng + "/?embed=location:nearest-cities/location:nearest-city/city:timezone").openStream())).getJSONObject("_embedded").getJSONArray("location:nearest-cities").getJSONObject(0).getJSONObject("_embedded").getJSONObject("location:nearest-city").getJSONObject("_embedded").getJSONObject("city:timezone").getString("iana_name")); //can use metaweather, but not accurate enough //can also broaden the scope for yahoo scrape for it to work better //JSONObject wsearch = new JSONObject(new JSONTokener(new URL("https://api.flickr.com/services/rest/?method=flickr.places.findByLatLon&api_key=bdaafeafab62267931d920dda27a4f90&lat=" + lat + "&lon=" + lng + "&format=json&nojsoncallback=1").openStream())).getJSONObject("places").getJSONArray("place").getJSONObject(0); //gonna use flickr find instead URLConnection iCon = new URL("https://www.yahoo.com/news/_tdnews/api/resource/WeatherService;woeids=[" + woeid + "]?lang=en-US&returnMeta=true").openConnection(); iCon.addRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0"); info = new JSONObject(new JSONTokener(iCon.getInputStream())).getJSONObject("data").getJSONArray("weathers").getJSONObject(0); } catch(IOException e) { return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } catch(JSONException e) { e.printStackTrace(); return new CommandResult(CommandResultType.NORESULT); //hasWeather = false; } /*String ftimezone = timeZoneName + " (UTC" + (Math.signum(hours) == 1 || Math.signum(hours) == 0 ? "+" + hours : hours) + ")"; if(ftimezone.length() < 34) { ftimezone += String.join("", Collections.nCopies(34 - ftimezone.length(), " ")); }*/ - StringBuffer unibuff = new StringBuffer(); - if(countryShort != null) { - char[] ch = countryShort.toLowerCase().toCharArray(); - for(char c : ch) { - int temp = (int)c; - int temp_integer = 96; //for lower case - if(temp<=122 & temp>=97) unibuff.append(Character.toChars(127461 + (temp-temp_integer))); - } - } else { - unibuff.append("N/A"); - } Date date = new Date(); //System.out.println(info); EmbedBuilder embedmsg = new EmbedBuilder(); embedmsg.setAuthor("Info for " + wQualifiedName, null, null); embedmsg.setColor(new Color(100, 0, 255)); //embedmsg.setFooter("Weather info last updated: " + info.getString("lastBuildDate") , null); - embedmsg.addField("Country", unibuff.toString(), true); + embedmsg.addField("Country", MiscUtils.countryNameToUnicode(countryShort), true); embedmsg.addField("Region", region, true); embedmsg.addField("Current time", OffsetDateTime.now(timezone.toZoneId()).format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ssa").withLocale(Locale.ENGLISH)).trim(), true); long hours = (timezone.getOffset(date.getTime())/1000/60/60); embedmsg.addField("Timezone" , timezone.getDisplayName(timezone.inDaylightTime(date), TimeZone.LONG, Locale.ENGLISH) + " (UTC" + (Math.signum(hours) == 1 || Math.signum(hours) == 0 ? "+" + hours : hours) + ")" + "\u1160", true); //FLICKR DED String footer = "Weather info not available"; if (hasWeather) { //use another api if weather info not available? JSONObject obs = info.getJSONObject("observation"); JSONObject temp = obs.getJSONObject("temperature"); JSONObject forecast = info.getJSONObject("forecasts").getJSONArray("daily").getJSONObject(0); if(temp.has("now")) { embedmsg.addField("Temperature", fToC(temp.getInt("now")) + "°C (↑" + fToC(temp.getInt("high")) + "°C | ↓" + fToC(temp.getInt("low")) + "°C)", true); embedmsg.addField("Humidity", obs.getInt("humidity") + "% (Chance of rain: " + forecast.getInt("precipitationProbability") + "%)", true); embedmsg.addField("Visibility", miToKm(obs.getDouble("visibility")) + "km (" + obs.getString("conditionDescription") + ")", true); embedmsg.addField("Atmospheric pressure", formatter.format(obs.getDouble("barometricPressure") / 0.029530) + "millibars", true); embedmsg.addField("Wind speed", miToKm(obs.getDouble("windSpeed")) + "km/h", true); embedmsg.addField("Wind direction", obs.getInt("windDirection") + "° (" + obs.getString("windDirectionCode") + ")", true); embedmsg.addField("Feels Like", fToC(temp.getInt("feelsLike")) + "°C", true); embedmsg.addField("UV index", obs.getInt("uvIndex") + " (" + obs.getString("uvDescription") + ")", true); embedmsg.addField("Sunrise", MiscUtils.convertMillis(info.getJSONObject("sunAndMoon").getLong("sunrise") * 1000).substring(0, 5), true); embedmsg.addField("Sunset", MiscUtils.convertMillis(info.getJSONObject("sunAndMoon").getLong("sunset") * 1000).substring(0, 5), true); String imgUrl = info.getJSONArray("photos").getJSONObject(0).getJSONArray("resolutions").getJSONObject(0).getString("url"); //seems to have dead urls, how fix embedmsg.setThumbnail(imgUrl.split(":\\/\\/").length > 2 ? "https://" + imgUrl.split(":\\/\\/")[2] : imgUrl); footer = "Weather info last updated: " + OffsetDateTime.parse(obs.getJSONObject("observationTime").getString("timestamp")).format(DateTimeFormatter.RFC_1123_DATE_TIME) .replace("GMT", timezone.getDisplayName(timezone.inDaylightTime(date), TimeZone.SHORT, Locale.ENGLISH)); //+ " | " + wQualifiedName; //add weather provider to footer? } } embedmsg.addField("Latitude", lat, true); embedmsg.addField("Longitude", lng, true); embedmsg.setFooter(footer, null); try { MessageEmbed fmsg = embedmsg.build(); channel.sendMessage(fmsg).queue(); } catch(InsufficientPermissionException e2) { return new CommandResult(CommandResultType.FAILURE, "Unfortunately, the bot is missing the permission `MESSAGE_EMBED_LINKS` which is required for this command to work."); } return new CommandResult(CommandResultType.SUCCESS); } catch (Exception e) { return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } } } private String fToC(int f) { //fucking no metric ree return formatter.format((f-32)*5.0/9); } private String miToKm(double mile) { return formatter.format(mile*1.609344); } } diff --git a/src/me/despawningbone/discordbot/utils/MiscUtils.java b/src/me/despawningbone/discordbot/utils/MiscUtils.java index c05fd18..9edba93 100644 --- a/src/me/despawningbone/discordbot/utils/MiscUtils.java +++ b/src/me/despawningbone/discordbot/utils/MiscUtils.java @@ -1,225 +1,238 @@ package me.despawningbone.discordbot.utils; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import me.despawningbone.discordbot.DiscordBot; import me.despawningbone.discordbot.command.Command; import me.despawningbone.discordbot.command.Command.BotUserLevel; import me.despawningbone.discordbot.command.admin.Settings; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.TextChannel; public class MiscUtils { public static String convertMillis(long m) { String c = null; if (TimeUnit.MILLISECONDS.toHours(m) == 0) { c = String.format("%02d:%02d", TimeUnit.MILLISECONDS.toMinutes(m) % TimeUnit.HOURS.toMinutes(1), TimeUnit.MILLISECONDS.toSeconds(m) % TimeUnit.MINUTES.toSeconds(1)); } else { c = String.format("%02d:%02d:%02d", TimeUnit.MILLISECONDS.toHours(m), TimeUnit.MILLISECONDS.toMinutes(m) % TimeUnit.HOURS.toMinutes(1), TimeUnit.MILLISECONDS.toSeconds(m) % TimeUnit.MINUTES.toSeconds(1)); } return c; } public static String appendZeroToMinute(String time) { if (time.split(":")[1].split(" ")[0].length() != 2) { time = new StringBuilder(time).insert(time.indexOf(":") + 1, "0").toString(); } return time; } public static String ordinal(int i) { String suffix; int lastTwoDigits = i % 100; int lastDigit = lastTwoDigits % 10; switch (lastDigit) { case 1: suffix = "st"; break; case 2: suffix = "nd"; break; case 3: suffix = "rd"; break; default: suffix = "th"; break; } if (11 <= lastTwoDigits && lastTwoDigits <= 13) { suffix = "th"; } return i + suffix; } + //can use delete().queueAfter() instead public static void delayDeleteMessage(Message msg, long millis) { new Thread() { public void run() { try { Thread.sleep(millis); msg.delete().queue(); } catch (InterruptedException v) { v.printStackTrace(); ; } } }.start(); } public static Long nameToBase(String sbase) throws IllegalArgumentException { long base = 10; if (sbase.equals("hex") || sbase.equals("hexadecimal")) { base = 16; } else if (sbase.equals("oct") || sbase.equals("octal")) { base = 8; } else if (sbase.equals("bin") || sbase.equals("binary")) { base = 2; } else if (sbase.equals("dec") || sbase.equals("decimal") || sbase.equals("denary")) { base = 10; } else if (sbase.equals("duodecimal") || sbase.equals("dozenal")) { base = 12; } else if (sbase.equals("vigesimal")) { base = 20; } else { throw new IllegalArgumentException("Please enter a valid base."); } return base; } public static String longToSubscript(long i) { StringBuilder sb = new StringBuilder(); for (char ch : String.valueOf(i).toCharArray()) { sb.append((char) ('\u2080' + (ch - '0'))); } return sb.toString(); } + + public static String countryNameToUnicode(String shorthand) { + StringBuffer unibuff = new StringBuffer(); + char[] ch = shorthand.toLowerCase().toCharArray(); + for (char c : ch) { + int temp = (int) c; + int temp_integer = 96; //for lower case + if (temp <= 122 & temp >= 97) + unibuff.append(Character.toChars(127461 + (temp - temp_integer))); + } + return unibuff.toString(); + } public static long calcPermOverrides(long base, long override) { long oDeny = (int) (override >> 32); long oAllow = (int) override; base &= ~oDeny; base |= oAllow; return base; } public static String getMissingPerms(long rawPerms, int botUserLevel, Member member, TextChannel channel) { if(hasDisabled(rawPerms)) return "DISABLED"; //dont return but append? EnumSet perms = Permission.getPermissions(rawPerms); boolean metBotUserLevel = Math.abs(botUserLevel) > 1 ? DiscordBot.OwnerID.equals(member.getUser().getId()) : Math.abs(botUserLevel) > 0 ? DiscordBot.ModID.contains(member.getUser().getId()) : true; boolean metPerms = member.hasPermission(channel, perms); if(rawPerms == 0 && botUserLevel < 0) botUserLevel = Math.abs(botUserLevel); //fall back in case misconfiguration if(botUserLevel > 0 && !metBotUserLevel) return BotUserLevel.values()[botUserLevel].name(); //level > 0 = the command is specifically for BotMod+ else if(botUserLevel == 0 && !metPerms) return perms.stream().map(p -> p.name()).collect(Collectors.joining(", ")); //not specified for BotMod+, check perms else if(botUserLevel < 0 && !(metBotUserLevel || metPerms)) return BotUserLevel.values()[Math.abs(botUserLevel)].name() + " OR " + perms.stream().map(p -> p.name()).collect(Collectors.joining(", ")); //since BotMod+ overrides perms, either they have perm or BotMod+ suffices else return null; } public static String getRequiredPerms(long rawPerms, int botUserLevel) { if(hasDisabled(rawPerms)) return "DISABLED"; EnumSet perms = Permission.getPermissions(rawPerms); if(botUserLevel > 0) return BotUserLevel.values()[botUserLevel].name(); //level > 0 = the command is specifically for BotMod+ else if(botUserLevel == 0 && !perms.isEmpty()) return perms.stream().map(p -> p.name()).collect(Collectors.joining(", ")); //not specified for BotMod+, check perms else if(botUserLevel < 0) return BotUserLevel.values()[Math.abs(botUserLevel)].name() + (!perms.isEmpty() ? " OR " + perms.stream().map(p -> p.name()).collect(Collectors.joining(", ")) : ""); //since BotMod+ overrides perms, either they have perm or BotMod+ suffices else return null; } public static boolean hasDisabled(long rawPerms) { return (rawPerms & Settings.DISABLED) == Settings.DISABLED; } //TODO move these to a new SQLUtils class? public static String getPrefix(Statement s, String guildId) throws SQLException { ResultSet gRs = s.executeQuery("SELECT prefix FROM settings WHERE id = " + guildId + ";"); String prefix = "!desp "; if(gRs.next()) prefix = gRs.getString(1); gRs.close(); return prefix; } public static long getActivePerms(Statement s, TextChannel channel, Command cmd) throws SQLException { long perm = 0; ArrayList defs = new ArrayList<>(); ArrayList components = new ArrayList<>(); int c = 2; components.add(cmd.getName()); defs.add(Permission.getRaw(cmd.getDefaultPerms())); while(cmd.getParent() != null) { cmd = cmd.getParent(); components.add(0, cmd.getName()); defs.add(Permission.getRaw(cmd.getDefaultPerms())); c++; } String nodes = "\"" + components.get(0) + "\""; for(int i = 2; i <= components.size(); i++) { nodes += ", \"" + String.join(".", components.subList(0, i)) + "\""; } //System.out.println(nodes); ResultSet pRs = s.executeQuery("SELECT _GLOBAL_, " + nodes + " FROM perms_" + cmd.getCategory().toLowerCase() + " WHERE id = " + channel.getGuild().getId() + ";"); if(pRs.next()) { //parse and calc overrides from db settings for(int i = 1; i <= c; i++) { //System.out.println(pRs.getString(i)); perm = getPermOverrides(pRs.getString(i), perm, channel.getId()); //cat(guild) < cmd(guild < channel) < subcmd(guild < channel) ... } } else { //directly calc overrides for defaults Collections.reverse(defs); for(long def : defs) perm = calcPermOverrides(perm, def); } pRs.close(); return perm; } private static Long getPermOverrides(String parse, long base, String cId) { long gPerms = 0, cPerms = 0; if(parse != null) { String g = parse.substring(0, parse.indexOf("\n")); gPerms = Long.parseLong(g); //gPerms should always be present if(parse.indexOf(cId + ":") != -1) { String cut = parse.substring(parse.indexOf(cId + ":") + cId.length() + 1); cPerms = Long.parseLong(cut.substring(0, cut.indexOf("\n"))); } } return calcPermOverrides(calcPermOverrides(base, gPerms), cPerms); } public static void copy(final InputStream in, final OutputStream out) { byte[] buffer = new byte[1024]; int count; try { while ((count = in.read(buffer)) != -1) { out.write(buffer, 0, count); } } catch (IOException e) { e.printStackTrace(); } // Flush out stream, to write any remaining buffered data try { out.flush(); } catch (IOException e) { e.printStackTrace(); } } }