C
C#โ€ข9mo ago
kalten

JumpList PublishAOT

Hello, I'm creating an Avalonia app and it support PublishAOT build I would like to add JumpList support to it but I'm not able to make it work after the AOT build. I tried to use Vanara (https://github.com/dahall/Vanara/) but it stop working as soon as set PublishAOT=true with an InvalidProgramException. I also tried to use CsWin32 (https://github.com/microsoft/CsWin32/discussions) hoping to have a more modern com support. But I don't have the knowledge to use this low level api. I tried Avalonia.Win32.JumpLists nuget package too without much more success. Any idea on how to make it work?
GitHub
GitHub - dahall/Vanara: A set of .NET libraries for Windows impleme...
A set of .NET libraries for Windows implementing PInvoke calls to many native Windows APIs with supporting wrappers. - dahall/Vanara
GitHub
microsoft CsWin32 ยท Discussions
Explore the GitHub Discussions forum for microsoft CsWin32. Discuss code, ask questions & collaborate with the developer community.
75 Replies
reflectronic
reflectronicโ€ข9mo ago
you will have to write the bindings to the COM interfaces yourself for the time being i think what you'll need to do is go to Vanara (or whichever libary you prefer), go look for the COM interfaces that define the jump list API (the ComImport ones), copy them into your project, and convert them to https://learn.microsoft.com/en-us/dotnet/standard/native-interop/comwrappers-source-generation. there is an auto-fixer in Visual Studio that can do this for you sometimes you can probably copy the C# wrapper glue too, and just have them work over the source-generated COM interfaces it is not trivial if you are not really experienced with this stuff, but i wouldn't say it's difficult
kalten
kaltenโ€ข9mo ago
thx Hello again, @reflectronic , I tried what you suggested but without too much success. I use the comwrappers source gen and reuse code from Vanara, but I have to update the interface definition to remove [MarshalAsAttribute] and object here and there. The issue is that I don't really know what to replace them with. They are lot of sample available but they all use the unsupported com "syntax". I was able to create COM object from their GUID cslid/iid, cast it to my com interface, call method on it. But even if it does not crash, I don't know if I'm doing it right. Do you know where I can find some documentation on how to translate C++ header to comwrapper compatible c# syntax ? In the meantime I created a small launcher app. 60 loc, but 170 Mo as single file ๐Ÿ˜ญ But at least it work.
reflectronic
reflectronicโ€ข9mo ago
for the [MarshalAs(UnmanagedType.IUnknown)] you just need to replace it with [MarshalAs(UnmanagedType.Interface)] they are the same, it's just that the source generator only recognizes one of them if there are other issues i can figure out what the equivalent should be
kalten
kaltenโ€ข9mo ago
Thank you very much. I will try. (I was not expecting you to answer that fast)
reflectronic
reflectronicโ€ข9mo ago
yeah i looked at it yesterday and i noticed that issue but i forgot to say anything about it sorry i don't know if there are other issues though i didn't look that deep
kalten
kaltenโ€ข9mo ago
I have this interface IObjectArray "92CA9DCD-5622-4bba-A805-5E9F541BD8C9" Defined as is in vanara
public partial interface IObjectArray
{
/// <summary>Provides a count of the objects in the collection.</summary>
/// <returns>The number of objects in the collection.</returns>
uint GetCount();

/// <summary>
/// Provides a pointer to a specified object's interface. The object and interface are specified by index and interface ID.
/// </summary>
/// <param name="uiIndex">The index of the object</param>
/// <param name="riid">Reference to the desired interface ID.</param>
/// <returns>Receives the interface pointer requested in riid.</returns>
[return: MarshalAs(UnmanagedType.IUnknown)]
object GetAt([In] uint uiIndex, in Guid riid);
}
public partial interface IObjectArray
{
/// <summary>Provides a count of the objects in the collection.</summary>
/// <returns>The number of objects in the collection.</returns>
uint GetCount();

/// <summary>
/// Provides a pointer to a specified object's interface. The object and interface are specified by index and interface ID.
/// </summary>
/// <param name="uiIndex">The index of the object</param>
/// <param name="riid">Reference to the desired interface ID.</param>
/// <returns>Receives the interface pointer requested in riid.</returns>
[return: MarshalAs(UnmanagedType.IUnknown)]
object GetAt([In] uint uiIndex, in Guid riid);
}
And I get error on GetAt and uiIndex
SYSLIB1052 The specified 'MarshalAsAttribute' configuration for the return value of method 'GetAt' is not supported by source-generated COM.If the specified configuration is required, use ComImport instead
And
SYSLIB1051 The '[In]' attribute is only supported on array parameters.By-value parameters are considered read-only by default. The generated source will not handle marshalling of parameter 'uiIndex'
No description
reflectronic
reflectronicโ€ข9mo ago
[In] should be deleted from anything that isn't an array, yeah. it makes no difference otherwise and the IUnknown should be changed to Interface
kalten
kaltenโ€ข9mo ago
Do you mean [return: MarshalAs(UnmanagedType.Interface)]?
reflectronic
reflectronicโ€ข9mo ago
right it's the same fix for all of the UnmanagedType.IUnknown applied to object
kalten
kaltenโ€ข9mo ago
Ok Btw do you know if it exists some tool to inspect com object or more globally debug it?
reflectronic
reflectronicโ€ข9mo ago
what exactly do you mean by debugging it once you have the object all you can really do with it is call methods on it or try casting it to a different interface it may not even be able to tell you what the class is. it is pretty limited
kalten
kaltenโ€ข9mo ago
Ok. I'm felling totally blind with it ๐Ÿ˜…
reflectronic
reflectronicโ€ข9mo ago
is there a specific problem you are having
kalten
kaltenโ€ข9mo ago
Yes, but I try to figure out myself first
reflectronic
reflectronicโ€ข9mo ago
are you having the same problem with the ComImport version? because if you are not, then it's likely that there's another mistake somewhere
kalten
kaltenโ€ข9mo ago
I have now this interface public partial interface IObjectCollection : IObjectArray
[Guid("5632B1A4-E38A-400A-928A-D4CD63230295")]
public partial interface IObjectCollection : IObjectArray
{
/// <summary>Provides a count of the objects in the collection.</summary>
/// <returns>The number of objects in the collection.</returns>
new uint GetCount();

/// <summary>
/// Provides a pointer to a specified object's interface. The object and interface are specified by index and interface ID.
/// </summary>
/// <param name="uiIndex">The index of the object</param>
/// <param name="riid">Reference to the desired interface ID.</param>
/// <returns>Receives the interface pointer requested in riid.</returns>
[return: MarshalAs(UnmanagedType.Interface)]
new object GetAt(uint uiIndex, in Guid riid);

/// <summary>Adds a single object to the collection.</summary>
/// <param name="punk">Pointer to the IUnknown of the object to be added to the collection.</param>
void AddObject([MarshalAs(UnmanagedType.Interface)] object punk);

/// <summary>Adds the objects contained in an IObjectArray to the collection.</summary>
/// <param name="poaSource">Pointer to the IObjectArray whose contents are to be added to the collection.</param>
void AddFromArray(IObjectArray poaSource);

/// <summary>Removes a single, specified object from the collection.</summary>
/// <param name="uiIndex">A pointer to the index of the object within the collection.</param>
void RemoveObjectAt(uint uiIndex);

/// <summary>Removes all objects from the collection.</summary>
void Clear();
}
[Guid("5632B1A4-E38A-400A-928A-D4CD63230295")]
public partial interface IObjectCollection : IObjectArray
{
/// <summary>Provides a count of the objects in the collection.</summary>
/// <returns>The number of objects in the collection.</returns>
new uint GetCount();

/// <summary>
/// Provides a pointer to a specified object's interface. The object and interface are specified by index and interface ID.
/// </summary>
/// <param name="uiIndex">The index of the object</param>
/// <param name="riid">Reference to the desired interface ID.</param>
/// <returns>Receives the interface pointer requested in riid.</returns>
[return: MarshalAs(UnmanagedType.Interface)]
new object GetAt(uint uiIndex, in Guid riid);

/// <summary>Adds a single object to the collection.</summary>
/// <param name="punk">Pointer to the IUnknown of the object to be added to the collection.</param>
void AddObject([MarshalAs(UnmanagedType.Interface)] object punk);

/// <summary>Adds the objects contained in an IObjectArray to the collection.</summary>
/// <param name="poaSource">Pointer to the IObjectArray whose contents are to be added to the collection.</param>
void AddFromArray(IObjectArray poaSource);

/// <summary>Removes a single, specified object from the collection.</summary>
/// <param name="uiIndex">A pointer to the index of the object within the collection.</param>
void RemoveObjectAt(uint uiIndex);

/// <summary>Removes all objects from the collection.</summary>
void Clear();
}
reflectronic
reflectronicโ€ข9mo ago
yup, that's another issue you need to delete all of the new methods, the ones that are inherited from IObjectArray
kalten
kaltenโ€ข9mo ago
But the error is in the generated code
reflectronic
reflectronicโ€ข9mo ago
in ComImport you needed to repeat them in GeneratedComInterface you must not repeat them
kalten
kaltenโ€ข9mo ago
I can just comment the method already in IObjectArray ?
reflectronic
reflectronicโ€ข9mo ago
yeah, they are inherited from IObjectArray
kalten
kaltenโ€ข9mo ago
Ok cool My code look like this right now. It build an run without error.
ComWrappers cw = new StrategyBasedComWrappers();
var cdl = ActivateClass<ICustomDestinationList>(
cw,
new Guid("77f10cf0-3db5-4966-b520-b7c54fd35ed6"),
new Guid("6332debf-87b5-4670-90c0-5e57b408a49e"));

//var IObjectArrayIID = new Guid("92CA9DCD-5622-4bba-A805-5E9F541BD8C9");
//var guid = typeof(IObjectArray).GUID;

var obj = cdl.BeginList(out uint pcMinSlots, typeof(IObjectArray).GUID);

//var rcw = cw.GetOrCreateObjectForComInstance(ppv, CreateObjectFlags.UniqueInstance);
var objArray = (IObjectArray)obj;
var removedObjs = ToArray<object>(cw, objArray);


var exceptions = new System.Collections.Generic.List<Exception>();

var poc = ActivateClass<IObjectCollection>(
cw,
new Guid("2d3468c1-36a7-43b6-ac24-d3f02fd9607a"),
new Guid("5632b1a4-e38a-400a-928a-d4cd63230295"));

var count = poc.GetCount();
ComWrappers cw = new StrategyBasedComWrappers();
var cdl = ActivateClass<ICustomDestinationList>(
cw,
new Guid("77f10cf0-3db5-4966-b520-b7c54fd35ed6"),
new Guid("6332debf-87b5-4670-90c0-5e57b408a49e"));

//var IObjectArrayIID = new Guid("92CA9DCD-5622-4bba-A805-5E9F541BD8C9");
//var guid = typeof(IObjectArray).GUID;

var obj = cdl.BeginList(out uint pcMinSlots, typeof(IObjectArray).GUID);

//var rcw = cw.GetOrCreateObjectForComInstance(ppv, CreateObjectFlags.UniqueInstance);
var objArray = (IObjectArray)obj;
var removedObjs = ToArray<object>(cw, objArray);


var exceptions = new System.Collections.Generic.List<Exception>();

var poc = ActivateClass<IObjectCollection>(
cw,
new Guid("2d3468c1-36a7-43b6-ac24-d3f02fd9607a"),
new Guid("5632b1a4-e38a-400a-928a-d4cd63230295"));

var count = poc.GetCount();
I'm just getting 0 at the end. Now I need to create some IShellLinkW and add them to IObjectCollection I didn't used GeneratedComClass yet. I use this (from msdn doc)
[DllImport("Ole32")]
private static extern int CoCreateInstance(
ref Guid rclsid,
IntPtr pUnkOuter,
int dwClsContext,
ref Guid riid,
out IntPtr ppObj);

public static I ActivateClass<I>(ComWrappers cw, Guid clsid, Guid iid)
{
Debug.Assert(iid == typeof(I).GUID);
int hr = CoCreateInstance(ref clsid, IntPtr.Zero, /*CLSCTX_INPROC_SERVER*/ 1, ref iid, out IntPtr obj);
if (hr < 0)
{
Marshal.ThrowExceptionForHR(hr);
}
return (I)cw.GetOrCreateObjectForComInstance(obj, CreateObjectFlags.None);
}
[DllImport("Ole32")]
private static extern int CoCreateInstance(
ref Guid rclsid,
IntPtr pUnkOuter,
int dwClsContext,
ref Guid riid,
out IntPtr ppObj);

public static I ActivateClass<I>(ComWrappers cw, Guid clsid, Guid iid)
{
Debug.Assert(iid == typeof(I).GUID);
int hr = CoCreateInstance(ref clsid, IntPtr.Zero, /*CLSCTX_INPROC_SERVER*/ 1, ref iid, out IntPtr obj);
if (hr < 0)
{
Marshal.ThrowExceptionForHR(hr);
}
return (I)cw.GetOrCreateObjectForComInstance(obj, CreateObjectFlags.None);
}
I think GeneratedComClass is mean to replace GetOrCreateObjectForComInstance, isn't it?
reflectronic
reflectronicโ€ข9mo ago
GeneratedComClass is for the opposite, it's for GetOrCreateComInterfaceForObject there is nothing like the CoClass where it will do the CoCreateInstance for you. so doing it manually is the right way, yes
kalten
kaltenโ€ข9mo ago
Ok, good and bad new at the same time ^^ So nothing different them for IShellLinkW . Let's try that I have plenty new error
reflectronic
reflectronicโ€ข9mo ago
yes, the StringBuilder will need to be replaced
kalten
kaltenโ€ข9mo ago
Yep, and also PIDL GetIDList();
No description
kalten
kaltenโ€ข9mo ago
Ah no, visual studio add itself the import PIDL from vanara. PIDL seem to be a wrapper of SafeHandle
reflectronic
reflectronicโ€ข9mo ago
all of the StringBuilders should be replaced with
[Out, MarshalAs(UnmanagedType.LPArray)] char[] pszFile
[Out, MarshalAs(UnmanagedType.LPArray)] char[] pszFile
let me see why PIDL does not work
kalten
kaltenโ€ข9mo ago
PIDL is defined in Vanara For the StringBuilder, I had to add this too [assembly:System.Runtime.CompilerServices.DisableRuntimeMarshalling] I also have some HWND I thing I can replace it by IntPtr I also replace some enum by uint
reflectronic
reflectronicโ€ข9mo ago
ok, i see the issue with PIDL you need to change GeneratedComInterface to GeneratedComInterface(Options = ComInterfaceOptions.ComObjectWrapper) to use it (or any SafeHandle)
kalten
kaltenโ€ข9mo ago
But where PIDL should be defined?
reflectronic
reflectronicโ€ข9mo ago
idk, to be honest it looks like a pain in the ass to copy does the code actually use it?
kalten
kaltenโ€ข9mo ago
I'm checking
reflectronic
reflectronicโ€ข9mo ago
because if you don't need it i would just put IntPtr
kalten
kaltenโ€ข9mo ago
Yep does not seem to be used for what I need Ok, I think I have the MVP ready ๐Ÿคž It failed
reflectronic
reflectronicโ€ข9mo ago
where did it fail
kalten
kaltenโ€ข9mo ago
System.Runtime.InteropServices.MarshalDirectiveException: 'Cannot marshal 'parameter #1': Cannot marshal managed types when the runtime marshalling system is disabled.'
in CoCreateInstance
reflectronic
reflectronicโ€ข9mo ago
hm, yes, the P/Invokes will need to be changed too use LibraryImport instead of DllImport it is the same deal as the GeneratedComInterface, it is the new source generator version
kalten
kaltenโ€ข9mo ago
is it still ok to have [assembly:System.Runtime.CompilerServices.DisableRuntimeMarshalling]
reflectronic
reflectronicโ€ข9mo ago
yes, LibraryImport is designed to work with that
kalten
kaltenโ€ข9mo ago
SYSLIB1050 Method 'CoCreateInstance' should be 'static', 'partial', and non-generic when marked with 'LibraryImportAttribute'. P/Invoke source generation will ignore method 'CoCreateInstance'.
reflectronic
reflectronicโ€ข9mo ago
yes, instead of extern, it needs to be partial and the class it's inside of needs to be partial too
kalten
kaltenโ€ข9mo ago
var poc = ActivateClass<IObjectCollection>(
cw,
new Guid("2d3468c1-36a7-43b6-ac24-d3f02fd9607a"),
new Guid("5632b1a4-e38a-400a-928a-d4cd63230295"));

var count = poc.GetCount();


foreach (var profile in files)
{
var psho = CreateTaskListShellObject(cw, profile, "M:\\Install\\MkpKafkaEventViewer-aot\\MkpKafkaEventViewer.exe");
//if (!IsRemoved(psho.Item))
poc.AddObject(psho);
}
//if (cat.Key is null)
cdl.AddUserTasks(poc);
var poc = ActivateClass<IObjectCollection>(
cw,
new Guid("2d3468c1-36a7-43b6-ac24-d3f02fd9607a"),
new Guid("5632b1a4-e38a-400a-928a-d4cd63230295"));

var count = poc.GetCount();


foreach (var profile in files)
{
var psho = CreateTaskListShellObject(cw, profile, "M:\\Install\\MkpKafkaEventViewer-aot\\MkpKafkaEventViewer.exe");
//if (!IsRemoved(psho.Item))
poc.AddObject(psho);
}
//if (cat.Key is null)
cdl.AddUserTasks(poc);
it fail in AddUserTasks
System.ArgumentException: 'Value does not fall within the expected range.'
reflectronic
reflectronicโ€ข9mo ago
can you show the full code
kalten
kaltenโ€ข9mo ago
Sure, let me remove lot of comments
kalten
kaltenโ€ข9mo ago
I commented the SetTitle in CreateTaskListShellObject But But does not seem to work with varana either. But without error Also I'm not sure about all clsid guid To use the SetTitle I need to add a new interface IPropertyStore And it have some needed methode with that use some STRUCT type
reflectronic
reflectronicโ€ข9mo ago
the SetIconLocation is required according to the documentation
kalten
kaltenโ€ข9mo ago
arf They are not setting it in varana
reflectronic
reflectronicโ€ข9mo ago
hm, i guess it's possible that the documentation is wrong it looks like it says SetTitle is required too, though that one is more believable
kalten
kaltenโ€ข9mo ago
On varana the title is set like this
if (!string.IsNullOrEmpty(Title))
(link as IPropertyStore)?.SetValue(PROPERTYKEY.System.Title, Title);
if (!string.IsNullOrEmpty(Title))
(link as IPropertyStore)?.SetValue(PROPERTYKEY.System.Title, Title);
kalten
kaltenโ€ข9mo ago
reflectronic
reflectronicโ€ข9mo ago
yes, you will need to use that and port all of those interfaces and structs
kalten
kaltenโ€ข9mo ago
GitHub
Vanara/PInvoke/Ole/Ole32/PropIdl.PROPVARIANT.cs at master ยท dahall/...
A set of .NET libraries for Windows implementing PInvoke calls to many native Windows APIs with supporting wrappers. - dahall/Vanara
kalten
kaltenโ€ข9mo ago
If I use Vanara.PInvoke.Ole32.PROPERTYKEY as is it seem ok from the source gen but i have on error for PROVARIANT void SetValue(in Vanara.PInvoke.Ole32.PROPERTYKEY pkey, [In] Vanara.PInvoke.Ole32.PROPVARIANT pv);
reflectronic
reflectronicโ€ข9mo ago
is it mentioning the [In]? that can also be deleted uh, wait
kalten
kaltenโ€ข9mo ago
Yep I tried, same error
reflectronic
reflectronicโ€ข9mo ago
can you paste the error
kalten
kaltenโ€ข9mo ago
No description
reflectronic
reflectronicโ€ข9mo ago
oh, it's a class
kalten
kaltenโ€ข9mo ago
Ah yes
reflectronic
reflectronicโ€ข9mo ago
it should be a struct that also means you need to change [In] to in, don't just delete it
kalten
kaltenโ€ข9mo ago
Better yes
kalten
kaltenโ€ข9mo ago
And for GetValue?
No description
kalten
kaltenโ€ข9mo ago
[In, Out]
reflectronic
reflectronicโ€ข9mo ago
should be ref though, looking at the native API, it looks like it should be out it will work either way in this case, but the out will be more convenient to use
kalten
kaltenโ€ข9mo ago
The PROPVARIANT class from vanara is pretty huge
reflectronic
reflectronicโ€ข9mo ago
yeah it might be best to copy it from somewhere else
kalten
kaltenโ€ข9mo ago
I end up with
using VARTYPE = System.Runtime.InteropServices.VarEnum;
///...
[StructLayout(LayoutKind.Explicit, Pack = 8)]
public struct PROPVARIANT
{
/// <summary>Value type tag.</summary>
[FieldOffset(0)] public VARTYPE vt;

/// <summary>Reserved for future use.</summary>
[FieldOffset(2)] public ushort wReserved1;

/// <summary>Reserved for future use.</summary>
[FieldOffset(4)] public ushort wReserved2;

/// <summary>Reserved for future use.</summary>
[FieldOffset(6)] public ushort wReserved3;

/// <summary>The decimal value when VT_DECIMAL.</summary>
[FieldOffset(0)] internal decimal _decimal;

/// <summary>The raw data pointer.</summary>
[FieldOffset(8)] internal IntPtr _ptr;

/// <summary>The FILETIME when VT_FILETIME.</summary>
[FieldOffset(8)] internal System.Runtime.InteropServices.ComTypes.FILETIME _ft;

/// <summary>The BLOB when VT_BLOB</summary>
[FieldOffset(8)] internal Vanara.PInvoke.Ole32.BLOB _blob;

/// <summary>The value when a numeric value less than 8 bytes.</summary>
[FieldOffset(8)] internal ulong _ulong;

/// <summary>Initializes a new instance of the <see cref="PROPVARIANT"/> class as VT_EMPTY.</summary>
public PROPVARIANT()
{

}
public PROPVARIANT(string value)
{
ArgumentNullException.ThrowIfNull(value);
vt = VarEnum.VT_LPWSTR;
_ptr = Marshal.StringToCoTaskMemUni(value);
}

public VarEnum VarType { get => (VarEnum)vt; set => vt = (VARTYPE)value; }
}
using VARTYPE = System.Runtime.InteropServices.VarEnum;
///...
[StructLayout(LayoutKind.Explicit, Pack = 8)]
public struct PROPVARIANT
{
/// <summary>Value type tag.</summary>
[FieldOffset(0)] public VARTYPE vt;

/// <summary>Reserved for future use.</summary>
[FieldOffset(2)] public ushort wReserved1;

/// <summary>Reserved for future use.</summary>
[FieldOffset(4)] public ushort wReserved2;

/// <summary>Reserved for future use.</summary>
[FieldOffset(6)] public ushort wReserved3;

/// <summary>The decimal value when VT_DECIMAL.</summary>
[FieldOffset(0)] internal decimal _decimal;

/// <summary>The raw data pointer.</summary>
[FieldOffset(8)] internal IntPtr _ptr;

/// <summary>The FILETIME when VT_FILETIME.</summary>
[FieldOffset(8)] internal System.Runtime.InteropServices.ComTypes.FILETIME _ft;

/// <summary>The BLOB when VT_BLOB</summary>
[FieldOffset(8)] internal Vanara.PInvoke.Ole32.BLOB _blob;

/// <summary>The value when a numeric value less than 8 bytes.</summary>
[FieldOffset(8)] internal ulong _ulong;

/// <summary>Initializes a new instance of the <see cref="PROPVARIANT"/> class as VT_EMPTY.</summary>
public PROPVARIANT()
{

}
public PROPVARIANT(string value)
{
ArgumentNullException.ThrowIfNull(value);
vt = VarEnum.VT_LPWSTR;
_ptr = Marshal.StringToCoTaskMemUni(value);
}

public VarEnum VarType { get => (VarEnum)vt; set => vt = (VARTYPE)value; }
}
from here https://github.com/jlnewton87/Programming/blob/master/C%23/Windows%20API%20Code%20Pack%201.1/source/WindowsAPICodePack/Core/PropertySystem/PropVariant.cs no error
reflectronic
reflectronicโ€ข9mo ago
seems fine though you need to make sure that you Marshal.FreeCoTaskMem once you are done with the variant
kalten
kaltenโ€ข9mo ago
No description
reflectronic
reflectronicโ€ข9mo ago
nice :)
kalten
kaltenโ€ข9mo ago
What a ride! I would never be able to find all of that by myself I really want to say thank you very much I hope one day I will have the same level of knowledge as you. I'm feeling so dumb next to you (even with my 10y of xp in dotnet) I still neet to check is still work with AOT and lot of polish
reflectronic
reflectronicโ€ข9mo ago
well i'm glad i could help :)
kalten
kaltenโ€ข9mo ago
I need to leave, 4am here. And I still need to go to work. Have a good night (or day) AOT work like a charm Just to let you know, I published the code as a gist here https://gist.github.com/latop2604/73bd7d8008e7ed925f5713fda9201ced
Want results from more Discord servers?
Add your server