Compare commits

...

3 Commits

Author SHA1 Message Date
d9e4a3c1d4 feat: Implement XEP-0444 2022-12-06 14:09:07 +01:00
0ae13acca0 chore(release): publish packages
- moxxmpp@0.1.6+1
 - moxxmpp_socket_tcp@0.1.2+9
2022-11-26 15:48:48 +01:00
d383fa31ae fix: Fix LMC not working 2022-11-26 15:48:29 +01:00
14 changed files with 138 additions and 20 deletions

View File

@ -16,10 +16,10 @@ dependencies:
version: 0.1.4+1 version: 0.1.4+1
moxxmpp: moxxmpp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.1.6 version: 0.1.6+1
moxxmpp_socket_tcp: moxxmpp_socket_tcp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.1.2+8 version: 0.1.2+9
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -1,3 +1,7 @@
## 0.1.6+1
- **FIX**: Fix LMC not working.
## 0.1.6 ## 0.1.6
- **FEAT**: Implement XEP-0308. - **FEAT**: Implement XEP-0308.

View File

@ -59,6 +59,7 @@ export 'package:moxxmpp/src/xeps/xep_0203.dart';
export 'package:moxxmpp/src/xeps/xep_0280.dart'; export 'package:moxxmpp/src/xeps/xep_0280.dart';
export 'package:moxxmpp/src/xeps/xep_0297.dart'; export 'package:moxxmpp/src/xeps/xep_0297.dart';
export 'package:moxxmpp/src/xeps/xep_0300.dart'; export 'package:moxxmpp/src/xeps/xep_0300.dart';
export 'package:moxxmpp/src/xeps/xep_0308.dart';
export 'package:moxxmpp/src/xeps/xep_0333.dart'; export 'package:moxxmpp/src/xeps/xep_0333.dart';
export 'package:moxxmpp/src/xeps/xep_0334.dart'; export 'package:moxxmpp/src/xeps/xep_0334.dart';
export 'package:moxxmpp/src/xeps/xep_0352.dart'; export 'package:moxxmpp/src/xeps/xep_0352.dart';
@ -74,6 +75,7 @@ export 'package:moxxmpp/src/xeps/xep_0384/xep_0384.dart';
export 'package:moxxmpp/src/xeps/xep_0385.dart'; export 'package:moxxmpp/src/xeps/xep_0385.dart';
export 'package:moxxmpp/src/xeps/xep_0414.dart'; export 'package:moxxmpp/src/xeps/xep_0414.dart';
export 'package:moxxmpp/src/xeps/xep_0424.dart'; export 'package:moxxmpp/src/xeps/xep_0424.dart';
export 'package:moxxmpp/src/xeps/xep_0444.dart';
export 'package:moxxmpp/src/xeps/xep_0446.dart'; export 'package:moxxmpp/src/xeps/xep_0446.dart';
export 'package:moxxmpp/src/xeps/xep_0447.dart'; export 'package:moxxmpp/src/xeps/xep_0447.dart';
export 'package:moxxmpp/src/xeps/xep_0448.dart'; export 'package:moxxmpp/src/xeps/xep_0448.dart';

View File

@ -9,6 +9,7 @@ import 'package:moxxmpp/src/xeps/xep_0085.dart';
import 'package:moxxmpp/src/xeps/xep_0359.dart'; import 'package:moxxmpp/src/xeps/xep_0359.dart';
import 'package:moxxmpp/src/xeps/xep_0385.dart'; import 'package:moxxmpp/src/xeps/xep_0385.dart';
import 'package:moxxmpp/src/xeps/xep_0424.dart'; import 'package:moxxmpp/src/xeps/xep_0424.dart';
import 'package:moxxmpp/src/xeps/xep_0444.dart';
import 'package:moxxmpp/src/xeps/xep_0446.dart'; import 'package:moxxmpp/src/xeps/xep_0446.dart';
import 'package:moxxmpp/src/xeps/xep_0447.dart'; import 'package:moxxmpp/src/xeps/xep_0447.dart';
import 'package:moxxmpp/src/xeps/xep_0461.dart'; import 'package:moxxmpp/src/xeps/xep_0461.dart';
@ -75,6 +76,7 @@ class MessageEvent extends XmppEvent {
this.funCancellation, this.funCancellation,
this.messageRetraction, this.messageRetraction,
this.messageCorrectionId, this.messageCorrectionId,
this.messageReactions,
}); });
final StanzaError? error; final StanzaError? error;
final String body; final String body;
@ -97,6 +99,7 @@ class MessageEvent extends XmppEvent {
final bool encrypted; final bool encrypted;
final MessageRetractionData? messageRetraction; final MessageRetractionData? messageRetraction;
final String? messageCorrectionId; final String? messageCorrectionId;
final MessageReactions? messageReactions;
final Map<String, dynamic> other; final Map<String, dynamic> other;
} }

View File

@ -7,6 +7,7 @@ import 'package:moxxmpp/src/xeps/xep_0359.dart';
import 'package:moxxmpp/src/xeps/xep_0380.dart'; import 'package:moxxmpp/src/xeps/xep_0380.dart';
import 'package:moxxmpp/src/xeps/xep_0385.dart'; import 'package:moxxmpp/src/xeps/xep_0385.dart';
import 'package:moxxmpp/src/xeps/xep_0424.dart'; import 'package:moxxmpp/src/xeps/xep_0424.dart';
import 'package:moxxmpp/src/xeps/xep_0444.dart';
import 'package:moxxmpp/src/xeps/xep_0446.dart'; import 'package:moxxmpp/src/xeps/xep_0446.dart';
import 'package:moxxmpp/src/xeps/xep_0447.dart'; import 'package:moxxmpp/src/xeps/xep_0447.dart';
import 'package:moxxmpp/src/xeps/xep_0461.dart'; import 'package:moxxmpp/src/xeps/xep_0461.dart';
@ -61,6 +62,8 @@ class StanzaHandlerData with _$StanzaHandlerData {
MessageRetractionData? messageRetraction, MessageRetractionData? messageRetraction,
// If non-null, then the message is a correction for the specified stanza Id // If non-null, then the message is a correction for the specified stanza Id
String? lastMessageCorrectionSid, String? lastMessageCorrectionSid,
// Reactions data
MessageReactions? messageReactions,
} }
) = _StanzaHandlerData; ) = _StanzaHandlerData;
} }

View File

@ -59,7 +59,9 @@ mixin _$StanzaHandlerData {
// retracted // retracted
MessageRetractionData? get messageRetraction => MessageRetractionData? get messageRetraction =>
throw _privateConstructorUsedError; // If non-null, then the message is a correction for the specified stanza Id throw _privateConstructorUsedError; // If non-null, then the message is a correction for the specified stanza Id
String? get lastMessageCorrectionSid => throw _privateConstructorUsedError; String? get lastMessageCorrectionSid =>
throw _privateConstructorUsedError; // Reactions data
MessageReactions? get messageReactions => throw _privateConstructorUsedError;
@JsonKey(ignore: true) @JsonKey(ignore: true)
$StanzaHandlerDataCopyWith<StanzaHandlerData> get copyWith => $StanzaHandlerDataCopyWith<StanzaHandlerData> get copyWith =>
@ -94,7 +96,8 @@ abstract class $StanzaHandlerDataCopyWith<$Res> {
DelayedDelivery? delayedDelivery, DelayedDelivery? delayedDelivery,
Map<String, dynamic> other, Map<String, dynamic> other,
MessageRetractionData? messageRetraction, MessageRetractionData? messageRetraction,
String? lastMessageCorrectionSid}); String? lastMessageCorrectionSid,
MessageReactions? messageReactions});
} }
/// @nodoc /// @nodoc
@ -131,6 +134,7 @@ class _$StanzaHandlerDataCopyWithImpl<$Res>
Object? other = freezed, Object? other = freezed,
Object? messageRetraction = freezed, Object? messageRetraction = freezed,
Object? lastMessageCorrectionSid = freezed, Object? lastMessageCorrectionSid = freezed,
Object? messageReactions = freezed,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
done: done == freezed done: done == freezed
@ -225,6 +229,10 @@ class _$StanzaHandlerDataCopyWithImpl<$Res>
? _value.lastMessageCorrectionSid ? _value.lastMessageCorrectionSid
: lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable : lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable
as String?, as String?,
messageReactions: messageReactions == freezed
? _value.messageReactions
: messageReactions // ignore: cast_nullable_to_non_nullable
as MessageReactions?,
)); ));
} }
} }
@ -259,7 +267,8 @@ abstract class _$$_StanzaHandlerDataCopyWith<$Res>
DelayedDelivery? delayedDelivery, DelayedDelivery? delayedDelivery,
Map<String, dynamic> other, Map<String, dynamic> other,
MessageRetractionData? messageRetraction, MessageRetractionData? messageRetraction,
String? lastMessageCorrectionSid}); String? lastMessageCorrectionSid,
MessageReactions? messageReactions});
} }
/// @nodoc /// @nodoc
@ -298,6 +307,7 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res>
Object? other = freezed, Object? other = freezed,
Object? messageRetraction = freezed, Object? messageRetraction = freezed,
Object? lastMessageCorrectionSid = freezed, Object? lastMessageCorrectionSid = freezed,
Object? messageReactions = freezed,
}) { }) {
return _then(_$_StanzaHandlerData( return _then(_$_StanzaHandlerData(
done == freezed done == freezed
@ -392,6 +402,10 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res>
? _value.lastMessageCorrectionSid ? _value.lastMessageCorrectionSid
: lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable : lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable
as String?, as String?,
messageReactions: messageReactions == freezed
? _value.messageReactions
: messageReactions // ignore: cast_nullable_to_non_nullable
as MessageReactions?,
)); ));
} }
} }
@ -418,7 +432,8 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
this.delayedDelivery, this.delayedDelivery,
final Map<String, dynamic> other = const <String, dynamic>{}, final Map<String, dynamic> other = const <String, dynamic>{},
this.messageRetraction, this.messageRetraction,
this.lastMessageCorrectionSid}) this.lastMessageCorrectionSid,
this.messageReactions})
: _other = other; : _other = other;
// Indicates to the runner that processing is now done. This means that all // Indicates to the runner that processing is now done. This means that all
@ -501,10 +516,13 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
// If non-null, then the message is a correction for the specified stanza Id // If non-null, then the message is a correction for the specified stanza Id
@override @override
final String? lastMessageCorrectionSid; final String? lastMessageCorrectionSid;
// Reactions data
@override
final MessageReactions? messageReactions;
@override @override
String toString() { String toString() {
return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, stableId: $stableId, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other, messageRetraction: $messageRetraction, lastMessageCorrectionSid: $lastMessageCorrectionSid)'; return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, stableId: $stableId, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other, messageRetraction: $messageRetraction, lastMessageCorrectionSid: $lastMessageCorrectionSid, messageReactions: $messageReactions)';
} }
@override @override
@ -544,7 +562,9 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other.messageRetraction, messageRetraction) && .equals(other.messageRetraction, messageRetraction) &&
const DeepCollectionEquality().equals( const DeepCollectionEquality().equals(
other.lastMessageCorrectionSid, lastMessageCorrectionSid)); other.lastMessageCorrectionSid, lastMessageCorrectionSid) &&
const DeepCollectionEquality()
.equals(other.messageReactions, messageReactions));
} }
@override @override
@ -572,7 +592,8 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
const DeepCollectionEquality().hash(delayedDelivery), const DeepCollectionEquality().hash(delayedDelivery),
const DeepCollectionEquality().hash(_other), const DeepCollectionEquality().hash(_other),
const DeepCollectionEquality().hash(messageRetraction), const DeepCollectionEquality().hash(messageRetraction),
const DeepCollectionEquality().hash(lastMessageCorrectionSid) const DeepCollectionEquality().hash(lastMessageCorrectionSid),
const DeepCollectionEquality().hash(messageReactions)
]); ]);
@JsonKey(ignore: true) @JsonKey(ignore: true)
@ -603,7 +624,8 @@ abstract class _StanzaHandlerData implements StanzaHandlerData {
final DelayedDelivery? delayedDelivery, final DelayedDelivery? delayedDelivery,
final Map<String, dynamic> other, final Map<String, dynamic> other,
final MessageRetractionData? messageRetraction, final MessageRetractionData? messageRetraction,
final String? lastMessageCorrectionSid}) = _$_StanzaHandlerData; final String? lastMessageCorrectionSid,
final MessageReactions? messageReactions}) = _$_StanzaHandlerData;
@override // Indicates to the runner that processing is now done. This means that all @override // Indicates to the runner that processing is now done. This means that all
// pre-processing is done and no other handlers should be consulted. // pre-processing is done and no other handlers should be consulted.
@ -658,6 +680,8 @@ abstract class _StanzaHandlerData implements StanzaHandlerData {
MessageRetractionData? get messageRetraction; MessageRetractionData? get messageRetraction;
@override // If non-null, then the message is a correction for the specified stanza Id @override // If non-null, then the message is a correction for the specified stanza Id
String? get lastMessageCorrectionSid; String? get lastMessageCorrectionSid;
@override // Reactions data
MessageReactions? get messageReactions;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith => _$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith =>

View File

@ -26,3 +26,4 @@ const cryptographicHashManager = 'org.moxxmpp.cryptographichashmanager';
const delayedDeliveryManager = 'org.moxxmpp.delayeddeliverymanager'; const delayedDeliveryManager = 'org.moxxmpp.delayeddeliverymanager';
const messageRetractionManager = 'org.moxxmpp.messageretractionmanager'; const messageRetractionManager = 'org.moxxmpp.messageretractionmanager';
const lastMessageCorrectionManager = 'org.moxxmpp.lastmessagecorrectionmanager'; const lastMessageCorrectionManager = 'org.moxxmpp.lastmessagecorrectionmanager';
const messageReactionsManager = 'org.moxxmpp.messagereactionsmanager';

View File

@ -15,6 +15,7 @@ import 'package:moxxmpp/src/xeps/xep_0308.dart';
import 'package:moxxmpp/src/xeps/xep_0333.dart'; import 'package:moxxmpp/src/xeps/xep_0333.dart';
import 'package:moxxmpp/src/xeps/xep_0359.dart'; import 'package:moxxmpp/src/xeps/xep_0359.dart';
import 'package:moxxmpp/src/xeps/xep_0424.dart'; import 'package:moxxmpp/src/xeps/xep_0424.dart';
import 'package:moxxmpp/src/xeps/xep_0444.dart';
import 'package:moxxmpp/src/xeps/xep_0446.dart'; import 'package:moxxmpp/src/xeps/xep_0446.dart';
import 'package:moxxmpp/src/xeps/xep_0447.dart'; import 'package:moxxmpp/src/xeps/xep_0447.dart';
import 'package:moxxmpp/src/xeps/xep_0448.dart'; import 'package:moxxmpp/src/xeps/xep_0448.dart';
@ -38,6 +39,7 @@ class MessageDetails {
this.shouldEncrypt = false, this.shouldEncrypt = false,
this.messageRetraction, this.messageRetraction,
this.lastMessageCorrectionId, this.lastMessageCorrectionId,
this.messageReactions,
}); });
final String to; final String to;
final String? body; final String? body;
@ -56,6 +58,7 @@ class MessageDetails {
final bool shouldEncrypt; final bool shouldEncrypt;
final MessageRetractionData? messageRetraction; final MessageRetractionData? messageRetraction;
final String? lastMessageCorrectionId; final String? lastMessageCorrectionId;
final MessageReactions? messageReactions;
} }
class MessageManager extends XmppManagerBase { class MessageManager extends XmppManagerBase {
@ -102,6 +105,7 @@ class MessageManager extends XmppManagerBase {
encrypted: state.encrypted, encrypted: state.encrypted,
messageRetraction: state.messageRetraction, messageRetraction: state.messageRetraction,
messageCorrectionId: state.lastMessageCorrectionSid, messageCorrectionId: state.lastMessageCorrectionSid,
messageReactions: state.messageReactions,
other: state.other, other: state.other,
error: StanzaError.fromStanza(message), error: StanzaError.fromStanza(message),
),); ),);
@ -262,6 +266,10 @@ class MessageManager extends XmppManagerBase {
); );
} }
if (details.messageReactions != null) {
stanza.addChild(details.messageReactions!.toXml());
}
getAttributes().sendStanza(stanza, awaitable: false); getAttributes().sendStanza(stanza, awaitable: false);
} }
} }

View File

@ -123,9 +123,12 @@ const fasteningXmlns = 'urn:xmpp:fasten:0';
// XEP-0424 // XEP-0424
const messageRetractionXmlns = 'urn:xmpp:message-retract:0'; const messageRetractionXmlns = 'urn:xmpp:message-retract:0';
// XEp-0428 // XEP-0428
const fallbackIndicationXmlns = 'urn:xmpp:fallback:0'; const fallbackIndicationXmlns = 'urn:xmpp:fallback:0';
// XEP-0444
const messageReactionsXmlns = 'urn:xmpp:reactions:0';
// XEP-0446 // XEP-0446
const fileMetadataXmlns = 'urn:xmpp:file:metadata:0'; const fileMetadataXmlns = 'urn:xmpp:file:metadata:0';

View File

@ -30,8 +30,8 @@ class LastMessageCorrectionManager extends XmppManagerBase {
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler( StanzaHandler(
stanzaTag: 'message', stanzaTag: 'message',
tagName: 'reply', tagName: 'replace',
tagXmlns: replyXmlns, tagXmlns: lmcXmlns,
callback: _onMessage, callback: _onMessage,
// Before the message handler // Before the message handler
priority: -99, priority: -99,
@ -42,9 +42,7 @@ class LastMessageCorrectionManager extends XmppManagerBase {
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onMessage(Stanza stanza, StanzaHandlerData state) async { Future<StanzaHandlerData> _onMessage(Stanza stanza, StanzaHandlerData state) async {
final edit = stanza.firstTag('replace', xmlns: lmcXmlns); final edit = stanza.firstTag('replace', xmlns: lmcXmlns)!;
if (edit == null) return state;
return state.copyWith( return state.copyWith(
lastMessageCorrectionSid: edit.attributes['id']! as String, lastMessageCorrectionSid: edit.attributes['id']! as String,
); );

View File

@ -0,0 +1,68 @@
import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
class MessageReactions {
const MessageReactions(this.messageId, this.emojis);
final String messageId;
final List<String> emojis;
XMLNode toXml() {
return XMLNode.xmlns(
tag: 'reactions',
xmlns: messageReactionsXmlns,
attributes: <String, String>{
'id': messageId,
},
children: emojis.map((emoji) {
return XMLNode(
tag: 'reaction',
text: emoji,
);
}).toList(),
);
}
}
class MessageReactionsManager extends XmppManagerBase {
@override
List<String> getDiscoFeatures() => [ messageReactionsXmlns ];
@override
String getName() => 'MessageReactionsManager';
@override
String getId() => messageReactionsManager;
@override
List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'message',
tagName: 'reactions',
tagXmlns: messageReactionsXmlns,
callback: _onReactionsReceived,
// Before the message handler
priority: -99,
),
];
@override
Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onReactionsReceived(Stanza message, StanzaHandlerData state) async {
final reactionsElement = message.firstTag('reactions', xmlns: messageReactionsXmlns)!;
return state.copyWith(
messageReactions: MessageReactions(
reactionsElement.attributes['id']! as String,
reactionsElement.children
.where((c) => c.tag == 'reaction')
.map((c) => c.innerText())
.toList(),
),
);
}
}

View File

@ -1,6 +1,6 @@
name: moxxmpp name: moxxmpp
description: A pure-Dart XMPP library description: A pure-Dart XMPP library
version: 0.1.6 version: 0.1.6+1
homepage: https://codeberg.org/moxxy/moxxmpp homepage: https://codeberg.org/moxxy/moxxmpp
publish_to: https://git.polynom.me/api/packages/Moxxy/pub publish_to: https://git.polynom.me/api/packages/Moxxy/pub
@ -31,6 +31,6 @@ dev_dependencies:
build_runner: ^2.1.11 build_runner: ^2.1.11
moxxmpp_socket_tcp: moxxmpp_socket_tcp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.2+8 version: ^0.1.2+9
test: ^1.16.0 test: ^1.16.0
very_good_analysis: ^3.0.1 very_good_analysis: ^3.0.1

View File

@ -1,3 +1,7 @@
## 0.1.2+9
- Update a dependency to the latest release.
## 0.1.2+8 ## 0.1.2+8
- Update a dependency to the latest release. - Update a dependency to the latest release.

View File

@ -1,6 +1,6 @@
name: moxxmpp_socket_tcp name: moxxmpp_socket_tcp
description: A socket for moxxmpp using TCP that implements the RFC6120 connection algorithm and XEP-0368 description: A socket for moxxmpp using TCP that implements the RFC6120 connection algorithm and XEP-0368
version: 0.1.2+8 version: 0.1.2+9
homepage: https://codeberg.org/moxxy/moxxmpp homepage: https://codeberg.org/moxxy/moxxmpp
publish_to: https://git.polynom.me/api/packages/Moxxy/pub publish_to: https://git.polynom.me/api/packages/Moxxy/pub
@ -12,7 +12,7 @@ dependencies:
meta: ^1.6.0 meta: ^1.6.0
moxxmpp: moxxmpp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.6 version: ^0.1.6+1
dev_dependencies: dev_dependencies:
lints: ^2.0.0 lints: ^2.0.0