Implement basic API requests and types
This commit is contained in:
parent
2379ee1a69
commit
4551ad36dc
127
lib/api/api.dart
Normal file
127
lib/api/api.dart
Normal file
|
@ -0,0 +1,127 @@
|
|||
library fever_api;
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
part 'feed.dart';
|
||||
part 'item.dart';
|
||||
part 'group.dart';
|
||||
part 'feeds_group.dart';
|
||||
part 'exceptions.dart';
|
||||
part 'util.dart';
|
||||
|
||||
/// Basic implementation of the Fever API
|
||||
/// https://github.com/dasmurphy/tinytinyrss-fever-plugin/blob/master/fever-api.md
|
||||
|
||||
class FeverAPI {
|
||||
String apiKey;
|
||||
String apiUrl;
|
||||
|
||||
/// We use a shared HTTP client for all requests
|
||||
final _httpClient = http.Client();
|
||||
|
||||
FeverAPI({ required this.apiKey, required this.apiUrl });
|
||||
|
||||
static String generateApiKey(String username, String password) {
|
||||
var bytes = utf8.encode('$username:$password');
|
||||
return md5.convert(bytes).toString();
|
||||
}
|
||||
|
||||
APIRequestBuilder request() {
|
||||
return APIRequestBuilder(this, _httpClient);
|
||||
}
|
||||
}
|
||||
|
||||
class APIRequestBuilder {
|
||||
final FeverAPI api;
|
||||
final http.Client _httpClient;
|
||||
|
||||
List<String> args = [];
|
||||
|
||||
APIRequestBuilder(this.api, this._httpClient);
|
||||
|
||||
void _addArg(String arg) {
|
||||
if (!args.contains(arg)) args.add(arg);
|
||||
}
|
||||
|
||||
Uri generateUrl() {
|
||||
return Uri.parse('${api.apiUrl}?api&${args.join('&')}');
|
||||
}
|
||||
|
||||
/// Include items in response
|
||||
APIRequestBuilder withItems() {
|
||||
_addArg('items');
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Include feeds in response
|
||||
APIRequestBuilder withFeeds() {
|
||||
_addArg('feeds');
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Include groups in response
|
||||
APIRequestBuilder withGroups() {
|
||||
_addArg('groups');
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Executes the API request and returns the JSON data
|
||||
Future<Map<String, dynamic>> fetch() async {
|
||||
final response = await _httpClient.post(generateUrl(), body: { 'api_key': api.apiKey });
|
||||
final data = jsonDecode((response).body);
|
||||
if (data['auth'] == 0) throw UnauthenticatedException(data['auth']);
|
||||
return data;
|
||||
}
|
||||
|
||||
/// Executes the API request and populates the local cache with the response data
|
||||
Future<void> execute() async {
|
||||
final data = await fetch();
|
||||
|
||||
List<Group> groups = [];
|
||||
List<Feed> feeds = [];
|
||||
List<Item> items = [];
|
||||
List<FeedsGroup> feedsGroups = [];
|
||||
|
||||
if (data.containsKey('groups')) {
|
||||
groups = (data['groups'] as List<dynamic>)
|
||||
.map((json) => Group.fromJSON(api, json))
|
||||
.toList();
|
||||
}
|
||||
|
||||
if (data.containsKey('feeds')) {
|
||||
feeds = (data['feeds'] as List<dynamic>)
|
||||
.map((json) => Feed.fromJSON(api, json))
|
||||
.toList();
|
||||
}
|
||||
|
||||
if (data.containsKey('items')) {
|
||||
items = (data['items'] as List<dynamic>)
|
||||
.map((json) => Item.fromJSON(api, json))
|
||||
.toList();
|
||||
}
|
||||
|
||||
if (data.containsKey('feeds_groups')) {
|
||||
feedsGroups = (data['feeds_groups'] as List<dynamic>)
|
||||
.map((json) => FeedsGroup.fromJSON(api, json))
|
||||
.toList();
|
||||
}
|
||||
|
||||
print(groups);
|
||||
print(feeds);
|
||||
print(items);
|
||||
print(feedsGroups);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
var api = FeverAPI(apiKey: "", apiUrl: "https://rss.amogus.cloud/api/fever.php");
|
||||
|
||||
api.request()
|
||||
.withFeeds()
|
||||
.withGroups()
|
||||
.withItems()
|
||||
.execute();
|
||||
}
|
9
lib/api/exceptions.dart
Normal file
9
lib/api/exceptions.dart
Normal file
|
@ -0,0 +1,9 @@
|
|||
part of fever_api;
|
||||
|
||||
class UnauthenticatedException implements Exception {
|
||||
final int statusCode;
|
||||
const UnauthenticatedException(this.statusCode);
|
||||
|
||||
@override
|
||||
String toString() => 'Unauthenticated: Fever API returned status $statusCode';
|
||||
}
|
38
lib/api/feed.dart
Normal file
38
lib/api/feed.dart
Normal file
|
@ -0,0 +1,38 @@
|
|||
part of fever_api;
|
||||
|
||||
class Feed {
|
||||
final FeverAPI api;
|
||||
|
||||
late int id;
|
||||
late int faviconId;
|
||||
late String title;
|
||||
late String url;
|
||||
late String siteUrl;
|
||||
late bool isSpark;
|
||||
late DateTime lastUpdatedOnTime;
|
||||
|
||||
Feed(this.api, {
|
||||
required this.id,
|
||||
required this.faviconId,
|
||||
required this.title,
|
||||
required this.url,
|
||||
required this.siteUrl,
|
||||
required this.isSpark,
|
||||
required this.lastUpdatedOnTime,
|
||||
});
|
||||
|
||||
Feed.fromJSON(this.api, Map<String, dynamic> json) {
|
||||
id = toInt(json['id']);
|
||||
faviconId = toInt(json['favicon_id']);
|
||||
title = json['title'];
|
||||
url = json['url'];
|
||||
siteUrl = json['site_url'];
|
||||
isSpark = json['isSpark'] == '1';
|
||||
lastUpdatedOnTime = DateTime.fromMillisecondsSinceEpoch(toInt(json['last_updated_on_time']) * 1000);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Feed $id: $title';
|
||||
}
|
||||
}
|
25
lib/api/feeds_group.dart
Normal file
25
lib/api/feeds_group.dart
Normal file
|
@ -0,0 +1,25 @@
|
|||
part of fever_api;
|
||||
|
||||
class FeedsGroup {
|
||||
final FeverAPI api;
|
||||
|
||||
late int groupId;
|
||||
late List<int> feedIds;
|
||||
|
||||
FeedsGroup(this.api, {
|
||||
required this.groupId,
|
||||
required this.feedIds,
|
||||
});
|
||||
|
||||
FeedsGroup.fromJSON(this.api, Map<String, dynamic> json) {
|
||||
groupId = toInt(json['group_id']);
|
||||
feedIds = (json['feed_ids'] as String)
|
||||
.split(',').map((i) => int.parse(i))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FeedsGroup $groupId -> ${feedIds.join(', ')}';
|
||||
}
|
||||
}
|
23
lib/api/group.dart
Normal file
23
lib/api/group.dart
Normal file
|
@ -0,0 +1,23 @@
|
|||
part of fever_api;
|
||||
|
||||
class Group {
|
||||
final FeverAPI api;
|
||||
|
||||
late int id;
|
||||
late String title;
|
||||
|
||||
Group(this.api, {
|
||||
required this.id,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
Group.fromJSON(this.api, Map<String, dynamic> json) {
|
||||
id = toInt(json['id']);
|
||||
title = json['title'];
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Group $id: $title';
|
||||
}
|
||||
}
|
44
lib/api/item.dart
Normal file
44
lib/api/item.dart
Normal file
|
@ -0,0 +1,44 @@
|
|||
part of fever_api;
|
||||
|
||||
class Item {
|
||||
final FeverAPI api;
|
||||
|
||||
late int id;
|
||||
late int feedId;
|
||||
late String title;
|
||||
late String author;
|
||||
late String html;
|
||||
late String url;
|
||||
late bool isSaved;
|
||||
late bool isRead;
|
||||
late DateTime createdOnTime;
|
||||
|
||||
Item(this.api, {
|
||||
required this.id,
|
||||
required this.feedId,
|
||||
required this.title,
|
||||
required this.author,
|
||||
required this.html,
|
||||
required this.url,
|
||||
required this.isSaved,
|
||||
required this.isRead,
|
||||
required this.createdOnTime,
|
||||
});
|
||||
|
||||
Item.fromJSON(this.api, Map<String, dynamic> json) {
|
||||
id = toInt(json['id']);
|
||||
feedId = toInt(json['feed_id']);
|
||||
title = json['title'];
|
||||
author = json['author'];
|
||||
html = json['html'];
|
||||
url = json['url'];
|
||||
isSaved = json['is_saved'] == '1';
|
||||
isRead = json['is_read'] == '1';
|
||||
createdOnTime = DateTime.fromMillisecondsSinceEpoch(toInt(json['created_on_time']) * 1000);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Item $id: $title';
|
||||
}
|
||||
}
|
8
lib/api/util.dart
Normal file
8
lib/api/util.dart
Normal file
|
@ -0,0 +1,8 @@
|
|||
part of fever_api;
|
||||
|
||||
/// PHP developers seem to be incapable of returning consistent types,
|
||||
/// so I need a toInt function that handles both ints and Strings.
|
||||
int toInt(dynamic input) {
|
||||
if (input is int) return input;
|
||||
return int.parse(input.toString());
|
||||
}
|
32
pubspec.lock
32
pubspec.lock
|
@ -41,6 +41,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -75,6 +83,22 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.5"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -176,6 +200,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.16"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -35,6 +35,8 @@ dependencies:
|
|||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.2
|
||||
http: ^0.13.5
|
||||
crypto: ^3.0.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Reference in a new issue