Skip to content
Snippets Groups Projects
Utils.cpp 73.3 KiB
Newer Older
  • Learn to ignore specific revisions
  •     case MsgType::Audio: {
    
            return QStringLiteral("sent an audio file.");
    
        }
        case MsgType::Video: {
    
            return QStringLiteral("sent a video");
    
        }
        default: {
            return related.quoted_body;
        }
        }
    
    QString
    utils::linkColor()
    {
    
        const auto theme = UserSettings::instance()->theme();
    
        if (theme == QLatin1String("light")) {
            return QStringLiteral("#0077b5");
        } else if (theme == QLatin1String("dark")) {
            return QStringLiteral("#38A3D8");
    
        } else {
            return QPalette().color(QPalette::Link).name();
        }
    
    utils::hashQString(const QString &input)
    
        auto h = QCryptographicHash::hash(input.toUtf8(), QCryptographicHash::Sha1);
    
        return (static_cast<uint32_t>(h[0]) << 24) ^ (static_cast<uint32_t>(h[1]) << 16) ^
               (static_cast<uint32_t>(h[2]) << 8) ^ static_cast<uint32_t>(h[3]);
    
    QColor
    utils::generateContrastingHexColor(const QString &input, const QColor &backgroundCol)
    
        const qreal backgroundLum = luminance(backgroundCol);
    
        // Create a color for the input
        auto hash = hashQString(input);
        // create a hue value based on the hash of the input.
    
        // Adapted to make Nico blue
    
    Nicolas Werner's avatar
    Nicolas Werner committed
        auto userHue = static_cast<double>(hash - static_cast<uint32_t>(0x60'00'00'00)) /
                       std::numeric_limits<uint32_t>::max() * 360.;
    
        // start with moderate saturation and lightness values.
    
    Nicolas Werner's avatar
    Nicolas Werner committed
        auto sat       = 230.;
        auto lightness = 125.;
    
    
        // converting to a QColor makes the luminance calc easier.
    
    Nicolas Werner's avatar
    Nicolas Werner committed
        QColor inputColor = QColor::fromHsl(
          static_cast<int>(userHue), static_cast<int>(sat), static_cast<int>(lightness));
    
    
        // calculate the initial luminance and contrast of the
        // generated color.  It's possible that no additional
        // work will be necessary.
        auto lum      = luminance(inputColor);
        auto contrast = computeContrast(lum, backgroundLum);
    
        // If the contrast doesn't meet our criteria,
        // try again and again until they do by modifying first
        // the lightness and then the saturation of the color.
        int iterationCount = 9;
    
        while (contrast < 4.5) {
    
            // if our lightness is at it's bounds, try changing
            // saturation instead.
    
            if (lightness >= 242 || lightness <= 13) {
    
                qreal newSat = qBound(26.0, sat * 1.25, 242.0);
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                inputColor.setHsl(static_cast<int>(userHue),
                                  static_cast<int>(qFloor(newSat)),
                                  static_cast<int>(lightness));
    
                auto tmpLum         = luminance(inputColor);
                auto higherContrast = computeContrast(tmpLum, backgroundLum);
                if (higherContrast > contrast) {
                    contrast = higherContrast;
                    sat      = newSat;
                } else {
                    newSat = qBound(26.0, sat / 1.25, 242.0);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    inputColor.setHsl(static_cast<int>(userHue),
                                      static_cast<int>(qFloor(newSat)),
                                      static_cast<int>(lightness));
    
                    tmpLum             = luminance(inputColor);
                    auto lowerContrast = computeContrast(tmpLum, backgroundLum);
                    if (lowerContrast > contrast) {
                        contrast = lowerContrast;
                        sat      = newSat;
    
                }
            } else {
                qreal newLightness = qBound(13.0, lightness * 1.25, 242.0);
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                inputColor.setHsl(static_cast<int>(userHue),
                                  static_cast<int>(sat),
                                  static_cast<int>(qFloor(newLightness)));
    
    
                auto tmpLum         = luminance(inputColor);
                auto higherContrast = computeContrast(tmpLum, backgroundLum);
    
                // Check to make sure we have actually improved contrast
                if (higherContrast > contrast) {
                    contrast  = higherContrast;
                    lightness = newLightness;
                    // otherwise, try going the other way instead.
                } else {
                    newLightness = qBound(13.0, lightness / 1.25, 242.0);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    inputColor.setHsl(static_cast<int>(userHue),
                                      static_cast<int>(sat),
                                      static_cast<int>(qFloor(newLightness)));
    
                    tmpLum             = luminance(inputColor);
                    auto lowerContrast = computeContrast(tmpLum, backgroundLum);
                    if (lowerContrast > contrast) {
                        contrast  = lowerContrast;
                        lightness = newLightness;
                    }
                }
    
            // don't loop forever, just give up at some point!
            // Someone smart may find a better solution
            if (--iterationCount < 0)
                break;
        }
    
        // get the hex value of the generated color.
        auto colorHex = inputColor.name();
    
        return colorHex;
    
    }
    
    qreal
    utils::computeContrast(const qreal &one, const qreal &two)
    {
    
        auto ratio = (one + 0.05) / (two + 0.05);
    
        if (two > one) {
            ratio = 1 / ratio;
        }
    
        return ratio;
    
        int colRgb[3] = {col.red(), col.green(), col.blue()};
        qreal lumRgb[3];
    
        for (int i = 0; i < 3; i++) {
            qreal v   = colRgb[i] / 255.0;
            lumRgb[i] = v <= 0.03928 ? v / 12.92 : qPow((v + 0.055) / 1.055, 2.4);
        }
    
        auto lum = lumRgb[0] * 0.2126 + lumRgb[1] * 0.7152 + lumRgb[2] * 0.0722;
    
        return lum;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    utils::centerWidget(QWidget *widget, QWindow *parent)
    
        if (parent) {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            widget->window()->windowHandle()->setTransientParent(parent);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
        auto findCenter = [childRect = widget->rect()](QRect hostRect) -> QPoint {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            return QPoint(static_cast<int>(hostRect.center().x() - (childRect.width() * 0.5)),
                          static_cast<int>(hostRect.center().y() - (childRect.height() * 0.5)));
    
        widget->move(findCenter(QGuiApplication::primaryScreen()->geometry()));
    
    
    void
    utils::restoreCombobox(QComboBox *combo, const QString &value)
    {
    
        for (auto i = 0; i < combo->count(); ++i) {
            if (value == combo->itemText(i)) {
                combo->setCurrentIndex(i);
                break;
    
    QImage
    utils::readImageFromFile(const QString &filename)
    {
    
        QImageReader reader(filename);
        reader.setAutoTransform(true);
        return reader.read();
    
    utils::readImage(const QByteArray &data)
    
        QBuffer buf;
        buf.setData(data);
        QImageReader reader(&buf);
        reader.setAutoTransform(true);
        return reader.read();
    
    
    bool
    utils::isReply(const mtx::events::collections::TimelineEvents &e)
    {
    
        return mtx::accessors::relations(e).reply_to().has_value();
    
    
    void
    utils::removeDirectFromRoom(QString roomid)
    {
        http::client()->get_account_data<mtx::events::account_data::Direct>(
          [roomid](mtx::events::account_data::Direct ev, mtx::http::RequestErr e) {
              if (e && e->status_code == 404)
                  ev = {};
              else if (e) {
                  nhlog::net()->error("Failed to retrieve m.direct: {}", *e);
                  return;
              }
    
              auto r = roomid.toStdString();
    
              for (auto it = ev.user_to_rooms.begin(); it != ev.user_to_rooms.end();) {
                  for (auto rit = it->second.begin(); rit != it->second.end();) {
                      if (r == *rit)
                          rit = it->second.erase(rit);
                      else
                          ++rit;
                  }
    
                  if (it->second.empty())
                      it = ev.user_to_rooms.erase(it);
                  else
                      ++it;
              }
    
              http::client()->put_account_data(ev, [r](mtx::http::RequestErr e) {
                  if (e)
                      nhlog::net()->error("Failed to update m.direct: {}", *e);
              });
          });
    }
    void
    utils::markRoomAsDirect(QString roomid, std::vector<RoomMember> members)
    {
        http::client()->get_account_data<mtx::events::account_data::Direct>(
          [roomid, members](mtx::events::account_data::Direct ev, mtx::http::RequestErr e) {
              if (e && e->status_code == 404)
                  ev = {};
              else if (e) {
                  nhlog::net()->error("Failed to retrieve m.direct: {}", *e);
                  return;
              }
    
              auto local = utils::localUser();
              auto r     = roomid.toStdString();
    
              for (const auto &m : members) {
                  if (m.user_id != local) {
                      ev.user_to_rooms[m.user_id.toStdString()].push_back(r);
                  }
              }
    
              http::client()->put_account_data(ev, [r](mtx::http::RequestErr e) {
                  if (e)
                      nhlog::net()->error("Failed to update m.direct: {}", *e);
              });
          });
    }
    
    
    std::vector<std::string>
    utils::roomVias(const std::string &roomid)
    {
        std::vector<std::string> vias;
    
    
        // for joined rooms
    
            // see https://spec.matrix.org/v1.6/appendices/#routing for the algorithm
    
            auto members = cache::roomMembers(roomid);
    
            if (!members.empty()) {
    
                auto powerlevels =
                  cache::client()->getStateEvent<mtx::events::state::PowerLevels>(roomid).value_or(
                    mtx::events::StateEvent<mtx::events::state::PowerLevels>{});
    
                auto acls = cache::client()->getStateEvent<mtx::events::state::ServerAcl>(roomid);
    
                std::vector<QRegularExpression> allowedServers;
                std::vector<QRegularExpression> deniedServers;
    
                if (acls) {
                    auto globToRegexp = [](const std::string &globExp) {
                        auto rawReg = QRegularExpression::escape(QString::fromStdString(globExp))
                                        .replace("\\*", ".*")
                                        .replace("\\?", ".");
                        return QRegularExpression(QRegularExpression::anchoredPattern(rawReg),
                                                  QRegularExpression::DotMatchesEverythingOption |
                                                    QRegularExpression::DontCaptureOption);
                    };
    
                    allowedServers.reserve(acls->content.allow.size());
                    for (const auto &s : acls->content.allow)
                        allowedServers.push_back(globToRegexp(s));
                    deniedServers.reserve(acls->content.deny.size());
                    for (const auto &s : acls->content.deny)
                        allowedServers.push_back(globToRegexp(s));
                }
    
                auto isHostAllowed = [&acls, &allowedServers, &deniedServers](const std::string &host) {
                    if (!acls)
                        return true;
    
                    auto url = QUrl::fromEncoded(
                      "https://" + QByteArray::fromRawData(host.data(), host.size()), QUrl::StrictMode);
                    if (url.hasQuery() || url.hasFragment())
                        return false;
    
                    auto hostname = url.host();
    
                    for (const auto &d : deniedServers)
                        if (d.match(hostname).hasMatch())
                            return false;
                    for (const auto &a : allowedServers)
                        if (a.match(hostname).hasMatch())
                            return true;
    
                    return false;
                };
    
    
                std::unordered_set<std::string> users_with_high_pl;
                std::set<std::string> users_with_high_pl_in_room;
                // we should pick PL > 50, but imo that is broken, so we just pick users who have admins
                // perm
                for (const auto &user : powerlevels.content.users) {
                    if (user.second >= powerlevels.content.events_default &&
                        user.second >= powerlevels.content.state_default) {
    
                        auto host =
                          mtx::identifiers::parse<mtx::identifiers::User>(user.first).hostname();
                        if (isHostAllowed(host))
                            users_with_high_pl.insert(user.first);
    
                    }
                }
    
                std::unordered_map<std::string, std::size_t> usercount_by_server;
    
                for (const auto &m : members) {
    
                    auto user_id = mtx::identifiers::parse<mtx::identifiers::User>(m);
                    usercount_by_server[user_id.hostname()] += 1;
    
                    if (users_with_high_pl.count(m))
                        users_with_high_pl_in_room.insert(m);
                }
    
                std::erase_if(usercount_by_server, [&isHostAllowed](const auto &item) {
                    return !isHostAllowed(item.first);
                });
    
    
                // add the highest powerlevel user
                auto max_pl_user = std::max_element(
                  users_with_high_pl_in_room.begin(),
                  users_with_high_pl_in_room.end(),
                  [&pl_content = powerlevels.content](const std::string &a, const std::string &b) {
                      return pl_content.user_level(a) < pl_content.user_level(b);
                  });
                if (max_pl_user != users_with_high_pl_in_room.end()) {
                    auto host =
                      mtx::identifiers::parse<mtx::identifiers::User>(*max_pl_user).hostname();
                    vias.push_back(host);
                    usercount_by_server.erase(host);
                }
    
                // add up to 3 users, by usercount size from that server
                std::vector<std::pair<std::size_t, std::string>> servers_sorted_by_usercount;
                servers_sorted_by_usercount.reserve(usercount_by_server.size());
                for (const auto &[server, count] : usercount_by_server)
                    servers_sorted_by_usercount.emplace_back(count, server);
    
                std::sort(servers_sorted_by_usercount.begin(),
                          servers_sorted_by_usercount.end(),
                          [](const auto &a, const auto &b) {
                              if (a.first == b.first)
                                  // same pl, sort lex smaller server first
                                  return a.second < b.second;
    
                              // sort high user count first
                              return a.first > b.first;
                          });
    
                for (const auto &server : servers_sorted_by_usercount) {
                    if (vias.size() >= 3)
                        break;
    
                    vias.push_back(server.second);
    
            auto members = cache::getMembersFromInvite(roomid, 0, 100);
            if (!members.empty()) {
                vias.push_back(http::client()->user_id().hostname());
                for (const auto &m : members) {
    
                    if (vias.size() >= 3)
    
                        break;
    
                    auto user_id =
                      mtx::identifiers::parse<mtx::identifiers::User>(m.user_id.toStdString());
    
                    auto server = user_id.hostname();
                    if (std::find(begin(vias), end(vias), server) == vias.end())
                        vias.push_back(server);
                }
    
        // for space previews
        auto parents = cache::client()->getParentRoomIds(roomid);
        for (const auto &p : parents) {
            auto child = cache::client()->getStateEvent<mtx::events::state::space::Child>(p, roomid);
            if (child && child->content.via)
                vias.insert(vias.end(), child->content.via->begin(), child->content.via->end());
        }
    
        std::sort(begin(vias), end(vias));
        auto last = std::unique(begin(vias), end(vias));
        vias.erase(last, end(vias));
    
    
    void
    utils::updateSpaceVias()
    {
        if (!UserSettings::instance()->updateSpaceVias())
            return;
    
        nhlog::net()->info("update space vias called");
    
        auto rooms = cache::roomInfo(false);
    
        auto us = http::client()->user_id().to_string();
    
        auto weekAgo = (uint64_t)QDateTime::currentDateTime().addDays(-7).toMSecsSinceEpoch();
    
        struct ApplySpaceUpdatesState
        {
            std::vector<mtx::events::StateEvent<mtx::events::state::space::Child>> childrenToUpdate;
            std::vector<mtx::events::StateEvent<mtx::events::state::space::Parent>> parentsToUpdate;
    
            static void next(std::shared_ptr<ApplySpaceUpdatesState> state)
            {
                if (!state->childrenToUpdate.empty()) {
                    const auto &child = state->childrenToUpdate.back();
    
                    http::client()->send_state_event(
                      child.room_id,
                      child.state_key,
                      child.content,
                      [state = std::move(state)](const mtx::responses::EventId &,
                                                 mtx::http::RequestErr e) mutable {
                          const auto &child_ = state->childrenToUpdate.back();
                          if (e) {
                              if (e->status_code == 429 && e->matrix_error.retry_after.count() != 0) {
                                  ChatPage::instance()->callFunctionOnGuiThread(
                                    [state    = std::move(state),
                                     interval = e->matrix_error.retry_after]() {
                                        QTimer::singleShot(interval,
                                                           ChatPage::instance(),
                                                           [self = std::move(state)]() mutable {
                                                               next(std::move(self));
                                                           });
                                    });
                                  return;
                              }
    
                              nhlog::net()->error("Failed to update space child {} -> {}: {}",
                                                  child_.room_id,
                                                  child_.state_key,
                                                  *e);
                          }
                          nhlog::net()->info(
                            "Updated space child {} -> {}", child_.room_id, child_.state_key);
                          state->childrenToUpdate.pop_back();
                          next(std::move(state));
                      });
                    return;
                } else if (!state->parentsToUpdate.empty()) {
                    const auto &parent = state->parentsToUpdate.back();
    
                    http::client()->send_state_event(
                      parent.room_id,
                      parent.state_key,
                      parent.content,
                      [state = std::move(state)](const mtx::responses::EventId &,
                                                 mtx::http::RequestErr e) mutable {
                          const auto &parent_ = state->parentsToUpdate.back();
                          if (e) {
                              if (e->status_code == 429 && e->matrix_error.retry_after.count() != 0) {
                                  ChatPage::instance()->callFunctionOnGuiThread(
                                    [state    = std::move(state),
                                     interval = e->matrix_error.retry_after]() {
                                        QTimer::singleShot(interval,
                                                           ChatPage::instance(),
                                                           [self = std::move(state)]() mutable {
                                                               next(std::move(self));
                                                           });
                                    });
                                  return;
                              }
    
                              nhlog::net()->error("Failed to update space parent {} -> {}: {}",
                                                  parent_.room_id,
                                                  parent_.state_key,
                                                  *e);
                          }
                          nhlog::net()->info(
                            "Updated space parent {} -> {}", parent_.room_id, parent_.state_key);
                          state->parentsToUpdate.pop_back();
                          next(std::move(state));
                      });
                    return;
                }
            }
        };
    
        auto asus = std::make_shared<ApplySpaceUpdatesState>();
    
        for (const auto &[roomid, info] : rooms.toStdMap()) {
            if (!info.is_space)
                continue;
    
            auto spaceid = roomid.toStdString();
    
            if (auto pl = cache::client()
                            ->getStateEvent<mtx::events::state::PowerLevels>(spaceid)
                            .value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
                            .content;
                pl.user_level(us) < pl.state_level(to_string(mtx::events::EventType::SpaceChild)))
                continue;
    
            auto children = cache::client()->getChildRoomIds(spaceid);
    
            for (const auto &childid : children) {
                // only update children we are joined to
                if (!rooms.contains(QString::fromStdString(childid)))
                    continue;
    
                auto child =
                  cache::client()->getStateEvent<mtx::events::state::space::Child>(spaceid, childid);
                if (child &&
                    // don't update too often
                    child->origin_server_ts < weekAgo &&
                    // ignore unset spaces
                    (child->content.via && !child->content.via->empty())) {
                    auto newVias = utils::roomVias(childid);
    
                    if (!newVias.empty() && newVias != child->content.via) {
                        nhlog::net()->info("Will update {} -> {} child relation from {} to {}",
                                           spaceid,
                                           childid,
                                           fmt::join(*child->content.via, ","),
                                           fmt::join(newVias, ","));
    
                        child->content.via = std::move(newVias);
                        child->room_id     = spaceid;
                        asus->childrenToUpdate.push_back(*std::move(child));
                    }
                }
    
                auto parent =
                  cache::client()->getStateEvent<mtx::events::state::space::Parent>(childid, spaceid);
                if (parent &&
                    // don't update too often
                    parent->origin_server_ts < weekAgo &&
                    // ignore unset spaces
                    (parent->content.via && !parent->content.via->empty())) {
                    if (auto pl =
                          cache::client()
                            ->getStateEvent<mtx::events::state::PowerLevels>(childid)
                            .value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
                            .content;
                        pl.user_level(us) <
                        pl.state_level(to_string(mtx::events::EventType::SpaceParent)))
                        continue;
    
                    auto newVias = utils::roomVias(spaceid);
    
                    if (!newVias.empty() && newVias != parent->content.via) {
                        nhlog::net()->info("Will update {} -> {} parent relation from {} to {}",
                                           childid,
                                           spaceid,
                                           fmt::join(*parent->content.via, ","),
                                           fmt::join(newVias, ","));
    
                        parent->content.via = std::move(newVias);
                        parent->room_id     = childid;
                        asus->parentsToUpdate.push_back(*std::move(parent));
                    }
                }
            }
        }
    
        ApplySpaceUpdatesState::next(std::move(asus));
    }
    
    
    std::atomic<bool> event_expiration_running = false;
    void
    utils::removeExpiredEvents()
    {
    
        if (!UserSettings::instance()->expireEvents())
    
            return;
    
        if (event_expiration_running.exchange(true)) {
            nhlog::net()->info("Event expiration still running, not starting second job.");
            return;
        }
    
        nhlog::net()->info("Remove expired events starting.");
    
        auto rooms = cache::roomInfo(false);
    
        auto us = http::client()->user_id().to_string();
    
        using ExpType =
          mtx::events::AccountDataEvent<mtx::events::account_data::nheko_extensions::EventExpiry>;
        static auto getExpEv = [](const std::string &room = "") -> std::optional<ExpType> {
            if (auto accountEvent =
                  cache::client()->getAccountData(mtx::events::EventType::NhekoEventExpiry, room))
                if (auto ev = std::get_if<ExpType>(&*accountEvent);
                    ev && (ev->content.expire_after_ms || ev->content.keep_only_latest))
                    return std::optional{*ev};
            return std::nullopt;
        };
    
        struct ApplyEventExpiration
        {
            std::optional<ExpType> globalExpiry;
            std::vector<std::string> roomsToUpdate;
            std::string filter;
    
            std::string currentRoom;
    
            std::uint64_t currentRoomCount = 0;
            std::string currentRoomPrevToken;
    
            std::set<std::pair<std::string, std::string>> currentRoomStateEvents;
    
            std::vector<std::string> currentRoomRedactionQueue;
            mtx::events::account_data::nheko_extensions::EventExpiry currentExpiry;
    
            static void next(std::shared_ptr<ApplyEventExpiration> state)
            {
                if (!state->currentRoomRedactionQueue.empty()) {
    
                    auto evid = state->currentRoomRedactionQueue.back();
                    auto room = state->currentRoom;
    
                    http::client()->redact_event(
    
                      room,
                      evid,
                      [state = std::move(state), evid](const mtx::responses::EventId &,
                                                       mtx::http::RequestErr e) mutable {
    
                          if (e) {
                              if (e->status_code == 429 && e->matrix_error.retry_after.count() != 0) {
                                  ChatPage::instance()->callFunctionOnGuiThread(
                                    [state    = std::move(state),
                                     interval = e->matrix_error.retry_after]() {
                                        QTimer::singleShot(interval,
                                                           ChatPage::instance(),
                                                           [self = std::move(state)]() mutable {
                                                               next(std::move(self));
                                                           });
                                    });
                                  return;
    
                              } else {
                                  nhlog::net()->error("Failed to redact event {} in {}: {}",
                                                      evid,
                                                      state->currentRoom,
                                                      *e);
                                  state->currentRoomRedactionQueue.pop_back();
                                  next(std::move(state));
    
                          } else {
                              nhlog::net()->info("Redacted event {} in {}", evid, state->currentRoom);
                              state->currentRoomRedactionQueue.pop_back();
                              next(std::move(state));
    
                          }
                      });
                } else if (!state->currentRoom.empty()) {
    
                    if (state->currentRoomPrevToken.empty() && !state->firstMessagesCall) {
                        nhlog::net()->info("Finished room {}", state->currentRoom);
                        state->currentRoom.clear();
                        next(std::move(state));
                        return;
                    }
    
    
                    mtx::http::MessagesOpts opts{};
    
                    opts.dir     = mtx::http::PaginationDirection::Backwards;
                    opts.from    = state->currentRoomPrevToken;
                    opts.limit   = 1000;
                    opts.filter  = state->filter;
    
                    opts.room_id = state->currentRoom;
    
                    http::client()->messages(
                      opts,
                      [state = std::move(state)](const mtx::responses::Messages &msgs,
    
                                                 mtx::http::RequestErr error) mutable {
    
                          if (error) {
                              // skip success handler
                              nhlog::net()->info(
                                "Finished room {} with error {}", state->currentRoom, *error);
    
                              state->currentRoom.clear();
    
                              state->currentRoomPrevToken.clear();
                          } else {
    
    
                              auto now = (uint64_t)QDateTime::currentMSecsSinceEpoch();
                              auto us  = http::client()->user_id().to_string();
    
                              for (const auto &e : msgs.chunk) {
                                  if (std::holds_alternative<
                                        mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(e))
                                      continue;
    
    
                                  if (std::holds_alternative<
                                        mtx::events::RoomEvent<mtx::events::msg::Redacted>>(e))
                                      continue;
    
                                  if (std::holds_alternative<
                                        mtx::events::StateEvent<mtx::events::msg::Redacted>>(e))
                                      continue;
    
    
                                  // synapse protects these 2 against redaction
                                  if (std::holds_alternative<
                                        mtx::events::StateEvent<mtx::events::state::Create>>(e))
                                      continue;
    
                                  if (std::holds_alternative<
                                        mtx::events::StateEvent<mtx::events::state::ServerAcl>>(e))
                                      continue;
    
    
                                  // skip events we don't know to protect us from mistakes.
                                  if (std::holds_alternative<
                                        mtx::events::RoomEvent<mtx::events::Unknown>>(e))
                                      continue;
    
    
                                  if (mtx::accessors::sender(e) != us)
                                      continue;
    
                                  state->currentRoomCount++;
                                  if (state->currentRoomCount <= state->currentExpiry.protect_latest) {
                                      continue;
                                  }
    
                                  if (state->currentExpiry.exclude_state_events &&
                                      mtx::accessors::is_state_event(e))
                                      continue;
    
    
                                  if (mtx::accessors::is_state_event(e)) {
                                      // skip the first state event of a type
                                      if (std::visit(
                                            [&state](const auto &se) {
                                                if constexpr (requires { se.state_key; })
                                                    return state->currentRoomStateEvents
                                                      .emplace(to_string(se.type), se.state_key)
                                                      .second;
                                                else
    
                                  if (state->currentExpiry.keep_only_latest &&
                                      state->currentRoomCount > state->currentExpiry.keep_only_latest) {
                                      state->currentRoomRedactionQueue.push_back(
                                        mtx::accessors::event_id(e));
                                  } else if (state->currentExpiry.expire_after_ms &&
                                             (state->currentExpiry.expire_after_ms +
                                              mtx::accessors::origin_server_ts(e).toMSecsSinceEpoch()) <
                                               now) {
                                      state->currentRoomRedactionQueue.push_back(
                                        mtx::accessors::event_id(e));
                                  }
                              }
                          }
    
                          next(std::move(state));
                      });
                } else if (!state->roomsToUpdate.empty()) {
                    const auto &room = state->roomsToUpdate.back();
    
                    auto localExp = getExpEv(room);
                    if (localExp) {
                        state->currentRoom   = room;
                        state->currentExpiry = localExp->content;
                    } else if (state->globalExpiry) {
                        state->currentRoom   = room;
                        state->currentExpiry = state->globalExpiry->content;
                    }
    
                    state->firstMessagesCall    = true;
                    state->currentRoomCount     = 0;
                    state->currentRoomPrevToken = "";
                    state->currentRoomRedactionQueue.clear();
                    state->currentRoomStateEvents.clear();
    
    
                    state->roomsToUpdate.pop_back();
                    next(std::move(state));
                } else {
                    nhlog::net()->info("Finished event expiry");
                    event_expiration_running = false;
                }
            }
        };
    
        auto asus = std::make_shared<ApplyEventExpiration>();
    
    
        nlohmann::json filter;
        filter["timeline"]["senders"]   = nlohmann::json::array({us});
        filter["timeline"]["not_types"] = nlohmann::json::array({"m.room.redaction"});
    
        asus->filter = filter.dump();
    
    
        asus->globalExpiry = getExpEv();
    
        for (const auto &[roomid_, info] : rooms.toStdMap()) {
            auto roomid = roomid_.toStdString();
    
            if (!asus->globalExpiry && !getExpEv(roomid))
                continue;
    
            if (auto pl = cache::client()
                            ->getStateEvent<mtx::events::state::PowerLevels>(roomid)
                            .value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
                            .content;
                pl.user_level(us) < pl.event_level(to_string(mtx::events::EventType::RoomRedaction))) {
                nhlog::net()->warn("Can't react events in {}, not running expiration.", roomid);
                continue;
            }
    
            asus->roomsToUpdate.push_back(roomid);
        }
    
        nhlog::db()->info("Running expiration in {} rooms", asus->roomsToUpdate.size());
    
        ApplyEventExpiration::next(std::move(asus));
    }