mirror of
https://github.com/Casvt/MIND.git
synced 2026-02-19 11:54:46 -05:00
@@ -1,5 +1,5 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
**/__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
@@ -129,14 +129,15 @@ dmypy.json
|
||||
.pyre/
|
||||
|
||||
# Database
|
||||
*.db
|
||||
**/*.db
|
||||
**/*.db-shm
|
||||
**/*.db-wal
|
||||
|
||||
# VS code
|
||||
*.code-workspace
|
||||
.vscode/
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
|
||||
@@ -146,8 +147,15 @@ docker-compose.yml
|
||||
.github/
|
||||
|
||||
# Various files
|
||||
*.md
|
||||
**/*.md
|
||||
LICENSE
|
||||
|
||||
# Tests
|
||||
tests/
|
||||
|
||||
# Project management files
|
||||
release.sh
|
||||
docs/
|
||||
docs-requirements.txt
|
||||
mkdocs.yml
|
||||
generate_api_docs.py
|
||||
|
||||
25
.github/workflows/build_docs.yml
vendored
Normal file
25
.github/workflows/build_docs.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Update and build docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- Development
|
||||
|
||||
jobs:
|
||||
update_build:
|
||||
name: Update API docs and build docs
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
name: Setup checkout
|
||||
- uses: actions/setup-python@v4
|
||||
name: Setup python 3.8
|
||||
with:
|
||||
python-version: 3.8
|
||||
cache: 'pip'
|
||||
- run: pip install -r requirements.txt -r project_management/docs-requirements.txt
|
||||
name: Install dependencies
|
||||
- run: python3 project_management/generate_api_docs.py
|
||||
name: Generate API docs
|
||||
- run: mkdocs gh-deploy --force -f project_management/mkdocs.yml
|
||||
name: Build docs
|
||||
32
.github/workflows/tests.yml
vendored
Normal file
32
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- Development
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
name: Set up checkout
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-python@v4
|
||||
name: Set up Python ${{ matrix.python-version }}
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
|
||||
- run: pip install -r requirements.txt
|
||||
name: Install dependencies
|
||||
|
||||
- run: |
|
||||
mkdir db
|
||||
python -m unittest discover -s ./tests -p '*.py'
|
||||
name: Run Tests
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -130,6 +130,11 @@ dmypy.json
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
|
||||
# VS code
|
||||
*.code-workspace
|
||||
|
||||
# Project management files
|
||||
release.sh
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Contributing to MIND
|
||||
## General steps
|
||||
Contributing to MIND consists of 5 steps, listed hereunder.
|
||||
Contributing to MIND consists of 5 steps:
|
||||
|
||||
1. Make a [contributing request](https://github.com/Casvt/MIND/issues/new?template=contribute-request.md), where you describe what you plan on doing. This request needs to get approved before you can start, or your pull request won't be accepted. This is to avoid multiple people from doing the same thing and to avoid you wasting your time if we do not wish the changes. This is also where discussions can be held about how something will be implemented.
|
||||
2. When the request is accepted, start your local development (more info about this below).
|
||||
@@ -11,24 +11,25 @@ Contributing to MIND consists of 5 steps, listed hereunder.
|
||||
## Local development steps
|
||||
Once your request is accepted, you can start your local development.
|
||||
|
||||
1. Clone the repository onto your computer and open it using your prefered IDE (Visual Studio Code is used by us).
|
||||
1. Fork the repository and clone the fork onto your computer and open it using your preferred IDE (Visual Studio Code is used by us).
|
||||
2. Make the changes needed and write accompanying tests.
|
||||
3. Check if the code written follows the styling guide below.
|
||||
4. Run the finished version, using python 3.7, to check if you've made any errors.
|
||||
5. Run the tests (unittest is used). This can be done with a button click within VS Code, or with the following command where you need to be inside the root folder of the project:
|
||||
4. If you want to run the tests manually before committing, use the command below in the root folder of the project:
|
||||
```bash
|
||||
python3 -m unittest discover -v -s './tests' -p '*_test.py'
|
||||
```
|
||||
6. Test your version thoroughly to catch as many bugs as possible (if any).
|
||||
5. Update the docs if needed.
|
||||
6. Commit and push to your fork. When you push, GitHub Actions will do a lot of work for you: the tests are run again on python versions 3.8 - 3.11, the API documentation is updated if any changes have been made to the API and the docs are updated if any changes have been made to the docs. All GitHub Actions need to succeed before you're allowed to make a PR (you'll see a green checkmark next to the commit in GitHub).
|
||||
|
||||
## Styling guide
|
||||
The code of MIND is written in such way that it follows the following rules. Your code should too.
|
||||
|
||||
1. Compatible with python 3.7 .
|
||||
1. Compatible with python 3.8 .
|
||||
2. Tabs (4 space size) are used for indentation.
|
||||
3. Use type hints as much as possible, though don't if it requires importing extra functions or classes (except for the `typing` library).
|
||||
4. Each function in the backend needs a doc string describing the function, what the inputs are, what errors could be raised from within the function and what the output is.
|
||||
5. Each function in the `api.py` file should have a doc string describing the api endpoint. For the formatting, look at the doc strings of the other functions in the file.
|
||||
6. The imports need to be sorted (the extension `isort` is used in VS Code).
|
||||
7. The code needs to be compatible with Linux, MacOS and Windows.
|
||||
8. The code should, though not strictly enforced, reasonably comply with the rule of 80 characters per line.
|
||||
5. The imports need to be sorted (the extension `isort` is used in VS Code).
|
||||
6. The code needs to be compatible with Linux, MacOS, Windows and Docker.
|
||||
7. The code should, though not strictly enforced, reasonably comply with the rule of 80 characters per line.
|
||||
|
||||
If you just code in the same style as the current code, you'll follow most of these rules automatically.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM python:3.8-slim-buster
|
||||
STOPSIGNAL SIGINT
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
29
MIND.py
29
MIND.py
@@ -11,16 +11,23 @@ from flask import Flask, render_template, request
|
||||
from waitress.server import create_server
|
||||
from werkzeug.middleware.dispatcher import DispatcherMiddleware
|
||||
|
||||
from backend.db import DBConnection, close_db, setup_db
|
||||
from frontend.api import api, reminder_handler
|
||||
from backend.db import DBConnection, ThreadedTaskDispatcher, close_db, setup_db
|
||||
from frontend.api import api, api_prefix, reminder_handler
|
||||
from frontend.ui import ui
|
||||
|
||||
HOST = '0.0.0.0'
|
||||
PORT = '8080'
|
||||
URL_PREFIX = '' # Must either be empty or start with '/' e.g. '/mind'
|
||||
LOGGING_LEVEL = logging.INFO
|
||||
THREADS = 10
|
||||
DB_FILENAME = 'db', 'MIND.db'
|
||||
|
||||
logging.basicConfig(
|
||||
level=LOGGING_LEVEL,
|
||||
format='[%(asctime)s][%(threadName)s][%(levelname)s] %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
def _folder_path(*folders) -> str:
|
||||
"""Turn filepaths relative to the project folder into absolute paths
|
||||
Returns:
|
||||
@@ -32,7 +39,7 @@ def _create_app() -> Flask:
|
||||
"""Create a Flask app instance
|
||||
Returns:
|
||||
Flask: The created app instance
|
||||
"""
|
||||
"""
|
||||
app = Flask(
|
||||
__name__,
|
||||
template_folder=_folder_path('frontend','templates'),
|
||||
@@ -65,7 +72,7 @@ def _create_app() -> Flask:
|
||||
return render_template('page_not_found.html', url_prefix=logging.URL_PREFIX)
|
||||
|
||||
app.register_blueprint(ui)
|
||||
app.register_blueprint(api, url_prefix="/api")
|
||||
app.register_blueprint(api, url_prefix=api_prefix)
|
||||
|
||||
# Setup closing database
|
||||
app.teardown_appcontext(close_db)
|
||||
@@ -79,7 +86,7 @@ def MIND() -> None:
|
||||
"""
|
||||
# Check python version
|
||||
if (version_info.major < 3) or (version_info.major == 3 and version_info.minor < 7):
|
||||
print('Error: the minimum python version required is python3.7 (currently ' + version_info.major + '.' + version_info.minor + '.' + version_info.micro + ')')
|
||||
logging.error('Error: the minimum python version required is python3.7 (currently ' + version_info.major + '.' + version_info.minor + '.' + version_info.micro + ')')
|
||||
exit(1)
|
||||
|
||||
# Register web server
|
||||
@@ -90,18 +97,22 @@ def MIND() -> None:
|
||||
app = _create_app()
|
||||
with app.app_context():
|
||||
if isfile(_folder_path('db', 'Noted.db')):
|
||||
move(_folder_path('db', 'Noted.db'), _folder_path('db', 'MIND.db'))
|
||||
move(_folder_path('db', 'Noted.db'), _folder_path(*DB_FILENAME))
|
||||
|
||||
db_location = _folder_path(*DB_FILENAME)
|
||||
logging.debug(f'Database location: {db_location}')
|
||||
makedirs(dirname(db_location), exist_ok=True)
|
||||
|
||||
DBConnection.file = db_location
|
||||
setup_db()
|
||||
reminder_handler.find_next_reminder()
|
||||
|
||||
# Create waitress server and run
|
||||
server = create_server(app, host=HOST, port=PORT, threads=THREADS)
|
||||
print(f'MIND running on http://{HOST}:{PORT}{URL_PREFIX}')
|
||||
dispatcher = ThreadedTaskDispatcher()
|
||||
dispatcher.set_thread_count(THREADS)
|
||||
server = create_server(app, _dispatcher=dispatcher, host=HOST, port=PORT, threads=THREADS)
|
||||
logging.info(f'MIND running on http://{HOST}:{PORT}{URL_PREFIX}')
|
||||
server.run()
|
||||
print(f'\nShutting down MIND...')
|
||||
|
||||
# Stopping thread
|
||||
reminder_handler.stop_handling()
|
||||
|
||||
52
README.md
52
README.md
@@ -1,30 +1,28 @@
|
||||
# MIND Reminders
|
||||
A simple self hosted reminder platform that uses push to send notifications to your device. Set the reminder and forget about it! 📢
|
||||
# MIND
|
||||
|
||||
[](https://hub.docker.com/r/mrcas/mind)
|
||||
[](https://github.com/Casvt/MIND/releases)
|
||||
|
||||
__A simple self hosted reminder application that can send push notifications to your device. Set the reminder and forget about it!__
|
||||
|
||||
Mind is a simple self hosted application for creating reminders that get pushed to your device using the [Apprise](https://github.com/caronc/apprise) API. You can send messages to just about every platform, including scheduled emails!
|
||||
|
||||
## Screenshots
|
||||

|
||||
## Workings
|
||||
MIND can be used for sending notifications at the desired time. This can be a set time, like a yearly reminder for a birthday, or at a button click, to easily send a predefined notification when you want to.
|
||||
|
||||

|
||||
## Features
|
||||
- Works cross-timezone
|
||||
- Notifications are sent with second-precision
|
||||
- Uses the [apprise library](https://github.com/caronc/apprise), giving you 80+ platforms to send notifications to
|
||||
- Easily manage the reminders with sorting options, search ability and color coding
|
||||
- Docker image available
|
||||
- Mobile friendly web-interface
|
||||
- API available
|
||||
|
||||

|
||||
### Planned Features
|
||||
You can see the planned features in the [Project board](https://github.com/users/Casvt/projects/3).
|
||||
|
||||

|
||||
|
||||
## Core Features
|
||||
* Basic auth
|
||||
* Utilizes Apprise
|
||||
* Create, edit and delete reminders
|
||||
* Schedule reminders
|
||||
* Recurring reminders
|
||||
* Docker image
|
||||
* Mobile friendly
|
||||
|
||||
## Planned Features
|
||||
You can see our planned features in our [Project board](https://github.com/users/Casvt/projects/3).
|
||||
|
||||
## Installation
|
||||
## Getting started
|
||||
Replace the timezone value (`TZ=`) to the [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) of your timezone!
|
||||
```bash
|
||||
docker run -d \
|
||||
@@ -34,13 +32,9 @@ docker run -d \
|
||||
-p 8080:8080 \
|
||||
mrcas/mind:latest
|
||||
```
|
||||
## Getting Started
|
||||
- Create a new account
|
||||
- Click the bell icon on the left side to add an Apprise push option and save it (Here is an example using Pushover)
|
||||
|
||||

|
||||
More information about installing can be found [in the wiki](https://casvt.github.io/MIND/).
|
||||
|
||||
|
||||
- Click the home icon and create a reminder!
|
||||
|
||||
You can see the [wiki](https://github.com/Casvt/MIND/wiki) for instructions on how to install MIND on other OS'es.
|
||||
## Contact
|
||||
- For support, a [discord server](https://discord.gg/nMNdgG7vsE) is available
|
||||
- Alternatively, [make an issue](https://github.com/Casvt/MIND/issues)
|
||||
|
||||
@@ -1,33 +1,39 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
class CustomException(Exception):
|
||||
def __init__(self, e=None) -> None:
|
||||
logging.warning(self.__doc__)
|
||||
super().__init__(e)
|
||||
return
|
||||
|
||||
class UsernameTaken(Exception):
|
||||
class UsernameTaken(CustomException):
|
||||
"""The username is already taken"""
|
||||
api_response = {'error': 'UsernameTaken', 'result': {}, 'code': 400}
|
||||
|
||||
class UsernameInvalid(Exception):
|
||||
class UsernameInvalid(CustomException):
|
||||
"""The username contains invalid characters"""
|
||||
api_response = {'error': 'UsernameInvalid', 'result': {}, 'code': 400}
|
||||
|
||||
class UserNotFound(Exception):
|
||||
"""The user requested by id or username can not be found"""
|
||||
class UserNotFound(CustomException):
|
||||
"""The user requested can not be found"""
|
||||
api_response = {'error': 'UserNotFound', 'result': {}, 'code': 404}
|
||||
|
||||
class AccessUnauthorized(Exception):
|
||||
class AccessUnauthorized(CustomException):
|
||||
"""The password given is not correct"""
|
||||
api_response = {'error': 'AccessUnauthorized', 'result': {}, 'code': 401}
|
||||
|
||||
class ReminderNotFound(Exception):
|
||||
class ReminderNotFound(CustomException):
|
||||
"""The reminder with the id can not be found"""
|
||||
api_response = {'error': 'ReminderNotFound', 'result': {}, 'code': 404}
|
||||
|
||||
class NotificationServiceNotFound(Exception):
|
||||
class NotificationServiceNotFound(CustomException):
|
||||
"""The notification service was not found"""
|
||||
api_response = {'error': 'NotificationServiceNotFound', 'result': {}, 'code': 404}
|
||||
|
||||
class NotificationServiceInUse(Exception):
|
||||
class NotificationServiceInUse(CustomException):
|
||||
"""The notification service is wished to be deleted but a reminder is still using it"""
|
||||
def __init__(self, type: str=''):
|
||||
self.type = type
|
||||
@@ -37,15 +43,11 @@ class NotificationServiceInUse(Exception):
|
||||
def api_response(self) -> Dict[str, Any]:
|
||||
return {'error': 'NotificationServiceInUse', 'result': {'type': self.type}, 'code': 400}
|
||||
|
||||
class InvalidTime(Exception):
|
||||
class InvalidTime(CustomException):
|
||||
"""The time given is in the past"""
|
||||
api_response = {'error': 'InvalidTime', 'result': {}, 'code': 400}
|
||||
|
||||
class InvalidURL(Exception):
|
||||
"""The apprise url is invalid"""
|
||||
api_response = {'error': 'InvalidURL', 'result': {}, 'code': 400}
|
||||
|
||||
class KeyNotFound(Exception):
|
||||
class KeyNotFound(CustomException):
|
||||
"""A key was not found in the input that is required to be given"""
|
||||
def __init__(self, key: str=''):
|
||||
self.key = key
|
||||
@@ -55,7 +57,7 @@ class KeyNotFound(Exception):
|
||||
def api_response(self) -> Dict[str, Any]:
|
||||
return {'error': 'KeyNotFound', 'result': {'key': self.key}, 'code': 400}
|
||||
|
||||
class InvalidKeyValue(Exception):
|
||||
class InvalidKeyValue(CustomException):
|
||||
"""The value of a key is invalid"""
|
||||
def __init__(self, key: str='', value: str=''):
|
||||
self.key = key
|
||||
@@ -66,6 +68,20 @@ class InvalidKeyValue(Exception):
|
||||
def api_response(self) -> Dict[str, Any]:
|
||||
return {'error': 'InvalidKeyValue', 'result': {'key': self.key, 'value': self.value}, 'code': 400}
|
||||
|
||||
class TemplateNotFound(Exception):
|
||||
class TemplateNotFound(CustomException):
|
||||
"""The template was not found"""
|
||||
api_response = {'error': 'TemplateNotFound', 'result': {}, 'code': 404}
|
||||
|
||||
class APIKeyInvalid(CustomException):
|
||||
"""The API key is not correct"""
|
||||
api_response = {'error': 'APIKeyInvalid', 'result': {}, 'code': 401}
|
||||
|
||||
def __init__(self, e=None) -> None:
|
||||
return
|
||||
|
||||
class APIKeyExpired(CustomException):
|
||||
"""The API key has expired"""
|
||||
api_response = {'error': 'APIKeyExpired', 'result': {}, 'code': 401}
|
||||
|
||||
def __init__(self, e=None) -> None:
|
||||
return
|
||||
|
||||
@@ -1,30 +1,58 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
from sqlite3 import Connection, Row
|
||||
from threading import current_thread
|
||||
import logging
|
||||
from sqlite3 import Connection, ProgrammingError, Row
|
||||
from threading import current_thread, main_thread
|
||||
from time import time
|
||||
from typing import Union
|
||||
|
||||
from flask import g
|
||||
from waitress.task import ThreadedTaskDispatcher as OldThreadedTaskDispatcher
|
||||
|
||||
__DATABASE_VERSION__ = 5
|
||||
from backend.custom_exceptions import AccessUnauthorized, UserNotFound
|
||||
|
||||
__DATABASE_VERSION__ = 6
|
||||
|
||||
class Singleton(type):
|
||||
_instances = {}
|
||||
def __call__(cls, *args, **kwargs):
|
||||
i = f'{cls}{current_thread()}'
|
||||
if i not in cls._instances:
|
||||
if (i not in cls._instances
|
||||
or cls._instances[i].closed):
|
||||
logging.debug(f'Creating singleton instance: {i}')
|
||||
cls._instances[i] = super(Singleton, cls).__call__(*args, **kwargs)
|
||||
|
||||
return cls._instances[i]
|
||||
|
||||
class ThreadedTaskDispatcher(OldThreadedTaskDispatcher):
|
||||
def handler_thread(self, thread_no: int) -> None:
|
||||
super().handler_thread(thread_no)
|
||||
i = f'{DBConnection}{current_thread()}'
|
||||
if i in Singleton._instances and not Singleton._instances[i].closed:
|
||||
logging.debug(f'Closing singleton instance: {i}')
|
||||
Singleton._instances[i].close()
|
||||
|
||||
def shutdown(self, cancel_pending: bool = True, timeout: int = 5) -> bool:
|
||||
print()
|
||||
logging.info('Shutting down MIND...')
|
||||
super().shutdown(cancel_pending, timeout)
|
||||
DBConnection(20.0).close()
|
||||
|
||||
class DBConnection(Connection, metaclass=Singleton):
|
||||
file = ''
|
||||
|
||||
def __init__(self, timeout: float) -> None:
|
||||
logging.debug(f'Opening database connection for {current_thread()}')
|
||||
super().__init__(self.file, timeout=timeout)
|
||||
super().cursor().execute("PRAGMA foreign_keys = ON;")
|
||||
self.closed = False
|
||||
return
|
||||
|
||||
def close(self) -> None:
|
||||
logging.debug(f'Closing database connection for {current_thread()}')
|
||||
self.closed = True
|
||||
super().close()
|
||||
return
|
||||
|
||||
def get_db(output_type: Union[dict, tuple]=tuple):
|
||||
@@ -54,11 +82,13 @@ def close_db(e=None) -> None:
|
||||
"""
|
||||
try:
|
||||
cursor = g.cursor
|
||||
db = cursor.connection
|
||||
db: DBConnection = cursor.connection
|
||||
cursor.close()
|
||||
delattr(g, 'cursor')
|
||||
db.commit()
|
||||
except AttributeError:
|
||||
if current_thread() is main_thread():
|
||||
db.close()
|
||||
except (AttributeError, ProgrammingError):
|
||||
pass
|
||||
return
|
||||
|
||||
@@ -67,7 +97,7 @@ def migrate_db(current_db_version: int) -> None:
|
||||
Migrate a MIND database from it's current version
|
||||
to the newest version supported by the MIND version installed.
|
||||
"""
|
||||
print('Migrating database to newer version...')
|
||||
logging.info('Migrating database to newer version...')
|
||||
cursor = get_db()
|
||||
if current_db_version == 1:
|
||||
# V1 -> V2
|
||||
@@ -168,13 +198,22 @@ def migrate_db(current_db_version: int) -> None:
|
||||
COMMIT;
|
||||
""")
|
||||
current_db_version = 5
|
||||
|
||||
|
||||
if current_db_version == 5:
|
||||
# V5 -> V6
|
||||
from backend.users import User
|
||||
try:
|
||||
User('User1', 'Password1').delete()
|
||||
except (UserNotFound, AccessUnauthorized):
|
||||
pass
|
||||
|
||||
return
|
||||
|
||||
def setup_db() -> None:
|
||||
"""Setup the database
|
||||
"""
|
||||
cursor = get_db()
|
||||
cursor.execute("PRAGMA journal_mode = wal;")
|
||||
|
||||
cursor.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS users(
|
||||
@@ -256,6 +295,7 @@ def setup_db() -> None:
|
||||
"SELECT value FROM config WHERE key = 'database_version' LIMIT 1;"
|
||||
).fetchone()[0])
|
||||
|
||||
logging.debug(f'Current database version {current_db_version} and desired database version {__DATABASE_VERSION__}')
|
||||
if current_db_version < __DATABASE_VERSION__:
|
||||
migrate_db(current_db_version)
|
||||
cursor.execute(
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from apprise import Apprise
|
||||
|
||||
from backend.custom_exceptions import (InvalidURL, NotificationServiceInUse,
|
||||
from backend.custom_exceptions import (NotificationServiceInUse,
|
||||
NotificationServiceNotFound)
|
||||
from backend.db import get_db
|
||||
|
||||
|
||||
class NotificationService:
|
||||
def __init__(self, notification_service_id: int) -> None:
|
||||
def __init__(self, user_id: int, notification_service_id: int) -> None:
|
||||
self.id = notification_service_id
|
||||
|
||||
if not get_db().execute(
|
||||
"SELECT 1 FROM notification_services WHERE id = ? LIMIT 1;",
|
||||
(self.id,)
|
||||
"SELECT 1 FROM notification_services WHERE id = ? AND user_id = ? LIMIT 1;",
|
||||
(self.id, user_id)
|
||||
).fetchone():
|
||||
raise NotificationServiceNotFound
|
||||
|
||||
@@ -46,9 +45,8 @@ class NotificationService:
|
||||
Returns:
|
||||
dict: The new info about the service
|
||||
"""
|
||||
if not Apprise().add(url):
|
||||
raise InvalidURL
|
||||
|
||||
logging.info(f'Updating notification service {self.id}: {title=}, {url=}')
|
||||
|
||||
# Get current data and update it with new values
|
||||
data = self.get()
|
||||
new_values = {
|
||||
@@ -79,7 +77,9 @@ class NotificationService:
|
||||
|
||||
Raises:
|
||||
NotificationServiceInUse: The service is still used by a reminder
|
||||
"""
|
||||
"""
|
||||
logging.info(f'Deleting notification service {self.id}')
|
||||
|
||||
# Check if no reminders exist with this service
|
||||
cursor = get_db()
|
||||
cursor.execute("""
|
||||
@@ -157,7 +157,7 @@ class NotificationServices:
|
||||
Returns:
|
||||
NotificationService: Instance of NotificationService
|
||||
"""
|
||||
return NotificationService(notification_service_id)
|
||||
return NotificationService(self.user_id, notification_service_id)
|
||||
|
||||
def add(self, title: str, url: str) -> NotificationService:
|
||||
"""Add a notification service
|
||||
@@ -166,15 +166,11 @@ class NotificationServices:
|
||||
title (str): The title of the service
|
||||
url (str): The apprise url of the service
|
||||
|
||||
Raises:
|
||||
InvalidURL: The apprise url is invalid
|
||||
|
||||
Returns:
|
||||
dict: The info about the new service
|
||||
"""
|
||||
if not Apprise().add(url):
|
||||
raise InvalidURL
|
||||
|
||||
"""
|
||||
logging.info(f'Adding notification service with {title=}, {url=}')
|
||||
|
||||
new_id = get_db().execute("""
|
||||
INSERT INTO notification_services(user_id, title, url)
|
||||
VALUES (?,?,?)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from sqlite3 import IntegrityError
|
||||
from threading import Timer
|
||||
from typing import List, Literal
|
||||
@@ -17,7 +18,6 @@ from backend.db import close_db, get_db
|
||||
filter_function = lambda query, p: (
|
||||
query in p["title"].lower()
|
||||
or query in p["text"].lower()
|
||||
or query in p["notification_service_title"].lower()
|
||||
)
|
||||
|
||||
def _find_next_time(
|
||||
@@ -30,7 +30,11 @@ def _find_next_time(
|
||||
current_time = datetime.fromtimestamp(datetime.utcnow().timestamp())
|
||||
while new_time <= current_time:
|
||||
new_time += td
|
||||
return int(new_time.timestamp())
|
||||
result = int(new_time.timestamp())
|
||||
logging.debug(
|
||||
f'{original_time=}, {current_time=} and interval of {repeat_interval} {repeat_quantity} leads to {result}'
|
||||
)
|
||||
return result
|
||||
|
||||
class ReminderHandler:
|
||||
"""Handle set reminders
|
||||
@@ -83,6 +87,7 @@ class ReminderHandler:
|
||||
"DELETE FROM reminders WHERE id = ?;",
|
||||
(reminder['id'],)
|
||||
)
|
||||
logging.info(f'Deleted reminder {reminder["id"]}')
|
||||
else:
|
||||
# Set next time
|
||||
new_time = _find_next_time(
|
||||
@@ -124,8 +129,14 @@ class ReminderHandler:
|
||||
or time < self.next_trigger['time']):
|
||||
if self.next_trigger['thread'] is not None:
|
||||
self.next_trigger['thread'].cancel()
|
||||
|
||||
t = time - datetime.utcnow().timestamp()
|
||||
self.next_trigger['thread'] = Timer(t, self.__trigger_reminders, (time,))
|
||||
self.next_trigger['thread'] = Timer(
|
||||
t,
|
||||
self.__trigger_reminders,
|
||||
(time,)
|
||||
)
|
||||
self.next_trigger['thread'].name = "ReminderHandler"
|
||||
self.next_trigger['thread'].start()
|
||||
self.next_trigger['time'] = time
|
||||
|
||||
@@ -143,13 +154,13 @@ reminder_handler = ReminderHandler(handler_context.app_context)
|
||||
class Reminder:
|
||||
"""Represents a reminder
|
||||
"""
|
||||
def __init__(self, reminder_id: int):
|
||||
def __init__(self, user_id: int, reminder_id: int):
|
||||
self.id = reminder_id
|
||||
|
||||
# Check if reminder exists
|
||||
if not get_db().execute(
|
||||
"SELECT 1 FROM reminders WHERE id = ? LIMIT 1",
|
||||
(self.id,)
|
||||
"SELECT 1 FROM reminders WHERE id = ? AND user_id = ? LIMIT 1",
|
||||
(self.id, user_id)
|
||||
).fetchone():
|
||||
raise ReminderNotFound
|
||||
|
||||
@@ -210,6 +221,10 @@ class Reminder:
|
||||
Returns:
|
||||
dict: The new reminder info
|
||||
"""
|
||||
logging.info(
|
||||
f'Updating notification service {self.id}: '
|
||||
+ f'{title=}, {time=}, {notification_services=}, {text=}, {repeat_quantity=}, {repeat_interval=}, {color=}'
|
||||
)
|
||||
cursor = get_db()
|
||||
|
||||
# Validate data
|
||||
@@ -302,7 +317,8 @@ class Reminder:
|
||||
|
||||
def delete(self) -> None:
|
||||
"""Delete the reminder
|
||||
"""
|
||||
"""
|
||||
logging.info(f'Deleting reminder {self.id}')
|
||||
get_db().execute("DELETE FROM reminders WHERE id = ?", (self.id,))
|
||||
reminder_handler.find_next_reminder()
|
||||
return
|
||||
@@ -311,24 +327,26 @@ class Reminders:
|
||||
"""Represents the reminder library of the user account
|
||||
"""
|
||||
sort_functions = {
|
||||
'title': (lambda r: (r['title'], r['time']), False),
|
||||
'title_reversed': (lambda r: (r['title'], r['time']), True),
|
||||
'time': (lambda r: r['time'], False),
|
||||
'time_reversed': (lambda r: r['time'], True)
|
||||
'time': (lambda r: (r['time'], r['title'], r['text'], r['color']), False),
|
||||
'time_reversed': (lambda r: (r['time'], r['title'], r['text'], r['color']), True),
|
||||
'title': (lambda r: (r['title'], r['time'], r['text'], r['color']), False),
|
||||
'title_reversed': (lambda r: (r['title'], r['time'], r['text'], r['color']), True),
|
||||
'date_added': (lambda r: r['id'], False),
|
||||
'date_added_reversed': (lambda r: r['id'], True)
|
||||
}
|
||||
|
||||
def __init__(self, user_id: int):
|
||||
self.user_id = user_id
|
||||
|
||||
def fetchall(self, sort_by: Literal["time", "time_reversed", "title", "title_reversed"] = "time") -> List[dict]:
|
||||
def fetchall(self, sort_by: Literal["time", "time_reversed", "title", "title_reversed", "date_added", "date_added_reversed"] = "time") -> List[dict]:
|
||||
"""Get all reminders
|
||||
|
||||
Args:
|
||||
sort_by (Literal["time", "time_reversed", "title", "title_reversed"], optional): How to sort the result. Defaults to "time".
|
||||
sort_by (Literal["time", "time_reversed", "title", "title_reversed", "date_added", "date_added_reversed"], optional): How to sort the result. Defaults to "time".
|
||||
|
||||
Returns:
|
||||
List[dict]: The id, title, text, time and color of each reminder
|
||||
"""
|
||||
"""
|
||||
sort_function = self.sort_functions.get(
|
||||
sort_by,
|
||||
self.sort_functions['time']
|
||||
@@ -354,11 +372,12 @@ class Reminders:
|
||||
|
||||
return reminders
|
||||
|
||||
def search(self, query: str) -> List[dict]:
|
||||
def search(self, query: str, sort_by: Literal["time", "time_reversed", "title", "title_reversed", "date_added", "date_added_reversed"] = "time") -> List[dict]:
|
||||
"""Search for reminders
|
||||
|
||||
Args:
|
||||
query (str): The term to search for
|
||||
sort_by (Literal["time", "time_reversed", "title", "title_reversed", "date_added", "date_added_reversed"], optional): How to sort the result. Defaults to "time".
|
||||
|
||||
Returns:
|
||||
List[dict]: All reminders that match. Similar output to self.fetchall
|
||||
@@ -366,7 +385,7 @@ class Reminders:
|
||||
query = query.lower()
|
||||
reminders = list(filter(
|
||||
lambda p: filter_function(query, p),
|
||||
self.fetchall()
|
||||
self.fetchall(sort_by)
|
||||
))
|
||||
return reminders
|
||||
|
||||
@@ -379,7 +398,7 @@ class Reminders:
|
||||
Returns:
|
||||
Reminder: A Reminder instance
|
||||
"""
|
||||
return Reminder(id)
|
||||
return Reminder(self.user_id, id)
|
||||
|
||||
def add(
|
||||
self,
|
||||
@@ -408,6 +427,11 @@ class Reminders:
|
||||
Returns:
|
||||
dict: The info about the reminder
|
||||
"""
|
||||
logging.info(
|
||||
f'Adding reminder with {title=}, {time=}, {notification_services=}, '
|
||||
+ f'{text=}, {repeat_quantity=}, {repeat_interval=}, {color=}'
|
||||
)
|
||||
|
||||
if time < datetime.utcnow().timestamp():
|
||||
raise InvalidTime
|
||||
time = round(time)
|
||||
@@ -418,6 +442,13 @@ class Reminders:
|
||||
raise InvalidKeyValue('repeat_interval', repeat_interval)
|
||||
|
||||
cursor = get_db()
|
||||
for service in notification_services:
|
||||
if not cursor.execute(
|
||||
"SELECT 1 FROM notification_services WHERE id = ? AND user_id = ? LIMIT 1;",
|
||||
(service, self.user_id)
|
||||
).fetchone():
|
||||
raise NotificationServiceNotFound
|
||||
|
||||
if repeat_quantity is None and repeat_interval is None:
|
||||
id = cursor.execute("""
|
||||
INSERT INTO reminders(user_id, title, text, time, color)
|
||||
@@ -444,27 +475,29 @@ class Reminders:
|
||||
# Return info
|
||||
return self.fetchone(id)
|
||||
|
||||
def test_reminder(
|
||||
title: str,
|
||||
notification_services: List[int],
|
||||
text: str = ''
|
||||
) -> None:
|
||||
"""Test send a reminder draft
|
||||
def test_reminder(
|
||||
self,
|
||||
title: str,
|
||||
notification_services: List[int],
|
||||
text: str = ''
|
||||
) -> None:
|
||||
"""Test send a reminder draft
|
||||
|
||||
Args:
|
||||
title (str): Title title of the entry
|
||||
notification_service (int): The id of the notification service to use to send the reminder
|
||||
text (str, optional): The body of the reminder. Defaults to ''.
|
||||
"""
|
||||
a = Apprise()
|
||||
cursor = get_db(dict)
|
||||
for service in notification_services:
|
||||
url = cursor.execute(
|
||||
"SELECT url FROM notification_services WHERE id = ? LIMIT 1;",
|
||||
(service,)
|
||||
).fetchone()
|
||||
if not url:
|
||||
raise NotificationServiceNotFound
|
||||
a.add(url[0])
|
||||
a.notify(title=title, body=text)
|
||||
return
|
||||
Args:
|
||||
title (str): Title title of the entry
|
||||
notification_service (int): The id of the notification service to use to send the reminder
|
||||
text (str, optional): The body of the reminder. Defaults to ''.
|
||||
"""
|
||||
logging.info(f'Testing reminder with {title=}, {notification_services=}, {text=}')
|
||||
a = Apprise()
|
||||
cursor = get_db(dict)
|
||||
for service in notification_services:
|
||||
url = cursor.execute(
|
||||
"SELECT url FROM notification_services WHERE id = ? AND user_id = ? LIMIT 1;",
|
||||
(service, self.user_id)
|
||||
).fetchone()
|
||||
if not url:
|
||||
raise NotificationServiceNotFound
|
||||
a.add(url[0])
|
||||
a.notify(title=title, body=text)
|
||||
return
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
from sqlite3 import IntegrityError
|
||||
from typing import List
|
||||
from typing import List, Literal
|
||||
|
||||
from apprise import Apprise
|
||||
|
||||
@@ -9,17 +10,21 @@ from backend.custom_exceptions import (NotificationServiceNotFound,
|
||||
ReminderNotFound)
|
||||
from backend.db import get_db
|
||||
|
||||
filter_function = lambda query, p: (
|
||||
query in p["title"].lower()
|
||||
or query in p["text"].lower()
|
||||
)
|
||||
|
||||
class StaticReminder:
|
||||
"""Represents a static reminder
|
||||
"""
|
||||
def __init__(self, reminder_id: int) -> None:
|
||||
def __init__(self, user_id: int, reminder_id: int) -> None:
|
||||
self.id = reminder_id
|
||||
|
||||
# Check if reminder exists
|
||||
if not get_db().execute(
|
||||
"SELECT 1 FROM static_reminders WHERE id = ? LIMIT 1;",
|
||||
(self.id,)
|
||||
"SELECT 1 FROM static_reminders WHERE id = ? AND user_id = ? LIMIT 1;",
|
||||
(self.id, user_id)
|
||||
).fetchone():
|
||||
raise ReminderNotFound
|
||||
|
||||
@@ -70,7 +75,12 @@ class StaticReminder:
|
||||
|
||||
Returns:
|
||||
dict: The new static reminder info
|
||||
"""
|
||||
"""
|
||||
logging.info(
|
||||
f'Updating static reminder {self.id}: '
|
||||
+ f'{title=}, {notification_services=}, {text=}, {color=}'
|
||||
)
|
||||
|
||||
# Get current data and update it with new values
|
||||
data = self.get()
|
||||
new_values = {
|
||||
@@ -115,22 +125,37 @@ class StaticReminder:
|
||||
def delete(self) -> None:
|
||||
"""Delete the static reminder
|
||||
"""
|
||||
logging.info(f'Deleting static reminder {self.id}')
|
||||
get_db().execute("DELETE FROM static_reminders WHERE id = ?", (self.id,))
|
||||
return
|
||||
|
||||
class StaticReminders:
|
||||
"""Represents the static reminder library of the user account
|
||||
"""
|
||||
|
||||
sort_functions = {
|
||||
'title': (lambda r: (r['title'], r['text'], r['color']), False),
|
||||
'title_reversed': (lambda r: (r['title'], r['text'], r['color']), True),
|
||||
'date_added': (lambda r: r['id'], False),
|
||||
'date_added_reversed': (lambda r: r['id'], True)
|
||||
}
|
||||
|
||||
def __init__(self, user_id: int) -> None:
|
||||
self.user_id = user_id
|
||||
|
||||
def fetchall(self) -> List[dict]:
|
||||
def fetchall(self, sort_by: Literal["title", "title_reversed", "date_added", "date_added_reversed"] = "title") -> List[dict]:
|
||||
"""Get all static reminders
|
||||
|
||||
Args:
|
||||
sort_by (Literal["title", "title_reversed", "date_added", "date_added_reversed"], optional): How to sort the result. Defaults to "title".
|
||||
|
||||
Returns:
|
||||
List[dict]: The id, title, text and color of each static reminder
|
||||
"""
|
||||
"""
|
||||
sort_function = self.sort_functions.get(
|
||||
sort_by,
|
||||
self.sort_functions['title']
|
||||
)
|
||||
|
||||
reminders: list = list(map(
|
||||
dict,
|
||||
get_db(dict).execute("""
|
||||
@@ -146,8 +171,28 @@ class StaticReminders:
|
||||
)
|
||||
))
|
||||
|
||||
# Sort result
|
||||
reminders.sort(key=sort_function[0], reverse=sort_function[1])
|
||||
|
||||
return reminders
|
||||
|
||||
|
||||
def search(self, query: str, sort_by: Literal["title", "title_reversed", "date_added", "date_added_reversed"] = "title") -> List[dict]:
|
||||
"""Search for static reminders
|
||||
|
||||
Args:
|
||||
query (str): The term to search for
|
||||
sort_by (Literal["title", "title_reversed", "date_added", "date_added_reversed"], optional): How to sort the result. Defaults to "title".
|
||||
|
||||
Returns:
|
||||
List[dict]: All static reminders that match. Similar output to self.fetchall
|
||||
"""
|
||||
query = query.lower()
|
||||
reminders = list(filter(
|
||||
lambda p: filter_function(query, p),
|
||||
self.fetchall(sort_by)
|
||||
))
|
||||
return reminders
|
||||
|
||||
def fetchone(self, id: int) -> StaticReminder:
|
||||
"""Get one static reminder
|
||||
|
||||
@@ -157,7 +202,7 @@ class StaticReminders:
|
||||
Returns:
|
||||
StaticReminder: A StaticReminder instance
|
||||
"""
|
||||
return StaticReminder(id)
|
||||
return StaticReminder(self.user_id, id)
|
||||
|
||||
def add(
|
||||
self,
|
||||
@@ -180,6 +225,10 @@ class StaticReminders:
|
||||
Returns:
|
||||
StaticReminder: A StaticReminder instance representing the newly created static reminder
|
||||
"""
|
||||
logging.info(
|
||||
f'Adding static reminder with {title=}, {notification_services=}, {text=}, {color=}'
|
||||
)
|
||||
|
||||
cursor = get_db()
|
||||
id = cursor.execute("""
|
||||
INSERT INTO static_reminders(user_id, title, text, color)
|
||||
@@ -207,13 +256,16 @@ class StaticReminders:
|
||||
Raises:
|
||||
ReminderNotFound: The static reminder with the given id was not found
|
||||
"""
|
||||
logging.info(f'Triggering static reminder {self.id}')
|
||||
cursor = get_db(dict)
|
||||
reminder = cursor.execute("""
|
||||
SELECT title, text
|
||||
FROM static_reminders
|
||||
WHERE id = ?
|
||||
WHERE
|
||||
id = ?
|
||||
AND user_id = ?
|
||||
LIMIT 1;
|
||||
""", (id,)).fetchone()
|
||||
""", (id, self.user_id)).fetchone()
|
||||
if not reminder:
|
||||
raise ReminderNotFound
|
||||
reminder = dict(reminder)
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
from sqlite3 import IntegrityError
|
||||
from typing import List
|
||||
from typing import List, Literal
|
||||
|
||||
from backend.custom_exceptions import (NotificationServiceNotFound,
|
||||
TemplateNotFound)
|
||||
from backend.db import get_db
|
||||
|
||||
filter_function = lambda query, p: (
|
||||
query in p["title"].lower()
|
||||
or query in p["text"].lower()
|
||||
)
|
||||
|
||||
class Template:
|
||||
"""Represents a template
|
||||
"""
|
||||
def __init__(self, template_id: int):
|
||||
def __init__(self, user_id: int, template_id: int):
|
||||
self.id = template_id
|
||||
|
||||
exists = get_db().execute(
|
||||
"SELECT 1 FROM templates WHERE id = ? LIMIT 1;",
|
||||
(self.id,)
|
||||
"SELECT 1 FROM templates WHERE id = ? AND user_id = ? LIMIT 1;",
|
||||
(self.id, user_id)
|
||||
).fetchone()
|
||||
if not exists:
|
||||
raise TemplateNotFound
|
||||
@@ -65,6 +70,11 @@ class Template:
|
||||
Returns:
|
||||
dict: The new template info
|
||||
"""
|
||||
logging.info(
|
||||
f'Updating template {self.id}: '
|
||||
+ f'{title=}, {notification_services=}, {text=}, {color=}'
|
||||
)
|
||||
|
||||
cursor = get_db()
|
||||
|
||||
data = self.get()
|
||||
@@ -107,21 +117,37 @@ class Template:
|
||||
def delete(self) -> None:
|
||||
"""Delete the template
|
||||
"""
|
||||
logging.info(f'Deleting template {self.id}')
|
||||
get_db().execute("DELETE FROM templates WHERE id = ?;", (self.id,))
|
||||
return
|
||||
|
||||
class Templates:
|
||||
"""Represents the template library of the user account
|
||||
"""
|
||||
sort_functions = {
|
||||
'title': (lambda r: (r['title'], r['text'], r['color']), False),
|
||||
'title_reversed': (lambda r: (r['title'], r['text'], r['color']), True),
|
||||
'date_added': (lambda r: r['id'], False),
|
||||
'date_added_reversed': (lambda r: r['id'], True)
|
||||
}
|
||||
|
||||
def __init__(self, user_id: int):
|
||||
self.user_id = user_id
|
||||
|
||||
def fetchall(self) -> List[dict]:
|
||||
def fetchall(self, sort_by: Literal["title", "title_reversed", "date_added", "date_added_reversed"] = "title") -> List[dict]:
|
||||
"""Get all templates
|
||||
|
||||
Args:
|
||||
sort_by (Literal["title", "title_reversed", "date_added", "date_added_reversed"], optional): How to sort the result. Defaults to "title".
|
||||
|
||||
Returns:
|
||||
List[dict]: The id, title, text and color
|
||||
"""
|
||||
sort_function = self.sort_functions.get(
|
||||
sort_by,
|
||||
self.sort_functions['title']
|
||||
)
|
||||
|
||||
templates: list = list(map(dict, get_db(dict).execute("""
|
||||
SELECT
|
||||
id,
|
||||
@@ -134,8 +160,28 @@ class Templates:
|
||||
(self.user_id,)
|
||||
)))
|
||||
|
||||
# Sort result
|
||||
templates.sort(key=sort_function[0], reverse=sort_function[1])
|
||||
|
||||
return templates
|
||||
|
||||
def search(self, query: str, sort_by: Literal["title", "title_reversed", "date_added", "date_added_reversed"] = "title") -> List[dict]:
|
||||
"""Search for templates
|
||||
|
||||
Args:
|
||||
query (str): The term to search for
|
||||
sort_by (Literal["title", "title_reversed", "date_added", "date_added_reversed"], optional): How to sort the result. Defaults to "title".
|
||||
|
||||
Returns:
|
||||
List[dict]: All templates that match. Similar output to self.fetchall
|
||||
"""
|
||||
query = query.lower()
|
||||
reminders = list(filter(
|
||||
lambda p: filter_function(query, p),
|
||||
self.fetchall(sort_by)
|
||||
))
|
||||
return reminders
|
||||
|
||||
def fetchone(self, id: int) -> Template:
|
||||
"""Get one template
|
||||
|
||||
@@ -145,7 +191,7 @@ class Templates:
|
||||
Returns:
|
||||
Template: A Template instance
|
||||
"""
|
||||
return Template(id)
|
||||
return Template(self.user_id, id)
|
||||
|
||||
def add(
|
||||
self,
|
||||
@@ -164,7 +210,11 @@ class Templates:
|
||||
|
||||
Returns:
|
||||
Template: The info about the template
|
||||
"""
|
||||
"""
|
||||
logging.info(
|
||||
f'Adding template with {title=}, {notification_services=}, {text=}, {color=}'
|
||||
)
|
||||
|
||||
cursor = get_db()
|
||||
id = cursor.execute("""
|
||||
INSERT INTO templates(user_id, title, text, color)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#-*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
from backend.custom_exceptions import (AccessUnauthorized, UsernameInvalid,
|
||||
UsernameTaken, UserNotFound)
|
||||
from backend.db import get_db
|
||||
@@ -27,7 +28,7 @@ class User:
|
||||
self.salt = result['salt']
|
||||
self.user_id = result['id']
|
||||
|
||||
# check password
|
||||
# Check password
|
||||
hash_password = get_hash(result['salt'], password)
|
||||
if not hash_password == result['hash']:
|
||||
raise AccessUnauthorized
|
||||
@@ -90,11 +91,14 @@ class User:
|
||||
"UPDATE users SET hash = ? WHERE id = ?",
|
||||
(hash_password, self.user_id)
|
||||
)
|
||||
logging.info(f'The user {self.username} ({self.user_id}) changed their password')
|
||||
return
|
||||
|
||||
def delete(self) -> None:
|
||||
"""Delete the user account
|
||||
"""
|
||||
"""
|
||||
logging.info(f'Deleting the user {self.username} ({self.user_id})')
|
||||
|
||||
cursor = get_db()
|
||||
cursor.execute("DELETE FROM reminders WHERE user_id = ?", (self.user_id,))
|
||||
cursor.execute("DELETE FROM templates WHERE user_id = ?", (self.user_id,))
|
||||
@@ -111,7 +115,8 @@ def _check_username(username: str) -> None:
|
||||
|
||||
Raises:
|
||||
UsernameInvalid: The username is not valid
|
||||
"""
|
||||
"""
|
||||
logging.debug(f'Checking the username {username}')
|
||||
if username in ONEPASS_INVALID_USERNAMES or username.isdigit():
|
||||
raise UsernameInvalid
|
||||
if list(filter(lambda c: not c in ONEPASS_USERNAME_CHARACTERS, username)):
|
||||
@@ -132,6 +137,8 @@ def register_user(username: str, password: str) -> int:
|
||||
Returns:
|
||||
user_id (int): The id of the new user. User registered successful
|
||||
"""
|
||||
logging.info(f'Registering user with username {username}')
|
||||
|
||||
# Check if username is valid
|
||||
_check_username(username)
|
||||
|
||||
@@ -156,4 +163,5 @@ def register_user(username: str, password: str) -> int:
|
||||
(username, salt, hashed_password)
|
||||
).lastrowid
|
||||
|
||||
logging.debug(f'Newly registered user has id {user_id}')
|
||||
return user_id
|
||||
|
||||
625
docs/api.md
Normal file
625
docs/api.md
Normal file
@@ -0,0 +1,625 @@
|
||||
# API
|
||||
Below is the API documentation. Report an issue on [GitHub](https://github.com/Casvt/MIND/issues).
|
||||
|
||||
All endpoints have the `/api` prefix. That means, for example, that `/auth/login` can be reached at `/api/auth/login`.
|
||||
|
||||
## Authentication
|
||||
|
||||
Authentication is done using an API key.
|
||||
To log in, make a POST request to the [`/api/auth/login`](#authlogin) endpoint.
|
||||
You'll receive an API key, which you can then use in your requests to authenticate.
|
||||
Supply it via the url parameter `api_key`.
|
||||
This API key is valid for one hour after which the key expires, any further requests return 401 'APIKeyExpired' and you are required to log in again.
|
||||
If no `api_key` is supplied or it is invalid, 401 `APIKeyInvalid` is returned.
|
||||
|
||||
For example:
|
||||
```bash
|
||||
curl -sSL 'http://192.168.2.15:8080/api/reminders?api_key=ABCDEFG'
|
||||
```
|
||||
|
||||
## Supplying data
|
||||
|
||||
Often, data needs to be supplied with a request.
|
||||
If the parameters need to be supplied via `url`, add them to the url as url parameters.
|
||||
If the parameters need to be supplied via `body`, add them to the body as a json object and supply the `Content-Type: application/json` header.
|
||||
|
||||
For example:
|
||||
```bash
|
||||
# URL parameter
|
||||
curl -sSL 'http://192.168.2.15:8080/api/reminders/search?api_key=ABCDEFG&query=Fountain&sort_by=time_reversed'
|
||||
|
||||
# Body parameter
|
||||
curl -sSLX POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"title": "Test service", "url": "test://fake/url"}' \
|
||||
'http://192.168.2.15:8080/api/notificationservices?api_key=ABCDEFG'
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
The following is automatically generated. Please report any issues on [GitHub](https://github.com/Casvt/MIND/issues).
|
||||
### `/auth/login`
|
||||
|
||||
| Requires being logged in | Description |
|
||||
| ------------------------ | ----------- |
|
||||
| No | Login to a user account |
|
||||
|
||||
??? POST
|
||||
|
||||
**Parameters (body)**
|
||||
|
||||
| Name | Required | Description | Allowed values |
|
||||
| ---- | -------- | ----------- | -------------- |
|
||||
| username | Yes | The username of the user account | N/A |
|
||||
| password | Yes | The password of the user account | N/A |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 201| N/A | Success |
|
||||
| 400 | KeyNotFound | A key was not found in the input that is required to be given |
|
||||
| 401 | AccessUnauthorized | The password given is not correct |
|
||||
| 404 | UserNotFound | The user requested can not be found |
|
||||
|
||||
### `/auth/logout`
|
||||
|
||||
| Requires being logged in | Description |
|
||||
| ------------------------ | ----------- |
|
||||
| Yes | Logout of a user account |
|
||||
|
||||
??? POST
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 201| N/A | Success |
|
||||
|
||||
### `/auth/status`
|
||||
|
||||
| Requires being logged in | Description |
|
||||
| ------------------------ | ----------- |
|
||||
| Yes | Get current status of login |
|
||||
|
||||
??? GET
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
|
||||
### `/user/add`
|
||||
|
||||
| Requires being logged in | Description |
|
||||
| ------------------------ | ----------- |
|
||||
| No | Create a new user account |
|
||||
|
||||
??? POST
|
||||
|
||||
**Parameters (body)**
|
||||
|
||||
| Name | Required | Description | Allowed values |
|
||||
| ---- | -------- | ----------- | -------------- |
|
||||
| username | Yes | The username of the user account | N/A |
|
||||
| password | Yes | The password of the user account | N/A |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 201| N/A | Success |
|
||||
| 400 | KeyNotFound | A key was not found in the input that is required to be given |
|
||||
| 400 | UsernameInvalid | The username contains invalid characters |
|
||||
| 400 | UsernameTaken | The username is already taken |
|
||||
|
||||
### `/user`
|
||||
|
||||
| Requires being logged in | Description |
|
||||
| ------------------------ | ----------- |
|
||||
| Yes | Manage a user account |
|
||||
|
||||
??? PUT
|
||||
|
||||
Change the password of the user account
|
||||
|
||||
**Parameters (body)**
|
||||
|
||||
| Name | Required | Description | Allowed values |
|
||||
| ---- | -------- | ----------- | -------------- |
|
||||
| new_password | Yes | The new password of the user account | N/A |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
| 400 | KeyNotFound | A key was not found in the input that is required to be given |
|
||||
|
||||
??? DELETE
|
||||
|
||||
Delete the user account
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
|
||||
### `/notificationservices`
|
||||
|
||||
| Requires being logged in | Description |
|
||||
| ------------------------ | ----------- |
|
||||
| Yes | Manage the notification services |
|
||||
|
||||
??? GET
|
||||
|
||||
Get a list of all notification services
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
|
||||
??? POST
|
||||
|
||||
Add a notification service
|
||||
|
||||
**Parameters (body)**
|
||||
|
||||
| Name | Required | Description | Allowed values |
|
||||
| ---- | -------- | ----------- | -------------- |
|
||||
| title | Yes | The title of the entry | N/A |
|
||||
| url | Yes | The Apprise URL of the notification service | N/A |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 201| N/A | Success |
|
||||
| 400 | InvalidKeyValue | The value of a key is invalid |
|
||||
| 400 | KeyNotFound | A key was not found in the input that is required to be given |
|
||||
|
||||
### `/notificationservices/<int:n_id>`
|
||||
|
||||
| Requires being logged in | Description |
|
||||
| ------------------------ | ----------- |
|
||||
| Yes | Manage a specific notification service |
|
||||
|
||||
Replace `<int:n_id>` with the ID of the entry. For example: `/notificationservices/2`.
|
||||
|
||||
??? GET
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
| 404 | NotificationServiceNotFound | The notification service was not found |
|
||||
|
||||
??? PUT
|
||||
|
||||
Edit the notification service
|
||||
|
||||
**Parameters (body)**
|
||||
|
||||
| Name | Required | Description | Allowed values |
|
||||
| ---- | -------- | ----------- | -------------- |
|
||||
| title | No | The title of the entry | N/A |
|
||||
| url | No | The Apprise URL of the notification service | N/A |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
| 400 | InvalidKeyValue | The value of a key is invalid |
|
||||
| 404 | NotificationServiceNotFound | The notification service was not found |
|
||||
|
||||
??? DELETE
|
||||
|
||||
Delete the notification service
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
| 404 | NotificationServiceNotFound | The notification service was not found |
|
||||
|
||||
### `/reminders`
|
||||
|
||||
| Requires being logged in | Description |
|
||||
| ------------------------ | ----------- |
|
||||
| Yes | Manage the reminders |
|
||||
|
||||
??? GET
|
||||
|
||||
Get a list of all reminders
|
||||
|
||||
**Parameters (url)**
|
||||
|
||||
| Name | Required | Description | Allowed values |
|
||||
| ---- | -------- | ----------- | -------------- |
|
||||
| sort_by | No | How to sort the result | `time`, `time_reversed`, `title`, `title_reversed`, `date_added`, `date_added_reversed` |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
| 400 | InvalidKeyValue | The value of a key is invalid |
|
||||
|
||||
??? POST
|
||||
|
||||
Add a reminder
|
||||
|
||||
**Parameters (body)**
|
||||
|
||||
| Name | Required | Description | Allowed values |
|
||||
| ---- | -------- | ----------- | -------------- |
|
||||
| title | Yes | The title of the entry | N/A |
|
||||
| time | Yes | The UTC epoch timestamp that the reminder should be sent at | N/A |
|
||||
| notification_services | Yes | Array of the id's of the notification services to use to send the notification | N/A |
|
||||
| text | No | The body of the entry | N/A |
|
||||
| repeat_quantity | No | The quantity of the repeat_interval | `years`, `months`, `weeks`, `days`, `hours`, `minutes` |
|
||||
| repeat_interval | No | The number of the interval | N/A |
|
||||
| color | No | The hex code of the color of the entry, which is shown in the web-ui | N/A |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 201| N/A | Success |
|
||||
| 400 | InvalidKeyValue | The value of a key is invalid |
|
||||
| 400 | InvalidTime | The time given is in the past |
|
||||
| 400 | KeyNotFound | A key was not found in the input that is required to be given |
|
||||
| 404 | NotificationServiceNotFound | The notification service was not found |
|
||||
|
||||
### `/reminders/search`
|
||||
|
||||
| Requires being logged in | Description |
|
||||
| ------------------------ | ----------- |
|
||||
| Yes | Search through the list of reminders |
|
||||
|
||||
??? GET
|
||||
|
||||
**Parameters (url)**
|
||||
|
||||
| Name | Required | Description | Allowed values |
|
||||
| ---- | -------- | ----------- | -------------- |
|
||||
| sort_by | No | How to sort the result | `time`, `time_reversed`, `title`, `title_reversed`, `date_added`, `date_added_reversed` |
|
||||
| query | Yes | The search term | N/A |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
| 400 | InvalidKeyValue | The value of a key is invalid |
|
||||
|
||||
### `/reminders/test`
|
||||
|
||||
| Requires being logged in | Description |
|
||||
| ------------------------ | ----------- |
|
||||
| Yes | Test send a reminder draft |
|
||||
|
||||
??? POST
|
||||
|
||||
**Parameters (body)**
|
||||
|
||||
| Name | Required | Description | Allowed values |
|
||||
| ---- | -------- | ----------- | -------------- |
|
||||
| title | Yes | The title of the entry | N/A |
|
||||
| notification_services | Yes | Array of the id's of the notification services to use to send the notification | N/A |
|
||||
| text | No | The body of the entry | N/A |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 201| N/A | Success |
|
||||
| 400 | InvalidKeyValue | The value of a key is invalid |
|
||||
| 400 | KeyNotFound | A key was not found in the input that is required to be given |
|
||||
| 404 | NotificationServiceNotFound | The notification service was not found |
|
||||
|
||||
### `/reminders/<int:r_id>`
|
||||
|
||||
| Requires being logged in | Description |
|
||||
| ------------------------ | ----------- |
|
||||
| Yes | Manage a specific reminder |
|
||||
|
||||
Replace `<int:r_id>` with the ID of the entry. For example: `/reminders/2`.
|
||||
|
||||
??? GET
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
| 404 | ReminderNotFound | The reminder with the id can not be found |
|
||||
|
||||
??? PUT
|
||||
|
||||
Edit the reminder
|
||||
|
||||
**Parameters (body)**
|
||||
|
||||
| Name | Required | Description | Allowed values |
|
||||
| ---- | -------- | ----------- | -------------- |
|
||||
| title | No | The title of the entry | N/A |
|
||||
| time | No | The UTC epoch timestamp that the reminder should be sent at | N/A |
|
||||
| notification_services | No | Array of the id's of the notification services to use to send the notification | N/A |
|
||||
| text | No | The body of the entry | N/A |
|
||||
| repeat_quantity | No | The quantity of the repeat_interval | `years`, `months`, `weeks`, `days`, `hours`, `minutes` |
|
||||
| repeat_interval | No | The number of the interval | N/A |
|
||||
| color | No | The hex code of the color of the entry, which is shown in the web-ui | N/A |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
| 400 | InvalidKeyValue | The value of a key is invalid |
|
||||
| 400 | InvalidTime | The time given is in the past |
|
||||
| 404 | NotificationServiceNotFound | The notification service was not found |
|
||||
| 404 | ReminderNotFound | The reminder with the id can not be found |
|
||||
|
||||
??? DELETE
|
||||
|
||||
Delete the reminder
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
| 404 | ReminderNotFound | The reminder with the id can not be found |
|
||||
|
||||
### `/templates`
|
||||
|
||||
| Requires being logged in | Description |
|
||||
| ------------------------ | ----------- |
|
||||
| Yes | Manage the templates |
|
||||
|
||||
??? GET
|
||||
|
||||
Get a list of all templates
|
||||
|
||||
**Parameters (url)**
|
||||
|
||||
| Name | Required | Description | Allowed values |
|
||||
| ---- | -------- | ----------- | -------------- |
|
||||
| sort_by | No | How to sort the result | `title`, `title_reversed`, `date_added`, `date_added_reversed` |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
| 400 | InvalidKeyValue | The value of a key is invalid |
|
||||
|
||||
??? POST
|
||||
|
||||
Add a template
|
||||
|
||||
**Parameters (body)**
|
||||
|
||||
| Name | Required | Description | Allowed values |
|
||||
| ---- | -------- | ----------- | -------------- |
|
||||
| title | Yes | The title of the entry | N/A |
|
||||
| notification_services | Yes | Array of the id's of the notification services to use to send the notification | N/A |
|
||||
| text | No | The body of the entry | N/A |
|
||||
| color | No | The hex code of the color of the entry, which is shown in the web-ui | N/A |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 201| N/A | Success |
|
||||
| 400 | InvalidKeyValue | The value of a key is invalid |
|
||||
| 400 | KeyNotFound | A key was not found in the input that is required to be given |
|
||||
| 404 | NotificationServiceNotFound | The notification service was not found |
|
||||
|
||||
### `/templates/search`
|
||||
|
||||
| Requires being logged in | Description |
|
||||
| ------------------------ | ----------- |
|
||||
| Yes | Search through the list of templates |
|
||||
|
||||
??? GET
|
||||
|
||||
**Parameters (url)**
|
||||
|
||||
| Name | Required | Description | Allowed values |
|
||||
| ---- | -------- | ----------- | -------------- |
|
||||
| sort_by | No | How to sort the result | `title`, `title_reversed`, `date_added`, `date_added_reversed` |
|
||||
| query | Yes | The search term | N/A |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
| 400 | InvalidKeyValue | The value of a key is invalid |
|
||||
|
||||
### `/templates/<int:t_id>`
|
||||
|
||||
| Requires being logged in | Description |
|
||||
| ------------------------ | ----------- |
|
||||
| Yes | Manage a specific template |
|
||||
|
||||
Replace `<int:t_id>` with the ID of the entry. For example: `/templates/2`.
|
||||
|
||||
??? GET
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
| 404 | TemplateNotFound | The template was not found |
|
||||
|
||||
??? PUT
|
||||
|
||||
Edit the template
|
||||
|
||||
**Parameters (body)**
|
||||
|
||||
| Name | Required | Description | Allowed values |
|
||||
| ---- | -------- | ----------- | -------------- |
|
||||
| title | No | The title of the entry | N/A |
|
||||
| notification_services | No | Array of the id's of the notification services to use to send the notification | N/A |
|
||||
| text | No | The body of the entry | N/A |
|
||||
| color | No | The hex code of the color of the entry, which is shown in the web-ui | N/A |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
| 400 | InvalidKeyValue | The value of a key is invalid |
|
||||
| 404 | NotificationServiceNotFound | The notification service was not found |
|
||||
| 404 | TemplateNotFound | The template was not found |
|
||||
|
||||
??? DELETE
|
||||
|
||||
Delete the template
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
| 404 | TemplateNotFound | The template was not found |
|
||||
|
||||
### `/staticreminders`
|
||||
|
||||
| Requires being logged in | Description |
|
||||
| ------------------------ | ----------- |
|
||||
| Yes | Manage the static reminders |
|
||||
|
||||
??? GET
|
||||
|
||||
Get a list of all static reminders
|
||||
|
||||
**Parameters (url)**
|
||||
|
||||
| Name | Required | Description | Allowed values |
|
||||
| ---- | -------- | ----------- | -------------- |
|
||||
| sort_by | No | How to sort the result | `title`, `title_reversed`, `date_added`, `date_added_reversed` |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
| 400 | InvalidKeyValue | The value of a key is invalid |
|
||||
|
||||
??? POST
|
||||
|
||||
Add a static reminder
|
||||
|
||||
**Parameters (body)**
|
||||
|
||||
| Name | Required | Description | Allowed values |
|
||||
| ---- | -------- | ----------- | -------------- |
|
||||
| title | Yes | The title of the entry | N/A |
|
||||
| notification_services | Yes | Array of the id's of the notification services to use to send the notification | N/A |
|
||||
| text | No | The body of the entry | N/A |
|
||||
| color | No | The hex code of the color of the entry, which is shown in the web-ui | N/A |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 201| N/A | Success |
|
||||
| 400 | InvalidKeyValue | The value of a key is invalid |
|
||||
| 400 | KeyNotFound | A key was not found in the input that is required to be given |
|
||||
| 404 | NotificationServiceNotFound | The notification service was not found |
|
||||
|
||||
### `/staticreminders/search`
|
||||
|
||||
| Requires being logged in | Description |
|
||||
| ------------------------ | ----------- |
|
||||
| Yes | Search through the list of staticreminders |
|
||||
|
||||
??? GET
|
||||
|
||||
**Parameters (url)**
|
||||
|
||||
| Name | Required | Description | Allowed values |
|
||||
| ---- | -------- | ----------- | -------------- |
|
||||
| sort_by | No | How to sort the result | `title`, `title_reversed`, `date_added`, `date_added_reversed` |
|
||||
| query | Yes | The search term | N/A |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
| 400 | InvalidKeyValue | The value of a key is invalid |
|
||||
|
||||
### `/staticreminders/<int:s_id>`
|
||||
|
||||
| Requires being logged in | Description |
|
||||
| ------------------------ | ----------- |
|
||||
| Yes | Manage a specific static reminder |
|
||||
|
||||
Replace `<int:s_id>` with the ID of the entry. For example: `/staticreminders/2`.
|
||||
|
||||
??? GET
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
| 404 | ReminderNotFound | The reminder with the id can not be found |
|
||||
|
||||
??? POST
|
||||
|
||||
Trigger the static reminder
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 201| N/A | Success |
|
||||
| 404 | ReminderNotFound | The reminder with the id can not be found |
|
||||
|
||||
??? PUT
|
||||
|
||||
Edit the static reminder
|
||||
|
||||
**Parameters (body)**
|
||||
|
||||
| Name | Required | Description | Allowed values |
|
||||
| ---- | -------- | ----------- | -------------- |
|
||||
| title | No | The title of the entry | N/A |
|
||||
| notification_services | No | Array of the id's of the notification services to use to send the notification | N/A |
|
||||
| text | No | The body of the entry | N/A |
|
||||
| color | No | The hex code of the color of the entry, which is shown in the web-ui | N/A |
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
| 400 | InvalidKeyValue | The value of a key is invalid |
|
||||
| 404 | NotificationServiceNotFound | The notification service was not found |
|
||||
| 404 | ReminderNotFound | The reminder with the id can not be found |
|
||||
|
||||
??? DELETE
|
||||
|
||||
Delete the static reminder
|
||||
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| 200| N/A | Success |
|
||||
| 404 | ReminderNotFound | The reminder with the id can not be found |
|
||||
|
||||
234
docs/assets/css/extra.css
Normal file
234
docs/assets/css/extra.css
Normal file
@@ -0,0 +1,234 @@
|
||||
:root {
|
||||
--color-light: #ffffff;
|
||||
--color-dim-light: #b5b5b5;
|
||||
--color-gray: #3c3c3c;
|
||||
--color-dark: #1b1b1b;
|
||||
}
|
||||
|
||||
.md-typeset code {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.md-clipboard {
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
:hover > .md-clipboard {
|
||||
color: var(--color-dark);
|
||||
}
|
||||
|
||||
.md-clipboard:hover {
|
||||
color: var(--color-light);
|
||||
}
|
||||
|
||||
.md-typeset table:not([class]) {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.md-typeset a:not(.headerlink) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* */
|
||||
/* Light mode */
|
||||
/* */
|
||||
[data-md-color-scheme="MIND"] {
|
||||
/* Background color of header */
|
||||
--md-primary-fg-color: var(--color-gray);
|
||||
/* Header and code background color and clipboard color */
|
||||
--md-default-fg-color--light: var(--color-dim-light);
|
||||
--md-primary-fg-color--lighter: #00000052;
|
||||
--md-default-fg-color--lightest: var(--color-dim-light);
|
||||
/* Color in header */
|
||||
--md-primary-bg-color: var(--color-light);
|
||||
/* Color in placeholder of search and non-hover for nav */
|
||||
--md-primary-bg-color--light: var(--color-dim-light);
|
||||
--md-primary-bg-color--lighter: #ffffff4d;
|
||||
--md-primary-bg-color--lightest: #ffffff1f;
|
||||
/* Background color */
|
||||
--md-default-bg-color: var(--color-light);
|
||||
|
||||
/* Color in search */
|
||||
--md-default-fg-color: var(--color-dark);
|
||||
|
||||
/* Hover color for mark and ToC */
|
||||
--md-accent-fg-color: var(--color-gray);
|
||||
|
||||
/* Code color */
|
||||
--md-code-fg-color: var(--color-light);
|
||||
/* Code background color */
|
||||
--md-code-bg-color: var(--color-gray);
|
||||
--md-code-hl-color: #ffff0080;
|
||||
--md-code-hl-number-color: #d52a2a;
|
||||
--md-code-hl-special-color: #db1457;
|
||||
--md-code-hl-function-color: #a846b9;
|
||||
--md-code-hl-constant-color: #6e59d9;
|
||||
--md-code-hl-keyword-color: #3f6ec6;
|
||||
--md-code-hl-string-color: #1c7d4d;
|
||||
|
||||
/* Slashes in code */
|
||||
--md-code-hl-name-color: var(--md-code-fg-color);
|
||||
--md-code-hl-operator-color: var(--color-dim-light);
|
||||
--md-code-hl-punctuation-color: var(--color-dim-light);
|
||||
--md-code-hl-comment-color: var(--color-dim-light);
|
||||
--md-code-hl-generic-color: var(--color-dim-light);
|
||||
--md-code-hl-variable-color: var(--color-dim-light);
|
||||
|
||||
/* Text color */
|
||||
--md-typeset-color: var(--color-dark);
|
||||
/* Link color */
|
||||
--md-typeset-a-color: var(--color-dim-light);
|
||||
--md-typeset-mark-color: #ffff0080;
|
||||
--md-typeset-del-color: #f5503d26;
|
||||
--md-typeset-ins-color: #0bd57026;
|
||||
--md-typeset-kbd-color: #fafafa;
|
||||
--md-typeset-kbd-accent-color: #fff;
|
||||
--md-typeset-kbd-border-color: #b8b8b8;
|
||||
--md-typeset-table-color: var(--color-gray);
|
||||
--md-typeset-table-color--light: rgba(0,0,0,.035);
|
||||
|
||||
--md-admonition-fg-color: var(--color-dark);
|
||||
--md-admonition-bg-color: var(--color-light);
|
||||
|
||||
--md-warning-fg-color: #000000de;
|
||||
--md-warning-bg-color: #ff9;
|
||||
|
||||
/* Footer color */
|
||||
--md-footer-fg-color: var(--color-light);
|
||||
/* Footer background color */
|
||||
--md-footer-bg-color: var(--color-gray);
|
||||
|
||||
--md-shadow-z1: 0 0.2rem 0.5rem #0000000d,0 0 0.05rem #0000001a;
|
||||
--md-shadow-z2: 0 0.2rem 0.5rem #0000001a,0 0 0.05rem #00000040;
|
||||
--md-shadow-z3: 0 0.2rem 0.5rem #0003,0 0 0.05rem #00000059;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="MIND"] button[data-md-component="top"],
|
||||
[data-md-color-scheme="MIND"] h1,
|
||||
[data-md-color-scheme="MIND"] [data-md-component="search"] {
|
||||
--md-default-fg-color--light: var(--color-dark);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="MIND"] [data-md-component="toc"] {
|
||||
color: var(--color-dark);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="MIND"] [data-md-component="toc"] nav {
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="MIND"] .tabbed-block {
|
||||
--md-default-fg-color--light: var(--color-dark);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="MIND"] details {
|
||||
border-color: var(--color-gray);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="MIND"] details > summary {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="MIND"] details > summary::before {
|
||||
background-color: var(--color-dark);
|
||||
}
|
||||
|
||||
/* */
|
||||
/* Dark mode */
|
||||
/* */
|
||||
[data-md-color-scheme="MIND-dark"] {
|
||||
/* Background color of header */
|
||||
--md-primary-fg-color: var(--color-gray);
|
||||
/* Header and code background color and clipboard color */
|
||||
--md-default-fg-color--light: var(--color-gray);
|
||||
--md-primary-fg-color--lighter: #00000052;
|
||||
--md-default-fg-color--lightest: var(--color-gray);
|
||||
/* Color in header */
|
||||
--md-primary-bg-color: var(--color-light);
|
||||
/* Color in placeholder of search and non-hover for nav */
|
||||
--md-primary-bg-color--light: var(--color-dim-light);
|
||||
--md-primary-bg-color--lighter: #ffffff4d;
|
||||
--md-primary-bg-color--lightest: #ffffff1f;
|
||||
/* Background color */
|
||||
--md-default-bg-color: var(--color-dark);
|
||||
|
||||
/* Color in search */
|
||||
--md-default-fg-color: var(--color-light);
|
||||
|
||||
/* Hover color for mark and ToC */
|
||||
--md-accent-fg-color: var(--color-dim-light);
|
||||
|
||||
/* Code color */
|
||||
--md-code-fg-color: var(--color-light);
|
||||
/* Code background color */
|
||||
--md-code-bg-color: var(--color-gray);
|
||||
--md-code-hl-color: #ffff0080;
|
||||
--md-code-hl-number-color: #d52a2a;
|
||||
--md-code-hl-special-color: #db1457;
|
||||
--md-code-hl-function-color: #a846b9;
|
||||
--md-code-hl-constant-color: #6e59d9;
|
||||
--md-code-hl-keyword-color: #3f6ec6;
|
||||
--md-code-hl-string-color: #1c7d4d;
|
||||
|
||||
/* Slashes in code */
|
||||
--md-code-hl-name-color: var(--md-code-fg-color);
|
||||
--md-code-hl-operator-color: var(--color-dim-light);
|
||||
--md-code-hl-punctuation-color: var(--color-dim-light);
|
||||
--md-code-hl-comment-color: var(--color-dim-light);
|
||||
--md-code-hl-generic-color: var(--color-dim-light);
|
||||
--md-code-hl-variable-color: var(--color-dim-light);
|
||||
|
||||
/* Text color */
|
||||
--md-typeset-color: var(--color-light);
|
||||
/* Link color */
|
||||
--md-typeset-a-color: var(--color-dim-light);
|
||||
--md-typeset-mark-color: #ffff0080;
|
||||
--md-typeset-del-color: #f5503d26;
|
||||
--md-typeset-ins-color: #0bd57026;
|
||||
--md-typeset-kbd-color: #fafafa;
|
||||
--md-typeset-kbd-accent-color: #fff;
|
||||
--md-typeset-kbd-border-color: #b8b8b8;
|
||||
--md-typeset-table-color: var(--color-gray);
|
||||
--md-typeset-table-color--light: rgba(0,0,0,.035);
|
||||
|
||||
--md-admonition-fg-color: var(--color-light);
|
||||
--md-admonition-bg-color: var(--color-dark);
|
||||
|
||||
--md-warning-fg-color: #000000de;
|
||||
--md-warning-bg-color: #ff9;
|
||||
|
||||
/* Footer color */
|
||||
--md-footer-fg-color: var(--color-light);
|
||||
/* Footer background color */
|
||||
--md-footer-bg-color: var(--color-gray);
|
||||
|
||||
--md-shadow-z1: 0 0.2rem 0.5rem #0000000d,0 0 0.05rem #0000001a;
|
||||
--md-shadow-z2: 0 0.2rem 0.5rem #0000001a,0 0 0.05rem #00000040;
|
||||
--md-shadow-z3: 0 0.2rem 0.5rem #0003,0 0 0.05rem #00000059;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="MIND-dark"] button[data-md-component="top"],
|
||||
[data-md-color-scheme="MIND-dark"] h1,
|
||||
[data-md-color-scheme="MIND-dark"] [data-md-component="search"] {
|
||||
--md-default-fg-color--light: var(--color-light);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="MIND-dark"] [data-md-component="toc"] nav {
|
||||
color: var(--color-dim-light);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="MIND-dark"] .tabbed-block {
|
||||
--md-default-fg-color--light: var(--color-dark);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="MIND-dark"] details {
|
||||
border-color: var(--color-gray);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="MIND-dark"] details > summary {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="MIND-dark"] details > summary::before {
|
||||
background-color: var(--color-dim-light);
|
||||
}
|
||||
30
docs/assets/img/favicon.svg
Normal file
30
docs/assets/img/favicon.svg
Normal file
@@ -0,0 +1,30 @@
|
||||
<svg viewBox="239.752 39.2 500 500" xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com">
|
||||
<defs>
|
||||
<linearGradient id="color-0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.000006, 0, 0, 0.999994, -30.815779, -277.541534)" bx:pinned="true">
|
||||
<title>White</title>
|
||||
<stop style="stop-color: rgb(255, 255, 255);"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="color-1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.000006, 0, 0, 0.999994, 19.424101, -198.084808)" bx:pinned="true">
|
||||
<title>Off-white</title>
|
||||
<stop style="stop-color: rgb(210, 210, 210)"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="color-2" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.000006, 0, 0, 0.999994, 5.575318, -62.311562)" bx:pinned="true">
|
||||
<title>Gray</title>
|
||||
<stop style="stop-color: rgb(155, 155, 155)"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="color-3" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.000006, 0, 0, 0.999994, -74.018517, 113.766891)" bx:pinned="true">
|
||||
<title>Dark gray</title>
|
||||
<stop style="stop-color: rgb(91, 91, 91)"></stop>
|
||||
</linearGradient>
|
||||
<bx:grid x="39.752" y="39.2" width="100" height="100"></bx:grid>
|
||||
</defs>
|
||||
<g transform="matrix(2.413543, 0, 0, 2.192117, -660.809998, -485.15448)" style="">
|
||||
<ellipse style="fill: rgb(91, 91, 91); stroke: url(#color-3);" cx="519.4" cy="257.1" rx="64" ry="64" transform="matrix(0, 1, -1, 0, 773.392883, -159.637634)"></ellipse>
|
||||
<ellipse style="fill: url(#color-2); stroke: url(#color-2);" cx="455.445" cy="193.1" rx="64.045" ry="64" transform="matrix(0, 1, -1, 0, 668.85199, -52.200024)"></ellipse>
|
||||
<ellipse style="stroke: url(#color-1); fill: url(#color-1);" cx="391.445" cy="129.1" rx="64" ry="64" transform="matrix(0, 1, -1, 0, 604.85199, -88.244972)"></ellipse>
|
||||
<g transform="matrix(1, 0, 0, 1, -43.907719, -71.28595)">
|
||||
<ellipse style="stroke: url(#color-0); fill: url(#color-0);" cx="327.402" cy="65.104" rx="64.045" ry="64" transform="matrix(0, 1, -1, 0, 546.14032, 103.69133)"></ellipse>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: 'Russo One'; font-size: 148.922px; font-weight: 700; line-height: 238.276px; white-space: pre;" transform="matrix(0.4969, 0, 0, 0.496903, 445.627899, 461.735657)"><tspan>M</tspan><tspan x="0" dy="1em"></tspan></text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
23
docs/backup_your_data.md
Normal file
23
docs/backup_your_data.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Backup your data
|
||||
|
||||
It's possible, and very easy, to backup all MIND data. This way, you can recover data if something goes wrong, port your instance over to an other computer or run redundant instances.
|
||||
|
||||
## Backing up the data
|
||||
|
||||
It's as simple as making a copy of the database file and storing it somewhere safe.
|
||||
|
||||
1. Stop MIND, if it's still running.
|
||||
2. Go inside the docker volume of the container (most likely `mind-db`) or the mapped folder.
|
||||
3. Inside the volume/folder, you'll find the `MIND.db` file. Make a copy of this. That's all you need.
|
||||
4. You can now start the instance back up.
|
||||
|
||||
The database file contains all data and is the only thing needed to keep a complete backup of your MIND instance.
|
||||
|
||||
## Recovering the data
|
||||
|
||||
It's as simple as putting the database file in the database folder and restarting the instance.
|
||||
|
||||
1. Stop MIND, if it's still running.
|
||||
2. Go inside the docker volume of the container (most likely `mind-db`) or the mapped folder.
|
||||
3. Inside the volume/folder, place the database file that you backed up.
|
||||
4. You can now start the instance back up. Everything should be recovered.
|
||||
38
docs/index.md
Normal file
38
docs/index.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# MIND Docs
|
||||
|
||||
[](https://hub.docker.com/r/mrcas/mind)
|
||||
[](https://github.com/Casvt/MIND/releases)
|
||||
|
||||
__A simple self hosted reminder application that can send push notifications to your device. Set the reminder and forget about it!__
|
||||
|
||||
Getting started:
|
||||
|
||||
- [Installation](./installation.md)
|
||||
- [Setup After Installation](./setup_after_installation.md)
|
||||
|
||||
General Information:
|
||||
|
||||
- [API Docs](./api.md)
|
||||
|
||||
## Workings
|
||||
|
||||
MIND can be used for sending notifications at the desired time. This can be a set time, like a yearly reminder for a birthday, or at a button click, to easily send a predefined notification when you want to.
|
||||
|
||||
## Features
|
||||
|
||||
- Works cross-timezone
|
||||
- Notifications are sent with second-precision
|
||||
- Uses the [apprise library](https://github.com/caronc/apprise), giving you 80+ platforms to send notifications to
|
||||
- Easily manage the reminders with sorting options, search ability and color coding
|
||||
- Docker image available
|
||||
- Mobile friendly web-interface
|
||||
- API available
|
||||
|
||||
### Planned Features
|
||||
|
||||
You can see the planned features in the [Project board](https://github.com/users/Casvt/projects/3).
|
||||
|
||||
## Contact
|
||||
|
||||
- For support, a [discord server](https://discord.gg/nMNdgG7vsE) is available
|
||||
- Alternatively, [make an issue](https://github.com/Casvt/MIND/issues)
|
||||
97
docs/installation.md
Normal file
97
docs/installation.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Installation
|
||||
|
||||
The recommended way to install MIND is using Docker. After installing MIND, it is advised to read the [Setup After Installation page](setup_after_installation.md).
|
||||
|
||||
NOTE: Make sure to set all time related settings (time, date, timezone, etc.) correct on your computer, as MIND depends on it to work correctly.
|
||||
|
||||
## Docker
|
||||
|
||||
### Database location
|
||||
|
||||
We first need to create a named volume, or a folder, to store the database file of MIND in.
|
||||
|
||||
=== "Docker CLI"
|
||||
```bash
|
||||
docker volume create mind-db
|
||||
```
|
||||
|
||||
=== "Portainer"
|
||||
- Open `Volumes`
|
||||
- Click `Add Volume`
|
||||
- Enter name matching the one you'll use in compose (`mind-db`, in the above provided command)
|
||||
- Click `Create the volume`
|
||||
- Open `Stacks`
|
||||
- Create the stack with the named volume in it.
|
||||
|
||||
=== "Folder"
|
||||
Linux standards suggest to put the database in `/opt/application_name`, as the `/opt` directory is where program options should be stored. In this case, you'd create the desired folder using the following command:
|
||||
```bash
|
||||
mkdir /opt/MIND/db
|
||||
```
|
||||
|
||||
### Run the container
|
||||
|
||||
Now that we can store the database somewhere, we can get the container running.
|
||||
|
||||
=== "Docker CLI"
|
||||
The command to get the docker container running can be found below. Replace the timezone value (`TZ=`) with the [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) of your timezone (value of `TZ identifier` on webpage).
|
||||
```bash
|
||||
docker run -d \
|
||||
--name mind \
|
||||
-v mind-db:/app/db \
|
||||
-e TZ=Europe/Amsterdam \
|
||||
-p 8080:8080 \
|
||||
mrcas/mind:latest
|
||||
```
|
||||
|
||||
=== "Docker Compose"
|
||||
The contents of the `docker-compose.yml` file would look like below. Replace the timezone value (`TZ=`) with the [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) of your timezone (value of `TZ identifier` on webpage).
|
||||
```yml
|
||||
version: '3.3'
|
||||
services:
|
||||
mind:
|
||||
container_name: mind
|
||||
volumes:
|
||||
- 'mind-db:/app/db'
|
||||
environment:
|
||||
- TZ=Europe/Amsterdam
|
||||
ports:
|
||||
- '8080:8080'
|
||||
image: 'mrcas/mind:latest'
|
||||
```
|
||||
Now run the compose by running the following command in the root folder:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
If you didn't name your docker volume `mind-db` (see [Database location](#database-location)), replace `mind-db` in the command with the name of your volume. If you created a folder, replace `mind-db` with `/opt/MIND/db` or the folder you want.
|
||||
|
||||
Information on how to change the port can be found on the [Setup After Installation page](setup_after_installation.md#port).
|
||||
|
||||
## Manual Install
|
||||
|
||||
See below for installation instructions for your OS if you want to install it manually.
|
||||
|
||||
=== "Linux / MacOS"
|
||||
```bash
|
||||
sudo apt-get install git python3-pip
|
||||
sudo git clone https://github.com/Casvt/MIND.git /opt/MIND
|
||||
cd /opt/MIND
|
||||
python3 -m pip install -r requirements.txt
|
||||
python3 MIND.py
|
||||
```
|
||||
|
||||
=== "Windows"
|
||||
1. Install python [in the Microsoft Store](https://www.microsoft.com/store/productId/9PJPW5LDXLZ5)
|
||||
2. Install pip [using these instructions](https://www.liquidweb.com/kb/install-pip-windows/)
|
||||
3. Download [the latest release](https://github.com/Casvt/MIND/zipball/master)
|
||||
4. Extract the ZIP file
|
||||
5. With the folder open, right click and select `Open in Terminal`
|
||||
6. Type the following command:
|
||||
```bash
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
7. Type the following command:
|
||||
```bash
|
||||
python MIND.py
|
||||
```
|
||||
34
docs/setup_after_installation.md
Normal file
34
docs/setup_after_installation.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Setup After Installation
|
||||
|
||||
After installing MIND, you should have access to the web-ui. MIND needs some configuration in order for it to work properly.
|
||||
|
||||
## Port
|
||||
|
||||
The first thing to do is decide if you want to leave MIND running on the default port of `8080`. If you _do_, you can go to the next step. If you want to _change_ the port, continue reading.
|
||||
|
||||
=== "Docker CLI"
|
||||
Alter the command to run the container and replace `-p 8080:8080` with `-p {PORT}:8080`, where `{PORT}` is the desired port (e.g. `-p 8009:8080`). Then run the container with the new version of the command.
|
||||
|
||||
=== "Docker Compose"
|
||||
Alter the file to run the container and replace `- 8080:8080` with `- {PORT}:8080`, where `{PORT}` is the desired port (e.g. `- 8009:8080`). Then run the container with the new version of the file.
|
||||
|
||||
=== "Manual Install"
|
||||
Inside the `MIND.py` file at the top, you can set the port via the `PORT` variable. Change it from `PORT = '8080'` to `PORT = '{PORT}'`, where `{PORT}` is the desired port (e.g. `PORT = '8009'`). Then restart the application.
|
||||
|
||||
## Base URL
|
||||
|
||||
If you want to set a base url (e.g. for a reverse proxy), go inside the `MIND.py` file and at the top, you can set the base URL via the `URL_PREFIX` variable. Change it from `URL_PREFIX = ''` to `URL_PREFIX = '/{PREFIX}'`, where `{PREFIX}` is the desired URL prefix (e.g. `URL_PREFIX = '/mind'`). Then restart the application.
|
||||
|
||||
## Creating an account
|
||||
|
||||
When accessing the web-ui, you'll be prompted to log in. Click on `Or create an account`, enter the desired username and password for the account and click `Create`. The account is created and can now be logged in with. The complete authentication process is local and no data is shared with any other service.
|
||||
|
||||
## Set your locale
|
||||
|
||||
In the settings, you can change your locale, so that the dates and times are displayed in the format used by your country.
|
||||
|
||||
## Add a notification service
|
||||
|
||||
A notification service is a way of sending a notification. For example an e-mail to a group of people or a PushBullet notification to a specific device. What the actual content of the notification is, is decided by the title and text of the reminder. The notification service only specifies in which way the title and text is sent. You set it up once, and then you can select it when creating a reminder.
|
||||
|
||||
Go to the "Notification Services" tab in the web-ui and click the `+` button. A notification service consists of a title (name) and an Apprise URL. See the [Apprise URL documentation](https://github.com/caronc/apprise#supported-notifications) to learn how to make a valid Apprise URL.
|
||||
1206
frontend/api.py
1206
frontend/api.py
File diff suppressed because it is too large
Load Diff
@@ -155,8 +155,9 @@ header > div {
|
||||
width: var(--height);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(1.3rem, 7vw, 2rem);
|
||||
header img {
|
||||
height: 3rem;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
/* */
|
||||
@@ -271,12 +272,12 @@ nav > div > button svg {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.window-container > div:not(#home) > p {
|
||||
text-align: center;
|
||||
.window-container > div:not(#home) > h2:not(:first-of-type) {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.window-container > div:not(#home) > p > a {
|
||||
color: var(--color-light);
|
||||
.window-container > div:not(#home) > p {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tab-selector {
|
||||
@@ -286,7 +287,8 @@ nav > div > button svg {
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
|
||||
|
||||
padding-inline: .5rem;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
|
||||
.notification-service-list > div > input {
|
||||
width: fit-content;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.repeat-bar,
|
||||
|
||||
@@ -45,10 +45,16 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#sort-input {
|
||||
width: min-content;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* REMINDER LIST */
|
||||
#reminder-list,
|
||||
#static-reminder-list,
|
||||
#template-list {
|
||||
#reminder-tab,
|
||||
#static-reminder-tab,
|
||||
#template-tab {
|
||||
--gap: 1rem;
|
||||
--entry-width: 13rem;
|
||||
max-width: 43rem;
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
box-shadow: var(--default-shadow);
|
||||
}
|
||||
|
||||
.settings-container input,
|
||||
.settings-container textarea,
|
||||
.settings-container select {
|
||||
max-width: 20rem;
|
||||
}
|
||||
|
||||
#change-password-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -33,10 +39,32 @@
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
#change-password-form > input {
|
||||
max-width: 20rem;
|
||||
}
|
||||
|
||||
#delete-account-button {
|
||||
background-color: var(--color-error);
|
||||
}
|
||||
|
||||
.contact-list {
|
||||
width: min(100%, 31rem);
|
||||
margin: auto;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.contact-list > a {
|
||||
height: 5rem;
|
||||
width: 15rem;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
border-radius: 6px;
|
||||
background-color: var(--color-gray);
|
||||
color: var(--color-light);
|
||||
|
||||
text-decoration: none;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
30
frontend/static/img/favicon.svg
Normal file
30
frontend/static/img/favicon.svg
Normal file
@@ -0,0 +1,30 @@
|
||||
<svg viewBox="239.752 39.2 500 500" xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com">
|
||||
<defs>
|
||||
<linearGradient id="color-0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.000006, 0, 0, 0.999994, -30.815779, -277.541534)" bx:pinned="true">
|
||||
<title>White</title>
|
||||
<stop style="stop-color: rgb(255, 255, 255);"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="color-1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.000006, 0, 0, 0.999994, 19.424101, -198.084808)" bx:pinned="true">
|
||||
<title>Off-white</title>
|
||||
<stop style="stop-color: rgb(210, 210, 210)"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="color-2" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.000006, 0, 0, 0.999994, 5.575318, -62.311562)" bx:pinned="true">
|
||||
<title>Gray</title>
|
||||
<stop style="stop-color: rgb(155, 155, 155)"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="color-3" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.000006, 0, 0, 0.999994, -74.018517, 113.766891)" bx:pinned="true">
|
||||
<title>Dark gray</title>
|
||||
<stop style="stop-color: rgb(91, 91, 91)"></stop>
|
||||
</linearGradient>
|
||||
<bx:grid x="39.752" y="39.2" width="100" height="100"></bx:grid>
|
||||
</defs>
|
||||
<g transform="matrix(2.413543, 0, 0, 2.192117, -660.809998, -485.15448)" style="">
|
||||
<ellipse style="fill: rgb(91, 91, 91); stroke: url(#color-3);" cx="519.4" cy="257.1" rx="64" ry="64" transform="matrix(0, 1, -1, 0, 773.392883, -159.637634)"></ellipse>
|
||||
<ellipse style="fill: url(#color-2); stroke: url(#color-2);" cx="455.445" cy="193.1" rx="64.045" ry="64" transform="matrix(0, 1, -1, 0, 668.85199, -52.200024)"></ellipse>
|
||||
<ellipse style="stroke: url(#color-1); fill: url(#color-1);" cx="391.445" cy="129.1" rx="64" ry="64" transform="matrix(0, 1, -1, 0, 604.85199, -88.244972)"></ellipse>
|
||||
<g transform="matrix(1, 0, 0, 1, -43.907719, -71.28595)">
|
||||
<ellipse style="stroke: url(#color-0); fill: url(#color-0);" cx="327.402" cy="65.104" rx="64.045" ry="64" transform="matrix(0, 1, -1, 0, 546.14032, 103.69133)"></ellipse>
|
||||
<text style="fill: rgb(51, 51, 51); font-family: 'Russo One'; font-size: 148.922px; font-weight: 700; line-height: 238.276px; white-space: pre;" transform="matrix(0.4969, 0, 0, 0.496903, 445.627899, 461.735657)"><tspan>M</tspan><tspan x="0" dy="1em"></tspan></text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -1,7 +1,7 @@
|
||||
const types = {
|
||||
'reminder': document.getElementById('reminder-list'),
|
||||
'static_reminder': document.getElementById('static-reminder-list'),
|
||||
'template': document.getElementById('template-list')
|
||||
'reminder': document.getElementById('reminder-tab'),
|
||||
'static_reminder': document.getElementById('static-reminder-tab'),
|
||||
'template': document.getElementById('template-tab')
|
||||
};
|
||||
|
||||
const icons = {
|
||||
@@ -47,15 +47,20 @@ function logout() {
|
||||
'method': 'POST'
|
||||
})
|
||||
.then(response => {
|
||||
localStorage.removeItem('MIND_api_key');
|
||||
const new_stor = JSON.parse(localStorage.getItem('MIND'))
|
||||
new_stor.api_key = null
|
||||
localStorage.setItem('MIND', JSON.stringify(new_stor));
|
||||
window.location.href = `${url_prefix}/`;
|
||||
});
|
||||
};
|
||||
|
||||
// code run on load
|
||||
|
||||
if (localStorage.getItem('MIND') === null)
|
||||
localStorage.setItem('MIND', JSON.stringify({'api_key': null, 'locale': 'en-GB'}))
|
||||
|
||||
const url_prefix = document.getElementById('url_prefix').dataset.value;
|
||||
const api_key = localStorage.getItem('MIND_api_key');
|
||||
const api_key = JSON.parse(localStorage.getItem('MIND')).api_key;
|
||||
if (api_key === null) {
|
||||
window.location.href = `${url_prefix}/`;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
const sorting_options = {};
|
||||
sorting_options[types.reminder.id] = [
|
||||
['time', 'Time'],
|
||||
['time_reversed', 'Time Reversed'],
|
||||
['title', 'Title'],
|
||||
['title_reversed', 'Title Reversed'],
|
||||
['date_added', 'Date Added'],
|
||||
['date_added_reversed', 'Date Added Reversed']
|
||||
];
|
||||
sorting_options[types.static_reminder.id] = [
|
||||
['title', 'Title'],
|
||||
['title_reversed', 'Title Reversed'],
|
||||
['date_added', 'Date Added'],
|
||||
['date_added_reversed', 'Date Added Reversed']
|
||||
];
|
||||
sorting_options[types.template.id] = [
|
||||
['title', 'Title'],
|
||||
['title_reversed', 'Title Reversed'],
|
||||
['date_added', 'Date Added'],
|
||||
['date_added_reversed', 'Date Added Reversed']
|
||||
];
|
||||
|
||||
function showTab(button) {
|
||||
// Apply styling to selected button
|
||||
document.querySelectorAll('.tab-selector > button').forEach(
|
||||
@@ -5,10 +27,13 @@ function showTab(button) {
|
||||
);
|
||||
|
||||
// Show desired tab and hide all others
|
||||
document.querySelectorAll('#home > div:not(.tab-selector)').forEach(
|
||||
document.querySelectorAll('#home > div:not(.tab-selector):not(.search-container)').forEach(
|
||||
e => e.classList.add('hidden')
|
||||
);
|
||||
document.getElementById(button.dataset.target).classList.remove('hidden');
|
||||
|
||||
fillSortOptions();
|
||||
document.querySelector('#search-input').value = '';
|
||||
};
|
||||
|
||||
//
|
||||
@@ -33,7 +58,7 @@ function fillTable(table, results) {
|
||||
const time = document.createElement('p');
|
||||
var offset = new Date(r.time * 1000).getTimezoneOffset() * -60;
|
||||
var d = new Date((r.time + offset) * 1000);
|
||||
var formatted_date = d.toLocaleString('en-CA').slice(0,10) + ' ' + d.toTimeString().slice(0,5);
|
||||
var formatted_date = d.toLocaleString(JSON.parse(localStorage.getItem('MIND')).locale);
|
||||
if (r.repeat_interval !== null) {
|
||||
if (r.repeat_interval === 1) {
|
||||
var quantity = r.repeat_quantity.endsWith('s') ? r.repeat_quantity.slice(0, -1) : r.repeat_quantity;
|
||||
@@ -72,28 +97,110 @@ function fillLibrary(url, type) {
|
||||
};
|
||||
|
||||
function fillReminders() {
|
||||
fillLibrary(`/api/reminders?api_key=${api_key}`, types.reminder);
|
||||
const sorting = document.querySelector('#sort-input').value;
|
||||
fillLibrary(`/api/reminders?api_key=${api_key}&sort_by=${sorting}`, types.reminder);
|
||||
};
|
||||
|
||||
function fillStaticReminders() {
|
||||
fillLibrary(`/api/staticreminders?api_key=${api_key}`, types.static_reminder);
|
||||
function fillStaticReminders(assume_sorting=false) {
|
||||
let sorting;
|
||||
if (assume_sorting)
|
||||
sorting = sorting_options[types.static_reminder.id][0][0];
|
||||
else
|
||||
sorting = document.querySelector('#sort-input').value;
|
||||
fillLibrary(`/api/staticreminders?api_key=${api_key}&sort_by=${sorting}`, types.static_reminder);
|
||||
}
|
||||
|
||||
function fillTemplates() {
|
||||
fillLibrary(`/api/templates?api_key=${api_key}`, types.template);
|
||||
function fillTemplates(assume_sorting=false) {
|
||||
let sorting;
|
||||
if (assume_sorting)
|
||||
sorting = sorting_options[types.template.id][0][0];
|
||||
else
|
||||
sorting = document.querySelector('#sort-input').value;
|
||||
fillLibrary(`/api/templates?api_key=${api_key}&sort_by=${sorting}`, types.template);
|
||||
};
|
||||
|
||||
//
|
||||
// Library search
|
||||
//
|
||||
function searchLibrary() {
|
||||
const query = document.querySelector('#search-input').value,
|
||||
tab = document.getElementById(
|
||||
document.querySelector('.tab-selector > button[data-selected="true"]').dataset.target
|
||||
)
|
||||
const sorting = document.querySelector('#sort-input').value;
|
||||
let url;
|
||||
if (tab === types.reminder)
|
||||
url = `${url_prefix}/api/reminders/search?api_key=${api_key}&query=${query}&sort_by=${sorting}`;
|
||||
else if (tab === types.static_reminder)
|
||||
url = `${url_prefix}/api/staticreminders/search?api_key=${api_key}&query=${query}&sort_by=${sorting}`;
|
||||
else if (tab === types.template)
|
||||
url = `${url_prefix}/api/templates/search?api_key=${api_key}&query=${query}&sort_by=${sorting}`;
|
||||
else return;
|
||||
|
||||
fillLibrary(url, tab);
|
||||
};
|
||||
|
||||
function clearSearchLibrary() {
|
||||
document.querySelector('#search-input').value = '';
|
||||
const tab = document.getElementById(
|
||||
document.querySelector('.tab-selector > button[data-selected="true"]').dataset.target
|
||||
)
|
||||
if (tab === types.reminder)
|
||||
fillReminders();
|
||||
else if (tab === types.static_reminder)
|
||||
fillStaticReminders();
|
||||
else if (tab === types.template)
|
||||
fillTemplates();
|
||||
else return;
|
||||
};
|
||||
|
||||
//
|
||||
// Library sort
|
||||
//
|
||||
function fillSortOptions() {
|
||||
const tab = document.getElementById(
|
||||
document.querySelector('.tab-selector > button[data-selected="true"]').dataset.target
|
||||
)
|
||||
const sort_options = sorting_options[tab.id];
|
||||
|
||||
const select = document.getElementById('sort-input');
|
||||
select.innerHTML = '';
|
||||
sort_options.forEach(o => {
|
||||
const entry = document.createElement('option');
|
||||
entry.value = o[0]
|
||||
entry.innerText = o[1]
|
||||
select.appendChild(entry);
|
||||
});
|
||||
select.querySelector(':first-child').setAttribute('selected', '');
|
||||
};
|
||||
|
||||
function applySorting() {
|
||||
const query = document.querySelector('#search-input').value;
|
||||
fetch(`${url_prefix}/api/reminders/search?api_key=${api_key}&query=${query}`)
|
||||
if (query !== '') {
|
||||
searchLibrary();
|
||||
return;
|
||||
};
|
||||
|
||||
const sorting = document.getElementById('sort-input').value,
|
||||
tab = document.getElementById(
|
||||
document.querySelector('.tab-selector > button[data-selected="true"]').dataset.target
|
||||
)
|
||||
|
||||
let url;
|
||||
if (tab === types.reminder)
|
||||
url = `${url_prefix}/api/reminders?api_key=${api_key}&sort_by=${sorting}`;
|
||||
else if (tab === types.static_reminder)
|
||||
url = `${url_prefix}/api/staticreminders?api_key=${api_key}&sort_by=${sorting}`;
|
||||
else if (tab === types.template)
|
||||
url = `${url_prefix}/api/templates?api_key=${api_key}&sort_by=${sorting}`;
|
||||
else return;
|
||||
|
||||
fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) return Promise.reject(response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(json => fillTable(types.reminder, json.result))
|
||||
.then(json => fillTable(tab, json.result))
|
||||
.catch(e => {
|
||||
if (e === 401)
|
||||
window.location.href = `${url_prefix}/`;
|
||||
@@ -102,21 +209,18 @@ function searchLibrary() {
|
||||
});
|
||||
};
|
||||
|
||||
function clearSearchLibrary() {
|
||||
document.querySelector('#search-input').value = '';
|
||||
fillReminders();
|
||||
};
|
||||
|
||||
// code run on load
|
||||
|
||||
document.querySelectorAll('.tab-selector > button').forEach(b => {
|
||||
b.addEventListener('click', e => showTab(b));
|
||||
});
|
||||
|
||||
fillSortOptions();
|
||||
fillReminders();
|
||||
fillStaticReminders();
|
||||
fillTemplates();
|
||||
fillStaticReminders(assume_sorting=true);
|
||||
fillTemplates(assume_sorting=true);
|
||||
setInterval(fillReminders, 60000);
|
||||
|
||||
document.querySelector('#search-form').setAttribute('action', 'javascript:searchLibrary();');
|
||||
document.querySelector('#clear-button').addEventListener('click', e => clearSearchLibrary());
|
||||
document.querySelector('#sort-input').addEventListener('change', e => applySorting());
|
||||
|
||||
@@ -44,7 +44,9 @@ function login(data=null) {
|
||||
return response.json();
|
||||
})
|
||||
.then(json => {
|
||||
localStorage.setItem('MIND_api_key', json.result.api_key);
|
||||
const new_stor = JSON.parse(localStorage.getItem('MIND'));
|
||||
new_stor.api_key = json.result.api_key;
|
||||
localStorage.setItem('MIND', JSON.stringify(new_stor));
|
||||
window.location.href = `${url_prefix}/reminders`;
|
||||
})
|
||||
.catch(e => {
|
||||
@@ -93,7 +95,7 @@ function create() {
|
||||
};
|
||||
|
||||
function checkLogin() {
|
||||
fetch(`${url_prefix}/api/auth/status?api_key=${localStorage.getItem('MIND_api_key')}`)
|
||||
fetch(`${url_prefix}/api/auth/status?api_key=${JSON.parse(localStorage.getItem('MIND')).api_key}`)
|
||||
.then(response => {
|
||||
if (!response.ok) return Promise.reject(response.status);
|
||||
window.location.href = '/reminders';
|
||||
@@ -108,6 +110,9 @@ function checkLogin() {
|
||||
|
||||
// code run on load
|
||||
|
||||
if (localStorage.getItem('MIND') === null)
|
||||
localStorage.setItem('MIND', JSON.stringify({'api_key': null, 'locale': 'en-GB'}))
|
||||
|
||||
const url_prefix = document.getElementById('url_prefix').dataset.value;
|
||||
|
||||
checkLogin();
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
const inputs_buttons = {
|
||||
'save_button': document.querySelector('#add-row button[data-type="save"]'),
|
||||
'title': document.querySelector('#add-row td.title-column input'),
|
||||
'url': document.querySelector('#add-row td.url-column input')
|
||||
};
|
||||
|
||||
function fillNotificationSelection() {
|
||||
fetch(`${url_prefix}/api/notificationservices?api_key=${api_key}`)
|
||||
.then(response => {
|
||||
@@ -33,6 +27,7 @@ function fillNotificationSelection() {
|
||||
|
||||
const table = document.getElementById('services-list');
|
||||
table.querySelectorAll('tr:not(#add-row)').forEach(e => e.remove());
|
||||
// table.innerHTML = '';
|
||||
json.result.forEach(service => {
|
||||
const entry = document.createElement('tr');
|
||||
entry.dataset.id = service.id;
|
||||
@@ -158,13 +153,165 @@ function deleteService(id) {
|
||||
|
||||
function toggleAddService() {
|
||||
document.getElementById('add-row').classList.toggle('hidden');
|
||||
return;
|
||||
|
||||
const cont = document.querySelector('.overflow-container');
|
||||
if (cont.classList.contains('show-add')) {
|
||||
// Hide add
|
||||
cont.classList.remove('show-add');
|
||||
hideAddServiceWindow();
|
||||
} else {
|
||||
// Show add
|
||||
if (notification_services === null) {
|
||||
fetch(`${url_prefix}/api/notificationservices/available?api_key=${api_key}`)
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
notification_services = json.result;
|
||||
const table = document.querySelector('#service-list');
|
||||
json.result.forEach((result, index) => {
|
||||
const entry = document.createElement('button');
|
||||
entry.innerText = result.name;
|
||||
entry.addEventListener('click', e => showAddServiceWindow(index));
|
||||
table.appendChild(entry);
|
||||
});
|
||||
});
|
||||
};
|
||||
cont.classList.add('show-add');
|
||||
};
|
||||
};
|
||||
|
||||
function showAddServiceWindow(index) {
|
||||
const window = document.getElementById('add-service-window');
|
||||
window.innerHTML = '';
|
||||
|
||||
if (index === -1) {
|
||||
// Custom url
|
||||
const title = document.createElement('h3');
|
||||
title.innerText = 'Custom URL';
|
||||
window.appendChild(title);
|
||||
|
||||
const desc = document.createElement('p');
|
||||
desc.innerHTML = 'Enter a custom Apprise URL. See the <a target="_blank" href="https://github.com/caronc/apprise#supported-notifications">Apprise URL documentation</a>.';
|
||||
window.appendChild(desc);
|
||||
|
||||
const service_title = document.createElement('input');
|
||||
service_title.id = 'service-title';
|
||||
service_title.type = 'text';
|
||||
service_title.placeholder = 'Service Title';
|
||||
service_title.required = true;
|
||||
window.appendChild(service_title);
|
||||
|
||||
const url_input = document.createElement('input');
|
||||
url_input.type = 'text';
|
||||
url_input.placeholder = 'Apprise URL';
|
||||
window.appendChild(url_input);
|
||||
} else {
|
||||
const data = notification_services[index];
|
||||
console.log(data);
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.innerText = data.name;
|
||||
window.appendChild(title);
|
||||
|
||||
const docs = document.createElement('a');
|
||||
docs.href = data.doc_url;
|
||||
docs.target = '_blank';
|
||||
docs.innerText = 'Documentation';
|
||||
window.appendChild(docs);
|
||||
|
||||
const service_title = document.createElement('input');
|
||||
service_title.id = 'service-title';
|
||||
service_title.type = 'text';
|
||||
service_title.placeholder = 'Service Title';
|
||||
service_title.required = true;
|
||||
window.appendChild(service_title);
|
||||
|
||||
data.details.tokens.forEach(token => {
|
||||
if (token.type === 'choice') {
|
||||
const choice = document.createElement('select');
|
||||
choice.dataset.map = token.map_to;
|
||||
choice.dataset.prefix = '';
|
||||
choice.placeholder = token.name;
|
||||
choice.required = token.required;
|
||||
token.options.forEach(option => {
|
||||
const entry = document.createElement('option');
|
||||
entry.value = option;
|
||||
entry.innerText = option;
|
||||
choice.appendChild(entry);
|
||||
});
|
||||
window.appendChild(choice);
|
||||
|
||||
} else if (token.type === 'list') {
|
||||
if (token.content.length === 0) {
|
||||
|
||||
} else {
|
||||
token.content.forEach(content => {
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
} else if (token.type === 'string') {
|
||||
const str_input = document.createElement('input');
|
||||
str_input.dataset.map = token.map_to;
|
||||
str_input.dataset.prefix = token.prefix;
|
||||
str_input.dataset.regex = token.regex;
|
||||
str_input.type = 'text';
|
||||
str_input.placeholder = `${token.name}${!token.required ? ' (Optional)' : ''}`;
|
||||
str_input.required = token.required;
|
||||
window.appendChild(str_input);
|
||||
|
||||
} else if (token.type === 'int') {
|
||||
const int_input = document.createElement('input');
|
||||
int_input.dataset.map = token.map_to;
|
||||
int_input.dataset.prefix = token.prefix;
|
||||
int_input.type = 'number';
|
||||
int_input.placeholder = `${token.name}${!token.required ? ' (Optional)' : ''}`;
|
||||
int_input.required = token.required;
|
||||
if (token.min !== null)
|
||||
int_input.min = token.min;
|
||||
if (token.max !== null)
|
||||
int_input.max = token.max;
|
||||
window.appendChild(int_input);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Bottom options
|
||||
const options = document.createElement('div');
|
||||
options.classList.add('options');
|
||||
const cancel = document.createElement('button');
|
||||
cancel.type = 'button';
|
||||
cancel.innerText = 'Cancel';
|
||||
cancel.addEventListener('click', e => toggleAddService());
|
||||
options.appendChild(cancel);
|
||||
const add = document.createElement('button');
|
||||
add.type = 'submit';
|
||||
add.innerText = 'Add';
|
||||
options.appendChild(add);
|
||||
window.appendChild(options);
|
||||
|
||||
document.getElementById('add-service-container').classList.add('show-add-window');
|
||||
};
|
||||
|
||||
function hideAddServiceWindow() {
|
||||
document.getElementById('add-service-container').classList.remove('show-add-window');
|
||||
};
|
||||
|
||||
function buildAppriseURL() {
|
||||
return null;
|
||||
};
|
||||
|
||||
function addService() {
|
||||
const add_button = document.querySelector('#add-row > .action-column > button');
|
||||
const data = {
|
||||
'title': inputs_buttons.title.value,
|
||||
'url': inputs_buttons.url.value
|
||||
'title': document.querySelector('#add-row > .title-column > input').value,
|
||||
'url': document.querySelector('#add-row > .url-column > input').value
|
||||
};
|
||||
// const add_button = document.querySelector('#add-service-window > .options > button[type="submit"]');
|
||||
// const data = {
|
||||
// 'title': document.querySelector('#service-title').value,
|
||||
// 'url': buildAppriseURL()
|
||||
// };
|
||||
fetch(`${url_prefix}/api/notificationservices?api_key=${api_key}`, {
|
||||
'method': 'POST',
|
||||
'headers': {'Content-Type': 'application/json'},
|
||||
@@ -173,13 +320,9 @@ function addService() {
|
||||
.then(response => {
|
||||
if (!response.ok) return Promise.reject(response.status);
|
||||
|
||||
inputs_buttons.title.value = '';
|
||||
inputs_buttons.url.value = '';
|
||||
add_button.classList.remove('error-input');
|
||||
add_button.title = '';
|
||||
|
||||
inputs_buttons.save_button.classList.remove('error-icon');
|
||||
inputs_buttons.save_button.title = 'Add';
|
||||
inputs_buttons.save_button.setAttribute('aria-label', 'Add');
|
||||
|
||||
toggleAddService();
|
||||
fillNotificationSelection();
|
||||
})
|
||||
@@ -187,9 +330,9 @@ function addService() {
|
||||
if (e === 401)
|
||||
window.location.href = `${url_prefix}/`;
|
||||
else if (e === 400) {
|
||||
inputs_buttons.save_button.classList.add('error-icon');
|
||||
inputs_buttons.save_button.title = 'Invalid Apprise URL';
|
||||
inputs_buttons.save_button.setAttribute('aria-label', 'Invalid Apprise URL');
|
||||
// add_button.classList.add('error-input');
|
||||
add_button.classList.add('error-icon');
|
||||
add_button.title = 'Invalid Apprise URL';
|
||||
} else
|
||||
console.log(e);
|
||||
});
|
||||
@@ -199,5 +342,9 @@ function addService() {
|
||||
|
||||
fillNotificationSelection();
|
||||
|
||||
let notification_services = null;
|
||||
|
||||
document.getElementById('add-service-button').addEventListener('click', e => toggleAddService());
|
||||
document.querySelector('#add-row button[data-type="save"]').addEventListener('click', e => addService());
|
||||
// document.querySelector('#service-list button').addEventListener('click', e => showAddServiceWindow(-1));
|
||||
// document.getElementById('add-service-window').setAttribute('action', 'javascript:addService();');
|
||||
document.querySelector('#add-row > .action-column > button').addEventListener('click', e => addService());
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
function loadSettings() {
|
||||
document.getElementById('locale-input').value = JSON.parse(localStorage.getItem('MIND')).locale;
|
||||
};
|
||||
|
||||
function changePassword() {
|
||||
const data = {
|
||||
'new_password': document.getElementById('password-input').value
|
||||
@@ -19,6 +23,13 @@ function changePassword() {
|
||||
});
|
||||
};
|
||||
|
||||
function updateLocale(e) {
|
||||
const new_stor = JSON.parse(localStorage.getItem('MIND'));
|
||||
new_stor.locale = e.target.value;
|
||||
localStorage.setItem('MIND', JSON.stringify(new_stor));
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
function deleteAccount() {
|
||||
fetch(`${url_prefix}/api/user?api_key=${api_key}`, {
|
||||
'method': 'DELETE'
|
||||
@@ -30,5 +41,8 @@ function deleteAccount() {
|
||||
|
||||
// code run on load
|
||||
|
||||
loadSettings();
|
||||
|
||||
document.getElementById('change-password-form').setAttribute('action', 'javascript:changePassword()');
|
||||
document.getElementById('locale-input').addEventListener('change', updateLocale);
|
||||
document.getElementById('delete-account-button').addEventListener('click', e => deleteAccount());
|
||||
|
||||
@@ -94,7 +94,7 @@ function testReminder() {
|
||||
const ns = [...
|
||||
document.querySelectorAll('.notification-service-list input[type="checkbox"]:checked')
|
||||
].map(c => parseInt(c.dataset.id))
|
||||
if (!ns) {
|
||||
if (!ns.length) {
|
||||
input.classList.add('error-input');
|
||||
input.title = 'No notification service set';
|
||||
return
|
||||
@@ -143,10 +143,17 @@ function deleteInfo() {
|
||||
.then(response => {
|
||||
if (!response.ok) return Promise.reject(response.status);
|
||||
|
||||
fillNotificationSelection();
|
||||
fillReminders();
|
||||
fillStaticReminders();
|
||||
fillTemplates();
|
||||
if (cl.contains('show-edit-reminder')) {
|
||||
// Delete reminder
|
||||
fillReminders();
|
||||
} else if (cl.contains('show-edit-template')) {
|
||||
// Delete template
|
||||
fillTemplates();
|
||||
loadTemplateSelection();
|
||||
} else if (cl.contains('show-edit-static-reminder')) {
|
||||
// Delete static reminder
|
||||
fillStaticReminders();
|
||||
};
|
||||
hideWindow();
|
||||
})
|
||||
.catch(e => {
|
||||
@@ -192,7 +199,7 @@ function submitInfo() {
|
||||
data['time'] = (new Date(inputs.time.value) / 1000) + (new Date(inputs.time.value).getTimezoneOffset() * 60)
|
||||
if (type_buttons.repeat_button.dataset.selected === 'true') {
|
||||
data['repeat_quantity'] = type_buttons.repeat_quantity.value;
|
||||
data['repeat_interval'] = type_buttons.repeat_interval.value
|
||||
data['repeat_interval'] = parseInt(type_buttons.repeat_interval.value)
|
||||
};
|
||||
fetch_data.url = `${url_prefix}/api/reminders?api_key=${api_key}`;
|
||||
fetch_data.method = 'POST';
|
||||
@@ -218,7 +225,7 @@ function submitInfo() {
|
||||
data['time'] = (new Date(inputs.time.value) / 1000) + (new Date(inputs.time.value).getTimezoneOffset() * 60)
|
||||
if (type_buttons.repeat_button.dataset.selected === 'true') {
|
||||
data['repeat_quantity'] = type_buttons.repeat_quantity.value;
|
||||
data['repeat_interval'] = type_buttons.repeat_interval.value
|
||||
data['repeat_interval'] = parseInt(type_buttons.repeat_interval.value)
|
||||
};
|
||||
fetch_data.url = `${url_prefix}/api/reminders/${e_id}?api_key=${api_key}`;
|
||||
fetch_data.method = 'PUT';
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta id="url_prefix" data-value="{{url_prefix}}">
|
||||
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='img/favicon.svg') }}" type="image/x-icon">
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/general.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/login.css') }}">
|
||||
<script src="{{ url_for('static', filename='js/login.js') }}" defer></script>
|
||||
@@ -14,7 +16,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>MIND Reminders</h1>
|
||||
<img src="{{ url_for('static', filename='img/favicon.svg') }}" alt="">
|
||||
</header>
|
||||
<main>
|
||||
<div class="form-container">
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='img/favicon.svg') }}" type="image/x-icon">
|
||||
|
||||
<link rel="stylesheet" href="/static/css/general.css">
|
||||
<link rel="stylesheet" href="/static/css/page_not_found.css">
|
||||
|
||||
@@ -12,7 +14,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>MIND Reminders</h1>
|
||||
<img src="{{ url_for('static', filename='img/favicon.svg') }}" alt="">
|
||||
</header>
|
||||
<main>
|
||||
<h2>MIND</h1>
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta id="url_prefix" data-value="{{url_prefix}}">
|
||||
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='img/favicon.svg') }}" type="image/x-icon">
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/general.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/info.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/library.css') }}">
|
||||
@@ -33,7 +35,7 @@
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<h1>MIND Reminders</h1>
|
||||
<img src="{{ url_for('static', filename='img/favicon.svg') }}" alt="">
|
||||
</div>
|
||||
</header>
|
||||
<div class="nav-divider">
|
||||
@@ -81,71 +83,66 @@
|
||||
<button data-target="static-reminder-tab">Static Reminders</button>
|
||||
<button data-target="template-tab">Templates</button>
|
||||
</div>
|
||||
<div id="reminder-tab">
|
||||
<div class="search-container">
|
||||
<noscript>Javascript is disabled. The web-ui of MIND does not work with JavaScript disabled.</noscript>
|
||||
<form id="search-form">
|
||||
<div class="search-bar">
|
||||
<button type="submit">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 513.749 513.749" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M504.352,459.061l-99.435-99.477c74.402-99.427,54.115-240.344-45.312-314.746S119.261-9.277,44.859,90.15 S-9.256,330.494,90.171,404.896c79.868,59.766,189.565,59.766,269.434,0l99.477,99.477c12.501,12.501,32.769,12.501,45.269,0 c12.501-12.501,12.501-32.769,0-45.269L504.352,459.061z M225.717,385.696c-88.366,0-160-71.634-160-160s71.634-160,160-160 s160,71.634,160,160C385.623,314.022,314.044,385.602,225.717,385.696z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<input type="text" id="search-input" required placeholder="Search..." aria-placeholder="Search for reminders">
|
||||
<button type="button" id="clear-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512.021 512.021" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M301.258,256.01L502.645,54.645c12.501-12.501,12.501-32.769,0-45.269c-12.501-12.501-32.769-12.501-45.269,0l0,0 L256.01,210.762L54.645,9.376c-12.501-12.501-32.769-12.501-45.269,0s-12.501,32.769,0,45.269L210.762,256.01L9.376,457.376 c-12.501,12.501-12.501,32.769,0,45.269s32.769,12.501,45.269,0L256.01,301.258l201.365,201.387 c12.501,12.501,32.769,12.501,45.269,0c12.501-12.501,12.501-32.769,0-45.269L301.258,256.01z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="reminder-list">
|
||||
<button class="entry add-entry" id="add-reminder" aria-label="Add reminder" title="Add reminder">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<div class="search-container">
|
||||
<noscript>Javascript is disabled. The web-ui of MIND does not work with JavaScript disabled.</noscript>
|
||||
<form id="search-form">
|
||||
<div class="search-bar">
|
||||
<button type="submit">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 513.749 513.749" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
|
||||
<g>
|
||||
<path d="M504.352,459.061l-99.435-99.477c74.402-99.427,54.115-240.344-45.312-314.746S119.261-9.277,44.859,90.15 S-9.256,330.494,90.171,404.896c79.868,59.766,189.565,59.766,269.434,0l99.477,99.477c12.501,12.501,32.769,12.501,45.269,0 c12.501-12.501,12.501-32.769,0-45.269L504.352,459.061z M225.717,385.696c-88.366,0-160-71.634-160-160s71.634-160,160-160 s160,71.634,160,160C385.623,314.022,314.044,385.602,225.717,385.696z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<input type="text" id="search-input" required placeholder="Search..." aria-placeholder="Search for reminders">
|
||||
<button type="button" id="clear-button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512.021 512.021" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M301.258,256.01L502.645,54.645c12.501-12.501,12.501-32.769,0-45.269c-12.501-12.501-32.769-12.501-45.269,0l0,0 L256.01,210.762L54.645,9.376c-12.501-12.501-32.769-12.501-45.269,0s-12.501,32.769,0,45.269L210.762,256.01L9.376,457.376 c-12.501,12.501-12.501,32.769,0,45.269s32.769,12.501,45.269,0L256.01,301.258l201.365,201.387 c12.501,12.501,32.769,12.501,45.269,0c12.501-12.501,12.501-32.769,0-45.269L301.258,256.01z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<select id="sort-input"></select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="reminder-tab">
|
||||
<button class="entry add-entry" id="add-reminder" aria-label="Add reminder" title="Add reminder">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
<p>Add a notification service first!</p>
|
||||
</button>
|
||||
</div>
|
||||
</g>
|
||||
</svg>
|
||||
<p>Add a notification service first!</p>
|
||||
</button>
|
||||
</div>
|
||||
<div id="static-reminder-tab" class="hidden">
|
||||
<div id="static-reminder-list">
|
||||
<button class="entry add-entry" id="add-static-reminder" aria-label="Add static reminder" title="Add static reminder">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<button class="entry add-entry" id="add-static-reminder" aria-label="Add static reminder" title="Add static reminder">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
|
||||
</g>
|
||||
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="template-tab" class="hidden">
|
||||
<div id="template-list">
|
||||
<button class="entry add-entry" id="add-template" aria-label="Add template" title="Add template">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<button class="entry add-entry" id="add-template" aria-label="Add template" title="Add template">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="256" height="256" x="0" y="0" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
|
||||
</g>
|
||||
<path d="M480,224H288V32c0-17.673-14.327-32-32-32s-32,14.327-32,32v192H32c-17.673,0-32,14.327-32,32s14.327,32,32,32h192v192 c0,17.673,14.327,32,32,32s32-14.327,32-32V288h192c17.673,0,32-14.327,32-32S497.673,224,480,224z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -272,9 +269,72 @@
|
||||
<input type="password" id="password-input" autocomplete="new-password" required>
|
||||
<button type="submit">Change</button>
|
||||
</form>
|
||||
<h3>Locale</h3>
|
||||
<select id="locale-input">
|
||||
<option value="ar-SA">ar-SA</option>
|
||||
<option value="bn-BD">bn-BD</option>
|
||||
<option value="bn-IN">bn-IN</option>
|
||||
<option value="cs-CZ">cs-CZ</option>
|
||||
<option value="da-DK">da-DK</option>
|
||||
<option value="de-AT">de-AT</option>
|
||||
<option value="de-CH">de-CH</option>
|
||||
<option value="de-DE">de-DE</option>
|
||||
<option value="el-GR">el-GR</option>
|
||||
<option value="en-AU">en-AU</option>
|
||||
<option value="en-CA">en-CA</option>
|
||||
<option value="en-GB" selected>en-GB</option>
|
||||
<option value="en-IE">en-IE</option>
|
||||
<option value="en-IN">en-IN</option>
|
||||
<option value="en-NZ">en-NZ</option>
|
||||
<option value="en-US">en-US</option>
|
||||
<option value="en-ZA">en-ZA</option>
|
||||
<option value="es-AR">es-AR</option>
|
||||
<option value="es-CL">es-CL</option>
|
||||
<option value="es-CO">es-CO</option>
|
||||
<option value="es-ES">es-ES</option>
|
||||
<option value="es-MX">es-MX</option>
|
||||
<option value="es-US">es-US</option>
|
||||
<option value="fi-FI">fi-FI</option>
|
||||
<option value="fr-BE">fr-BE</option>
|
||||
<option value="fr-CA">fr-CA</option>
|
||||
<option value="fr-CH">fr-CH</option>
|
||||
<option value="fr-FR">fr-FR</option>
|
||||
<option value="he-IL">he-IL</option>
|
||||
<option value="hi-IN">hi-IN</option>
|
||||
<option value="hu-HU">hu-HU</option>
|
||||
<option value="id-ID">id-ID</option>
|
||||
<option value="it-CH">it-CH</option>
|
||||
<option value="it-IT">it-IT</option>
|
||||
<option value="ja-JP">ja-JP</option>
|
||||
<option value="ko-KR">ko-KR</option>
|
||||
<option value="nl-BE">nl-BE</option>
|
||||
<option value="nl-NL">nl-NL</option>
|
||||
<option value="no-NO">no-NO</option>
|
||||
<option value="pl-PL">pl-PL</option>
|
||||
<option value="pt-BR">pt-BR</option>
|
||||
<option value="pt-PT">pt-PT</option>
|
||||
<option value="ro-RO">ro-RO</option>
|
||||
<option value="ru-RU">ru-RU</option>
|
||||
<option value="sk-SK">sk-SK</option>
|
||||
<option value="sv-SE">sv-SE</option>
|
||||
<option value="ta-IN">ta-IN</option>
|
||||
<option value="ta-LK">ta-LK</option>
|
||||
<option value="th-TH">th-TH</option>
|
||||
<option value="tr-TR">tr-TR</option>
|
||||
<option value="zh-CN">zh-CN</option>
|
||||
<option value="zh-HK">zh-HK</option>
|
||||
<option value="zh-TW">zh-TW</option>
|
||||
</select>
|
||||
<h3>Delete Account</h3>
|
||||
<button id="delete-account-button">Delete Account</button>
|
||||
</div>
|
||||
<h2>Contact and Donation</h2>
|
||||
<div class="contact-list">
|
||||
<a href="https://ko-fi.com/casvt">Donate to MIND</a>
|
||||
<a href="https://casvt.github.io/MIND/">Documentation</a>
|
||||
<a href="https://github.com/Casvt/MIND/issues">Report an issue</a>
|
||||
<a href="https://discord.gg/nMNdgG7vsE">Discord server</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
6
project_management/docs-requirements.txt
Normal file
6
project_management/docs-requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
wheel>=0.38.4
|
||||
mkdocs-material>=8.5.11
|
||||
mkdocs-redirects>=1.2.0
|
||||
mkdocs-git-revision-date-localized-plugin>=1.1.0
|
||||
Pygments>=2.13.0
|
||||
pymdown-extensions>=9.9
|
||||
136
project_management/generate_api_docs.py
Normal file
136
project_management/generate_api_docs.py
Normal file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
#-*- coding: utf-8 -*-
|
||||
|
||||
from sys import path
|
||||
from os.path import dirname
|
||||
|
||||
path.insert(0, dirname(path[0]))
|
||||
|
||||
from subprocess import run
|
||||
from typing import Union
|
||||
from frontend.api import (DataSource, NotificationServiceNotFound,
|
||||
ReminderNotFound, TemplateNotFound, api_docs)
|
||||
from MIND import _folder_path, api_prefix
|
||||
|
||||
url_var_map = {
|
||||
'int:n_id': NotificationServiceNotFound,
|
||||
'int:r_id': ReminderNotFound,
|
||||
'int:t_id': TemplateNotFound,
|
||||
'int:s_id': ReminderNotFound
|
||||
}
|
||||
|
||||
result = f"""# API
|
||||
Below is the API documentation. Report an issue on [GitHub](https://github.com/Casvt/MIND/issues).
|
||||
|
||||
All endpoints have the `{api_prefix}` prefix. That means, for example, that `/auth/login` can be reached at `{api_prefix}/auth/login`.
|
||||
|
||||
## Authentication
|
||||
|
||||
Authentication is done using an API key.
|
||||
To log in, make a POST request to the [`{api_prefix}/auth/login`](#authlogin) endpoint.
|
||||
You'll receive an API key, which you can then use in your requests to authenticate.
|
||||
Supply it via the url parameter `api_key`.
|
||||
This API key is valid for one hour after which the key expires, any further requests return 401 'APIKeyExpired' and you are required to log in again.
|
||||
If no `api_key` is supplied or it is invalid, 401 `APIKeyInvalid` is returned.
|
||||
|
||||
For example:
|
||||
```bash
|
||||
curl -sSL 'http://192.168.2.15:8080{api_prefix}/reminders?api_key=ABCDEFG'
|
||||
```
|
||||
|
||||
## Supplying data
|
||||
|
||||
Often, data needs to be supplied with a request.
|
||||
If the parameters need to be supplied via `url`, add them to the url as url parameters.
|
||||
If the parameters need to be supplied via `body`, add them to the body as a json object and supply the `Content-Type: application/json` header.
|
||||
|
||||
For example:
|
||||
```bash
|
||||
# URL parameter
|
||||
curl -sSL 'http://192.168.2.15:8080{api_prefix}/reminders/search?api_key=ABCDEFG&query=Fountain&sort_by=time_reversed'
|
||||
|
||||
# Body parameter
|
||||
curl -sSLX POST \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
-d '{{"title": "Test service", "url": "test://fake/url"}}' \\
|
||||
'http://192.168.2.15:8080{api_prefix}/notificationservices?api_key=ABCDEFG'
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
The following is automatically generated. Please report any issues on [GitHub](https://github.com/Casvt/MIND/issues).
|
||||
"""
|
||||
|
||||
for rule, data in api_docs.items():
|
||||
result += f"""### `{rule}`
|
||||
|
||||
| Requires being logged in | Description |
|
||||
| ------------------------ | ----------- |
|
||||
| {'Yes' if data['requires_auth'] else 'No'} | {data['description']} |
|
||||
"""
|
||||
|
||||
url_var = rule.replace('<', '>').split('>')
|
||||
url_var: Union[str, None] = None if len(url_var) == 1 else url_var[1]
|
||||
|
||||
if url_var:
|
||||
result += f"""
|
||||
Replace `<{url_var}>` with the ID of the entry. For example: `{rule.replace(f'<{url_var}>', '2')}`.
|
||||
"""
|
||||
|
||||
for method in data['methods']:
|
||||
result += f"\n??? {method}\n"
|
||||
|
||||
if method in data['method_descriptions']:
|
||||
result += f"\n {data['method_descriptions'][method]}\n"
|
||||
|
||||
var_types = {
|
||||
'url': list(var for var in data['input_variables'].get(method, []) if var.source == DataSource.VALUES),
|
||||
'body': list(var for var in data['input_variables'].get(method, []) if var.source == DataSource.DATA)
|
||||
}
|
||||
|
||||
for var_type, entries in var_types.items():
|
||||
if entries:
|
||||
entries = [e('') for e in entries]
|
||||
result += f"""
|
||||
**Parameters ({var_type})**
|
||||
|
||||
| Name | Required | Description | Allowed values |
|
||||
| ---- | -------- | ----------- | -------------- |
|
||||
"""
|
||||
for entry in entries:
|
||||
result += f" {entry}\n"
|
||||
|
||||
result += f"""
|
||||
**Returns**
|
||||
|
||||
| Code | Error | Description |
|
||||
| ---- | ----- | ----------- |
|
||||
| {201 if method == 'POST' else 200}| N/A | Success |
|
||||
"""
|
||||
|
||||
url_exception = [url_var_map[url_var]] if url_var in url_var_map else []
|
||||
variable_exceptions = [e for v in data['input_variables'].get(method, []) for e in v.related_exceptions]
|
||||
related_exceptions = sorted(
|
||||
(e() for e in set(variable_exceptions + url_exception)),
|
||||
key=lambda e: (e.api_response['code'], e.api_response['error'])
|
||||
)
|
||||
for related_exception in related_exceptions:
|
||||
ar = related_exception.api_response
|
||||
result += f" | {ar['code']} | {ar['error']} | {related_exception.__doc__} |\n"
|
||||
|
||||
result += '\n'
|
||||
|
||||
with open(_folder_path('docs', 'api.md'), 'r') as f:
|
||||
current_content = f.read()
|
||||
|
||||
if current_content == result:
|
||||
print('Nothing changed')
|
||||
else:
|
||||
with open(_folder_path('docs', 'api.md'), 'w+') as f:
|
||||
f.write(result)
|
||||
|
||||
run(["git", "config", "--global", "user.email", '"casvantijn@gmail.com"'])
|
||||
run(["git", "config", "--global", "user.name", '"CasVT"'])
|
||||
run(["git", "checkout", "Development"])
|
||||
run(["git", "add", _folder_path('docs', 'api.md')])
|
||||
run(["git", "commit", "-m", "Updated API docs"])
|
||||
run(["git", "push"])
|
||||
85
project_management/mkdocs.yml
Normal file
85
project_management/mkdocs.yml
Normal file
@@ -0,0 +1,85 @@
|
||||
# Site Details
|
||||
site_name: MIND Docs
|
||||
site_description: The official guide for MIND
|
||||
site_author: Casvt
|
||||
repo_url: https://github.com/Casvt/MIND
|
||||
edit_uri: blob/Development/docs/
|
||||
repo_name: Casvt/MIND
|
||||
docs_dir: ../docs
|
||||
|
||||
extra_css:
|
||||
- assets/css/extra.css
|
||||
|
||||
# Site appearance (logos, colours, icons)
|
||||
theme:
|
||||
name: material
|
||||
language: en
|
||||
code_wrap: true
|
||||
features:
|
||||
- content.tabs.link
|
||||
- content.code.copy
|
||||
- header.autohide
|
||||
- navigation.expand
|
||||
- navigation.indexes
|
||||
- navigation.instant
|
||||
- navigation.sections
|
||||
- navigation.tabs
|
||||
- navigation.tabs.sticky
|
||||
- navigation.top
|
||||
- navigation.tracking
|
||||
favicon: assets/img/favicon.svg
|
||||
logo: assets/img/favicon.svg
|
||||
palette:
|
||||
# Light mode
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: MIND
|
||||
toggle:
|
||||
icon: material/toggle-switch-off-outline
|
||||
name: Switch to dark mode
|
||||
# Dark mode
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: MIND-dark
|
||||
toggle:
|
||||
icon: material/toggle-switch
|
||||
name: Switch to light mode
|
||||
|
||||
# Markdown extensions
|
||||
markdown_extensions:
|
||||
- abbr
|
||||
- attr_list
|
||||
- meta
|
||||
- pymdownx.details
|
||||
- pymdownx.highlight:
|
||||
guess_lang: true
|
||||
anchor_linenums: true
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.keys
|
||||
- pymdownx.saneheaders
|
||||
- pymdownx.smartsymbols
|
||||
- pymdownx.snippets
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
- sane_lists
|
||||
- toc:
|
||||
permalink: true
|
||||
toc_depth: 5
|
||||
- tables
|
||||
|
||||
# mkdocs function extensions
|
||||
plugins:
|
||||
- search
|
||||
- git-revision-date-localized:
|
||||
type: timeago
|
||||
locale: en
|
||||
fallback_to_build_date: false
|
||||
|
||||
# Navigation Layout
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Getting Started:
|
||||
- Installation: installation.md
|
||||
- Setup After Installation: setup_after_installation.md
|
||||
- Other Docs:
|
||||
- API: api.md
|
||||
- Backup Your Data: backup_your_data.md
|
||||
@@ -3,7 +3,7 @@ import unittest
|
||||
from flask import Blueprint
|
||||
|
||||
from backend.custom_exceptions import *
|
||||
from frontend.api import api, auth, error_handler, extract_key, return_api
|
||||
from frontend.api import api, return_api
|
||||
|
||||
class Test_API(unittest.TestCase):
|
||||
def test_blueprint(self):
|
||||
@@ -22,41 +22,4 @@ class Test_API(unittest.TestCase):
|
||||
self.assertEqual(result[1], case['code'])
|
||||
else:
|
||||
self.assertEqual(result[1], 200)
|
||||
|
||||
def test_auth(self):
|
||||
method = lambda x: x
|
||||
result = auth(method)
|
||||
self.assertEqual(result.__name__, method.__name__)
|
||||
|
||||
def _raise_exception(self, e, *args):
|
||||
raise e(*args)
|
||||
|
||||
def test_error_handler(self):
|
||||
result = error_handler(self._raise_exception)
|
||||
self.assertEqual(result.__name__, self._raise_exception.__name__)
|
||||
self.assertEqual(result(UsernameTaken), return_api(**UsernameTaken.api_response))
|
||||
self.assertEqual(result(UsernameInvalid), return_api(**UsernameInvalid.api_response))
|
||||
self.assertEqual(result(UserNotFound), return_api(**UserNotFound.api_response))
|
||||
self.assertEqual(result(AccessUnauthorized), return_api(**AccessUnauthorized.api_response))
|
||||
self.assertEqual(result(ReminderNotFound), return_api(**ReminderNotFound.api_response))
|
||||
self.assertEqual(result(NotificationServiceNotFound), return_api(**NotificationServiceNotFound.api_response))
|
||||
self.assertEqual(result(InvalidTime), return_api(**InvalidTime.api_response))
|
||||
self.assertEqual(result(InvalidURL), return_api(**InvalidURL.api_response))
|
||||
self.assertEqual(result(NotificationServiceInUse, 'test'), return_api(**NotificationServiceInUse('test').api_response))
|
||||
self.assertEqual(result(KeyNotFound, 'test'), return_api(**KeyNotFound('test').api_response))
|
||||
self.assertEqual(result(InvalidKeyValue, 'test', 'value'), return_api(**InvalidKeyValue('test', 'value').api_response))
|
||||
with self.assertRaises(TypeError):
|
||||
result(TypeError)
|
||||
with self.assertRaises(KeyError):
|
||||
result(KeyError)
|
||||
|
||||
def test_extract_key(self):
|
||||
with self.assertRaises(KeyNotFound):
|
||||
extract_key({'test': 'value'}, 'no_key')
|
||||
self.assertIsNone(extract_key({'test': 'value'}, 'no_key', check_existence=False))
|
||||
with self.assertRaises(InvalidKeyValue):
|
||||
extract_key({'time': ''}, 'time')
|
||||
self.assertIsInstance(extract_key({'time': '1'}, 'time'), int)
|
||||
|
||||
|
||||
|
||||
@@ -7,9 +7,19 @@ import backend.custom_exceptions
|
||||
|
||||
class Test_Custom_Exceptions(unittest.TestCase):
|
||||
def test_type(self):
|
||||
defined_exceptions: List[Exception] = map(lambda c: c[1], getmembers(modules['backend.custom_exceptions'], isclass))
|
||||
defined_exceptions: List[Exception] = filter(
|
||||
lambda c: c.__module__ == 'backend.custom_exceptions' and c is not backend.custom_exceptions.CustomException,
|
||||
map(
|
||||
lambda c: c[1],
|
||||
getmembers(modules['backend.custom_exceptions'], isclass)
|
||||
)
|
||||
)
|
||||
|
||||
for defined_exception in defined_exceptions:
|
||||
self.assertEqual(getmro(defined_exception)[1], Exception)
|
||||
self.assertEqual(
|
||||
getmro(defined_exception)[1],
|
||||
backend.custom_exceptions.CustomException
|
||||
)
|
||||
result = defined_exception().api_response
|
||||
self.assertIsInstance(result, dict)
|
||||
result['error']
|
||||
|
||||
@@ -8,5 +8,3 @@ class Test_DB(unittest.TestCase):
|
||||
DBConnection.file = _folder_path(*DB_FILENAME)
|
||||
instance = DBConnection(timeout=20.0)
|
||||
self.assertEqual(instance.cursor().execute("PRAGMA foreign_keys;").fetchone()[0], 1)
|
||||
|
||||
|
||||
@@ -11,10 +11,9 @@ class Test_Reminder_Handler(unittest.TestCase):
|
||||
def test_filter_function(self):
|
||||
p = {
|
||||
'title': 'TITLE',
|
||||
'text': 'TEXT',
|
||||
'notification_service_title': 'NOTIFICATION_SERVICE_TITLE'
|
||||
'text': 'TEXT'
|
||||
}
|
||||
for test_case in ('', 'title', 'service', 'ex'):
|
||||
for test_case in ('', 'title', 'ex'):
|
||||
self.assertTrue(filter_function(test_case, p))
|
||||
for test_case in (' ', 'Hello'):
|
||||
self.assertFalse(filter_function(test_case, p))
|
||||
|
||||
Reference in New Issue
Block a user