diff --git a/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor
index 11ba18a..b49358d 100644
--- a/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor
+++ b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor
@@ -5,8 +5,9 @@
@using System.Collections.Frozen
@using LibMatrix.EventTypes
@using LibMatrix.RoomTypes
-<ModalWindow Title="@("Creating many new " + (PolicyTypes.ContainsKey(MappedType??"") ? PolicyTypes[MappedType!].GetFriendlyNamePluralOrNull()?.ToLower() ?? PolicyTypes[MappedType!].Name : "event"))"
- OnCloseClicked="@OnClose" X="60" Y="60" MinWidth="600">
+<ModalWindow
+ Title="@("Creating many new " + (PolicyTypes.ContainsKey(MappedType ?? "") ? PolicyTypes[MappedType!].GetFriendlyNamePluralOrNull()?.ToLower() ?? PolicyTypes[MappedType!].Name : "event"))"
+ OnCloseClicked="@OnClose" X="60" Y="60" MinWidth="600">
<span>Policy type:</span>
<select @bind="@MappedType">
<option>Select a value</option>
@@ -14,25 +15,58 @@
<option value="@type">@mappedType.GetFriendlyName().ToLower()</option>
}
</select><br/>
-
+
<span>Reason:</span>
- <FancyTextBox @bind-Value="@Reason"></FancyTextBox><br/>
-
+ <FancyTextBox @bind-Value="@Reason"></FancyTextBox>
+ <br/>
+
<span>Recommendation:</span>
- <FancyTextBox @bind-Value="@Recommendation"></FancyTextBox><br/>
+ <FancyTextBox @bind-Value="@Recommendation"></FancyTextBox>
+ <br/>
<span>Entities:</span><br/>
- <InputTextArea @bind-Value="@Users" style="width: 500px;"></InputTextArea><br/>
-
-
+ <FancyTextBox Multiline="true" @bind-Value="@Entities"></FancyTextBox>
+ <br/>
+
+
@* <details> *@
@* <summary>JSON data</summary> *@
@* <pre> *@
@* $1$ @PolicyEvent.ToJson(true, true) #1# *@
@* </pre> *@
@* </details> *@
- <LinkButton OnClick="@(() => { OnClose.Invoke(); return Task.CompletedTask; })"> Cancel </LinkButton>
- <LinkButton OnClick="@(() => { _ = Save(); return Task.CompletedTask; })"> Save </LinkButton>
+ @if (!VerifyIntent) {
+ <LinkButton OnClickAsync="@(() => {
+ OnClose.Invoke();
+ return Task.CompletedTask;
+ })"> Cancel
+ </LinkButton>
+ <LinkButton OnClickAsync="@(() => {
+ _ = Save();
+ return Task.CompletedTask;
+ })"> Save
+ </LinkButton>
+ @if (!string.IsNullOrWhiteSpace(Response)) {
+ <pre style="color: red;">@Response</pre>
+ }
+ }
+ else {
+ <b class="blink">WARNING!!!</b>
+ <br/>
+
+ @if (!string.IsNullOrWhiteSpace(Response)) {
+ <pre style="color: red;">@Response</pre>
+ }
+
+ <span>Are you sure you want to do this?</span>
+ <LinkButton Color="#00FF00" OnClick="@(() => {
+ VerifyIntent = false;
+ Response = null;
+ StateHasChanged();
+ })">No
+ </LinkButton>
+ <LinkButton Color="#FF0000" OnClick="@(() => { _ = Save(force: true); })">Yes</LinkButton>
+ }
</ModalWindow>
@@ -47,33 +81,118 @@
[Parameter]
public required GenericRoom Room { get; set; }
- public string Recommendation { get; set; } = "m.ban";
- public string Reason { get; set; } = "spam";
- public string Users { get; set; } = "";
+ private string Recommendation { get; set; } = "m.ban";
+ private string Reason { get; set; } = "spam";
- private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet();
+ private string Entities { get; set; } = "";
+
+ private string? Response {
+ get;
+ set {
+ field = value;
+ StateHasChanged();
+ }
+ }
+
+ private bool VerifyIntent { get; set; }
+
+ private static FrozenSet<Type> KnownPolicyTypes = MatrixEvent.KnownEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet();
private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes
.ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x);
+ private static FrozenSet<string> AllKnownPolicyTypes = KnownPolicyTypes
+ .SelectMany(x => x.GetCustomAttributes<MatrixEventAttribute>().Select(y => y.EventName))
+ .ToFrozenSet();
+
private string? MappedType { get; set; }
- private async Task Save() {
+ private async Task Save(bool force = false) {
+ if (string.IsNullOrWhiteSpace(MappedType)) {
+ Response = "No type selected";
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(Entities)) {
+ Response = "No users selected";
+ return;
+ }
+
+ Console.WriteLine("Saving ---");
+
+ var entities = Entities.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .Select(x => x.Trim())
+ .Distinct()
+ .ToList();
+
+ if (!force && !Validate(entities, PolicyTypes[MappedType])) {
+ List<string> distinctTypes = entities
+ .Select(GuessType)
+ .Where(x => x != null)
+ .Distinct()
+ .Select(x => x!.Name)
+ .ToList();
+
+ VerifyIntent = true;
+ Response = $"Invalid entities. Expected {PolicyTypes[MappedType].Name}, got:\n - " +
+ string.Join("\n - ", distinctTypes);
+ return;
+ }
+
try {
- await DoActualSave();
+ await SaveAll(entities);
}
catch (Exception e) {
- Console.WriteLine($"Failed to save: {e}");
+ Response = $"Failed to save: {e}";
}
}
- private async Task DoActualSave() {
- Console.WriteLine($"Saving ---");
- Console.WriteLine($"Users = {Users}");
- var users = Users.Split("\n").Select(x => x.Trim()).Where(x => x.StartsWith('@')).ToList();
- var tasks = users.Select(x => ExecuteBan(Room, x)).ToList();
- await Task.WhenAll(tasks);
-
+ private bool Validate(List<string> entities, Type expectedType) {
+ return entities.All(x => GuessType(x) == expectedType);
+ }
+
+ private Type? GuessType(string entity) {
+ var sigil = entity[0];
+ return TypesBySigil.GetValueOrDefault(sigil.ToString(), typeof(ServerPolicyRuleEventContent));
+ }
+
+ private Dictionary<string, Type> TypesBySigil = new() {
+ { "@", typeof(UserPolicyRuleEventContent) },
+ { "!", typeof(RoomPolicyRuleEventContent) },
+ { "#", typeof(RoomPolicyRuleEventContent) }
+ };
+
+ private async Task SaveAll(List<string> entities) {
+ await foreach (var evt in Room.GetFullStateAsync()) {
+ if (evt is null
+ || !AllKnownPolicyTypes.Contains(evt.Type)
+ || !evt.TypedContent!.GetType().IsAssignableTo(PolicyTypes[MappedType!])
+ ) continue;
+
+ if (evt.TypedContent is PolicyRuleEventContent content && content.Recommendation == Recommendation && content.Reason == Reason) {
+ if (content.Entity != null && entities.Contains(content.Entity))
+ entities.Remove(content.Entity);
+ }
+ }
+
+ // var tasks = entities.Select(x => ExecuteBan(Room, x)).ToList();
+ // await Task.WhenAll(tasks);
+
+ var events = entities.Select(entity => {
+ var content = Activator.CreateInstance(PolicyTypes[MappedType!]) as PolicyRuleEventContent ?? throw new InvalidOperationException("Failed to create event content");
+ content.Recommendation = Recommendation;
+ content.Reason = Reason;
+ content.Entity = entity;
+ return new MatrixEvent() {
+ Type = MappedType,
+ TypedContent = content,
+ StateKey = content.GetDraupnir2StateKey()
+ };
+ });
+
+ foreach (var chunk in events.Chunk(50))
+ await Room.BulkSendEventsAsync(chunk);
+
OnSaved.Invoke();
}
@@ -81,7 +200,7 @@
bool success = false;
while (!success) {
try {
- var content = Activator.CreateInstance(PolicyTypes[MappedType!]) as PolicyRuleEventContent;
+ var content = Activator.CreateInstance(PolicyTypes[MappedType!]) as PolicyRuleEventContent ?? throw new InvalidOperationException("Failed to create event content");
content.Recommendation = Recommendation;
content.Reason = Reason;
content.Entity = entity;
diff --git a/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor.css b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor.css
new file mode 100644
index 0000000..49ab31b
--- /dev/null
+++ b/MatrixUtils.Web/Shared/PolicyEditorComponents/MassPolicyEditorModal.razor.css
@@ -0,0 +1,15 @@
+.blink {
+ animation: blinker 2s linear infinite;
+}
+
+@keyframes blinker {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor b/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor
index 5819bee..501ca99 100644
--- a/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor
+++ b/MatrixUtils.Web/Shared/PolicyEditorComponents/PolicyEditorModal.razor
@@ -6,7 +6,7 @@
@using System.Collections.Frozen
@using LibMatrix.EventTypes
<ModalWindow Title="@((string.IsNullOrWhiteSpace(PolicyEvent.EventId) ? "Creating new " : "Editing ") + (PolicyEvent.MappedType.GetFriendlyNameOrNull()?.ToLower() ?? "event"))"
- OnCloseClicked="@OnClose" X="60" Y="60" MinWidth="300">
+ OnCloseClickedAsync="@InvokeOnClose" X="60" Y="60" MinWidth="300">
@if (string.IsNullOrWhiteSpace(PolicyEvent.EventId)) {
<span>Policy type:</span>
<select @bind="@MappedType">
@@ -52,7 +52,12 @@
else {
switch (Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType) {
case Type t when t == typeof(string):
- <FancyTextBox Value="@(getter?.Invoke(PolicyData, null) as string)" ValueChanged="@((string e) => { Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}"); setter?.Invoke(PolicyData, [e]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); })"></FancyTextBox>
+ <FancyTextBox Value="@(getter?.Invoke(PolicyData, null) as string)" ValueChanged="@((string e) => {
+ Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}");
+ setter?.Invoke(PolicyData, [e]);
+ PolicyEvent.TypedContent = PolicyData;
+ StateHasChanged();
+ })"></FancyTextBox>
break;
case Type t when t == typeof(DateTime):
if (!isNullable) {
@@ -61,13 +66,22 @@
else {
var value = getter?.Invoke(PolicyData, null) as DateTime?;
if (value is null) {
- <button @onclick="() => { setter?.Invoke(PolicyData, [DateTime.Now]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); }">Add value</button>
+ <button @onclick="() => { setter?.Invoke(PolicyData, [DateTime.Now]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); }">
+ Add value
+ </button>
}
else {
var notNullValue = Nullable.GetValueRefOrDefaultRef(ref value);
Console.WriteLine($"Value: {value?.ToString() ?? "null"}");
- <InputDate TValue="DateTime" ValueExpression="@(() => notNullValue)" ValueChanged="@(e => { Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}"); setter?.Invoke(PolicyData, [e]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); })"></InputDate>
- <button @onclick="() => { setter?.Invoke(PolicyData, [null]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); }">Remove value</button>
+ <InputDate TValue="DateTime" ValueExpression="@(() => notNullValue)" ValueChanged="@(e => {
+ Console.WriteLine($"{prop.Name} ({setter is not null}) -> {e}");
+ setter?.Invoke(PolicyData, [e]);
+ PolicyEvent.TypedContent = PolicyData;
+ StateHasChanged();
+ })"></InputDate>
+ <button @onclick="() => { setter?.Invoke(PolicyData, [null]); PolicyEvent.TypedContent = PolicyData; StateHasChanged(); }">Remove
+ value
+ </button>
}
}
@@ -88,8 +102,8 @@
@PolicyEvent.ToJson(true, true)
</pre>
</details>
- <LinkButton OnClick="@(() => { OnClose.Invoke(); return Task.CompletedTask; })"> Cancel </LinkButton>
- <LinkButton OnClick="@(() => { OnSave.Invoke(PolicyEvent); return Task.CompletedTask; })"> Save </LinkButton>
+ <LinkButton OnClickAsync="@InvokeOnClose">Cancel</LinkButton>
+ <LinkButton OnClickAsync="@InvokeOnSave">Save</LinkButton>
}
else {
<p>Policy data is null</p>
@@ -99,7 +113,7 @@
@code {
[Parameter]
- public StateEventResponse? PolicyEvent {
+ public MatrixEventResponse? PolicyEvent {
get => _policyEvent;
set {
if (value is not null && value != _policyEvent)
@@ -111,19 +125,41 @@
}
[Parameter]
- public required Action OnClose { get; set; }
+ public Action? OnClose { get; set; }
[Parameter]
- public required Action<StateEventResponse> OnSave { get; set; }
+ public Func<Task>? OnCloseAsync { get; set; }
+
+ private async Task InvokeOnClose() {
+ if (OnClose is not null)
+ OnClose.Invoke();
+
+ if (OnCloseAsync is not null)
+ await OnCloseAsync.Invoke();
+ }
+
+ [Parameter]
+ public Action<MatrixEventResponse>? OnSave { get; set; }
+
+ [Parameter]
+ public Func<MatrixEventResponse, Task>? OnSaveAsync { get; set; }
+
+ private async Task InvokeOnSave() {
+ if (OnSave is not null)
+ OnSave.Invoke(PolicyEvent);
+
+ if (OnSaveAsync is not null)
+ await OnSaveAsync.Invoke(PolicyEvent);
+ }
public PolicyRuleEventContent? PolicyData { get; set; }
- private static FrozenSet<Type> KnownPolicyTypes = StateEvent.KnownStateEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet();
+ private static FrozenSet<Type> KnownPolicyTypes = MatrixEvent.KnownEventTypes.Where(x => x.IsAssignableTo(typeof(PolicyRuleEventContent))).ToFrozenSet();
private static Dictionary<string, Type> PolicyTypes = KnownPolicyTypes
.ToDictionary(x => x.GetCustomAttributes<MatrixEventAttribute>().First(y => !string.IsNullOrWhiteSpace(y.EventName)).EventName, x => x);
- private StateEventResponse? _policyEvent;
+ private MatrixEventResponse? _policyEvent;
private string? MappedType {
get => _policyEvent?.Type;
|