@@ -0,0 +1,774 @@
using System.Diagnostics ;
using FlaUI.Core ;
using FlaUI.Core.AutomationElements ;
using FlaUI.Core.Definitions ;
using FlaUI.Core.Input ;
using FlaUI.Core.Tools ;
using FlaUI.Core.WindowsAPI ;
using FlaUI.UIA3 ;
using WxAgent.Core ;
namespace WxAgent.Windows ;
public static partial class WechatChatClient
{
private static readonly HashSet < string > ContactHeadings = new ( StringComparer . Ordinal )
{
"新的朋友" , "群聊" , "标签" , "公众号" , "企业微信联系人"
} ;
public static Task < IReadOnlyList < WechatSubWindowSnapshot > > GetSubWindowsAsync ( CancellationToken cancellationToken = default )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
var processIds = Process . GetProcessesByName ( "Weixin" ) . Select ( process = > process . Id ) . ToHashSet ( ) ;
using var automation = new UIA3Automation ( ) ;
var windows = automation . GetDesktop ( ) . FindAllChildren ( cf = > cf . ByControlType ( ControlType . Window ) )
. Where ( window = > processIds . Contains ( window . Properties . ProcessId . ValueOrDefault )
& & ! window . Properties . IsOffscreen . ValueOrDefault
& & ! window . BoundingRectangle . IsEmpty )
. Select ( window = > new WechatSubWindowSnapshot (
SafeName ( window ) ,
ClassifyWindow ( SafeName ( window ) ) ,
window . Properties . ProcessId . ValueOrDefault ) )
. ToArray ( ) ;
return Task . FromResult < IReadOnlyList < WechatSubWindowSnapshot > > ( windows ) ;
}
public static async Task < WechatSubWindowSnapshot ? > GetSubWindowAsync ( string title , CancellationToken cancellationToken = default ) = >
( await GetSubWindowsAsync ( cancellationToken ) . ConfigureAwait ( false ) )
. FirstOrDefault ( window = > string . Equals ( window . Title , title , StringComparison . Ordinal ) ) ;
public static Task CloseSubWindowAsync ( string title , CancellationToken cancellationToken = default )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
var processIds = Process . GetProcessesByName ( "Weixin" ) . Select ( process = > process . Id ) . ToHashSet ( ) ;
using var automation = new UIA3Automation ( ) ;
var window = automation . GetDesktop ( ) . FindAllChildren ( cf = > cf . ByControlType ( ControlType . Window ) )
. FirstOrDefault ( candidate = > processIds . Contains ( candidate . Properties . ProcessId . ValueOrDefault )
& & ! candidate . Properties . IsOffscreen . ValueOrDefault
& & ! candidate . BoundingRectangle . IsEmpty
& & string . Equals ( SafeName ( candidate ) , title , StringComparison . Ordinal ) ) ;
window ? . AsWindow ( ) . Close ( ) ;
return Task . CompletedTask ;
}
public static Task SwitchToContactsAsync ( CancellationToken cancellationToken = default ) = >
WithMainWindowAsync ( async ( main , _ ) = >
{
await SwitchToContactAsync ( main , cancellationToken ) . ConfigureAwait ( false ) ;
return true ;
} , cancellationToken ) ;
public static Task SwitchToChatsAsync ( CancellationToken cancellationToken = default ) = >
WithMainWindowAsync ( ( main , _ ) = >
{
ClickNamed ( main , "微信" , ControlType . Button ) ;
return Task . FromResult ( true ) ;
} , cancellationToken ) ;
public static Task OpenSessionInSubWindowAsync ( string session , CancellationToken cancellationToken = default ) = >
WithMainWindowAsync ( async ( main , _ ) = >
{
await OpenNamedSessionAsync ( main , session , cancellationToken ) . ConfigureAwait ( false ) ;
var item = FindByAutomationId ( main , $"session_item_{session}" )
? ? throw new WxAgentException ( WxAgentErrorCode . ControlNotFound , "Session was not found." ) ;
item . DoubleClick ( ) ;
return true ;
} , cancellationToken ) ;
public static Task < IReadOnlyList < string > > GetRecentGroupsAsync ( CancellationToken cancellationToken = default ) = >
WithMainWindowAsync ( async ( main , _ ) = >
{
await SwitchToContactAsync ( main , cancellationToken ) . ConfigureAwait ( false ) ;
ClickFirstContaining ( main , "群聊" , ControlType . ListItem ) ;
await Task . Delay ( 250 , cancellationToken ) . ConfigureAwait ( false ) ;
return ( IReadOnlyList < string > ) main . FindAllDescendants ( cf = > cf . ByControlType ( ControlType . ListItem ) )
. Select ( SafeName )
. Where ( name = > ! string . IsNullOrWhiteSpace ( name ) & & ! ContactHeadings . Contains ( name ) )
. Distinct ( StringComparer . Ordinal )
. ToArray ( ) ;
} , cancellationToken ) ;
public static Task < WechatAccountSnapshot > GetMyInfoAsync ( CancellationToken cancellationToken = default ) = >
WithMainWindowAsync ( async ( main , _ ) = >
{
var accountButton = main . FindAllDescendants ( cf = > cf . ByControlType ( ControlType . Button ) )
. FirstOrDefault ( button = > string . IsNullOrWhiteSpace ( SafeAutomationId ( button ) ) & & ! string . IsNullOrWhiteSpace ( SafeName ( button ) ) )
? ? throw new WxAgentException ( WxAgentErrorCode . ControlNotFound , "Account button was not found." ) ;
var displayName = SafeName ( accountButton ) ;
accountButton . Click ( ) ;
await Task . Delay ( 200 , cancellationToken ) . ConfigureAwait ( false ) ;
var values = main . FindAllDescendants ( cf = > cf . ByControlType ( ControlType . Text ) )
. Select ( SafeName ) . Where ( value = > ! string . IsNullOrWhiteSpace ( value ) ) . Distinct ( StringComparer . Ordinal ) . ToArray ( ) ;
return new WechatAccountSnapshot ( displayName , ValueAfter ( values , "微信号" ) , ValueAfter ( values , "地区" ) ) ;
} , cancellationToken ) ;
public static Task < IReadOnlyList < WechatContactSnapshot > > GetFriendsAsync (
int maxCount = 1000 ,
CancellationToken cancellationToken = default ) = >
WithMainWindowAsync ( async ( main , _ ) = >
{
if ( maxCount < = 0 )
{
throw new WxAgentException ( WxAgentErrorCode . InvalidArgument , "maxCount must be positive." ) ;
}
await SwitchToContactAsync ( main , cancellationToken ) . ConfigureAwait ( false ) ;
var contacts = main . FindAllDescendants ( cf = > cf . ByControlType ( ControlType . ListItem ) )
. Select ( element = > SafeName ( element ) )
. Where ( name = > ! string . IsNullOrWhiteSpace ( name ) & & ! ContactHeadings . Contains ( name ) )
. Distinct ( StringComparer . Ordinal )
. Take ( maxCount )
. Select ( name = > new WechatContactSnapshot ( name ) )
. ToArray ( ) ;
return ( IReadOnlyList < WechatContactSnapshot > ) contacts ;
} , cancellationToken ) ;
public static Task < IReadOnlyList < WechatNewFriendSnapshot > > GetNewFriendsAsync (
bool includeAccepted = false ,
CancellationToken cancellationToken = default ) = >
WithMainWindowAsync ( async ( main , _ ) = >
{
await SwitchToContactAsync ( main , cancellationToken ) . ConfigureAwait ( false ) ;
ClickNamed ( main , "新的朋友" , ControlType . ListItem ) ;
await Task . Delay ( 300 , cancellationToken ) . ConfigureAwait ( false ) ;
var entries = main . FindAllDescendants ( cf = > cf . ByControlType ( ControlType . ListItem ) )
. Select ( ParseNewFriend )
. Where ( entry = > entry is not null )
. Cast < WechatNewFriendSnapshot > ( ) ;
if ( ! includeAccepted )
{
entries = entries . Where ( entry = > ! entry . Status . Contains ( "已添加" , StringComparison . Ordinal ) ) ;
}
return ( IReadOnlyList < WechatNewFriendSnapshot > ) entries . ToArray ( ) ;
} , cancellationToken ) ;
public static Task < WechatContactSnapshot > GetFriendDetailsAsync ( string displayName , CancellationToken cancellationToken = default ) = >
WithMainWindowAsync ( async ( main , _ ) = >
{
await OpenContactAsync ( main , displayName , cancellationToken ) . ConfigureAwait ( false ) ;
var values = main . FindAllDescendants ( cf = > cf . ByControlType ( ControlType . Text ) )
. Select ( SafeName )
. Where ( value = > ! string . IsNullOrWhiteSpace ( value ) )
. Distinct ( StringComparer . Ordinal )
. ToArray ( ) ;
return new WechatContactSnapshot (
displayName ,
ValueAfter ( values , "微信号" ) ,
ValueAfter ( values , "标签" ) is { } tags ? tags . Split ( '、' , StringSplitOptions . RemoveEmptyEntries ) : null ,
ValueAfter ( values , "个性签名" ) ,
ValueAfter ( values , "来源" ) ,
ParseCount ( ValueAfter ( values , "共同群聊" ) ) ) ;
} , cancellationToken ) ;
public static Task < WechatOperationResult > AddNewFriendAsync (
string keywords ,
string? requestMessage ,
string confirmation ,
CancellationToken cancellationToken = default ) = >
WithConfirmedMainWindowAsync ( "add friend" , confirmation , async ( main , _ ) = >
{
if ( string . IsNullOrWhiteSpace ( keywords ) )
{
throw new WxAgentException ( WxAgentErrorCode . InvalidArgument , "keywords is required." ) ;
}
ClickNamed ( main , "通讯录" , ControlType . Button ) ;
await Task . Delay ( 200 , cancellationToken ) . ConfigureAwait ( false ) ;
ClickNamed ( main , "新的朋友" , ControlType . ListItem ) ;
await Task . Delay ( 200 , cancellationToken ) . ConfigureAwait ( false ) ;
ClickFirstNamed ( main , new [ ] { "添加朋友" , "添加好友" } , ControlType . Button ) ;
await Task . Delay ( 200 , cancellationToken ) . ConfigureAwait ( false ) ;
SetFirstEdit ( main , keywords ) ;
Keyboard . Press ( VirtualKeyShort . ENTER ) ;
await Task . Delay ( 500 , cancellationToken ) . ConfigureAwait ( false ) ;
ClickFirstNamed ( main , new [ ] { "添加到通讯录" , "添加朋友" } , ControlType . Button ) ;
await Task . Delay ( 200 , cancellationToken ) . ConfigureAwait ( false ) ;
if ( ! string . IsNullOrWhiteSpace ( requestMessage ) )
{
SetFirstEdit ( main , requestMessage ) ;
}
ClickFirstNamed ( main , new [ ] { "发送" , "确定" } , ControlType . Button ) ;
return WechatOperationResult . Ok ( "friend request submitted" ) ;
} , cancellationToken ) ;
public static Task < WechatOperationResult > AcceptNewFriendAsync (
string displayName ,
string? remark ,
string confirmation ,
CancellationToken cancellationToken = default ) = >
WithConfirmedMainWindowAsync ( "accept friend" , confirmation , async ( main , _ ) = >
{
await SwitchToContactAsync ( main , cancellationToken ) . ConfigureAwait ( false ) ;
ClickNamed ( main , "新的朋友" , ControlType . ListItem ) ;
await Task . Delay ( 200 , cancellationToken ) . ConfigureAwait ( false ) ;
ClickNamed ( main , displayName , ControlType . ListItem ) ;
ClickFirstNamed ( main , new [ ] { "接受" , "通过验证" } , ControlType . Button ) ;
await Task . Delay ( 200 , cancellationToken ) . ConfigureAwait ( false ) ;
if ( ! string . IsNullOrWhiteSpace ( remark ) )
{
SetFirstEdit ( main , remark ) ;
}
ClickFirstNamed ( main , new [ ] { "完成" , "确定" } , ControlType . Button ) ;
return WechatOperationResult . Ok ( "friend request accepted" ) ;
} , cancellationToken ) ;
public static Task < WechatOperationResult > EditFriendRemarkAsync (
string displayName ,
string remark ,
string confirmation ,
CancellationToken cancellationToken = default ) = >
WithConfirmedMainWindowAsync ( "edit friend remark" , confirmation , async ( main , _ ) = >
{
await OpenContactAsync ( main , displayName , cancellationToken ) . ConfigureAwait ( false ) ;
ClickFirstNamed ( main , new [ ] { "设置备注和标签" , "备注和标签" } ) ;
await Task . Delay ( 200 , cancellationToken ) . ConfigureAwait ( false ) ;
SetFirstEdit ( main , remark ? ? string . Empty ) ;
ClickFirstNamed ( main , new [ ] { "完成" , "确定" } , ControlType . Button ) ;
return WechatOperationResult . Ok ( "friend remark updated" ) ;
} , cancellationToken ) ;
public static Task < WechatOperationResult > CreateGroupAsync (
IReadOnlyList < string > contacts ,
string confirmation ,
CancellationToken cancellationToken = default ) = >
WithConfirmedMainWindowAsync ( "create group" , confirmation , async ( main , _ ) = >
{
WechatOperationPolicy . ValidateNames ( contacts , nameof ( contacts ) ) ;
ClickFirstNamed ( main , new [ ] { "发起群聊" , "创建群聊" } , ControlType . Button ) ;
await SelectPeopleAsync ( main , contacts , cancellationToken ) . ConfigureAwait ( false ) ;
ClickFirstNamed ( main , new [ ] { "完成" , "确定" } , ControlType . Button ) ;
return WechatOperationResult . Ok ( "group created" ) ;
} , cancellationToken ) ;
public static Task < WechatOperationResult > AddGroupMembersAsync ( string group , IReadOnlyList < string > members , string confirmation , CancellationToken cancellationToken = default ) = >
ChangeGroupMembersAsync ( group , members , new [ ] { "添加成员" , "+" } , "add group members" , confirmation , cancellationToken ) ;
public static Task < WechatOperationResult > RemoveGroupMembersAsync ( string group , IReadOnlyList < string > members , string confirmation , CancellationToken cancellationToken = default ) = >
ChangeGroupMembersAsync ( group , members , new [ ] { "移出群聊" , "删除成员" , "-" } , "remove group members" , confirmation , cancellationToken ) ;
public static Task < WechatOperationResult > SetGroupNameAsync ( string group , string value , string confirmation , CancellationToken cancellationToken = default ) = >
SetGroupTextAsync ( group , new [ ] { "群聊名称" , "群名称" } , value , "set group name" , confirmation , cancellationToken ) ;
public static Task < WechatOperationResult > SetGroupRemarkAsync ( string group , string value , string confirmation , CancellationToken cancellationToken = default ) = >
SetGroupTextAsync ( group , new [ ] { "备注" , "群聊备注" } , value , "set group remark" , confirmation , cancellationToken ) ;
public static Task < WechatOperationResult > SetGroupAnnouncementAsync ( string group , string value , string confirmation , CancellationToken cancellationToken = default ) = >
SetGroupTextAsync ( group , new [ ] { "群公告" } , value , "set group announcement" , confirmation , cancellationToken ) ;
public static Task < WechatOperationResult > SetMyNicknameInGroupAsync ( string group , string value , string confirmation , CancellationToken cancellationToken = default ) = >
SetGroupTextAsync ( group , new [ ] { "我在本群的昵称" , "群昵称" } , value , "set group nickname" , confirmation , cancellationToken ) ;
public static Task < IReadOnlyList < WechatGroupMemberSnapshot > > GetGroupMembersAsync ( string group , CancellationToken cancellationToken = default ) = >
WithMainWindowAsync ( async ( main , _ ) = >
{
await OpenGroupInfoAsync ( main , group , cancellationToken ) . ConfigureAwait ( false ) ;
var names = main . FindAllDescendants ( cf = > cf . ByControlType ( ControlType . ListItem ) )
. Select ( SafeName )
. Where ( name = > ! string . IsNullOrWhiteSpace ( name ) )
. Distinct ( StringComparer . Ordinal )
. Select ( name = > new WechatGroupMemberSnapshot ( name ) )
. ToArray ( ) ;
return ( IReadOnlyList < WechatGroupMemberSnapshot > ) names ;
} , cancellationToken ) ;
public static Task < WechatOperationResult > AtAllAsync ( string group , string? message , string confirmation , CancellationToken cancellationToken = default ) = >
WithConfirmedMainWindowAsync ( "mention all" , confirmation , async ( main , automation ) = >
{
await OpenNamedSessionAsync ( main , group , cancellationToken ) . ConfigureAwait ( false ) ;
var input = FindByAutomationId ( main , WechatLocators . ChatInput )
? ? throw new WxAgentException ( WxAgentErrorCode . UiStructureChanged , "Chat input was not found." ) ;
input . Click ( ) ;
Keyboard . TypeSimultaneously ( [ VirtualKeyShort . CONTROL , VirtualKeyShort . KEY_A ] ) ;
Keyboard . Press ( VirtualKeyShort . BACK ) ;
Keyboard . Type ( "@" ) ;
await Task . Delay ( 300 , cancellationToken ) . ConfigureAwait ( false ) ;
var everyone = automation . GetDesktop ( ) . FindAllDescendants ( cf = > cf . ByName ( "所有人" ) )
. FirstOrDefault ( element = > ! element . Properties . IsOffscreen . ValueOrDefault & & ! element . BoundingRectangle . IsEmpty )
? ? throw new WxAgentException ( WxAgentErrorCode . ControlNotFound , "The WeChat '所有人' mention option was not found." ) ;
everyone . Click ( ) ;
await Task . Delay ( 100 , cancellationToken ) . ConfigureAwait ( false ) ;
if ( ! string . IsNullOrEmpty ( message ) )
{
Keyboard . Type ( message ) ;
}
Keyboard . Press ( VirtualKeyShort . ENTER ) ;
return WechatOperationResult . Ok ( "message sent" ) ;
} , cancellationToken ) ;
public static Task < WechatOperationResult > SelectSessionOptionAsync ( string session , string option , string? confirmation = null , CancellationToken cancellationToken = default ) = >
WithMainWindowAsync ( async ( main , _ ) = >
{
if ( IsDestructiveOption ( option ) )
{
WechatOperationPolicy . RequireConfirmation ( confirmation , $"session option '{option}'" ) ;
}
await OpenNamedSessionAsync ( main , session , cancellationToken ) . ConfigureAwait ( false ) ;
var item = FindByAutomationId ( main , $"session_item_{session}" )
? ? throw new WxAgentException ( WxAgentErrorCode . ControlNotFound , "Session was not found." ) ;
item . RightClick ( ) ;
await Task . Delay ( 100 , cancellationToken ) . ConfigureAwait ( false ) ;
ClickNamed ( main , option ) ;
ConfirmDialogIfPresent ( main ) ;
return WechatOperationResult . Ok ( "session option selected" ) ;
} , cancellationToken ) ;
public static Task < WechatOperationResult > DeleteSessionAsync ( string session , string confirmation , CancellationToken cancellationToken = default ) = >
SelectSessionOptionAsync ( session , "删除聊天" , confirmation , cancellationToken ) ;
public static Task < WechatOperationResult > HideSessionAsync ( string session , string confirmation , CancellationToken cancellationToken = default ) = >
SelectSessionOptionAsync ( session , "不显示聊天" , confirmation , cancellationToken ) ;
private static Task < WechatOperationResult > ChangeGroupMembersAsync ( string group , IReadOnlyList < string > members , string [ ] actions , string operation , string confirmation , CancellationToken cancellationToken ) = >
WithConfirmedMainWindowAsync ( operation , confirmation , async ( main , _ ) = >
{
WechatOperationPolicy . ValidateNames ( members , nameof ( members ) ) ;
await OpenGroupInfoAsync ( main , group , cancellationToken ) . ConfigureAwait ( false ) ;
ClickFirstNamed ( main , actions ) ;
await SelectPeopleAsync ( main , members , cancellationToken ) . ConfigureAwait ( false ) ;
ClickFirstNamed ( main , new [ ] { "完成" , "确定" , "删除" } , ControlType . Button ) ;
return WechatOperationResult . Ok ( operation + " completed" ) ;
} , cancellationToken ) ;
private static Task < WechatOperationResult > SetGroupTextAsync ( string group , string [ ] labels , string value , string operation , string confirmation , CancellationToken cancellationToken ) = >
WithConfirmedMainWindowAsync ( operation , confirmation , async ( main , _ ) = >
{
await OpenGroupInfoAsync ( main , group , cancellationToken ) . ConfigureAwait ( false ) ;
ClickFirstNamed ( main , labels ) ;
await Task . Delay ( 100 , cancellationToken ) . ConfigureAwait ( false ) ;
SetFirstEdit ( main , value ) ;
ClickFirstNamed ( main , new [ ] { "完成" , "确定" , "发布" } , ControlType . Button ) ;
ConfirmDialogIfPresent ( main ) ;
return WechatOperationResult . Ok ( operation + " completed" ) ;
} , cancellationToken ) ;
private static async Task < T > WithConfirmedMainWindowAsync < T > ( string operation , string confirmation , Func < AutomationElement , UIA3Automation , Task < T > > action , CancellationToken cancellationToken )
{
WechatOperationPolicy . RequireConfirmation ( confirmation , operation ) ;
return await WithMainWindowAsync ( action , cancellationToken ) . ConfigureAwait ( false ) ;
}
private static async Task < T > WithMainWindowAsync < T > ( Func < AutomationElement , UIA3Automation , Task < T > > action , CancellationToken cancellationToken )
{
await CommandQueue . WaitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
try
{
using var automation = new UIA3Automation ( ) ;
var main = AttachWindow ( automation ) ;
return await action ( main , automation ) . ConfigureAwait ( false ) ;
}
catch ( OperationCanceledException )
{
throw ;
}
catch ( WxAgentException )
{
throw ;
}
catch ( Exception exception )
{
throw new WxAgentException ( WxAgentErrorCode . InvalidOperationState , "WeChat UI operation failed." , exception ) ;
}
finally
{
CommandQueue . Release ( ) ;
}
}
private static async Task SwitchToContactAsync ( AutomationElement main , CancellationToken cancellationToken )
{
ClickNamed ( main , "微信" , ControlType . Button ) ;
await Task . Delay ( 100 , cancellationToken ) . ConfigureAwait ( false ) ;
ClickNamed ( main , "通讯录" , ControlType . Button ) ;
await Task . Delay ( 250 , cancellationToken ) . ConfigureAwait ( false ) ;
}
private static async Task OpenContactAsync ( AutomationElement main , string displayName , CancellationToken cancellationToken )
{
if ( string . IsNullOrWhiteSpace ( displayName ) )
{
throw new WxAgentException ( WxAgentErrorCode . InvalidArgument , "displayName is required." ) ;
}
await SwitchToContactAsync ( main , cancellationToken ) . ConfigureAwait ( false ) ;
ClickNamed ( main , displayName , ControlType . ListItem ) ;
await Task . Delay ( 250 , cancellationToken ) . ConfigureAwait ( false ) ;
}
private static async Task OpenNamedSessionAsync ( AutomationElement main , string session , CancellationToken cancellationToken )
{
if ( string . IsNullOrWhiteSpace ( session ) )
{
throw new WxAgentException ( WxAgentErrorCode . InvalidArgument , "session is required." ) ;
}
if ( main . FindAllDescendants ( ) . Any ( element = >
SafeAutomationId ( element ) . EndsWith ( "current_chat_name_label" , StringComparison . Ordinal )
& & string . Equals ( SafeName ( element ) , session , StringComparison . Ordinal ) )
& & FindByAutomationId ( main , WechatLocators . MessageList ) is not null )
{
return ;
}
if ( string . Equals ( session , WechatLocators . FileTransferAssistant , StringComparison . Ordinal ) )
{
await OpenFileTransferAssistantCoreAsync ( main , cancellationToken ) . ConfigureAwait ( false ) ;
return ;
}
var direct = FindByAutomationId ( main , $"session_item_{session}" ) ;
if ( direct is not null )
{
direct . Click ( ) ;
await WaitForSessionPageAsync ( main , cancellationToken ) . ConfigureAwait ( false ) ;
return ;
}
var search = main . FindAllDescendants ( ) . FirstOrDefault ( element = >
SafeControlType ( element ) = = ControlType . Edit & & SafeName ( element ) = = WechatLocators . Search )
? ? throw new WxAgentException ( WxAgentErrorCode . UiStructureChanged , "Search box was not found." ) ;
search . AsTextBox ( ) . Text = session ;
await Task . Delay ( 500 , cancellationToken ) . ConfigureAwait ( false ) ;
ClickNamed ( main , session ) ;
await WaitForSessionPageAsync ( main , cancellationToken ) . ConfigureAwait ( false ) ;
}
private static async Task WaitForSessionPageAsync ( AutomationElement main , CancellationToken cancellationToken )
{
for ( var attempt = 0 ; attempt < 25 ; attempt + + )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
if ( FindByAutomationId ( main , WechatLocators . MessageList ) is not null ) return ;
await Task . Delay ( 100 , cancellationToken ) . ConfigureAwait ( false ) ;
}
throw new WxAgentException ( WxAgentErrorCode . ControlNotFound , $"Control {WechatLocators.MessageList} was not found." ) ;
}
private static async Task OpenGroupInfoAsync ( AutomationElement main , string group , CancellationToken cancellationToken )
{
await OpenNamedSessionAsync ( main , group , cancellationToken ) . ConfigureAwait ( false ) ;
ClickFirstNamed ( main , new [ ] { "聊天信息" , "更多" } , ControlType . Button ) ;
await Task . Delay ( 200 , cancellationToken ) . ConfigureAwait ( false ) ;
}
private static async Task SelectPeopleAsync ( AutomationElement main , IEnumerable < string > names , CancellationToken cancellationToken )
{
foreach ( var name in names )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
var edit = main . FindAllDescendants ( cf = > cf . ByControlType ( ControlType . Edit ) )
. FirstOrDefault ( candidate = > ! candidate . Properties . IsOffscreen . ValueOrDefault ) ;
if ( edit is not null )
{
edit . AsTextBox ( ) . Text = name ;
await Task . Delay ( 150 , cancellationToken ) . ConfigureAwait ( false ) ;
}
ClickNamed ( main , name ) ;
}
}
private static void SetFirstEdit ( AutomationElement main , string value )
{
var edit = main . FindAllDescendants ( cf = > cf . ByControlType ( ControlType . Edit ) )
. FirstOrDefault ( candidate = > ! candidate . Properties . IsOffscreen . ValueOrDefault )
? ? throw new WxAgentException ( WxAgentErrorCode . UiStructureChanged , "Expected text input was not found." ) ;
edit . AsTextBox ( ) . Text = value ;
}
private static void ClickNamed ( AutomationElement root , string name , ControlType ? controlType = null )
{
var element = root . FindFirstDescendant ( cf = > controlType is null
? cf . ByName ( name )
: cf . ByName ( name ) . And ( cf . ByControlType ( controlType . Value ) ) ) ;
if ( element is null )
{
throw new WxAgentException ( WxAgentErrorCode . UiStructureChanged , $"Expected control '{name}' was not found." ) ;
}
element . Click ( ) ;
}
private static void ClickFirstContaining ( AutomationElement root , string text , ControlType ? controlType = null )
{
var element = root . FindAllDescendants ( )
. FirstOrDefault ( candidate = > SafeName ( candidate ) . Contains ( text , StringComparison . Ordinal )
& & ( controlType is null | | SafeControlType ( candidate ) = = controlType ) ) ;
if ( element is null )
{
throw new WxAgentException ( WxAgentErrorCode . UiStructureChanged , $"Expected control containing '{text}' was not found." ) ;
}
element . Click ( ) ;
}
private static void ClickFirstNamed ( AutomationElement root , IEnumerable < string > names , ControlType ? controlType = null )
{
foreach ( var name in names )
{
var element = root . FindFirstDescendant ( cf = > controlType is null
? cf . ByName ( name )
: cf . ByName ( name ) . And ( cf . ByControlType ( controlType . Value ) ) ) ;
if ( element is null )
{
continue ;
}
element . Click ( ) ;
return ;
}
throw new WxAgentException ( WxAgentErrorCode . UiStructureChanged , "None of the expected controls were found." ) ;
}
private static void ConfirmDialogIfPresent ( AutomationElement root )
{
foreach ( var name in new [ ] { "确定" , "删除" , "发布" } )
{
var button = root . FindFirstDescendant ( cf = > cf . ByName ( name ) . And ( cf . ByControlType ( ControlType . Button ) ) ) ;
if ( button is not null )
{
button . Click ( ) ;
return ;
}
}
}
private static WechatNewFriendSnapshot ? ParseNewFriend ( AutomationElement element )
{
var values = element . FindAllDescendants ( cf = > cf . ByControlType ( ControlType . Text ) )
. Select ( SafeName )
. Where ( value = > ! string . IsNullOrWhiteSpace ( value ) )
. ToArray ( ) ;
return values . Length = = 0 ? null : new WechatNewFriendSnapshot ( values [ 0 ] , values . Length > 1 ? values [ ^ 1 ] : "unknown" ) ;
}
private static string? ValueAfter ( IReadOnlyList < string > values , string label )
{
for ( var index = 0 ; index < values . Count - 1 ; index + + )
{
if ( values [ index ] . StartsWith ( label , StringComparison . Ordinal ) )
{
var inline = values [ index ] [ label . Length . . ] . Trim ( ' ' , ': ' , ':' ) ;
return inline . Length > 0 ? inline : values [ index + 1 ] ;
}
}
return null ;
}
private static int? ParseCount ( string? value )
{
if ( value is null ) return null ;
var digits = new string ( value . Where ( char . IsDigit ) . ToArray ( ) ) ;
return int . TryParse ( digits , out var count ) ? count : null ;
}
public static Task < IReadOnlyList < ChatMessageSnapshot > > GetHistoryMessageAsync (
string session ,
int count ,
Func < ChatMessageSnapshot , bool > ? callback = null ,
bool returnToLatest = true ,
CancellationToken cancellationToken = default ) = >
WithMainWindowAsync ( async ( main , _ ) = >
{
if ( count is < 1 or > 1000 )
{
throw new WxAgentException ( WxAgentErrorCode . InvalidArgument , "count must be between 1 and 1000." ) ;
}
await OpenNamedSessionAsync ( main , session , cancellationToken ) . ConfigureAwait ( false ) ;
var messages = new List < ChatMessageSnapshot > ( ) ;
var seen = new HashSet < string > ( StringComparer . Ordinal ) ;
var unchanged = 0 ;
var stopped = false ;
var scrolls = 0 ;
while ( messages . Count < count & & unchanged < 3 & & ! stopped )
{
var before = messages . Count ;
foreach ( var message in ReadVisible ( main ) )
{
if ( ! seen . Add ( message . Fingerprint ) ) continue ;
messages . Add ( message ) ;
if ( callback ? . Invoke ( message ) = = false )
{
stopped = true ;
break ;
}
if ( messages . Count > = count ) break ;
}
if ( messages . Count > = count | | stopped ) break ;
unchanged = messages . Count = = before ? unchanged + 1 : 0 ;
var list = FindByAutomationId ( main , WechatLocators . MessageList )
? ? throw new WxAgentException ( WxAgentErrorCode . ControlNotFound , "Message list was not found." ) ;
MoveToCenter ( list ) ;
Mouse . Scroll ( 5 ) ;
scrolls + + ;
await Task . Delay ( 200 , cancellationToken ) . ConfigureAwait ( false ) ;
}
if ( returnToLatest & & scrolls > 0 )
{
Mouse . Scroll ( - 5d * scrolls ) ;
}
return ( IReadOnlyList < ChatMessageSnapshot > ) messages . Take ( count ) . ToArray ( ) ;
} , cancellationToken ) ;
public static Task < WechatOperationResult > ForwardVisibleMessageAsync (
string session ,
string fingerprint ,
IReadOnlyList < string > targets ,
string confirmation ,
CancellationToken cancellationToken = default ) = >
WithConfirmedMainWindowAsync ( "forward message" , confirmation , async ( main , _ ) = >
{
WechatOperationPolicy . ValidateNames ( targets , nameof ( targets ) ) ;
await OpenNamedSessionAsync ( main , session , cancellationToken ) . ConfigureAwait ( false ) ;
var element = FindVisibleMessageElement ( main , fingerprint ) ;
element . RightClick ( ) ;
await Task . Delay ( 100 , cancellationToken ) . ConfigureAwait ( false ) ;
ClickNamed ( main , "转发" ) ;
await SelectPeopleAsync ( main , targets , cancellationToken ) . ConfigureAwait ( false ) ;
ClickFirstNamed ( main , new [ ] { "发送" , "确定" } , ControlType . Button ) ;
return WechatOperationResult . Ok ( "message forwarded" ) ;
} , cancellationToken ) ;
public static Task < IReadOnlyDictionary < string , string > > GetVisibleMessageSenderInfoAsync (
string session ,
string fingerprint ,
CancellationToken cancellationToken = default ) = >
WithMainWindowAsync ( async ( main , _ ) = >
{
await OpenNamedSessionAsync ( main , session , cancellationToken ) . ConfigureAwait ( false ) ;
var element = FindVisibleMessageElement ( main , fingerprint ) ;
element . RightClick ( ) ;
await Task . Delay ( 100 , cancellationToken ) . ConfigureAwait ( false ) ;
ClickFirstNamed ( main , new [ ] { "查看资料" , "发送者信息" } ) ;
await Task . Delay ( 200 , cancellationToken ) . ConfigureAwait ( false ) ;
var values = main . FindAllDescendants ( cf = > cf . ByControlType ( ControlType . Text ) )
. Select ( SafeName ) . Where ( value = > ! string . IsNullOrWhiteSpace ( value ) ) . Distinct ( StringComparer . Ordinal ) . ToArray ( ) ;
var result = new Dictionary < string , string > ( StringComparer . Ordinal ) ;
foreach ( var label in new [ ] { "昵称" , "微信号" , "地区" , "来源" } )
{
if ( ValueAfter ( values , label ) is { } value ) result [ label ] = value ;
}
return ( IReadOnlyDictionary < string , string > ) result ;
} , cancellationToken ) ;
public static Task < WechatOperationResult > AddVisibleMessageSenderAsFriendAsync (
string session ,
string fingerprint ,
string? requestMessage ,
string confirmation ,
CancellationToken cancellationToken = default ) = >
WithConfirmedMainWindowAsync ( "add message sender as friend" , confirmation , async ( main , _ ) = >
{
await OpenNamedSessionAsync ( main , session , cancellationToken ) . ConfigureAwait ( false ) ;
FindVisibleMessageElement ( main , fingerprint ) . RightClick ( ) ;
ClickFirstNamed ( main , new [ ] { "添加好友" , "添加到通讯录" } ) ;
await Task . Delay ( 150 , cancellationToken ) . ConfigureAwait ( false ) ;
if ( ! string . IsNullOrWhiteSpace ( requestMessage ) ) SetFirstEdit ( main , requestMessage ) ;
ClickFirstNamed ( main , new [ ] { "发送" , "确定" } , ControlType . Button ) ;
return WechatOperationResult . Ok ( "friend request submitted" ) ;
} , cancellationToken ) ;
public static Task < WechatOperationResult > DeleteVisibleMessageSenderFriendAsync (
string session ,
string fingerprint ,
bool clearChatHistory ,
string confirmation ,
CancellationToken cancellationToken = default ) = >
WithConfirmedMainWindowAsync ( "delete friend" , confirmation , async ( main , _ ) = >
{
await OpenNamedSessionAsync ( main , session , cancellationToken ) . ConfigureAwait ( false ) ;
FindVisibleMessageElement ( main , fingerprint ) . RightClick ( ) ;
ClickFirstNamed ( main , new [ ] { "查看资料" , "发送者信息" } ) ;
await Task . Delay ( 150 , cancellationToken ) . ConfigureAwait ( false ) ;
ClickFirstNamed ( main , new [ ] { "更多" , "更多信息" } , ControlType . Button ) ;
ClickNamed ( main , "删除" ) ;
await Task . Delay ( 100 , cancellationToken ) . ConfigureAwait ( false ) ;
if ( ! clearChatHistory )
{
var check = main . FindFirstDescendant ( cf = > cf . ByName ( "同时删除聊天记录" ) . And ( cf . ByControlType ( ControlType . CheckBox ) ) ) ;
if ( check ? . AsCheckBox ( ) . IsChecked = = true ) check . Click ( ) ;
}
ConfirmDialogIfPresent ( main ) ;
return WechatOperationResult . Ok ( "friend deleted" ) ;
} , cancellationToken ) ;
public static Task < string > DownloadVisibleMessageAsync (
string session ,
string fingerprint ,
string destinationPath ,
string confirmation ,
CancellationToken cancellationToken = default ) = >
WithConfirmedMainWindowAsync ( "download message" , confirmation , async ( main , _ ) = >
{
if ( string . IsNullOrWhiteSpace ( destinationPath ) )
throw new WxAgentException ( WxAgentErrorCode . InvalidArgument , "destinationPath is required." ) ;
await OpenNamedSessionAsync ( main , session , cancellationToken ) . ConfigureAwait ( false ) ;
FindVisibleMessageElement ( main , fingerprint ) . RightClick ( ) ;
ClickFirstNamed ( main , new [ ] { "另存为..." , "另存为" , "保存" } ) ;
await Task . Delay ( 200 , cancellationToken ) . ConfigureAwait ( false ) ;
Keyboard . Type ( destinationPath ) ;
Keyboard . Press ( VirtualKeyShort . ENTER ) ;
return destinationPath ;
} , cancellationToken ) ;
public static Task < string > OcrVisibleImageAsync ( string session , string fingerprint , CancellationToken cancellationToken = default ) = >
ReadMessageDerivedTextAsync ( session , fingerprint , new [ ] { "提取文字" , "识别文字" } , cancellationToken ) ;
public static Task < string > VoiceToTextAsync ( string session , string fingerprint , CancellationToken cancellationToken = default ) = >
ReadMessageDerivedTextAsync ( session , fingerprint , new [ ] { "语音转文字" , "转文字" } , cancellationToken ) ;
public static Task < WechatOperationResult > SaveVisibleNoteFilesAsync ( string session , string fingerprint , string destinationPath , string confirmation , CancellationToken cancellationToken = default ) = >
WithConfirmedMainWindowAsync ( "save note files" , confirmation , async ( main , _ ) = >
{
await OpenNamedSessionAsync ( main , session , cancellationToken ) . ConfigureAwait ( false ) ;
FindVisibleMessageElement ( main , fingerprint ) . RightClick ( ) ;
ClickFirstNamed ( main , new [ ] { "保存附件" , "另存为..." , "另存为" } ) ;
await Task . Delay ( 200 , cancellationToken ) . ConfigureAwait ( false ) ;
Keyboard . Type ( destinationPath ) ;
Keyboard . Press ( VirtualKeyShort . ENTER ) ;
return WechatOperationResult . Ok ( "note files saved" ) ;
} , cancellationToken ) ;
public static Task < WechatOperationResult > SelectVisibleMessageOptionAsync ( string session , string fingerprint , string option , string? confirmation = null , CancellationToken cancellationToken = default ) = >
WithMainWindowAsync ( async ( main , _ ) = >
{
if ( IsDestructiveOption ( option ) ) WechatOperationPolicy . RequireConfirmation ( confirmation , $"message option '{option}'" ) ;
await OpenNamedSessionAsync ( main , session , cancellationToken ) . ConfigureAwait ( false ) ;
FindVisibleMessageElement ( main , fingerprint ) . RightClick ( ) ;
ClickNamed ( main , option ) ;
ConfirmDialogIfPresent ( main ) ;
return WechatOperationResult . Ok ( "message option selected" ) ;
} , cancellationToken ) ;
public static Task < WechatOperationResult > InviteGroupMembersAsync ( string group , IReadOnlyList < string > members , string confirmation , CancellationToken cancellationToken = default ) = >
AddGroupMembersAsync ( group , members , confirmation , cancellationToken ) ;
private static Task < string > ReadMessageDerivedTextAsync ( string session , string fingerprint , string [ ] actions , CancellationToken cancellationToken ) = >
WithMainWindowAsync ( async ( main , _ ) = >
{
await OpenNamedSessionAsync ( main , session , cancellationToken ) . ConfigureAwait ( false ) ;
FindVisibleMessageElement ( main , fingerprint ) . RightClick ( ) ;
ClickFirstNamed ( main , actions ) ;
await Task . Delay ( 300 , cancellationToken ) . ConfigureAwait ( false ) ;
return string . Join ( Environment . NewLine , main . FindAllDescendants ( cf = > cf . ByControlType ( ControlType . Text ) )
. Select ( SafeName ) . Where ( value = > ! string . IsNullOrWhiteSpace ( value ) ) . Distinct ( StringComparer . Ordinal ) ) ;
} , cancellationToken ) ;
private static AutomationElement FindVisibleMessageElement ( AutomationElement main , string fingerprint )
{
var list = FindByAutomationId ( main , WechatLocators . MessageList )
? ? throw new WxAgentException ( WxAgentErrorCode . UiStructureChanged , "Message list was not found." ) ;
var elements = list . FindAllDescendants ( )
. Where ( element = > SafeAutomationId ( element ) = = WechatLocators . ChatBubbleItem )
. ToArray ( ) ;
var snapshots = ReadVisible ( main ) ;
for ( var index = 0 ; index < Math . Min ( elements . Length , snapshots . Count ) ; index + + )
{
if ( snapshots [ index ] . Fingerprint = = fingerprint ) return elements [ index ] ;
}
throw new WxAgentException ( WxAgentErrorCode . ControlNotFound , "The visible message fingerprint was not found." ) ;
}
private static bool IsDestructiveOption ( string option ) = > option . Contains ( "删除" , StringComparison . Ordinal ) | | option . Contains ( "不显示" , StringComparison . Ordinal ) ;
private static string ClassifyWindow ( string title ) = >
title . Contains ( "图片" , StringComparison . Ordinal ) ? "image" :
title . Contains ( "视频" , StringComparison . Ordinal ) ? "video" :
title . Contains ( "文件" , StringComparison . Ordinal ) ? "file" :
title . Contains ( "聊天记录" , StringComparison . Ordinal ) ? "history" :
"window" ;
}