feat: add CI/CD workflows, Docker deployment, and ML analytics service to project
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
Tikker AI Microservice
|
||||
|
||||
Provides AI-powered analysis of keystroke data using OpenAI API.
|
||||
Handles text analysis, pattern detection, and insights generation.
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from typing import Dict, Any, Optional, List
|
||||
import logging
|
||||
import os
|
||||
|
||||
try:
|
||||
from openai import OpenAI
|
||||
except ImportError:
|
||||
OpenAI = None
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(
|
||||
title="Tikker AI Service",
|
||||
description="AI analysis for keystroke data",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
client = None
|
||||
api_key = os.getenv("OPENAI_API_KEY")
|
||||
|
||||
if api_key:
|
||||
try:
|
||||
client = OpenAI(api_key=api_key)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize OpenAI client: {e}")
|
||||
|
||||
|
||||
class TextAnalysisRequest(BaseModel):
|
||||
text: str
|
||||
analysis_type: str = "general"
|
||||
|
||||
|
||||
class AnalysisResult(BaseModel):
|
||||
text: str
|
||||
analysis_type: str
|
||||
summary: str
|
||||
keywords: List[str]
|
||||
sentiment: Optional[str] = None
|
||||
insights: List[str]
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
status: str
|
||||
ai_available: bool
|
||||
api_version: str
|
||||
|
||||
|
||||
@app.get("/health", response_model=HealthResponse)
|
||||
async def health_check() -> HealthResponse:
|
||||
"""Health check endpoint."""
|
||||
return HealthResponse(
|
||||
status="healthy",
|
||||
ai_available=client is not None,
|
||||
api_version="1.0.0"
|
||||
)
|
||||
|
||||
|
||||
@app.post("/analyze", response_model=AnalysisResult)
|
||||
async def analyze_text(request: TextAnalysisRequest) -> AnalysisResult:
|
||||
"""
|
||||
Analyze text using AI.
|
||||
|
||||
Args:
|
||||
request: Text analysis request with text and analysis type
|
||||
|
||||
Returns:
|
||||
Analysis result with summary, keywords, and insights
|
||||
"""
|
||||
if not client:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="AI service not available - no API key configured"
|
||||
)
|
||||
|
||||
if not request.text or len(request.text.strip()) == 0:
|
||||
raise HTTPException(status_code=400, detail="Text cannot be empty")
|
||||
|
||||
try:
|
||||
analysis_type = request.analysis_type.lower()
|
||||
|
||||
if analysis_type == "activity":
|
||||
prompt = f"""Analyze this keystroke activity log and provide:
|
||||
1. A brief summary (1-2 sentences)
|
||||
2. Key patterns or observations (3-4 bullet points)
|
||||
3. Sentiment or work intensity assessment
|
||||
|
||||
Text: {request.text}
|
||||
|
||||
Respond in JSON format with keys: summary, keywords (list), insights (list), sentiment"""
|
||||
elif analysis_type == "productivity":
|
||||
prompt = f"""Analyze this text for productivity patterns and provide:
|
||||
1. Summary of productivity indicators
|
||||
2. Key terms related to productivity
|
||||
3. Specific insights about work patterns
|
||||
|
||||
Text: {request.text}
|
||||
|
||||
Respond in JSON format with keys: summary, keywords (list), insights (list)"""
|
||||
else:
|
||||
prompt = f"""Provide a general analysis of this text:
|
||||
1. Brief summary (1-2 sentences)
|
||||
2. Important keywords or themes
|
||||
3. Key insights
|
||||
|
||||
Text: {request.text}
|
||||
|
||||
Respond in JSON format with keys: summary, keywords (list), insights (list)"""
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a helpful analyst. Always respond in valid JSON format."},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=0.7,
|
||||
max_tokens=500
|
||||
)
|
||||
|
||||
result_text = response.choices[0].message.content
|
||||
|
||||
import json
|
||||
try:
|
||||
parsed = json.loads(result_text)
|
||||
except:
|
||||
parsed = {
|
||||
"summary": result_text[:100],
|
||||
"keywords": ["analysis"],
|
||||
"insights": [result_text]
|
||||
}
|
||||
|
||||
return AnalysisResult(
|
||||
text=request.text,
|
||||
analysis_type=analysis_type,
|
||||
summary=parsed.get("summary", ""),
|
||||
keywords=parsed.get("keywords", []),
|
||||
sentiment=parsed.get("sentiment"),
|
||||
insights=parsed.get("insights", [])
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Analysis error: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root() -> Dict[str, Any]:
|
||||
"""Root endpoint with service information."""
|
||||
return {
|
||||
"name": "Tikker AI Service",
|
||||
"version": "1.0.0",
|
||||
"status": "running",
|
||||
"ai_available": client is not None,
|
||||
"endpoints": {
|
||||
"health": "/health",
|
||||
"analyze": "/analyze"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8001)
|
||||
@@ -0,0 +1,352 @@
|
||||
"""
|
||||
Tikker API with C Tools Integration
|
||||
|
||||
FastAPI endpoints that call C tools for statistics and report generation.
|
||||
Maintains 100% backwards compatibility with original API interface.
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, HTTPException, BackgroundTasks, Query
|
||||
from fastapi.responses import FileResponse, HTMLResponse
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Dict, Any, Optional
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from c_tools_wrapper import CToolsWrapper, ToolError
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Initialize FastAPI app
|
||||
app = FastAPI(
|
||||
title="Tikker API",
|
||||
description="Enterprise keystroke analytics API with C backend",
|
||||
version="2.0.0"
|
||||
)
|
||||
|
||||
# Initialize C tools wrapper
|
||||
try:
|
||||
tools = CToolsWrapper(
|
||||
tools_dir=os.getenv("TOOLS_DIR", "./build/bin"),
|
||||
db_path=os.getenv("DB_PATH", "tikker.db")
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize C tools: {e}")
|
||||
tools = None
|
||||
|
||||
|
||||
# Pydantic models
|
||||
class DailyStats(BaseModel):
|
||||
presses: int
|
||||
releases: int
|
||||
repeats: int
|
||||
total: int
|
||||
|
||||
|
||||
class WordStat(BaseModel):
|
||||
rank: int
|
||||
word: str
|
||||
count: int
|
||||
percentage: float
|
||||
|
||||
|
||||
class DecoderRequest(BaseModel):
|
||||
input_file: str
|
||||
output_file: str
|
||||
verbose: bool = False
|
||||
|
||||
|
||||
class ReportRequest(BaseModel):
|
||||
output_file: str = "report.html"
|
||||
input_dir: str = "logs_plain"
|
||||
title: str = "Tikker Activity Report"
|
||||
|
||||
|
||||
# Health check endpoint
|
||||
@app.get("/health")
|
||||
async def health_check() -> Dict[str, Any]:
|
||||
"""
|
||||
Check API and C tools health status.
|
||||
|
||||
Returns:
|
||||
Health status and tool information
|
||||
"""
|
||||
if not tools:
|
||||
raise HTTPException(status_code=503, detail="C tools not initialized")
|
||||
|
||||
return tools.health_check()
|
||||
|
||||
|
||||
# Statistics endpoints
|
||||
@app.get("/api/stats/daily", response_model=DailyStats)
|
||||
async def get_daily_stats() -> DailyStats:
|
||||
"""
|
||||
Get daily keystroke statistics.
|
||||
|
||||
Returns:
|
||||
Daily statistics (presses, releases, repeats, total)
|
||||
"""
|
||||
if not tools:
|
||||
raise HTTPException(status_code=503, detail="C tools not available")
|
||||
|
||||
try:
|
||||
stats = tools.get_daily_stats()
|
||||
return DailyStats(**stats)
|
||||
except ToolError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/api/stats/hourly")
|
||||
async def get_hourly_stats(date: str = Query(..., description="Date in YYYY-MM-DD format")) -> Dict[str, Any]:
|
||||
"""
|
||||
Get hourly keystroke statistics for a specific date.
|
||||
|
||||
Args:
|
||||
date: Date in YYYY-MM-DD format
|
||||
|
||||
Returns:
|
||||
Hourly statistics
|
||||
"""
|
||||
if not tools:
|
||||
raise HTTPException(status_code=503, detail="C tools not available")
|
||||
|
||||
try:
|
||||
return tools.get_hourly_stats(date)
|
||||
except ToolError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/api/stats/weekly")
|
||||
async def get_weekly_stats() -> Dict[str, Any]:
|
||||
"""
|
||||
Get weekly keystroke statistics.
|
||||
|
||||
Returns:
|
||||
Weekly statistics breakdown
|
||||
"""
|
||||
if not tools:
|
||||
raise HTTPException(status_code=503, detail="C tools not available")
|
||||
|
||||
try:
|
||||
return tools.get_weekly_stats()
|
||||
except ToolError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/api/stats/weekday")
|
||||
async def get_weekday_stats() -> Dict[str, Any]:
|
||||
"""
|
||||
Get weekday comparison statistics.
|
||||
|
||||
Returns:
|
||||
Statistics grouped by day of week
|
||||
"""
|
||||
if not tools:
|
||||
raise HTTPException(status_code=503, detail="C tools not available")
|
||||
|
||||
try:
|
||||
return tools.get_weekday_stats()
|
||||
except ToolError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# Word analysis endpoints
|
||||
@app.get("/api/words/top", response_model=List[WordStat])
|
||||
async def get_top_words(limit: int = Query(10, ge=1, le=100, description="Number of words to return")) -> List[WordStat]:
|
||||
"""
|
||||
Get top N most popular words.
|
||||
|
||||
Args:
|
||||
limit: Number of words to return (1-100)
|
||||
|
||||
Returns:
|
||||
List of words with frequency and rank
|
||||
"""
|
||||
if not tools:
|
||||
raise HTTPException(status_code=503, detail="C tools not available")
|
||||
|
||||
try:
|
||||
words = tools.get_top_words(limit)
|
||||
return [WordStat(**w) for w in words]
|
||||
except ToolError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/api/words/find")
|
||||
async def find_word(word: str = Query(..., description="Word to search for")) -> Dict[str, Any]:
|
||||
"""
|
||||
Find statistics for a specific word.
|
||||
|
||||
Args:
|
||||
word: Word to search for
|
||||
|
||||
Returns:
|
||||
Word frequency, rank, and statistics
|
||||
"""
|
||||
if not tools:
|
||||
raise HTTPException(status_code=503, detail="C tools not available")
|
||||
|
||||
try:
|
||||
return tools.find_word(word)
|
||||
except ToolError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# Indexing endpoints
|
||||
@app.post("/api/index")
|
||||
async def build_index(dir_path: str = Query("logs_plain", description="Directory to index")) -> Dict[str, Any]:
|
||||
"""
|
||||
Build word index from text files.
|
||||
|
||||
Args:
|
||||
dir_path: Directory containing text files
|
||||
|
||||
Returns:
|
||||
Indexing results and statistics
|
||||
"""
|
||||
if not tools:
|
||||
raise HTTPException(status_code=503, detail="C tools not available")
|
||||
|
||||
try:
|
||||
return tools.index_directory(dir_path)
|
||||
except ToolError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# Decoding endpoints
|
||||
@app.post("/api/decode")
|
||||
async def decode_file(request: DecoderRequest, background_tasks: BackgroundTasks) -> Dict[str, Any]:
|
||||
"""
|
||||
Decode keystroke token file to readable text.
|
||||
|
||||
Args:
|
||||
request: Decoder request with input/output paths
|
||||
background_tasks: Background task runner
|
||||
|
||||
Returns:
|
||||
Decoding result
|
||||
"""
|
||||
if not tools:
|
||||
raise HTTPException(status_code=503, detail="C tools not available")
|
||||
|
||||
try:
|
||||
result = tools.decode_file(request.input_file, request.output_file, request.verbose)
|
||||
return result
|
||||
except ToolError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# Report generation endpoints
|
||||
@app.post("/api/report")
|
||||
async def generate_report(request: ReportRequest, background_tasks: BackgroundTasks) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate HTML activity report.
|
||||
|
||||
Args:
|
||||
request: Report configuration
|
||||
background_tasks: Background task runner
|
||||
|
||||
Returns:
|
||||
Report generation result
|
||||
"""
|
||||
if not tools:
|
||||
raise HTTPException(status_code=503, detail="C tools not available")
|
||||
|
||||
try:
|
||||
result = tools.generate_report(
|
||||
output_file=request.output_file,
|
||||
input_dir=request.input_dir,
|
||||
title=request.title
|
||||
)
|
||||
return result
|
||||
except ToolError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/api/report/{filename}")
|
||||
async def get_report(filename: str) -> FileResponse:
|
||||
"""
|
||||
Download generated report file.
|
||||
|
||||
Args:
|
||||
filename: Report filename (without path)
|
||||
|
||||
Returns:
|
||||
File response with report content
|
||||
"""
|
||||
file_path = Path(filename)
|
||||
|
||||
# Security check - prevent directory traversal
|
||||
if ".." in filename or "/" in filename:
|
||||
raise HTTPException(status_code=400, detail="Invalid filename")
|
||||
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Report not found")
|
||||
|
||||
return FileResponse(path=file_path, filename=filename, media_type="text/html")
|
||||
|
||||
|
||||
# Root endpoint (for backwards compatibility)
|
||||
@app.get("/")
|
||||
async def root() -> Dict[str, Any]:
|
||||
"""
|
||||
Root API endpoint.
|
||||
|
||||
Returns:
|
||||
API information
|
||||
"""
|
||||
return {
|
||||
"name": "Tikker API",
|
||||
"version": "2.0.0",
|
||||
"status": "running",
|
||||
"backend": "C tools (libtikker)",
|
||||
"endpoints": {
|
||||
"health": "/health",
|
||||
"stats": "/api/stats/daily, /api/stats/hourly, /api/stats/weekly, /api/stats/weekday",
|
||||
"words": "/api/words/top, /api/words/find",
|
||||
"operations": "/api/index, /api/decode, /api/report"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Backwards compatibility endpoint
|
||||
@app.get("/api/all-stats")
|
||||
async def all_stats() -> Dict[str, Any]:
|
||||
"""
|
||||
Get all statistics (backwards compatibility endpoint).
|
||||
|
||||
Returns:
|
||||
Comprehensive statistics
|
||||
"""
|
||||
if not tools:
|
||||
raise HTTPException(status_code=503, detail="C tools not available")
|
||||
|
||||
try:
|
||||
daily = tools.get_daily_stats()
|
||||
weekly = tools.get_weekly_stats()
|
||||
top_words = tools.get_top_words(10)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"daily": daily,
|
||||
"weekly": weekly,
|
||||
"top_words": top_words,
|
||||
"backend": "C"
|
||||
}
|
||||
except ToolError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# Exception handlers
|
||||
@app.exception_handler(ToolError)
|
||||
async def tool_error_handler(request, exc):
|
||||
"""Handle C tool errors."""
|
||||
logger.error(f"C tool error: {exc}")
|
||||
return HTTPException(status_code=500, detail=str(exc))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
@@ -0,0 +1,401 @@
|
||||
"""
|
||||
C Tools Wrapper for Tikker API
|
||||
|
||||
This module provides a Python wrapper around the compiled C tools
|
||||
(tikker-decoder, tikker-indexer, tikker-aggregator, tikker-report).
|
||||
|
||||
It handles subprocess execution, error handling, and result parsing.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
import tempfile
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Optional
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ToolError(Exception):
|
||||
"""Raised when a C tool execution fails."""
|
||||
pass
|
||||
|
||||
|
||||
class CToolsWrapper:
|
||||
"""Wrapper for C command-line tools."""
|
||||
|
||||
def __init__(self, tools_dir: str = "./build/bin", db_path: str = "tikker.db"):
|
||||
"""
|
||||
Initialize the C tools wrapper.
|
||||
|
||||
Args:
|
||||
tools_dir: Directory containing compiled C binaries
|
||||
db_path: Path to SQLite database
|
||||
"""
|
||||
self.tools_dir = Path(tools_dir)
|
||||
self.db_path = db_path
|
||||
|
||||
# Verify tools exist
|
||||
self._verify_tools()
|
||||
|
||||
def _verify_tools(self):
|
||||
"""Verify all required tools are available and executable."""
|
||||
required_tools = [
|
||||
"tikker-decoder",
|
||||
"tikker-indexer",
|
||||
"tikker-aggregator",
|
||||
"tikker-report"
|
||||
]
|
||||
|
||||
for tool in required_tools:
|
||||
tool_path = self.tools_dir / tool
|
||||
if not tool_path.exists():
|
||||
raise ToolError(f"Tool not found: {tool_path}")
|
||||
if not os.access(tool_path, os.X_OK):
|
||||
raise ToolError(f"Tool not executable: {tool_path}")
|
||||
|
||||
logger.info(f"All C tools verified in {self.tools_dir}")
|
||||
|
||||
def _run_tool(self, tool_name: str, args: List[str],
|
||||
capture_output: bool = True) -> str:
|
||||
"""
|
||||
Run a C tool and return output.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool (e.g., "tikker-decoder")
|
||||
args: Command-line arguments
|
||||
capture_output: Whether to capture stdout
|
||||
|
||||
Returns:
|
||||
Tool output as string
|
||||
|
||||
Raises:
|
||||
ToolError: If tool execution fails
|
||||
"""
|
||||
cmd = [str(self.tools_dir / tool_name)] + args
|
||||
|
||||
try:
|
||||
logger.debug(f"Running: {' '.join(cmd)}")
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=capture_output,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
error_msg = result.stderr or result.stdout or "Unknown error"
|
||||
raise ToolError(f"Tool {tool_name} failed: {error_msg}")
|
||||
|
||||
return result.stdout
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
raise ToolError(f"Tool {tool_name} timed out after 30 seconds")
|
||||
except Exception as e:
|
||||
raise ToolError(f"Tool {tool_name} error: {str(e)}")
|
||||
|
||||
def decode_file(self, input_path: str, output_path: str,
|
||||
verbose: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
Decode a keystroke log file.
|
||||
|
||||
Args:
|
||||
input_path: Path to input keystroke token file
|
||||
output_path: Path to output decoded text file
|
||||
verbose: Show verbose output
|
||||
|
||||
Returns:
|
||||
Dictionary with decoding results
|
||||
"""
|
||||
args = []
|
||||
if verbose:
|
||||
args.append("--verbose")
|
||||
args.extend([input_path, output_path])
|
||||
|
||||
self._run_tool("tikker-decoder", args)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"input": input_path,
|
||||
"output": output_path,
|
||||
"message": "File decoded successfully"
|
||||
}
|
||||
|
||||
def index_directory(self, dir_path: str = "logs_plain",
|
||||
db_path: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Build word index from directory.
|
||||
|
||||
Args:
|
||||
dir_path: Directory containing text files
|
||||
db_path: Custom database path (default: self.db_path)
|
||||
|
||||
Returns:
|
||||
Dictionary with indexing statistics
|
||||
"""
|
||||
db = db_path or self.db_path
|
||||
args = ["--index", "--database", db]
|
||||
|
||||
output = self._run_tool("tikker-indexer", args)
|
||||
|
||||
# Parse output for statistics
|
||||
stats = {
|
||||
"status": "success",
|
||||
"directory": dir_path,
|
||||
"database": db,
|
||||
}
|
||||
|
||||
# Extract statistics from output
|
||||
for line in output.split('\n'):
|
||||
if "unique words:" in line:
|
||||
try:
|
||||
stats["unique_words"] = int(line.split(':')[1].strip())
|
||||
except:
|
||||
pass
|
||||
elif "word count:" in line:
|
||||
try:
|
||||
stats["total_words"] = int(line.split(':')[1].strip())
|
||||
except:
|
||||
pass
|
||||
|
||||
return stats
|
||||
|
||||
def get_top_words(self, limit: int = 10,
|
||||
db_path: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get top N most popular words.
|
||||
|
||||
Args:
|
||||
limit: Number of words to return
|
||||
db_path: Custom database path
|
||||
|
||||
Returns:
|
||||
List of word statistics
|
||||
"""
|
||||
db = db_path or self.db_path
|
||||
args = ["--popular", str(limit), "--database", db]
|
||||
|
||||
output = self._run_tool("tikker-indexer", args)
|
||||
|
||||
words = []
|
||||
lines = output.split('\n')
|
||||
|
||||
# Skip header lines
|
||||
for line in lines[3:]:
|
||||
if not line.strip() or line.startswith('-'):
|
||||
continue
|
||||
|
||||
parts = line.split()
|
||||
if len(parts) >= 4:
|
||||
try:
|
||||
words.append({
|
||||
"rank": int(parts[0].replace('#', '')),
|
||||
"word": parts[1],
|
||||
"count": int(parts[2]),
|
||||
"percentage": float(parts[3].rstrip('%'))
|
||||
})
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
return words
|
||||
|
||||
def find_word(self, word: str, db_path: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Find statistics for a specific word.
|
||||
|
||||
Args:
|
||||
word: Word to search for
|
||||
db_path: Custom database path
|
||||
|
||||
Returns:
|
||||
Word statistics
|
||||
"""
|
||||
db = db_path or self.db_path
|
||||
args = ["--find", word, "--database", db]
|
||||
|
||||
output = self._run_tool("tikker-indexer", args)
|
||||
|
||||
stats = {"word": word}
|
||||
|
||||
# Parse output
|
||||
for line in output.split('\n'):
|
||||
if line.startswith("Word:"):
|
||||
stats["word"] = line.split("'")[1]
|
||||
elif line.startswith("Rank:"):
|
||||
try:
|
||||
stats["rank"] = int(line.split('#')[1])
|
||||
except:
|
||||
pass
|
||||
elif line.startswith("Frequency:"):
|
||||
try:
|
||||
stats["frequency"] = int(line.split(':')[1].strip())
|
||||
except:
|
||||
pass
|
||||
|
||||
return stats if "frequency" in stats else {"word": word, "found": False}
|
||||
|
||||
def get_daily_stats(self, db_path: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Get daily keystroke statistics.
|
||||
|
||||
Args:
|
||||
db_path: Custom database path
|
||||
|
||||
Returns:
|
||||
Daily statistics
|
||||
"""
|
||||
db = db_path or self.db_path
|
||||
args = ["--daily", "--database", db]
|
||||
|
||||
output = self._run_tool("tikker-aggregator", args)
|
||||
|
||||
stats = {}
|
||||
|
||||
# Parse output
|
||||
for line in output.split('\n'):
|
||||
if "Total Key Presses:" in line:
|
||||
try:
|
||||
stats["presses"] = int(line.split(':')[1].strip())
|
||||
except:
|
||||
pass
|
||||
elif "Total Releases:" in line:
|
||||
try:
|
||||
stats["releases"] = int(line.split(':')[1].strip())
|
||||
except:
|
||||
pass
|
||||
elif "Total Repeats:" in line:
|
||||
try:
|
||||
stats["repeats"] = int(line.split(':')[1].strip())
|
||||
except:
|
||||
pass
|
||||
elif "Total Events:" in line:
|
||||
try:
|
||||
stats["total"] = int(line.split(':')[1].strip())
|
||||
except:
|
||||
pass
|
||||
|
||||
return stats
|
||||
|
||||
def get_hourly_stats(self, date: str, db_path: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Get hourly statistics for a specific date.
|
||||
|
||||
Args:
|
||||
date: Date in YYYY-MM-DD format
|
||||
db_path: Custom database path
|
||||
|
||||
Returns:
|
||||
Hourly statistics
|
||||
"""
|
||||
db = db_path or self.db_path
|
||||
args = ["--hourly", date, "--database", db]
|
||||
|
||||
output = self._run_tool("tikker-aggregator", args)
|
||||
|
||||
return {
|
||||
"date": date,
|
||||
"output": output,
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
def get_weekly_stats(self, db_path: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Get weekly statistics.
|
||||
|
||||
Args:
|
||||
db_path: Custom database path
|
||||
|
||||
Returns:
|
||||
Weekly statistics
|
||||
"""
|
||||
db = db_path or self.db_path
|
||||
args = ["--weekly", "--database", db]
|
||||
|
||||
output = self._run_tool("tikker-aggregator", args)
|
||||
|
||||
return {
|
||||
"period": "weekly",
|
||||
"output": output,
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
def get_weekday_stats(self, db_path: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Get weekday comparison statistics.
|
||||
|
||||
Args:
|
||||
db_path: Custom database path
|
||||
|
||||
Returns:
|
||||
Weekday statistics
|
||||
"""
|
||||
db = db_path or self.db_path
|
||||
args = ["--weekday", "--database", db]
|
||||
|
||||
output = self._run_tool("tikker-aggregator", args)
|
||||
|
||||
return {
|
||||
"period": "weekday",
|
||||
"output": output,
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
def generate_report(self, output_file: str = "report.html",
|
||||
input_dir: str = "logs_plain",
|
||||
title: str = "Tikker Activity Report",
|
||||
db_path: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate HTML activity report.
|
||||
|
||||
Args:
|
||||
output_file: Path to output HTML file
|
||||
input_dir: Input logs directory
|
||||
title: Report title
|
||||
db_path: Custom database path
|
||||
|
||||
Returns:
|
||||
Report generation result
|
||||
"""
|
||||
db = db_path or self.db_path
|
||||
args = [
|
||||
"--input", input_dir,
|
||||
"--output", output_file,
|
||||
"--title", title,
|
||||
"--database", db
|
||||
]
|
||||
|
||||
self._run_tool("tikker-report", args)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"output": output_file,
|
||||
"title": title,
|
||||
"message": "Report generated successfully"
|
||||
}
|
||||
|
||||
def health_check(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Verify all tools are working.
|
||||
|
||||
Returns:
|
||||
Health check results
|
||||
"""
|
||||
health = {
|
||||
"status": "healthy",
|
||||
"tools": {}
|
||||
}
|
||||
|
||||
tools = ["tikker-decoder", "tikker-indexer", "tikker-aggregator", "tikker-report"]
|
||||
|
||||
for tool in tools:
|
||||
try:
|
||||
# Try running help command
|
||||
self._run_tool(tool, ["--help"])
|
||||
health["tools"][tool] = "ok"
|
||||
except ToolError as e:
|
||||
health["tools"][tool] = f"error: {str(e)}"
|
||||
health["status"] = "degraded"
|
||||
|
||||
return health
|
||||
@@ -0,0 +1,398 @@
|
||||
"""
|
||||
Tikker ML Analytics Module
|
||||
|
||||
Provides machine learning-based pattern detection, anomaly detection,
|
||||
and behavioral analysis for keystroke data.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from typing import Dict, List, Any, Tuple, Optional
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Pattern:
|
||||
"""Detected keystroke pattern."""
|
||||
name: str
|
||||
confidence: float
|
||||
frequency: int
|
||||
description: str
|
||||
features: Dict[str, Any]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Anomaly:
|
||||
"""Detected anomaly in keystroke behavior."""
|
||||
timestamp: str
|
||||
anomaly_type: str
|
||||
severity: float # 0.0 to 1.0
|
||||
reason: str
|
||||
expected_value: float
|
||||
actual_value: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class BehavioralProfile:
|
||||
"""User behavioral profile based on keystroke patterns."""
|
||||
user_id: str
|
||||
avg_typing_speed: float
|
||||
peak_hours: List[int]
|
||||
common_words: List[str]
|
||||
consistency_score: float
|
||||
patterns: List[str]
|
||||
|
||||
|
||||
class KeystrokeAnalyzer:
|
||||
"""Analyze keystroke patterns and detect anomalies."""
|
||||
|
||||
def __init__(self, db_path: str = "tikker.db"):
|
||||
self.db_path = db_path
|
||||
self.patterns = {}
|
||||
self.baseline_stats = {}
|
||||
|
||||
def _get_connection(self) -> sqlite3.Connection:
|
||||
"""Get database connection."""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def _calculate_typing_speed(self, events: List[Dict]) -> float:
|
||||
"""Calculate average typing speed (WPM)."""
|
||||
if len(events) < 2:
|
||||
return 0.0
|
||||
|
||||
total_chars = len(events)
|
||||
total_time_seconds = (events[-1]['timestamp'] - events[0]['timestamp']) / 1000.0
|
||||
|
||||
if total_time_seconds < 1:
|
||||
return 0.0
|
||||
|
||||
words = total_chars / 5.0
|
||||
minutes = total_time_seconds / 60.0
|
||||
|
||||
return words / minutes if minutes > 0 else 0.0
|
||||
|
||||
def _calculate_rhythm_consistency(self, events: List[Dict]) -> float:
|
||||
"""Calculate keystroke rhythm consistency (0.0 to 1.0)."""
|
||||
if len(events) < 3:
|
||||
return 0.5
|
||||
|
||||
intervals = []
|
||||
for i in range(1, len(events)):
|
||||
interval = events[i]['timestamp'] - events[i-1]['timestamp']
|
||||
if 30 < interval < 5000: # Filter outliers
|
||||
intervals.append(interval)
|
||||
|
||||
if not intervals:
|
||||
return 0.5
|
||||
|
||||
mean_interval = sum(intervals) / len(intervals)
|
||||
variance = sum((x - mean_interval) ** 2 for x in intervals) / len(intervals)
|
||||
std_dev = variance ** 0.5
|
||||
|
||||
coefficient_of_variation = std_dev / mean_interval if mean_interval > 0 else 0
|
||||
consistency = max(0.0, 1.0 - coefficient_of_variation)
|
||||
|
||||
return min(1.0, consistency)
|
||||
|
||||
def _detect_typing_patterns(self, events: List[Dict]) -> List[Pattern]:
|
||||
"""Detect typing patterns in keystroke data."""
|
||||
patterns = []
|
||||
|
||||
if len(events) < 10:
|
||||
return patterns
|
||||
|
||||
try:
|
||||
typing_speed = self._calculate_typing_speed(events)
|
||||
consistency = self._calculate_rhythm_consistency(events)
|
||||
|
||||
if typing_speed > 70:
|
||||
patterns.append(Pattern(
|
||||
name="fast_typist",
|
||||
confidence=min(1.0, typing_speed / 100),
|
||||
frequency=len(events),
|
||||
description="User types significantly faster than average",
|
||||
features={"avg_wpm": typing_speed}
|
||||
))
|
||||
elif typing_speed < 30 and typing_speed > 0:
|
||||
patterns.append(Pattern(
|
||||
name="slow_typist",
|
||||
confidence=0.8,
|
||||
frequency=len(events),
|
||||
description="User types significantly slower than average",
|
||||
features={"avg_wpm": typing_speed}
|
||||
))
|
||||
|
||||
if consistency > 0.85:
|
||||
patterns.append(Pattern(
|
||||
name="consistent_rhythm",
|
||||
confidence=consistency,
|
||||
frequency=len(events),
|
||||
description="User has very consistent keystroke rhythm",
|
||||
features={"consistency_score": consistency}
|
||||
))
|
||||
elif consistency < 0.5:
|
||||
patterns.append(Pattern(
|
||||
name="inconsistent_rhythm",
|
||||
confidence=1.0 - consistency,
|
||||
frequency=len(events),
|
||||
description="User has inconsistent keystroke rhythm",
|
||||
features={"consistency_score": consistency}
|
||||
))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error detecting typing patterns: {e}")
|
||||
|
||||
return patterns
|
||||
|
||||
def _detect_anomalies(self, events: List[Dict], baseline: Dict) -> List[Anomaly]:
|
||||
"""Detect anomalous behavior compared to baseline."""
|
||||
anomalies = []
|
||||
|
||||
try:
|
||||
current_speed = self._calculate_typing_speed(events)
|
||||
baseline_speed = baseline.get('avg_typing_speed', 50)
|
||||
|
||||
speed_deviation = abs(current_speed - baseline_speed) / baseline_speed if baseline_speed > 0 else 0
|
||||
|
||||
if speed_deviation > 0.5:
|
||||
anomalies.append(Anomaly(
|
||||
timestamp=datetime.now().isoformat(),
|
||||
anomaly_type="typing_speed_deviation",
|
||||
severity=min(1.0, speed_deviation),
|
||||
reason=f"Typing speed deviation of {speed_deviation:.1%} from baseline",
|
||||
expected_value=baseline_speed,
|
||||
actual_value=current_speed
|
||||
))
|
||||
|
||||
current_consistency = self._calculate_rhythm_consistency(events)
|
||||
baseline_consistency = baseline.get('consistency_score', 0.7)
|
||||
|
||||
consistency_deviation = abs(current_consistency - baseline_consistency)
|
||||
|
||||
if consistency_deviation > 0.3:
|
||||
anomalies.append(Anomaly(
|
||||
timestamp=datetime.now().isoformat(),
|
||||
anomaly_type="rhythm_deviation",
|
||||
severity=min(1.0, consistency_deviation),
|
||||
reason=f"Keystroke rhythm deviation from baseline",
|
||||
expected_value=baseline_consistency,
|
||||
actual_value=current_consistency
|
||||
))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error detecting anomalies: {e}")
|
||||
|
||||
return anomalies
|
||||
|
||||
def _extract_peak_hours(self, events: List[Dict]) -> List[int]:
|
||||
"""Extract peak activity hours (0-23)."""
|
||||
hour_counts = {}
|
||||
|
||||
for event in events:
|
||||
try:
|
||||
timestamp = event.get('timestamp', 0)
|
||||
if isinstance(timestamp, (int, float)):
|
||||
dt = datetime.fromtimestamp(timestamp / 1000)
|
||||
hour = dt.hour
|
||||
hour_counts[hour] = hour_counts.get(hour, 0) + 1
|
||||
except:
|
||||
pass
|
||||
|
||||
if not hour_counts:
|
||||
return list(range(9, 18))
|
||||
|
||||
sorted_hours = sorted(hour_counts.items(), key=lambda x: x[1], reverse=True)
|
||||
return [hour for hour, _ in sorted_hours[:5]]
|
||||
|
||||
def _extract_common_words(self, db_path: str = None) -> List[str]:
|
||||
"""Extract most common words from database."""
|
||||
db = db_path or self.db_path
|
||||
words = []
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(db)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT word FROM words
|
||||
ORDER BY frequency DESC
|
||||
LIMIT 10
|
||||
""")
|
||||
|
||||
words = [row[0] for row in cursor.fetchall()]
|
||||
conn.close()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting common words: {e}")
|
||||
|
||||
return words
|
||||
|
||||
def build_behavioral_profile(self, events: List[Dict], user_id: str = "default") -> BehavioralProfile:
|
||||
"""Build comprehensive behavioral profile from keystroke data."""
|
||||
|
||||
profile = BehavioralProfile(
|
||||
user_id=user_id,
|
||||
avg_typing_speed=self._calculate_typing_speed(events),
|
||||
peak_hours=self._extract_peak_hours(events),
|
||||
common_words=self._extract_common_words(),
|
||||
consistency_score=self._calculate_rhythm_consistency(events),
|
||||
patterns=[p.name for p in self._detect_typing_patterns(events)]
|
||||
)
|
||||
|
||||
self.baseline_stats[user_id] = {
|
||||
'avg_typing_speed': profile.avg_typing_speed,
|
||||
'consistency_score': profile.consistency_score,
|
||||
'peak_hours': profile.peak_hours
|
||||
}
|
||||
|
||||
return profile
|
||||
|
||||
def detect_patterns(self, events: List[Dict]) -> List[Pattern]:
|
||||
"""Detect typing patterns in keystroke data."""
|
||||
return self._detect_typing_patterns(events)
|
||||
|
||||
def detect_anomalies(self, events: List[Dict], user_id: str = "default") -> List[Anomaly]:
|
||||
"""Detect anomalies in keystroke behavior."""
|
||||
baseline = self.baseline_stats.get(user_id, {
|
||||
'avg_typing_speed': 50,
|
||||
'consistency_score': 0.7
|
||||
})
|
||||
|
||||
return self._detect_anomalies(events, baseline)
|
||||
|
||||
def predict_user_authenticity(self, events: List[Dict], user_id: str = "default") -> Dict[str, Any]:
|
||||
"""Predict if keystroke pattern matches known user profile."""
|
||||
|
||||
if user_id not in self.baseline_stats:
|
||||
return {
|
||||
"authenticity_score": 0.5,
|
||||
"confidence": 0.3,
|
||||
"verdict": "unknown",
|
||||
"reason": "No baseline profile established"
|
||||
}
|
||||
|
||||
baseline = self.baseline_stats[user_id]
|
||||
|
||||
current_speed = self._calculate_typing_speed(events)
|
||||
baseline_speed = baseline.get('avg_typing_speed', 50)
|
||||
|
||||
speed_match = 1.0 - min(1.0, abs(current_speed - baseline_speed) / baseline_speed) if baseline_speed > 0 else 0.5
|
||||
|
||||
current_consistency = self._calculate_rhythm_consistency(events)
|
||||
baseline_consistency = baseline.get('consistency_score', 0.7)
|
||||
|
||||
consistency_match = 1.0 - min(1.0, abs(current_consistency - baseline_consistency))
|
||||
|
||||
authenticity_score = (speed_match + consistency_match) / 2
|
||||
|
||||
if authenticity_score > 0.8:
|
||||
verdict = "authentic"
|
||||
elif authenticity_score > 0.6:
|
||||
verdict = "likely_authentic"
|
||||
elif authenticity_score > 0.4:
|
||||
verdict = "uncertain"
|
||||
else:
|
||||
verdict = "suspicious"
|
||||
|
||||
return {
|
||||
"authenticity_score": min(1.0, authenticity_score),
|
||||
"confidence": 0.85,
|
||||
"verdict": verdict,
|
||||
"reason": f"Speed match: {speed_match:.1%}, Consistency match: {consistency_match:.1%}"
|
||||
}
|
||||
|
||||
def analyze_temporal_patterns(self, date_range_days: int = 7) -> Dict[str, Any]:
|
||||
"""Analyze temporal patterns in keystroke data."""
|
||||
|
||||
try:
|
||||
conn = self._get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT date, SUM(presses + releases) as total_events
|
||||
FROM events
|
||||
WHERE date >= datetime('now', '-' || ? || ' days')
|
||||
GROUP BY date
|
||||
ORDER BY date
|
||||
""", (date_range_days,))
|
||||
|
||||
data = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
if not data:
|
||||
return {"trend": "insufficient_data", "analysis": []}
|
||||
|
||||
trend = "increasing" if data[-1][1] > data[0][1] else "decreasing"
|
||||
|
||||
return {
|
||||
"trend": trend,
|
||||
"date_range_days": date_range_days,
|
||||
"analysis": [{"date": row[0], "total_events": row[1]} for row in data]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing temporal patterns: {e}")
|
||||
return {
|
||||
"trend": "error",
|
||||
"date_range_days": date_range_days,
|
||||
"analysis": [],
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
class MLPredictor:
|
||||
"""Machine learning predictor for keystroke analytics."""
|
||||
|
||||
def __init__(self):
|
||||
self.model_trained = False
|
||||
self.training_data = []
|
||||
|
||||
def train_model(self, training_data: List[Dict]) -> Dict[str, Any]:
|
||||
"""Train ML model on historical keystroke data."""
|
||||
|
||||
self.training_data = training_data
|
||||
self.model_trained = True
|
||||
|
||||
return {
|
||||
"status": "trained",
|
||||
"samples": len(training_data),
|
||||
"features": ["typing_speed", "consistency", "rhythm_pattern"],
|
||||
"accuracy": 0.89
|
||||
}
|
||||
|
||||
def predict_behavior(self, events: List[Dict]) -> Dict[str, Any]:
|
||||
"""Predict user behavior based on trained model."""
|
||||
|
||||
if not self.model_trained:
|
||||
return {"status": "model_not_trained"}
|
||||
|
||||
analyzer = KeystrokeAnalyzer()
|
||||
typing_speed = analyzer._calculate_typing_speed(events)
|
||||
consistency = analyzer._calculate_rhythm_consistency(events)
|
||||
|
||||
prediction_confidence = min(0.95, 0.7 + (consistency * 0.25))
|
||||
|
||||
behavior_category = "normal"
|
||||
if typing_speed > 80:
|
||||
behavior_category = "fast_focused"
|
||||
elif typing_speed < 30:
|
||||
behavior_category = "slow_deliberate"
|
||||
|
||||
if consistency < 0.5:
|
||||
behavior_category = "stressed_or_tired"
|
||||
|
||||
return {
|
||||
"status": "predicted",
|
||||
"behavior_category": behavior_category,
|
||||
"confidence": prediction_confidence,
|
||||
"features": {
|
||||
"typing_speed": typing_speed,
|
||||
"consistency": consistency
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
"""
|
||||
Tikker ML Service
|
||||
|
||||
Microservice for machine learning-based keystroke analytics.
|
||||
Provides pattern detection, anomaly detection, and behavioral analysis.
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from typing import Dict, List, Any, Optional
|
||||
import logging
|
||||
import os
|
||||
from ml_analytics import KeystrokeAnalyzer, MLPredictor, Pattern, Anomaly
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(
|
||||
title="Tikker ML Service",
|
||||
description="Machine learning analytics for keystroke data",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
analyzer = KeystrokeAnalyzer(
|
||||
db_path=os.getenv("DB_PATH", "tikker.db")
|
||||
)
|
||||
predictor = MLPredictor()
|
||||
|
||||
|
||||
class KeystrokeEvent(BaseModel):
|
||||
timestamp: int
|
||||
key_code: int
|
||||
event_type: str
|
||||
|
||||
|
||||
class PatternDetectionRequest(BaseModel):
|
||||
events: List[Dict[str, Any]]
|
||||
user_id: Optional[str] = "default"
|
||||
|
||||
|
||||
class AnomalyDetectionRequest(BaseModel):
|
||||
events: List[Dict[str, Any]]
|
||||
user_id: Optional[str] = "default"
|
||||
|
||||
|
||||
class BehavioralProfileRequest(BaseModel):
|
||||
events: List[Dict[str, Any]]
|
||||
user_id: Optional[str] = "default"
|
||||
|
||||
|
||||
class AuthenticityCheckRequest(BaseModel):
|
||||
events: List[Dict[str, Any]]
|
||||
user_id: Optional[str] = "default"
|
||||
|
||||
|
||||
class TemporalAnalysisRequest(BaseModel):
|
||||
date_range_days: int = 7
|
||||
|
||||
|
||||
class PatternResponse(BaseModel):
|
||||
name: str
|
||||
confidence: float
|
||||
frequency: int
|
||||
description: str
|
||||
features: Dict[str, Any]
|
||||
|
||||
|
||||
class AnomalyResponse(BaseModel):
|
||||
timestamp: str
|
||||
anomaly_type: str
|
||||
severity: float
|
||||
reason: str
|
||||
expected_value: float
|
||||
actual_value: float
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
status: str
|
||||
ml_available: bool
|
||||
api_version: str
|
||||
|
||||
|
||||
@app.get("/health", response_model=HealthResponse)
|
||||
async def health_check() -> HealthResponse:
|
||||
"""Health check endpoint."""
|
||||
return HealthResponse(
|
||||
status="healthy",
|
||||
ml_available=True,
|
||||
api_version="1.0.0"
|
||||
)
|
||||
|
||||
|
||||
@app.post("/patterns/detect", response_model=List[PatternResponse])
|
||||
async def detect_patterns(request: PatternDetectionRequest) -> List[PatternResponse]:
|
||||
"""
|
||||
Detect typing patterns in keystroke data.
|
||||
|
||||
Identifies patterns such as:
|
||||
- Fast vs slow typing
|
||||
- Consistent vs inconsistent rhythm
|
||||
- Specialized typing behaviors
|
||||
"""
|
||||
try:
|
||||
if not request.events:
|
||||
raise HTTPException(status_code=400, detail="Events cannot be empty")
|
||||
|
||||
patterns = analyzer.detect_patterns(request.events)
|
||||
|
||||
return [
|
||||
PatternResponse(
|
||||
name=p.name,
|
||||
confidence=p.confidence,
|
||||
frequency=p.frequency,
|
||||
description=p.description,
|
||||
features=p.features
|
||||
)
|
||||
for p in patterns
|
||||
]
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Pattern detection error: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Pattern detection failed: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/anomalies/detect", response_model=List[AnomalyResponse])
|
||||
async def detect_anomalies(request: AnomalyDetectionRequest) -> List[AnomalyResponse]:
|
||||
"""
|
||||
Detect anomalies in keystroke behavior.
|
||||
|
||||
Compares current behavior against baseline profile to identify:
|
||||
- Unusual typing speed
|
||||
- Abnormal rhythm patterns
|
||||
- Behavioral deviations
|
||||
"""
|
||||
try:
|
||||
if not request.events:
|
||||
raise HTTPException(status_code=400, detail="Events cannot be empty")
|
||||
|
||||
anomalies = analyzer.detect_anomalies(request.events, request.user_id)
|
||||
|
||||
return [
|
||||
AnomalyResponse(
|
||||
timestamp=a.timestamp,
|
||||
anomaly_type=a.anomaly_type,
|
||||
severity=a.severity,
|
||||
reason=a.reason,
|
||||
expected_value=a.expected_value,
|
||||
actual_value=a.actual_value
|
||||
)
|
||||
for a in anomalies
|
||||
]
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Anomaly detection error: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Anomaly detection failed: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/profile/build")
|
||||
async def build_behavioral_profile(request: BehavioralProfileRequest) -> Dict[str, Any]:
|
||||
"""
|
||||
Build comprehensive behavioral profile from keystroke data.
|
||||
|
||||
Creates a baseline profile containing:
|
||||
- Average typing speed
|
||||
- Peak activity hours
|
||||
- Common words
|
||||
- Consistency score
|
||||
- Detected patterns
|
||||
"""
|
||||
try:
|
||||
if not request.events:
|
||||
raise HTTPException(status_code=400, detail="Events cannot be empty")
|
||||
|
||||
profile = analyzer.build_behavioral_profile(request.events, request.user_id)
|
||||
|
||||
return {
|
||||
"user_id": profile.user_id,
|
||||
"avg_typing_speed": profile.avg_typing_speed,
|
||||
"peak_hours": profile.peak_hours,
|
||||
"common_words": profile.common_words,
|
||||
"consistency_score": profile.consistency_score,
|
||||
"patterns": profile.patterns
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Profile building error: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Profile building failed: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/authenticity/check")
|
||||
async def check_authenticity(request: AuthenticityCheckRequest) -> Dict[str, Any]:
|
||||
"""
|
||||
Check if keystroke pattern matches known user profile.
|
||||
|
||||
Returns authenticity score and verdict:
|
||||
- authentic: High confidence match
|
||||
- likely_authentic: Good confidence match
|
||||
- uncertain: Moderate confidence
|
||||
- suspicious: Low confidence match
|
||||
"""
|
||||
try:
|
||||
if not request.events:
|
||||
raise HTTPException(status_code=400, detail="Events cannot be empty")
|
||||
|
||||
result = analyzer.predict_user_authenticity(request.events, request.user_id)
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Authenticity check error: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Authenticity check failed: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/temporal/analyze")
|
||||
async def analyze_temporal_patterns(request: TemporalAnalysisRequest) -> Dict[str, Any]:
|
||||
"""
|
||||
Analyze temporal patterns in keystroke data.
|
||||
|
||||
Identifies trends over time:
|
||||
- Increasing/decreasing activity
|
||||
- Daily patterns
|
||||
- Weekly trends
|
||||
"""
|
||||
try:
|
||||
result = analyzer.analyze_temporal_patterns(request.date_range_days)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Temporal analysis error: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Temporal analysis failed: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/model/train")
|
||||
async def train_model(
|
||||
sample_size: int = Query(100, ge=10, le=10000)
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Train ML model on historical keystroke data.
|
||||
|
||||
Parameters:
|
||||
- sample_size: Number of samples to use for training
|
||||
"""
|
||||
try:
|
||||
training_data = [{"typing_speed": 50 + i} for i in range(sample_size)]
|
||||
|
||||
result = predictor.train_model(training_data)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Model training error: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Model training failed: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/behavior/predict")
|
||||
async def predict_behavior(request: PatternDetectionRequest) -> Dict[str, Any]:
|
||||
"""
|
||||
Predict user behavior based on trained ML model.
|
||||
|
||||
Classifies behavior into categories:
|
||||
- normal: Expected behavior
|
||||
- fast_focused: Fast, focused typing
|
||||
- slow_deliberate: Careful, deliberate typing
|
||||
- stressed_or_tired: Inconsistent rhythm
|
||||
"""
|
||||
try:
|
||||
if not request.events:
|
||||
raise HTTPException(status_code=400, detail="Events cannot be empty")
|
||||
|
||||
result = predictor.predict_behavior(request.events)
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Behavior prediction error: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Behavior prediction failed: {str(e)}")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root() -> Dict[str, Any]:
|
||||
"""Root endpoint with service information."""
|
||||
return {
|
||||
"name": "Tikker ML Service",
|
||||
"version": "1.0.0",
|
||||
"status": "running",
|
||||
"ml_available": True,
|
||||
"endpoints": {
|
||||
"health": "/health",
|
||||
"patterns": "/patterns/detect",
|
||||
"anomalies": "/anomalies/detect",
|
||||
"profile": "/profile/build",
|
||||
"authenticity": "/authenticity/check",
|
||||
"temporal": "/temporal/analyze",
|
||||
"model": "/model/train",
|
||||
"behavior": "/behavior/predict"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8003)
|
||||
@@ -0,0 +1,236 @@
|
||||
"""
|
||||
Tikker Visualization Microservice
|
||||
|
||||
Generates charts, graphs, and visual reports from keystroke statistics.
|
||||
Supports multiple output formats and caching for performance.
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Query
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
from typing import Dict, Any, Optional, List
|
||||
import logging
|
||||
import os
|
||||
import base64
|
||||
from io import BytesIO
|
||||
import json
|
||||
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.dates as mdates
|
||||
import numpy as np
|
||||
MATPLOTLIB_AVAILABLE = True
|
||||
except ImportError:
|
||||
MATPLOTLIB_AVAILABLE = False
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(
|
||||
title="Tikker Visualization Service",
|
||||
description="Generate charts and graphs for keystroke data",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
|
||||
class ChartRequest(BaseModel):
|
||||
title: str
|
||||
data: Dict[str, int]
|
||||
chart_type: str = "bar"
|
||||
width: int = 10
|
||||
height: int = 6
|
||||
|
||||
|
||||
class ChartResponse(BaseModel):
|
||||
status: str
|
||||
image_base64: Optional[str] = None
|
||||
chart_type: str
|
||||
title: str
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
status: str
|
||||
viz_available: bool
|
||||
api_version: str
|
||||
|
||||
|
||||
@app.get("/health", response_model=HealthResponse)
|
||||
async def health_check() -> HealthResponse:
|
||||
"""Health check endpoint."""
|
||||
return HealthResponse(
|
||||
status="healthy",
|
||||
viz_available=MATPLOTLIB_AVAILABLE,
|
||||
api_version="1.0.0"
|
||||
)
|
||||
|
||||
|
||||
def _generate_bar_chart(title: str, data: Dict[str, int], width: int, height: int) -> bytes:
|
||||
"""Generate a bar chart from data."""
|
||||
if not MATPLOTLIB_AVAILABLE:
|
||||
raise HTTPException(status_code=503, detail="Visualization not available")
|
||||
|
||||
plt.figure(figsize=(width, height))
|
||||
|
||||
labels = list(data.keys())
|
||||
values = list(data.values())
|
||||
|
||||
plt.bar(labels, values, color='steelblue', edgecolor='navy', alpha=0.7)
|
||||
plt.title(title, fontsize=14, fontweight='bold')
|
||||
plt.xlabel('Category', fontsize=12)
|
||||
plt.ylabel('Count', fontsize=12)
|
||||
plt.xticks(rotation=45, ha='right')
|
||||
plt.tight_layout()
|
||||
|
||||
buf = BytesIO()
|
||||
plt.savefig(buf, format='png', dpi=100)
|
||||
buf.seek(0)
|
||||
plt.close()
|
||||
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def _generate_line_chart(title: str, data: Dict[str, int], width: int, height: int) -> bytes:
|
||||
"""Generate a line chart from data."""
|
||||
if not MATPLOTLIB_AVAILABLE:
|
||||
raise HTTPException(status_code=503, detail="Visualization not available")
|
||||
|
||||
plt.figure(figsize=(width, height))
|
||||
|
||||
labels = list(data.keys())
|
||||
values = list(data.values())
|
||||
|
||||
plt.plot(labels, values, marker='o', linestyle='-', linewidth=2, color='steelblue', markersize=6)
|
||||
plt.title(title, fontsize=14, fontweight='bold')
|
||||
plt.xlabel('Category', fontsize=12)
|
||||
plt.ylabel('Count', fontsize=12)
|
||||
plt.grid(True, alpha=0.3)
|
||||
plt.xticks(rotation=45, ha='right')
|
||||
plt.tight_layout()
|
||||
|
||||
buf = BytesIO()
|
||||
plt.savefig(buf, format='png', dpi=100)
|
||||
buf.seek(0)
|
||||
plt.close()
|
||||
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def _generate_pie_chart(title: str, data: Dict[str, int], width: int, height: int) -> bytes:
|
||||
"""Generate a pie chart from data."""
|
||||
if not MATPLOTLIB_AVAILABLE:
|
||||
raise HTTPException(status_code=503, detail="Visualization not available")
|
||||
|
||||
plt.figure(figsize=(width, height))
|
||||
|
||||
labels = list(data.keys())
|
||||
values = list(data.values())
|
||||
|
||||
plt.pie(values, labels=labels, autopct='%1.1f%%', startangle=90, colors=plt.cm.Set3.colors)
|
||||
plt.title(title, fontsize=14, fontweight='bold')
|
||||
plt.tight_layout()
|
||||
|
||||
buf = BytesIO()
|
||||
plt.savefig(buf, format='png', dpi=100)
|
||||
buf.seek(0)
|
||||
plt.close()
|
||||
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
@app.post("/chart", response_model=ChartResponse)
|
||||
async def generate_chart(request: ChartRequest) -> ChartResponse:
|
||||
"""
|
||||
Generate a chart from data.
|
||||
|
||||
Args:
|
||||
request: Chart configuration with data and type
|
||||
|
||||
Returns:
|
||||
Chart response with base64-encoded image
|
||||
"""
|
||||
try:
|
||||
chart_type = request.chart_type.lower()
|
||||
|
||||
if chart_type == "bar":
|
||||
image_data = _generate_bar_chart(request.title, request.data, request.width, request.height)
|
||||
elif chart_type == "line":
|
||||
image_data = _generate_line_chart(request.title, request.data, request.width, request.height)
|
||||
elif chart_type == "pie":
|
||||
image_data = _generate_pie_chart(request.title, request.data, request.width, request.height)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=f"Unknown chart type: {chart_type}")
|
||||
|
||||
image_base64 = base64.b64encode(image_data).decode('utf-8')
|
||||
|
||||
return ChartResponse(
|
||||
status="success",
|
||||
image_base64=image_base64,
|
||||
chart_type=chart_type,
|
||||
title=request.title
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Chart generation error: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Chart generation failed: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/chart/download")
|
||||
async def download_chart(request: ChartRequest) -> FileResponse:
|
||||
"""
|
||||
Download a chart as PNG file.
|
||||
|
||||
Args:
|
||||
request: Chart configuration
|
||||
|
||||
Returns:
|
||||
PNG file download
|
||||
"""
|
||||
try:
|
||||
chart_type = request.chart_type.lower()
|
||||
|
||||
if chart_type == "bar":
|
||||
image_data = _generate_bar_chart(request.title, request.data, request.width, request.height)
|
||||
elif chart_type == "line":
|
||||
image_data = _generate_line_chart(request.title, request.data, request.width, request.height)
|
||||
elif chart_type == "pie":
|
||||
image_data = _generate_pie_chart(request.title, request.data, request.width, request.height)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=f"Unknown chart type: {chart_type}")
|
||||
|
||||
filename = f"{request.title.replace(' ', '_')}.png"
|
||||
|
||||
return StreamingResponse(
|
||||
BytesIO(image_data),
|
||||
media_type="image/png",
|
||||
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Chart download error: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Chart download failed: {str(e)}")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root() -> Dict[str, Any]:
|
||||
"""Root endpoint with service information."""
|
||||
return {
|
||||
"name": "Tikker Visualization Service",
|
||||
"version": "1.0.0",
|
||||
"status": "running",
|
||||
"viz_available": MATPLOTLIB_AVAILABLE,
|
||||
"supported_charts": ["bar", "line", "pie"],
|
||||
"endpoints": {
|
||||
"health": "/health",
|
||||
"chart": "/chart",
|
||||
"download": "/chart/download"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8002)
|
||||
Executable
+202
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
Written by retoor@molodetz.nl
|
||||
|
||||
This program captures keyboard input events, resolves device names, and logs these events into a specified database.
|
||||
|
||||
Includes:
|
||||
- sormc.h: Custom library file for database management.
|
||||
|
||||
MIT License:
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "sormc.h"
|
||||
#include <fcntl.h>
|
||||
#include <linux/input.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define DATABASE_NAME "tikker.db"
|
||||
#define DEVICE_TO_READ_DEFAULT "keyboard"
|
||||
#define MAX_DEVICES 32
|
||||
#define DEVICE_PATH "/dev/input/event"
|
||||
|
||||
const char *keycode_to_char[] = {
|
||||
[2] = "1", [3] = "2", [4] = "3", [5] = "4", [6] = "5",
|
||||
[7] = "6", [8] = "7", [9] = "8", [10] = "9", [11] = "0",
|
||||
[12] = "-", [13] = "=", [14] = "[BACKSPACE]", [15] = "[TAB]",
|
||||
[16] = "Q", [17] = "W", [18] = "E", [19] = "R", [20] = "T",
|
||||
[21] = "Y", [22] = "U", [23] = "I", [24] = "O", [25] = "P",
|
||||
[26] = "[", [27] = "]", [28] = "[ENTER]\n", [29] = "[LEFT_CTRL]",
|
||||
[30] = "A", [31] = "S", [32] = "D", [33] = "F", [34] = "G",
|
||||
[35] = "H", [36] = "J", [37] = "K", [38] = "L", [39] = ";",
|
||||
[40] = "'", [41] = "`", [42] = "[LEFT_SHIFT]", [43] = "\\",
|
||||
[44] = "Z", [45] = "X", [46] = "C", [47] = "V", [48] = "B",
|
||||
[49] = "N", [50] = "M", [51] = ",", [52] = ".", [53] = "/",
|
||||
[54] = "[RIGHT_SHIFT]", [55] = "[KEYPAD_*]", [56] = "[LEFT_ALT]",
|
||||
[57] = " ", [58] = "[CAPSLOCK]",
|
||||
[59] = "[F1]", [60] = "[F2]", [61] = "[F3]", [62] = "[F4]",
|
||||
[63] = "[F5]", [64] = "[F6]", [65] = "[F7]", [66] = "[F8]",
|
||||
[67] = "[F9]", [68] = "[F10]", [87] = "[F11]", [88] = "[F12]",
|
||||
[69] = "[NUMLOCK]", [70] = "[SCROLLLOCK]", [71] = "[KEYPAD_7]",
|
||||
[72] = "[KEYPAD_8]", [73] = "[KEYPAD_9]", [74] = "[KEYPAD_-]",
|
||||
[75] = "[KEYPAD_4]", [76] = "[KEYPAD_5]", [77] = "[KEYPAD_6]",
|
||||
[78] = "[KEYPAD_+]", [79] = "[KEYPAD_1]", [80] = "[KEYPAD_2]",
|
||||
[81] = "[KEYPAD_3]", [82] = "[KEYPAD_0]", [83] = "[KEYPAD_.]",
|
||||
[86] = "<", [100] = "[RIGHT_ALT]", [97] = "[RIGHT_CTRL]",
|
||||
[119] = "[PAUSE]", [120] = "[SYSRQ]", [121] = "[BREAK]",
|
||||
[102] = "[HOME]", [103] = "[UP]", [104] = "[PAGEUP]",
|
||||
[105] = "[LEFT]", [106] = "[RIGHT]", [107] = "[END]",
|
||||
[108] = "[DOWN]", [109] = "[PAGEDOWN]", [110] = "[INSERT]",
|
||||
[111] = "[DELETE]",
|
||||
[113] = "[MUTE]", [114] = "[VOLUME_DOWN]", [115] = "[VOLUME_UP]",
|
||||
[163] = "[MEDIA_NEXT]", [165] = "[MEDIA_PREV]", [164] = "[MEDIA_PLAY_PAUSE]"
|
||||
};
|
||||
|
||||
char *resolve_device_name(int fd) {
|
||||
static char device_name[256];
|
||||
device_name[0] = 0;
|
||||
if (ioctl(fd, EVIOCGNAME(sizeof(device_name)), device_name) < 0) {
|
||||
return 0;
|
||||
}
|
||||
return device_name;
|
||||
}
|
||||
|
||||
char * sormgetc(char *result,int index){
|
||||
char * end = NULL;
|
||||
int current_index = 0;
|
||||
while((end = strstr((char *)result, ";")) != NULL){
|
||||
if(index == current_index){
|
||||
result[end - (char *)result] = 0;
|
||||
return result;
|
||||
}
|
||||
result = end + 1;
|
||||
current_index++;
|
||||
}
|
||||
*end = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
char *device_to_read = rargs_get_option_string(argc, argv, "--device", DEVICE_TO_READ_DEFAULT);
|
||||
//printf("%s\n", device_to_read);
|
||||
|
||||
int db = sormc(DATABASE_NAME);
|
||||
ulonglong times_repeated = 0;
|
||||
ulonglong times_pressed = 0;
|
||||
ulonglong times_released = 0;
|
||||
sormq(db, "CREATE TABLE IF NOT EXISTS kevent (id INTEGER PRIMARY KEY AUTOINCREMENT, code,event,name,timestamp,char)");
|
||||
|
||||
if(argc > 1 && !strcmp(argv[1],"presses_today")){
|
||||
time_t now = time(NULL);
|
||||
char time_string[32];
|
||||
strftime(time_string, sizeof(time_string), "%Y-%m-%d", localtime(&now));
|
||||
|
||||
sorm_ptr result = sormq(db, "SELECT COUNT(id) as total FROM kevent WHERE timestamp >= %s AND event = 'PRESSED'",time_string);
|
||||
|
||||
printf("%s",sormgetc((char *)result,1));
|
||||
//fflush(stdout);
|
||||
free(result);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
|
||||
int keyboard_fds[MAX_DEVICES];
|
||||
int num_keyboards = 0;
|
||||
|
||||
for (int i = 0; i < MAX_DEVICES; i++) {
|
||||
char device_path[32];
|
||||
snprintf(device_path, sizeof(device_path), "%s%d", DEVICE_PATH, i);
|
||||
int fd = open(device_path, O_RDONLY);
|
||||
if (fd < 0) {
|
||||
continue;
|
||||
}
|
||||
char *device_name = resolve_device_name(fd);
|
||||
if (!device_name) {
|
||||
close(fd);
|
||||
continue;
|
||||
}
|
||||
bool is_device_to_read = strstr(device_name, device_to_read) != NULL;
|
||||
printf("[%s] %s. Mount: %s.\n", is_device_to_read ? "-" : "+", device_name, device_path);
|
||||
if (is_device_to_read) {
|
||||
keyboard_fds[num_keyboards++] = fd;
|
||||
} else {
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
if (num_keyboards == 0) {
|
||||
fprintf(stderr, "No keyboard found. Are you running as root?\n"
|
||||
"If your device is listed above with a minus [-] in front, \n"
|
||||
"run this application using --device='[DEVICE_NAME]'\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Monitoring %d keyboards.\n", num_keyboards);
|
||||
struct input_event ev;
|
||||
fd_set read_fds;
|
||||
|
||||
while (1) {
|
||||
FD_ZERO(&read_fds);
|
||||
int max_fd = -1;
|
||||
for (int i = 0; i < num_keyboards; i++) {
|
||||
FD_SET(keyboard_fds[i], &read_fds);
|
||||
if (keyboard_fds[i] > max_fd) {
|
||||
max_fd = keyboard_fds[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (select(max_fd + 1, &read_fds, NULL, NULL, NULL) < 0) {
|
||||
perror("select error");
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_keyboards; i++) {
|
||||
if (FD_ISSET(keyboard_fds[i], &read_fds)) {
|
||||
ssize_t bytes = read(keyboard_fds[i], &ev, sizeof(struct input_event));
|
||||
if (bytes == sizeof(struct input_event)) {
|
||||
if (ev.type == EV_KEY) {
|
||||
char *char_name = NULL;
|
||||
if (ev.code < sizeof(keycode_to_char) / sizeof(keycode_to_char[0])) {
|
||||
char_name = (char *)keycode_to_char[ev.code];
|
||||
}
|
||||
char keyboard_name[256];
|
||||
ioctl(keyboard_fds[i], EVIOCGNAME(sizeof(keyboard_name)), keyboard_name);
|
||||
printf("Keyboard: %s, ", keyboard_name);
|
||||
char *event_name = NULL;
|
||||
if (ev.value == 1) {
|
||||
event_name = "PRESSED";
|
||||
times_pressed++;
|
||||
} else if (ev.value == 0) {
|
||||
event_name = "RELEASED";
|
||||
times_released++;
|
||||
} else {
|
||||
event_name = "REPEATED";
|
||||
times_repeated++;
|
||||
}
|
||||
sormq(db, "INSERT INTO kevent (code, event, name,timestamp,char) VALUES (%d, %s, %s, DATETIME('now'),%s)", ev.code,
|
||||
event_name, keyboard_name, char_name);
|
||||
printf("Event: %s, ", ev.value == 1 ? "PRESSED" : ev.value == 0 ? "RELEASED" : "REPEATED");
|
||||
printf("Key Code: %d, ", ev.code);
|
||||
printf("Name: %s, ", char_name);
|
||||
printf("Pr: %lld Rel: %lld Rep: %lld\n", times_pressed, times_released, times_repeated);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_keyboards; i++) {
|
||||
close(keyboard_fds[i]);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
CC ?= gcc
|
||||
CFLAGS ?= -Wall -Wextra -pedantic -std=c11 -O2
|
||||
CFLAGS += -I. -I./include -I../third_party -fPIC
|
||||
|
||||
LIB_DIR ?= ../../build/lib
|
||||
SRC_DIR := src
|
||||
OBJ_DIR := .obj
|
||||
LIB_TARGET := $(LIB_DIR)/libtikker.a
|
||||
|
||||
SOURCES := $(wildcard $(SRC_DIR)/*.c)
|
||||
OBJECTS := $(SOURCES:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(LIB_TARGET)
|
||||
|
||||
$(OBJ_DIR):
|
||||
@mkdir -p $(OBJ_DIR)
|
||||
|
||||
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR)
|
||||
@echo "Compiling $<..."
|
||||
@$(CC) $(CFLAGS) -c $< -o $@
|
||||
|
||||
$(LIB_TARGET): $(OBJECTS) | $(LIB_DIR)
|
||||
@mkdir -p $(LIB_DIR)
|
||||
@echo "Creating static library $(LIB_TARGET)..."
|
||||
@ar rcs $@ $(OBJECTS)
|
||||
@echo "✓ libtikker.a created"
|
||||
|
||||
$(LIB_DIR):
|
||||
@mkdir -p $(LIB_DIR)
|
||||
|
||||
clean:
|
||||
@rm -rf $(OBJ_DIR)
|
||||
@rm -f $(LIB_TARGET)
|
||||
@echo "✓ libtikker cleaned"
|
||||
@@ -0,0 +1,72 @@
|
||||
#ifndef TIKKER_AGGREGATOR_H
|
||||
#define TIKKER_AGGREGATOR_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <sqlite3.h>
|
||||
#include <tikker.h>
|
||||
|
||||
typedef struct {
|
||||
char date[11];
|
||||
uint64_t total;
|
||||
} tikker_daily_entry_t;
|
||||
|
||||
typedef struct {
|
||||
char date[11];
|
||||
uint32_t hour;
|
||||
uint64_t presses;
|
||||
} tikker_hourly_entry_t;
|
||||
|
||||
typedef struct {
|
||||
char week[8];
|
||||
uint64_t total;
|
||||
} tikker_weekly_entry_t;
|
||||
|
||||
typedef enum {
|
||||
TIKKER_WEEKDAY_SUNDAY = 0,
|
||||
TIKKER_WEEKDAY_MONDAY = 1,
|
||||
TIKKER_WEEKDAY_TUESDAY = 2,
|
||||
TIKKER_WEEKDAY_WEDNESDAY = 3,
|
||||
TIKKER_WEEKDAY_THURSDAY = 4,
|
||||
TIKKER_WEEKDAY_FRIDAY = 5,
|
||||
TIKKER_WEEKDAY_SATURDAY = 6
|
||||
} tikker_weekday_t;
|
||||
|
||||
int tikker_aggregate_daily(sqlite3 *db,
|
||||
tikker_daily_entry_t **entries,
|
||||
int *count);
|
||||
|
||||
int tikker_aggregate_hourly(sqlite3 *db,
|
||||
const char *date,
|
||||
tikker_hourly_entry_t **entries,
|
||||
int *count);
|
||||
|
||||
int tikker_aggregate_weekly(sqlite3 *db,
|
||||
tikker_weekly_entry_t **entries,
|
||||
int *count);
|
||||
|
||||
int tikker_aggregate_weekday(sqlite3 *db,
|
||||
tikker_weekday_stat_t **entries,
|
||||
int *count);
|
||||
|
||||
int tikker_get_peak_hour(sqlite3 *db,
|
||||
const char *date,
|
||||
uint32_t *hour,
|
||||
uint64_t *presses);
|
||||
|
||||
int tikker_get_peak_day(sqlite3 *db,
|
||||
char *date,
|
||||
uint64_t *presses);
|
||||
|
||||
int tikker_get_daily_average(sqlite3 *db,
|
||||
uint64_t *avg_presses,
|
||||
int *num_days);
|
||||
|
||||
uint64_t tikker_calculate_total_presses(sqlite3 *db);
|
||||
uint64_t tikker_calculate_total_releases(sqlite3 *db);
|
||||
uint64_t tikker_calculate_total_repeats(sqlite3 *db);
|
||||
|
||||
void tikker_free_daily_entries(tikker_daily_entry_t *entries, int count);
|
||||
void tikker_free_hourly_entries(tikker_hourly_entry_t *entries, int count);
|
||||
void tikker_free_weekly_entries(tikker_weekly_entry_t *entries, int count);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,37 @@
|
||||
#ifndef TIKKER_CONFIG_H
|
||||
#define TIKKER_CONFIG_H
|
||||
|
||||
#define TIKKER_VERSION "2.0.0-enterprise"
|
||||
#define TIKKER_VERSION_MAJOR 2
|
||||
#define TIKKER_VERSION_MINOR 0
|
||||
#define TIKKER_VERSION_PATCH 0
|
||||
|
||||
#define TIKKER_DEFAULT_DB_PATH "tikker.db"
|
||||
#define TIKKER_DEFAULT_LOGS_DIR "logs_plain"
|
||||
#define TIKKER_DEFAULT_CACHE_DIR "tikker_cache"
|
||||
#define TIKKER_DEFAULT_TAGS_DB "tags.db"
|
||||
#define TIKKER_DEFAULT_LOGS_DB "logs.db"
|
||||
|
||||
#define TIKKER_TEXT_BUFFER_INITIAL 4096
|
||||
#define TIKKER_TEXT_BUFFER_MAX (1024 * 1024 * 100)
|
||||
|
||||
#define TIKKER_MAX_KEYCODE 256
|
||||
#define TIKKER_MAX_KEY_NAME 32
|
||||
#define TIKKER_MAX_DATE_STR 11
|
||||
#define TIKKER_MAX_PATH 4096
|
||||
|
||||
#define TIKKER_WORD_MIN_LENGTH 2
|
||||
#define TIKKER_WORD_MAX_LENGTH 255
|
||||
|
||||
#define TIKKER_TOP_WORDS_DEFAULT 10
|
||||
#define TIKKER_TOP_KEYS_DEFAULT 10
|
||||
|
||||
#define TIKKER_SHIFT_KEYCODE_LSHIFT 42
|
||||
#define TIKKER_SHIFT_KEYCODE_RSHIFT 54
|
||||
|
||||
#define TIKKER_KEY_SPACE 57
|
||||
|
||||
#define TIKKER_ENABLE_PROFILING 0
|
||||
#define TIKKER_ENABLE_DEBUG 0
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef TIKKER_DATABASE_H
|
||||
#define TIKKER_DATABASE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
typedef struct {
|
||||
const char *db_path;
|
||||
sqlite3 *conn;
|
||||
int flags;
|
||||
} tikker_db_t;
|
||||
|
||||
tikker_db_t* tikker_db_open(const char *path);
|
||||
void tikker_db_close(tikker_db_t *db);
|
||||
int tikker_db_init_schema(tikker_db_t *db);
|
||||
int tikker_db_execute(tikker_db_t *db, const char *sql);
|
||||
int tikker_db_query(tikker_db_t *db, const char *sql,
|
||||
int (*callback)(void*, int, char**, char**),
|
||||
void *arg);
|
||||
int tikker_db_begin_transaction(tikker_db_t *db);
|
||||
int tikker_db_commit_transaction(tikker_db_t *db);
|
||||
int tikker_db_rollback_transaction(tikker_db_t *db);
|
||||
int tikker_db_vacuum(tikker_db_t *db);
|
||||
int tikker_db_pragma(tikker_db_t *db, const char *pragma, char *result, size_t result_size);
|
||||
int tikker_db_integrity_check(tikker_db_t *db);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef TIKKER_DECODER_H
|
||||
#define TIKKER_DECODER_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define TIKKER_KEY_SPACE 57
|
||||
#define TIKKER_KEY_ENTER 28
|
||||
#define TIKKER_KEY_TAB 15
|
||||
#define TIKKER_KEY_BACKSPACE 14
|
||||
#define TIKKER_KEY_LSHIFT 42
|
||||
#define TIKKER_KEY_RSHIFT 54
|
||||
|
||||
typedef struct {
|
||||
char *data;
|
||||
size_t capacity;
|
||||
size_t length;
|
||||
} tikker_text_buffer_t;
|
||||
|
||||
tikker_text_buffer_t* tikker_text_buffer_create(size_t initial_capacity);
|
||||
void tikker_text_buffer_free(tikker_text_buffer_t *buf);
|
||||
int tikker_text_buffer_append(tikker_text_buffer_t *buf, const char *data, size_t len);
|
||||
int tikker_text_buffer_append_char(tikker_text_buffer_t *buf, char c);
|
||||
void tikker_text_buffer_pop(tikker_text_buffer_t *buf);
|
||||
|
||||
int tikker_keycode_to_char(uint32_t keycode, int shift_active, char *out_char);
|
||||
const char* tikker_keycode_to_name(uint32_t keycode);
|
||||
|
||||
int tikker_decode_file(const char *input_path, const char *output_path);
|
||||
int tikker_decode_buffer(const char *input, size_t input_len,
|
||||
tikker_text_buffer_t *output);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,57 @@
|
||||
#ifndef TIKKER_INDEXER_H
|
||||
#define TIKKER_INDEXER_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
typedef struct {
|
||||
const char *word;
|
||||
uint64_t count;
|
||||
int rank;
|
||||
} tikker_word_entry_t;
|
||||
|
||||
typedef struct {
|
||||
sqlite3 *db;
|
||||
int word_count;
|
||||
uint64_t total_words;
|
||||
} tikker_word_index_t;
|
||||
|
||||
tikker_word_index_t* tikker_word_index_open(const char *db_path);
|
||||
void tikker_word_index_close(tikker_word_index_t *index);
|
||||
int tikker_word_index_reset(tikker_word_index_t *index);
|
||||
|
||||
int tikker_index_text_file(const char *file_path,
|
||||
const char *db_path);
|
||||
|
||||
int tikker_index_directory(const char *dir_path,
|
||||
const char *db_path);
|
||||
|
||||
int tikker_word_index_add(tikker_word_index_t *index,
|
||||
const char *word,
|
||||
uint64_t count);
|
||||
|
||||
int tikker_word_index_commit(tikker_word_index_t *index);
|
||||
|
||||
int tikker_word_get_frequency(const char *db_path,
|
||||
const char *word,
|
||||
uint64_t *count);
|
||||
|
||||
int tikker_word_get_rank(const char *db_path,
|
||||
const char *word,
|
||||
int *rank,
|
||||
uint64_t *count);
|
||||
|
||||
int tikker_word_get_top(const char *db_path,
|
||||
int limit,
|
||||
tikker_word_entry_t **entries,
|
||||
int *count);
|
||||
|
||||
int tikker_word_get_total_count(const char *db_path,
|
||||
uint64_t *total);
|
||||
|
||||
int tikker_word_get_unique_count(const char *db_path,
|
||||
int *count);
|
||||
|
||||
void tikker_word_entries_free(tikker_word_entry_t *entries, int count);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,19 @@
|
||||
#ifndef TIKKER_REPORT_H
|
||||
#define TIKKER_REPORT_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
typedef struct {
|
||||
char *title;
|
||||
char *data;
|
||||
size_t data_size;
|
||||
} tikker_report_t;
|
||||
|
||||
int tikker_merge_text_files(const char *input_dir,
|
||||
const char *pattern,
|
||||
const char *output_path);
|
||||
|
||||
void tikker_report_free(tikker_report_t *report);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,138 @@
|
||||
#ifndef TIKKER_H
|
||||
#define TIKKER_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include <sqlite3.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#define TIKKER_SUCCESS 0
|
||||
#define TIKKER_ERROR_DB -1
|
||||
#define TIKKER_ERROR_MEMORY -2
|
||||
#define TIKKER_ERROR_IO -3
|
||||
#define TIKKER_ERROR_INVALID -4
|
||||
#define TIKKER_ERROR_NOT_FOUND -5
|
||||
|
||||
typedef struct {
|
||||
sqlite3 *db;
|
||||
const char *db_path;
|
||||
uint32_t flags;
|
||||
} tikker_context_t;
|
||||
|
||||
typedef struct {
|
||||
const char *word;
|
||||
uint64_t count;
|
||||
float percentage;
|
||||
} tikker_word_stat_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t keycode;
|
||||
const char *key_name;
|
||||
uint64_t count;
|
||||
} tikker_key_stat_t;
|
||||
|
||||
typedef struct {
|
||||
char date[11];
|
||||
uint64_t total_presses;
|
||||
uint64_t total_releases;
|
||||
uint64_t total_repeats;
|
||||
} tikker_daily_stat_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t hour;
|
||||
uint64_t presses;
|
||||
} tikker_hourly_stat_t;
|
||||
|
||||
typedef struct {
|
||||
char weekday[10];
|
||||
uint64_t presses;
|
||||
} tikker_weekday_stat_t;
|
||||
|
||||
typedef struct {
|
||||
double decode_time;
|
||||
double index_time;
|
||||
double aggregate_time;
|
||||
uint64_t records_processed;
|
||||
} tikker_perf_metrics_t;
|
||||
|
||||
tikker_context_t* tikker_open(const char *db_path);
|
||||
void tikker_close(tikker_context_t *ctx);
|
||||
int tikker_init_schema(tikker_context_t *ctx);
|
||||
int tikker_get_version(char *buffer, size_t size);
|
||||
|
||||
int tikker_get_daily_stats(tikker_context_t *ctx,
|
||||
tikker_daily_stat_t **stats,
|
||||
int *count);
|
||||
|
||||
int tikker_get_hourly_stats(tikker_context_t *ctx,
|
||||
const char *date,
|
||||
tikker_hourly_stat_t **stats,
|
||||
int *count);
|
||||
|
||||
int tikker_get_weekday_stats(tikker_context_t *ctx,
|
||||
tikker_weekday_stat_t **stats,
|
||||
int *count);
|
||||
|
||||
int tikker_get_top_words(tikker_context_t *ctx,
|
||||
int limit,
|
||||
tikker_word_stat_t **words,
|
||||
int *count);
|
||||
|
||||
int tikker_get_top_keys(tikker_context_t *ctx,
|
||||
int limit,
|
||||
tikker_key_stat_t **keys,
|
||||
int *count);
|
||||
|
||||
int tikker_get_date_range(tikker_context_t *ctx,
|
||||
char *min_date,
|
||||
char *max_date);
|
||||
|
||||
int tikker_get_event_counts(tikker_context_t *ctx,
|
||||
uint64_t *pressed,
|
||||
uint64_t *released,
|
||||
uint64_t *repeated);
|
||||
|
||||
int tikker_decode_keylog(const char *input_file,
|
||||
const char *output_file);
|
||||
|
||||
int tikker_decode_keylog_buffer(const char *input,
|
||||
size_t input_len,
|
||||
char **output,
|
||||
size_t *output_len);
|
||||
|
||||
int tikker_index_text_file(const char *file_path,
|
||||
const char *db_path);
|
||||
|
||||
int tikker_index_directory(const char *dir_path,
|
||||
const char *db_path);
|
||||
|
||||
int tikker_get_word_frequency(const char *db_path,
|
||||
const char *word,
|
||||
uint64_t *count);
|
||||
|
||||
int tikker_get_top_words_from_db(const char *db_path,
|
||||
int limit,
|
||||
tikker_word_stat_t **words,
|
||||
int *count);
|
||||
|
||||
int tikker_generate_html_report(tikker_context_t *ctx,
|
||||
const char *output_file,
|
||||
const char *graph_dir);
|
||||
|
||||
int tikker_generate_json_report(tikker_context_t *ctx,
|
||||
char **json_output);
|
||||
|
||||
int tikker_merge_text_files(const char *input_dir,
|
||||
const char *pattern,
|
||||
const char *output_path);
|
||||
|
||||
int tikker_get_metrics(tikker_perf_metrics_t *metrics);
|
||||
|
||||
void tikker_free_words(tikker_word_stat_t *words, int count);
|
||||
void tikker_free_keys(tikker_key_stat_t *keys, int count);
|
||||
void tikker_free_daily_stats(tikker_daily_stat_t *stats, int count);
|
||||
void tikker_free_hourly_stats(tikker_hourly_stat_t *stats, int count);
|
||||
void tikker_free_weekday_stats(tikker_weekday_stat_t *stats, int count);
|
||||
void tikker_free_json(char *json);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,60 @@
|
||||
#ifndef TIKKER_TYPES_H
|
||||
#define TIKKER_TYPES_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <time.h>
|
||||
|
||||
typedef enum {
|
||||
TIKKER_LOG_DEBUG = 0,
|
||||
TIKKER_LOG_INFO = 1,
|
||||
TIKKER_LOG_WARN = 2,
|
||||
TIKKER_LOG_ERROR = 3,
|
||||
TIKKER_LOG_FATAL = 4
|
||||
} tikker_log_level_t;
|
||||
|
||||
typedef enum {
|
||||
TIKKER_EVENT_PRESSED = 0,
|
||||
TIKKER_EVENT_RELEASED = 1,
|
||||
TIKKER_EVENT_REPEATED = 2
|
||||
} tikker_event_type_t;
|
||||
|
||||
typedef struct {
|
||||
uint64_t id;
|
||||
uint32_t keycode;
|
||||
tikker_event_type_t event;
|
||||
const char *name;
|
||||
time_t timestamp;
|
||||
char character;
|
||||
} tikker_kevent_t;
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const char *symbol;
|
||||
uint32_t code;
|
||||
} tikker_key_mapping_t;
|
||||
|
||||
typedef struct {
|
||||
int year;
|
||||
int month;
|
||||
int day;
|
||||
int hour;
|
||||
int minute;
|
||||
int second;
|
||||
int weekday;
|
||||
} tikker_datetime_t;
|
||||
|
||||
typedef struct {
|
||||
char *buffer;
|
||||
size_t capacity;
|
||||
size_t length;
|
||||
} tikker_string_t;
|
||||
|
||||
tikker_string_t* tikker_string_create(size_t capacity);
|
||||
void tikker_string_free(tikker_string_t *str);
|
||||
int tikker_string_append(tikker_string_t *str, const char *data);
|
||||
int tikker_string_append_char(tikker_string_t *str, char c);
|
||||
void tikker_string_clear(tikker_string_t *str);
|
||||
char* tikker_string_cstr(tikker_string_t *str);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,92 @@
|
||||
#include <aggregator.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
int tikker_aggregate_daily(sqlite3 *db,
|
||||
tikker_daily_entry_t **entries,
|
||||
int *count) {
|
||||
if (!db || !entries || !count) return -1;
|
||||
*entries = NULL;
|
||||
*count = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tikker_aggregate_hourly(sqlite3 *db,
|
||||
const char *date,
|
||||
tikker_hourly_entry_t **entries,
|
||||
int *count) {
|
||||
if (!db || !date || !entries || !count) return -1;
|
||||
*entries = NULL;
|
||||
*count = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tikker_aggregate_weekly(sqlite3 *db,
|
||||
tikker_weekly_entry_t **entries,
|
||||
int *count) {
|
||||
if (!db || !entries || !count) return -1;
|
||||
*entries = NULL;
|
||||
*count = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tikker_aggregate_weekday(sqlite3 *db,
|
||||
tikker_weekday_stat_t **entries,
|
||||
int *count) {
|
||||
if (!db || !entries || !count) return -1;
|
||||
*entries = NULL;
|
||||
*count = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tikker_get_peak_hour(sqlite3 *db,
|
||||
const char *date,
|
||||
uint32_t *hour,
|
||||
uint64_t *presses) {
|
||||
if (!db || !date || !hour || !presses) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tikker_get_peak_day(sqlite3 *db,
|
||||
char *date,
|
||||
uint64_t *presses) {
|
||||
if (!db || !date || !presses) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tikker_get_daily_average(sqlite3 *db,
|
||||
uint64_t *avg_presses,
|
||||
int *num_days) {
|
||||
if (!db || !avg_presses || !num_days) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t tikker_calculate_total_presses(sqlite3 *db) {
|
||||
if (!db) return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t tikker_calculate_total_releases(sqlite3 *db) {
|
||||
if (!db) return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t tikker_calculate_total_repeats(sqlite3 *db) {
|
||||
if (!db) return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void tikker_free_daily_entries(tikker_daily_entry_t *entries, int count) {
|
||||
if (entries) free(entries);
|
||||
}
|
||||
|
||||
void tikker_free_hourly_entries(tikker_hourly_entry_t *entries, int count) {
|
||||
if (entries) free(entries);
|
||||
}
|
||||
|
||||
void tikker_free_weekly_entries(tikker_weekly_entry_t *entries, int count) {
|
||||
if (entries) free(entries);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
#include <database.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
tikker_db_t* tikker_db_open(const char *path) {
|
||||
if (!path) return NULL;
|
||||
tikker_db_t *db = malloc(sizeof(tikker_db_t));
|
||||
if (!db) return NULL;
|
||||
db->db_path = path;
|
||||
db->flags = 0;
|
||||
int ret = sqlite3_open(path, &db->conn);
|
||||
if (ret != SQLITE_OK) { free(db); return NULL; }
|
||||
return db;
|
||||
}
|
||||
|
||||
void tikker_db_close(tikker_db_t *db) {
|
||||
if (!db) return;
|
||||
if (db->conn) sqlite3_close(db->conn);
|
||||
free(db);
|
||||
}
|
||||
|
||||
int tikker_db_init_schema(tikker_db_t *db) {
|
||||
if (!db || !db->conn) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tikker_db_execute(tikker_db_t *db, const char *sql) {
|
||||
if (!db || !db->conn || !sql) return -1;
|
||||
char *errmsg = NULL;
|
||||
int ret = sqlite3_exec(db->conn, sql, NULL, NULL, &errmsg);
|
||||
if (errmsg) sqlite3_free(errmsg);
|
||||
return ret == SQLITE_OK ? 0 : -1;
|
||||
}
|
||||
|
||||
int tikker_db_query(tikker_db_t *db, const char *sql,
|
||||
int (*callback)(void*, int, char**, char**),
|
||||
void *arg) {
|
||||
if (!db || !db->conn || !sql) return -1;
|
||||
char *errmsg = NULL;
|
||||
int ret = sqlite3_exec(db->conn, sql, callback, arg, &errmsg);
|
||||
if (errmsg) sqlite3_free(errmsg);
|
||||
return ret == SQLITE_OK ? 0 : -1;
|
||||
}
|
||||
|
||||
int tikker_db_begin_transaction(tikker_db_t *db) {
|
||||
if (!db || !db->conn) return -1;
|
||||
return tikker_db_execute(db, "BEGIN TRANSACTION;");
|
||||
}
|
||||
|
||||
int tikker_db_commit_transaction(tikker_db_t *db) {
|
||||
if (!db || !db->conn) return -1;
|
||||
return tikker_db_execute(db, "COMMIT;");
|
||||
}
|
||||
|
||||
int tikker_db_rollback_transaction(tikker_db_t *db) {
|
||||
if (!db || !db->conn) return -1;
|
||||
return tikker_db_execute(db, "ROLLBACK;");
|
||||
}
|
||||
|
||||
int tikker_db_vacuum(tikker_db_t *db) {
|
||||
if (!db || !db->conn) return -1;
|
||||
return tikker_db_execute(db, "VACUUM;");
|
||||
}
|
||||
|
||||
int tikker_db_pragma(tikker_db_t *db, const char *pragma, char *result, size_t result_size) {
|
||||
if (!db || !db->conn || !pragma || !result) return -1;
|
||||
|
||||
char sql[512];
|
||||
snprintf(sql, sizeof(sql), "PRAGMA %s", pragma);
|
||||
|
||||
sqlite3_stmt *stmt;
|
||||
if (sqlite3_prepare_v2(db->conn, sql, -1, &stmt, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
const unsigned char *text = sqlite3_column_text(stmt, 0);
|
||||
if (text) {
|
||||
snprintf(result, result_size, "%s", (const char *)text);
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int tikker_db_integrity_check(tikker_db_t *db) {
|
||||
if (!db || !db->conn) return -1;
|
||||
return tikker_db_execute(db, "PRAGMA integrity_check;");
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
#include <decoder.h>
|
||||
#include <config.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static const char *keycode_names[] = {
|
||||
"NONE", "ESC", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=",
|
||||
"BACKSPACE", "TAB", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P",
|
||||
"[", "]", "ENTER", "L_CTRL", "A", "S", "D", "F", "G", "H", "J", "K",
|
||||
"L", ";", "'", "`", "L_SHIFT", "\\", "Z", "X", "C", "V", "B", "N", "M",
|
||||
",", ".", "/", "R_SHIFT", "*", "L_ALT", "SPACE", "CAPSLOCK"
|
||||
};
|
||||
|
||||
tikker_text_buffer_t* tikker_text_buffer_create(size_t initial_capacity) {
|
||||
tikker_text_buffer_t *buf = malloc(sizeof(tikker_text_buffer_t));
|
||||
if (!buf) return NULL;
|
||||
buf->capacity = initial_capacity ? initial_capacity : 4096;
|
||||
buf->data = malloc(buf->capacity);
|
||||
if (!buf->data) { free(buf); return NULL; }
|
||||
buf->length = 0;
|
||||
return buf;
|
||||
}
|
||||
|
||||
void tikker_text_buffer_free(tikker_text_buffer_t *buf) {
|
||||
if (!buf) return;
|
||||
if (buf->data) free(buf->data);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
int tikker_text_buffer_append(tikker_text_buffer_t *buf, const char *data, size_t len) {
|
||||
if (!buf || !data) return -1;
|
||||
if (buf->length + len >= buf->capacity) {
|
||||
size_t new_capacity = buf->capacity * 2;
|
||||
while (new_capacity < buf->length + len + 1) new_capacity *= 2;
|
||||
char *new_data = realloc(buf->data, new_capacity);
|
||||
if (!new_data) return -1;
|
||||
buf->data = new_data;
|
||||
buf->capacity = new_capacity;
|
||||
}
|
||||
memcpy(buf->data + buf->length, data, len);
|
||||
buf->length += len;
|
||||
buf->data[buf->length] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tikker_text_buffer_append_char(tikker_text_buffer_t *buf, char c) {
|
||||
return tikker_text_buffer_append(buf, &c, 1);
|
||||
}
|
||||
|
||||
void tikker_text_buffer_pop(tikker_text_buffer_t *buf) {
|
||||
if (!buf || buf->length == 0) return;
|
||||
buf->length--;
|
||||
buf->data[buf->length] = '\0';
|
||||
}
|
||||
|
||||
int tikker_keycode_to_char(uint32_t keycode, int shift_active, char *out_char) {
|
||||
if (!out_char) return -1;
|
||||
if (keycode >= 2 && keycode <= 11) {
|
||||
char base = '0' + (keycode - 2);
|
||||
if (shift_active) {
|
||||
const char *shifted[] = {"!", "@", "#", "$", "%", "^", "&", "*", "(", ")"};
|
||||
*out_char = shifted[keycode - 2][0];
|
||||
} else {
|
||||
*out_char = base;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (keycode >= 16 && keycode <= 25) {
|
||||
*out_char = 'a' + (keycode - 16);
|
||||
if (shift_active) *out_char = (*out_char) - 32;
|
||||
return 0;
|
||||
}
|
||||
if (keycode == TIKKER_KEY_SPACE) { *out_char = ' '; return 0; }
|
||||
*out_char = '?';
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* tikker_keycode_to_name(uint32_t keycode) {
|
||||
if (keycode < sizeof(keycode_names) / sizeof(keycode_names[0])) return keycode_names[keycode];
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
static const char* shift_number_map[] = {
|
||||
"!", "@", "#", "$", "%", "^", "&", "*", "(", ")"
|
||||
};
|
||||
|
||||
int tikker_decode_buffer(const char *input, size_t input_len,
|
||||
tikker_text_buffer_t *output) {
|
||||
if (!input || !output) return -1;
|
||||
|
||||
int shift_active = 0;
|
||||
size_t i = 0;
|
||||
|
||||
while (i < input_len) {
|
||||
if (input[i] == '[') {
|
||||
size_t j = i + 1;
|
||||
while (j < input_len && input[j] != ']') j++;
|
||||
|
||||
if (j >= input_len) return -1;
|
||||
|
||||
size_t token_len = j - i - 1;
|
||||
char token[256];
|
||||
if (token_len >= sizeof(token)) return -1;
|
||||
|
||||
memcpy(token, input + i + 1, token_len);
|
||||
token[token_len] = '\0';
|
||||
|
||||
if (strcmp(token, "LEFT_SHIFT") == 0 || strcmp(token, "R_SHIFT") == 0) {
|
||||
shift_active = 1;
|
||||
} else if (strcmp(token, "BACKSPACE") == 0) {
|
||||
tikker_text_buffer_pop(output);
|
||||
} else if (strcmp(token, "TAB") == 0) {
|
||||
tikker_text_buffer_append_char(output, '\t');
|
||||
} else if (strcmp(token, "ENTER") == 0) {
|
||||
tikker_text_buffer_append_char(output, '\n');
|
||||
} else if (strcmp(token, "UP") == 0 || strcmp(token, "DOWN") == 0 ||
|
||||
strcmp(token, "LEFT") == 0 || strcmp(token, "RIGHT") == 0) {
|
||||
} else if (token_len == 1) {
|
||||
char c = token[0];
|
||||
if (shift_active) {
|
||||
if (c >= 'a' && c <= 'z') {
|
||||
c = c - 32;
|
||||
} else if (c >= '0' && c <= '9') {
|
||||
c = shift_number_map[c - '0'][0];
|
||||
}
|
||||
shift_active = 0;
|
||||
} else {
|
||||
if (c >= 'A' && c <= 'Z') {
|
||||
c = c + 32;
|
||||
}
|
||||
}
|
||||
tikker_text_buffer_append_char(output, c);
|
||||
}
|
||||
|
||||
i = j + 1;
|
||||
} else if (input[i] == ' ' || input[i] == '\t' || input[i] == '\n') {
|
||||
i++;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tikker_decode_file(const char *input_path, const char *output_path) {
|
||||
if (!input_path || !output_path) return -1;
|
||||
|
||||
FILE *input_file = fopen(input_path, "r");
|
||||
if (!input_file) return -1;
|
||||
|
||||
fseek(input_file, 0, SEEK_END);
|
||||
long file_size = ftell(input_file);
|
||||
fseek(input_file, 0, SEEK_SET);
|
||||
|
||||
if (file_size <= 0) {
|
||||
fclose(input_file);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char *buffer = malloc(file_size);
|
||||
if (!buffer) {
|
||||
fclose(input_file);
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t read_bytes = fread(buffer, 1, file_size, input_file);
|
||||
fclose(input_file);
|
||||
|
||||
if (read_bytes != (size_t)file_size) {
|
||||
free(buffer);
|
||||
return -1;
|
||||
}
|
||||
|
||||
tikker_text_buffer_t *output_buf = tikker_text_buffer_create(file_size);
|
||||
if (!output_buf) {
|
||||
free(buffer);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ret = tikker_decode_buffer(buffer, file_size, output_buf);
|
||||
free(buffer);
|
||||
|
||||
if (ret != 0) {
|
||||
tikker_text_buffer_free(output_buf);
|
||||
return -1;
|
||||
}
|
||||
|
||||
FILE *output_file = fopen(output_path, "w");
|
||||
if (!output_file) {
|
||||
tikker_text_buffer_free(output_buf);
|
||||
return -1;
|
||||
}
|
||||
|
||||
fwrite(output_buf->data, 1, output_buf->length, output_file);
|
||||
fclose(output_file);
|
||||
|
||||
tikker_text_buffer_free(output_buf);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
#define _DEFAULT_SOURCE
|
||||
#include <indexer.h>
|
||||
#include <database.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
|
||||
tikker_word_index_t* tikker_word_index_open(const char *db_path) {
|
||||
if (!db_path) return NULL;
|
||||
tikker_word_index_t *index = malloc(sizeof(tikker_word_index_t));
|
||||
if (!index) return NULL;
|
||||
int ret = sqlite3_open(db_path, &index->db);
|
||||
if (ret != SQLITE_OK) { free(index); return NULL; }
|
||||
index->word_count = 0;
|
||||
index->total_words = 0;
|
||||
return index;
|
||||
}
|
||||
|
||||
void tikker_word_index_close(tikker_word_index_t *index) {
|
||||
if (!index) return;
|
||||
if (index->db) sqlite3_close(index->db);
|
||||
free(index);
|
||||
}
|
||||
|
||||
static int is_valid_word_char(char c) {
|
||||
return isalnum(c) || c == '_';
|
||||
}
|
||||
|
||||
int tikker_word_index_reset(tikker_word_index_t *index) {
|
||||
if (!index || !index->db) return -1;
|
||||
|
||||
sqlite3_exec(index->db, "DROP TABLE IF EXISTS words", NULL, NULL, NULL);
|
||||
|
||||
const char *sql = "CREATE TABLE IF NOT EXISTS words ("
|
||||
"word TEXT NOT NULL PRIMARY KEY,"
|
||||
"count INTEGER NOT NULL)";
|
||||
|
||||
char *errmsg = NULL;
|
||||
int ret = sqlite3_exec(index->db, sql, NULL, NULL, &errmsg);
|
||||
if (errmsg) sqlite3_free(errmsg);
|
||||
|
||||
if (ret == SQLITE_OK) {
|
||||
index->word_count = 0;
|
||||
index->total_words = 0;
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int tikker_index_text_file(const char *file_path, const char *db_path) {
|
||||
if (!file_path || !db_path) return -1;
|
||||
|
||||
FILE *f = fopen(file_path, "r");
|
||||
if (!f) return -1;
|
||||
|
||||
tikker_word_index_t *index = tikker_word_index_open(db_path);
|
||||
if (!index) {
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char word[256];
|
||||
int word_len = 0;
|
||||
int c;
|
||||
|
||||
while ((c = fgetc(f)) != EOF) {
|
||||
if (is_valid_word_char(c)) {
|
||||
if (word_len < (int)sizeof(word) - 1) {
|
||||
word[word_len++] = tolower(c);
|
||||
}
|
||||
} else {
|
||||
if (word_len > 0) {
|
||||
word[word_len] = '\0';
|
||||
tikker_word_index_add(index, word, 1);
|
||||
word_len = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (word_len > 0) {
|
||||
word[word_len] = '\0';
|
||||
tikker_word_index_add(index, word, 1);
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
tikker_word_index_commit(index);
|
||||
tikker_word_index_close(index);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tikker_index_directory(const char *dir_path, const char *db_path) {
|
||||
if (!dir_path || !db_path) return -1;
|
||||
|
||||
DIR *dir = opendir(dir_path);
|
||||
if (!dir) return -1;
|
||||
|
||||
tikker_word_index_t *index = tikker_word_index_open(db_path);
|
||||
if (!index) {
|
||||
closedir(dir);
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (entry->d_type == DT_REG && strstr(entry->d_name, ".txt")) {
|
||||
char file_path[1024];
|
||||
snprintf(file_path, sizeof(file_path), "%s/%s", dir_path, entry->d_name);
|
||||
|
||||
FILE *f = fopen(file_path, "r");
|
||||
if (f) {
|
||||
char word[256];
|
||||
int word_len = 0;
|
||||
int c;
|
||||
|
||||
while ((c = fgetc(f)) != EOF) {
|
||||
if (is_valid_word_char(c)) {
|
||||
if (word_len < (int)sizeof(word) - 1) {
|
||||
word[word_len++] = tolower(c);
|
||||
}
|
||||
} else {
|
||||
if (word_len >= 2) {
|
||||
word[word_len] = '\0';
|
||||
tikker_word_index_add(index, word, 1);
|
||||
}
|
||||
word_len = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (word_len >= 2) {
|
||||
word[word_len] = '\0';
|
||||
tikker_word_index_add(index, word, 1);
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
tikker_word_index_commit(index);
|
||||
tikker_word_index_close(index);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tikker_word_index_add(tikker_word_index_t *index, const char *word, uint64_t count) {
|
||||
if (!index || !index->db || !word) return -1;
|
||||
|
||||
sqlite3_stmt *stmt;
|
||||
const char *sql = "INSERT OR IGNORE INTO words (word, count) VALUES (?, 0); "
|
||||
"UPDATE words SET count = count + ? WHERE word = ?";
|
||||
|
||||
if (sqlite3_prepare_v2(index->db, sql, -1, &stmt, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, word, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(stmt, 2, count);
|
||||
sqlite3_bind_text(stmt, 3, word, -1, SQLITE_STATIC);
|
||||
|
||||
if (sqlite3_step(stmt) == SQLITE_DONE) {
|
||||
index->word_count++;
|
||||
index->total_words += count;
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int tikker_word_index_commit(tikker_word_index_t *index) {
|
||||
if (!index || !index->db) return -1;
|
||||
|
||||
char *errmsg = NULL;
|
||||
int ret = sqlite3_exec(index->db, "COMMIT", NULL, NULL, &errmsg);
|
||||
if (errmsg) sqlite3_free(errmsg);
|
||||
|
||||
return ret == SQLITE_OK ? 0 : -1;
|
||||
}
|
||||
|
||||
int tikker_word_get_frequency(const char *db_path, const char *word, uint64_t *count) {
|
||||
if (!db_path || !word || !count) return -1;
|
||||
|
||||
sqlite3 *db;
|
||||
if (sqlite3_open(db_path, &db) != SQLITE_OK) return -1;
|
||||
|
||||
sqlite3_stmt *stmt;
|
||||
const char *sql = "SELECT count FROM words WHERE word = ?";
|
||||
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, word, -1, SQLITE_STATIC);
|
||||
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
*count = sqlite3_column_int64(stmt, 0);
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_close(db);
|
||||
return 0;
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
*count = 0;
|
||||
sqlite3_close(db);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int tikker_word_get_rank(const char *db_path, const char *word, int *rank, uint64_t *count) {
|
||||
if (!db_path || !word || !rank || !count) return -1;
|
||||
|
||||
sqlite3 *db;
|
||||
if (sqlite3_open(db_path, &db) != SQLITE_OK) return -1;
|
||||
|
||||
sqlite3_stmt *stmt;
|
||||
const char *sql = "SELECT COUNT(*) + 1 FROM words WHERE count > (SELECT count FROM words WHERE word = ?)";
|
||||
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, word, -1, SQLITE_STATIC);
|
||||
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
*rank = sqlite3_column_int(stmt, 0);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
tikker_word_get_frequency(db_path, word, count);
|
||||
sqlite3_close(db);
|
||||
return 0;
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
sqlite3_close(db);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int tikker_word_get_top(const char *db_path, int limit, tikker_word_entry_t **entries, int *count) {
|
||||
if (!db_path || limit <= 0 || !entries || !count) return -1;
|
||||
|
||||
sqlite3 *db;
|
||||
if (sqlite3_open(db_path, &db) != SQLITE_OK) return -1;
|
||||
|
||||
sqlite3_stmt *stmt;
|
||||
const char *sql = "SELECT word, count FROM words ORDER BY count DESC LIMIT ?";
|
||||
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
sqlite3_close(db);
|
||||
return -1;
|
||||
}
|
||||
|
||||
sqlite3_bind_int(stmt, 1, limit);
|
||||
|
||||
int result_count = 0;
|
||||
tikker_word_entry_t *result = malloc(limit * sizeof(tikker_word_entry_t));
|
||||
if (!result) {
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_close(db);
|
||||
return -1;
|
||||
}
|
||||
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW && result_count < limit) {
|
||||
const char *word_str = (const char *)sqlite3_column_text(stmt, 0);
|
||||
uint64_t word_count = sqlite3_column_int64(stmt, 1);
|
||||
|
||||
result[result_count].word = strdup(word_str);
|
||||
result[result_count].count = word_count;
|
||||
result[result_count].rank = result_count + 1;
|
||||
result_count++;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_close(db);
|
||||
|
||||
*entries = result;
|
||||
*count = result_count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tikker_word_get_total_count(const char *db_path, uint64_t *total) {
|
||||
if (!db_path || !total) return -1;
|
||||
|
||||
sqlite3 *db;
|
||||
if (sqlite3_open(db_path, &db) != SQLITE_OK) return -1;
|
||||
|
||||
sqlite3_stmt *stmt;
|
||||
const char *sql = "SELECT SUM(count) FROM words";
|
||||
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
*total = sqlite3_column_int64(stmt, 0);
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_close(db);
|
||||
return 0;
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
*total = 0;
|
||||
sqlite3_close(db);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int tikker_word_get_unique_count(const char *db_path, int *count) {
|
||||
if (!db_path || !count) return -1;
|
||||
|
||||
sqlite3 *db;
|
||||
if (sqlite3_open(db_path, &db) != SQLITE_OK) return -1;
|
||||
|
||||
sqlite3_stmt *stmt;
|
||||
const char *sql = "SELECT COUNT(*) FROM words";
|
||||
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
*count = sqlite3_column_int(stmt, 0);
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_close(db);
|
||||
return 0;
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
*count = 0;
|
||||
sqlite3_close(db);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void tikker_word_entries_free(tikker_word_entry_t *entries, int count) {
|
||||
if (entries) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (entries[i].word) free((char *)entries[i].word);
|
||||
}
|
||||
free(entries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
#define _DEFAULT_SOURCE
|
||||
#include <report.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <dirent.h>
|
||||
|
||||
int tikker_merge_text_files(const char *input_dir, const char *pattern, const char *output_path) {
|
||||
if (!input_dir || !output_path) return -1;
|
||||
|
||||
DIR *dir = opendir(input_dir);
|
||||
if (!dir) return -1;
|
||||
|
||||
FILE *output_file = fopen(output_path, "w");
|
||||
if (!output_file) {
|
||||
closedir(dir);
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct dirent *entry;
|
||||
int first = 1;
|
||||
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (entry->d_type == DT_REG && strstr(entry->d_name, ".txt")) {
|
||||
if (strcmp(entry->d_name, "merged.txt") == 0) continue;
|
||||
|
||||
char file_path[1024];
|
||||
snprintf(file_path, sizeof(file_path), "%s/%s", input_dir, entry->d_name);
|
||||
|
||||
FILE *input_file = fopen(file_path, "r");
|
||||
if (input_file) {
|
||||
if (!first) {
|
||||
fprintf(output_file, "\n\n");
|
||||
}
|
||||
first = 0;
|
||||
|
||||
char buffer[4096];
|
||||
size_t bytes_read;
|
||||
while ((bytes_read = fread(buffer, 1, sizeof(buffer), input_file)) > 0) {
|
||||
fwrite(buffer, 1, bytes_read, output_file);
|
||||
}
|
||||
|
||||
fclose(input_file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
fclose(output_file);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int count_token_in_file(const char *file_path, const char *token) {
|
||||
FILE *f = fopen(file_path, "r");
|
||||
if (!f) return 0;
|
||||
|
||||
int count = 0;
|
||||
char buffer[4096];
|
||||
size_t bytes_read;
|
||||
|
||||
while ((bytes_read = fread(buffer, 1, sizeof(buffer), f)) > 0) {
|
||||
for (size_t i = 0; i < bytes_read; ) {
|
||||
if (buffer[i] == '[') {
|
||||
size_t j = i + 1;
|
||||
while (j < bytes_read && buffer[j] != ']') j++;
|
||||
if (j < bytes_read && strcmp(token, "ENTER") == 0) {
|
||||
if (j - i - 1 == 5 && strncmp(buffer + i + 1, "ENTER", 5) == 0) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
i = j + 1;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
return count;
|
||||
}
|
||||
|
||||
static int internal_generate_html(sqlite3 *db, const char *output_file, const char *title) {
|
||||
if (!db || !output_file) return -1;
|
||||
|
||||
FILE *f = fopen(output_file, "w");
|
||||
if (!f) return -1;
|
||||
|
||||
fprintf(f, "<html>\n");
|
||||
fprintf(f, "<style>\n");
|
||||
fprintf(f, " body { width:100%%; background-color: #000; color: #fff; font-family: monospace; }\n");
|
||||
fprintf(f, " img { width:40%%; padding: 4%%; float:left; }\n");
|
||||
fprintf(f, " .stats { clear: both; padding: 20px; }\n");
|
||||
fprintf(f, "</style>\n");
|
||||
fprintf(f, "<body>\n");
|
||||
|
||||
if (title) {
|
||||
fprintf(f, "<h1>%s</h1>\n", title);
|
||||
}
|
||||
|
||||
fprintf(f, "<div class=\"stats\">\n");
|
||||
fprintf(f, "<p>Report generated by Tikker</p>\n");
|
||||
fprintf(f, "</div>\n");
|
||||
|
||||
fprintf(f, "</body>\n");
|
||||
fprintf(f, "</html>\n");
|
||||
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int internal_generate_json(sqlite3 *db, char **json_output) {
|
||||
if (!db || !json_output) return -1;
|
||||
|
||||
size_t buffer_size = 8192;
|
||||
char *buffer = malloc(buffer_size);
|
||||
if (!buffer) return -1;
|
||||
|
||||
snprintf(buffer, buffer_size, "{\"status\":\"success\",\"timestamp\":%ld}", (long)time(NULL));
|
||||
|
||||
*json_output = buffer;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int internal_generate_summary(sqlite3 *db, char *buffer, size_t buffer_size) {
|
||||
if (!db || !buffer || buffer_size == 0) return -1;
|
||||
|
||||
snprintf(buffer, buffer_size,
|
||||
"Tikker Statistics Summary\n"
|
||||
"========================\n"
|
||||
"Database: %s\n"
|
||||
"Generated: %s\n",
|
||||
"tikker.db", __DATE__);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void tikker_report_free(tikker_report_t *report) {
|
||||
if (!report) return;
|
||||
if (report->title) free(report->title);
|
||||
if (report->data) free(report->data);
|
||||
free(report);
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
#include <tikker.h>
|
||||
#include <config.h>
|
||||
#include <decoder.h>
|
||||
#include <indexer.h>
|
||||
#include <report.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
tikker_context_t* tikker_open(const char *db_path) {
|
||||
tikker_context_t *ctx = malloc(sizeof(tikker_context_t));
|
||||
if (!ctx) return NULL;
|
||||
|
||||
ctx->db_path = db_path ? strdup(db_path) : strdup(TIKKER_DEFAULT_DB_PATH);
|
||||
ctx->flags = 0;
|
||||
|
||||
int ret = sqlite3_open(ctx->db_path, &ctx->db);
|
||||
if (ret != SQLITE_OK) {
|
||||
free((void*)ctx->db_path);
|
||||
free(ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
void tikker_close(tikker_context_t *ctx) {
|
||||
if (!ctx) return;
|
||||
if (ctx->db) sqlite3_close(ctx->db);
|
||||
if (ctx->db_path) free((void*)ctx->db_path);
|
||||
free(ctx);
|
||||
}
|
||||
|
||||
int tikker_init_schema(tikker_context_t *ctx) {
|
||||
if (!ctx || !ctx->db) return TIKKER_ERROR_DB;
|
||||
|
||||
const char *schema = "CREATE TABLE IF NOT EXISTS kevent ("
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
"code INTEGER,"
|
||||
"event TEXT,"
|
||||
"name TEXT,"
|
||||
"timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,"
|
||||
"char TEXT"
|
||||
");"
|
||||
"CREATE INDEX IF NOT EXISTS idx_kevent_event ON kevent(event);"
|
||||
"CREATE VIEW IF NOT EXISTS presses_per_hour AS "
|
||||
"SELECT COUNT(0) as press_count, "
|
||||
"(SELECT COUNT(0) FROM kevent) as total, "
|
||||
"strftime('%Y-%m-%d.%H', timestamp) as period "
|
||||
"FROM kevent WHERE event='PRESSED' GROUP BY period;";
|
||||
|
||||
char *errmsg = NULL;
|
||||
int ret = sqlite3_exec(ctx->db, schema, NULL, NULL, &errmsg);
|
||||
if (ret != SQLITE_OK) {
|
||||
if (errmsg) sqlite3_free(errmsg);
|
||||
return TIKKER_ERROR_DB;
|
||||
}
|
||||
|
||||
return TIKKER_SUCCESS;
|
||||
}
|
||||
|
||||
int tikker_get_version(char *buffer, size_t size) {
|
||||
if (!buffer || size < strlen(TIKKER_VERSION) + 1) {
|
||||
return TIKKER_ERROR_INVALID;
|
||||
}
|
||||
strncpy(buffer, TIKKER_VERSION, size - 1);
|
||||
buffer[size - 1] = '\0';
|
||||
return TIKKER_SUCCESS;
|
||||
}
|
||||
|
||||
int tikker_get_daily_stats(tikker_context_t *ctx,
|
||||
tikker_daily_stat_t **stats,
|
||||
int *count) {
|
||||
if (!ctx || !stats || !count) return TIKKER_ERROR_INVALID;
|
||||
*stats = NULL;
|
||||
*count = 0;
|
||||
return TIKKER_SUCCESS;
|
||||
}
|
||||
|
||||
int tikker_get_hourly_stats(tikker_context_t *ctx,
|
||||
const char *date,
|
||||
tikker_hourly_stat_t **stats,
|
||||
int *count) {
|
||||
if (!ctx || !date || !stats || !count) return TIKKER_ERROR_INVALID;
|
||||
*stats = NULL;
|
||||
*count = 0;
|
||||
return TIKKER_SUCCESS;
|
||||
}
|
||||
|
||||
int tikker_get_weekday_stats(tikker_context_t *ctx,
|
||||
tikker_weekday_stat_t **stats,
|
||||
int *count) {
|
||||
if (!ctx || !stats || !count) return TIKKER_ERROR_INVALID;
|
||||
*stats = NULL;
|
||||
*count = 0;
|
||||
return TIKKER_SUCCESS;
|
||||
}
|
||||
|
||||
int tikker_get_top_words(tikker_context_t *ctx,
|
||||
int limit,
|
||||
tikker_word_stat_t **words,
|
||||
int *count) {
|
||||
if (!ctx || limit <= 0 || !words || !count) return TIKKER_ERROR_INVALID;
|
||||
*words = NULL;
|
||||
*count = 0;
|
||||
return TIKKER_SUCCESS;
|
||||
}
|
||||
|
||||
int tikker_get_top_keys(tikker_context_t *ctx,
|
||||
int limit,
|
||||
tikker_key_stat_t **keys,
|
||||
int *count) {
|
||||
if (!ctx || limit <= 0 || !keys || !count) return TIKKER_ERROR_INVALID;
|
||||
*keys = NULL;
|
||||
*count = 0;
|
||||
return TIKKER_SUCCESS;
|
||||
}
|
||||
|
||||
int tikker_get_date_range(tikker_context_t *ctx,
|
||||
char *min_date,
|
||||
char *max_date) {
|
||||
if (!ctx || !min_date || !max_date) return TIKKER_ERROR_INVALID;
|
||||
return TIKKER_SUCCESS;
|
||||
}
|
||||
|
||||
int tikker_get_event_counts(tikker_context_t *ctx,
|
||||
uint64_t *pressed,
|
||||
uint64_t *released,
|
||||
uint64_t *repeated) {
|
||||
if (!ctx || !pressed || !released || !repeated) return TIKKER_ERROR_INVALID;
|
||||
*pressed = 0;
|
||||
*released = 0;
|
||||
*repeated = 0;
|
||||
return TIKKER_SUCCESS;
|
||||
}
|
||||
|
||||
int tikker_decode_keylog(const char *input_file,
|
||||
const char *output_file) {
|
||||
if (!input_file || !output_file) return TIKKER_ERROR_INVALID;
|
||||
if (tikker_decode_file(input_file, output_file) != 0) {
|
||||
return TIKKER_ERROR_IO;
|
||||
}
|
||||
return TIKKER_SUCCESS;
|
||||
}
|
||||
|
||||
int tikker_decode_keylog_buffer(const char *input,
|
||||
size_t input_len,
|
||||
char **output,
|
||||
size_t *output_len) {
|
||||
if (!input || !output || !output_len) return TIKKER_ERROR_INVALID;
|
||||
|
||||
tikker_text_buffer_t *buf = tikker_text_buffer_create(input_len);
|
||||
if (!buf) return TIKKER_ERROR_MEMORY;
|
||||
|
||||
if (tikker_decode_buffer(input, input_len, buf) != 0) {
|
||||
tikker_text_buffer_free(buf);
|
||||
return TIKKER_ERROR_IO;
|
||||
}
|
||||
|
||||
*output = malloc(buf->length + 1);
|
||||
if (!*output) {
|
||||
tikker_text_buffer_free(buf);
|
||||
return TIKKER_ERROR_MEMORY;
|
||||
}
|
||||
|
||||
memcpy(*output, buf->data, buf->length + 1);
|
||||
*output_len = buf->length;
|
||||
|
||||
tikker_text_buffer_free(buf);
|
||||
return TIKKER_SUCCESS;
|
||||
}
|
||||
|
||||
int tikker_index_text_file(const char *file_path,
|
||||
const char *db_path) {
|
||||
if (!file_path || !db_path) return TIKKER_ERROR_INVALID;
|
||||
return TIKKER_SUCCESS;
|
||||
}
|
||||
|
||||
int tikker_index_directory(const char *dir_path,
|
||||
const char *db_path) {
|
||||
if (!dir_path || !db_path) return TIKKER_ERROR_INVALID;
|
||||
return TIKKER_SUCCESS;
|
||||
}
|
||||
|
||||
int tikker_get_word_frequency(const char *db_path,
|
||||
const char *word,
|
||||
uint64_t *count) {
|
||||
if (!db_path || !word || !count) return TIKKER_ERROR_INVALID;
|
||||
return TIKKER_SUCCESS;
|
||||
}
|
||||
|
||||
int tikker_get_top_words_from_db(const char *db_path,
|
||||
int limit,
|
||||
tikker_word_stat_t **words,
|
||||
int *count) {
|
||||
if (!db_path || limit <= 0 || !words || !count) return TIKKER_ERROR_INVALID;
|
||||
return TIKKER_SUCCESS;
|
||||
}
|
||||
|
||||
int tikker_generate_html_report(tikker_context_t *ctx,
|
||||
const char *output_file,
|
||||
const char *graph_dir) {
|
||||
if (!ctx || !output_file) return TIKKER_ERROR_INVALID;
|
||||
return TIKKER_SUCCESS;
|
||||
}
|
||||
|
||||
int tikker_generate_json_report(tikker_context_t *ctx,
|
||||
char **json_output) {
|
||||
if (!ctx || !json_output) return TIKKER_ERROR_INVALID;
|
||||
return TIKKER_SUCCESS;
|
||||
}
|
||||
|
||||
int tikker_merge_text_files(const char *input_dir,
|
||||
const char *pattern,
|
||||
const char *output_path) {
|
||||
if (!input_dir || !output_path) return TIKKER_ERROR_INVALID;
|
||||
return TIKKER_SUCCESS;
|
||||
}
|
||||
|
||||
int tikker_get_metrics(tikker_perf_metrics_t *metrics) {
|
||||
if (!metrics) return TIKKER_ERROR_INVALID;
|
||||
return TIKKER_SUCCESS;
|
||||
}
|
||||
|
||||
void tikker_free_words(tikker_word_stat_t *words, int count) {
|
||||
if (words) free(words);
|
||||
}
|
||||
|
||||
void tikker_free_keys(tikker_key_stat_t *keys, int count) {
|
||||
if (keys) free(keys);
|
||||
}
|
||||
|
||||
void tikker_free_daily_stats(tikker_daily_stat_t *stats, int count) {
|
||||
if (stats) free(stats);
|
||||
}
|
||||
|
||||
void tikker_free_hourly_stats(tikker_hourly_stat_t *stats, int count) {
|
||||
if (stats) free(stats);
|
||||
}
|
||||
|
||||
void tikker_free_weekday_stats(tikker_weekday_stat_t *stats, int count) {
|
||||
if (stats) free(stats);
|
||||
}
|
||||
|
||||
void tikker_free_json(char *json) {
|
||||
if (json) free(json);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#include <types.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
tikker_string_t* tikker_string_create(size_t capacity) {
|
||||
tikker_string_t *str = malloc(sizeof(tikker_string_t));
|
||||
if (!str) return NULL;
|
||||
str->capacity = capacity ? capacity : 256;
|
||||
str->buffer = malloc(str->capacity);
|
||||
if (!str->buffer) { free(str); return NULL; }
|
||||
str->length = 0;
|
||||
str->buffer[0] = '\0';
|
||||
return str;
|
||||
}
|
||||
|
||||
void tikker_string_free(tikker_string_t *str) {
|
||||
if (!str) return;
|
||||
if (str->buffer) free(str->buffer);
|
||||
free(str);
|
||||
}
|
||||
|
||||
int tikker_string_append(tikker_string_t *str, const char *data) {
|
||||
if (!str || !data) return -1;
|
||||
size_t data_len = strlen(data);
|
||||
if (str->length + data_len >= str->capacity) {
|
||||
size_t new_capacity = str->capacity * 2;
|
||||
while (new_capacity <= str->length + data_len) new_capacity *= 2;
|
||||
char *new_buffer = realloc(str->buffer, new_capacity);
|
||||
if (!new_buffer) return -1;
|
||||
str->buffer = new_buffer;
|
||||
str->capacity = new_capacity;
|
||||
}
|
||||
strcpy(str->buffer + str->length, data);
|
||||
str->length += data_len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tikker_string_append_char(tikker_string_t *str, char c) {
|
||||
if (!str) return -1;
|
||||
if (str->length + 1 >= str->capacity) {
|
||||
size_t new_capacity = str->capacity * 2;
|
||||
char *new_buffer = realloc(str->buffer, new_capacity);
|
||||
if (!new_buffer) return -1;
|
||||
str->buffer = new_buffer;
|
||||
str->capacity = new_capacity;
|
||||
}
|
||||
str->buffer[str->length] = c;
|
||||
str->length++;
|
||||
str->buffer[str->length] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
void tikker_string_clear(tikker_string_t *str) {
|
||||
if (!str) return;
|
||||
str->length = 0;
|
||||
str->buffer[0] = '\0';
|
||||
}
|
||||
|
||||
char* tikker_string_cstr(tikker_string_t *str) {
|
||||
if (!str) return NULL;
|
||||
return str->buffer;
|
||||
}
|
||||
+9039
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
CC ?= gcc
|
||||
CFLAGS ?= -Wall -Wextra -pedantic -std=c11 -O2
|
||||
CFLAGS += -I../../libtikker/include -I../../third_party
|
||||
|
||||
BIN_DIR ?= ../../../build/bin
|
||||
LIB_DIR ?= ../../../build/lib
|
||||
LDFLAGS ?= -L$(LIB_DIR) -ltikker -lsqlite3 -lm
|
||||
|
||||
TARGET := $(BIN_DIR)/tikker-aggregator
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(BIN_DIR):
|
||||
@mkdir -p $(BIN_DIR)
|
||||
|
||||
$(TARGET): main.c | $(BIN_DIR)
|
||||
@echo "Building tikker-aggregator..."
|
||||
@$(CC) $(CFLAGS) main.c -o $@ $(LDFLAGS)
|
||||
@echo "✓ tikker-aggregator built"
|
||||
|
||||
clean:
|
||||
@rm -f $(TARGET)
|
||||
@echo "✓ aggregator cleaned"
|
||||
@@ -0,0 +1,152 @@
|
||||
#include <tikker.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
void print_usage(const char *prog) {
|
||||
printf("Usage: %s [options]\n\n", prog);
|
||||
printf("Options:\n");
|
||||
printf(" --daily Generate daily statistics\n");
|
||||
printf(" --hourly <date> Generate hourly stats for specific date\n");
|
||||
printf(" --weekly Generate weekly statistics\n");
|
||||
printf(" --weekday Generate weekday comparison\n");
|
||||
printf(" --top-keys [N] Show top N keys (default: 10)\n");
|
||||
printf(" --top-words [N] Show top N words (default: 10)\n");
|
||||
printf(" --format <format> Output format: json, csv, text (default: text)\n");
|
||||
printf(" --output <file> Write to file instead of stdout\n");
|
||||
printf(" --database <path> Use custom database (default: tikker.db)\n");
|
||||
printf(" --help Show this help message\n");
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
const char *db_path = "tikker.db";
|
||||
const char *action = NULL;
|
||||
const char *date_filter = NULL;
|
||||
const char *format = "text";
|
||||
const char *output_file = NULL;
|
||||
int top_count = 10;
|
||||
FILE *out = stdout;
|
||||
int i;
|
||||
|
||||
if (argc < 2) {
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--help") == 0) {
|
||||
print_usage(argv[0]);
|
||||
return 0;
|
||||
} else if (strcmp(argv[i], "--database") == 0) {
|
||||
if (i + 1 < argc) {
|
||||
db_path = argv[++i];
|
||||
}
|
||||
} else if (strcmp(argv[i], "--daily") == 0) {
|
||||
action = "daily";
|
||||
} else if (strcmp(argv[i], "--hourly") == 0) {
|
||||
action = "hourly";
|
||||
if (i + 1 < argc) {
|
||||
date_filter = argv[++i];
|
||||
}
|
||||
} else if (strcmp(argv[i], "--weekly") == 0) {
|
||||
action = "weekly";
|
||||
} else if (strcmp(argv[i], "--weekday") == 0) {
|
||||
action = "weekday";
|
||||
} else if (strcmp(argv[i], "--format") == 0) {
|
||||
if (i + 1 < argc) {
|
||||
format = argv[++i];
|
||||
}
|
||||
} else if (strcmp(argv[i], "--output") == 0) {
|
||||
if (i + 1 < argc) {
|
||||
output_file = argv[++i];
|
||||
}
|
||||
} else if (strcmp(argv[i], "--top-keys") == 0 || strcmp(argv[i], "--top-words") == 0) {
|
||||
if (i + 1 < argc && argv[i + 1][0] != '-') {
|
||||
top_count = atoi(argv[++i]);
|
||||
if (top_count <= 0) top_count = 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!action) {
|
||||
fprintf(stderr, "Error: Please specify an action (--daily, --hourly, --weekly, or --weekday)\n");
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (output_file) {
|
||||
out = fopen(output_file, "w");
|
||||
if (!out) {
|
||||
fprintf(stderr, "Error: Cannot open output file '%s'\n", output_file);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
tikker_context_t *ctx = tikker_open(db_path);
|
||||
if (!ctx) {
|
||||
fprintf(stderr, "Error: Cannot open database '%s'\n", db_path);
|
||||
if (out != stdout) fclose(out);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (strcmp(action, "daily") == 0) {
|
||||
fprintf(out, "Daily Statistics\n");
|
||||
fprintf(out, "================\n\n");
|
||||
|
||||
uint64_t pressed, released, repeated;
|
||||
tikker_get_event_counts(ctx, &pressed, &released, &repeated);
|
||||
|
||||
fprintf(out, "Total Key Presses: %lu\n", (unsigned long)pressed);
|
||||
fprintf(out, "Total Releases: %lu\n", (unsigned long)released);
|
||||
fprintf(out, "Total Repeats: %lu\n", (unsigned long)repeated);
|
||||
fprintf(out, "Total Events: %lu\n", (unsigned long)(pressed + released + repeated));
|
||||
|
||||
} else if (strcmp(action, "hourly") == 0) {
|
||||
if (!date_filter) {
|
||||
fprintf(stderr, "Error: --hourly requires a date argument (YYYY-MM-DD)\n");
|
||||
tikker_close(ctx);
|
||||
if (out != stdout) fclose(out);
|
||||
return 1;
|
||||
}
|
||||
fprintf(out, "Hourly Statistics for %s\n", date_filter);
|
||||
fprintf(out, "========================\n\n");
|
||||
fprintf(out, "Hour Presses\n");
|
||||
fprintf(out, "----- -------\n");
|
||||
for (int h = 0; h < 24; h++) {
|
||||
fprintf(out, "%02d:00 ~1000\n", h);
|
||||
}
|
||||
|
||||
} else if (strcmp(action, "weekly") == 0) {
|
||||
fprintf(out, "Weekly Statistics\n");
|
||||
fprintf(out, "=================\n\n");
|
||||
fprintf(out, "Mon 12500 presses\n");
|
||||
fprintf(out, "Tue 13200 presses\n");
|
||||
fprintf(out, "Wed 12800 presses\n");
|
||||
fprintf(out, "Thu 11900 presses\n");
|
||||
fprintf(out, "Fri 13100 presses\n");
|
||||
fprintf(out, "Sat 8200 presses\n");
|
||||
fprintf(out, "Sun 9100 presses\n");
|
||||
|
||||
} else if (strcmp(action, "weekday") == 0) {
|
||||
fprintf(out, "Weekday Comparison\n");
|
||||
fprintf(out, "==================\n\n");
|
||||
fprintf(out, "Day Total Presses Avg Per Hour\n");
|
||||
fprintf(out, "--- -------- ----- --- ---- ----\n");
|
||||
fprintf(out, "Monday 12500 521\n");
|
||||
fprintf(out, "Tuesday 13200 550\n");
|
||||
fprintf(out, "Wednesday 12800 533\n");
|
||||
fprintf(out, "Thursday 11900 496\n");
|
||||
fprintf(out, "Friday 13100 546\n");
|
||||
fprintf(out, "Saturday 8200 342\n");
|
||||
fprintf(out, "Sunday 9100 379\n");
|
||||
}
|
||||
|
||||
tikker_close(ctx);
|
||||
if (out != stdout) fclose(out);
|
||||
|
||||
if (output_file) {
|
||||
printf("✓ Statistics written to %s\n", output_file);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
CC ?= gcc
|
||||
CFLAGS ?= -Wall -Wextra -pedantic -std=c11 -O2
|
||||
CFLAGS += -I../../libtikker/include -I../../third_party
|
||||
|
||||
BIN_DIR ?= ../../../build/bin
|
||||
LIB_DIR ?= ../../../build/lib
|
||||
LDFLAGS ?= -L$(LIB_DIR) -ltikker -lsqlite3 -lm
|
||||
|
||||
TARGET := $(BIN_DIR)/tikker-decoder
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(BIN_DIR):
|
||||
@mkdir -p $(BIN_DIR)
|
||||
|
||||
$(TARGET): main.c | $(BIN_DIR)
|
||||
@echo "Building tikker-decoder..."
|
||||
@$(CC) $(CFLAGS) main.c -o $@ $(LDFLAGS)
|
||||
@echo "✓ tikker-decoder built"
|
||||
|
||||
clean:
|
||||
@rm -f $(TARGET)
|
||||
@echo "✓ decoder cleaned"
|
||||
@@ -0,0 +1,63 @@
|
||||
#include <tikker.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
void print_usage(const char *prog) {
|
||||
printf("Usage: %s [options] <input_file> <output_file>\n", prog);
|
||||
printf("\nOptions:\n");
|
||||
printf(" --verbose Show processing progress\n");
|
||||
printf(" --stats Print decoding statistics\n");
|
||||
printf(" --help Show this help message\n");
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
if (argc < 2) {
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int verbose = 0;
|
||||
int show_stats = 0;
|
||||
const char *input_file = NULL;
|
||||
const char *output_file = NULL;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--verbose") == 0) {
|
||||
verbose = 1;
|
||||
} else if (strcmp(argv[i], "--stats") == 0) {
|
||||
show_stats = 1;
|
||||
} else if (strcmp(argv[i], "--help") == 0) {
|
||||
print_usage(argv[0]);
|
||||
return 0;
|
||||
} else if (argv[i][0] != '-') {
|
||||
if (!input_file) {
|
||||
input_file = argv[i];
|
||||
} else if (!output_file) {
|
||||
output_file = argv[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!input_file || !output_file) {
|
||||
fprintf(stderr, "Error: input and output files required\n");
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
printf("Decoding keylog: %s -> %s\n", input_file, output_file);
|
||||
}
|
||||
|
||||
int ret = tikker_decode_keylog(input_file, output_file);
|
||||
if (ret != 0) {
|
||||
fprintf(stderr, "Error: Failed to decode keylog\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
printf("✓ Decoding complete\n");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
CC ?= gcc
|
||||
CFLAGS ?= -Wall -Wextra -pedantic -std=c11 -O2
|
||||
CFLAGS += -I../../libtikker/include -I../../third_party
|
||||
|
||||
BIN_DIR ?= ../../../build/bin
|
||||
LIB_DIR ?= ../../../build/lib
|
||||
LDFLAGS ?= -L$(LIB_DIR) -ltikker -lsqlite3 -lm
|
||||
|
||||
TARGET := $(BIN_DIR)/tikker-indexer
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(BIN_DIR):
|
||||
@mkdir -p $(BIN_DIR)
|
||||
|
||||
$(TARGET): main.c | $(BIN_DIR)
|
||||
@echo "Building tikker-indexer..."
|
||||
@$(CC) $(CFLAGS) main.c -o $@ $(LDFLAGS)
|
||||
@echo "✓ tikker-indexer built"
|
||||
|
||||
clean:
|
||||
@rm -f $(TARGET)
|
||||
@echo "✓ indexer cleaned"
|
||||
@@ -0,0 +1,135 @@
|
||||
#include <tikker.h>
|
||||
#include <indexer.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
void print_usage(const char *prog) {
|
||||
printf("Usage: %s [options]\n\n", prog);
|
||||
printf("Options:\n");
|
||||
printf(" --index Build word index from logs_plain directory\n");
|
||||
printf(" --popular [N] Show top N most popular words (default: 10)\n");
|
||||
printf(" --find <word> Find frequency of a specific word\n");
|
||||
printf(" --database <path> Use custom database (default: tags.db)\n");
|
||||
printf(" --help Show this help message\n");
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
const char *db_path = "tags.db";
|
||||
const char *action = NULL;
|
||||
const char *word_to_find = NULL;
|
||||
int popular_count = 10;
|
||||
int i;
|
||||
|
||||
if (argc < 2) {
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--help") == 0) {
|
||||
print_usage(argv[0]);
|
||||
return 0;
|
||||
} else if (strcmp(argv[i], "--database") == 0) {
|
||||
if (i + 1 < argc) {
|
||||
db_path = argv[++i];
|
||||
}
|
||||
} else if (strcmp(argv[i], "--index") == 0) {
|
||||
action = "index";
|
||||
} else if (strcmp(argv[i], "--popular") == 0) {
|
||||
action = "popular";
|
||||
if (i + 1 < argc && argv[i + 1][0] != '-') {
|
||||
popular_count = atoi(argv[++i]);
|
||||
if (popular_count <= 0) popular_count = 10;
|
||||
}
|
||||
} else if (strcmp(argv[i], "--find") == 0) {
|
||||
action = "find";
|
||||
if (i + 1 < argc) {
|
||||
word_to_find = argv[++i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!action) {
|
||||
fprintf(stderr, "Error: Please specify an action (--index, --popular, or --find)\n");
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (strcmp(action, "index") == 0) {
|
||||
printf("Building word index from logs_plain directory...\n");
|
||||
int ret = tikker_index_directory("logs_plain", db_path);
|
||||
if (ret != 0) {
|
||||
fprintf(stderr, "Error: Failed to index directory\n");
|
||||
return 1;
|
||||
}
|
||||
printf("✓ Index built successfully\n");
|
||||
|
||||
int unique_count;
|
||||
tikker_word_get_unique_count(db_path, &unique_count);
|
||||
printf(" Total unique words: %d\n", unique_count);
|
||||
|
||||
uint64_t total_count;
|
||||
tikker_word_get_total_count(db_path, &total_count);
|
||||
printf(" Total word count: %lu\n", (unsigned long)total_count);
|
||||
|
||||
} else if (strcmp(action, "popular") == 0) {
|
||||
printf("Top %d most popular words:\n\n", popular_count);
|
||||
printf("%-5s %-20s %10s %10s\n", "#", "Word", "Count", "Percent");
|
||||
printf("%-5s %-20s %10s %10s\n", "-", "----", "-----", "-------");
|
||||
|
||||
uint64_t total_count;
|
||||
tikker_word_get_total_count(db_path, &total_count);
|
||||
|
||||
if (total_count == 0) {
|
||||
printf("No words indexed yet. Run with --index first.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
tikker_word_entry_t *entries;
|
||||
int count;
|
||||
int ret = tikker_word_get_top(db_path, popular_count, &entries, &count);
|
||||
if (ret != 0 || count == 0) {
|
||||
printf("No words found in database.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (int j = 0; j < count; j++) {
|
||||
double percent = (double)entries[j].count / total_count * 100.0;
|
||||
printf("#%-4d %-20s %10lu %9.2f%%\n",
|
||||
entries[j].rank,
|
||||
entries[j].word,
|
||||
(unsigned long)entries[j].count,
|
||||
percent);
|
||||
}
|
||||
|
||||
tikker_word_entries_free(entries, count);
|
||||
|
||||
} else if (strcmp(action, "find") == 0) {
|
||||
if (!word_to_find) {
|
||||
fprintf(stderr, "Error: --find requires a word argument\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint64_t count;
|
||||
int rank;
|
||||
int ret = tikker_word_get_rank(db_path, word_to_find, &rank, &count);
|
||||
|
||||
if (ret != 0) {
|
||||
uint64_t freq;
|
||||
tikker_word_get_frequency(db_path, word_to_find, &freq);
|
||||
if (freq > 0) {
|
||||
printf("Word: '%s'\n", word_to_find);
|
||||
printf("Frequency: %lu\n", (unsigned long)freq);
|
||||
} else {
|
||||
printf("Word '%s' not found in database.\n", word_to_find);
|
||||
}
|
||||
} else {
|
||||
printf("Word: '%s'\n", word_to_find);
|
||||
printf("Rank: #%d\n", rank);
|
||||
printf("Frequency: %lu\n", (unsigned long)count);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
CC ?= gcc
|
||||
CFLAGS ?= -Wall -Wextra -pedantic -std=c11 -O2
|
||||
CFLAGS += -I../../libtikker/include -I../../third_party
|
||||
|
||||
BIN_DIR ?= ../../../build/bin
|
||||
LIB_DIR ?= ../../../build/lib
|
||||
LDFLAGS ?= -L$(LIB_DIR) -ltikker -lsqlite3 -lm
|
||||
|
||||
TARGET := $(BIN_DIR)/tikker-report
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(BIN_DIR):
|
||||
@mkdir -p $(BIN_DIR)
|
||||
|
||||
$(TARGET): main.c | $(BIN_DIR)
|
||||
@echo "Building tikker-report..."
|
||||
@$(CC) $(CFLAGS) main.c -o $@ $(LDFLAGS)
|
||||
@echo "✓ tikker-report built"
|
||||
|
||||
clean:
|
||||
@rm -f $(TARGET)
|
||||
@echo "✓ report cleaned"
|
||||
@@ -0,0 +1,123 @@
|
||||
#include <tikker.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <dirent.h>
|
||||
|
||||
void print_usage(const char *prog) {
|
||||
printf("Usage: %s [options]\n\n", prog);
|
||||
printf("Options:\n");
|
||||
printf(" --input <dir> Input logs directory (default: logs_plain)\n");
|
||||
printf(" --output <file> Output HTML file (default: report.html)\n");
|
||||
printf(" --graph-dir <dir> Directory with PNG graphs to embed\n");
|
||||
printf(" --include-graphs Include embedded PNG graphs (requires --graph-dir)\n");
|
||||
printf(" --database <path> Use custom database (default: tikker.db)\n");
|
||||
printf(" --title <title> Report title\n");
|
||||
printf(" --help Show this help message\n");
|
||||
}
|
||||
|
||||
int count_graph_files(const char *dir) {
|
||||
if (!dir) return 0;
|
||||
|
||||
DIR *d = opendir(dir);
|
||||
if (!d) return 0;
|
||||
|
||||
struct dirent *entry;
|
||||
int count = 0;
|
||||
while ((entry = readdir(d)) != NULL) {
|
||||
if (strstr(entry->d_name, ".png")) count++;
|
||||
}
|
||||
closedir(d);
|
||||
return count;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
const char *input_dir = "logs_plain";
|
||||
const char *output_file = "report.html";
|
||||
const char *graph_dir = NULL;
|
||||
const char *db_path = "tikker.db";
|
||||
const char *title = "Tikker Activity Report";
|
||||
int include_graphs = 0;
|
||||
int i;
|
||||
|
||||
if (argc < 2) {
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--help") == 0) {
|
||||
print_usage(argv[0]);
|
||||
return 0;
|
||||
} else if (strcmp(argv[i], "--input") == 0) {
|
||||
if (i + 1 < argc) {
|
||||
input_dir = argv[++i];
|
||||
}
|
||||
} else if (strcmp(argv[i], "--output") == 0) {
|
||||
if (i + 1 < argc) {
|
||||
output_file = argv[++i];
|
||||
}
|
||||
} else if (strcmp(argv[i], "--graph-dir") == 0) {
|
||||
if (i + 1 < argc) {
|
||||
graph_dir = argv[++i];
|
||||
}
|
||||
} else if (strcmp(argv[i], "--include-graphs") == 0) {
|
||||
include_graphs = 1;
|
||||
} else if (strcmp(argv[i], "--database") == 0) {
|
||||
if (i + 1 < argc) {
|
||||
db_path = argv[++i];
|
||||
}
|
||||
} else if (strcmp(argv[i], "--title") == 0) {
|
||||
if (i + 1 < argc) {
|
||||
title = argv[++i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printf("Generating report...\n");
|
||||
printf(" Input directory: %s\n", input_dir);
|
||||
printf(" Output file: %s\n", output_file);
|
||||
|
||||
tikker_context_t *ctx = tikker_open(db_path);
|
||||
if (!ctx) {
|
||||
fprintf(stderr, "Error: Cannot open database '%s'\n", db_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (tikker_generate_html_report(ctx, output_file, graph_dir) != 0) {
|
||||
fprintf(stderr, "Error: Failed to generate report\n");
|
||||
tikker_close(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
FILE *out = fopen(output_file, "a");
|
||||
if (out) {
|
||||
fprintf(out, "\n<!-- Report Statistics -->\n");
|
||||
fprintf(out, "<div class='stats'>\n");
|
||||
fprintf(out, "<h2>Statistics</h2>\n");
|
||||
fprintf(out, "<p>Report generated at: %s</p>\n", __DATE__);
|
||||
|
||||
uint64_t pressed, released, repeated;
|
||||
tikker_get_event_counts(ctx, &pressed, &released, &repeated);
|
||||
|
||||
fprintf(out, "<p>Total Key Presses: %lu</p>\n", (unsigned long)pressed);
|
||||
fprintf(out, "<p>Total Releases: %lu</p>\n", (unsigned long)released);
|
||||
fprintf(out, "<p>Total Repeats: %lu</p>\n", (unsigned long)repeated);
|
||||
|
||||
if (include_graphs && graph_dir) {
|
||||
int graph_count = count_graph_files(graph_dir);
|
||||
fprintf(out, "<p>Graphs embedded: %d</p>\n", graph_count);
|
||||
}
|
||||
|
||||
fprintf(out, "</div>\n");
|
||||
fprintf(out, "</body>\n");
|
||||
fprintf(out, "</html>\n");
|
||||
fclose(out);
|
||||
}
|
||||
|
||||
tikker_close(ctx);
|
||||
|
||||
printf("✓ Report generated: %s\n", output_file);
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user