joplin#
Description#
Plugin to interact with Joplin, a free and open-source note-taking application.
## Advised setup
Joplin is mainly intended as a desktop application, with support for synchronization across multiple devices via various backends.
This plugin is designed to interact with the Joplin API exposed by the “desktop” application - the same one used by the Joplin Web Clipper extension.
This can be achieved in two ways:
Using the Joplin desktop application
Using Joplin in headless mode
### Using the Joplin desktop application
To use the Joplin desktop application, you need to enable the Web Clipper service in the Joplin settings. This will expose an HTTP API on the port 41184 on your local machine.
Note that the Joplin desktop application must be running for this plugin to work, and by default it will only accept requests from the local machine.
If you want to run the Platypush server on a different machine, you can e.g. open an SSH tunnel to the Joplin machine:
ssh -L 41184:localhost:41184 user@joplin-machine
### Using Joplin in headless mode
The downside of the Joplin desktop application is that it must be running on a live desktop session for the plugin to work. Unfortunately, Joplin does not provide an official headless mode, but there is a community project that allows you to run Joplin in a Docker container in headless mode, exposing the same HTTP API as the desktop application.
It’s first advised to run and configure your Joplin on the desktop app.
Then locate the Joplin profile directory (usually
~/.config/joplin-desktop on Linux, but depending on the installed
version it could also be named Joplin or joplin) and copy the
file named settings.json to ~/.config/joplin/settings.json on the
machine where you run the headless Joplin container.
Optionally, you can also copy the database.sqlite file from the
desktop Joplin profile directory to the headless Joplin profile, but make
sure that the desktop instance and the headless instance run the same
version of Joplin, otherwise you might run into database incompatibility
issues.
#### Credentials
If you opt not to copy the database file to the headless Joplin profile,
then you may have to manually set up the credentials in a separate file
named secrets.json in the Joplin profile directory. This will contain
both the API token and the passwords for any connected synchronization
services. For example:
{ "api.token": "your_api_token_here", "sync.1.password": "your_sync_password_here" }
Note:
The
api.tokenfield is mandatory, and it should match the one you configure in this plugin.
sync.<n>.passwordshould be such that<n>is the index of the synchronization target configured insettings.json(e.g. Dropbox, Nextcloud, S3, Joplin Cloud, a local Joplin server, etc.).
#### Running the service
docker run --rm \ --name joplin-headless \ -v ~/.config/joplin:/home/node/.config/joplin \ -v ~/.config/joplin/secrets.json:/run/secrets/joplin-config.json:ro \ -p 41184:80 \ jspiers/headless-joplin:2.13.2-node-20.11.1
#### Synchronization
Unlike the Joplin desktop application, the headless Joplin instance does not provide a periodic synchronization mechanism. But you can schedule a cronjob to periodically run synchronization each e.g. 5 minutes:
crontab -e # Add the following line to the crontab file */5 * * * * /usr/bin/docker exec joplin-headless joplin sync
It is advised to have at least a remote synchronization target, and have the same target configured both in the Joplin desktop or mobile application and in the headless instance, so that you can keep your notes in sync across all of your devices, even though this plugin will only interact with the headless instance.
Configuration#
joplin:
# [Required]
# The hostname or IP address of your Joplin application.
host: # type=str
# [Optional]
# The port number of your Joplin application (default: 41184).
# port: 41184 # type=int
# [Required]
# The access token of your Joplin server.
token: # type=str
# [Optional]
# Poll interval in seconds to check for updates (default: 300).
# If set to zero or null, the plugin will not poll for updates,
# and events will be generated only when you manually call `BaseNotePlugin.sync <https://docs.platypush.tech/platypush/plugins/notes._base.html#platypush.plugins.notes._base.BaseNotePlugin.sync>`_.
# poll_interval: 300 # type=float
# [Optional]
# Timeout in seconds for the plugin operations (default: 60).
# timeout: 60 # type=int | None
# [Optional]
# If the API used by the plugin doesn't support
# free-text search (that's currently the case for
# `NextcloudNotesPlugin <https://docs.platypush.tech/platypush/plugins/nextcloud.notes.html#platypush.plugins.nextcloud.notes.NextcloudNotesPlugin>`_ and
# for any notes plugins that use the local file system as a backend),
# then the plugin will use a search index to perform searches. This
# parameter specifies the maximum length of the search tokens that will
# be indexed, where each token is composed of a sequence of
# alphanumeric characters (including underscores). The longer the number,
# the more tokens will be indexed and longer exact phrases will be stored,
# but more disk space will be used for the search index (default: 4).
# max_tokens_length: 4 # type=int
# [Optional]
# How long we should wait for any running
# threads/processes to stop before exiting (default: 5 seconds).
# stop_timeout: 5 # type=float | None
# [Optional]
# If set to True then the plugin will not monitor
# for new events. This is useful if you want to run a plugin in
# stateless mode and only leverage its actions, without triggering any
# events. Defaults to False.
# disable_monitor: False # type=bool
Triggered events#
Actions#
Module reference#
- class platypush.plugins.joplin.JoplinPlugin(*args, host: str, port: int = 41184, token: str, **kwargs)[source]#
Bases:
BaseNotePluginPlugin to interact with Joplin, a free and open-source note-taking application.
## Advised setup
Joplin is mainly intended as a desktop application, with support for synchronization across multiple devices via various backends.
This plugin is designed to interact with the Joplin API exposed by the “desktop” application - the same one used by the Joplin Web Clipper extension.
This can be achieved in two ways:
Using the Joplin desktop application
Using Joplin in headless mode
### Using the Joplin desktop application
To use the Joplin desktop application, you need to enable the Web Clipper service in the Joplin settings. This will expose an HTTP API on the port 41184 on your local machine.
Note that the Joplin desktop application must be running for this plugin to work, and by default it will only accept requests from the local machine.
If you want to run the Platypush server on a different machine, you can e.g. open an SSH tunnel to the Joplin machine:
ssh -L 41184:localhost:41184 user@joplin-machine
### Using Joplin in headless mode
The downside of the Joplin desktop application is that it must be running on a live desktop session for the plugin to work. Unfortunately, Joplin does not provide an official headless mode, but there is a community project that allows you to run Joplin in a Docker container in headless mode, exposing the same HTTP API as the desktop application.
It’s first advised to run and configure your Joplin on the desktop app. Then locate the Joplin profile directory (usually
~/.config/joplin-desktopon Linux, but depending on the installed version it could also be namedJoplinorjoplin) and copy the file namedsettings.jsonto~/.config/joplin/settings.jsonon the machine where you run the headless Joplin container.Optionally, you can also copy the
database.sqlitefile from the desktop Joplin profile directory to the headless Joplin profile, but make sure that the desktop instance and the headless instance run the same version of Joplin, otherwise you might run into database incompatibility issues.#### Credentials
If you opt not to copy the database file to the headless Joplin profile, then you may have to manually set up the credentials in a separate file named
secrets.jsonin the Joplin profile directory. This will contain both the API token and the passwords for any connected synchronization services. For example:{ "api.token": "your_api_token_here", "sync.1.password": "your_sync_password_here" }
Note:
The
api.tokenfield is mandatory, and it should match the one you configure in this plugin.sync.<n>.passwordshould be such that<n>is the index of the synchronization target configured insettings.json(e.g. Dropbox, Nextcloud, S3, Joplin Cloud, a local Joplin server, etc.).
#### Running the service
docker run --rm \ --name joplin-headless \ -v ~/.config/joplin:/home/node/.config/joplin \ -v ~/.config/joplin/secrets.json:/run/secrets/joplin-config.json:ro \ -p 41184:80 \ jspiers/headless-joplin:2.13.2-node-20.11.1
#### Synchronization
Unlike the Joplin desktop application, the headless Joplin instance does not provide a periodic synchronization mechanism. But you can schedule a cronjob to periodically run synchronization each e.g. 5 minutes:
crontab -e # Add the following line to the crontab file */5 * * * * /usr/bin/docker exec joplin-headless joplin sync
It is advised to have at least a remote synchronization target, and have the same target configured both in the Joplin desktop or mobile application and in the headless instance, so that you can keep your notes in sync across all of your devices, even though this plugin will only interact with the headless instance.
- __init__(*args, host: str, port: int = 41184, token: str, **kwargs)[source]#
- Parameters:
host – The hostname or IP address of your Joplin application.
port – The port number of your Joplin application (default: 41184).
token – The access token of your Joplin server.
- create_collection(title: str, *args, description: str | None = None, parent: Any | None = None, **kwargs) dict#
Create a new note collection with the specified title and description.
- Parameters:
title – The title of the new collection.
description – Optional description for the new collection.
parent – Optional parent collection ID to create a sub-collection.
- Returns:
The created collection as a dictionary, format:
{ "collections": [ { "description": "Notes related to work projects", "id": 1235, "title": "Work Notes" } ], "created_at": "2020-01-01T00:00:00+00:00", "description": "A collection of my important notes", "id": 1234, "notes": [ { "content": "Note content goes here", "created_at": "2023-10-01T12:00:00Z", "id": 2345, "title": "My Important Note", "updated_at": "2023-10-02T12:00:00Z" } ], "parent": { "description": "A top-level collection of all notes", "id": 1233, "title": "All Notes" }, "title": "My Notes", "updated_at": "2020-01-01T00:00:00+00:00" }
- create_note(title: str, content: str, *args, description: str | None = None, collection: Any | None = None, geo: Dict[str, float | None] | None = None, source: dict | None = None, author: str | None = None, **kwargs) dict#
Create a new note with the specified title and content.
- Parameters:
title – The title of the new note.
content – The content of the new note.
description – Optional description for the note.
collection – Optional collection ID to add the note to.
geo – Optional geographical coordinates as a dictionary with the fields
latitude,longitude, andaltitude.source – Optional source information for the note, with at least one of the fields
url,nameorapp. By default, the source isplatypushand the app istech.platypush.author – Optional author of the note.
- Returns:
The created note as a dictionary, format:
{ "altitude": 30.0, "author": "John Doe", "content": "Note content goes here", "created_at": "2020-01-01T00:00:00+00:00", "description": "This note contains important information.", "digest": "abc123def456", "id": 2345, "latitude": 37.7749, "longitude": -122.4194, "parent": { "description": "A collection of my important notes", "id": 1234, "title": "My Notes" }, "source": { "app": "My Note App", "name": "My Note Source", "url": "https://example.com/note" }, "tags": [ "important", "work" ], "title": "My Important Note", "updated_at": "2020-01-01T00:00:00+00:00" }
- delete_collection(collection_id: Any, *args, **kwargs)#
Delete a note collection by ID.
- Parameters:
collection_id – The ID of the collection to delete.
- delete_note(note_id: Any, *args, **kwargs)#
Delete a note by ID.
- Parameters:
note_id – The ID of the note to delete.
- edit_collection(collection_id: Any, *args, title: str | None = None, description: str | None = None, parent: Any | None = None, **kwargs) dict#
Edit an existing note collection by ID.
- Parameters:
collection_id – The ID of the collection to edit.
title – New title for the collection.
description – New description for the collection.
parent – New parent collection ID to move the collection under.
- Returns:
The updated collection as a dictionary, format:
{ "collections": [ { "description": "Notes related to work projects", "id": 1235, "title": "Work Notes" } ], "created_at": "2020-01-01T00:00:00+00:00", "description": "A collection of my important notes", "id": 1234, "notes": [ { "content": "Note content goes here", "created_at": "2023-10-01T12:00:00Z", "id": 2345, "title": "My Important Note", "updated_at": "2023-10-02T12:00:00Z" } ], "parent": { "description": "A top-level collection of all notes", "id": 1233, "title": "All Notes" }, "title": "My Notes", "updated_at": "2020-01-01T00:00:00+00:00" }
- edit_note(note_id: Any, *args, title: str | None = None, content: str | None = None, description: str | None = None, collection: Any | None = None, geo: Dict[str, float | None] | None = None, source: dict | None = None, author: str | None = None, **kwargs) dict#
Edit an existing note by ID.
- Parameters:
note_id – The ID of the note to edit.
title – New title for the note.
content – New content for the note.
description – Optional new description for the note.
collection – New collection ID to move the note under.
geo – Optional geographical coordinates as a dictionary with the fields
latitude,longitude, andaltitude.source – Optional source information for the note, with at least one of the fields
url,nameorapp.author – Optional author of the note.
- Returns:
The updated note as a dictionary, format:
{ "altitude": 30.0, "author": "John Doe", "content": "Note content goes here", "created_at": "2020-01-01T00:00:00+00:00", "description": "This note contains important information.", "digest": "abc123def456", "id": 2345, "latitude": 37.7749, "longitude": -122.4194, "parent": { "description": "A collection of my important notes", "id": 1234, "title": "My Notes" }, "source": { "app": "My Note App", "name": "My Note Source", "url": "https://example.com/note" }, "tags": [ "important", "work" ], "title": "My Important Note", "updated_at": "2020-01-01T00:00:00+00:00" }
- get_collection(collection_id: Any, *args, **kwargs) dict#
Get a single note collection by ID.
- Parameters:
collection_id – The ID of the collection to retrieve.
- Returns:
The collection as a dictionary, format:
{ "collections": [ { "description": "Notes related to work projects", "id": 1235, "title": "Work Notes" } ], "created_at": "2020-01-01T00:00:00+00:00", "description": "A collection of my important notes", "id": 1234, "notes": [ { "content": "Note content goes here", "created_at": "2023-10-01T12:00:00Z", "id": 2345, "title": "My Important Note", "updated_at": "2023-10-02T12:00:00Z" } ], "parent": { "description": "A top-level collection of all notes", "id": 1233, "title": "All Notes" }, "title": "My Notes", "updated_at": "2020-01-01T00:00:00+00:00" }
- get_collections(*args, limit: int | None = None, offset: int | None = 0, sort: Dict[str, bool] | None = None, filter: Dict[str, Any] | None = None, fetch: bool = False, **kwargs) Iterable[dict]#
Get note collections from the cache or fetch them from the backend.
- Parameters:
limit – Maximum number of collections to retrieve (default: None, meaning no limit).
offset – Offset to start retrieving collections from (default: 0).
sort – A dictionary specifying the fields to sort by and their order. Example: {‘created_at’: True} sorts by creation date in ascending order, while {‘created_at’: False} sorts in descending order.
filter – A dictionary specifying filters to apply to the collections, in the form of a dictionary where the keys are field names and the values are regular expressions to match against the field values.
fetch – If True, always fetch the latest collections from the backend, regardless of the cache state (default: False).
kwargs – Additional keyword arguments to pass to the fetch method.
- Returns:
An iterable of note collections, format:
[ { "collections": [ { "description": "Notes related to work projects", "id": 1235, "title": "Work Notes" } ], "created_at": "2020-01-01T00:00:00+00:00", "description": "A collection of my important notes", "id": 1234, "notes": [ { "content": "Note content goes here", "created_at": "2023-10-01T12:00:00Z", "id": 2345, "title": "My Important Note", "updated_at": "2023-10-02T12:00:00Z" } ], "parent": { "description": "A top-level collection of all notes", "id": 1233, "title": "All Notes" }, "title": "My Notes", "updated_at": "2020-01-01T00:00:00+00:00" } ]
- get_note(note_id: Any, *args, **kwargs) dict#
Get a single note and its full content by ID.
- Parameters:
note_id – The ID of the note to retrieve.
- Returns:
The note as a dictionary, format:
{ "altitude": 30.0, "author": "John Doe", "content": "Note content goes here", "created_at": "2020-01-01T00:00:00+00:00", "description": "This note contains important information.", "digest": "abc123def456", "id": 2345, "latitude": 37.7749, "longitude": -122.4194, "parent": { "description": "A collection of my important notes", "id": 1234, "title": "My Notes" }, "source": { "app": "My Note App", "name": "My Note Source", "url": "https://example.com/note" }, "tags": [ "important", "work" ], "title": "My Important Note", "updated_at": "2020-01-01T00:00:00+00:00" }
- get_notes(*args, limit: int | None = None, offset: int | None = 0, sort: Dict[str, bool] | None = None, filter: Dict[str, Any] | None = None, fetch: bool = False, **kwargs) Iterable[dict]#
Get notes from the cache or fetch them from the backend.
- Parameters:
limit – Maximum number of collections to retrieve (default: None, meaning no limit).
offset – Offset to start retrieving collections from (default: 0).
sort – A dictionary specifying the fields to sort by and their order. Example: {‘created_at’: True} sorts by creation date in ascending order, while {‘created_at’: False} sorts in descending order.
filter – A dictionary specifying filters to apply to the notes, in the form of a dictionary where the keys are field names and the values are regular expressions to match against the field values.
fetch – If True, always fetch the latest collections from the backend, regardless of the cache state (default: False).
kwargs – Additional keyword arguments to pass to the fetch method.
- Returns:
An iterable of notes, format:
[ { "altitude": 30.0, "author": "John Doe", "content": "Note content goes here", "created_at": "2020-01-01T00:00:00+00:00", "description": "This note contains important information.", "digest": "abc123def456", "id": 2345, "latitude": 37.7749, "longitude": -122.4194, "parent": { "description": "A collection of my important notes", "id": 1234, "title": "My Notes" }, "source": { "app": "My Note App", "name": "My Note Source", "url": "https://example.com/note" }, "tags": [ "important", "work" ], "title": "My Important Note", "updated_at": "2020-01-01T00:00:00+00:00" } ]
- reset_sync()#
Reset the sync state.
Reset the last sync time to None, forcing a full resync on the next call to
sync(), which in turn will re-trigger all notes and collections events.Clear the local notes and collections cache.
- search(*args, query: str, type: str = 'note', include_terms: Dict[str, Any] | None = None, exclude_terms: Dict[str, Any] | None = None, created_before: datetime | None = None, created_after: datetime | None = None, updated_before: datetime | None = None, updated_after: datetime | None = None, limit: int | None = None, offset: int | None = 0, **kwargs)#
Search for notes or collections based on the provided query and filters.
In most of the cases (but it depends on the backend) double-quoted search terms will match exact phrases, while unquoted queries will match any of the words in the query.
Wildcards (again, depending on the backend) in the search terms are also supported.
- Parameters:
query – The search query string (it will be searched in all the fields).
type – The type of items to search for -
note,collection, ortag(default:note).include_terms – Optional dictionary of terms to include in the search. The keys are field names and the values are strings to match against.
exclude_terms – Optional dictionary of terms to exclude from the search. The keys are field names and the values are strings to exclude from the results.
created_before – Optional datetime ISO string or UNIX timestamp to filter items created before this date.
created_after – Optional datetime ISO string or UNIX timestamp to filter items created after this date.
updated_before – Optional datetime ISO string or UNIX timestamp to filter items updated before this date.
updated_after – Optional datetime ISO string or UNIX timestamp to filter items updated after this date.
limit – Maximum number of items to retrieve (default: None, meaning no limit, or depending on the default limit of the backend).
offset – Offset to start retrieving items from (default: 0).
- Returns:
An iterable of matching items, format:
{ "has_more": false "results" [ { "type": "note", "item": { "id": "note-id", "title": "Note Title", "content": "Note content...", "created_at": "2023-10-01T12:00:00Z", "updated_at": "2023-10-01T12:00:00Z", // ... } } ] }
- sync(*args, **kwargs)#
Synchronize the notes and collections with the backend. This method is called periodically based on the
poll_intervalsetting. Ifpoll_intervalis zero or null, you can manually call this method to synchronize the notes and collections.