388 lines
10 KiB
Dart
388 lines
10 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:latlong2/latlong.dart';
|
|
|
|
import '../auth/telegram_session.dart' as telegram_auth;
|
|
import '../models/place_models.dart';
|
|
|
|
class MapflowApi {
|
|
MapflowApi({
|
|
http.Client? client,
|
|
String? telegramInitData,
|
|
String? telegramLoginData,
|
|
String? mapflowSessionToken,
|
|
String endpoint = const String.fromEnvironment(
|
|
'API_BASE_URL',
|
|
defaultValue: '/graphql',
|
|
),
|
|
}) : _client = client ?? http.Client(),
|
|
_telegramInitData = telegramInitData ?? telegram_auth.telegramInitData(),
|
|
_telegramLoginData =
|
|
telegramLoginData ?? telegram_auth.telegramLoginData(),
|
|
_mapflowSessionToken =
|
|
mapflowSessionToken ?? telegram_auth.mapflowSessionToken(),
|
|
_endpoint = Uri.base.resolve(endpoint);
|
|
|
|
final http.Client _client;
|
|
final String _telegramInitData;
|
|
final String _telegramLoginData;
|
|
final String _mapflowSessionToken;
|
|
final Uri _endpoint;
|
|
|
|
bool get hasTelegramAuth =>
|
|
_telegramInitData.isNotEmpty ||
|
|
_telegramLoginData.isNotEmpty ||
|
|
_mapflowSessionToken.isNotEmpty;
|
|
|
|
Future<AppUser?> authenticateTelegram() async {
|
|
if (!hasTelegramAuth) {
|
|
return null;
|
|
}
|
|
|
|
if (_mapflowSessionToken.isNotEmpty) {
|
|
final data = await _graphql('''
|
|
query Me {
|
|
me {
|
|
id
|
|
telegramId
|
|
username
|
|
firstName
|
|
lastName
|
|
photoUrl
|
|
languageCode
|
|
isAdmin
|
|
}
|
|
}
|
|
''');
|
|
return AppUser.fromJson(data['me'] as Map<String, dynamic>);
|
|
}
|
|
|
|
if (_telegramLoginData.isNotEmpty) {
|
|
final loginData = jsonDecode(_telegramLoginData) as Map<String, dynamic>;
|
|
final data = await _graphql(
|
|
'''
|
|
mutation AuthenticateTelegramLogin(
|
|
\$input: AuthenticateTelegramLoginInput!
|
|
) {
|
|
authenticateTelegramLogin(input: \$input) {
|
|
user {
|
|
id
|
|
telegramId
|
|
username
|
|
firstName
|
|
lastName
|
|
photoUrl
|
|
languageCode
|
|
isAdmin
|
|
}
|
|
}
|
|
}
|
|
''',
|
|
variables: {'input': loginData},
|
|
);
|
|
|
|
final payload = data['authenticateTelegramLogin'] as Map<String, dynamic>;
|
|
final user = payload['user'] as Map<String, dynamic>;
|
|
return AppUser.fromJson(user);
|
|
}
|
|
|
|
final data = await _graphql(
|
|
'''
|
|
mutation AuthenticateTelegram(\$input: AuthenticateTelegramInput!) {
|
|
authenticateTelegram(input: \$input) {
|
|
user {
|
|
id
|
|
telegramId
|
|
username
|
|
firstName
|
|
lastName
|
|
photoUrl
|
|
languageCode
|
|
isAdmin
|
|
}
|
|
}
|
|
}
|
|
''',
|
|
variables: {
|
|
'input': {'initData': _telegramInitData},
|
|
},
|
|
);
|
|
|
|
final payload = data['authenticateTelegram'] as Map<String, dynamic>;
|
|
final user = payload['user'] as Map<String, dynamic>;
|
|
return AppUser.fromJson(user);
|
|
}
|
|
|
|
Future<TelegramBotLogin> startTelegramBotLogin() async {
|
|
final data = await _graphql('''
|
|
mutation StartTelegramBotLogin {
|
|
startTelegramBotLogin {
|
|
token
|
|
botUrl
|
|
expiresAt
|
|
}
|
|
}
|
|
''');
|
|
return TelegramBotLogin.fromJson(
|
|
data['startTelegramBotLogin'] as Map<String, dynamic>,
|
|
);
|
|
}
|
|
|
|
Future<TelegramBotLoginSession> completeTelegramBotLogin(String token) async {
|
|
final data = await _graphql(
|
|
'''
|
|
mutation CompleteTelegramBotLogin(\$token: String!) {
|
|
completeTelegramBotLogin(token: \$token) {
|
|
sessionToken
|
|
user {
|
|
id
|
|
telegramId
|
|
username
|
|
firstName
|
|
lastName
|
|
photoUrl
|
|
languageCode
|
|
isAdmin
|
|
}
|
|
}
|
|
}
|
|
''',
|
|
variables: {'token': token},
|
|
);
|
|
return TelegramBotLoginSession.fromJson(
|
|
data['completeTelegramBotLogin'] as Map<String, dynamic>,
|
|
);
|
|
}
|
|
|
|
Future<List<PlaceRecommendation>> fetchPlaces() async {
|
|
final data = await _graphql('''
|
|
query Places {
|
|
places {
|
|
id
|
|
googlePlaceId
|
|
name
|
|
latitude
|
|
longitude
|
|
googlePrimaryType
|
|
googleTypes
|
|
experiences {
|
|
id
|
|
status
|
|
analysis
|
|
createdAt
|
|
}
|
|
}
|
|
}
|
|
''');
|
|
|
|
final places = data['places'] as List<dynamic>;
|
|
return places.map((item) {
|
|
final place = item as Map<String, dynamic>;
|
|
return PlaceRecommendation(
|
|
id: place['id'] as String,
|
|
googlePlaceId: place['googlePlaceId'] as String,
|
|
name: place['name'] as String,
|
|
area: '',
|
|
photoUrls: const [],
|
|
coordinate: LatLng(
|
|
(place['latitude'] as num).toDouble(),
|
|
(place['longitude'] as num).toDouble(),
|
|
),
|
|
traits: _traitsFromExperiences(place['experiences'] as List<dynamic>),
|
|
googlePrimaryType: place['googlePrimaryType'] as String?,
|
|
googleTypes: (place['googleTypes'] as List<dynamic>)
|
|
.map((type) => type as String)
|
|
.toList(),
|
|
);
|
|
}).toList();
|
|
}
|
|
|
|
Future<List<PlaceRecommendation>> fetchNearbyPlaces({
|
|
required LatLng coordinate,
|
|
required int radiusMeters,
|
|
}) async {
|
|
final data = await _graphql(
|
|
'''
|
|
query NearbyPlaces(\$input: NearbyPlacesInput!) {
|
|
nearbyPlaces(input: \$input) {
|
|
id
|
|
googlePlaceId
|
|
name
|
|
latitude
|
|
longitude
|
|
googlePrimaryType
|
|
googleTypes
|
|
experiences {
|
|
id
|
|
status
|
|
analysis
|
|
createdAt
|
|
}
|
|
}
|
|
}
|
|
''',
|
|
variables: {
|
|
'input': {
|
|
'latitude': coordinate.latitude,
|
|
'longitude': coordinate.longitude,
|
|
'radiusMeters': radiusMeters,
|
|
},
|
|
},
|
|
);
|
|
|
|
final places = data['nearbyPlaces'] as List<dynamic>;
|
|
return places.map((item) {
|
|
final place = item as Map<String, dynamic>;
|
|
return PlaceRecommendation(
|
|
id: place['id'] as String,
|
|
googlePlaceId: place['googlePlaceId'] as String,
|
|
name: place['name'] as String,
|
|
area: '',
|
|
photoUrls: const [],
|
|
coordinate: LatLng(
|
|
(place['latitude'] as num).toDouble(),
|
|
(place['longitude'] as num).toDouble(),
|
|
),
|
|
traits: _traitsFromExperiences(place['experiences'] as List<dynamic>),
|
|
googlePrimaryType: place['googlePrimaryType'] as String?,
|
|
googleTypes: (place['googleTypes'] as List<dynamic>)
|
|
.map((type) => type as String)
|
|
.toList(),
|
|
);
|
|
}).toList();
|
|
}
|
|
|
|
Future<void> createVoiceExperience({
|
|
required String googlePlaceId,
|
|
required String googleName,
|
|
required LatLng coordinate,
|
|
required int durationSeconds,
|
|
required String audioObjectKey,
|
|
required String audioContentBase64,
|
|
required String audioMimeType,
|
|
}) async {
|
|
if (!hasTelegramAuth) {
|
|
throw StateError('Telegram authorization is required.');
|
|
}
|
|
|
|
await _graphql(
|
|
'''
|
|
mutation CreateVoiceExperience(\$input: CreateVoiceExperienceInput!) {
|
|
createVoiceExperience(input: \$input) {
|
|
id
|
|
}
|
|
}
|
|
''',
|
|
variables: {
|
|
'input': {
|
|
'googlePlaceId': googlePlaceId,
|
|
'googleName': googleName,
|
|
'latitude': coordinate.latitude,
|
|
'longitude': coordinate.longitude,
|
|
'durationSeconds': durationSeconds,
|
|
'audioObjectKey': audioObjectKey,
|
|
'audioContentBase64': audioContentBase64,
|
|
'audioMimeType': audioMimeType,
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<List<VoiceExperienceDebug>> fetchVoiceExperiences() async {
|
|
final data = await _graphql('''
|
|
query VoiceExperiences {
|
|
voiceExperiences {
|
|
id
|
|
status
|
|
durationSeconds
|
|
transcript
|
|
analysis
|
|
createdAt
|
|
place {
|
|
name
|
|
}
|
|
user {
|
|
telegramId
|
|
username
|
|
firstName
|
|
}
|
|
}
|
|
}
|
|
''');
|
|
|
|
final experiences = data['voiceExperiences'] as List<dynamic>;
|
|
return experiences
|
|
.map(
|
|
(item) => VoiceExperienceDebug.fromJson(item as Map<String, dynamic>),
|
|
)
|
|
.toList();
|
|
}
|
|
|
|
Future<Map<String, dynamic>> _graphql(
|
|
String query, {
|
|
Map<String, dynamic>? variables,
|
|
}) async {
|
|
final response = await _client.post(
|
|
_endpoint,
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
if (_telegramInitData.isNotEmpty)
|
|
'x-telegram-init-data': _telegramInitData,
|
|
if (_telegramLoginData.isNotEmpty)
|
|
'x-telegram-login-data': _telegramLoginData,
|
|
if (_mapflowSessionToken.isNotEmpty)
|
|
'x-mapflow-session-token': _mapflowSessionToken,
|
|
},
|
|
body: jsonEncode({'query': query, 'variables': variables ?? {}}),
|
|
);
|
|
|
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
throw StateError('GraphQL request failed with ${response.statusCode}.');
|
|
}
|
|
|
|
final payload = jsonDecode(response.body) as Map<String, dynamic>;
|
|
final errors = payload['errors'];
|
|
if (errors is List && errors.isNotEmpty) {
|
|
throw StateError(jsonEncode(errors));
|
|
}
|
|
|
|
return payload['data'] as Map<String, dynamic>;
|
|
}
|
|
|
|
Set<PlaceTrait> _traitsFromExperiences(List<dynamic> experiences) {
|
|
final traits = <PlaceTrait>{};
|
|
for (final item in experiences) {
|
|
final experience = item as Map<String, dynamic>;
|
|
final analysis = experience['analysis'];
|
|
if (analysis is! Map<String, dynamic>) {
|
|
continue;
|
|
}
|
|
|
|
final tags = analysis['tags'];
|
|
if (tags is! List) {
|
|
continue;
|
|
}
|
|
|
|
for (final tag in tags) {
|
|
final trait = _traitByTag(tag.toString());
|
|
if (trait != null) {
|
|
traits.add(trait);
|
|
}
|
|
}
|
|
}
|
|
|
|
return traits;
|
|
}
|
|
|
|
PlaceTrait? _traitByTag(String tag) {
|
|
final name = tag.contains(':') ? tag.split(':').last : tag;
|
|
for (final trait in PlaceTrait.values) {
|
|
if (trait.name == name) {
|
|
return trait;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|