diff --git a/config.yml b/config.yml index 959d0993..95c20720 100644 --- a/config.yml +++ b/config.yml @@ -34,10 +34,8 @@ logs: # username: elastic-username # password: elastic-password youtube: - clientId: google-client-id - clientSecret: google-client-secret - # adcKeyFilePath: path/to/adc-key-file.json - # maxAllowedQuotaUsageInPercentage: 95 + operationalApi: + url: http://127.0.0.1:8889 proxy: url: socks5://chisel-client:1080 chiselProxy: @@ -52,9 +50,6 @@ httpApi: port: 3001 ownerKey: ypp-owner-key joystream: - faucet: - endpoint: http://localhost:3002/register - captchaBypassKey: some-random-key app: name: app-name accountSeed: 'example_string_seed' diff --git a/docker-compose.yml b/docker-compose.yml index 07de3b4d..074fabc9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,8 +28,7 @@ services: YT_SYNCH__ENDPOINTS__REDIS__PORT: ${YT_SYNCH__ENDPOINTS__REDIS__PORT} # YT_SYNCH__HTTP_API__PORT: ${YT_SYNCH__HTTP_API__PORT} # YT_SYNCH__HTTP_API__OWNER_KEY: ${YT_SYNCH__HTTP_API__OWNER_KEY} - # YT_SYNCH__YOUTUBE__CLIENT_ID: ${YT_SYNCH__YOUTUBE__CLIENT_ID} - # YT_SYNCH__YOUTUBE__CLIENT_SECRET: ${YT_SYNCH__YOUTUBE__CLIENT_SECRET} + YT_SYNCH__YOUTUBE__OPERATIONAL_API__URL: http://youtube-operational-api # YT_SYNCH__AWS__ENDPOINT: http://host.docker.internal:4566 # YT_SYNCH__AWS__CREDENTIALS__ACCESS_KEY_ID: ${YT_SYNCH__AWS__CREDENTIALS__ACCESS_KEY_ID} # YT_SYNCH__AWS__CREDENTIALS__SECRET_ACCESS_KEY: ${YT_SYNCH__AWS__CREDENTIALS__SECRET_ACCESS_KEY} @@ -37,8 +36,6 @@ services: # YT_SYNCH__JOYSTREAM__CHANNEL_COLLABORATOR__MEMBER_ID: ${YT_SYNCH__JOYSTREAM__CHANNEL_COLLABORATOR__MEMBER_ID} # YT_SYNCH__JOYSTREAM__APP__NAME: ${YT_SYNCH__JOYSTREAM__APP__NAME} # YT_SYNCH__JOYSTREAM__APP__ACCOUNT_SEED: ${YT_SYNCH__JOYSTREAM__APP__ACCOUNT_SEED} - # YT_SYNCH__JOYSTREAM__FAUCET__ENDPOINT: ${YT_SYNCH__JOYSTREAM__FAUCET__ENDPOINT} - # YT_SYNCH__JOYSTREAM__FAUCET__CAPTCHA_BYPASS_KEY: ${YT_SYNCH__JOYSTREAM__FAUCET__CAPTCHA_BYPASS_KEY} OTEL_EXPORTER_OTLP_ENDPOINT: ${TELEMETRY_ENDPOINT} ports: - 127.0.0.1:${YT_SYNCH__HTTP_API__PORT}:${YT_SYNCH__HTTP_API__PORT} @@ -78,17 +75,14 @@ services: YT_SYNCH__ENDPOINTS__REDIS__PORT: ${YT_SYNCH__ENDPOINTS__REDIS__PORT} # YT_SYNCH__HTTP_API__PORT: ${YT_SYNCH__HTTP_API__PORT} # YT_SYNCH__HTTP_API__OWNER_KEY: ${YT_SYNCH__HTTP_API__OWNER_KEY} - # YT_SYNCH__YOUTUBE__CLIENT_ID: ${YT_SYNCH__YOUTUBE__CLIENT_ID} - # YT_SYNCH__YOUTUBE__CLIENT_SECRET: ${YT_SYNCH__YOUTUBE__CLIENT_SECRET} - # YT_SYNCH__AWS__ENDPOINT: http://host.docker.internal:4566 + YT_SYNCH__YOUTUBE__OPERATIONAL_API__URL: http://youtube-operational-api + # YT_SYNCH__AWS__ENDPOINT: ${YT_SYNCH__AWS__ENDPOINT} # YT_SYNCH__AWS__CREDENTIALS__ACCESS_KEY_ID: ${YT_SYNCH__AWS__CREDENTIALS__ACCESS_KEY_ID} # YT_SYNCH__AWS__CREDENTIALS__SECRET_ACCESS_KEY: ${YT_SYNCH__AWS__CREDENTIALS__SECRET_ACCESS_KEY} # YT_SYNCH__JOYSTREAM__CHANNEL_COLLABORATOR__ACCOUNT: ${YT_SYNCH__JOYSTREAM__CHANNEL_COLLABORATOR__ACCOUNT} # YT_SYNCH__JOYSTREAM__CHANNEL_COLLABORATOR__MEMBER_ID: ${YT_SYNCH__JOYSTREAM__CHANNEL_COLLABORATOR__MEMBER_ID} # YT_SYNCH__JOYSTREAM__APP__NAME: ${YT_SYNCH__JOYSTREAM__APP__NAME} # YT_SYNCH__JOYSTREAM__APP__ACCOUNT_SEED: ${YT_SYNCH__JOYSTREAM__APP__ACCOUNT_SEED} - # YT_SYNCH__JOYSTREAM__FAUCET__ENDPOINT: ${YT_SYNCH__JOYSTREAM__FAUCET__ENDPOINT} - # YT_SYNCH__JOYSTREAM__FAUCET__CAPTCHA_BYPASS_KEY: ${YT_SYNCH__JOYSTREAM__FAUCET__CAPTCHA_BYPASS_KEY} networks: - youtube-synch command: ['./bin/run', 'start', '--service', 'sync'] @@ -103,6 +97,13 @@ services: - youtube-synch volumes: - redis-data:/data + youtube-operational-api: + image: benjaminloison/youtube-operational-api + container_name: youtube-operational-api + ports: + - 127.0.0.1:8889:80 + networks: + - youtube-synch volumes: logs: diff --git a/docs/config/definition-properties-joystream-properties-faucet-properties-captchabypasskey.md b/docs/config/definition-properties-joystream-properties-faucet-properties-captchabypasskey.md deleted file mode 100644 index c450d7b9..00000000 --- a/docs/config/definition-properties-joystream-properties-faucet-properties-captchabypasskey.md +++ /dev/null @@ -1,3 +0,0 @@ -## captchaBypassKey Type - -`string` diff --git a/docs/config/definition-properties-joystream-properties-faucet-properties-endpoint.md b/docs/config/definition-properties-joystream-properties-faucet-properties-endpoint.md deleted file mode 100644 index 00e8b7f7..00000000 --- a/docs/config/definition-properties-joystream-properties-faucet-properties-endpoint.md +++ /dev/null @@ -1,3 +0,0 @@ -## endpoint Type - -`string` diff --git a/docs/config/definition-properties-joystream-properties-faucet.md b/docs/config/definition-properties-joystream-properties-faucet.md deleted file mode 100644 index 3abaa799..00000000 --- a/docs/config/definition-properties-joystream-properties-faucet.md +++ /dev/null @@ -1,46 +0,0 @@ -## faucet Type - -`object` ([Details](definition-properties-joystream-properties-faucet.md)) - -# faucet Properties - -| Property | Type | Required | Nullable | Defined by | -| :------------------------------------ | :------- | :------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [endpoint](#endpoint) | `string` | Required | cannot be null | [Youtube Sync node configuration](definition-properties-joystream-properties-faucet-properties-endpoint.md "https://joystream.org/schemas/youtube-synch/config#/properties/joystream/properties/faucet/properties/endpoint") | -| [captchaBypassKey](#captchabypasskey) | `string` | Required | cannot be null | [Youtube Sync node configuration](definition-properties-joystream-properties-faucet-properties-captchabypasskey.md "https://joystream.org/schemas/youtube-synch/config#/properties/joystream/properties/faucet/properties/captchaBypassKey") | - -## endpoint - -Joystream's faucet URL - -`endpoint` - -* is required - -* Type: `string` - -* cannot be null - -* defined in: [Youtube Sync node configuration](definition-properties-joystream-properties-faucet-properties-endpoint.md "https://joystream.org/schemas/youtube-synch/config#/properties/joystream/properties/faucet/properties/endpoint") - -### endpoint Type - -`string` - -## captchaBypassKey - -Bearer Authentication Key needed to bypass captcha verification on Faucet - -`captchaBypassKey` - -* is required - -* Type: `string` - -* cannot be null - -* defined in: [Youtube Sync node configuration](definition-properties-joystream-properties-faucet-properties-captchabypasskey.md "https://joystream.org/schemas/youtube-synch/config#/properties/joystream/properties/faucet/properties/captchaBypassKey") - -### captchaBypassKey Type - -`string` diff --git a/docs/config/definition-properties-joystream.md b/docs/config/definition-properties-joystream.md index c7b016db..56aab7c2 100644 --- a/docs/config/definition-properties-joystream.md +++ b/docs/config/definition-properties-joystream.md @@ -6,28 +6,9 @@ | Property | Type | Required | Nullable | Defined by | | :------------------------------------------ | :------- | :------- | :------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [faucet](#faucet) | `object` | Required | cannot be null | [Youtube Sync node configuration](definition-properties-joystream-properties-faucet.md "https://joystream.org/schemas/youtube-synch/config#/properties/joystream/properties/faucet") | | [app](#app) | `object` | Required | cannot be null | [Youtube Sync node configuration](definition-properties-joystream-properties-app.md "https://joystream.org/schemas/youtube-synch/config#/properties/joystream/properties/app") | | [channelCollaborator](#channelcollaborator) | `object` | Required | cannot be null | [Youtube Sync node configuration](definition-properties-joystream-properties-joystream-channel-collaborator-used-for-syncing-the-content.md "https://joystream.org/schemas/youtube-synch/config#/properties/joystream/properties/channelCollaborator") | -## faucet - -Joystream's faucet configuration (needed for captcha-free membership creation) - -`faucet` - -* is required - -* Type: `object` ([Details](definition-properties-joystream-properties-faucet.md)) - -* cannot be null - -* defined in: [Youtube Sync node configuration](definition-properties-joystream-properties-faucet.md "https://joystream.org/schemas/youtube-synch/config#/properties/joystream/properties/faucet") - -### faucet Type - -`object` ([Details](definition-properties-joystream-properties-faucet.md)) - ## app Joystream Metaprotocol App specific configuration diff --git a/docs/config/definition-properties-youtube-oauth2-client-configuration-dependencies.md b/docs/config/definition-properties-youtube-oauth2-client-configuration-dependencies.md deleted file mode 100644 index 5fece311..00000000 --- a/docs/config/definition-properties-youtube-oauth2-client-configuration-dependencies.md +++ /dev/null @@ -1,3 +0,0 @@ -## dependencies Type - -unknown diff --git a/docs/config/definition-properties-youtube-oauth2-client-configuration-properties-adckeyfilepath.md b/docs/config/definition-properties-youtube-oauth2-client-configuration-properties-adckeyfilepath.md deleted file mode 100644 index b029962a..00000000 --- a/docs/config/definition-properties-youtube-oauth2-client-configuration-properties-adckeyfilepath.md +++ /dev/null @@ -1,3 +0,0 @@ -## adcKeyFilePath Type - -`string` diff --git a/docs/config/definition-properties-youtube-oauth2-client-configuration-properties-clientid.md b/docs/config/definition-properties-youtube-oauth2-client-configuration-properties-clientid.md deleted file mode 100644 index a7b5d5fb..00000000 --- a/docs/config/definition-properties-youtube-oauth2-client-configuration-properties-clientid.md +++ /dev/null @@ -1,3 +0,0 @@ -## clientId Type - -`string` diff --git a/docs/config/definition-properties-youtube-oauth2-client-configuration-properties-clientsecret.md b/docs/config/definition-properties-youtube-oauth2-client-configuration-properties-clientsecret.md deleted file mode 100644 index 53452148..00000000 --- a/docs/config/definition-properties-youtube-oauth2-client-configuration-properties-clientsecret.md +++ /dev/null @@ -1,3 +0,0 @@ -## clientSecret Type - -`string` diff --git a/docs/config/definition-properties-youtube-oauth2-client-configuration-properties-maxallowedquotausageinpercentage.md b/docs/config/definition-properties-youtube-oauth2-client-configuration-properties-maxallowedquotausageinpercentage.md deleted file mode 100644 index 7c692483..00000000 --- a/docs/config/definition-properties-youtube-oauth2-client-configuration-properties-maxallowedquotausageinpercentage.md +++ /dev/null @@ -1,11 +0,0 @@ -## maxAllowedQuotaUsageInPercentage Type - -`number` - -## maxAllowedQuotaUsageInPercentage Default Value - -The default value is: - -```json -95 -``` diff --git a/docs/config/definition-properties-youtube-oauth2-client-configuration.md b/docs/config/definition-properties-youtube-oauth2-client-configuration.md deleted file mode 100644 index e7b63522..00000000 --- a/docs/config/definition-properties-youtube-oauth2-client-configuration.md +++ /dev/null @@ -1,92 +0,0 @@ -## youtube Type - -`object` ([Youtube Oauth2 Client configuration](definition-properties-youtube-oauth2-client-configuration.md)) - -# youtube Properties - -| Property | Type | Required | Nullable | Defined by | -| :-------------------------------------------------------------------- | :------- | :------- | :------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [clientId](#clientid) | `string` | Required | cannot be null | [Youtube Sync node configuration](definition-properties-youtube-oauth2-client-configuration-properties-clientid.md "https://joystream.org/schemas/youtube-synch/config#/properties/youtube/properties/clientId") | -| [clientSecret](#clientsecret) | `string` | Required | cannot be null | [Youtube Sync node configuration](definition-properties-youtube-oauth2-client-configuration-properties-clientsecret.md "https://joystream.org/schemas/youtube-synch/config#/properties/youtube/properties/clientSecret") | -| [maxAllowedQuotaUsageInPercentage](#maxallowedquotausageinpercentage) | `number` | Optional | cannot be null | [Youtube Sync node configuration](definition-properties-youtube-oauth2-client-configuration-properties-maxallowedquotausageinpercentage.md "https://joystream.org/schemas/youtube-synch/config#/properties/youtube/properties/maxAllowedQuotaUsageInPercentage") | -| [adcKeyFilePath](#adckeyfilepath) | `string` | Optional | cannot be null | [Youtube Sync node configuration](definition-properties-youtube-oauth2-client-configuration-properties-adckeyfilepath.md "https://joystream.org/schemas/youtube-synch/config#/properties/youtube/properties/adcKeyFilePath") | - -## clientId - -Youtube Oauth2 Client Id - -`clientId` - -* is required - -* Type: `string` - -* cannot be null - -* defined in: [Youtube Sync node configuration](definition-properties-youtube-oauth2-client-configuration-properties-clientid.md "https://joystream.org/schemas/youtube-synch/config#/properties/youtube/properties/clientId") - -### clientId Type - -`string` - -## clientSecret - -Youtube Oauth2 Client Secret - -`clientSecret` - -* is required - -* Type: `string` - -* cannot be null - -* defined in: [Youtube Sync node configuration](definition-properties-youtube-oauth2-client-configuration-properties-clientsecret.md "https://joystream.org/schemas/youtube-synch/config#/properties/youtube/properties/clientSecret") - -### clientSecret Type - -`string` - -## maxAllowedQuotaUsageInPercentage - -Maximum percentage of daily Youtube API quota that can be used by the Periodic polling service. Once this limit is reached the service will stop polling for new videos until the next day(when Quota resets). All the remaining quota (100 - maxAllowedQuotaUsageInPercentage) will be used for potential channel's signups. - -`maxAllowedQuotaUsageInPercentage` - -* is optional - -* Type: `number` - -* cannot be null - -* defined in: [Youtube Sync node configuration](definition-properties-youtube-oauth2-client-configuration-properties-maxallowedquotausageinpercentage.md "https://joystream.org/schemas/youtube-synch/config#/properties/youtube/properties/maxAllowedQuotaUsageInPercentage") - -### maxAllowedQuotaUsageInPercentage Type - -`number` - -### maxAllowedQuotaUsageInPercentage Default Value - -The default value is: - -```json -95 -``` - -## adcKeyFilePath - -Path to the Google Cloud's Application Default Credentials (ADC) key file. It is required to periodically monitor the Youtube API quota usage. - -`adcKeyFilePath` - -* is optional - -* Type: `string` - -* cannot be null - -* defined in: [Youtube Sync node configuration](definition-properties-youtube-oauth2-client-configuration-properties-adckeyfilepath.md "https://joystream.org/schemas/youtube-synch/config#/properties/youtube/properties/adcKeyFilePath") - -### adcKeyFilePath Type - -`string` diff --git a/docs/config/definition-properties-youtube-related-configuration-properties-youtube-operational-api-httpsgithubcombenjamin-loisonyoutube-operational-api-configuration-properties-url.md b/docs/config/definition-properties-youtube-related-configuration-properties-youtube-operational-api-httpsgithubcombenjamin-loisonyoutube-operational-api-configuration-properties-url.md new file mode 100644 index 00000000..073bf9cf --- /dev/null +++ b/docs/config/definition-properties-youtube-related-configuration-properties-youtube-operational-api-httpsgithubcombenjamin-loisonyoutube-operational-api-configuration-properties-url.md @@ -0,0 +1,3 @@ +## url Type + +`string` diff --git a/docs/config/definition-properties-youtube-related-configuration-properties-youtube-operational-api-httpsgithubcombenjamin-loisonyoutube-operational-api-configuration.md b/docs/config/definition-properties-youtube-related-configuration-properties-youtube-operational-api-httpsgithubcombenjamin-loisonyoutube-operational-api-configuration.md new file mode 100644 index 00000000..f098361c --- /dev/null +++ b/docs/config/definition-properties-youtube-related-configuration-properties-youtube-operational-api-httpsgithubcombenjamin-loisonyoutube-operational-api-configuration.md @@ -0,0 +1,27 @@ +## operationalApi Type + +`object` ([Youtube Operational API (https://github.com/Benjamin-Loison/YouTube-operational-API) configuration](definition-properties-youtube-related-configuration-properties-youtube-operational-api-httpsgithubcombenjamin-loisonyoutube-operational-api-configuration.md)) + +# operationalApi Properties + +| Property | Type | Required | Nullable | Defined by | +| :---------- | :------- | :------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [url](#url) | `string` | Required | cannot be null | [Youtube Sync node configuration](definition-properties-youtube-related-configuration-properties-youtube-operational-api-httpsgithubcombenjamin-loisonyoutube-operational-api-configuration-properties-url.md "https://joystream.org/schemas/youtube-synch/config#/properties/youtube/properties/operationalApi/properties/url") | + +## url + +URL of the Youtube Operational API server (for example: ) + +`url` + +* is required + +* Type: `string` + +* cannot be null + +* defined in: [Youtube Sync node configuration](definition-properties-youtube-related-configuration-properties-youtube-operational-api-httpsgithubcombenjamin-loisonyoutube-operational-api-configuration-properties-url.md "https://joystream.org/schemas/youtube-synch/config#/properties/youtube/properties/operationalApi/properties/url") + +### url Type + +`string` diff --git a/docs/config/definition-properties-youtube-related-configuration.md b/docs/config/definition-properties-youtube-related-configuration.md new file mode 100644 index 00000000..6f960c10 --- /dev/null +++ b/docs/config/definition-properties-youtube-related-configuration.md @@ -0,0 +1,27 @@ +## youtube Type + +`object` ([Youtube related configuration](definition-properties-youtube-related-configuration.md)) + +# youtube Properties + +| Property | Type | Required | Nullable | Defined by | +| :-------------------------------- | :------- | :------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [operationalApi](#operationalapi) | `object` | Required | cannot be null | [Youtube Sync node configuration](definition-properties-youtube-related-configuration-properties-youtube-operational-api-httpsgithubcombenjamin-loisonyoutube-operational-api-configuration.md "https://joystream.org/schemas/youtube-synch/config#/properties/youtube/properties/operationalApi") | + +## operationalApi + +Youtube Operational API () configuration + +`operationalApi` + +* is required + +* Type: `object` ([Youtube Operational API (https://github.com/Benjamin-Loison/YouTube-operational-API) configuration](definition-properties-youtube-related-configuration-properties-youtube-operational-api-httpsgithubcombenjamin-loisonyoutube-operational-api-configuration.md)) + +* cannot be null + +* defined in: [Youtube Sync node configuration](definition-properties-youtube-related-configuration-properties-youtube-operational-api-httpsgithubcombenjamin-loisonyoutube-operational-api-configuration.md "https://joystream.org/schemas/youtube-synch/config#/properties/youtube/properties/operationalApi") + +### operationalApi Type + +`object` ([Youtube Operational API (https://github.com/Benjamin-Loison/YouTube-operational-API) configuration](definition-properties-youtube-related-configuration-properties-youtube-operational-api-httpsgithubcombenjamin-loisonyoutube-operational-api-configuration.md)) diff --git a/docs/config/definition.md b/docs/config/definition.md index b252d076..d5e42611 100644 --- a/docs/config/definition.md +++ b/docs/config/definition.md @@ -9,7 +9,7 @@ | [joystream](#joystream) | `object` | Required | cannot be null | [Youtube Sync node configuration](definition-properties-joystream.md "https://joystream.org/schemas/youtube-synch/config#/properties/joystream") | | [endpoints](#endpoints) | `object` | Required | cannot be null | [Youtube Sync node configuration](definition-properties-endpoints.md "https://joystream.org/schemas/youtube-synch/config#/properties/endpoints") | | [logs](#logs) | `object` | Optional | cannot be null | [Youtube Sync node configuration](definition-properties-logs.md "https://joystream.org/schemas/youtube-synch/config#/properties/logs") | -| [youtube](#youtube) | `object` | Required | cannot be null | [Youtube Sync node configuration](definition-properties-youtube-oauth2-client-configuration.md "https://joystream.org/schemas/youtube-synch/config#/properties/youtube") | +| [youtube](#youtube) | `object` | Required | cannot be null | [Youtube Sync node configuration](definition-properties-youtube-related-configuration.md "https://joystream.org/schemas/youtube-synch/config#/properties/youtube") | | [aws](#aws) | `object` | Optional | cannot be null | [Youtube Sync node configuration](definition-properties-aws-configurations-needed-to-connect-with-dynamodb-instance.md "https://joystream.org/schemas/youtube-synch/config#/properties/aws") | | [proxy](#proxy) | `object` | Optional | cannot be null | [Youtube Sync node configuration](definition-properties-socks5-proxy-client-configuration-used-by-yt-dlp-to-bypass-ip-blockage-by-youtube.md "https://joystream.org/schemas/youtube-synch/config#/properties/proxy") | | [creatorOnboardingRequirements](#creatoronboardingrequirements) | `object` | Required | cannot be null | [Youtube Sync node configuration](definition-properties-creatoronboardingrequirements.md "https://joystream.org/schemas/youtube-synch/config#/properties/creatorOnboardingRequirements") | @@ -72,21 +72,21 @@ Specifies the logging configuration ## youtube -Youtube Oauth2 Client configuration +Youtube related configuration `youtube` * is required -* Type: `object` ([Youtube Oauth2 Client configuration](definition-properties-youtube-oauth2-client-configuration.md)) +* Type: `object` ([Youtube related configuration](definition-properties-youtube-related-configuration.md)) * cannot be null -* defined in: [Youtube Sync node configuration](definition-properties-youtube-oauth2-client-configuration.md "https://joystream.org/schemas/youtube-synch/config#/properties/youtube") +* defined in: [Youtube Sync node configuration](definition-properties-youtube-related-configuration.md "https://joystream.org/schemas/youtube-synch/config#/properties/youtube") ### youtube Type -`object` ([Youtube Oauth2 Client configuration](definition-properties-youtube-oauth2-client-configuration.md)) +`object` ([Youtube related configuration](definition-properties-youtube-related-configuration.md)) ## aws diff --git a/package-lock.json b/package-lock.json index 47497691..39d3e7ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "youtube-sync", - "version": "3.8.0", + "version": "4.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "youtube-sync", - "version": "3.8.0", + "version": "4.0.0", "license": "MIT", "dependencies": { "@apollo/client": "3.8.8", diff --git a/package.json b/package.json index 9741f4ea..61c35abf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "youtube-sync", - "version": "3.8.0", + "version": "4.0.0", "license": "MIT", "scripts": { "postpack": "rm -f oclif.manifest.json", @@ -14,7 +14,7 @@ "dynamodb:local:start": "npm run dynamodb:local:stop && export DEPLOYMENT_ENV=local && ./src/infrastructure/deploy.sh", "dynamodb:local:stop": "export DEPLOYMENT_ENV=local REMOVE_PULUMI_STACK=true && ./src/infrastructure/destroy.sh", "generate:docs:cli": "npx oclif-dev readme --multi --dir ./docs/cli", - "generate:docs:config": "npx ts-node ./src/schemas/scripts/generateConfigDoc.ts", + "generate:docs:config": "rm -rf ./docs/config && npx ts-node ./src/schemas/scripts/generateConfigDoc.ts", "generate:docs:all": "npm run generate:docs:cli && npm run generate:docs:config", "generate:types:all": "npm run generate:types:json-schema && npm run generate:types:graphql", "generate:types:graphql": "npx graphql-codegen --config ./src/services/query-node/codegen.yml", diff --git a/src/app/index.ts b/src/app/index.ts index c69a581f..9140cb88 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -10,14 +10,14 @@ import { RuntimeApi } from '../services/runtime/api' import { JoystreamClient } from '../services/runtime/client' import { ContentProcessingClient, ContentProcessingService } from '../services/syncProcessing' import { YoutubePollingService } from '../services/syncProcessing/YoutubePollingService' -import { IYoutubeApi, YoutubeApi } from '../services/youtube/api' +import { YoutubeApi } from '../services/youtube' import { Config, DisplaySafeConfig } from '../types' export class Service { private config: Config private logging: LoggingService private logger: Logger - private youtubeApi: IYoutubeApi + private youtubeApi: YoutubeApi private queryNodeApi: QueryNodeApi private dynamodbService: DynamodbService private runtimeApi: RuntimeApi @@ -33,7 +33,7 @@ export class Service { this.logger = this.logging.createLogger('Server') this.queryNodeApi = new QueryNodeApi(config.endpoints.queryNode, this.logging) this.dynamodbService = new DynamodbService(this.config.aws) - this.youtubeApi = YoutubeApi.create(this.config, this.dynamodbService.repo.stats) + this.youtubeApi = new YoutubeApi(this.config) this.runtimeApi = new RuntimeApi(config.endpoints.joystreamNodeWs, this.logging) this.joystreamClient = new JoystreamClient(config, this.runtimeApi, this.queryNodeApi, this.logging) this.contentProcessingClient = new ContentProcessingClient({ ...config.sync, ...config.endpoints }) diff --git a/src/cli/commands/sync/addUnauthorizedChannelForSyncing.ts b/src/cli/commands/sync/addUnauthorizedChannelForSyncing.ts index 2e955eeb..c2ade650 100644 --- a/src/cli/commands/sync/addUnauthorizedChannelForSyncing.ts +++ b/src/cli/commands/sync/addUnauthorizedChannelForSyncing.ts @@ -172,7 +172,6 @@ export default class AddUnauthorizedChannelForSyncing extends RuntimeApiCommandB const c = await dynamo.channels.save({ id: ytChannel.author.channelID, title: ytChannel.author.name, - userId: `UnauthorizedUser-${ytChannel.author.channelID}`, joystreamChannelId, createdAt: new Date(), lastActedAt: new Date(), diff --git a/src/infrastructure/index.ts b/src/infrastructure/index.ts index 57885d8a..d89fbdeb 100644 --- a/src/infrastructure/index.ts +++ b/src/infrastructure/index.ts @@ -17,13 +17,8 @@ const userTable = new aws.dynamodb.Table('users', { const channelsTable = new aws.dynamodb.Table('channels', { name: `${resourcePrefix}channels`, - hashKey: nameof('userId'), - rangeKey: nameof('id'), + hashKey: nameof('id'), attributes: [ - { - name: nameof('userId'), - type: 'S', - }, { name: nameof('id'), type: 'S', diff --git a/src/repository/channel.ts b/src/repository/channel.ts index af2f4aa7..499d51a3 100644 --- a/src/repository/channel.ts +++ b/src/repository/channel.ts @@ -20,19 +20,10 @@ function createChannelModel(tablePrefix: ResourcePrefix) { { // ID of the Youtube channel id: { - type: String, - rangeKey: true, - }, - - // ID of the user that owns the channel - userId: { type: String, hashKey: true, }, - // user provided email - email: String, - // ID of the corresponding Joystream Channel joystreamChannelId: { type: Number, @@ -46,9 +37,6 @@ function createChannelModel(tablePrefix: ResourcePrefix) { // video category ID to be added to all synced videos videoCategoryId: String, - // default language of youtube channel - language: String, - // language of corresponding Joystream channel joystreamChannelLanguageIso: String, @@ -123,22 +111,12 @@ function createChannelModel(tablePrefix: ResourcePrefix) { default: String, medium: String, high: String, - maxRes: String, - standard: String, }, }, // Banner or Background image URL bannerImageUrl: String, - // user access token obtained from authorization code after successful authentication - userAccessToken: String, - - // user refresh token that will be used to get new access token after expiration - userRefreshToken: String, - - uploadsPlaylistId: String, - // Should this channel be ingested for automated Youtube/Joystream syncing? shouldBeIngested: { type: Boolean, @@ -255,8 +233,8 @@ export class ChannelsRepository implements IRepository { async save(channel: YtChannel): Promise { return this.withLock(async () => { - const update = omit(['id', 'userId', 'updatedAt'], channel) - const result = await this.model.update({ id: channel.id, userId: channel.userId }, update) + const update = omit(['id', 'updatedAt'], channel) + const result = await this.model.update({ id: channel.id }, update) return mapTo(result) }) } @@ -268,16 +246,16 @@ export class ChannelsRepository implements IRepository { return this.withLock(async () => { const updateTransactions = channels.map((channel) => { - const update = omit(['id', 'userId', 'updatedAt'], channel) - return this.model.transaction.update({ id: channel.id, userId: channel.userId }, update) + const update = omit(['id', 'updatedAt'], channel) + return this.model.transaction.update({ id: channel.id }, update) }) return dynamoose.transaction(updateTransactions) }) } - async delete(id: string, userId: string): Promise { + async delete(id: string): Promise { return this.withLock(async () => { - await this.model.delete({ id, userId }) + await this.model.delete({ id }) return }) } @@ -325,7 +303,7 @@ export class ChannelsService { q .sort('descending') .filter('yppStatus') - .eq('Verified') + .beginsWith('Verified::') .or() .filter('yppStatus') .eq('Unverified') @@ -350,33 +328,13 @@ export class ChannelsService { * @returns Returns channel by youtube channelId */ async getById(channelId: string): Promise { - const result = await this.channelsRepository.get(channelId.toString()) + const result = await this.channelsRepository.get(channelId) if (!result) { throw new Error(`Could not find channel with id ${channelId}`) } return result } - /** - * @param userId - * @returns Returns channel by userId - */ - async getByUserId(userId: string): Promise { - const [result] = await this.channelsRepository.query('userId', (q) => q.eq(userId)) - if (!result) { - throw new Error(`Could not find user with id ${userId}`) - } - return result - } - - /** - * @param userId - * @returns List of Channels for given user - */ - async getAll(userId: string): Promise { - return await this.channelsRepository.query({ userId }, (q) => q) - } - /** * @param count Number of record to retrieve * @returns List of `n` recent verified channels diff --git a/src/repository/user.ts b/src/repository/user.ts index 450511b2..102ad274 100644 --- a/src/repository/user.ts +++ b/src/repository/user.ts @@ -15,32 +15,8 @@ function createUserModel(tablePrefix: ResourcePrefix) { hashKey: true, }, - // User email - email: String, - - // User youtube username - youtubeUsername: String, - - // User Google ID - googleId: String, - - // user authorization code - authorizationCode: String, - - // user access token obtained from authorization code after successful authentication - accessToken: String, - - // user refresh token that will be used to get new access token after expiration - refreshToken: String, - - // User avatar url - avatarUrl: String, - - // Corresponding Joystream member ID/s for Youtube user created through `POST /membership` (if any) - joystreamMemberIds: { - type: Array, - schema: [Number], - }, + // The URL for a specific video of the Youtube channel with which the user verified for YPP + youtubeVideoUrl: String, }, { saveUnknown: false, diff --git a/src/schemas/config.ts b/src/schemas/config.ts index 61c2f207..30eec768 100644 --- a/src/schemas/config.ts +++ b/src/schemas/config.ts @@ -20,17 +20,6 @@ export const configSchema: JSONSchema7 = objectSchema({ joystream: objectSchema({ description: 'Joystream network related configuration', properties: { - faucet: objectSchema({ - description: `Joystream's faucet configuration (needed for captcha-free membership creation)`, - properties: { - endpoint: { type: 'string', description: `Joystream's faucet URL` }, - captchaBypassKey: { - type: 'string', - description: `Bearer Authentication Key needed to bypass captcha verification on Faucet`, - }, - }, - required: ['endpoint', 'captchaBypassKey'], - }), app: objectSchema({ description: 'Joystream Metaprotocol App specific configuration', properties: { @@ -78,7 +67,7 @@ export const configSchema: JSONSchema7 = objectSchema({ required: ['memberId', 'account'], }), }, - required: ['faucet', 'app', 'channelCollaborator'], + required: ['app', 'channelCollaborator'], }), endpoints: objectSchema({ description: 'Specifies external endpoints that the distributor node will connect to', @@ -172,30 +161,24 @@ export const configSchema: JSONSchema7 = objectSchema({ required: [], }), youtube: objectSchema({ - title: 'Youtube Oauth2 Client configuration', - description: 'Youtube Oauth2 Client configuration', + title: 'Youtube related configuration', + description: 'Youtube related configuration', properties: { - clientId: { type: 'string', description: 'Youtube Oauth2 Client Id' }, - clientSecret: { type: 'string', description: 'Youtube Oauth2 Client Secret' }, - maxAllowedQuotaUsageInPercentage: { - description: - `Maximum percentage of daily Youtube API quota that can be used by the Periodic polling service. ` + - `Once this limit is reached the service will stop polling for new videos until the next day(when Quota resets). ` + - `All the remaining quota (100 - maxAllowedQuotaUsageInPercentage) will be used for potential channel's signups.`, - type: 'number', - default: 95, - }, - adcKeyFilePath: { - type: 'string', + operationalApi: objectSchema({ + title: 'Youtube Operational API (https://github.com/Benjamin-Loison/YouTube-operational-API) configuration', description: - `Path to the Google Cloud's Application Default Credentials (ADC) key file. ` + - `It is required to periodically monitor the Youtube API quota usage.`, - }, - }, - dependencies: { - maxAllowedQuotaUsageInPercentage: ['adcKeyFilePath'], + 'Youtube Operational API (https://github.com/Benjamin-Loison/YouTube-operational-API) configuration', + properties: { + url: { + type: 'string', + description: 'URL of the Youtube Operational API server (for example: http://localhost:8080)', + }, + }, + required: ['url'], + }), }, - required: ['clientId', 'clientSecret'], + + required: ['operationalApi'], }), aws: objectSchema({ title: 'AWS configurations needed to connect with DynamoDB instance', diff --git a/src/services/httpApi/api-spec.json b/src/services/httpApi/api-spec.json index 5b5742dd..1bd1119d 100644 --- a/src/services/httpApi/api-spec.json +++ b/src/services/httpApi/api-spec.json @@ -1,14 +1,14 @@ { "openapi": "3.0.0", "paths": { - "/users/{userId}/videos": { + "/channels/{channelId}/videos": { "get": { "operationId": "VideosController_get", "summary": "", "description": "Get videos across all channels owned by the user", "parameters": [ { - "name": "userId", + "name": "channelId", "required": true, "in": "path", "schema": { @@ -240,7 +240,7 @@ }, "/channels/{joystreamChannelId}/category": { "put": { - "operationId": "ChannelsController_updateCategoryChannel", + "operationId": "ChannelsController_updateChannelCategory", "summary": "", "description": "Updates given channel's videos category. Note: only channel owner can update the status", "parameters": [ @@ -573,7 +573,7 @@ "post": { "operationId": "UsersController_verifyUserAndChannel", "summary": "", - "description": "fetches user's channel from the supplied google authorization code, and verifies if it satisfies YPP induction criteria", + "description": "fetches YT creator's channel from the provided Youtube video URL, and verifies if it satisfies YPP induction criteria", "parameters": [], "requestBody": { "required": true, @@ -602,57 +602,6 @@ ] } }, - "/youtube/quota-usage": { - "get": { - "operationId": "YoutubeController_getAll", - "summary": "", - "description": "Get youtube quota usage information", - "deprecated": true, - "parameters": [], - "responses": { - "default": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Stats" - } - } - } - } - } - }, - "tags": [ - "youtube" - ] - } - }, - "/youtube/quota-usage/today": { - "get": { - "operationId": "YoutubeController_get", - "summary": "", - "description": "Get youtube quota usage information for today", - "deprecated": true, - "parameters": [], - "responses": { - "default": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Stats" - } - } - } - } - }, - "tags": [ - "youtube" - ] - } - }, "/status": { "get": { "operationId": "StatusController_getStatus", @@ -676,55 +625,6 @@ ] } }, - "/status/quota-usage": { - "get": { - "operationId": "StatusController_getQuotaStats", - "summary": "", - "description": "Get youtube quota usage information", - "parameters": [], - "responses": { - "default": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Stats" - } - } - } - } - } - }, - "tags": [ - "status" - ] - } - }, - "/status/quota-usage/today": { - "get": { - "operationId": "StatusController_getQuotaStatsForToday", - "summary": "", - "description": "Get youtube quota usage information for today", - "parameters": [], - "responses": { - "default": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Stats" - } - } - } - } - }, - "tags": [ - "status" - ] - } - }, "/status/collaborator": { "get": { "operationId": "StatusController_getCollaboratorStatus", @@ -747,39 +647,6 @@ "status" ] } - }, - "/membership": { - "post": { - "operationId": "MembershipController_createMembership", - "summary": "", - "description": "Create Joystream's on-chain Membership for a verfifed YPP user. It will forward request\n to Joystream faucet with an Authorization header to circumvent captcha verfication by the faucet", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateMembershipRequest" - } - } - } - }, - "responses": { - "default": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateMembershipResponse" - } - } - } - } - }, - "tags": [ - "membership" - ] - } } }, "info": { @@ -799,10 +666,10 @@ "SaveChannelRequest": { "type": "object", "properties": { - "authorizationCode": { + "youtubeVideoUrl": { "type": "string" }, - "userId": { + "id": { "type": "string" }, "email": { @@ -822,29 +689,14 @@ } }, "required": [ - "authorizationCode", - "userId", + "youtubeVideoUrl", + "id", "email", "joystreamChannelId", "shouldBeIngested", "videoCategoryId" ] }, - "UserDto": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "email": { - "type": "string" - } - }, - "required": [ - "id", - "email" - ] - }, "ThumbnailsDto": { "type": "object", "properties": { @@ -856,16 +708,12 @@ }, "high": { "type": "string" - }, - "standard": { - "type": "string" } }, "required": [ "default", "medium", - "high", - "standard" + "high" ] }, "ChannelDto": { @@ -943,15 +791,11 @@ "SaveChannelResponse": { "type": "object", "properties": { - "user": { - "$ref": "#/components/schemas/UserDto" - }, "channel": { "$ref": "#/components/schemas/ChannelDto" } }, "required": [ - "user", "channel" ] }, @@ -1129,6 +973,8 @@ "enum": [ "CHANNEL_NOT_FOUND", "VIDEO_NOT_FOUND", + "VIDEO_PRIVACY_STATUS_NOT_UNLISTED", + "VIDEO_TITLE_MISMATCH", "CHANNEL_ALREADY_REGISTERED", "CHANNEL_STATUS_SUSPENDED", "CHANNEL_CRITERIA_UNMET_SUBSCRIBERS", @@ -1187,25 +1033,18 @@ "VerifyChannelRequest": { "type": "object", "properties": { - "authorizationCode": { - "type": "string" - }, - "youtubeRedirectUri": { + "youtubeVideoUrl": { "type": "string" } }, "required": [ - "authorizationCode", - "youtubeRedirectUri" + "youtubeVideoUrl" ] }, "VerifyChannelResponse": { "type": "object", "properties": { - "email": { - "type": "string" - }, - "userId": { + "id": { "type": "string" }, "channelHandle": { @@ -1228,8 +1067,7 @@ } }, "required": [ - "email", - "userId", + "id", "channelHandle", "channelTitle", "channelDescription", @@ -1238,10 +1076,6 @@ "bannerUrl" ] }, - "Stats": { - "type": "object", - "properties": {} - }, "StatusDto": { "type": "object", "properties": { @@ -1261,55 +1095,9 @@ "syncBacklog" ] }, - "CreateMembershipRequest": { - "type": "object", - "properties": { - "userId": { - "type": "string" - }, - "authorizationCode": { - "type": "string" - }, - "account": { - "type": "string" - }, - "handle": { - "type": "string" - }, - "avatar": { - "type": "string" - }, - "about": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": [ - "userId", - "authorizationCode", - "account", - "handle", - "avatar", - "about", - "name" - ] - }, - "CreateMembershipResponse": { + "Stats": { "type": "object", - "properties": { - "memberId": { - "type": "number" - }, - "handle": { - "type": "string" - } - }, - "required": [ - "memberId", - "handle" - ] + "properties": {} } } } diff --git a/src/services/httpApi/controllers/channels.ts b/src/services/httpApi/controllers/channels.ts index 28646242..2d8eecb7 100644 --- a/src/services/httpApi/controllers/channels.ts +++ b/src/services/httpApi/controllers/channels.ts @@ -16,14 +16,13 @@ import { } from '@nestjs/common' import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' import { signatureVerify } from '@polkadot/util-crypto' -import { randomBytes } from 'crypto' import { DynamodbService } from '../../../repository' import { ReadonlyConfig } from '../../../types' -import { YtChannel, YtUser } from '../../../types/youtube' +import { YtChannel } from '../../../types/youtube' import { QueryNodeApi } from '../../query-node/api' import { ContentProcessingClient } from '../../syncProcessing' import { YoutubePollingService } from '../../syncProcessing/YoutubePollingService' -import { IYoutubeApi } from '../../youtube/api' +import { YoutubeApi } from '../../youtube' import { ChannelDto, ChannelInductionRequirementsDto, @@ -36,7 +35,6 @@ import { SetOperatorIngestionStatusDto, SuspendChannelDto, UpdateChannelCategoryDto, - UserDto, VerifyChannelDto, WhitelistChannelDto, } from '../dtos' @@ -46,7 +44,7 @@ import { export class ChannelsController { constructor( @Inject('config') private config: ReadonlyConfig, - @Inject('youtube') private youtubeApi: IYoutubeApi, + private youtubeApi: YoutubeApi, private qnApi: QueryNodeApi, private dynamodbService: DynamodbService, private youtubePollingService: YoutubePollingService, @@ -59,15 +57,15 @@ export class ChannelsController { @ApiOperation({ description: `Saves channel record of a YPP verified user` }) async saveChannel(@Body() channelInfo: SaveChannelRequest): Promise { try { - const { - userId, - authorizationCode, - email, - joystreamChannelId, - shouldBeIngested, - videoCategoryId, - referrerChannelId, - } = channelInfo + const { id, youtubeVideoUrl, joystreamChannelId, shouldBeIngested, videoCategoryId, referrerChannelId } = + channelInfo + + // TODO: ensure video is still unlisted + // TODO: check if needed. maybe add new flag `isSaved` to user record and check that instead + // // Ensure that channel is not already registered for YPP program + // if (await this.dynamodbService.repo.channels.get(id)) { + // throw new Error('Channel already exists, cannot re-save the channel') + // } /** * Input Validation @@ -77,16 +75,16 @@ export class ChannelsController { throw new Error('Referrer channel cannot be the same as the channel being verified.') } - // get user from userId - const user = await this.dynamodbService.users.get(userId) + // get user by channel Id + const user = await this.dynamodbService.users.get(id) - // ensure request's authorization code matches the user's authorization code - if (user.authorizationCode !== authorizationCode) { - throw new Error('Invalid request author. Permission denied.') + // ensure request is sent by the authorized actor + if (user.youtubeVideoUrl !== youtubeVideoUrl) { + throw new Error('Authorization error. Youtube video url is invalid.') } // ensure that Joystream channel exists - const jsChannel = await this.qnApi.getChannelById(joystreamChannelId.toString()) + const jsChannel = await this.qnApi.getChannelById(joystreamChannelId.toString()) // TODO: check if QN is lagging then what will happen if (!jsChannel) { throw new Error(`Joystream Channel ${joystreamChannelId} does not exist.`) } @@ -100,27 +98,22 @@ export class ChannelsController { } // get channel from user - let channel = await this.youtubeApi.getChannel(user) + let channel = await this.youtubeApi.operationalApi.getChannel(id) const existingChannel = await this.dynamodbService.repo.channels.get(channel.id) - // reset authorization code to prevent repeated save channel requests by authorization code re-use - const updatedUser: YtUser = { ...user, email, authorizationCode: randomBytes(10).toString('hex') } - const joystreamChannelLanguageIso = jsChannel.language || undefined // If channel already exists in the DB (in `OptedOut` state), then we // associate most properties of existing channel record with the new - // channel, i.e. createdAt, email. userId etc. and only override the + // channel, i.e. createdAt, email etc. and only override the // configuration properties provided in the request const updatedChannel: YtChannel = { ...(existingChannel ? { ...existingChannel, yppStatus: 'Unverified', - userAccessToken: channel.userAccessToken, - userRefreshToken: channel.userRefreshToken, } - : { ...channel, email }), + : { ...channel }), joystreamChannelId, shouldBeIngested, videoCategoryId, @@ -129,10 +122,10 @@ export class ChannelsController { } // save user and channel - await this.saveUserAndChannel(updatedUser, updatedChannel) + await this.saveChannelAndVideos(updatedChannel) // return user and channel - return new SaveChannelResponse(new UserDto(updatedUser), new ChannelDto(updatedChannel)) + return new SaveChannelResponse(new ChannelDto(updatedChannel)) } catch (error) { const message = error instanceof Error ? error.message : error throw new BadRequestException(message) @@ -202,6 +195,7 @@ export class ChannelsController { } } + // TODO: on optout/suspend/disable ingestion remove all the polled not yet created videos, stop ingestion of channel's videos @Put(':joystreamChannelId/optout') @ApiBody({ type: OptoutChannelDto }) @ApiResponse({ type: ChannelDto }) @@ -232,7 +226,7 @@ export class ChannelsController { @ApiOperation({ description: `Updates given channel's videos category. Note: only channel owner can update the status`, }) - async updateCategoryChannel( + async updateChannelCategory( @Param('joystreamChannelId', ParseIntPipe) id: number, @Body() action: UpdateChannelCategoryDto ) { @@ -425,10 +419,7 @@ export class ChannelsController { }) } - private async saveUserAndChannel(user: YtUser, channel: YtChannel) { - // save user - await this.dynamodbService.users.save(user) - + private async saveChannelAndVideos(channel: YtChannel) { // save channel await this.dynamodbService.channels.save(channel) @@ -458,7 +449,7 @@ export class ChannelsController { const channel = await this.dynamodbService.channels.getByJoystreamId(joystreamChannelId) // Ensure channel is not suspended or opted out - if (YtChannel.isSuspended(channel) || channel.yppStatus === 'OptedOut') { + if (YtChannel.isSuspended(channel)) { throw new Error(`Can't perform "${actionType}" action on a "${channel.yppStatus}" channel. Permission denied.`) } @@ -468,7 +459,7 @@ export class ChannelsController { } // verify the message signature using Channel owner's address - const { isValid } = signatureVerify(JSON.stringify(message), signature, jsChannel.ownerMember.controllerAccount) + const { isValid } = signatureVerify(JSON.stringify(message), signature, jsChannel.ownerMember.controllerAccount.id) // Ensure that the signature is valid and the message is not a playback message if (!isValid || channel.lastActedAt >= message.timestamp) { diff --git a/src/services/httpApi/controllers/index.ts b/src/services/httpApi/controllers/index.ts index b95527fa..fcf7b68a 100644 --- a/src/services/httpApi/controllers/index.ts +++ b/src/services/httpApi/controllers/index.ts @@ -1,5 +1,5 @@ export * from './channels' +export * from './referrers' export * from './status' export * from './users' export * from './videos' -export * from './youtube' diff --git a/src/services/httpApi/controllers/membership.ts b/src/services/httpApi/controllers/membership.ts deleted file mode 100644 index 63c99fee..00000000 --- a/src/services/httpApi/controllers/membership.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { BadRequestException, Body, Controller, Inject, Post } from '@nestjs/common' -import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' -import axios from 'axios' -import { DynamodbService } from '../../../repository' -import { ReadonlyConfig } from '../../../types' -import { FaucetApiError } from '../../../types/errors' -import { FaucetRegisterMembershipParams, FaucetRegisterMembershipResponse } from '../../../types/youtube' -import { CreateMembershipRequest, CreateMembershipResponse } from '../dtos' - -@Controller('membership') -@ApiTags('membership') -export class MembershipController { - constructor(@Inject('config') private config: ReadonlyConfig, private dynamodbService: DynamodbService) {} - - @ApiOperation({ - description: `Create Joystream's on-chain Membership for a verfifed YPP user. It will forward request - to Joystream faucet with an Authorization header to circumvent captcha verfication by the faucet`, - }) - @ApiBody({ type: CreateMembershipRequest }) - @ApiResponse({ type: CreateMembershipResponse }) - @Post() - async createMembership(@Body() membershipParams: CreateMembershipRequest): Promise { - try { - const { userId, authorizationCode, account, handle, avatar, about, name } = membershipParams - - // get user from userId - const user = await this.dynamodbService.users.get(userId) - - // ensure request's authorization code matches the user's authorization code - if (user.authorizationCode !== authorizationCode) { - throw new Error('Invalid request author. Permission denied.') - } - - // Only allow maximum of 5 captcha-free memberships to be created by YT-synch - // as avoiding this check leads sybil attack where any YPP verified user/channel - // can created infinitely many memberships causing faucet funds to exhaust. - if (user.joystreamMemberIds.length >= 2) { - throw new Error(`Already created Joysteam memberships ${user.joystreamMemberIds} for user ${user.id}`) - } - - // send create membership request to faucet - const { memberId } = await this.createMemberWithFaucet({ account, handle, avatar, about, name }) - - // save updated user entity - await this.dynamodbService.users.save({ ...user, joystreamMemberIds: [...user.joystreamMemberIds, memberId] }) - - return new CreateMembershipResponse(memberId, handle) - } catch (error) { - const message = error instanceof Error ? error.message : error - throw new BadRequestException(message) - } - } - - private async createMemberWithFaucet(params: FaucetRegisterMembershipParams): Promise<{ memberId: number }> { - const { endpoint, captchaBypassKey } = this.config.joystream.faucet - try { - const response = await axios.post(endpoint, params, { - headers: { Authorization: `Bearer ${captchaBypassKey}` }, - }) - return response.data - } catch (error) { - if (axios.isAxiosError(error)) { - throw new FaucetApiError( - error.response?.data?.error || error.cause || error.code, - `Failed to create membership through faucet for account address: ${params.account}` - ) - } - throw error - } - } -} diff --git a/src/services/httpApi/controllers/status.ts b/src/services/httpApi/controllers/status.ts index 5dc1fd51..3df83dbc 100644 --- a/src/services/httpApi/controllers/status.ts +++ b/src/services/httpApi/controllers/status.ts @@ -1,7 +1,6 @@ import { Controller, Get, Inject, NotFoundException } from '@nestjs/common' import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' import BN from 'bn.js' -import { DynamodbService } from '../../../repository' import { ReadonlyConfig } from '../../../types' import { Stats } from '../../../types/youtube' import { RuntimeApi } from '../../runtime/api' @@ -12,7 +11,6 @@ import { CollaboratorStatusDto, StatusDto } from '../dtos' @ApiTags('status') export class StatusController { constructor( - private dynamodbService: DynamodbService, private runtimeApi: RuntimeApi, private contentProcessingClient: ContentProcessingClient, @Inject('config') private config: ReadonlyConfig @@ -37,33 +35,6 @@ export class StatusController { } } - @Get('quota-usage') - @ApiResponse({ type: Stats, isArray: true }) - @ApiOperation({ description: `Get youtube quota usage information` }) - async getQuotaStats(): Promise { - try { - // Get complete quota usage statss - const stats = await this.dynamodbService.repo.stats.scan('partition', (s) => s) - return stats - } catch (error) { - const message = error instanceof Error ? error.message : error - throw new NotFoundException(message) - } - } - - @Get('quota-usage/today') - @ApiResponse({ type: Stats }) - @ApiOperation({ description: `Get youtube quota usage information for today` }) - async getQuotaStatsForToday(): Promise { - try { - const stats = this.dynamodbService.repo.stats.getOrSetTodaysStats() - return stats - } catch (error) { - const message = error instanceof Error ? error.message : error - throw new NotFoundException(message) - } - } - @Get('collaborator') @ApiResponse({ type: Stats }) @ApiOperation({ description: `Get Joystream collaborator account info` }) diff --git a/src/services/httpApi/controllers/users.ts b/src/services/httpApi/controllers/users.ts index 99b18a10..e7aae241 100644 --- a/src/services/httpApi/controllers/users.ts +++ b/src/services/httpApi/controllers/users.ts @@ -1,30 +1,26 @@ -import { BadRequestException, Body, Controller, Inject, Post } from '@nestjs/common' +import { BadRequestException, Body, Controller, Post } from '@nestjs/common' import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' import { DynamodbService } from '../../../repository' import { ExitCodes, YoutubeApiError } from '../../../types/errors' import { YtChannel } from '../../../types/youtube' -import { IYoutubeApi } from '../../youtube/api' +import { YoutubeApi } from '../../youtube' import { VerifyChannelRequest, VerifyChannelResponse } from '../dtos' @Controller('users') @ApiTags('channels') export class UsersController { - constructor(@Inject('youtube') private youtube: IYoutubeApi, private dynamodbService: DynamodbService) {} + constructor(private youtubeApi: YoutubeApi, private dynamodbService: DynamodbService) {} @ApiOperation({ - description: `fetches user's channel from the supplied google authorization code, and verifies if it satisfies YPP induction criteria`, + description: `fetches YT creator's channel from the provided Youtube video URL, and verifies if it satisfies YPP induction criteria`, }) @ApiBody({ type: VerifyChannelRequest }) @ApiResponse({ type: VerifyChannelResponse }) @Post() - async verifyUserAndChannel( - @Body() { authorizationCode, youtubeRedirectUri }: VerifyChannelRequest - ): Promise { + async verifyUserAndChannel(@Body() { youtubeVideoUrl }: VerifyChannelRequest): Promise { try { - // get user from authorization code - const user = await this.youtube.getUserFromCode(authorizationCode, youtubeRedirectUri) - - const [registeredChannel] = await this.dynamodbService.channels.getAll(user.id) + const { user, video } = await this.youtubeApi.ytdlp.getUserAndVideoFromVideoUrl(youtubeVideoUrl) + const registeredChannel = await this.dynamodbService.repo.channels.get(user.id) // Ensure 1. selected YT channel is not already registered for YPP program // OR 2. even if registered previously it has opted out. @@ -44,7 +40,7 @@ export class UsersController { } } - const { channel, errors } = await this.youtube.getVerifiedChannel(user) + const { channel, errors } = await this.youtubeApi.operationalApi.getVerifiedChannel(video) const whitelistedChannel = await this.dynamodbService.repo.whitelistChannels.get(channel.customUrl) // check if the channel is whitelisted @@ -52,22 +48,17 @@ export class UsersController { throw errors } - // Get existing user record from db (if any) - const existingUser = await this.dynamodbService.repo.users.get(user.id) - // save user & set joystreamMemberId if user already existed - await this.dynamodbService.users.save({ ...user, joystreamMemberIds: existingUser?.joystreamMemberIds || [] }) + await this.dynamodbService.users.save(user) // return verified user return { - email: user.email, - userId: user.id, + id: user.id, channelTitle: channel.title, channelDescription: channel.description, - avatarUrl: channel.thumbnails.medium, + avatarUrl: channel.thumbnails.high, bannerUrl: channel.bannerImageUrl, channelHandle: channel.customUrl, - channelLanguage: channel.language, } } catch (error) { const message = error instanceof Error ? error.message : error diff --git a/src/services/httpApi/controllers/videos.ts b/src/services/httpApi/controllers/videos.ts index e1daf82d..334734f6 100644 --- a/src/services/httpApi/controllers/videos.ts +++ b/src/services/httpApi/controllers/videos.ts @@ -3,7 +3,7 @@ import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' import { DynamodbService } from '../../../repository' import { YtVideo } from '../../../types/youtube' -@Controller('users/:userId/videos') +@Controller('channels/:channelId/videos') @ApiTags('channels') export class VideosController { constructor(private dynamodbService: DynamodbService) {} @@ -11,10 +11,10 @@ export class VideosController { @Get() @ApiResponse({ type: YtVideo, isArray: true }) @ApiOperation({ description: `Get videos across all channels owned by the user` }) - async get(@Param('userId') userId: string): Promise { + async get(@Param('channelId') channelId: string): Promise { try { // Get channels of the user - const channel = await this.dynamodbService.channels.getByUserId(userId) + const channel = await this.dynamodbService.channels.getById(channelId) // Get videos across all channels const result = await this.dynamodbService.repo.videos.query({ channelId: channel.id }, (q) => q) diff --git a/src/services/httpApi/controllers/youtube.ts b/src/services/httpApi/controllers/youtube.ts deleted file mode 100644 index 2d485e9d..00000000 --- a/src/services/httpApi/controllers/youtube.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Controller, Get, NotFoundException } from '@nestjs/common' -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger' -import { DynamodbService } from '../../../repository' -import { Stats } from '../../../types/youtube' - -@Controller('youtube') -@ApiTags('youtube') -export class YoutubeController { - constructor(private dynamodbService: DynamodbService) {} - - @Get('quota-usage') - @ApiResponse({ type: Stats, isArray: true }) - @ApiOperation({ description: `Get youtube quota usage information`, deprecated: true }) - async getAll(): Promise { - try { - // Get complete quota usage stats - const stats = await this.dynamodbService.repo.stats.scan('partition', (s) => s) - return stats - } catch (error) { - const message = error instanceof Error ? error.message : error - throw new NotFoundException(message) - } - } - - @Get('quota-usage/today') - @ApiResponse({ type: Stats }) - @ApiOperation({ description: `Get youtube quota usage information for today`, deprecated: true }) - async get(): Promise { - try { - const stats = this.dynamodbService.repo.stats.getOrSetTodaysStats() - return stats - } catch (error) { - const message = error instanceof Error ? error.message : error - throw new NotFoundException(message) - } - } -} diff --git a/src/services/httpApi/dtos.ts b/src/services/httpApi/dtos.ts index 8ede2b75..1c8487db 100644 --- a/src/services/httpApi/dtos.ts +++ b/src/services/httpApi/dtos.ts @@ -24,11 +24,11 @@ import { TopReferrer, VideoState, YtChannel, - YtUser, YtVideo, channelYppStatus, } from '../../types/youtube' import { pluralizeNoun } from '../../utils/misc' +import { YT_VIDEO_TITLE_REQUIRED_FOR_SIGNUP } from '../youtube' // NestJS Data Transfer Objects (DTO)s @@ -36,7 +36,6 @@ export class ThumbnailsDto { @ApiProperty() default: string @ApiProperty() medium: string @ApiProperty() high: string - @ApiProperty() standard: string } export class StatusDto { @@ -75,6 +74,16 @@ export class ChannelInductionRequirementsDto { constructor(requirements: Config['creatorOnboardingRequirements']) { this.requirements = [ + { + errorCode: ExitCodes.YoutubeApi.VIDEO_PRIVACY_STATUS_NOT_UNLISTED, + template: 'YouTube video should be {}.', + variables: ['Unlisted'], + }, + { + errorCode: ExitCodes.YoutubeApi.VIDEO_TITLE_MISMATCH, + template: 'YouTube video title should be {}.', + variables: [YT_VIDEO_TITLE_REQUIRED_FOR_SIGNUP], + }, { errorCode: ExitCodes.YoutubeApi.CHANNEL_CRITERIA_UNMET_SUBSCRIBERS, template: 'YouTube channel has at least {}.', @@ -130,7 +139,6 @@ export class ChannelDto { this.joystreamChannelId = channel.joystreamChannelId this.referrerChannelId = channel.referrerChannelId this.videoCategoryId = channel.videoCategoryId - this.language = channel.language this.shouldBeIngested = channel.shouldBeIngested this.yppStatus = channel.yppStatus this.thumbnails = channel.thumbnails @@ -169,31 +177,18 @@ export class TopReferrerDto { } } -export class UserDto { - @ApiProperty() id: string - @ApiProperty() email: string - - constructor(user: YtUser) { - this.id = user.id - this.email = user.email - } -} - -// Dto for verifying Youtube channel given the authorization code +// Dto for verifying Youtube channel given the Youtube video URL export class VerifyChannelRequest { - // Authorization code send to the backend after user o-auth verification - @IsString() @ApiProperty({ required: true }) authorizationCode: string - - @IsUrl({ require_tld: false }) @ApiProperty({ required: true }) youtubeRedirectUri: string + // Youtube video URL required for the verification + @IsUrl({ require_tld: false }) + @ApiProperty({ required: true }) + youtubeVideoUrl: string } // Dto for verified Youtube channel response export class VerifyChannelResponse { - // Email of the verified user - @IsEmail() @ApiProperty({ required: true }) email: string - - // ID of the verified user - @IsString() @ApiProperty({ required: true }) userId: string + // ID of the verified Youtube channel + @IsString() @ApiProperty({ required: true }) id: string // Youtube Channel/User handle @IsString() @ApiProperty() channelHandle: string @@ -204,9 +199,6 @@ export class VerifyChannelResponse { // Youtube Channel description @IsString() @ApiProperty({ required: true }) channelDescription: string - // Youtube Channel default language - @IsString() @ApiProperty() channelLanguage: string - // Youtube Channel avatar URL @IsString() @ApiProperty({ required: true }) avatarUrl: string @@ -216,13 +208,15 @@ export class VerifyChannelResponse { // Dto for saving the verified Youtube channel export class SaveChannelRequest { - // Authorization code send to the backend after user o-auth verification - @IsString() @ApiProperty({ required: true }) authorizationCode: string + // Youtube video URL required for the verification + @IsUrl({ require_tld: false }) + @ApiProperty({ required: true }) + youtubeVideoUrl: string - // UserId of the Youtube creator return from Google Oauth API - @IsString() @ApiProperty({ required: true }) userId: string + // ID of the verified Youtube channel + @IsString() @ApiProperty({ required: true }) id: string - // Email of the user + // Email of the YT user/channel @IsEmail() @ApiProperty({ required: true }) email: string // Joystream Channel ID of the user verifying his Youtube Channel for YPP @@ -242,53 +236,13 @@ export class SaveChannelRequest { // Dto for save channel response export class SaveChannelResponse { - @ApiProperty() user: UserDto @ApiProperty({ type: ChannelDto }) channel: ChannelDto - constructor(user: UserDto, channel: ChannelDto) { - this.user = user + constructor(channel: ChannelDto) { this.channel = channel } } -// Dto for creating membership request -export class CreateMembershipRequest { - // UserId of the Youtube creator return from Google Oauth API - @IsString() @ApiProperty({ required: true }) userId: string - - // Authorization code send to the backend after user o-auth verification - @IsString() @ApiProperty({ required: true }) authorizationCode: string - - // Membership Account address - @IsString() @ApiProperty({ required: true }) account: string - - // Membership Handle - @IsString() @ApiProperty({ required: true }) handle: string - - // Membership avatar URL - @IsOptional() @IsUrl({ require_tld: false }) @ApiProperty({ required: true }) avatar: string - - // `about` information to associate with new Membership - @ApiProperty() about: string - - // Membership name - @ApiProperty() name: string -} - -// Dto for create membership response -export class CreateMembershipResponse { - // Membership Account address - @IsNumber() @ApiProperty({ required: true }) memberId: number - - // Membership Handle - @IsString() @ApiProperty({ required: true }) handle: string - - constructor(memberId: number, handle: string) { - this.memberId = memberId - this.handle = handle - } -} - export class VideoDto extends YtVideo { @ApiProperty() url: string @ApiProperty() title: string diff --git a/src/services/httpApi/main.ts b/src/services/httpApi/main.ts index 7ca48270..0730e35b 100644 --- a/src/services/httpApi/main.ts +++ b/src/services/httpApi/main.ts @@ -15,15 +15,8 @@ import { QueryNodeApi } from '../query-node/api' import { RuntimeApi } from '../runtime/api' import { ContentProcessingClient } from '../syncProcessing' import { YoutubePollingService } from '../syncProcessing/YoutubePollingService' -import { IYoutubeApi } from '../youtube/api' -import { - ChannelsController, - StatusController, - UsersController, - VideosController, - YoutubeController, -} from './controllers' -import { MembershipController } from './controllers/membership' +import { YoutubeApi } from '../youtube' +import { ChannelsController, StatusController, UsersController, VideosController } from './controllers' import { ReferrersController } from './controllers/referrers' class ApiModule {} @@ -62,7 +55,7 @@ export async function bootstrapHttpApi( logging: LoggingService, runtimeApi: RuntimeApi, queryNodeApi: QueryNodeApi, - youtubeApi: IYoutubeApi, + youtubeApi: YoutubeApi, youtubePollingService: YoutubePollingService, contentProcessingClient: ContentProcessingClient ) { @@ -75,15 +68,7 @@ export async function bootstrapHttpApi( module: ApiModule, imports: [], exports: [], - controllers: [ - VideosController, - ChannelsController, - ReferrersController, - UsersController, - YoutubeController, - StatusController, - MembershipController, - ], + controllers: [VideosController, ChannelsController, ReferrersController, UsersController, StatusController], providers: [ { provide: DynamodbService, @@ -106,7 +91,7 @@ export async function bootstrapHttpApi( useValue: contentProcessingClient, }, { - provide: 'youtube', + provide: YoutubeApi, useValue: youtubeApi, }, { diff --git a/src/services/httpApi/utils.ts b/src/services/httpApi/utils.ts new file mode 100644 index 00000000..823145b6 --- /dev/null +++ b/src/services/httpApi/utils.ts @@ -0,0 +1,22 @@ +import { ValidationArguments, ValidationOptions, registerDecorator } from 'class-validator' + +export function IsMutuallyExclusiveWith(property: string, validationOptions?: ValidationOptions) { + return function (object: any, propertyName: string) { + registerDecorator({ + name: 'isMutuallyExclusiveWith', + target: object.constructor, + propertyName: propertyName, + constraints: [property], + options: validationOptions, + validator: { + validate(value: any, args: ValidationArguments) { + const relatedValue = (args.object as any)[args.constraints[0]] + return value === null || value === undefined || relatedValue === null || relatedValue === undefined + }, + defaultMessage(args: ValidationArguments) { + return `${propertyName} is exclusive with ${args.constraints[0]}` + }, + }, + }) + } +} diff --git a/src/services/query-node/generated/queries.ts b/src/services/query-node/generated/queries.ts index 2b922129..a65c35f6 100644 --- a/src/services/query-node/generated/queries.ts +++ b/src/services/query-node/generated/queries.ts @@ -69,7 +69,7 @@ export type ChannelFieldsFragment = { language?: string | null totalVideosCreated: number videos: Array<{ id: string; videoStateBloatBond: any }> - ownerMember?: { id: string; controllerAccount: string } | null + ownerMember?: { id: string; controllerAccount: { id: string } } | null } export type GetChannelByIdQueryVariables = Types.Exact<{ @@ -82,7 +82,7 @@ export type GetChannelByIdQuery = { language?: string | null totalVideosCreated: number videos: Array<{ id: string; videoStateBloatBond: any }> - ownerMember?: { id: string; controllerAccount: string } | null + ownerMember?: { id: string; controllerAccount: { id: string } } | null } | null } @@ -128,7 +128,7 @@ export type MemberMetadataFieldsFragment = { name?: string | null; about?: strin export type MembershipFieldsFragment = { id: string handle: string - controllerAccount: string + controllerAccount: { id: string } metadata?: { name?: string | null; about?: string | null } | null } @@ -140,7 +140,7 @@ export type GetMemberByIdQuery = { membershipByUniqueInput?: { id: string handle: string - controllerAccount: string + controllerAccount: { id: string } metadata?: { name?: string | null; about?: string | null } | null } | null } @@ -205,7 +205,9 @@ export const ChannelFields = gql` language ownerMember { id - controllerAccount + controllerAccount { + id + } } totalVideosCreated } @@ -238,7 +240,9 @@ export const MembershipFields = gql` fragment MembershipFields on Membership { id handle - controllerAccount + controllerAccount { + id + } metadata { ...MemberMetadataFields } diff --git a/src/services/query-node/generated/schema.ts b/src/services/query-node/generated/schema.ts index b87b2ae4..9694559d 100644 --- a/src/services/query-node/generated/schema.ts +++ b/src/services/query-node/generated/schema.ts @@ -26,18 +26,12 @@ export type Account = { id: Scalars['String']['output'] /** Indicates whether the access to the gateway account is blocked */ isBlocked: Scalars['Boolean']['output'] - /** Indicates whether the gateway account's e-mail has been confirmed or not. */ - isEmailConfirmed: Scalars['Boolean']['output'] /** Blockchain (joystream) account associated with the gateway account */ - joystreamAccount: Scalars['String']['output'] - /** On-chain membership associated with the gateway account */ - membership: Membership + joystreamAccount: BlockchainAccount /** notification preferences for the account */ notificationPreferences: AccountNotificationPreferences /** runtime notifications */ notifications: Array - /** ID of the channel which referred the user to the platform */ - referrerChannelId?: Maybe /** Time when the gateway account was registered */ registeredAt: Scalars['DateTime']['output'] /** The user associated with the gateway account (the Gateway Account Owner) */ @@ -56,9 +50,7 @@ export type AccountData = { email: Scalars['String']['output'] followedChannels: Array id: Scalars['String']['output'] - isEmailConfirmed: Scalars['Boolean']['output'] joystreamAccount: Scalars['String']['output'] - membershipId: Scalars['String']['output'] preferences?: Maybe } @@ -77,6 +69,15 @@ export type AccountNotificationPreferences = { channelPaymentReceived: NotificationPreference channelReceivedFundsFromWg: NotificationPreference creatorTimedAuctionExpired: NotificationPreference + crtIssued: NotificationPreference + crtMarketBurn: NotificationPreference + crtMarketMint: NotificationPreference + crtMarketStarted: NotificationPreference + crtRevenueShareEnded: NotificationPreference + crtRevenueSharePlanned: NotificationPreference + crtRevenueShareStarted: NotificationPreference + crtSaleMint: NotificationPreference + crtSaleStarted: NotificationPreference fundsFromCouncilReceived: NotificationPreference fundsFromWgReceived: NotificationPreference fundsToExternalWalletSent: NotificationPreference @@ -195,6 +196,24 @@ export type AccountNotificationPreferencesWhereInput = { channelReceivedFundsFromWg_isNull?: InputMaybe creatorTimedAuctionExpired?: InputMaybe creatorTimedAuctionExpired_isNull?: InputMaybe + crtIssued?: InputMaybe + crtIssued_isNull?: InputMaybe + crtMarketBurn?: InputMaybe + crtMarketBurn_isNull?: InputMaybe + crtMarketMint?: InputMaybe + crtMarketMint_isNull?: InputMaybe + crtMarketStarted?: InputMaybe + crtMarketStarted_isNull?: InputMaybe + crtRevenueShareEnded?: InputMaybe + crtRevenueShareEnded_isNull?: InputMaybe + crtRevenueSharePlanned?: InputMaybe + crtRevenueSharePlanned_isNull?: InputMaybe + crtRevenueShareStarted?: InputMaybe + crtRevenueShareStarted_isNull?: InputMaybe + crtSaleMint?: InputMaybe + crtSaleMint_isNull?: InputMaybe + crtSaleStarted?: InputMaybe + crtSaleStarted_isNull?: InputMaybe fundsFromCouncilReceived?: InputMaybe fundsFromCouncilReceived_isNull?: InputMaybe fundsFromWgReceived?: InputMaybe @@ -250,24 +269,8 @@ export enum AccountOrderByInput { IdDesc = 'id_DESC', IsBlockedAsc = 'isBlocked_ASC', IsBlockedDesc = 'isBlocked_DESC', - IsEmailConfirmedAsc = 'isEmailConfirmed_ASC', - IsEmailConfirmedDesc = 'isEmailConfirmed_DESC', - JoystreamAccountAsc = 'joystreamAccount_ASC', - JoystreamAccountDesc = 'joystreamAccount_DESC', - MembershipControllerAccountAsc = 'membership_controllerAccount_ASC', - MembershipControllerAccountDesc = 'membership_controllerAccount_DESC', - MembershipCreatedAtAsc = 'membership_createdAt_ASC', - MembershipCreatedAtDesc = 'membership_createdAt_DESC', - MembershipHandleRawAsc = 'membership_handleRaw_ASC', - MembershipHandleRawDesc = 'membership_handleRaw_DESC', - MembershipHandleAsc = 'membership_handle_ASC', - MembershipHandleDesc = 'membership_handle_DESC', - MembershipIdAsc = 'membership_id_ASC', - MembershipIdDesc = 'membership_id_DESC', - MembershipTotalChannelsCreatedAsc = 'membership_totalChannelsCreated_ASC', - MembershipTotalChannelsCreatedDesc = 'membership_totalChannelsCreated_DESC', - ReferrerChannelIdAsc = 'referrerChannelId_ASC', - ReferrerChannelIdDesc = 'referrerChannelId_DESC', + JoystreamAccountIdAsc = 'joystreamAccount_id_ASC', + JoystreamAccountIdDesc = 'joystreamAccount_id_DESC', RegisteredAtAsc = 'registeredAt_ASC', RegisteredAtDesc = 'registeredAt_DESC', UserIdAsc = 'user_id_ASC', @@ -316,50 +319,13 @@ export type AccountWhereInput = { isBlocked_eq?: InputMaybe isBlocked_isNull?: InputMaybe isBlocked_not_eq?: InputMaybe - isEmailConfirmed_eq?: InputMaybe - isEmailConfirmed_isNull?: InputMaybe - isEmailConfirmed_not_eq?: InputMaybe - joystreamAccount_contains?: InputMaybe - joystreamAccount_containsInsensitive?: InputMaybe - joystreamAccount_endsWith?: InputMaybe - joystreamAccount_eq?: InputMaybe - joystreamAccount_gt?: InputMaybe - joystreamAccount_gte?: InputMaybe - joystreamAccount_in?: InputMaybe> + joystreamAccount?: InputMaybe joystreamAccount_isNull?: InputMaybe - joystreamAccount_lt?: InputMaybe - joystreamAccount_lte?: InputMaybe - joystreamAccount_not_contains?: InputMaybe - joystreamAccount_not_containsInsensitive?: InputMaybe - joystreamAccount_not_endsWith?: InputMaybe - joystreamAccount_not_eq?: InputMaybe - joystreamAccount_not_in?: InputMaybe> - joystreamAccount_not_startsWith?: InputMaybe - joystreamAccount_startsWith?: InputMaybe - membership?: InputMaybe - membership_isNull?: InputMaybe notificationPreferences?: InputMaybe notificationPreferences_isNull?: InputMaybe notifications_every?: InputMaybe notifications_none?: InputMaybe notifications_some?: InputMaybe - referrerChannelId_contains?: InputMaybe - referrerChannelId_containsInsensitive?: InputMaybe - referrerChannelId_endsWith?: InputMaybe - referrerChannelId_eq?: InputMaybe - referrerChannelId_gt?: InputMaybe - referrerChannelId_gte?: InputMaybe - referrerChannelId_in?: InputMaybe> - referrerChannelId_isNull?: InputMaybe - referrerChannelId_lt?: InputMaybe - referrerChannelId_lte?: InputMaybe - referrerChannelId_not_contains?: InputMaybe - referrerChannelId_not_containsInsensitive?: InputMaybe - referrerChannelId_not_endsWith?: InputMaybe - referrerChannelId_not_eq?: InputMaybe - referrerChannelId_not_in?: InputMaybe> - referrerChannelId_not_startsWith?: InputMaybe - referrerChannelId_startsWith?: InputMaybe registeredAt_eq?: InputMaybe registeredAt_gt?: InputMaybe registeredAt_gte?: InputMaybe @@ -386,6 +352,295 @@ export type AddVideoViewResult = { viewsNum: Scalars['Int']['output'] } +export type AmmCurve = { + /** the amm intercept parameter b in the formula a * x + b */ + ammInitPrice: Scalars['BigInt']['output'] + /** the amm slope parameter a in the formula a * x + b */ + ammSlopeParameter: Scalars['BigInt']['output'] + /** quantity bought on the market by the amm */ + burnedByAmm: Scalars['BigInt']['output'] + /** finalized (i.e. closed) */ + finalized: Scalars['Boolean']['output'] + /** counter */ + id: Scalars['String']['output'] + /** quantity sold to the market */ + mintedByAmm: Scalars['BigInt']['output'] + /** token this Amm is for */ + token: CreatorToken + /** transaction for this amm */ + transactions: Array +} + +export type AmmCurveTransactionsArgs = { + limit?: InputMaybe + offset?: InputMaybe + orderBy?: InputMaybe> + where?: InputMaybe +} + +export type AmmCurveEdge = { + cursor: Scalars['String']['output'] + node: AmmCurve +} + +export enum AmmCurveOrderByInput { + AmmInitPriceAsc = 'ammInitPrice_ASC', + AmmInitPriceDesc = 'ammInitPrice_DESC', + AmmSlopeParameterAsc = 'ammSlopeParameter_ASC', + AmmSlopeParameterDesc = 'ammSlopeParameter_DESC', + BurnedByAmmAsc = 'burnedByAmm_ASC', + BurnedByAmmDesc = 'burnedByAmm_DESC', + FinalizedAsc = 'finalized_ASC', + FinalizedDesc = 'finalized_DESC', + IdAsc = 'id_ASC', + IdDesc = 'id_DESC', + MintedByAmmAsc = 'mintedByAmm_ASC', + MintedByAmmDesc = 'mintedByAmm_DESC', + TokenAccountsNumAsc = 'token_accountsNum_ASC', + TokenAccountsNumDesc = 'token_accountsNum_DESC', + TokenAnnualCreatorRewardPermillAsc = 'token_annualCreatorRewardPermill_ASC', + TokenAnnualCreatorRewardPermillDesc = 'token_annualCreatorRewardPermill_DESC', + TokenCreatedAtAsc = 'token_createdAt_ASC', + TokenCreatedAtDesc = 'token_createdAt_DESC', + TokenDeissuedAsc = 'token_deissued_ASC', + TokenDeissuedDesc = 'token_deissued_DESC', + TokenDescriptionAsc = 'token_description_ASC', + TokenDescriptionDesc = 'token_description_DESC', + TokenIdAsc = 'token_id_ASC', + TokenIdDesc = 'token_id_DESC', + TokenIsFeaturedAsc = 'token_isFeatured_ASC', + TokenIsFeaturedDesc = 'token_isFeatured_DESC', + TokenIsInviteOnlyAsc = 'token_isInviteOnly_ASC', + TokenIsInviteOnlyDesc = 'token_isInviteOnly_DESC', + TokenLastPriceAsc = 'token_lastPrice_ASC', + TokenLastPriceDesc = 'token_lastPrice_DESC', + TokenNumberOfRevenueShareActivationsAsc = 'token_numberOfRevenueShareActivations_ASC', + TokenNumberOfRevenueShareActivationsDesc = 'token_numberOfRevenueShareActivations_DESC', + TokenNumberOfVestedTransferIssuedAsc = 'token_numberOfVestedTransferIssued_ASC', + TokenNumberOfVestedTransferIssuedDesc = 'token_numberOfVestedTransferIssued_DESC', + TokenRevenueShareRatioPermillAsc = 'token_revenueShareRatioPermill_ASC', + TokenRevenueShareRatioPermillDesc = 'token_revenueShareRatioPermill_DESC', + TokenStatusAsc = 'token_status_ASC', + TokenStatusDesc = 'token_status_DESC', + TokenSymbolAsc = 'token_symbol_ASC', + TokenSymbolDesc = 'token_symbol_DESC', + TokenTotalSupplyAsc = 'token_totalSupply_ASC', + TokenTotalSupplyDesc = 'token_totalSupply_DESC', + TokenWhitelistApplicantLinkAsc = 'token_whitelistApplicantLink_ASC', + TokenWhitelistApplicantLinkDesc = 'token_whitelistApplicantLink_DESC', + TokenWhitelistApplicantNoteAsc = 'token_whitelistApplicantNote_ASC', + TokenWhitelistApplicantNoteDesc = 'token_whitelistApplicantNote_DESC', +} + +export type AmmCurveWhereInput = { + AND?: InputMaybe> + OR?: InputMaybe> + ammInitPrice_eq?: InputMaybe + ammInitPrice_gt?: InputMaybe + ammInitPrice_gte?: InputMaybe + ammInitPrice_in?: InputMaybe> + ammInitPrice_isNull?: InputMaybe + ammInitPrice_lt?: InputMaybe + ammInitPrice_lte?: InputMaybe + ammInitPrice_not_eq?: InputMaybe + ammInitPrice_not_in?: InputMaybe> + ammSlopeParameter_eq?: InputMaybe + ammSlopeParameter_gt?: InputMaybe + ammSlopeParameter_gte?: InputMaybe + ammSlopeParameter_in?: InputMaybe> + ammSlopeParameter_isNull?: InputMaybe + ammSlopeParameter_lt?: InputMaybe + ammSlopeParameter_lte?: InputMaybe + ammSlopeParameter_not_eq?: InputMaybe + ammSlopeParameter_not_in?: InputMaybe> + burnedByAmm_eq?: InputMaybe + burnedByAmm_gt?: InputMaybe + burnedByAmm_gte?: InputMaybe + burnedByAmm_in?: InputMaybe> + burnedByAmm_isNull?: InputMaybe + burnedByAmm_lt?: InputMaybe + burnedByAmm_lte?: InputMaybe + burnedByAmm_not_eq?: InputMaybe + burnedByAmm_not_in?: InputMaybe> + finalized_eq?: InputMaybe + finalized_isNull?: InputMaybe + finalized_not_eq?: InputMaybe + id_contains?: InputMaybe + id_containsInsensitive?: InputMaybe + id_endsWith?: InputMaybe + id_eq?: InputMaybe + id_gt?: InputMaybe + id_gte?: InputMaybe + id_in?: InputMaybe> + id_isNull?: InputMaybe + id_lt?: InputMaybe + id_lte?: InputMaybe + id_not_contains?: InputMaybe + id_not_containsInsensitive?: InputMaybe + id_not_endsWith?: InputMaybe + id_not_eq?: InputMaybe + id_not_in?: InputMaybe> + id_not_startsWith?: InputMaybe + id_startsWith?: InputMaybe + mintedByAmm_eq?: InputMaybe + mintedByAmm_gt?: InputMaybe + mintedByAmm_gte?: InputMaybe + mintedByAmm_in?: InputMaybe> + mintedByAmm_isNull?: InputMaybe + mintedByAmm_lt?: InputMaybe + mintedByAmm_lte?: InputMaybe + mintedByAmm_not_eq?: InputMaybe + mintedByAmm_not_in?: InputMaybe> + token?: InputMaybe + token_isNull?: InputMaybe + transactions_every?: InputMaybe + transactions_none?: InputMaybe + transactions_some?: InputMaybe +} + +export type AmmCurvesConnection = { + edges: Array + pageInfo: PageInfo + totalCount: Scalars['Int']['output'] +} + +export type AmmTransaction = { + /** buyer account */ + account: TokenAccount + /** Reference to the Amm Sale */ + amm: AmmCurve + /** block */ + createdIn: Scalars['Int']['output'] + /** counter */ + id: Scalars['String']['output'] + /** total HAPI paid/received for the quantity */ + pricePaid: Scalars['BigInt']['output'] + /** price per unit in HAPI */ + pricePerUnit: Scalars['BigInt']['output'] + /** amount of token bought/sold */ + quantity: Scalars['BigInt']['output'] + /** was it bought/sold */ + transactionType: AmmTransactionType +} + +export type AmmTransactionEdge = { + cursor: Scalars['String']['output'] + node: AmmTransaction +} + +export enum AmmTransactionOrderByInput { + AccountDeletedAsc = 'account_deleted_ASC', + AccountDeletedDesc = 'account_deleted_DESC', + AccountIdAsc = 'account_id_ASC', + AccountIdDesc = 'account_id_DESC', + AccountStakedAmountAsc = 'account_stakedAmount_ASC', + AccountStakedAmountDesc = 'account_stakedAmount_DESC', + AccountTotalAmountAsc = 'account_totalAmount_ASC', + AccountTotalAmountDesc = 'account_totalAmount_DESC', + AmmAmmInitPriceAsc = 'amm_ammInitPrice_ASC', + AmmAmmInitPriceDesc = 'amm_ammInitPrice_DESC', + AmmAmmSlopeParameterAsc = 'amm_ammSlopeParameter_ASC', + AmmAmmSlopeParameterDesc = 'amm_ammSlopeParameter_DESC', + AmmBurnedByAmmAsc = 'amm_burnedByAmm_ASC', + AmmBurnedByAmmDesc = 'amm_burnedByAmm_DESC', + AmmFinalizedAsc = 'amm_finalized_ASC', + AmmFinalizedDesc = 'amm_finalized_DESC', + AmmIdAsc = 'amm_id_ASC', + AmmIdDesc = 'amm_id_DESC', + AmmMintedByAmmAsc = 'amm_mintedByAmm_ASC', + AmmMintedByAmmDesc = 'amm_mintedByAmm_DESC', + CreatedInAsc = 'createdIn_ASC', + CreatedInDesc = 'createdIn_DESC', + IdAsc = 'id_ASC', + IdDesc = 'id_DESC', + PricePaidAsc = 'pricePaid_ASC', + PricePaidDesc = 'pricePaid_DESC', + PricePerUnitAsc = 'pricePerUnit_ASC', + PricePerUnitDesc = 'pricePerUnit_DESC', + QuantityAsc = 'quantity_ASC', + QuantityDesc = 'quantity_DESC', + TransactionTypeAsc = 'transactionType_ASC', + TransactionTypeDesc = 'transactionType_DESC', +} + +export enum AmmTransactionType { + Buy = 'BUY', + Sell = 'SELL', +} + +export type AmmTransactionWhereInput = { + AND?: InputMaybe> + OR?: InputMaybe> + account?: InputMaybe + account_isNull?: InputMaybe + amm?: InputMaybe + amm_isNull?: InputMaybe + createdIn_eq?: InputMaybe + createdIn_gt?: InputMaybe + createdIn_gte?: InputMaybe + createdIn_in?: InputMaybe> + createdIn_isNull?: InputMaybe + createdIn_lt?: InputMaybe + createdIn_lte?: InputMaybe + createdIn_not_eq?: InputMaybe + createdIn_not_in?: InputMaybe> + id_contains?: InputMaybe + id_containsInsensitive?: InputMaybe + id_endsWith?: InputMaybe + id_eq?: InputMaybe + id_gt?: InputMaybe + id_gte?: InputMaybe + id_in?: InputMaybe> + id_isNull?: InputMaybe + id_lt?: InputMaybe + id_lte?: InputMaybe + id_not_contains?: InputMaybe + id_not_containsInsensitive?: InputMaybe + id_not_endsWith?: InputMaybe + id_not_eq?: InputMaybe + id_not_in?: InputMaybe> + id_not_startsWith?: InputMaybe + id_startsWith?: InputMaybe + pricePaid_eq?: InputMaybe + pricePaid_gt?: InputMaybe + pricePaid_gte?: InputMaybe + pricePaid_in?: InputMaybe> + pricePaid_isNull?: InputMaybe + pricePaid_lt?: InputMaybe + pricePaid_lte?: InputMaybe + pricePaid_not_eq?: InputMaybe + pricePaid_not_in?: InputMaybe> + pricePerUnit_eq?: InputMaybe + pricePerUnit_gt?: InputMaybe + pricePerUnit_gte?: InputMaybe + pricePerUnit_in?: InputMaybe> + pricePerUnit_isNull?: InputMaybe + pricePerUnit_lt?: InputMaybe + pricePerUnit_lte?: InputMaybe + pricePerUnit_not_eq?: InputMaybe + pricePerUnit_not_in?: InputMaybe> + quantity_eq?: InputMaybe + quantity_gt?: InputMaybe + quantity_gte?: InputMaybe + quantity_in?: InputMaybe> + quantity_isNull?: InputMaybe + quantity_lt?: InputMaybe + quantity_lte?: InputMaybe + quantity_not_eq?: InputMaybe + quantity_not_in?: InputMaybe> + transactionType_eq?: InputMaybe + transactionType_in?: InputMaybe> + transactionType_isNull?: InputMaybe + transactionType_not_eq?: InputMaybe + transactionType_not_in?: InputMaybe> +} + +export type AmmTransactionsConnection = { + edges: Array + pageInfo: PageInfo + totalCount: Scalars['Int']['output'] +} + export type App = { appChannels: Array appVideos: Array