crbug.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. // Copyright 2022 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. 'use strict';
  5. const process = require('child_process');
  6. const https = require('https');
  7. function log(msg) {
  8. // console.log(msg);
  9. }
  10. class CrBugUser {
  11. constructor(json) {
  12. this.name_ = json.displayName;
  13. this.id_ = json.name;
  14. this.email_ = json.email;
  15. }
  16. get name() {
  17. return this.name_;
  18. }
  19. get id() {
  20. return this.id_;
  21. }
  22. get email() {
  23. return this.email_;
  24. }
  25. };
  26. class CrBugIssue {
  27. constructor(json) {
  28. this.number_ = json.name;
  29. this.reporter_id_ = json.reporter;
  30. this.owner_id_ = json.owner ? json.owner.user : undefined;
  31. this.last_update_ = json.modifyTime;
  32. this.close_ = json.closeTime ? new Date(json.closeTime) : undefined;
  33. this.url_ = undefined;
  34. const parts = this.number_.split('/');
  35. if (parts[0] === 'projects' && parts[2] === 'issues') {
  36. const project = parts[1];
  37. const num = parts[3];
  38. this.url_ =
  39. `https://bugs.chromium.org/p/${project}/issues/detail?id=${num}`;
  40. }
  41. }
  42. get number() {
  43. return this.number_;
  44. }
  45. get owner_id() {
  46. return this.owner_id_;
  47. }
  48. get reporter_id() {
  49. return this.reporter_id_;
  50. }
  51. get url() {
  52. return this.url_;
  53. }
  54. };
  55. class CrBugComment {
  56. constructor(json) {
  57. this.user_id_ = json.commenter;
  58. this.timestamp_ = new Date(json.createTime);
  59. this.timestamp_.setSeconds(0);
  60. this.content_ = json.content;
  61. this.fields_ = json.amendments ?
  62. json.amendments.map(m => m.fieldName.toLowerCase()) :
  63. undefined;
  64. this.json_ = JSON.stringify(json);
  65. }
  66. get user_id() {
  67. return this.user_id_;
  68. }
  69. get timestamp() {
  70. return this.timestamp_;
  71. }
  72. get content() {
  73. return this.content_;
  74. }
  75. get updatedFields() {
  76. return this.fields_;
  77. }
  78. isActivity() {
  79. if (this.content)
  80. return true;
  81. const fields = this.updatedFields;
  82. // If bug A gets merged into bug B, then ignore the update for bug A. There
  83. // will also be an update for bug B, and that will be counted instead.
  84. if (fields && fields.indexOf('mergedinto') >= 0) {
  85. return false;
  86. }
  87. // If bug A is marked as blocked on bug B, then that triggers updates for
  88. // both bugs. So only count 'blockedon', and ignore 'blocking'.
  89. const allowedFields = [
  90. 'blockedon', 'cc', 'components', 'label', 'owner', 'priority', 'status',
  91. 'summary'
  92. ];
  93. if (fields && fields.some(f => allowedFields.indexOf(f) >= 0)) {
  94. return true;
  95. }
  96. return false;
  97. }
  98. };
  99. class CrBug {
  100. constructor(project) {
  101. this.token_ = this.getAuthToken_();
  102. this.project_ = project;
  103. }
  104. getAuthToken_() {
  105. const scope = 'https://www.googleapis.com/auth/userinfo.email';
  106. const args = [
  107. 'luci-auth', 'token', '-use-id-token', '-audience',
  108. 'https://monorail-prod.appspot.com', '-scopes', scope, '-json-output', '-'
  109. ];
  110. const stdout = process.execSync(args.join(' ')).toString().trim();
  111. const json = JSON.parse(stdout);
  112. return json.token;
  113. }
  114. async fetchFromServer_(path, message) {
  115. const hostname = 'api-dot-monorail-prod.appspot.com';
  116. return new Promise((resolve, reject) => {
  117. const postData = JSON.stringify(message);
  118. const options = {
  119. hostname: hostname,
  120. method: 'POST',
  121. path: path,
  122. headers: {
  123. 'Content-Type': 'application/json',
  124. 'Accept': 'application/json',
  125. 'Authorization': `Bearer ${this.token_}`,
  126. }
  127. };
  128. let data = '';
  129. const req = https.request(options, (res) => {
  130. log(`STATUS: ${res.statusCode}`);
  131. log(`HEADERS: ${JSON.stringify(res.headers)}`);
  132. res.setEncoding('utf8');
  133. res.on('data', (chunk) => {
  134. log(`BODY: ${chunk}`);
  135. data += chunk;
  136. });
  137. res.on('end', () => {
  138. if (data.startsWith(')]}\'')) {
  139. resolve(JSON.parse(data.substr(4)));
  140. } else {
  141. resolve(data);
  142. }
  143. });
  144. });
  145. req.on('error', (e) => {
  146. console.error(`problem with request: ${e.message}`);
  147. reject(e.message);
  148. });
  149. // Write data to request body
  150. log(`Writing ${postData}`);
  151. req.write(postData);
  152. req.end();
  153. });
  154. }
  155. /**
  156. * Calls SearchIssues with the given parameters.
  157. *
  158. * @param {string} query The query to use to search.
  159. * @param {Number} pageSize The maximum issues to return.
  160. * @param {string} pageToken The page token from the previous call.
  161. *
  162. * @return {JSON}
  163. */
  164. async searchIssuesPagination_(query, pageSize, pageToken) {
  165. const message = {
  166. 'projects': [this.project_],
  167. 'query': query,
  168. 'pageToken': pageToken,
  169. };
  170. if (pageSize) {
  171. message['pageSize'] = pageSize;
  172. }
  173. const url = '/prpc/monorail.v3.Issues/SearchIssues';
  174. return this.fetchFromServer_(url, message);
  175. }
  176. /**
  177. * Searches Monorail for issues using the given query.
  178. * TODO(crbug.com/monorail/7143): SearchIssues only accepts one project.
  179. *
  180. * @param {string} query The query to use to search.
  181. *
  182. * @return {Array<CrBugIssue>}
  183. */
  184. async search(query) {
  185. const pageSize = 100;
  186. let pageToken;
  187. let issues = [];
  188. do {
  189. const resp =
  190. await this.searchIssuesPagination_(query, pageSize, pageToken);
  191. if (resp.issues) {
  192. issues = issues.concat(resp.issues.map(i => new CrBugIssue(i)));
  193. }
  194. pageToken = resp.nextPageToken;
  195. } while (pageToken);
  196. return issues;
  197. }
  198. /**
  199. * Calls ListComments with the given parameters.
  200. *
  201. * @param {string} issueName Resource name of the issue.
  202. * @param {string} filter The approval filter query.
  203. * @param {Number} pageSize The maximum number of comments to return.
  204. * @param {string} pageToken The page token from the previous request.
  205. *
  206. * @return {JSON}
  207. */
  208. async listCommentsPagination_(issueName, pageToken, pageSize) {
  209. const message = {
  210. 'parent': issueName,
  211. 'pageToken': pageToken,
  212. 'filter': '',
  213. };
  214. if (pageSize) {
  215. message['pageSize'] = pageSize;
  216. }
  217. const url = '/prpc/monorail.v3.Issues/ListComments';
  218. return this.fetchFromServer_(url, message);
  219. }
  220. /**
  221. * Returns all comments and previous/current descriptions of an issue.
  222. *
  223. * @param {CrBugIssue} issue The CrBugIssue instance.
  224. *
  225. * @return {Array<CrBugComment>}
  226. */
  227. async getComments(issue) {
  228. let pageToken;
  229. let comments = [];
  230. do {
  231. const resp = await this.listCommentsPagination_(issue.number, pageToken);
  232. if (resp.comments) {
  233. comments = comments.concat(resp.comments.map(c => new CrBugComment(c)));
  234. }
  235. pageToken = resp.nextPageToken;
  236. } while (pageToken);
  237. return comments;
  238. }
  239. /**
  240. * Returns the user associated with 'username'.
  241. *
  242. * @param {string} username The username (e.g. linus@chromium.org).
  243. *
  244. * @return {CrBugUser}
  245. */
  246. async getUser(username) {
  247. const url = '/prpc/monorail.v3.Users/GetUser';
  248. const message = {
  249. name: `users/${username}`,
  250. };
  251. return new CrBugUser(await this.fetchFromServer_(url, message));
  252. }
  253. };
  254. module.exports = {
  255. CrBug,
  256. };