1- import { ISSUE_QUERY , IssueQueryResponse , PULL_REQUEST_QUERY , PullRequestQueryResponse , GraphQLPullRequest , GraphQLIssue } from "../api /queries" ;
1+ import { ISSUE_QUERY , IssueQueryResponse , PULL_REQUEST_QUERY , PullRequestQueryResponse , GraphQLPullRequest , GraphQLIssue , GraphQLIssueCommentNode } from "./queries" ;
22import { Octokits } from "./client" ;
33import { executeWithRetry } from "../../utils/retry" ;
44import {
55 filterCommentsToTriggerTime ,
66 filterReviewsToTriggerTime ,
77 isBodySafeToUse
88} from "./time-filter" ;
9+ import { downloadAttachmentsFromHtml , replaceAttachmentsInText } from "../junie/attachment-downloader" ;
10+
11+ /**
12+ * Process timeline comments: download attachments and replace URLs
13+ */
14+ async function processTimelineComments (
15+ octokit : Octokits ,
16+ owner : string ,
17+ repo : string ,
18+ comments : GraphQLIssueCommentNode [ ]
19+ ) : Promise < GraphQLIssueCommentNode [ ] > {
20+ return Promise . all (
21+ comments . map ( async ( comment ) => {
22+ if ( ! comment . body ) return comment ;
23+
24+ try {
25+ // Get HTML version of comment
26+ const commentResponse = await octokit . rest . issues . getComment ( {
27+ owner,
28+ repo,
29+ comment_id : comment . databaseId ,
30+ mediaType : { format : "full+json" } ,
31+ } ) ;
32+ const bodyHtml = commentResponse . data . body_html ;
33+
34+ if ( bodyHtml ) {
35+ const commentAttachments = await downloadAttachmentsFromHtml ( bodyHtml ) ;
36+ return {
37+ ...comment ,
38+ body : commentAttachments . size > 0
39+ ? replaceAttachmentsInText ( comment . body , commentAttachments )
40+ : comment . body
41+ } ;
42+ }
43+ } catch ( error ) {
44+ console . error ( `Failed to process comment attachments:` , error ) ;
45+ }
46+
47+ return comment ;
48+ } )
49+ ) ;
50+ }
951
1052/**
1153 * GraphQL-based data fetcher - fetches all data in a single request
@@ -43,47 +85,142 @@ export class GraphQLGitHubDataFetcher {
4385
4486 const pr = response . repository . pullRequest ;
4587
46- // Filter timeline comments to trigger time
88+ // Check if body is safe to use
89+ const bodyIsSafe = isBodySafeToUse ( pr , triggerTime ) ;
90+ if ( ! bodyIsSafe ) {
91+ console . warn (
92+ `Security: PR #${ pullNumber } body was edited after the trigger event. ` +
93+ `Excluding body content to prevent potential injection attacks.`
94+ ) ;
95+ }
96+
97+ // Process PR body: download attachments and replace URLs
98+ let processedBody = "" ;
99+ if ( bodyIsSafe && pr . body ) {
100+ try {
101+ const prResponse = await this . octokit . rest . pulls . get ( {
102+ owner,
103+ repo,
104+ pull_number : pullNumber ,
105+ mediaType : { format : "full+json" } ,
106+ } ) ;
107+ const bodyHtml = ( prResponse . data as any ) . body_html ;
108+ if ( bodyHtml ) {
109+ const bodyAttachments = await downloadAttachmentsFromHtml ( bodyHtml ) ;
110+ processedBody = bodyAttachments . size > 0
111+ ? replaceAttachmentsInText ( pr . body , bodyAttachments )
112+ : pr . body ;
113+ } else {
114+ processedBody = pr . body ;
115+ }
116+ } catch ( error ) {
117+ console . error ( `Failed to process PR body attachments:` , error ) ;
118+ processedBody = pr . body ;
119+ }
120+ }
121+
122+ // Filter and process timeline comments
47123 const filteredTimelineNodes = filterCommentsToTriggerTime (
48124 pr . timelineItems . nodes ,
49125 triggerTime
50126 ) ;
51127
52- // Filter reviews to trigger time
128+ const processedTimelineNodes = await processTimelineComments (
129+ this . octokit ,
130+ owner ,
131+ repo ,
132+ filteredTimelineNodes
133+ ) ;
134+
135+ // Filter reviews
53136 const filteredReviews = filterReviewsToTriggerTime (
54137 pr . reviews . nodes ,
55138 triggerTime
56139 ) ;
57140
58- // Filter review comments within each review
59- const reviewsWithFilteredComments = filteredReviews . map ( review => ( {
60- ... review ,
61- comments : {
62- nodes : filterCommentsToTriggerTime (
141+ // Process each review and its comments
142+ const processedReviews = await Promise . all (
143+ filteredReviews . map ( async ( review ) => {
144+ // Filter review comments
145+ const filteredReviewComments = filterCommentsToTriggerTime (
63146 review . comments . nodes ,
64147 triggerTime
65- )
66- }
67- } ) ) ;
148+ ) ;
68149
69- // Check if body is safe to use
70- const bodyIsSafe = isBodySafeToUse ( pr , triggerTime ) ;
71- if ( ! bodyIsSafe ) {
72- console . warn (
73- `Security: PR #${ pullNumber } body was edited after the trigger event. ` +
74- `Excluding body content to prevent potential injection attacks.`
75- ) ;
76- }
150+ // Process review body
151+ let processedReviewBody = review . body ;
152+ if ( review . body ) {
153+ try {
154+ const reviewResponse = await this . octokit . rest . pulls . getReview ( {
155+ owner,
156+ repo,
157+ pull_number : pullNumber ,
158+ review_id : review . databaseId ,
159+ mediaType : { format : "full+json" } ,
160+ } ) ;
161+ const bodyHtml = reviewResponse . data . body_html ;
162+
163+ if ( bodyHtml ) {
164+ const reviewAttachments = await downloadAttachmentsFromHtml ( bodyHtml ) ;
165+ processedReviewBody = reviewAttachments . size > 0
166+ ? replaceAttachmentsInText ( review . body , reviewAttachments )
167+ : review . body ;
168+ }
169+ } catch ( error ) {
170+ console . error ( `Failed to process review body attachments:` , error ) ;
171+ }
172+ }
173+
174+ // Process each review comment
175+ const processedReviewComments = await Promise . all (
176+ filteredReviewComments . map ( async ( comment ) => {
177+ if ( ! comment . body ) return comment ;
178+
179+ try {
180+ const commentResponse = await this . octokit . rest . pulls . getReviewComment ( {
181+ owner,
182+ repo,
183+ comment_id : comment . databaseId ,
184+ mediaType : { format : "full+json" } ,
185+ } ) ;
186+ const bodyHtml = commentResponse . data . body_html ;
187+
188+ if ( bodyHtml ) {
189+ const commentAttachments = await downloadAttachmentsFromHtml ( bodyHtml ) ;
190+ return {
191+ ...comment ,
192+ body : commentAttachments . size > 0
193+ ? replaceAttachmentsInText ( comment . body , commentAttachments )
194+ : comment . body
195+ } ;
196+ }
197+ } catch ( error ) {
198+ console . error ( `Failed to process review comment attachments:` , error ) ;
199+ }
200+
201+ return comment ;
202+ } )
203+ ) ;
204+
205+ return {
206+ ...review ,
207+ body : processedReviewBody ,
208+ comments : {
209+ nodes : processedReviewComments
210+ }
211+ } ;
212+ } )
213+ ) ;
77214
78215 // Create filtered PR object
79216 const filteredPR : GraphQLPullRequest = {
80217 ...pr ,
81- body : bodyIsSafe ? pr . body : "" ,
218+ body : processedBody ,
82219 timelineItems : {
83- nodes : filteredTimelineNodes
220+ nodes : processedTimelineNodes
84221 } ,
85222 reviews : {
86- nodes : reviewsWithFilteredComments
223+ nodes : processedReviews
87224 }
88225 } ;
89226
@@ -107,12 +244,6 @@ export class GraphQLGitHubDataFetcher {
107244
108245 const issue = response . repository . issue ;
109246
110- // Filter timeline comments to trigger time
111- const filteredTimelineNodes = filterCommentsToTriggerTime (
112- issue . timelineItems . nodes ,
113- triggerTime
114- ) ;
115-
116247 // Check if body is safe to use
117248 const bodyIsSafe = isBodySafeToUse ( issue , triggerTime ) ;
118249 if ( ! bodyIsSafe ) {
@@ -122,12 +253,51 @@ export class GraphQLGitHubDataFetcher {
122253 ) ;
123254 }
124255
256+ // Process issue body: download attachments and replace URLs
257+ let processedBody = "" ;
258+ if ( bodyIsSafe && issue . body ) {
259+ try {
260+ const issueResponse = await this . octokit . rest . issues . get ( {
261+ owner,
262+ repo,
263+ issue_number : issueNumber ,
264+ mediaType : { format : "full+json" } ,
265+ } ) ;
266+ const bodyHtml = issueResponse . data . body_html ;
267+
268+ if ( bodyHtml ) {
269+ const bodyAttachments = await downloadAttachmentsFromHtml ( bodyHtml ) ;
270+ processedBody = bodyAttachments . size > 0
271+ ? replaceAttachmentsInText ( issue . body , bodyAttachments )
272+ : issue . body ;
273+ } else {
274+ processedBody = issue . body ;
275+ }
276+ } catch ( error ) {
277+ console . error ( `Failed to process issue body attachments:` , error ) ;
278+ processedBody = issue . body ;
279+ }
280+ }
281+
282+ // Filter and process timeline comments
283+ const filteredTimelineNodes = filterCommentsToTriggerTime (
284+ issue . timelineItems . nodes ,
285+ triggerTime
286+ ) ;
287+
288+ const processedTimelineNodes = await processTimelineComments (
289+ this . octokit ,
290+ owner ,
291+ repo ,
292+ filteredTimelineNodes
293+ ) ;
294+
125295 // Create filtered issue object
126296 const filteredIssue : GraphQLIssue = {
127297 ...issue ,
128- body : bodyIsSafe ? issue . body : "" ,
298+ body : processedBody ,
129299 timelineItems : {
130- nodes : filteredTimelineNodes
300+ nodes : processedTimelineNodes
131301 }
132302 } ;
133303
0 commit comments