Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Kavita #39

Merged
merged 9 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion bin/running_on_dart.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ void main() async {
..addCommand(admin)
..addCommand(jellyfin)
..addCommand(reminderMessageCommand)
..addCommand(kavita)
..addConverter(settingsConverter)
..addConverter(manageableTagConverter)
..addConverter(durationConverter)
..addConverter(reminderConverter)
..addConverter(packageDocsConverter)
..addConverter(jellyfinConfigConverter)
..addConverter(jellyfinConfigUserConverter);
..addConverter(jellyfinConfigUserConverter)
..addConverter(kavitaConfigConverter)
..addConverter(kavitaUserConfigsConverter);

commands.onCommandError.listen(handleException);

Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ services:
image: postgres:17
container_name: running_on_dart_db
restart: always
ports:
- "13323:5432"
env_file:
- .env
volumes:
Expand Down
1 change: 1 addition & 0 deletions lib/running_on_dart.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export 'src/commands/admin.dart' show admin;
export 'src/commands/jellyfin.dart' show jellyfin;
export 'src/commands/tag.dart' show tag;
export 'src/commands/reminder.dart' show reminderMessageCommand;
export 'src/commands/kavita.dart' show kavita;

export 'src/modules/join_logs.dart';
export 'src/modules/poop_name.dart';
Expand Down
45 changes: 35 additions & 10 deletions lib/src/checks.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:injector/injector.dart';
import 'package:nyxx/nyxx.dart';
import 'package:nyxx_commands/nyxx_commands.dart';
Expand All @@ -11,24 +13,31 @@ const jellyfinFeatureEnabledCheckName = 'jellyfinFeatureEnabledCheck';
final administratorCheck = UserCheck.anyId(adminIds, name: 'Administrator check');
final administratorGuildCheck = GuildCheck.id(adminGuildId, name: 'Administrator Guild check');

final jellyfinFeatureEnabledCheck = Check(
(CommandContext context) {
if (context.guild == null) {
return true;
}
FutureOr<bool> _checkForSetting(Setting setting, CommandContext context) {
if (context.guild == null) {
return true;
}

return Injector.appInstance.get<FeatureSettingsService>().isEnabled(Setting.jellyfin, context.guild!.id);
},
return Injector.appInstance.get<FeatureSettingsService>().isEnabled(Setting.jellyfin, context.guild!.id);
}

final kavitaJellyfinCheck = Check(
(CommandContext context) => _checkForSetting(Setting.kavita, context),
name: jellyfinFeatureEnabledCheckName,
);

Future<(bool?, FeatureSetting?)> fetchAndCheckSetting(CommandContext context) async {
final jellyfinFeatureEnabledCheck = Check(
(CommandContext context) => _checkForSetting(Setting.jellyfin, context),
name: jellyfinFeatureEnabledCheckName,
);

Future<(bool?, FeatureSetting?)> fetchAndCheckSetting(CommandContext context, Setting settingToCheck) async {
if (context.guild == null) {
return (true, null);
}

final setting =
await Injector.appInstance.get<FeatureSettingsRepository>().fetchSetting(Setting.jellyfin, context.guild!.id);
await Injector.appInstance.get<FeatureSettingsRepository>().fetchSetting(settingToCheck, context.guild!.id);
if (setting == null) {
return (false, null);
}
Expand All @@ -42,7 +51,23 @@ Future<(bool?, FeatureSetting?)> fetchAndCheckSetting(CommandContext context) as

final jellyfinFeatureCreateInstanceCommandCheck = Check(
(CommandContext context) async {
final (checkResult, setting) = await fetchAndCheckSetting(context);
final (checkResult, setting) = await fetchAndCheckSetting(context, Setting.jellyfin);
if (checkResult != null) {
return checkResult;
}

if (context.member?.permissions?.isAdministrator ?? false) {
return true;
}

final roleId = Snowflake.parse(setting!.dataAsJson!['create_instance_role']);
return context.member!.roleIds.contains(roleId);
},
);

final kavitaFeatureCreateInstanceCommandCheck = Check(
(CommandContext context) async {
final (checkResult, setting) = await fetchAndCheckSetting(context, Setting.kavita);
if (checkResult != null) {
return checkResult;
}
Expand Down
115 changes: 115 additions & 0 deletions lib/src/commands/kavita.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import 'package:injector/injector.dart';
import 'package:nyxx/nyxx.dart';
import 'package:nyxx_commands/nyxx_commands.dart';
import 'package:nyxx_extensions/nyxx_extensions.dart';
import 'package:running_on_dart/src/checks.dart';
import 'package:running_on_dart/src/models/kavita.dart';
import 'package:running_on_dart/src/modules/kavita.dart';
import 'package:running_on_dart/src/repository/kavita.dart';
import 'package:running_on_dart/src/util/kavita.dart';
import 'package:running_on_dart/src/util/util.dart';

Future<AuthenticatedKavitaClient> getKavitaClient(KavitaUserConfig? config, ChatContext context) async {
config ??= await Injector.appInstance
.get<KavitaModule>()
.fetchGetUserConfigWithFallback(userId: context.user.id, parentId: getParentIdFromContext(context));

if (config == null) {
throw Exception("Invalid jellyfin config or user not logged in.");
}
return Injector.appInstance.get<KavitaModule>().createAuthenticatedClient(config);
}

final kavita = ChatGroup('kavita', 'Kavita related commands', checks: [
kavitaJellyfinCheck,
], children: [
ChatGroup(
'settings',
'Settings for Kavita',
children: [
ChatCommand(
'add-instance',
'Add new kavita instance',
id('kavita-settings-add-instance', (InteractionChatContext context) async {
final modalResponse = await context.getModal(title: "New Instance Configuration", components: [
TextInputBuilder(customId: "name", style: TextInputStyle.short, label: "Instance Name", isRequired: true),
TextInputBuilder(customId: "base_url", style: TextInputStyle.short, label: "Base Url", isRequired: true),
TextInputBuilder(customId: "is_default", style: TextInputStyle.short, label: "Is Default (True/False)"),
]);

final newlyCreatedConfig = await Injector.appInstance.get<KavitaRepository>().saveConfig(
KavitaConfig(
name: modalResponse['name']!,
basePath: modalResponse['base_url']!,
isDefault: modalResponse['is_default']?.toLowerCase() == 'true',
parentId: getParentIdFromContext(context),
),
);

modalResponse
.respond(MessageBuilder(content: "Added new jellyfin instance with name: ${newlyCreatedConfig.name}"));
}),
checks: [kavitaFeatureCreateInstanceCommandCheck]),
],
),
ChatGroup(
'user',
'User related kavita commands',
children: [
ChatCommand(
"login",
"Login user into given kavita instance",
id('kavita-user-login',
(InteractionChatContext context, @Description('Kavita instance to be used') KavitaConfig config) async {
final kavitaModule = Injector.appInstance.get<KavitaModule>();

final modalResult = await context.getModal(title: "Login to Kavita", components: [
TextInputBuilder(customId: 'username', style: TextInputStyle.short, label: 'Username', isRequired: true),
TextInputBuilder(customId: 'password', style: TextInputStyle.short, label: 'Password', isRequired: true),
]);

final apiLoginResult = await kavitaModule
.createUnauthenticatedClient(config)
.login(modalResult['username']!, modalResult['password']!);

await kavitaModule.login(config, apiLoginResult, context.user.id);

return context.respond(MessageBuilder(content: "Logged in successfully!"));
}),
)
],
),
ChatCommand(
'search',
'Search kavita library',
id('kavita-test', (ChatContext context, @Description('Query string to search content with') String query,
[@Description('Kavita instance to be used. Default if not provided') KavitaUserConfig? config]) async {
final client = await getKavitaClient(config, context);

final items = await client.searchSeries(query);
final paginator = await pagination.builders(await getSearchEmbedPages(items, client).toList());

return context.respond(paginator);
})),
ChatCommand(
'read',
'Read series',
id('kavita-read', (ChatContext context, int seriesId,
[@Description('Whether save reading progress to kavita') bool saveReadProgress = true,
@Description('Kavita instance to be used. Default if not provided') KavitaUserConfig? config]) async {
final client = await getKavitaClient(config, context);

final continuePoint = await client.getContinuePoint(seriesId);
if (continuePoint.isBook) {
return context.respond(MessageBuilder(content: "Books are not currently supported."));
}

final paginator = await pagination.factories(
await generateReadingPaginationFactories(continuePoint, client, seriesId, saveReadProgress).toList(),
startIndex: continuePoint.pagesRead,
userId: context.user.id);

return context.respond(paginator);
}),
),
]);
21 changes: 21 additions & 0 deletions lib/src/converter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ import 'package:nyxx_commands/nyxx_commands.dart';
import 'package:running_on_dart/src/models/docs.dart';
import 'package:running_on_dart/src/models/feature_settings.dart';
import 'package:running_on_dart/src/models/jellyfin_config.dart';
import 'package:running_on_dart/src/models/kavita.dart';
import 'package:running_on_dart/src/models/reminder.dart';
import 'package:running_on_dart/src/modules/docs.dart';
import 'package:running_on_dart/src/modules/jellyfin.dart';
import 'package:running_on_dart/src/modules/kavita.dart';
import 'package:running_on_dart/src/modules/reminder.dart';
import 'package:running_on_dart/src/modules/tag.dart';
import 'package:running_on_dart/src/repository/jellyfin_config.dart';
import 'package:running_on_dart/src/repository/kavita.dart';
import 'package:running_on_dart/src/settings.dart';
import 'package:running_on_dart/src/util/util.dart';

import 'models/tag.dart';

Expand Down Expand Up @@ -72,6 +76,23 @@ final jellyfinConfigUserConverter = Converter<JellyfinConfigUser>(
.map((config) => CommandOptionChoiceBuilder(name: config.name, value: config.name)),
);

final kavitaUserConfigsConverter = Converter<KavitaUserConfig>(
(view, context) async {
return Injector.appInstance.get<KavitaModule>().fetchGetUserConfigWithFallback(
userId: context.user.id, parentId: context.guild?.id ?? context.user.id, instanceName: view.getQuotedWord());
},
autocompleteCallback: (context) async =>
(await Injector.appInstance.get<KavitaRepository>().findAllForParent(getParentIdFromContext(context).toString()))
.map((config) => CommandOptionChoiceBuilder(name: config.name, value: config.name)),
);

String stringifyKavitaConfig(KavitaConfig config) => config.name;
Future<Iterable<KavitaConfig>> getKavitaConfigs(ContextData context) =>
Injector.appInstance.get<KavitaRepository>().findAllForParent(getParentIdFromContext(context).toString());

const kavitaConfigConverter =
SimpleConverter<KavitaConfig>(provider: getKavitaConfigs, stringify: stringifyKavitaConfig);

Future<Iterable<JellyfinConfig>> getJellyfinConfigs(ContextData context) => Injector.appInstance
.get<JellyfinConfigRepository>()
.getConfigsForParent((context.guild?.id ?? context.user.id).toString());
Expand Down
6 changes: 5 additions & 1 deletion lib/src/init.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import 'package:running_on_dart/src/modules/bot_start_duration.dart';
import 'package:running_on_dart/src/modules/docs.dart';
import 'package:running_on_dart/src/modules/jellyfin.dart';
import 'package:running_on_dart/src/modules/join_logs.dart';
import 'package:running_on_dart/src/modules/kavita.dart';
import 'package:running_on_dart/src/modules/mentions.dart';
import 'package:running_on_dart/src/modules/mod_log.dart';
import 'package:running_on_dart/src/modules/poop_name.dart';
import 'package:running_on_dart/src/modules/reminder.dart';
import 'package:running_on_dart/src/modules/tag.dart';
import 'package:running_on_dart/src/repository/feature_settings.dart';
import 'package:running_on_dart/src/repository/jellyfin_config.dart';
import 'package:running_on_dart/src/repository/kavita.dart';
import 'package:running_on_dart/src/repository/reminder.dart';
import 'package:running_on_dart/src/repository/tag.dart';
import 'package:running_on_dart/src/services/db.dart';
Expand All @@ -24,6 +26,7 @@ Future<void> setupContainer(NyxxGateway client) async {
..registerSingleton(() => JellyfinConfigRepository())
..registerSingleton(() => ReminderRepository())
..registerSingleton(() => TagRepository())
..registerSingleton(() => KavitaRepository())
..registerSingleton(() => FeatureSettingsService())
..registerSingleton(() => BotStartDuration())
..registerSingleton(() => PoopNameModule())
Expand All @@ -33,7 +36,8 @@ Future<void> setupContainer(NyxxGateway client) async {
..registerSingleton(() => TagModule())
..registerSingleton(() => DocsModule())
..registerSingleton(() => JellyfinModuleV2())
..registerSingleton(() => MentionsMonitoringModule());
..registerSingleton(() => MentionsMonitoringModule())
..registerSingleton(() => KavitaModule());

await Injector.appInstance.get<DatabaseService>().init();
await Injector.appInstance.get<JellyfinModuleV2>().init();
Expand Down
3 changes: 2 additions & 1 deletion lib/src/models/feature_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ enum Setting {
joinLogs('join_logs', 'Logs member join events into specified channel', true, DataType.channelMention),
modLogs('mod_logs', 'Logs administration event into specified channel', true, DataType.channelMention),
jellyfin('jellyfin', 'Allows usage of jellyfin commands', true, DataType.json),
mentions('mentions', 'Monitors messages for mention abuse', false);
mentions('mentions', 'Monitors messages for mention abuse', false),
kavita('kavita', 'Allows usage of jellyfin command', true, DataType.json);

/// name of setting
final String name;
Expand Down
60 changes: 60 additions & 0 deletions lib/src/models/kavita.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import 'package:nyxx/nyxx.dart';

class KavitaUserConfig {
static String tableName = 'kavita_user_configs';

final Snowflake userId;
final String authToken;
final String apiKey;
final int kavitaConfigId;

KavitaConfig? config;
int? id;

KavitaUserConfig(
{required this.userId, required this.authToken, required this.apiKey, required this.kavitaConfigId, this.id});

factory KavitaUserConfig.fromDatabaseRow(Map<String, dynamic> row) {
return KavitaUserConfig(
userId: Snowflake.parse(row['user_id']),
authToken: row['auth_token'],
apiKey: row['api_key'],
kavitaConfigId: row['kavita_config_id'],
id: row['id'],
);
}

factory KavitaUserConfig.fromDatabaseRowWithConfig(Map<String, dynamic> row) {
return KavitaUserConfig.fromDatabaseRow(row)..config = KavitaConfig.fromDatabaseRow(row);
}
}

class KavitaConfig {
static String tableName = 'kavita_configs';

final String name;
final String basePath;
final bool isDefault;
final Snowflake parentId;

/// The ID of this config, or `null` if this config has not yet been added to the database.
int? id;

KavitaConfig({
required this.name,
required this.basePath,
required this.isDefault,
required this.parentId,
this.id,
});

factory KavitaConfig.fromDatabaseRow(Map<String, dynamic> row) {
return KavitaConfig(
id: row['id'] as int?,
name: row['name'],
basePath: row['base_path'],
isDefault: row['is_default'] as bool,
parentId: Snowflake.parse(row['parent_id']),
);
}
}
Loading