Skip to content

Commit

Permalink
feature: Kavita (#39)
Browse files Browse the repository at this point in the history
* feature: Kavita

* Kavita login, converts, repository and module improvements

* on conflict on constraint

* Use on conflict in kavita insert

* Kavita search command initial implementation

* Implement kavita read command

* Implement saving the continue point for kavita read command

* Implement new book page

* Add kavita add config command
  • Loading branch information
l7ssha authored Nov 13, 2024
1 parent dbe7565 commit a3593a0
Show file tree
Hide file tree
Showing 17 changed files with 762 additions and 23 deletions.
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

0 comments on commit a3593a0

Please sign in to comment.