From 89e13a85a7af1c617a7e7b87011fe2339640f45d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20L=C3=B3pez=20Guimaraes?=
 <112760654+DaniElectra@users.noreply.github.com>
Date: Wed, 24 Jan 2024 18:21:48 +0100
Subject: [PATCH] Implement NEWS service (#7377)

---
 src/common/logging/filter.cpp        |   1 +
 src/common/logging/types.h           |   1 +
 src/core/hle/service/news/news.cpp   | 751 ++++++++++++++++++++++++++-
 src/core/hle/service/news/news.h     | 470 +++++++++++++++++
 src/core/hle/service/news/news_s.cpp |  62 +--
 src/core/hle/service/news/news_s.h   |  35 +-
 src/core/hle/service/news/news_u.cpp |   4 +-
 src/core/hle/service/news/news_u.h   |  10 +-
 8 files changed, 1249 insertions(+), 85 deletions(-)

diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp
index 2a2b85e84..85f271d2c 100644
--- a/src/common/logging/filter.cpp
+++ b/src/common/logging/filter.cpp
@@ -115,6 +115,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) {
     SUB(Service, Y2R)                                                                              \
     SUB(Service, PS)                                                                               \
     SUB(Service, PLGLDR)                                                                           \
+    SUB(Service, NEWS)                                                                             \
     CLS(HW)                                                                                        \
     SUB(HW, Memory)                                                                                \
     SUB(HW, LCD)                                                                                   \
diff --git a/src/common/logging/types.h b/src/common/logging/types.h
index 8d27c18c2..346b36eb9 100644
--- a/src/common/logging/types.h
+++ b/src/common/logging/types.h
@@ -82,6 +82,7 @@ enum class Class : u8 {
     Service_Y2R,     ///< The Y2R (YUV to RGB conversion) service
     Service_PS,      ///< The PS (Process) service
     Service_PLGLDR,  ///< The PLGLDR (plugin loader) service
+    Service_NEWS,    ///< The NEWS (Notifications) service
     HW,              ///< Low-level hardware emulation
     HW_Memory,       ///< Memory-map and address translation
     HW_LCD,          ///< LCD register emulation
diff --git a/src/core/hle/service/news/news.cpp b/src/core/hle/service/news/news.cpp
index 957f05212..f1a32d168 100644
--- a/src/core/hle/service/news/news.cpp
+++ b/src/core/hle/service/news/news.cpp
@@ -2,18 +2,765 @@
 // Licensed under GPLv2 or any later version
 // Refer to the license.txt file included.
 
+#include <algorithm>
+#include <fmt/format.h>
+#include "common/archives.h"
+#include "common/assert.h"
+#include "common/file_util.h"
 #include "common/logging/log.h"
+#include "common/scope_exit.h"
+#include "common/string_util.h"
 #include "core/core.h"
+#include "core/file_sys/archive_systemsavedata.h"
+#include "core/file_sys/errors.h"
+#include "core/file_sys/file_backend.h"
+#include "core/hle/ipc_helpers.h"
+#include "core/hle/kernel/shared_page.h"
+#include "core/hle/result.h"
+#include "core/hle/service/fs/fs_user.h"
+#include "core/hle/service/news/news.h"
 #include "core/hle/service/news/news_s.h"
 #include "core/hle/service/news/news_u.h"
 #include "core/hle/service/service.h"
 
+SERVICE_CONSTRUCT_IMPL(Service::NEWS::Module)
+
 namespace Service::NEWS {
 
+namespace ErrCodes {
+enum {
+    /// This error is returned if either the NewsDB header or the header for a notification ID is
+    /// invalid
+    InvalidHeader = 5,
+};
+}
+
+constexpr Result ErrorInvalidHeader = // 0xC8A12805
+    Result(ErrCodes::InvalidHeader, ErrorModule::News, ErrorSummary::InvalidState,
+           ErrorLevel::Status);
+
+constexpr std::array<u8, 8> news_system_savedata_id{
+    0x00, 0x00, 0x00, 0x00, 0x35, 0x00, 0x01, 0x00,
+};
+
+template <class Archive>
+void Module::serialize(Archive& ar, const unsigned int) {
+    ar& db;
+    ar& notification_ids;
+    ar& automatic_sync_flag;
+    ar& news_system_save_data_archive;
+}
+SERIALIZE_IMPL(Module)
+
+void Module::Interface::AddNotificationImpl(Kernel::HLERequestContext& ctx, bool news_s) {
+    IPC::RequestParser rp(ctx);
+    const u32 header_size = rp.Pop<u32>();
+    const u32 message_size = rp.Pop<u32>();
+    const u32 image_size = rp.Pop<u32>();
+
+    u32 process_id;
+    if (!news_s) {
+        process_id = rp.PopPID();
+        LOG_INFO(Service_NEWS,
+                 "called header_size=0x{:x}, message_size=0x{:x}, image_size=0x{:x}, process_id={}",
+                 header_size, message_size, image_size, process_id);
+    } else {
+        LOG_INFO(Service_NEWS, "called header_size=0x{:x}, message_size=0x{:x}, image_size=0x{:x}",
+                 header_size, message_size, image_size);
+    }
+
+    auto header_buffer = rp.PopMappedBuffer();
+    auto message_buffer = rp.PopMappedBuffer();
+    auto image_buffer = rp.PopMappedBuffer();
+
+    NotificationHeader header{};
+    header_buffer.Read(&header, 0,
+                       std::min(sizeof(NotificationHeader), static_cast<std::size_t>(header_size)));
+
+    std::vector<u8> message(message_size);
+    message_buffer.Read(message.data(), 0, message.size());
+
+    std::vector<u8> image(image_size);
+    image_buffer.Read(image.data(), 0, image.size());
+
+    IPC::RequestBuilder rb = rp.MakeBuilder(1, 6);
+    SCOPE_EXIT({
+        rb.PushMappedBuffer(header_buffer);
+        rb.PushMappedBuffer(message_buffer);
+        rb.PushMappedBuffer(image_buffer);
+    });
+
+    if (!news_s) {
+        // Set the program_id using the input process ID
+        auto fs_user = news->system.ServiceManager().GetService<Service::FS::FS_USER>("fs:USER");
+        ASSERT_MSG(fs_user != nullptr, "fs:USER service is missing.");
+
+        auto program_info_result = fs_user->GetProgramLaunchInfo(process_id);
+        if (program_info_result.Failed()) {
+            rb.Push(program_info_result.Code());
+            return;
+        }
+
+        header.program_id = program_info_result.Unwrap().program_id;
+
+        // The date_time is set by the sysmodule on news:u requests
+        auto& share_page = news->system.Kernel().GetSharedPageHandler();
+        header.date_time = share_page.GetSystemTimeSince2000();
+    }
+
+    const auto save_result = news->SaveNotification(&header, header_size, message, image);
+    if (R_FAILED(save_result)) {
+        rb.Push(save_result);
+        return;
+    }
+
+    // Mark the DB header new notification flag
+    if ((news->db.header.flags & 1) == 0) {
+        news->db.header.flags |= 1;
+        const auto db_result = news->SaveNewsDBSavedata();
+        if (R_FAILED(db_result)) {
+            rb.Push(db_result);
+            return;
+        }
+    }
+
+    rb.Push(ResultSuccess);
+}
+
+void Module::Interface::AddNotification(Kernel::HLERequestContext& ctx) {
+    AddNotificationImpl(ctx, false);
+}
+
+void Module::Interface::AddNotificationSystem(Kernel::HLERequestContext& ctx) {
+    AddNotificationImpl(ctx, true);
+}
+
+void Module::Interface::ResetNotifications(Kernel::HLERequestContext& ctx) {
+    IPC::RequestParser rp(ctx);
+
+    LOG_INFO(Service_NEWS, "called");
+
+    // Cleanup the sorted notification IDs
+    for (u32 i = 0; i < MAX_NOTIFICATIONS; ++i) {
+        news->notification_ids[i] = i;
+    }
+
+    const std::string& nand_directory = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir);
+    FileSys::ArchiveFactory_SystemSaveData systemsavedata_factory(nand_directory);
+
+    FileSys::Path archive_path(news_system_savedata_id);
+
+    // Format the SystemSaveData archive 0x00010035
+    systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0);
+
+    news->news_system_save_data_archive = systemsavedata_factory.Open(archive_path, 0).Unwrap();
+
+    // NOTE: The original sysmodule doesn't clear the News DB in memory
+
+    IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
+    rb.Push(ResultSuccess);
+}
+
+void Module::Interface::GetTotalNotifications(Kernel::HLERequestContext& ctx) {
+    IPC::RequestParser rp(ctx);
+
+    LOG_INFO(Service_NEWS, "called");
+
+    IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
+    rb.Push(ResultSuccess);
+    rb.Push(static_cast<u32>(news->GetTotalNotifications()));
+}
+
+void Module::Interface::SetNewsDBHeader(Kernel::HLERequestContext& ctx) {
+    IPC::RequestParser rp(ctx);
+    const u32 size = rp.Pop<u32>();
+    auto input_buffer = rp.PopMappedBuffer();
+
+    LOG_INFO(Service_NEWS, "called size=0x{:x}", size);
+
+    NewsDBHeader header{};
+    input_buffer.Read(&header, 0, std::min(sizeof(NewsDBHeader), static_cast<std::size_t>(size)));
+
+    IPC::RequestBuilder rb = rp.MakeBuilder(1, 2);
+    rb.Push(news->SetNewsDBHeader(&header, size));
+    rb.PushMappedBuffer(input_buffer);
+}
+
+void Module::Interface::SetNotificationHeader(Kernel::HLERequestContext& ctx) {
+    IPC::RequestParser rp(ctx);
+    const u32 notification_index = rp.Pop<u32>();
+    const u32 size = rp.Pop<u32>();
+    auto input_buffer = rp.PopMappedBuffer();
+
+    LOG_INFO(Service_NEWS, "called notification_index={}, size=0x{:x}", notification_index, size);
+
+    NotificationHeader header{};
+    input_buffer.Read(&header, 0,
+                      std::min(sizeof(NotificationHeader), static_cast<std::size_t>(size)));
+
+    const auto result = news->SetNotificationHeader(notification_index, &header, size);
+
+    // TODO(DaniElectra): If flag_boss == 1, the original sysmodule updates the optout status of the
+    // source program on SpotPass with the boss:P command 0x00040600C0 (possibly named
+    // SetOptoutFlagPrivileged?) using the program_id and flag_optout as parameter
+
+    IPC::RequestBuilder rb = rp.MakeBuilder(1, 2);
+    rb.Push(result);
+    rb.PushMappedBuffer(input_buffer);
+}
+
+void Module::Interface::SetNotificationMessage(Kernel::HLERequestContext& ctx) {
+    IPC::RequestParser rp(ctx);
+    const u32 notification_index = rp.Pop<u32>();
+    const u32 size = rp.Pop<u32>();
+    auto input_buffer = rp.PopMappedBuffer();
+
+    LOG_INFO(Service_NEWS, "called notification_index={}, size=0x{:x}", notification_index, size);
+
+    std::vector<u8> data(size);
+    input_buffer.Read(data.data(), 0, data.size());
+
+    IPC::RequestBuilder rb = rp.MakeBuilder(1, 2);
+    rb.Push(news->SetNotificationMessage(notification_index, data));
+    rb.PushMappedBuffer(input_buffer);
+}
+
+void Module::Interface::SetNotificationImage(Kernel::HLERequestContext& ctx) {
+    IPC::RequestParser rp(ctx);
+    const u32 notification_index = rp.Pop<u32>();
+    const u32 size = rp.Pop<u32>();
+    auto input_buffer = rp.PopMappedBuffer();
+
+    LOG_INFO(Service_NEWS, "called notification_index={}, size=0x{:x}", notification_index, size);
+
+    std::vector<u8> data(size);
+    input_buffer.Read(data.data(), 0, data.size());
+
+    IPC::RequestBuilder rb = rp.MakeBuilder(1, 2);
+    rb.Push(news->SetNotificationImage(notification_index, data));
+    rb.PushMappedBuffer(input_buffer);
+}
+
+void Module::Interface::GetNewsDBHeader(Kernel::HLERequestContext& ctx) {
+    IPC::RequestParser rp(ctx);
+    const u32 size = rp.Pop<u32>();
+    auto output_buffer = rp.PopMappedBuffer();
+
+    LOG_INFO(Service_NEWS, "called size=0x{:x}", size);
+
+    NewsDBHeader header{};
+    const auto result = news->GetNewsDBHeader(&header, size);
+
+    IPC::RequestBuilder rb = rp.MakeBuilder(2, 2);
+
+    if (result.Failed()) {
+        rb.Push(result.Code());
+        rb.Push<u32>(0);
+    } else {
+        const auto copied_size = result.Unwrap();
+        output_buffer.Write(&header, 0, copied_size);
+
+        rb.Push(ResultSuccess);
+        rb.Push<u32>(static_cast<u32>(copied_size));
+    }
+
+    rb.PushMappedBuffer(output_buffer);
+}
+
+void Module::Interface::GetNotificationHeader(Kernel::HLERequestContext& ctx) {
+    IPC::RequestParser rp(ctx);
+    const u32 notification_index = rp.Pop<u32>();
+    const u32 size = rp.Pop<u32>();
+    auto output_buffer = rp.PopMappedBuffer();
+
+    LOG_DEBUG(Service_NEWS, "called notification_index={}, size=0x{:x}", notification_index, size);
+
+    NotificationHeader header{};
+    const auto result = news->GetNotificationHeader(notification_index, &header, size);
+
+    IPC::RequestBuilder rb = rp.MakeBuilder(2, 2);
+    SCOPE_EXIT({ rb.PushMappedBuffer(output_buffer); });
+
+    if (result.Failed()) {
+        rb.Push(result.Code());
+        rb.Push<u32>(0);
+        return;
+    }
+
+    // TODO(DaniElectra): If flag_boss == 1, the original sysmodule updates the optout flag of the
+    // header with the result of boss:P command 0x0004070080 (possibly named
+    // GetOptoutFlagPrivileged?) using the program_id as parameter
+
+    const auto copied_size = result.Unwrap();
+    output_buffer.Write(&header, 0, copied_size);
+
+    rb.Push(ResultSuccess);
+    rb.Push<u32>(static_cast<u32>(copied_size));
+}
+
+void Module::Interface::GetNotificationMessage(Kernel::HLERequestContext& ctx) {
+    IPC::RequestParser rp(ctx);
+    const u32 notification_index = rp.Pop<u32>();
+    const u32 size = rp.Pop<u32>();
+    auto output_buffer = rp.PopMappedBuffer();
+
+    LOG_DEBUG(Service_NEWS, "called notification_index={}, size=0x{:x}", notification_index, size);
+
+    std::vector<u8> message(size);
+    const auto result = news->GetNotificationMessage(notification_index, message);
+
+    IPC::RequestBuilder rb = rp.MakeBuilder(2, 2);
+    SCOPE_EXIT({ rb.PushMappedBuffer(output_buffer); });
+
+    if (result.Failed()) {
+        rb.Push(result.Code());
+        rb.Push<u32>(0);
+        return;
+    }
+
+    const auto copied_size = result.Unwrap();
+    output_buffer.Write(message.data(), 0, copied_size);
+
+    rb.Push(ResultSuccess);
+    rb.Push<u32>(static_cast<u32>(copied_size));
+}
+
+void Module::Interface::GetNotificationImage(Kernel::HLERequestContext& ctx) {
+    IPC::RequestParser rp(ctx);
+    const u32 notification_index = rp.Pop<u32>();
+    const u32 size = rp.Pop<u32>();
+    auto output_buffer = rp.PopMappedBuffer();
+
+    LOG_DEBUG(Service_NEWS, "called notification_index={}, size=0x{:x}", notification_index, size);
+
+    std::vector<u8> image(size);
+    const auto result = news->GetNotificationImage(notification_index, image);
+
+    IPC::RequestBuilder rb = rp.MakeBuilder(2, 2);
+    SCOPE_EXIT({ rb.PushMappedBuffer(output_buffer); });
+
+    if (result.Failed()) {
+        rb.Push(result.Code());
+        rb.Push<u32>(0);
+        return;
+    }
+
+    const auto copied_size = result.Unwrap();
+    output_buffer.Write(image.data(), 0, copied_size);
+
+    rb.Push(ResultSuccess);
+    rb.Push<u32>(static_cast<u32>(copied_size));
+}
+
+void Module::Interface::SetAutomaticSyncFlag(Kernel::HLERequestContext& ctx) {
+    IPC::RequestParser rp(ctx);
+    const u8 flag = rp.Pop<u8>();
+
+    LOG_INFO(Service_NEWS, "called flag=0x{:x}", flag);
+
+    news->automatic_sync_flag = flag;
+
+    IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
+    rb.Push(ResultSuccess);
+}
+
+void Module::Interface::SetNotificationHeaderOther(Kernel::HLERequestContext& ctx) {
+    IPC::RequestParser rp(ctx);
+    const u32 notification_index = rp.Pop<u32>();
+    const u32 size = rp.Pop<u32>();
+    auto output_buffer = rp.PopMappedBuffer();
+
+    LOG_INFO(Service_NEWS, "called notification_index={}, size=0x{:x}", notification_index, size);
+
+    NotificationHeader header{};
+    output_buffer.Read(&header, 0,
+                       std::min(sizeof(NotificationHeader), static_cast<std::size_t>(size)));
+
+    const auto result = news->SetNotificationHeaderOther(notification_index, &header, size);
+
+    // TODO(DaniElectra): If flag_boss == 1, the original sysmodule updates the optout status of the
+    // source program on SpotPass with the boss:P command 0x00040600C0 (possibly named
+    // SetOptoutFlagPrivileged?) using the program_id and flag_optout as parameter
+
+    IPC::RequestBuilder rb = rp.MakeBuilder(1, 2);
+    rb.Push(result);
+    rb.PushMappedBuffer(output_buffer);
+}
+
+void Module::Interface::WriteNewsDBSavedata(Kernel::HLERequestContext& ctx) {
+    IPC::RequestParser rp(ctx);
+
+    LOG_INFO(Service_NEWS, "called");
+
+    IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
+    rb.Push(news->SaveNewsDBSavedata());
+}
+
+void Module::Interface::GetTotalArrivedNotifications(Kernel::HLERequestContext& ctx) {
+    IPC::RequestParser rp(ctx);
+
+    LOG_WARNING(Service_NEWS, "(STUBBED) called");
+
+    IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
+    rb.Push(ResultSuccess);
+    rb.Push<u32>(0); // Total number of pending BOSS notifications to be synced
+}
+
+std::size_t Module::GetTotalNotifications() {
+    return std::count_if(
+        notification_ids.begin(), notification_ids.end(),
+        [this](const u32 notification_id) { return db.notifications[notification_id].IsValid(); });
+}
+
+ResultVal<std::size_t> Module::GetNewsDBHeader(NewsDBHeader* header, const std::size_t size) {
+    if (!db.header.IsValid()) {
+        return ErrorInvalidHeader;
+    }
+
+    const std::size_t copy_size = std::min(sizeof(NewsDBHeader), size);
+    std::memcpy(header, &db.header, copy_size);
+    return copy_size;
+}
+
+ResultVal<std::size_t> Module::GetNotificationHeader(const u32 notification_index,
+                                                     NotificationHeader* header,
+                                                     const std::size_t size) {
+    if (!db.header.IsValid()) {
+        return ErrorInvalidHeader;
+    }
+
+    if (notification_index >= MAX_NOTIFICATIONS) {
+        return ErrorInvalidHeader;
+    }
+
+    const u32 notification_id = notification_ids[notification_index];
+    if (!db.notifications[notification_id].IsValid()) {
+        return ErrorInvalidHeader;
+    }
+
+    const std::size_t copy_size = std::min(sizeof(NotificationHeader), size);
+    std::memcpy(header, &db.notifications[notification_id], copy_size);
+    return copy_size;
+}
+
+ResultVal<std::size_t> Module::GetNotificationMessage(const u32 notification_index,
+                                                      std::span<u8> message) {
+    if (!db.header.IsValid()) {
+        return ErrorInvalidHeader;
+    }
+
+    if (notification_index >= MAX_NOTIFICATIONS) {
+        return ErrorInvalidHeader;
+    }
+
+    const u32 notification_id = notification_ids[notification_index];
+    if (!db.notifications[notification_id].IsValid()) {
+        return ErrorInvalidHeader;
+    }
+
+    std::string message_file = fmt::format("/news{:03d}.txt", notification_id);
+    const auto result = LoadFileFromSavedata(message_file, message);
+    if (result.Failed()) {
+        return result.Code();
+    }
+
+    return result.Unwrap();
+}
+
+ResultVal<std::size_t> Module::GetNotificationImage(const u32 notification_index,
+                                                    std::span<u8> image) {
+    if (!db.header.IsValid()) {
+        return ErrorInvalidHeader;
+    }
+
+    if (notification_index >= MAX_NOTIFICATIONS) {
+        return ErrorInvalidHeader;
+    }
+
+    const u32 notification_id = notification_ids[notification_index];
+    if (!db.notifications[notification_id].IsValid()) {
+        return ErrorInvalidHeader;
+    }
+
+    std::string image_file = fmt::format("/news{:03d}.mpo", notification_id);
+    const auto result = LoadFileFromSavedata(image_file, image);
+    if (result.Failed()) {
+        return result.Code();
+    }
+
+    return result.Unwrap();
+}
+
+Result Module::SetNewsDBHeader(const NewsDBHeader* header, const std::size_t size) {
+    const std::size_t copy_size = std::min(sizeof(NewsDBHeader), static_cast<std::size_t>(size));
+    std::memcpy(&db.header, header, copy_size);
+    return SaveNewsDBSavedata();
+}
+
+Result Module::SetNotificationHeader(const u32 notification_index, const NotificationHeader* header,
+                                     const std::size_t size) {
+    if (notification_index >= MAX_NOTIFICATIONS) {
+        return ErrorInvalidHeader;
+    }
+
+    const u32 notification_id = notification_ids[notification_index];
+    const std::size_t copy_size = std::min(sizeof(NotificationHeader), size);
+    std::memcpy(&db.notifications[notification_id], header, copy_size);
+    return SaveNewsDBSavedata();
+}
+
+Result Module::SetNotificationHeaderOther(const u32 notification_index,
+                                          const NotificationHeader* header,
+                                          const std::size_t size) {
+    if (notification_index >= MAX_NOTIFICATIONS) {
+        return ErrorInvalidHeader;
+    }
+
+    const u32 notification_id = notification_ids[notification_index];
+    const std::size_t copy_size = std::min(sizeof(NotificationHeader), size);
+    std::memcpy(&db.notifications[notification_id], header, copy_size);
+    return ResultSuccess;
+}
+
+Result Module::SetNotificationMessage(const u32 notification_index, std::span<const u8> message) {
+    if (notification_index >= MAX_NOTIFICATIONS) {
+        return ErrorInvalidHeader;
+    }
+
+    const u32 notification_id = notification_ids[notification_index];
+    const std::string message_file = fmt::format("/news{:03d}.txt", notification_id);
+    return SaveFileToSavedata(message_file, message);
+}
+
+Result Module::SetNotificationImage(const u32 notification_index, std::span<const u8> image) {
+    if (notification_index >= MAX_NOTIFICATIONS) {
+        return ErrorInvalidHeader;
+    }
+
+    const u32 notification_id = notification_ids[notification_index];
+    const std::string image_file = fmt::format("/news{:03d}.mpo", notification_id);
+    return SaveFileToSavedata(image_file, image);
+}
+
+Result Module::SaveNotification(const NotificationHeader* header, const std::size_t header_size,
+                                std::span<const u8> message, std::span<const u8> image) {
+    if (!db.header.IsValid()) {
+        return ErrorInvalidHeader;
+    }
+
+    if (!header->IsValid()) {
+        return ErrorInvalidHeader;
+    }
+
+    u32 notification_count = static_cast<u32>(GetTotalNotifications());
+
+    // If we have reached the limit of 100 notifications, delete the oldest one
+    if (notification_count >= MAX_NOTIFICATIONS) {
+        LOG_WARNING(Service_NEWS,
+                    "Notification limit has been reached. Deleting oldest notification ID: {}",
+                    notification_ids[0]);
+        R_TRY(DeleteNotification(notification_ids[0]));
+
+        notification_count--;
+    }
+
+    // Check if there is enough space for storing the new notification data. The header is already
+    // allocated with the News DB
+    const u64 needed_space = static_cast<u64>(message.size() + image.size());
+    while (notification_count > 0) {
+        const u64 free_space = news_system_save_data_archive->GetFreeBytes();
+        if (needed_space <= free_space) {
+            break;
+        }
+
+        LOG_WARNING(Service_NEWS, "Not enough space available. Deleting oldest notification ID: {}",
+                    notification_ids[0]);
+
+        // If we don't have space, delete old notifications until we do
+        R_TRY(DeleteNotification(notification_ids[0]));
+
+        notification_count--;
+    }
+
+    LOG_DEBUG(Service_NEWS, "New notification: notification_id={}, title={}",
+              notification_ids[notification_count], Common::UTF16BufferToUTF8(header->title));
+
+    if (!image.empty()) {
+        R_TRY(SetNotificationImage(notification_count, image));
+    }
+
+    if (!message.empty()) {
+        R_TRY(SetNotificationMessage(notification_count, message));
+    }
+
+    R_TRY(SetNotificationHeader(notification_count, header, header_size));
+
+    // Sort the notifications after saving
+    std::sort(notification_ids.begin(), notification_ids.end(),
+              [this](const u32 first_id, const u32 second_id) -> bool {
+                  return CompareNotifications(first_id, second_id);
+              });
+
+    return ResultSuccess;
+}
+
+Result Module::DeleteNotification(const u32 notification_id) {
+    bool deleted = false;
+
+    // Check if the input notification ID exists, and clear it
+    if (db.notifications[notification_id].IsValid()) {
+        db.notifications[notification_id] = {};
+
+        R_TRY(SaveNewsDBSavedata());
+
+        deleted = true;
+    }
+
+    // Cleanup images and messages for invalid notifications
+    for (u32 i = 0; i < MAX_NOTIFICATIONS; ++i) {
+        if (!db.notifications[i].IsValid()) {
+            const std::string image_file = fmt::format("/news{:03d}.mpo", i);
+            auto result = news_system_save_data_archive->DeleteFile(image_file);
+            if (R_FAILED(result) && result != FileSys::ResultFileNotFound) {
+                return result;
+            }
+
+            const std::string message_file = fmt::format("/news{:03d}.txt", i);
+            result = news_system_save_data_archive->DeleteFile(message_file);
+            if (R_FAILED(result) && result != FileSys::ResultFileNotFound) {
+                return result;
+            }
+        }
+    }
+
+    // If the input notification ID was deleted, reorder the notification IDs list
+    if (deleted) {
+        std::sort(notification_ids.begin(), notification_ids.end(),
+                  [this](const u32 first_id, const u32 second_id) -> bool {
+                      return CompareNotifications(first_id, second_id);
+                  });
+    }
+
+    return ResultSuccess;
+}
+
+Result Module::LoadNewsDBSavedata() {
+    const std::string& nand_directory = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir);
+    FileSys::ArchiveFactory_SystemSaveData systemsavedata_factory(nand_directory);
+
+    // Open the SystemSaveData archive 0x00010035
+    FileSys::Path archive_path(news_system_savedata_id);
+    auto archive_result = systemsavedata_factory.Open(archive_path, 0);
+
+    // If the archive didn't exist, create the files inside
+    if (archive_result.Code() == FileSys::ResultNotFound) {
+        // Format the archive to create the directories
+        systemsavedata_factory.Format(archive_path, FileSys::ArchiveFormatInfo(), 0);
+
+        // Open it again to get a valid archive now that the folder exists
+        news_system_save_data_archive = systemsavedata_factory.Open(archive_path, 0).Unwrap();
+    } else {
+        ASSERT_MSG(archive_result.Succeeded(), "Could not open the NEWS SystemSaveData archive!");
+
+        news_system_save_data_archive = std::move(archive_result).Unwrap();
+    }
+
+    const std::string news_db_file = "/news.db";
+    auto news_result =
+        LoadFileFromSavedata(news_db_file, std::span{reinterpret_cast<u8*>(&db), sizeof(NewsDB)});
+
+    // Read the file if it already exists
+    if (news_result.Failed()) {
+        // Create the file immediately if it doesn't exist
+        db.header = {.valid = 1};
+        news_result = SaveFileToSavedata(
+            news_db_file, std::span{reinterpret_cast<const u8*>(&db), sizeof(NewsDB)});
+    } else {
+        // Sort the notifications from the file
+        std::sort(notification_ids.begin(), notification_ids.end(),
+                  [this](const u32 first_id, const u32 second_id) -> bool {
+                      return CompareNotifications(first_id, second_id);
+                  });
+    }
+
+    return news_result.Code();
+}
+
+Result Module::SaveNewsDBSavedata() {
+    return SaveFileToSavedata("/news.db",
+                              std::span{reinterpret_cast<const u8*>(&db), sizeof(NewsDB)});
+}
+
+ResultVal<std::size_t> Module::LoadFileFromSavedata(std::string filename, std::span<u8> buffer) {
+    FileSys::Mode mode = {};
+    mode.read_flag.Assign(1);
+
+    FileSys::Path path(filename);
+
+    auto result = news_system_save_data_archive->OpenFile(path, mode);
+    if (result.Failed()) {
+        return result.Code();
+    }
+
+    auto file = std::move(result).Unwrap();
+    const auto bytes_read = file->Read(0, buffer.size(), buffer.data());
+    file->Close();
+
+    ASSERT_MSG(bytes_read.Succeeded(), "could not read file");
+
+    return bytes_read.Unwrap();
+}
+
+Result Module::SaveFileToSavedata(std::string filename, std::span<const u8> buffer) {
+    FileSys::Mode mode = {};
+    mode.write_flag.Assign(1);
+    mode.create_flag.Assign(1);
+
+    FileSys::Path path(filename);
+
+    auto result = news_system_save_data_archive->OpenFile(path, mode);
+    ASSERT_MSG(result.Succeeded(), "could not open file");
+
+    auto file = std::move(result).Unwrap();
+    file->Write(0, buffer.size(), 1, buffer.data());
+    file->Close();
+
+    return ResultSuccess;
+}
+
+bool Module::CompareNotifications(const u32 first_id, const u32 second_id) {
+    // Notification IDs are sorted by date time, with valid notifications being first.
+    // This is done so that other system applications like the News applet can easily
+    // iterate over the notifications with an incrementing index.
+    ASSERT(first_id < MAX_NOTIFICATIONS && second_id < MAX_NOTIFICATIONS);
+
+    if (!db.notifications[first_id].IsValid()) {
+        return false;
+    }
+
+    if (!db.notifications[second_id].IsValid()) {
+        return true;
+    }
+
+    return db.notifications[first_id].date_time < db.notifications[second_id].date_time;
+}
+
+Module::Interface::Interface(std::shared_ptr<Module> news, const char* name, u32 max_session)
+    : ServiceFramework(name, max_session), news(std::move(news)) {}
+
+Module::Module(Core::System& system_) : system(system_) {
+    for (u32 i = 0; i < MAX_NOTIFICATIONS; ++i) {
+        notification_ids[i] = i;
+    }
+
+    LoadNewsDBSavedata();
+}
+
 void InstallInterfaces(Core::System& system) {
     auto& service_manager = system.ServiceManager();
-    std::make_shared<NEWS_S>()->InstallAsService(service_manager);
-    std::make_shared<NEWS_U>()->InstallAsService(service_manager);
+    auto news = std::make_shared<Module>(system);
+    std::make_shared<NEWS_S>(news)->InstallAsService(service_manager);
+    std::make_shared<NEWS_U>(news)->InstallAsService(service_manager);
 }
 
 } // namespace Service::NEWS
diff --git a/src/core/hle/service/news/news.h b/src/core/hle/service/news/news.h
index 98f3b1583..1d2382937 100644
--- a/src/core/hle/service/news/news.h
+++ b/src/core/hle/service/news/news.h
@@ -4,12 +4,482 @@
 
 #pragma once
 
+#include "core/file_sys/archive_backend.h"
+#include "core/hle/service/service.h"
+
 namespace Core {
 class System;
 }
 
 namespace Service::NEWS {
+constexpr u32 MAX_NOTIFICATIONS = 100;
+
+struct NewsDBHeader {
+    u8 valid;
+    u8 flags;
+    INSERT_PADDING_BYTES(0xE);
+
+    bool IsValid() const {
+        return valid == 1;
+    }
+
+private:
+    template <class Archive>
+    void serialize(Archive& ar, const unsigned int) {
+        ar& valid;
+        ar& flags;
+    }
+    friend class boost::serialization::access;
+};
+static_assert(sizeof(NewsDBHeader) == 0x10, "News DB Header structure size is wrong");
+
+struct NotificationHeader {
+    u8 flag_valid;
+    u8 flag_read;
+    u8 flag_jpeg;
+    u8 flag_boss;
+    u8 flag_optout;
+    u8 flag_url;
+    u8 flag_unk0x6;
+    INSERT_PADDING_BYTES(0x1);
+    u64_le program_id;
+    u32_le ns_data_id; // Only used in BOSS notifications
+    u32_le version;    // Only used in BOSS notifications
+    u64_le jump_param;
+    INSERT_PADDING_BYTES(0x8);
+    u64_le date_time;
+    std::array<u16_le, 0x20> title;
+
+    bool IsValid() const {
+        return flag_valid == 1;
+    }
+
+private:
+    template <class Archive>
+    void serialize(Archive& ar, const unsigned int) {
+        ar& flag_valid;
+        ar& flag_read;
+        ar& flag_jpeg;
+        ar& flag_boss;
+        ar& flag_optout;
+        ar& flag_url;
+        ar& flag_unk0x6;
+        ar& program_id;
+        ar& ns_data_id;
+        ar& version;
+        ar& jump_param;
+        ar& date_time;
+        ar& title;
+    }
+    friend class boost::serialization::access;
+};
+static_assert(sizeof(NotificationHeader) == 0x70, "Notification Header structure size is wrong");
+
+struct NewsDB {
+    NewsDBHeader header;
+    std::array<NotificationHeader, MAX_NOTIFICATIONS> notifications;
+
+private:
+    template <class Archive>
+    void serialize(Archive& ar, const unsigned int) {
+        ar& header;
+        ar& notifications;
+    }
+    friend class boost::serialization::access;
+};
+static_assert(sizeof(NewsDB) == 0x2BD0, "News DB structure size is wrong");
+
+class Module final {
+public:
+    explicit Module(Core::System& system_);
+    ~Module() = default;
+
+    class Interface : public ServiceFramework<Interface> {
+    public:
+        Interface(std::shared_ptr<Module> news, const char* name, u32 max_session);
+        ~Interface() = default;
+
+    private:
+        void AddNotificationImpl(Kernel::HLERequestContext& ctx, bool news_s);
+
+    protected:
+        /**
+         * AddNotification NEWS:U service function.
+         *  Inputs:
+         *      0 : 0x000100C8
+         *      1 : Header size
+         *      2 : Message size
+         *      3 : Image size
+         *      4 : PID Translation Header (0x20)
+         *      5 : Caller PID
+         *      6 : Header Buffer Mapping Translation Header ((Size << 4) | 0xA)
+         *      7 : Header Buffer Pointer
+         *      8 : Message Buffer Mapping Translation Header ((Size << 4) | 0xA)
+         *      9 : Message Buffer Pointer
+         *     10 : Image Buffer Mapping Translation Header ((Size << 4) | 0xA)
+         *     11 : Image Buffer Pointer
+         *  Outputs:
+         *      0 : 0x00010046
+         *      1 : Result of function, 0 on success, otherwise error code
+         */
+        void AddNotification(Kernel::HLERequestContext& ctx);
+
+        /**
+         * AddNotification NEWS:S service function.
+         *  Inputs:
+         *      0 : 0x000100C6
+         *      1 : Header size
+         *      2 : Message size
+         *      3 : Image size
+         *      4 : Header Buffer Mapping Translation Header ((Size << 4) | 0xA)
+         *      5 : Header Buffer Pointer
+         *      6 : Message Buffer Mapping Translation Header ((Size << 4) | 0xA)
+         *      7 : Message Buffer Pointer
+         *      8 : Image Buffer Mapping Translation Header ((Size << 4) | 0xA)
+         *      9 : Image Buffer Pointer
+         *  Outputs:
+         *      0 : 0x00010046
+         *      1 : Result of function, 0 on success, otherwise error code
+         */
+        void AddNotificationSystem(Kernel::HLERequestContext& ctx);
+
+        /**
+         * ResetNotifications service function.
+         *  Inputs:
+         *      0 : 0x00040000
+         *  Outputs:
+         *      0 : 0x00040040
+         *      1 : Result of function, 0 on success, otherwise error code
+         */
+        void ResetNotifications(Kernel::HLERequestContext& ctx);
+
+        /**
+         * GetTotalNotifications service function.
+         *  Inputs:
+         *      0 : 0x00050000
+         *  Outputs:
+         *      0 : 0x00050080
+         *      1 : Result of function, 0 on success, otherwise error code
+         *      2 : Number of notifications
+         */
+        void GetTotalNotifications(Kernel::HLERequestContext& ctx);
+
+        /**
+         * SetNewsDBHeader service function.
+         *  Inputs:
+         *      0 : 0x00060042
+         *      1 : Size
+         *      2 : Input Buffer Mapping Translation Header ((Size << 4) | 0xA)
+         *      3 : Input Buffer Pointer
+         *  Outputs:
+         *      0 : 0x00060042
+         *      1 : Result of function, 0 on success, otherwise error code
+         */
+        void SetNewsDBHeader(Kernel::HLERequestContext& ctx);
+
+        /**
+         * SetNotificationHeader service function.
+         *  Inputs:
+         *      0 : 0x00070082
+         *      1 : Notification index
+         *      2 : Size
+         *      3 : Input Buffer Mapping Translation Header ((Size << 4) | 0xA)
+         *      4 : Input Buffer Pointer
+         *  Outputs:
+         *      0 : 0x00070042
+         *      1 : Result of function, 0 on success, otherwise error code
+         */
+        void SetNotificationHeader(Kernel::HLERequestContext& ctx);
+
+        /**
+         * SetNotificationMessage service function.
+         *  Inputs:
+         *      0 : 0x00080082
+         *      1 : Notification index
+         *      2 : Size
+         *      3 : Input Buffer Mapping Translation Header ((Size << 4) | 0xA)
+         *      4 : Input Buffer Pointer
+         *  Outputs:
+         *      0 : 0x00080042
+         *      1 : Result of function, 0 on success, otherwise error code
+         */
+        void SetNotificationMessage(Kernel::HLERequestContext& ctx);
+
+        /**
+         * SetNotificationImage service function.
+         *  Inputs:
+         *      0 : 0x00090082
+         *      1 : Notification index
+         *      2 : Size
+         *      3 : Input Buffer Mapping Translation Header ((Size << 4) | 0xA)
+         *      4 : Input Buffer Pointer
+         *  Outputs:
+         *      0 : 0x00090042
+         *      1 : Result of function, 0 on success, otherwise error code
+         */
+        void SetNotificationImage(Kernel::HLERequestContext& ctx);
+
+        /**
+         * GetNewsDBHeader service function.
+         *  Inputs:
+         *      0 : 0x000A0042
+         *      1 : Size
+         *      2 : Output Buffer Mapping Translation Header ((Size << 4) | 0xC)
+         *      3 : Output Buffer Pointer
+         *  Outputs:
+         *      0 : 0x000A0082
+         *      1 : Result of function, 0 on success, otherwise error code
+         *      2 : Actual Size
+         */
+        void GetNewsDBHeader(Kernel::HLERequestContext& ctx);
+
+        /**
+         * GetNotificationHeader service function.
+         *  Inputs:
+         *      0 : 0x000B0082
+         *      1 : Notification index
+         *      2 : Size
+         *      3 : Output Buffer Mapping Translation Header ((Size << 4) | 0xC)
+         *      4 : Output Buffer Pointer
+         *  Outputs:
+         *      0 : 0x000B0082
+         *      1 : Result of function, 0 on success, otherwise error code
+         *      2 : Actual Size
+         */
+        void GetNotificationHeader(Kernel::HLERequestContext& ctx);
+
+        /**
+         * GetNotificationMessage service function.
+         *  Inputs:
+         *      0 : 0x000C0082
+         *      1 : Notification index
+         *      2 : Size
+         *      3 : Output Buffer Mapping Translation Header ((Size << 4) | 0xC)
+         *      4 : Output Buffer Pointer
+         *  Outputs:
+         *      0 : 0x000C0082
+         *      1 : Result of function, 0 on success, otherwise error code
+         *      2 : Actual Size
+         */
+        void GetNotificationMessage(Kernel::HLERequestContext& ctx);
+
+        /**
+         * GetNotificationImage service function.
+         *  Inputs:
+         *      0 : 0x000D0082
+         *      1 : Notification index
+         *      2 : Size
+         *      3 : Output Buffer Mapping Translation Header ((Size << 4) | 0xC)
+         *      4 : Output Buffer Pointer
+         *  Outputs:
+         *      0 : 0x000D0082
+         *      1 : Result of function, 0 on success, otherwise error code
+         *      2 : Actual Size
+         */
+        void GetNotificationImage(Kernel::HLERequestContext& ctx);
+
+        /**
+         * SetAutomaticSyncFlag service function.
+         *  Inputs:
+         *      0 : 0x00110040
+         *      1 : Flag
+         *  Outputs:
+         *      0 : 0x00110040
+         *      1 : Result of function, 0 on success, otherwise error code
+         */
+        void SetAutomaticSyncFlag(Kernel::HLERequestContext& ctx);
+
+        /**
+         * SetNotificationHeaderOther service function.
+         *  Inputs:
+         *      0 : 0x00120082
+         *      1 : Notification index
+         *      2 : Size
+         *      3 : Input Buffer Mapping Translation Header ((Size << 4) | 0xA)
+         *      4 : Input Buffer Pointer
+         *  Outputs:
+         *      0 : 0x00120042
+         *      1 : Result of function, 0 on success, otherwise error code
+         */
+        void SetNotificationHeaderOther(Kernel::HLERequestContext& ctx);
+
+        /**
+         * WriteNewsDBSavedata service function.
+         *  Inputs:
+         *      0 : 0x00130000
+         *  Outputs:
+         *      0 : 0x00130040
+         *      1 : Result of function, 0 on success, otherwise error code
+         */
+        void WriteNewsDBSavedata(Kernel::HLERequestContext& ctx);
+
+        /**
+         * GetTotalArrivedNotifications service function.
+         *  Inputs:
+         *      0 : 0x00140000
+         *  Outputs:
+         *      0 : 0x00140080
+         *      1 : Result of function, 0 on success, otherwise error code
+         *      2 : Number of pending notifications to be synced
+         */
+        void GetTotalArrivedNotifications(Kernel::HLERequestContext& ctx);
+
+    protected:
+        std::shared_ptr<Module> news;
+    };
+
+private:
+    /**
+     * Gets the total number of notifications
+     * @returns Number of notifications
+     */
+    std::size_t GetTotalNotifications();
+
+    /**
+     * Loads the News DB into the given buffer
+     * @param header The header buffer
+     * @param size The size of the header buffer
+     * @returns Number of bytes read, or error code
+     */
+    ResultVal<std::size_t> GetNewsDBHeader(NewsDBHeader* header, const std::size_t size);
+
+    /**
+     * Loads the header for a notification ID into the given buffer
+     * @param notification_index The index of the notification ID
+     * @param header The header buffer
+     * @param size The size of the header buffer
+     * @returns Number of bytes read, or error code
+     */
+    ResultVal<std::size_t> GetNotificationHeader(const u32 notification_index,
+                                                 NotificationHeader* header,
+                                                 const std::size_t size);
+
+    /**
+     * Opens the message file for a notification ID and loads it to the message buffer
+     * @param notification_index The index of the notification ID
+     * @param mesasge The message buffer
+     * @returns Number of bytes read, or error code
+     */
+    ResultVal<std::size_t> GetNotificationMessage(const u32 notification_index,
+                                                  std::span<u8> message);
+
+    /**
+     * Opens the image file for a notification ID and loads it to the image buffer
+     * @param notification_index The index of the notification ID
+     * @param image The image buffer
+     * @returns Number of bytes read, or error code
+     */
+    ResultVal<std::size_t> GetNotificationImage(const u32 notification_index, std::span<u8> image);
+
+    /**
+     * Modifies the header for the News DB in memory and saves the News DB file
+     * @param header The database header
+     * @param size The amount of bytes to copy from the header
+     * @returns Result indicating the result of the operation, 0 on success
+     */
+    Result SetNewsDBHeader(const NewsDBHeader* header, const std::size_t size);
+
+    /**
+     * Modifies the header for a notification ID on memory and saves the News DB file
+     * @param notification_index The index of the notification ID
+     * @param header The notification header
+     * @param size The amount of bytes to copy from the header
+     * @returns Result indicating the result of the operation, 0 on success
+     */
+    Result SetNotificationHeader(const u32 notification_index, const NotificationHeader* header,
+                                 const std::size_t size);
+
+    /**
+     * Modifies the header for a notification ID on memory. The News DB file isn't updated
+     * @param notification_index The index of the notification ID
+     * @param header The notification header
+     * @param size The amount of bytes to copy from the header
+     * @returns Result indicating the result of the operation, 0 on success
+     */
+    Result SetNotificationHeaderOther(const u32 notification_index,
+                                      const NotificationHeader* header, const std::size_t size);
+
+    /**
+     * Sets a given message to a notification ID
+     * @param notification_index The index of the notification ID
+     * @param message The notification message
+     * @returns Result indicating the result of the operation, 0 on success
+     */
+    Result SetNotificationMessage(const u32 notification_index, std::span<const u8> message);
+
+    /**
+     * Sets a given image to a notification ID
+     * @param notification_index The index of the notification ID
+     * @param image The notification image
+     * @returns Result indicating the result of the operation, 0 on success
+     */
+    Result SetNotificationImage(const u32 notification_index, std::span<const u8> image);
+
+    /**
+     * Creates a new notification with the given data and saves all the contents
+     * @param header The notification header
+     * @param header_size The amount of bytes to copy from the header
+     * @param message The notification message
+     * @param image The notification image
+     * @returns Result indicating the result of the operation, 0 on success
+     */
+    Result SaveNotification(const NotificationHeader* header, const std::size_t header_size,
+                            std::span<const u8> message, std::span<const u8> image);
+
+    /**
+     * Deletes the given notification ID from the database
+     * @param notification_id The notification ID to delete
+     * @returns Result indicating the result of the operation, 0 on success
+     */
+    Result DeleteNotification(const u32 notification_id);
+
+    /**
+     * Opens the news.db savedata file and load it to the memory buffer. If the file or the savedata
+     * don't exist, they are created
+     * @returns Result indicating the result of the operation, 0 on success
+     */
+    Result LoadNewsDBSavedata();
+
+    /**
+     * Writes the news.db savedata file to the the NEWS system savedata
+     * @returns Result indicating the result of the operation, 0 on success
+     */
+    Result SaveNewsDBSavedata();
+
+    /**
+     * Opens the file with the given filename inside the NEWS system savedata
+     * @param filename The file to open
+     * @param buffer The buffer to output the contents on
+     * @returns Number of bytes read, or error code
+     */
+    ResultVal<std::size_t> LoadFileFromSavedata(std::string filename, std::span<u8> buffer);
+
+    /**
+     * Writes the file with the given filename inside the NEWS system savedata
+     * @param filename The output file
+     * @param buffer The buffer to read the contents from
+     * @returns Result indicating the result of the operation, 0 on success
+     */
+    Result SaveFileToSavedata(std::string filename, std::span<const u8> buffer);
+
+    bool CompareNotifications(const u32 first_id, const u32 second_id);
+
+private:
+    Core::System& system;
+
+    NewsDB db{};
+    std::array<u32, MAX_NOTIFICATIONS> notification_ids; // Notifications ordered by date time
+    u8 automatic_sync_flag;
+    std::unique_ptr<FileSys::ArchiveBackend> news_system_save_data_archive;
+
+    template <class Archive>
+    void serialize(Archive& ar, const unsigned int);
+    friend class boost::serialization::access;
+};
 
 void InstallInterfaces(Core::System& system);
 
 } // namespace Service::NEWS
+
+SERVICE_CONSTRUCT(Service::NEWS::Module)
+BOOST_CLASS_EXPORT_KEY(Service::NEWS::Module)
diff --git a/src/core/hle/service/news/news_s.cpp b/src/core/hle/service/news/news_s.cpp
index c8981cc69..e32f190ee 100644
--- a/src/core/hle/service/news/news_s.cpp
+++ b/src/core/hle/service/news/news_s.cpp
@@ -3,63 +3,33 @@
 // Refer to the license.txt file included.
 
 #include "common/archives.h"
-#include "core/hle/ipc_helpers.h"
 #include "core/hle/service/news/news_s.h"
 
 SERIALIZE_EXPORT_IMPL(Service::NEWS::NEWS_S)
 
 namespace Service::NEWS {
 
-struct NewsDbHeader {
-    u8 unknown_one;
-    u8 flags;
-    INSERT_PADDING_BYTES(0xE);
-};
-static_assert(sizeof(NewsDbHeader) == 0x10, "News DB Header structure size is wrong");
-
-void NEWS_S::GetTotalNotifications(Kernel::HLERequestContext& ctx) {
-    IPC::RequestParser rp(ctx);
-
-    LOG_WARNING(Service, "(STUBBED) called");
-
-    IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
-
-    rb.Push(ResultSuccess);
-    rb.Push<u32>(0);
-}
-
-void NEWS_S::GetNewsDBHeader(Kernel::HLERequestContext& ctx) {
-    IPC::RequestParser rp(ctx);
-    const auto size = rp.Pop<u32>();
-    auto output_buffer = rp.PopMappedBuffer();
-
-    LOG_WARNING(Service, "(STUBBED) called size={}", size);
-
-    NewsDbHeader dummy = {.unknown_one = 1, .flags = 0};
-    output_buffer.Write(&dummy, 0, std::min(sizeof(NewsDbHeader), static_cast<std::size_t>(size)));
-
-    IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
-
-    rb.Push(ResultSuccess);
-    rb.Push<u32>(size);
-}
-
-NEWS_S::NEWS_S() : ServiceFramework("news:s", 2) {
+NEWS_S::NEWS_S(std::shared_ptr<Module> news) : Module::Interface(std::move(news), "news:s", 2) {
     const FunctionInfo functions[] = {
         // clang-format off
-        {0x0001, nullptr, "AddNotification"},
+        {0x0001, &NEWS_S::AddNotificationSystem, "AddNotification"},
+        {0x0004, &NEWS_S::ResetNotifications, "ResetNotifications"},
         {0x0005, &NEWS_S::GetTotalNotifications, "GetTotalNotifications"},
-        {0x0006, nullptr, "SetNewsDBHeader"},
-        {0x0007, nullptr, "SetNotificationHeader"},
-        {0x0008, nullptr, "SetNotificationMessage"},
-        {0x0009, nullptr, "SetNotificationImage"},
+        {0x0006, &NEWS_S::SetNewsDBHeader, "SetNewsDBHeader"},
+        {0x0007, &NEWS_S::SetNotificationHeader, "SetNotificationHeader"},
+        {0x0008, &NEWS_S::SetNotificationMessage, "SetNotificationMessage"},
+        {0x0009, &NEWS_S::SetNotificationImage, "SetNotificationImage"},
         {0x000A, &NEWS_S::GetNewsDBHeader, "GetNewsDBHeader"},
-        {0x000B, nullptr, "GetNotificationHeader"},
-        {0x000C, nullptr, "GetNotificationMessage"},
-        {0x000D, nullptr, "GetNotificationImage"},
+        {0x000B, &NEWS_S::GetNotificationHeader, "GetNotificationHeader"},
+        {0x000C, &NEWS_S::GetNotificationMessage, "GetNotificationMessage"},
+        {0x000D, &NEWS_S::GetNotificationImage, "GetNotificationImage"},
         {0x000E, nullptr, "SetInfoLEDPattern"},
-        {0x0012, nullptr, "GetNotificationHeaderOther"},
-        {0x0013, nullptr, "WriteNewsDBSavedata"},
+        {0x000F, nullptr, "SyncArrivedNotifications"},
+        {0x0010, nullptr, "SyncOneArrivedNotification"},
+        {0x0011, &NEWS_S::SetAutomaticSyncFlag, "SetAutomaticSyncFlag"},
+        {0x0012, &NEWS_S::SetNotificationHeaderOther, "SetNotificationHeaderOther"},
+        {0x0013, &NEWS_S::WriteNewsDBSavedata, "WriteNewsDBSavedata"},
+        {0x0014, &NEWS_S::GetTotalArrivedNotifications, "GetTotalArrivedNotifications"},
         // clang-format on
     };
     RegisterHandlers(functions);
diff --git a/src/core/hle/service/news/news_s.h b/src/core/hle/service/news/news_s.h
index a12d496c1..186fc9b0d 100644
--- a/src/core/hle/service/news/news_s.h
+++ b/src/core/hle/service/news/news_s.h
@@ -4,44 +4,19 @@
 
 #pragma once
 
-#include <memory>
-#include "core/hle/service/service.h"
+#include "core/hle/service/news/news.h"
 
 namespace Service::NEWS {
 
-class NEWS_S final : public ServiceFramework<NEWS_S> {
+class NEWS_S final : public Module::Interface {
 public:
-    NEWS_S();
+    explicit NEWS_S(std::shared_ptr<Module> news);
 
 private:
-    /**
-     * GetTotalNotifications service function.
-     *  Inputs:
-     *      0 : 0x00050000
-     *  Outputs:
-     *      0 : 0x00050080
-     *      1 : Result of function, 0 on success, otherwise error code
-     *      2 : Number of notifications
-     */
-    void GetTotalNotifications(Kernel::HLERequestContext& ctx);
-
-    /**
-     * GetNewsDBHeader service function.
-     *  Inputs:
-     *      0 : 0x000A0042
-     *      1 : Size
-     *      2 : Output Buffer Mapping Translation Header ((Size << 4) | 0xC)
-     *      3 : Output Buffer Pointer
-     *  Outputs:
-     *      0 : 0x000A0080
-     *      1 : Result of function, 0 on success, otherwise error code
-     *      2 : Actual Size
-     */
-    void GetNewsDBHeader(Kernel::HLERequestContext& ctx);
-
-    SERVICE_SERIALIZATION_SIMPLE
+    SERVICE_SERIALIZATION(NEWS_S, news, Module)
 };
 
 } // namespace Service::NEWS
 
 BOOST_CLASS_EXPORT_KEY(Service::NEWS::NEWS_S)
+BOOST_SERIALIZATION_CONSTRUCT(Service::NEWS::NEWS_S)
diff --git a/src/core/hle/service/news/news_u.cpp b/src/core/hle/service/news/news_u.cpp
index 1753b8e18..e39babd30 100644
--- a/src/core/hle/service/news/news_u.cpp
+++ b/src/core/hle/service/news/news_u.cpp
@@ -9,10 +9,10 @@ SERIALIZE_EXPORT_IMPL(Service::NEWS::NEWS_U)
 
 namespace Service::NEWS {
 
-NEWS_U::NEWS_U() : ServiceFramework("news:u", 1) {
+NEWS_U::NEWS_U(std::shared_ptr<Module> news) : Module::Interface(std::move(news), "news:u", 1) {
     const FunctionInfo functions[] = {
         // clang-format off
-        {0x0001, nullptr, "AddNotification"},
+        {0x0001, &NEWS_U::AddNotification, "AddNotification"},
         // clang-format on
     };
     RegisterHandlers(functions);
diff --git a/src/core/hle/service/news/news_u.h b/src/core/hle/service/news/news_u.h
index 8e672256d..7a23a6fb8 100644
--- a/src/core/hle/service/news/news_u.h
+++ b/src/core/hle/service/news/news_u.h
@@ -4,19 +4,19 @@
 
 #pragma once
 
-#include <memory>
-#include "core/hle/service/service.h"
+#include "core/hle/service/news/news.h"
 
 namespace Service::NEWS {
 
-class NEWS_U final : public ServiceFramework<NEWS_U> {
+class NEWS_U final : public Module::Interface {
 public:
-    NEWS_U();
+    explicit NEWS_U(std::shared_ptr<Module> news);
 
 private:
-    SERVICE_SERIALIZATION_SIMPLE
+    SERVICE_SERIALIZATION(NEWS_U, news, Module)
 };
 
 } // namespace Service::NEWS
 
 BOOST_CLASS_EXPORT_KEY(Service::NEWS::NEWS_U)
+BOOST_SERIALIZATION_CONSTRUCT(Service::NEWS::NEWS_U)