LCOV - code coverage report
Current view: top level - lib/msc_extensions/extension_timeline_export - timeline_export.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 76.2 % 84 64
Test Date: 2025-04-10 12:13:24 Functions: - 0 0

            Line data    Source code
       1              : import 'dart:convert';
       2              : 
       3              : import 'package:matrix/matrix_api_lite.dart';
       4              : import 'package:matrix/src/event.dart';
       5              : import 'package:matrix/src/timeline.dart';
       6              : 
       7              : extension TimelineExportExtension on Timeline {
       8              :   /// Exports timeline events from a Matrix room within a specified date range.
       9              :   ///
      10              :   /// The export process provides progress updates through the returned stream with the following information:
      11              :   /// - Total number of events exported
      12              :   /// - Count of unable-to-decrypt (UTD) events
      13              :   /// - Count of media events (images, audio, video, files)
      14              :   /// - Number of unique users involved
      15              :   ///
      16              :   /// ```dart
      17              :   /// // Example usage:
      18              :   /// final timeline = room.timeline;
      19              :   /// final oneWeekAgo = DateTime.now().subtract(Duration(days: 7));
      20              :   ///
      21              :   /// // Export last week's messages, excluding encrypted events
      22              :   /// await for (final result in timeline.export(
      23              :   ///   from: oneWeekAgo,
      24              :   ///   filter: (event) => event?.type != EventTypes.Encrypted,
      25              :   /// )) {
      26              :   ///   if (result is ExportProgress) {
      27              :   ///     print('Progress: ${result.totalEvents} events exported');
      28              :   ///   } else if (result is ExportComplete) {
      29              :   ///     print('Export completed with ${result.events.length} events');
      30              :   ///   } else if (result is ExportError) {
      31              :   ///     print('Export failed: ${result.error}');
      32              :   ///   }
      33              :   /// }
      34              :   /// ```
      35              :   ///
      36              :   /// [from] Optional start date to filter events. If null, exports from the beginning.
      37              :   /// [until] Optional end date to filter events. If null, exports up to the latest event.
      38              :   /// [filter] Optional function to filter events. Return true to include the event.
      39              :   /// [requestHistoryCount] Optional. The number of events to request from the server at once.
      40              :   ///
      41              :   /// Returns a [Stream] of [ExportResult] which can be:
      42              :   /// - [ExportProgress]: Provides progress updates during export
      43              :   /// - [ExportComplete]: Contains the final list of exported events
      44              :   /// - [ExportError]: Contains error information if export fails
      45            2 :   Stream<ExportResult> export({
      46              :     DateTime? from,
      47              :     DateTime? until,
      48              :     bool Function(Event)? filter,
      49              :     int requestHistoryCount = 500,
      50              :   }) async* {
      51            2 :     final eventsToExport = <Event>[];
      52              :     var utdEventsCount = 0;
      53              :     var mediaEventsCount = 0;
      54              :     final users = <String>{};
      55              : 
      56              :     try {
      57            2 :       yield ExportProgress(
      58              :         source: ExportSource.timeline,
      59              :         totalEvents: 0,
      60              :         utdEvents: 0,
      61              :         mediaEvents: 0,
      62              :         users: 0,
      63              :       );
      64              : 
      65            2 :       void exportEvent(Event event) {
      66            2 :         eventsToExport.add(event);
      67              : 
      68            4 :         if (event.type == EventTypes.Encrypted &&
      69            4 :             event.messageType == MessageTypes.BadEncrypted) {
      70            2 :           utdEventsCount++;
      71            4 :         } else if (event.type == EventTypes.Message &&
      72              :             {
      73            2 :               MessageTypes.Sticker,
      74            2 :               MessageTypes.Image,
      75            2 :               MessageTypes.Audio,
      76            2 :               MessageTypes.Video,
      77            2 :               MessageTypes.File,
      78            4 :             }.contains(event.messageType)) {
      79            2 :           mediaEventsCount++;
      80              :         }
      81            4 :         users.add(event.senderId);
      82              :       }
      83              : 
      84              :       // From the timeline
      85            8 :       if (until == null || events.last.originServerTs.isBefore(until)) {
      86            4 :         for (final event in events) {
      87            4 :           if (from != null && event.originServerTs.isBefore(from)) break;
      88            4 :           if (until != null && event.originServerTs.isAfter(until)) continue;
      89            0 :           if (filter != null && !filter(event)) continue;
      90            2 :           exportEvent(event);
      91              :         }
      92              :       }
      93            2 :       yield ExportProgress(
      94              :         source: ExportSource.timeline,
      95            2 :         totalEvents: eventsToExport.length,
      96              :         utdEvents: utdEventsCount,
      97              :         mediaEvents: mediaEventsCount,
      98            2 :         users: users.length,
      99              :       );
     100              : 
     101            8 :       if (from != null && events.last.originServerTs.isBefore(from)) {
     102            0 :         yield ExportComplete(
     103              :           events: eventsToExport,
     104            0 :           totalEvents: eventsToExport.length,
     105              :           utdEvents: utdEventsCount,
     106              :           mediaEvents: mediaEventsCount,
     107            0 :           users: users.length,
     108              :         );
     109              :         return;
     110              :       }
     111              : 
     112              :       // From the database
     113            6 :       final eventsFromStore = await room.client.database
     114            8 :               ?.getEventList(room, start: events.length) ??
     115            0 :           [];
     116            2 :       if (eventsFromStore.isNotEmpty) {
     117              :         if (until == null ||
     118            6 :             eventsFromStore.last.originServerTs.isBefore(until)) {
     119            4 :           for (final event in eventsFromStore) {
     120            4 :             if (from != null && event.originServerTs.isBefore(from)) break;
     121            4 :             if (until != null && event.originServerTs.isAfter(until)) continue;
     122            0 :             if (filter != null && !filter(event)) continue;
     123            2 :             exportEvent(event);
     124              :           }
     125              :         }
     126            2 :         yield ExportProgress(
     127              :           source: ExportSource.database,
     128            2 :           totalEvents: eventsToExport.length,
     129              :           utdEvents: utdEventsCount,
     130              :           mediaEvents: mediaEventsCount,
     131            2 :           users: users.length,
     132              :         );
     133              : 
     134              :         if (from != null &&
     135            6 :             eventsFromStore.last.originServerTs.isBefore(from)) {
     136            2 :           yield ExportComplete(
     137              :             events: eventsToExport,
     138            2 :             totalEvents: eventsToExport.length,
     139              :             utdEvents: utdEventsCount,
     140              :             mediaEvents: mediaEventsCount,
     141            2 :             users: users.length,
     142              :           );
     143              :           return;
     144              :         }
     145              :       }
     146              : 
     147              :       // From the server
     148            4 :       var prevBatch = room.prev_batch;
     149            6 :       final encryption = room.client.encryption;
     150              :       do {
     151              :         if (prevBatch == null) break;
     152              :         try {
     153            6 :           final resp = await room.client.getRoomEvents(
     154            4 :             room.id,
     155              :             Direction.b,
     156              :             from: prevBatch,
     157              :             limit: requestHistoryCount,
     158            6 :             filter: jsonEncode(StateFilter(lazyLoadMembers: true).toJson()),
     159              :           );
     160            4 :           if (resp.chunk.isEmpty) break;
     161              : 
     162            4 :           for (final matrixEvent in resp.chunk) {
     163            4 :             var event = Event.fromMatrixEvent(matrixEvent, room);
     164            4 :             if (event.type == EventTypes.Encrypted && encryption != null) {
     165            0 :               event = await encryption.decryptRoomEvent(event);
     166            0 :               if (event.type == EventTypes.Encrypted &&
     167            0 :                   event.messageType == MessageTypes.BadEncrypted &&
     168            0 :                   event.content['can_request_session'] == true) {
     169              :                 // Await requestKey() here to ensure decrypted message bodies
     170            0 :                 await event.requestKey().catchError((_) {});
     171              :               }
     172              :             }
     173            0 :             if (from != null && event.originServerTs.isBefore(from)) break;
     174            0 :             if (until != null && event.originServerTs.isAfter(until)) continue;
     175            0 :             if (filter != null && !filter(event)) continue;
     176            2 :             exportEvent(event);
     177              :           }
     178            2 :           yield ExportProgress(
     179              :             source: ExportSource.server,
     180            2 :             totalEvents: eventsToExport.length,
     181              :             utdEvents: utdEventsCount,
     182              :             mediaEvents: mediaEventsCount,
     183            2 :             users: users.length,
     184              :           );
     185              : 
     186            2 :           prevBatch = resp.end;
     187            6 :           if (resp.chunk.length < requestHistoryCount) break;
     188              : 
     189            0 :           if (from != null && resp.chunk.last.originServerTs.isBefore(from)) {
     190              :             break;
     191              :           }
     192            2 :         } on MatrixException catch (e) {
     193              :           // We have no permission anymore to request the history, so we stop here
     194              :           // and return the events we have so far
     195            4 :           if (e.error == MatrixError.M_FORBIDDEN) {
     196              :             break;
     197              :           }
     198              :           // If it's not a forbidden error, we yield an [ExportError]
     199              :           rethrow;
     200              :         }
     201              :       } while (true);
     202              : 
     203            2 :       yield ExportComplete(
     204              :         events: eventsToExport,
     205            2 :         totalEvents: eventsToExport.length,
     206              :         utdEvents: utdEventsCount,
     207              :         mediaEvents: mediaEventsCount,
     208            2 :         users: users.length,
     209              :       );
     210              :     } catch (e) {
     211            0 :       yield ExportError(
     212            0 :         error: e.toString(),
     213            0 :         totalEvents: eventsToExport.length,
     214              :         utdEvents: utdEventsCount,
     215              :         mediaEvents: mediaEventsCount,
     216            0 :         users: users.length,
     217              :       );
     218              :     }
     219              :   }
     220              : }
     221              : 
     222              : /// Base class for export results
     223              : sealed class ExportResult {
     224              :   /// Total events count
     225              :   final int totalEvents;
     226              : 
     227              :   /// Unable-to-decrypt events count
     228              :   final int utdEvents;
     229              : 
     230              :   /// Media events count
     231              :   final int mediaEvents;
     232              : 
     233              :   /// Users count
     234              :   final int users;
     235              : 
     236            2 :   ExportResult({
     237              :     required this.totalEvents,
     238              :     required this.utdEvents,
     239              :     required this.mediaEvents,
     240              :     required this.users,
     241              :   });
     242              : }
     243              : 
     244              : enum ExportSource {
     245              :   timeline,
     246              :   database,
     247              :   server,
     248              : }
     249              : 
     250              : /// Represents progress during export
     251              : final class ExportProgress extends ExportResult {
     252              :   /// Export source
     253              :   final ExportSource source;
     254              : 
     255            2 :   ExportProgress({
     256              :     required this.source,
     257              :     required super.totalEvents,
     258              :     required super.utdEvents,
     259              :     required super.mediaEvents,
     260              :     required super.users,
     261              :   });
     262              : }
     263              : 
     264              : /// Represents successful completion with exported events
     265              : final class ExportComplete extends ExportResult {
     266              :   final List<Event> events;
     267            2 :   ExportComplete({
     268              :     required this.events,
     269              :     required super.totalEvents,
     270              :     required super.utdEvents,
     271              :     required super.mediaEvents,
     272              :     required super.users,
     273              :   });
     274              : }
     275              : 
     276              : /// Represents an error during export
     277              : final class ExportError extends ExportResult {
     278              :   final String error;
     279            0 :   ExportError({
     280              :     required this.error,
     281              :     required super.totalEvents,
     282              :     required super.utdEvents,
     283              :     required super.mediaEvents,
     284              :     required super.users,
     285              :   });
     286              : }
        

Generated by: LCOV version 2.0-1