diff --git a/openlp/core/common/enum.py b/openlp/core/common/enum.py index c022d2c8d..325392a76 100644 --- a/openlp/core/common/enum.py +++ b/openlp/core/common/enum.py @@ -156,6 +156,7 @@ class SyncType(IntEnum): Disabled = 0 Folder = 1 Ftp = 2 + WebService = 3 @unique diff --git a/openlp/plugins/remotesync/lib/backends/restclient/__init__.py b/openlp/plugins/remotesync/lib/backends/restclient/__init__.py new file mode 100644 index 000000000..7ea1f7d5d --- /dev/null +++ b/openlp/plugins/remotesync/lib/backends/restclient/__init__.py @@ -0,0 +1,3 @@ +from .api_config import * +from .models import * +from .services import * diff --git a/openlp/plugins/remotesync/lib/backends/restclient/api_config.py b/openlp/plugins/remotesync/lib/backends/restclient/api_config.py new file mode 100644 index 000000000..c3d8b19b9 --- /dev/null +++ b/openlp/plugins/remotesync/lib/backends/restclient/api_config.py @@ -0,0 +1,30 @@ +import os +from typing import Optional, Union + +from pydantic import BaseModel, Field + + +class APIConfig(BaseModel): + base_path: str = "http://localhost:8888/api/v1" + verify: Union[bool, str] = True + + def get_access_token(self) -> Optional[str]: + try: + return os.environ["access_token"] + except KeyError: + return None + + def set_access_token(self, value: str): + raise Exception( + "This client was generated with an environment variable for the access token. Please set the environment variable 'access_token' to the access token." + ) + + +class HTTPException(Exception): + def __init__(self, status_code: int, message: str): + self.status_code = status_code + self.message = message + super().__init__(f"{status_code} {message}") + + def __str__(self): + return f"{self.status_code} {self.message}" diff --git a/openlp/plugins/remotesync/lib/backends/restclient/models/ApiResponse.py b/openlp/plugins/remotesync/lib/backends/restclient/models/ApiResponse.py new file mode 100644 index 000000000..b0c6781ee --- /dev/null +++ b/openlp/plugins/remotesync/lib/backends/restclient/models/ApiResponse.py @@ -0,0 +1,16 @@ +from typing import * + +from pydantic import BaseModel, Field + + +class ApiResponse(BaseModel): + """ + None model + + """ + + code: Optional[int] = Field(alias="code", default=None) + + type: Optional[str] = Field(alias="type", default=None) + + message: Optional[str] = Field(alias="message", default=None) diff --git a/openlp/plugins/remotesync/lib/backends/restclient/models/ItemInfo.py b/openlp/plugins/remotesync/lib/backends/restclient/models/ItemInfo.py new file mode 100644 index 000000000..ba98b1a6c --- /dev/null +++ b/openlp/plugins/remotesync/lib/backends/restclient/models/ItemInfo.py @@ -0,0 +1,18 @@ +from typing import * + +from pydantic import BaseModel, Field + + +class ItemInfo(BaseModel): + """ + None model + + """ + + uuid: Optional[str] = Field(alias="uuid", default=None) + + version: Optional[int] = Field(alias="version", default=None) + + created: Optional[str] = Field(alias="created", default=None) + + updated: Optional[str] = Field(alias="updated", default=None) diff --git a/openlp/plugins/remotesync/lib/backends/restclient/models/ItemList.py b/openlp/plugins/remotesync/lib/backends/restclient/models/ItemList.py new file mode 100644 index 000000000..7aadf6898 --- /dev/null +++ b/openlp/plugins/remotesync/lib/backends/restclient/models/ItemList.py @@ -0,0 +1,14 @@ +from typing import * + +from pydantic import BaseModel, Field + +from .ItemInfo import ItemInfo + + +class ItemList(BaseModel): + """ + None model + + """ + + list: Optional[List[Optional[ItemInfo]]] = Field(alias="list", default=None) diff --git a/openlp/plugins/remotesync/lib/backends/restclient/models/TextItem.py b/openlp/plugins/remotesync/lib/backends/restclient/models/TextItem.py new file mode 100644 index 000000000..291b008ac --- /dev/null +++ b/openlp/plugins/remotesync/lib/backends/restclient/models/TextItem.py @@ -0,0 +1,22 @@ +from typing import * + +from pydantic import BaseModel, Field + + +class TextItem(BaseModel): + """ + None model + + """ + + uuid: Optional[str] = Field(alias="uuid", default=None) + + version: Optional[int] = Field(alias="version", default=None) + + created: Optional[str] = Field(alias="created", default=None) + + updated: Optional[str] = Field(alias="updated", default=None) + + title: Optional[str] = Field(alias="title", default=None) + + itemxml: Optional[str] = Field(alias="itemxml", default=None) diff --git a/openlp/plugins/remotesync/lib/backends/restclient/models/__init__.py b/openlp/plugins/remotesync/lib/backends/restclient/models/__init__.py new file mode 100644 index 000000000..b8e737926 --- /dev/null +++ b/openlp/plugins/remotesync/lib/backends/restclient/models/__init__.py @@ -0,0 +1,4 @@ +from .ApiResponse import * +from .ItemInfo import * +from .ItemList import * +from .TextItem import * diff --git a/openlp/plugins/remotesync/lib/backends/restclient/readme.txt b/openlp/plugins/remotesync/lib/backends/restclient/readme.txt new file mode 100644 index 000000000..2086060a7 --- /dev/null +++ b/openlp/plugins/remotesync/lib/backends/restclient/readme.txt @@ -0,0 +1,3 @@ +The restclient client was created using the OpenAPI python generator + +See https://marcomuellner.github.io/openapi-python-generator/ diff --git a/openlp/plugins/remotesync/lib/backends/restclient/services/__init__.py b/openlp/plugins/remotesync/lib/backends/restclient/services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openlp/plugins/remotesync/lib/backends/restclient/services/custom_service.py b/openlp/plugins/remotesync/lib/backends/restclient/services/custom_service.py new file mode 100644 index 000000000..e17c6794d --- /dev/null +++ b/openlp/plugins/remotesync/lib/backends/restclient/services/custom_service.py @@ -0,0 +1,169 @@ +import json +from typing import * + +import requests + +from ..api_config import APIConfig, HTTPException +from ..models import * + + +def getCustomslideList(churchId: str, api_config_override: Optional[APIConfig] = None) -> ItemList: + api_config = api_config_override if api_config_override else APIConfig() + + base_path = api_config.base_path + path = f"/{churchId}/custom-list" + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": f"Bearer { api_config.get_access_token() }", + } + query_params: Dict[str, Any] = {} + + query_params = {key: value for (key, value) in query_params.items() if value is not None} + + response = requests.request( + "get", + f"{base_path}{path}", + headers=headers, + params=query_params, + verify=api_config.verify, + ) + if response.status_code != 200: + raise HTTPException(response.status_code, f" failed with status code: {response.status_code}") + + return ItemList(**response.json()) if response.json() is not None else ItemList() + + +def getCustomslide(churchId: str, uuid: str, api_config_override: Optional[APIConfig] = None) -> TextItem: + api_config = api_config_override if api_config_override else APIConfig() + + base_path = api_config.base_path + path = f"/{churchId}/custom/{uuid}" + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": f"Bearer { api_config.get_access_token() }", + } + query_params: Dict[str, Any] = {} + + query_params = {key: value for (key, value) in query_params.items() if value is not None} + + response = requests.request( + "get", + f"{base_path}{path}", + headers=headers, + params=query_params, + verify=api_config.verify, + ) + if response.status_code != 200: + raise HTTPException(response.status_code, f" failed with status code: {response.status_code}") + + return TextItem(**response.json()) if response.json() is not None else TextItem() + + +def updateCustomslide( + churchId: str, uuid: str, data: TextItem, api_config_override: Optional[APIConfig] = None +) -> None: + api_config = api_config_override if api_config_override else APIConfig() + + base_path = api_config.base_path + path = f"/{churchId}/custom/{uuid}" + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": f"Bearer { api_config.get_access_token() }", + } + query_params: Dict[str, Any] = {} + + query_params = {key: value for (key, value) in query_params.items() if value is not None} + + response = requests.request( + "put", f"{base_path}{path}", headers=headers, params=query_params, verify=api_config.verify, json=data.dict() + ) + if response.status_code != 200: + raise HTTPException(response.status_code, f" failed with status code: {response.status_code}") + + return None + + +def deleteCustomslide(churchId: str, uuid: str, api_config_override: Optional[APIConfig] = None) -> None: + api_config = api_config_override if api_config_override else APIConfig() + + base_path = api_config.base_path + path = f"/{churchId}/custom/{uuid}" + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": f"Bearer { api_config.get_access_token() }", + } + query_params: Dict[str, Any] = {} + + query_params = {key: value for (key, value) in query_params.items() if value is not None} + + response = requests.request( + "delete", + f"{base_path}{path}", + headers=headers, + params=query_params, + verify=api_config.verify, + ) + if response.status_code != 200: + raise HTTPException(response.status_code, f" failed with status code: {response.status_code}") + + return None + + +def getCustomslideVersion( + churchId: str, uuid: str, version: int, api_config_override: Optional[APIConfig] = None +) -> TextItem: + api_config = api_config_override if api_config_override else APIConfig() + + base_path = api_config.base_path + path = f"/{churchId}/custom/{uuid}/{version}" + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": f"Bearer { api_config.get_access_token() }", + } + query_params: Dict[str, Any] = {} + + query_params = {key: value for (key, value) in query_params.items() if value is not None} + + response = requests.request( + "get", + f"{base_path}{path}", + headers=headers, + params=query_params, + verify=api_config.verify, + ) + if response.status_code != 200: + raise HTTPException(response.status_code, f" failed with status code: {response.status_code}") + + return TextItem(**response.json()) if response.json() is not None else TextItem() + + +def getCustomslideHistory(churchId: str, uuid: str, api_config_override: Optional[APIConfig] = None) -> TextItem: + api_config = api_config_override if api_config_override else APIConfig() + + base_path = api_config.base_path + path = f"/{churchId}/custom-history/{uuid}" + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": f"Bearer { api_config.get_access_token() }", + } + query_params: Dict[str, Any] = {} + + query_params = {key: value for (key, value) in query_params.items() if value is not None} + + response = requests.request( + "get", + f"{base_path}{path}", + headers=headers, + params=query_params, + verify=api_config.verify, + ) + if response.status_code != 200: + raise HTTPException(response.status_code, f" failed with status code: {response.status_code}") + + return TextItem(**response.json()) if response.json() is not None else TextItem() diff --git a/openlp/plugins/remotesync/lib/backends/restclient/services/song_service.py b/openlp/plugins/remotesync/lib/backends/restclient/services/song_service.py new file mode 100644 index 000000000..315ad932b --- /dev/null +++ b/openlp/plugins/remotesync/lib/backends/restclient/services/song_service.py @@ -0,0 +1,165 @@ +import json +from typing import * + +import requests + +from ..api_config import APIConfig, HTTPException +from ..models import * + + +def getSongList(churchId: str, api_config_override: Optional[APIConfig] = None) -> ItemList: + api_config = api_config_override if api_config_override else APIConfig() + + base_path = api_config.base_path + path = f"/{churchId}/song-list" + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": f"Bearer { api_config.get_access_token() }", + } + query_params: Dict[str, Any] = {} + + query_params = {key: value for (key, value) in query_params.items() if value is not None} + + response = requests.request( + "get", + f"{base_path}{path}", + headers=headers, + params=query_params, + verify=api_config.verify, + ) + if response.status_code != 200: + raise HTTPException(response.status_code, f" failed with status code: {response.status_code}") + + return ItemList(**response.json()) if response.json() is not None else ItemList() + + +def getSong(churchId: str, uuid: str, api_config_override: Optional[APIConfig] = None) -> TextItem: + api_config = api_config_override if api_config_override else APIConfig() + + base_path = api_config.base_path + path = f"/{churchId}/song/{uuid}" + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": f"Bearer { api_config.get_access_token() }", + } + query_params: Dict[str, Any] = {} + + query_params = {key: value for (key, value) in query_params.items() if value is not None} + + response = requests.request( + "get", + f"{base_path}{path}", + headers=headers, + params=query_params, + verify=api_config.verify, + ) + if response.status_code != 200: + raise HTTPException(response.status_code, f" failed with status code: {response.status_code}") + + return TextItem(**response.json()) if response.json() is not None else TextItem() + + +def updateSong(churchId: str, uuid: str, data: TextItem, api_config_override: Optional[APIConfig] = None) -> None: + api_config = api_config_override if api_config_override else APIConfig() + + base_path = api_config.base_path + path = f"/{churchId}/song/{uuid}" + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": f"Bearer { api_config.get_access_token() }", + } + query_params: Dict[str, Any] = {} + + query_params = {key: value for (key, value) in query_params.items() if value is not None} + + response = requests.request( + "put", f"{base_path}{path}", headers=headers, params=query_params, verify=api_config.verify, json=data.dict() + ) + if response.status_code != 200: + raise HTTPException(response.status_code, f" failed with status code: {response.status_code}") + + return None + + +def deleteSong(churchId: str, uuid: str, api_config_override: Optional[APIConfig] = None) -> None: + api_config = api_config_override if api_config_override else APIConfig() + + base_path = api_config.base_path + path = f"/{churchId}/song/{uuid}" + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": f"Bearer { api_config.get_access_token() }", + } + query_params: Dict[str, Any] = {} + + query_params = {key: value for (key, value) in query_params.items() if value is not None} + + response = requests.request( + "delete", + f"{base_path}{path}", + headers=headers, + params=query_params, + verify=api_config.verify, + ) + if response.status_code != 200: + raise HTTPException(response.status_code, f" failed with status code: {response.status_code}") + + return None + + +def getSongVersion(churchId: str, uuid: str, version: int, api_config_override: Optional[APIConfig] = None) -> TextItem: + api_config = api_config_override if api_config_override else APIConfig() + + base_path = api_config.base_path + path = f"/{churchId}/song/{uuid}/{version}" + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": f"Bearer { api_config.get_access_token() }", + } + query_params: Dict[str, Any] = {} + + query_params = {key: value for (key, value) in query_params.items() if value is not None} + + response = requests.request( + "get", + f"{base_path}{path}", + headers=headers, + params=query_params, + verify=api_config.verify, + ) + if response.status_code != 200: + raise HTTPException(response.status_code, f" failed with status code: {response.status_code}") + + return TextItem(**response.json()) if response.json() is not None else TextItem() + + +def getSongHistory(churchId: str, uuid: str, api_config_override: Optional[APIConfig] = None) -> TextItem: + api_config = api_config_override if api_config_override else APIConfig() + + base_path = api_config.base_path + path = f"/{churchId}/song-history/{uuid}" + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": f"Bearer { api_config.get_access_token() }", + } + query_params: Dict[str, Any] = {} + + query_params = {key: value for (key, value) in query_params.items() if value is not None} + + response = requests.request( + "get", + f"{base_path}{path}", + headers=headers, + params=query_params, + verify=api_config.verify, + ) + if response.status_code != 200: + raise HTTPException(response.status_code, f" failed with status code: {response.status_code}") + + return TextItem(**response.json()) if response.json() is not None else TextItem() diff --git a/openlp/plugins/remotesync/lib/backends/webservicesynchronizer.py b/openlp/plugins/remotesync/lib/backends/webservicesynchronizer.py index 83138c60b..5b85c4507 100644 --- a/openlp/plugins/remotesync/lib/backends/webservicesynchronizer.py +++ b/openlp/plugins/remotesync/lib/backends/webservicesynchronizer.py @@ -26,6 +26,7 @@ from openlp.core.common import Settings, registry from openlp.core.lib.db import Manager from openlp.plugins.remotesync.lib.backends.synchronizer import Synchronizer from openlp.plugins.songs.lib.db import init_schema, Song +from openlp.plugins.remotesync.lib.backends.restclient.services import song_service, custom_service class WebServiceSynchronizer(Synchronizer): diff --git a/openlp/plugins/remotesync/remote-sync-api.yml b/openlp/plugins/remotesync/remote-sync-api.yml index e25d3bd8c..84603a705 100644 --- a/openlp/plugins/remotesync/remote-sync-api.yml +++ b/openlp/plugins/remotesync/remote-sync-api.yml @@ -6,13 +6,12 @@ info: Some useful links: - [The OpenLP remotesync MR](https://gitlab.com/openlp/openlp/-/merge_requests/9) - termsOfService: http://swagger.io/terms/ contact: email: dev@openlp.io license: name: Apache 2.0 url: http://www.apache.org/licenses/LICENSE-2.0.html - version: 1.0.11 + version: 0.9.1 externalDocs: description: Find out more about Swagger url: http://swagger.io @@ -21,6 +20,8 @@ servers: tags: - name: song description: Operations about songs + - name: custom + description: Operations about custom slides paths: /{churchId}/song-list: get: @@ -50,12 +51,55 @@ paths: description: Invalid uuid supplied '404': description: Song not found + security: + - openlp_auth: + - write:song + - read:song + /{churchId}/song-list/changes/{sinceDateTime}: + get: + tags: + - song + summary: Get list of songs changed since given timestamp + description: '' + operationId: getSongListChangesSince + parameters: + - name: churchId + in: path + description: The id of the church. + required: true + schema: + type: string + - name: sinceDateTime + in: path + description: The timestamp to look for changes after + required: true + schema: + type: string + format: date-time + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ItemList' + application/xml: + schema: + $ref: '#/components/schemas/ItemList' + '400': + description: Invalid uuid supplied + '404': + description: Song not found + security: + - openlp_auth: + - write:song + - read:song /{churchId}/song/{uuid}: get: tags: - song summary: Get song by uuid - description: fwef + description: Get song by uuid operationId: getSong parameters: - name: churchId @@ -84,6 +128,10 @@ paths: description: Invalid uuid supplied '404': description: Song not found + security: + - openlp_auth: + - write:song + - read:song put: tags: - song @@ -116,8 +164,14 @@ paths: schema: $ref: '#/components/schemas/TextItem' responses: - default: + '200': description: successful operation + '409': + description: A conflict occured. The version requested to be saved is not the newest. + security: + - openlp_auth: + - write:song + - read:song delete: tags: - song @@ -142,6 +196,10 @@ paths: description: Invalid uuid supplied '404': description: Song not found + security: + - openlp_auth: + - write:song + - read:song /{churchId}/song/{uuid}/{version}: get: tags: @@ -182,6 +240,10 @@ paths: description: Invalid uuid supplied '404': description: Song not found + security: + - openlp_auth: + - write:song + - read:song /{churchId}/song-history/{uuid}: get: tags: @@ -216,6 +278,10 @@ paths: description: Invalid uuid supplied '404': description: Song not found + security: + - openlp_auth: + - write:song + - read:song /{churchId}/custom-list: get: tags: @@ -244,6 +310,49 @@ paths: description: Invalid uuid supplied '404': description: Custom slide not found + security: + - openlp_auth: + - write:custom + - read:custom + /{churchId}/custom-list/changes/{sinceDateTime}: + get: + tags: + - song + summary: Get list of custom slides changed since given timestamp + description: '' + operationId: getCustomSlideListChangesSince + parameters: + - name: churchId + in: path + description: The id of the church. + required: true + schema: + type: string + - name: sinceDateTime + in: path + description: The timestamp to look for changes after + required: true + schema: + type: string + format: date-time + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ItemList' + application/xml: + schema: + $ref: '#/components/schemas/ItemList' + '400': + description: Invalid uuid supplied + '404': + description: Song not found + security: + - openlp_auth: + - write:song + - read:song /{churchId}/custom/{uuid}: get: tags: @@ -278,6 +387,10 @@ paths: description: Invalid uuid supplied '404': description: Custom slide not found + security: + - openlp_auth: + - write:custom + - read:custom put: tags: - custom @@ -310,8 +423,14 @@ paths: schema: $ref: '#/components/schemas/TextItem' responses: - default: + '200': description: successful operation + '409': + description: A conflict occured. The version requested to be saved is not the newest. + security: + - openlp_auth: + - write:custom + - read:custom delete: tags: - custom @@ -336,6 +455,10 @@ paths: description: Invalid uuid supplied '404': description: Custom slide not found + security: + - openlp_auth: + - write:custom + - read:custom /{churchId}/custom/{uuid}/{version}: get: tags: @@ -376,6 +499,10 @@ paths: description: Invalid uuid supplied '404': description: Custom slide not found + security: + - openlp_auth: + - write:custom + - read:custom /{churchId}/custom-history/{uuid}: get: tags: @@ -410,6 +537,10 @@ paths: description: Invalid uuid supplied '404': description: Custom slide not found + security: + - openlp_auth: + - write:custom + - read:custom components: schemas: TextItem: @@ -422,10 +553,10 @@ components: type: integer format: int32 examples: [2] - created: + user: type: string - format: date-time - updated: + examples: ['user1', '354364'] + timestamp: type: string format: date-time title: @@ -454,12 +585,15 @@ components: type: integer format: int32 examples: [2] - created: + user: + type: string + examples: ['user1', '354364'] + timestamp: type: string format: date-time - updated: + title: type: string - format: date-time + examples: ['Text item title'] ApiResponse: type: object properties: @@ -483,14 +617,16 @@ components: schema: $ref: '#/components/schemas/TextItem' securitySchemes: - petstore_auth: + openlp_auth: type: oauth2 flows: implicit: - authorizationUrl: https://petstore3.swagger.io/oauth/authorize + authorizationUrl: http://localhost:8888/oauth/authorize scopes: - write:pets: modify pets in your account - read:pets: read your pets + write:songs: modify songs in your account + read:songs: read your songs + write:customs: modify custom slides in your account + read:customs: read your custom slides api_key: type: apiKey name: api_key diff --git a/openlp/plugins/remotesync/remotesyncplugin.py b/openlp/plugins/remotesync/remotesyncplugin.py index 0222d890a..ebd84255e 100644 --- a/openlp/plugins/remotesync/remotesyncplugin.py +++ b/openlp/plugins/remotesync/remotesyncplugin.py @@ -117,6 +117,8 @@ class RemoteSyncPlugin(Plugin): Settings().value('remotesync/ftp server'), Settings().value('remotesync/ftp username'), Settings().value('remotesync/ftp password')) + elif sync_type == SyncType.WebService: + self.synchronizer = WebServiceSynchronizer() else: self.synchronizer = None if self.synchronizer and not self.synchronizer.check_connection():