From 9452627417098d60480554f5580efbd690f7f516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20St=C3=BCrmer?= Date: Thu, 21 Dec 2023 05:26:16 +0100 Subject: [PATCH] remove old project structure --- .devcontainer/Dockerfile.dev | 19 - .devcontainer/devcontainer.json | 43 - .gitignore | 73 +- .vscode/extensions.json | 7 - .vscode/launch.json | 12 - .vscode/settings.json | 46 - .vscode/tasks.json | 92 -- CHANGELOG.md | 13 + CONTRIBUTING.md | 84 ++ Dockerfile | 12 - LICENSE.md | 851 ++++++++++++++---- README.md | 110 ++- frontend/__init__.py | 1 - frontend/__main__.py | 87 -- frontend/application.py | 49 - frontend/base/__init__.py | 11 - frontend/base/handler.py | 15 - frontend/base/oauth.py | 182 ---- frontend/base/request.py | 151 ---- frontend/base/route.py | 10 - frontend/base/state.py | 16 - frontend/config.py | 67 -- frontend/const.py | 14 - frontend/frontend.py | 192 ---- frontend/i18n.py | 106 --- frontend/locales/base.pot | 24 - frontend/locales/de/LC_MESSAGES/messages.mo | Bin 435 -> 0 bytes frontend/locales/de/LC_MESSAGES/messages.po | 25 - .../locales/en_US/LC_MESSAGES/messages.mo | Bin 425 -> 0 bytes .../locales/en_US/LC_MESSAGES/messages.po | 25 - .../locales/vi_VN/LC_MESSAGES/messages.mo | Bin 410 -> 0 bytes .../locales/vi_VN/LC_MESSAGES/messages.po | 22 - .../locales/zh_TW/LC_MESSAGES/messages.mo | Bin 400 -> 0 bytes .../locales/zh_TW/LC_MESSAGES/messages.po | 22 - frontend/static/browserconfig.xml | 2 - frontend/static/custom-icon.png | Bin 15999 -> 0 bytes frontend/static/favicon.ico | Bin 1150 -> 0 bytes frontend/static/favicon.png | Bin 2427 -> 0 bytes frontend/templates/base.jinja2 | 72 -- frontend/templates/error.jinja2 | 28 - frontend/templates/index.jinja2 | 29 - frontend/templates/index/grid.jinja2 | 22 - frontend/templates/index/shared.jinja2 | 11 - frontend/templates/index/user.jinja2 | 171 ---- frontend/templates/login_required.jinja2 | 96 -- frontend/templates/partials/app.jinja2 | 29 - frontend/templates/partials/footer.jinja2 | 41 - frontend/templates/partials/header.jinja2 | 37 - frontend/views/callback.py | 57 -- frontend/views/index.py | 53 -- frontend/views/login.py | 36 - frontend/views/logout.py | 28 - frontend/views/photo.py | 42 - frontend/views/settings.py | 24 - frontend/views/shared.py | 46 - frontend/views/user.py | 85 -- frontend/webserver.py | 173 ---- requirements.txt | 6 - requirements_test.txt | 12 - server.py | 8 - setup.cfg | 42 - setup.py | 56 -- tests/__init__.py | 1 - tests/conftest.py | 13 - tests/login/test_routes_login.py | 48 - tests/test_internationalization.py | 26 - tests/test_oauth_flow.py | 4 - tests/test_routes_static.py | 12 - 68 files changed, 857 insertions(+), 2834 deletions(-) delete mode 100644 .devcontainer/Dockerfile.dev delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 .vscode/extensions.json delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/settings.json delete mode 100644 .vscode/tasks.json create mode 100644 CONTRIBUTING.md delete mode 100644 Dockerfile delete mode 100644 frontend/__init__.py delete mode 100644 frontend/__main__.py delete mode 100644 frontend/application.py delete mode 100644 frontend/base/__init__.py delete mode 100644 frontend/base/handler.py delete mode 100644 frontend/base/oauth.py delete mode 100644 frontend/base/request.py delete mode 100644 frontend/base/route.py delete mode 100644 frontend/base/state.py delete mode 100644 frontend/config.py delete mode 100644 frontend/const.py delete mode 100644 frontend/frontend.py delete mode 100644 frontend/i18n.py delete mode 100644 frontend/locales/base.pot delete mode 100644 frontend/locales/de/LC_MESSAGES/messages.mo delete mode 100644 frontend/locales/de/LC_MESSAGES/messages.po delete mode 100644 frontend/locales/en_US/LC_MESSAGES/messages.mo delete mode 100644 frontend/locales/en_US/LC_MESSAGES/messages.po delete mode 100644 frontend/locales/vi_VN/LC_MESSAGES/messages.mo delete mode 100644 frontend/locales/vi_VN/LC_MESSAGES/messages.po delete mode 100644 frontend/locales/zh_TW/LC_MESSAGES/messages.mo delete mode 100644 frontend/locales/zh_TW/LC_MESSAGES/messages.po delete mode 100644 frontend/static/browserconfig.xml delete mode 100644 frontend/static/custom-icon.png delete mode 100644 frontend/static/favicon.ico delete mode 100644 frontend/static/favicon.png delete mode 100644 frontend/templates/base.jinja2 delete mode 100644 frontend/templates/error.jinja2 delete mode 100644 frontend/templates/index.jinja2 delete mode 100644 frontend/templates/index/grid.jinja2 delete mode 100644 frontend/templates/index/shared.jinja2 delete mode 100644 frontend/templates/index/user.jinja2 delete mode 100644 frontend/templates/login_required.jinja2 delete mode 100644 frontend/templates/partials/app.jinja2 delete mode 100644 frontend/templates/partials/footer.jinja2 delete mode 100644 frontend/templates/partials/header.jinja2 delete mode 100644 frontend/views/callback.py delete mode 100644 frontend/views/index.py delete mode 100644 frontend/views/login.py delete mode 100644 frontend/views/logout.py delete mode 100644 frontend/views/photo.py delete mode 100644 frontend/views/settings.py delete mode 100644 frontend/views/shared.py delete mode 100644 frontend/views/user.py delete mode 100644 frontend/webserver.py delete mode 100644 requirements.txt delete mode 100644 requirements_test.txt delete mode 100644 server.py delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 tests/__init__.py delete mode 100644 tests/conftest.py delete mode 100644 tests/login/test_routes_login.py delete mode 100644 tests/test_internationalization.py delete mode 100644 tests/test_oauth_flow.py delete mode 100644 tests/test_routes_static.py diff --git a/.devcontainer/Dockerfile.dev b/.devcontainer/Dockerfile.dev deleted file mode 100644 index 2f7c844..0000000 --- a/.devcontainer/Dockerfile.dev +++ /dev/null @@ -1,19 +0,0 @@ -FROM mcr.microsoft.com/vscode/devcontainers/python:3.10 - -# Install zsh -ARG INSTALL_ZSH="true" - -# setup non-root user -ARG USERNAME=vscode -ARG USER_UID=1000 -ARG USER_GID=$USER_UID - -WORKDIR /workspaces - -COPY . . - -# Install Python dependencies -RUN pip3 --disable-pip-version-check --no-cache-dir install -r requirements_test.txt - -# Install frontend -RUN python3 setup.py install diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index e901607..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "Photos.network frontend", - "build": { - "dockerfile": "Dockerfile.dev", - "context": ".." - }, - "appPort": [ - "7778:7778" - ], - "postCreateCommand": "mkdir -p config && pip3 install -r requirements_test.txt", - "runArgs": ["-e", "GIT_EDITOR=code --wait"], - "extensions": [ - "ms-python.python", - "ms-python.vscode-pylance", - "esbenp.prettier-vscode" - ], - "remoteUser": "vscode", - "settings": { - "editor.formatOnPaste": true, - "editor.formatOnSave": true, - "editor.formatOnType": true, - "python.pythonPath": "/usr/local/bin/python", - "python.languageServer": "Pylance", - "python.linting.lintOnSave": true, - "python.linting.enabled": true, - "python.linting.pylintEnabled": true, - "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint", - "python.linting.flake8Enabled": true, - "python.linting.flake8Path": "/usr/local/bin/flake8", - "python.linting.flake8Args": [ - "--max-line-length=130" - ], - "python.testing.pytestEnabled": true, - "python.testing.pytestPath": "/usr/local/bin/pytest", - "python.formatting.provider": "black", - "python.formatting.blackArgs": [ - "--line-length", - "120" - ], - "python.formatting.blackPath": "/usr/local/py-utils/bin/black", - "files.trimTrailingWhitespace": true, - } -} diff --git a/.gitignore b/.gitignore index a012844..add8cdd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,67 +1,16 @@ -# pytest -.pytest_cache -.cache +# Generated by Cargo +# will have compiled files and executables +/target/ +pkg -# GITHUB Proposed Python stuff: -*.py[cod] +# These are backup files generated by rustfmt +**/*.rs.bk -# C extensions -*.so - -# Packages -*.egg -*.egg-info -dist -build -eggs -.eggs -parts -bin -var -sdist -develop-eggs -.installed.cfg -lib -lib64 -pip-wheel-metadata - -# Logs -*.log* -pip-log.txt - -# Unit test / coverage reports -.coverage -.tox -coverage.xml -nosetests.xml -htmlcov/ -test-reports/ -test-results.xml -test-output.xml - -# venv stuff -pyvenv.cfg -pip-selfcheck.json -venv -.venv -Pipfile* -share/* -/Scripts/ - -# Visual Studio Code -.vscode/* -!.vscode/cSpell.json -!.vscode/extensions.json -!.vscode/launch.json -!.vscode/settings.json -!.vscode/tasks.json -.env +# node e2e test tools and outputs +node_modules/ +test-results/ +end2end/playwright-report/ +playwright/.cache/ # Built docs docs/build - -# data directory -data/ - -# config directory -config/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 42f796f..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "recommendations": [ - "esbenp.prettier-vscode", - "ms-python.python", - "esbenp.prettier-vscode" - ] -} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 5288844..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Photos.network", - "type": "python", - "request": "launch", - "program": "server.py", - "console": "integratedTerminal" - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index af989ea..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "editor.formatOnSave": false, - "editor.formatOnPaste": true, - "editor.formatOnType": true, - "editor.fontFamily": "JetBrains Mono", - "editor.quickSuggestionsDelay": 1500, - "editor.detectIndentation": true, - "editor.insertSpaces": true, - "editor.tabSize": 4, - "editor.codeActionsOnSave": { - "source.organizeImports": true - }, - "files.exclude": { - ".gitattributes": true, - "**/__pycache__": true, - "**/.pytest_cache": true, - "**/.mypy_cache": true - }, - "breadcrumbs.enabled": true, - "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python3", - "python.linting.lintOnSave": true, - "python.linting.enabled": true, - "python.linting.pylintEnabled": false, - "python.linting.flake8Enabled": true, - "python.linting.flake8Path": "/usr/local/bin/flake8", - "python.linting.flake8Args": [ - "--max-line-length=130" - ], - "python.testing.pytestEnabled": true, - "python.testing.pytestPath": "/usr/local/bin/pytest", - "python.testing.pytestArgs": [ - "--no-cov" - ], - "python.formatting.provider": "black", - "python.formatting.blackArgs": [ - "--line-length", - "120" - ], - "[python]": { - "editor.tabSize": 4, - "editor.formatOnSave": true - }, - "python.terminal.activateEnvInCurrentTerminal": true, - "python.testing.unittestEnabled": false, - "python.testing.nosetestsEnabled": false -} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 20d5033..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "Photos.network Frontend", - "type": "shell", - "command": "python3 server.py", - "group": "build", - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Pytest", - "type": "shell", - "command": "pytest --timeout=10 tests", - "group": { - "kind": "test", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Flake8", - "type": "shell", - "command": "pre-commit run flake8 --all-files", - "dependsOn": [ - "Install all Test Requirements" - ], - "group": { - "kind": "test", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Pylint", - "type": "shell", - "command": "pylint core", - "dependsOn": [ - "Install all Requirements" - ], - "group": { - "kind": "test", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Install all Requirements", - "type": "shell", - "command": "pip3 install -r requirements.txt", - "group": { - "kind": "build", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Install all Test Requirements", - "type": "shell", - "command": "pip3 install -r requirements_test.txt", - "group": { - "kind": "build", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - } - ] -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8851dd7..30d15c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.3.0] - 2023-12-21 +### Changed +- Rewrite in Rust with Leptos Framework + + ## [0.2.2] - 2022-06-15 ### Fixed - persist refreshed token @@ -27,6 +39,7 @@ - draft of profile page +[0.3.0]: https://github.com/photos-network/frontend/compare/Release/v0.2.2...Release/v0.3.0 [0.2.2]: https://github.com/photos-network/frontend/compare/Release/v0.2.1...Release/v0.2.2 [0.2.1]: https://github.com/photos-network/frontend/compare/Release/v0.2.0...Release/v0.2.1 [0.2.0]: https://github.com/photos-network/frontend/compare/Release/v0.1.0...Release/v0.2.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b155351 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,84 @@ +# Welcome to Photos.network + +This is a **FOSS** (free and open-source software) and lives from contributions of the community. + +There are many ways to contribute: + + * ๐Ÿ“ฃ Spread the project or its apps to the world + * โœ๏ธ Writing tutorials and blog posts + * ๐Ÿ“ Create or update the documentation + * ๐Ÿ› Submit bug reports + * ๐Ÿ’ก Adding ideas and feature requests to Discussions + * ๐Ÿ‘ฉโ€๐ŸŽจ Create designs or UX flows + * ๐Ÿง‘โ€๐Ÿ’ป Contribute code or review PRs + + + +## ๐Ÿ“œ Ground Rules + +A community like this should be **open**, **considerate** and **respectful**. + +Behaviours that reinforce these values contribute to a positive environment, and include: + + * **Being open**. Members of the community are open to collaboration. + * **Focusing on what is best for the community**. We're respectful of the processes set forth in the community, and we work within them. + * **Acknowledging time and effort**. We're respectful and thoughtful when addressing the efforts of others, keeping in mind that often times the labor was completed simply for the good of the community. + * **Being respectful of differing viewpoints and experiences**. We're receptive to constructive comments and criticism, as the experiences and skill sets of other members contribute to the whole of our efforts. + * **Showing empathy towards other community members**. We're attentive in our communications, whether in person or online, and we're tactful when approaching differing views. + * **Being considerate**. Members of the community are considerate of their peers. + * **Being respectful**. We're respectful of others, their positions, their skills, their commitments, and their efforts. + * **Gracefully accepting constructive criticism**. When we disagree, we are courteous in raising our issues. + * **Using welcoming and inclusive language**. We're accepting of all who wish to take part in our activities, fostering an environment where anyone can participate and everyone can make a difference. + + + +## ๐Ÿง‘โ€๐Ÿ’ป Code Contribution + +To contribute code to the repository, you don't need any permissions. +First start by forking the repository, clone and checkout your clone and start coding. +When you're happy with your changes, create Atomic commits on a **new feature branch** and push it to ***your*** fork. + +Atomic commits will make it easier to track down regressions. Also, it enables the ability to cherry-pick or revert a change if needed. + +1. Fork it (https://github.com/photos-network/core/fork) +2. Create a new feature branch (`git checkout -b feature/fooBar`) +3. Commit your changes (`git commit -am 'Add some fooBar'`) +4. Push to the branch (`git push origin feature/fooBar`) +5. Create a new Pull Request + + + +## ๐Ÿ› How to report a bug + +> If you find a security vulnerability, do NOT open an issue. Email [security@photos.network](mailto:security@photos.network) instead. See [SECURITY.md](./SECURITY.md) for details. + +1. Open the [issues tab](https://github.com/photos-network/core/issues) on github +2. Click on [New issue](https://github.com/photos-network/core/issues/new/choose) +3. Choose the bug report ๐Ÿ› template and fill out all required fields + + + +## ๐Ÿ’ก How to suggest a feature or enhancement + +Check [open issues](https://github.com/photos-network/core/issues) for a list of proposed features. + +If your suggestion can not be found already, see if it is already covered by our [Roadmap](https://github.com/photos-network/core/#roadmap). + + + +## ๐Ÿ“Ÿ Communication + +To get in touch with the community or write use on Mastodon: [@photos@mastodon.cloud](https://mastodon.cloud/@photos). + + + +## ๐Ÿ’พ Technology + +The project is written in [Rust](https://rust-lang.org/) + +Underneath it is using these frameworks: + +* [tokio](https://github.com/tokio-rs/tokio) - an asynchronous runtime +* [tower](https://github.com/tower-rs/tower) - for networking +* [axum](https://github.com/tokio-rs/axum) - as web framework +* [abi_stable](https://github.com/rodrimati1992/abi_stable_crates) - FFI for dynamic library loading diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 3b4b78f..0000000 --- a/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM python:3.10 -LABEL "description"="Photos.network web frontend" -LABEL "version"="0.2.2" -LABEL "maintainer"="github.com/photos-network" - -WORKDIR /app - -ADD . . - -RUN python3 setup.py install - -CMD [ "python3", "/usr/local/bin/frontend" ] diff --git a/LICENSE.md b/LICENSE.md index 901233a..c796e17 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,190 +1,661 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -Copyright 2020 Photos network developers - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + Photos.network ยท A privacy first, self-hosted photo storage and sharing service for fediverse. + Copyright 2020 Photos network developers + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md index 6b34cd2..5512004 100644 --- a/README.md +++ b/README.md @@ -2,46 +2,112 @@ [![License](https://img.shields.io/github/license/photos-network/frontend)](./LICENSE.md) [![GitHub contributors](https://img.shields.io/github/contributors/photos-network/frontend?color=success)](https://github.com/photos.network/core/graphs/contributors) -[![Discord](https://img.shields.io/discord/793235453871390720)](https://discord.gg/dGFDpmWp46) -[Photos.network](https://photos.network) is an open source project for self hosted photo management. +Photos.network](https://photos.network) is a free and open source, privacy first, self-hosted photo storage and sharing service for fediverse. + Its core features are: - - Share photos with friends, family or public - - Filter / Search photos by attributes like location or date - - Group photos by objects like people of objects +- Share photos with friends, family or public +- Filter / Search photos by attributes like location or date +- Group photos by their content like people or objects +- Upload photos and videos without resolution or quality constraints + ## Frontend -This repository contains the official App-like web frontend. +This repository contains the official App-like web frontend. of the project. + +It is responsible for interacting with the core system via REST calls. +- **Overview** of the users media items in a grid +- **Albums** the users has access to +- **Upload** new media items +- **Details** of items like location, date and time taken. -Its containing -- a grid of the users photos -- the users profile -## Development +## ๐Ÿงฉ Contribution -### Visual Studio Code -The fastest start into development can be archived by using [Visual Studio Code](https://code.visualstudio.com/) and [Docker](https://www.docker.com/get-started). +This is a free and open project and lives from contributions of the community. -1. Install [Docker](https://www.docker.com/get-started) -2. Install [Visual Studio Code](https://code.visualstudio.com/) -3. Install [Visual Studio Code Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) -4. Clone and Open this repository in Visual Studio Code -5. Click the "Reopen in Container" Dialog +See our [Contribution Guide](CONTRIBUTING.md) +## ๐Ÿงช Development +The frontend is written in ๐Ÿฆ€ [Rust](https://rust-lang.org/) using the [Leptos](https://leptos.dev/) framework. ---- -## Release -Update the version in `frontend/const.py` and `Dockerfile` before creating a new image. +#### ๐Ÿƒ Running -To support multiple architectures, we need to create and use or own builder. +```shell +cargo leptos watch +``` + + +#### ๐Ÿ”ฌ Testing + +```shell +cargo leptos end-to-end +``` + +```shell +cargo leptos end-to-end --release +``` + +Cargo-leptos uses Playwright as the end-to-end test tool. +Tests are located in end2end/tests directory. + + +#### ๐Ÿ“ฆ Release + +```shell +cargo leptos build --release +``` + +1. The server binary located in `target/server/release` +2. The `site` directory and all files within located in `target/site` + +Copy these files to your remote server. The directory structure should be: +```text +frontend +site/ +``` +Set the following environment variables (updating for your project as needed): +```text +LEPTOS_OUTPUT_NAME="frontend" +LEPTOS_SITE_ROOT="site" +LEPTOS_SITE_PKG_DIR="pkg" +LEPTOS_SITE_ADDR="127.0.0.1:3000" +LEPTOS_RELOAD_PORT="3001" +``` + + +## ๐Ÿš€ Release + +To support multiple architectures, an own builder needs to be created. ```shell docker buildx create --name multiarchitecturebuilder docker buildx use multiarchitecturebuilder docker buildx build --platform linux/arm64,linux/amd64 --tag photosnetwork/frontend:latest --push . -``` +``` + + + +## ๐Ÿ›๏ธ License + +``` +Photos.network ยท A privacy first photo storage and sharing service for fediverse +Copyright (C) 2020 Photos network developers + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +``` diff --git a/frontend/__init__.py b/frontend/__init__.py deleted file mode 100644 index 543a16c..0000000 --- a/frontend/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""init of Photos.network frontend.""" diff --git a/frontend/__main__.py b/frontend/__main__.py deleted file mode 100644 index f1e1771..0000000 --- a/frontend/__main__.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Entry point of Photos.network frontend.""" -import asyncio -import json -import logging -import os -import socket -import sys -from typing import Any, Dict -from xml.dom.expatbuilder import parseString - -from aiohttp import web - -from frontend.application import create_application -from frontend.config import Config -from frontend.const import REQUIRED_PYTHON_VER -from frontend.frontend import Frontend - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -def validate_python() -> None: - """Validate that the right Python version is running.""" - if sys.version_info[:3] < REQUIRED_PYTHON_VER: - print( - "Photos.network requires at least Python " - f"{REQUIRED_PYTHON_VER[0]}.{REQUIRED_PYTHON_VER[1]}.{REQUIRED_PYTHON_VER[2]}" - ) - sys.exit(1) - - -def main() -> int: - """Start Photos.network application.""" - validate_python() - - config_dir = os.path.abspath(os.path.join(os.getcwd(), "config")) - config_file = os.path.join(config_dir, "frontend_configuration.json") - - if not os.path.exists(config_dir): - _LOGGER.info(f"create directory {config_dir}") - os.mkdir(config_dir) - - if not os.path.exists(config_file): - # create default config file - with open(file=config_file, mode="w+", encoding="utf-8") as file: - hostname = socket.gethostname() - - output = { - "frontend_url": "http://" + str(socket.gethostbyname(hostname)), - "frontend_port": 7778, - "core_url": "http://127.0.0.1", - "core_port": 7777, - "client_id": "", - "client_secret": "", - "redirect_uri": "http://127.0.0.1:7778/callback", - } - json.dump(output, file, indent=2) - file.close() - _LOGGER.info(f"default config_file {config_file} created.") - else: - _LOGGER.info(f"config_file found at {config_file}") - - with open(file=config_file, mode="r", encoding="utf-8") as file: - conf_dict = json.load(file) - file.close() - - if not isinstance(conf_dict, dict): - msg = f"The configuration file {os.path.basename(config_dir)} does not contain a dictionary" - _LOGGER.error(msg) - raise RuntimeError(msg) - - config = Config.fromConfigFile(conf_dict) - - frontend = Frontend(config) - frontend.async_enable_logging(verbose=True) - - try: - exit_code = asyncio.run(frontend.async_run()) - except KeyboardInterrupt: - # TODO: handle running threads - print("### Interrupt application without taking care of running threads ###") - exit_code = 0 - - return exit_code - -if __name__ == "__main__": - sys.exit(main()) diff --git a/frontend/application.py b/frontend/application.py deleted file mode 100644 index 5b2a562..0000000 --- a/frontend/application.py +++ /dev/null @@ -1,49 +0,0 @@ -import asyncio -import os -from typing import Any, Awaitable, Callable, Dict - -import aiohttp_jinja2 -import aiohttp_session -import jinja2 -from aiohttp import web -from aiohttp_session import session_middleware -from aiohttp_session.cookie_storage import EncryptedCookieStorage - -from frontend.config import Config -from frontend.i18n import i18n - - -async def username_ctx_processor(request: web.Request) -> Dict[str, Any]: - """Jinja2 context processor to extract the username from an active session.""" - session = await aiohttp_session.get_session(request) - if "first_name" in session: - username = session.get("first_name") - elif "last_name" in session: - username = session.get("last_name") - else: - username = session.get("username") - - return {"username": username} - - -def create_application( - loop=asyncio.new_event_loop(), - config: Config = None, -) -> web.Application: - app = web.Application(client_max_size=64 * 1024**2) - - # check if secret length is 32 bytes - if len(config.cookie_secret) == 32: - secret_key = bytes(config.cookie_secret, "utf-8") - else: - secret_key = bytes("Thirty two length bytes key!", "utf-8") - - aiohttp_session.setup(app, EncryptedCookieStorage(secret_key)) - - for route in app.routes.routes: - app.router.add_route(*route[0], **route[1]) - - for middleware in app.middlewares.middlewares: - app.middlewares.append(middleware) - - return app diff --git a/frontend/base/__init__.py b/frontend/base/__init__.py deleted file mode 100644 index 9b9e4ef..0000000 --- a/frontend/base/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Any, Awaitable, Callable, Dict - -from aiohttp import web - - -def require_login( - func: Callable[[web.Request], Awaitable[web.StreamResponse]], -) -> Callable[[web.Request], Awaitable[web.StreamResponse]]: - """Decorator to indicate internal calls checked by the `login_required_middleware`.""" - func.__require_login__ = True # type: ignore - return func diff --git a/frontend/base/handler.py b/frontend/base/handler.py deleted file mode 100644 index 7477c91..0000000 --- a/frontend/base/handler.py +++ /dev/null @@ -1,15 +0,0 @@ -from aiohttp import web -from aiohttp_session import get_session - - -class Handler(web.View): - async def get_current_user(self) -> str | None: - """Current user""" - - session = await get_session(self.request) - email = session.get("email", None) - - if email is None: - return None - - return email diff --git a/frontend/base/oauth.py b/frontend/base/oauth.py deleted file mode 100644 index 29a1bad..0000000 --- a/frontend/base/oauth.py +++ /dev/null @@ -1,182 +0,0 @@ -import asyncio -import logging -import time -from dataclasses import dataclass -from typing import Any, Dict - -import aiohttp -import aiohttp_session -from aiohttp import request, web -from frontend.config import Config - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class CoreClient: - """Http client for Photos.network core communication. - - * Handles oauth flow https://developers.photos.network/core/authentication_flow/ - """ - - state: str = None - accessToken: str = None - refreshToken: str = None - expiresIn: int = None - scope: str = None - - def __init__(self, config: Config) -> None: - self.config: Config = config - - def get_authorize_url(self, scope: str, state: str = None) -> str: - """generate authorize url based on inputs.""" - self.scope = scope - - return ( - str(self.config.core_url) - + ":" - + str(self.config.core_port) - + "/api/oauth/authorize?client_id=" - + str(self.config.client_id) - + "&response_type=code&redirect_uri=" - + str(self.config.redirect_uri) - + "&response_mode=query&scope=" - + scope - + "&state=" - + state - ) - - async def get_access_token(self, code: str): - """request access token.""" - url = str(self.config.core_url) + ":" + str(self.config.core_port) + "/api/oauth/token" - - raw_data = ( - "grant_type=authorization_code&code=" - + code - + "&client_id=" - + self.config.client_id - + "&redirect_uri=" - + self.config.redirect_uri - ) - - async with aiohttp.ClientSession() as session: - async with session.request( - method="POST", - url=url, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - data=raw_data, - ssl=None, - verify_ssl=False, - ) as resp: - response = await resp.json() - - self.accessToken = response["access_token"] - self.refreshToken = response["refresh_token"] - self.expiresIn = response["expires_in"] - - await session.close() - - return { - "expires_in": response["expires_in"], - "access_token": response["access_token"], - "refresh_token": response["refresh_token"], - } - - async def check_access_token(self) -> int: - """check if current access token is still valid.""" - url = str(self.config.core_url) + ":" + str(self.config.core_port) + "/api/protected" - - async with aiohttp.ClientSession() as session: - async with session.get(url=url, headers={"Authorization": "Bearer " + str(self.accessToken)}) as resp: - return resp.status - - async def user_info(self): - """user info.""" - - url = str(self.config.core_url) + ":" + str(self.config.core_port) + "/api/user/" - - async with aiohttp.ClientSession() as session: - async with session.get(url=url, headers={"Authorization": "Bearer " + str(self.accessToken)}) as resp: - response = await resp.json() - - return UserInfo( - response["id"], - response["email"], - response["firstname"], - response["lastname"], - ) - - async def refresh_access_token_call(self): - """refresh access token""" - - url = str(self.config.core_url) + ":" + str(self.config.core_port) + "/api/oauth/token" - - if self.refreshToken is not None and self.scope is not None: - - raw_data = ( - "grant_type=refresh_token" - + "&refresh_token=" - + str(self.refreshToken) - + "&client_id=" - + self.config.client_id - + "&scope=" - + self.scope - ) - - async with aiohttp.ClientSession() as clientSession: - async with clientSession.request( - method="POST", - url=url, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - data=raw_data, - ssl=None, - verify_ssl=False, - ) as resp: - response = await resp.json() - - timestamp = time.time() - expires_in = int(timestamp) + response["expires_in"] - - self.accessToken = response["access_token"] - self.refreshToken = response["refresh_token"] - self.expiresIn = expires_in - - status = resp.status - await clientSession.close() - - return (status, self.accessToken, self.refreshToken, self.expiresIn) - else: - return (None, None, None, None) - - async def get_photos(self): - async with aiohttp.ClientSession() as session: - async with session.get( - url=str(self.config.core_url) + ":" + str(self.config.core_port) + "/api/photos", - headers={"Authorization": "Bearer " + str(self.accessToken)}, - ) as resp: - return await resp.json() - - async def request(self, method: str, url: str): - """Check if user is authenticated and communicate with core instance.""" - - async with aiohttp.ClientSession() as session: - async with session.request( - method=method, - url=str(self.config.core_url) + ":" + str(self.config.core_port) + url, - headers={"Authorization": "Bearer " + str(self.accessToken)}, - ) as resp: - return await resp.read() - - -@dataclass -class UserInfo: - id: str - username: str - first_name: str - last_name: str - - def __init__(self, id: str, username: str, first_name: str, last_name: str): - self.id = id - self.username = username - self.first_name = first_name - self.last_name = last_name diff --git a/frontend/base/request.py b/frontend/base/request.py deleted file mode 100644 index 9a301a5..0000000 --- a/frontend/base/request.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Base class for webserver requests.""" -import asyncio -import json -import logging -from typing import TYPE_CHECKING, Any, Callable, List, Optional - -from aiohttp import web -from aiohttp.typedefs import LooseHeaders -from aiohttp.web_exceptions import HTTPInternalServerError, HTTPUnauthorized -from frontend.const import KEY_AUTHENTICATED, KEY_USER_ID - -if TYPE_CHECKING: - from frontend.frontend import Frontend - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -def is_callback(func: Callable[..., Any]) -> bool: - """Check if function is safe to be called in the event loop.""" - return getattr(func, "_callback", False) is True - - -class RequestView: - """Base request.""" - - requires_auth = True - url: Optional[str] = None - - extra_urls: List[str] = [] - - @staticmethod - def json( - result: Any, - status_code: int = 200, - headers: Optional[LooseHeaders] = None, - ) -> web.Response: - """Return a JSON response.""" - try: - msg = json.dumps(result, cls=ComplexEncoder, allow_nan=False).encode("UTF-8") - except (ValueError, TypeError) as err: - _LOGGER.error(f"Unable to serialize to JSON: {err}\n{result}") - raise HTTPInternalServerError from err - response = web.Response( - body=msg, - content_type="application/json", - status=status_code, - headers=headers, - ) - response.enable_compression() - return response - - def json_message( - self, - message: str, - status_code: int = 200, - message_code: Optional[str] = None, - headers: Optional[LooseHeaders] = None, - ) -> web.Response: - """Return a JSON message response.""" - data = {"message": message} - if message_code is not None: - data["code"] = message_code - return self.json(data, status_code, headers=headers) - - def register(self, frontend: "Frontend", router: web.UrlDispatcher) -> None: - """Register the view with a router.""" - assert self.url is not None, "No url set for view" - urls = [self.url] + self.extra_urls - routes = [] - - for method in ("get", "post", "delete", "put", "patch", "head", "options"): - handler = getattr(self, method, None) - - if not handler: - continue - - handler = request_handler_factory(self, frontend, handler) - - for url in urls: - routes.append(router.add_route(method, url, handler)) - - -class ComplexEncoder(json.JSONEncoder): - """Encoder for complex classes.""" - - def default(self, o): - """Encode all properties.""" - if isinstance(o, complex): - return [o.real, o.imag] - # Let the base class default method raise the TypeError. - return json.JSONEncoder.default(self, o) - - -def request_handler_factory(view: RequestView, frontend: "Frontend", handler: Callable) -> Callable: - """Wrap the handler classes.""" - assert asyncio.iscoroutinefunction(handler) or is_callback(handler), "Handler should be a coroutine or a callback." - - async def handle(request: web.Request) -> web.StreamResponse: - """Handle incoming request.""" - if frontend.is_stopping: - return web.Response(status=503) - - authenticated = request.get(KEY_AUTHENTICATED, False) - - if view.requires_auth and not authenticated: - raise HTTPUnauthorized() - - _LOGGER.info(f"Serving {request.path} to {request.remote} (auth: {authenticated})") - _LOGGER.info(f"match_info {request.match_info}") - - # try: - result = await handler(frontend, request, **request.match_info) - - if asyncio.iscoroutine(result): - result = await result - # except voluptuous.Invalid as err: - # raise HTTPBadRequest() from err - # except exceptions.ServiceNotFound as err: - # raise HTTPInternalServerError() from err - # except exceptions.Unauthorized as err: - # raise HTTPUnauthorized() from err - - if isinstance(result, web.StreamResponse): - # The method handler returned a ready-made Response, how nice of it - return result - - status_code = 200 - - if isinstance(result, tuple): - result, status_code = result - - bresult = convert_to_bytes(result) - - return web.Response(body=bresult, status=status_code) - - return handle - - -def convert_to_bytes(input: Any) -> bytes: - """Convert given input into bytes.""" - if isinstance(input, bytes): - bresult = input - elif isinstance(input, str): - bresult = input.encode("utf-8") - elif input is None: - bresult = b"" - else: - assert False, f"Result should be None, string, bytes or Response. Got: {input}" - - return bresult diff --git a/frontend/base/route.py b/frontend/base/route.py deleted file mode 100644 index 294fcb0..0000000 --- a/frontend/base/route.py +++ /dev/null @@ -1,10 +0,0 @@ -def __namedict(name): - return dict(name=name) if name is not None else dict() - - -def get(url, handler, name=None): - return (("GET", url, handler), __namedict(name)) - - -def post(url, handler, name=None): - return (("POST", url, handler), __namedict(name)) diff --git a/frontend/base/state.py b/frontend/base/state.py deleted file mode 100644 index ade3adc..0000000 --- a/frontend/base/state.py +++ /dev/null @@ -1,16 +0,0 @@ -import enum - - -class FrontendState(enum.Enum): - """Represent the current state of Photos.network frontend.""" - - not_running = "NOT_RUNNING" - starting = "STARTING" - running = "RUNNING" - stopping = "STOPPING" - final_write = "FINAL_WRITE" - stopped = "STOPPED" - - def __str__(self) -> str: # pylint: disable=invalid-str-returned - """Return the event.""" - return self.value # type: ignore diff --git a/frontend/config.py b/frontend/config.py deleted file mode 100644 index 70a3813..0000000 --- a/frontend/config.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Configurations for frontend instance.""" -import logging -from typing import Optional - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class Config: - """Representation class of configurations.""" - - def __init__( - self, - frontend_url: str = "http://127.0.0.1", - frontend_port: int = 7778, - core_url: str = "http://127.0.0.1", - core_port: int = 7777, - client_id: str | None = None, - client_secret: str | None = None, - redirect_uri: str | None = None, - cookie_name: str = "Photos.network", - cookie_secret: str = "", - ) -> None: - self.frontend_url: Optional[str] = frontend_url - self.frontend_port: int = frontend_port - self.core_url: Optional[str] = core_url - self.core_port: int = core_port - self.client_id: str = client_id - self.client_secret: str = client_secret - self.redirect_uri: str = redirect_uri - self.cookie_name: str = cookie_name - self.cookie_secret: str = cookie_secret - - @classmethod - def fromConfigFile(cls, conf_dict: dict): - """create config instance from configuration file.""" - - config = cls() - - if "frontend_url" in conf_dict: - config.frontend_url = conf_dict["frontend_url"] - - if "frontend_port" in conf_dict: - config.frontend_port = conf_dict["frontend_port"] - - if "core_url" in conf_dict: - config.core_url = conf_dict["core_url"] - - if "core_port" in conf_dict: - config.core_port = conf_dict["core_port"] - - if "client_id" in conf_dict: - config.client_id = conf_dict["client_id"] - - if "client_secret" in conf_dict: - config.client_secret = conf_dict["client_secret"] - - if "redirect_uri" in conf_dict: - config.redirect_uri = conf_dict["redirect_uri"] - - if "cookie_name" in conf_dict: - config.cookie_name = conf_dict["cookie_name"] - - if "cookie_secret" in conf_dict: - config.cookie_secret = conf_dict["cookie_secret"] - - return config diff --git a/frontend/const.py b/frontend/const.py deleted file mode 100644 index ad06558..0000000 --- a/frontend/const.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Constants used by Photos.network frontend.""" -MAJOR_VERSION = 0 -MINOR_VERSION = 2 -PATCH_VERSION = 2 -__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" -__version__ = f"{__short_version__}.{PATCH_VERSION}" -REQUIRED_PYTHON_VER = (3, 7, 1) - -FRONTEND_VERSION = __version__ - -KEY_AUTHENTICATED = "authenticated" -KEY_USER_ID = "user_id" - -SCOPES = "openid profile email phone library:read library:write" diff --git a/frontend/frontend.py b/frontend/frontend.py deleted file mode 100644 index 3d5bdfe..0000000 --- a/frontend/frontend.py +++ /dev/null @@ -1,192 +0,0 @@ -"""Frontend application""" -import asyncio -import logging -import os -import sys -from logging.handlers import TimedRotatingFileHandler -from time import monotonic -from typing import Any, Dict, List, Optional, Set - -from colorlog import ColoredFormatter - -from frontend.base.oauth import CoreClient -from frontend.base.state import FrontendState -from frontend.config import Config -from frontend.webserver import Webserver - -ERROR_LOG_FILENAME = "frontend.log" - -# How long to wait to log tasks that are blocking -BLOCK_LOG_TIMEOUT = 60 # seconds - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class Frontend: - """Frontend application class.""" - - http: Webserver = None - core_client: CoreClient = None - - def __init__(self, config: Config) -> None: - self.loop = asyncio.new_event_loop() - self.config: Config = config - self.state: FrontendState = FrontendState.not_running - self.exit_code: int = 0 - - # If not None, use to signal end-of-loop - self._stopped: Optional[asyncio.Event] = None - - self._pending_tasks: List = [] - - @property - def is_running(self) -> bool: - """Return if frontend is running.""" - return self.state in (FrontendState.starting, FrontendState.running) - - @property - def is_stopping(self) -> bool: - """Return if frontend is stopping.""" - return self.state in (FrontendState.stopping, FrontendState.final_write) - - def async_enable_logging(self, verbose: bool = False) -> None: - """Set up logging for frontend.""" - fmt = "%(asctime)s %(levelname)s (%(threadName)s) [%(name)s] %(message)s" - datefmt = "%Y-%m-%d %H:%M:%S" - logging.basicConfig(level=logging.INFO) - - colorfmt = f"%(log_color)s{fmt}%(reset)s" - logging.getLogger().handlers[0].setFormatter( - ColoredFormatter( - colorfmt, - datefmt=datefmt, - reset=True, - log_colors={ - "DEBUG": "cyan", - "INFO": "green", - "WARNING": "yellow", - "ERROR": "red", - "CRITICAL": "red", - }, - ) - ) - - logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO) - - logging.getLogger("requests").setLevel(logging.WARNING) - logging.getLogger("urllib3").setLevel(logging.WARNING) - logging.getLogger("aiohttp.access").setLevel(logging.WARNING) - - sys.excepthook = lambda *args: logging.getLogger("").exception( - "Uncaught exception", exc_info=args # type: ignore - ) - log_rotate_days = 14 - - err_log_path = os.path.join(os.getcwd(), ERROR_LOG_FILENAME) - err_dir = os.path.dirname(err_log_path) - if not err_dir: - os.mkdir(err_dir) - err_handler: logging.FileHandler = TimedRotatingFileHandler( - err_log_path, when="midnight", backupCount=log_rotate_days - ) - err_handler.setLevel(logging.DEBUG if verbose else logging.WARNING) - err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt)) - - logger = logging.getLogger("") - logger.addHandler(err_handler) - logger.setLevel(logging.DEBUG if verbose else logging.WARNING) - - async def start(self) -> int: - """Start frontend application. - Note: This function is only used for testing. - For regular use, use "await photos.run()". - """ - _LOGGER.debug("start core loop") - await self.async_block_till_done() - - return self.exit_code - - async def async_run(self, *, attach_signals: bool = True) -> int: - """Run the application. - Main entry point of the frontend application. - """ - if self.state != FrontendState.not_running: - raise RuntimeError("Frontend is already running") - - # _async_stop will set this instead of stopping the loop - self._stopped = asyncio.Event() - - await self.async_start() - - await self._stopped.wait() - return self.exit_code - - async def async_start(self) -> None: - """Finalize startup from inside the event loop.""" - self.state = FrontendState.starting - - try: - await self.async_block_till_done() - except asyncio.TimeoutError: - _LOGGER.warning( - "Something is blocking frontend from wrapping up the start up phase. We're going to continue anyway." - ) - - # Wait for all startup triggers before changing state - await asyncio.sleep(0) - - if self.state != FrontendState.starting: - _LOGGER.warning("Frontend startup has been interrupted. Its state may be inconsistent") - return - - self.state = FrontendState.running - - async def async_block_till_done(self) -> None: - """Block until all pending work is done.""" - # To flush out any call_soon_threadsafe - await asyncio.sleep(0) - - # setup core client - self.core_client = CoreClient(self.config) - _LOGGER.info("Core client setup done.") - - # setup webserver - self.http = Webserver(self) - await self.http.start() - _LOGGER.info("Webserver should be up and running...") - - start_time = None - - # iterate through pending tasks - while self._pending_tasks: - pending = [task for task in self._pending_tasks if not task.done()] - self._pending_tasks.clear() - if pending: - await self._await_and_log_pending(pending) - - if start_time is None: - # Avoid calling monotonic() until we know - # we may need to start logging blocked tasks. - start_time = 0 - elif start_time == 0: - # If we have waited twice then we set the start time - start_time = monotonic() - elif monotonic() - start_time > BLOCK_LOG_TIMEOUT: - # We have waited at least three loops and new tasks - # continue to block. At this point we start - # logging all waiting tasks. - for task in pending: - _LOGGER.debug("Waiting for task: %s", task) - else: - await asyncio.sleep(0) - - async def async_stop(self) -> None: - """Stop Photos.network core application.""" - self.state = FrontendState.stopping - self.state = FrontendState.final_write - self.state = FrontendState.not_running - self.state = FrontendState.stopped - - if self._stopped is not None: - self._stopped.set() diff --git a/frontend/i18n.py b/frontend/i18n.py deleted file mode 100644 index 8ec6962..0000000 --- a/frontend/i18n.py +++ /dev/null @@ -1,106 +0,0 @@ -import gettext -import importlib.resources as importlib_resources -import os -import sys -import threading - -from jinja2 import pass_context -from jinja2.ext import InternationalizationExtension -from markupsafe import Markup - -pkg = importlib_resources.files("frontend") -localedir = os.path.join(pkg / "locales") -domain = "messages" -threadLocalData = threading.local() -threadLocalData.locale = "en_US" - -locales = [] -for dirpath, dirnames, filenames in os.walk(localedir): - for dirname in dirnames: - locales.append(dirname) - break - -all_translations = {} -for locale in locales: - all_translations[locale] = gettext.translation(domain, localedir, [locale]) - - -def context_locale(context): - lang = "en_US" - if "locale" in context and context["locale"] in all_translations: - lang = context["locale"] - return lang - - -def parseAcceptLanguage(acceptLanguage): - try: - languages = acceptLanguage.split(",") - locale_q_pairs = [] - - for language in languages: - if language.split(";")[0] == language: - # no q => q = 1 - locale_q_pairs.append((language.strip().replace("-", "_"), "1")) - else: - locale = language.split(";")[0].strip() - q = language.split(";")[1].split("=")[1] - locale_q_pairs.append((locale.replace("-", "_"), q)) - - return locale_q_pairs - except AttributeError: - return "en_US" - - -def setLocale(locale): - languagePairs = parseAcceptLanguage(locale) - for pair in languagePairs: - if pair[0] in locales: - threadLocalData.locale = pair[0] - break - - -class InternationalizationWithContextExtension(InternationalizationExtension): - def _install_callables(self, gettext, ngettext, newstyle=None): - if newstyle is not None: - self.environment.newstyle_gettext = newstyle - if self.environment.newstyle_gettext: - gettext = _make_new_gettext(gettext) - ngettext = _make_new_ngettext(ngettext) - self.environment.globals.update(gettext=gettext, ngettext=ngettext) - - def gettext(msg): - return all_translations[threadLocalData.locale].gettext(msg) - - def ngettext(singular, plural, n): - return all_translations[threadLocalData.locale].ngettext(singular, plural, n) - - -@pass_context -def _gettext_alias(__context, *args, **kwargs): - return __context.call(__context.resolve("gettext"), *args, **kwargs) - - -def _make_new_gettext(func): - @pass_context - def gettext(__context, __string, **variables): - rv = __context.call(func, __context, __string) - if __context.eval_ctx.autoescape: - rv = Markup(rv) - return rv % variables - - return gettext - - -def _make_new_ngettext(func): - @pass_context - def ngettext(__context, __singular, __plural, __num, **variables): - variables.setdefault("num", __num) - rv = __context.call(func, __context, __singular, __plural, __num) - if __context.eval_ctx.autoescape: - rv = Markup(rv) - return rv % variables - - return ngettext - - -i18n = InternationalizationWithContextExtension diff --git a/frontend/locales/base.pot b/frontend/locales/base.pot deleted file mode 100644 index c283042..0000000 --- a/frontend/locales/base.pot +++ /dev/null @@ -1,24 +0,0 @@ -# English translations for Photos.network frontend. -# Copyright (C) 2020 Photos.network -# This file is distributed under the Apache License, Version 2.0 by Photos.network. -# -msgid "" -msgstr "" -"Project-Id-Version: 1.0.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-04-19 20:12+0800\n" -"PO-Revision-Date: 2022-04-19 20:12+0800\n" -"Last-Translator: Benjamin Stรผrmer \n" -"Language-Team: none\n" -"Language: en\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: frontend/views/partials/header.jinja2:21 -msgid "Home" -msgstr "Home" - -#: frontend/views/partials/header.jinja2:21 -msgid "Settings" -msgstr "Settings" diff --git a/frontend/locales/de/LC_MESSAGES/messages.mo b/frontend/locales/de/LC_MESSAGES/messages.mo deleted file mode 100644 index db80067e7304c09c0fe6eb01865438ae8f2fe657..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 435 zcmY+A!Ab)$5Qd{x&;=1Zd&pfpaZTBstjTOq@aiM<l^D9>pLr-Ad=0n*;L_yY8-MKHJW>2D-46a_M~E`(Px^hi)06I zrgMXGOrIYv2PfN;AM4ap36)hZoIT-HOiP`Dib3a*M2qn2MKXb4&>M9B#SP9a5+FbL zTyP?SC)zJEZSY7=AGGs8$2ak%YW>Ys!S2DSE>3O2KM~2+#84o{q#cPf%pprkOhwnA KC&)7H0mU~~ZF2em diff --git a/frontend/locales/de/LC_MESSAGES/messages.po b/frontend/locales/de/LC_MESSAGES/messages.po deleted file mode 100644 index 972a0ec..0000000 --- a/frontend/locales/de/LC_MESSAGES/messages.po +++ /dev/null @@ -1,25 +0,0 @@ -# German translations for Photos.network frontend. -# Copyright (C) 2020 Photos.network -# This file is distributed under the Apache License, Version 2.0 by Photos.network. -# -msgid "" -msgstr "" -"Project-Id-Version: 1.0.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-04-19 20:12+0800\n" -"PO-Revision-Date: 2022-04-19 20:12+0800\n" -"Last-Translator: Benjamin Stรผrmer \n" -"Language-Team: none\n" -"Language: de\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -msgid "Home" -msgstr "Start" - -msgid "Settings" -msgstr "Einstellungen" - -msgid "Login" -msgstr "Anmeldung" diff --git a/frontend/locales/en_US/LC_MESSAGES/messages.mo b/frontend/locales/en_US/LC_MESSAGES/messages.mo deleted file mode 100644 index beee7369448f88660ed12eb978b8d805ecdb540e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 425 zcmZvY!Ab)$5Qd{xki7_=J>)K_*~LSRRis+6&|+ywZCb3D}60s^GW)DR47sM9vOl+?)_Cgx-CG|0X zC5`zTDVe7|Kx~?#m4FS>EBCx)9Qy8P@OvDZNV4PJvt;@Ph22eSVq8AD? z5v_R-8&W1+9Z=+6+c4AUyy7jeQ(YR% zhxqaCOYpoo`nFC3&LLX$%DWS~YKyrpK;)oH#jUgO<0%d{>2-STKkFuE&LUWP@TsDW s5FT;AnrUN?`1oG?0C;?zUx>!R!2kdN diff --git a/frontend/locales/en_US/LC_MESSAGES/messages.po b/frontend/locales/en_US/LC_MESSAGES/messages.po deleted file mode 100644 index 9cb1f5c..0000000 --- a/frontend/locales/en_US/LC_MESSAGES/messages.po +++ /dev/null @@ -1,25 +0,0 @@ -# English translations for Photos.network frontend. -# Copyright (C) 2020 Photos.network -# This file is distributed under the Apache License, Version 2.0 by Photos.network. -# -msgid "" -msgstr "" -"Project-Id-Version: 1.0.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-04-19 20:12+0800\n" -"PO-Revision-Date: 2022-04-19 20:12+0800\n" -"Last-Translator: Benjamin Stรผrmer \n" -"Language-Team: none\n" -"Language: en\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -msgid "Home" -msgstr "Home" - -msgid "Settings" -msgstr "Settings" - -msgid "Login" -msgstr "Login" diff --git a/frontend/locales/vi_VN/LC_MESSAGES/messages.mo b/frontend/locales/vi_VN/LC_MESSAGES/messages.mo deleted file mode 100644 index d429c1f45bab2ac011f95295c38292b142b9d4c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 410 zcmY+A&q@O^5XPfckVO$Zd&pf~CvlR9xGE|qRLV+n6xZT7YAKH*b;|guLhB^2gPEvY8{Wg!yUW4x zv;I%((&G$5t6VsHL8~U4sSHF4Dwo_?1HV1V=Q?UP+l_yAqbq9=3_bWnua_Ulr|adSU|;8! BaKr!r diff --git a/frontend/locales/vi_VN/LC_MESSAGES/messages.po b/frontend/locales/vi_VN/LC_MESSAGES/messages.po deleted file mode 100644 index 249a817..0000000 --- a/frontend/locales/vi_VN/LC_MESSAGES/messages.po +++ /dev/null @@ -1,22 +0,0 @@ -# Vietnamese translations for Photos.network frontend. -# Copyright (C) 2020 Photos.network -# This file is distributed under the Apache License, Version 2.0 by Photos.network. -# -msgid "" -msgstr "" -"Project-Id-Version: 1.0.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-04-19 20:12+0800\n" -"PO-Revision-Date: 2022-04-19 20:12+0800\n" -"Last-Translator: Benjamin Stรผrmer \n" -"Language-Team: none\n" -"Language: vi\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -msgid "Home" -msgstr "Trang chรญnh" - -msgid "Settings" -msgstr "Thiแบฟt lแบญp" diff --git a/frontend/locales/zh_TW/LC_MESSAGES/messages.mo b/frontend/locales/zh_TW/LC_MESSAGES/messages.mo deleted file mode 100644 index 231e37df9647ffdde2e87088d6338f10f339f19f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 400 zcmY+Aze+8Eo(;5^Q_)yM_`>YTF_FF7kSi%+*r?=E>(S2ourN> zM$o8rz}j%8!X(?39t4ef95Y^Kr8O)?DPG|f&ZCBOo+ValHU1JgU\n" -"Language-Team: none\n" -"Language: zh_TW\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -msgid "Home" -msgstr "้ฆ–้ " - -msgid "Settings" -msgstr "่จญๅฎš" diff --git a/frontend/static/browserconfig.xml b/frontend/static/browserconfig.xml deleted file mode 100644 index c554148..0000000 --- a/frontend/static/browserconfig.xml +++ /dev/null @@ -1,2 +0,0 @@ - -#ffffff \ No newline at end of file diff --git a/frontend/static/custom-icon.png b/frontend/static/custom-icon.png deleted file mode 100644 index 6fd7f9750bfae850f4439e686ea3e4b4c7e524d1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15999 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 - - - - - - - Photos.network | {% block title %}{% endblock %} - - - - - - - - - - - - - - - - - - - - - - - - {% include 'partials/header.jinja2' %} - -
- {% block body %}{% endblock %} -
- - {% include 'partials/footer.jinja2' %} - - - - - - diff --git a/frontend/templates/error.jinja2 b/frontend/templates/error.jinja2 deleted file mode 100644 index e06841f..0000000 --- a/frontend/templates/error.jinja2 +++ /dev/null @@ -1,28 +0,0 @@ -{% extends 'base.jinja2' %} - -{% block title %}Error{% endblock %} - -{% block body %} - -
-
-
-
-
- {{ status_code}} -
-
-
-

{{ error }}

-

Please check the URL in the address bar and try again.

- Go back home - - - - -
-
-
-
- -{% endblock %} diff --git a/frontend/templates/index.jinja2 b/frontend/templates/index.jinja2 deleted file mode 100644 index 4d388f7..0000000 --- a/frontend/templates/index.jinja2 +++ /dev/null @@ -1,29 +0,0 @@ -{% extends 'base.jinja2' %} - -{% block title %}Index{% endblock %} - -{% block body %} - -{% if not username %} - -
-
-
-

Login required

-

There are no public images available, please login or check the link.

-
- -
-
- - {% include 'partials/app.jinja2' %} - -{% else %} - - - - {% include 'index/grid.jinja2' %} - -{% endif %} - -{% endblock %} diff --git a/frontend/templates/index/grid.jinja2 b/frontend/templates/index/grid.jinja2 deleted file mode 100644 index c617a8c..0000000 --- a/frontend/templates/index/grid.jinja2 +++ /dev/null @@ -1,22 +0,0 @@ -
- - -
-
- {% if files is defined and files|length > 0 %} - {% for item in files %} - - {% if item.filetype == "video/mp4" %} - - {% else %} - {{ item.name }} - {% endif %} - - {% endfor %} - - {% else %} - No files to show - {% endif %} -
-
-
diff --git a/frontend/templates/index/shared.jinja2 b/frontend/templates/index/shared.jinja2 deleted file mode 100644 index 59693bd..0000000 --- a/frontend/templates/index/shared.jinja2 +++ /dev/null @@ -1,11 +0,0 @@ -{% extends 'base.jinja2' %} - -{% block title %}Index{% endblock %} - -{% block body %} - - - -{% include 'index/grid.jinja2' %} - -{% endblock %} diff --git a/frontend/templates/index/user.jinja2 b/frontend/templates/index/user.jinja2 deleted file mode 100644 index 88d43f8..0000000 --- a/frontend/templates/index/user.jinja2 +++ /dev/null @@ -1,171 +0,0 @@ -{% extends 'base.jinja2' %} - -{% block title %}Index{% endblock %} - -{% block body %} - - - - - -
-
- -
-
-
-
-

Profile

-

This information will be displayed publicly so be careful what you share.

-
-
-
-
-
-
-
-
- - -
- -
- - -
- -
- -
- -
-

Brief description for your profile. URLs are hyperlinked.

-
- -
- -
-
- -
-
-
-
- - - -
-
-
-
-

Account Information

-

These informations are only used by the system.

-
-
-
-
-
-
-
- -
- - {{ username }} -
- -
- - -
- -
-
-
- -
-
-
-
-
-
- - - -
-
-
-
-

Notifications

-

Decide which communications you'd like to receive and how.

-
-
-
-
-
-
-
- By Email -
-
-
- -
-
- -

Get notified when someones posts a comment on an image of yours.

-
-
-
-
- -
-
- -

Get notified when someone tags you in an image.

-
-
-
-
-
-
- Push Notifications -

These are delivered directly to your app on the mobile phone.

-
-
-
- - -
-
- - -
-
- - -
-
-
-
-
- -
-
-
-
-
-
- -
-
- - -{% endblock %} diff --git a/frontend/templates/login_required.jinja2 b/frontend/templates/login_required.jinja2 deleted file mode 100644 index 6259357..0000000 --- a/frontend/templates/login_required.jinja2 +++ /dev/null @@ -1,96 +0,0 @@ -{% extends 'base.jinja2' %} - -{% block title %}Login{% endblock %} - -{% block body %} - -
-
-
-

Account required

-

To get access to all available system features, an account is needed. Please login to start.

-
-
- -
-
-
- - - - -
-

People

-

Search or filter for images containing a specific person or object.

-
-
- -
-
-
- - - -
-

Location

-

Find a list of images, taken in a specific areaSearch easily for images on a map.

-
-
- -
-
-
- - - -
-

Privacy

-

Hide private images behind a quick filter when showing co-workers or friends fotos.

-
-
- -
-
-
- - - - - - -
-

The Catalyzer

-

Fingerstache flexitarian street art 8-bit waist co, subway tile poke farm.

-
-
- -
-
-
- - - -
-

Bunker

-

Fingerstache flexitarian street art 8-bit waist co, subway tile poke farm.

-
-
- -
-
-
- - - -
-

Shooting Stars

-

Fingerstache flexitarian street art 8-bit waist co, subway tile poke farm.

-
-
- -
- -
-
- -{% endblock %} diff --git a/frontend/templates/partials/app.jinja2 b/frontend/templates/partials/app.jinja2 deleted file mode 100644 index 2001587..0000000 --- a/frontend/templates/partials/app.jinja2 +++ /dev/null @@ -1,29 +0,0 @@ -
-
-
-

NATIVE APP

-

Try our native mobile apps

-
-
- - -
-
-
diff --git a/frontend/templates/partials/footer.jinja2 b/frontend/templates/partials/footer.jinja2 deleted file mode 100644 index 11db3d4..0000000 --- a/frontend/templates/partials/footer.jinja2 +++ /dev/null @@ -1,41 +0,0 @@ - diff --git a/frontend/templates/partials/header.jinja2 b/frontend/templates/partials/header.jinja2 deleted file mode 100644 index b61a7a5..0000000 --- a/frontend/templates/partials/header.jinja2 +++ /dev/null @@ -1,37 +0,0 @@ -
-
- - - - - - - - - - Photos.network - - - - - {% if username %} -
- {% else %} -
- {% endif %} - - - - -
- -
-
diff --git a/frontend/views/callback.py b/frontend/views/callback.py deleted file mode 100644 index 736053a..0000000 --- a/frontend/views/callback.py +++ /dev/null @@ -1,57 +0,0 @@ -import logging -import time -from hashlib import sha1 -from random import SystemRandom -from typing import TYPE_CHECKING - -import aiohttp_session -from aiohttp import web -from frontend.base.request import RequestView -from frontend.const import SCOPES - -if TYPE_CHECKING: - from frontend.frontend import Frontend - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class OauthCallbackView(RequestView): - """View to handle oauth callback after login.""" - - requires_auth = False - url = "/callback" - - async def get(self, frontend: "Frontend", request: web.Request): - """Initiate the authorization code grant flow and redirect the user .""" - - # its not a valid redirect, if 'code' is not within the query - if request.url.query["state"] != frontend.core_client.state: - _LOGGER.warn("invalid redirect call! ") - - # redirect to oauth provider - raise web.HTTPUnauthorized() - - code = request.url.query["code"] - state = frontend.core_client.state - - # request a token pair from the authorization server - provider_data = await frontend.core_client.get_access_token(code=code) - - # calculate timestamp - timestamp = time.time() - expires_in = int(timestamp) + provider_data["expires_in"] - - # collect informations for current user - user = await frontend.core_client.user_info() - - # save data into session - session = await aiohttp_session.get_session(request) - session["username"] = str(user.username) - session["first_name"] = str(user.first_name) - session["last_name"] = str(user.last_name) - session["expires_in"] = expires_in - session["access_token"] = str(provider_data["access_token"]) - session["refresh_token"] = str(provider_data["refresh_token"]) - - raise web.HTTPSeeOther(location="/") diff --git a/frontend/views/index.py b/frontend/views/index.py deleted file mode 100644 index acf1c28..0000000 --- a/frontend/views/index.py +++ /dev/null @@ -1,53 +0,0 @@ -import logging -from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict - -import aiohttp_jinja2 -import aiohttp_session -from aiohttp import web -from frontend.base.request import RequestView -from frontend.const import KEY_AUTHENTICATED - -if TYPE_CHECKING: - from frontend.frontend import Frontend - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class IndexView(RequestView): - """View to handle Status requests.""" - - requires_auth = False - url = "/" - - async def get(self, frontend: "Frontend", request: web.Request): - """Retrieve photos grid or landing page if not authenticated.""" - - files = [] - - authenticated = request.get(KEY_AUTHENTICATED, False) - - if authenticated: - files = await self._get_files_for_user(frontend, request) - - response = aiohttp_jinja2.render_template( - template_name="index.jinja2", - request=request, - context={"files": files}, - ) - - return response - - async def head(self, frontend: "Frontend", request: web.Request): - """Retrieve if frontend is running.""" - return self.json_message("Frontend is running.") - - async def _get_files_for_user(self, frontend: "Frontend", request: web.Request) -> Dict[str, Any]: - """Load files where the current user has access""" - - response = await frontend.core_client.get_photos() - - if "results" in response: - return response["results"] - - return [] diff --git a/frontend/views/login.py b/frontend/views/login.py deleted file mode 100644 index d38e849..0000000 --- a/frontend/views/login.py +++ /dev/null @@ -1,36 +0,0 @@ -import logging -from hashlib import sha1 -from random import SystemRandom -from typing import TYPE_CHECKING - -from aiohttp import web -from frontend.base.request import RequestView -from frontend.const import SCOPES - -if TYPE_CHECKING: - from frontend.frontend import Frontend - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class LoginView(RequestView): - """View to handle login requests.""" - - requires_auth = False - url = "/login" - - async def get(self, frontend: "Frontend", request: web.Request): - """Initiate the authorization code grant flow and redirect the user .""" - # create an opaque value to prevent cross-site requests - state = str(sha1(str(SystemRandom().random()).encode("ascii")).hexdigest()) - - frontend.core_client.state = state - - authorization_url = frontend.core_client.get_authorize_url( - scope=SCOPES, - state=state, - ) - - # redirect the user-agent to the generated authorization endpoint - raise web.HTTPFound(location=authorization_url) diff --git a/frontend/views/logout.py b/frontend/views/logout.py deleted file mode 100644 index 09b6891..0000000 --- a/frontend/views/logout.py +++ /dev/null @@ -1,28 +0,0 @@ -import logging -from hashlib import sha1 -from random import SystemRandom -from typing import TYPE_CHECKING - -import aiohttp_session -from aiohttp import web -from frontend.base.request import RequestView -from frontend.const import SCOPES - -if TYPE_CHECKING: - from frontend.frontend import Frontend - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class LogoutView(RequestView): - """View to handle logout requests.""" - - requires_auth = False - url = "/logout" - - async def get(self, frontend: "Frontend", request: web.Request): - session = await aiohttp_session.get_session(request) - session["username"] = None - - raise web.HTTPSeeOther(location="/") diff --git a/frontend/views/photo.py b/frontend/views/photo.py deleted file mode 100644 index 0fde185..0000000 --- a/frontend/views/photo.py +++ /dev/null @@ -1,42 +0,0 @@ -import fileinput -import logging -import tempfile -from hashlib import sha1 -from random import SystemRandom -from typing import TYPE_CHECKING - -import aiohttp_session -from aiohttp import web -from frontend.base.request import RequestView -from frontend.const import SCOPES - -if TYPE_CHECKING: - from frontend.frontend import Frontend - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class PhotoView(RequestView): - """View to handle logout requests.""" - - requires_auth = False - url = "/photo/{entity_id}" - - async def get( - self, - frontend: "Frontend", - request: web.Request, - entity_id: str, - ): - file_response = await frontend.core_client.request( - method="GET", - url="/api/file/" + entity_id, - ) - - resp = web.StreamResponse(status=200) - resp.headers["Content-Type"] = "image/jpeg" - resp.headers["Content-Length"] = str(len(file_response)) - await resp.prepare(request) - await resp.write(file_response) - return resp diff --git a/frontend/views/settings.py b/frontend/views/settings.py deleted file mode 100644 index 4f1f10d..0000000 --- a/frontend/views/settings.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import TYPE_CHECKING - -import aiohttp_jinja2 -from aiohttp import web -from frontend.base.request import RequestView - -if TYPE_CHECKING: - from frontend.frontend import Frontend - - -class SettingsView(RequestView): - """View to handle user settings.""" - - requires_auth = True - url = "/settings" - - async def get(self, frontend: "Frontend", request: web.Request): - response = aiohttp_jinja2.render_template( - template_name="settings.jinja2", - request=request, - context={}, - ) - - return response diff --git a/frontend/views/shared.py b/frontend/views/shared.py deleted file mode 100644 index b5387d7..0000000 --- a/frontend/views/shared.py +++ /dev/null @@ -1,46 +0,0 @@ -import fileinput -import logging -from hashlib import sha1 -from random import SystemRandom -from typing import TYPE_CHECKING - -import aiohttp_jinja2 -import aiohttp_session -from aiohttp import web -from frontend.base.request import RequestView -from frontend.const import SCOPES - -if TYPE_CHECKING: - from frontend.frontend import Frontend - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class SharedView(RequestView): - """View to handle shared links.""" - - requires_auth = False - url = "/shared/{entity_id}" - - async def get( - self, - frontend: "Frontend", - request: web.Request, - entity_id: str, - ): - _LOGGER.warn("/shared/" + entity_id) - - session = await aiohttp_session.get_session(request) - username = session.get("username") - - # TODO: get files for shared album - files = [] - - response = aiohttp_jinja2.render_template( - template_name="index/shared.jinja2", - request=request, - context={"files": files}, - ) - - return response diff --git a/frontend/views/user.py b/frontend/views/user.py deleted file mode 100644 index c967a86..0000000 --- a/frontend/views/user.py +++ /dev/null @@ -1,85 +0,0 @@ -import logging -from hashlib import sha1 -from random import SystemRandom -from typing import TYPE_CHECKING - -import aiohttp_jinja2 -import aiohttp_session -from aiohttp import web -from frontend.base.request import RequestView -from frontend.const import SCOPES - -if TYPE_CHECKING: - from frontend.frontend import Frontend - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class UserView(RequestView): - """View to handle logout requests.""" - - requires_auth = True - url = "/user" - - async def get(self, frontend: "Frontend", request: web.Request): - """Display user profile and preferences.""" - session = await aiohttp_session.get_session(request) - username = session.get("username") - - user = await frontend.core_client.user_info() - - _LOGGER.debug(str(user)) - - response = aiohttp_jinja2.render_template( - template_name="index/user.jinja2", - request=request, - context={"username": username, "first_name": user.first_name, "last_name": user.last_name}, - ) - - return response - - async def post(self, frontend: "Frontend", request: web.Request): - """update user profile and preferences.""" - - _LOGGER.warn("apply user changes is not implemented yet") - - data = await request.post() - _LOGGER.error(str(data)) - - if "first-name" in data: - first_name = data["first-name"] - _LOGGER.debug("new first-name: " + str(first_name)) - - if "last-name" in data: - last_name = data["last-name"] - _LOGGER.debug("new last-name: " + str(last_name)) - - if "about" in data: - about = data["about"] - _LOGGER.debug("new about: " + str(about)) - - if "password" in data: - _LOGGER.debug("new password: " + str(data["password"])) - - user = await frontend.core_client.user_info() - - _LOGGER.debug("user: " + str(user)) - - user_id = user.id - update_url = "/api/user/" + str(user_id) - - raw_data = '{"firstname": "' + str(first_name) + '", "lastname": "Administrator"}' - _LOGGER.debug("payload: " + str(raw_data)) - _LOGGER.debug("payload: " + str(type(raw_data))) - - # TODO: patch user - - # await frontend.core_client.patch_user( - # method="PATCH", - # url=update_url, - # headers={"Content-Type": "application/x-www-form-urlencoded"}, - # data=raw_data, - # ) - - raise web.HTTPFound(location="/user") diff --git a/frontend/webserver.py b/frontend/webserver.py deleted file mode 100644 index 4ad8026..0000000 --- a/frontend/webserver.py +++ /dev/null @@ -1,173 +0,0 @@ -import importlib.resources as importlib_resources -import logging -import os -import time -from ipaddress import ip_address -from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict - -import aiohttp_jinja2 -import aiohttp_session -import jinja2 -from aiohttp import web -from aiohttp.web_middlewares import middleware -from aiohttp_session import session_middleware -from aiohttp_session.cookie_storage import EncryptedCookieStorage - -from frontend.const import KEY_AUTHENTICATED -from frontend.i18n import i18n -from frontend.views.callback import OauthCallbackView -from frontend.views.index import IndexView -from frontend.views.login import LoginView -from frontend.views.logout import LogoutView -from frontend.views.photo import PhotoView -from frontend.views.settings import SettingsView -from frontend.views.user import UserView - -if TYPE_CHECKING: - from frontend.frontend import Frontend - -MAX_CLIENT_SIZE: int = 1024**2 * 16 - -_LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) - - -class Webserver: - def __init__(self, frontend: "Frontend"): - self.frontend = frontend - self.app = web.Application(middlewares=[], client_max_size=MAX_CLIENT_SIZE) - self.runner = web.AppRunner(self.app) - - # check if secret length is 32 bytes - if len(self.frontend.config.cookie_secret) == 32: - _LOGGER.debug("use cookie_secret from file") - secret_key = bytes(self.frontend.config.cookie_secret, "utf-8") - else: - _LOGGER.debug("use fallback cookie_secret") - secret_key = bytes("Thirty two length bytes key!", "utf-8") - - # TODO: switch to Redis storage? - aiohttp_session.setup( - self.app, - EncryptedCookieStorage( - secret_key=secret_key, - cookie_name="Photos.network", - ), - ) - - # init jinja2 template engine - pkg = importlib_resources.files("frontend") - env = aiohttp_jinja2.setup( - self.app, - loader=jinja2.FileSystemLoader(pkg / "templates"), - extensions=["jinja2.ext.i18n", "jinja2.ext.debug"], - context_processors=[self.username_ctx_processor], - ) - - # setup i18n - env.install_gettext_translations(i18n, newstyle=True) - - self.register_request(LoginView()) - self.register_request(LogoutView()) - self.register_request(OauthCallbackView()) - self.register_request(IndexView()) - self.register_request(PhotoView()) - self.register_request(UserView()) - self.register_request(SettingsView()) - - # self.app.router.add_static("/static", os.path.join("frontend/static")) - - self.app.middlewares.append(self.auth_middleware) - - async def start(self): - """Start webserver.""" - await self.runner.setup() - - frontend_port = self.frontend.config.frontend_port - - # use host=None to listen on all interfaces. - site = web.TCPSite(runner=self.runner, host=None, port=frontend_port) - await site.start() - _LOGGER.info(f"Webserver is listening on {site._host}:{site._port}") - - async def stop(self): - """Stop webserver.""" - await self.runner.cleanup() - - def register_request(self, view): - """ - Register a request. - The view argument must be a class that inherits from Request. - It is optional to instantiate it before registering; this method will - handle it either way. - """ - # if isinstance(view, type): - # # Instantiate the view, if needed - # view = view() - - if not hasattr(view, "url"): - class_name = view.__class__.__name__ - raise AttributeError(f'{class_name} missing required attribute "url"') - - view.register(self.frontend, self.app.router) - - async def username_ctx_processor(self, request: web.Request) -> Dict[str, Any]: - """Jinja2 context processor to extract the username from an active session.""" - session = await aiohttp_session.get_session(request) - username = session.get("username") - - return {"username": username} - - @middleware - async def auth_middleware( - self, request: web.Request, handler: Callable[[web.Request], Awaitable[web.StreamResponse]] - ) -> web.StreamResponse: - """Check if user is authenticated and authentication is still valid.""" - - authenticated = False - session = await aiohttp_session.get_session(request) - username = session.get("username") - - if not username: - return await handler(request) - - refresh_token = session.get("refresh_token") - expires_in = session.get("expires_in") - - if expires_in is not None and refresh_token is not None: - now = int(time.time()) - - # refresh token if access_token expires in next 3 minutes or has been expired already - if (expires_in - now) <= 180: - _LOGGER.info("access token is expiring in " + str((expires_in - now)) + " seconds. Try to refresh...") - - ( - statusCode, - accessToken, - refreshToken, - expiresIn, - ) = await self.frontend.core_client.refresh_access_token_call() - if statusCode is not None and statusCode == 200: - session["expires_in"] = expiresIn - session["access_token"] = str(accessToken) - session["refresh_token"] = str(refreshToken) - - authenticated = True - else: - authenticated = False - - else: - # still authenticated - authenticated = True - - validate_access_token_status = await self.frontend.core_client.check_access_token() - if validate_access_token_status != 200: - authenticated = False - - if self.frontend.core_client.accessToken == None: - session.clear() - raise web.HTTPFound(location="/") - - request[KEY_AUTHENTICATED] = authenticated - - return await handler(request) diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d2da1bd..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -python-i18n>=0.3.9 -aiohttp-jinja2>=1.4.2 -aiohttp_session>=2.11.0 -aioauth-client>=0.27.0 -cryptography>=1.6 -colorlog>=4.0.0 diff --git a/requirements_test.txt b/requirements_test.txt deleted file mode 100644 index c0ef305..0000000 --- a/requirements_test.txt +++ /dev/null @@ -1,12 +0,0 @@ --r requirements.txt - -pre-commit>=2.17.0 -pylint>=2.12.0 -pytest-asyncio>=0.14.0 -pytest-aiohttp>=0.3.0 -pytest-cov>=2.10.1 -pytest-test-groups>=1.0.3 -pytest-sugar>=0.9.4 -pytest-timeout>=1.4.2 -pytest-xdist>=2.1.0 -pytest>=6.1.2 diff --git a/server.py b/server.py deleted file mode 100644 index fc53cad..0000000 --- a/server.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Entry point of Photos.network frontend.""" - -import sys - -from frontend.__main__ import main - -if __name__ == "__main__": - sys.exit(main()) diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 01445f6..0000000 --- a/setup.cfg +++ /dev/null @@ -1,42 +0,0 @@ -[metadata] -DEBUG=True - -license = Apache License 2.0 -license_file = LICENSE.md -platforms = any -description = Open-source platform for photo management running on Python 3. -long_description = file: README.md -keywords = photos, self-hosted, google-photos, apple-photos, object-detection, face-recognition, face-detection -classifier = - Intended Audience :: End Users/Desktop - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Operating System :: OS Independent - Topic :: Software Development :: Libraries :: Python Modules - Topic :: Scientific/Engineering :: Atmospheric Science - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - Programming Language :: Python :: 3.10 - -[flake8] -exclude = venv,.git,build -doctests = True -# To work with Black -# E501: line too long -# W503: Line break occurred before a binary operator -# W504 line break after binary operator -ignore = - E501, - E722, - W503, - W504 - -[mypy] -python_version = 3.10 -show_error_codes = true -ignore_errors = true -follow_imports = silent -ignore_missing_imports = true -warn_incomplete_stub = true -warn_redundant_casts = true -warn_unused_configs = true diff --git a/setup.py b/setup.py deleted file mode 100644 index f680eb9..0000000 --- a/setup.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Photos.network setup script""" -import sys - -from setuptools import setup - -from frontend import const - -if sys.version_info < (3, 0): - print("{PROJECT_NAME} requires python version >= 3.0") - sys.exit(1) - -setup( - name="frontend", - version=const.FRONTEND_VERSION, - description="The default web frontend for photos.network", - long_description="The default web frontend for photos.network to manage components.", - author="The Photos.network Authors", - author_email="devs@photos.network", - url="https://developers.photos.network/frontend/", - license="Apache License 2.0", - classifiers=[ - "Intended Audience :: End Users/Desktop", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Operating System :: OS Independent", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Scientific/Engineering :: Atmospheric Science", - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "Programming Language :: Python :: 3.10", - ], - keywords=["docker", "photos-network", "api"], - zip_safe=False, - platforms="any", - packages=[ - "frontend", - "frontend.base", - "frontend.static", - "frontend.templates", - "frontend.views", - ], - entry_points={"console_scripts": ["frontend = frontend.__main__:main"]}, - include_package_data=True, - package_data={ - "frontend": ["locales/**/**/*.mo", "static/**", "templates/*.jinja2", "templates/**/*.jinja2"], - }, - install_requires=[ - "aiohttp>=3.7.0,<4.0", - "python-i18n>=0.3.9", - "aiohttp-jinja2==1.5", - "aiohttp_session==2.11.0", - "aioauth-client==0.27.3", - "cryptography==37.0.4", - "colorlog==6.6.0", - ], -) diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index 459a6eb..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for Photos.network""" diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 036e16e..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Add helpers and fixtures for tests in this directory.""" - -import pytest -from aiohttp import web -from aiohttp.test_utils import TestServer - - -@pytest.fixture() -def mock_server(loop): - app = web.Application() - - test_server = TestServer(app) - return test_server diff --git a/tests/login/test_routes_login.py b/tests/login/test_routes_login.py deleted file mode 100644 index 136db43..0000000 --- a/tests/login/test_routes_login.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Test login routes.""" -from typing import Generator - -import aiohttp_session -from aiohttp import web -from aiohttp.test_utils import TestClient -from aiohttp_session import get_session, new_session -from frontend.application import create_application - - -async def test_login_redirect(aiohttp_client: Generator): - """Test login redirect.""" - # given - app = create_application() - client: TestClient = await aiohttp_client(app) - - # when - resp = await client.get("/login", allow_redirects=False) - - # then - assert resp.status == 302 - - -async def test_logout_endpoint(aiohttp_client: Generator): - """Test if logout endpoint is valid.""" - # given - app = create_application() - client: TestClient = await aiohttp_client(app) - - # when - resp = await client.get("/logout") - - # then - assert resp.status == 200 - - -async def test_logout_redirect(aiohttp_client: Generator): - """Test if logout clears session.""" - # given - app = create_application() - client: TestClient = await aiohttp_client(app) - await client.get("/logout") - - # when - resp = await client.get("/settings", allow_redirects=False) - - # then - assert resp.status == 401 diff --git a/tests/test_internationalization.py b/tests/test_internationalization.py deleted file mode 100644 index f9058cd..0000000 --- a/tests/test_internationalization.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Test application Internationalization.""" -from frontend.application import create_application - - -async def test_login_button_in_de(aiohttp_client): - """Test Internationalized login button in header layout.""" - - app = create_application() - - client = await aiohttp_client(app) - resp = await client.get("/", headers={"Accept-Language": "de"}) - assert resp.status == 200 - text = await resp.text() - assert "Anmeldung" in text - - -async def test_login_button_in_en(aiohttp_client): - """Test Internationalized login button in header layout.""" - - app = create_application() - - client = await aiohttp_client(app) - resp = await client.get("/", headers={"Accept-Language": "en"}) - assert resp.status == 200 - text = await resp.text() - assert "Login" in text diff --git a/tests/test_oauth_flow.py b/tests/test_oauth_flow.py deleted file mode 100644 index 3f79947..0000000 --- a/tests/test_oauth_flow.py +++ /dev/null @@ -1,4 +0,0 @@ - - -# TODO: check authorization url against mock server -# TODO: \ No newline at end of file diff --git a/tests/test_routes_static.py b/tests/test_routes_static.py deleted file mode 100644 index d049fe0..0000000 --- a/tests/test_routes_static.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Test static application routes.""" -from frontend.application import create_application - - -async def test_favicon(aiohttp_client): - """Test favicon delivery.""" - - app = create_application() - - client = await aiohttp_client(app) - resp = await client.get("/static/favicon.ico") - assert resp.status == 200