├── .github └── workflows │ ├── main.yml │ └── maven.yml ├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src └── main └── java └── net └── dec4234 └── javadestinyapi ├── exceptions ├── APIException.java ├── APIOfflineException.java ├── AccessTokenExpiredException.java ├── ConnectionException.java ├── InvalidConditionException.java ├── JsonParsingError.java ├── OAuthUnauthorizedException.java └── RefreshTokenExpiredException.java ├── material ├── DestinyAPI.java ├── clan │ ├── Clan.java │ ├── ClanChatSecuritySetting.java │ ├── ClanManagement.java │ ├── ClanMember.java │ └── GroupType.java ├── inventory │ ├── CollectionsManager.java │ ├── InventoryManager.java │ ├── items │ │ ├── Armor.java │ │ ├── DestinyItem.java │ │ ├── InventoryBucket.java │ │ ├── InventoryItem.java │ │ ├── ItemPerk.java │ │ └── ItemPlug.java │ └── loadouts │ │ └── Loadout.java ├── manifest │ ├── DestinyManifest.java │ └── ManifestEntityTypes.java ├── social │ ├── SocialFriend.java │ └── SocialFriendsList.java └── user │ ├── BungieUser.java │ ├── DestinyCharacter.java │ ├── DestinyPlatform.java │ ├── DestinyProfile.java │ ├── UserCredential.java │ └── UserCredentialType.java ├── responses └── user │ └── SanitizedUsernamesResponse.java ├── stats ├── activities │ ├── Activity.java │ ├── ActivityHistoryReview.java │ ├── ActivityIdentifier.java │ ├── ActivityInfo.java │ ├── ActivityMode.java │ └── ActivityParticipant.java └── character │ └── UserStats.java └── utils ├── HttpRequestModifier.java ├── HttpUtils.java ├── StringUtils.java ├── fast ├── ASyncPull.java ├── ASyncPuller.java └── Pagination.java └── framework ├── ContentFramework.java ├── ContentInterface.java ├── JDAOAuth.java ├── JsonOAuthManager.java ├── JsonObjectModifier.java ├── OAuthFlow.java └── OAuthManager.java /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: release version 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Build and Test 16 | uses: qcastel/github-actions-maven-cmd@master 17 | with: 18 | maven-args: "clean install -e" 19 | 20 | - name: Release 21 | uses: qcastel/github-actions-maven-release@master 22 | env: 23 | JAVA_HOME: /usr/lib/jvm/java-11-openjdk/ 24 | with: 25 | git-release-bot-name: "bot-idhub" 26 | git-release-bot-email: "bot@idhub.io" 27 | 28 | maven-args: "clean install -e" 29 | 30 | ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} 31 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 11 20 | uses: actions/setup-java@v2 21 | with: 22 | java-version: '11' 23 | distribution: 'adopt' 24 | - name: Build with Maven 25 | run: mvn -B package --file pom.xml 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /out/ 3 | /src/main/java/Testing.java 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 dec4234 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |

5 | 6 | # JavaDestinyAPI 7 | This library is used as an interface to the API for the game Destiny 2, created by Bungie. 8 | 9 | The API is a work in-progress, contributions are welcome. Please note that development is on-going and the latest versions may be prone to bugs. If you find any please create an issue to report it. 10 | ## Getting Started 11 | This project can be currently accessed through the jitpack repository, which allows any github repo to be used as a dependency 12 | ```xml 13 | 14 | jitpack.io 15 | https://jitpack.io 16 | 17 | ``` 18 | And then this dependency: 19 | ```xml 20 | 21 | com.github.dec4234 22 | JavaDestinyAPI 23 | master 24 | 25 | ``` 26 | (You may need to replace "master" with the latest commit hash) 27 | 28 | If you happen to need a JAR version, check out [releases](https://github.com/dec4234/JavaDestinyAPI/releases). 29 | 30 | **** 31 | 32 | **Getting the API Set Up** 33 | 34 | You need to get an API key from [bungie.net/developer](https://bungie.net/developer) 35 | 36 | ```java 37 | DestinyAPI api = new DestinyAPI("YOUR API KEY HERE"); 38 | ``` 39 | **IMPORTANT:** *DestinyAPI MUST be the first thing initialized before making any interactions with the API! This is because 40 | all interactions with the API rely on the API Key set in DestinyAPI.* 41 | 42 | **Getting the time played in hours, of the user named dec4234#9904** 43 | ```java 44 | System.out.println(DestinyAPI.getUserWithName("dec4234#9904").getTimePlayed() / 60.0); 45 | ``` 46 | 47 | **Get the name of the founder of a clan** 48 | ```java 49 | System.out.println(new Clan("Heavenly Mayhem").getFounder().getSupplementalDisplayName()); 50 | ``` 51 | 52 | ### Examples 53 | - Check out the [wiki](https://github.com/dec4234/JavaDestinyAPI/wiki/Getting-Started) for more specific examples and information. 54 | - Check out the [discord bot](https://github.com/dec4234/Benedict) that I made for my clan using this library 55 | 56 | ### An aside about APIException 57 | As of 4/30/2024, all functions that interact with the API in any way (i.e. could make an HTTP request) throw APIException. 58 | This is to allow for you, the user, to handle API errors in the way you see fit. This means you must use a try/catch or 59 | add "throws" to your function signature. 60 |
61 | This was done because the old way could create a lot of runtime exceptions which were hard to account for. 62 | The most important error you may want to look at is [APIOfflineException](https://github.com/dec4234/JavaDestinyAPI/blob/master/src/main/java/net/dec4234/javadestinyapi/exceptions/APIOfflineException.java) 63 | which indicates when Bungie has disabled the API for maintenence. This may be useful to tell your user's that the API is offline. 64 | 65 | ## How's it made? 66 | There is both official and unofficial documentation for the API available on [destinydevs.github.io](http://destinydevs.github.io/BungieNetPlatform/docs/Endpoints) and on the [offical bungie api documentation](https://bungie-net.github.io/). 67 | 68 | ### TO-DO 69 | - Managing inventory / Item transferring 70 | - Collections / Triumphs 71 | - Revamping the wiki -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 4.0.0 13 | 14 | com.github.dec4234 15 | JavaDestinyAPI 16 | 1.5 17 | jar 18 | 19 | JavaDestinyAPI 20 | A Java wrapper for bungie.net platform API, allowing users to get data about the video games Destiny and Destiny 2 21 | https://github.com/dec4234/JavaDestinyAPI 22 | 23 | 24 | 25 | MIT License 26 | https://www.opensource.org/licenses/mit-license.php 27 | 28 | 29 | 30 | 31 | 32 | dec4234 33 | https://github.com/dec4234/JavaDestinyAPI 34 | 35 | 36 | 37 | 38 | scm:git:${project.scm.url} 39 | scm:git:${project.scm.url} 40 | git@github.com:idhub-io/idhub-api.git 41 | HEAD 42 | 43 | 44 | 45 | 11 46 | 11 47 | 48 | 49 | 50 | 51 | com.google.code.gson 52 | gson 53 | 2.10.1 54 | 55 | 56 | org.jetbrains 57 | annotations 58 | 16.0.1 59 | compile 60 | 61 | 62 | org.apache.maven.plugins 63 | maven-compiler-plugin 64 | 3.11.0 65 | 66 | 67 | 68 | 69 | ${project.artifactId} 70 | clean install 71 | ${basedir}/src/main/java 72 | 73 | 74 | org.apache.maven.plugins 75 | maven-jar-plugin 76 | 2.4 77 | 78 | 79 | org.apache.maven.plugins 80 | maven-shade-plugin 81 | 2.3 82 | 83 | 84 | package 85 | 86 | shade 87 | 88 | 89 | 90 | 91 | 92 | org.apache.maven.plugins 93 | maven-compiler-plugin 94 | 95 | 11 96 | 11 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/exceptions/APIException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.exceptions; 9 | 10 | /** 11 | * Base class for all exceptions generated when an issue occurs in the request pipeline to Bungie.
12 | * This is defined as a RuntimeException to give more flexibility internally (namely in {@link net.dec4234.javadestinyapi.material.DestinyAPI#searchUsers(String)}). 13 | * You should still try/catch this rather than deferring it to being a solely runtime exception 14 | */ 15 | public abstract class APIException extends Exception { 16 | 17 | public APIException() { 18 | 19 | } 20 | 21 | public APIException(String errorMessage) { 22 | super(errorMessage); 23 | } 24 | 25 | public APIException(Exception e) { 26 | super(e); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/exceptions/APIOfflineException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.exceptions; 9 | 10 | /** 11 | * Indicates that the request returned error code 5 (The API has been disabled by Bungie) 12 | */ 13 | public class APIOfflineException extends APIException { 14 | 15 | public APIOfflineException(String returnMessage) { 16 | super("The Bungie API returned this message: " + returnMessage); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/exceptions/AccessTokenExpiredException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.exceptions; 9 | 10 | /** 11 | * Self explanatory. The access token needs to be regenerated using the refresh token. 12 | */ 13 | public class AccessTokenExpiredException extends APIException{ 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/exceptions/ConnectionException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.exceptions; 9 | 10 | /** 11 | * A connection could not be established to the Bungie servers. This is likely a problem with your internet or network 12 | * configuration. 13 | */ 14 | public class ConnectionException extends APIException { 15 | 16 | public ConnectionException(Exception exception) { 17 | super(exception); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/exceptions/InvalidConditionException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.exceptions; 9 | 10 | /** 11 | * When a token is missing or wrong, or some other pre-condition was not met that was needed to execute a request. 12 | * This is a generic error, check the associated error message for more information. 13 | */ 14 | public class InvalidConditionException extends APIException { 15 | 16 | public InvalidConditionException(String message) { 17 | super(message); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/exceptions/JsonParsingError.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.exceptions; 9 | 10 | import com.google.gson.JsonSyntaxException; 11 | 12 | /** 13 | * Malformed or non-json content found where JSON was expected. Usually at an API endpoint. 14 | */ 15 | public class JsonParsingError extends APIException { 16 | 17 | public JsonParsingError(String message) { 18 | super(message); 19 | } 20 | 21 | public JsonParsingError(JsonSyntaxException exception) { 22 | super(exception); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/exceptions/OAuthUnauthorizedException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.exceptions; 9 | 10 | /** 11 | * You either have insufficient permission to attempt this OAuth action, or you forgot to authorize yourself. 12 | * Alternatively, the access and/or refresh token has expired 13 | * See {@link net.dec4234.javadestinyapi.utils.framework.OAuthFlow} 14 | */ 15 | public class OAuthUnauthorizedException extends APIException { 16 | 17 | public OAuthUnauthorizedException(String message) { 18 | super(message); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/exceptions/RefreshTokenExpiredException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.exceptions; 9 | 10 | /** 11 | * The name is self-expanatory. The refresh token normally used to generate a new access token has expired since it 12 | * hasn't been refreshed in the past 90 days or it has been 1 year since the last oauth. 13 | * See {@link net.dec4234.javadestinyapi.utils.framework.OAuthFlow} to generate a new one. 14 | */ 15 | public class RefreshTokenExpiredException extends APIException { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/clan/Clan.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.clan; 9 | 10 | import com.google.gson.JsonArray; 11 | import com.google.gson.JsonElement; 12 | import com.google.gson.JsonObject; 13 | import net.dec4234.javadestinyapi.exceptions.APIException; 14 | import net.dec4234.javadestinyapi.material.DestinyAPI; 15 | import net.dec4234.javadestinyapi.material.user.BungieUser; 16 | import net.dec4234.javadestinyapi.stats.activities.ActivityMode; 17 | import net.dec4234.javadestinyapi.utils.HttpUtils; 18 | import net.dec4234.javadestinyapi.utils.StringUtils; 19 | import net.dec4234.javadestinyapi.utils.framework.ContentFramework; 20 | 21 | import java.util.ArrayList; 22 | import java.util.Arrays; 23 | import java.util.Date; 24 | import java.util.LinkedList; 25 | import java.util.List; 26 | 27 | public class Clan extends ContentFramework { 28 | 29 | private HttpUtils hu = DestinyAPI.getHttpUtils(); 30 | 31 | private long clanId = -1; 32 | private String clanName, clanDescription, motto; 33 | 34 | // Details about the clan (more or less in the order they appear in the JSON response) 35 | private Date creationDate; 36 | private int memberCount = -1; 37 | 38 | private ClanMember founder; 39 | private List admins, members; 40 | private ClanManagement clanManagement; 41 | private JsonObject jj, detail, founderJO; 42 | 43 | /** 44 | * @param clanId The ID of the clan 45 | */ 46 | public Clan(long clanId) { 47 | super("https://www.bungie.net/platform/GroupV2/" + clanId + "/?components=200", source -> { 48 | return source.getAsJsonObject("Response"); 49 | }); 50 | this.clanId = clanId; 51 | } 52 | 53 | /** 54 | * This is no longer the preferred method for searching by name. Use {@link DestinyAPI#searchClan(String)} 55 | * @param clanName The name of the clan that you would like to look at 56 | */ 57 | @Deprecated 58 | public Clan(String clanName) { 59 | super(("https://www.bungie.net/Platform/GroupV2/Name/" + StringUtils.httpEncode(clanName) + "/1/?components=200"), source -> { 60 | return source.getAsJsonObject("Response"); 61 | }); 62 | this.clanName = clanName; 63 | } 64 | 65 | public Clan(long clanId, String clanName) { 66 | super(("https://www.bungie.net/platform/GroupV2/" + clanId + "/?components=200"), source -> { 67 | return source.getAsJsonObject("Response"); 68 | }); 69 | this.clanId = clanId; 70 | this.clanName = clanName; 71 | } 72 | 73 | public Clan(long clanId, JsonObject detail, JsonObject founder) { 74 | super(("https://www.bungie.net/platform/GroupV2/" + clanId + "/?components=200"), source -> { 75 | return source.getAsJsonObject("Response"); 76 | }); 77 | this.clanId = clanId; 78 | this.detail = detail; 79 | this.founderJO = founder; 80 | } 81 | 82 | public String getClanID() throws APIException { 83 | if (clanId == -1) { 84 | clanId = getDetail().get("groupId").getAsLong(); 85 | } 86 | return clanId + ""; 87 | } 88 | 89 | public String getClanName() throws APIException { 90 | if (clanName == null) { 91 | clanName = getJO().getAsJsonObject("detail").get("name").getAsString(); 92 | } 93 | return clanName; 94 | } 95 | 96 | public String getClanDescription() throws APIException { 97 | if (clanDescription == null) { 98 | clanDescription = getDetail().get("about").getAsString(); 99 | } 100 | return clanDescription; 101 | } 102 | 103 | public Date getCreationDate() throws APIException { 104 | if (creationDate == null) { 105 | creationDate = StringUtils.valueOfZTime(getDetail().get("creationDate").getAsString()); 106 | } 107 | return creationDate; 108 | } 109 | 110 | public int getMemberCount() throws APIException { 111 | if (memberCount == -1) { 112 | memberCount = getDetail().get("memberCount").getAsInt(); 113 | } 114 | return memberCount; 115 | } 116 | 117 | public boolean isPublic() throws APIException { 118 | return getDetail().get("isPublic").getAsBoolean(); 119 | } 120 | 121 | public String getMotto() throws APIException { 122 | if (motto == null) { 123 | motto = getDetail().get("motto").getAsString(); 124 | } 125 | return motto; 126 | } 127 | 128 | /** 129 | * Get if this clan allows chat. I think this refers to the Bungie.net clan chat page 130 | */ 131 | public boolean isAllowChat() throws APIException { 132 | return getDetail().get("allowChat").getAsBoolean(); 133 | } 134 | 135 | /** 136 | * Get the founder of the clan 137 | */ 138 | public ClanMember getFounder() throws APIException { 139 | if(founder != null) { 140 | return new ClanMember(founderJO); 141 | } 142 | 143 | return new ClanMember(getJO().getAsJsonObject("founder")); 144 | } 145 | 146 | /** 147 | * Returns a list of the founder and the admins of the clan. 148 | * The founder is always the first in this list? 149 | * Followed by the admins in the order they were promoted 150 | */ 151 | public List getAdmins() throws APIException { 152 | if (admins != null) { 153 | return admins; 154 | } 155 | 156 | List temp = new ArrayList<>(); 157 | JsonArray ja = hu.urlRequestGET("https://www.bungie.net/Platform/GroupV2/" + getClanID() + "/AdminsAndFounder/?components=200").getAsJsonObject("Response").getAsJsonArray("results"); 158 | 159 | for (JsonElement je : ja) { 160 | temp.add(new ClanMember(je.getAsJsonObject())); 161 | } 162 | 163 | admins = temp; // Cache this information 164 | return temp; 165 | } 166 | 167 | /** 168 | * @return A double representing the average amount of days since clan members have last logged in. 169 | */ 170 | public double getAverageInactivityAmongMembers() throws APIException { 171 | ArrayList averages = new ArrayList<>(); 172 | int a = 0; 173 | for (ClanMember clanMember : this.getMembers()) { 174 | averages.add(clanMember.getDaysSinceLastPlayed()); 175 | } 176 | for (Double d : averages) { 177 | a += d; 178 | } 179 | return (double) a / getMembers().size(); 180 | } 181 | 182 | /** 183 | * Get the most inactive members of a clan, using the getMembers() endpoint and the ClanMember class 184 | * @param numberOfResults The number of inactive members you want 185 | * @param exclude An exclusion list of bungie IDs. If an inactive user has their id in this list then they will NOT 186 | * be included in the returned list. 187 | * @return A list of the most inactive members, sorted from most to least inactive 188 | */ 189 | public List getMostInactiveMembers(int numberOfResults, String... exclude) throws APIException { 190 | List list = getMembers(); 191 | List excluded = Arrays.asList(exclude); 192 | List toReturn = new LinkedList<>(); 193 | 194 | for (int i = 0; i < numberOfResults; i++) { 195 | ClanMember temp = null; 196 | 197 | for (ClanMember clanMember : list) { 198 | if(temp != clanMember && !toReturn.contains(clanMember) && !excluded.contains(clanMember.getID())) { 199 | if (temp == null || (clanMember.getLastOnlineStatusChange().getTime() != 0 && clanMember.getDaysSinceLastPlayed() > temp.getDaysSinceLastPlayed())) { 200 | temp = clanMember; 201 | } 202 | } 203 | } 204 | 205 | toReturn.add(temp); 206 | } 207 | 208 | return toReturn; 209 | } 210 | 211 | /** 212 | * Search for all of the members in this clan that have the string in their name 213 | * 214 | * TO-DO: Maybe switch this to the search part of the /Members/ endpoint? 215 | */ 216 | public List searchMembers(String name) throws APIException { 217 | List list = new LinkedList<>(); 218 | 219 | for (BungieUser bungieUser : getMembers()) { 220 | if (bungieUser.getGlobalDisplayName().contains(name)) { 221 | list.add(bungieUser); 222 | } 223 | } 224 | 225 | return list; 226 | } 227 | 228 | /** 229 | * Gets all presently online members of the clan 230 | * Returns very quickly because it only needs 1 request 231 | * 232 | * @return A list of all online members of the clan 233 | */ 234 | public List getOnlineMembers() throws APIException { 235 | List toReturn = new ArrayList<>(); 236 | 237 | for(ClanMember clanMember : getMembers()) { 238 | if(clanMember.isOnline()) { 239 | toReturn.add(clanMember); 240 | } 241 | } 242 | 243 | return toReturn; 244 | } 245 | 246 | /** 247 | * Only get the user ids of the members of this clan instead of BungieUsers 248 | * 249 | * @return The userIds belonging to the members of this clan 250 | */ 251 | public List getMembersIDs() throws APIException { 252 | List toReturn = new ArrayList<>(); 253 | 254 | for (BungieUser bungieUser : getMembers()) { 255 | toReturn.add(bungieUser.getID()); 256 | } 257 | 258 | return toReturn; 259 | } 260 | 261 | /** 262 | * Get the members of this clan 263 | * @return A List of ClanMember 264 | */ 265 | public List getMembers() throws APIException { 266 | List clanMembers = new ArrayList<>(); 267 | 268 | JsonObject response = hu.urlRequestGET("https://www.bungie.net/Platform/GroupV2/" + getClanID() + "/Members/").get("Response").getAsJsonObject(); 269 | 270 | for (JsonElement jsonElement : response.getAsJsonArray("results")) { 271 | clanMembers.add(new ClanMember(jsonElement.getAsJsonObject())); 272 | } 273 | 274 | return clanMembers; 275 | } 276 | 277 | /** 278 | * Get the oldest members of this clan, sorted from the first person to join to most recently joined 279 | * @param amount The amount of members to return 280 | * @return A sorted list of the oldest members of this clan 281 | */ 282 | public List getOldestMembers(int amount) throws APIException { 283 | List members = getMembers(); 284 | List sorted = new LinkedList<>(); 285 | 286 | ClanMember oldest = null; 287 | 288 | for(int i = 0; i < amount; i++) { 289 | if(i > members.size()) { 290 | break; 291 | } 292 | 293 | for(ClanMember inner : members) { 294 | if(inner != oldest && !sorted.contains(inner)) { 295 | if(oldest == null || inner.getJoinDate().getTime() < oldest.getJoinDate().getTime()) { 296 | oldest = inner; 297 | } 298 | } 299 | } 300 | 301 | sorted.add(oldest); 302 | oldest = null; 303 | } 304 | 305 | return sorted; 306 | } 307 | 308 | /** 309 | * Returns if this BungieUser is a member of the clan 310 | */ 311 | public boolean isMember(BungieUser bungieUser) throws APIException { 312 | return isMember(bungieUser.getID()); 313 | } 314 | 315 | /** 316 | * Checks if the member with the provided id is a member of the clan 317 | */ 318 | public boolean isMember(String bungieID) throws APIException { 319 | for (BungieUser bungieUser : getMembers()) { 320 | if (bungieUser.getID().equals(bungieID)) { 321 | return true; 322 | } 323 | } 324 | 325 | return false; 326 | } 327 | 328 | /** 329 | * Retrieve a JsonObject depicting the top stats of the clan 330 | * Unfortunately does not say who has those top stats 331 | */ 332 | public JsonObject getClanStats(ActivityMode... filter) throws APIException { 333 | String queryString = "/?modes="; 334 | for (ActivityMode activityMode : filter) { 335 | queryString = queryString.concat(activityMode.getBungieValue() + ","); 336 | } 337 | queryString = queryString.substring(0, queryString.length() - 2); // Remove the last comma 338 | 339 | return hu.urlRequestGET("https://www.bungie.net/Platform/Destiny2/Stats/AggregateClanStats/" + getClanID() + queryString); 340 | } 341 | 342 | /** 343 | * Get the date that this user joined the clan 344 | * 345 | * @return The Date this user joined the clan or null if that user was not found 346 | */ 347 | public Date getJoinDate(BungieUser member) throws APIException { 348 | if (jj == null) { 349 | jj = hu.urlRequestGET("https://www.bungie.net/Platform/GroupV2/" + getClanID() + "/Members/").get("Response").getAsJsonObject(); 350 | } 351 | 352 | for (JsonElement je : jj.getAsJsonArray("results")) { 353 | if (member.getID().equals(je.getAsJsonObject().getAsJsonObject("destinyUserInfo").get("membershipId").getAsString())) { 354 | return StringUtils.valueOfZTime(je.getAsJsonObject().get("joinDate").getAsString()); 355 | } 356 | } 357 | 358 | return null; // Return null if there were no matching users found 359 | } 360 | 361 | /** 362 | * Get the management class for this clan. 363 | */ 364 | public ClanManagement getClanManagement() { 365 | if (clanManagement != null) { 366 | return clanManagement; 367 | } 368 | clanManagement = new ClanManagement(this); 369 | return clanManagement; 370 | } 371 | 372 | private JsonObject getDetail() throws APIException { 373 | if(detail != null) { 374 | return detail; 375 | } 376 | 377 | return getJO().getAsJsonObject("detail"); 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/clan/ClanChatSecuritySetting.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.clan; 9 | 10 | public enum ClanChatSecuritySetting { 11 | 12 | GROUP(0), 13 | ADMINS(1); 14 | 15 | private int setting; 16 | 17 | private ClanChatSecuritySetting(int setting) { 18 | this.setting = setting; 19 | } 20 | 21 | public int getSetting() { 22 | return setting; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/clan/ClanManagement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.clan; 9 | 10 | import com.google.gson.JsonArray; 11 | import com.google.gson.JsonElement; 12 | import com.google.gson.JsonObject; 13 | import net.dec4234.javadestinyapi.exceptions.APIException; 14 | import net.dec4234.javadestinyapi.material.DestinyAPI; 15 | import net.dec4234.javadestinyapi.material.user.BungieUser; 16 | import net.dec4234.javadestinyapi.utils.HttpUtils; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | /** 22 | * Managing clans like kicking/accepting players and accessing sensitive information. 23 | * Requires OAuth to be set up! 24 | * Requires the user who authenticated the app to be an administrator of the clan they are trying to manage! 25 | */ 26 | public class ClanManagement { 27 | 28 | HttpUtils hu = DestinyAPI.getHttpUtils(); 29 | 30 | private Clan clan; 31 | private List bannedMembers; 32 | private List pendingMembers; 33 | 34 | public ClanManagement(Clan clan) { 35 | this.clan = clan; 36 | } 37 | 38 | /** 39 | * Kicks this user from the clan 40 | */ 41 | public void kickPlayer(BungieUser bungieUser) throws APIException { 42 | hu.urlRequestPOSTOauth("https://www.bungie.net/Platform/GroupV2/" + clan.getClanID() + "/Members/" + bungieUser.getMembershipType() + "/" + bungieUser.getID() + "/Kick/"); 43 | } 44 | 45 | /** 46 | * Bans the user from the clan 47 | */ 48 | public void banUser(BungieUser bungieUser) throws APIException { 49 | hu.urlRequestPOSTOauth("https://www.bungie.net/Platform/GroupV2/" + clan.getClanID() + "/Members/" + bungieUser.getMembershipType() + "/" + bungieUser.getID() + "/Ban/"); 50 | } 51 | 52 | /** 53 | * Unbans this user from the clan, as long as they are banned, of course 54 | */ 55 | public void unbanUser(BungieUser bungieUser) throws APIException { 56 | hu.urlRequestPOSTOauth("https://www.bungie.net/Platform/GroupV2/" + clan.getClanID() + "/Members/" + bungieUser.getMembershipType() + "/" + bungieUser.getID() + "/Unban/"); 57 | } 58 | 59 | /** 60 | * Invites the specified user to join the clan 61 | */ 62 | public void inviteUser(BungieUser bungieUser) throws APIException { 63 | hu.urlRequestPOSTOauth("https://www.bungie.net/Platform/GroupV2/" + clan.getClanID() + "/Members/IndividualInvite/" + bungieUser.getMembershipType() + "/" + bungieUser.getID() + "/"); 64 | } 65 | 66 | /** 67 | * Cancels the invite for this user to join the clan 68 | */ 69 | public void cancelInvite(BungieUser bungieUser) throws APIException { 70 | hu.urlRequestPOSTOauth("https://www.bungie.net/Platform/GroupV2/" + clan.getClanID() + "/Members/IndividualInviteCancel/" + bungieUser.getMembershipType() + "/" + bungieUser.getID() + "/"); 71 | } 72 | 73 | /** 74 | * Approves this user's request to join the clan if and only if they have requested to join 75 | */ 76 | public void approvePendingMember(BungieUser bungieUser) throws APIException { 77 | hu.urlRequestPOSTOauth("https://www.bungie.net/Platform/GroupV2/" + clan.getClanID() + "/Members/Approve/" + bungieUser.getMembershipType() + "/" + bungieUser.getID() + "/"); 78 | } 79 | 80 | /** 81 | * Approves all requests to join the clan 82 | */ 83 | public void approveAllPendingMembers() throws APIException { 84 | hu.urlRequestPOSTOauth("https://www.bungie.net/Platform/GroupV2/" + clan.getClanID() + "/Members/ApproveAll/"); 85 | } 86 | 87 | /** 88 | * Denies all pending requests to join the clan :) 89 | */ 90 | public void denyAllPendingMembers() throws APIException { 91 | hu.urlRequestPOSTOauth("https://www.bungie.net/Platform/GroupV2/" + clan.getClanID() + "/Members/DenyAll/?components=200"); 92 | } 93 | 94 | /** 95 | * Abdicates foundership to the admin specified (This user must already be an admin of the clan) 96 | * 97 | * @param bungieUser The user who will be the new founder (leader) of the clan 98 | */ 99 | public void abdicateFoundership(BungieUser bungieUser) throws APIException { 100 | hu.urlRequestPOSTOauth("https://www.bungie.net/Platform/GroupV2/" + clan.getClanID() + "/Admin/AbdicateFoundership/" + bungieUser.getMembershipType() + "/" + bungieUser.getID() + "/"); 101 | } 102 | 103 | /** 104 | * Adds a new optional conversation to the clan 105 | * 106 | * @param chatName The name of the chat 107 | * @param clanChatSecuritySetting The security setting of the chat 108 | */ 109 | public void addOptionalConversation(String chatName, ClanChatSecuritySetting clanChatSecuritySetting) throws APIException { 110 | JsonObject jsonObject = new JsonObject(); 111 | jsonObject.addProperty("chatName", chatName); 112 | jsonObject.addProperty("chatSecurity", clanChatSecuritySetting.getSetting()); 113 | 114 | hu.urlRequestPOSTOauth("https://www.bungie.net/Platform/GroupV2/" + clan.getClanID() + "/Admin/OptionalConversations/Add/", jsonObject); 115 | } 116 | 117 | /** 118 | * Edits an optional conversation 119 | * 120 | * @param conversationID The ID of the conversation 121 | * @param chatEnabled Whether or not the chat is enabled 122 | * @param chatName The name of the chat 123 | * @param clanChatSecuritySetting The security setting of the chat 124 | */ 125 | public void editOptionalConversation(String conversationID, boolean chatEnabled, String chatName, ClanChatSecuritySetting clanChatSecuritySetting) throws APIException { 126 | JsonObject jsonObject = new JsonObject(); 127 | jsonObject.addProperty("chatName", chatName); 128 | jsonObject.addProperty("chatSecurity", clanChatSecuritySetting.getSetting()); 129 | jsonObject.addProperty("chatEnabled", chatEnabled); 130 | 131 | hu.urlRequestPOSTOauth("https://www.bungie.net/Platform/GroupV2/" + clan.getClanID() + "/Admin/OptionalConversations/" + conversationID + "/Edit/", jsonObject); 132 | } 133 | 134 | /** 135 | * Gets a list of members who have been banned from the clan 136 | * 137 | * @return The list of banned users 138 | */ 139 | public List getBannedMembers() throws APIException { 140 | if (bannedMembers != null) { return bannedMembers; } 141 | 142 | List temp = new ArrayList<>(); 143 | 144 | JsonArray jo = hu.urlRequestGETOauth("https://www.bungie.net/Platform/GroupV2/" + clan.getClanID() + "/Banned/?componenets=200").getAsJsonObject("Response").get("results").getAsJsonArray(); 145 | for (JsonElement je : jo) { 146 | temp.add(new BungieUser(je.getAsJsonObject().getAsJsonObject("destinyUserInfo").get("membershipId").getAsString())); 147 | } 148 | return temp; 149 | } 150 | 151 | /** 152 | * Returns a list of pending members to the clan, Never cached: always makes a new request 153 | */ 154 | public List getPendingMembers() throws APIException { 155 | List temp = new ArrayList<>(); 156 | JsonArray ja = hu.urlRequestGETOauth("https://www.bungie.net/Platform/GroupV2/" + clan.getClanID() + "/Members/Pending/?components=200").get("Response").getAsJsonObject().get("results").getAsJsonArray(); 157 | 158 | for (JsonElement je : ja) { 159 | temp.add(new BungieUser(je.getAsJsonObject().getAsJsonObject("destinyUserInfo").get("membershipId").getAsString())); 160 | } 161 | 162 | return temp; 163 | } 164 | 165 | /** 166 | * Returns a list of members who have been invited to the clan 167 | * 168 | * Has pagination feature that is not implemented because what clan would have more than 50 invited users? 169 | * 170 | * @return The list of invited users 171 | */ 172 | public List getInvitedMembers() throws APIException { 173 | List temp = new ArrayList<>(); 174 | 175 | JsonArray ja = hu.urlRequestGETOauth("https://www.bungie.net/Platform/GroupV2/" + clan.getClanID() + "/Members/InvitedIndividuals/?components=200").getAsJsonObject("Response").getAsJsonArray("results"); 176 | 177 | for(JsonElement je : ja) { 178 | temp.add(new BungieUser(je.getAsJsonObject().getAsJsonObject("destinyUserInfo").get("membershipId").getAsString())); 179 | } 180 | 181 | return temp; 182 | } 183 | 184 | /** 185 | * Check if a BungieUser is a pending applicant without the performance overhead of creating multiple BungieUsers 186 | */ 187 | public boolean isPendingMember(BungieUser bungieUser) throws APIException { 188 | JsonArray ja = hu.urlRequestGETOauth("https://www.bungie.net/Platform/GroupV2/" + clan.getClanID() + "/Members/Pending/?components=200").get("Response").getAsJsonObject().get("results").getAsJsonArray(); 189 | 190 | for(JsonElement jsonElement : ja) { 191 | if(bungieUser.getID().equals(jsonElement.getAsJsonObject().getAsJsonObject("destinyUserInfo").get("membershipId").getAsString())) { 192 | return true; 193 | } 194 | } 195 | 196 | return false; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/clan/ClanMember.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.clan; 9 | 10 | import com.google.gson.JsonObject; 11 | import net.dec4234.javadestinyapi.material.user.BungieUser; 12 | import net.dec4234.javadestinyapi.utils.StringUtils; 13 | 14 | import java.util.Date; 15 | 16 | /** 17 | * Represents a "ClanMember" which is basically the same as a BungieUser but with some added-on info like join date, 18 | * is online and last online date. This information is pulled from the /Members/ list of a clan. 19 | */ 20 | public class ClanMember extends BungieUser { 21 | 22 | private int memberType; 23 | private boolean isOnline; 24 | private String lastOnlineStatusChange, groupId; 25 | private Date joinDate, lastOnline; 26 | 27 | public ClanMember(JsonObject jsonObject) { 28 | super(jsonObject.getAsJsonObject("destinyUserInfo").get("membershipId").getAsString(), jsonObject.getAsJsonObject("destinyUserInfo")); 29 | memberType = jsonObject.get("memberType").getAsInt(); 30 | isOnline = jsonObject.get("isOnline").getAsBoolean(); 31 | lastOnlineStatusChange = jsonObject.get("lastOnlineStatusChange").getAsString(); 32 | groupId = jsonObject.get("groupId").getAsString(); 33 | 34 | joinDate = StringUtils.valueOfZTime(jsonObject.get("joinDate").getAsString()); 35 | } 36 | 37 | public int getMemberType() { 38 | return memberType; 39 | } 40 | 41 | /** 42 | * @return True if this user is currently online in Destiny 2 43 | */ 44 | public boolean isOnline() { 45 | return isOnline; 46 | } 47 | 48 | /** 49 | * When was the last time this user's online status changed. In other words, when they went from offline to online 50 | * or online to offline. If they are currently online then this could tell you how long they have been on in the 51 | * current playing session. 52 | * @return The date representing when their online status changed 53 | */ 54 | public Date getLastOnlineStatusChange() { 55 | if(lastOnline == null) { 56 | try { 57 | lastOnline = new Date(Long.parseLong(lastOnlineStatusChange) * 1000); 58 | } catch (NumberFormatException e) { 59 | return null; 60 | } 61 | } 62 | 63 | return lastOnline; 64 | } 65 | 66 | /** 67 | * @return The number of days since this user's online status last changed 68 | */ 69 | public double getDaysSinceLastPlayed() { 70 | return StringUtils.getDaysSinceTime(getLastOnlineStatusChange()); 71 | } 72 | 73 | /** 74 | * @return The ID of the clan that this user belongs to 75 | */ 76 | public String getGroupId() { 77 | return groupId; 78 | } 79 | 80 | /** 81 | * @return The date this user joined their current clan 82 | */ 83 | public Date getJoinDate() { 84 | return joinDate; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/clan/GroupType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.clan; 9 | 10 | /** 11 | * The group type. This doesn't really matter since Clans are the only type of group. But, this is included for future 12 | * compatibility and clarity. 13 | */ 14 | public enum GroupType { 15 | 16 | GENERAL(0), 17 | CLAN(1); 18 | 19 | private int type; 20 | 21 | GroupType(int type) { 22 | this.type = type; 23 | } 24 | 25 | /** 26 | * @return the integer representation of the type 27 | */ 28 | public int getType() { 29 | return type; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/inventory/CollectionsManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.inventory; 9 | 10 | import net.dec4234.javadestinyapi.exceptions.APIException; 11 | import net.dec4234.javadestinyapi.material.inventory.items.DestinyItem; 12 | import net.dec4234.javadestinyapi.material.user.BungieUser; 13 | import net.dec4234.javadestinyapi.utils.framework.ContentFramework; 14 | 15 | public class CollectionsManager extends ContentFramework { 16 | 17 | private BungieUser bungieUser; 18 | 19 | public CollectionsManager(BungieUser bungieUser) throws APIException { 20 | super("https://www.bungie.net/Platform/Destiny2/" + bungieUser.getMembershipType() + "/Profile/" + bungieUser.getID() + "/?components=800", source -> { 21 | return source.getAsJsonObject("Response"); 22 | }); 23 | this.bungieUser = bungieUser; 24 | } 25 | 26 | public boolean hasCollectedItem(String collectibleHash) throws APIException { 27 | try { 28 | return getJO().getAsJsonObject("profileCollectibles").getAsJsonObject("data").getAsJsonObject("collectibles").getAsJsonObject(collectibleHash).get("state").getAsInt() == 0; 29 | } catch (NullPointerException e) { 30 | return false; 31 | } 32 | } 33 | 34 | public boolean hasCollectedItem(DestinyItem destinyItem) throws APIException { 35 | try { 36 | return getJO().getAsJsonObject("profileCollectibles").getAsJsonObject("data").getAsJsonObject("collectibles").getAsJsonObject(destinyItem.getCollectibleHash()).get("state").getAsInt() == 0; 37 | } catch (NullPointerException e) { 38 | return false; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/inventory/InventoryManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.inventory; 9 | 10 | import net.dec4234.javadestinyapi.exceptions.APIException; 11 | import net.dec4234.javadestinyapi.material.user.BungieUser; 12 | import net.dec4234.javadestinyapi.material.user.DestinyCharacter; 13 | 14 | /** 15 | * Provides useful utility methods for Inventory Management 16 | * 17 | * All Inventory Management functions work on the presumption that you have OAuth working 18 | */ 19 | public class InventoryManager { 20 | 21 | // Account in question 22 | private BungieUser bungieUser; 23 | 24 | public InventoryManager(BungieUser bungieUser) { 25 | this.bungieUser = bungieUser; 26 | } 27 | 28 | public DestinyCharacter getCharacterOfType(DestinyCharacter.DestinyClass destinyClass) throws APIException { 29 | return bungieUser.getCharacterOfType(destinyClass); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/inventory/items/Armor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.inventory.items; 9 | 10 | import net.dec4234.javadestinyapi.material.user.DestinyCharacter; 11 | 12 | public class Armor extends InventoryItem { 13 | 14 | public Armor(String hashID, String instanceID, DestinyCharacter characterOwner) { 15 | super(hashID, instanceID, characterOwner); 16 | } 17 | 18 | public Armor(String hashID, String instanceId, DestinyCharacter characterOwner, int quantity, int bindStatus, int location, String bucketHash, int transferStatus, boolean lockable, int state, int dismantlePermission, String overrideStyleItemHash, boolean isWrapper) { 19 | super(hashID, instanceId, characterOwner, quantity, bindStatus, location, bucketHash, transferStatus, lockable, state, dismantlePermission, overrideStyleItemHash, isWrapper); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/inventory/items/DestinyItem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.inventory.items; 9 | 10 | import com.google.gson.JsonElement; 11 | import com.google.gson.JsonObject; 12 | import net.dec4234.javadestinyapi.exceptions.APIException; 13 | import net.dec4234.javadestinyapi.material.DestinyAPI; 14 | import net.dec4234.javadestinyapi.material.manifest.DestinyManifest; 15 | import net.dec4234.javadestinyapi.material.manifest.ManifestEntityTypes; 16 | import net.dec4234.javadestinyapi.utils.HttpUtils; 17 | import net.dec4234.javadestinyapi.utils.StringUtils; 18 | import net.dec4234.javadestinyapi.utils.framework.ContentInterface; 19 | 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | 23 | /** 24 | * A Destiny Inventory Item such as any weapon or armor piece 25 | */ 26 | public class DestinyItem implements ContentInterface { 27 | 28 | HttpUtils hu = DestinyAPI.getHttpUtils(); 29 | 30 | private String hashID, name, icon, description; 31 | private boolean hasIcon; 32 | private String collectibleHash, screenshot; 33 | private ItemTier itemTier; 34 | 35 | private ItemTier tier; 36 | 37 | private JsonObject jo, dp; 38 | 39 | public DestinyItem(String hashID) { 40 | this.hashID = hashID; 41 | } 42 | 43 | public DestinyItem(String hashID, String name, String icon, boolean hasIcon) { 44 | this.hashID = hashID; 45 | this.name = name; 46 | this.icon = icon; 47 | this.hasIcon = hasIcon; 48 | } 49 | 50 | public String getHashID() throws APIException { 51 | if(hashID == null) { 52 | checkJO(); 53 | hashID = jo.get("hash").getAsString(); 54 | } 55 | return hashID; 56 | } 57 | 58 | public int getHashIDasInt() throws APIException { 59 | return Integer.parseInt(getHashID()); 60 | } 61 | 62 | /** 63 | * Gets the name of the item 64 | */ 65 | public String getName() throws APIException { 66 | if(name == null) { 67 | checkDP(); 68 | name = dp.get("name").getAsString(); 69 | } 70 | return name; 71 | } 72 | 73 | /** 74 | * Plug this after https://www.bungie.net/ in a browser 75 | */ 76 | public String getIcon() throws APIException { 77 | if(icon == null) { 78 | checkDP(); 79 | icon = dp.get("icon").getAsString(); 80 | } 81 | return icon; 82 | } 83 | 84 | /** 85 | * Gets the lore descriptions associated with this item 86 | */ 87 | public String getDescription() throws APIException { 88 | if(description == null) { 89 | checkDP(); 90 | description = dp.get("description").getAsString(); 91 | } 92 | return description; 93 | } 94 | 95 | public boolean hasIcon() throws APIException { 96 | checkDP(); 97 | hasIcon = dp.get("hasIcon").getAsBoolean(); 98 | return hasIcon; 99 | } 100 | 101 | public String getCollectibleHash() throws APIException { 102 | checkJO(); 103 | if(jo.has("collectibleHash")) { 104 | collectibleHash = jo.get("collectibleHash").getAsString(); 105 | } 106 | 107 | return collectibleHash; 108 | } 109 | 110 | public String getScreenshot() throws APIException { 111 | checkJO(); 112 | if(jo.has("screenshot") && screenshot == null) { 113 | screenshot = jo.get("screenshot").getAsString(); 114 | } 115 | 116 | return screenshot; 117 | } 118 | 119 | public ItemTier getItemTier() throws APIException { 120 | if(itemTier == null) { 121 | itemTier = assessItemTier(); 122 | } 123 | return itemTier; 124 | } 125 | 126 | private ItemTier assessItemTier() throws APIException { 127 | checkJO(); 128 | switch(jo.getAsJsonObject("inventory").get("tierTypeName").getAsString()) { 129 | case "Common": 130 | return ItemTier.COMMON; 131 | case "Uncommon": 132 | return ItemTier.UNCOMMON; 133 | case "Rare": 134 | return ItemTier.RARE; 135 | case "Legendary": 136 | return ItemTier.LEGENDARY; 137 | case "Exotic": 138 | return ItemTier.EXOTIC; 139 | } 140 | return null; 141 | } 142 | 143 | @Override 144 | public void checkJO() throws APIException { 145 | if(jo == null) { 146 | jo = new DestinyManifest().manifestGET(ManifestEntityTypes.INVENTORYITEM, hashID); 147 | // jo = hu.manifestGET(ManifestEntityTypes.INVENTORYITEM, hashID).getAsJsonObject("Response"); 148 | } 149 | } 150 | 151 | public void checkDP() throws APIException { 152 | if(dp == null) { 153 | checkJO(); 154 | dp = jo.getAsJsonObject("displayProperties"); 155 | } 156 | } 157 | 158 | public enum ItemTier { 159 | COMMON, 160 | UNCOMMON, 161 | RARE, 162 | LEGENDARY, 163 | EXOTIC; 164 | } 165 | 166 | /** 167 | * Return a list of all items that contain or match the name provided 168 | */ 169 | public static List searchForItems(String itemName) throws APIException { 170 | HttpUtils httpUtils = DestinyAPI.getHttpUtils(); 171 | List destinyItemList = new ArrayList<>(); 172 | itemName = StringUtils.httpEncode(itemName); 173 | 174 | for(JsonElement jsonElement : httpUtils.urlRequestGET("https://www.bungie.net/Platform/Destiny2/Armory/Search/DestinyInventoryItemDefinition/" + itemName + "/").getAsJsonObject("Response").getAsJsonObject("results").getAsJsonArray("results")) { 175 | JsonObject jsonObject = jsonElement.getAsJsonObject(); 176 | JsonObject displayProperties = jsonObject.getAsJsonObject("displayProperties"); 177 | destinyItemList.add(new DestinyItem(jsonObject.get("hash").getAsString(), displayProperties.get("name").getAsString(), 178 | displayProperties.get("icon").getAsString(), displayProperties.get("hasIcon").getAsBoolean())); 179 | } 180 | return destinyItemList; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/inventory/items/InventoryBucket.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.inventory.items; 9 | 10 | /** 11 | * An inventory Bucket is a specific slot which contains a group of items, 12 | * such as heavy weapons or helmets 13 | */ 14 | public enum InventoryBucket { 15 | KINETIC("1498876634", true), 16 | SPECIAL("2465295065", true), 17 | HEAVY("953998645", true), 18 | GHOST("4023194814", true), 19 | ARTIFACT(""), 20 | 21 | HELMET("3448274439", true), 22 | ARMS("3551918588", true), 23 | CHESTPLATE("14239492", true), 24 | BOOTS("20886954", true), 25 | CLASS_ITEM("1585787867", true), 26 | 27 | SPARROW("", true), 28 | SHIP("284967655", true), 29 | EMBLEM("4274335291", true), 30 | FINISHER("3683254069"), 31 | EMOTE(""), 32 | UNSEEN_EMOTE("2401704334"), 33 | 34 | ENGRAM("375726501"), 35 | SEASONAL_TOOLS("1345459588"), 36 | 37 | PROFILE_INVENTORY("1469714392"), 38 | MOD_INVENTORY("3313201758"), 39 | VAULT("138197802"), 40 | QUEST_MANAGER("1558457900"); 41 | 42 | private String hash; 43 | private boolean isEquippable = false; 44 | 45 | InventoryBucket(String hash) { 46 | this.hash = hash; 47 | } 48 | 49 | InventoryBucket(String hash, boolean isEquippable) { 50 | this.hash = hash; 51 | this.isEquippable = isEquippable; 52 | } 53 | 54 | public String getHash() { 55 | return hash; 56 | } 57 | 58 | public boolean isEquippable() { return isEquippable; } 59 | 60 | public static InventoryBucket fromHash(String hash) { 61 | for (InventoryBucket inventoryBucket : InventoryBucket.values()) { 62 | if (inventoryBucket.getHash().equals(hash)) { 63 | return inventoryBucket; 64 | } 65 | } 66 | 67 | return null; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/inventory/items/InventoryItem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.inventory.items; 9 | 10 | import com.google.gson.JsonElement; 11 | import com.google.gson.JsonObject; 12 | import net.dec4234.javadestinyapi.exceptions.APIException; 13 | import net.dec4234.javadestinyapi.material.DestinyAPI; 14 | import net.dec4234.javadestinyapi.material.user.DestinyCharacter; 15 | import net.dec4234.javadestinyapi.utils.HttpUtils; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | 20 | /** 21 | * An InventoryItem describes any item contained in a player's or one of their characters inventories. 22 | * This could be a weapon, armor piece, ghost, etc. 23 | *

24 | * This class is currently incomplete. If you would like to contribute, please create a pull request on GitHub 25 | */ 26 | public class InventoryItem extends DestinyItem { 27 | 28 | private static HttpUtils httpUtils = DestinyAPI.getHttpUtils(); 29 | 30 | private DestinyCharacter characterOwner; 31 | private ItemLocation itemLocation = ItemLocation.CHARACTER_INVENTORY; 32 | 33 | private String instanceId, bucketHash, overrideStyleItemHash; 34 | private InventoryBucket inventoryBucket; 35 | 36 | private int quantity, bindStatus, location, transferStatus, state, dismantlePermission; 37 | private boolean lockable = false, isWrapper; 38 | 39 | public InventoryItem(String instanceId, DestinyCharacter characterOwner) throws APIException { 40 | this(httpUtils.urlRequestGET(HttpUtils.URL_BASE + "/Destiny2/" + characterOwner.getMembershipType() + "/Profile/" + characterOwner.getMembershipID() + "/Item/" + instanceId + "/?components=305").getAsJsonObject("Response").getAsJsonObject("item").getAsJsonObject("data")); 41 | 42 | this.characterOwner = characterOwner; 43 | } 44 | 45 | public InventoryItem(String hashID, String instanceId, DestinyCharacter characterOwner) { 46 | super(hashID); 47 | this.instanceId = instanceId; 48 | this.characterOwner = characterOwner; 49 | } 50 | 51 | /** 52 | * Used if an item is in a profile-level inventory such as the "Inventory" or "Mods Page" as well as the vault 53 | */ 54 | public InventoryItem(String hashID, String instanceId, DestinyCharacter characterOwner, ItemLocation itemLocation) { 55 | super(hashID); 56 | this.instanceId = instanceId; 57 | 58 | this.characterOwner = characterOwner; 59 | this.itemLocation = itemLocation; 60 | } 61 | 62 | public InventoryItem(String hashID, String instanceId, DestinyCharacter characterOwner, int quantity, int bindStatus, int location, String bucketHash, int transferStatus, boolean lockable, int state, int dismantlePermission, boolean isWrapper) { 63 | super(hashID); 64 | this.characterOwner = characterOwner; 65 | this.instanceId = instanceId; 66 | 67 | this.quantity = quantity; 68 | this.bindStatus = bindStatus; 69 | this.location = location; 70 | this.bucketHash = bucketHash; 71 | this.inventoryBucket = InventoryBucket.fromHash(bucketHash); 72 | this.transferStatus = transferStatus; 73 | this.lockable = lockable; 74 | this.state = state; 75 | this.dismantlePermission = dismantlePermission; 76 | this.isWrapper = isWrapper; 77 | } 78 | 79 | public InventoryItem(String hashID, String instanceId, DestinyCharacter characterOwner, int quantity, int bindStatus, int location, String bucketHash, int transferStatus, boolean lockable, int state, int dismantlePermission, String overrideStyleItemHash, boolean isWrapper) { 80 | super(hashID); 81 | this.instanceId = instanceId; 82 | this.characterOwner = characterOwner; 83 | 84 | this.quantity = quantity; 85 | this.bindStatus = bindStatus; 86 | this.location = location; 87 | this.bucketHash = bucketHash; 88 | this.transferStatus = transferStatus; 89 | this.lockable = lockable; 90 | this.state = state; 91 | this.dismantlePermission = dismantlePermission; 92 | this.overrideStyleItemHash = overrideStyleItemHash; 93 | this.isWrapper = isWrapper; 94 | } 95 | 96 | private InventoryItem(JsonObject jsonObject) { 97 | super(jsonObject.get("itemHash").getAsString()); 98 | 99 | 100 | } 101 | 102 | /** 103 | * Gets the plugs on the item such as selected perks and mods 104 | * 105 | * @return Returns a list of ItemPlugs 106 | */ 107 | public List getItemPlugs() throws APIException { 108 | JsonObject jsonObject = httpUtils.urlRequestGET(HttpUtils.URL_BASE + "/Destiny2/" + characterOwner.getMembershipType() + "/Profile/" + characterOwner.getMembershipID() + "/Item/" + instanceId + "/?components=305"); 109 | List itemPlugs = new ArrayList<>(); 110 | jsonObject = jsonObject.getAsJsonObject("Response").getAsJsonObject("sockets").getAsJsonObject("data"); 111 | 112 | for(JsonElement jsonElement : jsonObject.getAsJsonArray("sockets")) { 113 | JsonObject object = jsonElement.getAsJsonObject(); 114 | itemPlugs.add(new ItemPlug(object.get("plugHash").getAsString(), object.get("isEnabled").getAsBoolean(), object.get("isVisible").getAsBoolean())); 115 | } 116 | 117 | return itemPlugs; 118 | } 119 | 120 | /** 121 | * Move an item from the current character to another 122 | * 123 | * @return Returns true if the move was succesful 124 | */ 125 | public boolean moveTo(DestinyCharacter destinyCharacter) throws APIException { 126 | if(!isInVault()) { 127 | moveToVault(); 128 | } 129 | JsonObject jsonObject = httpUtils.urlRequestPOSTOauth(HttpUtils.URL_BASE + "/Destiny2/Actions/Items/TransferItem/", prepareJsonObject(destinyCharacter, false, isItemEquippable())); 130 | 131 | boolean wasSuccesful = wasTransferSuccesful(jsonObject); 132 | 133 | if(wasSuccesful) { 134 | characterOwner = destinyCharacter; 135 | itemLocation = ItemLocation.CHARACTER_INVENTORY; 136 | } 137 | 138 | return wasSuccesful; 139 | } 140 | 141 | /** 142 | * Move the item from its current position to the vault 143 | * 144 | * @return True if the move was reported as succesful 145 | */ 146 | public boolean moveToVault() throws APIException { 147 | JsonObject jsonObject = httpUtils.urlRequestPOSTOauth(HttpUtils.URL_BASE + "/Destiny2/Actions/Items/TransferItem/", prepareJsonObject(getCharacterOwner(), true, isItemEquippable())); 148 | 149 | boolean wasSuccesful = wasTransferSuccesful(jsonObject); 150 | 151 | if(wasSuccesful) { 152 | characterOwner = null; 153 | itemLocation = ItemLocation.VAULT; 154 | } 155 | 156 | return wasSuccesful; 157 | } 158 | 159 | /** 160 | * "Equip" this item, aka move it to the active slot 161 | * There are specific restrictions on when an item can be equipped 162 | * 1. If it is an exotic, the exotic slot must be free within that vertical category 163 | * 2. The guardian must be offline, in orbit or at the tower 164 | * 165 | * @return Returns whether or not the action was succesful 166 | */ 167 | public boolean equip() throws APIException { 168 | JsonObject jsonObject = new JsonObject(); 169 | jsonObject.addProperty("itemId", getInstanceIDAsLong()); 170 | jsonObject.addProperty("characterId", getCharacterOwner().getCharacterIDAsLong()); 171 | jsonObject.addProperty("membershipType", getCharacterOwner().getMembershipType()); 172 | 173 | JsonObject response = httpUtils.urlRequestPOSTOauth(HttpUtils.URL_BASE + "/Destiny2/Actions/Items/EquipItem/", jsonObject); 174 | 175 | if(response.get("ErrorCode").getAsInt() == 1641) { // Item is an exotic and cannot be equipped because there is an exotic present in another slot 176 | return false; 177 | } 178 | 179 | // Figure out what message is returned when item cannot be equipped at that characters present location 180 | if(response.get("ErrorCode").getAsInt() == 1641) { // Character is in a location that does not permit item equipping through the API 181 | return false; 182 | } 183 | 184 | return true; 185 | } 186 | 187 | /** 188 | * Needs more testing 189 | * 190 | * Is the item equippable in the invenory bucket that it belongs to 191 | * Sometimes not possible like if it is in the vault or inventory tab 192 | */ 193 | public boolean isItemEquippable() { 194 | InventoryBucket inventoryBucket = getInventoryBucket(); 195 | 196 | return inventoryBucket.isEquippable(); 197 | } 198 | 199 | /** 200 | * Lock or unlock an item 201 | * @param state The state of the lock the item should be set to 202 | */ 203 | public void setLockState(boolean state) throws APIException { 204 | JsonObject jsonObject = new JsonObject(); 205 | jsonObject.addProperty("state", state); 206 | jsonObject.addProperty("itemId", getInstanceId()); 207 | jsonObject.addProperty("characterId", getCharacterOwner().getCharacterIDAsLong()); 208 | jsonObject.addProperty("membershipType", getCharacterOwner().getMembershipType()); 209 | 210 | 211 | httpUtils.urlRequestPOSTOauth(HttpUtils.URL_BASE + "/Destiny2/Actions/Items/SetLockState/", jsonObject); 212 | } 213 | 214 | /** 215 | * Is the item capable of being locked 216 | */ 217 | public boolean isItemLockable() { 218 | return this.lockable; 219 | } 220 | 221 | public enum ItemLocation { 222 | CHARACTER_INVENTORY, 223 | PROFILE_INVENTORY, 224 | MOD_INVENTORY, 225 | VAULT; 226 | } 227 | 228 | /** 229 | * The character the item is currently in the inventory of 230 | * will be null if the item is in the vault 231 | */ 232 | public DestinyCharacter getCharacterOwner() { return characterOwner; } 233 | 234 | public ItemLocation getItemLocation() { 235 | return itemLocation; 236 | } 237 | 238 | public boolean isInVault() { 239 | return getItemLocation() == ItemLocation.VAULT; 240 | } 241 | 242 | public String getInstanceId() { 243 | return instanceId; 244 | } 245 | 246 | public long getInstanceIDAsLong() { return Long.parseLong(getInstanceId()); } 247 | 248 | public String getBucketHash() { 249 | return bucketHash; 250 | } 251 | 252 | public InventoryBucket getInventoryBucket() { return InventoryBucket.fromHash(getBucketHash()); } 253 | 254 | public String getOverrideStyleItemHash() { 255 | return overrideStyleItemHash; 256 | } 257 | 258 | public int getQuantity() { 259 | return quantity; 260 | } 261 | 262 | public int getBindStatus() { 263 | return bindStatus; 264 | } 265 | 266 | public int getLocation() { 267 | return location; 268 | } 269 | 270 | public int getTransferStatus() { 271 | return transferStatus; 272 | } 273 | 274 | public int getState() { 275 | return state; 276 | } 277 | 278 | public int getDismantlePermission() { 279 | return dismantlePermission; 280 | } 281 | 282 | public boolean isLockable() { 283 | return lockable; 284 | } 285 | 286 | public boolean isWrapper() { 287 | return isWrapper; 288 | } 289 | 290 | /** 291 | * Prepares a POST body for item transferring 292 | */ 293 | private JsonObject prepareJsonObject(DestinyCharacter destinyCharacter, boolean moveToVault, boolean isEquippable) throws APIException { 294 | JsonObject jsonObject = new JsonObject(); 295 | jsonObject.addProperty("itemReferenceHash", getHashIDasInt()); 296 | jsonObject.addProperty("stackSize", isEquippable ? 1 : getQuantity()); // If the item is equippable, set stack size as 1 297 | jsonObject.addProperty("transferToVault", moveToVault); 298 | jsonObject.addProperty("itemId", getInstanceIDAsLong()); 299 | jsonObject.addProperty("characterId", destinyCharacter.getCharacterIDAsLong()); 300 | jsonObject.addProperty("membershipType", destinyCharacter.getMembershipType()); 301 | 302 | return jsonObject; 303 | } 304 | 305 | /** 306 | * Determines whether or not an item transfer was succesful by Bungie's standards 307 | */ 308 | private boolean wasTransferSuccesful(JsonObject response) { 309 | try { 310 | return response.get("Response").getAsInt() == 0 && response.get("ErrorCode").getAsInt() == 1; 311 | } catch (NullPointerException ex) { 312 | return false; 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/inventory/items/ItemPerk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.inventory.items; 9 | 10 | import com.google.gson.JsonObject; 11 | import net.dec4234.javadestinyapi.exceptions.APIException; 12 | import net.dec4234.javadestinyapi.material.manifest.DestinyManifest; 13 | import net.dec4234.javadestinyapi.material.manifest.ManifestEntityTypes; 14 | 15 | public class ItemPerk { 16 | 17 | private JsonObject jsonObject; 18 | private String requirementDisplayString, perkHash, iconPath; 19 | private int perkVisibility; 20 | private boolean isActive, visible; 21 | 22 | public ItemPerk(String perkHash) throws APIException { 23 | this(new DestinyManifest().manifestGET(ManifestEntityTypes.INVENTORYITEM, perkHash)); 24 | } 25 | 26 | public ItemPerk(JsonObject jsonObject) { 27 | System.out.println(jsonObject); 28 | this.jsonObject = jsonObject; 29 | } 30 | 31 | public JsonObject getJsonObject() { 32 | return jsonObject; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/inventory/items/ItemPlug.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.inventory.items; 9 | 10 | import com.google.gson.JsonObject; 11 | import net.dec4234.javadestinyapi.exceptions.APIException; 12 | import net.dec4234.javadestinyapi.material.manifest.DestinyManifest; 13 | import net.dec4234.javadestinyapi.material.manifest.ManifestEntityTypes; 14 | import org.jetbrains.annotations.NotNull; 15 | 16 | /** 17 | * An ItemPlug refers to something like a mod on a gun or armor piece 18 | * 19 | * // System.out.println(httpUtils.urlRequestGET(HttpUtils.URL_BASE + "/Destiny2/3/Profile/4611686018468620320/Item/6917529335994005821/?components=305")); 20 | */ 21 | public class ItemPlug { 22 | 23 | // Outer Information 24 | private boolean isEnabled, isVisible; 25 | // Parent object info 26 | private JsonObject jsonObject, displayProperties, inventory, plug; 27 | private String hash, name, description, icon, itemTypeDisplayName, flavorText, uiItemDisplayStyle, itemTypeAndTierDisplayName, displaySource, acquireRewardSiteHash, acquireUnlockHash, 28 | summaryItemHash; 29 | private boolean hasIcon, allowActions, doesPostmasterPullHaveSideEffects, nonTransferrable, equippable, isWrapper, redacted, blacklisted; 30 | private String[] itemCategoryHashes; 31 | private ItemPerk[] itemPerks; 32 | // TO-DO: Action Sub-Object Info 33 | // Inventory Sub-Object Info 34 | private String bucketTypeHash, recoveryBucketTypeHash, tierTypeHash, tierTypeName, expirationTooltip, expiredInActivityMessage, expiredInOrbitMessage; 35 | private int maxStackSize, tierType; 36 | private boolean isInstanceItem, nonTransferrableOriginal, suppressExpirationWhenObjectivesComplete; 37 | // Plug Sub-Object Info 38 | private String plugCategoryIdentifier, plugCategoryHash, uiPlugLabel, alternateUiPlugLabel, actionRewardSiteHash, actionRewardItemOverrideHash, insertionMaterialRequirementHash, previewItemOverrideHash, enabledMaterialRequirementHash; 39 | private boolean onActionRecreateSelf, isDummyPlug, applyStatsToSocketOwnerItem; 40 | private int plugStyle, plugAvailibility, alternatePlugStyle; 41 | 42 | public ItemPlug(@NotNull String hash) throws APIException { 43 | this(new DestinyManifest().manifestGET(ManifestEntityTypes.INVENTORYITEM, hash)); 44 | } 45 | 46 | public ItemPlug(@NotNull String hash, boolean isEnabled, boolean isVisible) throws APIException { 47 | this(hash); 48 | 49 | this.isEnabled = isEnabled; 50 | this.isVisible = isVisible; 51 | } 52 | 53 | public ItemPlug(@NotNull JsonObject jsonObject) { 54 | this.jsonObject = jsonObject; 55 | this.displayProperties = jsonObject.getAsJsonObject("displayProperties"); 56 | this.inventory = jsonObject.getAsJsonObject("inventory"); 57 | this.plug = jsonObject.getAsJsonObject("plug"); 58 | 59 | 60 | this.hash = jsonObject.get("hash").getAsString(); 61 | 62 | setFromObjects(); 63 | } 64 | 65 | private void setFromObjects() { 66 | // Display Properties 67 | this.description = displayProperties.get("description").getAsString(); 68 | this.name = displayProperties.get("name").getAsString(); 69 | this.icon = displayProperties.get("icon").getAsString(); 70 | this.hasIcon = displayProperties.get("hasIcon").getAsBoolean(); 71 | 72 | // Inventory Properties 73 | this.maxStackSize = inventory.get("maxStackSize").getAsInt(); 74 | this.bucketTypeHash = inventory.get("bucketTypeHash").getAsString(); 75 | this.recoveryBucketTypeHash = inventory.get("recoveryBucketTypeHash").getAsString(); 76 | this.tierTypeHash = inventory.get("tierTypeHash").getAsString(); 77 | this.isInstanceItem = inventory.get("isInstanceItem").getAsBoolean(); 78 | this.nonTransferrableOriginal = inventory.get("nonTransferrableOriginal").getAsBoolean(); 79 | this.tierTypeName = inventory.get("tierTypeName").getAsString(); 80 | this.tierType = inventory.get("tierType").getAsInt(); 81 | this.expirationTooltip = inventory.get("expirationTooltip").getAsString(); 82 | this.expiredInActivityMessage = inventory.get("expiredInActivityMessage").getAsString(); 83 | this.expiredInOrbitMessage = inventory.get("expiredInOrbitMessage").getAsString(); 84 | this.suppressExpirationWhenObjectivesComplete = inventory.get("suppressExpirationWhenObjectivesComplete").getAsBoolean(); 85 | 86 | // Plug Properties 87 | } 88 | 89 | public JsonObject getJsonObject() { 90 | return jsonObject; 91 | } 92 | 93 | /* 94 | Reference taken 12/10/2021 95 | 96 | { 97 | "displayProperties":{ 98 | "description":"High damage, high recoil.", 99 | "name":"Aggressive Frame", 100 | "icon":"/common/destiny2_content/icons/0a18b7264e9fb76764756a25d0a20fd2.png", 101 | "iconSequences":[ 102 | { 103 | "frames":[ 104 | "/common/destiny2_content/icons/0a18b7264e9fb76764756a25d0a20fd2.png" 105 | ] 106 | }, 107 | { 108 | "frames":[ 109 | "/common/destiny2_content/icons/a4a27f236a0724056b56d02495d8a857.png" 110 | ] 111 | } 112 | ], 113 | "hasIcon":true 114 | }, 115 | "tooltipNotifications":[ 116 | 117 | ], 118 | "backgroundColor":{ 119 | "colorHash":0, 120 | "red":0, 121 | "green":0, 122 | "blue":0, 123 | "alpha":0 124 | }, 125 | "itemTypeDisplayName":"Intrinsic", 126 | "flavorText":"", 127 | "uiItemDisplayStyle":"ui_display_style_intrinsic_plug", 128 | "itemTypeAndTierDisplayName":"Exotic Intrinsic", 129 | "displaySource":"", 130 | "action":{ 131 | "verbName":"Dismantle", 132 | "verbDescription":"", 133 | "isPositive":false, 134 | "requiredCooldownSeconds":0, 135 | "requiredItems":[ 136 | 137 | ], 138 | "progressionRewards":[ 139 | 140 | ], 141 | "actionTypeLabel":"shard", 142 | "rewardSheetHash":0, 143 | "rewardItemHash":0, 144 | "rewardSiteHash":0, 145 | "requiredCooldownHash":0, 146 | "deleteOnAction":true, 147 | "consumeEntireStack":false, 148 | "useOnAcquire":false 149 | }, 150 | "inventory":{ 151 | "maxStackSize":1, 152 | "bucketTypeHash":1469714392, 153 | "recoveryBucketTypeHash":215593132, 154 | "tierTypeHash":2759499571, 155 | "isInstanceItem":false, 156 | "nonTransferrableOriginal":false, 157 | "tierTypeName":"Exotic", 158 | "tierType":6, 159 | "expirationTooltip":"", 160 | "expiredInActivityMessage":"", 161 | "expiredInOrbitMessage":"", 162 | "suppressExpirationWhenObjectivesComplete":true 163 | }, 164 | "plug":{ 165 | "insertionRules":[ 166 | 167 | ], 168 | "plugCategoryIdentifier":"intrinsics", 169 | "plugCategoryHash":1744546145, 170 | "onActionRecreateSelf":false, 171 | "actionRewardSiteHash":0, 172 | "actionRewardItemOverrideHash":0, 173 | "insertionMaterialRequirementHash":0, 174 | "previewItemOverrideHash":0, 175 | "enabledMaterialRequirementHash":0, 176 | "enabledRules":[ 177 | 178 | ], 179 | "uiPlugLabel":"", 180 | "plugStyle":0, 181 | "plugAvailability":0, 182 | "alternateUiPlugLabel":"", 183 | "alternatePlugStyle":0, 184 | "isDummyPlug":false, 185 | "applyStatsToSocketOwnerItem":false 186 | }, 187 | "acquireRewardSiteHash":0, 188 | "acquireUnlockHash":0, 189 | "investmentStats":[ 190 | 191 | ], 192 | "perks":[ 193 | { 194 | "requirementDisplayString":"", 195 | "perkHash":886374680, 196 | "perkVisibility":0 197 | } 198 | ], 199 | "summaryItemHash":3520001075, 200 | "allowActions":true, 201 | "doesPostmasterPullHaveSideEffects":false, 202 | "nonTransferrable":true, 203 | "itemCategoryHashes":[ 204 | 59, 205 | 2237038328, 206 | 610365472 207 | ], 208 | "specialItemType":0, 209 | "itemType":19, 210 | "itemSubType":0, 211 | "classType":3, 212 | "breakerType":0, 213 | "equippable":false, 214 | "defaultDamageType":0, 215 | "isWrapper":false, 216 | "hash":1525239159, 217 | "index":5016, 218 | "redacted":false, 219 | "blacklisted":false 220 | } 221 | */ 222 | } 223 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/inventory/loadouts/Loadout.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.inventory.loadouts; 9 | 10 | import com.google.gson.JsonObject; 11 | import net.dec4234.javadestinyapi.material.inventory.items.InventoryItem; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | /** 17 | * This class is currently incomplete. If you would like to contribute, please create a pull request on GitHub 18 | */ 19 | public class Loadout { 20 | 21 | private String colorHash; 22 | private String nameHash; 23 | private String iconHash; 24 | 25 | private String characterID; 26 | 27 | private List items = new ArrayList<>(); 28 | 29 | public Loadout(String characterID, String colorHash, String nameHash, String iconHash, List items) { 30 | this.colorHash = colorHash; 31 | this.nameHash = nameHash; 32 | this.iconHash = iconHash; 33 | this.items = items; 34 | } 35 | 36 | public Loadout(String characterID, JsonObject jsonObject) { 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/manifest/DestinyManifest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.manifest; 9 | 10 | import com.google.gson.JsonObject; 11 | import net.dec4234.javadestinyapi.exceptions.APIException; 12 | import net.dec4234.javadestinyapi.material.DestinyAPI; 13 | import net.dec4234.javadestinyapi.utils.framework.ContentFramework; 14 | 15 | import java.util.HashMap; 16 | 17 | /** 18 | * The Manifest is where you go to translate hashes commonly returned in longer requests to more specific information. 19 | * The Manifest is available in the most common languages, English, Spanish, etc. 20 | * Hashes are split into multiple categories such as InventoryItem and Perk categories and hashes must be appropriately 21 | * matched in order to get the response you want. 22 | */ 23 | public class DestinyManifest extends ContentFramework { 24 | 25 | private static HashMap worldComponents = new HashMap<>(); 26 | 27 | public DestinyManifest() { 28 | super("https://www.bungie.net/Platform/Destiny2/Manifest/", source -> { 29 | return source.getAsJsonObject("Response"); 30 | }); 31 | } 32 | 33 | /** 34 | * A standard Manifest GET 35 | * Unlike the manifestGET in HttpUtils(), this will not make a request every single time 36 | * 37 | * It'll download the entire definition library the first time and it'll cache it 38 | */ 39 | public JsonObject manifestGET(ManifestEntityTypes manifestEntityTypes, String hash) throws APIException { 40 | JsonObject jsonObject = getDefinitionLibrary(manifestEntityTypes); 41 | 42 | if(jsonObject.has(hash)) { 43 | return jsonObject.getAsJsonObject(hash); 44 | } 45 | 46 | return null; 47 | } 48 | 49 | /** 50 | * Get the current version of the manifest 51 | * Useful for checking for updates 52 | */ 53 | public String getVersion() throws APIException { 54 | return getJO().get("version").getAsString(); 55 | } 56 | 57 | public String getMobileAssetContentPath() throws APIException { 58 | return getJO().get("mobileAssetContentPath").getAsString(); 59 | } 60 | 61 | public String getMobileWorldContentPath(Language language) throws APIException { 62 | return getJO().getAsJsonObject("mobileWorldContentPaths").get(language.getCode()).getAsString(); 63 | } 64 | 65 | public String getJsonWorldContentPath(Language language) throws APIException { 66 | return getJO().getAsJsonObject("jsonWorldContentPaths").get(language.getCode()).getAsString(); 67 | } 68 | 69 | public JsonObject getWorldContent(Language language) throws APIException { 70 | return DestinyAPI.getHttpUtils().urlRequestGET("https://www.bungie.net" + getJsonWorldContentPath(language)); 71 | } 72 | 73 | /** 74 | * Get the entirety of the specified definition library 75 | */ 76 | public JsonObject getDefinitionLibrary(ManifestEntityTypes manifestEntityTypes) throws APIException { 77 | Language language = Language.ENGLISH; 78 | 79 | return getDefinitionLibrary(language, manifestEntityTypes); 80 | } 81 | 82 | public JsonObject getDefinitionLibrary(Language language, ManifestEntityTypes manifestEntityTypes) throws APIException { 83 | if(!worldComponents.containsKey(manifestEntityTypes.getBungieEntityValue())) { 84 | worldComponents.put(manifestEntityTypes.getBungieEntityValue(), DestinyAPI.getHttpUtils().urlRequestGET("https://www.bungie.net" + getJO().getAsJsonObject("jsonWorldComponentContentPaths").getAsJsonObject(language.getCode()).get(manifestEntityTypes.getBungieEntityValue()).getAsString())); 85 | } 86 | 87 | return worldComponents.get(manifestEntityTypes.getBungieEntityValue()); 88 | } 89 | 90 | public String getMobileClanBannerDatabasePath() throws APIException { 91 | return getJO().get("mobileClanBannerDatabasePath").getAsString(); 92 | } 93 | 94 | public enum Language { 95 | 96 | ENGLISH("en"), 97 | FRENCH("fr"), 98 | SPANISH("es"), 99 | SPANISH_MEXICO("es-mx"), 100 | GERMAN("de"), 101 | ITALIAN("it"), 102 | JAPANESE("ja"), 103 | PORTUGUESE_BRAZIL("pt-br"), 104 | RUSSIAN("ru"), 105 | KOREAN("ko"), 106 | ZH_CHT("zh-cht"), 107 | ZH_CHS("zh-chs"); 108 | 109 | String code; 110 | 111 | private Language(String code) { 112 | this.code = code; 113 | } 114 | 115 | public String getCode() { 116 | return code; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/manifest/ManifestEntityTypes.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.manifest; 9 | 10 | /** 11 | * Stores definitions as seen on https://www.bungie-net.github.io
12 | * Used for accessing things in the Destiny Manifest
13 | * Sorted by category, then by alphabetical order 14 | */ 15 | public enum ManifestEntityTypes { 16 | 17 | // Relating to items/character's inventory 18 | ARTIFACT("DestinyArtifactDefinition"), 19 | BREAKERTYPE("DestinyBreakerTypeDefinition"), 20 | COLLECTIBLE("DestinyCollectibleDefinition"), 21 | EQUIPMENTSLOT("DestinyEquipmentSlotDefinition"), 22 | INVENTORYBUCKET("DesintInventoryBucketDefinition"), 23 | /** Any item that can go in a user's invenory such as armor, weapons, and planetary materials */ 24 | INVENTORYITEM("DestinyInventoryItemDefinition"), 25 | ITEMCATEGORY("DestinyItemCategoryDefinition"), 26 | ITEMSTAT("DestinyStatDefinition"), 27 | ITEMSTATGROUP("DestinyStatGroupDefinition"), 28 | ITEMTIER("DestinyItemTierTypeDefinition"), 29 | MATERIALREQUIREMENTSET("DestinyMaterialRequirementSetDefinition"), 30 | POWERCAP("DestinyPowerCapDefinition"), 31 | RECORD("DestinyRecordDefinition"), 32 | REWARDSOURCE("DestinyRewardSourceDefinition"), 33 | SANDBOXPERK("DestinySandboxPerkDefinition"), 34 | TALENTGRID("DestinyTalentGridDefinition"), 35 | 36 | // Relating to users 37 | CLASS("DestinyClassDefinition"), 38 | GENDER("DestinyGenderDefinition"), 39 | MILESTONE("DestinyMilestoneDefinition"), 40 | PROGRESSION("DestinyProgressionDefinition"), 41 | RACE("DestinyRaceDefinition"), 42 | 43 | // Vendors/world 44 | ACTIVITY("DestinyActivityDefinition"), 45 | ACTIVITYGRAPH("DestinyActivityGraphDefinition"), 46 | ACTIVITYMODE("DestinyActivityModeDefinition"), 47 | ACTIVITYMODIFIER("DestinyActivityModifierDefinition"), 48 | ACTIVITYTYPE("DestinyActivityTypeDefinition"), 49 | DAMAGETYPE("DestinyDamageTypeDefinition"), 50 | DESTINATION("DestinyDestinationDefinition"), 51 | FACTION("DestinyFactionDefinition"), 52 | LOCATION("DestinyLocationDefinition"), 53 | OBJECTIVE("DestinyObjectiveDefinition"), 54 | PLACE("DestinyPlaceDefinition"), 55 | VENDOR("DestinyVendorDefinition"), 56 | VENDORGROUP("DestinyVendorGroupDefinition"), 57 | 58 | // Misc 59 | CHECKLIST("DestinyChecklistDefinition"), 60 | ENERGYTYPE("DestinyEnergyTypeDefinition"), 61 | HISTORICALSTATS("DestinyHistoricalStatsDefinition"), 62 | PRESENTATIONNODE("DestinyPresentationNodeDefinition"), 63 | LORE("DestinyLoreDefinition"), 64 | METRIC("DestinyMetricDefinition"), 65 | PLUGSET("DestinyPlugSetDefinition"), 66 | REPORTREASONCATEGORY("DestinyReportReasonCategoryDefinition"), 67 | SEASON("DestinySeasonDefinition"), 68 | SEASONPASS("DestinySeasonPassDefinition"), 69 | SOCKETCATEGORY("DestinySocketCategoryDefinition"), 70 | SOCKETTYPE("DestinySocketTypeDefinition"), 71 | TAGMETADATA("TagMetadataDefinition"), 72 | TRAIT("DestinyTraitDefinition"), 73 | TRAITCATEGORY("DestinyTraitCategoryDefinition"), 74 | UNLOCK("DestinyUnlockDefinition"); 75 | 76 | String bungieEntityValue; 77 | 78 | private ManifestEntityTypes(String bungieEntityValue) { 79 | this.bungieEntityValue = bungieEntityValue; 80 | } 81 | 82 | public String getBungieEntityValue() { 83 | return bungieEntityValue; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/social/SocialFriend.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.social; 9 | 10 | import com.google.gson.JsonObject; 11 | 12 | import java.util.Date; 13 | 14 | public class SocialFriend { 15 | 16 | // Outer 17 | private String id, bungieGlobalDisplayName; 18 | private int membershipType, bungieGlobalDisplayNameCode, onlineStatus, onlineTitle, relationship; 19 | 20 | // Bungie Net User 21 | private String bungieNetId, uniqueName, displayName, successMessageFlags, about, locale, profilePicturePath, profileThemeName, userTitleDisplay, statusText; 22 | private int profilePicture, profileTheme, userTitle; 23 | private boolean isDeleted, showActivity, localeInheritDefault, showGroupMessaging; 24 | private Date firstAccess, lastUpdate, statusDate; 25 | private String steamDisplayName, blizzardDisplayName, stadiaDisplayName, xboxDisplayName, psnDisplayName; // Null by default unless found in the response, could have any combo 26 | 27 | 28 | // Context 29 | private boolean isFollowing, isIgnored; 30 | private int ignoreFlags; 31 | 32 | public SocialFriend(JsonObject jsonObject) { 33 | JsonObject bungieNetUser = jsonObject.getAsJsonObject("bungieNetUser"); 34 | 35 | id = jsonObject.get("lastSeenAsMembershipId").getAsString(); 36 | membershipType = jsonObject.get("lastSeenAsBungieMembershipType").getAsInt(); 37 | bungieGlobalDisplayName = jsonObject.get("bungieGlobalDisplayName").getAsString(); 38 | bungieGlobalDisplayNameCode = jsonObject.get("bungieGlobalDisplayNameCode").getAsInt(); 39 | 40 | onlineStatus = jsonObject.get("onlineStatus").getAsInt(); 41 | onlineTitle = jsonObject.get("onlineTitle").getAsInt(); 42 | relationship = jsonObject.get("relationship").getAsInt(); 43 | 44 | bungieNetId = bungieNetUser.get("membershipId").getAsString(); 45 | uniqueName = bungieNetUser.get("displayName").getAsString(); 46 | profilePicture = bungieNetUser.get("profilePicture").getAsInt(); 47 | profileTheme = bungieNetUser.get("profileTheme").getAsInt(); 48 | } 49 | } 50 | 51 | /* 52 | {"Response":{"friends":[{"lastSeenAsMembershipId":"4611686018467335984","lastSeenAsBungieMembershipType":3,"bungieGlobalDisplayName":"Wertesse","bungieGlobalDisplayNameCode":4231,"onlineStatus":0,"onlineTitle":0,"relationship":1,"bungieNetUser":{"membershipId":"4047006","uniqueName":"Wertesse#4231","displayName":"Wertesse","profilePicture":70533,"profileTheme":6,"userTitle":0,"successMessageFlags":"8","isDeleted":false,"about":"0b6f70c8","firstAccess":"2013-10-09T19:45:31.861Z","lastUpdate":"2021-11-12T01:14:22.989Z","context":{"isFollowing":false,"ignoreStatus":{"isIgnored":false,"ignoreFlags":0}},"psnDisplayName":"Wertesse","xboxDisplayName":"Wertesse","showActivity":true,"locale":"en","localeInheritDefault":true,"showGroupMessaging":true,"profilePicturePath":"/img/profile/avatars/bungieday_11.jpg","profileThemeName":"d2cover","userTitleDisplay":"Newbie","statusText":"","statusDate":"0001-01-01T00:00:00Z","blizzardDisplayName":"Wertesse","steamDisplayName":"Wertesse","stadiaDisplayName":"Wertesse","cachedBungieGlobalDisplayName":"Wertesse","cachedBungieGlobalDisplayNameCode":4231}},{"lastSeenAsMembershipId":"4611686018475577660","lastSeenAsBungieMembershipType":3,"bungieGlobalDisplayName":"MagicTaco","bungieGlobalDisplayNameCode":6492,"onlineStatus":0,"onlineTitle":0,"relationship":1,"bungieNetUser":{"membershipId":"11420910","uniqueName":"MagicTaco#6492","displayName":"MagicTaco","profilePicture":70589,"profileTheme":1103,"userTitle":0,"successMessageFlags":"8","isDeleted":false,"about":"","firstAccess":"2015-08-11T03:07:39.073Z","lastUpdate":"2022-02-28T21:36:24.871Z","context":{"isFollowing":false,"ignoreStatus":{"isIgnored":false,"ignoreFlags":0}},"showActivity":true,"locale":"en","localeInheritDefault":false,"showGroupMessaging":true,"profilePicturePath":"/img/profile/avatars/bungie_day_15_02.jpg","profileThemeName":"d2_03","userTitleDisplay":"Newbie","statusText":"","statusDate":"0001-01-01T00:00:00Z","blizzardDisplayName":"Magictaco55","steamDisplayName":"MagicTaco","cachedBungieGlobalDisplayName":"MagicTaco","cachedBungieGlobalDisplayNameCode":6492}},{"lastSeenAsMembershipId":"4611686018511450218","lastSeenAsBungieMembershipType":3,"bungieGlobalDisplayName":"DIOSCABINET","bungieGlobalDisplayNameCode":2143,"onlineStatus":0,"onlineTitle":0,"relationship":1,"bungieNetUser":{"membershipId":"27343616","uniqueName":"DIOSCABINET#2143","displayName":"DIOSCABINET","profilePicture":70069,"profileTheme":1130,"userTitle":0,"successMessageFlags":"0","isDeleted":false,"about":"","firstAccess":"2021-01-07T10:28:55.813Z","lastUpdate":"2021-03-02T13:11:01.154Z","context":{"isFollowing":false,"ignoreStatus":{"isIgnored":false,"ignoreFlags":0}},"xboxDisplayName":"DIOSCABINET","showActivity":true,"locale":"en","localeInheritDefault":false,"showGroupMessaging":true,"profilePicturePath":"/img/profile/avatars/h3_icon.gif","profileThemeName":"d2_30","userTitleDisplay":"Newbie","statusText":"","statusDate":"0001-01-01T00:00:00Z","steamDisplayName":"DIOSCABINET","cachedBungieGlobalDisplayName":"DIOSCABINET","cachedBungieGlobalDisplayNameCode":2143}},{"lastSeenAsMembershipId":"4611686018467307956","lastSeenAsBungieMembershipType":3,"bungieGlobalDisplayName":"RarePepe","bungieGlobalDisplayNameCode":3185,"onlineStatus":1,"onlineTitle":2,"relationship":1,"bungieNetUser":{"membershipId":"14950176","uniqueName":"RarePepe#3185","displayName":"RarePepe","profilePicture":0,"profileTheme":0,"userTitle":0,"successMessageFlags":"0","isDeleted":false,"about":"","firstAccess":"2017-07-14T16:00:53.342Z","lastUpdate":"2019-08-20T20:12:33.326Z","context":{"isFollowing":false,"ignoreStatus":{"isIgnored":false,"ignoreFlags":0}},"showActivity":true,"locale":"en","localeInheritDefault":false,"showGroupMessaging":true,"profilePicturePath":"/img/profile/avatars/default_avatar.gif","profileThemeName":"d2cover","userTitleDisplay":"Newbie","statusText":"","statusDate":"0001-01-01T00:00:00Z","blizzardDisplayName":"RarePePe","steamDisplayName":"RarePepe","cachedBungieGlobalDisplayName":"RarePepe","cachedBungieGlobalDisplayNameCode":3185}},{"lastSeenAsMembershipId":"4611686018429577316","lastSeenAsBungieMembershipType":1,"bungieGlobalDisplayName":"MeatballGG","bungieGlobalDisplayNameCode":1711,"onlineStatus":1,"onlineTitle":2,"relationship":1,"bungieNetUser":{"membershipId":"13319567","uniqueName":"MeatballGG#1711","displayName":"MeatballGG","profilePicture":70637,"profileTheme":1103,"userTitle":0,"successMessageFlags":"0","isDeleted":false,"about":"","firstAccess":"2016-05-20T23:49:50.334Z","lastUpdate":"2021-09-16T18:31:28.274Z","context":{"isFollowing":false,"ignoreStatus":{"isIgnored":false,"ignoreFlags":0}},"psnDisplayName":"ItalagMD","xboxDisplayName":"ItalagPhD","showActivity":true,"locale":"en","localeInheritDefault":false,"showGroupMessaging":true,"profilePicturePath":"/img/profile/avatars/cc06.jpg","profileThemeName":"d2_03","userTitleDisplay":"Newbie","statusText":"","statusDate":"0001-01-01T00:00:00Z","steamDisplayName":"MeatballGG","stadiaDisplayName":"ItalagPhD#2924","twitchDisplayName":"italagphd","cachedBungieGlobalDisplayName":"MeatballGG","cachedBungieGlobalDisplayNameCode":1711}},{"lastSeenAsMembershipId":"4611686018430111996","lastSeenAsBungieMembershipType":1,"bungieGlobalDisplayName":"Etho","bungieGlobalDisplayNameCode":880,"onlineStatus":0,"onlineTitle":0,"relationship":1,"bungieNetUser":{"membershipId":"12955985","uniqueName":"Etho#880","displayName":"Etho","profilePicture":70652,"profileTheme":111,"userTitle":0,"successMessageFlags":"32","isDeleted":false,"about":"","firstAccess":"2016-02-20T16:54:10.195Z","lastUpdate":"2020-06-04T20:01:39.915Z","context":{"isFollowing":false,"ignoreStatus":{"isIgnored":false,"ignoreFlags":0}},"psnDisplayName":"heavenly_sl","xboxDisplayName":"ER 3RR","showActivity":true,"locale":"en","localeInheritDefault":false,"showGroupMessaging":true,"profilePicturePath":"/img/profile/avatars/cc22.jpg","profileThemeName":"d2cover","userTitleDisplay":"Newbie","statusText":"To much core not enough muscle","statusDate":"2017-01-10T22:46:47.29Z","steamDisplayName":"Etho","cachedBungieGlobalDisplayName":"Etho","cachedBungieGlobalDisplayNameCode":880}},{"lastSeenAsMembershipId":"4611686018503453178","lastSeenAsBungieMembershipType":3,"bungieGlobalDisplayName":"MyLittleCocky","bungieGlobalDisplayNameCode":2781,"onlineStatus":0,"onlineTitle":0,"relationship":1,"bungieNetUser":{"membershipId":"25599365","uniqueName":"25599365","displayName":"MyLittleCocky","profilePicture":0,"profileTheme":0,"userTitle":0,"successMessageFlags":"0","isDeleted":false,"about":"","firstAccess":"2020-05-15T06:15:15.214Z","lastUpdate":"2020-05-15T06:15:15.214Z","context":{"isFollowing":false,"ignoreStatus":{"isIgnored":false,"ignoreFlags":0}},"xboxDisplayName":"FR MOULD","showActivity":true,"locale":"en","localeInheritDefault":false,"showGroupMessaging":true,"profilePicturePath":"/img/profile/avatars/default_avatar.gif","profileThemeName":"d2cover","userTitleDisplay":"Newbie","statusText":"","statusDate":"0001-01-01T00:00:00Z","steamDisplayName":"MyLittleCocky"}},{"lastSeenAsMembershipId":"4611686018473535234","lastSeenAsBungieMembershipType":3,"bungieGlobalDisplayName":"Hydro_Neck","bungieGlobalDisplayNameCode":3789,"onlineStatus":0,"onlineTitle":0,"relationship":1,"bungieNetUser":{"membershipId":"18117827","uniqueName":"Hydro_Neck#3789","displayName":"Hydro_Neck","profilePicture":70700,"profileTheme":1125,"userTitle":0,"successMessageFlags":"0","isDeleted":false,"about":"","firstAccess":"2018-05-27T02:40:00.451Z","lastUpdate":"2019-09-05T04:48:38.525Z","context":{"isFollowing":false,"ignoreStatus":{"isIgnored":false,"ignoreFlags":0}},"psnDisplayName":"Hydro_Neck","xboxDisplayName":"KnavishHorizon1","showActivity":true,"locale":"en","localeInheritDefault":false,"showGroupMessaging":true,"profilePicturePath":"/img/profile/avatars/cc72.jpg","profileThemeName":"d2_25","userTitleDisplay":"Newbie","statusText":"","statusDate":"0001-01-01T00:00:00Z","blizzardDisplayName":"HydroNeck#1482","steamDisplayName":"Hydro_Neck","twitchDisplayName":"hydro_neck","cachedBungieGlobalDisplayName":"Hydro_Neck","cachedBungieGlobalDisplayNameCode":3789}},{"lastSeenAsMembershipId":"4611686018470828611","lastSeenAsBungieMembershipType":3,"bungieGlobalDisplayName":"SJ","bungieGlobalDisplayNameCode":7821,"onlineStatus":0,"onlineTitle":0,"relationship":1,"bungieNetUser":{"membershipId":"16619921","uniqueName":"16619921","displayName":"SJD","profilePicture":70516,"profileTheme":1119,"userTitle":0,"successMessageFlags":"0","isDeleted":false,"about":"","firstAccess":"2017-10-24T23:44:02.897Z","lastUpdate":"2021-09-04T20:56:14.075Z","context":{"isFollowing":false,"ignoreStatus":{"isIgnored":false,"ignoreFlags":0}},"showActivity":false,"locale":"en","localeInheritDefault":false,"showGroupMessaging":true,"profilePicturePath":"/img/profile/avatars/Destiny25.jpg","profileThemeName":"d2_19","userTitleDisplay":"Newbie","statusText":"","statusDate":"0001-01-01T00:00:00Z","blizzardDisplayName":"SJD","steamDisplayName":"SJ","cachedBungieGlobalDisplayName":""}}]},"ErrorCode":1,"ThrottleSeconds":0,"ErrorStatus":"Success","Message":"Ok","MessageData":{}} 53 | */ -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/social/SocialFriendsList.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.social; 9 | 10 | import com.google.gson.JsonElement; 11 | import com.google.gson.JsonObject; 12 | import net.dec4234.javadestinyapi.exceptions.APIException; 13 | import net.dec4234.javadestinyapi.material.DestinyAPI; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | import static net.dec4234.javadestinyapi.utils.HttpUtils.URL_BASE; 19 | 20 | public class SocialFriendsList { 21 | 22 | public List getFriendsList() throws APIException { 23 | List list = new ArrayList<>(); 24 | 25 | JsonObject jo = DestinyAPI.getHttpUtils().urlRequestGETOauth(URL_BASE + "/Social/Friends/"); 26 | 27 | if(!jo.has("Response")) { 28 | return null; 29 | } 30 | 31 | for(JsonElement jsonElement : jo.getAsJsonObject("Response").getAsJsonArray("friends")) { 32 | JsonObject jsonObject = jsonElement.getAsJsonObject(); 33 | list.add(new SocialFriend(jsonObject)); 34 | } 35 | 36 | return list; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/user/DestinyCharacter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.user; 9 | 10 | import com.google.gson.JsonArray; 11 | import com.google.gson.JsonElement; 12 | import com.google.gson.JsonObject; 13 | import net.dec4234.javadestinyapi.exceptions.APIException; 14 | import net.dec4234.javadestinyapi.material.DestinyAPI; 15 | import net.dec4234.javadestinyapi.material.inventory.items.DestinyItem; 16 | import net.dec4234.javadestinyapi.material.inventory.items.InventoryItem; 17 | import net.dec4234.javadestinyapi.material.inventory.loadouts.Loadout; 18 | import net.dec4234.javadestinyapi.material.manifest.ManifestEntityTypes; 19 | import net.dec4234.javadestinyapi.stats.activities.Activity; 20 | import net.dec4234.javadestinyapi.utils.HttpUtils; 21 | import net.dec4234.javadestinyapi.utils.StringUtils; 22 | import net.dec4234.javadestinyapi.utils.framework.ContentFramework; 23 | 24 | import java.util.ArrayList; 25 | import java.util.Date; 26 | import java.util.List; 27 | 28 | /** 29 | * A DestinyCharacter is a character on a user's account like a Warlock, Titan, or Hunter. 30 | *
31 | * There is a lot of character-specific information that is captured within this class. 32 | */ 33 | public class DestinyCharacter extends ContentFramework { 34 | 35 | private BungieUser bungieUser; 36 | 37 | private String characterID, minutesPlayedTotal; 38 | private Date lastPlayed; 39 | private int minutesPlayedThisSession, lightLevel = -1; 40 | 41 | private Gender gender; 42 | private DestinyClass d2class; 43 | private Race race; 44 | 45 | private String emblemPath, emblemBackgroundPath, emblemHash; 46 | 47 | private List allActivities; 48 | 49 | HttpUtils hu = DestinyAPI.getHttpUtils(); 50 | 51 | public DestinyCharacter(BungieUser bungieUser, String characterID) throws APIException { 52 | super("https://www.bungie.net/Platform/Destiny2/" + bungieUser.getMembershipType() + "/Profile/" + bungieUser.getID() + "/Character/" + characterID + "/?components=200", 53 | source -> source.getAsJsonObject("Response").getAsJsonObject("character").getAsJsonObject("data")); 54 | this.characterID = characterID; 55 | this.bungieUser = bungieUser; 56 | } 57 | 58 | protected DestinyCharacter(JsonObject jsonObject, BungieUser bungieUser, String characterID) { 59 | super(jsonObject); 60 | jo = jsonObject; 61 | this.characterID = characterID; 62 | this.bungieUser = bungieUser; 63 | } 64 | 65 | /** 66 | * Get the membership ID of the account that owns this character 67 | */ 68 | public String getMembershipID() { 69 | return bungieUser.getID(); 70 | } 71 | 72 | /** 73 | * Get the membershipType of the account that owns this character 74 | */ 75 | public int getMembershipType() throws APIException { 76 | return bungieUser.getMembershipType(); 77 | } 78 | 79 | /** 80 | * Get the BungieUser that owns this account 81 | */ 82 | public BungieUser getBungieUser() { return bungieUser; } 83 | 84 | /** 85 | * Get the characterID of this character 86 | */ 87 | public String getCharacterID() { return characterID; } 88 | 89 | /** 90 | * Return the character id as a long, aka int64 91 | */ 92 | public Long getCharacterIDAsLong() { return Long.parseLong(getCharacterID()); } 93 | 94 | /** 95 | * Get the Date that this character was last played 96 | */ 97 | public Date getLastPlayed() throws APIException { 98 | if (lastPlayed == null) { 99 | lastPlayed = StringUtils.valueOfZTime(getJO().get("dateLastPlayed").getAsString()); 100 | } 101 | return lastPlayed; 102 | } 103 | 104 | /** 105 | * Get the total amount of time, in minutes, that this character has been played in this session 106 | * TO-DO: Define what a "session" is 107 | */ 108 | public int getMinutesPlayedThisSession() throws APIException { 109 | if (minutesPlayedThisSession == -1) { 110 | minutesPlayedThisSession = getJO().get("minutesPlayedThisSession").getAsInt(); 111 | } 112 | return minutesPlayedThisSession; 113 | } 114 | 115 | /** 116 | * Get the total amount of time, in minutes, that has been played on this character 117 | */ 118 | public String getMinutesPlayedTotal() throws APIException { 119 | if (minutesPlayedTotal == null) { 120 | minutesPlayedTotal = getJO().get("minutesPlayedTotal").getAsString(); 121 | } 122 | return minutesPlayedTotal; 123 | } 124 | 125 | /** 126 | * Get the light level of this character 127 | * 128 | * @return The light level of this character, as an integer 129 | */ 130 | public int getLightLevel() throws APIException { 131 | if (lightLevel == -1) { 132 | lightLevel = getJO().get("light").getAsInt(); 133 | } 134 | return lightLevel; 135 | } 136 | 137 | /** 138 | * Get the race of this character 139 | * Either Human, Exo or Awoken 140 | */ 141 | public Race getRace() throws APIException { 142 | if (race == null) { 143 | race = evaluateRace(getJO().get("raceHash").getAsString()); 144 | } 145 | return race; 146 | } 147 | 148 | /** 149 | * Get the gender of this character 150 | * 151 | * @return Male or Female 152 | */ 153 | public Gender getGender() throws APIException { 154 | if (gender == null) { 155 | gender = evaluateGender(getJO().get("genderHash").getAsString()); 156 | } 157 | return gender; 158 | } 159 | 160 | /** 161 | * Get the Class of this character 162 | * Either Warlock, Titan or Hunter 163 | */ 164 | public DestinyClass getD2class() throws APIException { 165 | if (d2class == null) { 166 | d2class = evaluateClass(getJO().get("classHash").getAsString()); 167 | } 168 | return d2class; 169 | } 170 | 171 | /** 172 | * Get the currently equipped emblem of this character 173 | * Add this path to the end of "https://bungie.net/" 174 | * 175 | * @return 176 | */ 177 | public String getEmblemPath() throws APIException { 178 | if (emblemPath == null) { 179 | emblemPath = getJO().get("emblemPath").getAsString(); 180 | } 181 | return emblemPath; 182 | } 183 | 184 | /** 185 | * Get the background of the currently equipped emblem 186 | * Add this path to the end of "https://bungie.net/" 187 | */ 188 | public String getEmblemBackgroundPath() throws APIException { 189 | if (emblemBackgroundPath == null) { 190 | emblemBackgroundPath = getJO().get("emblemBackgroundPath").getAsString(); 191 | } 192 | return emblemBackgroundPath; 193 | } 194 | 195 | /** 196 | * Get the hash of the currently equipped emblem to be used in a manifest request 197 | */ 198 | public String getEmblemHash() throws APIException { 199 | if (emblemHash == null) { 200 | emblemHash = getJO().get("emblemHash").getAsString(); 201 | } 202 | return emblemHash; 203 | } 204 | 205 | /** 206 | * Get a list of the currently equipped items of this character 207 | */ 208 | public List getEquippedItems() throws APIException { 209 | JsonArray jsonArray = hu.urlRequestGET("https://www.bungie.net/Platform/Destiny2/" + getMembershipType() + "/Profile/" + bungieUser.getID() + "/Character/" 210 | + getCharacterID() + "/?components=205").getAsJsonObject("Response").getAsJsonObject("equipment").getAsJsonObject("data").getAsJsonArray("items"); 211 | 212 | List destinyItems = new ArrayList<>(); 213 | 214 | for (JsonElement jsonElement : jsonArray) { 215 | destinyItems.add(new DestinyItem(jsonElement.getAsJsonObject().get("itemHash").getAsString())); 216 | } 217 | 218 | return destinyItems; 219 | } 220 | 221 | /* 222 | public InventoryItem getItemInSlot(InventoryItem.ItemLocation itemLocation) { 223 | for(DestinyItem destinyItem : getEquippedItems()) { 224 | return destinyItem; 225 | } 226 | } 227 | */ 228 | 229 | /** 230 | * Get a list of inventory items present in this character's inventory. 231 | *
232 | * This is an OAUTH action that required inventory reading permission. 233 | * @return A list of inventory items, equipped or unequipped 234 | */ 235 | public List getAllItemsInInventory() throws APIException { 236 | JsonArray jsonArray = hu.urlRequestGETOauth("https://www.bungie.net/Platform/Destiny2/" + getMembershipType() + "/Profile/" + bungieUser.getID() + "/Character/" 237 | + getCharacterID() + "/?components=201").getAsJsonObject("Response").getAsJsonObject("inventory").getAsJsonObject("data").getAsJsonArray("items"); 238 | 239 | List list = new ArrayList<>(); 240 | 241 | for (JsonElement jsonElement : jsonArray) { 242 | JsonObject jsonObject = jsonElement.getAsJsonObject(); 243 | 244 | if (jsonObject.has("itemInstanceId")) { 245 | list.add(new InventoryItem(jsonObject.get("itemHash").getAsString(), 246 | jsonObject.get("itemInstanceId").getAsString(), 247 | this, 248 | jsonObject.get("quantity").getAsInt(), 249 | jsonObject.get("bindStatus").getAsInt(), 250 | jsonObject.get("location").getAsInt(), 251 | jsonObject.get("bucketHash").getAsString(), 252 | jsonObject.get("transferStatus").getAsInt(), 253 | jsonObject.get("lockable").getAsBoolean(), 254 | jsonObject.get("state").getAsInt(), 255 | jsonObject.get("dismantlePermission").getAsInt(), 256 | jsonObject.get("isWrapper").getAsBoolean())); 257 | } 258 | } 259 | 260 | return list; 261 | } 262 | 263 | /** 264 | * TODO: complete 265 | *
266 | * This function is currently incomplete. If you would like to contribute, please create a pull request on GitHub 267 | * @return A list of loadouts on this character 268 | */ 269 | public List getLoadouts() throws APIException { 270 | hu.urlRequestGETOauth(HttpUtils.URL_BASE + "/Destiny2/" + getMembershipType() + "/Profile/" + bungieUser.getID() + "/Character/" + getCharacterID() + "/?components=206,201"); 271 | 272 | List loadouts = new ArrayList<>(); 273 | 274 | 275 | 276 | return loadouts; 277 | } 278 | 279 | /** 280 | * A very resource intensive task, use at your own risk 281 | * Needs work because not all activities return the same JSON info 282 | * = 283 | */ 284 | public List getAllActivities() throws APIException { 285 | if (allActivities != null) { return allActivities; } 286 | allActivities = new ArrayList<>(); 287 | JsonObject jj = hu.urlRequestGET("https://www.bungie.net/Platform/Destiny2/" + getMembershipType() + "/Account/" + getMembershipID() + "/Character/" + getCharacterID() + "/Stats/AggregateActivityStats/"); 288 | for (JsonElement je : jj.getAsJsonObject("Response").getAsJsonArray("activities")) { 289 | allActivities.add(new Activity(je.getAsJsonObject().getAsJsonObject("values").getAsJsonObject("fastestCompletionMsForActivity").get("activityId").getAsString())); 290 | } 291 | return allActivities; 292 | } 293 | 294 | 295 | private boolean hasPlayedInActivity(String pgcrId, List source) throws APIException { 296 | for (Activity activity : source) { 297 | if (pgcrId.equals(activity.getInstanceId())) { 298 | return true; 299 | } 300 | } 301 | 302 | return false; 303 | } 304 | 305 | /** 306 | * Has this character played in the activity with the specified pgcrID 307 | * 308 | * @param pgcrId The ID of the activity you would like to be analyzed 309 | * 310 | * @return True or false depending on if the user played in it 311 | */ 312 | public boolean hasPlayedInActivity(String pgcrId) throws APIException { 313 | List participantIds = new ArrayList<>(); 314 | 315 | new Activity(pgcrId).getParticipants().forEach(activityParticipant -> { 316 | participantIds.add(activityParticipant.getCharacterId()); 317 | }); 318 | 319 | return participantIds.contains(getCharacterID()); 320 | } 321 | 322 | private Gender evaluateGender(String genderHash) throws APIException { 323 | JsonObject jj = hu.manifestGET(ManifestEntityTypes.GENDER, genderHash).getAsJsonObject("Response"); 324 | switch (jj.get("genderType").getAsString()) { 325 | case "0": 326 | return Gender.MALE; 327 | case "1": 328 | return Gender.FEMALE; 329 | } 330 | return null; 331 | } 332 | 333 | private DestinyClass evaluateClass(String classHash) throws APIException { 334 | JsonObject jj = hu.manifestGET(ManifestEntityTypes.CLASS, classHash).getAsJsonObject("Response"); 335 | switch (jj.getAsJsonObject("displayProperties").get("name").getAsString()) { 336 | case "Warlock": 337 | return DestinyClass.WARLOCK; 338 | case "Titan": 339 | return DestinyClass.TITAN; 340 | case "Hunter": 341 | return DestinyClass.HUNTER; 342 | } 343 | return null; 344 | } 345 | 346 | private Race evaluateRace(String raceHash) throws APIException { 347 | JsonObject jj = hu.manifestGET(ManifestEntityTypes.RACE, raceHash).getAsJsonObject("Response"); 348 | switch (jj.getAsJsonObject("displayProperties").get("name").getAsString()) { 349 | case "Exo": 350 | return Race.EXO; 351 | case "Awoken": 352 | return Race.AWOKEN; 353 | case "Human": 354 | return Race.HUMAN; 355 | } 356 | return null; 357 | } 358 | 359 | /** 360 | * Gender of this character. Male or Female 361 | */ 362 | public enum Gender { 363 | MALE("Male"), 364 | FEMALE("Female"); 365 | 366 | private String value; 367 | 368 | private Gender(String value) { 369 | this.value = value; 370 | } 371 | 372 | public String getValue() { return value; } 373 | 374 | } 375 | 376 | /** 377 | * The class of this character. Hunter, Titan or Warlock 378 | */ 379 | public enum DestinyClass { 380 | HUNTER("Hunter"), 381 | TITAN("Titan"), 382 | WARLOCK("Warlock"); 383 | 384 | private String value; 385 | 386 | private DestinyClass(String value) { 387 | this.value = value; 388 | } 389 | 390 | public String getValue() { return value; } 391 | } 392 | 393 | /** 394 | * The race of this character. Awoken, Exo or Human 395 | */ 396 | public enum Race { 397 | AWOKEN("Awoken"), 398 | EXO("Exo"), 399 | HUMAN("Human"); 400 | 401 | private String value; 402 | 403 | private Race(String value) { 404 | this.value = value; 405 | } 406 | 407 | public String getValue() { return value; } 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/user/DestinyPlatform.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.user; 9 | 10 | /** 11 | * Information about each platform that Destiny 2 has/is on 12 | * 13 | * Platform codes and fancy names for each are provided 14 | */ 15 | public enum DestinyPlatform { 16 | 17 | NONE(0, "None"), 18 | 19 | XBOX(1, "Xbox"), 20 | PSN(2, "PSN"), 21 | STEAM(3, "Steam"), 22 | BLIZZARD(4, "Blizzard"), 23 | STADIA(5, "Stadia"), 24 | EPIC(6, "Epic Games Store"), 25 | DEMON(10, "Demon"), 26 | 27 | // Used for searching 28 | BUNGIE_NEXT(254, "BungieNext"), 29 | ALL(-1, "All"); 30 | 31 | private int platformCode; 32 | private String fancyName; 33 | 34 | DestinyPlatform(int platformCode, String fancyName) { 35 | this.platformCode = platformCode; 36 | this.fancyName = fancyName; 37 | } 38 | 39 | /** 40 | * Get the plaform code associated with this platform 41 | */ 42 | public int getPlatformCode() { 43 | return platformCode; 44 | } 45 | 46 | /** 47 | * Get the fancy name associated with this platofrm e.g. "Steam" instead of "STEAM" 48 | * @return 49 | */ 50 | public String getFancyName() { 51 | return fancyName; 52 | } 53 | 54 | /** 55 | * Get a DestinyPlatform from a platform code 56 | */ 57 | public static DestinyPlatform fromMembershipType(int platformCode) { 58 | for(DestinyPlatform destinyPlatform : DestinyPlatform.values()) { 59 | if(destinyPlatform.getPlatformCode() == platformCode) { 60 | return destinyPlatform; 61 | } 62 | } 63 | 64 | return null; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/user/DestinyProfile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.user; 9 | 10 | import com.google.gson.JsonElement; 11 | import com.google.gson.JsonObject; 12 | import net.dec4234.javadestinyapi.utils.StringUtils; 13 | 14 | import java.util.ArrayList; 15 | import java.util.Date; 16 | import java.util.List; 17 | import java.util.Optional; 18 | 19 | public class DestinyProfile { 20 | 21 | private String id, platformDisplayName, globalDisplayName; 22 | private int membershipType, crossSaveOverride, globalDisplayNameCode; 23 | private boolean isPublic; 24 | private List applicableMembershipTypes; 25 | private Optional isCrossSavePrimary = Optional.empty(), isOverridden = Optional.empty(); 26 | private Optional lastSeenDisplayName = Optional.empty(); 27 | private Optional lastSeenDisplayNameType = Optional.empty(); 28 | private Optional dateLastPlayed = Optional.empty(); 29 | 30 | public DestinyProfile(JsonObject jsonObject) { 31 | id = jsonObject.get("membershipId").getAsString(); 32 | membershipType = jsonObject.get("membershipType").getAsInt(); 33 | 34 | platformDisplayName = jsonObject.get("displayName").getAsString(); 35 | crossSaveOverride = jsonObject.get("crossSaveOverride").getAsInt(); 36 | 37 | globalDisplayName = jsonObject.get("bungieGlobalDisplayName").getAsString(); 38 | globalDisplayNameCode = jsonObject.get("bungieGlobalDisplayNameCode").getAsInt(); 39 | 40 | isPublic = jsonObject.get("isPublic").getAsBoolean(); 41 | 42 | List ints = new ArrayList<>(); 43 | 44 | for(JsonElement jsonElement : jsonObject.getAsJsonArray("applicableMembershipTypes")) { 45 | ints.add(jsonElement.getAsInt()); 46 | } 47 | 48 | applicableMembershipTypes = ints; 49 | 50 | if(jsonObject.has("isOverridden")) { 51 | isOverridden = Optional.of(jsonObject.get("isOverridden").getAsBoolean()); 52 | } 53 | 54 | if(jsonObject.has("isCrossSavePrimary")) { 55 | isCrossSavePrimary = Optional.of(jsonObject.get("isCrossSavePrimary").getAsBoolean()); 56 | } 57 | 58 | if(jsonObject.has("dateLastPlayed")) { 59 | dateLastPlayed = Optional.ofNullable(StringUtils.valueOfZTime(jsonObject.get("dateLastPlayed").getAsString())); 60 | } 61 | 62 | if(jsonObject.has("LastSeenDisplayName")) { 63 | lastSeenDisplayName = Optional.of(jsonObject.get("LastSeenDisplayName").getAsString()); 64 | } 65 | 66 | if(jsonObject.has("LastSeenDisplayNameType")) { 67 | lastSeenDisplayNameType = Optional.of(jsonObject.get("LastSeenDisplayNameType").getAsInt()); 68 | } 69 | } 70 | 71 | /** 72 | * Returns the unqiue Bungie ID of this user 73 | */ 74 | public String getId() { 75 | return id; 76 | } 77 | 78 | /** 79 | * Returns the platform display name of this user 80 | * such as one from Steam or Xbox 81 | */ 82 | public String getPlatformDisplayName() { 83 | return platformDisplayName; 84 | } 85 | 86 | /** 87 | * Returns the Global Display Name shared by all active profiles under this account 88 | */ 89 | public String getGlobalDisplayName() { 90 | return globalDisplayName; 91 | } 92 | 93 | /** 94 | * Returns the membership type of this profile 95 | */ 96 | public int getMembershipType() { 97 | return membershipType; 98 | } 99 | 100 | /** 101 | * The cross save override of this profile, and account as a whole 102 | */ 103 | public int getCrossSaveOverride() { 104 | return crossSaveOverride; 105 | } 106 | 107 | public int getGlobalDisplayNameCode() { 108 | return globalDisplayNameCode; 109 | } 110 | 111 | public boolean isPublic() { 112 | return isPublic; 113 | } 114 | 115 | public List getApplicableMembershipTypes() { 116 | return applicableMembershipTypes; 117 | } 118 | 119 | public Optional getIsCrossSavePrimary() { 120 | return isCrossSavePrimary; 121 | } 122 | 123 | public Optional getIsOverridden() { 124 | return isOverridden; 125 | } 126 | 127 | public Optional getLastSeenDisplayName() { 128 | return lastSeenDisplayName; 129 | } 130 | 131 | public Optional getLastSeenDisplayNameType() { 132 | return lastSeenDisplayNameType; 133 | } 134 | 135 | /** 136 | * States whether or not this profile is valid. 137 | * 138 | * A profile is valid if: 139 | * - It's membership type matches the cross save override 140 | * - The cross save override is 0 141 | */ 142 | public boolean isValid() { 143 | return getCrossSaveOverride() == getMembershipType() || getCrossSaveOverride() == 0; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/user/UserCredential.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.user; 9 | 10 | /** 11 | * A "UserCredential" is a linked Platform to a Bungie Account 12 | * 13 | * Such as a twitch account 14 | */ 15 | public class UserCredential { 16 | 17 | private UserCredentialType userCredentialType; 18 | private boolean isPublic; 19 | private String credentialDisplayName; 20 | private String credentialAsString; 21 | 22 | public UserCredential(UserCredentialType userCredentialType, boolean isPublic) { 23 | this.userCredentialType = userCredentialType; 24 | this.isPublic = isPublic; 25 | } 26 | 27 | public UserCredential(UserCredentialType userCredentialType, boolean isPublic, String credentialDisplayName) { 28 | this.userCredentialType = userCredentialType; 29 | this.isPublic = isPublic; 30 | this.credentialDisplayName = credentialDisplayName; 31 | } 32 | 33 | public UserCredential(UserCredentialType userCredentialType, boolean isPublic, String credentialDisplayName, String credentialAsString) { 34 | this.userCredentialType = userCredentialType; 35 | this.isPublic = isPublic; 36 | this.credentialDisplayName = credentialDisplayName; 37 | this.credentialAsString = credentialAsString; 38 | } 39 | 40 | public UserCredentialType getUserCredentialType() { 41 | return userCredentialType; 42 | } 43 | 44 | public boolean isPublic() { 45 | return isPublic; 46 | } 47 | 48 | public String getCredentialDisplayName() { 49 | return credentialDisplayName; 50 | } 51 | 52 | public String getCredentialAsString() { 53 | return credentialAsString; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/material/user/UserCredentialType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.material.user; 9 | 10 | /** 11 | * Not to be confused with DestinyPlatform 12 | * 13 | * Used to identify linked platforms on a bungie.net account such as 14 | * links to twitch and steam 15 | */ 16 | public enum UserCredentialType { 17 | 18 | // When an account has no associated links 19 | NONE(0, "None"), 20 | WILD(3, "Wild"), 21 | FAKE(4, "Fake"), 22 | 23 | // Original consoles 24 | XBOX_ID(1, "Xbox ID"), 25 | PSN_ID(2, "PSN ID"), 26 | 27 | // PC platforms 28 | STEAM_ID(12, "Steam ID"), 29 | BATTLENET_ID(14, "Battle.net ID"), 30 | 31 | // Later Platforms 32 | STADIA_ID(16, "Stadia ID"), 33 | 34 | // Social Media and misc. 35 | FACEBOOK(5, "Facebook"), 36 | GOOGLE(8, "Google"), 37 | WINDOWS(9, "Windows"), 38 | DEMON_ID(10, "Demon ID"), 39 | TWITCH_ID(19, "Twitch ID"); 40 | 41 | private int platformCode; 42 | private String fancyName; 43 | 44 | UserCredentialType(int platformCode, String fancyName) { 45 | this.platformCode = platformCode; 46 | this.fancyName = fancyName; 47 | } 48 | 49 | public int getPlatformCode() { 50 | return platformCode; 51 | } 52 | 53 | public String getFancyName() { 54 | return fancyName; 55 | } 56 | 57 | public static UserCredentialType fromPlatformCode(int platformCode) { 58 | for(UserCredentialType userCredentialType : UserCredentialType.values()) { 59 | if(userCredentialType.getPlatformCode() == platformCode) { 60 | return userCredentialType; 61 | } 62 | } 63 | 64 | return null; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/responses/user/SanitizedUsernamesResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.responses.user; 9 | 10 | import com.google.gson.JsonObject; 11 | 12 | import java.lang.reflect.Field; 13 | 14 | /** 15 | * A class that represents a response from the Bungie API for a request for a list of sanitized usernames. 16 | * Potential responses will include a list of accounts that have been connected to this user's Bungie.net account. 17 | */ 18 | public class SanitizedUsernamesResponse { 19 | private JsonObject raw; 20 | private String SteamId; // capitalized because that's how it is in the JSON response 21 | private String TwitchId; // presumably there are more like this but I don't know what they are 22 | 23 | public SanitizedUsernamesResponse(JsonObject raw) { 24 | this.raw = raw; 25 | 26 | // If performance becomes a concern: https://github.com/Nesaak/NoReflection 27 | for(String key: raw.keySet()) { // trying out a new method of filling in fields automatically 28 | try { 29 | Field field = getClass().getDeclaredField(key); 30 | field.set(this, raw.get(key).getAsString()); 31 | } catch (NoSuchFieldException | IllegalAccessException e) { 32 | e.printStackTrace(); 33 | } 34 | } 35 | } 36 | 37 | public JsonObject getRaw() { 38 | return raw; 39 | } 40 | 41 | public String getSteamId() { 42 | return SteamId; 43 | } 44 | 45 | public String getTwitchId() { 46 | return TwitchId; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/stats/activities/Activity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.stats.activities; 9 | 10 | import com.google.gson.JsonElement; 11 | import com.google.gson.JsonObject; 12 | import net.dec4234.javadestinyapi.exceptions.APIException; 13 | import net.dec4234.javadestinyapi.material.DestinyAPI; 14 | import net.dec4234.javadestinyapi.utils.HttpUtils; 15 | import net.dec4234.javadestinyapi.utils.StringUtils; 16 | import net.dec4234.javadestinyapi.utils.framework.ContentFramework; 17 | 18 | import java.util.ArrayList; 19 | import java.util.Date; 20 | import java.util.List; 21 | 22 | /** 23 | * An activity in this case is a PGCR (Post Game Carnage Report) 24 | * It contains data about an activity that happened like a raid or crucible match 25 | * Can not retrieve info about Activities that have not ended yet 26 | */ 27 | public class Activity extends ContentFramework { 28 | 29 | private HttpUtils hu = DestinyAPI.getHttpUtils(); 30 | 31 | private Date time; 32 | private String activityId, referenceId, directoryActivityHash, instanceId; 33 | private int mode = -1; 34 | private int[] modes; 35 | 36 | public Activity(String activityId) { 37 | super("https://stats.bungie.net/Platform/Destiny2/Stats/PostGameCarnageReport/" + activityId + "/", source -> { 38 | return source.getAsJsonObject("Response"); 39 | }); 40 | this.activityId = activityId; 41 | } 42 | 43 | public Activity(String activityId, Date dateOccured) { 44 | super("https://stats.bungie.net/Platform/Destiny2/Stats/PostGameCarnageReport/" + activityId + "/", source -> { 45 | return source.getAsJsonObject("Response"); 46 | }); 47 | this.activityId = activityId; 48 | this.time = dateOccured; 49 | } 50 | 51 | /** 52 | * Initialize an activity with more information which could improve load times 53 | */ 54 | public Activity(String activityId, String referenceId, String directoryActivityHash, String rawDate, int mode, int[] modes) { 55 | super("https://stats.bungie.net/Platform/Destiny2/Stats/PostGameCarnageReport/" + activityId + "/", source -> { 56 | return source.getAsJsonObject("Response"); 57 | }); 58 | this.activityId = activityId; 59 | this.referenceId = referenceId; 60 | this.directoryActivityHash = directoryActivityHash; 61 | this.time = StringUtils.valueOfZTime(rawDate); 62 | this.mode = mode; 63 | this.modes = modes; 64 | } 65 | 66 | /** 67 | * Get the date this Activity was played 68 | * 69 | * @return 70 | */ 71 | public Date getDatePlayed() throws APIException { 72 | checkJO(); 73 | return time == null ? time = StringUtils.valueOfZTime(getJO().get("period").getAsString()) : time; 74 | } 75 | 76 | /** 77 | * Get the referenceID of this activity 78 | * 79 | * @return 80 | */ 81 | public String getReferenceId() throws APIException { 82 | checkJO(); 83 | return referenceId == null ? referenceId = getJO().getAsJsonObject("activityDetails").get("referenceId").getAsString() : referenceId; 84 | } 85 | 86 | /** 87 | * Get the director activity hash (the type of activity played?) 88 | */ 89 | public String getDirectoryActivityHash() throws APIException { 90 | checkJO(); 91 | return directoryActivityHash == null ? directoryActivityHash = getJO().getAsJsonObject("activityDetails").get("directorActivityHash").getAsString() : directoryActivityHash; 92 | } 93 | 94 | /** 95 | * Gets the instance id, which happens to be the same as the activityId :) 96 | * = 97 | */ 98 | public String getInstanceId() throws APIException { 99 | checkJO(); 100 | return activityId == null ? activityId = getJO().get("instanceId").getAsString() : activityId; 101 | } 102 | 103 | /** 104 | * Get the mode number of the Activity 105 | */ 106 | private int getModeNumber() throws APIException { 107 | checkJO(); 108 | return mode == -1 ? mode = getJO().getAsJsonObject("activityDetails").get("mode").getAsInt() : mode; 109 | } 110 | 111 | /** 112 | * Return the mode of the activity 113 | * Mode list on this page 114 | * https://bungie-net.github.io/multi/schema_Destiny-Definitions-DestinyActivityDefinition.html 115 | */ 116 | public ActivityMode getMode() throws APIException { 117 | checkJO(); 118 | for (ActivityMode am : ActivityMode.values()) { 119 | if (am.getBungieValue() == getModeNumber()) { 120 | return am; 121 | } 122 | } 123 | return null; 124 | } 125 | 126 | /** 127 | * Get all of the participants of this activity 128 | * 129 | * @return 130 | */ 131 | public List getParticipants() throws APIException { 132 | 133 | List temp = new ArrayList<>(); 134 | try { 135 | for (JsonElement je : getJsonObject().get("entries").getAsJsonArray()) { 136 | temp.add(new ActivityParticipant(je.getAsJsonObject())); 137 | } 138 | } catch (NullPointerException e) { 139 | System.out.println(getJO()); 140 | } 141 | 142 | return temp; 143 | } 144 | 145 | /** 146 | * If you need to get data not inside of this class 147 | */ 148 | public JsonObject getJsonObject() throws APIException { 149 | checkJO(); 150 | return getJO(); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/stats/activities/ActivityHistoryReview.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.stats.activities; 9 | 10 | import com.google.gson.JsonArray; 11 | import com.google.gson.JsonElement; 12 | import com.google.gson.JsonObject; 13 | import net.dec4234.javadestinyapi.exceptions.APIException; 14 | import net.dec4234.javadestinyapi.material.DestinyAPI; 15 | import net.dec4234.javadestinyapi.material.clan.Clan; 16 | import net.dec4234.javadestinyapi.material.clan.ClanMember; 17 | import net.dec4234.javadestinyapi.material.manifest.ManifestEntityTypes; 18 | import net.dec4234.javadestinyapi.material.user.BungieUser; 19 | import net.dec4234.javadestinyapi.material.user.DestinyCharacter; 20 | import net.dec4234.javadestinyapi.utils.HttpUtils; 21 | import net.dec4234.javadestinyapi.utils.StringUtils; 22 | 23 | import java.util.*; 24 | 25 | public class ActivityHistoryReview { 26 | 27 | private HttpUtils httpUtils = DestinyAPI.getHttpUtils(); 28 | 29 | public LinkedHashMap getMostUnrecentAttempts(Clan clan, ActivityIdentifier activityIdentifier) throws APIException { 30 | HashMap map = new HashMap<>(); 31 | HashMap doubleMap = new HashMap<>(); 32 | LinkedHashMap toReturn = new LinkedHashMap<>(); 33 | 34 | for(BungieUser bungieUser : clan.getMembers()) { 35 | for(DestinyCharacter destinyCharacter : bungieUser.getCharacters()) { 36 | for (int i = 0; i < Integer.MAX_VALUE; i++) { 37 | JsonObject jo = httpUtils.urlRequestGET("https://www.bungie.net/Platform/Destiny2/" + bungieUser.getMembershipType() + "/Account/" + bungieUser.getID() + "/Character/" + destinyCharacter.getCharacterID() + "/Stats/Activities/?page=" + i + "&count=250&mode=" + activityIdentifier.getMode().getBungieValue()); 38 | 39 | if (jo == null || !jo.has("Response") || !jo.getAsJsonObject("Response").has("activities")) { 40 | break; 41 | } 42 | 43 | JsonArray ja = jo.getAsJsonObject("Response").getAsJsonArray("activities"); 44 | JsonObject jo2 = ja.get(0).getAsJsonObject(); 45 | Date date = StringUtils.valueOfZTime(jo2.get("period").getAsString()); 46 | 47 | if (map.containsKey(bungieUser.getID())) { 48 | if (StringUtils.getDaysSinceTime(date) < StringUtils.getDaysSinceTime(map.get(bungieUser.getID()).getDatePlayed())) { 49 | map.remove(bungieUser.getID()); 50 | map.put(bungieUser.getID(), new Activity(jo2.getAsJsonObject("activityDetails").get("instanceId").getAsString(), date)); 51 | } 52 | } else { 53 | map.put(bungieUser.getID(), new Activity(jo2.getAsJsonObject("activityDetails").get("instanceId").getAsString(), date)); 54 | } 55 | } 56 | } 57 | 58 | if(map.containsKey(bungieUser.getID())) { 59 | doubleMap.put(bungieUser.getID(), StringUtils.getDaysSinceTime(map.get(bungieUser.getID()).getDatePlayed())); 60 | } 61 | } 62 | 63 | List> list = new LinkedList<>(doubleMap.entrySet()); 64 | 65 | Collections.sort(list, new Comparator<>() { 66 | public int compare(Map.Entry o1, 67 | Map.Entry o2) { 68 | return (o2.getValue()).compareTo(o1.getValue()); 69 | } 70 | }); 71 | 72 | for(Map.Entry map2 : list) { 73 | toReturn.put(map2.getKey(), map.get(map2.getKey())); 74 | } 75 | 76 | return toReturn; 77 | } 78 | 79 | /** 80 | * Takes a very long time 81 | */ 82 | public double getAverageCompletions(Clan clan, ActivityIdentifier activityIdentifier) throws APIException { 83 | List members = clan.getMembers(); 84 | double count = 0; 85 | 86 | for (BungieUser bungieUser : members) { 87 | count += getCompletions(bungieUser, activityIdentifier); 88 | } 89 | 90 | return count / members.size(); 91 | } 92 | 93 | public LinkedHashMap getTopClearers(Clan clan, ActivityIdentifier activityIdentifier) throws APIException { 94 | HashMap map = new HashMap<>(); 95 | LinkedHashMap toReturn = new LinkedHashMap<>(); 96 | 97 | for(BungieUser bungieUser : clan.getMembers()) { 98 | map.put(bungieUser, getCompletions(bungieUser, activityIdentifier)); 99 | } 100 | 101 | List> list = new LinkedList<>(map.entrySet()); 102 | 103 | Collections.sort(list, new Comparator<>() { 104 | public int compare(Map.Entry o1, 105 | Map.Entry o2) { 106 | return (o2.getValue()).compareTo(o1.getValue()); 107 | } 108 | }); 109 | 110 | for(Map.Entry map2 : list) { 111 | toReturn.put(map2.getKey(), map2.getValue()); 112 | } 113 | 114 | return toReturn; 115 | } 116 | 117 | public int getCompletions(BungieUser bungieUser, ActivityIdentifier activityIdentifier) throws APIException { 118 | int count = 0; 119 | 120 | for (DestinyCharacter destinyCharacter : bungieUser.getCharacters()) { 121 | count += getCompletions(activityIdentifier, bungieUser, destinyCharacter); 122 | } 123 | 124 | return count; 125 | } 126 | 127 | public int getCompletions(ActivityIdentifier activityIdentifier, BungieUser bungieUser, DestinyCharacter destinyCharacter) throws APIException { 128 | int count = 0; 129 | 130 | for (JsonArray ja : getArrays(activityIdentifier, bungieUser, destinyCharacter)) { 131 | for (JsonElement je : ja) { 132 | JsonObject jo1 = je.getAsJsonObject(); 133 | for (String s : activityIdentifier.getHashes()) { 134 | if (jo1.getAsJsonObject("activityDetails").get("referenceId").getAsString().equals(s)) { 135 | if (jo1.getAsJsonObject("values").getAsJsonObject("completed").getAsJsonObject("basic").get("value").getAsDouble() == 1) { 136 | count++; 137 | } 138 | } 139 | } 140 | } 141 | } 142 | 143 | return count; 144 | } 145 | 146 | public boolean hasPlayedInActivity(BungieUser bungieUser, String pgcrId) throws APIException { 147 | Activity activity = new Activity(pgcrId); 148 | for (ActivityParticipant activityParticipant : activity.getParticipants()) { 149 | if (activityParticipant.getMembershipId().equals(bungieUser.getID())) { 150 | return true; 151 | } 152 | } 153 | 154 | return false; 155 | } 156 | 157 | /** 158 | * This is a utility function to assist in development 159 | *
160 | * This prints a list of hashes and the names to put inside {@link ActivityIdentifier} 161 | * @param activityIdentifier Finds all unknown hashes that share the same mode as this activity. Like Raid/Strike/etc. 162 | */ 163 | public void getUndiscoveredActivityHashes(ActivityIdentifier activityIdentifier) throws APIException { 164 | BungieUser bungieUser = new BungieUser("4611686018467284386"); // just use a tryhard player that does every activity (Datto) 165 | 166 | for (DestinyCharacter destinyCharacter : bungieUser.getCharacters()) { 167 | for (int i = 0; i < 250; i++) { 168 | JsonObject jo = httpUtils.urlRequestGET("https://www.bungie.net/Platform/Destiny2/" + bungieUser.getMembershipType() + "/Account/" + bungieUser.getID() + "/Character/" + destinyCharacter.getCharacterID() + "/Stats/Activities/?page=" + i + "&count=250&mode=" + activityIdentifier.getMode().getBungieValue()); 169 | 170 | if (!jo.getAsJsonObject("Response").has("activities")) { 171 | break; 172 | } 173 | 174 | JsonArray ja = jo.getAsJsonObject("Response").getAsJsonArray("activities"); 175 | for (JsonElement je : ja) { 176 | JsonObject jo1 = je.getAsJsonObject(); 177 | String hash = jo1.getAsJsonObject("activityDetails").get("referenceId").getAsString(); 178 | if (ActivityIdentifier.fromHash(hash) == null) { 179 | JsonObject jo2 = httpUtils.manifestGET(ManifestEntityTypes.ACTIVITY, hash); 180 | if (jo2.has("Response") && jo2.getAsJsonObject("Response").has("displayProperties") && jo2.getAsJsonObject("Response").getAsJsonObject("displayProperties").has("name")) { 181 | jo2 = jo2.getAsJsonObject("Response").getAsJsonObject("displayProperties"); 182 | System.out.println(jo2.get("name").getAsString() + " - " + hash); 183 | } 184 | } 185 | 186 | } 187 | } 188 | 189 | } 190 | } 191 | 192 | private JsonArray[] getArrays(ActivityIdentifier activityIdentifier, BungieUser bungieUser, DestinyCharacter destinyCharacter) throws APIException { 193 | List jsonArrays = new ArrayList<>(); 194 | 195 | for (int i = 0; i < Integer.MAX_VALUE; i++) { 196 | try { 197 | JsonObject jo = httpUtils.urlRequestGET("https://www.bungie.net/Platform/Destiny2/" + bungieUser.getMembershipType() + "/Account/" + bungieUser.getID() + "/Character/" + destinyCharacter.getCharacterID() + "/Stats/Activities/?page=" + i + "&count=250&mode=" + activityIdentifier.getMode().getBungieValue()); 198 | 199 | // 1665 = User has chosen for their data to be private :( 200 | if (jo.get("ErrorCode").getAsInt() == 1665 || !jo.getAsJsonObject("Response").has("activities")) { 201 | break; 202 | } 203 | 204 | JsonArray ja = jo.getAsJsonObject("Response").getAsJsonArray("activities"); 205 | jsonArrays.add(ja); 206 | } catch (NullPointerException e) { 207 | break; 208 | } 209 | } 210 | 211 | return jsonArrays.toArray(new JsonArray[0]); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/stats/activities/ActivityIdentifier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.stats.activities; 9 | 10 | /** 11 | * This a list of known activity hash identifiers mapped to their names, activity mode and short name. Some activities 12 | * such as raids are also mapped to an image in case you want it.
13 | * Note that each hash represents something different, like the different rotations or challenges of Leviathan. 14 | * I have not investigated this very deeply but that's what I suspect. You can probably use the manifest to find out 15 | * more information. 16 | */ 17 | public enum ActivityIdentifier { 18 | 19 | // STRIKES 20 | ARMS_DEALER("AD", ActivityMode.STRIKE, "442671778", "2080275457", "2378719026", "2724706103", "2378719025", "770196931", "3240321863", "1258914202", "1679518121"), 21 | LAKE_OF_SHADOWS("LOS", ActivityMode.STRIKE, "2318521576", "3711627564", "3725993747", "2630091891", "4134816102"), 22 | 23 | THE_DISGRACED("TD", ActivityMode.STRIKE, "1684420962", "174131855"), 24 | FALLEN_SABER("FS", ActivityMode.STRIKE, "3597990372", "3777220691"), 25 | DEVILS_LAIR("DL", ActivityMode.STRIKE, "969982762"), 26 | 27 | SAVATHUNS_SONG("SS", ActivityMode.STRIKE, "2359594803", "1101792305", "3191123858", "649648599", "1542611209"), 28 | 29 | INVERTED_SPIRE("IS", ActivityMode.STRIKE, "3704910925", "1563393783", "286562305", "1107473294", "1743518003", "338662534", "2753180142", "1743518000", "467266668"), 30 | EXODUS_CRASH("EC", ActivityMode.STRIKE, "2459768558", "1549614516", "4260306233", "1930116823", "2479262829", "1930116820", "2971335647"), 31 | INSIGHT_TERMINUS("IT", ActivityMode.STRIKE, "3751421841", "291911094", "3735153516", "3735153519"), 32 | PROVING_GROUND("PG", ActivityMode.STRIKE, "546528643", "1754609040"), 33 | 34 | THE_PYRAMIDION("P", ActivityMode.STRIKE, "1035135049", "1603374112", "1332567112", "2704613535", "1332567115", "981383202", "2799837309", "4261351281"), 35 | FESTERING_CORE("FC", ActivityMode.STRIKE, "1035850837", "3596828104"), 36 | 37 | TREE_OF_PROBABILITIES("TOP", ActivityMode.STRIKE, "2678510381", "1263901594", "561345572", "561345575", "840678113", "4085493024", "2684121894"), 38 | A_GARDEN_OF_WORLD("AGOW", ActivityMode.STRIKE, "656703508", "3676029623", "2230236215", "2230236212", "689927878", "117447065", "2579344189", "743963294"), 39 | 40 | STRANGE_TERRAIN("ST", ActivityMode.STRIKE, "2992505404", "861639649", "3801775390", "2248296964", "861639650"), 41 | WILL_OF_THE_THOUSANDS("WOTT", ActivityMode.STRIKE, "1198216109", "3944547192", "3510043585", "1317492847", "1891220709", "3944547195"), 42 | 43 | WARDEN_OF_NOTHING("WON", ActivityMode.STRIKE, "1360385764", "1360385767", "1134446996", "1493405720"), 44 | THE_HOLLOWED_LAIR("THL", ActivityMode.STRIKE, "663301842", "1475539136", "1475539139", "955874134"), 45 | BROODHOLD("BRO", ActivityMode.STRIKE, "1666283939", "3813623455"), 46 | 47 | THE_CORRUPTED("TC", ActivityMode.STRIKE, "3374205762", "723056533", "224295651"), 48 | 49 | THE_SCARLET_KEEP("TSK", ActivityMode.STRIKE, "1775791936", "3879143309", "3643233460", "2047723007", "346345236"), 50 | 51 | THE_GLASSWAY("TG", ActivityMode.STRIKE, "2226120409", "3965479856", "3329390423"), 52 | 53 | QUEST_EXODUS_CRASH("QEC", ActivityMode.STRIKE, "940394831"), 54 | 55 | // NIGHTFALLS 56 | ORDEAL_ADEPT("OA", ActivityMode.ALL_STRIKES, "887176540", "3265488360", "1203950596", "1753547897"), 57 | ORDEAL_HERO("OH", ActivityMode.ALL_STRIKES, "3849697859", "887176543", "3265488363", "2136458567", "265186830", "1203950599", "3293630131", "1358381371"), 58 | ORDEAL_LEGEND("OL", ActivityMode.ALL_STRIKES, "3849697858", "1302909045", "2136458566", "265186831", "2599001913", "1495545954", "3233498448"), 59 | ORDEAL_MASTER("OM", ActivityMode.ALL_STRIKES, "3233498455", "2136458561", "2136458567"), 60 | ORDEAL_GRANDMASTER("OGM", ActivityMode.ALL_STRIKES, "2136458560", "1753547901", "265186825"), 61 | 62 | ARMS_DEALER_NF("ADNF", ActivityMode.NIGHTFALL, "145302664", "3145298904"), 63 | ARMS_DEALER_NF_PRESTIGE("ADNFP", ActivityMode.ALL_STRIKES, "601540706"), 64 | QUEST_ARMS_DEALER_PRESTIGE("QADP", ActivityMode.HEROIC_NIGHTFALL, "1207505828"), 65 | LAKE_OF_SHADOWS_NF("LOSNF", ActivityMode.ALL_STRIKES, "3372160277"), 66 | 67 | SAVATHUNS_SONG_NF("SSNF", ActivityMode.NIGHTFALL, "1975064760", "3280234344"), 68 | SAVATHUNS_SONG_NF_PRESTIGE("SSNFP", ActivityMode.ALL_STRIKES, "585071442"), 69 | 70 | INVERTED_SPIRE_NF("ISNF", ActivityMode.NIGHTFALL, "3368226533", "4259769141"), 71 | INVERTED_SPIRE_NF_PRESTIGE("ISNFP", ActivityMode.NIGHTFALL, "3050465729"), 72 | EXODUS_CRASH_NF("ECNF", ActivityMode.ALL_STRIKES, "1282886582"), 73 | INSIGHT_TERMINUS_NF("ITNF", ActivityMode.ALL_STRIKES, "1034003646"), 74 | 75 | THE_PYRAMIDION_NF("TPNF", ActivityMode.NIGHTFALL, "926940962", "3289589202"), 76 | THE_PYRAMIDION_NF_PRESTIGE("TPNFP", ActivityMode.HEROIC_NIGHTFALL, "1129066976"), 77 | UNIDENTIFIED_FRAME_THE_PYRAMIDION("UFTP", ActivityMode.HEROIC_NIGHTFALL, "1431348899"), 78 | 79 | TREE_OF_PROBABILITIES_NF("TOPNF", ActivityMode.NIGHTFALL, "2046332536"), 80 | TREE_OF_PROBABILITIES_NF_PRESTIGE("TOPNFP", ActivityMode.NIGHTFALL, "2416546450"), 81 | A_GARDEN_WORLD_NF("AGWNF", ActivityMode.ALL_STRIKES, "2322829199", "936308438"), 82 | A_GARDEN_WORLD_NF_PRESTIGE("AGWP", ActivityMode.ALL_STRIKES, "2688061647"), 83 | 84 | STRANGE_TERRAIN_NF("STNF", ActivityMode.ALL_STRIKES, "2179568029", "4279557030", "522318687"), 85 | STRANGE_TERRAIN_NF_PRESTIGE("STNFP", ActivityMode.ALL_STRIKES, "1794007817"), 86 | WILL_OF_THE_THOUSANDS_NF("WOTT", ActivityMode.NIGHTFALL, "3907468134", "272852450"), 87 | WILL_OF_THE_THOUSANDS_NF_PRESTIGE("WOTTP", ActivityMode.NIGHTFALL, "2383858990"), 88 | 89 | THE_HOLLOWED_LAIR_NF("THLNF", ActivityMode.ALL_STRIKES, "3701132453"), 90 | WARDEN_OF_NOTHING_NF("WONNF", ActivityMode.ALL_STRIKES, "3108813009"), 91 | 92 | THE_CORRUPTED_NF("TCNF", ActivityMode.NIGHTFALL, "3388474648", "3034843176"), 93 | 94 | // LOST SECTORS 95 | BUNKER_E15_LEGEND("BE15L", ActivityMode.ALL_STRIKES, "1648125541"), 96 | BUNKER_E15_MASTER("BE15M", ActivityMode.ALL_STRIKES, "1648125538"), 97 | 98 | K1_CREW_QUARTERS_LEGEND("K1CQL", ActivityMode.ALL_STRIKES, "184186581"), 99 | 100 | CONCEALED_VOID_LEGEND("CVL", ActivityMode.ALL_STRIKES, "912873277"), 101 | 102 | // SEASONAL CRAP 103 | THE_COIL_MATCHMADE("COIL", ActivityMode.NIGHTFALL, "443581538"), 104 | THE_COIL_PRIVATE("COIL", ActivityMode.NIGHTFALL, "4233082192"), 105 | 106 | // EVENTS 107 | GUARDIAN_GAMES_COMPETITIVE_NIGHTFALL("GGCNF", ActivityMode.NIGHTFALL, "717738476", "1207998557", "1664895903", "1823573320", "1298659439", "2038344197", "3787810787", "4189303825"), 108 | GUARDIAN_GAMES_TRAINING_PLAYLIST("GGCNF", ActivityMode.NIGHTFALL, "14936429", "1413951275", "1705059120", "2550911054", "552852420", "2758636626"), 109 | 110 | // RAIDS 111 | LEVIATHAN("LEV", "https://www.bungie.net/img/destiny_content/pgcr/raid_gluttony.jpg", ActivityMode.RAID, "2693136600", "2693136601", "2693136602", "2693136603", "2693136604", "2693136605"), 112 | LEVIATHAN_PRESTIGE("LEVP", "https://www.bungie.net/img/destiny_content/pgcr/raid_gluttony.jpg", ActivityMode.RAID, "1685065161", "3446541099", "3879860661", "417231112", "2449714930", "757116822"), 113 | EATER_OF_WORLDS("EOW", "https://www.bungie.net/img/destiny_content/pgcr/raids_leviathan_eater_of_worlds.jpg", ActivityMode.RAID, "3089205900"), 114 | EATER_OF_WORLDS_PRESTIGE("EOWP", "https://www.bungie.net/img/destiny_content/pgcr/raids_leviathan_eater_of_worlds.jpg", ActivityMode.RAID, "809170886"), 115 | SPIRE_OF_STARS("SOS", "https://www.bungie.net/img/destiny_content/pgcr/raid_greed.jpg", ActivityMode.RAID, "119944200"), 116 | SPIRE_OF_STARS_PRESTIGE("SOSP", "https://www.bungie.net/img/destiny_content/pgcr/raid_greed.jpg", ActivityMode.RAID, "3213556450"), 117 | 118 | LAST_WISH("LW", "https://www.bungie.net/img/destiny_content/pgcr/raid_beanstalk.jpg", ActivityMode.RAID, "2122313384"), 119 | SCOURGE_OF_THE_PAST("SOTP", "https://www.bungie.net/img/destiny_content/pgcr/raids.1305rh0093145r13t5hn10tnz.raid_sunset.jpg", ActivityMode.RAID, "548750096"), 120 | CROWN_OF_SORROW("COS", "https://www.bungie.net/img/destiny_content/pgcr/raid_eclipse.jpg", ActivityMode.RAID, "3333172150"), 121 | 122 | GARDEN_OF_SALVATION("GOS", "https://www.bungie.net/img/destiny_content/pgcr/raid_garden_of_salvation.jpg", ActivityMode.RAID, "2659723068", "3458480158"), 123 | 124 | DEEP_STONE_CRYPT("DSC", "https://www.bungie.net/img/destiny_content/pgcr/europa-raid-deep-stone-crypt.jpg", ActivityMode.RAID, "910380154"), 125 | 126 | VAULT_OF_GLASS("VOG", "https://www.bungie.net/pubassets/pkgs/150/150569/FrontpageBanner_1920x590.jpg?cv=3983621215&av=1926358162", ActivityMode.RAID, "3881495763"), 127 | VAULT_OF_GLASS_MASTER("VOGM", "https://www.bungie.net/pubassets/pkgs/150/150569/FrontpageBanner_1920x590.jpg?cv=3983621215&av=1926358162", ActivityMode.RAID, "1681562271", "3022541210"), 128 | VAULT_OF_GLASS_CHALLENGE_MODE("VOGCM", "https://www.bungie.net/pubassets/pkgs/150/150569/FrontpageBanner_1920x590.jpg?cv=3983621215&av=1926358162", ActivityMode.RAID, "1485585878"), 129 | 130 | VOW_OF_THE_DISCIPLE("VOTD", "https://www.bungie.net/pubassets/pkgs/157/157111/FrontPageBanner_1920x590.jpg", ActivityMode.RAID, "1441982566"), 131 | VOW_OF_THE_DISCIPLE_MASTER("VOTDM", "https://www.bungie.net/pubassets/pkgs/157/157111/FrontPageBanner_1920x590.jpg", ActivityMode.RAID, "4217492330"), 132 | 133 | KINGS_FALL("KF", "https://www.bungie.net/pubassets/pkgs/170/170501/ArticleBanner_997x500.jpg", ActivityMode.RAID, "1374392663"), 134 | KINGS_FALL_MASTER("KFM", "https://www.bungie.net/pubassets/pkgs/170/170501/ArticleBanner_997x500.jpg", ActivityMode.RAID, "2964135793", "3257594522"), 135 | KINGS_FALL_LEGEND("KFL", "https://www.bungie.net/pubassets/pkgs/170/170501/ArticleBanner_997x500.jpg", ActivityMode.RAID, "1063970578"), 136 | 137 | ROOT_OF_NIGHTMARES("RON", "https://images.contentstack.io/v3/assets/blte410e3b15535c144/blt745cb94aef1d49fd/640a30abe16bc77f58332cc4/FrontPageBanner_PC_1920x590.jpg", ActivityMode.RAID, "2381413764"), 138 | ROOT_OF_NIGHTMARES_MASTER("RONM", "https://images.contentstack.io/v3/assets/blte410e3b15535c144/blt745cb94aef1d49fd/640a30abe16bc77f58332cc4/FrontPageBanner_PC_1920x590.jpg", ActivityMode.RAID, "2918919505"), 139 | 140 | CROTAS_END("CE", "https://images.contentstack.io/v3/assets/blte410e3b15535c144/blt8023dab456754205/64f0c82d8c8fe96dfe98bbf8/16x9Standard_1920x1080.jpg", ActivityMode.RAID, "4179289725"), 141 | CROTAS_END_MASTER("CEM", "https://images.contentstack.io/v3/assets/blte410e3b15535c144/blt8023dab456754205/64f0c82d8c8fe96dfe98bbf8/16x9Standard_1920x1080.jpg", ActivityMode.RAID, "1507509200"), 142 | CROTAS_END_LEGEND("CEL", "https://images.contentstack.io/v3/assets/blte410e3b15535c144/blt8023dab456754205/64f0c82d8c8fe96dfe98bbf8/16x9Standard_1920x1080.jpg", ActivityMode.RAID, "156253568"), 143 | 144 | PANTHEON_ATRAKS("PANA", "https://images.contentstack.io/v3/assets/blte410e3b15535c144/blt5394fcdccef3f5ac/66290d35a9b0ab6193b925f0/thisweekatbungie-7_28_22.png", ActivityMode.RAID, "4169648179"), 145 | PANTHEON_ORYX("PANO", "https://images.contentstack.io/v3/assets/blte410e3b15535c144/blt5394fcdccef3f5ac/66290d35a9b0ab6193b925f0/thisweekatbungie-7_28_22.png", ActivityMode.RAID, ""), 146 | PANTHEON_RHULK("PANR", "https://images.contentstack.io/v3/assets/blte410e3b15535c144/blt5394fcdccef3f5ac/66290d35a9b0ab6193b925f0/thisweekatbungie-7_28_22.png", ActivityMode.RAID, ""), 147 | PANTHEON_NEZAREC("PANN", "https://images.contentstack.io/v3/assets/blte410e3b15535c144/blt5394fcdccef3f5ac/66290d35a9b0ab6193b925f0/thisweekatbungie-7_28_22.png", ActivityMode.RAID, ""); 148 | 149 | private String identifier; 150 | private String[] hashes; 151 | private ActivityMode mode; 152 | private String pgcrImage; 153 | 154 | ActivityIdentifier(String identifier, ActivityMode mode, String... hashes) { 155 | this.identifier = identifier; 156 | this.hashes = hashes; 157 | this.mode = mode; 158 | } 159 | 160 | ActivityIdentifier(String identifier, String pgcrImage, ActivityMode mode, String... hashes) { 161 | this.identifier = identifier; 162 | this.pgcrImage = pgcrImage; 163 | this.hashes = hashes; 164 | this.mode = mode; 165 | } 166 | 167 | public String getIdentifier() { 168 | return identifier; 169 | } 170 | 171 | public String getPgcrImage() { return pgcrImage; } 172 | 173 | /** 174 | * Get the directorActivityHash of the Activity 175 | * Can be used to compare different activities 176 | */ 177 | public String[] getHashes() { 178 | return hashes; 179 | } 180 | 181 | public ActivityMode getMode() { return mode; } 182 | 183 | public static ActivityIdentifier fromShorthand(String shortHand) { 184 | for(ActivityIdentifier activityIdentifier : ActivityIdentifier.values()) { 185 | if(activityIdentifier.getIdentifier().equals(shortHand)) { 186 | return activityIdentifier; 187 | } 188 | } 189 | 190 | return null; 191 | } 192 | 193 | public static ActivityIdentifier fromHash(String hash) { 194 | for(ActivityIdentifier activityIdentifier : ActivityIdentifier.values()) { 195 | for(String s : activityIdentifier.getHashes()) { 196 | if(s.equals(hash)) { 197 | return activityIdentifier; 198 | } 199 | } 200 | } 201 | 202 | return null; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/stats/activities/ActivityInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.stats.activities; 9 | 10 | import com.google.gson.JsonObject; 11 | import net.dec4234.javadestinyapi.exceptions.APIException; 12 | import net.dec4234.javadestinyapi.material.manifest.ManifestEntityTypes; 13 | import net.dec4234.javadestinyapi.utils.framework.ContentFramework; 14 | 15 | public class ActivityInfo extends ContentFramework { 16 | 17 | private String name, hash, completionUnlockHash, destinationHash, placeHash, activityTypeHash, activityModeHash, description, icon, pgcrImage, releaseIcon; 18 | private int modeType, activityModeCategory, releaseTime, activityLightLevel, tier, minParty, maxParty, maxPlayers; 19 | private ActivityMode activityMode; 20 | private boolean hasIcon, isPlaylist, inheritFromFreeRoam, suppressOtherRewards, isMatchmade, requiresGuardianOath, isPvP; 21 | 22 | public ActivityInfo(String hash) throws APIException { 23 | super(ManifestEntityTypes.ACTIVITY, hash, source -> { 24 | return source.getAsJsonObject("Response"); 25 | }); 26 | 27 | // Intialize values of the Activity Mode 28 | this.name = getDisplayProperties().get("name").getAsString(); 29 | this.description = getDisplayProperties().get("description").getAsString(); 30 | 31 | this.hasIcon = getDisplayProperties().get("hasIcon").getAsBoolean(); 32 | 33 | if(hasIcon) { 34 | icon = getDisplayProperties().get("icon").getAsString(); 35 | } 36 | 37 | // this.pgcrImage = getJO().get("pgcrImage").getAsString(); 38 | this.releaseIcon = getJO().get("releaseIcon").getAsString(); 39 | 40 | // Hashes 41 | this.hash = hash; 42 | this.completionUnlockHash = getJO().get("completionUnlockHash").getAsString(); 43 | this.destinationHash = getJO().get("destinationHash").getAsString(); 44 | this.placeHash = getJO().get("placeHash").getAsString(); 45 | this.activityTypeHash = getJO().get("activityTypeHash").getAsString(); 46 | // this.activityModeHash = getJO().get("activityModeHash").getAsString(); 47 | 48 | // Numbers 49 | if(getJO().has("directActivityModeType")) { 50 | this.modeType = getJO().get("directActivityModeType").getAsInt(); 51 | this.activityMode = ActivityMode.fromBungieValue(modeType); 52 | } 53 | 54 | if(getJO().has("activityModeCategory")) { 55 | this.activityModeCategory = getJO().get("activityModeCategory").getAsInt(); 56 | } 57 | this.releaseTime = getJO().get("releaseTime").getAsInt(); 58 | this.tier = getJO().get("tier").getAsInt(); 59 | this.activityLightLevel = getJO().get("activityLightLevel").getAsInt(); 60 | 61 | // Random booleans 62 | this.isPlaylist = getJO().get("isPlaylist").getAsBoolean(); 63 | this.inheritFromFreeRoam = getJO().get("inheritFromFreeRoam").getAsBoolean(); 64 | this.suppressOtherRewards = getJO().get("suppressOtherRewards").getAsBoolean(); 65 | this.isPvP = getJO().get("isPvP").getAsBoolean(); 66 | 67 | // Matchmaking 68 | this.isMatchmade = getMatchmaking().get("isMatchmade").getAsBoolean(); 69 | this.minParty = getMatchmaking().get("minParty").getAsInt(); 70 | this.maxParty = getMatchmaking().get("maxParty").getAsInt(); 71 | this.maxPlayers = getMatchmaking().get("maxPlayers").getAsInt(); 72 | this.requiresGuardianOath = getMatchmaking().get("requiresGuardianOath").getAsBoolean(); 73 | } 74 | 75 | public JsonObject getDisplayProperties() throws APIException { 76 | return getJO().getAsJsonObject("displayProperties"); 77 | } 78 | 79 | public JsonObject getMatchmaking() throws APIException { 80 | return getJO().getAsJsonObject("matchmaking"); 81 | } 82 | 83 | public String getName() { 84 | return name; 85 | } 86 | 87 | public String getHash() { 88 | return hash; 89 | } 90 | 91 | public String getCompletionUnlockHash() { 92 | return completionUnlockHash; 93 | } 94 | 95 | public String getDestinationHash() { 96 | return destinationHash; 97 | } 98 | 99 | public String getPlaceHash() { 100 | return placeHash; 101 | } 102 | 103 | public String getActivityTypeHash() { 104 | return activityTypeHash; 105 | } 106 | 107 | public String getActivityModeHash() { 108 | return activityModeHash; 109 | } 110 | 111 | public String getDescription() { 112 | return description; 113 | } 114 | 115 | public String getIcon() { 116 | return icon; 117 | } 118 | 119 | public String getPgcrImage() { 120 | return pgcrImage; 121 | } 122 | 123 | public String getReleaseIcon() { 124 | return releaseIcon; 125 | } 126 | 127 | public int getModeType() { 128 | return modeType; 129 | } 130 | 131 | public int getActivityModeCategory() { 132 | return activityModeCategory; 133 | } 134 | 135 | public int getReleaseTime() { 136 | return releaseTime; 137 | } 138 | 139 | public int getActivityLightLevel() { 140 | return activityLightLevel; 141 | } 142 | 143 | public int getTier() { 144 | return tier; 145 | } 146 | 147 | public int getMinParty() { 148 | return minParty; 149 | } 150 | 151 | public int getMaxParty() { 152 | return maxParty; 153 | } 154 | 155 | public int getMaxPlayers() { 156 | return maxPlayers; 157 | } 158 | 159 | public ActivityMode getActivityMode() { 160 | return activityMode; 161 | } 162 | 163 | public boolean isHasIcon() { 164 | return hasIcon; 165 | } 166 | 167 | public boolean isPlaylist() { 168 | return isPlaylist; 169 | } 170 | 171 | public boolean isInheritFromFreeRoam() { 172 | return inheritFromFreeRoam; 173 | } 174 | 175 | public boolean isSuppressOtherRewards() { 176 | return suppressOtherRewards; 177 | } 178 | 179 | public boolean isMatchmade() { 180 | return isMatchmade; 181 | } 182 | 183 | public boolean isRequiresGuardianOath() { 184 | return requiresGuardianOath; 185 | } 186 | 187 | public boolean isPvP() { 188 | return isPvP; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/stats/activities/ActivityMode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.stats.activities; 9 | 10 | /** 11 | * A list of activity mode numbers according to bungie.net 12 | * Some numbers are randomly skipped or "reserved", thus they are not listed here 13 | * https://bungie-net.github.io/multi/schema_Destiny-Definitions-DestinyActivityDefinition.html 14 | * 15 | * https://bungie-net.github.io/multi/schema_Destiny-HistoricalStats-Definitions-DestinyActivityModeType.html#schema_Destiny-HistoricalStats-Definitions-DestinyActivityModeType 16 | */ 17 | public enum ActivityMode { 18 | 19 | NONE(0), 20 | STORY(2), 21 | STRIKE(3), 22 | RAID(4), 23 | ALL_PVP(5), 24 | PATROL(6), 25 | ALL_PVE(7), 26 | CONTROL(10), 27 | CLASH(12), 28 | CRIMSON_DOUBLES(15), 29 | NIGHTFALL(16), 30 | HEROIC_NIGHTFALL(17), 31 | ALL_STRIKES(18), 32 | IRON_BANNER(19), 33 | ALL_MAYHEM(25), 34 | SUPREMACY(31), 35 | PRIVATE_MATCHES_ALL(32), 36 | SURVIVAL(37), 37 | COUNTDOWN(38), 38 | TRIALS_OF_THE_NINE(39), 39 | SOCIAL(40), 40 | TRIALS_COUNTDOWN(41), 41 | TRIALS_SURVIVAL(42), 42 | IBCONTROL(43), 43 | IBCLASH(44), 44 | IBSUPREMACY(45), 45 | SCOREDNF(46), 46 | SCOREDHEROICNF(47), 47 | RUMBLE(48), 48 | ALL_DOUBLES(49), 49 | DOUBLES(50), 50 | PM_CLASH(51), 51 | PM_CONTROL(52), 52 | PM_SUPREMACY(53), 53 | PM_COUNTDOWN(54), 54 | PM_SURVIVAL(55), 55 | PM_MAYHEM(56), 56 | PM_RUMBLE(57), 57 | HEROIC_ADVENTURE(58), 58 | SHOWDOWN(59), 59 | LOCKDOWN(60), 60 | SCORCHED(61), 61 | SCORCHED_TEAM(62), 62 | GAMBIT(63), 63 | ALL_PVE_COMP(64), 64 | BREAKTHROUGH(65), 65 | BLACK_ARMORY_RUN(66), 66 | SALVAGE(67), 67 | IRON_BANNER_SALVAGE(68), 68 | PVP_COMP(69), 69 | PVP_QUICKPLAY(70), 70 | CLASH_QUICKPLAY(71), 71 | CLASH_COMP(72), 72 | CONTROL_QUICKPLAY(73), 73 | CONTROL_COMP(74), 74 | GAMBIT_PRIME(75), 75 | RECKONING(76), 76 | MENAGERIE(77), 77 | VEX_OFFENSIVE(78), 78 | NIGHTMARE_HUNT(79), 79 | ELIMINATION(80), 80 | MOMENTUM(81), 81 | DUNGEON(82), 82 | SUNDIAL(83), 83 | TRIALS_OF_OSIRIS(84), 84 | DARES(85), 85 | OFFENSIVE(86); 86 | 87 | 88 | private int bungieValue; 89 | 90 | private ActivityMode(int bungieValue) { 91 | this.bungieValue = bungieValue; 92 | } 93 | 94 | public int getBungieValue() { 95 | return bungieValue; 96 | } 97 | 98 | public static ActivityMode fromBungieValue(int bungieValue) { 99 | for(ActivityMode activityMode : ActivityMode.values()) { 100 | if(activityMode.getBungieValue() == bungieValue) { 101 | return activityMode; 102 | } 103 | } 104 | 105 | return null; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/stats/activities/ActivityParticipant.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.stats.activities; 9 | 10 | import com.google.gson.JsonObject; 11 | import net.dec4234.javadestinyapi.material.user.BungieUser; 12 | 13 | public class ActivityParticipant { 14 | 15 | private JsonObject jo; 16 | private BungieUser bungieUser; 17 | private String membershipId, characterId; 18 | private int score, assists, deaths, kills, opponentsDefeated, activityDuration, completionReason; 19 | private double kdr, kda, completed, efficiency, startSeconds, timePlayed; 20 | JsonObject values; 21 | 22 | public ActivityParticipant(JsonObject entry) { 23 | jo = entry; 24 | } 25 | 26 | private void assignValues() { 27 | 28 | } 29 | 30 | /** 31 | * Get the membership ID of this user 32 | */ 33 | public String getMembershipId() { 34 | if (membershipId != null) return membershipId; 35 | membershipId = jo.getAsJsonObject("player").getAsJsonObject("destinyUserInfo").get("membershipId").getAsString(); 36 | return membershipId; 37 | } 38 | 39 | /** 40 | * Gets the bungie user of this 41 | = */ 42 | public BungieUser getBungieUser() { 43 | if (bungieUser != null) return bungieUser; 44 | bungieUser = new BungieUser(getMembershipId()); 45 | return bungieUser; 46 | } 47 | 48 | /** 49 | * Get the character ID of the player 50 | = */ 51 | public String getCharacterId() { 52 | if (characterId != null) return characterId; 53 | characterId = jo.get("characterId").getAsString(); 54 | return characterId; 55 | } 56 | 57 | /** 58 | * Gets the score of the user in this activity 59 | */ 60 | public int getScore() { 61 | if (score != 0) return score; 62 | score = jo.getAsJsonObject("score").getAsJsonObject("basic").get("value").getAsInt(); 63 | return score; 64 | } 65 | 66 | /** 67 | * Get the assists of the user 68 | - */ 69 | public int getAssists() { 70 | if (assists != 0) return assists; 71 | assists = jo.getAsJsonObject("assists").getAsJsonObject("basic").get("value").getAsInt(); 72 | return assists; 73 | } 74 | 75 | /** 76 | * Get if this user completed the activity 77 | - */ 78 | public double getCompleted() { 79 | if(completed != 0) return completed; 80 | completed = jo.getAsJsonObject("completed").getAsJsonObject("basic").get("value").getAsDouble(); 81 | return completed; 82 | } 83 | 84 | /** 85 | * Get the number of deaths for this user 86 | */ 87 | public int getDeaths() { 88 | if(deaths != 0) return deaths; 89 | deaths = jo.getAsJsonObject("deaths").getAsJsonObject("basic").get("value").getAsInt(); 90 | return deaths; 91 | } 92 | 93 | /** 94 | * Get the number of kills for this user 95 | */ 96 | public int getKills() { 97 | if(kills != 0) return kills; 98 | kills = jo.getAsJsonObject("kills").getAsJsonObject("basic").get("value").getAsInt(); 99 | return kills; 100 | } 101 | 102 | /** 103 | * Gets the number of opponents this user defeated 104 | - */ 105 | public int getOpponentsDefeated() { 106 | if(opponentsDefeated != 0) return opponentsDefeated; 107 | opponentsDefeated = jo.getAsJsonObject("opponentsDefeated").getAsJsonObject("basic").get("value").getAsInt(); 108 | return opponentsDefeated; 109 | } 110 | 111 | /** 112 | * Get the efficiency of this user 113 | */ 114 | public double getEfficiency() { 115 | if(efficiency != 0) return efficiency; 116 | efficiency = jo.getAsJsonObject("efficiency").getAsJsonObject("basic").get("value").getAsDouble(); 117 | return efficiency; 118 | } 119 | 120 | /** 121 | * Gets the Kill-Death Ratio of this user 122 | */ 123 | public double getKdr() { 124 | if(kdr != 0) return kdr; 125 | kdr = jo.getAsJsonObject("killsDeathsRatio").getAsJsonObject("basic").get("value").getAsDouble(); 126 | return kdr; 127 | } 128 | 129 | /** 130 | * Gets the Kill-Deaths-Assists of this user 131 | */ 132 | public double getKda() { 133 | if(kda != 0) return kda; 134 | kda = jo.getAsJsonObject("killsDeathsAssists").getAsJsonObject("basic").get("value").getAsDouble(); 135 | return kda; 136 | } 137 | 138 | /** 139 | * Gets the duration, in seconds, of this activity 140 | */ 141 | public int getActivityDuration() { 142 | if(activityDuration != 0) return activityDuration; 143 | activityDuration = jo.getAsJsonObject("activityDurationSeconds").getAsJsonObject("basic").get("value").getAsInt(); 144 | return activityDuration; 145 | } 146 | 147 | /** 148 | * Gets the reason why this activity was completed 149 | */ 150 | public int getCompletionReason() { 151 | if(completionReason != 0) return completionReason; 152 | completionReason = jo.getAsJsonObject("completionReason").getAsJsonObject("basic").get("value").getAsInt(); 153 | return completionReason; 154 | } 155 | 156 | /** 157 | * Gets the time the user joined the activity? 158 | */ 159 | public double getStartSeconds() { 160 | if(startSeconds != 0) return startSeconds; 161 | startSeconds = jo.getAsJsonObject("startSeconds").getAsJsonObject("basic").get("value").getAsDouble(); 162 | return startSeconds; 163 | } 164 | 165 | /** 166 | * Gets how long this user played in this activity 167 | */ 168 | public double getTimePlayed() { 169 | if(timePlayed != 0) return timePlayed; 170 | timePlayed = jo.getAsJsonObject("timePlayedSeconds").getAsJsonObject("basic").get("value").getAsDouble(); 171 | return timePlayed; 172 | } 173 | 174 | public JsonObject getJO() { 175 | return jo; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/stats/character/UserStats.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.stats.character; 9 | 10 | import com.google.gson.JsonObject; 11 | import net.dec4234.javadestinyapi.exceptions.APIException; 12 | import net.dec4234.javadestinyapi.material.DestinyAPI; 13 | import net.dec4234.javadestinyapi.material.user.BungieUser; 14 | import net.dec4234.javadestinyapi.material.user.DestinyCharacter; 15 | import net.dec4234.javadestinyapi.utils.HttpUtils; 16 | 17 | import java.text.DecimalFormat; 18 | 19 | public class UserStats { 20 | 21 | HttpUtils hu = DestinyAPI.getHttpUtils(); 22 | BungieUser bungieUser; 23 | DestinyCharacter destinyCharacter; 24 | JsonObject jo; 25 | JsonObject allPve; 26 | DecimalFormat df = new DecimalFormat("##.00"); 27 | 28 | private int activitiesCleared, activitiesEntered, assists, totalKillDistance, kills, deaths; 29 | private double pgaAssists, pgaKills, averageKillDistance, pgaSecondsPlayed, pgaDeaths; 30 | private long secondsPlayed; 31 | 32 | /** 33 | * Gets stats for this user's entire account 34 | */ 35 | public UserStats(BungieUser bungieUser) throws APIException { 36 | this.bungieUser = bungieUser; 37 | jo = hu.urlRequestGET("https://www.bungie.net/Platform/Destiny2/" + bungieUser.getMembershipType() + "/Account/" + bungieUser.getID() + "/Stats/").getAsJsonObject("Response").getAsJsonObject("mergedAllCharacters").getAsJsonObject("results"); 38 | allPve = jo.getAsJsonObject("allPvE").getAsJsonObject("allTime"); 39 | } 40 | 41 | /** 42 | * Gets stats for this user's specific character 43 | */ 44 | public UserStats(BungieUser bungieUser, DestinyCharacter destinyCharacter) throws APIException { 45 | this.bungieUser = bungieUser; 46 | jo = hu.urlRequestGET("https://www.bungie.net/Platform/Destiny2/" + bungieUser.getMembershipType() + "/Account/" + bungieUser.getID() + "/Character/" + destinyCharacter.getCharacterID() + "/Stats/").getAsJsonObject("Response"); 47 | allPve = jo.getAsJsonObject("allPvE").getAsJsonObject("allTime"); 48 | } 49 | 50 | public int getActivitiesCleared() { 51 | if(activitiesCleared != 0) return activitiesCleared; 52 | activitiesCleared = getBasicPVE("activitiesCleared").get("value").getAsInt(); 53 | return activitiesCleared; 54 | } 55 | 56 | public int getActivitiesEntered() { 57 | if(activitiesEntered != 0) return activitiesEntered; 58 | activitiesEntered = getBasicPVE("activitiesEntered").get("value").getAsInt(); 59 | return activitiesEntered; 60 | } 61 | 62 | public int getAssists() { 63 | if(assists != 0) return assists; 64 | assists = getBasicPVE("assists").get("value").getAsInt(); 65 | return assists; 66 | } 67 | 68 | public double getPgaAssists() { 69 | if(pgaAssists != 0) return pgaAssists; 70 | pgaAssists = getPGAPVE("assists").get("displayValue").getAsDouble(); 71 | return pgaAssists; 72 | } 73 | 74 | public int getTotalKillDistance() { 75 | if(totalKillDistance != 0) return totalKillDistance; 76 | totalKillDistance = getBasicPVE("totalKillDistance").get("value").getAsInt(); 77 | return totalKillDistance; 78 | } 79 | 80 | public int getKills() { 81 | if(kills != 0) return kills; 82 | kills = getBasicPVE("kills").get("value").getAsInt(); 83 | return kills; 84 | } 85 | 86 | public double getPgaKills() { 87 | if(pgaKills != 0) return pgaKills; 88 | pgaKills = getPGAPVE("kills").get("displayValue").getAsDouble(); 89 | return pgaKills; 90 | } 91 | 92 | public double getAverageKillDistance() { 93 | if(averageKillDistance != 0) return averageKillDistance; 94 | averageKillDistance = getBasicPVE("averageKillDistance").get("displayValue").getAsDouble(); 95 | return averageKillDistance; 96 | } 97 | 98 | public long getSecondsPlayed() { 99 | if(secondsPlayed != 0) return secondsPlayed; 100 | secondsPlayed = getBasicPVE("secondsPlayed").get("value").getAsLong(); 101 | return secondsPlayed; 102 | } 103 | 104 | public double getPgaSecondsPlayed() { 105 | if(pgaSecondsPlayed != 0) return pgaSecondsPlayed; 106 | pgaSecondsPlayed = Double.parseDouble(df.format(getPGAPVE("secondsPlayed").get("value").getAsDouble())); 107 | return pgaSecondsPlayed; 108 | } 109 | 110 | public int getDeaths() { 111 | if(deaths != 0) return deaths; 112 | deaths = getBasicPVE("deaths").get("value").getAsInt(); 113 | return deaths; 114 | } 115 | 116 | public double getPgaDeaths() { 117 | if(pgaDeaths != 0) return pgaDeaths; 118 | pgaDeaths = Double.parseDouble(df.format(getPGAPVE("deaths").get("value").getAsDouble())); 119 | return pgaDeaths; 120 | } 121 | 122 | 123 | 124 | 125 | // util methods 126 | public JsonObject getJsonObject() { 127 | return jo; 128 | } 129 | 130 | private JsonObject getBasicPVE(String name) { 131 | return allPve.getAsJsonObject(name).getAsJsonObject("basic"); 132 | } 133 | 134 | private JsonObject getPGAPVE(String name) { 135 | return allPve.getAsJsonObject(name).getAsJsonObject("pga"); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/utils/HttpRequestModifier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.utils; 9 | 10 | import java.net.http.HttpRequest; 11 | 12 | @FunctionalInterface 13 | public interface HttpRequestModifier { 14 | 15 | HttpRequest.Builder modifyRequest(HttpRequest.Builder starter); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/utils/HttpUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.utils; 9 | 10 | import com.google.gson.JsonObject; 11 | import com.google.gson.JsonParser; 12 | import com.google.gson.JsonSyntaxException; 13 | import net.dec4234.javadestinyapi.exceptions.APIException; 14 | import net.dec4234.javadestinyapi.exceptions.APIOfflineException; 15 | import net.dec4234.javadestinyapi.exceptions.AccessTokenExpiredException; 16 | import net.dec4234.javadestinyapi.exceptions.ConnectionException; 17 | import net.dec4234.javadestinyapi.exceptions.JsonParsingError; 18 | import net.dec4234.javadestinyapi.exceptions.OAuthUnauthorizedException; 19 | import net.dec4234.javadestinyapi.exceptions.RefreshTokenExpiredException; 20 | import net.dec4234.javadestinyapi.material.DestinyAPI; 21 | import net.dec4234.javadestinyapi.material.manifest.ManifestEntityTypes; 22 | 23 | import java.net.URI; 24 | import java.net.http.HttpClient; 25 | import java.net.http.HttpRequest; 26 | import java.net.http.HttpResponse; 27 | import java.time.Duration; 28 | import java.util.Base64; 29 | import java.util.concurrent.ExecutionException; 30 | 31 | /** 32 | * A net.dec4234.javadestinyapi.utils class used by JavaDestinyAPI to collect information from bungie.net 33 | * It is important that the DestinyAPI class is initialized prior to this class 34 | */ 35 | public class HttpUtils { 36 | 37 | public static final String URL_BASE = "https://www.bungie.net/Platform"; 38 | 39 | private String apiKey; 40 | private static String bearerToken; 41 | 42 | public HttpUtils(String apiKey) { 43 | this.apiKey = apiKey; 44 | } 45 | 46 | public void setApiKey(String apiKey) { 47 | this.apiKey = apiKey; 48 | } 49 | 50 | /** 51 | * Send a GET url request to the url provided, returns a JsonObject of the response 52 | */ 53 | public JsonObject urlRequestGET(String url) throws APIException { 54 | return getJsonObject(getRequest(true, url, starter -> { 55 | starter.GET(); 56 | return starter; 57 | })); 58 | } 59 | 60 | public JsonObject urlRequestGETOauth(String url) throws APIException { 61 | if(bearerToken == null) { 62 | bearerToken = DestinyAPI.getAccessToken(); 63 | } 64 | if(bearerToken == null || DestinyAPI.getLastRefreshTime() == 0 || DestinyAPI.getLastRefreshTime() + (45 * 60 * 1000) < System.currentTimeMillis()) { 65 | setTokenViaRefresh(); 66 | } 67 | 68 | try { 69 | return getJsonObject(getRequest(true, url, starter -> { 70 | starter.GET() 71 | .setHeader("Authorization", "Bearer " + HttpUtils.bearerToken); 72 | return starter; 73 | })); 74 | } catch (AccessTokenExpiredException e) { 75 | setTokenViaRefresh(); 76 | 77 | return getJsonObject(getRequest(true, url, starter -> { 78 | starter.GET() 79 | .setHeader("Authorization", "Bearer " + HttpUtils.bearerToken); 80 | return starter; 81 | })); 82 | } 83 | } 84 | 85 | public JsonObject urlRequestPOST(String url, JsonObject body) throws APIException { 86 | return urlRequestPOST(url, body.toString()); 87 | } 88 | 89 | public JsonObject urlRequestPOST(String url, String body) throws APIException { 90 | if (body.isEmpty()) { body = "{\"message\": \"\",}"; } 91 | String finalBody = body; 92 | 93 | if(DestinyAPI.isDebugEnabled()) { 94 | System.out.println("Body: " + finalBody); 95 | } 96 | 97 | return getJsonObject(getRequest(true, url, starter -> { 98 | starter.setHeader("Content-Type", "application/json") 99 | .POST(HttpRequest.BodyPublishers.ofString(finalBody)); 100 | 101 | return starter; 102 | })); 103 | } 104 | 105 | public JsonObject urlRequestPOSTOauth(String url, JsonObject body) throws APIException { 106 | if(bearerToken == null) { 107 | bearerToken = DestinyAPI.getAccessToken(); 108 | } 109 | if(bearerToken == null || DestinyAPI.getLastRefreshTime() == 0 || DestinyAPI.getLastRefreshTime() + (45 * 60 * 1000) < System.currentTimeMillis()) { 110 | setTokenViaRefresh(); 111 | } 112 | 113 | if (body.toString().isEmpty()) { 114 | body = new JsonObject(); 115 | body.addProperty("message", ""); 116 | } 117 | 118 | final String finalBody = body.toString(); 119 | 120 | if(DestinyAPI.isDebugEnabled()) { 121 | System.out.println("Body: " + finalBody); 122 | } 123 | 124 | try { 125 | return getJsonObject(getRequest(true, url, starter -> { 126 | starter.setHeader("Authorization", "Bearer " + HttpUtils.bearerToken) 127 | .setHeader("Content-Type", "application/json") 128 | .POST(HttpRequest.BodyPublishers.ofString(finalBody)); 129 | 130 | return starter; 131 | })); 132 | } catch (AccessTokenExpiredException e) { 133 | setTokenViaRefresh(); 134 | return getJsonObject(getRequest(true, url, starter -> { 135 | starter.setHeader("Authorization", "Bearer " + HttpUtils.bearerToken) 136 | .setHeader("Content-Type", "application/json") 137 | .POST(HttpRequest.BodyPublishers.ofString(finalBody)); 138 | 139 | return starter; 140 | })); 141 | } 142 | } 143 | 144 | public JsonObject urlRequestPOSTOauth(String url) throws APIException { 145 | return urlRequestPOSTOauth(url, new JsonObject()); 146 | } 147 | 148 | /** 149 | * Make a request to the Bungie manifest to reveal information about a hash-identified item. 150 | * 151 | * Deprecated in favor of {@link net.dec4234.javadestinyapi.material.manifest.DestinyManifest#manifestGET(ManifestEntityTypes, String)} 152 | */ 153 | @Deprecated 154 | public JsonObject manifestGET(ManifestEntityTypes entityType, String hashIdentifier) throws APIException { 155 | return urlRequestGET("https://www.bungie.net/Platform/Destiny2/Manifest/" + entityType.getBungieEntityValue() + "/" + hashIdentifier + "/"); 156 | } 157 | 158 | /** 159 | * Gets an access token using the refresh token in storage and replaces the old refresh token with the new one 160 | * 161 | * @return Returns the new access token 162 | */ 163 | public String setTokenViaRefresh() throws APIException { 164 | if(DestinyAPI.getRefreshToken() == null) { 165 | throw new OAuthUnauthorizedException("Refresh token is null. You must set new access and refresh tokens OR your OAuthManager is misconfigured and they can't be accessed."); 166 | } 167 | 168 | String url = "https://www.bungie.net/Platform/App/OAuth/Token/"; 169 | 170 | String requestBody = "grant_type=refresh_token&refresh_token=" + DestinyAPI.getRefreshToken(); 171 | 172 | JsonObject response = getJsonObject(getRequest(false, url, starter -> { 173 | starter.setHeader("Content-Type", "application/x-www-form-urlencoded") 174 | .setHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString((DestinyAPI.getClientId() + ":" + DestinyAPI.getClientSecret()).getBytes())) 175 | .POST(HttpRequest.BodyPublishers.ofString(requestBody)); 176 | 177 | return starter; 178 | })); 179 | 180 | if(response.has("error_description") && response.get("error_description").getAsString().equals("ApplicationTokenKeyIdDoesNotExist")) { 181 | throw new RefreshTokenExpiredException(); 182 | } 183 | 184 | if(!response.has("access_token")) { 185 | return null; 186 | } 187 | 188 | String at = response.get("access_token").getAsString(); 189 | String rt = response.get("refresh_token").getAsString(); 190 | bearerToken = at; 191 | DestinyAPI.setAccessToken(at); 192 | DestinyAPI.setRefreshToken(rt); 193 | DestinyAPI.setLastRefreshTime(); 194 | 195 | if(DestinyAPI.isDebugEnabled()) { 196 | System.out.println("TOKENS REFRESHED"); 197 | } 198 | 199 | return at; 200 | } 201 | 202 | /** 203 | * Requires an OAuthCode to be manually set inside the DestinyAPI.setOAuthCode() 204 | */ 205 | public void setTokenViaAuth() throws APIException { 206 | setTokenViaAuth(DestinyAPI.getOauthCode()); 207 | } 208 | 209 | public void setTokenViaAuth(String oAuthCode) throws APIException { 210 | if(oAuthCode == null) { 211 | throw new OAuthUnauthorizedException("OAuth code is null. You must generate an OAuth code using the OAuth process."); 212 | } 213 | 214 | String url = "https://www.bungie.net/Platform/App/OAuth/Token/"; 215 | 216 | String requestBody = "grant_type=authorization_code&code=" + oAuthCode; 217 | 218 | JsonObject jsonObject = getJsonObject(getRequest(false, url, starter -> { 219 | starter.setHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString((DestinyAPI.getClientId() + ":" + DestinyAPI.getClientSecret()).getBytes())) 220 | .setHeader("Content-Type", "application/x-www-form-urlencoded") 221 | .POST(HttpRequest.BodyPublishers.ofString(requestBody)); 222 | 223 | return starter; 224 | })); 225 | 226 | String accessToken = jsonObject.get("access_token").getAsString(); 227 | String refreshToken = jsonObject.get("refresh_token").getAsString(); 228 | 229 | DestinyAPI.setRefreshToken(refreshToken); 230 | DestinyAPI.setAccessToken(accessToken); 231 | DestinyAPI.setLastRefreshTime(); 232 | 233 | HttpUtils.bearerToken = accessToken; 234 | } 235 | 236 | public boolean hasValidOAuthTokens() throws APIException { 237 | boolean value = DestinyAPI.hasOauthManager() && DestinyAPI.getAccessToken() != null; 238 | 239 | if(value) { 240 | try { 241 | if(DestinyAPI.getHttpUtils().setTokenViaRefresh() == null) { 242 | value = false; 243 | } 244 | } catch (AccessTokenExpiredException | RefreshTokenExpiredException | OAuthUnauthorizedException e) { 245 | value = false; 246 | } 247 | } 248 | 249 | return value; 250 | } 251 | 252 | private JsonObject getJsonObject(HttpRequest httpRequest) throws APIException { 253 | HttpClient httpClient = HttpClient.newHttpClient(); 254 | String responseString; 255 | 256 | try { // TODO: are we even taking advantage of async? this seems pointless to just block right away 257 | responseString = httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString()).thenApplyAsync(HttpResponse::body).get(); 258 | 259 | if (DestinyAPI.isDebugEnabled()) { 260 | String type = httpRequest.method() + " " + httpRequest.uri().toString(); 261 | if(httpRequest.headers().firstValue("Authorization").isPresent()) { 262 | type = httpRequest.method() + " OAUTH " + httpRequest.uri().toString(); 263 | } 264 | System.out.println(type); 265 | if(DestinyAPI.doPrintHeaders()) { 266 | httpRequest.headers().map().forEach((s, strings) -> { 267 | if(!s.equals("Content-Type")) { 268 | System.out.println(s + " " + strings.toString()); 269 | } 270 | }); 271 | } 272 | System.out.println(responseString); 273 | } 274 | } catch (InterruptedException | ExecutionException e) { 275 | throw new ConnectionException(e); 276 | } 277 | 278 | JsonObject jsonObject; 279 | 280 | try { 281 | jsonObject = JsonParser.parseString(responseString).getAsJsonObject(); 282 | } catch (JsonSyntaxException e) { 283 | throw new JsonParsingError(e); 284 | } 285 | 286 | // Check for API errors - https://bungie-net.github.io/multi/schema_Exceptions-PlatformErrorCodes.html#schema_Exceptions-PlatformErrorCodes 287 | if(jsonObject.has("ErrorCode")) { 288 | switch (jsonObject.get("ErrorCode").getAsInt()) { //TODO: lots of errors we could catch here 289 | case 5: // APIOffline 290 | throw new APIOfflineException(jsonObject.get("Message").getAsString()); 291 | case 99: // WebAuthRequired 292 | throw new OAuthUnauthorizedException("OAuth - access denied. Try authenticating."); 293 | case 2111 | 2115: // AccessTokenHasExpired, OAuthAccessTokenExpired 294 | throw new AccessTokenExpiredException(); 295 | case 2118: // RefreshTokenExpired -- need to reauth using oauth 296 | throw new RefreshTokenExpiredException(); 297 | } 298 | } 299 | 300 | return jsonObject; 301 | } 302 | 303 | private String getStringResponse(HttpRequest httpRequest) throws ConnectionException { 304 | HttpClient httpClient = HttpClient.newHttpClient(); 305 | try { 306 | String responseString = httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString()).thenApplyAsync(HttpResponse::body).get(); 307 | 308 | if (DestinyAPI.isDebugEnabled()) { 309 | System.out.println(httpRequest.method() + " " + httpRequest.uri().toString()); 310 | System.out.println("Response: " + responseString); 311 | } 312 | return responseString; 313 | } catch (InterruptedException | ExecutionException e) { 314 | throw new ConnectionException(e); 315 | } 316 | } 317 | 318 | private HttpRequest getRequest(boolean standardRequest, String url, HttpRequestModifier httpRequestModifier) { 319 | HttpRequest.Builder builder = httpRequestModifier.modifyRequest(HttpRequest.newBuilder()); 320 | 321 | builder.uri(URI.create(url)).timeout(Duration.ofMinutes(3)); 322 | 323 | if(standardRequest) { 324 | builder.setHeader("X-API-KEY", apiKey); 325 | } 326 | 327 | return builder.build(); 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/utils/StringUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.utils; 9 | 10 | import java.io.IOException; 11 | import java.net.URLEncoder; 12 | import java.nio.charset.StandardCharsets; 13 | import java.text.DateFormat; 14 | import java.text.DecimalFormat; 15 | import java.text.ParseException; 16 | import java.text.SimpleDateFormat; 17 | import java.util.Date; 18 | import java.util.TimeZone; 19 | 20 | public class StringUtils { 21 | 22 | /** 23 | * @param zuluTimeInString The Zulu time, in the form of a String, you wish to convert 24 | * @return Returns a date object representing the Zulu time provided 25 | */ 26 | public static Date valueOfZTime(String zuluTimeInString) { 27 | String temp = zuluTimeInString; 28 | if(temp.length() == 24) { // Sometimes the date will be in an extra precise format, which is truncated here 29 | temp = temp.substring(0, temp.length() - 5); 30 | temp = temp + "Z"; 31 | } 32 | DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:sss'Z'"); 33 | df.setTimeZone(TimeZone.getTimeZone("Zulu")); 34 | try { 35 | return df.parse(temp); 36 | } catch (ParseException e) { // TODO: log error here 37 | return null; 38 | } 39 | } 40 | 41 | /** 42 | * Get the number of days since the time date provided 43 | */ 44 | public static double getDaysSinceTime(Date date) { 45 | DecimalFormat df = new DecimalFormat("0.##"); 46 | return Double.parseDouble(df.format((new Date().getTime() - date.getTime()) / 1000.0 / 60.0 / 60.0 / 24.0)); 47 | } 48 | 49 | /** 50 | * Encode a string to be suitable for use in a url 51 | * 52 | * Specific characters need to be encoded in order to have a successful request 53 | */ 54 | public static String httpEncode(String input) { 55 | return URLEncoder.encode(input, StandardCharsets.UTF_8); 56 | } 57 | 58 | public static void executeCommandLine(String command) { 59 | try { 60 | Process process = Runtime.getRuntime().exec(command); 61 | } catch (IOException e) { 62 | e.printStackTrace(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/utils/fast/ASyncPull.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.utils.fast; 9 | 10 | import java.util.List; 11 | 12 | public interface ASyncPull { 13 | 14 | List run(List source); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/utils/fast/ASyncPuller.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.utils.fast; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | public class ASyncPuller { 14 | 15 | /** 16 | * Split the stream list into multiple parts, handle using the asyncpull consumer method 17 | * Useful for accelerating processing of large or taxing operations 18 | * 19 | * @param stream The source of info to be split 20 | * @param partsToSplitInto The amount of threads to be created, the stream list will be split as much as possible based on this 21 | * @param aSyncPull What you want to happen to the info when in its in an ASync thread 22 | */ 23 | public static List asyncPull(List stream, int partsToSplitInto, ASyncPull aSyncPull) { 24 | int index = 0; // The index of the stream to start from 25 | int[] parts = splitIntoParts(stream.size(), partsToSplitInto); // A list of integers used to separate the stream 26 | int listIndex = 0; // The index in the integer array we are currently on 27 | List returnList = new ArrayList<>(); 28 | 29 | while (index < stream.size()) { // Until we have completely looped through the stream 30 | int i = parts[listIndex]; 31 | new MemberThread(returnList, stream.subList(index, index + i), aSyncPull).start(); 32 | 33 | index += i; 34 | 35 | listIndex++; 36 | } 37 | 38 | return returnList; 39 | } 40 | 41 | private static class MemberThread extends Thread { 42 | 43 | public MemberThread(List source, List stream, ASyncPull aSyncPull) { 44 | source.addAll(aSyncPull.run(stream)); 45 | } 46 | 47 | } 48 | 49 | private static int[] splitIntoParts(int whole, int parts) { 50 | int[] arr = new int[parts]; 51 | int remain = whole; 52 | int partsLeft = parts; 53 | for (int i = 0; partsLeft > 0; i++) { 54 | int size = (remain + partsLeft - 1) / partsLeft; // rounded up, aka ceiling 55 | arr[i] = size; 56 | remain -= size; 57 | partsLeft--; 58 | } 59 | return arr; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/utils/fast/Pagination.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.utils.fast; 9 | 10 | import net.dec4234.javadestinyapi.exceptions.APIException; 11 | import net.dec4234.javadestinyapi.material.user.BungieUser; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | import java.util.Iterator; 15 | import java.util.List; 16 | 17 | /** 18 | * Paginate through an indexed portion of the API. This is usually used wherever you search for something, and could get 19 | * a large list of items in return. Paging through it allows you to potentially save on memory + time, since you could 20 | * find what you want early on and terminate early. 21 | *
22 | * Implementation Note: See an example in {@link net.dec4234.javadestinyapi.material.DestinyAPI#searchUsers(String)} 23 | * @param The type that EACH page will return. For example, each page could return a List of users, which would need 24 | * to be combined into one large list if you wanted to look at all of them. 25 | */ 26 | public abstract class Pagination implements Iterator, Iterable { 27 | 28 | protected T currentResponse; 29 | protected boolean hasGrabbed; 30 | 31 | public Pagination() { 32 | 33 | } 34 | 35 | public Pagination(final T currentResponse) { 36 | this.currentResponse = currentResponse; 37 | } 38 | 39 | /** 40 | * Does the pagination object have another page to look at? 41 | *
42 | * Implementation Note: This should make a request to the API iff there isn't an unread request inside. It then 43 | * checks if the response is a new valid page, then updates currentResponse and hasGrabbed appropriately. 44 | * @return True if there is another valid page of objects ready 45 | * @throws APIException If something goes wrong in the request to the API 46 | */ 47 | public abstract boolean hasNext(); 48 | 49 | /** 50 | * Returns the next page of objects. Grabs a new set of objects (page) if the current one has already been used. 51 | * @return The next page of objects, or null if you reach the end 52 | * @throws APIException If something goes wrong in the request to the API 53 | */ 54 | public abstract T next(); 55 | 56 | /** 57 | * Aggregate all results into a single list. This has to be implemented by the specific type in order to work. 58 | * @return An aggregated list of all results, presuming that type T is a list, otherwise it is null 59 | */ 60 | public T aggregate() { 61 | return null; 62 | } 63 | 64 | @NotNull 65 | @Override 66 | public Iterator iterator() { 67 | return this; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/utils/framework/ContentFramework.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.utils.framework; 9 | 10 | import com.google.gson.JsonObject; 11 | import net.dec4234.javadestinyapi.exceptions.APIException; 12 | import net.dec4234.javadestinyapi.material.DestinyAPI; 13 | import net.dec4234.javadestinyapi.material.manifest.ManifestEntityTypes; 14 | 15 | /** 16 | * Used as a standard framework for most stuff that bases its content off of a single request 17 | */ 18 | public class ContentFramework implements ContentInterface { 19 | 20 | private String url; 21 | private ManifestEntityTypes manifestType; 22 | protected JsonObject jo = null; 23 | private JsonObjectModifier jsonObjectModifier; 24 | 25 | public ContentFramework(JsonObject jsonObject) { 26 | this.jo = jsonObject; 27 | this.jsonObjectModifier = source -> source; 28 | } 29 | 30 | public ContentFramework(String url, JsonObjectModifier jsonObjectModifier) { 31 | this.url = url; 32 | this.jsonObjectModifier = jsonObjectModifier; 33 | } 34 | 35 | public ContentFramework(ManifestEntityTypes manifestType, String url, JsonObjectModifier jsonObjectModifier) { 36 | this.manifestType = manifestType; 37 | this.url = url; 38 | this.jsonObjectModifier = jsonObjectModifier; 39 | } 40 | 41 | @Override 42 | public void checkJO() throws APIException { 43 | if(jo == null) { 44 | if(manifestType == null) { 45 | jo = DestinyAPI.getHttpUtils().urlRequestGET(url); 46 | } else { 47 | jo = DestinyAPI.getHttpUtils().manifestGET(manifestType, url); 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * Refresh the jsonobject to potentially account for new changes 54 | */ 55 | public void refreshJO() throws APIException { 56 | if(manifestType == null) { 57 | jo = DestinyAPI.getHttpUtils().urlRequestGET(url); 58 | } else { 59 | jo = DestinyAPI.getHttpUtils().manifestGET(manifestType, url); 60 | } 61 | } 62 | 63 | public JsonObject getJO() throws APIException { 64 | checkJO(); 65 | return jsonObjectModifier.modify(jo); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/utils/framework/ContentInterface.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.utils.framework; 9 | 10 | import net.dec4234.javadestinyapi.exceptions.APIException; 11 | 12 | public interface ContentInterface { 13 | 14 | /** 15 | * Used to verify if the raw JsonObject has been initialized 16 | * Initialize the JsonObject from here if it is not initialized 17 | */ 18 | void checkJO() throws APIException; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/utils/framework/JDAOAuth.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.utils.framework; 9 | 10 | /** 11 | * Contains all the functions needed for the API to adequately store OAuth tokens. 12 | *
13 | * Note: OAuth could allow potentially dangerous actions such as full control over your clan (if you are an admin) as 14 | * well as your inventory. Use at your own risk, and use good data management and protection practices. 15 | */ 16 | public interface JDAOAuth { 17 | 18 | String getAccessToken(); 19 | String getRefreshToken(); 20 | String getAPIToken(); 21 | 22 | void setAccessToken(String accessToken); 23 | void setRefreshToken(String refreshToken); 24 | void setAPIToken(String apiToken); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/utils/framework/JsonOAuthManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.utils.framework; 9 | 10 | import com.google.gson.JsonObject; 11 | import com.google.gson.JsonParser; 12 | import net.dec4234.javadestinyapi.material.DestinyAPI; 13 | import net.dec4234.javadestinyapi.utils.framework.OAuthManager; 14 | 15 | import java.io.*; 16 | import java.nio.file.Paths; 17 | 18 | /** 19 | * This class is provided for convenience for local apps using OAuth. It is also provided as a template for what 20 | * you should do to implement your own token manager. 21 | *
22 | * The author assumes no responsibility for the use of this class. It is unsafe to use this in a production environment, 23 | * proceed with your own risk. 24 | */ 25 | public class JsonOAuthManager extends OAuthManager { 26 | 27 | private File file = new File(Paths.get("").toAbsolutePath() + "\\oauth.json"); 28 | private JsonObject jsonObject; 29 | 30 | public JsonOAuthManager() { 31 | try { 32 | if (!file.exists()) { 33 | file.createNewFile(); 34 | } 35 | 36 | try { 37 | jsonObject = new JsonParser().parse(new FileReader(file.getAbsolutePath())).getAsJsonObject(); 38 | } catch (IllegalStateException exception) { // If the file is empty or corrupted 39 | jsonObject = new JsonObject(); 40 | } 41 | } catch (IOException e) { 42 | e.printStackTrace(); 43 | } 44 | } 45 | 46 | @Override 47 | public String getAccessToken() { 48 | if(jsonObject.has("access-token")) { 49 | return jsonObject.get("access-token").getAsString(); 50 | } 51 | 52 | return null; 53 | } 54 | 55 | @Override 56 | public String getRefreshToken() { 57 | if(jsonObject.has("refresh-token")) { 58 | return jsonObject.get("refresh-token").getAsString(); 59 | } 60 | 61 | return null; 62 | } 63 | 64 | @Override 65 | public String getAPIToken() { 66 | return DestinyAPI.getApiKey(); 67 | } 68 | 69 | @Override 70 | public void setAccessToken(String accessToken) { 71 | jsonObject.addProperty("access-token", accessToken); 72 | save(); 73 | } 74 | 75 | @Override 76 | public void setRefreshToken(String refreshToken) { 77 | jsonObject.addProperty("refresh-token", refreshToken); 78 | save(); 79 | } 80 | 81 | @Override 82 | public void setAPIToken(String apiToken) { 83 | 84 | } 85 | 86 | public void save() { 87 | try (FileWriter fileWriter = new FileWriter(file.getAbsolutePath())) { 88 | fileWriter.write(jsonObject.toString()); 89 | fileWriter.flush(); 90 | } catch (IOException e) { 91 | e.printStackTrace(); 92 | } 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/utils/framework/JsonObjectModifier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.utils.framework; 9 | 10 | import com.google.gson.JsonObject; 11 | 12 | /** 13 | * Used to modify JsonObjects returned by HttpRequests 14 | */ 15 | @FunctionalInterface 16 | public interface JsonObjectModifier { 17 | 18 | JsonObject modify(JsonObject source); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/utils/framework/OAuthFlow.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.utils.framework; 9 | 10 | import com.sun.net.httpserver.*; 11 | import net.dec4234.javadestinyapi.exceptions.APIException; 12 | import net.dec4234.javadestinyapi.exceptions.InvalidConditionException; 13 | import net.dec4234.javadestinyapi.material.DestinyAPI; 14 | import net.dec4234.javadestinyapi.utils.StringUtils; 15 | 16 | import javax.net.ssl.KeyManagerFactory; 17 | import javax.net.ssl.SSLContext; 18 | import javax.net.ssl.SSLParameters; 19 | import javax.net.ssl.TrustManagerFactory; 20 | import java.awt.Desktop; 21 | import java.io.*; 22 | import java.net.*; 23 | import java.nio.file.Paths; 24 | import java.security.KeyStore; 25 | import java.util.concurrent.Executors; 26 | 27 | /** 28 | * The OAuthFlow class allows a user to easily generate oauth tokens for a small project that is not distributed to 29 | * external users. This class could be replicated by an experienced user to allow for handling multiple oauth tokens. 30 | *

31 | * Note: OAuth could allow potentially dangerous actions such as full control over your clan (if you are an admin) as 32 | * well as your inventory. Use at your own risk, and use good data management and protection practices. 33 | *

34 | * Here is the process:
35 | * 1. Go to your app on https://www.bungie.net/en/Application
36 | * 2. Under "App authentication" set the client type to confidential and set the redirect to something like 37 | * "https://localhost:8080". Note that it MUST be HTTPS not HTTP
38 | * 3. Adjust the permissions for the app using the checkboxes right below it.
39 | * 4. Use the following code to init your oauth tokens. 40 | *

{@code
 41 |  * OAuthFlow oAuthFlow = new OAuthFlow();
 42 |  * oAuthFlow.initOAuthFlowIfNeeded(8080);
 43 |  * }
44 | * This will open the browser on your local machine where it will direct you to do oauth. It might say that the site is 45 | * unsafe since it doesn't have a valid certificate. On chrome, click the "advanced" button and then "continue". You 46 | * should now be able to perform OAuth requests and all the interesting things you can build with it.
47 | * 5.(Optionally) When initializing the DestinyAPI you can use {@link DestinyAPI#setOauthManager(OAuthManager)} to save 48 | * the tokens however you want, so you don't have to oauth frequently. 49 | */ 50 | public class OAuthFlow { 51 | 52 | // keytool -genkey -dname "cn=dec 4234, ou=github/JavaDestinyAPI, o=ou=github/JavaDestinyAPI, c=US" -keyalg RSA -alias alias -keystore keystore.jks -storepass mypassword -keypass mypassword -validity 360 -keysize 2048 53 | 54 | private String queryParameters = "empty"; 55 | private volatile boolean hasQueryBeenReturned = false; 56 | 57 | /** 58 | * Initiate the OAuthFlow class which goes through the following steps 59 | * 60 | * 1. Opens the OAuth page on the user's default browser 61 | * 2. Creates an HTTPS localhost server to receive that information 62 | * 3. Extracts the oauth code from the query parameters 63 | * 4. Sets the tokens using that information 64 | * @param port The port to start the server on 65 | */ 66 | public void initOAuthFlow(int port) throws APIException { 67 | setTokens(port); //TODO: bug: waits 30s for no reason? not part of this class? 68 | } 69 | 70 | /** 71 | * Initiate the OAuth Flow only if an existing key cannot be found or if it has expired 72 | * @param port The port to start the server on 73 | */ 74 | public void initOAuthFlowIfNeeded(int port) throws APIException { 75 | if(DestinyAPI.getHttpUtils().hasValidOAuthTokens()) { 76 | return; 77 | } 78 | 79 | try { 80 | if(!DestinyAPI.hasOauthManager() || DestinyAPI.getAccessToken() == null || DestinyAPI.getHttpUtils().setTokenViaRefresh() == null) { 81 | initOAuthFlow(port); 82 | } 83 | } catch (InvalidConditionException e) { 84 | initOAuthFlow(port); 85 | } 86 | } 87 | 88 | private void setTokens(int serverPort) throws APIException { 89 | openOAuthPage(); 90 | 91 | startSecureServer(serverPort); 92 | 93 | String rawCode = getRawCode(queryParameters); 94 | 95 | DestinyAPI.getHttpUtils().setTokenViaAuth(rawCode); 96 | } 97 | 98 | private String getRawCode(String queryInput) { 99 | String codeString = queryInput.split("&")[0]; 100 | 101 | return codeString.split("=")[1]; 102 | } 103 | 104 | private void openOAuthPage() { 105 | if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { 106 | try { 107 | Desktop.getDesktop().browse(new URI("https://www.bungie.net/en/OAuth/Authorize?client_id=" + DestinyAPI.getClientId() + "&response_type=code")); 108 | } catch (IOException | URISyntaxException e) { 109 | e.printStackTrace(); 110 | } 111 | } 112 | } 113 | 114 | private void startSecureServer(int port) { 115 | HttpsServer server = null; 116 | final String[] queryParams = {""}; 117 | String filePath = new File(Paths.get("").toAbsolutePath().toString()).getPath() + "\\keystore.jks"; 118 | File file = new File(filePath); 119 | 120 | try { 121 | SSLContext sslContext = null; 122 | try { 123 | server = HttpsServer.create(new InetSocketAddress(port), 0); 124 | sslContext = SSLContext.getInstance("TLS"); 125 | char[] password = "mypassword".toCharArray(); 126 | KeyStore ks = KeyStore.getInstance("JKS"); 127 | 128 | file.delete(); 129 | 130 | // Generate a new key store 131 | StringUtils.executeCommandLine("keytool -genkey -dname \"cn=dec 4234, ou=github/JavaDestinyAPI, o=ou=github/JavaDestinyAPI, c=US\" -ext san=dns:www.dev.dec4234.net -keyalg RSA -alias alias -keystore keystore.jks -storepass mypassword -keypass mypassword -validity 360 -keysize 2048"); 132 | 133 | // Sleep to allow for the keystore to be generated 134 | Thread.sleep(2000); 135 | 136 | FileInputStream fis = new FileInputStream(filePath); 137 | ks.load(fis, password); 138 | 139 | KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); 140 | kmf.init(ks, password); 141 | 142 | TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); 143 | tmf.init(ks); 144 | 145 | sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); 146 | 147 | } catch (Exception e) { 148 | e.printStackTrace(); 149 | System.exit(-1); 150 | } 151 | 152 | HttpsConfigurator httpsConfigurator = new HttpsConfigurator(sslContext) { 153 | @Override 154 | public void configure(HttpsParameters httpsParameters) { 155 | SSLContext sslContext = getSSLContext(); 156 | SSLParameters defaultSSLParameters = sslContext.getDefaultSSLParameters(); 157 | httpsParameters.setSSLParameters(defaultSSLParameters); 158 | } 159 | }; 160 | 161 | server.createContext("/", t -> { 162 | HttpsExchange s = (HttpsExchange) t; 163 | s.getSSLSession(); 164 | 165 | String response = "You can now close this page."; 166 | t.sendResponseHeaders(200, response.length()); 167 | OutputStream os = t.getResponseBody(); 168 | os.write(response.getBytes()); 169 | 170 | getQueryParameters(s); 171 | 172 | os.close(); 173 | 174 | s.close(); 175 | }); 176 | 177 | long time = System.currentTimeMillis() + (30 * 1000); 178 | 179 | server.setExecutor(Executors.newCachedThreadPool()); 180 | server.setHttpsConfigurator(httpsConfigurator); 181 | server.start(); 182 | 183 | // A crude attempt at a time-out 184 | while (!hasQueryBeenReturned && time > System.currentTimeMillis()) { 185 | 186 | } 187 | } finally { 188 | if(server != null) { 189 | server.stop(0); 190 | } 191 | file.delete(); // Delete keystore 192 | } 193 | } 194 | 195 | /** 196 | * Return the query parameters of a particular incoming request 197 | */ 198 | private String getQueryParameters(HttpsExchange exchange) { 199 | queryParameters = exchange.getRequestURI().getQuery(); 200 | hasQueryBeenReturned = true; 201 | 202 | return queryParameters; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/main/java/net/dec4234/javadestinyapi/utils/framework/OAuthManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. dec4234 3 | * A standard open MIT license applies. Modififcation and usage permitted with credit. No warranties or express guarentees are given in any way. 4 | * 5 | * Github -> https://github.com/dec4234/JavaDestinyAPI 6 | */ 7 | 8 | package net.dec4234.javadestinyapi.utils.framework; 9 | 10 | /** 11 | * Used to direct the storage of OAuth tokens using the user's own code. See {@link OAuthFlow} and {@link JDAOAuth} for 12 | * more info. 13 | *
14 | * Note: OAuth could allow potentially dangerous actions such as full control over your clan (if you are an admin) as 15 | * well as your inventory. Use at your own risk, and use good data management and protection practices. 16 | *
17 | * See {@link JsonOAuthManager} for an example implementation 18 | */ 19 | public abstract class OAuthManager implements JDAOAuth { 20 | } 21 | --------------------------------------------------------------------------------