├── locales ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── autorun ├── src └── corebot │ ├── CoreBot.java │ ├── Prefs.java │ ├── ServerBridge.java │ ├── Info.java │ ├── Reports.java │ ├── VideoScanner.java │ ├── StreamScanner.java │ ├── Net.java │ ├── ContentHandler.java │ └── Messages.java ├── gradlew.bat └── gradlew /locales: -------------------------------------------------------------------------------- 1 | en -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anuken/CoreBot/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | /bin/ 3 | /modules/ 4 | /.settings/ 5 | /.gradle/ 6 | /build/ 7 | .classpath 8 | .project 9 | /.idea/ 10 | /out/ 11 | CoreBot.iml 12 | prefs.properties 13 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Sep 21 13:08:26 CEST 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip 7 | -------------------------------------------------------------------------------- /autorun: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | while true; do 4 | #auto-restart until ctrl-c or exit 0 5 | 6 | git pull 7 | ./gradlew -Dorg.gradle.daemon=false dist 8 | java -jar build/libs/CoreBot.jar 9 | 10 | excode=$? 11 | if [ $excode -eq 0 ] || [ $excode -eq 130 ]; then 12 | exit 0 13 | fi 14 | done -------------------------------------------------------------------------------- /src/corebot/CoreBot.java: -------------------------------------------------------------------------------- 1 | package corebot; 2 | 3 | import java.awt.*; 4 | import java.io.*; 5 | 6 | public class CoreBot{ 7 | public static final String releasesURL = "https://api.github.com/repos/Anuken/Mindustry/releases"; 8 | public static final File prefsFile = new File("prefs.properties"); 9 | 10 | public static final Color normalColor = Color.decode("#FAB462"); 11 | public static final Color errorColor = Color.decode("#ff3838"); 12 | 13 | public static final long messageDeleteTime = 20000; //milliseconds 14 | public static final int warnExpireDays = 15; 15 | 16 | public static ContentHandler contentHandler = new ContentHandler(); 17 | public static Messages messages = new Messages(); 18 | public static Net net = new Net(); 19 | public static Prefs prefs = new Prefs(prefsFile); 20 | public static StreamScanner streams = new StreamScanner(); 21 | public static VideoScanner videos = new VideoScanner(); 22 | //public static Reports reports = new Reports(); 23 | 24 | public static void main(String[] args){ 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/corebot/Prefs.java: -------------------------------------------------------------------------------- 1 | package corebot; 2 | 3 | import arc.struct.*; 4 | import arc.util.serialization.*; 5 | 6 | import java.io.*; 7 | import java.util.*; 8 | 9 | //TODO horrible 10 | public class Prefs{ 11 | private Properties prop; 12 | private File file; 13 | private Json json = new Json(); 14 | 15 | public Prefs(File file){ 16 | this.file = file; 17 | try{ 18 | if(!file.exists()) file.createNewFile(); 19 | prop = new Properties(); 20 | prop.load(new FileInputStream(file)); 21 | }catch(IOException e){ 22 | throw new RuntimeException(e); 23 | } 24 | } 25 | 26 | @SuppressWarnings("unchecked") 27 | public Seq getArray(String property){ 28 | String value = prop.getProperty(property, "[]"); 29 | return json.fromJson(Seq.class, value); 30 | } 31 | 32 | public void putArray(String property, Seq arr){ 33 | prop.put(property, json.toJson(arr)); 34 | save(); 35 | } 36 | 37 | public String get(String property, String def){ 38 | return prop.getProperty(property, def); 39 | } 40 | 41 | public int getInt(String property, int def){ 42 | return Integer.parseInt(prop.getProperty(property, def + "")); 43 | } 44 | 45 | public void put(String property, String value){ 46 | prop.put(property, value); 47 | save(); 48 | } 49 | 50 | public void save(){ 51 | try{ 52 | prop.store(new FileOutputStream(file), null); 53 | }catch(IOException e){ 54 | throw new RuntimeException(e); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/corebot/ServerBridge.java: -------------------------------------------------------------------------------- 1 | package corebot; 2 | 3 | import arc.func.*; 4 | import arc.util.*; 5 | 6 | import java.io.*; 7 | import java.net.*; 8 | import java.util.concurrent.*; 9 | 10 | public class ServerBridge{ 11 | private static final int port = 6859; 12 | private static final int waitPeriod = 5000; 13 | 14 | private boolean connected; 15 | private ArrayBlockingQueue queue = new ArrayBlockingQueue<>(32); 16 | 17 | public void connect(Cons inputHandler){ 18 | Thread thread = new Thread(() -> { 19 | while(true){ 20 | try(Socket sock = new Socket()){ 21 | sock.connect(new InetSocketAddress("localhost", port)); 22 | Log.info("Connected to server."); 23 | connected = true; 24 | PrintWriter out = new PrintWriter(sock.getOutputStream(), true); 25 | BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream())); 26 | 27 | Thread readerThread = new Thread(() -> { 28 | try{ 29 | String line; 30 | while((line = in.readLine()) != null){ 31 | inputHandler.get(line); 32 | } 33 | }catch(Exception e){ 34 | e.printStackTrace(); 35 | } 36 | }); 37 | readerThread.setDaemon(true); 38 | readerThread.start(); 39 | 40 | String send; 41 | while(true){ 42 | send = queue.take(); 43 | Log.info("Sending command: @", send); 44 | out.println(send); 45 | } 46 | }catch(Exception ignored){} 47 | connected = false; 48 | 49 | try{ 50 | Thread.sleep(waitPeriod); 51 | }catch(InterruptedException ignored){} 52 | } 53 | }); 54 | thread.setDaemon(true); 55 | thread.start(); 56 | } 57 | 58 | public void send(String s){ 59 | if(!connected){ 60 | return; 61 | } 62 | 63 | try{ 64 | queue.put(s); 65 | }catch(InterruptedException e){ 66 | throw new RuntimeException(e); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/corebot/Info.java: -------------------------------------------------------------------------------- 1 | package corebot; 2 | 3 | public enum Info{ 4 | links("Relevant Links", 5 | """ 6 | [Source Code on Github](https://github.com/Anuken/Mindustry/) 7 | [Suggestion Form](https://github.com/Anuken/Mindustry-Suggestions/issues/new/choose) 8 | [Bug Report Form](https://github.com/Anuken/Mindustry/issues/new/choose) 9 | [Trello](https://trello.com/b/aE2tcUwF) 10 | [Steam Version](https://store.steampowered.com/app/1127400/Mindustry/) 11 | [Android APKs and itch.io version](https://anuke.itch.io/mindustry) 12 | [iOS version](https://itunes.apple.com/us/app/mindustry/id1385258906?mt=8&ign-mpt=uo%3D8) 13 | [Google Play Listing](https://play.google.com/store/apps/details?id=mindustry) 14 | [TestFlight Link](https://testflight.apple.com/join/79Azm1hZ) 15 | [Mindustry Subreddit](https://www.reddit.com/r/mindustry) 16 | [Unofficial Matrix Space](https://matrix.to/#/#mindustry-space:matrix.org) 17 | """), 18 | beta("iOS Beta", 19 | """ 20 | To join the iOS beta, click this [TestFlight Link](https://testflight.apple.com/join/79Azm1hZ), then install the Apple TestFlight app to play Mindustry. 21 | 22 | There is currently no beta available on Google Play. Download the itch.io version. 23 | """), 24 | rules("Rules", 25 | """ 26 | **1.** Don't be rude. This should be obvious. No racism/sexism/etc. 27 | 28 | **2.** No spamming or advertising. 29 | 30 | **3.** No NSFW, sensitive or political content. This includes NSFW conversations. Take it elsewhere. 31 | 32 | **4.** Keep content to the appropriate text channels. 33 | 34 | **5.** Please do not post invite links to this server in public places without context. 35 | 36 | **6.** Do not ask for roles. If I need a moderator and I think you fit the position, I will ask you personally. 37 | 38 | **7.** Do not impersonate other members or intentionally edit your messages to mislead others. 39 | 40 | **8.** Do not cross-post the same message to multiple channels. 41 | 42 | **9.** Do not advertise in DMs or send unsolicited messages to other users. Report violations of this sort to moderators immediately. 43 | 44 | **10.** Ban evasion and alternate accounts are not allowed. Alts posting any content on the server will be banned immediately. 45 | 46 | **11.** Please do not PM me (Anuke) unless you are reporting an exploit, a significant problem with the Discord server, or need to discuss something relating to Github PRs. If you need help with the game, *ask in #help. or #mindustry*. Do *not* PM me suggestions - use the suggestions form. 47 | 48 | *If I don't like your behavior, you're out. Obey the spirit, not the word.* 49 | """); 50 | public final String text; 51 | public final String title; 52 | 53 | Info(String title, String text){ 54 | this.text = text; 55 | this.title = title; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/corebot/Reports.java: -------------------------------------------------------------------------------- 1 | package corebot; 2 | 3 | import arc.util.*; 4 | import arc.util.serialization.*; 5 | import com.sun.net.httpserver.*; 6 | 7 | import java.io.*; 8 | import java.net.*; 9 | 10 | public class Reports{ 11 | 12 | public Reports(){ 13 | try{ 14 | HttpServer server = HttpServer.create(new InetSocketAddress(6748), 0); 15 | server.createContext("/report", t -> { 16 | //who the HECK is this and why are they sending me the same crash report over and over 17 | if(t.getRemoteAddress().getAddress().getHostAddress().equals("221.229.196.229")) return; 18 | 19 | byte[] bytes = new byte[t.getRequestBody().available()]; 20 | new DataInputStream(t.getRequestBody()).readFully(bytes); 21 | 22 | String message = new String(bytes); 23 | Json json = new Json(); 24 | JsonValue value = json.fromJson(null, message); 25 | String build = value.getInt("build") + (value.getInt("revision") == 0 ? "" : "." + value.getInt("revision")); 26 | 27 | //custom builds and uninitialized builds (0) are skipped. 28 | if(build.equals(CoreBot.net.getLastBuild()) || String.valueOf(value.getInt("build")).equals(CoreBot.net.getLastBuild())){ 29 | JsonValue value1 = value; 30 | 31 | StringBuilder builder = new StringBuilder(); 32 | value1 = value1.child; 33 | while(value1 != null){ 34 | builder.append("**"); 35 | builder.append(value1.name); 36 | builder.append("**"); 37 | builder.append(": "); 38 | if(value1.name.equals("trace")){ 39 | builder.append("```xl\n"); //xl formatting looks nice 40 | builder.append(value1.asString().replace("\\n", "\n").replace("\t", " ")); 41 | builder.append("```"); 42 | }else{ 43 | builder.append(value1.asString()); 44 | } 45 | builder.append("\n"); 46 | value1 = value1.next; 47 | } 48 | CoreBot.messages.crashReportChannel.sendMessage(builder.toString()).queue(); 49 | }else{ 50 | Log.info("Rejecting report with invalid build: @. Current latest build is @.", build, CoreBot.net.getLastBuild()); 51 | } 52 | 53 | Log.info("Recieved crash report."); 54 | 55 | t.sendResponseHeaders(200, 0); 56 | }); 57 | server.setExecutor(null); 58 | server.start(); 59 | Log.info("Crash reporting server up."); 60 | }catch(Exception e){ 61 | Log.err("Error parsing report", e); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/corebot/VideoScanner.java: -------------------------------------------------------------------------------- 1 | package corebot; 2 | 3 | import arc.files.*; 4 | import arc.func.*; 5 | import arc.struct.*; 6 | import arc.util.*; 7 | import arc.util.serialization.*; 8 | import net.dv8tion.jda.api.*; 9 | 10 | import java.time.format.*; 11 | import java.util.Timer; 12 | import java.util.*; 13 | 14 | public class VideoScanner{ 15 | private static final String api = "https://youtube.googleapis.com/youtube/v3/"; 16 | private static final long updatePeriod = 1000 * 60 * 30; 17 | private static final String minChannelId = "UCR-XYZA9YQVhCIdkssqVNuQ", popularVideosPlaylist = "PLO5a8SnRwlbQ8zKnz_upUGlxQv9qF2Mxo"; 18 | private static final String key = OS.env("GOOGLE_API_KEY"); 19 | 20 | private final Fi seenfi = new Fi("videos.txt"); 21 | private final ObjectSet seen; 22 | 23 | public VideoScanner(){ 24 | seen = Seq.with(seenfi.exists() ? seenfi.readString().split("\n") : new String[0]).asSet(); 25 | 26 | new Timer().scheduleAtFixedRate(new TimerTask(){ 27 | @Override 28 | public void run(){ 29 | try{ 30 | query("playlistItems", 31 | StringMap.of( 32 | "part", "snippet", 33 | "playlistId", popularVideosPlaylist, 34 | "maxResults", "25" 35 | ), result -> { 36 | var items = result.get("items"); 37 | for(var video : items.asArray()){ 38 | String id = video.get("snippet").get("resourceId").getString("videoId"); 39 | if(seen.add(id)){ 40 | newVideo(video.get("snippet")); 41 | } 42 | } 43 | }); 44 | 45 | seenfi.writeString(seen.toSeq().toString("\n")); 46 | }catch(Exception e){ 47 | Log.err(e); 48 | } 49 | } 50 | }, 1000, updatePeriod); 51 | } 52 | 53 | void query(String url,StringMap params, Cons cons){ 54 | params.put("key", key); 55 | 56 | Http.get(api + url + "?" + params.keys().toSeq().map(entry -> Strings.encode(entry) + "=" + Strings.encode(params.get(entry))).toString("&")) 57 | .timeout(10000) 58 | .header("Accept", "application/json") 59 | .block(response -> cons.get(Jval.read(response.getResultAsString()))); 60 | } 61 | 62 | void newVideo(Jval video){ 63 | var id = video.getString("videoOwnerChannelId"); 64 | Jval[] user = {null}; 65 | 66 | query("channels", StringMap.of("part", "snippet", "id", id), result -> { 67 | var items = result.get("items").asArray(); 68 | if(items.any()){ 69 | user[0] = items.get(0); 70 | } 71 | }); 72 | 73 | var videoUrl = "https://youtube.com/watch/?v=" + video.get("resourceId").get("videoId"); 74 | 75 | if(user[0] != null){ 76 | var avatar = user[0].get("snippet").get("thumbnails").get("default").getString("url"); 77 | var desc = video.getString("description").replace("\\n", "\n"); 78 | 79 | if(desc.length() > 200) desc = desc.substring(0, 200) + "..."; 80 | 81 | CoreBot.messages.videosChannel 82 | .sendMessageEmbeds( 83 | new EmbedBuilder() 84 | .setTitle(video.getString("title"), videoUrl) 85 | .setColor(CoreBot.normalColor) 86 | .setAuthor(video.getString("videoOwnerChannelTitle"), videoUrl, avatar) 87 | .setImage(video.get("thumbnails").get("high").getString("url")) 88 | .setTimestamp(DateTimeFormatter.ISO_INSTANT.parse(video.getString("publishedAt"))) 89 | .setFooter(desc) 90 | .build()).queue(); 91 | }else{ 92 | Log.warn("unable to get user with ID @", id); 93 | } 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/corebot/StreamScanner.java: -------------------------------------------------------------------------------- 1 | package corebot; 2 | 3 | import arc.files.*; 4 | import arc.struct.*; 5 | import arc.util.*; 6 | import arc.util.serialization.*; 7 | import net.dv8tion.jda.api.*; 8 | 9 | import java.time.*; 10 | import java.time.format.*; 11 | import java.util.Timer; 12 | import java.util.*; 13 | 14 | public class StreamScanner{ 15 | private static final long updatePeriod = 1000 * 60 * 3, seenCleanPeriod = 1000 * 60 * 60 * 24 * 2, startDelayMins = 15; 16 | private static final String minId = "502103", testId = "31376", clientId = "worwycsp7vvr6049q2f88l1cj1jj1i", clientSecret = OS.env("TWITCH_SECRET"); 17 | 18 | private ObjectSet seenIds; 19 | private String token; 20 | 21 | public StreamScanner(){ 22 | //clean up old file 23 | Fi.get("seen_" + (Time.millis() / seenCleanPeriod - 1) + ".txt").delete(); 24 | 25 | seenIds = Seq.with(seen().exists() ? seen().readString().split("\n") : new String[0]).asSet(); 26 | 27 | //periodically re-authorize 28 | new Timer().scheduleAtFixedRate(new TimerTask(){ 29 | @Override 30 | public void run(){ 31 | Http.post("https://id.twitch.tv/oauth2/token?client_id=" + clientId + "&client_secret=" + clientSecret + "&grant_type=client_credentials").submit(result -> { 32 | token = Jval.read(result.getResultAsString()).getString("access_token"); 33 | }); 34 | } 35 | }, 0, 1000 * 60 * 60); //once an hour 36 | 37 | //periodically refresh (with delay) 38 | new Timer().scheduleAtFixedRate(new TimerTask(){ 39 | @Override 40 | public void run(){ 41 | if(token == null) return; 42 | 43 | try{ 44 | var list = request("https://api.twitch.tv/helix/streams?game_id=" + minId); 45 | 46 | for(var stream : list.get("data").asArray()){ 47 | var instant = Instant.from(DateTimeFormatter.ISO_INSTANT.parse(stream.getString("started_at"))); 48 | //only display streams that started a few minutes ago, so the thumbnail is correct 49 | if(!Duration.between(instant, Instant.now()).minus(Duration.ofMinutes(startDelayMins)).isNegative() && 50 | seenIds.add(stream.getString("id"))){ 51 | newStream(stream); 52 | } 53 | } 54 | 55 | seen().writeString(seenIds.toSeq().toString("\n")); 56 | }catch(Exception e){ 57 | e.printStackTrace(); 58 | } 59 | } 60 | }, 5000, updatePeriod); 61 | } 62 | 63 | void newStream(Jval stream){ 64 | Jval users = request("https://api.twitch.tv/helix/users?id=" + stream.getString("user_id")).get("data"); 65 | 66 | if(users.asArray().size > 0){ 67 | var avatar = users.asArray().first().getString("profile_image_url"); 68 | 69 | //comedy 70 | if(stream.getString("title").contains("18")) return; 71 | 72 | CoreBot.messages.streamsChannel 73 | .sendMessageEmbeds( 74 | new EmbedBuilder() 75 | .setTitle(stream.getString("title"), "https://twitch.tv/" + stream.getString("user_login")) 76 | .setColor(CoreBot.normalColor) 77 | .setAuthor(stream.getString("user_name"), "https://twitch.tv/" + stream.getString("user_login"), avatar) 78 | .setImage(stream.getString("thumbnail_url").replace("{width}", "390").replace("{height}", "220")) 79 | .setTimestamp(DateTimeFormatter.ISO_INSTANT.parse(stream.getString("started_at"))) 80 | .build()).queue(); 81 | } 82 | } 83 | 84 | Jval request(String url){ 85 | Jval[] val = {null}; 86 | 87 | Http.get(url) 88 | .header("Client-Id", clientId) 89 | .header("Authorization", "Bearer " + token) 90 | .error(e -> { throw new RuntimeException(e); }) 91 | .block(res -> val[0] = Jval.read(res.getResultAsString())); 92 | 93 | return val[0]; 94 | } 95 | 96 | Fi seen(){ 97 | return Fi.get("seen_" + (Time.millis() / seenCleanPeriod) + ".txt"); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/corebot/Net.java: -------------------------------------------------------------------------------- 1 | package corebot; 2 | 3 | import arc.struct.*; 4 | import arc.util.*; 5 | import arc.util.io.*; 6 | import arc.util.serialization.*; 7 | import mindustry.net.*; 8 | import net.dv8tion.jda.api.*; 9 | 10 | import java.io.*; 11 | import java.net.*; 12 | import java.nio.*; 13 | import java.util.Timer; 14 | import java.util.*; 15 | import java.util.concurrent.*; 16 | import java.util.function.*; 17 | 18 | import static corebot.CoreBot.*; 19 | 20 | public class Net{ 21 | 22 | public Net(){ 23 | Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> getChangelog(list -> { 24 | try{ 25 | VersionInfo latest = list.first(); 26 | 27 | String lastVersion = getLastBuild(); 28 | 29 | if(parseVersionFloat(latest.build) > parseVersionFloat(lastVersion)){ 30 | Log.info("Posting update!"); 31 | 32 | //don't post revisions 33 | String text = latest.description; 34 | int maxLength = 2000; 35 | while(true){ 36 | String current = text.substring(0, Math.min(maxLength, text.length())); 37 | CoreBot.messages.announcementsChannel 38 | .sendMessageEmbeds(new EmbedBuilder() 39 | .setColor(normalColor).setTitle(latest.name) 40 | .setDescription(current).build()).queue(); 41 | 42 | if(text.length() < maxLength){ 43 | break; 44 | } 45 | 46 | text = text.substring(maxLength); 47 | } 48 | CoreBot.prefs.put("lastBuild", latest.build); 49 | } 50 | }catch(Throwable e){ 51 | e.printStackTrace(); 52 | } 53 | }, e -> {}), 60, 240, TimeUnit.SECONDS); 54 | } 55 | 56 | public float parseVersionFloat(String str){ 57 | if(str.startsWith("v")){ 58 | str = str.substring(1); 59 | } 60 | 61 | if(str.contains(".")){ 62 | var split = str.split("\\."); 63 | return Integer.parseInt(split[0]) + Integer.parseInt(split[1]) / 10f; 64 | }else{ 65 | return Integer.parseInt(str); 66 | } 67 | } 68 | 69 | public String getLastBuild(){ 70 | return CoreBot.prefs.get("lastBuild", "127.1"); 71 | } 72 | 73 | public InputStream download(String url){ 74 | try{ 75 | HttpURLConnection connection = (HttpURLConnection)new URL(url).openConnection(); 76 | connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36"); 77 | return connection.getInputStream(); 78 | }catch(Exception e){ 79 | throw new RuntimeException(e); 80 | } 81 | } 82 | 83 | public void pingServer(String ip, Consumer listener){ 84 | run(0, () -> { 85 | try{ 86 | String resultIP = ip; 87 | int port = 6567; 88 | if(ip.contains(":") && Strings.canParsePositiveInt(ip.split(":")[1])){ 89 | resultIP = ip.split(":")[0]; 90 | port = Strings.parseInt(ip.split(":")[1]); 91 | } 92 | 93 | DatagramSocket socket = new DatagramSocket(); 94 | socket.send(new DatagramPacket(new byte[]{-2, 1}, 2, InetAddress.getByName(resultIP), port)); 95 | 96 | socket.setSoTimeout(2000); 97 | 98 | DatagramPacket packet = new DatagramPacket(new byte[256], 256); 99 | 100 | long start = System.currentTimeMillis(); 101 | socket.receive(packet); 102 | 103 | ByteBuffer buffer = ByteBuffer.wrap(packet.getData()); 104 | listener.accept(readServerData(buffer, ip, (int)(System.currentTimeMillis() - start))); 105 | socket.disconnect(); 106 | }catch(Exception e){ 107 | listener.accept(new Host(0, null, ip, null, 0, 0, 0, null, null, 0, null, null)); 108 | } 109 | }); 110 | } 111 | 112 | public void getChangelog(Consumer> success, Consumer fail){ 113 | try{ 114 | URL url = new URL(CoreBot.releasesURL); 115 | URLConnection con = url.openConnection(); 116 | InputStream in = con.getInputStream(); 117 | String encoding = con.getContentEncoding(); 118 | encoding = encoding == null ? "UTF-8" : encoding; 119 | String body = Streams.copyString(in, 1000, encoding); 120 | 121 | Json j = new Json(); 122 | Seq list = j.fromJson(null, body); 123 | Seq out = new Seq<>(); 124 | for(JsonValue value : list){ 125 | String name = value.getString("name"); 126 | String description = value.getString("body").replace("\r", ""); 127 | int id = value.getInt("id"); 128 | String build = value.getString("tag_name").substring(1); 129 | out.add(new VersionInfo(name, description, id, build)); 130 | } 131 | success.accept(out); 132 | }catch(Throwable e){ 133 | fail.accept(e); 134 | } 135 | } 136 | 137 | public static class VersionInfo{ 138 | public final String name, description, build; 139 | public final int id; 140 | 141 | public VersionInfo(String name, String description, int id, String build){ 142 | this.name = name; 143 | this.description = description; 144 | this.id = id; 145 | this.build = build; 146 | } 147 | 148 | @Override 149 | public String toString(){ 150 | return "VersionInfo{" + 151 | "name='" + name + '\'' + 152 | ", description='" + description + '\'' + 153 | ", id=" + id + 154 | ", build=" + build + 155 | '}'; 156 | } 157 | } 158 | 159 | public void run(long delay, Runnable r){ 160 | new Timer().schedule(new TimerTask(){ 161 | @Override 162 | public void run(){ 163 | r.run(); 164 | } 165 | }, delay); 166 | } 167 | 168 | public Host readServerData(ByteBuffer buffer, String ip, int ping){ 169 | Host host = NetworkIO.readServerData(ping, ip, buffer); 170 | host.ping = ping; 171 | return host; 172 | //return new PingResult(ip, ping, players + "", host, map, wave + "", version == -1 ? "Custom Build" : (""+version)); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/corebot/ContentHandler.java: -------------------------------------------------------------------------------- 1 | package corebot; 2 | 3 | import arc.*; 4 | import arc.files.*; 5 | import arc.graphics.Color; 6 | import arc.graphics.*; 7 | import arc.graphics.g2d.*; 8 | import arc.graphics.g2d.TextureAtlas.*; 9 | import arc.math.*; 10 | import arc.math.geom.*; 11 | import arc.struct.*; 12 | import arc.util.io.*; 13 | import arc.util.serialization.*; 14 | import mindustry.*; 15 | import mindustry.content.*; 16 | import mindustry.core.*; 17 | import mindustry.ctype.*; 18 | import mindustry.entities.units.*; 19 | import mindustry.game.*; 20 | import mindustry.game.Schematic.*; 21 | import mindustry.io.*; 22 | import mindustry.world.*; 23 | import mindustry.world.blocks.environment.*; 24 | import mindustry.world.blocks.legacy.*; 25 | 26 | import javax.imageio.*; 27 | import java.awt.*; 28 | import java.awt.geom.*; 29 | import java.awt.image.*; 30 | import java.io.*; 31 | import java.util.zip.*; 32 | 33 | import static mindustry.Vars.*; 34 | 35 | public class ContentHandler{ 36 | public static final String schemHeader = schematicBaseStart; 37 | 38 | Color co = new Color(); 39 | Graphics2D currentGraphics; 40 | BufferedImage currentImage; 41 | ObjectMap imageFiles = new ObjectMap<>(); 42 | ObjectMap regions = new ObjectMap<>(); 43 | 44 | //for testing only 45 | //public static void main(String[] args) throws Exception{ 46 | // new ContentHandler().previewSchematic(Schematics.readBase64("bXNjaAF4nDWQXW6DQAyEB3b5MX/JW0/BQ6repuoDJa6EBEsFJFJu01v0WL1C7XWLhD6NGc8sizPOKXwYFsbTyzIF7i/P+zgcB2/9lT84jIx8Ht553pG9/nx9v3kUfwaU4xru/Fg31NPBS7+vt038p8/At2U4prG/btM8A7jIiwzxISBBihypghTOlFMlx4EXayIDr3MICkRFqmJMIog72f+w06HancIZvCGD04ocsak0Z4VEURsaQyufpM1rZiGW1Ik97pW6F0+v62RFZEVkRaRFihhNFk0WTRZNds5KMyGIP1bZndQ6VETVmGpMtaZa6+/sEjpVv/XMJCs=")); 47 | //} 48 | 49 | public ContentHandler(){ 50 | //clear cache 51 | new Fi("cache").deleteDirectory(); 52 | 53 | Version.enabled = false; 54 | Vars.content = new ContentLoader(); 55 | Vars.content.createBaseContent(); 56 | for(ContentType type : ContentType.all){ 57 | for(Content content : Vars.content.getBy(type)){ 58 | try{ 59 | content.init(); 60 | }catch(Throwable ignored){ 61 | } 62 | } 63 | } 64 | 65 | String assets = "../Mindustry/core/assets/"; 66 | Vars.state = new GameState(); 67 | 68 | TextureAtlasData data = new TextureAtlasData(new Fi(assets + "sprites/sprites.aatls"), new Fi(assets + "sprites"), false); 69 | Core.atlas = new TextureAtlas(); 70 | 71 | new Fi("../Mindustry/core/assets-raw/sprites_out").walk(f -> { 72 | if(f.extEquals("png")){ 73 | imageFiles.put(f.nameWithoutExtension(), f); 74 | } 75 | }); 76 | 77 | data.getPages().each(page -> { 78 | page.texture = Texture.createEmpty(null); 79 | page.texture.width = page.width; 80 | page.texture.height = page.height; 81 | }); 82 | 83 | data.getRegions().each(reg -> Core.atlas.addRegion(reg.name, new AtlasRegion(reg.page.texture, reg.left, reg.top, reg.width, reg.height){{ 84 | name = reg.name; 85 | texture = reg.page.texture; 86 | }})); 87 | 88 | Lines.useLegacyLine = true; 89 | Core.atlas.setErrorRegion("error"); 90 | Draw.scl = 1f / 4f; 91 | Core.batch = new SpriteBatch(0){ 92 | @Override 93 | protected void draw(TextureRegion region, float x, float y, float originX, float originY, float width, float height, float rotation){ 94 | x += 4; 95 | y += 4; 96 | 97 | x *= 4; 98 | y *= 4; 99 | width *= 4; 100 | height *= 4; 101 | 102 | y = currentImage.getHeight() - (y + height/2f) - height/2f; 103 | 104 | AffineTransform at = new AffineTransform(); 105 | at.translate(x, y); 106 | at.rotate(-rotation * Mathf.degRad, originX * 4, originY * 4); 107 | 108 | currentGraphics.setTransform(at); 109 | BufferedImage image = getImage(((AtlasRegion)region).name); 110 | if(!color.equals(Color.white)){ 111 | image = tint(image, color); 112 | } 113 | 114 | currentGraphics.drawImage(image, 0, 0, (int)width, (int)height, null); 115 | } 116 | 117 | @Override 118 | protected void draw(Texture texture, float[] spriteVertices, int offset, int count){ 119 | //do nothing 120 | } 121 | }; 122 | 123 | for(ContentType type : ContentType.values()){ 124 | for(Content content : Vars.content.getBy(type)){ 125 | try{ 126 | content.load(); 127 | content.loadIcon(); 128 | }catch(Throwable ignored){ 129 | } 130 | } 131 | } 132 | 133 | try{ 134 | BufferedImage image = ImageIO.read(new File("../Mindustry/core/assets/sprites/block_colors.png")); 135 | 136 | for(Block block : Vars.content.blocks()){ 137 | block.mapColor.argb8888(image.getRGB(block.id, 0)); 138 | if(block instanceof OreBlock){ 139 | block.mapColor.set(block.itemDrop.color); 140 | } 141 | } 142 | }catch(Exception e){ 143 | throw new RuntimeException(e); 144 | } 145 | 146 | world = new World(){ 147 | public Tile tile(int x, int y){ 148 | return new Tile(x, y); 149 | } 150 | }; 151 | } 152 | 153 | private BufferedImage getImage(String name){ 154 | return regions.get(name, () -> { 155 | try{ 156 | return ImageIO.read(imageFiles.get(name, imageFiles.get("error")).file()); 157 | }catch(Exception e){ 158 | throw new RuntimeException(e); 159 | } 160 | }); 161 | } 162 | 163 | private BufferedImage tint(BufferedImage image, Color color){ 164 | BufferedImage copy = new BufferedImage(image.getWidth(), image.getHeight(), image.getType()); 165 | Color tmp = new Color(); 166 | for(int x = 0; x < copy.getWidth(); x++){ 167 | for(int y = 0; y < copy.getHeight(); y++){ 168 | int argb = image.getRGB(x, y); 169 | tmp.argb8888(argb); 170 | tmp.mul(color); 171 | copy.setRGB(x, y, tmp.argb8888()); 172 | } 173 | } 174 | return copy; 175 | } 176 | 177 | public Schematic parseSchematic(String text) throws Exception{ 178 | return read(new ByteArrayInputStream(Base64Coder.decode(text))); 179 | } 180 | 181 | public Schematic parseSchematicURL(String text) throws Exception{ 182 | return read(CoreBot.net.download(text)); 183 | } 184 | 185 | static Schematic read(InputStream input) throws IOException{ 186 | byte[] header = {'m', 's', 'c', 'h'}; 187 | for(byte b : header){ 188 | if(input.read() != b){ 189 | throw new IOException("Not a schematic file (missing header)."); 190 | } 191 | } 192 | 193 | //discard version 194 | input.read(); 195 | 196 | try(DataInputStream stream = new DataInputStream(new InflaterInputStream(input))){ 197 | short width = stream.readShort(), height = stream.readShort(); 198 | 199 | StringMap map = new StringMap(); 200 | byte tags = stream.readByte(); 201 | for(int i = 0; i < tags; i++){ 202 | map.put(stream.readUTF(), stream.readUTF()); 203 | } 204 | 205 | IntMap blocks = new IntMap<>(); 206 | byte length = stream.readByte(); 207 | for(int i = 0; i < length; i++){ 208 | String name = stream.readUTF(); 209 | Block block = Vars.content.getByName(ContentType.block, SaveFileReader.fallback.get(name, name)); 210 | blocks.put(i, block == null || block instanceof LegacyBlock ? Blocks.air : block); 211 | } 212 | 213 | int total = stream.readInt(); 214 | if(total > 64 * 64) throw new IOException("Schematic has too many blocks."); 215 | Seq tiles = new Seq<>(total); 216 | for(int i = 0; i < total; i++){ 217 | Block block = blocks.get(stream.readByte()); 218 | int position = stream.readInt(); 219 | Object config = TypeIO.readObject(Reads.get(stream)); 220 | byte rotation = stream.readByte(); 221 | if(block != Blocks.air){ 222 | tiles.add(new Stile(block, Point2.x(position), Point2.y(position), config, rotation)); 223 | } 224 | } 225 | 226 | return new Schematic(tiles, map, width, height); 227 | } 228 | } 229 | 230 | public BufferedImage previewSchematic(Schematic schem) throws Exception{ 231 | if(schem.width > 64 || schem.height > 64) throw new IOException("Schematic cannot be larger than 64x64."); 232 | BufferedImage image = new BufferedImage(schem.width * 32, schem.height * 32, BufferedImage.TYPE_INT_ARGB); 233 | 234 | Draw.reset(); 235 | Seq requests = schem.tiles.map(t -> new BuildPlan(t.x, t.y, t.rotation, t.block, t.config)); 236 | currentGraphics = image.createGraphics(); 237 | currentImage = image; 238 | requests.each(req -> { 239 | req.animScale = 1f; 240 | req.worldContext = false; 241 | req.block.drawPlanRegion(req, requests); 242 | Draw.reset(); 243 | }); 244 | 245 | requests.each(req -> req.block.drawPlanConfigTop(req, requests)); 246 | 247 | return image; 248 | } 249 | 250 | public Map readMap(InputStream is) throws IOException{ 251 | try(InputStream ifs = new InflaterInputStream(is); CounterInputStream counter = new CounterInputStream(ifs); DataInputStream stream = new DataInputStream(counter)){ 252 | Map out = new Map(); 253 | 254 | SaveIO.readHeader(stream); 255 | int version = stream.readInt(); 256 | SaveVersion ver = SaveIO.getSaveWriter(version); 257 | StringMap[] metaOut = {null}; 258 | ver.region("meta", stream, counter, in -> metaOut[0] = ver.readStringMap(in)); 259 | 260 | StringMap meta = metaOut[0]; 261 | 262 | out.name = meta.get("name", "Unknown"); 263 | out.author = meta.get("author"); 264 | out.description = meta.get("description"); 265 | out.tags = meta; 266 | 267 | int width = meta.getInt("width"), height = meta.getInt("height"); 268 | 269 | var floors = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 270 | var walls = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 271 | var fgraphics = floors.createGraphics(); 272 | var jcolor = new java.awt.Color(0, 0, 0, 64); 273 | int black = 255; 274 | CachedTile tile = new CachedTile(){ 275 | @Override 276 | public void setBlock(Block type){ 277 | super.setBlock(type); 278 | 279 | int c = MapIO.colorFor(block(), Blocks.air, Blocks.air, team()); 280 | if(c != black && c != 0){ 281 | walls.setRGB(x, floors.getHeight() - 1 - y, conv(c)); 282 | fgraphics.setColor(jcolor); 283 | fgraphics.drawRect(x, floors.getHeight() - 1 - y + 1, 1, 1); 284 | } 285 | } 286 | }; 287 | 288 | ver.region("content", stream, counter, ver::readContentHeader); 289 | ver.region("preview_map", stream, counter, in -> ver.readMap(in, new WorldContext(){ 290 | @Override public void resize(int width, int height){} 291 | @Override public boolean isGenerating(){return false;} 292 | @Override public void begin(){ 293 | world.setGenerating(true); 294 | } 295 | @Override public void end(){ 296 | world.setGenerating(false); 297 | } 298 | 299 | @Override 300 | public void onReadBuilding(){ 301 | //read team colors 302 | if(tile.build != null){ 303 | int c = tile.build.team.color.argb8888(); 304 | int size = tile.block().size; 305 | int offsetx = -(size - 1) / 2; 306 | int offsety = -(size - 1) / 2; 307 | for(int dx = 0; dx < size; dx++){ 308 | for(int dy = 0; dy < size; dy++){ 309 | int drawx = tile.x + dx + offsetx, drawy = tile.y + dy + offsety; 310 | walls.setRGB(drawx, floors.getHeight() - 1 - drawy, c); 311 | } 312 | } 313 | } 314 | } 315 | 316 | @Override 317 | public Tile tile(int index){ 318 | tile.x = (short)(index % width); 319 | tile.y = (short)(index / width); 320 | return tile; 321 | } 322 | 323 | @Override 324 | public Tile create(int x, int y, int floorID, int overlayID, int wallID){ 325 | if(overlayID != 0){ 326 | floors.setRGB(x, floors.getHeight() - 1 - y, conv(MapIO.colorFor(Blocks.air, Blocks.air, content.block(overlayID), Team.derelict))); 327 | }else{ 328 | floors.setRGB(x, floors.getHeight() - 1 - y, conv(MapIO.colorFor(Blocks.air, content.block(floorID), Blocks.air, Team.derelict))); 329 | } 330 | return tile; 331 | } 332 | })); 333 | 334 | fgraphics.drawImage(walls, 0, 0, null); 335 | fgraphics.dispose(); 336 | 337 | out.image = floors; 338 | 339 | return out; 340 | 341 | }finally{ 342 | content.setTemporaryMapper(null); 343 | } 344 | } 345 | 346 | int conv(int rgba){ 347 | return co.set(rgba).argb8888(); 348 | } 349 | 350 | public static class Map{ 351 | public String name, author, description; 352 | public ObjectMap tags = new ObjectMap<>(); 353 | public BufferedImage image; 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/corebot/Messages.java: -------------------------------------------------------------------------------- 1 | package corebot; 2 | 3 | import arc.files.*; 4 | import arc.math.*; 5 | import arc.struct.*; 6 | import arc.util.*; 7 | import arc.util.Nullable; 8 | import arc.util.CommandHandler.*; 9 | import arc.util.io.Streams; 10 | import arc.util.serialization.*; 11 | import arc.util.serialization.Jval.*; 12 | import mindustry.*; 13 | import mindustry.game.*; 14 | import mindustry.type.*; 15 | import net.dv8tion.jda.api.*; 16 | import net.dv8tion.jda.api.entities.*; 17 | import net.dv8tion.jda.api.entities.Message.*; 18 | import net.dv8tion.jda.api.events.guild.member.*; 19 | import net.dv8tion.jda.api.events.message.*; 20 | import net.dv8tion.jda.api.events.message.react.*; 21 | import net.dv8tion.jda.api.hooks.*; 22 | import net.dv8tion.jda.api.requests.*; 23 | import net.dv8tion.jda.api.utils.*; 24 | import net.dv8tion.jda.api.utils.cache.*; 25 | import org.jetbrains.annotations.*; 26 | 27 | import javax.imageio.*; 28 | import java.awt.image.*; 29 | import java.io.*; 30 | import java.text.*; 31 | import java.time.*; 32 | import java.util.*; 33 | import java.util.concurrent.*; 34 | import java.util.regex.*; 35 | import java.util.stream.*; 36 | import java.util.zip.*; 37 | 38 | import static corebot.CoreBot.*; 39 | 40 | public class Messages extends ListenerAdapter{ 41 | private static final String prefix = "!"; 42 | private static final int scamAutobanLimit = 3, pingSpamLimit = 20, minModStars = 10, naughtyTimeoutMins = 20; 43 | private static final SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd"); 44 | private static final String[] warningStrings = {"once", "twice", "thrice", "too many times"}; 45 | 46 | private static final String 47 | cyrillicFrom = "абсдефгнигклмпоркгзтюушхуз", 48 | cyrillicTo = "abcdefghijklmnopqrstuvwxyz"; 49 | 50 | // https://stackoverflow.com/a/48769624 51 | private static final Pattern urlPattern = Pattern.compile("(?:(?:https?):\\/\\/)?[\\w/\\-?=%.]+\\.[\\w/\\-&?=%.]+"); 52 | private static final Set trustedDomains = Set.of( 53 | "discord.com", 54 | "discord.co", 55 | "discord.gg", 56 | "discord.media", 57 | "discord.gift", 58 | "discordapp.com", 59 | "discordapp.net", 60 | "discordstatus.com" 61 | ); 62 | 63 | //yes it's base64 encoded, I don't want any of these words typed here 64 | private static final Pattern badWordPattern = Pattern.compile(new String(Base64Coder.decode("KD88IVthLXpBLVpdKSg/OmN1bXxzZW1lbnxjb2NrfHB1c3N5fGN1bnR8bmlnZy5yKSg/IVthLXpBLVpdKQ=="))); 65 | private static final Pattern notBadWordPattern = Pattern.compile(""); 66 | private static final Pattern invitePattern = Pattern.compile("(discord\\.gg/\\w|discordapp\\.com/invite/\\w|discord\\.com/invite/\\w)"); 67 | private static final Pattern linkPattern = Pattern.compile("http(s?)://"); 68 | private static final Pattern notScamPattern = Pattern.compile("discord\\.py|discord\\.js|nitrome\\.com"); 69 | private static final Pattern scamPattern = Pattern.compile(String.join("|", 70 | "stea.*co.*\\.ru", 71 | "http.*stea.*c.*\\..*trad", 72 | "csgo.*kni[fv]e", 73 | "cs.?go.*inventory", 74 | "cs.?go.*cheat", 75 | "cheat.*cs.?go", 76 | "cs.?go.*skins", 77 | "skins.*cs.?go", 78 | "stea.*com.*partner", 79 | "скин.*partner", 80 | "steamcommutiny", 81 | "di.*\\.gift.*nitro", 82 | "http.*disc.*gift.*\\.", 83 | "free.*nitro.*http", 84 | "http.*free.*nitro.*", 85 | "nitro.*free.*http", 86 | "discord.*nitro.*free", 87 | "free.*discord.*nitro", 88 | "@everyone.*http", 89 | "http.*@everyone", 90 | "discordgivenitro", 91 | "http.*gift.*nitro", 92 | "http.*nitro.*gift", 93 | "http.*n.*gift", 94 | "бесплат.*нитро.*http", 95 | "нитро.*бесплат.*http", 96 | "nitro.*http.*disc.*nitro", 97 | "http.*click.*nitro", 98 | "http.*st.*nitro", 99 | "http.*nitro", 100 | "stea.*give.*nitro", 101 | "discord.*nitro.*steam.*get", 102 | "gift.*nitro.*http", 103 | "http.*discord.*gift", 104 | "discord.*nitro.*http", 105 | "personalize.*your*profile.*http", 106 | "nitro.*steam.*http", 107 | "steam.*nitro.*http", 108 | "nitro.*http.*d", 109 | "http.*d.*gift", 110 | "gift.*http.*d.*s", 111 | "discord.*steam.*http.*d", 112 | "nitro.*steam.*http", 113 | "steam.*nitro.*http", 114 | "dliscord.com", 115 | "free.*nitro.*http", 116 | "discord.*nitro.*http", 117 | "@everyone.*http", 118 | "http.*@everyone", 119 | "@everyone.*nitro", 120 | "nitro.*@everyone", 121 | "discord.*gi.*nitro" 122 | )); 123 | 124 | private final ObjectMap userData = new ObjectMap<>(); 125 | private final CommandHandler handler = new CommandHandler(prefix); 126 | private final CommandHandler adminHandler = new CommandHandler(prefix); 127 | private final JDA jda; 128 | 129 | public Guild guild; 130 | public TextChannel 131 | pluginChannel, crashReportChannel, announcementsChannel, artChannel, 132 | mapsChannel, moderationChannel, schematicsChannel, baseSchematicsChannel, 133 | logChannel, joinChannel, videosChannel, streamsChannel, testingChannel, 134 | alertsChannel, curatedSchematicsChannel, botsChannel; 135 | public Emote aaaaa; 136 | 137 | public Role modderRole; 138 | 139 | LongSeq schematicChannels = new LongSeq(); 140 | 141 | public Messages(){ 142 | String token = System.getenv("CORE_BOT_TOKEN"); 143 | 144 | register(); 145 | 146 | try{ 147 | jda = JDABuilder.createDefault(token, GatewayIntent.GUILD_MESSAGES, GatewayIntent.GUILD_EMOJIS, GatewayIntent.GUILD_MEMBERS, GatewayIntent.GUILD_PRESENCES, GatewayIntent.GUILD_MESSAGE_REACTIONS) 148 | .setMemberCachePolicy(MemberCachePolicy.ALL).disableCache(CacheFlag.VOICE_STATE).build(); 149 | jda.awaitReady(); 150 | jda.addEventListener(this); 151 | 152 | loadChannels(); 153 | 154 | Log.info("Discord bot up."); 155 | }catch(Exception e){ 156 | throw new RuntimeException(e); 157 | } 158 | } 159 | 160 | TextChannel channel(long id){ 161 | return guild.getTextChannelById(id); 162 | } 163 | 164 | void loadChannels(){ 165 | //all guilds and channels are loaded here for faster lookup 166 | guild = jda.getGuildById(391020510269669376L); 167 | 168 | modderRole = guild.getRoleById(965691639811149865L); 169 | 170 | pluginChannel = channel(617833229973717032L); 171 | crashReportChannel = channel(467033526018113546L); 172 | announcementsChannel = channel(391020997098340352L); 173 | artChannel = channel(754011833928515664L); 174 | mapsChannel = channel(416719902641225732L); 175 | moderationChannel = channel(488049830275579906L); 176 | schematicsChannel = channel(640604827344306207L); 177 | baseSchematicsChannel = channel(718536034127839252L); 178 | logChannel = channel(568416809964011531L); 179 | joinChannel = channel(832688792338038844L); 180 | streamsChannel = channel(833420066238103604L); 181 | videosChannel = channel(833826797048692747L); 182 | testingChannel = channel(432984286099144706L); 183 | alertsChannel = channel(864139464401223730L); 184 | curatedSchematicsChannel = channel(878022862915653723L); 185 | botsChannel = channel(414179246693679124L); 186 | aaaaa = guild.getEmotesByName("alphaaaaaaaa", true).get(0); 187 | 188 | schematicChannels.add(schematicsChannel.getIdLong(), baseSchematicsChannel.getIdLong(), curatedSchematicsChannel.getIdLong()); 189 | } 190 | 191 | void printCommands(CommandHandler handler, StringBuilder builder){ 192 | for(Command command : handler.getCommandList()){ 193 | builder.append(prefix); 194 | builder.append("**"); 195 | builder.append(command.text); 196 | builder.append("**"); 197 | if(command.params.length > 0){ 198 | builder.append(" *"); 199 | builder.append(command.paramText); 200 | builder.append("*"); 201 | } 202 | builder.append(" - "); 203 | builder.append(command.description); 204 | builder.append("\n"); 205 | } 206 | } 207 | 208 | void register(){ 209 | handler.register("help", "Displays all bot commands.", (args, msg) -> { 210 | StringBuilder builder = new StringBuilder(); 211 | printCommands(handler, builder); 212 | info(msg.getChannel(), "Commands", builder.toString()); 213 | }); 214 | 215 | handler.register("ping", "", "Pings a server.", (args, msg) -> { 216 | if(msg.getChannel().getIdLong() != botsChannel.getIdLong()){ 217 | errDelete(msg, "Use this command in #bots."); 218 | return; 219 | } 220 | 221 | net.pingServer(args[0], result -> { 222 | if(result.name != null){ 223 | info(msg.getChannel(), "Server Online", "Host: @\nPlayers: @\nMap: @\nWave: @\nVersion: @\nPing: @ms", 224 | Strings.stripColors(result.name), result.players, Strings.stripColors(result.mapname), result.wave, result.version, result.ping); 225 | }else{ 226 | errDelete(msg, "Server Offline", "Timed out."); 227 | } 228 | }); 229 | }); 230 | 231 | handler.register("info", "", "Displays information about a topic.", (args, msg) -> { 232 | try{ 233 | Info info = Info.valueOf(args[0]); 234 | infoDesc(msg.getChannel(), info.title, info.text); 235 | }catch(IllegalArgumentException e){ 236 | errDelete(msg, "Error", "Invalid topic '@'.\nValid topics: *@*", args[0], Arrays.toString(Info.values())); 237 | } 238 | }); 239 | 240 | 241 | handler.register("postplugin", " ", "Post a plugin via Github repository URL.", (args, msg) -> { 242 | // https://docs.github.com/en/rest/repos/repos#get-a-repository 243 | Http.get("https://api.github.com/repos/" + args[0] + "/" + args[1]) 244 | .header("Accept", "application/vnd.github+json") 245 | .error(err -> errDelete(msg, "Error querying Github", Strings.getSimpleMessage(err))) 246 | .block(result -> { 247 | try{ 248 | Jval repo = Jval.read(result.getResultAsString()); 249 | String repoUrl = repo.getString("html_url"); 250 | Jval author = repo.get("owner"); 251 | 252 | EmbedBuilder builder = new EmbedBuilder() 253 | .setColor(normalColor) 254 | .setTitle(repo.getString("name"), repoUrl); 255 | 256 | if(!repo.getString("description").isBlank()){ 257 | builder.addField("About", repo.getString("description"), false); 258 | } 259 | 260 | builder.addField("Downloads", repoUrl + "/releases", false); 261 | 262 | pluginChannel.sendMessageEmbeds(builder.build()).queue(); 263 | text(msg, "*Plugin posted.*"); 264 | }catch(Exception e){ 265 | errDelete(msg, "Failed to fetch plugin info from URL."); 266 | } 267 | }); 268 | }); 269 | 270 | handler.register("postmap", "Post a .msav file to the #maps channel.", (args, msg) -> { 271 | 272 | if(msg.getAttachments().size() != 1 || !msg.getAttachments().get(0).getFileName().endsWith(".msav")){ 273 | errDelete(msg, "You must have one .msav file in the same message as the command!"); 274 | return; 275 | } 276 | 277 | Attachment a = msg.getAttachments().get(0); 278 | 279 | try{ 280 | ContentHandler.Map map = contentHandler.readMap(net.download(a.getUrl())); 281 | new File("cache/").mkdir(); 282 | File mapFile = new File("cache/" + a.getFileName()); 283 | Fi imageFile = Fi.get("cache/image_" + a.getFileName().replace(".msav", ".png")); 284 | Streams.copy(net.download(a.getUrl()), new FileOutputStream(mapFile)); 285 | ImageIO.write(map.image, "png", imageFile.file()); 286 | 287 | EmbedBuilder builder = new EmbedBuilder().setColor(normalColor).setColor(normalColor) 288 | .setImage("attachment://" + imageFile.name()) 289 | .setAuthor(msg.getAuthor().getName(), msg.getAuthor().getEffectiveAvatarUrl(), msg.getAuthor().getEffectiveAvatarUrl()) 290 | .setTitle(map.name == null ? a.getFileName().replace(".msav", "") : map.name); 291 | 292 | if(map.description != null) builder.setFooter(map.description); 293 | 294 | mapsChannel.sendFile(mapFile).addFile(imageFile.file()).setEmbeds(builder.build()).queue(); 295 | 296 | text(msg, "*Map posted successfully.*"); 297 | }catch(Exception e){ 298 | String err = Strings.neatError(e, true); 299 | int max = 900; 300 | errDelete(msg, "Error parsing map.", err.length() < max ? err : err.substring(0, max)); 301 | } 302 | }); 303 | 304 | handler.register("verifymodder", "[user/repo]", "Verify yourself as a modder by showing a mod repository that you own. Invoke with no arguments for additional info.", (args, msg) -> { 305 | if(msg.getChannel().getIdLong() != botsChannel.getIdLong()){ 306 | errDelete(msg, "Use this command in #bots."); 307 | return; 308 | } 309 | 310 | if(msg.getMember() == null){ 311 | errDelete(msg, "Absolutely no ghosts allowed."); 312 | return; 313 | } 314 | 315 | String rawSearchString = (msg.getAuthor().getName() + "#" + msg.getAuthor().getDiscriminator()); 316 | 317 | if(args.length == 0){ 318 | info(msg.getChannel(), "Modder Verification", """ 319 | To obtain the Modder role, you must do the following: 320 | 321 | 1. Own a Github repository with the `mindustry-mod` tag. 322 | 2. Have at least @ stars on the repository. 323 | 3. Temporarily add your Discord `USERNAME#DISCRIMINATOR` (`@`) to the repository description or your user bio, to verify ownership. 324 | 4. Run this command with the repository URL or `Username/Repo` as an argument. 325 | """, minModStars, rawSearchString); 326 | }else{ 327 | if(msg.getMember().getRoles().stream().anyMatch(r -> r.equals(modderRole))){ 328 | errDelete(msg, "You already have that role."); 329 | return; 330 | } 331 | 332 | String repo = args[0]; 333 | int offset = "https://github.com/".length(); 334 | if(repo.startsWith("https://") && repo.length() > offset + 1){ 335 | repo = repo.substring(offset); 336 | } 337 | 338 | Http.get("https://api.github.com/repos/" + repo) 339 | .header("Accept", "application/vnd.github.v3+json") 340 | .error(err -> errDelete(msg, "Error fetching repository (Did you type the name correctly?)", Strings.getSimpleMessage(err))) 341 | .block(res -> { 342 | Jval val = Jval.read(res.getResultAsString()); 343 | String searchString = rawSearchString.toLowerCase(Locale.ROOT); 344 | 345 | boolean contains = val.getString("description").toLowerCase(Locale.ROOT).contains(searchString); 346 | boolean[] actualContains = {contains}; 347 | 348 | //check bio if not found 349 | if(!contains){ 350 | Http.get(val.get("owner").getString("url")) 351 | .error(Log::err) //why would this ever happen 352 | .block(user -> { 353 | Jval userVal = Jval.read(user.getResultAsString()); 354 | if(userVal.getString("bio", "").toLowerCase(Locale.ROOT).contains(searchString)){ 355 | actualContains[0] = true; 356 | } 357 | }); 358 | } 359 | 360 | if(!val.get("topics").asArray().contains(j -> j.asString().contains("mindustry-mod"))){ 361 | errDelete(msg, "Unable to find `mindustry-mod` in the list of repository topics.\nAdd it in the topics section *(this can be edited next to the 'About' section)*."); 362 | return; 363 | } 364 | 365 | if(!actualContains[0]){ 366 | errDelete(msg, "Unable to find your Discord username + discriminator in the repo description or owner bio.\n\nMake sure `" + rawSearchString + "` is written in one of these locations."); 367 | return; 368 | } 369 | 370 | if(val.getInt("stargazers_count", 0) < minModStars){ 371 | errDelete(msg, "You need at least " + minModStars + " stars on your repository to get the Modder role."); 372 | return; 373 | } 374 | 375 | guild.addRoleToMember(msg.getMember(), modderRole).queue(); 376 | 377 | info(msg.getChannel(), "Success!", "You have now obtained the Modder role."); 378 | }); 379 | } 380 | }); 381 | 382 | handler.register("google", "", "Let me google that for you.", (args, msg) -> { 383 | text(msg, "https://lmgt.org/?q=@", Strings.encode(args[0])); 384 | }); 385 | 386 | handler.register("cleanmod", "Clean up a modded zip archive. Changes json into hjson and formats code.", (args, msg) -> { 387 | 388 | if(msg.getAttachments().size() != 1 || !msg.getAttachments().get(0).getFileName().endsWith(".zip")){ 389 | errDelete(msg, "You must have one .zip file in the same message as the command!"); 390 | return; 391 | } 392 | 393 | Attachment a = msg.getAttachments().get(0); 394 | 395 | if(a.getSize() > 1024 * 1024 * 6){ 396 | errDelete(msg, "Zip files may be no more than 6 MB."); 397 | } 398 | 399 | try{ 400 | new File("cache/").mkdir(); 401 | File baseFile = new File("cache/" + a.getFileName()); 402 | Fi destFolder = new Fi("cache/dest_mod" + a.getFileName()); 403 | Fi destFile = new Fi("cache/" + new Fi(baseFile).nameWithoutExtension() + "-cleaned.zip"); 404 | 405 | if(destFolder.exists()) destFolder.deleteDirectory(); 406 | if(destFile.exists()) destFile.delete(); 407 | 408 | Streams.copy(net.download(a.getUrl()), new FileOutputStream(baseFile)); 409 | ZipFi zip = new ZipFi(new Fi(baseFile.getPath())); 410 | zip.walk(file -> { 411 | Fi output = destFolder.child(file.extension().equals("json") ? file.pathWithoutExtension() + ".hjson" : file.path()); 412 | output.parent().mkdirs(); 413 | 414 | if(file.extension().equals("json") || file.extension().equals("hjson")){ 415 | output.writeString(fixJval(Jval.read(file.readString())).toString(Jformat.hjson)); 416 | }else{ 417 | file.copyTo(output); 418 | } 419 | }); 420 | 421 | try(OutputStream fos = destFile.write(false, 2048); ZipOutputStream zos = new ZipOutputStream(fos)){ 422 | for(Fi add : destFolder.findAll(f -> true)){ 423 | if(add.isDirectory()) continue; 424 | zos.putNextEntry(new ZipEntry(add.path().substring(destFolder.path().length()))); 425 | Streams.copy(add.read(), zos); 426 | zos.closeEntry(); 427 | } 428 | 429 | } 430 | 431 | msg.getChannel().sendFile(destFile.file()).queue(); 432 | 433 | text(msg, "*Mod converted successfully.*"); 434 | }catch(Throwable e){ 435 | errDelete(msg, "Error parsing mod.", Strings.neatError(e, false)); 436 | } 437 | }); 438 | 439 | handler.register("file", "", "Find a Mindustry source file by name", (args, msg) -> { 440 | //epic asynchronous code, I know 441 | Http.get("https://api.github.com/search/code?q=" + 442 | "filename:" + Strings.encode(args[0]) + "%20" + 443 | "repo:Anuken/Mindustry") 444 | .header("Accept", "application/vnd.github.v3+json") 445 | .error(err -> errDelete(msg, "Error querying Github", Strings.getSimpleMessage(err))) 446 | .block(result -> { 447 | msg.delete().queue(); 448 | Jval val = Jval.read(result.getResultAsString()); 449 | 450 | //merge with arc results 451 | Http.get("https://api.github.com/search/code?q=" + 452 | "filename:" + Strings.encode(args[0]) + "%20" + 453 | "repo:Anuken/Arc") 454 | .header("Accept", "application/vnd.github.v3+json") 455 | .block(arcResult -> { 456 | Jval arcVal = Jval.read(arcResult.getResultAsString()); 457 | 458 | val.get("items").asArray().addAll(arcVal.get("items").asArray()); 459 | val.put("total_count", val.getInt("total_count", 0) + arcVal.getInt("total_count", 0)); 460 | }); 461 | 462 | int count = val.getInt("total_count", 0); 463 | 464 | if(count > 0){ 465 | val.get("items").asArray().removeAll(j -> !j.getString("name").contains(args[0])); 466 | count = val.get("items").asArray().size; 467 | } 468 | 469 | if(count == 0){ 470 | errDelete(msg, "No results found."); 471 | return; 472 | } 473 | 474 | EmbedBuilder embed = new EmbedBuilder(); 475 | embed.setColor(normalColor); 476 | embed.setAuthor(msg.getAuthor().getName() + ": Github Search Results", val.get("items").asArray().first().getString("html_url"), "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"); 477 | embed.setTitle("Github Search Results"); 478 | 479 | if(count == 1){ 480 | Jval item = val.get("items").asArray().first(); 481 | embed.setTitle(item.getString("name")); 482 | embed.setDescription("[View on Github](" + item.getString("html_url") + ")"); 483 | }else{ 484 | int maxResult = 5, i = 0; 485 | StringBuilder results = new StringBuilder(); 486 | for(Jval item : val.get("items").asArray()){ 487 | if(i++ > maxResult){ 488 | break; 489 | } 490 | results.append("[").append(item.getString("name")).append("]").append("(").append(item.getString("html_url")).append(")\n"); 491 | } 492 | 493 | embed.setTitle((count > maxResult ? maxResult + "+" : count) + " Source Results"); 494 | embed.setDescription(results.toString()); 495 | } 496 | 497 | msg.getChannel().sendMessageEmbeds(embed.build()).queue(); 498 | }); 499 | }); 500 | 501 | 502 | handler.register("mywarnings", "Get information about your own warnings. Only usable in #bots.", (args, msg) -> { 503 | if(msg.getChannel().getIdLong() != botsChannel.getIdLong()){ 504 | errDelete(msg, "Use this command in #bots."); 505 | return; 506 | } 507 | 508 | sendWarnings(msg, msg.getAuthor()); 509 | }); 510 | 511 | handler.register("avatar", "[@user]", "Get a user's full avatar.", (args, msg) -> { 512 | if(msg.getChannel().getIdLong() != botsChannel.getIdLong() && !isAdmin(msg.getAuthor())){ 513 | errDelete(msg, "Use this command in #bots."); 514 | return; 515 | } 516 | 517 | try{ 518 | User user; 519 | if(args.length > 0){ 520 | long id; 521 | try{ 522 | id = Long.parseLong(args[0]); 523 | }catch(NumberFormatException e){ 524 | String author = args[0].substring(2, args[0].length() - 1); 525 | if(author.startsWith("!")) author = author.substring(1); 526 | id = Long.parseLong(author); 527 | } 528 | 529 | user = jda.retrieveUserById(id).complete(); 530 | }else{ 531 | user = msg.getAuthor(); 532 | } 533 | 534 | //if(user.getIdLong() == 737869099811733527L){ 535 | // text(msg, "no"); 536 | //}else 537 | if(user.getIdLong() == jda.getSelfUser().getIdLong() && Mathf.chance(0.5)){ 538 | msg.getChannel().sendMessage(aaaaa.getAsMention()).queue(); 539 | }else{ 540 | String link = user.getEffectiveAvatarUrl() + "?size=1024"; 541 | 542 | EmbedBuilder embed = new EmbedBuilder(); 543 | embed.setColor(normalColor); 544 | embed.setTitle("Avatar: " + user.getName() + "#" + user.getDiscriminator()); 545 | embed.setImage(link); 546 | embed.setDescription("[Link](" + link + ")"); 547 | embed.setFooter("Requested by " + msg.getAuthor().getName() + "#" + msg.getAuthor().getDiscriminator()); 548 | msg.getChannel().sendMessageEmbeds(embed.build()).queue(); 549 | } 550 | 551 | }catch(Exception e){ 552 | errDelete(msg, "Incorrect name format or ID."); 553 | } 554 | }); 555 | 556 | adminHandler.register("adminhelp", "Displays all bot commands.", (args, msg) -> { 557 | StringBuilder builder = new StringBuilder(); 558 | printCommands(adminHandler, builder); 559 | info(msg.getChannel(), "Admin Commands", builder.toString()); 560 | }); 561 | 562 | adminHandler.register("userinfo", "<@user>", "Get user info.", (args, msg) -> { 563 | String author = args[0].substring(2, args[0].length() - 1); 564 | if(author.startsWith("!")) author = author.substring(1); 565 | try{ 566 | long l = Long.parseLong(author); 567 | User user = jda.retrieveUserById(l).complete(); 568 | 569 | if(user == null){ 570 | errDelete(msg, "That user (ID @) is not in the cache. How did this happen?", l); 571 | }else{ 572 | Member member = guild.retrieveMember(user).complete(); 573 | 574 | info(msg.getChannel(), "Info for " + member.getEffectiveName(), 575 | "Nickname: @\nUsername: @\nID: @\nStatus: @\nRoles: @\nIs Admin: @\nTime Joined: @", 576 | member.getNickname(), 577 | user.getName(), 578 | member.getIdLong(), 579 | member.getOnlineStatus(), 580 | member.getRoles().stream().map(Role::getName).collect(Collectors.toList()), 581 | isAdmin(user), 582 | member.getTimeJoined() 583 | ); 584 | } 585 | }catch(Exception e){ 586 | errDelete(msg, "Incorrect name format or missing user."); 587 | } 588 | }); 589 | 590 | adminHandler.register("warnings", "<@user>", "Get number of warnings a user has.", (args, msg) -> { 591 | String author = args[0].substring(2, args[0].length() - 1); 592 | if(author.startsWith("!")) author = author.substring(1); 593 | try{ 594 | long l = Long.parseLong(author); 595 | User user = jda.retrieveUserById(l).complete(); 596 | sendWarnings(msg, user); 597 | }catch(Exception e){ 598 | errDelete(msg, "Incorrect name format."); 599 | } 600 | }); 601 | 602 | adminHandler.register("testemoji", "", "Send an emoji by ID.", (args, msg) -> { 603 | Emote emoji = null; 604 | 605 | try{ 606 | emoji = guild.getEmoteById(args[0]); 607 | }catch(Exception ignored){ 608 | } 609 | 610 | if(emoji == null){ 611 | var emotes = guild.getEmotesByName(args[0], true); 612 | if(emotes.size() > 0){ 613 | emoji = emotes.get(0); 614 | } 615 | } 616 | 617 | if(emoji == null){ 618 | errDelete(msg, "Emoji not found."); 619 | }else{ 620 | msg.delete().queue(); 621 | text(msg.getChannel(), emoji.getAsMention()); 622 | } 623 | }); 624 | 625 | adminHandler.register("delete", "", "Delete some messages.", (args, msg) -> { 626 | try{ 627 | int number = Integer.parseInt(args[0]); 628 | MessageHistory hist = msg.getChannel().getHistoryBefore(msg, number).complete(); 629 | msg.delete().queue(); 630 | msg.getTextChannel().deleteMessages(hist.getRetrievedHistory()).queue(); 631 | }catch(NumberFormatException e){ 632 | errDelete(msg, "Invalid number."); 633 | } 634 | }); 635 | 636 | adminHandler.register("warn", "<@user> [reason...]", "Warn a user.", (args, msg) -> { 637 | String author = args[0].substring(2, args[0].length() - 1); 638 | if(author.startsWith("!")) author = author.substring(1); 639 | try{ 640 | long l = Long.parseLong(author); 641 | User user = jda.retrieveUserById(l).complete(); 642 | var list = getWarnings(user); 643 | list.add(System.currentTimeMillis() + ":::" + msg.getAuthor().getName() + (args.length > 1 ? ":::" + args[1] : "")); 644 | text(msg, "**@**, you've been warned *@*.", user.getAsMention(), warningStrings[Mathf.clamp(list.size - 1, 0, warningStrings.length - 1)]); 645 | prefs.putArray("warning-list-" + user.getIdLong(), list); 646 | if(list.size >= 3){ 647 | moderationChannel.sendMessage("User " + user.getAsMention() + " has been warned 3 or more times!").queue(); 648 | } 649 | }catch(Exception e){ 650 | errDelete(msg, "Incorrect name format."); 651 | } 652 | }); 653 | 654 | adminHandler.register("unwarn", "<@user> ", "Remove a warning.", (args, msg) -> { 655 | String author = args[0].substring(2, args[0].length() - 1); 656 | if(author.startsWith("!")) author = author.substring(1); 657 | try{ 658 | int index = Math.max(Integer.parseInt(args[1]), 1); 659 | long l = Long.parseLong(author); 660 | User user = jda.retrieveUserById(l).complete(); 661 | var list = getWarnings(user); 662 | if(list.size > index - 1){ 663 | list.remove(index - 1); 664 | prefs.putArray("warning-list-" + user.getIdLong(), list); 665 | text(msg, "Removed warning for user."); 666 | }else{ 667 | errDelete(msg, "Invalid index. @ > @", index, list.size); 668 | } 669 | }catch(Exception e){ 670 | errDelete(msg, "Incorrect name/index format."); 671 | } 672 | }); 673 | 674 | adminHandler.register("clearwarnings", "<@user>", "Clear number of warnings for a person.", (args, msg) -> { 675 | String author = args[0].substring(2, args[0].length() - 1); 676 | if(author.startsWith("!")) author = author.substring(1); 677 | try{ 678 | long l = Long.parseLong(author); 679 | User user = jda.retrieveUserById(l).complete(); 680 | prefs.putArray("warning-list-" + user.getIdLong(), new Seq<>()); 681 | text(msg, "Cleared warnings for user '@'.", user.getName()); 682 | }catch(Exception e){ 683 | errDelete(msg, "Incorrect name format."); 684 | } 685 | }); 686 | 687 | adminHandler.register("schemdesigner", " <@user>", "Make a user a verified schematic designer.", (args, msg) -> { 688 | String author = args[1].substring(2, args[1].length() - 1); 689 | if(author.startsWith("!")) author = author.substring(1); 690 | try{ 691 | 692 | var l = UserSnowflake.fromId(author); 693 | User user = jda.retrieveUserById(author).complete(); 694 | boolean add = args[0].equals("add"); 695 | if(add){ 696 | guild.addRoleToMember(l, guild.getRoleById(877171645427621889L)).queue(); 697 | }else{ 698 | guild.removeRoleFromMember(l, guild.getRoleById(877171645427621889L)).queue(); 699 | } 700 | 701 | text(msg, "**@** is @ a verified schematic designer.", user.getName(), add ? "now" : "no longer"); 702 | }catch(Exception e){ 703 | errDelete(msg, "Incorrect name format."); 704 | } 705 | }); 706 | 707 | adminHandler.register("banid", " [reason...]", "Ban a user by a raw numeric ID.", (args, msg) -> { 708 | try{ 709 | long l = Long.parseLong(args[0]); 710 | User user = jda.retrieveUserById(l).complete(); 711 | 712 | guild.ban(user, 0, args.length > 1 ? msg.getAuthor().getName() + " used banid: " + args[1] : msg.getAuthor().getName() + ": ").queue(); 713 | text(msg, "Banned user: **@**", l); 714 | }catch(Exception e){ 715 | errDelete(msg, "Incorrect name format, or user not found."); 716 | } 717 | }); 718 | } 719 | 720 | @Override 721 | public void onMessageReactionAdd(@NotNull MessageReactionAddEvent event){ 722 | try{ 723 | if(event.getUser() != null && event.getChannel().equals(mapsChannel) && event.getReactionEmote().isEmoji() && event.getReactionEmote().getEmoji().equals("❌")){ 724 | event.getChannel().retrieveMessageById(event.getMessageIdLong()).queue(m -> { 725 | try{ 726 | String baseUrl = event.retrieveUser().complete().getEffectiveAvatarUrl(); 727 | 728 | for(var embed : m.getEmbeds()){ 729 | if(embed.getAuthor() != null && embed.getAuthor().getIconUrl() != null && embed.getAuthor().getIconUrl().equals(baseUrl)){ 730 | m.delete().queue(); 731 | return; 732 | } 733 | } 734 | }catch(Exception e){ 735 | Log.err(e); 736 | } 737 | }); 738 | } 739 | }catch(Exception e){ 740 | Log.err(e); 741 | } 742 | } 743 | 744 | @Override 745 | public void onMessageReceived(MessageReceivedEvent event){ 746 | try{ 747 | 748 | var msg = event.getMessage(); 749 | 750 | if(msg.getAuthor().isBot() || msg.getChannel().getType() != ChannelType.TEXT) return; 751 | 752 | if(msg.getMentionedUsers().contains(jda.getSelfUser())){ 753 | msg.addReaction(aaaaa).queue(); 754 | } 755 | 756 | EmbedBuilder log = new EmbedBuilder() 757 | .setAuthor(msg.getAuthor().getName(), msg.getAuthor().getEffectiveAvatarUrl(), msg.getAuthor().getEffectiveAvatarUrl()) 758 | .setDescription(msg.getContentRaw().length() >= 2040 ? msg.getContentRaw().substring(0, 2040) + "..." : msg.getContentRaw()) 759 | .addField("Author", msg.getAuthor().getAsMention(), false) 760 | .addField("Channel", msg.getTextChannel().getAsMention(), false) 761 | .setColor(normalColor); 762 | 763 | for(var attach : msg.getAttachments()){ 764 | log.addField("File: " + attach.getFileName(), attach.getUrl(), false); 765 | } 766 | 767 | if(msg.getReferencedMessage() != null){ 768 | log.addField("Replying to", msg.getReferencedMessage().getAuthor().getAsMention() + " [Jump](" + msg.getReferencedMessage().getJumpUrl() + ")", false); 769 | } 770 | 771 | if(msg.getMentionedUsers().stream().anyMatch(u -> u.getIdLong() == 123539225919488000L)){ 772 | log.addField("Note", "thisisamention", false); 773 | } 774 | 775 | if(msg.getChannel().getIdLong() != testingChannel.getIdLong()){ 776 | logChannel.sendMessageEmbeds(log.build()).queue(); 777 | } 778 | 779 | //delete stray invites 780 | if(!isAdmin(msg.getAuthor()) && checkSpam(msg, false)){ 781 | return; 782 | } 783 | 784 | //delete non-art 785 | if(!isAdmin(msg.getAuthor()) && msg.getChannel().getIdLong() == artChannel.getIdLong() && msg.getAttachments().isEmpty()){ 786 | msg.delete().queue(); 787 | 788 | if(msg.getType() != MessageType.CHANNEL_PINNED_ADD){ 789 | try{ 790 | msg.getAuthor().openPrivateChannel().complete().sendMessage("Don't send messages without images in that channel.").queue(); 791 | }catch(Exception e1){ 792 | e1.printStackTrace(); 793 | } 794 | } 795 | } 796 | 797 | String text = msg.getContentRaw(); 798 | 799 | //schematic preview 800 | if((msg.getContentRaw().startsWith(ContentHandler.schemHeader) && msg.getAttachments().isEmpty()) || 801 | (msg.getAttachments().size() == 1 && msg.getAttachments().get(0).getFileExtension() != null && msg.getAttachments().get(0).getFileExtension().equals(Vars.schematicExtension))){ 802 | try{ 803 | Schematic schem = msg.getAttachments().size() == 1 ? contentHandler.parseSchematicURL(msg.getAttachments().get(0).getUrl()) : contentHandler.parseSchematic(msg.getContentRaw()); 804 | BufferedImage preview = contentHandler.previewSchematic(schem); 805 | String sname = schem.name().replace("/", "_").replace(" ", "_"); 806 | if(sname.isEmpty()) sname = "empty"; 807 | 808 | new File("cache").mkdir(); 809 | File previewFile = new File("cache/img_" + UUID.randomUUID() + ".png"); 810 | File schemFile = new File("cache/" + sname + "." + Vars.schematicExtension); 811 | Schematics.write(schem, new Fi(schemFile)); 812 | ImageIO.write(preview, "png", previewFile); 813 | 814 | EmbedBuilder builder = new EmbedBuilder().setColor(normalColor).setColor(normalColor) 815 | .setImage("attachment://" + previewFile.getName()) 816 | .setAuthor(msg.getAuthor().getName(), msg.getAuthor().getEffectiveAvatarUrl(), msg.getAuthor().getEffectiveAvatarUrl()).setTitle(schem.name()); 817 | 818 | if(!schem.description().isEmpty()) builder.setFooter(schem.description()); 819 | 820 | StringBuilder field = new StringBuilder(); 821 | 822 | for(ItemStack stack : schem.requirements()){ 823 | List emotes = guild.getEmotesByName(stack.item.name.replace("-", ""), true); 824 | Emote result = emotes.isEmpty() ? guild.getEmotesByName("ohno", true).get(0) : emotes.get(0); 825 | 826 | field.append(result.getAsMention()).append(stack.amount).append(" "); 827 | } 828 | builder.addField("Requirements", field.toString(), false); 829 | 830 | msg.getChannel().sendFile(schemFile).addFile(previewFile).setEmbeds(builder.build()).queue(); 831 | msg.delete().queue(); 832 | }catch(Throwable e){ 833 | if(schematicChannels.contains(msg.getChannel().getIdLong())){ 834 | msg.delete().queue(); 835 | try{ 836 | msg.getAuthor().openPrivateChannel().complete().sendMessage("Invalid schematic: " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " (" + e.getMessage() + ")")).queue(); 837 | }catch(Exception e2){ 838 | e2.printStackTrace(); 839 | } 840 | } 841 | //ignore errors 842 | } 843 | }else if(schematicChannels.contains(msg.getChannel().getIdLong()) && !isAdmin(msg.getAuthor())){ 844 | //delete non-schematics 845 | msg.delete().queue(); 846 | try{ 847 | msg.getAuthor().openPrivateChannel().complete().sendMessage("Only send valid schematics in the #schematics channel. You may send them either as clipboard text or as a schematic file.").queue(); 848 | }catch(Exception e){ 849 | e.printStackTrace(); 850 | } 851 | return; 852 | } 853 | 854 | if(!text.replace(prefix, "").trim().isEmpty()){ 855 | if(isAdmin(msg.getAuthor())){ 856 | boolean unknown = handleResponse(msg, adminHandler.handleMessage(text, msg), false); 857 | 858 | handleResponse(msg, handler.handleMessage(text, msg), !unknown); 859 | }else{ 860 | handleResponse(msg, handler.handleMessage(text, msg), true); 861 | } 862 | } 863 | }catch(Exception e){ 864 | Log.err(e); 865 | } 866 | } 867 | 868 | @Override 869 | public void onMessageUpdate(MessageUpdateEvent event){ 870 | var msg = event.getMessage(); 871 | 872 | if(isAdmin(msg.getAuthor()) || checkSpam(msg, true)){ 873 | return; 874 | } 875 | 876 | if((msg.getChannel().getIdLong() == artChannel.getIdLong()) && msg.getAttachments().isEmpty()){ 877 | msg.delete().queue(); 878 | try{ 879 | msg.getAuthor().openPrivateChannel().complete().sendMessage("Don't send messages without images in that channel.").queue(); 880 | }catch(Exception e){ 881 | e.printStackTrace(); 882 | } 883 | } 884 | } 885 | 886 | @Override 887 | public void onGuildMemberJoin(GuildMemberJoinEvent event){ 888 | event.getUser().openPrivateChannel().complete().sendMessage( 889 | """ 890 | **Welcome to the Mindustry Discord.** 891 | 892 | *Make sure you read #rules and the channel topics before posting.* 893 | 894 | **View a list of all frequently answered questions here:** 895 | 896 | """ 897 | ).queue(); 898 | 899 | joinChannel 900 | .sendMessageEmbeds(new EmbedBuilder() 901 | .setAuthor(event.getUser().getName(), event.getUser().getAvatarUrl(), event.getUser().getAvatarUrl()) 902 | .addField("User", event.getUser().getAsMention(), false) 903 | .addField("ID", "`" + event.getUser().getId() + "`", false) 904 | .setColor(normalColor).build()) 905 | .queue(); 906 | } 907 | 908 | void sendWarnings(Message msg, User user){ 909 | var list = getWarnings(user); 910 | text(msg, "User '@' has **@** @.\n@", user.getName(), list.size, list.size == 1 ? "warning" : "warnings", 911 | list.map(s -> { 912 | String[] split = s.split(":::"); 913 | long time = Long.parseLong(split[0]); 914 | String warner = split.length > 1 ? split[1] : null, reason = split.length > 2 ? split[2] : null; 915 | return "- `" + fmt.format(new Date(time)) + "`: Expires in " + (warnExpireDays - Duration.ofMillis((System.currentTimeMillis() - time)).toDays()) + " days" + 916 | (warner == null ? "" : "\n ↳ *From:* " + warner) + 917 | (reason == null ? "" : "\n ↳ *Reason:* " + reason); 918 | }).toString("\n")); 919 | } 920 | 921 | public void text(MessageChannel channel, String text, Object... args){ 922 | channel.sendMessage(Strings.format(text, args)).queue(); 923 | } 924 | 925 | public void text(Message message, String text, Object... args){ 926 | text(message.getChannel(), text, args); 927 | } 928 | 929 | public void info(MessageChannel channel, String title, String text, Object... args){ 930 | channel.sendMessageEmbeds(new EmbedBuilder().addField(title, Strings.format(text, args), true).setColor(normalColor).build()).queue(); 931 | } 932 | 933 | public void infoDesc(MessageChannel channel, String title, String text, Object... args){ 934 | channel.sendMessageEmbeds(new EmbedBuilder().setTitle(title).setDescription(Strings.format(text, args)).setColor(normalColor).build()).queue(); 935 | } 936 | 937 | /** Sends an error, deleting the base message and the error message after a delay. */ 938 | public void errDelete(Message message, String text, Object... args){ 939 | errDelete(message, "Error", text, args); 940 | } 941 | 942 | /** Sends an error, deleting the base message and the error message after a delay. */ 943 | public void errDelete(Message message, String title, String text, Object... args){ 944 | message.getChannel().sendMessageEmbeds(new EmbedBuilder() 945 | .addField(title, Strings.format(text, args), true).setColor(errorColor).build()) 946 | .queue(result -> result.delete().queueAfter(messageDeleteTime, TimeUnit.MILLISECONDS)); 947 | 948 | //delete base message too 949 | message.delete().queueAfter(messageDeleteTime, TimeUnit.MILLISECONDS); 950 | } 951 | 952 | private Seq getWarnings(User user){ 953 | var list = prefs.getArray("warning-list-" + user.getIdLong()); 954 | //remove invalid warnings 955 | list.removeAll(s -> { 956 | String[] split = s.split(":::"); 957 | return Duration.ofMillis((System.currentTimeMillis() - Long.parseLong(split[0]))).toDays() >= warnExpireDays; 958 | }); 959 | 960 | return list; 961 | } 962 | 963 | private Jval fixJval(Jval val){ 964 | if(val.isArray()){ 965 | Seq list = val.asArray().copy(); 966 | for(Jval child : list){ 967 | if(child.isObject() && (child.has("item")) && child.has("amount")){ 968 | val.asArray().remove(child); 969 | val.asArray().add(Jval.valueOf(child.getString("item", child.getString("liquid", "")) + "/" + child.getInt("amount", 0))); 970 | }else{ 971 | fixJval(child); 972 | } 973 | } 974 | }else if(val.isObject()){ 975 | Seq keys = val.asObject().keys().toArray(); 976 | 977 | for(String key : keys){ 978 | Jval child = val.get(key); 979 | if(child.isObject() && (child.has("item")) && child.has("amount")){ 980 | val.remove(key); 981 | val.add(key, Jval.valueOf(child.getString("item", child.getString("liquid", "")) + "/" + child.getInt("amount", 0))); 982 | }else{ 983 | fixJval(child); 984 | } 985 | } 986 | } 987 | 988 | return val; 989 | } 990 | 991 | boolean isAdmin(User user){ 992 | var member = guild.retrieveMember(user).complete(); 993 | return member != null && member.getRoles().stream().anyMatch(role -> role.getName().equals("Developer") || role.getName().equals("Moderator") || role.getName().equals("\uD83D\uDD28 \uD83D\uDD75️\u200D♂️")); 994 | } 995 | 996 | String replaceCyrillic(String in){ 997 | StringBuilder out = new StringBuilder(in.length()); 998 | for(int i = 0; i < in.length(); i++){ 999 | char c = in.charAt(i); 1000 | int index = cyrillicFrom.indexOf(c); 1001 | if(index == -1){ 1002 | out.append(c); 1003 | }else{ 1004 | out.append(cyrillicTo.charAt(index)); 1005 | } 1006 | } 1007 | return out.toString(); 1008 | } 1009 | 1010 | boolean checkSpam(Message message, boolean edit){ 1011 | 1012 | if(message.getChannel().getType() != ChannelType.PRIVATE){ 1013 | Seq mentioned = 1014 | //ignore reply messages, bots don't use those 1015 | message.getReferencedMessage() != null ? new Seq<>() : 1016 | //get all mentioned members and roles in one list 1017 | Seq.with(message.getMentionedMembers()).map(IMentionable::getAsMention).add(Seq.with(message.getMentionedRoles()).map(IMentionable::getAsMention)); 1018 | 1019 | var data = data(message.getAuthor()); 1020 | String content = message.getContentStripped().toLowerCase(Locale.ROOT); 1021 | 1022 | //go through every ping individually 1023 | for(var ping : mentioned){ 1024 | if(data.idsPinged.add(ping) && data.idsPinged.size >= pingSpamLimit){ 1025 | String banMessage = "Banned for spamming member pings in a row. If you believe this was in error, file an issue on the CoreBot Github (https://github.com/Anuken/CoreBot/issues) or contact a moderator."; 1026 | Log.info("Autobanning @ for spamming @ pings in a row.", message.getAuthor().getName() + "#" + message.getAuthor().getId(), data.idsPinged.size); 1027 | alertsChannel.sendMessage(message.getAuthor().getAsMention() + " **has been auto-banned for pinging " + pingSpamLimit + " unique members in a row!**").queue(); 1028 | 1029 | Runnable banMember = () -> message.getGuild().ban(message.getAuthor(), 1, banMessage).queue(); 1030 | 1031 | try{ 1032 | message.getAuthor().openPrivateChannel().complete().sendMessage(banMessage).queue(done -> banMember.run(), failed -> banMember.run()); 1033 | }catch(Exception e){ 1034 | //can fail to open PM channel sometimes. 1035 | banMember.run(); 1036 | } 1037 | } 1038 | } 1039 | 1040 | if(mentioned.isEmpty()){ 1041 | data.idsPinged.clear(); 1042 | } 1043 | 1044 | //check for consecutive links 1045 | if(!edit && linkPattern.matcher(content).find()){ 1046 | 1047 | if(content.equals(data.lastLinkMessage) && !message.getChannel().getId().equals(data.lastLinkChannelId)){ 1048 | Log.warn("User @ just spammed a link in @ (message: @): '@'", message.getAuthor().getName(), message.getChannel().getName(), message.getId(), content); 1049 | 1050 | //only start deleting after 2 posts 1051 | if(data.linkCrossposts >= 1){ 1052 | alertsChannel.sendMessage( 1053 | message.getAuthor().getAsMention() + 1054 | " **is spamming a link** in " + message.getTextChannel().getAsMention() + 1055 | ":\n\n" + message.getContentRaw() 1056 | ).queue(); 1057 | 1058 | message.delete().queue(); 1059 | message.getAuthor().openPrivateChannel().complete().sendMessage("You have posted a link several times. Do not send any similar messages, or **you will be auto-banned.**").queue(); 1060 | } 1061 | 1062 | //4 posts = ban 1063 | if(data.linkCrossposts ++ >= 3){ 1064 | Log.warn("User @ (@) has been auto-banned after spamming link messages.", message.getAuthor().getName(), message.getAuthor().getAsMention()); 1065 | 1066 | alertsChannel.sendMessage(message.getAuthor().getAsMention() + " **has been auto-banned for spam-posting links!**").queue(); 1067 | message.getGuild().ban(message.getAuthor(), 1, "[Auto-Ban] Spam-posting links. If you are not a bot or spammer, please report this at https://github.com/Anuken/CoreBot/issues immediately!").queue(); 1068 | } 1069 | } 1070 | 1071 | data.lastLinkMessage = content; 1072 | data.lastLinkChannelId = message.getChannel().getId(); 1073 | }else{ 1074 | data.linkCrossposts = 0; 1075 | data.lastLinkMessage = null; 1076 | data.lastLinkChannelId = null; 1077 | } 1078 | 1079 | //zwj 1080 | content = content.replaceAll("\u200B", "").replaceAll("\u200D", ""); 1081 | 1082 | if(invitePattern.matcher(content).find()){ 1083 | Log.warn("User @ just sent a discord invite in @.", message.getAuthor().getName(), message.getChannel().getName()); 1084 | message.delete().queue(); 1085 | message.getAuthor().openPrivateChannel().complete().sendMessage("Do not send invite links in the Mindustry Discord server! Read the rules.").queue(); 1086 | return true; 1087 | }else if((badWordPattern.matcher(content).find() || badWordPattern.matcher(replaceCyrillic(content)).find())){ 1088 | alertsChannel.sendMessage( 1089 | message.getAuthor().getAsMention() + 1090 | " **has sent a message with inaproppriate language** in " + message.getTextChannel().getAsMention() + 1091 | ":\n\n" + message.getContentRaw() 1092 | ).queue(); 1093 | 1094 | message.delete().queue(); 1095 | message.getAuthor().openPrivateChannel().complete().sendMessage("uou have been timed out for " + naughtyTimeoutMins + 1096 | " minutes for using an unacceptable word in `#" + message.getChannel().getName() + "`.\nYour message:\n\n" + message.getContentRaw()).queue(); 1097 | message.getMember().timeoutFor(Duration.ofMinutes(naughtyTimeoutMins)).queue(); 1098 | 1099 | return true; 1100 | }else if(containsScamLink(message)){ 1101 | Log.warn("User @ just sent a potential scam message in @: '@'", message.getAuthor().getName(), message.getChannel().getName(), message.getContentRaw()); 1102 | 1103 | int count = data.scamMessages ++; 1104 | 1105 | alertsChannel.sendMessage( 1106 | message.getAuthor().getAsMention() + 1107 | " **has sent a potential scam message** in " + message.getTextChannel().getAsMention() + 1108 | ":\n\n" + message.getContentRaw() 1109 | ).queue(); 1110 | 1111 | message.delete().queue(); 1112 | message.getAuthor().openPrivateChannel().complete().sendMessage("Your message has been flagged as a potential scam. Do not send any similar messages, or **you will be auto-banned.**").queue(); 1113 | 1114 | if(count >= scamAutobanLimit - 1){ 1115 | Log.warn("User @ (@) has been auto-banned after @ scam messages.", message.getAuthor().getName(), message.getAuthor().getAsMention(), count + 1); 1116 | 1117 | alertsChannel.sendMessage(message.getAuthor().getAsMention() + " **has been auto-banned for posting " + scamAutobanLimit + " scam messages in a row!**").queue(); 1118 | message.getGuild().ban(message.getAuthor(), 0, "[Auto-Ban] Posting several potential scam messages in a row. If you are not a bot or spammer, please report this at https://github.com/Anuken/CoreBot/issues immediately!").queue(); 1119 | } 1120 | 1121 | return true; 1122 | }else{ 1123 | //non-consecutive scam messages don't count 1124 | data.scamMessages = 0; 1125 | } 1126 | 1127 | } 1128 | return false; 1129 | } 1130 | 1131 | boolean handleResponse(Message msg, CommandResponse response, boolean logUnknown){ 1132 | if(response.type == ResponseType.unknownCommand){ 1133 | if(logUnknown){ 1134 | errDelete(msg, "Error", "Unknown command. Type !help for a list of commands."); 1135 | } 1136 | return false; 1137 | }else if(response.type == ResponseType.manyArguments || response.type == ResponseType.fewArguments){ 1138 | if(response.command.params.length == 0){ 1139 | errDelete(msg, "Invalid arguments.", "Usage: @@", prefix, response.command.text); 1140 | }else{ 1141 | errDelete(msg, "Invalid arguments.", "Usage: @@ *@*", prefix, response.command.text, response.command.paramText); 1142 | } 1143 | } 1144 | return true; 1145 | } 1146 | 1147 | boolean containsScamLink(Message message){ 1148 | String content = message.getContentRaw().toLowerCase(Locale.ROOT); 1149 | 1150 | //some discord-related keywords are never scams (at least, not from bots) 1151 | if(notScamPattern.matcher(content).find()){ 1152 | return false; 1153 | } 1154 | 1155 | // Regular check 1156 | if(scamPattern.matcher(content.replace("\n", " ")).find()){ 1157 | return true; 1158 | } 1159 | 1160 | // Extracts the urls of the message 1161 | List urls = urlPattern.matcher(content).results().map(MatchResult::group).toList(); 1162 | 1163 | for(String url : urls){ 1164 | // Gets the domain and splits its different parts 1165 | String[] rawDomain = url 1166 | .replace("https://", "") 1167 | .replace("http://", "") 1168 | .split("/")[0] 1169 | .split("\\."); 1170 | 1171 | // Gets rid of the subdomains 1172 | rawDomain = Arrays.copyOfRange(rawDomain, Math.max(rawDomain.length - 2, 0), rawDomain.length); 1173 | 1174 | // Re-assemble 1175 | String domain = String.join(".", rawDomain); 1176 | 1177 | // Matches slightly altered links 1178 | if(!trustedDomains.contains(domain) && trustedDomains.stream().anyMatch(genuine -> Strings.levenshtein(genuine, domain) <= 2)){ 1179 | return true; 1180 | } 1181 | } 1182 | 1183 | return false; 1184 | } 1185 | 1186 | UserData data(User user){ 1187 | return userData.get(user.getId(), UserData::new); 1188 | } 1189 | 1190 | static class UserData{ 1191 | /** consecutive scam messages sent */ 1192 | int scamMessages; 1193 | /** last message that contained any link */ 1194 | @Nullable String lastLinkMessage; 1195 | /** channel ID of last link posted */ 1196 | @Nullable String lastLinkChannelId; 1197 | /** link cross-postings in a row */ 1198 | int linkCrossposts; 1199 | /** all members pinged in consecutive messages */ 1200 | ObjectSet idsPinged = new ObjectSet<>(); 1201 | } 1202 | } 1203 | --------------------------------------------------------------------------------