Files
openclaw-taskboard/gitea-integration.js

255 lines
6.3 KiB
JavaScript

/**
* Gitea Integration for AgentDash
* Provides real-time data from Gitea API
*/
const https = require('https');
const http = require('http');
class GiteaIntegration {
constructor(config = {}) {
this.baseUrl = config.baseUrl || 'https://gitea.tophermayor.com';
this.token = config.token || process.env.GITEA_TOKEN;
this.owner = config.owner || 'TopherMayor';
this.cache = new Map();
this.cacheTimeout = config.cacheTimeout || 30000; // 30 seconds
}
/**
* Make authenticated request to Gitea API
*/
async request(endpoint) {
return new Promise((resolve, reject) => {
const url = new URL(endpoint, this.baseUrl);
const client = url.protocol === 'https:' ? https : http;
const options = {
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname + url.search,
method: 'GET',
headers: {
'Authorization': `token ${this.token}`,
'Accept': 'application/json'
}
};
const req = client.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(new Error(`Failed to parse response: ${e.message}`));
}
});
});
req.on('error', reject);
req.setTimeout(10000, () => {
req.destroy();
reject(new Error('Request timeout'));
});
req.end();
});
}
/**
* Get cached data or fetch fresh
*/
async getCached(key, fetcher) {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
return cached.data;
}
const data = await fetcher();
this.cache.set(key, { data, timestamp: Date.now() });
return data;
}
/**
* Get all repositories for the owner
*/
async getRepos() {
return this.getCached('repos', () =>
this.request(`/api/v1/user/repos?limit=50`)
);
}
/**
* Get repository details
*/
async getRepo(repoName) {
return this.getCached(`repo:${repoName}`, () =>
this.request(`/api/v1/repos/${this.owner}/${repoName}`)
);
}
/**
* Get open pull requests for a repository
*/
async getPullRequests(repoName, state = 'open') {
return this.getCached(`prs:${repoName}:${state}`, () =>
this.request(`/api/v1/repos/${this.owner}/${repoName}/pulls?state=${state}&limit=20`)
);
}
/**
* Get issues for a repository
*/
async getIssues(repoName, state = 'open') {
return this.getCached(`issues:${repoName}:${state}`, () =>
this.request(`/api/v1/repos/${this.owner}/${repoName}/issues?state=${state}&limit=20`)
);
}
/**
* Get recent commits for a repository
*/
async getCommits(repoName, branch = 'main', limit = 10) {
return this.getCached(`commits:${repoName}:${branch}`, () =>
this.request(`/api/v1/repos/${this.owner}/${repoName}/commits?sha=${branch}&limit=${limit}`)
);
}
/**
* Get branches for a repository
*/
async getBranches(repoName) {
return this.getCached(`branches:${repoName}`, () =>
this.request(`/api/v1/repos/${this.owner}/${repoName}/branches`)
);
}
/**
* Get repository activity feed
*/
async getActivity(repoName) {
return this.getCached(`activity:${repoName}`, () =>
this.request(`/api/v1/repos/${this.owner}/${repoName}/activities/feeds?limit=20`)
);
}
/**
* Get user info
*/
async getUser() {
return this.getCached('user', () =>
this.request('/api/v1/user')
);
}
/**
* Get organization info (if applicable)
*/
async getOrgs() {
return this.getCached('orgs', () =>
this.request('/api/v1/user/orgs')
);
}
/**
* Clear cache
*/
clearCache() {
this.cache.clear();
}
/**
* Get swarm summary - all repos with PRs and issues
*/
async getSwarmSummary() {
const repos = await this.getRepos();
const summary = [];
for (const repo of repos.slice(0, 10)) { // Limit to 10 repos
try {
const [prs, issues, branches] = await Promise.all([
this.getPullRequests(repo.name, 'open').catch(() => []),
this.getIssues(repo.name, 'open').catch(() => []),
this.getBranches(repo.name).catch(() => [])
]);
summary.push({
name: repo.name,
full_name: repo.full_name,
stars: repo.stars_count || 0,
forks: repo.forks_count || 0,
open_prs: prs.length,
open_issues: issues.length,
branches: branches.length,
updated_at: repo.updated_at,
html_url: repo.html_url
});
} catch (e) {
console.error(`Error fetching data for ${repo.name}:`, e.message);
}
}
return summary;
}
/**
* Get pending reviews (PRs needing attention)
*/
async getPendingReviews() {
const repos = await this.getRepos();
const pending = [];
for (const repo of repos.slice(0, 10)) {
try {
const prs = await this.getPullRequests(repo.name, 'open');
for (const pr of prs) {
pending.push({
repo: repo.name,
repo_url: repo.html_url,
pr_number: pr.number,
pr_title: pr.title,
pr_url: pr.html_url,
author: pr.user?.login,
created_at: pr.created_at,
mergeable: pr.mergeable,
draft: pr.draft,
labels: pr.labels || []
});
}
} catch (e) {
console.error(`Error fetching PRs for ${repo.name}:`, e.message);
}
}
return pending;
}
/**
* Get recent activity across all repos
*/
async getRecentActivity() {
const repos = await this.getRepos();
const activities = [];
for (const repo of repos.slice(0, 5)) {
try {
const activity = await this.getActivity(repo.name);
for (const act of (activity || []).slice(0, 5)) {
activities.push({
repo: repo.name,
repo_url: repo.html_url,
...act
});
}
} catch (e) {
// Skip repos without activity access
}
}
return activities
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
.slice(0, 20);
}
}
module.exports = GiteaIntegration;