// This is the source code of AyuGram for Desktop. // // We do not and cannot prevent the use of our code, // but be respectful and credit the original author. // // Copyright @Radolyn, 2026 #include "ayu/data/ayu_database.h" #include "ayu/data/entities.h" #include "ayu/libs/sqlite/sqlite_orm.h" #include "base/unixtime.h" using namespace sqlite_orm; auto storage = make_storage( "./tdata/ayudata.db", make_table( "SchemaVersion", make_column("id", &SchemaVersion::id, primary_key()), make_column("version", &SchemaVersion::version) ), make_index("idx_deleted_message_userId_dialogId_topicId_messageId", column(&DeletedMessage::userId), column(&DeletedMessage::dialogId), column(&DeletedMessage::topicId), column(&DeletedMessage::messageId)), make_index("idx_edited_message_userId_dialogId_messageId", column(&EditedMessage::userId), column(&EditedMessage::dialogId), column(&EditedMessage::messageId)), make_table( "DeletedMessage", make_column("fakeId", &DeletedMessage::fakeId, primary_key().autoincrement()), make_column("userId", &DeletedMessage::userId), make_column("dialogId", &DeletedMessage::dialogId), make_column("groupedId", &DeletedMessage::groupedId), make_column("peerId", &DeletedMessage::peerId), make_column("fromId", &DeletedMessage::fromId), make_column("topicId", &DeletedMessage::topicId), make_column("messageId", &DeletedMessage::messageId), make_column("date", &DeletedMessage::date), make_column("flags", &DeletedMessage::flags), make_column("editDate", &DeletedMessage::editDate), make_column("views", &DeletedMessage::views), make_column("fwdFlags", &DeletedMessage::fwdFlags), make_column("fwdFromId", &DeletedMessage::fwdFromId), make_column("fwdName", &DeletedMessage::fwdName), make_column("fwdDate", &DeletedMessage::fwdDate), make_column("fwdPostAuthor", &DeletedMessage::fwdPostAuthor), make_column("replyFlags", &DeletedMessage::replyFlags), make_column("replyMessageId", &DeletedMessage::replyMessageId), make_column("replyPeerId", &DeletedMessage::replyPeerId), make_column("replyTopId", &DeletedMessage::replyTopId), make_column("replyForumTopic", &DeletedMessage::replyForumTopic), make_column("replySerialized", &DeletedMessage::replySerialized), make_column("entityCreateDate", &DeletedMessage::entityCreateDate), make_column("text", &DeletedMessage::text), make_column("textEntities", &DeletedMessage::textEntities), make_column("mediaPath", &DeletedMessage::mediaPath), make_column("hqThumbPath", &DeletedMessage::hqThumbPath), make_column("documentType", &DeletedMessage::documentType), make_column("documentSerialized", &DeletedMessage::documentSerialized), make_column("thumbsSerialized", &DeletedMessage::thumbsSerialized), make_column("documentAttributesSerialized", &DeletedMessage::documentAttributesSerialized), make_column("mimeType", &DeletedMessage::mimeType) ), make_table( "EditedMessage", make_column("fakeId", &EditedMessage::fakeId, primary_key().autoincrement()), make_column("userId", &EditedMessage::userId), make_column("dialogId", &EditedMessage::dialogId), make_column("groupedId", &EditedMessage::groupedId), make_column("peerId", &EditedMessage::peerId), make_column("fromId", &EditedMessage::fromId), make_column("topicId", &EditedMessage::topicId), make_column("messageId", &EditedMessage::messageId), make_column("date", &EditedMessage::date), make_column("flags", &EditedMessage::flags), make_column("editDate", &EditedMessage::editDate), make_column("views", &EditedMessage::views), make_column("fwdFlags", &EditedMessage::fwdFlags), make_column("fwdFromId", &EditedMessage::fwdFromId), make_column("fwdName", &EditedMessage::fwdName), make_column("fwdDate", &EditedMessage::fwdDate), make_column("fwdPostAuthor", &EditedMessage::fwdPostAuthor), make_column("replyFlags", &EditedMessage::replyFlags), make_column("replyMessageId", &EditedMessage::replyMessageId), make_column("replyPeerId", &EditedMessage::replyPeerId), make_column("replyTopId", &EditedMessage::replyTopId), make_column("replyForumTopic", &EditedMessage::replyForumTopic), make_column("replySerialized", &EditedMessage::replySerialized), make_column("entityCreateDate", &EditedMessage::entityCreateDate), make_column("text", &EditedMessage::text), make_column("textEntities", &EditedMessage::textEntities), make_column("mediaPath", &EditedMessage::mediaPath), make_column("hqThumbPath", &EditedMessage::hqThumbPath), make_column("documentType", &EditedMessage::documentType), make_column("documentSerialized", &EditedMessage::documentSerialized), make_column("thumbsSerialized", &EditedMessage::thumbsSerialized), make_column("documentAttributesSerialized", &EditedMessage::documentAttributesSerialized), make_column("mimeType", &EditedMessage::mimeType) ), make_table( "DeletedDialog", make_column("fakeId", &DeletedDialog::fakeId, primary_key().autoincrement()), make_column("userId", &DeletedDialog::userId), make_column("dialogId", &DeletedDialog::dialogId), make_column("peerId", &DeletedDialog::peerId), make_column("folderId", &DeletedDialog::folderId), make_column("topMessage", &DeletedDialog::topMessage), make_column("lastMessageDate", &DeletedDialog::lastMessageDate), make_column("flags", &DeletedDialog::flags), make_column("entityCreateDate", &DeletedDialog::entityCreateDate) ), make_table( "RegexFilter", make_column("id", &RegexFilter::id, primary_key()), make_column("text", &RegexFilter::text), make_column("enabled", &RegexFilter::enabled), make_column("reversed", &RegexFilter::reversed), make_column("caseInsensitive", &RegexFilter::caseInsensitive), make_column("dialogId", &RegexFilter::dialogId) ), make_table( "RegexFilterGlobalExclusion", make_column("fakeId", &RegexFilterGlobalExclusion::fakeId, primary_key().autoincrement()), make_column("dialogId", &RegexFilterGlobalExclusion::dialogId), make_column("filterId", &RegexFilterGlobalExclusion::filterId) ), make_table( "SpyMessageRead", make_column("fakeId", &SpyMessageRead::fakeId, primary_key().autoincrement()), make_column("userId", &SpyMessageRead::userId), make_column("dialogId", &SpyMessageRead::dialogId), make_column("messageId", &SpyMessageRead::messageId), make_column("entityCreateDate", &SpyMessageRead::entityCreateDate) ), make_table( "SpyMessageContentsRead", make_column("fakeId", &SpyMessageContentsRead::fakeId, primary_key().autoincrement()), make_column("userId", &SpyMessageContentsRead::userId), make_column("dialogId", &SpyMessageContentsRead::dialogId), make_column("messageId", &SpyMessageContentsRead::messageId), make_column("entityCreateDate", &SpyMessageContentsRead::entityCreateDate) ) ); namespace AyuMigrations { void migrateToV1(decltype(storage) &storage) { // drop RegexFilter table as we've added primary_key() try { storage.drop_table_if_exists("RegexFilter"); LOG(("Migration to V1 successful.")); } catch (const std::exception &ex) { LOG(("Migration to V1 failed: %1").arg(ex.what())); } } } void runMigrations(decltype(storage) &storage) { constexpr int kLatestVersion = 1; const std::map> migrations = { {1, AyuMigrations::migrateToV1}, }; int currentVersion = 0; try { if (auto versionRow = storage.get_pointer(1)) { currentVersion = versionRow->version; } else { storage.insert(SchemaVersion{1, 0}); } } catch (...) { LOG(("No SchemaVersion, assuming 0")); storage.insert(SchemaVersion{1, 0}); } if (currentVersion >= kLatestVersion) { LOG(("Database is ok")); return; } LOG(("Database version: %1. Latest version: %2.").arg(currentVersion).arg(kLatestVersion)); for (int v = currentVersion + 1; v <= kLatestVersion; ++v) { if (migrations.contains(v)) { try { LOG(("Migration for version: %1").arg(v)); storage.begin_transaction(); migrations.at(v)(storage); storage.update_all(set(c(&SchemaVersion::version) = v), where(c(&SchemaVersion::id) == 1)); storage.commit(); LOG(("Applied migration for version: %1.").arg(v)); } catch (...) { storage.rollback(); LOG(("Failed to apply migration for version: %1.").arg(v)); AyuDatabase::moveCurrentDatabase(); return; } } } } namespace AyuDatabase { void moveCurrentDatabase() { const auto time = base::unixtime::now(); if (QFile::exists("./tdata/ayudata.db")) { QFile::rename("./tdata/ayudata.db", QString("./tdata/ayudata_%1.db").arg(time)); } if (QFile::exists("./tdata/ayudata.db-shm")) { QFile::rename("./tdata/ayudata.db-shm", QString("./tdata/ayudata_%1.db-shm").arg(time)); } if (QFile::exists("./tdata/ayudata.db-wal")) { QFile::rename("./tdata/ayudata.db-wal", QString("./tdata/ayudata_%1.db-wal").arg(time)); } } void initialize() { try { storage.sync_schema(true); runMigrations(storage); storage.sync_schema(true); } catch (const std::exception &ex) { LOG(("Database initialization failed: %1").arg(ex.what())); moveCurrentDatabase(); storage.sync_schema(true); if (!storage.get_pointer(1)) { storage.insert(SchemaVersion{1, 0}); } } } void addEditedMessage(const EditedMessage &message) { try { storage.begin_transaction(); storage.insert(message); storage.commit(); } catch (std::exception &ex) { LOG(("Failed to save edited message for some reason: %1").arg(ex.what())); } } std::vector getEditedMessages(ID userId, ID dialogId, ID messageId, ID minId, ID maxId, int totalLimit) { return storage.get_all( where( column(&EditedMessage::userId) == userId and column(&EditedMessage::dialogId) == dialogId and column(&EditedMessage::messageId) == messageId and (column(&EditedMessage::fakeId) > minId or minId == 0) and (column(&EditedMessage::fakeId) < maxId or maxId == 0) ), order_by(column(&EditedMessage::fakeId)).desc(), limit(totalLimit) ); } bool hasRevisions(ID userId, ID dialogId, ID messageId) { try { return !storage.select( columns(column(&EditedMessage::messageId)), where( column(&EditedMessage::userId) == userId and column(&EditedMessage::dialogId) == dialogId and column(&EditedMessage::messageId) == messageId ), limit(1) ).empty(); } catch (std::exception &ex) { LOG(("Failed to check if message has revisions: %1").arg(ex.what())); return false; } } void addDeletedMessage(const DeletedMessage &message) { try { storage.begin_transaction(); storage.insert(message); storage.commit(); } catch (std::exception &ex) { LOG(("Failed to save edited message for some reason: %1").arg(ex.what())); } } std::vector getDeletedMessages(ID userId, ID dialogId, ID topicId, ID minId, ID maxId, int totalLimit, const std::string &searchQuery) { if (searchQuery.empty()) { return storage.get_all( where( column(&DeletedMessage::userId) == userId and column(&DeletedMessage::dialogId) == dialogId and (column(&DeletedMessage::topicId) == topicId or topicId == 0) and (column(&DeletedMessage::messageId) > minId or minId == 0) and (column(&DeletedMessage::messageId) < maxId or maxId == 0) ), order_by(column(&DeletedMessage::messageId)).desc(), limit(totalLimit) ); } std::string escaped; escaped.reserve(searchQuery.size()); for (const auto c : searchQuery) { if (c == '%' || c == '_' || c == '\\') { escaped += '\\'; } escaped += c; } const auto pattern = "%" + escaped + "%"; return storage.get_all( where( column(&DeletedMessage::userId) == userId and column(&DeletedMessage::dialogId) == dialogId and (column(&DeletedMessage::topicId) == topicId or topicId == 0) and (column(&DeletedMessage::messageId) > minId or minId == 0) and (column(&DeletedMessage::messageId) < maxId or maxId == 0) and like(column(&DeletedMessage::text), pattern, "\\") ), order_by(column(&DeletedMessage::messageId)).desc(), limit(totalLimit) ); } bool hasDeletedMessages(ID userId, ID dialogId, ID topicId) { try { return !storage.select( columns(column(&DeletedMessage::dialogId)), where( column(&DeletedMessage::userId) == userId and column(&DeletedMessage::dialogId) == dialogId and (column(&DeletedMessage::topicId) == topicId or topicId == 0) ), limit(1) ).empty(); } catch (std::exception &ex) { LOG(("Failed to check if dialog has deleted message: %1").arg(ex.what())); return false; } } template std::vector getAllT() { try { return storage.get_all(); } catch (std::exception &ex) { LOG(("Failed to get all: %1").arg(ex.what())); return {}; } } std::vector getAllRegexFilters() { return getAllT(); } std::vector getAllFiltersExclusions() { return getAllT(); } std::vector getExcludedByDialogId(ID dialogId) { try { return storage.get_all( where(in(&RegexFilter::id, storage.select(columns(&RegexFilterGlobalExclusion::filterId), where(is_equal(&RegexFilterGlobalExclusion::dialogId, dialogId)) ) )) ); } catch (std::exception &ex) { LOG(("Failed to get excluded by dialog id: %1").arg(ex.what())); return {}; } } int getCount() { try { return storage.count(); } catch (std::exception &ex) { LOG(("Failed to get count: %1").arg(ex.what())); return 0; } } RegexFilter getById(std::vector id) { try { return storage.get( where(column(&RegexFilter::id) == std::move(id)) ); } catch (std::exception &ex) { LOG(("Failed to get filters by id: %1").arg(ex.what())); return {}; } } std::vector getShared() { try { return storage.get_all( where(is_null(column(&RegexFilter::dialogId))) ); } catch (std::exception &ex) { LOG(("Failed to get shared filters: %1").arg(ex.what())); return {}; } } std::vector getByDialogId(ID dialogId) { try { return storage.get_all( where(column(&RegexFilter::dialogId) == dialogId) ); } catch (std::exception &ex) { LOG(("Failed to get filters by dialog id: %1").arg(ex.what())); return {}; } } void addRegexFilter(const RegexFilter &filter) { try { storage.begin_transaction(); storage.replace(filter); // we're using replace as we set std::vector as primary key storage.commit(); } catch (std::exception &ex) { storage.rollback(); LOG(("Failed to save regex filter for some reason: %1").arg(ex.what())); } } void addRegexExclusion(const RegexFilterGlobalExclusion &exclusion) { try { storage.begin_transaction(); storage.insert(exclusion); storage.commit(); } catch (std::exception &ex) { LOG(("Failed to save regex filter exclusion for some reason: %1").arg(ex.what())); } } void updateRegexFilter(const RegexFilter &filter) { try { storage.update_all( set( c(&RegexFilter::text) = filter.text, c(&RegexFilter::enabled) = filter.enabled, c(&RegexFilter::reversed) = filter.reversed, c(&RegexFilter::caseInsensitive) = filter.caseInsensitive, c(&RegexFilter::dialogId) = filter.dialogId ), where(c(&RegexFilter::id) == filter.id) ); } catch (std::exception &ex) { LOG(("Failed to update regex filter for some reason: %1").arg(ex.what())); } } void deleteFilter(const std::vector &id) { try { storage.remove_all( where(column(&RegexFilter::id) == id) ); } catch (std::exception &ex) { LOG(("Failed to delete regex filter for some reason: %1").arg(ex.what())); } } void deleteExclusionsByFilterId(const std::vector &id) { try { storage.remove_all( where(column(&RegexFilterGlobalExclusion::filterId) == id) ); } catch (std::exception &ex) { LOG(("Failed to delete regex filter exclusion by filter id for some reason: %1").arg(ex.what())); } } void deleteExclusion(ID dialogId, std::vector filterId) { try { storage.remove_all( where(column(&RegexFilterGlobalExclusion::filterId) == filterId and column(&RegexFilterGlobalExclusion::dialogId) == dialogId ) ); } catch (std::exception &ex) { LOG(("Failed to delete regex filter exclusion for some reason: %1").arg(ex.what())); } } void deleteAllFilters() { try { storage.remove_all(); } catch (std::exception &ex) { LOG(("Failed to delete all regex filter for some reason: %1").arg(ex.what())); } } void deleteAllExclusions() { try { storage.remove_all(); } catch (std::exception &ex) { LOG(("Failed to delete all regex filter exclusions for some reason: %1").arg(ex.what())); } } bool hasFilters() { try { return !storage.select( columns(column(&RegexFilter::id)), limit(1) ).empty(); } catch (std::exception &ex) { LOG(("Failed to check if there's any filters: %1").arg(ex.what())); return false; } } bool hasPerDialogFilters() { try { return !storage.select( columns(column(&RegexFilter::id)), where(is_not_null(column(&RegexFilter::dialogId))), limit(1) ).empty() || !storage.select( columns(column(&RegexFilterGlobalExclusion::fakeId)), limit(1) ).empty(); } catch (std::exception &ex) { LOG(("Failed to check if there's any filters: %1").arg(ex.what())); return false; } } }