├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── bash ├── launch.sh └── update.sh ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── html ├── default_error_page.html ├── login.html ├── playlist_view.html └── queue_view.html ├── osiris ├── .env ├── .gitignore ├── Cargo.lock ├── Cargo.toml └── src │ ├── command.rs │ ├── command │ ├── help.rs │ ├── invite.rs │ └── overview.rs │ ├── db.rs │ ├── error.rs │ ├── event_handler.rs │ ├── glyph_api.rs │ ├── main.rs │ ├── model.rs │ └── schema.rs ├── resources-public ├── img │ ├── aiode-logo-small.png │ ├── aiode-logo-wide.png │ ├── aiode-logo.png │ ├── botify-logo-legacy.png │ ├── botify-logo-small-legacy.png │ ├── botify-logo-small.png │ ├── botify-logo-wide-legacy.png │ ├── botify-logo-wide.png │ ├── botify-logo.png │ └── filebroker-logo-small.png └── style.css ├── settings.gradle ├── src ├── main │ ├── java │ │ └── net │ │ │ └── robinfriedli │ │ │ └── aiode │ │ │ ├── Aiode.java │ │ │ ├── audio │ │ │ ├── AbstractSoftCachedPlayable.java │ │ │ ├── AudioManager.java │ │ │ ├── AudioPlayback.java │ │ │ ├── AudioPlayerSendHandler.java │ │ │ ├── AudioTrackLoader.java │ │ │ ├── ChartService.java │ │ │ ├── Playable.java │ │ │ ├── QueueIterator.java │ │ │ ├── UrlPlayable.java │ │ │ ├── exec │ │ │ │ ├── BlockingTrackLoadingExecutor.java │ │ │ │ ├── PooledTrackLoadingExecutor.java │ │ │ │ ├── ReplaceableTrackLoadingExecutor.java │ │ │ │ ├── SpotifyTrackRedirectionRunnable.java │ │ │ │ ├── TrackLoadingExecutor.java │ │ │ │ ├── TrackLoadingRunnable.java │ │ │ │ └── YouTubePlaylistPopulationRunnable.java │ │ │ ├── spotify │ │ │ │ ├── PlayableTrackWrapper.java │ │ │ │ ├── SpotifyContext.java │ │ │ │ ├── SpotifyRedirectService.java │ │ │ │ ├── SpotifyService.java │ │ │ │ ├── SpotifyTrack.java │ │ │ │ ├── SpotifyTrackBulkLoadingService.java │ │ │ │ ├── SpotifyTrackKind.java │ │ │ │ ├── SpotifyTrackRedirect.java │ │ │ │ ├── SpotifyTrackResultHandler.java │ │ │ │ └── SpotifyUri.java │ │ │ └── youtube │ │ │ │ ├── HollowYouTubeVideo.java │ │ │ │ ├── YouTubePlaylist.java │ │ │ │ ├── YouTubeService.java │ │ │ │ ├── YouTubeVideo.java │ │ │ │ └── YouTubeVideoImpl.java │ │ │ ├── boot │ │ │ ├── AbstractShutdownable.java │ │ │ ├── Shutdownable.java │ │ │ ├── ShutdownableExecutorService.java │ │ │ ├── SpringBootstrap.java │ │ │ ├── SpringPropertiesConfig.java │ │ │ ├── StartupTask.java │ │ │ ├── VersionManager.java │ │ │ ├── configurations │ │ │ │ ├── FilebrokerComponent.java │ │ │ │ ├── GroovySandboxComponent.java │ │ │ │ ├── HibernateComponent.java │ │ │ │ ├── JdaComponent.java │ │ │ │ ├── JxpComponent.java │ │ │ │ ├── SpotifyComponent.java │ │ │ │ ├── TopGGComponent.java │ │ │ │ └── YouTubeComponent.java │ │ │ └── tasks │ │ │ │ ├── CreatePermissionAccessConfigurationTask.java │ │ │ │ ├── InitialiseCommandContributionsTask.java │ │ │ │ ├── LeaveUnassignedPrivateBotTask.java │ │ │ │ ├── MigrateGuildSpecificationsTask.java │ │ │ │ ├── MigratePlaylistsTask.java │ │ │ │ ├── RefreshPersistentGlobalChartsStartupTask.java │ │ │ │ ├── ResetOutdatedYouTubeQuotaTask.java │ │ │ │ ├── SetPlaylistItemIndexTask.java │ │ │ │ ├── SetRedirectedSpotifyTrackNameTask.java │ │ │ │ ├── UpdateTopGGStatsTask.java │ │ │ │ ├── UpsertSlashCommandsTask.java │ │ │ │ └── VersionUpdateAlertTask.java │ │ │ ├── command │ │ │ ├── AbstractAdminCommand.java │ │ │ ├── AbstractCommand.java │ │ │ ├── ClientQuestionEvent.java │ │ │ ├── ClientQuestionEventManager.java │ │ │ ├── Command.java │ │ │ ├── CommandContext.java │ │ │ ├── CommandManager.java │ │ │ ├── PermissionTarget.java │ │ │ ├── SecurityManager.java │ │ │ ├── argument │ │ │ │ ├── ArgumentContributionDelegate.java │ │ │ │ ├── ArgumentController.java │ │ │ │ ├── ArgumentDefinition.java │ │ │ │ └── CommandArgument.java │ │ │ ├── commands │ │ │ │ ├── AbstractPlayableLoadingCommand.java │ │ │ │ ├── AbstractQueueLoadingCommand.java │ │ │ │ ├── AbstractSourceDecidingCommand.java │ │ │ │ ├── admin │ │ │ │ │ ├── AudioTrafficSimulationCommand.java │ │ │ │ │ ├── CleanDbCommand.java │ │ │ │ │ ├── GarbageCollectCommand.java │ │ │ │ │ ├── LoadDocumentCommand.java │ │ │ │ │ ├── QuitCommand.java │ │ │ │ │ ├── RebootCommand.java │ │ │ │ │ ├── UpdateCommand.java │ │ │ │ │ └── YouTubeQuotaCommand.java │ │ │ │ ├── customisation │ │ │ │ │ ├── PrefixCommand.java │ │ │ │ │ ├── PresetCommand.java │ │ │ │ │ ├── PropertyCommand.java │ │ │ │ │ └── RenameCommand.java │ │ │ │ ├── general │ │ │ │ │ ├── AbortCommand.java │ │ │ │ │ ├── AnalyticsCommand.java │ │ │ │ │ ├── AnswerCommand.java │ │ │ │ │ ├── ChartsCommand.java │ │ │ │ │ ├── HelpCommand.java │ │ │ │ │ ├── InviteCommand.java │ │ │ │ │ └── PermissionCommand.java │ │ │ │ ├── playback │ │ │ │ │ ├── ClearCommand.java │ │ │ │ │ ├── ForwardCommand.java │ │ │ │ │ ├── PauseCommand.java │ │ │ │ │ ├── PlayCommand.java │ │ │ │ │ ├── QueueCommand.java │ │ │ │ │ ├── RepeatCommand.java │ │ │ │ │ ├── ReverseCommand.java │ │ │ │ │ ├── RewindCommand.java │ │ │ │ │ ├── ShuffleCommand.java │ │ │ │ │ ├── SkipCommand.java │ │ │ │ │ ├── StopCommand.java │ │ │ │ │ └── VolumeCommand.java │ │ │ │ ├── playlistmanagement │ │ │ │ │ ├── AddCommand.java │ │ │ │ │ ├── CreateCommand.java │ │ │ │ │ ├── DeleteCommand.java │ │ │ │ │ ├── EmptyCommand.java │ │ │ │ │ ├── ExportCommand.java │ │ │ │ │ ├── InsertCommand.java │ │ │ │ │ ├── MoveCommand.java │ │ │ │ │ ├── RemoveCommand.java │ │ │ │ │ └── SynchroniseCommand.java │ │ │ │ ├── scripting │ │ │ │ │ ├── AbstractScriptCommand.java │ │ │ │ │ ├── EvalCommand.java │ │ │ │ │ ├── FinalizerCommand.java │ │ │ │ │ ├── InterceptorCommand.java │ │ │ │ │ ├── ScriptCommand.java │ │ │ │ │ └── TriggerCommand.java │ │ │ │ ├── search │ │ │ │ │ └── SearchCommand.java │ │ │ │ ├── spotify │ │ │ │ │ ├── LoginCommand.java │ │ │ │ │ ├── LogoutCommand.java │ │ │ │ │ └── UploadCommand.java │ │ │ │ └── web │ │ │ │ │ └── ConnectCommand.java │ │ │ ├── interceptor │ │ │ │ ├── AbstractChainableCommandInterceptor.java │ │ │ │ ├── CommandInterceptor.java │ │ │ │ ├── CommandInterceptorChain.java │ │ │ │ └── interceptors │ │ │ │ │ ├── CommandExecutionInterceptor.java │ │ │ │ │ ├── CommandMonitoringInterceptor.java │ │ │ │ │ ├── CommandParserInterceptor.java │ │ │ │ │ ├── CommandVerificationInterceptor.java │ │ │ │ │ ├── HistoryInterceptor.java │ │ │ │ │ ├── ScriptCommandInterceptor.java │ │ │ │ │ ├── SecurityInterceptor.java │ │ │ │ │ └── SpotifyContextInterceptor.java │ │ │ ├── parser │ │ │ │ ├── ArgumentBuildingMode.java │ │ │ │ ├── CommandInputBuildingMode.java │ │ │ │ ├── CommandParseListener.java │ │ │ │ ├── CommandParser.java │ │ │ │ └── ScanningMode.java │ │ │ └── widget │ │ │ │ ├── AbstractDecoratingWidget.java │ │ │ │ ├── AbstractPaginationAction.java │ │ │ │ ├── AbstractPaginationWidget.java │ │ │ │ ├── AbstractWidget.java │ │ │ │ ├── AbstractWidgetAction.java │ │ │ │ ├── DynamicEmbedTablePaginationWidget.java │ │ │ │ ├── EmbedTablePaginationWidget.java │ │ │ │ ├── WidgetManager.java │ │ │ │ ├── WidgetRegistry.java │ │ │ │ ├── actions │ │ │ │ ├── FirstPageAction.java │ │ │ │ ├── LastPageAction.java │ │ │ │ ├── NextPageAction.java │ │ │ │ ├── PlayPauseAction.java │ │ │ │ ├── PrevPageAction.java │ │ │ │ ├── RepeatAllAction.java │ │ │ │ ├── RepeatOneAction.java │ │ │ │ ├── RewindAction.java │ │ │ │ ├── ShuffleAction.java │ │ │ │ ├── SkipAction.java │ │ │ │ ├── VolumeDownAction.java │ │ │ │ └── VolumeUpAction.java │ │ │ │ └── widgets │ │ │ │ ├── NowPlayingWidget.java │ │ │ │ ├── PaginatedMessageEmbedWidget.java │ │ │ │ ├── PermissionListPaginationWidget.java │ │ │ │ ├── PlaylistPaginationWidget.java │ │ │ │ └── QueueWidget.java │ │ │ ├── concurrent │ │ │ ├── CloseableThreadContext.java │ │ │ ├── CommandExecutionQueueManager.java │ │ │ ├── CommandExecutionTask.java │ │ │ ├── CompletableFutures.java │ │ │ ├── DaemonThreadPool.java │ │ │ ├── EagerFetchQueue.java │ │ │ ├── EagerlyScalingThreadPoolExecutor.java │ │ │ ├── EventHandlerPool.java │ │ │ ├── ExecutionContext.java │ │ │ ├── ForkTaskThreadPool.java │ │ │ ├── ForkableThreadContext.java │ │ │ ├── HistoryPool.java │ │ │ ├── LoggingThreadFactory.java │ │ │ ├── QueuedTask.java │ │ │ ├── ThreadContext.java │ │ │ ├── ThreadContextClosingRunnableDelegate.java │ │ │ └── ThreadExecutionQueue.java │ │ │ ├── cron │ │ │ ├── AbstractCronTask.java │ │ │ ├── CronJobService.java │ │ │ └── tasks │ │ │ │ ├── ClearAbandonedGuildContextsTask.java │ │ │ │ ├── DeleteGrantedRolesForDeletedRolesTask.java │ │ │ │ ├── DestroyInactiveWidgetsTask.java │ │ │ │ ├── PlaybackCleanupTask.java │ │ │ │ ├── PrivateBotAssignmentHeartbeatTask.java │ │ │ │ ├── RefreshPersistentGlobalChartsTask.java │ │ │ │ ├── RefreshSpotifyRedirectIndicesTask.java │ │ │ │ └── ResetCurrentYouTubeQuotaTask.java │ │ │ ├── discord │ │ │ ├── DiscordEntity.java │ │ │ ├── GuildContext.java │ │ │ ├── GuildManager.java │ │ │ ├── MessageService.java │ │ │ ├── listeners │ │ │ │ ├── CommandListener.java │ │ │ │ ├── EventWaiter.java │ │ │ │ ├── GuildManagementListener.java │ │ │ │ ├── StartupListener.java │ │ │ │ ├── VoiceChannelListener.java │ │ │ │ └── WidgetListener.java │ │ │ └── property │ │ │ │ ├── AbstractBoolProperty.java │ │ │ │ ├── AbstractGuildProperty.java │ │ │ │ ├── AbstractIntegerProperty.java │ │ │ │ ├── GuildPropertyManager.java │ │ │ │ └── properties │ │ │ │ ├── ArgumentPrefixProperty.java │ │ │ │ ├── AutoPauseProperty.java │ │ │ │ ├── AutoQueueModeProperty.java │ │ │ │ ├── BotNameProperty.java │ │ │ │ ├── ColorSchemeProperty.java │ │ │ │ ├── DefaultListSourceProperty.java │ │ │ │ ├── DefaultSourceProperty.java │ │ │ │ ├── DefaultTextChannelProperty.java │ │ │ │ ├── DefaultVolumeProperty.java │ │ │ │ ├── EnableScriptingProperty.java │ │ │ │ ├── PlaybackNotificationProperty.java │ │ │ │ ├── PrefixProperty.java │ │ │ │ └── TempMessageTimeoutProperty.java │ │ │ ├── entities │ │ │ ├── AccessConfiguration.java │ │ │ ├── Artist.java │ │ │ ├── ClientSession.java │ │ │ ├── CommandHistory.java │ │ │ ├── CurrentYouTubeQuotaUsage.java │ │ │ ├── CustomPermissionTarget.java │ │ │ ├── Episode.java │ │ │ ├── FilebrokerTrack.java │ │ │ ├── GeneratedToken.java │ │ │ ├── GlobalArtistChart.java │ │ │ ├── GlobalTrackChart.java │ │ │ ├── GrantedRole.java │ │ │ ├── GuildSpecification.java │ │ │ ├── LookupEntity.java │ │ │ ├── PlaybackHistory.java │ │ │ ├── PlaybackHistorySource.java │ │ │ ├── Playlist.java │ │ │ ├── PlaylistItem.java │ │ │ ├── Preset.java │ │ │ ├── PrivateBotInstance.java │ │ │ ├── SanitizedEntity.java │ │ │ ├── Song.java │ │ │ ├── SpotifyItemKind.java │ │ │ ├── SpotifyRedirectIndex.java │ │ │ ├── SpotifyRedirectIndexModificationLock.java │ │ │ ├── StoredScript.java │ │ │ ├── UrlTrack.java │ │ │ ├── UserPlaybackHistory.java │ │ │ ├── Video.java │ │ │ └── xml │ │ │ │ ├── ArgumentContribution.java │ │ │ │ ├── CommandContribution.java │ │ │ │ ├── CommandHierarchyNode.java │ │ │ │ ├── CommandInterceptorContribution.java │ │ │ │ ├── CronJobContribution.java │ │ │ │ ├── EmbedDocumentContribution.java │ │ │ │ ├── GenericClassContribution.java │ │ │ │ ├── GroovyVariableProviderContribution.java │ │ │ │ ├── GuildPropertyContribution.java │ │ │ │ ├── HttpHandlerContribution.java │ │ │ │ ├── StartupTaskContribution.java │ │ │ │ ├── Version.java │ │ │ │ └── WidgetContribution.java │ │ │ ├── exceptions │ │ │ ├── AdditionalInformationException.java │ │ │ ├── AmbiguousCommandException.java │ │ │ ├── CommandFailure.java │ │ │ ├── CommandParseException.java │ │ │ ├── CommandRuntimeException.java │ │ │ ├── DiscordEntityInitialisationException.java │ │ │ ├── ExceptionUtils.java │ │ │ ├── ForbiddenCommandException.java │ │ │ ├── IllegalEscapeCharacterException.java │ │ │ ├── InvalidArgumentException.java │ │ │ ├── InvalidCommandException.java │ │ │ ├── InvalidPropertyValueException.java │ │ │ ├── InvalidRequestException.java │ │ │ ├── NoLoginException.java │ │ │ ├── NoResultsFoundException.java │ │ │ ├── NoSpotifyResultsFoundException.java │ │ │ ├── RateLimitException.java │ │ │ ├── UnavailableResourceException.java │ │ │ ├── UnclosedQuotationsException.java │ │ │ ├── UnexpectedCommandSetupException.java │ │ │ ├── UserException.java │ │ │ └── handler │ │ │ │ ├── CommandExceptionHandlerExecutor.java │ │ │ │ ├── ExceptionHandler.java │ │ │ │ ├── ExceptionHandlerExecutor.java │ │ │ │ ├── ExceptionHandlerRegistry.java │ │ │ │ ├── TrackLoadingExceptionHandlerExecutor.java │ │ │ │ └── handlers │ │ │ │ ├── CommandRuntimeExceptionHandler.java │ │ │ │ ├── CommandUncaughtExceptionHandler.java │ │ │ │ ├── LoggingUncaughtExceptionHandler.java │ │ │ │ ├── PermissionExceptionHandler.java │ │ │ │ └── TrackLoadingUncaughtExceptionHandler.java │ │ │ ├── function │ │ │ ├── ChainableRunnable.java │ │ │ ├── CheckedBiFunction.java │ │ │ ├── CheckedConsumer.java │ │ │ ├── CheckedFunction.java │ │ │ ├── CheckedRunnable.java │ │ │ ├── FunctionInvoker.java │ │ │ ├── HibernateInvoker.java │ │ │ ├── Invoker.java │ │ │ ├── LoggingRunnable.java │ │ │ ├── RateLimitInvoker.java │ │ │ ├── SpotifyInvoker.java │ │ │ └── modes │ │ │ │ ├── HibernateTransactionMode.java │ │ │ │ ├── RecursionPreventionMode.java │ │ │ │ ├── SpotifyAuthorizationMode.java │ │ │ │ ├── SpotifyMarketMode.java │ │ │ │ └── SpotifyUserAuthorizationMode.java │ │ │ ├── login │ │ │ ├── Login.java │ │ │ ├── LoginHandler.java │ │ │ └── LoginManager.java │ │ │ ├── persist │ │ │ ├── StaticSessionProvider.java │ │ │ ├── customchange │ │ │ │ ├── InsertEnumLookupValuesChange.java │ │ │ │ ├── MigratePlaybackHistorySource.java │ │ │ │ ├── PermissionTargetTypeInitialValues.java │ │ │ │ ├── PlaybackHistorySourceInitialValues.java │ │ │ │ ├── RestrictCommandAccessChange.java │ │ │ │ ├── ScriptUsageInitialValues.java │ │ │ │ └── SpotifyItemKindInitialValues.java │ │ │ ├── interceptors │ │ │ │ ├── AlertAccessConfigurationModificationInterceptor.java │ │ │ │ ├── AlertPlaylistModificationInterceptor.java │ │ │ │ ├── AlertPresetCreationInterceptor.java │ │ │ │ ├── AlertScriptModificationInterceptor.java │ │ │ │ ├── ChainableInterceptor.java │ │ │ │ ├── CollectingInterceptor.java │ │ │ │ ├── EntityValidationInterceptor.java │ │ │ │ ├── GuildPropertyInterceptor.java │ │ │ │ ├── InterceptorChain.java │ │ │ │ ├── PlaylistItemTimestampInterceptor.java │ │ │ │ ├── PresetSlashCommandsInterceptor.java │ │ │ │ ├── SanitizingEntityInterceptor.java │ │ │ │ └── VerifyPlaylistInterceptor.java │ │ │ ├── qb │ │ │ │ ├── AbstractQueryBuilder.java │ │ │ │ ├── BaseQueryBuilder.java │ │ │ │ ├── PredicateBuilder.java │ │ │ │ ├── QueryBuilder.java │ │ │ │ ├── QueryBuilderFactory.java │ │ │ │ ├── QueryConsumer.java │ │ │ │ ├── SimplePredicateBuilder.java │ │ │ │ ├── SubQueryBuilderFactory.java │ │ │ │ ├── builders │ │ │ │ │ ├── CountQueryBuilder.java │ │ │ │ │ ├── EntityQueryBuilder.java │ │ │ │ │ ├── SelectQueryBuilder.java │ │ │ │ │ ├── SingleSelectQueryBuilder.java │ │ │ │ │ └── sub │ │ │ │ │ │ ├── CorrelatedSubQueryBuilder.java │ │ │ │ │ │ └── UncorrelatedSubQueryBuilder.java │ │ │ │ └── interceptor │ │ │ │ │ ├── QueryInterceptor.java │ │ │ │ │ ├── QueryInterceptorRegistry.java │ │ │ │ │ └── interceptors │ │ │ │ │ ├── AccessConfigurationPartitionInterceptor.java │ │ │ │ │ └── PartitionedQueryInterceptor.java │ │ │ └── tasks │ │ │ │ ├── HibernatePlaylistMigrator.java │ │ │ │ ├── PersistTask.java │ │ │ │ └── UpdatePlaylistItemIndicesTask.java │ │ │ ├── rest │ │ │ ├── ExceptionHandlerAdvice.java │ │ │ ├── RequestContext.java │ │ │ ├── RequestInterceptorHandler.java │ │ │ ├── ServletCustomizer.java │ │ │ ├── SessionBean.java │ │ │ ├── WebConfig.java │ │ │ ├── annotations │ │ │ │ └── AuthenticationRequired.java │ │ │ ├── endpoints │ │ │ │ └── SessionManagementEndpoint.java │ │ │ └── exceptions │ │ │ │ └── MissingAccessException.java │ │ │ ├── scripting │ │ │ ├── GroovyCompilationCustomizer.java │ │ │ ├── GroovyVariableManager.java │ │ │ ├── GroovyVariableProvider.java │ │ │ ├── GroovyWhitelistManager.java │ │ │ ├── SafeGroovyScriptRunner.java │ │ │ ├── ScriptCommandRunner.java │ │ │ ├── ScriptUsageType.java │ │ │ ├── TypeCheckingExtension.java │ │ │ └── variables │ │ │ │ ├── ExecutionContextVariableProvider.java │ │ │ │ ├── SingletonVariableProvider.java │ │ │ │ └── ThreadContextVariableProvider.java │ │ │ ├── servers │ │ │ ├── HttpServerManager.java │ │ │ ├── PlaylistViewHandler.java │ │ │ ├── QueueViewHandler.java │ │ │ ├── ResourceHandler.java │ │ │ └── ServerUtil.java │ │ │ └── util │ │ │ ├── BulkOperationService.java │ │ │ ├── ClassDescriptorNode.java │ │ │ ├── EmbedTable.java │ │ │ ├── EmojiConstants.java │ │ │ ├── InjectorService.java │ │ │ ├── MutableTuple2.java │ │ │ ├── SearchEngine.java │ │ │ ├── SnowflakeMap.java │ │ │ ├── Table.java │ │ │ └── Util.java │ ├── kotlin │ │ └── net │ │ │ └── robinfriedli │ │ │ └── aiode │ │ │ ├── audio │ │ │ ├── playables │ │ │ │ ├── AbstractPlayableContainer.kt │ │ │ │ ├── AbstractSinglePlayableContainer.kt │ │ │ │ ├── PlayableContainer.kt │ │ │ │ ├── PlayableContainerManager.kt │ │ │ │ ├── PlayableContainerProvider.kt │ │ │ │ ├── PlayableFactory.kt │ │ │ │ └── containers │ │ │ │ │ ├── AudioPlaylistPlayableContainer.kt │ │ │ │ │ ├── AudioTrackPlayableContainer.kt │ │ │ │ │ ├── FilebrokerPlayableContainers.kt │ │ │ │ │ ├── PlaylistPlayableContainer.kt │ │ │ │ │ ├── SinglePlayableContainer.kt │ │ │ │ │ ├── SpotifyAlbumSimplifiedPlayableContainer.kt │ │ │ │ │ ├── SpotifyPlaylistSimplifiedPlayableContainer.kt │ │ │ │ │ ├── SpotifyShowPlayableContainer.kt │ │ │ │ │ ├── SpotifyTrackPlayableContainers.kt │ │ │ │ │ ├── UrlTrackPlayableContainer.kt │ │ │ │ │ └── YouTubePlaylistPlayableContainer.kt │ │ │ └── queue │ │ │ │ ├── AudioQueue.kt │ │ │ │ ├── PlayableContainerQueueFragment.kt │ │ │ │ ├── QueueFragment.kt │ │ │ │ └── SinglePlayableQueueFragment.kt │ │ │ └── filebroker │ │ │ ├── FilebrokerApi.kt │ │ │ ├── FilebrokerPlayableWrapper.kt │ │ │ └── FilebrokerPostBulkLoadingService.kt │ ├── resources │ │ ├── META-INF │ │ │ └── additional-spring-configuration-metadata.json │ │ ├── application.properties │ │ ├── banner.txt │ │ ├── current-version.txt │ │ ├── ehcache.xml │ │ ├── liquibase │ │ │ └── dbchangelog.xml │ │ ├── logback.xml │ │ ├── playlists.xml │ │ ├── quartz.properties │ │ ├── schemas │ │ │ ├── commandInterceptorSchema.xsd │ │ │ ├── commandSchema.xsd │ │ │ ├── cronJobSchema.xsd │ │ │ ├── embedDocumentSchema.xsd │ │ │ ├── groovyVariableProviderSchema.xsd │ │ │ ├── groovyWhitelistSchema.xsd │ │ │ ├── guildPropertySchema.xsd │ │ │ ├── guildSpecificationSchema.xsd │ │ │ ├── httpHandlerSchema.xsd │ │ │ ├── playlistSchema.xsd │ │ │ ├── startupTaskSchema.xsd │ │ │ ├── versionSchema.xsd │ │ │ └── widgetSchema.xsd │ │ └── xml-contributions │ │ │ ├── commandInterceptors.xml │ │ │ ├── commands.xml │ │ │ ├── cronJobs.xml │ │ │ ├── embedDocuments.xml │ │ │ ├── groovyVariableProviders.xml │ │ │ ├── groovyWhitelist.xml │ │ │ ├── guildProperties.xml │ │ │ ├── httpHandlers.xml │ │ │ ├── startupTasks.xml │ │ │ └── widgets.xml │ └── webapp │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── Makefile.toml │ │ ├── css │ │ └── style.css │ │ ├── img │ │ ├── botify-logo-legacy.png │ │ ├── botify-logo-small-legacy.png │ │ ├── botify-logo-small.png │ │ ├── botify-logo-wide-legacy.png │ │ ├── botify-logo-wide-transparent.png │ │ ├── botify-logo-wide.png │ │ └── botify-logo.png │ │ ├── index.html │ │ └── src │ │ ├── lib.rs │ │ ├── page │ │ ├── auth.rs │ │ ├── home.rs │ │ └── mod.rs │ │ └── session.rs └── test │ └── java │ └── net │ └── robinfriedli │ └── aiode │ └── function │ └── modes │ └── RecursionPreventionModeTest.java └── versions.xml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [robinfriedli] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: meteora98 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /resources/logs/ 2 | /resources/archive/ 3 | /.idea/ 4 | /.gradle/ 5 | /target/ 6 | /botify.iml 7 | /build/ 8 | /logs/ 9 | /src/main/resources/settings-private.properties 10 | /derby.log 11 | -------------------------------------------------------------------------------- /bash/launch.sh: -------------------------------------------------------------------------------- 1 | heap_size=$(grep -w "aiode.preferences.max_heap_size" src/main/resources/application.properties|cut -d'=' -f2) 2 | if [ -z "$heap_size" ] 3 | then 4 | java -Dsun.net.httpserver.maxReqTime=30 -Dsun.net.httpserver.maxRspTime=30 -jar build/libs/aiode-1.0-SNAPSHOT.jar 5 | else 6 | java -Xmx"$heap_size" -Dsun.net.httpserver.maxReqTime=30 -Dsun.net.httpserver.maxRspTime=30 -jar build/libs/aiode-1.0-SNAPSHOT.jar 7 | fi 8 | -------------------------------------------------------------------------------- /bash/update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | git fetch 3 | 4 | UPSTREAM=${1:-'@{u}'} 5 | LOCAL=$(git rev-parse @) 6 | REMOTE=$(git rev-parse "$UPSTREAM") 7 | BASE=$(git merge-base @ "$UPSTREAM") 8 | 9 | if [ $LOCAL = $REMOTE ]; then 10 | echo "Already Up-to-date" 11 | elif [ $LOCAL = $BASE ]; then 12 | git pull 13 | ./gradlew build 14 | echo "Pulled updates and executed gradle build. You might need to restart the bot." 15 | elif [ $REMOTE = $BASE ]; then 16 | echo "Need to push" 17 | else 18 | echo "Diverged" 19 | fi -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robinfriedli/aiode/bed8fd818131c760056782bf45c1a7d354ca78a7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /html/default_error_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Error 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Error: %s

13 | 14 | 15 | -------------------------------------------------------------------------------- /html/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | botify login 6 | 7 | 8 | 9 | 10 | 11 | 12 |

%s

13 | 14 | 15 | -------------------------------------------------------------------------------- /html/playlist_view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %s 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
Name:%s
Duration:%s
Created by:%s
Tracks:%s
33 |
34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | %s 47 | 48 |
NrTrackDuration
49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /html/queue_view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Queue 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

Your Queue:

16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
Paused%s
Shuffle%s
Repeat all%s
Repeat one%s
34 |
35 |
36 |
37 |
38 | %s 39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /osiris/.env: -------------------------------------------------------------------------------- 1 | OSIRIS_MAX_DB_CONNECTIONS=10 2 | OSIRIS_AIODE_PUBLIC_INVITE=https://discordapp.com/api/oauth2/authorize?client_id=483377420494176258&permissions=70315072&scope=bot 3 | OSIRIS_AIODE_PUBLIC_INVITE_ENABLED=true 4 | OSIRIS_GLYPH_ENDPOINT_URL=https://glyph.robinfriedli.net/glyphbot/ 5 | -------------------------------------------------------------------------------- /osiris/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.idea 3 | /.env.local 4 | /.env.secret 5 | /logs/ 6 | -------------------------------------------------------------------------------- /osiris/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "osiris" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | chrono = { version = "0.4.19", features = ["serde"] } 8 | diesel = { version = "2.2.4", features = ["chrono", "numeric", "postgres", "r2d2", "uuid"] } 9 | diesel-async = { version = "0.5.0", features = ["postgres", "deadpool"] } 10 | dotenvy = "0.15" 11 | fern = { version = "0.6.1", features = ["date-based"] } 12 | futures = "0.3" 13 | lazy_static = "1.5.0" 14 | log = "0.4" 15 | reqwest = { version = "0.12.8", features = ["json"] } 16 | rustls = "0.23" 17 | rustls-native-certs = "0.8" 18 | rustls-pemfile = "2.2" 19 | serde = "1.0" 20 | serde_json = "1.0" 21 | serenity = "0.12" 22 | thiserror = "1.0" 23 | tokio = { version = "1", features = ["full"] } 24 | tokio-postgres = "0.7.12" 25 | tokio-postgres-rustls = "0.12.0" 26 | -------------------------------------------------------------------------------- /osiris/src/command.rs: -------------------------------------------------------------------------------- 1 | use serenity::all::{CommandInteraction, EditInteractionResponse}; 2 | 3 | use crate::error::Error; 4 | 5 | pub mod help; 6 | pub mod invite; 7 | pub mod overview; 8 | 9 | pub trait BotCommand { 10 | async fn run(interaction: &CommandInteraction) -> Result; 11 | } 12 | -------------------------------------------------------------------------------- /osiris/src/command/help.rs: -------------------------------------------------------------------------------- 1 | use serenity::all::{CommandInteraction, CreateEmbed, EditInteractionResponse}; 2 | 3 | use crate::error::Error; 4 | 5 | use super::BotCommand; 6 | 7 | pub struct HelpCommand; 8 | 9 | impl BotCommand for HelpCommand { 10 | async fn run(_interaction: &CommandInteraction) -> Result { 11 | Ok(EditInteractionResponse::new().embed( 12 | CreateEmbed::new() 13 | .title("Help") 14 | .description("This bot manages invite links for the aiode bot. Use the 'overview' command to view available public and private invite links or use 'invite private' or 'invite public' to get an invite link for a private or public aiode instance.") 15 | )) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /osiris/src/glyph_api.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use lazy_static::lazy_static; 3 | use reqwest::Url; 4 | use serde::Deserialize; 5 | use serenity::all::UserId; 6 | 7 | lazy_static! { 8 | pub static ref GLYPH_ENDPOINT_URL: Url = Url::parse( 9 | &std::env::var("OSIRIS_GLYPH_ENDPOINT_URL") 10 | .unwrap_or_else(|_| String::from("https://glyph.robinfriedli.net/glyphbot/")) 11 | ) 12 | .expect("OSIRIS_GLYPH_ENDPOINT_URL is not a valid URL"); 13 | } 14 | 15 | #[derive(Deserialize)] 16 | pub struct CheckIsAiodeSupporterResponse { 17 | pub is_supporter: bool, 18 | #[allow(dead_code)] 19 | pub supporter_since: Option>, 20 | } 21 | 22 | pub async fn user_is_aiode_supporter(user_id: UserId) -> bool { 23 | let url = match GLYPH_ENDPOINT_URL 24 | .join("is-aiode-supporter/") 25 | .map(|endpoint| endpoint.join(&user_id.get().to_string())) 26 | { 27 | Ok(Ok(url)) => url, 28 | Err(e) | Ok(Err(e)) => { 29 | log::error!("Failed to construct URL for is-aiode-supporter endpoint: {e}"); 30 | return false; 31 | } 32 | }; 33 | 34 | let result = reqwest::get(url).await; 35 | let response = match result { 36 | Ok(response) => response, 37 | Err(e) => { 38 | log::error!("Failed to send request to is-aiode-supporter endpoint: {e}"); 39 | return false; 40 | } 41 | }; 42 | 43 | let deserialized_result = response.json::().await; 44 | let deserialized_response = match deserialized_result { 45 | Ok(deserialized_response) => deserialized_response, 46 | Err(e) => { 47 | log::error!("Failed to deserialize is-aiode-supporter response body to json: {e}"); 48 | return false; 49 | } 50 | }; 51 | 52 | deserialized_response.is_supporter 53 | } 54 | -------------------------------------------------------------------------------- /osiris/src/schema.rs: -------------------------------------------------------------------------------- 1 | diesel::table! { 2 | guild_specification (pk) { 3 | pk -> BigInt, 4 | bot_name -> Nullable, 5 | color -> Nullable, 6 | guild_id -> Nullable, 7 | guild_name -> Nullable, 8 | prefix -> Nullable, 9 | send_playback_notification -> Nullable, 10 | default_list_source -> Nullable, 11 | default_source -> Nullable, 12 | enable_auto_pause -> Nullable, 13 | argument_prefix -> Nullable, 14 | default_text_channel_id -> Nullable, 15 | temp_message_timeout -> Nullable, 16 | enable_scripting -> Nullable, 17 | default_volume -> Nullable, 18 | auto_queue_mode -> Nullable, 19 | version_update_alert_sent -> Nullable, 20 | assigned_private_bot_instance -> Nullable, 21 | private_bot_assignment_last_heartbeat -> Nullable, 22 | initialized -> Nullable 23 | } 24 | } 25 | 26 | diesel::table! { 27 | private_bot_instance (identifier) { 28 | identifier -> VarChar, 29 | invite_link -> VarChar, 30 | server_limit -> Int4 31 | } 32 | } 33 | 34 | diesel::joinable!(guild_specification -> private_bot_instance (assigned_private_bot_instance)); 35 | 36 | diesel::allow_tables_to_appear_in_same_query!(guild_specification, private_bot_instance,); 37 | -------------------------------------------------------------------------------- /resources-public/img/aiode-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robinfriedli/aiode/bed8fd818131c760056782bf45c1a7d354ca78a7/resources-public/img/aiode-logo-small.png -------------------------------------------------------------------------------- /resources-public/img/aiode-logo-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robinfriedli/aiode/bed8fd818131c760056782bf45c1a7d354ca78a7/resources-public/img/aiode-logo-wide.png -------------------------------------------------------------------------------- /resources-public/img/aiode-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robinfriedli/aiode/bed8fd818131c760056782bf45c1a7d354ca78a7/resources-public/img/aiode-logo.png -------------------------------------------------------------------------------- /resources-public/img/botify-logo-legacy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robinfriedli/aiode/bed8fd818131c760056782bf45c1a7d354ca78a7/resources-public/img/botify-logo-legacy.png -------------------------------------------------------------------------------- /resources-public/img/botify-logo-small-legacy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robinfriedli/aiode/bed8fd818131c760056782bf45c1a7d354ca78a7/resources-public/img/botify-logo-small-legacy.png -------------------------------------------------------------------------------- /resources-public/img/botify-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robinfriedli/aiode/bed8fd818131c760056782bf45c1a7d354ca78a7/resources-public/img/botify-logo-small.png -------------------------------------------------------------------------------- /resources-public/img/botify-logo-wide-legacy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robinfriedli/aiode/bed8fd818131c760056782bf45c1a7d354ca78a7/resources-public/img/botify-logo-wide-legacy.png -------------------------------------------------------------------------------- /resources-public/img/botify-logo-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robinfriedli/aiode/bed8fd818131c760056782bf45c1a7d354ca78a7/resources-public/img/botify-logo-wide.png -------------------------------------------------------------------------------- /resources-public/img/botify-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robinfriedli/aiode/bed8fd818131c760056782bf45c1a7d354ca78a7/resources-public/img/botify-logo.png -------------------------------------------------------------------------------- /resources-public/img/filebroker-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robinfriedli/aiode/bed8fd818131c760056782bf45c1a7d354ca78a7/resources-public/img/filebroker-logo-small.png -------------------------------------------------------------------------------- /resources-public/style.css: -------------------------------------------------------------------------------- 1 | .general-wrapper { 2 | font-family: 'DejaVu Sans', Arial, Helvetica, sans-serif; 3 | font-size: 14px; 4 | } 5 | 6 | .head-wrapper { 7 | font-family: 'DejaVu Sans', Arial, Helvetica, sans-serif; 8 | font-size: 20px; 9 | } 10 | 11 | 12 | .content-table td { 13 | padding: 5px; 14 | } 15 | 16 | .head-table td { 17 | padding: 10px; 18 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "aiode" 2 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/audio/AbstractSoftCachedPlayable.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.audio; 2 | 3 | import java.lang.ref.SoftReference; 4 | 5 | import javax.annotation.Nullable; 6 | 7 | import com.sedmelluq.discord.lavaplayer.track.AudioTrack; 8 | 9 | /** 10 | * creates a soft reference to the resulting AudioTrack when playing a Playable 11 | */ 12 | public abstract class AbstractSoftCachedPlayable implements Playable { 13 | 14 | private SoftReference cachedTrack; 15 | 16 | @Nullable 17 | @Override 18 | public AudioTrack getCached() { 19 | if (cachedTrack != null) { 20 | return cachedTrack.get(); 21 | } 22 | 23 | return null; 24 | } 25 | 26 | @Override 27 | public void setCached(AudioTrack audioTrack) { 28 | cachedTrack = new SoftReference<>(audioTrack); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/audio/AudioPlayerSendHandler.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.audio; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; 6 | import com.sedmelluq.discord.lavaplayer.track.playback.MutableAudioFrame; 7 | import net.dv8tion.jda.api.audio.AudioSendHandler; 8 | 9 | public class AudioPlayerSendHandler implements AudioSendHandler { 10 | 11 | private final AudioPlayer audioPlayer; 12 | private final ByteBuffer buffer; 13 | private final MutableAudioFrame audioFrame; 14 | 15 | public AudioPlayerSendHandler(AudioPlayer audioPlayer) { 16 | this.audioPlayer = audioPlayer; 17 | buffer = ByteBuffer.allocate(2048); 18 | audioFrame = new MutableAudioFrame(); 19 | audioFrame.setBuffer(buffer); 20 | } 21 | 22 | @Override 23 | public boolean canProvide() { 24 | return audioPlayer.provide(audioFrame); 25 | } 26 | 27 | @Override 28 | public ByteBuffer provide20MsAudio() { 29 | buffer.flip(); 30 | return buffer; 31 | } 32 | 33 | @Override 34 | public boolean isOpus() { 35 | return true; 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/audio/exec/BlockingTrackLoadingExecutor.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.audio.exec; 2 | 3 | import net.robinfriedli.aiode.command.commands.playlistmanagement.AddCommand; 4 | 5 | /** 6 | * Simple TrackLoadingExecutor that performs the task blocking in the current thread. This is used for commands that 7 | * require all data to be fetched to continue, such as the {@link AddCommand} and its subclasses. 8 | */ 9 | public class BlockingTrackLoadingExecutor implements TrackLoadingExecutor { 10 | 11 | @Override 12 | public void execute(Runnable trackLoadingRunnable) { 13 | trackLoadingRunnable.run(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/audio/exec/TrackLoadingExecutor.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.audio.exec; 2 | 3 | /** 4 | * Interface whose implementations will specify how tracks are loaded, async pooled / replaceable or blocking 5 | */ 6 | public interface TrackLoadingExecutor { 7 | 8 | void execute(Runnable trackLoadingRunnable); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/audio/exec/TrackLoadingRunnable.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.audio.exec; 2 | 3 | import java.util.Collection; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | import net.robinfriedli.aiode.function.ChainableRunnable; 8 | 9 | /** 10 | * Interface for tasks that loads data for tracks asynchronously, e.g. populating YouTube playlists or fetching the matching 11 | * YouTube video for a Spotify track. 12 | * 13 | * @param type of the objects to handle 14 | */ 15 | public interface TrackLoadingRunnable extends ChainableRunnable { 16 | 17 | default void addItem(T item) { 18 | addItems(Collections.singleton(item)); 19 | } 20 | 21 | void addItems(Collection items); 22 | 23 | List getItems(); 24 | 25 | void handleCancellation(); 26 | 27 | void loadItem(T item) throws Exception; 28 | 29 | default void loadItems() throws Exception { 30 | for (T item : getItems()) { 31 | if (Thread.interrupted()) { 32 | handleCancellation(); 33 | break; 34 | } 35 | 36 | try { 37 | loadItem(item); 38 | } catch (Exception e) { 39 | handleCancellation(); 40 | throw e; 41 | } 42 | } 43 | } 44 | 45 | @Override 46 | default void doRun() throws Exception { 47 | loadItems(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/audio/exec/YouTubePlaylistPopulationRunnable.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.audio.exec; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collection; 5 | import java.util.List; 6 | 7 | import net.robinfriedli.aiode.audio.youtube.YouTubePlaylist; 8 | import net.robinfriedli.aiode.audio.youtube.YouTubeService; 9 | 10 | public class YouTubePlaylistPopulationRunnable implements TrackLoadingRunnable { 11 | 12 | private final List youTubePlaylistsToLoad; 13 | private final YouTubeService youTubeService; 14 | 15 | public YouTubePlaylistPopulationRunnable(YouTubeService youTubeService, YouTubePlaylist... playlists) { 16 | this(Arrays.asList(playlists), youTubeService); 17 | } 18 | 19 | public YouTubePlaylistPopulationRunnable(List youTubePlaylistsToLoad, YouTubeService youTubeService) { 20 | this.youTubePlaylistsToLoad = youTubePlaylistsToLoad; 21 | this.youTubeService = youTubeService; 22 | } 23 | 24 | @Override 25 | public void addItems(Collection items) { 26 | youTubePlaylistsToLoad.addAll(items); 27 | } 28 | 29 | @Override 30 | public List getItems() { 31 | return youTubePlaylistsToLoad; 32 | } 33 | 34 | @Override 35 | public void handleCancellation() { 36 | youTubePlaylistsToLoad.forEach(YouTubePlaylist::cancelLoading); 37 | } 38 | 39 | @Override 40 | public void loadItem(YouTubePlaylist item) throws Exception { 41 | youTubeService.populateList(item); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/audio/spotify/SpotifyContext.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.audio.spotify; 2 | 3 | import com.neovisionaries.i18n.CountryCode; 4 | import net.robinfriedli.aiode.login.Login; 5 | import se.michaelthelin.spotify.SpotifyApi; 6 | 7 | public class SpotifyContext { 8 | 9 | private SpotifyApi spotifyApi; 10 | private CountryCode market; 11 | private Login login; 12 | 13 | public CountryCode getMarket() { 14 | return market; 15 | } 16 | 17 | public void setMarket(CountryCode market) { 18 | this.market = market; 19 | } 20 | 21 | public Login getLogin() { 22 | return login; 23 | } 24 | 25 | public void setLogin(Login login) { 26 | this.login = login; 27 | } 28 | 29 | public SpotifyApi getSpotifyApi() { 30 | return spotifyApi; 31 | } 32 | 33 | public void setSpotifyApi(SpotifyApi spotifyApi) { 34 | this.spotifyApi = spotifyApi; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/audio/youtube/YouTubeVideoImpl.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.audio.youtube; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | import javax.annotation.Nullable; 6 | 7 | import net.robinfriedli.aiode.audio.AbstractSoftCachedPlayable; 8 | import net.robinfriedli.aiode.audio.spotify.SpotifyTrack; 9 | 10 | /** 11 | * Represents a fully loaded YouTube video 12 | */ 13 | public class YouTubeVideoImpl extends AbstractSoftCachedPlayable implements YouTubeVideo { 14 | 15 | private final String title; 16 | private final String id; 17 | private final long duration; 18 | private SpotifyTrack redirectedSpotifyTrack; 19 | 20 | public YouTubeVideoImpl(String title, String id, long duration) { 21 | this.title = title; 22 | this.id = id; 23 | this.duration = duration; 24 | } 25 | 26 | @Override 27 | public String getDisplay() { 28 | return title; 29 | } 30 | 31 | @Override 32 | public String getDisplay(long timeOut, TimeUnit unit) { 33 | return getDisplay(); 34 | } 35 | 36 | @Override 37 | public String getDisplayNow(String alternativeValue) { 38 | return getDisplay(); 39 | } 40 | 41 | @Override 42 | public String getVideoId() { 43 | return id; 44 | } 45 | 46 | @Override 47 | public long getDuration() { 48 | return duration; 49 | } 50 | 51 | @Override 52 | public long getDuration(long timeOut, TimeUnit unit) { 53 | return getDuration(); 54 | } 55 | 56 | @Override 57 | @Nullable 58 | public SpotifyTrack getRedirectedSpotifyTrack() { 59 | return redirectedSpotifyTrack; 60 | } 61 | 62 | @Override 63 | public void setRedirectedSpotifyTrack(@Nullable SpotifyTrack track) { 64 | redirectedSpotifyTrack = track; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/boot/AbstractShutdownable.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.boot; 2 | 3 | public abstract class AbstractShutdownable implements Shutdownable { 4 | 5 | protected AbstractShutdownable() { 6 | register(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/boot/Shutdownable.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.boot; 2 | 3 | import net.robinfriedli.aiode.Aiode; 4 | 5 | public interface Shutdownable { 6 | 7 | default void register() { 8 | Aiode.SHUTDOWNABLES.add(this); 9 | } 10 | 11 | void shutdown(int delayMs); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/boot/ShutdownableExecutorService.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.boot; 2 | 3 | import java.util.concurrent.ExecutorService; 4 | 5 | public class ShutdownableExecutorService implements Shutdownable { 6 | 7 | private final ExecutorService executorService; 8 | 9 | public ShutdownableExecutorService(ExecutorService executorService) { 10 | this.executorService = executorService; 11 | } 12 | 13 | @Override 14 | public void shutdown(int delayMs) { 15 | executorService.shutdown(); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/boot/StartupTask.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.boot; 2 | 3 | import javax.annotation.Nullable; 4 | 5 | import net.dv8tion.jda.api.JDA; 6 | import net.robinfriedli.aiode.entities.xml.StartupTaskContribution; 7 | 8 | /** 9 | * Interface for tasks that migrate data for updates or ensure the integrity of the database 10 | */ 11 | public interface StartupTask { 12 | 13 | /** 14 | * The task to run. 15 | * 16 | * @param shard the shard this task is executed for, null if runForEachShard is false 17 | */ 18 | void perform(@Nullable JDA shard) throws Exception; 19 | 20 | default void runTask(@Nullable JDA shard) throws Exception { 21 | if (shard == null && getContribution().getAttribute("runForEachShard").getBool()) { 22 | throw new IllegalStateException("Shard is null despite startupTask being marked runForEachShard"); 23 | } 24 | perform(shard); 25 | } 26 | 27 | StartupTaskContribution getContribution(); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/boot/configurations/SpotifyComponent.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.boot.configurations; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.context.annotation.Scope; 7 | import se.michaelthelin.spotify.SpotifyApi; 8 | import se.michaelthelin.spotify.SpotifyHttpManager; 9 | 10 | @Configuration 11 | public class SpotifyComponent { 12 | 13 | @Value("${aiode.tokens.spotify_client_id}") 14 | private String spotifyClientId; 15 | @Value("${aiode.tokens.spotify_client_secret}") 16 | private String spotifyClientSecret; 17 | @Value("${aiode.server.spotify_login_callback}") 18 | private String redirectUri; 19 | @Value("${aiode.preferences.spotify_market}") 20 | private String defaultMarket; 21 | 22 | @Bean 23 | public SpotifyApi.Builder spotifyApiBuilder() { 24 | return new SpotifyApi.Builder() 25 | .setClientId(spotifyClientId) 26 | .setClientSecret(spotifyClientSecret) 27 | .setRedirectUri(SpotifyHttpManager.makeUri(redirectUri)); 28 | } 29 | 30 | @Bean 31 | @Scope(scopeName = "prototype") 32 | public SpotifyApi spotifyApi(SpotifyApi.Builder builder) { 33 | return builder.build(); 34 | } 35 | 36 | public String getDefaultMarket() { 37 | return defaultMarket; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/boot/configurations/YouTubeComponent.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.boot.configurations; 2 | 3 | import java.io.IOException; 4 | import java.security.GeneralSecurityException; 5 | 6 | import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; 7 | import com.google.api.client.http.javanet.NetHttpTransport; 8 | import com.google.api.client.json.JsonFactory; 9 | import com.google.api.client.json.gson.GsonFactory; 10 | import com.google.api.services.youtube.YouTube; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | 14 | @Configuration 15 | public class YouTubeComponent { 16 | 17 | @Bean 18 | public YouTube getYouTube() { 19 | try { 20 | // setup YouTube API 21 | NetHttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport(); 22 | JsonFactory jsonFactory = GsonFactory.getDefaultInstance(); 23 | return new YouTube.Builder(httpTransport, jsonFactory, httpRequest -> { 24 | // no-op 25 | }).setApplicationName("botify-youtube-search").build(); 26 | } catch (GeneralSecurityException | IOException e) { 27 | throw new RuntimeException("Exception while instantiating YouTube API"); 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/boot/tasks/RefreshPersistentGlobalChartsStartupTask.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.boot.tasks; 2 | 3 | import net.dv8tion.jda.api.JDA; 4 | import net.robinfriedli.aiode.Aiode; 5 | import net.robinfriedli.aiode.audio.ChartService; 6 | import net.robinfriedli.aiode.boot.StartupTask; 7 | import net.robinfriedli.aiode.entities.xml.StartupTaskContribution; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | public class RefreshPersistentGlobalChartsStartupTask implements StartupTask { 11 | 12 | private final ChartService chartService; 13 | private final StartupTaskContribution contribution; 14 | 15 | public RefreshPersistentGlobalChartsStartupTask(ChartService chartService, StartupTaskContribution contribution) { 16 | this.chartService = chartService; 17 | this.contribution = contribution; 18 | } 19 | 20 | @Override 21 | public void perform(@Nullable JDA shard) throws Exception { 22 | Thread persistentChartUpdateThread = new Thread(() -> { 23 | Aiode.LOGGER.info("Starting to refresh persistent global charts"); 24 | long millis = System.currentTimeMillis(); 25 | try { 26 | chartService.refreshPersistentGlobalCharts(); 27 | } catch (Exception e) { 28 | Aiode.LOGGER.error("Error occurred refreshing persistent global charts", e); 29 | } 30 | Aiode.LOGGER.info("Completed refreshing persistent global charts after {}ms", System.currentTimeMillis() - millis); 31 | }); 32 | persistentChartUpdateThread.setName("refresh-persistent-global-charts-thread"); 33 | persistentChartUpdateThread.setDaemon(true); 34 | persistentChartUpdateThread.start(); 35 | } 36 | 37 | @Override 38 | public StartupTaskContribution getContribution() { 39 | return contribution; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/boot/tasks/UpdateTopGGStatsTask.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.boot.tasks; 2 | 3 | import java.util.Objects; 4 | 5 | import net.dv8tion.jda.api.JDA; 6 | import net.robinfriedli.aiode.boot.StartupTask; 7 | import net.robinfriedli.aiode.boot.configurations.TopGGComponent; 8 | import net.robinfriedli.aiode.entities.xml.StartupTaskContribution; 9 | import org.jetbrains.annotations.Nullable; 10 | 11 | public class UpdateTopGGStatsTask implements StartupTask { 12 | 13 | private final StartupTaskContribution contribution; 14 | private final TopGGComponent topGGComponent; 15 | 16 | public UpdateTopGGStatsTask(StartupTaskContribution contribution, TopGGComponent topGGComponent) { 17 | this.contribution = contribution; 18 | this.topGGComponent = topGGComponent; 19 | } 20 | 21 | @Override 22 | public void perform(@Nullable JDA shard) throws Exception { 23 | topGGComponent.updateStatsForShard(Objects.requireNonNull(shard)); 24 | } 25 | 26 | @Override 27 | public StartupTaskContribution getContribution() { 28 | return contribution; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/ClientQuestionEventManager.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import com.google.api.client.util.Lists; 7 | 8 | public class ClientQuestionEventManager { 9 | 10 | /** 11 | * all unanswered Questions. Questions get removed after 5 minutes or after the same user enters a different command 12 | * that triggers a question. 13 | */ 14 | private final List pendingQuestions = Lists.newArrayList(); 15 | 16 | public synchronized void addQuestion(ClientQuestionEvent question) { 17 | Optional existingQuestion = getQuestion(question.getCommandContext()); 18 | existingQuestion.ifPresent(ClientQuestionEvent::destroy); 19 | 20 | pendingQuestions.add(question); 21 | } 22 | 23 | public synchronized void removeQuestion(ClientQuestionEvent question) { 24 | pendingQuestions.remove(question); 25 | } 26 | 27 | public synchronized Optional getQuestion(CommandContext commandContext) { 28 | return pendingQuestions 29 | .stream() 30 | .filter(question -> question.getUser().getId().equals(commandContext.getUser().getId())) 31 | .findFirst(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/argument/ArgumentContributionDelegate.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.argument; 2 | 3 | import net.robinfriedli.aiode.entities.xml.ArgumentContribution; 4 | 5 | /** 6 | * Interface for any argument configuration type that can resolve to an {@link ArgumentContribution}. 7 | */ 8 | public interface ArgumentContributionDelegate { 9 | 10 | ArgumentContribution unwrapArgumentContribution(); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/commands/AbstractQueueLoadingCommand.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.commands; 2 | 3 | import net.robinfriedli.aiode.audio.exec.TrackLoadingExecutor; 4 | import net.robinfriedli.aiode.command.CommandContext; 5 | import net.robinfriedli.aiode.command.CommandManager; 6 | import net.robinfriedli.aiode.entities.xml.CommandContribution; 7 | 8 | public abstract class AbstractQueueLoadingCommand extends AbstractPlayableLoadingCommand { 9 | 10 | public AbstractQueueLoadingCommand(CommandContribution commandContribution, 11 | CommandContext context, 12 | CommandManager commandManager, 13 | String commandString, 14 | boolean requiresInput, 15 | String identifier, 16 | String description, 17 | Category category, 18 | TrackLoadingExecutor trackLoadingExecutor) { 19 | super(commandContribution, context, commandManager, commandString, requiresInput, identifier, description, category, trackLoadingExecutor); 20 | } 21 | 22 | @Override 23 | protected boolean shouldRedirectSpotify() { 24 | return !argumentSet("preview"); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/commands/admin/GarbageCollectCommand.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.commands.admin; 2 | 3 | import net.robinfriedli.aiode.command.AbstractAdminCommand; 4 | import net.robinfriedli.aiode.command.CommandContext; 5 | import net.robinfriedli.aiode.command.CommandManager; 6 | import net.robinfriedli.aiode.entities.xml.CommandContribution; 7 | 8 | public class GarbageCollectCommand extends AbstractAdminCommand { 9 | 10 | private double memoryBefore; 11 | private double memoryAfter; 12 | 13 | public GarbageCollectCommand(CommandContribution commandContribution, CommandContext context, CommandManager commandManager, String commandString, boolean requiresInput, String identifier, String description, Category category) { 14 | super(commandContribution, context, commandManager, commandString, requiresInput, identifier, description, category); 15 | } 16 | 17 | @Override 18 | public void runAdmin() { 19 | Runtime runtime = Runtime.getRuntime(); 20 | memoryBefore = getUsedMemory(runtime); 21 | runtime.gc(); 22 | memoryAfter = getUsedMemory(runtime); 23 | } 24 | 25 | private double getUsedMemory(Runtime runtime) { 26 | // convert to MB by right shifting by 20 bytes (same as dividing by 2^20) 27 | double allocatedMemory = runtime.totalMemory() >> 20; 28 | double allocFreeMemory = runtime.freeMemory() >> 20; 29 | return allocatedMemory - allocFreeMemory; 30 | } 31 | 32 | @Override 33 | public void onSuccess() { 34 | sendSuccess(String.format("Executed garbage collection. Usage went from %f MB to %f MB.", memoryBefore, memoryAfter)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/commands/playback/ClearCommand.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.commands.playback; 2 | 3 | import net.robinfriedli.aiode.Aiode; 4 | import net.robinfriedli.aiode.audio.AudioPlayback; 5 | import net.robinfriedli.aiode.audio.queue.AudioQueue; 6 | import net.robinfriedli.aiode.command.AbstractCommand; 7 | import net.robinfriedli.aiode.command.CommandContext; 8 | import net.robinfriedli.aiode.command.CommandManager; 9 | import net.robinfriedli.aiode.entities.xml.CommandContribution; 10 | 11 | public class ClearCommand extends AbstractCommand { 12 | 13 | public ClearCommand(CommandContribution commandContribution, CommandContext context, CommandManager commandManager, String commandString, boolean requiresInput, String identifier, String description, Category category) { 14 | super(commandContribution, context, commandManager, commandString, requiresInput, identifier, description, category); 15 | } 16 | 17 | @Override 18 | public void doRun() { 19 | AudioPlayback playback = Aiode.get().getAudioManager().getPlaybackForGuild(getContext().getGuild()); 20 | AudioQueue audioQueue = playback.getAudioQueue(); 21 | audioQueue.clear(playback.isPlaying()); 22 | } 23 | 24 | @Override 25 | public void onSuccess() { 26 | sendSuccess("Cleared queue"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/commands/playback/PauseCommand.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.commands.playback; 2 | 3 | import net.dv8tion.jda.api.entities.Guild; 4 | import net.robinfriedli.aiode.Aiode; 5 | import net.robinfriedli.aiode.audio.AudioPlayback; 6 | import net.robinfriedli.aiode.command.AbstractCommand; 7 | import net.robinfriedli.aiode.command.CommandContext; 8 | import net.robinfriedli.aiode.command.CommandManager; 9 | import net.robinfriedli.aiode.entities.xml.CommandContribution; 10 | 11 | public class PauseCommand extends AbstractCommand { 12 | 13 | public PauseCommand(CommandContribution commandContribution, CommandContext context, CommandManager commandManager, String commandString, boolean requiresValue, String identifier, String description, Category category) { 14 | super(commandContribution, context, commandManager, commandString, requiresValue, identifier, description, category); 15 | } 16 | 17 | @Override 18 | public void doRun() { 19 | Guild guild = getContext().getGuild(); 20 | AudioPlayback playback = Aiode.get().getAudioManager().getPlaybackForGuild(guild); 21 | playback.pause(); 22 | } 23 | 24 | @Override 25 | public void onSuccess() { 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/commands/playback/RepeatCommand.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.commands.playback; 2 | 3 | import net.robinfriedli.aiode.Aiode; 4 | import net.robinfriedli.aiode.audio.AudioPlayback; 5 | import net.robinfriedli.aiode.command.AbstractCommand; 6 | import net.robinfriedli.aiode.command.CommandContext; 7 | import net.robinfriedli.aiode.command.CommandManager; 8 | import net.robinfriedli.aiode.entities.xml.CommandContribution; 9 | 10 | public class RepeatCommand extends AbstractCommand { 11 | 12 | public RepeatCommand(CommandContribution commandContribution, CommandContext context, CommandManager commandManager, String commandString, boolean requiresInput, String identifier, String description, Category category) { 13 | super(commandContribution, context, commandManager, commandString, requiresInput, identifier, description, category); 14 | } 15 | 16 | @Override 17 | public void doRun() { 18 | AudioPlayback playback = Aiode.get().getAudioManager().getPlaybackForGuild(getContext().getGuild()); 19 | if (argumentSet("one")) { 20 | playback.setRepeatOne(!playback.isRepeatOne()); 21 | } else { 22 | playback.setRepeatAll(!playback.isRepeatAll()); 23 | } 24 | } 25 | 26 | @Override 27 | public void onSuccess() { 28 | AudioPlayback playback = Aiode.get().getAudioManager().getPlaybackForGuild(getContext().getGuild()); 29 | if (argumentSet("one")) { 30 | sendSuccess("Repeat one set to " + playback.isRepeatOne()); 31 | } else { 32 | sendSuccess("Repeat all set to " + playback.isRepeatAll()); 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/commands/playback/StopCommand.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.commands.playback; 2 | 3 | import net.dv8tion.jda.api.entities.Guild; 4 | import net.robinfriedli.aiode.Aiode; 5 | import net.robinfriedli.aiode.audio.AudioManager; 6 | import net.robinfriedli.aiode.audio.AudioPlayback; 7 | import net.robinfriedli.aiode.command.AbstractCommand; 8 | import net.robinfriedli.aiode.command.CommandContext; 9 | import net.robinfriedli.aiode.command.CommandManager; 10 | import net.robinfriedli.aiode.entities.xml.CommandContribution; 11 | 12 | public class StopCommand extends AbstractCommand { 13 | 14 | public StopCommand(CommandContribution commandContribution, CommandContext context, CommandManager commandManager, String commandString, boolean requiresInput, String identifier, String description, Category category) { 15 | super(commandContribution, context, commandManager, commandString, requiresInput, identifier, description, category); 16 | } 17 | 18 | @Override 19 | public void doRun() { 20 | Guild guild = getContext().getGuild(); 21 | AudioManager audioManager = Aiode.get().getAudioManager(); 22 | AudioPlayback playback = audioManager.getPlaybackForGuild(guild); 23 | getContext().getGuildContext().getReplaceableTrackLoadingExecutor().abort(); 24 | playback.stop(); 25 | playback.getAudioQueue().clear(); 26 | } 27 | 28 | @Override 29 | public void onSuccess() { 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/commands/playback/VolumeCommand.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.commands.playback; 2 | 3 | import net.robinfriedli.aiode.Aiode; 4 | import net.robinfriedli.aiode.audio.AudioPlayback; 5 | import net.robinfriedli.aiode.command.AbstractCommand; 6 | import net.robinfriedli.aiode.command.CommandContext; 7 | import net.robinfriedli.aiode.command.CommandManager; 8 | import net.robinfriedli.aiode.entities.xml.CommandContribution; 9 | import net.robinfriedli.aiode.exceptions.InvalidCommandException; 10 | 11 | public class VolumeCommand extends AbstractCommand { 12 | 13 | public VolumeCommand(CommandContribution commandContribution, CommandContext context, CommandManager commandManager, String commandString, boolean requiresInput, String identifier, String description, Category category) { 14 | super(commandContribution, context, commandManager, commandString, requiresInput, identifier, description, category); 15 | } 16 | 17 | @Override 18 | public void doRun() { 19 | AudioPlayback playback = Aiode.get().getAudioManager().getPlaybackForGuild(getContext().getGuild()); 20 | 21 | int volume; 22 | try { 23 | volume = Integer.parseInt(getCommandInput()); 24 | } catch (NumberFormatException e) { 25 | throw new InvalidCommandException("'" + getCommandInput() + "' is not an integer"); 26 | } 27 | 28 | if (!(volume > 0 && volume <= 200)) { 29 | throw new InvalidCommandException("Expected a value between 1 and 200"); 30 | } 31 | 32 | playback.setVolume(volume); 33 | } 34 | 35 | @Override 36 | public void onSuccess() { 37 | AudioPlayback playback = Aiode.get().getAudioManager().getPlaybackForGuild(getContext().getGuild()); 38 | sendSuccess("Volume set to: " + playback.getVolume()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/commands/playlistmanagement/CreateCommand.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.commands.playlistmanagement; 2 | 3 | import net.robinfriedli.aiode.command.AbstractCommand; 4 | import net.robinfriedli.aiode.command.CommandContext; 5 | import net.robinfriedli.aiode.command.CommandManager; 6 | import net.robinfriedli.aiode.entities.Playlist; 7 | import net.robinfriedli.aiode.entities.xml.CommandContribution; 8 | import net.robinfriedli.aiode.exceptions.InvalidCommandException; 9 | import net.robinfriedli.aiode.util.SearchEngine; 10 | import org.hibernate.Session; 11 | 12 | public class CreateCommand extends AbstractCommand { 13 | 14 | public CreateCommand(CommandContribution commandContribution, CommandContext context, CommandManager commandManager, String commandString, boolean requiresInput, String identifier, String description, Category category) { 15 | super(commandContribution, context, commandManager, commandString, requiresInput, identifier, description, category); 16 | } 17 | 18 | @Override 19 | public void doRun() { 20 | Session session = getContext().getSession(); 21 | Playlist existingPlaylist = SearchEngine.searchLocalList(session, getCommandInput()); 22 | 23 | if (existingPlaylist != null) { 24 | throw new InvalidCommandException("Playlist " + getCommandInput() + " already exists"); 25 | } 26 | 27 | Playlist playlist = new Playlist(getCommandInput(), getContext().getUser(), getContext().getGuild()); 28 | invoke(() -> session.persist(playlist)); 29 | } 30 | 31 | @Override 32 | public void onSuccess() { 33 | // notification sent by interceptor 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/commands/playlistmanagement/DeleteCommand.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.commands.playlistmanagement; 2 | 3 | import net.robinfriedli.aiode.command.AbstractCommand; 4 | import net.robinfriedli.aiode.command.CommandContext; 5 | import net.robinfriedli.aiode.command.CommandManager; 6 | import net.robinfriedli.aiode.entities.Playlist; 7 | import net.robinfriedli.aiode.entities.xml.CommandContribution; 8 | import net.robinfriedli.aiode.exceptions.NoResultsFoundException; 9 | import net.robinfriedli.aiode.util.SearchEngine; 10 | import org.hibernate.Session; 11 | 12 | public class DeleteCommand extends AbstractCommand { 13 | 14 | public DeleteCommand(CommandContribution commandContribution, CommandContext context, CommandManager commandManager, String commandString, boolean requiresInput, String identifier, String description, Category category) { 15 | super(commandContribution, context, commandManager, commandString, requiresInput, identifier, description, category); 16 | } 17 | 18 | @Override 19 | public void doRun() { 20 | Session session = getContext().getSession(); 21 | Playlist playlist = SearchEngine.searchLocalList(session, getCommandInput()); 22 | 23 | if (playlist == null) { 24 | throw new NoResultsFoundException(String.format("No local list found for '%s'", getCommandInput())); 25 | } 26 | 27 | invoke(() -> { 28 | playlist.getItems().forEach(session::remove); 29 | session.remove(playlist); 30 | }); 31 | } 32 | 33 | @Override 34 | public void onSuccess() { 35 | // notification sent by interceptor 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/commands/playlistmanagement/SynchroniseCommand.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.commands.playlistmanagement; 2 | 3 | import java.util.List; 4 | 5 | import net.robinfriedli.aiode.command.CommandContext; 6 | import net.robinfriedli.aiode.command.CommandManager; 7 | import net.robinfriedli.aiode.entities.Playlist; 8 | import net.robinfriedli.aiode.entities.PlaylistItem; 9 | import net.robinfriedli.aiode.entities.xml.CommandContribution; 10 | import net.robinfriedli.aiode.exceptions.NoResultsFoundException; 11 | import org.hibernate.Session; 12 | 13 | public class SynchroniseCommand extends AddCommand { 14 | 15 | public SynchroniseCommand(CommandContribution commandContribution, CommandContext context, CommandManager commandManager, String commandBody, boolean requiresInput, String identifier, String description, Category category) { 16 | super(commandContribution, context, commandManager, commandBody, requiresInput, identifier, description, category, "with"); 17 | } 18 | 19 | @Override 20 | protected void addToList(Playlist playlist, List items) { 21 | if (items.isEmpty()) { 22 | throw new NoResultsFoundException("Result is empty!"); 23 | } 24 | 25 | invoke(() -> { 26 | if (!playlist.isEmpty()) { 27 | Session session = getContext().getSession(); 28 | for (PlaylistItem item : playlist.getItems()) { 29 | session.remove(item); 30 | } 31 | } 32 | super.addToList(playlist, items); 33 | }); 34 | } 35 | 36 | @Override 37 | public void onSuccess() { 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/commands/scripting/FinalizerCommand.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.commands.scripting; 2 | 3 | import net.robinfriedli.aiode.command.CommandContext; 4 | import net.robinfriedli.aiode.command.CommandManager; 5 | import net.robinfriedli.aiode.entities.xml.CommandContribution; 6 | 7 | public class FinalizerCommand extends AbstractScriptCommand { 8 | 9 | public FinalizerCommand(CommandContribution commandContribution, CommandContext context, CommandManager commandManager, String commandBody, boolean requiresInput, String identifier, String description, Category category) { 10 | super(commandContribution, context, commandManager, commandBody, requiresInput, identifier, description, category, "finalizer"); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/commands/scripting/InterceptorCommand.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.commands.scripting; 2 | 3 | import net.robinfriedli.aiode.command.CommandContext; 4 | import net.robinfriedli.aiode.command.CommandManager; 5 | import net.robinfriedli.aiode.entities.xml.CommandContribution; 6 | 7 | public class InterceptorCommand extends AbstractScriptCommand { 8 | 9 | public InterceptorCommand(CommandContribution commandContribution, CommandContext context, CommandManager commandManager, String commandBody, boolean requiresInput, String identifier, String description, Category category) { 10 | super(commandContribution, context, commandManager, commandBody, requiresInput, identifier, description, category, "interceptor"); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/commands/spotify/LogoutCommand.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.commands.spotify; 2 | 3 | import net.dv8tion.jda.api.entities.User; 4 | import net.robinfriedli.aiode.Aiode; 5 | import net.robinfriedli.aiode.command.AbstractCommand; 6 | import net.robinfriedli.aiode.command.CommandContext; 7 | import net.robinfriedli.aiode.command.CommandManager; 8 | import net.robinfriedli.aiode.entities.xml.CommandContribution; 9 | import net.robinfriedli.aiode.login.Login; 10 | import net.robinfriedli.aiode.login.LoginManager; 11 | 12 | public class LogoutCommand extends AbstractCommand { 13 | 14 | public LogoutCommand(CommandContribution commandContribution, CommandContext context, CommandManager commandManager, String commandString, boolean requiresInput, String identifier, String description, Category category) { 15 | super(commandContribution, context, commandManager, commandString, requiresInput, identifier, description, category); 16 | } 17 | 18 | @Override 19 | public void doRun() { 20 | LoginManager loginManager = Aiode.get().getLoginManager(); 21 | User user = getContext().getUser(); 22 | Login login = loginManager.requireLoginForUser(user); 23 | login.cancel(); 24 | loginManager.removeLogin(user); 25 | } 26 | 27 | @Override 28 | public void onSuccess() { 29 | sendSuccess("User " + getContext().getUser().getName() + " logged out from Spotify."); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/interceptor/AbstractChainableCommandInterceptor.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.interceptor; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import net.robinfriedli.aiode.command.Command; 7 | import net.robinfriedli.aiode.entities.xml.CommandInterceptorContribution; 8 | 9 | /** 10 | * CommandInterceptor extension that calls the next interceptor and handles exceptions according to the 11 | * {@link CommandInterceptorContribution} itself. 12 | */ 13 | public abstract class AbstractChainableCommandInterceptor implements CommandInterceptor { 14 | 15 | private final CommandInterceptorContribution contribution; 16 | private final CommandInterceptor next; 17 | 18 | protected AbstractChainableCommandInterceptor(CommandInterceptorContribution contribution, CommandInterceptor next) { 19 | this.contribution = contribution; 20 | this.next = next; 21 | } 22 | 23 | @Override 24 | public void intercept(Command command) { 25 | try { 26 | performChained(command); 27 | } catch (Exception e) { 28 | if (contribution.throwException(e)) { 29 | throw e; 30 | } else { 31 | Logger logger = LoggerFactory.getLogger(getClass()); 32 | logger.error("Unexpected exception in interceptor", e); 33 | } 34 | } 35 | 36 | next.intercept(command); 37 | } 38 | 39 | /** 40 | * Perform the task of this interceptor handling exceptions according to the CommandInterceptor's contribution 41 | */ 42 | public abstract void performChained(Command command); 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/interceptor/CommandInterceptor.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.interceptor; 2 | 3 | import net.robinfriedli.aiode.command.Command; 4 | 5 | /** 6 | * Interface for classes that intercept and run the command execution. Its implementations are added and configured in 7 | * the commandInterceptors XML file 8 | */ 9 | public interface CommandInterceptor { 10 | 11 | void intercept(Command command); 12 | 13 | class EmptyCommandInterceptor implements CommandInterceptor { 14 | 15 | @Override 16 | public void intercept(Command command) { 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/interceptor/interceptors/CommandParserInterceptor.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.interceptor.interceptors; 2 | 3 | import net.robinfriedli.aiode.command.AbstractCommand; 4 | import net.robinfriedli.aiode.command.Command; 5 | import net.robinfriedli.aiode.command.interceptor.AbstractChainableCommandInterceptor; 6 | import net.robinfriedli.aiode.command.interceptor.CommandInterceptor; 7 | import net.robinfriedli.aiode.command.parser.CommandParser; 8 | import net.robinfriedli.aiode.discord.property.properties.ArgumentPrefixProperty; 9 | import net.robinfriedli.aiode.entities.xml.CommandInterceptorContribution; 10 | import net.robinfriedli.aiode.exceptions.CommandParseException; 11 | import net.robinfriedli.aiode.exceptions.UnexpectedCommandSetupException; 12 | 13 | /** 14 | * Interceptor that parses the used arguments and command input for text based commands by calling the {@link CommandParser} 15 | */ 16 | public class CommandParserInterceptor extends AbstractChainableCommandInterceptor { 17 | 18 | public CommandParserInterceptor(CommandInterceptorContribution contribution, CommandInterceptor next) { 19 | super(contribution, next); 20 | } 21 | 22 | @Override 23 | public void performChained(Command command) { 24 | if (command instanceof AbstractCommand textBasedCommand && !textBasedCommand.getContext().isSlashCommand()) { 25 | CommandParser commandParser = new CommandParser(textBasedCommand, ArgumentPrefixProperty.getForCurrentContext()); 26 | 27 | try { 28 | commandParser.parse(); 29 | } catch (CommandParseException e) { 30 | throw e; 31 | } catch (Exception e) { 32 | throw new UnexpectedCommandSetupException(e); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/interceptor/interceptors/CommandVerificationInterceptor.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.interceptor.interceptors; 2 | 3 | import net.robinfriedli.aiode.command.AbstractCommand; 4 | import net.robinfriedli.aiode.command.Command; 5 | import net.robinfriedli.aiode.command.interceptor.AbstractChainableCommandInterceptor; 6 | import net.robinfriedli.aiode.command.interceptor.CommandInterceptor; 7 | import net.robinfriedli.aiode.entities.xml.CommandInterceptorContribution; 8 | import net.robinfriedli.aiode.exceptions.InvalidCommandException; 9 | import net.robinfriedli.aiode.exceptions.UnexpectedCommandSetupException; 10 | import net.robinfriedli.aiode.exceptions.UserException; 11 | 12 | /** 13 | * Interceptor that verifies a command by checking all argument rules and command input 14 | */ 15 | public class CommandVerificationInterceptor extends AbstractChainableCommandInterceptor { 16 | 17 | public CommandVerificationInterceptor(CommandInterceptorContribution contribution, CommandInterceptor next) { 18 | super(contribution, next); 19 | } 20 | 21 | @Override 22 | public void performChained(Command command) { 23 | if (command instanceof AbstractCommand) { 24 | AbstractCommand textBasedCommand = (AbstractCommand) command; 25 | try { 26 | textBasedCommand.verify(); 27 | } catch (UserException e) { 28 | throw e; 29 | } catch (Exception e) { 30 | throw new UnexpectedCommandSetupException("Unexpected exception while verifying command", e); 31 | } 32 | 33 | if (textBasedCommand.getCommandInput().length() > 1000) { 34 | throw new InvalidCommandException("Command input exceeds maximum length of 1000 characters."); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/widget/AbstractDecoratingWidget.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.widget; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | 5 | import net.dv8tion.jda.api.entities.Guild; 6 | import net.dv8tion.jda.api.entities.Message; 7 | 8 | /** 9 | * Widget specialisation for widgets that decorate an existing message. 10 | */ 11 | public abstract class AbstractDecoratingWidget extends AbstractWidget { 12 | 13 | // this message won't be stored for long and is just used for the initial setup, it's fine to directly store the jda entity here 14 | private final Message decoratedMessage; 15 | 16 | public AbstractDecoratingWidget(WidgetRegistry widgetRegistry, Guild guild, Message decoratedMessage) { 17 | super(widgetRegistry, guild, decoratedMessage.getChannel()); 18 | this.decoratedMessage = decoratedMessage; 19 | } 20 | 21 | @Override 22 | public CompletableFuture prepareInitialMessage() { 23 | return CompletableFuture.completedFuture(decoratedMessage); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/widget/AbstractPaginationAction.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.widget; 2 | 3 | import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; 4 | import net.robinfriedli.aiode.command.CommandContext; 5 | 6 | public abstract class AbstractPaginationAction extends AbstractWidgetAction { 7 | 8 | public AbstractPaginationAction(String identifier, String emojiUnicode, boolean resetRequired, CommandContext context, AbstractWidget widget, ButtonInteractionEvent event, WidgetManager.WidgetActionDefinition widgetActionDefinition) { 9 | super(identifier, emojiUnicode, resetRequired, context, widget, event, widgetActionDefinition); 10 | } 11 | 12 | protected AbstractPaginationWidget getPaginationWidget() { 13 | AbstractWidget widget = getWidget(); 14 | 15 | if (!(widget instanceof AbstractPaginationWidget)) { 16 | throw new IllegalStateException(String.format("Action %s can only be used for widgets that implement %s", getClass().getSimpleName(), AbstractPaginationWidget.class)); 17 | } 18 | 19 | return (AbstractPaginationWidget) widget; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/widget/actions/FirstPageAction.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.widget.actions; 2 | 3 | import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; 4 | import net.robinfriedli.aiode.command.CommandContext; 5 | import net.robinfriedli.aiode.command.widget.AbstractPaginationAction; 6 | import net.robinfriedli.aiode.command.widget.AbstractPaginationWidget; 7 | import net.robinfriedli.aiode.command.widget.AbstractWidget; 8 | import net.robinfriedli.aiode.command.widget.WidgetManager; 9 | 10 | public class FirstPageAction extends AbstractPaginationAction { 11 | 12 | public FirstPageAction(String identifier, String emojiUnicode, boolean resetRequired, CommandContext context, AbstractWidget widget, ButtonInteractionEvent event, WidgetManager.WidgetActionDefinition widgetActionDefinition) { 13 | super(identifier, emojiUnicode, resetRequired, context, widget, event, widgetActionDefinition); 14 | } 15 | 16 | @Override 17 | public void doRun() throws Exception { 18 | AbstractPaginationWidget paginationWidget = getPaginationWidget(); 19 | 20 | int pageCount = paginationWidget.getPages().size(); 21 | 22 | if (pageCount > 1) { 23 | paginationWidget.setCurrentPage(0); 24 | } else { 25 | setFailed(true); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/widget/actions/LastPageAction.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.widget.actions; 2 | 3 | import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; 4 | import net.robinfriedli.aiode.command.CommandContext; 5 | import net.robinfriedli.aiode.command.widget.AbstractPaginationAction; 6 | import net.robinfriedli.aiode.command.widget.AbstractPaginationWidget; 7 | import net.robinfriedli.aiode.command.widget.AbstractWidget; 8 | import net.robinfriedli.aiode.command.widget.WidgetManager; 9 | 10 | public class LastPageAction extends AbstractPaginationAction { 11 | 12 | public LastPageAction(String identifier, String emojiUnicode, boolean resetRequired, CommandContext context, AbstractWidget widget, ButtonInteractionEvent event, WidgetManager.WidgetActionDefinition widgetActionDefinition) { 13 | super(identifier, emojiUnicode, resetRequired, context, widget, event, widgetActionDefinition); 14 | } 15 | 16 | @Override 17 | public void doRun() throws Exception { 18 | AbstractPaginationWidget paginationWidget = getPaginationWidget(); 19 | 20 | int pageCount = paginationWidget.getPages().size(); 21 | if (pageCount > 1) { 22 | paginationWidget.setCurrentPage(pageCount - 1); 23 | } else { 24 | setFailed(true); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/widget/actions/NextPageAction.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.widget.actions; 2 | 3 | import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; 4 | import net.robinfriedli.aiode.command.CommandContext; 5 | import net.robinfriedli.aiode.command.widget.AbstractPaginationAction; 6 | import net.robinfriedli.aiode.command.widget.AbstractPaginationWidget; 7 | import net.robinfriedli.aiode.command.widget.AbstractWidget; 8 | import net.robinfriedli.aiode.command.widget.WidgetManager; 9 | 10 | public class NextPageAction extends AbstractPaginationAction { 11 | 12 | public NextPageAction(String identifier, String emojiUnicode, boolean resetRequired, CommandContext context, AbstractWidget widget, ButtonInteractionEvent event, WidgetManager.WidgetActionDefinition widgetActionDefinition) { 13 | super(identifier, emojiUnicode, resetRequired, context, widget, event, widgetActionDefinition); 14 | } 15 | 16 | @Override 17 | public void doRun() throws Exception { 18 | AbstractPaginationWidget paginationWidget = getPaginationWidget(); 19 | 20 | int pageCount = paginationWidget.getPages().size(); 21 | int currentPage = paginationWidget.getCurrentPage(); 22 | 23 | if (pageCount > 1) { 24 | if (currentPage < pageCount - 1) { 25 | paginationWidget.incrementPage(); 26 | } else { 27 | paginationWidget.setCurrentPage(0); 28 | } 29 | } else { 30 | setFailed(true); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/widget/actions/PrevPageAction.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.widget.actions; 2 | 3 | import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; 4 | import net.robinfriedli.aiode.command.CommandContext; 5 | import net.robinfriedli.aiode.command.widget.AbstractPaginationAction; 6 | import net.robinfriedli.aiode.command.widget.AbstractPaginationWidget; 7 | import net.robinfriedli.aiode.command.widget.AbstractWidget; 8 | import net.robinfriedli.aiode.command.widget.WidgetManager; 9 | 10 | public class PrevPageAction extends AbstractPaginationAction { 11 | 12 | public PrevPageAction(String identifier, String emojiUnicode, boolean resetRequired, CommandContext context, AbstractWidget widget, ButtonInteractionEvent event, WidgetManager.WidgetActionDefinition widgetActionDefinition) { 13 | super(identifier, emojiUnicode, resetRequired, context, widget, event, widgetActionDefinition); 14 | } 15 | 16 | @Override 17 | public void doRun() throws Exception { 18 | AbstractPaginationWidget paginationWidget = getPaginationWidget(); 19 | 20 | int pageCount = paginationWidget.getPages().size(); 21 | int currentPage = paginationWidget.getCurrentPage(); 22 | 23 | if (pageCount > 1) { 24 | if (currentPage > 0) { 25 | paginationWidget.decrementPage(); 26 | } else { 27 | paginationWidget.setCurrentPage(pageCount - 1); 28 | } 29 | } else { 30 | setFailed(true); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/widget/actions/RepeatAllAction.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.widget.actions; 2 | 3 | import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; 4 | import net.robinfriedli.aiode.audio.AudioPlayback; 5 | import net.robinfriedli.aiode.command.CommandContext; 6 | import net.robinfriedli.aiode.command.widget.AbstractWidget; 7 | import net.robinfriedli.aiode.command.widget.AbstractWidgetAction; 8 | import net.robinfriedli.aiode.command.widget.WidgetManager; 9 | 10 | public class RepeatAllAction extends AbstractWidgetAction { 11 | 12 | private final AudioPlayback audioPlayback; 13 | 14 | public RepeatAllAction(String identifier, String emojiUnicode, boolean resetRequired, CommandContext context, AbstractWidget widget, ButtonInteractionEvent event, WidgetManager.WidgetActionDefinition widgetActionDefinition) { 15 | super(identifier, emojiUnicode, resetRequired, context, widget, event, widgetActionDefinition); 16 | audioPlayback = context.getGuildContext().getPlayback(); 17 | } 18 | 19 | @Override 20 | public void doRun() { 21 | audioPlayback.setRepeatAll(!audioPlayback.isRepeatAll()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/widget/actions/RepeatOneAction.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.widget.actions; 2 | 3 | import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; 4 | import net.robinfriedli.aiode.audio.AudioPlayback; 5 | import net.robinfriedli.aiode.command.CommandContext; 6 | import net.robinfriedli.aiode.command.widget.AbstractWidget; 7 | import net.robinfriedli.aiode.command.widget.AbstractWidgetAction; 8 | import net.robinfriedli.aiode.command.widget.WidgetManager; 9 | 10 | public class RepeatOneAction extends AbstractWidgetAction { 11 | 12 | private final AudioPlayback audioPlayback; 13 | 14 | public RepeatOneAction(String identifier, String emojiUnicode, boolean resetRequired, CommandContext context, AbstractWidget widget, ButtonInteractionEvent event, WidgetManager.WidgetActionDefinition widgetActionDefinition) { 15 | super(identifier, emojiUnicode, resetRequired, context, widget, event, widgetActionDefinition); 16 | audioPlayback = context.getGuildContext().getPlayback(); 17 | } 18 | 19 | @Override 20 | public void doRun() { 21 | audioPlayback.setRepeatOne(!audioPlayback.isRepeatOne()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/widget/actions/ShuffleAction.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.widget.actions; 2 | 3 | import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; 4 | import net.robinfriedli.aiode.audio.AudioPlayback; 5 | import net.robinfriedli.aiode.command.CommandContext; 6 | import net.robinfriedli.aiode.command.widget.AbstractWidget; 7 | import net.robinfriedli.aiode.command.widget.AbstractWidgetAction; 8 | import net.robinfriedli.aiode.command.widget.WidgetManager; 9 | 10 | public class ShuffleAction extends AbstractWidgetAction { 11 | 12 | private final AudioPlayback audioPlayback; 13 | 14 | public ShuffleAction(String identifier, String emojiUnicode, boolean resetRequired, CommandContext context, AbstractWidget widget, ButtonInteractionEvent event, WidgetManager.WidgetActionDefinition widgetActionDefinition) { 15 | super(identifier, emojiUnicode, resetRequired, context, widget, event, widgetActionDefinition); 16 | audioPlayback = context.getGuildContext().getPlayback(); 17 | } 18 | 19 | @Override 20 | public void doRun() { 21 | audioPlayback.setShuffle(!audioPlayback.isShuffle()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/widget/actions/VolumeDownAction.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.widget.actions; 2 | 3 | import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; 4 | import net.robinfriedli.aiode.audio.AudioPlayback; 5 | import net.robinfriedli.aiode.command.CommandContext; 6 | import net.robinfriedli.aiode.command.widget.AbstractWidget; 7 | import net.robinfriedli.aiode.command.widget.AbstractWidgetAction; 8 | import net.robinfriedli.aiode.command.widget.WidgetManager; 9 | import net.robinfriedli.aiode.util.EmojiConstants; 10 | 11 | /** 12 | * Action registered on the {@link EmojiConstants#VOLUME_DOWN} emoji that decreases the volume of the currently playing audio. 13 | */ 14 | public class VolumeDownAction extends AbstractWidgetAction { 15 | 16 | public VolumeDownAction(String identifier, String emojiUnicode, boolean resetRequired, CommandContext context, AbstractWidget widget, ButtonInteractionEvent event, WidgetManager.WidgetActionDefinition widgetActionDefinition) { 17 | super(identifier, emojiUnicode, resetRequired, context, widget, event, widgetActionDefinition); 18 | } 19 | 20 | @Override 21 | public void doRun() { 22 | AudioPlayback audioPlayback = getContext().getGuildContext().getPlayback(); 23 | audioPlayback.setVolume(audioPlayback.getVolume() - 10); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/widget/actions/VolumeUpAction.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.widget.actions; 2 | 3 | import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; 4 | import net.robinfriedli.aiode.audio.AudioPlayback; 5 | import net.robinfriedli.aiode.command.CommandContext; 6 | import net.robinfriedli.aiode.command.widget.AbstractWidget; 7 | import net.robinfriedli.aiode.command.widget.AbstractWidgetAction; 8 | import net.robinfriedli.aiode.command.widget.WidgetManager; 9 | import net.robinfriedli.aiode.util.EmojiConstants; 10 | 11 | /** 12 | * Action registered on the {@link EmojiConstants#VOLUME_UP} emoji that increases the volume of the currently playing audio. 13 | */ 14 | public class VolumeUpAction extends AbstractWidgetAction { 15 | 16 | public VolumeUpAction(String identifier, String emojiUnicode, boolean resetRequired, CommandContext context, AbstractWidget widget, ButtonInteractionEvent event, WidgetManager.WidgetActionDefinition widgetActionDefinition) { 17 | super(identifier, emojiUnicode, resetRequired, context, widget, event, widgetActionDefinition); 18 | } 19 | 20 | @Override 21 | public void doRun() { 22 | AudioPlayback audioPlayback = getContext().getGuildContext().getPlayback(); 23 | audioPlayback.setVolume(audioPlayback.getVolume() + 10); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/widget/widgets/NowPlayingWidget.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.widget.widgets; 2 | 3 | import net.dv8tion.jda.api.entities.Guild; 4 | import net.dv8tion.jda.api.entities.Message; 5 | import net.dv8tion.jda.api.entities.MessageEmbed; 6 | import net.robinfriedli.aiode.command.widget.AbstractDecoratingWidget; 7 | import net.robinfriedli.aiode.command.widget.WidgetRegistry; 8 | 9 | public class NowPlayingWidget extends AbstractDecoratingWidget { 10 | 11 | public NowPlayingWidget(WidgetRegistry widgetRegistry, Guild guild, Message message) { 12 | super(widgetRegistry, guild, message); 13 | } 14 | 15 | @Override 16 | public MessageEmbed reset() { 17 | setMessageDeleted(true); 18 | // if a different track is played after using the skip or rewind action, the old "now playing" message will get 19 | // deleted by the AudioPlayback anyway 20 | return null; 21 | } 22 | 23 | @Override 24 | public void destroy() { 25 | getWidgetRegistry().removeWidget(this); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/widget/widgets/PaginatedMessageEmbedWidget.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.widget.widgets; 2 | 3 | import java.util.List; 4 | 5 | import net.dv8tion.jda.api.EmbedBuilder; 6 | import net.dv8tion.jda.api.entities.Guild; 7 | import net.dv8tion.jda.api.entities.MessageEmbed; 8 | import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; 9 | import net.robinfriedli.aiode.command.widget.AbstractPaginationWidget; 10 | import net.robinfriedli.aiode.command.widget.WidgetRegistry; 11 | import org.jetbrains.annotations.Nullable; 12 | 13 | public class PaginatedMessageEmbedWidget extends AbstractPaginationWidget { 14 | 15 | private final EmbedBuilder embedBuilder; 16 | 17 | public PaginatedMessageEmbedWidget(WidgetRegistry widgetRegistry, Guild guild, MessageChannel channel, EmbedBuilder embedBuilder) { 18 | super(widgetRegistry, guild, channel, embedBuilder.getFields(), 25); 19 | this.embedBuilder = embedBuilder; 20 | } 21 | 22 | 23 | // title and description provided by prepareEmbedBuilder() 24 | 25 | @Override 26 | protected String getTitle() { 27 | return ""; 28 | } 29 | 30 | @Nullable 31 | @Override 32 | protected String getDescription() { 33 | return null; 34 | } 35 | 36 | @Override 37 | protected void handlePage(EmbedBuilder embedBuilder, List page) { 38 | for (MessageEmbed.Field field : page) { 39 | embedBuilder.addField(field); 40 | } 41 | } 42 | 43 | @Override 44 | protected EmbedBuilder prepareEmbedBuilder() { 45 | EmbedBuilder builderForPage = new EmbedBuilder(); 46 | builderForPage.copyFrom(embedBuilder); 47 | builderForPage.clearFields(); 48 | return builderForPage; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/command/widget/widgets/QueueWidget.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.command.widget.widgets; 2 | 3 | import net.dv8tion.jda.api.EmbedBuilder; 4 | import net.dv8tion.jda.api.entities.Guild; 5 | import net.dv8tion.jda.api.entities.Message; 6 | import net.dv8tion.jda.api.entities.MessageEmbed; 7 | import net.robinfriedli.aiode.Aiode; 8 | import net.robinfriedli.aiode.audio.AudioPlayback; 9 | import net.robinfriedli.aiode.command.widget.AbstractDecoratingWidget; 10 | import net.robinfriedli.aiode.command.widget.WidgetRegistry; 11 | import net.robinfriedli.aiode.discord.MessageService; 12 | 13 | public class QueueWidget extends AbstractDecoratingWidget { 14 | 15 | private final AudioPlayback audioPlayback; 16 | 17 | public QueueWidget(WidgetRegistry widgetRegistry, Guild guild, Message message, AudioPlayback audioPlayback) { 18 | super(widgetRegistry, guild, message); 19 | this.audioPlayback = audioPlayback; 20 | } 21 | 22 | @Override 23 | public MessageEmbed reset() { 24 | MessageService messageService = Aiode.get().getMessageService(); 25 | Guild guild = getGuild().get(); 26 | 27 | EmbedBuilder embedBuilder = audioPlayback.getAudioQueue().buildMessageEmbed(audioPlayback, guild); 28 | return messageService.buildEmbed(embedBuilder); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/concurrent/CloseableThreadContext.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.concurrent; 2 | 3 | /** 4 | * Interface for contexts that may be installed on the {@link ThreadContext} that should close resources when the ThreadContext 5 | * is cleared. 6 | */ 7 | public interface CloseableThreadContext { 8 | 9 | void close(); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/concurrent/EagerFetchQueue.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.concurrent; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | import net.robinfriedli.aiode.Aiode; 6 | import net.robinfriedli.aiode.boot.ShutdownableExecutorService; 7 | import net.robinfriedli.threadpool.ThreadPool; 8 | 9 | /** 10 | * Thread pool commonly used to submit requests to fetch data ahead of time or in parallel. This pool utilizes 11 | * {@link ThreadPool} to enable creating creating additional threads before enqueueing tasks 12 | * while only allowing a maximum number of threads. 13 | */ 14 | public class EagerFetchQueue { 15 | 16 | public static final ForkTaskThreadPool FETCH_POOL = new ForkTaskThreadPool( 17 | ThreadPool.Builder.create() 18 | .setCoreSize(3) 19 | .setMaxSize(20) 20 | .setKeepAlive(1L, TimeUnit.MINUTES) 21 | .setThreadFactory(new LoggingThreadFactory("eager-fetch-pool")).build() 22 | ); 23 | 24 | static { 25 | Aiode.SHUTDOWNABLES.add(new ShutdownableExecutorService(FETCH_POOL)); 26 | } 27 | 28 | public static void submitFetch(Runnable fetchTask) { 29 | FETCH_POOL.execute(fetchTask); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/concurrent/EventHandlerPool.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.concurrent; 2 | 3 | import java.util.concurrent.ExecutorService; 4 | import java.util.concurrent.TimeUnit; 5 | 6 | import net.robinfriedli.aiode.Aiode; 7 | import net.robinfriedli.aiode.boot.ShutdownableExecutorService; 8 | import net.robinfriedli.threadpool.ThreadPool; 9 | 10 | public class EventHandlerPool { 11 | 12 | public static final ExecutorService POOL = ThreadPool.Builder.create() 13 | .setCoreSize(3) 14 | .setMaxSize(50) 15 | .setKeepAlive(5L, TimeUnit.MINUTES) 16 | .setThreadFactory(new LoggingThreadFactory("event-handler-pool")).build(); 17 | 18 | static { 19 | Aiode.SHUTDOWNABLES.add(new ShutdownableExecutorService(POOL)); 20 | } 21 | 22 | private EventHandlerPool() { 23 | } 24 | 25 | public static void execute(Runnable command) { 26 | POOL.execute(new ThreadContextClosingRunnableDelegate(command)); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/concurrent/ForkTaskThreadPool.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.concurrent; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.AbstractExecutorService; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import net.robinfriedli.threadpool.ThreadPool; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | public class ForkTaskThreadPool extends AbstractExecutorService { 11 | 12 | private final ThreadPool threadPool; 13 | 14 | public ForkTaskThreadPool(ThreadPool threadPool) { 15 | this.threadPool = threadPool; 16 | } 17 | 18 | @Override 19 | public void execute(Runnable command) { 20 | ThreadContext forkedThreadContext = ThreadContext.Current.get().fork(); 21 | threadPool.execute(() -> { 22 | ThreadContext.Current.installExplicitly(forkedThreadContext); 23 | try { 24 | command.run(); 25 | } finally { 26 | forkedThreadContext.clear(); 27 | } 28 | }); 29 | } 30 | 31 | public ThreadPool getThreadPool() { 32 | return threadPool; 33 | } 34 | 35 | @Override 36 | public void shutdown() { 37 | threadPool.shutdown(); 38 | } 39 | 40 | @NotNull 41 | @Override 42 | public List shutdownNow() { 43 | return threadPool.shutdownNow(); 44 | } 45 | 46 | @Override 47 | public boolean isShutdown() { 48 | return threadPool.isShutdown(); 49 | } 50 | 51 | @Override 52 | public boolean isTerminated() { 53 | return threadPool.isTerminated(); 54 | } 55 | 56 | @Override 57 | public boolean awaitTermination(long timeout, @NotNull TimeUnit unit) throws InterruptedException { 58 | return threadPool.awaitTermination(timeout, unit); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/concurrent/ForkableThreadContext.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.concurrent; 2 | 3 | import javax.annotation.Nullable; 4 | 5 | public interface ForkableThreadContext> { 6 | 7 | /** 8 | * Set the thread where this context was installed. 9 | */ 10 | void setThread(Thread thread); 11 | 12 | /** 13 | * @return a fork of this context to be used in a fork task. If this returns null the context will not be installed 14 | * on the forked task. 15 | */ 16 | @Nullable 17 | T fork(); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/concurrent/HistoryPool.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.concurrent; 2 | 3 | import java.util.concurrent.ExecutorService; 4 | import java.util.concurrent.Executors; 5 | 6 | import net.robinfriedli.aiode.Aiode; 7 | import net.robinfriedli.aiode.boot.ShutdownableExecutorService; 8 | 9 | /** 10 | * Thread pool commonly used to store playback and command history entries. 11 | */ 12 | public class HistoryPool { 13 | 14 | public static final ExecutorService POOL = Executors.newFixedThreadPool(3, new LoggingThreadFactory("history-pool")); 15 | 16 | static { 17 | Aiode.SHUTDOWNABLES.add(new ShutdownableExecutorService(POOL)); 18 | } 19 | 20 | public static void execute(Runnable runnable) { 21 | POOL.execute(runnable); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/concurrent/LoggingThreadFactory.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.concurrent; 2 | 3 | import java.util.concurrent.ThreadFactory; 4 | import java.util.concurrent.atomic.AtomicLong; 5 | 6 | import net.robinfriedli.aiode.exceptions.handler.handlers.LoggingUncaughtExceptionHandler; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | public class LoggingThreadFactory implements ThreadFactory { 10 | 11 | private final String poolName; 12 | private final AtomicLong threadNumber = new AtomicLong(1); 13 | 14 | public LoggingThreadFactory(String poolName) { 15 | this.poolName = poolName; 16 | } 17 | 18 | @Override 19 | public Thread newThread(@NotNull Runnable r) { 20 | Thread thread = new Thread(r); 21 | thread.setName(poolName + "-thread-" + threadNumber.getAndIncrement()); 22 | thread.setUncaughtExceptionHandler(new LoggingUncaughtExceptionHandler()); 23 | return thread; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/concurrent/ThreadContextClosingRunnableDelegate.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.concurrent; 2 | 3 | public class ThreadContextClosingRunnableDelegate implements Runnable { 4 | 5 | private final Runnable delegate; 6 | 7 | public ThreadContextClosingRunnableDelegate(Runnable delegate) { 8 | this.delegate = delegate; 9 | } 10 | 11 | @Override 12 | public void run() { 13 | try { 14 | delegate.run(); 15 | } finally { 16 | ThreadContext.Current.clear(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/cron/tasks/RefreshPersistentGlobalChartsTask.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.cron.tasks; 2 | 3 | import net.robinfriedli.aiode.Aiode; 4 | import net.robinfriedli.aiode.audio.ChartService; 5 | import net.robinfriedli.aiode.cron.AbstractCronTask; 6 | import net.robinfriedli.aiode.function.modes.HibernateTransactionMode; 7 | import net.robinfriedli.exec.Mode; 8 | import org.quartz.JobExecutionContext; 9 | 10 | public class RefreshPersistentGlobalChartsTask extends AbstractCronTask { 11 | 12 | @Override 13 | protected void run(JobExecutionContext jobExecutionContext) throws Exception { 14 | ChartService chartService = Aiode.get().getChartService(); 15 | Aiode.LOGGER.info("Starting to refresh persistent global charts"); 16 | long millis = System.currentTimeMillis(); 17 | chartService.refreshPersistentGlobalCharts(); 18 | Aiode.LOGGER.info("Completed refreshing persistent global charts after {}ms", System.currentTimeMillis() - millis); 19 | } 20 | 21 | @Override 22 | protected Mode getMode() { 23 | return Mode.create().with(new HibernateTransactionMode()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/cron/tasks/ResetCurrentYouTubeQuotaTask.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.cron.tasks; 2 | 3 | import org.slf4j.LoggerFactory; 4 | 5 | import jakarta.persistence.LockModeType; 6 | import net.robinfriedli.aiode.Aiode; 7 | import net.robinfriedli.aiode.audio.youtube.YouTubeService; 8 | import net.robinfriedli.aiode.cron.AbstractCronTask; 9 | import net.robinfriedli.aiode.entities.CurrentYouTubeQuotaUsage; 10 | import net.robinfriedli.aiode.function.modes.HibernateTransactionMode; 11 | import net.robinfriedli.aiode.persist.StaticSessionProvider; 12 | import net.robinfriedli.exec.Mode; 13 | import org.quartz.JobExecutionContext; 14 | 15 | /** 16 | * Reset the {@link CurrentYouTubeQuotaUsage} every midnight PT. 17 | */ 18 | public class ResetCurrentYouTubeQuotaTask extends AbstractCronTask { 19 | 20 | @Override 21 | protected void run(JobExecutionContext jobExecutionContext) { 22 | YouTubeService youTubeService = Aiode.get().getAudioManager().getYouTubeService(); 23 | StaticSessionProvider.consumeSession(session -> { 24 | CurrentYouTubeQuotaUsage currentQuotaUsage = YouTubeService.getCurrentQuotaUsage(session, LockModeType.PESSIMISTIC_WRITE); 25 | youTubeService.setAtomicQuotaUsage(0); 26 | currentQuotaUsage.setQuota(0); 27 | }); 28 | LoggerFactory.getLogger(getClass()).info("Reset current YouTube API Quota counter"); 29 | } 30 | 31 | @Override 32 | protected Mode getMode() { 33 | return Mode.create().with(new HibernateTransactionMode()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/discord/property/AbstractBoolProperty.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.discord.property; 2 | 3 | import net.robinfriedli.aiode.entities.GuildSpecification; 4 | import net.robinfriedli.aiode.entities.xml.GuildPropertyContribution; 5 | 6 | /** 7 | * Property extension for properties that have a boolean value. 8 | */ 9 | public abstract class AbstractBoolProperty extends AbstractGuildProperty { 10 | 11 | public AbstractBoolProperty(GuildPropertyContribution contribution) { 12 | super(contribution); 13 | } 14 | 15 | @Override 16 | public void validate(Object state) { 17 | } 18 | 19 | @Override 20 | public Object process(String input) { 21 | return Boolean.parseBoolean(input); 22 | } 23 | 24 | @Override 25 | public void setValue(String value, GuildSpecification guildSpecification) { 26 | setBoolValue((boolean) process(value), guildSpecification); 27 | } 28 | 29 | protected abstract void setBoolValue(boolean bool, GuildSpecification guildSpecification); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/discord/property/AbstractIntegerProperty.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.discord.property; 2 | 3 | import net.robinfriedli.aiode.entities.GuildSpecification; 4 | import net.robinfriedli.aiode.entities.xml.GuildPropertyContribution; 5 | import net.robinfriedli.aiode.exceptions.InvalidPropertyValueException; 6 | 7 | /** 8 | * Property extension for properties that have an integer value. 9 | */ 10 | public abstract class AbstractIntegerProperty extends AbstractGuildProperty { 11 | 12 | public AbstractIntegerProperty(GuildPropertyContribution contribution) { 13 | super(contribution); 14 | } 15 | 16 | @Override 17 | public Object process(String input) { 18 | try { 19 | return Integer.parseInt(input); 20 | } catch (NumberFormatException e) { 21 | throw new InvalidPropertyValueException(String.format("'%s' is not an integer", input)); 22 | } 23 | } 24 | 25 | @Override 26 | public void setValue(String value, GuildSpecification guildSpecification) { 27 | setIntegerValue((Integer) process(value), guildSpecification); 28 | } 29 | 30 | protected abstract void setIntegerValue(Integer integer, GuildSpecification guildSpecification); 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/discord/property/properties/AutoPauseProperty.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.discord.property.properties; 2 | 3 | import net.robinfriedli.aiode.discord.listeners.VoiceChannelListener; 4 | import net.robinfriedli.aiode.discord.property.AbstractBoolProperty; 5 | import net.robinfriedli.aiode.entities.GuildSpecification; 6 | import net.robinfriedli.aiode.entities.xml.GuildPropertyContribution; 7 | 8 | /** 9 | * Property that enables / disables auto pause, meaning the bot will automatically pause the playback and leave the channel 10 | * if all members in a voice channel leave. This is handled by the {@link VoiceChannelListener} 11 | */ 12 | public class AutoPauseProperty extends AbstractBoolProperty { 13 | 14 | public AutoPauseProperty(GuildPropertyContribution contribution) { 15 | super(contribution); 16 | } 17 | 18 | @Override 19 | protected void setBoolValue(boolean bool, GuildSpecification guildSpecification) { 20 | guildSpecification.setEnableAutoPause(bool); 21 | } 22 | 23 | @Override 24 | public Object extractPersistedValue(GuildSpecification guildSpecification) { 25 | return guildSpecification.isEnableAutoPause(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/discord/property/properties/DefaultListSourceProperty.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.discord.property.properties; 2 | 3 | import net.robinfriedli.aiode.discord.property.AbstractGuildProperty; 4 | import net.robinfriedli.aiode.entities.GuildSpecification; 5 | import net.robinfriedli.aiode.entities.xml.GuildPropertyContribution; 6 | 7 | /** 8 | * Property that defines the default source to use for playlist search, "spotify", "youtube" or "local" 9 | */ 10 | public class DefaultListSourceProperty extends AbstractGuildProperty { 11 | 12 | public DefaultListSourceProperty(GuildPropertyContribution contribution) { 13 | super(contribution); 14 | } 15 | 16 | @Override 17 | public void validate(Object state) { 18 | } 19 | 20 | @Override 21 | public Object process(String input) { 22 | return input; 23 | } 24 | 25 | @Override 26 | public void setValue(String value, GuildSpecification guildSpecification) { 27 | guildSpecification.setDefaultListSource(value.toUpperCase()); 28 | } 29 | 30 | @Override 31 | public Object extractPersistedValue(GuildSpecification guildSpecification) { 32 | return guildSpecification.getDefaultListSource(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/discord/property/properties/DefaultSourceProperty.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.discord.property.properties; 2 | 3 | import net.robinfriedli.aiode.discord.property.AbstractGuildProperty; 4 | import net.robinfriedli.aiode.entities.GuildSpecification; 5 | import net.robinfriedli.aiode.entities.xml.GuildPropertyContribution; 6 | 7 | /** 8 | * Property that defines the default source to use for track search, "spotify" or "youtube" 9 | */ 10 | public class DefaultSourceProperty extends AbstractGuildProperty { 11 | 12 | public DefaultSourceProperty(GuildPropertyContribution contribution) { 13 | super(contribution); 14 | } 15 | 16 | @Override 17 | public void validate(Object state) { 18 | } 19 | 20 | @Override 21 | public Object process(String input) { 22 | return input; 23 | } 24 | 25 | @Override 26 | public void setValue(String value, GuildSpecification guildSpecification) { 27 | guildSpecification.setDefaultSource(value.toUpperCase()); 28 | } 29 | 30 | @Override 31 | public Object extractPersistedValue(GuildSpecification guildSpecification) { 32 | return guildSpecification.getDefaultSource(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/discord/property/properties/DefaultVolumeProperty.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.discord.property.properties; 2 | 3 | import java.util.Objects; 4 | 5 | import net.robinfriedli.aiode.Aiode; 6 | import net.robinfriedli.aiode.audio.AudioPlayback; 7 | import net.robinfriedli.aiode.concurrent.ExecutionContext; 8 | import net.robinfriedli.aiode.discord.property.AbstractIntegerProperty; 9 | import net.robinfriedli.aiode.entities.GuildSpecification; 10 | import net.robinfriedli.aiode.entities.xml.GuildPropertyContribution; 11 | import net.robinfriedli.aiode.exceptions.InvalidPropertyValueException; 12 | 13 | /** 14 | * Property that toggles "Now playing..." messages 15 | */ 16 | public class DefaultVolumeProperty extends AbstractIntegerProperty { 17 | 18 | public DefaultVolumeProperty(GuildPropertyContribution contribution) { 19 | super(contribution); 20 | } 21 | 22 | @Override 23 | public void validate(Object state) { 24 | int volume = (Integer) state; 25 | if (volume < 1 || volume > 200) { 26 | throw new InvalidPropertyValueException("Volume must be between 1 and 200"); 27 | } 28 | } 29 | 30 | @Override 31 | protected void setIntegerValue(Integer volume, GuildSpecification guildSpecification) { 32 | ExecutionContext executionContext = ExecutionContext.Current.get(); 33 | if (executionContext != null) { 34 | AudioPlayback playback = Aiode.get().getAudioManager().getPlaybackForGuild(executionContext.getGuild()); 35 | playback.setDefaultVolume(Objects.requireNonNullElse(volume, 100)); 36 | } 37 | guildSpecification.setDefaultVolume(volume); 38 | } 39 | 40 | @Override 41 | public Object extractPersistedValue(GuildSpecification guildSpecification) { 42 | return guildSpecification.getDefaultVolume(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/discord/property/properties/EnableScriptingProperty.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.discord.property.properties; 2 | 3 | import net.robinfriedli.aiode.discord.property.AbstractBoolProperty; 4 | import net.robinfriedli.aiode.entities.GuildSpecification; 5 | import net.robinfriedli.aiode.entities.xml.GuildPropertyContribution; 6 | 7 | public class EnableScriptingProperty extends AbstractBoolProperty { 8 | 9 | public EnableScriptingProperty(GuildPropertyContribution contribution) { 10 | super(contribution); 11 | } 12 | 13 | @Override 14 | protected void setBoolValue(boolean bool, GuildSpecification guildSpecification) { 15 | guildSpecification.setEnableScripting(bool); 16 | } 17 | 18 | @Override 19 | public Object extractPersistedValue(GuildSpecification guildSpecification) { 20 | return guildSpecification.isEnableScripting(); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/discord/property/properties/PlaybackNotificationProperty.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.discord.property.properties; 2 | 3 | import net.robinfriedli.aiode.discord.property.AbstractBoolProperty; 4 | import net.robinfriedli.aiode.entities.GuildSpecification; 5 | import net.robinfriedli.aiode.entities.xml.GuildPropertyContribution; 6 | 7 | /** 8 | * Property that toggles "Now playing..." messages 9 | */ 10 | public class PlaybackNotificationProperty extends AbstractBoolProperty { 11 | 12 | public PlaybackNotificationProperty(GuildPropertyContribution contribution) { 13 | super(contribution); 14 | } 15 | 16 | @Override 17 | protected void setBoolValue(boolean bool, GuildSpecification guildSpecification) { 18 | guildSpecification.setSendPlaybackNotification(bool); 19 | } 20 | 21 | @Override 22 | public Object extractPersistedValue(GuildSpecification guildSpecification) { 23 | return guildSpecification.isSendPlaybackNotification(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/entities/GeneratedToken.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.entities; 2 | 3 | import java.io.Serializable; 4 | import java.util.UUID; 5 | 6 | import jakarta.persistence.Column; 7 | import jakarta.persistence.Entity; 8 | import jakarta.persistence.GeneratedValue; 9 | import jakarta.persistence.GenerationType; 10 | import jakarta.persistence.Id; 11 | import jakarta.persistence.Table; 12 | 13 | @Entity 14 | @Table(name = "generated_token") 15 | public class GeneratedToken implements Serializable { 16 | 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | @Column(name = "pk") 20 | private long pk; 21 | @Column(name = "token", unique = true, nullable = false) 22 | private UUID token; 23 | @Column(name = "ip", nullable = false) 24 | private String ip; 25 | 26 | 27 | public long getPk() { 28 | return pk; 29 | } 30 | 31 | public void setPk(long pk) { 32 | this.pk = pk; 33 | } 34 | 35 | public UUID getToken() { 36 | return token; 37 | } 38 | 39 | public void setToken(UUID token) { 40 | this.token = token; 41 | } 42 | 43 | public String getIp() { 44 | return ip; 45 | } 46 | 47 | public void setIp(String ip) { 48 | this.ip = ip; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/entities/GlobalArtistChart.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.entities; 2 | 3 | import java.io.Serializable; 4 | 5 | import jakarta.persistence.Column; 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.ForeignKey; 8 | import jakarta.persistence.GeneratedValue; 9 | import jakarta.persistence.GenerationType; 10 | import jakarta.persistence.Id; 11 | import jakarta.persistence.JoinColumn; 12 | import jakarta.persistence.ManyToOne; 13 | import jakarta.persistence.Table; 14 | 15 | @Entity 16 | @Table(name = "global_artist_chart") 17 | public class GlobalArtistChart implements Serializable { 18 | 19 | @Id 20 | @GeneratedValue(strategy = GenerationType.IDENTITY) 21 | @Column(name = "pk") 22 | private long pk; 23 | 24 | @Column(name = "count", nullable = false) 25 | private long count; 26 | 27 | @ManyToOne 28 | @JoinColumn(name = "artist_pk", referencedColumnName = "pk", nullable = false, foreignKey = @ForeignKey(name = "global_artist_chart_artist_pk_fkey")) 29 | private Artist artist; 30 | 31 | @Column(name = "is_monthly") 32 | private boolean isMonthly = false; 33 | 34 | public long getPk() { 35 | return pk; 36 | } 37 | 38 | public void setPk(long pk) { 39 | this.pk = pk; 40 | } 41 | 42 | public long getCount() { 43 | return count; 44 | } 45 | 46 | public void setCount(long count) { 47 | this.count = count; 48 | } 49 | 50 | public Artist getArtist() { 51 | return artist; 52 | } 53 | 54 | public void setArtist(Artist source) { 55 | this.artist = source; 56 | } 57 | 58 | public boolean isMonthly() { 59 | return isMonthly; 60 | } 61 | 62 | public void setMonthly(boolean monthly) { 63 | isMonthly = monthly; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/entities/LookupEntity.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.entities; 2 | 3 | 4 | import java.io.Serializable; 5 | import java.util.Optional; 6 | 7 | import javax.annotation.Nullable; 8 | 9 | import jakarta.persistence.Column; 10 | import jakarta.persistence.MappedSuperclass; 11 | import net.robinfriedli.aiode.Aiode; 12 | import org.hibernate.Session; 13 | 14 | @MappedSuperclass 15 | public class LookupEntity implements Serializable { 16 | 17 | @Column(name = "unique_id", nullable = false, unique = true) 18 | private String uniqueId; 19 | 20 | public static T require(Session session, Class type, String id) { 21 | return getOptional(session, type, id).orElseThrow(); 22 | } 23 | 24 | @Nullable 25 | public static T get(Session session, Class type, String id) { 26 | return getOptional(session, type, id).orElse(null); 27 | } 28 | 29 | public static Optional getOptional(Session session, Class type, String id) { 30 | return Aiode.get().getQueryBuilderFactory().find(type) 31 | .where(((cb, root) -> cb.equal(root.get("uniqueId"), id))) 32 | .build(session) 33 | .uniqueResultOptional(); 34 | } 35 | 36 | public String getUniqueId() { 37 | return uniqueId; 38 | } 39 | 40 | public void setUniqueId(String uniqueId) { 41 | this.uniqueId = uniqueId; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/entities/PlaybackHistorySource.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.entities; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.GeneratedValue; 6 | import jakarta.persistence.GenerationType; 7 | import jakarta.persistence.Id; 8 | import jakarta.persistence.Table; 9 | import net.robinfriedli.aiode.audio.Playable; 10 | 11 | @Entity 12 | @Table(name = "playback_history_source") 13 | public class PlaybackHistorySource extends LookupEntity { 14 | 15 | @Id 16 | @GeneratedValue(strategy = GenerationType.IDENTITY) 17 | @Column(name = "pk") 18 | private long pk; 19 | 20 | public long getPk() { 21 | return pk; 22 | } 23 | 24 | public void setPk(long pk) { 25 | this.pk = pk; 26 | } 27 | 28 | public Playable.Source asEnum() { 29 | return Playable.Source.valueOf(getUniqueId()); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/entities/PrivateBotInstance.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.entities; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.Id; 6 | import jakarta.persistence.Table; 7 | 8 | @Entity 9 | @Table(name = "private_bot_instance") 10 | public class PrivateBotInstance { 11 | 12 | @Id 13 | @Column(name = "identifier", nullable = false, unique = true, updatable = false) 14 | private String identifier; 15 | @Column(name = "server_limit", nullable = false) 16 | private int serverLimit; 17 | @Column(name = "invite_link", nullable = false) 18 | private String inviteLink; 19 | 20 | public String getIdentifier() { 21 | return identifier; 22 | } 23 | 24 | public void setIdentifier(String identifier) { 25 | this.identifier = identifier; 26 | } 27 | 28 | public int getServerLimit() { 29 | return serverLimit; 30 | } 31 | 32 | public void setServerLimit(int serverLimit) { 33 | this.serverLimit = serverLimit; 34 | } 35 | 36 | public String getInviteLink() { 37 | return inviteLink; 38 | } 39 | 40 | public void setInviteLink(String inviteLink) { 41 | this.inviteLink = inviteLink; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/entities/SpotifyItemKind.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.entities; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.GeneratedValue; 6 | import jakarta.persistence.GenerationType; 7 | import jakarta.persistence.Id; 8 | import jakarta.persistence.Table; 9 | import net.robinfriedli.aiode.audio.spotify.SpotifyTrackKind; 10 | 11 | // TODO create validators to check that videos with a redirected spotify track and playback history entries of type "spotify" have a kind 12 | @Entity 13 | @Table(name = "spotify_item_kind") 14 | public class SpotifyItemKind extends LookupEntity { 15 | 16 | @Id 17 | @GeneratedValue(strategy = GenerationType.IDENTITY) 18 | @Column(name = "pk") 19 | private long pk; 20 | 21 | public long getPk() { 22 | return pk; 23 | } 24 | 25 | public void setPk(long pk) { 26 | this.pk = pk; 27 | } 28 | 29 | public SpotifyTrackKind asEnum() { 30 | return SpotifyTrackKind.valueOf(getUniqueId()); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/entities/SpotifyRedirectIndexModificationLock.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.entities; 2 | 3 | import java.io.Serializable; 4 | import java.time.LocalDateTime; 5 | 6 | import jakarta.persistence.Column; 7 | import jakarta.persistence.Entity; 8 | import jakarta.persistence.GeneratedValue; 9 | import jakarta.persistence.GenerationType; 10 | import jakarta.persistence.Id; 11 | import jakarta.persistence.Table; 12 | import net.robinfriedli.aiode.audio.spotify.SpotifyRedirectService; 13 | import net.robinfriedli.aiode.cron.tasks.RefreshSpotifyRedirectIndicesTask; 14 | 15 | /** 16 | * Creates a lock while running the {@link RefreshSpotifyRedirectIndicesTask} to signal the {@link SpotifyRedirectService} 17 | * not to update or delete any {@link SpotifyRedirectIndex} entities 18 | */ 19 | @Entity 20 | @Table(name = "spotify_redirect_index_modification_lock") 21 | public class SpotifyRedirectIndexModificationLock implements Serializable { 22 | 23 | @Id 24 | @GeneratedValue(strategy = GenerationType.IDENTITY) 25 | @Column(name = "pk") 26 | private long pk; 27 | @Column(name = "creation_time_stamp") 28 | private LocalDateTime creationTimeStamp; 29 | 30 | public long getPk() { 31 | return pk; 32 | } 33 | 34 | public void setPk(long pk) { 35 | this.pk = pk; 36 | } 37 | 38 | public LocalDateTime getCreationTimeStamp() { 39 | return creationTimeStamp; 40 | } 41 | 42 | public void setCreationTimeStamp(LocalDateTime creationTimeStamp) { 43 | this.creationTimeStamp = creationTimeStamp; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/entities/xml/CronJobContribution.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.entities.xml; 2 | 3 | import java.time.ZoneId; 4 | import java.util.TimeZone; 5 | 6 | import javax.annotation.Nonnull; 7 | 8 | import net.robinfriedli.aiode.cron.AbstractCronTask; 9 | import net.robinfriedli.jxp.collections.NodeList; 10 | import net.robinfriedli.jxp.persist.Context; 11 | import org.w3c.dom.Element; 12 | 13 | public class CronJobContribution extends GenericClassContribution { 14 | 15 | // invoked by JXP 16 | @SuppressWarnings("unused") 17 | public CronJobContribution(Element element, NodeList subElements, Context context) { 18 | super(element, subElements, context); 19 | } 20 | 21 | @Override 22 | @Nonnull 23 | public String getId() { 24 | return getAttribute("id").getValue(); 25 | } 26 | 27 | public String getCronExpression() { 28 | return getAttribute("cron").getValue(); 29 | } 30 | 31 | public TimeZone getTimeZone() { 32 | if (hasAttribute("timeZone")) { 33 | return TimeZone.getTimeZone(ZoneId.of(getAttribute("timeZone").getValue(), ZoneId.SHORT_IDS)); 34 | } 35 | return TimeZone.getTimeZone(ZoneId.systemDefault()); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/entities/xml/GroovyVariableProviderContribution.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.entities.xml; 2 | 3 | import net.robinfriedli.aiode.scripting.GroovyVariableProvider; 4 | import net.robinfriedli.jxp.collections.NodeList; 5 | import net.robinfriedli.jxp.persist.Context; 6 | import org.jetbrains.annotations.Nullable; 7 | import org.w3c.dom.Element; 8 | 9 | public class GroovyVariableProviderContribution extends GenericClassContribution { 10 | 11 | public GroovyVariableProviderContribution(Element element, NodeList subElements, Context context) { 12 | super(element, subElements, context); 13 | } 14 | 15 | @Nullable 16 | @Override 17 | public String getId() { 18 | return getAttribute("implementation").getValue(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/entities/xml/HttpHandlerContribution.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.entities.xml; 2 | 3 | import javax.annotation.Nullable; 4 | 5 | import com.sun.net.httpserver.HttpHandler; 6 | import net.robinfriedli.jxp.collections.NodeList; 7 | import net.robinfriedli.jxp.persist.Context; 8 | import org.w3c.dom.Element; 9 | 10 | public class HttpHandlerContribution extends GenericClassContribution { 11 | 12 | // invoked by JXP 13 | @SuppressWarnings("unused") 14 | public HttpHandlerContribution(Element element, NodeList subElements, Context context) { 15 | super(element, subElements, context); 16 | } 17 | 18 | @Nullable 19 | @Override 20 | public String getId() { 21 | return getAttribute("path").getValue(); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/entities/xml/StartupTaskContribution.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.entities.xml; 2 | 3 | import javax.annotation.Nullable; 4 | 5 | import net.robinfriedli.aiode.boot.StartupTask; 6 | import net.robinfriedli.jxp.collections.NodeList; 7 | import net.robinfriedli.jxp.persist.Context; 8 | import org.w3c.dom.Element; 9 | 10 | public class StartupTaskContribution extends GenericClassContribution { 11 | 12 | // invoked by JXP 13 | @SuppressWarnings("unused") 14 | public StartupTaskContribution(Element element, NodeList subElements, Context context) { 15 | super(element, subElements, context); 16 | } 17 | 18 | @Nullable 19 | @Override 20 | public String getId() { 21 | return getAttribute("implementation").getValue(); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/AdditionalInformationException.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions; 2 | 3 | /** 4 | * Type of UserException that may provide additional information about the error 5 | */ 6 | public abstract class AdditionalInformationException extends UserException { 7 | 8 | public AdditionalInformationException() { 9 | super(); 10 | } 11 | 12 | public AdditionalInformationException(String message) { 13 | super(message); 14 | } 15 | 16 | public AdditionalInformationException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | 20 | public AdditionalInformationException(Throwable cause) { 21 | super(cause); 22 | } 23 | 24 | public abstract String getAdditionalInformation(); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/AmbiguousCommandException.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions; 2 | 3 | import java.util.List; 4 | import java.util.function.Function; 5 | 6 | import com.google.common.collect.Lists; 7 | import net.robinfriedli.aiode.command.AbstractCommand; 8 | import net.robinfriedli.aiode.command.ClientQuestionEvent; 9 | 10 | /** 11 | * Exception that indicates that several results were found when only one was expected. Used to trigger a 12 | * {@link ClientQuestionEvent} from outside the {@link AbstractCommand} class during a command execution. 13 | */ 14 | public class AmbiguousCommandException extends UserException { 15 | 16 | private final List options; 17 | private final Function displayFunc; 18 | 19 | public AmbiguousCommandException(List options, Function displayFunc) { 20 | super("Several options found when only one was expected"); 21 | this.options = Lists.newArrayList(options); 22 | this.displayFunc = displayFunc; 23 | } 24 | 25 | public List getOptions() { 26 | return options; 27 | } 28 | 29 | public Function getDisplayFunc() { 30 | return displayFunc; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/CommandFailure.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions; 2 | 3 | /** 4 | * Special exception class that propagates exceptions thrown during command execution that have already been handled 5 | */ 6 | public class CommandFailure extends RuntimeException { 7 | 8 | public CommandFailure(Throwable cause) { 9 | super(cause); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/CommandRuntimeException.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions; 2 | 3 | import javax.annotation.Nonnull; 4 | 5 | import net.robinfriedli.aiode.exceptions.handler.handlers.CommandUncaughtExceptionHandler; 6 | 7 | /** 8 | * Exception class to wrap checked exceptions thrown during command execution. Used by {@link CommandUncaughtExceptionHandler} 9 | * to handle the cause checked exception directly. 10 | */ 11 | public class CommandRuntimeException extends RuntimeException { 12 | 13 | public CommandRuntimeException(@Nonnull Throwable cause) { 14 | super(cause); 15 | } 16 | 17 | /** 18 | * Throws the exception and wraps it into a CommandRuntimeException if it is not already a RuntimeException or Error. 19 | * 20 | * @param e the exception to throw and potentially wrap 21 | */ 22 | public static void throwRuntimeException(Throwable e) { 23 | if (e instanceof RuntimeException) { 24 | throw (RuntimeException) e; 25 | } else if (e instanceof Error) { 26 | throw (Error) e; 27 | } else { 28 | throw new CommandRuntimeException(e); 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/DiscordEntityInitialisationException.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions; 2 | 3 | /** 4 | * Exception thrown when a discord entity stored by id could not be loaded. 5 | */ 6 | public class DiscordEntityInitialisationException extends RuntimeException { 7 | 8 | public DiscordEntityInitialisationException() { 9 | super(); 10 | } 11 | 12 | public DiscordEntityInitialisationException(String message) { 13 | super(message); 14 | } 15 | 16 | public DiscordEntityInitialisationException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | 20 | public DiscordEntityInitialisationException(Throwable cause) { 21 | super(cause); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/ForbiddenCommandException.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions; 2 | 3 | import java.util.List; 4 | 5 | import net.dv8tion.jda.api.entities.Role; 6 | import net.dv8tion.jda.api.entities.User; 7 | import net.robinfriedli.aiode.command.PermissionTarget; 8 | import net.robinfriedli.stringlist.StringList; 9 | 10 | 11 | /** 12 | * Exception thrown when a user tries to use a command that requires a certain role the user does not have 13 | */ 14 | public class ForbiddenCommandException extends UserException { 15 | 16 | public ForbiddenCommandException(User user, PermissionTarget permissionTarget, List roles) { 17 | super( 18 | String.format( 19 | "User %s is not allowed to use %s %s. %s.", 20 | user.getAsMention(), 21 | permissionTarget.getPermissionTargetType().getName(), 22 | permissionTarget.getFullPermissionTargetIdentifier(), 23 | roles.isEmpty() 24 | ? "Only available to guild owner and administrator roles" 25 | : "Requires any of these roles: " + StringList.create(roles, Role::getName).toSeparatedString(", ") 26 | ) 27 | ); 28 | } 29 | 30 | public ForbiddenCommandException(User user, PermissionTarget permissionTarget, String availableTo) { 31 | super( 32 | String.format( 33 | "User %s is not allowed to use %s %s. Only available to %s.", 34 | user.getAsMention(), 35 | permissionTarget.getPermissionTargetType().getName(), 36 | permissionTarget.getFullPermissionTargetIdentifier(), 37 | availableTo 38 | ) 39 | ); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/IllegalEscapeCharacterException.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions; 2 | 3 | public class IllegalEscapeCharacterException extends AdditionalInformationException { 4 | 5 | public IllegalEscapeCharacterException() { 6 | super(); 7 | } 8 | 9 | public IllegalEscapeCharacterException(String message) { 10 | super(message); 11 | } 12 | 13 | public IllegalEscapeCharacterException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | 17 | public IllegalEscapeCharacterException(Throwable cause) { 18 | super(cause); 19 | } 20 | 21 | @Override 22 | public String getAdditionalInformation() { 23 | return "The escape character (`\\`) is used to indicate that a character that normally triggers an action (such as " + 24 | "the argument prefix '$' (makes the bot parse an argument), whitespace ' ' (may cause the bot to stop parsing " + 25 | "an argument) or double quotes '\"' (may cause the bot to start / stop parsing an argument value)) should be " + 26 | "treated as normal command input instead. So if you need to use the character `\\` in your command you need to " + 27 | "escape it by adding a second `\\` to treat it as a normal character. E.g. wrong: `play tr\\ack`, correct: " + 28 | "`play tr\\\\ack`."; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/InvalidArgumentException.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions; 2 | 3 | /** 4 | * Exception class that signals that an undefined argument was used 5 | */ 6 | public class InvalidArgumentException extends AdditionalInformationException { 7 | 8 | public InvalidArgumentException() { 9 | super(); 10 | } 11 | 12 | public InvalidArgumentException(String message) { 13 | super(message); 14 | } 15 | 16 | public InvalidArgumentException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | 20 | public InvalidArgumentException(Throwable cause) { 21 | super(cause); 22 | } 23 | 24 | @Override 25 | public String getAdditionalInformation() { 26 | return "Is this not supposed to be an argument? Aiode interpreted it as one because it started with the argument" + 27 | " prefix ('$' or your custom argument prefix defined with the property command). If this was a mistake and the" + 28 | " argument prefix is supposed to be part of the command input you can escape the prefix by putting a `\\`" + 29 | " in front of it. E.g. `play $spotify \\$trackname`."; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/InvalidCommandException.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions; 2 | 3 | import net.robinfriedli.aiode.discord.property.properties.PrefixProperty; 4 | 5 | /** 6 | * Type of command thrown when the cause is user error. This exception typically gets caught and its message sent to 7 | * Discord. 8 | */ 9 | public class InvalidCommandException extends AdditionalInformationException { 10 | 11 | public InvalidCommandException() { 12 | super(); 13 | } 14 | 15 | public InvalidCommandException(String errorMessage) { 16 | super(errorMessage); 17 | } 18 | 19 | public InvalidCommandException(Throwable cause) { 20 | super(cause); 21 | } 22 | 23 | public InvalidCommandException(String errorMessage, Throwable cause) { 24 | super(errorMessage, cause); 25 | } 26 | 27 | @Override 28 | public String getAdditionalInformation() { 29 | String prefix = PrefixProperty.getEffectiveCommandStartForCurrentContext(); 30 | return String.format("If you need help with a command you can use the help command. E.g. %shelp play", prefix); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/InvalidPropertyValueException.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions; 2 | 3 | public class InvalidPropertyValueException extends UserException { 4 | 5 | public InvalidPropertyValueException() { 6 | super(); 7 | } 8 | 9 | public InvalidPropertyValueException(String message) { 10 | super(message); 11 | } 12 | 13 | public InvalidPropertyValueException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | 17 | public InvalidPropertyValueException(Throwable cause) { 18 | super(cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/InvalidRequestException.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions; 2 | 3 | public class InvalidRequestException extends UserException { 4 | 5 | public InvalidRequestException() { 6 | super(); 7 | } 8 | 9 | public InvalidRequestException(String message) { 10 | super(message); 11 | } 12 | 13 | public InvalidRequestException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | 17 | public InvalidRequestException(Throwable cause) { 18 | super(cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/NoLoginException.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions; 2 | 3 | import net.dv8tion.jda.api.entities.User; 4 | 5 | /** 6 | * Thrown when a Spotify login is required for an action but none is found for the corresponding user. 7 | */ 8 | public class NoLoginException extends UserException { 9 | 10 | public NoLoginException(User user) { 11 | super(String.format("User %s is not logged in.", user.getName())); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/NoResultsFoundException.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions; 2 | 3 | public class NoResultsFoundException extends UserException { 4 | 5 | public NoResultsFoundException() { 6 | super(); 7 | } 8 | 9 | public NoResultsFoundException(String errorMessage) { 10 | super(errorMessage); 11 | } 12 | 13 | public NoResultsFoundException(Throwable cause) { 14 | super(cause); 15 | } 16 | 17 | public NoResultsFoundException(String errorMessage, Throwable cause) { 18 | super(errorMessage, cause); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/NoSpotifyResultsFoundException.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions; 2 | 3 | public class NoSpotifyResultsFoundException extends AdditionalInformationException { 4 | 5 | public NoSpotifyResultsFoundException() { 6 | super(); 7 | } 8 | 9 | public NoSpotifyResultsFoundException(String errorMessage) { 10 | super(errorMessage); 11 | } 12 | 13 | public NoSpotifyResultsFoundException(Throwable cause) { 14 | super(cause); 15 | } 16 | 17 | public NoSpotifyResultsFoundException(String errorMessage, Throwable cause) { 18 | super(errorMessage, cause); 19 | } 20 | 21 | @Override 22 | public String getAdditionalInformation() { 23 | return "Mind that Spotify queries, unlike YouTube, are not a fulltext search so you shouldn't type 'numb by linkin park' " + 24 | "but rather use the appropriate filters; e.g. 'numb artist:linkin park'. If you didn't actually mean to search " + 25 | "for a Spotify track but rather a playlist or YouTube video, see 'help play' to find the needed arguments or " + 26 | "adjust the default source using the property command."; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/RateLimitException.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions; 2 | 3 | /** 4 | * Exception that indicates that a user is submitting tasks too quickly. 5 | */ 6 | public class RateLimitException extends RuntimeException { 7 | 8 | private final boolean isTimeout; 9 | 10 | public RateLimitException(boolean isTimeout) { 11 | super(); 12 | this.isTimeout = isTimeout; 13 | } 14 | 15 | public RateLimitException(boolean isTimeout, String message) { 16 | super(message); 17 | this.isTimeout = isTimeout; 18 | } 19 | 20 | public RateLimitException(boolean isTimeout, String message, Throwable cause) { 21 | super(message, cause); 22 | this.isTimeout = isTimeout; 23 | } 24 | 25 | public RateLimitException(boolean isTimeout, Throwable cause) { 26 | super(cause); 27 | this.isTimeout = isTimeout; 28 | } 29 | 30 | /** 31 | * @return true if the exception was caused by the client having previously hit the rate limit and trying to submit 32 | * a task again while the time out is still active 33 | */ 34 | public boolean isTimeout() { 35 | return isTimeout; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/UnavailableResourceException.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions; 2 | 3 | /** 4 | * Exception thrown to signal an item could not be loaded. E.g. if the user attempted to play a private, copyright 5 | * claimed or otherwise unavailable YouTube video or if loading the item was cancelled 6 | */ 7 | @SuppressWarnings("unused") 8 | public class UnavailableResourceException extends Exception { 9 | 10 | public UnavailableResourceException() { 11 | super(); 12 | } 13 | 14 | public UnavailableResourceException(String message) { 15 | super(message); 16 | } 17 | 18 | public UnavailableResourceException(String message, Throwable cause) { 19 | super(message, cause); 20 | } 21 | 22 | public UnavailableResourceException(Throwable cause) { 23 | super(cause); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/UnclosedQuotationsException.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions; 2 | 3 | public class UnclosedQuotationsException extends AdditionalInformationException { 4 | 5 | public UnclosedQuotationsException() { 6 | super(); 7 | } 8 | 9 | public UnclosedQuotationsException(String message) { 10 | super(message); 11 | } 12 | 13 | public UnclosedQuotationsException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | 17 | public UnclosedQuotationsException(Throwable cause) { 18 | super(cause); 19 | } 20 | 21 | @Override 22 | public String getAdditionalInformation() { 23 | return "If you need to use a quotation mark in your command without the bot treating it as a quotation but a " + 24 | "normal character you can escape the quotation mark by putting a `\\` in front of it. " + 25 | "E.g. `play tr\\\"ack`."; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/UnexpectedCommandSetupException.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions; 2 | 3 | /** 4 | * Exception denoting errors caused by an invalid command configuration or other unexpected issues when setting up a command, 5 | * e.g. errors when running the groovy script of an argument rule or when parsing the command fails due to an unexpected 6 | * exception. 7 | */ 8 | public class UnexpectedCommandSetupException extends RuntimeException { 9 | 10 | public UnexpectedCommandSetupException() { 11 | super(); 12 | } 13 | 14 | public UnexpectedCommandSetupException(String message) { 15 | super(message); 16 | } 17 | 18 | public UnexpectedCommandSetupException(String message, Throwable cause) { 19 | super(message, cause); 20 | } 21 | 22 | public UnexpectedCommandSetupException(Throwable cause) { 23 | super(cause); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/UserException.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions; 2 | 3 | import java.awt.Color; 4 | 5 | import net.dv8tion.jda.api.EmbedBuilder; 6 | import net.robinfriedli.aiode.discord.MessageService; 7 | 8 | /** 9 | * Superclass for all exceptions that are based on user fault and should be sent to discord as error message via 10 | * {@link MessageService#sendError(String, MessageChannel)} 11 | */ 12 | public class UserException extends RuntimeException { 13 | 14 | public UserException() { 15 | super(); 16 | } 17 | 18 | public UserException(String message) { 19 | super(shortenMessage(message)); 20 | } 21 | 22 | public UserException(String message, Throwable cause) { 23 | super(shortenMessage(message), cause); 24 | } 25 | 26 | public UserException(Throwable cause) { 27 | super(cause); 28 | } 29 | 30 | private static String shortenMessage(String message) { 31 | if (message.length() > 1000) { 32 | return message.substring(0, 995) + "[...]"; 33 | } 34 | 35 | return message; 36 | } 37 | 38 | public EmbedBuilder buildEmbed() { 39 | EmbedBuilder embedBuilder = new EmbedBuilder(); 40 | embedBuilder.setColor(Color.RED); 41 | embedBuilder.setTitle("Error"); 42 | 43 | StringBuilder builder = new StringBuilder(getMessage()); 44 | 45 | if (this instanceof AdditionalInformationException) { 46 | String additionalInformation = ((AdditionalInformationException) this).getAdditionalInformation(); 47 | builder.append(System.lineSeparator()).append(System.lineSeparator()) 48 | .append("_").append(additionalInformation).append("_"); 49 | } 50 | 51 | embedBuilder.setDescription(builder.toString()); 52 | return embedBuilder; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/handler/CommandExceptionHandlerExecutor.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions.handler; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import net.robinfriedli.aiode.command.Command; 7 | import net.robinfriedli.aiode.exceptions.ExceptionUtils; 8 | 9 | public class CommandExceptionHandlerExecutor extends ExceptionHandlerExecutor { 10 | 11 | private final Command command; 12 | 13 | public CommandExceptionHandlerExecutor(Command command) { 14 | this.command = command; 15 | } 16 | 17 | @Override 18 | protected void handleUnhandled(Throwable e) { 19 | Logger logger = LoggerFactory.getLogger(getClass()); 20 | try { 21 | ExceptionUtils.handleCommandException(e, command, logger); 22 | } catch (Exception e1) { 23 | logger.error("Exception while calling ExceptionUtils#handleCommandException, falling back to logging error", e1); 24 | logger.error("Exception in command handler thread", e); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/handler/ExceptionHandlerRegistry.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions.handler; 2 | 3 | import java.util.List; 4 | import java.util.stream.Collectors; 5 | 6 | import net.robinfriedli.aiode.util.ClassDescriptorNode; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | public class ExceptionHandlerRegistry { 11 | 12 | private final List> exceptionHandlers; 13 | 14 | public ExceptionHandlerRegistry(List> exceptionHandlers) { 15 | this.exceptionHandlers = exceptionHandlers; 16 | } 17 | 18 | public List> getExceptionHandlers() { 19 | return exceptionHandlers; 20 | } 21 | 22 | /** 23 | * @param exceptionType type of the exception 24 | * @return all applicable {@link ExceptionHandler} instances, ordered by most applicable (closest to the provided type first, 25 | * superclasses later) first, then using {@link ExceptionHandler#getPriority()}. 26 | */ 27 | public List> getApplicableExceptionHandlersOrdered(Class exceptionType) { 28 | return exceptionHandlers.stream() 29 | .filter(eh -> eh.getType().isAssignableFrom(exceptionType)) 30 | .sorted(ClassDescriptorNode.>getComparator().thenComparing(ExceptionHandler::getPriority)) 31 | .collect(Collectors.toList()); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/handler/TrackLoadingExceptionHandlerExecutor.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions.handler; 2 | 3 | import javax.annotation.Nullable; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; 9 | import net.robinfriedli.aiode.concurrent.ExecutionContext; 10 | import net.robinfriedli.aiode.exceptions.ExceptionUtils; 11 | 12 | public class TrackLoadingExceptionHandlerExecutor extends ExceptionHandlerExecutor { 13 | 14 | @Nullable 15 | private final ExecutionContext executionContext; 16 | @Nullable 17 | private final MessageChannel channel; 18 | 19 | public TrackLoadingExceptionHandlerExecutor(@Nullable ExecutionContext executionContext, @Nullable MessageChannel channel) { 20 | this.executionContext = executionContext; 21 | this.channel = channel; 22 | } 23 | 24 | @Override 25 | protected void handleUnhandled(Throwable e) { 26 | Logger logger = LoggerFactory.getLogger(getClass()); 27 | try { 28 | ExceptionUtils.handleTrackLoadingException(e, logger, executionContext, channel); 29 | } catch (Exception e1) { 30 | logger.error("Exception while calling ExceptionUtils#handleTrackLoadingException, falling back to logging error", e1); 31 | logger.error("Exception in track loading thread", e); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/handler/handlers/CommandRuntimeExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions.handler.handlers; 2 | 3 | import net.robinfriedli.aiode.exceptions.CommandRuntimeException; 4 | import net.robinfriedli.aiode.exceptions.handler.ExceptionHandler; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | public class CommandRuntimeExceptionHandler implements ExceptionHandler { 9 | 10 | @Override 11 | public Class getType() { 12 | return CommandRuntimeException.class; 13 | } 14 | 15 | @Override 16 | public Result handleException(Throwable uncaughtException, CommandRuntimeException exceptionToHandle) { 17 | return Result.SKIP_TO_CAUSE; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/handler/handlers/CommandUncaughtExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions.handler.handlers; 2 | 3 | import org.slf4j.Logger; 4 | 5 | import net.robinfriedli.aiode.command.Command; 6 | import net.robinfriedli.aiode.concurrent.ThreadContext; 7 | import net.robinfriedli.aiode.exceptions.ExceptionUtils; 8 | 9 | public class CommandUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { 10 | 11 | private final Logger logger; 12 | 13 | public CommandUncaughtExceptionHandler(Logger logger) { 14 | this.logger = logger; 15 | } 16 | 17 | @Override 18 | public void uncaughtException(Thread t, Throwable e) { 19 | Command command = ThreadContext.Current.get(Command.class); 20 | if (command != null) { 21 | try { 22 | ExceptionUtils.handleCommandException(e, command, logger); 23 | return; 24 | } catch (Exception e1) { 25 | logger.error("Exception while calling ExceptionUtils#handleCommandException, falling back to logging error", e1); 26 | } 27 | } 28 | logger.error("Exception in command handler thread", e); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/handler/handlers/LoggingUncaughtExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions.handler.handlers; 2 | 3 | import net.robinfriedli.aiode.Aiode; 4 | 5 | public class LoggingUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { 6 | 7 | @Override 8 | public void uncaughtException(Thread t, Throwable e) { 9 | Aiode.LOGGER.error("Uncaught exception in thread " + t, e); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/exceptions/handler/handlers/TrackLoadingUncaughtExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.exceptions.handler.handlers; 2 | 3 | import org.slf4j.Logger; 4 | 5 | import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; 6 | import net.robinfriedli.aiode.concurrent.ExecutionContext; 7 | import net.robinfriedli.aiode.concurrent.ThreadContext; 8 | import net.robinfriedli.aiode.exceptions.ExceptionUtils; 9 | 10 | public class TrackLoadingUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { 11 | 12 | private final Logger logger; 13 | private MessageChannel messageChannel; 14 | private ExecutionContext executionContext; 15 | 16 | public TrackLoadingUncaughtExceptionHandler(Logger logger) { 17 | this(logger, null, null); 18 | } 19 | 20 | public TrackLoadingUncaughtExceptionHandler(Logger logger, MessageChannel messageChannel, ExecutionContext executionContext) { 21 | this.logger = logger; 22 | this.messageChannel = messageChannel; 23 | this.executionContext = executionContext; 24 | } 25 | 26 | @Override 27 | public void uncaughtException(Thread t, Throwable e) { 28 | if (executionContext == null) { 29 | executionContext = ExecutionContext.Current.get(); 30 | } 31 | 32 | if (messageChannel == null) { 33 | messageChannel = executionContext != null ? executionContext.getChannel() : ThreadContext.Current.get(MessageChannel.class); 34 | } 35 | 36 | ExceptionUtils.handleTrackLoadingException(e, logger, executionContext, messageChannel); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/function/ChainableRunnable.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.function; 2 | 3 | public interface ChainableRunnable extends CheckedRunnable { 4 | 5 | default ChainableRunnable andThen(Runnable next) { 6 | return () -> { 7 | run(); 8 | next.run(); 9 | }; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/function/CheckedBiFunction.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.function; 2 | 3 | import java.util.function.BiFunction; 4 | 5 | import net.robinfriedli.aiode.exceptions.CommandRuntimeException; 6 | 7 | /** 8 | * BiFunction that handles checked exceptions by wrapping them into {@link CommandRuntimeException} 9 | * 10 | * @param the type of the first parameter 11 | * @param the type of the second parameter 12 | * @param the return type 13 | */ 14 | @FunctionalInterface 15 | public interface CheckedBiFunction extends BiFunction { 16 | 17 | @Override 18 | default R apply(P1 p1, P2 p2) { 19 | try { 20 | return doApply(p1, p2); 21 | } catch (RuntimeException e) { 22 | throw e; 23 | } catch (Exception e) { 24 | throw new CommandRuntimeException(e); 25 | } 26 | } 27 | 28 | R doApply(P1 p1, P2 p2) throws Exception; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/function/CheckedConsumer.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.function; 2 | 3 | import java.util.function.Consumer; 4 | 5 | import net.robinfriedli.aiode.exceptions.CommandRuntimeException; 6 | 7 | /** 8 | * Consumer that handles checked exceptions by wrapping them into {@link CommandRuntimeException} 9 | * 10 | * @param the type of parameter to execute an operation on 11 | */ 12 | @FunctionalInterface 13 | public interface CheckedConsumer extends Consumer { 14 | 15 | @Override 16 | default void accept(T t) { 17 | try { 18 | doAccept(t); 19 | } catch (RuntimeException e) { 20 | throw e; 21 | } catch (Exception e) { 22 | throw new CommandRuntimeException(e); 23 | } 24 | } 25 | 26 | void doAccept(T t) throws Exception; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/function/CheckedFunction.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.function; 2 | 3 | import java.util.function.Function; 4 | 5 | import net.robinfriedli.aiode.exceptions.CommandRuntimeException; 6 | 7 | /** 8 | * Function that handles checked exceptions by wrapping them into {@link CommandRuntimeException} 9 | * 10 | * @param

the type of the parameter 11 | * @param the return type 12 | */ 13 | @FunctionalInterface 14 | public interface CheckedFunction extends Function { 15 | 16 | @Override 17 | default R apply(P p) { 18 | try { 19 | return doApply(p); 20 | } catch (RuntimeException e) { 21 | throw e; 22 | } catch (Exception e) { 23 | throw new CommandRuntimeException(e); 24 | } 25 | } 26 | 27 | R doApply(P p) throws Exception; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/function/CheckedRunnable.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.function; 2 | 3 | import net.robinfriedli.aiode.exceptions.CommandRuntimeException; 4 | 5 | /** 6 | * Runnable that handles checked exceptions by wrapping them into {@link CommandRuntimeException} 7 | */ 8 | @FunctionalInterface 9 | public interface CheckedRunnable extends Runnable { 10 | 11 | @Override 12 | default void run() { 13 | try { 14 | doRun(); 15 | } catch (RuntimeException e) { 16 | throw e; 17 | } catch (Exception e) { 18 | throw new CommandRuntimeException(e); 19 | } 20 | } 21 | 22 | void doRun() throws Exception; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/function/FunctionInvoker.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.function; 2 | 3 | import java.util.function.Consumer; 4 | import java.util.function.Function; 5 | 6 | import net.robinfriedli.exec.Mode; 7 | 8 | /** 9 | * Implementations may manage calling a function in a certain way, e.g. setting up a transaction. 10 | * 11 | * @param

the type of the function parameter 12 | */ 13 | public interface FunctionInvoker

{ 14 | 15 | V invokeFunction(Function function); 16 | 17 | default void invokeConsumer(Consumer

consumer) { 18 | invokeFunction(p -> { 19 | consumer.accept(p); 20 | return null; 21 | }); 22 | } 23 | 24 | V invokeFunction(Mode mode, Function function); 25 | 26 | default void invokeConsumer(Mode mode, Consumer

consumer) { 27 | invokeFunction(mode, p -> { 28 | consumer.accept(p); 29 | return null; 30 | }); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/function/LoggingRunnable.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.function; 2 | 3 | import net.robinfriedli.aiode.Aiode; 4 | 5 | @FunctionalInterface 6 | public interface LoggingRunnable extends CheckedRunnable { 7 | 8 | @Override 9 | default void run() { 10 | try { 11 | doRun(); 12 | } catch (Exception e) { 13 | Aiode.LOGGER.error("Uncaught exception in thread " + Thread.currentThread(), e); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/function/modes/RecursionPreventionMode.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.function.modes; 2 | 3 | import java.util.Set; 4 | import java.util.concurrent.Callable; 5 | 6 | import com.google.common.collect.Sets; 7 | import net.robinfriedli.aiode.concurrent.ThreadContext; 8 | import net.robinfriedli.exec.AbstractNestedModeWrapper; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | /** 12 | * Mode that breaks recursion by simply not executing tasks with this mode applied if there already is a task with this 13 | * mode and the same recursion key running in the current thread. 14 | */ 15 | public class RecursionPreventionMode extends AbstractNestedModeWrapper { 16 | 17 | private final String key; 18 | 19 | public RecursionPreventionMode(String key) { 20 | this.key = key; 21 | } 22 | 23 | @NotNull 24 | @Override 25 | public Callable wrap(@NotNull Callable callable) { 26 | return () -> { 27 | ThreadContext threadContext = ThreadContext.Current.get(); 28 | Set usedKeys; 29 | if (threadContext.isInstalled("recursion_prevention_keys")) { 30 | //noinspection unchecked 31 | usedKeys = threadContext.require("recursion_prevention_keys", Set.class); 32 | 33 | if (!usedKeys.add(key)) { 34 | // task was already running in this mode, this is a recursive call -> return 35 | return null; 36 | } 37 | } else { 38 | usedKeys = Sets.newHashSet(key); 39 | threadContext.install("recursion_prevention_keys", usedKeys); 40 | } 41 | 42 | try { 43 | return callable.call(); 44 | } finally { 45 | usedKeys.remove(key); 46 | } 47 | }; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/function/modes/SpotifyAuthorizationMode.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.function.modes; 2 | 3 | import java.util.concurrent.Callable; 4 | 5 | import net.robinfriedli.exec.AbstractNestedModeWrapper; 6 | import net.robinfriedli.exec.Mode; 7 | import org.jetbrains.annotations.NotNull; 8 | import se.michaelthelin.spotify.SpotifyApi; 9 | import se.michaelthelin.spotify.model_objects.credentials.ClientCredentials; 10 | 11 | /** 12 | * Mode that runs the given task with default Spotify credentials applied 13 | */ 14 | public class SpotifyAuthorizationMode extends AbstractNestedModeWrapper { 15 | 16 | private final SpotifyApi spotifyApi; 17 | 18 | public SpotifyAuthorizationMode(SpotifyApi spotifyApi) { 19 | this.spotifyApi = spotifyApi; 20 | } 21 | 22 | @Override 23 | public @NotNull Callable wrap(@NotNull Callable callable) { 24 | return () -> { 25 | String prevAccessToken = spotifyApi.getAccessToken(); 26 | try { 27 | ClientCredentials credentials = spotifyApi.clientCredentials().build().execute(); 28 | spotifyApi.setAccessToken(credentials.getAccessToken()); 29 | 30 | return callable.call(); 31 | } finally { 32 | spotifyApi.setAccessToken(prevAccessToken); 33 | } 34 | }; 35 | } 36 | 37 | public Mode getMode(SpotifyApi spotifyApi) { 38 | return Mode.create().with(new SpotifyAuthorizationMode(spotifyApi)); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/function/modes/SpotifyMarketMode.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.function.modes; 2 | 3 | import java.util.concurrent.Callable; 4 | 5 | import com.neovisionaries.i18n.CountryCode; 6 | import net.robinfriedli.aiode.audio.spotify.SpotifyContext; 7 | import net.robinfriedli.aiode.concurrent.ThreadContext; 8 | import net.robinfriedli.exec.AbstractNestedModeWrapper; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | /** 12 | * Set the Spotify market used for requests in the current {@link SpotifyContext}. 13 | */ 14 | public class SpotifyMarketMode extends AbstractNestedModeWrapper { 15 | 16 | private final CountryCode market; 17 | 18 | public SpotifyMarketMode(CountryCode market) { 19 | this.market = market; 20 | } 21 | 22 | @Override 23 | public @NotNull Callable wrap(@NotNull Callable callable) { 24 | return () -> { 25 | SpotifyContext spotifyContext = ThreadContext.Current.get().getOrCompute(SpotifyContext.class, SpotifyContext::new); 26 | spotifyContext.setMarket(market); 27 | return callable.call(); 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/function/modes/SpotifyUserAuthorizationMode.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.function.modes; 2 | 3 | import java.util.concurrent.Callable; 4 | 5 | import net.robinfriedli.aiode.login.Login; 6 | import net.robinfriedli.exec.AbstractNestedModeWrapper; 7 | import org.jetbrains.annotations.NotNull; 8 | import se.michaelthelin.spotify.SpotifyApi; 9 | 10 | /** 11 | * Mode that runs the given task with Spotify credentials for the given Login applied applied 12 | */ 13 | public class SpotifyUserAuthorizationMode extends AbstractNestedModeWrapper { 14 | 15 | private final Login login; 16 | private final SpotifyApi spotifyApi; 17 | 18 | public SpotifyUserAuthorizationMode(Login login, SpotifyApi spotifyApi) { 19 | this.login = login; 20 | this.spotifyApi = spotifyApi; 21 | } 22 | 23 | @Override 24 | public @NotNull Callable wrap(@NotNull Callable callable) { 25 | return () -> { 26 | String prevAccessToken = spotifyApi.getAccessToken(); 27 | try { 28 | spotifyApi.setAccessToken(login.getAccessToken()); 29 | return callable.call(); 30 | } finally { 31 | spotifyApi.setAccessToken(prevAccessToken); 32 | } 33 | }; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/persist/customchange/PermissionTargetTypeInitialValues.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.persist.customchange; 2 | 3 | import net.robinfriedli.aiode.command.PermissionTarget; 4 | 5 | public class PermissionTargetTypeInitialValues extends InsertEnumLookupValuesChange { 6 | 7 | @Override 8 | protected PermissionTarget.TargetType[] getValues() { 9 | return PermissionTarget.TargetType.values(); 10 | } 11 | 12 | @Override 13 | protected String getTableName() { 14 | return "permission_type"; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/persist/customchange/PlaybackHistorySourceInitialValues.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.persist.customchange; 2 | 3 | import net.robinfriedli.aiode.audio.Playable; 4 | 5 | public class PlaybackHistorySourceInitialValues extends InsertEnumLookupValuesChange { 6 | 7 | @Override 8 | protected Playable.Source[] getValues() { 9 | return Playable.Source.values(); 10 | } 11 | 12 | @Override 13 | protected String getTableName() { 14 | return "playback_history_source"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/persist/customchange/ScriptUsageInitialValues.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.persist.customchange; 2 | 3 | import net.robinfriedli.aiode.scripting.ScriptUsageType; 4 | 5 | public class ScriptUsageInitialValues extends InsertEnumLookupValuesChange { 6 | 7 | @Override 8 | protected ScriptUsageType[] getValues() { 9 | return ScriptUsageType.values(); 10 | } 11 | 12 | @Override 13 | protected String getTableName() { 14 | return "script_usage"; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/persist/customchange/SpotifyItemKindInitialValues.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.persist.customchange; 2 | 3 | import net.robinfriedli.aiode.audio.spotify.SpotifyTrackKind; 4 | 5 | public class SpotifyItemKindInitialValues extends InsertEnumLookupValuesChange { 6 | 7 | @Override 8 | protected SpotifyTrackKind[] getValues() { 9 | return SpotifyTrackKind.values(); 10 | } 11 | 12 | @Override 13 | protected String getTableName() { 14 | return "spotify_item_kind"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/persist/interceptors/PlaylistItemTimestampInterceptor.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.persist.interceptors; 2 | 3 | import java.io.Serializable; 4 | import java.util.Date; 5 | 6 | import org.slf4j.Logger; 7 | 8 | import net.robinfriedli.aiode.entities.PlaylistItem; 9 | import org.hibernate.Interceptor; 10 | import org.hibernate.type.Type; 11 | 12 | public class PlaylistItemTimestampInterceptor extends ChainableInterceptor { 13 | 14 | public PlaylistItemTimestampInterceptor(Interceptor next, Logger logger) { 15 | super(next, logger); 16 | } 17 | 18 | @Override 19 | public void onSaveChained(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) { 20 | if (entity instanceof PlaylistItem) { 21 | Date createdTimestamp = new Date(); 22 | ((PlaylistItem) entity).setCreatedTimestamp(createdTimestamp); 23 | for (int i = 0; i < propertyNames.length; i++) { 24 | if ("createdTimestamp".equals(propertyNames[i])) { 25 | state[i] = createdTimestamp; 26 | } 27 | } 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/persist/qb/PredicateBuilder.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.persist.qb; 2 | 3 | import javax.annotation.CheckReturnValue; 4 | 5 | import jakarta.persistence.criteria.CriteriaBuilder; 6 | import jakarta.persistence.criteria.From; 7 | import jakarta.persistence.criteria.Predicate; 8 | 9 | /** 10 | * Functional interface that builds a JPA predicate by applying the provided criteria builder and query root. 11 | */ 12 | @FunctionalInterface 13 | public interface PredicateBuilder { 14 | 15 | Predicate build(CriteriaBuilder cb, From root, SubQueryBuilderFactory subQueryFactory); 16 | 17 | @CheckReturnValue 18 | default PredicateBuilder combine(PredicateBuilder other) { 19 | return (cb, root, subQueryFactory) -> cb.and(this.build(cb, root, subQueryFactory), other.build(cb, root, subQueryFactory)); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/persist/qb/QueryConsumer.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.persist.qb; 2 | 3 | import jakarta.persistence.criteria.AbstractQuery; 4 | import jakarta.persistence.criteria.CriteriaBuilder; 5 | import jakarta.persistence.criteria.CriteriaQuery; 6 | import jakarta.persistence.criteria.From; 7 | import jakarta.persistence.criteria.Subquery; 8 | 9 | /** 10 | * Custom consumer that accepts a Query root, CriteriaBuilder and parameterized JPA query implementation, either 11 | * {@link CriteriaQuery} or {@link Subquery}. 12 | * 13 | * @param the type of JPA query implementation 14 | */ 15 | @FunctionalInterface 16 | public interface QueryConsumer> { 17 | 18 | void accept(From root, CriteriaBuilder cb, Q query); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/persist/qb/SimplePredicateBuilder.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.persist.qb; 2 | 3 | import jakarta.persistence.criteria.CriteriaBuilder; 4 | import jakarta.persistence.criteria.From; 5 | import jakarta.persistence.criteria.Predicate; 6 | 7 | /** 8 | * Simplification of {@link PredicateBuilder} with only the essential parameters. 9 | */ 10 | @FunctionalInterface 11 | public interface SimplePredicateBuilder extends PredicateBuilder { 12 | 13 | Predicate build(CriteriaBuilder cb, From root); 14 | 15 | @Override 16 | default Predicate build(CriteriaBuilder cb, From root, SubQueryBuilderFactory subQueryFactory) { 17 | return build(cb, root); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/persist/qb/interceptor/QueryInterceptor.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.persist.qb.interceptor; 2 | 3 | import net.robinfriedli.aiode.persist.qb.QueryBuilder; 4 | 5 | /** 6 | * Interface for query interceptors that are called when building a {@link QueryBuilder} implementation. Implementations 7 | * may, for example, filter the query based on current guild context, only returning results owned by the current guild 8 | * without having to explicitly specify the condition in the query each time. 9 | */ 10 | public interface QueryInterceptor { 11 | 12 | /** 13 | * Executes the actual intercept logic that modifies the given query builder instance which is currently being built. 14 | * 15 | * @param queryBuilder the query builder to extend 16 | */ 17 | void intercept(QueryBuilder queryBuilder); 18 | 19 | /** 20 | * Executes a check whether this interceptor should get involved with the current query, based on the selected 21 | * entity model. 22 | * 23 | * @param entityClass the selected entity model 24 | * @return true if this interceptor should modify queries selecting the provided entity model 25 | */ 26 | boolean shouldIntercept(Class entityClass); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/persist/tasks/PersistTask.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.persist.tasks; 2 | 3 | public interface PersistTask { 4 | 5 | E perform() throws Exception; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/rest/ExceptionHandlerAdvice.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.rest; 2 | 3 | import net.robinfriedli.aiode.rest.exceptions.MissingAccessException; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.web.bind.annotation.ControllerAdvice; 6 | import org.springframework.web.bind.annotation.ExceptionHandler; 7 | 8 | @ControllerAdvice 9 | public class ExceptionHandlerAdvice { 10 | 11 | @ExceptionHandler 12 | public ResponseEntity handleMissingAccessException(MissingAccessException e) { 13 | return ResponseEntity.status(403).body(e.getMessage()); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/rest/RequestContext.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.rest; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | 5 | public class RequestContext { 6 | 7 | private final HttpServletRequest request; 8 | 9 | public RequestContext(HttpServletRequest request) { 10 | this.request = request; 11 | } 12 | 13 | public HttpServletRequest getRequest() { 14 | return request; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/rest/ServletCustomizer.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.rest; 2 | 3 | import org.springframework.boot.web.server.MimeMappings; 4 | import org.springframework.boot.web.server.WebServerFactoryCustomizer; 5 | import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | public class ServletCustomizer implements WebServerFactoryCustomizer { 10 | 11 | @Override 12 | public void customize(ConfigurableServletWebServerFactory factory) { 13 | MimeMappings mappings = new MimeMappings(MimeMappings.DEFAULT); 14 | mappings.add("wasm", "application/wasm"); 15 | factory.setMimeMappings(mappings); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/rest/annotations/AuthenticationRequired.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.rest.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Target; 5 | 6 | /** 7 | * Annotations for handler methods that states that the client must be connected to an active session to use this endpoint. 8 | * If {@link #requiredPermissions()} is not empty this further checks whether the member connected with this session 9 | * has the required permissions. 10 | */ 11 | @Target(ElementType.METHOD) 12 | public @interface AuthenticationRequired { 13 | 14 | /** 15 | * @return a string array containing the accessConfiguration permissionIdentifiers required to access this endpoint. 16 | */ 17 | String[] requiredPermissions() default {}; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/rest/exceptions/MissingAccessException.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.rest.exceptions; 2 | 3 | /** 4 | * Thrown when the bot cannot connect to the guild or user specified by the session of the web client, i.e. when getGuild 5 | * or getMember return null. 6 | */ 7 | public class MissingAccessException extends RuntimeException { 8 | 9 | public MissingAccessException() { 10 | super(); 11 | } 12 | 13 | public MissingAccessException(String message) { 14 | super(message); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/scripting/GroovyVariableProvider.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.scripting; 2 | 3 | import java.util.Map; 4 | 5 | public interface GroovyVariableProvider { 6 | 7 | Map provideVariables(); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/scripting/ScriptUsageType.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.scripting; 2 | 3 | public enum ScriptUsageType { 4 | script, 5 | interceptor, 6 | finalizer, 7 | trigger, 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/scripting/variables/ExecutionContextVariableProvider.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.scripting.variables; 2 | 3 | import java.util.Collections; 4 | import java.util.Map; 5 | 6 | import net.robinfriedli.aiode.concurrent.ExecutionContext; 7 | import net.robinfriedli.aiode.scripting.GroovyVariableProvider; 8 | 9 | public class ExecutionContextVariableProvider implements GroovyVariableProvider { 10 | 11 | @Override 12 | public Map provideVariables() { 13 | return ExecutionContext.Current.optional().map(ExecutionContext::getScriptParameters).orElse(Collections.emptyMap()); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/scripting/variables/SingletonVariableProvider.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.scripting.variables; 2 | 3 | import java.util.Map; 4 | 5 | import net.robinfriedli.aiode.Aiode; 6 | import net.robinfriedli.aiode.scripting.GroovyVariableProvider; 7 | 8 | public class SingletonVariableProvider implements GroovyVariableProvider { 9 | 10 | @Override 11 | public Map provideVariables() { 12 | Aiode aiode = Aiode.get(); 13 | return Map.of( 14 | "messages", aiode.getMessageService(), 15 | "securityManager", aiode.getSecurityManager(), 16 | "audioManager", aiode.getAudioManager() 17 | ); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/scripting/variables/ThreadContextVariableProvider.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.scripting.variables; 2 | 3 | import java.util.Collections; 4 | import java.util.Map; 5 | 6 | import net.robinfriedli.aiode.command.AbstractCommand; 7 | import net.robinfriedli.aiode.command.Command; 8 | import net.robinfriedli.aiode.concurrent.ThreadContext; 9 | import net.robinfriedli.aiode.scripting.GroovyVariableProvider; 10 | 11 | public class ThreadContextVariableProvider implements GroovyVariableProvider { 12 | 13 | @Override 14 | public Map provideVariables() { 15 | return ThreadContext.Current.optional(Command.class).map(command -> { 16 | if (command instanceof AbstractCommand) { 17 | return Map.of( 18 | "command", command, 19 | "input", ((AbstractCommand) command).getCommandInput() 20 | ); 21 | } else { 22 | return Map.of("command", command); 23 | } 24 | }).orElse(Collections.emptyMap()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/servers/ResourceHandler.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.servers; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | import java.nio.file.Files; 6 | import java.nio.file.Path; 7 | 8 | import org.slf4j.LoggerFactory; 9 | 10 | import com.sun.net.httpserver.HttpExchange; 11 | import com.sun.net.httpserver.HttpHandler; 12 | 13 | /** 14 | * Handler responsible for loading binaries in the resources-public directory 15 | */ 16 | public class ResourceHandler implements HttpHandler { 17 | 18 | @Override 19 | public void handle(HttpExchange exchange) throws IOException { 20 | try { 21 | byte[] bytes = Files.readAllBytes(Path.of("." + exchange.getRequestURI().toString())); 22 | exchange.sendResponseHeaders(200, bytes.length); 23 | OutputStream responseBody = exchange.getResponseBody(); 24 | responseBody.write(bytes); 25 | responseBody.close(); 26 | } catch (Exception e) { 27 | String response = e.toString(); 28 | exchange.sendResponseHeaders(500, response.getBytes().length); 29 | OutputStream responseBody = exchange.getResponseBody(); 30 | responseBody.write(response.getBytes()); 31 | responseBody.close(); 32 | LoggerFactory.getLogger(getClass()).error("Error in HttpHandler", e); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/servers/ServerUtil.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.servers; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | import java.nio.charset.StandardCharsets; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | import org.apache.http.NameValuePair; 13 | import org.apache.http.client.utils.URLEncodedUtils; 14 | 15 | import com.sun.net.httpserver.HttpExchange; 16 | 17 | public class ServerUtil { 18 | 19 | public static void handleError(HttpExchange exchange, Throwable e) throws IOException { 20 | String html = Files.readString(Path.of("html/default_error_page.html")); 21 | String response = String.format(html, e.getMessage()); 22 | exchange.sendResponseHeaders(500, response.getBytes().length); 23 | OutputStream responseBody = exchange.getResponseBody(); 24 | responseBody.write(response.getBytes()); 25 | responseBody.close(); 26 | } 27 | 28 | public static Map getParameters(HttpExchange exchange) { 29 | List parameters = URLEncodedUtils.parse(exchange.getRequestURI(), StandardCharsets.UTF_8); 30 | Map parameterMap = new HashMap<>(); 31 | parameters.forEach(param -> parameterMap.put(param.getName(), param.getValue())); 32 | return parameterMap; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/util/EmojiConstants.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.util; 2 | 3 | public class EmojiConstants { 4 | 5 | public static final String PLAY_PAUSE = "⏯"; 6 | public static final String PLAY = "\u25B6"; 7 | public static final String PAUSE = "\u23F8"; 8 | public static final String REWIND = "⏮"; 9 | public static final String SKIP = "⏭"; 10 | public static final String SHUFFLE = "\uD83D\uDD00"; 11 | public static final String REPEAT = "\uD83D\uDD01"; 12 | public static final String REPEAT_ONE = "\uD83D\uDD02"; 13 | public static final String VOLUME = "\uD83D\uDD08"; 14 | public static final String VOLUME_UP = "\uD83D\uDD0A"; 15 | public static final String VOLUME_DOWN = "\uD83D\uDD09"; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/net/robinfriedli/aiode/util/MutableTuple2.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.util; 2 | 3 | public class MutableTuple2 { 4 | 5 | private L left; 6 | private R right; 7 | 8 | public MutableTuple2(L left, R right) { 9 | this.left = left; 10 | this.right = right; 11 | } 12 | 13 | public static MutableTuple2 of(L left, R right) { 14 | return new MutableTuple2<>(left, right); 15 | } 16 | 17 | public L getLeft() { 18 | return left; 19 | } 20 | 21 | public void setLeft(L left) { 22 | this.left = left; 23 | } 24 | 25 | public R getRight() { 26 | return right; 27 | } 28 | 29 | public void setRight(R right) { 30 | this.right = right; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/kotlin/net/robinfriedli/aiode/audio/playables/AbstractPlayableContainer.kt: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.audio.playables 2 | 3 | import com.google.common.collect.Lists 4 | import net.robinfriedli.aiode.audio.Playable 5 | import net.robinfriedli.aiode.audio.queue.AudioQueue 6 | import net.robinfriedli.aiode.audio.queue.PlayableContainerQueueFragment 7 | import net.robinfriedli.aiode.audio.queue.QueueFragment 8 | 9 | /** 10 | * Abstract class for anything that represents a [Playable] or contains [Playable] instances, e.g. Spotify tracks, playlists 11 | * albums etc. 12 | */ 13 | abstract class AbstractPlayableContainer(private val item: T) : PlayableContainer { 14 | 15 | var playables: List? = null 16 | 17 | final override fun loadPlayables(playableFactory: PlayableFactory): List { 18 | if (playables == null) { 19 | playables = doLoadPlayables(playableFactory) 20 | } 21 | 22 | return playables!! 23 | } 24 | 25 | override fun createQueueFragment(playableFactory: PlayableFactory, queue: AudioQueue): QueueFragment? { 26 | val playables = loadPlayables(playableFactory) 27 | return if (playables.isNotEmpty()) { 28 | PlayableContainerQueueFragment(queue, Lists.newArrayList(playables), this) 29 | } else { 30 | null 31 | } 32 | } 33 | 34 | abstract fun doLoadPlayables(playableFactory: PlayableFactory): List 35 | 36 | override fun getItem(): T { 37 | return item 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/net/robinfriedli/aiode/audio/playables/AbstractSinglePlayableContainer.kt: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.audio.playables 2 | 3 | import net.robinfriedli.aiode.audio.Playable 4 | import net.robinfriedli.aiode.audio.queue.AudioQueue 5 | import net.robinfriedli.aiode.audio.queue.QueueFragment 6 | import net.robinfriedli.aiode.audio.queue.SinglePlayableQueueFragment 7 | import java.util.* 8 | 9 | abstract class AbstractSinglePlayableContainer(item: T) : AbstractPlayableContainer(item) { 10 | 11 | override fun doLoadPlayables(playableFactory: PlayableFactory): List { 12 | val playable = doLoadPlayable(playableFactory) 13 | return if (playable != null) { 14 | Collections.singletonList(playable) 15 | } else { 16 | Collections.emptyList() 17 | } 18 | } 19 | 20 | override fun loadPlayable(playableFactory: PlayableFactory): Playable? { 21 | val playables = loadPlayables(playableFactory) 22 | return when { 23 | playables.size == 1 -> playables[0] 24 | playables.isNotEmpty() -> throw UnsupportedOperationException("PlayableContainer $this does not resolve to a single Playable") 25 | else -> null 26 | } 27 | } 28 | 29 | override fun createQueueFragment(playableFactory: PlayableFactory, queue: AudioQueue): QueueFragment? { 30 | val playable = loadPlayable(playableFactory) 31 | return if (playable != null) { 32 | SinglePlayableQueueFragment(queue, playable, this) 33 | } else { 34 | null 35 | } 36 | } 37 | 38 | abstract fun doLoadPlayable(playableFactory: PlayableFactory): Playable? 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/net/robinfriedli/aiode/audio/playables/PlayableContainer.kt: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.audio.playables 2 | 3 | import net.robinfriedli.aiode.audio.Playable 4 | import net.robinfriedli.aiode.audio.queue.AudioQueue 5 | import net.robinfriedli.aiode.audio.queue.QueueFragment 6 | 7 | interface PlayableContainer { 8 | 9 | fun getItem(): T 10 | 11 | fun loadPlayables(playableFactory: PlayableFactory): List 12 | 13 | /** 14 | * Load a single Playable for this container. Only available for [AbstractSinglePlayableContainer] implementations, i.e. 15 | * containers that resolve to a single playable. Else this method always throws [UnsupportedOperationException]. 16 | * See [isSinglePlayable] to check whether the method is available for any given implementation of this interface. 17 | */ 18 | @Throws(UnsupportedOperationException::class) 19 | fun loadPlayable(playableFactory: PlayableFactory): Playable? { 20 | throw UnsupportedOperationException("PlayableContainer $this does not support resolving to a single Playable") 21 | } 22 | 23 | /** 24 | * Create a [QueueFragment] for this PlayableContainer, may return `null` if [loadPlayables] does not return any results. 25 | */ 26 | fun createQueueFragment(playableFactory: PlayableFactory, queue: AudioQueue): QueueFragment? 27 | 28 | fun isSinglePlayable(): Boolean { 29 | return false 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/kotlin/net/robinfriedli/aiode/audio/playables/PlayableContainerManager.kt: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.audio.playables 2 | 3 | import net.robinfriedli.aiode.util.ClassDescriptorNode 4 | import org.springframework.stereotype.Component 5 | 6 | @Component 7 | open class PlayableContainerManager(private val playableContainerProviders: List>) { 8 | 9 | /** 10 | * Finds a registered [PlayableContainerProvider] for the type of the provided item. This also finds [PlayableContainerProvider] 11 | * for supertypes, casting them down to the type of of the provided item, which is safe since the type parameter exclusively 12 | * refers to the type of the item. 13 | */ 14 | @Suppress("UNCHECKED_CAST") 15 | fun getPlayableContainer(item: T): PlayableContainer? { 16 | return playableContainerProviders.stream() 17 | .filter { playableContainerProvider -> playableContainerProvider.type.isAssignableFrom(item.javaClass) } 18 | .sorted(ClassDescriptorNode.getComparator()) 19 | .findFirst() 20 | .map { playableContainerProvider -> (playableContainerProvider as PlayableContainerProvider).getPlayableContainer(item) } 21 | .orElse(null) 22 | } 23 | 24 | fun requirePlayableContainer(item: T): PlayableContainer { 25 | return getPlayableContainer(item) 26 | ?: throw java.util.NoSuchElementException("No playable container provider found for item of type ${item::class.java}") 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/net/robinfriedli/aiode/audio/playables/PlayableContainerProvider.kt: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.audio.playables 2 | 3 | import net.robinfriedli.aiode.util.ClassDescriptorNode 4 | 5 | /** 6 | * Interface for singleton components that provide a [PlayableContainer] instance for an object of a specific type. 7 | */ 8 | interface PlayableContainerProvider : ClassDescriptorNode { 9 | 10 | override fun getType(): Class 11 | 12 | fun getPlayableContainer(item: T): PlayableContainer 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/kotlin/net/robinfriedli/aiode/audio/playables/containers/AudioPlaylistPlayableContainer.kt: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.audio.playables.containers 2 | 3 | import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist 4 | import net.robinfriedli.aiode.audio.Playable 5 | import net.robinfriedli.aiode.audio.UrlPlayable 6 | import net.robinfriedli.aiode.audio.playables.AbstractPlayableContainer 7 | import net.robinfriedli.aiode.audio.playables.PlayableContainer 8 | import net.robinfriedli.aiode.audio.playables.PlayableContainerProvider 9 | import net.robinfriedli.aiode.audio.playables.PlayableFactory 10 | import org.springframework.stereotype.Component 11 | import java.util.stream.Collectors 12 | 13 | class AudioPlaylistPlayableContainer(audioPlaylist: AudioPlaylist) : AbstractPlayableContainer(audioPlaylist) { 14 | override fun doLoadPlayables(playableFactory: PlayableFactory): List { 15 | return getItem().tracks.stream().map { track -> UrlPlayable(track) }.collect(Collectors.toList()) 16 | } 17 | } 18 | 19 | @Component 20 | class AudioPlaylistPlayableContainerProvider : PlayableContainerProvider { 21 | override fun getType(): Class { 22 | return AudioPlaylist::class.java 23 | } 24 | 25 | override fun getPlayableContainer(item: AudioPlaylist): PlayableContainer { 26 | return AudioPlaylistPlayableContainer(item) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/net/robinfriedli/aiode/audio/playables/containers/AudioTrackPlayableContainer.kt: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.audio.playables.containers 2 | 3 | import com.sedmelluq.discord.lavaplayer.track.AudioTrack 4 | import net.robinfriedli.aiode.audio.Playable 5 | import net.robinfriedli.aiode.audio.UrlPlayable 6 | import net.robinfriedli.aiode.audio.playables.AbstractSinglePlayableContainer 7 | import net.robinfriedli.aiode.audio.playables.PlayableContainer 8 | import net.robinfriedli.aiode.audio.playables.PlayableContainerProvider 9 | import net.robinfriedli.aiode.audio.playables.PlayableFactory 10 | import org.springframework.stereotype.Component 11 | 12 | class AudioTrackPlayableContainer(audioTrack: AudioTrack) : AbstractSinglePlayableContainer(audioTrack) { 13 | override fun doLoadPlayable(playableFactory: PlayableFactory): Playable { 14 | return UrlPlayable(getItem()) 15 | } 16 | } 17 | 18 | @Component 19 | class AudioTrackPlayableContainerProvider : PlayableContainerProvider { 20 | override fun getType(): Class { 21 | return AudioTrack::class.java 22 | } 23 | 24 | override fun getPlayableContainer(item: AudioTrack): PlayableContainer { 25 | return AudioTrackPlayableContainer(item) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/net/robinfriedli/aiode/audio/playables/containers/PlaylistPlayableContainer.kt: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.audio.playables.containers 2 | 3 | import net.robinfriedli.aiode.audio.Playable 4 | import net.robinfriedli.aiode.audio.playables.* 5 | import net.robinfriedli.aiode.entities.Playlist 6 | import net.robinfriedli.aiode.function.SpotifyInvoker 7 | import org.springframework.context.annotation.Lazy 8 | import org.springframework.stereotype.Component 9 | 10 | class PlaylistPlayableContainer( 11 | playlist: Playlist, 12 | private val playableContainerManager: PlayableContainerManager 13 | ) : AbstractPlayableContainer(playlist) { 14 | 15 | private val spotifyInvoker: SpotifyInvoker = SpotifyInvoker.createForCurrentContext() 16 | 17 | override fun doLoadPlayables(playableFactory: PlayableFactory): List { 18 | val items = spotifyInvoker.invokeFunction { spotifyApi -> getItem().getTracks(spotifyApi) } 19 | 20 | val playableContainers = items 21 | .stream() 22 | .map { track -> playableContainerManager.requirePlayableContainer(track) } 23 | .toList() 24 | 25 | return playableFactory.loadAll(playableContainers) 26 | } 27 | } 28 | 29 | @Component 30 | class PlaylistPlayableContainerProvider( 31 | @Lazy private val playableContainerManager: PlayableContainerManager 32 | ) : PlayableContainerProvider { 33 | 34 | override fun getType(): Class { 35 | return Playlist::class.java 36 | } 37 | 38 | override fun getPlayableContainer(item: Playlist): PlayableContainer { 39 | return PlaylistPlayableContainer(item, playableContainerManager) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/kotlin/net/robinfriedli/aiode/audio/playables/containers/SinglePlayableContainer.kt: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.audio.playables.containers 2 | 3 | import net.robinfriedli.aiode.audio.Playable 4 | import net.robinfriedli.aiode.audio.playables.AbstractSinglePlayableContainer 5 | import net.robinfriedli.aiode.audio.playables.PlayableContainer 6 | import net.robinfriedli.aiode.audio.playables.PlayableContainerProvider 7 | import net.robinfriedli.aiode.audio.playables.PlayableFactory 8 | import org.springframework.stereotype.Component 9 | 10 | class SinglePlayableContainer(playable: Playable) : AbstractSinglePlayableContainer(playable) { 11 | override fun doLoadPlayable(playableFactory: PlayableFactory): Playable { 12 | return getItem() 13 | } 14 | } 15 | 16 | @Component 17 | class SinglePlayableContainerProvider : PlayableContainerProvider { 18 | override fun getType(): Class { 19 | return Playable::class.java 20 | } 21 | 22 | override fun getPlayableContainer(item: Playable): PlayableContainer { 23 | return SinglePlayableContainer(item) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/kotlin/net/robinfriedli/aiode/audio/playables/containers/UrlTrackPlayableContainer.kt: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.audio.playables.containers 2 | 3 | import net.robinfriedli.aiode.audio.Playable 4 | import net.robinfriedli.aiode.audio.playables.AbstractSinglePlayableContainer 5 | import net.robinfriedli.aiode.audio.playables.PlayableContainer 6 | import net.robinfriedli.aiode.audio.playables.PlayableContainerProvider 7 | import net.robinfriedli.aiode.audio.playables.PlayableFactory 8 | import net.robinfriedli.aiode.entities.UrlTrack 9 | import org.springframework.stereotype.Component 10 | 11 | class UrlTrackPlayableContainer(urlTrack: UrlTrack) : AbstractSinglePlayableContainer(urlTrack) { 12 | override fun doLoadPlayable(playableFactory: PlayableFactory): Playable? { 13 | return getItem().asPlayable() 14 | } 15 | } 16 | 17 | @Component 18 | class UrlTrackPlayableContainerProvider : PlayableContainerProvider { 19 | override fun getType(): Class { 20 | return UrlTrack::class.java 21 | } 22 | 23 | override fun getPlayableContainer(item: UrlTrack): PlayableContainer { 24 | return UrlTrackPlayableContainer(item) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/kotlin/net/robinfriedli/aiode/audio/playables/containers/YouTubePlaylistPlayableContainer.kt: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.audio.playables.containers 2 | 3 | import net.robinfriedli.aiode.audio.Playable 4 | import net.robinfriedli.aiode.audio.playables.AbstractPlayableContainer 5 | import net.robinfriedli.aiode.audio.playables.PlayableContainer 6 | import net.robinfriedli.aiode.audio.playables.PlayableContainerProvider 7 | import net.robinfriedli.aiode.audio.playables.PlayableFactory 8 | import net.robinfriedli.aiode.audio.youtube.YouTubePlaylist 9 | import org.springframework.stereotype.Component 10 | 11 | class YouTubePlaylistPlayableContainer(youTubePlaylist: YouTubePlaylist) : AbstractPlayableContainer(youTubePlaylist) { 12 | override fun doLoadPlayables(playableFactory: PlayableFactory): List { 13 | return playableFactory.createPlayables(getItem()) 14 | } 15 | } 16 | 17 | @Component 18 | class YouTubePlaylistPlayableContainerProvider : PlayableContainerProvider { 19 | override fun getType(): Class { 20 | return YouTubePlaylist::class.java 21 | } 22 | 23 | override fun getPlayableContainer(item: YouTubePlaylist): PlayableContainer { 24 | return YouTubePlaylistPlayableContainer(item) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/resources/current-version.txt: -------------------------------------------------------------------------------- 1 | 2.3.1 -------------------------------------------------------------------------------- /src/main/resources/ehcache.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 13 | 14 | 15 | 19 | 20 | 21 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %highlight(%-5level) %d{ISO8601} [%blue(%t)] %yellow(%C): %msg%n%throwable 9 | 10 | 11 | 12 | 13 | ${LOGS}/logs.log 14 | 15 | %p %d [%t] %C %m%n 16 | 17 | 18 | 19 | ${LOGS}/logs-%d{yyyy-MM-dd}.log 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/main/resources/quartz.properties: -------------------------------------------------------------------------------- 1 | org.quartz.scheduler.instanceName=QuartzScheduler 2 | org.quartz.threadPool.threadCount=3 3 | org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore -------------------------------------------------------------------------------- /src/main/resources/schemas/commandInterceptorSchema.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/resources/schemas/cronJobSchema.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Whether this task should only run on the main instance, see application property 19 | aiode.preferences.main_instance. 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/resources/schemas/embedDocumentSchema.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/resources/schemas/groovyVariableProviderSchema.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/resources/schemas/guildPropertySchema.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/resources/schemas/guildSpecificationSchema.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/resources/schemas/httpHandlerSchema.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/resources/schemas/startupTaskSchema.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Whether this task should only run on the main instance, see application property 17 | aiode.preferences.main_instance. Only applicable to tasks where runForEachShard is false. 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/resources/schemas/versionSchema.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/resources/schemas/widgetSchema.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Allow multiple instances of this widget class to be active at the same time within the same guild. If false, 19 | creating a new widget of this class destroys the previous widget. 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/resources/xml-contributions/cronJobs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/resources/xml-contributions/groovyVariableProviders.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/xml-contributions/httpHandlers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/resources/xml-contributions/startupTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/webapp/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | 5 | /pkg 6 | wasm-pack.log -------------------------------------------------------------------------------- /src/main/webapp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webapp" 3 | version = "0.1.0" 4 | authors = ["robinfriedli "] 5 | edition = "2018" 6 | license = "Apache-2.0" 7 | description = "Web client for the botify REST API" 8 | repository = "https://github.com/robinfriedli/botify" 9 | 10 | [lib] 11 | crate-type = ["cdylib"] 12 | 13 | [dependencies] 14 | seed = "0.6.0" 15 | serde = "1" 16 | serde_json = "1" 17 | uuid = { version = "0.8", features = ["wasm-bindgen", "v4"] } 18 | wasm-bindgen = "0.2.58" 19 | -------------------------------------------------------------------------------- /src/main/webapp/css/style.css: -------------------------------------------------------------------------------- 1 | .standard_text { 2 | font-family: "Montserrat", Arial, serif; 3 | } 4 | 5 | .white { 6 | color: white; 7 | } 8 | 9 | .green { 10 | color: #1DB954; 11 | } 12 | 13 | .green_background { 14 | background-color: #1DB954; 15 | } 16 | 17 | .grey_background { 18 | background-color: dimgrey; 19 | } 20 | 21 | .red { 22 | color: red; 23 | } 24 | 25 | .center { 26 | text-align: center; 27 | margin: 0 auto; 28 | } 29 | 30 | .left { 31 | display:inline-block; 32 | float: left; 33 | } 34 | 35 | .right { 36 | display:inline-block; 37 | float: right; 38 | } 39 | 40 | .standard_table { 41 | border-spacing: 10px; 42 | } 43 | 44 | .round { 45 | border-radius: 50%; 46 | } 47 | 48 | .no_border { 49 | border: 0 solid black; 50 | } -------------------------------------------------------------------------------- /src/main/webapp/img/botify-logo-legacy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robinfriedli/aiode/bed8fd818131c760056782bf45c1a7d354ca78a7/src/main/webapp/img/botify-logo-legacy.png -------------------------------------------------------------------------------- /src/main/webapp/img/botify-logo-small-legacy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robinfriedli/aiode/bed8fd818131c760056782bf45c1a7d354ca78a7/src/main/webapp/img/botify-logo-small-legacy.png -------------------------------------------------------------------------------- /src/main/webapp/img/botify-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robinfriedli/aiode/bed8fd818131c760056782bf45c1a7d354ca78a7/src/main/webapp/img/botify-logo-small.png -------------------------------------------------------------------------------- /src/main/webapp/img/botify-logo-wide-legacy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robinfriedli/aiode/bed8fd818131c760056782bf45c1a7d354ca78a7/src/main/webapp/img/botify-logo-wide-legacy.png -------------------------------------------------------------------------------- /src/main/webapp/img/botify-logo-wide-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robinfriedli/aiode/bed8fd818131c760056782bf45c1a7d354ca78a7/src/main/webapp/img/botify-logo-wide-transparent.png -------------------------------------------------------------------------------- /src/main/webapp/img/botify-logo-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robinfriedli/aiode/bed8fd818131c760056782bf45c1a7d354ca78a7/src/main/webapp/img/botify-logo-wide.png -------------------------------------------------------------------------------- /src/main/webapp/img/botify-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robinfriedli/aiode/bed8fd818131c760056782bf45c1a7d354ca78a7/src/main/webapp/img/botify-logo.png -------------------------------------------------------------------------------- /src/main/webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | botify 15 | 16 | 17 | 18 |

19 | 23 | 24 | 25 | 33 | 34 | -------------------------------------------------------------------------------- /src/main/webapp/src/page/home.rs: -------------------------------------------------------------------------------- 1 | use seed::{*, prelude::*}; 2 | 3 | use crate::session::Client; 4 | 5 | pub type Msg = i32; 6 | 7 | pub struct Model; 8 | 9 | pub fn view(model: &Model, client: &Client) -> Node { 10 | let user = &client.user; 11 | let guild = &client.guild; 12 | let text_channel = &client.text_channel; 13 | 14 | div![ 15 | h1![class!["standard_text white"], 16 | format!("Welcome {}", client.user.name) 17 | ], 18 | table![class!["standard_text standard_table white"], 19 | tr![ 20 | td!["User:"], 21 | td![format!("{}", user.name)] 22 | ], 23 | tr![ 24 | td!["Guild:"], 25 | td![format!("{}", guild.name)] 26 | ], 27 | tr![ 28 | td!["Channel:"], 29 | td![format!("{}", text_channel.name)] 30 | ] 31 | ] 32 | ] 33 | } -------------------------------------------------------------------------------- /src/main/webapp/src/page/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; 2 | pub mod home; -------------------------------------------------------------------------------- /src/test/java/net/robinfriedli/aiode/function/modes/RecursionPreventionModeTest.java: -------------------------------------------------------------------------------- 1 | package net.robinfriedli.aiode.function.modes; 2 | 3 | import org.testng.annotations.*; 4 | 5 | import net.robinfriedli.exec.BaseInvoker; 6 | import net.robinfriedli.exec.Mode; 7 | 8 | public class RecursionPreventionModeTest { 9 | 10 | @Test 11 | public void testNoRecurse() { 12 | RecursionPreventionMode recursionPreventionMode = new RecursionPreventionMode("test"); 13 | 14 | new BaseInvoker().invoke(Mode.create().with(recursionPreventionMode), this::testNoRecurse); 15 | } 16 | 17 | } 18 | --------------------------------------------------------------------------------