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.8 % 82 63
Test Date: 2025-10-13 02:23:18 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              :       final eventsFromStore =
     114           14 :           await room.client.database.getEventList(room, start: events.length);
     115            2 :       if (eventsFromStore.isNotEmpty) {
     116              :         if (until == null ||
     117            6 :             eventsFromStore.last.originServerTs.isBefore(until)) {
     118            4 :           for (final event in eventsFromStore) {
     119            4 :             if (from != null && event.originServerTs.isBefore(from)) break;
     120            4 :             if (until != null && event.originServerTs.isAfter(until)) continue;
     121            0 :             if (filter != null && !filter(event)) continue;
     122            2 :             exportEvent(event);
     123              :           }
     124              :         }
     125            2 :         yield ExportProgress(
     126              :           source: ExportSource.database,
     127            2 :           totalEvents: eventsToExport.length,
     128              :           utdEvents: utdEventsCount,
     129              :           mediaEvents: mediaEventsCount,
     130            2 :           users: users.length,
     131              :         );
     132              : 
     133              :         if (from != null &&
     134            6 :             eventsFromStore.last.originServerTs.isBefore(from)) {
     135            2 :           yield ExportComplete(
     136              :             events: eventsToExport,
     137            2 :             totalEvents: eventsToExport.length,
     138              :             utdEvents: utdEventsCount,
     139              :             mediaEvents: mediaEventsCount,
     140            2 :             users: users.length,
     141              :           );
     142              :           return;
     143              :         }
     144              :       }
     145              : 
     146              :       // From the server
     147            4 :       var prevBatch = room.prev_batch;
     148            6 :       final encryption = room.client.encryption;
     149              :       do {
     150              :         if (prevBatch == null) break;
     151              :         try {
     152            6 :           final resp = await room.client.getRoomEvents(
     153            4 :             room.id,
     154              :             Direction.b,
     155              :             from: prevBatch,
     156              :             limit: requestHistoryCount,
     157            6 :             filter: jsonEncode(StateFilter(lazyLoadMembers: true).toJson()),
     158              :           );
     159            4 :           if (resp.chunk.isEmpty) break;
     160              : 
     161            4 :           for (final matrixEvent in resp.chunk) {
     162            4 :             var event = Event.fromMatrixEvent(matrixEvent, room);
     163            4 :             if (event.type == EventTypes.Encrypted && encryption != null) {
     164            0 :               event = await encryption.decryptRoomEvent(event);
     165            0 :               if (event.type == EventTypes.Encrypted &&
     166            0 :                   event.messageType == MessageTypes.BadEncrypted &&
     167            0 :                   event.content['can_request_session'] == true) {
     168              :                 // Await requestKey() here to ensure decrypted message bodies
     169            0 :                 await event.requestKey().catchError((_) {});
     170              :               }
     171              :             }
     172            0 :             if (from != null && event.originServerTs.isBefore(from)) break;
     173            0 :             if (until != null && event.originServerTs.isAfter(until)) continue;
     174            0 :             if (filter != null && !filter(event)) continue;
     175            2 :             exportEvent(event);
     176              :           }
     177            2 :           yield ExportProgress(
     178              :             source: ExportSource.server,
     179            2 :             totalEvents: eventsToExport.length,
     180              :             utdEvents: utdEventsCount,
     181              :             mediaEvents: mediaEventsCount,
     182            2 :             users: users.length,
     183              :           );
     184              : 
     185            2 :           prevBatch = resp.end;
     186            6 :           if (resp.chunk.length < requestHistoryCount) break;
     187              : 
     188            0 :           if (from != null && resp.chunk.last.originServerTs.isBefore(from)) {
     189              :             break;
     190              :           }
     191            2 :         } on MatrixException catch (e) {
     192              :           // We have no permission anymore to request the history, so we stop here
     193              :           // and return the events we have so far
     194            4 :           if (e.error == MatrixError.M_FORBIDDEN) {
     195              :             break;
     196              :           }
     197              :           // If it's not a forbidden error, we yield an [ExportError]
     198              :           rethrow;
     199              :         }
     200              :       } while (true);
     201              : 
     202            2 :       yield ExportComplete(
     203              :         events: eventsToExport,
     204            2 :         totalEvents: eventsToExport.length,
     205              :         utdEvents: utdEventsCount,
     206              :         mediaEvents: mediaEventsCount,
     207            2 :         users: users.length,
     208              :       );
     209              :     } catch (e) {
     210            0 :       yield ExportError(
     211            0 :         error: e.toString(),
     212            0 :         totalEvents: eventsToExport.length,
     213              :         utdEvents: utdEventsCount,
     214              :         mediaEvents: mediaEventsCount,
     215            0 :         users: users.length,
     216              :       );
     217              :     }
     218              :   }
     219              : }
     220              : 
     221              : /// Base class for export results
     222              : sealed class ExportResult {
     223              :   /// Total events count
     224              :   final int totalEvents;
     225              : 
     226              :   /// Unable-to-decrypt events count
     227              :   final int utdEvents;
     228              : 
     229              :   /// Media events count
     230              :   final int mediaEvents;
     231              : 
     232              :   /// Users count
     233              :   final int users;
     234              : 
     235            2 :   ExportResult({
     236              :     required this.totalEvents,
     237              :     required this.utdEvents,
     238              :     required this.mediaEvents,
     239              :     required this.users,
     240              :   });
     241              : }
     242              : 
     243              : enum ExportSource {
     244              :   timeline,
     245              :   database,
     246              :   server,
     247              : }
     248              : 
     249              : /// Represents progress during export
     250              : final class ExportProgress extends ExportResult {
     251              :   /// Export source
     252              :   final ExportSource source;
     253              : 
     254            2 :   ExportProgress({
     255              :     required this.source,
     256              :     required super.totalEvents,
     257              :     required super.utdEvents,
     258              :     required super.mediaEvents,
     259              :     required super.users,
     260              :   });
     261              : }
     262              : 
     263              : /// Represents successful completion with exported events
     264              : final class ExportComplete extends ExportResult {
     265              :   final List<Event> events;
     266            2 :   ExportComplete({
     267              :     required this.events,
     268              :     required super.totalEvents,
     269              :     required super.utdEvents,
     270              :     required super.mediaEvents,
     271              :     required super.users,
     272              :   });
     273              : }
     274              : 
     275              : /// Represents an error during export
     276              : final class ExportError extends ExportResult {
     277              :   final String error;
     278            0 :   ExportError({
     279              :     required this.error,
     280              :     required super.totalEvents,
     281              :     required super.utdEvents,
     282              :     required super.mediaEvents,
     283              :     required super.users,
     284              :   });
     285              : }
        

Generated by: LCOV version 2.0-1