searchEvents method

Future<({List<Event> events, String? nextBatch, DateTime? searchedUntil})> searchEvents({
  1. String? searchTerm,
  2. bool searchFunc(
    1. Event
    )?,
  3. String? nextBatch,
  4. int limit = 1000,
  5. Set<String> includeEventTypes = const {EventTypes.Message, EventTypes.Encrypted},
})

Performs a search for searchTerm or a more complex searchFunc(). Be aware that the entire room history (limited by limit) will be downloaded, decrypted and then checked which needs a lot of time. If there are events left in the timeline you get a nextBatch back which you can use for the next iteration.

Implementation

Future<({List<Event> events, String? nextBatch, DateTime? searchedUntil})>
searchEvents({
  String? searchTerm,
  bool Function(Event)? searchFunc,
  String? nextBatch,

  /// The amount of events requested from the server in one call. Also used
  /// to chunk database requests.
  int limit = 1000,

  /// Other event types are not requested from the server.
  Set<String> includeEventTypes = const {
    EventTypes.Message,
    EventTypes.Encrypted,
  },
}) async {
  assert(searchTerm != null || searchFunc != null);

  searchFunc ??= (event) =>
      event.body.toLowerCase().contains(searchTerm!.toLowerCase());
  final foundEvents = <Event>[];

  // We first check out what we have in database:
  if (nextBatch == null) {
    List<Event> databaseEvents;
    var start = 0;
    var timelineComplete = false;
    do {
      databaseEvents = await client.database.getEventList(
        this,
        start: start,
        limit: limit,
      );
      start += limit;
      foundEvents.addAll(databaseEvents.where(searchFunc));
      if (databaseEvents.lastOrNull?.type == EventTypes.RoomCreate) {
        timelineComplete = true;
        break;
      }
    } while (databaseEvents.isNotEmpty);

    if (timelineComplete) {
      // We have the complete timeline locally, so we stop here:
      return (
        events: foundEvents,
        nextBatch: null,
        searchedUntil: databaseEvents.lastOrNull?.originServerTs,
      );
    }
  }

  final filter = StateFilter(types: includeEventTypes.toList());
  nextBatch ??= prev_batch;
  // Request `limit` events from server, decrypt and filter them and return.
  final historyResult = await client.getRoomEvents(
    id,
    Direction.b,
    from: nextBatch ?? prev_batch,
    limit: limit,
    filter: jsonEncode(filter.toJson()),
  );
  final events = historyResult.chunk
      .map((matrixEvent) => Event.fromMatrixEvent(matrixEvent, this))
      .toList();

  /// Attempt decrypting the event, if it fails, load the key and try again once.
  /// Returns null if event cannot be decrypted.
  Future<Event?> loadKeysAndDecryptEvent(
    Event event, {
    bool keyAlreadyLoaded = false,
  }) async {
    final decrypted = await client.encryption!.decryptRoomEvent(
      event,
      store: false,
      updateType: EventUpdateType.history,
    );
    if (decrypted.type != EventTypes.Encrypted) {
      return decrypted;
    } else if (!keyAlreadyLoaded) {
      final content = event.parsedRoomEncryptedContent;
      if (content.sessionId != null) {
        await client.encryption!.keyManager.maybeAutoRequest(
          id,
          content.sessionId!,
          content.senderKey,
          tryOnlineBackup: true,
          onlineKeyBackupOnly: true,
          awaitRequest: true,
        );
        return loadKeysAndDecryptEvent(event, keyAlreadyLoaded: true);
      }
    }
    return null;
  }

  // Decrypt all events one after another:
  for (final (index, event) in events.indexed) {
    if (event.type == EventTypes.Encrypted && client.encryption != null) {
      final decrypted = await loadKeysAndDecryptEvent(event);
      if (decrypted != null) {
        events[index] = decrypted;
      }
    }
  }
  events.removeWhere((event) => event.type == EventTypes.Encrypted);
  foundEvents.addAll(events.where(searchFunc));
  return (
    events: foundEvents,
    nextBatch: historyResult.end,
    searchedUntil: historyResult.chunk.lastOrNull?.originServerTs,
  );
}