Attachment Support Added (#118)

This commit is contained in:
Chris Caron 2023-05-15 17:10:15 -04:00 committed by GitHub
parent a5f8767094
commit 86c9f16d69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 504 additions and 74 deletions

View File

@ -11,6 +11,7 @@ LABEL maintainer="Chris-Caron"
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV APPRISE_CONFIG_DIR /config
ENV APPRISE_ATTACH_DIR /attach
ENV CRYPTOGRAPHY_DONT_BUILD_RUST=1
# Install nginx and supervisord
@ -38,11 +39,12 @@ RUN apt-get remove -y -qq build-essential libffi-dev libssl-dev python-dev && \
# Configuration Permissions (to run nginx as a non-root user)
RUN umask 0002 && \
mkdir -p /config /run/apprise && \
chown www-data:www-data -R /run/apprise /var/lib/nginx /config
mkdir -p /attach /config /run/apprise && \
chown www-data:www-data -R /run/apprise /var/lib/nginx /attach /config
# Handle running as a non-root user (www-data is id/gid 33)
USER www-data
VOLUME /config
VOLUME /attach
EXPOSE 8000
CMD ["/usr/bin/supervisord", "-c", "/opt/apprise/webapp/etc/supervisord.conf"]

View File

@ -111,6 +111,25 @@ Here is a *stateless* example of how one might send a notification (using `/noti
curl -X POST -d 'urls=mailto://user:pass@gmail.com&body=test message' \
http://localhost:8000/notify
# Send a notification with an attachment
curl -X POST \
-F 'urls=mailto://user:pass@gmail.com' \
-F 'body=test message' \
-F attach=@Screenshot-1.png \
http://localhost:8000/notify
# Send multiple attachments; just make sure the attach keyword is unique:
curl -X POST \
-F 'urls=mailto://user:pass@gmail.com' \
-F 'body=test message' \
-F attach1=@Screenshot-1.png \
-F attach2=@/my/path/to/Apprise.doc \
http://localhost:8000/notify
curl -X POST -d 'urls=mailto://user:pass@gmail.com&body=test message' \
-F @/path/to/your/attachment \
http://localhost:8000/notify
# Send your notifications directly using JSON
curl -X POST -d '{"urls": "mailto://user:pass@gmail.com", "body":"test message"}' \
-H "Content-Type: application/json" \
@ -167,6 +186,14 @@ curl -X POST -d "body=test message" \
curl -X POST -d '{"body":"test message"}' \
-H "Content-Type: application/json" \
http://localhost:8000/notify/abc123
# Send attachments:
curl -X POST \
-F 'urls=mailto://user:pass@gmail.com' \
-F 'body=test message' \
-F attach1=@Screenshot-1.png \
-F attach2=@/my/path/to/Apprise.doc \
http://localhost:8000/notify/abc123
```
🏷️ You can also leverage _tagging_ which allows you to associate one or more tags with your Apprise URLs. By doing this, notifications only need to be referred to by their easy to remember notify tag name such as `devops`, `admin`, `family`, etc. You can very easily group more than one notification service under the same _tag_ allowing you to notify a group of services at once. This is accomplished through configuration files ([documented here](https://github.com/caronc/apprise/wiki/config)) that can be saved to the persistent storage previously associated with a `{KEY}`.
@ -239,6 +266,7 @@ The use of environment variables allow you to provide over-rides to default sett
| Variable | Description |
|--------------------- | ----------- |
| `APPRISE_CONFIG_DIR` | Defines an (optional) persistent store location of all configuration files saved. By default:<br/> - Configuration is written to the `apprise_api/var/config` directory when just using the _Django_ `manage runserver` script. However for the path for the container is `/config`.
| `APPRISE_ATTACH_DIR` | The directory the uploaded attachments are placed in. By default:<br/> - Attachments are written to the `apprise_api/var/attach` directory when just using the _Django_ `manage runserver` script. However for the path for the container is `/attach`.
| `APPRISE_STATELESS_URLS` | For a non-persistent solution, you can take advantage of this global variable. Use this to define a default set of Apprise URLs to notify when using API calls to `/notify`. If no `{KEY}` is defined when calling `/notify` then the URLs defined here are used instead. By default, nothing is defined for this variable.
| `APPRISE_STATEFUL_MODE` | This can be set to the following possible modes:<br/>📌 **hash**: This is also the default. It stores the server configuration in a hash formatted that can be easily indexed and compressed.<br/>📌 **simple**: Configuration is written straight to disk using the `{KEY}.cfg` (if `TEXT` based) and `{KEY}.yml` (if `YAML` based).<br/>📌 **disabled**: Straight up deny any read/write queries to the servers stateful store. Effectively turn off the Apprise Stateful feature completely.
| `APPRISE_CONFIG_LOCK` | Locks down your API hosting so that you can no longer delete/update/access stateful information. Your configuration is still referenced when stateful calls are made to `/notify`. The idea of this switch is to allow someone to set their (Apprise) configuration up and then as an added security tactic, they may choose to lock their configuration down (in a read-only state). Those who use the Apprise CLI tool may still do it, however the `--config` (`-c`) switch will not successfully reference this access point anymore. You can however use the `apprise://` plugin without any problem ([see here for more details](https://github.com/caronc/apprise/wiki/Notify_apprise_api)). This defaults to `no` and can however be set to `yes` by simply defining the global variable as such.

View File

@ -138,6 +138,12 @@ class NotifyForm(forms.Form):
max_length=apprise.NotifyBase.body_maxlen,
)
# Attachment Support
attachment = forms.FileField(
label=_('Attachment'),
required=False,
)
tag = forms.CharField(
label=_('Tags'),
widget=forms.TextInput(

View File

@ -40,7 +40,7 @@
<!-- Page Layout here -->
<div class="row">
<div class="col s3">
<div class="col s3" style="width:20em">
{% if STATEFUL_MODE != 'disabled' %}
<ul class="collection z-depth-1">
<a class="collection-item" href="{% url 'config' 'apprise' %}"><i class="tiny material-icons">settings</i>

View File

@ -23,6 +23,7 @@
{% blocktrans %}
Here is where you can store your Apprise configuration associated with the key <code>{{key}}</code>.
{% endblocktrans %}
For some examples on how to build a development environment around this, <strong><a href="{% url 'welcome'%}?key={{key}}">click here</a></strong>.
</li>
<li>
{% blocktrans %}
@ -78,7 +79,14 @@
{% blocktrans %}The following command would cause apprise to directly notify all of your services:{% endblocktrans %}
<br />
<pre><code class="bash">apprise --body="Test Message" \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;apprise{% if request.is_secure %}s{% endif %}://{{request.META.HTTP_HOST}}{{BASE_URL}}/{{key}}?tags=all</code></pre>
&nbsp;&nbsp;&nbsp;&nbsp;apprise{% if request.is_secure %}s{% endif %}://{{request.META.HTTP_HOST}}{{BASE_URL}}/<em>{{key}}</em>/?tags=all</code></pre>
<br />
{% blocktrans %}Send one or more attachments like this:{% endblocktrans %}
<pre><code class="bash">apprise --body="Test Message" \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;apprise{% if request.is_secure %}s{% endif %}://{{request.META.HTTP_HOST}}{{BASE_URL}}/<em>{{key}}</em>/?tags=all \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;--attach=/path/to/an/attachment.jpeg \ <br/>
&nbsp;&nbsp;&nbsp;&nbsp;--attach=https://raw.githubusercontent.com/caronc/apprise/master/apprise/assets/themes/default/apprise-logo.png<br/>
</code></pre>
</p>
{% if not CONFIG_LOCK %}
<p>
@ -86,7 +94,7 @@
send a test notification to all of your added services:{% endblocktrans %}
<br />
<pre><code class="bash">apprise --body="Test Message" --tag=all \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;--config={{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/get/{{key}}</code></pre>
&nbsp;&nbsp;&nbsp;&nbsp;--config={{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/get/<em>{{key}}</em></code></pre>
</p>
{% endif %}
</div>
@ -122,7 +130,7 @@
{% blocktrans %}
You can send a notification using the loaded configuration:
{% endblocktrans %}
<form id="donotify" action="{% url "notify" key %}" method="post">
<form id="donotify" enctype="multipart/form-data" action="{% url "notify" key %}" method="post">
{{ form_notify }}
<button class="btn waves-effect waves-light" type="submit" name="action">{% trans "Send Notification" %}
<i class="material-icons right">send</i>
@ -411,8 +419,8 @@ function notify_init() {
return s;
}, []).join(",")
const form = this;
const body = new URLSearchParams(new FormData(form));
// our Form
const form = new FormData(this);
// perform our notification
Swal.fire(
@ -422,7 +430,7 @@ function notify_init() {
Swal.showLoading()
let response = fetch('{% url "notify" key %}', {
method: 'POST',
body: body,
body: form,
headers: {
'Accept': 'text/html',
'X-Apprise-Log-Level': 'info'

View File

@ -71,7 +71,7 @@
<ul class="collapsible">
<li>
<div class="collapsible-header">
<i class="material-icons">code</i>curl example
<i class="material-icons">code</i>Curl Example
</div>
<div class="collapsible-body">
<pre><code class="bash">
@ -83,7 +83,7 @@
</li>
<li>
<div class="collapsible-header">
<i class="material-icons">code</i>python example
<i class="material-icons">code</i>Python Example
</div>
<div class="collapsible-body">
<pre><code class="python">
@ -101,7 +101,7 @@
</li>
<li>
<div class="collapsible-header">
<i class="material-icons">code</i>php example
<i class="material-icons">code</i>PHP Example
</div>
<div class="collapsible-body">
<pre><code class="php">

View File

@ -25,7 +25,7 @@
<table class="highlighted">
<thead>
<tr>
<th>{% trans "URL" %}</th>
<th style="min-width: 18%;">{% trans "URL" %}</th>
<th>{% trans "Description" %}</th>
</tr>
</thead>
@ -83,17 +83,26 @@
</table>
<ul class="collapsible">
<li>
<div class="collapsible-header"><i class="material-icons">code</i>curl example</div>
<div class="collapsible-header"><i class="material-icons">code</i>Curl Example</div>
<div class="collapsible-body">
<pre><code class="bash">
# {% blocktrans %}Notifies an email address{% endblocktrans %}<br/>
curl -X POST -d '{"urls":"mailto://user:pass@gmail.com","body":"test body","title":"test title"}' \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;-H "Content-Type: application/json" \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/notify/</code></pre>
&nbsp;&nbsp;&nbsp;&nbsp;"{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/notify/"
<br/><br/>
# {% blocktrans %}Notifies an email address with attachments{% endblocktrans %}<br/>
curl -X POST -F 'urls=mailto://user:pass@gmail.com' \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;-F 'title=test title' \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;-F 'body=test body' \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;-F attach1=@/path/to/attachment.doc \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;-F attach2=@Screenshot-2.png \ <br/>
&nbsp;&nbsp;&nbsp;&nbsp;"{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/notify/"
</code></pre>
</div>
</li>
<li>
<div class="collapsible-header"><i class="material-icons">code</i>python example</div>
<div class="collapsible-header"><i class="material-icons">code</i>Python Example</div>
<div class="collapsible-body">
<pre><code class="python">
import json<br/>
@ -104,18 +113,33 @@
&nbsp;&nbsp;&nbsp;&nbsp;'title': 'test title',<br/>
&nbsp;&nbsp;&nbsp;&nbsp;'body': 'test body',<br/>
}<br/>
<br/># The URL<br/>
req = Request(<br/>
<br/># The Request<br/>
response = Request(<br/>
&nbsp;&nbsp;&nbsp;&nbsp;"{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/notify/",<br/>
&nbsp;&nbsp;&nbsp;&nbsp;json.dumps(payload).encode('utf-8'),<br/>
&nbsp;&nbsp;&nbsp;&nbsp;{"Content-Type": "application/json"},<br/>
&nbsp;&nbsp;&nbsp;&nbsp;method='POST',<br/>
)
<br/>
</code></pre>
<pre><code class="python">
# {% blocktrans %}Notifies an email address with attachments{% endblocktrans %}<br/>
import requests<br/><br/>
payload = {<br/>
&nbsp;&nbsp;&nbsp;&nbsp;'urls': 'mailto://user:pass@gmail.com',<br/>
&nbsp;&nbsp;&nbsp;&nbsp;'title': 'test title',<br/>
&nbsp;&nbsp;&nbsp;&nbsp;'body': 'test body',<br/>
}<br/><br/>
with open("my/path/to/attachment.png", 'rb') as fp:<br/>
&nbsp;&nbsp;&nbsp;&nbsp;response = request.post("{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/notify/",<br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;data=payload,<br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;files={'attach1':('attachment.png', fp)},<br/>
&nbsp;&nbsp;&nbsp;&nbsp;)
</code></pre>
</div>
</li>
<li>
<div class="collapsible-header"><i class="material-icons">code</i>php example</div>
<div class="collapsible-header"><i class="material-icons">code</i>PHP Example</div>
<div class="collapsible-body">
<pre><code class="php">&lt;?php<br/>
<br/>
@ -145,7 +169,49 @@
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));<br/>
<br/>
//Execute the request<br/>
$result = curl_exec($ch);
$result = curl_exec($ch);<br/>
<br/>
// Close our handler<br/>
curl_close($ch);<br/>
?>
</code></pre>
<pre><code class="php">&lt;?php<br/>
// Sending an Attachment using PHP<br/><br/>
// The URL<br/>
$url = '{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/notify/';<br/>
<br/>
//Initiate cURL.<br/>
$ch = curl_init($url);<br/>
<br/>
// Prepare our File attachment<br/>
$path = '/path/to/photo.jpg';<br/>
<br/>
// Acquire our Filename<br/>
$fname = basename($path);<br/>
<br/>
// Get our attachment mime-type (in this case it's 'image/jpg')<br/>
$mimeType = mime_content_type($path);<br/>
<br/>
//The multipart data.<br/>
$data = array(<br/>
&nbsp;&nbsp;&nbsp;&nbsp;'urls' => 'mailto://user:pass@hotmail.com',<br/>
&nbsp;&nbsp;&nbsp;&nbsp;'title' => 'test title',<br/>
&nbsp;&nbsp;&nbsp;&nbsp;'body' => 'test body',<br/>
&nbsp;&nbsp;&nbsp;&nbsp;'attach1' => new CURLFile($path, $mimeType, $fname)<br/>
);<br/>
<br/>
//Tell cURL that we want to send a POST request.<br/>
curl_setopt($ch, CURLOPT_POST, 1);<br/>
<br/>
//Attach our data to the POST fields.<br/>
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);<br/>
<br/>
//Execute the request<br/>
$result = curl_exec($ch);<br/>
<br/>
// Close our handler<br/>
curl_close($ch);<br/>
?>
</code></pre>
</div>
</li>
@ -171,17 +237,17 @@
<table class="highlighted">
<thead>
<tr>
<th>{% trans "URL" %}</th>
<th style="min-width: 18%;">{% trans "URL" %}</th>
<th>{% trans "Description" %}</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>{{BASE_URL}}/add/<em>{% trans "KEY" %}</em></code></td>
<td><code>{{BASE_URL}}/add/<em>{{key}}</em></code></td>
<td>
{% blocktrans %}Used to add a new Apprise configuration or a set of URLs and associates them with the
specified <em>KEY</em>. See the <a target="_blank"
{% blocktrans %}Used to add a new Apprise configuration or a set of URLs and associates them with configuration
identified with the id of <em>{{key}}</em>. See the <a target="_blank"
href="https://github.com/caronc/apprise/wiki#notification-services">Apprise Wiki</a> if you need help
constructing your URLs.{% endblocktrans %}
<div class="section">
@ -229,23 +295,23 @@
</table>
<ul class="collapsible">
<li>
<div class="collapsible-header"><i class="material-icons">code</i>curl example</div>
<div class="collapsible-header"><i class="material-icons">code</i>Curl Example</div>
<div class="collapsible-body">
<pre><code class="bash">
# {% blocktrans %}Load a single URL and assign it to the <em>KEY</em> of abc123{% endblocktrans %}<br/>
# {% blocktrans %}Load a single URL and assign it to: <em>{{key}}</em>{% endblocktrans %}<br/>
curl -X POST -d '{"urls":"mailto://user:pass@gmail.com"}' \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;-H "Content-Type: application/json" \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/add/<em>abc123</em></code></pre>
&nbsp;&nbsp;&nbsp;&nbsp;{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/add/<em>{{key}}</em></code></pre>
<pre><code class="bash">
# {% blocktrans %}Load a simple TEXT config entry <em>KEY</em> abc123{% endblocktrans %}<br/>
# {% blocktrans %}Load a simple TEXT config entry sent to: <em>{{key}}</em>{% endblocktrans %}<br/>
curl -X POST -d '{"format":"text","config":"devops=mailto://user:pass@gmail.com"}' \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;-H "Content-Type: application/json" \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/add/abc123/
&nbsp;&nbsp;&nbsp;&nbsp;{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/add/{{key}}/
</code></pre>
</div>
</li>
<li>
<div class="collapsible-header"><i class="material-icons">code</i>python example</div>
<div class="collapsible-header"><i class="material-icons">code</i>Python Example</div>
<div class="collapsible-body">
<pre><code class="python">
import json<br/>
@ -254,9 +320,9 @@
payload = {<br/>
&nbsp;&nbsp;&nbsp;&nbsp;'urls': 'mailto://user:pass@gmail.com',<br/>
}<br/>
<br/># The URL if the key was <em>abc123</em><br/>
<br/># The URL if the key was <em>{{key}}</em><br/>
req = Request(<br/>
&nbsp;&nbsp;&nbsp;&nbsp;"{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/add/<em>abc123</em>",<br/>
&nbsp;&nbsp;&nbsp;&nbsp;"{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/add/<em>{{key}}</em>",<br/>
&nbsp;&nbsp;&nbsp;&nbsp;json.dumps(payload).encode('utf-8'),<br/>
&nbsp;&nbsp;&nbsp;&nbsp;{"Content-Type": "application/json"},<br/>
&nbsp;&nbsp;&nbsp;&nbsp;method='POST',<br/>
@ -265,12 +331,12 @@
</div>
</li>
<li>
<div class="collapsible-header"><i class="material-icons">code</i>php example</div>
<div class="collapsible-header"><i class="material-icons">code</i>PHP Example</div>
<div class="collapsible-body">
<pre><code class="php">&lt;?php<br/>
<br/>
// The URL if the key was <em>abc123</em><br/>
$url = '{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/add/<em>abc123</em>';<br/>
// The URL if the key was <em>{{key}}</em><br/>
$url = '{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/add/<em>{{key}}</em>';<br/>
<br/>
//Initiate cURL.<br/>
$ch = curl_init($url);<br/>
@ -293,7 +359,11 @@
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));<br/>
<br/>
//Execute the request<br/>
$result = curl_exec($ch);
$result = curl_exec($ch);<br/>
<br/>
// Close our handler<br/>
curl_close($ch);<br/>
?>
</code></pre>
</div>
</li>
@ -302,28 +372,27 @@
</td>
</tr>
<tr>
<td><code>{{BASE_URL}}/del/<em>{% trans "KEY" %}</em></code></td>
<td>{% blocktrans %}There are no arguments required. If the <em>KEY</em> exists and has data associated with
it,
it will be removed.{% endblocktrans %}
<td><code>{{BASE_URL}}/del/<em>{{key}}</em></code></td>
<td>{% blocktrans %}There are no arguments required. If configuration id of <em>{{key}}</em> exists and has data associated with
it, it will be removed.{% endblocktrans %}
<ul class="collapsible">
<li>
<div class="collapsible-header"><i class="material-icons">code</i>curl example</div>
<div class="collapsible-header"><i class="material-icons">code</i>Curl Example</div>
<div class="collapsible-body">
<pre><code class="bash">
# {% blocktrans %}Remove previously loaded configuration associated with the <em>KEY</em> of abc123{% endblocktrans %}<br/>
curl -X POST {{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/del/<em>abc123</em></code></pre>
# {% blocktrans %}Remove previously loaded configuration associated with the id of <em>{{key}}</em>{% endblocktrans %}<br/>
curl -X POST {{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/del/<em>{{key}}</em></code></pre>
</div>
</li>
<li>
<div class="collapsible-header"><i class="material-icons">code</i>python example</div>
<div class="collapsible-header"><i class="material-icons">code</i>Python Example</div>
<div class="collapsible-body">
<pre><code class="python">
import json<br/>
from urllib.request import Request<br/>
<br/># The request if the key was <em>abc123</em><br/>
<br/># The request if the key was <em>{{key}}</em><br/>
req = Request(<br/>
&nbsp;&nbsp;&nbsp;&nbsp;"{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/del/<em>abc123</em>",<br/>
&nbsp;&nbsp;&nbsp;&nbsp;"{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/del/<em>{{key}}</em>",<br/>
&nbsp;&nbsp;&nbsp;&nbsp;json.dumps(payload).encode('utf-8'),<br/>
&nbsp;&nbsp;&nbsp;&nbsp;{"Content-Type": "application/json"},<br/>
&nbsp;&nbsp;&nbsp;&nbsp;method='POST',<br/>
@ -332,12 +401,12 @@
</div>
<li>
<li>
<div class="collapsible-header"><i class="material-icons">code</i>php example</div>
<div class="collapsible-header"><i class="material-icons">code</i>PHP Example</div>
<div class="collapsible-body">
<pre><code class="php">&lt;?php<br/>
<br/>
// The URL if the key was <em>abc123</em><br/>
$url = '{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/del/<em>abc123</em>';<br/>
// The URL if the key was <em>{{key}}</em><br/>
$url = '{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/del/<em>{{key}}</em>';<br/>
<br/>
//Initiate cURL.<br/>
$ch = curl_init($url);<br/>
@ -346,7 +415,11 @@
curl_setopt($ch, CURLOPT_POST, 1);<br/>
<br/>
//Execute the request<br/>
$result = curl_exec($ch);
$result = curl_exec($ch);<br/>
<br/>
// Close our handler<br/>
curl_close($ch);<br/>
?>
</code></pre>
</div>
</li>
@ -354,17 +427,17 @@
</td>
</tr>
<tr>
<td><code>{{BASE_URL}}/get/<em>{% trans "KEY" %}</em></code></td>
<td><code>{{BASE_URL}}/get/<em>{{key}}</em></code></td>
<td>{% blocktrans %}This feature can be used by Apprise itself. It provides a means of remotely fetching it's
configuration.{% endblocktrans %}<br />
the configuration associated with the configuration identified through the id of <em>{{key}}</em>.{% endblocktrans %}<br />
<pre><code lang="bash"># Use Apprise to retrieve your configuration:<br/>
apprise --body="test message" --config={{ request.scheme }}://{{request.META.HTTP_HOST}}{{BASE_URL}}/get/<em>{% trans "KEY" %}</em></code></pre>
apprise --body="test message" --config={{ request.scheme }}://{{request.META.HTTP_HOST}}{{BASE_URL}}/get/<em>{{key}}</em></code></pre>
</p>
</td>
</tr>
<tr>
<td><code>{{BASE_URL}}/notify/<em>{% trans "KEY" %}</em></code></td>
<td>{% blocktrans %}Notifies the URLs associated with the specified <em>KEY</em>.{% endblocktrans %}
<td><code>{{BASE_URL}}/notify/<em>{{key}}</em></code></td>
<td>{% blocktrans %}Notifies the URLs associated with configuration identified by the id of <em>{{key}}</em>.{% endblocktrans %}
<div class="section">
<table>
<thead>
@ -411,18 +484,42 @@
</table>
<ul class="collapsible">
<li>
<div class="collapsible-header"><i class="material-icons">code</i>curl example</div>
<div class="collapsible-header"><i class="material-icons">code</i>Curl Example</div>
<div class="collapsible-body">
<pre><code class="bash">
# {% blocktrans %}Notifies all URLs assigned to the configuration{% endblocktrans %}<br/>
curl -X POST \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;-F "tag=all" \ <br/>
&nbsp;&nbsp;&nbsp;&nbsp;-F "body=test body" \ <br/>
&nbsp;&nbsp;&nbsp;&nbsp;-F "title=test title" \ <br/>
&nbsp;&nbsp;&nbsp;&nbsp;"{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/notify/<em>{{key}}</em>"</code></pre>
<pre><code class="bash">
# {% blocktrans %}Notifies all URLs assigned the <em>devops</em> tag{% endblocktrans %}<br/>
curl -X POST -d '{"tag":"devops","body":"test body","title":"test title"}' \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;-H "Content-Type: application/json" \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/notify/<em>KEY</em></code></pre>
&nbsp;&nbsp;&nbsp;&nbsp;"{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/notify/<em>{{key}}</em>"</code></pre>
</div>
</li>
<li>
<div class="collapsible-header"><i class="material-icons">code</i>python example</div>
<div class="collapsible-header"><i class="material-icons">code</i>Python Example</div>
<div class="collapsible-body">
Ideally you should leverage the Apprise Library, it will make your life much easier:
<pre><code class="python">
import apprise<br/>
<br/>
# Create an Apprise Instance<br/>
aobj = apprise.Apprise()<br/>
<br/>
# Add our URL</br>
aobj.add("apprise{% if secure %}s{%else%}{%endif%}://{{request.META.HTTP_HOST}}{{BASE_URL}}/<em>{{key}}</em>")<br/>
<br/>
# Send our notification:<br/>
aobj.notify("test body", "test title")<br/>
<br/>
# Sending an attachment is just as easy:<br/>
aobj.notify("test body", "test title", attach="/path/to/file")<br/>
</code></pre>
The legacy (but more compatible and light weight) way of doing things:
<pre><code class="python">
import json<br/>
from urllib.request import Request<br/>
@ -432,9 +529,9 @@
&nbsp;&nbsp;&nbsp;&nbsp;'title': 'test title',<br/>
&nbsp;&nbsp;&nbsp;&nbsp;'body': 'test body',<br/>
}<br/>
<br/># The URL if the key was <em>abc123</em><br/>
<br/># The URL if the key was <em>{{key}}</em><br/>
req = Request(<br/>
&nbsp;&nbsp;&nbsp;&nbsp;"{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/notify/<em>abc123</em>",<br/>
&nbsp;&nbsp;&nbsp;&nbsp;"{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/notify/<em>{{key}}</em>",<br/>
&nbsp;&nbsp;&nbsp;&nbsp;json.dumps(payload).encode('utf-8'),<br/>
&nbsp;&nbsp;&nbsp;&nbsp;{"Content-Type": "application/json"},<br/>
&nbsp;&nbsp;&nbsp;&nbsp;method='POST',<br/>
@ -443,12 +540,12 @@
</div>
<li>
<li>
<div class="collapsible-header"><i class="material-icons">code</i>php example</div>
<div class="collapsible-header"><i class="material-icons">code</i>PHP Example</div>
<div class="collapsible-body">
<pre><code class="php">&lt;?php<br/>
<br/>
// The URL if the key was <em>abc123</em><br/>
$url = '{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/notify/<em>abc123</em>';<br/>
// The URL if the key was <em>{{key}}</em><br/>
$url = '{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/notify/<em>{{key}}</em>';<br/>
<br/>
//Initiate cURL.<br/>
$ch = curl_init($url);<br/>
@ -473,7 +570,49 @@
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));<br/>
<br/>
//Execute the request<br/>
$result = curl_exec($ch);
$result = curl_exec($ch);<br/>
<br/>
// Close our handler<br/>
curl_close($ch);<br/>
?>
</code></pre>
<pre><code class="php">&lt;?php<br/>
// Sending an Attachment using PHP<br/><br/>
// The URL<br/>
$url = '{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/notify/<em>{{key}}</em>';<br/>
<br/>
//Initiate cURL.<br/>
$ch = curl_init($url);<br/>
<br/>
// Prepare our File attachment<br/>
$path = '/path/to/photo.jpg';<br/>
<br/>
// Acquire our Filename<br/>
$fname = basename($path);<br/>
<br/>
// Get our attachment mime-type (in this case it's 'image/jpg')<br/>
$mimeType = mime_content_type($path);<br/>
<br/>
//The multipart data.<br/>
$data = array(<br/>
&nbsp;&nbsp;&nbsp;&nbsp;'urls' => 'mailto://user:pass@hotmail.com',<br/>
&nbsp;&nbsp;&nbsp;&nbsp;'title' => 'test title',<br/>
&nbsp;&nbsp;&nbsp;&nbsp;'body' => 'test body',<br/>
&nbsp;&nbsp;&nbsp;&nbsp;'attach1' => new CURLFile($path, $mimeType, $fname)<br/>
);<br/>
<br/>
//Tell cURL that we want to send a POST request.<br/>
curl_setopt($ch, CURLOPT_POST, 1);<br/>
<br/>
//Attach our data to the POST fields.<br/>
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);<br/>
<br/>
//Execute the request<br/>
$result = curl_exec($ch);<br/>
<br/>
// Close our handler<br/>
curl_close($ch);<br/>
?>
</code></pre>
</div>
</li>
@ -487,7 +626,7 @@
<div class="section">
<h4>{% trans "Endpoint Notes" %}</h4>
<p>
The <em>KEY</em> you plan to associate your configuration with:
The Configuration ID (<em>{{key}}</em>) you plan to associate your configuration with:
<ol>
<li>Can not have spaces and/or special characters in it. Both a dash (<code>-</code>) and underscore
(<code>_</code>) are the only exceptions to this rule.</li>

View File

@ -23,6 +23,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import re
import binascii
import os
import tempfile
import shutil
@ -30,6 +31,7 @@ import gzip
import apprise
import hashlib
import errno
import base64
from django.conf import settings
@ -63,6 +65,202 @@ STORE_MODES = (
)
class Attachment(apprise.attachment.AttachFile):
"""
A Light Weight Attachment Object for Auto-cleanup that wraps the Apprise
Attachments
"""
def __init__(self, filename, path=None, delete=True):
"""
Initialize our attachment
"""
self._filename = filename
try:
os.makedirs(settings.APPRISE_ATTACH_DIR, exist_ok=True)
except OSError:
# Permission error
raise ValueError('Could not create directory {}'.format(
settings.APPRISE_ATTACH_DIR))
if not path:
try:
d, path = tempfile.mkstemp(dir=settings.APPRISE_ATTACH_DIR)
# Close our file descriptor
os.close(d)
except FileNotFoundError:
raise ValueError(
'Could not prepare {} attachment in {}'.format(
filename, settings.APPRISE_ATTACH_DIR))
self._path = path
self.delete = delete
# Prepare our item
super().__init__(path=self._path, name=filename)
@property
def filename(self):
return self._filename
@property
def size(self):
"""
Return filesize
"""
return os.stat(self._path).st_size
def __del__(self):
"""
De-Construtor is used to tidy up files during garbage collection
"""
if self.delete:
try:
os.remove(self._path)
except FileNotFoundError:
# no problem
pass
def parse_attachments(attachment_payload, files_request):
"""
Takes the payload provided in a `/notify` call and extracts the
attachments out of it.
Content is written to a temporary directory until the garbage
collection kicks in.
"""
attachments = []
# Attachment Count
count = sum([
0 if not isinstance(attachment_payload, (tuple, list))
else len(attachment_payload),
0 if not isinstance(files_request, dict) else len(files_request),
])
if settings.APPRISE_MAX_ATTACHMENTS > 0 and \
count > settings.APPRISE_MAX_ATTACHMENTS:
raise ValueError(
"There is a maximum of %d attachments" %
settings.APPRISE_MAX_ATTACHMENTS)
if isinstance(attachment_payload, (tuple, list)):
for no, entry in enumerate(attachment_payload, start=1):
if isinstance(entry, str):
filename = "attachment.%.3d" % no
elif isinstance(entry, dict):
try:
filename = entry.get("filename", "").strip()
# Max filename size is 250
if len(filename) > 250:
raise ValueError(
"The filename associated with attachment "
"%d is too long" % no)
elif not filename:
filename = "attachment.%.3d" % no
except TypeError:
raise ValueError(
"An invalid filename was provided for attachment %d" %
no)
else:
# you must pass in a base64 string, or a dict containing our
# required parameters
raise ValueError(
"An invalid filename was provided for attachment %d" % no)
#
# Prepare our Attachment
#
attachment = Attachment(filename)
try:
with open(attachment.path, 'wb') as f:
# Write our content to disk
f.write(base64.b64decode(entry["base64"]))
except binascii.Error:
# The file ws not base64 encoded
raise ValueError(
"Invalid filecontent was provided for attachment %s" %
filename)
except OSError:
raise ValueError(
"Could not write attachment %s to disk" % filename)
#
# Some Validation
#
if settings.APPRISE_MAX_ATTACHMENT_SIZE > 0 and \
attachment.size > settings.APPRISE_MAX_ATTACHMENT_SIZE:
raise ValueError(
"attachment %s's filesize is to large" % filename)
# Add our attachment
attachments.append(attachment)
#
# Now handle the request.FILES
#
if isinstance(files_request, dict):
for no, (key, meta) in enumerate(
files_request.items(), start=len(attachments) + 1):
try:
# Filetype is presumed to be of base class
# django.core.files.UploadedFile
filename = meta.name.strip()
# Max filename size is 250
if len(filename) > 250:
raise ValueError(
"The filename associated with attachment "
"%d is too long" % no)
elif not filename:
filename = "attachment.%.3d" % no
except (AttributeError, TypeError):
raise ValueError(
"An invalid filename was provided for attachment %d" %
no)
#
# Prepare our Attachment
#
attachment = Attachment(filename)
try:
with open(attachment.path, 'wb') as f:
# Write our content to disk
f.write(meta.read())
except OSError:
raise ValueError(
"Could not write attachment %s to disk" % filename)
#
# Some Validation
#
if settings.APPRISE_MAX_ATTACHMENT_SIZE > 0 and \
attachment.size > settings.APPRISE_MAX_ATTACHMENT_SIZE:
raise ValueError(
"attachment %s's filesize is to large" % filename)
# Add our attachment
attachments.append(attachment)
return attachments
class SimpleFileExtension(object):
"""
Defines the simple file exension lookups
@ -130,10 +328,10 @@ class AppriseConfigCache(object):
return False
# Write our file to a temporary file
_, tmp_path = tempfile.mkstemp(suffix='.tmp', dir=path)
d, tmp_path = tempfile.mkstemp(suffix='.tmp', dir=path)
# Close the file handle provided by mkstemp()
# We're reopening it, and it can't be renamed while open on Windows
os.close(_)
os.close(d)
if self.mode == AppriseStoreMode.HASH:
try:

View File

@ -34,6 +34,7 @@ from django.views.decorators.gzip import gzip_page
from django.utils.translation import gettext_lazy as _
from django.core.serializers.json import DjangoJSONEncoder
from .utils import parse_attachments
from .utils import ConfigCache
from .utils import apply_global_filters
from .forms import AddByUrlForm
@ -121,7 +122,12 @@ class WelcomeView(View):
template_name = 'welcome.html'
def get(self, request):
return render(request, self.template_name, {})
default_key = 'KEY'
key = request.GET.get('key', default_key).strip()
return render(request, self.template_name, {
'secure': request.scheme[-1].lower() == 's',
'key': key if key else default_key,
})
@method_decorator((gzip_page, never_cache), name='dispatch')
@ -571,9 +577,7 @@ class NotifyView(View):
# our content
content = {}
if MIME_IS_FORM.match(request.content_type):
content = {}
form = NotifyForm(request.POST)
form = NotifyForm(data=request.POST, files=request.FILES)
if form.is_valid():
content.update(form.cleaned_data)
@ -604,6 +608,12 @@ class NotifyView(View):
status=status,
)
# Handle Attachments
attach = None
if 'attachments' in content or request.FILES:
attach = parse_attachments(
content.get('attachments'), request.FILES)
#
# Allow 'tag' value to be specified as part of the URL parameters
# if not found otherwise defined.
@ -837,6 +847,7 @@ class NotifyView(View):
title=content.get('title', ''),
notify_type=content.get('type', apprise.NotifyType.INFO),
tag=content.get('tag'),
attach=attach,
)
if content_type == 'text/html':
@ -863,6 +874,7 @@ class NotifyView(View):
title=content.get('title', ''),
notify_type=content.get('type', apprise.NotifyType.INFO),
tag=content.get('tag'),
attach=attach,
)
if not result:
@ -901,7 +913,7 @@ class StatelessNotifyView(View):
content = {}
if MIME_IS_FORM.match(request.content_type):
content = {}
form = NotifyByUrlForm(request.POST)
form = NotifyByUrlForm(request.POST, request.FILES)
if form.is_valid():
content.update(form.cleaned_data)
@ -999,12 +1011,19 @@ class StatelessNotifyView(View):
status=ResponseCode.no_content,
)
# Handle Attachments
attach = None
if 'attachments' in content or request.FILES:
attach = parse_attachments(
content.get('attachments'), request.FILES)
# Perform our notification at this point
result = a_obj.notify(
content.get('body'),
title=content.get('title', ''),
notify_type=content.get('type', apprise.NotifyType.INFO),
tag='all',
attach=attach,
)
if not result:

View File

@ -124,6 +124,10 @@ STATIC_URL = BASE_URL + '/s/'
APPRISE_CONFIG_DIR = os.environ.get(
'APPRISE_CONFIG_DIR', os.path.join(BASE_DIR, 'var', 'config'))
# The location to place file attachments
APPRISE_ATTACH_DIR = os.environ.get(
'APPRISE_ATTACH_DIR', os.path.join(BASE_DIR, 'var', 'attach'))
# When set Apprise API Locks itself down so that future (configuration)
# changes can not be made or accessed. It disables access to:
# - the configuration screen: /cfg/{token}
@ -155,7 +159,7 @@ APPRISE_STATELESS_URLS = os.environ.get('APPRISE_STATELESS_URLS', '')
APPRISE_STATEFUL_MODE = os.environ.get('APPRISE_STATEFUL_MODE', 'hash')
# Our Apprise Deny List
# - By default we disable all non-remote calling servicess
# - By default we disable all non-remote calling services
# - You do not need to identify every schema supported by the service you
# wish to disable (only one). For example, if you were to specify
# xml, that would include the xmls entry as well (or vs versa)
@ -172,3 +176,16 @@ APPRISE_ALLOW_SERVICES = os.environ.get('APPRISE_ALLOW_SERVICES', '')
# a call to the same server again, and again and again. By default we allow
# 1 level of recursion
APPRISE_RECURSION_MAX = int(os.environ.get('APPRISE_RECURSION_MAX', 1))
# Provided optional plugin paths to scan for custom schema definitions
APPRISE_PLUGIN_PATHS = os.environ.get(
'APPRISE_PLUGIN_PATHS', os.path.join(BASE_DIR, 'var', 'plugin')).split(',')
# Define the number of attachments that can exist as part of a payload
# Setting this to zero disables the limit
APPRISE_MAX_ATTACHMENTS = int(os.environ.get('APPRISE_MAX_ATTACHMENTS', 6))
# Defines the maximum size each attachment can be
# 8388608 == 8MB
APPRISE_MAX_ATTACHMENT_SIZE = int(
os.environ.get('APPRISE_MAX_ATTACHMENT_SIZE', 8388608))

View File

@ -17,6 +17,11 @@ http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# Upload Restriction
##
client_max_body_size 100M;
##
# Logging Settings
##

View File

@ -13,6 +13,10 @@
font-size: 0.7rem;
}
input {
display: block;
}
code {
background-color: #eee;
font-family: monospace;

View File

@ -16,3 +16,7 @@ services:
# $> chown -R 33:33 ./config
# $> chmod -R 775 ./config
# - ./config:/config:rw
# Note: The attachment directory can be exposed outside of the container if required
# $> chown -R 33:33 ./attach
# $> chmod -R 775 ./attach
# - ./attach:/attach:rw

View File

@ -1,5 +1,5 @@
django
apprise == 1.3.0
apprise == 1.4.0
# 3rd party service support
paho-mqtt