import 'dart:convert'; import 'dart:math'; import 'package:fluent_reader_lite/models/service.dart'; import 'package:fluent_reader_lite/utils/global.dart'; import 'package:fluent_reader_lite/utils/store.dart'; import 'package:fluent_reader_lite/utils/utils.dart'; import 'package:html/parser.dart'; import 'package:tuple/tuple.dart'; import 'package:http/http.dart' as http; import '../item.dart'; import '../source.dart'; class FeedbinServiceHandler extends ServiceHandler { String endpoint; String username; String password; int fetchLimit; int _lastId; Tuple2, Set> _lastSynced; FeedbinServiceHandler() { endpoint = Store.sp.getString(StoreKeys.ENDPOINT); username = Store.sp.getString(StoreKeys.USERNAME); password = Store.sp.getString(StoreKeys.PASSWORD); fetchLimit = Store.sp.getInt(StoreKeys.FETCH_LIMIT); _lastId = Store.sp.getInt(StoreKeys.LAST_ID) ?? 0; } FeedbinServiceHandler.fromValues( this.endpoint, this.username, this.password, this.fetchLimit ) { _lastId = Store.sp.getInt(StoreKeys.LAST_ID) ?? 0; } void persist() { Store.sp.setInt(StoreKeys.SYNC_SERVICE, SyncService.Feedbin.index); Store.sp.setString(StoreKeys.ENDPOINT, endpoint); Store.sp.setString(StoreKeys.USERNAME, username); Store.sp.setString(StoreKeys.PASSWORD, password); Store.sp.setInt(StoreKeys.FETCH_LIMIT, fetchLimit); Store.sp.setInt(StoreKeys.LAST_ID, _lastId); Global.service = this; } @override void remove() { Store.sp.remove(StoreKeys.SYNC_SERVICE); Store.sp.remove(StoreKeys.ENDPOINT); Store.sp.remove(StoreKeys.USERNAME); Store.sp.remove(StoreKeys.PASSWORD); Store.sp.remove(StoreKeys.FETCH_LIMIT); Store.sp.remove(StoreKeys.LAST_ID); Global.service = null; } String _getApiKey() { final credentials = "$username:$password"; final bytes = utf8.encode(credentials); return base64.encode(bytes); } Future _fetchAPI(String params) async { return await http.get( endpoint + params, headers: { "Authorization": "Basic ${_getApiKey()}", } ); } Future _markItems(String type, String method, List refs) async { final auth = "Basic ${_getApiKey()}"; final promises = List.empty(growable: true); final client = http.Client(); try { while (refs.length > 0) { final batch = List.empty(growable: true); while (batch.length < 1000 && refs.length > 0) { batch.add(int.parse(refs.removeLast())); } final bodyObject = { "${type}_entries": batch, }; final request = http.Request( method, Uri.parse(endpoint + type + "_entries.json"), ); request.headers["Authorization"] = auth; request.headers["Content-Type"] = "application/json; charset=utf-8"; request.body = jsonEncode(bodyObject); promises.add(client.send(request)); } await Future.wait(promises); } finally { client.close(); } } int get lastId => _lastId; set lastId(int value) { _lastId = value; Store.sp.setInt(StoreKeys.LAST_ID, value); } @override Future validate() async { try { final response = await _fetchAPI("authentication.json"); return response.statusCode == 200; } catch(exp) { print(exp); return false; } } @override Future, Map>>> getSources() async { final response = await _fetchAPI("subscriptions.json"); assert(response.statusCode == 200); final subscriptions = jsonDecode(response.body); final groupsMap = Map>(); final tagsResponse = await _fetchAPI("taggings.json"); assert(tagsResponse.statusCode == 200); final tags = jsonDecode(tagsResponse.body); for (var tag in tags) { final name = tag["name"].trim(); groupsMap.putIfAbsent(name, () => []); groupsMap[name].add(tag["feed_id"].toString()); } final sources = subscriptions.map((s) { return RSSSource(s["feed_id"].toString(), s["feed_url"], s["title"]); }).toList(); return Tuple2(sources, groupsMap); } @override Future> fetchItems() async { var page = 1; var minId = Utils.syncMaxId; var items = []; List lastFetched; do { try { final response = await _fetchAPI("entries.json?mode=extended&per_page=125&page=$page"); assert(response.statusCode == 200); lastFetched = jsonDecode(response.body); items.addAll(lastFetched.where((i) => i["id"] > lastId && i["id"] < minId)); minId = lastFetched.fold(minId, (m, n) => min(m, n["id"])); page += 1; } catch(exp) { break; } } while ( minId > lastId && lastFetched != null && lastFetched.length >= 125 && items.length < fetchLimit ); lastId = items.fold(lastId, (m, n) => max(m, n["id"])); final parsedItems = List.empty(growable: true); final unread = _lastSynced.item1; final starred = _lastSynced.item2; for (var i in items) { if (i["content"] == null) continue; final dom = parse(i["content"]); final iid = i["id"].toString(); final item = RSSItem( id: iid, source: i["feed_id"].toString(), title: i["title"], link: i["url"], date: DateTime.parse(i["published"]), content: i["content"], snippet: dom.documentElement.text.trim(), creator: i["author"], hasRead: !unread.contains(iid), starred: starred.contains(iid), ); if (i["images"] != null && i["images"]["original_url"] != null) { item.thumb = i["images"]["original_url"]; } else { var img = dom.querySelector("img"); if (img != null && img.attributes["src"] != null) { var thumb = img.attributes["src"]; if (thumb.startsWith("http")) { item.thumb = thumb; } } } parsedItems.add(item); } _lastSynced = null; return parsedItems; } @override Future, Set>> syncItems() async { final responses = await Future.wait([ _fetchAPI("unread_entries.json"), _fetchAPI("starred_entries.json"), ]); assert(responses[0].statusCode == 200); assert(responses[1].statusCode == 200); final unread = jsonDecode(responses[0].body); final starred = jsonDecode(responses[1].body); _lastSynced = Tuple2( Set.from(unread.map((i) => i.toString())), Set.from(starred.map((i) => i.toString())), ); return _lastSynced; } Future markAllRead(Set sids, DateTime date, bool before) async { List predicates = ["hasRead = 0"]; if (sids.length > 0) { predicates.add("source IN (${List.filled(sids.length, "?").join(" , ")})"); } if (date != null) { predicates.add("date ${before ? "<=" : ">="} ${date.millisecondsSinceEpoch}"); } final rows = await Global.db.query( "items", columns: ["iid"], where: predicates.join(" AND "), whereArgs: sids.toList(), ); final iids = rows.map((r) => r["iid"]); await _markItems("unread", "DELETE", List.from(iids)); } Future markRead(RSSItem item) async{ await _markItems("unread", "DELETE", [item.id]); } Future markUnead(RSSItem item) async { await _markItems("unread", "POST", [item.id]); } Future star(RSSItem item) async { await _markItems("starred", "POST", [item.id]); } Future unstar(RSSItem item) async { await _markItems("starred", "DELETE", [item.id]); } }