Use user accounts for extra data
All checks were successful
Deploy API / deploy (ubuntu-latest, 2.44.0) (push) Successful in 4s
All checks were successful
Deploy API / deploy (ubuntu-latest, 2.44.0) (push) Successful in 4s
This commit is contained in:
parent
451fc89afc
commit
c7bc2a2bdd
@ -1,2 +1,3 @@
|
|||||||
# API
|
# API
|
||||||
|
|
||||||
The API for Tether (:
|
The API for Tether (:
|
26
API/pom.xml
26
API/pom.xml
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<parent>
|
<parent>
|
||||||
@ -49,6 +49,19 @@
|
|||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
<!-- Dependency Management -->
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.konghq</groupId>
|
||||||
|
<artifactId>unirest-java-bom</artifactId>
|
||||||
|
<version>4.4.0</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
<!-- Dependencies -->
|
<!-- Dependencies -->
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- Spring -->
|
<!-- Spring -->
|
||||||
@ -91,5 +104,14 @@
|
|||||||
<version>2.11.0</version>
|
<version>2.11.0</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.konghq</groupId>
|
||||||
|
<artifactId>unirest-java-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.konghq</groupId>
|
||||||
|
<artifactId>unirest-modules-gson</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
37
API/src/main/java/me/braydon/tether/common/DiscordUtils.java
Normal file
37
API/src/main/java/me/braydon/tether/common/DiscordUtils.java
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package me.braydon.tether.common;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@UtilityClass
|
||||||
|
public final class DiscordUtils {
|
||||||
|
public static final long DISCORD_EPOCH = 1420070400000L;
|
||||||
|
public static final long TIMESTAMP_OFFSET = 22;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the unix creation-time of a Discord-entity by
|
||||||
|
* doing the reverse snowflake algorithm on its id.
|
||||||
|
*
|
||||||
|
* @param entitySnowflake The id of the entity where the creation-time should be determined for
|
||||||
|
* @return The creation time of the entity as unix time
|
||||||
|
* @see <a href="https://github.com/discord-jda/JDA/blob/master/src/main/java/net/dv8tion/jda/api/utils/TimeUtil.java#L61">Credits</a>
|
||||||
|
*/
|
||||||
|
public static long getTimeCreated(long entitySnowflake) {
|
||||||
|
return ((entitySnowflake >>> TIMESTAMP_OFFSET) + DISCORD_EPOCH) / 1000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the extension of the
|
||||||
|
* media with the given id.
|
||||||
|
*
|
||||||
|
* @param mediaId the media id
|
||||||
|
* @return the media extension
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static String getMediaExtension(@NonNull String mediaId) {
|
||||||
|
return mediaId.startsWith("a_") ? "gif" : "png";
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,7 @@ public final class EnvironmentUtils {
|
|||||||
* Is the app running in a production environment?
|
* Is the app running in a production environment?
|
||||||
*/
|
*/
|
||||||
@Getter private static final boolean production;
|
@Getter private static final boolean production;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
String appEnv = System.getenv("APP_ENV");
|
String appEnv = System.getenv("APP_ENV");
|
||||||
production = appEnv != null && (appEnv.equals("production"));
|
production = appEnv != null && (appEnv.equals("production"));
|
||||||
|
@ -36,8 +36,7 @@ public final class UserController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping("/{snowflake}") @ResponseBody @NonNull
|
@GetMapping("/{snowflake}") @ResponseBody @NonNull
|
||||||
public ResponseEntity<DiscordUserResponse> getUserBySnowflake(@PathVariable @NonNull String snowflake)
|
public ResponseEntity<DiscordUserResponse> getUserBySnowflake(@PathVariable @NonNull String snowflake)
|
||||||
throws BadRequestException, ServiceUnavailableException, ResourceNotFoundException
|
throws BadRequestException, ServiceUnavailableException, ResourceNotFoundException {
|
||||||
{
|
|
||||||
return ResponseEntity.ok(discordService.getUserBySnowflake(snowflake));
|
return ResponseEntity.ok(discordService.getUserBySnowflake(snowflake));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -26,7 +26,7 @@ public final class ExceptionController extends AbstractErrorController {
|
|||||||
public ExceptionController(@NonNull ErrorAttributes errorAttributes) {
|
public ExceptionController(@NonNull ErrorAttributes errorAttributes) {
|
||||||
super(errorAttributes);
|
super(errorAttributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping @ResponseBody @NonNull
|
@RequestMapping @ResponseBody @NonNull
|
||||||
public ResponseEntity<ErrorResponse> onError(@NonNull HttpServletRequest request) {
|
public ResponseEntity<ErrorResponse> onError(@NonNull HttpServletRequest request) {
|
||||||
Map<String, Object> error = getErrorAttributes(request, ErrorAttributeOptions.of(
|
Map<String, Object> error = getErrorAttributes(request, ErrorAttributeOptions.of(
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
package me.braydon.tether.model;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NonNull;
|
|
||||||
import lombok.Setter;
|
|
||||||
import net.dv8tion.jda.api.entities.User;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A model representing a cached Discord user.
|
|
||||||
*
|
|
||||||
* @author Braydon
|
|
||||||
*/
|
|
||||||
@AllArgsConstructor @Setter @Getter
|
|
||||||
public final class CachedDiscordUser {
|
|
||||||
/**
|
|
||||||
* The cached user.
|
|
||||||
*/
|
|
||||||
@NonNull private final User user;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The cached user profile.
|
|
||||||
*/
|
|
||||||
@NonNull private final User.Profile profile;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The unix time of when this user was cached.
|
|
||||||
*/
|
|
||||||
private long cached;
|
|
||||||
}
|
|
@ -1,248 +0,0 @@
|
|||||||
package me.braydon.tether.model;
|
|
||||||
|
|
||||||
import lombok.*;
|
|
||||||
import net.dv8tion.jda.api.OnlineStatus;
|
|
||||||
import net.dv8tion.jda.api.entities.*;
|
|
||||||
|
|
||||||
import java.util.EnumSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A model of a Discord user.
|
|
||||||
*
|
|
||||||
* @author Braydon
|
|
||||||
*/
|
|
||||||
@AllArgsConstructor @Getter @EqualsAndHashCode @ToString
|
|
||||||
public final class DiscordUser {
|
|
||||||
/**
|
|
||||||
* The unique snowflake of this user.
|
|
||||||
*/
|
|
||||||
@EqualsAndHashCode.Include private final long snowflake;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The username of this user.
|
|
||||||
*/
|
|
||||||
@NonNull private final String username;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The display name of this user, if any.
|
|
||||||
*/
|
|
||||||
private final String displayName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The flags of this user.
|
|
||||||
*/
|
|
||||||
@NonNull private final UserFlags flags;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The avatar of this user.
|
|
||||||
*/
|
|
||||||
@NonNull private final Avatar avatar;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The banner of this user, if any.
|
|
||||||
*/
|
|
||||||
private final Banner banner;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The accent color of this user.
|
|
||||||
*/
|
|
||||||
@NonNull private final String accentColor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The online status of this user.
|
|
||||||
*/
|
|
||||||
@NonNull private final OnlineStatus onlineStatus;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The clients this user is active on, if known.
|
|
||||||
*/
|
|
||||||
private final EnumSet<ClientType> activeClients;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The activities of this user, if known.
|
|
||||||
*/
|
|
||||||
private final List<Activity> activities;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Spotify activity of this user, if known.
|
|
||||||
*/
|
|
||||||
@EqualsAndHashCode.Exclude private final SpotifyActivity spotify;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is this user a bot?
|
|
||||||
*/
|
|
||||||
private final boolean bot;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The unix time of when this user joined Discord.
|
|
||||||
*/
|
|
||||||
private final long createdAt;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a Discord user from the
|
|
||||||
* raw entities returned from Discord.
|
|
||||||
*
|
|
||||||
* @param user the raw user entity
|
|
||||||
* @param profile the raw profile entity
|
|
||||||
* @param member the raw member entity, if any
|
|
||||||
* @return the built user
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public static DiscordUser buildFromEntity(@NonNull User user, @NonNull User.Profile profile, Member member) {
|
|
||||||
Avatar avatar = new Avatar(user.getAvatarId() == null ? user.getDefaultAvatarId() : user.getAvatarId(), user.getEffectiveAvatarUrl());
|
|
||||||
Banner banner = profile.getBannerId() == null || profile.getBannerUrl() == null ? null : new Banner(profile.getBannerId(), profile.getBannerUrl());
|
|
||||||
String accentColor = String.format("#%06X", (0xFFFFFF & profile.getAccentColorRaw()));
|
|
||||||
|
|
||||||
OnlineStatus onlineStatus = member == null ? OnlineStatus.OFFLINE : member.getOnlineStatus();
|
|
||||||
if (onlineStatus == OnlineStatus.UNKNOWN) {
|
|
||||||
onlineStatus = OnlineStatus.OFFLINE;
|
|
||||||
}
|
|
||||||
|
|
||||||
EnumSet<ClientType> activeClients = member == null ? null : member.getActiveClients();
|
|
||||||
List<Activity> activities = member == null ? null : member.getActivities();
|
|
||||||
SpotifyActivity spotify = null;
|
|
||||||
if (activities != null) {
|
|
||||||
for (Activity activity : activities) {
|
|
||||||
if (!activity.getName().equals("Spotify") || !activity.isRich()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
spotify = SpotifyActivity.fromActivity(Objects.requireNonNull(activity.asRichPresence()));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new DiscordUser(
|
|
||||||
user.getIdLong(), user.getName(), user.getGlobalName(), new UserFlags(user.getFlags(), user.getFlagsRaw()),
|
|
||||||
avatar, banner, accentColor, onlineStatus, activeClients, activities, spotify, user.isBot(), user.getTimeCreated().toInstant().toEpochMilli()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A user's flags.
|
|
||||||
*/
|
|
||||||
@AllArgsConstructor @Getter @EqualsAndHashCode
|
|
||||||
public static class UserFlags {
|
|
||||||
/**
|
|
||||||
* The list of flags the user has.
|
|
||||||
*/
|
|
||||||
@NonNull private final EnumSet<User.UserFlag> list;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The raw flags the user has.
|
|
||||||
*/
|
|
||||||
private final int raw;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A user's avatar.
|
|
||||||
*/
|
|
||||||
@AllArgsConstructor @Getter @EqualsAndHashCode
|
|
||||||
public static class Avatar {
|
|
||||||
/**
|
|
||||||
* The id of the user's avatar.
|
|
||||||
*/
|
|
||||||
@NonNull private final String id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The URL of the user's avatar.
|
|
||||||
*/
|
|
||||||
@NonNull private final String url;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A user's banner.
|
|
||||||
*/
|
|
||||||
@AllArgsConstructor @Getter @EqualsAndHashCode
|
|
||||||
public static class Banner {
|
|
||||||
/**
|
|
||||||
* The id of the user's banner.
|
|
||||||
*/
|
|
||||||
@NonNull private final String id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The URL of the user's banner.
|
|
||||||
*/
|
|
||||||
@NonNull private final String url;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A user's Spotify activity data.
|
|
||||||
*/
|
|
||||||
@AllArgsConstructor @Getter
|
|
||||||
public static class SpotifyActivity {
|
|
||||||
/**
|
|
||||||
* The ID of the currently playing track.
|
|
||||||
*/
|
|
||||||
@NonNull private final String trackId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name of the currently playing track.
|
|
||||||
*/
|
|
||||||
@NonNull private final String song;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The currently playing artist.
|
|
||||||
*/
|
|
||||||
@NonNull private final String artist;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The album the song is from.
|
|
||||||
*/
|
|
||||||
@NonNull private final String album;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The URL to the art for the currently playing album.
|
|
||||||
*/
|
|
||||||
@NonNull private final String albumArtUrl;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The URL to the playing track.
|
|
||||||
*/
|
|
||||||
@NonNull private final String trackUrl;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current progress of the track (in millis).
|
|
||||||
*/
|
|
||||||
private final long trackProgress;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The total length of the track (in millis).
|
|
||||||
*/
|
|
||||||
private final long trackLength;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The unix time of when this track started playing.
|
|
||||||
*/
|
|
||||||
private final long started;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The unix time of when this track stops playing.
|
|
||||||
*/
|
|
||||||
private final long ends;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a Spotify activity from the raw Discord data.
|
|
||||||
*
|
|
||||||
* @param richPresence the raw Discord data
|
|
||||||
* @return the built Spotify activity
|
|
||||||
*/
|
|
||||||
@NonNull @SuppressWarnings("DataFlowIssue")
|
|
||||||
public static SpotifyActivity fromActivity(@NonNull RichPresence richPresence) {
|
|
||||||
String trackUrl = "https://open.spotify.com/track/" + richPresence.getSyncId();
|
|
||||||
|
|
||||||
// Track progress
|
|
||||||
long started = Objects.requireNonNull(richPresence.getTimestamps()).getStart();
|
|
||||||
long ends = richPresence.getTimestamps().getEnd();
|
|
||||||
|
|
||||||
long trackLength = ends - started;
|
|
||||||
long trackProgress = Math.min(System.currentTimeMillis() - started, trackLength);
|
|
||||||
|
|
||||||
return new SpotifyActivity(
|
|
||||||
richPresence.getSyncId(), richPresence.getDetails(), richPresence.getState().replace(";", ","),
|
|
||||||
richPresence.getLargeImage().getText(), richPresence.getLargeImage().getUrl(), trackUrl,trackProgress,
|
|
||||||
trackLength, started, ends
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,7 +3,7 @@ package me.braydon.tether.model.response;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import me.braydon.tether.model.DiscordUser;
|
import me.braydon.tether.model.user.DiscordUser;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A response for a successful Discord user request.
|
* A response for a successful Discord user request.
|
||||||
|
40
API/src/main/java/me/braydon/tether/model/user/Banner.java
Normal file
40
API/src/main/java/me/braydon/tether/model/user/Banner.java
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package me.braydon.tether.model.user;
|
||||||
|
|
||||||
|
import kong.unirest.core.json.JSONObject;
|
||||||
|
import lombok.*;
|
||||||
|
import me.braydon.tether.common.DiscordUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link DiscordUser}'s banner.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter @EqualsAndHashCode
|
||||||
|
public class Banner {
|
||||||
|
private static final String BANNER_URL = "https://cdn.discordapp.com/banners/%s/%s.%s";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the user's banner.
|
||||||
|
*/
|
||||||
|
@NonNull private final String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL of the user's banner.
|
||||||
|
*/
|
||||||
|
@NonNull private final String url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a banner for a user.
|
||||||
|
*
|
||||||
|
* @param userSnowflake the snowflake of the user the avatar belongs to
|
||||||
|
* @param detailsJson the user details json
|
||||||
|
* @return the constructed banner, if any
|
||||||
|
*/
|
||||||
|
protected static Banner fromJson(long userSnowflake, @NonNull JSONObject detailsJson) {
|
||||||
|
String bannerId = detailsJson.optString("banner", null);
|
||||||
|
if (bannerId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Banner(bannerId, BANNER_URL.formatted(userSnowflake, bannerId, DiscordUtils.getMediaExtension(bannerId)));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package me.braydon.tether.model.user;
|
||||||
|
|
||||||
|
import kong.unirest.core.json.JSONObject;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A model representing a cached Discord user.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor @Getter
|
||||||
|
public final class CachedDiscordUser {
|
||||||
|
/**
|
||||||
|
* The Json object for the user's data.
|
||||||
|
*/
|
||||||
|
@NonNull private final JSONObject userJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unix time of when this user was cached.
|
||||||
|
*/
|
||||||
|
private final long cached;
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
package me.braydon.tether.model.user;
|
||||||
|
|
||||||
|
import kong.unirest.core.json.JSONObject;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A linked connection to a {@link DiscordUser}.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter @EqualsAndHashCode
|
||||||
|
public class ConnectedAccount {
|
||||||
|
/**
|
||||||
|
* The id of this account.
|
||||||
|
*/
|
||||||
|
@NonNull private final String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of this account.
|
||||||
|
*/
|
||||||
|
@NonNull private final String type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of this account.
|
||||||
|
*/
|
||||||
|
@NonNull private final String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this account is verified.
|
||||||
|
*/
|
||||||
|
private final boolean verified;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a connected account for a user.
|
||||||
|
*
|
||||||
|
* @param accountJson the connected account json
|
||||||
|
* @return the constructed account
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected static ConnectedAccount fromJson(@NonNull JSONObject accountJson) {
|
||||||
|
String id = accountJson.getString("id");
|
||||||
|
String type = accountJson.getString("type");
|
||||||
|
String name = accountJson.getString("name");
|
||||||
|
boolean verified = accountJson.getBoolean("verified");
|
||||||
|
return new ConnectedAccount(id, type, name, verified);
|
||||||
|
}
|
||||||
|
}
|
210
API/src/main/java/me/braydon/tether/model/user/DiscordUser.java
Normal file
210
API/src/main/java/me/braydon/tether/model/user/DiscordUser.java
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
package me.braydon.tether.model.user;
|
||||||
|
|
||||||
|
import kong.unirest.core.json.JSONArray;
|
||||||
|
import kong.unirest.core.json.JSONObject;
|
||||||
|
import lombok.*;
|
||||||
|
import me.braydon.tether.common.DiscordUtils;
|
||||||
|
import me.braydon.tether.model.user.avatar.Avatar;
|
||||||
|
import me.braydon.tether.model.user.avatar.AvatarDecoration;
|
||||||
|
import me.braydon.tether.model.user.clan.Clan;
|
||||||
|
import me.braydon.tether.model.user.nitro.NitroSubscription;
|
||||||
|
import net.dv8tion.jda.api.OnlineStatus;
|
||||||
|
import net.dv8tion.jda.api.entities.Activity;
|
||||||
|
import net.dv8tion.jda.api.entities.ClientType;
|
||||||
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A model of a Discord user.
|
||||||
|
* <p>
|
||||||
|
* This model will contain ALL
|
||||||
|
* data returned from Discord.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor @Getter @EqualsAndHashCode @ToString
|
||||||
|
public final class DiscordUser {
|
||||||
|
/**
|
||||||
|
* The unique snowflake of this user.
|
||||||
|
*/
|
||||||
|
@EqualsAndHashCode.Include private final long snowflake;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The username of this user.
|
||||||
|
*/
|
||||||
|
@NonNull private final String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The display name of this user, if any.
|
||||||
|
*/
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user's discriminator, 0 if not legacy.
|
||||||
|
*/
|
||||||
|
private final int discriminator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The flags of this user.
|
||||||
|
*/
|
||||||
|
@NonNull private final UserFlags flags;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The avatar of this user.
|
||||||
|
*/
|
||||||
|
@NonNull private final Avatar avatar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The avatar decoration of this user, if any.
|
||||||
|
*/
|
||||||
|
private final AvatarDecoration avatarDecoration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The banner of this user, if any.
|
||||||
|
*/
|
||||||
|
private final Banner banner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The banner color (hex) of this user, if any.
|
||||||
|
*/
|
||||||
|
private final String bannerColor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user's bio, if any.
|
||||||
|
*/
|
||||||
|
private final String bio;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The accent color (hex) of this user.
|
||||||
|
*/
|
||||||
|
@NonNull private final String accentColor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The online status of this user.
|
||||||
|
*/
|
||||||
|
@NonNull private final OnlineStatus onlineStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The clients this user is active on, if known.
|
||||||
|
*/
|
||||||
|
@NonNull private final EnumSet<ClientType> activeClients;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The activities of this user, if known.
|
||||||
|
*/
|
||||||
|
private final List<Activity> activities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Spotify activity of this user, if known.
|
||||||
|
*/
|
||||||
|
@EqualsAndHashCode.Exclude private final SpotifyActivity spotify;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The connected accounts of this user.
|
||||||
|
*/
|
||||||
|
@NonNull private final Set<ConnectedAccount> connectedAccounts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The clan this user is in, if any.
|
||||||
|
*/
|
||||||
|
private final Clan clan;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This user's Nitro subscription, if any.
|
||||||
|
*/
|
||||||
|
private final NitroSubscription nitroSubscription;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is this user a bot?
|
||||||
|
*/
|
||||||
|
private final boolean bot;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this user is a legacy user.
|
||||||
|
* <p>
|
||||||
|
* A user is "legacy" if they haven't yet
|
||||||
|
* moved to the new username system and got
|
||||||
|
* rid of their discriminator.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private final boolean legacy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unix time of when this user joined Discord.
|
||||||
|
*/
|
||||||
|
private final long createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a Discord user from the
|
||||||
|
* raw entities returned from Discord.
|
||||||
|
*
|
||||||
|
* @param userJson the Json object for the user's data
|
||||||
|
* @param member the raw member entity, if any
|
||||||
|
* @return the constructed user
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static DiscordUser buildFromEntity(@NonNull JSONObject userJson, Member member) {
|
||||||
|
JSONObject detailsJson = userJson.has("user") ? userJson.getJSONObject("user") : userJson;
|
||||||
|
|
||||||
|
long snowflake = Long.parseLong(detailsJson.getString("id"));
|
||||||
|
String username = detailsJson.getString("username");
|
||||||
|
String displayName = detailsJson.optString("global_name", null);
|
||||||
|
int discriminator = Integer.parseInt(detailsJson.getString("discriminator"));
|
||||||
|
boolean isUserLegacy = discriminator > 0;
|
||||||
|
UserFlags flags = UserFlags.fromJson(detailsJson);
|
||||||
|
|
||||||
|
Avatar avatar = Avatar.fromJson(snowflake, discriminator, isUserLegacy, detailsJson);
|
||||||
|
AvatarDecoration avatarDecoration = detailsJson.isNull("avatar_decoration_data") ? null
|
||||||
|
: AvatarDecoration.fromJson(detailsJson.getJSONObject("avatar_decoration_data"));
|
||||||
|
Banner banner = Banner.fromJson(snowflake, detailsJson);
|
||||||
|
String bannerColor = detailsJson.optString("banner_color", null);
|
||||||
|
|
||||||
|
String bio = detailsJson.optString("bio", null);
|
||||||
|
String accentColor = String.format("#%06X", detailsJson.isNull("accent_color") ? 0xFFFFFF
|
||||||
|
: 0xFFFFFF & detailsJson.getInt("accent_color"));
|
||||||
|
Clan clan = detailsJson.isNull("clan") ? null : Clan.fromJson(detailsJson.getJSONObject("clan"));
|
||||||
|
NitroSubscription nitroSubscription = userJson.isNull("premium_type")
|
||||||
|
|| userJson.isNull("premium_since") ? null : NitroSubscription.fromJson(userJson);
|
||||||
|
|
||||||
|
boolean bot = detailsJson.optBoolean("bot", false);
|
||||||
|
long created = DiscordUtils.getTimeCreated(snowflake);
|
||||||
|
|
||||||
|
// Get the user's online status
|
||||||
|
OnlineStatus onlineStatus = member == null ? OnlineStatus.OFFLINE : member.getOnlineStatus();
|
||||||
|
if (onlineStatus == OnlineStatus.UNKNOWN) {
|
||||||
|
onlineStatus = OnlineStatus.OFFLINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the user's active clients and activities
|
||||||
|
EnumSet<ClientType> activeClients = member == null ? EnumSet.noneOf(ClientType.class) : member.getActiveClients();
|
||||||
|
List<Activity> activities = member == null ? null : member.getActivities();
|
||||||
|
SpotifyActivity spotify = null;
|
||||||
|
if (activities != null) {
|
||||||
|
for (Activity activity : activities) {
|
||||||
|
if (!activity.getName().equals("Spotify") || !activity.isRich()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
spotify = SpotifyActivity.fromActivity(Objects.requireNonNull(activity.asRichPresence()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the user's connected accounts
|
||||||
|
Set<ConnectedAccount> connectedAccounts = new HashSet<>();
|
||||||
|
if (userJson.has("connected_accounts")) {
|
||||||
|
JSONArray accounts = userJson.getJSONArray("connected_accounts");
|
||||||
|
for (int i = 0; i < accounts.length(); i++) {
|
||||||
|
connectedAccounts.add(ConnectedAccount.fromJson(accounts.getJSONObject(i)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally return the constructed user
|
||||||
|
return new DiscordUser(
|
||||||
|
snowflake, username, displayName, discriminator, flags, avatar, avatarDecoration, banner, bannerColor, bio,
|
||||||
|
accentColor, onlineStatus, activeClients, activities, spotify, connectedAccounts, clan, nitroSubscription,
|
||||||
|
bot, isUserLegacy, created
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
package me.braydon.tether.model.user;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import net.dv8tion.jda.api.entities.RichPresence;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Spotify activity data
|
||||||
|
* of a {@link DiscordUser}.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter
|
||||||
|
public class SpotifyActivity {
|
||||||
|
/**
|
||||||
|
* The ID of the currently playing track.
|
||||||
|
*/
|
||||||
|
@NonNull private final String trackId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the currently playing track.
|
||||||
|
*/
|
||||||
|
@NonNull private final String song;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently playing artist.
|
||||||
|
*/
|
||||||
|
@NonNull private final String artist;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The album the song is from.
|
||||||
|
*/
|
||||||
|
@NonNull private final String album;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL to the art for the currently playing album.
|
||||||
|
*/
|
||||||
|
@NonNull private final String albumArtUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL to the playing track.
|
||||||
|
*/
|
||||||
|
@NonNull private final String trackUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current progress of the track (in millis).
|
||||||
|
*/
|
||||||
|
private final long trackProgress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total length of the track (in millis).
|
||||||
|
*/
|
||||||
|
private final long trackLength;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unix time of when this track started playing.
|
||||||
|
*/
|
||||||
|
private final long started;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unix time of when this track stops playing.
|
||||||
|
*/
|
||||||
|
private final long ends;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a Spotify activity for a user.
|
||||||
|
*
|
||||||
|
* @param richPresence the raw Discord data
|
||||||
|
* @return the constructed activity
|
||||||
|
*/
|
||||||
|
@NonNull @SuppressWarnings("DataFlowIssue")
|
||||||
|
protected static SpotifyActivity fromActivity(@NonNull RichPresence richPresence) {
|
||||||
|
String trackUrl = "https://open.spotify.com/track/" + richPresence.getSyncId();
|
||||||
|
|
||||||
|
// Track progress
|
||||||
|
long started = Objects.requireNonNull(richPresence.getTimestamps()).getStart();
|
||||||
|
long ends = richPresence.getTimestamps().getEnd();
|
||||||
|
|
||||||
|
long trackLength = ends - started;
|
||||||
|
long trackProgress = Math.min(System.currentTimeMillis() - started, trackLength);
|
||||||
|
|
||||||
|
return new SpotifyActivity(
|
||||||
|
richPresence.getSyncId(), richPresence.getDetails(), richPresence.getState().replace(";", ","),
|
||||||
|
richPresence.getLargeImage().getText(), richPresence.getLargeImage().getUrl(), trackUrl, trackProgress,
|
||||||
|
trackLength, started, ends
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package me.braydon.tether.model.user;
|
||||||
|
|
||||||
|
import kong.unirest.core.json.JSONObject;
|
||||||
|
import lombok.*;
|
||||||
|
import net.dv8tion.jda.api.entities.User;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The flag's of a {@link DiscordUser}.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter @EqualsAndHashCode
|
||||||
|
public class UserFlags {
|
||||||
|
/**
|
||||||
|
* The list of flags the user has.
|
||||||
|
*/
|
||||||
|
@NonNull private final EnumSet<User.UserFlag> list;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The raw flags the user has.
|
||||||
|
*/
|
||||||
|
private final int raw;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct the flags for a user.
|
||||||
|
*
|
||||||
|
* @param detailsJson the user details json
|
||||||
|
* @return the constructed flags
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected static UserFlags fromJson(@NonNull JSONObject detailsJson) {
|
||||||
|
int flags = detailsJson.getInt("flags");
|
||||||
|
return new UserFlags(User.UserFlag.getFlags(flags), flags);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package me.braydon.tether.model.user.avatar;
|
||||||
|
|
||||||
|
import kong.unirest.core.json.JSONObject;
|
||||||
|
import lombok.*;
|
||||||
|
import me.braydon.tether.common.DiscordUtils;
|
||||||
|
import me.braydon.tether.model.user.DiscordUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link DiscordUser}'s avatar.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter @EqualsAndHashCode
|
||||||
|
public class Avatar {
|
||||||
|
private static final String DEFAULT_AVATAR_URL = "https://cdn.discordapp.com/embed/avatars/%s.png";
|
||||||
|
private static final String AVATAR_URL = "https://cdn.discordapp.com/avatars/%s/%s.%s";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the user's avatar.
|
||||||
|
*/
|
||||||
|
@NonNull private final String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL of the user's avatar.
|
||||||
|
*/
|
||||||
|
@NonNull private final String url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an avatar for a user.
|
||||||
|
*
|
||||||
|
* @param userSnowflake the snowflake of the user the avatar belongs to
|
||||||
|
* @param userDiscriminator the snowflake of the user the avatar belongs to
|
||||||
|
* @param isUserLegacy whether the user is legacy
|
||||||
|
* @param detailsJson the user details json
|
||||||
|
* @return the constructed avatar
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static Avatar fromJson(long userSnowflake, int userDiscriminator, boolean isUserLegacy, @NonNull JSONObject detailsJson) {
|
||||||
|
String avatarId = detailsJson.getString("avatar");
|
||||||
|
String avatarUrl;
|
||||||
|
if (avatarId == null) { // Fallback to the default avatar
|
||||||
|
avatarId = String.valueOf(isUserLegacy ? userDiscriminator % 5 : (userDiscriminator >> 22) % 6);
|
||||||
|
avatarUrl = DEFAULT_AVATAR_URL.formatted(avatarId);
|
||||||
|
} else {
|
||||||
|
avatarUrl = AVATAR_URL.formatted(userSnowflake, avatarId, DiscordUtils.getMediaExtension(avatarId));
|
||||||
|
}
|
||||||
|
return new Avatar(avatarId, avatarUrl);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package me.braydon.tether.model.user.avatar;
|
||||||
|
|
||||||
|
import kong.unirest.core.json.JSONObject;
|
||||||
|
import lombok.*;
|
||||||
|
import me.braydon.tether.model.user.DiscordUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The avatar decoration
|
||||||
|
* of a {@link DiscordUser}.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter @EqualsAndHashCode
|
||||||
|
public class AvatarDecoration {
|
||||||
|
/**
|
||||||
|
* The asset of this decoration.
|
||||||
|
*/
|
||||||
|
@NonNull private final DecorationAsset asset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the decoration sku.
|
||||||
|
*/
|
||||||
|
@NonNull private final String skuId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unix time of when this decorations expires, null if permanent.
|
||||||
|
*/
|
||||||
|
private final Long expires;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an avatar decoration for a user.
|
||||||
|
*
|
||||||
|
* @param decorationJson the decoration json
|
||||||
|
* @return the constructed decoration
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static AvatarDecoration fromJson(@NonNull JSONObject decorationJson) {
|
||||||
|
DecorationAsset asset = DecorationAsset.fromJson(decorationJson);
|
||||||
|
String skuId = decorationJson.getString("sku_id");
|
||||||
|
long expires = decorationJson.optLong("expires_at", -1L);
|
||||||
|
return new AvatarDecoration(asset, skuId, expires == -1L ? null : expires);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package me.braydon.tether.model.user.avatar;
|
||||||
|
|
||||||
|
import kong.unirest.core.json.JSONObject;
|
||||||
|
import lombok.*;
|
||||||
|
import me.braydon.tether.model.user.DiscordUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link DiscordUser}'s avatar.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter @EqualsAndHashCode
|
||||||
|
public class DecorationAsset {
|
||||||
|
private static final String DECORATION_URL = "https://cdn.discordapp.com/avatar-decoration-presets/%s.png";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the clan badge.
|
||||||
|
*/
|
||||||
|
@NonNull private final String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL of the clan badge.
|
||||||
|
*/
|
||||||
|
@NonNull private final String url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an avatar decoration asset for a user.
|
||||||
|
*
|
||||||
|
* @param decorationJson the clan json
|
||||||
|
* @return the constructed asset
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected static DecorationAsset fromJson(@NonNull JSONObject decorationJson) {
|
||||||
|
String badgeId = decorationJson.getString("asset");
|
||||||
|
return new DecorationAsset(badgeId, DECORATION_URL.formatted(badgeId));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package me.braydon.tether.model.user.clan;
|
||||||
|
|
||||||
|
import kong.unirest.core.json.JSONObject;
|
||||||
|
import lombok.*;
|
||||||
|
import me.braydon.tether.common.DiscordUtils;
|
||||||
|
import me.braydon.tether.model.user.DiscordUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link DiscordUser}'s avatar.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter @EqualsAndHashCode
|
||||||
|
public class Badge {
|
||||||
|
private static final String CLAN_BADGE_URL = "https://cdn.discordapp.com/clan-badges/%s/%s.%s";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the clan badge.
|
||||||
|
*/
|
||||||
|
@NonNull private final String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL of the clan badge.
|
||||||
|
*/
|
||||||
|
@NonNull private final String url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a badge for a clan.
|
||||||
|
*
|
||||||
|
* @param userSnowflake the user's snowflake
|
||||||
|
* @param clanJson the clan json
|
||||||
|
* @return the constructed clan badge
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected static Badge fromJson(long userSnowflake, @NonNull JSONObject clanJson) {
|
||||||
|
String badgeId = clanJson.getString("badge");
|
||||||
|
return new Badge(badgeId, CLAN_BADGE_URL.formatted(userSnowflake, badgeId, DiscordUtils.getMediaExtension(badgeId)));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
package me.braydon.tether.model.user.clan;
|
||||||
|
|
||||||
|
import kong.unirest.core.json.JSONObject;
|
||||||
|
import lombok.*;
|
||||||
|
import me.braydon.tether.model.user.DiscordUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The clan of a {@link DiscordUser}.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter @EqualsAndHashCode
|
||||||
|
public class Clan {
|
||||||
|
/**
|
||||||
|
* The snowflake of the Guild this clan belongs to.
|
||||||
|
*/
|
||||||
|
private final long guildSnowflake;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tag of this clan.
|
||||||
|
*/
|
||||||
|
@NonNull private final String tag;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The badge for this clan.
|
||||||
|
*/
|
||||||
|
@NonNull private final Badge badge;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the identity is enabled for this clan.
|
||||||
|
*/
|
||||||
|
private final boolean identityEnabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a clan for a user.
|
||||||
|
*
|
||||||
|
* @param clanJson the user details json
|
||||||
|
* @return the constructed clan
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static Clan fromJson(@NonNull JSONObject clanJson) {
|
||||||
|
long snowflake = Long.parseLong(clanJson.getString("identity_guild_id"));
|
||||||
|
String tag = clanJson.getString("tag");
|
||||||
|
Badge badge = Badge.fromJson(snowflake, clanJson);
|
||||||
|
boolean identityEnabled = clanJson.getBoolean("identity_enabled");
|
||||||
|
return new Clan(snowflake, tag, badge, identityEnabled);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
package me.braydon.tether.model.user.nitro;
|
||||||
|
|
||||||
|
import kong.unirest.core.json.JSONObject;
|
||||||
|
import lombok.*;
|
||||||
|
import me.braydon.tether.model.user.DiscordUser;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Nitro subscription for a {@link DiscordUser}.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter @EqualsAndHashCode
|
||||||
|
public class NitroSubscription {
|
||||||
|
/**
|
||||||
|
* The type of this subscription.
|
||||||
|
*/
|
||||||
|
@NonNull private final NitroType type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unix time this subscription was started.
|
||||||
|
*/
|
||||||
|
private final long subscribed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a Nitro subscription for a user.
|
||||||
|
*
|
||||||
|
* @param userJson the user json
|
||||||
|
* @return the constructed subscription
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static NitroSubscription fromJson(@NonNull JSONObject userJson) {
|
||||||
|
NitroType type = NitroType.byType(userJson.getInt("premium_type"));
|
||||||
|
long subscribed = ZonedDateTime.parse(userJson.getString("premium_since")).toInstant().toEpochMilli();
|
||||||
|
return new NitroSubscription(type, subscribed);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package me.braydon.tether.model.user.nitro;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Different types of Nitro subscriptions.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
public enum NitroType {
|
||||||
|
CLASSIC, NITRO, BASIC, UNKNOWN;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the nitro type
|
||||||
|
* from the given raw type.
|
||||||
|
*
|
||||||
|
* @param type the raw type
|
||||||
|
* @return the nitro type
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static NitroType byType(int type) {
|
||||||
|
for (NitroType nitroType : values()) {
|
||||||
|
if (type == nitroType.ordinal() + 1) {
|
||||||
|
return nitroType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@ public final class PacketRegistry {
|
|||||||
* A registry of packets, identified by their op code.
|
* A registry of packets, identified by their op code.
|
||||||
*/
|
*/
|
||||||
private static final Map<Integer, Class<? extends Packet>> REGISTRY = Collections.synchronizedMap(new HashMap<>());
|
private static final Map<Integer, Class<? extends Packet>> REGISTRY = Collections.synchronizedMap(new HashMap<>());
|
||||||
|
|
||||||
static {
|
static {
|
||||||
register(OpCode.LISTEN_TO_USER, ListenToUserPacket.class);
|
register(OpCode.LISTEN_TO_USER, ListenToUserPacket.class);
|
||||||
register(OpCode.USER_STATUS, UserStatusPacket.class);
|
register(OpCode.USER_STATUS, UserStatusPacket.class);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package me.braydon.tether.packet.impl.websocket.user;
|
package me.braydon.tether.packet.impl.websocket.user;
|
||||||
|
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import me.braydon.tether.model.DiscordUser;
|
import me.braydon.tether.model.user.DiscordUser;
|
||||||
import me.braydon.tether.packet.OpCode;
|
import me.braydon.tether.packet.OpCode;
|
||||||
import me.braydon.tether.packet.Packet;
|
import me.braydon.tether.packet.Packet;
|
||||||
|
|
||||||
|
@ -3,22 +3,31 @@ package me.braydon.tether.service;
|
|||||||
import com.github.benmanes.caffeine.cache.Cache;
|
import com.github.benmanes.caffeine.cache.Cache;
|
||||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import kong.unirest.core.HttpResponse;
|
||||||
|
import kong.unirest.core.HttpStatus;
|
||||||
|
import kong.unirest.core.JsonNode;
|
||||||
|
import kong.unirest.core.Unirest;
|
||||||
|
import kong.unirest.core.json.JSONObject;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
import me.braydon.tether.exception.impl.BadRequestException;
|
import me.braydon.tether.exception.impl.BadRequestException;
|
||||||
import me.braydon.tether.exception.impl.ResourceNotFoundException;
|
import me.braydon.tether.exception.impl.ResourceNotFoundException;
|
||||||
import me.braydon.tether.exception.impl.ServiceUnavailableException;
|
import me.braydon.tether.exception.impl.ServiceUnavailableException;
|
||||||
import me.braydon.tether.model.CachedDiscordUser;
|
|
||||||
import me.braydon.tether.model.DiscordUser;
|
|
||||||
import me.braydon.tether.model.response.DiscordUserResponse;
|
import me.braydon.tether.model.response.DiscordUserResponse;
|
||||||
|
import me.braydon.tether.model.user.CachedDiscordUser;
|
||||||
|
import me.braydon.tether.model.user.DiscordUser;
|
||||||
import net.dv8tion.jda.api.JDA;
|
import net.dv8tion.jda.api.JDA;
|
||||||
import net.dv8tion.jda.api.JDABuilder;
|
import net.dv8tion.jda.api.JDABuilder;
|
||||||
import net.dv8tion.jda.api.entities.*;
|
import net.dv8tion.jda.api.entities.Activity;
|
||||||
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
|
import net.dv8tion.jda.api.entities.SelfUser;
|
||||||
import net.dv8tion.jda.api.exceptions.ErrorResponseException;
|
import net.dv8tion.jda.api.exceptions.ErrorResponseException;
|
||||||
import net.dv8tion.jda.api.requests.GatewayIntent;
|
import net.dv8tion.jda.api.requests.GatewayIntent;
|
||||||
import net.dv8tion.jda.api.utils.cache.CacheFlag;
|
import net.dv8tion.jda.api.utils.cache.CacheFlag;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -32,14 +41,6 @@ import java.util.concurrent.TimeUnit;
|
|||||||
@Service
|
@Service
|
||||||
@Log4j2(topic = "Discord")
|
@Log4j2(topic = "Discord")
|
||||||
public final class DiscordService {
|
public final class DiscordService {
|
||||||
@Value("${discord.bot-token}")
|
|
||||||
private String botToken;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current instance of the Discord bot.
|
|
||||||
*/
|
|
||||||
private JDA jda;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A cache of users retrieved from Discord.
|
* A cache of users retrieved from Discord.
|
||||||
*/
|
*/
|
||||||
@ -47,6 +48,17 @@ public final class DiscordService {
|
|||||||
.expireAfterAccess(3L, TimeUnit.MINUTES)
|
.expireAfterAccess(3L, TimeUnit.MINUTES)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
@Value("${discord.bot-token}")
|
||||||
|
private String botToken;
|
||||||
|
|
||||||
|
@Value("${discord.user-account-token}")
|
||||||
|
private String userAccountToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current instance of the Discord bot.
|
||||||
|
*/
|
||||||
|
private JDA jda;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void onInitialize() {
|
public void onInitialize() {
|
||||||
connectBot();
|
connectBot();
|
||||||
@ -58,7 +70,7 @@ public final class DiscordService {
|
|||||||
* @param rawSnowflake the user snowflake
|
* @param rawSnowflake the user snowflake
|
||||||
* @return the user response
|
* @return the user response
|
||||||
* @throws ServiceUnavailableException if the bot is not connected
|
* @throws ServiceUnavailableException if the bot is not connected
|
||||||
* @throws ResourceNotFoundException if the user is not found
|
* @throws ResourceNotFoundException if the user is not found
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public DiscordUserResponse getUserBySnowflake(@NonNull String rawSnowflake) throws BadRequestException, ServiceUnavailableException, ResourceNotFoundException {
|
public DiscordUserResponse getUserBySnowflake(@NonNull String rawSnowflake) throws BadRequestException, ServiceUnavailableException, ResourceNotFoundException {
|
||||||
@ -66,7 +78,7 @@ public final class DiscordService {
|
|||||||
try {
|
try {
|
||||||
snowflake = Long.parseLong(rawSnowflake);
|
snowflake = Long.parseLong(rawSnowflake);
|
||||||
} catch (NumberFormatException ex) {
|
} catch (NumberFormatException ex) {
|
||||||
throw new BadRequestException("Not a valid snowflake");
|
throw new BadRequestException("Not a valid snowflake.");
|
||||||
}
|
}
|
||||||
return getUserBySnowflake(snowflake);
|
return getUserBySnowflake(snowflake);
|
||||||
}
|
}
|
||||||
@ -77,7 +89,7 @@ public final class DiscordService {
|
|||||||
* @param snowflake the user snowflake
|
* @param snowflake the user snowflake
|
||||||
* @return the user response
|
* @return the user response
|
||||||
* @throws ServiceUnavailableException if the bot is not connected
|
* @throws ServiceUnavailableException if the bot is not connected
|
||||||
* @throws ResourceNotFoundException if the user is not found
|
* @throws ResourceNotFoundException if the user is not found
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public DiscordUserResponse getUserBySnowflake(long snowflake) throws BadRequestException, ServiceUnavailableException, ResourceNotFoundException {
|
public DiscordUserResponse getUserBySnowflake(long snowflake) throws BadRequestException, ServiceUnavailableException, ResourceNotFoundException {
|
||||||
@ -91,15 +103,13 @@ public final class DiscordService {
|
|||||||
CachedDiscordUser cachedUser = cachedUsers.getIfPresent(snowflake);
|
CachedDiscordUser cachedUser = cachedUsers.getIfPresent(snowflake);
|
||||||
boolean fromCache = cachedUser != null;
|
boolean fromCache = cachedUser != null;
|
||||||
if (cachedUser == null) { // No cache, retrieve fresh data
|
if (cachedUser == null) { // No cache, retrieve fresh data
|
||||||
User user = jda.retrieveUserById(snowflake).complete();
|
cachedUser = new CachedDiscordUser(getUser(snowflake, member != null), System.currentTimeMillis());
|
||||||
cachedUser = new CachedDiscordUser(
|
|
||||||
user, user.retrieveProfile().complete(), System.currentTimeMillis()
|
|
||||||
);
|
|
||||||
cachedUsers.put(snowflake, cachedUser);
|
cachedUsers.put(snowflake, cachedUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally build the response and respond with it
|
// Finally build the response and respond with it
|
||||||
return new DiscordUserResponse(
|
return new DiscordUserResponse(
|
||||||
DiscordUser.buildFromEntity(cachedUser.getUser(), cachedUser.getProfile(), member),
|
DiscordUser.buildFromEntity(cachedUser.getUserJson(), member),
|
||||||
fromCache ? cachedUser.getCached() : -1L
|
fromCache ? cachedUser.getCached() : -1L
|
||||||
);
|
);
|
||||||
} catch (ErrorResponseException ex) {
|
} catch (ErrorResponseException ex) {
|
||||||
@ -133,6 +143,25 @@ public final class DiscordService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user with the given snowflake from Discord.
|
||||||
|
*
|
||||||
|
* @param snowflake the user snowflake
|
||||||
|
* @param includeProfile whether to include the user's profile
|
||||||
|
* @return the user json object
|
||||||
|
* @throws BadRequestException if the request fails
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private JSONObject getUser(long snowflake, boolean includeProfile) throws BadRequestException {
|
||||||
|
HttpResponse<JsonNode> response = Unirest.get("https://discord.com/api/v10/users/" + snowflake + (includeProfile ? "/profile?with_mutual_guilds=false" : ""))
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, userAccountToken).asJson();
|
||||||
|
JSONObject json = response.getBody().getObject();
|
||||||
|
if (response.getStatus() == HttpStatus.OK) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
throw new BadRequestException(json.getInt("code") + ": " + json.getString("message"));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a member from a guild by their snowflake.
|
* Get a member from a guild by their snowflake.
|
||||||
*
|
*
|
||||||
@ -148,6 +177,7 @@ public final class DiscordService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ErrorResponseException ex) {
|
} catch (ErrorResponseException ex) {
|
||||||
|
// Ignore if the member is not in the guild
|
||||||
if (ex.getErrorCode() != 10007) {
|
if (ex.getErrorCode() != 10007) {
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import me.braydon.tether.config.AppConfig;
|
|||||||
import me.braydon.tether.exception.impl.BadRequestException;
|
import me.braydon.tether.exception.impl.BadRequestException;
|
||||||
import me.braydon.tether.exception.impl.ResourceNotFoundException;
|
import me.braydon.tether.exception.impl.ResourceNotFoundException;
|
||||||
import me.braydon.tether.exception.impl.ServiceUnavailableException;
|
import me.braydon.tether.exception.impl.ServiceUnavailableException;
|
||||||
import me.braydon.tether.model.DiscordUser;
|
import me.braydon.tether.model.user.DiscordUser;
|
||||||
import me.braydon.tether.packet.Packet;
|
import me.braydon.tether.packet.Packet;
|
||||||
import me.braydon.tether.packet.PacketRegistry;
|
import me.braydon.tether.packet.PacketRegistry;
|
||||||
import me.braydon.tether.packet.impl.websocket.misc.ErrorMessagePacket;
|
import me.braydon.tether.packet.impl.websocket.misc.ErrorMessagePacket;
|
||||||
@ -147,7 +147,7 @@ public class WebSocket extends TextWebSocketHandler {
|
|||||||
* Send a packet to the given session.
|
* Send a packet to the given session.
|
||||||
*
|
*
|
||||||
* @param session the session to send to
|
* @param session the session to send to
|
||||||
* @param packet the packet to send
|
* @param packet the packet to send
|
||||||
*/
|
*/
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
private void dispatch(@NonNull WebSocketSession session, @NonNull Packet packet) {
|
private void dispatch(@NonNull WebSocketSession session, @NonNull Packet packet) {
|
||||||
|
@ -3,7 +3,7 @@ package me.braydon.tether.service.websocket;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import me.braydon.tether.model.DiscordUser;
|
import me.braydon.tether.model.user.DiscordUser;
|
||||||
import org.springframework.web.socket.WebSocketSession;
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -18,13 +18,15 @@ public class WebSocketClient {
|
|||||||
* The session this client is for.
|
* The session this client is for.
|
||||||
*/
|
*/
|
||||||
@NonNull private final WebSocketSession session;
|
@NonNull private final WebSocketSession session;
|
||||||
|
/**
|
||||||
|
* The unix time this client connected.
|
||||||
|
*/
|
||||||
|
private final long connected;
|
||||||
/**
|
/**
|
||||||
* The snowflake of the user this client
|
* The snowflake of the user this client
|
||||||
* is listening to for updates, if any.
|
* is listening to for updates, if any.
|
||||||
*/
|
*/
|
||||||
private Long listeningTo;
|
private Long listeningTo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The last user this client
|
* The last user this client
|
||||||
* has been sent a status for.
|
* has been sent a status for.
|
||||||
@ -35,11 +37,6 @@ public class WebSocketClient {
|
|||||||
*/
|
*/
|
||||||
private DiscordUser lastUser;
|
private DiscordUser lastUser;
|
||||||
|
|
||||||
/**
|
|
||||||
* The unix time this client connected.
|
|
||||||
*/
|
|
||||||
private final long connected;
|
|
||||||
|
|
||||||
protected WebSocketClient(@NonNull WebSocketSession session) {
|
protected WebSocketClient(@NonNull WebSocketSession session) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
connected = System.currentTimeMillis();
|
connected = System.currentTimeMillis();
|
||||||
|
@ -10,6 +10,10 @@ logging:
|
|||||||
|
|
||||||
# Discord Configuration
|
# Discord Configuration
|
||||||
discord:
|
discord:
|
||||||
|
# The user account token also for general API calls
|
||||||
|
user-account-token: "CHANGE_ME"
|
||||||
|
|
||||||
|
# The bot token for realtime API calls (online status, activities, etc)
|
||||||
bot-token: "CHANGE_ME"
|
bot-token: "CHANGE_ME"
|
||||||
|
|
||||||
# Spring Configuration
|
# Spring Configuration
|
||||||
|
@ -4,4 +4,4 @@ An API designed to provide real-time access to a user's Discord data.
|
|||||||
## TODO
|
## TODO
|
||||||
- [x] Caching
|
- [x] Caching
|
||||||
- [x] WebSockets
|
- [x] WebSockets
|
||||||
- [ ] User account for extra data? (about me, connections, etc)
|
- [x] User account for extra data? (about me, connections, etc)
|
Loading…
x
Reference in New Issue
Block a user