Skip to content

Commit 2c07469

Browse files
committed
Emoji reactions module
1 parent 1548098 commit 2c07469

File tree

7 files changed

+143
-4
lines changed

7 files changed

+143
-4
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ tests: ## Run unit tests
1616
run: ## Run dev project
1717
docker compose up --build
1818

19-
fix-project: analyze fix format ## Fix whole project
19+
fix-project: fix analyze format ## Fix whole project
2020

2121
check-project: fix-project tests ## Run all checks

lib/src/init.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:injector/injector.dart';
22
import 'package:nyxx/nyxx.dart';
33
import 'package:running_on_dart/src/modules/bot_start_duration.dart';
44
import 'package:running_on_dart/src/modules/docs.dart';
5+
import 'package:running_on_dart/src/modules/emoji_react_module.dart';
56
import 'package:running_on_dart/src/modules/jellyfin.dart';
67
import 'package:running_on_dart/src/modules/join_logs.dart';
78
import 'package:running_on_dart/src/modules/kavita.dart';
@@ -37,7 +38,8 @@ Future<void> setupContainer(NyxxGateway client) async {
3738
..registerSingleton(() => DocsModule())
3839
..registerSingleton(() => JellyfinModuleV2())
3940
..registerSingleton(() => MentionsMonitoringModule())
40-
..registerSingleton(() => KavitaModule());
41+
..registerSingleton(() => KavitaModule())
42+
..registerSingleton(() => EmojiReactModule());
4143

4244
await Injector.appInstance.get<DatabaseService>().init();
4345
await Injector.appInstance.get<JellyfinModuleV2>().init();
@@ -49,4 +51,5 @@ Future<void> setupContainer(NyxxGateway client) async {
4951
await Injector.appInstance.get<PoopNameModule>().init();
5052
await Injector.appInstance.get<BotStartDuration>().init();
5153
await Injector.appInstance.get<MentionsMonitoringModule>().init();
54+
await Injector.appInstance.get<EmojiReactModule>().init();
5255
}

lib/src/models/feature_settings.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@ import 'package:nyxx/nyxx.dart';
55
enum DataType {
66
channelMention,
77
json,
8+
string,
89
}
910

1011
enum Setting {
1112
poopName('poop_name', 'Replace nickname of a member with poop emoji if the member tries to hoist itself', false),
1213
joinLogs('join_logs', 'Logs member join events into specified channel', true, DataType.channelMention),
1314
modLogs('mod_logs', 'Logs administration event into specified channel', true, DataType.channelMention),
14-
jellyfin('jellyfin', 'Allows usage of jellyfin commands', true, DataType.json),
15+
jellyfin('jellyfin', 'Allows usage of jellyfin commands', true,
16+
DataType.json), // {"create_instance_role":"419506523467939853"}
1517
mentions('mentions', 'Monitors messages for mention abuse', false),
16-
kavita('kavita', 'Allows usage of jellyfin command', true, DataType.json);
18+
kavita('kavita', 'Allows usage of jellyfin command', true,
19+
DataType.json), // {"create_instance_role":"419506523467939853"}
20+
emojiReact('emoji_react', 'React to predefined words with emojis', true,
21+
DataType.string); //{"use_builtin": true|false, "mode": "react|message", "process_other_bots": true}
1722

1823
/// name of setting
1924
final String name;
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import 'package:collection/collection.dart';
2+
import 'package:injector/injector.dart';
3+
import 'package:nyxx/nyxx.dart';
4+
import 'package:running_on_dart/src/models/feature_settings.dart';
5+
import 'package:running_on_dart/src/repository/feature_settings.dart';
6+
import 'package:running_on_dart/src/settings.dart';
7+
import 'package:running_on_dart/src/util/util.dart';
8+
9+
import 'package:nyxx/src/models/emoji.dart'; // TODO: This should be imported
10+
11+
enum Mode {
12+
react('react'),
13+
message('message');
14+
15+
final String name;
16+
17+
const Mode(this.name);
18+
}
19+
20+
class EmojiFeatureSetting {
21+
final bool useBuiltin;
22+
final Mode mode;
23+
final bool processOtherBots;
24+
25+
EmojiFeatureSetting({required this.useBuiltin, required this.mode, required this.processOtherBots});
26+
27+
factory EmojiFeatureSetting.fromJson(Map<String, dynamic> raw) {
28+
return EmojiFeatureSetting(
29+
useBuiltin: raw['use_builtin'] ?? true,
30+
mode: Mode.values.singleWhereOrNull((e) => e.name == raw['mode']) ?? Mode.message,
31+
processOtherBots: raw['process_other_bots'] ?? true,
32+
);
33+
}
34+
}
35+
36+
class EmojiReactModule implements RequiresInitialization {
37+
final NyxxGateway _client = Injector.appInstance.get();
38+
final FeatureSettingsRepository _featureSettingsRepository = Injector.appInstance.get();
39+
40+
late Set<ApplicationEmoji> _emojis;
41+
late Map<Snowflake, EmojiFeatureSetting> _emojiFeatureSettingsCache;
42+
43+
@override
44+
Future<void> init() async {
45+
if (!intentFeaturesEnabled) {
46+
return;
47+
}
48+
49+
_emojiFeatureSettingsCache = (await _featureSettingsRepository.fetchSettingsForType(Setting.emojiReact))
50+
.map((setting) => MapEntry(setting.guildId, EmojiFeatureSetting.fromJson(setting.dataAsJson!)))
51+
.toMap();
52+
_emojis = (await _client.application.emojis.list())
53+
.toSet(); // TODO: Add ability to reload module (download new emojis in this case)
54+
55+
_client.onMessageCreate.listen(_handleMessage);
56+
}
57+
58+
Future<void> _handleMessage(MessageCreateEvent event) async {
59+
if (event.message.author.id == _client.user.id) {
60+
return;
61+
}
62+
63+
if (event.guildId == null) {
64+
return;
65+
}
66+
67+
final (enabled, data) = _fetchSettingForGuild(event.guildId!);
68+
if (!enabled) {
69+
return;
70+
}
71+
72+
if (!data!.processOtherBots && event.message.author is User && (event.message.author as User).isBot) {
73+
return;
74+
}
75+
76+
final matchingEmojis = [
77+
if (data.useBuiltin) ..._findBuiltinEmojis(event.message.content.toLowerCase()),
78+
];
79+
80+
if (matchingEmojis.isEmpty) {
81+
return;
82+
}
83+
84+
switch (data.mode) {
85+
case Mode.react:
86+
for (final emoji in matchingEmojis) {
87+
event.message.react(ReactionBuilder(name: emoji.name, id: emoji.id));
88+
}
89+
break;
90+
case Mode.message:
91+
final content = matchingEmojis.map((emoji) => emoji.mention).join(' ');
92+
93+
event.message.channel.sendMessage(MessageBuilder(content: content));
94+
break;
95+
}
96+
}
97+
98+
Iterable<ApplicationEmoji> _findBuiltinEmojis(String messageContent) =>
99+
_emojis.where((emoji) => messageContent.contains(emoji.name));
100+
(bool, EmojiFeatureSetting?) _fetchSettingForGuild(Snowflake guildId) {
101+
final result = _emojiFeatureSettingsCache[guildId];
102+
103+
return (result != null, result);
104+
}
105+
}

lib/src/repository/feature_settings.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ class FeatureSettingsRepository {
4242
return result.map((row) => row.toColumnMap()).map(FeatureSetting.fromRow);
4343
}
4444

45+
/// Fetch all settings for all guilds from the database.
46+
Future<Iterable<FeatureSetting>> fetchSettingsForType(Setting setting) async {
47+
final result = await _database.getConnection().execute(Sql.named('''
48+
SELECT * FROM feature_settings WHERE name = @name;
49+
'''), parameters: {'name': setting.name});
50+
51+
return result.map((row) => row.toColumnMap()).map(FeatureSetting.fromRow);
52+
}
53+
4554
/// Fetch all settings for all guilds from the database.
4655
Future<Iterable<FeatureSetting>> fetchSettingsForGuild(Snowflake guild) async {
4756
final result = await _database.getConnection().execute(Sql.named('''

lib/src/services/feature_settings.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ import 'package:running_on_dart/src/repository/feature_settings.dart';
66
class FeatureSettingsService {
77
final _featureSettingsRepository = Injector.appInstance.get<FeatureSettingsRepository>();
88

9+
Future<(bool, FeatureSetting?)> fetchSetting(Setting setting, Snowflake guildId) async {
10+
final result = await _featureSettingsRepository.fetchSetting(setting, guildId);
11+
12+
return (
13+
result != null,
14+
result,
15+
);
16+
}
17+
918
/// Returns whether a setting is enabled in a particular guild.
1019
Future<bool> isEnabled(Setting setting, Snowflake guildId) async =>
1120
await _featureSettingsRepository.isEnabled(setting, guildId);

lib/src/util/util.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ extension FormatShortDurationExtension on Duration {
2525
String formatShort() => toString().split('.').first.padLeft(8, "0");
2626
}
2727

28+
extension ToMapExtension<K, V> on Iterable<MapEntry<K, V>> {
29+
Map<K, V> toMap() => Map.fromEntries(this);
30+
}
31+
32+
extension EmojiToMention on Emoji {
33+
String get mention => "<:$name:${this.id}>";
34+
}
35+
2836
abstract class RequiresInitialization {
2937
Future<void> init();
3038
}

0 commit comments

Comments
 (0)