blob: d809a8ba19f9e30877c31344af28062f5dd37d04 [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:firebase/firebase.dart' as firebase;
import 'package:firebase/firestore.dart' as firestore;
/// Service for accessing label subscription information stored in the
/// Firestore.
class SubscriptionsService {
firebase.App _app;
Stream<firebase.User> _onAuth;
/// Returns a stream which fires an event every time authentication state
/// changes.
Stream<firebase.User> get onAuth {
if (_onAuth == null) {
final controller = StreamController<firebase.User>();
_ensureApp().then((app) {
app.auth().onAuthStateChanged.pipe(controller.sink);
}).catchError((error, StackTrace stackTrace) {
controller.addError(error, stackTrace);
});
_onAuth = controller.stream;
}
return _onAuth;
}
/// Returns true if we have an authenticated user.
bool get isLoggedIn => _app?.auth()?.currentUser != null;
/// Returns email of the currently authenticated user.
String get userEmail => _app?.auth()?.currentUser?.email;
/// Provides user a chance to authenticate using Google Auth.
Future<void> logIn() async {
final app = await _ensureApp();
if (isLoggedIn) return;
final provider = firebase.GoogleAuthProvider();
provider.addScope('openid https://www.googleapis.com/auth/datastore');
await app.auth().signInWithPopup(provider);
}
/// Returns subscriptions of the current user grouped by repository name.
///
/// As a side-effect creates an empty subscription record for the current
/// user in the firestore - if it is the first time the user is logging in.
Future<Map<String, List<String>>> getSubscriptions() async {
final ref = await _computeSubscriptionsRef();
if (ref == null) {
return {};
}
final doc = await ref.get();
final List<String> subscriptions =
doc.exists ? doc.data()['subscriptions'].cast<String>() : <String>[];
final groupedByRepo = <String, List<String>>{};
for (var labelId in subscriptions) {
final label = GithubLabel.fromId(labelId);
groupedByRepo
.putIfAbsent(label.repositoryName, () => <String>[])
.add(label.labelName);
}
return groupedByRepo;
}
/// Fetch configuration of keyword subscription for the specific repository.
Future<KeywordSubscription> getKeywordSubscription(
String repositoryName) async {
final app = await _ensureApp();
if (!isLoggedIn) {
return null;
}
final sanitizedRepositoryName = repositoryName.replaceAll('/', r'$');
final snapshot = await app
.firestore()
.doc('github-keyword-subscriptions/$sanitizedRepositoryName')
.get();
if (!snapshot.exists) {
return null;
}
return KeywordSubscription(
label: snapshot.get('label'),
keywords: snapshot.get('keywords').cast<String>());
}
/// Adds a subscription to a label for the current user.
Future<void> subscribeTo(String repositoryName, String labelName) async {
final labelId =
GithubLabel(repositoryName: repositoryName, labelName: labelName)
.toId();
final ref = await _computeSubscriptionsRef();
if (ref == null) {
return;
}
final doc = await ref.get();
if (!doc.exists) {
await ref.set({
'email': userEmail,
'subscriptions': [labelId]
}, firestore.SetOptions(merge: true));
} else {
await ref.update(data: {
'subscriptions': firestore.FieldValue.arrayUnion([labelId]),
});
}
}
/// Removes a subscription to a label for the current user.
Future<void> unsubscribeFrom(String repositoryName, String labelName) async {
final ref = await _computeSubscriptionsRef();
if (ref == null) {
return;
}
await ref.update(data: {
'subscriptions': firestore.FieldValue.arrayRemove([
GithubLabel(repositoryName: repositoryName, labelName: labelName).toId()
]),
});
}
/// Initializes firebase App.
Future<firebase.App> _ensureApp() async {
if (_app == null) {
_app = firebase.initializeApp(
apiKey: "AIzaSyBFKKpPdV3xPQU4jPYiMvUnUfhB5pDDMRI",
authDomain: "dart-ci.firebaseapp.com",
databaseURL: "https://dart-ci.firebaseio.com",
projectId: "dart-ci",
storageBucket: "dart-ci.appspot.com");
await _app.auth().setPersistence(firebase.Persistence.LOCAL);
}
return _app;
}
/// Returns path to a firestore document which contains subscriptions for
/// the current logged in user (and null if there is no currently logged in
/// user).
Future<firestore.DocumentReference> _computeSubscriptionsRef() async {
final app = await _ensureApp();
if (!isLoggedIn) {
return null;
}
final currentUser = app.auth().currentUser;
final userId = currentUser.uid;
return app.firestore().doc('github-label-subscriptions/${userId}');
}
}
/// Describes a label in a specific repository.
class GithubLabel {
final String repositoryName;
final String labelName;
GithubLabel({this.repositoryName, this.labelName});
factory GithubLabel.fromId(String labelId) {
final components = labelId.split(':');
return GithubLabel(
repositoryName: components.first,
labelName: components.skip(1).join(':'));
}
String toId() => '$repositoryName:$labelName';
}
/// Represents subscription to particular keywords in a repository.
///
/// Whenever a new issue is opened that contains one of the keywords
/// in the body notifier will notify users subscribed to the specified label.
class KeywordSubscription {
final String label;
final List<String> keywords;
KeywordSubscription({this.label, this.keywords});
}