1
0
mirror of https://github.com/git-touch/git-touch synced 2025-03-11 16:50:12 +01:00

355 lines
9.8 KiB
Dart
Raw Normal View History

import 'dart:io';
2019-02-07 14:35:19 +08:00
import 'dart:convert';
import 'dart:async';
2020-01-29 17:33:54 +08:00
import 'package:git_touch/models/gitea.dart';
2019-12-07 13:18:44 +08:00
import 'package:git_touch/utils/request_serilizer.dart';
import 'package:gql_http_link/gql_http_link.dart';
import 'package:artemis/artemis.dart';
import 'package:fimber/fimber.dart';
2019-02-07 14:35:19 +08:00
import 'package:http/http.dart' as http;
import 'package:uni_links/uni_links.dart';
import 'package:nanoid/nanoid.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
2019-02-07 14:35:19 +08:00
import 'package:shared_preferences/shared_preferences.dart';
2019-03-10 16:09:26 +08:00
import '../utils/utils.dart';
2019-09-08 20:07:35 +08:00
import 'account.dart';
2019-12-30 20:50:31 +08:00
import 'gitlab.dart';
2020-01-27 13:24:01 +08:00
const clientId = 'df930d7d2e219f26142a';
2019-02-21 21:21:16 +08:00
class PlatformType {
static const github = 'github';
static const gitlab = 'gitlab';
2020-01-29 17:33:54 +08:00
static const gitea = 'gitea';
}
2020-01-29 12:32:35 +08:00
class DataWithPage<T> {
T data;
int cursor;
bool hasMore;
int total;
DataWithPage({this.data, this.cursor, this.hasMore, this.total});
2020-01-29 12:32:35 +08:00
}
2019-09-27 20:52:38 +08:00
class AuthModel with ChangeNotifier {
2019-09-26 22:14:14 +08:00
static const _apiPrefix = 'https://api.github.com';
2019-02-21 21:21:16 +08:00
2019-09-27 20:52:38 +08:00
List<Account> _accounts;
2019-09-26 22:14:14 +08:00
int activeAccountIndex;
2019-02-07 14:35:19 +08:00
StreamSubscription<Uri> _sub;
bool loading = false;
2019-02-07 14:35:19 +08:00
2019-09-27 20:52:38 +08:00
List<Account> get accounts => _accounts;
Account get activeAccount {
2019-09-26 22:14:14 +08:00
if (activeAccountIndex == null || _accounts == null) return null;
return _accounts[activeAccountIndex];
2019-02-07 14:35:19 +08:00
}
2019-01-30 14:46:18 +08:00
2019-09-26 22:14:14 +08:00
String get token => activeAccount.token;
2019-09-27 20:52:38 +08:00
_addAccount(Account account) async {
2020-02-01 18:14:42 +08:00
_accounts = [...accounts, account];
// Save
final prefs = await SharedPreferences.getInstance();
await prefs.setString(StorageKeys.accounts, json.encode(_accounts));
}
2020-02-01 18:30:32 +08:00
removeAccount(int index) async {
if (activeAccountIndex == index) {
activeAccountIndex = null;
}
_accounts.removeAt(index);
// Save
final prefs = await SharedPreferences.getInstance();
await prefs.setString(StorageKeys.accounts, json.encode(_accounts));
notifyListeners();
}
2019-02-07 14:35:19 +08:00
// https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#web-application-flow
Future<void> _onSchemeDetected(Uri uri) async {
await closeWebView();
2019-09-08 20:07:35 +08:00
loading = true;
notifyListeners();
2019-09-26 22:14:14 +08:00
// Get token by code
final res = await http.post(
2020-01-27 13:24:01 +08:00
'https://git-touch-oauth.now.sh/api/token',
2019-02-07 14:35:19 +08:00
headers: {
HttpHeaders.acceptHeader: 'application/json',
HttpHeaders.contentTypeHeader: 'application/json',
},
body: json.encode({
'client_id': clientId,
2019-09-26 22:14:14 +08:00
'code': uri.queryParameters['code'],
'state': _oauthState,
2019-02-07 14:35:19 +08:00
}),
);
2019-09-26 22:14:14 +08:00
final token = json.decode(res.body)['access_token'] as String;
2019-10-03 12:55:17 +08:00
await loginWithToken(token);
}
2019-02-07 14:35:19 +08:00
2019-10-03 12:55:17 +08:00
Future<void> loginWithToken(String token) async {
2020-02-01 17:00:30 +08:00
try {
final queryData = await query('''
2019-02-07 14:35:19 +08:00
{
viewer {
login
avatarUrl
}
}
''', token);
2019-02-07 14:35:19 +08:00
2020-02-01 17:00:30 +08:00
await _addAccount(Account(
platform: PlatformType.github,
domain: 'https://github.com',
token: token,
login: queryData['viewer']['login'] as String,
avatarUrl: queryData['viewer']['avatarUrl'] as String,
));
} finally {
loading = false;
notifyListeners();
}
2019-02-07 14:35:19 +08:00
}
2019-02-21 21:21:16 +08:00
Future<void> loginToGitlab(String domain, String token) async {
2020-02-01 17:00:30 +08:00
loading = true;
notifyListeners();
2019-02-21 21:21:16 +08:00
try {
2019-09-26 22:14:14 +08:00
final res = await http
2019-02-21 21:21:16 +08:00
.get('$domain/api/v4/user', headers: {'Private-Token': token});
2019-09-26 22:14:14 +08:00
final info = json.decode(res.body);
2019-02-21 21:21:16 +08:00
if (info['message'] != null) {
throw info['message'];
}
2019-12-30 20:50:31 +08:00
final user = GitlabUser.fromJson(info);
2019-02-21 21:21:16 +08:00
2019-09-27 20:52:38 +08:00
await _addAccount(Account(
2019-09-26 22:14:14 +08:00
platform: PlatformType.gitlab,
domain: domain,
token: token,
2019-12-30 20:50:31 +08:00
login: user.username,
avatarUrl: user.avatarUrl,
gitlabId: user.id,
));
2019-02-21 21:21:16 +08:00
} finally {
2019-09-08 20:07:35 +08:00
loading = false;
notifyListeners();
2019-02-21 21:21:16 +08:00
}
}
2019-12-11 23:37:29 +08:00
Future<String> fetchWithGitlabToken(String p) async {
final res = await http.get(p, headers: {'Private-Token': token});
return res.body;
}
2019-11-02 01:12:17 +08:00
Future fetchGitlab(String p) async {
2020-01-29 12:32:35 +08:00
final res = await http.get('${activeAccount.domain}/api/v4$p',
2019-11-02 01:12:17 +08:00
headers: {'Private-Token': token});
final info = json.decode(utf8.decode(res.bodyBytes));
2020-01-30 12:53:07 +08:00
if (info is Map && info['message'] != null) throw info['message'];
2019-11-02 01:12:17 +08:00
return info;
}
2020-01-29 12:32:35 +08:00
Future<DataWithPage> fetchGitlabWithPage(String p) async {
final res = await http.get('${activeAccount.domain}/api/v4$p',
headers: {'Private-Token': token});
final next = int.tryParse(
res.headers['X-Next-Pages'] ?? res.headers['x-next-page'] ?? '');
final info = json.decode(utf8.decode(res.bodyBytes));
2020-01-30 12:53:07 +08:00
if (info is Map && info['message'] != null) throw info['message'];
return DataWithPage(
data: info,
cursor: next,
hasMore: next != null,
total:
int.tryParse(res.headers['X-Total'] ?? res.headers['x-total'] ?? ''),
);
2020-01-29 12:32:35 +08:00
}
2020-01-29 17:33:54 +08:00
Future loginToGitea(String domain, String token) async {
try {
loading = true;
notifyListeners();
final res = await http.get('$domain/api/v1/user',
headers: {'Authorization': 'token $token'});
final info = json.decode(res.body);
if (info['message'] != null) {
throw info['message'];
}
final user = GiteaUser.fromJson(info);
await _addAccount(Account(
platform: PlatformType.gitea,
domain: domain,
token: token,
login: user.login,
avatarUrl: user.avatarUrl,
));
} finally {
loading = false;
notifyListeners();
}
}
2019-12-04 22:02:22 +08:00
Future fetchGitea(String p) async {
2020-01-29 18:00:48 +08:00
final res = await http.get('${activeAccount.domain}/api/v1$p',
headers: {'Authorization': 'token $token'});
2019-12-04 22:02:22 +08:00
final info = json.decode(utf8.decode(res.bodyBytes));
return info;
}
2020-02-01 15:21:42 +08:00
Future<DataWithPage> fetchGiteaWithPage(String p) async {
final res = await http.get('${activeAccount.domain}/api/v1$p',
headers: {'Authorization': 'token $token'});
final info = json.decode(utf8.decode(res.bodyBytes));
return DataWithPage(
data: info,
2020-02-01 15:42:10 +08:00
cursor: int.tryParse(res.headers["x-page"] ?? ''),
hasMore: res.headers['x-hasmore'] == 'true',
2020-02-01 15:21:42 +08:00
total: int.tryParse(res.headers['x-total'] ?? ''),
);
}
2019-11-05 15:09:54 +08:00
Future<void> init() async {
2019-09-26 22:14:14 +08:00
// Listen scheme
2019-09-08 20:07:35 +08:00
_sub = getUriLinksStream().listen(_onSchemeDetected, onError: (err) {
Fimber.e('getUriLinksStream failed', ex: err);
2019-09-08 20:07:35 +08:00
});
var prefs = await SharedPreferences.getInstance();
2019-02-07 14:35:19 +08:00
2019-09-26 22:14:14 +08:00
// Read accounts
2019-02-21 21:21:16 +08:00
try {
2019-09-26 22:14:14 +08:00
String str = prefs.getString(StorageKeys.accounts);
Fimber.d('read accounts: $str');
2019-09-26 22:14:14 +08:00
_accounts = (json.decode(str ?? '[]') as List)
2019-09-27 20:52:38 +08:00
.map((item) => Account.fromJson(item))
2019-09-26 22:14:14 +08:00
.toList();
2019-02-21 21:21:16 +08:00
} catch (err) {
Fimber.e('prefs getAccount failed', ex: err);
2019-09-26 22:14:14 +08:00
_accounts = [];
2019-02-21 21:21:16 +08:00
}
2019-09-08 20:07:35 +08:00
notifyListeners();
2019-02-07 14:35:19 +08:00
}
2019-10-03 12:24:09 +08:00
@override
void dispose() {
_sub.cancel();
super.dispose();
}
2020-02-01 14:44:15 +08:00
var rootKey = UniqueKey();
void setActiveAccountAndReload(int index) {
// https://stackoverflow.com/a/50116077
rootKey = UniqueKey();
2019-09-26 22:14:14 +08:00
activeAccountIndex = index;
2020-01-14 14:46:36 +08:00
_gqlClient = null;
2019-09-08 20:07:35 +08:00
notifyListeners();
2019-02-07 14:35:19 +08:00
}
Map<String, String> get _headers =>
{HttpHeaders.authorizationHeader: 'token $token'};
// http timeout
var _timeoutDuration = Duration(seconds: 10);
// var _timeoutDuration = Duration(seconds: 1);
ArtemisClient _gqlClient;
ArtemisClient get gqlClient {
if (token == null) return null;
2019-11-06 21:27:37 +08:00
if (_gqlClient == null) {
_gqlClient = ArtemisClient.fromLink(
2019-12-07 13:18:44 +08:00
HttpLink(
_apiPrefix + '/graphql',
defaultHeaders: {HttpHeaders.authorizationHeader: 'token $token'},
serializer: GithubRequestSerializer(),
),
);
}
2019-11-06 21:27:37 +08:00
return _gqlClient;
}
2019-11-06 21:27:37 +08:00
2019-02-07 14:35:19 +08:00
Future<dynamic> query(String query, [String _token]) async {
if (_token == null) {
_token = token;
}
if (_token == null) {
2019-09-26 22:14:14 +08:00
throw 'token is null';
2019-02-07 14:35:19 +08:00
}
final res = await http
2019-09-08 20:07:35 +08:00
.post(_apiPrefix + '/graphql',
headers: {
HttpHeaders.authorizationHeader: 'token $_token',
HttpHeaders.contentTypeHeader: 'application/json'
},
body: json.encode({'query': query}))
.timeout(_timeoutDuration);
// Fimber.d(res.body);
2019-02-07 14:35:19 +08:00
final data = json.decode(res.body);
if (data['errors'] != null) {
2019-09-26 22:14:14 +08:00
throw data['errors'][0]['message'];
2019-02-07 14:35:19 +08:00
}
2019-02-10 19:15:50 +08:00
2019-02-07 14:35:19 +08:00
return data['data'];
}
2020-01-31 20:57:01 +08:00
Future<dynamic> getWithCredentials(String url) async {
final res = await http
2020-01-31 20:57:01 +08:00
.get(_apiPrefix + url, headers: _headers)
.timeout(_timeoutDuration);
2019-02-07 14:35:19 +08:00
final data = json.decode(res.body);
2020-01-31 15:37:17 +08:00
if (res.statusCode >= 400) {
throw data['message'];
}
2019-02-07 14:35:19 +08:00
return data;
}
2020-01-31 20:57:01 +08:00
Future<String> getRaw(String url) async {
final res = await http.get(_apiPrefix + url, headers: {
..._headers,
// https://developer.github.com/v3/repos/contents/#custom-media-types
HttpHeaders.acceptHeader: 'application/vnd.github.v3.raw'
}).timeout(_timeoutDuration);
return res.body;
}
Future<void> patchWithCredentials(String url) async {
2019-09-08 20:07:35 +08:00
await http
.patch(_apiPrefix + url, headers: _headers)
.timeout(_timeoutDuration);
2019-02-07 14:35:19 +08:00
}
2019-10-03 12:16:44 +08:00
Future<http.Response> putWithCredentials(
String url, {
String contentType = 'application/json',
Map<String, dynamic> body = const {},
}) async {
return http
.put(
_apiPrefix + url,
headers: {..._headers, HttpHeaders.contentTypeHeader: contentType},
body: json.encode(body),
)
.timeout(_timeoutDuration);
2019-02-07 14:35:19 +08:00
}
String _oauthState;
void redirectToGithubOauth() {
_oauthState = nanoid();
2020-01-20 14:33:46 +08:00
var scope = Uri.encodeComponent('user,repo,read:org,notifications');
2019-09-29 13:32:53 +08:00
launchUrl(
2019-09-22 01:02:14 +08:00
'https://github.com/login/oauth/authorize?client_id=$clientId&redirect_uri=gittouch://login&scope=$scope&state=$_oauthState',
);
}
2019-01-30 14:46:18 +08:00
}