Files
flutter/lib/api/mapflow_api.dart
Ruslan Bakiev b93f7ec4ec
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m48s
Use nearby Google places for reviews
2026-05-09 15:19:30 +07:00

341 lines
8.9 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
}
}
''');
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
}
}
}
''',
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
}
}
}
''',
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<TelegramBotLoginStatus> fetchTelegramBotLoginStatus(
String token,
) async {
final data = await _graphql(
'''
query TelegramBotLoginStatus(\$token: String!) {
telegramBotLoginStatus(token: \$token) {
status
sessionToken
user {
id
telegramId
username
firstName
lastName
photoUrl
languageCode
}
}
}
''',
variables: {'token': token},
);
return TelegramBotLoginStatus.fromJson(
data['telegramBotLoginStatus'] as Map<String, dynamic>,
);
}
Future<List<PlaceRecommendation>> fetchPlaces() async {
final data = await _graphql('''
query Places {
places {
id
googlePlaceId
name
latitude
longitude
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>),
);
}).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
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>),
);
}).toList();
}
Future<void> createVoiceExperience({
required String googlePlaceId,
required String googleName,
required LatLng coordinate,
required int durationSeconds,
required String audioObjectKey,
}) 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,
},
},
);
}
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;
}
}