├── .github ├── FUNDING.yml ├── auto_assign.yml └── workflows │ └── auto-assign.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── config └── checkstyle │ ├── checkstyle.xml │ └── suppressions.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── icon.png ├── runelite-plugin.properties ├── settings.gradle └── src ├── main ├── java │ └── com │ │ └── botdetector │ │ ├── BotDetectorConfig.java │ │ ├── BotDetectorPlugin.java │ │ ├── events │ │ └── BotDetectorPanelActivated.java │ │ ├── http │ │ ├── BotDetectorClient.java │ │ ├── UnauthorizedTokenException.java │ │ └── ValidationException.java │ │ ├── model │ │ ├── AuthToken.java │ │ ├── AuthTokenPermission.java │ │ ├── AuthTokenType.java │ │ ├── CaseInsensitiveString.java │ │ ├── FeedbackPredictionLabel.java │ │ ├── FeedbackValue.java │ │ ├── PlayerSighting.java │ │ ├── PlayerStats.java │ │ ├── PlayerStatsType.java │ │ ├── Prediction.java │ │ └── StatsCommandDetailLevel.java │ │ └── ui │ │ ├── BotDetectorPanel.java │ │ ├── Icons.java │ │ ├── NameAutocompleter.java │ │ ├── PanelFontType.java │ │ └── components │ │ ├── ComboBoxSelfTextTooltipListRenderer.java │ │ └── JLimitedTextArea.java └── resources │ └── com │ └── botdetector │ ├── bot-icon.png │ └── ui │ ├── discord.png │ ├── error.png │ ├── github.png │ ├── loading_spinner_darker.gif │ ├── patreon.png │ ├── strong_warning.png │ ├── twitter.png │ ├── warning.png │ └── web.png └── test └── java └── com └── botdetector └── BotDetectorPluginTest.java /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: # bot_detector 4 | -------------------------------------------------------------------------------- /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | # Set to true to add reviewers to pull requests 2 | addReviewers: true 3 | 4 | # Set to true to add assignees to pull requests 5 | addAssignees: true 6 | 7 | # A list of reviewers to be added to pull requests (GitHub user name) 8 | reviewers: 9 | - extreme4all 10 | - ThorntonMatthewD 11 | - Ferrariic 12 | 13 | # A number of reviewers added to the pull request 14 | # Set 0 to add all the reviewers (default: 0) 15 | numberOfReviewers: 0 16 | 17 | # A list of assignees, overrides reviewers if set 18 | assignees: 19 | - author 20 | 21 | # A number of assignees to add to the pull request 22 | # Set to 0 to add all of the assignees. 23 | # Uses numberOfReviewers if unset. 24 | # numberOfAssignees: 2 25 | 26 | # A list of keywords to be skipped the process that add reviewers if pull requests include it 27 | # skipKeywords: 28 | # - wip 29 | # test where thefuck does this go to -------------------------------------------------------------------------------- /.github/workflows/auto-assign.yml: -------------------------------------------------------------------------------- 1 | name: 'Auto Assign' 2 | on: 3 | pull_request: 4 | types: [opened, ready_for_review] 5 | 6 | jobs: 7 | add-reviews: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: kentaro-m/auto-assign-action@v1.1.2 11 | with: 12 | configuration-path: '.github/auto_assign.yml' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build 3 | .idea/ 4 | .project 5 | .settings/ 6 | .classpath 7 | nbactions.xml 8 | nb-configuration.xml 9 | nbproject/ 10 | spam_log.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2021, Ferrariic 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bot Detector Plugin 2 | [![GitHub](https://img.shields.io/github/license/Bot-Detector/bot-detector)](https://github.com/Bot-detector) 3 | [![Plugin Rank](https://img.shields.io/endpoint?url=https://api.runelite.net/pluginhub/shields/rank/plugin/bot-detector)](https://runelite.net/plugin-hub) 4 | [![Plugin Installs](https://img.shields.io/endpoint?url=https://api.runelite.net/pluginhub/shields/installs/plugin/bot-detector)](https://runelite.net/plugin-hub) 5 | [![Website](https://img.shields.io/website?down_color=lightgrey&down_message=down&up_color=green&up_message=up&url=https%3A%2F%2Fosrsbotdetector.com%2F)](https://osrsbotdetector.com) 6 | [![Discord](https://img.shields.io/discord/817916789668708384?label=discord)](https://discord.com/invite/JCAGpcjbfP) 7 | [![Twitter Follow](https://img.shields.io/twitter/follow/osrsbotdetector?style=social)](https://x.com/osrsbotdetector) 8 | 9 | Identifies bots by sending nearby players' information to a third-party machine learning algorithm, trained with data provided by Jagex Moderators. 10 | 11 | ![chart](https://user-images.githubusercontent.com/5789682/115799298-296f7100-a3a6-11eb-863d-3cf7e9d32234.png) 12 | 13 | # Connect With Us! 14 | | Site | Link | 15 | |:-------------|:--------------------------------------| 16 | | Discord | https://discord.com/invite/JCAGpcjbfP | 17 | | Our Website | https://www.osrsbotdetector.com | 18 | | Patreon | https://www.patreon.com/Ferrariic | 19 | | GitHub | https://github.com/Bot-detector | 20 | 21 | # Getting Started 22 | ## Installing the Plugin 23 | 1. Open RuneLite 24 | 2. Navigate to the Configuration wrench on the Top Right of the Sidebar. 25 | 3. Click on Plugin Hub at the very bottom. 26 | 4. Type in Bot Detector in the search bar. 27 | 5. Click Install 28 | 6. Accept the warning message. (See the FAQ below if you have questions about this) 29 | 7. Done! 30 | 31 | ## Using the Plugin 32 | ### The Plugin's Settings 33 | | Setting Type | Setting Name | Description | 34 | |:-------------------|:-------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 35 | | Upload Settings | Anonymous Uploading | When enabled, your username will not be sent with your uploads. You also cannot manually flag players. | 36 | | Upload Settings | Send Names Only After Logout | Recommended for players with low bandwidth. | 37 | | Upload Settings | Attempt Send on Close | Attempts to upload names when closing Runelite while being logged in. Take note that enabling this option may cause the client to take a long time to close if our servers are being unresponsive. | 38 | | Upload Settings | Send Names Every *x* mins | Determines how often the information collected by the plugin is flushed to our servers. The maximum rate is once per 5 minutes. | 39 | | Panel Settings | Prediction Autocomplete | Allows for prediction auto-completion for dialogue box entry. | 40 | | Panel Settings | Show Breakdown in Special Cases | Displays the Prediction Breakdown when predicting certain types of accounts, such as ones with 'Stats Too Low'. | 41 | | Panel Settings | Show Feedback Textbox | Adds a textbox to the prediction feedback panel, so you can explain your choice in up to 250 characters. Make sure you type your feedback *before* you make your choice! | 42 | | Panel Settings | Panel Default Stats Tab | Sets panel default stats tab when the Plugin turns on. | 43 | | Panel Settings | Panel Font Size | Sets the font size of most of the Plugin's Panel elements. | 44 | | 'Predict' Settings | Right-click 'Predict' Players | Allows you to right-click predict players in the game world, instead of having to type their name in the Plugin's panel manually. | 45 | | 'Predict' Settings | Right-click 'Predict' Menus | Allows you to right-click predict players in interfaces. | 46 | | 'Predict' Settings | 'Predict' on Right-click 'Report' | If you right-click Report someone via Jagex's official in-game report system, the player will be automatically predicted in the Plugin's Panel. | 47 | | 'Predict' Settings | 'Predict' Copy Name to Clipboard | Copies the predicted player's name to your clipboard when right-click predicting. | 48 | | 'Predict' Settings | 'Predict' Default Color | If set, highlights unflagged/unfeedbacked players' 'Predict' option in the given color so that you can easily spot it on the in-game menu. | 49 | | 'Predict' Settings | 'Predict' Voted/Flagged Color | If set, highlights flagged/feedbacked players' 'Predict' option in the given color so that you can easily spot it on the in-game menu. | 50 | | 'Predict' Settings | Apply Colors to 'Report' | If set, applies the above 'Predict' color options to the in-game 'Report' option as well.
⚠️ May cause issues with other plugins that rely on the 'Report' option being unchanged.⚠️ | 51 | | Other Settings | Enable Chat Status Messages | Inserts chat messages in your chatbox to inform you about your uploads being sent. | 52 | | Other Settings | '!bdstats' Chat Command Detail Level | Enable processing the '!bdstats' command when it appears in the chatbox, which will fetch the message author's plugin stats and display them. Disable to reduce spam. | 53 | 54 | ### ⚠️ Anonymous Mode ⚠️ 55 | **Anonymous Mode** is enabled by default, which means your username will not be sent with your uploads. However, we cannot tally your uploads, and you will not be able to manually flag players. 56 | 57 | ### Understanding The Plugin Panel 58 | #### The Player Statistics Panel 59 | ![stats_panel](https://user-images.githubusercontent.com/45152844/123029711-88a22f80-d3af-11eb-92a9-99f7fde75506.png) 60 | 61 | | Label | Description | 62 | |:----------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------| 63 | | Plugin Version | Indicates the Bot Detector Plugin version you are currently running. | 64 | | Current Uploads | Tallies the number of uploads the plugin has performed for the current RuneLite session. | 65 | | Total Uploads | Tallies the number of unique Names you've uploaded so far. | 66 | | Feedback Sent | Tallies the number of prediction feedbacks you've sent to us so far. | 67 | | Possible Bans | Tallies the number of unique Names you've uploaded that may have been banned. This is usually because a given Name stopped appearing on the official Hiscores. | 68 | | Confirmed Bans | Tallies the number of unique Names you've uploaded confirmed to have been banned by Jagex Moderators. | 69 | | Incorrect Flags | **(Manual Tab Only)** Tallies the number of unique Names you've flagged as bots that ended up being confirmed as real players by Jagex Moderators. | 70 | | Flag Accuracy | **(Manual Tab Only)** Indicates your personal accuracy when manually flagging, determined by the previous two fields. | 71 | 72 | **Note for ⚠️ Anonymous Users ⚠️**: Except for *Plugin Version* and *Current Uploads*, all of these fields are disabled. 73 | 74 | #### The Prediction Panels 75 | ![prediction_panels](https://user-images.githubusercontent.com/45152844/130818338-d9bb78b6-e111-418d-9b3b-bc4025511e3f.png) 76 | 77 | | Panel | Description | 78 | |:---------------------|:------------------------------------------------------------------------------------------------------------------------------------------------| 79 | | Primary Prediction | Displays the primary prediction label and confidence for the predicted player. | 80 | | Prediction Breakdown | Displays a breakdown of confidences for other labels our algorithm considered. These are not considered in our manual reviews. | 81 | | Prediction Feedback | Allows the user to send in a feedback for the **Primary Prediction** and choose the most appropriate available label. | 82 | | Manual Flagging | Allows the user to upload the most recent sighting for the predicted player with an extra flag to tell us to pay more attention to that player. | 83 | 84 | **Note for ⚠️ Anonymous Users ⚠️**: The *Manual Flagging* panel is disabled. 85 | 86 | ### Understanding How the Plugin Works 87 | You, as a plugin user, automatically and passively send some information about every player you come across in-game to our servers. 88 | The information sent, all collected at the time of the player spawning on screen, includes: 89 | - The display name of the player 90 | - The exact world location of the player 91 | - The world number 92 | - The exact time of the sighting 93 | - The visibly equipped gear of the player 94 | - The GE gear value of the player 95 | 96 | We use this information to train one of our many machine learning models to identify patterns in accounts. 97 | Example: Smithing bots are accounts which have a very high Smithing level, wear no gear, and are typically seen in Edgeville and at the Blast Furnace. 98 | - We take their levels: High Smithing to Total ratio 99 | - We take their locations: Edgeville & Blast Furnace 100 | - We take their gear: Nothing 101 | - We take their timestamp: Usually on between 16:00 UTC and 22:00 UTC 102 | - Then we build a 'pattern' or label for those accounts. In this case, this would be labeled as `Smithing_bot`. 103 | 104 | Note: We usually have to gather data for several weeks or months in order to get enough viable training data! That way our estimations are quite close to the actual status of the accounts we are attempting to evaluate. 105 | 106 | After building enough accounts into a label, we then test the label on a variety of Machine Learning Models, and choose the best one to be used in evaluation. Here is an example output below: 107 | 108 | ![labels](https://user-images.githubusercontent.com/45152844/123032193-893cc500-d3b3-11eb-86b7-9e371c1734bd.png) 109 | 110 | Once we are happy with these results, we will then output the labels to you whenever you use the Plugin to predict a player. 111 | 112 | We will then await your feedback to see if our label is accurate enough to be used viably. If the label scores poorly, it will be pulled from the plugin and be reevaluated. 113 | 114 | Once we are confident a given label performs well, we search our database for all accounts matching that label prediction with a very high confidence (typically above 90%) and send those names to Jagex for evaluation. 115 | 116 | Jagex then lets us know if they're banned accounts or real players. If they're banned, they're marked as confirmed bans. If they're real players, they're marked as confirmed players. We then retain and reinforce the model from these confirmed ban/player marks. 117 | 118 | # Frequently Asked Questions 119 | ## Is this Plugin malicious? 120 | The Plugin can be installed through RuneLite's [Plugin Hub](https://runelite.net/plugin-hub) system, which is curated by the RuneLite developers to not allow anything that's even mildly suspect. If you have any doubt about the Plugin's inner workings, all of its components' source codes are available in this GitHub! You may also contact us through our Discord server if you have any questions you'd like answered. 121 | 122 | ## Why do I need a RuneLite Client/Plugin to capture OSRS names? 123 | RuneLite's APIs allow us to easily collect nearby players' location, gear and gear cost information, all of which prove to be viable in detecting potential bots. While we could simply scrape the entirety of the Runescape Hiscores for "bot-like" accounts, it would be quite inefficient. The combined information of active player names along with the aforementioned collected data gives our system much more power, especially for differentiating between bots and alt accounts. 124 | 125 | ## When I try to install the Plugin, I get a warning about it needing my IP address. What is this about? 126 | That is simply how the Internet works. As the Plugin needs to connect to our servers to send and receive data, your IP comes along for the ride. We do not log your IP address and **Anonymous Mode** is enabled by default, which means that we will not receive your Runescape Name with the data you send. You may however turn off the option yourself in the Plugin's settings to access extra features. 127 | 128 | ## I installed the Plugin, and it works, but it's not tallying my uploads? I also can't manually flag players as bots! 129 | **Anonymous Mode** is enabled by default. If it is enabled, we cannot associate your uploads with your Runescape Name. If you wish to tally your uploads, you'll need to disable Anonymous Mode in the Plugin's settings. In addition, we've disabled manual flagging for Anonymous users. 130 | 131 | ## I changed my Runescape Name and my tallies got reset! 132 | The only way we can easily attribute uploads is through the sender's Runescape Name. If you have a way of confirming to us your previous name, come talk to us on our Discord server where we may be able to transfer your tallies. If you've used our account linking feature on our Discord with your previous name, this process should be even easier for us. 133 | 134 | ## How does Bot Detector identifies potential bots? 135 | We use several machine learning algorithms, and variations within, in order to identify potential botters and legitimate players. We do realise that sometimes we can get it wrong, but thankfully by your feedback, as well as Jagex's, we are able to improve over time! 136 | 137 | ## How accurate is Bot Detector at identifying bots? 138 | We use player feedback to measure the perceived accuracy of our systems in addition to Jagex's confirming bots and real players for us. Lately (as of June 22nd 2021), we have increased our accuracy from 80% to 90% in a short period of time, as shown below. 139 | 140 | ![feedback](https://user-images.githubusercontent.com/45152844/123023792-07926a80-d3a6-11eb-8851-6ceefc2568a2.png) 141 | 142 | ## How can I contribute to the project? 143 | There are several ways you can contribute to the Bot Detector Project: 144 | - Install and use this Plugin: As we increase our number of users, we are able to better train our model and detect bots, thanks to the extra data we can collect. 145 | - Donate to our Patreon: All that data has to go somewhere! All money donated through our Patreon goes towards our server upkeep. 146 | - Join the Development Team: Come join us on our Discord server and ask an admin for the **New Developer** rank, we'll get you set up! 147 | 148 | ## Do I have to manually predict and flag players for them to be sent to the Bot Detector server? 149 | No, the plugin will grab the data for every player that renders on your screen passively. Manually flagging a player uploads the same data as the passive collection does, but tells us to pay more attention to that data when training our models. 150 | 151 | ## Does this replace manual in-game reporting? 152 | No, reporting players in-game sends a ton of data which we cannot collect directly to Jagex, we urge you to continue using that feature whenever you are confident someone is botting! Also, please keep in mind that even if our plugin predicts a player as a bot, it does not mean it is true! Investigate the players yourself before making your decision to report. Please be kind to these players and do not falsely accuse or harrass them based upon a prediction that could be faulty. 153 | 154 | ## I checked myself with the Plugin and my Primary Prediction says I'm a bot! Will I get banned? 155 | No, at least, not by that alone. We take into account not only the Machine Learning's response but also review the names manually prior to sending to Jagex. All we send over are the names of suspected bot accounts and are treated no differently than any other user would whenever submitting leads to tipoff@jagex.com in Jagex's decision process. If you know you didn't bot, you have nothing to worry about. 156 | 157 | ## My Primary Prediction says I'm a Real Player, but it isn't 100% confident and includes several bot labels in the breakdown. What is going on? 158 | The model's ability to evaluate a prediction must take into account a variety of factors. Stats, gear, location, and more are used to determine a predictive score. It is very unlikely, and uncommon, to be rated as a 100% Real Player. Usually if the primary prediction label is at any Real Player percent, the account will not show up in our manual reviews. 159 | 160 | ## Can you ban accounts? 161 | No, as said before, all we do is send over lists of suspicious accounts as you would through tipoff@jagex.com. 162 | 163 | ## Can you unban accounts? 164 | No, for that you will have to file an appeal with [Jagex](https://support.runescape.com/hc/en-gb/articles/115002238729-Appeal-an-offence). 165 | 166 | ## I've seen some of these cool heatmaps, how can I generate more of them? 167 | Generating a heatmap is currently only available to our Patrons. You could ask one to generate a map for you, but we do ask for your consideration in supporting us on our Patreon. If you do, you will have access to an exclusive channel on our Discord server where you can execute the heatmap command at any time. Our Patrons are what make this all possible! 168 | 169 | ## This FAQ did not answer my questions. 170 | Come talk to us on our Discord server, look for the **Plugin Issues** and **Plugin Discussion** channels. 171 | 172 | # Thanks for your continued support!
❤️ The Bot Detector Team ❤️ 173 | ![wyvern_extreme_pogging][w_e_p]![wyvern_extreme_pogging][w_e_p]![wyvern_extreme_pogging][w_e_p]![wyvern_extreme_pogging][w_e_p]![wyvern_extreme_pogging][w_e_p] 174 | 175 | [w_e_p]: https://user-images.githubusercontent.com/45152844/116952387-8ac1fa80-ac58-11eb-8a31-5fe0fc6f5f88.gif 176 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'checkstyle' 4 | //id "com.github.johnrengelman.shadow" version "5.2.0" 5 | } 6 | 7 | repositories { 8 | mavenLocal() 9 | maven { 10 | url = 'https://repo.runelite.net' 11 | } 12 | mavenCentral() 13 | } 14 | 15 | def runeLiteVersion = 'latest.release' 16 | 17 | dependencies { 18 | compileOnly group: 'net.runelite', name:'client', version: runeLiteVersion 19 | 20 | compileOnly 'org.projectlombok:lombok:1.18.4' 21 | annotationProcessor 'org.projectlombok:lombok:1.18.4' 22 | 23 | testImplementation 'junit:junit:4.12' 24 | testImplementation group: 'net.runelite', name:'client', version: runeLiteVersion 25 | testImplementation group: 'net.runelite', name:'jshell', version: runeLiteVersion 26 | } 27 | 28 | group = 'com.botdetector' 29 | version = '1.3.9.6' 30 | sourceCompatibility = '1.8' 31 | 32 | tasks.withType(JavaCompile) { 33 | options.encoding = 'UTF-8' 34 | } 35 | 36 | // https://github.com/dillydill123/inventory-setups/blob/2643da8f1952e30be554d0d601397daced9c3120/build.gradle#L39 37 | task createProperties(dependsOn: processResources) { 38 | doLast { 39 | new File("$buildDir/resources/main/com/botdetector/version.txt").text = "version=$project.version" 40 | } 41 | } 42 | 43 | classes { 44 | dependsOn createProperties 45 | } 46 | 47 | // java -DBotDetectorAPIPath=https://www.osrsbotdetector.com/dev -jar JARFILE 48 | 49 | /* 50 | shadowJar { 51 | from sourceSets.test.output 52 | configurations = [project.configurations.testRuntimeClasspath] 53 | manifest { 54 | attributes "Main-Class": "com.botdetector.BotDetectorPluginTest" 55 | } 56 | } 57 | */ 58 | -------------------------------------------------------------------------------- /config/checkstyle/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /config/checkstyle/suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 27 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bot-detector/bot-detector/bc410b29edc2545fb8c25941ebbeabcd74b86aef/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bot-detector/bot-detector/bc410b29edc2545fb8c25941ebbeabcd74b86aef/icon.png -------------------------------------------------------------------------------- /runelite-plugin.properties: -------------------------------------------------------------------------------- 1 | displayName=Bot Detector 2 | author=Ferrariic 3 | support=https://github.com/Bot-detector/bot-detector 4 | description=Identifies bots by sending nearby player information to a third-party machine learning algorithm, trained with data provided by Jagex Moderators. 5 | tags= bot, report, machine, learning 6 | plugins=com.botdetector.BotDetectorPlugin 7 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'botdetector' 2 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/BotDetectorConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | package com.botdetector; 27 | 28 | import com.botdetector.model.PlayerStatsType; 29 | import com.botdetector.model.StatsCommandDetailLevel; 30 | import com.botdetector.ui.PanelFontType; 31 | import java.awt.Color; 32 | import net.runelite.client.config.Config; 33 | import net.runelite.client.config.ConfigGroup; 34 | import net.runelite.client.config.ConfigItem; 35 | import net.runelite.client.config.ConfigSection; 36 | import net.runelite.client.config.Range; 37 | import net.runelite.client.config.Units; 38 | 39 | @ConfigGroup(BotDetectorConfig.CONFIG_GROUP) 40 | public interface BotDetectorConfig extends Config 41 | { 42 | String CONFIG_GROUP = "botdetector"; 43 | String ONLY_SEND_AT_LOGOUT_KEY = "sendAtLogout"; 44 | String AUTO_SEND_MINUTES_KEY = "autoSendMinutes"; 45 | String ADD_PREDICT_PLAYER_OPTION_KEY = "addDetectOption"; // I know it says detect, don't change it. 46 | String ADD_PREDICT_MENU_OPTION_KEY = "addPlayerMenuOption"; 47 | String ANONYMOUS_UPLOADING_KEY = "enableAnonymousReporting"; 48 | String PANEL_FONT_TYPE_KEY = "panelFontType"; 49 | String AUTH_FULL_TOKEN_KEY = "authToken"; 50 | String SHOW_FEEDBACK_TEXTBOX = "showFeedbackTextbox"; 51 | String SHOW_DISCORD_VERIFICATION_ERRORS = "showDiscordVerificationErrors"; 52 | String ANONYMOUS_UUID_KEY = "anonymousUUID"; 53 | String ACKNOWLEDGED_HARASSMENT_WARNING_KEY = "acknowledgedHarassmentWarning"; 54 | 55 | int AUTO_SEND_MINIMUM_MINUTES = 5; 56 | int AUTO_SEND_MAXIMUM_MINUTES = 360; 57 | 58 | @ConfigSection( 59 | position = 1, 60 | name = "Upload Settings", 61 | description = "Settings for how the plugin uploads player data." 62 | ) 63 | String uploadSection = "uploadSection"; 64 | 65 | @ConfigSection( 66 | position = 2, 67 | name = "Panel Settings", 68 | description = "Settings for the plugin's panel." 69 | ) 70 | String panelSection = "panelSection"; 71 | 72 | @ConfigSection( 73 | position = 3, 74 | name = "'Predict' Settings", 75 | description = "Settings for the 'Predict' right-click option." 76 | ) 77 | String predictSection = "predictSection"; 78 | 79 | @ConfigSection( 80 | position = 4, 81 | name = "Other Settings", 82 | description = "Other miscellaneous settings." 83 | ) 84 | String miscSection = "miscSection"; 85 | 86 | @ConfigItem( 87 | position = 1, 88 | keyName = ACKNOWLEDGED_HARASSMENT_WARNING_KEY, 89 | name = "Acknowledge Harassment Warning", 90 | description = "Set this config to acknowledge you understand not to harass other players while using this plugin.", 91 | warning = "We have received reports of legitimate players being harassed by users of this plugin." + 92 | "
" + 93 | "
Bot predictions from this plugin are not to be taken at face value, as players with lower" + 94 | "
total XP or account builds that generally deviate from the expected average may throw" + 95 | "
off our Machine Learning models, resulting in reductions in 'Real Player' confidence." + 96 | "
" + 97 | "
We ask that you do not harass other players based on our predictions." + 98 | "
Harassment never helps, as bots do not care and legitimate players suffer unnecessarily." + 99 | "
" + 100 | "
For more context, please read the plugin's FAQ, available on both the plugin's GitHub" + 101 | "
page and our Discord server before you continue using the plugin." + 102 | "
" + 103 | "
Thank you," + 104 | "
- The Bot Detector Team." 105 | ) 106 | default boolean acknowledgedHarassmentWarning() 107 | { 108 | return false; 109 | } 110 | 111 | @ConfigItem( 112 | position = 1, 113 | keyName = ANONYMOUS_UPLOADING_KEY, 114 | name = "Anonymous Uploading", 115 | description = "Your name will not be included with your name uploads.
Disable if you'd like to track your contributions.", 116 | section = uploadSection 117 | ) 118 | default boolean enableAnonymousUploading() 119 | { 120 | return true; 121 | } 122 | 123 | @ConfigItem( 124 | position = 2, 125 | keyName = ONLY_SEND_AT_LOGOUT_KEY, 126 | name = "Send Names Only After Logout", 127 | description = "Waits to upload names until you've logged out. Use this if you have a poor connection." 128 | + "
WARNING: Names will not be sent if RuneLite is closed completely" 129 | + "
before logging out, unless 'Attempt Send on Close' is turned on.", 130 | section = uploadSection 131 | ) 132 | default boolean onlySendAtLogout() 133 | { 134 | return false; 135 | } 136 | 137 | @ConfigItem( 138 | position = 3, 139 | keyName = "uploadOnShutdown", 140 | name = "Attempt Send on Close", 141 | description = "Attempts to upload names when closing RuneLite while being logged in." 142 | + "
WARNING: This may cause the client to take significantly longer to close" 143 | + "
in the event that the Bot Detector server is being slow or unresponsive.", 144 | section = uploadSection 145 | ) 146 | default boolean uploadOnShutdown() 147 | { 148 | return false; 149 | } 150 | 151 | @ConfigItem( 152 | position = 4, 153 | keyName = AUTO_SEND_MINUTES_KEY, 154 | name = "Send Names Every", 155 | description = "Sets the amount of time between automatic name uploads.", 156 | section = uploadSection 157 | ) 158 | @Range(min = AUTO_SEND_MINIMUM_MINUTES, max = AUTO_SEND_MAXIMUM_MINUTES) 159 | @Units(Units.MINUTES) 160 | default int autoSendMinutes() 161 | { 162 | return 5; 163 | } 164 | 165 | @ConfigItem( 166 | position = 1, 167 | keyName = "autocomplete", 168 | name = "Prediction Autocomplete", 169 | description = "Autocomplete names when typing a name to predict in the prediction panel.", 170 | section = panelSection 171 | ) 172 | default boolean panelAutocomplete() 173 | { 174 | return true; 175 | } 176 | 177 | @ConfigItem( 178 | position = 2, 179 | keyName = "showBreakdownOnNullConfidence", 180 | name = "Show Breakdown in Special Cases", 181 | description = "Show the Prediction Breakdown when predicting certain types of accounts, such as 'Stats Too Low'.", 182 | section = panelSection 183 | ) 184 | default boolean showBreakdownOnNullConfidence() 185 | { 186 | return false; 187 | } 188 | 189 | @ConfigItem( 190 | position = 3, 191 | keyName = SHOW_FEEDBACK_TEXTBOX, 192 | name = "Show Feedback Textbox", 193 | description = "Show a textbox on the prediction feedback panel where you can explain your feedback to us.", 194 | section = panelSection 195 | ) 196 | default boolean showFeedbackTextbox() 197 | { 198 | return true; 199 | } 200 | 201 | @ConfigItem( 202 | position = 4, 203 | keyName = "panelDefaultStatsType", 204 | name = "Panel Default Stats Tab", 205 | description = "Sets the initial player statistics tab in the prediction panel for when the plugin is launched.", 206 | section = panelSection 207 | ) 208 | default PlayerStatsType panelDefaultStatsType() 209 | { 210 | return PlayerStatsType.TOTAL; 211 | } 212 | 213 | @ConfigItem( 214 | position = 5, 215 | keyName = PANEL_FONT_TYPE_KEY, 216 | name = "Panel Font Size", 217 | description = "Sets the size of the label fields in the prediction panel.", 218 | section = panelSection 219 | ) 220 | default PanelFontType panelFontType() 221 | { 222 | return PanelFontType.NORMAL; 223 | } 224 | 225 | @ConfigItem( 226 | position = 1, 227 | keyName = ADD_PREDICT_PLAYER_OPTION_KEY, 228 | name = "Right-click 'Predict' Players", 229 | description = "Adds an entry to game world player menus to quickly check them in the prediction panel.", 230 | section = predictSection 231 | ) 232 | default boolean addPredictPlayerOption() 233 | { 234 | return false; 235 | } 236 | 237 | @ConfigItem( 238 | position = 2, 239 | keyName = ADD_PREDICT_MENU_OPTION_KEY, 240 | name = "Right-click 'Predict' Menus", 241 | description = "Adds an entry to interface player menus to quickly check them in the prediction panel.", 242 | section = predictSection 243 | ) 244 | default boolean addPredictMenuOption() 245 | { 246 | return false; 247 | } 248 | 249 | @ConfigItem( 250 | position = 3, 251 | keyName = "predictOnReport", 252 | name = "'Predict' on Right-click 'Report'", 253 | description = "Makes the in-game right-click 'Report' option also open the prediction panel.", 254 | section = predictSection 255 | ) 256 | default boolean predictOnReport() 257 | { 258 | return false; 259 | } 260 | 261 | @ConfigItem( 262 | position = 4, 263 | keyName = "predictOptionCopyName", 264 | name = "'Predict' Copy Name to Clipboard", 265 | description = "Copies the player's name to the clipboard when right-click predicting a player.", 266 | section = predictSection 267 | ) 268 | default boolean predictOptionCopyName() 269 | { 270 | return false; 271 | } 272 | 273 | @ConfigItem( 274 | position = 5, 275 | keyName = "predictOptionDefaultColor", 276 | name = "'Predict' Default Color", 277 | description = "When right-clicking on a player, the predict option will be this color by default.", 278 | section = predictSection 279 | ) 280 | Color predictOptionDefaultColor(); 281 | 282 | @ConfigItem( 283 | position = 6, 284 | keyName = "predictOptionFlaggedColor", 285 | name = "'Predict' Voted/Flagged Color", 286 | description = "When right-clicking on a player that has been flagged or given feedback, the predict option will be this color instead.", 287 | section = predictSection 288 | ) 289 | Color predictOptionFlaggedColor(); 290 | 291 | @ConfigItem( 292 | position = 7, 293 | keyName = "applyPredictColorsOnReportOption", 294 | name = "Apply Colors to 'Report'", 295 | description = "Applies the above 'Predict' color options to the in-game 'Report' option as well.", 296 | section = predictSection, 297 | warning = "Enabling this setting may cause issues with other plugins that rely on the 'Report' option being unchanged." 298 | ) 299 | default boolean applyPredictColorsOnReportOption() 300 | { 301 | return false; 302 | } 303 | 304 | @ConfigItem( 305 | position = 1, 306 | keyName = "enableChatNotifications", 307 | name = "Enable Chat Status Messages", 308 | description = "Show various plugin status messages in the game chat.", 309 | section = miscSection 310 | ) 311 | default boolean enableChatStatusMessages() 312 | { 313 | return false; 314 | } 315 | 316 | @ConfigItem( 317 | position = 2, 318 | keyName = "statsChatCommandDetailLevel", 319 | name = "'!bdstats' Chat Command Detail Level", 320 | description = "Enable processing the '!bdstats' command when it appears in the chatbox," 321 | + "
which will fetch the message author's plugin stats and display them.", 322 | section = miscSection 323 | ) 324 | default StatsCommandDetailLevel statsChatCommandDetailLevel() 325 | { 326 | return StatsCommandDetailLevel.CONFIRMED_ONLY; 327 | } 328 | 329 | @ConfigItem( 330 | keyName = AUTH_FULL_TOKEN_KEY, 331 | name = "", 332 | description = "", 333 | hidden = true 334 | ) 335 | default String authFullToken() 336 | { 337 | return null; 338 | } 339 | 340 | @ConfigItem( 341 | keyName = AUTH_FULL_TOKEN_KEY, 342 | name = "", 343 | description = "", 344 | hidden = true 345 | ) 346 | void setAuthFullToken(String fullToken); 347 | 348 | @ConfigItem( 349 | keyName = SHOW_DISCORD_VERIFICATION_ERRORS, 350 | name = "", 351 | description = "", 352 | hidden = true 353 | ) 354 | default boolean showDiscordVerificationErrors() 355 | { 356 | return true; 357 | } 358 | 359 | @ConfigItem( 360 | keyName = SHOW_DISCORD_VERIFICATION_ERRORS, 361 | name = "", 362 | description = "", 363 | hidden = true 364 | ) 365 | void setShowDiscordVerificationErrors(boolean show); 366 | } 367 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/BotDetectorPlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | package com.botdetector; 27 | 28 | import com.botdetector.http.BotDetectorClient; 29 | import com.botdetector.http.UnauthorizedTokenException; 30 | import com.botdetector.http.ValidationException; 31 | import com.botdetector.model.AuthToken; 32 | import com.botdetector.model.AuthTokenPermission; 33 | import com.botdetector.model.AuthTokenType; 34 | import com.botdetector.model.CaseInsensitiveString; 35 | import com.botdetector.model.PlayerSighting; 36 | import com.botdetector.model.PlayerStats; 37 | import com.botdetector.model.PlayerStatsType; 38 | import com.botdetector.model.FeedbackPredictionLabel; 39 | import com.botdetector.model.StatsCommandDetailLevel; 40 | import com.botdetector.ui.BotDetectorPanel; 41 | import com.botdetector.events.BotDetectorPanelActivated; 42 | import com.google.common.collect.EvictingQueue; 43 | import com.google.common.collect.HashBasedTable; 44 | import com.google.common.collect.ImmutableMap; 45 | import com.google.common.collect.ImmutableSet; 46 | import com.google.common.collect.Table; 47 | import com.google.common.collect.Tables; 48 | import com.google.common.primitives.Ints; 49 | import java.awt.Toolkit; 50 | import java.awt.Color; 51 | import java.awt.datatransfer.DataFlavor; 52 | import java.awt.datatransfer.StringSelection; 53 | import java.awt.datatransfer.UnsupportedFlavorException; 54 | import java.awt.image.BufferedImage; 55 | import java.io.IOException; 56 | import java.text.DecimalFormat; 57 | import java.time.Duration; 58 | import java.time.Instant; 59 | import java.time.temporal.ChronoUnit; 60 | import java.util.ArrayList; 61 | import java.util.Collection; 62 | import java.util.EnumSet; 63 | import java.util.HashMap; 64 | import java.util.Map; 65 | import java.util.Properties; 66 | import java.util.UUID; 67 | import java.util.concurrent.CompletableFuture; 68 | import java.util.concurrent.ConcurrentHashMap; 69 | import java.util.function.Consumer; 70 | import java.util.regex.Pattern; 71 | import javax.swing.JEditorPane; 72 | import javax.swing.JOptionPane; 73 | import javax.swing.SwingUtilities; 74 | import javax.swing.event.HyperlinkEvent; 75 | import lombok.Getter; 76 | import lombok.extern.slf4j.Slf4j; 77 | import net.runelite.api.ChatMessageType; 78 | import net.runelite.api.Client; 79 | import net.runelite.api.GameState; 80 | import net.runelite.api.MenuAction; 81 | import net.runelite.api.MenuEntry; 82 | import net.runelite.api.MessageNode; 83 | import net.runelite.api.Player; 84 | import net.runelite.api.WorldType; 85 | import net.runelite.api.WorldView; 86 | import net.runelite.api.coords.WorldPoint; 87 | import net.runelite.api.events.ChatMessage; 88 | import net.runelite.api.events.CommandExecuted; 89 | import net.runelite.api.events.GameStateChanged; 90 | import net.runelite.api.events.MenuEntryAdded; 91 | import net.runelite.api.events.MenuOpened; 92 | import net.runelite.api.events.MenuOptionClicked; 93 | import net.runelite.api.events.PlayerSpawned; 94 | import net.runelite.api.events.WorldChanged; 95 | import net.runelite.api.kit.KitType; 96 | import net.runelite.api.widgets.ComponentID; 97 | import net.runelite.api.widgets.InterfaceID; 98 | import net.runelite.api.widgets.WidgetUtil; 99 | import net.runelite.client.callback.ClientThread; 100 | import net.runelite.client.chat.ChatColorType; 101 | import net.runelite.client.chat.ChatCommandManager; 102 | import net.runelite.client.chat.ChatMessageBuilder; 103 | import net.runelite.client.chat.ChatMessageManager; 104 | import net.runelite.client.chat.QueuedMessage; 105 | import net.runelite.client.config.ConfigManager; 106 | import net.runelite.client.config.RuneScapeProfileType; 107 | import net.runelite.client.eventbus.Subscribe; 108 | import net.runelite.client.events.ClientShutdown; 109 | import net.runelite.client.game.ItemManager; 110 | import net.runelite.client.menus.MenuManager; 111 | import net.runelite.client.plugins.Plugin; 112 | import net.runelite.client.plugins.PluginDescriptor; 113 | import net.runelite.client.events.ConfigChanged; 114 | import javax.inject.Inject; 115 | import net.runelite.client.plugins.PluginManager; 116 | import net.runelite.client.task.Schedule; 117 | import net.runelite.client.ui.ClientToolbar; 118 | import net.runelite.client.ui.NavigationButton; 119 | import net.runelite.client.util.ColorUtil; 120 | import net.runelite.client.util.ImageUtil; 121 | import net.runelite.client.util.LinkBrowser; 122 | import net.runelite.client.util.Text; 123 | import com.google.inject.Provides; 124 | import org.apache.commons.lang3.StringUtils; 125 | import static com.botdetector.model.CaseInsensitiveString.wrap; 126 | 127 | @Slf4j 128 | @PluginDescriptor( 129 | name = "Bot Detector", 130 | description = "This plugin sends encountered Player Names to a server in order to detect Botting Behavior.", 131 | tags = {"Bot", "Detector", "Player"} 132 | ) 133 | public class BotDetectorPlugin extends Plugin 134 | { 135 | /** {@link PlayerSighting}s should only be created if the player is logged into a world set up for one of these {@link RuneScapeProfileType}s. **/ 136 | private static final ImmutableSet ALLOWED_PROFILE_TYPES = 137 | ImmutableSet.of( 138 | RuneScapeProfileType.STANDARD 139 | ); 140 | 141 | /** {@link PlayerSighting}s should only be created if the returned region id is <= this amount. **/ 142 | private static final int MAX_ALLOWED_REGION_ID = 16000; 143 | 144 | private static final Pattern UUID_PATTERN = Pattern.compile("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"); 145 | 146 | private static final String PREDICT_OPTION = "Predict"; 147 | private static final String REPORT_OPTION = "Report"; 148 | 149 | private static final ImmutableSet PLAYER_MENU_ACTIONS = ImmutableSet.of( 150 | MenuAction.PLAYER_FIRST_OPTION, MenuAction.PLAYER_SECOND_OPTION, MenuAction.PLAYER_THIRD_OPTION, MenuAction.PLAYER_FOURTH_OPTION, 151 | MenuAction.PLAYER_FIFTH_OPTION, MenuAction.PLAYER_SIXTH_OPTION, MenuAction.PLAYER_SEVENTH_OPTION, MenuAction.PLAYER_EIGHTH_OPTION 152 | ); 153 | 154 | 155 | private static final String VERIFY_DISCORD_COMMAND = "!code"; 156 | private static final int VERIFY_DISCORD_CODE_SIZE = 4; 157 | private static final Pattern VERIFY_DISCORD_CODE_PATTERN = Pattern.compile("\\d{1," + VERIFY_DISCORD_CODE_SIZE + "}"); 158 | 159 | private static final String STATS_CHAT_COMMAND = "!bdstats"; 160 | 161 | private static final String COMMAND_PREFIX = "bd"; 162 | private static final String MANUAL_FLUSH_COMMAND = COMMAND_PREFIX + "Flush"; 163 | private static final String MANUAL_SIGHT_COMMAND = COMMAND_PREFIX + "Snap"; 164 | private static final String MANUAL_REFRESH_COMMAND = COMMAND_PREFIX + "Refresh"; 165 | private static final String SHOW_HIDE_ID_COMMAND = COMMAND_PREFIX + "ShowId"; 166 | private static final String GET_AUTH_TOKEN_COMMAND = COMMAND_PREFIX + "GetToken"; 167 | private static final String SET_AUTH_TOKEN_COMMAND = COMMAND_PREFIX + "SetToken"; 168 | private static final String CLEAR_AUTH_TOKEN_COMMAND = COMMAND_PREFIX + "ClearToken"; 169 | private static final String TOGGLE_SHOW_DISCORD_VERIFICATION_ERRORS_COMMAND = COMMAND_PREFIX + "ToggleShowDiscordVerificationErrors"; 170 | private static final String TOGGLE_SHOW_DISCORD_VERIFICATION_ERRORS_COMMAND_ALIAS = COMMAND_PREFIX + "ToggleDVE"; 171 | 172 | /** Command to method map to be used in {@link #onCommandExecuted(CommandExecuted)}. **/ 173 | private final ImmutableMap> commandConsumerMap = 174 | ImmutableMap.>builder() 175 | .put(wrap(MANUAL_FLUSH_COMMAND), s -> manualFlushCommand()) 176 | .put(wrap(MANUAL_SIGHT_COMMAND), s -> manualSightCommand()) 177 | .put(wrap(MANUAL_REFRESH_COMMAND), s -> manualRefreshStatsCommand()) 178 | .put(wrap(SHOW_HIDE_ID_COMMAND), this::showHideIdCommand) 179 | .put(wrap(GET_AUTH_TOKEN_COMMAND), s -> putAuthTokenIntoClipboardCommand()) 180 | .put(wrap(SET_AUTH_TOKEN_COMMAND), s -> setAuthTokenFromClipboardCommand()) 181 | .put(wrap(CLEAR_AUTH_TOKEN_COMMAND), s -> clearAuthTokenCommand()) 182 | .put(wrap(TOGGLE_SHOW_DISCORD_VERIFICATION_ERRORS_COMMAND), s -> toggleShowDiscordVerificationErrors()) 183 | .put(wrap(TOGGLE_SHOW_DISCORD_VERIFICATION_ERRORS_COMMAND_ALIAS), s -> toggleShowDiscordVerificationErrors()) 184 | .build(); 185 | 186 | private static final int MANUAL_FLUSH_COOLDOWN_SECONDS = 60; 187 | private static final int AUTO_REFRESH_STATS_COOLDOWN_SECONDS = 150; 188 | private static final int AUTO_REFRESH_LAST_FLUSH_GRACE_PERIOD_SECONDS = 30; 189 | private static final int API_HIT_SCHEDULE_SECONDS = 5; 190 | 191 | private static final String CHAT_MESSAGE_HEADER = "[Bot Detector] "; 192 | public static final String ANONYMOUS_USER_NAME = "AnonymousUser"; 193 | public static final String ANONYMOUS_USER_NAME_UUID_FORMAT = ANONYMOUS_USER_NAME + "_%s"; 194 | 195 | @Inject 196 | private Client client; 197 | 198 | @Inject 199 | private ClientThread clientThread; 200 | 201 | @Inject 202 | private ConfigManager configManager; 203 | 204 | @Inject 205 | private MenuManager menuManager; 206 | 207 | @Inject 208 | private ItemManager itemManager; 209 | 210 | @Inject 211 | private BotDetectorConfig config; 212 | 213 | @Inject 214 | private PluginManager pluginManager; 215 | 216 | @Inject 217 | private ClientToolbar clientToolbar; 218 | 219 | @Inject 220 | private ChatCommandManager chatCommandManager; 221 | 222 | @Inject 223 | private ChatMessageManager chatMessageManager; 224 | 225 | @Inject 226 | private BotDetectorClient detectorClient; 227 | 228 | private BotDetectorPanel panel; 229 | private NavigationButton navButton; 230 | 231 | @Provides 232 | BotDetectorConfig provideConfig(ConfigManager configManager) 233 | { 234 | return configManager.getConfig(BotDetectorConfig.class); 235 | } 236 | 237 | /** The currently logged in player name, or {@code null} if the user is logged out. **/ 238 | @Getter 239 | private String loggedPlayerName; 240 | /** The next time an automatic call to {@link #flushPlayersToClient(boolean)} should be allowed to run. **/ 241 | private Instant timeToAutoSend; 242 | /** The total number of names uploaded in the current login session. **/ 243 | private int namesUploaded; 244 | /** The last time a {@link #flushPlayersToClient(boolean)} was successfully attempted. **/ 245 | private Instant lastFlush = Instant.MIN; 246 | /** The last time a {@link #refreshPlayerStats(boolean)}} was successfully attempted. **/ 247 | private Instant lastStatsRefresh = Instant.MIN; 248 | /** See {@link #processCurrentWorld()}. **/ 249 | private int currentWorldNumber; 250 | /** See {@link #processCurrentWorld()}. **/ 251 | private boolean isCurrentWorldMembers; 252 | /** See {@link #processCurrentWorld()}. **/ 253 | private boolean isCurrentWorldPVP; 254 | /** A blocked world should not log {@link PlayerSighting}s (see {@link #processCurrentWorld()} and {@link #ALLOWED_PROFILE_TYPES}). **/ 255 | private boolean isCurrentWorldBlocked; 256 | /** A queue containing the last two {@link GameState}s from {@link #onGameStateChanged(GameStateChanged)}. **/ 257 | private EvictingQueue previousTwoGameStates = EvictingQueue.create(2); 258 | 259 | /** The currently loaded token or {@link AuthToken#EMPTY_TOKEN} if no valid token is loaded. **/ 260 | @Getter 261 | private AuthToken authToken = AuthToken.EMPTY_TOKEN; 262 | 263 | /** The currently loaded anonymous UUID. **/ 264 | private String anonymousUUID; 265 | 266 | /** 267 | * Contains the last {@link PlayerSighting} for the given {@code player} and {@code regionId} 268 | * since the last successful call to {@link #flushPlayersToClient(boolean, boolean)}. 269 | * Always use {@link #normalizeAndWrapPlayerName(String)} when keying into this table. 270 | */ 271 | @Getter 272 | private final Table sightingTable = Tables.synchronizedTable(HashBasedTable.create()); 273 | 274 | /** 275 | * Contains the last {@link PlayerSighting} for the given {@code player} for the current login session. 276 | * Always use {@link #normalizeAndWrapPlayerName(String)} when keying into this map. 277 | */ 278 | @Getter 279 | private final Map persistentSightings = new ConcurrentHashMap<>(); 280 | 281 | /** 282 | * Contains the feedbacks (See {@link FeedbackPredictionLabel}) sent per {@code player} for the current login session. 283 | * Always use {@link #normalizeAndWrapPlayerName(String)} when keying into this map. 284 | */ 285 | @Getter 286 | private final Map feedbackedPlayers = new ConcurrentHashMap<>(); 287 | 288 | /** 289 | * Contains the feedback texts sent per {@code player} for the current login session. 290 | * Always use {@link #normalizeAndWrapPlayerName(String)} when keying into this map. 291 | */ 292 | @Getter 293 | private final Map feedbackedPlayersText = new ConcurrentHashMap<>(); 294 | 295 | /** 296 | * Contains the flagging (yes/no) sent per {@code player} for the current login session. 297 | * Always use {@link #normalizeAndWrapPlayerName(String)} when keying into this map. 298 | */ 299 | @Getter 300 | private final Map flaggedPlayers = new ConcurrentHashMap<>(); 301 | 302 | @Override 303 | protected void startUp() 304 | { 305 | // Get current version of the plugin using properties file generated by build.gradle 306 | // Thanks to https://github.com/dillydill123/inventory-setups/ 307 | try 308 | { 309 | final Properties props = new Properties(); 310 | props.load(getClass().getResourceAsStream("version.txt")); 311 | detectorClient.setPluginVersion(props.getProperty("version")); 312 | } 313 | catch (Exception e) 314 | { 315 | log.error("Could not parse plugin version from properties file!", e); 316 | 317 | // Turn plugin back off and display an error message 318 | pluginManager.setPluginEnabled(this, false); 319 | displayPluginVersionError(); 320 | 321 | return; 322 | } 323 | 324 | // Load up the anonymous UUID 325 | anonymousUUID = configManager.getConfiguration(BotDetectorConfig.CONFIG_GROUP, BotDetectorConfig.ANONYMOUS_UUID_KEY); 326 | if (StringUtils.isBlank(anonymousUUID) || !UUID_PATTERN.matcher(anonymousUUID).matches()) 327 | { 328 | anonymousUUID = UUID.randomUUID().toString(); 329 | configManager.setConfiguration(BotDetectorConfig.CONFIG_GROUP, BotDetectorConfig.ANONYMOUS_UUID_KEY, anonymousUUID); 330 | } 331 | 332 | panel = injector.getInstance(BotDetectorPanel.class); 333 | SwingUtilities.invokeLater(() -> 334 | { 335 | panel.setWarningVisible(BotDetectorPanel.WarningLabel.ANONYMOUS, config.enableAnonymousUploading()); 336 | panel.setWarningVisible(BotDetectorPanel.WarningLabel.HARASSMENT_WARNING, !config.acknowledgedHarassmentWarning()); 337 | panel.setPluginVersion(detectorClient.getPluginVersion()); 338 | panel.setNamesUploaded(0, false); 339 | panel.setNamesUploaded(0, true); 340 | panel.setFeedbackTextboxVisible(config.showFeedbackTextbox()); 341 | }); 342 | 343 | processCurrentWorld(); 344 | 345 | final BufferedImage icon = ImageUtil.loadImageResource(getClass(), "bot-icon.png"); 346 | 347 | navButton = NavigationButton.builder() 348 | .panel(panel) 349 | .tooltip("Bot Detector") 350 | .icon(icon) 351 | .priority(90) 352 | .build(); 353 | 354 | clientToolbar.addNavigation(navButton); 355 | 356 | if (config.addPredictPlayerOption() && client != null) 357 | { 358 | menuManager.addPlayerMenuItem(PREDICT_OPTION); 359 | } 360 | 361 | updateTimeToAutoSend(); 362 | 363 | authToken = AuthToken.fromFullToken(config.authFullToken()); 364 | 365 | previousTwoGameStates.offer(client.getGameState()); 366 | 367 | chatCommandManager.registerCommand(VERIFY_DISCORD_COMMAND, this::verifyDiscord); 368 | chatCommandManager.registerCommand(STATS_CHAT_COMMAND, this::statsChatCommand); 369 | } 370 | 371 | @Override 372 | protected void shutDown() 373 | { 374 | panel.shutdown(); 375 | 376 | flushPlayersToClient(false); 377 | persistentSightings.clear(); 378 | feedbackedPlayers.clear(); 379 | feedbackedPlayersText.clear(); 380 | flaggedPlayers.clear(); 381 | 382 | if (client != null) 383 | { 384 | menuManager.removePlayerMenuItem(PREDICT_OPTION); 385 | } 386 | 387 | clientToolbar.removeNavigation(navButton); 388 | 389 | namesUploaded = 0; 390 | loggedPlayerName = null; 391 | lastFlush = Instant.MIN; 392 | lastStatsRefresh = Instant.MIN; 393 | authToken = AuthToken.EMPTY_TOKEN; 394 | 395 | previousTwoGameStates.clear(); 396 | 397 | chatCommandManager.unregisterCommand(VERIFY_DISCORD_COMMAND); 398 | chatCommandManager.unregisterCommand(STATS_CHAT_COMMAND); 399 | } 400 | 401 | /** 402 | * Updates {@link #timeToAutoSend} according to {@link BotDetectorConfig#autoSendMinutes()}. 403 | */ 404 | private void updateTimeToAutoSend() 405 | { 406 | timeToAutoSend = Instant.now().plusSeconds(60L * 407 | Ints.constrainToRange(config.autoSendMinutes(), 408 | BotDetectorConfig.AUTO_SEND_MINIMUM_MINUTES, 409 | BotDetectorConfig.AUTO_SEND_MAXIMUM_MINUTES)); 410 | } 411 | 412 | /** 413 | * Do not call this method in code. Continuously calls the automatic variants of API calling methods. 414 | */ 415 | @Schedule(period = API_HIT_SCHEDULE_SECONDS, unit = ChronoUnit.SECONDS, asynchronous = true) 416 | public void hitApi() 417 | { 418 | if (loggedPlayerName == null) 419 | { 420 | return; 421 | } 422 | 423 | if (!config.onlySendAtLogout() && Instant.now().isAfter(timeToAutoSend)) 424 | { 425 | flushPlayersToClient(true); 426 | } 427 | 428 | refreshPlayerStats(false); 429 | } 430 | 431 | /** 432 | * Attempts to send the contents of {@link #sightingTable} to {@link BotDetectorClient#sendSightings(Collection, String, boolean)}. 433 | * @param restoreOnFailure The table is cleared before sending. If {@code true}, re-insert the cleared sightings into the table on failure. 434 | * @return A completable future if there were any names to attempt to send, {@code null} otherwise. 435 | */ 436 | public synchronized CompletableFuture flushPlayersToClient(boolean restoreOnFailure) 437 | { 438 | return flushPlayersToClient(restoreOnFailure, false); 439 | } 440 | 441 | /** 442 | * Attempts to send the contents of {@link #sightingTable} to {@link BotDetectorClient#sendSightings(Collection, String, boolean)}. 443 | * @param restoreOnFailure The table is cleared before sending. If {@code true}, re-insert the cleared sightings into the table on failure. 444 | * @param forceChatNotification Force displays the chat notifications. 445 | * @return A completable future if there were any names to attempt to send, {@code null} otherwise. 446 | */ 447 | public synchronized CompletableFuture flushPlayersToClient(boolean restoreOnFailure, boolean forceChatNotification) 448 | { 449 | String uploader = getUploaderName(); 450 | if (uploader == null) 451 | { 452 | return null; 453 | } 454 | 455 | updateTimeToAutoSend(); 456 | 457 | int uniqueNames; 458 | Collection sightings; 459 | int numUploads; 460 | synchronized (sightingTable) 461 | { 462 | uniqueNames = sightingTable.rowKeySet().size(); 463 | if (uniqueNames <= 0) 464 | { 465 | return null; 466 | } 467 | 468 | sightings = new ArrayList<>(sightingTable.values()); 469 | sightingTable.clear(); 470 | numUploads = sightings.size(); 471 | } 472 | 473 | lastFlush = Instant.now(); 474 | 475 | return detectorClient.sendSightings(sightings, getUploaderName(), false) 476 | .whenComplete((b, ex) -> 477 | { 478 | if (ex == null && b) 479 | { 480 | namesUploaded += uniqueNames; 481 | SwingUtilities.invokeLater(() -> panel.setNamesUploaded(namesUploaded, false)); 482 | sendChatStatusMessage("Successfully uploaded " + numUploads + 483 | " locations for " + uniqueNames + " unique players.", 484 | forceChatNotification); 485 | } 486 | else 487 | { 488 | sendChatStatusMessage("Error sending player sightings!", forceChatNotification); 489 | // Put the sightings back, but not if it's because of a validation error 490 | if (restoreOnFailure && !(ex instanceof ValidationException)) 491 | { 492 | synchronized (sightingTable) 493 | { 494 | sightings.forEach(s -> 495 | { 496 | CaseInsensitiveString name = wrap(s.getPlayerName()); 497 | int region = s.getRegionID(); 498 | // Don't replace if new sightings were added to the table during the request 499 | if (!sightingTable.contains(name, region)) 500 | { 501 | sightingTable.put(name, region, s); 502 | } 503 | }); 504 | } 505 | } 506 | } 507 | }); 508 | } 509 | 510 | /** 511 | * Attempts to refresh the current player uploading statistics on the plugin panel according to various checks. 512 | * @param forceRefresh If {@code true}, ignore checks in place meant for the automatic calling of this method. 513 | */ 514 | public synchronized void refreshPlayerStats(boolean forceRefresh) 515 | { 516 | if (!forceRefresh) 517 | { 518 | Instant now = Instant.now(); 519 | // Only perform non-manual refreshes when a player is not anon, logged in and the panel is open 520 | if (config.enableAnonymousUploading() || loggedPlayerName == null || !panel.isActive() 521 | || now.isBefore(lastStatsRefresh.plusSeconds(AUTO_REFRESH_STATS_COOLDOWN_SECONDS)) 522 | || now.isBefore(lastFlush.plusSeconds(AUTO_REFRESH_LAST_FLUSH_GRACE_PERIOD_SECONDS))) 523 | { 524 | return; 525 | } 526 | } 527 | 528 | lastStatsRefresh = Instant.now(); 529 | 530 | if (config.enableAnonymousUploading() || loggedPlayerName == null) 531 | { 532 | SwingUtilities.invokeLater(() -> 533 | { 534 | panel.setPlayerStatsMap(null); 535 | panel.setPlayerStatsLoading(false); 536 | panel.setWarningVisible(BotDetectorPanel.WarningLabel.ANONYMOUS, config.enableAnonymousUploading()); 537 | panel.setWarningVisible(BotDetectorPanel.WarningLabel.PLAYER_STATS_ERROR, false); 538 | if (loggedPlayerName == null) 539 | { 540 | panel.forceHideFeedbackPanel(); 541 | } 542 | panel.forceHideFlaggingPanel(); 543 | }); 544 | return; 545 | } 546 | 547 | SwingUtilities.invokeLater(() -> 548 | { 549 | panel.setPlayerStatsLoading(true); 550 | panel.setWarningVisible(BotDetectorPanel.WarningLabel.ANONYMOUS, false); 551 | }); 552 | 553 | String nameAtRequest = loggedPlayerName; 554 | detectorClient.requestPlayerStats(nameAtRequest) 555 | .whenComplete((psm, ex) -> 556 | { 557 | // Player could have logged out in the mean time, don't update panel 558 | // Player could also have switched to anon mode, don't update either. 559 | if (config.enableAnonymousUploading() || !nameAtRequest.equals(loggedPlayerName)) 560 | { 561 | return; 562 | } 563 | 564 | SwingUtilities.invokeLater(() -> panel.setPlayerStatsLoading(false)); 565 | 566 | if (ex == null && psm != null) 567 | { 568 | SwingUtilities.invokeLater(() -> 569 | { 570 | panel.setPlayerStatsMap(psm); 571 | panel.setWarningVisible(BotDetectorPanel.WarningLabel.PLAYER_STATS_ERROR, false); 572 | }); 573 | } 574 | else 575 | { 576 | SwingUtilities.invokeLater(() -> 577 | panel.setWarningVisible(BotDetectorPanel.WarningLabel.PLAYER_STATS_ERROR, true)); 578 | } 579 | }); 580 | } 581 | 582 | @Subscribe 583 | private void onBotDetectorPanelActivated(BotDetectorPanelActivated event) 584 | { 585 | if (!config.enableAnonymousUploading()) 586 | { 587 | refreshPlayerStats(false); 588 | } 589 | } 590 | 591 | @Subscribe 592 | private void onConfigChanged(ConfigChanged event) 593 | { 594 | if (!event.getGroup().equals(BotDetectorConfig.CONFIG_GROUP) || event.getKey() == null) 595 | { 596 | return; 597 | } 598 | 599 | switch (event.getKey()) 600 | { 601 | case BotDetectorConfig.ADD_PREDICT_PLAYER_OPTION_KEY: 602 | if (client != null) 603 | { 604 | menuManager.removePlayerMenuItem(PREDICT_OPTION); 605 | 606 | if (config.addPredictPlayerOption()) 607 | { 608 | menuManager.addPlayerMenuItem(PREDICT_OPTION); 609 | } 610 | } 611 | break; 612 | case BotDetectorConfig.ANONYMOUS_UPLOADING_KEY: 613 | refreshPlayerStats(true); 614 | SwingUtilities.invokeLater(() -> 615 | { 616 | panel.forceHideFeedbackPanel(); 617 | panel.forceHideFlaggingPanel(); 618 | }); 619 | break; 620 | case BotDetectorConfig.ACKNOWLEDGED_HARASSMENT_WARNING_KEY: 621 | SwingUtilities.invokeLater(() -> 622 | panel.setWarningVisible( 623 | BotDetectorPanel.WarningLabel.HARASSMENT_WARNING, 624 | !config.acknowledgedHarassmentWarning())); 625 | break; 626 | case BotDetectorConfig.PANEL_FONT_TYPE_KEY: 627 | SwingUtilities.invokeLater(() -> panel.setFontType(config.panelFontType())); 628 | break; 629 | case BotDetectorConfig.SHOW_FEEDBACK_TEXTBOX: 630 | SwingUtilities.invokeLater(() -> panel.setFeedbackTextboxVisible(config.showFeedbackTextbox())); 631 | break; 632 | case BotDetectorConfig.AUTO_SEND_MINUTES_KEY: 633 | case BotDetectorConfig.ONLY_SEND_AT_LOGOUT_KEY: 634 | updateTimeToAutoSend(); 635 | break; 636 | } 637 | } 638 | 639 | @Subscribe 640 | private void onGameStateChanged(GameStateChanged event) 641 | { 642 | switch (event.getGameState()) 643 | { 644 | case LOGIN_SCREEN: 645 | if (loggedPlayerName != null) 646 | { 647 | flushPlayersToClient(false); 648 | persistentSightings.clear(); 649 | feedbackedPlayers.clear(); 650 | feedbackedPlayersText.clear(); 651 | flaggedPlayers.clear(); 652 | loggedPlayerName = null; 653 | 654 | refreshPlayerStats(true); 655 | SwingUtilities.invokeLater(() -> panel.setWarningVisible(BotDetectorPanel.WarningLabel.NAME_ERROR, false)); 656 | lastStatsRefresh = Instant.MIN; 657 | } 658 | break; 659 | case LOGGED_IN: 660 | // Reload Sighting cache when passing from LOGGED_IN -> LOADING -> LOGGED_IN 661 | if (!isCurrentWorldBlocked && loggedPlayerName != null 662 | && previousTwoGameStates.contains(GameState.LOGGED_IN) 663 | && previousTwoGameStates.contains(GameState.LOADING)) 664 | { 665 | client.getPlayers().forEach(this::processPlayer); 666 | } 667 | break; 668 | } 669 | previousTwoGameStates.offer(event.getGameState()); 670 | } 671 | 672 | @Subscribe 673 | private void onPlayerSpawned(PlayerSpawned event) 674 | { 675 | processPlayer(event.getPlayer()); 676 | } 677 | 678 | /** 679 | * Processes the given {@code player}, creating and saving a {@link PlayerSighting}. 680 | * @param player The player to process. 681 | */ 682 | private void processPlayer(Player player) 683 | { 684 | if (player == null) 685 | { 686 | return; 687 | } 688 | 689 | String rawName = player.getName(); 690 | 691 | boolean invalidName = rawName == null || rawName.length() == 0 || rawName.charAt(0) == '#' || rawName.charAt(0) == '['; 692 | 693 | if (player == client.getLocalPlayer()) 694 | { 695 | if (loggedPlayerName == null || !loggedPlayerName.equals(rawName)) 696 | { 697 | if (invalidName) 698 | { 699 | loggedPlayerName = null; 700 | SwingUtilities.invokeLater(() -> panel.setWarningVisible(BotDetectorPanel.WarningLabel.NAME_ERROR, true)); 701 | } 702 | else 703 | { 704 | loggedPlayerName = rawName; 705 | updateTimeToAutoSend(); 706 | refreshPlayerStats(true); 707 | SwingUtilities.invokeLater(() -> panel.setWarningVisible(BotDetectorPanel.WarningLabel.NAME_ERROR, false)); 708 | } 709 | } 710 | return; 711 | } 712 | 713 | // Block processing AFTER local player check 714 | if (isCurrentWorldBlocked || invalidName) 715 | { 716 | return; 717 | } 718 | 719 | String playerName = normalizePlayerName(rawName); 720 | CaseInsensitiveString wrappedName = wrap(playerName); 721 | 722 | // Maybe using clientThread will help with whatever is going on with instance regions sneaking through? 723 | // Theory is on some machines, maybe isInInstance() returns false, but player gets changed before getWorldLocation() runs? 724 | // IDK man I can't ever seem to be able to repro this... 725 | clientThread.invoke(() -> 726 | { 727 | boolean instanced = client.isInInstancedRegion(); 728 | 729 | WorldPoint wp = !instanced ? player.getWorldLocation() 730 | : WorldPoint.fromLocalInstance(client, player.getLocalLocation()); 731 | 732 | if (wp.getRegionID() > MAX_ALLOWED_REGION_ID) 733 | { 734 | log.warn(String.format("Player sighting with invalid region ID. (name:'%s' x:%d y:%d z:%d r:%d s:%d)", 735 | playerName, wp.getX(), wp.getY(), wp.getPlane(), wp.getRegionID(), 736 | (instanced ? 1 : 0) + (client.isInInstancedRegion() ? 2 : 0))); // Sanity check 737 | return; 738 | } 739 | 740 | // Get player's equipment item ids (botanicvelious/Equipment-Inspector) 741 | Map equipment = new HashMap<>(); 742 | long geValue = 0; 743 | for (KitType kitType : KitType.values()) 744 | { 745 | int itemId = player.getPlayerComposition().getEquipmentId(kitType); 746 | if (itemId >= 0) 747 | { 748 | equipment.put(kitType, itemId); 749 | // Use GE price, not Wiki price 750 | geValue += itemManager.getItemPriceWithSource(itemId, false); 751 | } 752 | } 753 | 754 | PlayerSighting p = PlayerSighting.builder() 755 | .playerName(playerName) 756 | .regionID(wp.getRegionID()) 757 | .worldX(wp.getX()) 758 | .worldY(wp.getY()) 759 | .plane(wp.getPlane()) 760 | .equipment(equipment) 761 | .equipmentGEValue(geValue) 762 | .timestamp(Instant.now()) 763 | .worldNumber(currentWorldNumber) 764 | .inMembersWorld(isCurrentWorldMembers) 765 | .inPVPWorld(isCurrentWorldPVP) 766 | .build(); 767 | 768 | synchronized (sightingTable) 769 | { 770 | sightingTable.put(wrappedName, p.getRegionID(), p); 771 | } 772 | persistentSightings.put(wrappedName, p); 773 | } 774 | ); 775 | } 776 | 777 | @Subscribe 778 | private void onCommandExecuted(CommandExecuted event) 779 | { 780 | Consumer consumer = commandConsumerMap.get(wrap(event.getCommand())); 781 | if (consumer != null) 782 | { 783 | consumer.accept(event.getArguments()); 784 | } 785 | } 786 | 787 | @Subscribe 788 | private void onClientShutdown(ClientShutdown event) 789 | { 790 | if (config.uploadOnShutdown()) 791 | { 792 | CompletableFuture future = flushPlayersToClient(false); 793 | if (future != null) 794 | { 795 | event.waitFor(future); 796 | } 797 | } 798 | } 799 | 800 | /** 801 | * Parses the Author and Code from the given message arguments and sends them over to 802 | * {@link BotDetectorClient#verifyDiscord(String, String, String)} for verification. 803 | * Requires that {@link #authToken} has the {@link AuthTokenPermission#VERIFY_DISCORD} permission. 804 | * @param chatMessage The ChatMessage event object. 805 | * @param message The actual chat message. 806 | */ 807 | private void verifyDiscord(ChatMessage chatMessage, String message) 808 | { 809 | if (!authToken.getTokenType().getPermissions().contains(AuthTokenPermission.VERIFY_DISCORD)) 810 | { 811 | return; 812 | } 813 | 814 | if (message.length() <= VERIFY_DISCORD_COMMAND.length()) 815 | { 816 | return; 817 | } 818 | 819 | String author; 820 | if (chatMessage.getType().equals(ChatMessageType.PRIVATECHATOUT)) 821 | { 822 | author = loggedPlayerName; 823 | } 824 | else 825 | { 826 | author = Text.sanitize(chatMessage.getName()); 827 | } 828 | 829 | String code = message.substring(VERIFY_DISCORD_COMMAND.length() + 1).trim(); 830 | 831 | if (!VERIFY_DISCORD_CODE_PATTERN.matcher(code).matches()) 832 | { 833 | return; 834 | } 835 | 836 | detectorClient.verifyDiscord(authToken.getToken(), author, 837 | StringUtils.leftPad(code, VERIFY_DISCORD_CODE_SIZE, '0')) 838 | .whenComplete((b, ex) -> 839 | { 840 | if (ex == null && b) 841 | { 842 | sendChatStatusMessage("Discord verified for '" + author + "'!", true); 843 | } 844 | else if (ex instanceof UnauthorizedTokenException) 845 | { 846 | sendChatStatusMessage("Invalid token for Discord verification, cannot verify '" + author + "'.", true); 847 | } 848 | else if (config.showDiscordVerificationErrors()) 849 | { 850 | sendChatStatusMessage("Could not verify Discord for '" + author + "'" + (ex != null ? ": " + ex.getMessage() : "."), true); 851 | } 852 | }); 853 | } 854 | 855 | /** 856 | * Displays the Bot Detector statistics for the message's author 857 | * @param chatMessage The ChatMessage event object. 858 | * @param message The actual chat message. 859 | */ 860 | private void statsChatCommand(ChatMessage chatMessage, String message) 861 | { 862 | if (message.length() != STATS_CHAT_COMMAND.length()) 863 | { 864 | return; 865 | } 866 | 867 | final StatsCommandDetailLevel detailLevel = config.statsChatCommandDetailLevel(); 868 | if (detailLevel == StatsCommandDetailLevel.OFF) 869 | { 870 | return; 871 | } 872 | 873 | final String author; 874 | if (chatMessage.getType().equals(ChatMessageType.PRIVATECHATOUT)) 875 | { 876 | author = loggedPlayerName; 877 | } 878 | else 879 | { 880 | author = Text.sanitize(chatMessage.getName()); 881 | } 882 | 883 | detectorClient.requestPlayerStats(author) 884 | .whenComplete((map, ex) -> 885 | { 886 | if (ex == null && map != null) 887 | { 888 | PlayerStats totalStats = map.get(PlayerStatsType.TOTAL); 889 | 890 | ChatMessageBuilder response = new ChatMessageBuilder() 891 | .append(ChatColorType.HIGHLIGHT) 892 | .append("Bot Detector stats -"); 893 | 894 | if (totalStats == null || totalStats.getNamesUploaded() <= 0) 895 | { 896 | response.append(ChatColorType.NORMAL) 897 | .append(" No plugin stats for this player"); 898 | } 899 | else 900 | { 901 | if (detailLevel == StatsCommandDetailLevel.DETAILED) 902 | { 903 | response.append(ChatColorType.NORMAL) 904 | .append(" Total Uploads:") 905 | .append(ChatColorType.HIGHLIGHT) 906 | .append(String.format(" %,d", totalStats.getNamesUploaded())) 907 | .append(ChatColorType.NORMAL) 908 | .append(" Feedback Sent:") 909 | .append(ChatColorType.HIGHLIGHT) 910 | .append(String.format(" %,d", totalStats.getFeedbackSent())) 911 | .append(ChatColorType.NORMAL) 912 | .append(" Possible Bans:") 913 | .append(ChatColorType.HIGHLIGHT) 914 | .append(String.format(" %,d", totalStats.getPossibleBans())); 915 | } 916 | 917 | response.append(ChatColorType.NORMAL) 918 | .append(" Confirmed Bans:") 919 | .append(ChatColorType.HIGHLIGHT) 920 | .append(String.format(" %,d", totalStats.getConfirmedBans())); 921 | 922 | PlayerStats manualStats = map.get(PlayerStatsType.MANUAL); 923 | if (manualStats != null && manualStats.getNamesUploaded() > 0) 924 | { 925 | if (detailLevel == StatsCommandDetailLevel.DETAILED) 926 | { 927 | response.append(ChatColorType.NORMAL) 928 | .append(" Manual Flags:") 929 | .append(ChatColorType.HIGHLIGHT) 930 | .append(String.format(" %,d", manualStats.getNamesUploaded())) 931 | .append(ChatColorType.NORMAL) 932 | .append(" Manual Possible Bans:") 933 | .append(ChatColorType.HIGHLIGHT) 934 | .append(String.format(" %,d", manualStats.getPossibleBans())); 935 | } 936 | 937 | response.append(ChatColorType.NORMAL) 938 | .append(" Manual Confirmed Bans:") 939 | .append(ChatColorType.HIGHLIGHT) 940 | .append(String.format(" %,d", manualStats.getConfirmedBans())); 941 | 942 | response.append(ChatColorType.NORMAL) 943 | .append(" Manual Flag Accuracy:") 944 | .append(ChatColorType.HIGHLIGHT) 945 | .append(new DecimalFormat(" 0.00%").format(manualStats.getAccuracy())); 946 | } 947 | } 948 | 949 | final String builtResponse = response.build(); 950 | final MessageNode messageNode = chatMessage.getMessageNode(); 951 | 952 | clientThread.invokeLater(() -> 953 | { 954 | messageNode.setRuneLiteFormatMessage(builtResponse); 955 | client.refreshChat(); 956 | }); 957 | } 958 | }); 959 | } 960 | 961 | @Subscribe 962 | private void onMenuEntryAdded(MenuEntryAdded event) 963 | { 964 | if (!config.addPredictMenuOption()) 965 | { 966 | return; 967 | } 968 | 969 | if (event.getType() != MenuAction.CC_OP.getId() && event.getType() != MenuAction.CC_OP_LOW_PRIORITY.getId()) 970 | { 971 | return; 972 | } 973 | 974 | final int componentId = event.getActionParam1(); 975 | final int groupId = WidgetUtil.componentToInterface(componentId); 976 | final String option = event.getOption(); 977 | 978 | if (groupId == InterfaceID.FRIEND_LIST && option.equals("Delete") 979 | || groupId == InterfaceID.FRIENDS_CHAT && (option.equals("Add ignore") || option.equals("Remove friend")) 980 | || groupId == InterfaceID.CHATBOX && (option.equals("Add ignore") || option.equals("Message")) 981 | || groupId == InterfaceID.IGNORE_LIST && option.equals("Delete") 982 | || (componentId == ComponentID.CLAN_MEMBERS || componentId == ComponentID.CLAN_GUEST_MEMBERS) && (option.equals("Add ignore") || option.equals("Remove friend")) 983 | || groupId == InterfaceID.PRIVATE_CHAT && (option.equals("Add ignore") || option.equals("Message")) 984 | || groupId == InterfaceID.GROUP_IRON && (option.equals("Add friend") || option.equals("Remove friend") || option.equals("Remove ignore"))) 985 | { 986 | // TODO: Properly use the new menu entry callbacks 987 | client.createMenuEntry(-1) 988 | .setOption(getPredictOption(event.getTarget())) 989 | .setTarget(event.getTarget()) 990 | .setType(MenuAction.RUNELITE) 991 | .setParam0(event.getActionParam0()) 992 | .setParam1(event.getActionParam1()) 993 | .setIdentifier(event.getIdentifier()); 994 | } 995 | } 996 | 997 | @Subscribe 998 | private void onMenuOpened(MenuOpened event) 999 | { 1000 | // If neither color changing options are set, this is unnecessary 1001 | if (config.predictOptionDefaultColor() == null && config.predictOptionFlaggedColor() == null) 1002 | { 1003 | return; 1004 | } 1005 | 1006 | final WorldView wv = client.getTopLevelWorldView(); 1007 | if (wv == null) 1008 | { 1009 | return; 1010 | } 1011 | 1012 | boolean changeReportOption = config.applyPredictColorsOnReportOption(); 1013 | // Do this once when the menu opens 1014 | // Avoids having to loop the menu entries on every 'added' event 1015 | MenuEntry[] menuEntries = event.getMenuEntries(); 1016 | for (MenuEntry entry : menuEntries) 1017 | { 1018 | int type = entry.getType().getId(); 1019 | if (type >= MenuAction.MENU_ACTION_DEPRIORITIZE_OFFSET) 1020 | { 1021 | type -= MenuAction.MENU_ACTION_DEPRIORITIZE_OFFSET; 1022 | } 1023 | 1024 | if (type == MenuAction.RUNELITE_PLAYER.getId() 1025 | && entry.getOption().equals(PREDICT_OPTION)) 1026 | { 1027 | Player player = wv.players().byIndex(entry.getIdentifier()); 1028 | if (player != null) 1029 | { 1030 | entry.setOption(getPredictOption(player.getName())); 1031 | } 1032 | } 1033 | 1034 | // Check for Report option 1035 | if (changeReportOption && entry.getOption().equals(REPORT_OPTION) 1036 | && (PLAYER_MENU_ACTIONS.contains(entry.getType()) || entry.getType() == MenuAction.CC_OP_LOW_PRIORITY)) 1037 | { 1038 | Player player = wv.players().byIndex(entry.getIdentifier()); 1039 | if (player != null) 1040 | { 1041 | entry.setOption(getReportOption(player.getName())); 1042 | } 1043 | } 1044 | } 1045 | } 1046 | 1047 | @Subscribe 1048 | private void onMenuOptionClicked(MenuOptionClicked event) 1049 | { 1050 | String optionText = Text.removeTags(event.getMenuOption()); 1051 | if (((event.getMenuAction() == MenuAction.RUNELITE || event.getMenuAction() == MenuAction.RUNELITE_PLAYER) 1052 | && optionText.equals(PREDICT_OPTION)) 1053 | || (config.predictOnReport() && (PLAYER_MENU_ACTIONS.contains(event.getMenuAction()) || event.getMenuAction() == MenuAction.CC_OP_LOW_PRIORITY) 1054 | && optionText.equals(REPORT_OPTION))) 1055 | { 1056 | String name; 1057 | if (event.getMenuAction() == MenuAction.RUNELITE_PLAYER 1058 | || PLAYER_MENU_ACTIONS.contains(event.getMenuAction())) 1059 | { 1060 | WorldView wv = client.getTopLevelWorldView(); 1061 | if (wv == null) 1062 | { 1063 | return; 1064 | } 1065 | 1066 | Player player = wv.players().byIndex(event.getId()); 1067 | if (player == null) 1068 | { 1069 | return; 1070 | } 1071 | 1072 | name = player.getName(); 1073 | } 1074 | else 1075 | { 1076 | name = event.getMenuTarget(); 1077 | } 1078 | 1079 | if (name != null) 1080 | { 1081 | String toPredict = Text.removeTags(name); 1082 | if (config.predictOptionCopyName()) 1083 | { 1084 | Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(toPredict), null); 1085 | } 1086 | predictPlayer(toPredict); 1087 | } 1088 | } 1089 | } 1090 | 1091 | @Subscribe 1092 | private void onWorldChanged(WorldChanged event) 1093 | { 1094 | processCurrentWorld(); 1095 | } 1096 | 1097 | /** 1098 | * Opens the plugin panel and sends over {@code playerName} to {@link BotDetectorPanel#predictPlayer(String)} for prediction. 1099 | * @param playerName The player name to predict. 1100 | */ 1101 | public void predictPlayer(String playerName) 1102 | { 1103 | SwingUtilities.invokeLater(() -> 1104 | { 1105 | clientToolbar.openPanel(navButton); 1106 | panel.predictPlayer(playerName); 1107 | }); 1108 | } 1109 | 1110 | /** 1111 | * Sends a message to the in-game chatbox if {@link BotDetectorConfig#enableChatStatusMessages()} is {@code true}. 1112 | * @param msg The message to send. 1113 | */ 1114 | public void sendChatStatusMessage(String msg) 1115 | { 1116 | sendChatStatusMessage(msg, false); 1117 | } 1118 | 1119 | /** 1120 | * Sends a message to the in-game chatbox. 1121 | * @param msg The message to send. 1122 | * @param forceShow If {@code true}, bypasses {@link BotDetectorConfig#enableChatStatusMessages()}. 1123 | */ 1124 | public void sendChatStatusMessage(String msg, boolean forceShow) 1125 | { 1126 | if ((forceShow || config.enableChatStatusMessages()) && loggedPlayerName != null) 1127 | { 1128 | final String message = new ChatMessageBuilder() 1129 | .append(ChatColorType.HIGHLIGHT) 1130 | .append(CHAT_MESSAGE_HEADER + msg) 1131 | .build(); 1132 | 1133 | chatMessageManager.queue( 1134 | QueuedMessage.builder() 1135 | .type(ChatMessageType.CONSOLE) 1136 | .runeLiteFormattedMessage(message) 1137 | .build()); 1138 | } 1139 | } 1140 | 1141 | /** 1142 | * Sets various class variables and panel warnings according to what {@link Client#getWorld()} returns. 1143 | */ 1144 | private void processCurrentWorld() 1145 | { 1146 | currentWorldNumber = client.getWorld(); 1147 | EnumSet types = client.getWorldType(); 1148 | isCurrentWorldMembers = types.contains(WorldType.MEMBERS); 1149 | isCurrentWorldPVP = types.contains(WorldType.PVP); 1150 | isCurrentWorldBlocked = !ALLOWED_PROFILE_TYPES.contains(RuneScapeProfileType.getCurrent(client)); 1151 | SwingUtilities.invokeLater(() -> 1152 | panel.setWarningVisible(BotDetectorPanel.WarningLabel.BLOCKED_WORLD, isCurrentWorldBlocked)); 1153 | } 1154 | 1155 | /** 1156 | * Gets the name that should be used when an uploader name is required, 1157 | * according to {@link BotDetectorConfig#enableAnonymousUploading()}. 1158 | * @return {@link #loggedPlayerName} or {@link #ANONYMOUS_USER_NAME}. Returns {@code null} if logged out. 1159 | */ 1160 | public String getUploaderName() 1161 | { 1162 | return getUploaderName(false); 1163 | } 1164 | 1165 | /** 1166 | * Gets the name that should be used when an uploader name is required, 1167 | * according to {@link BotDetectorConfig#enableAnonymousUploading()}. 1168 | * @param useAnonymousUUIDFormat Whether or not to use the UUID anonymous username format. 1169 | * @return {@link #loggedPlayerName} if not anonymous. When anonymous, returns 1170 | * {@link #ANONYMOUS_USER_NAME_UUID_FORMAT} with {@link #anonymousUUID} 1171 | * or simply {@link #ANONYMOUS_USER_NAME} depending on {@code useAnonymousUUIDFormat}. 1172 | * Returns {@code null} if logged out. 1173 | */ 1174 | public String getUploaderName(boolean useAnonymousUUIDFormat) 1175 | { 1176 | if (loggedPlayerName == null) 1177 | { 1178 | return null; 1179 | } 1180 | 1181 | if (config.enableAnonymousUploading()) 1182 | { 1183 | return useAnonymousUUIDFormat ? 1184 | String.format(ANONYMOUS_USER_NAME_UUID_FORMAT, anonymousUUID) 1185 | : ANONYMOUS_USER_NAME; 1186 | } 1187 | 1188 | return loggedPlayerName; 1189 | } 1190 | 1191 | /** 1192 | * Gets the correct variant of {@link #PREDICT_OPTION} to show for the given {@code player}. 1193 | * @param playerName The player to get the menu option string for. 1194 | * @return A variant of {@link #PREDICT_OPTION} prepended or not with some color. 1195 | */ 1196 | private String getPredictOption(String playerName) 1197 | { 1198 | return getMenuOption(playerName, PREDICT_OPTION); 1199 | } 1200 | 1201 | /** 1202 | * Gets the correct variant of {@link #REPORT_OPTION} to show for the given {@code player}. 1203 | * @param playerName The player to get the menu option string for. 1204 | * @return A variant of {@link #REPORT_OPTION} prepended or not with some color. 1205 | */ 1206 | private String getReportOption(String playerName) 1207 | { 1208 | return getMenuOption(playerName, REPORT_OPTION); 1209 | } 1210 | 1211 | /** 1212 | * Gets the correct variant of the given option string to show for the given {@code player}. 1213 | * @param playerName The player to get the menu option string for. 1214 | * @return A variant of the option string prepended or not with some color. 1215 | */ 1216 | private String getMenuOption(String playerName, String option) 1217 | { 1218 | CaseInsensitiveString name = normalizeAndWrapPlayerName(playerName); 1219 | Color prepend = (feedbackedPlayers.containsKey(name) || flaggedPlayers.containsKey(name)) ? 1220 | config.predictOptionFlaggedColor() : config.predictOptionDefaultColor(); 1221 | 1222 | return prepend != null ? ColorUtil.prependColorTag(option, prepend) : option; 1223 | } 1224 | 1225 | /** 1226 | * Normalizes the given {@code playerName} by sanitizing the player name string, 1227 | * removing any Jagex tags and replacing any {@code _} or {@code -} with spaces. 1228 | * @param playerName The player name to normalize. 1229 | * @return The normalized {@code playerName}. 1230 | */ 1231 | public static String normalizePlayerName(String playerName) 1232 | { 1233 | if (playerName == null) 1234 | { 1235 | return null; 1236 | } 1237 | 1238 | return Text.removeTags(Text.toJagexName(playerName)); 1239 | } 1240 | 1241 | /** 1242 | * Normalizes the given {@code playerName} using {@link #normalizePlayerName(String)}, 1243 | * then wraps the resulting {@link String} with {@link CaseInsensitiveString#wrap(String)}. 1244 | * @param playerName The player name to normalize and wrap. 1245 | * @return A {@link CaseInsensitiveString} containing the normalized {@code playerName}. 1246 | */ 1247 | public static CaseInsensitiveString normalizeAndWrapPlayerName(String playerName) 1248 | { 1249 | return wrap(normalizePlayerName(playerName)); 1250 | } 1251 | 1252 | //region Commands 1253 | 1254 | /** 1255 | * Manually executes {@link #flushPlayersToClient(boolean, boolean)}, 1256 | * first checking that {@link #lastFlush} did not occur within {@link #MANUAL_FLUSH_COOLDOWN_SECONDS}. 1257 | */ 1258 | private void manualFlushCommand() 1259 | { 1260 | Instant canFlush = lastFlush.plusSeconds(MANUAL_FLUSH_COOLDOWN_SECONDS); 1261 | Instant now = Instant.now(); 1262 | if (now.isAfter(canFlush)) 1263 | { 1264 | if (flushPlayersToClient(true, true) == null) 1265 | { 1266 | sendChatStatusMessage("No player sightings to flush!", true); 1267 | } 1268 | } 1269 | else 1270 | { 1271 | long secs = (Duration.between(now, canFlush).toMillis() / 1000) + 1; 1272 | sendChatStatusMessage("Please wait " + secs + " seconds before manually flushing players.", true); 1273 | } 1274 | } 1275 | 1276 | /** 1277 | * Manually force a full rescan of all players in {@link Client#getPlayers()} using {@link #processPlayer(Player)}. 1278 | */ 1279 | private void manualSightCommand() 1280 | { 1281 | if (isCurrentWorldBlocked) 1282 | { 1283 | sendChatStatusMessage("Cannot refresh player sightings on a blocked world.", true); 1284 | } 1285 | else if (client.getGameState() != GameState.LOGGED_IN) 1286 | { 1287 | // Just in case! 1288 | sendChatStatusMessage("Current game state must be 'LOGGED_IN'!", true); 1289 | } 1290 | else 1291 | { 1292 | client.getPlayers().forEach(this::processPlayer); 1293 | sendChatStatusMessage("Player sightings refreshed.", true); 1294 | } 1295 | } 1296 | 1297 | /** 1298 | * Manually force executes {@link #refreshPlayerStats(boolean)}. 1299 | */ 1300 | private void manualRefreshStatsCommand() 1301 | { 1302 | refreshPlayerStats(true); 1303 | sendChatStatusMessage("Refreshing player stats...", true); 1304 | } 1305 | 1306 | /** 1307 | * Shows or hides the player ID field in the plugin panel using {@link BotDetectorPanel#setPlayerIdVisible(boolean)}. 1308 | * @param args String arguments from {@link CommandExecuted#getArguments()}, requires 1 argument being either "0" or "1". 1309 | */ 1310 | private void showHideIdCommand(String[] args) 1311 | { 1312 | String arg = args.length > 0 ? args[0] : ""; 1313 | switch (arg) 1314 | { 1315 | case "1": 1316 | SwingUtilities.invokeLater(() -> panel.setPlayerIdVisible(true)); 1317 | sendChatStatusMessage("Player ID field added to panel.", true); 1318 | break; 1319 | case "0": 1320 | SwingUtilities.invokeLater(() -> panel.setPlayerIdVisible(false)); 1321 | sendChatStatusMessage("Player ID field hidden.", true); 1322 | break; 1323 | default: 1324 | sendChatStatusMessage("Argument must be 0 or 1.", true); 1325 | break; 1326 | } 1327 | } 1328 | 1329 | /** 1330 | * Gets the currently loaded {@link AuthToken} and copies it into the user's system clipboard. 1331 | */ 1332 | private void putAuthTokenIntoClipboardCommand() 1333 | { 1334 | if (authToken.getTokenType() == AuthTokenType.NONE) 1335 | { 1336 | sendChatStatusMessage("No auth token currently set.", true); 1337 | } 1338 | else 1339 | { 1340 | Toolkit.getDefaultToolkit().getSystemClipboard().setContents( 1341 | new StringSelection(authToken.toFullToken()), null); 1342 | sendChatStatusMessage(authToken.getTokenType() + " auth token copied to clipboard.", true); 1343 | } 1344 | } 1345 | 1346 | /** 1347 | * Sets the {@link AuthToken} saved in {@link BotDetectorConfig#authFullToken()} to the contents of the clipboard, 1348 | * assuming the contents respect the defined token format in {@link AuthToken#AUTH_TOKEN_PATTERN}. 1349 | */ 1350 | private void setAuthTokenFromClipboardCommand() 1351 | { 1352 | final String clipboardText; 1353 | try 1354 | { 1355 | clipboardText = Toolkit.getDefaultToolkit() 1356 | .getSystemClipboard() 1357 | .getData(DataFlavor.stringFlavor) 1358 | .toString().trim(); 1359 | } 1360 | catch (IOException | UnsupportedFlavorException ex) 1361 | { 1362 | sendChatStatusMessage("Unable to read system clipboard for dev token.", true); 1363 | log.warn("Error reading clipboard", ex); 1364 | return; 1365 | } 1366 | 1367 | AuthToken token = AuthToken.fromFullToken(clipboardText); 1368 | 1369 | if (token.getTokenType() == AuthTokenType.NONE) 1370 | { 1371 | sendChatStatusMessage(AuthToken.AUTH_TOKEN_DESCRIPTION_MESSAGE, true); 1372 | } 1373 | else 1374 | { 1375 | authToken = token; 1376 | config.setAuthFullToken(token.toFullToken()); 1377 | sendChatStatusMessage(token.getTokenType() + " auth token successfully set from clipboard.", true); 1378 | } 1379 | } 1380 | 1381 | /** 1382 | * Clears the current {@link AuthToken} saved in {@link BotDetectorConfig#authFullToken()}. 1383 | */ 1384 | private void clearAuthTokenCommand() 1385 | { 1386 | authToken = AuthToken.EMPTY_TOKEN; 1387 | config.setAuthFullToken(null); 1388 | sendChatStatusMessage("Auth token cleared.", true); 1389 | } 1390 | 1391 | /** 1392 | * Toggles the config value in {@link BotDetectorConfig#showDiscordVerificationErrors()} and notifies the user of the change. 1393 | */ 1394 | private void toggleShowDiscordVerificationErrors() 1395 | { 1396 | boolean newVal = !config.showDiscordVerificationErrors(); 1397 | config.setShowDiscordVerificationErrors(newVal); 1398 | if (newVal) 1399 | { 1400 | sendChatStatusMessage("Discord verification errors will now be shown in the chat", true); 1401 | } 1402 | else 1403 | { 1404 | sendChatStatusMessage("Discord verification errors will no longer be shown in the chat", true); 1405 | } 1406 | } 1407 | 1408 | //endregion 1409 | 1410 | 1411 | /** 1412 | * Displays an error message about being unable to parse a plugin version and links to the Bot Detector Discord. 1413 | */ 1414 | private void displayPluginVersionError() 1415 | { 1416 | JEditorPane ep = new JEditorPane("text/html", 1417 | "Could not parse the plugin version from the properties file!" 1418 | + "
This should never happen! Please contact us on our Discord."); 1420 | ep.addHyperlinkListener(e -> 1421 | { 1422 | if (e.getEventType().equals(HyperlinkEvent.EventType.ACTIVATED)) 1423 | { 1424 | LinkBrowser.browse(e.getURL().toString()); 1425 | } 1426 | }); 1427 | ep.setEditable(false); 1428 | JOptionPane.showOptionDialog(null, ep, 1429 | "Error starting Bot Detector!", JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE, 1430 | null, new String[]{"Ok"}, "Ok"); 1431 | } 1432 | } 1433 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/events/BotDetectorPanelActivated.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | package com.botdetector.events; 27 | 28 | /** 29 | * Event for when the {@link com.botdetector.ui.BotDetectorPanel} is activated. 30 | */ 31 | public class BotDetectorPanelActivated 32 | { 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/http/BotDetectorClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | package com.botdetector.http; 27 | 28 | import com.botdetector.BotDetectorPlugin; 29 | import com.botdetector.model.FeedbackPredictionLabel; 30 | import com.botdetector.model.PlayerSighting; 31 | import com.botdetector.model.PlayerStats; 32 | import com.botdetector.model.PlayerStatsType; 33 | import com.botdetector.model.Prediction; 34 | import com.google.common.base.Strings; 35 | import com.google.common.collect.ImmutableList; 36 | import com.google.common.collect.ImmutableMap; 37 | import com.google.gson.Gson; 38 | import com.google.gson.JsonDeserializationContext; 39 | import com.google.gson.JsonDeserializer; 40 | import com.google.gson.JsonElement; 41 | import com.google.gson.JsonObject; 42 | import com.google.gson.JsonParseException; 43 | import com.google.gson.JsonPrimitive; 44 | import com.google.gson.JsonSerializationContext; 45 | import com.google.gson.JsonSerializer; 46 | import com.google.gson.JsonSyntaxException; 47 | import com.google.gson.annotations.SerializedName; 48 | import com.google.gson.reflect.TypeToken; 49 | import com.google.inject.Inject; 50 | import com.google.inject.Singleton; 51 | import java.io.IOException; 52 | import java.lang.reflect.Type; 53 | import java.time.Instant; 54 | import java.util.Collection; 55 | import java.util.List; 56 | import java.util.Map; 57 | import java.util.Optional; 58 | import java.util.concurrent.CompletableFuture; 59 | import java.util.concurrent.TimeUnit; 60 | import java.util.function.Supplier; 61 | import java.util.stream.Collectors; 62 | import lombok.AllArgsConstructor; 63 | import lombok.Getter; 64 | import lombok.Setter; 65 | import lombok.Value; 66 | import lombok.extern.slf4j.Slf4j; 67 | import net.runelite.api.kit.KitType; 68 | import okhttp3.Call; 69 | import okhttp3.Callback; 70 | import okhttp3.HttpUrl; 71 | import okhttp3.MediaType; 72 | import okhttp3.OkHttpClient; 73 | import okhttp3.Request; 74 | import okhttp3.RequestBody; 75 | import okhttp3.Response; 76 | 77 | /** 78 | * Class containing various methods to interact with the Bot Detector API. 79 | */ 80 | @Slf4j 81 | @Singleton 82 | public class BotDetectorClient 83 | { 84 | private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); 85 | private static final String API_VERSION_FALLBACK_WORD = "latest"; 86 | private static final HttpUrl BASE_HTTP_URL = HttpUrl.parse( 87 | System.getProperty("BotDetectorAPIPath", "https://api.prd.osrsbotdetector.com")); 88 | private static final Supplier CURRENT_EPOCH_SUPPLIER = () -> String.valueOf(Instant.now().getEpochSecond()); 89 | 90 | @Getter 91 | @AllArgsConstructor 92 | private enum ApiPath 93 | { 94 | DETECTION("v1/report"), 95 | PLAYER_STATS_PASSIVE("v1/report/count"), 96 | PLAYER_STATS_MANUAL("v1/report/manual/count"), 97 | PLAYER_STATS_FEEDBACK("v1/feedback/count"), 98 | PREDICTION("v1/prediction"), 99 | FEEDBACK("v1/feedback/"), 100 | VERIFY_DISCORD("site/discord_user/") 101 | ; 102 | 103 | final String path; 104 | } 105 | 106 | public OkHttpClient okHttpClient; 107 | 108 | @Inject 109 | private Gson gson; 110 | 111 | @Getter 112 | @Setter 113 | private String pluginVersion; 114 | 115 | private final Supplier pluginVersionSupplier = () -> 116 | (pluginVersion != null && !pluginVersion.isEmpty()) ? pluginVersion : API_VERSION_FALLBACK_WORD; 117 | 118 | /** 119 | * Constructs a base URL for the given {@code path}. 120 | * @param path The path to get the base URL for. 121 | * @param addVersion Whether to add a version prefix. 122 | * @return The base URL for the given {@code path}. 123 | */ 124 | private HttpUrl getUrl(ApiPath path, boolean addVersion) 125 | { 126 | HttpUrl.Builder builder = BASE_HTTP_URL.newBuilder(); 127 | 128 | if (addVersion) 129 | { 130 | builder.addPathSegment(pluginVersionSupplier.get()); 131 | } 132 | 133 | return builder.addPathSegments(path.getPath()).build(); 134 | } 135 | 136 | /** 137 | * Constructs a base URL for the given {@code path} with no version prefix. 138 | * @param path The path to get the base URL for 139 | * @return The base URL for the given {@code path}. 140 | */ 141 | private HttpUrl getUrl(ApiPath path) 142 | { 143 | return getUrl(path, false); 144 | } 145 | 146 | @Inject 147 | public BotDetectorClient(OkHttpClient rlClient) 148 | { 149 | okHttpClient = rlClient.newBuilder() 150 | .pingInterval(0, TimeUnit.SECONDS) 151 | .connectTimeout(30, TimeUnit.SECONDS) 152 | .readTimeout(30, TimeUnit.SECONDS) 153 | .addNetworkInterceptor(chain -> 154 | { 155 | Request headerRequest = chain.request() 156 | .newBuilder() 157 | .header("Request-Epoch", CURRENT_EPOCH_SUPPLIER.get()) 158 | .header("Plugin-Version", pluginVersionSupplier.get()) 159 | .build(); 160 | return chain.proceed(headerRequest); 161 | }) 162 | .build(); 163 | } 164 | 165 | /** 166 | * Sends a single {@link PlayerSighting} to the API to be persisted in the Bot Detector database. 167 | * @param sighting The sighting to send. 168 | * @param uploaderName The user's player name (See {@link BotDetectorPlugin#getUploaderName()}). 169 | * @param manual Whether or not the given sighting is to be manually flagged as a bot by the user. 170 | * @return A future that will eventually return a boolean indicating success. 171 | */ 172 | public CompletableFuture sendSighting(PlayerSighting sighting, String uploaderName, boolean manual) 173 | { 174 | return sendSightings(ImmutableList.of(sighting), uploaderName, manual); 175 | } 176 | 177 | /** 178 | * Sends a collection of {@link PlayerSighting}s to the API to be persisted in the Bot Detector database. 179 | * @param sightings The collection of sightings to send. 180 | * @param uploaderName The user's player name (See {@link BotDetectorPlugin#getUploaderName()}). 181 | * @param manual Whether or not the given sightings are to be manually flagged as bots by the user. 182 | * @return A future that will eventually return a boolean indicating success. 183 | */ 184 | public CompletableFuture sendSightings(Collection sightings, String uploaderName, boolean manual) 185 | { 186 | List wrappedList = sightings.stream() 187 | .map(p -> new PlayerSightingWrapper(uploaderName, manual, p)).collect(Collectors.toList()); 188 | 189 | Gson bdGson = gson.newBuilder().enableComplexMapKeySerialization() 190 | .registerTypeAdapter(PlayerSightingWrapper.class, new PlayerSightingWrapperSerializer()) 191 | .registerTypeAdapter(KitType.class, new KitTypeSerializer()) 192 | .registerTypeAdapter(Boolean.class, new BooleanToZeroOneConverter()) 193 | .registerTypeAdapter(Instant.class, new InstantSecondsConverter()) 194 | .create(); 195 | 196 | Request request = new Request.Builder() 197 | .url(getUrl(ApiPath.DETECTION).newBuilder() 198 | .build()) 199 | .post(RequestBody.create(JSON, bdGson.toJson(wrappedList))) 200 | .build(); 201 | 202 | CompletableFuture future = new CompletableFuture<>(); 203 | okHttpClient.newCall(request).enqueue(new Callback() 204 | { 205 | @Override 206 | public void onFailure(Call call, IOException e) 207 | { 208 | log.warn("Error sending player sighting data", e); 209 | future.completeExceptionally(e); 210 | } 211 | 212 | @Override 213 | public void onResponse(Call call, Response response) 214 | { 215 | try 216 | { 217 | if (!response.isSuccessful()) 218 | { 219 | throw getIOException(response); 220 | } 221 | 222 | future.complete(true); 223 | } 224 | catch (IOException e) 225 | { 226 | log.warn("Error sending player sighting data", e); 227 | future.completeExceptionally(e); 228 | } 229 | finally 230 | { 231 | response.close(); 232 | } 233 | } 234 | }); 235 | 236 | return future; 237 | } 238 | 239 | /** 240 | * Tokenized API route to verify the given player name and code pair for Discord linking. 241 | * @param token The auth token to use. 242 | * @param nameToVerify The player name up for verification. 243 | * @param code The code given by the player. 244 | * @return A future that will eventually return a boolean indicating success. 245 | */ 246 | public CompletableFuture verifyDiscord(String token, String nameToVerify, String code) 247 | { 248 | Request request = new Request.Builder() 249 | .url(getUrl(ApiPath.VERIFY_DISCORD, true).newBuilder() 250 | .addPathSegment(token) 251 | .build()) 252 | .post(RequestBody.create(JSON, gson.toJson(new DiscordVerification(nameToVerify, code)))) 253 | .build(); 254 | 255 | CompletableFuture future = new CompletableFuture<>(); 256 | okHttpClient.newCall(request).enqueue(new Callback() 257 | { 258 | @Override 259 | public void onFailure(Call call, IOException e) 260 | { 261 | log.warn("Error verifying discord user", e); 262 | future.completeExceptionally(e); 263 | } 264 | 265 | @Override 266 | public void onResponse(Call call, Response response) 267 | { 268 | try 269 | { 270 | // TODO: Differenciate between bad token and failed auth (return false) 271 | if (!response.isSuccessful()) 272 | { 273 | if (response.code() == 401) 274 | { 275 | throw new UnauthorizedTokenException("Invalid or unauthorized token for operation"); 276 | } 277 | else 278 | { 279 | throw getIOException(response); 280 | } 281 | } 282 | 283 | future.complete(true); 284 | } 285 | catch (UnauthorizedTokenException | IOException e) 286 | { 287 | log.warn("Error verifying discord user", e); 288 | future.completeExceptionally(e); 289 | } 290 | finally 291 | { 292 | response.close(); 293 | } 294 | } 295 | }); 296 | 297 | return future; 298 | } 299 | 300 | /** 301 | * Sends a feedback to the API for the given prediction. 302 | * @param pred The prediction object to give a feedback for. 303 | * @param uploaderName The user's player name (See {@link BotDetectorPlugin#getUploaderName()}). 304 | * @param proposedLabel The user's proposed label and feedback. 305 | * @param feedbackText The user's feedback text to include with the feedback. 306 | * @return A future that will eventually return a boolean indicating success. 307 | */ 308 | public CompletableFuture sendFeedback(Prediction pred, String uploaderName, FeedbackPredictionLabel proposedLabel, String feedbackText) 309 | { 310 | Request request = new Request.Builder() 311 | .url(getUrl(ApiPath.FEEDBACK)) 312 | .post(RequestBody.create(JSON, gson.toJson(new PredictionFeedback( 313 | uploaderName, 314 | proposedLabel.getFeedbackValue().getApiValue(), 315 | pred.getPredictionLabel(), 316 | Optional.ofNullable(pred.getConfidence()).orElse(0.0), 317 | pred.getPlayerId(), 318 | proposedLabel.getLabel(), 319 | proposedLabel.getLabelConfidence(), 320 | feedbackText 321 | )))).build(); 322 | 323 | CompletableFuture future = new CompletableFuture<>(); 324 | okHttpClient.newCall(request).enqueue(new Callback() 325 | { 326 | @Override 327 | public void onFailure(Call call, IOException e) 328 | { 329 | log.warn("Error sending prediction feedback", e); 330 | future.completeExceptionally(e); 331 | } 332 | 333 | @Override 334 | public void onResponse(Call call, Response response) 335 | { 336 | try 337 | { 338 | if (!response.isSuccessful()) 339 | { 340 | throw getIOException(response); 341 | } 342 | 343 | future.complete(true); 344 | } 345 | catch (IOException e) 346 | { 347 | log.warn("Error sending prediction feedback", e); 348 | future.completeExceptionally(e); 349 | } 350 | finally 351 | { 352 | response.close(); 353 | } 354 | } 355 | }); 356 | 357 | return future; 358 | } 359 | 360 | /** 361 | * Requests a bot prediction for the given {@code playerName}. 362 | * Breakdown will be provided by default in special cases (see {@link BotDetectorClient#requestPrediction(String, boolean)}). 363 | * @param playerName The player name to predict. 364 | * @return A future that will eventually return the player's bot prediction. 365 | */ 366 | public CompletableFuture requestPrediction(String playerName) 367 | { 368 | return requestPrediction(playerName, true); 369 | } 370 | 371 | /** 372 | * Requests a bot prediction for the given {@code playerName}. 373 | * @param playerName The player name to predict. 374 | * @param receiveBreakdownOnSpecialCases Whether to receive a prediction breakdown in special cases, such as "Player Stats Too Low". 375 | * @return A future that will eventually return the player's bot prediction. 376 | */ 377 | public CompletableFuture requestPrediction(String playerName, boolean receiveBreakdownOnSpecialCases) 378 | { 379 | Request request = new Request.Builder() 380 | .url(getUrl(ApiPath.PREDICTION).newBuilder() 381 | .addQueryParameter("name", playerName) 382 | .addQueryParameter("breakdown", Boolean.toString(receiveBreakdownOnSpecialCases)) 383 | .build()) 384 | .build(); 385 | 386 | CompletableFuture future = new CompletableFuture<>(); 387 | okHttpClient.newCall(request).enqueue(new Callback() 388 | { 389 | @Override 390 | public void onFailure(Call call, IOException e) 391 | { 392 | log.warn("Error obtaining player prediction data", e); 393 | future.completeExceptionally(e); 394 | } 395 | 396 | @Override 397 | public void onResponse(Call call, Response response) 398 | { 399 | try 400 | { 401 | future.complete(processResponse(gson, response, Prediction.class)); 402 | } 403 | catch (IOException e) 404 | { 405 | log.warn("Error obtaining player prediction data", e); 406 | future.completeExceptionally(e); 407 | } 408 | finally 409 | { 410 | response.close(); 411 | } 412 | } 413 | }); 414 | 415 | return future; 416 | } 417 | 418 | /** 419 | * Requests the uploading contributions for the given {@code playerName}. 420 | * @param playerName The name to request the uploading contributions. 421 | * @return A future that will eventually return the player's statistics. 422 | */ 423 | public CompletableFuture> requestPlayerStats(String playerName) 424 | { 425 | Gson bdGson = gson.newBuilder() 426 | .registerTypeAdapter(boolean.class, new BooleanToZeroOneConverter()) 427 | .create(); 428 | 429 | Request requestP = new Request.Builder() 430 | .url(getUrl(ApiPath.PLAYER_STATS_PASSIVE).newBuilder() 431 | .addQueryParameter("name", playerName) 432 | .build()) 433 | .build(); 434 | 435 | Request requestM = new Request.Builder() 436 | .url(getUrl(ApiPath.PLAYER_STATS_MANUAL).newBuilder() 437 | .addQueryParameter("name", playerName) 438 | .build()) 439 | .build(); 440 | 441 | Request requestF = new Request.Builder() 442 | .url(getUrl(ApiPath.PLAYER_STATS_FEEDBACK).newBuilder() 443 | .addQueryParameter("name", playerName) 444 | .build()) 445 | .build(); 446 | 447 | CompletableFuture> passiveFuture = new CompletableFuture<>(); 448 | CompletableFuture> manualFuture = new CompletableFuture<>(); 449 | CompletableFuture> feedbackFuture = new CompletableFuture<>(); 450 | 451 | okHttpClient.newCall(requestP).enqueue(new PlayerStatsCallback(passiveFuture, bdGson)); 452 | okHttpClient.newCall(requestM).enqueue(new PlayerStatsCallback(manualFuture, bdGson)); 453 | okHttpClient.newCall(requestF).enqueue(new PlayerStatsCallback(feedbackFuture, bdGson)); 454 | 455 | CompletableFuture> finalFuture = new CompletableFuture<>(); 456 | 457 | // Doing this so we log only the first future failing, not all 3 within the callback. 458 | CompletableFuture.allOf(passiveFuture, manualFuture, feedbackFuture).whenComplete((v, e) -> 459 | { 460 | if (e != null) 461 | { 462 | // allOf will send a CompletionException when one of the futures fail, just get the cause. 463 | log.warn("Error obtaining player stats data", e.getCause()); 464 | finalFuture.completeExceptionally(e.getCause()); 465 | } 466 | else 467 | { 468 | finalFuture.complete(processPlayerStats( 469 | passiveFuture.join(), manualFuture.join(), feedbackFuture.join())); 470 | } 471 | }); 472 | 473 | return finalFuture; 474 | } 475 | 476 | /** 477 | * Utility class intended for {@link BotDetectorClient#requestPlayerStats(String)}. 478 | */ 479 | private class PlayerStatsCallback implements Callback 480 | { 481 | private final CompletableFuture> future; 482 | private final Gson gson; 483 | 484 | public PlayerStatsCallback(CompletableFuture> future, Gson gson) 485 | { 486 | this.future = future; 487 | this.gson = gson; 488 | } 489 | 490 | @Override 491 | public void onFailure(Call call, IOException e) 492 | { 493 | future.completeExceptionally(e); 494 | } 495 | 496 | @Override 497 | public void onResponse(Call call, Response response) throws IOException 498 | { 499 | try 500 | { 501 | future.complete(processResponse(gson, response, 502 | new TypeToken>() 503 | { 504 | }.getType())); 505 | } 506 | catch (IOException e) 507 | { 508 | future.completeExceptionally(e); 509 | } 510 | finally 511 | { 512 | response.close(); 513 | } 514 | } 515 | } 516 | 517 | /** 518 | * Processes the body of the given response and parses out the contained JSON object. 519 | * @param gson The {@link Gson} instance to use for parsing the JSON object in the {@code response}. 520 | * @param response The response containing the object to parse in {@link Response#body()}. 521 | * @param type The type of the JSON object to parse. 522 | * @param The type of the JSON object to parse, inferred from {@code type}. 523 | * @return The parsed object, or {@code null} if the API returned a 404. 524 | * @throws IOException If the response is unsuccessful or the {@link Response#body()} contains malformed data. 525 | */ 526 | private T processResponse(Gson gson, Response response, Type type) throws IOException 527 | { 528 | if (!response.isSuccessful()) 529 | { 530 | if (response.code() == 404) 531 | { 532 | return null; 533 | } 534 | 535 | throw getIOException(response); 536 | } 537 | 538 | try 539 | { 540 | return gson.fromJson(response.body().string(), type); 541 | } 542 | catch (IOException | IllegalStateException | JsonSyntaxException ex) 543 | { 544 | throw new IOException("Error parsing API response body", ex); 545 | } 546 | } 547 | 548 | /** 549 | * Gets the {@link IOException} to return for when {@link Response#isSuccessful()} returns false. 550 | * @param response The response object to get the {@link IOException} for. 551 | * @return The {@link IOException} with the appropriate message for the given {@code response}. 552 | */ 553 | private IOException getIOException(Response response) 554 | { 555 | int code = response.code(); 556 | if (code >= 400 && code < 500) 557 | { 558 | try 559 | { 560 | String body = response.body().string(); 561 | try 562 | { 563 | Map map = gson.fromJson(body, 564 | new TypeToken>() 565 | { 566 | }.getType()); 567 | 568 | // "error" has priority if it exists, else use "detail" (FastAPI) 569 | String error = map.get("error"); 570 | if (Strings.isNullOrEmpty(error)) 571 | { 572 | error = map.getOrDefault("detail", "Unknown " + code + " error from API"); 573 | } 574 | return new IOException(error); 575 | } 576 | catch (JsonSyntaxException ex) 577 | { 578 | // If can't parse, just log the response body 579 | // TODO: Parse actual error info received from FastAPI (details -> loc, msg, ctx, etc.) especially for 422 errors 580 | log.warn("Received HTTP error code " + code + " from API with the following response body:\n" + body); 581 | return new IOException("Error " + code + ", see log for more info"); 582 | } 583 | } 584 | catch (IOException ex) 585 | { 586 | return new IOException("Error " + code + " with no error info", ex); 587 | } 588 | } 589 | 590 | return new IOException("Error " + code + " from API"); 591 | } 592 | 593 | /** 594 | * Collects the given {@link PlayerStatsAPIItem} into a combined map that the plugin expects. 595 | * @param passive The passive usage stats from the API. 596 | * @param manual The manual flagging stats from the API. 597 | * @param feedback The feedback stats from the API. 598 | * @return The combined processed map expected by the plugin. 599 | */ 600 | private Map processPlayerStats(Collection passive, Collection manual, Collection feedback) 601 | { 602 | if (passive == null || manual == null || feedback == null) 603 | { 604 | return null; 605 | } 606 | 607 | PlayerStats passiveStats = countStats(passive, false); 608 | PlayerStats manualStats = countStats(manual, true); 609 | PlayerStats feedbackStats = countStats(feedback, false); 610 | 611 | PlayerStats totalStats = PlayerStats.builder() 612 | .namesUploaded(passiveStats.getNamesUploaded() + manualStats.getNamesUploaded()) 613 | .confirmedBans(passiveStats.getConfirmedBans() + manualStats.getConfirmedBans()) 614 | .possibleBans(passiveStats.getPossibleBans() + manualStats.getPossibleBans()) 615 | .feedbackSent(feedbackStats.getNamesUploaded()) // Might change the total/passive/manual thing in the future. 616 | .build(); 617 | 618 | return ImmutableMap.of( 619 | PlayerStatsType.TOTAL, totalStats, 620 | PlayerStatsType.PASSIVE, passiveStats, 621 | PlayerStatsType.MANUAL, manualStats 622 | ); 623 | } 624 | 625 | /** 626 | * Utility function for {@link BotDetectorClient#processPlayerStats(Collection, Collection, Collection)}. 627 | * Compile each element from the API into a {@link PlayerStats} object. 628 | * @param fromAPI The returned collections of player stats from the API to accumulate. 629 | * @param countIncorrect Intended for manual flagging stats. If true, count confirmed players into {@link PlayerStats#getIncorrectFlags()}. 630 | * @return The stats object with accumulated counts from the API. 631 | */ 632 | private PlayerStats countStats(Collection fromAPI, boolean countIncorrect) 633 | { 634 | long total = 0, confirmedBans = 0, possibleBans = 0, incorrectFlags = 0; 635 | for (PlayerStatsAPIItem item : fromAPI) 636 | { 637 | if (item.isBanned()) 638 | { 639 | confirmedBans += item.getCount(); 640 | } 641 | else 642 | { 643 | if (item.isPossibleBanned()) 644 | { 645 | possibleBans += item.getCount(); 646 | } 647 | 648 | if (countIncorrect && item.isPlayer()) 649 | { 650 | incorrectFlags += item.getCount(); 651 | } 652 | } 653 | 654 | total += item.getCount(); 655 | } 656 | 657 | return PlayerStats.builder() 658 | .namesUploaded(total) 659 | .confirmedBans(confirmedBans) 660 | .possibleBans(possibleBans) 661 | .incorrectFlags(incorrectFlags) 662 | .build(); 663 | } 664 | 665 | /** 666 | * For use with {@link PlayerSightingWrapperSerializer}. 667 | */ 668 | @Value 669 | private static class PlayerSightingWrapper 670 | { 671 | String uploaderName; 672 | boolean manualDetect; 673 | PlayerSighting sightingData; 674 | } 675 | 676 | @Value 677 | private static class DiscordVerification 678 | { 679 | @SerializedName("player_name") 680 | String nameToVerify; 681 | String code; 682 | } 683 | 684 | @Value 685 | private static class PredictionFeedback 686 | { 687 | @SerializedName("player_name") 688 | String playerName; 689 | int vote; 690 | @SerializedName("prediction") 691 | String predictionLabel; 692 | // Important: API requires this to be non-null! 693 | @SerializedName("confidence") 694 | double predictionConfidence; 695 | @SerializedName("subject_id") 696 | long targetId; 697 | @SerializedName("proposed_label") 698 | String proposedLabel; 699 | @SerializedName("proposed_label_confidence") 700 | Double proposedLabelConfidence; 701 | @SerializedName("feedback_text") 702 | String feedbackText; 703 | } 704 | 705 | @Value 706 | private static class PlayerStatsAPIItem 707 | { 708 | @SerializedName("possible_ban") 709 | boolean possibleBanned; 710 | @SerializedName("confirmed_ban") 711 | boolean banned; 712 | @SerializedName("confirmed_player") 713 | boolean player; 714 | long count; 715 | } 716 | 717 | /** 718 | * Wrapper around the {@link PlayerSighting}'s json serializer. 719 | * Adds the reporter name as an element on the same level as the {@link PlayerSighting}'s fields. 720 | */ 721 | private static class PlayerSightingWrapperSerializer implements JsonSerializer 722 | { 723 | @Override 724 | public JsonElement serialize(PlayerSightingWrapper src, Type typeOfSrc, JsonSerializationContext context) 725 | { 726 | JsonElement json = context.serialize(src.getSightingData()); 727 | JsonObject jo = json.getAsJsonObject(); 728 | jo.addProperty("reporter", src.getUploaderName()); 729 | jo.add("manual_detect", context.serialize(src.isManualDetect())); 730 | return json; 731 | } 732 | } 733 | 734 | /** 735 | * Serializes a {@link KitType} for the API. 736 | */ 737 | private static class KitTypeSerializer implements JsonSerializer 738 | { 739 | @Override 740 | public JsonElement serialize(KitType kitType, Type typeOfSrc, JsonSerializationContext context) 741 | { 742 | return context.serialize("equip_" + kitType.name().toLowerCase() + "_id"); 743 | } 744 | } 745 | 746 | /** 747 | * Serializes/Deserializes a {@link Boolean} as the integers {@code 0} or {@code 1}. 748 | */ 749 | private static class BooleanToZeroOneConverter implements JsonSerializer, JsonDeserializer 750 | { 751 | @Override 752 | public JsonElement serialize(Boolean src, Type typeOfSrc, JsonSerializationContext context) 753 | { 754 | return context.serialize(src ? 1 : 0); 755 | } 756 | 757 | @Override 758 | public Boolean deserialize(JsonElement json, Type type, JsonDeserializationContext context) 759 | throws JsonParseException 760 | { 761 | return json.getAsInt() != 0; 762 | } 763 | } 764 | 765 | /** 766 | * Serializes/Unserializes {@link Instant} using {@link Instant#getEpochSecond()}/{@link Instant#ofEpochSecond(long)} 767 | */ 768 | private static class InstantSecondsConverter implements JsonSerializer, JsonDeserializer 769 | { 770 | @Override 771 | public JsonElement serialize(Instant src, Type srcType, JsonSerializationContext context) 772 | { 773 | return new JsonPrimitive(src.getEpochSecond()); 774 | } 775 | 776 | @Override 777 | public Instant deserialize(JsonElement json, Type type, JsonDeserializationContext context) 778 | throws JsonParseException 779 | { 780 | return Instant.ofEpochSecond(json.getAsLong()); 781 | } 782 | } 783 | } 784 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/http/UnauthorizedTokenException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | package com.botdetector.http; 27 | 28 | /** 29 | * Exception for when a tokenized route in {@link BotDetectorClient} fails due to using a bad or unauthorized token. 30 | */ 31 | public class UnauthorizedTokenException extends Exception 32 | { 33 | public UnauthorizedTokenException(String message) 34 | { 35 | super(message); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/http/ValidationException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | package com.botdetector.http; 27 | 28 | import java.io.IOException; 29 | 30 | /** 31 | * Exception for when a route in {@link BotDetectorClient} fails due to HTTP Error 422. 32 | */ 33 | public class ValidationException extends IOException 34 | { 35 | public ValidationException(String message) 36 | { 37 | super(message); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/model/AuthToken.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | package com.botdetector.model; 27 | 28 | import java.util.regex.Matcher; 29 | import java.util.regex.Pattern; 30 | import lombok.Value; 31 | 32 | /** 33 | * Wrapper class for a {@link AuthTokenType} and {@link String} (token) pair. 34 | */ 35 | @Value 36 | public class AuthToken 37 | { 38 | AuthTokenType tokenType; 39 | String token; 40 | 41 | /** 42 | * The default token when no valid token is present. 43 | * Contains {@link AuthTokenType#NONE} along with an empty token {@link String}. 44 | */ 45 | public static final AuthToken EMPTY_TOKEN = new AuthToken(AuthTokenType.NONE, ""); 46 | /** The separator between the prefix (token type) and suffix (token) part of a full token **/ 47 | public static final String AUTH_TOKEN_SEPARATOR = "|"; 48 | public static final Pattern AUTH_TOKEN_PATTERN = Pattern.compile("^([a-zA-Z_]+)" 49 | + Pattern.quote(AUTH_TOKEN_SEPARATOR) 50 | + "([\\w\\-]{12,32})$"); 51 | 52 | /** Should describe {@link #AUTH_TOKEN_PATTERN} in a human readable form. **/ 53 | public static final String AUTH_TOKEN_DESCRIPTION_MESSAGE = 54 | "Auth token in clipboard must be of format 'prefix|Suffix_Alpha-numeric'" + 55 | " with a valid prefix and a suffix between 12 and 32 characters long."; 56 | 57 | /** 58 | * Parses out an {@link AuthToken} from the given {@code fullToken}. Also see {@link #toFullToken()}. 59 | * @param fullToken The full token to parse. 60 | * @return The parsed {@link AuthToken}, or {@link AuthToken#EMPTY_TOKEN} if {@code fullToken} was invalid. 61 | */ 62 | public static AuthToken fromFullToken(String fullToken) 63 | { 64 | if (fullToken == null) 65 | { 66 | return EMPTY_TOKEN; 67 | } 68 | 69 | Matcher m = AUTH_TOKEN_PATTERN.matcher(fullToken); 70 | if (m.matches()) 71 | { 72 | return new AuthToken(AuthTokenType.fromPrefix(m.group(1)), m.group(2)); 73 | } 74 | else 75 | { 76 | return EMPTY_TOKEN; 77 | } 78 | } 79 | 80 | /** 81 | * Converts this {@link AuthToken} back into a pure {@link String} form parsable with {@link #fromFullToken(String)}. 82 | * @return The full token {@link String} form for this {@link AuthToken}. 83 | */ 84 | public String toFullToken() 85 | { 86 | return tokenType.name() + AUTH_TOKEN_SEPARATOR + token; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/model/AuthTokenPermission.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | package com.botdetector.model; 27 | 28 | /** 29 | * Represents the operations that a given {@link AuthTokenType} should allow. 30 | */ 31 | public enum AuthTokenPermission 32 | { 33 | /** Allows verification of RSN/Discord pairs in-game **/ 34 | VERIFY_DISCORD 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/model/AuthTokenType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | package com.botdetector.model; 27 | 28 | import com.google.common.collect.ImmutableSet; 29 | import java.util.Arrays; 30 | import lombok.Getter; 31 | import lombok.RequiredArgsConstructor; 32 | import static com.botdetector.model.AuthTokenPermission.*; 33 | 34 | @Getter 35 | @RequiredArgsConstructor 36 | public enum AuthTokenType 37 | { 38 | /** 39 | * No permissions 40 | */ 41 | NONE(ImmutableSet.of()), 42 | 43 | /** 44 | * All permissions 45 | */ 46 | DEV(Arrays.stream(AuthTokenPermission.values()).collect(ImmutableSet.toImmutableSet())), 47 | 48 | /** 49 | * Can perform discord verification 50 | */ 51 | MOD(ImmutableSet.of(VERIFY_DISCORD)) 52 | ; 53 | 54 | private final ImmutableSet permissions; 55 | 56 | /** 57 | * Parses the token type from the given {@code prefix}. 58 | * @param prefix The prefix to parse. 59 | * @return The token type if parsed successfully, {@link #NONE} otherwise. 60 | */ 61 | public static AuthTokenType fromPrefix(String prefix) 62 | { 63 | if (prefix == null) 64 | { 65 | return AuthTokenType.NONE; 66 | } 67 | 68 | try 69 | { 70 | return AuthTokenType.valueOf(prefix.toUpperCase()); 71 | } 72 | catch (IllegalArgumentException e) 73 | { 74 | return AuthTokenType.NONE; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/model/CaseInsensitiveString.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 under CC BY 3.0, CrypticCabub 3 | * Copyright (c) 2021 under BSD 2, Ferrariic, Seltzer Bro, Cyborger1 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, this 10 | * list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | package com.botdetector.model; 28 | 29 | import lombok.Value; 30 | 31 | /** 32 | * A string wrapper that makes .equals a caseInsensitive match 33 | *

34 | * a collection that wraps a String mapping in CaseInsensitiveStrings will still accept a String but will now 35 | * return a caseInsensitive match rather than a caseSensitive one 36 | *

37 | */ 38 | @Value 39 | public class CaseInsensitiveString 40 | { 41 | String str; 42 | 43 | public static CaseInsensitiveString wrap(String str) 44 | { 45 | return new CaseInsensitiveString(str); 46 | } 47 | 48 | @Override 49 | public boolean equals(Object o) 50 | { 51 | if (this == o) 52 | { 53 | return true; 54 | } 55 | 56 | if (o == null) 57 | { 58 | return false; 59 | } 60 | 61 | if (o.getClass() == getClass()) 62 | { 63 | // Is another CaseInsensitiveString 64 | CaseInsensitiveString that = (CaseInsensitiveString) o; 65 | return (str != null) ? str.equalsIgnoreCase(that.str) : that.str == null; 66 | } 67 | 68 | if (o.getClass() == String.class) 69 | { 70 | // Is just a regular String 71 | String that = (String) o; 72 | return that.equalsIgnoreCase(str); 73 | } 74 | 75 | return false; 76 | } 77 | 78 | @Override 79 | public int hashCode() 80 | { 81 | return (str != null) ? str.toUpperCase().hashCode() : 0; 82 | } 83 | 84 | @Override 85 | public String toString() 86 | { 87 | return str; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/model/FeedbackPredictionLabel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | package com.botdetector.model; 27 | 28 | import java.util.Objects; 29 | import lombok.Value; 30 | import org.apache.commons.text.WordUtils; 31 | 32 | @Value 33 | public class FeedbackPredictionLabel 34 | { 35 | String label; 36 | String normalizedLabel; 37 | FeedbackValue feedbackValue; 38 | Double labelConfidence; 39 | 40 | public FeedbackPredictionLabel(String label, Double labelConfidence, FeedbackValue feedbackValue) 41 | { 42 | this.label = label; 43 | this.normalizedLabel = normalizeLabel(label); 44 | this.labelConfidence = labelConfidence; 45 | this.feedbackValue = feedbackValue; 46 | } 47 | 48 | @Override 49 | public boolean equals(Object o) 50 | { 51 | if (this == o) 52 | { 53 | return true; 54 | } 55 | 56 | if (o instanceof FeedbackPredictionLabel) 57 | { 58 | FeedbackPredictionLabel that = (FeedbackPredictionLabel) o; 59 | return Objects.equals(label, that.label) 60 | && Objects.equals(labelConfidence, that.labelConfidence) 61 | && Objects.equals(feedbackValue, that.feedbackValue); 62 | } 63 | 64 | return false; 65 | } 66 | 67 | @Override 68 | public int hashCode() 69 | { 70 | return (label != null ? label.hashCode() : 0) 71 | + (labelConfidence != null ? labelConfidence.hashCode() : 0) 72 | + (feedbackValue != null ? feedbackValue.hashCode() : 0); 73 | } 74 | 75 | @Override 76 | public String toString() 77 | { 78 | return normalizedLabel; 79 | } 80 | 81 | /** 82 | * Normalizes the given prediction label by separating word 83 | * with spaces and making each word capitalized. 84 | * @param label The label to normalize. 85 | * @return The normalized label. 86 | */ 87 | public static String normalizeLabel(String label) 88 | { 89 | if (label == null) 90 | { 91 | return null; 92 | } 93 | 94 | return WordUtils.capitalize(label.replace('_', ' ').trim(), ' '); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/model/FeedbackValue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | package com.botdetector.model; 27 | 28 | import lombok.Getter; 29 | import lombok.RequiredArgsConstructor; 30 | 31 | @Getter 32 | @RequiredArgsConstructor 33 | public enum FeedbackValue 34 | { 35 | POSITIVE(1), 36 | NEGATIVE(-1), 37 | NEUTRAL(0) 38 | ; 39 | 40 | private final int apiValue; 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/model/PlayerSighting.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | package com.botdetector.model; 27 | 28 | import com.google.gson.annotations.SerializedName; 29 | import java.time.Instant; 30 | import java.util.Map; 31 | import lombok.Builder; 32 | import lombok.Value; 33 | import net.runelite.api.kit.KitType; 34 | 35 | @Value 36 | @Builder 37 | public class PlayerSighting 38 | { 39 | @SerializedName("reported") 40 | String playerName; 41 | 42 | @SerializedName("region_id") 43 | int regionID; 44 | 45 | @SerializedName("x_coord") 46 | int worldX; 47 | 48 | @SerializedName("y_coord") 49 | int worldY; 50 | 51 | @SerializedName("z_coord") 52 | int plane; 53 | 54 | @SerializedName("equipment") 55 | Map equipment; 56 | 57 | @SerializedName("equipment_ge_value") 58 | long equipmentGEValue; 59 | 60 | @SerializedName("world_number") 61 | int worldNumber; 62 | 63 | @SerializedName("on_members_world") 64 | boolean inMembersWorld; 65 | 66 | @SerializedName("on_pvp_world") 67 | boolean inPVPWorld; 68 | 69 | @SerializedName("ts") 70 | Instant timestamp; 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/model/PlayerStats.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | package com.botdetector.model; 27 | 28 | import lombok.Builder; 29 | import lombok.Value; 30 | 31 | @Value 32 | @Builder 33 | public class PlayerStats 34 | { 35 | long namesUploaded; 36 | long confirmedBans; 37 | long possibleBans; 38 | long incorrectFlags; 39 | long feedbackSent; 40 | 41 | /** 42 | * The accuracy represents {@link #confirmedBans} divided by 43 | * the sum of {@link #confirmedBans} and {@link #incorrectFlags}. 44 | */ 45 | public double getAccuracy() 46 | { 47 | long divisor = incorrectFlags + confirmedBans; 48 | return divisor > 0 ? confirmedBans / (double)divisor : 0; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/model/PlayerStatsType.java: -------------------------------------------------------------------------------- 1 | package com.botdetector.model; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.experimental.Accessors; 7 | 8 | @Getter 9 | @RequiredArgsConstructor 10 | public enum PlayerStatsType 11 | { 12 | @SerializedName("manual") 13 | MANUAL("Manual", "Manual uploading statistics, uploads from manually flagging a player as a bot.", true), 14 | @SerializedName("passive") 15 | PASSIVE("Auto", "Passive uploading statistics, uploads from simply seeing other players in-game.", false), 16 | @SerializedName("total") 17 | TOTAL("Total", "Total uploading statistics, both passive and manual.", false) 18 | ; 19 | 20 | private final String shorthand; 21 | private final String description; 22 | @Accessors(fluent = true) 23 | private final boolean canDisplayAccuracy; 24 | 25 | @Override 26 | public String toString() 27 | { 28 | return shorthand; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/model/Prediction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | package com.botdetector.model; 27 | 28 | import com.google.gson.annotations.SerializedName; 29 | import java.util.Map; 30 | import lombok.Value; 31 | import lombok.Builder; 32 | 33 | @Value 34 | @Builder 35 | public class Prediction 36 | { 37 | @SerializedName("player_id") 38 | long playerId; 39 | @SerializedName("player_name") 40 | String playerName; 41 | @SerializedName("prediction_label") 42 | String predictionLabel; 43 | @SerializedName("prediction_confidence") 44 | Double confidence; 45 | @SerializedName("predictions_breakdown") 46 | Map predictionBreakdown; 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/model/StatsCommandDetailLevel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | package com.botdetector.model; 27 | 28 | import lombok.Getter; 29 | import lombok.RequiredArgsConstructor; 30 | 31 | @Getter 32 | @RequiredArgsConstructor 33 | public enum StatsCommandDetailLevel 34 | { 35 | OFF("Disabled"), 36 | CONFIRMED_ONLY("Confirmed Bans"), 37 | DETAILED("Detailed Stats") 38 | ; 39 | 40 | private final String name; 41 | 42 | @Override 43 | public String toString() 44 | { 45 | return name; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/ui/Icons.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | package com.botdetector.ui; 27 | 28 | import java.util.Objects; 29 | import javax.swing.ImageIcon; 30 | import net.runelite.client.util.ImageUtil; 31 | 32 | public class Icons 33 | { 34 | private static final Class PLUGIN_CLASS = BotDetectorPanel.class; 35 | 36 | public static final ImageIcon GITHUB_ICON = new ImageIcon(ImageUtil.loadImageResource(PLUGIN_CLASS, "github.png")); 37 | public static final ImageIcon DISCORD_ICON = new ImageIcon(ImageUtil.loadImageResource(PLUGIN_CLASS, "discord.png")); 38 | public static final ImageIcon PATREON_ICON = new ImageIcon(ImageUtil.loadImageResource(PLUGIN_CLASS, "patreon.png")); 39 | public static final ImageIcon WEB_ICON = new ImageIcon(ImageUtil.loadImageResource(PLUGIN_CLASS, "web.png")); 40 | public static final ImageIcon TWITTER_ICON = new ImageIcon(ImageUtil.loadImageResource(PLUGIN_CLASS, "twitter.png")); 41 | public static final ImageIcon WARNING_ICON = new ImageIcon(ImageUtil.loadImageResource(PLUGIN_CLASS, "warning.png")); 42 | public static final ImageIcon STRONG_WARNING_ICON = new ImageIcon(ImageUtil.loadImageResource(PLUGIN_CLASS, "strong_warning.png")); 43 | public static final ImageIcon ERROR_ICON = new ImageIcon(ImageUtil.loadImageResource(PLUGIN_CLASS, "error.png")); 44 | 45 | // Must not be ImageUtil.loadImageResource as it produces a static image 46 | public static final ImageIcon LOADING_SPINNER = new ImageIcon(Objects.requireNonNull(PLUGIN_CLASS.getResource("loading_spinner_darker.gif"))); 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/ui/NameAutocompleter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018, John Pettenger 3 | * Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, this 10 | * list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | package com.botdetector.ui; 28 | 29 | import com.botdetector.BotDetectorConfig; 30 | import com.google.common.collect.EvictingQueue; 31 | import com.google.inject.Inject; 32 | import java.awt.event.KeyEvent; 33 | import java.awt.event.KeyListener; 34 | import java.util.Arrays; 35 | import java.util.Objects; 36 | import java.util.Optional; 37 | import java.util.regex.Pattern; 38 | import javax.annotation.Nullable; 39 | import javax.inject.Singleton; 40 | import javax.swing.SwingUtilities; 41 | import javax.swing.text.BadLocationException; 42 | import javax.swing.text.Document; 43 | import javax.swing.text.JTextComponent; 44 | import lombok.NonNull; 45 | import lombok.extern.slf4j.Slf4j; 46 | import net.runelite.api.FriendsChatManager; 47 | import net.runelite.api.Client; 48 | import net.runelite.api.Friend; 49 | import net.runelite.api.Nameable; 50 | import net.runelite.api.NameableContainer; 51 | import net.runelite.api.Player; 52 | import net.runelite.api.WorldView; 53 | 54 | @Slf4j 55 | @Singleton 56 | public class NameAutocompleter implements KeyListener 57 | { 58 | /** 59 | * Non-breaking space character. 60 | */ 61 | private static final String NBSP = Character.toString((char) 160); 62 | 63 | /** 64 | * Character class for characters that cannot be in an RSN. 65 | */ 66 | private static final Pattern INVALID_CHARS = Pattern.compile("[^a-zA-Z0-9_ -]"); 67 | 68 | private static final int MAX_SEARCH_HISTORY = 25; 69 | 70 | private final Client client; 71 | private final BotDetectorConfig botDetectorConfig; 72 | 73 | private final EvictingQueue searchHistory = EvictingQueue.create(MAX_SEARCH_HISTORY); 74 | 75 | /** 76 | * The name currently being autocompleted. 77 | */ 78 | private String autocompleteName; 79 | 80 | /** 81 | * Pattern for the name currently being autocompleted. 82 | */ 83 | private Pattern autocompleteNamePattern; 84 | 85 | @Inject 86 | private NameAutocompleter(@Nullable Client client, BotDetectorConfig botDetectorConfig) 87 | { 88 | this.client = client; 89 | this.botDetectorConfig = botDetectorConfig; 90 | } 91 | 92 | @Override 93 | public void keyPressed(KeyEvent e) 94 | { 95 | 96 | } 97 | 98 | @Override 99 | public void keyReleased(KeyEvent e) 100 | { 101 | 102 | } 103 | 104 | @Override 105 | public void keyTyped(KeyEvent e) 106 | { 107 | if (!botDetectorConfig.panelAutocomplete()) 108 | { 109 | return; 110 | } 111 | 112 | final JTextComponent input = (JTextComponent) e.getSource(); 113 | final String inputText = input.getText(); 114 | 115 | // Only autocomplete if the selection end is at the end of the text. 116 | if (input.getSelectionEnd() != inputText.length()) 117 | { 118 | return; 119 | } 120 | 121 | // Character to be inserted at the selection start. 122 | final String charToInsert = Character.toString(e.getKeyChar()); 123 | 124 | // Don't attempt to autocomplete if the name is invalid. 125 | // This condition is also true when the user presses a key like backspace. 126 | if (INVALID_CHARS.matcher(charToInsert).find() 127 | || INVALID_CHARS.matcher(inputText).find()) 128 | { 129 | return; 130 | } 131 | 132 | // Check if we are already autocompleting. 133 | if (autocompleteName != null && autocompleteNamePattern.matcher(inputText).matches()) 134 | { 135 | if (isExpectedNext(input, charToInsert)) 136 | { 137 | try 138 | { 139 | // Insert the character and move the selection. 140 | final int insertIndex = input.getSelectionStart(); 141 | Document doc = input.getDocument(); 142 | doc.remove(insertIndex, 1); 143 | doc.insertString(insertIndex, charToInsert, null); 144 | input.select(insertIndex + 1, input.getSelectionEnd()); 145 | } 146 | catch (BadLocationException ex) 147 | { 148 | log.warn("Could not insert character.", ex); 149 | } 150 | 151 | // Prevent default behavior. 152 | e.consume(); 153 | } 154 | else // Character to insert does not match current autocompletion. Look for another name. 155 | { 156 | newAutocomplete(e); 157 | } 158 | } 159 | else // Search for a name to autocomplete 160 | { 161 | newAutocomplete(e); 162 | } 163 | } 164 | 165 | private void newAutocomplete(KeyEvent e) 166 | { 167 | final JTextComponent input = (JTextComponent) e.getSource(); 168 | final String inputText = input.getText(); 169 | final String nameStart = inputText.substring(0, input.getSelectionStart()) + e.getKeyChar(); 170 | 171 | if (findAutocompleteName(nameStart)) 172 | { 173 | // Assert this.autocompleteName != null 174 | final String name = this.autocompleteName; 175 | SwingUtilities.invokeLater(() -> 176 | { 177 | try 178 | { 179 | input.getDocument().insertString( 180 | nameStart.length(), 181 | name.substring(nameStart.length()), 182 | null); 183 | input.select(nameStart.length(), name.length()); 184 | } 185 | catch (BadLocationException ex) 186 | { 187 | log.warn("Could not autocomplete name.", ex); 188 | } 189 | }); 190 | } 191 | } 192 | 193 | private boolean findAutocompleteName(String nameStart) 194 | { 195 | final Pattern pattern; 196 | Optional autocompleteName; 197 | 198 | // Pattern to match names that start with nameStart. 199 | // Allows spaces to be represented as common whitespaces, underscores, 200 | // hyphens, or non-breaking spaces. 201 | // Matching non-breaking spaces is necessary because the API 202 | // returns non-breaking spaces when a name has whitespace. 203 | pattern = Pattern.compile( 204 | "(?i)^" + nameStart.replaceAll("[ _-]", "[ _" + NBSP + "-]") + ".+?"); 205 | 206 | if (client == null) 207 | { 208 | return false; 209 | } 210 | 211 | // Search all previous successful queries 212 | autocompleteName = searchHistory.stream() 213 | .filter(n -> pattern.matcher(n).matches()) 214 | .findFirst(); 215 | 216 | // Search friends if previous searches weren't matched 217 | if (!autocompleteName.isPresent()) 218 | { 219 | NameableContainer friendContainer = client.getFriendContainer(); 220 | if (friendContainer != null) 221 | { 222 | autocompleteName = Arrays.stream(friendContainer.getMembers()) 223 | .map(Nameable::getName) 224 | .filter(n -> pattern.matcher(n).matches()) 225 | .findFirst(); 226 | } 227 | } 228 | 229 | // Search friends chat if a friend wasn't found 230 | if (!autocompleteName.isPresent()) 231 | { 232 | final FriendsChatManager friendsChatManager = client.getFriendsChatManager(); 233 | if (friendsChatManager != null) 234 | { 235 | autocompleteName = Arrays.stream(friendsChatManager.getMembers()) 236 | .map(Nameable::getName) 237 | .filter(n -> pattern.matcher(n).matches()) 238 | .findFirst(); 239 | } 240 | } 241 | 242 | // Search cached players if a friend wasn't found 243 | if (!autocompleteName.isPresent()) 244 | { 245 | final WorldView wv = client.getTopLevelWorldView(); 246 | if (wv != null) 247 | { 248 | autocompleteName = wv.players().stream() 249 | .filter(Objects::nonNull) 250 | .map(Player::getName) 251 | .filter(Objects::nonNull) 252 | .filter(n -> pattern.matcher(n).matches()) 253 | .findFirst(); 254 | } 255 | } 256 | 257 | if (autocompleteName.isPresent()) 258 | { 259 | this.autocompleteName = autocompleteName.get().replace(NBSP, " "); 260 | this.autocompleteNamePattern = Pattern.compile( 261 | "(?i)^" + this.autocompleteName.replaceAll("[ _-]", "[ _-]") + "$"); 262 | } 263 | else 264 | { 265 | this.autocompleteName = null; 266 | this.autocompleteNamePattern = null; 267 | } 268 | 269 | return autocompleteName.isPresent(); 270 | } 271 | 272 | void addToSearchHistory(@NonNull String name) 273 | { 274 | if (!searchHistory.contains(name)) 275 | { 276 | searchHistory.offer(name); 277 | } 278 | } 279 | 280 | private boolean isExpectedNext(JTextComponent input, String nextChar) 281 | { 282 | String expected; 283 | if (input.getSelectionStart() < input.getSelectionEnd()) 284 | { 285 | try 286 | { 287 | expected = input.getText(input.getSelectionStart(), 1); 288 | } 289 | catch (BadLocationException ex) 290 | { 291 | log.warn("Could not get first character from input selection.", ex); 292 | return false; 293 | } 294 | } 295 | else 296 | { 297 | expected = ""; 298 | } 299 | return nextChar.equalsIgnoreCase(expected); 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/ui/PanelFontType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | package com.botdetector.ui; 27 | 28 | public enum PanelFontType 29 | { 30 | SMALL, 31 | NORMAL, 32 | BOLD 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/ui/components/ComboBoxSelfTextTooltipListRenderer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1 3 | * Copyright (c) 2017, Psikoi 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, this 10 | * list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | package com.botdetector.ui.components; 28 | 29 | import java.awt.Color; 30 | import java.awt.Component; 31 | import javax.swing.JLabel; 32 | import javax.swing.JList; 33 | import javax.swing.ListCellRenderer; 34 | import javax.swing.border.EmptyBorder; 35 | import net.runelite.client.ui.ColorScheme; 36 | 37 | public final class ComboBoxSelfTextTooltipListRenderer extends JLabel implements ListCellRenderer 38 | { 39 | @Override 40 | public Component getListCellRendererComponent(JList list, T o, int index, boolean isSelected, boolean cellHasFocus) 41 | { 42 | if (isSelected) 43 | { 44 | setBackground(ColorScheme.DARK_GRAY_COLOR); 45 | setForeground(Color.WHITE); 46 | } 47 | else 48 | { 49 | setBackground(list.getBackground()); 50 | setForeground(ColorScheme.LIGHT_GRAY_COLOR); 51 | } 52 | 53 | setBorder(new EmptyBorder(0, 0, 0, 0)); 54 | 55 | setText(o.toString()); 56 | setToolTipText(o.toString()); 57 | 58 | return this; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/botdetector/ui/components/JLimitedTextArea.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 under CC BY 3.0, Francisco J. Güemes Sevilla 3 | * Copyright (c) 2021 under BSD 2, Ferrariic, Seltzer Bro, Cyborger1 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, this 10 | * list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | package com.botdetector.ui.components; 28 | 29 | import javax.swing.JTextArea; 30 | import javax.swing.text.AttributeSet; 31 | import javax.swing.text.BadLocationException; 32 | import javax.swing.text.Document; 33 | import javax.swing.text.PlainDocument; 34 | 35 | /** 36 | * An extension of {@link JTextArea} that automatically implements 37 | * a default {@link Document} model that limits the number of characters 38 | * that can be entered to the given {@code limit} in {@link #JLimitedTextArea(int)}. 39 | */ 40 | public class JLimitedTextArea extends JTextArea 41 | { 42 | private final int limit; 43 | 44 | /** 45 | * Instanciates a {@link JTextArea} implementing a default {@link Document} model 46 | * that limits the number of characters that can be entered. 47 | * @param limit The maximum number of characters that can be entered in the underlying {@link JTextArea}. 48 | */ 49 | public JLimitedTextArea(int limit) 50 | { 51 | super(); 52 | this.limit = limit; 53 | } 54 | 55 | @Override 56 | protected Document createDefaultModel() 57 | { 58 | return new LimitDocument(); 59 | } 60 | 61 | private class LimitDocument extends PlainDocument 62 | { 63 | @Override 64 | public void insertString( int offset, String str, AttributeSet attr ) throws BadLocationException 65 | { 66 | if (str == null) return; 67 | 68 | if ((getLength() + str.length()) <= limit) 69 | { 70 | super.insertString(offset, str, attr); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/resources/com/botdetector/bot-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bot-detector/bot-detector/bc410b29edc2545fb8c25941ebbeabcd74b86aef/src/main/resources/com/botdetector/bot-icon.png -------------------------------------------------------------------------------- /src/main/resources/com/botdetector/ui/discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bot-detector/bot-detector/bc410b29edc2545fb8c25941ebbeabcd74b86aef/src/main/resources/com/botdetector/ui/discord.png -------------------------------------------------------------------------------- /src/main/resources/com/botdetector/ui/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bot-detector/bot-detector/bc410b29edc2545fb8c25941ebbeabcd74b86aef/src/main/resources/com/botdetector/ui/error.png -------------------------------------------------------------------------------- /src/main/resources/com/botdetector/ui/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bot-detector/bot-detector/bc410b29edc2545fb8c25941ebbeabcd74b86aef/src/main/resources/com/botdetector/ui/github.png -------------------------------------------------------------------------------- /src/main/resources/com/botdetector/ui/loading_spinner_darker.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bot-detector/bot-detector/bc410b29edc2545fb8c25941ebbeabcd74b86aef/src/main/resources/com/botdetector/ui/loading_spinner_darker.gif -------------------------------------------------------------------------------- /src/main/resources/com/botdetector/ui/patreon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bot-detector/bot-detector/bc410b29edc2545fb8c25941ebbeabcd74b86aef/src/main/resources/com/botdetector/ui/patreon.png -------------------------------------------------------------------------------- /src/main/resources/com/botdetector/ui/strong_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bot-detector/bot-detector/bc410b29edc2545fb8c25941ebbeabcd74b86aef/src/main/resources/com/botdetector/ui/strong_warning.png -------------------------------------------------------------------------------- /src/main/resources/com/botdetector/ui/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bot-detector/bot-detector/bc410b29edc2545fb8c25941ebbeabcd74b86aef/src/main/resources/com/botdetector/ui/twitter.png -------------------------------------------------------------------------------- /src/main/resources/com/botdetector/ui/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bot-detector/bot-detector/bc410b29edc2545fb8c25941ebbeabcd74b86aef/src/main/resources/com/botdetector/ui/warning.png -------------------------------------------------------------------------------- /src/main/resources/com/botdetector/ui/web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bot-detector/bot-detector/bc410b29edc2545fb8c25941ebbeabcd74b86aef/src/main/resources/com/botdetector/ui/web.png -------------------------------------------------------------------------------- /src/test/java/com/botdetector/BotDetectorPluginTest.java: -------------------------------------------------------------------------------- 1 | package com.botdetector; 2 | 3 | import net.runelite.client.RuneLite; 4 | import net.runelite.client.externalplugins.ExternalPluginManager; 5 | 6 | public class BotDetectorPluginTest 7 | { 8 | public static void main(String[] args) throws Exception 9 | { 10 | ExternalPluginManager.loadBuiltin(BotDetectorPlugin.class); 11 | RuneLite.main(args); 12 | } 13 | } 14 | --------------------------------------------------------------------------------