Skip to content

Commit 2cdfe7a

Browse files
author
Seven Du
committed
feat(gateway): add 短信宝 gateway
1 parent 02dff23 commit 2cdfe7a

File tree

2 files changed

+170
-0
lines changed

2 files changed

+170
-0
lines changed

lib/smsbao.dart

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
library odroe.easysms.smsbao;
2+
3+
import 'dart:convert';
4+
5+
import 'package:crypto/crypto.dart';
6+
import 'package:http/http.dart' as http;
7+
8+
import 'easysms.dart';
9+
10+
/// 短信宝 gateway.
11+
///
12+
/// API reference: https://www.smsbao.com/openapi
13+
///
14+
/// ## Usage:
15+
/// ```dart
16+
/// import 'package:odroe/easysms/smsbao.dart';
17+
///
18+
/// final smsbao = SmsBaoGateway(
19+
/// username: 'username',
20+
/// password: 'password',
21+
/// );
22+
///
23+
/// final easysms = EasySms(gateways: [smsbao]);
24+
///
25+
/// final result = await easysms.send(...);
26+
/// ```
27+
class SmsBaoGateway implements Gateway {
28+
/// Endpoint of 短信宝
29+
static final String endpoint = 'api.smsbao.com';
30+
31+
/// Your username of smsbao.
32+
final String username;
33+
34+
/// Your password of smsbao.
35+
final String? password;
36+
37+
/// Your api key of smsbao.
38+
final String? apiKey;
39+
40+
/// Your project id of smsbao.
41+
///
42+
/// **Note**: This is only available for china mainland.
43+
final String? projectId;
44+
45+
/// Creates a new smsbao gateway.
46+
///
47+
/// You can use either [password] or [apiKey] to authenticate.
48+
const SmsBaoGateway({
49+
required this.username,
50+
this.password,
51+
this.apiKey,
52+
this.projectId,
53+
});
54+
55+
/// Authorization parameter
56+
String get authorization {
57+
// If api key is provided, use it.
58+
if (apiKey != null) {
59+
return apiKey!;
60+
61+
// If password is provided, covert it to md5.
62+
} else if (password != null) {
63+
return md5.convert(utf8.encode(password!)).toString();
64+
}
65+
66+
// Otherwise, throw an error.
67+
throw ArgumentError('Either password or apiKey must be provided.');
68+
}
69+
70+
@override
71+
Future<Iterable<Response>> send(
72+
Iterable<PhoneNumber> to, Message message, http.Client client) async {
73+
final requests = await groupRequests(to, message);
74+
print(requests);
75+
final responses = requests.map<Future<Iterable<Response>>>((e) async {
76+
final streamedResponse = await client.send(e.key);
77+
final httpResponse = await http.Response.fromStream(streamedResponse);
78+
79+
return e.value.map(
80+
(to) => Response(
81+
gateway: this,
82+
to: to,
83+
success: httpResponse.body == '0',
84+
response: httpResponse,
85+
),
86+
);
87+
});
88+
89+
final results = await Future.wait(responses);
90+
91+
return results.expand((e) => e);
92+
}
93+
94+
/// Groups requests.
95+
///
96+
/// Group Chinese Mainland and international mobile phone numbers.
97+
Future<Iterable<MapEntry<http.Request, Iterable<PhoneNumber>>>> groupRequests(
98+
Iterable<PhoneNumber> to, Message message) async {
99+
final groups = <bool, Set<PhoneNumber>>{
100+
true: to.where((phone) => isChineseMainland(phone)).toSet(),
101+
false: to.where((phone) => !isChineseMainland(phone)).toSet(),
102+
};
103+
104+
final resutls = groups.entries.map((e) async {
105+
final url = await generateRequestUrl(e.value, message, e.key);
106+
final request = http.Request('GET', url);
107+
108+
return MapEntry(request, e.value);
109+
});
110+
111+
return Future.wait(resutls);
112+
}
113+
114+
/// Is Chinese Mainland.
115+
bool isChineseMainland(PhoneNumber phone) {
116+
return phone.countryCode == 86 && phone.nationalNumber.trim().length == 11;
117+
}
118+
119+
/// Generates the request url.
120+
Future<Uri> generateRequestUrl(
121+
Iterable<PhoneNumber> to, Message message, bool isChineseMainland) async {
122+
final queryParameters = <String, String>{
123+
if (isChineseMainland && projectId != null) 'g': projectId!,
124+
'm': to.map((e) => generatePhoneNumber(e, isChineseMainland)).join(','),
125+
'c': await message.toText(this),
126+
'u': username,
127+
'p': authorization,
128+
};
129+
final path = generatePath(isChineseMainland);
130+
131+
// Return the request url.
132+
return Uri.https(endpoint, path, queryParameters);
133+
}
134+
135+
/// Generate phone number.
136+
String generatePhoneNumber(PhoneNumber phone, bool isChineseMainland) {
137+
if (isChineseMainland) {
138+
return phone.nationalNumber;
139+
} else {
140+
return '+${phone.countryCode}${phone.nationalNumber}';
141+
}
142+
}
143+
144+
/// Generate path.
145+
String generatePath(bool isChineseMainland) {
146+
return isChineseMainland ? '/sms' : '/wsms';
147+
}
148+
}

test/smsbao_test.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import 'package:easysms/easysms.dart';
2+
import 'package:easysms/smsbao.dart';
3+
import 'package:test/test.dart';
4+
5+
void main() {
6+
final smsbao = SmsBaoGateway(
7+
username: 'username',
8+
password: 'password',
9+
);
10+
11+
test('authorization md5', () {
12+
expect(smsbao.authorization, '5f4dcc3b5aa765d61d8327deb882cf99');
13+
});
14+
15+
test('isChineseMainland', () {
16+
final chinaPhoneNumber = PhoneNumber(86, '12345678901');
17+
final internationalPhoneNumber = PhoneNumber(1, '16354732');
18+
19+
expect(smsbao.isChineseMainland(chinaPhoneNumber), true);
20+
expect(smsbao.isChineseMainland(internationalPhoneNumber), false);
21+
});
22+
}

0 commit comments

Comments
 (0)