1
0
mirror of https://codeberg.org/diginaut/digimarks.git synced 2026-03-22 05:20:48 +01:00

21 Commits

Author SHA1 Message Date
99d2011e65 Favicon config 2026-02-11 16:11:30 +01:00
bade114b40 Fixed some links 2026-02-11 16:07:28 +01:00
8558b518f8 Example nginx configuration 2026-02-11 15:59:47 +01:00
9524fec672 Updated example configuration files 2026-02-11 15:59:10 +01:00
157303aba2 Disable Alpine.js clipboard for now 2026-02-11 15:50:48 +01:00
a961d90bda Reworked into Markdown 2026-02-11 15:32:30 +01:00
7fc7fdb171 Moving from ReStructured Text to Markdown 2026-02-11 14:28:46 +01:00
1d4bc73ece Bookmarkmoji 2026-02-11 14:25:17 +01:00
71d5c6533d Updates and fixes 2026-02-11 14:15:43 +01:00
dae6c5da18 Correctly update bookmark form, and show HTTP status on error 2026-01-04 21:43:21 +01:00
f68daf4ac0 Handle autocompletion, raise appropriate errors 2026-01-04 16:17:48 +01:00
be34c6e88f More complete direnv config 2026-01-04 14:59:50 +01:00
47a0f31ec3 Ignore env files, like dev.env 2026-01-04 12:11:00 +01:00
05fa94ef41 uvicorn is dep from fastapi already, we also need gunicorn for server 2026-01-04 11:41:18 +01:00
b4aff120c8 Dependency updates 2026-01-03 23:45:56 +01:00
82e4202482 No need to await this 2026-01-03 23:45:50 +01:00
9b03d51276 Better declaration of the httpx client 2026-01-03 23:45:39 +01:00
fe734d6dd8 Keep in account that there might be no modifications yet 2026-01-03 23:14:26 +01:00
2936a4815a Ignore some environment 2026-01-03 20:29:54 +01:00
09c685f2aa Typing fixes 2026-01-03 20:27:41 +01:00
0b08f0fa81 Codeberg and dependency config 2026-01-03 12:40:42 +01:00
18 changed files with 422 additions and 140 deletions

9
.gitignore vendored
View File

@@ -77,10 +77,15 @@ celerybeat-schedule
# dotenv
.env
*.env
# direnv
.envrc
# virtualenv
venv/
ENV/
.venv
# Spyder project settings
.spyderproject
@@ -93,6 +98,10 @@ ENV/
# vim
*.swp
*.swo
# Zed editor
.zed
# digimarks
static/favicons

130
README.md Normal file
View File

@@ -0,0 +1,130 @@
# 🔖 digimarks
[![PyPI version](https://img.shields.io/pypi/v/digimarks.svg)](https://pypi.python.org/pypi/digimarks/)
[![PyPI license](https://img.shields.io/github/license/aquatix/digimarks.svg)](https://pypi.python.org/pypi/digimarks/)
[![Codacy](https://api.codacy.com/project/badge/Grade/9a34319d917b43219a29e59e9ac75e3b)](https://app.codacy.com/app/aquatix/digimarks?utm_source=github.com&utm_medium=referral&utm_content=aquatix/digimarks&utm_campaign=badger)
## 📚 Overview
Simple bookmarking service, using a SQLite database to store bookmarks, supporting tags and automatic title fetching. Notes can be added, the items are cached locally in the browser, and the API is documented so everything can be accessed through that too.
[digimarks source](https://codeberg.org/diginaut/digimarks)
## 📥 Installation
There are a few ways to install digimarks to your computer or server.
### From PyPI
Assuming you already are inside a virtualenv:
```bash
# Using the wonderfully fast uv
uv pip install digimarks
# Alternatively, use Python pip
pip install digimarks
```
### From Git
Create a new virtualenv (if you are not already in one) and install the
necessary packages:
```bash
git clone https://codeberg.org/diginaut/digimarks.git
cd digimarks
# direnv will now create or activate a virtualenv
# See https://codeberg.org/diginaut/dotfiles/src/branch/master/.config/direnv/direnvrc for direnv uv config
# If you just want to run it, no need for development dependencies
uv sync --active --no-dev
# Otherwise, install everything in the active virtualenv
uv sync --active
```
## ⚙️ Migrating from version 1
To be able to use the new database schema's, you will need to migrate your existing `bookmarks.db` to one under the control of the `alembic` migrations tool.
To do so, start with making a backup of this `bookmarks.db` file to a safe place.
Then, stamp the initial migration into the database, and migrate to the latest version:
```bash
# Initiate migrations with the first one (only needs to be done once!)
alembic stamp 115bcd2e1a38
# Apply all migrations to get up-to-date
alembic upgrade head
```
## 🛠️ Usage / example configuration
⚠️ OUT OF DATE! ⚠️
Copy `settings.py` from example_config to the parent directory and
configure to your needs (*at the least* change the value of `SYSTEMKEY`).
Do not forget to fill in the `MASHAPE_API_KEY` value, which you `can request on the RapidAPI website <https://rapidapi.com/realfavicongenerator/api/realfavicongenerator>`_.
Run digimarks as a service under nginx or apache and call the appropriate url's when wanted.
Url's are of the form `https://marks.example.com/<userkey>/<action>`
digimarks can also be run from the command line: `uvicorn digimarks:app --reload`
Be sure to export/set the `SECRETKEY` environment variable before running, it's needed for some management URI's.
Run `gunicorn -k uvicorn.workers.UvicornWorker` for production. For an example of how to set up a server `see this article <https://www.slingacademy.com/article/deploying-fastapi-on-ubuntu-with-nginx-and-lets-encrypt/>`_ with configuration for nginx, uvicorn, systemd, security and such.
The RQ background worker can be run from the command line: `rq worker --with-scheduler`
Url's are of the form `https://hook.example.com/app/<appkey>/<triggerkey>`
API documentation is auto-generated, and can be browsed at https://hook.example.com/docs
## 🧩 Bookmarklet
To easily save a link from your browser, open its bookmark manager and create a new bookmark with as url:
```javascript
javascript:location.href='http://marks.example.com/1234567890abcdef/add?url='+encodeURIComponent(location.href);
```
## Creating a new user
After having set up the `settings.py` as under Usage, you can add a new user, by going to this path on your digimarks server:
```
/<secretkey>/adduser
```
where `secretkey` is the value set in settings.SYSTEMKEY
digimarks will then redirect to the bookmarks overview page of the new user. Please remember the user key (the hash in the url), as it will not be visible otherwise in the interface.
If you for whatever reason would lose this user key, just either look on the console (or webserver logs) where the list of available user keys is printed on digimarks startup, or open bookmarks.db with a SQLite editor.
## 🔧 Server configuration
- [systemd for digimarks API](https://codeberg.org/diginaut/digimarks/src/branch/fastapi/example_config/systemd/digimarks.service) which uses the [gunicorn config](https://codeberg.org/diginaut/digimarks/src/branch/fastapi/example_config/gunicorn_digimarks_conf.py)
- [nginx for digimarks API](https://codeberg.org/diginaut/digimarks/src/branch/fastapi/example_config/nginx_digimarks.conf)
- [more config](https://codeberg.org/diginaut/digimarks/src/branch/fastapi/example_config)
## ✨ What's new?
See the [Changelog](https://codeberg.org/diginaut/digimarks/src/branch/fastapi/CHANGELOG.md)
## 🙏 Attributions
'M' favicon by [Freepik](http://www.flaticon.com/free-icon/letter-m_2041)

View File

@@ -1,13 +1,17 @@
digimarks
=========
🔖 digimarks
===========
|PyPI version| |PyPI license| |Code health| |Codacy|
Simple bookmarking service, using a SQLite database to store bookmarks, supporting tags and automatic title fetching.
📚 Overview
----------
Simple bookmarking service, using a SQLite database to store bookmarks, supporting tags and automatic title fetching. Notes can be added, the items are cached locally in the browser, and the API is documented so everything can be accessed through that too.
Installation
------------
📥 Installation
--------------
From PyPI
~~~~~~~~~
@@ -16,6 +20,10 @@ Assuming you already are inside a virtualenv:
.. code-block:: bash
# Using the wonderfully fast uv
uv pip install digimarks
# Alternatively, use Python pip
pip install digimarks
@@ -30,15 +38,15 @@ necessary packages:
git clone https://codeberg.org/diginaut/digimarks.git
cd digimarks
# direnv will now create or activate a virtualenv
# See https://github.com/direnv/direnv/wiki/Python#uv for direnv uv config
# See https://codeberg.org/diginaut/dotfiles/src/branch/master/.config/direnv/direnvrc for direnv uv config
# If you just want to run it, no need for development dependencies
uv sync --active --no-dev
# Otherwise, install everything in the active virtualenv
uv sync --active
Migrating from version 1
------------------------
⚙️ Migrating from version 1
--------------------------
To be able to use the new database schema's, you will need to migrate your existing ``bookmarks.db`` to one under the control of the ``alembic`` migrations tool.
@@ -48,23 +56,24 @@ Then, stamp the initial migration into the database, and migrate to the latest v
.. code-block:: bash
# Initiate migrations with the first one (only needs to be done once!)
alembic stamp 115bcd2e1a38
# Apply all migrations to get up-to-date
alembic upgrade head
Usage / example configuration
-----------------------------
🛠️ Usage / example configuration
-------------------------------
OUT OF DATE!
⚠️ OUT OF DATE! ⚠️
Copy ``settings.py`` from example_config to the parent directory and
configure to your needs (*at the least* change the value of `SYSTEMKEY`).
Do not forget to fill in the `MASHAPE_API_KEY` value, which you ``can request on the RapidAPI website <https://rapidapi.com/realfavicongenerator/api/realfavicongenerator>`_.
Do not forget to fill in the ``MASHAPE_API_KEY`` value, which you `can request on the RapidAPI website <https://rapidapi.com/realfavicongenerator/api/realfavicongenerator>`_.
Run digimarks as a service under nginx or apache and call the appropriate
url's when wanted.
Run digimarks as a service under nginx or apache and call the appropriate url's when wanted.
Url's are of the form ``https://marks.example.com/<userkey>/<action>``
@@ -94,7 +103,7 @@ To easily save a link from your browser, open its bookmark manager and create a
Creating a new user
-------------------
After having set up the ```settings.py``` as under Usage, you can add a new user, by going to this path on your digimarks server:
After having set up the ``settings.py`` as under Usage, you can add a new user, by going to this path on your digimarks server:
/<secretkey>/adduser
@@ -105,27 +114,27 @@ digimarks will then redirect to the bookmarks overview page of the new user. Ple
If you for whatever reason would lose this user key, just either look on the console (or webserver logs) where the list of available user keys is printed on digimarks startup, or open bookmarks.db with a SQLite editor.
Server configuration
~~~~~~~~~~~~~~~~~~~~
🔧 Server configuration
~~~~~~~~~~~~~~~~~~~~~~
* `systemd for digimarks API`_ which uses the `gunicorn config`_
* `nginx for digimarks API`_
* `more config`_
What's new?
-----------
What's new?
-------------
See the `Changelog`_.
Attributions
------------
🙏 Attributions
--------------
'M' favicon by `Freepik`_.
.. _digimarks: https://github.com/aquatix/digimarks
.. _digimarks: https://codeberg.org/diginaut/digimarks
.. |PyPI version| image:: https://img.shields.io/pypi/v/digimarks.svg
:target: https://pypi.python.org/pypi/digimarks/
.. |PyPI license| image:: https://img.shields.io/github/license/aquatix/digimarks.svg
@@ -136,11 +145,11 @@ Attributions
.. |Codacy| image:: https://api.codacy.com/project/badge/Grade/9a34319d917b43219a29e59e9ac75e3b
:alt: Codacy Badge
:target: https://app.codacy.com/app/aquatix/digimarks?utm_source=github.com&utm_medium=referral&utm_content=aquatix/digimarks&utm_campaign=badger
.. _hook settings: https://github.com/aquatix/digimarks/blob/master/example_config/examples.yaml
.. _vhost for Apache2.4: https://github.com/aquatix/digimarks/blob/master/example_config/apache_vhost.conf
.. _uwsgi.ini: https://github.com/aquatix/digimarks/blob/master/example_config/uwsgi.ini
.. _Changelog: https://github.com/aquatix/digimarks/blob/master/CHANGELOG.md
.. _hook settings: https://codeberg.org/diginaut/digimarks/blob/master/example_config/examples.yaml
.. _vhost for Apache2.4: https://codeberg.org/diginaut/digimarks/blob/master/example_config/apache_vhost.conf
.. _uwsgi.ini: https://codeberg.org/diginaut/digimarks/blob/master/example_config/uwsgi.ini
.. _Changelog: https://codeberg.org/diginaut/digimarks/blob/master/CHANGELOG.md
.. _Freepik: http://www.flaticon.com/free-icon/letter-m_2041
.. _systemd for digimarks API: https://github.com/aquatix/digimarks/blob/master/example_config/systemd/digimarks.service
.. _gunicorn config: https://github.com/aquatix/digimarks/blob/master/example_config/gunicorn_digimarks_conf.py
.. _more config: https://github.com/aquatix/digimarks/tree/master/example_config
.. _systemd for digimarks API: https://codeberg.org/diginaut/digimarks/blob/master/example_config/systemd/digimarks.service
.. _gunicorn config: https://codeberg.org/diginaut/digimarks/src/branch/master/example_config/uwsgi.ini
.. _more config: https://codeberg.org/diginaut/digimarks/src/branch/master/example_config

View File

@@ -1,29 +0,0 @@
<VirtualHost *:80>
ServerAdmin webmaster@example.com
ServerName marks.example.com
WSGIDaemonProcess digimarks user=youruser group=youruser threads=5 python-path=/srv/marks.example.com/digimarks/
WSGIScriptAlias / /srv/marks.example.com/digimarks/wsgi.py
<Directory /srv/marks.example.com/digimarks>
WSGIProcessGroup digimarks
WSGIApplicationGroup %{GLOBAL}
Require all granted
</Directory>
<Directory /srv/marks.example.com/digimarks>
<Files wsgi.py>
Require all granted
</Files>
</Directory>
ErrorLog /var/log/apache2/error_marks.example.com.log
# Possible values include: debug, info, notice, warn, error, crit,
# alert, emerg.
LogLevel warn
CustomLog /var/log/apache2/access_marks.example.com.log combined
ServerSignature On
</VirtualHost>

View File

@@ -0,0 +1,14 @@
# gunicorn_conf.py
from multiprocessing import cpu_count
bind = "127.0.0.1:8890"
# Worker Options
#workers = cpu_count() + 1
workers = 1
worker_class = 'uvicorn.workers.UvicornWorker'
# Logging Options
loglevel = 'debug'
accesslog = '/var/log/digimarks/access_log'
errorlog = '/var/log/digimarks/error_log'

View File

@@ -0,0 +1,73 @@
server {
server_name marks.example.org;
listen [::]:443 ssl; # managed by Certbot
listen 443 ssl; # managed by Certbot
real_ip_header X-Forwarded-For;
access_log /var/log/nginx/access_marks.example.org.log;
error_log /var/log/nginx/error_marks.example.org.log warn;
# Media: images, icons, video, audio, HTC
#location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|mp3|ogg|ogv|webm|htc|woff2|woff)$ {
# expires 1M;
# access_log off;
# # max-age must be in seconds
# add_header Cache-Control "max-age=2629746, public";
#}
# CSS and Javascript
#location ~* \.(?:css|js)$ {
# expires 1M;
# access_log off;
# add_header Cache-Control "max-age=31556952, public";
#}
location / {
proxy_pass http://127.0.0.1:8890;
proxy_read_timeout 60;
proxy_connect_timeout 60;
proxy_redirect off;
# Allow the use of websockets
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
location /content/favicons/ {
alias /srv/www/marks.example.org/favicons/;
# This can certainly be cached, so do so for a month
expires 1M;
add_header Cache-Control "public";
}
location /static/ {
alias /srv/www/marks.example.org/digimarks/src/digimarks/static/;
# This can certainly be cached, so do so for a month
#expires 1M;
#add_header Cache-Control "public";
}
location /favicon.ico {
# Favicon for the webapp, shown in the browser
alias /srv/www/marks.example.org/digimarks/src/digimarks/static/favicon.ico;
}
ssl_certificate /etc/letsencrypt/live/marks.example.org/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/marks.example.org/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = marks.example.org) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen [::]:80 ;
listen 80;
server_name marks.example.org;
return 404; # managed by Certbot
}

View File

@@ -1,20 +0,0 @@
# Virtualenv to use with the wsgi file (optional)
VENV = '/srv/marks.example.com/venv/bin/activate_this.py'
PORT = 8086
DEBUG = False
# Password/url key to do admin stuff with, like adding a user
# NB: change this to something else! For example, in bash:
# echo -n "yourstring" | sha1sum
SYSTEMKEY = 'S3kr1t'
# RapidAPI key for favicons
# https://rapidapi.com/realfavicongenerator/api/realfavicongenerator
MASHAPE_API_KEY = 'your_MASHAPE_key'
LOG_LOCATION = 'digimarks.log'
#LOG_LOCATION = '/var/log/digimarks/digimarks.log'
# How many logs to keep in log rotation:
LOG_BACKUP_COUNT = 10

View File

@@ -0,0 +1,22 @@
[Unit]
Description=Gunicorn Daemon for digimarks FastAPI
After=network.target
[Service]
User=YOUR_USER
Group=YOUR_USER
WorkingDirectory=/srv/www/marks.example.org/digimarks/src
Environment="SYSTEM_KEY=RanDomSeCretKeyFoRAdmin"
Environment="FAVICONS_DIR=/srv/www/marks.example.org/favicons"
Environment="DATABASE_FILE=/srv/www/marks.example.org/bookmarks.db"
Environment="STATIC_DIR=digimarks/static"
Environment="TEMPLATE_DIR=digimarks/templates"
ExecStart=/srv/www/marks.example.org/venv/bin/gunicorn -c /srv/www/marks.example.org/gunicorn_digimarks_conf.py digimarks.main:app
StandardOutput=file:///var/log/digimarks/stdout.log
StandardError=file:///var/log/digimarks/stderr.log
[Install]
WantedBy=multi-user.target

View File

@@ -1,15 +0,0 @@
# Example supervisord configuration
# Run with /srv/venv/bin/uwsgi --ini /srv/digimarks/uwsgi.ini:digimarks
[digimarks]
chdir = /srv/digimarks
socket = /tmp/uwsgi_digimarks.sock
module = wsgi
threads = 4
master = true
processes = 5
vacuum = true
no-orphans = true
chmod-socket = 666
logger = main file:/var/log/webapps/digimarks.log
logger = file:/var/log/webapps/digimarks_debug.log

View File

@@ -10,7 +10,7 @@ authors = [
]
description = 'Simple bookmarking service, using a SQLite database to store bookmarks, supporting tags, automatic title fetching and REST API calls.'
readme = "README.rst"
requires-python = ">=3.10"
requires-python = ">=3.11"
keywords = ["bookmarks", "api"]
license = { text = "Apache" }
classifiers = [
@@ -30,6 +30,11 @@ dependencies = [
"feedgen",
]
[project.optional-dependencies]
server = [
"uvicorn",
]
[dependency-groups]
dev = [
{ include-group = "lint" },
@@ -50,7 +55,7 @@ pub = [
"twine"
]
server = [
"uvicorn",
"gunicorn>=23.0.0",
]
# dynamic = ["version"]

View File

@@ -1,7 +1,7 @@
# Core application
fastapi[all]
sqlmodel
sqlalchemy
sqlalchemy[asyncio]
pydantic
pydantic_settings
alembic

View File

@@ -10,7 +10,8 @@ import bs4
import httpx
from extract_favicon import from_html
from fastapi import Query, Request
from pydantic import AnyUrl
from fastapi.exceptions import HTTPException
from pydantic import AnyUrl, ValidationError
from sqlmodel import select
from digimarks import tags_service, utils
@@ -34,8 +35,11 @@ async def set_information_from_source(bookmark: Bookmark, request: Request) -> B
"""Request the title by requesting the source url."""
logger.info('Extracting information from url %s', bookmark.url)
try:
result = await request.app.requests_client.get(bookmark.url, headers={'User-Agent': DIGIMARKS_USER_AGENT})
result = await request.app.state.requests_client.get(
str(bookmark.url), headers={'User-Agent': DIGIMARKS_USER_AGENT}
)
bookmark.http_status = result.status_code
logger.info('HTTP status code %s for %s', bookmark.http_status, bookmark.url)
except httpx.HTTPError as err:
# For example, "MissingSchema: Invalid URL 'abc': No schema supplied. Perhaps you meant http://abc?"
logger.error('Exception when trying to retrieve title for %s. Error: %s', bookmark.url, str(err))
@@ -43,11 +47,12 @@ async def set_information_from_source(bookmark: Bookmark, request: Request) -> B
bookmark.title = ''
return bookmark
if bookmark.http_status == 200 or bookmark.http_status == 202:
html = bs4.BeautifulSoup(result.text, 'html.parser')
html_content = bs4.BeautifulSoup(result.text, 'html.parser')
try:
bookmark.title = html.title.text.strip()
except AttributeError:
bookmark.title = ''
bookmark.title = html_content.title.text.strip()
except AttributeError as exc:
logger.error('Error while trying to extract title from URL %s: %s', str(bookmark.url), str(exc))
raise HTTPException(status_code=400, detail='Error while trying to extract title')
url_parts = urlparse(str(bookmark.url))
root_url = url_parts.scheme + '://' + url_parts.netloc
@@ -56,8 +61,8 @@ async def set_information_from_source(bookmark: Bookmark, request: Request) -> B
# with open(filename, 'wb') as out_file:
# shutil.copyfileobj(response.raw, out_file)
# Extraction was successful
logger.info('Extracting information was successful')
# Extraction was successful
logger.info('Extracting information was successful')
return bookmark
@@ -72,11 +77,15 @@ def strip_url_params(url: str) -> str:
return urlunparse((parsed.scheme, parsed.netloc, parsed.path, parsed.params, '', parsed.fragment))
def update_bookmark_with_info(bookmark: Bookmark, request: Request, strip_params: bool = False):
async def update_bookmark_with_info(bookmark: Bookmark, request: Request, strip_params: bool = False):
"""Automatically update title, favicon, etc."""
if isinstance(bookmark.url, str):
# If type of the url is a 'simple' string, ensure it to be an AnyUrl
bookmark.url = AnyUrl(bookmark.url)
if not bookmark.title:
# Title was empty, automatically fetch it from the url, will also update the status code
set_information_from_source(bookmark, request)
await set_information_from_source(bookmark, request)
if strip_params:
# Strip URL parameters, e.g., tracking params
@@ -129,7 +138,12 @@ async def autocomplete_bookmark(
bookmark.user_key = user_key
# Auto-fill title, fix tags etc.
update_bookmark_with_info(bookmark, request, strip_params)
try:
await update_bookmark_with_info(bookmark, request, strip_params)
except ValidationError as exc:
logger.error('ValidationError while autocompleting bookmark with URL %s', bookmark.url)
logger.error('Error was: %s', str(exc))
raise HTTPException(status_code=400, detail='Error while autocompleting, likely the URL contained an error')
url_hash = utils.generate_hash(str(bookmark.url))
result = await session.exec(
@@ -157,7 +171,7 @@ async def add_bookmark(
bookmark.user_key = user_key
# Auto-fill title, fix tags etc.
update_bookmark_with_info(bookmark, request, strip_params)
await update_bookmark_with_info(bookmark, request, strip_params)
bookmark.url_hash = utils.generate_hash(str(bookmark.url))
logger.info('Adding bookmark %s for user %s', bookmark.url_hash, user_key)
@@ -193,7 +207,7 @@ async def update_bookmark(
bookmark_db.sqlmodel_update(bookmark_data)
# Autofill title, fix tags, etc. where (still) needed
update_bookmark_with_info(bookmark, request, strip_params)
await update_bookmark_with_info(bookmark, request, strip_params)
session.add(bookmark_db)
await session.commit()

View File

@@ -4,7 +4,7 @@ import logging
from collections.abc import Sequence
from contextlib import asynccontextmanager
from datetime import UTC, datetime
from typing import Annotated
from typing import Annotated, AsyncGenerator, cast
import httpx
from fastapi import Depends, FastAPI, HTTPException, Query, Request
@@ -36,8 +36,8 @@ class Settings(BaseSettings):
# inside the codebase
# static_dir: DirectoryPath = Path('digimarks/static')
# template_dir: DirectoryPath = Path('digimarks/templates')
static_dir: DirectoryPath = 'digimarks/static'
template_dir: DirectoryPath = 'digimarks/templates'
static_dir: DirectoryPath = DirectoryPath('digimarks/static')
template_dir: DirectoryPath = DirectoryPath('digimarks/templates')
media_url: str = '/static/'
@@ -54,22 +54,32 @@ engine = create_async_engine(f'sqlite+aiosqlite:///{settings.database_file}', co
async def get_session() -> AsyncSession:
"""SQLAlchemy session factory."""
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async_session = sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:
yield session
# Shorter way of getting the DB session in an endpoint
SessionDep = Annotated[AsyncSession, Depends(get_session)]
@asynccontextmanager
async def lifespan(the_app: FastAPI):
async def lifespan(the_app: FastAPI) -> AsyncGenerator[None, None]:
"""Upon start, initialise an AsyncClient and assign it to an attribute named requests_client on the app object."""
the_app.requests_client = httpx.AsyncClient()
yield
await the_app.requests_client.aclose()
async with httpx.AsyncClient() as requests_client:
the_app.state.requests_client = requests_client
yield
await the_app.state.requests_client.aclose()
async def get_requests_client(request: Request) -> httpx.AsyncClient:
"""Get the httpx client from the application object."""
return cast(httpx.AsyncClient, request.app.state.requests_client)
# Shorter way of getting the httpx client in an endpoint
RequestsDep = Annotated[AsyncSession, Depends(get_requests_client)]
app = FastAPI(lifespan=lifespan)
app.mount('/static', StaticFiles(directory=settings.static_dir), name='static')
app.mount('/content/favicons', StaticFiles(directory=settings.favicons_dir), name='favicons')
@@ -127,7 +137,7 @@ def index(request: Request):
@app.get('/api/v1/admin/{system_key}/users/{user_id}', response_model=User)
async def get_user(session: SessionDep, system_key: str, user_id: int) -> type[User]:
async def get_user(session: SessionDep, system_key: str, user_id: int) -> User:
"""Show user information."""
logger.info('User %d requested', user_id)
if system_key != settings.system_key:
@@ -275,12 +285,24 @@ async def bookmarks_changed_since(
)
latest_created_bookmark = result.first()
latest_modification = max(latest_modified_bookmark.modified_date, latest_created_bookmark.created_date)
# There needs to be at least one bookmark of course
if latest_created_bookmark:
latest_created_datetime = latest_created_bookmark.created_date
else:
latest_created_datetime = datetime.min
# We only have a modified datetime when at least one has been edited
if latest_modified_bookmark:
latest_modified_datetime = latest_modified_bookmark.modified_date
else:
latest_modified_datetime = datetime.min
latest_modification = max(latest_modified_datetime, latest_created_datetime)
return {
'current_time': datetime.now(UTC),
'latest_change': latest_modified_bookmark.modified_date,
'latest_created': latest_created_bookmark.created_date,
'latest_change': latest_modified_datetime,
'latest_created': latest_created_datetime,
'latest_modification': latest_modification,
}

View File

@@ -3,7 +3,7 @@
* v0.0.2
*
* Created by: Michiel Scholten
* Source: https://github.com/aquatix/digui
* Source: https://codeberg.org/diginaut/digui
*/
/** Colours and themes */

View File

@@ -105,7 +105,7 @@ document.addEventListener('alpine:init', () => {
this.cache[this.userKey]['tags'] = await tagsResponse.json();
/* Filter bookmarks by (blacklisted) tags */
await this.filterBookmarksByTags();
this.filterBookmarksByTags();
this.loading = false;
},
@@ -214,15 +214,18 @@ document.addEventListener('alpine:init', () => {
resetEditBookmark() {
this.bookmarkToEdit = {
'url_hash': '',
'url': '',
'title': '',
'note': '',
'tags': ''
'tags': '',
'http_status': 0,
'strip_params': false
}
},
async startAddingBookmark() {
/* Open 'add bookmark' page */
console.log('Start adding bookmark');
console.log('Open "add bookmark" modal');
this.resetEditBookmark();
// this.show_bookmark_details = true;
const editFormDialog = document.getElementById("editFormDialog");
@@ -233,7 +236,54 @@ document.addEventListener('alpine:init', () => {
console.log('Bookmark URL changed');
// let response = await fetch('/api/v1/' + this.userKey + '/autocomplete_bookmark/');
try {
const response = await fetch('/api/v1/' + this.userKey + '/autocomplete_bookmark/', {
const response = await fetch('/api/v1/' + this.userKey + '/autocomplete_bookmark/?strip_params=' + this.bookmarkToEdit.strip_params, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
// Bookmark form data
url_hash: this.bookmarkToEdit.url_hash,
url: this.bookmarkToEdit.url,
title: this.bookmarkToEdit.title,
note: this.bookmarkToEdit.note,
tags: this.bookmarkToEdit.tags
})
});
const data = await response.json();
// TODO: update form fields if needed (auto-fetched title for example
console.log('Got response');
console.log(response);
console.log(data);
if (response.ok) {
this.bookmarkToEdit.url_hash = data.url_hash;
this.bookmarkToEdit.url = data.url;
this.bookmarkToEdit.title = data.title;
this.bookmarkToEdit.note = data.note;
this.bookmarkToEdit.tags = data.tags;
this.bookmarkToEdit.http_status = data.http_status;
} else {
console.log('Error occurred');
this.bookmarkToEditError = data.detail;
}
} catch (error) {
// enter logic for when there is an error (ex. error toast)
console.log('error occurred');
console.log(error);
this.bookmarkToEditError = error.detail;
console.log('yesssh?');
}
},
async saveBookmark() {
console.log('Saving bookmark');
// this.bookmarkToEditVisible = false;
// this.show_bookmark_details = false;
},
async addBookmark() {
/* Post new bookmark to the backend */
console.log('Adding bookmark');
try {
const response = await fetch('/api/v1/' + this.userKey + '/add_bookmark/?strip_params=' + this.bookmarkToEdit.strip_params, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -249,21 +299,12 @@ document.addEventListener('alpine:init', () => {
const data = await response.json();
// TODO: update form fields if needed (auto-fetched title for example
console.log(data);
this.bookmarkToEditError = 'lolwut';
// this.bookmarkToEditError = 'lolwut';
} catch (error) {
// enter your logic for when there is an error (ex. error toast)
console.log(error)
}
},
async saveBookmark() {
console.log('Saving bookmark');
// this.bookmarkToEditVisible = false;
// this.show_bookmark_details = false;
},
async addBookmark() {
/* Post new bookmark to the backend */
//
}
})
});

View File

@@ -13,8 +13,10 @@
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png">
<link rel="manifest" href="/static/images/site.webmanifest">
<script src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>
{#-
<script src="https://cdn.jsdelivr.net/npm/@ryangjchandler/alpine-clipboard@2.x.x/dist/alpine-clipboard.js"
defer></script>
#}
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body>

View File

@@ -9,7 +9,7 @@
<ul>
<li><h1>digimarks</h1></li>
<li>
<a class="button" href="https://github.com/aquatix/digimarks">digimarks project page</a>
<a class="button" href="https://codeberg.org/diginaut/digimarks">digimarks project page</a>
</li>
</ul>
</nav>
@@ -18,7 +18,7 @@
<main>
<h1>Welcome to digimarks, your online bookmarking and notes tool</h1>
<p>Please visit your personal url, or <a href="https://github.com/aquatix/digimarks">see the digimarks
<p>Please visit your personal url, or <a href="https://codeberg.org/diginaut/digimarks">see the digimarks
project page</a>.</p>
<p>If you forgot/lost your personal url, contact your digimarks

View File

@@ -192,6 +192,10 @@
<input id="bookmark_url" type="text" name="bookmark_url" placeholder="url"
x-on:change.debounce="$store.digimarks.bookmarkURLChanged()"
x-model="$store.digimarks.bookmarkToEdit.url">
<p x-show="$store.digimarks.bookmarkToEdit.http_status > 202"
x-text="'HTTP statuscode: ' + $store.digimarks.bookmarkToEdit.http_status" x-cloak
class="error"></p>
<p>
</fieldset>
<fieldset class="form-group">
<label for="bookmark_title">Title</label>
@@ -212,16 +216,17 @@
x-model="$store.digimarks.bookmarkToEdit.tags">
</fieldset>
<p x-show="$store.digimarks.bookmarkToEditError"
x-data="$store.digimarks.bookmarkToEditError"></p>
x-text="$store.digimarks.bookmarkToEditError" x-cloak class="error"></p>
<p>
<label>
<input type="checkbox" name="strip" id="strip"/>
<input type="checkbox" x-model="$store.digimarks.bookmarkToEdit.strip_params"/>
<span>Strip parameters from url (like <em>?utm_source=social</em> - can break the link!)</span>
</label>
</p>
<div>
<button value="cancel">Cancel</button>
<button @click="$store.digimarks.saveBookmark()">Save</button>
<button @click="$store.digimarks.addBookmark()">Add</button>
</div>
</form>
</template>