378 lines
12 KiB
TypeScript
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');
|
|
}
|
|
} |