import os import sys import json import time import psutil import requests import win32gui import win32process import win32api import win32evtlog import win32evtlogutil import winreg import sqlite3 import shutil from datetime import datetime from pathlib import Path import pystray from PIL import Image, ImageDraw from threading import Thread import schedule import atexit import argparse import subprocess API_URL = "https://admin.noidle.tech/api" CONFIG_FILE = Path(os.getenv('APPDATA')) / 'NoIdle' / 'config.json' INSTALL_DIR = Path(os.getenv('ProgramFiles')) / 'NoIdle' INSTALL_EXE = INSTALL_DIR / 'NoIdle.exe' MONITOR_INTERVAL = 10 # 10 segundos (mais frequente para capturar mudanças) IDLE_THRESHOLD = 30 # 30 segundos para considerar ocioso class Config: def __init__(self): self.config_dir = CONFIG_FILE.parent self.config_dir.mkdir(parents=True, exist_ok=True) self.data = self.load() def load(self): if CONFIG_FILE.exists(): with open(CONFIG_FILE, 'r') as f: return json.load(f) return {} def save(self): with open(CONFIG_FILE, 'w') as f: json.dump(self.data, f, indent=2) def get(self, key, default=None): return self.data.get(key, default) def set(self, key, value): self.data[key] = value self.save() def has(self, key): return key in self.data class ActivationWindow: def __init__(self): self.activation_key = None self.result = None def show(self): import tkinter as tk from tkinter import messagebox root = tk.Tk() root.title("NoIdle - Ativacao") root.geometry("500x300") root.resizable(False, False) root.eval('tk::PlaceWindow . center') title = tk.Label(root, text="NoIdle", font=("Arial", 24, "bold"), fg="#2563eb") title.pack(pady=20) subtitle = tk.Label(root, text="Zero Idle, Maximum Productivity", font=("Arial", 10)) subtitle.pack() instruction = tk.Label(root, text="Insira a chave de ativacao:", font=("Arial", 10)) instruction.pack(pady=20) key_var = tk.StringVar() entry = tk.Entry(root, textvariable=key_var, font=("Courier", 12), width=40, justify="center") entry.pack(pady=10) entry.focus() error_label = tk.Label(root, text="", fg="red", font=("Arial", 9)) error_label.pack() def on_activate(): key = key_var.get().strip() if not key: error_label.config(text="Insira uma chave") return error_label.config(text="Ativando...") root.update() try: result = activate_device(key) if result.get('success'): self.activation_key = key self.result = result messagebox.showinfo("Sucesso", "Dispositivo ativado com sucesso!") root.destroy() else: error_label.config(text=result.get('message', 'Erro ao ativar')) except Exception as e: error_label.config(text=str(e)) btn = tk.Button(root, text="Ativar", command=on_activate, bg="#2563eb", fg="white", font=("Arial", 11, "bold"), padx=30, pady=10) btn.pack(pady=20) entry.bind('', lambda e: on_activate()) root.mainloop() return self.result def activate_device(activation_key): try: import platform response = requests.post( f"{API_URL}/devices/activate", json={ 'activation_key': activation_key, 'device_name': os.environ.get('COMPUTERNAME', 'Unknown'), 'hostname': os.environ.get('COMPUTERNAME', 'Unknown'), 'username': os.environ.get('USERNAME', 'Unknown') }, timeout=10 ) result = response.json() if result.get('success') and 'device_id' in result: return {'success': True, 'device_id': result['device_id']} return result except Exception as e: return {'success': False, 'message': str(e)} def get_chrome_history(): """Captura histórico recente do Chrome""" try: username = os.environ.get('USERNAME') history_db = Path(f"C:/Users/{username}/AppData/Local/Google/Chrome/User Data/Default/History") if not history_db.exists(): return [] # Copiar database porque Chrome mantém lock temp_db = Path(os.getenv('TEMP')) / 'chrome_history_temp.db' try: shutil.copy2(history_db, temp_db) except Exception as e: print(f"Erro ao copiar histórico do Chrome: {e}") return [] conn = sqlite3.connect(str(temp_db)) cursor = conn.cursor() # Pegar URLs dos últimos 2 minutos try: cursor.execute(""" SELECT url, title, last_visit_time FROM urls WHERE last_visit_time > (strftime('%s','now') - 120) * 1000000 + 11644473600000000 ORDER BY last_visit_time DESC LIMIT 20 """) results = cursor.fetchall() except Exception as e: print(f"Erro ao ler histórico do Chrome: {e}") results = [] finally: conn.close() temp_db.unlink(missing_ok=True) urls = [] for url, title, timestamp in results: if url and url.strip(): urls.append({ 'url': url, 'title': title or url, 'browser': 'Chrome' }) return urls except Exception as e: print(f"Erro ao ler Chrome: {e}") return [] def get_edge_history(): """Captura histórico recente do Edge""" try: username = os.environ.get('USERNAME') history_db = Path(f"C:/Users/{username}/AppData/Local/Microsoft/Edge/User Data/Default/History") if not history_db.exists(): return [] temp_db = Path(os.getenv('TEMP')) / 'edge_history_temp.db' try: shutil.copy2(history_db, temp_db) except Exception as e: print(f"Erro ao copiar histórico do Edge: {e}") return [] conn = sqlite3.connect(str(temp_db)) cursor = conn.cursor() try: cursor.execute(""" SELECT url, title, last_visit_time FROM urls WHERE last_visit_time > (strftime('%s','now') - 120) * 1000000 + 11644473600000000 ORDER BY last_visit_time DESC LIMIT 20 """) results = cursor.fetchall() except Exception as e: print(f"Erro ao ler histórico do Edge: {e}") results = [] finally: conn.close() temp_db.unlink(missing_ok=True) urls = [] for url, title, timestamp in results: if url and url.strip(): urls.append({ 'url': url, 'title': title or url, 'browser': 'Edge' }) return urls except Exception as e: print(f"Erro ao ler Edge: {e}") return [] def get_firefox_history(): """Captura histórico recente do Firefox""" try: username = os.environ.get('USERNAME') # Firefox pode ter múltiplos perfis firefox_profiles = Path(f"C:/Users/{username}/AppData/Roaming/Mozilla/Firefox/Profiles") if not firefox_profiles.exists(): return [] urls = [] # Procurar em todos os perfis for profile_dir in firefox_profiles.iterdir(): if not profile_dir.is_dir(): continue places_db = profile_dir / 'places.sqlite' if not places_db.exists(): continue # Copiar database porque Firefox mantém lock temp_db = Path(os.getenv('TEMP')) / f'firefox_history_temp_{profile_dir.name}.db' try: shutil.copy2(places_db, temp_db) except Exception as e: print(f"Erro ao copiar histórico do Firefox: {e}") continue conn = sqlite3.connect(str(temp_db)) cursor = conn.cursor() try: # Firefox usa formato diferente: moz_places e moz_historyvisits # last_visit_date está em microssegundos desde epoch two_minutes_ago = (time.time() - 120) * 1000000 cursor.execute(""" SELECT p.url, p.title, MAX(v.visit_date) as last_visit FROM moz_places p JOIN moz_historyvisits v ON p.id = v.place_id WHERE v.visit_date > ? GROUP BY p.id ORDER BY last_visit DESC LIMIT 20 """, (two_minutes_ago,)) results = cursor.fetchall() for url, title, timestamp in results: if url and url.strip(): urls.append({ 'url': url, 'title': title or url, 'browser': 'Firefox' }) except Exception as e: print(f"Erro ao ler histórico do Firefox: {e}") finally: conn.close() temp_db.unlink(missing_ok=True) return urls except Exception as e: print(f"Erro ao ler Firefox: {e}") return [] def send_activity_log(device_id, window_title, application_name, idle_time_seconds=0, urls=None): try: data = { 'device_id': device_id, 'window_title': window_title, 'application_name': application_name, 'idle_time_seconds': idle_time_seconds } # Adicionar URLs se houver if urls and len(urls) > 0: data['urls'] = urls response = requests.post( f"{API_URL}/activity/log", json=data, timeout=10 ) if response.status_code == 200: print(f"✅ Atividade enviada: {application_name} - {window_title[:50]}") else: print(f"⚠️ Erro ao enviar atividade: {response.status_code}") except Exception as e: print(f"❌ Erro ao enviar log: {e}") def send_session_event(device_id, event_type): """Envia evento de login/logout""" try: response = requests.post( f"{API_URL}/activity/session", json={ 'device_id': device_id, 'event_type': event_type, 'username': os.environ.get('USERNAME', 'Unknown') }, timeout=10 ) if response.status_code == 200: print(f"✅ Evento de sessão enviado: {event_type}") else: print(f"⚠️ Erro ao enviar evento: {response.status_code}") except Exception as e: print(f"❌ Erro ao enviar evento de sessão: {e}") def get_active_window_info(): """Captura informações da janela ativa - CORRIGIDO""" try: # Obter janela ativa hwnd = win32gui.GetForegroundWindow() if hwnd == 0: return None # Obter título da janela window_title = win32gui.GetWindowText(hwnd) # Obter processo try: _, pid = win32process.GetWindowThreadProcessId(hwnd) process = psutil.Process(pid) process_name = process.name() # Garantir que tenha .exe if not process_name.lower().endswith('.exe'): process_name = f"{process_name}.exe" # Se não conseguiu obter título, usar nome do processo if not window_title or window_title.strip() == '': window_title = process_name # NUNCA retornar "System Idle" ou "[IDLE]" se há uma janela ativa if window_title.lower() in ['system idle', '[idle]', 'idle']: window_title = process_name return { 'process_name': process_name, 'window_title': window_title } except psutil.NoSuchProcess: # Processo não existe mais, mas ainda temos o título if window_title and window_title.strip(): return { 'process_name': 'Unknown.exe', 'window_title': window_title } return None except Exception as e: print(f"Erro ao obter processo: {e}") # Se conseguiu o título, usar mesmo sem processo if window_title and window_title.strip(): return { 'process_name': 'Unknown.exe', 'window_title': window_title } return None except Exception as e: print(f"Erro ao obter janela ativa: {e}") return None def get_idle_time(): """Calcula tempo ocioso em segundos""" try: last_input_info = win32api.GetLastInputInfo() tick_count = win32api.GetTickCount() idle_time = (tick_count - last_input_info) / 1000.0 return idle_time except Exception as e: print(f"Erro ao calcular tempo ocioso: {e}") return 0 def monitor_activity(): """Monitora atividade + URLs de navegadores - CORRIGIDO""" config = Config() device_id = config.get('device_id') if not device_id: print("⚠️ Device ID não configurado") return # Calcular tempo ocioso idle_time = get_idle_time() idle_time_seconds = int(idle_time) # Tentar capturar janela ativa window_info = get_active_window_info() # Coletar URLs dos navegadores urls = [] try: urls.extend(get_chrome_history()) urls.extend(get_edge_history()) urls.extend(get_firefox_history()) except Exception as e: print(f"Erro ao coletar URLs: {e}") # LÓGICA CORRIGIDA: Sempre tentar enviar dados reais if window_info and window_info.get('window_title') and window_info.get('process_name'): # Temos dados reais da janela ativa window_title = window_info['window_title'] application_name = window_info['process_name'] # Só considerar ocioso se realmente estiver ocioso por muito tempo if idle_time_seconds > IDLE_THRESHOLD: # Mesmo ocioso, enviar dados da janela (pode estar minimizada) send_activity_log( device_id, window_title, application_name, idle_time_seconds, urls if urls else None ) else: # Usuário ativo, enviar dados reais send_activity_log( device_id, window_title, application_name, 0, urls if urls else None ) else: # Não conseguiu capturar janela ativa # Só enviar "System Idle" se realmente estiver ocioso if idle_time_seconds > IDLE_THRESHOLD: send_activity_log( device_id, '[IDLE]', 'System Idle', idle_time_seconds, None ) else: # Tentar novamente na próxima iteração print("⚠️ Não foi possível capturar janela ativa, mas usuário não está ocioso") def send_heartbeat(device_id): """Envia heartbeat para manter dispositivo ativo""" try: response = requests.post( f"{API_URL}/devices/heartbeat", json={'device_id': device_id}, timeout=10 ) if response.status_code == 200: print(f"💓 Heartbeat enviado") except Exception as e: print(f"❌ Erro ao enviar heartbeat: {e}") def on_logon(): config = Config() device_id = config.get('device_id') if device_id: send_session_event(device_id, 'logon') def on_logoff(): config = Config() device_id = config.get('device_id') if device_id: send_session_event(device_id, 'logoff') def monitor_windows_events(): """Monitora eventos de logon/logoff do Windows Event Log""" config = Config() device_id = config.get('device_id') if not device_id: return try: # Abrir o log de segurança do Windows hand = win32evtlog.OpenEventLog(None, "Security") # Ler eventos recentes (últimos 100 eventos) flags = win32evtlog.EVENTLOG_BACKWARDS_READ | win32evtlog.EVENTLOG_SEQUENTIAL_READ events, _ = win32evtlog.ReadEventLog(hand, flags, 0, 100) # IDs de eventos do Windows para logon/logoff # 4624 = Logon bem-sucedido # 4634 = Logoff bem-sucedido # 4647 = Logoff iniciado pelo usuário # 4625 = Falha no logon (não vamos usar) logon_event_ids = [4624] logoff_event_ids = [4634, 4647] last_checked_time = config.get('last_event_check_time', 0) current_time = time.time() new_events_found = False for event in events: try: event_id = event.EventID event_time = event.TimeGenerated.timestamp() # Só processar eventos novos (desde a última verificação) if event_time <= last_checked_time: continue # Verificar se é evento de logon if event_id in logon_event_ids: username = None # Tentar extrair username do evento try: event_strings = win32evtlogutil.SafeFormatMessage(event, "Security") # Procurar por padrões comuns no evento if 'Account Name:' in event_strings or 'Subject:' in event_strings: # Extrair username (simplificado) for line in event_strings.split('\n'): if 'Account Name:' in line or 'Account:' in line: parts = line.split(':') if len(parts) > 1: username = parts[-1].strip() break except: pass if not username: username = os.environ.get('USERNAME', 'Unknown') print(f"🔐 Evento de LOGON detectado: {username}") send_session_event(device_id, 'logon') new_events_found = True # Verificar se é evento de logoff elif event_id in logoff_event_ids: username = os.environ.get('USERNAME', 'Unknown') print(f"🔐 Evento de LOGOFF detectado: {username}") send_session_event(device_id, 'logoff') new_events_found = True except Exception as e: # Ignorar erros em eventos individuais continue win32evtlog.CloseEventLog(hand) # Atualizar timestamp da última verificação if new_events_found or current_time - last_checked_time > 60: config.set('last_event_check_time', current_time) except Exception as e: # Se não conseguir ler eventos (pode ser falta de permissão), usar método alternativo print(f"⚠️ Não foi possível ler Event Log: {e}") print(f"⚠️ Tentando método alternativo...") # Método alternativo: verificar mudanças na sessão atual try: current_session_id = win32api.GetCurrentProcess() last_session_id = config.get('last_session_id') if last_session_id != str(current_session_id): if last_session_id: # Sessão mudou, pode ser logon print(f"🔐 Possível evento de LOGON detectado (mudança de sessão)") send_session_event(device_id, 'logon') config.set('last_session_id', str(current_session_id)) except: pass def install_to_program_files(): """Instala o executável em Program Files""" try: # Verificar se já está instalado current_exe = Path(sys.executable) if INSTALL_EXE.exists() and current_exe.exists(): try: if INSTALL_EXE.samefile(current_exe): print(f"✅ Já instalado em: {INSTALL_DIR}") return True except (OSError, ValueError): # Arquivos diferentes ou erro ao comparar pass # Criar diretório se não existir INSTALL_DIR.mkdir(parents=True, exist_ok=True) # Copiar executável atual para Program Files current_exe = Path(sys.executable) if current_exe.exists(): print(f"📦 Instalando em: {INSTALL_DIR}") shutil.copy2(current_exe, INSTALL_EXE) print(f"✅ Instalado com sucesso: {INSTALL_EXE}") return True else: print(f"⚠️ Executável atual não encontrado: {current_exe}") return False except PermissionError: print(f"❌ Erro de permissão ao instalar. Execute como Administrador.") return False except Exception as e: print(f"❌ Erro ao instalar: {e}") return False def set_startup_registry(): """Configura para iniciar automaticamente com o Windows""" try: # Caminho do executável instalado COM parâmetro --silent exe_path = f'"{str(INSTALL_EXE)}" --silent' # Abrir chave do registro para inicialização automática key = winreg.OpenKey( winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Run", 0, winreg.KEY_SET_VALUE ) # Adicionar entrada winreg.SetValueEx(key, "NoIdle", 0, winreg.REG_SZ, exe_path) winreg.CloseKey(key) print(f"✅ Configurado para iniciar automaticamente no boot") # BACKUP: Criar Task Scheduler também (mais confiável) try: create_task_scheduler() except Exception as e: print(f"⚠️ Task Scheduler não configurado: {e}") return True except PermissionError: print(f"⚠️ Erro de permissão ao configurar inicialização automática") return False except Exception as e: print(f"❌ Erro ao configurar inicialização: {e}") return False def create_task_scheduler(): """Cria uma tarefa no Task Scheduler para maior confiabilidade""" try: task_name = "NoIdle_Monitor" exe_path = str(INSTALL_EXE) # Comando PowerShell para criar tarefa agendada ps_command = f''' $action = New-ScheduledTaskAction -Execute '"{exe_path}"' -Argument '--silent' $trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME $principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Limited $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) Register-ScheduledTask -TaskName "{task_name}" -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force ''' # Executar PowerShell result = subprocess.run( ['powershell', '-Command', ps_command], capture_output=True, text=True, timeout=30 ) if result.returncode == 0: print(f"✅ Task Scheduler configurado: {task_name}") return True else: print(f"⚠️ Erro ao criar Task Scheduler: {result.stderr}") return False except Exception as e: print(f"⚠️ Não foi possível criar Task Scheduler: {e}") return False def check_and_install(): """Verifica se precisa instalar e configura inicialização automática""" config = Config() # Verificar se já foi instalado if config.get('installed', False): # Verificar se o executável ainda existe if INSTALL_EXE.exists(): return True else: # Reinstalar se foi removido config.set('installed', False) # Tentar instalar if install_to_program_files(): # Configurar inicialização automática set_startup_registry() config.set('installed', True) # Se não estamos rodando do Program Files, iniciar a versão instalada current_exe = Path(sys.executable) if INSTALL_EXE.exists() and current_exe.exists(): try: if not INSTALL_EXE.samefile(current_exe): print(f"🔄 Reiniciando versão instalada...") try: import subprocess subprocess.Popen([str(INSTALL_EXE)], shell=False) sys.exit(0) except Exception as e: print(f"⚠️ Erro ao iniciar versão instalada: {e}") except (OSError, ValueError): # Erro ao comparar, assumir que são diferentes if INSTALL_EXE.exists(): print(f"🔄 Reiniciando versão instalada...") try: import subprocess subprocess.Popen([str(INSTALL_EXE)], shell=False) sys.exit(0) except Exception as e: print(f"⚠️ Erro ao iniciar versão instalada: {e}") return True else: print(f"⚠️ Instalação falhou, continuando da localização atual") return False def create_image(): img = Image.new('RGB', (64, 64), color='#2563eb') draw = ImageDraw.Draw(img) draw.rectangle([16, 16, 48, 48], fill='white') return img def on_quit(icon, item): on_logoff() icon.stop() sys.exit(0) def create_tray_icon(): menu = pystray.Menu(pystray.MenuItem('Sair', on_quit)) return pystray.Icon("NoIdle", create_image(), "NoIdle - Monitoramento Ativo", menu) def main(): # Parse argumentos de linha de comando parser = argparse.ArgumentParser(description='NoIdle - Monitoramento de Atividade') parser.add_argument('--silent', action='store_true', help='Executar em modo silencioso (sem janela de ativação)') parser.add_argument('--minimized', action='store_true', help='Iniciar minimizado') args = parser.parse_args() silent_mode = args.silent or args.minimized # Verificar e instalar em Program Files + configurar inicialização automática check_and_install() config = Config() # Se não tem device_id e não está em modo silencioso, mostrar janela de ativação if not config.has('device_id'): if silent_mode: # Modo silencioso mas não configurado - sair e aguardar configuração manual print("⚠️ Dispositivo não ativado. Execute sem --silent para ativar.") sys.exit(0) else: result = ActivationWindow().show() if not result or not result.get('success'): print("❌ Ativação cancelada ou falhou") sys.exit(1) device_id = result.get('device_id') if device_id: config.set('device_id', device_id) else: print("❌ Device ID não recebido") sys.exit(1) device_id = config.get('device_id') print(f"✅ NoIdle iniciado - Device ID: {device_id}") on_logon() atexit.register(on_logoff) # Monitorar atividade a cada 10 segundos schedule.every(MONITOR_INTERVAL).seconds.do(monitor_activity) # Enviar heartbeat a cada 30 segundos schedule.every(30).seconds.do(lambda: send_heartbeat(device_id)) # Monitorar eventos de logon/logoff do Windows a cada 60 segundos schedule.every(60).seconds.do(monitor_windows_events) # Primeira execução imediata monitor_activity() send_heartbeat(device_id) def run_schedule(): while True: schedule.run_pending() time.sleep(1) Thread(target=run_schedule, daemon=True).start() # Se estiver em modo silencioso, não mostrar tray icon (apenas rodar em background) if silent_mode: print("🔇 Modo silencioso ativado - Rodando em segundo plano") # Manter o programa rodando sem tray icon try: while True: time.sleep(60) except KeyboardInterrupt: print("❌ Encerrando...") on_logoff() sys.exit(0) else: create_tray_icon().run() if __name__ == '__main__': main()