diff --git a/Makefile b/Makefile index 7541964..e7ae943 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,6 @@ tests: ## Run unit tests run: ## Run dev project docker compose up --build -fix-project: analyze fix format ## Fix whole project +fix-project: fix analyze format ## Fix whole project check-project: fix-project tests ## Run all checks diff --git a/lib/src/init.dart b/lib/src/init.dart index 666e1cf..6a13543 100644 --- a/lib/src/init.dart +++ b/lib/src/init.dart @@ -2,6 +2,7 @@ import 'package:injector/injector.dart'; import 'package:nyxx/nyxx.dart'; 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/emoji_react_module.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'; @@ -37,7 +38,8 @@ Future setupContainer(NyxxGateway client) async { ..registerSingleton(() => DocsModule()) ..registerSingleton(() => JellyfinModuleV2()) ..registerSingleton(() => MentionsMonitoringModule()) - ..registerSingleton(() => KavitaModule()); + ..registerSingleton(() => KavitaModule()) + ..registerSingleton(() => EmojiReactModule()); await Injector.appInstance.get().init(); await Injector.appInstance.get().init(); @@ -49,4 +51,5 @@ Future setupContainer(NyxxGateway client) async { await Injector.appInstance.get().init(); await Injector.appInstance.get().init(); await Injector.appInstance.get().init(); + await Injector.appInstance.get().init(); } diff --git a/lib/src/models/feature_settings.dart b/lib/src/models/feature_settings.dart index 22872d5..8039ee3 100644 --- a/lib/src/models/feature_settings.dart +++ b/lib/src/models/feature_settings.dart @@ -5,15 +5,20 @@ import 'package:nyxx/nyxx.dart'; enum DataType { channelMention, json, + string, } enum Setting { poopName('poop_name', 'Replace nickname of a member with poop emoji if the member tries to hoist itself', false), 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), + jellyfin('jellyfin', 'Allows usage of jellyfin commands', true, + DataType.json), // {"create_instance_role":"419506523467939853"} mentions('mentions', 'Monitors messages for mention abuse', false), - kavita('kavita', 'Allows usage of jellyfin command', true, DataType.json); + kavita('kavita', 'Allows usage of jellyfin command', true, + DataType.json), // {"create_instance_role":"419506523467939853"} + emojiReact('emoji_react', 'React to predefined words with emojis', true, + DataType.string); //{"use_builtin": true|false, "mode": "react|message", "process_other_bots": true} /// name of setting final String name; diff --git a/lib/src/modules/emoji_react_module.dart b/lib/src/modules/emoji_react_module.dart new file mode 100644 index 0000000..61546fc --- /dev/null +++ b/lib/src/modules/emoji_react_module.dart @@ -0,0 +1,105 @@ +import 'package:collection/collection.dart'; +import 'package:injector/injector.dart'; +import 'package:nyxx/nyxx.dart'; +import 'package:running_on_dart/src/models/feature_settings.dart'; +import 'package:running_on_dart/src/repository/feature_settings.dart'; +import 'package:running_on_dart/src/settings.dart'; +import 'package:running_on_dart/src/util/util.dart'; + +import 'package:nyxx/src/models/emoji.dart'; // TODO: This should be imported + +enum Mode { + react('react'), + message('message'); + + final String name; + + const Mode(this.name); +} + +class EmojiFeatureSetting { + final bool useBuiltin; + final Mode mode; + final bool processOtherBots; + + EmojiFeatureSetting({required this.useBuiltin, required this.mode, required this.processOtherBots}); + + factory EmojiFeatureSetting.fromJson(Map raw) { + return EmojiFeatureSetting( + useBuiltin: raw['use_builtin'] ?? true, + mode: Mode.values.singleWhereOrNull((e) => e.name == raw['mode']) ?? Mode.message, + processOtherBots: raw['process_other_bots'] ?? true, + ); + } +} + +class EmojiReactModule implements RequiresInitialization { + final NyxxGateway _client = Injector.appInstance.get(); + final FeatureSettingsRepository _featureSettingsRepository = Injector.appInstance.get(); + + late Set _emojis; + late Map _emojiFeatureSettingsCache; + + @override + Future init() async { + if (!intentFeaturesEnabled) { + return; + } + + _emojiFeatureSettingsCache = (await _featureSettingsRepository.fetchSettingsForType(Setting.emojiReact)) + .map((setting) => MapEntry(setting.guildId, EmojiFeatureSetting.fromJson(setting.dataAsJson!))) + .toMap(); + _emojis = (await _client.application.emojis.list()) + .toSet(); // TODO: Add ability to reload module (download new emojis in this case) + + _client.onMessageCreate.listen(_handleMessage); + } + + Future _handleMessage(MessageCreateEvent event) async { + if (event.message.author.id == _client.user.id) { + return; + } + + if (event.guildId == null) { + return; + } + + final (enabled, data) = _fetchSettingForGuild(event.guildId!); + if (!enabled) { + return; + } + + if (!data!.processOtherBots && event.message.author is User && (event.message.author as User).isBot) { + return; + } + + final matchingEmojis = [ + if (data.useBuiltin) ..._findBuiltinEmojis(event.message.content.toLowerCase()), + ]; + + if (matchingEmojis.isEmpty) { + return; + } + + switch (data.mode) { + case Mode.react: + for (final emoji in matchingEmojis) { + event.message.react(ReactionBuilder(name: emoji.name, id: emoji.id)); + } + break; + case Mode.message: + final content = matchingEmojis.map((emoji) => emoji.mention).join(' '); + + event.message.channel.sendMessage(MessageBuilder(content: content)); + break; + } + } + + Iterable _findBuiltinEmojis(String messageContent) => + _emojis.where((emoji) => messageContent.contains(emoji.name)); + (bool, EmojiFeatureSetting?) _fetchSettingForGuild(Snowflake guildId) { + final result = _emojiFeatureSettingsCache[guildId]; + + return (result != null, result); + } +} diff --git a/lib/src/repository/feature_settings.dart b/lib/src/repository/feature_settings.dart index a25fc56..30bf6e1 100644 --- a/lib/src/repository/feature_settings.dart +++ b/lib/src/repository/feature_settings.dart @@ -42,6 +42,15 @@ class FeatureSettingsRepository { return result.map((row) => row.toColumnMap()).map(FeatureSetting.fromRow); } + /// Fetch all settings for all guilds from the database. + Future> fetchSettingsForType(Setting setting) async { + final result = await _database.getConnection().execute(Sql.named(''' + SELECT * FROM feature_settings WHERE name = @name; + '''), parameters: {'name': setting.name}); + + return result.map((row) => row.toColumnMap()).map(FeatureSetting.fromRow); + } + /// Fetch all settings for all guilds from the database. Future> fetchSettingsForGuild(Snowflake guild) async { final result = await _database.getConnection().execute(Sql.named(''' diff --git a/lib/src/services/feature_settings.dart b/lib/src/services/feature_settings.dart index 8787e13..d836970 100644 --- a/lib/src/services/feature_settings.dart +++ b/lib/src/services/feature_settings.dart @@ -6,6 +6,15 @@ import 'package:running_on_dart/src/repository/feature_settings.dart'; class FeatureSettingsService { final _featureSettingsRepository = Injector.appInstance.get(); + Future<(bool, FeatureSetting?)> fetchSetting(Setting setting, Snowflake guildId) async { + final result = await _featureSettingsRepository.fetchSetting(setting, guildId); + + return ( + result != null, + result, + ); + } + /// Returns whether a setting is enabled in a particular guild. Future isEnabled(Setting setting, Snowflake guildId) async => await _featureSettingsRepository.isEnabled(setting, guildId); diff --git a/lib/src/util/util.dart b/lib/src/util/util.dart index a4d5d37..eefc37a 100644 --- a/lib/src/util/util.dart +++ b/lib/src/util/util.dart @@ -25,6 +25,14 @@ extension FormatShortDurationExtension on Duration { String formatShort() => toString().split('.').first.padLeft(8, "0"); } +extension ToMapExtension on Iterable> { + Map toMap() => Map.fromEntries(this); +} + +extension EmojiToMention on Emoji { + String get mention => "<:$name:${this.id}>"; +} + abstract class RequiresInitialization { Future init(); }