Skip to content

Commit 920b42d

Browse files
committed
Implement pinch hitter function for key staff
1 parent dd3ecb7 commit 920b42d

File tree

13 files changed

+234
-3
lines changed

13 files changed

+234
-3
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ node_modules/*
1010
**/obj/
1111
.vs/
1212
.DS_STORE
13-
*.user
13+
*.user
14+
.idea/

Nino.Records/Episode.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public record Episode
1111
public required bool Done;
1212
public required bool ReminderPosted;
1313
public required Staff[] AdditionalStaff;
14+
public required PinchHitter[] PinchHitters;
1415
public required Task[] Tasks;
1516
public DateTimeOffset? Updated;
1617

Nino.Records/PinchHitter.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Newtonsoft.Json;
2+
3+
namespace Nino.Records
4+
{
5+
public record PinchHitter
6+
{
7+
[JsonIgnore] public required ulong UserId;
8+
public required string Abbreviation;
9+
10+
[JsonProperty("UserId")]
11+
[System.Text.Json.Serialization.JsonIgnore]
12+
public string SerializationUserId
13+
{
14+
get => UserId.ToString();
15+
set => UserId = ulong.Parse(value);
16+
}
17+
}
18+
}

Nino/Commands/Episodes/EpisodeAdd.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public async Task<RuntimeResult> Add(
4545
Done = false,
4646
ReminderPosted = false,
4747
AdditionalStaff = [],
48+
PinchHitters = [],
4849
Tasks = project.KeyStaff.Select(ks => new Records.Task { Abbreviation = ks.Role.Abbreviation, Done = false }).ToArray()
4950
};
5051

Nino/Commands/KeyStaff/KeyStaff.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,13 @@ public partial class KeyStaff(InteractionHandler handler, InteractionService com
1010
public InteractionService Commands { get; private set; } = commands;
1111
private readonly InteractionHandler _handler = handler;
1212
private static readonly Logger log = LogManager.GetCurrentClassLogger();
13+
14+
[Group("pinchhitter", "Key Staff pinch hitters")]
15+
public partial class PinchHitterManagement(InteractionHandler handler, InteractionService commands) : InteractionModuleBase<SocketInteractionContext>
16+
{
17+
public InteractionService Commands { get; private set; } = commands;
18+
private readonly InteractionHandler _handler = handler;
19+
private static readonly Logger log = LogManager.GetCurrentClassLogger();
20+
}
1321
}
1422
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using Discord;
2+
using Discord.Interactions;
3+
using Microsoft.Azure.Cosmos;
4+
using Nino.Handlers;
5+
using Nino.Utilities;
6+
using static Localizer.Localizer;
7+
8+
namespace Nino.Commands
9+
{
10+
public partial class KeyStaff
11+
{
12+
public partial class PinchHitterManagement
13+
{
14+
[SlashCommand("remove", "Remove a pinch hitter from an episode")]
15+
public async Task<RuntimeResult> Remove(
16+
[Summary("project", "Project nickname"), Autocomplete(typeof(ProjectAutocompleteHandler))] string alias,
17+
[Summary("episode", "Episode number"), Autocomplete(typeof(EpisodeAutocompleteHandler))] decimal episodeNumber,
18+
[Summary("abbreviation", "Position shorthand"), Autocomplete(typeof(KeyStaffAutocompleteHandler))] string abbreviation
19+
)
20+
{
21+
var interaction = Context.Interaction;
22+
var lng = interaction.UserLocale;
23+
24+
// Sanitize inputs
25+
alias = alias.Trim();
26+
abbreviation = abbreviation.Trim().ToUpperInvariant();
27+
28+
// Verify project and user - Owner or Admin required
29+
var project = Utils.ResolveAlias(alias, interaction);
30+
if (project == null)
31+
return await Response.Fail(T("error.alias.resolutionFailed", lng, alias), interaction);
32+
33+
if (!Utils.VerifyUser(interaction.User.Id, project))
34+
return await Response.Fail(T("error.permissionDenied", lng), interaction);
35+
36+
// Verify episode
37+
var episode = await Getters.GetEpisode(project, episodeNumber);
38+
if (episode == null)
39+
return await Response.Fail(T("error.noSuchEpisode", lng, episodeNumber), interaction);
40+
41+
// Check if position exists
42+
if (project.KeyStaff.All(ks => ks.Role.Abbreviation != abbreviation))
43+
return await Response.Fail(T("error.noSuchTask", lng, abbreviation), interaction);
44+
45+
// Remove from database
46+
TransactionalBatch batch = AzureHelper.Episodes!.CreateTransactionalBatch(partitionKey: AzureHelper.EpisodePartitionKey(episode));
47+
48+
var phIndex = Array.IndexOf(episode.PinchHitters, episode.PinchHitters.SingleOrDefault(k => k.Abbreviation == abbreviation));
49+
if (phIndex < 0)
50+
return await Response.Fail(T("error.noSuchPinchHitter", lng, abbreviation), interaction);
51+
52+
batch.PatchItem(id: episode.Id, [
53+
PatchOperation.Remove($"/pinchHitters/{phIndex}")
54+
]);
55+
await batch.ExecuteAsync();
56+
log.Info($"Removed pinch hitter for {abbreviation} from {episode.Id}");
57+
58+
// Send success embed
59+
var embed = new EmbedBuilder()
60+
.WithTitle(T("title.projectModification", lng))
61+
.WithDescription(T("keyStaff.pinchHitter.removed", lng, episode.Number, abbreviation))
62+
.Build();
63+
await interaction.FollowupAsync(embed: embed);
64+
65+
await Cache.RebuildCacheForProject(episode.ProjectId);
66+
return ExecutionResult.Success;
67+
}
68+
}
69+
}
70+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using Discord;
2+
using Discord.Interactions;
3+
using Discord.WebSocket;
4+
using Microsoft.Azure.Cosmos;
5+
using Nino.Handlers;
6+
using Nino.Records;
7+
using Nino.Utilities;
8+
using static Localizer.Localizer;
9+
10+
namespace Nino.Commands
11+
{
12+
public partial class KeyStaff
13+
{
14+
public partial class PinchHitterManagement
15+
{
16+
[SlashCommand("set", "Set a pinch hitter for an episode")]
17+
public async Task<RuntimeResult> Set(
18+
[Summary("project", "Project nickname"), Autocomplete(typeof(ProjectAutocompleteHandler))] string alias,
19+
[Summary("episode", "Episode number"), Autocomplete(typeof(EpisodeAutocompleteHandler))] decimal episodeNumber,
20+
[Summary("abbreviation", "Position shorthand"), Autocomplete(typeof(KeyStaffAutocompleteHandler))] string abbreviation,
21+
[Summary("member", "Staff member")] SocketUser member
22+
)
23+
{
24+
var interaction = Context.Interaction;
25+
var lng = interaction.UserLocale;
26+
27+
// Sanitize inputs
28+
var memberId = member.Id;
29+
alias = alias.Trim();
30+
abbreviation = abbreviation.Trim().ToUpperInvariant();
31+
32+
// Verify project and user - Owner or Admin required
33+
var project = Utils.ResolveAlias(alias, interaction);
34+
if (project == null)
35+
return await Response.Fail(T("error.alias.resolutionFailed", lng, alias), interaction);
36+
37+
if (!Utils.VerifyUser(interaction.User.Id, project))
38+
return await Response.Fail(T("error.permissionDenied", lng), interaction);
39+
40+
// Verify episode
41+
var episode = await Getters.GetEpisode(project, episodeNumber);
42+
if (episode == null)
43+
return await Response.Fail(T("error.noSuchEpisode", lng, episodeNumber), interaction);
44+
45+
// Check if position exists
46+
if (project.KeyStaff.All(ks => ks.Role.Abbreviation != abbreviation))
47+
return await Response.Fail(T("error.noSuchTask", lng, abbreviation), interaction);
48+
49+
// All good!
50+
var hitter = new PinchHitter
51+
{
52+
UserId = memberId,
53+
Abbreviation = abbreviation
54+
};
55+
56+
// Add to database
57+
TransactionalBatch batch = AzureHelper.Episodes!.CreateTransactionalBatch(partitionKey: AzureHelper.EpisodePartitionKey(episode));
58+
59+
var phIndex = Array.IndexOf(episode.PinchHitters, episode.PinchHitters.SingleOrDefault(k => k.Abbreviation == abbreviation));
60+
batch.PatchItem(id: episode.Id, [
61+
PatchOperation.Set($"/pinchHitters/{(phIndex != -1 ? phIndex : "-")}", hitter)
62+
]);
63+
64+
await batch.ExecuteAsync();
65+
66+
log.Info($"Set {memberId} as pinch hitter for {abbreviation} for {episode.Id}");
67+
68+
// Send success embed
69+
var staffMention = $"<@{memberId}>";
70+
var embed = new EmbedBuilder()
71+
.WithTitle(T("title.projectModification", lng))
72+
.WithDescription(T("keyStaff.pinchHitter.set", lng, staffMention, episode.Number, abbreviation))
73+
.Build();
74+
await interaction.FollowupAsync(embed: embed);
75+
76+
await Cache.RebuildCacheForProject(episode.ProjectId);
77+
return ExecutionResult.Success;
78+
}
79+
}
80+
}
81+
}

Nino/Commands/Project/ProjectCreate.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ public async Task<RuntimeResult> Create(
8282
Done = false,
8383
ReminderPosted = false,
8484
AdditionalStaff = [],
85+
PinchHitters = [],
8586
Tasks = [],
8687
});
8788
}

Nino/Utilities/StaffList.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ public static string GenerateRoster(Project project, Episode episode)
1919
foreach (var ks in project.KeyStaff.Concat(episode.AdditionalStaff).OrderBy(k => k.Role.Weight ?? 1000000))
2020
{
2121
var task = episode.Tasks.First(t => t.Abbreviation == ks.Role.Abbreviation);
22+
var userId = episode.PinchHitters.FirstOrDefault(k => k.Abbreviation == ks.Role.Abbreviation)?.UserId ?? ks.UserId;
23+
2224
if (task.Done)
23-
sb.AppendLine($"~~{task.Abbreviation}~~: <@{ks.UserId}>");
25+
sb.AppendLine($"~~{task.Abbreviation}~~: <@{userId}>");
2426
else
25-
sb.AppendLine($"**{task.Abbreviation}**: <@{ks.UserId}>");
27+
sb.AppendLine($"**{task.Abbreviation}**: <@{userId}>");
2628
}
2729

2830
return sb.ToString();

Nino/Utilities/Utils.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ public static bool VerifyTaskUser(ulong userId, Project project, Episode episode
9696
return true;
9797
if (Cache.GetConfig(project.GuildId)?.AdministratorIds?.Any(a => a == userId) ?? false)
9898
return true;
99+
if (episode.PinchHitters.Any(ph => ph.Abbreviation == abbreviation && ph.UserId == userId))
100+
return true;
99101
if (project.KeyStaff.Concat(episode.AdditionalStaff).Any(ks => ks.Role.Abbreviation == abbreviation && ks.UserId == userId))
100102
return true;
101103
return false;

Nino/i18n/cmd/nino.en-US.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,46 @@
233233
"description": "Full position name"
234234
}
235235
},
236+
"pinchhitter": {
237+
"name": "pinchhitter",
238+
"description": "Key Staff pinch hitters",
239+
"remove": {
240+
"name": "remove",
241+
"description": "Remove a pinch hitter from an episode",
242+
"project": {
243+
"name": "project",
244+
"description": "Project nickname"
245+
},
246+
"episode": {
247+
"name": "episode",
248+
"description": "Episode number"
249+
},
250+
"abbreviation": {
251+
"name": "abbreviation",
252+
"description": "Position shorthand"
253+
}
254+
},
255+
"set": {
256+
"name": "set",
257+
"description": "Set a pinch hitter for an episode",
258+
"project": {
259+
"name": "project",
260+
"description": "Project nickname"
261+
},
262+
"episode": {
263+
"name": "episode",
264+
"description": "Episode number"
265+
},
266+
"abbreviation": {
267+
"name": "abbreviation",
268+
"description": "Position shorthand"
269+
},
270+
"member": {
271+
"name": "member",
272+
"description": "Staff member"
273+
}
274+
}
275+
},
236276
"remove": {
237277
"name": "remove",
238278
"description": "Remove a Key Staff from the whole project",

Nino/i18n/str/en-US.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"error.noSuchConga": "That Conga Participant does not exist!",
7979
"error.noSuchEpisode": "Episode {{episodeNumber|1}} does not exist!",
8080
"error.noSuchObserver": "That observer does not exist!",
81+
"error.noSuchPinchHitter": "There is no pinch hitter set!",
8182
"error.noSuchProject": "That project does not exist!",
8283
"error.noSuchServer": "Server {{server|1}} does not exist!",
8384
"error.noSuchTask": "Task {{abbreviation|1}} does not exist!",
@@ -94,6 +95,8 @@
9495
"error.taskCompleteAllEpisodes": "That task is complete for all episodes!",
9596
"info.resettable": "To reset this value, set a hyphen: `-`",
9697
"keyStaff.added": "Added {{staff|1}} for position {{abbreviation|2}}.",
98+
"keyStaff.pinchHitter.set": "Set {{staff|1}} as pinch hitter for episode {{episode|2}}'s {{abbreviation|3}}.",
99+
"keyStaff.pinchHitter.removed": "Removed the pinch hitter for episode {{episode|1}}'s {{abbreviation|2}}.",
97100
"keyStaff.removed": "Removed position {{abbreviation|1}} from the project.",
98101
"keyStaff.swapped": "Swapped {{staff|1}} in for position {{abbreviation|2}}.",
99102
"keyStaff.weight.updated": "Set weight of position {{abbreviation|1}} to {{weight|2}}.",

Nino/i18n/str/ru.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"error.noSuchConga": "That Conga Participant does not exist!",
7979
"error.noSuchEpisode": "Episode {{episodeNumber|1}} does not exist!",
8080
"error.noSuchObserver": "Такого наблюдателя не существует!",
81+
"error.noSuchPinchHitter": "There is no pinch hitter set!",
8182
"error.noSuchProject": "Такого проекта не существует!",
8283
"error.noSuchServer": "Server {{server|1}} does not exist!",
8384
"error.noSuchTask": "Не существует задачи {{abbreviation|1}}!",
@@ -94,6 +95,8 @@
9495
"error.taskCompleteAllEpisodes": "That task is complete for all episodes!",
9596
"info.resettable": "To reset this value, set a hyphen: `-`",
9697
"keyStaff.added": "{{staff|1}} теперь есть на позиции {{abbreviation|2}}.",
98+
"keyStaff.pinchHitter.set": "Set {{staff|1}} as pinch hitter for episode {{episode|2}}'s {{abbreviation|3}}.",
99+
"keyStaff.pinchHitter.removed": "Removed the pinch hitter for episode {{episode|1}}'s {{abbreviation|2}}.",
97100
"keyStaff.removed": "Позиция {{abbreviation|1}} убрана из проекта.",
98101
"keyStaff.swapped": "{{staff|1}} становится на замену на позицию {{abbreviation|2}}.",
99102
"keyStaff.weight.updated": "Вес позиции {{abbreviation|1}} установлен в {{weight|2}}.",

0 commit comments

Comments
 (0)