GuildVoiceState always returns false for inAudioChannel() - java

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.

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

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());
//...
}

JDA - send message

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.

AWS Java Lambda local variables vs object variables

I am trying to find answer to a very specific question. Trying to go through documentation but so far no luck.
Imagine this piece of code
#Override
public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException {
Request request = parseRequest(input);
List<String> validationErrors = validate(request);
if (validationErrors.size() == 0){
ordersManager.getOrderStatusForStore(orderId, storeId);
} else {
generateBadRequestResponse(output, "Invalid Request", null);
}
}
private List<String> validate(Request request) {
orderId = request.getPathParameters().get(PATH_PARAM_ORDER_ID);
programId = request.getPathParameters().get(PATH_PARAM_STORE_ID);
return new ArrayList<>();
}
Here, I am storing orderId and storeId in field variables. Is this okay? I am not sure if AWS will cache this function and hence cache the field variables or would it initiate a new Java object for every request. If its a new object, then storing in field variable is fine but not sure.
AWS will spin up a JVM and instantiate an instance of your code on the first request. AWS has an undocumented spin down time, where if you do not invoke your Lambda again within this time limit, it will shut down the JVM. You will notice these initial requests can take significantly longer but once your function is "warmed up", then it will be much quicker.
So to directly answer your question, your instance will be reused if the next request comes in quick enough. Otherwise, a new instance will be stood up.
A simple Lambda function that can illustrate this point:
/**
* A Lambda handler to see where this runs and when instances are reused.
*/
public class LambdaStatus {
private String hostname;
private AtomicLong counter;
public LambdaStatus() throws UnknownHostException {
this.counter = new AtomicLong(0L);
this.hostname = InetAddress.getLocalHost().getCanonicalHostName();
}
public void handle(Context context) {
counter.getAndIncrement();
context.getLogger().log("hostname=" + hostname + ",counter=" + counter.get());
}
}
Logs from invoking the above.
22:49:20 hostname=ip-10-12-169-156.ec2.internal,counter=1
22:49:27 hostname=ip-10-12-169-156.ec2.internal,counter=2
22:49:39 hostname=ip-10-12-169-156.ec2.internal,counter=3
01:19:05 hostname=ip-10-33-101-18.ec2.internal,counter=1
Strongly not recommended.
Multiple invocations may use the same Lambda function instance and this will break your current functionality.
You need to ensure your instance variables are thread safe and can be accessed by multiple threads when it comes to Lambda. Limit your instance variable writes to initialization - once only.

Java method implementation for Apache Thrift RPCs in a distributed environment

Brief description of my project:
I'm writing a java class named "GreetingsNode", that works in a distributed environment where there is a "managementNode", that is just as service repository and receives and stores info (host port number and service offered) of other nodes and dispatches RPCs of methods offered by services registered. If a node can answer to an RPC, then a thrift socket is opened and a connection is established between the calling node and the answering node, and the answering node returns the result.
I'm using Apache thrift as IDL and framework for RPCs.
Now the problem.
My GreetingsNodeHandler class implements a simple thrift interface containing a single method "getHello(user)" (user being a struct containing the name of the node, which is a parameter of the constructor of GreetingsNode class).
When a GreetingsNode X, connected to the management Node, makes an RPC of that method, another registered GreetingsNode must answer with the message "hello X".
I don't understand properly how to implement the part of the handler where the result is returned, and consequently I fail to understand how I should write the junit test that should check if the method implementation works correctly.
an assert like
assertEquals(client.getHello(user).getMessage(), "Hello John Doe")
would work, but I don't get how, in my case, should i put the client part...
The code for GreetingService thrift service:
struct Message {
1: string message
}
struct User {
1: string name
}
service GreetingsService {
Message getHello(1: User user)
}
Code for GreetingsServiceHandler that must implement GreetingsService method getHello()
public class GreetingsServiceHandler implements GreetingsService.Iface {
private static Random random = new Random(10);
private ManagementService.Client managementClient;
private GreetingsService.Client helloClient;
#Override
public Message getHello(User user) throws TException {
Message answer = null;
// class ServiceProvider is generated by thrift, part of ManagementService thrift service
ServiceProvider provider = null;
List<ServiceProvider>providers = managementClient.getProvidersForService(user.name);
if (providers.isEmpty())
throw new NoProviderAvailableException(); //separate file contains Exception
else {
provider = providers.get(random.nextInt(providers.size()));
//connection between nodes is established here
TTransport helloTransport = new TSocket(provider.getHostName(), provider.getPort());
TProtocol helloProtocol = new TBinaryProtocol(helloTransport);
helloClient = new GreetingsService.Client(helloProtocol);
helloTransport.open();
// here lies my problem
answer = helloClient.getHello(user);
//if I use this instead, then helloClient variable is clearly not used, but of course I need it to answer the method call
answer = answer.setMessage("Ciao " + user.getName() + ", welcome among us!");
}
return answer;
}
and GreetingsNode code is the following:
public class GreetingsNode implements NodeIface {
private ThriftServer helloServer;
private ManagementService.Client managementClient;
private NodeManifest nodeManifest;
private User user;
private String name;
public GreetingsNode(NodeManifest nodeManifest, String name) {
this.nodeManifest = nodeManifest;
this.helloServer = new ThriftServer(GreetingsServiceHandler.class);
this.name = name;
}
#Override
public void turnOn() throws TException {
helloServer.start();
TSocket helloServerTransport = new TSocket("localhost", Constants.SERVER_PORT);
TBinaryProtocol helloServerProtocol = new TBinaryProtocol(helloServerTransport);
managementClient = new ManagementService.Client(helloServerProtocol);
this.setUser(new User(name));
helloServerTransport.open();
helloServer = new ThriftServer(GreetingsServiceHandler.class);
//portNegotiator is a class described in a separate file, that handles the registration of other nodes to the managementNode. NodeManifest is a file generated by thrift, part of managementService thrift file, describing a struct that contains hostname and port number of nodes.
PortNegotiator negotiator = new PortNegotiator(managementClient);
negotiator.negotiate(nodeManifest, helloServer);
}
#Override
public void turnOff() {
helloServer.stop();
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
The basic method impl in the handler is pretty simple, something like the following should do (disclaimer: not tested):
#Override
public Message getHello(User user) throws TException {
Message answer = new Message();
answer = answer.setMessage("Ciao " + user.getName() + ", welcome among us!");
return answer;
}
if I use this instead, then helloClient variable is clearly not used, but of course I need it to answer the method call
When a GreetingsNode X, connected to the management Node, makes an RPC of that method, another registered GreetingsNode must answer with the message "hello X".
If that means that we want a call sequence like Client => ServerA => Server B then this is also possible and requires only slight modifications. Starting from our basic example above, we enhance the code accordingly:
private Message callTheOtherNode(User user) {
// class ServiceProvider is generated by Thrift,
// part of ManagementService Thrift service
ServiceProvider provider = null;
List<ServiceProvider>providers = managementClient.getProvidersForService(user.name);
if (providers.isEmpty())
throw new NoProviderAvailableException(); //separate file contains Exception
provider = providers.get(random.nextInt(providers.size()));
//connection between nodes is established here
TTransport helloTransport = new TSocket(provider.getHostName(), provider.getPort());
TProtocol helloProtocol = new TBinaryProtocol(helloTransport);
helloClient = new GreetingsService.Client(helloProtocol);
helloTransport.open();
return helloClient.getHello(user);
}
#Override
public Message getHello(User user) throws TException {
Message answer = callTheOtherNode(user);
return answer;
}
Of course the "other node" being called needs to actually do something with the request, instead of simply forwarding it again to yet another node.

Categories