├── source ├── index.ts └── test.ts ├── tos.md ├── packs ├── daet │ ├── .coda-pack.json │ └── pack.ts ├── ftx │ ├── .coda-pack.json │ └── pack.ts ├── middle │ ├── .coda-pack.json │ └── pack.ts ├── twitch │ ├── .coda-pack.json │ ├── logo.png │ ├── logo-white.png │ ├── config.ts │ ├── readme.md │ ├── params.ts │ ├── schemas.ts │ ├── types.ts │ ├── pack.ts │ └── api.ts ├── twitter │ ├── .coda-pack.json │ ├── params.ts │ ├── pack.ts │ ├── schemas.ts │ └── api.ts ├── upmoney │ ├── .coda-pack.json │ └── pack.ts ├── zapier │ ├── .coda-pack.json │ └── pack.ts ├── coingecko │ ├── .coda-pack.json │ ├── params.ts │ ├── README.md │ ├── pack.ts │ ├── types.ts │ ├── api.ts │ └── schemas.ts ├── cryptocom │ ├── .coda-pack.json │ └── pack.ts ├── formatting │ ├── .coda-pack.json │ ├── logo.jpg │ ├── banner.png │ └── pack.ts └── shared │ └── schemas.ts ├── privacy.md ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── automerge.yml │ └── bevry.yml ├── HISTORY.md ├── tsconfig.json ├── .vscode ├── settings.json └── launch.json ├── .editorconfig ├── .prettierignore ├── .gitignore ├── CONTRIBUTING.md ├── README.md ├── package.json └── LICENSE.md /source/index.ts: -------------------------------------------------------------------------------- 1 | export default '@todo' 2 | -------------------------------------------------------------------------------- /tos.md: -------------------------------------------------------------------------------- 1 | # Terms of Service 2 | 3 | No warranty. 4 | -------------------------------------------------------------------------------- /packs/daet/.coda-pack.json: -------------------------------------------------------------------------------- 1 | { 2 | "packId": 14018 3 | } 4 | -------------------------------------------------------------------------------- /packs/ftx/.coda-pack.json: -------------------------------------------------------------------------------- 1 | { 2 | "packId": 14017 3 | } 4 | -------------------------------------------------------------------------------- /packs/middle/.coda-pack.json: -------------------------------------------------------------------------------- 1 | { 2 | "packId": 11973 3 | } 4 | -------------------------------------------------------------------------------- /packs/twitch/.coda-pack.json: -------------------------------------------------------------------------------- 1 | { 2 | "packId": 14122 3 | } 4 | -------------------------------------------------------------------------------- /packs/twitter/.coda-pack.json: -------------------------------------------------------------------------------- 1 | { 2 | "packId": 11837 3 | } 4 | -------------------------------------------------------------------------------- /packs/upmoney/.coda-pack.json: -------------------------------------------------------------------------------- 1 | { 2 | "packId": 14020 3 | } 4 | -------------------------------------------------------------------------------- /packs/zapier/.coda-pack.json: -------------------------------------------------------------------------------- 1 | { 2 | "packId": 11840 3 | } 4 | -------------------------------------------------------------------------------- /privacy.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | No data is collected. 4 | -------------------------------------------------------------------------------- /packs/coingecko/.coda-pack.json: -------------------------------------------------------------------------------- 1 | { 2 | "packId": 11734 3 | } 4 | -------------------------------------------------------------------------------- /packs/cryptocom/.coda-pack.json: -------------------------------------------------------------------------------- 1 | { 2 | "packId": 14204 3 | } 4 | -------------------------------------------------------------------------------- /packs/formatting/.coda-pack.json: -------------------------------------------------------------------------------- 1 | { 2 | "packId": 14448 3 | } 4 | -------------------------------------------------------------------------------- /packs/twitch/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bevry/coda-packs/HEAD/packs/twitch/logo.png -------------------------------------------------------------------------------- /packs/formatting/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bevry/coda-packs/HEAD/packs/formatting/logo.jpg -------------------------------------------------------------------------------- /packs/daet/pack.ts: -------------------------------------------------------------------------------- 1 | import * as coda from '@codahq/packs-sdk' 2 | 3 | export const pack = coda.newPack() 4 | -------------------------------------------------------------------------------- /packs/formatting/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bevry/coda-packs/HEAD/packs/formatting/banner.png -------------------------------------------------------------------------------- /packs/ftx/pack.ts: -------------------------------------------------------------------------------- 1 | import * as coda from '@codahq/packs-sdk' 2 | 3 | export const pack = coda.newPack() 4 | -------------------------------------------------------------------------------- /packs/twitch/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bevry/coda-packs/HEAD/packs/twitch/logo-white.png -------------------------------------------------------------------------------- /packs/upmoney/pack.ts: -------------------------------------------------------------------------------- 1 | import * as coda from '@codahq/packs-sdk' 2 | 3 | export const pack = coda.newPack() 4 | -------------------------------------------------------------------------------- /packs/cryptocom/pack.ts: -------------------------------------------------------------------------------- 1 | import * as coda from '@codahq/packs-sdk' 2 | 3 | export const pack = coda.newPack() 4 | 5 | console.log('hello world') 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [balupton] 2 | patreon: bevry 3 | open_collective: bevry 4 | ko_fi: balupton 5 | liberapay: bevry 6 | custom: ['https://bevry.me/fund'] 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | day: sunday 8 | -------------------------------------------------------------------------------- /packs/twitch/config.ts: -------------------------------------------------------------------------------- 1 | export const clientId = 'eq160p3kfxhm46e1nn24o5l602xk3z' 2 | export const minute = 60 3 | export const hour = minute * 60 4 | export const day = hour * 24 5 | export const width = 285 6 | export const height = 380 7 | -------------------------------------------------------------------------------- /source/test.ts: -------------------------------------------------------------------------------- 1 | import { equal } from 'assert-helpers' 2 | import kava from 'kava' 3 | 4 | kava.suite('@bevry/coda-packs', function (suite, test) { 5 | test('no tests yet', function () { 6 | console.log('no tests yet') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | ## v0.1.0 2023 November 1 4 | 5 | - Updated dependencies, [base files](https://github.com/bevry/base), and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) 6 | 7 | ## v1.0.0 2015 October 25 8 | 9 | - Some feature 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "esModuleInterop": true, 5 | "isolatedModules": true, 6 | "maxNodeModuleJsDepth": 5, 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "target": "ES2017", 10 | "module": "ESNext" 11 | }, 12 | "include": ["source"] 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: automerge 2 | 'on': 3 | - pull_request 4 | jobs: 5 | automerge: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 10 | with: 11 | github-token: ${{ secrets.DEPENDABOT_AUTOMERGE_GITHUB_TOKEN }} 12 | -------------------------------------------------------------------------------- /packs/twitch/readme.md: -------------------------------------------------------------------------------- 1 | # [Twitch Pack for Coda](https://coda.io/packs/twitch-14122) 2 | 3 | Inspired by [John-Mark Strickland](https://devpost.com/johnmarkstrickland)'s [Twitch Pack](https://devpost.com/software/twitch-pack-integration), as a Twitch streamer myself, I want a quick way to update my stream information (title, tags, category) between several templates (a use case John's Pack currently does not support). 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "files.exclude": { 4 | "**/.coda.json": true, 5 | "**/node_modules": true, 6 | "**/patches": true, 7 | "**/package-lock.json": true 8 | }, 9 | "cSpell.words": [ 10 | "alexa", 11 | "bitcointalk", 12 | "codahq", 13 | "datetime", 14 | "defi", 15 | "icos", 16 | "mcap", 17 | "sats", 18 | "sparkline" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Upload Pack File", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeExecutable": "npx", 9 | // https://code.visualstudio.com/docs/editor/variables-reference 10 | "args": ["coda", "upload", "${relativeFileDirname}/pack.ts"], 11 | "cwd": "${workspaceRoot}", 12 | "console": "integratedTerminal" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # 2023 June 22 2 | # https://github.com/bevry/base 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = false 11 | indent_style = tab 12 | 13 | [{*.mk,*.py}] 14 | indent_style = tab 15 | indent_size = 4 16 | 17 | [*.md] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | [{*.json,*.lsrules,*.yaml,*.yml,*.bowerrc,*.babelrc,*.code-workspace}] 22 | indent_style = space 23 | indent_size = 2 24 | 25 | [{*.json,*.lsrules}] 26 | insert_final_newline = true 27 | -------------------------------------------------------------------------------- /packs/twitter/params.ts: -------------------------------------------------------------------------------- 1 | import * as coda from '@codahq/packs-sdk' 2 | 3 | export const TweetMessageParam = coda.makeParameter({ 4 | name: 'content', 5 | description: 'Content to Tweet', 6 | type: coda.ParameterType.String, 7 | optional: false, 8 | }) 9 | 10 | export const UserIdParam = coda.makeParameter({ 11 | name: 'user', 12 | description: 'User Identifier', 13 | type: coda.ParameterType.String, 14 | optional: false, 15 | }) 16 | 17 | export const TweetIdParam = coda.makeParameter({ 18 | name: 'tweet', 19 | description: 'Tweet Identifier', 20 | type: coda.ParameterType.String, 21 | optional: false, 22 | }) 23 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # 2023 March 24 2 | # https://github.com/bevry/base 3 | 4 | # VCS Files 5 | .git 6 | .svn 7 | .hg 8 | 9 | # System Files 10 | **/.DS_Store 11 | 12 | # Temp Files 13 | **/.docpad.db 14 | **/*.log 15 | **/*.cpuprofile 16 | **/*.heapsnapshot 17 | 18 | # Yarn Files 19 | .yarn/* 20 | !.yarn/releases 21 | !.yarn/plugins 22 | !.yarn/sdks 23 | !.yarn/versions 24 | .pnp.* 25 | .pnp/ 26 | 27 | # Build Caches 28 | build/ 29 | components/ 30 | bower_components/ 31 | node_modules/ 32 | 33 | # Build Outputs 34 | **/out.* 35 | **/*.out.* 36 | **/out/ 37 | **/output/ 38 | *compiled* 39 | edition*/ 40 | coffeejs/ 41 | coffee/ 42 | es5/ 43 | es2015/ 44 | esnext/ 45 | docs/ 46 | 47 | # Development Files 48 | test/ 49 | **/*fixtures* 50 | 51 | # Ecosystem Caches 52 | .trunk/*/ 53 | 54 | # ===================================== 55 | # CUSTOM 56 | 57 | # None 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2020 June 3 2 | # https://github.com/bevry/base 3 | 4 | # System Files 5 | **/.DS_Store 6 | 7 | # Temp Files 8 | **/.docpad.db 9 | **/*.log 10 | **/*.cpuprofile 11 | **/*.heapsnapshot 12 | 13 | # Editor Files 14 | .c9/ 15 | .vscode/ 16 | 17 | # Yarn Files 18 | .yarn/* 19 | !.yarn/releases 20 | !.yarn/plugins 21 | !.yarn/sdks 22 | !.yarn/versions 23 | .pnp.* 24 | .pnp/ 25 | 26 | # Private Files 27 | .env 28 | .idea 29 | .cake_task_cache 30 | 31 | # Build Caches 32 | build/ 33 | bower_components/ 34 | node_modules/ 35 | .next/ 36 | 37 | # ------------------------------------- 38 | # CDN Inclusions, Git Exclusions 39 | 40 | # Build Outputs 41 | **/out.* 42 | **/*.out.* 43 | **/out/ 44 | **/output/ 45 | *compiled* 46 | edition*/ 47 | coffeejs/ 48 | coffee/ 49 | es5/ 50 | es2015/ 51 | esnext/ 52 | docs/ 53 | 54 | # ===================================== 55 | # CUSTOM 56 | 57 | .now 58 | _upload_build/ 59 | **/patches/ 60 | **/node_modules/ 61 | **/.coda.json 62 | **/.coda-credentials.json 63 | !.vscode/ 64 | -------------------------------------------------------------------------------- /packs/coingecko/params.ts: -------------------------------------------------------------------------------- 1 | import * as coda from '@codahq/packs-sdk' 2 | 3 | export const CoinSearchParam = coda.makeParameter({ 4 | name: 'Coin Search', 5 | description: 6 | 'Searches for coins that match the search query, if not search query is provided, will fetch the trending coins.', 7 | type: coda.ParameterType.String, 8 | suggestedValue: 'Bitcoin', 9 | optional: false, 10 | }) 11 | 12 | export const CoinInputParam = coda.makeParameter({ 13 | name: 'Coin Identifier or URL or JSON', 14 | description: 15 | 'Use `SearchCoins("query").First().id` to discover the Coin Identifier', 16 | type: coda.ParameterType.String, 17 | suggestedValue: 'bitcoin', 18 | }) 19 | 20 | export const CurrencyParam = coda.makeParameter({ 21 | name: 'Currency Identifier', 22 | description: 'Use `GetCurrencies` to get a list of supported currencies', 23 | type: coda.ParameterType.String, 24 | suggestedValue: 'usd', 25 | optional: true, 26 | }) 27 | 28 | export const WhenParam = coda.makeParameter({ 29 | name: 'When', 30 | description: 'The date to fetch historical data for.', 31 | type: coda.ParameterType.Date, 32 | optional: true, 33 | }) 34 | -------------------------------------------------------------------------------- /packs/middle/pack.ts: -------------------------------------------------------------------------------- 1 | import * as coda from '@codahq/packs-sdk' 2 | export const pack = coda.newPack() 3 | 4 | pack.addFormula({ 5 | name: 'MiddleNumber', 6 | description: 'Returns the middle item in a list of numbers.', 7 | parameters: [ 8 | coda.makeParameter({ 9 | type: coda.ParameterType.NumberArray, 10 | name: 'numbers', 11 | description: 'The numbers to perform the calculation on.', 12 | }), 13 | ], 14 | resultType: coda.ValueType.Number, 15 | execute: async function ([list]) { 16 | if (list.length === 0) { 17 | throw new coda.UserVisibleError('The list cannot be empty.') 18 | } 19 | return list[Math.floor(list.length / 2)] 20 | }, 21 | }) 22 | 23 | pack.addFormula({ 24 | name: 'MiddleAverageNumber', 25 | description: 'Returns the middle average number in a list of numbers.', 26 | parameters: [ 27 | coda.makeParameter({ 28 | type: coda.ParameterType.NumberArray, 29 | name: 'numbers', 30 | description: 'The numbers to perform the calculation on.', 31 | }), 32 | ], 33 | resultType: coda.ValueType.Number, 34 | execute: async function ([list]) { 35 | if (list.length === 0) { 36 | throw new coda.UserVisibleError('The list cannot be empty.') 37 | } 38 | const sublist = list.slice( 39 | Math.floor(list.length / 3), 40 | Math.ceil((list.length / 3) * 2), 41 | ) 42 | return sublist.reduce((a, b) => a + b, 0) / sublist.length 43 | }, 44 | }) 45 | -------------------------------------------------------------------------------- /.github/workflows/bevry.yml: -------------------------------------------------------------------------------- 1 | name: bevry 2 | 'on': 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | os: 10 | - ubuntu-latest 11 | node: 12 | - '18' 13 | - '20' 14 | - '21' 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Install desired Node.js version 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: '20' 22 | - run: npm run our:setup 23 | - run: npm run our:compile 24 | - run: npm run our:verify 25 | - name: Install targeted Node.js 26 | if: ${{ matrix.node != 20 }} 27 | uses: actions/setup-node@v2 28 | with: 29 | node-version: ${{ matrix.node }} 30 | - run: npm test 31 | publish: 32 | if: ${{ github.event_name == 'push' }} 33 | needs: test 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v2 37 | - name: Install desired Node.js version 38 | uses: actions/setup-node@v2 39 | with: 40 | node-version: '20' 41 | - run: npm run our:setup 42 | - run: npm run our:compile 43 | - run: npm run our:meta 44 | - name: publish to surge 45 | uses: bevry-actions/surge@v1.0.3 46 | with: 47 | surgeLogin: ${{ secrets.SURGE_LOGIN }} 48 | surgeToken: ${{ secrets.SURGE_TOKEN }} 49 | -------------------------------------------------------------------------------- /packs/shared/schemas.ts: -------------------------------------------------------------------------------- 1 | import * as coda from '@codahq/packs-sdk' 2 | 3 | export const usdSchema: coda.CurrencySchema = { 4 | type: coda.ValueType.Number, 5 | codaType: coda.ValueHintType.Currency, 6 | currencyCode: 'USD', 7 | precision: 2, 8 | // format?: CurrencyFormat; 9 | } 10 | export const currencySchema: coda.CurrencySchema = { 11 | type: coda.ValueType.Number, 12 | codaType: coda.ValueHintType.Currency, 13 | precision: 2, 14 | } 15 | export const percentSchema: coda.NumberSchema = { 16 | type: coda.ValueType.Number, 17 | codaType: coda.ValueHintType.Percent, 18 | useThousandsSeparator: true, 19 | precision: 2, 20 | } 21 | export const booleanSchema: coda.BooleanSchema = { 22 | type: coda.ValueType.Boolean, 23 | } 24 | export const minutesSchema: coda.NumberSchema = { 25 | type: coda.ValueType.Number, 26 | useThousandsSeparator: true, 27 | } 28 | export const stringSchema: coda.StringSchema = { 29 | type: coda.ValueType.String, 30 | } 31 | export const numberSchema: coda.NumberSchema = { 32 | type: coda.ValueType.Number, 33 | useThousandsSeparator: true, 34 | precision: 2, 35 | } 36 | export const datetimeStringSchema: 37 | | coda.StringSchema 38 | | coda.StringDateTimeSchema = { 39 | type: coda.ValueType.String, 40 | codaType: coda.ValueHintType.DateTime, 41 | } 42 | export const datetimeNumberSchema: 43 | | coda.NumberSchema 44 | | coda.NumericDateTimeSchema = { 45 | type: coda.ValueType.Number, 46 | codaType: coda.ValueHintType.DateTime, 47 | } 48 | export const urlSchema: coda.StringSchema = { 49 | type: coda.ValueType.String, 50 | codaType: coda.ValueHintType.Url, 51 | } 52 | export const imageSchema: coda.StringSchema = { 53 | type: coda.ValueType.String, 54 | codaType: coda.ValueHintType.ImageReference, 55 | } 56 | export const linkSchema: coda.ArraySchema = { 57 | type: coda.ValueType.Array, 58 | items: { 59 | type: coda.ValueType.String, 60 | }, 61 | } 62 | -------------------------------------------------------------------------------- /packs/twitch/params.ts: -------------------------------------------------------------------------------- 1 | import * as coda from '@codahq/packs-sdk' 2 | 3 | export const UserIdentifierParam = coda.makeParameter({ 4 | name: 'User Identifier', 5 | description: 'User Identifier', 6 | type: coda.ParameterType.String, 7 | optional: false, 8 | }) 9 | 10 | export const ChannelIdentifierParam = coda.makeParameter({ 11 | name: 'Channel Identifier', 12 | description: 'Channel Identifier', 13 | type: coda.ParameterType.String, 14 | optional: false, 15 | }) 16 | 17 | export const TagIdentifierParam = coda.makeParameter({ 18 | name: 'Tag Identifier', 19 | description: 'Tag Identifier', 20 | type: coda.ParameterType.String, 21 | optional: false, 22 | }) 23 | 24 | export const CategoryIdentifierParam = coda.makeParameter({ 25 | name: 'Category Identifier', 26 | description: 'Category Identifier', 27 | type: coda.ParameterType.String, 28 | optional: false, 29 | }) 30 | 31 | export const CategoryNameParam = coda.makeParameter({ 32 | name: 'Category Name', 33 | description: 'Category Name', 34 | type: coda.ParameterType.String, 35 | optional: false, 36 | }) 37 | 38 | export const StreamTitleParam = coda.makeParameter({ 39 | name: 'stream_title', 40 | description: 'Stream Title', 41 | optional: true, 42 | type: coda.ParameterType.String, 43 | }) 44 | 45 | export const StreamDelayParam = coda.makeParameter({ 46 | name: 'stream_delay', 47 | description: 'Stream Delay', 48 | optional: true, 49 | type: coda.ParameterType.Number, 50 | }) 51 | 52 | export const LanguageParam = coda.makeParameter({ 53 | name: 'language', 54 | description: 'Language Identifier (ISO 639-1 two-letter code)', 55 | optional: true, 56 | type: coda.ParameterType.String, 57 | }) 58 | 59 | export const SearchQueryParam = coda.makeParameter({ 60 | name: 'query', 61 | description: 'Search Query', 62 | optional: false, 63 | type: coda.ParameterType.String, 64 | }) 65 | 66 | export const SearchLiveOnlyParam = coda.makeParameter({ 67 | name: 'live_only', 68 | description: 'Search Live Only', 69 | optional: true, 70 | type: coda.ParameterType.Boolean, 71 | }) 72 | -------------------------------------------------------------------------------- /packs/coingecko/README.md: -------------------------------------------------------------------------------- 1 | ## Inspiration 2 | 3 | I had coded a CoinGecko pack via the Web Interface as soon as packs became available. It has been used in several of my documents, from cryptocurrency project analysis, to cryptocurrency portfolio analysis. The Hackathon was the motivation to improve it for public consumption, and with parity with the CoinGecko API, as it was the most suitable pack to learn advanced Schemas and Sync Tables, to unlock further ambitions with Coda. 4 | 5 | ## What it does 6 | 7 | Fetches cryptocurrency data from CoinGecko, the largest cryptocurrency data API. 8 | 9 | ## How I built it 10 | 11 | Originally with the Web Interface with JavaScript, then for the Hackathon rewrote it with TypeScript using the `@codahq/packs-sdk` CLI for deployment, and added the additional functionality to make it a top quality submission. This process was livestream on Twitch at https://twitch.tv/balupton 12 | 13 | ## Challenges we ran into 14 | 15 | Several challenges along the way, using `schema` or `items`, wrestling with TypeScript type errors, wrestling with CLI upload errors such as `Error in field at path "formats[0]": Could not find a formula definition for this format. Each format must reference the name of a formula defined in this pack.` which could do with a line number. 16 | 17 | The final result uncovered this Coda bug: 18 | 19 | 1. Numbers with huge decimal values fail to convert into currencies. 20 | 21 | And demonstrates the need for these improvements: 22 | 23 | 1. If the column format matches the formula object, do not refetch but use the result. 24 | 2. A schema's objects, and their properties, should be available via the "Add Column" dropdown without needing to manually create a matching column format for them first. 25 | 3. String schemas should be able to specify "Canvas" as a hint 26 | 4. Object schemas should be able to specify their identity column format as the hint, so manual conversion of the added column is not required (objects are added as text columns) 27 | 28 | A demonstration of these was livestreamed, and the link will be sent to Coda once the livestream has finished. 29 | 30 | ## Accomplishments that I'm proud of 31 | 32 | It works really well! 33 | 34 | ## What I learned 35 | 36 | 1. How to use the CLI 37 | 2. How smooth it is to use TypeScript and publish releases via the CLI 38 | 3. The building blocks for building more advanced types 39 | 40 | ## What's next for CoinGecko 41 | 42 | Once CoinGecko provides an API for portfolio management, I will incorporate that, probably as a Paid tier. 43 | 44 | I'll also use what I learned in this pack to code a FTX exchange pack, that will be able to bring in position data and even place advanced trades, such as using coda automations to place trades at specific price points. 45 | -------------------------------------------------------------------------------- /packs/zapier/pack.ts: -------------------------------------------------------------------------------- 1 | import * as coda from '@codahq/packs-sdk' 2 | export const pack = coda.newPack() 3 | 4 | pack.addNetworkDomain('hooks.zapier.com') 5 | 6 | pack.addFormula({ 7 | name: 'ZapierHook', 8 | description: 'Trigger a Zapier Hook', 9 | parameters: [ 10 | coda.makeParameter({ 11 | type: coda.ParameterType.String, 12 | name: 'url', 13 | description: 'The Webhook URL of the Zapier Zap', 14 | }), 15 | coda.makeParameter({ 16 | type: coda.ParameterType.String, 17 | name: 'body', 18 | description: 'Body for a Zapier Catch Raw Hook', 19 | optional: true, 20 | }), 21 | coda.makeParameter({ 22 | type: coda.ParameterType.StringArray, 23 | name: 'bodyParams', 24 | description: 25 | 'Body Params in the form of List(key, value, ...) for a Zapier Catch Hook', 26 | optional: true, 27 | }), 28 | coda.makeParameter({ 29 | type: coda.ParameterType.StringArray, 30 | name: 'queryParams', 31 | description: 32 | 'Query/Search Params in the form of List(key, value, ...) for a Zapier Catch Hook', 33 | optional: true, 34 | }), 35 | ], 36 | resultType: coda.ValueType.String, 37 | isAction: true, 38 | execute: async function ( 39 | [where, body, bodyParamsList = [], queryParamsList = []], 40 | context, 41 | ) { 42 | // verify 43 | if (body && (bodyParamsList.length || queryParamsList.length)) { 44 | throw new coda.UserVisibleError( 45 | [ 46 | 'You cannot use body with bodyParams nor queryParams.', 47 | 'Catch Raw Hooks only use body.', 48 | 'Catch Hooks only use either bodyParams, queryParams, or both.', 49 | 'See https://coda.io/@balupton/zapier-hooks-pack#_luaQX for details.', 50 | ].join('\n'), 51 | ) 52 | } 53 | 54 | // generate body 55 | if (!body && bodyParamsList.length) { 56 | const bodyParamsRecord: Record = {} 57 | while (bodyParamsList.length) { 58 | const key = bodyParamsList.shift() as string 59 | const value = bodyParamsList.length 60 | ? (bodyParamsList.shift() as string) 61 | : '' 62 | bodyParamsRecord[key] = value 63 | } 64 | body = JSON.stringify(bodyParamsRecord) 65 | } 66 | 67 | // generate url 68 | const queryParamsRecord: Record = {} 69 | while (queryParamsList.length) { 70 | const key = queryParamsList.shift() as string 71 | const value = queryParamsList.length 72 | ? (queryParamsList.shift() as string) 73 | : '' 74 | queryParamsRecord[key] = value 75 | } 76 | const url = coda.withQueryParams(where, queryParamsRecord) 77 | 78 | // request with the optional body 79 | const request: coda.FetchRequest = { 80 | url, 81 | method: 'POST', 82 | } 83 | if (body) request.body = body 84 | 85 | // return the response 86 | try { 87 | const response = await context.fetcher.fetch(request) 88 | const result = response.body || '' 89 | return result 90 | } catch (rawError: any) { 91 | if (rawError.statusCode) { 92 | const error = rawError as coda.StatusCodeError 93 | throw new coda.UserVisibleError( 94 | `Required failed with error ${error.statusCode}`, 95 | ) 96 | } 97 | throw new coda.UserVisibleError('Request failed.') 98 | } 99 | }, 100 | }) 101 | -------------------------------------------------------------------------------- /packs/twitter/pack.ts: -------------------------------------------------------------------------------- 1 | import * as coda from '@codahq/packs-sdk' 2 | import { 3 | ActiveUser, 4 | DeleteTweetById, 5 | SendTweet, 6 | TweetsByActiveUser, 7 | TweetsByUserId, 8 | } from './api' 9 | import { TweetIdParam, TweetMessageParam, UserIdParam } from './params' 10 | import { TweetSchema, UserSchema } from './schemas' 11 | 12 | export const pack = coda.newPack() 13 | pack.addNetworkDomain('twitter.com') 14 | 15 | // User 16 | // https://developer.twitter.com/en/docs/authentication/oauth-2-0/user-access-token 17 | // https://developer.twitter.com/en/docs/authentication/oauth-2-0/authorization-code 18 | pack.setUserAuthentication({ 19 | type: coda.AuthenticationType.OAuth2, 20 | authorizationUrl: 'https://twitter.com/i/oauth2/authorize', 21 | tokenUrl: 'https://api.twitter.com/2/oauth2/token', 22 | scopes: ['tweet.read', 'tweet.write', 'users.read', 'offline.access'], 23 | useProofKeyForCodeExchange: true, 24 | getConnectionName: async function (context) { 25 | const user = await ActiveUser([], context) 26 | return user.username 27 | }, 28 | }) 29 | 30 | // User 31 | pack.addFormula({ 32 | name: 'ActiveUser', 33 | description: 'Active User', 34 | parameters: [], 35 | resultType: coda.ValueType.Object, 36 | schema: UserSchema, 37 | extraOAuthScopes: ['tweet.read', 'users.read'], 38 | execute: ActiveUser, 39 | }) 40 | 41 | // Tweet 42 | pack.addFormula({ 43 | name: 'SendTweet', 44 | description: 'Send Tweet', 45 | parameters: [TweetMessageParam], 46 | resultType: coda.ValueType.String, 47 | isAction: true, 48 | extraOAuthScopes: ['tweet.read', 'tweet.write', 'users.read'], 49 | execute: SendTweet, 50 | }) 51 | 52 | // Tweets by Active User 53 | pack.addFormula({ 54 | name: 'TweetsByActiveUser', 55 | description: 'Fetch Tweets for the Active User', 56 | parameters: [], 57 | resultType: coda.ValueType.Array, 58 | items: TweetSchema, 59 | extraOAuthScopes: ['tweet.read', 'users.read'], 60 | execute: TweetsByActiveUser, 61 | }) 62 | 63 | // Tweets by User Id 64 | pack.addFormula({ 65 | name: 'TweetsByUserId', 66 | description: 'Fetch Tweets by User Identifier', 67 | parameters: [UserIdParam], 68 | resultType: coda.ValueType.Array, 69 | items: TweetSchema, 70 | extraOAuthScopes: ['tweet.read', 'users.read'], 71 | execute: TweetsByUserId, 72 | }) 73 | 74 | // Delete Tweet 75 | pack.addFormula({ 76 | name: 'DeleteTweetById', 77 | description: 'Delete a Tweet by Tweet Identifier', 78 | parameters: [TweetIdParam], 79 | resultType: coda.ValueType.Boolean, 80 | isAction: true, 81 | extraOAuthScopes: ['tweet.read', 'tweet.write', 'users.read'], 82 | execute: DeleteTweetById, 83 | }) 84 | 85 | // ==================================== 86 | // CODA SYNC TABLES 87 | 88 | // Tweets by Active User 89 | pack.addSyncTable({ 90 | name: 'TweetsByActiveUser', 91 | schema: TweetSchema, 92 | identityName: 'Tweet', 93 | formula: { 94 | name: 'TweetsByActiveUser', 95 | description: 'Sync Tweets by the Active User.', 96 | parameters: [], 97 | execute: async function ([], context) { 98 | const tweets = await TweetsByActiveUser([], context) 99 | return { result: tweets } 100 | }, 101 | }, 102 | }) 103 | 104 | // ==================================== 105 | // CODA COLUMN FORMATS 106 | 107 | // pack.addColumnFormat({ 108 | // name: 'UserById', 109 | // formulaName: 'UserById', 110 | // }) 111 | 112 | // pack.addColumnFormat({ 113 | // name: 'Tweet', 114 | // formulaName: 'TweetById', 115 | // }) 116 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Before You Post! 7 | 8 | ## Support 9 | 10 | We offer support through our [Official Support Channels](https://bevry.me/support). Do not use GitHub Issues for support, your issue will be closed. 11 | 12 | ## Contribute 13 | 14 | Our [Contributing Guide](https://bevry.me/contribute) contains useful tips and suggestions for how to contribute to this project, it's worth the read. 15 | 16 | ## Development 17 | 18 | ### Setup 19 | 20 | 1. [Install Node.js](https://bevry.me/install/node) 21 | 22 | 1. Fork the project and clone your fork - [guide](https://help.github.com/articles/fork-a-repo/) 23 | 24 | 1. Setup the project for development 25 | 26 | ```bash 27 | npm run our:setup 28 | ``` 29 | 30 | ### Developing 31 | 32 | 1. Compile changes 33 | 34 | ```bash 35 | npm run our:compile 36 | ``` 37 | 38 | 1. Run tests 39 | 40 | ```bash 41 | npm test 42 | ``` 43 | 44 | ### Publishing 45 | 46 | Follow these steps in order to implement your changes/improvements into your desired project: 47 | 48 | #### Preparation 49 | 50 | 1. Make sure your changes are on their own branch that is branched off from master. 51 | 52 | 1. You can do this by: `git checkout master; git checkout -b your-new-branch` 53 | 1. And push the changes up by: `git push origin your-new-branch` 54 | 55 | 1. Ensure all tests pass: 56 | 57 | ```bash 58 | npm test 59 | ``` 60 | 61 | > If possible, add tests for your change, if you don't know how, mention this in your pull request 62 | 63 | 1. Ensure the project is ready for publishing: 64 | 65 | ``` 66 | npm run our:release:prepare 67 | ``` 68 | 69 | #### Pull Request 70 | 71 | To send your changes for the project owner to merge in: 72 | 73 | 1. Submit your pull request 74 | 1. When submitting, if the original project has a `dev` or `integrate` branch, use that as the target branch for your pull request instead of the default `master` 75 | 1. By submitting a pull request you agree for your changes to have the same license as the original plugin 76 | 77 | #### Publish 78 | 79 | To publish your changes as the project owner: 80 | 81 | 1. Switch to the master branch: 82 | 83 | ```bash 84 | git checkout master 85 | ``` 86 | 87 | 1. Merge in the changes of the feature branch (if applicable) 88 | 89 | 1. Increment the version number in the `package.json` file according to the [semantic versioning](http://semver.org) standard, that is: 90 | 91 | 1. `x.0.0` MAJOR version when you make incompatible API changes (note: DocPad plugins must use v2 as the major version, as v2 corresponds to the current DocPad v6.x releases) 92 | 1. `x.y.0` MINOR version when you add functionality in a backwards-compatible manner 93 | 1. `x.y.z` PATCH version when you make backwards-compatible bug fixes 94 | 95 | 1. Add an entry to the changelog following the format of the previous entries, an example of this is: 96 | 97 | ```markdown 98 | ## v6.29.0 2013 April 1 99 | 100 | - Progress on [issue #474](https://github.com/docpad/docpad/issues/474) 101 | - DocPad will now set permissions based on the process's ability 102 | - Thanks to [Avi Deitcher](https://github.com/deitch), [Stephan Lough](https://github.com/stephanlough) for [issue #165](https://github.com/docpad/docpad/issues/165) 103 | - Updated dependencies 104 | ``` 105 | 106 | 1. Commit the changes with the commit title set to something like `v6.29.0. Bugfix. Improvement.` and commit description set to the changelog entry 107 | 108 | 1. Ensure the project is ready for publishing: 109 | 110 | ``` 111 | npm run our:release:prepare 112 | ``` 113 | 114 | 1. Prepare the release and publish it to npm and git: 115 | 116 | ```bash 117 | npm run our:release 118 | ``` 119 | -------------------------------------------------------------------------------- /packs/twitter/schemas.ts: -------------------------------------------------------------------------------- 1 | import * as coda from '@codahq/packs-sdk' 2 | import { AttributionNode } from '@codahq/packs-sdk' 3 | import { components } from 'twitter-api-sdk/dist/types' 4 | 5 | const attribution: AttributionNode[] = [ 6 | { 7 | type: coda.AttributionNodeType.Text, 8 | text: 'Provided by Twitter', 9 | }, 10 | { 11 | type: coda.AttributionNodeType.Link, 12 | anchorText: 'twitter.com', 13 | anchorUrl: 'https://twitter.com', 14 | }, 15 | { 16 | type: coda.AttributionNodeType.Image, 17 | imageUrl: 'https://twitter.com/favicon.ico', 18 | anchorUrl: 'https://twitter.com', 19 | }, 20 | ] 21 | 22 | import { 23 | usdSchema, 24 | currencySchema, 25 | percentSchema, 26 | minutesSchema, 27 | stringSchema, 28 | numberSchema, 29 | datetimeStringSchema, 30 | datetimeNumberSchema, 31 | urlSchema, 32 | imageSchema, 33 | linkSchema, 34 | booleanSchema, 35 | } from '../shared/schemas' 36 | 37 | // ==================================== 38 | // USER 39 | 40 | export type RawUser = components['schemas']['User'] 41 | export interface User { 42 | id: string 43 | username: string 44 | name?: string 45 | description?: string 46 | homepage?: string 47 | image?: string 48 | created_at?: string 49 | } 50 | export const UserSchema = coda.makeObjectSchema({ 51 | properties: { 52 | id: { 53 | description: 'Identifier', 54 | required: true, 55 | ...stringSchema, 56 | }, 57 | username: { 58 | description: 'Username', 59 | required: true, 60 | ...stringSchema, 61 | }, 62 | name: { 63 | description: 'Name', 64 | required: false, 65 | ...stringSchema, 66 | }, 67 | description: { 68 | description: 'Description', 69 | required: false, 70 | ...stringSchema, 71 | }, 72 | homepage: { 73 | description: 'Homepage', 74 | required: false, 75 | fromKey: 'url', 76 | ...urlSchema, 77 | }, 78 | image: { 79 | description: 'Image', 80 | required: false, 81 | fromKey: 'profile_image_url', 82 | ...imageSchema, 83 | }, 84 | created_at: { 85 | description: 'Creation time', 86 | required: false, 87 | ...datetimeStringSchema, 88 | }, 89 | }, 90 | identity: { 91 | name: 'User', 92 | }, 93 | idProperty: 'id', 94 | displayProperty: 'username', 95 | descriptionProperty: 'description', 96 | imageProperty: 'image', 97 | attribution, 98 | includeUnknownProperties: false, 99 | }) 100 | 101 | // ==================================== 102 | // Tweet 103 | 104 | export type RawTweet = components['schemas']['Tweet'] 105 | export interface Tweet { 106 | id: string 107 | content: string 108 | created_at: string 109 | author_id: string 110 | conversation_id: string 111 | in_reply_to_user_id: string 112 | // source?: string 113 | } 114 | export const TweetSchema = coda.makeObjectSchema({ 115 | properties: { 116 | id: { 117 | // Unique identifier of this Tweet. This is returned as a string in order to avoid complications with languages and tools that cannot handle large integers. 118 | description: 'Tweet ID', 119 | required: true, 120 | ...stringSchema, 121 | }, 122 | content: { 123 | // The content of the Tweet. 124 | description: 'Content', 125 | required: true, 126 | fromKey: 'text', 127 | ...stringSchema, 128 | }, 129 | created_at: { 130 | // Creation time of the Tweet. For example: 2020-12-10T20:00:10Z 131 | description: `Creation Time`, 132 | required: true, 133 | ...datetimeStringSchema, 134 | }, 135 | author_id: { 136 | // Unique identifier of this user. This is returned as a string in order to avoid complications with languages and tools that cannot handle large integers. 137 | description: 'User ID of Tweet Author', 138 | required: true, 139 | ...stringSchema, 140 | }, 141 | conversation_id: { 142 | // The Tweet ID of the original Tweet of the conversation (which includes direct replies, replies of replies). 143 | description: 'Conversation Tweet ID', 144 | required: true, 145 | ...stringSchema, 146 | }, 147 | in_reply_to_user_id: { 148 | // If this Tweet is a Reply, indicates the user ID of the parent Tweet's author. This is returned as a string in order to avoid complications with languages and tools that cannot handle large integers. 149 | description: `User ID of Parent Tweet`, 150 | required: true, 151 | ...stringSchema, 152 | }, 153 | }, 154 | identity: { 155 | name: 'Tweet', 156 | }, 157 | idProperty: 'id', 158 | displayProperty: 'content', 159 | // title, subtitle 160 | // featuredProperties: ['symbol', 'name', 'url', 'image'], 161 | attribution, 162 | includeUnknownProperties: false, 163 | }) 164 | -------------------------------------------------------------------------------- /packs/twitter/api.ts: -------------------------------------------------------------------------------- 1 | import * as coda from '@codahq/packs-sdk' 2 | 3 | import { 4 | createTweet, 5 | deleteTweetById, 6 | findMyUser, 7 | TwitterBody, 8 | TwitterParams, 9 | TwitterResponse, 10 | usersIdTweets, 11 | } from 'twitter-api-sdk/dist/types' 12 | 13 | // don't use caching, as data is always changing 14 | // const minute = 60 15 | // const hour = minute * 60 16 | // const day = hour * 24 17 | 18 | // https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me 19 | // tweet.read users.read 20 | export async function ActiveUser( 21 | []: [], 22 | // [user_fields = []]: [Array['user.fields']>?], 23 | context: coda.SyncExecutionContext | coda.ExecutionContext, 24 | ) { 25 | // fetch 26 | const query: TwitterParams = { 27 | // @ts-ignore 28 | // 'user.fields': user_fields.join(','), 29 | } 30 | const url = coda.withQueryParams('https://api.twitter.com/2/users/me', query) 31 | const response = await context.fetcher.fetch>({ 32 | url, 33 | method: 'GET', 34 | }) 35 | 36 | // verify 37 | if (!response.body || response.body.errors || !response.body.data) { 38 | throw new coda.UserVisibleError(`Failed to fetch user data.`) 39 | } 40 | 41 | // return 42 | const result = response.body.data! 43 | return result 44 | } 45 | 46 | // https://developer.twitter.com/en/docs/twitter-api/tweets/manage-tweets/api-reference/post-tweets 47 | // tweet.read tweet.write users.read 48 | export async function SendTweet( 49 | [message]: [string], 50 | context: coda.SyncExecutionContext | coda.ExecutionContext, 51 | ) { 52 | // check 53 | if (!message) { 54 | throw new coda.UserVisibleError(`A message is required to send a tweet.`) 55 | } 56 | 57 | // fetch 58 | const body: TwitterBody = { 59 | text: message, 60 | } 61 | const response = await context.fetcher.fetch>({ 62 | url: 'https://api.twitter.com/2/tweets', 63 | method: 'POST', 64 | headers: { 65 | 'Content-Type': 'application/json', 66 | }, 67 | body: JSON.stringify(body), 68 | }) 69 | 70 | // verify 71 | if (!response.body || response.body.errors || !response.body.data) { 72 | throw new coda.UserVisibleError(`Failed to send tweet.`) 73 | } 74 | 75 | // return 76 | const result = response.body.data! 77 | return result.id 78 | } 79 | 80 | // https://developer.twitter.com/en/docs/twitter-api/tweets/timelines/api-reference/get-users-id-tweets 81 | // tweet.read users.read 82 | export async function TweetsByUserId( 83 | [id]: [string], 84 | context: coda.SyncExecutionContext | coda.ExecutionContext, 85 | ) { 86 | // check 87 | if (!id) { 88 | throw new coda.UserVisibleError( 89 | `A user identifier is required to fetch tweets for a user.`, 90 | ) 91 | } 92 | 93 | // fetch 94 | const query: TwitterParams = { 95 | max_results: 100, 96 | // @ts-ignore 97 | 'tweet.fields': [ 98 | 'id', 99 | 'author_id', 100 | 'created_at', 101 | 'conversation_id', 102 | 'in_reply_to_user_id', 103 | ].join(','), 104 | } 105 | const url = coda.withQueryParams( 106 | `https://api.twitter.com/2/users/${id}/tweets`, 107 | query, 108 | ) 109 | const response = await context.fetcher.fetch>({ 110 | url: url, 111 | method: 'GET', 112 | headers: { 113 | 'Content-Type': 'application/json', 114 | }, 115 | }) 116 | 117 | // verify 118 | if (!response.body || response.body.errors || !response.body.data) { 119 | throw new coda.UserVisibleError(`Failed to send tweet.`) 120 | } 121 | 122 | // return 123 | const result = response.body.data! 124 | return result 125 | } 126 | 127 | export async function TweetsByActiveUser( 128 | []: [], 129 | context: coda.SyncExecutionContext | coda.ExecutionContext, 130 | ) { 131 | const user = await ActiveUser([], context) 132 | const tweets = await TweetsByUserId([user.id], context) 133 | return tweets 134 | } 135 | 136 | // https://developer.twitter.com/en/docs/twitter-api/tweets/manage-tweets/api-reference/delete-tweets-id 137 | // tweet.read tweet.write users.read 138 | export async function DeleteTweetById( 139 | [id]: [string], 140 | context: coda.SyncExecutionContext | coda.ExecutionContext, 141 | ) { 142 | // check 143 | if (!id) { 144 | throw new coda.UserVisibleError( 145 | `A tweet identifier is required to delete a tweet.`, 146 | ) 147 | } 148 | 149 | // fetch 150 | const url = `https://api.twitter.com/2/tweets/${id}` 151 | const response = await context.fetcher.fetch< 152 | TwitterResponse 153 | >({ 154 | url: url, 155 | method: 'DELETE', 156 | headers: { 157 | 'Content-Type': 'application/json', 158 | }, 159 | }) 160 | 161 | // verify 162 | if (!response.body || response.body.errors || !response.body.data) { 163 | throw new coda.UserVisibleError(`Failed to delete tweet.`) 164 | } 165 | if (!response.body.data.deleted) { 166 | throw new coda.UserVisibleError(`Tweet was not deleted.`) 167 | } 168 | 169 | // return 170 | const result = response.body.data! 171 | return result.deleted 172 | } 173 | -------------------------------------------------------------------------------- /packs/twitch/schemas.ts: -------------------------------------------------------------------------------- 1 | import * as coda from '@codahq/packs-sdk' 2 | import { AttributionNode } from '@codahq/packs-sdk' 3 | 4 | const attribution: AttributionNode[] = [ 5 | { 6 | type: coda.AttributionNodeType.Text, 7 | text: 'Provided by Twitch', 8 | }, 9 | { 10 | type: coda.AttributionNodeType.Link, 11 | anchorText: 'twitch.tv', 12 | anchorUrl: 'https://twitch.tv', 13 | }, 14 | { 15 | type: coda.AttributionNodeType.Image, 16 | imageUrl: 'https://twitch.tv/favicon.ico', 17 | anchorUrl: 'https://twitch.tv', 18 | }, 19 | ] 20 | 21 | import { 22 | usdSchema, 23 | currencySchema, 24 | percentSchema, 25 | minutesSchema, 26 | stringSchema, 27 | numberSchema, 28 | datetimeStringSchema, 29 | datetimeNumberSchema, 30 | urlSchema, 31 | imageSchema, 32 | linkSchema, 33 | booleanSchema, 34 | } from '../shared/schemas' 35 | 36 | // ==================================== 37 | // USER & CHANNEL 38 | 39 | export const UserSchema = coda.makeObjectSchema({ 40 | properties: { 41 | id: { 42 | description: 'User’s Identifier', 43 | required: true, 44 | ...stringSchema, 45 | }, 46 | login: { 47 | description: 'User’s login name', 48 | required: true, 49 | ...stringSchema, 50 | }, 51 | name: { 52 | description: 'User’s display name', 53 | required: true, 54 | ...stringSchema, 55 | }, 56 | description: { 57 | description: 'User’s channel description', 58 | required: false, 59 | ...stringSchema, 60 | }, 61 | email: { 62 | description: 'User’s verified email address', 63 | required: false, 64 | ...stringSchema, 65 | }, 66 | promotion: { 67 | description: 'User’s broadcaster type: "partner", "affiliate", or "".', 68 | required: false, 69 | ...stringSchema, 70 | }, 71 | privilege: { 72 | description: 'User’s type: "staff", "admin", "global_mod", or ""', 73 | required: false, 74 | ...stringSchema, 75 | }, 76 | offline_image_url: { 77 | description: 'URL of the user’s offline image', 78 | required: false, 79 | ...imageSchema, 80 | }, 81 | profile_image_url: { 82 | description: 'URL of the user’s profile image', 83 | required: false, 84 | ...imageSchema, 85 | }, 86 | created_at: { 87 | description: 'Date when the user was created', 88 | required: false, 89 | ...datetimeStringSchema, 90 | }, 91 | }, 92 | identity: { 93 | name: 'User', 94 | }, 95 | idProperty: 'id', 96 | displayProperty: 'login', 97 | descriptionProperty: 'description', 98 | imageProperty: 'profile_image_url', 99 | // title, subtitle 100 | // featuredProperties: ['symbol', 'name', 'url', 'image'], 101 | attribution, 102 | includeUnknownProperties: false, 103 | }) 104 | 105 | export const ChannelSchema = coda.makeObjectSchema({ 106 | properties: { 107 | id: { 108 | description: 'User’s Identifier', 109 | required: true, 110 | ...stringSchema, 111 | }, 112 | login: { 113 | description: 'User’s login name', 114 | required: true, 115 | ...stringSchema, 116 | }, 117 | name: { 118 | description: 'User’s display name', 119 | required: true, 120 | ...stringSchema, 121 | }, 122 | language: { 123 | description: 124 | 'Language of the channel. A language value is either the ISO 639-1 two-letter code for a supported stream language or “other”.', 125 | required: true, 126 | ...stringSchema, 127 | }, 128 | category_id: { 129 | description: 'Current category ID being streamed on the channel', 130 | required: true, 131 | ...stringSchema, 132 | }, 133 | category_name: { 134 | description: ' Name of the category being streamed on the channel', 135 | required: true, 136 | ...stringSchema, 137 | }, 138 | stream_title: { 139 | description: 'Title of the stream', 140 | required: true, 141 | ...stringSchema, 142 | }, 143 | stream_delay: { 144 | description: 'Stream delay in seconds', 145 | required: true, 146 | ...numberSchema, 147 | }, 148 | }, 149 | identity: { 150 | name: 'Channel', 151 | }, 152 | idProperty: 'id', 153 | displayProperty: 'login', 154 | // title, subtitle 155 | // featuredProperties: ['symbol', 'name', 'url', 'image'], 156 | attribution, 157 | includeUnknownProperties: false, 158 | }) 159 | 160 | // ==================================== 161 | // TAGS 162 | 163 | export const TagSchema = coda.makeObjectSchema({ 164 | properties: { 165 | id: { 166 | description: 'Identifier', 167 | required: true, 168 | ...stringSchema, 169 | }, 170 | name: { 171 | description: 'Name', 172 | required: true, 173 | ...stringSchema, 174 | }, 175 | description: { 176 | description: 'Description', 177 | required: true, 178 | ...stringSchema, 179 | }, 180 | is_auto: { 181 | description: 'Automatic Tag?', 182 | required: true, 183 | ...booleanSchema, 184 | }, 185 | }, 186 | identity: { 187 | name: 'Tag', 188 | }, 189 | idProperty: 'id', 190 | displayProperty: 'name', 191 | descriptionProperty: 'description', 192 | // title, subtitle 193 | // featuredProperties: ['symbol', 'name', 'url', 'image'], 194 | attribution, 195 | includeUnknownProperties: false, 196 | }) 197 | 198 | // ==================================== 199 | // CATEGORIES 200 | 201 | export const CategorySchema = coda.makeObjectSchema({ 202 | properties: { 203 | id: { 204 | description: 'Identifier', 205 | required: true, 206 | ...stringSchema, 207 | }, 208 | name: { 209 | description: 'Name', 210 | required: true, 211 | ...stringSchema, 212 | }, 213 | image: { 214 | description: 'Image', 215 | required: true, 216 | ...imageSchema, 217 | }, 218 | }, 219 | identity: { 220 | name: 'Category', 221 | }, 222 | idProperty: 'id', 223 | displayProperty: 'name', 224 | imageProperty: 'image', 225 | // descriptionProperty: 'url', 226 | // title, subtitle 227 | // featuredProperties: ['symbol', 'name', 'url', 'image'], 228 | attribution, 229 | includeUnknownProperties: false, 230 | }) 231 | -------------------------------------------------------------------------------- /packs/coingecko/pack.ts: -------------------------------------------------------------------------------- 1 | import * as coda from '@codahq/packs-sdk' 2 | 3 | import { 4 | getCategories, 5 | getCoinDetails, 6 | getCoinMarket, 7 | getCoins, 8 | getCurrencies, 9 | getDefiMarket, 10 | getExchangeRates, 11 | getGlobalMarket, 12 | searchCoins, 13 | trendingCoins, 14 | } from './api' 15 | 16 | import { 17 | CategorySchema, 18 | CoinDetailsSchema, 19 | CoinMarketSchema, 20 | CoinSchema, 21 | CurrencySchema, 22 | DefiMarketSchema, 23 | ExchangeRateSchema, 24 | GlobalMarketSchema, 25 | } from './schemas' 26 | 27 | import { CoinInputParam, CoinSearchParam, WhenParam } from './params' 28 | 29 | // ==================================== 30 | // CODA PACK 31 | 32 | // https://www.coingecko.com/en/api/documentation 33 | export const pack = coda.newPack() 34 | pack.addNetworkDomain('api.coingecko.com') 35 | // pack.setUserAuthentication({ 36 | // type: coda.AuthenticationType.HeaderBearerToken, 37 | // instructionsUrl: 'https://www.coingecko.com/en/api/pricing', 38 | // defaultConnectionRequirement: coda.ConnectionRequirement.Optional, 39 | // }) 40 | 41 | // ==================================== 42 | // DEFI MARKET 43 | 44 | pack.addFormula({ 45 | name: 'GetDefiMarket', 46 | description: 'Fetch defi market data from CoinGecko.', 47 | resultType: coda.ValueType.Object, 48 | schema: DefiMarketSchema, 49 | parameters: [], 50 | execute: getDefiMarket, 51 | }) 52 | 53 | // ==================================== 54 | // GLOBAL MARKET 55 | 56 | pack.addFormula({ 57 | name: 'GetGlobalMarket', 58 | description: 'Fetch global market data from CoinGecko.', 59 | resultType: coda.ValueType.Object, 60 | schema: GlobalMarketSchema, 61 | parameters: [], 62 | execute: getGlobalMarket, 63 | }) 64 | 65 | // ==================================== 66 | // CATEGORIES 67 | 68 | pack.addFormula({ 69 | name: 'GetCategories', 70 | description: 'Fetch categories from CoinGecko.', 71 | resultType: coda.ValueType.Array, 72 | items: CategorySchema, 73 | parameters: [], 74 | execute: getCategories, 75 | }) 76 | 77 | // ==================================== 78 | // EXCHANGE RATES 79 | 80 | pack.addFormula({ 81 | name: 'GetBitcoinExchangeRates', 82 | description: 'Fetch exchange rates for Bitcoin from CoinGecko.', 83 | resultType: coda.ValueType.Array, 84 | items: ExchangeRateSchema, 85 | parameters: [], 86 | execute: getExchangeRates, 87 | }) 88 | 89 | // ==================================== 90 | // CURRENCIES 91 | 92 | pack.addFormula({ 93 | name: 'GetCurrencies', 94 | description: 'Fetch currencies from CoinGecko.', 95 | resultType: coda.ValueType.Array, 96 | items: CurrencySchema, 97 | parameters: [], 98 | execute: getCurrencies, 99 | }) 100 | 101 | // Coin Currency 102 | // pack.addFormula({ 103 | // name: 'GetCoinCurrency', 104 | // description: 'Fetch historical currency price data for a coin identifier.', 105 | // resultType: coda.ValueType.Number, 106 | // schema: CoinCurrencySchema, 107 | // parameters: [CoinIdParam, CurrencyParam, WhenParam], 108 | // execute: getCoinCurrency, 109 | // }) 110 | 111 | // ==================================== 112 | // COIN MARKET 113 | 114 | pack.addFormula({ 115 | name: 'GetCoinMarket', 116 | description: 117 | 'Fetch market data for a coin identifier, including historical support.', 118 | resultType: coda.ValueType.Object, 119 | schema: CoinMarketSchema, 120 | parameters: [CoinInputParam, WhenParam], 121 | execute: getCoinMarket, 122 | }) 123 | 124 | // ==================================== 125 | // COIN DETAILS 126 | 127 | pack.addFormula({ 128 | name: 'GetCoinDetails', 129 | description: 'Fetch details for a coin identifier.', 130 | resultType: coda.ValueType.Object, 131 | schema: CoinDetailsSchema, 132 | parameters: [CoinInputParam], 133 | execute: getCoinDetails, 134 | }) 135 | 136 | // ==================================== 137 | // COINS 138 | 139 | pack.addFormula({ 140 | name: 'GetCoins', 141 | description: 'Fetch minimal information for all coins.', 142 | resultType: coda.ValueType.Array, 143 | items: CoinSchema, 144 | parameters: [], 145 | execute: getCoins, 146 | }) 147 | 148 | // ==================================== 149 | // TRENDING COINS 150 | 151 | pack.addFormula({ 152 | name: 'TrendingCoins', 153 | description: 'Fetch information for coins that are trending.', 154 | resultType: coda.ValueType.Array, 155 | items: CoinSchema, 156 | parameters: [], 157 | execute: trendingCoins, 158 | }) 159 | 160 | // ==================================== 161 | // SEARCH COINS 162 | 163 | pack.addFormula({ 164 | name: 'SearchCoins', 165 | description: 'Fetch information for coins that match the search query.', 166 | resultType: coda.ValueType.Array, 167 | items: CoinSchema, 168 | parameters: [CoinSearchParam], 169 | execute: searchCoins, 170 | }) 171 | 172 | // ==================================== 173 | // CODA SYNC TABLES 174 | 175 | // Categories 176 | pack.addSyncTable({ 177 | name: 'Categories', 178 | schema: CategorySchema, 179 | identityName: 'Category', 180 | formula: { 181 | name: 'SyncCategories', 182 | description: 'Sync categories from CoinGecko.', 183 | parameters: [], 184 | execute: async function ([], context) { 185 | const categories = await getCategories([], context) 186 | return { result: categories } 187 | }, 188 | }, 189 | }) 190 | 191 | // Exchange Rates 192 | pack.addSyncTable({ 193 | name: 'BitcoinExchangeRates', 194 | schema: ExchangeRateSchema, 195 | identityName: 'BitcoinExchangeRate', 196 | formula: { 197 | name: 'SyncExchangeRates', 198 | description: 'Sync bitcoin exchange rates from CoinGecko.', 199 | parameters: [], 200 | execute: async function ([], context) { 201 | const rates = await getExchangeRates([], context) 202 | return { result: rates } 203 | }, 204 | }, 205 | }) 206 | 207 | // add the rest of the coingecko formulas/schemas 208 | // and then consider what to do about sync tables 209 | // where to have column formats with the extra details 210 | // or whether to just have one coin schema with all the details 211 | 212 | // Coins 213 | pack.addSyncTable({ 214 | name: 'Coins', 215 | schema: CoinSchema, 216 | identityName: 'Coin', 217 | formula: { 218 | name: 'SyncCoins', 219 | description: 'Sync coins from CoinGecko.', 220 | parameters: [CoinSearchParam], 221 | execute: async function ([search], context) { 222 | const coins = await searchCoins([search], context) 223 | return { result: coins } 224 | }, 225 | }, 226 | }) 227 | 228 | // : await trendingCoins([], context) 229 | 230 | // ==================================== 231 | // CODA COLUMN FORMATS 232 | 233 | pack.addColumnFormat({ 234 | name: 'CoinDetails', 235 | formulaName: 'GetCoinDetails', 236 | }) 237 | 238 | pack.addColumnFormat({ 239 | name: 'CoinMarket', 240 | formulaName: 'GetCoinMarket', 241 | }) 242 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Bevry's Coda Packs

4 | 5 | 6 | 7 | 8 | 9 | 10 | Status of the GitHub Workflow: bevry 11 |
12 | GitHub Sponsors donate button 13 | ThanksDev donate button 14 | Patreon donate button 15 | Flattr donate button 16 | Liberapay donate button 17 | Buy Me A Coffee donate button 18 | Open Collective donate button 19 | crypto donate button 20 | PayPal donate button 21 | Wishlist browse button 22 | 23 | 24 | 25 | 26 | # About 27 | 28 | This repository is the monorepo for the various packs by [Benjamin Lupton](https://balupton.com) / [Bevry](https://bevry.me), which can be found inside the `packs/` directory. 29 | 30 | [All of Benjamin’s Packs are livestreamed to twitch, open-source, and sponsorable.](https://coda.io/@balupton/packs) 31 | 32 | 33 | 34 |

Backers

35 | 36 |

Maintainers

37 | 38 | These amazing people are maintaining this project: 39 | 40 | 41 | 42 |

Sponsors

43 | 44 | No sponsors yet! Will you be the first? 45 | 46 | GitHub Sponsors donate button 47 | ThanksDev donate button 48 | Patreon donate button 49 | Flattr donate button 50 | Liberapay donate button 51 | Buy Me A Coffee donate button 52 | Open Collective donate button 53 | crypto donate button 54 | PayPal donate button 55 | Wishlist browse button 56 | 57 |

Contributors

58 | 59 | These amazing people have contributed code to this project: 60 | 61 | 62 | 63 | Discover how you can contribute by heading on over to the CONTRIBUTING.md file. 64 | 65 | 66 | 67 | 68 | 69 | 70 |

License

71 | 72 | Unless stated otherwise all works are: 73 | 74 | 75 | 76 | and licensed under: 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Bevry's Coda Packs", 3 | "name": "@bevry/coda-packs", 4 | "version": "0.1.0", 5 | "private": true, 6 | "description": "no description was provided", 7 | "homepage": "https://github.com/bevry/coda-packs", 8 | "license": "Artistic-2.0", 9 | "keywords": [ 10 | "", 11 | "es2017", 12 | "typed", 13 | "types", 14 | "typescript" 15 | ], 16 | "badges": { 17 | "list": [ 18 | "githubworkflow", 19 | "---", 20 | "githubsponsors", 21 | "thanksdev", 22 | "patreon", 23 | "flattr", 24 | "liberapay", 25 | "buymeacoffee", 26 | "opencollective", 27 | "crypto", 28 | "paypal", 29 | "wishlist" 30 | ], 31 | "config": { 32 | "githubWorkflow": "bevry", 33 | "githubSponsorsUsername": "balupton", 34 | "thanksdevGithubUsername": "balupton", 35 | "buymeacoffeeUsername": "balupton", 36 | "cryptoURL": "https://bevry.me/crypto", 37 | "flattrUsername": "balupton", 38 | "liberapayUsername": "bevry", 39 | "opencollectiveUsername": "bevry", 40 | "patreonUsername": "bevry", 41 | "paypalURL": "https://bevry.me/paypal", 42 | "wishlistURL": "https://bevry.me/wishlist", 43 | "githubUsername": "bevry", 44 | "githubRepository": "coda-packs", 45 | "githubSlug": "bevry/coda-packs", 46 | "npmPackageName": "@bevry/coda-packs" 47 | } 48 | }, 49 | "funding": "https://bevry.me/fund", 50 | "author": "2022+ Benjamin Lupton (https://github.com/balupton)", 51 | "maintainers": [ 52 | "Benjamin Lupton (https://github.com/balupton)" 53 | ], 54 | "contributors": [ 55 | "Benjamin Lupton (https://github.com/balupton)" 56 | ], 57 | "bugs": { 58 | "url": "https://github.com/bevry/coda-packs/issues" 59 | }, 60 | "repository": { 61 | "type": "git", 62 | "url": "https://github.com/bevry/coda-packs.git" 63 | }, 64 | "engines": { 65 | "node": ">=18" 66 | }, 67 | "editions": [ 68 | { 69 | "description": "TypeScript source code with Import for modules", 70 | "directory": "source", 71 | "entry": "index.ts", 72 | "tags": [ 73 | "source", 74 | "typescript", 75 | "import" 76 | ], 77 | "engines": false 78 | }, 79 | { 80 | "description": "TypeScript compiled against ES2017 for Node.js with Require for modules", 81 | "directory": "edition-es2017", 82 | "entry": "index.js", 83 | "tags": [ 84 | "compiled", 85 | "javascript", 86 | "es2017", 87 | "require" 88 | ], 89 | "engines": { 90 | "node": "18 || 20 || 21", 91 | "browsers": false 92 | } 93 | }, 94 | { 95 | "description": "TypeScript compiled against ES2017 for Node.js with Import for modules", 96 | "directory": "edition-es2017-esm", 97 | "entry": "index.js", 98 | "tags": [ 99 | "compiled", 100 | "javascript", 101 | "es2017", 102 | "import" 103 | ], 104 | "engines": { 105 | "node": "18 || 20 || 21", 106 | "browsers": false 107 | } 108 | } 109 | ], 110 | "types": "./compiled-types/", 111 | "type": "module", 112 | "main": "edition-es2017/index.js", 113 | "exports": { 114 | "node": { 115 | "import": "./edition-es2017-esm/index.js", 116 | "require": "./edition-es2017/index.js" 117 | } 118 | }, 119 | "dependencies": { 120 | "@codahq/packs-sdk": "^1.7.0", 121 | "twitter-api-sdk": "^1.2.1" 122 | }, 123 | "devDependencies": { 124 | "@types/node": "^20.8.10", 125 | "@typescript-eslint/eslint-plugin": "^6.9.1", 126 | "@typescript-eslint/parser": "^6.9.1", 127 | "assert-helpers": "^8.4.0", 128 | "eslint": "^8.52.0", 129 | "eslint-config-bevry": "^3.27.0", 130 | "eslint-config-prettier": "^9.0.0", 131 | "eslint-plugin-prettier": "^5.0.1", 132 | "kava": "^5.15.0", 133 | "prettier": "^3.0.3", 134 | "projectz": "^2.23.0", 135 | "surge": "^0.23.1", 136 | "typedoc": "^0.25.3", 137 | "typescript": "5.2.2" 138 | }, 139 | "scripts": { 140 | "our:clean": "rm -Rf ./docs ./edition* ./es2015 ./es5 ./out ./.next", 141 | "our:compile": "npm run our:compile:edition-es2017 && npm run our:compile:edition-es2017-esm && npm run our:compile:types", 142 | "our:compile:edition-es2017": "tsc --module commonjs --target ES2017 --outDir ./edition-es2017 --project tsconfig.json && ( test ! -d edition-es2017/source || ( mv edition-es2017/source edition-temp && rm -Rf edition-es2017 && mv edition-temp edition-es2017 ) ) && printf '%s' '{\"type\": \"commonjs\"}' > edition-es2017/package.json", 143 | "our:compile:edition-es2017-esm": "tsc --module ESNext --target ES2017 --outDir ./edition-es2017-esm --project tsconfig.json && ( test ! -d edition-es2017-esm/source || ( mv edition-es2017-esm/source edition-temp && rm -Rf edition-es2017-esm && mv edition-temp edition-es2017-esm ) ) && printf '%s' '{\"type\": \"module\"}' > edition-es2017-esm/package.json", 144 | "our:compile:types": "tsc --project tsconfig.json --emitDeclarationOnly --declaration --declarationMap --declarationDir ./compiled-types && ( test ! -d compiled-types/source || ( mv compiled-types/source edition-temp && rm -Rf compiled-types && mv edition-temp compiled-types ) )", 145 | "our:deploy": "printf '%s\n' 'no need for this project'", 146 | "our:meta": "npm run our:meta:docs && npm run our:meta:projectz", 147 | "our:meta:docs": "npm run our:meta:docs:typedoc", 148 | "our:meta:docs:typedoc": "rm -Rf ./docs && typedoc --exclude '**/+(*test*|node_modules)' --excludeExternals --out ./docs ./source", 149 | "our:meta:projectz": "projectz compile", 150 | "our:release": "npm run our:release:push", 151 | "our:release:prepare": "npm run our:clean && npm run our:compile && npm run our:test && npm run our:meta", 152 | "our:release:push": "git push origin && git push origin --tags", 153 | "our:setup": "npm run our:setup:install", 154 | "our:setup:install": "npm install", 155 | "our:test": "npm run our:verify && npm test", 156 | "our:verify": "npm run our:verify:eslint && npm run our:verify:prettier", 157 | "our:verify:eslint": "eslint --fix --ignore-pattern '**/*.d.ts' --ignore-pattern '**/vendor/' --ignore-pattern '**/node_modules/' --ext .mjs,.js,.jsx,.ts,.tsx ./source", 158 | "our:verify:prettier": "prettier --write .", 159 | "clean": "bash -ic 'rm -Rf packs/*/{.coda.json,.coda-credentials.json,.gitignore,tsconfig.json,package.json,package-lock.json,node_modules}'", 160 | "test": "node ./edition-es2017/test.js" 161 | }, 162 | "eslintConfig": { 163 | "extends": [ 164 | "bevry" 165 | ], 166 | "rules": { 167 | "require-atomic-updates": 0, 168 | "no-console": 0, 169 | "no-use-before-define": 1, 170 | "valid-jsdoc": 0 171 | } 172 | }, 173 | "prettier": { 174 | "semi": false, 175 | "singleQuote": true 176 | } 177 | } -------------------------------------------------------------------------------- /packs/twitch/types.ts: -------------------------------------------------------------------------------- 1 | interface Paginated { 2 | pagination: { 3 | /** A cursor value, to be used in a subsequent request to specify the starting point of the next set of results. */ 4 | cursor: string 5 | } 6 | } 7 | 8 | // ==================================== 9 | // CHANNEL 10 | 11 | export interface User { 12 | /** User’s ID. */ 13 | id: string 14 | /** User’s login name. */ 15 | login: string 16 | /** User’s display name. */ 17 | name: string 18 | /** User’s channel description. */ 19 | description?: string 20 | /** User’s verified email address. Returned if the request includes the user:read:email scope. */ 21 | email?: string 22 | /** User’s broadcaster type: "partner", "affiliate", or "". */ 23 | promotion?: string 24 | /** User’s type: "staff", "admin", "global_mod", or "". */ 25 | privilege?: string 26 | /** URL of the user’s offline image. */ 27 | offline_image_url?: string 28 | /** URL of the user’s profile image. */ 29 | profile_image_url?: string 30 | /** Date when the user was created. */ 31 | created_at?: string 32 | } 33 | 34 | export interface Channel { 35 | /** User’s ID. */ 36 | id: string 37 | /** User’s login name. */ 38 | login: string 39 | /** User’s display name. */ 40 | name: string 41 | /** Language of the channel. A language value is either the ISO 639-1 two-letter code for a supported stream language or “other”. */ 42 | language: string 43 | /** Current game ID being played on the channel. */ 44 | category_id: string 45 | /** Name of the game being played on the channel. */ 46 | category_name: string 47 | /** Title of the stream. */ 48 | stream_title: string 49 | /** Stream delay in seconds. */ 50 | stream_delay: number 51 | } 52 | 53 | export interface RawActiveUser { 54 | /** User’s ID. */ 55 | id: string 56 | /** User’s login name. */ 57 | login: string 58 | /** User’s display name. */ 59 | display_name: string 60 | /** User’s channel description. */ 61 | description: string 62 | /** User’s verified email address. Returned if the request includes the user:read:email scope. */ 63 | email: string 64 | /** User’s broadcaster type: "partner", "affiliate", or "". */ 65 | broadcaster_type: string 66 | /** User’s type: "staff", "admin", "global_mod", or "". */ 67 | type: string 68 | /** URL of the user’s offline image. */ 69 | offline_image_url: string 70 | /** URL of the user’s profile image. */ 71 | profile_image_url: string 72 | /** Date when the user was created. */ 73 | created_at: string 74 | } 75 | 76 | export interface ActiveUserResponse { 77 | data: Array 78 | } 79 | 80 | export interface RawUser { 81 | /** User’s ID. */ 82 | id: string 83 | /** User’s login name. */ 84 | login: string 85 | /** User’s display name. */ 86 | display_name: string 87 | /** User’s channel description. */ 88 | description: string 89 | /** User’s verified email address. Returned if the request includes the user:read:email scope. */ 90 | email: string 91 | /** User’s broadcaster type: "partner", "affiliate", or "". */ 92 | broadcaster_type: string 93 | /** User’s type: "staff", "admin", "global_mod", or "". */ 94 | type: string 95 | /** URL of the user’s offline image. */ 96 | offline_image_url: string 97 | /** URL of the user’s profile image. */ 98 | profile_image_url: string 99 | /** Date when the user was created. */ 100 | created_at: string 101 | /** Total number of views of the user’s channel. NOTE: This field has been deprecated. For information, see Get Users API endpoint – “view_count” deprecation. The response continues to include the field; however, it contains stale data. You should stop displaying this data at your earliest convenience. */ 102 | view_count: number 103 | } 104 | 105 | export interface UserResponse { 106 | data: Array 107 | } 108 | 109 | export interface RawChannel { 110 | /** Twitch User ID of this channel owner. */ 111 | broadcaster_id: string 112 | /** Broadcaster’s user login name. */ 113 | broadcaster_login: string 114 | /** Twitch user display name of this channel owner. */ 115 | broadcaster_name: string 116 | /** Name of the game being played on the channel. */ 117 | game_name: string 118 | /** Current game ID being played on the channel. */ 119 | game_id: string 120 | /** Language of the channel. A language value is either the ISO 639-1 two-letter code for a supported stream language or “other”. */ 121 | broadcaster_language: string 122 | /** Title of the stream. */ 123 | title: string 124 | /** Stream delay in seconds. */ 125 | delay: number 126 | } 127 | 128 | export interface ChannelResponse { 129 | data: Array 130 | } 131 | 132 | export interface UpdateChannelInformationRequest { 133 | /** The current game ID being played on the channel. Use “0” or “” (an empty string) to unset the game. */ 134 | game_id?: string 135 | 136 | /** The language of the channel. A language value must be either the ISO 639-1 two-letter code for a supported stream language or “other”. */ 137 | broadcaster_language?: string 138 | 139 | /** The title of the stream. Value must not be an empty string. */ 140 | title?: string 141 | 142 | /** Stream delay in seconds. Stream delay is a Twitch Partner feature; trying to set this value for other account types will return a 400 error. */ 143 | delay?: number 144 | } 145 | 146 | // ==================================== 147 | // TAGS 148 | 149 | export interface Tag { 150 | id: string 151 | name: string 152 | description: string 153 | is_auto: boolean 154 | } 155 | 156 | export interface RawTag { 157 | /** An ID that identifies the tag. */ 158 | tag_id: string 159 | /** A Boolean value that determines whether the tag is an automatic tag. An automatic tag is one that Twitch adds to the stream. You cannot add or remove automatic tags. The value is true if the tag is an automatic tag; otherwise, false. */ 160 | is_auto: boolean 161 | /** A dictionary that contains the localized names of the tag. The key is in the form, -. For example, us-en. The value is the localized name. */ 162 | localization_names: Localization 163 | /** A dictionary that contains the localized descriptions of the tag. The key is in the form, -. For example, us-en. The value is the localized description. */ 164 | localization_descriptions: Localization 165 | } 166 | 167 | export interface TagsResponse extends Paginated { 168 | data: Array 169 | } 170 | 171 | // ==================================== 172 | // CATEGORIES 173 | 174 | export interface Category { 175 | id: string 176 | name: string 177 | image: string 178 | } 179 | 180 | export interface RawCategory { 181 | box_art_url: string 182 | id: string 183 | name: string 184 | } 185 | 186 | export type GameRequest = 187 | | { 188 | /** Game ID. At most 100 id values can be specified. */ 189 | id: string 190 | } 191 | | { 192 | /** Game name. The name must be an exact match. For example, “Pokemon” will not return a list of Pokemon games; instead, query any specific Pokemon games in which you are interested. At most 100 name values can be specified. */ 193 | name: string 194 | } 195 | 196 | export interface GameResponse extends Paginated { 197 | data: Array 198 | } 199 | 200 | export interface TopGamesRequest { 201 | /** Cursor for forward pagination: tells the server where to start fetching the next set of results, in a multi-page response. The cursor value specified here is from the pagination response field of a prior query. */ 202 | after?: string 203 | /** Cursor for backward pagination: tells the server where to start fetching the next set of results, in a multi-page response. The cursor value specified here is from the pagination response field of a prior query. */ 204 | before?: string 205 | /** Maximum number of objects to return. Maximum: 100. Default: 20. */ 206 | first?: number 207 | } 208 | 209 | export interface TopGamesResponse extends Paginated { 210 | data: Array 211 | } 212 | 213 | interface Localization { 214 | [locale: string]: string 215 | 'en-us': string 216 | } 217 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

License

4 | 5 | Unless stated otherwise all works are: 6 | 7 | 8 | 9 | and licensed under: 10 | 11 | 12 | 13 |

The Artistic License 2.0

14 | 15 |
 16 | Copyright (c) 2000-2006, The Perl Foundation.
 17 | 
 18 | Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
 19 | 
 20 | Preamble
 21 | 
 22 | This license establishes the terms under which a given free software Package may be copied, modified, distributed, and/or redistributed. The intent is that the Copyright Holder maintains some artistic control over the development of that Package while still keeping the Package available as open source and free software.
 23 | 
 24 | You are always permitted to make arrangements wholly outside of this license directly with the Copyright Holder of a given Package.  If the terms of this license do not permit the full use that you propose to make of the Package, you should contact the Copyright Holder and seek a different licensing arrangement.
 25 | 
 26 | Definitions
 27 | 
 28 |      "Copyright Holder" means the individual(s) or organization(s) named in the copyright notice for the entire Package.
 29 | 
 30 |      "Contributor" means any party that has contributed code or other material to the Package, in accordance with the Copyright Holder's procedures.
 31 | 
 32 |      "You" and "your" means any person who would like to copy, distribute, or modify the Package.
 33 | 
 34 |      "Package" means the collection of files distributed by the Copyright Holder, and derivatives of that collection and/or of those files. A given Package may consist of either the Standard Version, or a Modified Version.
 35 | 
 36 |      "Distribute" means providing a copy of the Package or making it accessible to anyone else, or in the case of a company or organization, to others outside of your company or organization.
 37 | 
 38 |      "Distributor Fee" means any fee that you charge for Distributing this Package or providing support for this Package to another party.  It does not mean licensing fees.
 39 | 
 40 |      "Standard Version" refers to the Package if it has not been modified, or has been modified only in ways explicitly requested by the Copyright Holder.
 41 | 
 42 |      "Modified Version" means the Package, if it has been changed, and such changes were not explicitly requested by the Copyright Holder.
 43 | 
 44 |      "Original License" means this Artistic License as Distributed with the Standard Version of the Package, in its current version or as it may be modified by The Perl Foundation in the future.
 45 | 
 46 |      "Source" form means the source code, documentation source, and configuration files for the Package.
 47 | 
 48 |      "Compiled" form means the compiled bytecode, object code, binary, or any other form resulting from mechanical transformation or translation of the Source form.
 49 | 
 50 | Permission for Use and Modification Without Distribution
 51 | 
 52 | (1) You are permitted to use the Standard Version and create and use Modified Versions for any purpose without restriction, provided that you do not Distribute the Modified Version.
 53 | 
 54 | Permissions for Redistribution of the Standard Version
 55 | 
 56 | (2) You may Distribute verbatim copies of the Source form of the Standard Version of this Package in any medium without restriction, either gratis or for a Distributor Fee, provided that you duplicate all of the original copyright notices and associated disclaimers.  At your discretion, such verbatim copies may or may not include a Compiled form of the Package.
 57 | 
 58 | (3) You may apply any bug fixes, portability changes, and other modifications made available from the Copyright Holder.  The resulting Package will still be considered the Standard Version, and as such will be subject to the Original License.
 59 | 
 60 | Distribution of Modified Versions of the Package as Source
 61 | 
 62 | (4) You may Distribute your Modified Version as Source (either gratis or for a Distributor Fee, and with or without a Compiled form of the Modified Version) provided that you clearly document how it differs from the Standard Version, including, but not limited to, documenting any non-standard features, executables, or modules, and provided that you do at least ONE of the following:
 63 | 
 64 |      (a) make the Modified Version available to the Copyright Holder of the Standard Version, under the Original License, so that the Copyright Holder may include your modifications in the Standard Version.
 65 |      (b) ensure that installation of your Modified Version does not prevent the user installing or running the Standard Version. In addition, the Modified Version must bear a name that is different from the name of the Standard Version.
 66 |      (c) allow anyone who receives a copy of the Modified Version to make the Source form of the Modified Version available to others under
 67 | 
 68 |           (i) the Original License or
 69 |           (ii) a license that permits the licensee to freely copy, modify and redistribute the Modified Version using the same licensing terms that apply to the copy that the licensee received, and requires that the Source form of the Modified Version, and of any works derived from it, be made freely available in that license fees are prohibited but Distributor Fees are allowed.
 70 | 
 71 | Distribution of Compiled Forms of the Standard Version or Modified Versions without the Source
 72 | 
 73 | (5)  You may Distribute Compiled forms of the Standard Version without the Source, provided that you include complete instructions on how to get the Source of the Standard Version.  Such instructions must be valid at the time of your distribution.  If these instructions, at any time while you are carrying out such distribution, become invalid, you must provide new instructions on demand or cease further distribution. If you provide valid instructions or cease distribution within thirty days after you become aware that the instructions are invalid, then you do not forfeit any of your rights under this license.
 74 | 
 75 | (6)  You may Distribute a Modified Version in Compiled form without the Source, provided that you comply with Section 4 with respect to the Source of the Modified Version.
 76 | 
 77 | Aggregating or Linking the Package
 78 | 
 79 | (7)  You may aggregate the Package (either the Standard Version or Modified Version) with other packages and Distribute the resulting aggregation provided that you do not charge a licensing fee for the Package.  Distributor Fees are permitted, and licensing fees for other components in the aggregation are permitted. The terms of this license apply to the use and Distribution of the Standard or Modified Versions as included in the aggregation.
 80 | 
 81 | (8) You are permitted to link Modified and Standard Versions with other works, to embed the Package in a larger work of your own, or to build stand-alone binary or bytecode versions of applications that include the Package, and Distribute the result without restriction, provided the result does not expose a direct interface to the Package.
 82 | 
 83 | Items That are Not Considered Part of a Modified Version
 84 | 
 85 | (9) Works (including, but not limited to, modules and scripts) that merely extend or make use of the Package, do not, by themselves, cause the Package to be a Modified Version.  In addition, such works are not considered parts of the Package itself, and are not subject to the terms of this license.
 86 | 
 87 | General Provisions
 88 | 
 89 | (10)  Any use, modification, and distribution of the Standard or Modified Versions is governed by this Artistic License. By using, modifying or distributing the Package, you accept this license. Do not use, modify, or distribute the Package, if you do not accept this license.
 90 | 
 91 | (11)  If your Modified Version has been derived from a Modified Version made by someone other than you, you are nevertheless required to ensure that your Modified Version complies with the requirements of this license.
 92 | 
 93 | (12)  This license does not grant you the right to use any trademark, service mark, tradename, or logo of the Copyright Holder.
 94 | 
 95 | (13)  This license includes the non-exclusive, worldwide, free-of-charge patent license to make, have made, use, offer to sell, sell, import and otherwise transfer the Package with respect to any patent claims licensable by the Copyright Holder that are necessarily infringed by the Package. If you institute patent litigation (including a cross-claim or counterclaim) against any party alleging that the Package constitutes direct or contributory patent infringement, then this Artistic License to you shall terminate on the date that such litigation is filed.
 96 | 
 97 | (14)  Disclaimer of Warranty:
 98 | THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 99 | 
100 | 101 | 102 | -------------------------------------------------------------------------------- /packs/twitch/pack.ts: -------------------------------------------------------------------------------- 1 | import * as coda from '@codahq/packs-sdk' 2 | 3 | import { CategorySchema, ChannelSchema, TagSchema, UserSchema } from './schemas' 4 | import { 5 | CategoryIdentifierParam, 6 | CategoryNameParam, 7 | ChannelIdentifierParam, 8 | LanguageParam, 9 | SearchLiveOnlyParam, 10 | SearchQueryParam, 11 | StreamDelayParam, 12 | StreamTitleParam, 13 | TagIdentifierParam, 14 | UserIdentifierParam, 15 | } from './params' 16 | import { 17 | syncAvailableTags, 18 | getCategoriesFromIdentifiers, 19 | getCategoriesFromNames, 20 | getTagsFromIdentifiers, 21 | getTopCategories, 22 | updateChannel, 23 | getActiveUser, 24 | getChannelsFromIdentifiers, 25 | getUsersFromIdentifiers, 26 | getUsersFromLogins, 27 | replaceStreamTags, 28 | getTagsFromChannelIdentifier, 29 | searchChannels, 30 | searchCategories, 31 | getUserByIdentifier, 32 | getUserByLogin, 33 | getChannelByIdentifier, 34 | getTagByIdentifier, 35 | getCategoryByIdentifier, 36 | getCategoryByName, 37 | } from './api' 38 | 39 | // https://dev.twitch.tv/console/apps/create 40 | // https://dev.twitch.tv/docs/api/get-started 41 | // https://dev.twitch.tv/docs/authentication/refresh-tokens 42 | // https://dev.twitch.tv/docs/authentication/validate-tokens 43 | // https://dev.twitch.tv/docs/authentication/scopes 44 | 45 | // channel:manage:broadcast 46 | // https://dev.twitch.tv/docs/api/reference#modify-channel-information 47 | // https://dev.twitch.tv/docs/api/reference#create-stream-marker 48 | // https://dev.twitch.tv/docs/api/reference#replace-stream-tags 49 | 50 | // channel:manage:raids 51 | // https://dev.twitch.tv/docs/api/reference#start-a-raid 52 | 53 | // channel:manage:videos 54 | // https://dev.twitch.tv/docs/api/reference#delete-videos 55 | 56 | // clips:edit 57 | // https://dev.twitch.tv/docs/api/reference#create-clip 58 | // creating clips 59 | 60 | // user:edit 61 | // https://dev.twitch.tv/docs/api/reference#update-user 62 | // only user description, don't care 63 | 64 | // ============== 65 | // CODA PACK 66 | 67 | export const pack = coda.newPack() 68 | 69 | pack.addNetworkDomain('twitch.tv') 70 | 71 | // https://dev.twitch.tv/console/apps 72 | pack.setUserAuthentication({ 73 | type: coda.AuthenticationType.OAuth2, 74 | authorizationUrl: 'https://id.twitch.tv/oauth2/authorize', 75 | tokenUrl: 'https://id.twitch.tv/oauth2/token', 76 | scopes: [ 77 | 'channel:manage:broadcast', 78 | 'channel:manage:raids', 79 | 'channel:manage:videos', 80 | 'clips:edit', 81 | ], 82 | // useProofKeyForCodeExchange: true, 83 | getConnectionName: async function (context) { 84 | const user = await getActiveUser([], context) 85 | return user.login 86 | }, 87 | }) 88 | 89 | // ==================================== 90 | // USER 91 | 92 | // @todo https://dev.twitch.tv/docs/api/reference#get-users-follows 93 | 94 | pack.addFormula({ 95 | name: 'GetActiveUser', 96 | description: 'Get the active User information', 97 | parameters: [], 98 | resultType: coda.ValueType.Object, 99 | schema: UserSchema, 100 | execute: getActiveUser, 101 | }) 102 | 103 | pack.addFormula({ 104 | name: 'GetUsersFromIdentifiers', 105 | description: 'Fetch the User that exact match a series of Identifiers', 106 | resultType: coda.ValueType.Array, 107 | items: UserSchema, 108 | parameters: [], 109 | varargParameters: [UserIdentifierParam], 110 | execute: getUsersFromIdentifiers, 111 | }) 112 | 113 | pack.addFormula({ 114 | name: 'GetUserByIdentifier', 115 | description: 'Fetch the User that exact matches this Identifier', 116 | resultType: coda.ValueType.Object, 117 | schema: UserSchema, 118 | parameters: [UserIdentifierParam], 119 | execute: getUserByIdentifier, 120 | }) 121 | 122 | pack.addFormula({ 123 | name: 'GetUsersFromLogins', 124 | description: 'Fetch the Users that exact match a series of Logins', 125 | resultType: coda.ValueType.Array, 126 | items: UserSchema, 127 | parameters: [], 128 | varargParameters: [UserIdentifierParam], 129 | execute: getUsersFromLogins, 130 | }) 131 | 132 | pack.addFormula({ 133 | name: 'GetUserByLogin', 134 | description: 'Fetch the User that exact matches this Login', 135 | resultType: coda.ValueType.Object, 136 | schema: UserSchema, 137 | parameters: [UserIdentifierParam], 138 | execute: getUserByLogin, 139 | }) 140 | 141 | // ==================================== 142 | // CHANNEL 143 | 144 | pack.addFormula({ 145 | name: 'SearchChannels', 146 | description: 'Fetch the Channels that match the search query', 147 | resultType: coda.ValueType.Array, 148 | items: ChannelSchema, 149 | parameters: [SearchQueryParam, SearchLiveOnlyParam], 150 | execute: searchChannels, 151 | }) 152 | 153 | pack.addFormula({ 154 | name: 'GetChannelsFromIdentifiers', 155 | description: 'Fetch the Channels that exact match a series of Identifiers', 156 | resultType: coda.ValueType.Array, 157 | items: ChannelSchema, 158 | parameters: [], 159 | varargParameters: [ChannelIdentifierParam], 160 | execute: getChannelsFromIdentifiers, 161 | }) 162 | 163 | pack.addFormula({ 164 | name: 'GetChannelByIdentifier', 165 | description: 'Fetch the Channel that exact matches this Identifier', 166 | resultType: coda.ValueType.Object, 167 | schema: ChannelSchema, 168 | parameters: [ChannelIdentifierParam], 169 | execute: getChannelByIdentifier, 170 | }) 171 | 172 | pack.addFormula({ 173 | name: 'UpdateChannel', 174 | description: 'Update Channel information', 175 | parameters: [ 176 | ChannelIdentifierParam, 177 | CategoryIdentifierParam, 178 | LanguageParam, 179 | StreamTitleParam, 180 | StreamDelayParam, 181 | ], 182 | resultType: coda.ValueType.String, 183 | isAction: true, 184 | extraOAuthScopes: ['channel:manage:broadcast'], 185 | execute: updateChannel, 186 | }) 187 | 188 | // ==================================== 189 | // TAGS 190 | 191 | // @todo https://dev.twitch.tv/docs/api/reference#replace-stream-tags 192 | 193 | // pack.addFormula({ 194 | // name: 'GetAvailableTags', 195 | // description: 'Fetch all the available tags', 196 | // resultType: coda.ValueType.Array, 197 | // items: TagSchema, 198 | // parameters: [], 199 | // execute: getAvailableTags, 200 | // }) 201 | 202 | pack.addSyncTable({ 203 | name: 'Tags', 204 | schema: TagSchema, 205 | identityName: 'Tag', 206 | formula: { 207 | name: 'SyncAvailableTags', 208 | description: 'Sync available Tags', 209 | parameters: [], 210 | execute: async function ([], context) { 211 | return await syncAvailableTags([], context) 212 | }, 213 | }, 214 | }) 215 | 216 | pack.addFormula({ 217 | name: 'getTagsFromChannelIdentifier', 218 | description: 'Fetch the Tags currently applied to the Channel', 219 | resultType: coda.ValueType.Array, 220 | items: TagSchema, 221 | parameters: [ChannelIdentifierParam], 222 | execute: getTagsFromChannelIdentifier, 223 | }) 224 | 225 | pack.addFormula({ 226 | name: 'GetTagsFromIdentifiers', 227 | description: 'Fetch the Tags that exact match a series of Identifiers', 228 | resultType: coda.ValueType.Array, 229 | items: TagSchema, 230 | parameters: [], 231 | varargParameters: [TagIdentifierParam], 232 | execute: getTagsFromIdentifiers, 233 | }) 234 | 235 | pack.addFormula({ 236 | name: 'GetTagByIdentifier', 237 | description: 'Fetch the Tag that exact matches this Identifier', 238 | resultType: coda.ValueType.Object, 239 | schema: TagSchema, 240 | parameters: [TagIdentifierParam], 241 | execute: getTagByIdentifier, 242 | }) 243 | 244 | pack.addFormula({ 245 | name: 'ReplaceStreamTags', 246 | description: 'Replace Stream Tags', 247 | parameters: [ChannelIdentifierParam], 248 | varargParameters: [TagIdentifierParam], 249 | resultType: coda.ValueType.String, 250 | isAction: true, 251 | extraOAuthScopes: ['channel:manage:broadcast'], 252 | execute: replaceStreamTags, 253 | }) 254 | 255 | // ==================================== 256 | // CATEGORIES 257 | 258 | pack.addFormula({ 259 | name: 'GetTopCategories', 260 | description: 'Fetch the top Categories', 261 | resultType: coda.ValueType.Array, 262 | items: CategorySchema, 263 | parameters: [], 264 | execute: getTopCategories, 265 | }) 266 | 267 | pack.addFormula({ 268 | name: 'SearchCategories', 269 | description: 'Fetch the Categories that match a Search Query', 270 | resultType: coda.ValueType.Array, 271 | items: CategorySchema, 272 | parameters: [SearchQueryParam], 273 | execute: searchCategories, 274 | }) 275 | 276 | pack.addFormula({ 277 | name: 'GetCategoriesFromIdentifiers', 278 | description: 'Fetch the Categories that exact match a series of Identifiers', 279 | resultType: coda.ValueType.Array, 280 | items: CategorySchema, 281 | parameters: [], 282 | varargParameters: [CategoryIdentifierParam], 283 | execute: getCategoriesFromIdentifiers, 284 | }) 285 | 286 | pack.addFormula({ 287 | name: 'GetCategoryByIdentifier', 288 | description: 'Fetch the Category that exact matches this Identifier', 289 | resultType: coda.ValueType.Object, 290 | schema: CategorySchema, 291 | parameters: [CategoryIdentifierParam], 292 | execute: getCategoryByIdentifier, 293 | }) 294 | 295 | pack.addFormula({ 296 | name: 'GetCategoriesFromNames', 297 | description: 'Fetch the Categories that exact match a series of Names', 298 | resultType: coda.ValueType.Array, 299 | items: CategorySchema, 300 | parameters: [], 301 | varargParameters: [CategoryNameParam], 302 | execute: getCategoriesFromNames, 303 | }) 304 | 305 | pack.addFormula({ 306 | name: 'GetCategoryByName', 307 | description: 'Fetch the Category that exact matches this Name', 308 | resultType: coda.ValueType.Object, 309 | schema: CategorySchema, 310 | parameters: [CategoryNameParam], 311 | execute: getCategoryByName, 312 | }) 313 | 314 | // ==================================== 315 | // STREAMS 316 | 317 | // @todo https://dev.twitch.tv/docs/api/reference#get-streams 318 | 319 | // @todo https://dev.twitch.tv/docs/api/reference#get-followed-streams 320 | 321 | // ==================================== 322 | // STREAM MARKERS 323 | 324 | // @todo https://dev.twitch.tv/docs/api/reference#create-stream-marker 325 | 326 | // @todo https://dev.twitch.tv/docs/api/reference#get-stream-markers 327 | 328 | // ==================================== 329 | // VIDEOS 330 | 331 | // @todo https://dev.twitch.tv/docs/api/reference#get-videos 332 | 333 | // @skip https://dev.twitch.tv/docs/api/reference#delete-videos 334 | 335 | // ==================================== 336 | // CLIPS 337 | 338 | // @todo https://dev.twitch.tv/docs/api/reference#create-clip 339 | 340 | // @todo https://dev.twitch.tv/docs/api/reference#get-clips 341 | 342 | // ==================================== 343 | // ANNOUNCEMENTS 344 | 345 | // @todo https://dev.twitch.tv/docs/api/reference#send-chat-announcement 346 | 347 | // ==================================== 348 | // CODA COLUMN FORMATS 349 | 350 | pack.addColumnFormat({ 351 | name: 'UserByIdentifier', 352 | formulaName: 'GetUserByIdentifier', 353 | }) 354 | 355 | pack.addColumnFormat({ 356 | name: 'UserByLogin', 357 | formulaName: 'GetUserByLogin', 358 | }) 359 | 360 | pack.addColumnFormat({ 361 | name: 'ChannelByIdentifier', 362 | formulaName: 'GetChannelByIdentifier', 363 | }) 364 | 365 | pack.addColumnFormat({ 366 | name: 'TagByIdentifier', 367 | formulaName: 'GetTagByIdentifier', 368 | }) 369 | 370 | pack.addColumnFormat({ 371 | name: 'CategoryByIdentifier', 372 | formulaName: 'GetCategoryByIdentifier', 373 | }) 374 | 375 | pack.addColumnFormat({ 376 | name: 'CategoryByName', 377 | formulaName: 'GetCategoryByName', 378 | }) 379 | -------------------------------------------------------------------------------- /packs/coingecko/types.ts: -------------------------------------------------------------------------------- 1 | export type Currencies = Array 2 | 3 | export interface ErrorResponse { 4 | status?: { 5 | error_code?: number 6 | error_message?: string 7 | } 8 | } 9 | 10 | // type Missing = undefined | null 11 | 12 | interface ExchangeRateResponse { 13 | name: string 14 | unit: string 15 | value: number 16 | type: string // 'crypto' | 'fiat' 17 | } 18 | 19 | export interface ExchangeRate extends ExchangeRateResponse { 20 | id: string 21 | } 22 | 23 | export interface ExchangeRatesResponse { 24 | rates: Record 25 | } 26 | 27 | export interface Coin { 28 | id: string 29 | symbol: string 30 | name: string 31 | url: string 32 | image?: string 33 | } 34 | 35 | export interface CoinMarket 36 | extends Partial, 37 | Partial { 38 | id: string 39 | json: string 40 | when: string 41 | coin: Coin 42 | 43 | // historical and detailed 44 | price?: CurrencyComparisons 45 | volume?: CurrencyComparisons 46 | market_cap?: CurrencyComparisons 47 | 48 | // public interest 49 | alexa_rank?: number 50 | bing_matches?: number 51 | 52 | // manual 53 | price_usd?: number 54 | price_btc?: number 55 | volume_usd?: number 56 | volume_btc?: number 57 | market_cap_usd?: number 58 | market_cap_btc?: number 59 | } 60 | export interface CoinDetails extends Partial, Partial { 61 | id: string 62 | json: string 63 | when: string // last_updated 64 | coin: Coin 65 | market: CoinMarket 66 | 67 | // detailed response 68 | categories: Array 69 | description?: string // Description 70 | block_time_in_minutes?: number 71 | coingecko_rank?: number 72 | coingecko_score?: number 73 | community_score?: number 74 | country_origin?: string 75 | developer_score?: number 76 | genesis_date?: string // iso 77 | hashing_algorithm?: string 78 | liquidity_score?: number 79 | market_cap_rank?: number 80 | public_interest_score?: number 81 | sentiment_votes_down_percentage?: number 82 | sentiment_votes_up_percentage?: number 83 | } 84 | 85 | export interface CoinListingResponse { 86 | id: string 87 | symbol: string 88 | name: string 89 | } 90 | export type CoinsListingResponse = Array 91 | 92 | interface CoinSearchResponse { 93 | id: string 94 | name: string 95 | symbol: string 96 | market_cap_rank?: number 97 | large?: string 98 | small?: string 99 | thumb?: string 100 | } 101 | 102 | interface CoinTrendingResponse extends CoinSearchResponse { 103 | slug: string 104 | price_btc: number 105 | score: number 106 | } 107 | 108 | export interface TrendingResponse { 109 | coins?: Array<{ 110 | item: CoinTrendingResponse 111 | }> 112 | exchanges?: [] 113 | } 114 | 115 | export interface SearchResponse { 116 | coins?: Array 117 | exchanges?: [] 118 | icos?: [] 119 | categories?: Array 120 | nfts?: Array 121 | } 122 | 123 | export interface CoinHistoryResponse { 124 | id: string 125 | name: string 126 | symbol: string 127 | image: Image 128 | // may not exist if the coin is not popular enough 129 | market_data?: MarketData 130 | community_data?: CommunityData 131 | developer_data?: DeveloperData 132 | public_interest_stats?: PublicInterestStats 133 | // different from detailed 134 | localization: Record 135 | } 136 | 137 | export interface CoinResponse { 138 | id: string 139 | name: string 140 | symbol: string 141 | image: Image 142 | public_interest_stats: PublicInterestStats 143 | // different from historical 144 | last_updated: string 145 | block_time_in_minutes: number 146 | categories?: Array 147 | coingecko_rank: number 148 | coingecko_score: number 149 | community_score: number 150 | country_origin: string 151 | description?: Description 152 | developer_score: number 153 | genesis_date: string 154 | hashing_algorithm: string 155 | links: LinksResponse 156 | liquidity_score: number 157 | market_cap_rank: number 158 | public_interest_score: number 159 | sentiment_votes_down_percentage: number 160 | sentiment_votes_up_percentage: number 161 | // required query params to enable 162 | market_data?: MarketDataDetailed 163 | community_data?: CommunityData 164 | developer_data?: DeveloperData 165 | // different from historical 166 | additional_notices?: [] 167 | asset_platform_id?: unknown 168 | platforms: Platforms 169 | public_notice?: unknown 170 | status_updates?: [] 171 | tickers?: Array 172 | } 173 | 174 | interface MarketData { 175 | current_price: CurrencyComparisons 176 | market_cap: CurrencyComparisons 177 | total_volume: CurrencyComparisons 178 | } 179 | 180 | interface MarketDataDetailed extends MarketData { 181 | total_value_locked?: unknown 182 | mcap_to_tvl_ratio?: unknown 183 | fdv_to_tvl_ratio?: unknown 184 | roi?: unknown 185 | ath: CurrencyComparisons 186 | ath_change_percentage: CurrencyComparisons 187 | ath_date: CurrencyDates 188 | atl: CurrencyComparisons 189 | atl_change_percentage: CurrencyComparisons 190 | atl_date: CurrencyDates 191 | market_cap_rank: number 192 | fully_diluted_valuation: CurrencyComparisons 193 | high_24h: CurrencyComparisons 194 | low_24h: CurrencyComparisons 195 | price_change_24h: number 196 | price_change_percentage_24h: number 197 | price_change_percentage_7d: number 198 | price_change_percentage_14d: number 199 | price_change_percentage_30d: number 200 | price_change_percentage_60d: number 201 | price_change_percentage_200d: number 202 | price_change_percentage_1y: number 203 | market_cap_change_24h: number 204 | market_cap_change_percentage_24h: number 205 | price_change_24h_in_currency: CurrencyComparisons 206 | price_change_percentage_1h_in_currency: CurrencyComparisons 207 | price_change_percentage_24h_in_currency: CurrencyComparisons 208 | price_change_percentage_7d_in_currency: CurrencyComparisons 209 | price_change_percentage_14d_in_currency: CurrencyComparisons 210 | price_change_percentage_30d_in_currency: CurrencyComparisons 211 | price_change_percentage_60d_in_currency: CurrencyComparisons 212 | price_change_percentage_200d_in_currency: CurrencyComparisons 213 | price_change_percentage_1y_in_currency: CurrencyComparisons 214 | market_cap_change_24h_in_currency: CurrencyComparisons 215 | market_cap_change_percentage_24h_in_currency: CurrencyComparisons 216 | total_supply: number 217 | max_supply: number 218 | circulating_supply: number 219 | sparkline_7d: Sparkline7d 220 | /** iso string */ 221 | last_updated: string 222 | } 223 | 224 | interface CategorySearchResponse { 225 | id: number 226 | name: string 227 | } 228 | 229 | interface NftSearchResponse { 230 | id?: string 231 | name: string 232 | symbol: string 233 | large?: string 234 | small?: string 235 | thumb?: string 236 | } 237 | 238 | interface Platforms { 239 | [key: string]: string 240 | } 241 | 242 | interface Description { 243 | [key: string]: string 244 | } 245 | 246 | interface Links { 247 | homepage?: Array 248 | blockchain_site?: Array 249 | official_forum_url?: Array 250 | chat_url?: Array 251 | announcement_url?: Array 252 | twitter_screen_name?: string 253 | facebook_username?: string 254 | bitcointalk_thread_identifier?: string 255 | telegram_channel_identifier?: string 256 | subreddit_url?: string 257 | } 258 | interface LinksResponse extends Links { 259 | repos_url: Repos 260 | } 261 | 262 | interface Repos { 263 | github?: Array 264 | bitbucket?: [] 265 | } 266 | 267 | interface Image { 268 | large?: string 269 | small?: string 270 | thumb?: string 271 | } 272 | 273 | interface CurrencyDates { 274 | // https://api.coingecko.com/api/v3/simple/supported_vs_currencies 275 | [currency: string]: string 276 | btc: string 277 | eth: string 278 | ltc: string 279 | bch: string 280 | bnb: string 281 | eos: string 282 | xrp: string 283 | xlm: string 284 | link: string 285 | dot: string 286 | yfi: string 287 | usd: string // USD 288 | aed: string 289 | ars: string 290 | aud: string 291 | bdt: string 292 | bhd: string 293 | bmd: string 294 | brl: string 295 | cad: string 296 | chf: string 297 | clp: string 298 | cny: string 299 | czk: string 300 | dkk: string 301 | eur: string 302 | gbp: string 303 | hkd: string 304 | huf: string 305 | idr: string 306 | ils: string 307 | inr: string 308 | jpy: string 309 | krw: string 310 | kwd: string 311 | lkr: string 312 | mmk: string 313 | mxn: string 314 | myr: string 315 | ngn: string 316 | nok: string 317 | nzd: string 318 | php: string 319 | pkr: string 320 | pln: string 321 | rub: string 322 | sar: string 323 | sek: string 324 | sgd: string 325 | thb: string 326 | try: string 327 | twd: string 328 | uah: string 329 | vef: string 330 | vnd: string 331 | zar: string 332 | xdr: string 333 | xag: string 334 | xau: string 335 | bits: string 336 | sats: string 337 | } 338 | 339 | interface CurrencyComparisons { 340 | // https://api.coingecko.com/api/v3/simple/supported_vs_currencies 341 | [currency: string]: number 342 | btc: number 343 | eth: number 344 | ltc: number 345 | bch: number 346 | bnb: number 347 | eos: number 348 | xrp: number 349 | xlm: number 350 | link: number 351 | dot: number 352 | yfi: number 353 | usd: number // USD 354 | aed: number 355 | ars: number 356 | aud: number 357 | bdt: number 358 | bhd: number 359 | bmd: number 360 | brl: number 361 | cad: number 362 | chf: number 363 | clp: number 364 | cny: number 365 | czk: number 366 | dkk: number 367 | eur: number 368 | gbp: number 369 | hkd: number 370 | huf: number 371 | idr: number 372 | ils: number 373 | inr: number 374 | jpy: number 375 | krw: number 376 | kwd: number 377 | lkr: number 378 | mmk: number 379 | mxn: number 380 | myr: number 381 | ngn: number 382 | nok: number 383 | nzd: number 384 | php: number 385 | pkr: number 386 | pln: number 387 | rub: number 388 | sar: number 389 | sek: number 390 | sgd: number 391 | thb: number 392 | try: number 393 | twd: number 394 | uah: number 395 | vef: number 396 | vnd: number 397 | zar: number 398 | xdr: number 399 | xag: number 400 | xau: number 401 | bits: number 402 | sats: number 403 | } 404 | 405 | interface Sparkline7d { 406 | price?: Array 407 | } 408 | 409 | interface CommunityData { 410 | // even doing ? here to mark it as optional, does not fix the type error, so seems it is nested object schemas are disregarding `required: false` 411 | facebook_likes?: number 412 | twitter_followers?: number 413 | reddit_average_posts_48h?: number 414 | reddit_average_comments_48h?: number 415 | reddit_subscribers?: number 416 | reddit_accounts_active_48h?: number 417 | telegram_channel_user_count?: number 418 | } 419 | 420 | interface DeveloperData { 421 | forks?: number 422 | stars?: number 423 | subscribers?: number 424 | total_issues?: number 425 | closed_issues?: number 426 | pull_requests_merged?: number 427 | pull_request_contributors?: number 428 | commit_count_4_weeks?: number 429 | last_4_weeks_commit_activity_series?: Array 430 | code_additions_deletions_4_weeks?: CodeAdditionsDeletions4Weeks 431 | } 432 | 433 | interface CodeAdditionsDeletions4Weeks { 434 | additions?: number 435 | deletions?: number 436 | } 437 | 438 | interface PublicInterestStats { 439 | alexa_rank?: number 440 | bing_matches?: number 441 | } 442 | 443 | interface TickersEntity { 444 | base: string 445 | target: string 446 | market: Market 447 | last: number 448 | volume: number 449 | converted_last: ConvertedLastOrConvertedVolume 450 | converted_volume: ConvertedLastOrConvertedVolume 451 | trust_score: string 452 | bid_ask_spread_percentage: number 453 | timestamp: string 454 | last_traded_at: string 455 | last_fetch_at: string 456 | is_anomaly: boolean 457 | is_stale: boolean 458 | trade_url?: string 459 | token_info_url: null 460 | coin_id: string 461 | target_coin_id?: string 462 | } 463 | 464 | interface Market { 465 | name: string 466 | identifier: string 467 | has_trading_incentive: boolean 468 | } 469 | 470 | interface ConvertedLastOrConvertedVolume { 471 | btc: number 472 | eth: number 473 | usd: number 474 | } 475 | 476 | export interface Category { 477 | id: string 478 | name: string 479 | description: string 480 | volume_24h_usd: number 481 | market_cap_usd: number 482 | market_cap_24h_percent: number 483 | updated_at: string 484 | } 485 | 486 | interface CategoryResponse { 487 | id: string 488 | name: string 489 | market_cap: number 490 | market_cap_change_24h: number 491 | volume_24h: number 492 | /** iso date string */ 493 | updated_at: string 494 | /** lengthy description */ 495 | content: string 496 | /** images of top three coins */ 497 | top_3_coins: Array 498 | } 499 | 500 | export type CategoriesResponse = Array 501 | 502 | export interface MarketRatios { 503 | btc: number 504 | eth: number 505 | usdt: number 506 | usdc: number 507 | bnb: number 508 | xrp: number 509 | busd: number 510 | ada: number 511 | sol: number 512 | dot: number 513 | } 514 | export interface GlobalMarket { 515 | active_cryptocurrencies: number 516 | upcoming_icos: number 517 | ongoing_icos: number 518 | ended_icos: number 519 | markets: number 520 | volume_usd: number 521 | market_cap_usd: number 522 | market_cap_24h_percent: number 523 | market_ratios: MarketRatios 524 | updated_at: number 525 | } 526 | 527 | export interface GlobalMarketResponse { 528 | data: { 529 | active_cryptocurrencies: number 530 | upcoming_icos: number 531 | ongoing_icos: number 532 | ended_icos: number 533 | markets: number 534 | market_cap_change_percentage_24h_usd: number 535 | updated_at: number 536 | /** market cap */ 537 | total_market_cap: CurrencyComparisons 538 | /** volume in currencies, not sure why it is measured this way, and what is the duration of the volume? */ 539 | total_volume: CurrencyComparisons 540 | /** percentage of dominance */ 541 | market_cap_percentage: MarketRatios 542 | } 543 | } 544 | 545 | export interface DefiMarket { 546 | volume_24h_usd: number 547 | market_cap_usd: number 548 | defi_dominance: number 549 | } 550 | 551 | export interface DefiMarketResponse { 552 | data: { 553 | defi_market_cap: string 554 | eth_market_cap: string 555 | defi_to_eth_ratio: string 556 | trading_volume_24h: string 557 | // percentage 558 | defi_dominance: string 559 | top_coin_name: string 560 | // percentage 561 | top_coin_defi_dominance: string 562 | } 563 | } 564 | -------------------------------------------------------------------------------- /packs/twitch/api.ts: -------------------------------------------------------------------------------- 1 | import * as coda from '@codahq/packs-sdk' 2 | import { FetchMethodType } from '@codahq/packs-sdk' 3 | import { clientId, height, hour, day, width } from './config' 4 | import { 5 | Category, 6 | GameResponse, 7 | RawCategory, 8 | RawTag, 9 | RawActiveUser, 10 | Tag, 11 | TagsResponse, 12 | TopGamesResponse, 13 | UpdateChannelInformationRequest, 14 | ActiveUserResponse, 15 | ChannelResponse, 16 | Channel, 17 | RawChannel, 18 | UserResponse, 19 | RawUser, 20 | User, 21 | } from './types' 22 | 23 | function parseImageUrl(url: string) { 24 | return url 25 | .replace('{width}', width.toString()) 26 | .replace('{height}', height.toString()) 27 | } 28 | 29 | function fetchFromTwitch( 30 | context: coda.SyncExecutionContext | coda.ExecutionContext, 31 | method: FetchMethodType = 'GET', 32 | url: string, 33 | cache: number = hour, 34 | ) { 35 | return context.fetcher.fetch({ 36 | method, 37 | url, 38 | cacheTtlSecs: cache, 39 | headers: { 40 | 'Content-Type': 'application/json', 41 | 'Client-ID': clientId, 42 | }, 43 | }) 44 | } 45 | 46 | function parseUser(raw: RawActiveUser | RawUser): User { 47 | const user: User = { 48 | id: raw.id, 49 | login: raw.login, 50 | name: raw.display_name, 51 | description: raw.description, 52 | email: raw.email, 53 | promotion: raw.broadcaster_type, 54 | privilege: raw.type, 55 | offline_image_url: raw.offline_image_url, 56 | profile_image_url: raw.profile_image_url, 57 | created_at: raw.created_at, 58 | } 59 | return user 60 | } 61 | 62 | function parseChannel(raw: RawChannel): Channel { 63 | const channel: Channel = { 64 | id: raw.broadcaster_id, 65 | login: raw.broadcaster_login, 66 | name: raw.broadcaster_name, 67 | category_id: raw.game_id, 68 | category_name: raw.game_name, 69 | language: raw.broadcaster_language, 70 | stream_title: raw.title, 71 | stream_delay: raw.delay, 72 | } 73 | return channel 74 | } 75 | 76 | // ==================================== 77 | // USER & CHANNEL 78 | 79 | export async function getActiveUser( 80 | []: [], 81 | context: coda.SyncExecutionContext | coda.ExecutionContext, 82 | ) { 83 | const url = 'https://api.twitch.tv/helix/users' 84 | const response = await fetchFromTwitch( 85 | context, 86 | 'GET', 87 | url, 88 | ) 89 | const raw: RawActiveUser = response.body.data[0] 90 | const user: User = parseUser(raw) 91 | return user 92 | } 93 | 94 | // https://dev.twitch.tv/docs/api/reference#get-users 95 | export async function getUsersFromIdentifiers( 96 | [...ids]: [...Array], 97 | context: coda.ExecutionContext, 98 | ) { 99 | // check 100 | if (ids.join('') == '') { 101 | throw new coda.UserVisibleError(`Need at least one valid identifier.`) 102 | } 103 | 104 | // fetch 105 | const url = coda.withQueryParams('https://api.twitch.tv/helix/users', { 106 | id: ids, 107 | }) 108 | const response = await fetchFromTwitch(context, 'GET', url) 109 | 110 | // parse 111 | const result: Array = response.body.data 112 | const users: Array = result.map(parseUser) 113 | 114 | // return 115 | return users 116 | } 117 | 118 | export async function getUserByIdentifier( 119 | [id]: [string], 120 | context: coda.ExecutionContext, 121 | ) { 122 | const results = await getUsersFromIdentifiers([id], context) 123 | return results[0] 124 | } 125 | 126 | // https://dev.twitch.tv/docs/api/reference#get-users 127 | export async function getUsersFromLogins( 128 | [...logins]: [...Array], 129 | context: coda.ExecutionContext, 130 | ) { 131 | // check 132 | if (logins.join('') == '') { 133 | throw new coda.UserVisibleError(`Need at least one valid login.`) 134 | } 135 | 136 | // fetch 137 | const url = coda.withQueryParams('https://api.twitch.tv/helix/users', { 138 | login: logins, 139 | }) 140 | const response = await fetchFromTwitch(context, 'GET', url) 141 | 142 | // parse 143 | const result: Array = response.body.data 144 | const users: Array = result.map(parseUser) 145 | 146 | // return 147 | return users 148 | } 149 | 150 | export async function getUserByLogin( 151 | [login]: [string], 152 | context: coda.ExecutionContext, 153 | ) { 154 | const results = await getUsersFromLogins([login], context) 155 | return results[0] 156 | } 157 | 158 | // https://dev.twitch.tv/docs/api/reference#search-channels 159 | export async function searchChannels( 160 | [query, live_only = false]: [string, boolean], 161 | context: coda.ExecutionContext, 162 | ) { 163 | // check 164 | if (query == '') { 165 | throw new coda.UserVisibleError(`Need a search query.`) 166 | } 167 | 168 | // fetch 169 | const url = coda.withQueryParams( 170 | 'https://api.twitch.tv/helix/search/channels', 171 | { 172 | query, 173 | live_only, 174 | first: 100, 175 | }, 176 | ) 177 | const response = await fetchFromTwitch(context, 'GET', url) 178 | 179 | // parse 180 | const result: Array = response.body.data 181 | const channels: Array = result.map(parseChannel) 182 | 183 | // @todo, handle the extra returned properties 184 | // https://dev.twitch.tv/docs/api/reference#search-channels 185 | 186 | // return 187 | return channels 188 | } 189 | 190 | // https://dev.twitch.tv/docs/api/reference#get-channel-information 191 | export async function getChannelsFromIdentifiers( 192 | [...ids]: [...Array], 193 | context: coda.ExecutionContext, 194 | ) { 195 | // check 196 | if (ids.join('') == '') { 197 | throw new coda.UserVisibleError(`Need at least one valid identifier.`) 198 | } 199 | 200 | // fetch 201 | const url = coda.withQueryParams('https://api.twitch.tv/helix/channels', { 202 | broadcaster_id: ids, 203 | }) 204 | const response = await fetchFromTwitch(context, 'GET', url) 205 | 206 | // parse 207 | const result: Array = response.body.data 208 | const channels: Array = result.map(parseChannel) 209 | 210 | // return 211 | return channels 212 | } 213 | 214 | export async function getChannelByIdentifier( 215 | [id]: [string], 216 | context: coda.ExecutionContext, 217 | ) { 218 | const results = await getChannelsFromIdentifiers([id], context) 219 | return results[0] 220 | } 221 | 222 | // https://dev.twitch.tv/docs/api/reference#modify-channel-information 223 | export async function updateChannel( 224 | [channel_id, category_id, language, stream_title, stream_delay]: [ 225 | string, 226 | string, 227 | string, 228 | string, 229 | number, 230 | ], 231 | context: coda.SyncExecutionContext | coda.ExecutionContext, 232 | ) { 233 | // send only necessary params 234 | const body: UpdateChannelInformationRequest = {} 235 | if (language) body.broadcaster_language = language 236 | if (category_id) body.game_id = category_id 237 | if (stream_title) body.title = stream_title 238 | if (stream_delay) body.delay = stream_delay 239 | 240 | // check channel 241 | if (Object.keys(body).length !== 0) { 242 | // fetch 243 | const url = coda.withQueryParams('https://api.twitch.tv/helix/channels', { 244 | broadcaster_id: channel_id, 245 | }) 246 | const response = await context.fetcher.fetch({ 247 | method: 'PATCH', 248 | url, 249 | headers: { 250 | 'Content-Type': 'application/json', 251 | 'Client-ID': clientId, 252 | }, 253 | body: JSON.stringify(body), 254 | }) 255 | 256 | // verify 257 | if (response.status !== 204) { 258 | throw new coda.UserVisibleError( 259 | `Failed to update the stream information.`, 260 | ) 261 | } 262 | } else { 263 | throw new coda.UserVisibleError(`Need at least one valid parameter.`) 264 | } 265 | 266 | // return 267 | return 'ok' 268 | } 269 | 270 | // ==================================== 271 | // TAGS 272 | 273 | function parseTag(raw: RawTag): Tag { 274 | return { 275 | id: raw.tag_id, 276 | name: raw.localization_names['en-us'], 277 | description: raw.localization_descriptions['en-us'], 278 | is_auto: raw.is_auto, 279 | } 280 | } 281 | 282 | // https://dev.twitch.tv/docs/api/reference#get-all-stream-tags 283 | // https://www.twitch.tv/directory/all/tags 284 | export async function syncAvailableTags( 285 | []: [], 286 | context: coda.SyncExecutionContext, 287 | ) { 288 | // fetch 289 | const url = coda.withQueryParams('https://api.twitch.tv/helix/tags/streams', { 290 | first: 100, 291 | after: context.sync.continuation?.after || null, 292 | }) 293 | const response = await fetchFromTwitch(context, 'GET', url, day) 294 | 295 | // parse 296 | const result: Array = response.body.data 297 | const tags: Array = result.map(parseTag) 298 | 299 | // last page, return results 300 | if (tags.length === 0) { 301 | return { result: tags } 302 | } 303 | 304 | // result results and continue paging 305 | return { 306 | result: tags, 307 | continuation: { 308 | after: response.body.pagination.cursor, 309 | }, 310 | } 311 | } 312 | 313 | // https://dev.twitch.tv/docs/api/reference#get-stream-tags 314 | export async function getTagsFromChannelIdentifier( 315 | [channel_id]: [string], 316 | context: coda.ExecutionContext, 317 | ) { 318 | // fetch 319 | const url = coda.withQueryParams('https://api.twitch.tv/helix/streams/tags', { 320 | broadcaster_id: channel_id, 321 | }) 322 | const response = await fetchFromTwitch(context, 'GET', url) 323 | 324 | // parse 325 | const result: Array = response.body.data 326 | const tags = result.map(parseTag) 327 | 328 | // return 329 | return tags 330 | } 331 | 332 | export async function getTagsFromIdentifiers( 333 | [...ids]: [...Array], 334 | context: coda.ExecutionContext, 335 | ) { 336 | // check 337 | if (ids.join('') == '') { 338 | throw new coda.UserVisibleError(`Need at least one valid identifier.`) 339 | } 340 | 341 | // fetch 342 | const url = coda.withQueryParams('https://api.twitch.tv/helix/tags/streams', { 343 | first: 100, 344 | tag_id: ids, 345 | }) 346 | const response = await fetchFromTwitch(context, 'GET', url) 347 | 348 | // parse 349 | const result: Array = response.body.data 350 | const tags = result.map(parseTag) 351 | 352 | // return 353 | return tags 354 | } 355 | 356 | export async function getTagByIdentifier( 357 | [id]: [string], 358 | context: coda.ExecutionContext, 359 | ) { 360 | const results = await getTagsFromIdentifiers([id], context) 361 | return results[0] 362 | } 363 | 364 | // https://dev.twitch.tv/docs/api/reference#replace-stream-tags 365 | export async function replaceStreamTags( 366 | [channel_id, ...tag_ids]: [string, ...Array], 367 | context: coda.ExecutionContext, 368 | ) { 369 | // check 370 | if (channel_id == '' || tag_ids.join('') == '') { 371 | throw new coda.UserVisibleError(`Need at least one valid identifier.`) 372 | } 373 | if (tag_ids.length > 5) { 374 | throw new coda.UserVisibleError( 375 | `Too many tags were provided, it must be at max 5.`, 376 | ) 377 | } 378 | 379 | // fetch 380 | const url = coda.withQueryParams('https://api.twitch.tv/helix/streams/tags', { 381 | broadcaster_id: channel_id, 382 | }) 383 | const response = await context.fetcher.fetch({ 384 | method: 'PUT', 385 | url, 386 | headers: { 387 | 'Content-Type': 'application/json', 388 | 'Client-ID': clientId, 389 | }, 390 | body: JSON.stringify({ 391 | tag_ids: tag_ids, 392 | }), 393 | }) 394 | 395 | // verify 396 | if (response.status !== 204) { 397 | throw new coda.UserVisibleError(`Failed to update the stream information.`) 398 | } 399 | 400 | // return 401 | return 'ok' 402 | } 403 | 404 | // ==================================== 405 | // CATEGORIES 406 | 407 | function parseCategory(raw: RawCategory): Category { 408 | return { 409 | id: raw.id, 410 | name: raw.name, 411 | image: parseImageUrl(raw.box_art_url), 412 | } 413 | } 414 | 415 | // https://dev.twitch.tv/docs/api/reference#get-top-games 416 | export async function getTopCategories( 417 | []: [], 418 | context: coda.SyncExecutionContext | coda.ExecutionContext, 419 | ) { 420 | // fetch 421 | const url = `https://api.twitch.tv/helix/games/top` 422 | const response = await fetchFromTwitch(context, 'GET', url) 423 | 424 | // parse 425 | const categories: Array = response.body.data.map(parseCategory) 426 | 427 | // return 428 | return categories 429 | } 430 | 431 | // https://dev.twitch.tv/docs/api/reference#search-categories 432 | export async function searchCategories( 433 | [query]: [string], 434 | context: coda.SyncExecutionContext | coda.ExecutionContext, 435 | ) { 436 | // check 437 | if (query == '') { 438 | throw new coda.UserVisibleError(`Need a search query.`) 439 | } 440 | 441 | // fetch 442 | const url = coda.withQueryParams( 443 | 'https://api.twitch.tv/helix/search/categories', 444 | { 445 | query, 446 | }, 447 | ) 448 | const response = await fetchFromTwitch(context, 'GET', url) 449 | 450 | // parse 451 | const categories: Array = response.body.data.map(parseCategory) 452 | 453 | // return 454 | return categories 455 | } 456 | 457 | // https://dev.twitch.tv/docs/api/reference#get-games 458 | export async function getCategoriesFromIdentifiers( 459 | [...ids]: [...Array], 460 | context: coda.SyncExecutionContext | coda.ExecutionContext, 461 | ) { 462 | // check 463 | if (ids.join('') == '') { 464 | throw new coda.UserVisibleError(`Need at least one valid identifier.`) 465 | } 466 | 467 | // fetch 468 | const url = coda.withQueryParams('https://api.twitch.tv/helix/games', { 469 | id: ids, 470 | }) 471 | const response = await fetchFromTwitch(context, 'GET', url) 472 | 473 | // parse 474 | const categories: Array = response.body.data.map(parseCategory) 475 | 476 | // return 477 | return categories 478 | } 479 | 480 | export async function getCategoryByIdentifier( 481 | [id]: [string], 482 | context: coda.ExecutionContext, 483 | ) { 484 | const results = await getCategoriesFromIdentifiers([id], context) 485 | return results[0] 486 | } 487 | 488 | export async function getCategoriesFromNames( 489 | [...names]: [...Array], 490 | context: coda.SyncExecutionContext | coda.ExecutionContext, 491 | ) { 492 | // check 493 | if (names.join('') == '') { 494 | throw new coda.UserVisibleError(`Need at least one valid name.`) 495 | } 496 | 497 | // fetch 498 | const url = coda.withQueryParams('https://api.twitch.tv/helix/games', { 499 | name: names, 500 | }) 501 | const response = await fetchFromTwitch(context, 'GET', url) 502 | 503 | // parse 504 | const categories: Array = response.body.data.map((result) => ({ 505 | id: result.id, 506 | name: result.name, 507 | image: parseImageUrl(result.box_art_url), 508 | })) 509 | 510 | // return 511 | return categories 512 | } 513 | 514 | export async function getCategoryByName( 515 | [name]: [string], 516 | context: coda.ExecutionContext, 517 | ) { 518 | const results = await getCategoriesFromNames([name], context) 519 | return results[0] 520 | } 521 | -------------------------------------------------------------------------------- /packs/formatting/pack.ts: -------------------------------------------------------------------------------- 1 | import * as coda from '@codahq/packs-sdk' 2 | 3 | export const pack = coda.newPack() 4 | 5 | export const UnformattedTextParam = coda.makeParameter({ 6 | name: 'UnformattedText', 7 | description: 'The unformatted text that you would like to format', 8 | type: coda.ParameterType.String, 9 | optional: false, 10 | }) 11 | 12 | export const CodaBlockLanguageParam = coda.makeParameter({ 13 | name: 'CodeLanguage', 14 | description: 'The language that the code is using', 15 | type: coda.ParameterType.String, 16 | optional: true, 17 | }) 18 | 19 | function hydrateUnsupportedElements(html: string) { 20 | // ALL elements within an unsupported element are trimmed 21 | return html.replace(/<(sup|sub)>/g, '').replace(/<\/(sup|sub)>/g, '') 22 | } 23 | 24 | pack.addFormula({ 25 | name: 'Newline', 26 | description: 'Give us a newline', 27 | parameters: [], 28 | resultType: coda.ValueType.String, 29 | // codaType: coda.ValueHintType.Html, 30 | execute: async function ([]: [], context) { 31 | return '\n' 32 | }, 33 | }) 34 | 35 | // ==================================== 36 | // HTML 37 | 38 | pack.addFormula({ 39 | name: 'Html', 40 | description: 'Format the input text as if it is HTML', 41 | parameters: [UnformattedTextParam], 42 | resultType: coda.ValueType.String, 43 | codaType: coda.ValueHintType.Html, 44 | execute: async function ([input]: [string], context) { 45 | return hydrateUnsupportedElements(input) 46 | }, 47 | }) 48 | 49 | pack.addFormula({ 50 | name: 'HtmlHeading1', 51 | description: 'Make the text the primary header', 52 | parameters: [UnformattedTextParam], 53 | resultType: coda.ValueType.String, 54 | codaType: coda.ValueHintType.Html, 55 | execute: async function ([input]: [string], context) { 56 | return hydrateUnsupportedElements(`

${input}

`) 57 | }, 58 | }) 59 | 60 | pack.addFormula({ 61 | name: 'HtmlHeading2', 62 | description: 'Make the text the secondary header', 63 | parameters: [UnformattedTextParam], 64 | resultType: coda.ValueType.String, 65 | codaType: coda.ValueHintType.Html, 66 | execute: async function ([input]: [string], context) { 67 | return hydrateUnsupportedElements(`

${input}

`) 68 | }, 69 | }) 70 | 71 | pack.addFormula({ 72 | name: 'HtmlHeading3', 73 | description: 'Make the text the third level header', 74 | parameters: [UnformattedTextParam], 75 | resultType: coda.ValueType.String, 76 | codaType: coda.ValueHintType.Html, 77 | execute: async function ([input]: [string], context) { 78 | return hydrateUnsupportedElements(`

${input}

`) 79 | }, 80 | }) 81 | 82 | pack.addFormula({ 83 | name: 'HtmlHeading4', 84 | description: 'Make the text the fourth level header', 85 | parameters: [UnformattedTextParam], 86 | resultType: coda.ValueType.String, 87 | codaType: coda.ValueHintType.Html, 88 | execute: async function ([input]: [string], context) { 89 | return hydrateUnsupportedElements(`

${input}

`) 90 | }, 91 | }) 92 | 93 | pack.addFormula({ 94 | name: 'HtmlHeading5', 95 | description: 'Make the text the fifth level header', 96 | parameters: [UnformattedTextParam], 97 | resultType: coda.ValueType.String, 98 | codaType: coda.ValueHintType.Html, 99 | execute: async function ([input]: [string], context) { 100 | return hydrateUnsupportedElements(`
${input}
`) 101 | }, 102 | }) 103 | 104 | pack.addFormula({ 105 | name: 'HtmlHeading6', 106 | description: 'Make the text the sixth level header', 107 | parameters: [UnformattedTextParam], 108 | resultType: coda.ValueType.String, 109 | codaType: coda.ValueHintType.Html, 110 | execute: async function ([input]: [string], context) { 111 | return hydrateUnsupportedElements(`
${input}
`) 112 | }, 113 | }) 114 | 115 | pack.addFormula({ 116 | name: 'HtmlItalic', 117 | description: 'Make the text italic', 118 | parameters: [UnformattedTextParam], 119 | resultType: coda.ValueType.String, 120 | codaType: coda.ValueHintType.Html, 121 | execute: async function ([input]: [string], context) { 122 | return hydrateUnsupportedElements(`${input}`) 123 | }, 124 | }) 125 | 126 | pack.addFormula({ 127 | name: 'HtmlBold', 128 | description: 'Make the text bold', 129 | parameters: [UnformattedTextParam], 130 | resultType: coda.ValueType.String, 131 | codaType: coda.ValueHintType.Html, 132 | execute: async function ([input]: [string], context) { 133 | return hydrateUnsupportedElements(`${input}`) 134 | }, 135 | }) 136 | 137 | pack.addFormula({ 138 | name: 'HtmlUnderline', 139 | description: 'Make the text underlined', 140 | parameters: [UnformattedTextParam], 141 | resultType: coda.ValueType.String, 142 | codaType: coda.ValueHintType.Html, 143 | execute: async function ([input]: [string], context) { 144 | return hydrateUnsupportedElements(`${input}`) 145 | }, 146 | }) 147 | 148 | pack.addFormula({ 149 | name: 'HtmlStrikethrough', 150 | description: 151 | 'Make the text render with a line through it, as if it has been cancelled out', 152 | parameters: [UnformattedTextParam], 153 | resultType: coda.ValueType.String, 154 | codaType: coda.ValueHintType.Html, 155 | execute: async function ([input]: [string], context) { 156 | // strike is 157 | return hydrateUnsupportedElements(`${input}`) 158 | }, 159 | }) 160 | 161 | pack.addFormula({ 162 | name: 'HtmlDelete', 163 | description: 'Make the text indicate that it was deleted', 164 | parameters: [UnformattedTextParam], 165 | resultType: coda.ValueType.String, 166 | codaType: coda.ValueHintType.Html, 167 | execute: async function ([input]: [string], context) { 168 | // strike is 169 | return hydrateUnsupportedElements(`${input}`) 170 | }, 171 | }) 172 | 173 | pack.addFormula({ 174 | name: 'HtmlInsert', 175 | description: 'Make the text indicate that it was inserted', 176 | parameters: [UnformattedTextParam], 177 | resultType: coda.ValueType.String, 178 | codaType: coda.ValueHintType.Html, 179 | execute: async function ([input]: [string], context) { 180 | // strike is 181 | return hydrateUnsupportedElements(`${input}`) 182 | }, 183 | }) 184 | 185 | pack.addFormula({ 186 | name: 'HtmlCodeInline', 187 | description: 'Make the text render as inline code', 188 | parameters: [UnformattedTextParam], 189 | resultType: coda.ValueType.String, 190 | codaType: coda.ValueHintType.Html, 191 | execute: async function ([input]: [string], context) { 192 | return hydrateUnsupportedElements(`${input}`) 193 | }, 194 | }) 195 | 196 | pack.addFormula({ 197 | name: 'HtmlSupertext', 198 | description: 199 | 'Make the text render as supertext, which currently Coda does not support, so it will be replaced by emphasis', 200 | parameters: [UnformattedTextParam], 201 | resultType: coda.ValueType.String, 202 | codaType: coda.ValueHintType.Html, 203 | execute: async function ([input]: [string], context) { 204 | return hydrateUnsupportedElements(`${input}`) 205 | }, 206 | }) 207 | 208 | pack.addFormula({ 209 | name: 'HtmlSubtext', 210 | description: 211 | 'Make the text render as subtext, which currently Coda does not support, so it will be replaced by emphasis', 212 | parameters: [UnformattedTextParam], 213 | resultType: coda.ValueType.String, 214 | codaType: coda.ValueHintType.Html, 215 | execute: async function ([input]: [string], context) { 216 | return hydrateUnsupportedElements(`${input}`) 217 | }, 218 | }) 219 | 220 | pack.addFormula({ 221 | name: 'HtmlQuote', 222 | description: 'Make the text render as a quote', 223 | parameters: [UnformattedTextParam], 224 | resultType: coda.ValueType.String, 225 | codaType: coda.ValueHintType.Html, 226 | execute: async function ([input]: [string], context) { 227 | return hydrateUnsupportedElements(`
${input}
`) 228 | }, 229 | }) 230 | 231 | pack.addFormula({ 232 | name: 'HtmlPreformatted', 233 | description: 'Make the text render as a preformatted block', 234 | parameters: [UnformattedTextParam], 235 | resultType: coda.ValueType.String, 236 | codaType: coda.ValueHintType.Html, 237 | execute: async function ([input]: [string], context) { 238 | return hydrateUnsupportedElements(`
${input}
`) 239 | }, 240 | }) 241 | 242 | pack.addFormula({ 243 | name: 'HtmlElements', 244 | description: 245 | 'Attempt to render all the elements that Coda may or may not support', 246 | parameters: [], 247 | resultType: coda.ValueType.String, 248 | codaType: coda.ValueHintType.Html, 249 | execute: async function ([]: [], context) { 250 | // https://developer.mozilla.org/en-US/docs/Web/HTML/Element 251 | return [ 252 | 'a', 253 | 'abbr', 254 | 'acronym', 255 | 'address', 256 | 'applet', 257 | 'area', 258 | 'article', 259 | 'aside', 260 | 'audio', 261 | 'b', 262 | 'base', 263 | 'bdi', 264 | 'bdo', 265 | 'bgsound', 266 | 'big', 267 | 'blink', 268 | 'blockquote', 269 | 'body', 270 | 'br', 271 | 'button', 272 | 'canvas', 273 | 'caption', 274 | 'center', 275 | 'cite', 276 | 'code', 277 | 'col', 278 | 'colgroup', 279 | 'content', 280 | 'data', 281 | 'datalist', 282 | 'dd', 283 | 'del', 284 | 'details', 285 | 'dfn', 286 | 'dialog', 287 | 'dir', 288 | 'div', 289 | 'dl', 290 | 'dt', 291 | 'em', 292 | 'embed', 293 | 'fieldset', 294 | 'figcaption', 295 | 'figure', 296 | 'font', 297 | 'footer', 298 | 'form', 299 | 'frame', 300 | 'frameset', 301 | 'h1', 302 | 'h2', 303 | 'h3', 304 | 'h4', 305 | 'h5', 306 | 'h6', 307 | 'head', 308 | 'header', 309 | 'hgroup', 310 | 'hr', 311 | 'html', 312 | 'i', 313 | 'iframe', 314 | 'image', 315 | 'img', 316 | 'input', 317 | 'ins', 318 | 'kbd', 319 | 'keygen', 320 | 'label', 321 | 'legend', 322 | 'li', 323 | 'link', 324 | 'main', 325 | 'map', 326 | 'mark', 327 | 'marquee', 328 | 'menu', 329 | 'menuitem', 330 | 'meta', 331 | 'meter', 332 | 'nav', 333 | 'nobr', 334 | 'noembed', 335 | 'noframes', 336 | 'noscript', 337 | 'object', 338 | 'ol', 339 | 'optgroup', 340 | 'option', 341 | 'output', 342 | 'p', 343 | 'param', 344 | 'picture', 345 | 'plaintext', 346 | 'portal', 347 | 'pre', 348 | 'progress', 349 | 'q', 350 | 'rb', 351 | 'rp', 352 | 'rt', 353 | 'rtc', 354 | 'ruby', 355 | 's', 356 | 'samp', 357 | 'script', 358 | 'section', 359 | 'select', 360 | 'shadow', 361 | 'slot', 362 | 'small', 363 | 'source', 364 | 'spacer', 365 | 'span', 366 | 'strike', 367 | 'strong', 368 | 'style', 369 | 'sub', 370 | 'summary', 371 | 'sup', 372 | 'table', 373 | 'tbody', 374 | 'td', 375 | 'template', 376 | 'textarea', 377 | 'tfoot', 378 | 'th', 379 | 'thead', 380 | 'time', 381 | 'title', 382 | 'tr', 383 | 'track', 384 | 'tt', 385 | 'u', 386 | 'ul', 387 | 'var', 388 | 'video', 389 | 'wbr', 390 | 'xmp', 391 | ] 392 | .map((i: string) => `<${i}>${i}`) 393 | .join(' ') 394 | }, 395 | }) 396 | 397 | // ==================================== 398 | // Markdown 399 | 400 | pack.addFormula({ 401 | name: 'Markdown', 402 | description: 'Format the input text as if it is markdown', 403 | parameters: [UnformattedTextParam], 404 | resultType: coda.ValueType.String, 405 | codaType: coda.ValueHintType.Markdown, 406 | execute: async function ([input]: [string], context) { 407 | return input 408 | }, 409 | }) 410 | 411 | pack.addFormula({ 412 | name: 'MarkdownHeading1', 413 | description: 'Make the text the primary header', 414 | parameters: [UnformattedTextParam], 415 | resultType: coda.ValueType.String, 416 | codaType: coda.ValueHintType.Markdown, 417 | execute: async function ([input]: [string], context) { 418 | return `# ${input}` 419 | }, 420 | }) 421 | 422 | pack.addFormula({ 423 | name: 'MarkdownHeading2', 424 | description: 'Make the text the secondary header', 425 | parameters: [UnformattedTextParam], 426 | resultType: coda.ValueType.String, 427 | codaType: coda.ValueHintType.Markdown, 428 | execute: async function ([input]: [string], context) { 429 | return `## ${input}` 430 | }, 431 | }) 432 | 433 | pack.addFormula({ 434 | name: 'MarkdownHeading3', 435 | description: 'Make the text the third level header', 436 | parameters: [UnformattedTextParam], 437 | resultType: coda.ValueType.String, 438 | codaType: coda.ValueHintType.Markdown, 439 | execute: async function ([input]: [string], context) { 440 | return `### ${input}` 441 | }, 442 | }) 443 | 444 | pack.addFormula({ 445 | name: 'MarkdownHeading4', 446 | description: 'Make the text the fourth level header', 447 | parameters: [UnformattedTextParam], 448 | resultType: coda.ValueType.String, 449 | codaType: coda.ValueHintType.Markdown, 450 | execute: async function ([input]: [string], context) { 451 | return `#### ${input}` 452 | }, 453 | }) 454 | 455 | pack.addFormula({ 456 | name: 'MarkdownHeading5', 457 | description: 'Make the text the fifth level header', 458 | parameters: [UnformattedTextParam], 459 | resultType: coda.ValueType.String, 460 | codaType: coda.ValueHintType.Markdown, 461 | execute: async function ([input]: [string], context) { 462 | return `##### ${input}` 463 | }, 464 | }) 465 | 466 | pack.addFormula({ 467 | name: 'MarkdownHeading6', 468 | description: 'Make the text the sixth level header', 469 | parameters: [UnformattedTextParam], 470 | resultType: coda.ValueType.String, 471 | codaType: coda.ValueHintType.Markdown, 472 | execute: async function ([input]: [string], context) { 473 | return `###### ${input}` 474 | }, 475 | }) 476 | 477 | pack.addFormula({ 478 | name: 'MarkdownItalic', 479 | description: 'Make the text italic', 480 | parameters: [UnformattedTextParam], 481 | resultType: coda.ValueType.String, 482 | codaType: coda.ValueHintType.Markdown, 483 | execute: async function ([input]: [string], context) { 484 | return `*${input}*` 485 | }, 486 | }) 487 | 488 | pack.addFormula({ 489 | name: 'MarkdownBold', 490 | description: 'Make the text bold', 491 | parameters: [UnformattedTextParam], 492 | resultType: coda.ValueType.String, 493 | codaType: coda.ValueHintType.Markdown, 494 | execute: async function ([input]: [string], context) { 495 | return `**${input}**` 496 | }, 497 | }) 498 | 499 | pack.addFormula({ 500 | name: 'MarkdownUnderline', 501 | description: 'Make the text underlined', 502 | parameters: [UnformattedTextParam], 503 | resultType: coda.ValueType.String, 504 | codaType: coda.ValueHintType.Markdown, 505 | execute: async function ([input]: [string], context) { 506 | return `***${input}***` 507 | }, 508 | }) 509 | 510 | pack.addFormula({ 511 | name: 'MarkdownStrikethrough', 512 | description: 513 | 'Make the text render with a line through it, as if it has been cancelled out', 514 | parameters: [UnformattedTextParam], 515 | resultType: coda.ValueType.String, 516 | codaType: coda.ValueHintType.Markdown, 517 | execute: async function ([input]: [string], context) { 518 | return `~~${input}~~` 519 | }, 520 | }) 521 | 522 | pack.addFormula({ 523 | name: 'MarkdownCodeInline', 524 | description: 'Make the text render as inline code', 525 | parameters: [UnformattedTextParam], 526 | resultType: coda.ValueType.String, 527 | codaType: coda.ValueHintType.Markdown, 528 | execute: async function ([input]: [string], context) { 529 | return `\`${input}\`` 530 | }, 531 | }) 532 | 533 | pack.addFormula({ 534 | name: 'MarkdownCodeBlock', 535 | description: 'Make the text render as a code block', 536 | parameters: [UnformattedTextParam, CodaBlockLanguageParam], 537 | resultType: coda.ValueType.String, 538 | codaType: coda.ValueHintType.Markdown, 539 | execute: async function ([input, language]: [string, string?], context) { 540 | return '```' + ` ${language}\n${input}\n` + '```' 541 | }, 542 | }) 543 | 544 | pack.addFormula({ 545 | name: 'MarkdownQuote', 546 | description: 'Make each line of text render as the same quote', 547 | parameters: [UnformattedTextParam], 548 | resultType: coda.ValueType.String, 549 | codaType: coda.ValueHintType.Markdown, 550 | execute: async function ([input]: [string], context) { 551 | return '> ' + input.replace(/\n/g, '\n> ') 552 | }, 553 | }) 554 | 555 | pack.addFormula({ 556 | name: 'MarkdownUnorderedList', 557 | description: 'Make each line of text render as an unordered list item', 558 | parameters: [UnformattedTextParam], 559 | resultType: coda.ValueType.String, 560 | codaType: coda.ValueHintType.Markdown, 561 | execute: async function ([input]: [string], context) { 562 | return '* ' + input.replace(/\n/g, '\n* ') 563 | }, 564 | }) 565 | 566 | pack.addFormula({ 567 | name: 'MarkdownOrderedList', 568 | description: 'Make each line of text render as an ordered list item', 569 | parameters: [UnformattedTextParam], 570 | resultType: coda.ValueType.String, 571 | codaType: coda.ValueHintType.Markdown, 572 | execute: async function ([input]: [string], context) { 573 | return '1. ' + input.replace(/\n/g, '\n1. ') 574 | }, 575 | }) 576 | -------------------------------------------------------------------------------- /packs/coingecko/api.ts: -------------------------------------------------------------------------------- 1 | import * as coda from '@codahq/packs-sdk' 2 | 3 | import type { 4 | CategoriesResponse, 5 | Category, 6 | Coin, 7 | CoinDetails, 8 | CoinHistoryResponse, 9 | CoinMarket, 10 | CoinResponse, 11 | CoinsListingResponse, 12 | Currencies, 13 | DefiMarket, 14 | DefiMarketResponse, 15 | ErrorResponse, 16 | ExchangeRate, 17 | ExchangeRatesResponse, 18 | GlobalMarket, 19 | GlobalMarketResponse, 20 | SearchResponse, 21 | TrendingResponse, 22 | } from './types' 23 | 24 | export const minute = 60 25 | export const hour = minute * 60 26 | export const day = hour * 24 27 | export const longTime = day * 7 28 | 29 | // ==================================== 30 | // HELPERS 31 | 32 | async function fetch( 33 | request: coda.FetchRequest, 34 | context: coda.SyncExecutionContext | coda.ExecutionContext, 35 | ) { 36 | try { 37 | return await context.fetcher.fetch(request) 38 | } catch (rawError: any) { 39 | if (rawError.statusCode) { 40 | const error = rawError as coda.StatusCodeError 41 | const coinGeckoErrorCode = error.body?.status?.error_code 42 | const coinGeckoErrorMessage = error.body?.status?.error_message 43 | if (coinGeckoErrorCode === 429) { 44 | throw new coda.UserVisibleError( 45 | `Rate limits exceeded: https://coda.io/@balupton/coingecko/rate-limits-14`, 46 | ) 47 | } 48 | throw new coda.UserVisibleError( 49 | coinGeckoErrorMessage || 50 | `Required failed with error ${error.statusCode}`, 51 | ) 52 | } 53 | throw new coda.UserVisibleError('Request failed.') 54 | } 55 | } 56 | 57 | // ------------------------------------ 58 | // Dates 59 | 60 | function getCoinGeckoDate(input: null | string | number | Date = new Date()) { 61 | const when = !input 62 | ? new Date() 63 | : typeof input === 'string' || typeof input === 'number' 64 | ? new Date(input) 65 | : input 66 | when.setMilliseconds(0) 67 | when.setSeconds(0) 68 | when.setMinutes(0) 69 | return when 70 | } 71 | 72 | function getCoinGeckoDateString(when = new Date()) { 73 | return `${when.getDate()}-${when.getMonth() + 1}-${when.getFullYear()}` 74 | } 75 | 76 | function isToday(when: Date) { 77 | return getCoinGeckoDateString(when) === getCoinGeckoDateString() 78 | } 79 | 80 | function getYesterday(when = new Date()) { 81 | return getCoinGeckoDate(when.getTime() - 24 * 60 * 60 * 1000) 82 | } 83 | 84 | // ------------------------------------ 85 | // Identifiers 86 | 87 | function getCoinUrl(id: string) { 88 | return `https://coingecko.com/coins/${id}` 89 | } 90 | 91 | function getCoinWhenFromInput(identifier: string): Date { 92 | const match = identifier.match('^.+?@(.+)$') 93 | return getCoinGeckoDate(match && match[1]) 94 | } 95 | 96 | function getCoinIdFromInput(input: string): string { 97 | if (input.startsWith('http')) { 98 | const match = input.match( 99 | '^https?://.*?coingecko.com/.*?coins/(.+?)(@.+)?$', 100 | ) 101 | return ((match && match[1]) || input).toLowerCase() 102 | } else { 103 | const match = input.match('^(.+?)(@.+)?$') 104 | return ((match && match[1]) || input).toLocaleLowerCase() 105 | } 106 | } 107 | 108 | function parseInput(input: string) { 109 | if (input.startsWith('{')) { 110 | try { 111 | const data = JSON.parse(input) 112 | if (data?.codaSchemaIdentity === 'CoinDetails') { 113 | const coinDetails = data as CoinDetails 114 | const coinMarket: CoinMarket = coinDetails.market 115 | return { 116 | id: getCoinIdFromInput(coinMarket.id), 117 | when: coinMarket.when, 118 | coinDetails, 119 | coinMarket, 120 | } 121 | } else if (data?.codaSchemaIdentity === 'CoinMarket') { 122 | const coinMarket = data as CoinMarket 123 | return { 124 | id: getCoinIdFromInput(coinMarket.id), 125 | when: coinMarket.when, 126 | coinMarket, 127 | } 128 | } 129 | } catch (error) {} 130 | } else { 131 | return { 132 | id: getCoinIdFromInput(input), 133 | when: getCoinWhenFromInput(input), 134 | } 135 | } 136 | return {} 137 | } 138 | 139 | // ------------------------------------ 140 | // Parsing 141 | 142 | function parseCoin(rawCoin: { 143 | id: string 144 | symbol: string 145 | name: string 146 | image?: { 147 | large?: string 148 | small?: string 149 | thumb?: string 150 | } 151 | }): Coin { 152 | return { 153 | id: rawCoin.id, 154 | symbol: rawCoin.symbol, 155 | name: rawCoin.name, 156 | url: getCoinUrl(rawCoin.id), 157 | image: 158 | rawCoin.image?.large || 159 | rawCoin.image?.small || 160 | rawCoin.image?.thumb || 161 | '', 162 | } 163 | } 164 | 165 | function parseCoins( 166 | rawCoins: SearchResponse['coins'] | CoinsListingResponse, 167 | ): Coin[] { 168 | return (rawCoins || []).map(parseCoin) 169 | } 170 | 171 | function parseTrendingCoins(trendingCoins: TrendingResponse['coins']): Coin[] { 172 | return (trendingCoins || []).map((result) => parseCoin(result.item)) 173 | } 174 | 175 | function parseCoinMarket( 176 | result: CoinResponse | CoinHistoryResponse, 177 | when: Date, 178 | ): CoinMarket { 179 | // parse 180 | const whenString = when.toISOString() 181 | const coin = parseCoin(result) 182 | const coinMarket: CoinMarket = { 183 | // essential 184 | id: `${result.id}@${whenString}`, 185 | json: '', 186 | when: whenString, 187 | coin, 188 | 189 | // community data 190 | facebook_likes: result.community_data?.facebook_likes || undefined, 191 | twitter_followers: result.community_data?.twitter_followers || undefined, 192 | reddit_average_posts_48h: 193 | result.community_data?.reddit_average_posts_48h || undefined, 194 | reddit_average_comments_48h: 195 | result.community_data?.reddit_average_comments_48h || undefined, 196 | reddit_subscribers: result.community_data?.reddit_subscribers || undefined, 197 | reddit_accounts_active_48h: 198 | result.community_data?.reddit_accounts_active_48h || undefined, 199 | telegram_channel_user_count: 200 | result.community_data?.telegram_channel_user_count || undefined, 201 | 202 | // developer data 203 | forks: result.developer_data?.forks || undefined, 204 | stars: result.developer_data?.stars || undefined, 205 | subscribers: result.developer_data?.subscribers || undefined, 206 | total_issues: result.developer_data?.total_issues || undefined, 207 | closed_issues: result.developer_data?.closed_issues || undefined, 208 | pull_requests_merged: 209 | result.developer_data?.pull_requests_merged || undefined, 210 | pull_request_contributors: 211 | result.developer_data?.pull_request_contributors || undefined, 212 | commit_count_4_weeks: 213 | result.developer_data?.commit_count_4_weeks || undefined, 214 | // last_4_weeks_commit_additions: 215 | // result.developer_data?.code_additions_deletions_4_weeks?.additions || 216 | // null, 217 | // last_4_weeks_commit_deletions: 218 | // result.developer_data?.code_additions_deletions_4_weeks?.additions || 219 | // null, 220 | 221 | // public interest 222 | alexa_rank: result.public_interest_stats?.alexa_rank || undefined, 223 | bing_matches: result.public_interest_stats?.bing_matches || undefined, 224 | 225 | // market data shortcuts 226 | price_usd: result.market_data?.current_price?.usd || undefined, 227 | price_btc: result.market_data?.current_price?.btc || undefined, 228 | volume_usd: result.market_data?.total_volume?.usd || undefined, 229 | volume_btc: result.market_data?.total_volume?.btc || undefined, 230 | market_cap_usd: result.market_data?.market_cap?.usd || undefined, 231 | market_cap_btc: result.market_data?.market_cap?.btc || undefined, 232 | 233 | // market data 234 | price: result.market_data?.current_price || undefined, 235 | volume: result.market_data?.total_volume || undefined, 236 | market_cap: result.market_data?.market_cap || undefined, 237 | } 238 | 239 | // return 240 | return coinMarket 241 | } 242 | 243 | function wrapCoinMarket(coinMarket: CoinMarket): CoinMarket { 244 | return { 245 | ...coinMarket, 246 | json: JSON.stringify({ 247 | ...coinMarket, 248 | codaSchemaIdentity: 'CoinMarket', 249 | }), 250 | } 251 | } 252 | 253 | function parseCoinDetails(result: CoinResponse): CoinDetails { 254 | const when = getCoinGeckoDate( 255 | result.last_updated || result.market_data?.last_updated, 256 | ) 257 | const whenString = when.toISOString() 258 | const coin = parseCoin(result) 259 | const coinMarket = parseCoinMarket(result, when) 260 | const coinDetails: CoinDetails = { 261 | id: result.id, 262 | json: '', 263 | when: whenString, 264 | coin, 265 | market: coinMarket, 266 | 267 | // fields 268 | description: result.description?.en, 269 | block_time_in_minutes: result.block_time_in_minutes, 270 | coingecko_rank: result.coingecko_rank, 271 | coingecko_score: result.coingecko_score, 272 | community_score: result.community_score, 273 | country_origin: result.country_origin, 274 | developer_score: result.developer_score, 275 | genesis_date: result.genesis_date, 276 | hashing_algorithm: result.hashing_algorithm, 277 | liquidity_score: result.liquidity_score, 278 | market_cap_rank: result.market_cap_rank, 279 | public_interest_score: result.public_interest_score, 280 | sentiment_votes_down_percentage: 281 | result.sentiment_votes_down_percentage / 100, 282 | sentiment_votes_up_percentage: result.sentiment_votes_up_percentage / 100, 283 | 284 | // categories 285 | categories: result.categories || [], 286 | 287 | // links 288 | homepage: result.links.homepage, 289 | blockchain_site: result.links.blockchain_site, 290 | official_forum_url: result.links.official_forum_url, 291 | chat_url: result.links.chat_url, 292 | announcement_url: result.links.announcement_url, 293 | twitter_screen_name: result.links.twitter_screen_name, 294 | facebook_username: result.links.facebook_username, 295 | bitcointalk_thread_identifier: result.links.bitcointalk_thread_identifier, 296 | telegram_channel_identifier: result.links.telegram_channel_identifier, 297 | subreddit_url: result.links.subreddit_url, 298 | 299 | // repos 300 | github: result.links.repos_url.github, 301 | bitbucket: result.links.repos_url.bitbucket, 302 | } 303 | 304 | // return 305 | return coinDetails 306 | } 307 | 308 | function wrapCoinDetails(coinDetails: CoinDetails): CoinDetails { 309 | const market = wrapCoinMarket(coinDetails.market) 310 | return { 311 | ...coinDetails, 312 | market, 313 | json: JSON.stringify({ 314 | ...coinDetails, 315 | market, 316 | codaSchemaIdentity: 'CoinDetails', 317 | }), 318 | } 319 | } 320 | 321 | function parseDefiMarket( 322 | rawDefiMarket: DefiMarketResponse['data'], 323 | ): DefiMarket { 324 | // parse 325 | const defiMarket: DefiMarket = { 326 | volume_24h_usd: Number(rawDefiMarket.trading_volume_24h), 327 | market_cap_usd: Number(rawDefiMarket.defi_market_cap), 328 | defi_dominance: Number(rawDefiMarket.defi_dominance) / 100, 329 | } 330 | 331 | // return 332 | return defiMarket 333 | } 334 | function parseGlobalMarket( 335 | rawGlobalMarket: GlobalMarketResponse['data'], 336 | ): GlobalMarket { 337 | // parse 338 | const globalMarket: GlobalMarket = { 339 | active_cryptocurrencies: rawGlobalMarket.active_cryptocurrencies, 340 | upcoming_icos: rawGlobalMarket.upcoming_icos, 341 | ongoing_icos: rawGlobalMarket.ongoing_icos, 342 | ended_icos: rawGlobalMarket.ended_icos, 343 | markets: rawGlobalMarket.markets, 344 | volume_usd: rawGlobalMarket.total_volume.usd, 345 | market_cap_usd: rawGlobalMarket.total_market_cap.usd, 346 | market_cap_24h_percent: 347 | rawGlobalMarket.market_cap_change_percentage_24h_usd / 100, 348 | market_ratios: { 349 | btc: rawGlobalMarket.market_cap_percentage.btc / 100, 350 | eth: rawGlobalMarket.market_cap_percentage.eth / 100, 351 | usdt: rawGlobalMarket.market_cap_percentage.usdt / 100, 352 | usdc: rawGlobalMarket.market_cap_percentage.usdc / 100, 353 | bnb: rawGlobalMarket.market_cap_percentage.bnb / 100, 354 | xrp: rawGlobalMarket.market_cap_percentage.xrp / 100, 355 | busd: rawGlobalMarket.market_cap_percentage.busd / 100, 356 | ada: rawGlobalMarket.market_cap_percentage.ada / 100, 357 | sol: rawGlobalMarket.market_cap_percentage.sol / 100, 358 | dot: rawGlobalMarket.market_cap_percentage.dot / 100, 359 | }, 360 | updated_at: rawGlobalMarket.updated_at, 361 | } 362 | 363 | // return 364 | return globalMarket 365 | } 366 | 367 | function parseCategories(rawCategories: CategoriesResponse): Array { 368 | // parse 369 | const categories: Array = rawCategories.map((rawCategory) => ({ 370 | id: rawCategory.id, 371 | name: rawCategory.name, 372 | description: rawCategory.content, 373 | volume_24h_usd: rawCategory.volume_24h, 374 | market_cap_usd: rawCategory.market_cap, 375 | market_cap_24h_percent: rawCategory.market_cap_change_24h / 100, 376 | updated_at: rawCategory.updated_at, 377 | })) 378 | 379 | // return 380 | return categories 381 | } 382 | 383 | function parseRates( 384 | rawRates: ExchangeRatesResponse['rates'], 385 | ): Array { 386 | // parse 387 | const rates: Array = [] 388 | for (const key of Object.keys(rawRates)) { 389 | const rawRate = rawRates[key] 390 | const rate: ExchangeRate = { 391 | id: key, 392 | name: rawRate.name, 393 | unit: rawRate.unit, 394 | value: rawRate.value, 395 | type: rawRate.type, 396 | } 397 | rates.push(rate) 398 | } 399 | 400 | // return 401 | return rates 402 | } 403 | 404 | // export async function bindFormulasForSyncTables( 405 | // formula: ( 406 | // params: P, 407 | // context: coda.SyncExecutionContext | coda.ExecutionContext 408 | // ) => R 409 | // ) { 410 | // return async (params: P, context: coda.SyncExecutionContext) => { 411 | // const result = await formula(params, context) 412 | // return { result } 413 | // } 414 | // } 415 | 416 | // ==================================== 417 | // DEFI MARKET 418 | 419 | export async function getDefiMarket( 420 | []: [], 421 | context: coda.SyncExecutionContext | coda.ExecutionContext, 422 | ) { 423 | // fetch 424 | const url = 425 | 'https://api.coingecko.com/api/v3/global/decentralized_finance_defi' 426 | const response = await fetch( 427 | { 428 | method: 'GET', 429 | url: url, 430 | cacheTtlSecs: day, 431 | }, 432 | context, 433 | ) 434 | 435 | // verify 436 | if (!response.body || !response.body.data) { 437 | throw new coda.UserVisibleError(`Failed to fetch defi market data.`) 438 | } 439 | 440 | // return 441 | const defiMarket = parseDefiMarket(response.body.data) 442 | return defiMarket 443 | } 444 | 445 | // ==================================== 446 | // GLOBAL MARKET 447 | 448 | export async function getGlobalMarket( 449 | []: [], 450 | context: coda.SyncExecutionContext | coda.ExecutionContext, 451 | ) { 452 | // fetch 453 | const url = 'https://api.coingecko.com/api/v3/global' 454 | const response = await fetch( 455 | { 456 | method: 'GET', 457 | url: url, 458 | cacheTtlSecs: day, 459 | }, 460 | context, 461 | ) 462 | 463 | // verify 464 | if (!response.body || !response.body.data) { 465 | throw new coda.UserVisibleError(`Failed to fetch global market data.`) 466 | } 467 | 468 | // return 469 | const globalMarket = parseGlobalMarket(response.body.data) 470 | return globalMarket 471 | } 472 | 473 | // ==================================== 474 | // CATEGORIES 475 | 476 | export async function getCategories( 477 | []: [], 478 | context: coda.SyncExecutionContext | coda.ExecutionContext, 479 | ) { 480 | // fetch 481 | const url = 'https://api.coingecko.com/api/v3/coins/categories' 482 | const response = await fetch( 483 | { 484 | method: 'GET', 485 | url: url, 486 | cacheTtlSecs: day, 487 | }, 488 | context, 489 | ) 490 | 491 | // verify 492 | if (!response.body) { 493 | throw new coda.UserVisibleError(`Failed to fetch categories.`) 494 | } 495 | 496 | // return 497 | const categories = parseCategories(response.body) 498 | return categories 499 | } 500 | 501 | // ==================================== 502 | // EXCHANGE RATES 503 | 504 | export async function getExchangeRates( 505 | []: [], 506 | context: coda.SyncExecutionContext | coda.ExecutionContext, 507 | ): Promise> { 508 | // fetch 509 | const url = 'https://api.coingecko.com/api/v3/exchange_rates' 510 | const response = await fetch( 511 | { 512 | method: 'GET', 513 | url: url, 514 | cacheTtlSecs: day, 515 | }, 516 | context, 517 | ) 518 | 519 | // verify 520 | if (!response.body || !response.body.rates) { 521 | throw new coda.UserVisibleError(`Failed to fetch exchange rates.`) 522 | } 523 | 524 | // return 525 | const rates = parseRates(response.body.rates) 526 | return rates 527 | } 528 | 529 | // ==================================== 530 | // CURRENCIES 531 | 532 | export async function getCurrencies( 533 | []: [], 534 | context: coda.SyncExecutionContext | coda.ExecutionContext, 535 | ): Promise { 536 | // fetch 537 | const url = 'https://api.coingecko.com/api/v3/simple/supported_vs_currencies' 538 | const response = await fetch( 539 | { 540 | method: 'GET', 541 | url: url, 542 | cacheTtlSecs: day, 543 | }, 544 | context, 545 | ) 546 | 547 | // verify 548 | if (!response.body) { 549 | throw new coda.UserVisibleError(`Failed to fetch currencies.`) 550 | } 551 | 552 | // return 553 | const currencies: Currencies = response.body 554 | return currencies 555 | } 556 | 557 | // ==================================== 558 | // COIN MARKET 559 | 560 | export async function getCoinMarket( 561 | [input, inputWhen]: [id: string, when?: Date], 562 | context: coda.SyncExecutionContext | coda.ExecutionContext, 563 | ): Promise { 564 | // prepare 565 | const request = parseInput(input) 566 | const when = getCoinGeckoDate(inputWhen || request.when) 567 | if (request.coinMarket?.when === when.toISOString()) { 568 | return wrapCoinMarket(request.coinMarket) 569 | } 570 | const wasToday = isToday(when) 571 | 572 | // check 573 | if (!request.id) { 574 | throw new coda.UserVisibleError(`A coin identifier must be provided`) 575 | } 576 | 577 | // fetch 578 | const query = { 579 | date: getCoinGeckoDateString(when), 580 | localization: false, 581 | } 582 | const url = coda.withQueryParams( 583 | `https://api.coingecko.com/api/v3/coins/${request.id}/history`, 584 | query, 585 | ) 586 | const response = await fetch( 587 | { 588 | method: 'GET', 589 | url: url, 590 | cacheTtlSecs: wasToday ? day : longTime, 591 | }, 592 | context, 593 | ) 594 | 595 | // verify 596 | if (!response.body || !response.body.market_data) { 597 | if (wasToday) { 598 | // try yesterday, as perhaps we live in the future 599 | return getCoinMarket([request.id, getYesterday(when)], context) 600 | } else { 601 | throw new coda.UserVisibleError(`Failed to fetch coin market data.`) 602 | } 603 | } 604 | 605 | // return 606 | const coinMarket = parseCoinMarket(response.body, when) 607 | return wrapCoinMarket(coinMarket) 608 | } 609 | 610 | // ==================================== 611 | // COIN DETAILS 612 | 613 | export async function getCoinDetails( 614 | [input]: [input: string], 615 | context: coda.SyncExecutionContext | coda.ExecutionContext, 616 | ): Promise { 617 | // prepare 618 | const request = parseInput(input) 619 | if (request.coinDetails) return wrapCoinDetails(request.coinDetails) 620 | 621 | // check 622 | if (!request.id) { 623 | throw new coda.UserVisibleError(`A coin identifier must be provided`) 624 | } 625 | 626 | // fetch 627 | const query = { 628 | market_data: true, 629 | community_data: true, 630 | developer_data: true, 631 | localization: false, 632 | sparkline: false, 633 | tickers: false, 634 | } 635 | const url = coda.withQueryParams( 636 | `https://api.coingecko.com/api/v3/coins/${request.id}`, 637 | query, 638 | ) 639 | const response = await fetch( 640 | { 641 | method: 'GET', 642 | url: url, 643 | cacheTtlSecs: day, 644 | }, 645 | context, 646 | ) 647 | 648 | // verify 649 | if (!response.body) { 650 | throw new coda.UserVisibleError(`Failed to fetch coin details.`) 651 | } 652 | 653 | // return 654 | const coinDetails = parseCoinDetails(response.body) 655 | return wrapCoinDetails(coinDetails) 656 | } 657 | 658 | // export async function getCoinCurrency( 659 | // [id, when = new Date(), currency = 'usd']: [ 660 | // id: string, 661 | // currency?: string, 662 | // when?: Date 663 | // ], 664 | // context: coda.SyncExecutionContext | coda.ExecutionContext 665 | // ) { 666 | // const result = await getCoinMarket([id, when], context) 667 | // const prices = result.market_data?.current_price 668 | // if (!prices) { 669 | // throw new coda.UserVisibleError(`Coin [${id}] returns no market data.`) 670 | // } 671 | // const price = prices[currency.toLowerCase()] 672 | // if (!price) { 673 | // const currencies = Object.keys(prices).sort().join(', ') 674 | // throw new coda.UserVisibleError( 675 | // `Invalid currency [${currency}]. Available currencies are: ${currencies}` 676 | // ) 677 | // } 678 | // return price 679 | // } 680 | 681 | // ==================================== 682 | // COINS 683 | 684 | export async function getCoins( 685 | []: [], 686 | context: coda.SyncExecutionContext | coda.ExecutionContext, 687 | ) { 688 | // fetch 689 | const url = `https://api.coingecko.com/api/v3/coins/list` 690 | const response = await fetch( 691 | { 692 | method: 'GET', 693 | url: url, 694 | cacheTtlSecs: day, 695 | }, 696 | context, 697 | ) 698 | 699 | // verify 700 | if (!response.body) { 701 | throw new coda.UserVisibleError(`Failed to fetch coins.`) 702 | } 703 | 704 | // return 705 | const coins = parseCoins(response.body) 706 | return coins 707 | } 708 | 709 | // ==================================== 710 | // TRENDING COINS 711 | 712 | export async function trendingCoins( 713 | []: [], 714 | context: coda.SyncExecutionContext | coda.ExecutionContext, 715 | ) { 716 | // fetch 717 | const url = `https://api.coingecko.com/api/v3/search/trending` 718 | const response = await fetch( 719 | { 720 | method: 'GET', 721 | url: url, 722 | cacheTtlSecs: day, 723 | }, 724 | context, 725 | ) 726 | 727 | // verify 728 | if (!response.body || !response.body.coins) { 729 | throw new coda.UserVisibleError(`Failed to fetch trending coins.`) 730 | } 731 | 732 | // return 733 | const coins = parseTrendingCoins(response.body.coins) 734 | return coins 735 | } 736 | 737 | // ==================================== 738 | // SEARCH COINS 739 | 740 | export async function searchCoins( 741 | [search]: [search: string], 742 | context: coda.SyncExecutionContext | coda.ExecutionContext, 743 | ) { 744 | // fetch 745 | const url = coda.withQueryParams(`https://api.coingecko.com/api/v3/search`, { 746 | query: search, 747 | }) 748 | const response = await fetch( 749 | { 750 | method: 'GET', 751 | url: url, 752 | cacheTtlSecs: day, 753 | }, 754 | context, 755 | ) 756 | 757 | // verify 758 | if (!response.body || !response.body.coins) { 759 | throw new coda.UserVisibleError(`Failed to fetch searched coins.`) 760 | } 761 | 762 | // return 763 | const coins = parseCoins(response.body.coins) 764 | return coins 765 | } 766 | -------------------------------------------------------------------------------- /packs/coingecko/schemas.ts: -------------------------------------------------------------------------------- 1 | import * as coda from '@codahq/packs-sdk' 2 | import { AttributionNode } from '@codahq/packs-sdk' 3 | 4 | import { 5 | usdSchema, 6 | currencySchema, 7 | percentSchema, 8 | minutesSchema, 9 | stringSchema, 10 | numberSchema, 11 | datetimeStringSchema, 12 | datetimeNumberSchema, 13 | urlSchema, 14 | imageSchema, 15 | linkSchema, 16 | booleanSchema, 17 | } from '../shared/schemas' 18 | 19 | export const Attribution: AttributionNode[] = [ 20 | { 21 | type: coda.AttributionNodeType.Text, 22 | text: 'Provided by CoinGecko', 23 | }, 24 | { 25 | type: coda.AttributionNodeType.Link, 26 | anchorText: 'coingecko.com', 27 | anchorUrl: 'https://coingecko.com', 28 | }, 29 | { 30 | type: coda.AttributionNodeType.Image, 31 | imageUrl: 'https://coingecko.com/favicon.ico', 32 | anchorUrl: 'https://coingecko.com', 33 | }, 34 | ] 35 | 36 | // Defi Market 37 | export const DefiMarketSchema = coda.makeObjectSchema({ 38 | properties: { 39 | volume_24h_usd: { 40 | description: 'Volume in USD (24 Hours)', 41 | required: true, 42 | ...usdSchema, 43 | }, 44 | market_cap_usd: { 45 | description: 'Market Cap in USD', 46 | required: true, 47 | ...usdSchema, 48 | }, 49 | defi_dominance: { 50 | description: 'Defi Dominance', 51 | required: true, 52 | ...percentSchema, 53 | }, 54 | }, 55 | identity: { 56 | name: 'DefiMarket', 57 | }, 58 | displayProperty: 'market_cap_usd', 59 | featuredProperties: ['volume_24h_usd', 'market_cap_usd', 'defi_dominance'], 60 | attribution: Attribution, 61 | includeUnknownProperties: false, 62 | }) 63 | 64 | // Coin Market Data 65 | export const CurrencyComparisonSchema = coda.makeObjectSchema({ 66 | properties: { 67 | btc: { 68 | description: 'BTC', 69 | required: true, 70 | ...currencySchema, 71 | }, 72 | eth: { 73 | description: 'ETH', 74 | required: true, 75 | ...currencySchema, 76 | }, 77 | ltc: { 78 | description: 'LTC', 79 | required: true, 80 | ...currencySchema, 81 | }, 82 | bch: { 83 | description: 'BCH', 84 | required: true, 85 | ...currencySchema, 86 | }, 87 | bnb: { 88 | description: 'BNB', 89 | required: true, 90 | ...currencySchema, 91 | }, 92 | eos: { 93 | description: 'EOS', 94 | required: true, 95 | ...currencySchema, 96 | }, 97 | xrp: { 98 | description: 'XRP', 99 | required: true, 100 | ...currencySchema, 101 | }, 102 | xlm: { 103 | description: 'XLM', 104 | required: true, 105 | ...currencySchema, 106 | }, 107 | link: { 108 | description: 'LIN', 109 | required: true, 110 | ...currencySchema, 111 | }, 112 | dot: { 113 | description: 'DOT', 114 | required: true, 115 | ...currencySchema, 116 | }, 117 | yfi: { 118 | description: 'YFI', 119 | required: true, 120 | ...currencySchema, 121 | }, 122 | usd: { 123 | description: 'USD', 124 | required: true, 125 | ...currencySchema, 126 | }, 127 | aed: { 128 | description: 'AED', 129 | required: true, 130 | ...currencySchema, 131 | }, 132 | ars: { 133 | description: 'ARS', 134 | required: true, 135 | ...currencySchema, 136 | }, 137 | aud: { 138 | description: 'AUD', 139 | required: true, 140 | ...currencySchema, 141 | }, 142 | bdt: { 143 | description: 'BDT', 144 | required: true, 145 | ...currencySchema, 146 | }, 147 | bhd: { 148 | description: 'BHD', 149 | required: true, 150 | ...currencySchema, 151 | }, 152 | bmd: { 153 | description: 'BMD', 154 | required: true, 155 | ...currencySchema, 156 | }, 157 | brl: { 158 | description: 'BRL', 159 | required: true, 160 | ...currencySchema, 161 | }, 162 | cad: { 163 | description: 'CAD', 164 | required: true, 165 | ...currencySchema, 166 | }, 167 | chf: { 168 | description: 'CHF', 169 | required: true, 170 | ...currencySchema, 171 | }, 172 | clp: { 173 | description: 'CLP', 174 | required: true, 175 | ...currencySchema, 176 | }, 177 | cny: { 178 | description: 'CNY', 179 | required: true, 180 | ...currencySchema, 181 | }, 182 | czk: { 183 | description: 'CZK', 184 | required: true, 185 | ...currencySchema, 186 | }, 187 | dkk: { 188 | description: 'DKK', 189 | required: true, 190 | ...currencySchema, 191 | }, 192 | eur: { 193 | description: 'EUR', 194 | required: true, 195 | ...currencySchema, 196 | }, 197 | gbp: { 198 | description: 'GBP', 199 | required: true, 200 | ...currencySchema, 201 | }, 202 | hkd: { 203 | description: 'HKD', 204 | required: true, 205 | ...currencySchema, 206 | }, 207 | huf: { 208 | description: 'HUF', 209 | required: true, 210 | ...currencySchema, 211 | }, 212 | idr: { 213 | description: 'IDR', 214 | required: true, 215 | ...currencySchema, 216 | }, 217 | ils: { 218 | description: 'ILS', 219 | required: true, 220 | ...currencySchema, 221 | }, 222 | inr: { 223 | description: 'INR', 224 | required: true, 225 | ...currencySchema, 226 | }, 227 | jpy: { 228 | description: 'JPY', 229 | required: true, 230 | ...currencySchema, 231 | }, 232 | krw: { 233 | description: 'KRW', 234 | required: true, 235 | ...currencySchema, 236 | }, 237 | kwd: { 238 | description: 'KWD', 239 | required: true, 240 | ...currencySchema, 241 | }, 242 | lkr: { 243 | description: 'LKR', 244 | required: true, 245 | ...currencySchema, 246 | }, 247 | mmk: { 248 | description: 'MMK', 249 | required: true, 250 | ...currencySchema, 251 | }, 252 | mxn: { 253 | description: 'MXN', 254 | required: true, 255 | ...currencySchema, 256 | }, 257 | myr: { 258 | description: 'MYR', 259 | required: true, 260 | ...currencySchema, 261 | }, 262 | ngn: { 263 | description: 'NGN', 264 | required: true, 265 | ...currencySchema, 266 | }, 267 | nok: { 268 | description: 'NOK', 269 | required: true, 270 | ...currencySchema, 271 | }, 272 | nzd: { 273 | description: 'NZD', 274 | required: true, 275 | ...currencySchema, 276 | }, 277 | php: { 278 | description: 'PHP', 279 | required: true, 280 | ...currencySchema, 281 | }, 282 | pkr: { 283 | description: 'PKR', 284 | required: true, 285 | ...currencySchema, 286 | }, 287 | pln: { 288 | description: 'PLN', 289 | required: true, 290 | ...currencySchema, 291 | }, 292 | rub: { 293 | description: 'RUB', 294 | required: true, 295 | ...currencySchema, 296 | }, 297 | sar: { 298 | description: 'SAR', 299 | required: true, 300 | ...currencySchema, 301 | }, 302 | sek: { 303 | description: 'SEK', 304 | required: true, 305 | ...currencySchema, 306 | }, 307 | sgd: { 308 | description: 'SGD', 309 | required: true, 310 | ...currencySchema, 311 | }, 312 | thb: { 313 | description: 'THB', 314 | required: true, 315 | ...currencySchema, 316 | }, 317 | try: { 318 | description: 'TRY', 319 | required: true, 320 | ...currencySchema, 321 | }, 322 | twd: { 323 | description: 'TWD', 324 | required: true, 325 | ...currencySchema, 326 | }, 327 | uah: { 328 | description: 'UAH', 329 | required: true, 330 | ...currencySchema, 331 | }, 332 | vef: { 333 | description: 'VEF', 334 | required: true, 335 | ...currencySchema, 336 | }, 337 | vnd: { 338 | description: 'VND', 339 | required: true, 340 | ...currencySchema, 341 | }, 342 | zar: { 343 | description: 'ZAR', 344 | required: true, 345 | ...currencySchema, 346 | }, 347 | xdr: { 348 | description: 'XDR', 349 | required: true, 350 | ...currencySchema, 351 | }, 352 | xag: { 353 | description: 'XAG', 354 | required: true, 355 | ...currencySchema, 356 | }, 357 | xau: { 358 | description: 'XAU', 359 | required: true, 360 | ...currencySchema, 361 | }, 362 | bits: { 363 | description: 'BIT', 364 | required: true, 365 | ...currencySchema, 366 | }, 367 | sats: { 368 | description: 'SAT', 369 | required: true, 370 | ...currencySchema, 371 | }, 372 | }, 373 | identity: { 374 | name: 'CurrencyComparison', 375 | }, 376 | displayProperty: 'usd', 377 | featuredProperties: ['usd', 'btc', 'eth', 'eur'], 378 | attribution: Attribution, 379 | includeUnknownProperties: false, 380 | }) 381 | 382 | // Market Ratios 383 | export const GlobalMarketRatiosSchema = coda.makeObjectSchema({ 384 | properties: { 385 | btc: { 386 | description: 'BTC dominance', 387 | required: true, 388 | ...percentSchema, 389 | }, 390 | eth: { 391 | description: 'ETH dominance', 392 | required: true, 393 | ...percentSchema, 394 | }, 395 | usdt: { 396 | description: 'USDT dominance', 397 | required: true, 398 | ...percentSchema, 399 | }, 400 | usdc: { 401 | description: 'USDC dominance', 402 | required: true, 403 | ...percentSchema, 404 | }, 405 | bnb: { 406 | description: 'BNB dominance', 407 | required: true, 408 | ...percentSchema, 409 | }, 410 | xrp: { 411 | description: 'XRP dominance', 412 | required: true, 413 | ...percentSchema, 414 | }, 415 | busd: { 416 | description: 'BUSD dominance', 417 | required: true, 418 | ...percentSchema, 419 | }, 420 | ada: { 421 | description: 'ADA dominance', 422 | required: true, 423 | ...percentSchema, 424 | }, 425 | sol: { 426 | description: 'SOL dominance', 427 | required: true, 428 | ...percentSchema, 429 | }, 430 | dot: { 431 | description: 'DOT dominance', 432 | required: true, 433 | ...percentSchema, 434 | }, 435 | }, 436 | identity: { 437 | name: 'MarketRatios', 438 | }, 439 | displayProperty: 'btc', 440 | featuredProperties: ['btc', 'eth', 'usdt', 'usdc'], 441 | attribution: Attribution, 442 | includeUnknownProperties: false, 443 | }) 444 | 445 | // Global Market 446 | export const GlobalMarketSchema = coda.makeObjectSchema({ 447 | properties: { 448 | active_cryptocurrencies: { 449 | description: 'Count of Active Cryptocurrencies', 450 | required: true, 451 | ...numberSchema, 452 | }, 453 | upcoming_icos: { 454 | description: 'Count of Upcoming ICOs', 455 | required: true, 456 | ...numberSchema, 457 | }, 458 | ongoing_icos: { 459 | description: 'Count of Ended ICOs', 460 | required: true, 461 | ...numberSchema, 462 | }, 463 | markets: { 464 | description: 'Count of Markets', 465 | required: true, 466 | ...numberSchema, 467 | }, 468 | volume_usd: { 469 | description: 'Volume in USD', 470 | required: true, 471 | ...usdSchema, 472 | }, 473 | market_cap_usd: { 474 | description: 'Market Cap in USD', 475 | required: true, 476 | ...usdSchema, 477 | }, 478 | market_cap_24h_percent: { 479 | description: 'Market Cap % Change (24 Hours)', 480 | required: true, 481 | ...percentSchema, 482 | }, 483 | market_ratios: GlobalMarketRatiosSchema, 484 | updated_at: { 485 | description: 'Last Updated', 486 | required: true, 487 | ...datetimeNumberSchema, 488 | }, 489 | }, 490 | identity: { 491 | name: 'GlobalMarket', 492 | }, 493 | idProperty: 'updated_at', 494 | displayProperty: 'market_cap_usd', 495 | featuredProperties: [ 496 | 'volume_usd', 497 | 'market_cap_usd', 498 | 'market_cap_24h_percent', 499 | 'market_ratios', 500 | ], 501 | attribution: Attribution, 502 | includeUnknownProperties: false, 503 | }) 504 | 505 | // Category 506 | export const CategorySchema = coda.makeObjectSchema({ 507 | properties: { 508 | id: { 509 | description: 510 | 'CoinGecko Identifier for the Category, e.g. `binance-smart-chain`', 511 | required: true, 512 | ...stringSchema, 513 | }, 514 | name: { 515 | description: 'Name, e.g. `BNB Chain Ecosystem`', 516 | required: true, 517 | ...stringSchema, 518 | }, 519 | description: { 520 | description: 'Description', 521 | required: true, 522 | ...stringSchema, 523 | }, 524 | volume_24h_usd: { 525 | description: 'Volume in USD (24 Hours), e.g. `83554662546.89467`', 526 | required: true, 527 | ...usdSchema, 528 | }, 529 | market_cap_usd: { 530 | description: 'Market Cap in USD, e.g. `249398905817.62985`', 531 | required: true, 532 | ...usdSchema, 533 | }, 534 | market_cap_24h_percent: { 535 | description: 'Market Cap % Change (24 Hours), e.g. `-0.7515690936538925`', 536 | required: true, 537 | ...percentSchema, 538 | }, 539 | updated_at: { 540 | description: 'Last Updated', 541 | required: true, 542 | ...datetimeStringSchema, 543 | }, 544 | }, 545 | identity: { 546 | name: 'Category', 547 | }, 548 | idProperty: 'id', 549 | displayProperty: 'name', 550 | descriptionProperty: 'description', 551 | // title, subtitle 552 | featuredProperties: [ 553 | 'name', 554 | 'volume_24h_usd', 555 | 'market_cap_usd', 556 | 'market_cap_24h_percent', 557 | 'description', 558 | ], 559 | attribution: Attribution, 560 | includeUnknownProperties: false, 561 | }) 562 | 563 | // Exchange Rate 564 | export const ExchangeRateSchema = coda.makeObjectSchema({ 565 | properties: { 566 | id: { 567 | description: 'CoinGecko Identifier for the Currency, e.g. `chf`', 568 | required: true, 569 | ...stringSchema, 570 | }, 571 | name: { 572 | description: 'Name, e.g. `Swiss Franc`', 573 | required: true, 574 | ...stringSchema, 575 | }, 576 | unit: { 577 | description: 'Unit, e.g. `Fr.`', 578 | required: true, 579 | ...stringSchema, 580 | }, 581 | value: { 582 | description: 'BTC-to-Currency exchange rate', 583 | required: true, 584 | ...numberSchema, 585 | }, 586 | type: { 587 | description: '`fiat` or `crypto`', 588 | required: true, 589 | ...stringSchema, 590 | }, 591 | }, 592 | identity: { 593 | name: 'BitcoinExchangeRate', 594 | }, 595 | idProperty: 'id', 596 | displayProperty: 'name', 597 | // title, subtitle 598 | featuredProperties: ['name', 'unit', 'value', 'type'], 599 | attribution: Attribution, 600 | includeUnknownProperties: false, 601 | }) 602 | 603 | // Currencies 604 | export const CurrencySchema = coda.makeSchema({ 605 | ...stringSchema, 606 | }) 607 | 608 | // Coin Currency 609 | // const CoinCurrencySchema = coda.makeSchema({ 610 | // ...currencySchema, 611 | // }) 612 | 613 | // Coin 614 | export const CoinSchema = coda.makeObjectSchema({ 615 | properties: { 616 | id: { 617 | description: 618 | 'CoinGecko Identifier for the Coin, e.g. `1x-short-bitcoin-token`', 619 | required: true, 620 | ...stringSchema, 621 | }, 622 | symbol: { 623 | description: 'Symbol of the Coin used across exchanges, e.g. `hedge`', 624 | required: true, 625 | ...stringSchema, 626 | }, 627 | name: { 628 | description: 'Name of the Coin, e.g. `1X Short Bitcoin Token`', 629 | required: true, 630 | ...stringSchema, 631 | }, 632 | url: { 633 | description: 634 | 'URL of the Coin, e.g. https://coingecko.com/coins/1x-short-bitcoin-token', 635 | required: true, 636 | ...urlSchema, 637 | }, 638 | image: { 639 | description: 'Image of the Coin', 640 | required: false, 641 | ...imageSchema, 642 | }, 643 | }, 644 | identity: { 645 | name: 'Coin', 646 | }, 647 | idProperty: 'id', 648 | displayProperty: 'name', 649 | descriptionProperty: 'url', 650 | imageProperty: 'image', 651 | // title, subtitle 652 | featuredProperties: ['symbol', 'name', 'url', 'image'], 653 | attribution: Attribution, 654 | includeUnknownProperties: false, 655 | }) 656 | 657 | export const CoinMarketSchema = coda.makeObjectSchema({ 658 | properties: { 659 | id: { 660 | description: 661 | 'Identifier for the Coin Market, e.g. `1x-short-bitcoin-token@2022-08-03T00:36:23.098Z`', 662 | required: true, 663 | ...stringSchema, 664 | }, 665 | coin: CoinSchema, 666 | when: { 667 | description: 'Date of this data', 668 | required: true, 669 | ...datetimeStringSchema, 670 | }, 671 | price_usd: { 672 | description: 'Price in USD', 673 | required: false, 674 | ...usdSchema, 675 | }, 676 | price_btc: { 677 | description: 'Price in BTC', 678 | required: false, 679 | ...currencySchema, 680 | }, 681 | volume_usd: { 682 | description: 'Volume in USD', 683 | required: false, 684 | ...usdSchema, 685 | }, 686 | volume_btc: { 687 | description: 'Volume in BTC', 688 | required: false, 689 | ...currencySchema, 690 | }, 691 | market_cap_usd: { 692 | description: 'Market Cap in USD', 693 | required: false, 694 | ...usdSchema, 695 | }, 696 | market_cap_btc: { 697 | description: 'Market Cap in BTC', 698 | required: false, 699 | ...currencySchema, 700 | }, 701 | // market 702 | price: CurrencyComparisonSchema, 703 | volume: CurrencyComparisonSchema, 704 | market_cap: CurrencyComparisonSchema, 705 | // community 706 | facebook_likes: { 707 | description: 'Number of Facebook likes', 708 | required: false, 709 | ...numberSchema, 710 | }, 711 | twitter_followers: { 712 | description: 'Number of Twitter followers', 713 | required: false, 714 | ...numberSchema, 715 | }, 716 | reddit_average_posts_48h: { 717 | description: 'Number of Reddit average posts (48 hours)', 718 | required: false, 719 | ...numberSchema, 720 | }, 721 | reddit_average_comments_48h: { 722 | description: 'Number of Reddit average comments (48 hours)', 723 | required: false, 724 | ...numberSchema, 725 | }, 726 | reddit_subscribers: { 727 | description: 'Number of Reddit subscribers', 728 | required: false, 729 | ...numberSchema, 730 | }, 731 | reddit_accounts_active_48h: { 732 | description: 'Number of Reddit accounts active (48 hours)', 733 | required: false, 734 | ...numberSchema, 735 | }, 736 | telegram_channel_user_count: { 737 | description: 'Number of Telegram channel user count', 738 | required: false, 739 | ...numberSchema, 740 | }, 741 | // developer 742 | forks: { 743 | description: 'Number of forks', 744 | required: false, 745 | ...numberSchema, 746 | }, 747 | stars: { 748 | description: 'Number of stars', 749 | required: false, 750 | ...numberSchema, 751 | }, 752 | subscribers: { 753 | description: 'Number of subscribers', 754 | required: false, 755 | ...numberSchema, 756 | }, 757 | total_issues: { 758 | description: 'Number of total issues', 759 | required: false, 760 | ...numberSchema, 761 | }, 762 | closed_issues: { 763 | description: 'Number of closed issues', 764 | required: false, 765 | ...numberSchema, 766 | }, 767 | pull_requests_merged: { 768 | description: 'Number of pull requests merged', 769 | required: false, 770 | ...numberSchema, 771 | }, 772 | pull_request_contributors: { 773 | description: 'Number of pull request contributors', 774 | required: false, 775 | ...numberSchema, 776 | }, 777 | commit_count_4_weeks: { 778 | description: 'Number of commit count (4 weeks)', 779 | required: false, 780 | ...numberSchema, 781 | }, 782 | // public interest 783 | alexa_rank: { 784 | description: 'Alexa Rank', 785 | required: false, 786 | ...numberSchema, 787 | }, 788 | bing_matches: { 789 | description: 'Bing Matches', 790 | required: false, 791 | ...numberSchema, 792 | }, 793 | // wrapper 794 | json: { 795 | description: 'JSON data for sending to Coda Column Types', 796 | required: true, 797 | ...stringSchema, 798 | }, 799 | }, 800 | identity: { 801 | name: 'CoinMarket', 802 | }, 803 | idProperty: 'id', 804 | displayProperty: 'price_usd', // 'coin', 805 | // title, subtitle 806 | featuredProperties: [ 807 | 'id', 808 | 'when', 809 | 'price_usd', 810 | 'price_btc', 811 | 'volume_usd', 812 | 'volume_btc', 813 | 'market_cap_usd', 814 | 'market_cap_btc', 815 | 'price', 816 | 'volume', 817 | 'market_cap', 818 | 'twitter_followers', 819 | 'commit_count_4_weeks', 820 | 'alexa_rank', 821 | 'bing_matches', 822 | ], 823 | attribution: Attribution, 824 | includeUnknownProperties: false, 825 | }) 826 | 827 | export const CoinDetailsSchema = coda.makeObjectSchema({ 828 | properties: { 829 | id: { 830 | description: 831 | 'CoinGecko Identifier for the Coin, e.g. `1x-short-bitcoin-token`', 832 | required: true, 833 | ...stringSchema, 834 | }, 835 | when: { 836 | description: 'Date of this data', 837 | required: true, 838 | ...datetimeStringSchema, 839 | }, 840 | coin: CoinSchema, 841 | market: CoinMarketSchema, 842 | categories: { 843 | description: 'CoinGecko Category Identifiers', 844 | required: false, 845 | type: coda.ValueType.Array, 846 | items: { 847 | type: coda.ValueType.String, 848 | required: false, 849 | }, 850 | }, 851 | block_time_in_minutes: { 852 | description: 'Block Time in Minutes', 853 | required: false, 854 | ...minutesSchema, 855 | }, 856 | coingecko_rank: { 857 | required: false, 858 | ...numberSchema, 859 | }, 860 | coingecko_score: { 861 | required: false, 862 | ...numberSchema, 863 | }, 864 | community_score: { 865 | required: false, 866 | ...numberSchema, 867 | }, 868 | country_origin: { 869 | required: false, 870 | ...stringSchema, 871 | }, 872 | description: { 873 | required: false, 874 | ...stringSchema, 875 | }, 876 | developer_score: { 877 | required: false, 878 | ...numberSchema, 879 | }, 880 | genesis_date: { 881 | required: false, 882 | ...datetimeStringSchema, 883 | }, 884 | hashing_algorithm: { 885 | required: false, 886 | ...stringSchema, 887 | }, 888 | liquidity_score: { 889 | required: false, 890 | ...numberSchema, 891 | }, 892 | market_cap_rank: { 893 | required: false, 894 | ...numberSchema, 895 | }, 896 | public_interest_score: { 897 | required: false, 898 | ...numberSchema, 899 | }, 900 | sentiment_votes_down_percentage: { 901 | required: false, 902 | ...percentSchema, 903 | }, 904 | sentiment_votes_up_percentage: { 905 | required: false, 906 | ...percentSchema, 907 | }, 908 | // urls 909 | homepage: linkSchema, 910 | blockchain_site: linkSchema, 911 | official_forum_url: linkSchema, 912 | chat_url: linkSchema, 913 | announcement_url: linkSchema, 914 | twitter_screen_name: { 915 | required: false, 916 | ...stringSchema, 917 | }, 918 | facebook_username: { 919 | required: false, 920 | ...stringSchema, 921 | }, 922 | bitcointalk_thread_identifier: { 923 | required: false, 924 | ...stringSchema, 925 | }, 926 | telegram_channel_identifier: { 927 | required: false, 928 | ...stringSchema, 929 | }, 930 | subreddit_url: { 931 | required: false, 932 | ...stringSchema, 933 | }, 934 | // repo urls 935 | github: linkSchema, 936 | bitbucket: linkSchema, 937 | // wrapper 938 | json: { 939 | description: 'JSON data for sending to Coda Column Types', 940 | required: true, 941 | ...stringSchema, 942 | }, 943 | }, 944 | identity: { 945 | name: 'CoinDetails', 946 | }, 947 | idProperty: 'id', 948 | displayProperty: 'coin', 949 | // title, subtitle 950 | featuredProperties: [ 951 | 'id', 952 | 'coin', 953 | 'market', 954 | 'description', 955 | 'coingecko_rank', 956 | 'coingecko_score', 957 | ], 958 | attribution: Attribution, 959 | includeUnknownProperties: false, 960 | }) 961 | 962 | // const CoinReferenceSchema = coda.makeReferenceSchemaFromObjectSchema( 963 | // CoinSchema, 964 | // 'Coin' 965 | // ) 966 | --------------------------------------------------------------------------------