Skip to content

Commit 9dba89f

Browse files
committed
feat: add auto-mode slash commands
/autovoice, /autoop, /autohalfop (and un- variants) add or remove mask rules for the current network, /autolist prints the configured rules. Registered in the command service for autocomplete and help.
1 parent 819b269 commit 9dba89f

3 files changed

Lines changed: 190 additions & 0 deletions

File tree

lib/features/chat/application/chat_session_controller.dart

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2670,12 +2670,134 @@ class ChatSessionController extends ChangeNotifier {
26702670
case 'disconnect':
26712671
await _ircService.disconnect(rest.isEmpty ? null : rest);
26722672
return;
2673+
case 'autovoice':
2674+
await _handleAutoModeCommand(UserListType.autoVoice, rest, remove: false);
2675+
return;
2676+
case 'unautovoice':
2677+
await _handleAutoModeCommand(UserListType.autoVoice, rest, remove: true);
2678+
return;
2679+
case 'autoop':
2680+
await _handleAutoModeCommand(UserListType.autoOp, rest, remove: false);
2681+
return;
2682+
case 'unautoop':
2683+
await _handleAutoModeCommand(UserListType.autoOp, rest, remove: true);
2684+
return;
2685+
case 'autohalfop':
2686+
await _handleAutoModeCommand(
2687+
UserListType.autoHalfOp,
2688+
rest,
2689+
remove: false,
2690+
);
2691+
return;
2692+
case 'unautohalfop':
2693+
await _handleAutoModeCommand(
2694+
UserListType.autoHalfOp,
2695+
rest,
2696+
remove: true,
2697+
);
2698+
return;
2699+
case 'autolist':
2700+
case 'autolists':
2701+
_handleAutoListCommand();
2702+
return;
26732703
default:
26742704
await _ircService.sendRaw(commandLine);
26752705
return;
26762706
}
26772707
}
26782708

2709+
Future<void> _handleAutoModeCommand(
2710+
UserListType type,
2711+
String rest, {
2712+
required bool remove,
2713+
}) async {
2714+
final tokens = rest
2715+
.trim()
2716+
.split(RegExp(r'\s+'))
2717+
.where((token) => token.isNotEmpty)
2718+
.toList();
2719+
if (tokens.isEmpty) {
2720+
_appendMessage(
2721+
tabId: activeTab.id,
2722+
sender: 'error',
2723+
content:
2724+
'Usage: /${remove ? 'un' : ''}${type.id} <nick|mask> [#chan,#chan]',
2725+
kind: IrcMessageKind.error,
2726+
);
2727+
return;
2728+
}
2729+
final mask = tokens.first;
2730+
final channels = tokens.length > 1
2731+
? tokens[1].split(',').map((c) => c.trim()).where((c) => c.isNotEmpty).toList()
2732+
: const <String>[];
2733+
2734+
if (remove) {
2735+
final normalized = UserListEntry(type: type, mask: mask)
2736+
.normalizedMask
2737+
.toLowerCase();
2738+
final matches = _autoModeEntries
2739+
.where(
2740+
(entry) =>
2741+
entry.type == type &&
2742+
entry.normalizedMask.toLowerCase() == normalized &&
2743+
(entry.network == null || entry.network == network.id),
2744+
)
2745+
.toList();
2746+
for (final match in matches) {
2747+
await removeAutoModeEntry(match);
2748+
}
2749+
_appendMessage(
2750+
tabId: activeTab.id,
2751+
sender: '*',
2752+
content: matches.isEmpty
2753+
? '$mask was not on ${type.label}.'
2754+
: 'Removed $mask from ${type.label}.',
2755+
kind: IrcMessageKind.system,
2756+
);
2757+
return;
2758+
}
2759+
2760+
await addAutoModeEntry(
2761+
UserListEntry(
2762+
type: type,
2763+
mask: mask,
2764+
channels: channels,
2765+
network: network.id,
2766+
),
2767+
);
2768+
_appendMessage(
2769+
tabId: activeTab.id,
2770+
sender: '*',
2771+
content: channels.isEmpty
2772+
? 'Added $mask to ${type.label}.'
2773+
: 'Added $mask to ${type.label} (${channels.join(', ')}).',
2774+
kind: IrcMessageKind.system,
2775+
);
2776+
}
2777+
2778+
void _handleAutoListCommand() {
2779+
if (_autoModeEntries.isEmpty) {
2780+
_appendMessage(
2781+
tabId: activeTab.id,
2782+
sender: '*',
2783+
content: 'No auto-mode rules configured.',
2784+
kind: IrcMessageKind.system,
2785+
);
2786+
return;
2787+
}
2788+
for (final entry in _autoModeEntries) {
2789+
final scope = entry.channels.isEmpty
2790+
? 'all channels'
2791+
: entry.channels.join(', ');
2792+
_appendMessage(
2793+
tabId: activeTab.id,
2794+
sender: '*',
2795+
content: '${entry.type.label}: ${entry.mask} · $scope',
2796+
kind: IrcMessageKind.system,
2797+
);
2798+
}
2799+
}
2800+
26792801
void _handleHelpCommand(String rest) {
26802802
final requested = rest.trim();
26812803
if (requested.isNotEmpty) {

lib/features/chat/application/command_service.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,48 @@ class CommandService {
701701
description: 'Send a command to BotServ',
702702
kind: CommandKind.service,
703703
),
704+
CommandDefinition(
705+
name: 'autovoice',
706+
usage: '/autovoice <nick|mask> [#chan,#chan]',
707+
description: 'Auto-voice matching users on join',
708+
kind: CommandKind.channel,
709+
),
710+
CommandDefinition(
711+
name: 'unautovoice',
712+
usage: '/unautovoice <nick|mask>',
713+
description: 'Remove an auto-voice rule',
714+
kind: CommandKind.channel,
715+
),
716+
CommandDefinition(
717+
name: 'autoop',
718+
usage: '/autoop <nick|mask> [#chan,#chan]',
719+
description: 'Auto-op matching users on join',
720+
kind: CommandKind.channel,
721+
),
722+
CommandDefinition(
723+
name: 'unautoop',
724+
usage: '/unautoop <nick|mask>',
725+
description: 'Remove an auto-op rule',
726+
kind: CommandKind.channel,
727+
),
728+
CommandDefinition(
729+
name: 'autohalfop',
730+
usage: '/autohalfop <nick|mask> [#chan,#chan]',
731+
description: 'Auto-halfop matching users on join',
732+
kind: CommandKind.channel,
733+
),
734+
CommandDefinition(
735+
name: 'unautohalfop',
736+
usage: '/unautohalfop <nick|mask>',
737+
description: 'Remove an auto-halfop rule',
738+
kind: CommandKind.channel,
739+
),
740+
CommandDefinition(
741+
name: 'autolist',
742+
usage: '/autolist',
743+
description: 'List configured auto-mode rules',
744+
kind: CommandKind.local,
745+
),
704746
];
705747

706748
static final Map<String, CommandDefinition> _commandRegistry = {

test/auto_mode_test.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,32 @@ void main() {
9191
controller.dispose();
9292
});
9393

94+
test('slash commands add and remove auto-mode rules', () async {
95+
final (controller, _) = await _connected();
96+
97+
await controller.handleComposerSubmit('/autovoice bob #flutter,#dart');
98+
expect(controller.autoModeEntries, hasLength(1));
99+
final entry = controller.autoModeEntries.first;
100+
expect(entry.type, UserListType.autoVoice);
101+
expect(entry.mask, 'bob');
102+
expect(entry.channels, ['#flutter', '#dart']);
103+
expect(entry.network, 'dbase');
104+
105+
await controller.handleComposerSubmit(r'/autoop *!*@evil.host');
106+
expect(controller.autoModeEntries, hasLength(2));
107+
108+
await controller.handleComposerSubmit('/unautovoice bob');
109+
expect(
110+
controller.autoModeEntries.where(
111+
(e) => e.type == UserListType.autoVoice,
112+
),
113+
isEmpty,
114+
);
115+
expect(controller.autoModeEntries, hasLength(1));
116+
117+
controller.dispose();
118+
});
119+
94120
test('auto-ops take priority and honor channel scope', () async {
95121
final (controller, transport) = await _connected();
96122
transport.emit(':AndroidIRCX!u@h JOIN #ops');

0 commit comments

Comments
 (0)