From bc7700635250e2aa915a7f6bb830b1882fc3c8a1 Mon Sep 17 00:00:00 2001 From: Rapougnac Date: Wed, 27 Sep 2023 13:47:03 +0200 Subject: [PATCH] Initial commit --- CHANGELOG.md | 3 + README.md | 39 +++++++++ a.html | 0 analysis_options.yaml | 30 +++++++ example/perspective_api_example.dart | 15 ++++ lib/perspective_api.dart | 7 ++ lib/src/http/request.dart | 71 ++++++++++++++++ lib/src/models/analyze_comment_response.dart | 50 +++++++++++ lib/src/models/attribute_score.dart | 72 ++++++++++++++++ lib/src/models/language.dart | 33 ++++++++ lib/src/models/requested_attribute.dart | 88 ++++++++++++++++++++ lib/src/perspective_api.dart | 64 ++++++++++++++ pubspec.yaml | 17 ++++ test/perspective_api_test.dart | 0 14 files changed, 489 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 a.html create mode 100644 analysis_options.yaml create mode 100644 example/perspective_api_example.dart create mode 100644 lib/perspective_api.dart create mode 100644 lib/src/http/request.dart create mode 100644 lib/src/models/analyze_comment_response.dart create mode 100644 lib/src/models/attribute_score.dart create mode 100644 lib/src/models/language.dart create mode 100644 lib/src/models/requested_attribute.dart create mode 100644 lib/src/perspective_api.dart create mode 100644 pubspec.yaml create mode 100644 test/perspective_api_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8b55e73 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/a.html b/a.html new file mode 100644 index 0000000..e69de29 diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/example/perspective_api_example.dart b/example/perspective_api_example.dart new file mode 100644 index 0000000..93f3907 --- /dev/null +++ b/example/perspective_api_example.dart @@ -0,0 +1,15 @@ +import 'package:perspective_api/perspective_api.dart'; + +void main(List args) async { + final client = PerspectiveApi( + apiKey: 'AIzaSyBbovs3G9iJBycX_K_JmfXDkoi0lRj-ifA', + ); + + final response = await client.analyzeComment( + 'Salut, belle gosse 😏', + requestedAttributes: {RequestedAttribute.flirtation}, + languages: {Language.french}, + ); + + print(response); +} diff --git a/lib/perspective_api.dart b/lib/perspective_api.dart new file mode 100644 index 0000000..528f6e0 --- /dev/null +++ b/lib/perspective_api.dart @@ -0,0 +1,7 @@ +library perspective_api; + +export 'src/perspective_api.dart'; +export 'src/models/analyze_comment_response.dart'; +export 'src/models/language.dart'; +export 'src/models/requested_attribute.dart'; +export 'src/models/attribute_score.dart'; diff --git a/lib/src/http/request.dart b/lib/src/http/request.dart new file mode 100644 index 0000000..3bed9b1 --- /dev/null +++ b/lib/src/http/request.dart @@ -0,0 +1,71 @@ +import 'package:http/http.dart'; + +/// An HTTP request to be made against the API. +abstract class HttpRequest { + /// The route for this request. + final String route; + + /// The method for this request. + final String method; + + /// The query parameters for this request. + final Map queryParameters; + + /// The headers for this request. + final Map headers; + + /// Create a new [HttpRequest]. + /// + /// {@macro http_request} + HttpRequest( + this.route, { + this.method = 'GET', + this.queryParameters = const {}, + this.headers = const {}, + }); + + /// Transform this [HttpRequest] into a [BaseRequest] to be sent. + BaseRequest prepare(); + + Uri _getUri() => Uri.https( + 'commentanalyzer.googleapis.com', + '/v1alpha1/$route', + queryParameters.isNotEmpty ? queryParameters : null, + ); + + @override + String toString() => 'HttpRequest($method $route)'; +} + +/// An [HttpRequest] with a JSON body. +class BasicRequest extends HttpRequest { + /// The `Content-Type` header for JSON requests. + static const jsonContentTypeHeader = {'Content-Type': 'application/json'}; + + /// The JSON-encoded body of this request. + /// + /// Set to `null` to send no body. + final String? body; + + /// Create a new [BasicRequest]. + BasicRequest( + super.route, { + this.body, + super.method, + super.queryParameters, + super.headers, + }); + + @override + Request prepare() { + final request = Request(method, _getUri()); + request.headers.addAll(headers); + + if (body != null) { + request.headers.addAll(jsonContentTypeHeader); + request.body = body!; + } + + return request; + } +} diff --git a/lib/src/models/analyze_comment_response.dart b/lib/src/models/analyze_comment_response.dart new file mode 100644 index 0000000..9a14d58 --- /dev/null +++ b/lib/src/models/analyze_comment_response.dart @@ -0,0 +1,50 @@ +import 'package:perspective_api/src/models/attribute_score.dart'; +import 'package:perspective_api/src/models/language.dart'; + +import 'requested_attribute.dart'; + +class AnalyzeCommentResponse { + /// The requested languages. + final Set languages; + + /// The auto-detected languages. + final Set detectedLanguages; + + /// The attribute scores for this comment. + final List attributeScores; + + const AnalyzeCommentResponse({ + required this.languages, + required this.detectedLanguages, + required this.attributeScores, + }); + + static AnalyzeCommentResponse parseAnalyzeCommentResponse(Map res) { + final {'languages': languages as List, 'detectedLanguages': detectedLanguages as List, 'attributeScores': rawAttributeScores as Map} = res; + + final attributeScores = []; + + for (final ra in rawAttributeScores.keys) { + final attribute = RequestedAttribute.fromScreamingSnakeCase(ra); + + final attributeScore = AttributeScore.parseAttributeScore(rawAttributeScores[ra] as Map, attribute); + + attributeScores.add(attributeScore); + } + + return AnalyzeCommentResponse( + languages: languages.map((l) => Language.fromCode(l)).toSet(), + detectedLanguages: detectedLanguages + .map((dl) { + try { + return Language.fromCode(dl); + } on ArgumentError { + return null; + } + }) + .whereType() + .toSet(), + attributeScores: attributeScores, + ); + } +} diff --git a/lib/src/models/attribute_score.dart b/lib/src/models/attribute_score.dart new file mode 100644 index 0000000..31400ae --- /dev/null +++ b/lib/src/models/attribute_score.dart @@ -0,0 +1,72 @@ +import 'package:perspective_api/src/models/requested_attribute.dart'; + +class AttributeScore { + /// The requested attribute. + final RequestedAttribute attribute; + + /// The summary score for this attribute. + final double summaryScore; + + /// The span scores for this attribute. + final List spanScores; + + const AttributeScore({ + required this.attribute, + required this.summaryScore, + required this.spanScores, + }); + + static AttributeScore parseAttributeScore(Map res, RequestedAttribute attribute) { + final { + 'summaryScore': sum as Map, + 'spanScores': spanScores as List, + } = res; + + final summaryScore = sum['value'] as double; + + return AttributeScore( + attribute: attribute, + summaryScore: summaryScore, + spanScores: spanScores.map((ss) => SpanScore.parseSpanScore(ss)).toList(), + ); + } +} + +class SpanScore { + /// The beginning of the span. + final int begin; + + /// The end of the span. + final int end; + + /// The score for this span. + final double score; + + // /// The type of the span. + // final String type; + + const SpanScore({ + required this.begin, + required this.end, + required this.score, + // required this.type, + }); + + static SpanScore parseSpanScore(Map res) { + final { + 'begin': begin as int, + 'end': end as int, + 'score': sc as Map, + // 'type': type as String, + } = res; + + final score = sc['value'] as double; + + return SpanScore( + begin: begin, + end: end, + score: score, + // type: type, + ); + } +} diff --git a/lib/src/models/language.dart b/lib/src/models/language.dart new file mode 100644 index 0000000..82df7bb --- /dev/null +++ b/lib/src/models/language.dart @@ -0,0 +1,33 @@ +enum Language { + english('en'), + arabic('ar'), + chinese('zh'), + czech('cs'), + dutch('nl'), + french('fr'), + german('de'), + italian('it'), + hindi('hi'), + hinglish('hi-Latn'), + indonesian('id'), + japanese('ja'), + korean('ko'), + polish('pl'), + portuguese('pt'), + russian('ru'), + spanish('es'), + swedish('sv'); + + final String code; + + const Language(this.code); + + static Language fromCode(String code) => Language.values.firstWhere( + (e) => e.code == code, + orElse: () => throw ArgumentError.value( + code, + 'code', + 'Invalid language code.', + ), + ); +} diff --git a/lib/src/models/requested_attribute.dart b/lib/src/models/requested_attribute.dart new file mode 100644 index 0000000..778bd56 --- /dev/null +++ b/lib/src/models/requested_attribute.dart @@ -0,0 +1,88 @@ +import 'package:meta/meta.dart'; + +enum RequestedAttribute { + toxicity, + severeToxicity, + identityAttack, + insult, + profanity, + threat, + + // Experimental attributes. + @experimental + toxicityExperimental, + @experimental + severeToxicityExperimental, + @experimental + identityAttackExperimental, + @experimental + insultExperimental, + @experimental + profanityExperimental, + @experimental + threatExperimental, + @experimental + sexuallyExplicit, + @experimental + flirtation, + + // N-Y-T attributes. + attackOnAuthor, + attackOnCommenter, + incoherent, + inflammatory, + likelyToReject, + obscene, + spam, + unsubstantial; + + String toScreamingSnakeCase() { + final split = name.split(''); + StringBuffer res = StringBuffer(); + + for (final char in split) { + if (char != char.toLowerCase() && res.isNotEmpty) { + res.write('_'); + res.write(char); + } else { + res.write(char); + } + } + + return res.toString().toUpperCase(); + } + + static RequestedAttribute fromScreamingSnakeCase(String snakeCase) { + final split = snakeCase.split('_'); + StringBuffer res = StringBuffer(); + + for (final (i, word) in split.map((e) => e.toLowerCase()).indexed) { + if (i == 0) { + res.write(word); + } else { + res.write(word[0].toUpperCase()); + res.write(word.substring(1)); + } + } + + return RequestedAttribute.values.firstWhere( + (e) => e.name == res.toString(), + orElse: () => throw ArgumentError.value( + snakeCase, + 'snakeCase', + 'Invalid attribute name.', + ), + ); + } + + static const Set restrictedLanguages = { + attackOnAuthor, + attackOnCommenter, + incoherent, + inflammatory, + likelyToReject, + obscene, + spam, + unsubstantial, + }; +} diff --git a/lib/src/perspective_api.dart b/lib/src/perspective_api.dart new file mode 100644 index 0000000..a0fcdde --- /dev/null +++ b/lib/src/perspective_api.dart @@ -0,0 +1,64 @@ +import 'dart:convert'; + +import 'package:http/retry.dart'; +import 'package:http/http.dart'; +import 'package:perspective_api/src/http/request.dart'; + +import 'models/analyze_comment_response.dart'; +import 'models/language.dart'; +import 'models/requested_attribute.dart'; + +DateTime? _lastRequestTime; + +final class PerspectiveApi { + final String _apiKey; + + final _client = RetryClient(Client(), when: (res) { + return _lastRequestTime == null || + _lastRequestTime!.isAfter( + DateTime.now().subtract( + const Duration(seconds: 1), + ), + ); + }); + + PerspectiveApi({ + /// The API key to use for requests. + required String apiKey, + }) : _apiKey = apiKey; + + /// Analyze a comment. + Future analyzeComment( + String text, { + required Set requestedAttributes, + required Set languages, + }) async { + final body = { + 'comment': {'text': text}, + 'requestedAttributes': { + for (final ra in requestedAttributes) + ra.toScreamingSnakeCase(): {}, + }, + 'languages': languages.map((l) => l.code).toList(), + }; + + final request = BasicRequest( + 'comments:analyze', + queryParameters: { + 'key': _apiKey, + }, + body: json.encode(body), + method: 'POST', + ); + + final res = await _client.send(request.prepare()); + + _lastRequestTime = DateTime.now(); + + final resBody = (await res.stream.bytesToString()); + + final jsonBody = json.decode(resBody); + + return AnalyzeCommentResponse.parseAnalyzeCommentResponse(jsonBody); + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..865655c --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,17 @@ +name: perspective_api +description: A starting point for Dart libraries or applications. +version: 1.0.0 +# repository: https://github.com/my_org/my_repo + +environment: + sdk: ^3.0.5 + +# Add regular dependencies here. +dependencies: + http: ^1.1.0 + meta: ^1.10.0 + # path: ^1.8.0 + +dev_dependencies: + lints: ^2.0.0 + test: ^1.21.0 diff --git a/test/perspective_api_test.dart b/test/perspective_api_test.dart new file mode 100644 index 0000000..e69de29