Skip to content

Commit 7e9eb0e

Browse files
committed
add labels command
1 parent b8f6417 commit 7e9eb0e

File tree

15 files changed

+571
-50
lines changed

15 files changed

+571
-50
lines changed

src/.config/dotnet-tools.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"isRoot": true,
44
"tools": {
55
"dotnet-ef": {
6-
"version": "9.0.6",
6+
"version": "9.0.8",
77
"commands": [
88
"dotnet-ef"
99
],

src/EidolonicBot.Bot/Command.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,17 @@ public enum Command {
4646
[CommandArg("full", "(optional) show full address", "list")]
4747
[CommandArg("min_delta", "(optional) minimum balance delta for notification", "add address", "edit address")]
4848
[CommandArg("label", "(optional) subscription label without spaces", "add address min_delta", "edit address min_delta")]
49-
Subscription
49+
Subscription,
50+
51+
[Command(
52+
"/label",
53+
Description = "Get or control list of labels",
54+
IsBotInitCommand = true)]
55+
[CommandArg("list", "show labels for this chat")]
56+
[CommandArg("assign", "assign label for address")]
57+
[CommandArg("unassign", "unassign label for address")]
58+
[CommandArg("address", "account address", "assign", "unassign")]
59+
[CommandArg("full", "(optional) show full address", "list")]
60+
[CommandArg("label", "label without spaces", "assign address")]
61+
Label
5062
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
namespace EidolonicBot.Events.BotCommandReceivedConsumers;
2+
3+
public class LabelBotCommandReceivedConsumer(
4+
ITelegramBotClient botClient,
5+
IMemoryCache memoryCache,
6+
AppDbContext db,
7+
ILinkFormatter linkFormatter
8+
)
9+
: BotCommandReceivedConsumerBase(
10+
Command.Label, botClient,
11+
memoryCache) {
12+
private readonly string[] _adminActions = ["add", "edit", "remove"];
13+
14+
protected override async Task<string?> ConsumeAndGetReply(string[] args, Message message, long chatId,
15+
int messageThreadId, bool isAdmin,
16+
CancellationToken cancellationToken) {
17+
return args switch {
18+
[{ } action, ..]
19+
when _adminActions.Contains(action) && !isAdmin => "Only chat admin can control labels"
20+
.ToEscapedMarkdownV2(),
21+
22+
["assign", { } address, { } label] when RegexList.TvmAddressRegex().IsMatch(address) =>
23+
await Assign(address, chatId, messageThreadId, label, cancellationToken),
24+
25+
["unassign", { } address] when RegexList.TvmAddressRegex().IsMatch(address) =>
26+
await Unassign(address, chatId, messageThreadId, cancellationToken),
27+
28+
["list", ..] => await GetLabelList(
29+
chatId, messageThreadId,
30+
args is [_, "full", ..] or [_, "true", ..],
31+
cancellationToken),
32+
33+
_ => CommandHelpers.HelpByCommand[Command.Label]
34+
};
35+
}
36+
37+
private async Task<string?> GetLabelList(long chatId, int messageThreadId, bool full,
38+
CancellationToken cancellationToken) {
39+
var labelsByChat = await db.LabelByChat
40+
.Where(s => s.ChatId == chatId && s.MessageThreadId == messageThreadId)
41+
.ToArrayAsync(cancellationToken: cancellationToken);
42+
43+
if (labelsByChat.Length == 0) {
44+
return "Assign your first label with\n" +
45+
" `/label assign `address label";
46+
}
47+
48+
var labelsString = labelsByChat.Select(l => full
49+
? $@"`{l.Address}`` \| ``{l.Label.ToEscapedMarkdownV2()}`"
50+
: $@"{linkFormatter.GetAddressLink(l.Address)} \| {l.Label.ToEscapedMarkdownV2()}"
51+
);
52+
53+
return "Address \\| Label\n" +
54+
$"{string.Join('\n', labelsString)}";
55+
}
56+
57+
private async Task<string> Assign(string address, long chatId, int messageThreadId, string label, CancellationToken cancellationToken) {
58+
var labelByChat = await db.LabelByChat.FindAsync([chatId, messageThreadId, address], cancellationToken);
59+
60+
if (labelByChat is null) {
61+
await db.LabelByChat.AddAsync(
62+
new LabelByChat {
63+
ChatId = chatId,
64+
MessageThreadId = messageThreadId,
65+
Address = address,
66+
Label = label
67+
},
68+
cancellationToken);
69+
}
70+
else {
71+
labelByChat.Label = label;
72+
}
73+
74+
var savedEntries = await db.SaveChangesAsync(cancellationToken);
75+
76+
return savedEntries > 0
77+
? $"{label} updated for {linkFormatter.GetAddressLink(address)}"
78+
: $"{label} was already assigned to {linkFormatter.GetAddressLink(address)} earlier";
79+
}
80+
81+
private async Task<string> Unassign(string address, long chatId, int messageThreadId, CancellationToken cancellationToken) {
82+
var labelByChat = await db.LabelByChat.FindAsync([chatId, messageThreadId, address], cancellationToken);
83+
if (labelByChat is null) {
84+
return "Label not found";
85+
}
86+
db.LabelByChat.Remove(labelByChat);
87+
await db.SaveChangesAsync(cancellationToken);
88+
return $"Label was unassigned for {linkFormatter.GetAddressLink(address)}";
89+
}
90+
}

src/EidolonicBot.Bot/Events/BotCommandReceivedConsumers/SubscriptionBotCommandReceivedConsumer.cs

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,29 @@ private static decimal GetMinDeltaByArgs(IReadOnlyList<string> args) {
5656
var subscription = await db.Subscription.SingleAsync(s => s.Address == address, cancellationToken);
5757

5858
var subscriptionByChat = await db.SubscriptionByChat.FindAsync(
59-
[chatId, messageThreadId, subscription.Id, cancellationToken],
59+
[chatId, messageThreadId, subscription.Id],
6060
cancellationToken);
6161

6262
if (subscriptionByChat is null) {
6363
return "Subscription wasn't found\\. Add new one first";
6464
}
6565

6666
subscriptionByChat.MinDelta = minDelta;
67-
subscriptionByChat.Label = label;
67+
68+
if (!string.IsNullOrWhiteSpace(label)) {
69+
var labelByChat = await db.LabelByChat.FindAsync([chatId, messageThreadId, address], cancellationToken);
70+
if (labelByChat is null) {
71+
await db.LabelByChat.AddAsync(new LabelByChat {
72+
ChatId = chatId,
73+
MessageThreadId = messageThreadId,
74+
Address = address,
75+
Label = label
76+
}, cancellationToken);
77+
}
78+
else {
79+
labelByChat.Label = label;
80+
}
81+
}
6882

6983
var savedEntries = await db.SaveChangesAsync(cancellationToken);
7084

@@ -75,18 +89,33 @@ private static decimal GetMinDeltaByArgs(IReadOnlyList<string> args) {
7589

7690
private async Task<string?> GetSubscriptionList(long chatId, int messageThreadId, bool full,
7791
CancellationToken cancellationToken) {
78-
var subscriptionStrings = (await db.SubscriptionByChat
79-
.Where(s => s.ChatId == chatId && s.MessageThreadId == messageThreadId)
80-
.Select(s => new {
81-
s.Subscription.Address,
82-
MinDeltaStr = s.MinDelta.ToEvers(),
83-
s.Label
84-
})
85-
.ToArrayAsync(cancellationToken))
86-
.Select(s => full
87-
? $@"`{s.Address}`` \| ``{s.MinDeltaStr}`` \| ``{s.Label?.ToEscapedMarkdownV2()}`"
88-
: $@"{linkFormatter.GetAddressLink(s.Address)} \| {s.MinDeltaStr} \| {s.Label?.ToEscapedMarkdownV2()}")
89-
.ToArray();
92+
var labelsByChat = await db.LabelByChat
93+
.Where(s => s.ChatId == chatId && s.MessageThreadId == messageThreadId)
94+
.ToArrayAsync(cancellationToken: cancellationToken);
95+
96+
var subscriptionByChat = await db.SubscriptionByChat
97+
.Where(s => s.ChatId == chatId && s.MessageThreadId == messageThreadId)
98+
.Select(s => new { s.Subscription.Address, s.MinDelta })
99+
.ToArrayAsync(cancellationToken);
100+
101+
var subscriptionStrings =
102+
subscriptionByChat
103+
.GroupJoin(labelsByChat, s => s.Address, l => l.Address,
104+
(s, labels) => new {
105+
s.Address,
106+
s.MinDelta,
107+
Labels = labels
108+
})
109+
.SelectMany(x => x.Labels.DefaultIfEmpty(),
110+
(s, l) => new {
111+
s.Address,
112+
s.MinDelta,
113+
l?.Label
114+
})
115+
.Select(s => full
116+
? $@"`{s.Address}`` \| ``{s.MinDelta.ToEvers()}`` \| ``{s.Label?.ToEscapedMarkdownV2()}`"
117+
: $@"{linkFormatter.GetAddressLink(s.Address)} \| {s.MinDelta.ToEvers()} \| {s.Label?.ToEscapedMarkdownV2()}")
118+
.ToArray();
90119

91120
if (subscriptionStrings.Length == 0) {
92121
return "Get your first subscription with\n" +
@@ -104,22 +133,34 @@ private async Task<string> Subscribe(string address, long chatId, int messageThr
104133
subscription ??= (await db.Subscription.AddAsync(new Subscription { Address = address }, cancellationToken))
105134
.Entity;
106135

107-
var subscriptionByChat = await db.SubscriptionByChat.FindAsync(
108-
[chatId, messageThreadId, subscription.Id, cancellationToken],
109-
cancellationToken);
136+
var subscriptionByChat = await db.SubscriptionByChat.FindAsync([chatId, messageThreadId, subscription.Id], cancellationToken);
110137

111138
if (subscriptionByChat is null) {
112139
await db.SubscriptionByChat.AddAsync(
113140
new SubscriptionByChat {
114141
SubscriptionId = subscription.Id,
115142
ChatId = chatId,
116143
MessageThreadId = messageThreadId,
117-
MinDelta = minDelta,
118-
Label = label
144+
MinDelta = minDelta
119145
},
120146
cancellationToken);
121147
}
122148

149+
if (!string.IsNullOrWhiteSpace(label)) {
150+
var labelByChat = await db.LabelByChat.FindAsync([chatId, messageThreadId, address], cancellationToken);
151+
if (labelByChat is null) {
152+
await db.LabelByChat.AddAsync(new LabelByChat {
153+
ChatId = chatId,
154+
MessageThreadId = messageThreadId,
155+
Address = address,
156+
Label = label
157+
}, cancellationToken);
158+
}
159+
else {
160+
labelByChat.Label = label;
161+
}
162+
}
163+
123164
var savedEntries = await db.SaveChangesAsync(cancellationToken);
124165

125166
await mediator.Send(new ReloadSubscriptionService(), cancellationToken);
@@ -145,9 +186,8 @@ private async Task<string> Unsubscribe(string address, long chatId, int messageT
145186
db.Subscription.Remove(subscription);
146187
break;
147188
default:
148-
var subscriptionByChat = await db.SubscriptionByChat.FindAsync(
149-
[chatId, messageThreadId, subscription.Id, cancellationToken],
150-
cancellationToken) ?? throw new InvalidOperationException();
189+
var subscriptionByChat = await db.SubscriptionByChat.FindAsync([chatId, messageThreadId, subscription.Id], cancellationToken)
190+
?? throw new InvalidOperationException();
151191

152192
db.SubscriptionByChat.Remove(subscriptionByChat);
153193
break;

src/EidolonicBot.Database.Models/AppDbContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ public abstract class AppDbContext(
88
DbContextOptions options
99
) : DbContext(options) {
1010
public DbSet<Subscription> Subscription { get; set; } = null!;
11+
public DbSet<LabelByChat> LabelByChat { get; set; } = null!;
1112
public DbSet<SubscriptionByChat> SubscriptionByChat { get; set; } = null!;
1213
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
namespace EidolonicBot.Models;
4+
5+
[PrimaryKey(nameof(ChatId), nameof(MessageThreadId), nameof(Address))]
6+
[SuppressMessage("ReSharper", "NullableWarningSuppressionIsUsed")]
7+
[SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")]
8+
public class LabelByChat {
9+
public long ChatId { get; set; }
10+
public int MessageThreadId { get; set; }
11+
12+
[Required] [MaxLength(66)] public string Address { get; set; } = null!;
13+
14+
[Required] [MaxLength(100)] public string Label { get; set; } = null!;
15+
}

src/EidolonicBot.Database.Models/Models/SubscriptionByChat.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,4 @@ public class SubscriptionByChat {
1111
public decimal MinDelta { get; set; }
1212
public Guid SubscriptionId { get; set; }
1313
public Subscription Subscription { get; set; } = null!;
14-
15-
[MaxLength(1000)] public string? Label { get; set; }
1614
}

src/EidolonicBot.Database.Postgres/Migrations/20250901153440_MigrateToLabelsDbSet.Designer.cs

Lines changed: 110 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)