-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Addresses multiple intertwining issues. #166
Conversation
private final Observable<Event> events; | ||
|
||
private Routes routes; | ||
|
||
private RepositoryManager repositories; | ||
|
||
public BotContext(Observable<Event> events, Routes routes, RepositoryManager repositories) { | ||
public BotContext( | ||
ErisCasper erisCasperInstance, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should need to keep an ErisCasper instance in BotContext
@@ -26,41 +30,107 @@ | |||
private final ObjectMapper jackson = Jackson.newObjectMapper(); | |||
private final Payloads payloads = new Payloads(jackson); | |||
|
|||
private final Integer[] shard; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use a class provides more semantics than Integer[]
.observeOn(Schedulers.computation()) | ||
.share(); | ||
} | ||
|
||
public Integer getShardNum() { | ||
return shard == null ? -1 : shard[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why return -1 when we could use Optional
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed this alltogether, we only need the shard for identifying
public static ErisCasper create() { | ||
return create(System.getenv("EC_TOKEN")); | ||
String token; | ||
if (System.getenv().containsKey("EC_TOKEN")) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | ||
String shardNumRaw = System.getProperty("shard.num", null); | ||
String shardTotalRaw = System.getProperty("shard.total", null); | ||
if (shardNumRaw != null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This kind of validation logic is what can be shifted into a Shard class
@Value.Immutable | ||
public interface UpdatePresence { | ||
@JsonProperty("since") | ||
@Nullable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why @Nullable
instead of Optional
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Passing through null afaik means something different than not passing it through at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll look into it and see if it's optional, but from what I saw it's necessary but nullable
@@ -0,0 +1,29 @@ | |||
package com.github.princesslana.eriscasper.gateway.commands.util; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this in its own package?
util
is often a bit of a meaningless name because it can mean anything. Often it's a dumping spot because there wasn't a better name. If this is the case, wouldn't it be better off near the class that uses it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can put it as an inner class to Identify.
.doOnNext(e -> LOG.trace("Received: {}.", e)) | ||
.doOnError(e -> LOG.warn("Error: {}.", e)); | ||
} | ||
|
||
private void close0() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why close0
? Does this method name make it obvious what this method does?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's the default closing of the socket private to the class. it's the ultimate goto when wanting to close the connection and stop accepting packets.
socket.close(1002, "Invalid token."); | ||
new ErisCasperFatalException("Failed to authenticate with discord servers.") | ||
.fillInStackTrace() | ||
.printStackTrace(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't print to stdout.
Why fillInStackTrace
? Did you mean to throw
this exception?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
throwing it leaves a long very ambiguous looking error, seems to leave a much cleaner looking error if we directly print it, more useful to the end user imo, but I can change it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we can't continue, then throwing an exception is the best way to indicate this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, fair enough, I'll throw it rather than printing it.
}) | ||
.takeUntil(event -> closed) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Referring to mutable things outside of the reactive stream is a bit of a code smell - it's kind of like the global variables of rx.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I can remove that, I think those are in there for no reason at this point since I added in the basic level of takeUntil
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm looked at it a bit more, the reactive stream needs to look outside of the reactive stream to check if the RxWebsocket is closed before it can continue. Is there a good reason we should change this? It makes the most sense for shutting down the stream so we stop sending heartbeats and such when the pipe breaks or the socket closes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should detect closing by seeing the Closed websocket event: https://github.com/princesslana/ErisCasper.java/blob/master/src/main/java/com/github/princesslana/eriscasper/rx/websocket/RxWebSocketEvent.java#L14
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gotcha, implemented that
~ Moved gateway commands into a more Route based system ~ Changed how takeUntil receives the event and decides whether to continue ~ Changed how Shards are processed and boxed ~ Implemented configuration acceptance for sharding ~ Moved ErisCasper instance outside of the BotContext.
~ Moved gateway commands into a more Route based system ~ Changed how takeUntil receives the event and decides whether to continue ~ Changed how Shards are processed and boxed ~ Implemented configuration acceptance for sharding ~ Moved ErisCasper instance outside of the BotContext. Things left uncommitted to stand for merge?
* <p>Messages are considered to be directed at the Robot if they begin with "+", the username of | ||
* the bot account, or a mention of the bot account. | ||
* <p>Messages are considered to be directed at the Robot if they begin with "+", the username on | ||
* the bot account, or a mention on the bot account. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why are all these 'of's replaced with 'on'. I don't think it's correct in all cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I don't actually know when I did that.
public interface UpdatePresence extends GatewayCommand { | ||
@JsonProperty("since") | ||
@Nullable | ||
Integer getSince(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reading the docs to me seems like this can be absent, or present and null.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having the option for the user to have it as null is important, Optional restricts that by leaving it out. I don't think leaving it out and having it null have the same effect.
return connect(guildId, channelId, false, false); | ||
} | ||
|
||
static UpdateVoiceState connect( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this starts to get too much. I think the client should just use the builder themselves instead of a four parameter method. Method signatures like (Snowflake, Snowflake, boolean, boolean) are error prone also. It's easy to accidentally flip the order of parameters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha, I'll remove the connect/disconnect methods, I thought it might be too much as well.
@@ -16,6 +17,7 @@ | |||
|
|||
private static final Logger LOG = LoggerFactory.getLogger(RxWebSocket.class); | |||
|
|||
private volatile boolean closed = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to be removed in favor of picking up the Closed web socket event?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to see where else it's being used, if unused I'll remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah it's being removed
import com.ufoscout.properlty.Properlty; | ||
import java.util.Optional; | ||
|
||
public class Shard implements Wrapper<Integer[]> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think implementing Wrapper here is semantically correct. I think just having a toArray
method is cleaner.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you considered using immutables and making this a @Tuple
? You could still do the checks on the values by creating our own Shard.of
factory method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implementing Wrapper allows it to be used with jackson properly. It also is correct in the sense that it's wrapping an array of integers. If I don't use Wrapper Jackson may not like it so much. If you know how to get it to where Jackson treats it like an Integer[] like it does with Wrapper I can replace it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Except we don't wrap an array of integers, we have two integer fields.
The Jackson magic you are looking for is @JsonValue
. If you annotate the toArray
method with it, then Jackson will use that to serialize.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, I'll use that then.
return new Integer[] {getShardNumber(), getShardTotal()}; | ||
} | ||
|
||
public int getShardNumber() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need an unwrap method along with the getters? Seems like one or the other should be redundant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I'll probably remove the getters.
private final int shardTotal; | ||
|
||
public Shard(int shardNumber, int shardTotal) { | ||
if (shardNumber >= shardTotal || shardTotal < 1 || shardNumber < 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Preconditions.checkArgument
from Guava works well for this kinds of checks. It'll throw IllegalArgumentException
instead of ErisCasperFatalException
. I can see arguments for either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's necessary to mix exceptions. It's also a very primitive thing to check, I don't think it's necessary to wrap it in Preconditions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The judgement should be made based on readability. I agree it's not a big deal with exception is throw and that the code is straight forward. Either changing or leaving is fine.
Preconditions.checkArgument(shardNumber >= 0, "shardNumber should be zero or more");
Preconditions.checkArgument(shardTotal >= 1, "shardTotal should be one or more");
Preconditions.checkState(shardNumber < shardTotal, "shardNumber should be less than shardTotal");
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might change it to preconditions then I do think that is more descriptive, tests will change with it
} | ||
|
||
public Completable doNothing() { | ||
return Completable.complete(); | ||
} | ||
|
||
public Completable sendGatewayCommand(GatewayCommand command) { | ||
return routes.sendGatewayCommand(command); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this belongs on Routes. Routes is kind of HTTP specific.
But for what reason are we exposing the gateway to the user?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gateway commands must be sent under the gateway, I dont know of a way to format them under a request since they have no actual statement. If we want to let users send commands to the gateway we need to expose it somehow. Safest way to do that is to give them access to a method which sends the command for them. I can remove Resume and Identify from GatewayCommand and it would make it a little safer in that regard.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the user wants to do something like send a message, the fact that it's happening over HTTP is hidden from them. There is nothing in the API that refers to rest or http, they do it using the Action
concept we have.
So, gateway commands can be done in the same way.
Perhaps Action
is an interface, with implementations RestAction
, GatewayAction
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's kind of impractical if we want the users to know which one to use, we also need to know which one it is because each of them have special attributes. It would cause casting and checking and such if we just wanted to use Action.
|
||
private ErisCasper(BotToken token) { | ||
private ErisCasper(BotToken token, Optional<Shard> shard) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we avoid this optional by having using a Shard of [0,1] as a default?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think having no sharding is a bit safer, I dont think telling discord we are using shard [0,1] is useful and Optional makes sense and looks like what it does.
There are still open comments on this. e.g., the exposing of 'gateway' to the user |
Completable.fromAction(() -> ws.close(1002, "Invalid session.")) | ||
.doOnComplete( | ||
() -> LOG.warn("Socket disconnected due to an invalid session.")) | ||
.blockingAwait(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't need to use something blocking during an observable stream.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's blocking because we don't want anything more to even try to come through, if we do it will throw broken pipes and have issues with the websocket. If we block it then we can avoid anything even attempting to come through when it's a non-resumable invalid session.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Putting the blocking here isn't stopping other websocket events from coming through or happening.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, how would we push through that command into the stream? Should we just run it outside of a completable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not really sure, this isn't something I have thought about. I'm not 100% clear on what situations lead to this happening and what action we're taking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not exactly sure what can throw a non-resumable invalid sessions in all honesty. I tried for around 3 hours to get github to throw me an invalid session without success. Invalid tokens throw an initial unauthorized message not an invalid session message so that's a bit odd. Basically right now within this codebase we're just shutting down the websocket.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, so I'm not keen to merge something where we're not sure what we're meant to be doing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, maybe we should remove it then, or just throw a fatal exception when I receive an invalid session which can't be resumed. Most invalid sessions are able to be resumed and I haven't seen one where I couldn't. So maybe that's the right move? Not 100% sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say remove anything related to #64 from this PR and address it at another time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good to me. I think it's a more complicated issue due to being able to test and understand the circumstances that lead to the outcomes we're trying to prevent.
socket.close(1002, "Invalid token."); | ||
em.onNext(ClosingTuple.of(ws, code, reason)); | ||
throw new ErisCasperFatalException( | ||
"Failed to authenticate with discord servers: [" + reason + "]"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The websocket class shouldn't know about the existence of discord.
Why can't the discord listeners listen for the Closing event and handle this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't even realize this was still in the codebase. I'll move it.
The gateway isn't exposed anymore? It's just mixing |
The eventual goal is that all requests for data go via repositories. So, if I want to get all guild members I get a repository via the BotContext and call |
So really the goal is pushing all route commands / gateway commands into repositories? However the issue I see with that is most gateway commands don't return data. They aren't anything for repositories to care about, just because they are requests/commands made to the discord API to perform specific actions. Same with certain route commands. Like, prune messages, or update voice state. I'm not sure how those are at all related to repositories so I find it more into the thought of building a command API we can throw into bot context to reach all of those commands without the user knowing about the gateway or our rest API. Eventually we should never see Route<X, Y> as the user and instead be entirely concerned about Actions. Actions need to be reformed though since they don't at all accept special conditions. |
Ok, to clarify, I didn't mean all gateway commands (and http routes) should be in repositories. Repository - where you query something about discord or the state |
Alright, so the |
getGuildMembers should be in a repository. The say I see it is it should handle the GuildMemberChunk events as they come in, and present them as something like |
My only hesitation comes when the user doesn't understand the command. |
ErisCasper should take care of how getGuildMembers happens. So, ErisCasper should know whether it should get them via a gateway event, or via a REST call. Or maybe it just returns what it has cached in memory. This is why we want to put it behind the repository interface - so that the bot writer doesn't have to care about that. |
Then maybe we should implement the gateway command into the initialization of the repository so the user doesn't ever have to care about it and we automagically cache every single guild member anyways. When a guild is presented we run the get guild members gateway command and it throws in chunk events so the user is always able to access every guild member. |
I'd prefer it to be lazy/on-demand |
I see it more complicated than that. Just because from the gateway perspective. |
This is why I like things as separate PRs. Because it allows more thinking and reviewing of things without blocking other changes. From the bot writers perspective, I should call How we implement that, is a separate discussion, and yeah, there are issues to work through like you raised (e.g., if we cache, how do we know the cache is valid?). But, Observable is a great abstraction for presenting the GuildMembers back to the bot writer. They write code that operators on the Observable, that is called when the GuildMembers become available - it doesn't expect them to all be there immediately. |
Alright fair enough. In all honesty, I may strip gateway commands entirely from here considering we're handling them through Data and representing them here in multiple different ways. I don't think gateway commands can be handled in the current state of the codebase. I think we should first reformat the Actions API and the repositories to handle information better and have a more viable state. I think right now we're moreso in the stage of setting up the skeletal framework and the framework for gateway commands is being done in -Data now. |
Stripped Gateway commands due to a rethought of how we're handling them. Moved invalid token handling to the gateway. Stripped invalid session handling due to uncertainty.
|
||
private ErisCasper(BotToken token) { | ||
this.token = token; | ||
private Gateway gateway; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
final?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the gateway is initialized later, not when created
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is gateway used out side the getEvents
method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll probably remove the field altogether and add it with the action rewrite
public Completable send(String text) { | ||
return Completable.fromAction(() -> ws.send(text)) | ||
.doOnComplete(() -> LOG.trace("Sent: {}.", text)); | ||
} | ||
|
||
private static class Listener extends WebSocketListener { | ||
private final ObservableEmitter<RxWebSocketEvent> em; | ||
private final RxWebSocket socket; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is unused?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can remove this, yeah, leftover
.doOnNext(e -> LOG.trace("Received: {}.", e)) | ||
.doOnError(e -> LOG.warn("Error: {}.", e)); | ||
} | ||
|
||
public void close(int code) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah I think I can remove that now
@ianagbip1oti this needs to be commented on. |
Which is the best path forward to 0.6.0 data? Making a PR to upgrade to data 0.6.0 and merging that before updating this, or merging this and then modifying what's here for data 0.6.0? |
I think the most logical is merging this then modifying. If we update to 0.6.0 then merge the PRs might get messy. Once 0.6.0 is updated we need to make everything conform anyways. There's going to be quite a bit in the update PR just because of how much changed between 0.5.0 -> 0.6.0. Not much is changing within this PR besides the sharding pieces afaik. This also kind of keeps things consistent with time and commit consistency for everyone. Neater. |
Fixes #71
Fixes #120
Addresses #105
Addresses #122
The reason for these all being included in this PR is they all have to do with connection and really they go hand-in-hand.