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"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
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:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -75,6 +83,22 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
js:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -176,6 +200,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.16"
|
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:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -35,6 +35,8 @@ dependencies:
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^1.0.2
|
cupertino_icons: ^1.0.2
|
||||||
|
http: ^0.13.5
|
||||||
|
crypto: ^3.0.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Loading…
Reference in a new issue