caronc / apprise

Apprise - Push Notifications that work with just about every platform!
https://hub.docker.com/r/caronc/apprise
BSD 2-Clause "Simplified" License
11.95k stars 418 forks source link

Include schema details in Apprise.details() function #59

Closed caronc closed 5 years ago

caronc commented 5 years ago

A request made in #55 was to enhance the details() function to additionally include the information needed generate an Apprise URL by knowing what attributes to ask for.

Attributes will have to address the following:

The other hairy thing to consider is that some URLs have some hacky logic behind the scenes to make it really easy for a user to input the data, but make it difficult to reverse engineer separate fields back into the URL (Telegram example).

I propose that if the details() function is to provide the possible tokens/url syntax, then the function that re-assembles them back together (knowing the tokens) should come from apprise too.

Consider the API changes:

{
    "version": "0.5.2",
    ...
    "schemas": [
    {
        "service_name": "Growl",
        "setup_url": "https://github.com/caronc/apprise/wiki/Notify_growl",
        "service_url": "http://growl.info/",
        "protocols": [
            "growl"
        ],

        # New - identify each token and some details around it
        #       that developers could use to dynamically build
        #       urls with:
        #
        "tokens": {
           "protocol": {
              "required": True,
              "private": False,
              "multi": {
                  fixed_values: [ "growl" ],
              },
            },
           "hostname": {
              "required": True,
              "private": False,
              "multi": None,
            },
           "port": {
              "required": False,
              "private": False,
              "multi": None,
            },
           "password": {
              "required": False,
              "private": True,
              "multi": None,
            }
        }
    },

This should be enough details for developers to dynamically build websites, or input fields and/or options.

From there, the construction of the URL should be as simple as:

   import apprise
   # You only need to provide the options that are required.  It can be assumed that
   # the protocol entry is the bare minimum to this:
   options = {
      # required entries
      "protocol": "growl",
      "hostname": "localhost",

      # Optional configuration"
      "password: "mypass"
   }
   print(apprise.Apprise.build_url(**options})
   # provided that the minimum required entries are met, you should always receive a
   # string response here.  In the above case:
   #           growl://mypass@localhost

This would require each notification which currently must provide a static parse_url() function the hood would also provide a build_url() function as well that the apprise class could handle figuring out based on the options it was provided.

philborman commented 5 years ago

You could use urllib quote/unquote to get around the telegram problem, and if the details are coming from a yaml file do you really need to combine them into something that looks like a url and then take them apart again?

caronc commented 5 years ago

You could use urllib quote/unquote to get around the telegram problem

Yes, I definitely could, but that would mean anyone who wants to use the Telegram notification has to know that they must change the colon (:) X characters into their URL to %3A so that it won't interfere with the URL. That's not so user friendly anymore. It's easier for them to just copy and paste token directly into their URL and just know that it will work. :slightly_smiling_face:. The hacking involved here was subtle; but it does make url generation a potential headache for developers.

... do you really need to combine them into something that looks like a url and then take them apart again?

Not quite...

Just taking a step back and considering the potential apprise.load() function that doesn't exist yet. load(). It will read in YAML files and create notification services from them we can use, but this same function could also take a dict() of tokens already parsed from another source (such as a web page or application much like yours) and still accomplish the same task as apprise.add() does.

If the blueprints were included with each notification service that identifying all of the arguments it accepts, it would make reading in the their YAML configuration seamless. But even better than that; if you knew the tokens a head of time (fed to you from the apprise.details() function), you could still build the rich website you had when you first shared your screenshot on another thread again while using apprise: i hope you don't mind me re-pasting it again here: philborman-telegram-screenshot

Then with the tokens you get back (after your users fill in the fields), you could utilize the (unwritten) apprise.load() function and save back a simple apprise URL to store in some sort of persistent storage (databases, etc). I mean the real benefit to apprise (at the end of the day) is that it is driven by a single url per notification service.

Tokens -> IN, URL -> out

It would kill 2 birds with 1 stone.

Here is a potential proposed solution based on my ranting above :slightly_smiling_face:

# import our library
import apprise

a = apprise.Apprise()

# your website/application code goes here and you constructed it dynamically
# knowing all of the notification services you'll support and the tokens it'll
# accept.

if user.submitted:
    # ... time passes and you reach this part of the code.
    # ... lets say the user submitted his configuration
    # you should have enough details to build this:
    tokens = {
       'protocol': 'growl',
       'hostname': 'nuxref.com',
       'password': 'abc123'
    }

    # new function equivalent to add('growl://abc123@nuxref.com')
    a.load(tokens)

    # outputs 1 because it was loaded okay
    print(len(a))

   # this doesn't exist yet, but this is where i'm going with it:
   for notification in a:
      # prints growl://abc123@nuxref.com so now you can save it to
      # your database for later retrieval; you don't want to save the tokens!
      # you can use the add() function to load it again - easy peasy!
      print(notification.url())

      # the ability to iterate over the apprise object would be a nice enhancement
      # regardless

   # other ideas; apprise.urls() would return a tuple containing all of
   # the urls currently loaded.
   a.urls()

I'm just bouncing ideas right now; nothing at all is set in stone. But does this make sense?

Thoughts?

philborman commented 5 years ago

Yes that looks very similar to how I think it should work. I would probably keep the tokens in the database myself rather than the url and regenerate on each notification, just because if the user typed it in wrong it's easier to give them the tokens back for editing, and also if they want to send to multiple instances of the same notifier type (several kodi instances for example) they can cut and paste tokens.
The app currently allows different notifiers depending on the reason for the notification so we need to rebuild anyway, or have two Apprise() objects with different notifiers in each.

caronc commented 5 years ago

The app currently allows different notifiers depending on the reason for the notification so we need to rebuild anyway, or have two Apprise() objects with different notifiers in each.

You're going to benefit from #34 then :slightly_smiling_face: . The idea is to do something like this:

import apprise

a = apprise.Apprise()

# Tag/Category/Pool/Namespace (name hasn't been determined yet)
# name the tag whatever you want:
a.add('growl://abc123@nuxref.com', tag=["critical_failures", "website_info"])
a.add('mailto://user@pass:gmail.com', tag="website_info")
a.add('mailto://system_admin@pass:yahoo.ca', tag="critical_failures")

# Then the notify() changes ever so slightly and only notifies the services
# you tagged:
a.notify(title="uh oh", body="won't send to user@gmail.com", tag="critical_failures")

You'll be able to notify a group as well if the tag is a list/tuple/set as well in conjunction with the notify() call. That will allow you to no longer need to manage multiple apprise objects per different system event/use.

caronc commented 5 years ago

I started trying to get this feature request going, but it feels incomplete; here is what the JSON output looks like:

...
    {
      "service_name": "JSON", 
      "setup_url": "https://github.com/caronc/apprise/wiki/Notify_Custom_JSON", 
      "secure_protocols": [
        "jsons"
      ], 
      "service_url": null, 
      "protocols": [
        "json"
      ],
      "details": {
        "templates": [
          "{schema}://{host}", 
          "{schema}://{host}:{port}", 
          "{schema}://{user}@{host}:{port}", 
          "{schema}://{user}:{pass}@{host}:{port}"
        ], 
        "tokens": {
          "schema": {
            "values": [
              "json", 
              "jsons"
            ], 
            "required": true, 
            "type": "choice:string", 
            "private": false
          }, 
          "host": {
            "values": null, 
            "required": true, 
            "type": "string", 
            "private": false
          }, 
          "password": {
            "values": null, 
            "required": false, 
            "type": "string", 
            "private": true
          }, 
          "port": {
            "min": 1, 
            "max": 65535, 
            "required": false, 
            "private": false, 
            "values": null, 
            "type": "int"
          }, 
          "user": {
            "values": null, 
            "required": false, 
            "type": "string", 
            "private": false
          }
        }, 
        "args": {
          "overflow": {
            "default": "upstream", 
            "values": [
              "upstream", 
              "truncate", 
              "split"
            ], 
            "type": "choice:string"
          }, 
          "image": {
            "default": true, 
            "type": "choice:bool"
          }, 
          "format": {
            "default": "text", 
            "values": [
              "text", 
              "html", 
              "markdown"
            ], 
            "type": "choice:string"
          }
        }, 
        "kwargs": {
          "+": {
            "key": "{http_header_name}", 
            "value": {
              "type": "string"
            }
          }
        }
      }
    },
...

My problem is it seems like i need to include a label as well, however then it should probably accommodate more then just English. I'm trying to justify if the demand for this is worth the effort at this time...

All code so far has been pushed to this branch. The enhancement looks like this so far.

I'd be interested in some feedback before I spend to much more time here.

philborman commented 5 years ago

Maybe apprise should only supply the list of valid templates and let the calling app do the rest of the work, so in your example above the json would only include

"templates":  
 [   
          "{schema}://{host}",   
          "{schema}://{host}:{port}",   
          "{schema}://{user}@{host}:{port}",  
          "{schema}://{user}:{pass}@{host}:{port}" 
        ],

then the app could prompt the user for the parts it doesn't know and construct a valid url. Saves a lot of work in apprise and the app can handle language issues, optional elements, default values

caronc commented 5 years ago

Thanks for your input @philborman

The only problem with just providing the templates is to decide how many URL combinations should I provide? I mean those are 4 variations above are for a very simple URL (JSON). There are much more complicated ones. Even the simple one above doesn't identify all of the combinations by far. It also doesn't include for the http-header entries you can create using the plus (+) symbol on arguments specified. But wait... there's more :slightly_smiling_face: ... It also doesn't account for some args that can always be added (to any url) such as ?format=text (html or markdown), ?image=No or ?Image=Yes (depends), and ?overflow=upstream (split or truncate). Other individual schema's (such as Discord) have many more of these combinations too. This would make for a really messy templates reference. I think the template entry is a good spot to put some basic references as a guide (so you don't have to route everyone to my Github page); but not a spot to identify every variation and/or combination.

Honestly, for this kind of feature request, it still kinda makes sense to provide the individual token details too. But if you're okay with just references to {token} and {secret}, etc without a gettext description of the field, then maybe what is presented above is still okay? Can you think of any fields in the token details that might be missing that would help you dynamically work with the different notifications?

Thoughts?

caronc commented 5 years ago

@philborman i'd be curious on any feedback or suggestions ~you might want to add before i finish going through the remaining 30+ services~. See pull request #112 for details.

caronc commented 5 years ago

Closing this ticket as it has been completed.