Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions ntfy_push_core/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
====================================
ntfy.sh Push Notifications Core
====================================

.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3

This module integrates Odoo with the **ntfy.sh** protocol to provide real-time,
low-latency, and battery-efficient push notifications. It is designed as a
privacy-first alternative to proprietary push services.

.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! Developed by nurefexc (https://nurefexc.com) !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

Key Features
============

* **Zero-Configuration for Users**: Subscription URLs are automatically generated
when a user switches their notification preference to 'ntfy'.
* **Asynchronous Processing**: Notifications are handled via an internal queue
and processed in the background to ensure a snappy UI experience.
* **Branded Notifications**: Push messages include the developer's branding (nurefexc)
and support Markdown formatting.
* **Smart Priority**: All notifications are delivered with **High Priority** to
ensure they bypass system battery optimizations.
* **Deep Linking**: One-tap access from the notification directly to the Odoo record.
* **Integrated Settings**: Server configuration is seamlessly integrated into the
Odoo **Discuss** settings.

Configuration
=============

1. Navigate to **Settings > General Settings**.
2. Locate the **Discuss** section.
3. Under **ntfy.sh Push Notifications**, set your **ntfy Server URL** (default is ``https://ntfy.sh``).
4. Ensure the scheduled action **"ntfy: Notification Queue Worker"** is active.

Usage
=====

1. Go to your **User Preferences** (My Profile).
2. Change the **Notification** field to ``ntfy.sh (Push Notification)``.
3. The **ntfy Subscription** field will appear and automatically populate.
4. Hover over the field to see the setup instructions in the tooltip.
5. Use the **Refresh icon** next to the URL if you need to rotate your security token.
6. Copy the URL into your ntfy mobile app (Android/iOS).

Technical Notes
===============

* **Model**: ``ntfy.notification.queue`` manages the outgoing message buffer.
* **Security**: Topic IDs are generated using a SHA256 hash of the database UUID
and user-specific seeds.
* **UI**: Extends ``base.view_users_form_simple_modif`` for a seamless profile experience.

Credits
=======

Authors
-------

* nurefexc <https://nurefexc.com>

Contributors
------------

* nurefexc <https://nurefexc.com>

Maintainers
-----------

This module is maintained by **nurefexc**.

For professional Odoo development and support, visit `nurefexc.com <https://nurefexc.com>`_.
4 changes: 4 additions & 0 deletions ntfy_push_core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright 2026 nurefexc (https://nurefexc.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0).

from . import models
19 changes: 19 additions & 0 deletions ntfy_push_core/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2026 nurefexc (https://nurefexc.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0).
{
"name": "ntfy.sh Push Notifications",
"version": "17.0.1.0.0",
"summary": "Asynchronous push notifications via ntfy.sh protocol",
"author": "nurefexc, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/social",
"category": "Technical/Communication",
"depends": ["mail"],
"data": [
"security/ir.model.access.csv",
"data/ir_cron_data.xml",
"views/res_users_views.xml",
"views/res_config_settings_views.xml",
],
"installable": True,
"license": "LGPL-3",
}
13 changes: 13 additions & 0 deletions ntfy_push_core/data/ir_cron_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="ir_cron_ntfy_queue_worker" model="ir.cron">
<field name="name">ntfy: Notification Queue Worker</field>
<field name="model_id" ref="model_ntfy_notification_queue" />
<field name="state">code</field>
<field name="code">model.cron_process_ntfy_queue(batch_limit=100)</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">minutes</field>
<field name="numbercall">-1</field>
</record>
</odoo>
7 changes: 7 additions & 0 deletions ntfy_push_core/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Copyright 2026 nurefexc (https://nurefexc.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0).

from . import res_config_settings
from . import res_users
from . import mail_thread
from . import ntfy_notification_queue
44 changes: 44 additions & 0 deletions ntfy_push_core/models/mail_thread.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright 2026 nurefexc (https://nurefexc.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0).

from odoo import models, tools


class MailThread(models.AbstractModel):
_inherit = "mail.thread"

def _message_post_after_hook(self, message, msg_vals):
res = super()._message_post_after_hook(message, msg_vals)
self._enqueue_ntfy_notification(message)
return res

def _enqueue_ntfy_notification(self, message):
"""Minimalist queueing with fixed high priority"""
ntfy_users = message.partner_ids.user_ids.filtered(
lambda u: u.notification_type == "ntfy" and u.id != self.env.uid
)
if not ntfy_users:
return

ntfy_users._check_ntfy_url_consistency()

body_text = tools.html2plaintext(message.body or "")
base_url = (
self.env["ir.config_parameter"].sudo().get_param("web.base.url").rstrip("/")
)
link = f"{base_url}/web#model={self._name}&id={self.id}"
title = (
f"{message.author_id.name or 'Odoo'}:"
"{message.record_name or self._description}"
)
queue_vals = [
{
"res_user_id": user.id,
"title": title,
"body": body_text[:250],
"click_url": link,
}
for user in ntfy_users
]

self.env["ntfy.notification.queue"].create(queue_vals)
64 changes: 64 additions & 0 deletions ntfy_push_core/models/ntfy_notification_queue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright 2026 nurefexc (https://nurefexc.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0).

import logging

import requests

from odoo import api, fields, models

_logger = logging.getLogger(__name__)


class NtfyNotificationQueue(models.Model):
_name = "ntfy.notification.queue"
_description = "ntfy Queue"
_order = "create_date desc"

res_user_id = fields.Many2one(
"res.users", string="Recipient", required=True, ondelete="cascade"
)
title = fields.Char(required=True)
body = fields.Text()
click_url = fields.Char("Action URL")
state = fields.Selection(
[("pending", "Pending"), ("sent", "Sent"), ("error", "Error")],
default="pending",
index=True,
)
error_log = fields.Text(readonly=True)

@api.model
def cron_process_ntfy_queue(self, batch_limit=100):
"""Processes queue with high priority"""
records = self.search(
[("state", "in", ["pending", "error"])], limit=batch_limit
)

for record in records:
user = record.res_user_id
if not user.ntfy_topic_url:
continue

headers = {
"Title": record.title.encode("utf-8"),
"Priority": "4",
"Tags": "odoo,bell",
"Click": record.click_url,
}

try:
response = requests.post(
user.ntfy_topic_url,
data=record.body.encode("utf-8"),
headers=headers,
timeout=5,
)
if response.status_code == 200:
record.state = "sent"
else:
record.write(
{"state": "error", "error_log": f"HTTP {response.status_code}"}
)
except Exception as e:
record.write({"state": "error", "error_log": str(e)})
15 changes: 15 additions & 0 deletions ntfy_push_core/models/res_config_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2026 nurefexc (https://nurefexc.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0).

from odoo import fields, models


class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"

ntfy_server_url = fields.Char(
string="ntfy Server URL",
help="The base URL of the ntfy server (e.g., https://ntfy.sh)",
config_parameter="ntfy.server_url",
default="https://ntfy.sh",
)
72 changes: 72 additions & 0 deletions ntfy_push_core/models/res_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Copyright 2026 nurefexc (https://nurefexc.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0).

import hashlib
import time

from odoo import api, fields, models


class ResUsers(models.Model):
_inherit = "res.users"

notification_type = fields.Selection(
selection_add=[("ntfy", "ntfy.sh (Push Notification)")],
ondelete={"ntfy": "set default"},
)

ntfy_topic_url = fields.Char(
string="ntfy Topic URL",
readonly=True,
copy=False,
help="Paste this URL into your ntfy mobile app.",
)
ntfy_last_server_url = fields.Char(string="Last ntfy Server", readonly=True)

@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
for user in res:
if user.notification_type == "ntfy" and not user.ntfy_topic_url:
user.action_generate_ntfy_url()
return res

def write(self, vals):
res = super().write(vals)
# If switching to ntfy and no URL, or if something changed that requires it
if "notification_type" in vals and vals["notification_type"] == "ntfy":
for user in self:
if not user.ntfy_topic_url:
user.action_generate_ntfy_url()
return res

def action_generate_ntfy_url(self):
"""Generates the secure SHA224 hashed topic URL"""
self.ensure_one()
config = self.env["ir.config_parameter"].sudo()
base_url = config.get_param("ntfy.server_url", "https://ntfy.sh").rstrip("/")
db_uuid = config.get_param("database.uuid", "shared")

seed = f"{db_uuid}-{self.id}-{time.time()}"
secure_hash = hashlib.sha224(seed.encode()).hexdigest()
topic_id = f"odoo-{self.id}-{secure_hash}"

self.write(
{
"ntfy_topic_url": f"{base_url}/{topic_id}",
"ntfy_last_server_url": base_url,
}
)

def _check_ntfy_url_consistency(self):
"""Auto-sync when server config changes."""
current_base = (
self.env["ir.config_parameter"]
.sudo()
.get_param("ntfy.server_url", "https://ntfy.sh")
.rstrip("/")
)
for user in self:
if user.notification_type == "ntfy":
if not user.ntfy_topic_url or user.ntfy_last_server_url != current_base:
user.action_generate_ntfy_url()
3 changes: 3 additions & 0 deletions ntfy_push_core/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
2 changes: 2 additions & 0 deletions ntfy_push_core/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_ntfy_queue_user,ntfy.queue.user,model_ntfy_notification_queue,base.group_user,1,1,1,1
Binary file added ntfy_push_core/static/description/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions ntfy_push_core/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright 2026 nurefexc (https://nurefexc.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0).

from . import test_ntfy_url
Loading