diff --git a/CHANGELOG.md b/CHANGELOG.md index fc53505..7fa0c1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.3.0 + +- Implement new `/admin perform-nickname-pooping` and `/system clear-cache` command + ## 3.2.3 - Fix `/reminder list` command diff --git a/bin/running_on_dart.dart b/bin/running_on_dart.dart index efe36c8..11ac077 100644 --- a/bin/running_on_dart.dart +++ b/bin/running_on_dart.dart @@ -1,6 +1,7 @@ import 'package:nyxx/nyxx.dart'; import 'package:nyxx_commands/nyxx_commands.dart'; import 'package:running_on_dart/running_on_dart.dart'; +import 'package:running_on_dart/src/commands/system.dart'; void main() async { // Create nyxx client and nyxx_commands plugin @@ -23,7 +24,8 @@ void main() async { ..addCommand(admin) ..addCommand(settings) ..addCommand(github) - ..addCommand(music); + ..addCommand(music) + ..addCommand(system); // Add our error handler commands.onCommandError.listen(commandErrorHandler); diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 2874808..74a4799 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,7 +1,7 @@ version: '3.9' services: running_on_dart: - image: ghcr.io/nyxx-discord/running_on_dart:3.2.3 + image: ghcr.io/nyxx-discord/running_on_dart:3.3.0 container_name: running_on_dart env_file: - .env diff --git a/lib/src/checks.dart b/lib/src/checks.dart index 7c2c749..3d0678d 100644 --- a/lib/src/checks.dart +++ b/lib/src/checks.dart @@ -1,4 +1,3 @@ -import 'package:nyxx/nyxx.dart'; import 'package:nyxx_commands/nyxx_commands.dart'; import 'package:running_on_dart/src/settings.dart'; diff --git a/lib/src/commands/admin.dart b/lib/src/commands/admin.dart index 3d0f14a..bf51962 100644 --- a/lib/src/commands/admin.dart +++ b/lib/src/commands/admin.dart @@ -1,17 +1,11 @@ import 'package:nyxx/nyxx.dart'; import 'package:nyxx_commands/nyxx_commands.dart'; -import 'package:running_on_dart/src/checks.dart'; import 'package:running_on_dart/src/exception.dart'; +import 'package:running_on_dart/src/services/poop_name.dart'; ChatGroup admin = ChatGroup( 'admin', 'Administrative commands', - checks: [ - administratorCheck, - ], - options: CommandsOptions( - hideOriginalResponse: true, - ), children: [ ChatCommand( 'cleanup', @@ -60,6 +54,47 @@ ChatGroup admin = ChatGroup( await context.respond(MessageBuilder.content('Successfully deleted messages!')); } }), + checks: [PermissionsCheck(PermissionsConstants.manageMessages)], + options: CommandsOptions( + hideOriginalResponse: true, + ), ), + ChatCommand( + "perform-nickname-pooping", + "Perform pooping of usernames in current guild", + id('perform-nickname-pooping', (IChatContext context, [bool dryRun = true, int batchSize = 100]) async { + var nickNamesToRemove = []; + for(final disallowedChar in poopCharacters) { + await for(final member in context.guild!.searchMembersGateway(disallowedChar, limit: batchSize)) { + final nick = await PoopNameService.instance.poopUser(member, dryRun: dryRun); + if (nick != null) { + nickNamesToRemove.add(nick); + } + } + } + + final outPutMessageHeader = "Pooping nicknames" + (dryRun ? "[DRY RUN]" : ""); + var nickString = nickNamesToRemove.where((element) => element.isNotEmpty).join(","); + if (nickString.length > 1950) { + nickString = nickString.substring(0, 1950) + " ..."; + } else if (nickString.isEmpty) { + nickString = "-/-"; + } + + var outputMessage = """ + $outPutMessageHeader:\n + ``` + $nickString + ``` + """; + + await context.respond(MessageBuilder.content(outputMessage)); + }), + checks: [ + GuildCheck.all(), + PermissionsCheck(PermissionsConstants.manageNicknames), + ], + options: CommandOptions(autoAcknowledgeInteractions: true) + ) ], ); diff --git a/lib/src/commands/info.dart b/lib/src/commands/info.dart index 84cdd65..1766d29 100644 --- a/lib/src/commands/info.dart +++ b/lib/src/commands/info.dart @@ -5,7 +5,6 @@ import 'package:nyxx_commands/nyxx_commands.dart'; import 'package:nyxx_interactions/nyxx_interactions.dart'; import 'package:running_on_dart/running_on_dart.dart'; import 'package:running_on_dart/src/util.dart'; -import 'package:time_ago_provider/time_ago_provider.dart'; String getCurrentMemoryString() { final current = (ProcessInfo.currentRss / 1024 / 1024).toStringAsFixed(2); @@ -37,19 +36,19 @@ ChatCommand info = ChatCommand( ..addField(name: 'Cached channels', content: context.client.channels.length, inline: true) ..addField( name: 'Cached voice states', - content: context.client.guilds.values.map((g) => g.voiceStates.length).reduce((value, element) => value + element), + content: context.client.guilds.values.map((g) => g.voiceStates.length).fold(0, (value, element) => value + element), inline: true, ) ..addField(name: 'Shard count', content: (context.client as INyxxWebsocket).shards, inline: true) ..addField( name: 'Cached messages', - content: context.client.channels.values.whereType().map((c) => c.messageCache.length).reduce((value, element) => value + element), + content: context.client.channels.values.whereType().map((c) => c.messageCache.length).fold(0, (value, element) => value + element), inline: true, ) ..addField(name: 'Memory usage (current/RSS)', content: getCurrentMemoryString(), inline: true) - ..addField(name: 'Uptime', content: formatFull(context.client.startTime)) + ..addField(name: 'Uptime', content: TimeStampStyle.relativeTime.format(context.client.startTime)) ..addField( - name: 'Last documentation cache update', content: DocsService.instance.lastUpdate == null ? 'never' : formatFull(DocsService.instance.lastUpdate!)); + name: 'Last documentation cache update', content: DocsService.instance.lastUpdate == null ? 'never' : TimeStampStyle.relativeTime.format(DocsService.instance.lastUpdate!)); await context.respond(ComponentMessageBuilder() ..embeds = [embed] diff --git a/lib/src/commands/system.dart b/lib/src/commands/system.dart new file mode 100644 index 0000000..2732c3d --- /dev/null +++ b/lib/src/commands/system.dart @@ -0,0 +1,26 @@ +import 'package:nyxx/nyxx.dart'; +import 'package:nyxx_commands/nyxx_commands.dart'; +import 'package:running_on_dart/running_on_dart.dart'; +import 'package:running_on_dart/src/checks.dart'; + +ChatGroup system = ChatGroup( + 'system', + 'Commands designed for ROD administrators', + children: [ + ChatCommand( + "clear-cache", + "Clear bot cache", + id("clear-cache", (IChatContext context) async { + context.client.channels.clear(); + context.client.users.clear(); + + await context.respond(MessageBuilder.content("Cache cleared successfully!")); + }) + ) + ], + checks: [ + administratorCheck, + GuildCheck.id(adminGuildId) + ], + options: CommandOptions(hideOriginalResponse: true) +); diff --git a/lib/src/services/poop_name.dart b/lib/src/services/poop_name.dart index 956a4df..2fd355f 100644 --- a/lib/src/services/poop_name.dart +++ b/lib/src/services/poop_name.dart @@ -3,8 +3,9 @@ import 'package:nyxx/nyxx.dart'; import 'package:running_on_dart/running_on_dart.dart'; import 'package:running_on_dart/src/models/guild_settings.dart'; -const _poopEmoji = "💩"; -final _poopRegexp = RegExp(r"[!#@^%&-*\.+']"); +const poopEmoji = "💩"; +final poopCharacters = ['!', '#', '@', '^', '%', '&', '-', '*', '.' '+', '\'']; +final poopRegexp = RegExp("[${poopCharacters.join()}]"); class PoopNameService { static PoopNameService get instance => _instance ?? (throw Exception('PoopNameService must be initialised with PoopNameService.init')); @@ -27,11 +28,22 @@ class PoopNameService { return; } + poopUser(member); + } + + /// Returns null if user should be pooped. Returns pooped user username otherwise + Future poopUser(IMember member, {bool dryRun = false}) async { final memberUser = await member.user.getOrDownload(); - if ((member.nickname ?? memberUser.username).startsWith(_poopRegexp)) { - _logger.fine("Changing ${member.id} (${member.nickname ?? memberUser.username})'s nickname to poop emoji"); + final memberName = member.nickname ?? memberUser.username; + if (!memberName.startsWith(poopRegexp)) { + return null; + } - await member.edit(builder: MemberBuilder()..nick = _poopEmoji); + if(!dryRun) { + _logger.fine("Changing ${member.id} ($memberName) nickname to poop emoji"); + await member.edit(builder: MemberBuilder()..nick = poopEmoji); } + + return memberName; } } diff --git a/lib/src/settings.dart b/lib/src/settings.dart index dba3a9c..8c36b7b 100644 --- a/lib/src/settings.dart +++ b/lib/src/settings.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:nyxx/nyxx.dart'; -String get version => '3.2.3'; +String get version => '3.3.0'; /// Get a [String] from an environment variable, throwing an exception if it is not set. /// @@ -25,6 +25,8 @@ final bool intentFeaturesEnabled = getEnvBool('ROD_INTENT_FEATURES_ENABLE'); /// The prefix to use for text commands for this instance. final String prefix = getEnv('ROD_PREFIX'); +final Snowflake adminGuildId = Snowflake(getEnv('ROD_ADMIN_GUILD')); + /// The IDs of the users that are allowed to use administrator commands final List adminIds = getEnv('ROD_ADMIN_IDS').split(RegExp(r'\s+')).map(Snowflake.new).toList(); @@ -88,7 +90,7 @@ bool useSSL = getEnvBool('LAVALINK_USE_SSL', false); final int _baseIntents = GatewayIntents.directMessages | GatewayIntents.guilds | GatewayIntents.guildVoiceState; /// Privileged intents that can be enabled to add additional features to Running on Dart. -final int _privilegedIntents = _baseIntents | GatewayIntents.guildMessages | GatewayIntents.guildMembers; +final int _privilegedIntents = _baseIntents | GatewayIntents.guildMessages | GatewayIntents.guildMembers | GatewayIntents.messageContent; /// The intents to use for this instance. final int intents = intentFeaturesEnabled ? _privilegedIntents : _baseIntents; diff --git a/pubspec.yaml b/pubspec.yaml index b0659f8..05dc94a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: running_on_dart -version: 3.2.3 +version: 3.3.0 description: Discord Bot for nyxx development homepage: https://github.com/nyxx-discord/running_on_dart repository: https://github.com/nyxx-discord/running_on_dart @@ -30,7 +30,6 @@ dependencies: prometheus_client_shelf: ^1.0.0 shelf: ^1.3.0 shelf_router: ^1.1.2 - time_ago_provider: ^4.1.2 dev_dependencies: lints: ^1.0.1