forensic-pathways/src/utils/gitContributions.ts
2025-08-10 23:00:01 +02:00

378 lines
12 KiB
TypeScript

// src/utils/gitContributions.ts
import { dump } from 'js-yaml';
export interface ContributionData {
type: 'add' | 'edit';
tool: {
name: string;
icon?: string | null;
type: 'software' | 'method' | 'concept';
description: string;
domains: string[];
phases: string[];
platforms: string[];
skillLevel: string;
accessType?: string | null;
url: string;
projectUrl?: string | null;
license?: string | null;
knowledgebase?: boolean | null;
'domain-agnostic-software'?: string[] | null;
related_concepts?: string[] | null;
related_software?: string[] | null;
tags: string[];
statusUrl?: string | null;
};
metadata: {
submitter: string;
reason?: string;
contact?: string;
};
}
export interface GitOperationResult {
success: boolean;
message: string;
issueUrl?: string;
issueNumber?: number;
}
interface KnowledgebaseContribution {
toolName?: string;
title?: string;
description?: string;
content?: string;
externalLink?: string;
difficulty?: string;
categories?: string[];
tags?: string[];
uploadedFiles?: { name: string; url: string }[];
submitter: string;
reason?: string;
contact?: string;
}
interface GitConfig {
provider: 'gitea' | 'github' | 'gitlab';
apiEndpoint: string;
apiToken: string;
repoOwner: string;
repoName: string;
}
export class GitContributionManager {
private config: GitConfig;
constructor() {
const repoUrl = process.env.GIT_REPO_URL || '';
const { owner, name } = this.parseRepoUrl(repoUrl);
this.config = {
provider: (process.env.GIT_PROVIDER as any) || 'gitea',
apiEndpoint: process.env.GIT_API_ENDPOINT || '',
apiToken: process.env.GIT_API_TOKEN || '',
repoOwner: owner,
repoName: name
};
if (!this.config.apiEndpoint || !this.config.apiToken) {
throw new Error('Missing required git configuration');
}
}
private parseRepoUrl(url: string): { owner: string; name: string } {
const match = url.match(/\/([^\/]+)\/([^\/]+?)(?:\.git)?$/);
if (!match) {
throw new Error('Invalid repository URL format');
}
return { owner: match[1], name: match[2] };
}
async submitContribution(data: ContributionData): Promise<GitOperationResult> {
try {
const toolYaml = this.generateYAML(data.tool);
const issueUrl = await this.createIssue(data, toolYaml);
return {
success: true,
message: 'Tool contribution submitted as issue',
issueUrl
};
} catch (error) {
throw new Error(`Issue creation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async submitKnowledgebaseContribution(data: KnowledgebaseContribution): Promise<GitOperationResult> {
try {
const issueUrl = await this.createKnowledgebaseIssue(data);
return {
success: true,
message: 'Knowledge base article submitted as issue',
issueUrl
};
} catch (error) {
throw new Error(`KB issue creation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
private generateYAML(tool: any): string {
const cleanTool: any = {
name: tool.name,
type: tool.type,
description: tool.description,
domains: tool.domains || [],
phases: tool.phases || [],
skillLevel: tool.skillLevel,
url: tool.url
};
if (tool.icon) cleanTool.icon = tool.icon;
if (tool.platforms?.length) cleanTool.platforms = tool.platforms;
if (tool.license) cleanTool.license = tool.license;
if (tool.accessType) cleanTool.accessType = tool.accessType;
if (tool.projectUrl) cleanTool.projectUrl = tool.projectUrl;
if (tool.knowledgebase) cleanTool.knowledgebase = tool.knowledgebase;
if (tool.related_concepts?.length) cleanTool.related_concepts = tool.related_concepts;
if (tool.related_software?.length) cleanTool.related_software = tool.related_software;
if (tool.tags?.length) cleanTool.tags = tool.tags;
if (tool['domain-agnostic-software']?.length) {
cleanTool['domain-agnostic-software'] = tool['domain-agnostic-software'];
}
return dump(cleanTool, {
lineWidth: -1,
noRefs: true,
quotingType: '"',
forceQuotes: false,
indent: 2
}).trim();
}
private async createIssue(data: ContributionData, toolYaml: string): Promise<string> {
const title = `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}`;
const body = this.generateIssueBody(data, toolYaml);
let apiUrl: string;
let requestBody: any;
switch (this.config.provider) {
case 'gitea':
apiUrl = `${this.config.apiEndpoint}/repos/${this.config.repoOwner}/${this.config.repoName}/issues`;
requestBody = { title, body };
break;
case 'github':
apiUrl = `${this.config.apiEndpoint}/repos/${this.config.repoOwner}/${this.config.repoName}/issues`;
requestBody = { title, body };
break;
case 'gitlab':
apiUrl = `${this.config.apiEndpoint}/projects/${encodeURIComponent(this.config.repoOwner + '/' + this.config.repoName)}/issues`;
requestBody = { title, description: body };
break;
default:
throw new Error(`Unsupported git provider: ${this.config.provider}`);
}
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.config.apiToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const issueData = await response.json();
switch (this.config.provider) {
case 'gitea':
case 'github':
return issueData.html_url || issueData.url;
case 'gitlab':
return issueData.web_url;
default:
throw new Error('Unknown provider response format');
}
}
private async createKnowledgebaseIssue(data: KnowledgebaseContribution): Promise<string> {
const title = `Knowledge Base: ${data.title || data.toolName || 'New Article'}`;
const body = this.generateKnowledgebaseIssueBody(data);
let apiUrl: string;
let requestBody: any;
switch (this.config.provider) {
case 'gitea':
apiUrl = `${this.config.apiEndpoint}/repos/${this.config.repoOwner}/${this.config.repoName}/issues`;
requestBody = { title, body };
break;
case 'github':
apiUrl = `${this.config.apiEndpoint}/repos/${this.config.repoOwner}/${this.config.repoName}/issues`;
requestBody = { title, body };
break;
case 'gitlab':
apiUrl = `${this.config.apiEndpoint}/projects/${encodeURIComponent(this.config.repoOwner + '/' + this.config.repoName)}/issues`;
requestBody = { title, description: body };
break;
default:
throw new Error(`Unsupported git provider: ${this.config.provider}`);
}
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.config.apiToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const issueData = await response.json();
switch (this.config.provider) {
case 'gitea':
case 'github':
return issueData.html_url || issueData.url;
case 'gitlab':
return issueData.web_url;
default:
throw new Error('Unknown provider response format');
}
}
private generateIssueBody(data: ContributionData, toolYaml: string): string {
return `## ${data.type === 'add' ? 'Add' : 'Update'} Tool: ${data.tool.name}
**Submitted by:** ${data.metadata.submitter}
**Type:** ${data.tool.type}
**Action:** ${data.type}
### Tool Information
- **Name:** ${data.tool.name}
- **Description:** ${data.tool.description}
- **URL:** ${data.tool.url}
- **Skill Level:** ${data.tool.skillLevel}
${data.tool.platforms?.length ? `- **Platforms:** ${data.tool.platforms.join(', ')}` : ''}
${data.tool.license ? `- **License:** ${data.tool.license}` : ''}
${data.tool.domains?.length ? `- **Domains:** ${data.tool.domains.join(', ')}` : ''}
${data.tool.phases?.length ? `- **Phases:** ${data.tool.phases.join(', ')}` : ''}
${data.tool.related_concepts?.length ? `- **Related Concepts:** ${data.tool.related_concepts.join(', ')}` : ''}
${data.tool.related_software?.length ? `- **Related Software:** ${data.tool.related_software.join(', ')}` : ''}
${data.metadata.reason ? `### Reason
${data.metadata.reason}
` : ''}${data.metadata.contact ? `### Contact
${data.metadata.contact}
` : ''}### Copy-Paste YAML
\`\`\`yaml
- ${toolYaml.split('\n').join('\n ')}
\`\`\`
### For Maintainers
1. Copy the YAML above
2. Add to \`src/data/tools.yaml\` in the tools array
3. Maintain alphabetical order
4. Close this issue when done
---
*Submitted via ForensicPathways contribution form*`;
}
private generateKnowledgebaseIssueBody(data: KnowledgebaseContribution): string {
const sections: string[] = [];
sections.push(`## Knowledge Base Article: ${data.title ?? 'Untitled'}`);
sections.push('');
sections.push(`**Submitted by:** ${data.submitter}`);
if (data.contact) sections.push(`**Contact:** ${data.contact}`);
if (data.toolName) sections.push(`**Related Tool:** ${data.toolName}`);
if (data.difficulty) sections.push(`**Difficulty:** ${data.difficulty}`);
sections.push('');
if (data.description) {
sections.push('### Description');
sections.push(data.description);
sections.push('');
}
if (data.content) {
sections.push('### Article Content');
sections.push('```markdown');
sections.push(data.content);
sections.push('```');
sections.push('');
}
if (data.externalLink) {
sections.push('### External Resource');
sections.push(`- [External Documentation](${data.externalLink})`);
sections.push('');
}
if (Array.isArray(data.uploadedFiles) && data.uploadedFiles.length) {
sections.push('### Uploaded Files');
data.uploadedFiles.forEach((file) => {
const fileType = file.name.toLowerCase();
let icon = '📎';
if (fileType.endsWith('.pdf')) icon = '📄';
else if (/(png|jpe?g|gif|webp)$/.test(fileType)) icon = '🖼️';
else if (/(mp4|webm|mov|avi)$/.test(fileType)) icon = '🎥';
else if (/(docx?)$/.test(fileType)) icon = '📝';
else if (/(zip|tar|gz)$/.test(fileType)) icon = '📦';
sections.push(`- ${icon} [${file.name}](${file.url})`);
});
sections.push('');
}
const hasCategories = Array.isArray(data.categories) && data.categories.length > 0;
const hasTags = Array.isArray(data.tags) && data.tags.length > 0;
if (hasCategories || hasTags) {
sections.push('### Metadata');
if (hasCategories) sections.push(`**Categories:** ${data.categories!.join(', ')}`);
if (hasTags) sections.push(`**Tags:** ${data.tags!.join(', ')}`);
sections.push('');
}
if (data.reason) {
sections.push('### Reason for Contribution');
sections.push(data.reason);
sections.push('');
}
sections.push('### For Maintainers');
sections.push('1. Review the content for quality and accuracy');
sections.push('2. Create the appropriate markdown file in `src/content/knowledgebase/`');
sections.push('3. Process any uploaded files as needed');
sections.push('4. Close this issue when the article is published');
sections.push('');
sections.push('---');
sections.push('*Submitted via ForensicPathways knowledge base contribution form*');
return sections.join('\n');
}
}