├── .npmignore
├── .git-blame-ignore-revs
├── .gitignore
├── client
├── plotly.d.ts
├── tsconfig.json
├── common-client-plugin.ts
├── video-edit-client-plugin.ts
└── video-watch-client-plugin.ts
├── assets
└── style.css
├── server
├── tsconfig.json
└── main.ts
├── languages
├── de.json
└── ru.json
├── public
└── images
│ ├── webmon_icon.svg
│ ├── wm-icon.svg
│ └── wm-icon-grey.svg
├── webpack.config.js
├── LICENSE-MIT
├── shared
├── api.ts
├── common.ts
└── paid.ts
├── CHANGELOG.md
├── README.md
├── package.json
└── LICENSE-APACHE
/.npmignore:
--------------------------------------------------------------------------------
1 | # Emacs files
2 | *~
3 | \#*#
4 |
--------------------------------------------------------------------------------
/.git-blame-ignore-revs:
--------------------------------------------------------------------------------
1 | # format
2 | 444e2a09068dc0d1696cceb2df83576a63c4e7a2
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .cache
3 | dist
4 |
5 | # Emacs backup
6 | *~
7 | # Emacs autosave
8 | \#*#
--------------------------------------------------------------------------------
/client/plotly.d.ts:
--------------------------------------------------------------------------------
1 | declare module "plotly.js-finance-dist-min" {
2 | export * from 'plotly.js'
3 | }
4 |
--------------------------------------------------------------------------------
/assets/style.css:
--------------------------------------------------------------------------------
1 | .video-miniature-name {
2 | /* price labels may wrap to second line
3 | 3em is good for 2 lines, 4em for 3 lines
4 | */
5 | max-height: 4em !important
6 | }
7 |
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/node12/tsconfig.json",
3 | "compilerOptions": {
4 | "lib": ["dom"],
5 | "outDir": "../dist/",
6 | "target": "es6" // same as PeerTube
7 | },
8 | "include": [
9 | "./**/*",
10 | "../shared/**/*"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/languages/de.json:
--------------------------------------------------------------------------------
1 | {
2 | "Web Monetization payment pointer": "Web Monetization Payment Pointer",
3 | "Interledger payment pointer for Web Monetization. In the form of $example.org/account.": "Interledger Payment Pointer für Web Monetization. In Form von $example.org/account.",
4 | "Invalid payment pointer format.": "Ungültiges Payment Pointer-Format."
5 | }
--------------------------------------------------------------------------------
/languages/ru.json:
--------------------------------------------------------------------------------
1 | {
2 | "Web Monetization payment pointer": "Указатель платежей Web Monetization",
3 | "Interledger payment pointer for Web Monetization. In the form of $example.org/account.": "Значение указателя платежей для Web Monetization. В формате $example.org/account.",
4 | "Invalid payment pointer format.": "Неверный формат указателя платежа."
5 | }
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/node12/tsconfig.json",
3 | "compilerOptions": {
4 | "lib": ["dom"],
5 | "module": "es6",
6 | "esModuleInterop": true,
7 | "allowSyntheticDefaultImports": true,
8 | "outDir": "../dist/",
9 | "moduleResolution": "node",
10 | "target": "es6", // same as PeerTube
11 | "paths": {
12 | "shared/*": ["../shared/*"]
13 | }
14 | },
15 | "include": [
16 | "./**/*",
17 | "../shared/**/*"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/public/images/webmon_icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/wm-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/wm-icon-grey.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | const EsmWebpackPlugin = require('@purtuga/esm-webpack-plugin')
4 |
5 | const clientFiles = [
6 | 'video-edit-client-plugin',
7 | 'video-watch-client-plugin',
8 | 'common-client-plugin'
9 | ]
10 |
11 | const config = clientFiles.map(f => ({
12 | // mode: 'production',
13 | devtool: process.env.NODE_ENV === 'dev' ? 'eval-source-map' : false,
14 | entry: './client/' + f + '.ts',
15 | output: {
16 | path: path.resolve(__dirname, './dist/client'),
17 | filename: './' + f + '.js',
18 | library: 'script',
19 | libraryTarget: 'var'
20 | },
21 | plugins: [new EsmWebpackPlugin()],
22 | module: {
23 | rules: [
24 | {
25 | test: /\.ts$/,
26 | use: 'ts-loader'
27 | }
28 | ]
29 | },
30 | resolve: {
31 | alias: {
32 | shared: path.resolve(__dirname, 'shared/')
33 | },
34 | extensions: ['.ts']
35 | }
36 | }))
37 |
38 | module.exports = config
39 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | Permission is hereby granted, free of charge, to any
2 | person obtaining a copy of this software and associated
3 | documentation files (the "Software"), to deal in the
4 | Software without restriction, including without
5 | limitation the rights to use, copy, modify, merge,
6 | publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software
8 | is furnished to do so, subject to the following
9 | conditions:
10 |
11 | The above copyright notice and this permission notice
12 | shall be included in all copies or substantial portions
13 | of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23 | DEALINGS IN THE SOFTWARE.
24 |
--------------------------------------------------------------------------------
/shared/api.ts:
--------------------------------------------------------------------------------
1 | import { SerializedReceipts, SerializedHistogramBinUncommitted, SerializedChanges, SerializedAmount } from '../shared/paid'
2 |
3 | export type Histogram = {
4 | parts: number[],
5 | history: Record
6 | }
7 |
8 | export type HistogramUpdate = {
9 | receipts: any,
10 | histogram: any,
11 | subscribed: boolean,
12 | }
13 |
14 | export type StatsHistogramGet = {
15 | histogram: Histogram,
16 | }
17 |
18 | export type StatsHistogramUpdatePost = {
19 | receipts: SerializedReceipts,
20 | histogram: SerializedHistogramBinUncommitted[],
21 | subscribed: boolean,
22 | }
23 |
24 | export type StatsViewPost = {
25 | receipts: SerializedReceipts,
26 | changes: SerializedChanges,
27 | subscribed: boolean,
28 | }
29 |
30 | export type MonetizationStatusBulkPost = {
31 | videos: string[],
32 | }
33 |
34 | export type MonetizationStatusBulkStatus = {
35 | monetization: 'unmonetized' | 'monetized' | 'ad-skip' | 'pay-wall' | 'unknown',
36 | currency?: string,
37 | viewCost?: number,
38 | duration?: number,
39 | paid?: SerializedAmount | null,
40 | }
41 | export type MonetizationStatusBulkPostRes = {
42 | statuses: Record,
43 | }
44 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 1.0.1 --- Patch
2 |
3 | - Display per-channel contributions pie chart
4 | - Histograms initialized when not logged in are done properly now
5 | - Video currency defaults to USD if not set
6 |
7 | # 1.0.0 --- Stable release
8 |
9 | - Add receipt verification to check payments server-side
10 | - Add stats tracking and viewing: payments made during each 15 seconds of the video are shown as a
11 | histogram, revenue per day is tracked by subscriber/non-subscriber. User payments to channels
12 | is tracked, but not currently shown as we cannot retrieve the channel name.
13 | - Currency conversion: Costs now specified in any currency, and stats are shown in any currency.
14 | It is assumed that all payments are converted to the currency specified for the video upon receipt for
15 | purposes of stats tracking.
16 | - Opt-out for stats tracking (still records payments made to prevent double payments, although maybe
17 | this could also be disabled with a more fine-grained option)
18 | - Monetization status is shown when video title/thumbnail is visible. Basic icon for monetized,
19 | icon with circle for ad skips, and icon with circle followed by cost for mandatory payment.
20 |
21 |
22 | # 0.0.2 --- Initial release
23 |
24 | - Add payment pointer field to videos
25 | - Use payment pointer field to direction Web Monetization payments during playback
26 |
--------------------------------------------------------------------------------
/shared/common.ts:
--------------------------------------------------------------------------------
1 | const version = '1.0.6'
2 |
3 | export interface StoreKey {
4 | k: string
5 | }
6 | export interface StoreObjectKey {
7 | k: string,
8 | validate: (x: object) => T | null
9 | }
10 |
11 | export function paymentPointerStore(videoId: string): StoreKey {
12 | return { k: paymentPointerField + '_v-' + videoId }
13 | }
14 | export function receiptServiceStore(videoId: string): StoreKey {
15 | return { k: receiptServiceField + '_v-' + videoId }
16 | }
17 | export function currencyStore(videoId: string): StoreKey {
18 | return { k: currencyField + '_v-' + videoId }
19 | }
20 | export function viewCostStore(videoId: string): StoreKey {
21 | return { k: viewCostField + '_v-' + videoId }
22 | }
23 | export function adSkipCostStore(videoId: string): StoreKey {
24 | return { k: adSkipCostField + '_v-' + videoId }
25 | }
26 |
27 | const paymentPointerField = 'web-monetization-payment-pointer'
28 | const receiptServiceField = 'web-monetization-receipt-service'
29 | const currencyField = 'web-monetization-currency'
30 | const viewCostField = 'web-monetization-view-cost'
31 | const adSkipCostField = 'web-monetization-ad-skip-cost'
32 |
33 | function hms (duration: number | null) {
34 | if (duration == null || isNaN(duration)) {
35 | return '' + duration
36 | }
37 | var s = duration % 60
38 | const m = Math.round((duration - s) / 60 % 60)
39 | const h = Math.round((duration - 60 * m - s) / 3600)
40 | // Only round if it's too long to avoid floating point precision issues
41 | if (6 < ('' + s).length) {
42 | s = Math.round(s * 1000) / 1000
43 | }
44 |
45 | if (h != 0) {
46 | return h + 'h' + m + 'm' + s + 's'
47 | }
48 | if (m != 0) {
49 | return m + 'm' + s + 's'
50 | }
51 | return s + 's'
52 | }
53 |
54 | export {
55 | version,
56 | paymentPointerField,
57 | receiptServiceField,
58 | currencyField,
59 | viewCostField,
60 | adSkipCostField,
61 | hms}
62 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PeerTube Web Monetization Plugin
2 |
3 | Web Monetization makes it easy for viewers to support creators through anonymous micropayments.
4 |
5 | Viewers can sign up for a service such as [Coil](https://coil.com/) and either install their extension or use their Puma Browser (Coil is currently the only provider). Then, when viewing a supported video, payments will be made while the video is playing.
6 |
7 | Creators can monetize their content by using a PeerTube instance with the Web Monetization plugin installed, and adding their Interledger payment pointer.
8 | A payment pointer provides a way for funds to be deposited, and a supported wallet can be created using [GateHub](https://gatehub.net/) or [Uphold](https://uphold.com). The payment pointer is added under the "Plugin Settings" tab in the video editing interface. You can also set a minimum pay rate to view.
9 |
10 | 
11 |
12 | The monetization status of videos will then show when viewing their thumbnails.
13 |
14 | 
15 |
16 | Creators can specify the location of sponsors segments using the [PeerTube chapters plugin](https://milesdewitt.com/peertube-chapters), and those who pay with Web Monetization will automatically skip those sponsor segments. You can also set a minimum pay rate for ad-skipping as seen above.
17 |
18 | 
19 |
20 | Segments of the video which have already been paid for are remembered and will not receive double-payment. Users can store this data when logged-in to not pay multiple times. General contribution data is made available in the stats tray below the video. However, no data sharing is necessary.
21 |
22 | 
23 |
24 | ## Contributing
25 |
26 | Code is run through `npx standard-format `. Some of the changes it makes are wrong, but at least it's consistent.
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "peertube-plugin-web-monetization",
3 | "description": "Web Monetization PeerTube plugin",
4 | "license": "(MIT OR Apache-2.0)",
5 | "version": "1.0.6",
6 | "author": "Miles DeWitt",
7 | "homepage": "https://milesdewitt.com/peertube-web-monetization",
8 | "repository": "https://github.com/samlich/peertube-plugin-web-monetization",
9 | "bugs": "https://github.com/samlich/peertube-plugin-web-monetization",
10 | "engine": {
11 | "peertube": ">=3.2.0"
12 | },
13 | "clientScripts": [
14 | {
15 | "script": "dist/client/video-watch-client-plugin.js",
16 | "scopes": [
17 | "video-watch",
18 | "embed"
19 | ]
20 | },
21 | {
22 | "script": "dist/client/video-edit-client-plugin.js",
23 | "scopes": [
24 | "video-edit"
25 | ]
26 | },
27 | {
28 | "script": "dist/client/common-client-plugin.js",
29 | "scopes": [
30 | "common"
31 | ]
32 | }
33 | ],
34 | "css": [
35 | "assets/style.css"
36 | ],
37 | "dependencies": {
38 | "interval-promise": "^1.4",
39 | "plotly.js-finance-dist-min": "^2.12",
40 | "short-uuid": "^4.2"
41 | },
42 | "devDependencies": {
43 | "@peertube/peertube-types": "^4.2",
44 | "@types/express": "4.17",
45 | "@types/plotly.js": "^1.54",
46 | "video.js": "^7.19",
47 | "@types/video.js": "^7.3",
48 | "@webmonetization/types": "^0.0.0",
49 | "@tsconfig/node12": "^1.0",
50 | "@purtuga/esm-webpack-plugin": "^1.0",
51 | "transform-loader": "^0.2.4",
52 | "ify-loader": "^1.1",
53 | "typescript": "^4.3",
54 | "ts-loader": "^8.3",
55 | "webpack": "^4.0",
56 | "webpack-cli": "^4.0",
57 | "standard": "^14.0",
58 | "npm-run-all": "^4.1"
59 | },
60 | "keywords": [
61 | "peertube",
62 | "plugin"
63 | ],
64 | "library": "./dist/server/main.js",
65 | "scripts": {
66 | "clean:light": "rm -rf dist/*",
67 | "prepare": "npm run build",
68 | "build:webpack": "webpack --mode production",
69 | "build:server": "npx -p typescript tsc --build server/tsconfig.json",
70 | "build": "npm-run-all -s clean:light -p build:webpack build:server",
71 | "test": "standard"
72 | },
73 | "staticDirs": {
74 | "images": "public/images"
75 | },
76 | "translations": {
77 | "de-DE": "./languages/de.json"
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/client/common-client-plugin.ts:
--------------------------------------------------------------------------------
1 | import { RegisterClientHelpers, RegisterClientOptions } from '@peertube/peertube-types/client'
2 | import interval from 'interval-promise'
3 | import { Amount, Exchange, quoteCurrencies } from 'shared/paid'
4 | import { MonetizationStatusBulkPost, MonetizationStatusBulkPostRes, MonetizationStatusBulkStatus } from 'shared/api'
5 |
6 | var ptHelpers: RegisterClientHelpers | null = null
7 | var exchange = new Exchange()
8 | var displayCurrency = quoteCurrencies['usd']
9 |
10 | export function register ({ peertubeHelpers }: RegisterClientOptions) {
11 | ptHelpers = peertubeHelpers
12 | interval(async () => await populateBadges(), 2500, { stopOnError: false })
13 | }
14 |
15 | var monetizationStatus: Record = {}
16 |
17 | async function populateBadges (recurse: boolean = false): Promise {
18 | if (ptHelpers == null) {
19 | console.error("`populateBadges` without `peertubeHelpers`")
20 | return
21 | }
22 |
23 | const names = document.getElementsByClassName('video-miniature-name')
24 |
25 | var fetchMonetizationStatus: string[] = []
26 |
27 | for (var i = 0; i < names.length; i++) {
28 | if (names[i].classList.contains('web-monetization-badge-checked')) {
29 | continue
30 | }
31 | // Price labels may wrap to second line
32 | // names[i].setAttribute('style', 'maxHeight:3emw')
33 | var link
34 | // older versions use ``, newer versions user `` with an `` child
35 | if (names[i].tagName.toLowerCase() == 'a') {
36 | link = names[i]
37 | } else {
38 | const children = names[i].getElementsByTagName('a')
39 | if (children.length != 0) {
40 | link = children[0]
41 | } else {
42 | continue
43 | }
44 | }
45 |
46 | const dest = link.getAttribute('href')
47 | if (dest == null) {
48 | continue
49 | }
50 | const videoUuid = dest.substring(dest.lastIndexOf('/') + 1)
51 | const status = monetizationStatus[videoUuid]
52 | if (status == null) {
53 | fetchMonetizationStatus.push(videoUuid)
54 | continue
55 | }
56 |
57 | if (status.monetization == 'monetized' || status.monetization == 'ad-skip' || status.monetization == 'pay-wall') {
58 | var badge = document.createElement('img')
59 | badge.setAttribute('style', 'padding-left:0.5em;height:1.5em;')
60 | if (status.monetization == 'monetized') {
61 | badge.src = ptHelpers.getBaseStaticRoute() + '/images/wm-icon.svg'
62 | badge.title = 'Monetized'
63 | }
64 | if (status.monetization == 'ad-skip') {
65 | badge.src = ptHelpers.getBaseStaticRoute() + '/images/webmon_icon.svg'
66 | badge.title = 'Monetized (ad-skip)'
67 | }
68 | if (status.monetization == 'pay-wall') {
69 | badge.src = ptHelpers.getBaseStaticRoute() + '/images/webmon_icon.svg'
70 | badge.title = 'Pay-wall'
71 | }
72 | link.append(badge)
73 | }
74 |
75 | if (status.monetization == 'pay-wall') {
76 | var costTag = document.createElement('span')
77 | costTag.setAttribute('style', 'padding-left:0.5em;height:1.5em;font-size:0.95em;')
78 |
79 | if (status.viewCost != null && status.duration != null && status.currency != null ) {
80 | var costAmount = new Amount(true)
81 | // 600 to convert from per 10 min to per second
82 | var significand = status.viewCost / 600 * status.duration
83 | var exponent = 0
84 | while (significand * 0.001 < Math.abs(significand - (significand >> 0))) {
85 | significand *= 10
86 | exponent -= 1
87 | }
88 | significand >>= 0
89 | costAmount.depositUnchecked(significand, exponent, status.currency, true, null)
90 |
91 | var paidConverted = null
92 | if (status.paid != null) {
93 | try {
94 | const paid = Amount.deserialize(status.paid)
95 | paidConverted = await paid.inCurrency(exchange, quoteCurrencies[status.currency.toLowerCase()])
96 | } catch (e) {
97 | console.error(e)
98 | }
99 | }
100 | costTag.innerText = ''
101 | if (paidConverted != null && !paidConverted.isEmpty()) {
102 | costTag.innerText = paidConverted.display() + '/'
103 | }
104 | costTag.innerText += costAmount.display()
105 |
106 | if (displayCurrency.code.toLowerCase() != status.currency.toLowerCase()) {
107 | var exchanged = null
108 | try {
109 | exchanged = await costAmount.inCurrency(exchange, displayCurrency)
110 | } catch (e) {
111 | console.error(e)
112 | }
113 | if (exchanged != null) {
114 | costTag.innerText += ' (' + exchanged.display() + ')'
115 | }
116 | }
117 | }
118 |
119 | link.append(costTag)
120 | }
121 | names[i].classList.add('web-monetization-badge-checked')
122 | }
123 |
124 | if (0 < fetchMonetizationStatus.length) {
125 | var route = ptHelpers.getBaseStaticRoute()
126 | route = route.slice(0, route.lastIndexOf('/') + 1) + 'router/monetization_status_bulk'
127 |
128 | var headers: Record = {}
129 | // needed for checking if video is already paid for
130 | var tryHeaders = ptHelpers.getAuthHeader()
131 | if (tryHeaders != null) {
132 | headers = tryHeaders
133 | }
134 | headers['content-type'] = 'application/json; charset=utf-8'
135 |
136 | fetch(route, {
137 | method: 'POST',
138 | headers,
139 | body: JSON.stringify({ videos: fetchMonetizationStatus })
140 | }).then(res => res.json())
141 | .then((data: MonetizationStatusBulkPostRes) => {
142 | for (const key in data.statuses) {
143 | monetizationStatus[key] = data.statuses[key]
144 | }
145 | if (!recurse) {
146 | populateBadges(true)
147 | }
148 | })
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/client/video-edit-client-plugin.ts:
--------------------------------------------------------------------------------
1 | import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@peertube/peertube-types'
2 | import type { RegisterClientOptions } from '@peertube/peertube-types/client'
3 | import { adSkipCostField, currencyField, paymentPointerField, receiptServiceField, viewCostField } from '../shared/common'
4 | import { quoteCurrencies } from '../shared/paid'
5 |
6 | var invalidPaymentPointerFormatMsg = 'Invalid payment pointer format.'
7 |
8 | export async function register ({ registerVideoField, peertubeHelpers }: RegisterClientOptions): Promise {
9 | // Payment pointer
10 | {
11 | const commonOptions: RegisterClientFormFieldOptions = {
12 | name: paymentPointerField,
13 | label: await peertubeHelpers.translate('Web Monetization payment pointer'),
14 | type: 'input',
15 | descriptionHTML: await peertubeHelpers.translate(
16 | 'Interledger payment pointer for Web Monetization. In the form of $example.org/account.'
17 | ),
18 | default: ''
19 | }
20 | const types: ('upload' | 'import-url' | 'import-torrent' | 'update' | 'go-live')[] = ['upload', 'import-url', 'import-torrent', 'update']
21 | for (const type of types) {
22 | const videoFormOptions: RegisterClientVideoFieldOptions = { type}
23 | registerVideoField(commonOptions, videoFormOptions)
24 | }
25 | invalidPaymentPointerFormatMsg = await peertubeHelpers.translate(invalidPaymentPointerFormatMsg)
26 | finishAddPaymentPointerField()
27 | }
28 |
29 | // Receipt service
30 | {
31 | const commonOptions: RegisterClientFormFieldOptions = {
32 | name: receiptServiceField,
33 | label: await peertubeHelpers.translate('Add receipt service to payment pointer (to verify payments)'),
34 | type: 'input-checkbox',
35 | descriptionHTML: '',
36 | default: true
37 | }
38 | const types: ('upload' | 'import-url' | 'import-torrent' | 'update' | 'go-live')[] = ['upload', 'import-url', 'import-torrent', 'update']
39 | for (const type of types) {
40 | const videoFormOptions: RegisterClientVideoFieldOptions = { type}
41 | registerVideoField(commonOptions, videoFormOptions)
42 | }
43 | }
44 |
45 | // Currency
46 | {
47 | var currencies = []
48 | const commonCurrencies = ['usd', 'eur', 'xrp']
49 | for (var i = 0; i < commonCurrencies.length; i++) {
50 | const currency = quoteCurrencies[commonCurrencies[i]]
51 | currencies.push({
52 | label: currency.network,
53 | value: currency.code
54 | })
55 | }
56 | currencies.push({
57 | label: '================',
58 | value: 'USD'
59 | })
60 | var codes = Object.keys(quoteCurrencies)
61 | for (var i = 0; i < codes.length; i++) {
62 | const currency = quoteCurrencies[codes[i]]
63 | currencies.push({
64 | label: currency.network,
65 | value: currency.code
66 | })
67 | }
68 |
69 | const commonOptions: RegisterClientFormFieldOptions = {
70 | name: currencyField,
71 | label: await peertubeHelpers.translate('Currency which costs are quoted in'),
72 | type: 'select',
73 | options: currencies,
74 | descriptionHTML: '',
75 | default: 'USD'
76 | }
77 | const types: ('upload' | 'import-url' | 'import-torrent' | 'update' | 'go-live')[] = ['upload', 'import-url', 'import-torrent', 'update']
78 | for (const type of types) {
79 | const videoFormOptions: RegisterClientVideoFieldOptions = { type}
80 | registerVideoField(commonOptions, videoFormOptions)
81 | }
82 | }
83 |
84 | // View cost
85 | {
86 | const commonOptions: RegisterClientFormFieldOptions = {
87 | name: viewCostField,
88 | label: await peertubeHelpers.translate('Minimum payment rate to view per 10 minutes'),
89 | type: 'input',
90 | descriptionHTML: await peertubeHelpers.translate(''),
91 | default: '0'
92 | }
93 | const types: ('upload' | 'import-url' | 'import-torrent' | 'update' | 'go-live')[] = ['upload', 'import-url', 'import-torrent', 'update']
94 | for (const type of types) {
95 | const videoFormOptions: RegisterClientVideoFieldOptions = { type}
96 | registerVideoField(commonOptions, videoFormOptions)
97 | }
98 | }
99 |
100 | // Ad skip cost
101 | {
102 | const commonOptions: RegisterClientFormFieldOptions = {
103 | name: adSkipCostField,
104 | label: await peertubeHelpers.translate('Minimum payment rate to skip ads per 10 minutes'),
105 | type: 'input',
106 | descriptionHTML: await peertubeHelpers.translate('Payment rates at or above this level will skip chapters with the "Sponsor" tag, labelled using the chapters plugin.'),
107 | default: '0'
108 | }
109 | const types: ('upload' | 'import-url' | 'import-torrent' | 'update' | 'go-live')[] = ['upload', 'import-url', 'import-torrent', 'update']
110 | for (const type of types) {
111 | const videoFormOptions: RegisterClientVideoFieldOptions = { type}
112 | registerVideoField(commonOptions, videoFormOptions)
113 | }
114 | }
115 | }
116 |
117 | function finishAddPaymentPointerField () {
118 | var paymentPointerElement = document.getElementById(paymentPointerField)
119 | // The element is not added until the user switches to the "Plugin settings" tab
120 | if (paymentPointerElement == null) {
121 | setTimeout(() => {
122 | finishAddPaymentPointerField()
123 | }, 3000)
124 | return
125 | }
126 |
127 | var paymentPointerValid = true
128 |
129 | function update () {
130 | if (paymentPointerElement == null) {
131 | throw 'typescript unreachable'
132 | }
133 |
134 | if (paymentPointerElement.getAttribute('value') == null ||
135 | paymentPointerElement.getAttribute('value') === '' ||
136 | validatePaymentPointer(paymentPointerElement.getAttribute('value'))) {
137 | if (!paymentPointerValid) {
138 | paymentPointerValid = true
139 |
140 | paymentPointerElement.classList.remove('ng-invalid')
141 | paymentPointerElement.classList.add('ng-valid')
142 |
143 | var errorElRemove = document.getElementById(paymentPointerField + '-error')
144 | if (errorElRemove != null) {
145 | errorElRemove.parentNode!.removeChild(errorElRemove)
146 | }
147 | }
148 | } else {
149 | if (paymentPointerValid) {
150 | paymentPointerValid = false
151 |
152 | paymentPointerElement.classList.remove('ng-valid')
153 | paymentPointerElement.classList.add('ng-invalid')
154 |
155 | var errorEl = document.createElement('div')
156 | errorEl.id = paymentPointerField + '-error'
157 | errorEl.classList.add('form-error')
158 | errorEl.innerText = invalidPaymentPointerFormatMsg
159 | paymentPointerElement.parentNode!.appendChild(errorEl)
160 | }
161 | }
162 | }
163 |
164 | paymentPointerElement.addEventListener('input', () => {
165 | update()
166 | })
167 | update()
168 | }
169 |
170 | function validatePaymentPointer (value: string | null): boolean {
171 | if (value == null) {
172 | return false
173 | }
174 | if (!value.startsWith('$')) {
175 | return false
176 | }
177 |
178 | const unparsed = 'https://' + value.substring(1)
179 | const parsed = new URL(unparsed)
180 |
181 | return parsed.host != null &&
182 | parsed.username == null &&
183 | parsed.password == null &&
184 | parsed.search == null &&
185 | parsed.hash == null
186 | }
187 |
--------------------------------------------------------------------------------
/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/server/main.ts:
--------------------------------------------------------------------------------
1 | import type { Request, Response } from 'express'
2 | import type { RegisterServerOptions, MVideoFullLight, MVideoThumbnail, PluginStorageManager } from '@peertube/peertube-types'
3 | import short from 'short-uuid'
4 | import { paymentPointerField, paymentPointerStore, receiptServiceField, receiptServiceStore, currencyField, currencyStore, viewCostField, viewCostStore, adSkipCostField, adSkipCostStore, StoreKey, StoreObjectKey } from '../shared/common.js'
5 | import { VideoPaidStorage, Amount, Receipts, Exchange, quoteCurrencies, SerializedState, SerializedHistogramBinUncommitted, SerializedVideoPaid } from '../shared/paid.js'
6 | import { Histogram, StatsHistogramGet, StatsViewPost, StatsHistogramUpdatePost, MonetizationStatusBulkPost, MonetizationStatusBulkPostRes, MonetizationStatusBulkStatus } from '../shared/api'
7 |
8 | const shortUuidTranslator = short()
9 |
10 | var exchange = new Exchange()
11 |
12 | class StorageManagerTypeFix {
13 | storageManager: PluginStorageManager
14 |
15 | constructor (storageManager: PluginStorageManager) {
16 | this.storageManager = storageManager
17 | }
18 |
19 | // PeerTube lies and says it will always return a string, when it actually
20 | // returns undefined when no value exists, and returns an object, number, string, boolean, or null
21 | // if it's able to parse as json
22 | async getDataUnknown (key: string): Promise