err
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
This commit is contained in:
parent
75048f4f33
commit
a3a5a55fdc
20 changed files with 123 additions and 90 deletions
|
|
@ -57,8 +57,9 @@ ENV PORT=3000 \
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
# Healthcheck - проверка доступности сервера
|
# Healthcheck - проверка доступности сервера
|
||||||
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=5 \
|
# Increased start-period to 30s to allow server initialization (Isar, cron jobs, etc.)
|
||||||
CMD sh -c 'curl -sf http://127.0.0.1:${PORT:-3000}/health || exit 1'
|
HEALTHCHECK --interval=10s --timeout=5s --start-period=20s --retries=3 \
|
||||||
|
CMD curl -f http://127.0.0.1:${PORT:-3000}/health || exit 1
|
||||||
|
|
||||||
# Start server - все настройки читаются из environment variables
|
# Start server - все настройки читаются из environment variables
|
||||||
CMD ["/app/server"]
|
CMD ["/app/server"]
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,8 @@ class MnemoShelf {
|
||||||
final port = int.tryParse(portArg ?? '') ?? 3000;
|
final port = int.tryParse(portArg ?? '') ?? 3000;
|
||||||
final certs = Platform.environment['CERTS_PATH']; // null = HTTP without SSL
|
final certs = Platform.environment['CERTS_PATH']; // null = HTTP without SSL
|
||||||
final dualMode = Platform.environment['DUAL_MODE'] == 'true' ||
|
final dualMode = Platform.environment['DUAL_MODE'] == 'true' ||
|
||||||
Platform.environment['DUAL_MODE'] == '1'; // Запуск HTTP и HTTPS одновременно
|
Platform.environment['DUAL_MODE'] ==
|
||||||
|
'1'; // Запуск HTTP и HTTPS одновременно
|
||||||
|
|
||||||
// V2 APIs (new RESTful API with OAuth2/JWT)
|
// V2 APIs (new RESTful API with OAuth2/JWT)
|
||||||
final v2Routers = [
|
final v2Routers = [
|
||||||
|
|
|
||||||
|
|
@ -67,24 +67,32 @@ class AdminAnalyticsApiV2 {
|
||||||
final paymentCount = await backend_main.isar.paymentModels.count();
|
final paymentCount = await backend_main.isar.paymentModels.count();
|
||||||
|
|
||||||
// Get recent users (last 10)
|
// Get recent users (last 10)
|
||||||
final allUsers = await backend_main.isar.txn(() async => backend_main.isar.userModels.where().findAll());
|
final allUsers = await backend_main.isar
|
||||||
|
.txn(() async => backend_main.isar.userModels.where().findAll());
|
||||||
final sortedUsers = allUsers
|
final sortedUsers = allUsers
|
||||||
..sort((a, b) => (b.id ?? 0).compareTo(a.id ?? 0));
|
..sort((a, b) => (b.id ?? 0).compareTo(a.id ?? 0));
|
||||||
final recentUsers = sortedUsers.take(10).map((u) => {
|
final recentUsers = sortedUsers
|
||||||
|
.take(10)
|
||||||
|
.map((u) => {
|
||||||
'id': u.id,
|
'id': u.id,
|
||||||
'name': u.name,
|
'name': u.name,
|
||||||
'email': u.email,
|
'email': u.email,
|
||||||
'createdAt': DateTime(1999).toIso8601String(),
|
'createdAt': DateTime(1999).toIso8601String(),
|
||||||
}).toList();
|
})
|
||||||
|
.toList();
|
||||||
|
|
||||||
// Get top packs by user count (mock data for now)
|
// Get top packs by user count (mock data for now)
|
||||||
final allPacks = await backend_main.isar.txn(() async => backend_main.isar.cardPackModels.where().findAll());
|
final allPacks = await backend_main.isar
|
||||||
final topPacks = allPacks.take(5).map((p) => {
|
.txn(() async => backend_main.isar.cardPackModels.where().findAll());
|
||||||
|
final topPacks = allPacks
|
||||||
|
.take(5)
|
||||||
|
.map((p) => {
|
||||||
'id': p.id,
|
'id': p.id,
|
||||||
'title': p.title,
|
'title': p.title,
|
||||||
'cards': p.cards.length,
|
'cards': p.cards.length,
|
||||||
'enabled': p.enabled,
|
'enabled': p.enabled,
|
||||||
}).toList();
|
})
|
||||||
|
.toList();
|
||||||
|
|
||||||
return _json({
|
return _json({
|
||||||
'stats': {
|
'stats': {
|
||||||
|
|
@ -126,7 +134,8 @@ class AdminAnalyticsApiV2 {
|
||||||
|
|
||||||
for (int i = 29; i >= 0; i--) {
|
for (int i = 29; i >= 0; i--) {
|
||||||
final date = now.subtract(Duration(days: i));
|
final date = now.subtract(Duration(days: i));
|
||||||
final registrations = (i % 7) + 1; // Mock data: 1-7 registrations per day
|
final registrations =
|
||||||
|
(i % 7) + 1; // Mock data: 1-7 registrations per day
|
||||||
|
|
||||||
chartData.add({
|
chartData.add({
|
||||||
'date': date.toIso8601String().split('T')[0],
|
'date': date.toIso8601String().split('T')[0],
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,8 @@ Use this code to access the admin panel at admin.mnemo-cards.online
|
||||||
// Get user from database
|
// Get user from database
|
||||||
// Note: UserModel doesn't have telegramUserId field, so we need to find user differently
|
// Note: UserModel doesn't have telegramUserId field, so we need to find user differently
|
||||||
// For now, we'll look for admin users (assuming admin field exists)
|
// For now, we'll look for admin users (assuming admin field exists)
|
||||||
final allUsers = await backend_main.isar.txn(() async => backend_main.isar.userModels.where().findAll());
|
final allUsers = await backend_main.isar
|
||||||
|
.txn(() async => backend_main.isar.userModels.where().findAll());
|
||||||
final user = allUsers.where((u) => u.admin).firstOrNull;
|
final user = allUsers.where((u) => u.admin).firstOrNull;
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,8 @@ class AdminCardsApiV2 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all cards from database
|
// Get all cards from database
|
||||||
final allCards = await backend_main.isar.txn(() async => backend_main.isar.gameCardModels.where().findAll());
|
final allCards = await backend_main.isar
|
||||||
|
.txn(() async => backend_main.isar.gameCardModels.where().findAll());
|
||||||
|
|
||||||
// Apply search filter if provided
|
// Apply search filter if provided
|
||||||
List<GameCardModel> filteredCards = allCards;
|
List<GameCardModel> filteredCards = allCards;
|
||||||
|
|
@ -104,8 +105,10 @@ class AdminCardsApiV2 {
|
||||||
return card.original.toLowerCase().contains(searchLower) ||
|
return card.original.toLowerCase().contains(searchLower) ||
|
||||||
card.translation.toLowerCase().contains(searchLower) ||
|
card.translation.toLowerCase().contains(searchLower) ||
|
||||||
card.mnemo.toLowerCase().contains(searchLower) ||
|
card.mnemo.toLowerCase().contains(searchLower) ||
|
||||||
(card.transcription?.toLowerCase().contains(searchLower) ?? false) ||
|
(card.transcription?.toLowerCase().contains(searchLower) ??
|
||||||
(card.transcriptionMnemo?.toLowerCase().contains(searchLower) ?? false);
|
false) ||
|
||||||
|
(card.transcriptionMnemo?.toLowerCase().contains(searchLower) ??
|
||||||
|
false);
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,7 +187,8 @@ class AdminCardsApiV2 {
|
||||||
|
|
||||||
late final GameCardDto cardDto;
|
late final GameCardDto cardDto;
|
||||||
try {
|
try {
|
||||||
cardDto = GameCardDto.fromJson(jsonDecode(body) as Map<String, dynamic>);
|
cardDto =
|
||||||
|
GameCardDto.fromJson(jsonDecode(body) as Map<String, dynamic>);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return _json(
|
return _json(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -98,8 +98,10 @@ class AdminPacksApiV2 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all packs (admin can see disabled packs)
|
// Get all packs (admin can see disabled packs)
|
||||||
final allPacks = await backend_main.isar.txn(() async => backend_main.isar.cardPackModels.where().findAll());
|
final allPacks = await backend_main.isar
|
||||||
List<CardPackModel> filteredPacks = showDisabled ? allPacks : allPacks.where((p) => p.enabled).toList();
|
.txn(() async => backend_main.isar.cardPackModels.where().findAll());
|
||||||
|
List<CardPackModel> filteredPacks =
|
||||||
|
showDisabled ? allPacks : allPacks.where((p) => p.enabled).toList();
|
||||||
if (search != null && search.isNotEmpty) {
|
if (search != null && search.isNotEmpty) {
|
||||||
final searchLower = search.toLowerCase();
|
final searchLower = search.toLowerCase();
|
||||||
filteredPacks = allPacks.where((pack) {
|
filteredPacks = allPacks.where((pack) {
|
||||||
|
|
@ -119,11 +121,8 @@ class AdminPacksApiV2 {
|
||||||
final paginatedPacks = filteredPacks.skip(offset).take(limit).toList();
|
final paginatedPacks = filteredPacks.skip(offset).take(limit).toList();
|
||||||
|
|
||||||
// Convert to preview DTOs
|
// Convert to preview DTOs
|
||||||
final packDtos = await Future.wait(
|
final packDtos = await Future.wait(paginatedPacks.map((pack) async =>
|
||||||
paginatedPacks.map((pack) async =>
|
await _packDtoConverter.toCardPackPreviewDto(pack, null)));
|
||||||
await _packDtoConverter.toCardPackPreviewDto(pack, null)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return _json({
|
return _json({
|
||||||
'items': packDtos.map((p) => p.toJson()).toList(),
|
'items': packDtos.map((p) => p.toJson()).toList(),
|
||||||
|
|
@ -194,7 +193,8 @@ class AdminPacksApiV2 {
|
||||||
|
|
||||||
late final EditCardPackDto packDto;
|
late final EditCardPackDto packDto;
|
||||||
try {
|
try {
|
||||||
packDto = EditCardPackDto.fromJson(jsonDecode(body) as Map<String, dynamic>);
|
packDto =
|
||||||
|
EditCardPackDto.fromJson(jsonDecode(body) as Map<String, dynamic>);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return _json(
|
return _json(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -53,15 +53,16 @@ Middleware authorizeV2(UserManager userManager, JwtService jwtService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a public auth endpoint - always allow for all HTTP methods
|
// Check if this is a public auth endpoint - always allow for all HTTP methods
|
||||||
if (normalizedPath.startsWith('/auth/') || normalizedPath.startsWith('/admin/auth/')) {
|
if (normalizedPath.startsWith('/auth/') ||
|
||||||
|
normalizedPath.startsWith('/admin/auth/')) {
|
||||||
if (_publicAuthPaths.contains(normalizedPath)) {
|
if (_publicAuthPaths.contains(normalizedPath)) {
|
||||||
// Auth endpoints in public list are accessible without token
|
// Auth endpoints in public list are accessible without token
|
||||||
return await innerHandler(request);
|
return await innerHandler(request);
|
||||||
}
|
}
|
||||||
// Check for dynamic auth paths (e.g., /auth/telegram/code-status/<code>)
|
// Check for dynamic auth paths (e.g., /auth/telegram/code-status/<code>)
|
||||||
if (normalizedPath.startsWith('/auth/telegram/code-status')
|
if (normalizedPath.startsWith('/auth/telegram/code-status') ||
|
||||||
|| normalizedPath.startsWith('/admin/auth/request-code')
|
normalizedPath.startsWith('/admin/auth/request-code') ||
|
||||||
|| normalizedPath.startsWith('/admin/auth/verify-code')) {
|
normalizedPath.startsWith('/admin/auth/verify-code')) {
|
||||||
// Code status endpoint is public (used before authentication)
|
// Code status endpoint is public (used before authentication)
|
||||||
return await innerHandler(request);
|
return await innerHandler(request);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -491,9 +491,7 @@ class PacksApiV2 {
|
||||||
final items = voices
|
final items = voices
|
||||||
.where((voice) => voice.id != null)
|
.where((voice) => voice.id != null)
|
||||||
.map(
|
.map(
|
||||||
(voice) => voice
|
(voice) => voice.toDto(url: '/api/v2/voice/${voice.id}').toJson(),
|
||||||
.toDto(url: '/api/v2/voice/${voice.id}')
|
|
||||||
.toJson(),
|
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
|
@ -563,7 +561,8 @@ class PacksApiV2 {
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _sanitizeVoicePath(String path) {
|
String? _sanitizeVoicePath(String path) {
|
||||||
final normalized = path.replaceAll('\\', '/').replaceFirst(RegExp('^/'), '');
|
final normalized =
|
||||||
|
path.replaceAll('\\', '/').replaceFirst(RegExp('^/'), '');
|
||||||
if (normalized.contains('..')) {
|
if (normalized.contains('..')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,8 +61,7 @@ class PromocodesApiV2 {
|
||||||
return _unauthorized();
|
return _unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
final campaigns =
|
final campaigns = await _promoCodesManager.listAvailablePromocodes(user);
|
||||||
await _promoCodesManager.listAvailablePromocodes(user);
|
|
||||||
return _json({
|
return _json({
|
||||||
'campaigns': campaigns.map((c) => c.toJson()).toList(),
|
'campaigns': campaigns.map((c) => c.toJson()).toList(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -91,12 +91,15 @@ class SubscriptionsApiV2 {
|
||||||
return _unauthorized();
|
return _unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
final subscriptionDto = await _subscriptionManager.getSubscriptionDto(user);
|
final subscriptionDto =
|
||||||
|
await _subscriptionManager.getSubscriptionDto(user);
|
||||||
|
|
||||||
return _ok({
|
return _ok({
|
||||||
'active': subscriptionDto.isActive,
|
'active': subscriptionDto.isActive,
|
||||||
if (subscriptionDto.start != null) 'start': subscriptionDto.start!.toIso8601String(),
|
if (subscriptionDto.start != null)
|
||||||
if (subscriptionDto.finish != null) 'finish': subscriptionDto.finish!.toIso8601String(),
|
'start': subscriptionDto.start!.toIso8601String(),
|
||||||
|
if (subscriptionDto.finish != null)
|
||||||
|
'finish': subscriptionDto.finish!.toIso8601String(),
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
developer.log(
|
developer.log(
|
||||||
|
|
|
||||||
|
|
@ -216,11 +216,13 @@ class TelegramBotApiV2 {
|
||||||
if (users.length > 1) {
|
if (users.length > 1) {
|
||||||
return _ok({
|
return _ok({
|
||||||
'multiple': true,
|
'multiple': true,
|
||||||
'users': users.map((u) => {
|
'users': users
|
||||||
|
.map((u) => {
|
||||||
'id': u.id,
|
'id': u.id,
|
||||||
'email': u.email,
|
'email': u.email,
|
||||||
'name': u.name,
|
'name': u.name,
|
||||||
}).toList(),
|
})
|
||||||
|
.toList(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,10 +237,12 @@ class TelegramBotApiV2 {
|
||||||
'email': user.email,
|
'email': user.email,
|
||||||
'name': user.name,
|
'name': user.name,
|
||||||
'tags': user.userData.value?.tags.join(',') ?? '',
|
'tags': user.userData.value?.tags.join(',') ?? '',
|
||||||
'packs': user.packs.map((p) => {
|
'packs': user.packs
|
||||||
|
.map((p) => {
|
||||||
'id': p.id,
|
'id': p.id,
|
||||||
'title': p.title,
|
'title': p.title,
|
||||||
}).toList(),
|
})
|
||||||
|
.toList(),
|
||||||
'purchases': user.purchases.length,
|
'purchases': user.purchases.length,
|
||||||
'subscription': sub == null
|
'subscription': sub == null
|
||||||
? null
|
? null
|
||||||
|
|
@ -260,12 +264,14 @@ class TelegramBotApiV2 {
|
||||||
|
|
||||||
return _ok({
|
return _ok({
|
||||||
'total': users.length,
|
'total': users.length,
|
||||||
'users': users.map((user) => {
|
'users': users
|
||||||
|
.map((user) => {
|
||||||
'id': user.id,
|
'id': user.id,
|
||||||
'email': user.email ?? '${user.id} ${user.name}',
|
'email': user.email ?? '${user.id} ${user.name}',
|
||||||
'lastTimeOnline': user.userData.value?.lastTimeOnline
|
'lastTimeOnline':
|
||||||
?.toIso8601String(),
|
user.userData.value?.lastTimeOnline?.toIso8601String(),
|
||||||
}).toList(),
|
})
|
||||||
|
.toList(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ Middleware telegramBotAuthMiddleware() {
|
||||||
return Response(
|
return Response(
|
||||||
500,
|
500,
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: '{"error":"Internal Server Error","message":"API key not configured"}',
|
body:
|
||||||
|
'{"error":"Internal Server Error","message":"API key not configured"}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -475,7 +475,8 @@ class UsersApiV2 {
|
||||||
studyDates.add(now);
|
studyDates.add(now);
|
||||||
|
|
||||||
// Update total study time
|
// Update total study time
|
||||||
final totalStudyTime = userData.totalStudyTimeMinutes + sessionDto.duration.inMinutes;
|
final totalStudyTime =
|
||||||
|
userData.totalStudyTimeMinutes + sessionDto.duration.inMinutes;
|
||||||
|
|
||||||
// Recalculate streak
|
// Recalculate streak
|
||||||
final currentStreak = _statisticsCalculator.calculateStreak(studyDates);
|
final currentStreak = _statisticsCalculator.calculateStreak(studyDates);
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,10 @@ class PackDtoConverter {
|
||||||
'icons/ad.webp',
|
'icons/ad.webp',
|
||||||
position: PackTipPosition.bottomRight,
|
position: PackTipPosition.bottomRight,
|
||||||
)
|
)
|
||||||
: AssetPackTip('icons/lock.webp',
|
: AssetPackTip(
|
||||||
position: PackTipPosition.bottomRight,),
|
'icons/lock.webp',
|
||||||
|
position: PackTipPosition.bottomRight,
|
||||||
|
),
|
||||||
version: model.version,
|
version: model.version,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -384,11 +384,16 @@ class PackManager {
|
||||||
String appVersion,
|
String appVersion,
|
||||||
) async {
|
) async {
|
||||||
final pack = await _fetchPackModel(packId);
|
final pack = await _fetchPackModel(packId);
|
||||||
|
try {
|
||||||
final archiveFile = File(
|
final archiveFile = File(
|
||||||
'${assetsDirectory.path}/pack_archives/${appVersion}/${packId}_${pack.version}.zip',
|
'${assetsDirectory.path}/pack_archives/${appVersion}/${packId}_${pack.version}.zip',
|
||||||
);
|
);
|
||||||
if (await archiveFile.exists()) {
|
if (await archiveFile.exists()) {
|
||||||
return archiveFile.readAsBytes();
|
return await archiveFile.readAsBytes();
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
log('Error when fetching pack images archive', error: e, stackTrace: s);
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
final cards = pack.cards;
|
final cards = pack.cards;
|
||||||
final rawData = _fetchPackImages(cards.toList());
|
final rawData = _fetchPackImages(cards.toList());
|
||||||
|
|
|
||||||
|
|
@ -226,8 +226,10 @@ class PromoCodesManager {
|
||||||
final upperCode = code.toUpperCase();
|
final upperCode = code.toUpperCase();
|
||||||
|
|
||||||
// Check if code exists
|
// Check if code exists
|
||||||
final codeModel =
|
final codeModel = await isar.promoCodeModels
|
||||||
await isar.promoCodeModels.filter().codeEqualTo(upperCode).findFirst();
|
.filter()
|
||||||
|
.codeEqualTo(upperCode)
|
||||||
|
.findFirst();
|
||||||
await codeModel?.campaign.load();
|
await codeModel?.campaign.load();
|
||||||
final campaign = codeModel?.campaign.value;
|
final campaign = codeModel?.campaign.value;
|
||||||
if (codeModel == null || campaign == null) {
|
if (codeModel == null || campaign == null) {
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,8 @@ class InputButtonsQuestionGenerator implements QuestionGenerator {
|
||||||
final questionType = type ?? _getRandomQuestionType();
|
final questionType = type ?? _getRandomQuestionType();
|
||||||
final questionText =
|
final questionText =
|
||||||
questionType.translationQuestion ? answerItem.translation : null;
|
questionType.translationQuestion ? answerItem.translation : null;
|
||||||
final questionImage = questionType.imageQuestion
|
final questionImage =
|
||||||
? _imageIdToUrl(answerItem.image)
|
questionType.imageQuestion ? _imageIdToUrl(answerItem.image) : null;
|
||||||
: null;
|
|
||||||
|
|
||||||
String? questionAudio = null;
|
String? questionAudio = null;
|
||||||
if (questionType.audioQuestion) {
|
if (questionType.audioQuestion) {
|
||||||
|
|
@ -116,7 +115,6 @@ class InputButtonsQuestionGenerator implements QuestionGenerator {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<InputButtonsTestQuestionBody> verbTestQuestion(
|
Future<InputButtonsTestQuestionBody> verbTestQuestion(
|
||||||
TestDataItem answerCard) async {
|
TestDataItem answerCard) async {
|
||||||
final ends = ['o', 'as', 'a', 'a', 'a', 'amos', 'ais', 'an', 'an', 'an'];
|
final ends = ['o', 'as', 'a', 'a', 'a', 'amos', 'ais', 'an', 'an', 'an'];
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,8 @@ class SimpleQuestionGenerator implements QuestionGenerator {
|
||||||
: questionType.translationQuestion
|
: questionType.translationQuestion
|
||||||
? answerCard.translation
|
? answerCard.translation
|
||||||
: null;
|
: null;
|
||||||
final questionImage = questionType.imageQuestion
|
final questionImage =
|
||||||
? _imageIdToUrl(answerCard.image)
|
questionType.imageQuestion ? _imageIdToUrl(answerCard.image) : null;
|
||||||
: null;
|
|
||||||
String? questionAudio = null;
|
String? questionAudio = null;
|
||||||
if (questionType.audioQuestion) {
|
if (questionType.audioQuestion) {
|
||||||
if (questionType.translationAnswers || questionType.imagesAnswers) {
|
if (questionType.translationAnswers || questionType.imagesAnswers) {
|
||||||
|
|
@ -93,7 +92,6 @@ class SimpleQuestionGenerator implements QuestionGenerator {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<SimpleTestQuestionBody> verbTestQuestion(
|
Future<SimpleTestQuestionBody> verbTestQuestion(
|
||||||
TestDataItem answerCard, {
|
TestDataItem answerCard, {
|
||||||
bool withAudio = true,
|
bool withAudio = true,
|
||||||
|
|
|
||||||
|
|
@ -76,9 +76,11 @@ class TelegramUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send message to admin via Telegram Bot API
|
/// Send message to admin via Telegram Bot API
|
||||||
static Future<bool> sendMessageToAdmin(String telegramUserId, String message) async {
|
static Future<bool> sendMessageToAdmin(
|
||||||
|
String telegramUserId, String message) async {
|
||||||
try {
|
try {
|
||||||
final url = Uri.parse('https://api.telegram.org/bot$_botToken/sendMessage');
|
final url =
|
||||||
|
Uri.parse('https://api.telegram.org/bot$_botToken/sendMessage');
|
||||||
|
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
url,
|
url,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue