Skip to content

Automatically leave voice channel after some time spent idle#42

Open
SriRamanujam wants to merge 4 commits intofreyacodes:masterfrom
SriRamanujam:auto-part
Open

Automatically leave voice channel after some time spent idle#42
SriRamanujam wants to merge 4 commits intofreyacodes:masterfrom
SriRamanujam:auto-part

Conversation

@SriRamanujam
Copy link
Copy Markdown

@SriRamanujam SriRamanujam commented Nov 4, 2021

This PR enables the bot to leave a voice channel after some time spent idle. Upon joining a channel, a scheduled task will fire every minute that checks to see if the player is idle. If it is, the task will increment an internal timer. After some configurable amount of time (idleTimeMinutes in ukulele.yml, defaults to 10 minutes) the task will have the bot automatically leave the voice channel.

First time working with Kotlin and Spring, so please do point out any stylistic foibles. If there's a linter I should run, I can do that no problem.

Fixes #26

Copy link
Copy Markdown
Owner

@freyacodes freyacodes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've changed the bot so that it will leave once the queue is empty after approximately 10 minutes without explanation

This is probably not what all Ukulele users want. This feature should default to be disabled.

var game: String = "",
var trackDurationLimit: Int = 0
var trackDurationLimit: Int = 0,
var idleTimeMinutes: Int = 10
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be disabled by default

Suggested change
var idleTimeMinutes: Int = 10
var idleTimeMinutes: Int = 0

@SriRamanujam
Copy link
Copy Markdown
Author

Okay, so in the process of reworking the PR with code review feedback I discovered a race condition of sorts. Basically, oftentimes the onVoiceJoin handler of the service would fire before the onTrackStart handler in Player, so the logic would determine see an empty queue and no playing track, which meant remainingDuration was 0, and so it would start the idle timeout task. If the video was under one minute long, that meant that the bot would leave early. Not the end of the world, but incorrect behavior nonetheless.

I tried a couple things to resolve the issue. I first tried implementing onTrackStart and onTrackEnd on LeaveOnIdleService itself, which didn't work for the same reason (couldn't guarantee the new handlers would be called after Player's handlers). The approach that does seem to work is what I've reworked this PR into: extending the event handlers on Player to call into LeaveOnIdleService. This neatly resolves the ordering issues, and so far has worked for me in my testing.

class Player(
private val beans: Beans,
guildProperties: GuildProperties,
private val leaveOnIdleService: LeaveOnIdleService,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're actually supposed to put this value in the Beans class and not change PlayerRegistry at all

* Create a new timer which will start counting from the current moment and cancel any existing timers
* associated with this guild.
*/
fun maybeCreateTimer(guild: Guild) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A more appropriate name is onQueueEmpty. The player doesn't control this service

/**
* If a timer exists for this guild, cancel it.
*/
fun maybeDestroyTimer(guild: Guild) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

destroyTimer sounds better. We don't need to communicate that there might not be a timer to destroy

// don't do anything if idleTimeMinutes is not set
if (botProps.idleTimeMinutes <= 0) return

timers[guild.idLong]?.let {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do timers.remove(guild.idLong)?.let { ... } and delete the other remove call

* a player in a guild has spent idling.
*/
class IdleTask(val guild: Guild, private val idleTimeMinutes: Int) : Runnable {
private var numberOfMinutesIdleElapsed = -1
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit odd to me that you're using a counter and not a unix timestamp, but I suppose this works too.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, could you not just schedule the voice leave after idleTimeMinutes minutes?

* Runnable that gets passed to the Spring TaskScheduler. Used to keep track of how many minutes
* a player in a guild has spent idling.
*/
class IdleTask(val guild: Guild, private val idleTimeMinutes: Int) : Runnable {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you make this an inner class you don't need to pass idleTimeMinutes

@SriRamanujam
Copy link
Copy Markdown
Author

Hi, anything else I can do to move this along? I believed I've addressed all the comments from the last round of review.

@Abi45x
Copy link
Copy Markdown

Abi45x commented Dec 26, 2021

Hi SriRamanujam, Freya is currently focusing her time on her bachelor project until the 4th of January, I would probably expect replies to be sparse until then

Copy link
Copy Markdown
Owner

@freyacodes freyacodes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the wait! There's one small thing I want you to change still

Unfortunately, trying to model the task as a duration-based timeout saw
race conditions where the callback to determine whether to start a timer
was called before the callback to add a new track to the player had run.
This caused an issue where the code believed that no track was playing
when in fact a track would be queued mere milliseconds later. When the
video in question was under a minute long, this led to a state where the
bot would leave the channel a minute early.

I've re-implemented the functionality such that it is invoked as part of
the onTrackStart and onTrackEnd callbacks of Player. This guarantees
that the idle timer kickoff logic runs _after_ all other Player state
updates have completed, which neatly avoids race/ordering bugs.
* put leaveOnIdleService in Player.Beans
* better names for leaveOnIdleService's callbacks
* Directly call timers.remove()
* Schedule VC leave task directly, rather than periodically calling
  timer
* Clean up redundant comments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: Leave channel after some amount of time idle

3 participants