Merge branch 'main' into main

pull/374/head
Dave Jones 2022-08-08 16:23:17 -05:00 zatwierdzone przez GitHub
commit 8d031cd7b9
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
6 zmienionych plików z 854 dodań i 99 usunięć

107
README.md
Wyświetl plik

@ -29,8 +29,7 @@ The podcast namespace is part of the larger "Podcasting 2.0" project which exist
**Phase 4** - [Closed] Comment period closed on `12/1/2021` and [3 tags](https://github.com/Podcastindex-org/podcast-namespace#phase-4-closed-on-1212021) were **formalized**.
**Phase 5** - [Open] Comment period is open as of `3/2/2022`. Tag submissions and comments are welcome at this time. Tags [here](https://github.com/Podcastindex-org/podcast-namespace#phase-5-open-as-of-322022)
are under active consideration. New proposals are also [here](https://github.com/Podcastindex-org/podcast-namespace/issues?q=is%3Aissue+is%3Aopen+label%3Aproposal).
**Phase 5** - [Closed] Comment period closed on `7/15/2022` and [1 tag](https://github.com/Podcastindex-org/podcast-namespace#phase-5-closed-as-of-7152022) was **formalized**.
<br><br>
@ -164,106 +163,20 @@ full implementation details.
<br>
## <u>Phase 5 (Open as of 3/2/2022)</u>
## <u>Phase 5 (Closed as of 7/15/2022)</u>
The following tags are being actively considered for adoption into the namespace as part of this phase. They are a work in progress and feedback on them is desired.
<br>
The following tags have been formally adopted into the namespace. They are fully documented in the XMLNS document located [here](docs/1.0.md). Please see that file for
full implementation details.
<br><br><!-- Tag block -->
### **\<podcast:socialInteract>** - <small>[Discuss](https://github.com/Podcastindex-org/podcast-namespace/issues/357)</small>
<b>
- **\<podcast:socialInteract>** <br>
- **\<podcast:block>** <br>
```xml
<podcast:socialInteract
uri="[uri of root post/comment(string)]"
protocol="[slug of social protocol being used(slug)]"
accountId="[account id of posting party(string)]"
accountUrl="[url to posting party's platform profile(string)]"
priority="[the order of rendering(int)]"
/>
```
<br>
</b>
#### Item
#### (optional | multiple)
This element allows a podcaster to attach the "root post" of a comment thread to an episode. This "root post" is treated as the canonical location of where the comments and discussion around this
episode will take place. This can be thought of as the "official" social media comment space for this episode. If a protocol such as "activitypub" is used, or some other
protocol that allows programmatic API access, these comments can be directly pulled into the app, and replies can be posted back to the thread from the app itself.
If multiple `<podcast:socialInteract>` tags are given for an `<item>`, the `priority` attribute is strongly recommended to give the app an indication as to which comments to
display first.
This tag can also be used as a signal to platforms and apps that the podcaster does not want public comments shown alongside this episode. For this purpose a `protocol` value
of "disabled" can be specified, with no other attributes or node value present.
#### Attributes
- **uri** (required) The uri/url of root post comment.
- **protocol** (required) The [protocol](socialprotocols.txt) in use for interacting with the comment root post.
- **accountId** (recommended) The account id (on the commenting platform) of the account that created this root post.
- **accountUrl** (optional) The public url (on the commenting platform) of the account that created this root post.
- **priority** (optional) When multiple socialInteract tags are present, this integer gives order or priority in ascending order of importance. A lower number means higher priority.
Example (simple):
```xml
<podcast:socialInteract uri="https://podcastindex.social/web/@dave/108013847520053258" protocol="activitypub" accountId="@dave" />
```
Example (complex):
```xml
<podcast:socialInteract priority="1" uri="https://podcastindex.social/web/@dave/108013847520053258" protocol="activitypub" accountId="@dave" accountUrl="https://podcastindex.social/web/@dave" />
<podcast:socialInteract priority="2" uri="https://twitter.com/PodcastindexOrg/status/1507120226361647115" protocol="twitter" accountId="@podcastindexorg" accountUrl="https://twitter.com/PodcastindexOrg" />
```
Example (disabled):
```xml
<podcast:socialInteract protocol="disabled" />
```
<br><br><!-- Tag block -->
### **\<podcast:block>** - <small>[Discuss](https://github.com/Podcastindex-org/podcast-namespace/issues/179)</small>
<b>
```xml
<podcast:block
exclude="[comma delimited list(string)]"
>
[yes|no(bool)]
</podcast:block>
```
</b>
#### Channel
#### (optional | single)
This element allows a podcaster to express which platforms are allowed to publicly display this feed and it's contents. In it's basic form, it is a direct drop-in
replacement for `<itunes:block>` and functions identically. The addition of the `exclude` attribute allows for a comma separated list of platforms from the [slug list](serviceslugs.txt)
to be given which operates as an inversion of the intent expressed by the node value.
#### Attributes
- **exclude** (optional) A comma separated list of platforms that will be interpreted as a block list or an allow list based on the inversion of the node value (yes or no).
Examples:
```xml
<!-- This means "block everything" -->
<podcast:block>yes</podcast:block>
<!-- This means "block nothing" -->
<podcast:block>no</podcast:block>
<!-- This means "block everything except spotify and google" -->
<podcast:block exclude="spotify,google">yes</podcast:block>
<!-- This means "block nothing other than apple and podcast index" -->
<podcast:block exclude="apple,podcastindex">no</podcast:block>
```
The following tags are under review for inclusion in this phase. Changes to them are now frozen. Any modifications will mean the tag needs
to be reworked and submitted to Phase 6.

Wyświetl plik

@ -424,7 +424,7 @@ to allow for file integrity checking.
### Attributes
- **type:** (required) Mime type of the media asset.
- **length:** (required) Length of the file in bytes.
- **length:** (recommended) Length of the file in bytes.
- **bitrate:** (optional) Average encoding bitrate of the media asset, expressed in bits per second.
- **height:** (optional) Height of the media asset for video formats.
- **lang:** (optional) An [IETF language tag (BCP 47)](https://en.wikipedia.org/wiki/BCP_47) code identifying the language of this media.
@ -872,3 +872,105 @@ The node value is a free form string meant to explain to the user where this con
```xml
<podcast:contentLink href="https://twitter.com/statuses/somepost">Chat on Twitter!</podcast:contentLink>
```
<br><br><br><br><!-- Tag block -->
## Social Interact
`<podcast:socialInteract>`<br><br>
The `socialInteract` tag allows a podcaster to attach the url of a "root post" of a comment thread to an episode. This "root post"
is treated as the canonical location of where the comments and discussion around this episode will take place. This can be thought
of as the "official" social media comment space for this episode. If a protocol such as "activitypub" is used, or some other
protocol that allows programmatic API access, these comments can be directly pulled into the app, and replies can be posted back to
the thread from the app itself.
If multiple `socialInteract` tags are given for an `<item>`, the `priority` attribute is strongly recommended to give the app an
indication as to which comments to display first.
This tag can also be used as a signal to platforms and apps that the podcaster does not want public comments shown alongside this
episode. For this purpose a `protocol` value of "disabled" can be specified, with no other attributes or node value present.
### Parent
&nbsp; `<item>`
### Count
&nbsp; Multiple
### Attributes
- **uri** (required) The uri/url of root post comment.
- **protocol** (required) The [protocol](/socialprotocols.txt) in use for interacting with the comment root post.
- **accountId** (recommended) The account id (on the commenting platform) of the account that created this root post.
- **accountUrl** (optional) The public url (on the commenting platform) of the account that created this root post.
- **priority** (optional) When multiple socialInteract tags are present, this integer gives order of priority. A
lower number means higher priority.
Example (simple):
```xml
<podcast:socialInteract uri="https://podcastindex.social/web/@dave/108013847520053258" protocol="activitypub" accountId="@dave" />
```
Example (complex):
```xml
<podcast:socialInteract priority="1" uri="https://podcastindex.social/web/@dave/108013847520053258" protocol="activitypub" accountId="@dave" accountUrl="https://podcastindex.social/web/@dave" />
<podcast:socialInteract priority="2" uri="https://twitter.com/PodcastindexOrg/status/1507120226361647115" protocol="twitter" accountId="@podcastindexorg" accountUrl="https://twitter.com/PodcastindexOrg" />
```
Example (disabled):
```xml
<podcast:socialInteract protocol="disabled" />
```
<br><br><br><br><!-- Tag block -->
## Block
`<podcast:block>`<br><br>
This element allows a podcaster to express which platforms are allowed to publicly display this feed and its contents.
In its basic form, it is a direct drop-in replacement for the `<itunes:block>` tag, but allows for greater flexibility
by the inclusion of the `id` attribute and by including multiple copies of itself in the feed.
Platforms should not ingest a feed for public display/use if their slug exists in the `id` of a `yes` block tag, or if
an unbounded `yes` block tag exists (meaning block all public ingestion). Conversely, if a platform finds their slug in
the `id` of a `no` block tag, they are free to ingest that feed for public display/usage.
In plain language, the sequence of discovery an ingesting platform should use is as follows:
1. Does `<podcast:block id="[myslug]">no</podcast:block>` exist in this feed? Safe to ingest.
2. Does `<podcast:block id="[myslug]">yes</podcast:block>` exist in this feed? Do not ingest.
3. Does `<podcast:block>yes</podcast:block>` exist in this feed? Do not ingest.
### Parent
&nbsp; `<channel>`
### Count
&nbsp; Multiple
### Attributes
- **id** (optional) A single entry from the [service slug list](https://github.com/Podcastindex-org/podcast-namespace/blob/main/serviceslugs.txt).
### Examples
```xml
<!-- This means "block everything" -->
<podcast:block>yes</podcast:block>
```
```xml
<!-- This means "block nothing" (same as not present) -->
<podcast:block>no</podcast:block>
```
```xml
<!-- This means "block only google and amazon" -->
<podcast:block id="google">yes</podcast:block>
<podcast:block id="amazon">yes</podcast:block>
```
```xml
<!-- This means "block every platform _except_ google and amazon" -->
<podcast:block>yes</podcast:block>
<podcast:block id="google">no</podcast:block>
<podcast:block id="amazon">no</podcast:block>
```

Wyświetl plik

@ -102,6 +102,7 @@ For elements that are included in the official [DTD](https://github.com/Podcasti
1. [Castos](https://castos.com/earn-bitcoin-from-your-listeners/)
2. [usocial](http://usocial.me/history#v0.1.1)
3. [RSS Blue](https://rssblue.com/help/podcast-metadata#lightning-node)
4. [Alby](https://getalby.com/podcast-wallet)
## Medium `<podcast:medium>`
1. [RSS Blue](https://rssblue.com/help/podcast-metadata#medium)
1. [RSS Blue](https://rssblue.com/help/podcast-metadata#medium)

Wyświetl plik

@ -0,0 +1,212 @@
# The Shared SoundBites Specification
<small>Version 0.01 by John Chidgey - 2022.05.18</small>
<br>
## Purpose
SoundBites to date have been placed in the podcast source of truth: the RSS feed. Whether they remain embedded in the XML or converted into a referenced JSON file (as per PC2.0 Chapters) as a potential future evolution should be considered to reduce potential feed bloat/hosting bandwidth considerations. The podcaster controls and owns their RSS feed (or should) and as such controls the flow of value and approval of that feeds' content.
SoundBites remain a burden (however slight) to the podcaster to create and embed in their feed. Traditionally SoundBites have been shared via applications, encoding as separate audio files and attaching/posting to social media platforms, or via embedding them in a website and shared as a link. All existing methods do not allow the podcast creator to easily import them in their RSS feed, nor to reward those that created them. (Soundbiters)
To accomplish this, a standard SoundBite sharing format could permit sharing in a simple form that can be imported by the podcaster using local tools or easily added to hosting provider platforms, as well as played in any web/device app. Such a format should contain the existing elements of the SoundBite tag, but also reference the audio file and value address for V4V.
In this way the podcaster could present a new incentivisation pathway for V4V redistribution, with any playback of the SoundBite they incorporate into the RSS feed getting a set split of streaming value of an agreed fixed limit (per minute is not useful as clips vary in length and longer clips would incentivise poor behaviours) between the SoundBiter and the podcaster.
## Requirements
Audio only, Constant BitRate encoded audio file.
## Implementation Challenges
The onus on playhead position and duration for audio/video remains on the client application, and if the podcaster chooses a non-linear format (eg VBR encoding) then playhead position and duration can be difficult to determine. There are many video formats in use which could be problematic, hence restricting this to Audio/MPEG3 at Constant BitRate (CBR) is the best place to start as a requirement for use in this standard. It might make more sense (TBD) for the podcaster to specify the MP3 to use if Alternate enclosure is used - in some cases primary download references for statistics will be thrown out significantly if the same file is used for play/scrub to position for soundclips.
## SoundBite Tag Ammendments
Group soundbites under a podcast:soundbites element, with each soundbite being an individual element beneath that, with the option to use JSON (per the Chapters standard). With the introduction of the alternate enclosure tag and to allow editing without needing to query the RSS feed item directly, linking to a source audio file of truth will reduce ambiguity and guarantee timestamps are correctly aligned.
#### Parent
`<item>`
#### Count
Single
#### Attributes
- **open (OPTIONAL)**: Default = TRUE. Boolean: If TRUE, open to accepting soundbite submissions for this item/episode. If FALSE, any submissions will be rejected.
- **split (OPTIONAL):** The number of shares of the payment this recipient will receive, hence 50 = equal amount to podcaster and to soundbiter, 1 all to soundbiter, 0 all to podcaster.
- **url (OPTIONAL):** The URL where the soundbites file is located.
- **type (OPTIONAL):** Mime type of file - JSON preferred, 'application/json+soundbites'.
#### Examples
`<podcast:soundbites open="true" split="50" url="https://example.com/episode1/soundbites.json" type="application/json+soundbites" />`
## The episode is open for submissions, however none currently exist or have been accepted into the feed
`<podcast:soundbites open="true" split="50" />`
<br><br>
### Soundbites Element
The `valueRecipient` tag designates various destinations for payments to be sent to during consumption of the enclosed
#### Parent
`<podcast:soundbite>`
#### Count
Multiple
#### Attributes
- **startTime:** (UNCHANGED) The time where the soundbite begins
- **duration:** (UNCHANGED) How long is the soundbite (recommended between 15 and 120 seconds)
- **title:** (Now required, was a node value now a named attribute) Used as free form string from the podcast creator to specify a title for the soundbite. Please do not exceed `128 characters` for the title value or it may be truncated by aggregators.
- **url**: Source Audio file URL
## Value Attributes
- **name (OPTIONAL):** A free-form string that designates who or what this recipient is.
- **type (OPTIONAL/REQUIRED):** This is the service slug of the cryptocurrency or protocol layer hat represents the type of receiving address that will receive the payment.
- **method (OPTIONAL):** This is the transport mechanism that will be used. (TBD: Was Keysend now blank in podcast:value?)
- **address (OPTIONAL/REQUIRED):** This denotes the receiving address of the payee.
- **customKey (OPTIONAL):** The name of a custom record key to send along with the payment.
- **customValue (OPTIONAL):** A custom value to pass along with the payment. This is considered the value that belongs to the `customKey`.
#### Value notes:
The value tag attribute: 'split' is defined at the top level and should be equal for all soundBites in a given feed, which is set by the Podcaster. Both the Podcaster(s) and the soundBite(r) should be compensated for their effort and hence a 50/50 split (0.5) should be default.
The fee is a per play amount before the split and is derived from the podcast:value tags `suggested` attribute, applied over the recommended maximum duration of a soundbite (nominally 120 seconds). For a client to process a soundbite value split therefore, it is mandatory to have a podcast:value block defined as well.
TBD 1: For Lightning, 1 sat/min streaming rate, 50/50 split is only 1 sat/soundbite which is too small to route. So we consider a 10x multipler by default to avoid this? How much do we value soundbiters - I think that's probably fair?)
TBD 2: Which is the Podcaster true recipient? In a multi-host podcast there could be multiple therefore perhaps an split between all parties defined in the value block for this item.
There is nothing stopping a podcaster from changing this split after multiple soundBites have been created however those creating the soundBite that are motivated by this would observe no value from that effort and would stop contributing in future in that was the case.
Value attributes are all optional, however if they are to be used, optional/required indicates they are required if value is to be used. Some soundbiter(s) may be happy with name attribution, no attribution whatsoever and not have a streaming value destination available to them.
#### XML Examples
`<podcast:soundbites split="50" url="https://example.com/episode1/soundbites.json" type="application/json+soundbites" />`
```
<podcast:soundbites split="50">
<podcast:soundbite
startTime="1234.5"
duration="42.25"
title="Why the Podcast Namespace Matters"
url="https://somewhere.hostingplace.com/ashow/E001-anEpisode.mp3"
name="A Soundbiter"
type="node"
address="02d5c1bf8b940dc9cadca86d1b0a3c37fbe39cee4c7e839e33bef9174531d27f52"
customKey="[optional key to pass(mixed)]"
customValue="[optional value to pass(mixed)]"
/>
<podcast:soundbite
startTime="134.5"
duration="30.0"
title="Why Soundbites Matter"
url="https://somewhere.hostingplace.com/ashow/E001-anEpisode.mp3"
name="A Soundbiter again"
type="node"
address="02d5c1bf8b940dc9cadca86d1b0a3c37fbe39cee4c7e839e33bef9174531d27f52"
customKey="[optional key to pass(mixed)]"
customValue="[optional value to pass(mixed)]"
/>
</podcast:soundbites>
```
#### JSON Server File Example
```
{
"version" : "1.0.0",
"soundbites" :
[
{
"startTime" : 1234.5,
"duration" : 42.25,
"title" : "Why the Podcast Namespace Matters",
"url" : "https://somewhere.hostingplace.com/ashow/E001-anEpisode.mp3",
{
"name" : "A Soundbiter",
"type" : "node",
"address" : "02d5c1bf8b940dc9cadca86d1b0a3c37fbe39cee4c7e839e33bef9174531d27f52",
"customKey" : "[optional key to pass(mixed)]",
"customValue" : "[optional value to pass(mixed)]"
}
},
{
"startTime" : 134.5,
"duration" : 30.0,
"title" : "Why Soundbites Matter",
"url" : "https://somewhere.hostingplace.com/ashow/E001-anEpisode.mp3",
{
"name" : "A Soundbiter again",
"type" : "node",
"address" : "02d5c1bf8b940dc9cadca86d1b0a3c37fbe39cee4c7e839e33bef9174531d27f52",
"customKey" : "[optional key to pass(mixed)]",
"customValue" : "[optional value to pass(mixed)]"
}
}
]
}
```
## Soundbite Sharing Specification
Sharing a soundbite has to be easy for client or web applications to implement and for podcast hosts or podcast servers to ingest with tools to add/insert into the RSS/JSON for the source of truth RSS Feed.
Two methods are suggested for sharing: JSON File and a Query string URL. In either scenario, only one soundbite may be shared per file/string. Each soundbite will be parsed individually.
#### Sharing Attributes
When submitting a Soundbite to the podcaster, it is necessary to include both the RSS Feed URL and the Episode number to make it easier to correlate the entry with the podcasters show. It should be possible to reverse look-up based on the Audio file URL provided however this is kinder approach to those implementing this and it should be transparent to the Soundbite creator anyhow.
- **feed**: The RSS Feed of the podcast the Soundbite is for
- **episode**: The episode number of the podcast the Soundbite is for
#### JSON Sharing File Example
```
{
"startTime" : 134.5,
"duration" : 30.0,
"title" : "Why Soundbites Matter",
"url" : "https://somewhere.hostingplace.com/ashow/E001-anEpisode.mp3",
"feed" : "https://somewhere.hostingplace.com/ashow/feed.xml",
"episode" : 10,
{
"name" : "A Soundbiter again",
"type" : "node",
"address" : "02d5c1bf8b940dc9cadca86d1b0a3c37fbe39cee4c7e839e33bef9174531d27f52",
"customKey" : "[optional key to pass(mixed)]",
"customValue" : "[optional value to pass(mixed)]"
}
}
```
#### Query String Formatting
String formatting shall comply with RFC3986.
#### Proposal for Web Library
Loads a audio from a RSS Feed Item, play to position, pause, start in flag, play to stop flag, scrape EMail from RSS Feed, send EMail from webpage.
#### GoHugo Template Converter JSON
Develop this and publish it
#### Server Component (Soundbitten)
Accepts API calls from a client, presents the soundbites for a podcaster to select from, download the ones you want to keep and name them in JSON file for inclusion/import into RSS feed.
#### Client Component (Soundbiter)
Sends API call to Soundbitten service.
Discussion here:
- https://github.com/Podcastindex-org/podcast-namespace/issues/205
- https://podcastindex.social/web/statuses/105833620038854052

Wyświetl plik

@ -1,7 +1,25 @@
<hr>
<hr>
<hr>
# THIS IS NOT THE FINAL SPECIFICATION
You [want to look here for that](https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#social-interact)
<hr>
<hr>
<hr>
# The "podcast:social" Specification
<small>Version 1.0 by Benjamin Bellamy - 2021.04.13</small>
**This is not the final specification. You [want to look here for that](https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#social-interact)**
<br />
## Purpose

Wyświetl plik

@ -0,0 +1,509 @@
# The "podcast:verify" Specification
<small>Version 1.0 by [@pofmagicfingers](https://github.com/pofmagicfingers) - 2022.08.03</small>
<br>
# Purpose
"Claiming" a podcast in a podcast directory, platform, app or service means that your customer is proving that
they are the owner of the podcast feed and have current control of it. You might claim a podcast in Apple to add to your
iTunes Connect account, Podchaser to add host information, in GoodPods to be notified of comments, in
Podcasterwallet.com to add value4value info, or in Spotify to add it to the Spotify dashboard for analytics.
The "quick-claim" process described in this document, is a programmatic way to claim a podcast, by taking a podcast
owner from the claiming service to an authenticated page on their podcast host to prove ownership.
To enable the quick claiming of a podcast, we propose a new `<podcast:verify>` tag to hold the relevant URI's and key
material.
<br>
# The Claiming Service
Run a podcast directory, app, platform or service and want to check that someone is the owner of this podcast?
Benefits of using "quick-claim" are that it's one-click within a browser or webview, rather than sending a potential
user an email. Assuming they are logged-in to their podcast host (or can log in), they can approve your request
instantly and come back to your service within seconds.
Additional benefits are:
* Anyone authorized to access the podcast host dashboard can claim the show (subject to the user having the right
permissions).
* Email confirmations are often subject to delay, fall into spam, or are blocked altogether.
* Email confirmation emails can be faked and lead to security concerns.
* The listing of an email address in an RSS feed leads to privacy issues and spam.
* Email confirmations often lead to around [25%](https://www.quora.com/What-is-a-typical-abandonment-rate-for-email-verifications) abandomnent
<br>
## 1. Check that quick-claiming is enabled in the RSS feed
```xml
<podcast:verify
auth="https://hostingprovider.com/claiming/"
pub="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg=="
/>
```
The presence of the `<podcast:verify>` tag means that this feed supports quick claiming. The tag will have two required
attributes that must be present:
* `auth` (required): The https URL of a "quick claim" page on the hosting provider that is protected by customer login.
* `pub` (required): The public key corresponding to the private key the hosting provider users to sign claiming requests.
<br>
## 2. Give the podcaster a "Quick Claim" button
You will redirect your user to the `auth` url present in the podcaster's RSS feed, with 1 to 3 URL parameters, as
defined below.
### Param: `consumer`
Your directory/service base URL of the return_path.
It's recommanded to support open graph allowing the hosting provider to present a clean name and icon for your
service while asking permission. If open graph is not supported, hosting provider will most likely fallback to
hostname, path, favicon.
Splitting the return url into a `consumer` and a `return_path` gives us control of which part of the url is used
for open graph data while ensuring the return path is tied to the hostname and identity presented to the user.
#### Examples:
`consumer=https://podcastindex.org`
```html
PodcastIndex.org (podcastindex.org)
The Podcast Index is here to preserve, protect and extend the open, independent podcasting ecosystem.
This service would like to verify you are the owner of this podcast. Do you want us to confirm ?
```
`consumer=https://podcastindex.org/quick_claim`
```html
🗼PodcastIndex Quick Claiming (podcastindex.org)
Claiming your podcast into The Podcast Index directory gives you cool stuff !
This service would like to verify you are the owner of this podcast. Do you want us to confirm ?
```
<br>
### Param: `return_path`
A path relative to the `consumer` parameter, to redirect with the result of the authentication. If the consumer is enough,
you can omit it.
#### Examples:
These are example calculations of what the return url will be with different inputs:
`consumer=https://podcastindex.org/quick_claim` (no return path) ⤵ <br>
```http
https://podcastindex.org/quick_claim?token=[token]
```
`consumer=https://podcastindex.org/quick_claim&return_path=/claimed.php`<br>
```http
https://podcastindex.org/quick_claim/claimed.php?token=[token]
```
`consumer=https://podcastindex.org/quick_claim&return_path=../claim.php`<br>
```http
https://podcastindex.org/claim.php?token=[token]
```
<br>
### Param: `guid`
If a [`<podcast:guid>`](https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#guid) is present in the RSS feed it SHOULD be included. If it's not we omit it or send an empty string.
We keep it simple, we should trust the users of our spec (in this case the hosting providers) : if they don't include
a [`<podcast:guid>`](https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#guid), I don't see the point to calculate it and give it to them, as they most likely don't have it on their
hand and probably don't use it. If they would, it would be included in the feed.
If we don't have any [`<podcast:guid>`](https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#guid), maybe the hosting provider has only partial support of the podcasting 2.0 spec. Maybe
they only support claiming and include the guid in the auth url. Perhaps it's a Wordpress extension and there is only
one podcast and no need to "select a podcast" when you're logged in. We can't know and guess all usages.
<br>
### PHP Example
Here's a simple example, in PHP, of a quick claim button:
```php
<a
class="btn btn-default"
href="<?php echo $feed_auth_url."?consumer=https://myservice.com/quick_claim&guid=".$feed_guid; ?>"
>
Claim this now
</a>
```
<br>
## 3. Handle the response from the hosting provider
If the podcaster wants to claim this podcast, they will confirm it inside their hosting service. You will receive a GET
request to the `return URL` computed from the `consumer` and `return_path`.
The GET request will contain only one parameter: `token`.
This parameter will be a JSON Web Token ([JWT](https://jwt.io/)), and will include the following data :
```typescript
type QuickClaimResponse = {
guid?: GUID;
accepted: true;
} | {
guid?: GUID;
accepted: false;
failureReason: "back" | string;
};
```
Using JWT lets us ensure the hosting provider wrote the response. It also allow us to define, or not, an expiration
date and such. ([More info on JWT](https://jwt.io/))
The JWT must use an asymmetric signing algorithm (ES256 for example). The response is signed by the hosting provider
private key, and it can be verified using the `pub` attribute of the tag (put there by the same hosting provider).
You can verify the signature in PHP with a JWT library. If it fails to decode, it means the signature is wrong or the
token has expired:
```php
$result = JWT::decode($quickClaimResponse, $feed["podcast:lock"]["pub"]);
```
As a podcast directory, app, platform or service you now have the confirmation the user was indeed authenticated,
confirmed the operation, and that it's the same entity giving you this information and producing the RSS feed.
<br><br>
# The Hosting Service
Quick-claim is designed to allow your customers to demonstrate that they own their podcast on third-party services.
The benefits of using quick claiming are that it's one-click for your customers from the service they wish to claim,
direct to you. You can monitor the services your users are using, and can give multiple people access to claiming a
podcast on a separate service based on your service's access levels.
The email in your customer's RSS feed need not be the registration email of the user on this third-party service, thus
lowering your customer support calls and streamlining access for your customers. Ease of use on third-party services
retains the customer with your company, thus lowering churn, and possibly giving them more access and interaction,
prolonging their activity with you.
If you're a podcast host wanting to add quick claiming for your customers, then here's how you can do it painlessly:
<br>
## 1. Add the `<podcast:verify>` tag to your RSS feeds
```xml
<podcast:verify
auth="https://amazingpodcasthost.example.com/claiming"
pub="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg=="
/>
```
The tag needs two attributes :
* `auth` (required): The URL of a secure page that is protected on your server by customer login.
* `pub` (required): The public key corresponding to the private key you use to sign claiming requests.
<br>
## 2. Host a page for your customer to agree to "claim" a podcast
Your claiming `auth` url will be called with 1 to 3 parameters, as defined below:
### Param: `consumer`
The directory/service base URL.
This url must be used to present the permission asker to the user. It's recommended for services to support
open graph on this URL allowing the hosting provider to present a clean name and icon. If open graph is not supported,
you will most likely fallback to title and favicon, hostname only, or full url.
Splitting the return url into a `consumer` and a `return_path` gives us control of which part of the url is used for
open graph data while ensuring the return path is tied to the hostname and identity presented to the user.
<br>
### Param: `return_path`
A path relative to the `consumer` parameter, to use as a callback to the `consumer` with the result of the authentication.
If the `consumer` is enough, you can omit this parameter.
These are example calculations of what the return url will be with different inputs:
#### Examples:
`consumer=https://podcastindex.org/quick_claim` (no return path) ⤵ <br>
```http
https://podcastindex.org/quick_claim?token=[token]
```
`consumer=https://podcastindex.org/quick_claim&return_path=/claimed.php`<br>
```http
https://podcastindex.org/quick_claim/claimed.php?token=[token]
```
`consumer=https://podcastindex.org/quick_claim&return_path=../claim.php`<br>
```http
https://podcastindex.org/claim.php?token=[token]
```
<br>
### Param: `guid`
If a `<podcast:guid>` tag was present in the RSS feed, expect it to also be included as a parameter here.
### PHP Example:
```php
<?php
$HOSTING_PRIVATE_KEY = "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2
OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r
1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G";
if (!$user['loggedin']) {
// this user is not logged in. Take them to log in
// Retain the consumer, return_path, guid, and bring them back here
//
exit;
}
// You might want to give confirmation that the podcast they are claiming
// is the correct podcast.
// Grab the podcast details in an array from your local systems.
// Check that the podcast is owned by this person, of course.
$consumer = $params["consumer"];
$return_path = $params["return_path"];
$podcast = lookup_from_guid($params['guid']);
// If podcast is not found on this user we redirect with an error message:
if(!$podcast) {
header(
"Location: ".
$consumer.
$return_path.
"?token=".
build_jwt_token(
[
"accepted" => false,
"error" => "Podcast could not be found for this user"
],
$HOSTING_PRIVATE_KEY
)
);
exit();
}
$guid = $podcast["guid"];
// If we already shown the page and got user response
//
// checking csrf depends on your context, but is a strong recommendation for security
if(check_csrf()) exit("Bad request");
$action = $params["action"];
if($action == "accept") {
header(
"Location: ".
$consumer.
$return_path.
"?token=".
build_jwt_token(
[
"accepted" => true
],
$HOSTING_PRIVATE_KEY
)
);
exit();
} else if ($action == "back") {
header(
"Location: ".
$consumer.
$return_path.
"?token=".
build_jwt_token(
[
"accepted" => false,
"error" => "back"
],
$HOSTING_PRIVATE_KEY
)
);
exit();
}
$service = opengraph($consumer);
$claim_fields = <<<TAGS
<input type="hidden" name="guid" value="$guid" />
<input type="hidden" name="consumer" value="$consumer" />
<input type="hidden" name="return_path" value="$return_path />
TAGS;
?>
<h1>Claiming your podcast</h1>
<img src="<?= $service["icon"] ?>">
<h3>
<?= $service["name"] ?>
<small>(<?= parse_url($consumer,PHP_URL_HOST) ?>)</small>
</h3>
<p>This service wants us to confirm you have control over this podcast : <?= $podcast["name"]; ?></p>
<form method="post" action="/quick_claim">
<?= $claim_fields ?>
<?= put_csrf_protection() ?>
<input type="hidden" name="action" value="accept" />
<input type="submit" value="I do">
</form>
<form method="post" action="/quick_claim">
<?= $claim_fields ?>
<?= some_csrf_protection ?>
<input type="hidden" name="action" value="back" />
<input type="submit" value="cancel this request">
</form>
```
The above will successfully check that your user is authenticated and send back the JWT token to the directory service
if the user agreed.
<br><br>
## Full Workflow Example
This section will summarize everything into one big example. Here are the defined terms we will use in this example:
- **DIRECTORY**: a podcast directory platform
- **HOST**: a podcast hosting service
- **PODCAST** is a show with an RSS feed
- **CREATOR** is an owner of the **PODCAST** who is allowed to claim it
<br>
**HOST** adds these elements to the RSS feed:
```xml
<podcast:guid>ead4c236-bf58-58c6-a2c6-a6b28d128cb6</podcast:guid>
<podcast:verify
auth="https://host.com/studio/quick_claim/"
pub="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg=="
/>
```
**DIRECTORY** presents a button labeled "Quick Claim" on the page of every **PODCAST**.
When **CREATOR** clicks on the "Quick Claim" button on the page belonging to their **PODCAST**, they are redirected
to their **HOST** with this URL:
```http
https://host.com/studio/quick_claim/?guid=ead4c236-bf58-58c6-a2c6-a6b28d128cb6&consumer=https://directory.com/quick_claiming/ead4c236-bf58-58c6-a2c6-a6b28d128cb6&return_path=/return
```
**HOST** can use the `consumer` parameter to fetch some data (name, icon, description) through open graph. In this
example **DIRECTORY** uses the `consumer` URL, including podcast guid, and could use it to add some podcast info into the
open graph description.
**HOST** asks for **CREATOR** confirmation in a logged-in only area after verifying, on their own responsibility, if
this user account indeed has access to this podcast.
When **CREATOR** agrees to the claim request, the **HOST** will generate a JWT token, signed by their private key,
authenticating the **CREATOR** decision.
#### Unsigned JWT:
```json
{
"guid": "ead4c236-bf58-58c6-a2c6-a6b28d128cb6",
"accepted": true
}
```
#### Signed JWT:
```base64
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJndWlkIjoiZWFkNGMyMzYtYmY1OC01OGM2LWEyYzYtYTZiMjhkMTI4Y2I2IiwiYWNjZXB0ZWQiOnRydWV9.eOXYFi9uUSUAKWcI8GdJ15RIhjoCvR0l9TUCPsqhsTYqaGFTwbH6zXzYqIqhxmtSotvL8ZLumP64LRFBjHX5Mw
```
Decode/Encode online with : https://jwt.io/
#### Key Details
- Private Key
```
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2
OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r
1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G
-----END PRIVATE KEY-----
```
- Public Key
```
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9
q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==
-----END PUBLIC KEY-----
```
<br><br>
**CREATOR** are redirected to the **DIRECTORY** with this URL:
```http
https://directory.com/quick_claiming/ead4c236-bf58-58c6-a2c6-a6b28d128cb6/return?token=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJndWlkIjoiZWFkNGMyMzYtYmY1OC01OGM2LWEyYzYtYTZiMjhkMTI4Y2I2IiwiYWNjZXB0ZWQiOnRydWV9.eOXYFi9uUSUAKWcI8GdJ15RIhjoCvR0l9TUCPsqhsTYqaGFTwbH6zXzYqIqhxmtSotvL8ZLumP64LRFBjHX5Mw
```
**DIRECTORY** can now check the token parameter to ensure it has been correctly signed with the private key
corresponding to the public key seen in the RSS feed, and in this case, the claiming request has been accepted.
Other responses could be, for example:
```http
https://directory.com/quick_claiming/ead4c236-bf58-58c6-a2c6-a6b28d128cb6/return?token=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJndWlkIjoiZWFkNGMyMzYtYmY1OC01OGM2LWEyYzYtYTZiMjhkMTI4Y2I2IiwiYWNjZXB0ZWQiOmZhbHNlLCJlcnJvciI6IlVzZXIgY2FuJ3QgYWNjZXNzIHRvIHRoaXMgc2hvdyJ9.MDkZanxlukjQRAj5zd2GoWetAwMWPZs1RU24HdSw8LJm3Z73kL2U4gHMOJUg62LtZdIoH3tktSR0w-1Ltuo4Ig
```
```json
{
"guid": "ead4c236-bf58-58c6-a2c6-a6b28d128cb6",
"accepted": false,
"error": "User can't access to this show"
}
```
```http
https://directory.com/quick_claiming/ead4c236-bf58-58c6-a2c6-a6b28d128cb6/return?token=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJndWlkIjoiZWFkNGMyMzYtYmY1OC01OGM2LWEyYzYtYTZiMjhkMTI4Y2I2IiwiYWNjZXB0ZWQiOmZhbHNlLCJlcnJvciI6ImJhY2sifQ.TP8h8Hwh7oRpcuTPXOeqrO46sNwlwC4RLdyMtdFqZQfsS0pUT71_ljoUWq3a0o_hUjuVvPoWnDXar7o2BbLw6w
```
...or...
```json
{
"guid": "ead4c236-bf58-58c6-a2c6-a6b28d128cb6",
"accepted": false,
"error": "back"
}
```
<br><br>
## Final Thoughts
Here are my thoughts on this idea and how to implement it, feel free to make any remarks about it.
JWT seems to me to be the middle ground between complexity and simplicity for a decentralized authorization system.
It only requires the hosting provider to add a private key somewhere and print the pub key on the feed.
Signing/Verifying JWT is quick and easy and there are libraries in almost any languages. It basically falls down
to something like `[verify|sign]JWT(content, priv/pub_key)` in most languages.
Open Graph is a nice bonus "out of the box" for permission asker identity.
We could also make this a kind of API spec, and add more data to the token.
Maybe mimick oauth and add a scope param asking specifically for some data, permissions about the
feed. (`scope="stats,edit,delete,admin"` etc)
That's a big subject and a spec of its own. That could be a nice addition, but it has to be done in a way we can trust
the system and all information it conveys.