diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/CertificateEntry.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/CertificateEntry.cs index 4b8ff66f..fa3cfb45 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/CertificateEntry.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/CertificateEntry.cs @@ -17,7 +17,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings public string AlgorithmType { get; } /// - /// 获取证书内容(CRT/CER 格式,即 -----BEGIN CERTIFICATE----- ... -----END CERTIFICATE) + /// 获取证书内容(CRT/CER PEM 格式,即 -----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----) /// public string Certificate { get; } diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/CertificateManager.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/CertificateManager.cs index f4a9660b..7f3a2a49 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/CertificateManager.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/CertificateManager.cs @@ -11,7 +11,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings public abstract class CertificateManager { /// - /// 获取存储的全部证书。 + /// 获取存储的全部证书实体。 /// /// public abstract IEnumerable AllEntries(); @@ -30,7 +30,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings public abstract CertificateEntry? GetEntry(string serialNumber); /// - /// 移除指定的证书实体。 + /// 根据证书序列号移除证书实体。 /// /// /// diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientEventExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientEventExtensions.cs index 760ed4a6..c2843186 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientEventExtensions.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientEventExtensions.cs @@ -37,7 +37,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work try { if (string.IsNullOrEmpty(client.Credentials.PushEncodingAESKey)) - throw new Exceptions.WechatWorkEventSerializationException("Decrypt event failed, because there is no encoding AES key."); + throw new Exceptions.WechatWorkEventSerializationException("Failed to decrypt event data, because there is no encoding AES key."); InnerEncryptedEvent encryptedEvent = client.JsonSerializer.Deserialize(callbackJson); callbackJson = Utilities.WxMsgCryptor.AESDecrypt(cipherText: encryptedEvent.EncryptedData, encodingAESKey: client.Credentials.PushEncodingAESKey!, out _); @@ -50,7 +50,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work } catch (Exception ex) { - throw new Exceptions.WechatWorkEventSerializationException("Deserialize event failed. Please see the `InnerException` for more details.", ex); + throw new Exceptions.WechatWorkEventSerializationException("Failed to deserialize event data. Please see the inner exception for more details.", ex); } } @@ -63,7 +63,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work try { if (!Utilities.WxMsgCryptor.TryParseXml(callbackXml, out string? encryptedXml)) - throw new Exceptions.WechatWorkEventSerializationException("Decrypt event failed, because of empty encrypted data."); + throw new Exceptions.WechatWorkEventSerializationException("Failed to decrypt event data, because of empty encrypted data."); callbackXml = Utilities.WxMsgCryptor.AESDecrypt(cipherText: encryptedXml!, encodingAESKey: client.Credentials.PushEncodingAESKey!, out _); return Utilities.XmlUtility.Deserialize(callbackXml); @@ -74,7 +74,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work } catch (Exception ex) { - throw new Exceptions.WechatWorkEventSerializationException("Deserialize event failed. Please see the `InnerException` for more details.", ex); + throw new Exceptions.WechatWorkEventSerializationException("Failed to deserialize event data. Please see the inner exception for more details.", ex); } } @@ -144,13 +144,13 @@ namespace SKIT.FlurlHttpClient.Wechat.Work } catch (Exception ex) { - throw new Exceptions.WechatWorkEventSerializationException("Serialize event failed. Please see the `InnerException` for more details.", ex); + throw new Exceptions.WechatWorkEventSerializationException("Failed to serialize event data. Please see the inner exception for more details.", ex); } if (string.IsNullOrEmpty(client.Credentials.PushEncodingAESKey)) - throw new Exceptions.WechatWorkEventSerializationException("Encrypt event failed, because there is no encoding AES key."); + throw new Exceptions.WechatWorkEventSerializationException("Failed to encrypt event data, because there is no encoding AES key."); if (string.IsNullOrEmpty(client.Credentials.PushToken)) - throw new Exceptions.WechatWorkEventSerializationException("Encrypt event failed, because there is no token."); + throw new Exceptions.WechatWorkEventSerializationException("Failed to encrypt event data, because there is no token."); try { @@ -178,7 +178,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work } catch (Exception ex) { - throw new Exceptions.WechatWorkEventSerializationException("Encrypt event failed. Please see the `InnerException` for more details.", ex); + throw new Exceptions.WechatWorkEventSerializationException("Failed to encrypt event data. Please see the inner exception for more details.", ex); } return json; @@ -202,13 +202,13 @@ namespace SKIT.FlurlHttpClient.Wechat.Work } catch (Exception ex) { - throw new Exceptions.WechatWorkEventSerializationException("Serialize event failed. Please see the `InnerException` for more details.", ex); + throw new Exceptions.WechatWorkEventSerializationException("Failed to serialize event data. Please see the inner exception for more details.", ex); } if (string.IsNullOrEmpty(client.Credentials.PushEncodingAESKey)) - throw new Exceptions.WechatWorkEventSerializationException("Encrypt event failed, because there is no encoding AES key."); + throw new Exceptions.WechatWorkEventSerializationException("Failed to encrypt event data, because there is no encoding AES key."); if (string.IsNullOrEmpty(client.Credentials.PushToken)) - throw new Exceptions.WechatWorkEventSerializationException("Encrypt event failed, because there is no token."); + throw new Exceptions.WechatWorkEventSerializationException("Failed to encrypt event data, because there is no token."); try { @@ -222,7 +222,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work } catch (Exception ex) { - throw new Exceptions.WechatWorkEventSerializationException("Encrypt event failed. Please see the `InnerException` for more details.", ex); + throw new Exceptions.WechatWorkEventSerializationException("Failed to encrypt event data. Please see the inner exception for more details.", ex); } return xml; diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/Models/CgibinExternalContact/GroupWelcome/CgibinExternalContactGroupWelcomeTemplateAddRequest.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/Models/CgibinExternalContact/GroupWelcome/CgibinExternalContactGroupWelcomeTemplateAddRequest.cs index ffc701a5..82ee2af9 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Work/Models/CgibinExternalContact/GroupWelcome/CgibinExternalContactGroupWelcomeTemplateAddRequest.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/Models/CgibinExternalContact/GroupWelcome/CgibinExternalContactGroupWelcomeTemplateAddRequest.cs @@ -1,4 +1,4 @@ -namespace SKIT.FlurlHttpClient.Wechat.Work.Models +namespace SKIT.FlurlHttpClient.Wechat.Work.Models { /// /// 表示 [POST] /cgi-bin/externalcontact/group_welcome_template/add 接口的请求。 @@ -47,7 +47,7 @@ public Types.ImageMessage? Image { get; set; } /// - /// 获取或设置图文消息信息。 + /// 获取或设置图文链接消息信息。 /// [Newtonsoft.Json.JsonProperty("link")] [System.Text.Json.Serialization.JsonPropertyName("link")] diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/Models/CgibinExternalContact/GroupWelcome/CgibinExternalContactGroupWelcomeTemplateEditRequest.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/Models/CgibinExternalContact/GroupWelcome/CgibinExternalContactGroupWelcomeTemplateEditRequest.cs index a34aa070..f34ca1ef 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Work/Models/CgibinExternalContact/GroupWelcome/CgibinExternalContactGroupWelcomeTemplateEditRequest.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/Models/CgibinExternalContact/GroupWelcome/CgibinExternalContactGroupWelcomeTemplateEditRequest.cs @@ -1,4 +1,4 @@ -namespace SKIT.FlurlHttpClient.Wechat.Work.Models +namespace SKIT.FlurlHttpClient.Wechat.Work.Models { /// /// 表示 [POST] /cgi-bin/externalcontact/group_welcome_template/edit 接口的请求。 @@ -54,7 +54,7 @@ public Types.ImageMessage? Image { get; set; } /// - /// 获取或设置图文消息信息。 + /// 获取或设置图文链接消息信息。 /// [Newtonsoft.Json.JsonProperty("link")] [System.Text.Json.Serialization.JsonPropertyName("link")] diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/Models/CgibinExternalContact/GroupWelcome/CgibinExternalContactGroupWelcomeTemplateGetResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/Models/CgibinExternalContact/GroupWelcome/CgibinExternalContactGroupWelcomeTemplateGetResponse.cs index 2e2899aa..34ccc853 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Work/Models/CgibinExternalContact/GroupWelcome/CgibinExternalContactGroupWelcomeTemplateGetResponse.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/Models/CgibinExternalContact/GroupWelcome/CgibinExternalContactGroupWelcomeTemplateGetResponse.cs @@ -1,4 +1,4 @@ -namespace SKIT.FlurlHttpClient.Wechat.Work.Models +namespace SKIT.FlurlHttpClient.Wechat.Work.Models { /// /// 表示 [POST] /cgi-bin/externalcontact/group_welcome_template/get 接口的响应。 @@ -47,7 +47,7 @@ public Types.ImageMessage? Image { get; set; } /// - /// 获取或设置图文消息信息。 + /// 获取或设置图文链接消息信息。 /// [Newtonsoft.Json.JsonProperty("link")] [System.Text.Json.Serialization.JsonPropertyName("link")] diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/Models/CgibinKf/CgibinKfSendMessageRequest.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/Models/CgibinKf/CgibinKfSendMessageRequest.cs index e55dd8bc..1401139b 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Work/Models/CgibinKf/CgibinKfSendMessageRequest.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/Models/CgibinKf/CgibinKfSendMessageRequest.cs @@ -260,7 +260,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Models public string Name { get; set; } = string.Empty; /// - /// 获取或设置详情地址。 + /// 获取或设置详细地址。 /// [Newtonsoft.Json.JsonProperty("address")] [System.Text.Json.Serialization.JsonPropertyName("address")] @@ -342,7 +342,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Models public Types.FileMessage? MessageContentForFile { get; set; } /// - /// 获取或设置图文消息信息。 + /// 获取或设置图文链接消息信息。 /// [Newtonsoft.Json.JsonProperty("link")] [System.Text.Json.Serialization.JsonPropertyName("link")] diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/Models/CgibinKf/CgibinKfSyncMessageResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/Models/CgibinKf/CgibinKfSyncMessageResponse.cs index 5638be45..a92aedac 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Work/Models/CgibinKf/CgibinKfSyncMessageResponse.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/Models/CgibinKf/CgibinKfSyncMessageResponse.cs @@ -368,7 +368,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Models public Types.FileMessage? MessageContentForFile { get; set; } /// - /// 获取或设置图文消息信息。 + /// 获取或设置图文链接消息信息。 /// [Newtonsoft.Json.JsonProperty("link")] [System.Text.Json.Serialization.JsonPropertyName("link")] diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/IWechatWorkFinanceClient.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/IWechatWorkFinanceClient.cs new file mode 100644 index 00000000..30c8f3e9 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/IWechatWorkFinanceClient.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance +{ + public interface IWechatWorkFinanceClient : IDisposable + { + /// + /// 异步调用会话内容存档之获取会话记录数据接口。 + /// REF: https://developer.work.weixin.qq.com/document/path/91774 + /// + /// + /// + /// + Task ExecuteGetChatRecordsAsync(Models.GetChatRecordsRequest request, CancellationToken cancellationToken = default); + + /// + /// 异步调用会话内容存档之解密会话记录数据接口。 + /// REF: https://developer.work.weixin.qq.com/document/path/91774 + /// + /// + /// + /// + Task ExecuteDecryptChatRecordAsync(Models.DecryptChatRecordRequest request, CancellationToken cancellationToken = default); + + /// + /// 异步调用会话内容存档之获取媒体文件分片接口。 + /// REF: https://developer.work.weixin.qq.com/document/path/91774 + /// + /// + /// + /// + Task ExecuteGetMediaFileBufferAsync(Models.GetMediaFileBufferRequest request, CancellationToken cancellationToken = default); + + /// + /// 异步调用会话内容存档之获取媒体文件接口。 + /// REF: https://developer.work.weixin.qq.com/document/path/91774 + /// + /// + /// + /// + Task ExecuteGetMediaFileAsync(Models.GetMediaFileRequest request, CancellationToken cancellationToken = default); + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/InteropServices/FinanceDllLinuxPInvoker.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/InteropServices/FinanceDllLinuxPInvoker.cs new file mode 100644 index 00000000..097452e7 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/InteropServices/FinanceDllLinuxPInvoker.cs @@ -0,0 +1,61 @@ +using System; +using System.Runtime.InteropServices; + +namespace SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance.InteropServices +{ + internal static partial class FinanceDllLinuxPInvoker + { + private const string DLL_NAME = "libWeWorkFinanceSdk_C.so"; + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr NewSdk(); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern int Init(IntPtr sdk, string corpId, string secret); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern int GetChatData(IntPtr sdk, long seq, long limit, string proxy, string passwd, long timeout, IntPtr chatData); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern int GetMediaData(IntPtr sdk, string indexBuf, string fileId, string proxy, string passwd, long timeout, IntPtr mediaData); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern int DecryptData(IntPtr sdk, string encryptKey, string encryptMsg, IntPtr msgData); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern void DestroySdk(IntPtr sdk); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr NewSlice(); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern void FreeSlice(IntPtr slice); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern string GetContentFromSlice(IntPtr slice); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern int GetSliceLen(IntPtr slice); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr NewMediaData(); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern void FreeMediaData(IntPtr mediaData); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern string GetOutIndexBuf(IntPtr mediaData); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr GetData(IntPtr mediaData); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern int GetIndexLen(IntPtr mediaData); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern int GetDataLen(IntPtr mediaData); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern int IsMediaDataFinish(IntPtr mediaData); + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/InteropServices/FinanceDllWindowsPInvoker.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/InteropServices/FinanceDllWindowsPInvoker.cs new file mode 100644 index 00000000..dd0baedb --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/InteropServices/FinanceDllWindowsPInvoker.cs @@ -0,0 +1,80 @@ +using System; +using System.Runtime.InteropServices; +using System.Security; + +namespace SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance.InteropServices +{ + internal static partial class FinanceDllWindowsPInvoker + { + private const string DLL_NAME = "WeWorkFinanceSdk.dll"; + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [SuppressUnmanagedCodeSecurity] + public static extern IntPtr NewSdk(); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [SuppressUnmanagedCodeSecurity] + public static extern int Init(IntPtr sdk, string corpId, string secret); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [SuppressUnmanagedCodeSecurity] + public static extern int GetChatData(IntPtr sdk, long seq, long limit, string proxy, string passwd, long timeout, IntPtr chatData); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [SuppressUnmanagedCodeSecurity] + public static extern int GetMediaData(IntPtr sdk, string indexBuf, string fileId, string proxy, string passwd, long timeout, IntPtr mediaData); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [SuppressUnmanagedCodeSecurity] + public static extern int DecryptData(IntPtr sdk, string encryptKey, string encryptMsg, IntPtr msgData); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [SuppressUnmanagedCodeSecurity] + public static extern void DestroySdk(IntPtr sdk); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [SuppressUnmanagedCodeSecurity] + public static extern IntPtr NewSlice(); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [SuppressUnmanagedCodeSecurity] + public static extern void FreeSlice(IntPtr slice); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [SuppressUnmanagedCodeSecurity] + [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(UTF8Marshaler))] + public static extern string GetContentFromSlice(IntPtr slice); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [SuppressUnmanagedCodeSecurity] + public static extern int GetSliceLen(IntPtr slice); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [SuppressUnmanagedCodeSecurity] + public static extern IntPtr NewMediaData(); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [SuppressUnmanagedCodeSecurity] + public static extern void FreeMediaData(IntPtr mediaData); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [SuppressUnmanagedCodeSecurity] + public static extern string GetOutIndexBuf(IntPtr mediaData); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [SuppressUnmanagedCodeSecurity] + public static extern IntPtr GetData(IntPtr mediaData); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [SuppressUnmanagedCodeSecurity] + public static extern int GetIndexLen(IntPtr mediaData); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [SuppressUnmanagedCodeSecurity] + public static extern int GetDataLen(IntPtr mediaData); + + [DllImport(DLL_NAME, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [SuppressUnmanagedCodeSecurity] + public static extern int IsMediaDataFinish(IntPtr mediaData); + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/InteropServices/UTF8Marshaler.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/InteropServices/UTF8Marshaler.cs new file mode 100644 index 00000000..2651ea41 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/InteropServices/UTF8Marshaler.cs @@ -0,0 +1,83 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance.InteropServices +{ + internal sealed class UTF8Marshaler : ICustomMarshaler + { + private static readonly Lazy _instance = new Lazy(() => new UTF8Marshaler()); + + public static ICustomMarshaler GetInstance(string pstrCookie) + { + return _instance.Value; + } + + public static string? PtrToStringUTF8(IntPtr pNativeData) + { + return _instance.Value.MarshalNativeToManaged(pNativeData) as string; + } + + public IntPtr MarshalManagedToNative(object managedObj) + { + if (managedObj is null) + return IntPtr.Zero; + if (!(managedObj is string)) + throw new InvalidOperationException(); + + byte[] bytes = Encoding.UTF8.GetBytes((string)managedObj); + IntPtr pNativeData = Marshal.AllocHGlobal(bytes.Length + 1); + Marshal.Copy(bytes, 0, pNativeData, bytes.Length); + Marshal.WriteByte(pNativeData, bytes.Length, 0); + return pNativeData; + } + + public object MarshalNativeToManaged(IntPtr pNativeData) + { + if (pNativeData == IntPtr.Zero) + return default!; + +#if NETCOREAPP || NET5_0_OR_GREATER + return Marshal.PtrToStringUTF8(pNativeData)!; +#else + byte b; + int offset = 0; + + do + { + b = Marshal.ReadByte(pNativeData, offset); + offset++; + } + while (b != 0); + + byte[] bytes = new byte[offset - 1]; + Marshal.Copy(pNativeData, bytes, 0, bytes.Length); + return Encoding.UTF8.GetString(bytes); +#endif + } + + public void CleanUpManagedData(object managedObj) + { + } + + public void CleanUpNativeData(IntPtr pNativeData) + { + if (pNativeData == IntPtr.Zero) + { + return; + } + + /** + * NOTICE: + * 这里释放内存会导致外部 P/Invoke 调用 FreeSlice() 方法时抛出异常 + * 因此请注释下面的代码 + */ + // Marshal.FreeHGlobal(pNativeData); + } + + public int GetNativeDataSize() + { + return -1; + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/Abstractions/ChatMessage.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/Abstractions/ChatMessage.cs new file mode 100644 index 00000000..e27c4dd5 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/Abstractions/ChatMessage.cs @@ -0,0 +1,894 @@ +using System.Collections.Generic; + +namespace SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance.Models.Abstractions +{ + public abstract class ChatMessageBase + { + /// + /// 获取或设置扩展字段。 + /// + [Newtonsoft.Json.JsonExtensionData] + [System.Text.Json.Serialization.JsonExtensionData] + public IDictionary? ExtensionData { get; set; } + } + + public class TextMessage : ChatMessageBase + { + /// + /// 获取或设置文本内容。 + /// + [Newtonsoft.Json.JsonProperty("content")] + [System.Text.Json.Serialization.JsonPropertyName("content")] + public string Content { get; set; } = default!; + } + + public class ImageMessage : ChatMessageBase + { + /// + /// 获取或设置图片 FileId。 + /// + [Newtonsoft.Json.JsonProperty("sdkfileid")] + [System.Text.Json.Serialization.JsonPropertyName("sdkfileid")] + public string FileId { get; set; } = default!; + + /// + /// 获取或设置图片文件 MD5 哈希值。 + /// + [Newtonsoft.Json.JsonProperty("md5sum")] + [System.Text.Json.Serialization.JsonPropertyName("md5sum")] + public string FileMD5 { get; set; } = default!; + + /// + /// 获取或设置图片文件大小(单位:字节)。 + /// + [Newtonsoft.Json.JsonProperty("filesize")] + [System.Text.Json.Serialization.JsonPropertyName("filesize")] + public int FileSize { get; set; } + } + + public class RevokeMessage : ChatMessageBase + { + /// + /// 获取或设置原消息 ID。 + /// + [Newtonsoft.Json.JsonProperty("pre_msgid")] + [System.Text.Json.Serialization.JsonPropertyName("pre_msgid")] + public string PreviousMessageId { get; set; } = default!; + } + + public class AgreeMessage : ChatMessageBase + { + /// + /// 获取或设置 UserId。 + /// + [Newtonsoft.Json.JsonProperty("userid")] + [System.Text.Json.Serialization.JsonPropertyName("userid")] + public string UserId { get; set; } = default!; + + /// + /// 获取或设置毫秒级时间戳。 + /// + [Newtonsoft.Json.JsonProperty("agree_time")] + [System.Text.Json.Serialization.JsonPropertyName("agree_time")] + public long AgreeTimeMilliseconds { get; set; } + } + + public class VoiceMessage : ChatMessageBase + { + /// + /// 获取或设置语音时长(单位:秒)。 + /// + [Newtonsoft.Json.JsonProperty("play_length")] + [System.Text.Json.Serialization.JsonPropertyName("play_length")] + public int Duration { get; set; } + + /// + /// 获取或设置语音 FileId。 + /// + [Newtonsoft.Json.JsonProperty("sdkfileid")] + [System.Text.Json.Serialization.JsonPropertyName("sdkfileid")] + public string FileId { get; set; } = default!; + + /// + /// 获取或设置语音文件 MD5 哈希值。 + /// + [Newtonsoft.Json.JsonProperty("md5sum")] + [System.Text.Json.Serialization.JsonPropertyName("md5sum")] + public string FileMD5 { get; set; } = default!; + + /// + /// 获取或设置语音文件大小(单位:字节)。 + /// + [Newtonsoft.Json.JsonProperty("voice_size")] + [System.Text.Json.Serialization.JsonPropertyName("voice_size")] + public int FileSize { get; set; } + } + + public class VideoMessage : ChatMessageBase + { + /// + /// 获取或设置视频时长(单位:秒)。 + /// + [Newtonsoft.Json.JsonProperty("play_length")] + [System.Text.Json.Serialization.JsonPropertyName("play_length")] + public int Duration { get; set; } + + /// + /// 获取或设置视频 FileId。 + /// + [Newtonsoft.Json.JsonProperty("sdkfileid")] + [System.Text.Json.Serialization.JsonPropertyName("sdkfileid")] + public string FileId { get; set; } = default!; + + /// + /// 获取或设置视频文件 MD5 哈希值。 + /// + [Newtonsoft.Json.JsonProperty("md5sum")] + [System.Text.Json.Serialization.JsonPropertyName("md5sum")] + public string FileMD5 { get; set; } = default!; + + /// + /// 获取或设置视频文件大小(单位:字节)。 + /// + [Newtonsoft.Json.JsonProperty("filesize")] + [System.Text.Json.Serialization.JsonPropertyName("filesize")] + public int FileSize { get; set; } + } + + public class BusinessCardMessage : ChatMessageBase + { + /// + /// 获取或设置企业名称。 + /// + [Newtonsoft.Json.JsonProperty("corpname")] + [System.Text.Json.Serialization.JsonPropertyName("corpname")] + public string CorpName { get; set; } = default!; + + /// + /// 获取或设置 UserId。 + /// + [Newtonsoft.Json.JsonProperty("userid")] + [System.Text.Json.Serialization.JsonPropertyName("userid")] + public string UserId { get; set; } = default!; + } + + public class LocationMessage : ChatMessageBase + { + /// + /// 获取或设置纬度坐标。 + /// + [Newtonsoft.Json.JsonProperty("latitude")] + [System.Text.Json.Serialization.JsonPropertyName("latitude")] + public double Latitude { get; set; } + + /// + /// 获取或设置经度坐标。 + /// + [Newtonsoft.Json.JsonProperty("longitude")] + [System.Text.Json.Serialization.JsonPropertyName("longitude")] + public double Longitude { get; set; } + + /// + /// 获取或设置位置名称。 + /// + [Newtonsoft.Json.JsonProperty("title")] + [System.Text.Json.Serialization.JsonPropertyName("title")] + public string Title { get; set; } = default!; + + /// + /// 获取或设置详细地址。 + /// + [Newtonsoft.Json.JsonProperty("address")] + [System.Text.Json.Serialization.JsonPropertyName("address")] + public string Address { get; set; } = default!; + + /// + /// 获取或设置缩放比例。 + /// + [Newtonsoft.Json.JsonProperty("zoom")] + [System.Text.Json.Serialization.JsonPropertyName("zoom")] + public int Zoom { get; set; } + } + + public class EmotionMessage : ChatMessageBase + { + /// + /// 获取或设置表情类型。 + /// + [Newtonsoft.Json.JsonProperty("type")] + [System.Text.Json.Serialization.JsonPropertyName("type")] + public int Type { get; set; } + + /// + /// 获取或设置宽度(单位:像素)。 + /// + [Newtonsoft.Json.JsonProperty("width")] + [System.Text.Json.Serialization.JsonPropertyName("width")] + public int Width { get; set; } + + /// + /// 获取或设置高度(单位:像素)。 + /// + [Newtonsoft.Json.JsonProperty("height")] + [System.Text.Json.Serialization.JsonPropertyName("height")] + public int Height { get; set; } + + /// + /// 获取或设置表情 FileId。 + /// + [Newtonsoft.Json.JsonProperty("sdkfileid")] + [System.Text.Json.Serialization.JsonPropertyName("sdkfileid")] + public string FileId { get; set; } = default!; + + /// + /// 获取或设置表情文件 MD5 哈希值。 + /// + [Newtonsoft.Json.JsonProperty("md5sum")] + [System.Text.Json.Serialization.JsonPropertyName("md5sum")] + public string FileMD5 { get; set; } = default!; + + /// + /// 获取或设置表情文件大小(单位:字节)。 + /// + [Newtonsoft.Json.JsonProperty("imagesize")] + [System.Text.Json.Serialization.JsonPropertyName("imagesize")] + public int FileSize { get; set; } + } + + public class FileMessage : ChatMessageBase + { + /// + /// 获取或设置文件名称。 + /// + [Newtonsoft.Json.JsonProperty("filename")] + [System.Text.Json.Serialization.JsonPropertyName("filename")] + public string FileName { get; set; } = default!; + + /// + /// 获取或设置文件后缀。 + /// + [Newtonsoft.Json.JsonProperty("fileext")] + [System.Text.Json.Serialization.JsonPropertyName("fileext")] + public string FileExtension { get; set; } = default!; + + /// + /// 获取或设置文件 FileId。 + /// + [Newtonsoft.Json.JsonProperty("sdkfileid")] + [System.Text.Json.Serialization.JsonPropertyName("sdkfileid")] + public string FileId { get; set; } = default!; + + /// + /// 获取或设置文件 MD5 哈希值。 + /// + [Newtonsoft.Json.JsonProperty("md5sum")] + [System.Text.Json.Serialization.JsonPropertyName("md5sum")] + public string FileMD5 { get; set; } = default!; + + /// + /// 获取或设置文件大小(单位:字节)。 + /// + [Newtonsoft.Json.JsonProperty("filesize")] + [System.Text.Json.Serialization.JsonPropertyName("filesize")] + public int FileSize { get; set; } + } + + public class LinkMessage : ChatMessageBase + { + /// + /// 获取或设置点击后跳转的链接。 + /// + [Newtonsoft.Json.JsonProperty("link_url")] + [System.Text.Json.Serialization.JsonPropertyName("link_url")] + public string LinkUrl { get; set; } = default!; + + /// + /// 获取或设置图文标题。 + /// + [Newtonsoft.Json.JsonProperty("title")] + [System.Text.Json.Serialization.JsonPropertyName("title")] + public string Title { get; set; } = default!; + + /// + /// 获取或设置图文描述。 + /// + [Newtonsoft.Json.JsonProperty("description")] + [System.Text.Json.Serialization.JsonPropertyName("description")] + public string Description { get; set; } = default!; + + /// + /// 获取或设置图文封面的 URL。 + /// + [Newtonsoft.Json.JsonProperty("image_url")] + [System.Text.Json.Serialization.JsonPropertyName("image_url")] + public string ImageUrl { get; set; } = default!; + } + + public class MiniProgramMessage : ChatMessageBase + { + /// + /// 获取或设置消息标题。 + /// + [Newtonsoft.Json.JsonProperty("title")] + [System.Text.Json.Serialization.JsonPropertyName("title")] + public string Title { get; set; } = default!; + + /// + /// 获取或设置消息描述。 + /// + [Newtonsoft.Json.JsonProperty("description")] + [System.Text.Json.Serialization.JsonPropertyName("description")] + public string Description { get; set; } = default!; + + /// + /// 获取或设置小程序名称。 + /// + [Newtonsoft.Json.JsonProperty("displayname")] + [System.Text.Json.Serialization.JsonPropertyName("displayname")] + public string DisplayName { get; set; } = default!; + + /// + /// 获取或设置用户名称。 + /// + [Newtonsoft.Json.JsonProperty("username")] + [System.Text.Json.Serialization.JsonPropertyName("username")] + public string UserName { get; set; } = default!; + } + + public class ChatRecordMessage : ChatMessageBase + { + public static class Types + { + public class Record + { + /// + /// 获取或设置消息发送时间戳。 + /// + [Newtonsoft.Json.JsonProperty("msgtime")] + [System.Text.Json.Serialization.JsonPropertyName("msgtime")] + public long MessageTimestamp { get; set; } + + /// + /// 获取或设置消息类型。 + /// + [Newtonsoft.Json.JsonProperty("type")] + [System.Text.Json.Serialization.JsonPropertyName("type")] + public string MessageType { get; set; } = default!; + + /// + /// 获取或设置消息内容 JSON 字符串。 + /// + [Newtonsoft.Json.JsonProperty("content")] + [System.Text.Json.Serialization.JsonPropertyName("content")] + public string MessageContentJson { get; set; } = default!; + + /// + /// 获取或设置是否来自群聊。 + /// + [Newtonsoft.Json.JsonProperty("from_chatroom")] + [System.Text.Json.Serialization.JsonPropertyName("from_chatroom")] + public bool IsFromChatroom { get; set; } + } + } + + /// + /// 获取或设置聊天记录标题。 + /// + [Newtonsoft.Json.JsonProperty("title")] + [System.Text.Json.Serialization.JsonPropertyName("title")] + public string Title { get; set; } = default!; + + /// + /// 获取或设置聊天记录列表。 + /// + [Newtonsoft.Json.JsonProperty("item")] + [System.Text.Json.Serialization.JsonPropertyName("item")] + public Types.Record[] RecordList { get; set; } = default!; + } + + public class TodoMessage : ChatMessageBase + { + /// + /// 获取或设置待办的来源文本。 + /// + [Newtonsoft.Json.JsonProperty("title")] + [System.Text.Json.Serialization.JsonPropertyName("title")] + public string Title { get; set; } = default!; + + /// + /// 获取或设置待办的具体内容。 + /// + [Newtonsoft.Json.JsonProperty("content")] + [System.Text.Json.Serialization.JsonPropertyName("content")] + public string Content { get; set; } = default!; + } + + public class VoteMessage : ChatMessageBase + { + /// + /// 获取或设置投票 ID。 + /// + [Newtonsoft.Json.JsonProperty("voteid")] + [System.Text.Json.Serialization.JsonPropertyName("voteid")] + public string VoteId { get; set; } = default!; + + /// + /// 获取或设置投票类型。 + /// + [Newtonsoft.Json.JsonProperty("votetype")] + [System.Text.Json.Serialization.JsonPropertyName("votetype")] + public int Type { get; set; } + + /// + /// 获取或设置投票主题。 + /// + [Newtonsoft.Json.JsonProperty("votetitle")] + [System.Text.Json.Serialization.JsonPropertyName("votetitle")] + public string Title { get; set; } = default!; + + /// + /// 获取或设置投票选项列表。 + /// + [Newtonsoft.Json.JsonProperty("voteitem")] + [System.Text.Json.Serialization.JsonPropertyName("voteitem")] + public string[] Options { get; set; } = default!; + } + + public class CollectMessage : ChatMessageBase + { + public static class Types + { + public class Detail + { + /// + /// 获取或设置表项 ID。 + /// + [Newtonsoft.Json.JsonProperty("id")] + [System.Text.Json.Serialization.JsonPropertyName("id")] + public long ID { get; set; } + + /// + /// 获取或设置表项类型。 + /// + [Newtonsoft.Json.JsonProperty("type")] + [System.Text.Json.Serialization.JsonPropertyName("type")] + public string Type { get; set; } = default!; + + /// + /// 获取或设置表项名称。 + /// + [Newtonsoft.Json.JsonProperty("ques")] + [System.Text.Json.Serialization.JsonPropertyName("ques")] + public string Question { get; set; } = default!; + } + } + + /// + /// 获取或设置群聊名称。 + /// + [Newtonsoft.Json.JsonProperty("room_name")] + [System.Text.Json.Serialization.JsonPropertyName("room_name")] + public string RoomName { get; set; } = default!; + + /// + /// 获取或设置创建者名称。 + /// + [Newtonsoft.Json.JsonProperty("creator")] + [System.Text.Json.Serialization.JsonPropertyName("creator")] + public string CreatorName { get; set; } = default!; + + /// + /// 获取或设置创建时间字符串(格式:yyyy-MM-dd HH:mm:ss)。 + /// + [Newtonsoft.Json.JsonProperty("create_time")] + [System.Text.Json.Serialization.JsonPropertyName("create_time")] + public string CreateTimeString { get; set; } = default!; + + /// + /// 获取或设置表名。 + /// + [Newtonsoft.Json.JsonProperty("title")] + [System.Text.Json.Serialization.JsonPropertyName("title")] + public string Title { get; set; } = default!; + + /// + /// 获取或设置表项列表。 + /// + [Newtonsoft.Json.JsonProperty("details")] + [System.Text.Json.Serialization.JsonPropertyName("details")] + public Types.Detail[] DetailList { get; set; } = default!; + } + + public class RedPacketMessage : ChatMessageBase + { + /// + /// 获取或设置红包类型。 + /// + [Newtonsoft.Json.JsonProperty("type")] + [System.Text.Json.Serialization.JsonPropertyName("type")] + public int Type { get; set; } + + /// + /// 获取或设置红包祝福语。 + /// + [Newtonsoft.Json.JsonProperty("wish")] + [System.Text.Json.Serialization.JsonPropertyName("wish")] + public string Wishing { get; set; } = default!; + + /// + /// 获取或设置总个数。 + /// + [Newtonsoft.Json.JsonProperty("totalcnt")] + [System.Text.Json.Serialization.JsonPropertyName("totalcnt")] + public int TotalCount { get; set; } + + /// + /// 获取或设置总金额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("totalamount")] + [System.Text.Json.Serialization.JsonPropertyName("totalamount")] + public int TotalAmount { get; set; } + } + + public class MeetingMessage : ChatMessageBase + { + /// + /// 获取或设置会议类型。 + /// + [Newtonsoft.Json.JsonProperty("meetingtype")] + [System.Text.Json.Serialization.JsonPropertyName("meetingtype")] + public int Type { get; set; } + + /// + /// 获取或设置会议 ID。 + /// + [Newtonsoft.Json.JsonProperty("meetingid")] + [System.Text.Json.Serialization.JsonPropertyName("meetingid")] + [System.Text.Json.Serialization.JsonNumberHandling(System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString)] + public long MeetingId { get; set; } + + /// + /// 获取或设置会议主题。 + /// + [Newtonsoft.Json.JsonProperty("topic")] + [System.Text.Json.Serialization.JsonPropertyName("topic")] + public string Topic { get; set; } = default!; + + /// + /// 获取或设置会议开始时间戳。 + /// + [Newtonsoft.Json.JsonProperty("starttime")] + [System.Text.Json.Serialization.JsonPropertyName("starttime")] + public long StartTimestamp { get; set; } + + /// + /// 获取或设置会议结束时间戳。 + /// + [Newtonsoft.Json.JsonProperty("endtime")] + [System.Text.Json.Serialization.JsonPropertyName("endtime")] + public long EndTimestamp { get; set; } + + /// + /// 获取或设置会议地址。 + /// + [Newtonsoft.Json.JsonProperty("address")] + [System.Text.Json.Serialization.JsonPropertyName("address")] + public string Address { get; set; } = default!; + + /// + /// 获取或设置备注信息。 + /// + [Newtonsoft.Json.JsonProperty("remarks")] + [System.Text.Json.Serialization.JsonPropertyName("remarks")] + public string Remark { get; set; } = default!; + + /// + /// 获取或设置会议邀请处理状态。 + /// + [Newtonsoft.Json.JsonProperty("status")] + [System.Text.Json.Serialization.JsonPropertyName("status")] + public int? Status { get; set; } + } + + public class DocumentMessage : ChatMessageBase + { + /// + /// 获取或设置文档标题。 + /// + [Newtonsoft.Json.JsonProperty("title")] + [System.Text.Json.Serialization.JsonPropertyName("title")] + public string Title { get; set; } = default!; + + /// + /// 获取或设置文档链接。 + /// + [Newtonsoft.Json.JsonProperty("link_url")] + [System.Text.Json.Serialization.JsonPropertyName("link_url")] + public string LinkUrl { get; set; } = default!; + + /// + /// 获取或设置创建者 UserId。 + /// + [Newtonsoft.Json.JsonProperty("doc_creator")] + [System.Text.Json.Serialization.JsonPropertyName("doc_creator")] + public string CreatorUserId { get; set; } = default!; + } + + public class InfoMessage : ChatMessageBase + { + /// + /// 获取或设置 Markdown 内容。 + /// + [Newtonsoft.Json.JsonProperty("content")] + [System.Text.Json.Serialization.JsonPropertyName("content")] + public string? MarkdownContent { get; set; } + + /// + /// 获取或设置图文消息链接。 + /// + [Newtonsoft.Json.JsonProperty("url")] + [System.Text.Json.Serialization.JsonPropertyName("url")] + public string? NewsUrl { get; set; } + + /// + /// 获取或设置图文消息标题。 + /// + [Newtonsoft.Json.JsonProperty("title")] + [System.Text.Json.Serialization.JsonPropertyName("title")] + public string? NewsTitle { get; set; } + + /// + /// 获取或设置图文消息描述。 + /// + [Newtonsoft.Json.JsonProperty("description")] + [System.Text.Json.Serialization.JsonPropertyName("description")] + public string? NewsDescription { get; set; } + + /// + /// 获取或设置 VoIP 通话时长(单位:秒)。 + /// + [Newtonsoft.Json.JsonProperty("callduration")] + [System.Text.Json.Serialization.JsonPropertyName("callduration")] + public int? VoIPCallDuration { get; set; } + + /// + /// 获取或设置 VoIP 通话类型。 + /// + [Newtonsoft.Json.JsonProperty("invitetype")] + [System.Text.Json.Serialization.JsonPropertyName("invitetype")] + public int? VoIPInviteType { get; set; } + + /// + /// 获取或设置微盘文件名称。 + /// + [Newtonsoft.Json.JsonProperty("filename")] + [System.Text.Json.Serialization.JsonPropertyName("filename")] + public string? WedriveFileName { get; set; } + } + + public class CalendarMessage : ChatMessageBase + { + /// + /// 获取或设置日程主题。 + /// + [Newtonsoft.Json.JsonProperty("title")] + [System.Text.Json.Serialization.JsonPropertyName("title")] + public string Title { get; set; } = default!; + + /// + /// 获取或设置日程组织者名称。 + /// + [Newtonsoft.Json.JsonProperty("creatorname")] + [System.Text.Json.Serialization.JsonPropertyName("creatorname")] + public string CreatorName { get; set; } = default!; + + /// + /// 获取或设置日程开始时间戳。 + /// + [Newtonsoft.Json.JsonProperty("starttime")] + [System.Text.Json.Serialization.JsonPropertyName("starttime")] + public long StartTimestamp { get; set; } + + /// + /// 获取或设置日程结束时间戳。 + /// + [Newtonsoft.Json.JsonProperty("endtime")] + [System.Text.Json.Serialization.JsonPropertyName("endtime")] + public long EndTimestamp { get; set; } + + /// + /// 获取或设置日程参与人名称列表。 + /// + [Newtonsoft.Json.JsonProperty("attendeename")] + [System.Text.Json.Serialization.JsonPropertyName("attendeename")] + public string[] AttendeeNameList { get; set; } = default!; + + /// + /// 获取或设置日程地点。 + /// + [Newtonsoft.Json.JsonProperty("place")] + [System.Text.Json.Serialization.JsonPropertyName("place")] + public string Place { get; set; } = default!; + + /// + /// 获取或设置备注信息。 + /// + [Newtonsoft.Json.JsonProperty("remarks")] + [System.Text.Json.Serialization.JsonPropertyName("remarks")] + public string Remark { get; set; } = default!; + } + + public class MixedMessage : ChatMessageBase + { + public static class Types + { + public class Message + { + /// + /// 获取或设置消息类型。 + /// + [Newtonsoft.Json.JsonProperty("type")] + [System.Text.Json.Serialization.JsonPropertyName("type")] + public string Type { get; set; } = default!; + + /// + /// 获取或设置消息内容 JSON 字符串。 + /// + [Newtonsoft.Json.JsonProperty("content")] + [System.Text.Json.Serialization.JsonPropertyName("content")] + public string ContentJson { get; set; } = default!; + } + } + + /// + /// 获取或设置混合消息列表。 + /// + [Newtonsoft.Json.JsonProperty("item")] + [System.Text.Json.Serialization.JsonPropertyName("item")] + public Types.Message[] MessageList { get; set; } = default!; + } + + public class MeetingVoiceCallMessage : ChatMessageBase + { + public static class Types + { + public class ShareFileData + { + /// + /// 获取或设置文件名称。 + /// + [Newtonsoft.Json.JsonProperty("filename")] + [System.Text.Json.Serialization.JsonPropertyName("filename")] + public string FileName { get; set; } = default!; + + /// + /// 获取或设置操作者 UserId。 + /// + [Newtonsoft.Json.JsonProperty("demooperator")] + [System.Text.Json.Serialization.JsonPropertyName("demooperator")] + public string OperatorUserId { get; set; } = default!; + + /// + /// 获取或设置开始时间戳。 + /// + [Newtonsoft.Json.JsonProperty("starttime")] + [System.Text.Json.Serialization.JsonPropertyName("starttime")] + public long StartTimestamp { get; set; } + + /// + /// 获取或设置结束时间戳。 + /// + [Newtonsoft.Json.JsonProperty("endtime")] + [System.Text.Json.Serialization.JsonPropertyName("endtime")] + public long EndTimestamp { get; set; } + } + + public class ShareScreenData + { + /// + /// 获取或设置分享者 UserId。 + /// + [Newtonsoft.Json.JsonProperty("share")] + [System.Text.Json.Serialization.JsonPropertyName("share")] + public string SharerUserId { get; set; } = default!; + + /// + /// 获取或设置开始时间戳。 + /// + [Newtonsoft.Json.JsonProperty("starttime")] + [System.Text.Json.Serialization.JsonPropertyName("starttime")] + public long StartTimestamp { get; set; } + + /// + /// 获取或设置结束时间戳。 + /// + [Newtonsoft.Json.JsonProperty("endtime")] + [System.Text.Json.Serialization.JsonPropertyName("endtime")] + public long EndTimestamp { get; set; } + } + } + + /// + /// 获取或设置音频 FileId。 + /// + [Newtonsoft.Json.JsonProperty("sdkfileid")] + [System.Text.Json.Serialization.JsonPropertyName("sdkfileid")] + public string FileId { get; set; } = default!; + + /// + /// 获取或设置音频结束时间戳。 + /// + [Newtonsoft.Json.JsonProperty("endtime")] + [System.Text.Json.Serialization.JsonPropertyName("endtime")] + public long EndTimestamp { get; set; } + + /// + /// 获取或设置文档分享对象列表。 + /// + [Newtonsoft.Json.JsonProperty("demofiledata")] + [System.Text.Json.Serialization.JsonPropertyName("demofiledata")] + public Types.ShareFileData[]? ShareFileDataList { get; set; } + + /// + /// 获取或设置屏幕共享对象列表。 + /// + [Newtonsoft.Json.JsonProperty("sharescreendata")] + [System.Text.Json.Serialization.JsonPropertyName("sharescreendata")] + public Types.ShareScreenData[]? ShareScreenDataList { get; set; } + } + + public class VoIPDocumentShareMessage : ChatMessageBase + { + /// + /// 获取或设置音频文件名称。 + /// + [Newtonsoft.Json.JsonProperty("filename")] + [System.Text.Json.Serialization.JsonPropertyName("filename")] + public string FileName { get; set; } = default!; + + /// + /// 获取或设置音频 FileId。 + /// + [Newtonsoft.Json.JsonProperty("sdkfileid")] + [System.Text.Json.Serialization.JsonPropertyName("sdkfileid")] + public string FileId { get; set; } = default!; + + /// + /// 获取或设置音频文件 MD5 哈希值。 + /// + [Newtonsoft.Json.JsonProperty("md5sum")] + [System.Text.Json.Serialization.JsonPropertyName("md5sum")] + public string FileMD5 { get; set; } = default!; + + /// + /// 获取或设置音频文件大小(单位:字节)。 + /// + [Newtonsoft.Json.JsonProperty("filesize")] + [System.Text.Json.Serialization.JsonPropertyName("filesize")] + public int FileSize { get; set; } + } + + public class ChannelsFeedMessage : ChatMessageBase + { + /// + /// 获取或设置消息类型。 + /// + [Newtonsoft.Json.JsonProperty("feed_type")] + [System.Text.Json.Serialization.JsonPropertyName("feed_type")] + public int FeedType { get; set; } + + /// + /// 获取或设置视频号账号名称。 + /// + [Newtonsoft.Json.JsonProperty("sph_name")] + [System.Text.Json.Serialization.JsonPropertyName("sph_name")] + public string ChannelsNickName { get; set; } = default!; + + /// + /// 获取或设置消息描述。 + /// + [Newtonsoft.Json.JsonProperty("feed_desc")] + [System.Text.Json.Serialization.JsonPropertyName("feed_desc")] + public string Description { get; set; } = default!; + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/DecryptChatRecordRequest.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/DecryptChatRecordRequest.cs new file mode 100644 index 00000000..a3042a37 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/DecryptChatRecordRequest.cs @@ -0,0 +1,29 @@ +namespace SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance.Models +{ + /// + /// 表示会话内容存档之解密会话记录数据接口的请求。 + /// + public class DecryptChatRecordRequest : WechatWorkFinanceRequest + { + /// + /// 获取或设置消息加解密公钥版本号。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public int PublicKeyVersion { get; set; } + + /// + /// 获取或设置经过加密的随机密钥。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public string EncryptedRandomKey { get; set; } = string.Empty; + + /// + /// 获取或设置经过加密的聊天内容。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public string EncryptedChatMessage { get; set; } = string.Empty; + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/DecryptChatRecordResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/DecryptChatRecordResponse.cs new file mode 100644 index 00000000..1a86034a --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/DecryptChatRecordResponse.cs @@ -0,0 +1,358 @@ +using System.Collections.Generic; + +namespace SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance.Models +{ + /// + /// 表示会话内容存档之解密会话记录数据接口的响应。 + /// + public class DecryptChatRecordResponse : WechatWorkFinanceResponse + { + public static class Types + { + public class TextMessage : Abstractions.TextMessage + { + } + + public class ImageMessage : Abstractions.ImageMessage + { + } + + public class RevokeMessage : Abstractions.RevokeMessage + { + } + + public class AgreeMessage : Abstractions.AgreeMessage + { + } + + public class VoiceMessage : Abstractions.VoiceMessage + { + } + + public class VideoMessage : Abstractions.VideoMessage + { + } + + public class BusinessCardMessage : Abstractions.BusinessCardMessage + { + } + + public class LocationMessage : Abstractions.LocationMessage + { + } + + public class EmotionMessage : Abstractions.EmotionMessage + { + } + + public class FileMessage : Abstractions.FileMessage + { + } + + public class LinkMessage : Abstractions.LinkMessage + { + } + + public class MiniProgramMessage : Abstractions.MiniProgramMessage + { + } + + public class ChatRecordMessage : Abstractions.ChatRecordMessage + { + } + + public class TodoMessage : Abstractions.TodoMessage + { + } + + public class VoteMessage : Abstractions.VoteMessage + { + } + + public class CollectMessage : Abstractions.CollectMessage + { + } + + public class RedPacketMessage : Abstractions.RedPacketMessage + { + } + + public class MeetingMessage : Abstractions.MeetingMessage + { + } + + public class DocumentMessage : Abstractions.DocumentMessage + { + } + + public class InfoMessage : Abstractions.InfoMessage + { + } + + public class CalendarMessage : Abstractions.CalendarMessage + { + } + + public class MixedMessage : Abstractions.MixedMessage + { + } + + public class MeetingVoiceCallMessage : Abstractions.MeetingVoiceCallMessage + { + } + + public class VoIPDocumentShareMessage : Abstractions.VoIPDocumentShareMessage + { + } + + public class ChannelsFeedMessage : Abstractions.ChannelsFeedMessage + { + } + } + + /// + /// 获取或设置消息 ID。 + /// + [Newtonsoft.Json.JsonProperty("msgid")] + [System.Text.Json.Serialization.JsonPropertyName("msgid")] + public string MessageId { get; set; } = default!; + + /// + /// 获取或设置消息动作。 + /// + [Newtonsoft.Json.JsonProperty("action")] + [System.Text.Json.Serialization.JsonPropertyName("action")] + public string Action { get; set; } = default!; + + /// + /// 获取或设置消息发送方 UserId。 + /// + [Newtonsoft.Json.JsonProperty("from")] + [System.Text.Json.Serialization.JsonPropertyName("from")] + public string? FromUserId { get; set; } + + /// + /// 获取或设置消息接收方 UserId 列表。 + /// + [Newtonsoft.Json.JsonProperty("tolist")] + [System.Text.Json.Serialization.JsonPropertyName("tolist")] + public string[]? ToUserIdList { get; set; } + + /// + /// 获取或设置群聊房间 ID。 + /// + [Newtonsoft.Json.JsonProperty("roomid")] + [System.Text.Json.Serialization.JsonPropertyName("roomid")] + public string? RoomId { get; set; } + + /// + /// 获取或设置消息发送毫秒级时间戳。 + /// + [Newtonsoft.Json.JsonProperty("msgtime")] + [System.Text.Json.Serialization.JsonPropertyName("msgtime")] + public long MessageTimeMilliseconds { get; set; } + + /// + /// 获取或设置消息类型。 + /// + [Newtonsoft.Json.JsonProperty("msgtype")] + [System.Text.Json.Serialization.JsonPropertyName("msgtype")] + public string MessageType { get; set; } = default!; + + /// + /// 获取或设置文本消息信息。 + /// + [Newtonsoft.Json.JsonProperty("text")] + [System.Text.Json.Serialization.JsonPropertyName("text")] + public Types.TextMessage? MessageContentForText { get; set; } + + /// + /// 获取或设置图片消息信息。 + /// + [Newtonsoft.Json.JsonProperty("image")] + [System.Text.Json.Serialization.JsonPropertyName("image")] + public Types.ImageMessage? MessageContentForImage { get; set; } + + /// + /// 获取或设置撤回消息信息。 + /// + [Newtonsoft.Json.JsonProperty("revoke")] + [System.Text.Json.Serialization.JsonPropertyName("revoke")] + public Types.RevokeMessage? MessageContentForRevoke { get; set; } + + /// + /// 获取或设置同意/不同意消息信息。 + /// + [Newtonsoft.Json.JsonProperty("agree")] + [System.Text.Json.Serialization.JsonPropertyName("agree")] + public Types.AgreeMessage? MessageContentForAgree { get; set; } + + /// + /// 获取或设置语音消息信息。 + /// + [Newtonsoft.Json.JsonProperty("voice")] + [System.Text.Json.Serialization.JsonPropertyName("voice")] + public Types.VoiceMessage? MessageContentForVoice { get; set; } + + /// + /// 获取或设置视频消息信息。 + /// + [Newtonsoft.Json.JsonProperty("video")] + [System.Text.Json.Serialization.JsonPropertyName("video")] + public Types.VideoMessage? MessageContentForVideo { get; set; } + + /// + /// 获取或设置名片消息信息。 + /// + [Newtonsoft.Json.JsonProperty("card")] + [System.Text.Json.Serialization.JsonPropertyName("card")] + public Types.BusinessCardMessage? MessageContentForBusinessCard { get; set; } + + /// + /// 获取或设置位置消息信息。 + /// + [Newtonsoft.Json.JsonProperty("location")] + [System.Text.Json.Serialization.JsonPropertyName("location")] + public Types.LocationMessage? MessageContentForLocation { get; set; } + + /// + /// 获取或设置表情消息信息。 + /// + [Newtonsoft.Json.JsonProperty("emotion")] + [System.Text.Json.Serialization.JsonPropertyName("emotion")] + public Types.EmotionMessage? MessageContentForEmotion { get; set; } + + /// + /// 获取或设置文件消息信息。 + /// + [Newtonsoft.Json.JsonProperty("file")] + [System.Text.Json.Serialization.JsonPropertyName("file")] + public Types.FileMessage? MessageContentForFile { get; set; } + + /// + /// 获取或设置图文链接消息信息。 + /// + [Newtonsoft.Json.JsonProperty("link")] + [System.Text.Json.Serialization.JsonPropertyName("link")] + public Types.LinkMessage? MessageContentForLink { get; set; } + + /// + /// 获取或设置小程序消息信息。 + /// + [Newtonsoft.Json.JsonProperty("weapp")] + [System.Text.Json.Serialization.JsonPropertyName("weapp")] + public Types.MiniProgramMessage? MessageContentForMiniProgram { get; set; } + + /// + /// 获取或设置会话记录消息信息。 + /// + [Newtonsoft.Json.JsonProperty("chatrecord")] + [System.Text.Json.Serialization.JsonPropertyName("chatrecord")] + public Types.ChatRecordMessage? MessageContentForChatRecord { get; set; } + + /// + /// 获取或设置待办消息信息。 + /// + [Newtonsoft.Json.JsonProperty("todo")] + [System.Text.Json.Serialization.JsonPropertyName("todo")] + public Types.TodoMessage? MessageContentForTodo { get; set; } + + /// + /// 获取或设置投票消息信息。 + /// + [Newtonsoft.Json.JsonProperty("vote")] + [System.Text.Json.Serialization.JsonPropertyName("vote")] + public Types.VoteMessage? MessageContentForVote { get; set; } + + /// + /// 获取或设置填表消息信息。 + /// + [Newtonsoft.Json.JsonProperty("collect")] + [System.Text.Json.Serialization.JsonPropertyName("collect")] + public Types.CollectMessage? MessageContentForCollect { get; set; } + + /// + /// 获取或设置红包消息信息。 + /// + [Newtonsoft.Json.JsonProperty("redpacket")] + [System.Text.Json.Serialization.JsonPropertyName("redpacket")] + public Types.RedPacketMessage? MessageContentForRedPacket { get; set; } + + /// + /// 获取或设置会议消息信息。 + /// + [Newtonsoft.Json.JsonProperty("meeting")] + [System.Text.Json.Serialization.JsonPropertyName("meeting")] + public Types.MeetingMessage? MessageContentForMeeting { get; set; } + + /// + /// 获取或设置在线文档消息信息。 + /// + [Newtonsoft.Json.JsonProperty("doc")] + [System.Text.Json.Serialization.JsonPropertyName("doc")] + public Types.DocumentMessage? MessageContentForDocument { get; set; } + + /// + /// 获取或设置 Markdown、图文消息或音视频通话信息。 + /// + [Newtonsoft.Json.JsonProperty("info")] + [System.Text.Json.Serialization.JsonPropertyName("info")] + public Types.InfoMessage? MessageContentForInfo { get; set; } + + /// + /// 获取或设置日程消息信息。 + /// + [Newtonsoft.Json.JsonProperty("calendar")] + [System.Text.Json.Serialization.JsonPropertyName("calendar")] + public Types.CalendarMessage? MessageContentForCalendar { get; set; } + + /// + /// 获取或设置混合消息信息。 + /// + [Newtonsoft.Json.JsonProperty("mixed")] + [System.Text.Json.Serialization.JsonPropertyName("mixed")] + public Types.MixedMessage? MessageContentForMixed { get; set; } + + /// + /// 获取或设置会议音频存档消息信息。 + /// + [Newtonsoft.Json.JsonProperty("meeting_voice_call")] + [System.Text.Json.Serialization.JsonPropertyName("meeting_voice_call")] + public Types.MeetingVoiceCallMessage? MessageContentForMeetingVoiceCall { get; set; } + + /// + /// 获取或设置 VoIP 音频存档消息信息。 + /// + [Newtonsoft.Json.JsonProperty("voip_doc_share")] + [System.Text.Json.Serialization.JsonPropertyName("voip_doc_share")] + public Types.VoIPDocumentShareMessage? MessageContentForVoIPDocumentShare { get; set; } + + /// + /// 获取或设置视频号消息信息。 + /// + [Newtonsoft.Json.JsonProperty("sphfeed")] + [System.Text.Json.Serialization.JsonPropertyName("sphfeed")] + public Types.ChannelsFeedMessage? MessageContentForChannelsFeed { get; set; } + + /// + /// 获取或设置音频 ID。 + /// + [Newtonsoft.Json.JsonProperty("voiceid")] + [System.Text.Json.Serialization.JsonPropertyName("voiceid")] + public string? VoiceId { get; set; } = default!; + + /// + /// 获取或设置 VoIP ID。 + /// + [Newtonsoft.Json.JsonProperty("voipid")] + [System.Text.Json.Serialization.JsonPropertyName("voipid")] + public string? VoIPId { get; set; } = default!; + + /// + /// 获取或设置扩展字段。 + /// + [Newtonsoft.Json.JsonExtensionData] + [System.Text.Json.Serialization.JsonExtensionData] + public IDictionary? ExtensionData { get; set; } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/GetChatRecordsRequest.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/GetChatRecordsRequest.cs new file mode 100644 index 00000000..ab41ef9d --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/GetChatRecordsRequest.cs @@ -0,0 +1,23 @@ +namespace SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance.Models +{ + /// + /// 表示会话内容存档之获取会话记录数据接口的请求。 + /// + public class GetChatRecordsRequest : WechatWorkFinanceRequest + { + /// + /// 获取或设置起始序号。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public long LastSequence { get; set; } + + /// + /// 获取或设置分页每页数量。 + /// 默认值:1000 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public int Limit { get; set; } = 1000; + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/GetChatRecordsResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/GetChatRecordsResponse.cs new file mode 100644 index 00000000..0d10735b --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/GetChatRecordsResponse.cs @@ -0,0 +1,75 @@ +namespace SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance.Models +{ + /// + /// 表示会话内容存档之获取会话记录数据接口的响应。 + /// + public class GetChatRecordsResponse : WechatWorkFinanceResponse + { + public static class Types + { + public class Record + { + /// + /// 获取或设置序号。 + /// + [Newtonsoft.Json.JsonProperty("seq")] + [System.Text.Json.Serialization.JsonPropertyName("seq")] + public long Sequence { get; set; } + + /// + /// 获取或设置消息 ID。 + /// + [Newtonsoft.Json.JsonProperty("msgid")] + [System.Text.Json.Serialization.JsonPropertyName("msgid")] + public string MessageId { get; set; } = default!; + + /// + /// 获取或设置消息加解密公钥版本号。 + /// + [Newtonsoft.Json.JsonProperty("publickey_ver")] + [System.Text.Json.Serialization.JsonPropertyName("publickey_ver")] + public int PublicKeyVersion { get; set; } + + /// + /// 获取或设置经过加密的随机密钥。 + /// + [Newtonsoft.Json.JsonProperty("encrypt_random_key")] + [System.Text.Json.Serialization.JsonPropertyName("encrypt_random_key")] + public string EncryptedRandomKey { get; set; } = default!; + + /// + /// 获取或设置经过加密的聊天内容。 + /// + [Newtonsoft.Json.JsonProperty("encrypt_chat_msg")] + [System.Text.Json.Serialization.JsonPropertyName("encrypt_chat_msg")] + public string EncryptedChatMessage { get; set; } = default!; + } + } + + /// + /// 获取或设置错误码。 + /// + [Newtonsoft.Json.JsonProperty("errcode")] + [System.Text.Json.Serialization.JsonPropertyName("errcode")] + public int ErrorCode { get; set; } + + /// + /// 获取或设置错误描述。 + /// + [Newtonsoft.Json.JsonProperty("errmsg")] + [System.Text.Json.Serialization.JsonPropertyName("errmsg")] + public string? ErrorMessage { get; set; } + + /// + /// 获取或设置聊天记录列表。 + /// + [Newtonsoft.Json.JsonProperty("chatdata")] + [System.Text.Json.Serialization.JsonPropertyName("chatdata")] + public Types.Record[] RecordList { get; set; } = default!; + + public override bool IsSuccessful() + { + return base.IsSuccessful() && ErrorCode == 0; + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/GetMediaFileBufferRequest.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/GetMediaFileBufferRequest.cs new file mode 100644 index 00000000..0d327c44 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/GetMediaFileBufferRequest.cs @@ -0,0 +1,22 @@ +namespace SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance.Models +{ + /// + /// 表示会话内容存档之获取媒体文件分片接口的请求。 + /// + public class GetMediaFileBufferRequest : WechatWorkFinanceRequest + { + /// + /// 获取或设置起始分片索引。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public string? BufferIndex { get; set; } + + /// + /// 获取或设置文件 FileId。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public string FileId { get; set; } = string.Empty; + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/GetMediaFileBufferResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/GetMediaFileBufferResponse.cs new file mode 100644 index 00000000..e2058f3c --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/GetMediaFileBufferResponse.cs @@ -0,0 +1,38 @@ +namespace SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance.Models +{ + /// + /// 表示会话内容存档之获取媒体文件分片接口的响应。 + /// + public class GetMediaFileBufferResponse : WechatWorkFinanceResponse + { + /// + /// 获取或设置文件分片二进制数组。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public byte[] FileBufferBytes + { + get { return RawBytes; } + set { RawBytes = value; } + } + + /// + /// 获取或设置下一次的分片索引。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public string? NextBufferIndex { get; set; } + + /// + /// 获取或设置是否完成。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public bool IsFinished { get; set; } + + public override bool IsSuccessful() + { + return base.IsSuccessful() && FileBufferBytes?.Length > 0; + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/GetMediaFileRequest.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/GetMediaFileRequest.cs new file mode 100644 index 00000000..7636c07e --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/GetMediaFileRequest.cs @@ -0,0 +1,15 @@ +namespace SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance.Models +{ + /// + /// 表示会话内容存档之获取媒体文件接口的请求。 + /// + public class GetMediaFileRequest : WechatWorkFinanceRequest + { + /// + /// 获取或设置文件 FileId。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public string FileId { get; set; } = string.Empty; + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/GetMediaFileResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/GetMediaFileResponse.cs new file mode 100644 index 00000000..287c5a78 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Models/GetMediaFileResponse.cs @@ -0,0 +1,24 @@ +namespace SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance.Models +{ + /// + /// 表示会话内容存档之获取媒体文件接口的响应。 + /// + public class GetMediaFileResponse : WechatWorkFinanceResponse + { + /// + /// 获取或设置文件二进制数组。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public byte[] FileBytes + { + get { return RawBytes; } + set { RawBytes = value; } + } + + public override bool IsSuccessful() + { + return base.IsSuccessful() && FileBytes?.Length > 0; + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Settings/Credentials.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Settings/Credentials.cs new file mode 100644 index 00000000..ca67d074 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Settings/Credentials.cs @@ -0,0 +1,25 @@ +using System; + +namespace SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance.Settings +{ + public class Credentials + { + /// + /// 初始化客户端时 的副本。 + /// + public string CorpId { get; } + + /// + /// 初始化客户端时 的副本。 + /// + public string SecretKey { get; } + + internal Credentials(WechatWorkFinanceClientOptions options) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + CorpId = options.CorpId; + SecretKey = options.SecretKey; + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Settings/EncryptionKeyEntry.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Settings/EncryptionKeyEntry.cs new file mode 100644 index 00000000..dc5e7402 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Settings/EncryptionKeyEntry.cs @@ -0,0 +1,70 @@ +using System; + +namespace SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance.Settings +{ + /// + /// 表示一个企业微信会话内容存档的消息加解密密钥实体。 + /// + public struct EncryptionKeyEntry : IEquatable + { + /// + /// 获取版本号。 + /// + public int Version { get; } + + /// + /// 获取私钥内容(PKCS#1 PEM 格式,即 -----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY-----) + /// + public string PrivateKey { get; } + + [Newtonsoft.Json.JsonConstructor] + [System.Text.Json.Serialization.JsonConstructor] + public EncryptionKeyEntry(int version, string privateKey) + { + if (version <= 0) + throw new ArgumentException("The value of `version` can not be less than zero.", nameof(version)); + if (string.IsNullOrEmpty(privateKey)) + throw new ArgumentException("The value of `privateKey` can not be empty.", nameof(privateKey)); + if (!privateKey.Trim().StartsWith("-----BEGIN RSA PRIVATE KEY-----") || !privateKey.Trim().EndsWith("-----END RSA PRIVATE KEY-----")) + throw new ArgumentException("The value of `privateKey` is an invalid private key file content.", nameof(privateKey)); + + Version = version; + PrivateKey = privateKey; + } + + public bool Equals(EncryptionKeyEntry other) + { + return int.Equals(Version, other.Version) && + string.Equals(PrivateKey, other.PrivateKey); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (GetType() != obj.GetType()) + return false; + + return Equals((EncryptionKeyEntry)obj); + } + + public override int GetHashCode() + { +#if NETFRAMEWORK || NETSTANDARD2_0 + return (Version.GetHashCode(), PrivateKey?.GetHashCode()).GetHashCode(); +#else + return HashCode.Combine(Version.GetHashCode(), PrivateKey?.GetHashCode()); +#endif + } + + public static bool operator ==(EncryptionKeyEntry left, EncryptionKeyEntry right) + { + return left.Equals(right); + } + + public static bool operator !=(EncryptionKeyEntry left, EncryptionKeyEntry right) + { + return !left.Equals(right); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Settings/EncryptionKeyManager.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Settings/EncryptionKeyManager.cs new file mode 100644 index 00000000..dc4423d4 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/Settings/EncryptionKeyManager.cs @@ -0,0 +1,77 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance.Settings +{ + /// + /// 企业微信会话内容存档的消息加解密密钥管理器接口。 + /// + public abstract class EncryptionKeyManager + { + /// + /// 获取存储的全部消息加解密密钥实体。 + /// + /// + public abstract IEnumerable AllEntries(); + + /// + /// 添加一个消息加解密密钥实体。 + /// + /// + public abstract void AddEntry(EncryptionKeyEntry entry); + + /// + /// 根据版本号获取消息加解密密钥实体。 + /// + /// + /// + public abstract EncryptionKeyEntry? GetEntry(int version); + + /// + /// 根据版本号移除消息加解密密钥实体。 + /// + /// + /// + public abstract bool RemoveEntry(int version); + } + + /// + /// 一个基于内存实现的 。 + /// + public class InMemoryEncryptionKeyManager : EncryptionKeyManager + { + private readonly ConcurrentDictionary _dict; + + public InMemoryEncryptionKeyManager() + { + _dict = new ConcurrentDictionary(); + } + + public override IEnumerable AllEntries() + { + return _dict.Values.ToArray(); + } + + public override void AddEntry(EncryptionKeyEntry entry) + { + _dict.TryRemove(entry.Version, out _); + _dict.TryAdd(entry.Version, entry); + } + + public override EncryptionKeyEntry? GetEntry(int version) + { + if (_dict.TryGetValue(version, out var entry)) + { + return entry; + } + + return null; + } + + public override bool RemoveEntry(int version) + { + return _dict.TryRemove(version, out _); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/WechatWorkFinanceClient.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/WechatWorkFinanceClient.cs new file mode 100644 index 00000000..63dd27e6 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/WechatWorkFinanceClient.cs @@ -0,0 +1,452 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance +{ + using SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance.InteropServices; + + /// + /// 一个企业微信会话内容存档 API HTTP 客户端。 + /// + public sealed class WechatWorkFinanceClient : CommonClientBase, ICommonClient, IWechatWorkFinanceClient, IDisposable + { + private static readonly object _lockObj = new object(); + + private readonly int _timeout; + private readonly string? _proxyAddress; + private readonly string? _proxyAuthentication; + + private IntPtr _sdkPtr; + private bool _initialized; + private bool _disposed; + + /// + /// 获取当前客户端使用的企业微信会话内容存档凭证。 + /// + public Settings.Credentials Credentials { get; } + + /// + /// 获取当前客户端使用的企业微信会话内容存档消息加解密密钥管理器。 + /// + public Settings.EncryptionKeyManager EncryptionKeyManager { get; } + + /// + /// 用指定的配置项初始化 类的新实例。 + /// + /// 配置项。 + public WechatWorkFinanceClient(WechatWorkFinanceClientOptions options) + : base() + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + Credentials = new Settings.Credentials(options); + EncryptionKeyManager = options.EncryptionKeyManager; + + _timeout = options.Timeout; + _proxyAddress = options.ProxyAddress; + _proxyAuthentication = options.ProxyAuthentication; + + _sdkPtr = /* 申请用于构造 SDK 的内存空间 */ + IsRunOnWindows() ? FinanceDllWindowsPInvoker.NewSdk() : + IsRunOnLinux() ? FinanceDllLinuxPInvoker.NewSdk() : + throw new PlatformNotSupportedException(); + } + + ~WechatWorkFinanceClient() + { + Dispose(disposing: false); + } + + private static bool IsRunOnWindows() + { +#if NET471_OR_GREATER || NETSTANDARD + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); +#else + return Environment.OSVersion.Platform == PlatformID.Win32NT; +#endif + } + + private static bool IsRunOnLinux() + { +#if NET471_OR_GREATER || NETSTANDARD + return RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || + RuntimeInformation.IsOSPlatform(OSPlatform.OSX); +#elif NETFRAMEWORK + return Environment.OSVersion.Platform == PlatformID.Unix || + Environment.OSVersion.Platform == PlatformID.MacOSX; +#else + return Environment.OSVersion.Platform == PlatformID.Unix; +#endif + } + + /// + /// + /// + /// + /// + /// + public Task ExecuteGetChatRecordsAsync(Models.GetChatRecordsRequest request, CancellationToken cancellationToken = default) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + + EnsureInitialized(); + + return Task.Run(() => + { + IntPtr dataPtr = IntPtr.Zero; + Action freeDataPtr = () => + { + if (dataPtr != IntPtr.Zero) + { + if (IsRunOnWindows()) FinanceDllWindowsPInvoker.FreeSlice(dataPtr); + else if (IsRunOnLinux()) FinanceDllLinuxPInvoker.FreeSlice(dataPtr); + else Marshal.FreeHGlobal(dataPtr); + + dataPtr = IntPtr.Zero; + } + }; + cancellationToken.Register(freeDataPtr); + + dataPtr = /* 申请用于存储聊天记录数据的内存空间 */ + IsRunOnWindows() ? FinanceDllWindowsPInvoker.NewSlice() : + IsRunOnLinux() ? FinanceDllLinuxPInvoker.NewSlice() : + throw new PlatformNotSupportedException(); + + Models.GetChatRecordsResponse response = new Models.GetChatRecordsResponse(); + try + { + int ret = /* 获取聊天记录数据 */ + IsRunOnWindows() ? FinanceDllWindowsPInvoker.GetChatData(_sdkPtr, request.LastSequence, request.Limit, _proxyAddress!, _proxyAuthentication!, (request.Timeout ?? _timeout) / 1000, dataPtr) : + IsRunOnLinux() ? FinanceDllLinuxPInvoker.GetChatData(_sdkPtr, request.LastSequence, request.Limit, _proxyAddress!, _proxyAuthentication!, (request.Timeout ?? _timeout) / 1000, dataPtr) : + throw new PlatformNotSupportedException(); + if (ret == 0) + { + //int dataSize = /* 获取聊天记录数据内容长度 */ + // IsRunOnWindows() ? FinanceDllWindowsPInvoker.GetSliceLen(dataPtr) : + // IsRunOnLinux() ? FinanceDllLinuxPInvoker.GetSliceLen(dataPtr) : + // throw new PlatformNotSupportedException(); + string dataContent = /* 获取聊天记录数据内容 */ + IsRunOnWindows() ? FinanceDllWindowsPInvoker.GetContentFromSlice(dataPtr) : + IsRunOnLinux() ? FinanceDllLinuxPInvoker.GetContentFromSlice(dataPtr) : + throw new PlatformNotSupportedException(); + + response = JsonSerializer.Deserialize(dataContent); + response.RawBytes = Encoding.UTF8.GetBytes(dataContent); + } + + response.ReturnCode = ret; + return response; + } + catch (Exception ex) + { + throw new WechatWorkFinanceException("Failed to fetch chat data. Please see the inner exception for more details.", ex); + } + finally + { + freeDataPtr(); + } + }, cancellationToken); + } + + /// + /// + /// + /// + /// + /// + public Task ExecuteDecryptChatRecordAsync(Models.DecryptChatRecordRequest request, CancellationToken cancellationToken = default) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + + EnsureInitialized(); + + return Task.Run(() => + { + string encryptKey; + + try + { + Settings.EncryptionKeyEntry? encryptionKeyEntry = EncryptionKeyManager.GetEntry(request.PublicKeyVersion); + if (!encryptionKeyEntry.HasValue) + throw new WechatWorkFinanceException($"Failed to decrypt random key of the encrypted chat data, because there is no private key matched the verion: \"{request.PublicKeyVersion}\"."); + + encryptKey = Utilities.RSAUtility.DecryptWithECB( + privateKey: encryptionKeyEntry.Value.PrivateKey, + cipherText: request.EncryptedRandomKey + ); + } + catch (WechatWorkFinanceException) + { + throw; + } + catch (Exception ex) + { + throw new WechatWorkFinanceException("Failed to decrypt random key of the encrypted chat data. Please see the inner exception for more details.", ex); + } + + IntPtr dataPtr = IntPtr.Zero; + Action freeDataPtr = () => + { + if (dataPtr != IntPtr.Zero) + { + if (IsRunOnWindows()) FinanceDllWindowsPInvoker.FreeSlice(dataPtr); + else if (IsRunOnLinux()) FinanceDllLinuxPInvoker.FreeSlice(dataPtr); + else Marshal.FreeHGlobal(dataPtr); + + dataPtr = IntPtr.Zero; + } + }; + cancellationToken.Register(freeDataPtr); + + dataPtr = /* 申请用于存储聊天记录数据的内存空间 */ + IsRunOnWindows() ? FinanceDllWindowsPInvoker.NewSlice() : + IsRunOnLinux() ? FinanceDllLinuxPInvoker.NewSlice() : + throw new PlatformNotSupportedException(); + + Models.DecryptChatRecordResponse response = new Models.DecryptChatRecordResponse(); + try + { + int ret = /* 解密聊天记录数据 */ + IsRunOnWindows() ? FinanceDllWindowsPInvoker.DecryptData(_sdkPtr, encryptKey, request.EncryptedChatMessage, dataPtr) : + IsRunOnLinux() ? FinanceDllLinuxPInvoker.DecryptData(_sdkPtr, encryptKey, request.EncryptedChatMessage, dataPtr) : + throw new PlatformNotSupportedException(); + if (ret == 0) + { + //int dataSize = /* 获取聊天记录数据内容长度 */ + // IsRunOnWindows() ? FinanceDllWindowsPInvoker.GetSliceLen(dataPtr) : + // IsRunOnLinux() ? FinanceDllLinuxPInvoker.GetSliceLen(dataPtr) : + // throw new PlatformNotSupportedException(); + string dataContent = /* 获取聊天记录数据内容 */ + IsRunOnWindows() ? FinanceDllWindowsPInvoker.GetContentFromSlice(dataPtr) : + IsRunOnLinux() ? FinanceDllLinuxPInvoker.GetContentFromSlice(dataPtr) : + throw new PlatformNotSupportedException(); + + response = JsonSerializer.Deserialize(dataContent); + response.RawBytes = Encoding.UTF8.GetBytes(dataContent); + } + + response.ReturnCode = ret; + return response; + } + catch (Exception ex) + { + throw new WechatWorkFinanceException("Failed to decrypt chat data. Please see the inner exception for more details.", ex); + } + finally + { + freeDataPtr(); + } + }, cancellationToken); + } + + /// + /// + /// + /// + /// + /// + public Task ExecuteGetMediaFileBufferAsync(Models.GetMediaFileBufferRequest request, CancellationToken cancellationToken = default) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + + EnsureInitialized(); + + return Task.Run(() => + { + IntPtr dataPtr = IntPtr.Zero; + Action freeDataPtr = () => + { + if (dataPtr != IntPtr.Zero) + { + if (IsRunOnWindows()) FinanceDllWindowsPInvoker.FreeMediaData(dataPtr); + else if (IsRunOnLinux()) FinanceDllLinuxPInvoker.FreeMediaData(dataPtr); + else Marshal.FreeHGlobal(dataPtr); + + dataPtr = IntPtr.Zero; + } + }; + cancellationToken.Register(freeDataPtr); + + dataPtr = /* 申请用于存储媒体文件数据的内存空间 */ + IsRunOnWindows() ? FinanceDllWindowsPInvoker.NewMediaData() : + IsRunOnLinux() ? FinanceDllLinuxPInvoker.NewMediaData() : + throw new PlatformNotSupportedException(); + + Models.GetMediaFileBufferResponse response = new Models.GetMediaFileBufferResponse(); + try + { + int ret = /* 获取媒体文件数据 */ + IsRunOnWindows() ? FinanceDllWindowsPInvoker.GetMediaData(_sdkPtr, request.BufferIndex ?? string.Empty, request.FileId, _proxyAddress!, _proxyAuthentication!, (request.Timeout ?? _timeout) / 1000, dataPtr) : + IsRunOnLinux() ? FinanceDllLinuxPInvoker.GetMediaData(_sdkPtr, request.BufferIndex ?? string.Empty, request.FileId, _proxyAddress!, _proxyAuthentication!, (request.Timeout ?? _timeout) / 1000, dataPtr) : + throw new PlatformNotSupportedException(); + if (ret == 0) + { + int dataSize = /* 获取媒体文件数据内容长度 */ + IsRunOnWindows() ? FinanceDllWindowsPInvoker.GetDataLen(dataPtr) : + IsRunOnLinux() ? FinanceDllLinuxPInvoker.GetDataLen(dataPtr) : + throw new PlatformNotSupportedException(); + IntPtr dataContentPtr = /* 获取媒体文件数据内容 */ + IsRunOnWindows() ? FinanceDllWindowsPInvoker.GetData(dataPtr) : + IsRunOnLinux() ? FinanceDllLinuxPInvoker.GetData(dataPtr) : + throw new PlatformNotSupportedException(); + string dataNextBufferIndex = /* 获取媒体文件数据内容缓冲标识 */ + IsRunOnWindows() ? FinanceDllWindowsPInvoker.GetOutIndexBuf(dataPtr) : + IsRunOnLinux() ? FinanceDllLinuxPInvoker.GetOutIndexBuf(dataPtr) : + throw new PlatformNotSupportedException(); + int dataIsFinishFlag = /* 获取媒体文件数据内容完结标识 */ + IsRunOnWindows() ? FinanceDllWindowsPInvoker.IsMediaDataFinish(dataPtr) : + IsRunOnLinux() ? FinanceDllLinuxPInvoker.IsMediaDataFinish(dataPtr) : + throw new PlatformNotSupportedException(); + + byte[] bytes = new byte[dataSize]; + Marshal.Copy(dataContentPtr, bytes, 0, bytes.Length); + Marshal.FreeHGlobal(dataContentPtr); + + response.FileBufferBytes = bytes; + response.NextBufferIndex = dataNextBufferIndex; + response.IsFinished = dataIsFinishFlag != 0; + } + + response.ReturnCode = ret; + return response; + } + catch (Exception ex) + { + throw new WechatWorkFinanceException("Failed to get media data. Please see the inner exception for more details.", ex); + } + finally + { + freeDataPtr(); + } + }, cancellationToken); + } + + /// + /// + /// + /// + /// + /// + public async Task ExecuteGetMediaFileAsync(Models.GetMediaFileRequest request, CancellationToken cancellationToken = default) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + + EnsureInitialized(); + + const int ATTAMPT_MAX = 3; // 错误最大重试次数 + const int ATTAMPT_INTERVAL = 500; // 错误等待间隔(单位:毫秒) + int retryCount = 0; // 当前已重试次数,获取每个分片前都重置为 0 + + string fileId = request.FileId; + string? nextBufferIndex = null; + + Models.GetMediaFileResponse response = new Models.GetMediaFileResponse(); + + using (MemoryStream stream = new MemoryStream()) + { + while (true) + { + if (retryCount >= ATTAMPT_MAX) + break; + cancellationToken.ThrowIfCancellationRequested(); + + var reqBuffer = new Models.GetMediaFileBufferRequest() + { + FileId = fileId, + BufferIndex = nextBufferIndex, + Timeout = request.Timeout + }; + var resBuffer = await ExecuteGetMediaFileBufferAsync(reqBuffer, cancellationToken); + response.ReturnCode = resBuffer.ReturnCode; + + if (resBuffer.IsSuccessful()) + { + retryCount = 0; + nextBufferIndex = resBuffer.NextBufferIndex; + await stream.WriteAsync(resBuffer.FileBufferBytes, 0, resBuffer.FileBufferBytes.Length, cancellationToken); + + if (resBuffer.IsFinished) + break; + } + else + { + if (10001 == resBuffer.ReturnCode || + 10002 == resBuffer.ReturnCode || + 10003 == resBuffer.ReturnCode) + { + // 根据官方建议,这三种错误代码需要重试 + await Task.Delay(ATTAMPT_INTERVAL); + retryCount++; + continue; + } + + break; + } + } + + response.RawBytes = stream.ToArray(); + return response; + } + } + + private void EnsureInitialized() + { + if (_disposed) + throw new ObjectDisposedException(nameof(WechatWorkFinanceClient)); + + if (!_initialized) + { + lock (_lockObj) + { + if (!_initialized) + { + int ret = /* 初始化 SDK */ + IsRunOnWindows() ? FinanceDllWindowsPInvoker.Init(_sdkPtr, Credentials.CorpId, Credentials.SecretKey) : + IsRunOnLinux() ? FinanceDllLinuxPInvoker.Init(_sdkPtr, Credentials.CorpId, Credentials.SecretKey) : + throw new PlatformNotSupportedException(); + if (ret != 0) + throw new WechatWorkFinanceException($"Failed to initialize Wechat Work Finance SDK (ret: {ret})."); + + _initialized = true; + } + } + } + } + + private void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + // 释放托管资源 + } + + // 释放非托管资源 + IntPtr tmpptr = _sdkPtr; + if (tmpptr != IntPtr.Zero) + { + if (IsRunOnWindows()) FinanceDllWindowsPInvoker.DestroySdk(tmpptr); + else if (IsRunOnLinux()) FinanceDllLinuxPInvoker.DestroySdk(tmpptr); + else Marshal.FreeHGlobal(tmpptr); + + _sdkPtr = IntPtr.Zero; + } + + _disposed = true; + } + } + + public override void Dispose() + { + base.Dispose(); + + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/WechatWorkFinanceClientOptions.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/WechatWorkFinanceClientOptions.cs new file mode 100644 index 00000000..33070042 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/WechatWorkFinanceClientOptions.cs @@ -0,0 +1,40 @@ +namespace SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance +{ + /// + /// 一个用于构造 时使用的配置项。 + /// + public class WechatWorkFinanceClientOptions + { + /// + /// 获取或设置请求超时时间(单位:毫秒),建议设置为 1000 的整数倍。 + /// 默认值:30000 + /// + public int Timeout { get; set; } = 30 * 1000; + + /// + /// 获取或设置代理地址。 + /// + public string? ProxyAddress { get; set; } + + /// + /// 获取或设置代理认证信息(如账号、密码)。 + /// + public string? ProxyAuthentication { get; set; } + + /// + /// 获取或设置企业微信 CorpId。 + /// + public string CorpId { get; set; } = default!; + + /// + /// 获取或设置企业微信会话内容存档 SecretKey。 + /// + public string SecretKey { get; set; } = default!; + + /// + /// 获取或设置企业微信会话内容存档消息加解密密钥管理器。 + /// 默认值: + /// + public Settings.EncryptionKeyManager EncryptionKeyManager { get; set; } = new Settings.InMemoryEncryptionKeyManager(); + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/WechatWorkFinanceException.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/WechatWorkFinanceException.cs new file mode 100644 index 00000000..5dbc2fde --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/WechatWorkFinanceException.cs @@ -0,0 +1,27 @@ +using System; + +namespace SKIT.FlurlHttpClient.Wechat.Work +{ + /// + /// 当调用企业微信会话内容存档 API 出错时引发的异常。 + /// + public class WechatWorkFinanceException : WechatWorkException + { + /// + public WechatWorkFinanceException() + { + } + + /// + public WechatWorkFinanceException(string message) + : base(message) + { + } + + /// + public WechatWorkFinanceException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/WechatWorkFinanceRequest.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/WechatWorkFinanceRequest.cs new file mode 100644 index 00000000..7dd608e4 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/WechatWorkFinanceRequest.cs @@ -0,0 +1,15 @@ +namespace SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance +{ + /// + /// 表示企业微信会话内容存档 API 请求的基类。 + /// + public abstract class WechatWorkFinanceRequest : ICommonRequest + { + /// + /// 获取或设置请求超时时间(单位:毫秒)。如果不指定将使用构造 时的 参数。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public virtual int? Timeout { get; set; } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/WechatWorkFinanceResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/WechatWorkFinanceResponse.cs new file mode 100644 index 00000000..4176381e --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/WechatWorkFinanceResponse.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; + +namespace SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance +{ + /// + /// 表示企业微信会话内容存档 API 响应的基类。 + /// + public abstract class WechatWorkFinanceResponse : ICommonResponse + { + /// + /// + /// + int ICommonResponse.RawStatus + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + /// + /// + /// + IDictionary ICommonResponse.RawHeaders + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + /// + /// + /// + byte[] ICommonResponse.RawBytes { get; set; } = Array.Empty(); + + /// + /// 获取原始的响应数据。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public byte[] RawBytes + { + get { return ((ICommonResponse)this).RawBytes; } + internal set { ((ICommonResponse)this).RawBytes = value; } + } + + /// + /// 获取企业微信会话内容存档 API 返回的返回值。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public int ReturnCode { get; internal set; } + + /// + /// 获取一个值,该值指示调用企业微信会话内容存档 API 是否成功(即 "ret" 值为 0)。 + /// + /// + public virtual bool IsSuccessful() + { + return ReturnCode == 0; + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/_Libs/WeWorkFinanceSdk.dll b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/_Libs/WeWorkFinanceSdk.dll new file mode 100644 index 00000000..faf2cc98 Binary files /dev/null and b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/_Libs/WeWorkFinanceSdk.dll differ diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/_Libs/libWeWorkFinanceSdk_C.so b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/_Libs/libWeWorkFinanceSdk_C.so new file mode 100644 index 00000000..6bc84a40 Binary files /dev/null and b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/_Libs/libWeWorkFinanceSdk_C.so differ diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/_Libs/libcrypto-1_1-x64.dll b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/_Libs/libcrypto-1_1-x64.dll new file mode 100644 index 00000000..cf13eedf Binary files /dev/null and b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/_Libs/libcrypto-1_1-x64.dll differ diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/_Libs/libcurl-x64.dll b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/_Libs/libcurl-x64.dll new file mode 100644 index 00000000..cceb7786 Binary files /dev/null and b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/_Libs/libcurl-x64.dll differ diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/_Libs/libssl-1_1-x64.dll b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/_Libs/libssl-1_1-x64.dll new file mode 100644 index 00000000..413262d3 Binary files /dev/null and b/src/SKIT.FlurlHttpClient.Wechat.Work/SDK/Finance/_Libs/libssl-1_1-x64.dll differ diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/SKIT.FlurlHttpClient.Wechat.Work.csproj b/src/SKIT.FlurlHttpClient.Wechat.Work/SKIT.FlurlHttpClient.Wechat.Work.csproj index ab603c58..78a0bba4 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Work/SKIT.FlurlHttpClient.Wechat.Work.csproj +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/SKIT.FlurlHttpClient.Wechat.Work.csproj @@ -1,7 +1,7 @@ - net461; netstandard2.0; net6.0 + net461; net471; netstandard2.0; net6.0 8.0 enable true @@ -33,13 +33,21 @@ - - - - + + PreserveNewest + %(Filename)%(Extension) + + + + + + + + + diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/Utilities/RSAUtility.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/Utilities/RSAUtility.cs new file mode 100644 index 00000000..9e672516 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/Utilities/RSAUtility.cs @@ -0,0 +1,98 @@ +using System; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities.IO; +using Org.BouncyCastle.Utilities; +using System.Security.Cryptography.X509Certificates; +using System.Xml.Linq; + +namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities +{ + /// + /// RSA 算法工具类。 + /// + public static class RSAUtility + { + private const string RSA_CIPHER_ALGORITHM_NONE = "RSA/ECB"; + private const string RSA_CIPHER_PADDING_PKCS1 = "PKCS1PADDING"; + + private static byte[] ConvertPrivateKeyPkcs1PemToByteArray(string privateKey) + { + using (TextReader textReader = new StringReader(privateKey)) + using (PemReader pemReader = new PemReader(textReader)) + { + AsymmetricCipherKeyPair cipherKeyPair = (AsymmetricCipherKeyPair)pemReader.ReadObject(); + using (TextWriter textWriter = new StringWriter()) + using (PemWriter pemWriter = new PemWriter(textWriter)) + { + Pkcs8Generator pkcs8 = new Pkcs8Generator(cipherKeyPair.Private); + pemWriter.WriteObject(pkcs8); + pemWriter.Writer.Close(); + + privateKey = textWriter.ToString(); + privateKey = privateKey + .Replace("-----BEGIN PRIVATE KEY-----", string.Empty) + .Replace("-----END PRIVATE KEY-----", string.Empty); + privateKey = Regex.Replace(privateKey, "\\s+", string.Empty); + return Convert.FromBase64String(privateKey); + } + } + } + + private static RsaKeyParameters ParsePrivateKeyPemToPrivateKeyParameters(byte[] privateKeyBytes) + { + return (RsaKeyParameters)PrivateKeyFactory.CreateKey(privateKeyBytes); + } + + private static byte[] DecryptWithECB(RsaKeyParameters rsaPrivateKeyParams, byte[] cipherBytes, string paddingMode) + { + IBufferedCipher cipher = CipherUtilities.GetCipher($"{RSA_CIPHER_ALGORITHM_NONE}/{paddingMode}"); + cipher.Init(false, rsaPrivateKeyParams); + return cipher.DoFinal(cipherBytes); + } + + /// + /// 使用私钥基于 ECB 模式解密数据。 + /// + /// PKCS#1 私钥字节数据。 + /// 待解密的数据字节数据。 + /// 填充模式。(默认值:) + /// 解密后的数据字节数组。 + public static byte[] DecryptWithECB(byte[] privateKeyBytes, byte[] cipherBytes, string paddingMode = RSA_CIPHER_PADDING_PKCS1) + { + if (privateKeyBytes == null) throw new ArgumentNullException(nameof(privateKeyBytes)); + if (cipherBytes == null) throw new ArgumentNullException(nameof(cipherBytes)); + + RsaKeyParameters rsaPrivateKeyParams = ParsePrivateKeyPemToPrivateKeyParameters(privateKeyBytes); + return DecryptWithECB(rsaPrivateKeyParams, cipherBytes, paddingMode); + } + + /// + /// 使用私钥基于 ECB 模式解密数据。 + /// + /// PKCS#1 私钥(PEM 格式)。 + /// 经 Base64 编码的待解密数据。 + /// 填充模式。(默认值:) + /// 解密后的文本数据。 + public static string DecryptWithECB(string privateKey, string cipherText, string paddingMode = RSA_CIPHER_PADDING_PKCS1) + { + if (privateKey == null) throw new ArgumentNullException(nameof(privateKey)); + if (cipherText == null) throw new ArgumentNullException(nameof(cipherText)); + + byte[] privateKeyBytes = ConvertPrivateKeyPkcs1PemToByteArray(privateKey); + byte[] cipherBytes = Convert.FromBase64String(cipherText); + byte[] plainBytes = DecryptWithECB(privateKeyBytes, cipherBytes, paddingMode); + return Encoding.UTF8.GetString(plainBytes); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/WechatWorkEndpoints.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/WechatWorkEndpoints.cs index c149567e..c746e9ed 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Work/WechatWorkEndpoints.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/WechatWorkEndpoints.cs @@ -1,4 +1,4 @@ -namespace SKIT.FlurlHttpClient.Wechat.Work +namespace SKIT.FlurlHttpClient.Wechat.Work { /// /// 企业微信 API 接口域名。 diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/WechatWorkException.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/WechatWorkException.cs index 73ddb8ad..4adcb232 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Work/WechatWorkException.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/WechatWorkException.cs @@ -1,4 +1,5 @@ -using System; +using System; +using Newtonsoft.Json; namespace SKIT.FlurlHttpClient.Wechat.Work { diff --git a/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestCase_FinanceSdkTests.cs b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestCase_FinanceSdkTests.cs new file mode 100644 index 00000000..fa45c8e0 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestCase_FinanceSdkTests.cs @@ -0,0 +1,1125 @@ +using System.Threading.Tasks; +using Flurl.Http.Configuration; +using Xunit; + +namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests +{ + using SKIT.FlurlHttpClient; + using SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance; + using SKIT.FlurlHttpClient.Wechat.Work.SDK.Finance.Models; + + public class TestCase_FinanceSdkTests + { + [Fact(DisplayName = "测试用例:聊天记录 - 会话内容存档 SDK")] + public async Task TestFinanceSdk() + { + var options = new WechatWorkFinanceClientOptions() + { + CorpId = TestConfigs.WechatCorpId, + SecretKey = TestConfigs.WechatFinanceSecretKey + }; + options.EncryptionKeyManager.AddEntry(new SDK.Finance.Settings.EncryptionKeyEntry(1, TestConfigs.WechatFinanceEncryptionPrivateKey)); + using var client = new WechatWorkFinanceClient(options); + + var reqGetChatRecords = new GetChatRecordsRequest(); + var resGetChatRecords = await client.ExecuteGetChatRecordsAsync(reqGetChatRecords); + + Assert.True(resGetChatRecords.IsSuccessful()); + Assert.NotEmpty(resGetChatRecords.RecordList); + + foreach (var record in resGetChatRecords.RecordList) + { + var reqDecryptChatRecord = new DecryptChatRecordRequest() + { + PublicKeyVersion = record.PublicKeyVersion, + EncryptedRandomKey = record.EncryptedRandomKey, + EncryptedChatMessage = record.EncryptedChatMessage + }; + var resDecryptChatRecord = await client.ExecuteDecryptChatRecordAsync(reqDecryptChatRecord); + + Assert.True(resDecryptChatRecord.IsSuccessful()); + Assert.NotNull(resDecryptChatRecord.MessageId); + Assert.NotNull(resDecryptChatRecord.Action); + Assert.NotNull(resDecryptChatRecord.FromUserId); + Assert.NotNull(resDecryptChatRecord.MessageType); + + switch (resDecryptChatRecord.MessageType) + { + case "image": + { + var reqGetMediaFile = new GetMediaFileRequest() + { + FileId = resDecryptChatRecord.MessageContentForImage!.FileId + }; + var resGetMediaFile = await client.ExecuteGetMediaFileAsync(reqGetMediaFile); + + Assert.True(resGetMediaFile.IsSuccessful()); + Assert.Equal(resDecryptChatRecord.MessageContentForImage!.FileSize, resGetMediaFile.FileBytes.Length); + } + break; + + case "voice": + { + var reqGetMediaFile = new GetMediaFileRequest() + { + FileId = resDecryptChatRecord.MessageContentForVoice!.FileId + }; + var resGetMediaFile = await client.ExecuteGetMediaFileAsync(reqGetMediaFile); + + Assert.True(resGetMediaFile.IsSuccessful()); + Assert.Equal(resDecryptChatRecord.MessageContentForVoice!.FileSize, resGetMediaFile.FileBytes.Length); + } + break; + + case "video": + { + var reqGetMediaFile = new GetMediaFileRequest() + { + FileId = resDecryptChatRecord.MessageContentForVideo!.FileId + }; + var resGetMediaFile = await client.ExecuteGetMediaFileAsync(reqGetMediaFile); + + Assert.True(resGetMediaFile.IsSuccessful()); + Assert.Equal(resDecryptChatRecord.MessageContentForVideo!.FileSize, resGetMediaFile.FileBytes.Length); + } + break; + + case "file": + { + var reqGetMediaFile = new GetMediaFileRequest() + { + FileId = resDecryptChatRecord.MessageContentForFile!.FileId + }; + var resGetMediaFile = await client.ExecuteGetMediaFileAsync(reqGetMediaFile); + + Assert.True(resGetMediaFile.IsSuccessful()); + Assert.Equal(resDecryptChatRecord.MessageContentForFile!.FileSize, resGetMediaFile.FileBytes.Length); + } + break; + } + } + } + + [Fact(DisplayName = "测试用例:聊天记录 - 会话内容存档数据结构反序列化")] + public void TestFinanceSdkModelsDeserialization() + { + var newtonsoftJsonSerializer = new FlurlNewtonsoftJsonSerializer(); + var systemTextJsonSerializer = new FlurlSystemTextJsonSerializer(); + + #region 用例:聊天记录列表 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""errcode"": 0, + ""errmsg"": ""ok"", + ""chatdata"": [{ + ""seq"": 196, + ""msgid"": ""CAQQ2fbb4QUY0On2rYSAgAMgip/yzgs="", + ""publickey_ver"": 3, + ""encrypt_random_key"": ""ftJ+uz3n/z1DsxlkwxNgE+mL38H42/KCvN8T60gbbtPD+Rta1hKTuQPzUzO6Hzne97MgKs7FfdDxDck/v8cDT6gUVjA2tZ/M7euSD0L66opJ/IUeBtpAtvgVSD5qhlaQjvfKJc/zPMGNK2xCLFYqwmQBZXbNT7uA69Fflm512nZKW/piK2RKdYJhRyvQnA1ISxK097sp9WlEgDg250fM5tgwMjujdzr7ehK6gtVBUFldNSJS7ndtIf6aSBfaLktZgwHZ57ONewWq8GJe7WwQf1hwcDbCh7YMG8nsweEwhDfUz+u8rz9an+0lgrYMZFRHnmzjgmLwrR7B/32Qxqd79A=="", + ""encrypt_chat_msg"": ""898WSfGMnIeytTsea7Rc0WsOocs0bIAerF6de0v2cFwqo9uOxrW9wYe5rCjCHHH5bDrNvLxBE/xOoFfcwOTYX0HQxTJaH0ES9OHDZ61p8gcbfGdJKnq2UU4tAEgGb8H+Q9n8syRXIjaI3KuVCqGIi4QGHFmxWenPFfjF/vRuPd0EpzUNwmqfUxLBWLpGhv+dLnqiEOBW41Zdc0OO0St6E+JeIeHlRZAR+E13Isv9eS09xNbF0qQXWIyNUi+ucLr5VuZnPGXBrSfvwX8f0QebTwpy1tT2zvQiMM2MBugKH6NuMzzuvEsXeD+6+3VRqL"" + }] + }"; + + var model = serializer.Deserialize(json); + Assert.Equal(196, model.RecordList[0]!.Sequence); + Assert.Equal("CAQQ2fbb4QUY0On2rYSAgAMgip/yzgs=", model.RecordList[0]!.MessageId); + Assert.Equal(3, model.RecordList[0]!.PublicKeyVersion); + Assert.Equal("ftJ+uz3n/z1DsxlkwxNgE+mL38H42/KCvN8T60gbbtPD+Rta1hKTuQPzUzO6Hzne97MgKs7FfdDxDck/v8cDT6gUVjA2tZ/M7euSD0L66opJ/IUeBtpAtvgVSD5qhlaQjvfKJc/zPMGNK2xCLFYqwmQBZXbNT7uA69Fflm512nZKW/piK2RKdYJhRyvQnA1ISxK097sp9WlEgDg250fM5tgwMjujdzr7ehK6gtVBUFldNSJS7ndtIf6aSBfaLktZgwHZ57ONewWq8GJe7WwQf1hwcDbCh7YMG8nsweEwhDfUz+u8rz9an+0lgrYMZFRHnmzjgmLwrR7B/32Qxqd79A==", model.RecordList[0]!.EncryptedRandomKey); + Assert.Equal("898WSfGMnIeytTsea7Rc0WsOocs0bIAerF6de0v2cFwqo9uOxrW9wYe5rCjCHHH5bDrNvLxBE/xOoFfcwOTYX0HQxTJaH0ES9OHDZ61p8gcbfGdJKnq2UU4tAEgGb8H+Q9n8syRXIjaI3KuVCqGIi4QGHFmxWenPFfjF/vRuPd0EpzUNwmqfUxLBWLpGhv+dLnqiEOBW41Zdc0OO0St6E+JeIeHlRZAR+E13Isv9eS09xNbF0qQXWIyNUi+ucLr5VuZnPGXBrSfvwX8f0QebTwpy1tT2zvQiMM2MBugKH6NuMzzuvEsXeD+6+3VRqL", model.RecordList[0]!.EncryptedChatMessage); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = "{\"msgid\":\"MSGID\",\"action\":\"ACTION\",\"from\":\"FROM\",\"tolist\":[\"TO\"],\"roomid\":\"ROOMID\",\"msgtime\":1234567890}"; + + var model = serializer.Deserialize(json); + Assert.Equal("MSGID", model.MessageId); + Assert.Equal("ACTION", model.Action); + Assert.Equal("FROM", model.FromUserId); + Assert.Equal("TO", model.ToUserIdList![0]); + Assert.Equal("ROOMID", model.RoomId); + Assert.Equal(1234567890, model.MessageTimeMilliseconds); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 文本 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""CAQQluDa4QUY0On2rYSAgAMgzPrShAE="", + ""action"": ""send"", + ""from"": ""XuJinSheng"", + ""tolist"": [""icefog""], + ""roomid"": """", + ""msgtime"": 1547087894783, + ""msgtype"": ""text"", + ""text"": { + ""content"": ""test"" + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("text", model.MessageType); + Assert.Equal("test", model.MessageContentForText!.Content); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 图片 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""CAQQvPnc4QUY0On2rYSAgAMgooLa0Q8="", + ""action"": ""send"", + ""from"": ""XuJinSheng"", + ""tolist"": [""icefog""], + ""roomid"": """", + ""msgtime"": 0, + ""msgtype"": ""image"", + ""image"": { + ""md5sum"": ""50de8e5ae8ffe4f1df7a93841f71993a"", + ""filesize"": 70961, + ""sdkfileid"": ""FILEID"" + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("image", model.MessageType); + Assert.Equal("FILEID", model.MessageContentForImage!.FileId); + Assert.Equal("50de8e5ae8ffe4f1df7a93841f71993a", model.MessageContentForImage!.FileMD5); + Assert.Equal(70961, model.MessageContentForImage!.FileSize); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 撤回消息 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""15775510700152506326_1603875615"", + ""action"": ""recall"", + ""from"": ""kenshin"", + ""tolist"": [""wmUu0zBgAALV7ZymkcMyxvbTe8YdWxxA""], + ""roomid"": """", + ""msgtime"": 1603875615723, + ""msgtype"": ""revoke"", + ""revoke"": { + ""pre_msgid"": ""14822339130656386894_1603875600"" + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("revoke", model.MessageType); + Assert.Equal("14822339130656386894_1603875600", model.MessageContentForRevoke!.PreviousMessageId); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 同意会话聊天内容 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""8891446340739254950_1603875826"", + ""action"": ""send"", + ""from"": ""wmGAgeDQAAvQeaTqWwkMTxGMkvI7OOuQ"", + ""tolist"": [""kenshin""], + ""roomid"": """", + ""msgtime"": 1603875826656, + ""msgtype"": ""agree"", + ""agree"": { + ""userid"": ""wmGAgeDQAAvQeaTqWwkMTxGMkvI7OOuQ"", + ""agree_time"": 1603875826656 + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("agree", model.MessageType); + Assert.Equal("wmGAgeDQAAvQeaTqWwkMTxGMkvI7OOuQ", model.MessageContentForAgree!.UserId); + Assert.Equal(1603875826656, model.MessageContentForAgree!.AgreeTimeMilliseconds); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 语音 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""10958372969718811103_1603875609"", + ""action"": ""send"", + ""from"": ""wmGAgeDQAAdBjb8CK4ieMPRm7Cqm-9VA"", + ""tolist"": [""kenshin""], + ""roomid"": """", + ""msgtime"": 1603875609704, + ""msgtype"": ""voice"", + ""voice"": { + ""md5sum"": ""9db09c7fa627c9e53f17736c786a74d5"", + ""voice_size"": 6810, + ""play_length"": 10, + ""sdkfileid"": ""FILEID"" + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("voice", model.MessageType); + Assert.Equal("FILEID", model.MessageContentForVoice!.FileId); + Assert.Equal("9db09c7fa627c9e53f17736c786a74d5", model.MessageContentForVoice!.FileMD5); + Assert.Equal(6810, model.MessageContentForVoice!.FileSize); + Assert.Equal(10, model.MessageContentForVoice!.Duration); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 视频 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""17955920891003447432_1603875627"", + ""action"": ""send"", + ""from"": ""kenshin"", + ""tolist"": [""wmGAgeDQAAHuRJbt4ZQI_1cqoQcf41WQ""], + ""roomid"": """", + ""msgtime"": 1603875626823, + ""msgtype"": ""video"", + ""video"": { + ""md5sum"": ""d06fc80c01d6fbffcca3b229ba41eac6"", + ""filesize"": 15169724, + ""play_length"": 108, + ""sdkfileid"": ""FILEID"" + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("video", model.MessageType); + Assert.Equal("FILEID", model.MessageContentForVideo!.FileId); + Assert.Equal("d06fc80c01d6fbffcca3b229ba41eac6", model.MessageContentForVideo!.FileMD5); + Assert.Equal(15169724, model.MessageContentForVideo!.FileSize); + Assert.Equal(108, model.MessageContentForVideo!.Duration); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 名片 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""13714216591700685558_1603875680"", + ""action"": ""send"", + ""from"": ""kenshin"", + ""tolist"": [""wmGAgeDQAAy2Dtr0F8aK4dTuatfm-5Rg""], + ""roomid"": """", + ""msgtime"": 1603875680377, + ""msgtype"": ""card"", + ""card"": { + ""corpname"": ""微信联系人"", + ""userid"": ""wmGAgeDQAAGjFmfnP7A3j2JxQDdLNhSw"" + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("card", model.MessageType); + Assert.Equal("微信联系人", model.MessageContentForBusinessCard!.CorpName); + Assert.Equal("wmGAgeDQAAGjFmfnP7A3j2JxQDdLNhSw", model.MessageContentForBusinessCard!.UserId); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 位置 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""2641513858500683770_1603876152"", + ""action"": ""send"", + ""from"": ""icefog"", + ""tolist"": [""wmN6etBgAA0sbJ3invMvRxPQDFoq9uWA""], + ""roomid"": """", + ""msgtime"": 1603876152141, + ""msgtype"": ""location"", + ""location"": { + ""longitude"": 116.586285899, + ""latitude"": 39.911125799, + ""address"": ""北京市xxx区xxx路xxx大厦x座"", + ""title"": ""xxx管理中心"", + ""zoom"": 15 + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("location", model.MessageType); + Assert.Equal(116.586285899, model.MessageContentForLocation!.Longitude); + Assert.Equal(39.911125799, model.MessageContentForLocation!.Latitude); + Assert.Equal("北京市xxx区xxx路xxx大厦x座", model.MessageContentForLocation.Address); + Assert.Equal("xxx管理中心", model.MessageContentForLocation.Title); + Assert.Equal(15, model.MessageContentForLocation.Zoom); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 位置 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""6623217619416669654_1603875612"", + ""action"": ""send"", + ""from"": ""icef"", + ""tolist"": [""wmErxtDgAAhteCglUZH2kUt3rq431qmg""], + ""roomid"": """", + ""msgtime"": 1603875611148, + ""msgtype"": ""emotion"", + ""emotion"": { + ""type"": 1, + ""width"": 290, + ""height"": 290, + ""imagesize"": 962604, + ""md5sum"": ""94c2b0bba52cc456cb8221b248096612"", + ""sdkfileid"": ""FILEID"" + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("emotion", model.MessageType); + Assert.Equal(1, model.MessageContentForEmotion!.Type); + Assert.Equal(290, model.MessageContentForEmotion!.Width); + Assert.Equal(290, model.MessageContentForEmotion!.Height); + Assert.Equal("FILEID", model.MessageContentForEmotion.FileId); + Assert.Equal("94c2b0bba52cc456cb8221b248096612", model.MessageContentForEmotion.FileMD5); + Assert.Equal(962604, model.MessageContentForEmotion.FileSize); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 文件 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""18039699423706571225_1603875608"", + ""action"": ""send"", + ""from"": ""kens"", + ""tolist"": [""wmErxtDgAArDlFIhf76O6w4GxU81al8w""], + ""roomid"": """", + ""msgtime"": 1603875608214, + ""msgtype"": ""file"", + ""file"": { + ""md5sum"": ""18e93fc2ea884df23b3d2d3b8667b9f0"", + ""filename"": ""资料.docx"", + ""fileext"": ""docx"", + ""filesize"": 18181, + ""sdkfileid"": ""FILEID"" + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("file", model.MessageType); + Assert.Equal("资料.docx", model.MessageContentForFile!.FileName); + Assert.Equal("docx", model.MessageContentForFile!.FileExtension); + Assert.Equal("FILEID", model.MessageContentForFile.FileId); + Assert.Equal("18e93fc2ea884df23b3d2d3b8667b9f0", model.MessageContentForFile.FileMD5); + Assert.Equal(18181, model.MessageContentForFile.FileSize); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 链接 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""11788441727514772650_1603875624"", + ""action"": ""send"", + ""from"": ""kenshin"", + ""tolist"": [""0000726""], + ""roomid"": """", + ""msgtime"": 1603875624476, + ""msgtype"": ""link"", + ""link"": { + ""title"": ""邀请你加入群聊"", + ""description"": ""技术支持群,进入可查看详情"", + ""link_url"": ""https://work.weixin.qq.com/wework_admin/external_room/join/exceed?vcode=xxx"", + ""image_url"": ""https://wework.qpic.cn/wwpic/xxx/0"" + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("link", model.MessageType); + Assert.Equal("邀请你加入群聊", model.MessageContentForLink!.Title); + Assert.Equal("技术支持群,进入可查看详情", model.MessageContentForLink!.Description); + Assert.Equal("https://work.weixin.qq.com/wework_admin/external_room/join/exceed?vcode=xxx", model.MessageContentForLink.LinkUrl); + Assert.Equal("https://wework.qpic.cn/wwpic/xxx/0", model.MessageContentForLink.ImageUrl); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 小程序消息 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""11930598857592605935_1603875608"", + ""action"": ""send"", + ""from"": ""kens"", + ""tolist"": [""wmGAgeDQAAsgQetTQGqRbMxrkodpM3fA""], + ""roomid"": """", + ""msgtime"": 1603875608691, + ""msgtype"": ""weapp"", + ""weapp"": { + ""title"": ""开始聊天前请仔细阅读服务须知事项"", + ""description"": ""客户需同意存档聊天记录"", + ""username"": ""xxx@app"", + ""displayname"": ""服务须知"" + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("weapp", model.MessageType); + Assert.Equal("开始聊天前请仔细阅读服务须知事项", model.MessageContentForMiniProgram!.Title); + Assert.Equal("客户需同意存档聊天记录", model.MessageContentForMiniProgram!.Description); + Assert.Equal("xxx@app", model.MessageContentForMiniProgram.UserName); + Assert.Equal("服务须知", model.MessageContentForMiniProgram.DisplayName); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 会话记录消息 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""11354299838102555191_1603875658"", + ""action"": ""send"", + ""from"": ""ken"", + ""tolist"": [""icef""], + ""roomid"": """", + ""msgtime"": 1603875657905, + ""msgtype"": ""chatrecord"", + ""chatrecord"": { + ""title"": ""群聊"", + ""item"": [{ + ""type"": ""ChatRecordText"", + ""msgtime"": 1603875610, + ""content"": ""{\""content\"":\""test\""}"", + ""from_chatroom"": false + }, { + ""type"": ""ChatRecordText"", + ""msgtime"": 1603875620, + ""content"": ""{\""content\"":\""test2\""}"", + ""from_chatroom"": false + }] + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("chatrecord", model.MessageType); + Assert.Equal("群聊", model.MessageContentForChatRecord!.Title); + Assert.Equal("ChatRecordText", model.MessageContentForChatRecord!.RecordList[0]!.MessageType); + Assert.Equal("{\"content\":\"test\"}", model.MessageContentForChatRecord!.RecordList[0]!.MessageContentJson); + Assert.Equal(1603875610, model.MessageContentForChatRecord!.RecordList[0]!.MessageTimestamp); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 待办消息 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""11354299838102555191_1603875658"", + ""action"": ""send"", + ""from"": ""ken"", + ""tolist"": [""icef""], + ""roomid"": """", + ""msgtime"": 1603875657905, + ""msgtype"": ""todo"", + ""todo"": { + ""title"": ""来源文本"", + ""content"": ""具体内容"" + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("todo", model.MessageType); + Assert.Equal("来源文本", model.MessageContentForTodo!.Title); + Assert.Equal("具体内容", model.MessageContentForTodo!.Content); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 投票消息 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""11354299838102555191_1603875658"", + ""action"": ""send"", + ""from"": ""ken"", + ""tolist"": [""icef""], + ""roomid"": """", + ""msgtime"": 1603875657905, + ""msgtype"": ""vote"", + ""vote"": { + ""votetitle"": ""投票主题"", + ""voteitem"": [""投票选项""], + ""votetype"": 101, + ""voteid"": ""1603875610"" + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("vote", model.MessageType); + Assert.Equal(101, model.MessageContentForVote!.Type); + Assert.Equal("1603875610", model.MessageContentForVote!.VoteId); + Assert.Equal("投票主题", model.MessageContentForVote!.Title); + Assert.Equal("投票选项", model.MessageContentForVote!.Options[0]!); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 填表消息 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""2500536226619379797_1576034482"", + ""action"": ""send"", + ""from"": ""nick"", + ""tolist"": [""XuJinSheng"", ""15108264797""], + ""roomid"": ""wrjc7bDwYAOAhf9quEwRRxyyoMm0QAAA"", + ""msgtime"": 1576034482344, + ""msgtype"": ""collect"", + ""collect"": { + ""room_name"": ""这是一个群"", + ""creator"": ""nick"", + ""create_time"": ""2019-12-11 11:21:22"", + ""title"": ""这是填表title"", + ""details"": [{ + ""id"": 1, + ""ques"": ""表项1,文本"", + ""type"": ""Text"" + }, { + ""id"": 2, + ""ques"": ""表项2,数字"", + ""type"": ""Number"" + }, { + ""id"": 3, + ""ques"": ""表项3,日期"", + ""type"": ""Date"" + }, { + ""id"": 4, + ""ques"": ""表项4,时间"", + ""type"": ""Time"" + }] + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("collect", model.MessageType); + Assert.Equal("这是一个群", model.MessageContentForCollect!.RoomName); + Assert.Equal("nick", model.MessageContentForCollect!.CreatorName); + Assert.Equal("2019-12-11 11:21:22", model.MessageContentForCollect!.CreateTimeString); + Assert.Equal("这是填表title", model.MessageContentForCollect!.Title); + Assert.Equal(1, model.MessageContentForCollect!.DetailList[0]!.ID); + Assert.Equal("Text", model.MessageContentForCollect!.DetailList[0]!.Type); + Assert.Equal("表项1,文本", model.MessageContentForCollect!.DetailList[0]!.Question); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 红包消息 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""333590477316965370_1603877439"", + ""action"": ""send"", + ""from"": ""kens"", + ""tolist"": [""1000000444696""], + ""roomid"": """", + ""msgtime"": 1603877439038, + ""msgtype"": ""redpacket"", + ""redpacket"": { + ""type"": 1, + ""wish"": ""恭喜发财,大吉大利"", + ""totalcnt"": 10, + ""totalamount"": 3000 + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("redpacket", model.MessageType); + Assert.Equal(1, model.MessageContentForRedPacket!.Type); + Assert.Equal("恭喜发财,大吉大利", model.MessageContentForRedPacket!.Wishing); + Assert.Equal(10, model.MessageContentForRedPacket!.TotalCount); + Assert.Equal(3000, model.MessageContentForRedPacket!.TotalAmount); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 会议邀请消息 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""5935786683775673543_1603877328"", + ""action"": ""send"", + ""from"": ""ken"", + ""tolist"": [""icef"", ""test""], + ""roomid"": ""wr2vOpDgAAN4zVWKbS"", + ""msgtime"": 1603877328914, + ""msgtype"": ""meeting"", + ""meeting"": { + ""topic"": ""夕会"", + ""starttime"": 1603877400, + ""endtime"": 1603881000, + ""address"": ""地址"", + ""remarks"": ""备注"", + ""meetingtype"": 102, + ""meetingid"": 1210342560, + ""status"": 1 + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("meeting", model.MessageType); + Assert.Equal(102, model.MessageContentForMeeting!.Type); + Assert.Equal(1210342560, model.MessageContentForMeeting!.MeetingId); + Assert.Equal("夕会", model.MessageContentForMeeting!.Topic); + Assert.Equal(1603877400, model.MessageContentForMeeting!.StartTimestamp); + Assert.Equal(1603881000, model.MessageContentForMeeting!.EndTimestamp); + Assert.Equal("地址", model.MessageContentForMeeting!.Address); + Assert.Equal("备注", model.MessageContentForMeeting!.Remark); + Assert.Equal(1, model.MessageContentForMeeting!.Status); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 切换企业日志 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""125289002219525886280"", + ""action"": ""switch"", + ""time"": 1554119421840, + ""user"": ""XuJinSheng"" + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("XuJinSheng", model.ExtensionData!["user"]!.ToString()); + Assert.Equal("1554119421840", model.ExtensionData!["time"]!.ToString()); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 在线文档消息 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""9732089160923053207_1603877765"", + ""action"": ""send"", + ""from"": ""ken"", + ""tolist"": [""icef"", ""test""], + ""roomid"": ""wrJawBCQAAStr3jxVxEH"", + ""msgtime"": 1603877765291, + ""msgtype"": ""docmsg"", + ""doc"": { + ""title"": ""测试&演示客户"", + ""doc_creator"": ""test"", + ""link_url"": ""https://doc.weixin.qq.com/txdoc/excel?docid=xxx"" + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("docmsg", model.MessageType); + Assert.Equal("测试&演示客户", model.MessageContentForDocument!.Title); + Assert.Equal("test", model.MessageContentForDocument!.CreatorUserId); + Assert.Equal("https://doc.weixin.qq.com/txdoc/excel?docid=xxx", model.MessageContentForDocument!.LinkUrl); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - Markdown 格式消息 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""7546287934688259248_1603875715"", + ""action"": ""send"", + ""from"": ""ken"", + ""tolist"": [""icef"", ""test""], + ""roomid"": ""wr0SfLCgAAgCaCPeM33UNe"", + ""msgtime"": 1603875715782, + ""msgtype"": ""markdown"", + ""info"": { + ""content"": ""请前往系统查看,谢谢。"" + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("markdown", model.MessageType); + Assert.Equal("请前往系统查看,谢谢。", model.MessageContentForInfo!.MarkdownContent); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 日程消息 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""2345881211604379705_1603877680"", + ""action"": ""send"", + ""from"": ""ken"", + ""tolist"": [""icef"", ""test""], + ""roomid"": ""wr2LO0CAAAFrTZCGWWAxBA"", + ""msgtime"": 1603877680795, + ""msgtype"": ""calendar"", + ""calendar"": { + ""title"": ""xxx业绩复盘会"", + ""creatorname"": ""test"", + ""attendeename"": [""aaa"", ""bbb""], + ""starttime"": 1603882800, + ""endtime"": 1603886400, + ""place"": ""地点"", + ""remarks"": ""备注"" + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("calendar", model.MessageType); + Assert.Equal("xxx业绩复盘会", model.MessageContentForCalendar!.Title); + Assert.Equal("test", model.MessageContentForCalendar!.CreatorName); + Assert.Equal("aaa", model.MessageContentForCalendar!.AttendeeNameList[0]); + Assert.Equal(1603882800, model.MessageContentForCalendar!.StartTimestamp); + Assert.Equal(1603886400, model.MessageContentForCalendar!.EndTimestamp); + Assert.Equal("地点", model.MessageContentForCalendar!.Place); + Assert.Equal("备注", model.MessageContentForCalendar!.Remark); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 混合消息 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""DAQQluDa4QUY0On4kYSABAMgzPrShAE="", + ""action"": ""send"", + ""from"": ""HeMiao"", + ""tolist"": [""HeChangTian"", ""LiuZeYu""], + ""roomid"": ""wr_tZ2BwAAUwHpYMwy9cIWqnlU3Hzqfg"", + ""msgtime"": 1577414359072, + ""msgtype"": ""mixed"", + ""mixed"": { + ""item"": [{ + ""type"": ""text"", + ""content"": ""{\""content\"":\""你好[微笑]\\n\""}"" + }, { + ""type"": ""image"", + ""content"": ""{\""md5sum\"":\""368b6c18c82e6441bfd89b343e9d2429\"",\""filesize\"":13177,\""sdkfileid\"":\""CtYBMzA2OTAyMDEwMjA0NjIzMDYwMDIwMTAwMDWwNDVmYWY4Y2Q3MDIwMzBmNTliMTAyMDQwYzljNTQ3NzAyMDQ1ZTA1NmFlMjA0MjQ2NjM0NjIzNjY2MzYzNTMyMmQzNzYxMzQ2NDJkMzQ2MjYxNjQyZDM4MzMzMzM4MmQ3MTYyMzczMTM4NjM2NDYxMzczMjY2MzkwMjAxMDAwMjAzMDIwMDEwMDQxMDM2OGI2YzE4YzgyZTY0NDFiZmQ4OWIyNDNlOWQyNDI4MDIwMTAyMDIwMTAwMDQwMBI4TkRkZk2UWTRPRGcxTVRneE5URTFNRGc1TVY4eE1UTTFOak0yTURVeFh6RTFOemMwTVRNek5EYz0aIDQzMTY5NDFlM2MxZDRmZjhhMjEwY2M0NDQzZGUXOTEy\""}"" + }] + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("mixed", model.MessageType); + Assert.Equal("text", model.MessageContentForMixed!.MessageList[0]!.Type); + Assert.Equal("{\"content\":\"你好[微笑]\\n\"}", model.MessageContentForMixed!.MessageList[0]!.ContentJson); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 音频存档消息 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""17952229780246929345_1594197637"", + ""action"": ""send"", + ""from"": ""wo137MCgAAYW6pIiKKrDe5SlzEhSgwbA"", + ""tolist"": [""wo137MCgAAYW6pIiKKrDe5SlzEhSgwbA""], + ""msgtime"": 1594197581203, + ""msgtype"": ""meeting_voice_call"", + ""voiceid"": ""grb8a4c48a3c094a70982c518d55e40557"", + ""meeting_voice_call"": { + ""endtime"": 1594197635, + ""sdkfileid"": ""FILEID"", + ""demofiledata"": [{ + ""filename"": ""65eb1cdd3e7a3c1740ecd74220b6c627.docx"", + ""demooperator"": ""wo137MCgAAYW6pIiKKrDe5SlzEhSgwbA"", + ""starttime"": 1594197599, + ""endtime"": 1594197609 + }], + ""sharescreendata"": [{ + ""share"": ""wo137MCgAAYW6pIiKKrDe5SlzEhSgwbA"", + ""starttime"": 1594197624, + ""endtime"": 1594197624 + }] + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("meeting_voice_call", model.MessageType); + Assert.Equal("grb8a4c48a3c094a70982c518d55e40557", model.VoiceId); + Assert.Equal(1594197635, model.MessageContentForMeetingVoiceCall!.EndTimestamp); + Assert.Equal("FILEID", model.MessageContentForMeetingVoiceCall!.FileId); + Assert.Equal("65eb1cdd3e7a3c1740ecd74220b6c627.docx", model.MessageContentForMeetingVoiceCall!.ShareFileDataList![0]!.FileName); + Assert.Equal("wo137MCgAAYW6pIiKKrDe5SlzEhSgwbA", model.MessageContentForMeetingVoiceCall!.ShareFileDataList![0]!.OperatorUserId); + Assert.Equal(1594197599, model.MessageContentForMeetingVoiceCall!.ShareFileDataList![0]!.StartTimestamp); + Assert.Equal(1594197609, model.MessageContentForMeetingVoiceCall!.ShareFileDataList![0]!.EndTimestamp); + Assert.Equal("wo137MCgAAYW6pIiKKrDe5SlzEhSgwbA", model.MessageContentForMeetingVoiceCall!.ShareScreenDataList![0]!.SharerUserId); + Assert.Equal(1594197624, model.MessageContentForMeetingVoiceCall!.ShareScreenDataList![0]!.StartTimestamp); + Assert.Equal(1594197624, model.MessageContentForMeetingVoiceCall!.ShareScreenDataList![0]!.EndTimestamp); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 音频共享文档消息 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""16527954622422422847_1594199256"", + ""action"": ""send"", + ""from"": ""18002520162"", + ""tolist"": [""wo137MCgAAYW6pIiKKrDe5SlzEhSgwbA""], + ""msgtime"": 1594199235014, + ""msgtype"": ""voip_doc_share"", + ""voipid"": ""gr2751c98b19300571f8afb3b74514bd32"", + ""voip_doc_share"": { + ""filename"": ""欢迎使用微盘.pdf"", + ""md5sum"": ""ff893900f24e55e216e617a40e5c4648"", + ""filesize"": 4400654, + ""sdkfileid"": ""FILEID"" + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("voip_doc_share", model.MessageType); + Assert.Equal("gr2751c98b19300571f8afb3b74514bd32", model.VoIPId); + Assert.Equal("欢迎使用微盘.pdf", model.MessageContentForVoIPDocumentShare!.FileName); + Assert.Equal("FILEID", model.MessageContentForVoIPDocumentShare!.FileId); + Assert.Equal("ff893900f24e55e216e617a40e5c4648", model.MessageContentForVoIPDocumentShare!.FileMD5); + Assert.Equal(4400654, model.MessageContentForVoIPDocumentShare!.FileSize); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 互通红包消息 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""8632214264349267353_1603786184"", + ""action"": ""send"", + ""from"": ""woJ7ijBwAAmqwojT8r_DaNMbr_NAvaag"", + ""tolist"": [""woJ7ijBwAA6SjS_sIyPLZtyEPJlT7Cfw"", ""tiny-six768""], + ""roomid"": ""wrJ7ijBwAAG1vly_DzVI72Ghc-PtA5Dw"", + ""msgtime"": 1603786183955, + ""msgtype"": ""external_redpacket"", + ""redpacket"": { + ""type"": 1, + ""wish"": ""恭喜发财,大吉大利"", + ""totalcnt"": 2, + ""totalamount"": 20 + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("external_redpacket", model.MessageType); + Assert.Equal(1, model.MessageContentForRedPacket!.Type); + Assert.Equal("恭喜发财,大吉大利", model.MessageContentForRedPacket!.Wishing); + Assert.Equal(2, model.MessageContentForRedPacket!.TotalCount); + Assert.Equal(20, model.MessageContentForRedPacket!.TotalAmount); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 视频号消息 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""5702551662099334532_1619511584_external"", + ""action"": ""send"", + ""from"": ""yangzhu1"", + ""tolist"": [""wmJSb5CgAA4aWXWndJspQGpJMDbsMwMA""], + ""roomid"": """", + ""msgtime"": 1619511584444, + ""msgtype"": ""sphfeed"", + ""sphfeed"": { + ""feed_type"": 4, + ""sph_name"": ""云游天地旅行家"", + ""feed_desc"": ""瑞士丨盖尔默缆车,名副其实的过山车~\n\n#旅行#风景#热门"" + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("sphfeed", model.MessageType); + Assert.Equal(4, model.MessageContentForChannelsFeed!.FeedType); + Assert.Equal("云游天地旅行家", model.MessageContentForChannelsFeed!.ChannelsNickName); + Assert.Equal("瑞士丨盖尔默缆车,名副其实的过山车~\n\n#旅行#风景#热门", model.MessageContentForChannelsFeed!.Description); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 音视频通话 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""10950316726562067988_1666601563058"", + ""action"": ""send"", + ""from"": ""icef"", + ""tolist"": [""WoBeiQi...""], + ""roomid"": """", + ""msgtime"": 1666601558876, + ""msgtype"": ""voiptext"", + ""info"": { + ""callduration"": 9, + ""invitetype"": 2 + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("voiptext", model.MessageType); + Assert.Equal(9, model.MessageContentForInfo!.VoIPCallDuration); + Assert.Equal(2, model.MessageContentForInfo!.VoIPInviteType); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + + #region 用例:聊天记录 - 微盘文件 + { + static void AssertModelWithJsonSerializer(ISerializer serializer) + { + const string json = @"{ + ""msgid"": ""904076622482680588_1666602581569_external"", + ""action"": ""send"", + ""from"": ""WoBeiQi..."", + ""tolist"": [""wmYKhyDwAAxda9Vrt7e6h0CgRhRVlGdQ""], + ""roomid"": """", + ""msgtime"": 1666602577356, + ""msgtype"": ""qydiskfile"", + ""info"": { + ""filename"": "".sys.log"" + } + }"; + + var model = serializer.Deserialize(json); + Assert.Equal("qydiskfile", model.MessageType); + Assert.Equal(".sys.log", model.MessageContentForInfo!.WedriveFileName); + } + + AssertModelWithJsonSerializer(newtonsoftJsonSerializer); + AssertModelWithJsonSerializer(systemTextJsonSerializer); + } + #endregion + } + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestCase_ToolsRSAUtilityTests.cs b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestCase_ToolsRSAUtilityTests.cs new file mode 100644 index 00000000..5cd164f7 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestCase_ToolsRSAUtilityTests.cs @@ -0,0 +1,21 @@ +using Xunit; + +namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests +{ + public class TestCase_ToolsRSAUtilityTests + { + // 此处测试的 RSA 公钥/私钥是自签名生成的,仅供执行 RSA 相关的单元测试,不能用于调用企业微信 API + private const string RSA_PEM_PUBLIC_KEY = "-----BEGIN RSA PUBLIC KEY-----MIIBCgKCAQEAuwQaAJGSMda9ESGhyY2PPuVds8OjoqyRi29IEQgXJ03Bu/o6KjZVbPQT4n3WncrT0c92zA6lazJDbaXQkHlvdDpOo3FqBvczDDT0jveCfg7azeOmRxHE8P/iWOkQm+Dhk5hnmfxqtX7pzu2quzuGt9JH4FxPhNEOkn4/uRn+1qo/KrrU2Db09gm3aPjOWTT5XEVD9tPNTsZr/vaKYGCQKTqeRWhDGL3JAgyyLgGyGTAOTt0gl0MPG/O6omwELTVQdzXqyRrKgx0tEhIKoeYBVPKbWOTJyXsRO9dcfXKu56jry9QyqjFHcVtfCuphOaFIhGUEDYuAIJsqBKJqrSZoVQIDAQAB-----END RSA PUBLIC KEY-----"; + private const string RSA_PEM_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----MIIEogIBAAKCAQEAuwQaAJGSMda9ESGhyY2PPuVds8OjoqyRi29IEQgXJ03Bu/o6KjZVbPQT4n3WncrT0c92zA6lazJDbaXQkHlvdDpOo3FqBvczDDT0jveCfg7azeOmRxHE8P/iWOkQm+Dhk5hnmfxqtX7pzu2quzuGt9JH4FxPhNEOkn4/uRn+1qo/KrrU2Db09gm3aPjOWTT5XEVD9tPNTsZr/vaKYGCQKTqeRWhDGL3JAgyyLgGyGTAOTt0gl0MPG/O6omwELTVQdzXqyRrKgx0tEhIKoeYBVPKbWOTJyXsRO9dcfXKu56jry9QyqjFHcVtfCuphOaFIhGUEDYuAIJsqBKJqrSZoVQIDAQABAoIBAFEqL4rNovBkbTvxJ8FM4b1/WFJ7dxpT4Prt+g4CP+I7+ff2WqYVXK/jonmq+akT7Shi7QEU3jAO6Xq9+y2otnlwEM8YmtaZFJuYpAckXosNMWMoCPNRP/MEax0BUccFK4GeJGCNT1aj1R+MwItwA0DmT3GNPqm6/aMQjeFs6dAJ500SUVlWfMGnO3c40XGq3bHHNfTdZsdRa5NzlCViRKtc6vgoPf4iOHGE30da771XJ06iZb/2iLYcZyWlhQMU5vBUYyVd4oykVldNuGcrLQIffJsVR7Gq/aHEujQ6WSAmd2VZwGYRLxBCXVm/LGsaiw1AffAgLEazxLMBmFfWg2ECgYEA4TC6wE1GVacft6D3UsLUcKxTLIrqAE23RI6ky5wT+3MDlMIwWRLBdJO9zTQ2MG3EgruVKXgE6GEEM4cLPmhaxg1YrpRoLBr+t78lR7WwU+i9dM8GrJwoIqnCXjF1sjKa05/iEtZHbTMaXuSirHHMjEoW40I3N6ygLd4Bewzct70CgYEA1JpR4/9TiVY9k4ml5/dvatzr+4mVplTqJ3U5Ie8lhwaAtj+ouXsERaLwpLp72gP+m1rjDHoU8zuYnfVqxm35GNf7Wv1YOlpXHcebMWLkvBc6112KmcnxmSiRAHv94AAT2YxSRPu57ITlB0biUIWiCxUlevbavLsn1vHtpx1S0HkCgYBDF8esX9miz2ZNybGmgNHWuCEX1lOdv4no7S8AUwJJGp1ohursvv/Qgew85V930lyILudkMZQUwEMGLygUcfcJpxRS/3iCG5DkohizYtikR2WbFcuBRg1XNojok4fjjdw/TRWIUzt4t48V0rz87/LnoXNsRmA5QD+BKvH5/X0NaQKBgD8oXP17Y0igSwiiUpv3oKzBVoVSGRfhj/IK298d2SsknmYFwUzgo6NARXbaQ2K/3wot1NdnCQQ9BxidyIuMLfzYZL5iFqy3G7woCQ2B0GukBwHlsv/+wvv51iGrs/6wZzUwf1wo39HIpPUldKPxHvNl482EufRpMOuk0THc/zYZAoGAROAO+sObNRQilQ4iKIO/8VQBoHnde6WF1Z7FNsQlxZxJrzZrO82OIWOPucaPNNOn0I/V4ttaFo7GSWvRX16ADvA0dNMXuB+5syGt5dgDPCOtTXzPZq0Twzwgc5uyknkS1cn9Gt+a0oZ9/+zGzjKK2tcX+HwM7r6bTaxVNzjd6Hg=-----END RSA PRIVATE KEY-----"; + + [Fact(DisplayName = "测试用例:使用 RSA 私钥解密")] + public void TestRSADecrypt() + { + string cipherText = "ewwZ8LmXVJpkJpj/JWcz16L4bePAGcf3Fi2EKyC6AS3JsF5u4aku7iOYqtcAczjoYwE1fqSadRd6YTrWr3tLP3uWFYmhqthQoaAcjmQS0vHYRFeS1V7q5hbziVLRp7C42S4YrvqXAmSmUyjPUXG5tXFVchARVkTr1F53HGoPP+iBg+i8y0uJK4FgiuKraFgdtKofv/k5/30xKzRHxdLFCFt1rF7wL+Hk/7Bl0tFZM/rfhmuvwbf46zWhxKKviAge+61tEot4QCSBLnAFpPuSQsTOOSOrlCl92DwW54dWdlWwhqkTVHdm6pXEdUE66y1yoZkXfpqjnONjta0njqN/Jw=="; + string actualPlain = Utilities.RSAUtility.DecryptWithECB(RSA_PEM_PRIVATE_KEY, cipherText); + string expectedPlain = "RsaDecryptTest"; + + Assert.Equal(expectedPlain, actualPlain); + } + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestCase_WxMsgCryptorTests.cs b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestCase_ToolsWxMsgCryptorTests.cs similarity index 99% rename from test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestCase_WxMsgCryptorTests.cs rename to test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestCase_ToolsWxMsgCryptorTests.cs index f6c201b5..cd7d7160 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestCase_WxMsgCryptorTests.cs +++ b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestCase_ToolsWxMsgCryptorTests.cs @@ -2,7 +2,7 @@ using Xunit; namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests { - public class TestCase_WxMsgCryptorTests + public class TestCase_ToolsWxMsgCryptorTests { [Fact(DisplayName = "测试用例:回调信息解析")] public void TestWxBizMsgCryptorParsing() diff --git a/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestConfigs.cs b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestConfigs.cs index 8b0aa559..126b7f9d 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestConfigs.cs +++ b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/TestConfigs.cs @@ -22,6 +22,8 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests WechatAgentId = int.Parse(config.GetProperty("AgentId").GetString()!); WechatAgentSecret = config.GetProperty("AgentSecret").GetString()!; WechatAccessToken = config.GetProperty("AccessToken").GetString()!; + WechatFinanceSecretKey = config.GetProperty("FinanceSecretKey").GetString()!; + WechatFinanceEncryptionPrivateKey = config.GetProperty("FinanceEncryptionPrivateKey").GetString()!; WorkDirectoryForSdk = jdoc.RootElement.GetProperty("WorkDirectoryForSdk").GetString()!; WorkDirectoryForTest = jdoc.RootElement.GetProperty("WorkDirectoryForTest").GetString()!; @@ -36,6 +38,8 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests public static readonly int WechatAgentId; public static readonly string WechatAgentSecret; public static readonly string WechatAccessToken; + public static readonly string WechatFinanceSecretKey; + public static readonly string WechatFinanceEncryptionPrivateKey; public static readonly string WorkDirectoryForSdk; public static readonly string WorkDirectoryForTest; diff --git a/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/appsettings.json b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/appsettings.json index 1909d584..4319cbb3 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/appsettings.json +++ b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/appsettings.json @@ -1,9 +1,11 @@ -{ +{ "TestConfig": { "CorpId": "请在此填写用于测试的企业微信 CorpId", "AgentId": "请在此填写用于测试的企业微信 AgentId", "AgentSecret": "请在此填写用于测试的企业微信 AgentSecret", - "AccessToken": "请在此填写用于测试的微信 AccessToken" + "AccessToken": "请在此填写用于测试的企业微信 AccessToken", + "FinanceSecretKey": "请在此填写用于测试的企业微信会话内容存档 SecretKey", + "FinanceEncryptionPrivateKey": "请在此填写用于测试的企业微信会话内容存档消息加解密私钥" }, "WorkDirectoryForSdk": "请输入当前 SDK 项目所在的目录完整路径,如 C:\\Project\\src\\SKIT.FlurlHttpClient.Wechat.Work\\", "WorkDirectoryForTest": "请输入当前测试项目所在的目录完整路径,如 C:\\Project\\test\\SKIT.FlurlHttpClient.Wechat.Work.UnitTests\\"