Skip to content

Commit

Permalink
Emoji reactions module
Browse files Browse the repository at this point in the history
  • Loading branch information
l7ssha committed Nov 14, 2024
1 parent 1548098 commit 2c07469
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 4 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 4 additions & 1 deletion lib/src/init.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -37,7 +38,8 @@ Future<void> setupContainer(NyxxGateway client) async {
..registerSingleton(() => DocsModule())
..registerSingleton(() => JellyfinModuleV2())
..registerSingleton(() => MentionsMonitoringModule())
..registerSingleton(() => KavitaModule());
..registerSingleton(() => KavitaModule())
..registerSingleton(() => EmojiReactModule());

await Injector.appInstance.get<DatabaseService>().init();
await Injector.appInstance.get<JellyfinModuleV2>().init();
Expand All @@ -49,4 +51,5 @@ Future<void> setupContainer(NyxxGateway client) async {
await Injector.appInstance.get<PoopNameModule>().init();
await Injector.appInstance.get<BotStartDuration>().init();
await Injector.appInstance.get<MentionsMonitoringModule>().init();
await Injector.appInstance.get<EmojiReactModule>().init();
}
9 changes: 7 additions & 2 deletions lib/src/models/feature_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
105 changes: 105 additions & 0 deletions lib/src/modules/emoji_react_module.dart
Original file line number Diff line number Diff line change
@@ -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<String, dynamic> 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<ApplicationEmoji> _emojis;
late Map<Snowflake, EmojiFeatureSetting> _emojiFeatureSettingsCache;

@override
Future<void> 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<void> _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<ApplicationEmoji> _findBuiltinEmojis(String messageContent) =>
_emojis.where((emoji) => messageContent.contains(emoji.name));
(bool, EmojiFeatureSetting?) _fetchSettingForGuild(Snowflake guildId) {
final result = _emojiFeatureSettingsCache[guildId];

return (result != null, result);
}
}
9 changes: 9 additions & 0 deletions lib/src/repository/feature_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Iterable<FeatureSetting>> 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<Iterable<FeatureSetting>> fetchSettingsForGuild(Snowflake guild) async {
final result = await _database.getConnection().execute(Sql.named('''
Expand Down
9 changes: 9 additions & 0 deletions lib/src/services/feature_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ import 'package:running_on_dart/src/repository/feature_settings.dart';
class FeatureSettingsService {
final _featureSettingsRepository = Injector.appInstance.get<FeatureSettingsRepository>();

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<bool> isEnabled(Setting setting, Snowflake guildId) async =>
await _featureSettingsRepository.isEnabled(setting, guildId);
Expand Down
8 changes: 8 additions & 0 deletions lib/src/util/util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ extension FormatShortDurationExtension on Duration {
String formatShort() => toString().split('.').first.padLeft(8, "0");
}

extension ToMapExtension<K, V> on Iterable<MapEntry<K, V>> {
Map<K, V> toMap() => Map.fromEntries(this);
}

extension EmojiToMention on Emoji {
String get mention => "<:$name:${this.id}>";
}

abstract class RequiresInitialization {
Future<void> init();
}
Expand Down

0 comments on commit 2c07469

Please sign in to comment.