CloudPackageChecker.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. # Copyright (c) 2020 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import json
  4. from typing import List, Dict, Any, Set
  5. from typing import Optional
  6. from PyQt5.QtCore import QObject
  7. from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
  8. from UM import i18nCatalog
  9. from UM.Logger import Logger
  10. from UM.Message import Message
  11. from UM.Signal import Signal
  12. from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
  13. from cura.API.Account import SyncState
  14. from cura.CuraApplication import CuraApplication, ApplicationMetadata
  15. from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
  16. from .SubscribedPackagesModel import SubscribedPackagesModel
  17. from ..CloudApiModel import CloudApiModel
  18. class CloudPackageChecker(QObject):
  19. SYNC_SERVICE_NAME = "CloudPackageChecker"
  20. def __init__(self, application: CuraApplication) -> None:
  21. super().__init__()
  22. self.discrepancies = Signal() # Emits SubscribedPackagesModel
  23. self._application = application # type: CuraApplication
  24. self._scope = JsonDecoratorScope(UltimakerCloudScope(application))
  25. self._model = SubscribedPackagesModel()
  26. self._message = None # type: Optional[Message]
  27. self._application.initializationFinished.connect(self._onAppInitialized)
  28. self._i18n_catalog = i18nCatalog("cura")
  29. self._sdk_version = ApplicationMetadata.CuraSDKVersion
  30. self._last_notified_packages = set() # type: Set[str]
  31. """Packages for which a notification has been shown. No need to bother the user twice fo equal content"""
  32. # This is a plugin, so most of the components required are not ready when
  33. # this is initialized. Therefore, we wait until the application is ready.
  34. def _onAppInitialized(self) -> None:
  35. self._package_manager = self._application.getPackageManager()
  36. # initial check
  37. self._getPackagesIfLoggedIn()
  38. self._application.getCuraAPI().account.loginStateChanged.connect(self._onLoginStateChanged)
  39. self._application.getCuraAPI().account.syncRequested.connect(self._getPackagesIfLoggedIn)
  40. def _onLoginStateChanged(self) -> None:
  41. # reset session
  42. self._last_notified_packages = set()
  43. self._getPackagesIfLoggedIn()
  44. def _getPackagesIfLoggedIn(self) -> None:
  45. if self._application.getCuraAPI().account.isLoggedIn:
  46. self._getUserSubscribedPackages()
  47. else:
  48. self._hideSyncMessage()
  49. def _getUserSubscribedPackages(self) -> None:
  50. self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SYNCING)
  51. Logger.debug("Requesting subscribed packages metadata from server.")
  52. url = CloudApiModel.api_url_user_packages
  53. self._application.getHttpRequestManager().get(url,
  54. callback = self._onUserPackagesRequestFinished,
  55. error_callback = self._onUserPackagesRequestFinished,
  56. scope = self._scope)
  57. def _onUserPackagesRequestFinished(self, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"] = None) -> None:
  58. if error is not None or reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
  59. Logger.log("w",
  60. "Requesting user packages failed, response code %s while trying to connect to %s",
  61. reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url())
  62. self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR)
  63. return
  64. try:
  65. json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
  66. # Check for errors:
  67. if "errors" in json_data:
  68. for error in json_data["errors"]:
  69. Logger.log("e", "%s", error["title"])
  70. self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR)
  71. return
  72. self._handleCompatibilityData(json_data["data"])
  73. except json.decoder.JSONDecodeError:
  74. Logger.log("w", "Received invalid JSON for user subscribed packages from the Web Marketplace")
  75. self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SUCCESS)
  76. def _handleCompatibilityData(self, subscribed_packages_payload: List[Dict[str, Any]]) -> None:
  77. user_subscribed_packages = {plugin["package_id"] for plugin in subscribed_packages_payload}
  78. user_installed_packages = self._package_manager.getAllInstalledPackageIDs()
  79. if user_subscribed_packages == self._last_notified_packages:
  80. # already notified user about these
  81. return
  82. # We need to re-evaluate the dismissed packages
  83. # (i.e. some package might got updated to the correct SDK version in the meantime,
  84. # hence remove them from the Dismissed Incompatible list)
  85. self._package_manager.reEvaluateDismissedPackages(subscribed_packages_payload, self._sdk_version)
  86. user_dismissed_packages = self._package_manager.getDismissedPackages()
  87. if user_dismissed_packages:
  88. user_installed_packages += user_dismissed_packages
  89. # We check if there are packages installed in Web Marketplace but not in Cura marketplace
  90. package_discrepancy = list(user_subscribed_packages.difference(user_installed_packages))
  91. if package_discrepancy:
  92. Logger.log("d", "Discrepancy found between Cloud subscribed packages and Cura installed packages")
  93. self._model.addDiscrepancies(package_discrepancy)
  94. self._model.initialize(self._package_manager, subscribed_packages_payload)
  95. self._showSyncMessage()
  96. self._last_notified_packages = user_subscribed_packages
  97. def _showSyncMessage(self) -> None:
  98. """Show the message if it is not already shown"""
  99. if self._message is not None:
  100. self._message.show()
  101. return
  102. sync_message = Message(self._i18n_catalog.i18nc(
  103. "@info:generic",
  104. "Do you want to sync material and software packages with your account?"),
  105. title = self._i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ),
  106. lifetime = 0)
  107. sync_message.addAction("sync",
  108. name = self._i18n_catalog.i18nc("@action:button", "Sync"),
  109. icon = "",
  110. description = "Sync your Cloud subscribed packages to your local environment.",
  111. button_align = Message.ActionButtonAlignment.ALIGN_RIGHT)
  112. sync_message.actionTriggered.connect(self._onSyncButtonClicked)
  113. sync_message.show()
  114. self._message = sync_message
  115. def _hideSyncMessage(self) -> None:
  116. """Hide the message if it is showing"""
  117. if self._message is not None:
  118. self._message.hide()
  119. self._message = None
  120. def _onSyncButtonClicked(self, sync_message: Message, sync_message_action: str) -> None:
  121. sync_message.hide()
  122. self._hideSyncMessage() # Should be the same message, but also sets _message to None
  123. self.discrepancies.emit(self._model)