using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using AutoMapper; using Kavita.API.Database; using Kavita.API.Repositories; using Kavita.API.Services; using Kavita.API.Services.SignalR; using Kavita.Common; using Kavita.Models.Constants; using Kavita.Models.DTOs.Device.ClientDevice; using Kavita.Models.DTOs.Device.EmailDevice; using Kavita.Models.DTOs.Progress; using Kavita.Models.DTOs.SignalR; using Kavita.Server.Attributes; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Kavita.Server.Controllers; /// /// Responsible for interacting and creating Devices /// public class DeviceController( IUnitOfWork unitOfWork, IDeviceService deviceService, IEventHub eventHub, ILocalizationService localizationService, IMapper mapper, IClientDeviceService clientDeviceService) : BaseApiController { /// /// Creates a new Device /// /// /// [HttpPost("create")] [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> CreateOrUpdateDevice(CreateEmailDeviceDto dto) { var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Devices); if (user == null) return Unauthorized(); try { var device = await deviceService.Create(dto, user); if (device == null) return BadRequest(await localizationService.Translate(UserId, "generic-device-create")); return Ok(mapper.Map(device)); } catch (KavitaException ex) { return BadRequest(await localizationService.Translate(UserId, ex.Message)); } } /// /// Updates an existing Device /// /// /// [HttpPost("update")] [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdateDevice(UpdateEmailDeviceDto dto) { var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Devices); if (user == null) return Unauthorized(); var device = await deviceService.Update(dto, user); if (device == null) return BadRequest(await localizationService.Translate(UserId, "generic-device-update")); return Ok(mapper.Map(device)); } /// /// Deletes the device from the user /// /// /// [HttpDelete] [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteDevice(int deviceId) { if (deviceId <= 0) return BadRequest(await localizationService.Translate(UserId, "device-doesnt-exist")); var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Devices); if (user == null) return Unauthorized(); if (await deviceService.Delete(user, deviceId)) return Ok(); return BadRequest(await localizationService.Translate(UserId, "generic-device-delete")); } [HttpGet] public async Task>> GetDevices() { return Ok(await unitOfWork.DeviceRepository.GetDevicesForUserAsync(UserId)); } /// /// Sends a collection of chapters to the user's device /// /// /// [HttpPost("send-to")] [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task SendToDevice(SendToEmailDeviceDto dto) { var userId = UserId; if (dto.ChapterIds.Any(i => i < 0)) return BadRequest(await localizationService.Translate(userId, "greater-0", "ChapterIds")); if (dto.DeviceId < 0) return BadRequest(await localizationService.Translate(userId, "greater-0", "DeviceId")); var isEmailSetup = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).IsEmailSetupForSendToDevice(); if (!isEmailSetup) return BadRequest(await localizationService.Translate(userId, "send-to-kavita-email")); // // Validate that the device belongs to the user var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.Devices); if (user == null || user.Devices.All(d => d.Id != dto.DeviceId)) return BadRequest(await localizationService.Translate(userId, "send-to-unallowed")); await eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, MessageFactory.SendingToDeviceEvent(await localizationService.Translate(userId, "send-to-device-status"), "started"), userId); try { var success = await deviceService.SendTo(dto.ChapterIds, dto.DeviceId); if (success) return Ok(); } catch (KavitaException ex) { return BadRequest(await localizationService.Translate(userId, ex.Message)); } finally { await eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, MessageFactory.SendingToDeviceEvent(await localizationService.Translate(userId, "send-to-device-status"), "ended"), userId); } return BadRequest(await localizationService.Translate(userId, "generic-send-to")); } /// /// Attempts to send a whole series to a device. /// /// /// [HttpPost("send-series-to")] [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task SendSeriesToDevice(SendSeriesToEmailDeviceDto dto) { var userId = UserId; if (dto.SeriesId <= 0) return BadRequest(await localizationService.Translate(userId, "greater-0", "SeriesId")); if (dto.DeviceId < 0) return BadRequest(await localizationService.Translate(userId, "greater-0", "DeviceId")); var isEmailSetup = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).IsEmailSetupForSendToDevice(); if (!isEmailSetup) return BadRequest(await localizationService.Translate(userId, "send-to-kavita-email")); await eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, MessageFactory.SendingToDeviceEvent(await localizationService.Translate(userId, "send-to-device-status"), "started"), userId); var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, SeriesIncludes.Volumes | SeriesIncludes.Chapters); if (series == null) return BadRequest(await localizationService.Translate(userId, "series-doesnt-exist")); var chapterIds = series.Volumes.SelectMany(v => v.Chapters.Select(c => c.Id)).ToList(); try { var success = await deviceService.SendTo(chapterIds, dto.DeviceId); if (success) return Ok(); } catch (KavitaException ex) { return BadRequest(await localizationService.Translate(userId, ex.Message)); } finally { await eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, MessageFactory.SendingToDeviceEvent(await localizationService.Translate(userId, "send-to-device-status"), "ended"), userId); } return BadRequest(await localizationService.Translate(userId, "generic-send-to")); } #region Client Devices /// /// Get my client devices /// /// /// [HttpGet("client/devices")] public async Task>> GetMyClientDevices(bool includeInactive = false) { return Ok(await unitOfWork.ClientDeviceRepository.GetUserDeviceDtosAsync(UserId, includeInactive)); } /// /// Get All user client devices /// /// /// [Authorize(PolicyGroups.AdminPolicy)] [HttpGet("client/all-devices")] public async Task>> GetAllClientDevices(bool includeInactive = false) { return Ok(await unitOfWork.ClientDeviceRepository.GetAllUserDeviceDtos(includeInactive)); } /// /// Removes the client device from DB /// /// /// [HttpDelete("client/device")] [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> DeleteClientDevice(int clientDeviceId) { return Ok(await clientDeviceService.DeleteDeviceAsync(UserId, clientDeviceId)); } /// /// Update the friendly name of the Device /// /// /// [HttpPost("client/update-name")] [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateClientDeviceName(UpdateClientDeviceNameDto dto) { await clientDeviceService.UpdateFriendlyNameAsync(UserId, dto); return Ok(); } #endregion Client Devices }