Skip to content

Commit 3e1fbc2

Browse files
committed
Added WhatsNew command
1 parent 0cfbf8f commit 3e1fbc2

File tree

8 files changed

+442
-34
lines changed

8 files changed

+442
-34
lines changed

Slack-GPT-Socket.sln.DotSettings.user

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,12 @@
1212
<TestId>NUnit3x::8513CAC6-F4FB-4821-B229-7B2E31E1DA66::net7.0::Slack_GPT_Tests.GptApi.GptClientUserCommandsTests.ResolveParameters_UserCommand_Exists_NotFound_Ok</TestId>
1313
<TestId>NUnit3x::8513CAC6-F4FB-4821-B229-7B2E31E1DA66::net7.0::Slack_GPT_Tests.Handlers.CommandHandlerTests.ExecuteCommand_Ok</TestId>
1414
<TestId>NUnit3x::8513CAC6-F4FB-4821-B229-7B2E31E1DA66::net7.0::Slack_GPT_Tests.Handlers.CommandHandlerTests.HelpCommand_CustomGlobal_Ok</TestId>
15+
<TestId>NUnit3x::8513CAC6-F4FB-4821-B229-7B2E31E1DA66::net7.0::Slack_GPT_Tests.Handlers.CommandHandlerTests.CommandsCommand_List_Ok</TestId>
16+
<TestId>NUnit3x::8513CAC6-F4FB-4821-B229-7B2E31E1DA66::net7.0::Slack_GPT_Tests.Handlers.CommandHandlerTests.CommandsCommand_Add_And_List_Ok</TestId>
17+
<TestId>NUnit3x::8513CAC6-F4FB-4821-B229-7B2E31E1DA66::net7.0::Slack_GPT_Tests.Handlers.CommandHandlerTests.CommandsCommand_AddRemove_And_List_Ok</TestId>
18+
<TestId>NUnit3x::8513CAC6-F4FB-4821-B229-7B2E31E1DA66::net7.0::Slack_GPT_Tests.Handlers.CommandHandlerTests.CommandsCommand_AddRemove_And_List_CannotRemove</TestId>
19+
<TestId>NUnit3x::8513CAC6-F4FB-4821-B229-7B2E31E1DA66::net7.0::Slack_GPT_Tests.Handlers.CommandHandlerTests.CommandsCommand_AddSameCommand_MultipleUsers_CannotRemove</TestId>
20+
<TestId>NUnit3x::8513CAC6-F4FB-4821-B229-7B2E31E1DA66::net7.0::Slack_GPT_Tests.Handlers.CommandHandlerTests.WhatsNewCommand_Ok</TestId>
21+
<TestId>NUnit3x::8513CAC6-F4FB-4821-B229-7B2E31E1DA66::net7.0::Slack_GPT_Tests.Handlers.CommandHandlerTests.WhatsNewCommand_Error</TestId>
1522
</TestAncestor>
1623
&lt;/SessionState&gt;</s:String></wpf:ResourceDictionary>

Slack-GPT-Socket/Settings/GptCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public class GptCommand
1313
public string Command
1414
{
1515
get => _command;
16-
set => _command = value.ToLower();
16+
set => _command = value?.ToLower();
1717
}
1818

1919
/// <summary>

Slack-GPT-Socket/Slack-GPT-Socket.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
<ItemGroup>
1818
<PackageReference Include="LiteDB" Version="5.0.16" />
19+
<PackageReference Include="Octokit" Version="5.0.4" />
1920
<PackageReference Include="OpenAI-DotNet" Version="6.3.2" />
2021
<PackageReference Include="SlackNet" Version="0.10.22" />
2122
<PackageReference Include="SlackNet.AspNetCore" Version="0.10.22" />

Slack-GPT-Socket/SlackHandlers/Command/CommandManager.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public CommandManager(
3030
AddCommandStrategy(new HelpCommandStrategy(gptDefaults, botInfo, customCommands, userCommandDb,
3131
parameterManager));
3232
AddCommandStrategy(new StatusCommandStrategy());
33+
AddCommandStrategy(new CommandsCommandStrategy(userCommandDb));
34+
AddCommandStrategy(new WhatsNewCommandStrategy());
3335
}
3436

3537

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
using System.Text.RegularExpressions;
2+
using Slack_GPT_Socket.Settings;
3+
using Slack_GPT_Socket.Utilities.LiteDB;
4+
using SlackNet.Interaction;
5+
6+
namespace Slack_GPT_Socket.Command;
7+
8+
/// <summary>
9+
/// Custom command to list all commands, allows also for adding and removing commands.
10+
/// </summary>
11+
public class CommandsCommandStrategy : ICommandStrategy
12+
{
13+
private readonly IUserCommandDb _userCommandDb;
14+
15+
public CommandsCommandStrategy(IUserCommandDb userCommandDb)
16+
{
17+
_userCommandDb = userCommandDb;
18+
}
19+
20+
public string Command => "commands";
21+
22+
/// <summary>
23+
/// Execute commands command
24+
/// </summary>
25+
/// <param name="command"></param>
26+
/// <returns></returns>
27+
public async Task<SlashCommandResponse> Execute(SlashCommand command)
28+
{
29+
var restOfCommand = command.Text.Substring(Command.Length).Trim();
30+
var commandName = restOfCommand.Split(" ")[0];
31+
restOfCommand = restOfCommand.Substring(commandName.Length).Trim();
32+
33+
switch (commandName)
34+
{
35+
case "": // List all commands
36+
return ListCommands(command);
37+
case "add": // Add a command
38+
return AddCommand(command, restOfCommand);
39+
case "remove": // Remove a command
40+
return RemoveCommand(command, restOfCommand);
41+
case "help": // Help for this command
42+
return HelpCommand(command, restOfCommand);
43+
}
44+
45+
return CommandStrategyUtils.SlashCommandResponse("Command not found.");
46+
}
47+
48+
/// <summary>
49+
/// List help for commands prompt or details for a specific command
50+
/// </summary>
51+
/// <param name="command"></param>
52+
/// <param name="commandToHelp"></param>
53+
/// <returns></returns>
54+
private SlashCommandResponse HelpCommand(SlashCommand command, string? commandToHelp)
55+
{
56+
57+
if (string.IsNullOrWhiteSpace(commandToHelp))
58+
{
59+
return CommandStrategyUtils.SlashCommandResponse(
60+
"Usage: /commands [add|remove|help] [command] [prompt] [description] [options: -global]\n" +
61+
"Examples:\n" +
62+
"/commands add -test \"This is a test\" \"This is a test command\"\n" +
63+
"/commands remove -test\n" +
64+
"/commands help\n" +
65+
"/commands help -test\n" +
66+
"/commands");
67+
}
68+
else
69+
{
70+
var commandHelp = _userCommandDb.FindCommand(commandToHelp, command.UserId);
71+
return CommandStrategyUtils.SlashCommandResponse(
72+
$"{commandHelp.Command}\n" +
73+
$"\tPrompt: {commandHelp.Prompt}\n" +
74+
$"\tDescription: {commandHelp.Description}\n" +
75+
$"\tIs Global: {(commandHelp.UserId == null)}\n" +
76+
$"\tAs System: {commandHelp.AsSystem}");
77+
}
78+
}
79+
80+
/// <summary>
81+
/// Removes a command
82+
/// </summary>
83+
/// <param name="command"></param>
84+
/// <param name="commandToRemove"></param>
85+
/// <returns></returns>
86+
private SlashCommandResponse RemoveCommand(SlashCommand command, string commandToRemove)
87+
{
88+
var toRemove = _userCommandDb.FindCommand(commandToRemove, command.UserId);
89+
if (toRemove == null) return CommandStrategyUtils.SlashCommandResponse($"Command {commandToRemove} not found.");
90+
_userCommandDb.RemoveCommand(toRemove);
91+
return CommandStrategyUtils.SlashCommandResponse($"Removed command {commandToRemove}.");
92+
}
93+
94+
/// <summary>
95+
/// New command should be in the format "add command prompt description" or "add command prompt"
96+
/// Prompt and Description can have " " in them, so we need to split on " " and then recombine the last two
97+
/// You can escape " " with \ within the prompt and description. Eg: add test "this is a \"test\""
98+
/// </summary>
99+
/// <param name="command"></param>
100+
/// <param name="restOfCommand"></param>
101+
/// <returns></returns>
102+
private SlashCommandResponse AddCommand(SlashCommand command, string restOfCommand)
103+
{
104+
var commandToAdd = restOfCommand;
105+
var isGlobal = false;
106+
var matches = Regex.Matches(commandToAdd,
107+
@"[^\s""']+|""([^""\\]*(?:\\.[^""\\]*)*)""|'([^'\\]*(?:\\.[^'\\]*)*)'");
108+
109+
var splitCommand = matches.Select(m => m.Value).ToArray();
110+
111+
// If there is "-global" parameter after prompt or description, then this is a global command.
112+
// There can be more then one option
113+
var options = splitCommand[1..].Where(c => c.StartsWith("-")).ToArray();
114+
if (options.Contains("-global")) isGlobal = true;
115+
116+
117+
GptUserCommand newCommand = new()
118+
{
119+
Command = splitCommand[0],
120+
Description = splitCommand.Length > 2
121+
? splitCommand[2]
122+
: "",
123+
Prompt = splitCommand.Length > 1
124+
? splitCommand[1]
125+
: "",
126+
UserId = isGlobal
127+
? null
128+
: command.UserId
129+
};
130+
_userCommandDb.AddCommand(newCommand);
131+
return CommandStrategyUtils.SlashCommandResponse(
132+
$"Added command {newCommand.Command} described as {newCommand.Description}\n" +
133+
$"\tPrompt:{newCommand.Prompt}.\n" +
134+
$"\tGlobal: {isGlobal}");
135+
}
136+
137+
/// <summary>
138+
/// List all commands
139+
/// </summary>
140+
/// <param name="command"></param>
141+
/// <returns></returns>
142+
private SlashCommandResponse ListCommands(SlashCommand command)
143+
{
144+
var commands = _userCommandDb.GetAllCommands(command.UserId);
145+
var globalCommands = _userCommandDb.GetAllCommands();
146+
var allCommands = commands.Concat(globalCommands).ToList();
147+
148+
if (!allCommands.Any()) return CommandStrategyUtils.SlashCommandResponse("No commands found.");
149+
150+
var commandList = string.Join("\n",
151+
allCommands.Select(c => $"{c.Command} [{(c.UserId == null ? "Global" : "User")}]"));
152+
return CommandStrategyUtils.SlashCommandResponse(commandList);
153+
}
154+
}

Slack-GPT-Socket/SlackHandlers/Command/ICommandStrategy.cs

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,30 @@
22

33
namespace Slack_GPT_Socket.Command;
44

5+
/// <summary>
6+
/// Interface for a strategy that will handle commands eg: /gpt help
7+
/// </summary>
58
public interface ICommandStrategy
69
{
10+
/// <summary>
11+
/// Command to execute
12+
/// </summary>
713
string Command { get; }
814

15+
/// <summary>
16+
/// Can this command be handled by this strategy?
17+
/// </summary>
18+
/// <param name="command"></param>
19+
/// <returns></returns>
920
bool CanHandle(SlashCommand command)
1021
{
11-
return command.Text.StartsWith(Command);
22+
return command.Text.ToLower().StartsWith(Command.ToLower());
1223
}
1324

25+
/// <summary>
26+
/// Execute the command
27+
/// </summary>
28+
/// <param name="command"></param>
29+
/// <returns></returns>
1430
Task<SlashCommandResponse> Execute(SlashCommand command);
15-
}
16-
17-
public class CommandsCommandStrategy : ICommandStrategy
18-
{
19-
public string Command => "commands";
20-
21-
public bool CanHandle(SlashCommand command)
22-
{
23-
return false;
24-
}
25-
26-
public async Task<SlashCommandResponse> Execute(SlashCommand command)
27-
{
28-
return CommandStrategyUtils.SlashCommandResponse("ok");
29-
}
3031
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using Octokit;
2+
using SlackNet.Interaction;
3+
4+
namespace Slack_GPT_Socket.Command;
5+
6+
public class WhatsNewCommandStrategy : ICommandStrategy
7+
{
8+
private readonly GitHubClient _github;
9+
private readonly string _owner;
10+
private readonly string _repo;
11+
12+
public WhatsNewCommandStrategy()
13+
{
14+
_github = new GitHubClient(new ProductHeaderValue("Prographers"));
15+
_owner = "Prographers";
16+
_repo = "Slack-GPT";
17+
}
18+
19+
public string Command => "whatsnew";
20+
21+
public async Task<SlashCommandResponse> Execute(SlashCommand command)
22+
{
23+
var versionString = command.Text.Substring(8).Trim();
24+
25+
var releases = await _github.Repository.Release.GetAll(_owner, _repo);
26+
var currentVersion = versionString == string.Empty
27+
? Application.VersionString
28+
: versionString;
29+
30+
var latestRelease = releases.FirstOrDefault(r => r.TagName == currentVersion);
31+
32+
if (latestRelease == null)
33+
return CommandStrategyUtils.SlashCommandResponse($"No release found for current version. {currentVersion}");
34+
35+
return CommandStrategyUtils.SlashCommandResponse(latestRelease.Body);
36+
}
37+
}

0 commit comments

Comments
 (0)