@@ -16,7 +16,29 @@ import { Service } from "typedi";
1616import { TagRepository } from "src/tag/repository" ;
1717import { AIService } from "src/ai/service" ;
1818import { 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 ( )
2143export 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}
0 commit comments