diff --git a/.github/workflows/frontend-build.yml b/.github/workflows/frontend-build.yml index fe0bc6db..746dd4bb 100644 --- a/.github/workflows/frontend-build.yml +++ b/.github/workflows/frontend-build.yml @@ -1,4 +1,4 @@ -name: "Frontend Build Check" +name: Frontend Build Check on: pull_request: @@ -37,7 +37,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version-file: .nvmrc - cache: "pnpm" + cache: pnpm - name: Install dependencies with pnpm run: pnpm install diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ed42d9c2..da0f4b2c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version-file: .nvmrc - cache: 'pnpm' + cache: pnpm - name: Install dependencies with pnpm run: pnpm install @@ -40,4 +40,4 @@ jobs: echo $mrdiff git diff exit 1 - fi \ No newline at end of file + fi diff --git a/.github/workflows/mainnet-api-main.yml b/.github/workflows/mainnet-api-main.yml index 0938bd00..04adebf8 100644 --- a/.github/workflows/mainnet-api-main.yml +++ b/.github/workflows/mainnet-api-main.yml @@ -25,7 +25,7 @@ jobs: FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN_API_MAIN }} smoke-tests: - needs: [ deploy-mainnet ] + needs: [deploy-mainnet] if: needs.deploy-mainnet.result == 'success' uses: ./.github/workflows/smoke-tests.yml secrets: inherit @@ -33,7 +33,7 @@ jobs: app_name: dither-mainnet-api-main notify: - needs: [ smoke-tests ] + needs: [smoke-tests] if: failure() runs-on: ubuntu-latest steps: @@ -43,13 +43,13 @@ jobs: token: ${{ secrets.TELEGRAM_BOT_TOKEN }} message: | 🚨 **Smoke Tests Failed** 🚨 - + 🔗 Repository: ${{ github.repository }} 📝 Commit: ${{ github.event.commits[0].message }} 👤 Author: ${{ github.actor }} ⛓️‍💥 App: dither-mainnet-api-main - - ⚠️ **Failed Tests:** + + ⚠️ **Failed Tests:** 🔍 [View Logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) - + Please investigate and fix ASAP! 🔧 diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index ffdde40d..b0920b58 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: package: - description: "Package name to publish" + description: Package name to publish required: true type: choice options: @@ -22,8 +22,8 @@ jobs: uses: actions/setup-node@v4 with: node-version: 20 - registry-url: "https://registry.npmjs.org/" - scope: "@atomone" + registry-url: 'https://registry.npmjs.org/' + scope: '@atomone' - name: Install pnpm run: npm install -g pnpm diff --git a/.github/workflows/retype-action.yml b/.github/workflows/retype-action.yml index 88881d03..e3a420bf 100644 --- a/.github/workflows/retype-action.yml +++ b/.github/workflows/retype-action.yml @@ -21,4 +21,4 @@ jobs: - uses: retypeapp/action-github-pages@latest with: - update-branch: true \ No newline at end of file + update-branch: true diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 9465aed2..450c25f1 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version-file: .nvmrc - cache: 'pnpm' + cache: pnpm - name: Install dependencies with pnpm run: pnpm install diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f5a597fe..a3715482 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,11 +37,15 @@ jobs: - uses: actions/setup-node@v4 with: node-version-file: .nvmrc - cache: 'pnpm' + cache: pnpm - name: Install dependencies with pnpm run: pnpm install + - name: Build dependencies + working-directory: packages/api-main + run: pnpm build:deps + - name: Setup database schema working-directory: packages/api-main run: pnpm db:push:force diff --git a/.github/workflows/testnet-api-main.yml b/.github/workflows/testnet-api-main.yml index f998c080..49b6bfe9 100644 --- a/.github/workflows/testnet-api-main.yml +++ b/.github/workflows/testnet-api-main.yml @@ -45,7 +45,7 @@ jobs: uses: actions/setup-node@v4 with: node-version-file: .nvmrc - cache: "pnpm" + cache: pnpm - name: Install dependencies with pnpm run: pnpm install @@ -75,7 +75,7 @@ jobs: pnpm test deploy-testnet: - needs: [ detect-changes, test ] + needs: [detect-changes, test] if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.changed == 'true' runs-on: ubuntu-latest environment: testnet @@ -94,7 +94,7 @@ jobs: FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN_API_MAIN }} smoke-tests: - needs: [ deploy-testnet ] + needs: [deploy-testnet] if: needs.deploy-testnet.result == 'success' uses: ./.github/workflows/smoke-tests.yml secrets: inherit @@ -102,7 +102,7 @@ jobs: app_name: dither-testnet-api-main notify: - needs: [ smoke-tests ] + needs: [smoke-tests] if: failure() runs-on: ubuntu-latest steps: @@ -112,13 +112,13 @@ jobs: token: ${{ secrets.TELEGRAM_BOT_TOKEN }} message: | 🚨 **Smoke Tests Failed** 🚨 - + 🔗 Repository: ${{ github.repository }} 📝 Commit: ${{ github.event.commits[0].message }} 👤 Author: ${{ github.actor }} ⛓️‍💥 App: dither-testnet-api-main - - ⚠️ **Failed Tests:** + + ⚠️ **Failed Tests:** 🔍 [View Logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) - + Please investigate and fix ASAP! 🔧 diff --git a/.github/workflows/testnet-reader-main.yml b/.github/workflows/testnet-reader-main.yml index 72d6e8ec..13dbe029 100644 --- a/.github/workflows/testnet-reader-main.yml +++ b/.github/workflows/testnet-reader-main.yml @@ -37,7 +37,7 @@ jobs: # Add reader-main specific test steps here deploy-testnet: - needs: [ detect-changes, test ] + needs: [detect-changes, test] if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.changed == 'true' runs-on: ubuntu-latest environment: testnet diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..cb2c84d5 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm lint-staged diff --git a/docker-compose.yml b/docker-compose.yml index 00e1dc76..a5e30e2f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,11 +10,11 @@ services: POSTGRES_USER: default POSTGRES_PASSWORD: password ports: - - "5432:5432" + - '5432:5432' volumes: - ./data/postgres_data:/var/lib/postgresql/18/data healthcheck: - test: ["CMD", "pg_isready", "-U", "default"] + test: [CMD, pg_isready, -U, default] interval: 10s timeout: 5s retries: 3 @@ -24,7 +24,7 @@ services: # REST Services # ================================ api-main: - container_name: "api-main" + container_name: api-main build: ./packages/api-main image: ditherchat/api-main restart: always @@ -32,12 +32,12 @@ services: postgres: condition: service_healthy environment: - PG_URI: "postgresql://default:password@postgres:5432/postgres" - AUTH: "dev" + PG_URI: 'postgresql://default:password@postgres:5432/postgres' + AUTH: dev ports: - 3000:3000 healthcheck: - test: "curl http://localhost:3000/v1/health || exit 1" + test: 'curl http://localhost:3000/v1/health || exit 1' interval: 5s timeout: 3s retries: 30 @@ -47,24 +47,24 @@ services: # ChronoSync Service # ================================ reader-main: - container_name: "reader-main" + container_name: reader-main build: ./packages/reader-main image: ditherchat/reader-main restart: always - command: ["pnpm", "start"] + command: [pnpm, start] depends_on: postgres: condition: service_healthy api-main: condition: service_healthy environment: - API_URLS: "https://atomone-testnet-1-api.allinbits.services" - START_BLOCK: "1979480" + API_URLS: 'https://atomone-testnet-1-api.allinbits.services' + START_BLOCK: '1979480' BATCH_SIZE: 500 - MEMO_PREFIX: "dither." - RECEIVER: "atone1uq6zjslvsa29cy6uu75y8txnl52mw06j6fzlep" - API_ROOT: "http://api-main:3000/v1" - AUTH: "dev" + MEMO_PREFIX: dither. + RECEIVER: atone1uq6zjslvsa29cy6uu75y8txnl52mw06j6fzlep + API_ROOT: 'http://api-main:3000/v1' + AUTH: dev # Uncomment to enable fast sync # ECLESIA_GRAPHQL_ENDPOINT: "https://graphql-atomone-testnet-1.allinbits.services/v1/graphql" # ECLESIA_GRAPHQL_SECRET: "" @@ -84,4 +84,3 @@ services: # depends_on: # - postgres # - api-main - diff --git a/eslint.config.mjs b/eslint.config.mjs index 7630157f..70343ab9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,123 +1,43 @@ -import { builtinModules } from 'module'; +import antfu from '@antfu/eslint-config'; -import jsLint from '@eslint/js'; -import stylistic from '@stylistic/eslint-plugin'; -import pluginSimpleImportSort from 'eslint-plugin-simple-import-sort'; -import vueLint from 'eslint-plugin-vue'; -import globals from 'globals'; -import tsLint from 'typescript-eslint'; - -export default [ - // config parsers - { - files: ['**/*.{js,mjs,cjs,ts,mts,jsx,tsx}'], - languageOptions: { - parser: tsLint.parser, - parserOptions: { - sourceType: 'module', - }, - }, - }, - { - files: ['*.vue', '**/*.vue'], - languageOptions: { - parser: vueLint.parser, - parserOptions: { - parser: '@typescript-eslint/parser', - sourceType: 'module', - }, - }, - }, - // config envs - { - languageOptions: { - globals: { ...globals.browser, ...globals.node }, - }, - }, - // rules - jsLint.configs.recommended, - ...tsLint.configs.recommended, - ...vueLint.configs['flat/essential'], - { - rules: { - '@typescript-eslint/consistent-type-imports': [ - 'error', - { - prefer: 'type-imports', - fixStyle: 'separate-type-imports', - }, - ], - '@typescript-eslint/no-explicit-any': [ - 'warn', - { ignoreRestArgs: true }, - ], - '@typescript-eslint/no-unused-vars': [ - 'error', - { - args: 'all', - argsIgnorePattern: '^_', - caughtErrors: 'all', - caughtErrorsIgnorePattern: '^_', - destructuredArrayIgnorePattern: '^_', - varsIgnorePattern: '^_', - }, - ], - }, - }, - { - plugins: { - 'vue': vueLint, - '@typescript-eslint': tsLint.plugin, - 'simple-import-sort': pluginSimpleImportSort, - }, - rules: { - 'simple-import-sort/imports': [ - 'error', - { - groups: [ - [ - `node:`, - `^(${builtinModules.join('|')})(/|$)`, - ], - // style less,scss,css - ['^.+\\.less$', '^.+\\.s?css$'], - // Side effect imports. - ['^\\u0000'], - ['^@?\\w.*\\u0000$', '^[^.].*\\u0000$', '^\\..*\\u0000$'], - ['^vue', '^@vue', '^@?\\w', '^\\u0000'], - ['^@/utils'], - ['^@/composables'], - // Parent imports. Put `..` last. - ['^\\.\\.(?!/?$)', '^\\.\\./?$'], - // Other relative imports. Put same-folder imports and `.` last. - ['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'], - ], - }, - ], - // see more vue rules: https://eslint.vuejs.org/rules/ - 'vue/no-mutating-props': 'error', - 'vue/multi-word-component-names': 'off', - 'vue/html-indent': ['error', 2], - }, - }, - // see: https://eslint.style/guide/getting-started - // see: https://github.com/eslint-stylistic/eslint-stylistic/blob/main/packages/eslint-plugin/configs/disable-legacy.ts - stylistic.configs['disable-legacy'], - stylistic.configs.customize({ - indent: 4, - quotes: 'single', - semi: true, - jsx: true, - }), - { - // https://eslint.org/docs/latest/use/configure/ignore - ignores: [ - 'node_modules', - '**/node_modules', - '**/*.conf', - '**/nginx/**', - '**/data/postgres_data', - '**/dist/**', - ], +export default antfu({ + typescript: true, + vue: true, + jsonc: true, + yaml: true, + markdown: true, + formatters: true, + stylistic: { + indent: 2, + quotes: 'single', + semi: true, + jsx: true, + overrides: { + 'style/brace-style': ['error', '1tbs'], }, -]; + }, + ignores: ['docs/**'], +}, { + files: ['tsconfig.json', 'package.json'], + rules: { 'jsonc/sort-keys': 'off' }, +}).overrideRules({ + 'no-console': 'off', + 'antfu/if-newline': 'off', + 'perfectionist/sort-imports': ['error', { + groups: [ + 'type', + ['parent-type', 'sibling-type', 'index-type', 'internal-type'], + 'builtin', + 'external', + 'internal', + ['parent', 'sibling', 'index'], + 'side-effect', + 'object', + 'unknown', + ], + newlinesBetween: 'always', + order: 'asc', + type: 'natural', + internalPattern: ['^~/.+', '^@/.+'], + }], +}); diff --git a/netlify.toml b/netlify.toml index c06fe038..78868309 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,7 +1,8 @@ [build] publish = "./packages/frontend-main/dist" command = "pnpm -F ./packages/frontend-main build" + [[redirects]] from = "/*" to = "/index.html" -status = 200 \ No newline at end of file +status = 200 diff --git a/package.json b/package.json index 4d823ba5..6cd2a4c0 100644 --- a/package.json +++ b/package.json @@ -7,26 +7,28 @@ "author": "", "license": "ISC", "scripts": { - "lint": "eslint . --ext .js,.ts,.jsx,.tsx,.vue", - "lint:fix": "eslint . --ext .js,.ts,.jsx,.tsx,.vue --fix", + "lint": "eslint", + "lint:fix": "eslint --fix", "docs:build": "retype build ./docs", - "docs:start": "retype start ./docs" + "docs:start": "retype start ./docs", + "prepare": "husky" + }, + "dependencies": { + "retypeapp": "^3.11.0" }, "devDependencies": { - "@eslint/js": "^9.37.0", - "@stylistic/eslint-plugin": "^4.4.1", - "@typescript-eslint/eslint-plugin": "^8.46.1", + "@antfu/eslint-config": "^6.0.0", + "@types/node": "^22.18.10", "eslint": "^9.37.0", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-prettier": "^5.5.4", - "eslint-plugin-simple-import-sort": "^12.1.1", - "eslint-plugin-vue": "^10.5.0", - "globals": "^16.4.0", - "prettier": "^3.6.2", - "typescript-eslint": "^8.46.1" + "eslint-plugin-format": "^1.0.2", + "husky": "^9.1.7", + "lint-staged": "^16.2.6", + "tailwindcss": "^4.1.14", + "typescript": "^5.9.3" }, - "packageManager": "pnpm@10.11.1+sha512.e519b9f7639869dc8d5c3c5dfef73b3f091094b0a006d7317353c72b124e80e1afd429732e28705ad6bfa1ee879c1fce46c128ccebd3192101f43dd67c667912", - "dependencies": { - "retypeapp": "^3.11.0" - } + "lint-staged": { + "*.{js,jsx,ts,tsx,mjs,cjs,vue}": "eslint --fix", + "*.{json,yml,yaml,md}": "eslint --fix" + }, + "packageManager": "pnpm@10.11.1+sha512.e519b9f7639869dc8d5c3c5dfef73b3f091094b0a006d7317353c72b124e80e1afd429732e28705ad6bfa1ee879c1fce46c128ccebd3192101f43dd67c667912" } diff --git a/packages/api-main/README.md b/packages/api-main/README.md index f17c23a3..921f3e77 100644 --- a/packages/api-main/README.md +++ b/packages/api-main/README.md @@ -4,9 +4,9 @@ This uses elysia to serve data from the relevant database. ## Tech Stack -- [REST API - ElysiaJS](https://elysiajs.com) -- [Database - PostgreSQL](https://www.postgresql.org) -- [SQL ORM - Drizzle](https://orm.drizzle.team) +- [REST API - ElysiaJS](https://elysiajs.com) +- [Database - PostgreSQL](https://www.postgresql.org) +- [SQL ORM - Drizzle](https://orm.drizzle.team) ## Usage @@ -35,7 +35,7 @@ All data for postgres is stored in the `data` directory. ## Testing -> [!Caution] +> [!Caution] > When running tests all tables will be cleaned out for a clean test environment. You've been warned. Start the PostgreSQL Database diff --git a/packages/api-main/docker-compose-github.yml b/packages/api-main/docker-compose-github.yml index 86fbc915..dbfb64d6 100644 --- a/packages/api-main/docker-compose-github.yml +++ b/packages/api-main/docker-compose-github.yml @@ -8,6 +8,6 @@ services: POSTGRES_USER: default POSTGRES_PASSWORD: password ports: - - "5432:5432" + - '5432:5432' volumes: - ./data/postgres_data:/var/lib/postgresql/18/docker diff --git a/packages/api-main/docker-compose.yml b/packages/api-main/docker-compose.yml index 59818684..14b631ad 100644 --- a/packages/api-main/docker-compose.yml +++ b/packages/api-main/docker-compose.yml @@ -1,32 +1,32 @@ services: - postgres: - # network_mode: host - image: postgres:18.0-alpine3.22 - container_name: postgres_db - restart: always - environment: - POSTGRES_USER: default - POSTGRES_PASSWORD: password - ports: - - '5432:5432' - volumes: - - ./data/postgres_data:/var/lib/postgresql/18/docker - # command: - # [ - # '-c', - # 'shared_buffers=4GB', - # '-c', - # 'work_mem=128MB', - # '-c', - # 'maintenance_work_mem=2GB', - # '-c', - # 'effective_cache_size=12GB', - # '-c', - # 'max_connections=150', - # '-c', - # 'max_worker_processes=8', - # '-c', - # 'max_parallel_workers_per_gather=4', - # '-c', - # 'max_parallel_workers=8', - # ] \ No newline at end of file + postgres: + # network_mode: host + image: postgres:18.0-alpine3.22 + container_name: postgres_db + restart: always + environment: + POSTGRES_USER: default + POSTGRES_PASSWORD: password + ports: + - '5432:5432' + volumes: + - ./data/postgres_data:/var/lib/postgresql/18/docker + # command: + # [ + # '-c', + # 'shared_buffers=4GB', + # '-c', + # 'work_mem=128MB', + # '-c', + # 'maintenance_work_mem=2GB', + # '-c', + # 'effective_cache_size=12GB', + # '-c', + # 'max_connections=150', + # '-c', + # 'max_worker_processes=8', + # '-c', + # 'max_parallel_workers_per_gather=4', + # '-c', + # 'max_parallel_workers=8', + # ] diff --git a/packages/api-main/drizzle.config.ts b/packages/api-main/drizzle.config.ts index 04f9d965..9cc72bca 100644 --- a/packages/api-main/drizzle.config.ts +++ b/packages/api-main/drizzle.config.ts @@ -1,16 +1,18 @@ +import process from 'node:process'; + import dotenv from 'dotenv'; import { defineConfig } from 'drizzle-kit'; dotenv.config(); export default defineConfig({ - out: './drizzle/migrations', - schema: './drizzle/schema.ts', - dialect: 'postgresql', - verbose: true, - strict: true, - dbCredentials: { - url: process.env.PG_URI!, - }, - tablesFilter: ['!pg_*', '!information_schema.*'], // Exclude system tables + out: './drizzle/migrations', + schema: './drizzle/schema.ts', + dialect: 'postgresql', + verbose: true, + strict: true, + dbCredentials: { + url: process.env.PG_URI!, + }, + tablesFilter: ['!pg_*', '!information_schema.*'], // Exclude system tables }); diff --git a/packages/api-main/drizzle/db.ts b/packages/api-main/drizzle/db.ts index aa5af1da..b9684287 100644 --- a/packages/api-main/drizzle/db.ts +++ b/packages/api-main/drizzle/db.ts @@ -1,3 +1,5 @@ +import process from 'node:process'; + import dotenv from 'dotenv'; import { drizzle } from 'drizzle-orm/node-postgres'; import pg from 'pg'; @@ -9,15 +11,15 @@ const { Pool } = pg; let db: ReturnType; export function getDatabase() { - if (!db) { - const client = new Pool({ - connectionString: process.env.PG_URI!, - max: 150, - connectionTimeoutMillis: 0, - idleTimeoutMillis: 1000, - }); - db = drizzle(client); - } + if (!db) { + const client = new Pool({ + connectionString: process.env.PG_URI!, + max: 150, + connectionTimeoutMillis: 0, + idleTimeoutMillis: 1000, + }); + db = drizzle(client); + } - return db; + return db; } diff --git a/packages/api-main/drizzle/schema.ts b/packages/api-main/drizzle/schema.ts index 96355f33..80cde5ba 100644 --- a/packages/api-main/drizzle/schema.ts +++ b/packages/api-main/drizzle/schema.ts @@ -4,166 +4,166 @@ import { bigint, boolean, index, integer, pgEnum, pgTable, primaryKey, serial, t const MEMO_LENGTH = 512; export const FeedTable = pgTable( - 'feed', - { - hash: varchar({ length: 64 }).primaryKey(), // Main hash from the transaction - post_hash: varchar({ length: 64 }), // Optional, this makes a post a reply, provided through memo - author: varchar({ length: 44 }).notNull(), // Address of user, usually in the transfer message - timestamp: timestamp({ withTimezone: true }).notNull(), // Timestamp parsed with new Date() - message: varchar({ length: MEMO_LENGTH }).notNull(), // The message inside of the memo - quantity: text().default('0'), // The total amount of tokens the user spent to post this message - replies: integer().default(0), // The amount of replies this post / reply has - likes: integer().default(0), // The amount of likes this post / reply has - dislikes: integer().default(0), // The amount of dislikes this post / reply has - flags: integer().default(0), // The amount of flags this post / reply has - likes_burnt: text().default('0'), // The amount of tokens burnt from each user who liked this post / reply - dislikes_burnt: text().default('0'), // The amount of tokens burnt from each user who disliked this post / reply - flags_burnt: text().default('0'), // The amount of tokens burnt from each user who wante dto flag this post / reply - removed_hash: varchar({ length: 64 }), // The hash that corresponds with the soft delete request - removed_at: timestamp({ withTimezone: true }), // When this post was removed - removed_by: varchar({ length: 44 }), // Who removed this post - }, - t => [ - index('feed_hash_index').on(t.hash), - index('post_hash_index').on(t.post_hash), - index('message_search_index').using('gin', sql`to_tsvector('english', ${t.message})`), - ], + 'feed', + { + hash: varchar({ length: 64 }).primaryKey(), // Main hash from the transaction + post_hash: varchar({ length: 64 }), // Optional, this makes a post a reply, provided through memo + author: varchar({ length: 44 }).notNull(), // Address of user, usually in the transfer message + timestamp: timestamp({ withTimezone: true }).notNull(), // Timestamp parsed with new Date() + message: varchar({ length: MEMO_LENGTH }).notNull(), // The message inside of the memo + quantity: text().default('0'), // The total amount of tokens the user spent to post this message + replies: integer().default(0), // The amount of replies this post / reply has + likes: integer().default(0), // The amount of likes this post / reply has + dislikes: integer().default(0), // The amount of dislikes this post / reply has + flags: integer().default(0), // The amount of flags this post / reply has + likes_burnt: text().default('0'), // The amount of tokens burnt from each user who liked this post / reply + dislikes_burnt: text().default('0'), // The amount of tokens burnt from each user who disliked this post / reply + flags_burnt: text().default('0'), // The amount of tokens burnt from each user who wante dto flag this post / reply + removed_hash: varchar({ length: 64 }), // The hash that corresponds with the soft delete request + removed_at: timestamp({ withTimezone: true }), // When this post was removed + removed_by: varchar({ length: 44 }), // Who removed this post + }, + t => [ + index('feed_hash_index').on(t.hash), + index('post_hash_index').on(t.post_hash), + index('message_search_index').using('gin', sql`to_tsvector('english', ${t.message})`), + ], ); export const DislikesTable = pgTable( - 'dislikes', - { - hash: varchar({ length: 64 }).primaryKey(), - post_hash: varchar({ length: 64 }).notNull(), - author: varchar({ length: 44 }).notNull(), - quantity: text().default('0'), - timestamp: timestamp({ withTimezone: true }).notNull(), - }, - t => [ - index('dislike_post_hash_idx').on(t.post_hash), - index('dislike_author_idx').on(t.author), - ], + 'dislikes', + { + hash: varchar({ length: 64 }).primaryKey(), + post_hash: varchar({ length: 64 }).notNull(), + author: varchar({ length: 44 }).notNull(), + quantity: text().default('0'), + timestamp: timestamp({ withTimezone: true }).notNull(), + }, + t => [ + index('dislike_post_hash_idx').on(t.post_hash), + index('dislike_author_idx').on(t.author), + ], ); export const LikesTable = pgTable( - 'likes', - { - hash: varchar({ length: 64 }).primaryKey(), - post_hash: varchar({ length: 64 }).notNull(), - author: varchar({ length: 44 }).notNull(), - quantity: text().default('0'), - timestamp: timestamp({ withTimezone: true }).notNull(), - }, - t => [ - index('like_post_hash_idx').on(t.post_hash), - index('like_author_idx').on(t.author), - ], + 'likes', + { + hash: varchar({ length: 64 }).primaryKey(), + post_hash: varchar({ length: 64 }).notNull(), + author: varchar({ length: 44 }).notNull(), + quantity: text().default('0'), + timestamp: timestamp({ withTimezone: true }).notNull(), + }, + t => [ + index('like_post_hash_idx').on(t.post_hash), + index('like_author_idx').on(t.author), + ], ); export const FlagsTable = pgTable( - 'flags', - { - hash: varchar({ length: 64 }).primaryKey(), - post_hash: varchar({ length: 64 }).notNull(), - author: varchar({ length: 44 }).notNull(), - quantity: text().default('0'), - timestamp: timestamp({ withTimezone: true }).notNull(), - }, - t => [ - index('flags_post_hash_idx').on(t.post_hash), - index('flag_author_idx').on(t.author), - ], + 'flags', + { + hash: varchar({ length: 64 }).primaryKey(), + post_hash: varchar({ length: 64 }).notNull(), + author: varchar({ length: 44 }).notNull(), + quantity: text().default('0'), + timestamp: timestamp({ withTimezone: true }).notNull(), + }, + t => [ + index('flags_post_hash_idx').on(t.post_hash), + index('flag_author_idx').on(t.author), + ], ); export const FollowsTable = pgTable( - 'follows', - { - follower: varchar({ length: 44 }).notNull(), - following: varchar({ length: 44 }).notNull(), - hash: varchar({ length: 64 }).notNull(), - timestamp: timestamp({ withTimezone: true }).notNull(), - removed_at: timestamp({ withTimezone: true }), - }, - t => [ - primaryKey({ columns: [t.follower, t.following] }), - index('follower_id_idx').on(t.follower), - index('following_id_idx').on(t.following), - ], + 'follows', + { + follower: varchar({ length: 44 }).notNull(), + following: varchar({ length: 44 }).notNull(), + hash: varchar({ length: 64 }).notNull(), + timestamp: timestamp({ withTimezone: true }).notNull(), + removed_at: timestamp({ withTimezone: true }), + }, + t => [ + primaryKey({ columns: [t.follower, t.following] }), + index('follower_id_idx').on(t.follower), + index('following_id_idx').on(t.following), + ], ); export const AuthRequests = pgTable( - 'authrequests', - { - id: serial('id').primaryKey(), - msg: varchar().notNull(), - timestamp: timestamp({ withTimezone: true }).notNull(), - }, + 'authrequests', + { + id: serial('id').primaryKey(), + msg: varchar().notNull(), + timestamp: timestamp({ withTimezone: true }).notNull(), + }, ); export const rateLimits = pgTable( - 'ratelimits', - { - id: serial('id').primaryKey(), - ip: text().notNull().unique(), - requests: integer().notNull().default(0), - lastRequest: bigint({ mode: 'number' }).notNull(), - }, + 'ratelimits', + { + id: serial('id').primaryKey(), + ip: text().notNull().unique(), + requests: integer().notNull().default(0), + lastRequest: bigint({ mode: 'number' }).notNull(), + }, ); // Audits are append only export const AuditTable = pgTable('audits', { - id: serial('id').primaryKey(), - hash: varchar({ length: 64 }).notNull(), - post_hash: varchar({ length: 64 }), // This is a post removal - user_address: varchar({ length: 44 }), // This is a user removal - created_by: varchar({ length: 44 }), - created_at: timestamp({ withTimezone: true }), - reason: text(), - restored_by: varchar({ length: 44 }), // the post or user that was restored - restored_at: timestamp({ withTimezone: true }), // the time the post or user was restored + id: serial('id').primaryKey(), + hash: varchar({ length: 64 }).notNull(), + post_hash: varchar({ length: 64 }), // This is a post removal + user_address: varchar({ length: 44 }), // This is a user removal + created_by: varchar({ length: 44 }), + created_at: timestamp({ withTimezone: true }), + reason: text(), + restored_by: varchar({ length: 44 }), // the post or user that was restored + restored_at: timestamp({ withTimezone: true }), // the time the post or user was restored }); export const ModeratorTable = pgTable('moderators', { - address: varchar({ length: 44 }).primaryKey(), - alias: varchar({ length: 16 }), // Optional short name - deleted_at: timestamp({ withTimezone: true }), + address: varchar({ length: 44 }).primaryKey(), + alias: varchar({ length: 16 }), // Optional short name + deleted_at: timestamp({ withTimezone: true }), }); export const notificationTypeEnum = pgEnum('notification_type', ['like', 'dislike', 'flag', 'follow', 'reply']); export const NotificationTable = pgTable( - 'notifications', - { - hash: varchar({ length: 64 }).notNull(), - post_hash: varchar({ length: 64 }), - owner: varchar({ length: 44 }).notNull(), - actor: varchar({ length: 44 }).notNull(), - type: notificationTypeEnum().notNull(), - subcontext: varchar({ length: 64 }), - timestamp: timestamp({ withTimezone: true }), - was_read: boolean().default(false), - }, - t => [ - primaryKey({ columns: [t.hash, t.owner] }), - index('notification_owner_idx').on(t.owner), - ], + 'notifications', + { + hash: varchar({ length: 64 }).notNull(), + post_hash: varchar({ length: 64 }), + owner: varchar({ length: 44 }).notNull(), + actor: varchar({ length: 44 }).notNull(), + type: notificationTypeEnum().notNull(), + subcontext: varchar({ length: 64 }), + timestamp: timestamp({ withTimezone: true }), + was_read: boolean().default(false), + }, + t => [ + primaryKey({ columns: [t.hash, t.owner] }), + index('notification_owner_idx').on(t.owner), + ], ); export const ReaderState = pgTable('state', { - id: serial().primaryKey(), - last_block: varchar().notNull(), + id: serial().primaryKey(), + last_block: varchar().notNull(), }); export const tables = [ - 'feed', - 'likes', - 'dislikes', - 'flags', - 'follows', - 'audits', - 'moderators', - 'notifications', - 'state', - 'authrequests', - 'ratelimits', + 'feed', + 'likes', + 'dislikes', + 'flags', + 'follows', + 'audits', + 'moderators', + 'notifications', + 'state', + 'authrequests', + 'ratelimits', ]; diff --git a/packages/api-main/fly.mainnet.toml b/packages/api-main/fly.mainnet.toml index 971f2246..d47d1101 100644 --- a/packages/api-main/fly.mainnet.toml +++ b/packages/api-main/fly.mainnet.toml @@ -8,7 +8,7 @@ internal_port = 3000 force_https = true auto_start_machines = true min_machines_running = 1 -processes = ['app'] +processes = [ 'app' ] sticky_machines = true [http_service.http_options] diff --git a/packages/api-main/fly.testnet.toml b/packages/api-main/fly.testnet.toml index c74f291a..aac01937 100644 --- a/packages/api-main/fly.testnet.toml +++ b/packages/api-main/fly.testnet.toml @@ -2,12 +2,13 @@ app = 'dither-testnet-api-main' primary_region = 'iad' [build] + [http_service] internal_port = 3000 force_https = true auto_start_machines = true min_machines_running = 1 -processes = ['app'] +processes = [ 'app' ] sticky_machines = true [http_service.http_options] diff --git a/packages/api-main/package.json b/packages/api-main/package.json index 6e1ba6ae..51a01cd2 100644 --- a/packages/api-main/package.json +++ b/packages/api-main/package.json @@ -1,24 +1,25 @@ { "name": "api-feed", + "type": "module", "version": "1.0.0", "description": "", "author": "stuyk", "main": "dist/index.js", - "type": "module", "scripts": { "start": "vite-node ./src/index.ts", - "build": "tsc", + "build": "pnpm build:deps && tsc", + "build:deps": "pnpm --filter {.}^... build", "test": "vitest --exclude tests/smoke", "test:smoke": "vitest run tests/smoke", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:push": "drizzle-kit push", "db:push:force": "drizzle-kit push --force", - "push-and-start": "drizzle-kit push --force && pnpm start" + "push-and-start": "drizzle-kit push --force && build:deps && pnpm start" }, "dependencies": { "@atomone/chronostate": "^2.3.0", - "@atomone/dither-api-types": "^1.8.0", + "@atomone/dither-api-types": "workspace:*", "@cosmjs/crypto": "^0.33.1", "@cosmjs/encoding": "^0.33.1", "@elysiajs/cors": "~1.2.0", @@ -37,17 +38,10 @@ "devDependencies": { "@cosmjs/amino": "^0.33.1", "@types/jsonwebtoken": "^9.0.10", - "@types/node": "^22.18.10", "@types/pg": "^8.15.5", "drizzle-kit": "^0.31.5", "tsx": "^4.20.6", "vite-node": "^3.2.4", "vitest": "^3.2.4" - }, - "prettier": { - "tabWidth": 4, - "singleQuote": true, - "semi": true, - "printWidth": 120 } } diff --git a/packages/api-main/src/config.ts b/packages/api-main/src/config.ts index 9f3ee247..a1c91cd7 100644 --- a/packages/api-main/src/config.ts +++ b/packages/api-main/src/config.ts @@ -1,4 +1,5 @@ import crypto from 'node:crypto'; +import process from 'node:process'; import dotenv from 'dotenv'; @@ -6,54 +7,54 @@ dotenv.config(); type JWT_STRICTNESS = boolean | 'lax' | 'strict' | 'none' | undefined; -type Config = { - PORT: number; - PG_URI: string; - AUTH: string; - JWT: string; - JWT_STRICTNESS: JWT_STRICTNESS; - DISCORD_WEBHOOK_URL: string; -}; +interface Config { + PORT: number; + PG_URI: string; + AUTH: string; + JWT: string; + JWT_STRICTNESS: JWT_STRICTNESS; + DISCORD_WEBHOOK_URL: string; +} let config: Config; export function useConfig(): Config { - if (typeof config !== 'undefined') { - return config; - } - - if (typeof process.env.PG_URI === 'undefined') { - console.error(`Failed to specify PG_URI, no database uri provided`); - process.exit(1); - } - - if (!process.env.AUTH || process.env.AUTH === 'default') { - throw new Error(`AUTH must be set to a strong secret`); - } - - if (!process.env.JWT) { - console.log(`JWT was not set, defaulting to a randomized byte hex string.`); - process.env.JWT = crypto.randomBytes(128).toString('hex'); - } - - if (typeof process.env.JWT_STRICTNESS === 'undefined') { - console.warn(`JWT_STRICTNESS not set, defaulting to lax`); - process.env.JWT_STRICTNESS = 'lax'; - } - - if (typeof process.env.DISCORD_WEBHOOK_URL === 'undefined') { - console.warn(`DISCORD_WEBHOOK_URL not set, defaulting to empty`); - process.env.DISCORD_WEBHOOK_URL = ''; - } - - config = { - PORT: process.env.PORT ? parseInt(process.env.PORT) : 3000, - PG_URI: process.env.PG_URI, - AUTH: process.env.AUTH ?? 'default', - JWT: process.env.JWT ?? 'default-secret-key', - JWT_STRICTNESS: process.env.JWT_STRICTNESS as JWT_STRICTNESS, - DISCORD_WEBHOOK_URL: process.env.DISCORD_WEBHOOK_URL, - }; - + if (typeof config !== 'undefined') { return config; + } + + if (typeof process.env.PG_URI === 'undefined') { + console.error(`Failed to specify PG_URI, no database uri provided`); + process.exit(1); + } + + if (!process.env.AUTH || process.env.AUTH === 'default') { + throw new Error(`AUTH must be set to a strong secret`); + } + + if (!process.env.JWT) { + console.log(`JWT was not set, defaulting to a randomized byte hex string.`); + process.env.JWT = crypto.randomBytes(128).toString('hex'); + } + + if (typeof process.env.JWT_STRICTNESS === 'undefined') { + console.warn(`JWT_STRICTNESS not set, defaulting to lax`); + process.env.JWT_STRICTNESS = 'lax'; + } + + if (typeof process.env.DISCORD_WEBHOOK_URL === 'undefined') { + console.warn(`DISCORD_WEBHOOK_URL not set, defaulting to empty`); + process.env.DISCORD_WEBHOOK_URL = ''; + } + + config = { + PORT: process.env.PORT ? Number.parseInt(process.env.PORT) : 3000, + PG_URI: process.env.PG_URI, + AUTH: process.env.AUTH ?? 'default', + JWT: process.env.JWT ?? 'default-secret-key', + JWT_STRICTNESS: process.env.JWT_STRICTNESS as JWT_STRICTNESS, + DISCORD_WEBHOOK_URL: process.env.DISCORD_WEBHOOK_URL, + }; + + return config; } diff --git a/packages/api-main/src/gets/authVerify.ts b/packages/api-main/src/gets/authVerify.ts index a32a4302..919c5f80 100644 --- a/packages/api-main/src/gets/authVerify.ts +++ b/packages/api-main/src/gets/authVerify.ts @@ -3,16 +3,15 @@ import type { Cookie } from 'elysia'; import { verifyJWT } from '../shared/jwt'; export async function AuthVerify(auth: Cookie) { - try { - const response = await verifyJWT(auth.value); - if (typeof response === 'undefined') { - return { status: 401, error: 'Unauthorized token proivided' }; - } - - return typeof response === 'undefined' ? { status: 401, error: 'token expired' } : { status: 200 }; - } - catch (err) { - console.error(err); - return { status: 401, error: 'unauthorized signature or key provided, failed to verify' }; + try { + const response = await verifyJWT(auth.value); + if (typeof response === 'undefined') { + return { status: 401, error: 'Unauthorized token proivided' }; } + + return typeof response === 'undefined' ? { status: 401, error: 'token expired' } : { status: 200 }; + } catch (err) { + console.error(err); + return { status: 401, error: 'unauthorized signature or key provided, failed to verify' }; + } } diff --git a/packages/api-main/src/gets/dislikes.ts b/packages/api-main/src/gets/dislikes.ts index f569f3f1..a1e99b69 100644 --- a/packages/api-main/src/gets/dislikes.ts +++ b/packages/api-main/src/gets/dislikes.ts @@ -1,4 +1,5 @@ -import { type Gets } from '@atomone/dither-api-types'; +import type { Gets } from '@atomone/dither-api-types'; + import { desc, eq, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; @@ -6,47 +7,46 @@ import { DislikesTable } from '../../drizzle/schema'; import { getJsonbArrayCount } from '../utility'; const statement = getDatabase() - .select() - .from(DislikesTable) - .where(eq(DislikesTable.post_hash, sql.placeholder('post_hash'))) - .limit(sql.placeholder('limit')) - .offset(sql.placeholder('offset')) - .orderBy(desc(DislikesTable.timestamp)) - .prepare('stmnt_get_dislikes'); - -export async function Dislikes(query: typeof Gets.DislikesQuery.static) { - if (!query.hash) { - return { - status: 400, - error: 'malformed query, no hash provided', - }; - } - - let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; - const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; - - if (limit > 100) { - limit = 100; - } - - if (limit <= 0) { - return { status: 400, error: 'limit must be at least 1' }; + .select() + .from(DislikesTable) + .where(eq(DislikesTable.post_hash, sql.placeholder('post_hash'))) + .limit(sql.placeholder('limit')) + .offset(sql.placeholder('offset')) + .orderBy(desc(DislikesTable.timestamp)) + .prepare('stmnt_get_dislikes'); + +export async function Dislikes(query: Gets.DislikesQuery) { + if (!query.hash) { + return { + status: 400, + error: 'malformed query, no hash provided', + }; + } + + let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; + const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; + + if (limit > 100) { + limit = 100; + } + + if (limit <= 0) { + return { status: 400, error: 'limit must be at least 1' }; + } + + if (offset < 0) { + return { status: 400, error: 'offset must be at least 0' }; + } + + try { + if (query.count) { + return await getJsonbArrayCount(query.hash, DislikesTable._.name); } - if (offset < 0) { - return { status: 400, error: 'offset must be at least 0' }; - } - - try { - if (query.count) { - return await getJsonbArrayCount(query.hash, DislikesTable._.name); - } - - const results = await statement.execute({ post_hash: query.hash, limit, offset }); - return { status: 200, rows: results }; - } - catch (error) { - console.error(error); - return { error: 'failed to read data from database' }; - } + const results = await statement.execute({ post_hash: query.hash, limit, offset }); + return { status: 200, rows: results }; + } catch (error) { + console.error(error); + return { error: 'failed to read data from database' }; + } } diff --git a/packages/api-main/src/gets/feed.ts b/packages/api-main/src/gets/feed.ts index 8af19137..7b2eb575 100644 --- a/packages/api-main/src/gets/feed.ts +++ b/packages/api-main/src/gets/feed.ts @@ -1,57 +1,56 @@ -import { type Gets } from '@atomone/dither-api-types'; +import type { Gets } from '@atomone/dither-api-types'; + import { and, count, desc, gte, isNull, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { FeedTable } from '../../drizzle/schema'; const statement = getDatabase() - .select() - .from(FeedTable) - .limit(sql.placeholder('limit')) - .offset(sql.placeholder('offset')) - .where( - and( - isNull(FeedTable.removed_at), - isNull(FeedTable.post_hash), - gte(FeedTable.quantity, sql.placeholder('minQuantity')), - ), - ) - .orderBy(desc(FeedTable.timestamp)) - .prepare('stmnt_get_feed'); - -export async function Feed(query: typeof Gets.FeedQuery.static) { - if (query.count) { - try { - return await getDatabase().select({ count: count() }).from(FeedTable); - } - catch (err) { - console.error(err); - return { status: 400, error: 'failed to read data from database' }; - } - } - - let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; - const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; - const minQuantity = typeof query.minQuantity !== 'undefined' ? query.minQuantity : '0'; - - if (limit > 100) { - limit = 100; - } - - if (limit <= 0) { - return { status: 400, error: 'limit must be at least 1' }; - } - - if (offset < 0) { - return { status: 400, error: 'offset must be at least 0' }; - } - + .select() + .from(FeedTable) + .limit(sql.placeholder('limit')) + .offset(sql.placeholder('offset')) + .where( + and( + isNull(FeedTable.removed_at), + isNull(FeedTable.post_hash), + gte(FeedTable.quantity, sql.placeholder('minQuantity')), + ), + ) + .orderBy(desc(FeedTable.timestamp)) + .prepare('stmnt_get_feed'); + +export async function Feed(query: Gets.FeedQuery) { + if (query.count) { try { - const results = await statement.execute({ offset, limit, minQuantity }); - return { status: 200, rows: results }; - } - catch (error) { - console.error(error); - return { status: 400, error: 'failed to read data from database' }; + return await getDatabase().select({ count: count() }).from(FeedTable); + } catch (err) { + console.error(err); + return { status: 400, error: 'failed to read data from database' }; } + } + + let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; + const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; + const minQuantity = typeof query.minQuantity !== 'undefined' ? query.minQuantity : '0'; + + if (limit > 100) { + limit = 100; + } + + if (limit <= 0) { + return { status: 400, error: 'limit must be at least 1' }; + } + + if (offset < 0) { + return { status: 400, error: 'offset must be at least 0' }; + } + + try { + const results = await statement.execute({ offset, limit, minQuantity }); + return { status: 200, rows: results }; + } catch (error) { + console.error(error); + return { status: 400, error: 'failed to read data from database' }; + } } diff --git a/packages/api-main/src/gets/flags.ts b/packages/api-main/src/gets/flags.ts index b16e9f5e..d5cae7a0 100644 --- a/packages/api-main/src/gets/flags.ts +++ b/packages/api-main/src/gets/flags.ts @@ -1,4 +1,5 @@ -import { type Gets } from '@atomone/dither-api-types'; +import type { Gets } from '@atomone/dither-api-types'; + import { desc, eq, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; @@ -6,47 +7,46 @@ import { FlagsTable } from '../../drizzle/schema'; import { getJsonbArrayCount } from '../utility'; const statement = getDatabase() - .select() - .from(FlagsTable) - .where(eq(FlagsTable.post_hash, sql.placeholder('post_hash'))) - .limit(sql.placeholder('limit')) - .offset(sql.placeholder('offset')) - .orderBy(desc(FlagsTable.timestamp)) - .prepare('stmnt_get_flags'); - -export async function Flags(query: typeof Gets.FlagsQuery.static) { - if (!query.hash) { - return { - status: 400, - error: 'malformed query, no hash provided', - }; - } - - let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; - const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; - - if (limit > 100) { - limit = 100; - } - - if (limit <= 0) { - return { status: 400, error: 'limit must be at least 1' }; + .select() + .from(FlagsTable) + .where(eq(FlagsTable.post_hash, sql.placeholder('post_hash'))) + .limit(sql.placeholder('limit')) + .offset(sql.placeholder('offset')) + .orderBy(desc(FlagsTable.timestamp)) + .prepare('stmnt_get_flags'); + +export async function Flags(query: Gets.FlagsQuery) { + if (!query.hash) { + return { + status: 400, + error: 'malformed query, no hash provided', + }; + } + + let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; + const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; + + if (limit > 100) { + limit = 100; + } + + if (limit <= 0) { + return { status: 400, error: 'limit must be at least 1' }; + } + + if (offset < 0) { + return { status: 400, error: 'offset must be at least 0' }; + } + + try { + if (query.count) { + return await getJsonbArrayCount(query.hash, FlagsTable._.name); } - if (offset < 0) { - return { status: 400, error: 'offset must be at least 0' }; - } - - try { - if (query.count) { - return await getJsonbArrayCount(query.hash, FlagsTable._.name); - } - - const results = await statement.execute({ post_hash: query.hash, limit, offset }); - return { status: 200, rows: results }; - } - catch (error) { - console.error(error); - return { error: 'failed to read data from database' }; - } + const results = await statement.execute({ post_hash: query.hash, limit, offset }); + return { status: 200, rows: results }; + } catch (error) { + console.error(error); + return { error: 'failed to read data from database' }; + } } diff --git a/packages/api-main/src/gets/followers.ts b/packages/api-main/src/gets/followers.ts index b57a5345..bf85b9ac 100644 --- a/packages/api-main/src/gets/followers.ts +++ b/packages/api-main/src/gets/followers.ts @@ -1,40 +1,40 @@ -import { type Gets } from '@atomone/dither-api-types'; +import type { Gets } from '@atomone/dither-api-types'; + import { and, desc, eq, isNull, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { FollowsTable } from '../../drizzle/schema'; const statementGetFollowers = getDatabase() - .select({ address: FollowsTable.follower, hash: FollowsTable.hash }) - .from(FollowsTable) - .where(and(eq(FollowsTable.following, sql.placeholder('following')), isNull(FollowsTable.removed_at))) - .limit(sql.placeholder('limit')) - .offset(sql.placeholder('offset')) - .orderBy(desc(FollowsTable.timestamp)) - .prepare('stmnt_get_followers'); - -export async function Followers(query: typeof Gets.FollowersQuery.static) { - let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; - const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; - - if (limit > 100) { - limit = 100; - } - - if (limit <= 0) { - return { status: 400, error: 'limit must be at least 1' }; - } - - if (offset < 0) { - return { status: 400, error: 'offset must be at least 0' }; - } - - try { - const results = await statementGetFollowers.execute({ limit, offset, following: query.address }); - return { status: 200, rows: results }; - } - catch (error) { - console.error(error); - return { status: 404, error: 'failed to find matching followers' }; - } + .select({ address: FollowsTable.follower, hash: FollowsTable.hash }) + .from(FollowsTable) + .where(and(eq(FollowsTable.following, sql.placeholder('following')), isNull(FollowsTable.removed_at))) + .limit(sql.placeholder('limit')) + .offset(sql.placeholder('offset')) + .orderBy(desc(FollowsTable.timestamp)) + .prepare('stmnt_get_followers'); + +export async function Followers(query: Gets.FollowersQuery) { + let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; + const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; + + if (limit > 100) { + limit = 100; + } + + if (limit <= 0) { + return { status: 400, error: 'limit must be at least 1' }; + } + + if (offset < 0) { + return { status: 400, error: 'offset must be at least 0' }; + } + + try { + const results = await statementGetFollowers.execute({ limit, offset, following: query.address }); + return { status: 200, rows: results }; + } catch (error) { + console.error(error); + return { status: 404, error: 'failed to find matching followers' }; + } } diff --git a/packages/api-main/src/gets/following.ts b/packages/api-main/src/gets/following.ts index a66d5abd..22d7da52 100644 --- a/packages/api-main/src/gets/following.ts +++ b/packages/api-main/src/gets/following.ts @@ -1,40 +1,40 @@ -import { type Gets } from '@atomone/dither-api-types'; +import type { Gets } from '@atomone/dither-api-types'; + import { and, desc, eq, isNull, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { FollowsTable } from '../../drizzle/schema'; const statementGetFollowing = getDatabase() - .select({ address: FollowsTable.following, hash: FollowsTable.hash }) - .from(FollowsTable) - .where(and(eq(FollowsTable.follower, sql.placeholder('follower')), isNull(FollowsTable.removed_at))) - .limit(sql.placeholder('limit')) - .offset(sql.placeholder('offset')) - .orderBy(desc(FollowsTable.timestamp)) - .prepare('stmnt_get_following'); - -export async function Following(query: typeof Gets.FollowingQuery.static) { - let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; - const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; - - if (limit > 100) { - limit = 100; - } - - if (limit <= 0) { - return { status: 400, error: 'limit must be at least 1' }; - } - - if (offset < 0) { - return { status: 400, error: 'offset must be at least 0' }; - } - - try { - const results = await statementGetFollowing.execute({ follower: query.address, limit, offset }); - return { status: 200, rows: results }; - } - catch (error) { - console.error(error); - return { status: 404, error: 'failed to find matching following' }; - } + .select({ address: FollowsTable.following, hash: FollowsTable.hash }) + .from(FollowsTable) + .where(and(eq(FollowsTable.follower, sql.placeholder('follower')), isNull(FollowsTable.removed_at))) + .limit(sql.placeholder('limit')) + .offset(sql.placeholder('offset')) + .orderBy(desc(FollowsTable.timestamp)) + .prepare('stmnt_get_following'); + +export async function Following(query: Gets.FollowingQuery) { + let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; + const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; + + if (limit > 100) { + limit = 100; + } + + if (limit <= 0) { + return { status: 400, error: 'limit must be at least 1' }; + } + + if (offset < 0) { + return { status: 400, error: 'offset must be at least 0' }; + } + + try { + const results = await statementGetFollowing.execute({ follower: query.address, limit, offset }); + return { status: 200, rows: results }; + } catch (error) { + console.error(error); + return { status: 404, error: 'failed to find matching following' }; + } } diff --git a/packages/api-main/src/gets/health.ts b/packages/api-main/src/gets/health.ts index 9ec136ee..1ac97ba1 100644 --- a/packages/api-main/src/gets/health.ts +++ b/packages/api-main/src/gets/health.ts @@ -1,3 +1,3 @@ export function health() { - return { status: 'ok' }; + return { status: 'ok' }; } diff --git a/packages/api-main/src/gets/index.ts b/packages/api-main/src/gets/index.ts index f29ef636..5425689a 100644 --- a/packages/api-main/src/gets/index.ts +++ b/packages/api-main/src/gets/index.ts @@ -5,6 +5,7 @@ export * from './flags'; export * from './followers'; export * from './following'; export * from './health'; +export * from './isFollowing'; export * from './lastBlock'; export * from './likes'; export * from './notifications'; @@ -13,4 +14,3 @@ export * from './post'; export * from './posts'; export * from './replies'; export * from './search'; -export * from './isFollowing'; diff --git a/packages/api-main/src/gets/isFollowing.ts b/packages/api-main/src/gets/isFollowing.ts index dd0ee8ed..151cbf4f 100644 --- a/packages/api-main/src/gets/isFollowing.ts +++ b/packages/api-main/src/gets/isFollowing.ts @@ -1,32 +1,32 @@ -import { type Gets } from '@atomone/dither-api-types'; +import type { Gets } from '@atomone/dither-api-types'; + import { and, eq, isNull, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { FollowsTable } from '../../drizzle/schema'; const statementIsFollowing = getDatabase() - .select() - .from(FollowsTable) - .where( - and( - eq(FollowsTable.following, sql.placeholder('following')), - eq(FollowsTable.follower, sql.placeholder('follower')), - isNull(FollowsTable.removed_at), - ), - ) - .limit(1) - .prepare('stmnt_is_following'); + .select() + .from(FollowsTable) + .where( + and( + eq(FollowsTable.following, sql.placeholder('following')), + eq(FollowsTable.follower, sql.placeholder('follower')), + isNull(FollowsTable.removed_at), + ), + ) + .limit(1) + .prepare('stmnt_is_following'); -export async function IsFollowing(query: typeof Gets.IsFollowingQuery.static) { - try { - const results = await statementIsFollowing.execute({ following: query.following, follower: query.follower }); - if (results.length <= 0) { - return { status: 404, error: 'user is not following' }; - } - return { status: 200, rows: results }; - } - catch (error) { - console.error(error); - return { status: 404, error: 'user is not following' }; +export async function IsFollowing(query: Gets.IsFollowingQuery) { + try { + const results = await statementIsFollowing.execute({ following: query.following, follower: query.follower }); + if (results.length <= 0) { + return { status: 404, error: 'user is not following' }; } + return { status: 200, rows: results }; + } catch (error) { + console.error(error); + return { status: 404, error: 'user is not following' }; + } } diff --git a/packages/api-main/src/gets/lastBlock.ts b/packages/api-main/src/gets/lastBlock.ts index 80520950..8ccbc1ba 100644 --- a/packages/api-main/src/gets/lastBlock.ts +++ b/packages/api-main/src/gets/lastBlock.ts @@ -4,23 +4,22 @@ import { getDatabase } from '../../drizzle/db'; import { ReaderState } from '../../drizzle/schema'; const statement = getDatabase() - .select() - .from(ReaderState) - .where(eq(ReaderState.id, 0)) - .limit(1) - .prepare('stmnt_get_state'); + .select() + .from(ReaderState) + .where(eq(ReaderState.id, 0)) + .limit(1) + .prepare('stmnt_get_state'); export async function LastBlock() { - try { - const [state] = await statement.execute(); - if (!state) { - return { status: 404, rows: [] }; - } - - return { status: 200, rows: [state] }; - } - catch (error) { - console.error(error); - return { error: 'failed to read data from database' }; + try { + const [state] = await statement.execute(); + if (!state) { + return { status: 404, rows: [] }; } + + return { status: 200, rows: [state] }; + } catch (error) { + console.error(error); + return { error: 'failed to read data from database' }; + } } diff --git a/packages/api-main/src/gets/likes.ts b/packages/api-main/src/gets/likes.ts index 846ac8fd..e90954ab 100644 --- a/packages/api-main/src/gets/likes.ts +++ b/packages/api-main/src/gets/likes.ts @@ -1,4 +1,5 @@ -import { type Gets } from '@atomone/dither-api-types'; +import type { Gets } from '@atomone/dither-api-types'; + import { desc, eq, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; @@ -6,47 +7,46 @@ import { LikesTable } from '../../drizzle/schema'; import { getJsonbArrayCount } from '../utility'; const statement = getDatabase() - .select() - .from(LikesTable) - .where(eq(LikesTable.post_hash, sql.placeholder('post_hash'))) - .limit(sql.placeholder('limit')) - .offset(sql.placeholder('offset')) - .orderBy(desc(LikesTable.timestamp)) - .prepare('stmnt_get_likes'); - -export async function Likes(query: typeof Gets.LikesQuery.static) { - if (!query.hash) { - return { - status: 400, - error: 'malformed query, no hash provided', - }; - } - - let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; - const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; - - if (limit > 100) { - limit = 100; - } - - if (limit <= 0) { - return { status: 400, error: 'limit must be at least 1' }; + .select() + .from(LikesTable) + .where(eq(LikesTable.post_hash, sql.placeholder('post_hash'))) + .limit(sql.placeholder('limit')) + .offset(sql.placeholder('offset')) + .orderBy(desc(LikesTable.timestamp)) + .prepare('stmnt_get_likes'); + +export async function Likes(query: Gets.LikesQuery) { + if (!query.hash) { + return { + status: 400, + error: 'malformed query, no hash provided', + }; + } + + let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; + const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; + + if (limit > 100) { + limit = 100; + } + + if (limit <= 0) { + return { status: 400, error: 'limit must be at least 1' }; + } + + if (offset < 0) { + return { status: 400, error: 'offset must be at least 0' }; + } + + try { + if (query.count) { + return await getJsonbArrayCount(query.hash, LikesTable._.name); } - if (offset < 0) { - return { status: 400, error: 'offset must be at least 0' }; - } - - try { - if (query.count) { - return await getJsonbArrayCount(query.hash, LikesTable._.name); - } - - const results = await statement.execute({ post_hash: query.hash, limit, offset }); - return { status: 200, rows: results }; - } - catch (error) { - console.error(error); - return { error: 'failed to read data from database' }; - } + const results = await statement.execute({ post_hash: query.hash, limit, offset }); + return { status: 200, rows: results }; + } catch (error) { + console.error(error); + return { error: 'failed to read data from database' }; + } } diff --git a/packages/api-main/src/gets/notifications.ts b/packages/api-main/src/gets/notifications.ts index 2ba1b3b1..fed7f149 100644 --- a/packages/api-main/src/gets/notifications.ts +++ b/packages/api-main/src/gets/notifications.ts @@ -1,6 +1,6 @@ +import type { Gets } from '@atomone/dither-api-types'; import type { Cookie } from 'elysia'; -import { type Gets } from '@atomone/dither-api-types'; import { and, eq, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; @@ -8,74 +8,72 @@ import { NotificationTable } from '../../drizzle/schema'; import { verifyJWT } from '../shared/jwt'; const getNotificationsStatement = getDatabase() - .select() - .from(NotificationTable) - .where(and(eq(NotificationTable.owner, sql.placeholder('owner')), eq(NotificationTable.was_read, false))) - .limit(sql.placeholder('limit')) - .offset(sql.placeholder('offset')) - .prepare('stmnt_get_notifications'); + .select() + .from(NotificationTable) + .where(and(eq(NotificationTable.owner, sql.placeholder('owner')), eq(NotificationTable.was_read, false))) + .limit(sql.placeholder('limit')) + .offset(sql.placeholder('offset')) + .prepare('stmnt_get_notifications'); -export async function Notifications(query: typeof Gets.NotificationsQuery.static, auth: Cookie) { - const response = await verifyJWT(auth.value); - if (typeof response === 'undefined') { - return { status: 401, error: 'Unauthorized token proivided' }; - } +export async function Notifications(query: Gets.NotificationsQuery, auth: Cookie) { + const response = await verifyJWT(auth.value); + if (typeof response === 'undefined') { + return { status: 401, error: 'Unauthorized token proivided' }; + } - let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; - const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; + let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; + const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; - if (limit > 100) { - limit = 100; - } + if (limit > 100) { + limit = 100; + } - if (limit <= 0) { - return { status: 400, error: 'limit must be at least 1' }; - } + if (limit <= 0) { + return { status: 400, error: 'limit must be at least 1' }; + } - if (offset < 0) { - return { status: 400, error: 'offset must be at least 0' }; - } + if (offset < 0) { + return { status: 400, error: 'offset must be at least 0' }; + } - try { - const results = await getNotificationsStatement.execute({ owner: response, limit, offset }); - return { status: 200, rows: results }; - } - catch (error) { - console.error(error); - return { status: 404, error: 'failed to find matching reply' }; - } + try { + const results = await getNotificationsStatement.execute({ owner: response, limit, offset }); + return { status: 200, rows: results }; + } catch (error) { + console.error(error); + return { status: 404, error: 'failed to find matching reply' }; + } } const statementReadNotification = getDatabase() - .update(NotificationTable) - .set({ - was_read: true, - }) - .where( - and(eq(NotificationTable.hash, sql.placeholder('hash')), eq(NotificationTable.owner, sql.placeholder('owner'))), - ) - .prepare('stmnt_read_notification'); + .update(NotificationTable) + .set({ + was_read: true, + }) + .where( + and(eq(NotificationTable.hash, sql.placeholder('hash')), eq(NotificationTable.owner, sql.placeholder('owner'))), + ) + .prepare('stmnt_read_notification'); -export async function ReadNotification(query: typeof Gets.ReadNotificationQuery.static, auth: Cookie) { - const response = await verifyJWT(auth.value); - if (typeof response === 'undefined') { - return { status: 401, error: 'Unauthorized token proivided' }; - } +export async function ReadNotification(query: Gets.ReadNotificationQuery, auth: Cookie) { + const response = await verifyJWT(auth.value); + if (typeof response === 'undefined') { + return { status: 401, error: 'Unauthorized token proivided' }; + } - try { - const [notification] = await getDatabase() - .select() - .from(NotificationTable) - .where(and(eq(NotificationTable.hash, query.hash), eq(NotificationTable.owner, response))) - .limit(1); - if (!notification) { - return { status: 404, error: 'notification not found' }; - } - const results = await statementReadNotification.execute({ owner: response, hash: query.hash }); - return { status: 200, rows: results }; - } - catch (error) { - console.error(error); - return { status: 404, error: 'failed to find matching reply' }; + try { + const [notification] = await getDatabase() + .select() + .from(NotificationTable) + .where(and(eq(NotificationTable.hash, query.hash), eq(NotificationTable.owner, response))) + .limit(1); + if (!notification) { + return { status: 404, error: 'notification not found' }; } + const results = await statementReadNotification.execute({ owner: response, hash: query.hash }); + return { status: 200, rows: results }; + } catch (error) { + console.error(error); + return { status: 404, error: 'failed to find matching reply' }; + } } diff --git a/packages/api-main/src/gets/notificationsCount.ts b/packages/api-main/src/gets/notificationsCount.ts index c7191588..683bd8ef 100644 --- a/packages/api-main/src/gets/notificationsCount.ts +++ b/packages/api-main/src/gets/notificationsCount.ts @@ -1,6 +1,6 @@ +import type { Gets } from '@atomone/dither-api-types'; import type { Cookie } from 'elysia'; -import { type Gets } from '@atomone/dither-api-types'; import { and, count, eq, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; @@ -8,28 +8,27 @@ import { NotificationTable } from '../../drizzle/schema'; import { verifyJWT } from '../shared/jwt'; const getNotificationsCountStatement = getDatabase() - .select({ count: count() }) - .from(NotificationTable) - .where( - and( - eq(NotificationTable.owner, sql.placeholder('owner')), - eq(NotificationTable.was_read, false), - ), - ) - .prepare('stmnt_get_notifications_count'); + .select({ count: count() }) + .from(NotificationTable) + .where( + and( + eq(NotificationTable.owner, sql.placeholder('owner')), + eq(NotificationTable.was_read, false), + ), + ) + .prepare('stmnt_get_notifications_count'); -export async function NotificationsCount(_query: typeof Gets.NotificationsCountQuery.static, auth: Cookie) { - const response = await verifyJWT(auth.value); - if (typeof response === 'undefined') { - return { status: 401, error: 'Unauthorized token proivided' }; - } +export async function NotificationsCount(_query: Gets.NotificationsCountQuery, auth: Cookie) { + const response = await verifyJWT(auth.value); + if (typeof response === 'undefined') { + return { status: 401, error: 'Unauthorized token proivided' }; + } - try { - const [result] = await getNotificationsCountStatement.execute({ owner: response }); - return { status: 200, count: result?.count ?? 0 }; - } - catch (error) { - console.error(error); - return { status: 500, error: 'failed to count notifications' }; - } + try { + const [result] = await getNotificationsCountStatement.execute({ owner: response }); + return { status: 200, count: result?.count ?? 0 }; + } catch (error) { + console.error(error); + return { status: 500, error: 'failed to count notifications' }; + } } diff --git a/packages/api-main/src/gets/post.ts b/packages/api-main/src/gets/post.ts index 3fda0cf2..08a2fad7 100644 --- a/packages/api-main/src/gets/post.ts +++ b/packages/api-main/src/gets/post.ts @@ -1,33 +1,33 @@ -import { type Gets } from '@atomone/dither-api-types'; +import type { Gets } from '@atomone/dither-api-types'; + import { and, eq, isNull, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { FeedTable } from '../../drizzle/schema'; const statementGetPost = getDatabase() - .select() - .from(FeedTable) - .where(and(isNull(FeedTable.removed_at), eq(FeedTable.hash, sql.placeholder('hash')))) - .prepare('stmnt_get_post'); + .select() + .from(FeedTable) + .where(and(isNull(FeedTable.removed_at), eq(FeedTable.hash, sql.placeholder('hash')))) + .prepare('stmnt_get_post'); const statementGetReply = getDatabase() - .select() - .from(FeedTable) - .where(and(isNull(FeedTable.removed_at), eq(FeedTable.hash, sql.placeholder('hash')), eq(FeedTable.post_hash, sql.placeholder('post_hash')))) - .prepare('stmnt_get_reply'); - -export async function Post(query: typeof Gets.PostQuery.static) { - try { - if (query.post_hash) { - const results = await statementGetReply.execute({ hash: query.hash, post_hash: query.post_hash }); - return results.length <= 0 ? { status: 404, rows: [] } : { status: 200, rows: results }; - } + .select() + .from(FeedTable) + .where(and(isNull(FeedTable.removed_at), eq(FeedTable.hash, sql.placeholder('hash')), eq(FeedTable.post_hash, sql.placeholder('post_hash')))) + .prepare('stmnt_get_reply'); - const results = await statementGetPost.execute({ hash: query.hash }); - return results.length <= 0 ? { status: 404, rows: [] } : { status: 200, rows: results }; - } - catch (error) { - console.error(error); - return { status: 400, error: 'failed to read data from database' }; +export async function Post(query: Gets.PostQuery) { + try { + if (query.post_hash) { + const results = await statementGetReply.execute({ hash: query.hash, post_hash: query.post_hash }); + return results.length <= 0 ? { status: 404, rows: [] } : { status: 200, rows: results }; } + + const results = await statementGetPost.execute({ hash: query.hash }); + return results.length <= 0 ? { status: 404, rows: [] } : { status: 200, rows: results }; + } catch (error) { + console.error(error); + return { status: 400, error: 'failed to read data from database' }; + } } diff --git a/packages/api-main/src/gets/posts.ts b/packages/api-main/src/gets/posts.ts index 3e7dec9b..9cc1d0da 100644 --- a/packages/api-main/src/gets/posts.ts +++ b/packages/api-main/src/gets/posts.ts @@ -1,92 +1,89 @@ -import { type Gets } from '@atomone/dither-api-types'; +import type { Gets } from '@atomone/dither-api-types'; + import { and, desc, eq, gte, inArray, isNull, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { FeedTable, FollowsTable } from '../../drizzle/schema'; const statement = getDatabase() - .select() - .from(FeedTable) - .where(and( - eq(FeedTable.author, sql.placeholder('author')), - isNull(FeedTable.removed_at), - isNull(FeedTable.post_hash), // Do not return replies - gte(FeedTable.quantity, sql.placeholder('minQuantity')), - )) - .limit(sql.placeholder('limit')) - .offset(sql.placeholder('offset')) - .orderBy(desc(FeedTable.timestamp)) - .prepare('stmnt_get_posts'); + .select() + .from(FeedTable) + .where(and( + eq(FeedTable.author, sql.placeholder('author')), + isNull(FeedTable.removed_at), + isNull(FeedTable.post_hash), // Do not return replies + gte(FeedTable.quantity, sql.placeholder('minQuantity')), + )) + .limit(sql.placeholder('limit')) + .offset(sql.placeholder('offset')) + .orderBy(desc(FeedTable.timestamp)) + .prepare('stmnt_get_posts'); -export async function Posts(query: typeof Gets.PostsQuery.static) { - let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; - const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; - const minQuantity = typeof query.minQuantity !== 'undefined' ? query.minQuantity : '0'; +export async function Posts(query: Gets.PostsQuery) { + let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; + const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; + const minQuantity = typeof query.minQuantity !== 'undefined' ? query.minQuantity : '0'; - if (limit > 100) { - limit = 100; - } + if (limit > 100) { + limit = 100; + } - if (limit <= 0) { - return { status: 400, error: 'limit must be at least 1' }; - } + if (limit <= 0) { + return { status: 400, error: 'limit must be at least 1' }; + } - if (offset < 0) { - return { status: 400, error: 'offset must be at least 0' }; - } + if (offset < 0) { + return { status: 400, error: 'offset must be at least 0' }; + } - try { - const results = await statement.execute({ author: query.address, limit, offset, minQuantity }); - return { status: 200, rows: results }; - } - catch (error) { - console.error(error); - return { status: 404, error: 'failed to find matching reply' }; - } + try { + const results = await statement.execute({ author: query.address, limit, offset, minQuantity }); + return { status: 200, rows: results }; + } catch (error) { + console.error(error); + return { status: 404, error: 'failed to find matching reply' }; + } } const followingPostsStatement = getDatabase() - .select() - .from(FeedTable) - .where(and( - inArray(FeedTable.author, - getDatabase() - .select({ following: FollowsTable.following }) - .from(FollowsTable) - .where(and(eq(FollowsTable.follower, sql.placeholder('address')), isNull(FollowsTable.removed_at))), - ), - isNull(FeedTable.post_hash), - isNull(FeedTable.removed_at), - gte(FeedTable.quantity, sql.placeholder('minQuantity')), - )) - .orderBy(desc(FeedTable.timestamp)) - .limit(sql.placeholder('limit')) - .offset(sql.placeholder('offset')) - .prepare('stmnt_posts_from_following'); + .select() + .from(FeedTable) + .where(and( + inArray(FeedTable.author, getDatabase() + .select({ following: FollowsTable.following }) + .from(FollowsTable) + .where(and(eq(FollowsTable.follower, sql.placeholder('address')), isNull(FollowsTable.removed_at)))), + isNull(FeedTable.post_hash), + isNull(FeedTable.removed_at), + gte(FeedTable.quantity, sql.placeholder('minQuantity')), + )) + .orderBy(desc(FeedTable.timestamp)) + .limit(sql.placeholder('limit')) + .offset(sql.placeholder('offset')) + .prepare('stmnt_posts_from_following'); -export async function FollowingPosts(query: typeof Gets.PostsQuery.static) { - let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; - const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; - const minQuantity = typeof query.minQuantity !== 'undefined' ? query.minQuantity : '0'; +export async function FollowingPosts(query: Gets.PostsQuery) { + let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; + const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; + const minQuantity = typeof query.minQuantity !== 'undefined' ? query.minQuantity : '0'; - if (limit > 100) { - limit = 100; - } + if (limit > 100) { + limit = 100; + } - if (limit <= 0) { - return { status: 400, error: 'limit must be at least 1' }; - } + if (limit <= 0) { + return { status: 400, error: 'limit must be at least 1' }; + } - if (offset < 0) { - return { status: 400, error: 'offset must be at least 0' }; - } + if (offset < 0) { + return { status: 400, error: 'offset must be at least 0' }; + } - try { - const results = await followingPostsStatement.execute({ address: query.address, limit, offset, minQuantity }); - return { status: 200, rows: results }; - } - catch (error) { - console.error(error); - return { status: 404, error: 'failed to posts from followed users' }; - } + try { + const results = await followingPostsStatement.execute({ address: query.address, limit, offset, minQuantity }); + return { status: 200, rows: results }; + } catch (error) { + console.error(error); + return { status: 404, error: 'failed to posts from followed users' }; + } } diff --git a/packages/api-main/src/gets/replies.ts b/packages/api-main/src/gets/replies.ts index 03b67f86..58df990c 100644 --- a/packages/api-main/src/gets/replies.ts +++ b/packages/api-main/src/gets/replies.ts @@ -1,4 +1,5 @@ -import { type Gets } from '@atomone/dither-api-types'; +import type { Gets } from '@atomone/dither-api-types'; + import { and, desc, eq, gte, isNotNull, isNull, sql } from 'drizzle-orm'; import { alias } from 'drizzle-orm/pg-core'; @@ -6,88 +7,86 @@ import { getDatabase } from '../../drizzle/db'; import { FeedTable } from '../../drizzle/schema'; const statement = getDatabase() - .select() - .from(FeedTable) - .where(and( - eq(FeedTable.post_hash, sql.placeholder('hash')), - isNull(FeedTable.removed_at), - gte(FeedTable.quantity, sql.placeholder('minQuantity')), - )) - .limit(sql.placeholder('limit')) - .offset(sql.placeholder('offset')) - .orderBy(desc(FeedTable.timestamp)) - .prepare('stmnt_get_replies'); + .select() + .from(FeedTable) + .where(and( + eq(FeedTable.post_hash, sql.placeholder('hash')), + isNull(FeedTable.removed_at), + gte(FeedTable.quantity, sql.placeholder('minQuantity')), + )) + .limit(sql.placeholder('limit')) + .offset(sql.placeholder('offset')) + .orderBy(desc(FeedTable.timestamp)) + .prepare('stmnt_get_replies'); -export async function Replies(query: typeof Gets.RepliesQuery.static) { - let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; - const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; - const minQuantity = typeof query.minQuantity !== 'undefined' ? query.minQuantity : '0'; +export async function Replies(query: Gets.RepliesQuery) { + let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; + const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; + const minQuantity = typeof query.minQuantity !== 'undefined' ? query.minQuantity : '0'; - if (limit > 100) { - limit = 100; - } + if (limit > 100) { + limit = 100; + } - if (limit <= 0) { - return { status: 400, error: 'limit must be at least 1' }; - } + if (limit <= 0) { + return { status: 400, error: 'limit must be at least 1' }; + } - if (offset < 0) { - return { status: 400, error: 'offset must be at least 0' }; - } + if (offset < 0) { + return { status: 400, error: 'offset must be at least 0' }; + } - try { - const results = await statement.execute({ hash: query.hash, limit, offset, minQuantity }); - return { status: 200, rows: results }; - } - catch (error) { - console.error(error); - return { status: 404, error: 'failed to find matching reply' }; - } + try { + const results = await statement.execute({ hash: query.hash, limit, offset, minQuantity }); + return { status: 200, rows: results }; + } catch (error) { + console.error(error); + return { status: 404, error: 'failed to find matching reply' }; + } } const feed = FeedTable; const parentFeed = alias(FeedTable, 'parent'); const getUserRepliesWithParent = getDatabase() - .select({ - reply: feed, - parent: parentFeed, - }) - .from(feed) - .innerJoin(parentFeed, eq(feed.post_hash, parentFeed.hash)) - .where(and( - eq(feed.author, sql.placeholder('author')), - isNotNull(feed.post_hash), - gte(feed.quantity, sql.placeholder('minQuantity')), - )) - .orderBy(desc(feed.timestamp)) - .limit(sql.placeholder('limit')) - .offset(sql.placeholder('offset')) - .prepare('stmnt_get_user_replies_with_parents'); + .select({ + reply: feed, + parent: parentFeed, + }) + .from(feed) + .innerJoin(parentFeed, eq(feed.post_hash, parentFeed.hash)) + .where(and( + eq(feed.author, sql.placeholder('author')), + isNotNull(feed.post_hash), + gte(feed.quantity, sql.placeholder('minQuantity')), + )) + .orderBy(desc(feed.timestamp)) + .limit(sql.placeholder('limit')) + .offset(sql.placeholder('offset')) + .prepare('stmnt_get_user_replies_with_parents'); -export async function UserReplies(query: typeof Gets.UserRepliesQuery.static) { - let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; - const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; - const minQuantity = typeof query.minQuantity !== 'undefined' ? query.minQuantity : '0'; +export async function UserReplies(query: Gets.UserRepliesQuery) { + let limit = typeof query.limit !== 'undefined' ? Number(query.limit) : 100; + const offset = typeof query.offset !== 'undefined' ? Number(query.offset) : 0; + const minQuantity = typeof query.minQuantity !== 'undefined' ? query.minQuantity : '0'; - if (limit > 100) { - limit = 100; - } + if (limit > 100) { + limit = 100; + } - if (limit <= 0) { - return { status: 400, error: 'limit must be at least 1' }; - } + if (limit <= 0) { + return { status: 400, error: 'limit must be at least 1' }; + } - if (offset < 0) { - return { status: 400, error: 'offset must be at least 0' }; - } + if (offset < 0) { + return { status: 400, error: 'offset must be at least 0' }; + } - try { - const results = await getUserRepliesWithParent.execute({ author: query.address, limit, offset, minQuantity }); - return { status: 200, rows: results }; - } - catch (error) { - console.error(error); - return { status: 404, error: 'failed to find user replies' }; - } + try { + const results = await getUserRepliesWithParent.execute({ author: query.address, limit, offset, minQuantity }); + return { status: 200, rows: results }; + } catch (error) { + console.error(error); + return { status: 404, error: 'failed to find user replies' }; + } } diff --git a/packages/api-main/src/gets/search.ts b/packages/api-main/src/gets/search.ts index 6cf7a112..8dcfa1c9 100644 --- a/packages/api-main/src/gets/search.ts +++ b/packages/api-main/src/gets/search.ts @@ -1,50 +1,50 @@ -import { type Gets } from '@atomone/dither-api-types'; +import type { Gets } from '@atomone/dither-api-types'; + import { and, desc, gte, ilike, inArray, isNull, or, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { FeedTable } from '../../drizzle/schema'; -export async function Search(query: typeof Gets.SearchQuery.static) { - try { - const processedQuery = query.text - .trim() - .split(/\s+/) - .filter((w: string) => w.length > 0) - .map((w: string) => `${w}:*`) - .join(' & '); +export async function Search(query: Gets.SearchQuery) { + try { + const processedQuery = query.text + .trim() + .split(/\s+/) + .filter((w: string) => w.length > 0) + .map((w: string) => `${w}:*`) + .join(' & '); - if (!processedQuery) { - return []; - } + if (!processedQuery) { + return []; + } - const minQuantity = typeof query.minQuantity !== 'undefined' ? query.minQuantity : '0'; - const matchedAuthors = await getDatabase() - .selectDistinct({ author: FeedTable.author }) - .from(FeedTable) - .where(and(ilike(FeedTable.author, `%${query.text}%`), isNull(FeedTable.removed_at))); - const matchedAuthorAddresses = matchedAuthors.map(a => a.author); + const minQuantity = typeof query.minQuantity !== 'undefined' ? query.minQuantity : '0'; + const matchedAuthors = await getDatabase() + .selectDistinct({ author: FeedTable.author }) + .from(FeedTable) + .where(and(ilike(FeedTable.author, `%${query.text}%`), isNull(FeedTable.removed_at))); + const matchedAuthorAddresses = matchedAuthors.map(a => a.author); - const matchedPosts = await getDatabase() - .select() - .from(FeedTable) - .where( - and( - or( - sql`to_tsvector('english', ${FeedTable.message}) @@ to_tsquery('english', ${processedQuery})`, - inArray(FeedTable.author, matchedAuthorAddresses), - ), - gte(FeedTable.quantity, minQuantity), - isNull(FeedTable.removed_at), - ), - ) - .limit(100) - .offset(0) - .orderBy(desc(FeedTable.timestamp)) - .execute(); - return { status: 200, rows: [...matchedPosts], users: matchedAuthorAddresses }; - } - catch (error) { - console.error(error); - return { status: 400, error: 'failed to read data from database' }; - } + const matchedPosts = await getDatabase() + .select() + .from(FeedTable) + .where( + and( + or( + sql`to_tsvector('english', ${FeedTable.message}) @@ to_tsquery('english', ${processedQuery})`, + inArray(FeedTable.author, matchedAuthorAddresses), + ), + gte(FeedTable.quantity, minQuantity), + isNull(FeedTable.removed_at), + ), + ) + .limit(100) + .offset(0) + .orderBy(desc(FeedTable.timestamp)) + .execute(); + return { status: 200, rows: [...matchedPosts], users: matchedAuthorAddresses }; + } catch (error) { + console.error(error); + return { status: 400, error: 'failed to read data from database' }; + } } diff --git a/packages/api-main/src/index.ts b/packages/api-main/src/index.ts index c65f8d59..b2a14af4 100644 --- a/packages/api-main/src/index.ts +++ b/packages/api-main/src/index.ts @@ -1,32 +1,34 @@ +import process from 'node:process'; + import { cors } from '@elysiajs/cors'; import node from '@elysiajs/node'; import { Elysia } from 'elysia'; +import { useConfig } from './config'; import { authRoutes } from './routes/auth'; import { moderatorRoutes } from './routes/moderator'; import { publicRoutes } from './routes/public'; import { readerRoutes } from './routes/reader'; import { userRoutes } from './routes/user'; -import { useConfig } from './config'; const config = useConfig(); const app = new Elysia({ adapter: node(), prefix: '/v1' }); export function start() { - app.use(cors()); - app.use(publicRoutes); - app.use(authRoutes); - app.use(readerRoutes); - app.use(userRoutes); - app.use(moderatorRoutes); + app.use(cors()); + app.use(publicRoutes); + app.use(authRoutes); + app.use(readerRoutes); + app.use(userRoutes); + app.use(moderatorRoutes); - app.listen(config.PORT); + app.listen(config.PORT); } export function stop() { - app.stop(true); + app.stop(true); } if (!process.env.SKIP_START) { - start(); + start(); } diff --git a/packages/api-main/src/middleware/readerAuth.ts b/packages/api-main/src/middleware/readerAuth.ts index 191f6dc8..ad697b43 100644 --- a/packages/api-main/src/middleware/readerAuth.ts +++ b/packages/api-main/src/middleware/readerAuth.ts @@ -3,8 +3,8 @@ import { isReaderAuthorizationValid } from '../utility'; /** * Middleware to validate reader authorization header */ -export const readerAuthMiddleware = (context: { headers: Record }) => { - if (!isReaderAuthorizationValid(context.headers)) { - return { status: 401, error: 'Unauthorized to make write request' }; - } -}; +export function readerAuthMiddleware(context: { headers: Record }) { + if (!isReaderAuthorizationValid(context.headers)) { + return { status: 401, error: 'Unauthorized to make write request' }; + } +} diff --git a/packages/api-main/src/posts/auth.ts b/packages/api-main/src/posts/auth.ts index 2d7937cf..61c0076d 100644 --- a/packages/api-main/src/posts/auth.ts +++ b/packages/api-main/src/posts/auth.ts @@ -1,7 +1,6 @@ +import type { Posts } from '@atomone/dither-api-types'; import type { Cookie } from 'elysia'; -import { type Posts } from '@atomone/dither-api-types'; - import { useConfig } from '../config'; import { useRateLimiter } from '../shared/useRateLimiter'; import { useUserAuth } from '../shared/useUserAuth'; @@ -11,32 +10,31 @@ const { verifyAndCreate } = useUserAuth(); const { JWT_STRICTNESS } = useConfig(); const rateLimiter = useRateLimiter(); -export async function Auth(body: typeof Posts.AuthBody.static, auth: Cookie, request: Request) { - const ip = getRequestIP(request); - const isLimited = await rateLimiter.isLimited(ip); - if (isLimited) { - return { status: 429, error: 'Too many requests, try again later' }; - } - - await rateLimiter.update(ip); +export async function Auth(body: Posts.AuthBody, auth: Cookie, request: Request) { + const ip = getRequestIP(request); + const isLimited = await rateLimiter.isLimited(ip); + if (isLimited) { + return { status: 429, error: 'Too many requests, try again later' }; + } - try { - if ('json' in body) { - const jwt = await verifyAndCreate(body.pub_key.value, body.signature, body.id); - return jwt; - } + await rateLimiter.update(ip); - const result = await verifyAndCreate(body.pub_key.value, body.signature, body.id); - if (result.status === 200) { - auth.remove(); - auth.set({ sameSite: JWT_STRICTNESS, httpOnly: true, secure: true, value: result.bearer, maxAge: 259200, priority: 'high' }); - return { status: 200 }; - } - - return result; + try { + if ('json' in body) { + const jwt = await verifyAndCreate(body.pub_key.value, body.signature, body.id); + return jwt; } - catch (err) { - console.error(err); - return { status: 400, error: 'unauthorized signature or key provided, failed to verify' }; + + const result = await verifyAndCreate(body.pub_key.value, body.signature, body.id); + if (result.status === 200) { + auth.remove(); + auth.set({ sameSite: JWT_STRICTNESS, httpOnly: true, secure: true, value: result.bearer, maxAge: 259200, priority: 'high' }); + return { status: 200 }; } + + return result; + } catch (err) { + console.error(err); + return { status: 400, error: 'unauthorized signature or key provided, failed to verify' }; + } } diff --git a/packages/api-main/src/posts/authCreate.ts b/packages/api-main/src/posts/authCreate.ts index 2c4d25a9..b871e52f 100644 --- a/packages/api-main/src/posts/authCreate.ts +++ b/packages/api-main/src/posts/authCreate.ts @@ -1,4 +1,4 @@ -import { type Posts } from '@atomone/dither-api-types'; +import type { Posts } from '@atomone/dither-api-types'; import { useRateLimiter } from '../shared/useRateLimiter'; import { useUserAuth } from '../shared/useUserAuth'; @@ -7,21 +7,20 @@ import { getRequestIP } from '../utility'; const { add } = useUserAuth(); const rateLimiter = useRateLimiter(); -export async function AuthCreate(body: typeof Posts.AuthCreateBody.static, request: Request) { - const ip = getRequestIP(request); - const isLimited = await rateLimiter.isLimited(ip); - if (isLimited) { - return { status: 429, error: 'Too many requests, try again later' }; - } +export async function AuthCreate(body: Posts.AuthCreateBody, request: Request) { + const ip = getRequestIP(request); + const isLimited = await rateLimiter.isLimited(ip); + if (isLimited) { + return { status: 429, error: 'Too many requests, try again later' }; + } - await rateLimiter.update(ip); + await rateLimiter.update(ip); - try { - const result = await add(body.address); - return { status: 200, ...result }; - } - catch (err) { - console.error(err); - return { status: 400, error: 'failed to create authorization request' }; - } + try { + const result = await add(body.address); + return { status: 200, ...result }; + } catch (err) { + console.error(err); + return { status: 400, error: 'failed to create authorization request' }; + } } diff --git a/packages/api-main/src/posts/dislike.ts b/packages/api-main/src/posts/dislike.ts index 972609cf..e3398814 100644 --- a/packages/api-main/src/posts/dislike.ts +++ b/packages/api-main/src/posts/dislike.ts @@ -1,71 +1,71 @@ -import { type Posts } from '@atomone/dither-api-types'; +import type { Posts } from '@atomone/dither-api-types'; + import { eq, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { DislikesTable, FeedTable } from '../../drizzle/schema'; +import { notify } from '../shared/notify'; import { useSharedQueries } from '../shared/useSharedQueries'; +import { postToDiscord } from '../utility'; const sharedQueries = useSharedQueries(); -import { notify } from '../shared/notify'; -import { postToDiscord } from '../utility'; const statement = getDatabase() - .insert(DislikesTable) - .values({ - post_hash: sql.placeholder('post_hash'), - hash: sql.placeholder('hash'), - author: sql.placeholder('author'), - quantity: sql.placeholder('quantity'), - timestamp: sql.placeholder('timestamp'), - }) - .onConflictDoNothing() - .prepare('stmnt_add_dislike'); + .insert(DislikesTable) + .values({ + post_hash: sql.placeholder('post_hash'), + hash: sql.placeholder('hash'), + author: sql.placeholder('author'), + quantity: sql.placeholder('quantity'), + timestamp: sql.placeholder('timestamp'), + }) + .onConflictDoNothing() + .prepare('stmnt_add_dislike'); const statementAddDislikeToPost = getDatabase() - .update(FeedTable) - .set({ - dislikes: sql`${FeedTable.dislikes} + 1`, - dislikes_burnt: sql`(${FeedTable.dislikes_burnt})::int + ${sql.placeholder('quantity')}`, - }) - .where(eq(FeedTable.hash, sql.placeholder('post_hash'))) - .prepare('stmnt_add_dislike_count_to_post'); + .update(FeedTable) + .set({ + dislikes: sql`${FeedTable.dislikes} + 1`, + dislikes_burnt: sql`(${FeedTable.dislikes_burnt})::int + ${sql.placeholder('quantity')}`, + }) + .where(eq(FeedTable.hash, sql.placeholder('post_hash'))) + .prepare('stmnt_add_dislike_count_to_post'); -export async function Dislike(body: typeof Posts.DislikeBody.static) { - if (body.post_hash.length !== 64) { - return { status: 400, error: 'Provided post_hash is not valid for dislike' }; - } - - try { - const result = await sharedQueries.doesPostExist(body.post_hash); - if (result.status !== 200) { - return { status: result.status, error: 'provided post_hash does not exist' }; - } +export async function Dislike(body: Posts.DislikeBody) { + if (body.post_hash.length !== 64) { + return { status: 400, error: 'Provided post_hash is not valid for dislike' }; + } - const resultChanges = await statement.execute({ - post_hash: body.post_hash.toLowerCase(), - hash: body.hash.toLowerCase(), - author: body.from.toLowerCase(), - quantity: body.quantity, - timestamp: new Date(body.timestamp), - }); + try { + const result = await sharedQueries.doesPostExist(body.post_hash); + if (result.status !== 200) { + return { status: result.status, error: 'provided post_hash does not exist' }; + } - // Ensure that 'dislike' was triggered, and not already existing. - if (typeof resultChanges.rowCount === 'number' && resultChanges.rowCount >= 1) { - await statementAddDislikeToPost.execute({ post_hash: body.post_hash, quantity: body.quantity }); - await notify({ - post_hash: body.post_hash, - hash: body.hash, - type: 'dislike', - timestamp: new Date(body.timestamp), - actor: body.from, - }); - } + const resultChanges = await statement.execute({ + post_hash: body.post_hash.toLowerCase(), + hash: body.hash.toLowerCase(), + author: body.from.toLowerCase(), + quantity: body.quantity, + timestamp: new Date(body.timestamp), + }); - await postToDiscord(`Disliked by ${body.from.toLowerCase()}`, `https://dither.chat/post/${body.hash.toLowerCase()}`); - return { status: 200 }; - } - catch (err) { - console.error(err); - return { status: 400, error: 'failed to upsert data for dislike, dislike already exists' }; + // Ensure that 'dislike' was triggered, and not already existing. + if (typeof resultChanges.rowCount === 'number' && resultChanges.rowCount >= 1) { + await statementAddDislikeToPost.execute({ post_hash: body.post_hash, quantity: body.quantity }); + await notify({ + post_hash: body.post_hash, + hash: body.hash, + type: 'dislike', + timestamp: new Date(body.timestamp), + actor: body.from, + }); } + + await postToDiscord(`Disliked by ${body.from.toLowerCase()}`, `https://dither.chat/post/${body.hash.toLowerCase()}`); + return { status: 200 }; + } catch (err) { + console.error(err); + return { status: 400, error: 'failed to upsert data for dislike, dislike already exists' }; + } } diff --git a/packages/api-main/src/posts/flag.ts b/packages/api-main/src/posts/flag.ts index c8c4f0af..519abba9 100644 --- a/packages/api-main/src/posts/flag.ts +++ b/packages/api-main/src/posts/flag.ts @@ -1,69 +1,69 @@ -import { type Posts } from '@atomone/dither-api-types'; +import type { Posts } from '@atomone/dither-api-types'; + import { eq, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { FeedTable, FlagsTable } from '../../drizzle/schema'; +import { notify } from '../shared/notify'; import { useSharedQueries } from '../shared/useSharedQueries'; const sharedQueries = useSharedQueries(); -import { notify } from '../shared/notify'; const statement = getDatabase() - .insert(FlagsTable) - .values({ - post_hash: sql.placeholder('post_hash'), - hash: sql.placeholder('hash'), - author: sql.placeholder('author'), - quantity: sql.placeholder('quantity'), - timestamp: sql.placeholder('timestamp'), - }) - .onConflictDoNothing() - .prepare('stmnt_add_flag'); + .insert(FlagsTable) + .values({ + post_hash: sql.placeholder('post_hash'), + hash: sql.placeholder('hash'), + author: sql.placeholder('author'), + quantity: sql.placeholder('quantity'), + timestamp: sql.placeholder('timestamp'), + }) + .onConflictDoNothing() + .prepare('stmnt_add_flag'); const statementAddFlagToPost = getDatabase() - .update(FeedTable) - .set({ - flags: sql`${FeedTable.flags} + 1`, - flags_burnt: sql`(${FeedTable.flags_burnt})::int + ${sql.placeholder('quantity')}`, - }) - .where(eq(FeedTable.hash, sql.placeholder('post_hash'))) - .prepare('stmnt_add_flag_count_to_post'); - -export async function Flag(body: typeof Posts.FlagBody.static) { - if (body.post_hash.length !== 64) { - return { status: 400, error: 'Provided post_hash is not valid for flag' }; - } + .update(FeedTable) + .set({ + flags: sql`${FeedTable.flags} + 1`, + flags_burnt: sql`(${FeedTable.flags_burnt})::int + ${sql.placeholder('quantity')}`, + }) + .where(eq(FeedTable.hash, sql.placeholder('post_hash'))) + .prepare('stmnt_add_flag_count_to_post'); - try { - const result = await sharedQueries.doesPostExist(body.post_hash); - if (result.status !== 200) { - return { status: result.status, error: 'provided post_hash does not exist' }; - } +export async function Flag(body: Posts.FlagBody) { + if (body.post_hash.length !== 64) { + return { status: 400, error: 'Provided post_hash is not valid for flag' }; + } - const resultChanges = await statement.execute({ - post_hash: body.post_hash.toLowerCase(), - hash: body.hash.toLowerCase(), - author: body.from.toLowerCase(), - quantity: body.quantity, - timestamp: new Date(body.timestamp), - }); + try { + const result = await sharedQueries.doesPostExist(body.post_hash); + if (result.status !== 200) { + return { status: result.status, error: 'provided post_hash does not exist' }; + } - // Ensure that 'flag' was triggered, and not already existing. - if (typeof resultChanges.rowCount === 'number' && resultChanges.rowCount >= 1) { - await statementAddFlagToPost.execute({ post_hash: body.post_hash, quantity: body.quantity }); - await notify({ - post_hash: body.post_hash, - hash: body.hash, - type: 'flag', - timestamp: new Date(body.timestamp), - actor: body.from, - }); - } + const resultChanges = await statement.execute({ + post_hash: body.post_hash.toLowerCase(), + hash: body.hash.toLowerCase(), + author: body.from.toLowerCase(), + quantity: body.quantity, + timestamp: new Date(body.timestamp), + }); - return { status: 200 }; - } - catch (err) { - console.error(err); - return { status: 400, error: 'failed to upsert data for flag, flag already exists' }; + // Ensure that 'flag' was triggered, and not already existing. + if (typeof resultChanges.rowCount === 'number' && resultChanges.rowCount >= 1) { + await statementAddFlagToPost.execute({ post_hash: body.post_hash, quantity: body.quantity }); + await notify({ + post_hash: body.post_hash, + hash: body.hash, + type: 'flag', + timestamp: new Date(body.timestamp), + actor: body.from, + }); } + + return { status: 200 }; + } catch (err) { + console.error(err); + return { status: 400, error: 'failed to upsert data for flag, flag already exists' }; + } } diff --git a/packages/api-main/src/posts/follow.ts b/packages/api-main/src/posts/follow.ts index ae1a36fa..b937a4f0 100644 --- a/packages/api-main/src/posts/follow.ts +++ b/packages/api-main/src/posts/follow.ts @@ -1,4 +1,5 @@ -import { type Posts } from '@atomone/dither-api-types'; +import type { Posts } from '@atomone/dither-api-types'; + import { and, eq, isNotNull, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; @@ -6,71 +7,70 @@ import { FollowsTable } from '../../drizzle/schema'; import { notify } from '../shared/notify'; const statementAddFollower = getDatabase() - .insert(FollowsTable) - .values({ - follower: sql.placeholder('follower'), - following: sql.placeholder('following'), - hash: sql.placeholder('hash'), - timestamp: sql.placeholder('timestamp'), - }) - .onConflictDoNothing() - .prepare('stmnt_add_follower'); - -export async function Follow(body: typeof Posts.FollowBody.static) { - if (body.hash.length !== 64) { - return { status: 400, error: 'Provided hash is not valid for follow' }; - } + .insert(FollowsTable) + .values({ + follower: sql.placeholder('follower'), + following: sql.placeholder('following'), + hash: sql.placeholder('hash'), + timestamp: sql.placeholder('timestamp'), + }) + .onConflictDoNothing() + .prepare('stmnt_add_follower'); - if (body.address.length !== 44) { - return { status: 400, error: 'Provided address is not valid for follow' }; - } +export async function Follow(body: Posts.FollowBody) { + if (body.hash.length !== 64) { + return { status: 400, error: 'Provided hash is not valid for follow' }; + } - if (body.from.length !== 44) { - return { status: 400, error: 'Provided from address is not valid for follow' }; - } + if (body.address.length !== 44) { + return { status: 400, error: 'Provided address is not valid for follow' }; + } - if (body.from === body.address) { - return { status: 400, error: 'Provided from address cannot equal provided address, cannot follow self' }; - } + if (body.from.length !== 44) { + return { status: 400, error: 'Provided from address is not valid for follow' }; + } - try { - let result = await statementAddFollower.execute({ - follower: body.from.toLowerCase(), - following: body.address.toLowerCase(), - hash: body.hash.toLowerCase(), - timestamp: new Date(body.timestamp), - }); + if (body.from === body.address) { + return { status: 400, error: 'Provided from address cannot equal provided address, cannot follow self' }; + } - if (typeof result.rowCount !== 'number' || result.rowCount <= 0) { - // Attempts to add the follower because the entry already exists; and may be null - result = await getDatabase().update(FollowsTable).set( - { removed_at: null, hash: body.hash.toLowerCase() }).where( - and( - eq(FollowsTable.follower, body.from.toLowerCase()), - eq(FollowsTable.following, body.address.toLowerCase()), - isNotNull(FollowsTable.removed_at), - ), - ); + try { + let result = await statementAddFollower.execute({ + follower: body.from.toLowerCase(), + following: body.address.toLowerCase(), + hash: body.hash.toLowerCase(), + timestamp: new Date(body.timestamp), + }); - if (typeof result.rowCount !== 'number' || result.rowCount <= 0) { - return { status: 400, error: 'failed to add follow, follow already exists' }; - } - } - else { - // Only allow for notification of a new follower once, forever - await notify({ - owner: body.address, - hash: body.hash, - type: 'follow', - timestamp: new Date(body.timestamp), - actor: body.from, - }); - } + if (typeof result.rowCount !== 'number' || result.rowCount <= 0) { + // Attempts to add the follower because the entry already exists; and may be null + result = await getDatabase().update(FollowsTable).set( + { removed_at: null, hash: body.hash.toLowerCase() }, + ).where( + and( + eq(FollowsTable.follower, body.from.toLowerCase()), + eq(FollowsTable.following, body.address.toLowerCase()), + isNotNull(FollowsTable.removed_at), + ), + ); - return { status: 200 }; - } - catch (err) { - console.error(err); - return { status: 400, error: 'failed to communicate with database' }; + if (typeof result.rowCount !== 'number' || result.rowCount <= 0) { + return { status: 400, error: 'failed to add follow, follow already exists' }; + } + } else { + // Only allow for notification of a new follower once, forever + await notify({ + owner: body.address, + hash: body.hash, + type: 'follow', + timestamp: new Date(body.timestamp), + actor: body.from, + }); } + + return { status: 200 }; + } catch (err) { + console.error(err); + return { status: 400, error: 'failed to communicate with database' }; + } } diff --git a/packages/api-main/src/posts/like.ts b/packages/api-main/src/posts/like.ts index 1df667d2..49b39008 100644 --- a/packages/api-main/src/posts/like.ts +++ b/packages/api-main/src/posts/like.ts @@ -1,71 +1,71 @@ -import { type Posts } from '@atomone/dither-api-types'; +import type { Posts } from '@atomone/dither-api-types'; + import { eq, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { FeedTable, LikesTable } from '../../drizzle/schema'; +import { notify } from '../shared/notify'; import { useSharedQueries } from '../shared/useSharedQueries'; +import { postToDiscord } from '../utility'; const sharedQueries = useSharedQueries(); -import { notify } from '../shared/notify'; -import { postToDiscord } from '../utility'; const statement = getDatabase() - .insert(LikesTable) - .values({ - post_hash: sql.placeholder('post_hash'), - hash: sql.placeholder('hash'), - author: sql.placeholder('author'), - quantity: sql.placeholder('quantity'), - timestamp: sql.placeholder('timestamp'), - }) - .onConflictDoNothing() - .prepare('stmnt_add_like'); + .insert(LikesTable) + .values({ + post_hash: sql.placeholder('post_hash'), + hash: sql.placeholder('hash'), + author: sql.placeholder('author'), + quantity: sql.placeholder('quantity'), + timestamp: sql.placeholder('timestamp'), + }) + .onConflictDoNothing() + .prepare('stmnt_add_like'); const statementAddLikeToPost = getDatabase() - .update(FeedTable) - .set({ - likes: sql`${FeedTable.likes} + 1`, - likes_burnt: sql`(${FeedTable.likes_burnt})::int + ${sql.placeholder('quantity')}`, - }) - .where(eq(FeedTable.hash, sql.placeholder('post_hash'))) - .prepare('stmnt_add_like_count_to_post'); + .update(FeedTable) + .set({ + likes: sql`${FeedTable.likes} + 1`, + likes_burnt: sql`(${FeedTable.likes_burnt})::int + ${sql.placeholder('quantity')}`, + }) + .where(eq(FeedTable.hash, sql.placeholder('post_hash'))) + .prepare('stmnt_add_like_count_to_post'); -export async function Like(body: typeof Posts.LikeBody.static) { - if (body.post_hash.length !== 64) { - return { status: 400, error: 'Provided post_hash is not valid for like' }; - } - - try { - const result = await sharedQueries.doesPostExist(body.post_hash); - if (result.status !== 200) { - return { status: result.status, error: 'provided post_hash does not exist' }; - } +export async function Like(body: Posts.LikeBody) { + if (body.post_hash.length !== 64) { + return { status: 400, error: 'Provided post_hash is not valid for like' }; + } - const resultChanges = await statement.execute({ - post_hash: body.post_hash.toLowerCase(), - hash: body.hash.toLowerCase(), - author: body.from.toLowerCase(), - quantity: body.quantity, - timestamp: new Date(body.timestamp), - }); + try { + const result = await sharedQueries.doesPostExist(body.post_hash); + if (result.status !== 200) { + return { status: result.status, error: 'provided post_hash does not exist' }; + } - // Ensure that 'like' was triggered, and not already existing. - if (typeof resultChanges.rowCount === 'number' && resultChanges.rowCount >= 1) { - await statementAddLikeToPost.execute({ post_hash: body.post_hash, quantity: body.quantity }); - await notify({ - post_hash: body.post_hash, - hash: body.hash, - type: 'like', - timestamp: new Date(body.timestamp), - actor: body.from, - }); - } + const resultChanges = await statement.execute({ + post_hash: body.post_hash.toLowerCase(), + hash: body.hash.toLowerCase(), + author: body.from.toLowerCase(), + quantity: body.quantity, + timestamp: new Date(body.timestamp), + }); - await postToDiscord(`Liked by ${body.from.toLowerCase()}`, `https://dither.chat/post/${body.hash.toLowerCase()}`); - return { status: 200 }; - } - catch (err) { - console.error(err); - return { status: 400, error: 'failed to upsert data for like, like already exists' }; + // Ensure that 'like' was triggered, and not already existing. + if (typeof resultChanges.rowCount === 'number' && resultChanges.rowCount >= 1) { + await statementAddLikeToPost.execute({ post_hash: body.post_hash, quantity: body.quantity }); + await notify({ + post_hash: body.post_hash, + hash: body.hash, + type: 'like', + timestamp: new Date(body.timestamp), + actor: body.from, + }); } + + await postToDiscord(`Liked by ${body.from.toLowerCase()}`, `https://dither.chat/post/${body.hash.toLowerCase()}`); + return { status: 200 }; + } catch (err) { + console.error(err); + return { status: 400, error: 'failed to upsert data for like, like already exists' }; + } } diff --git a/packages/api-main/src/posts/logout.ts b/packages/api-main/src/posts/logout.ts index 8fe6781d..902538a9 100644 --- a/packages/api-main/src/posts/logout.ts +++ b/packages/api-main/src/posts/logout.ts @@ -1,12 +1,11 @@ import type { Cookie } from 'elysia'; export async function Logout(auth: Cookie) { - try { - auth.remove(); - return { status: 200 }; - } - catch (err) { - console.error(err); - return { status: 400, error: 'unauthorized signature or key provided, failed to verify' }; - } + try { + auth.remove(); + return { status: 200 }; + } catch (err) { + console.error(err); + return { status: 400, error: 'unauthorized signature or key provided, failed to verify' }; + } } diff --git a/packages/api-main/src/posts/mod.ts b/packages/api-main/src/posts/mod.ts index fc686033..0ab5adeb 100644 --- a/packages/api-main/src/posts/mod.ts +++ b/packages/api-main/src/posts/mod.ts @@ -1,6 +1,6 @@ +import type { Posts } from '@atomone/dither-api-types'; import type { Cookie } from 'elysia'; -import { type Posts } from '@atomone/dither-api-types'; import { and, eq, isNull, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; @@ -8,232 +8,228 @@ import { AuditTable, FeedTable, ModeratorTable } from '../../drizzle/schema'; import { verifyJWT } from '../shared/jwt'; const statementAuditRemovePost = getDatabase() - .insert(AuditTable) - .values({ - post_hash: sql.placeholder('post_hash'), - hash: sql.placeholder('hash'), - created_by: sql.placeholder('created_by'), - created_at: sql.placeholder('created_at'), - reason: sql.placeholder('reason'), - }) - .prepare('stmnt_audit_remove_post'); - -export async function ModRemovePost(body: typeof Posts.ModRemovePostBody.static, auth: Cookie) { - const response = await verifyJWT(auth.value); - if (typeof response === 'undefined') { - return { status: 401, error: 'Unauthorized token proivided' }; + .insert(AuditTable) + .values({ + post_hash: sql.placeholder('post_hash'), + hash: sql.placeholder('hash'), + created_by: sql.placeholder('created_by'), + created_at: sql.placeholder('created_at'), + reason: sql.placeholder('reason'), + }) + .prepare('stmnt_audit_remove_post'); + +export async function ModRemovePost(body: Posts.ModRemovePostBody, auth: Cookie) { + const response = await verifyJWT(auth.value); + if (typeof response === 'undefined') { + return { status: 401, error: 'Unauthorized token proivided' }; + } + + try { + const [mod] = await getDatabase() + .select() + .from(ModeratorTable) + .where(eq(ModeratorTable.address, response)) + .limit(1); + if (!mod) { + return { status: 404, error: 'moderator not found' }; } - try { - const [mod] = await getDatabase() - .select() - .from(ModeratorTable) - .where(eq(ModeratorTable.address, response)) - .limit(1); - if (!mod) { - return { status: 404, error: 'moderator not found' }; - } - - const [post] = await getDatabase().select().from(FeedTable).where(eq(FeedTable.hash, body.post_hash)).limit(1); - if (!post) { - return { status: 404, error: 'post not found' }; - } - - const statement = getDatabase() - .update(FeedTable) - .set({ - removed_at: new Date(body.timestamp), - removed_hash: body.hash.toLowerCase(), - removed_by: mod.address.toLowerCase(), - }) - .where(eq(FeedTable.hash, body.post_hash)) - .returning(); - - await statement.execute(); - - await statementAuditRemovePost.execute({ - post_hash: body.post_hash.toLowerCase(), - hash: body.hash.toLowerCase(), - created_by: mod.address.toLowerCase(), - created_at: new Date(body.timestamp), - reason: body.reason, - }); - - return { status: 200 }; - } - catch (err) { - console.error(err); - return { status: 400, error: 'failed to delete post' }; + const [post] = await getDatabase().select().from(FeedTable).where(eq(FeedTable.hash, body.post_hash)).limit(1); + if (!post) { + return { status: 404, error: 'post not found' }; } + + const statement = getDatabase() + .update(FeedTable) + .set({ + removed_at: new Date(body.timestamp), + removed_hash: body.hash.toLowerCase(), + removed_by: mod.address.toLowerCase(), + }) + .where(eq(FeedTable.hash, body.post_hash)) + .returning(); + + await statement.execute(); + + await statementAuditRemovePost.execute({ + post_hash: body.post_hash.toLowerCase(), + hash: body.hash.toLowerCase(), + created_by: mod.address.toLowerCase(), + created_at: new Date(body.timestamp), + reason: body.reason, + }); + + return { status: 200 }; + } catch (err) { + console.error(err); + return { status: 400, error: 'failed to delete post' }; + } } const statementAuditRestorePost = getDatabase() - .insert(AuditTable) - .values({ - post_hash: sql.placeholder('post_hash'), - hash: sql.placeholder('hash'), - restored_at: sql.placeholder('restored_at'), - restored_by: sql.placeholder('restored_by'), - reason: sql.placeholder('reason'), - }) - .prepare('stmnt_audit_restore_post'); - -export async function ModRestorePost(body: typeof Posts.ModRemovePostBody.static, auth: Cookie) { - const response = await verifyJWT(auth.value); - if (typeof response === 'undefined') { - return { status: 401, error: 'Unauthorized token proivided' }; + .insert(AuditTable) + .values({ + post_hash: sql.placeholder('post_hash'), + hash: sql.placeholder('hash'), + restored_at: sql.placeholder('restored_at'), + restored_by: sql.placeholder('restored_by'), + reason: sql.placeholder('reason'), + }) + .prepare('stmnt_audit_restore_post'); + +export async function ModRestorePost(body: Posts.ModRemovePostBody, auth: Cookie) { + const response = await verifyJWT(auth.value); + if (typeof response === 'undefined') { + return { status: 401, error: 'Unauthorized token proivided' }; + } + + try { + const [mod] = await getDatabase() + .select() + .from(ModeratorTable) + .where(eq(ModeratorTable.address, response)) + .limit(1); + if (!mod) { + return { status: 404, error: 'moderator not found' }; } - try { - const [mod] = await getDatabase() - .select() - .from(ModeratorTable) - .where(eq(ModeratorTable.address, response)) - .limit(1); - if (!mod) { - return { status: 404, error: 'moderator not found' }; - } - - const [post] = await getDatabase().select().from(FeedTable).where(eq(FeedTable.hash, body.post_hash)).limit(1); - if (!post) { - return { status: 404, error: 'post not found' }; - } - - if (!post.removed_at) { - return { status: 404, error: 'post not removed' }; - } - - const [postWasRemovedByMod] = await getDatabase() - .select() - .from(ModeratorTable) - .where(eq(ModeratorTable.address, post.removed_by ?? '')) - .limit(1); - if (!postWasRemovedByMod) { - return { status: 401, error: 'cannot restore a post removed by the user' }; - } - - const statement = getDatabase() - .update(FeedTable) - .set({ - removed_at: null, - removed_hash: null, - removed_by: null, - }) - .where(eq(FeedTable.hash, body.post_hash)) - .returning(); - - await statement.execute(); - - await statementAuditRestorePost.execute({ - post_hash: body.post_hash.toLowerCase(), - hash: body.hash.toLowerCase(), - restored_by: mod.address.toLowerCase(), - restored_at: new Date(body.timestamp), - reason: body.reason, - }); - - return { status: 200 }; + const [post] = await getDatabase().select().from(FeedTable).where(eq(FeedTable.hash, body.post_hash)).limit(1); + if (!post) { + return { status: 404, error: 'post not found' }; } - catch (err) { - console.error(err); - return { status: 400, error: 'failed to delete post, maybe invalid' }; + + if (!post.removed_at) { + return { status: 404, error: 'post not removed' }; + } + + const [postWasRemovedByMod] = await getDatabase() + .select() + .from(ModeratorTable) + .where(eq(ModeratorTable.address, post.removed_by ?? '')) + .limit(1); + if (!postWasRemovedByMod) { + return { status: 401, error: 'cannot restore a post removed by the user' }; } + + const statement = getDatabase() + .update(FeedTable) + .set({ + removed_at: null, + removed_hash: null, + removed_by: null, + }) + .where(eq(FeedTable.hash, body.post_hash)) + .returning(); + + await statement.execute(); + + await statementAuditRestorePost.execute({ + post_hash: body.post_hash.toLowerCase(), + hash: body.hash.toLowerCase(), + restored_by: mod.address.toLowerCase(), + restored_at: new Date(body.timestamp), + reason: body.reason, + }); + + return { status: 200 }; + } catch (err) { + console.error(err); + return { status: 400, error: 'failed to delete post, maybe invalid' }; + } } const statementAuditBanUser = getDatabase() - .insert(AuditTable) - .values({ - user_address: sql.placeholder('user_address'), - hash: sql.placeholder('hash'), - created_at: sql.placeholder('created_at'), - created_by: sql.placeholder('created_by'), - reason: sql.placeholder('reason'), - }) - .prepare('stmnt_audit_ban_user'); - -export async function ModBan(body: typeof Posts.ModBanBody.static, auth: Cookie) { - const response = await verifyJWT(auth.value); - if (typeof response === 'undefined') { - return { status: 401, error: 'Unauthorized token proivided' }; + .insert(AuditTable) + .values({ + user_address: sql.placeholder('user_address'), + hash: sql.placeholder('hash'), + created_at: sql.placeholder('created_at'), + created_by: sql.placeholder('created_by'), + reason: sql.placeholder('reason'), + }) + .prepare('stmnt_audit_ban_user'); + +export async function ModBan(body: Posts.ModBanBody, auth: Cookie) { + const response = await verifyJWT(auth.value); + if (typeof response === 'undefined') { + return { status: 401, error: 'Unauthorized token proivided' }; + } + + try { + const [mod] = await getDatabase() + .select() + .from(ModeratorTable) + .where(eq(ModeratorTable.address, response)) + .limit(1); + if (!mod) { + return { status: 404, error: 'moderator not found' }; } - try { - const [mod] = await getDatabase() - .select() - .from(ModeratorTable) - .where(eq(ModeratorTable.address, response)) - .limit(1); - if (!mod) { - return { status: 404, error: 'moderator not found' }; - } - - const statement = getDatabase() - .update(FeedTable) - .set({ - removed_at: new Date(body.timestamp), - removed_hash: body.hash.toLowerCase(), - removed_by: mod.address.toLowerCase(), - }) - .where(and(eq(FeedTable.author, body.user_address), isNull(FeedTable.removed_at))) - .returning(); - - await statement.execute(); - - await statementAuditBanUser.execute({ - user_address: body.user_address.toLowerCase(), - hash: body.hash.toLowerCase(), - created_by: mod.address.toLowerCase(), - created_at: new Date(body.timestamp), - reason: body.reason, - }); - - return { status: 200 }; - } - catch (err) { - console.error(err); - return { status: 400, error: 'failed to ban user' }; - } + const statement = getDatabase() + .update(FeedTable) + .set({ + removed_at: new Date(body.timestamp), + removed_hash: body.hash.toLowerCase(), + removed_by: mod.address.toLowerCase(), + }) + .where(and(eq(FeedTable.author, body.user_address), isNull(FeedTable.removed_at))) + .returning(); + + await statement.execute(); + + await statementAuditBanUser.execute({ + user_address: body.user_address.toLowerCase(), + hash: body.hash.toLowerCase(), + created_by: mod.address.toLowerCase(), + created_at: new Date(body.timestamp), + reason: body.reason, + }); + + return { status: 200 }; + } catch (err) { + console.error(err); + return { status: 400, error: 'failed to ban user' }; + } } const statementAuditUnbanUser = getDatabase() - .insert(AuditTable) - .values({ - user_address: sql.placeholder('user_address'), - hash: sql.placeholder('hash'), - restored_at: sql.placeholder('restored_at'), - restored_by: sql.placeholder('restored_by'), - reason: sql.placeholder('reason'), - }) - .prepare('stmnt_audit_unban_user'); - -export async function ModUnban(body: typeof Posts.ModBanBody.static, auth: Cookie) { - const response = await verifyJWT(auth.value); - if (typeof response === 'undefined') { - return { status: 401, error: 'Unauthorized token proivided' }; + .insert(AuditTable) + .values({ + user_address: sql.placeholder('user_address'), + hash: sql.placeholder('hash'), + restored_at: sql.placeholder('restored_at'), + restored_by: sql.placeholder('restored_by'), + reason: sql.placeholder('reason'), + }) + .prepare('stmnt_audit_unban_user'); + +export async function ModUnban(body: Posts.ModBanBody, auth: Cookie) { + const response = await verifyJWT(auth.value); + if (typeof response === 'undefined') { + return { status: 401, error: 'Unauthorized token proivided' }; + } + + try { + const [mod] = await getDatabase() + .select() + .from(ModeratorTable) + .where(eq(ModeratorTable.address, response)) + .limit(1); + if (!mod) { + return { status: 404, error: 'moderator not found' }; } - try { - const [mod] = await getDatabase() - .select() - .from(ModeratorTable) - .where(eq(ModeratorTable.address, response)) - .limit(1); - if (!mod) { - return { status: 404, error: 'moderator not found' }; - } - - await statementAuditUnbanUser.execute({ - user_address: body.user_address.toLowerCase(), - hash: body.hash.toLowerCase(), - restored_by: mod.address.toLowerCase(), - restored_at: new Date(body.timestamp), - reason: body.reason, - }); - - return { status: 200 }; - } - catch (err) { - console.error(err); - return { status: 400, error: 'failed to unban user' }; - } + await statementAuditUnbanUser.execute({ + user_address: body.user_address.toLowerCase(), + hash: body.hash.toLowerCase(), + restored_by: mod.address.toLowerCase(), + restored_at: new Date(body.timestamp), + reason: body.reason, + }); + + return { status: 200 }; + } catch (err) { + console.error(err); + return { status: 400, error: 'failed to unban user' }; + } } diff --git a/packages/api-main/src/posts/post.ts b/packages/api-main/src/posts/post.ts index ea1a1ecd..bb80d51a 100644 --- a/packages/api-main/src/posts/post.ts +++ b/packages/api-main/src/posts/post.ts @@ -1,4 +1,5 @@ -import { type Posts } from '@atomone/dither-api-types'; +import type { Posts } from '@atomone/dither-api-types'; + import { desc, eq, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; @@ -6,65 +7,64 @@ import { AuditTable, FeedTable } from '../../drizzle/schema'; import { postToDiscord } from '../utility'; const statement = getDatabase() - .insert(FeedTable) - .values({ - hash: sql.placeholder('hash'), - timestamp: sql.placeholder('timestamp'), - author: sql.placeholder('author'), - message: sql.placeholder('message'), - quantity: sql.placeholder('quantity'), - }) - .onConflictDoNothing() - .prepare('stmnt_post'); + .insert(FeedTable) + .values({ + hash: sql.placeholder('hash'), + timestamp: sql.placeholder('timestamp'), + author: sql.placeholder('author'), + message: sql.placeholder('message'), + quantity: sql.placeholder('quantity'), + }) + .onConflictDoNothing() + .prepare('stmnt_post'); -export async function Post(body: typeof Posts.PostBody.static) { - try { - if (body.msg.length >= 512) { - return { status: 400, error: 'message is too long' }; - } +export async function Post(body: Posts.PostBody) { + try { + if (body.msg.length >= 512) { + return { status: 400, error: 'message is too long' }; + } - await statement.execute({ - hash: body.hash.toLowerCase(), - timestamp: new Date(body.timestamp), - author: body.from.toLowerCase(), - message: body.msg, - quantity: body.quantity, - }); + await statement.execute({ + hash: body.hash.toLowerCase(), + timestamp: new Date(body.timestamp), + author: body.from.toLowerCase(), + message: body.msg, + quantity: body.quantity, + }); - await removePostIfBanned(body); + await removePostIfBanned(body); - await postToDiscord(body.msg, `https://dither.chat/post/${body.hash.toLowerCase()}`); + await postToDiscord(body.msg, `https://dither.chat/post/${body.hash.toLowerCase()}`); - return { status: 200 }; - } - catch (err) { - console.error(err); - return { status: 400, error: 'failed to upsert data for post' }; - } + return { status: 200 }; + } catch (err) { + console.error(err); + return { status: 400, error: 'failed to upsert data for post' }; + } } -async function removePostIfBanned(body: typeof Posts.PostBody.static) { - const [lastAuditOnUser] = await getDatabase() - .select() - .from(AuditTable) - .where(eq(AuditTable.user_address, body.from)) - .orderBy(desc(AuditTable.created_at)) - .limit(1); +async function removePostIfBanned(body: Posts.PostBody) { + const [lastAuditOnUser] = await getDatabase() + .select() + .from(AuditTable) + .where(eq(AuditTable.user_address, body.from)) + .orderBy(desc(AuditTable.created_at)) + .limit(1); // If there are not action over the user of they were restored (unbanned), do nothing - if (!lastAuditOnUser || lastAuditOnUser.restored_at) { - return; - } + if (!lastAuditOnUser || lastAuditOnUser.restored_at) { + return; + } - const statement = getDatabase() - .update(FeedTable) - .set({ - removed_at: new Date(body.timestamp), - removed_by: lastAuditOnUser.created_by, - }) - .where(eq(FeedTable.hash, body.hash)) - .returning(); + const statement = getDatabase() + .update(FeedTable) + .set({ + removed_at: new Date(body.timestamp), + removed_by: lastAuditOnUser.created_by, + }) + .where(eq(FeedTable.hash, body.hash)) + .returning(); - await statement.execute(); + await statement.execute(); - return lastAuditOnUser; + return lastAuditOnUser; } diff --git a/packages/api-main/src/posts/postRemove.ts b/packages/api-main/src/posts/postRemove.ts index 7dddcc0a..335aef68 100644 --- a/packages/api-main/src/posts/postRemove.ts +++ b/packages/api-main/src/posts/postRemove.ts @@ -1,37 +1,37 @@ -import { type Posts } from '@atomone/dither-api-types'; +import type { Posts } from '@atomone/dither-api-types'; + import { and, eq } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { FeedTable } from '../../drizzle/schema'; -export async function PostRemove(body: typeof Posts.PostRemoveBody.static) { - try { - const selectResults = await getDatabase() - .select() - .from(FeedTable) - .where(and(eq(FeedTable.hash, body.post_hash), eq(FeedTable.author, body.from))); +export async function PostRemove(body: Posts.PostRemoveBody) { + try { + const selectResults = await getDatabase() + .select() + .from(FeedTable) + .where(and(eq(FeedTable.hash, body.post_hash), eq(FeedTable.author, body.from))); - const hasOwnership = selectResults.length >= 1; - if (!hasOwnership) { - return { status: 200, error: 'did not have ownership for post removal, ignored removal' }; - } + const hasOwnership = selectResults.length >= 1; + if (!hasOwnership) { + return { status: 200, error: 'did not have ownership for post removal, ignored removal' }; + } - const statement = getDatabase() - .update(FeedTable) - .set({ - removed_at: new Date(body.timestamp), - removed_hash: body.hash.toLowerCase(), - removed_by: body.from.toLowerCase(), - }) - .where(eq(FeedTable.hash, body.post_hash)) - .returning(); + const statement = getDatabase() + .update(FeedTable) + .set({ + removed_at: new Date(body.timestamp), + removed_hash: body.hash.toLowerCase(), + removed_by: body.from.toLowerCase(), + }) + .where(eq(FeedTable.hash, body.post_hash)) + .returning(); - await statement.execute(); + await statement.execute(); - return { status: 200 }; - } - catch (err) { - console.error(err); - return { status: 400, error: 'failed to delete post, maybe invalid' }; - } + return { status: 200 }; + } catch (err) { + console.error(err); + return { status: 400, error: 'failed to delete post, maybe invalid' }; + } } diff --git a/packages/api-main/src/posts/reply.ts b/packages/api-main/src/posts/reply.ts index c134f201..511e6561 100644 --- a/packages/api-main/src/posts/reply.ts +++ b/packages/api-main/src/posts/reply.ts @@ -1,73 +1,73 @@ -import { type Posts } from '@atomone/dither-api-types'; +import type { Posts } from '@atomone/dither-api-types'; + import { eq, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { FeedTable } from '../../drizzle/schema'; +import { notify } from '../shared/notify'; import { useSharedQueries } from '../shared/useSharedQueries'; +import { postToDiscord } from '../utility'; const sharedQueries = useSharedQueries(); -import { notify } from '../shared/notify'; -import { postToDiscord } from '../utility'; const statement = getDatabase() - .insert(FeedTable) - .values({ - author: sql.placeholder('author'), - hash: sql.placeholder('hash'), - message: sql.placeholder('message'), - post_hash: sql.placeholder('post_hash'), - quantity: sql.placeholder('quantity'), - timestamp: sql.placeholder('timestamp'), - }) - .onConflictDoNothing() - .prepare('stmnt_reply'); + .insert(FeedTable) + .values({ + author: sql.placeholder('author'), + hash: sql.placeholder('hash'), + message: sql.placeholder('message'), + post_hash: sql.placeholder('post_hash'), + quantity: sql.placeholder('quantity'), + timestamp: sql.placeholder('timestamp'), + }) + .onConflictDoNothing() + .prepare('stmnt_reply'); const statementAddReplyCount = getDatabase() - .update(FeedTable) - .set({ replies: sql`${FeedTable.replies} + 1` }) - .where(eq(FeedTable.hash, sql.placeholder('post_hash'))) - .prepare('stmnt_add_reply_count'); + .update(FeedTable) + .set({ replies: sql`${FeedTable.replies} + 1` }) + .where(eq(FeedTable.hash, sql.placeholder('post_hash'))) + .prepare('stmnt_add_reply_count'); -export async function Reply(body: typeof Posts.ReplyBody.static) { - if (body.post_hash.length !== 64) { - return { status: 400, error: 'Provided post_hash is not valid for reply' }; - } - - try { - const result = await sharedQueries.doesPostExist(body.post_hash); - if (result.status !== 200) { - return { status: result.status, error: 'provided post_hash does not exist' }; - } +export async function Reply(body: Posts.ReplyBody) { + if (body.post_hash.length !== 64) { + return { status: 400, error: 'Provided post_hash is not valid for reply' }; + } - const resultChanges = await statement.execute({ - author: body.from.toLowerCase(), - hash: body.hash.toLowerCase(), - message: body.msg, - post_hash: body.post_hash.toLowerCase(), - quantity: body.quantity, - timestamp: new Date(body.timestamp), - }); + try { + const result = await sharedQueries.doesPostExist(body.post_hash); + if (result.status !== 200) { + return { status: result.status, error: 'provided post_hash does not exist' }; + } - if (typeof resultChanges.rowCount === 'number' && resultChanges.rowCount >= 1) { - await statementAddReplyCount.execute({ - post_hash: body.post_hash, - }); + const resultChanges = await statement.execute({ + author: body.from.toLowerCase(), + hash: body.hash.toLowerCase(), + message: body.msg, + post_hash: body.post_hash.toLowerCase(), + quantity: body.quantity, + timestamp: new Date(body.timestamp), + }); - await notify({ - post_hash: body.post_hash, - hash: body.hash, - type: 'reply', - timestamp: new Date(body.timestamp), - subcontext: body.msg.length >= 61 ? body.msg.slice(0, 61) + '...' : body.msg, - actor: body.from, - }); - } + if (typeof resultChanges.rowCount === 'number' && resultChanges.rowCount >= 1) { + await statementAddReplyCount.execute({ + post_hash: body.post_hash, + }); - await postToDiscord(`${body.msg}`, `https://dither.chat/post/${body.hash.toLowerCase()}`); - return { status: 200 }; - } - catch (err) { - console.error(err); - return { status: 400, error: 'failed to upsert data for post' }; + await notify({ + post_hash: body.post_hash, + hash: body.hash, + type: 'reply', + timestamp: new Date(body.timestamp), + subcontext: body.msg.length >= 61 ? `${body.msg.slice(0, 61)}...` : body.msg, + actor: body.from, + }); } + + await postToDiscord(`${body.msg}`, `https://dither.chat/post/${body.hash.toLowerCase()}`); + return { status: 200 }; + } catch (err) { + console.error(err); + return { status: 400, error: 'failed to upsert data for post' }; + } } diff --git a/packages/api-main/src/posts/unfollow.ts b/packages/api-main/src/posts/unfollow.ts index 9ad00af3..f18ebb26 100644 --- a/packages/api-main/src/posts/unfollow.ts +++ b/packages/api-main/src/posts/unfollow.ts @@ -1,27 +1,27 @@ -import { type Posts } from '@atomone/dither-api-types'; +import type { Posts } from '@atomone/dither-api-types'; + import { and, eq, sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; import { FollowsTable } from '../../drizzle/schema'; const statementRemoveFollowing = getDatabase() - .update(FollowsTable) - .set({ removed_at: sql.placeholder('removed_at') as never }) // Drizzle Type Issue atm. - .where( - and( - eq(FollowsTable.follower, sql.placeholder('follower')), - eq(FollowsTable.following, sql.placeholder('following')), - ), - ) - .prepare('stmnt_remove_follower'); + .update(FollowsTable) + .set({ removed_at: sql.placeholder('removed_at') as never }) // Drizzle Type Issue atm. + .where( + and( + eq(FollowsTable.follower, sql.placeholder('follower')), + eq(FollowsTable.following, sql.placeholder('following')), + ), + ) + .prepare('stmnt_remove_follower'); -export async function Unfollow(body: typeof Posts.UnfollowBody.static) { - try { - await statementRemoveFollowing.execute({ follower: body.from.toLowerCase(), following: body.address.toLowerCase(), removed_at: new Date(body.timestamp) }); - return { status: 200 }; - } - catch (err) { - console.error(err); - return { status: 200, error: 'failed to unfollow user, user may not exist' }; - } +export async function Unfollow(body: Posts.UnfollowBody) { + try { + await statementRemoveFollowing.execute({ follower: body.from.toLowerCase(), following: body.address.toLowerCase(), removed_at: new Date(body.timestamp) }); + return { status: 200 }; + } catch (err) { + console.error(err); + return { status: 200, error: 'failed to unfollow user, user may not exist' }; + } } diff --git a/packages/api-main/src/posts/updateState.ts b/packages/api-main/src/posts/updateState.ts index bf0ffd83..49bfe2d0 100644 --- a/packages/api-main/src/posts/updateState.ts +++ b/packages/api-main/src/posts/updateState.ts @@ -2,22 +2,22 @@ import { getDatabase } from '../../drizzle/db'; import { ReaderState } from '../../drizzle/schema'; export async function UpdateState(body: { last_block: string }) { - try { - await getDatabase() - .insert(ReaderState) - .values({ id: 0, last_block: body.last_block }) - .onConflictDoUpdate({ - target: ReaderState.id, - set: { - last_block: body.last_block, - }, - }).execute(); + try { + await getDatabase() + .insert(ReaderState) + .values({ id: 0, last_block: body.last_block }) + .onConflictDoUpdate({ + target: ReaderState.id, + set: { + last_block: body.last_block, + }, + }) + .execute(); - console.info(`Last Block Updated: ${body.last_block}`); - return { status: 200 }; - } - catch (err) { - console.error(err); - return { status: 500, error: 'failed to write to database' }; - } + console.info(`Last Block Updated: ${body.last_block}`); + return { status: 200 }; + } catch (err) { + console.error(err); + return { status: 500, error: 'failed to write to database' }; + } } diff --git a/packages/api-main/src/routes/auth.ts b/packages/api-main/src/routes/auth.ts index 099f2a56..29c651bb 100644 --- a/packages/api-main/src/routes/auth.ts +++ b/packages/api-main/src/routes/auth.ts @@ -8,14 +8,14 @@ import * as PostRequests from '../posts/index'; * Authentication-related routes */ export const authRoutes = new Elysia() - .get('/auth-verify', ({ cookie: { auth } }) => GetRequests.AuthVerify(auth)) - .post('/auth-create', ({ body, request }) => PostRequests.AuthCreate(body, request), { body: Posts.AuthCreateBody }) - .post('/auth', ({ body, cookie: { auth }, request }) => PostRequests.Auth(body, auth, request), { - body: t.Object({ - id: t.Number(), - pub_key: t.Object({ type: t.String(), value: t.String() }), - signature: t.String(), - json: t.Optional(t.Boolean()), - }), - }) - .post('/logout', ({ cookie: { auth } }) => PostRequests.Logout(auth)); + .get('/auth-verify', ({ cookie: { auth } }) => GetRequests.AuthVerify(auth)) + .post('/auth-create', ({ body, request }) => PostRequests.AuthCreate(body, request), { body: Posts.AuthCreateBodySchema }) + .post('/auth', ({ body, cookie: { auth }, request }) => PostRequests.Auth(body, auth, request), { + body: t.Object({ + id: t.Number(), + pub_key: t.Object({ type: t.String(), value: t.String() }), + signature: t.String(), + json: t.Optional(t.Boolean()), + }), + }) + .post('/logout', ({ cookie: { auth } }) => PostRequests.Logout(auth)); diff --git a/packages/api-main/src/routes/moderator.ts b/packages/api-main/src/routes/moderator.ts index 7581b44c..336a14e0 100644 --- a/packages/api-main/src/routes/moderator.ts +++ b/packages/api-main/src/routes/moderator.ts @@ -8,15 +8,15 @@ import * as PostRequests from '../posts/index'; * These routes are prefixed with /mod */ export const moderatorRoutes = new Elysia({ prefix: '/mod' }) - .post('/post-remove', ({ body, cookie: { auth } }) => PostRequests.ModRemovePost(body, auth), { - body: Posts.ModRemovePostBody, - }) - .post('/post-restore', ({ body, cookie: { auth } }) => PostRequests.ModRestorePost(body, auth), { - body: Posts.ModRemovePostBody, - }) - .post('/ban', ({ body, cookie: { auth } }) => PostRequests.ModBan(body, auth), { - body: Posts.ModBanBody, - }) - .post('/unban', ({ body, cookie: { auth } }) => PostRequests.ModUnban(body, auth), { - body: Posts.ModBanBody, - }); + .post('/post-remove', ({ body, cookie: { auth } }) => PostRequests.ModRemovePost(body, auth), { + body: Posts.ModRemovePostBodySchema, + }) + .post('/post-restore', ({ body, cookie: { auth } }) => PostRequests.ModRestorePost(body, auth), { + body: Posts.ModRemovePostBodySchema, + }) + .post('/ban', ({ body, cookie: { auth } }) => PostRequests.ModBan(body, auth), { + body: Posts.ModBanBodySchema, + }) + .post('/unban', ({ body, cookie: { auth } }) => PostRequests.ModUnban(body, auth), { + body: Posts.ModBanBodySchema, + }); diff --git a/packages/api-main/src/routes/public.ts b/packages/api-main/src/routes/public.ts index 3943e603..40317c7e 100644 --- a/packages/api-main/src/routes/public.ts +++ b/packages/api-main/src/routes/public.ts @@ -7,18 +7,18 @@ import * as GetRequests from '../gets/index'; * Public routes that don't require authentication */ export const publicRoutes = new Elysia() - .get('/health', GetRequests.health) - .get('/dislikes', ({ query }) => GetRequests.Dislikes(query), { query: Gets.DislikesQuery }) - .get('/feed', ({ query }) => GetRequests.Feed(query), { query: Gets.FeedQuery }) - .get('/flags', ({ query }) => GetRequests.Flags(query), { query: Gets.FlagsQuery }) - .get('/is-following', ({ query }) => GetRequests.IsFollowing(query), { query: Gets.IsFollowingQuery }) - .get('/followers', ({ query }) => GetRequests.Followers(query), { query: Gets.FollowersQuery }) - .get('/following', ({ query }) => GetRequests.Following(query), { query: Gets.FollowingQuery }) - .get('/likes', ({ query }) => GetRequests.Likes(query), { query: Gets.LikesQuery }) - .get('/posts', ({ query }) => GetRequests.Posts(query), { query: Gets.PostsQuery }) - .get('/post', ({ query }) => GetRequests.Post(query), { query: Gets.PostQuery }) - .get('/replies', ({ query }) => GetRequests.Replies(query), { query: Gets.RepliesQuery }) - .get('/search', ({ query }) => GetRequests.Search(query), { query: Gets.SearchQuery }) - .get('/user-replies', ({ query }) => GetRequests.UserReplies(query), { query: Gets.UserRepliesQuery }) - .get('/following-posts', ({ query }) => GetRequests.FollowingPosts(query), { query: Gets.PostsQuery }) - .get('/last-block', GetRequests.LastBlock); + .get('/health', GetRequests.health) + .get('/dislikes', ({ query }) => GetRequests.Dislikes(query), { query: Gets.DislikesQuerySchema }) + .get('/feed', ({ query }) => GetRequests.Feed(query), { query: Gets.FeedQuerySchema }) + .get('/flags', ({ query }) => GetRequests.Flags(query), { query: Gets.FlagsQuerySchema }) + .get('/is-following', ({ query }) => GetRequests.IsFollowing(query), { query: Gets.IsFollowingQuerySchema }) + .get('/followers', ({ query }) => GetRequests.Followers(query), { query: Gets.FollowersQuerySchema }) + .get('/following', ({ query }) => GetRequests.Following(query), { query: Gets.FollowingQuerySchema }) + .get('/likes', ({ query }) => GetRequests.Likes(query), { query: Gets.LikesQuerySchema }) + .get('/posts', ({ query }) => GetRequests.Posts(query), { query: Gets.PostsQuerySchema }) + .get('/post', ({ query }) => GetRequests.Post(query), { query: Gets.PostQuerySchema }) + .get('/replies', ({ query }) => GetRequests.Replies(query), { query: Gets.RepliesQuerySchema }) + .get('/search', ({ query }) => GetRequests.Search(query), { query: Gets.SearchQuerySchema }) + .get('/user-replies', ({ query }) => GetRequests.UserReplies(query), { query: Gets.UserRepliesQuerySchema }) + .get('/following-posts', ({ query }) => GetRequests.FollowingPosts(query), { query: Gets.PostsQuerySchema }) + .get('/last-block', GetRequests.LastBlock); diff --git a/packages/api-main/src/routes/reader.ts b/packages/api-main/src/routes/reader.ts index 905d51e3..f8e1d973 100644 --- a/packages/api-main/src/routes/reader.ts +++ b/packages/api-main/src/routes/reader.ts @@ -9,15 +9,15 @@ import * as PostRequests from '../posts/index'; * These routes use the Authorization header for authentication */ export const readerRoutes = new Elysia() - .onBeforeHandle(readerAuthMiddleware) - .post('/post', ({ body }) => PostRequests.Post(body), { body: Posts.PostBody }) - .post('/reply', ({ body }) => PostRequests.Reply(body), { body: Posts.ReplyBody }) - .post('/follow', ({ body }) => PostRequests.Follow(body), { body: Posts.FollowBody }) - .post('/unfollow', ({ body }) => PostRequests.Unfollow(body), { body: Posts.UnfollowBody }) - .post('/like', ({ body }) => PostRequests.Like(body), { body: Posts.LikeBody }) - .post('/dislike', ({ body }) => PostRequests.Dislike(body), { body: Posts.DislikeBody }) - .post('/flag', ({ body }) => PostRequests.Flag(body), { body: Posts.FlagBody }) - .post('/post-remove', ({ body }) => PostRequests.PostRemove(body), { body: Posts.PostRemoveBody }) - .post('/update-state', ({ body }) => PostRequests.UpdateState(body), { - body: t.Object({ last_block: t.String() }), - }); + .onBeforeHandle(readerAuthMiddleware) + .post('/post', ({ body }) => PostRequests.Post(body), { body: Posts.PostBodySchema }) + .post('/reply', ({ body }) => PostRequests.Reply(body), { body: Posts.ReplyBodySchema }) + .post('/follow', ({ body }) => PostRequests.Follow(body), { body: Posts.FollowBodySchema }) + .post('/unfollow', ({ body }) => PostRequests.Unfollow(body), { body: Posts.UnfollowBodySchema }) + .post('/like', ({ body }) => PostRequests.Like(body), { body: Posts.LikeBodySchema }) + .post('/dislike', ({ body }) => PostRequests.Dislike(body), { body: Posts.DislikeBodySchema }) + .post('/flag', ({ body }) => PostRequests.Flag(body), { body: Posts.FlagBodySchema }) + .post('/post-remove', ({ body }) => PostRequests.PostRemove(body), { body: Posts.PostRemoveBodySchema }) + .post('/update-state', ({ body }) => PostRequests.UpdateState(body), { + body: t.Object({ last_block: t.String() }), + }); diff --git a/packages/api-main/src/routes/user.ts b/packages/api-main/src/routes/user.ts index 5744f798..cd17bb28 100644 --- a/packages/api-main/src/routes/user.ts +++ b/packages/api-main/src/routes/user.ts @@ -7,12 +7,12 @@ import * as GetRequests from '../gets/index'; * Routes that require user authentication via JWT cookie */ export const userRoutes = new Elysia() - .get('/notifications', ({ query, cookie: { auth } }) => GetRequests.Notifications(query, auth), { - query: Gets.NotificationsQuery, - }) - .get('/notifications-count', ({ query, cookie: { auth } }) => GetRequests.NotificationsCount(query, auth), { - query: Gets.NotificationsCountQuery, - }) - .post('/notification-read', ({ query, cookie: { auth } }) => GetRequests.ReadNotification(query, auth), { - query: Gets.ReadNotificationQuery, - }); + .get('/notifications', ({ query, cookie: { auth } }) => GetRequests.Notifications(query, auth), { + query: Gets.NotificationsQuerySchema, + }) + .get('/notifications-count', ({ query, cookie: { auth } }) => GetRequests.NotificationsCount(query, auth), { + query: Gets.NotificationsCountQuerySchema, + }) + .post('/notification-read', ({ query, cookie: { auth } }) => GetRequests.ReadNotification(query, auth), { + query: Gets.ReadNotificationQuerySchema, + }); diff --git a/packages/api-main/src/shared/jwt.ts b/packages/api-main/src/shared/jwt.ts index e50d8927..174145bf 100644 --- a/packages/api-main/src/shared/jwt.ts +++ b/packages/api-main/src/shared/jwt.ts @@ -4,24 +4,23 @@ import { useConfig } from '../config'; const { JWT } = useConfig(); -export const verifyJWT = async (token: string | undefined) => { - if (!token) { - return undefined; - } - - try { - const tokenData = jwt.verify(token, JWT, { algorithms: ['HS256'], maxAge: '3d' }) as { data: string; iat: number; exp: number }; - if (!tokenData) { - return undefined; - } +export async function verifyJWT(token: string | undefined) { + if (!token) { + return undefined; + } - // token data is on the form Login,id,date,publicKey,nonce - // so to obtain the user address we need to split on the comma - // and take the 4th element - return tokenData.data.split(',')[2]; // Returns user address + try { + const tokenData = jwt.verify(token, JWT, { algorithms: ['HS256'], maxAge: '3d' }) as { data: string; iat: number; exp: number }; + if (!tokenData) { + return undefined; } - catch (err) { - console.error(err); - return undefined; - } -}; + + // token data is on the form Login,id,date,publicKey,nonce + // so to obtain the user address we need to split on the comma + // and take the 4th element + return tokenData.data.split(',')[2]; // Returns user address + } catch (err) { + console.error(err); + return undefined; + } +} diff --git a/packages/api-main/src/shared/notify.ts b/packages/api-main/src/shared/notify.ts index 4e06f3c2..1899c5d0 100644 --- a/packages/api-main/src/shared/notify.ts +++ b/packages/api-main/src/shared/notify.ts @@ -4,60 +4,60 @@ import { getDatabase } from '../../drizzle/db'; import { FeedTable, NotificationTable } from '../../drizzle/schema'; const statementInsertNotification = getDatabase() - .insert(NotificationTable) - .values({ - owner: sql.placeholder('owner'), - hash: sql.placeholder('hash'), - post_hash: sql.placeholder('post_hash'), - type: sql.placeholder('type'), - timestamp: sql.placeholder('timestamp'), - subcontext: sql.placeholder('subcontext'), - actor: sql.placeholder('actor'), - }) - .onConflictDoNothing() - .prepare('stmnt_insert_notification'); + .insert(NotificationTable) + .values({ + owner: sql.placeholder('owner'), + hash: sql.placeholder('hash'), + post_hash: sql.placeholder('post_hash'), + type: sql.placeholder('type'), + timestamp: sql.placeholder('timestamp'), + subcontext: sql.placeholder('subcontext'), + actor: sql.placeholder('actor'), + }) + .onConflictDoNothing() + .prepare('stmnt_insert_notification'); const statementGetTargetPost = getDatabase().select().from(FeedTable).where(eq(FeedTable.hash, sql.placeholder('post_hash'))).limit(1).prepare('stmnt_fetch_target_post'); -export const notify = async (data: { - hash: string; - type: string; - timestamp: Date; - actor: string; - subcontext?: string; - post_hash?: string; - owner?: string; -}) => { - let owner = data.owner; - - if (data.post_hash) { - const [post] = await statementGetTargetPost.execute({ post_hash: data.post_hash?.toLowerCase() }); - if (!post) { - throw new Error('post not found'); - } - owner ??= post.author; - - if (!data.subcontext) { - const subcontext = post.message.length >= 64 ? post.message.slice(0, 61) + '...' : post.message; - data.subcontext = subcontext; - } +export async function notify(data: { + hash: string; + type: string; + timestamp: Date; + actor: string; + subcontext?: string; + post_hash?: string; + owner?: string; +}) { + let owner = data.owner; + + if (data.post_hash) { + const [post] = await statementGetTargetPost.execute({ post_hash: data.post_hash?.toLowerCase() }); + if (!post) { + throw new Error('post not found'); } + owner ??= post.author; - if (!owner) { - throw new Error('failed to add owner'); + if (!data.subcontext) { + const subcontext = post.message.length >= 64 ? `${post.message.slice(0, 61)}...` : post.message; + data.subcontext = subcontext; } - - if (owner === data.actor) { - return; - } - - await statementInsertNotification.execute({ - owner: owner.toLowerCase(), - post_hash: data.post_hash?.toLowerCase(), - hash: data.hash.toLowerCase(), - type: data.type, - timestamp: data.timestamp ?? null, - subcontext: data.subcontext, - actor: data.actor, - }); -}; + } + + if (!owner) { + throw new Error('failed to add owner'); + } + + if (owner === data.actor) { + return; + } + + await statementInsertNotification.execute({ + owner: owner.toLowerCase(), + post_hash: data.post_hash?.toLowerCase(), + hash: data.hash.toLowerCase(), + type: data.type, + timestamp: data.timestamp ?? null, + subcontext: data.subcontext, + actor: data.actor, + }); +} diff --git a/packages/api-main/src/shared/useRateLimiter.ts b/packages/api-main/src/shared/useRateLimiter.ts index 069c3166..dcc236a8 100644 --- a/packages/api-main/src/shared/useRateLimiter.ts +++ b/packages/api-main/src/shared/useRateLimiter.ts @@ -13,113 +13,110 @@ let cleanupInterval: NodeJS.Timeout | null = null; let isCleaningUp = false; async function cleanup() { - if (isCleaningUp) { - return; - } + if (isCleaningUp) { + return; + } - isCleaningUp = true; - try { - const now = Date.now(); - const threshold = now - MAX_REQUEST_TIME_MS; - const result = await getDatabase() - .delete(rateLimits) - .where( - lt(rateLimits.lastRequest, sql`${threshold}`), - ) - .execute(); + isCleaningUp = true; + try { + const now = Date.now(); + const threshold = now - MAX_REQUEST_TIME_MS; + const result = await getDatabase() + .delete(rateLimits) + .where( + lt(rateLimits.lastRequest, sql`${threshold}`), + ) + .execute(); - if (result.rowCount && result.rowCount > 0) { - console.log(`Cleaned Up Requests | Count: ${result.rowCount}`); - } - } - catch (err) { - console.error('Error during database cleanup:', err); - } - finally { - isCleaningUp = false; + if (result.rowCount && result.rowCount > 0) { + console.log(`Cleaned Up Requests | Count: ${result.rowCount}`); } + } catch (err) { + console.error('Error during database cleanup:', err); + } finally { + isCleaningUp = false; + } } export function useRateLimiter() { - if (!cleanupInterval) { - cleanupInterval = setInterval(cleanup, TIME_BETWEEN_CLEANUP_MS); - // Ensure the process doesn't hang on this interval - if (cleanupInterval.unref) { - cleanupInterval.unref(); - } + if (!cleanupInterval) { + cleanupInterval = setInterval(cleanup, TIME_BETWEEN_CLEANUP_MS); + // Ensure the process doesn't hang on this interval + if (cleanupInterval.unref) { + cleanupInterval.unref(); } + } - /** - * Updates the request count for a given IP address. - * @param ip The IP address of the user. - */ - async function update(ip: string) { - const now = Date.now(); + /** + * Updates the request count for a given IP address. + * @param ip The IP address of the user. + */ + async function update(ip: string) { + const now = Date.now(); - // Use a transaction to ensure atomicity - await getDatabase().transaction(async (tx) => { - const existingRecord = await tx - .select() - .from(rateLimits) - .where(eq(rateLimits.ip, ip)) - .limit(1) - .execute(); + // Use a transaction to ensure atomicity + await getDatabase().transaction(async (tx) => { + const existingRecord = await tx + .select() + .from(rateLimits) + .where(eq(rateLimits.ip, ip)) + .limit(1) + .execute(); - if (existingRecord.length > 0) { - // If a record exists, update it. - await tx - .update(rateLimits) - .set({ - requests: sql`${rateLimits.requests} + 1`, - lastRequest: now, - }) - .where(eq(rateLimits.ip, ip)) - .execute(); - } - else { - // If no record exists, insert a new one. - await tx - .insert(rateLimits) - .values({ - ip: ip, - requests: 1, - lastRequest: now, - }) - .execute(); - } - }); - } - - /** - * Checks if a given IP address is limited. - * @param ip The IP address of the user. - * @returns True if the IP is limited, otherwise false. - */ - async function isLimited(ip: string): Promise { - const now = Date.now(); - const record = await getDatabase() - .select() - .from(rateLimits) - .where( - and( - eq(rateLimits.ip, ip), - gt(rateLimits.lastRequest, now - MAX_REQUEST_TIME_MS), - ), - ) - .limit(1) - .execute(); + if (existingRecord.length > 0) { + // If a record exists, update it. + await tx + .update(rateLimits) + .set({ + requests: sql`${rateLimits.requests} + 1`, + lastRequest: now, + }) + .where(eq(rateLimits.ip, ip)) + .execute(); + } else { + // If no record exists, insert a new one. + await tx + .insert(rateLimits) + .values({ + ip, + requests: 1, + lastRequest: now, + }) + .execute(); + } + }); + } - if (record.length === 0) { - // No record, so it's not limited - return false; - } + /** + * Checks if a given IP address is limited. + * @param ip The IP address of the user. + * @returns True if the IP is limited, otherwise false. + */ + async function isLimited(ip: string): Promise { + const now = Date.now(); + const record = await getDatabase() + .select() + .from(rateLimits) + .where( + and( + eq(rateLimits.ip, ip), + gt(rateLimits.lastRequest, now - MAX_REQUEST_TIME_MS), + ), + ) + .limit(1) + .execute(); - const { requests } = record[0]; - return requests >= MAX_REQUESTS; + if (record.length === 0) { + // No record, so it's not limited + return false; } - return { - update, - isLimited, - }; + const { requests } = record[0]; + return requests >= MAX_REQUESTS; + } + + return { + update, + isLimited, + }; } diff --git a/packages/api-main/src/shared/useSharedQueries.ts b/packages/api-main/src/shared/useSharedQueries.ts index 550893b8..660d12e6 100644 --- a/packages/api-main/src/shared/useSharedQueries.ts +++ b/packages/api-main/src/shared/useSharedQueries.ts @@ -6,18 +6,17 @@ import { FeedTable } from '../../drizzle/schema'; const doesPostExistStatement = getDatabase().select().from(FeedTable).where(eq(FeedTable.hash, sql.placeholder('post_hash'))).prepare('stmnt_get_post_by_hash'); export function useSharedQueries() { - const doesPostExist = async (post_hash: string) => { - try { - const results = await doesPostExistStatement.execute({ post_hash }); - return results.length >= 1 ? { status: 200 } : { status: 404 }; - } - catch (err) { - console.error(err); - return { status: 500 }; - } - }; + const doesPostExist = async (post_hash: string) => { + try { + const results = await doesPostExistStatement.execute({ post_hash }); + return results.length >= 1 ? { status: 200 } : { status: 404 }; + } catch (err) { + console.error(err); + return { status: 500 }; + } + }; - return { - doesPostExist, - }; + return { + doesPostExist, + }; } diff --git a/packages/api-main/src/shared/useUserAuth.ts b/packages/api-main/src/shared/useUserAuth.ts index 44426b58..eb0464e0 100644 --- a/packages/api-main/src/shared/useUserAuth.ts +++ b/packages/api-main/src/shared/useUserAuth.ts @@ -1,4 +1,5 @@ -import { randomBytes } from 'crypto'; +import { Buffer } from 'node:buffer'; +import { randomBytes } from 'node:crypto'; import { encodeSecp256k1Pubkey, pubkeyToAddress } from '@cosmjs/amino'; import { verifyADR36Amino } from '@keplr-wallet/cosmos'; @@ -14,96 +15,96 @@ const { JWT } = useConfig(); const expirationTime = 60_000 * 5; function getSignerAddressFromPublicKey(publicKeyBase64: string, prefix: string = 'atone'): string { - const publicKeyBytes = new Uint8Array(Buffer.from(publicKeyBase64, 'base64')); - const secp256k1Pubkey = encodeSecp256k1Pubkey(publicKeyBytes); - return pubkeyToAddress(secp256k1Pubkey, prefix); + const publicKeyBytes = new Uint8Array(Buffer.from(publicKeyBase64, 'base64')); + const secp256k1Pubkey = encodeSecp256k1Pubkey(publicKeyBytes); + return pubkeyToAddress(secp256k1Pubkey, prefix); } async function cleanupRequests() { - const epoch = new Date(Date.now()); - await getDatabase().delete(AuthRequests).where(lt(AuthRequests.timestamp, epoch)).execute(); + const epoch = new Date(Date.now()); + await getDatabase().delete(AuthRequests).where(lt(AuthRequests.timestamp, epoch)).execute(); } export function useUserAuth() { - /** - * Simply creates an authentication request for a specific key. - * It is a time-locked request with a unique identifier. - * - * @param {string} publicKey - * @return {*} - */ - const add = async (publicKey: string) => { - const nonce = randomBytes(16).toString('hex'); - const timestamp = Date.now() + expirationTime; - - let signableMessage = ''; - - // [msg, timestamp, key, nonce] - signableMessage += 'Login,'; - signableMessage += `${timestamp},`; - signableMessage += `${publicKey},`; - signableMessage += `${nonce}`; - - const rows = await getDatabase().insert(AuthRequests).values({ msg: signableMessage, timestamp: new Date(timestamp) }).returning(); - return { id: rows[0].id, message: signableMessage }; - }; - - /** - * How this works is that a user makes a request to authenticate. - * They are given a message that needs to be signed. - * In that message contains a timestamp with an expiration set 5 minutes in the future. - * Additionally they are given an id for their request. - * - * When they authenticate, they sign the message with a wallet. - * The signature and public key are passed up. - * We used the public key and id to identify the data that was stored in-memory. - * We take the signature bytes and verify it against the message that was signed. - * We take the original message, apply the future time, and verify the timestamp is in the correct window. - * - * Finally, if everything is valid the data is cleaned up and can never be authenticated against again. - * Additionally, during each failed attempt we go through and cleanup old login requests. - * - * @param {string} publicKey - * @param {string} signature - * @param {number} id - * @return {*} - */ - const verifyAndCreate = async (publicKey: string, signature: string, id: number) => { - const publicAddress = getSignerAddressFromPublicKey(publicKey, 'atone'); - - const rows = await getDatabase().select().from(AuthRequests).where(eq(AuthRequests.id, id)).limit(1).execute(); - if (rows.length <= 0) { - cleanupRequests(); - return { status: 401, error: 'no available requests found' }; - } - - if (Date.now() > new Date(rows[0].timestamp).getTime()) { - cleanupRequests(); - return { status: 401, error: 'request expired' }; - } - - const originalMessage = rows[0].msg; - const didVerify = verifyADR36Amino( - 'atone', - publicAddress, - originalMessage, - new Uint8Array(Buffer.from(publicKey, 'base64')), - new Uint8Array(Buffer.from(signature, 'base64')), - 'secp256k1', - ); - - if (!didVerify) { - console.warn(`Failed to Verify: ${publicAddress}, ${originalMessage}`); - cleanupRequests(); - return { status: 401, error: 'failed to verify request from public key' }; - } - - await getDatabase().delete(AuthRequests).where(eq(AuthRequests.id, id)).returning(); - return { status: 200, bearer: jwt.sign({ data: rows[0].msg }, JWT, { expiresIn: '3d', algorithm: 'HS256' }) }; - }; - - return { - add, - verifyAndCreate, - }; + /** + * Simply creates an authentication request for a specific key. + * It is a time-locked request with a unique identifier. + * + * @param {string} publicKey + * @return {*} + */ + const add = async (publicKey: string) => { + const nonce = randomBytes(16).toString('hex'); + const timestamp = Date.now() + expirationTime; + + let signableMessage = ''; + + // [msg, timestamp, key, nonce] + signableMessage += 'Login,'; + signableMessage += `${timestamp},`; + signableMessage += `${publicKey},`; + signableMessage += `${nonce}`; + + const rows = await getDatabase().insert(AuthRequests).values({ msg: signableMessage, timestamp: new Date(timestamp) }).returning(); + return { id: rows[0].id, message: signableMessage }; + }; + + /** + * How this works is that a user makes a request to authenticate. + * They are given a message that needs to be signed. + * In that message contains a timestamp with an expiration set 5 minutes in the future. + * Additionally they are given an id for their request. + * + * When they authenticate, they sign the message with a wallet. + * The signature and public key are passed up. + * We used the public key and id to identify the data that was stored in-memory. + * We take the signature bytes and verify it against the message that was signed. + * We take the original message, apply the future time, and verify the timestamp is in the correct window. + * + * Finally, if everything is valid the data is cleaned up and can never be authenticated against again. + * Additionally, during each failed attempt we go through and cleanup old login requests. + * + * @param {string} publicKey + * @param {string} signature + * @param {number} id + * @return {*} + */ + const verifyAndCreate = async (publicKey: string, signature: string, id: number) => { + const publicAddress = getSignerAddressFromPublicKey(publicKey, 'atone'); + + const rows = await getDatabase().select().from(AuthRequests).where(eq(AuthRequests.id, id)).limit(1).execute(); + if (rows.length <= 0) { + cleanupRequests(); + return { status: 401, error: 'no available requests found' }; + } + + if (Date.now() > new Date(rows[0].timestamp).getTime()) { + cleanupRequests(); + return { status: 401, error: 'request expired' }; + } + + const originalMessage = rows[0].msg; + const didVerify = verifyADR36Amino( + 'atone', + publicAddress, + originalMessage, + new Uint8Array(Buffer.from(publicKey, 'base64')), + new Uint8Array(Buffer.from(signature, 'base64')), + 'secp256k1', + ); + + if (!didVerify) { + console.warn(`Failed to Verify: ${publicAddress}, ${originalMessage}`); + cleanupRequests(); + return { status: 401, error: 'failed to verify request from public key' }; + } + + await getDatabase().delete(AuthRequests).where(eq(AuthRequests.id, id)).returning(); + return { status: 200, bearer: jwt.sign({ data: rows[0].msg }, JWT, { expiresIn: '3d', algorithm: 'HS256' }) }; + }; + + return { + add, + verifyAndCreate, + }; } diff --git a/packages/api-main/src/types/feed.ts b/packages/api-main/src/types/feed.ts index 9a605758..6e7e995c 100644 --- a/packages/api-main/src/types/feed.ts +++ b/packages/api-main/src/types/feed.ts @@ -9,10 +9,10 @@ export type Post = InferSelectModel; export const postSchema = createSelectSchema(FeedTable); export interface ReplyWithParent { - reply: InferSelectModel; - parent: InferSelectModel; + reply: InferSelectModel; + parent: InferSelectModel; }; export const replyWithParentSchema = Type.Object({ - reply: postSchema, - parent: postSchema, + reply: postSchema, + parent: postSchema, }); diff --git a/packages/api-main/src/types/follows.ts b/packages/api-main/src/types/follows.ts index caa93efd..7027833d 100644 --- a/packages/api-main/src/types/follows.ts +++ b/packages/api-main/src/types/follows.ts @@ -1,4 +1,6 @@ -import { type Static, Type } from '@sinclair/typebox'; +import type { Static } from '@sinclair/typebox'; + +import { Type } from '@sinclair/typebox'; import { createSelectSchema } from 'drizzle-typebox'; import { FollowsTable } from '../../drizzle/schema'; @@ -6,7 +8,7 @@ import { FollowsTable } from '../../drizzle/schema'; export const followSchema = createSelectSchema(FollowsTable); export const followingSchema = Type.Object({ - address: followSchema.properties.following, - hash: followSchema.properties.hash, + address: followSchema.properties.following, + hash: followSchema.properties.hash, }); export type Following = Static; diff --git a/packages/api-main/src/types/index.ts b/packages/api-main/src/types/index.ts index 4993e267..48f3d0ea 100644 --- a/packages/api-main/src/types/index.ts +++ b/packages/api-main/src/types/index.ts @@ -1,12 +1,12 @@ import type { getDatabase } from '../../drizzle/db'; -export type MsgTransfer = { - '@type': string; - 'from_address': string; - 'to_address': string; - 'amount': Array<{ amount: string; denom: string }>; -}; +export interface MsgTransfer { + '@type': string; + 'from_address': string; + 'to_address': string; + 'amount': Array<{ amount: string; denom: string }>; +} -export type MsgGeneric = { [key: string]: unknown }; +export interface MsgGeneric { [key: string]: unknown } export type TransactionFunction = (tx: ReturnType) => Promise; diff --git a/packages/api-main/src/utility/index.ts b/packages/api-main/src/utility/index.ts index e61c4293..b9f87ba2 100644 --- a/packages/api-main/src/utility/index.ts +++ b/packages/api-main/src/utility/index.ts @@ -1,7 +1,8 @@ -import crypto from 'node:crypto'; - import type * as T from '../types/index'; +import { Buffer } from 'node:buffer'; +import crypto from 'node:crypto'; + import { sql } from 'drizzle-orm'; import { getDatabase } from '../../drizzle/db'; @@ -10,124 +11,121 @@ import { useConfig } from '../config'; const { AUTH, DISCORD_WEBHOOK_URL } = useConfig(); export function getTransferMessage(messages: Array) { - const msgTransfer = messages.find(msg => msg['@type'] === '/cosmos.bank.v1beta1.MsgSend'); - if (!msgTransfer) { - return null; - } + const msgTransfer = messages.find(msg => msg['@type'] === '/cosmos.bank.v1beta1.MsgSend'); + if (!msgTransfer) { + return null; + } - return msgTransfer as T.MsgTransfer; + return msgTransfer as unknown as T.MsgTransfer; } export function getTransferQuantities(messages: Array, denom = 'uatone') { - const msgTransfers = messages.filter(msg => msg['@type'] === '/cosmos.bank.v1beta1.MsgSend') as T.MsgTransfer[]; - let amount = BigInt('0'); + const msgTransfers = messages.filter(msg => msg['@type'] === '/cosmos.bank.v1beta1.MsgSend') as unknown as T.MsgTransfer[]; + let amount = BigInt('0'); - for (const msg of msgTransfers) { - for (const quantity of msg.amount) { - if (quantity.denom !== denom) { - continue; - } + for (const msg of msgTransfers) { + for (const quantity of msg.amount) { + if (quantity.denom !== denom) { + continue; + } - amount += BigInt(quantity.amount); - } + amount += BigInt(quantity.amount); } + } - return amount.toString(); + return amount.toString(); } export function isReaderAuthorizationValid(headers: Record) { - if (!headers['authorization']) { - return false; + if (!headers.authorization) { + return false; + } + + try { + const authHeaderBuffer = Buffer.from(headers.authorization, 'utf8'); + const authSecretBuffer = Buffer.from(AUTH, 'utf8'); + if (authHeaderBuffer.length !== authSecretBuffer.length) { + return false; } - try { - const authHeaderBuffer = Buffer.from(headers['authorization'], 'utf8'); - const authSecretBuffer = Buffer.from(AUTH, 'utf8'); - if (authHeaderBuffer.length !== authSecretBuffer.length) { - return false; - } - - return crypto.timingSafeEqual(authHeaderBuffer, authSecretBuffer); - } - catch (error) { - console.error('Error during authorization validation:', error); - return false; - } + return crypto.timingSafeEqual(authHeaderBuffer, authSecretBuffer); + } catch (error) { + console.error('Error during authorization validation:', error); + return false; + } } export async function getJsonbArrayCount(hash: string, tableName: string) { - const result = await getDatabase().execute(sql` + const result = await getDatabase().execute(sql` SELECT jsonb_array_length(data)::integer AS array_count FROM ${tableName} WHERE hash = ${hash} `); - return result.rows.length > 0 ? result.rows[0].array_count : 0; + return result.rows.length > 0 ? result.rows[0].array_count : 0; } export function getRequestIP(request: Request) { - const forwardedFor = request.headers.get('x-forwarded-for'); - if (forwardedFor) { - return forwardedFor.split(',')[0].trim(); - } - - const realIp = request.headers.get('x-real-ip'); - if (realIp) { - return realIp; - } - - const flyClientIP = request.headers.get('fly-client-ip'); - if (flyClientIP) { - return flyClientIP; - } - - const cfIp = request.headers.get('cf-connecting-ip'); - if (cfIp) { - return cfIp; - } - - // We'll just default to `host` if not found - return request.headers.get('host') ?? 'localhost:3000'; + const forwardedFor = request.headers.get('x-forwarded-for'); + if (forwardedFor) { + return forwardedFor.split(',')[0].trim(); + } + + const realIp = request.headers.get('x-real-ip'); + if (realIp) { + return realIp; + } + + const flyClientIP = request.headers.get('fly-client-ip'); + if (flyClientIP) { + return flyClientIP; + } + + const cfIp = request.headers.get('cf-connecting-ip'); + if (cfIp) { + return cfIp; + } + + // We'll just default to `host` if not found + return request.headers.get('host') ?? 'localhost:3000'; } export async function postToDiscord(content: string, url: string) { - if (DISCORD_WEBHOOK_URL == '') { - console.log(`DISCORD_WEBHOOK_URL was not provided.`); - return; - } - - const payload = { - content: '', - username: 'dither.chat', - embeds: [ - { - title: 'New Activity', - description: content, - color: 3447003, - url, - }, - ], - }; - - try { - const response = await fetch(DISCORD_WEBHOOK_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }); - - if (response.ok) { - console.log('Discord webhook message sent successfully! 🎉'); - } - else { - console.error(`Failed to send Discord message. Status: ${response.status}`); - const errorText = await response.text(); - console.error('Response body:', errorText); - } - } - catch (error) { - console.error('An error occurred during the fetch request:', error); + if (!DISCORD_WEBHOOK_URL) { + console.log(`DISCORD_WEBHOOK_URL was not provided.`); + return; + } + + const payload = { + content: '', + username: 'dither.chat', + embeds: [ + { + title: 'New Activity', + description: content, + color: 3447003, + url, + }, + ], + }; + + try { + const response = await fetch(DISCORD_WEBHOOK_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (response.ok) { + console.log('Discord webhook message sent successfully! 🎉'); + } else { + console.error(`Failed to send Discord message. Status: ${response.status}`); + const errorText = await response.text(); + console.error('Response body:', errorText); } + } catch (error) { + console.error('An error occurred during the fetch request:', error); + } } diff --git a/packages/api-main/tests/auth.test.ts b/packages/api-main/tests/auth.test.ts index 341a28dd..53c6571b 100644 --- a/packages/api-main/tests/auth.test.ts +++ b/packages/api-main/tests/auth.test.ts @@ -5,28 +5,28 @@ import { assert, describe, it } from 'vitest'; import { createWallet, get, post, signADR36Document } from './shared'; describe('v1/auth', async () => { - const walletA = await createWallet(); - - it('create and verify request for wallet', async () => { - const body: typeof Posts.AuthCreateBody.static = { - address: walletA.publicKey, - }; - - const response = (await post(`auth-create`, body)) as { status: 200; id: number; message: string }; - assert.isOk(response?.status === 200, 'response was not okay'); - - const signData = await signADR36Document(walletA.mnemonic, response.message); - const verifyBody: typeof Posts.AuthBody.static & { json?: boolean } = { - id: response.id, - ...signData.signature, - json: true, - }; - - const responseVerifyCreated = (await post(`auth`, verifyBody)) as { status: 200; bearer: string }; - assert.isOk(responseVerifyCreated?.status === 200, 'response was not verified and confirmed okay'); - assert.isOk(responseVerifyCreated.bearer.length >= 1, 'bearer was not passed back'); - - const responseVerify = await get('auth-verify', responseVerifyCreated.bearer) as { status: number }; - assert.isOk(responseVerify.status === 200, 'could not verify through auth-verify endpoint, invalid cookie?'); - }); + const walletA = await createWallet(); + + it('create and verify request for wallet', async () => { + const body: typeof Posts.AuthCreateBody.static = { + address: walletA.publicKey, + }; + + const response = (await post(`auth-create`, body)) as { status: 200; id: number; message: string }; + assert.isOk(response?.status === 200, 'response was not okay'); + + const signData = await signADR36Document(walletA.mnemonic, response.message); + const verifyBody: typeof Posts.AuthBody.static & { json?: boolean } = { + id: response.id, + ...signData.signature, + json: true, + }; + + const responseVerifyCreated = (await post(`auth`, verifyBody)) as { status: 200; bearer: string }; + assert.isOk(responseVerifyCreated?.status === 200, 'response was not verified and confirmed okay'); + assert.isOk(responseVerifyCreated.bearer.length >= 1, 'bearer was not passed back'); + + const responseVerify = await get('auth-verify', responseVerifyCreated.bearer) as { status: number }; + assert.isOk(responseVerify.status === 200, 'could not verify through auth-verify endpoint, invalid cookie?'); + }); }); diff --git a/packages/api-main/tests/feed.test.ts b/packages/api-main/tests/feed.test.ts index 78d25ebf..90375689 100644 --- a/packages/api-main/tests/feed.test.ts +++ b/packages/api-main/tests/feed.test.ts @@ -5,229 +5,229 @@ import { assert, describe, it } from 'vitest'; import { createWallet, get, getRandomHash, post } from './shared'; describe('filter post depending on send tokens', async () => { - const walletB = await createWallet(); - const cheapPostMessage = 'cheap post'; - const expensivePostMessage = 'expensive post'; - const expensivePostTokens = '20'; - - it('user creates one cheap and one expensive posts', async () => { - const body: typeof Posts.PostBody.static = { - from: walletB.publicKey, - hash: getRandomHash(), - msg: cheapPostMessage, - quantity: '1', - timestamp: '2025-04-16T19:46:42Z', - }; - - let postResponse = await post(`post`, body); - - const expensiveBody: typeof Posts.PostBody.static = { - from: walletB.publicKey, - hash: getRandomHash(), - msg: expensivePostMessage, - quantity: expensivePostTokens, - timestamp: '2025-04-16T19:46:42Z', - }; - - postResponse = await post(`post`, expensiveBody); - assert.isOk(postResponse != null); - assert.isOk(postResponse && postResponse.status === 200, 'response was not okay'); - }); - - it('get feed without filtering by tokens', async () => { - const readResponse = await get<{ - status: number; - rows: { - hash: string; - author: string; - message: string; - deleted_at: Date; - deleted_reason: string; - deleted_hash: string; - }[]; - }>(`feed`); - assert.isOk(readResponse?.status === 200, `response was not okay, got ${readResponse?.status}`); - assert.isOk(readResponse.rows.length >= 2); - }); - - it('filtering expensive posts', async () => { - const readResponse = await get<{ - status: number; - rows: { - hash: string; - author: string; - message: string; - deleted_at: Date; - deleted_reason: string; - deleted_hash: string; - }[]; - }>(`feed?minQuantity=${expensivePostTokens}`); - assert.isOk(readResponse?.status === 200, `response was not okay, got ${readResponse?.status}`); - assert.lengthOf(readResponse.rows, 1); - }); - - it('Search: filtering cheap posts', async () => { - let readResponse = await get<{ - status: number; - rows: { - hash: string; - author: string; - message: string; - deleted_at: Date; - deleted_reason: string; - deleted_hash: string; - }[]; - }>(`search?text="${cheapPostMessage}"&minQuantity=1`); - assert.isOk(readResponse?.status === 200, `response was not okay, got ${readResponse?.status}`); - assert.lengthOf(readResponse.rows, 1); - - readResponse = await get<{ - status: number; - rows: { - hash: string; - author: string; - message: string; - deleted_at: Date; - deleted_reason: string; - deleted_hash: string; - }[]; - }>(`search?text="${cheapPostMessage}"&minQuantity=${expensivePostTokens}`); - assert.isOk(readResponse?.status === 200, `response was not okay, got ${readResponse?.status}`); - assert.lengthOf(readResponse.rows, 0); - }); + const walletB = await createWallet(); + const cheapPostMessage = 'cheap post'; + const expensivePostMessage = 'expensive post'; + const expensivePostTokens = '20'; + + it('user creates one cheap and one expensive posts', async () => { + const body: typeof Posts.PostBody.static = { + from: walletB.publicKey, + hash: getRandomHash(), + msg: cheapPostMessage, + quantity: '1', + timestamp: '2025-04-16T19:46:42Z', + }; + + let postResponse = await post(`post`, body); + + const expensiveBody: typeof Posts.PostBody.static = { + from: walletB.publicKey, + hash: getRandomHash(), + msg: expensivePostMessage, + quantity: expensivePostTokens, + timestamp: '2025-04-16T19:46:42Z', + }; + + postResponse = await post(`post`, expensiveBody); + assert.isOk(postResponse != null); + assert.isOk(postResponse && postResponse.status === 200, 'response was not okay'); + }); + + it('get feed without filtering by tokens', async () => { + const readResponse = await get<{ + status: number; + rows: { + hash: string; + author: string; + message: string; + deleted_at: Date; + deleted_reason: string; + deleted_hash: string; + }[]; + }>(`feed`); + assert.isOk(readResponse?.status === 200, `response was not okay, got ${readResponse?.status}`); + assert.isOk(readResponse.rows.length >= 2); + }); + + it('filtering expensive posts', async () => { + const readResponse = await get<{ + status: number; + rows: { + hash: string; + author: string; + message: string; + deleted_at: Date; + deleted_reason: string; + deleted_hash: string; + }[]; + }>(`feed?minQuantity=${expensivePostTokens}`); + assert.isOk(readResponse?.status === 200, `response was not okay, got ${readResponse?.status}`); + assert.lengthOf(readResponse.rows, 1); + }); + + it('search: filtering cheap posts', async () => { + let readResponse = await get<{ + status: number; + rows: { + hash: string; + author: string; + message: string; + deleted_at: Date; + deleted_reason: string; + deleted_hash: string; + }[]; + }>(`search?text="${cheapPostMessage}"&minQuantity=1`); + assert.isOk(readResponse?.status === 200, `response was not okay, got ${readResponse?.status}`); + assert.lengthOf(readResponse.rows, 1); + + readResponse = await get<{ + status: number; + rows: { + hash: string; + author: string; + message: string; + deleted_at: Date; + deleted_reason: string; + deleted_hash: string; + }[]; + }>(`search?text="${cheapPostMessage}"&minQuantity=${expensivePostTokens}`); + assert.isOk(readResponse?.status === 200, `response was not okay, got ${readResponse?.status}`); + assert.lengthOf(readResponse.rows, 0); + }); }); describe('user replies with parent', async () => { - const walletA = await createWallet(); - const walletB = await createWallet(); - const parentPost = getRandomHash(); - const replyPost = getRandomHash(); - const postMessage = 'this is a post'; - const replyMessage = 'this is a reply'; - - it('POST - /post', async () => { - const body: typeof Posts.PostBody.static = { - from: walletA.publicKey, - hash: parentPost, - msg: postMessage, - quantity: '1', - timestamp: '2025-04-16T19:46:42Z', - }; - - const response = await post(`post`, body); - assert.isOk(response?.status === 200, 'response was not okay'); - }); - - it('POST - /reply', async () => { - const body: typeof Posts.ReplyBody.static = { - from: walletB.publicKey, - hash: replyPost, - post_hash: parentPost, - msg: replyMessage, - quantity: '1', - timestamp: '2025-04-16T19:46:42Z', - }; - - const replyResponse = await post(`reply`, body); - assert.isOk(replyResponse?.status === 200, 'response was not okay'); - }); - - it('Get user replies', async () => { - const userRepliesResponse = await get<{ - status: number; - rows: { - parent: { hash: string; author: string; message: string }; - reply: { hash: string; author: string; message: string }; - }[]; - }>(`user-replies?address=${walletB.publicKey}`); - assert.isOk(userRepliesResponse?.status === 200, `response was not okay, got ${userRepliesResponse?.status}`); - assert.isOk(userRepliesResponse.rows.length >= 1); - assert.equal(userRepliesResponse.rows[0].reply.hash, replyPost); - assert.equal(userRepliesResponse.rows[0].parent.hash, parentPost); - assert.equal(userRepliesResponse.rows[0].reply.message, replyMessage); - assert.equal(userRepliesResponse.rows[0].parent.message, postMessage); - }); + const walletA = await createWallet(); + const walletB = await createWallet(); + const parentPost = getRandomHash(); + const replyPost = getRandomHash(); + const postMessage = 'this is a post'; + const replyMessage = 'this is a reply'; + + it('pOST - /post', async () => { + const body: typeof Posts.PostBody.static = { + from: walletA.publicKey, + hash: parentPost, + msg: postMessage, + quantity: '1', + timestamp: '2025-04-16T19:46:42Z', + }; + + const response = await post(`post`, body); + assert.isOk(response?.status === 200, 'response was not okay'); + }); + + it('pOST - /reply', async () => { + const body: typeof Posts.ReplyBody.static = { + from: walletB.publicKey, + hash: replyPost, + post_hash: parentPost, + msg: replyMessage, + quantity: '1', + timestamp: '2025-04-16T19:46:42Z', + }; + + const replyResponse = await post(`reply`, body); + assert.isOk(replyResponse?.status === 200, 'response was not okay'); + }); + + it('get user replies', async () => { + const userRepliesResponse = await get<{ + status: number; + rows: { + parent: { hash: string; author: string; message: string }; + reply: { hash: string; author: string; message: string }; + }[]; + }>(`user-replies?address=${walletB.publicKey}`); + assert.isOk(userRepliesResponse?.status === 200, `response was not okay, got ${userRepliesResponse?.status}`); + assert.isOk(userRepliesResponse.rows.length >= 1); + assert.equal(userRepliesResponse.rows[0].reply.hash, replyPost); + assert.equal(userRepliesResponse.rows[0].parent.hash, parentPost); + assert.equal(userRepliesResponse.rows[0].reply.message, replyMessage); + assert.equal(userRepliesResponse.rows[0].parent.message, postMessage); + }); }); describe('get post from followed', async () => { - const walletA = await createWallet(); - const walletB = await createWallet(); - const postMessage = 'this is a post'; - - it('zero posts if not followers', async () => { - const readResponse = await get<{ - status: number; - rows: { - hash: string; - author: string; - message: string; - deleted_at: Date; - deleted_reason: string; - deleted_hash: string; - quantity: string; - }[]; - }>(`following-posts?address=${walletA.publicKey}`); - assert.isOk(readResponse?.status === 200, `response was not okay, got ${readResponse?.status}`); - assert.lengthOf(readResponse.rows, 0); - }); - - it('POST - now followed user posts', async () => { - const body: typeof Posts.PostBody.static = { - from: walletB.publicKey, - hash: getRandomHash(), - msg: postMessage, - quantity: '1', - timestamp: '2025-04-16T19:46:42Z', - }; - - const postResponse = await post(`post`, body); - assert.isOk(postResponse != null); - assert.isOk(postResponse && postResponse.status === 200, 'response was not okay'); - }); - - it('Still empty response', async () => { - const readResponse = await get<{ - status: number; - rows: { - hash: string; - author: string; - message: string; - deleted_at: Date; - deleted_reason: string; - deleted_hash: string; - }[]; - }>(`following-posts?address=${walletA.publicKey}`); - assert.isOk(readResponse?.status === 200, `response was not okay, got ${readResponse?.status}`); - assert.lengthOf(readResponse.rows, 0); - }); - - it('One post when user follows', async () => { - const body: typeof Posts.FollowBody.static = { - from: walletA.publicKey, - hash: getRandomHash(), - address: walletB.publicKey, - timestamp: '2025-04-16T19:46:42Z', - }; - - const response = await post(`follow`, body); - assert.isOk(response?.status === 200, 'unable to follow user'); - - const readResponse = await get<{ - status: number; - rows: { - hash: string; - author: string; - message: string; - deleted_at: Date; - deleted_reason: string; - deleted_hash: string; - quantity: string; - }[]; - }>(`following-posts?address=${walletA.publicKey}`); - assert.isOk(readResponse?.status === 200, `response was not okay, got ${readResponse?.status}`); - assert.lengthOf(readResponse.rows, 1); - assert.isOk(readResponse.rows[0].author, 'Author was not included'); - assert.isOk(readResponse.rows[0].message, 'message was not included'); - assert.isOk(readResponse.rows[0].quantity, 'quantity was not included'); - }); + const walletA = await createWallet(); + const walletB = await createWallet(); + const postMessage = 'this is a post'; + + it('zero posts if not followers', async () => { + const readResponse = await get<{ + status: number; + rows: { + hash: string; + author: string; + message: string; + deleted_at: Date; + deleted_reason: string; + deleted_hash: string; + quantity: string; + }[]; + }>(`following-posts?address=${walletA.publicKey}`); + assert.isOk(readResponse?.status === 200, `response was not okay, got ${readResponse?.status}`); + assert.lengthOf(readResponse.rows, 0); + }); + + it('pOST - now followed user posts', async () => { + const body: typeof Posts.PostBody.static = { + from: walletB.publicKey, + hash: getRandomHash(), + msg: postMessage, + quantity: '1', + timestamp: '2025-04-16T19:46:42Z', + }; + + const postResponse = await post(`post`, body); + assert.isOk(postResponse != null); + assert.isOk(postResponse && postResponse.status === 200, 'response was not okay'); + }); + + it('still empty response', async () => { + const readResponse = await get<{ + status: number; + rows: { + hash: string; + author: string; + message: string; + deleted_at: Date; + deleted_reason: string; + deleted_hash: string; + }[]; + }>(`following-posts?address=${walletA.publicKey}`); + assert.isOk(readResponse?.status === 200, `response was not okay, got ${readResponse?.status}`); + assert.lengthOf(readResponse.rows, 0); + }); + + it('one post when user follows', async () => { + const body: typeof Posts.FollowBody.static = { + from: walletA.publicKey, + hash: getRandomHash(), + address: walletB.publicKey, + timestamp: '2025-04-16T19:46:42Z', + }; + + const response = await post(`follow`, body); + assert.isOk(response?.status === 200, 'unable to follow user'); + + const readResponse = await get<{ + status: number; + rows: { + hash: string; + author: string; + message: string; + deleted_at: Date; + deleted_reason: string; + deleted_hash: string; + quantity: string; + }[]; + }>(`following-posts?address=${walletA.publicKey}`); + assert.isOk(readResponse?.status === 200, `response was not okay, got ${readResponse?.status}`); + assert.lengthOf(readResponse.rows, 1); + assert.isOk(readResponse.rows[0].author, 'Author was not included'); + assert.isOk(readResponse.rows[0].message, 'message was not included'); + assert.isOk(readResponse.rows[0].quantity, 'quantity was not included'); + }); }); diff --git a/packages/api-main/tests/follow.test.ts b/packages/api-main/tests/follow.test.ts index 4ac27734..f7b9d221 100644 --- a/packages/api-main/tests/follow.test.ts +++ b/packages/api-main/tests/follow.test.ts @@ -5,100 +5,100 @@ import { assert, describe, it } from 'vitest'; import { get, getAtomOneAddress, getRandomHash, post } from './shared'; describe('follows', async () => { - const addressUserA = getAtomOneAddress(); - const addressUserB = getAtomOneAddress(); - const addressUserC = getAtomOneAddress(); - - // Follows - it('POST - /follow', async () => { - const body: typeof Posts.FollowBody.static = { - from: addressUserA, - hash: getRandomHash(), - address: addressUserB, - timestamp: '2025-04-16T19:46:42Z', - }; - - const response = await post(`follow`, body); - assert.isOk(response?.status === 200, 'response was not okay'); - }); - - it('GET - /is-following', async () => { - let response = await get<{ status: number; rows: { hash: string; address: string }[] }>( - `is-following?follower=${addressUserA}&following=${addressUserB}`, - ); - - assert.isOk(response?.status === 200, 'follower was not found, should have follower'); - response = await get<{ status: number; rows: { hash: string; address: string }[] }>( - `is-following?follower=${addressUserA}&following=${addressUserC}`, - ); - - assert.isOk(response?.status === 404, 'follower was found when follower should not be following anyone'); - }); - - it('POST - /follow - no duplicates', async () => { - const body: typeof Posts.FollowBody.static = { - hash: getRandomHash(), - from: addressUserA, - address: addressUserB, - timestamp: '2025-04-16T19:46:42Z', - }; - - const response = await post(`follow`, body, 'WRITE'); - assert.isOk(response?.status === 400, 'additional follow was allowed somehow'); - }); - - it('GET - /following', async () => { - const response = await get<{ status: number; rows: { hash: string; address: string }[] }>( - `following?address=${addressUserA}`, - ); - - assert.isOk(response && Array.isArray(response.rows), 'following response was not an array'); - assert.isOk(response && response.rows.find(x => x.address === addressUserB)); - }); - - it('GET - /followers', async () => { - const response = await get<{ status: number; rows: { hash: string; address: string }[] }>( - `followers?address=${addressUserB}`, - ); - - assert.isOk(response && Array.isArray(response.rows), 'following response was not an array'); - assert.isOk(response && response.rows.find(x => x.address === addressUserA)); - }); - - // Unfollow - it('POST - /unfollow', async () => { - const body: typeof Posts.UnfollowBody.static = { - hash: getRandomHash(), - from: addressUserA, - address: addressUserB, - timestamp: '2025-04-16T19:46:42Z', - }; - - const response = await post(`unfollow`, body); - assert.isOk(response?.status === 200, 'response was not okay'); - }); - - it('GET - /is-following (Not Following)', async () => { - const response = await get<{ status: number; rows: { hash: string; address: string }[] }>( - `is-following?follower=${addressUserA}&following=${addressUserB}`, - ); - - assert.isOk(response?.status === 404, 'follower was found, should not have follower'); - }); - - it('GET - /followers', async () => { - const response = await get<{ status: number; rows: { hash: string; address: string }[] }>( - `followers?address=${addressUserB}`, - ); - assert.isOk(response && Array.isArray(response.rows), 'followers response was not an array'); - assert.isOk(response && response.rows.length <= 0, 'did not unfollow all users'); - }); - - it('GET - /following', async () => { - const response = await get<{ status: number; rows: { hash: string; address: string }[] }>( - `following?address=${addressUserA}`, - ); - assert.isOk(response && Array.isArray(response.rows), 'following response was not an array'); - assert.isOk(response && response.rows.length <= 0, 'did not unfollow all users'); - }); + const addressUserA = getAtomOneAddress(); + const addressUserB = getAtomOneAddress(); + const addressUserC = getAtomOneAddress(); + + // Follows + it('pOST - /follow', async () => { + const body: typeof Posts.FollowBody.static = { + from: addressUserA, + hash: getRandomHash(), + address: addressUserB, + timestamp: '2025-04-16T19:46:42Z', + }; + + const response = await post(`follow`, body); + assert.isOk(response?.status === 200, 'response was not okay'); + }); + + it('gET - /is-following', async () => { + let response = await get<{ status: number; rows: { hash: string; address: string }[] }>( + `is-following?follower=${addressUserA}&following=${addressUserB}`, + ); + + assert.isOk(response?.status === 200, 'follower was not found, should have follower'); + response = await get<{ status: number; rows: { hash: string; address: string }[] }>( + `is-following?follower=${addressUserA}&following=${addressUserC}`, + ); + + assert.isOk(response?.status === 404, 'follower was found when follower should not be following anyone'); + }); + + it('pOST - /follow - no duplicates', async () => { + const body: typeof Posts.FollowBody.static = { + hash: getRandomHash(), + from: addressUserA, + address: addressUserB, + timestamp: '2025-04-16T19:46:42Z', + }; + + const response = await post(`follow`, body, 'WRITE'); + assert.isOk(response?.status === 400, 'additional follow was allowed somehow'); + }); + + it('gET - /following', async () => { + const response = await get<{ status: number; rows: { hash: string; address: string }[] }>( + `following?address=${addressUserA}`, + ); + + assert.isOk(response && Array.isArray(response.rows), 'following response was not an array'); + assert.isOk(response && response.rows.find(x => x.address === addressUserB)); + }); + + it('gET - /followers', async () => { + const response = await get<{ status: number; rows: { hash: string; address: string }[] }>( + `followers?address=${addressUserB}`, + ); + + assert.isOk(response && Array.isArray(response.rows), 'following response was not an array'); + assert.isOk(response && response.rows.find(x => x.address === addressUserA)); + }); + + // Unfollow + it('pOST - /unfollow', async () => { + const body: typeof Posts.UnfollowBody.static = { + hash: getRandomHash(), + from: addressUserA, + address: addressUserB, + timestamp: '2025-04-16T19:46:42Z', + }; + + const response = await post(`unfollow`, body); + assert.isOk(response?.status === 200, 'response was not okay'); + }); + + it('gET - /is-following (Not Following)', async () => { + const response = await get<{ status: number; rows: { hash: string; address: string }[] }>( + `is-following?follower=${addressUserA}&following=${addressUserB}`, + ); + + assert.isOk(response?.status === 404, 'follower was found, should not have follower'); + }); + + it('gET - /followers - user B', async () => { + const response = await get<{ status: number; rows: { hash: string; address: string }[] }>( + `followers?address=${addressUserB}`, + ); + assert.isOk(response && Array.isArray(response.rows), 'followers response was not an array'); + assert.isOk(response && response.rows.length <= 0, 'did not unfollow all users'); + }); + + it('gET - /following - empty', async () => { + const response = await get<{ status: number; rows: { hash: string; address: string }[] }>( + `following?address=${addressUserA}`, + ); + assert.isOk(response && Array.isArray(response.rows), 'following response was not an array'); + assert.isOk(response && response.rows.length <= 0, 'did not unfollow all users'); + }); }); diff --git a/packages/api-main/tests/moderator.test.ts b/packages/api-main/tests/moderator.test.ts index 0ee248e5..44b9a11a 100644 --- a/packages/api-main/tests/moderator.test.ts +++ b/packages/api-main/tests/moderator.test.ts @@ -4,310 +4,309 @@ import { assert, describe, it } from 'vitest'; import { getDatabase } from '../drizzle/db'; import { ModeratorTable } from '../drizzle/schema'; - import { createWallet, get, getAtomOneAddress, getRandomHash, post, signADR36Document } from './shared'; describe('v1 - mod', { sequential: true }, () => { - const addressUserA = getAtomOneAddress(); - let addressModerator = getAtomOneAddress(); - const genericPostMessage - = 'hello world, this is a really intereresting post $@!($)@!()@!$21,4214,12,42142,14,12,421,'; - const postHash = getRandomHash(); - const secondPostHash = getRandomHash(); - let bearerToken: string; - - it('POST mod obtain bearer token', async () => { - const walletA = await createWallet(); - addressModerator = walletA.publicKey; - const body: typeof Posts.AuthCreateBody.static = { - address: walletA.publicKey, - }; - - const response = (await post(`auth-create`, body)) as { status: 200; id: number; message: string }; - assert.isOk(response?.status === 200, 'response was not okay'); - - const signData = await signADR36Document(walletA.mnemonic, response.message); - const verifyBody: typeof Posts.AuthBody.static & { json?: boolean } = { - id: response.id, - ...signData.signature, - json: true, - }; - - const responseVerify = (await post(`auth`, verifyBody)) as { status: 200; bearer: string }; - assert.isOk(responseVerify?.status === 200, 'response was not verified and confirmed okay'); - assert.isOk(responseVerify.bearer.length >= 1, 'bearer was not passed back'); - bearerToken = responseVerify.bearer; - }); - - it('POST - /post', async () => { - const body: typeof Posts.PostBody.static = { - from: addressUserA, - hash: postHash, - msg: genericPostMessage, - quantity: '1', - timestamp: '2025-04-16T19:46:42Z', - }; - - const response = await post(`post`, body); - assert.isOk(response?.status === 200, 'response was not okay'); - }); - - it('POST - /mod/post-remove without autorization', async () => { - const body: typeof Posts.ModRemovePostBody.static = { - hash: getRandomHash(), - timestamp: '2025-04-16T19:46:42Z', - post_hash: postHash, - reason: 'spam', - }; - - const replyResponse = await post(`mod/post-remove`, body); - assert.isOk(replyResponse?.status === 401, `expected unauthorized, got ${JSON.stringify(replyResponse)}`); - }); - - it('POST - /mod/post-remove moderator does not exists', async () => { - const response = await get<{ status: number; rows: { hash: string; author: string; message: string }[] }>( - `posts?address=${addressUserA}`, - ); - assert.isOk(response, 'failed to fetch posts data'); - assert.isOk(Array.isArray(response.rows) && response.rows.length >= 1, 'feed result was not an array type'); - - const body: typeof Posts.ModRemovePostBody.static = { - hash: getRandomHash(), - timestamp: '2025-04-16T19:46:42Z', - post_hash: response.rows[0].hash, - reason: 'spam', - }; - - const replyResponse = await post(`mod/post-remove`, body, bearerToken); - assert.isOk(replyResponse?.status === 404, `expected moderator was not found`); - - const postsResponse = await get<{ - status: number; - rows: { - hash: string; - author: string; - message: string; - deleted_at: Date; - deleted_reason: string; - deleted_hash: string; - }[]; - }>(`posts?address=${addressUserA}`); - - assert.isOk(postsResponse?.status === 200, 'posts did not resolve'); - const data = postsResponse?.rows.find(x => x.hash === response.rows[0].hash); - assert.isOk(data, 'data was hidden'); - }); - - it('POST - /mod/post-remove moderator exists', async () => { - await getDatabase() - .insert(ModeratorTable) - .values({ - address: addressModerator, - alias: 'mod', - }) - .execute(); - const response = await get<{ status: number; rows: { hash: string; author: string; message: string }[] }>( - `posts?address=${addressUserA}`, - ); - assert.isOk(response, 'failed to fetch posts data'); - assert.isOk(Array.isArray(response.rows) && response.rows.length >= 1, 'feed result was not an array type'); - - const body: typeof Posts.ModRemovePostBody.static = { - hash: getRandomHash(), - timestamp: '2025-04-16T19:46:42Z', - post_hash: response.rows[0].hash, - reason: 'spam', - }; - - const replyResponse = await post(`mod/post-remove`, body, bearerToken); - assert.isOk(replyResponse?.status === 200, `response was not okay, got ${JSON.stringify(replyResponse)}`); - - const postsResponse = await get<{ - status: number; - rows: { - hash: string; - author: string; - message: string; - deleted_at: Date; - deleted_reason: string; - deleted_hash: string; - }[]; - }>(`posts?address=${addressUserA}`); - - assert.isOk(postsResponse?.status === 200, 'posts did not resolve'); - const data = postsResponse?.rows.find(x => x.hash === response.rows[0].hash); - assert.isUndefined(data, 'data was not hidden'); - }); - - it('POST - /mod/post-restore', async () => { - const body: typeof Posts.ModRemovePostBody.static = { - hash: getRandomHash(), - timestamp: '2025-04-16T19:46:42Z', - post_hash: postHash, - reason: 'spam', - }; - - const replyResponse = await post(`mod/post-restore`, body, bearerToken); - assert.isOk(replyResponse?.status === 200, `response was not okay, got ${JSON.stringify(replyResponse)}`); - - const postsResponse = await get<{ - status: number; - rows: { - hash: string; - author: string; - message: string; - deleted_at: Date; - deleted_reason: string; - deleted_hash: string; - }[]; - }>(`posts?address=${addressUserA}`); - - assert.isOk(postsResponse?.status === 200, 'posts did not resolve'); - const data = postsResponse?.rows.find(x => x.hash === postHash); - assert.isOk(data, 'data is hidden'); - }); - - it('POST - /mod/post-restore on an user deleted post', async () => { - // USER REMOVES POST - const body: typeof Posts.PostRemoveBody.static = { - from: addressUserA, - hash: getRandomHash(), - timestamp: '2025-04-16T19:46:42Z', - post_hash: postHash, - }; - - const userRemoveResponse = await post(`post-remove`, body, bearerToken); - assert.isOk(userRemoveResponse?.status === 200, 'response was not okay'); - - // MOD tries to restore post - const bodymod: typeof Posts.ModRemovePostBody.static = { - hash: getRandomHash(), - timestamp: '2025-04-16T19:46:42Z', - post_hash: postHash, - reason: 'spam', - }; - - const replyResponse = await post(`mod/post-restore`, bodymod); - assert.isOk(replyResponse?.status === 401, `response was not okay, expected unauthorized`); - }); - - it('POST - /post user creates a second post', async () => { - const body: typeof Posts.PostBody.static = { - from: addressUserA, - hash: secondPostHash, - msg: genericPostMessage, - quantity: '1', - timestamp: '2025-04-16T19:46:42Z', - }; - - const response = await post(`post`, body); - assert.isOk(response?.status === 200, 'response was not okay'); - }); - - it('POST - /mod/ban user banned deletes posts', async () => { - // moderator bans user - const body: typeof Posts.ModBanBody.static = { - hash: getRandomHash(), - timestamp: '2025-04-16T19:46:42Z', - user_address: addressUserA, - reason: 'user too political', - }; - - const userBanResponse = await post(`mod/ban`, body, bearerToken); - assert.isOk(userBanResponse?.status === 200, `response was not okay ${JSON.stringify(userBanResponse)}`); - - // post from user should be all hidden - const postsResponse = await get<{ - status: number; - rows: { - hash: string; - author: string; - message: string; - deleted_at: Date; - deleted_reason: string; - deleted_hash: string; - }[]; - }>(`posts?address=${addressUserA}`); - - assert.isOk(postsResponse?.status === 200, 'posts did not resolve'); - assert.isOk( - Array.isArray(postsResponse.rows) && postsResponse.rows.length == 0, - 'some of the user posts are shown', - ); - }); - - it('POST - banned user publishes post is deleted automatically', async () => { - const body: typeof Posts.PostBody.static = { - from: addressUserA, - hash: getRandomHash(), - msg: genericPostMessage, - quantity: '1', - timestamp: '2025-04-16T19:46:42Z', - }; - - const response = await post(`post`, body, bearerToken); - assert.isOk(response?.status === 200, 'response was not okay'); - - // Even new post should be hidden - const postsResponse = await get<{ - status: number; - rows: { - hash: string; - author: string; - message: string; - deleted_at: Date; - deleted_reason: string; - deleted_hash: string; - }[]; - }>(`posts?address=${addressUserA}`); - - assert.isOk(postsResponse?.status === 200, 'posts did not resolve'); - assert.isOk( - Array.isArray(postsResponse.rows) && postsResponse.rows.length == 0, - 'some of the user posts are shown', - ); - }); - - it('POST - unban restore all posts but user deleted ones', async () => { - const body: typeof Posts.ModBanBody.static = { - hash: getRandomHash(), - timestamp: '2025-04-16T19:46:42Z', - user_address: addressUserA, - reason: 'user too political', - }; - - const userBanResponse = await post(`mod/unban`, body, bearerToken); - assert.isOk(userBanResponse?.status === 200, `response was not okay ${JSON.stringify(userBanResponse)}`); - }); - - it('POST - freshly unbanned user publishes without problems', async () => { - const newPostHash = getRandomHash(); - const body: typeof Posts.PostBody.static = { - from: addressUserA, - hash: newPostHash, - msg: genericPostMessage, - quantity: '1', - timestamp: '2025-04-16T19:46:42Z', - }; - - const response = await post(`post`, body, bearerToken); - assert.isOk(response?.status === 200, 'response was not okay'); - - // Even new post should be hidden - const postsResponse = await get<{ - status: number; - rows: { - hash: string; - author: string; - message: string; - deleted_at: Date; - deleted_reason: string; - deleted_hash: string; - }[]; - }>(`posts?address=${addressUserA}`); - - assert.isOk(postsResponse?.status === 200, 'posts did not resolve'); - const data = postsResponse?.rows.find(x => x.hash === newPostHash); - assert.isOk(data, 'New post was hidden'); - }); + const addressUserA = getAtomOneAddress(); + let addressModerator = getAtomOneAddress(); + const genericPostMessage + = 'hello world, this is a really intereresting post $@!($)@!()@!$21,4214,12,42142,14,12,421,'; + const postHash = getRandomHash(); + const secondPostHash = getRandomHash(); + let bearerToken: string; + + it('pOST mod obtain bearer token', async () => { + const walletA = await createWallet(); + addressModerator = walletA.publicKey; + const body: typeof Posts.AuthCreateBody.static = { + address: walletA.publicKey, + }; + + const response = (await post(`auth-create`, body)) as { status: 200; id: number; message: string }; + assert.isOk(response?.status === 200, 'response was not okay'); + + const signData = await signADR36Document(walletA.mnemonic, response.message); + const verifyBody: typeof Posts.AuthBody.static & { json?: boolean } = { + id: response.id, + ...signData.signature, + json: true, + }; + + const responseVerify = (await post(`auth`, verifyBody)) as { status: 200; bearer: string }; + assert.isOk(responseVerify?.status === 200, 'response was not verified and confirmed okay'); + assert.isOk(responseVerify.bearer.length >= 1, 'bearer was not passed back'); + bearerToken = responseVerify.bearer; + }); + + it('pOST - /post', async () => { + const body: typeof Posts.PostBody.static = { + from: addressUserA, + hash: postHash, + msg: genericPostMessage, + quantity: '1', + timestamp: '2025-04-16T19:46:42Z', + }; + + const response = await post(`post`, body); + assert.isOk(response?.status === 200, 'response was not okay'); + }); + + it('pOST - /mod/post-remove without autorization', async () => { + const body: typeof Posts.ModRemovePostBody.static = { + hash: getRandomHash(), + timestamp: '2025-04-16T19:46:42Z', + post_hash: postHash, + reason: 'spam', + }; + + const replyResponse = await post(`mod/post-remove`, body); + assert.isOk(replyResponse?.status === 401, `expected unauthorized, got ${JSON.stringify(replyResponse)}`); + }); + + it('pOST - /mod/post-remove moderator does not exists', async () => { + const response = await get<{ status: number; rows: { hash: string; author: string; message: string }[] }>( + `posts?address=${addressUserA}`, + ); + assert.isOk(response, 'failed to fetch posts data'); + assert.isOk(Array.isArray(response.rows) && response.rows.length >= 1, 'feed result was not an array type'); + + const body: typeof Posts.ModRemovePostBody.static = { + hash: getRandomHash(), + timestamp: '2025-04-16T19:46:42Z', + post_hash: response.rows[0].hash, + reason: 'spam', + }; + + const replyResponse = await post(`mod/post-remove`, body, bearerToken); + assert.isOk(replyResponse?.status === 404, `expected moderator was not found`); + + const postsResponse = await get<{ + status: number; + rows: { + hash: string; + author: string; + message: string; + deleted_at: Date; + deleted_reason: string; + deleted_hash: string; + }[]; + }>(`posts?address=${addressUserA}`); + + assert.isOk(postsResponse?.status === 200, 'posts did not resolve'); + const data = postsResponse?.rows.find(x => x.hash === response.rows[0].hash); + assert.isOk(data, 'data was hidden'); + }); + + it('pOST - /mod/post-remove moderator exists', async () => { + await getDatabase() + .insert(ModeratorTable) + .values({ + address: addressModerator, + alias: 'mod', + }) + .execute(); + const response = await get<{ status: number; rows: { hash: string; author: string; message: string }[] }>( + `posts?address=${addressUserA}`, + ); + assert.isOk(response, 'failed to fetch posts data'); + assert.isOk(Array.isArray(response.rows) && response.rows.length >= 1, 'feed result was not an array type'); + + const body: typeof Posts.ModRemovePostBody.static = { + hash: getRandomHash(), + timestamp: '2025-04-16T19:46:42Z', + post_hash: response.rows[0].hash, + reason: 'spam', + }; + + const replyResponse = await post(`mod/post-remove`, body, bearerToken); + assert.isOk(replyResponse?.status === 200, `response was not okay, got ${JSON.stringify(replyResponse)}`); + + const postsResponse = await get<{ + status: number; + rows: { + hash: string; + author: string; + message: string; + deleted_at: Date; + deleted_reason: string; + deleted_hash: string; + }[]; + }>(`posts?address=${addressUserA}`); + + assert.isOk(postsResponse?.status === 200, 'posts did not resolve'); + const data = postsResponse?.rows.find(x => x.hash === response.rows[0].hash); + assert.isUndefined(data, 'data was not hidden'); + }); + + it('pOST - /mod/post-restore', async () => { + const body: typeof Posts.ModRemovePostBody.static = { + hash: getRandomHash(), + timestamp: '2025-04-16T19:46:42Z', + post_hash: postHash, + reason: 'spam', + }; + + const replyResponse = await post(`mod/post-restore`, body, bearerToken); + assert.isOk(replyResponse?.status === 200, `response was not okay, got ${JSON.stringify(replyResponse)}`); + + const postsResponse = await get<{ + status: number; + rows: { + hash: string; + author: string; + message: string; + deleted_at: Date; + deleted_reason: string; + deleted_hash: string; + }[]; + }>(`posts?address=${addressUserA}`); + + assert.isOk(postsResponse?.status === 200, 'posts did not resolve'); + const data = postsResponse?.rows.find(x => x.hash === postHash); + assert.isOk(data, 'data is hidden'); + }); + + it('pOST - /mod/post-restore on an user deleted post', async () => { + // USER REMOVES POST + const body: typeof Posts.PostRemoveBody.static = { + from: addressUserA, + hash: getRandomHash(), + timestamp: '2025-04-16T19:46:42Z', + post_hash: postHash, + }; + + const userRemoveResponse = await post(`post-remove`, body, bearerToken); + assert.isOk(userRemoveResponse?.status === 200, 'response was not okay'); + + // MOD tries to restore post + const bodymod: typeof Posts.ModRemovePostBody.static = { + hash: getRandomHash(), + timestamp: '2025-04-16T19:46:42Z', + post_hash: postHash, + reason: 'spam', + }; + + const replyResponse = await post(`mod/post-restore`, bodymod); + assert.isOk(replyResponse?.status === 401, `response was not okay, expected unauthorized`); + }); + + it('pOST - /post user creates a second post', async () => { + const body: typeof Posts.PostBody.static = { + from: addressUserA, + hash: secondPostHash, + msg: genericPostMessage, + quantity: '1', + timestamp: '2025-04-16T19:46:42Z', + }; + + const response = await post(`post`, body); + assert.isOk(response?.status === 200, 'response was not okay'); + }); + + it('pOST - /mod/ban user banned deletes posts', async () => { + // moderator bans user + const body: typeof Posts.ModBanBody.static = { + hash: getRandomHash(), + timestamp: '2025-04-16T19:46:42Z', + user_address: addressUserA, + reason: 'user too political', + }; + + const userBanResponse = await post(`mod/ban`, body, bearerToken); + assert.isOk(userBanResponse?.status === 200, `response was not okay ${JSON.stringify(userBanResponse)}`); + + // post from user should be all hidden + const postsResponse = await get<{ + status: number; + rows: { + hash: string; + author: string; + message: string; + deleted_at: Date; + deleted_reason: string; + deleted_hash: string; + }[]; + }>(`posts?address=${addressUserA}`); + + assert.isOk(postsResponse?.status === 200, 'posts did not resolve'); + assert.isOk( + Array.isArray(postsResponse.rows) && postsResponse.rows.length === 0, + 'some of the user posts are shown', + ); + }); + + it('pOST - banned user publishes post is deleted automatically', async () => { + const body: typeof Posts.PostBody.static = { + from: addressUserA, + hash: getRandomHash(), + msg: genericPostMessage, + quantity: '1', + timestamp: '2025-04-16T19:46:42Z', + }; + + const response = await post(`post`, body, bearerToken); + assert.isOk(response?.status === 200, 'response was not okay'); + + // Even new post should be hidden + const postsResponse = await get<{ + status: number; + rows: { + hash: string; + author: string; + message: string; + deleted_at: Date; + deleted_reason: string; + deleted_hash: string; + }[]; + }>(`posts?address=${addressUserA}`); + + assert.isOk(postsResponse?.status === 200, 'posts did not resolve'); + assert.isOk( + Array.isArray(postsResponse.rows) && postsResponse.rows.length === 0, + 'some of the user posts are shown', + ); + }); + + it('pOST - unban restore all posts but user deleted ones', async () => { + const body: typeof Posts.ModBanBody.static = { + hash: getRandomHash(), + timestamp: '2025-04-16T19:46:42Z', + user_address: addressUserA, + reason: 'user too political', + }; + + const userBanResponse = await post(`mod/unban`, body, bearerToken); + assert.isOk(userBanResponse?.status === 200, `response was not okay ${JSON.stringify(userBanResponse)}`); + }); + + it('pOST - freshly unbanned user publishes without problems', async () => { + const newPostHash = getRandomHash(); + const body: typeof Posts.PostBody.static = { + from: addressUserA, + hash: newPostHash, + msg: genericPostMessage, + quantity: '1', + timestamp: '2025-04-16T19:46:42Z', + }; + + const response = await post(`post`, body, bearerToken); + assert.isOk(response?.status === 200, 'response was not okay'); + + // Even new post should be hidden + const postsResponse = await get<{ + status: number; + rows: { + hash: string; + author: string; + message: string; + deleted_at: Date; + deleted_reason: string; + deleted_hash: string; + }[]; + }>(`posts?address=${addressUserA}`); + + assert.isOk(postsResponse?.status === 200, 'posts did not resolve'); + const data = postsResponse?.rows.find(x => x.hash === newPostHash); + assert.isOk(data, 'New post was hidden'); + }); }); diff --git a/packages/api-main/tests/notifications.test.ts b/packages/api-main/tests/notifications.test.ts index 21667f8a..58063fd5 100644 --- a/packages/api-main/tests/notifications.test.ts +++ b/packages/api-main/tests/notifications.test.ts @@ -5,143 +5,143 @@ import { assert, describe, it } from 'vitest'; import { createWallet, get, getRandomHash, post, userLogin } from './shared'; describe('v1/notifications', async () => { - const walletA = await createWallet(); - const walletB = await createWallet(); - - let bearerToken: string; - - it('User obtains bearer token', async () => { - bearerToken = await userLogin(walletB); - assert.isOk(bearerToken.length >= 1, 'bearer was not passed back'); - }); - - // Follows - it('POST - /follow', async () => { - const body: typeof Posts.FollowBody.static = { - from: walletA.publicKey, - hash: getRandomHash(), - address: walletB.publicKey, - timestamp: '2025-04-16T19:46:42Z', - }; - - const response = await post(`follow`, body); - assert.isOk(response?.status === 200, 'response was not okay'); - const notificationResponse = await get<{ - status: number; - rows: { - hash: string; - owner: string; - type: 'like' | 'dislike' | 'flag' | 'follow' | 'reply'; - timestamp: Date | null; - was_read: boolean | null; - actor: string; - }[]; - }>(`notifications?address=${walletB.publicKey}`, bearerToken); - // Asert user got a notification and can read it - assert.isOk(notificationResponse?.status === 200, `response was not okay, got ${notificationResponse?.status}`); - assert.lengthOf(notificationResponse.rows, 1); - assert.isFalse(notificationResponse.rows[0].was_read, `notification was not marked as read, got true`); - assert.isOk(notificationResponse.rows[0].actor === walletA.publicKey, `unexpected actor, got ${notificationResponse.rows[0].actor}`); - - const readResponse = await post<{ - status: number; - }>( - `notification-read?address=${walletB.publicKey}&hash=${notificationResponse.rows[0].hash}`, - {}, - bearerToken, - ); - assert.isOk(readResponse?.status === 200, `response was not okay, got ${readResponse?.status}`); - // after reading the notification it should no longer show - const lastResponse = await get<{ - status: number; - rows: { - hash: string; - owner: string; - type: 'like' | 'dislike' | 'flag' | 'follow' | 'reply'; - timestamp: Date | null; - was_read: boolean | null; - }[]; - }>(`notifications?address=${walletB.publicKey}`, bearerToken); - - assert.isOk(lastResponse?.rows.findIndex(x => x.hash == notificationResponse.rows[0].hash) === -1, 'notification was still available in array'); - }); - - // Follows - it('liking a post notify the post owner', async () => { - const genericPostMessage - = 'hello world, this is a really intereresting post $@!($)@!()@!$21,4214,12,42142,14,12,421,'; - const postHash = getRandomHash(); - const body: typeof Posts.PostBody.static = { - from: walletA.publicKey, - hash: postHash, - msg: genericPostMessage, - quantity: '1', - timestamp: '2025-04-16T19:46:42Z', - }; - - const postResponse = await post(`post`, body); - assert.isOk(postResponse != null); - assert.isOk(postResponse && postResponse.status === 200, 'response was not okay'); - - const likeBody: typeof Posts.LikeBody.static = { - from: walletB.publicKey, - hash: getRandomHash(), - post_hash: postHash, - quantity: '1', - timestamp: '2025-04-16T19:46:42Z', - }; - - const likeResponse = await post(`like`, likeBody); - assert.isOk(likeResponse != null); - assert.isOk(likeResponse && likeResponse.status === 200, 'response was not okay got ' + likeResponse.status); - - // Login as userA - bearerToken = await userLogin(walletA); - assert.isOk(bearerToken.length >= 1, 'bearer was not passed back'); - - const notificationCount = await get<{ - status: number; - count: number; - }>(`notifications-count?address=${walletA.publicKey}`, bearerToken); - assert.isOk(notificationCount?.count === 1, 'notification count was not at least one'); - - const notificationResponse = await get<{ - status: number; - rows: { - hash: string; - owner: string; - type: 'like' | 'dislike' | 'flag' | 'follow' | 'reply'; - timestamp: Date | null; - was_read: boolean | null; - actor: string; - }[]; - }>(`notifications?address=${walletA.publicKey}`, bearerToken); - // Asert user got a notification and can read it - assert.isOk(notificationResponse?.status === 200, `response was not okay, got ${notificationResponse?.status}`); - assert.lengthOf(notificationResponse.rows, 1); - assert.isFalse(notificationResponse.rows[0].was_read, `notification was not marked as read, got true`); - assert.isOk(notificationResponse.rows[0].actor === walletB.publicKey, `unexpected actor, got ${notificationResponse.rows[0].actor}`); - - const readResponse = await post<{ - status: number; - }>( - `notification-read?address=${walletA.publicKey}&hash=${notificationResponse.rows[0].hash}`, - {}, - bearerToken, - ); - assert.isOk(readResponse?.status === 200, `response was not okay, got ${readResponse?.status}`); - // after reading the notification it should be marked as read - const lastResponse = await get<{ - status: number; - rows: { - hash: string; - owner: string; - type: 'like' | 'dislike' | 'flag' | 'follow' | 'reply'; - timestamp: Date | null; - was_read: boolean | null; - }[]; - }>(`notifications?address=${walletA.publicKey}`, bearerToken); - - assert.isOk(lastResponse?.rows.findIndex(x => x.hash == notificationResponse.rows[0].hash) === -1, 'notification was still available in array'); - }); + const walletA = await createWallet(); + const walletB = await createWallet(); + + let bearerToken: string; + + it('user obtains bearer token', async () => { + bearerToken = await userLogin(walletB); + assert.isOk(bearerToken.length >= 1, 'bearer was not passed back'); + }); + + // Follows + it('pOST - /follow', async () => { + const body: typeof Posts.FollowBody.static = { + from: walletA.publicKey, + hash: getRandomHash(), + address: walletB.publicKey, + timestamp: '2025-04-16T19:46:42Z', + }; + + const response = await post(`follow`, body); + assert.isOk(response?.status === 200, 'response was not okay'); + const notificationResponse = await get<{ + status: number; + rows: { + hash: string; + owner: string; + type: 'like' | 'dislike' | 'flag' | 'follow' | 'reply'; + timestamp: Date | null; + was_read: boolean | null; + actor: string; + }[]; + }>(`notifications?address=${walletB.publicKey}`, bearerToken); + // Asert user got a notification and can read it + assert.isOk(notificationResponse?.status === 200, `response was not okay, got ${notificationResponse?.status}`); + assert.lengthOf(notificationResponse.rows, 1); + assert.isFalse(notificationResponse.rows[0].was_read, `notification was not marked as read, got true`); + assert.isOk(notificationResponse.rows[0].actor === walletA.publicKey, `unexpected actor, got ${notificationResponse.rows[0].actor}`); + + const readResponse = await post<{ + status: number; + }>( + `notification-read?address=${walletB.publicKey}&hash=${notificationResponse.rows[0].hash}`, + {}, + bearerToken, + ); + assert.isOk(readResponse?.status === 200, `response was not okay, got ${readResponse?.status}`); + // after reading the notification it should no longer show + const lastResponse = await get<{ + status: number; + rows: { + hash: string; + owner: string; + type: 'like' | 'dislike' | 'flag' | 'follow' | 'reply'; + timestamp: Date | null; + was_read: boolean | null; + }[]; + }>(`notifications?address=${walletB.publicKey}`, bearerToken); + + assert.isOk(lastResponse?.rows.findIndex(x => x.hash === notificationResponse.rows[0].hash) === -1, 'notification was still available in array'); + }); + + // Follows + it('liking a post notify the post owner', async () => { + const genericPostMessage + = 'hello world, this is a really intereresting post $@!($)@!()@!$21,4214,12,42142,14,12,421,'; + const postHash = getRandomHash(); + const body: typeof Posts.PostBody.static = { + from: walletA.publicKey, + hash: postHash, + msg: genericPostMessage, + quantity: '1', + timestamp: '2025-04-16T19:46:42Z', + }; + + const postResponse = await post(`post`, body); + assert.isOk(postResponse != null); + assert.isOk(postResponse && postResponse.status === 200, 'response was not okay'); + + const likeBody: typeof Posts.LikeBody.static = { + from: walletB.publicKey, + hash: getRandomHash(), + post_hash: postHash, + quantity: '1', + timestamp: '2025-04-16T19:46:42Z', + }; + + const likeResponse = await post(`like`, likeBody); + assert.isOk(likeResponse != null); + assert.isOk(likeResponse && likeResponse.status === 200, `response was not okay got ${likeResponse.status}`); + + // Login as userA + bearerToken = await userLogin(walletA); + assert.isOk(bearerToken.length >= 1, 'bearer was not passed back'); + + const notificationCount = await get<{ + status: number; + count: number; + }>(`notifications-count?address=${walletA.publicKey}`, bearerToken); + assert.isOk(notificationCount?.count === 1, 'notification count was not at least one'); + + const notificationResponse = await get<{ + status: number; + rows: { + hash: string; + owner: string; + type: 'like' | 'dislike' | 'flag' | 'follow' | 'reply'; + timestamp: Date | null; + was_read: boolean | null; + actor: string; + }[]; + }>(`notifications?address=${walletA.publicKey}`, bearerToken); + // Asert user got a notification and can read it + assert.isOk(notificationResponse?.status === 200, `response was not okay, got ${notificationResponse?.status}`); + assert.lengthOf(notificationResponse.rows, 1); + assert.isFalse(notificationResponse.rows[0].was_read, `notification was not marked as read, got true`); + assert.isOk(notificationResponse.rows[0].actor === walletB.publicKey, `unexpected actor, got ${notificationResponse.rows[0].actor}`); + + const readResponse = await post<{ + status: number; + }>( + `notification-read?address=${walletA.publicKey}&hash=${notificationResponse.rows[0].hash}`, + {}, + bearerToken, + ); + assert.isOk(readResponse?.status === 200, `response was not okay, got ${readResponse?.status}`); + // after reading the notification it should be marked as read + const lastResponse = await get<{ + status: number; + rows: { + hash: string; + owner: string; + type: 'like' | 'dislike' | 'flag' | 'follow' | 'reply'; + timestamp: Date | null; + was_read: boolean | null; + }[]; + }>(`notifications?address=${walletA.publicKey}`, bearerToken); + + assert.isOk(lastResponse?.rows.findIndex(x => x.hash === notificationResponse.rows[0].hash) === -1, 'notification was still available in array'); + }); }); diff --git a/packages/api-main/tests/search.test.ts b/packages/api-main/tests/search.test.ts index 574725ef..1fb59f46 100644 --- a/packages/api-main/tests/search.test.ts +++ b/packages/api-main/tests/search.test.ts @@ -5,51 +5,51 @@ import { assert, describe, it } from 'vitest'; import { get, getAtomOneAddress, getRandomHash, post } from './shared'; describe('should search for posts', async () => { - const addressUserA = getAtomOneAddress(); - - it('Search - /search', async () => { - const body: typeof Posts.PostBody.static = { - from: addressUserA, - hash: getRandomHash(), - msg: 'this is a very unique message with a very unique result', - quantity: '1', - timestamp: '2025-04-16T19:46:42Z', - }; - - const response = await post(`post`, body); - assert.isOk(response?.status === 200, 'response was not okay'); - - const results1 = await get<{ status: number; rows: { message: string }[] }>( - 'search?text="very unique message"', - ); - assert.isOk(results1?.status === 200); - assert.isOk(results1.rows.length === 1); - - const results2 = await get<{ status: number; rows: { message: string }[] }>( - 'search?text="supercalifragilisticexpialidocious"', - ); - assert.isOk(results2?.status === 200); - assert.isOk(results2.rows.length <= 0); - }); - - it('Search - /search post with owner', async () => { - const body: typeof Posts.PostBody.static = { - from: addressUserA, - hash: getRandomHash(), - msg: 'content not related at all with owner', - quantity: '1', - timestamp: '2025-04-16T19:46:42Z', - }; - - const response = await post(`post`, body); - assert.isOk(response?.status === 200, 'response was not okay'); - - const results1 = await get<{ status: number; rows: { message: string }[]; users: string[] }>( - `search?text=${addressUserA}`, - ); - assert.isOk(results1?.status === 200); - assert.isOk(results1.rows.length > 1); - assert.isOk(results1.users.length === 1); - assert.isOk(results1.users[0] === addressUserA); - }); + const addressUserA = getAtomOneAddress(); + + it('search - /search', async () => { + const body: typeof Posts.PostBody.static = { + from: addressUserA, + hash: getRandomHash(), + msg: 'this is a very unique message with a very unique result', + quantity: '1', + timestamp: '2025-04-16T19:46:42Z', + }; + + const response = await post(`post`, body); + assert.isOk(response?.status === 200, 'response was not okay'); + + const results1 = await get<{ status: number; rows: { message: string }[] }>( + 'search?text="very unique message"', + ); + assert.isOk(results1?.status === 200); + assert.isOk(results1.rows.length === 1); + + const results2 = await get<{ status: number; rows: { message: string }[] }>( + 'search?text="supercalifragilisticexpialidocious"', + ); + assert.isOk(results2?.status === 200); + assert.isOk(results2.rows.length <= 0); + }); + + it('search - /search post with owner', async () => { + const body: typeof Posts.PostBody.static = { + from: addressUserA, + hash: getRandomHash(), + msg: 'content not related at all with owner', + quantity: '1', + timestamp: '2025-04-16T19:46:42Z', + }; + + const response = await post(`post`, body); + assert.isOk(response?.status === 200, 'response was not okay'); + + const results1 = await get<{ status: number; rows: { message: string }[]; users: string[] }>( + `search?text=${addressUserA}`, + ); + assert.isOk(results1?.status === 200); + assert.isOk(results1.rows.length > 1); + assert.isOk(results1.users.length === 1); + assert.isOk(results1.users[0] === addressUserA); + }); }); diff --git a/packages/api-main/tests/setup.ts b/packages/api-main/tests/setup.ts index 300d400e..5ef4d4c1 100644 --- a/packages/api-main/tests/setup.ts +++ b/packages/api-main/tests/setup.ts @@ -7,24 +7,23 @@ import { tables } from '../drizzle/schema'; import { start } from '../src/index'; async function clearTables() { - console.log('Clearing Tables'); - try { - for (const tableName of tables) { - await getDatabase().execute(sql`TRUNCATE TABLE ${sql.raw(tableName)};`); - } - } - catch (err) { - console.error('Error clearing tables:', err); - // Continue anyway - tables might not exist yet + console.log('Clearing Tables'); + try { + for (const tableName of tables) { + await getDatabase().execute(sql`TRUNCATE TABLE ${sql.raw(tableName)};`); } + } catch (err) { + console.error('Error clearing tables:', err); + // Continue anyway - tables might not exist yet + } } export async function setup(project: TestProject) { - start(); + start(); - // Give server time to start - await new Promise(resolve => setTimeout(resolve, 1000)); + // Give server time to start + await new Promise(resolve => setTimeout(resolve, 1000)); - project.onTestsRerun(clearTables); - await clearTables(); + project.onTestsRerun(clearTables); + await clearTables(); } diff --git a/packages/api-main/tests/shared.ts b/packages/api-main/tests/shared.ts index 9284b428..eede318e 100644 --- a/packages/api-main/tests/shared.ts +++ b/packages/api-main/tests/shared.ts @@ -1,7 +1,8 @@ -import { createHash, randomBytes } from 'crypto'; - import type { Posts } from '@atomone/dither-api-types'; +import { createHash, randomBytes } from 'node:crypto'; +import process from 'node:process'; + import { Secp256k1HdWallet } from '@cosmjs/amino'; import { toBech32 } from '@cosmjs/encoding'; import { makeADR36AminoSignDoc } from '@keplr-wallet/cosmos'; @@ -9,162 +10,162 @@ import { makeADR36AminoSignDoc } from '@keplr-wallet/cosmos'; let lastHeight = 1_000_000; export async function get(endpoint: string, token?: string) { - const headers: Record = { - 'Content-Type': 'application/json', - }; - - if (token) { - headers['Cookie'] = `auth=${token}`; - } - - const response = await fetch(`http://localhost:3000/v1/${endpoint}`, { - method: 'GET', - headers, - }).catch((err) => { - console.error(err); - return null; - }); - - if (!response?.ok) { - return null; - } - - const jsonData = await response.json(); - return jsonData as T; + const headers: Record = { + 'Content-Type': 'application/json', + }; + + if (token) { + headers.Cookie = `auth=${token}`; + } + + const response = await fetch(`http://localhost:3000/v1/${endpoint}`, { + method: 'GET', + headers, + }).catch((err) => { + console.error(err); + return null; + }); + + if (!response?.ok) { + return null; + } + + const jsonData = await response.json(); + return jsonData as T; } export async function post( - endpoint: string, - body: object, - token?: string, + endpoint: string, + body: object, + token?: string, ): Promise { - const headers: Record = { - 'Content-Type': 'application/json', - 'authorization': process.env.AUTH ?? 'whatever', - }; - - if (token) { - headers['Cookie'] = `auth=${token}`; - } - - const response = await fetch(`http://localhost:3000/v1/${endpoint}`, { - method: 'POST', - headers, - body: JSON.stringify({ ...body }), - }).catch((err) => { - console.error(err); - return null; - }); - - if (!response?.ok) { - console.log(await response?.json()); - return null; - } - - const jsonData = (await response.json()) as { status: number }; - if (jsonData.status && jsonData.status !== 200) { - return jsonData as T; - } - + const headers: Record = { + 'Content-Type': 'application/json', + 'authorization': process.env.AUTH ?? 'whatever', + }; + + if (token) { + headers.Cookie = `auth=${token}`; + } + + const response = await fetch(`http://localhost:3000/v1/${endpoint}`, { + method: 'POST', + headers, + body: JSON.stringify({ ...body }), + }).catch((err) => { + console.error(err); + return null; + }); + + if (!response?.ok) { + console.log(await response?.json()); + return null; + } + + const jsonData = (await response.json()) as { status: number }; + if (jsonData.status && jsonData.status !== 200) { return jsonData as T; + } + + return jsonData as T; } export function getSha256Hash(input: string | Uint8Array): string { - const hash = createHash('sha256'); - hash.update(input); - return hash.digest('hex'); + const hash = createHash('sha256'); + hash.update(input); + return hash.digest('hex'); } export function getAtomOneAddress(): string { - const randomData = randomBytes(32); - const hash = createHash('sha256').update(randomData).digest(); - const addressBytes = hash.slice(0, 20); - const encodedAddress = toBech32('atone', addressBytes); - return encodedAddress; + const randomData = randomBytes(32); + const hash = createHash('sha256').update(randomData).digest(); + const addressBytes = hash.slice(0, 20); + const encodedAddress = toBech32('atone', addressBytes); + return encodedAddress; } export async function createWallet() { - const wallet = await Secp256k1HdWallet.generate(24, { prefix: 'atone' }); - const accounts = await wallet.getAccounts(); - return { mnemonic: wallet.mnemonic, publicKey: accounts[0].address }; + const wallet = await Secp256k1HdWallet.generate(24, { prefix: 'atone' }); + const accounts = await wallet.getAccounts(); + return { mnemonic: wallet.mnemonic, publicKey: accounts[0].address }; } export async function signADR36Document(mnemonic: string, messageToSign: string) { - const wallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, { prefix: 'atone' }); - const accounts = await wallet.getAccounts(); + const wallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, { prefix: 'atone' }); + const accounts = await wallet.getAccounts(); - const document = makeADR36AminoSignDoc(accounts[0].address, messageToSign); - return await wallet.signAmino(accounts[0].address, document); + const document = makeADR36AminoSignDoc(accounts[0].address, messageToSign); + return await wallet.signAmino(accounts[0].address, document); } export function getRandomHash() { - return getSha256Hash(randomBytes(256).toString()); + return getSha256Hash(randomBytes(256).toString()); } export function generateFakeData(memo: string, from_address: string, to_address: string) { - lastHeight++; - - return { - hash: getSha256Hash(randomBytes(256).toString()), - height: lastHeight.toString(), - timestamp: '2025-04-16T19:46:42Z', // Doesn't matter, just need to store some timestamps - memo, - messages: [ - { - '@type': '/cosmos.bank.v1beta1.MsgSend', - 'from_address': from_address, - 'to_address': to_address, - 'amount': [{ denom: 'uatone', amount: '1' }], - }, - ], - }; + lastHeight++; + + return { + hash: getSha256Hash(randomBytes(256).toString()), + height: lastHeight.toString(), + timestamp: '2025-04-16T19:46:42Z', // Doesn't matter, just need to store some timestamps + memo, + messages: [ + { + '@type': '/cosmos.bank.v1beta1.MsgSend', + 'from_address': from_address, + 'to_address': to_address, + 'amount': [{ denom: 'uatone', amount: '1' }], + }, + ], + }; } export function sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise(resolve => setTimeout(resolve, ms)); } export async function userLogin(wallet: { mnemonic: string; publicKey: string }) { - const body: typeof Posts.AuthCreateBody.static = { - address: wallet.publicKey, - }; - - const response = (await post(`auth-create`, body, 'READ')) as { status: 200; id: number; message: string }; - if (response?.status !== 200) { - return ''; - } - - const signData = await signADR36Document(wallet.mnemonic, response.message); - const verifyBody: typeof Posts.AuthBody.static & { json: boolean } = { - id: response.id, - ...signData.signature, - json: true, - }; - - const responseVerify = (await post(`auth`, verifyBody, 'READ')) as { status: 200; bearer: string }; - if (response?.status !== 200) { - return ''; - } - - return responseVerify.bearer; + const body: typeof Posts.AuthCreateBody.static = { + address: wallet.publicKey, + }; + + const response = (await post(`auth-create`, body, 'READ')) as { status: 200; id: number; message: string }; + if (response?.status !== 200) { + return ''; + } + + const signData = await signADR36Document(wallet.mnemonic, response.message); + const verifyBody: typeof Posts.AuthBody.static & { json: boolean } = { + id: response.id, + ...signData.signature, + json: true, + }; + + const responseVerify = (await post(`auth`, verifyBody, 'READ')) as { status: 200; bearer: string }; + if (response?.status !== 200) { + return ''; + } + + return responseVerify.bearer; } export async function createPost(msg = 'default content') { - const address = getAtomOneAddress(); - const hash = getRandomHash(); - - const body: typeof Posts.PostBody.static = { - from: address, - hash: hash, - msg, - quantity: '1', - timestamp: '2025-04-16T19:46:42Z', - }; - - const response = await post(`post`, body); - if (response?.status !== 200) { - return undefined; - } - - return { hash, address }; + const address = getAtomOneAddress(); + const hash = getRandomHash(); + + const body: typeof Posts.PostBody.static = { + from: address, + hash, + msg, + quantity: '1', + timestamp: '2025-04-16T19:46:42Z', + }; + + const response = await post(`post`, body); + if (response?.status !== 200) { + return undefined; + } + + return { hash, address }; }; diff --git a/packages/api-main/tests/smoke/health.test.ts b/packages/api-main/tests/smoke/health.test.ts index 9230b864..05e5bc59 100644 --- a/packages/api-main/tests/smoke/health.test.ts +++ b/packages/api-main/tests/smoke/health.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from 'vitest'; describe('smoke', () => { - it('GET /v1/health returns 200', async () => { - const res = await fetch(`https://${process.env.APP_NAME}.fly.dev/v1/health`); - const json = await res.json().catch(() => null); + it('gET /v1/health returns 200', async () => { + const res = await fetch(`https://${process.env.APP_NAME}.fly.dev/v1/health`); + const json = await res.json().catch(() => null); - expect(res.ok).toBe(true); - expect(json).toBeTruthy(); - }); + expect(res.ok).toBe(true); + expect(json).toBeTruthy(); + }); }); diff --git a/packages/api-main/tests/state.test.ts b/packages/api-main/tests/state.test.ts index a9b81935..27780ce7 100644 --- a/packages/api-main/tests/state.test.ts +++ b/packages/api-main/tests/state.test.ts @@ -3,21 +3,20 @@ import { assert, describe, it } from 'vitest'; import { getDatabase } from '../drizzle/db'; import { ReaderState } from '../drizzle/schema'; - import { post } from './shared'; describe('update state', () => { - it('should update state', async () => { - let response = await post<{ status: number; error: string }>(`update-state`, { last_block: '1' }); - assert.isOk(response?.status === 200, 'could not post to update state'); + it('should update state', async () => { + let response = await post<{ status: number; error: string }>(`update-state`, { last_block: '1' }); + assert.isOk(response?.status === 200, 'could not post to update state'); - let [state] = await getDatabase().select().from(ReaderState).where(eq(ReaderState.id, 0)).limit(1); - assert.isOk(state.last_block == '1'); + let [state] = await getDatabase().select().from(ReaderState).where(eq(ReaderState.id, 0)).limit(1); + assert.isOk(state.last_block === '1'); - response = await post<{ status: number; error: string }>(`update-state`, { last_block: '2' }); - assert.isOk(response?.status === 200, 'could not post to update state'); + response = await post<{ status: number; error: string }>(`update-state`, { last_block: '2' }); + assert.isOk(response?.status === 200, 'could not post to update state'); - [state] = await getDatabase().select().from(ReaderState).where(eq(ReaderState.id, 0)).limit(1); - assert.isOk(state.last_block == '2'); - }); + [state] = await getDatabase().select().from(ReaderState).where(eq(ReaderState.id, 0)).limit(1); + assert.isOk(state.last_block === '2'); + }); }); diff --git a/packages/api-main/tests/utility.test.ts b/packages/api-main/tests/utility.test.ts index 83716199..68b30cab 100644 --- a/packages/api-main/tests/utility.test.ts +++ b/packages/api-main/tests/utility.test.ts @@ -1,35 +1,34 @@ import { describe, expect, it } from 'vitest'; import { getTransferMessage, getTransferQuantities } from '../src/utility'; - import { generateFakeData, getAtomOneAddress } from './shared'; describe('utility tests', () => { - it('getTransferMessage', () => { - const userA = getAtomOneAddress(); - const userB = getAtomOneAddress(); + it('getTransferMessage', () => { + const userA = getAtomOneAddress(); + const userB = getAtomOneAddress(); - const msgTransfer = getTransferMessage([generateFakeData('whatever', userA, userB)]); + const msgTransfer = getTransferMessage([generateFakeData('whatever', userA, userB)]); - expect(msgTransfer && msgTransfer.from_address == userA, 'from address did not match'); - expect(msgTransfer && msgTransfer.to_address == userB, 'to address did not match'); - }); + expect(msgTransfer && msgTransfer.from_address === userA, 'from address did not match'); + expect(msgTransfer && msgTransfer.to_address === userB, 'to address did not match'); + }); - it('getAtomOneAddress', () => { - for (let i = 0; i < 100; i++) { - expect(getAtomOneAddress().length === 44, 'address length was incorrect'); - } - }); + it('getAtomOneAddress', () => { + for (let i = 0; i < 100; i++) { + expect(getAtomOneAddress().length === 44, 'address length was incorrect'); + } + }); - it('getTransferQuantities', () => { - let totalQuantity = BigInt('0'); + it('getTransferQuantities', () => { + let totalQuantity = BigInt('0'); - for (let i = 0; i < 100; i++) { - totalQuantity += BigInt( - getTransferQuantities([generateFakeData('whatever', getAtomOneAddress(), getAtomOneAddress())]), - ); - } + for (let i = 0; i < 100; i++) { + totalQuantity += BigInt( + getTransferQuantities([generateFakeData('whatever', getAtomOneAddress(), getAtomOneAddress())]), + ); + } - expect(totalQuantity === BigInt('100')); - }); + expect(totalQuantity === BigInt('100')); + }); }); diff --git a/packages/api-main/tests/v1.test.ts b/packages/api-main/tests/v1.test.ts index 7b56350c..038e13be 100644 --- a/packages/api-main/tests/v1.test.ts +++ b/packages/api-main/tests/v1.test.ts @@ -5,306 +5,306 @@ import { assert, describe, it } from 'vitest'; import { createPost, get, getAtomOneAddress, getRandomHash, post } from './shared'; describe('v1', { sequential: true }, async () => { - const addressUserA = getAtomOneAddress(); - const replyHash = getRandomHash(); - const genericPostMessage - = 'hello world, this is a really intereresting post $@!($)@!()@!$21,4214,12,42142,14,12,421,'; - - // Posts - it('POST - /post', async () => { - const body: typeof Posts.PostBody.static = { - from: addressUserA, - hash: getRandomHash(), - msg: genericPostMessage, - quantity: '1', - timestamp: '2025-04-16T19:46:42Z', - }; - - const response = await post(`post`, body); - assert.isOk(response?.status === 200, 'response was not okay'); - }); - - it('POST - /reply', async () => { - const response = await get<{ status: number; rows: { hash: string; author: string; message: string }[] }>( - `feed`, - ); - assert.isOk(response, 'failed to fetch feed data'); - assert.isOk( - response && Array.isArray(response.rows) && response.rows.length >= 1, - 'feed result was not an array type', - ); - - const body: typeof Posts.ReplyBody.static = { - from: addressUserA, - hash: replyHash, - post_hash: response.rows[0].hash, - msg: genericPostMessage, - quantity: '1', - timestamp: '2025-04-16T19:46:42Z', - }; - - const replyResponse = await post(`reply`, body); - assert.isOk(replyResponse?.status === 200, 'response was not okay'); - }); - - it('GET - /feed', async () => { - const response = await get<{ status: number; rows: { hash: string; author: string; message: string }[] }>( - `feed`, - ); - assert.isOk(response, 'failed to fetch feed data'); - assert.isOk( - response && Array.isArray(response.rows) && response.rows.length >= 1, - 'feed result was not an array type', - ); - - const message = response.rows.find(x => x.author === addressUserA && x.message === genericPostMessage); - assert.isOk(message); - }); - - it('GET - /posts', async () => { - const response = await get<{ status: number; rows: { hash: string; author: string; message: string }[] }>( - `posts?address=${addressUserA}`, - ); - assert.isOk(response, 'failed to fetch posts data'); - assert.isOk( - response && Array.isArray(response.rows) && response.rows.length >= 1, - 'feed result was not an array type', - ); - }); - - // Likes - const likeablePost = await createPost('A Likeable Post'); - it ('should have a likeable post', () => { - assert.isOk(likeablePost); - }); - - it('POST - /like', async () => { - if (!likeablePost) { - assert.fail('Likeable post does not exist'); - } - - for (let i = 0; i < 50; i++) { - const body: typeof Posts.LikeBody.static = { - from: addressUserA, - hash: getRandomHash(), - post_hash: likeablePost?.hash, - quantity: '1', - timestamp: '2025-04-16T19:46:42Z', - }; - - const likeResponse = await post(`like`, body); - assert.isOk(likeResponse != null); - assert.isOk(likeResponse && likeResponse.status === 200, 'response was not okay'); - } - }); - - it('GET - /likes', async () => { - if (!likeablePost) { - assert.fail('Likeable post does not exist'); - } - - const response = await get<{ status: number; rows: { hash: string; likes: number }[] }>( - `post?hash=${likeablePost.hash}`, - ); - - assert.isOk(response, 'failed to fetch posts data'); - assert.isOk(Array.isArray(response.rows) && response.rows.length >= 1, 'feed result was not an array type'); - assert.isOk(response && response.rows[0].likes >= 50, 'likes were not incremented on post'); - }); - - // Dislikes - const dislikeablePost = await createPost('A Dislikeable Post'); - it ('should have a dislikeable post', () => { - assert.isOk(dislikeablePost); - }); - - it('POST - /dislike', async () => { - if (!dislikeablePost) { - assert.fail('Likeable post does not exist'); - } - - for (let i = 0; i < 50; i++) { - const body: typeof Posts.DislikeBody.static = { - from: addressUserA, - hash: getRandomHash(), - post_hash: dislikeablePost.hash, - quantity: '1', - timestamp: '2025-04-16T19:46:42Z', - }; - - const dislikeResponse = await post(`dislike`, body); - assert.isOk(dislikeResponse != null); - assert.isOk(dislikeResponse && dislikeResponse.status === 200, 'response was not okay'); - } - }); - - it('GET - /dislikes', async () => { - if (!dislikeablePost) { - assert.fail('Likeable post does not exist'); - } - - const response = await get<{ - status: number; - rows: { hash: string; author: string; message: string; dislikes: number }[]; - }>(`post?hash=${dislikeablePost.hash}`); - assert.isOk(response, 'failed to fetch posts data'); - assert.isOk(Array.isArray(response.rows) && response.rows.length >= 1, 'feed result was not an array type'); - assert.isOk(response && response.rows[0].dislikes >= 50, 'likes were not incremented on post'); - }); - - // Flags - const flagPost = await createPost('A Dislikeable Post'); - it ('should have a dislikeable post', () => { - assert.isOk(flagPost); - }); - - it('POST - /flag', async () => { - if (!flagPost) { - assert.fail('Likeable post does not exist'); - } - - for (let i = 0; i < 50; i++) { - const body: typeof Posts.FlagBody.static = { - from: addressUserA, - hash: getRandomHash(), - post_hash: flagPost.hash, - quantity: '1', - timestamp: '2025-04-16T19:46:42Z', - }; - - const flagResponse = await post(`flag`, body); - assert.isOk(flagResponse != null); - assert.isOk(flagResponse && flagResponse.status === 200, 'response was not okay'); - } - }); - - it('GET - /flags', async () => { - if (!flagPost) { - assert.fail('Likeable post does not exist'); - } - - const getResponse = await get<{ status: number; rows: Array<{ hash: string }> }>( - `flags?hash=${flagPost.hash}`, - ); - assert.isOk(getResponse, 'failed to fetch posts data'); - assert.isOk(getResponse.status == 200, 'flags result was not valid'); - assert.isOk( - getResponse && Array.isArray(getResponse.rows) && getResponse.rows.length >= 50, - 'feed result was not an array type', - ); - }); - - // PostRemove - it('POST - /post-remove', async () => { - const bodyPost: typeof Posts.PostBody.static = { - from: addressUserA, - hash: getRandomHash(), - msg: genericPostMessage, - quantity: '1', - timestamp: '2025-04-16T19:46:42Z', - }; - - const responsePost = await post(`post`, bodyPost); - assert.isOk(responsePost?.status === 200, 'response was not okay'); - - const response = await get<{ status: number; rows: { hash: string; author: string; message: string }[] }>( - `posts?address=${addressUserA}`, - ); - assert.isOk(response, 'failed to fetch posts data'); - assert.isOk(Array.isArray(response.rows) && response.rows.length >= 1, 'feed result was not an array type'); - - const body: typeof Posts.PostRemoveBody.static = { - from: addressUserA, - hash: getRandomHash(), - timestamp: '2025-04-16T19:46:42Z', - post_hash: response.rows[0].hash, - }; - - const replyResponse = await post(`post-remove`, body); - assert.isOk(replyResponse?.status === 200, 'response was not okay'); - - const postsResponse = await get<{ - status: number; - rows: { - hash: string; - author: string; - message: string; - deleted_at: Date; - deleted_reason: string; - deleted_hash: string; - }[]; - }>(`posts?address=${addressUserA}`); - - assert.isOk(postsResponse?.status === 200, 'posts did not resolve'); - const data = postsResponse?.rows.find(x => x.hash === response.rows[0].hash); - assert.isUndefined(data, 'data was not hidden'); - }); - - it('POST - /post-remove - No Permission', async () => { - const response = await get<{ status: number; rows: { hash: string; author: string; message: string }[] }>( - `posts?address=${addressUserA}`, - ); - assert.isOk(response, 'failed to fetch posts data'); - assert.isOk(Array.isArray(response.rows) && response.rows.length >= 1, 'feed result was not an array type'); - - const body: typeof Posts.PostRemoveBody.static = { - from: addressUserA + 'abcd', - hash: getRandomHash(), - timestamp: '2025-04-16T19:46:42Z', - post_hash: response.rows[0].hash, - }; - - const replyResponse = await post(`post-remove`, body); - assert.isOk(replyResponse?.status === 200, 'response was not okay'); - - const postsResponse = await get<{ - status: number; - rows: { - hash: string; - author: string; - message: string; - deleted_at: Date; - deleted_reason: string; - deleted_hash: string; - }[]; - }>(`posts?address=${addressUserA}`); - - assert.isOk(postsResponse?.status === 200, 'posts did not resolve'); - const data = postsResponse?.rows.find(x => x.hash === response.rows[0].hash); - assert.isOk(data, 'data was hidden'); - }); - - it('POST - /mod/post-remove - No Permission', async () => { - const response = await get<{ status: number; rows: { hash: string; author: string; message: string }[] }>( - `posts?address=${addressUserA}`, - ); - assert.isOk(response, 'failed to fetch posts data'); - assert.isOk(Array.isArray(response.rows) && response.rows.length >= 1, 'feed result was not an array type'); - - const body: typeof Posts.PostRemoveBody.static = { - from: addressUserA + 'abcd', - hash: getRandomHash(), - timestamp: '2025-04-16T19:46:42Z', - post_hash: response.rows[0].hash, - }; - - const replyResponse = await post(`post-remove`, body); - assert.isOk(replyResponse?.status === 200, 'response was not okay'); - - const postsResponse = await get<{ - status: number; - rows: { - hash: string; - author: string; - message: string; - deleted_at: Date; - deleted_reason: string; - deleted_hash: string; - }[]; - }>(`posts?address=${addressUserA}`); - - assert.isOk(postsResponse?.status === 200, 'posts did not resolve'); - const data = postsResponse?.rows.find(x => x.hash === response.rows[0].hash); - assert.isOk(data, 'data was hidden'); - }); + const addressUserA = getAtomOneAddress(); + const replyHash = getRandomHash(); + const genericPostMessage + = 'hello world, this is a really intereresting post $@!($)@!()@!$21,4214,12,42142,14,12,421,'; + + // Posts + it('pOST - /post', async () => { + const body: typeof Posts.PostBody.static = { + from: addressUserA, + hash: getRandomHash(), + msg: genericPostMessage, + quantity: '1', + timestamp: '2025-04-16T19:46:42Z', + }; + + const response = await post(`post`, body); + assert.isOk(response?.status === 200, 'response was not okay'); + }); + + it('pOST - /reply', async () => { + const response = await get<{ status: number; rows: { hash: string; author: string; message: string }[] }>( + `feed`, + ); + assert.isOk(response, 'failed to fetch feed data'); + assert.isOk( + response && Array.isArray(response.rows) && response.rows.length >= 1, + 'feed result was not an array type', + ); + + const body: typeof Posts.ReplyBody.static = { + from: addressUserA, + hash: replyHash, + post_hash: response.rows[0].hash, + msg: genericPostMessage, + quantity: '1', + timestamp: '2025-04-16T19:46:42Z', + }; + + const replyResponse = await post(`reply`, body); + assert.isOk(replyResponse?.status === 200, 'response was not okay'); + }); + + it('gET - /feed', async () => { + const response = await get<{ status: number; rows: { hash: string; author: string; message: string }[] }>( + `feed`, + ); + assert.isOk(response, 'failed to fetch feed data'); + assert.isOk( + response && Array.isArray(response.rows) && response.rows.length >= 1, + 'feed result was not an array type', + ); + + const message = response.rows.find(x => x.author === addressUserA && x.message === genericPostMessage); + assert.isOk(message); + }); + + it('gET - /posts', async () => { + const response = await get<{ status: number; rows: { hash: string; author: string; message: string }[] }>( + `posts?address=${addressUserA}`, + ); + assert.isOk(response, 'failed to fetch posts data'); + assert.isOk( + response && Array.isArray(response.rows) && response.rows.length >= 1, + 'feed result was not an array type', + ); + }); + + // Likes + const likeablePost = await createPost('A Likeable Post'); + it ('should have a likeable post', () => { + assert.isOk(likeablePost); + }); + + it('pOST - /like', async () => { + if (!likeablePost) { + assert.fail('Likeable post does not exist'); + } + + for (let i = 0; i < 50; i++) { + const body: typeof Posts.LikeBody.static = { + from: addressUserA, + hash: getRandomHash(), + post_hash: likeablePost?.hash, + quantity: '1', + timestamp: '2025-04-16T19:46:42Z', + }; + + const likeResponse = await post(`like`, body); + assert.isOk(likeResponse != null); + assert.isOk(likeResponse && likeResponse.status === 200, 'response was not okay'); + } + }); + + it('gET - /likes', async () => { + if (!likeablePost) { + assert.fail('Likeable post does not exist'); + } + + const response = await get<{ status: number; rows: { hash: string; likes: number }[] }>( + `post?hash=${likeablePost.hash}`, + ); + + assert.isOk(response, 'failed to fetch posts data'); + assert.isOk(Array.isArray(response.rows) && response.rows.length >= 1, 'feed result was not an array type'); + assert.isOk(response && response.rows[0].likes >= 50, 'likes were not incremented on post'); + }); + + // Dislikes + const dislikeablePost = await createPost('A Dislikeable Post'); + it ('should have a dislikeable post', () => { + assert.isOk(dislikeablePost); + }); + + it('pOST - /dislike', async () => { + if (!dislikeablePost) { + assert.fail('Likeable post does not exist'); + } + + for (let i = 0; i < 50; i++) { + const body: typeof Posts.DislikeBody.static = { + from: addressUserA, + hash: getRandomHash(), + post_hash: dislikeablePost.hash, + quantity: '1', + timestamp: '2025-04-16T19:46:42Z', + }; + + const dislikeResponse = await post(`dislike`, body); + assert.isOk(dislikeResponse != null); + assert.isOk(dislikeResponse && dislikeResponse.status === 200, 'response was not okay'); + } + }); + + it('gET - /dislikes', async () => { + if (!dislikeablePost) { + assert.fail('Likeable post does not exist'); + } + + const response = await get<{ + status: number; + rows: { hash: string; author: string; message: string; dislikes: number }[]; + }>(`post?hash=${dislikeablePost.hash}`); + assert.isOk(response, 'failed to fetch posts data'); + assert.isOk(Array.isArray(response.rows) && response.rows.length >= 1, 'feed result was not an array type'); + assert.isOk(response && response.rows[0].dislikes >= 50, 'likes were not incremented on post'); + }); + + // Flags + const flagPost = await createPost('A Dislikeable Post'); + it ('should have a dislikeable post (flag)', () => { + assert.isOk(flagPost); + }); + + it('pOST - /flag', async () => { + if (!flagPost) { + assert.fail('Likeable post does not exist'); + } + + for (let i = 0; i < 50; i++) { + const body: typeof Posts.FlagBody.static = { + from: addressUserA, + hash: getRandomHash(), + post_hash: flagPost.hash, + quantity: '1', + timestamp: '2025-04-16T19:46:42Z', + }; + + const flagResponse = await post(`flag`, body); + assert.isOk(flagResponse != null); + assert.isOk(flagResponse && flagResponse.status === 200, 'response was not okay'); + } + }); + + it('gET - /flags', async () => { + if (!flagPost) { + assert.fail('Likeable post does not exist'); + } + + const getResponse = await get<{ status: number; rows: Array<{ hash: string }> }>( + `flags?hash=${flagPost.hash}`, + ); + assert.isOk(getResponse, 'failed to fetch posts data'); + assert.isOk(getResponse.status === 200, 'flags result was not valid'); + assert.isOk( + getResponse && Array.isArray(getResponse.rows) && getResponse.rows.length >= 50, + 'feed result was not an array type', + ); + }); + + // PostRemove + it('pOST - /post-remove', async () => { + const bodyPost: typeof Posts.PostBody.static = { + from: addressUserA, + hash: getRandomHash(), + msg: genericPostMessage, + quantity: '1', + timestamp: '2025-04-16T19:46:42Z', + }; + + const responsePost = await post(`post`, bodyPost); + assert.isOk(responsePost?.status === 200, 'response was not okay'); + + const response = await get<{ status: number; rows: { hash: string; author: string; message: string }[] }>( + `posts?address=${addressUserA}`, + ); + assert.isOk(response, 'failed to fetch posts data'); + assert.isOk(Array.isArray(response.rows) && response.rows.length >= 1, 'feed result was not an array type'); + + const body: typeof Posts.PostRemoveBody.static = { + from: addressUserA, + hash: getRandomHash(), + timestamp: '2025-04-16T19:46:42Z', + post_hash: response.rows[0].hash, + }; + + const replyResponse = await post(`post-remove`, body); + assert.isOk(replyResponse?.status === 200, 'response was not okay'); + + const postsResponse = await get<{ + status: number; + rows: { + hash: string; + author: string; + message: string; + deleted_at: Date; + deleted_reason: string; + deleted_hash: string; + }[]; + }>(`posts?address=${addressUserA}`); + + assert.isOk(postsResponse?.status === 200, 'posts did not resolve'); + const data = postsResponse?.rows.find(x => x.hash === response.rows[0].hash); + assert.isUndefined(data, 'data was not hidden'); + }); + + it('pOST - /post-remove - No Permission', async () => { + const response = await get<{ status: number; rows: { hash: string; author: string; message: string }[] }>( + `posts?address=${addressUserA}`, + ); + assert.isOk(response, 'failed to fetch posts data'); + assert.isOk(Array.isArray(response.rows) && response.rows.length >= 1, 'feed result was not an array type'); + + const body: typeof Posts.PostRemoveBody.static = { + from: `${addressUserA}abcd`, + hash: getRandomHash(), + timestamp: '2025-04-16T19:46:42Z', + post_hash: response.rows[0].hash, + }; + + const replyResponse = await post(`post-remove`, body); + assert.isOk(replyResponse?.status === 200, 'response was not okay'); + + const postsResponse = await get<{ + status: number; + rows: { + hash: string; + author: string; + message: string; + deleted_at: Date; + deleted_reason: string; + deleted_hash: string; + }[]; + }>(`posts?address=${addressUserA}`); + + assert.isOk(postsResponse?.status === 200, 'posts did not resolve'); + const data = postsResponse?.rows.find(x => x.hash === response.rows[0].hash); + assert.isOk(data, 'data was hidden'); + }); + + it('pOST - /mod/post-remove - No Permission', async () => { + const response = await get<{ status: number; rows: { hash: string; author: string; message: string }[] }>( + `posts?address=${addressUserA}`, + ); + assert.isOk(response, 'failed to fetch posts data'); + assert.isOk(Array.isArray(response.rows) && response.rows.length >= 1, 'feed result was not an array type'); + + const body: typeof Posts.PostRemoveBody.static = { + from: `${addressUserA}abcd`, + hash: getRandomHash(), + timestamp: '2025-04-16T19:46:42Z', + post_hash: response.rows[0].hash, + }; + + const replyResponse = await post(`post-remove`, body); + assert.isOk(replyResponse?.status === 200, 'response was not okay'); + + const postsResponse = await get<{ + status: number; + rows: { + hash: string; + author: string; + message: string; + deleted_at: Date; + deleted_reason: string; + deleted_hash: string; + }[]; + }>(`posts?address=${addressUserA}`); + + assert.isOk(postsResponse?.status === 200, 'posts did not resolve'); + const data = postsResponse?.rows.find(x => x.hash === response.rows[0].hash); + assert.isOk(data, 'data was hidden'); + }); }); diff --git a/packages/api-main/tsconfig.json b/packages/api-main/tsconfig.json index f3adf71b..4af00f3e 100644 --- a/packages/api-main/tsconfig.json +++ b/packages/api-main/tsconfig.json @@ -1,19 +1,19 @@ { - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "lib": ["ESNext", "DOM"], - "outDir": "./dist", - "strict": true, - "allowJs": true, - "checkJs": false, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "rootDir": ".", - "types": ["vitest/importMeta"] - }, - "include": ["src", "drizzle.config.ts", "db", "drizzle/schema.ts", "drizzle/db.ts"], - "exclude": ["node_modules", "dist"] + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext", "DOM"], + "rootDir": ".", + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["vitest/importMeta"], + "allowJs": true, + "checkJs": false, + "strict": true, + "outDir": "./dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": ["src", "drizzle.config.ts", "db", "drizzle/schema.ts", "drizzle/db.ts"], + "exclude": ["node_modules", "dist"] } diff --git a/packages/api-main/vitest.config.ts b/packages/api-main/vitest.config.ts index b97b1eb2..1f7ffaf3 100644 --- a/packages/api-main/vitest.config.ts +++ b/packages/api-main/vitest.config.ts @@ -1,27 +1,27 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ - test: { - sequence: { - setupFiles: 'list', - }, - globalSetup: './tests/setup.ts', - forceRerunTriggers: [ - '**/tests/**/*', - ], - pool: 'forks', - poolOptions: { - forks: { - singleFork: true, - }, - }, - // reporters: ['verbose'], + test: { + sequence: { + setupFiles: 'list', }, - define: { - 'process.env.SKIP_START': JSON.stringify(true), - 'process.env.PG_URI': JSON.stringify('postgresql://default:password@localhost:5432/postgres'), - 'process.env.JWT': JSON.stringify('default_jwt_secret'), - 'process.env.JWT_STRICTNESS': JSON.stringify('lax'), - 'process.env.AUTH': JSON.stringify('whatever'), + globalSetup: './tests/setup.ts', + forceRerunTriggers: [ + '**/tests/**/*', + ], + pool: 'forks', + poolOptions: { + forks: { + singleFork: true, + }, }, + // reporters: ['verbose'], + }, + define: { + 'process.env.SKIP_START': JSON.stringify(true), + 'process.env.PG_URI': JSON.stringify('postgresql://default:password@localhost:5432/postgres'), + 'process.env.JWT': JSON.stringify('default_jwt_secret'), + 'process.env.JWT_STRICTNESS': JSON.stringify('lax'), + 'process.env.AUTH': JSON.stringify('whatever'), + }, }); diff --git a/packages/cli/README.md b/packages/cli/README.md index 4e4c66a6..c17abf00 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -18,4 +18,4 @@ go run main.go ## Endpoint -Modify `config.json` to change the `rpc` endpoint. \ No newline at end of file +Modify `config.json` to change the `rpc` endpoint. diff --git a/packages/cli/config.json b/packages/cli/config.json index 718bca69..d4aa044b 100644 --- a/packages/cli/config.json +++ b/packages/cli/config.json @@ -1,3 +1,3 @@ { "rpc_endpoint": "https://atomone-testnet-1-rpc.allinbits.services" -} \ No newline at end of file +} diff --git a/packages/frontend-main/STYLEGUIDE.md b/packages/frontend-main/STYLEGUIDE.md index a4cb5187..3c4f5d92 100644 --- a/packages/frontend-main/STYLEGUIDE.md +++ b/packages/frontend-main/STYLEGUIDE.md @@ -33,10 +33,10 @@ This document defines the coding standards, patterns, and constraints for the Di ```json { - "tabWidth": 4, - "singleQuote": true, - "semi": true, - "printWidth": 120 + "tabWidth": 4, + "singleQuote": true, + "semi": true, + "printWidth": 120 } ``` @@ -55,29 +55,29 @@ This document defines the coding standards, patterns, and constraints for the Di ```vue ``` @@ -151,19 +154,19 @@ onMounted(() => { ```typescript // Good - Explicit interface interface Props { - title: string; - isVisible?: boolean; - items: Array<{ id: string; name: string }>; + title: string; + isVisible?: boolean; + items: Array<{ id: string; name: string }>; } const props = withDefaults(defineProps(), { - isVisible: false, + isVisible: false, }); // Bad - Inline props const props = defineProps<{ - title: string; - isVisible?: boolean; + title: string; + isVisible?: boolean; }>(); ``` @@ -172,9 +175,9 @@ const props = defineProps<{ ```typescript // Good - Explicit interface interface Emits { - update: [value: string]; - close: []; - error: [error: Error]; + update: [value: string]; + close: []; + error: [error: Error]; } const emit = defineEmits(); @@ -188,24 +191,24 @@ const emit = defineEmits(['update', 'close', 'error']); ```typescript // Good - Return object with named properties export function useWallet() { - const address = ref(''); - const isConnected = computed(() => !!address.value); - - function connect() { - // Connection logic - } - - return { - address: readonly(address), - isConnected, - connect, - }; + const address = ref(''); + const isConnected = computed(() => !!address.value); + + function connect() { + // Connection logic + } + + return { + address: readonly(address), + isConnected, + connect, + }; } // Bad - Return array export function useWallet() { - const address = ref(''); - return [address, connect]; + const address = ref(''); + return [address, connect]; } ``` @@ -215,14 +218,18 @@ export function useWallet() { ```vue ``` @@ -231,16 +238,16 @@ export function useWallet() { ```css /* Good - Use design tokens */ .component { - background-color: var(--background); - color: var(--foreground); - border-radius: var(--radius); + background-color: var(--background); + color: var(--foreground); + border-radius: var(--radius); } /* Bad - Hard-coded values */ .component { - background-color: #ffffff; - color: #000000; - border-radius: 8px; + background-color: #ffffff; + color: #000000; + border-radius: 8px; } ``` @@ -252,18 +259,18 @@ export function useWallet() { import { cn } from '@/utility'; const props = defineProps<{ - variant: 'primary' | 'secondary'; - size: 'sm' | 'md' | 'lg'; + variant: 'primary' | 'secondary'; + size: 'sm' | 'md' | 'lg'; }>(); const buttonClasses = computed(() => - cn('px-4 py-2 rounded-md font-medium transition-colors', { - 'bg-primary text-primary-foreground': props.variant === 'primary', - 'bg-secondary text-secondary-foreground': props.variant === 'secondary', - 'px-2 py-1 text-sm': props.size === 'sm', - 'px-4 py-2': props.size === 'md', - 'px-6 py-3 text-lg': props.size === 'lg', - }), + cn('px-4 py-2 rounded-md font-medium transition-colors', { + 'bg-primary text-primary-foreground': props.variant === 'primary', + 'bg-secondary text-secondary-foreground': props.variant === 'secondary', + 'px-2 py-1 text-sm': props.size === 'sm', + 'px-4 py-2': props.size === 'md', + 'px-6 py-3 text-lg': props.size === 'lg', + }), ); ``` @@ -275,43 +282,43 @@ const buttonClasses = computed(() => ```typescript // Good - Clear store structure export const useWalletStore = defineStore( - 'wallet', - () => { - // State - const address = ref(''); - const isConnected = ref(false); - - // Getters - const shortAddress = computed(() => (address.value ? shorten(address.value) : '')); - - // Actions - function setAddress(newAddress: string) { - address.value = newAddress; - isConnected.value = !!newAddress; - } - - function disconnect() { - address.value = ''; - isConnected.value = false; - } - - return { - // State - address: readonly(address), - isConnected: readonly(isConnected), - // Getters - shortAddress, - // Actions - setAddress, - disconnect, - }; - }, - { - persist: { - storage: sessionStorage, - pick: ['address', 'isConnected'], - }, + 'wallet', + () => { + // State + const address = ref(''); + const isConnected = ref(false); + + // Getters + const shortAddress = computed(() => (address.value ? shorten(address.value) : '')); + + // Actions + function setAddress(newAddress: string) { + address.value = newAddress; + isConnected.value = !!newAddress; + } + + function disconnect() { + address.value = ''; + isConnected.value = false; + } + + return { + // State + address: readonly(address), + isConnected: readonly(isConnected), + // Getters + shortAddress, + // Actions + setAddress, + disconnect, + }; + }, + { + persist: { + storage: sessionStorage, + pick: ['address', 'isConnected'], }, + }, ); ``` @@ -320,19 +327,19 @@ export const useWalletStore = defineStore( ```typescript // Good - Proper query structure export function useFeed() { - const queryClient = useQueryClient(); - - return useInfiniteQuery({ - queryKey: ['feed'], - queryFn: async ({ pageParam = 0 }) => { - const response = await fetch(`/api/feed?page=${pageParam}`); - return response.json(); - }, - getNextPageParam: (lastPage, allPages) => { - return lastPage.hasMore ? allPages.length : undefined; - }, - staleTime: 5 * 60 * 1000, // 5 minutes - }); + const queryClient = useQueryClient(); + + return useInfiniteQuery({ + queryKey: ['feed'], + queryFn: async ({ pageParam = 0 }) => { + const response = await fetch(`/api/feed?page=${pageParam}`); + return response.json(); + }, + getNextPageParam: (lastPage, allPages) => { + return lastPage.hasMore ? allPages.length : undefined; + }, + staleTime: 5 * 60 * 1000, // 5 minutes + }); } ``` @@ -344,25 +351,20 @@ export function useFeed() { - - - - + + + + ``` ### Use These Patterns Instead @@ -391,30 +400,32 @@ import { ref } from 'vue'; const count = ref(0); function increment() { - count.value++; + count.value++; } - - - - - - + + + + + + ``` ## Code Quality Rules diff --git a/packages/frontend-main/components.json b/packages/frontend-main/components.json index e99b45b8..fb3c596b 100644 --- a/packages/frontend-main/components.json +++ b/packages/frontend-main/components.json @@ -16,4 +16,4 @@ "ui": "@/components/ui" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/packages/frontend-main/index.html b/packages/frontend-main/index.html index aaadcdac..c21cc285 100644 --- a/packages/frontend-main/index.html +++ b/packages/frontend-main/index.html @@ -1,26 +1,39 @@ - + - - - - - dither.chat - - - - - - - - - - - - - - - -
- - + + + + + dither.chat + + + + + + + + + + + + + + + +
+ + diff --git a/packages/frontend-main/package.json b/packages/frontend-main/package.json index 4aff1e75..9f6a380b 100644 --- a/packages/frontend-main/package.json +++ b/packages/frontend-main/package.json @@ -1,57 +1,50 @@ { - "name": "frontend-main", - "private": true, - "version": "0.0.1", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vue-tsc -b && vite build", - "preview": "vite preview" - }, - "dependencies": { - "@cosmjs/math": "^0.33.1", - "@cosmjs/proto-signing": "^0.32.4", - "@cosmjs/stargate": "^0.32.4", - "@cosmostation/cosmos-client": "^0.0.5", - "@sinclair/typebox": "^0.34.41", - "@tailwindcss/vite": "^4.1.14", - "@tanstack/vue-query": "^5.90.3", - "@vueuse/core": "^13.9.0", - "bech32": "^2.0.0", - "bignumber.js": "^9.3.1", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cosmjs-types": "^0.9.0", - "json-editor-vue": "^0.18.1", - "lucide-vue-next": "^0.507.0", - "mitt": "^3.0.1", - "pinia": "^3.0.3", - "pinia-plugin-persistedstate": "^4.5.0", - "reka-ui": "^2.5.1", - "stint-signer": "^0.4.0", - "tailwind-merge": "^3.3.1", - "tailwindcss": "^4.1.14", - "tw-animate-css": "^1.4.0", - "vanilla-jsoneditor": "^3.10.0", - "vue": "^3.5.22", - "vue-i18n": "^10.0.8", - "vue-router": "^4.6.0", - "vue-sonner": "^2.0.9" - }, - "devDependencies": { - "@keplr-wallet/types": "^0.12.280", - "@types/node": "^22.18.10", - "@vitejs/plugin-vue": "^5.2.4", - "@vue/tsconfig": "^0.7.0", - "typescript": "~5.7.3", - "vite": "^6.3.7", - "vite-plugin-node-polyfills": "^0.23.0", - "vue-tsc": "^2.2.12" - }, - "prettier": { - "tabWidth": 4, - "singleQuote": true, - "semi": true, - "printWidth": 120 - } + "name": "frontend-main", + "type": "module", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@cosmjs/math": "^0.33.1", + "@cosmjs/proto-signing": "^0.32.4", + "@cosmjs/stargate": "^0.32.4", + "@cosmostation/cosmos-client": "^0.0.5", + "@sinclair/typebox": "^0.34.41", + "@tailwindcss/vite": "^4.1.14", + "@tanstack/vue-query": "^5.90.3", + "@vueuse/core": "^13.9.0", + "bech32": "^2.0.0", + "bignumber.js": "^9.3.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cosmjs-types": "^0.9.0", + "json-editor-vue": "^0.18.1", + "lucide-vue-next": "^0.507.0", + "mitt": "^3.0.1", + "pinia": "^3.0.3", + "pinia-plugin-persistedstate": "^4.5.0", + "reka-ui": "^2.5.1", + "stint-signer": "^0.4.0", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.14", + "tw-animate-css": "^1.4.0", + "vanilla-jsoneditor": "^3.10.0", + "vue": "^3.5.22", + "vue-i18n": "^10.0.8", + "vue-router": "^4.6.0", + "vue-sonner": "^2.0.9" + }, + "devDependencies": { + "@keplr-wallet/types": "^0.12.280", + "@vitejs/plugin-vue": "^5.2.4", + "@vue/tsconfig": "^0.7.0", + "typescript": "~5.7.3", + "vite": "^6.3.7", + "vite-plugin-node-polyfills": "^0.23.0", + "vue-tsc": "^2.2.12" + } } diff --git a/packages/frontend-main/src/App.vue b/packages/frontend-main/src/App.vue index f01ecc60..871b2ede 100644 --- a/packages/frontend-main/src/App.vue +++ b/packages/frontend-main/src/App.vue @@ -21,28 +21,27 @@ const wallet = useWallet(); // Fetch balance when wallet state is loaded from storage onMounted(() => { - // Wait for next tick to ensure state is loaded from storage - nextTick(() => { - if (wallet.address.value) { - wallet.refreshAddress(); - balanceFetcher.updateAddress(wallet.address.value); - } - }); + // Wait for next tick to ensure state is loaded from storage + nextTick(() => { + if (wallet.address.value) { + wallet.refreshAddress(); + balanceFetcher.updateAddress(wallet.address.value); + } + }); }); -