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
31 changes: 31 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Tests

on:
push:
branches: [master]
pull_request:
branches: [master]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12']

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[test]"

- name: Run tests
run: |
python -m pytest tests/ -v --ignore=tests/test_integration.py
24 changes: 18 additions & 6 deletions ntfy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import logging
import logging as _logging
from getpass import getuser
from os import getcwd, path, name
from socket import gethostname
Expand All @@ -9,13 +9,22 @@
__version__ = '2.7.1'

_user_home = path.expanduser('~')
_cwd = getcwd()
try:
_cwd = getcwd()
except OSError:
_cwd = '[unknown]'
if name != 'nt' and _cwd.startswith(_user_home):
default_title = '{}@{}:{}'.format(
getuser(), gethostname(), path.join('~', _cwd[len(_user_home) + 1:]))
else:
default_title = '{}@{}:{}'.format(getuser(), gethostname(), _cwd)

# Backend name aliases to avoid conflicts with third-party packages.
# Maps old/conflicting backend names to their renamed modules.
BACKEND_ALIASES = {
'telegram': 'telegram_send',
}


def notify(message, title, config=None, **kwargs):
from .config import load_config
Expand All @@ -38,13 +47,16 @@ def notify(message, title, config=None, **kwargs):
elif 'title' in backend_config:
del backend_config['title']

# Resolve backend aliases
backend = BACKEND_ALIASES.get(backend, backend)

try:
notifier = import_module('ntfy.backends.{}'.format(backend))
except ImportError:
try:
notifier = import_module(backend)
except ImportError:
logging.getLogger(__name__).error(
_logging.getLogger(__name__).error(
'Invalid backend {}'.format(backend))
ret = 1
continue
Expand Down Expand Up @@ -73,15 +85,15 @@ def notify(message, title, config=None, **kwargs):
missing_args = required_args - set(backend_config)

if unknown_args:
logging.getLogger(__name__).error(
_logging.getLogger(__name__).error(
'Got unknown arguments: {}'.format(unknown_args))

if missing_args:
logging.getLogger(__name__).error(
_logging.getLogger(__name__).error(
'Missing arguments: {}'.format(missing_args))

if not any([unknown_args, missing_args]):
logging.getLogger(__name__).error(
_logging.getLogger(__name__).error(
'Failed to send notification using {}'.format(backend),
exc_info=True)

Expand Down
2 changes: 1 addition & 1 deletion ntfy/backends/notifico.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ def notify(title, message, retcode=None, webhook=None):
params={
'payload': '{title}\n{message}'.format(
title=title, message=message)
})
}, timeout=10)
response.raise_for_status()
1 change: 1 addition & 0 deletions ntfy/backends/ntfy_sh.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ def notify(title, message, topic, host='https://ntfy.sh', user=None, password=No
headers=dict(title=title),
data=message,
**auth_kwarg,
timeout=10,
)
2 changes: 1 addition & 1 deletion ntfy/backends/prowl.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,6 @@ def notify(title,
resp = requests.post(
API_URL, data=data, headers={
'User-Agent': USER_AGENT,
})
}, timeout=10)

resp.raise_for_status()
2 changes: 1 addition & 1 deletion ntfy/backends/pushalot.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,5 @@ def notify(title,
data['IsSilent'] = 'True'

headers = {'User-Agent': USER_AGENT}
response = requests.post(PUSHALOT_API_URL, data=data, headers=headers)
response = requests.post(PUSHALOT_API_URL, data=data, headers=headers, timeout=10)
response.raise_for_status()
2 changes: 1 addition & 1 deletion ntfy/backends/pushbullet.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ def notify(title,
headers = {'Access-Token': access_token, 'User-Agent': USER_AGENT}

resp = requests.post(
'https://api.pushbullet.com/v2/pushes', data=data, headers=headers)
'https://api.pushbullet.com/v2/pushes', data=data, headers=headers, timeout=10)

resp.raise_for_status()
2 changes: 1 addition & 1 deletion ntfy/backends/pushjet.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,6 @@ def notify(title,
if endpoint is None:
endpoint = 'https://api.pushjet.io'

resp = requests.post(endpoint + '/message', data=data, headers=headers)
resp = requests.post(endpoint + '/message', data=data, headers=headers, timeout=10)

resp.raise_for_status()
7 changes: 6 additions & 1 deletion ntfy/backends/pushover.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def notify(title,
url=None,
url_title=None,
html=False,
ttl=None,
retcode=None):
"""
Required parameters:
Expand Down Expand Up @@ -106,12 +107,16 @@ def notify(title,
else:
raise ValueError('priority must be an integer from -2 to 2')

if ttl is not None:
data['ttl'] = ttl

resp = requests.post(
'https://api.pushover.net/1/messages.json',
data=data,
headers={
'User-Agent': USER_AGENT,
})
},
timeout=10)

if resp.status_code == 429:
print("ntfy's default api_token has reached pushover's rate limit")
Expand Down
2 changes: 1 addition & 1 deletion ntfy/backends/simplepush.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ def notify(title, message, key, event=None, retcode=None):

endpoint = "https://api.simplepush.io"

resp = requests.post(endpoint + '/send', data=data, headers=headers)
resp = requests.post(endpoint + '/send', data=data, headers=headers, timeout=10)

resp.raise_for_status()
1 change: 1 addition & 0 deletions ntfy/backends/slack_webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ def notify(title, message, url, user, **kwargs):
}
],
},
timeout=10,
)
File renamed without changes.
31 changes: 13 additions & 18 deletions ntfy/backends/xmpp.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import asyncio
import logging
import os

import sleekxmpp
import slixmpp


class NtfySendMsgBot(sleekxmpp.ClientXMPP):
class NtfySendMsgBot(slixmpp.ClientXMPP):
"""
Modified the commented sleekxmpp example:
http://sleekxmpp.com/getting_started/sendlogout.html
Updated from sleekxmpp to slixmpp (async-based XMPP library).

NOTE: supplying mtype='chat' was required for
Google Hangouts to work
"""

def __init__(self, jid, password, recipient, title, message, mtype=None):
super(NtfySendMsgBot, self).__init__(jid, password)
super().__init__(jid, password)

self.recipient = recipient
self.title = title
Expand All @@ -23,10 +23,9 @@ def __init__(self, jid, password, recipient, title, message, mtype=None):

self.add_event_handler("session_start", self.start)

def start(self, event):

async def start(self, event):
self.send_presence()
self.get_roster()
await self.get_roster()
msg_args = {
'mto': self.recipient,
'msubject': self.title,
Expand All @@ -37,7 +36,7 @@ def start(self, event):

self.send_message(**msg_args)

self.disconnect(wait=True)
self.disconnect()


def notify(title,
Expand Down Expand Up @@ -72,16 +71,12 @@ def notify(title,

xmpp_bot = NtfySendMsgBot(jid, password, recipient, title, message, mtype)

# NOTE: Below plugins weren't needed for Google Hangouts
# but may be useful (from original sleekxmpp example)
# xmpp_bot.register_plugin('xep_0030') # Service Discovery
# xmpp_bot.register_plugin('xep_0199') # XMPP Ping

if path_to_certs and os.path.isdir(path_to_certs):
xmpp_bot.ca_certs = path_to_certs

# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp_bot.connect(*([(hostname, int(port)) if hostname else []])):
xmpp_bot.process(block=True)
if hostname:
xmpp_bot.connect((hostname, int(port)))
else:
logging.getLogger(__name__).error('Unable to connect', exc_info=True)
xmpp_bot.connect()

xmpp_bot.process()
23 changes: 17 additions & 6 deletions ntfy/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ def auto_done(args):
args.longer_than))
if args.unfocused_only:
print('export AUTO_NTFY_DONE_UNFOCUSED_ONLY=-b')
ignore = getattr(args, 'config_data', {}).get('auto_ntfy_done_ignore')
if ignore:
print('export AUTO_NTFY_DONE_IGNORE="{}"'.format(ignore))
if args.shell == 'bash':
print('source {}'.format(sh_quote(scripts['bash-preexec.sh'])))
print('source {}'.format(sh_quote(scripts['auto-ntfy-done.sh'])))
Expand Down Expand Up @@ -207,6 +210,11 @@ def __call__(self, parser, args, values, option_string=None):
'-t',
'--title',
help='a title for the notification (default: {})'.format(default_title))
parser.add_argument(
'--timeout',
type=int,
default=None,
help='How long the notification stays visible (seconds)')

subparsers = parser.add_subparsers()

Expand Down Expand Up @@ -243,12 +251,11 @@ def default_sender(args):
nargs=3,
help="Format and send cmd, retcode & duration instead of running command. "
"Used internally by shell-integration")
if psutil is not None:
done_parser.add_argument(
'-p',
'--pid',
type=int,
help="Watch a PID instead of running a new command")
done_parser.add_argument(
'-p',
'--pid',
type=int,
help="Watch a PID instead of running a new command")
done_parser.add_argument(
'-o',
'--stdout',
Expand Down Expand Up @@ -353,7 +360,11 @@ def main(cli_args=None):
if getattr(args, 'func', None) == run_cmd and 'hide_command' in config:
args.hide_command = config['hide_command']

if getattr(args, 'timeout', None) is not None:
args.option.setdefault(None, {})['timeout'] = args.timeout

if hasattr(args, 'func'):
args.config_data = config
message, retcode = args.func(args)
if message is None:
return 0
Expand Down
31 changes: 31 additions & 0 deletions ntfy/logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import logging

from . import notify
from .config import load_config


class NtfyHandler(logging.Handler):
"""A logging handler that sends notifications via ntfy.

Example usage::

import logging
from ntfy.logging import NtfyHandler

logger = logging.getLogger(__name__)
logger.addHandler(NtfyHandler(title="My App"))
logger.error("Something went wrong!") # sends notification
"""

def __init__(self, title="ntfy", level=logging.ERROR, config=None):
super().__init__(level=level)
self.title = title
self._config = config

def emit(self, record):
try:
message = self.format(record)
config = self._config if self._config is not None else load_config()
notify(message=message, title=self.title, config=config)
except Exception:
self.handleError(record)
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
extra_deps = {
':sys_platform == "win32"': ['pywin32'],
':sys_platform == "darwin"': ['pyobjc-core', 'pyobjc'],
'xmpp': ['sleekxmpp', 'dnspython3'],
'xmpp': ['slixmpp', 'dnspython'],
'telegram': ['telegram-send'],
'instapush': ['instapush'],
'emoji': ['emoji >= 1.6.2'],
Expand All @@ -17,7 +17,7 @@
'rocketchat':['rocketchat-API'],
'matrix':['matrix_client'],
}
test_deps = ['mock', 'sleekxmpp', 'emoji', 'psutil']
test_deps = ['slixmpp', 'emoji', 'psutil']

long_description = "See the repo readme for mor information"

Expand Down
2 changes: 1 addition & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from time import time
from unittest import TestCase, main

from mock import MagicMock, Mock, patch
from unittest.mock import MagicMock, Mock, patch
from ntfy.cli import main as ntfy_main
from ntfy.cli import auto_done, run_cmd

Expand Down
4 changes: 2 additions & 2 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from sys import version_info
from unittest import TestCase, main, skipIf

from mock import mock_open, patch
from unittest.mock import mock_open, patch
from ntfy.config import DEFAULT_CONFIG, load_config

py = version_info.major
Expand All @@ -21,7 +21,7 @@ class TestLoadConfig(TestCase):
@patch(builtin_module + '.open', mock_open_dne_error)
def test_default_config(self):
config = load_config(DEFAULT_CONFIG)
self.assertEqual(config, {})
self.assertEqual(config, {'backends': ['default']})

@patch(builtin_module + '.open', mock_open())
@patch('ntfy.config.safe_load')
Expand Down
2 changes: 1 addition & 1 deletion tests/test_integration.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from sys import modules, version_info
from unittest import TestCase, main

from mock import MagicMock, mock_open, patch
from unittest.mock import MagicMock, mock_open, patch
from ntfy.cli import main as ntfy_main

py = version_info.major
Expand Down
Loading