fluffychat/lib/pangea/controllers/my_analytics_controller.dart

361 lines
11 KiB
Dart

import 'dart:async';
import 'dart:developer';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/controllers/base_controller.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/analytics_event.dart';
import 'package:fluffychat/pangea/models/constructs_event.dart';
import 'package:fluffychat/pangea/models/constructs_model.dart';
import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart';
import 'package:fluffychat/pangea/models/summary_analytics_event.dart';
import 'package:fluffychat/pangea/models/summary_analytics_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
import '../extensions/client_extension/client_extension.dart';
import '../extensions/pangea_room_extension/pangea_room_extension.dart';
import '../models/constructs_analytics_model.dart';
class MyAnalyticsController extends BaseController {
late PangeaController _pangeaController;
MyAnalyticsController(PangeaController pangeaController) {
_pangeaController = pangeaController;
}
final List<String> analyticsEventTypes = [
PangeaEventTypes.summaryAnalytics,
PangeaEventTypes.construct,
];
Future<void> sendAllAnalyticsEvents(
Room analyticsRoom,
) async {
final String? langCode = analyticsRoom.madeForLang;
if (langCode == null) {
ErrorHandler.logError(
e: "no lang code found for analytics room: ${analyticsRoom.id}",
s: StackTrace.current,
);
return;
}
final Map<String, AnalyticsEvent?> prevEvents = {};
for (final type in analyticsEventTypes) {
final prevEvent = await analyticsRoom.getLastAnalyticsEvent(type);
prevEvents[type] = prevEvent;
}
final List<DateTime?> lastUpdates = prevEvents.values
.map((e) => e?.content.lastUpdated)
.cast<DateTime?>()
.toList();
DateTime? earliestLastUpdated;
if (!lastUpdates.any((updated) => updated == null)) {
earliestLastUpdated = lastUpdates.reduce(
(min, e) => e!.isBefore(min!) ? e : min,
);
}
final List<RecentMessageRecord> analyticsContent = [];
final List<OneConstructUse> constructsContent = [];
for (final Room chat in _studentChats) {
final String? chatLangCode =
_pangeaController.languageController.activeL2Code(roomID: chat.id);
if (chatLangCode != langCode) continue;
final List<PangeaMessageEvent> recentMsgs =
await chat.myMessageEventsInChat(
since: earliestLastUpdated,
);
analyticsContent.addAll(
formatAnalyticsContent(
recentMsgs,
prevEvents[PangeaEventTypes.summaryAnalytics]
as SummaryAnalyticsEvent?,
),
);
constructsContent.addAll(
formatConstructsContent(
recentMsgs,
prevEvents[PangeaEventTypes.construct] as ConstructAnalyticsEvent?,
),
);
}
if (analyticsContent.isNotEmpty) {
final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel(
messages: analyticsContent,
lastUpdated: DateTime.now(),
prevEventId:
prevEvents[PangeaEventTypes.summaryAnalytics]?.event.eventId,
prevLastUpdated:
prevEvents[PangeaEventTypes.summaryAnalytics]?.content.lastUpdated,
);
await analyticsRoom.sendEvent(
analyticsModel.toJson(),
type: PangeaEventTypes.summaryAnalytics,
);
}
if (constructsContent.isNotEmpty) {
final Map<String, List<OneConstructUse>> lemmasUses = {};
for (final use in constructsContent) {
if (use.lemma == null) {
debugPrint("use has no lemma!");
continue;
}
lemmasUses[use.lemma!] ??= [];
lemmasUses[use.lemma]!.add(use);
}
final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel(
type: ConstructType.grammar,
uses: lemmasUses.entries
.map(
(entry) => LemmaConstructsModel(
lemma: entry.key,
uses: entry.value,
),
)
.toList(),
lastUpdated: DateTime.now(),
prevEventId: prevEvents[PangeaEventTypes.construct]?.event.eventId,
prevLastUpdated:
prevEvents[PangeaEventTypes.construct]?.content.lastUpdated,
);
await analyticsRoom.sendEvent(
constructsModel.toJson(),
type: PangeaEventTypes.construct,
);
}
}
List<RecentMessageRecord> formatAnalyticsContent(
List<PangeaMessageEvent> recentMsgs,
SummaryAnalyticsEvent? prevEvent,
) {
List<PangeaMessageEvent> filtered = List.from(recentMsgs);
if (prevEvent?.content.lastUpdated != null) {
filtered = recentMsgs
.where(
(msg) => msg.event.originServerTs.isAfter(
prevEvent!.content.lastUpdated!,
),
)
.toList();
}
final List<String> addedMsgIds =
prevEvent?.content.messages.map((msg) => msg.eventId).toList() ?? [];
filtered.removeWhere((msg) => addedMsgIds.contains(msg.eventId));
final List<RecentMessageRecord> records = filtered
.map(
(msg) => RecentMessageRecord(
eventId: msg.eventId,
chatId: msg.room.id,
useType: msg.useType,
time: msg.originServerTs,
),
)
.toList();
return records;
}
List<OneConstructUse> formatConstructsContent(
List<PangeaMessageEvent> recentMsgs,
ConstructAnalyticsEvent? prevEvent,
) {
List<PangeaMessageEvent> filtered = List.from(recentMsgs);
if (prevEvent?.content.lastUpdated != null) {
filtered = recentMsgs
.where(
(msg) => msg.event.originServerTs.isAfter(
prevEvent!.content.lastUpdated!,
),
)
.toList();
}
final List<String> addedMsgIds = prevEvent?.content.uses
.map((lemmause) => lemmause.uses.map((use) => use.msgId))
.expand((element) => element)
.where((element) => element != null)
.cast<String>()
.toList() ??
[];
filtered.removeWhere((msg) => addedMsgIds.contains(msg.eventId));
final List<OneConstructUse> uses = filtered
.map(
(msg) => msg.originalSent?.choreo?.toGrammarConstructUse(
msg.eventId,
msg.room.id,
msg.originServerTs,
),
)
.where((element) => element != null)
.cast<List<OneConstructUse>>()
.expand((element) => element)
.toList();
return uses;
}
List<Room> _studentChats = [];
Future<void> setStudentChats() async {
final List<String> teacherRoomIds =
await _pangeaController.matrixState.client.teacherRoomIds;
_studentChats = _pangeaController.matrixState.client.rooms
.where(
(r) =>
!r.isSpace &&
!r.isAnalyticsRoom &&
!teacherRoomIds.contains(r.id),
)
.toList();
setState(data: _studentChats);
}
List<Room> get studentChats {
try {
if (_studentChats.isNotEmpty) return _studentChats;
setStudentChats();
return _studentChats;
} catch (err) {
debugger(when: kDebugMode);
return [];
}
}
List<Room> _studentSpaces = [];
Future<void> setStudentSpaces() async {
_studentSpaces = await _pangeaController
.matrixState.client.classesAndExchangesImStudyingIn;
}
List<Room> get studentSpaces {
try {
if (_studentSpaces.isNotEmpty) return _studentSpaces;
setStudentSpaces();
return _studentSpaces;
} catch (err) {
debugger(when: kDebugMode);
return [];
}
}
// on the off chance that the user is in a class but doesn't have an analytics
// room for the target language of that class, create the analytics room(s)
Future<List<Room>> createMissingAnalyticsRoom() async {
List<String> targetLangs = [];
final String? userL2 = _pangeaController.languageController.activeL2Code();
if (userL2 != null) targetLangs.add(userL2);
final List<String?> spaceL2s = studentSpaces
.map(
(space) => _pangeaController.languageController.activeL2Code(
roomID: space.id,
),
)
.toList();
targetLangs.addAll(spaceL2s.where((l2) => l2 != null).cast<String>());
targetLangs = targetLangs.toSet().toList();
for (final String langCode in targetLangs) {
await _pangeaController.matrixState.client.getMyAnalyticsRoom(langCode);
}
return _pangeaController.matrixState.client.allMyAnalyticsRooms;
}
Future<void> updateAnalytics() async {
await setStudentChats();
await setStudentSpaces();
final List<Room> analyticsRooms =
_pangeaController.matrixState.client.allMyAnalyticsRooms;
analyticsRooms.addAll(await createMissingAnalyticsRoom());
for (final Room analyticsRoom in analyticsRooms) {
await sendAllAnalyticsEvents(analyticsRoom);
}
}
// used to aggregate ConstructEvents, from multiple senders (students) with the same lemma
List<AggregateConstructUses> aggregateConstructData(
List<ConstructAnalyticsEvent> constructs,
) {
final Map<String, List<LemmaConstructsModel>> lemmasToConstructs = {};
for (final construct in constructs) {
for (final lemmaUses in construct.content.uses) {
lemmasToConstructs[lemmaUses.lemma] ??= [];
lemmasToConstructs[lemmaUses.lemma]!.add(lemmaUses);
}
}
final List<AggregateConstructUses> aggregatedConstructs = [];
for (final lemmaToConstructs in lemmasToConstructs.entries) {
final List<LemmaConstructsModel> lemmaConstructs =
lemmaToConstructs.value;
final AggregateConstructUses aggregatedData = AggregateConstructUses(
lemmaUses: lemmaConstructs,
);
aggregatedConstructs.add(aggregatedData);
}
return aggregatedConstructs;
}
Future<DateTime?> analyticsLastUpdated(String type) async {
final List<Room> analyticsRooms =
_pangeaController.matrixState.client.allMyAnalyticsRooms;
if (analyticsRooms.isEmpty) return null;
final List<DateTime> lastUpdates = [];
for (final analyticsRoom in analyticsRooms) {
final AnalyticsEvent? lastEvent =
await analyticsRoom.getLastAnalyticsEvent(
type,
);
if (lastEvent?.content.lastUpdated != null) {
lastUpdates.add(lastEvent!.content.lastUpdated!);
}
}
if (lastUpdates.isEmpty) return null;
return lastUpdates.reduce(
(value, element) => value.isAfter(element) ? value : element,
);
}
}
class AggregateConstructUses {
final List<LemmaConstructsModel> _lemmaUses;
AggregateConstructUses({required List<LemmaConstructsModel> lemmaUses})
: _lemmaUses = lemmaUses;
String get lemma {
assert(
_lemmaUses.isNotEmpty &&
_lemmaUses.every(
(construct) => construct.lemma == _lemmaUses.first.lemma,
),
);
return _lemmaUses.first.lemma;
}
List<OneConstructUse> get uses => _lemmaUses
.map((lemmaUse) => lemmaUse.uses)
.expand((element) => element)
.toList();
}