Various improvements to 1st party Discord API

This commit is contained in:
Vankka 2022-01-13 19:02:25 +02:00
parent 0c9732fe11
commit 7457fd90d0
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
20 changed files with 718 additions and 220 deletions

View File

@ -28,6 +28,7 @@ import com.discordsrv.api.discord.api.entity.channel.DiscordDMChannel;
import com.discordsrv.api.placeholder.annotation.Placeholder;
import net.dv8tion.jda.api.entities.User;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.CompletableFuture;
@ -64,6 +65,23 @@ public interface DiscordUser extends Snowflake, Mentionable {
@NotNull
String getDiscriminator();
/**
* Gets the Discord user's avatar url, if an avatar is set.
* @return the user's avatar url or {@code null}
*/
@Placeholder("user_avatar_url")
@Nullable
String getAvatarUrl();
/**
* Gets the Discord user's avatar that is currently active,
* if an avatar isn't set it'll be the url to the default avatar provided by Discord.
* @return the user's avatar url
*/
@Placeholder("user_effective_avatar_url")
@NotNull
String getEffectiveAvatarUrl();
/**
* Gets the Discord user's username followed by a {@code #} and their discriminator.
* @return the Discord user's username & discriminator in the following format {@code Username#1234}

View File

@ -0,0 +1,44 @@
/*
* This file is part of the DiscordSRV API, licensed under the MIT License
* Copyright (c) 2016-2021 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.discordsrv.api.discord.api.entity.channel;
import com.discordsrv.api.discord.api.entity.guild.DiscordGuild;
import org.jetbrains.annotations.NotNull;
public interface DiscordGuildChannel {
/**
* Gets the name of the channel.
* @return the name of the channel
*/
@NotNull
String getName();
/**
* Gets the Discord server that this channel is in.
* @return the Discord server that contains this channel
*/
@NotNull
DiscordGuild getGuild();
}

View File

@ -0,0 +1,47 @@
/*
* This file is part of the DiscordSRV API, licensed under the MIT License
* Copyright (c) 2016-2021 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.discordsrv.api.discord.api.entity.channel;
import com.discordsrv.api.discord.api.entity.Mentionable;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* A regular Discord channel that messages can be sent to (threads not included).
*/
public interface DiscordGuildMessageChannel extends DiscordMessageChannel, DiscordGuildChannel, Mentionable {
@NotNull
List<DiscordThreadChannel> getActiveThreads();
CompletableFuture<List<DiscordThreadChannel>> retrieveArchivedPrivateThreads();
CompletableFuture<List<DiscordThreadChannel>> retrieveArchivedJoinedPrivateThreads();
CompletableFuture<List<DiscordThreadChannel>> retrieveArchivedPublicThreads();
CompletableFuture<DiscordThreadChannel> createThread(String name, boolean privateThread);
CompletableFuture<DiscordThreadChannel> createThread(String name, long messageId);
}

View File

@ -0,0 +1,37 @@
/*
* This file is part of the DiscordSRV API, licensed under the MIT License
* Copyright (c) 2016-2021 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.discordsrv.api.discord.api.entity.channel;
import com.discordsrv.api.DiscordSRVApi;
import net.dv8tion.jda.api.entities.NewsChannel;
public interface DiscordNewsChannel extends DiscordGuildMessageChannel {
/**
* Returns the JDA representation of this object. This should not be used if it can be avoided.
* @return the JDA representation of this object
* @see DiscordSRVApi#jda()
*/
NewsChannel getAsJDANewsChannel();
}

View File

@ -24,23 +24,13 @@
package com.discordsrv.api.discord.api.entity.channel;
import com.discordsrv.api.DiscordSRVApi;
import com.discordsrv.api.discord.api.entity.Mentionable;
import com.discordsrv.api.discord.api.entity.guild.DiscordGuild;
import net.dv8tion.jda.api.entities.TextChannel;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A Discord text channel.
*/
public interface DiscordTextChannel extends DiscordMessageChannel, Mentionable {
/**
* Gets the name of the text channel.
* @return the name of the channel
*/
@NotNull
String getName();
public interface DiscordTextChannel extends DiscordGuildMessageChannel {
/**
* Gets the topic of the text channel.
@ -49,13 +39,6 @@ public interface DiscordTextChannel extends DiscordMessageChannel, Mentionable {
@Nullable
String getTopic();
/**
* Gets the Discord server that this channel is in.
* @return the Discord server that contains this channel
*/
@NotNull
DiscordGuild getGuild();
/**
* Returns the JDA representation of this object. This should not be used if it can be avoided.
* @return the JDA representation of this object

View File

@ -67,6 +67,13 @@ public interface DiscordGuildMember extends DiscordUser, Mentionable {
return getNickname().orElseGet(this::getUsername);
}
/**
* Gets the avatar url that is active for this user in this server.
* @return the user's avatar url in this server
*/
@Placeholder("user_effective_avatar_url")
String getEffectiveServerAvatarUrl();
/**
* Gets the color of this user's highest role that has a color.
* @return the color that will be used for this user

View File

@ -160,6 +160,20 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
return this;
}
@Override
public @NotNull Builder convertToNonWebhook() {
String webhookUsername = this.webhookUsername;
if (webhookUsername == null) {
return this;
}
// TODO: configuration?
this.content = webhookUsername + " > " + content;
this.webhookUsername = null;
this.webhookAvatarUrl = null;
return this;
}
@Override
public @NotNull SendableDiscordMessage build() {
return new SendableDiscordMessageImpl(content, embeds, allowedMentions, webhookUsername, webhookAvatarUrl);
@ -167,8 +181,7 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
@Override
public Formatter toFormatter() {
return new FormatterImpl(
clone());
return new FormatterImpl(clone());
}
@SuppressWarnings("MethodDoesntCallSuperMethod")
@ -218,6 +231,12 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
return this;
}
@Override
public @NotNull Formatter convertToNonWebhook() {
builder.convertToNonWebhook();
return this;
}
private Function<Matcher, Object> wrapFunction(Function<Matcher, Object> function) {
return matcher -> {
Object result = function.apply(matcher);

View File

@ -16,15 +16,19 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.module.modules;
package com.discordsrv.common.discord.api;
import com.discordsrv.api.discord.events.DiscordMessageDeleteEvent;
import com.discordsrv.api.discord.events.DiscordMessageReceiveEvent;
import com.discordsrv.api.discord.events.DiscordMessageUpdateEvent;
import com.discordsrv.api.event.bus.Subscribe;
import com.discordsrv.api.discord.events.DiscordMessageReceivedEvent;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.discord.api.channel.DiscordMessageChannelImpl;
import com.discordsrv.common.discord.api.message.ReceivedDiscordMessageImpl;
import com.discordsrv.common.discord.api.entity.channel.DiscordMessageChannelImpl;
import com.discordsrv.common.discord.api.entity.message.ReceivedDiscordMessageImpl;
import com.discordsrv.common.module.type.AbstractModule;
import net.dv8tion.jda.api.events.message.MessageDeleteEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.events.message.MessageUpdateEvent;
public class DiscordAPIEventModule extends AbstractModule {
@ -33,10 +37,26 @@ public class DiscordAPIEventModule extends AbstractModule {
}
@Subscribe
public void onMessageReceivedEvent(MessageReceivedEvent event) {
discordSRV.eventBus().publish(new DiscordMessageReceivedEvent(
ReceivedDiscordMessageImpl.fromJDA(discordSRV, event.getMessage()),
DiscordMessageChannelImpl.get(discordSRV, event.getChannel()))
);
public void onMessageReceived(MessageReceivedEvent event) {
discordSRV.eventBus().publish(new DiscordMessageReceiveEvent(
DiscordMessageChannelImpl.get(discordSRV, event.getChannel()),
ReceivedDiscordMessageImpl.fromJDA(discordSRV, event.getMessage())
));
}
@Subscribe
public void onMessageUpdate(MessageUpdateEvent event) {
discordSRV.eventBus().publish(new DiscordMessageUpdateEvent(
DiscordMessageChannelImpl.get(discordSRV, event.getChannel()),
ReceivedDiscordMessageImpl.fromJDA(discordSRV, event.getMessage())
));
}
@Subscribe
public void onMessageDelete(MessageDeleteEvent event) {
discordSRV.eventBus().publish(new DiscordMessageDeleteEvent(
DiscordMessageChannelImpl.get(discordSRV, event.getChannel()),
event.getMessageIdLong()
));
}
}

View File

@ -21,10 +21,12 @@ package com.discordsrv.common.discord.api;
import club.minnced.discord.webhook.WebhookClient;
import club.minnced.discord.webhook.WebhookClientBuilder;
import com.discordsrv.api.discord.api.DiscordAPI;
import com.discordsrv.api.discord.api.ThreadChannelLookup;
import com.discordsrv.api.discord.api.entity.DiscordUser;
import com.discordsrv.api.discord.api.entity.channel.DiscordDMChannel;
import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.api.entity.channel.DiscordTextChannel;
import com.discordsrv.api.discord.api.entity.channel.DiscordThreadChannel;
import com.discordsrv.api.discord.api.entity.guild.DiscordGuild;
import com.discordsrv.api.discord.api.entity.guild.DiscordRole;
import com.discordsrv.api.discord.api.exception.NotReadyException;
@ -32,35 +34,43 @@ import com.discordsrv.api.discord.api.exception.RestErrorResponseException;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.main.channels.base.BaseChannelConfig;
import com.discordsrv.common.config.main.channels.base.IChannelConfig;
import com.discordsrv.common.discord.api.channel.DiscordDMChannelImpl;
import com.discordsrv.common.discord.api.channel.DiscordTextChannelImpl;
import com.discordsrv.common.discord.api.guild.DiscordGuildImpl;
import com.discordsrv.common.discord.api.guild.DiscordRoleImpl;
import com.discordsrv.common.config.main.channels.base.ThreadConfig;
import com.discordsrv.common.discord.api.entity.DiscordUserImpl;
import com.discordsrv.common.discord.api.entity.channel.DiscordDMChannelImpl;
import com.discordsrv.common.discord.api.entity.channel.DiscordTextChannelImpl;
import com.discordsrv.common.discord.api.entity.guild.DiscordGuildImpl;
import com.discordsrv.common.discord.api.entity.guild.DiscordRoleImpl;
import com.discordsrv.common.function.CheckedSupplier;
import com.github.benmanes.caffeine.cache.AsyncCacheLoader;
import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.Expiry;
import com.github.benmanes.caffeine.cache.RemovalListener;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.ThreadChannel;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.Webhook;
import net.dv8tion.jda.api.exceptions.ErrorResponseException;
import net.dv8tion.jda.api.exceptions.InsufficientPermissionException;
import net.dv8tion.jda.api.requests.ErrorResponse;
import net.dv8tion.jda.api.requests.GatewayIntent;
import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
public class DiscordAPIImpl implements DiscordAPI {
private final DiscordSRV discordSRV;
private final AsyncLoadingCache<Long, WebhookClient> cachedClients;
private final List<ThreadChannelLookup> threadLookups = new CopyOnWriteArrayList<>();
public DiscordAPIImpl(DiscordSRV discordSRV) {
this.discordSRV = discordSRV;
@ -82,6 +92,226 @@ public class DiscordAPIImpl implements DiscordAPI {
return cachedClients;
}
/**
* Finds active threads based for the provided {@link IChannelConfig}.
* @param config the config that specified the threads
* @return the list of active threads
*/
public List<DiscordThreadChannel> findThreads(IChannelConfig config) {
List<DiscordThreadChannel> channels = new ArrayList<>();
findOrCreateThreads(config, channels::add, null);
return channels;
}
/**
* Finds or potentially unarchives or creates threads based on the provided {@link IChannelConfig}.
* @param config the config
* @param channelConsumer the consumer that will take the channels as they are gathered
* @param futures a possibly null list of {@link CompletableFuture} for tasks that need to be completed to get all threads
*/
public void findOrCreateThreads(
IChannelConfig config,
Consumer<DiscordThreadChannel> channelConsumer,
@Nullable List<CompletableFuture<DiscordThreadChannel>> futures
) {
List<ThreadConfig> threads = config.threads();
if (threads == null) {
return;
}
for (ThreadConfig threadConfig : threads) {
long channelId = threadConfig.channelId;
DiscordTextChannel channel = getTextChannelById(channelId).orElse(null);
if (channel == null) {
if (channelId > 0) {
discordSRV.logger().error("Unable to find channel with ID "
+ Long.toUnsignedString(channelId)
+ ", unable to forward message to Discord");
}
continue;
}
// Check if a thread by the same name is still active
DiscordThreadChannel thread = findThread(threadConfig, channel.getActiveThreads());
if (thread != null) {
ThreadChannel jdaChannel = thread.getAsJDAThreadChannel();
if (!jdaChannel.isLocked() && !jdaChannel.isArchived()) {
channelConsumer.accept(thread);
continue;
}
}
if (futures == null) {
// Futures not specified, don't try to unarchive or create threads
continue;
}
CompletableFuture<DiscordThreadChannel> future;
if (thread != null) {
// Unarchive the thread
future = new CompletableFuture<>();
unarchiveOrCreateThread(threadConfig, channel, thread, future);
} else {
// Find or create the thread
future = findOrCreateThread(threadConfig, channel);
}
futures.add(future.handle((threadChannel, t) -> {
if (t instanceof InsufficientPermissionException) {
discordSRV.logger().error(
"Unable to send message to channel " + channel
+ " because the bot is lacking the "
+ ((InsufficientPermissionException) t).getPermission().getName()
+ " permission");
throw new RuntimeException();
} else if (t != null) {
discordSRV.logger().error("Failed to find thread in channel " + channel, t);
throw new RuntimeException();
}
if (threadChannel != null) {
channelConsumer.accept(threadChannel);
}
return threadChannel;
}));
}
}
private DiscordThreadChannel findThread(ThreadConfig config, List<DiscordThreadChannel> threads) {
for (DiscordThreadChannel thread : threads) {
if (thread.getName().equals(config.threadName)) {
return thread;
}
}
return null;
}
private CompletableFuture<DiscordThreadChannel> findOrCreateThread(ThreadConfig config, DiscordTextChannel textChannel) {
if (!config.unarchive) {
return textChannel.createThread(config.threadName, config.privateThread);
}
CompletableFuture<DiscordThreadChannel> completableFuture = new CompletableFuture<>();
lookupThreads(
textChannel,
config.privateThread,
lookup -> findOrCreateThread(config, textChannel, lookup, completableFuture),
(thread, throwable) -> {
if (throwable != null) {
completableFuture.completeExceptionally(throwable);
} else {
completableFuture.complete(thread);
}
});
return completableFuture;
}
private void findOrCreateThread(
ThreadConfig config,
DiscordTextChannel textChannel,
ThreadChannelLookup lookup,
CompletableFuture<DiscordThreadChannel> completableFuture
) {
completableFuture.whenComplete((threadChannel, throwable) -> {
CompletableFuture<DiscordThreadChannel> future = lookup.getChannelFuture();
if (throwable != null) {
future.completeExceptionally(throwable);
} else {
future.complete(threadChannel);
}
});
lookup.getFuture().whenComplete((channels, throwable) -> {
if (throwable != null) {
completableFuture.completeExceptionally(throwable);
return;
}
DiscordThreadChannel thread = findThread(config, channels);
unarchiveOrCreateThread(config, textChannel, thread, completableFuture);
}).exceptionally(t -> {
if (t instanceof CompletionException) {
completableFuture.completeExceptionally(t.getCause());
return null;
}
completableFuture.completeExceptionally(t);
return null;
});
}
private void unarchiveOrCreateThread(
ThreadConfig config,
DiscordTextChannel textChannel,
DiscordThreadChannel thread,
CompletableFuture<DiscordThreadChannel> future
) {
if (thread != null) {
ThreadChannel channel = thread.getAsJDAThreadChannel();
if (channel.isLocked() || channel.isArchived()) {
try {
channel.getManager().setArchived(false).queue(
v -> future.complete(thread),
future::completeExceptionally
);
} catch (Throwable t) {
future.completeExceptionally(t);
}
} else {
future.complete(thread);
}
return;
}
textChannel.createThread(config.threadName, config.privateThread).whenComplete(((threadChannel, t) -> {
if (t != null) {
future.completeExceptionally(t);
} else {
future.complete(threadChannel);
}
}));
}
public void lookupThreads(
DiscordTextChannel textChannel,
boolean privateThreads,
Consumer<ThreadChannelLookup> lookupConsumer,
BiConsumer<DiscordThreadChannel, Throwable> channelConsumer
) {
ThreadChannelLookup lookup;
synchronized (threadLookups) {
for (ThreadChannelLookup threadLookup : threadLookups) {
if (threadLookup.isPrivateThreads() != privateThreads
|| threadLookup.getChannelId() != textChannel.getId()) {
continue;
}
threadLookup.getChannelFuture().whenComplete(channelConsumer);
return;
}
lookup = new ThreadChannelLookup(
textChannel.getId(), privateThreads,
privateThreads
? textChannel.retrieveArchivedPrivateThreads()
: textChannel.retrieveArchivedPublicThreads()
);
threadLookups.add(lookup);
}
lookup.getChannelFuture().whenComplete(channelConsumer);
lookupConsumer.accept(lookup);
lookup.getFuture().whenComplete((channel, t) -> threadLookups.remove(lookup));
}
public <T> CompletableFuture<T> mapExceptions(CheckedSupplier<CompletableFuture<T>> futureSupplier) {
try {
return mapExceptions(futureSupplier.get());
} catch (Throwable t) {
CompletableFuture<T> future = new CompletableFuture<>();
future.completeExceptionally(t);
return future;
}
}
public <T> CompletableFuture<T> mapExceptions(CompletableFuture<T> future) {
return future.handle((msg, t) -> {
if (t instanceof ErrorResponseException) {
@ -172,7 +402,7 @@ public class DiscordAPIImpl implements DiscordAPI {
public @NonNull CompletableFuture<WebhookClient> asyncLoad(@NonNull Long channelId, @NonNull Executor executor) {
JDA jda = discordSRV.jda().orElse(null);
if (jda == null) {
return discordSRV.discordAPI().notReady();
return notReady();
}
CompletableFuture<WebhookClient> future = new CompletableFuture<>();
@ -208,7 +438,7 @@ public class DiscordAPIImpl implements DiscordAPI {
}).thenApply(webhook ->
WebhookClientBuilder.fromJDA(webhook)
.setHttpClient(jda.getHttpClient())
.setExecutorService(discordSRV.scheduler().scheduledExecutor())
.setExecutorService(discordSRV.scheduler().scheduledExecutorService())
.build()
);
}
@ -219,7 +449,7 @@ public class DiscordAPIImpl implements DiscordAPI {
private boolean isConfiguredChannel(Long channelId) {
for (BaseChannelConfig config : discordSRV.config().channels.values()) {
if (config instanceof IChannelConfig
&& ((IChannelConfig) config).ids().contains(channelId)) {
&& ((IChannelConfig) config).channelIds().contains(channelId)) {
return true;
}
}

View File

@ -1,127 +0,0 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2021 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.discord.api.channel;
import club.minnced.discord.webhook.WebhookClient;
import club.minnced.discord.webhook.receive.ReadonlyMessage;
import club.minnced.discord.webhook.send.WebhookMessage;
import com.discordsrv.api.discord.api.entity.channel.DiscordTextChannel;
import com.discordsrv.api.discord.api.entity.guild.DiscordGuild;
import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage;
import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.discord.api.guild.DiscordGuildImpl;
import com.discordsrv.common.discord.api.message.ReceivedDiscordMessageImpl;
import com.discordsrv.common.discord.api.message.util.SendableDiscordMessageUtil;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageChannel;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.requests.restaction.MessageAction;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
public class DiscordTextChannelImpl extends DiscordMessageChannelImpl implements DiscordTextChannel {
private final DiscordSRV discordSRV;
private final TextChannel textChannel;
private final DiscordGuild guild;
public DiscordTextChannelImpl(DiscordSRV discordSRV, TextChannel textChannel) {
this.discordSRV = discordSRV;
this.textChannel = textChannel;
this.guild = new DiscordGuildImpl(discordSRV, textChannel.getGuild());
}
@Override
public long getId() {
return textChannel.getIdLong();
}
@Override
public @NotNull String getName() {
return textChannel.getName();
}
@Override
public @Nullable String getTopic() {
return textChannel.getTopic();
}
@Override
public @NotNull DiscordGuild getGuild() {
return guild;
}
@Override
public TextChannel getAsJDATextChannel() {
return textChannel;
}
@Override
public @NotNull CompletableFuture<ReceivedDiscordMessage> sendMessage(SendableDiscordMessage message) {
return message(message, WebhookClient::send, MessageChannel::sendMessage);
}
@Override
public CompletableFuture<Void> deleteMessageById(long id) {
return null; // TODO
}
@Override
public @NotNull CompletableFuture<ReceivedDiscordMessage> editMessageById(long id, SendableDiscordMessage message) {
return message(
message,
(client, msg) -> client.edit(id, msg),
(textChannel, msg) -> textChannel.editMessageById(id, msg)
);
}
@Override
public MessageChannel getAsJDAMessageChannel() {
return textChannel;
}
private CompletableFuture<ReceivedDiscordMessage> message(
SendableDiscordMessage message,
BiFunction<WebhookClient, WebhookMessage, CompletableFuture<ReadonlyMessage>> webhookFunction,
BiFunction<TextChannel, Message, MessageAction> jdaFunction) {
CompletableFuture<ReceivedDiscordMessage> future;
if (message.isWebhookMessage()) {
future = discordSRV.discordAPI().queryWebhookClient(getId())
.thenCompose(client -> webhookFunction.apply(
client, SendableDiscordMessageUtil.toWebhook(message)))
.thenApply(msg -> ReceivedDiscordMessageImpl.fromWebhook(discordSRV, msg));
} else {
future = jdaFunction
.apply(textChannel, SendableDiscordMessageUtil.toJDA(message))
.submit()
.thenApply(msg -> ReceivedDiscordMessageImpl.fromJDA(discordSRV, msg));
}
return discordSRV.discordAPI().mapExceptions(future);
}
@Override
public String getAsMention() {
return textChannel.getAsMention();
}
}

View File

@ -16,15 +16,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.discord.api;
package com.discordsrv.common.discord.api.entity;
import com.discordsrv.api.discord.api.entity.DiscordUser;
import com.discordsrv.api.discord.api.entity.channel.DiscordDMChannel;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.discord.api.channel.DiscordDMChannelImpl;
import com.discordsrv.common.discord.api.entity.channel.DiscordDMChannelImpl;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.User;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.CompletableFuture;
@ -65,6 +66,16 @@ public class DiscordUserImpl implements DiscordUser {
return user.getDiscriminator();
}
@Override
public @Nullable String getAvatarUrl() {
return user.getAvatarUrl();
}
@Override
public @NotNull String getEffectiveAvatarUrl() {
return user.getEffectiveAvatarUrl();
}
@Override
public CompletableFuture<DiscordDMChannel> openPrivateChannel() {
JDA jda = discordSRV.jda().orElse(null);

View File

@ -16,56 +16,42 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.discord.api.channel;
package com.discordsrv.common.discord.api.entity.channel;
import com.discordsrv.api.discord.api.entity.DiscordUser;
import com.discordsrv.api.discord.api.entity.channel.DiscordDMChannel;
import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage;
import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.discord.api.DiscordUserImpl;
import com.discordsrv.common.discord.api.message.ReceivedDiscordMessageImpl;
import com.discordsrv.common.discord.api.message.util.SendableDiscordMessageUtil;
import net.dv8tion.jda.api.entities.MessageChannel;
import com.discordsrv.common.discord.api.entity.DiscordUserImpl;
import com.discordsrv.common.discord.api.entity.message.ReceivedDiscordMessageImpl;
import com.discordsrv.common.discord.api.entity.message.util.SendableDiscordMessageUtil;
import net.dv8tion.jda.api.entities.PrivateChannel;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
public class DiscordDMChannelImpl extends DiscordMessageChannelImpl implements DiscordDMChannel {
public class DiscordDMChannelImpl extends DiscordMessageChannelImpl<PrivateChannel> implements DiscordDMChannel {
private final DiscordSRV discordSRV;
private final PrivateChannel privateChannel;
private final DiscordUser user;
public DiscordDMChannelImpl(DiscordSRV discordSRV, PrivateChannel privateChannel) {
this.discordSRV = discordSRV;
this.privateChannel = privateChannel;
super(discordSRV, privateChannel);
this.user = new DiscordUserImpl(discordSRV, privateChannel.getUser());
}
@Override
public long getId() {
return privateChannel.getIdLong();
}
@Override
public DiscordUser getUser() {
return user;
}
@Override
public PrivateChannel getAsJDAPrivateChannel() {
return privateChannel;
}
@Override
public @NotNull CompletableFuture<ReceivedDiscordMessage> sendMessage(SendableDiscordMessage message) {
public @NotNull CompletableFuture<ReceivedDiscordMessage> sendMessage(@NotNull SendableDiscordMessage message) {
if (message.isWebhookMessage()) {
throw new IllegalArgumentException("Cannot send webhook messages to DMChannels");
}
CompletableFuture<ReceivedDiscordMessage> future = privateChannel
CompletableFuture<ReceivedDiscordMessage> future = channel
.sendMessage(SendableDiscordMessageUtil.toJDA(message))
.submit()
.thenApply(msg -> ReceivedDiscordMessageImpl.fromJDA(discordSRV, msg));
@ -74,17 +60,20 @@ public class DiscordDMChannelImpl extends DiscordMessageChannelImpl implements D
}
@Override
public CompletableFuture<Void> deleteMessageById(long id) {
return discordSRV.discordAPI().mapExceptions(privateChannel.deleteMessageById(id).submit());
public CompletableFuture<Void> deleteMessageById(long id, boolean webhookMessage) {
if (webhookMessage) {
throw new IllegalArgumentException("DMChannels do not contain webhook messages");
}
return discordSRV.discordAPI().mapExceptions(channel.deleteMessageById(id).submit());
}
@Override
public @NotNull CompletableFuture<ReceivedDiscordMessage> editMessageById(long id, SendableDiscordMessage message) {
public @NotNull CompletableFuture<ReceivedDiscordMessage> editMessageById(long id, @NotNull SendableDiscordMessage message) {
if (message.isWebhookMessage()) {
throw new IllegalArgumentException("Cannot send webhook messages to DMChannels");
}
CompletableFuture<ReceivedDiscordMessage> future = privateChannel
CompletableFuture<ReceivedDiscordMessage> future = channel
.editMessageById(id, SendableDiscordMessageUtil.toJDA(message))
.submit()
.thenApply(msg -> ReceivedDiscordMessageImpl.fromJDA(discordSRV, msg));
@ -93,7 +82,7 @@ public class DiscordDMChannelImpl extends DiscordMessageChannelImpl implements D
}
@Override
public MessageChannel getAsJDAMessageChannel() {
return privateChannel;
public PrivateChannel getAsJDAPrivateChannel() {
return channel;
}
}

View File

@ -0,0 +1,174 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2021 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.discord.api.entity.channel;
import club.minnced.discord.webhook.WebhookClient;
import club.minnced.discord.webhook.receive.ReadonlyMessage;
import club.minnced.discord.webhook.send.WebhookMessage;
import com.discordsrv.api.discord.api.entity.channel.DiscordGuildMessageChannel;
import com.discordsrv.api.discord.api.entity.channel.DiscordThreadChannel;
import com.discordsrv.api.discord.api.entity.guild.DiscordGuild;
import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage;
import com.discordsrv.api.discord.api.entity.message.SendableDiscordMessage;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.discord.api.entity.guild.DiscordGuildImpl;
import com.discordsrv.common.discord.api.entity.message.ReceivedDiscordMessageImpl;
import com.discordsrv.common.discord.api.entity.message.util.SendableDiscordMessageUtil;
import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.requests.restaction.MessageAction;
import net.dv8tion.jda.api.requests.restaction.ThreadChannelAction;
import net.dv8tion.jda.api.requests.restaction.pagination.ThreadChannelPaginationAction;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
public abstract class DiscordGuildMessageChannelImpl<T extends GuildMessageChannel & IThreadContainer>
extends DiscordMessageChannelImpl<T>
implements DiscordGuildMessageChannel {
private final DiscordGuild guild;
public DiscordGuildMessageChannelImpl(DiscordSRV discordSRV, T channel) {
super(discordSRV, channel);
this.guild = new DiscordGuildImpl(discordSRV, channel.getGuild());
}
@Override
public @NotNull String getName() {
return channel.getName();
}
@Override
public String getAsMention() {
return channel.getAsMention();
}
@Override
public @NotNull DiscordGuild getGuild() {
return guild;
}
@Override
public @NotNull List<DiscordThreadChannel> getActiveThreads() {
List<ThreadChannel> threads = channel.getThreadChannels();
List<DiscordThreadChannel> threadChannels = new ArrayList<>(threads.size());
for (ThreadChannel thread : threads) {
threadChannels.add(new DiscordThreadChannelImpl(discordSRV, thread));
}
return threadChannels;
}
@Override
public CompletableFuture<List<DiscordThreadChannel>> retrieveArchivedPrivateThreads() {
return threads(IThreadContainer::retrieveArchivedPrivateThreadChannels);
}
@Override
public CompletableFuture<List<DiscordThreadChannel>> retrieveArchivedJoinedPrivateThreads() {
return threads(IThreadContainer::retrieveArchivedPrivateJoinedThreadChannels);
}
@Override
public CompletableFuture<List<DiscordThreadChannel>> retrieveArchivedPublicThreads() {
return threads(IThreadContainer::retrieveArchivedPublicThreadChannels);
}
private CompletableFuture<List<DiscordThreadChannel>> threads(Function<IThreadContainer, ThreadChannelPaginationAction> action) {
return discordSRV.discordAPI().mapExceptions(() ->
action.apply(channel)
.submit()
.thenApply(channels -> channels.stream()
.map(channel -> new DiscordThreadChannelImpl(discordSRV, channel))
.collect(Collectors.toList())
)
);
}
@Override
public CompletableFuture<DiscordThreadChannel> createThread(String name, boolean privateThread) {
return thread(channel -> channel.createThreadChannel(name, privateThread));
}
@Override
public CompletableFuture<DiscordThreadChannel> createThread(String name, long messageId) {
return thread(channel -> channel.createThreadChannel(name, messageId));
}
private CompletableFuture<DiscordThreadChannel> thread(Function<T, ThreadChannelAction> action) {
return discordSRV.discordAPI().mapExceptions(() ->
action.apply(channel)
.submit()
.thenApply(channel -> new DiscordThreadChannelImpl(discordSRV, channel))
);
}
@Override
public @NotNull CompletableFuture<ReceivedDiscordMessage> sendMessage(@NotNull SendableDiscordMessage message) {
return message(message, WebhookClient::send, MessageChannel::sendMessage);
}
@Override
public @NotNull CompletableFuture<ReceivedDiscordMessage> editMessageById(long id, @NotNull SendableDiscordMessage message) {
return message(
message,
(client, msg) -> client.edit(id, msg),
(textChannel, msg) -> textChannel.editMessageById(id, msg)
);
}
private CompletableFuture<ReceivedDiscordMessage> message(
SendableDiscordMessage message,
BiFunction<WebhookClient, WebhookMessage, CompletableFuture<ReadonlyMessage>> webhookFunction,
BiFunction<T, Message, MessageAction> jdaFunction) {
return discordSRV.discordAPI().mapExceptions(() -> {
CompletableFuture<ReceivedDiscordMessage> future;
if (message.isWebhookMessage()) {
future = discordSRV.discordAPI().queryWebhookClient(getId())
.thenCompose(client -> webhookFunction.apply(
client, SendableDiscordMessageUtil.toWebhook(message)))
.thenApply(msg -> ReceivedDiscordMessageImpl.fromWebhook(discordSRV, msg));
} else {
future = jdaFunction
.apply(channel, SendableDiscordMessageUtil.toJDA(message))
.submit()
.thenApply(msg -> ReceivedDiscordMessageImpl.fromJDA(discordSRV, msg));
}
return future;
});
}
@Override
public CompletableFuture<Void> deleteMessageById(long id, boolean webhookMessage) {
CompletableFuture<Void> future;
if (webhookMessage) {
future = channel.deleteMessageById(id).submit();
} else {
future = discordSRV.discordAPI()
.queryWebhookClient(channel.getIdLong())
.thenCompose(client -> client.delete(id));
}
return discordSRV.discordAPI().mapExceptions(future);
}
}

View File

@ -0,0 +1,37 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2021 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.discord.api.entity.channel;
import com.discordsrv.api.discord.api.entity.channel.DiscordNewsChannel;
import com.discordsrv.common.DiscordSRV;
import net.dv8tion.jda.api.entities.NewsChannel;
public class DiscordNewsChannelImpl
extends DiscordGuildMessageChannelImpl<NewsChannel>
implements DiscordNewsChannel {
public DiscordNewsChannelImpl(DiscordSRV discordSRV, NewsChannel channel) {
super(discordSRV, channel);
}
@Override
public NewsChannel getAsJDANewsChannel() {
return channel;
}
}

View File

@ -16,23 +16,27 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.discord.api.channel;
package com.discordsrv.common.discord.api.entity.channel;
import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel;
import com.discordsrv.api.discord.api.entity.channel.DiscordTextChannel;
import com.discordsrv.common.DiscordSRV;
import net.dv8tion.jda.api.entities.MessageChannel;
import net.dv8tion.jda.api.entities.PrivateChannel;
import net.dv8tion.jda.api.entities.TextChannel;
import org.jetbrains.annotations.Nullable;
public abstract class DiscordMessageChannelImpl implements DiscordMessageChannel {
public class DiscordTextChannelImpl extends DiscordGuildMessageChannelImpl<TextChannel> implements DiscordTextChannel {
public static DiscordMessageChannelImpl get(DiscordSRV discordSRV, MessageChannel messageChannel) {
if (messageChannel instanceof TextChannel) {
return new DiscordTextChannelImpl(discordSRV, (TextChannel) messageChannel);
} else if (messageChannel instanceof PrivateChannel) {
return new DiscordDMChannelImpl(discordSRV, (PrivateChannel) messageChannel);
} else {
throw new IllegalArgumentException("Unknown MessageChannel type");
public DiscordTextChannelImpl(DiscordSRV discordSRV, TextChannel textChannel) {
super(discordSRV, textChannel);
}
@Override
public @Nullable String getTopic() {
return channel.getTopic();
}
@Override
public TextChannel getAsJDATextChannel() {
return channel;
}
}

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.discord.api.guild;
package com.discordsrv.common.discord.api.entity.guild;
import com.discordsrv.api.discord.api.entity.guild.DiscordGuild;
import com.discordsrv.api.discord.api.entity.guild.DiscordGuildMember;

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.discord.api.guild;
package com.discordsrv.common.discord.api.entity.guild;
import com.discordsrv.api.color.Color;
import com.discordsrv.api.discord.api.entity.guild.DiscordGuild;
@ -26,7 +26,7 @@ import com.discordsrv.api.placeholder.annotation.Placeholder;
import com.discordsrv.api.placeholder.annotation.PlaceholderRemainder;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.component.util.ComponentUtil;
import com.discordsrv.common.discord.api.DiscordUserImpl;
import com.discordsrv.common.discord.api.entity.DiscordUserImpl;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.User;
@ -73,6 +73,11 @@ public class DiscordGuildMemberImpl extends DiscordUserImpl implements DiscordGu
return roles;
}
@Override
public String getEffectiveServerAvatarUrl() {
return member.getEffectiveAvatarUrl();
}
@Override
public Color getColor() {
return color;

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.discord.api.guild;
package com.discordsrv.common.discord.api.entity.guild;
import com.discordsrv.api.color.Color;
import com.discordsrv.api.discord.api.entity.guild.DiscordRole;

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.discord.api.message;
package com.discordsrv.common.discord.api.entity.message;
import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessage;
import com.discordsrv.api.discord.api.entity.message.ReceivedDiscordMessageCluster;

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.discord.api.message;
package com.discordsrv.common.discord.api.entity.message;
import club.minnced.discord.webhook.WebhookClient;
import club.minnced.discord.webhook.receive.ReadonlyAttachment;
@ -40,9 +40,9 @@ import com.discordsrv.api.placeholder.annotation.PlaceholderRemainder;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.component.util.ComponentUtil;
import com.discordsrv.common.config.main.channels.base.BaseChannelConfig;
import com.discordsrv.common.discord.api.DiscordUserImpl;
import com.discordsrv.common.discord.api.channel.DiscordMessageChannelImpl;
import com.discordsrv.common.discord.api.guild.DiscordGuildMemberImpl;
import com.discordsrv.common.discord.api.entity.DiscordUserImpl;
import com.discordsrv.common.discord.api.entity.channel.DiscordMessageChannelImpl;
import com.discordsrv.common.discord.api.entity.guild.DiscordGuildMemberImpl;
import com.discordsrv.common.function.OrDefault;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
@ -265,7 +265,7 @@ public class ReceivedDiscordMessageImpl extends SendableDiscordMessageImpl imple
return future;
}
return textChannel.deleteMessageById(getId());
return textChannel.deleteMessageById(getId(), fromSelf && getWebhookUsername().isPresent());
}
@Override