From c1c6113a1ae596388fbe2f4605c86074835617cd Mon Sep 17 00:00:00 2001 From: Mitch Downey Date: Sat, 22 Jul 2023 00:02:37 -0500 Subject: [PATCH 01/11] Add UPDevice handling --- migrations/0047_upDevices.sql | 15 ++ src/app.ts | 4 + src/controllers/upDevice.ts | 160 +++++++++++++++++++++ src/entities/index.ts | 1 + src/entities/upDevice.ts | 30 ++++ src/entities/user.ts | 4 + src/lib/db.ts | 2 + src/middleware/queryValidation/upDevice.ts | 42 ++++++ src/routes/index.ts | 1 + src/routes/upDevice.ts | 108 ++++++++++++++ src/seeds/qa/populateDatabase.ts | 2 + 11 files changed, 369 insertions(+) create mode 100644 migrations/0047_upDevices.sql create mode 100644 src/controllers/upDevice.ts create mode 100644 src/entities/upDevice.ts create mode 100644 src/middleware/queryValidation/upDevice.ts create mode 100644 src/routes/upDevice.ts diff --git a/migrations/0047_upDevices.sql b/migrations/0047_upDevices.sql new file mode 100644 index 00000000..ddf76180 --- /dev/null +++ b/migrations/0047_upDevices.sql @@ -0,0 +1,15 @@ +-- Create upDevices table + +CREATE TABLE public."upDevices" ( + "upEndpoint" character varying, + "upPublicKey" character varying NOT NULL, + "upAuthKey" character varying NOT NULL, + "userId" character varying(14) references users(id) ON DELETE CASCADE, + "createdAt" timestamp without time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp without time zone DEFAULT now() NOT NULL, + PRIMARY KEY ("upEndpoint") +); + +ALTER TABLE public."upDevices" OWNER TO postgres; + +CREATE INDEX "IDX_upDevices_userId" ON public."upDevices" USING btree ("userId"); diff --git a/src/app.ts b/src/app.ts index 4a00cc03..a880e96a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -30,6 +30,7 @@ import { podcastRouter, podcastIndexRouter, podpingRouter, + upDeviceRouter, userHistoryItemRouter, userNowPlayingItemRouter, userQueueItemRouter, @@ -198,6 +199,9 @@ export const createApp = async (conn: Connection) => { app.use(toolsRouter.routes()) app.use(toolsRouter.allowedMethods()) + app.use(upDeviceRouter.routes()) + app.use(upDeviceRouter.allowedMethods()) + app.use(userHistoryItemRouter.routes()) app.use(userHistoryItemRouter.allowedMethods()) diff --git a/src/controllers/upDevice.ts b/src/controllers/upDevice.ts new file mode 100644 index 00000000..c73c20a2 --- /dev/null +++ b/src/controllers/upDevice.ts @@ -0,0 +1,160 @@ +import { getRepository } from 'typeorm' +import { UPDevice, Notification } from '~/entities' +import { getLoggedInUser } from './user' +const createError = require('http-errors') + +export const createUPDevice = async ({ + upEndpoint, + upPublicKey, + upAuthKey, + loggedInUserId +}: { + upEndpoint: string + upPublicKey: string + upAuthKey: string + loggedInUserId: string +}) => { + if (!upEndpoint) { + throw new createError.BadRequest('An upEndpoint must be provided.') + } + + if (!upPublicKey) { + throw new createError.BadRequest('An upPublicKey must be provided.') + } + + if (!upAuthKey) { + throw new createError.BadRequest('An upAuthKey must be provided.') + } + + if (!loggedInUserId) { + throw new createError.BadRequest('A user id must be provided.') + } + + const user = await getLoggedInUser(loggedInUserId) + if (!user) { + throw new createError.NotFound(`User for id ${loggedInUserId} not found.`) + } + + const newUPDevice = new UPDevice() + newUPDevice.upEndpoint = upEndpoint + newUPDevice.upPublicKey = upPublicKey + newUPDevice.upAuthKey = upAuthKey + newUPDevice.user = user + + const repository = getRepository(UPDevice) + await repository.save(newUPDevice) +} + +export const updateUPDevice = async ({ + previousUPEndpoint, + nextUPEndpoint, + upPublicKey, + upAuthKey, + loggedInUserId +}: { + previousUPEndpoint + nextUPEndpoint + upPublicKey + upAuthKey + loggedInUserId +}) => { + if (!previousUPEndpoint) { + throw new createError.BadRequest('A previous upEndpoint must be provided.') + } + + if (!nextUPEndpoint) { + throw new createError.BadRequest('A new upEndpoint must be provided.') + } + + if (!upPublicKey) { + throw new createError.BadRequest('A new upPublicKey must be provided.') + } + + if (!upAuthKey) { + throw new createError.BadRequest('A new upAuthKey must be provided.') + } + + if (!loggedInUserId) { + throw new createError.BadRequest('A user id must be provided.') + } + + const user = await getLoggedInUser(loggedInUserId) + if (!user) { + throw new createError.NotFound(`User for id ${loggedInUserId} not found.`) + } + + const existingUPDevice = await getUPDevice(previousUPEndpoint, loggedInUserId) + const repository = getRepository(UPDevice) + if (!existingUPDevice) { + const newUPDevice = new UPDevice() + newUPDevice.upEndpoint = nextUPEndpoint + newUPDevice.upPublicKey = upPublicKey + newUPDevice.upAuthKey = upAuthKey + newUPDevice.user = user + await repository.save(newUPDevice) + } else { + const newData = { upEndpoint: nextUPEndpoint, upPublicKey, upAuthKey } + await repository.update(previousUPEndpoint, newData) + } +} + +export const deleteUPDevice = async (upEndpoint: string, loggedInUserId: string) => { + if (!upEndpoint) { + throw new createError.BadRequest('An upEndpoint must be provided.') + } + + if (!loggedInUserId) { + throw new createError.BadRequest('A user id must be provided.') + } + + const user = await getLoggedInUser(loggedInUserId) + if (!user) { + throw new createError.NotFound(`User for id ${loggedInUserId} not found.`) + } + + const upDevice = await getUPDevice(upEndpoint, loggedInUserId) + + if (!upDevice) { + throw new createError.NotFound(`upDevice for upEndpoint ${upEndpoint} not found.`) + } + + const repository = getRepository(UPDevice) + await repository.remove(upDevice) +} + +const getUPDevice = async (upEndpoint: string, loggedInUserId: string) => { + if (!upEndpoint) { + throw new createError.BadRequest('An upEndpoint must be provided.') + } + + if (!loggedInUserId) { + throw new createError.BadRequest('A user id must be provided.') + } + + const repository = getRepository(UPDevice) + return repository + .createQueryBuilder('upDevices') + .select('"upDevices"."upEndpoint"', 'upEndpoint') + .where('"upEndpoint" = :upEndpoint', { upEndpoint }) + .andWhere('user.id = :loggedInUserId', { loggedInUserId }) + .leftJoin('upDevices.user', 'user') + .getRawOne() +} + +export const getUPEndpointsForPodcastId = async (podcastId: string) => { + if (!podcastId) { + throw new createError.BadRequest('A podcastId but be provided.') + } + + const repository = getRepository(Notification) + const notifications = await repository + .createQueryBuilder('notifications') + .select('"upDevices"."upEndpoint", "upEndpoint"') + .innerJoin(UPDevice, 'upDevices', 'notifications."userId" = "upDevices"."userId"') + .where('notifications."podcastId" = :podcastId', { podcastId }) + .getRawMany() + + const upEndpoints = notifications.map((upDevice: UPDevice) => upDevice.upEndpoint) + + return upEndpoints +} diff --git a/src/entities/index.ts b/src/entities/index.ts index 5e2897da..ade6d7c0 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -17,6 +17,7 @@ export { Playlist } from './playlist' export { Podcast } from './podcast' export { RecentEpisodeByCategory } from './recentEpisodeByCategory' export { RecentEpisodeByPodcast } from './recentEpisodeByPodcast' +export { UPDevice } from './upDevice' export { User } from './user' export { UserHistoryItem } from './userHistoryItem' export { UserNowPlayingItem } from './userNowPlayingItem' diff --git a/src/entities/upDevice.ts b/src/entities/upDevice.ts new file mode 100644 index 00000000..e1489ffc --- /dev/null +++ b/src/entities/upDevice.ts @@ -0,0 +1,30 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm' +import { User } from '~/entities' + +@Entity('upDevices') +export class UPDevice { + @PrimaryColumn() + upEndpoint: string + + // E2EE + @Column() + upPublicKey: string + + // E2EE + @Column() + upAuthKey: string + + @ManyToOne((type) => User, (user) => user.upDevices, { + nullable: false, + onDelete: 'CASCADE' + }) + user: User + + @CreateDateColumn() + createdAt: Date + + @UpdateDateColumn() + updatedAt: Date +} diff --git a/src/entities/user.ts b/src/entities/user.ts index 09c27c19..58a7db1e 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -23,6 +23,7 @@ import { Notification, PayPalOrder, Playlist, + UPDevice, UserHistoryItem, UserQueueItem } from '~/entities' @@ -184,6 +185,9 @@ export class User { @OneToMany((type) => Playlist, (playlist) => playlist.owner) playlists: Playlist[] + @OneToMany((type) => UPDevice, (upDevice) => upDevice.user) + upDevices: UPDevice[] + @OneToMany((type) => UserHistoryItem, (userHistoryItem) => userHistoryItem.owner) userHistoryItems: UserHistoryItem[] diff --git a/src/lib/db.ts b/src/lib/db.ts index f9d0b0a7..b774545a 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -20,6 +20,7 @@ import { Podcast, RecentEpisodeByCategory, RecentEpisodeByPodcast, + UPDevice, User, UserHistoryItem, UserNowPlayingItem, @@ -47,6 +48,7 @@ const entities = [ Podcast, RecentEpisodeByCategory, RecentEpisodeByPodcast, + UPDevice, User, UserHistoryItem, UserNowPlayingItem, diff --git a/src/middleware/queryValidation/upDevice.ts b/src/middleware/queryValidation/upDevice.ts new file mode 100644 index 00000000..b75dac87 --- /dev/null +++ b/src/middleware/queryValidation/upDevice.ts @@ -0,0 +1,42 @@ +const Joi = require('joi') +import { validateBaseBody } from './base' + +const validateUPDeviceCreate = async (ctx, next) => { + const schema = Joi.object() + .keys({ + upEndpoint: Joi.string(), + upPublicKey: Joi.string(), + upAuthKey: Joi.string() + }) + .required() + .min(1) + + await validateBaseBody(schema, ctx, next) +} + +const validateUPDeviceDelete = async (ctx, next) => { + const schema = Joi.object() + .keys({ + upEndpoint: Joi.string() + }) + .required() + .min(1) + + await validateBaseBody(schema, ctx, next) +} + +const validateUPDeviceUpdate = async (ctx, next) => { + const schema = Joi.object() + .keys({ + nextUPEndpoint: Joi.string(), + previousUPEndpoint: Joi.string(), + upPublicKey: Joi.string(), + upAuthKey: Joi.string() + }) + .required() + .min(4) + + await validateBaseBody(schema, ctx, next) +} + +export { validateUPDeviceCreate, validateUPDeviceDelete, validateUPDeviceUpdate } diff --git a/src/routes/index.ts b/src/routes/index.ts index 2b4d0f06..1730357d 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -21,6 +21,7 @@ export { podcastRouter } from '~/routes/podcast' export { podcastIndexRouter } from '~/routes/podcastIndex' export { podpingRouter } from '~/routes/podping' export { toolsRouter } from '~/routes/tools' +export { upDeviceRouter } from '~/routes/upDevice' export { userRouter } from '~/routes/user' export { userHistoryItemRouter } from '~/routes/userHistoryItem' export { userNowPlayingItemRouter } from '~/routes/userNowPlayingItem' diff --git a/src/routes/upDevice.ts b/src/routes/upDevice.ts new file mode 100644 index 00000000..c88dce27 --- /dev/null +++ b/src/routes/upDevice.ts @@ -0,0 +1,108 @@ +import * as bodyParser from 'koa-bodyparser' +import * as Router from 'koa-router' +import { config } from '~/config' +import { createUPDevice, deleteUPDevice, updateUPDevice } from '~/controllers/upDevice' +import { emitRouterError } from '~/lib/errors' +import { jwtAuth } from '~/middleware/auth/jwtAuth' +import { + validateUPDeviceCreate, + validateUPDeviceDelete, + validateUPDeviceUpdate +} from '~/middleware/queryValidation/upDevice' +const RateLimit = require('koa2-ratelimit').RateLimit +const { rateLimiterMaxOverride } = config + +const router = new Router({ prefix: `${config.apiPrefix}${config.apiVersion}/up-device` }) +router.use(bodyParser()) + +const createUPDeviceLimiter = RateLimit.middleware({ + interval: 1 * 60 * 1000, + max: rateLimiterMaxOverride || 10, + message: `You're doing that too much. Please try again in a minute.`, + prefixKey: 'post/up-device/create' +}) + +// Create an UPDevice for a logged-in user +router.post('/create', createUPDeviceLimiter, jwtAuth, validateUPDeviceCreate, async (ctx) => { + try { + const { upEndpoint, upPublicKey, upAuthKey } = ctx.request.body as any + await createUPDevice({ + upEndpoint, + upPublicKey, + upAuthKey, + loggedInUserId: ctx.state.user.id + }) + ctx.status = 200 + ctx.body = { + message: 'UPDevice created' + } + } catch (error) { + emitRouterError(error, ctx) + } +}) + +const updateUPDeviceLimiter = RateLimit.middleware({ + interval: 1 * 60 * 1000, + max: rateLimiterMaxOverride || 10, + message: `You're doing that too much. Please try again in a minute.`, + prefixKey: 'post/up-device/update' +}) + +// Update an UPDevice for a logged-in user +router.post('/update', updateUPDeviceLimiter, jwtAuth, validateUPDeviceUpdate, async (ctx) => { + try { + const { nextUPEndpoint, previousUPEndpoint, upPublicKey, upAuthKey } = ctx.request.body as any + await updateUPDevice({ + previousUPEndpoint, + nextUPEndpoint, + upPublicKey, + upAuthKey, + loggedInUserId: ctx.state.user.id + }) + ctx.status = 200 + ctx.body = { + message: 'UPDevice updated' + } + } catch (error) { + emitRouterError(error, ctx) + } +}) + +const deleteUPDeviceLimiter = RateLimit.middleware({ + interval: 1 * 60 * 1000, + max: rateLimiterMaxOverride || 10, + message: `You're doing that too much. Please try again in a minute.`, + prefixKey: 'post/up-device/delete' +}) + +// Delete an UPDevice for a logged-in user +router.post('/delete', deleteUPDeviceLimiter, jwtAuth, validateUPDeviceDelete, async (ctx) => { + try { + console.log('wtffffff') + const { upEndpoint } = ctx.request.body as any + await deleteUPDevice(upEndpoint, ctx.state.user.id) + ctx.status = 200 + ctx.body = { + message: 'UPDevice deleted' + } + } catch (error) { + emitRouterError(error, ctx) + } +}) + +// // NOTE: This endpoint is just for debugging, should not be in prod. +// // Get UPDevice.upEndpoints for podcast +// router.get('/podcast/up-tokens/:podcastId', jwtAuth, async (ctx) => { +// try { +// const { podcastId } = ctx.params +// const response = await getUPEndpointsForPodcastId(podcastId) +// ctx.status = 200 +// ctx.body = { +// upEndpoints: response +// } +// } catch (error) { +// emitRouterError(error, ctx) +// } +// }) + +export const upDeviceRouter = router diff --git a/src/seeds/qa/populateDatabase.ts b/src/seeds/qa/populateDatabase.ts index 467c0eed..9c9d4361 100644 --- a/src/seeds/qa/populateDatabase.ts +++ b/src/seeds/qa/populateDatabase.ts @@ -51,6 +51,8 @@ const populateDatabase = async (connection: Connection, isQuickRun: boolean): Pr /* TODO: RecentEpisodesByPodcast */ + /* TODO: UPDevices */ + /* TODO: UserHistoryItems */ /* TODO: UserNowPlayingItems */ From 574c3c38ba32a8a9d4a760a5e96268bb4fce6b12 Mon Sep 17 00:00:00 2001 From: Mitch Downey Date: Sat, 22 Jul 2023 00:46:39 -0500 Subject: [PATCH 02/11] Create podverse-api_v4-13-4.postman_collection.json --- ...dverse-api_v4-13-4.postman_collection.json | 3456 +++++++++++++++++ 1 file changed, 3456 insertions(+) create mode 100644 docs/postman/podverse-api_v4-13-4.postman_collection.json diff --git a/docs/postman/podverse-api_v4-13-4.postman_collection.json b/docs/postman/podverse-api_v4-13-4.postman_collection.json new file mode 100644 index 00000000..de7cc855 --- /dev/null +++ b/docs/postman/podverse-api_v4-13-4.postman_collection.json @@ -0,0 +1,3456 @@ +{ + "info": { + "_postman_id": "38dcff82-6a40-4bbe-982c-20822acfe831", + "name": "podverse-api", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "1122094" + }, + "item": [ + { + "name": "AccountClaimToken / Get by ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/claim-account/:id", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "claim-account", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "6fc489b1-46b1-4f89-8734-e2f3725da3d8", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "AccountClaimToken / Redeem by ID", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"id\": \"6fc489b1-46b1-4f89-8734-e2f3725da3d8\",\n \"email\": \"accountClaimTokenTest@stage.podverse.fm\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/claim-account", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "claim-account" + ] + } + }, + "response": [] + }, + { + "name": "AddByRSSPodcastFeedUrl / Add", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"addByRSSPodcastFeedUrl\" : \"http://rss.art19.com/the-daily\"\r\n}" + }, + "url": { + "raw": "{{base}}/api/v1/add-by-rss-podcast-feed-url/add", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "add-by-rss-podcast-feed-url", + "add" + ] + } + }, + "response": [] + }, + { + "name": "AddByRSSPodcastFeedUrls / Add Many", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"addByRSSPodcastFeedUrls\" : [\r\n \"https://ginl-podcast.s3.amazonaws.com/0_Resources/Timothy_Keller_Podcasts.xml\",\r\n \"https://podnews.net/rss\",\r\n \"http://feeds.feedburner.com/dancarlin/history?format=xml\"\r\n ]\r\n}" + }, + "url": { + "raw": "{{base}}/api/v1/add-by-rss-podcast-feed-url/add-many", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "add-by-rss-podcast-feed-url", + "add-many" + ] + } + }, + "response": [] + }, + { + "name": "AddByRSSPodcastFeedUrl / Remove", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"addByRSSPodcastFeedUrl\": \"https://feeds.npr.org/381444908/podcast.xml\"\n}\n" + }, + "url": { + "raw": "{{base}}/api/v1/add-by-rss-podcast-feed-url/remove", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "add-by-rss-podcast-feed-url", + "remove" + ], + "query": [ + { + "key": "", + "value": "", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Apple - App Store - VerifyReceipt - Sandbox", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"receipt-data\": \"\",\n \"password\": \"\"\n}" + }, + "url": { + "raw": "https://sandbox.itunes.apple.com/verifyReceipt", + "protocol": "https", + "host": [ + "sandbox", + "itunes", + "apple", + "com" + ], + "path": [ + "verifyReceipt" + ] + } + }, + "response": [] + }, + { + "name": "App Store Purchase / Update Purchase Status", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"transactionReceipt\": \"\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/app-store/update-purchase-status", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "app-store", + "update-purchase-status" + ] + } + }, + "response": [] + }, + { + "name": "Auth / Get Authenticated User Info", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{base}}/api/v1/auth/get-authenticated-user-info", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "auth", + "get-authenticated-user-info" + ] + } + }, + "response": [] + }, + { + "name": "Auth / Login", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"email\": \"premium@qa.podverse.fm\",\n\t\"password\": \"Test!1Aa\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/auth/login", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "auth", + "login" + ] + } + }, + "response": [] + }, + { + "name": "Auth / Logout", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{base}}/api/v1/auth/logout", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "auth", + "logout" + ] + } + }, + "response": [] + }, + { + "name": "Auth / Reset Password", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"password\": \"\",\n\t\"resetPasswordToken\": \"\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/auth/reset-password", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "auth", + "reset-password" + ] + } + }, + "response": [] + }, + { + "name": "Auth / Send Reset Password", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"email\": \"premium@qa.podverse.fm\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/auth/send-reset-password", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "auth", + "send-reset-password" + ] + } + }, + "response": [] + }, + { + "name": "Auth / Send Verification", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"email\": \"\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/auth/send-verification", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "auth", + "send-verification" + ] + } + }, + "response": [] + }, + { + "name": "Auth / Sign Up", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"email\": \"\",\n\t\"name\": \"\",\n\t\"password\": \"\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/auth/sign-up", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "auth", + "sign-up" + ], + "query": [ + { + "key": "includeBodyToken", + "value": "true", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Auth / Verify Email", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/auth/verify-email?token=", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "auth", + "verify-email" + ], + "query": [ + { + "key": "token", + "value": "" + } + ] + } + }, + "response": [] + }, + { + "name": "Author / Find by Query", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/author?page=1&sort=top-past-week", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "author" + ], + "query": [ + { + "key": "authorId", + "value": "", + "disabled": true + }, + { + "key": "authorName", + "value": "", + "disabled": true + }, + { + "key": "authorSlug", + "value": "", + "disabled": true + }, + { + "key": "page", + "value": "1" + }, + { + "key": "sort", + "value": "top-past-week" + } + ] + } + }, + "response": [] + }, + { + "name": "Author / Get by ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/author/:id", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "author", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "BitPay / Invoice / Create", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{base}}/api/v1/bitpay/invoice", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "bitpay", + "invoice" + ] + } + }, + "response": [] + }, + { + "name": "BitPay / Invoice / Get by ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/bitpay/invoice/:id", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "bitpay", + "invoice", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "BitPay / Notification", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"id\": \"\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/bitpay/notification", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "bitpay", + "notification" + ], + "query": [ + { + "key": "", + "value": "", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Category / Find by Query", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/category?page=&sort=top-past-week", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "category" + ], + "query": [ + { + "key": "id", + "value": "", + "disabled": true + }, + { + "key": "slug", + "value": "", + "disabled": true + }, + { + "key": "title", + "value": "", + "disabled": true + }, + { + "key": "page", + "value": "" + }, + { + "key": "sort", + "value": "top-past-week" + } + ] + } + }, + "response": [] + }, + { + "name": "Category / Get by ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/category/:id", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "category", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "Clips / Get Public MediaRefs by Episode MediaUrl", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/clips?mediaUrl=https://chtbl.com/track/282487/traffic.libsyn.com/secure/whatbitcoindid/WBD278.mp3", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "clips" + ], + "query": [ + { + "key": "mediaUrl", + "value": "https://chtbl.com/track/282487/traffic.libsyn.com/secure/whatbitcoindid/WBD278.mp3" + } + ] + } + }, + "response": [] + }, + { + "name": "Dev Admin / Parse Feed by Podcast ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/dev-admin/parse-feed-by-podcast-id/:podcastId", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "dev-admin", + "parse-feed-by-podcast-id", + ":podcastId" + ], + "variable": [ + { + "key": "podcastId", + "value": "asdfasdf" + } + ] + } + }, + "response": [] + }, + { + "name": "Dev Admin / Add or Update Feed from Podcast Index ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/dev-admin/parse-feed-by-podcast-id/:podcastId", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "dev-admin", + "parse-feed-by-podcast-id", + ":podcastId" + ], + "variable": [ + { + "key": "podcastId", + "value": "asdfasdf" + } + ] + } + }, + "response": [] + }, + { + "name": "Episode / Find by Query", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/episode?page=1&sort=most-recent", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "episode" + ], + "query": [ + { + "key": "includePodcast", + "value": "true", + "disabled": true + }, + { + "key": "searchAllFieldsText", + "value": "podcasting 2.0", + "disabled": true + }, + { + "key": "sincePubDate", + "value": "2012-02-16 19:11:25", + "disabled": true + }, + { + "key": "page", + "value": "1" + }, + { + "key": "sort", + "value": "most-recent" + }, + { + "key": "categories", + "value": "gFYkKhBCkf,4EO8feINB", + "disabled": true + }, + { + "key": "podcastId", + "value": "w6CgXlyi,njihQPuP4SO", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Episode / Get by ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/episode/:id", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "episode", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "Episode / Get by PodcastId and GUID", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"episodeGuid\": \"63a2f5f8-5ad3-4dd1-8cd3-129ab7d00f25\",\n \"podcastId\": \"g40Um-HP1\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/episode/get-by-guid", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "episode", + "get-by-guid" + ] + } + }, + "response": [] + }, + { + "name": "Episode / Get by PodcastId and MediaUrl", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"episodeGuid\": \"63a2f5f8-5ad3-4dd1-8cd3-129ab7d00f25\",\n \"podcastId\": \"g40Um-HP1\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/episode/get-by-guid", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "episode", + "get-by-guid" + ] + } + }, + "response": [] + }, + { + "name": "Episode / Retrieve Latest Chapters", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/episode/:id/retrieve-latest-chapters", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "episode", + ":id", + "retrieve-latest-chapters" + ], + "variable": [ + { + "key": "id", + "value": "z3kazYivU", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "Episode / Proxy Activity Pub", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/episode/:id/proxy/activity-pub", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "episode", + ":id", + "proxy", + "activity-pub" + ], + "variable": [ + { + "key": "id", + "value": "78pYMaFdJ" + } + ] + } + }, + "response": [] + }, + { + "name": "Episode / Proxy Transcript", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/episode/:id/proxy/transcript", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "episode", + ":id", + "proxy", + "transcript" + ], + "variable": [ + { + "key": "id", + "value": "5Ww0yiWlA" + } + ] + } + }, + "response": [] + }, + { + "name": "Episode / Proxy Twitter", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/episode/:id/proxy/activity-pub", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "episode", + ":id", + "proxy", + "activity-pub" + ], + "variable": [ + { + "key": "id", + "value": "78pYMaFdJ" + } + ] + } + }, + "response": [] + }, + { + "name": "FCMDevice / Create", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fcmToken\": \"2022\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/fcm-device/create", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "fcm-device", + "create" + ] + } + }, + "response": [] + }, + { + "name": "FCMDevice / Delete", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fcmToken\": \"2\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/fcm-device/delete", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "fcm-device", + "delete" + ] + } + }, + "response": [] + }, + { + "name": "FCMDevice / Update", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"previousFCMToken\": \"10101010\",\n \"nextFCMToken\": \"1984\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/fcm-device/update", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "fcm-device", + "update" + ] + } + }, + "response": [] + }, + { + "name": "FCMDevice / Get FCMTokens for Podcast", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fcmToken\": \"123\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/fcm-device/podcast/fcm-tokens/:podcastId", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "fcm-device", + "podcast", + "fcm-tokens", + ":podcastId" + ], + "variable": [ + { + "key": "podcastId", + "value": "1ejynW3J3" + } + ] + } + }, + "response": [] + }, + { + "name": "FeedUrl / Get by ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/feedUrl/:id", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "feedUrl", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "FeedUrl / Get Many Podcast Ids", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"feedUrls\": [\n \"https://anchor.fm/s/3cbbb3b8/podcast/rss\",\n \"https://2.5admins.com/feed/podcast\",\n \"https://badvoltage.org/feed/mp3/\",\n \"https://feeds.fireside.fm/linuxspotlight/rss\",\n \"https://www.patreon.com/rss/thedickshow?auth=wHj_CpnDBBmPE3EQrR5aHgQqWvKOt4n2\",\n \"https://www.patreon.com/rss/Podculture?auth=3hi1GH9yJlP2rzJ4SLysn_5ZiUEkWZoz\"\n ]\n}" + }, + "url": { + "raw": "{{base}}/api/v1/feedUrl/get-many-podcast-ids", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "feedUrl", + "get-many-podcast-ids" + ] + } + }, + "response": [] + }, + { + "name": "Firebase / FCM / Send push notification", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "key=AAAAyEpy3n8:APA91bG0snSeK1qYvDnYSzMjOxRngfyg-wLgV4uVbLcv98CVr_HItq08_gX8vy3qtYHa2hTFPtA9VYMnFE6rJXaPCM_VhepF-Jr5anASMB-Fs3R-wgo5wj5tfi5Kf3zj_t8jx643qwNc", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"to\" : \"f6GFBdYrFEo_gewUShy0ID:APA91bHjU57aCoHrm3pzKu8XsJnppmvwXIe0H4OHLpRpI_qYyFMUITsDZ1mmNROsaxxhojXT8818gDVUdMkd-w89P9_Yu9RVoJzOetzdhtMQCVCFWakVmM7OPTBbvr_f_Pv_W3i4XouY\",\n \"notification\": {\n \"body\": \"Podverse - Livestream Test\",\n \"title\": \"LIVE: Podverse - Test Feed - Livestream Solo\",\n \"podcastId\": \"x6tZlp8gL5Y\",\n \"episodeId\": \"M_tYcPb7b\",\n \"podcastTitle\": \"Podverse - Test Feed - Livestream Solo\",\n \"episodeTitle\": \"Podverse - Livestream Test\",\n \"notificationType\": \"live\",\n \"timeSent\": \"2022-12-12T03:06:42.615Z\",\n \"image\": \"https://images.podverse.fm/podcast-images/x6tZlp8gL5Y/podversetestfeedlivestreamsolo-14.jpg\"\n },\n \"data\": {\n \"body\": \"Podverse - Livestream Test\",\n \"title\": \"LIVE: Podverse - Test Feed - Livestream Solo\",\n \"podcastId\": \"x6tZlp8gL5Y\",\n \"episodeId\": \"M_tYcPb7b\",\n \"podcastTitle\": \"Podverse - Test Feed - Livestream Solo\",\n \"episodeTitle\": \"Podverse - Livestream Test\",\n \"notificationType\": \"new-episode\",\n \"timeSent\": \"2022-12-12T03:06:42.615Z\"\n }\n}" + }, + "url": { + "raw": "https://fcm.googleapis.com/fcm/send", + "protocol": "https", + "host": [ + "fcm", + "googleapis", + "com" + ], + "path": [ + "fcm", + "send" + ] + } + }, + "response": [] + }, + { + "name": "Google Play Purchase / Update Purchase Status", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"productId\": \"\",\n\t\"purchaseToken\": \"\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/google-play/update-purchase-status", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "google-play", + "update-purchase-status" + ] + } + }, + "response": [] + }, + { + "name": "LiveItems / Find by Podcast ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/liveItem/podcast/:id", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "liveItem", + "podcast", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "vPJzOSmVW9", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "LiveItem / Find by Query", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/liveItem?searchAllFieldsText=live&page=1&sort=most-recent&liveItemStatus=pending", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "liveItem" + ], + "query": [ + { + "key": "includePodcast", + "value": "true", + "disabled": true + }, + { + "key": "searchAllFieldsText", + "value": "live" + }, + { + "key": "sincePubDate", + "value": "2012-02-16 19:11:25", + "disabled": true + }, + { + "key": "page", + "value": "1" + }, + { + "key": "sort", + "value": "most-recent" + }, + { + "key": "categories", + "value": "gFYkKhBCkf,4EO8feINB", + "disabled": true + }, + { + "key": "podcastId", + "value": "w6CgXlyi,njihQPuP4SO", + "disabled": true + }, + { + "key": "liveItemStatus", + "value": "pending" + } + ] + } + }, + "response": [] + }, + { + "name": "MediaRef / Find by Query", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/mediaRef?searchTitle=Podcasting 2.0&page=1&sort=top-past-year&includeEpisode=true", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "mediaRef" + ], + "query": [ + { + "key": "episodeId", + "value": "", + "disabled": true + }, + { + "key": "podcastId", + "value": "", + "disabled": true + }, + { + "key": "searchTitle", + "value": "Podcasting 2.0" + }, + { + "key": "page", + "value": "1" + }, + { + "key": "sort", + "value": "top-past-year" + }, + { + "key": "includeEpisode", + "value": "true" + }, + { + "key": "includePodcast", + "value": "true", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "MediaRef / Create", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"authors\": [],\n \"categories\": [],\n \"endTime\": 100,\n \"episodeId\": \"gRgjd3YcKb\",\n \"isPublic\": \"true\",\n \"startTime\": 50,\n \"title\": \"Sample clip title\"\n }" + }, + "url": { + "raw": "{{base}}/api/v1/mediaRef", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "mediaRef" + ] + } + }, + "response": [] + }, + { + "name": "MediaRef / Update", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "", + "value": "", + "disabled": true + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"authors\": [],\n \"categories\": [],\n \"endTime\": 100,\n \"episodeId\": \"\",\n \"id\": \"\",\n \"isPublic\": \"true\",\n \"startTime\": 50,\n \"title\": \"New sample clip title\"\n }" + }, + "url": { + "raw": "{{base}}/api/v1/mediaRef", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "mediaRef" + ] + } + }, + "response": [] + }, + { + "name": "MediaRef / Get by ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/mediaRef/:id", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "mediaRef", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "9rA5BhWp", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "MediaRef / Delete", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{base}}/api/v1/mediaRef/:id", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "mediaRef", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "Meta / Minimum Mobile Version", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/meta/min-mobile-version", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "meta", + "min-mobile-version" + ] + } + }, + "response": [] + }, + { + "name": "Network Reachability Check", + "request": { + "method": "HEAD", + "header": [], + "url": { + "raw": "{{base}}/api/v1/network-reachability-check", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "network-reachability-check" + ] + } + }, + "response": [] + }, + { + "name": "Notification / Podcast / Subscribe", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"podcastId\": \"3NkC9iQh6\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/notification/podcast/subscribe", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "notification", + "podcast", + "subscribe" + ] + } + }, + "response": [] + }, + { + "name": "Notification / Podcast / Unsubscribe", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"podcastId\": \"gyEGNwJud\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/notification/podcast/unsubscribe", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "notification", + "podcast", + "unsubscribe" + ] + } + }, + "response": [] + }, + { + "name": "PayPal / Order / Create", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"paymentID\": \"test\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/paypal/order", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "paypal", + "order" + ] + } + }, + "response": [] + }, + { + "name": "PayPal / Order / Get by ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/paypal/order/:id", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "paypal", + "order", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "1234567890", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "PayPal / Webhooks / Payment Completed", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"resource\": {\n\t\t\"parent_payment\": \"PAYID-L5MGRDQ1C945042FF8655610\"\n\t}\n}" + }, + "url": { + "raw": "{{base}}/api/v1/paypal/webhooks/payment-completed", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "paypal", + "webhooks", + "payment-completed" + ] + } + }, + "response": [] + }, + { + "name": "Playlist / Find by Query", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/playlist?playlistId=hKxZyKJjW&page=1&sort=top-past-week", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "playlist" + ], + "query": [ + { + "key": "playlistId", + "value": "hKxZyKJjW" + }, + { + "key": "page", + "value": "1" + }, + { + "key": "sort", + "value": "top-past-week" + } + ] + } + }, + "response": [] + }, + { + "name": "Playlist / Find by Query Subscribed", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/playlist/subscribed?page=1&sort=top-past-week", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "playlist", + "subscribed" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "sort", + "value": "top-past-week" + } + ] + } + }, + "response": [] + }, + { + "name": "Playlist / Create", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"description\": \"Test description\",\n\t\"isPublic\": true,\n\t\"itemsOrder\": [],\n\t\"mediaRefs\": [],\n\t\"title\": \"Test title\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/playlist", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "playlist" + ] + } + }, + "response": [] + }, + { + "name": "Playlist / Update", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"id\": \"\",\n\t\"description\": \"New test description\",\n\t\"isPublic\": false,\n\t\"itemsOrder\": [],\n\t\"mediaRefs\": [],\n\t\"title\": \"Premium - Test Playlist 2345\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/playlist", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "playlist" + ] + } + }, + "response": [] + }, + { + "name": "Playlist / Get by ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/playlist/:id", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "playlist", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "hKxZyKJjW", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "Playlist / Delete", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{base}}/api/v1/playlist/:id", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "playlist", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "Playlist / Add or Remove Item", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"episodeId\": \"\",\n\t\"mediaRefId\": \"\",\n\t\"playlistId\": \"\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/playlist/add-or-remove", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "playlist", + "add-or-remove" + ] + } + }, + "response": [] + }, + { + "name": "Playlist / Toggle Subscribe", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/playlist/toggle-subscribe/:id", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "playlist", + "toggle-subscribe", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "Podcast / Find by Query", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/podcast?page=1&searchTitle=Podcasting 2.0", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "podcast" + ], + "query": [ + { + "key": "includeAuthors", + "value": "true", + "disabled": true + }, + { + "key": "includeCategories", + "value": "true", + "disabled": true + }, + { + "key": "page", + "value": "1" + }, + { + "key": "podcastId", + "value": "AEF2P6R78R,IF0Gw5CsiF,8BkTSdqJjWJ,ahsnnaYRRJLA,rQww79O8MyAX,6O-ZIQRXM6uB,mnBSPyRb6oB9,GQAkOsG9CZiy,9AWGyxtnbDXE,pIGik4mwyv,V-mLzU2Z29NL,fLUZ4pMZofu9,C310r9_AbO", + "disabled": true + }, + { + "key": "sort", + "value": "random", + "disabled": true + }, + { + "key": "searchAuthor", + "value": "Josh", + "disabled": true + }, + { + "key": "searchTitle", + "value": "Podcasting 2.0" + } + ] + } + }, + "response": [] + }, + { + "name": "Podcast / Find by Query Subscribed", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/podcast/subscribed?page=1&sort=most-recent", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "podcast", + "subscribed" + ], + "query": [ + { + "key": "includeAuthors", + "value": "true", + "disabled": true + }, + { + "key": "includeCategories", + "value": "true", + "disabled": true + }, + { + "key": "page", + "value": "1" + }, + { + "key": "podcastId", + "value": "AEF2P6R78R,IF0Gw5CsiF,8BkTSdqJjWJ,ahsnnaYRRJLA,rQww79O8MyAX,6O-ZIQRXM6uB,mnBSPyRb6oB9,GQAkOsG9CZiy,9AWGyxtnbDXE,pIGik4mwyv,V-mLzU2Z29NL,fLUZ4pMZofu9,C310r9_AbO", + "disabled": true + }, + { + "key": "sort", + "value": "most-recent" + }, + { + "key": "searchAuthor", + "value": "Josh", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Podcast / Find Podcasts by FeedUrls", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"feedUrls\": [\n \"http://feeds.megaphone.fm/wethepeoplelive\",\n \"http://feeds.feedburner.com/dancarlin/history?format=xml\",\n \"https://ginl-podcasdfast.s3.amazonaws.com/0_Resources/Timothy_Keller_Podcasts.xml\",\n \"http://feeds.feedburner.com/TheBryanCallenShow\"\n ]\n}" + }, + "url": { + "raw": "{{base}}/api/v1/podcast/find-by-feed-urls", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "podcast", + "find-by-feed-urls" + ] + } + }, + "response": [] + }, + { + "name": "Podcast / Get by ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/podcast/:id", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "podcast", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "mN25xFjDG", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "Podcast / Redirect to Website by podcastIndexId", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/podcast/podcastindex/:podcastIndexId", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "podcast", + "podcastindex", + ":podcastIndexId" + ], + "variable": [ + { + "key": "podcastIndexId", + "value": "920666" + } + ] + } + }, + "response": [] + }, + { + "name": "Podcast / Redirect to Website by podcastGuid", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/podcast/by-podcast-guid/:podcastGuid", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "podcast", + "by-podcast-guid", + ":podcastGuid" + ], + "variable": [ + { + "key": "podcastGuid", + "value": "917393e3-1b1e-5cef-ace4-edaa54e1f810" + } + ] + } + }, + "response": [] + }, + { + "name": "Podcast / Redirect to Website by feedUrl", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/podcast/by-feed-url?feedUrl=http://mp3s.nashownotes.com/pc20rss.xml", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "podcast", + "by-feed-url" + ], + "query": [ + { + "key": "feedUrl", + "value": "http://mp3s.nashownotes.com/pc20rss.xml" + } + ] + } + }, + "response": [] + }, + { + "name": "Podcast / Get by Podcast Index ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/podcast/podcastindex/:id", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "podcast", + "podcastindex", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "199138", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "Podcast / Metadata", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/podcast/metadata", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "podcast", + "metadata" + ], + "query": [ + { + "key": "podcastId", + "value": "", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Podcast / Toggle Subscribe", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/podcast/toggle-subscribe/:id", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "podcast", + "toggle-subscribe", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "PodcastIndex / Podcast / Get by ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/podcast/podcastindex/data/:id", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "podcast", + "podcastindex", + "data", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "169991" + } + ] + } + }, + "response": [] + }, + { + "name": "PodcastIndex / Value Tags / Get by Podcast and Episode GUIDs", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/podcast-index/value/podcast-guid/:podcastGuid/episode-guid/:episodeGuid", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "podcast-index", + "value", + "podcast-guid", + ":podcastGuid", + "episode-guid", + ":episodeGuid" + ], + "variable": [ + { + "key": "podcastGuid", + "value": "917393e3-1b1e-5cef-ace4-edaa54e1f810" + }, + { + "key": "episodeGuid", + "value": "PC20137" + } + ] + } + }, + "response": [] + }, + { + "name": "PodcastIndex / Value Tags / Get by Podcast GUID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/podcast-index/value/podcast-guid/:podcastGuid", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "podcast-index", + "value", + "podcast-guid", + ":podcastGuid" + ], + "variable": [ + { + "key": "podcastGuid", + "value": "917393e3-1b1e-5cef-ace4-edaa54e1f810" + } + ] + } + }, + "response": [] + }, + { + "name": "Podping - Send LiveItem Status Update Notification", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"feedUrl\": \"http://www.feed.behindthesch3m3s.com/feed.xml\"\n}\n" + }, + "url": { + "raw": "{{base}}/api/v1/podping/feed/live-update", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "podping", + "feed", + "live-update" + ] + } + }, + "response": [] + }, + { + "name": "Tools / FindHTTPS", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"url\": \"\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/tools/findHTTPS", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "tools", + "findHTTPS" + ] + } + }, + "response": [] + }, + { + "name": "UPDevice / Create", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"upEndpoint\": \"https://example.com\",\n \"upPublicKey\": \"some-public-key\",\n \"upAuthKey\": \"some-secret-key\"\n}\n" + }, + "url": { + "raw": "{{base}}/api/v1/up-device/create", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "up-device", + "create" + ] + } + }, + "response": [] + }, + { + "name": "UPDevice / Delete", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"upEndpoint\": \"https://example.com\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/up-device/delete", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "up-device", + "delete" + ] + } + }, + "response": [] + }, + { + "name": "UPDevice / Update", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"previousUPEndpoint\": \"https://example.com\",\n \"nextUPEndpoint\": \"https://beta.example.com\",\n \"upPublicKey\": \"some-public-key\",\n \"upAuthKey\": \"some-auth-key\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/up-device/update", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "up-device", + "update" + ] + } + }, + "response": [] + }, + { + "name": "UPDevice / Get UPTokens for Podcast", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"upEndpoint\": \"https://example.com\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/up-device/podcast/fcm-tokens/:podcastId", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "up-device", + "podcast", + "fcm-tokens", + ":podcastId" + ], + "variable": [ + { + "key": "podcastId", + "value": "1ejynW3J3" + } + ] + } + }, + "response": [] + }, + { + "name": "User / Delete", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{base}}/api/v1/user", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user" + ] + } + }, + "response": [] + }, + { + "name": "User / Download Data", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/user/download", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user", + "download" + ] + } + }, + "response": [] + }, + { + "name": "User / Find Public Users by Query", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/user?page=1", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user" + ], + "query": [ + { + "key": "userIds", + "value": "", + "disabled": true + }, + { + "key": "page", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "User / Find Public Users by Query Subscribed", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/user/subscribed?page=1", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user", + "subscribed" + ], + "query": [ + { + "key": "userIds", + "value": "", + "disabled": true + }, + { + "key": "page", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "User / Get Public User by ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/user/:id", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "EVHDBRZY", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "User / Get MediaRefs", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/user/:id/mediaRefs", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user", + ":id", + "mediaRefs" + ], + "variable": [ + { + "key": "id", + "value": "", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "User / Get Playlists", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/user/:id/playlists", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user", + ":id", + "playlists" + ], + "variable": [ + { + "key": "id", + "value": "", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "User / History Item / Add or Update", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"historyItem\": {\n \"episodeDescription\": \"Test episode description 2\",\n \"episodeId\": \"4s2CiyLsJJ\",\n \"episodeImageUrl\": \"http://example.com/imageUrl\",\n \"episodeMediaUrl\": \"http://example.com/mediaUrl\",\n \"episodePubDate\": \"2020-01-01T23:54:08.000Z\",\n \"episodeTitle\": \"Test episode title 2\",\n \"isPublic\": true,\n \"ownerId\": \"\",\n \"ownerIsPublic\": null,\n \"ownerName\": \"\",\n \"podcastAuthors\": [\"Rk1zs7vs\"],\n \"podcastCategories\": [\"5vNa3RnSZpC\"],\n \"podcastId\": \"0RMk6UYGq\",\n \"podcastImageUrl\": \"http://example.com/imageUrl\",\n \"podcastTitle\": \"Test podcast title 2\",\n \"userPlaybackPosition\": 345\n }\n}" + }, + "url": { + "raw": "{{base}}/api/v1/user/add-or-update-history-item", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user", + "add-or-update-history-item" + ] + } + }, + "response": [] + }, + { + "name": "User / History Items / Update Playback Position", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"historyItems\": [\n\t\t{\n\t \"clipEndTime\": 100,\n\t \"clipId\": \"\",\n\t \"clipStartTime\": \"50\",\n\t \"clipTitle\": \"Test clip title\",\n\t \"episodeDescription\": \"Test episode description\",\n\t \"episodeId\": \"\",\n\t \"episodeImageUrl\": \"http://example.com/imageUrl\",\n\t \"episodeMediaUrl\": \"http://example.com/mediaUrl\",\n\t \"episodePubDate\": \"\",\n\t \"episodeTitle\": \"Test episode title\",\n\t \"isPublic\": true,\n\t \"ownerId\": \"\",\n\t \"ownerIsPublic\": \"\",\n\t \"ownerName\": \"\",\n\t \"podcastAuthors\": null,\n\t \"podcastCategories\": null,\n\t \"podcastId\": \"\",\n\t \"podcastImageUrl\": \"http://example.com/imageUrl\",\n\t \"podcastTitle\": \"Test podcast title\",\n\t \"userPlaybackPosition\": 123\t\t\n\t\t}\n\t]\n}" + }, + "url": { + "raw": "{{base}}/api/v1/user/update-history-item-playback-position", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user", + "update-history-item-playback-position" + ] + } + }, + "response": [] + }, + { + "name": "User / History Item / Remove", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{base}}/api/v1/user/history-item", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user", + "history-item" + ], + "query": [ + { + "key": "episodeId", + "value": "ki9isiMWLLQ", + "disabled": true + }, + { + "key": "mediaRefId", + "value": "TPYjMAZQhMY", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "User / History Items / Clear All", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{base}}/api/v1/user/history-item/clear-all", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user", + "history-item", + "clear-all" + ] + } + }, + "response": [] + }, + { + "name": "User / Logged-in User MediaRefs", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/user/mediaRefs", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user", + "mediaRefs" + ] + } + }, + "response": [] + }, + { + "name": "User / Logged-in User Playlists", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/user/playlists", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user", + "playlists" + ] + } + }, + "response": [] + }, + { + "name": "User / Logged-in User Playlists Combined", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/user/playlists", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user", + "playlists" + ] + } + }, + "response": [] + }, + { + "name": "User / Toggle Subscribe", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base}}/api/v1/user/toggle-subscribe/:id", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user", + "toggle-subscribe", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "User / Update", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"email\": \"premium@stage.podverse.fm\",\n\t\"id\": \"QMReJmbE\",\n\t\"isPublic\": false,\n\t\"name\": \"New Tester\"\n}" + }, + "url": { + "raw": "{{base}}/api/v1/user", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user" + ] + } + }, + "response": [] + }, + { + "name": "User / Update Queue", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"queueItems\": [\n\t\t{\n\t \"clipEndTime\": 100,\n\t \"clipId\": \"\",\n\t \"clipStartTime\": \"50\",\n\t \"clipTitle\": \"Test clip title\",\n\t \"episodeDescription\": \"Test episode description\",\n\t \"episodeId\": \"\",\n\t \"episodeImageUrl\": \"http://example.com/imageUrl\",\n\t \"episodeMediaUrl\": \"http://example.com/mediaUrl\",\n\t \"episodePubDate\": \"\",\n\t \"episodeTitle\": \"Test episode title\",\n\t \"isPublic\": true,\n\t \"ownerId\": \"\",\n\t \"ownerIsPublic\": \"\",\n\t \"ownerName\": \"\",\n\t \"podcastAuthors\": null,\n\t \"podcastCategories\": null,\n\t \"podcastId\": \"\",\n\t \"podcastImageUrl\": \"http://example.com/imageUrl\",\n\t \"podcastTitle\": \"Test podcast title\",\n\t \"userPlaybackPosition\": 123\n\t\t}\n\t]\n}" + }, + "url": { + "raw": "{{base}}/api/v1/user/update-queue", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user", + "update-queue" + ] + } + }, + "response": [] + }, + { + "name": "UserHistoryItem / Add or Update", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"episodeId\": \"TZ_-AXmtV\",\n \"forceUpdateOrderDate\": false,\n \"mediaFileDuration\": 1000,\n \"mediaRefId\": null,\n \"userPlaybackPosition\": 500,\n \"completed\": true\n}" + }, + "url": { + "raw": "{{base}}/api/v1/user-history-item", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user-history-item" + ] + } + }, + "response": [] + }, + { + "name": "UserHistoryItem / Get Items", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{base}}/api/v1/user-history-item?page=1", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user-history-item" + ], + "query": [ + { + "key": "page", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "UserHistoryItem / Get Items Metadata", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{base}}/api/v1/user-history-item/metadata", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user-history-item", + "metadata" + ] + } + }, + "response": [] + }, + { + "name": "UserHistoryItem / Get Items Metadata Mini", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{base}}/api/v1/user-history-item/metadata-mini", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user-history-item", + "metadata-mini" + ] + } + }, + "response": [] + }, + { + "name": "UserHistoryItem / Remove All for User", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{base}}/api/v1/user-history-item/remove-all", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user-history-item", + "remove-all" + ] + } + }, + "response": [] + }, + { + "name": "UserHistoryItem / Remove by Episode Id", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{base}}/api/v1/user-history-item/episode/:episodeId", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user-history-item", + "episode", + ":episodeId" + ], + "variable": [ + { + "key": "episodeId", + "value": "n43uIo6zR8", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "UserHistoryItem / Remove by MediaRef Id", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{base}}/api/v1/user-history-item/mediaRef/:mediaRefId", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user-history-item", + "mediaRef", + ":mediaRefId" + ], + "variable": [ + { + "key": "mediaRefId", + "value": "jE4kZmZhl9", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "UserNowPlayingItem / Delete", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{base}}/api/v1/user-now-playing-item", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user-now-playing-item" + ] + } + }, + "response": [] + }, + { + "name": "UserNowPlayingItem / Get", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{base}}/api/v1/user-now-playing-item", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user-now-playing-item" + ] + } + }, + "response": [] + }, + { + "name": "UserNowPlayingItem / Update", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"episodeId\": null,\n \"clipId\": \"jE4kZmZhl9\",\n \"userPlaybackPosition\": 0\n}" + }, + "url": { + "raw": "{{base}}/api/v1/user-now-playing-item", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user-now-playing-item" + ] + } + }, + "response": [] + }, + { + "name": "UserQueueItem / Add or Update", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"episodeId\": null,\n \"mediaRefId\": \"jE4kZmZhl9\",\n \"queuePosition\": 0\n}" + }, + "url": { + "raw": "{{base}}/api/v1/user-queue-item", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user-queue-item" + ] + } + }, + "response": [] + }, + { + "name": "UserQueueItem / Get Items", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{base}}/api/v1/user-queue-item", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user-queue-item" + ] + } + }, + "response": [] + }, + { + "name": "UserQueueItem / Pop Next", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{base}}/api/v1/user-queue-item/pop-next", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user-queue-item", + "pop-next" + ] + } + }, + "response": [] + }, + { + "name": "UserQueueItem / Remove All", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{base}}/api/v1/user-queue-item/remove-all", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user-queue-item", + "remove-all" + ] + } + }, + "response": [] + }, + { + "name": "UserQueueItem / Remove by Episode Id", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{base}}/api/v1/user-queue-item/episode/:episodeId", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user-queue-item", + "episode", + ":episodeId" + ], + "variable": [ + { + "key": "episodeId", + "value": "n43uIo6zR8", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "UserQueueItem / Remove by Media Id", + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{base}}/api/v1/user-queue-item/mediaRef/:mediaRefId", + "host": [ + "{{base}}" + ], + "path": [ + "api", + "v1", + "user-queue-item", + "mediaRef", + ":mediaRefId" + ], + "variable": [ + { + "key": "mediaRefId", + "value": "jE4kZmZhl9", + "type": "string" + } + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] +} \ No newline at end of file From 057df09719016e6d00ffa38cbdac11093210e0f8 Mon Sep 17 00:00:00 2001 From: Mitch Downey Date: Sat, 22 Jul 2023 13:40:42 -0500 Subject: [PATCH 03/11] Update podverse-api_v4-13-4.postman_collection.json --- docs/postman/podverse-api_v4-13-4.postman_collection.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/postman/podverse-api_v4-13-4.postman_collection.json b/docs/postman/podverse-api_v4-13-4.postman_collection.json index de7cc855..1832edc9 100644 --- a/docs/postman/podverse-api_v4-13-4.postman_collection.json +++ b/docs/postman/podverse-api_v4-13-4.postman_collection.json @@ -1186,7 +1186,7 @@ "header": [ { "key": "Authorization", - "value": "key=AAAAyEpy3n8:APA91bG0snSeK1qYvDnYSzMjOxRngfyg-wLgV4uVbLcv98CVr_HItq08_gX8vy3qtYHa2hTFPtA9VYMnFE6rJXaPCM_VhepF-Jr5anASMB-Fs3R-wgo5wj5tfi5Kf3zj_t8jx643qwNc", + "value": "key=some-private-key-here", "type": "text" }, { From 0e9f921979c6e55af7cac2d1b0060fcc173ea57c Mon Sep 17 00:00:00 2001 From: Mitch Downey Date: Sat, 22 Jul 2023 14:17:20 -0500 Subject: [PATCH 04/11] Bump to version 4.13.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 31474f60..97be859c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "podverse-api", - "version": "4.13.4", + "version": "4.13.5", "description": "Data API, database migration scripts, and backend services for all Podverse models.", "contributors": [ "Mitch Downey" From 8818a2211b02420a77f57fcc0230f8fe464639b2 Mon Sep 17 00:00:00 2001 From: Mitch Downey Date: Sat, 22 Jul 2023 14:46:31 -0500 Subject: [PATCH 05/11] Update podverse-api_v4-13-4.postman_collection.json --- docs/postman/podverse-api_v4-13-4.postman_collection.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/postman/podverse-api_v4-13-4.postman_collection.json b/docs/postman/podverse-api_v4-13-4.postman_collection.json index 1832edc9..28223b2c 100644 --- a/docs/postman/podverse-api_v4-13-4.postman_collection.json +++ b/docs/postman/podverse-api_v4-13-4.postman_collection.json @@ -1186,7 +1186,7 @@ "header": [ { "key": "Authorization", - "value": "key=some-private-key-here", + "value": "key={{FCM_GOOGLE_API_AUTH_TOKEN}}", "type": "text" }, { @@ -1197,7 +1197,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"to\" : \"f6GFBdYrFEo_gewUShy0ID:APA91bHjU57aCoHrm3pzKu8XsJnppmvwXIe0H4OHLpRpI_qYyFMUITsDZ1mmNROsaxxhojXT8818gDVUdMkd-w89P9_Yu9RVoJzOetzdhtMQCVCFWakVmM7OPTBbvr_f_Pv_W3i4XouY\",\n \"notification\": {\n \"body\": \"Podverse - Livestream Test\",\n \"title\": \"LIVE: Podverse - Test Feed - Livestream Solo\",\n \"podcastId\": \"x6tZlp8gL5Y\",\n \"episodeId\": \"M_tYcPb7b\",\n \"podcastTitle\": \"Podverse - Test Feed - Livestream Solo\",\n \"episodeTitle\": \"Podverse - Livestream Test\",\n \"notificationType\": \"live\",\n \"timeSent\": \"2022-12-12T03:06:42.615Z\",\n \"image\": \"https://images.podverse.fm/podcast-images/x6tZlp8gL5Y/podversetestfeedlivestreamsolo-14.jpg\"\n },\n \"data\": {\n \"body\": \"Podverse - Livestream Test\",\n \"title\": \"LIVE: Podverse - Test Feed - Livestream Solo\",\n \"podcastId\": \"x6tZlp8gL5Y\",\n \"episodeId\": \"M_tYcPb7b\",\n \"podcastTitle\": \"Podverse - Test Feed - Livestream Solo\",\n \"episodeTitle\": \"Podverse - Livestream Test\",\n \"notificationType\": \"new-episode\",\n \"timeSent\": \"2022-12-12T03:06:42.615Z\"\n }\n}" + "raw": "{\n \"to\" : \"\",\n \"notification\": {\n \"body\": \"Podverse - Livestream Test\",\n \"title\": \"LIVE: Podverse - Test Feed - Livestream Solo\",\n \"podcastId\": \"x6tZlp8gL5Y\",\n \"episodeId\": \"M_tYcPb7b\",\n \"podcastTitle\": \"Podverse - Test Feed - Livestream Solo\",\n \"episodeTitle\": \"Podverse - Livestream Test\",\n \"notificationType\": \"live\",\n \"timeSent\": \"2022-12-12T03:06:42.615Z\",\n \"image\": \"https://images.podverse.fm/podcast-images/x6tZlp8gL5Y/podversetestfeedlivestreamsolo-14.jpg\"\n },\n \"data\": {\n \"body\": \"Podverse - Livestream Test\",\n \"title\": \"LIVE: Podverse - Test Feed - Livestream Solo\",\n \"podcastId\": \"x6tZlp8gL5Y\",\n \"episodeId\": \"M_tYcPb7b\",\n \"podcastTitle\": \"Podverse - Test Feed - Livestream Solo\",\n \"episodeTitle\": \"Podverse - Livestream Test\",\n \"notificationType\": \"new-episode\",\n \"timeSent\": \"2022-12-12T03:06:42.615Z\"\n }\n}" }, "url": { "raw": "https://fcm.googleapis.com/fcm/send", From 87a50a99dda7610d1c184928f41303bd9dd17901 Mon Sep 17 00:00:00 2001 From: Mitch Downey Date: Sun, 23 Jul 2023 10:41:07 -0500 Subject: [PATCH 06/11] Update upDevice.ts --- src/routes/upDevice.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/upDevice.ts b/src/routes/upDevice.ts index c88dce27..bf45041c 100644 --- a/src/routes/upDevice.ts +++ b/src/routes/upDevice.ts @@ -78,7 +78,6 @@ const deleteUPDeviceLimiter = RateLimit.middleware({ // Delete an UPDevice for a logged-in user router.post('/delete', deleteUPDeviceLimiter, jwtAuth, validateUPDeviceDelete, async (ctx) => { try { - console.log('wtffffff') const { upEndpoint } = ctx.request.body as any await deleteUPDevice(upEndpoint, ctx.state.user.id) ctx.status = 200 From f993cee7b49091b03ff8aac3a48ded3f2655d6ba Mon Sep 17 00:00:00 2001 From: Alecks Gates Date: Sun, 23 Jul 2023 16:53:17 -0500 Subject: [PATCH 07/11] WIP implementation for sending UP notifications from the parser --- package.json | 2 + src/controllers/upDevice.ts | 19 ++++ src/entities/upDevice.ts | 6 + src/lib/notifications/fcmGoogleApi.ts | 29 ++--- src/lib/notifications/notifications.ts | 22 ++++ .../notifications/sendNotificationOptions.ts | 8 ++ src/lib/notifications/unifiedPush.ts | 106 ++++++++++++++++++ src/services/parser.ts | 37 +++--- tsconfig.json | 2 +- yarn.lock | 49 +++++++- 10 files changed, 241 insertions(+), 39 deletions(-) create mode 100644 src/lib/notifications/notifications.ts create mode 100644 src/lib/notifications/sendNotificationOptions.ts create mode 100644 src/lib/notifications/unifiedPush.ts diff --git a/package.json b/package.json index 97be859c..7050a3f0 100644 --- a/package.json +++ b/package.json @@ -133,6 +133,7 @@ "@types/passport-local": "1.0.33", "@types/shelljs": "0.8.6", "@types/validator": "12.0.1", + "@types/web-push": "^3.3.2", "koa2-swagger-ui": "2.15.4", "nodemon": "2.0.1", "prettier": "^2.5.1", @@ -211,6 +212,7 @@ "uuid": "3.3.3", "valid-url": "1.0.9", "validator": "13.7.0", + "web-push": "^3.6.3", "webpack": "4.41.2", "winston": "3.2.1", "ws": "^8.5.0" diff --git a/src/controllers/upDevice.ts b/src/controllers/upDevice.ts index c73c20a2..9a38a197 100644 --- a/src/controllers/upDevice.ts +++ b/src/controllers/upDevice.ts @@ -1,6 +1,7 @@ import { getRepository } from 'typeorm' import { UPDevice, Notification } from '~/entities' import { getLoggedInUser } from './user' +import { UPEndpointData } from '~/entities/upDevice' const createError = require('http-errors') export const createUPDevice = async ({ @@ -158,3 +159,21 @@ export const getUPEndpointsForPodcastId = async (podcastId: string) => { return upEndpoints } + +export const getUPDevicesForPodcastId = async ( + podcastId: string + ): Promise => { + if (!podcastId) { + throw new createError.BadRequest('A podcastId but be provided.') + } + + const repository = getRepository(Notification) + return await repository + .createQueryBuilder('notifications') + .select( + '"upDevices"."upEndpoint" AS "upEndpoint", "upDevices"."upPublicKey" AS "upPublicKey", "upDevices"."upAuthKey" AS "upAuthKey"', + ) + .innerJoin(UPDevice, 'fcmDevices', 'notifications."userId" = "upDevices"."userId"') + .where('notifications."podcastId" = :podcastId', { podcastId }) + .getRawMany() +} \ No newline at end of file diff --git a/src/entities/upDevice.ts b/src/entities/upDevice.ts index e1489ffc..55a4a365 100644 --- a/src/entities/upDevice.ts +++ b/src/entities/upDevice.ts @@ -3,6 +3,12 @@ import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm' import { User } from '~/entities' +export interface UPEndpointData { + upEndpoint: string + upPublicKey: string + upAuthKey: string +} + @Entity('upDevices') export class UPDevice { @PrimaryColumn() diff --git a/src/lib/notifications/fcmGoogleApi.ts b/src/lib/notifications/fcmGoogleApi.ts index 2d7d5c95..c94e1f7f 100644 --- a/src/lib/notifications/fcmGoogleApi.ts +++ b/src/lib/notifications/fcmGoogleApi.ts @@ -2,21 +2,18 @@ import { request } from '../request' import { config } from '~/config' import { getFCMTokensForPodcastId } from '~/controllers/fcmDevice' +import { SendNotificationOptions } from './sendNotificationOptions' const { fcmGoogleApiAuthToken } = config const fcmGoogleApiPath = 'https://fcm.googleapis.com/fcm/send' -export const sendNewEpisodeDetectedNotification = async ( - podcastId: string, - podcastTitle?: string, - episodeTitle?: string, - podcastImage?: string, - episodeImage?: string, - episodeId?: string +export const sendFcmNewEpisodeDetectedNotification = async ( + options: SendNotificationOptions ) => { + const { podcastId, podcastImage, episodeImage, episodeId } = options const fcmTokens = await getFCMTokensForPodcastId(podcastId) - podcastTitle = podcastTitle || 'Untitled Podcast' - episodeTitle = episodeTitle || 'Untitled Episode' + const podcastTitle = options.podcastTitle || 'Untitled Podcast' + const episodeTitle = options.episodeTitle || 'Untitled Episode' const title = podcastTitle const body = episodeTitle return sendFCMGoogleApiNotification( @@ -33,17 +30,13 @@ export const sendNewEpisodeDetectedNotification = async ( ) } -export const sendLiveItemLiveDetectedNotification = async ( - podcastId: string, - podcastTitle?: string, - episodeTitle?: string, - podcastImage?: string, - episodeImage?: string, - episodeId?: string +export const sendFcmLiveItemLiveDetectedNotification = async ( + options: SendNotificationOptions ) => { + const { podcastId, podcastImage, episodeImage, episodeId } = options const fcmTokens = await getFCMTokensForPodcastId(podcastId) - podcastTitle = podcastTitle || 'Untitled Podcast' - episodeTitle = episodeTitle || 'Livestream starting' + const podcastTitle = options.podcastTitle || 'Untitled Podcast' + const episodeTitle = options.episodeTitle || 'Livestream starting' const title = `LIVE: ${podcastTitle}` const body = episodeTitle return sendFCMGoogleApiNotification( diff --git a/src/lib/notifications/notifications.ts b/src/lib/notifications/notifications.ts new file mode 100644 index 00000000..7defa2d7 --- /dev/null +++ b/src/lib/notifications/notifications.ts @@ -0,0 +1,22 @@ +import { SendNotificationOptions } from "./sendNotificationOptions" +import { sendFcmLiveItemLiveDetectedNotification, sendFcmNewEpisodeDetectedNotification } from "./fcmGoogleApi" +import { sendUpLiveItemLiveDetectedNotification, sendUpNewEpisodeDetectedNotification } from "./unifiedPush" + + +export const sendNewEpisodeDetectedNotification = async ( + options: SendNotificationOptions +) => { + return Promise.all([ + sendFcmNewEpisodeDetectedNotification(options), + sendUpNewEpisodeDetectedNotification(options) + ]) +} + +export const sendLiveItemLiveDetectedNotification = async ( + options: SendNotificationOptions +) => { + return Promise.all([ + sendFcmLiveItemLiveDetectedNotification(options), + sendUpLiveItemLiveDetectedNotification(options) + ]) +} \ No newline at end of file diff --git a/src/lib/notifications/sendNotificationOptions.ts b/src/lib/notifications/sendNotificationOptions.ts new file mode 100644 index 00000000..e2dcb3d9 --- /dev/null +++ b/src/lib/notifications/sendNotificationOptions.ts @@ -0,0 +1,8 @@ +export interface SendNotificationOptions { + podcastId: string; + podcastTitle?: string; + episodeTitle?: string; + podcastImage?: string; + episodeImage?: string; + episodeId?: string; +} diff --git a/src/lib/notifications/unifiedPush.ts b/src/lib/notifications/unifiedPush.ts new file mode 100644 index 00000000..0e000298 --- /dev/null +++ b/src/lib/notifications/unifiedPush.ts @@ -0,0 +1,106 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { SendNotificationOptions } from './sendNotificationOptions' +import { getUPDevicesForPodcastId } from '~/controllers/upDevice' +import { UPEndpointData } from '~/entities/upDevice' +import webpush from 'web-push' + + +export const sendUpNewEpisodeDetectedNotification = async ( + options: SendNotificationOptions +) => { + const { podcastId, podcastImage, episodeImage, episodeId } = options + const upDevices = await getUPDevicesForPodcastId(podcastId) + const podcastTitle = options.podcastTitle || 'Untitled Podcast' + const episodeTitle = options.episodeTitle || 'Untitled Episode' + const title = podcastTitle + const body = episodeTitle + return sendUPNotification( + upDevices, + title, + body, + podcastId, + 'new-episode', + podcastTitle, + episodeTitle, + podcastImage, + episodeImage, + episodeId + ) +} + +export const sendUpLiveItemLiveDetectedNotification = async ( + options: SendNotificationOptions +) => { + const { podcastId, podcastImage, episodeImage, episodeId } = options + const upDevices = await getUPDevicesForPodcastId(podcastId) + const podcastTitle = options.podcastTitle || 'Untitled Podcast' + const episodeTitle = options.episodeTitle || 'Livestream starting' + const title = `LIVE: ${podcastTitle}` + const body = episodeTitle + return sendUPNotification( + upDevices, + title, + body, + podcastId, + 'live', + podcastTitle, + episodeTitle, + podcastImage, + episodeImage, + episodeId + ) +} + +export const sendUPNotification = async ( + upDevices: UPEndpointData[], + title: string, + body: string, + podcastId: string, + notificationType: 'live' | 'new-episode', + podcastTitle: string, + episodeTitle: string, + podcastImage?: string, + episodeImage?: string, + episodeId?: string +) => { + if (!upDevices || upDevices.length === 0) return + + const upDeviceBatches: UPEndpointData[][] = [] + const size = 100 + for (let i = 0; i < upDevices.length; i += size) { + upDeviceBatches.push(upDevices.slice(i, i + size)) + } + + const data = { + body, + title, + podcastId, + episodeId, + podcastTitle: podcastTitle, + episodeTitle: episodeTitle, + notificationType, + timeSent: (new Date()).toISOString(), + image: episodeImage || podcastImage + } + const jsonPayload = JSON.stringify(data) + + + for (const upDeviceBatch of upDeviceBatches) { + if (upDeviceBatch?.length > 0) { + try { + await Promise.all(upDeviceBatch.map((upd: UPEndpointData) => webpush.sendNotification( + { + endpoint: upd.upEndpoint, + keys: { + auth: upd.upAuthKey, + p256dh: upd.upPublicKey + } + }, + jsonPayload + ))) + } catch (error) { + console.log('sendUPNotification error', error) + } + } + } +} diff --git a/src/services/parser.ts b/src/services/parser.ts index 07ddb492..6e23d335 100644 --- a/src/services/parser.ts +++ b/src/services/parser.ts @@ -28,8 +28,8 @@ import { getFeedUrls, getFeedUrlsByPodcastIndexIds } from '~/controllers/feedUrl import { shrinkImage } from './imageShrinker' import { Phase4PodcastLiveItem } from 'podcast-partytime/dist/parser/phase/phase-4' import { - sendLiveItemLiveDetectedNotification, - sendNewEpisodeDetectedNotification + sendFcmLiveItemLiveDetectedNotification, + sendFcmNewEpisodeDetectedNotification } from '~/lib/notifications/fcmGoogleApi' import { getAllEpisodeValueTagsFromPodcastIndexById, @@ -42,6 +42,7 @@ import { } from '~/controllers/episode' import { getLiveItemByGuid } from '~/controllers/liveItem' import { PhasePendingChat } from 'podcast-partytime/dist/parser/phase/phase-pending' +import { sendLiveItemLiveDetectedNotification, sendNewEpisodeDetectedNotification } from '~/lib/notifications/notifications' const { awsConfig, userAgent } = config const { queueUrls /*, s3ImageLimitUpdateDays */ } = awsConfig @@ -521,14 +522,14 @@ export const parseFeedUrl = async (feedUrl, forceReparsing = false, cacheBust = const latestEpisodeWithId = await getEpisodeByPodcastIdAndGuid(podcast.id, latestEpisodeGuid) if (latestEpisodeWithId?.id) { - await sendNewEpisodeDetectedNotification( - podcast.id, - podcast.title, - podcast.lastEpisodeTitle, - finalPodcastImageUrl, - finalEpisodeImageUrl, - latestEpisodeWithId.id - ) + await sendNewEpisodeDetectedNotification({ + podcastId: podcast.id, + podcastTitle: podcast.title, + episodeTitle: podcast.lastEpisodeTitle, + podcastImage: finalPodcastImageUrl, + episodeImage: finalEpisodeImageUrl, + episodeId: latestEpisodeWithId.id + }) } logPerformance('sendNewEpisodeDetectedNotification', _logEnd) @@ -542,14 +543,14 @@ export const parseFeedUrl = async (feedUrl, forceReparsing = false, cacheBust = const liveItemWithId = await getLiveItemByGuid(liveItemNotificationData.episodeGuid, podcast.id) if (liveItemWithId?.episode?.id) { - await sendLiveItemLiveDetectedNotification( - liveItemNotificationData.podcastId, - liveItemNotificationData.podcastTitle, - liveItemNotificationData.episodeTitle, - liveItemNotificationData.podcastImageUrl, - liveItemNotificationData.episodeImageUrl, - liveItemWithId?.episode?.id - ) + await sendLiveItemLiveDetectedNotification({ + podcastId: liveItemNotificationData.podcastId, + podcastTitle: liveItemNotificationData.podcastTitle, + episodeTitle: liveItemNotificationData.episodeTitle, + podcastImage: liveItemNotificationData.podcastImageUrl, + episodeImage: liveItemNotificationData.episodeImageUrl, + episodeId: liveItemWithId?.episode?.id + }) } else { console.log('not found: liveItemWithId not found', liveItemNotificationData.episodeGuid, podcast.id) } diff --git a/tsconfig.json b/tsconfig.json index 6f1b504a..234a8f7e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -41,6 +41,6 @@ "node_modules" ], "files": [ - "global.d.ts" + "global.d.ts", "src/lib/notifications/sendNotificationOptions.ts" ] } diff --git a/yarn.lock b/yarn.lock index 05295d74..b94bbe09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1124,6 +1124,13 @@ resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.7.10.tgz#f9763dc0933f8324920afa9c0790308eedf55ca7" integrity sha512-t1yxFAR2n0+VO6hd/FJ9F2uezAZVWHLmpmlJzm1eX03+H7+HsuTAp7L8QJs+2pQCfWkP1+EXsGK9Z9v7o/qPVQ== +"@types/web-push@^3.3.2": + version "3.3.2" + resolved "https://registry.yarnpkg.com/@types/web-push/-/web-push-3.3.2.tgz#8c32434147c0396415862e86405c9edc9c50fc15" + integrity sha512-JxWGVL/m7mWTIg4mRYO+A6s0jPmBkr4iJr39DqJpRJAc+jrPiEe1/asmkwerzRon8ZZDxaZJpsxpv0Z18Wo9gw== + dependencies: + "@types/node" "*" + "@types/webpack-sources@*": version "3.2.0" resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-3.2.0.tgz#16d759ba096c289034b26553d2df1bf45248d38b" @@ -1431,6 +1438,13 @@ agent-base@6: dependencies: debug "4" +agent-base@^7.0.2: + version "7.1.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.0.tgz#536802b76bc0b34aa50195eb2442276d613e3434" + integrity sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg== + dependencies: + debug "^4.3.4" + aggregate-error@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-1.0.0.tgz#888344dad0220a72e3af50906117f48771925fac" @@ -1688,7 +1702,7 @@ arrify@^2.0.0: resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== -asn1.js@^5.2.0: +asn1.js@^5.2.0, asn1.js@^5.3.0: version "5.4.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== @@ -3058,7 +3072,7 @@ debug@3.1.0, debug@~3.1.0: dependencies: ms "2.0.0" -debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: +debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -4929,6 +4943,13 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +http_ece@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/http_ece/-/http_ece-1.1.0.tgz#74780c6eb32d8ddfe9e36a83abcd81fe0cd4fb75" + integrity sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA== + dependencies: + urlsafe-base64 "~1.0.0" + https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" @@ -4942,6 +4963,14 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" +https-proxy-agent@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.1.tgz#0277e28f13a07d45c663633841e20a40aaafe0ab" + integrity sha512-Eun8zV0kcYS1g19r78osiQLEFIRspRUDd9tIfBCTBPBeMieF/EsJNL8VI3xOIdYRDEkjQnqOYPsZ2DsWsVsFwQ== + dependencies: + agent-base "^7.0.2" + debug "4" + human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -10168,6 +10197,11 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +urlsafe-base64@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz#23f89069a6c62f46cf3a1d3b00169cefb90be0c6" + integrity sha512-RtuPeMy7c1UrHwproMZN9gN6kiZ0SvJwRaEzwZY0j9MypEkFqyBaKv176jvlPtg58Zh36bOkS0NFABXMHvvGCA== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" @@ -10312,6 +10346,17 @@ watchpack@^1.6.0: chokidar "^3.4.1" watchpack-chokidar2 "^2.0.1" +web-push@^3.6.3: + version "3.6.3" + resolved "https://registry.yarnpkg.com/web-push/-/web-push-3.6.3.tgz#8a68509dcff70f74a2aeeed9dad5447c9414aa3d" + integrity sha512-3RlA0lRmLcwlHCRR94Tz+Fw6wPtm0lFm8oyukQunlEIarANxE84Ox9XBgF4+jNlXgO40DIwblOiC43oR46helA== + dependencies: + asn1.js "^5.3.0" + http_ece "1.1.0" + https-proxy-agent "^7.0.0" + jws "^4.0.0" + minimist "^1.2.5" + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" From 53d12c4892c67b73dfb827b28cdadae2f85115d5 Mon Sep 17 00:00:00 2001 From: Mitch Downey Date: Sun, 23 Jul 2023 22:48:10 -0500 Subject: [PATCH 08/11] Fixes to UP notifications from the parser --- src/controllers/upDevice.ts | 10 +++----- src/lib/notifications/unifiedPush.ts | 38 ++++++++++++++-------------- src/lib/utility/promise.ts | 5 ++++ src/services/parser.ts | 19 +++++++------- 4 files changed, 38 insertions(+), 34 deletions(-) create mode 100644 src/lib/utility/promise.ts diff --git a/src/controllers/upDevice.ts b/src/controllers/upDevice.ts index 9a38a197..56b47bf5 100644 --- a/src/controllers/upDevice.ts +++ b/src/controllers/upDevice.ts @@ -160,9 +160,7 @@ export const getUPEndpointsForPodcastId = async (podcastId: string) => { return upEndpoints } -export const getUPDevicesForPodcastId = async ( - podcastId: string - ): Promise => { +export const getUPDevicesForPodcastId = async (podcastId: string): Promise => { if (!podcastId) { throw new createError.BadRequest('A podcastId but be provided.') } @@ -171,9 +169,9 @@ export const getUPDevicesForPodcastId = async ( return await repository .createQueryBuilder('notifications') .select( - '"upDevices"."upEndpoint" AS "upEndpoint", "upDevices"."upPublicKey" AS "upPublicKey", "upDevices"."upAuthKey" AS "upAuthKey"', + '"upDevices"."upEndpoint" AS "upEndpoint", "upDevices"."upPublicKey" AS "upPublicKey", "upDevices"."upAuthKey" AS "upAuthKey"' ) - .innerJoin(UPDevice, 'fcmDevices', 'notifications."userId" = "upDevices"."userId"') + .innerJoin(UPDevice, 'upDevices', 'notifications."userId" = "upDevices"."userId"') .where('notifications."podcastId" = :podcastId', { podcastId }) .getRawMany() -} \ No newline at end of file +} diff --git a/src/lib/notifications/unifiedPush.ts b/src/lib/notifications/unifiedPush.ts index 0e000298..c9820173 100644 --- a/src/lib/notifications/unifiedPush.ts +++ b/src/lib/notifications/unifiedPush.ts @@ -2,12 +2,11 @@ import { SendNotificationOptions } from './sendNotificationOptions' import { getUPDevicesForPodcastId } from '~/controllers/upDevice' import { UPEndpointData } from '~/entities/upDevice' -import webpush from 'web-push' +import { promiseAllSkippingErrors } from '~/lib/utility/promise' +const webpush = require('web-push') -export const sendUpNewEpisodeDetectedNotification = async ( - options: SendNotificationOptions -) => { +export const sendUpNewEpisodeDetectedNotification = async (options: SendNotificationOptions) => { const { podcastId, podcastImage, episodeImage, episodeId } = options const upDevices = await getUPDevicesForPodcastId(podcastId) const podcastTitle = options.podcastTitle || 'Untitled Podcast' @@ -28,9 +27,7 @@ export const sendUpNewEpisodeDetectedNotification = async ( ) } -export const sendUpLiveItemLiveDetectedNotification = async ( - options: SendNotificationOptions -) => { +export const sendUpLiveItemLiveDetectedNotification = async (options: SendNotificationOptions) => { const { podcastId, podcastImage, episodeImage, episodeId } = options const upDevices = await getUPDevicesForPodcastId(podcastId) const podcastTitle = options.podcastTitle || 'Untitled Podcast' @@ -79,25 +76,28 @@ export const sendUPNotification = async ( podcastTitle: podcastTitle, episodeTitle: episodeTitle, notificationType, - timeSent: (new Date()).toISOString(), + timeSent: new Date().toISOString(), image: episodeImage || podcastImage } const jsonPayload = JSON.stringify(data) - for (const upDeviceBatch of upDeviceBatches) { if (upDeviceBatch?.length > 0) { try { - await Promise.all(upDeviceBatch.map((upd: UPEndpointData) => webpush.sendNotification( - { - endpoint: upd.upEndpoint, - keys: { - auth: upd.upAuthKey, - p256dh: upd.upPublicKey - } - }, - jsonPayload - ))) + await promiseAllSkippingErrors( + upDeviceBatch.map((upd: UPEndpointData) => + webpush.sendNotification( + { + endpoint: upd.upEndpoint, + keys: { + auth: upd.upAuthKey, + p256dh: upd.upPublicKey + } + }, + jsonPayload + ) + ) + ) } catch (error) { console.log('sendUPNotification error', error) } diff --git a/src/lib/utility/promise.ts b/src/lib/utility/promise.ts new file mode 100644 index 00000000..5278c2dd --- /dev/null +++ b/src/lib/utility/promise.ts @@ -0,0 +1,5 @@ +// Helper function by salezica on Stack Overflow +// https://stackoverflow.com/a/43503921/2608858 +export const promiseAllSkippingErrors = (promises) => { + return Promise.all(promises.map((p) => p.catch((error) => null))) +} diff --git a/src/services/parser.ts b/src/services/parser.ts index 6e23d335..aa14cbf8 100644 --- a/src/services/parser.ts +++ b/src/services/parser.ts @@ -27,10 +27,6 @@ import { deleteMessage, receiveMessageFromQueue, sendMessageToQueue } from '~/se import { getFeedUrls, getFeedUrlsByPodcastIndexIds } from '~/controllers/feedUrl' import { shrinkImage } from './imageShrinker' import { Phase4PodcastLiveItem } from 'podcast-partytime/dist/parser/phase/phase-4' -import { - sendFcmLiveItemLiveDetectedNotification, - sendFcmNewEpisodeDetectedNotification -} from '~/lib/notifications/fcmGoogleApi' import { getAllEpisodeValueTagsFromPodcastIndexById, getPodcastValueTagForPodcastIndexId @@ -42,7 +38,10 @@ import { } from '~/controllers/episode' import { getLiveItemByGuid } from '~/controllers/liveItem' import { PhasePendingChat } from 'podcast-partytime/dist/parser/phase/phase-pending' -import { sendLiveItemLiveDetectedNotification, sendNewEpisodeDetectedNotification } from '~/lib/notifications/notifications' +import { + sendLiveItemLiveDetectedNotification, + sendNewEpisodeDetectedNotification +} from '~/lib/notifications/notifications' const { awsConfig, userAgent } = config const { queueUrls /*, s3ImageLimitUpdateDays */ } = awsConfig @@ -302,17 +301,19 @@ export const parseFeedUrl = async (feedUrl, forceReparsing = false, cacheBust = const latestLiveItemStatus = parseLatestLiveItemStatus(parsedLiveItemEpisodes) const { liveItemLatestPubDate } = parseLatestLiveItemInfo(parsedLiveItemEpisodes) - const { mostRecentEpisodeTitle, mostRecentEpisodePubDate, mostRecentUpdateDateFromFeed } = + const { /* mostRecentEpisodeTitle, */ mostRecentEpisodePubDate, mostRecentUpdateDateFromFeed } = getMostRecentEpisodeDataFromFeed(meta, parsedEpisodes) const previousLastEpisodePubDate = podcast.lastEpisodePubDate - const previousLastEpisodeTitle = podcast.lastEpisodeTitle + // const previousLastEpisodeTitle = podcast.lastEpisodeTitle const shouldSendNewEpisodeNotification = (!previousLastEpisodePubDate && mostRecentEpisodePubDate) || (previousLastEpisodePubDate && mostRecentEpisodePubDate && - new Date(previousLastEpisodePubDate) < new Date(mostRecentEpisodePubDate) && - previousLastEpisodeTitle !== mostRecentEpisodeTitle) + new Date(previousLastEpisodePubDate) < new Date(mostRecentEpisodePubDate)) + // NOTE: I disabled this condition because it seems unnecessary... + // but I must have added it for a reason at some point... + // && previousLastEpisodeTitle !== mostRecentEpisodeTitle // Stop parsing if the feed has not been updated since it was last parsed. if ( From 1ec3d8ad72ef2a3be20ddcc5c588a35f3e5a5395 Mon Sep 17 00:00:00 2001 From: Mitch Downey Date: Sun, 23 Jul 2023 22:48:29 -0500 Subject: [PATCH 09/11] Linter fixes --- src/lib/notifications/fcmGoogleApi.ts | 8 ++----- src/lib/notifications/notifications.ts | 22 ++++++------------- .../notifications/sendNotificationOptions.ts | 12 +++++----- 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/src/lib/notifications/fcmGoogleApi.ts b/src/lib/notifications/fcmGoogleApi.ts index c94e1f7f..fba04234 100644 --- a/src/lib/notifications/fcmGoogleApi.ts +++ b/src/lib/notifications/fcmGoogleApi.ts @@ -7,9 +7,7 @@ const { fcmGoogleApiAuthToken } = config const fcmGoogleApiPath = 'https://fcm.googleapis.com/fcm/send' -export const sendFcmNewEpisodeDetectedNotification = async ( - options: SendNotificationOptions -) => { +export const sendFcmNewEpisodeDetectedNotification = async (options: SendNotificationOptions) => { const { podcastId, podcastImage, episodeImage, episodeId } = options const fcmTokens = await getFCMTokensForPodcastId(podcastId) const podcastTitle = options.podcastTitle || 'Untitled Podcast' @@ -30,9 +28,7 @@ export const sendFcmNewEpisodeDetectedNotification = async ( ) } -export const sendFcmLiveItemLiveDetectedNotification = async ( - options: SendNotificationOptions -) => { +export const sendFcmLiveItemLiveDetectedNotification = async (options: SendNotificationOptions) => { const { podcastId, podcastImage, episodeImage, episodeId } = options const fcmTokens = await getFCMTokensForPodcastId(podcastId) const podcastTitle = options.podcastTitle || 'Untitled Podcast' diff --git a/src/lib/notifications/notifications.ts b/src/lib/notifications/notifications.ts index 7defa2d7..03d1d767 100644 --- a/src/lib/notifications/notifications.ts +++ b/src/lib/notifications/notifications.ts @@ -1,22 +1,14 @@ -import { SendNotificationOptions } from "./sendNotificationOptions" -import { sendFcmLiveItemLiveDetectedNotification, sendFcmNewEpisodeDetectedNotification } from "./fcmGoogleApi" -import { sendUpLiveItemLiveDetectedNotification, sendUpNewEpisodeDetectedNotification } from "./unifiedPush" +import { SendNotificationOptions } from './sendNotificationOptions' +import { sendFcmLiveItemLiveDetectedNotification, sendFcmNewEpisodeDetectedNotification } from './fcmGoogleApi' +import { sendUpLiveItemLiveDetectedNotification, sendUpNewEpisodeDetectedNotification } from './unifiedPush' - -export const sendNewEpisodeDetectedNotification = async ( - options: SendNotificationOptions -) => { - return Promise.all([ - sendFcmNewEpisodeDetectedNotification(options), - sendUpNewEpisodeDetectedNotification(options) - ]) +export const sendNewEpisodeDetectedNotification = async (options: SendNotificationOptions) => { + return Promise.all([sendFcmNewEpisodeDetectedNotification(options), sendUpNewEpisodeDetectedNotification(options)]) } -export const sendLiveItemLiveDetectedNotification = async ( - options: SendNotificationOptions -) => { +export const sendLiveItemLiveDetectedNotification = async (options: SendNotificationOptions) => { return Promise.all([ sendFcmLiveItemLiveDetectedNotification(options), sendUpLiveItemLiveDetectedNotification(options) ]) -} \ No newline at end of file +} diff --git a/src/lib/notifications/sendNotificationOptions.ts b/src/lib/notifications/sendNotificationOptions.ts index e2dcb3d9..b38a8bc9 100644 --- a/src/lib/notifications/sendNotificationOptions.ts +++ b/src/lib/notifications/sendNotificationOptions.ts @@ -1,8 +1,8 @@ export interface SendNotificationOptions { - podcastId: string; - podcastTitle?: string; - episodeTitle?: string; - podcastImage?: string; - episodeImage?: string; - episodeId?: string; + podcastId: string + podcastTitle?: string + episodeTitle?: string + podcastImage?: string + episodeImage?: string + episodeId?: string } From 3185c48251f71fed69fe5f2b22e276f48202ba31 Mon Sep 17 00:00:00 2001 From: Mitch Downey Date: Sun, 23 Jul 2023 22:48:38 -0500 Subject: [PATCH 10/11] Update parser.ts --- src/services/parser.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/parser.ts b/src/services/parser.ts index aa14cbf8..fa70dc2a 100644 --- a/src/services/parser.ts +++ b/src/services/parser.ts @@ -311,9 +311,9 @@ export const parseFeedUrl = async (feedUrl, forceReparsing = false, cacheBust = (previousLastEpisodePubDate && mostRecentEpisodePubDate && new Date(previousLastEpisodePubDate) < new Date(mostRecentEpisodePubDate)) - // NOTE: I disabled this condition because it seems unnecessary... - // but I must have added it for a reason at some point... - // && previousLastEpisodeTitle !== mostRecentEpisodeTitle + // NOTE: I disabled this condition because it seems unnecessary... + // but I must have added it for a reason at some point... + // && previousLastEpisodeTitle !== mostRecentEpisodeTitle // Stop parsing if the feed has not been updated since it was last parsed. if ( From 323df339adfa7e3b5ff7fe5712300985be46e601 Mon Sep 17 00:00:00 2001 From: Mitch Downey Date: Mon, 24 Jul 2023 00:21:48 -0500 Subject: [PATCH 11/11] Bump to version 4.13.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7050a3f0..a51ec903 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "podverse-api", - "version": "4.13.5", + "version": "4.13.6", "description": "Data API, database migration scripts, and backend services for all Podverse models.", "contributors": [ "Mitch Downey"