import 'dart:convert'; import 'dart:io'; import 'package:github/github.dart'; class GitHubClient { GitHubClient({this.ghCommand = 'gh'}); final String ghCommand; Future? _authenticatedLoginFuture; Future> fetchUpdatedIssues({ required RepositorySlug repo, required DateTime? since, }) async { final query = { 'state': 'all', 'sort': 'updated', 'direction': 'asc', 'per_page': '100', if (since != null) 'since': since.toUtc().toIso8601String(), }; final json = await _runJson([ 'api', '-X', 'GET', _buildApiPath(repo, ['issues'], query), ]); return (json as List) .cast>() .where((item) => !item.containsKey('pull_request')) .map(GitHubIssueSummary.fromJson) .toList(growable: false); } Future fetchThread({ required RepositorySlug repo, required GitHubIssueSummary issue, }) async { final comments = await _runJson([ 'api', '-X', 'GET', _buildApiPath( repo, ['issues', '${issue.number}', 'comments'], {'per_page': '100'}, ), ]); return IssueThread( issue: issue, comments: (comments as List) .cast>() .map(GitHubIssueComment.fromJson) .toList(growable: false), ); } Future createIssueComment({ required RepositorySlug repo, required int issueNumber, required String body, }) async { final json = await _runJson([ 'api', '-X', 'POST', _buildApiPath(repo, ['issues', '$issueNumber', 'comments']), '-f', 'body=$body', ]); final map = json as Map; return PostedComment( id: map['id'] as int, url: map['html_url'] as String? ?? '', body: map['body'] as String? ?? body, ); } Future getAuthenticatedLogin() { return _authenticatedLoginFuture ??= _loadAuthenticatedLogin(); } Future _loadAuthenticatedLogin() async { final json = await _runJson(['api', 'user']); if (json is! Map) { return null; } final login = json['login']?.toString().trim(); if (login == null || login.isEmpty) { return null; } return login; } Future _runJson(List arguments) async { final result = await Process.run(ghCommand, arguments); if (result.exitCode != 0) { throw ProcessException( ghCommand, arguments, '${result.stdout}\n${result.stderr}', result.exitCode, ); } final stdoutText = result.stdout.toString().trim(); if (stdoutText.isEmpty) { return null; } return jsonDecode(stdoutText); } String _buildApiPath( RepositorySlug repo, List tailSegments, [ Map query = const {}, ]) { return Uri( pathSegments: ['repos', repo.owner, repo.name, ...tailSegments], queryParameters: query.isEmpty ? null : query, ).toString(); } } class GitHubIssueSummary { GitHubIssueSummary({ required this.number, required this.title, required this.body, required this.state, required this.url, required this.updatedAt, required this.userLogin, required this.labels, }); final int number; final String title; final String body; final String state; final String url; final DateTime updatedAt; final String userLogin; final List labels; factory GitHubIssueSummary.fromJson(Map json) { final labelsNode = json['labels'] as List? ?? const []; return GitHubIssueSummary( number: json['number'] as int, title: json['title'] as String? ?? '', body: json['body'] as String? ?? '', state: json['state'] as String? ?? 'open', url: json['html_url'] as String? ?? '', updatedAt: DateTime.parse(json['updated_at'] as String), userLogin: (json['user'] as Map? ?? const {})['login'] as String? ?? 'unknown', labels: labelsNode .map((label) => (label as Map)['name'].toString()) .toList(growable: false), ); } Map toJson() => { 'number': number, 'title': title, 'body': body, 'state': state, 'url': url, 'updated_at': updatedAt.toUtc().toIso8601String(), 'user_login': userLogin, 'labels': labels, }; } class GitHubIssueComment { GitHubIssueComment({ required this.id, required this.body, required this.userLogin, required this.createdAt, required this.updatedAt, required this.url, }); final int id; final String body; final String userLogin; final DateTime createdAt; final DateTime updatedAt; final String url; factory GitHubIssueComment.fromJson(Map json) { return GitHubIssueComment( id: json['id'] as int, body: json['body'] as String? ?? '', userLogin: (json['user'] as Map? ?? const {})['login'] as String? ?? 'unknown', createdAt: DateTime.parse(json['created_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String), url: json['html_url'] as String? ?? '', ); } Map toJson() => { 'id': id, 'body': body, 'user_login': userLogin, 'created_at': createdAt.toUtc().toIso8601String(), 'updated_at': updatedAt.toUtc().toIso8601String(), 'url': url, }; } class IssueThread { IssueThread({required this.issue, required this.comments}); final GitHubIssueSummary issue; final List comments; bool get isOpen => issue.state == 'open'; GitHubIssueComment? get latestComment => comments.isEmpty ? null : comments.last; Map toJson() => { 'issue': issue.toJson(), 'comments': comments.map((comment) => comment.toJson()).toList(), }; } class PostedComment { PostedComment({required this.id, required this.url, required this.body}); final int id; final String url; final String body; }