Skip to content

Commit d348c86

Browse files
Merge pull request #1041 from Windows10CE/fyi-command
Add shortener for lab.razor.fyi
2 parents 021d31d + a39daad commit d348c86

File tree

3 files changed

+104
-21
lines changed

3 files changed

+104
-21
lines changed

Directory.Build.targets

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@
6767
<PackageReference Update="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.NUnit" Version="1.*" />
6868
<PackageReference Update="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.NUnit" Version="1.*" />
6969
<PackageReference Update="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.9.2" />
70+
71+
<PackageReference Update="protobuf-net" Version="3.2.52" />
72+
73+
<!-- unneeded, after an upgrade to net9.0 or higher -->
74+
<PackageReference Update="Microsoft.Bcl.Memory" Version="9.0.4" />
7075
</ItemGroup>
7176

7277
</Project>

src/Modix.Bot/Modix.Bot.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
<PackageReference Include="Microsoft.Extensions.Http" />
1010
<PackageReference Include="Newtonsoft.Json" />
1111
<PackageReference Include="LZStringCSharp" />
12+
<PackageReference Include="protobuf-net" />
13+
<!-- unneeded, after an upgrade to net9.0 or higher -->
14+
<PackageReference Include="Microsoft.Bcl.Memory" />
1215
</ItemGroup>
1316
<ItemGroup>
1417
<ProjectReference Include="..\Modix.Analyzers\Modix.Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" PrivateAssets="all" SetTargetFramework="TargetFramework=netstandard2.0" />

src/Modix.Bot/Modules/SharpLabModule.cs renamed to src/Modix.Bot/Modules/LabShortenerModule.cs

Lines changed: 96 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,38 @@
11
#nullable enable
22

33
using System;
4+
using System.Buffers.Text;
45
using System.Collections.Immutable;
6+
using System.Diagnostics;
7+
using System.Diagnostics.CodeAnalysis;
8+
using System.IO;
9+
using System.IO.Compression;
510
using System.Text.RegularExpressions;
611
using System.Threading.Tasks;
712
using Discord;
813
using Discord.Interactions;
914
using LZStringCSharp;
1015
using Modix.Bot.Responders.AutoRemoveMessages;
11-
using Modix.Services.AutoRemoveMessage;
1216
using Modix.Services.CommandHelp;
1317
using Modix.Services.Utilities;
18+
using ProtoBuf;
1419

1520
namespace Modix.Bot.Modules
1621
{
17-
[ModuleHelp("SharpLab", "Commands for working with SharpLab.")]
18-
public class SharpLabModule : InteractionModuleBase
22+
[ModuleHelp("LabShortener", "Commands for working with sharplab.io/lab.razor.fyi.")]
23+
public partial class LabShortenerModule : InteractionModuleBase
1924
{
2025
private readonly AutoRemoveMessageService _autoRemoveMessageService;
2126

22-
public SharpLabModule(AutoRemoveMessageService autoRemoveMessageService)
27+
public LabShortenerModule(AutoRemoveMessageService autoRemoveMessageService)
2328
{
2429
_autoRemoveMessageService = autoRemoveMessageService;
2530
}
2631

27-
[SlashCommand("sharplab", "Shortens the provided link.")]
32+
[SlashCommand("shorten", "Shortens the provided link.")]
2833
public async Task LinkAsync(
2934
[Summary(description: "The link to shorten.")]
30-
Uri uri)
35+
Uri uri)
3136
{
3237
var host = uri.Host;
3338

@@ -43,7 +48,15 @@ public async Task LinkAsync(
4348

4449
var urlMarkdown = Format.Url($"{host} (click here)", uri.ToString());
4550

46-
var description = host.Equals("sharplab.io") && TryPrepareSharplabPreview(uri.OriginalString, urlMarkdown.Length + 1, out var preview)
51+
string? preview = null;
52+
var previewSuccess = host switch
53+
{
54+
"sharplab.io" => TryPrepareSharplabPreview(uri.Fragment, urlMarkdown.Length + 1, out preview),
55+
"lab.razor.fyi" => TryPrepareRazorLabPreview(uri.Fragment, urlMarkdown.Length + 1, out preview),
56+
_ => throw new UnreachableException("already checked for other hosts"),
57+
};
58+
59+
var description = previewSuccess
4760
? $"{urlMarkdown}\n{preview}"
4861
: urlMarkdown;
4962

@@ -61,9 +74,9 @@ public async Task LinkAsync(
6174
await _autoRemoveMessageService.RegisterRemovableMessageAsync(Context.User, embed, async e => await FollowupAsync(embed: e.Build()));
6275
}
6376

64-
private static bool TryPrepareSharplabPreview(string url, int markdownLength, out string? preview)
77+
private static bool TryPrepareSharplabPreview(string fragment, int markdownLength, [NotNullWhen(true)] out string? preview)
6578
{
66-
if (!url.Contains("#v2:"))
79+
if (!fragment.StartsWith("#v2:"))
6780
{
6881
preview = null;
6982
return false;
@@ -72,7 +85,7 @@ private static bool TryPrepareSharplabPreview(string url, int markdownLength, ou
7285
try
7386
{
7487
// Decode the compressed code from the URL payload
75-
var base64Text = url.Substring(url.IndexOf("#v2:") + "#v2:".Length);
88+
var base64Text = fragment[4..];
7689
var plainText = LZString.DecompressFromBase64(base64Text);
7790

7891
// Extract the option and get the target language
@@ -87,7 +100,7 @@ private static bool TryPrepareSharplabPreview(string url, int markdownLength, ou
87100
sourceCode = ReplaceTokens(sourceCode, _sharplabCSTokens);
88101

89102
// Strip using directives
90-
sourceCode = Regex.Replace(sourceCode, @"using \w+(?:\.\w+)*;", string.Empty);
103+
sourceCode = RemoveUsings(sourceCode);
91104
}
92105
else if (language is "il")
93106
sourceCode = ReplaceTokens(sourceCode, _sharplabILTokens);
@@ -107,6 +120,47 @@ private static bool TryPrepareSharplabPreview(string url, int markdownLength, ou
107120
}
108121
}
109122

123+
private static bool TryPrepareRazorLabPreview(string fragment, int markdownLength, [NotNullWhen(true)] out string? preview)
124+
{
125+
if (string.IsNullOrWhiteSpace(fragment))
126+
{
127+
preview = null;
128+
return false;
129+
}
130+
131+
try
132+
{
133+
var bytes = Base64Url.DecodeFromChars(fragment.AsSpan(1));
134+
using var deflateStream = new DeflateStream(new MemoryStream(bytes), CompressionMode.Decompress);
135+
136+
var savedState = Serializer.Deserialize<RazorLabSavedState>(deflateStream);
137+
var selectedFile = savedState.Inputs[savedState.SelectedInputIndex];
138+
139+
if (selectedFile.FileExtension != ".cs")
140+
{
141+
preview = null;
142+
return false;
143+
}
144+
145+
var source = RemoveUsings(selectedFile.Text);
146+
147+
var maxPreviewLength = EmbedBuilder.MaxDescriptionLength - (markdownLength + "```cs\n\n```".Length);
148+
149+
preview = FormatUtilities.FormatCodeForEmbed("cs", source, maxPreviewLength);
150+
151+
return !string.IsNullOrWhiteSpace(preview);
152+
}
153+
catch
154+
{
155+
preview = null;
156+
return false;
157+
}
158+
}
159+
160+
[GeneratedRegex(@"using \w+(?:\.\w+)*;")]
161+
private static partial Regex UsingsRegex();
162+
private static string RemoveUsings(string sourceCode) => UsingsRegex().Replace(sourceCode, "");
163+
110164
private static string ReplaceTokens(string sourceCode, ImmutableArray<string> tokens)
111165
{
112166
return Regex.Replace(sourceCode, @"@(\d+|@)", match =>
@@ -119,14 +173,13 @@ private static string ReplaceTokens(string sourceCode, ImmutableArray<string> to
119173
}
120174

121175
private static readonly ImmutableArray<string> _allowedHosts
122-
= ImmutableArray.Create
123-
(
124-
"sharplab.io"
125-
);
176+
= [
177+
"sharplab.io",
178+
"lab.razor.fyi"
179+
];
126180

127181
private static readonly ImmutableArray<string> _sharplabCSTokens
128-
= ImmutableArray.Create(new[]
129-
{
182+
= [
130183
"using",
131184
"System",
132185
"class",
@@ -153,11 +206,10 @@ private static readonly ImmutableArray<string> _sharplabCSTokens
153206
"public static class Program",
154207
"Inspect.Allocations(() =>",
155208
"Inspect.MemoryGraph("
156-
});
209+
];
157210

158211
private static readonly ImmutableArray<string> _sharplabILTokens
159-
= ImmutableArray.Create(new[]
160-
{
212+
= [
161213
"Main ()",
162214
"Program",
163215
"ConsoleApp",
@@ -169,6 +221,29 @@ private static readonly ImmutableArray<string> _sharplabILTokens
169221
"extends System.Object",
170222
".method public hidebysig",
171223
"call void [System.Console]System.Console::WriteLine("
172-
});
224+
];
225+
}
226+
227+
// the below definitions were taken from https://github.com/jjonescz/DotNetLab at commit dedcefec241a1d32fe8a6683ccaa39ff40dc1730
228+
// as such they are licensed under the MIT license in that repo, https://github.com/jjonescz/DotNetLab/blob/dedcefec241a1d32fe8a6683ccaa39ff40dc1730/LICENSE
229+
[ProtoContract]
230+
sealed file record RazorLabInputCode
231+
{
232+
[ProtoMember(1)]
233+
public required string FileName { get; init; }
234+
[ProtoMember(2)]
235+
public required string Text { get; init; }
236+
237+
public string FileExtension => Path.GetExtension(FileName);
238+
}
239+
240+
[ProtoContract]
241+
sealed file record RazorLabSavedState
242+
{
243+
[ProtoMember(1)]
244+
public ImmutableArray<RazorLabInputCode> Inputs { get; init; }
245+
246+
[ProtoMember(8)]
247+
public int SelectedInputIndex { get; init; }
173248
}
174249
}

0 commit comments

Comments
 (0)