Skip to content

Commit 165352f

Browse files
committed
Refactor timeline event processing to handle comments only and improve attachment handling
- Removed support for `CrossReferencedEvent` and `ReferencedEvent` types across timeline-related queries and processing. - Added logic to download attachments and replace URLs in PR/Issue bodies, timeline comments, and review comments. - Simplified attachment downloader by relying on signed URLs and improving URL-to-local-path mapping. - Enhanced handling of review bodies by processing attachments from HTML content.
1 parent a8b8ff9 commit 165352f

File tree

7 files changed

+328
-236
lines changed

7 files changed

+328
-236
lines changed

src/github/api/graphql-data-fetcher.ts

Lines changed: 200 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,53 @@
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";
22
import {Octokits} from "./client";
33
import { executeWithRetry } from "../../utils/retry";
44
import {
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

Comments
 (0)