Skip to content

Commit c374591

Browse files
committed
Enhance DigestCron to support Bitbucket repositories and ther contributors
1 parent 5d57781 commit c374591

File tree

2 files changed

+189
-50
lines changed

2 files changed

+189
-50
lines changed

api/src/digest/cron.ts

Lines changed: 188 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,29 @@ import { Service } from "typedi";
1616
import { TagRepository } from "src/tag/repository";
1717
import { AIService } from "src/ai/service";
1818
import { AIResponseTranslateNameDto, AIResponseTranslateTitleDto } from "./dto";
19+
import { DataProjectEntity } from "src/data/types";
20+
import { RepositoryEntity } from "@dzcode.io/models/dist/repository";
21+
import { BitbucketService } from "src/bitbucket/service";
22+
23+
type RepoInfo = Pick<RepositoryEntity, "id" | "name" | "owner" | "provider" | "stars">;
24+
interface RepoContributor {
25+
id: string;
26+
name: string;
27+
username: string;
28+
url: string;
29+
avatarUrl: string;
30+
contributions: number;
31+
}
1932

33+
interface RepoContribution {
34+
user: RepoContributor;
35+
type: "PULL_REQUEST" | "ISSUE";
36+
title: string;
37+
updatedAt: string;
38+
activityCount: number;
39+
url: string;
40+
id: string;
41+
}
2042
@Service()
2143
export class DigestCron {
2244
private readonly schedule = "15 * * * *";
@@ -33,6 +55,7 @@ export class DigestCron {
3355
private readonly searchService: SearchService,
3456
private readonly tagRepository: TagRepository,
3557
private readonly aiService: AIService,
58+
private readonly bitbucketService: BitbucketService,
3659
) {
3760
const SentryCronJob = cron.instrumentCron(CronJob, "DigestCron");
3861
new SentryCronJob(
@@ -79,7 +102,7 @@ export class DigestCron {
79102
// todo-ZM: make this configurable
80103
// uncomment during development
81104
// const projectsFromDataFolder = (await this.dataService.listProjects()).filter((p) =>
82-
// ["dzcode.io website", "Mishkal", "System Monitor"].includes(p.name),
105+
// ["Open-listings", "dzcode.io website", "Mishkal", "System Monitor"].includes(p.name),
83106
// );
84107
// or uncomment to skip the cron
85108
// if (Math.random()) return;
@@ -128,36 +151,22 @@ it may contain non-translatable parts like acronyms, keep them as is.`;
128151
const repositoriesFromDataFolder = project.repositories;
129152
for (const repository of repositoriesFromDataFolder) {
130153
try {
131-
const repoInfo = await this.githubService.getRepository({
132-
owner: repository.owner,
133-
repo: repository.name,
134-
});
154+
const provider = repository.provider;
155+
const repoInfo = await this.getRepoInfo(repository);
135156

136-
const provider = "github";
137157
const [{ id: repositoryId }] = await this.repositoriesRepository.upsert({
158+
...repoInfo,
138159
provider,
139-
name: repoInfo.name,
140-
owner: repoInfo.owner.login,
141160
runId,
142161
projectId,
143-
stars: repoInfo.stargazers_count,
144162
id: `${provider}-${repoInfo.id}`,
145163
});
146164
addedRepositoryCount++;
147165

148-
const issues = await this.githubService.listRepositoryIssues({
149-
owner: repository.owner,
150-
repo: repository.name,
151-
});
152-
153-
for (const issue of issues) {
154-
const githubUser = await this.githubService.getUser({
155-
username: issue.user.login,
156-
});
157-
158-
if (githubUser.type !== "User") continue;
166+
const repoContributions = await this.getRepoContributions(repository);
159167

160-
let name_en = githubUser.name || githubUser.login;
168+
for (const repoContribution of repoContributions) {
169+
let name_en = repoContribution.user.name;
161170
let name_ar = name_en;
162171
try {
163172
const aiRes = await this.aiService.query(
@@ -177,27 +186,28 @@ it may contain non-translatable parts like acronyms, keep them as is.`;
177186
const contributorEntity: ContributorRow = {
178187
name_en,
179188
name_ar,
180-
username: githubUser.login,
181-
url: githubUser.html_url,
182-
avatarUrl: githubUser.avatar_url,
189+
username: repoContribution.user.username,
190+
url: repoContribution.user.url,
191+
avatarUrl: repoContribution.user.avatarUrl,
183192
runId,
184-
id: `${provider}-${githubUser.login}`,
193+
id: `${provider}-${repoContribution.user.username}`,
185194
};
186195

187196
const [{ id: contributorId }] =
188197
await this.contributorsRepository.upsert(contributorEntity);
189198
await this.searchService.upsert("contributor", contributorEntity);
190199

200+
// todo-zm: insert instead, and allow duplicates, and update the score calculation
191201
await this.contributorsRepository.upsertRelationWithRepository({
192202
contributorId,
193203
repositoryId,
194204
runId,
195205
score: 1,
196206
});
197207

198-
const type = issue.pull_request ? "PULL_REQUEST" : "ISSUE";
208+
const type = repoContribution.type;
199209

200-
let title_en = issue.title;
210+
let title_en = repoContribution.title;
201211
let title_ar = `ar ${title_en}`;
202212
try {
203213
const aiRes = await this.aiService.query(
@@ -218,33 +228,22 @@ it may contain non-translatable parts like acronyms, keep them as is.`;
218228
title_en,
219229
title_ar,
220230
type,
221-
updatedAt: issue.updated_at,
222-
activityCount: issue.comments,
231+
updatedAt: repoContribution.updatedAt,
232+
activityCount: repoContribution.activityCount,
223233
runId,
224-
url: type === "PULL_REQUEST" ? issue.pull_request.html_url : issue.html_url,
234+
url: repoContribution.url,
225235
repositoryId,
226236
contributorId,
227-
id: `${provider}-${issue.id}`,
237+
id: `${provider}-${repoContribution.id}`,
228238
};
229239
await this.contributionsRepository.upsert(contributionEntity);
230240
await this.searchService.upsert("contribution", contributionEntity);
231241
}
232242

233-
const repoContributors = await this.githubService.listRepositoryContributors({
234-
owner: repository.owner,
235-
repository: repository.name,
236-
});
237-
238-
const repoContributorsFiltered = repoContributors.filter(
239-
(contributor) => contributor.type === "User",
240-
);
241-
242-
for (const repoContributor of repoContributorsFiltered) {
243-
const contributor = await this.githubService.getUser({
244-
username: repoContributor.login,
245-
});
243+
const repoContributors = await this.getRepoContributors(repository);
246244

247-
let name_en = contributor.name || contributor.login;
245+
for (const repoContributor of repoContributors) {
246+
let name_en = repoContributor.name;
248247
let name_ar = `ar ${name_en}`;
249248
try {
250249
const aiRes = await this.aiService.query(
@@ -264,16 +263,17 @@ it may contain non-translatable parts like acronyms, keep them as is.`;
264263
const contributorEntity: ContributorRow = {
265264
name_en,
266265
name_ar,
267-
username: contributor.login,
268-
url: contributor.html_url,
269-
avatarUrl: contributor.avatar_url,
266+
username: repoContributor.username,
267+
url: repoContributor.url,
268+
avatarUrl: repoContributor.avatarUrl,
270269
runId,
271-
id: `${provider}-${contributor.login}`,
270+
id: `${provider}-${repoContributor.id}`,
272271
};
273272
const [{ id: contributorId }] =
274273
await this.contributorsRepository.upsert(contributorEntity);
275274
await this.searchService.upsert("contributor", contributorEntity);
276275

276+
// todo-zm: insert instead, and allow duplicates, and update the score calculation
277277
await this.contributorsRepository.upsertRelationWithRepository({
278278
contributorId,
279279
repositoryId,
@@ -320,4 +320,143 @@ it may contain non-translatable parts like acronyms, keep them as is.`;
320320

321321
this.logger.info({ message: `Digest cron finished, runId: ${runId}` });
322322
}
323+
324+
private async getRepoInfo(
325+
reposotory: DataProjectEntity["repositories"][number],
326+
): Promise<RepoInfo> {
327+
switch (reposotory.provider) {
328+
case "github": {
329+
const repoInfo = await this.githubService.getRepository({
330+
owner: reposotory.owner,
331+
repo: reposotory.name,
332+
});
333+
return {
334+
id: `${repoInfo.id}`,
335+
name: repoInfo.name,
336+
owner: repoInfo.owner.login,
337+
provider: reposotory.provider,
338+
stars: repoInfo.stargazers_count,
339+
};
340+
}
341+
342+
case "bitbucket": {
343+
const repoInfo = await this.bitbucketService.getRepository({
344+
owner: reposotory.owner,
345+
repo: reposotory.name,
346+
});
347+
return {
348+
id: `${repoInfo.owner.username}-${repoInfo.slug}`,
349+
name: repoInfo.name,
350+
owner: reposotory.owner,
351+
provider: reposotory.provider,
352+
stars: 0, // Bitbucket API doesn't provide stars count
353+
};
354+
}
355+
356+
default:
357+
throw new Error(`Unsupported provider: ${reposotory.provider}`);
358+
}
359+
}
360+
361+
private async getRepoContributors(
362+
reposotory: DataProjectEntity["repositories"][number],
363+
): Promise<RepoContributor[]> {
364+
switch (reposotory.provider) {
365+
case "github": {
366+
const repoContributors = await this.githubService.listRepositoryContributors({
367+
owner: reposotory.owner,
368+
repository: reposotory.name,
369+
});
370+
const r = await Promise.all(
371+
repoContributors
372+
.filter(({ type }) => type === "User")
373+
.map(async (contributor) => {
374+
const userInfo = await this.githubService.getUser({ username: contributor.login });
375+
return {
376+
id: contributor.login,
377+
name: userInfo.name,
378+
avatarUrl: contributor.avatar_url,
379+
url: contributor.html_url,
380+
username: contributor.login,
381+
contributions: contributor.contributions,
382+
};
383+
}),
384+
);
385+
386+
return r;
387+
}
388+
389+
case "bitbucket": {
390+
const repoContributors = await this.bitbucketService.listRepositoryContributors({
391+
owner: reposotory.owner,
392+
repo: reposotory.name,
393+
});
394+
395+
return repoContributors
396+
.filter(({ type }) => ["user"].includes(type))
397+
.map((contributor) => ({
398+
id: contributor.uuid,
399+
name: contributor.display_name,
400+
avatarUrl: contributor.links.avatar.href,
401+
url: "#", // Bitbucket API doesn't provide user URL
402+
username: contributor.username || contributor.display_name.replace(/ /g, "-"),
403+
contributions: contributor.contributions,
404+
}));
405+
}
406+
407+
default:
408+
throw new Error(`Unsupported provider: ${reposotory.provider}`);
409+
}
410+
}
411+
412+
private async getRepoContributions(
413+
reposotory: DataProjectEntity["repositories"][number],
414+
): Promise<RepoContribution[]> {
415+
switch (reposotory.provider) {
416+
case "github": {
417+
const repoContributions = await this.githubService.listRepositoryIssues({
418+
owner: reposotory.owner,
419+
repo: reposotory.name,
420+
});
421+
return (
422+
await Promise.all(
423+
repoContributions.map(async (contribution) => {
424+
const githubUser = await this.githubService.getUser({
425+
username: contribution.user.login,
426+
});
427+
428+
if (githubUser.type !== "User") return null;
429+
430+
return {
431+
user: {
432+
id: githubUser.login,
433+
name: githubUser.name,
434+
avatarUrl: githubUser.avatar_url,
435+
url: githubUser.html_url,
436+
username: githubUser.login,
437+
contributions: 1,
438+
},
439+
type: contribution.pull_request ? "PULL_REQUEST" : "ISSUE",
440+
title: contribution.title,
441+
updatedAt: contribution.updated_at,
442+
activityCount: contribution.comments,
443+
url: contribution.pull_request
444+
? contribution.pull_request.html_url
445+
: contribution.html_url,
446+
id: `${reposotory.provider}-${contribution.id}`,
447+
};
448+
}),
449+
)
450+
).filter(Boolean) as RepoContribution[];
451+
}
452+
453+
case "bitbucket": {
454+
// todo-ZM: fetch PRs and issues from Bitbucket
455+
return [];
456+
}
457+
458+
default:
459+
throw new Error(`Unsupported provider: ${reposotory.provider}`);
460+
}
461+
}
323462
}

api/src/github/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { GeneralResponse } from "src/app/types";
22

33
interface GithubUser {
44
login: string;
5-
name: string | null;
5+
name: string;
66
html_url: string;
77
avatar_url: string;
88
type: "User" | "_other";

0 commit comments

Comments
 (0)