├── .coveragerc
├── .gitignore
├── LICENSE
├── MANIFEST.in
├── README.md
├── docs
├── .gitignore
├── Makefile
├── README.rst
├── conf.py
├── demo.gif
├── index.rst
├── logo.png
├── logo.xcf
├── make.bat
├── modules.rst
├── ntfy.backends.rst
├── ntfy.rst
└── requirements.txt
├── ntfy
├── __init__.py
├── backends
│ ├── __init__.py
│ ├── darwin.py
│ ├── default.py
│ ├── insta.py
│ ├── linux.py
│ ├── matrix.py
│ ├── notifico.py
│ ├── ntfy_sh.py
│ ├── prowl.py
│ ├── pushalot.py
│ ├── pushbullet.py
│ ├── pushjet.py
│ ├── pushover.py
│ ├── rocketchat.py
│ ├── simplepush.py
│ ├── slack.py
│ ├── slack_webhook.py
│ ├── systemlog.py
│ ├── telegram.py
│ ├── termux.py
│ ├── win32.py
│ └── xmpp.py
├── cli.py
├── config.py
├── data.py
├── default_config.py
├── icon.ico
├── icon.png
├── shell_integration
│ ├── auto-ntfy-done.sh
│ └── bash-preexec.sh
└── terminal.py
├── setup.cfg
├── setup.py
└── tests
├── __init__.py
├── test_cli.py
├── test_config.py
├── test_integration.py
├── test_notifico.py
├── test_ntfy.py
├── test_prowl.py
├── test_pushalot.py
├── test_pushbullet.py
├── test_pushjet.py
├── test_pushover.py
├── test_simplepush.py
├── test_systemlog.py
└── test_xmpp.py
/.coveragerc:
--------------------------------------------------------------------------------
1 | [report]
2 | exclude_lines =
3 | pragma: no cover
4 | except ImportError:
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[co]
2 | *.egg-info
3 | /build
4 | /dist
5 | *travis.log
6 | .eggs
7 | htmlcov
8 | *.orig
9 | .coverage
10 | cover
11 | venv/
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include MANIFEST.in
2 | include README.rst
3 | include LICENSE
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # About `ntfy`
2 |
3 | [](https://pypi.org/project/ntfy/)
4 | [](http://ntfy.readthedocs.org/en/stable/?badge=latest)
5 | [](https://saythanks.io/to/dschep)
6 |
7 | `ntfy` brings notification to your shell. It can automatically provide
8 | desktop notifications when long running commands finish or it can send
9 | push notifications to your phone when a specific command finishes.
10 | Confused? This video demonstrates some of this functionality:
11 |
12 | 
13 |
14 | ## Quickstart
15 |
16 | ``` shell
17 | $ sudo pip install ntfy
18 | $ ntfy send test
19 | # send a notification when the command `sleep 10` finishes
20 | # this sends the message '"sleep 10" succeeded in 0:10 minutes'
21 | $ ntfy done sleep 10
22 | $ ntfy -b pushover -o user_key t0k3n send 'Pushover test!'
23 | $ ntfy -t 'ntfy' send "Here's a custom notification title!"
24 | $ echo -e 'backends: ["pushover"]\npushover: {"user_key": "t0k3n"}' > ~/.ntfy.yml
25 | $ ntfy send "Pushover via config file!"
26 | $ ntfy done --pid 6379 # pid extra
27 | $ ntfy send ":tada: ntfy supports emoji! :100:" # emoji extra
28 | # Enable shell integration
29 | $ echo 'eval "$(ntfy shell-integration)"' >> ~/.bashrc
30 | ```
31 |
32 | ## Install
33 |
34 | The install technique in the quickstart is the suggested method of
35 | installation. It can be installed in a virtualenv, but with some
36 | caveats: Linux notifications require `--system-site-packages` for the
37 | virtualenv and OS X notifications don\'t work at all.
38 |
39 | **:penguin: NOTE:** [Linux Desktop
40 | Notifications](#linux-desktop-notifications---linux) require Python DBUS
41 | bindings. See [here](#linux-desktop-notifications---linux) for more
42 | info.
43 |
44 | ### Shell integration
45 |
46 | `ntfy` has support for **automatically** sending notifications when long
47 | running commands finish in bash and zsh. In bash it emulates zsh\'s
48 | preexec and precmd functionality with
49 | [rcaloras/bash-preexec](https://github.com/rcaloras/bash-preexec). To
50 | enable it add the following to your `.bashrc` or `.zshrc`:
51 |
52 | ``` shell
53 | eval "$(ntfy shell-integration)"
54 | ```
55 |
56 | By default it will only send notifications for commands lasting longer
57 | than 10 seconds and if the terminal is focused. Terminal focus works on
58 | X11(Linux) and with Terminal.app and iTerm2 on MacOS. Both options can
59 | be configured via the `--longer-than` and `--foreground-too` options.
60 |
61 | To avoid unnecessary notifications when running interactive programs,
62 | programs listed in `AUTO_NTFY_DONE_IGNORE` don\'t generate
63 | notifications. For example:
64 |
65 | ``` shell
66 | export AUTO_NTFY_DONE_IGNORE="vim screen meld"
67 | ```
68 |
69 | ### Extras
70 |
71 | `ntfy` has a few features that require extra dependencies.
72 |
73 | : - `ntfy done -p $PID` requires installing as
74 | `pip install ntfy[pid]`
75 | - [emoji](https://en.wikipedia.org/wiki/Emoji) support requires
76 | installing as `pip install ntfy[emoji]`
77 | - [XMPP](https://xmpp.org/) support requires installing as
78 | `pip install ntfy[xmpp]`
79 | - [Telegram](https://telegram.org/) support requires installing as
80 | `pip install ntfy[telegram]`
81 | - [Instapush](https://instapush.im/) support requires installing
82 | as `pip install ntfy[instapush]`
83 | - [Slack](https://slack.com/) support requires installing as
84 | `pip install ntfy[slack]`
85 | - [Slack Incoming webhook](https://slack.com/) - simpler slack
86 | implementation that doesn\'t have additional dependencies
87 | - [Rocket.Chat](https://Rocket.Chat) support requires installing
88 | as `pip install ntfy[rocketchat]`
89 |
90 | To install multiple extras, separate with commas: e.g.,
91 | `pip install ntfy[pid,emoji]`.
92 |
93 | ## Configuring `ntfy`
94 |
95 | `ntfy` is configured with a YAML file stored at `~/.ntfy.yml` or in
96 | standard platform specific locations:
97 |
98 | - Linux - `~/.config/ntfy/ntfy.yml`
99 | - macOS - `~/Library/Application Support/ntfy/ntfy.yml`
100 | - Windows - `C:\Users\\AppData\Local\dschep\ntfy.yml`
101 |
102 | ### Backends
103 |
104 | The backends key specifies what backends to use by default. Each backend
105 | has its own configuration, stored in a key of its own name. For example:
106 |
107 | ``` yaml
108 | ---
109 | backends:
110 | - pushover
111 | pushover:
112 | user_key: hunter2
113 | pushbullet:
114 | access_token: hunter2
115 | simplepush:
116 | key: hunter2
117 | slack:
118 | token: slacktoken
119 | recipient: "#slackchannel"
120 | xmpp:
121 | jid: "user@gmail.com"
122 | password: "xxxx"
123 | mtype: "chat"
124 | recipient: "me@jit.si"
125 | ```
126 |
127 | If you want mulitple configs for the same backend type, you can specify
128 | any name and then specify the backend with a backend key. For example:
129 |
130 | ``` yaml
131 | ---
132 | pushover:
133 | user_key: hunter2
134 | cellphone:
135 | backend: pushover
136 | user_key: hunter2
137 | ```
138 |
139 | See the backends below for available backends and options. As of v2.6.0
140 | `ntfy` also supports [3rd party backends](#3rd-party-backends)
141 |
142 | ### [Pushover](https://pushover.net) - `pushover`
143 |
144 | Required parameters:
145 |
146 | : - `user_key`
147 |
148 | Optional parameters:
149 |
150 | : - `sound`
151 | - `priority`
152 | - `expire`
153 | - `retry`
154 | - `callback`
155 | - `api_token` - use your own application token
156 | - `device` - target a device, if omitted, notification is sent to
157 | all devices
158 | - `url`
159 | - `url_title`
160 | - `html`
161 |
162 | ### [Pushbullet](https://pushbullet.com) - `pushbullet`
163 |
164 | Required parameter:
165 |
166 | : - `access_token` - Your Pushbullet access token, created at
167 |
168 |
169 | Optional parameters:
170 |
171 | : - `device_iden` - a device identifier, if omited, notification is
172 | sent to all devices
173 | - `email` - send notification to pushbullet user with the
174 | specified email or send an email if they aren\'t a pushullet
175 | user
176 |
177 | ### [Simplepush](https://simplepush.io) - `simplepush`
178 |
179 | Required parameter:
180 |
181 | : - `key` - Your Simplepush key, created by installing the Android
182 | App (no registration required) at
183 |
184 | Optional parameters:
185 |
186 | : - `event` - sets ringtone and vibration pattern for incoming
187 | notifications (can be defined in the simplepush app)
188 |
189 | ### XMPP - `xmpp`
190 |
191 | Requires parameters:
192 |
193 | : - `jid`
194 | - `password`
195 | - `recipient`
196 |
197 | Optional parameters
198 |
199 | : - `hostname` (if not from jid)
200 | - `port`
201 | - `path_to_certs`
202 | - `mtype`
203 |
204 | Requires extras, install like this: `pip install ntfy[xmpp]`.
205 |
206 | To verify the SSL certificates offered by a server: path_to_certs =
207 | \"path/to/ca/cert\"
208 |
209 | Without dnspython library installed, you will need to specify the server
210 | hostname if it doesn\'t match the jid.
211 |
212 | Specify port if other than 5222. NOTE: Ignored without specified
213 | hostname
214 |
215 | NOTE: Google Hangouts doesn\'t support XMPP since 2017
216 |
217 | ### [Telegram](https://telegram.org) - `telegram`
218 |
219 | Requires extras, install like this: `pip install ntfy[telegram]`.
220 |
221 | Requires `ntfy` to be installed as `ntfy[telegram]`. This backend is
222 | configured the first time you will try to use it:
223 | `ntfy -b telegram send "Telegram configured for ntfy"`.
224 |
225 | ### [Pushjet](https://pushjet.io/) - `pushjet`
226 |
227 | Required parameter:
228 |
229 | : - `secret` - The Pushjet service secret token, created with
230 |
231 |
232 | Optional parameters:
233 |
234 | : -
235 |
236 | `endpoint` - custom Pushjet API endpoint
237 |
238 | : (defaults to )
239 |
240 | - `level` - The importance level from 1(low) to 5(high)
241 |
242 | - `link`
243 |
244 | ### [Notifico](https://n.tkte.ch/) - `notifico`
245 |
246 | Required parameter:
247 |
248 | : -
249 |
250 | `webhook` - The webhook link, created at
251 |
252 | : (choose `Plain Text` service when creating the webhook)
253 |
254 | ### [Slack](https://slack.com) - `slack`
255 |
256 | Requires extras, install like this: `pip install ntfy[slack]`.
257 |
258 | Required parameter:
259 |
260 | : - `token` - The Slack service secret token, either a legacy user
261 | token created at
262 | or a
263 | token obtained by creating an app at
264 | with `chat:write:bot`
265 | scope and linking it to a workspace.
266 | - `recipient` - The Slack channel or user to send notifications
267 | to. If you use the `#` symbol the message is send to a Slack
268 | channel and if you use the `@` symbol the message is send to a
269 | Slack user.
270 |
271 | [Slack Incoming Webhook](https://slack.com) - `slack_webhook`
272 | \~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~
273 | Required parameter: \* `url` - the URL of the incoming webhook \*
274 | `user` - The Slack channel or user to send notifications to
275 |
276 | ### [Instapush](https://instapush.im/) - `insta`
277 |
278 | Requires extras, install like this `pip install ntfy[instapush]`.
279 |
280 | Instapush does not support notification title. It sends template-driven
281 | notifications, so you have to setup you events on the dashboard first.
282 | The backend is called insta due to homonymy with the instapush python
283 | wrapper
284 |
285 | Required parameters:
286 |
287 | : - `appid` - The application id
288 | - `secret` - The application secret
289 | - `event_name` - The instapush event to be used
290 | - `trackers` - The array of trakers to use
291 |
292 | Note on trackers: Trackers are placeholders for events (a sort of
293 | notification template). If you defined more than one tracker in your
294 | event you\'ll have to provide more messages. At the moment, the only way
295 | to do so is to separate each message with a colon (:) character. You can
296 | also escape the separator character: Example:
297 |
298 | ``` shell
299 | ntfy -b insta send "message1:message2"
300 | ntfy -b insta send "message1:message2\:with\:colons"
301 | ```
302 |
303 | ### [Prowl](https://www.prowlapp.com/) - `prowl`
304 |
305 | Optional parameters:
306 |
307 | : - `api_key`
308 | - `provider_key`
309 | - `priority`
310 | - `url`
311 |
312 | ### [Linux Desktop Notifications](https://developer.gnome.org/notification-spec/) - `linux`
313 |
314 | Works via [dbus]{.title-ref}, works with most DEs like Gnome, KDE, XFCE
315 | and with libnotify.
316 |
317 | The following dependecies should be installed.
318 |
319 | ``` shell
320 | $ sudo apt install python-dbus # on ubuntu/debian
321 | ```
322 |
323 | You will need to install some font that supports emojis (in Debian
324 | [fonts-symbola]{.title-ref} or Gentoo
325 | [media-fonts/symbola]{.title-ref}).
326 |
327 | Optional parameters:
328 |
329 | : - `icon` - Specifies path to the notification icon, empty string
330 | for no icon.
331 | - `urgency` - Specifies the urgency level (low, normal, critical).
332 | - `transient` - Skip the history (exp: the Gnome message tray)
333 | (true, false).
334 | - `soundfile` - Specifies the notification sound file (e.g.
335 | /usr/share/sounds/notif.wav).
336 | - `timeout` - Specifies notification expiration time level (-1 -
337 | system default, 0 - never expire).
338 |
339 | ### Windows Desktop Notifications - `win32`
340 |
341 | Uses `pywin32`.
342 |
343 | ### Mac OS X Notification Center - `darwin`
344 |
345 | Requires `ntfy` to be installed globally (not in a virtualenv).
346 |
347 | ### System log - `systemlog`
348 |
349 | Uses the `syslog` core Python module, which is not available on Windows
350 | platforms.
351 |
352 | Optional parameters:
353 |
354 | : - `prio` - Syslog priority level. Default is `ALERT`. Possible
355 | values are:
356 |
357 | - EMERG
358 | - ALERT
359 | - CRIT
360 | - ERR
361 | - WARNING
362 | - NOTICE
363 | - INFO
364 | - DEBUG
365 |
366 | - `facility` - Syslog facility. Default is `LOCAL5`. Possible
367 | values are:
368 |
369 | - KERN
370 | - USER
371 | - MAIL
372 | - DAEMON
373 | - AUTH
374 | - LPR
375 | - NEWS
376 | - UUCP
377 | - CRON
378 | - SYSLOG
379 | - LOCAL0
380 | - LOCAL1
381 | - LOCAL2
382 | - LOCAL3
383 | - LOCAL4
384 | - LOCAL5
385 | - LOCAL6
386 | - LOCAL7
387 |
388 | - `fmt` - Format of the message to be sent to the system logger.
389 | The title and the message are specified using the following
390 | placeholders:
391 |
392 | - `{title}`
393 | - `{message}`
394 |
395 | Default is `[{title}] {message}`.
396 |
397 | ### [Termux:API](https://play.google.com/store/apps/details?id=com.termux.api&hl=en) - `termux`
398 |
399 | Requires the app to be install from the Play store and the CLI utility
400 | be installed with `apt install termux-api`.
401 |
402 | ### [Pushalot](https://pushalot.com) - `pushalot`
403 |
404 | Required parameter:
405 |
406 | : - `auth_token` - Your private Pushalot auth token, found here
407 |
408 |
409 | Optional parameters:
410 |
411 | : - `source` - source of the notification
412 | - `ttl` - message expire time in minutes (time to live)
413 | - `url` - URL to include in the notifications
414 | - `url_title` - visible URL title (ignored if no url specified)
415 | - `image` - URL of image included in the notifications
416 | - `important` - mark notifications as important
417 | - `silent` - mark notifications as silent
418 |
419 | ### [Rocket.Chat](https://rocket.chat) - `rocketchat`
420 |
421 | Requires extras, install like this: `pip install ntfy[rocketchat]`.
422 |
423 | Required parameters:
424 |
425 | : - `url` - URL of your Rocket.Chat instance
426 | - `username` - login username
427 | - `password` - login password
428 | - `room` - room/channel name to post in
429 |
430 | ### [Matrix.org](https://matrix.org) - `matrix`
431 |
432 | Requires extras, install like this: `pip install ntfy[matrix]`.
433 |
434 | Required parameters:
435 |
436 | : - `url` - URL of your homeserver instance
437 | - `roomId` - room to post in
438 | - `userId` - login userid
439 | - `password` - login password
440 | - `token` - access token
441 |
442 | You must either specify `token`, or `userId` and `password`.
443 |
444 | [Webpush](https://github.com/dschep/ntfy-webpush) - `ntfy_webpush`
445 | \~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~
446 | Webpush support is provded by an external ntfy module, install like
447 | this: `pip install ntfy ntfy-webpush`.
448 |
449 | Required parameters:
450 |
451 | : - `subscription_info` - A
452 | [PushSubscription](https://developer.mozilla.org/en-US/docs/Web/API/PushSubscription)
453 | Object
454 | - `private_key` - the path to private key file or anything else
455 | that works with
456 | [pywebpush](https://github.com/web-push-libs/pywebpush).
457 |
458 | For more info, see [ntfy-webpush]{#ntfy-webpush}
459 | \\`\_
460 |
461 | ### 3rd party backends
462 |
463 | To use or implement your own backends, specify the full path of the
464 | module as your backend. The module needs to contain a module with a
465 | function called `notify` with the following signature:
466 |
467 | ``` python
468 | def notify(title, message, **kwargs):
469 | """
470 | kwargs contains retcode if using ntfy done or ntfy shell-integration
471 | and all options in your backend's section of the config
472 | """
473 | pass
474 | ```
475 |
476 | ### Other options
477 |
478 | Title is configurable with the [title]{.title-ref} key in the config.
479 | Example:
480 |
481 | ``` yaml
482 | ---
483 | title: Customized Title
484 | ```
485 |
486 | ### Backends ToDo
487 |
488 | - [Airgram](http://www.airgramapp.com)
489 | - [Boxcar](https://boxcar.io)
490 |
491 | ## Testing
492 |
493 | ``` shell
494 | python setup.py test
495 | ```
496 |
497 | ## Contributors
498 |
499 | - [dschep](https://github.com/dschep) - Maintainer & Lead Developer
500 | - [danryder](https://github.com/danryder) - XMPP Backend & emoji
501 | support
502 | - [oz123](https://github.com/oz123) - Linux desktop notification
503 | improvements
504 | - [schwert](https://github.com/schwert) - PushJet support
505 | - [rahiel](https://github.com/rahiel) - Telegram support
506 | - [tymm](https://github.com/tymm) - Simplepush support
507 | - [jungle-boogie](https://github.com/jungle-boogie) - Documentation
508 | updates
509 | - [tjbenator](https://github.com/tjbenator) - Advanced Pushover
510 | options
511 | - [mobiusklein](https://github.com/mobiusklein) - Win32 Bugfix
512 | - [rcaloras](https://github.com/rcaloras) - Creator of
513 | [bash-prexec]{.title-ref}, without which there woudn\'t be bash
514 | shell integration for [ntfy]{.title-ref}
515 | - [eightnoteight](https://github.com/eightnoteight) - Notifico support
516 | - [juanpabloaj](https://github.com/juanpabloaj) - Slack support
517 | - [giuseongit](https://github.com/giuseongit) - Instapush support
518 | - [jlesage](https://github.com/jlesage) - Systemlog support
519 | - [sambrightman](https://github.com/sambrightman) - Prowl support
520 | - [mlesniew](https://github.com/mlesniew) - Pushalot support
521 | - [webworxshop](https://github.com/webworxshop) - Rocket.Chat support
522 | - [rhabbachi](https://github.com/rhabbachi) - transient option in
523 | Linux desktop notifications
524 | - [Half-Shot](https://github.com/Half-Shot) - Matrix support
525 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | _build
2 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # User-friendly check for sphinx-build
11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
13 | endif
14 |
15 | # Internal variables.
16 | PAPEROPT_a4 = -D latex_paper_size=a4
17 | PAPEROPT_letter = -D latex_paper_size=letter
18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
19 | # the i18n builder cannot share the environment and doctrees with the others
20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
21 |
22 | .PHONY: help
23 | help:
24 | @echo "Please use \`make ' where is one of"
25 | @echo " html to make standalone HTML files"
26 | @echo " dirhtml to make HTML files named index.html in directories"
27 | @echo " singlehtml to make a single large HTML file"
28 | @echo " pickle to make pickle files"
29 | @echo " json to make JSON files"
30 | @echo " htmlhelp to make HTML files and a HTML help project"
31 | @echo " qthelp to make HTML files and a qthelp project"
32 | @echo " applehelp to make an Apple Help Book"
33 | @echo " devhelp to make HTML files and a Devhelp project"
34 | @echo " epub to make an epub"
35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
36 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
38 | @echo " text to make text files"
39 | @echo " man to make manual pages"
40 | @echo " texinfo to make Texinfo files"
41 | @echo " info to make Texinfo files and run them through makeinfo"
42 | @echo " gettext to make PO message catalogs"
43 | @echo " changes to make an overview of all changed/added/deprecated items"
44 | @echo " xml to make Docutils-native XML files"
45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
46 | @echo " linkcheck to check all external links for integrity"
47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
48 | @echo " coverage to run coverage check of the documentation (if enabled)"
49 |
50 | .PHONY: clean
51 | clean:
52 | rm -rf $(BUILDDIR)/*
53 |
54 | .PHONY: html
55 | html:
56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
57 | @echo
58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
59 |
60 | .PHONY: dirhtml
61 | dirhtml:
62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
63 | @echo
64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
65 |
66 | .PHONY: singlehtml
67 | singlehtml:
68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
69 | @echo
70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
71 |
72 | .PHONY: pickle
73 | pickle:
74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
75 | @echo
76 | @echo "Build finished; now you can process the pickle files."
77 |
78 | .PHONY: json
79 | json:
80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
81 | @echo
82 | @echo "Build finished; now you can process the JSON files."
83 |
84 | .PHONY: htmlhelp
85 | htmlhelp:
86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
87 | @echo
88 | @echo "Build finished; now you can run HTML Help Workshop with the" \
89 | ".hhp project file in $(BUILDDIR)/htmlhelp."
90 |
91 | .PHONY: qthelp
92 | qthelp:
93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
94 | @echo
95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ntfy.qhcp"
98 | @echo "To view the help file:"
99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ntfy.qhc"
100 |
101 | .PHONY: applehelp
102 | applehelp:
103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
104 | @echo
105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
106 | @echo "N.B. You won't be able to view it unless you put it in" \
107 | "~/Library/Documentation/Help or install it in your application" \
108 | "bundle."
109 |
110 | .PHONY: devhelp
111 | devhelp:
112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
113 | @echo
114 | @echo "Build finished."
115 | @echo "To view the help file:"
116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/ntfy"
117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ntfy"
118 | @echo "# devhelp"
119 |
120 | .PHONY: epub
121 | epub:
122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
123 | @echo
124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
125 |
126 | .PHONY: latex
127 | latex:
128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
129 | @echo
130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
132 | "(use \`make latexpdf' here to do that automatically)."
133 |
134 | .PHONY: latexpdf
135 | latexpdf:
136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
137 | @echo "Running LaTeX files through pdflatex..."
138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
140 |
141 | .PHONY: latexpdfja
142 | latexpdfja:
143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
144 | @echo "Running LaTeX files through platex and dvipdfmx..."
145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
147 |
148 | .PHONY: text
149 | text:
150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
151 | @echo
152 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
153 |
154 | .PHONY: man
155 | man:
156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
157 | @echo
158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
159 |
160 | .PHONY: texinfo
161 | texinfo:
162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
163 | @echo
164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
165 | @echo "Run \`make' in that directory to run these through makeinfo" \
166 | "(use \`make info' here to do that automatically)."
167 |
168 | .PHONY: info
169 | info:
170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
171 | @echo "Running Texinfo files through makeinfo..."
172 | make -C $(BUILDDIR)/texinfo info
173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
174 |
175 | .PHONY: gettext
176 | gettext:
177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
178 | @echo
179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
180 |
181 | .PHONY: changes
182 | changes:
183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
184 | @echo
185 | @echo "The overview file is in $(BUILDDIR)/changes."
186 |
187 | .PHONY: linkcheck
188 | linkcheck:
189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
190 | @echo
191 | @echo "Link check complete; look for any errors in the above output " \
192 | "or in $(BUILDDIR)/linkcheck/output.txt."
193 |
194 | .PHONY: doctest
195 | doctest:
196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
197 | @echo "Testing of doctests in the sources finished, look at the " \
198 | "results in $(BUILDDIR)/doctest/output.txt."
199 |
200 | .PHONY: coverage
201 | coverage:
202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
203 | @echo "Testing of coverage in the sources finished, look at the " \
204 | "results in $(BUILDDIR)/coverage/python.txt."
205 |
206 | .PHONY: xml
207 | xml:
208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
209 | @echo
210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
211 |
212 | .PHONY: pseudoxml
213 | pseudoxml:
214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
215 | @echo
216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
217 |
--------------------------------------------------------------------------------
/docs/README.rst:
--------------------------------------------------------------------------------
1 | ../README.rst
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # ntfy documentation build configuration file, created by
4 | # sphinx-quickstart on Sat Feb 6 09:16:07 2016.
5 | #
6 | # This file is execfile()d with the current directory set to its
7 | # containing dir.
8 | #
9 | # Note that not all possible configuration values are present in this
10 | # autogenerated file.
11 | #
12 | # All configuration values have a default; values that are commented out
13 | # serve to show the default.
14 |
15 | import sys
16 | import os
17 |
18 | # If extensions (or modules to document with autodoc) are in another directory,
19 | # add these directories to sys.path here. If the directory is relative to the
20 | # documentation root, use os.path.abspath to make it absolute, like shown here.
21 | sys.path.insert(0, os.path.abspath('..'))
22 |
23 | # -- General configuration ------------------------------------------------
24 |
25 | # If your documentation needs a minimal Sphinx version, state it here.
26 | #needs_sphinx = '1.0'
27 |
28 | # Add any Sphinx extension module names here, as strings. They can be
29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
30 | # ones.
31 | extensions = [
32 | 'sphinx.ext.autodoc',
33 | ]
34 |
35 | # Add any paths that contain templates here, relative to this directory.
36 | templates_path = ['_templates']
37 |
38 | # The suffix(es) of source filenames.
39 | # You can specify multiple suffix as a list of string:
40 | # source_suffix = ['.rst', '.md']
41 | source_suffix = '.rst'
42 |
43 | # The encoding of source files.
44 | #source_encoding = 'utf-8-sig'
45 |
46 | # The master toctree document.
47 | master_doc = 'index'
48 |
49 | # General information about the project.
50 | project = u'ntfy'
51 | copyright = u'2016, Daniel Schep'
52 | author = u'Daniel Schep'
53 |
54 | # The version info for the project you're documenting, acts as replacement for
55 | # |version| and |release|, also used in various other places throughout the
56 | # built documents.
57 | #
58 | # The short X.Y version.
59 | from subprocess import check_output
60 | version = check_output(['python', '../setup.py', '--version']).decode().strip()
61 | # The full version, including alpha/beta/rc tags.
62 | release = version
63 |
64 | # The language for content autogenerated by Sphinx. Refer to documentation
65 | # for a list of supported languages.
66 | #
67 | # This is also used if you do content translation via gettext catalogs.
68 | # Usually you set "language" from the command line for these cases.
69 | language = None
70 |
71 | # There are two options for replacing |today|: either, you set today to some
72 | # non-false value, then it is used:
73 | #today = ''
74 | # Else, today_fmt is used as the format for a strftime call.
75 | #today_fmt = '%B %d, %Y'
76 |
77 | # List of patterns, relative to source directory, that match files and
78 | # directories to ignore when looking for source files.
79 | exclude_patterns = ['_build']
80 |
81 | # The reST default role (used for this markup: `text`) to use for all
82 | # documents.
83 | #default_role = None
84 |
85 | # If true, '()' will be appended to :func: etc. cross-reference text.
86 | #add_function_parentheses = True
87 |
88 | # If true, the current module name will be prepended to all description
89 | # unit titles (such as .. function::).
90 | #add_module_names = True
91 |
92 | # If true, sectionauthor and moduleauthor directives will be shown in the
93 | # output. They are ignored by default.
94 | #show_authors = False
95 |
96 | # The name of the Pygments (syntax highlighting) style to use.
97 | pygments_style = 'sphinx'
98 |
99 | # A list of ignored prefixes for module index sorting.
100 | #modindex_common_prefix = []
101 |
102 | # If true, keep warnings as "system message" paragraphs in the built documents.
103 | #keep_warnings = False
104 |
105 | # If true, `todo` and `todoList` produce output, else they produce nothing.
106 | todo_include_todos = False
107 |
108 |
109 | # -- Options for HTML output ----------------------------------------------
110 |
111 | # The theme to use for HTML and HTML Help pages. See the documentation for
112 | # a list of builtin themes.
113 | html_theme = 'alabaster'
114 |
115 | # Theme options are theme-specific and customize the look and feel of a theme
116 | # further. For a list of options available for each theme, see the
117 | # documentation.
118 | html_theme_options = {'github_user': 'dschep', 'github_repo': 'ntfy',
119 | 'github_banner': True}
120 |
121 | # Add any paths that contain custom themes here, relative to this directory.
122 | #html_theme_path = []
123 |
124 | # The name for this set of Sphinx documents. If None, it defaults to
125 | # " v documentation".
126 | #html_title = None
127 |
128 | # A shorter title for the navigation bar. Default is the same as html_title.
129 | #html_short_title = None
130 |
131 | # The name of an image file (relative to this directory) to place at the top
132 | # of the sidebar.
133 | html_logo = 'logo.png'
134 |
135 | # The name of an image file (within the static path) to use as favicon of the
136 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
137 | # pixels large.
138 | #html_favicon = None
139 |
140 | # Add any paths that contain custom static files (such as style sheets) here,
141 | # relative to this directory. They are copied after the builtin static files,
142 | # so a file named "default.css" will overwrite the builtin "default.css".
143 | html_static_path = ['_static']
144 |
145 | # Add any extra paths that contain custom files (such as robots.txt or
146 | # .htaccess) here, relative to this directory. These files are copied
147 | # directly to the root of the documentation.
148 | #html_extra_path = []
149 |
150 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
151 | # using the given strftime format.
152 | #html_last_updated_fmt = '%b %d, %Y'
153 |
154 | # If true, SmartyPants will be used to convert quotes and dashes to
155 | # typographically correct entities.
156 | #html_use_smartypants = True
157 |
158 | # Custom sidebar templates, maps document names to template names.
159 | #html_sidebars = {}
160 |
161 | # Additional templates that should be rendered to pages, maps page names to
162 | # template names.
163 | #html_additional_pages = {}
164 |
165 | # If false, no module index is generated.
166 | #html_domain_indices = True
167 |
168 | # If false, no index is generated.
169 | #html_use_index = True
170 |
171 | # If true, the index is split into individual pages for each letter.
172 | #html_split_index = False
173 |
174 | # If true, links to the reST sources are added to the pages.
175 | #html_show_sourcelink = True
176 |
177 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
178 | #html_show_sphinx = True
179 |
180 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
181 | #html_show_copyright = True
182 |
183 | # If true, an OpenSearch description file will be output, and all pages will
184 | # contain a tag referring to it. The value of this option must be the
185 | # base URL from which the finished HTML is served.
186 | #html_use_opensearch = ''
187 |
188 | # This is the file name suffix for HTML files (e.g. ".xhtml").
189 | #html_file_suffix = None
190 |
191 | # Language to be used for generating the HTML full-text search index.
192 | # Sphinx supports the following languages:
193 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
194 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
195 | #html_search_language = 'en'
196 |
197 | # A dictionary with options for the search language support, empty by default.
198 | # Now only 'ja' uses this config value
199 | #html_search_options = {'type': 'default'}
200 |
201 | # The name of a javascript file (relative to the configuration directory) that
202 | # implements a search results scorer. If empty, the default will be used.
203 | #html_search_scorer = 'scorer.js'
204 |
205 | # Output file base name for HTML help builder.
206 | htmlhelp_basename = 'ntfydoc'
207 |
208 | # -- Options for LaTeX output ---------------------------------------------
209 |
210 | latex_elements = {
211 | # The paper size ('letterpaper' or 'a4paper').
212 | #'papersize': 'letterpaper',
213 |
214 | # The font size ('10pt', '11pt' or '12pt').
215 | #'pointsize': '10pt',
216 |
217 | # Additional stuff for the LaTeX preamble.
218 | #'preamble': '',
219 |
220 | # Latex figure (float) alignment
221 | #'figure_align': 'htbp',
222 | }
223 |
224 | # Grouping the document tree into LaTeX files. List of tuples
225 | # (source start file, target name, title,
226 | # author, documentclass [howto, manual, or own class]).
227 | latex_documents = [
228 | (master_doc, 'ntfy.tex', u'ntfy Documentation',
229 | u'Daniel Schep', 'manual'),
230 | ]
231 |
232 | # The name of an image file (relative to this directory) to place at the top of
233 | # the title page.
234 | #latex_logo = None
235 |
236 | # For "manual" documents, if this is true, then toplevel headings are parts,
237 | # not chapters.
238 | #latex_use_parts = False
239 |
240 | # If true, show page references after internal links.
241 | #latex_show_pagerefs = False
242 |
243 | # If true, show URL addresses after external links.
244 | #latex_show_urls = False
245 |
246 | # Documents to append as an appendix to all manuals.
247 | #latex_appendices = []
248 |
249 | # If false, no module index is generated.
250 | #latex_domain_indices = True
251 |
252 |
253 | # -- Options for manual page output ---------------------------------------
254 |
255 | # One entry per manual page. List of tuples
256 | # (source start file, name, description, authors, manual section).
257 | man_pages = [
258 | (master_doc, 'ntfy', u'ntfy Documentation',
259 | [author], 1)
260 | ]
261 |
262 | # If true, show URL addresses after external links.
263 | #man_show_urls = False
264 |
265 |
266 | # -- Options for Texinfo output -------------------------------------------
267 |
268 | # Grouping the document tree into Texinfo files. List of tuples
269 | # (source start file, target name, title, author,
270 | # dir menu entry, description, category)
271 | texinfo_documents = [
272 | (master_doc, 'ntfy', u'ntfy Documentation',
273 | author, 'ntfy', 'One line description of project.',
274 | 'Miscellaneous'),
275 | ]
276 |
277 | # Documents to append as an appendix to all manuals.
278 | #texinfo_appendices = []
279 |
280 | # If false, no module index is generated.
281 | #texinfo_domain_indices = True
282 |
283 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
284 | #texinfo_show_urls = 'footnote'
285 |
286 | # If true, do not generate a @detailmenu in the "Top" node's menu.
287 | #texinfo_no_detailmenu = False
288 |
--------------------------------------------------------------------------------
/docs/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dschep/ntfy/c4ef3b54f26390b7b51ec612f41f11a1ba284227/docs/demo.gif
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. ntfy documentation master file, created by
2 | sphinx-quickstart on Sat Feb 6 09:16:07 2016.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | .. include:: ../README.rst
7 |
8 |
9 | .. toctree::
10 | :maxdepth: 2
11 |
12 |
13 |
14 | Indices and tables
15 | ------------------
16 |
17 | * :ref:`genindex`
18 | * :ref:`modindex`
19 | * :ref:`search`
20 |
21 |
--------------------------------------------------------------------------------
/docs/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dschep/ntfy/c4ef3b54f26390b7b51ec612f41f11a1ba284227/docs/logo.png
--------------------------------------------------------------------------------
/docs/logo.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dschep/ntfy/c4ef3b54f26390b7b51ec612f41f11a1ba284227/docs/logo.xcf
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | REM Command file for Sphinx documentation
4 |
5 | if "%SPHINXBUILD%" == "" (
6 | set SPHINXBUILD=sphinx-build
7 | )
8 | set BUILDDIR=_build
9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
10 | set I18NSPHINXOPTS=%SPHINXOPTS% .
11 | if NOT "%PAPER%" == "" (
12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
14 | )
15 |
16 | if "%1" == "" goto help
17 |
18 | if "%1" == "help" (
19 | :help
20 | echo.Please use `make ^` where ^ is one of
21 | echo. html to make standalone HTML files
22 | echo. dirhtml to make HTML files named index.html in directories
23 | echo. singlehtml to make a single large HTML file
24 | echo. pickle to make pickle files
25 | echo. json to make JSON files
26 | echo. htmlhelp to make HTML files and a HTML help project
27 | echo. qthelp to make HTML files and a qthelp project
28 | echo. devhelp to make HTML files and a Devhelp project
29 | echo. epub to make an epub
30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
31 | echo. text to make text files
32 | echo. man to make manual pages
33 | echo. texinfo to make Texinfo files
34 | echo. gettext to make PO message catalogs
35 | echo. changes to make an overview over all changed/added/deprecated items
36 | echo. xml to make Docutils-native XML files
37 | echo. pseudoxml to make pseudoxml-XML files for display purposes
38 | echo. linkcheck to check all external links for integrity
39 | echo. doctest to run all doctests embedded in the documentation if enabled
40 | echo. coverage to run coverage check of the documentation if enabled
41 | goto end
42 | )
43 |
44 | if "%1" == "clean" (
45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
46 | del /q /s %BUILDDIR%\*
47 | goto end
48 | )
49 |
50 |
51 | REM Check if sphinx-build is available and fallback to Python version if any
52 | %SPHINXBUILD% 1>NUL 2>NUL
53 | if errorlevel 9009 goto sphinx_python
54 | goto sphinx_ok
55 |
56 | :sphinx_python
57 |
58 | set SPHINXBUILD=python -m sphinx.__init__
59 | %SPHINXBUILD% 2> nul
60 | if errorlevel 9009 (
61 | echo.
62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
63 | echo.installed, then set the SPHINXBUILD environment variable to point
64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
65 | echo.may add the Sphinx directory to PATH.
66 | echo.
67 | echo.If you don't have Sphinx installed, grab it from
68 | echo.http://sphinx-doc.org/
69 | exit /b 1
70 | )
71 |
72 | :sphinx_ok
73 |
74 |
75 | if "%1" == "html" (
76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
77 | if errorlevel 1 exit /b 1
78 | echo.
79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html.
80 | goto end
81 | )
82 |
83 | if "%1" == "dirhtml" (
84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
85 | if errorlevel 1 exit /b 1
86 | echo.
87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
88 | goto end
89 | )
90 |
91 | if "%1" == "singlehtml" (
92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
93 | if errorlevel 1 exit /b 1
94 | echo.
95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
96 | goto end
97 | )
98 |
99 | if "%1" == "pickle" (
100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
101 | if errorlevel 1 exit /b 1
102 | echo.
103 | echo.Build finished; now you can process the pickle files.
104 | goto end
105 | )
106 |
107 | if "%1" == "json" (
108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
109 | if errorlevel 1 exit /b 1
110 | echo.
111 | echo.Build finished; now you can process the JSON files.
112 | goto end
113 | )
114 |
115 | if "%1" == "htmlhelp" (
116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
117 | if errorlevel 1 exit /b 1
118 | echo.
119 | echo.Build finished; now you can run HTML Help Workshop with the ^
120 | .hhp project file in %BUILDDIR%/htmlhelp.
121 | goto end
122 | )
123 |
124 | if "%1" == "qthelp" (
125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
126 | if errorlevel 1 exit /b 1
127 | echo.
128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^
129 | .qhcp project file in %BUILDDIR%/qthelp, like this:
130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\ntfy.qhcp
131 | echo.To view the help file:
132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\ntfy.ghc
133 | goto end
134 | )
135 |
136 | if "%1" == "devhelp" (
137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
138 | if errorlevel 1 exit /b 1
139 | echo.
140 | echo.Build finished.
141 | goto end
142 | )
143 |
144 | if "%1" == "epub" (
145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
146 | if errorlevel 1 exit /b 1
147 | echo.
148 | echo.Build finished. The epub file is in %BUILDDIR%/epub.
149 | goto end
150 | )
151 |
152 | if "%1" == "latex" (
153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
154 | if errorlevel 1 exit /b 1
155 | echo.
156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
157 | goto end
158 | )
159 |
160 | if "%1" == "latexpdf" (
161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
162 | cd %BUILDDIR%/latex
163 | make all-pdf
164 | cd %~dp0
165 | echo.
166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex.
167 | goto end
168 | )
169 |
170 | if "%1" == "latexpdfja" (
171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
172 | cd %BUILDDIR%/latex
173 | make all-pdf-ja
174 | cd %~dp0
175 | echo.
176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex.
177 | goto end
178 | )
179 |
180 | if "%1" == "text" (
181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
182 | if errorlevel 1 exit /b 1
183 | echo.
184 | echo.Build finished. The text files are in %BUILDDIR%/text.
185 | goto end
186 | )
187 |
188 | if "%1" == "man" (
189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
190 | if errorlevel 1 exit /b 1
191 | echo.
192 | echo.Build finished. The manual pages are in %BUILDDIR%/man.
193 | goto end
194 | )
195 |
196 | if "%1" == "texinfo" (
197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
198 | if errorlevel 1 exit /b 1
199 | echo.
200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
201 | goto end
202 | )
203 |
204 | if "%1" == "gettext" (
205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
206 | if errorlevel 1 exit /b 1
207 | echo.
208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
209 | goto end
210 | )
211 |
212 | if "%1" == "changes" (
213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
214 | if errorlevel 1 exit /b 1
215 | echo.
216 | echo.The overview file is in %BUILDDIR%/changes.
217 | goto end
218 | )
219 |
220 | if "%1" == "linkcheck" (
221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
222 | if errorlevel 1 exit /b 1
223 | echo.
224 | echo.Link check complete; look for any errors in the above output ^
225 | or in %BUILDDIR%/linkcheck/output.txt.
226 | goto end
227 | )
228 |
229 | if "%1" == "doctest" (
230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
231 | if errorlevel 1 exit /b 1
232 | echo.
233 | echo.Testing of doctests in the sources finished, look at the ^
234 | results in %BUILDDIR%/doctest/output.txt.
235 | goto end
236 | )
237 |
238 | if "%1" == "coverage" (
239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
240 | if errorlevel 1 exit /b 1
241 | echo.
242 | echo.Testing of coverage in the sources finished, look at the ^
243 | results in %BUILDDIR%/coverage/python.txt.
244 | goto end
245 | )
246 |
247 | if "%1" == "xml" (
248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
249 | if errorlevel 1 exit /b 1
250 | echo.
251 | echo.Build finished. The XML files are in %BUILDDIR%/xml.
252 | goto end
253 | )
254 |
255 | if "%1" == "pseudoxml" (
256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
257 | if errorlevel 1 exit /b 1
258 | echo.
259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
260 | goto end
261 | )
262 |
263 | :end
264 |
--------------------------------------------------------------------------------
/docs/modules.rst:
--------------------------------------------------------------------------------
1 | ntfy
2 | ====
3 |
4 | .. toctree::
5 | :maxdepth: 4
6 |
7 | ntfy
8 |
--------------------------------------------------------------------------------
/docs/ntfy.backends.rst:
--------------------------------------------------------------------------------
1 | ntfy.backends package
2 | =====================
3 |
4 | Submodules
5 | ----------
6 |
7 | ntfy.backends.darwin module
8 | ---------------------------
9 |
10 | .. automodule:: ntfy.backends.darwin
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | ntfy.backends.default module
16 | ----------------------------
17 |
18 | .. automodule:: ntfy.backends.default
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | ntfy.backends.linux module
24 | --------------------------
25 |
26 | .. automodule:: ntfy.backends.linux
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
31 | ntfy.backends.pushbullet module
32 | -------------------------------
33 |
34 | .. automodule:: ntfy.backends.pushbullet
35 | :members:
36 | :undoc-members:
37 | :show-inheritance:
38 |
39 | ntfy.backends.pushover module
40 | -----------------------------
41 |
42 | .. automodule:: ntfy.backends.pushover
43 | :members:
44 | :undoc-members:
45 | :show-inheritance:
46 |
47 | ntfy.backends.simplepush module
48 | -------------------------------
49 |
50 | .. automodule:: ntfy.backends.simplepush
51 | :members:
52 | :undoc-members:
53 | :show-inheritance:
54 |
55 | ntfy.backends.notifico module
56 | -----------------------------
57 |
58 | .. automodule:: ntfy.backends.notifico
59 | :members:
60 | :undoc-members:
61 | :show-inheritance:
62 |
63 | ntfy.backends.insta module
64 | -----------------------------
65 |
66 | .. automodule:: ntfy.backends.insta
67 | :members:
68 | :undoc-members:
69 | :show-inheritance:
70 |
71 |
72 | ntfy.backends.win32 module
73 | --------------------------
74 |
75 | .. automodule:: ntfy.backends.win32
76 | :members:
77 | :undoc-members:
78 | :show-inheritance:
79 |
80 | ntfy.backends.xmpp module
81 | -------------------------
82 |
83 | .. automodule:: ntfy.backends.xmpp
84 | :members:
85 | :undoc-members:
86 | :show-inheritance:
87 |
88 | ntfy.backends.systemlog module
89 | -------------------------
90 |
91 | .. automodule:: ntfy.backends.systemlog
92 | :members:
93 | :undoc-members:
94 | :show-inheritance:
95 |
96 | ntfy.backends.rocketchat module
97 | -------------------------
98 |
99 | .. automodule:: ntfy.backends.rocketchat
100 | :members:
101 | :undoc-members:
102 | :show-inheritance:
103 |
104 | ntfy.backends.matrix module
105 | -------------------------
106 |
107 | .. automodule:: ntfy.backends.matrix
108 | :members:
109 | :undoc-members:
110 | :show-inheritance:
111 |
112 |
113 | Module contents
114 | ---------------
115 |
116 | .. automodule:: ntfy.backends
117 | :members:
118 | :undoc-members:
119 | :show-inheritance:
120 |
--------------------------------------------------------------------------------
/docs/ntfy.rst:
--------------------------------------------------------------------------------
1 | ntfy package
2 | ============
3 |
4 | Subpackages
5 | -----------
6 |
7 | .. toctree::
8 |
9 | ntfy.backends
10 |
11 | Submodules
12 | ----------
13 |
14 | ntfy.cli module
15 | ---------------
16 |
17 | .. automodule:: ntfy.cli
18 | :members:
19 | :undoc-members:
20 | :show-inheritance:
21 |
22 | ntfy.config module
23 | ------------------
24 |
25 | .. automodule:: ntfy.config
26 | :members:
27 | :undoc-members:
28 | :show-inheritance:
29 |
30 |
31 | Module contents
32 | ---------------
33 |
34 | .. automodule:: ntfy
35 | :members:
36 | :undoc-members:
37 | :show-inheritance:
38 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | ntfy[xmpp,telegram,instapush,rocketchat,slack,pid,emoji]
2 |
--------------------------------------------------------------------------------
/ntfy/__init__.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from getpass import getuser
3 | from os import getcwd, path, name
4 | from socket import gethostname
5 | from importlib import import_module
6 | from inspect import getfullargspec
7 | from .backends.default import DefaultNotifierError
8 |
9 | __version__ = '2.7.1'
10 |
11 | _user_home = path.expanduser('~')
12 | _cwd = getcwd()
13 | if name != 'nt' and _cwd.startswith(_user_home):
14 | default_title = '{}@{}:{}'.format(
15 | getuser(), gethostname(), path.join('~', _cwd[len(_user_home) + 1:]))
16 | else:
17 | default_title = '{}@{}:{}'.format(getuser(), gethostname(), _cwd)
18 |
19 |
20 | def notify(message, title, config=None, **kwargs):
21 | from .config import load_config
22 |
23 | if config is None:
24 | config = load_config()
25 |
26 | ret = 0
27 | retcode = kwargs.pop('retcode', None)
28 |
29 | for backend in config.get('backends', ['default']):
30 | backend_config = config.get(backend, {})
31 | backend_config.update(kwargs)
32 | if 'backend' in backend_config:
33 | backend = backend_config.pop('backend')
34 |
35 | if title is None:
36 | title = backend_config.pop('title',
37 | config.get('title', default_title))
38 | elif 'title' in backend_config:
39 | del backend_config['title']
40 |
41 | try:
42 | notifier = import_module('ntfy.backends.{}'.format(backend))
43 | except ImportError:
44 | try:
45 | notifier = import_module(backend)
46 | except ImportError:
47 | logging.getLogger(__name__).error(
48 | 'Invalid backend {}'.format(backend))
49 | ret = 1
50 | continue
51 |
52 | try:
53 | notify_ret = notifier.notify(
54 | message=message,
55 | title=title,
56 | retcode=retcode,
57 | **backend_config)
58 | if notify_ret:
59 | ret = notify_ret
60 | except (SystemExit, KeyboardInterrupt):
61 | raise
62 | except Exception as e:
63 | ret = 1
64 | if isinstance(e, DefaultNotifierError):
65 | notifier = e.module
66 | e = e.exception
67 |
68 | args, _, _, defaults, *_ = getfullargspec(notifier.notify)
69 | possible_args = set(args)
70 | required_args = set(args) if defaults is None else set(args[:-len(defaults)])
71 | required_args -= set(['title', 'message', 'retcode'])
72 | unknown_args = set(backend_config) - possible_args
73 | missing_args = required_args - set(backend_config)
74 |
75 | if unknown_args:
76 | logging.getLogger(__name__).error(
77 | 'Got unknown arguments: {}'.format(unknown_args))
78 |
79 | if missing_args:
80 | logging.getLogger(__name__).error(
81 | 'Missing arguments: {}'.format(missing_args))
82 |
83 | if not any([unknown_args, missing_args]):
84 | logging.getLogger(__name__).error(
85 | 'Failed to send notification using {}'.format(backend),
86 | exc_info=True)
87 |
88 | return ret
89 |
--------------------------------------------------------------------------------
/ntfy/backends/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dschep/ntfy/c4ef3b54f26390b7b51ec612f41f11a1ba284227/ntfy/backends/__init__.py
--------------------------------------------------------------------------------
/ntfy/backends/darwin.py:
--------------------------------------------------------------------------------
1 | def notify(title, message, retcode=None):
2 | """
3 | adapted from https://gist.github.com/baliw/4020619
4 | """
5 | try:
6 | import Foundation
7 | import objc
8 | except ImportError:
9 | import sys
10 | import logging
11 |
12 | logger = logging.getLogger(__name__)
13 | if sys.platform.startswith('darwin') and hasattr(sys, 'real_prefix'):
14 | logger.error(
15 | "Using ntfy with the MacOS Notification Center doesn't "
16 | "work within a virtualenv")
17 | sys.exit(1)
18 | else:
19 | raise
20 |
21 | NSUserNotification = objc.lookUpClass('NSUserNotification')
22 | NSUserNotificationCenter = objc.lookUpClass('NSUserNotificationCenter')
23 |
24 | notification = NSUserNotification.alloc().init()
25 | notification.setTitle_(title)
26 | if message is not None:
27 | notification.setInformativeText_(message)
28 | notification.setDeliveryDate_(Foundation.NSDate.date())
29 |
30 | NSUserNotificationCenter.defaultUserNotificationCenter()\
31 | .scheduleNotification_(notification)
32 |
--------------------------------------------------------------------------------
/ntfy/backends/default.py:
--------------------------------------------------------------------------------
1 | from importlib import import_module
2 | from sys import platform
3 |
4 |
5 | class DefaultNotifierError(Exception):
6 | def __init__(self, exception, module):
7 | self.exception = exception
8 | self.module = module
9 |
10 |
11 | def notify(title, message, **kwargs):
12 | """
13 | This backend automatically selects the correct desktop notification backend
14 | for your operating system.
15 | """
16 | for os in ['linux', 'win32', 'darwin']:
17 | if platform.startswith(os):
18 | module = import_module('ntfy.backends.{}'.format(os))
19 | try:
20 | module.notify(title=title, message=message, **kwargs)
21 | except Exception as e:
22 | raise DefaultNotifierError(e, module)
23 | break
24 |
--------------------------------------------------------------------------------
/ntfy/backends/insta.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import re
3 |
4 | from instapush import App
5 |
6 |
7 | class WrongMessageCountException(Exception):
8 | pass
9 |
10 |
11 | class ApiException(Exception):
12 | pass
13 |
14 |
15 | def notify(title, message, event_name, appid, secret, trackers, retcode=None):
16 | """
17 | Required parameter:
18 | * ``event_name`` - Instapush event (the notification template)
19 | * ``appid`` - The appid found on the dashboard
20 | * ``secret`` - The secret found on the dashboard
21 | * ``traskers`` - List of the placeholders for the selected event
22 | """
23 |
24 | logger = logging.getLogger(__name__)
25 | _msgs = re.split(r'(?{} {}".format(title, message)
17 | room = client.join_room(roomId)
18 | room.send_html(msg_html, msg_plain)
19 |
--------------------------------------------------------------------------------
/ntfy/backends/notifico.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | import requests
4 |
5 |
6 | def notify(title, message, retcode=None, webhook=None):
7 | """
8 | Required parameter:
9 | * ``webhook`` - The webhook link, created at https://n.tkte.ch/
10 | (choose ``Plain Text`` service when creating the webhook)
11 | """
12 |
13 | logger = logging.getLogger(__name__)
14 | if webhook is None:
15 | logger.error('please set webhook variable under '
16 | 'notifico backend of the config file')
17 | return
18 | response = requests.get(
19 | webhook,
20 | params={
21 | 'payload': '{title}\n{message}'.format(
22 | title=title, message=message)
23 | })
24 | response.raise_for_status()
25 |
--------------------------------------------------------------------------------
/ntfy/backends/ntfy_sh.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 |
4 | def notify(title, message, topic, host='https://ntfy.sh', user=None, password=None, **kwargs):
5 | auth_kwarg = {'auth': (user, password)} if user and password else {}
6 |
7 | requests.post(
8 | f"{host}/{topic}",
9 | headers=dict(title=title),
10 | data=message,
11 | **auth_kwarg,
12 | )
13 |
--------------------------------------------------------------------------------
/ntfy/backends/prowl.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from ..config import USER_AGENT
4 |
5 | NTFY_API_KEY = '7fb59b2bedc4df26afa306d5dc54495b6394295a'
6 | API_URL = 'https://api.prowlapp.com/publicapi/add'
7 | MIN_PRIORITY = -2
8 | MAX_PRIORITY = 2
9 |
10 |
11 | def notify(title,
12 | message,
13 | api_key=NTFY_API_KEY,
14 | provider_key=None,
15 | priority=0,
16 | url=None,
17 | retcode=None):
18 | """
19 | Optional parameters:
20 | * ``api_key`` - use your own application token
21 | * ``provider_key`` - if you are whitelisted
22 | * ``priority``
23 | * ``url``
24 | """
25 |
26 | data = {
27 | 'apikey': api_key,
28 | 'application': 'ntfy',
29 | 'event': title,
30 | 'description': message,
31 | }
32 |
33 | if MIN_PRIORITY <= priority <= MAX_PRIORITY:
34 | data['priority'] = priority
35 | else:
36 | raise ValueError('priority must be an integer from {:d} to {:d}'
37 | .format(MIN_PRIORITY, MAX_PRIORITY))
38 |
39 | if url is not None:
40 | data['url'] = url
41 |
42 | if provider_key is not None:
43 | data['providerkey'] = provider_key
44 |
45 | resp = requests.post(
46 | API_URL, data=data, headers={
47 | 'User-Agent': USER_AGENT,
48 | })
49 |
50 | resp.raise_for_status()
51 |
--------------------------------------------------------------------------------
/ntfy/backends/pushalot.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from ..config import USER_AGENT
4 |
5 | # URL to pushalot.com notification sending endpoint
6 | PUSHALOT_API_URL = 'https://pushalot.com/api/sendmessage'
7 |
8 |
9 | def notify(title,
10 | message,
11 | auth_token,
12 | source=None,
13 | url=None,
14 | url_title=None,
15 | image=None,
16 | ttl=None,
17 | important=False,
18 | silent=False,
19 | retcode=None):
20 | """
21 | Required parameters:
22 | * ``auth_token``
23 |
24 | Optional parameters:
25 | * ``source``
26 | * ``url``
27 | * ``url_title``
28 | * ``image``
29 | * ``ttl``
30 | * ``important``
31 | * ``silent``
32 | """
33 |
34 | data = {
35 | 'Title': title,
36 | 'Body': message,
37 | 'AuthorizationToken': auth_token,
38 | }
39 |
40 | if source:
41 | data['Source'] = source
42 | if url:
43 | data['Link'] = url
44 | if url and url_title:
45 | data['LinkTitle'] = url_title
46 | if image:
47 | data['Image'] = image
48 | if ttl:
49 | data['TimeToLive'] = int(ttl)
50 | if important:
51 | data['IsImportant'] = 'True'
52 | if silent:
53 | data['IsSilent'] = 'True'
54 |
55 | headers = {'User-Agent': USER_AGENT}
56 | response = requests.post(PUSHALOT_API_URL, data=data, headers=headers)
57 | response.raise_for_status()
58 |
--------------------------------------------------------------------------------
/ntfy/backends/pushbullet.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from ..config import USER_AGENT
4 |
5 |
6 | def notify(title,
7 | message,
8 | access_token,
9 | device_iden=None,
10 | email=None,
11 | retcode=None):
12 | """
13 | Required parameter:
14 | * ``access_token`` - Your Pushbullet access token, created at
15 | https://www.pushbullet.com/#settings/account
16 |
17 | Optional parameters:
18 | * ``device_iden`` - a device identifier, if omited, notification is
19 | sent to all devices
20 | * ``email`` - send notification to pushbullte user with the specified
21 | email or send an email if they aren't a pushullet user
22 | """
23 |
24 | data = {
25 | 'type': 'note',
26 | 'title': title,
27 | 'body': message,
28 | }
29 | if device_iden is not None:
30 | data['device_iden'] = device_iden
31 | if email is not None:
32 | data['email'] = email
33 |
34 | headers = {'Access-Token': access_token, 'User-Agent': USER_AGENT}
35 |
36 | resp = requests.post(
37 | 'https://api.pushbullet.com/v2/pushes', data=data, headers=headers)
38 |
39 | resp.raise_for_status()
40 |
--------------------------------------------------------------------------------
/ntfy/backends/pushjet.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from ..config import USER_AGENT
4 |
5 |
6 | def notify(title,
7 | message,
8 | secret,
9 | endpoint=None,
10 | level=3,
11 | link=None,
12 | retcode=None):
13 | """
14 | Required parameter:
15 | * ``secret`` - The Pushjet service secret token, created with
16 | http://docs.pushjet.io/docs/creating-a-new-service
17 |
18 | Optional parameters:
19 | * ``endpoint`` - custom Pushjet API endpoint
20 | (defaults to https://api.pushjet.io)
21 | * ``level`` - The importance level from 1(low) to 5(high)
22 | * ``link``
23 | """
24 |
25 | data = {
26 | 'title': title,
27 | 'message': message,
28 | 'level': level,
29 | 'secret': secret,
30 | }
31 |
32 | if link:
33 | data['link'] = link
34 |
35 | headers = {'User-Agent': USER_AGENT}
36 |
37 | if endpoint is None:
38 | endpoint = 'https://api.pushjet.io'
39 |
40 | resp = requests.post(endpoint + '/message', data=data, headers=headers)
41 |
42 | resp.raise_for_status()
43 |
--------------------------------------------------------------------------------
/ntfy/backends/pushover.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 |
3 | import logging
4 |
5 | import requests
6 |
7 | from ..config import USER_AGENT
8 |
9 |
10 | def notify(title,
11 | message,
12 | user_key,
13 | api_token='aUnsraBiEZVsmrG89AZp47K3S2dX2a',
14 | device=None,
15 | sound=None,
16 | priority=0,
17 | expire=None,
18 | retry=None,
19 | callback=None,
20 | url=None,
21 | url_title=None,
22 | html=False,
23 | retcode=None):
24 | """
25 | Required parameters:
26 | * ``user_key``
27 |
28 | Optional parameters:
29 | * ``sound``
30 | * ``priority``
31 | * ``expire``
32 | * ``retry``
33 | * ``callback``
34 | * ``api_token`` - use your own application token
35 | * ``device`` - target a device, if omitted, target all devices
36 | * ``url``
37 | * ``url_title``
38 | * ``html``
39 | """
40 |
41 | data = {
42 | 'message': message,
43 | 'token': api_token,
44 | 'user': user_key,
45 | 'title': title,
46 | }
47 | if device:
48 | data['device'] = device
49 |
50 | if sound:
51 | data['sound'] = sound
52 |
53 | if url:
54 | data['url'] = url
55 |
56 | if url_title:
57 | if not url:
58 | logging.getLogger(__name__).warning(
59 | 'url_title specified without specifying url')
60 | else:
61 | data['url_title'] = url_title
62 |
63 | if html:
64 | data['html'] = 1
65 |
66 | priority = int(priority)
67 | if priority <= 2 and priority >= -2:
68 | if priority != 0:
69 | data['priority'] = priority
70 |
71 | # Expire, Retry, and Callback only apply to an Emergency Priority
72 | if priority == 2:
73 | # Retry can not be less than 30 per the API
74 | if not retry or retry < 30:
75 | logging.getLogger(__name__).error(
76 | 'retry is less than 30 or is not set, '
77 | 'setting retry to 30 to comply with '
78 | 'pushover API requirements')
79 | data['retry'] = 30
80 | else:
81 | data['retry'] = retry
82 |
83 | # Expire can not be more than 86400 (24 hours)
84 | if not expire or expire > 86400:
85 | logging.getLogger(__name__).error(
86 | 'expire is greater than 86400 seconds or is not set,'
87 | 'setting expire to 86400 to comply with'
88 | 'pushover API requirements')
89 | data['expire'] = 86400
90 | elif expire <= 86400:
91 | data['expire'] = expire
92 |
93 | if callback:
94 | data['callback'] = callback
95 | else:
96 | if retry:
97 | logging.getLogger(__name__).warning(
98 | 'Non-emergency, ignoring retry set in config')
99 | if expire:
100 | logging.getLogger(__name__).warning(
101 | 'Non-emergency, ignoring expire set in config')
102 | if callback:
103 | logging.getLogger(__name__).warning(
104 | 'Non-emergency, ignoring callback set in config')
105 |
106 | else:
107 | raise ValueError('priority must be an integer from -2 to 2')
108 |
109 | resp = requests.post(
110 | 'https://api.pushover.net/1/messages.json',
111 | data=data,
112 | headers={
113 | 'User-Agent': USER_AGENT,
114 | })
115 |
116 | if resp.status_code == 429:
117 | print("ntfy's default api_token has reached pushover's rate limit")
118 | print("create your own app at https://pushover.net/apps/clone/ntfy")
119 | print("and set api_token in your config file.")
120 | return 1
121 |
122 | resp.raise_for_status()
123 |
--------------------------------------------------------------------------------
/ntfy/backends/rocketchat.py:
--------------------------------------------------------------------------------
1 | from rocketchat_API.rocketchat import RocketChat
2 |
3 |
4 | def notify(title, message, url, username, password, room, retcode=None):
5 |
6 | rocket = RocketChat(username, password, server_url=url)
7 |
8 | msg = "**{}:** {}".format(title, message)
9 | rocket.chat_post_message(msg, channel=room)
10 |
--------------------------------------------------------------------------------
/ntfy/backends/simplepush.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from ..config import USER_AGENT
4 |
5 |
6 | def notify(title, message, key, event=None, retcode=None):
7 | """
8 | Required paramter:
9 | * ``key`` - The Simplepush identification key, created by
10 | installing the Android App (https://simplepush.io)
11 |
12 | Optional parameters:
13 | * ``event`` - use custom ringtones and vibration patterns
14 | """
15 |
16 | data = {
17 | 'title': title if len(title) <= 20 else title[:19] + u'\u2026',
18 | 'msg': message,
19 | 'key': key
20 | }
21 |
22 | if event:
23 | data['event'] = event
24 |
25 | headers = {'User-Agent': USER_AGENT}
26 |
27 | endpoint = "https://api.simplepush.io"
28 |
29 | resp = requests.post(endpoint + '/send', data=data, headers=headers)
30 |
31 | resp.raise_for_status()
32 |
--------------------------------------------------------------------------------
/ntfy/backends/slack.py:
--------------------------------------------------------------------------------
1 | from slack_sdk import WebClient
2 |
3 |
4 | def notify(title, message, token, recipient, retcode=None):
5 |
6 | slack = WebClient(token=token)
7 |
8 | slack.chat_postMessage(channel=recipient, text=message)
9 |
--------------------------------------------------------------------------------
/ntfy/backends/slack_webhook.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 |
4 | def notify(title, message, url, user, **kwargs):
5 |
6 | requests.post(
7 | url,
8 | json={
9 | "username": "ntfy",
10 | "icon_url": "https://ntfy.readthedocs.io/en/latest/_static/logo.png",
11 | "text": "{0}\n{1}".format(title, message),
12 | "channel": user,
13 | "blocks": [
14 | {
15 | "type": "section",
16 | "text": {
17 | "type": "mrkdwn",
18 | "text": "*{0}* {1}".format(title, message),
19 | },
20 | }
21 | ],
22 | },
23 | )
24 |
--------------------------------------------------------------------------------
/ntfy/backends/systemlog.py:
--------------------------------------------------------------------------------
1 | import syslog
2 |
3 |
4 | def notify(title,
5 | message,
6 | prio='ALERT',
7 | facility='LOCAL5',
8 | fmt='[{title}] {message}',
9 | retcode=None):
10 | """
11 | Uses the ``syslog`` core Python module, which is not available on Windows
12 | platforms.
13 |
14 | Optional parameters:
15 | * ``prio`` - Syslog prority level. Default is ``ALERT``. Possible
16 | values are:
17 |
18 | * EMERG
19 | * ALERT
20 | * CRIT
21 | * ERR
22 | * WARNING
23 | * NOTICE
24 | * INFO
25 | * DEBUG
26 |
27 | * ``facility`` - Syslog facility. Default is ``LOCAL5``. Possible
28 | values are:
29 |
30 | * KERN
31 | * USER
32 | * MAIL
33 | * DAEMON
34 | * AUTH
35 | * LPR
36 | * NEWS
37 | * UUCP
38 | * CRON
39 | * SYSLOG
40 | * LOCAL0
41 | * LOCAL1
42 | * LOCAL2
43 | * LOCAL3
44 | * LOCAL4
45 | * LOCAL5
46 | * LOCAL6
47 | * LOCAL7
48 |
49 | * ``fmt`` - Format of the message to be sent to the system logger. The
50 | title and the message are specified using the following placeholders:
51 |
52 | * ``{title}``
53 | * ``{message}``
54 |
55 | Default is ``[{title}] {message}``.
56 | """
57 |
58 | prio_map = {
59 | 'EMERG': syslog.LOG_EMERG,
60 | 'ALERT': syslog.LOG_ALERT,
61 | 'CRIT': syslog.LOG_CRIT,
62 | 'ERR': syslog.LOG_ERR,
63 | 'WARNING': syslog.LOG_WARNING,
64 | 'NOTICE': syslog.LOG_NOTICE,
65 | 'INFO': syslog.LOG_INFO,
66 | 'DEBUG': syslog.LOG_DEBUG,
67 | }
68 |
69 | facility_map = {
70 | 'KERN': syslog.LOG_KERN,
71 | 'USER': syslog.LOG_USER,
72 | 'MAIL': syslog.LOG_MAIL,
73 | 'DAEMON': syslog.LOG_DAEMON,
74 | 'AUTH': syslog.LOG_AUTH,
75 | 'LPR': syslog.LOG_LPR,
76 | 'NEWS': syslog.LOG_NEWS,
77 | 'UUCP': syslog.LOG_UUCP,
78 | 'CRON': syslog.LOG_CRON,
79 | 'SYSLOG': syslog.LOG_SYSLOG,
80 | 'LOCAL0': syslog.LOG_LOCAL0,
81 | 'LOCAL1': syslog.LOG_LOCAL1,
82 | 'LOCAL2': syslog.LOG_LOCAL2,
83 | 'LOCAL3': syslog.LOG_LOCAL3,
84 | 'LOCAL4': syslog.LOG_LOCAL4,
85 | 'LOCAL5': syslog.LOG_LOCAL5,
86 | 'LOCAL6': syslog.LOG_LOCAL6,
87 | 'LOCAL7': syslog.LOG_LOCAL7,
88 | }
89 |
90 | if prio not in prio_map:
91 | raise ValueError('invalid syslog priority')
92 | elif facility not in facility_map:
93 | raise ValueError('invalid syslog facility')
94 |
95 | msg = fmt.format(title=title, message=message)
96 | for line in msg.splitlines():
97 | syslog.syslog(facility_map[facility] | prio_map[prio], line)
98 |
--------------------------------------------------------------------------------
/ntfy/backends/telegram.py:
--------------------------------------------------------------------------------
1 | from os import makedirs, path
2 | from appdirs import user_config_dir
3 | from telegram_send import configure, send
4 | import asyncio
5 |
6 | config_dir = user_config_dir('ntfy', 'dschep')
7 | config_file = path.join(config_dir, 'telegram.ini')
8 |
9 | def notify(title, message, retcode=None):
10 | """Sends message over Telegram using telegram-send, title is ignored."""
11 | if not path.exists(config_file):
12 | if not path.exists(config_dir):
13 | makedirs(config_dir)
14 | print("Follow the instructions to configure the Telegram backend.\n")
15 | asyncio.run(configure(config_file))
16 | asyncio.run(send(messages=[message], conf=config_file))
--------------------------------------------------------------------------------
/ntfy/backends/termux.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 |
3 | from subprocess import check_call
4 |
5 |
6 | def notify(title, message, retcode=None):
7 | """
8 | Termux:API backend.
9 | """
10 |
11 | check_call(['termux-notification', '--content', message, '--title', title])
12 |
--------------------------------------------------------------------------------
/ntfy/backends/win32.py:
--------------------------------------------------------------------------------
1 | # -- coding: utf-8 --
2 |
3 | import os
4 |
5 | from ..data import icon
6 |
7 |
8 | def notify(title, message, icon=icon.ico, retcode=None):
9 | """
10 | Optional parameters:
11 | * ``icon`` - path to an ICO file to display instead of the ntfy icon
12 | """
13 |
14 | import win32api
15 | import win32con
16 | import win32gui
17 |
18 | class WindowsBalloonTip:
19 | def __init__(self, title, msg):
20 | message_map = {
21 | win32con.WM_DESTROY: self.OnDestroy,
22 | }
23 | # Register the Window class.
24 | wc = win32gui.WNDCLASS()
25 | hinst = wc.hInstance = win32api.GetModuleHandle(None)
26 | wc.lpszClassName = "PythonTaskbar"
27 | wc.lpfnWndProc = message_map # could also specify a wndproc.
28 | classAtom = win32gui.RegisterClass(wc)
29 | # Create the Window.
30 | style = win32con.WS_OVERLAPPED | win32con.WS_SYSMENU
31 | self.hwnd = win32gui.CreateWindow(
32 | classAtom, "Taskbar", style, 0, 0, win32con.CW_USEDEFAULT,
33 | win32con.CW_USEDEFAULT, 0, 0, hinst, None)
34 | win32gui.UpdateWindow(self.hwnd)
35 | iconPathName = os.path.abspath(icon)
36 | icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE
37 | try:
38 | hicon = win32gui.LoadImage(
39 | hinst, iconPathName, win32con.IMAGE_ICON, 0, 0, icon_flags)
40 | except:
41 | hicon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION)
42 | flags = win32gui.NIF_ICON | win32gui.NIF_MESSAGE | win32gui.NIF_TIP
43 | nid = (self.hwnd, 0, flags, win32con.WM_USER + 20, hicon,
44 | "tooltip")
45 | win32gui.Shell_NotifyIcon(win32gui.NIM_ADD, nid)
46 | win32gui.Shell_NotifyIcon(
47 | win32gui.NIM_MODIFY,
48 | (self.hwnd, 0, win32gui.NIF_INFO, win32con.WM_USER + 20, hicon,
49 | "Balloon tooltip", title, 200, msg),
50 | )
51 | win32gui.DestroyWindow(self.hwnd)
52 | win32gui.UnregisterClass(wc.lpszClassName, None)
53 |
54 | def OnDestroy(self, hwnd, msg, wparam, lparam):
55 | win32api.PostQuitMessage(0) # Terminate the app.
56 |
57 | WindowsBalloonTip(message, title)
58 |
--------------------------------------------------------------------------------
/ntfy/backends/xmpp.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 |
4 | import sleekxmpp
5 |
6 |
7 | class NtfySendMsgBot(sleekxmpp.ClientXMPP):
8 | """
9 | Modified the commented sleekxmpp example:
10 | http://sleekxmpp.com/getting_started/sendlogout.html
11 |
12 | NOTE: supplying mtype='chat' was required for
13 | Google Hangouts to work
14 | """
15 |
16 | def __init__(self, jid, password, recipient, title, message, mtype=None):
17 | super(NtfySendMsgBot, self).__init__(jid, password)
18 |
19 | self.recipient = recipient
20 | self.title = title
21 | self.msg = message
22 | self.mtype = mtype
23 |
24 | self.add_event_handler("session_start", self.start)
25 |
26 | def start(self, event):
27 |
28 | self.send_presence()
29 | self.get_roster()
30 | msg_args = {
31 | 'mto': self.recipient,
32 | 'msubject': self.title,
33 | 'mbody': self.msg
34 | }
35 | if self.mtype:
36 | msg_args['mtype'] = self.mtype
37 |
38 | self.send_message(**msg_args)
39 |
40 | self.disconnect(wait=True)
41 |
42 |
43 | def notify(title,
44 | message,
45 | jid,
46 | password,
47 | recipient,
48 | hostname=None,
49 | port=5222,
50 | path_to_certs=None,
51 | mtype=None,
52 | retcode=None):
53 | """
54 | Optional parameters
55 | * ``hostname`` (if not from jid)
56 | * ``port``
57 | * ``path_to_certs``
58 | * ``mtype`` ('chat' required for Google Hangouts)
59 |
60 | To verify the SSL certificates offered by a server:
61 | path_to_certs = "path/to/ca/cert"
62 |
63 | Without dnspython library installed, you will need
64 | to specify the server hostname if it doesn't match the jid.
65 |
66 | For example, to use Google Talk you would need to use:
67 | hostname = 'talk.google.com'
68 |
69 | Specify port if other than 5222.
70 | NOTE: Ignored without specified hostname
71 | """
72 |
73 | xmpp_bot = NtfySendMsgBot(jid, password, recipient, title, message, mtype)
74 |
75 | # NOTE: Below plugins weren't needed for Google Hangouts
76 | # but may be useful (from original sleekxmpp example)
77 | # xmpp_bot.register_plugin('xep_0030') # Service Discovery
78 | # xmpp_bot.register_plugin('xep_0199') # XMPP Ping
79 |
80 | if path_to_certs and os.path.isdir(path_to_certs):
81 | xmpp_bot.ca_certs = path_to_certs
82 |
83 | # Connect to the XMPP server and start processing XMPP stanzas.
84 | if xmpp_bot.connect(*([(hostname, int(port)) if hostname else []])):
85 | xmpp_bot.process(block=True)
86 | else:
87 | logging.getLogger(__name__).error('Unable to connect', exc_info=True)
88 |
--------------------------------------------------------------------------------
/ntfy/cli.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import logging
3 | import logging.config
4 | import sys
5 | from os import environ, path
6 | from subprocess import PIPE, STDOUT, Popen
7 | from time import time
8 |
9 | from . import __version__, default_title, notify
10 | from .config import (DEFAULT_CONFIG, OLD_DEFAULT_CONFIG, SITE_DEFAULT_CONFIG,
11 | load_config)
12 | from .data import scripts
13 |
14 | try:
15 | from shlex import quote as sh_quote
16 | except ImportError:
17 | from pipes import quote as sh_quote
18 |
19 | try:
20 | from emoji import emojize
21 | except ImportError:
22 | emojize = None
23 |
24 | try:
25 | import psutil
26 | except ImportError:
27 | psutil = None
28 |
29 | try:
30 | from .terminal import is_focused
31 | except ImportError:
32 |
33 | def is_focused():
34 | return True
35 |
36 |
37 | def run_cmd(args):
38 | if getattr(args, 'pid', False):
39 | return watch_pid(args)
40 | if not args.command:
41 | if args.formatter:
42 | args.command, retcode, duration = args.formatter
43 | args.command, retcode, duration = ([args.command], int(retcode),
44 | int(duration))
45 | args.option.setdefault('linux', {}).setdefault('transient', 'true')
46 | stdout, stderr = None, None
47 | else:
48 | sys.stderr.write('usage: ntfy done [-h|-L N] command\n'
49 | 'ntfy done: error: the following arguments '
50 | 'are required: command\n')
51 | sys.exit(1)
52 | else:
53 | if args.stdout and args.stderr:
54 | out = PIPE
55 | err = STDOUT
56 | else:
57 | out = PIPE if args.stdout else None
58 | err = PIPE if args.stderr else None
59 | start_time = time()
60 | process = Popen(args.command, stdout=out, stderr=err)
61 | stdout, stderr = process.communicate()
62 | process.wait()
63 | duration = time() - start_time
64 | retcode = process.returncode
65 | if args.longer_than is not None and duration <= args.longer_than:
66 | return None, None
67 | if args.unfocused_only and is_focused():
68 | return None, None
69 | message = _result_message(args.command if not args.hide_command else None,
70 | retcode, stdout, stderr, duration,
71 | emojize is not None and not args.no_emoji)
72 | return message, retcode
73 |
74 |
75 | def _result_message(command, return_code, stdout, stderr, duration, emoji):
76 | if emoji:
77 | prefix = ':white_check_mark: ' if return_code == 0 else ':x: '
78 | else:
79 | prefix = ''
80 | if return_code == 0:
81 | result = 'succeeded'
82 | else:
83 | result = 'failed (code {:d})'.format(return_code)
84 | if command is None:
85 | command = 'Your command'
86 | else:
87 | command = '"{command}"'.format(command=' '.join(command))
88 | if stdout is not None or stderr is not None:
89 | all_output = ':\n{}{}'.format(stdout
90 | if stdout is not None else '', stderr
91 | if stderr is not None else '')
92 | else:
93 | all_output = ''
94 | template = '{prefix}{command} {result} in {:d}:{:02d} minutes{output}'
95 | return template.format(
96 | prefix=prefix,
97 | command=command,
98 | result=result,
99 | output=all_output,
100 | *map(int, divmod(duration, 60)))
101 |
102 |
103 | def watch_pid(args):
104 | if psutil is None: # pragma: no cover
105 | logging.error(
106 | "This command requires psutil module. Pleases install psutil.")
107 | sys.exit(1)
108 | try:
109 | p = psutil.Process(args.pid)
110 | cmd = p.cmdline()
111 | start_time = p.create_time()
112 | except psutil.NoSuchProcess:
113 | logging.error("PID {} not found".format(args.pid))
114 | sys.exit(1)
115 | ret = None
116 | try:
117 | ret = p.wait()
118 | except psutil.NoSuchProcess: # pragma: no cover
119 | pass # this happens when the PID disapears
120 | duration = time() - start_time
121 | return 'PID[{}]: "{}" finished in {:d}:{:02d} minutes'.format(
122 | p.pid, ' '.join(cmd), *map(int, divmod(duration, 60))), ret
123 |
124 |
125 | def auto_done(args):
126 | if args.longer_than:
127 | print('export AUTO_NTFY_DONE_LONGER_THAN=-L{}'.format(
128 | args.longer_than))
129 | if args.unfocused_only:
130 | print('export AUTO_NTFY_DONE_UNFOCUSED_ONLY=-b')
131 | if args.shell == 'bash':
132 | print('source {}'.format(sh_quote(scripts['bash-preexec.sh'])))
133 | print('source {}'.format(sh_quote(scripts['auto-ntfy-done.sh'])))
134 | print("# To use ntfy's shell integration, run "
135 | "this and add it to your shell's rc file:")
136 | print('# eval "$(ntfy shell-integration)"')
137 | return None, None
138 |
139 |
140 | class BackendOptionAction(argparse.Action):
141 | backend = None
142 |
143 | def __call__(self, parser, args, values, option_string=None):
144 | if self.dest == 'backend':
145 | self.__class__.backend = values
146 | if args.backend is None:
147 | args.backend = []
148 | args.backend.append(values)
149 | elif self.dest == 'option':
150 | if args.option is None:
151 | args.option = {}
152 | args.option.setdefault(self.__class__.backend,
153 | {})[values[0]] = values[1]
154 | else:
155 | raise Exception("'BackendOptionAction only supports dest of "
156 | "'backend' and 'option'")
157 |
158 |
159 | parser = argparse.ArgumentParser(
160 | description='Send push notification when command finishes')
161 |
162 | parser.add_argument(
163 | '-c',
164 | '--config',
165 | help='config file to use (default: {})'.format(DEFAULT_CONFIG))
166 | parser.add_argument(
167 | '-b',
168 | '--backend',
169 | action=BackendOptionAction,
170 | help='override backend specified in config')
171 | parser.add_argument(
172 | '-o',
173 | '--option',
174 | nargs=2,
175 | default=None,
176 | action=BackendOptionAction,
177 | metavar=('key', 'value'),
178 | help='backend specific options')
179 | parser.add_argument(
180 | '-l',
181 | '--log-level',
182 | action='store',
183 | default='WARNING',
184 | choices=['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'],
185 | help=('Specify the how verbose CLI output is '
186 | '(default: WARNING)'))
187 | parser.add_argument(
188 | '-v',
189 | '--verbose',
190 | dest='log_level',
191 | action='store_const',
192 | const='DEBUG',
193 | help='a shortcut for --log-level=DEBUG')
194 | parser.add_argument(
195 | '-q',
196 | '--quiet',
197 | dest='log_level',
198 | action='store_const',
199 | const='CRITICAL',
200 | help='a shortcut for --log-level=CRITICAL')
201 | parser.add_argument('--version', action='version', version=__version__)
202 | if emojize is not None:
203 | parser.add_argument(
204 | '-E', '--no-emoji', action='store_true', help='Disable emoji support')
205 |
206 | parser.add_argument(
207 | '-t',
208 | '--title',
209 | help='a title for the notification (default: {})'.format(default_title))
210 |
211 | subparsers = parser.add_subparsers()
212 |
213 | send_parser = subparsers.add_parser('send', help='send a notification')
214 | send_parser.add_argument('message', help='notification message')
215 |
216 |
217 | def default_sender(args):
218 | return args.message, 0
219 |
220 |
221 | send_parser.set_defaults(func=default_sender)
222 |
223 | done_parser = subparsers.add_parser(
224 | 'done', help='run a command and send a notification when done')
225 | done_parser.add_argument(
226 | 'command', nargs=argparse.REMAINDER, help='command to run')
227 | done_parser.add_argument(
228 | '-L',
229 | '--longer-than',
230 | type=int,
231 | metavar='N',
232 | help="Only notify if the command runs longer than N seconds")
233 | done_parser.add_argument(
234 | '-b',
235 | '--background-only',
236 | action='store_true',
237 | default=False,
238 | dest='unfocused_only',
239 | help="Only notify if shell isn't in the foreground")
240 | done_parser.add_argument(
241 | '--formatter',
242 | metavar=('command', 'retcode', 'duration'),
243 | nargs=3,
244 | help="Format and send cmd, retcode & duration instead of running command. "
245 | "Used internally by shell-integration")
246 | if psutil is not None:
247 | done_parser.add_argument(
248 | '-p',
249 | '--pid',
250 | type=int,
251 | help="Watch a PID instead of running a new command")
252 | done_parser.add_argument(
253 | '-o',
254 | '--stdout',
255 | action='store_true',
256 | help="Capture and send standard output")
257 | done_parser.add_argument(
258 | '-e',
259 | '--stderr',
260 | action='store_true',
261 | help="Capture and send standard error")
262 | done_parser.add_argument(
263 | '-H',
264 | '--hide-command',
265 | action='store_true',
266 | default=False,
267 | help="Do not display the executed command in any notifications")
268 | done_parser.set_defaults(func=run_cmd)
269 |
270 | shell_integration_parser = subparsers.add_parser(
271 | 'shell-integration',
272 | help='automatically get notifications when long running commands finish')
273 | shell_integration_parser.add_argument(
274 | '-s',
275 | '--shell',
276 | default=path.split(environ.get('SHELL', ''))[1],
277 | choices=['bash', 'zsh'],
278 | help='The shell to integrate ntfy with (default: $SHELL)')
279 | shell_integration_parser.add_argument(
280 | '-L',
281 | '--longer-than',
282 | default=10,
283 | type=int,
284 | metavar='N',
285 | help="Only notify if the command runs longer than N seconds")
286 | shell_integration_parser.add_argument(
287 | '-f',
288 | '--foreground-too',
289 | action='store_false',
290 | default=True,
291 | dest='unfocused_only',
292 | help="Also notify if shell is in the foreground")
293 | shell_integration_parser.set_defaults(func=auto_done)
294 |
295 |
296 | def main(cli_args=None):
297 | if cli_args is not None:
298 | args = parser.parse_args(cli_args)
299 | else:
300 | args = parser.parse_args()
301 |
302 | logging.config.dictConfig({
303 | 'version': 1,
304 | 'disable_existing_loggers': False,
305 | 'formatters': {
306 | 'default': {
307 | 'format': '%(levelname)s: %(message)s'
308 | },
309 | },
310 | 'handlers': {
311 | 'default': {
312 | 'level': args.log_level,
313 | 'class': 'logging.StreamHandler',
314 | 'formatter': 'default',
315 | },
316 | },
317 | 'loggers': {
318 | '': {
319 | 'handlers': ['default'],
320 | 'level': args.log_level,
321 | 'propagate': True,
322 | }
323 | }
324 | })
325 |
326 | if args.config is not None:
327 | config = load_config(args.config)
328 | elif path.exists(path.expanduser(DEFAULT_CONFIG)):
329 | config = load_config(DEFAULT_CONFIG)
330 | elif path.exists(OLD_DEFAULT_CONFIG):
331 | config = load_config(OLD_DEFAULT_CONFIG)
332 | elif path.exists(path.expanduser(SITE_DEFAULT_CONFIG)):
333 | config = load_config(SITE_DEFAULT_CONFIG)
334 | else: # get default config and print message about missing file
335 | config = load_config()
336 |
337 | if 'NTFY_BACKENDS' in environ:
338 | config['backends'] = environ['NTFY_BACKENDS'].split(',')
339 |
340 | if args.backend:
341 | config['backends'] = args.backend
342 |
343 | if args.option is None:
344 | args.option = {}
345 | for backend, backend_options in args.option.items():
346 | if backend is not None:
347 | config.setdefault(backend, {}).update(backend_options)
348 |
349 | if getattr(args, 'func', None) == run_cmd and args.longer_than is None and\
350 | 'longer_than' in config:
351 | args.longer_than = config['longer_than']
352 |
353 | if getattr(args, 'func', None) == run_cmd and 'hide_command' in config:
354 | args.hide_command = config['hide_command']
355 |
356 | if hasattr(args, 'func'):
357 | message, retcode = args.func(args)
358 | if message is None:
359 | return 0
360 | if emojize is not None and not args.no_emoji:
361 | message = emojize(message, language='alias')
362 | return notify(
363 | message,
364 | args.title,
365 | config,
366 | retcode=retcode,
367 | **dict(args.option.get(None, [])))
368 | else:
369 | parser.print_help()
370 |
371 |
372 | if __name__ == '__main__':
373 | sys.exit(main())
374 |
--------------------------------------------------------------------------------
/ntfy/config.py:
--------------------------------------------------------------------------------
1 | import errno
2 | import logging
3 | from os.path import join as join_path
4 | from os.path import expanduser
5 | from sys import exit
6 |
7 | import requests
8 | from appdirs import site_config_dir, user_config_dir
9 | from ruamel import yaml
10 |
11 | from . import __version__
12 | from .default_config import config as default_configuration
13 |
14 | if yaml.version_info < (0, 15):
15 | safe_load = yaml.safe_load
16 | else:
17 | yml = yaml.YAML(typ='safe', pure=True)
18 | safe_load = lambda stream: yml.load(stream)
19 |
20 | DEFAULT_CONFIG = join_path(user_config_dir('ntfy', 'dschep'), 'ntfy.yml')
21 | SITE_DEFAULT_CONFIG = join_path(site_config_dir('ntfy', 'dschep'), 'ntfy.yml')
22 | OLD_DEFAULT_CONFIG = expanduser('~/.ntfy.yml')
23 |
24 | USER_AGENT = 'ntfy/{version} {default_user_agent}'.format(
25 | version=__version__,
26 | default_user_agent=requests.utils.default_user_agent())
27 |
28 |
29 | def load_config(config_path=DEFAULT_CONFIG):
30 | logger = logging.getLogger(__name__)
31 |
32 | try:
33 | config = safe_load(open(expanduser(config_path)))
34 | except IOError as e:
35 | if e.errno == errno.ENOENT and config_path == DEFAULT_CONFIG:
36 | logger.info('{} not found'.format(config_path))
37 | config = default_configuration
38 | else:
39 | logger.error(
40 | 'Failed to open {}'.format(config_path), exc_info=True)
41 | exit(1)
42 | except ValueError as e:
43 | logger.error('Failed to load {}'.format(config_path), exc_info=True)
44 | exit(1)
45 |
46 | if 'backend' in config:
47 | logger.warning(
48 | "The 'backend' config option is deprecated, use 'backends'")
49 | if 'backends' in config:
50 | logger.warning("Both 'backend' and 'backends' in config, "
51 | "ignoring 'backend'.")
52 | else:
53 | config['backends'] = [config['backend']]
54 |
55 | return config
56 |
--------------------------------------------------------------------------------
/ntfy/data.py:
--------------------------------------------------------------------------------
1 | from os import makedirs, path
2 | from pkgutil import get_data
3 | from sys import argv
4 |
5 | from appdirs import user_data_dir
6 |
7 | ntfy_data_dir = user_data_dir('ntfy', 'dschep')
8 | if not path.isdir(ntfy_data_dir):
9 | makedirs(ntfy_data_dir)
10 |
11 |
12 | class icon(object):
13 | png = None
14 | ico = None
15 |
16 |
17 | progmtime = path.isfile(argv[0]) and path.getmtime(argv[0])
18 | for fmt in ['png', 'ico']:
19 | icon_path = path.abspath(path.join(ntfy_data_dir, 'icon.' + fmt))
20 | setattr(icon, fmt, icon_path)
21 | if not path.isfile(icon_path) or progmtime > path.getmtime(icon_path):
22 | with open(icon_path, 'wb') as icon_file:
23 | icon_file.write(get_data('ntfy', 'icon.' + fmt))
24 |
25 | scripts = {}
26 | for script in ['auto-ntfy-done.sh', 'bash-preexec.sh']:
27 | script_path = path.abspath(path.join(ntfy_data_dir, script))
28 | scripts[script] = script_path
29 | if not path.isfile(script_path) or progmtime > path.getmtime(script_path):
30 | with open(script_path, 'wb') as script_file:
31 | script_file.write(
32 | get_data('ntfy', path.join('shell_integration', script)))
33 |
--------------------------------------------------------------------------------
/ntfy/default_config.py:
--------------------------------------------------------------------------------
1 | config = {}
2 |
--------------------------------------------------------------------------------
/ntfy/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dschep/ntfy/c4ef3b54f26390b7b51ec612f41f11a1ba284227/ntfy/icon.ico
--------------------------------------------------------------------------------
/ntfy/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dschep/ntfy/c4ef3b54f26390b7b51ec612f41f11a1ba284227/ntfy/icon.png
--------------------------------------------------------------------------------
/ntfy/shell_integration/auto-ntfy-done.sh:
--------------------------------------------------------------------------------
1 | # In bash this requires https://github.com/rcaloras/bash-preexec
2 | # If sourcing this via ntfy auto-done, it is sourced for you.
3 |
4 | # Default to ignoring some well known interactive programs
5 | AUTO_NTFY_DONE_IGNORE=${AUTO_NTFY_DONE_IGNORE:-ntfy emacs htop info less mail man meld most mutt nano screen ssh tail tmux top vi vim watch}
6 | # Bash option example
7 | #AUTO_NTFY_DONE_OPTS='-b default'
8 | # Zsh option example
9 | #AUTO_NTFY_DONE_OPTS=(-b default)
10 | # notify for unfocused only (Used by ntfy internally)
11 | #AUTO_NTFY_DONE_UNFOCUSED_ONLY=-b
12 | # notify for commands runing longer than N sec only (Used by ntfy internally)
13 | #AUTO_NTFY_DONE_LONGER_THAN=-L10
14 |
15 | function _ntfy_precmd () {
16 | local ret_value="$?"
17 | [ -n "$ntfy_start_time" ] || return
18 | local duration=$(( $(date +%s) - $ntfy_start_time ))
19 | ntfy_start_time=''
20 |
21 | local appname=$(basename "${ntfy_command%% *}")
22 | [[ " $AUTO_NTFY_DONE_IGNORE " == *" $appname "* ]] && return
23 |
24 | (ntfy $AUTO_NTFY_DONE_OPTS done \
25 | $AUTO_NTFY_DONE_UNFOCUSED_ONLY $AUTO_NTFY_DONE_LONGER_THAN \
26 | --formatter "$ntfy_command" "$ret_value" "$duration" &)
27 | }
28 |
29 | function _ntfy_preexec () {
30 | ntfy_start_time=$(date +%s)
31 | ntfy_command="$1"
32 | }
33 |
34 | function _contains_element() {
35 | local e
36 | for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 0; done
37 | return 1
38 | }
39 |
40 | if ! _contains_element _ntfy_preexec "${preexec_functions[@]}"; then
41 | preexec_functions+=(_ntfy_preexec)
42 | fi
43 |
44 | if ! _contains_element _ntfy_precmd "${precmd_functions[@]}"; then
45 | precmd_functions+=(_ntfy_precmd)
46 | fi
47 |
--------------------------------------------------------------------------------
/ntfy/shell_integration/bash-preexec.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # bash-preexec.sh -- Bash support for ZSH-like 'preexec' and 'precmd' functions.
4 | # https://github.com/rcaloras/bash-preexec
5 | #
6 | #
7 | # 'preexec' functions are executed before each interactive command is
8 | # executed, with the interactive command as its argument. The 'precmd'
9 | # function is executed before each prompt is displayed.
10 | #
11 | # Author: Ryan Caloras (ryan@bashhub.com)
12 | # Forked from Original Author: Glyph Lefkowitz
13 | #
14 | # V0.3.7
15 | #
16 |
17 | # General Usage:
18 | #
19 | # 1. Source this file at the end of your bash profile so as not to interfere
20 | # with anything else that's using PROMPT_COMMAND.
21 | #
22 | # 2. Add any precmd or preexec functions by appending them to their arrays:
23 | # e.g.
24 | # precmd_functions+=(my_precmd_function)
25 | # precmd_functions+=(some_other_precmd_function)
26 | #
27 | # preexec_functions+=(my_preexec_function)
28 | #
29 | # 3. Consider changing anything using the DEBUG trap or PROMPT_COMMAND
30 | # to use preexec and precmd instead. Preexisting usages will be
31 | # preserved, but doing so manually may be less surprising.
32 | #
33 | # Note: This module requires two Bash features which you must not otherwise be
34 | # using: the "DEBUG" trap, and the "PROMPT_COMMAND" variable. If you override
35 | # either of these after bash-preexec has been installed it will most likely break.
36 |
37 | # Avoid duplicate inclusion
38 | if [[ "$__bp_imported" == "defined" ]]; then
39 | return 0
40 | fi
41 | __bp_imported="defined"
42 |
43 | # Should be available to each precmd and preexec
44 | # functions, should they want it.
45 | __bp_last_ret_value="$?"
46 | __bp_last_argument_prev_command="$_"
47 |
48 | __bp_inside_precmd=0
49 | __bp_inside_preexec=0
50 |
51 | # Remove ignorespace and or replace ignoreboth from HISTCONTROL
52 | # so we can accurately invoke preexec with a command from our
53 | # history even if it starts with a space.
54 | __bp_adjust_histcontrol() {
55 | local histcontrol
56 | histcontrol="${HISTCONTROL//ignorespace}"
57 | # Replace ignoreboth with ignoredups
58 | if [[ "$histcontrol" == *"ignoreboth"* ]]; then
59 | histcontrol="ignoredups:${histcontrol//ignoreboth}"
60 | fi;
61 | export HISTCONTROL="$histcontrol"
62 | }
63 |
64 | # This variable describes whether we are currently in "interactive mode";
65 | # i.e. whether this shell has just executed a prompt and is waiting for user
66 | # input. It documents whether the current command invoked by the trace hook is
67 | # run interactively by the user; it's set immediately after the prompt hook,
68 | # and unset as soon as the trace hook is run.
69 | __bp_preexec_interactive_mode=""
70 |
71 | __bp_trim_whitespace() {
72 | local var=$@
73 | var="${var#"${var%%[![:space:]]*}"}" # remove leading whitespace characters
74 | var="${var%"${var##*[![:space:]]}"}" # remove trailing whitespace characters
75 | echo -n "$var"
76 | }
77 |
78 | # This function is installed as part of the PROMPT_COMMAND;
79 | # It sets a variable to indicate that the prompt was just displayed,
80 | # to allow the DEBUG trap to know that the next command is likely interactive.
81 | __bp_interactive_mode() {
82 | __bp_preexec_interactive_mode="on";
83 | }
84 |
85 |
86 | # This function is installed as part of the PROMPT_COMMAND.
87 | # It will invoke any functions defined in the precmd_functions array.
88 | __bp_precmd_invoke_cmd() {
89 | # Save the returned value from our last command. Note: this MUST be the
90 | # first thing done in this function.
91 | __bp_last_ret_value="$?"
92 |
93 | # Don't invoke precmds if we are inside an execution of an "original
94 | # prompt command" by another precmd execution loop. This avoids infinite
95 | # recursion.
96 | if (( __bp_inside_precmd > 0 )); then
97 | return
98 | fi
99 | local __bp_inside_precmd=1
100 |
101 | # Invoke every function defined in our function array.
102 | local precmd_function
103 | for precmd_function in "${precmd_functions[@]}"; do
104 |
105 | # Only execute this function if it actually exists.
106 | # Test existence of functions with: declare -[Ff]
107 | if type -t "$precmd_function" 1>/dev/null; then
108 | __bp_set_ret_value "$__bp_last_ret_value" "$__bp_last_argument_prev_command"
109 | "$precmd_function"
110 | fi
111 | done
112 | }
113 |
114 | # Sets a return value in $?. We may want to get access to the $? variable in our
115 | # precmd functions. This is available for instance in zsh. We can simulate it in bash
116 | # by setting the value here.
117 | __bp_set_ret_value() {
118 | return $1
119 | }
120 |
121 | __bp_in_prompt_command() {
122 |
123 | local prompt_command_array
124 | IFS=';' read -ra prompt_command_array <<< "$PROMPT_COMMAND"
125 |
126 | local trimmed_arg
127 | trimmed_arg=$(__bp_trim_whitespace "$1")
128 |
129 | local command
130 | for command in "${prompt_command_array[@]}"; do
131 | local trimmed_command
132 | trimmed_command=$(__bp_trim_whitespace "$command")
133 | # Only execute each function if it actually exists.
134 | if [[ "$trimmed_command" == "$trimmed_arg" ]]; then
135 | return 0
136 | fi
137 | done
138 |
139 | return 1
140 | }
141 |
142 | # This function is installed as the DEBUG trap. It is invoked before each
143 | # interactive prompt display. Its purpose is to inspect the current
144 | # environment to attempt to detect if the current command is being invoked
145 | # interactively, and invoke 'preexec' if so.
146 | __bp_preexec_invoke_exec() {
147 | # Save the contents of $_ so that it can be restored later on.
148 | # https://stackoverflow.com/questions/40944532/bash-preserve-in-a-debug-trap#40944702
149 | __bp_last_argument_prev_command="$1"
150 |
151 | # Don't invoke preexecs if we are inside of another preexec.
152 | if (( __bp_inside_preexec > 0 )); then
153 | return
154 | fi
155 | local __bp_inside_preexec=1
156 |
157 | # Checks if the file descriptor is not standard out (i.e. '1')
158 | # __bp_delay_install checks if we're in test. Needed for bats to run.
159 | # Prevents preexec from being invoked for functions in PS1
160 | if [[ ! -t 1 && -z "$__bp_delay_install" ]]; then
161 | return
162 | fi
163 |
164 | if [[ -n "$COMP_LINE" ]]; then
165 | # We're in the middle of a completer. This obviously can't be
166 | # an interactively issued command.
167 | return
168 | fi
169 | if [[ -z "$__bp_preexec_interactive_mode" ]]; then
170 | # We're doing something related to displaying the prompt. Let the
171 | # prompt set the title instead of me.
172 | return
173 | else
174 | # If we're in a subshell, then the prompt won't be re-displayed to put
175 | # us back into interactive mode, so let's not set the variable back.
176 | # In other words, if you have a subshell like
177 | # (sleep 1; sleep 2)
178 | # You want to see the 'sleep 2' as a set_command_title as well.
179 | if [[ 0 -eq "$BASH_SUBSHELL" ]]; then
180 | __bp_preexec_interactive_mode=""
181 | fi
182 | fi
183 |
184 | if __bp_in_prompt_command "$BASH_COMMAND"; then
185 | # If we're executing something inside our prompt_command then we don't
186 | # want to call preexec. Bash prior to 3.1 can't detect this at all :/
187 | __bp_preexec_interactive_mode=""
188 | return
189 | fi
190 |
191 | local this_command
192 | this_command=$(HISTTIMEFORMAT= builtin history 1 | { IFS=" " read -r _ this_command; echo "$this_command"; })
193 |
194 | # Sanity check to make sure we have something to invoke our function with.
195 | if [[ -z "$this_command" ]]; then
196 | return
197 | fi
198 |
199 | # If none of the previous checks have returned out of this function, then
200 | # the command is in fact interactive and we should invoke the user's
201 | # preexec functions.
202 |
203 | # Invoke every function defined in our function array.
204 | local preexec_function
205 | local preexec_function_ret_value
206 | local preexec_ret_value=0
207 | for preexec_function in "${preexec_functions[@]}"; do
208 |
209 | # Only execute each function if it actually exists.
210 | # Test existence of function with: declare -[fF]
211 | if type -t "$preexec_function" 1>/dev/null; then
212 | __bp_set_ret_value $__bp_last_ret_value
213 | "$preexec_function" "$this_command"
214 | preexec_function_ret_value="$?"
215 | if [[ "$preexec_function_ret_value" != 0 ]]; then
216 | preexec_ret_value="$preexec_function_ret_value"
217 | fi
218 | fi
219 | done
220 |
221 | # Restore the last argument of the last executed command, and set the return
222 | # value of the DEBUG trap to be the return code of the last preexec function
223 | # to return an error.
224 | # If `extdebug` is enabled a non-zero return value from any preexec function
225 | # will cause the user's command not to execute.
226 | # Run `shopt -s extdebug` to enable
227 | __bp_set_ret_value "$preexec_ret_value" "$__bp_last_argument_prev_command"
228 | }
229 |
230 | __bp_install() {
231 | # Exit if we already have this installed.
232 | if [[ "$PROMPT_COMMAND" == *"__bp_precmd_invoke_cmd"* ]]; then
233 | return 1;
234 | fi
235 |
236 | trap '__bp_preexec_invoke_exec "$_"' DEBUG
237 |
238 | # Preserve any prior DEBUG trap as a preexec function
239 | local prior_trap=$(sed "s/[^']*'\(.*\)'[^']*/\1/" <<<"$__bp_trap_string")
240 | unset __bp_trap_string
241 | if [[ -n "$prior_trap" ]]; then
242 | eval '__bp_original_debug_trap() {
243 | '"$prior_trap"'
244 | }'
245 | preexec_functions+=(__bp_original_debug_trap)
246 | fi
247 |
248 | # Adjust our HISTCONTROL Variable if needed.
249 | __bp_adjust_histcontrol
250 |
251 |
252 | # Issue #25. Setting debug trap for subshells causes sessions to exit for
253 | # backgrounded subshell commands (e.g. (pwd)& ). Believe this is a bug in Bash.
254 | #
255 | # Disabling this by default. It can be enabled by setting this variable.
256 | if [[ -n "$__bp_enable_subshells" ]]; then
257 |
258 | # Set so debug trap will work be invoked in subshells.
259 | set -o functrace > /dev/null 2>&1
260 | shopt -s extdebug > /dev/null 2>&1
261 | fi;
262 |
263 | # Install our hooks in PROMPT_COMMAND to allow our trap to know when we've
264 | # actually entered something.
265 | PROMPT_COMMAND="__bp_precmd_invoke_cmd; __bp_interactive_mode"
266 |
267 | # Add two functions to our arrays for convenience
268 | # of definition.
269 | precmd_functions+=(precmd)
270 | preexec_functions+=(preexec)
271 |
272 | # Since this function is invoked via PROMPT_COMMAND, re-execute PC now that it's properly set
273 | eval "$PROMPT_COMMAND"
274 | }
275 |
276 | # Sets our trap and __bp_install as part of our PROMPT_COMMAND to install
277 | # after our session has started. This allows bash-preexec to be inlucded
278 | # at any point in our bash profile. Ideally we could set our trap inside
279 | # __bp_install, but if a trap already exists it'll only set locally to
280 | # the function.
281 | __bp_install_after_session_init() {
282 |
283 | # Make sure this is bash that's running this and return otherwise.
284 | if [[ -z "$BASH_VERSION" ]]; then
285 | return 1;
286 | fi
287 |
288 | # If there's an existing PROMPT_COMMAND capture it and convert it into a function
289 | # So it is preserved and invoked during precmd.
290 | if [[ -n "$PROMPT_COMMAND" ]]; then
291 | eval '__bp_original_prompt_command() {
292 | '"$PROMPT_COMMAND"'
293 | }'
294 | precmd_functions+=(__bp_original_prompt_command)
295 | fi
296 |
297 | # Installation is finalized in PROMPT_COMMAND, which allows us to override the DEBUG
298 | # trap. __bp_install sets PROMPT_COMMAND to its final value, so these are only
299 | # invoked once.
300 | # It's necessary to clear any existing DEBUG trap in order to set it from the install function.
301 | # Using \n as it's the most universal delimiter of bash commands
302 | PROMPT_COMMAND=$'\n__bp_trap_string="$(trap -p DEBUG)"\ntrap DEBUG\n__bp_install\n'
303 | }
304 |
305 | # Run our install so long as we're not delaying it.
306 | if [[ -z "$__bp_delay_install" ]]; then
307 | __bp_install_after_session_init
308 | fi;
309 |
--------------------------------------------------------------------------------
/ntfy/terminal.py:
--------------------------------------------------------------------------------
1 | import shlex
2 | from os import environ, ttyname
3 | from subprocess import PIPE, Popen, check_output, CalledProcessError
4 | from sys import platform, stdout
5 |
6 |
7 | def linux_window_is_focused():
8 | xprop_cmd = shlex.split('xprop -root _NET_ACTIVE_WINDOW')
9 | try:
10 | xprop_window_id = int(check_output(xprop_cmd, stdout=PIPE, stderr=PIPE).split()[-1], 16)
11 | except CalledProcessError:
12 | return False
13 | except ValueError:
14 | return False
15 | except OSError as e:
16 | if 'No such file' in e.strerror:
17 | return False
18 | else:
19 | raise
20 | env_window_id = int(environ.get('WINDOWID', '0'))
21 | return env_window_id == xprop_window_id
22 |
23 |
24 | def osascript_tell(app, script):
25 | p = Popen(['osascript'], stdin=PIPE, stdout=PIPE)
26 | stdout, stderr = p.communicate(
27 | ('tell application "{}"\n{}\nend tell'.format(app, script)
28 | .encode('utf-8')))
29 | return stdout.decode('utf-8').rstrip('\n')
30 |
31 |
32 | def darwin_iterm2_shell_is_focused():
33 | focused_tty = osascript_tell(
34 | 'iTerm',
35 | 'tty of current session of current window',
36 | )
37 | return focused_tty == ttyname(stdout.fileno())
38 |
39 |
40 | def darwin_terminal_shell_is_focused():
41 | focused_tty = osascript_tell(
42 | 'Terminal',
43 | 'tty of (first tab of (first window whose frontmost is true) '
44 | 'whose selected is true)',
45 | )
46 | return focused_tty == ttyname(stdout.fileno())
47 |
48 |
49 | def darwin_app_shell_is_focused():
50 | current_appid = {
51 | 'iTerm.app': 'iTerm2',
52 | 'Apple_Terminal': 'Terminal',
53 | }.get(environ.get('TERM_PROGRAM'))
54 | focused_appid = osascript_tell(
55 | 'System Events',
56 | 'name of first application process whose frontmost is true',
57 | )
58 | if current_appid == focused_appid:
59 | return {
60 | 'Terminal': darwin_terminal_shell_is_focused,
61 | 'iTerm2': darwin_iterm2_shell_is_focused,
62 | }[current_appid]()
63 |
64 |
65 | def is_focused():
66 | if platform.startswith('linux') and environ.get('DISPLAY'):
67 | return linux_window_is_focused()
68 | elif platform == 'darwin':
69 | return darwin_app_shell_is_focused()
70 | else:
71 | return False
72 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file = README.rst
3 |
4 | [bdist_wheel]
5 | universal = 1
6 |
7 | [build_sphinx]
8 | source-dir = docs
9 | build-dir = docs/_build
10 | all_files = 1
11 |
12 | [upload_sphinx]
13 | upload-dir = docs/_build/html
14 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from sys import version_info
2 |
3 | from setuptools import find_packages, setup
4 |
5 | from ntfy import __version__
6 |
7 | deps = ['requests', 'ruamel.yaml', 'appdirs']
8 | extra_deps = {
9 | ':sys_platform == "win32"': ['pywin32'],
10 | ':sys_platform == "darwin"': ['pyobjc-core', 'pyobjc'],
11 | 'xmpp': ['sleekxmpp', 'dnspython3'],
12 | 'telegram': ['telegram-send'],
13 | 'instapush': ['instapush'],
14 | 'emoji': ['emoji >= 1.6.2'],
15 | 'pid':['psutil'],
16 | 'slack':['slack_sdk'],
17 | 'rocketchat':['rocketchat-API'],
18 | 'matrix':['matrix_client'],
19 | }
20 | test_deps = ['mock', 'sleekxmpp', 'emoji', 'psutil']
21 |
22 | long_description = "See the repo readme for mor information"
23 |
24 | setup(
25 | name='ntfy',
26 |
27 | version=__version__,
28 |
29 | description='A utility for sending push notifications',
30 | long_description=long_description,
31 |
32 | url='https://github.com/dschep/ntfy',
33 |
34 | author='Daniel Schep',
35 | author_email='dschep@gmail.com',
36 |
37 | license='GPLv3',
38 |
39 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers
40 | classifiers=[
41 | 'Development Status :: 5 - Production/Stable',
42 |
43 | 'Environment :: Console',
44 |
45 | 'Intended Audience :: End Users/Desktop',
46 |
47 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
48 |
49 | 'Programming Language :: Python :: 3',
50 | ],
51 |
52 | keywords='push notification',
53 |
54 | packages=find_packages(exclude=['tests', 'tests.*']),
55 | package_data={'ntfy': ['icon.png', 'icon.ico', 'shell_integration/*.sh']},
56 |
57 | install_requires=deps,
58 |
59 | extras_require=extra_deps,
60 |
61 | tests_require=test_deps,
62 | test_suite='tests',
63 |
64 | entry_points={
65 | 'console_scripts': [
66 | 'ntfy = ntfy.cli:main',
67 | ],
68 | },
69 | )
70 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dschep/ntfy/c4ef3b54f26390b7b51ec612f41f11a1ba284227/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_cli.py:
--------------------------------------------------------------------------------
1 | from time import time
2 | from unittest import TestCase, main
3 |
4 | from mock import MagicMock, Mock, patch
5 | from ntfy.cli import main as ntfy_main
6 | from ntfy.cli import auto_done, run_cmd
7 |
8 |
9 | def process_mock(returncode=0, stdout=None, stderr=None):
10 | process_mock = Mock()
11 | attrs = {
12 | 'communicate.return_value': (stdout, stderr),
13 | 'returncode': returncode
14 | }
15 | process_mock.configure_mock(**attrs)
16 | return process_mock
17 |
18 |
19 | class TestRunCmd(TestCase):
20 | @patch('ntfy.cli.Popen')
21 | def test_default(self, mock_Popen):
22 | mock_Popen.return_value = process_mock()
23 | args = MagicMock()
24 | args.longer_than = -1
25 | args.command = ['true']
26 | args.pid = None
27 | args.unfocused_only = False
28 | args.hide_command = False
29 | self.assertEqual(('"true" succeeded in 0:00 minutes', 0),
30 | run_cmd(args))
31 |
32 | @patch('ntfy.cli.Popen')
33 | def test_emoji(self, mock_Popen):
34 | mock_Popen.return_value = process_mock()
35 | args = MagicMock()
36 | args.longer_than = -1
37 | args.command = ['true']
38 | args.pid = None
39 | args.no_emoji = False
40 | args.unfocused_only = False
41 | args.hide_command = False
42 | self.assertEqual(
43 | (':white_check_mark: "true" succeeded in 0:00 minutes', 0),
44 | run_cmd(args))
45 |
46 | def tests_usage(self):
47 | args = MagicMock()
48 | args.pid = False
49 | args.formatter = False
50 | args.command = []
51 | self.assertRaises(SystemExit, run_cmd, args)
52 |
53 | @patch('ntfy.cli.Popen')
54 | def test_longerthan(self, mock_Popen):
55 | mock_Popen.return_value = process_mock()
56 | args = MagicMock()
57 | args.longer_than = 1
58 | args.command = ['true']
59 | args.pid = None
60 | args.unfocused_only = False
61 | args.hide_command = False
62 | self.assertEqual((None, None), run_cmd(args))
63 |
64 | @patch('ntfy.cli.Popen')
65 | def test_failure(self, mock_Popen):
66 | mock_Popen.return_value = process_mock(42)
67 | args = MagicMock()
68 | args.longer_than = -1
69 | args.command = ['false']
70 | args.pid = None
71 | args.unfocused_only = False
72 | args.hide_command = False
73 | self.assertEqual(('"false" failed (code 42) in 0:00 minutes', 42),
74 | run_cmd(args))
75 |
76 | @patch('ntfy.cli.Popen')
77 | def test_stdout(self, mock_Popen):
78 | mock_Popen.return_value = process_mock(stdout='output')
79 | args = MagicMock()
80 | args.longer_than = -1
81 | args.command = ['true']
82 | args.pid = None
83 | args.unfocused_only = False
84 | args.hide_command = False
85 | # not actually used
86 | args.stdout = True
87 | args.stderr = False
88 | self.assertEqual(('"true" succeeded in 0:00 minutes:\noutput', 0),
89 | run_cmd(args))
90 |
91 | @patch('ntfy.cli.Popen')
92 | def test_stderr(self, mock_Popen):
93 | mock_Popen.return_value = process_mock(stderr='error')
94 | args = MagicMock()
95 | args.longer_than = -1
96 | args.command = ['true']
97 | args.pid = None
98 | args.unfocused_only = False
99 | args.hide_command = False
100 | # not actually used
101 | args.stdout = False
102 | args.stderr = True
103 | self.assertEqual(('"true" succeeded in 0:00 minutes:\nerror', 0),
104 | run_cmd(args))
105 |
106 | @patch('ntfy.cli.Popen')
107 | def test_stdout_and_stderr(self, mock_Popen):
108 | mock_Popen.return_value = process_mock(stdout='output', stderr='error')
109 | args = MagicMock()
110 | args.longer_than = -1
111 | args.command = ['true']
112 | args.pid = None
113 | args.unfocused_only = False
114 | args.hide_command = False
115 | # not actually used
116 | args.stdout = True
117 | args.stderr = True
118 | self.assertEqual(('"true" succeeded in 0:00 minutes:\noutputerror', 0),
119 | run_cmd(args))
120 |
121 | @patch('ntfy.cli.Popen')
122 | def test_failure_stdout_and_stderr(self, mock_Popen):
123 | mock_Popen.return_value = process_mock(
124 | 1, stdout='output', stderr='error')
125 | args = MagicMock()
126 | args.longer_than = -1
127 | args.command = ['true']
128 | args.pid = None
129 | args.unfocused_only = False
130 | args.hide_command = False
131 | # not actually used
132 | args.stdout = True
133 | args.stderr = True
134 | self.assertEqual(
135 | ('"true" failed (code 1) in 0:00 minutes:\noutputerror', 1),
136 | run_cmd(args))
137 |
138 | @patch('ntfy.cli.Popen')
139 | def test_hide_command(self, mock_Popen):
140 | mock_Popen.return_value = process_mock()
141 | args = MagicMock()
142 | args.longer_than = -1
143 | args.command = ['true']
144 | args.pid = None
145 | args.unfocused_only = False
146 | args.hide_command = True
147 | self.assertEqual(('Your command succeeded in 0:00 minutes', 0),
148 | run_cmd(args))
149 |
150 | def test_formatter(self):
151 | args = MagicMock()
152 | args.pid = None
153 | args.command = None
154 | args.formatter = ("true", 0, 65)
155 | args.longer_than = -1
156 | args.unfocused_only = False
157 | args.hide_command = False
158 | self.assertEqual(('"true" succeeded in 1:05 minutes', 0),
159 | run_cmd(args))
160 |
161 | def test_formatter_failure(self):
162 | args = MagicMock()
163 | args.pid = None
164 | args.command = None
165 | args.formatter = ("false", 1, 10)
166 | args.longer_than = -1
167 | args.unfocused_only = False
168 | args.hide_command = False
169 | self.assertEqual(('"false" failed (code 1) in 0:10 minutes', 1),
170 | run_cmd(args))
171 |
172 |
173 | class TestMain(TestCase):
174 | @patch('ntfy.backends.default.notify')
175 | def test_args(self, mock_notify):
176 | mock_notify.return_value = None
177 | self.assertEqual(0,
178 | ntfy_main([
179 | '-o', 'foo', 'bar', '-b', 'default', '-t',
180 | 'TITLE', 'send', 'test'
181 | ]))
182 | mock_notify.assert_called_once_with(
183 | message='test', title='TITLE', foo='bar', retcode=0)
184 |
185 |
186 | class ShellIntegrationTestCase(TestCase):
187 | def test_shellintegration_printout(self):
188 | # not mocking print to check calls because test runner uses print...
189 | args = MagicMock()
190 | auto_done(args)
191 |
192 |
193 | class TestWatchPID(TestCase):
194 | @patch('psutil.Process')
195 | def test_watch_pid(self, mock_process):
196 | mock_process.return_value.pid = 1
197 | mock_process.return_value.create_time.return_value = time()
198 | mock_process.return_value.cmdline.return_value = ['cmd']
199 | args = MagicMock()
200 | args.pid = 1
201 | args.unfocused_only = False
202 | self.assertEqual('PID[1]: "cmd" finished in 0:00 minutes',
203 | run_cmd(args)[0])
204 |
205 | def test_watch_bad_pid(self):
206 | args = MagicMock()
207 | args.pid = 100000
208 | self.assertRaises(SystemExit, run_cmd, args)
209 |
210 |
211 | if __name__ == '__main__':
212 | main()
213 |
--------------------------------------------------------------------------------
/tests/test_config.py:
--------------------------------------------------------------------------------
1 | from errno import ENOENT
2 | from os import environ
3 | from sys import version_info
4 | from unittest import TestCase, main, skipIf
5 |
6 | from mock import mock_open, patch
7 | from ntfy.config import DEFAULT_CONFIG, load_config
8 |
9 | py = version_info.major
10 | py_ = version_info.major, version_info.minor
11 |
12 | mock_open_dne_error = mock_open()
13 | mock_open_dne_error.side_effect = IOError(ENOENT, 'foobar')
14 | mock_open_other_error = mock_open()
15 | mock_open_other_error.side_effect = IOError()
16 |
17 | builtin_module = '__builtin__' if py == 2 else 'builtins'
18 |
19 |
20 | class TestLoadConfig(TestCase):
21 | @patch(builtin_module + '.open', mock_open_dne_error)
22 | def test_default_config(self):
23 | config = load_config(DEFAULT_CONFIG)
24 | self.assertEqual(config, {})
25 |
26 | @patch(builtin_module + '.open', mock_open())
27 | @patch('ntfy.config.safe_load')
28 | def test_backwards_compat(self, mock_yamlload):
29 | mock_yamlload.return_value = {'backend': 'foobar'}
30 | config = load_config(DEFAULT_CONFIG)
31 | self.assertIn('backends', config)
32 | self.assertEqual(config['backends'], ['foobar'])
33 |
34 | @skipIf(
35 | environ.get('CI') and py_ in [(3, 3), (3, 4)],
36 | 'Python 3.3 and 3.4 fail in TravisCI, but 3.4 works on Ubuntu 14.04')
37 | @patch(builtin_module + '.open', mock_open())
38 | @patch('ntfy.config.safe_load')
39 | def test_parse_error(self, mock_yamlload):
40 | mock_yamlload.side_effect = ValueError
41 | self.assertRaises(SystemExit, load_config)
42 |
43 | @patch(builtin_module + '.open', mock_open_other_error)
44 | def test_open_error(self):
45 | self.assertRaises(SystemExit, load_config)
46 |
--------------------------------------------------------------------------------
/tests/test_integration.py:
--------------------------------------------------------------------------------
1 | from sys import modules, version_info
2 | from unittest import TestCase, main
3 |
4 | from mock import MagicMock, mock_open, patch
5 | from ntfy.cli import main as ntfy_main
6 |
7 | py = version_info.major
8 |
9 | builtin_module = '__builtin__' if py == 2 else 'builtins'
10 |
11 |
12 | class TestIntegration(TestCase):
13 | @patch(builtin_module + '.open', mock_open())
14 | @patch('ntfy.config.safe_load')
15 | @patch('ntfy.backends.pushover.requests.post')
16 | def test_pushover(self, mock_post, mock_yamlload):
17 | mock_yamlload.return_value = {
18 | 'backends': ['pushover'],
19 | 'pushover': {
20 | 'user_key': MagicMock()
21 | },
22 | }
23 | self.assertEqual(0, ntfy_main(['send', 'foobar']))
24 |
25 | @patch(builtin_module + '.open', mock_open())
26 | @patch('ntfy.config.safe_load')
27 | @patch('ntfy.backends.prowl.requests.post')
28 | def test_prowl(self, mock_post, mock_yamlload):
29 | mock_yamlload.return_value = {
30 | 'backends': ['prowl'],
31 | 'prowl': {
32 | 'apikey': MagicMock()
33 | },
34 | }
35 | ntfy_main(['send', 'foobar'])
36 |
37 | @patch(builtin_module + '.open', mock_open())
38 | @patch('ntfy.config.safe_load')
39 | @patch('ntfy.backends.pushbullet.requests.post')
40 | def test_pushbullet(self, mock_post, mock_yamlload):
41 | mock_yamlload.return_value = {
42 | 'backends': ['pushbullet'],
43 | 'pushbullet': {
44 | 'access_token': MagicMock()
45 | },
46 | }
47 | self.assertEqual(0, ntfy_main(['send', 'foobar']))
48 |
49 | @patch(builtin_module + '.open', mock_open())
50 | @patch('ntfy.config.safe_load')
51 | @patch('ntfy.backends.simplepush.requests.post')
52 | def test_simplepush(self, mock_post, mock_yamlload):
53 | mock_yamlload.return_value = {
54 | 'backends': ['simplepush'],
55 | 'simplepush': {
56 | 'key': MagicMock()
57 | },
58 | }
59 | self.assertEqual(0, ntfy_main(['send', 'foobar']))
60 |
61 | @patch(builtin_module + '.open', mock_open())
62 | @patch('ntfy.backends.default.platform', 'linux')
63 | @patch('ntfy.config.safe_load')
64 | def test_default(self, mock_yamlload):
65 | old_dbus = modules.get('dbus')
66 | modules['dbus'] = MagicMock()
67 | try:
68 | mock_yamlload.return_value = {
69 | 'backends': ['default'],
70 | }
71 | self.assertEqual(0, ntfy_main(['send', 'foobar']))
72 | finally:
73 | if old_dbus is not None:
74 | modules['dbus'] = old_dbus
75 |
76 | @patch(builtin_module + '.open', mock_open())
77 | @patch('ntfy.config.safe_load')
78 | def test_linux(self, mock_yamlload):
79 | old_dbus = modules.get('dbus')
80 | modules['dbus'] = MagicMock()
81 | try:
82 | mock_yamlload.return_value = {
83 | 'backends': ['linux'],
84 | }
85 | self.assertEqual(0, ntfy_main(['send', 'foobar']))
86 | finally:
87 | if old_dbus is not None:
88 | modules['dbus'] = old_dbus
89 |
90 | @patch(builtin_module + '.open', mock_open())
91 | @patch('ntfy.config.safe_load')
92 | def test_darwin(self, mock_yamlload):
93 | old_foundation = modules.get('Foundation')
94 | old_objc = modules.get('objc')
95 | old_appkit = modules.get('AppKit')
96 | modules['Foundation'] = MagicMock()
97 | modules['objc'] = MagicMock()
98 | modules['AppKit'] = MagicMock()
99 | try:
100 | mock_yamlload.return_value = {
101 | 'backends': ['darwin'],
102 | }
103 | self.assertEqual(0, ntfy_main(['send', 'foobar']))
104 | finally:
105 | if old_foundation is not None:
106 | modules['Foundation'] = old_foundation
107 | if old_objc is not None:
108 | modules['objc'] = old_objc
109 | if old_appkit is not None:
110 | modules['AppKit'] = old_appkit
111 |
112 | @patch(builtin_module + '.open', mock_open())
113 | @patch('ntfy.config.safe_load')
114 | def test_win32(self, mock_yamlload):
115 | old_win32api = modules.get('win32api')
116 | old_win32gui = modules.get('win32gui')
117 | old_win32con = modules.get('win32con')
118 | modules['win32api'] = MagicMock()
119 | modules['win32gui'] = MagicMock()
120 | modules['win32con'] = MagicMock()
121 | try:
122 | mock_yamlload.return_value = {
123 | 'backends': ['win32'],
124 | }
125 | self.assertEqual(0, ntfy_main(['send', 'foobar']))
126 | finally:
127 | if old_win32api is not None:
128 | modules['win32api'] = old_win32api
129 | if old_win32gui is not None:
130 | modules['win32gui'] = old_win32gui
131 | if old_win32con is not None:
132 | modules['win32con'] = old_win32con
133 |
134 | @patch(builtin_module + '.open', mock_open())
135 | @patch('ntfy.config.safe_load')
136 | @patch('ntfy.backends.xmpp.NtfySendMsgBot')
137 | def test_xmpp(self, mock_bot, mock_yamlload):
138 | mock_yamlload.return_value = {
139 | 'backends': ['xmpp'],
140 | 'xmpp': {
141 | 'jid': 'foo@bar',
142 | 'password': 'hunter2',
143 | 'recipient': 'bar@foo'
144 | }
145 | }
146 | self.assertEqual(0, ntfy_main(['send', 'foobar']))
147 |
148 | @patch(builtin_module + '.open', mock_open())
149 | @patch('ntfy.config.safe_load')
150 | def test_instapush(self, mock_yamlload):
151 | modules['instapush'] = MagicMock()
152 | modules['instapush'].App().notify.return_value = {'status': 200}
153 |
154 | mock_yamlload.return_value = {
155 | 'backends': ['insta'],
156 | 'insta': {
157 | 'appid': 'appid',
158 | 'secret': 'secret',
159 | 'event_name': 'event',
160 | 'trackers': ['a']
161 | }
162 | }
163 | ntfy_main(['send', 'ms'])
164 |
165 |
166 | if __name__ == '__main__':
167 | main()
168 |
--------------------------------------------------------------------------------
/tests/test_notifico.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase, main
2 |
3 | from requests import HTTPError, Response
4 |
5 | from mock import patch
6 | from ntfy.backends.notifico import notify
7 |
8 |
9 | class TestNotifico(TestCase):
10 | def setUp(self):
11 | self.webhook = 'https://n.tkte.ch/h/1234/testing_webhook'
12 |
13 | @patch('requests.get')
14 | def test_basic(self, mock_get):
15 | resp = Response()
16 | resp.status_code = 200
17 | mock_get.return_value = resp
18 | notify('title', 'message', webhook=self.webhook)
19 | mock_get.assert_called_once_with(
20 | self.webhook, params={'payload': 'title\nmessage'})
21 |
22 | @patch('requests.get')
23 | def test_none_webhook(self, mock_get):
24 | notify('title', 'message', webhook=None)
25 | mock_get.assert_not_called()
26 |
27 | @patch('requests.get')
28 | def test_exception(self, mock_get):
29 | resp = Response()
30 | resp.status_code = 400
31 | mock_get.return_value = resp
32 | with self.assertRaises(HTTPError):
33 | notify('title', 'message', webhook=self.webhook)
34 | mock_get.assert_called_once_with(
35 | self.webhook, params={'payload': 'title\nmessage'})
36 |
37 |
38 | if __name__ == '__main__':
39 | main()
40 |
--------------------------------------------------------------------------------
/tests/test_ntfy.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | import ntfy
4 | from mock import patch
5 | from ntfy import notify
6 |
7 |
8 | def mock_notify(message, title, retcode=None):
9 | raise Exception
10 |
11 |
12 | class OverrideBackendTestCase(TestCase):
13 | @patch('requests.post')
14 | def test_runcmd(self, mock_post):
15 | ret = notify('message', 'title', {
16 | 'backends': ['foobar'],
17 | 'foobar': {
18 | 'backend': 'pushover',
19 | 'user_key': 't0k3n',
20 | }
21 | })
22 | self.assertEqual(ret, 0)
23 |
24 |
25 | class ErrorTestCase(TestCase):
26 | def test_invalidbackend(self):
27 | ret = notify('message', 'title', {'backends': ['foobar']})
28 | self.assertEqual(ret, 1)
29 |
30 | @patch('ntfy.backends.default.notify', mock_notify)
31 | def test_backenderror(self):
32 | ret = ntfy.notify('message', 'title')
33 | self.assertEqual(ret, 1)
34 |
--------------------------------------------------------------------------------
/tests/test_prowl.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase, main
2 |
3 | from mock import patch
4 | from ntfy.backends.prowl import API_URL, NTFY_API_KEY, notify
5 | from ntfy.config import USER_AGENT
6 |
7 | TITLE = 'title'
8 | MESSAGE = 'message'
9 |
10 |
11 | class TestProwl(TestCase):
12 | def setUp(self):
13 | self.post_patcher = patch('requests.post')
14 | self.response_patcher = patch('requests.Response')
15 | self.mock_post = self.post_patcher.start()
16 | self.mock_response = self.response_patcher.start()
17 | self.mock_post.return_value = self.mock_response
18 |
19 | def tearDown(self):
20 | self.post_patcher.stop()
21 | self.response_patcher.stop()
22 |
23 | def verify_post(self, priority=0, **kwargs):
24 | data = {
25 | 'apikey': NTFY_API_KEY,
26 | 'event': TITLE,
27 | 'description': MESSAGE,
28 | 'application': 'ntfy',
29 | 'priority': priority,
30 | }
31 | data.update(kwargs)
32 | self.mock_post.assert_called_once_with(
33 | API_URL, data=data, headers={'User-Agent': USER_AGENT})
34 | self.mock_response.raise_for_status.assert_called_once()
35 |
36 | def test_basic(self):
37 | notify(TITLE, MESSAGE)
38 | self.verify_post()
39 |
40 | def test_high_priority(self):
41 | notify(TITLE, MESSAGE, priority=2)
42 | self.verify_post(priority=2)
43 |
44 | def test_low_priority(self):
45 | notify(TITLE, MESSAGE, priority=-2)
46 | self.verify_post(priority=-2)
47 |
48 | def test_priority_too_high(self):
49 | self.assertRaises(ValueError, notify, TITLE, MESSAGE, priority=3)
50 |
51 | def test_priority_too_low(self):
52 | self.assertRaises(ValueError, notify, TITLE, MESSAGE, priority=-3)
53 |
54 | def test_url(self):
55 | notify(TITLE, MESSAGE, url='foobar')
56 | self.verify_post(url='foobar')
57 |
58 | def test_provider_key(self):
59 | notify(TITLE, MESSAGE, provider_key='providerkey')
60 | self.verify_post(providerkey='providerkey')
61 |
62 |
63 | if __name__ == '__main__':
64 | main()
65 |
--------------------------------------------------------------------------------
/tests/test_pushalot.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase, main
2 |
3 | from mock import patch
4 | from ntfy.backends.pushalot import notify
5 | from ntfy.config import USER_AGENT
6 |
7 |
8 | class TestPushalot(TestCase):
9 | URL = 'https://pushalot.com/api/sendmessage'
10 |
11 | @patch('requests.post')
12 | def test_basic(self, mock_post):
13 | notify('title', 'message', auth_token='access_token')
14 | mock_post.assert_called_once_with(
15 | self.URL,
16 | data={
17 | 'Body': 'message',
18 | 'Title': 'title',
19 | 'AuthorizationToken': 'access_token'
20 | },
21 | headers={'User-Agent': USER_AGENT})
22 |
23 | @patch('requests.post')
24 | def test_silent(self, mock_post):
25 | notify('title', 'message', silent=True, auth_token='access_token')
26 | mock_post.assert_called_once_with(
27 | self.URL,
28 | data={
29 | 'Body': 'message',
30 | 'Title': 'title',
31 | 'IsSilent': 'True',
32 | 'AuthorizationToken': 'access_token'
33 | },
34 | headers={'User-Agent': USER_AGENT})
35 |
36 | @patch('requests.post')
37 | def test_important(self, mock_post):
38 | notify('title', 'message', important=True, auth_token='access_token')
39 | mock_post.assert_called_once_with(
40 | self.URL,
41 | data={
42 | 'Body': 'message',
43 | 'Title': 'title',
44 | 'IsImportant': 'True',
45 | 'AuthorizationToken': 'access_token'
46 | },
47 | headers={'User-Agent': USER_AGENT})
48 |
49 | @patch('requests.post')
50 | def test_source(self, mock_post):
51 | notify('title', 'message', source='source', auth_token='access_token')
52 | mock_post.assert_called_once_with(
53 | self.URL,
54 | data={
55 | 'Body': 'message',
56 | 'Title': 'title',
57 | 'Source': 'source',
58 | 'AuthorizationToken': 'access_token'
59 | },
60 | headers={'User-Agent': USER_AGENT})
61 |
62 | @patch('requests.post')
63 | def test_url(self, mock_post):
64 | notify(
65 | 'title', 'message', url='example.com', auth_token='access_token')
66 | mock_post.assert_called_once_with(
67 | self.URL,
68 | data={
69 | 'Body': 'message',
70 | 'Title': 'title',
71 | 'Link': 'example.com',
72 | 'AuthorizationToken': 'access_token'
73 | },
74 | headers={'User-Agent': USER_AGENT})
75 |
76 | @patch('requests.post')
77 | def test_url_title(self, mock_post):
78 | notify(
79 | 'title',
80 | 'message',
81 | url='example.com',
82 | url_title='url title',
83 | auth_token='access_token')
84 | mock_post.assert_called_once_with(
85 | self.URL,
86 | data={
87 | 'Body': 'message',
88 | 'Title': 'title',
89 | 'Link': 'example.com',
90 | 'LinkTitle': 'url title',
91 | 'AuthorizationToken': 'access_token'
92 | },
93 | headers={'User-Agent': USER_AGENT})
94 |
95 | @patch('requests.post')
96 | def test_image(self, mock_post):
97 | notify(
98 | 'title', 'message', image='image.jpg', auth_token='access_token')
99 | mock_post.assert_called_once_with(
100 | self.URL,
101 | data={
102 | 'Body': 'message',
103 | 'Title': 'title',
104 | 'Image': 'image.jpg',
105 | 'AuthorizationToken': 'access_token'
106 | },
107 | headers={'User-Agent': USER_AGENT})
108 |
109 | @patch('requests.post')
110 | def test_ttl(self, mock_post):
111 | notify('title', 'message', ttl=100, auth_token='access_token')
112 | mock_post.assert_called_once_with(
113 | self.URL,
114 | data={
115 | 'Body': 'message',
116 | 'Title': 'title',
117 | 'TimeToLive': 100,
118 | 'AuthorizationToken': 'access_token'
119 | },
120 | headers={'User-Agent': USER_AGENT})
121 |
122 |
123 | if __name__ == '__main__':
124 | main()
125 |
--------------------------------------------------------------------------------
/tests/test_pushbullet.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase, main
2 |
3 | from mock import patch
4 | from ntfy.backends.pushbullet import notify
5 | from ntfy.config import USER_AGENT
6 |
7 |
8 | class TestPushbullet(TestCase):
9 | @patch('requests.post')
10 | def test_basic(self, mock_post):
11 | notify('title', 'message', access_token='access_token')
12 | mock_post.assert_called_once_with(
13 | 'https://api.pushbullet.com/v2/pushes',
14 | data={'body': 'message',
15 | 'title': 'title',
16 | 'type': 'note'},
17 | headers={'Access-Token': 'access_token',
18 | 'User-Agent': USER_AGENT})
19 |
20 | @patch('requests.post')
21 | def test_device(self, mock_post):
22 | notify(
23 | 'title',
24 | 'message',
25 | access_token='access_token',
26 | device_iden='foobar')
27 | mock_post.assert_called_once_with(
28 | 'https://api.pushbullet.com/v2/pushes',
29 | data={
30 | 'body': 'message',
31 | 'title': 'title',
32 | 'device_iden': 'foobar',
33 | 'type': 'note'
34 | },
35 | headers={'Access-Token': 'access_token',
36 | 'User-Agent': USER_AGENT})
37 |
38 | @patch('requests.post')
39 | def test_email(self, mock_post):
40 | notify(
41 | 'title',
42 | 'message',
43 | access_token='access_token',
44 | email='foobar@example.com')
45 | mock_post.assert_called_once_with(
46 | 'https://api.pushbullet.com/v2/pushes',
47 | data={
48 | 'body': 'message',
49 | 'title': 'title',
50 | 'email': 'foobar@example.com',
51 | 'type': 'note'
52 | },
53 | headers={'Access-Token': 'access_token',
54 | 'User-Agent': USER_AGENT})
55 |
56 |
57 | if __name__ == '__main__':
58 | main()
59 |
--------------------------------------------------------------------------------
/tests/test_pushjet.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from mock import patch
4 | from ntfy.backends.pushjet import notify
5 | from ntfy.config import USER_AGENT
6 |
7 |
8 | class TestPushjet(TestCase):
9 | @patch('requests.post')
10 | def test_basic(self, mock_post):
11 | notify('title', 'message', secret='secret')
12 | mock_post.assert_called_once_with(
13 | 'https://api.pushjet.io/message',
14 | data={
15 | 'title': 'title',
16 | 'message': 'message',
17 | 'secret': 'secret',
18 | 'level': 3
19 | },
20 | headers={'User-Agent': USER_AGENT})
21 |
22 | @patch('requests.post')
23 | def test_link(self, mock_post):
24 | notify('title', 'message', secret='secret', link='foobar')
25 | mock_post.assert_called_once_with(
26 | 'https://api.pushjet.io/message',
27 | data={
28 | 'title': 'title',
29 | 'message': 'message',
30 | 'secret': 'secret',
31 | 'level': 3,
32 | 'link': 'foobar'
33 | },
34 | headers={'User-Agent': USER_AGENT})
35 |
36 | @patch('requests.post')
37 | def test_endpoint(self, mock_post):
38 | notify('title', 'message', secret='secret', endpoint='http://foobar')
39 | mock_post.assert_called_once_with(
40 | 'http://foobar/message',
41 | data={
42 | 'title': 'title',
43 | 'message': 'message',
44 | 'secret': 'secret',
45 | 'level': 3
46 | },
47 | headers={'User-Agent': USER_AGENT})
48 |
--------------------------------------------------------------------------------
/tests/test_pushover.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase, main
2 |
3 | from mock import patch
4 | from ntfy.backends.pushover import notify
5 | from ntfy.config import USER_AGENT
6 |
7 |
8 | class TestPushover(TestCase):
9 | @patch('requests.post')
10 | def test_basic(self, mock_post):
11 | notify('title', 'message', user_key='user_key')
12 | mock_post.assert_called_once_with(
13 | 'https://api.pushover.net/1/messages.json',
14 | data={
15 | 'user': 'user_key',
16 | 'message': 'message',
17 | 'token': 'aUnsraBiEZVsmrG89AZp47K3S2dX2a',
18 | 'title': 'title'
19 | },
20 | headers={'User-Agent': USER_AGENT})
21 |
22 | @patch('requests.post')
23 | def test_url_title(self, mock_post):
24 | notify(
25 | 'title',
26 | 'message',
27 | user_key='user_key',
28 | url_title='foo',
29 | url='bar')
30 | mock_post.assert_called_once_with(
31 | 'https://api.pushover.net/1/messages.json',
32 | data={
33 | 'user': 'user_key',
34 | 'message': 'message',
35 | 'token': 'aUnsraBiEZVsmrG89AZp47K3S2dX2a',
36 | 'title': 'title',
37 | 'url_title': 'foo',
38 | 'url': 'bar'
39 | },
40 | headers={'User-Agent': USER_AGENT})
41 |
42 | mock_post.reset_mock()
43 | notify('title', 'message', user_key='user_key', url_title='foo')
44 | mock_post.assert_called_once_with(
45 | 'https://api.pushover.net/1/messages.json',
46 | data={
47 | 'user': 'user_key',
48 | 'message': 'message',
49 | 'token': 'aUnsraBiEZVsmrG89AZp47K3S2dX2a',
50 | 'title': 'title'
51 | },
52 | headers={'User-Agent': USER_AGENT})
53 |
54 | @patch('requests.post')
55 | def test_html(self, mock_post):
56 | notify('title', 'message', user_key='user_key', html=True)
57 | mock_post.assert_called_once_with(
58 | 'https://api.pushover.net/1/messages.json',
59 | data={
60 | 'user': 'user_key',
61 | 'message': 'message',
62 | 'token': 'aUnsraBiEZVsmrG89AZp47K3S2dX2a',
63 | 'title': 'title',
64 | 'html': 1
65 | },
66 | headers={'User-Agent': USER_AGENT})
67 |
68 | @patch('requests.post')
69 | def test_priority(self, mock_post):
70 | notify('title', 'message', user_key='user_key', priority=1)
71 | mock_post.assert_called_once_with(
72 | 'https://api.pushover.net/1/messages.json',
73 | data={
74 | 'user': 'user_key',
75 | 'message': 'message',
76 | 'token': 'aUnsraBiEZVsmrG89AZp47K3S2dX2a',
77 | 'title': 'title',
78 | 'priority': 1
79 | },
80 | headers={'User-Agent': USER_AGENT})
81 |
82 | @patch('requests.post')
83 | def test_invalid_priority(self, mock_post):
84 | self.assertRaises(
85 | ValueError,
86 | notify,
87 | 'title',
88 | 'message',
89 | user_key='user_key',
90 | priority=3)
91 |
92 | @patch('requests.post')
93 | def test_hi_priority(self, mock_post):
94 | notify('title', 'message', user_key='user_key', priority=2)
95 | mock_post.assert_called_once_with(
96 | 'https://api.pushover.net/1/messages.json',
97 | data={
98 | 'user': 'user_key',
99 | 'message': 'message',
100 | 'token': 'aUnsraBiEZVsmrG89AZp47K3S2dX2a',
101 | 'title': 'title',
102 | 'priority': 2,
103 | 'retry': 30,
104 | 'expire': 86400
105 | },
106 | headers={'User-Agent': USER_AGENT})
107 |
108 | @patch('requests.post')
109 | def test_hi_priority_retry(self, mock_post):
110 | notify('title', 'message', user_key='user_key', priority=2, retry=60)
111 | mock_post.assert_called_once_with(
112 | 'https://api.pushover.net/1/messages.json',
113 | data={
114 | 'user': 'user_key',
115 | 'message': 'message',
116 | 'token': 'aUnsraBiEZVsmrG89AZp47K3S2dX2a',
117 | 'title': 'title',
118 | 'priority': 2,
119 | 'retry': 60,
120 | 'expire': 86400
121 | },
122 | headers={'User-Agent': USER_AGENT})
123 |
124 | @patch('requests.post')
125 | def test_hi_priority_expire(self, mock_post):
126 | notify('title', 'message', user_key='user_key', priority=2, expire=60)
127 | mock_post.assert_called_once_with(
128 | 'https://api.pushover.net/1/messages.json',
129 | data={
130 | 'user': 'user_key',
131 | 'message': 'message',
132 | 'token': 'aUnsraBiEZVsmrG89AZp47K3S2dX2a',
133 | 'title': 'title',
134 | 'priority': 2,
135 | 'retry': 30,
136 | 'expire': 60
137 | },
138 | headers={'User-Agent': USER_AGENT})
139 |
140 | @patch('requests.post')
141 | def test_hi_priority_callback(self, mock_post):
142 | notify(
143 | 'title',
144 | 'message',
145 | user_key='user_key',
146 | priority=2,
147 | callback='http://example.com')
148 | mock_post.assert_called_once_with(
149 | 'https://api.pushover.net/1/messages.json',
150 | data={
151 | 'user': 'user_key',
152 | 'message': 'message',
153 | 'token': 'aUnsraBiEZVsmrG89AZp47K3S2dX2a',
154 | 'title': 'title',
155 | 'priority': 2,
156 | 'retry': 30,
157 | 'expire': 86400,
158 | 'callback': 'http://example.com'
159 | },
160 | headers={'User-Agent': USER_AGENT})
161 |
162 | @patch('requests.post')
163 | def test_device(self, mock_post):
164 | notify('title', 'message', user_key='user_key', device='foobar')
165 | mock_post.assert_called_once_with(
166 | 'https://api.pushover.net/1/messages.json',
167 | data={
168 | 'user': 'user_key',
169 | 'message': 'message',
170 | 'token': 'aUnsraBiEZVsmrG89AZp47K3S2dX2a',
171 | 'title': 'title',
172 | 'device': 'foobar'
173 | },
174 | headers={'User-Agent': USER_AGENT})
175 |
176 | @patch('requests.post')
177 | def test_sound(self, mock_post):
178 | notify('title', 'message', user_key='user_key', sound='foobar')
179 | mock_post.assert_called_once_with(
180 | 'https://api.pushover.net/1/messages.json',
181 | data={
182 | 'user': 'user_key',
183 | 'message': 'message',
184 | 'token': 'aUnsraBiEZVsmrG89AZp47K3S2dX2a',
185 | 'title': 'title',
186 | 'sound': 'foobar'
187 | },
188 | headers={'User-Agent': USER_AGENT})
189 |
190 | @patch('requests.post')
191 | def test_url(self, mock_post):
192 | notify('title', 'message', user_key='user_key', url='foobar')
193 | mock_post.assert_called_once_with(
194 | 'https://api.pushover.net/1/messages.json',
195 | data={
196 | 'user': 'user_key',
197 | 'message': 'message',
198 | 'token': 'aUnsraBiEZVsmrG89AZp47K3S2dX2a',
199 | 'title': 'title',
200 | 'url': 'foobar'
201 | },
202 | headers={'User-Agent': USER_AGENT})
203 |
204 |
205 | if __name__ == '__main__':
206 | main()
207 |
--------------------------------------------------------------------------------
/tests/test_simplepush.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from mock import patch
4 | from ntfy.backends.simplepush import notify
5 | from ntfy.config import USER_AGENT
6 |
7 |
8 | class TestSimplepush(TestCase):
9 | @patch('requests.post')
10 | def test_basic(self, mock_post):
11 | notify('title', 'message', key='secret')
12 | mock_post.assert_called_once_with(
13 | 'https://api.simplepush.io/send',
14 | data={'title': 'title',
15 | 'msg': 'message',
16 | 'key': 'secret'},
17 | headers={'User-Agent': USER_AGENT})
18 |
19 | @patch('requests.post')
20 | def test_event(self, mock_post):
21 | notify('title', 'message', key='secret', event='foo')
22 | mock_post.assert_called_once_with(
23 | 'https://api.simplepush.io/send',
24 | data={
25 | 'title': 'title',
26 | 'msg': 'message',
27 | 'key': 'secret',
28 | 'event': 'foo'
29 | },
30 | headers={'User-Agent': USER_AGENT})
31 |
--------------------------------------------------------------------------------
/tests/test_systemlog.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase, skipIf
2 |
3 | from mock import call, patch
4 |
5 | try:
6 | import syslog
7 | syslog_supported = True
8 | except ImportError:
9 | syslog_supported = False
10 |
11 | if syslog_supported:
12 | from ntfy.backends.systemlog import notify
13 |
14 |
15 | class TestSystemlog(TestCase):
16 | @skipIf(not syslog_supported, 'Syslog not supported')
17 | @patch('syslog.syslog')
18 | def test_basic(self, mock_post):
19 | notify('title', 'message')
20 | mock_post.assert_called_once_with(syslog.LOG_LOCAL5 | syslog.LOG_ALERT,
21 | '[title] message')
22 |
23 | @skipIf(not syslog_supported, 'Syslog not supported')
24 | @patch('syslog.syslog')
25 | def test_facility(self, mock_post):
26 | notify('title', 'message', facility='MAIL')
27 | mock_post.assert_called_once_with(syslog.LOG_MAIL | syslog.LOG_ALERT,
28 | '[title] message')
29 |
30 | @skipIf(not syslog_supported, 'Syslog not supported')
31 | @patch('syslog.syslog')
32 | def test_prio(self, mock_post):
33 | notify('title', 'message', prio='DEBUG')
34 | mock_post.assert_called_once_with(syslog.LOG_LOCAL5 | syslog.LOG_DEBUG,
35 | '[title] message')
36 |
37 | @skipIf(not syslog_supported, 'Syslog not supported')
38 | @patch('syslog.syslog')
39 | def test_fmt(self, mock_post):
40 | notify('title', 'message', fmt='Title: {title} Message: {message}')
41 | mock_post.assert_called_once_with(syslog.LOG_LOCAL5 | syslog.LOG_ALERT,
42 | 'Title: title Message: message')
43 |
44 | @skipIf(not syslog_supported, 'Syslog not supported')
45 | @patch('syslog.syslog')
46 | def test_multiple_lines(self, mock_post):
47 | notify('title', 'message\non multiple\nlines')
48 | calls = [
49 | call(syslog.LOG_LOCAL5 | syslog.LOG_ALERT, '[title] message'),
50 | call(syslog.LOG_LOCAL5 | syslog.LOG_ALERT, 'on multiple'),
51 | call(syslog.LOG_LOCAL5 | syslog.LOG_ALERT, 'lines'),
52 | ]
53 | mock_post.assert_has_calls(calls)
54 |
55 | @skipIf(not syslog_supported, 'Syslog not supported')
56 | @patch('syslog.syslog')
57 | def test_invalid_prio(self, mock_post):
58 | self.assertRaises(
59 | ValueError, notify, 'title', 'message', prio='INVALID_PRIO')
60 |
61 | @skipIf(not syslog_supported, 'Syslog not supported')
62 | @patch('syslog.syslog')
63 | def test_invalid_facility(self, mock_post):
64 | self.assertRaises(
65 | ValueError,
66 | notify,
67 | 'title',
68 | 'message',
69 | facility='INVALID_FACILITY')
70 |
--------------------------------------------------------------------------------
/tests/test_xmpp.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from mock import MagicMock, patch
4 | from ntfy.backends.xmpp import NtfySendMsgBot, notify
5 | from ntfy.config import USER_AGENT
6 |
7 |
8 | class NtfySendMsgBotTestCase(TestCase):
9 | @patch('sleekxmpp.ClientXMPP.add_event_handler')
10 | def test_eventhandler(self, mock_add_event_handler):
11 | bot = NtfySendMsgBot('foo@bar', 'hunter2', 'bar@foo', 'title',
12 | 'message')
13 | mock_add_event_handler.assert_called_with('session_start', bot.start)
14 |
15 | @patch('sleekxmpp.ClientXMPP.send_presence')
16 | @patch('sleekxmpp.ClientXMPP.get_roster')
17 | @patch('sleekxmpp.ClientXMPP.disconnect')
18 | @patch('sleekxmpp.ClientXMPP.send_message')
19 | def test_start(self, mock_send_message, *other_mocks):
20 | bot = NtfySendMsgBot('foo@bar', 'hunter2', 'bar@foo', 'title',
21 | 'message')
22 | bot.start(MagicMock)
23 | mock_send_message.assert_called_with(
24 | mbody='message', msubject='title', mto='bar@foo')
25 |
26 | @patch('sleekxmpp.ClientXMPP.send_presence')
27 | @patch('sleekxmpp.ClientXMPP.get_roster')
28 | @patch('sleekxmpp.ClientXMPP.disconnect')
29 | @patch('sleekxmpp.ClientXMPP.send_message')
30 | def test_start_mtype(self, mock_send_message, *other_mocks):
31 | bot = NtfySendMsgBot(
32 | 'foo@bar', 'hunter2', 'bar@foo', 'title', 'message', mtype='chat')
33 | bot.start(MagicMock)
34 | mock_send_message.assert_called_with(
35 | mbody='message', msubject='title', mto='bar@foo', mtype='chat')
36 |
37 |
38 | class XMPPTestCase(TestCase):
39 | @patch('os.path.isdir')
40 | @patch('ntfy.backends.xmpp.NtfySendMsgBot')
41 | def test_capath(self, mock_bot_class, mock_isdir):
42 | notify(
43 | 'title',
44 | 'message',
45 | 'foo@bar',
46 | 'hunter2',
47 | 'bar@foo',
48 | path_to_certs='/custom/ca')
49 | self.assertEqual('/custom/ca', mock_bot_class().ca_certs)
50 |
--------------------------------------------------------------------------------