JDA - send message - java

I have my own Discord BOT based on JDA. I need to send a text message to the specific channel. I know how to send the message as onEvent response, but in my situation I do not have such event.
I have: Author (BOT), Token and Channel number.
My question is: how to send the message to this channel without an event?

Ok I think I know what you mean. You don't need to have an event to get an ID of a channel and send a message. The only thing you need to do is to instantiate the JDA, call awaitReady(), from the instance you can get all channels (MessageChannels, TextChannels, VoiceChannels, either by calling
get[Text]Channels()
get[Text]ChannelById(id=..)
get[Text]ChannelsByName(name, ignore case))
So 1. Instantiate JDA
JDABuilder builder;
JDA jda = builder.build();
jda.awaitReady();
Get Channel
List<TextChannel> channels = jda.getTextChannelsByName("general", true);
for(TextChannel ch : channels)
{
sendMessage(ch, "message");
}
Send message
static void sendMessage(TextChannel ch, String msg)
{
ch.sendMessage(msg).queue();
}
Hope it helps.

You need only one thing to make this happen, that is an instance of JDA. This can be retrieved from most entities like User/Guild/Channel and every Event instance. With that you can use JDA.getTextChannelById to retrieve the TextChannel instance for sending your message.
class MyClass {
private final JDA api;
private final long channelId;
private final String content;
public MyClass(JDA api) {
this.api = api;
}
public void doThing() {
TextChannel channel = api.getTextChannelById(this.channelId);
if (channel != null) {
channel.sendMessage(this.content).queue();
}
}
}
If you don't have a JDA instance you would have to manually do an HTTP request to send the message, for this lookup the discord documentation or jda source code. The JDA source code might be a little too complicated to take as an example as its more abstract to allow using any endpoint.

Related

WebSocketSession.send does not do anything

I'm coding a game, when a player end its turn, I want to notify the opponent that it's his turn to play.
So I'm storing WebSocketSessions in "Player" classes, so I just need to get an instance of a player to have access to his websocketsession.
The problem is that nothing is happening when I use the "send" method of a websocketsession stored in a "player" instance.
Here is my code to store a WebSocketSession in a player object, it actually receive properly messages from front end, and it is able to send a message back and it works:
#Component("ReactiveWebSocketHandler")
public class ReactiveWebSocketHandler implements WebSocketHandler {
#Autowired
private AuthenticationService authenticationService;
#Override
public Mono<Void> handle(WebSocketSession webSocketSession) {
Flux<WebSocketMessage> output = webSocketSession.receive()
.map(msg -> {
String payloadAsText = msg.getPayloadAsText();
Account account = authenticationService.getAccountByToken(payloadAsText);
Games.getInstance().getGames().get(account.getIdCurrentGame()).getPlayerById(account.getId()).setSession(webSocketSession);
return "WebSocketSession id: " + webSocketSession.getId();
})
.map(webSocketSession::textMessage);
return webSocketSession
.send(output);
}
}
And here is the code I use to notify the opponent player that it is its turn to play, the "opponentSession.send" method seems to produce no result, there is no error message, and it looks like I receive nothing on the front end. The sessions has the same ID than in the handle method so I think the session object is good, also the websocket session was opened and ready when I did my tests:
#RequestMapping(value = "/game/endTurn", method = RequestMethod.POST)
GameBean endTurn(
#RequestHeader(value = "token", required = true) String token) {
ObjectMapper mapper = new ObjectMapper();
Account account = authenticationService.getAccountByToken(token);
gameService.endTurn(account);
Game game = gameService.getGameByAccount(account);
//GameBean opponentGameBean = game.getOpponentGameState(account.getId());
//WebSocketMessage webSocketMessage = opponentSession.textMessage(mapper.writeValueAsString(opponentGameBean));
WebSocketSession opponentSession = game.getPlayerById(game.getOpponentId(account.getId())).getSession();
WebSocketMessage webSocketMessage = opponentSession.textMessage("test message");
opponentSession.send(Mono.just(webSocketMessage));
return gameService.getGameStateByAccount(account);
}
}
You can see on the screenshot that the handle method is working correctly, I'm able to send and receive message.
Websocket input and output
Does someone know how can I make the opponentSession.send method works correctly so that I can receive messages on the front end?
You are using the reactive stack for your websocket and WebSocketSession#send return a Mono<Void> but you don't subscribe to this Mono (you just assembled it) so nothing will happen until something subscribe to it.
In your endpoint it doesn't look like you are using webflux so you are in synchronous world so you don't have other choice than to block
opponentSession.send(Mono.just(webSocketMessage)).block();
If you are using webflux then you should change your method to return a Mono and do something like:
return opponentSession.send(Mono.just(webSocketMessage)).then(gameService.getGameStateByAccount(account));
If you are not familiar with this you should look into projectreactor and WebFlux

GuildVoiceState always returns false for inAudioChannel()

I can't figure for the life of me why the JDA doesn't return the right object for a given call.
I create the instance this way:
public class CatBot extends ListenerAdapter {
private static final String TOKEN = "temporary";
private static final Logger LOGGER = LoggerFactory.getLogger(CatBot.class);
public static void main(String[] args) throws LoginException
{
JDABuilder.createLight(TOKEN)
.enableIntents(GatewayIntent.GUILD_MESSAGES, GatewayIntent.GUILD_VOICE_STATES)
.addEventListeners(new CatBot())
.setActivity(Activity.playing("Here for the Sel"))
.setStatus(OnlineStatus.ONLINE)
.enableCache(CacheFlag.VOICE_STATE)
.build();
}
#Override
public void onMessageReceived(MessageReceivedEvent messageEvent) {
Message message = messageEvent.getMessage();
User author = message.getAuthor();
String content = message.getContentRaw();
Guild guild = messageEvent.getGuild();
// avoid spam by filtering bots
if (messageEvent.getAuthor().isBot())
return;
// We only want to handle messages in Guilds
if (!messageEvent.isFromGuild()) {
return;
}
// handling the case of messages that aren't in the command list
if (!content.equals("!elcat")){
MessageChannel channel = messageEvent.getChannel();
channel.sendMessage("<#" + author.getId() + "> OH NO YOU'RE A DOG").queue();
} else {
Member member = messageEvent.getMember();
GuildVoiceState voiceState = member.getVoiceState();
LOGGER.info("Someone used the cat command {}", member);
if (voiceState.inAudioChannel()) {
AudioChannel channel = voiceState.getChannel();
connectTo(guild, channel);
}
LOGGER.info("Seems like I can't get into the voice channel");
}
}
When my bot receives a command, it should check if the user is in a voice channel to connect to the channel and say "hi". That should happen in the else block.
At the moment, the code stops at this line:
if (voiceState.inAudioChannel())
I tried debugging, and the voiceState object is always instantiated but the most informations are false/empty.
The doc says that to be able to use "GuildVoiceState" I need to enable "CacheFlag.VOICE_STATE" which I did, but no matter what I do, the "inAudioChannel" always returns false.
Gave the bot Admin privileges and tested it on text messages and that works.
Any idea?
The voice state cache is directly linked to the member cache. Since you disabled all the member caching with createLight, the voice states attached to those members will also not be cached properly.
You need to enable voice member cache via setMemberCachePolicy(MemberCachePolicy.VOICE) on your JDABuilder instance.

Detect destination channel of SessionUnsubscribeEvent

My Situation
I'm building a small web chat to learn about Spring and Spring WebSocket. You can create different rooms, and each room has it's own channel at /topic/room/{id}.
My goal is to detect when users join and leave a chat room and I thought I could use Spring WebSocket's SessionSubscribeEvent and SessionUnsubscribeEvent for this.
Getting the Destination from the SessionSubscribeEvent is trivial:
#EventListener
public void handleSubscribe(final SessionSubscribeEvent event) {
final String destination =
SimpMessageHeaderAccessor.wrap(event.getMessage()).getDestination();
//...
}
However, the SessionUnsubscribeEvent does not seem to carry the destination channel, destination is null in the following snippet:
#EventListener
public void handleUnsubscribe(final SessionUnsubscribeEvent event) {
final String destination =
SimpMessageHeaderAccessor.wrap(event.getMessage()).getDestination();
//...
}
My Question
Is there a better way to watch for subscribe/unsubscribe events and should I even be using those as a way for a user to "log in" to a chat room, or should I rather use a separate channel to send separate "log in"/"log out" messages and work with those?
I thought using subscribe/unsubscribe would've been very convenient, but apparently Spring makes it very hard, so I feel like there has to be a better way.
STOMP Headers only appear in the frames relevant to your question as described here: https://stomp.github.io/stomp-specification-1.2.html#SUBSCRIBE and here: https://stomp.github.io/stomp-specification-1.2.html#UNSUBSCRIBE
Only the SUBSCRIBE frame has both destination and id, the UNSUBSCRIBE frame has only an id.
This means you have to remember the subscription id with the destination for future lookup. Care must be taken because different Websocket connections usually use/assign the same subscription ids, so to save destinations reliably, you have to include the websocket session id in your storage key.
I wrote the following method to get it:
protected String getWebsocketSessionId(StompHeaderAccessor headerAccessor)
{
// SimpMessageHeaderAccessor.SESSION_ID_HEADER seems to be set in StompSubProtocolHandler.java:261 ("headerAccessor.setSessionId(session.getId());")
return headerAccessor.getHeader(SimpMessageHeaderAccessor.SESSION_ID_HEADER).toString();
}
StompHeaderAccessor is created like this:
StompHeaderAccessor headerAccessor=StompHeaderAccessor.wrap(((SessionSubscribeEvent)event).getMessage());
StompHeaderAccessor headerAccessor=StompHeaderAccessor.wrap(((SessionUnsubscribeEvent)event).getMessage());
This can then be used to create a unique subscription id which can be used as a key for a map to save data about the subscription, including the destination:
protected String getUniqueSubscriptionId(StompHeaderAccessor headerAccessor)
{
return getWebsocketSessionId(headerAccessor)+"--"+headerAccessor.getSubscriptionId();
}
Like this:
Map<String, String> destinationLookupTable=...;
// on subscribe:
destinationLookupTable.put(getUniqueSubscriptionId(headerAccessor), destination);
// on other occasions, including unsubscribe:
destination=destinationLookupTable.get(getUniqueSubscriptionId(headerAccessor));
I think using SessionSubscribeEvent and SessionUnsubscribeEvent is a good idea for that matter. You can get the destination if you keep track of the SessionID:
private Map<String, String> destinationTracker = new HashMap<>();
#EventListener
public void handleSubscribe(final SessionSubscribeEvent event) {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage());
destinationTracker.put(headers.getSessionId(), headers.getDestination());
//...
}
#EventListener
public void handleUnsubscribe(final SessionUnsubscribeEvent event) {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage());
final String destination = destinationTracker.get(headers.getSessionId());
//...
}

Akka-Java Routing Issue

I have the following issue in Akka-Java.
I have one Parent Actor MainActor.class and this parent has 5 child routes. Following is the hierarchy:
My App => Main Actor => [Child Route-1,Child-Route-2,..]
Simple Use case is String input is parsed to Integer as output:
MyApp ===ask[string input]===> Main Actor ==route==> Child(parses to Integer) === integer result===> Main Actor ===result==> MyApp
Here is the Main Actor snippet:
class MainActor extends UntypedActor {
Router router; {
// ...routes are configured here
}
public void onReceive(Object message) {
if (message instanceof String) {
router.route(message, self()); // route it to child
} else if (message instanceof Integer) {
// received from child, 'tell' this result to actual sender/parent i.e, MyApp
sender().tell(message, self());
} else unhandled(message);
}
}
And Child Actor does nothing but String parsing to Integer and takes the result and sends it back to it's sender by sender().tell(result,getContext().parent())
Issue
MainActor is sending the Parsed integer result sent by child back to child itself instead of MyApp. I also tried replacing sender() to getContext().parent() in MainActor but still it did not work.
Since you receive Integers from a child actor of MainActor, the sender points to the child actor.
When you invoke tell on sender it returns the message back to the child actor.
This should work:
getContext().parent().tell(message, self())
You have 2 options:
You can either go directly from your child to your app (bypassing the root actor on the way back), by forwarding the message (see the self() -> sender() change):
if (message instanceof String) {
router.route(message, sender());
}
So when the child responds, his sender is now the app instead of the root.
Alternatively, you can use futures, the ask/pipe patten in particular
You should really go with Diego Martinola 's answer.
The reason why your code isn't working is the following:
when you use sender().tell(message, self), sender() is not MainApp, because sender() gives you the sender of the message you are currently processing. Since you are processing the Integer message from the child actor, the effect is you are sending the message back to it.
The modification you tried, i.e. getContext().parent().tell(message, self) is no good either. In this case you are sending to the parent of MainActor, not to MainApp. Actually I suspect MainApp is not even an actor, right? You can only send messages to actors. If your intention was sending back to the actor who sent the initial message to MainActor, keep in mind this is not MainApp, it is a temporary actor used by Patterns.ask. Regardless, the main point is that, in the handler of the Integer message, you have no way to get an ActorRef of that temporary actor, cause as said before, sender() gives the sender of the current message. A workaround could be getting the sender in the handler of the String message and storing it in variable for later, like this:
class MainActor extends UntypedActor {
ActorRef lastRequester;
Router router; {
// ...routes are configured here
}
public void onReceive(Object message) {
if (message instanceof String) {
lastRequester = sender();
router.route(message, self()); // route it to child
} else if (message instanceof Integer) {
lastRequester.tell(message, self());
} else unhandled(message);
}
}
But this is dangerous, since you would be relying on all Integer messages arriving before the next String message; if an Integer message for the first String message arrives after the second String message, it will be sent to the actor who send the second String. Waiting for all childs to answer is not really an option because Akka gives you at-most-once delivery semantics. You would have to create a mechanism to recognize what String message a certain Integer message is associated to and keep a list of ActorRef instead of a single one.
While the latter is not terribly complicated to implement, it is not really worth it for your use case, and I would say in most routing scenarios, Diego Martinola 's answer is the way to go.

Java - Atmosphere long polling sends only first message

I'm trying to implement a notification mechanism where a client connects to the server and is receiving updates.
Each user connects to a service end point like this
#ManagedService(path = "/chat/{userId}")
When they connect, they are registered in the broadcaster like this
#Ready
public void onReady(final AtmosphereResource resource) {
Broadcaster broadcaster = BroadcasterFactory.getDefault().lookup(userId,true);
broadcaster.addAtmosphereResource(resource);
}
When i want to send a message from a REST end point for example, i do it like this
#RequestMapping(value = "/ws2/{userId}", method = RequestMethod.GET)
public void test(#PathVariable("userId") String userId) {
Broadcaster broadcaster = BroadcasterFactory.getDefault().lookup(userId,true);
broadcaster.broadcast(new Message(userId, "User id : "));
}
It works very well when i'm using the web-socket implementation.
When i change to long-polling and calling this REST method, only the first message is sent, others are ignored with no errors or logs of any kind.
What can i do in this case?

Categories