Java tutorial
/* * This file is part of Discord4J. * * Discord4J is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Discord4J is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Discord4J. If not, see <http://www.gnu.org/licenses/>. */ package sx.blah.discord.api.internal; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.module.afterburner.AfterburnerModule; import org.apache.commons.lang3.tuple.Pair; import sx.blah.discord.Discord4J; import sx.blah.discord.api.IDiscordClient; import sx.blah.discord.api.IShard; import sx.blah.discord.api.internal.json.event.PresenceUpdateEventResponse; import sx.blah.discord.api.internal.json.objects.*; import sx.blah.discord.api.internal.json.objects.audit.AuditLogEntryObject; import sx.blah.discord.api.internal.json.objects.audit.AuditLogObject; import sx.blah.discord.handle.audit.ActionType; import sx.blah.discord.handle.audit.AuditLog; import sx.blah.discord.handle.audit.entry.AuditLogEntry; import sx.blah.discord.handle.audit.entry.DiscordObjectEntry; import sx.blah.discord.handle.audit.entry.TargetedEntry; import sx.blah.discord.handle.audit.entry.change.ChangeMap; import sx.blah.discord.handle.audit.entry.option.OptionMap; import sx.blah.discord.handle.impl.obj.*; import sx.blah.discord.handle.obj.*; import sx.blah.discord.util.LogMarkers; import sx.blah.discord.util.LongMapCollector; import sx.blah.discord.util.RequestBuilder; import sx.blah.discord.util.cache.Cache; import sx.blah.discord.util.cache.LongMap; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import java.awt.Color; import java.time.Instant; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; import java.util.stream.Collectors; /** * Collection of internal Discord4J utilities. */ public class DiscordUtils { /** * The version of Discord's API and Gateway used by Discord4J. */ public static final String API_VERSION = "6"; /** * Re-usable instance of jackson. */ public static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new AfterburnerModule()) .enable(SerializationFeature.USE_EQUALITY_FOR_OBJECT_ID) .enable(SerializationFeature.WRITE_NULL_MAP_VALUES) .enable(DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY) .enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).enable(JsonParser.Feature.ALLOW_COMMENTS) .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES).enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES) .enable(JsonParser.Feature.ALLOW_MISSING_VALUES) .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); /** * Like {@link #MAPPER} but it doesn't serialize nulls. */ public static final ObjectMapper MAPPER_NO_NULLS = new ObjectMapper().registerModule(new AfterburnerModule()) .setSerializationInclusion(JsonInclude.Include.NON_NULL) .enable(SerializationFeature.USE_EQUALITY_FOR_OBJECT_ID) .disable(SerializationFeature.WRITE_NULL_MAP_VALUES) .enable(DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY) .enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).enable(JsonParser.Feature.ALLOW_COMMENTS) .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES).enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES) .enable(JsonParser.Feature.ALLOW_MISSING_VALUES) .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); /** * The unix time that represents Discord's epoch. (January 1, 2015). */ public static final long DISCORD_EPOCH = 1420070400000L; /** * Pattern for Discord's custom emoji. */ public static final Pattern CUSTOM_EMOJI_PATTERN = Pattern.compile("<?:[A-Za-z_0-9]+:\\d+>?"); /** * Pattern for naming Discord's custom emoji. */ public static final Pattern EMOJI_NAME_PATTERN = Pattern.compile("([A-Za-z0-9_]{2,32})"); /** * Pattern for Discord's emoji aliases (e.g. :heart: or :thinking:). */ public static final Pattern EMOJI_ALIAS_PATTERN = Pattern.compile(":.+:"); /** * Pattern for Discord's nsfw channel name indicator. */ public static final Pattern NSFW_CHANNEL_PATTERN = Pattern.compile("^nsfw(-|$)"); /** * Pattern for Discord's valid streaming URL strings passed to {@link IShard#streaming(String, String)}. */ public static final Pattern STREAM_URL_PATTERN = Pattern.compile("https?://(www\\.)?twitch\\.tv/.+"); /** * Pattern for Discord's valid channel names. */ public static final Pattern CHANNEL_NAME_PATTERN = Pattern.compile("^[a-z0-9-_[^\\p{ASCII}]]{2,100}$"); /** * Gets a snowflake from a unix timestamp. * <p> * This snowflake only contains accurate information about the timestamp (not about other parts of the snowflake). * The returned snowflake is only one of many that could exist at the given timestamp. * * @param date The date that should be converted to a unix timestamp for use in the snowflake. * @return A snowflake with the given timestamp. */ public static long getSnowflakeFromTimestamp(Instant date) { return (date.toEpochMilli() - DISCORD_EPOCH) << 22; } /** * Converts a String timestamp into a {@link Instant}. * * @param time The string timestamp. * @return The LocalDateTime representing the timestamp. */ public static Instant convertFromTimestamp(String time) { return time == null ? Instant.now() : ZonedDateTime.parse(time).toInstant(); } /** * Converts a json {@link UserObject} to a {@link User}. This method first checks the internal user cache and returns * that object with updated information if it exists. Otherwise, it constructs a new user. * * @param shard The shard the user belongs to. * @param response The json object representing the user. * @return The converted user object. */ public static User getUserFromJSON(IShard shard, UserObject response) { if (response == null) return null; User user; if (shard != null && (user = (User) shard.getUserByID(Long.parseUnsignedLong(response.id))) != null) { user.setAvatar(response.avatar); user.setName(response.username); user.setDiscriminator(response.discriminator); } else { user = new User(shard, response.username, Long.parseUnsignedLong(response.id), response.discriminator, response.avatar, new Presence(null, null, StatusType.OFFLINE, ActivityType.PLAYING), response.bot); } return user; } /** * Converts a json {@link InviteObject} to an {@link IInvite}. * * @param client The client the invite belongs to. * @param json The json object representing the invite. * @return The converted invite object. */ public static IInvite getInviteFromJSON(IDiscordClient client, InviteObject json) { return new Invite(client, json); } /** * Converts a json {@link ExtendedInviteObject} to an {@link IExtendedInvite}. * * @param client The client the invite belongs to. * @param json The json object representing the invite. * @return The converted extended invite object. */ public static IExtendedInvite getExtendedInviteFromJSON(IDiscordClient client, ExtendedInviteObject json) { return new ExtendedInvite(client, json); } /** * Gets the users mentioned in a message. * * @param json The json response to use. * @return The list of IDs of mentioned users. */ public static List<Long> getMentionsFromJSON(MessageObject json) { List<Long> mentions = new ArrayList<>(); if (json.mentions != null) for (UserObject response : json.mentions) mentions.add(Long.parseUnsignedLong(response.id)); return mentions; } /** * Gets the roles mentioned in a message. * * @param json The json response to use. * @return The list IDs of mentioned roles. */ public static List<Long> getRoleMentionsFromJSON(MessageObject json) { List<Long> mentions = new ArrayList<>(); if (json.mention_roles != null) for (String role : json.mention_roles) mentions.add(Long.parseUnsignedLong(role)); return mentions; } /** * Gets the attachments on a message. * * @param json The json response to use. * @return The attachments. */ public static List<IMessage.Attachment> getAttachmentsFromJSON(MessageObject json) { List<IMessage.Attachment> attachments = new ArrayList<>(); if (json.attachments != null) for (MessageObject.AttachmentObject response : json.attachments) { attachments.add(new IMessage.Attachment(response.filename, response.size, Long.parseUnsignedLong(response.id), response.url)); } return attachments; } /** * Gets the embeds on a message. * * @param json The json response to use. * @return The embeds. */ public static List<Embed> getEmbedsFromJSON(MessageObject json) { List<Embed> embeds = new ArrayList<>(); if (json.embeds != null) for (EmbedObject response : json.embeds) { embeds.add(new Embed(response.title, response.type, response.description, response.url, response.thumbnail, response.provider, convertFromTimestamp(response.timestamp), new Color(response.color), response.footer, response.image, response.video, response.author, response.fields)); } return embeds; } /** * Converts a json {@link GuildObject} to a {@link IGuild}. This method first checks the internal guild cache and returns * that object with updated information if it exists. Otherwise, it constructs a new guild. * * @param shard The shard the guild belongs to. * @param json The json object representing the guild. * @return The converted guild object. */ public static IGuild getGuildFromJSON(IShard shard, GuildObject json) { Guild guild; long guildId = Long.parseUnsignedLong(json.id); long systemChannelId = json.system_channel_id == null ? 0L : Long.parseUnsignedLong(json.system_channel_id); if ((guild = (Guild) shard.getGuildByID(guildId)) != null) { guild.setIcon(json.icon); guild.setName(json.name); guild.setOwnerID(Long.parseUnsignedLong(json.owner_id)); guild.setAFKChannel(json.afk_channel_id == null ? 0 : Long.parseUnsignedLong(json.afk_channel_id)); guild.setAfkTimeout(json.afk_timeout); guild.setRegionID(json.region); guild.setVerificationLevel(json.verification_level); guild.setTotalMemberCount(json.member_count); guild.setSystemChannelId(systemChannelId); List<IRole> newRoles = new ArrayList<>(); for (RoleObject roleResponse : json.roles) { newRoles.add(getRoleFromJSON(guild, roleResponse)); } guild.roles.clear(); guild.roles.putAll(newRoles); for (IUser user : guild.getUsers()) { //Removes all deprecated roles for (IRole role : user.getRolesForGuild(guild)) { if (guild.getRoleByID(role.getLongID()) == null) { user.getRolesForGuild(guild).remove(role); } } } } else { guild = new Guild(shard, json.name, guildId, json.icon, Long.parseUnsignedLong(json.owner_id), json.afk_channel_id == null ? 0 : Long.parseUnsignedLong(json.afk_channel_id), json.afk_timeout, json.region, json.verification_level, systemChannelId); if (json.roles != null) for (RoleObject roleResponse : json.roles) { getRoleFromJSON(guild, roleResponse); //Implicitly adds the role to the guild. } guild.setTotalMemberCount(json.member_count); if (json.members != null) { for (MemberObject member : json.members) { IUser user = getUserFromGuildMemberResponse(guild, member); guild.users.put(user); } } if (json.presences != null) for (PresenceObject presence : json.presences) { User user = (User) guild.getUserByID(Long.parseUnsignedLong(presence.user.id)); if (user != null) { user.setPresence(DiscordUtils.getPresenceFromJSON(presence)); } } if (json.channels != null) for (ChannelObject channelJSON : json.channels) { IChannel channel = getChannelFromJSON(shard, guild, channelJSON); if (channelJSON.type == ChannelObject.Type.GUILD_TEXT) { guild.channels.put(channel); } else if (channelJSON.type == ChannelObject.Type.GUILD_VOICE) { guild.voiceChannels.put((IVoiceChannel) channel); } else if (channelJSON.type == ChannelObject.Type.GUILD_CATEGORY) { guild.categories.put(DiscordUtils.getCategoryFromJSON(shard, guild, channelJSON)); } } if (json.voice_states != null) { for (VoiceStateObject voiceState : json.voice_states) { final AtomicReference<IUser> user = new AtomicReference<>( guild.getUserByID(Long.parseUnsignedLong(voiceState.user_id))); if (user.get() == null) { new RequestBuilder(shard.getClient()).shouldBufferRequests(true).doAction(() -> { if (user.get() == null) user.set(shard.fetchUser(Long.parseUnsignedLong(voiceState.user_id))); return true; }).execute(); } if (user.get() != null) ((User) user.get()).voiceStates.put(DiscordUtils.getVoiceStateFromJson(guild, voiceState)); } } } // emoji are always updated guild.emojis.clear(); for (EmojiObject obj : json.emojis) { guild.emojis.put(DiscordUtils.getEmojiFromJSON(guild, obj)); } return guild; } /** * Converts a json {@link MemberObject} to a {@link IUser}. This method uses {@link #getUserFromJSON(IShard, UserObject)} * to get or create a {@link IUser} and then updates the guild's appropriate member caches for that user. * * @param guild The guild the member belongs to. * @param json The json object representing the member. * @return The converted user object. */ public static IUser getUserFromGuildMemberResponse(IGuild guild, MemberObject json) { User user = getUserFromJSON(guild.getShard(), json.user); for (String role : json.roles) { Role roleObj = (Role) guild.getRoleByID(Long.parseUnsignedLong(role)); if (roleObj != null && !user.getRolesForGuild(guild).contains(roleObj)) user.addRole(guild.getLongID(), roleObj); } user.addRole(guild.getLongID(), guild.getRoleByID(guild.getLongID())); //@everyone role user.addNick(guild.getLongID(), json.nick); VoiceState voiceState = (VoiceState) user.getVoiceStateForGuild(guild); voiceState.setDeafened(json.deaf); voiceState.setMuted(json.mute); ((Guild) guild).joinTimes .put(new Guild.TimeStampHolder(user.getLongID(), convertFromTimestamp(json.joined_at))); return user; } /** * Converts a json {@link MessageObject} to a {@link IMessage}. This method first checks the internal message cache * and returns that object with updated information if it exists. Otherwise, it constructs a new message. * * @param channel The channel the message belongs to. * @param json The json object representing the message. * @return The converted message object. */ public static IMessage getMessageFromJSON(Channel channel, MessageObject json) { if (json == null) return null; if (channel.messages.containsKey(json.id)) { Message message = (Message) channel.getMessageByID(Long.parseUnsignedLong(json.id)); message.setAttachments(getAttachmentsFromJSON(json)); message.setEmbeds(getEmbedsFromJSON(json)); message.setContent(json.content); message.setMentionsEveryone(json.mention_everyone); message.setMentions(getMentionsFromJSON(json), getRoleMentionsFromJSON(json)); message.setTimestamp(convertFromTimestamp(json.timestamp)); message.setEditedTimestamp( json.edited_timestamp == null ? null : convertFromTimestamp(json.edited_timestamp)); message.setPinned(Boolean.TRUE.equals(json.pinned)); message.setChannelMentions(); return message; } else { long authorId = Long.parseUnsignedLong(json.author.id); IGuild guild = channel.isPrivate() ? null : channel.getGuild(); IUser author = guild == null ? getUserFromJSON(channel.getShard(), json.author) : guild.getUsers().stream().filter(it -> it.getLongID() == authorId).findAny() .orElseGet(() -> getUserFromJSON(channel.getShard(), json.author)); IMessage.Type type = Arrays.stream(IMessage.Type.values()).filter(t -> t.getValue() == json.type) .findFirst().orElse(IMessage.Type.UNKNOWN); Message message = new Message(channel.getClient(), Long.parseUnsignedLong(json.id), json.content, author, channel, convertFromTimestamp(json.timestamp), json.edited_timestamp == null ? null : convertFromTimestamp(json.edited_timestamp), json.mention_everyone, getMentionsFromJSON(json), getRoleMentionsFromJSON(json), getAttachmentsFromJSON(json), Boolean.TRUE.equals(json.pinned), getEmbedsFromJSON(json), json.webhook_id != null ? Long.parseUnsignedLong(json.webhook_id) : 0, type); message.setReactions(getReactionsFromJSON(message, json.reactions)); return message; } } /** * Updates a {@link IMessage} object with the non-null or non-empty contents of a json {@link MessageObject}. * * @param client The client this message belongs to. * @param toUpdate The message to update. * @param json The json object representing the message. * @return The updated message object. */ public static IMessage getUpdatedMessageFromJSON(IDiscordClient client, IMessage toUpdate, MessageObject json) { if (toUpdate == null) { Channel channel = (Channel) client.getChannelByID(Long.parseUnsignedLong(json.channel_id)); return channel == null ? null : channel.getMessageByID(Long.parseUnsignedLong(json.id)); } Message message = (Message) toUpdate; List<IMessage.Attachment> attachments = getAttachmentsFromJSON(json); List<Embed> embeds = getEmbedsFromJSON(json); if (!attachments.isEmpty()) message.setAttachments(attachments); if (!embeds.isEmpty()) message.setEmbeds(embeds); if (json.content != null) { message.setContent(json.content); message.setMentions(getMentionsFromJSON(json), getRoleMentionsFromJSON(json)); message.setMentionsEveryone(json.mention_everyone); message.setChannelMentions(); } if (json.timestamp != null) message.setTimestamp(convertFromTimestamp(json.timestamp)); if (json.edited_timestamp != null) message.setEditedTimestamp(convertFromTimestamp(json.edited_timestamp)); if (json.pinned != null) message.setPinned(json.pinned); return message; } /** * Converts a json {@link WebhookObject} to a {@link IWebhook}. This method first checks the internal webhook cache * and returns that object with updated information if it exists. Otherwise, it constructs a new webhook. * * @param channel The channel the webhook belongs to. * @param json The json object representing the webhook. * @return The converted webhook object. */ public static IWebhook getWebhookFromJSON(IChannel channel, WebhookObject json) { long webhookId = Long.parseUnsignedLong(json.id); if (channel.getWebhookByID(webhookId) != null) { Webhook webhook = (Webhook) channel.getWebhookByID(webhookId); webhook.setName(json.name); webhook.setAvatar(json.avatar); return webhook; } else { long userId = Long.parseUnsignedLong(json.user.id); IUser author = channel.getGuild().getUsers().stream().filter(it -> it.getLongID() == userId).findAny() .orElseGet(() -> getUserFromJSON(channel.getShard(), json.user)); return new Webhook(channel.getClient(), json.name, Long.parseUnsignedLong(json.id), channel, author, json.avatar, json.token); } } /** * Converts a json {@link ChannelObject} to a {@link IChannel}. This method first checks the internal channel cache * and returns that object with updated information if it exists. Otherwise, it constructs a new channel. * * @param shard The shard the channel belongs to. * @param json The json object representing the channel. * @return The converted channel object. */ public static IChannel getChannelFromJSON(IShard shard, IGuild guild, ChannelObject json) { DiscordClientImpl client = (DiscordClientImpl) shard.getClient(); long id = Long.parseUnsignedLong(json.id); Channel channel = (Channel) shard.getChannelByID(id); if (channel == null) channel = (Channel) shard.getVoiceChannelByID(id); if (json.type == ChannelObject.Type.PRIVATE) { if (channel == null) { User recipient = getUserFromJSON(shard, json.recipients[0]); channel = new PrivateChannel(client, recipient, id); } } else if (json.type == ChannelObject.Type.GUILD_TEXT || json.type == ChannelObject.Type.GUILD_VOICE) { Pair<Cache<PermissionOverride>, Cache<PermissionOverride>> overrides = getPermissionOverwritesFromJSONs( client, json.permission_overwrites); long categoryID = json.parent_id == null ? 0L : Long.parseUnsignedLong(json.parent_id); if (channel != null) { channel.setName(json.name); channel.setPosition(json.position); channel.setNSFW(json.nsfw); channel.userOverrides.clear(); channel.roleOverrides.clear(); channel.userOverrides.putAll(overrides.getLeft()); channel.roleOverrides.putAll(overrides.getRight()); channel.setCategoryID(categoryID); if (json.type == ChannelObject.Type.GUILD_TEXT) { channel.setTopic(json.topic); } else { VoiceChannel vc = (VoiceChannel) channel; vc.setUserLimit(json.user_limit); vc.setBitrate(json.bitrate); } } else if (json.type == ChannelObject.Type.GUILD_TEXT) { channel = new Channel(client, json.name, id, guild, json.topic, json.position, json.nsfw, categoryID, overrides.getRight(), overrides.getLeft()); } else if (json.type == ChannelObject.Type.GUILD_VOICE) { channel = new VoiceChannel(client, json.name, id, guild, json.topic, json.position, json.nsfw, json.user_limit, json.bitrate, categoryID, overrides.getRight(), overrides.getLeft()); } } return channel; } /** * Converts an array of json {@link OverwriteObject}s to sets of user and role overrides. * * @param overwrites The array of json overwrite objects. * @return A pair representing the overwrites per id; left value = user overrides and right value = role overrides. */ public static Pair<Cache<PermissionOverride>, Cache<PermissionOverride>> getPermissionOverwritesFromJSONs( DiscordClientImpl client, OverwriteObject[] overwrites) { Cache<PermissionOverride> userOverrides = new Cache<>(client, PermissionOverride.class); Cache<PermissionOverride> roleOverrides = new Cache<>(client, PermissionOverride.class); for (OverwriteObject overrides : overwrites) { if (overrides.type.equalsIgnoreCase("role")) { roleOverrides .put(new PermissionOverride(Permissions.getAllowedPermissionsForNumber(overrides.allow), Permissions.getDeniedPermissionsForNumber(overrides.deny), Long.parseUnsignedLong(overrides.id))); } else if (overrides.type.equalsIgnoreCase("member")) { userOverrides .put(new PermissionOverride(Permissions.getAllowedPermissionsForNumber(overrides.allow), Permissions.getDeniedPermissionsForNumber(overrides.deny), Long.parseUnsignedLong(overrides.id))); } else { Discord4J.LOGGER.warn(LogMarkers.API, "Unknown permissions overwrite type \"{}\"!", overrides.type); } } return Pair.of(userOverrides, roleOverrides); } /** * Converts a json {@link RoleObject} to a {@link IRole}. This method first checks the internal role cache * and returns that object with updated information if it exists. Otherwise, it constructs a new role. * * @param guild The guild the role belongs to. * @param json The json object representing the role. * @return The converted role object. */ public static IRole getRoleFromJSON(IGuild guild, RoleObject json) { Role role; if ((role = (Role) guild.getRoleByID(Long.parseUnsignedLong(json.id))) != null) { role.setColor(json.color); role.setHoist(json.hoist); role.setName(json.name); role.setPermissions(json.permissions); role.setPosition(json.position); role.setMentionable(json.mentionable); } else { role = new Role(json.position, json.permissions, json.name, json.managed, Long.parseUnsignedLong(json.id), json.hoist, json.color, json.mentionable, guild); ((Guild) guild).roles.put(role); } return role; } /** * Converts a json {@link VoiceRegionObject} to an {@link IRegion}. * * @param json The json object representing the region. * @return The converted region object. */ public static IRegion getRegionFromJSON(VoiceRegionObject json) { return new Region(json.id, json.name, json.vip); } /** * Converts a json {@link VoiceStateObject} to a {@link IVoiceState}. * * @param guild The guild the voice state is in. * @param json The json object representing the voice state. * @return The converted voice state object. */ public static IVoiceState getVoiceStateFromJson(IGuild guild, VoiceStateObject json) { IVoiceChannel channel = json.channel_id != null ? guild.getVoiceChannelByID(Long.parseUnsignedLong(json.channel_id)) : null; return new VoiceState(guild, channel, guild.getUserByID(Long.parseUnsignedLong(json.user_id)), json.session_id, json.deaf, json.mute, json.self_deaf, json.self_mute, json.suppress); } /** * Converts a json {@link PresenceObject} to a {@link IPresence}. * * @param presence The json object representing the presence. * @return The converted presence object. */ public static IPresence getPresenceFromJSON(PresenceObject presence) { return getPresenceFromJSON(presence.game, presence.status); } /** * Converts a json {@link PresenceUpdateEventResponse} to a {@link IPresence}. * * @param response The event response with presence information. * @return The converted presence object. */ public static IPresence getPresenceFromJSON(PresenceUpdateEventResponse response) { return getPresenceFromJSON(response.game, response.status); } /** * Creates a {@link IPresence} from a {@link GameObject game} and status type. * * @param game The game of the presence. * @param status The status of the presence. * @return The presence object with the given game and status. */ private static IPresence getPresenceFromJSON(GameObject game, String status) { return new Presence(game == null ? null : game.name, game == null ? null : game.url, StatusType.get(status), game == null ? ActivityType.PLAYING : ActivityType.values()[game.type]); } /** * Converts a json {@link EmojiObject} to a {@link IEmoji}. * * @param guild The guild the emoji belongs to. * @param json The json object representing the emoji. * @return The converted emoji object. */ public static IEmoji getEmojiFromJSON(IGuild guild, EmojiObject json) { long id = Long.parseUnsignedLong(json.id); List<IRole> roles = Arrays.stream(json.roles).map(role -> guild.getRoleByID(Long.parseUnsignedLong(role))) .collect(Collectors.toList()); EmojiImpl emoji = (EmojiImpl) guild.getEmojiByID(id); if (emoji != null) { emoji.setName(json.name); emoji.setRoles(roles); return emoji; } Cache<IRole> roleCache = new Cache<>((DiscordClientImpl) guild.getClient(), IRole.class); roleCache.putAll(roles); return new EmojiImpl(id, guild, json.name, roleCache, json.require_colons, json.managed, json.animated); } /** * Converts an array of json {@link MessageObject.ReactionObject}s to a list of {@link IReaction}s. * * @param message The message the reactions belong to. * @param json The json objects representing the reactions. * @return The converted reaction objects. */ public static List<IReaction> getReactionsFromJSON(IMessage message, MessageObject.ReactionObject[] json) { List<IReaction> reactions = new CopyOnWriteArrayList<>(); if (json != null) for (MessageObject.ReactionObject object : json) { long id = object.emoji.id == null ? 0 : Long.parseUnsignedLong(object.emoji.id); ReactionEmoji emoji = ReactionEmoji.of(object.emoji.name, id, object.emoji.animated); reactions.add(new Reaction(message, object.count, emoji)); } return reactions; } /** * Converts a json {@link AuditLogObject} to a {@link AuditLog}. * * @param guild The guild the audit log belongs to. * @param json The json object representing the audit log. * @return The converted audit log object. */ public static AuditLog getAuditLogFromJSON(IGuild guild, AuditLogObject json) { LongMap<IUser> users = Arrays.stream(json.users).map(u -> DiscordUtils.getUserFromJSON(guild.getShard(), u)) .collect(LongMapCollector.toLongMap()); LongMap<IWebhook> webhooks = Arrays.stream(json.webhooks).map( w -> DiscordUtils.getWebhookFromJSON(guild.getChannelByID(Long.parseUnsignedLong(w.channel_id)), w)) .collect(LongMapCollector.toLongMap()); LongMap<AuditLogEntry> entries = Arrays.stream(json.audit_log_entries) .map(e -> DiscordUtils.getAuditLogEntryFromJSON(guild, users, webhooks, e)) .collect(LongMapCollector.toLongMap()); return new AuditLog(entries); } /** * Converts a json {@link AuditLogEntry} to a {@link AuditLogEntry}. * * @param guild The guild the entry belongs to. * @param users The users of the parent audit log. * @param webhooks The webhooks of the parent audit log. * @param json The converted audit log entry object. * @return The converted audit log entry. */ public static AuditLogEntry getAuditLogEntryFromJSON(IGuild guild, LongMap<IUser> users, LongMap<IWebhook> webhooks, AuditLogEntryObject json) { long targetID = json.target_id == null ? 0 : Long.parseUnsignedLong(json.target_id); long id = Long.parseUnsignedLong(json.id); IUser user = users.get(Long.parseUnsignedLong(json.user_id)); ChangeMap changes = json.changes == null ? new ChangeMap() : Arrays.stream(json.changes).collect(ChangeMap.Collector.toChangeMap()); OptionMap options = new OptionMap(json.options); ActionType actionType = ActionType.fromRaw(json.action_type); switch (actionType) { case GUILD_UPDATE: return new DiscordObjectEntry<>(guild, id, user, changes, json.reason, actionType, options); case CHANNEL_CREATE: case CHANNEL_UPDATE: case CHANNEL_OVERWRITE_CREATE: case CHANNEL_OVERWRITE_UPDATE: case CHANNEL_OVERWRITE_DELETE: IChannel channel = guild.getChannelByID(targetID); if (channel == null) channel = guild.getVoiceChannelByID(targetID); if (channel == null) { return new TargetedEntry(id, user, changes, json.reason, actionType, options, targetID); } return new DiscordObjectEntry<>(channel, id, user, changes, json.reason, actionType, options); case MEMBER_KICK: case MEMBER_BAN_ADD: case MEMBER_BAN_REMOVE: case MEMBER_UPDATE: case MEMBER_ROLE_UPDATE: case MESSAGE_DELETE: // message delete target is the author of the message IUser target = users.get(targetID); if (target == null) { return new TargetedEntry(id, user, changes, json.reason, actionType, options, targetID); } return new DiscordObjectEntry<>(target, id, user, changes, json.reason, actionType, options); case ROLE_CREATE: case ROLE_UPDATE: IRole role = guild.getRoleByID(targetID); if (role == null) { return new TargetedEntry(id, user, changes, json.reason, actionType, options, targetID); } return new DiscordObjectEntry<>(role, id, user, changes, json.reason, actionType, options); case WEBHOOK_CREATE: case WEBHOOK_UPDATE: IWebhook webhook = webhooks.get(targetID); if (webhook == null) { return new TargetedEntry(id, user, changes, json.reason, actionType, options, targetID); } return new DiscordObjectEntry<>(webhook, id, user, changes, json.reason, actionType, options); case EMOJI_CREATE: case EMOJI_UPDATE: IEmoji emoji = guild.getEmojiByID(targetID); if (emoji == null) { return new TargetedEntry(id, user, changes, json.reason, actionType, options, targetID); } return new DiscordObjectEntry<>(emoji, id, user, changes, json.reason, actionType, options); case CHANNEL_DELETE: case ROLE_DELETE: case WEBHOOK_DELETE: case EMOJI_DELETE: return new TargetedEntry(id, user, changes, json.reason, actionType, options, targetID); case INVITE_CREATE: case INVITE_DELETE: case INVITE_UPDATE: case MEMBER_PRUNE: return new AuditLogEntry(id, user, changes, json.reason, actionType, options); } return null; } public static ICategory getCategoryFromJSON(IShard shard, IGuild guild, ChannelObject json) { Pair<Cache<PermissionOverride>, Cache<PermissionOverride>> permissionOverwrites = getPermissionOverwritesFromJSONs( (DiscordClientImpl) shard.getClient(), json.permission_overwrites); Category category = (Category) shard.getCategoryByID(Long.parseUnsignedLong(json.id)); if (category != null) { category.setName(json.name); category.setPosition(json.position); category.setNSFW(json.nsfw); category.userOverrides.clear(); category.roleOverrides.clear(); category.userOverrides.putAll(permissionOverwrites.getLeft()); category.roleOverrides.putAll(permissionOverwrites.getRight()); } else { category = new Category(shard, json.name, Long.parseUnsignedLong(json.id), guild, json.position, json.nsfw, permissionOverwrites.getLeft(), permissionOverwrites.getRight()); } return category; } /** * Gets the timestamp portion of a Snowflake ID as a {@link Instant} using the system's default timezone. * * @param id The Snowflake ID. * @return The timestamp portion of the ID. */ public static Instant getSnowflakeTimeFromID(long id) { return Instant.ofEpochMilli(DISCORD_EPOCH + (id >>> 22)); } /** * Converts an {@link AudioInputStream} to 48000Hz 16 bit stereo signed Big Endian PCM format. * * @param stream The original stream. * @return The PCM encoded stream. */ public static AudioInputStream getPCMStream(AudioInputStream stream) { AudioFormat baseFormat = stream.getFormat(); //Converts first to PCM data. If the data is already PCM data, this will not change anything. AudioFormat toPCM = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, baseFormat.getSampleRate(), //AudioConnection.OPUS_SAMPLE_RATE, baseFormat.getSampleSizeInBits() != -1 ? baseFormat.getSampleSizeInBits() : 16, baseFormat.getChannels(), //If we are given a frame size, use it. Otherwise, assume 16 bits (2 8bit shorts) per channel. baseFormat.getFrameSize() != -1 ? baseFormat.getFrameSize() : 2 * baseFormat.getChannels(), baseFormat.getFrameRate() != -1 ? baseFormat.getFrameRate() : baseFormat.getSampleRate(), baseFormat.isBigEndian()); AudioInputStream pcmStream = AudioSystem.getAudioInputStream(toPCM, stream); //Then resamples to a sample rate of 48000hz and ensures that data is Big Endian. AudioFormat audioFormat = new AudioFormat(toPCM.getEncoding(), OpusUtil.OPUS_SAMPLE_RATE, toPCM.getSampleSizeInBits(), toPCM.getChannels(), toPCM.getFrameSize(), toPCM.getFrameRate(), true); return AudioSystem.getAudioInputStream(audioFormat, pcmStream); } /** * Creates a {@link ThreadFactory} which produces threads which run as daemons. * * @return The new daemon thread factory. */ public static ThreadFactory createDaemonThreadFactory() { return createDaemonThreadFactory(null); } /** * This creates a {@link ThreadFactory} which produces threads which run as daemons. * * @param threadName The name of threads created by the returned factory. * @return The new daemon thread factory. */ public static ThreadFactory createDaemonThreadFactory(String threadName) { return (runnable) -> { //Ensures all threads are daemons Thread thread = Executors.defaultThreadFactory().newThread(runnable); if (threadName != null) thread.setName(threadName); thread.setDaemon(true); return thread; }; } /** * Checks equality between two {@link IDiscordObject}s using their IDs. * If one of the given objects is not a discord object, it will use the {@link Object#equals(Object)} method of that * object instead. * * @param a The first object. * @param b The second object. * @return If the two objects are equal. */ public static boolean equals(Object a, Object b) { if (a == b) return true; if (a == null || b == null) return false; if (!IDiscordObject.class.isAssignableFrom(a.getClass())) { return a.equals(b); } if (!IDiscordObject.class.isAssignableFrom(b.getClass())) { return b.equals(a); } if (!a.getClass().isAssignableFrom(b.getClass())) return false; if (((IDiscordObject) a).getLongID() != ((IDiscordObject) b).getLongID()) return false; return true; } }