Java tutorial
/* * Copyright 2015-2016 Austin Keener & Michael Ritter * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.dv8tion.jda.entities.impl; import com.mashape.unirest.http.HttpResponse; import com.mashape.unirest.http.JsonNode; import com.mashape.unirest.http.Unirest; import com.mashape.unirest.http.exceptions.UnirestException; import com.mashape.unirest.request.body.MultipartBody; import net.dv8tion.jda.JDA; import net.dv8tion.jda.MessageBuilder; import net.dv8tion.jda.MessageHistory; import net.dv8tion.jda.Permission; import net.dv8tion.jda.entities.*; import net.dv8tion.jda.exceptions.PermissionException; import net.dv8tion.jda.exceptions.RateLimitedException; import net.dv8tion.jda.exceptions.VerificationLevelException; import net.dv8tion.jda.handle.EntityBuilder; import net.dv8tion.jda.managers.ChannelManager; import net.dv8tion.jda.managers.PermissionOverrideManager; import net.dv8tion.jda.requests.Requester; import net.dv8tion.jda.utils.InviteUtil; import net.dv8tion.jda.utils.MiscUtil; import net.dv8tion.jda.utils.PermissionUtil; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.time.OffsetDateTime; import java.util.*; import java.util.function.Consumer; import java.util.stream.Collectors; public class TextChannelImpl implements TextChannel { private final String id; private final Guild guild; private String name; private String topic; private int position; private final Map<User, PermissionOverride> userPermissionOverrides = new HashMap<>(); private final Map<Role, PermissionOverride> rolePermissionOverrides = new HashMap<>(); private ChannelManager manager = null; public TextChannelImpl(String id, Guild guild) { this.id = id; this.guild = guild; } @Override public JDA getJDA() { return guild.getJDA(); } @Override public String getAsMention() { return "<#" + getId() + '>'; } @Override public PermissionOverride getOverrideForUser(User user) { return userPermissionOverrides.get(user); } @Override public PermissionOverride getOverrideForRole(Role role) { return rolePermissionOverrides.get(role); } @Override public List<PermissionOverride> getPermissionOverrides() { List<PermissionOverride> overrides = new LinkedList<>(); overrides.addAll(userPermissionOverrides.values()); overrides.addAll(rolePermissionOverrides.values()); return Collections.unmodifiableList(overrides); } @Override public List<PermissionOverride> getUserPermissionOverrides() { return Collections.unmodifiableList(new LinkedList<PermissionOverride>(userPermissionOverrides.values())); } @Override public List<PermissionOverride> getRolePermissionOverrides() { return Collections.unmodifiableList(new LinkedList<PermissionOverride>(rolePermissionOverrides.values())); } @Override public String getId() { return id; } @Override public String getName() { return name; } @Override public String getTopic() { return topic; } @Override public Guild getGuild() { return guild; } @Override public List<User> getUsers() { List<User> users = getGuild().getUsers().stream() .filter(user -> checkPermission(user, Permission.MESSAGE_READ)).collect(Collectors.toList()); return Collections.unmodifiableList(users); } @Override public int getPosition() { List<TextChannel> channels = guild.getTextChannels(); for (int i = 0; i < channels.size(); i++) { if (channels.get(i) == this) return i; } throw new RuntimeException( "Somehow when determining position we never found the TextChannel in the Guild's channels? wtf?"); } @Override public int getPositionRaw() { return position; } @Override public Message sendMessage(String text) { return sendMessage(new MessageBuilder().appendString(text).build()); } @Override public Message sendMessage(Message msg) { checkVerification(); SelfInfo self = getJDA().getSelfInfo(); if (!checkPermission(self, Permission.MESSAGE_WRITE)) throw new PermissionException(Permission.MESSAGE_WRITE); JDAImpl api = (JDAImpl) getJDA(); if (api.getMessageLimit(guild.getId()) != null) { throw new RateLimitedException(api.getMessageLimit(guild.getId()) - System.currentTimeMillis()); } try { Requester.Response response = api.getRequester().post( Requester.DISCORD_API_PREFIX + "channels/" + getId() + "/messages", new JSONObject().put("content", msg.getRawContent()).put("tts", msg.isTTS())); if (response.isRateLimit()) { long retry_after = response.getObject().getLong("retry_after"); api.setMessageTimeout(guild.getId(), retry_after); throw new RateLimitedException(retry_after); } if (!response.isOk()) //sending failed (Verification-level?) return null; return new EntityBuilder(api).createMessage(response.getObject()); } catch (JSONException ex) { JDAImpl.LOG.log(ex); //sending failed return null; } } @Override public void sendMessageAsync(String text, Consumer<Message> callback) { sendMessageAsync(new MessageBuilder().appendString(text).build(), callback); } @Override public void sendMessageAsync(Message msg, Consumer<Message> callback) { checkVerification(); SelfInfo self = getJDA().getSelfInfo(); if (!checkPermission(self, Permission.MESSAGE_WRITE)) throw new PermissionException(Permission.MESSAGE_WRITE); ((MessageImpl) msg).setChannelId(id); AsyncMessageSender.getInstance(getJDA(), guild.getId()).enqueue(msg, false, callback); } @Override public Message sendFile(File file, Message message) { checkVerification(); if (!checkPermission(getJDA().getSelfInfo(), Permission.MESSAGE_WRITE)) throw new PermissionException(Permission.MESSAGE_WRITE); if (!checkPermission(getJDA().getSelfInfo(), Permission.MESSAGE_ATTACH_FILES)) throw new PermissionException(Permission.MESSAGE_ATTACH_FILES); if (file == null || !file.exists() || !file.canRead()) throw new IllegalArgumentException("Provided file is either null, doesn't exist or is not readable!"); if (file.length() > 8 << 20) //8MB throw new IllegalArgumentException("File is to big! Max file-size is 8MB"); JDAImpl api = (JDAImpl) getJDA(); try { MultipartBody body = Unirest.post(Requester.DISCORD_API_PREFIX + "channels/" + getId() + "/messages") .header("authorization", getJDA().getAuthToken()).header("user-agent", Requester.USER_AGENT) .field("file", file); if (message != null) body.field("content", message.getRawContent()).field("tts", message.isTTS()); String dbg = String.format( "Requesting %s -> %s\n\tPayload: file: %s, message: %s, tts: %s\n\tResponse: ", body.getHttpRequest().getHttpMethod().name(), body.getHttpRequest().getUrl(), file.getAbsolutePath(), message == null ? "null" : message.getRawContent(), message == null ? "N/A" : message.isTTS()); HttpResponse<JsonNode> response = body.asJson(); Requester.LOG.trace(dbg + body); try { int status = response.getStatus(); if (status >= 200 && status < 300) { return new EntityBuilder(api).createMessage(response.getBody().getObject()); } else if (response.getStatus() == 429) { long retryAfter = response.getBody().getObject().getLong("retry_after"); api.setMessageTimeout(guild.getId(), retryAfter); throw new RateLimitedException(retryAfter); } else { throw new RuntimeException( "An unknown status code was returned when attempting to upload file. Status: " + status + " JSON: " + response.getBody().toString()); } } catch (JSONException e) { Requester.LOG.fatal("Following json caused an exception: " + response.getBody().toString()); Requester.LOG.log(e); } } catch (UnirestException e) { Requester.LOG.log(e); } return null; } @Override public void sendFileAsync(File file, Message message, Consumer<Message> callback) { checkVerification(); if (!checkPermission(getJDA().getSelfInfo(), Permission.MESSAGE_WRITE)) throw new PermissionException(Permission.MESSAGE_WRITE); if (!checkPermission(getJDA().getSelfInfo(), Permission.MESSAGE_ATTACH_FILES)) throw new PermissionException(Permission.MESSAGE_ATTACH_FILES); Thread thread = new Thread(() -> { Message messageReturn; try { messageReturn = sendFile(file, message); } catch (RateLimitedException e) { JDAImpl.LOG.warn("Got ratelimited when trying to upload file. Providing null to callback."); messageReturn = null; } if (callback != null) callback.accept(messageReturn); }); thread.setName("TextChannelImpl sendFileAsync Channel: " + id); thread.setDaemon(true); thread.start(); } @Override public Message getMessageById(String messageId) { if (!checkPermission(getJDA().getSelfInfo(), Permission.MESSAGE_READ)) throw new PermissionException(Permission.MESSAGE_READ); if (!checkPermission(getJDA().getSelfInfo(), Permission.MESSAGE_HISTORY)) throw new PermissionException(Permission.MESSAGE_HISTORY); Requester.Response response = ((JDAImpl) getJDA()).getRequester() .get(Requester.DISCORD_API_PREFIX + "channels/" + id + "/messages/" + messageId); if (response.isOk()) return new EntityBuilder((JDAImpl) getJDA()).createMessage(response.getObject()); //Doesn't exist. return null; } @Override public boolean deleteMessageById(String messageId) { if (!checkPermission(getJDA().getSelfInfo(), Permission.MESSAGE_READ)) throw new PermissionException(Permission.MESSAGE_READ); Requester.Response response = ((JDAImpl) getJDA()).getRequester() .delete(Requester.DISCORD_API_PREFIX + "channels/" + id + "/messages/" + messageId); if (response.isOk()) return true; else if (response.code == 403) //This block is needed because we cant check who owns the message before attempting to delete. { //We double check to make sure the permission didn't change. if (!checkPermission(getJDA().getSelfInfo(), Permission.MESSAGE_READ)) throw new PermissionException(Permission.MESSAGE_READ); else throw new PermissionException(Permission.MESSAGE_MANAGE, "You need MESSAGE_MANAGE permission to delete another users Messages"); } //Doesn't exist. Either never existed, bad id, was deleted already, or not in this channel. return false; } @Override public MessageHistory getHistory() { return new MessageHistory(this); } public void sendTyping() { ((JDAImpl) getJDA()).getRequester().post(Requester.DISCORD_API_PREFIX + "channels/" + getId() + "/typing", new JSONObject()); } @Override public boolean pinMessageById(String messageId) { if (!checkPermission(getJDA().getSelfInfo(), Permission.MESSAGE_READ)) throw new PermissionException(Permission.MESSAGE_READ, "You cannot pin a message in a channel you can't access. (MESSAGE_READ)"); if (!checkPermission(getJDA().getSelfInfo(), Permission.MESSAGE_MANAGE)) throw new PermissionException(Permission.MESSAGE_MANAGE, "You need MESSAGE_MANAGE to pin or unpin messages."); Requester.Response response = ((JDAImpl) getJDA()).getRequester() .put(Requester.DISCORD_API_PREFIX + "/channels/" + id + "/pins/" + messageId, new JSONObject()); if (response.isRateLimit()) throw new RateLimitedException(response.getObject().getInt("retry_after")); return response.isOk(); } @Override public boolean unpinMessageById(String messageId) { if (!checkPermission(getJDA().getSelfInfo(), Permission.MESSAGE_READ)) throw new PermissionException(Permission.MESSAGE_READ, "You cannot unpin a message in a channel you can't access. (MESSAGE_READ)"); if (!checkPermission(getJDA().getSelfInfo(), Permission.MESSAGE_MANAGE)) throw new PermissionException(Permission.MESSAGE_MANAGE, "You need MESSAGE_MANAGE to pin or unpin messages."); Requester.Response response = ((JDAImpl) getJDA()).getRequester() .delete(Requester.DISCORD_API_PREFIX + "/channels/" + id + "/pins/" + messageId); if (response.isRateLimit()) throw new RateLimitedException(response.getObject().getInt("retry_after")); return response.isOk(); } @Override public List<Message> getPinnedMessages() { if (!checkPermission(getJDA().getSelfInfo(), Permission.MESSAGE_READ)) throw new PermissionException(Permission.MESSAGE_READ, "Cannot get the pinned message of a channel without MESSAGE_READ access."); List<Message> pinnedMessages = new LinkedList<>(); Requester.Response response = ((JDAImpl) getJDA()).getRequester() .get(Requester.DISCORD_API_PREFIX + "/channels/" + id + "/pins"); if (response.isOk()) { JSONArray pins = response.getArray(); for (int i = 0; i < pins.length(); i++) { pinnedMessages.add(new EntityBuilder((JDAImpl) getJDA()).createMessage(pins.getJSONObject(i))); } return Collections.unmodifiableList(pinnedMessages); } else if (response.isRateLimit()) throw new RateLimitedException(response.getObject().getInt("retry_after")); else throw new RuntimeException( "An unknown error occured attempting to get pinned messages. Ask devs for help.\n" + response); } @Override public boolean checkPermission(User user, Permission... permissions) { return PermissionUtil.checkPermission(this, user, permissions); } @Override public synchronized ChannelManager getManager() { if (manager == null) manager = new ChannelManager(this); return manager; } @Override public PermissionOverrideManager createPermissionOverride(User user) { if (!checkPermission(getJDA().getSelfInfo(), Permission.MANAGE_PERMISSIONS)) { throw new PermissionException(Permission.MANAGE_PERMISSIONS); } if (!getGuild().getUsers().contains(user)) { throw new IllegalArgumentException("Given user is not member of this Guild"); } PermissionOverrideImpl override = new PermissionOverrideImpl(this, user, null); //hacky way of putting entity to server without using requester here override.setAllow(1 << Permission.MANAGE_PERMISSIONS.getOffset()).setDeny(0); PermissionOverrideManager manager = override.getManager(); manager.reset(Permission.MANAGE_PERMISSIONS).update(); return manager; } @Override public PermissionOverrideManager createPermissionOverride(Role role) { if (!checkPermission(getJDA().getSelfInfo(), Permission.MANAGE_PERMISSIONS)) { throw new PermissionException(Permission.MANAGE_PERMISSIONS); } if (!getGuild().getRoles().contains(role)) { throw new IllegalArgumentException("Given role does not exist in this Guild"); } PermissionOverrideImpl override = new PermissionOverrideImpl(this, null, role); //hacky way of putting entity to server without using requester here override.setAllow(1 << Permission.MANAGE_PERMISSIONS.getOffset()).setDeny(0); PermissionOverrideManager manager = override.getManager(); manager.reset(Permission.MANAGE_PERMISSIONS).update(); return manager; } @Override public List<InviteUtil.AdvancedInvite> getInvites() { return InviteUtil.getInvites(this); } public TextChannelImpl setName(String name) { this.name = name; return this; } public TextChannelImpl setTopic(String topic) { this.topic = topic; return this; } public TextChannelImpl setPosition(int position) { this.position = position; return this; } public Map<User, PermissionOverride> getUserPermissionOverridesMap() { return userPermissionOverrides; } public Map<Role, PermissionOverride> getRolePermissionOverridesMap() { return rolePermissionOverrides; } @Override public void deleteMessages(Collection<Message> messages) { deleteMessagesByIds(messages.stream().map(msg -> msg.getId()).collect(Collectors.toList())); } @Override public void deleteMessagesByIds(Collection<String> messageIds) { if (messageIds.size() < 2 || messageIds.size() > 100) { throw new IllegalArgumentException("Must provide at least 2 or at most 100 messages to be deleted."); } else if (!PermissionUtil.checkPermission(getJDA().getSelfInfo(), Permission.MESSAGE_MANAGE, this)) { throw new PermissionException(Permission.MESSAGE_MANAGE, "Must have MESSAGE_MANAGE in order to bulk delete messages in this channel regardless of author."); } JSONObject body = new JSONObject().put("messages", messageIds); Requester.Response response = ((JDAImpl) getJDA()).getRequester() .post(Requester.DISCORD_API_PREFIX + "channels/" + id + "/messages/bulk_delete", body); if (response.isRateLimit()) throw new RateLimitedException(response.getObject().getInt("retry_after")); } private void checkVerification() { if (!guild.checkVerification()) { throw new VerificationLevelException(guild.getVerificationLevel()); } } @Override public boolean equals(Object o) { if (!(o instanceof TextChannel)) return false; TextChannel oTChannel = (TextChannel) o; return this == oTChannel || this.getId().equals(oTChannel.getId()); } @Override public int hashCode() { return getId().hashCode(); } @Override public String toString() { return "TC:" + getName() + '(' + getId() + ')'; } @Override public int compareTo(TextChannel chan) { if (this == chan) return 0; if (this.getGuild() != chan.getGuild()) throw new IllegalArgumentException("Cannot compare TextChannels that aren't from the same guild!"); if (this.getPositionRaw() != chan.getPositionRaw()) return chan.getPositionRaw() - this.getPositionRaw(); OffsetDateTime thisTime = MiscUtil.getCreationTime(this); OffsetDateTime chanTime = MiscUtil.getCreationTime(chan); //We compare the provided channel's time to this's time instead of the reverse as one would expect due to how // discord deals with hierarchy. The more recent a channel was created, the lower its hierarchy ranking when // it shares the same position as another channel. return chanTime.compareTo(thisTime); } public static class AsyncMessageSender { private static final Map<JDA, Map<String, AsyncMessageSender>> instances = new HashMap<>(); private final JDAImpl api; private final String ratelimitIdentifier; //GuildId or GlobalPrivateChannel private Runner runner = null; private boolean runnerRunning = false; private boolean alive = true; private final Queue<Task> queue = new LinkedList<>(); private AsyncMessageSender(JDAImpl api, String ratelimitIdentifier) { this.api = api; this.ratelimitIdentifier = ratelimitIdentifier; } public static AsyncMessageSender getInstance(JDA api, String ratelimitIdentifier) { Map<String, AsyncMessageSender> senders = instances.get(api); if (senders == null) { senders = new HashMap<>(); instances.put(api, senders); } AsyncMessageSender sender = senders.get(ratelimitIdentifier); if (sender == null) { sender = new AsyncMessageSender(((JDAImpl) api), ratelimitIdentifier); senders.put(ratelimitIdentifier, sender); } return sender; } public synchronized static void stop(JDA api, String ratelimitIdentifier) { Map<String, AsyncMessageSender> senders = instances.get(api); if (senders != null && !senders.isEmpty()) { AsyncMessageSender sender = senders.get(ratelimitIdentifier); if (sender != null) { sender.kill(); senders.remove(ratelimitIdentifier); } } } public synchronized static void stopAll(JDA api) { Map<String, AsyncMessageSender> senders = instances.get(api); if (senders != null && !senders.isEmpty()) { senders.values().forEach(sender -> { sender.kill(); }); senders.clear(); } } public synchronized void enqueue(Message msg, boolean isEdit, Consumer<Message> callback) { enqueue(new Task(msg, isEdit, callback)); } public synchronized void enqueue(Task task) { queue.add(task); if (runner == null) { runnerRunning = true; runner = new Runner(this); runner.setDaemon(true); runner.start(); } else if (!runnerRunning) { runnerRunning = true; notifyAll(); } } public synchronized void kill() { alive = false; notifyAll(); } private synchronized void waitNew() { if (!queue.isEmpty()) return; runnerRunning = false; while (!runnerRunning) { try { wait(); } catch (InterruptedException ignored) { } } } private synchronized Queue<Task> getQueue() { Queue<Task> copy = new LinkedList<>(queue); queue.clear(); return copy; } public static class Task { public final Message message; public final boolean isEdit; public final Consumer<Message> callback; public Task(Message message, boolean isEdit, Consumer<Message> callback) { this.message = message; this.isEdit = isEdit; this.callback = callback; } } private static class Runner extends Thread { private final AsyncMessageSender sender; public Runner(AsyncMessageSender sender) { this.sender = sender; this.setName("AsyncMessageSender Runner. Identifier: " + sender.ratelimitIdentifier); } @Override public void run() { sending: //Label so that, if needed, we can completely kill the while loop from inside the nested loop. while (sender.alive) { Queue<Task> queue = sender.getQueue(); while (sender.alive && !queue.isEmpty()) { Long messageLimit = sender.api.getMessageLimit(sender.ratelimitIdentifier); if (messageLimit != null) { try { Thread.sleep(messageLimit - System.currentTimeMillis()); } catch (InterruptedException e) { JDAImpl.LOG.log(e); } } Task task = queue.peek(); Message msg = task.message; Requester.Response response; if (sender.api.getTextChannelById(msg.getChannelId()) == null && sender.api.getPrivateChannelById(msg.getChannelId()) == null) { //We no longer have access to the MessageChannel that this message is queued to // send to. This is most likely because it was deleted. AsyncMessageSender.stop(sender.api, sender.ratelimitIdentifier); break sending; } if (task.isEdit) { response = sender.api.getRequester() .patch(Requester.DISCORD_API_PREFIX + "channels/" + msg.getChannelId() + "/messages/" + msg.getId(), new JSONObject().put("content", msg.getRawContent())); } else { response = sender.api.getRequester().post( Requester.DISCORD_API_PREFIX + "channels/" + msg.getChannelId() + "/messages", new JSONObject().put("content", msg.getRawContent()).put("tts", msg.isTTS())); } if (response.responseText == null) { JDAImpl.LOG .debug("Error sending async-message (returned null-text)... Retrying after 1s"); sender.api.setMessageTimeout(sender.ratelimitIdentifier, 1000); } else if (!response.isRateLimit()) //success/unrecoverable error { queue.poll();//remove from queue try { if (response.isOk()) { if (task.callback != null) task.callback.accept( new EntityBuilder(sender.api).createMessage(response.getObject())); } else { //if response didn't have id, sending failed (due to permission/blocked pm,... JDAImpl.LOG.fatal("Could not send/update async message to channel: " + msg.getChannelId() + ". Discord-response: " + response.toString()); if (task.callback != null) task.callback.accept(null); } } catch (JSONException ex) { //could not generate message from json JDAImpl.LOG.log(ex); } catch (IllegalArgumentException ex) { JDAImpl.LOG.log(ex); } } else { sender.api.setMessageTimeout(sender.ratelimitIdentifier, response.getObject().getLong("retry_after")); } if (queue.isEmpty()) { queue = sender.getQueue(); } } sender.waitNew(); } } } } }