diff --git a/.cursor/rules/discord-bot/RULE.md b/.cursor/rules/discord-bot/RULE.md index d104f81f..bb416e9a 100644 --- a/.cursor/rules/discord-bot/RULE.md +++ b/.cursor/rules/discord-bot/RULE.md @@ -1,5 +1,6 @@ --- description: "Discord.js patterns and bot class architecture for ArtBot" +globs: "**/Classes/**,**/Utils/smartBotResponse.ts,**/Utils/activityTriager.ts,**/index.ts" alwaysApply: false --- @@ -7,69 +8,48 @@ alwaysApply: false ## Bot Class Architecture -Bot classes follow a consistent pattern: - ```typescript export class SomeBot { - // Private fields first private bot: Client private someState: Map = new Map() constructor(bot: Client, otherDeps?: OtherType) { this.bot = bot - // Synchronous initialization only in constructor } - // Async initialization in separate init() method if needed async init() { await this.buildSomething() - - // Set up intervals for periodic tasks - setInterval(() => { - this.periodicTask() - }, 60000) + setInterval(() => this.periodicTask(), 60000) } - // Public handler methods async handleSomeEvent(event: EventType) { try { - // Handle the event + // ... } catch (err) { - console.error('Error handling event:', err) + logger.error({ err }, 'Error handling event') } } - // Private helper methods - private helperMethod(): void { - // Implementation - } - - // Cleanup method for resource management cleanup() { - // Clear intervals, close connections, reset state this.someState.clear() } } ``` -## Message Handling +Register bots that need shutdown cleanup on `botsToCleanup` in `index.ts` (SIGINT/SIGTERM). MintBot/TriviaBot/TwitterBot are not all on that list today — be careful adding long-lived intervals. -Check if channel is sendable before sending messages: +## Message Handling ```typescript async handleNumberMessage(msg: Message) { - if (!msg.channel.isSendable()) { - return - } - - // Process message - msg.channel.send('Response') + if (!msg.channel.isSendable()) return + await msg.channel.send('Response') } ``` -## Embed Messages +Use `logger` (pino) from `src/logger` / project logger helpers — not `console.error`. -Use `EmbedBuilder` for rich message formatting: +## Embeds ```typescript import { EmbedBuilder } from 'discord.js' @@ -77,66 +57,37 @@ import { EmbedBuilder } from 'discord.js' const embed = new EmbedBuilder() .setTitle('Token Name - Artist') .setURL('https://artblocks.io/...') - .setColor(0x00ff00) .setImage(assetUrl) - .setDescription('Description text') .addFields( { name: 'Owner', value: ownerText, inline: true }, { name: 'Price', value: priceText, inline: true } ) - .setFooter({ text: 'Footer text' }) -msg.channel.send({ embeds: [embed] }) +await msg.channel.send({ embeds: [embed] }) ``` -## Event Listeners +## Message Routing -Handle Discord events via the client exported from index.ts: +`#` prefix → project/token queries via `ArtIndexerBot` / `projectConfig`: -```typescript -import { discordClient } from '..' - -discordClient.on(Events.MessageCreate, async (msg) => { - const content = msg.content - const channelID = msg.channel.id - - if (content.startsWith('#')) { - // Handle # commands - } -}) +- `#42 fidenza` — specific token +- `#? squiggle` — random +- `#entry curated` — lowest listed in vertical +- `#set AB500` — set collection helpers -discordClient.on('ready', () => { - console.log(`Logged in as ${discordClient.user?.tag}!`) -}) -``` +Non-`#` → `smartBotResponse.ts`. Channel must be present in active `channels*.json`. -## Process Signal Handling +## OpenSea Dual Path -Register cleanup handlers for graceful shutdown: +1. **Stream** (`@opensea/stream-js`): primary listings + sales; gated by `PRODUCTION_MODE` in handlers +2. **REST poll** (`OpenSeaEventsPollBot`): sales backfill; registers stream sale IDs to dedupe -```typescript -// In index.ts -const botsToCleanup: { cleanup: () => void }[] = [] - -// Add bots that need cleanup -botsToCleanup.push(openSeaListBot) -botsToCleanup.push(openSeaSaleBot) - -process.on('SIGINT', () => { - console.log('Received SIGINT. Cleaning up...') - botsToCleanup.forEach((bot) => bot.cleanup()) - discordClient.destroy() - process.exit(0) -}) -``` +Activity posts go through `activityTriager.ts` (hardcoded artist/platform → channel mapping + ban list). New sale destinations usually need code there, not only JSON. -## Message Routing +## Mint Webhook -The `#` prefix triggers project/token queries: +`POST /new-mint` — Hasura payload; auth header `webhook_secret` must equal `MINT_WEBHOOK_SECRET`. MintBot polls media-proxy until an image is ready, then posts using `mintBotConfig.json` channel names. -- `#42 fidenza` - Get Fidenza #42 -- `#? squiggle` - Random Chromie Squiggle -- `#entry curated` - Lowest priced Curated token -- `#set AB500` - Set collection data +## Process Signals -Messages are routed through `ArtIndexerBot.handleNumberMessage()` which normalizes project names via `toProjectKey()` and dispatches to the appropriate `ProjectBot`. +`SIGINT`/`SIGTERM` in `index.ts`: OpenSea cleanup → `botsToCleanup` → `discordClient.destroy()`. diff --git a/.cursor/rules/graphql-hasura/RULE.md b/.cursor/rules/graphql-hasura/RULE.md index a1741f96..b7975fcb 100644 --- a/.cursor/rules/graphql-hasura/RULE.md +++ b/.cursor/rules/graphql-hasura/RULE.md @@ -1,17 +1,22 @@ --- description: "GraphQL and Hasura patterns for ArtBot data queries" +globs: "**/Data/**,**/generated/**,**/codegen.ts" alwaysApply: false --- # GraphQL & Hasura Patterns -## Query File Location +## Endpoint -Define all GraphQL queries in `.graphql` files under `src/Data/graphql/`: +- Public Hasura: `https://data.artblocks.io/v1/graphql` (hardcoded in `queryGraphQL.ts` and `codegen.ts`) +- Optional auth: `HASURA_GRAPHQL_ADMIN_SECRET` → `x-hasura-admin-secret` +- **One client only** — there is no separate Arbitrum Hasura endpoint/client in this repo -```graphql -# src/Data/graphql/artbot-hasura-queries.graphql +## Query Files + +Define documents under `src/Data/graphql/`: +```graphql fragment ProjectDetail on projects_metadata { id project_id @@ -21,138 +26,42 @@ fragment ProjectDetail on projects_metadata { invocations max_invocations } - -query getProject($id: String!) { - projects_metadata(where: { id: { _eq: $id } }) { - ...ProjectDetail - } -} ``` ## Code Generation -After modifying `.graphql` files, regenerate TypeScript types: - ```bash yarn codegen ``` -This generates types in `generated/graphql.ts`. Import from there: +Writes `generated/graphql.ts` (gitignored). `postinstall` and pre-commit also run codegen. Import generated types/documents from there. -```typescript -import { - ProjectDetailFragment, - GetProjectDocument, - TokenDetailFragment, -} from '../../generated/graphql' -``` +## Query Wrapper Pattern -## Query Function Pattern - -Create wrapper functions in `src/Data/queryGraphQL.ts`: +Wrappers live in `src/Data/queryGraphQL.ts` (urql, `requestPolicy: 'network-only'`). ```typescript -import { createClient } from 'urql/core' -import { GetProjectDocument, ProjectDetailFragment } from '../../generated/graphql' - -const client = createClient({ - url: PUBLIC_HASURA_ENDPOINT, - fetch: fetch, - requestPolicy: 'network-only', -}) - -export async function getProject(projectId: string): Promise { - const { data, error } = await client - .query(GetProjectDocument, { id: projectId }) - .toPromise() - - if (error) { - throw Error(error.message) - } - - if (!data || !data.projects_metadata?.length) { - throw Error('No data returned from getProject query') - } - - return data.projects_metadata[0] +export async function getProject( + projectId: number, + contractAddress?: string +): Promise { + // builds Hasura id, queries, throws if missing } ``` -## Pagination Pattern +Token IDs in Hasura are typically `{contractAddress}-{tokenId}` where tokenId encodes invocation + project number × 1e6. -For queries that may return many results, use pagination: +## Pagination -```typescript -const maxProjectsPerQuery = 1000 - -export async function getAllProjects(): Promise { - const allProjects: ProjectDetailFragment[] = [] - let loop = true - - while (loop) { - const { data } = await client - .query(GetAllProjectsDocument, { - first: maxProjectsPerQuery, - skip: allProjects.length, - }) - .toPromise() - - if (!data) { - throw Error('No data returned from query') - } - - allProjects.push(...data.projects_metadata) - - if (data.projects_metadata.length !== maxProjectsPerQuery) { - loop = false - } - } - - return allProjects -} -``` - -## Multi-Chain Support - -ArtBot supports both Ethereum mainnet and Arbitrum. Use the appropriate client: - -```typescript -const client = createClient({ url: PUBLIC_HASURA_ENDPOINT }) -const arbitrumClient = createClient({ url: PUBLIC_ARB_HASURA_ENDPOINT }) - -const getClientForContract = (contract: string) => { - if (isArbitrumContract(contract)) { - return arbitrumClient - } - return client -} -``` +Large lists use 1000-item loops (`first` / `skip`) until a short page is returned — see `getAllProjects` and similar helpers. -## Fragment Reuse +## Multi-Chain Note -Use fragments for consistent field selection across queries: +Hasura rows may include `chain_id` for Ethereum / Arbitrum / Base. That does **not** imply OpenSea Discord feeds cover those chains — stream filtering in `index.ts` is Ethereum-oriented. Do not document a phantom `arbitrumClient`. -```graphql -fragment TokenDetail on tokens_metadata { - invocation - preview_asset_url - live_view_url - owner { public_address } - list_price - list_currency_symbol - project { name, artist_name } - contract { token_base_url, name } -} +## After Schema/Query Changes -query getToken($token_id: String!) { - tokens_metadata(where: { id: { _eq: $token_id } }) { - ...TokenDetail - } -} - -query getWalletTokens($wallet: String!, $contracts: [String!]!) { - tokens_metadata(where: { owner_address: { _eq: $wallet } }) { - ...TokenDetail - } -} -``` +1. Edit `.graphql` +2. `yarn codegen` +3. Update `queryGraphQL.ts` callers/types +4. Smoke with `yarn start` (CI uses `PRODUCTION_MODE=false`) diff --git a/.cursor/rules/ops-gotchas/RULE.md b/.cursor/rules/ops-gotchas/RULE.md new file mode 100644 index 00000000..7eb18ef9 --- /dev/null +++ b/.cursor/rules/ops-gotchas/RULE.md @@ -0,0 +1,51 @@ +--- +description: "Operational gotchas, env flags, and deployment notes for ArtBot" +alwaysApply: true +--- + +# Ops & Gotchas + +Read `AGENTS.md` for the full matrix. This rule is the short always-on checklist. + +## Env Flags + +| Flag | Meaning | +|------|---------| +| `ARTBOT_IS_PROD=true` | Load `channels.json` + `projectBots.json` | +| `ARTBOT_IS_PROD` unset/false | Load `*_dev.json` (a-t test server) | +| `PRODUCTION_MODE=true` | Discord login + process OpenSea events + start poll bot | +| `PRODUCTION_MODE=false` | Boot Express/indexers; skip Discord login (CI) | + +Never assume one flag implies the other. + +## Deploy + +- Host: **Render.com**, command `yarn start`, binds `0.0.0.0:$PORT` +- No in-repo Dockerfile / health route; `/update` is a stub +- Shared Render IPs can hit Discord 429 — login retries/backoff exist in `index.ts` +- Secrets live in the host dashboard; keep `.env` out of git + +## Codegen + +`generated/` is gitignored. Fresh clones need `yarn install` or `yarn codegen`. Editing `.graphql` without codegen breaks TypeScript. + +## OpenSea / Chains + +- Stream handlers filter to **ethereum** NFT IDs +- REST sale poll uses **mainnet** `chainId: 1` +- Multi-chain mint webhooks can still arrive — do not conflate mint support with sale-feed support + +## Config Pitfalls + +- Contract addresses: **lowercase** +- `mintBotConfig.json` → channel **names** matching `channels.json` `name` +- `tokenIdTriggers` with `null` max: **broken**; use a large number +- Sales routing: `activityTriager.ts` (code), not channels JSON alone +- Update prod + `_dev` JSON when testing on a-t + +## Safe Agent Behavior + +- Prefer config PRs for channels/aliases/mappings +- Do not invent Reservoir / Google Sheets / Arbitrum-Hasura clients — removed or never present +- Do not commit secrets; `.env.example` is the template +- After mint outages, prefer `scripts/replay-mints.ts --dry-run` before posting diff --git a/.cursor/rules/project-config/RULE.md b/.cursor/rules/project-config/RULE.md index 14b058de..d945596a 100644 --- a/.cursor/rules/project-config/RULE.md +++ b/.cursor/rules/project-config/RULE.md @@ -1,28 +1,32 @@ --- description: "Project and channel configuration patterns for ArtBot" +globs: "**/ProjectConfig/**,**/NamedMappings/**" alwaysApply: false --- # Project Configuration -## Configuration Files +Full task checklists: `docs/MAINTENANCE.md`. Env flag that selects prod vs dev JSON: `ARTBOT_IS_PROD` (not `PRODUCTION_MODE`). -Configuration lives in `src/ProjectConfig/`: +## Configuration Files | File | Purpose | |------|---------| -| `channels.json` | Discord channel IDs and their project bot handlers | -| `channels_dev.json` | Development channel configuration | -| `projectBots.json` | Named mappings and custom config per project | -| `coreContracts.json` | Art Blocks core contract addresses | -| `partnerContracts.json` | Partner/Engine contract addresses | -| `collaborationContracts.json` | Collaboration contract addresses | -| `explorationsContracts.json` | Explorations contract addresses | +| `channels.json` / `channels_dev.json` | Discord channel IDs → name + `projectBotHandlers` | +| `projectBots.json` / `projectBots_dev.json` | Named mapping refs per bot ID | +| `coreContracts.json` | Art Blocks core contracts | +| `partnerContracts.json` | Engine partner contracts | +| `collaborationContracts.json` | Collab contracts (Pace, BM, …) | +| `explorationsContracts.json` | Explorations contracts | +| `blockedEngineContracts.json` | Excluded from Engine index | +| `blockedMintContracts.json` | Mint suppression | +| `stagingContracts.json` | Staging mint routing | +| `mintBotConfig.json` | Partner/collection → Discord channel **names** | +| `project_aliases.json` | User shorthand → project name | +| `contract_aliases.json` | Platform aliases | ## Channel Configuration -Each channel can have a `projectBotHandlers` object: - ```json { "123456789012345678": { @@ -40,40 +44,36 @@ Each channel can have a `projectBotHandlers` object: } ``` -- `default`: Project ID to handle messages by default -- `stringTriggers`: Map project IDs to trigger words -- `tokenIdTriggers`: Map project IDs to token ID ranges +- `default`: Bot ID when no trigger matches +- `stringTriggers`: Bot ID → trigger words (substring match on lowercase content) +- `tokenIdTriggers`: Bot ID → `[min, max]` inclusive ranges + +**Gotcha:** Open-ended ranges with `null` (e.g. `[555, null]`) are documented historically but **not handled** by the active comparison in `projectConfig.ts` (uses `<= ranges[1]`). Use a large numeric max until fixed. Prefer updating both `channels.json` and `channels_dev.json`. ## Contract Addresses -**Always use lowercase for contract addresses:** +Always lowercase: ```json { - "DOODLE": "0x28f2d3805652fb5d359486dffb7d08320d403240", - "PLOTTABLES": "0xa319c382a702682129fcbf55d514e61a16f97f9c" + "PLOTTABLES": "0xa319c382a702682129fcbf55d514e61a16f97f9c", + "HODLERS": "0x9f79e46a309f804aa4b7b53a1f72c69137427794" } ``` -## Named Mappings +Current partners live in `partnerContracts.json` (PLOTTABLES, BM, HODLERS, etc.). Do not copy stale DOODLE examples — DOODLE is blocked, not a live partner entry. -For projects with community-named tokens, create JSON files in `NamedMappings/`: +## Named Mappings ```json -// ringerSingles.json - single token aliases -{ - "goose": "879", - "theone": "109" -} +// *Singles.json — single token aliases +{ "goose": "879", "theone": "109" } -// ringerSets.json - sets of tokens -{ - "perfects": [109, 879, 1024], - "rainbows": [42, 156, 789] -} +// *Sets.json — sets of tokens +{ "perfects": [109, 879, 1024] } ``` -Reference in `projectBots.json`: +In `projectBots.json`: ```json { @@ -86,62 +86,20 @@ Reference in `projectBots.json`: } ``` -## Adding a New Project Channel +## mintBotConfig -1. Get the Discord channel ID -2. Add entry to `channels.json`: +Keys are collection types or partner contract names; values are arrays of channel **names** that must exist in `channels.json`: ```json { - "CHANNEL_ID": { - "name": "artist-channel-name", - "projectBotHandlers": { - "default": "PROJECT_ID" - } - } + "STUDIO": ["studio-mints"], + "PLOTTABLES": ["plottables-mints"] } ``` -3. Optionally add named mappings in `projectBots.json` - ## Adding a New Contract -1. Add to appropriate contracts file (lowercase address): - -```json -{ - "CONTRACT_NAME": "0xcontractaddress..." -} -``` - -2. If it's a new Engine partner, also add to `partnerContracts.json` - -## Environment-Based Config - -The bot loads different configs based on environment: - -```typescript -const ARTBOT_IS_PROD = process.env.ARTBOT_IS_PROD?.toLowerCase() === 'true' - -const CHANNELS = ARTBOT_IS_PROD - ? require('./channels.json') - : require('./channels_dev.json') - -const PROJECT_BOTS = ARTBOT_IS_PROD - ? require('./projectBots.json') - : require('./projectBots_dev.json') -``` - -## Project Aliases - -Common project name aliases are in `project_aliases.json`: - -```json -{ - "squig": "Chromie Squiggle", - "fiddy": "Fidenza", - "ringer": "Ringers" -} -``` - -These allow users to use shorthand in commands like `#? squig`. +1. Lowercase address → appropriate `*Contracts.json` +2. Mints → `mintBotConfig.json` (+ channel if needed) +3. Sales/listings → often `activityTriager.ts` as well +4. Update README partner table if public diff --git a/.cursor/rules/project-overview/RULE.md b/.cursor/rules/project-overview/RULE.md index 70e9a8d3..07abc67b 100644 --- a/.cursor/rules/project-overview/RULE.md +++ b/.cursor/rules/project-overview/RULE.md @@ -5,68 +5,70 @@ alwaysApply: true # ArtBot Project Overview -ArtBot is a Discord bot for the Art Blocks NFT platform. It provides information about Art Blocks projects, handles sales/listing feeds from OpenSea, and supports Twitter integration for notable sales. +ArtBot is the Art Blocks Discord bot: `#` project/token queries, OpenSea sales/listings, Hasura-driven mint posts, optional Twitter sales, and trivia. + +For full handoff context (env matrix, gotchas, ops), read **`AGENTS.md`** at the repo root. For step-by-step tasks, see **`docs/MAINTENANCE.md`**. ## Tech Stack -- **Runtime**: Node.js 20.x -- **Language**: TypeScript 5.x with strict mode enabled -- **Package Manager**: Yarn 4.3.1 +- **Runtime**: Node.js 20.x (`ts-node`, no separate build step) +- **Language**: TypeScript 5.x, strict mode +- **Package Manager**: Yarn 4.3.1 (`packageManager` field; use corepack) - **Discord**: discord.js v14 -- **GraphQL**: Hasura with urql client -- **NFT APIs**: OpenSea Stream API, OpenSea REST API -- **Blockchain**: viem for Ethereum interactions -- **Twitter**: twitter-api-v2 +- **GraphQL**: Hasura at `https://data.artblocks.io/v1/graphql` via urql +- **NFT activity**: OpenSea Stream API + OpenSea REST poll backfill +- **HTTP**: Express (`PORT`, default 3001) for mint webhook + Twitter OAuth +- **Other**: viem (ENS/mainnet helpers), twitter-api-v2, Supabase (trivia/Twitter state), OpenAI (trivia), pino logger +- **Hosting**: Render.com (long-running `yarn start`) ## Project Structure ``` src/ -├── index.ts # Main entry point, Discord client, bot initialization -├── Classes/ # Bot class implementations -│ ├── APIBots/ # External API integration bots (OpenSea, etc.) -│ ├── ArtIndexerBot.ts # Project indexing and message routing -│ ├── ProjectBot.ts # Individual project handlers -│ ├── MintBot.ts # Mint event handling -│ ├── TwitterBot.ts # Twitter posting functionality -│ └── TriviaBot.ts # Trivia game functionality -├── Data/ # Data access layer -│ ├── graphql/ # GraphQL queries (.graphql files) -│ ├── queryGraphQL.ts # GraphQL client and query functions -│ └── supabase.ts # Supabase client -├── ProjectConfig/ # Configuration files -│ ├── channels.json # Discord channel configurations -│ ├── projectBots.json # Project-specific bot configurations -│ ├── *Contracts.json # Contract address configurations -│ └── projectConfig.ts # Configuration loading/parsing -├── NamedMappings/ # Project-specific token name mappings -└── Utils/ # Utility functions - ├── smartBotResponse.ts # FAQ/help response handling - ├── activityTriager.ts # Sales/listing channel routing - └── common.ts # Shared utilities +├── index.ts # Express + Discord + OpenSea stream entry +├── Classes/ +│ ├── APIBots/ # OpenSea list/sale/poll bots +│ ├── ArtIndexerBot.ts # Project index + # routing +│ ├── ProjectBot.ts # Per-project handlers +│ ├── MintBot.ts # Mint queue → Discord +│ ├── TwitterBot.ts # Optional Twitter sale posts +│ ├── TriviaBot.ts # Trivia game +│ └── SchedulerBot.ts # Birthdays + trivia cron +├── Data/ +│ ├── graphql/ # .graphql documents +│ ├── queryGraphQL.ts # urql client + wrappers +│ └── supabase.ts # Trivia + Twitter state only +├── ProjectConfig/ # channels, contracts, aliases, mintBotConfig +├── NamedMappings/ # Token singles/sets JSON +└── Utils/ + ├── smartBotResponse.ts # Non-# / mention responses + ├── activityTriager.ts # Sale/list channel routing (code, not JSON) + └── common.ts +scripts/replay-mints.ts # Replay missed mint posts +generated/graphql.ts # Codegen output (gitignored) ``` ## Key Concepts -- **ProjectBot**: Handles queries for a specific Art Blocks project (e.g., Fidenza, Chromie Squiggles) -- **ArtIndexerBot**: Indexes all projects and routes `#` commands to the right ProjectBot -- **Named Mappings**: Aliases for specific tokens or sets (e.g., "the goose" for a specific Squiggle) -- **Verticals**: Art Blocks collections (Curated, Presents, Explorations, Heritage, Collaborations) - -## Environment Configuration - -- `PRODUCTION_MODE` - Set to "true" for production behavior -- `ARTBOT_IS_PROD` - Set to "true" to use production config files -- `DISCORD_TOKEN` - Discord bot token -- `OPENSEA_API_KEY` - OpenSea API key -- `TWITTER_ENABLED` - Set to "true" to enable Twitter posting +- **ProjectBot**: One Art Blocks (or partner) project +- **ArtIndexerBot**: Indexes projects from Hasura; routes `#` commands; four instances (main, Engine, Pace, BM) +- **Bot ID**: `{projectId}` or `{projectId}-{CONTRACT_NAME}` +- **Named mappings**: Community aliases for tokens/sets +- **`ARTBOT_IS_PROD` vs `PRODUCTION_MODE`**: Independent flags — see `AGENTS.md` ## Development Commands ```bash -yarn start # Run the bot -yarn dev # Same as start -yarn format # Format code with Prettier -yarn lint # Lint with ESLint -yarn codegen # Regenerate GraphQL types +yarn install # postinstall runs codegen +yarn start # run bot +yarn codegen # after .graphql edits +yarn lint-and-format ``` + +## Agent Defaults + +- Prefer JSON config changes for channels/aliases/mappings/contracts +- Use `logger` (pino), not `console.*` +- Run `yarn codegen` after GraphQL document changes +- Contract addresses: lowercase +- `mintBotConfig.json` values are channel **names**, not IDs diff --git a/.cursor/rules/typescript-patterns/RULE.md b/.cursor/rules/typescript-patterns/RULE.md index ea80b27d..43514a39 100644 --- a/.cursor/rules/typescript-patterns/RULE.md +++ b/.cursor/rules/typescript-patterns/RULE.md @@ -1,5 +1,6 @@ --- description: "TypeScript coding conventions and patterns for ArtBot" +globs: "**/*.{ts,tsx}" alwaysApply: false --- @@ -29,7 +30,6 @@ const name = project.name // Error if project could be null - Import generated types from `generated/graphql.ts` for GraphQL data ```typescript -// Interface for object shapes interface SaleEvent { contractAddress: string tokenId: string @@ -37,55 +37,40 @@ interface SaleEvent { currency: string } -// Type for unions type MessageType = 'random' | 'project' | 'artist' | 'wallet' -// Explicit return types -async function getProject(id: string): Promise { +async function getProject( + projectId: number, + contractAddress?: string +): Promise { // ... } ``` ## Async Patterns -Use `async/await` over raw promises: - -```typescript -// Good -async function fetchData() { - const result = await client.query(SomeDocument, {}).toPromise() - return result.data -} - -// Avoid chained .then() when possible -``` +Prefer `async/await` over chained `.then()`. ## Import Organization -Order imports: external packages first, then internal modules: +External packages first, then internal modules: ```typescript -// External packages import { Client, EmbedBuilder } from 'discord.js' -import axios from 'axios' +import fetch from 'node-fetch' -// Internal modules import { ProjectBot } from './ProjectBot' import { getProject } from '../Data/queryGraphQL' -import { CHANNEL_BLOCK_TALK } from '..' +import { logger } from '../logger' ``` ## Error Handling -Use try/catch blocks around external API calls. Log errors with context: - ```typescript try { const data = await someApiCall() - // process data } catch (err) { - console.error('Error in someOperation:', err) - // Handle gracefully - don't rethrow unless necessary + logger.error({ err }, 'Error in someOperation') } ``` diff --git a/.env.example b/.env.example index c20ac205..bb2f62d2 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,28 @@ GOOGLE_AUTH_DETAILS={} METADATA_REFRESH_INTERVAL_MINUTES=60 -OPENSEA_API_KEY= -RANDOM_ART_INTERVAL_MINUTES=20 -RESERVOIR_API_KEY= -DISCORD_TOKEN= -ETHERSCAN_API_KEY= +OPENSEA_API_KEY= +DISCORD_TOKEN= +ETHERSCAN_API_KEY= MINT_REFRESH_TIME_SECONDS=30 -PRODUCTION_MODE=true \ No newline at end of file +MINT_WEBHOOK_SECRET= +PRODUCTION_MODE=true +ARTBOT_IS_PROD=false + +# Optional features +# TWITTER_ENABLED=false +# AB_TWITTER_API_KEY= +# AB_TWITTER_API_SECRET= +# AB_TWITTER_OAUTH_TOKEN= +# AB_TWITTER_OAUTH_SECRET= +# AB_TWITTER_CLIENT_ID= +# AB_TWITTER_CLIENT_SECRET= +# TWITTER_CALLBACK_URL= +# HASURA_GRAPHQL_ADMIN_SECRET= +# SUPABASE_URL= +# SUPABASE_API_KEY= +# TRIVIA_TABLE= +# OPENAI_API_KEY= +# TRIVIA_CADENCE=0 +# PORT=3001 + +# See AGENTS.md for the full env matrix (ARTBOT_IS_PROD vs PRODUCTION_MODE). diff --git a/.github/ISSUE_TEMPLATE/curated-project-bot-support.md b/.github/ISSUE_TEMPLATE/curated-project-bot-support.md index 09f25da5..e2234931 100644 --- a/.github/ISSUE_TEMPLATE/curated-project-bot-support.md +++ b/.github/ISSUE_TEMPLATE/curated-project-bot-support.md @@ -3,7 +3,6 @@ name: Curated Project Bot Support about: 'Ticket for requesting bot support for a new Curated project. ' title: '[CURATED PROJECT] Project #N' labels: curated, project-query-support -assignees: grantoesterling --- ###################################################################### diff --git a/.github/ISSUE_TEMPLATE/new-command.md b/.github/ISSUE_TEMPLATE/new-command.md index 87297b24..d4f90d7e 100644 --- a/.github/ISSUE_TEMPLATE/new-command.md +++ b/.github/ISSUE_TEMPLATE/new-command.md @@ -3,7 +3,6 @@ name: New Command about: 'Ticket for requesting a new command (e.g. 'staysafe?') or trigger (e.g. 'otc')' title: 'New Command: ' labels: command -assignees: grantoesterling --- ###################################################################### diff --git a/.github/ISSUE_TEMPLATE/new-project-alias.md b/.github/ISSUE_TEMPLATE/new-project-alias.md index d970c449..5653e404 100644 --- a/.github/ISSUE_TEMPLATE/new-project-alias.md +++ b/.github/ISSUE_TEMPLATE/new-project-alias.md @@ -3,7 +3,6 @@ name: Project Alias about: 'Ticket for requesting bot support for new project aliases.' title: '[ALIAS] Project #N' labels: project-query-support -assignees: grantoesterling --- ###################################################################### diff --git a/.github/ISSUE_TEMPLATE/new-project-set.md b/.github/ISSUE_TEMPLATE/new-project-set.md index 873028dd..fbd1fb94 100644 --- a/.github/ISSUE_TEMPLATE/new-project-set.md +++ b/.github/ISSUE_TEMPLATE/new-project-set.md @@ -3,7 +3,6 @@ name: Project Named Set about: 'Ticket for requesting bot support for new named sets. ' title: '[SETS] Project #N' labels: project-query-support -assignees: grantoesterling --- ###################################################################### diff --git a/.github/ISSUE_TEMPLATE/new-project-singles.md b/.github/ISSUE_TEMPLATE/new-project-singles.md index 93aff951..f03abd18 100644 --- a/.github/ISSUE_TEMPLATE/new-project-singles.md +++ b/.github/ISSUE_TEMPLATE/new-project-singles.md @@ -3,7 +3,6 @@ name: Project Named Singles about: 'Ticket for requesting bot support for new named singles. ' title: '[SINGLES] Project #N' labels: project-query-support -assignees: grantoesterling --- ###################################################################### diff --git a/.github/ISSUE_TEMPLATE/playground-project-bot-support.md b/.github/ISSUE_TEMPLATE/playground-project-bot-support.md index 02c2f6ca..2dc43c0f 100644 --- a/.github/ISSUE_TEMPLATE/playground-project-bot-support.md +++ b/.github/ISSUE_TEMPLATE/playground-project-bot-support.md @@ -3,7 +3,6 @@ name: Playground Project Bot Support about: Ticket for requesting bot support for a new Playground project title: '[PLAYGROUND PROJECT] Project #N' labels: playground, project-query-support -assignees: grantoesterling --- ###################################################################### diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..ba3d2e2d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,164 @@ +# ArtBot — Agent & Maintainer Guide + +Canonical context for AI agents (Cursor, Claude Code, etc.) and human maintainers. Prefer this file over the README when they disagree; keep both in sync when you change behavior. + +## What this is + +ArtBot is the Art Blocks Discord bot: project/token queries (`#42 fidenza`), OpenSea sales/listings feeds, mint announcements (Hasura webhook), optional Twitter sale posts, and scheduled trivia. + +It is a **single long-running Node process** (Express HTTP + Discord.js + OpenSea stream). Hosted on **Render.com** (see comment in `src/index.ts`). There is no Dockerfile in-repo; deploy is `yarn start` with env vars set in the host. + +## Quick commands + +```bash +corepack enable # Yarn 4 via packageManager field +yarn install # also runs postinstall → yarn codegen +yarn start # ts-node src/index.ts +yarn codegen # regenerate generated/graphql.ts from Hasura schema +yarn lint +yarn format +yarn lint-and-format +``` + +- **Node**: 20.x (see `.node-version`, `engines`) +- **Package manager**: Yarn 4.3.1 (`nodeLinker: node-modules`) — do not switch to npm for day-to-day work +- **`generated/` is gitignored** — a fresh clone must `yarn install` or `yarn codegen` before TypeScript works +- **Pre-commit** (husky): `yarn codegen && yarn lint-and-format` +- **Tests**: `yarn test` exists but there are **no test files**; CI does not run Jest +- **CI** (`.github/workflows/build-check.yml`): install → codegen → `timeout 1m yarn start` with `PRODUCTION_MODE=false` (exit 124 = success) + +## Two production flags (critical) + +These are **independent**. Mis-setting them is the most common ops footgun. + +| Env var | Controls | Prod value | Local Discord testing | +|---------|----------|------------|------------------------| +| `ARTBOT_IS_PROD` | Which JSON configs load (`channels.json` / `projectBots.json` vs `*_dev.json`) | `true` | `false` (a-t test server channels) | +| `PRODUCTION_MODE` | Discord login, OpenSea event handling, OpenSea REST poll bot | `true` | `true` if you want live Discord/OpenSea; CI uses `false` | + +Local pattern that works: `ARTBOT_IS_PROD=false` + `PRODUCTION_MODE=true` + test-server `DISCORD_TOKEN` + join [a-t Discord](https://discord.gg/W6eYPpEk3a). + +## Architecture (actual) + +``` +src/index.ts + ├── Express (:PORT, default 3001) + │ POST /new-mint ← Hasura mint webhook (header: webhook_secret) + │ GET /callback ← Twitter OAuth + │ GET|POST /update ← stub OK responses (not a real health check) + ├── Discord client (if PRODUCTION_MODE) + │ MessageCreate → # commands OR smartBotResponse + ├── OpenSea WebSocket stream → OpenSeaListBot / OpenSeaSaleBot → activityTriager + ├── OpenSeaEventsPollBot (sales backfill, 30s) when PRODUCTION_MODE + ├── MintBot (queue + media-proxy poll → Discord) + ├── ScheduleBot (birthdays + optional trivia) + └── TwitterBot (optional, TWITTER_ENABLED) +``` + +### Message routing + +1. Channel must exist in the active `channels*.json` or the message is ignored. +2. If content starts with `#`: + - Special channels → dedicated `ArtIndexerBot` instances (`engine-chat`, Pace, Bright Moments, block-talk, etc.) + - Artist channels → `projectConfig.routeProjectNumberMsg()` (`default` / `stringTriggers` / `tokenIdTriggers`) +3. Else → `smartBotResponse()` (mentions, FAQ, gas, trivia helpers). + +Four indexers are constructed in `index.ts`: main (`getAllProjects`), Engine (`getEngineProjects`), Pace, Bright Moments. Only the main indexer initializes `projectConfig` project bots. + +### Data + +- **Hasura** (public): `https://data.artblocks.io/v1/graphql` — hardcoded in `queryGraphQL.ts` / `codegen.ts`. Optional `HASURA_GRAPHQL_ADMIN_SECRET`. +- **One GraphQL client** — there is **no** separate Arbitrum Hasura client (older docs were wrong). +- **Supabase**: trivia scores + Twitter OAuth/state only (`src/Data/supabase.ts`). +- Queries live in `src/Data/graphql/*.graphql`; wrappers in `src/Data/queryGraphQL.ts`. + +### Multi-chain reality + +Token/mint data can include chain IDs (Ethereum, Arbitrum, Base). **OpenSea stream handling currently drops non-`ethereum` NFT IDs**, and the REST poll bot hardcodes mainnet (`chainId: 1`). Do not assume Arbitrum/Base sales appear in Discord feeds without code changes. + +## Config map + +| File | Purpose | +|------|---------| +| `channels.json` / `channels_dev.json` | Discord channel ID → name + optional `projectBotHandlers` | +| `projectBots.json` / `projectBots_dev.json` | Named mapping file refs per bot ID | +| `coreContracts.json` | Art Blocks core contracts | +| `partnerContracts.json` | Engine partner contracts (lowercase addresses) | +| `collaborationContracts.json` / `explorationsContracts.json` | Collab / Explorations | +| `blockedEngineContracts.json` | Excluded from Engine project index | +| `blockedMintContracts.json` | Suppress mints per chain | +| `stagingContracts.json` | Staging → `ab-art-chat` | +| `mintBotConfig.json` | Collection/partner key → Discord **channel names** (must match `channels.json` `name`) | +| `project_aliases.json` | User shorthand → project name | +| `contract_aliases.json` | Platform aliases for `#recent` etc. | +| `NamedMappings/*Singles.json` / `*Sets.json` | Token aliases / sets | + +**Bot ID**: `{projectNumber}` or `{projectNumber}-{CONTRACT_NAME}` (contract name from partner/explorations/collab JSON). + +**Contract addresses must be lowercase.** + +## Environment variables + +### Required in production + +- `DISCORD_TOKEN` +- `PRODUCTION_MODE=true` +- `ARTBOT_IS_PROD=true` +- `OPENSEA_API_KEY` (stream + REST + username lookup) +- `MINT_WEBHOOK_SECRET` (Hasura → `POST /new-mint`, header name `webhook_secret`) +- `PORT` (set by Render) + +### Feature / optional + +- `HASURA_GRAPHQL_ADMIN_SECRET` +- `METADATA_REFRESH_INTERVAL_MINUTES` (default 480) +- `MINT_REFRESH_TIME_SECONDS` (default 60) +- `TWITTER_ENABLED` + `AB_TWITTER_*` / OAuth client vars / `TWITTER_CALLBACK_URL` +- `SUPABASE_URL`, `SUPABASE_API_KEY`, `TRIVIA_TABLE` +- `OPENAI_API_KEY`, `TRIVIA_CADENCE` (hours; `0` = off) +- `ETHERSCAN_API_KEY` (gas command) +- OpenSea reconnect tuning: `OPENSEA_MAX_RECONNECT_ATTEMPTS`, `OPENSEA_INITIAL_RECONNECT_DELAY`, `OPENSEA_MAX_RECONNECT_DELAY`, `OPENSEA_HEALTH_CHECK_INTERVAL` + +### Unused / stale (do not invent new uses without cleanup) + +`.env.example` historically listed `GOOGLE_AUTH_DETAILS`, `RANDOM_ART_INTERVAL_MINUTES`, `RESERVOIR_API_KEY` — **not referenced in code**. Reservoir bots were removed. CI sets `HASURA_GRAPHQL_ENDPOINT` but the app hardcodes the public URL. + +## Agent rules of thumb + +1. **Config PRs are the common path** — new artist channels, aliases, named mappings, partner contracts. Prefer JSON changes over code when possible. +2. **Sales/listing channel routing is code** — `src/Utils/activityTriager.ts` (artist/platform heuristics + ban list). New partners often need both `partnerContracts.json` / `mintBotConfig.json` **and** triager updates. +3. **After editing `.graphql` files**, run `yarn codegen` (pre-commit will too). +4. **Use `logger` (pino)**, not `console.log` / `console.error`, for new logging. +5. **Check `msg.channel.isSendable()`** before sending Discord messages. +6. **Do not assume null works in `tokenIdTriggers` ranges** — code compares `tokenID <= ranges[1]` without null handling; `_inRange()` exists but is unused. Use a large max instead of `null` until fixed. +7. **Mint channel names** in `mintBotConfig.json` must match the `name` field in `channels.json`, not channel IDs. +8. **Issue templates** under `.github/ISSUE_TEMPLATE/` map 1:1 to maintenance tasks — follow those file touch lists. +9. **No health endpoint** — hosting liveness is process + logs. `/update` is a stub. +10. **CODEOWNERS**: `@ArtBlocks/Eng-Approvers-Product`. Do not assign personal Discord handles from the old README. + +## Common tasks + +See [docs/MAINTENANCE.md](./docs/MAINTENANCE.md) for step-by-step: add channel, alias, named mappings, partner contract, replay mints, GraphQL changes. + +## Key files + +| Path | Role | +|------|------| +| `src/index.ts` | Entry, Express, Discord, OpenSea stream | +| `src/Classes/ArtIndexerBot.ts` | Project index + `#` routing | +| `src/Classes/ProjectBot.ts` | Per-project responses | +| `src/ProjectConfig/projectConfig.ts` | Channel config + bot ID resolution | +| `src/Utils/activityTriager.ts` | Sale/list Discord routing | +| `src/Utils/smartBotResponse.ts` | Non-`#` / mention responses | +| `src/Data/queryGraphQL.ts` | Hasura client + query helpers | +| `src/Classes/MintBot.ts` | Mint queue/posting | +| `scripts/replay-mints.ts` | Replay missed mint posts | + +## Known gaps / debt (for successors) + +- Near-zero automated tests; CI only smoke-boots the process +- OpenSea feeds are Ethereum-mainnet-oriented despite multi-chain mint data +- Empty `Procfile`; Render config lives outside the repo +- `googleapis` dependency appears unused +- Trivia error copy still mentions a former maintainer by name in `TriviaBot.ts` +- GitHub issue templates may still list a personal assignee — update when ownership transfers diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..1e132355 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +# ArtBot (Claude Code) + +Project memory for Claude Code lives in **[`AGENTS.md`](./AGENTS.md)** (shared with Cursor and other agents). Operational checklists: **[`docs/MAINTENANCE.md`](./docs/MAINTENANCE.md)**. + +When you learn a durable project fact, update `AGENTS.md` (or the relevant `.cursor/rules/*`) rather than duplicating long notes here. diff --git a/README.md b/README.md index 1bd00b7c..20002c06 100644 --- a/README.md +++ b/README.md @@ -3,64 +3,59 @@ ![Build status](https://github.com/ArtBlocks/artbot/actions/workflows/build-check.yml/badge.svg) [![GitPOAPs](https://public-api.gitpoap.io/v1/repo/ArtBlocks/artbot/badge)](https://www.gitpoap.io/gh/ArtBlocks/artbot) -The Discord bot for [ArtBlocks](http://artblocks.io/). +The Discord bot for [Art Blocks](http://artblocks.io/). -ArtBot is a Node.js application. It uses the [Yarn Package Manager](https://yarnpkg.com/) to manage dependencies and run the application. It can be interacted with via Discord messages. +ArtBot is a Node.js / TypeScript application. It uses [Yarn](https://yarnpkg.com/) (v4) to manage dependencies and runs via `ts-node`. It listens to Discord messages, OpenSea activity, and Hasura mint webhooks. + +**Maintainer / AI-agent handoff:** see [`AGENTS.md`](./AGENTS.md) and [`docs/MAINTENANCE.md`](./docs/MAINTENANCE.md). Prefer those over this README when they disagree. ## Running artbot -- Verify you have Node.js and npm installed. If not, you can refer to the [Node.js official page](https://nodejs.org/) to get started. +- Node.js **20.x** and Yarn 4 (via corepack): ```bash node -v -npm -v -``` - -- Install Yarn Package Manager. For detailed instructions, refer to the [Yarn official page](https://yarnpkg.com/getting-started/install). - -```bash -npm install -g yarn +corepack enable ``` -- Install the package dependencies +- Install dependencies (also runs GraphQL codegen): ```bash yarn install ``` -- Join the a-t Discord Server +- Join the a-t Discord Server (test instance): + +- Copy `.env.example` → `.env` and fill secrets (ask Art Blocks eng for shared test tokens). -Artbot is based on the [discord.js](https://discord.js.org/) package, and is exclusively concerned with processing and sending Discord messages. If you want to be able to interact with it, joining the Artbot test Discord server is the way to go. +**Important env flags** (they are independent): -- Set up `.env` file based on `.env.example`. +| Variable | Local testing | Production | +|----------|---------------|------------| +| `ARTBOT_IS_PROD` | `false` → `channels_dev.json` | `true` → `channels.json` | +| `PRODUCTION_MODE` | `true` to connect Discord / process OpenSea | `true` | -- Run the application +- Start: ```bash yarn start ``` -- You should now be able to interact with your local Artbot instance in the a-t Discord server! +You should be able to interact with your local Artbot in the a-t Discord server. ## Basic structure of artbot -The core engine of Artbot is built around the discord.js package. It serves several functions, all of which are based on listening to messages in the ArtBlocks Discord, and responding with other messages. This core functionality is driven from index.js, and there are several helper Classes and Utility packages that assist with this logic. - -- Project Queries - - One of the most widely used features is Artbot's ability to respond to a #[n] [project_name] query with a link to the appropriate token w/ embedded image. Currently this is implemented via the ProjectBot class. +The core engine uses [discord.js](https://discord.js.org/). Entry point: `src/index.ts` (Express HTTP server + Discord client + OpenSea stream). - - All projects and their metadata are retrieved from the subgraph on startup in the `ArtIndexerBot.ts` class, which in turn creates a `ProjectBot` for every project. `#[n] [project_name]`, `#?`, etc queries are triaged by the `ArtIndexerBot` class, and the corresponding `ProjectBot` is triggered to respond. - - Curated artist channels are handled a bit differently. ProjectBots for the artist's projects are defined in `ProjectConfig/channels.json` and are triggered by the artist's name in the query. e.g. `#1 ringer` in `#dmitri-cherniak` will trigger the Ringer project bot. - - Additional configuration for these projects can be defined in `ProjectConfig/projectBots.json`. See [Adding query support for a project](#adding-query-support-for-a-project) for more details. +- **Project queries** — `#[n] [project]`, `#?`, etc. `ArtIndexerBot` indexes projects from Hasura and routes to `ProjectBot` instances. Curated artist channels are configured in `ProjectConfig/channels.json` (string/token triggers). Optional named mappings live in `ProjectConfig/projectBots.json` + `NamedMappings/`. -- Sales/Listing Feeds +- **Sales / listings** — OpenSea Stream API (`OpenSeaListBot` / `OpenSeaSaleBot`) with REST poll backfill (`OpenSeaEventsPollBot`). Discord routing is in `Utils/activityTriager.ts`. - Artbot also provides feeds for sales and listings of Art Blocks projects. It uses the OpenSea Stream API to get the latest activity (using the `OpenSeaListBot.ts` and `OpenSeaSaleBot.ts` classes, respectively), and then posts them to the appropriate Discord channels (`Utils/activityTriager.ts`). +- **Mints** — Hasura calls `POST /new-mint` (header `webhook_secret`). `MintBot` waits for media-proxy images, then posts using `mintBotConfig.json`. -- SmartBot Responses +- **SmartBot responses** — Mentions / FAQ / gas / etc. in `Utils/smartBotResponse.ts`. - Artbot has been taught to respond to some specific queries about gas price, curated/playground/factory, etc. when directly queried. This logic lives in `Utils/smartBotResponse.js`. +- **Trivia / Twitter** — Optional; see `AGENTS.md` for env vars and feature flags. ## Adding query support for a project @@ -68,104 +63,87 @@ The core engine of Artbot is built around the discord.js package. It serves seve #### Bot ID -A bot ID consists of a project ID and contract name concatenated via a `-`. This is used in the config files to identify which bot should be used where or which bot you're configuring. For Art Blocks projects the contract name is optional and as such the `-` is not required. +A bot ID is a project ID, optionally concatenated with a contract name via `-` (e.g. `0` for Chromie Squiggles, `0-PLOTTABLES` for a Plottable project). Contract names are defined in `partnerContracts.json`, `explorationsContracts.json`, and `collaborationContracts.json`. -An example of a simple bot ID would be `0` for Chromie Squiggles or `0-DOODLE` for The Family Mooks. Contract names are defined in `partnerContracts.json`. +##### Partner contract names -##### Contract Names +Current entries in `partnerContracts.json` (addresses must be **lowercase**): -Here are the currently valid contract names. +| Partner / label | Contract Name | Contract Address | +| ----------------- | ----------------- | ------------------------------------------ | +| Plottables | PLOTTABLES | 0xa319c382a702682129fcbf55d514e61a16f97f9c | +| Plottables V3 | PLOTTABLESV3 | 0xac521ea7a83a3bc3f9f1e09f8300a6301743fb1f | +| Bright Moments | BM | 0x0a1bbd57033f57e7b6743621b79fcb9eb2ce3676 | +| Rozendaal | ROZENDAAL | 0x68c01cb4733a82a58d5e7bb31bddbff26a3a35d5 | +| Hodlers | HODLERS | 0x9f79e46a309f804aa4b7b53a1f72c69137427794 | +| Hodlers Pass | HODLERS-PASS | 0xd00495689d5161c511882364e0c342e12dcc5f08 | +| Classical Revival | CLASSICAL_REVIVAL | 0x000000098a14b4e08132fd55faec521ab597a001 | -| Partner | Contract Name | Contract Address | -| ----------- | ------------- | ------------------------------------------ | -| Doodle Labs | DOODLE | 0x28f2d3805652fb5d359486dffb7d08320d403240 | -| Plottables | PLOTTABLES | 0xa319C382a702682129fcbF55d514E61a16f97f9c | +### Required configuration -### Required Configuration - -- `ProjectConfig/channels.json`: +- `ProjectConfig/channels.json` (and `channels_dev.json` for the test server): - key: Discord channel ID - - value: object: - - key: `"name"` - - value: name of Discord channel - - key: `"projectBotHandlers"` - - value: object: - - key: `"default"` - - value: [Bot ID](#bot-id) - - (optional) key: `"stringTriggers"` - - value: object: - - key: [Bot ID](#bot-id) - - value: array of strings that trigger artbot to use the project bot - - (optional) key: `"tokenIdTriggers"`: - - value: object: - - key: [Bot ID](#bot-id) - - value: length-2 array defining range of token IDs that trigger artbot to use the project bot. e.g. [555, null] means all tokens >= 555 should use the project bot defined in key. [100, 200] means all tokens from 100 to 200 should use the project bot defined in key. - -### Optional Configuration + - value: + - `name`: Discord channel name slug + - `projectBotHandlers` (optional): + - `default`: [Bot ID](#bot-id) + - `stringTriggers` (optional): map of Bot ID → array of trigger substrings + - `tokenIdTriggers` (optional): array of `{ "BOT_ID": [min, max] }` inclusive ranges + +> **Note:** Open-ended ranges with `null` (e.g. `[555, null]`) are not reliably supported by the current range check. Prefer a large numeric max. + +### Optional configuration - `ProjectConfig/projectBots.json` - key: [Bot ID](#bot-id) - - value: object: - - (optional) key: `"namedMappings"` - - value: object: - - (optional) key: `"sets"` - - value: json filename defining single token labels; located in 'NamedMappings' directory. e.g. `ringerSingles.json` - - (optional) key: `"singles"` - value: json filename defining sets of token labels; located in 'NamedMappings' directory. e.g. `ringerSets.json` -- `ProjectConfig/partnerContracts.json` - - key: contract name - - value: contract address (lowercase) -- `NamedMappings/Singles.json` - - json file defining trigger names for single tokens. See `ringerSingles.json` for example. -- `NamedMappings/Seets.json` - - json file defining trigger names for single tokens. See `ringerSets.json` for example. + - `namedMappings.singles` → filename under `NamedMappings/` for single-token aliases (e.g. `ringerSingles.json`) + - `namedMappings.sets` → filename for token sets (e.g. `ringerSets.json`) +- `ProjectConfig/partnerContracts.json` — partner contract name → lowercase address +- `NamedMappings/Singles.json` / `Sets.json` — see existing Ringers files for examples -## Engine instructions +For end-to-end checklists (aliases, mint routing, replay scripts), use [`docs/MAINTENANCE.md`](./docs/MAINTENANCE.md). -These instructions explain how to configure Art Bot to serve project data in relevant channels. +## Engine instructions -1. Invite ArtBot to your server by clicking [here](https://discord.com/oauth2/authorize?client_id=794646394420854824&scope=bot&permissions=19520). Note that you must have the "Manage Server" permission on the desired server to invite Art Bot. -2. As a Engine partner you most likely have a contract of your own. To configure this you will have to follow the [optional configuration](#optional-configuration) scheme in `ProjectConfig/partnerContracts.json` by adding a new entry. +Configure Art Bot to serve project data in partner channels: -> :warning: Address must be all lowercase characters +1. Invite ArtBot: [OAuth link](https://discord.com/oauth2/authorize?client_id=794646394420854824&scope=bot&permissions=19520) (requires Manage Server). +2. Add your contract to `ProjectConfig/partnerContracts.json` (address **all lowercase**). -**Example Config** +**Example** ```json { - "DOODLE": "0x28f2d3805652fb5d359486dffb7d08320d403240", "PLOTTABLES": "0xa319c382a702682129fcbf55d514e61a16f97f9c", "": "" } ``` -3. Create a pull request following the configuration schema in [required configuration](#required-configuration) to set up Art Bot to listen to a relevant channel or channels. Note that Art Bot must have the proper permissions to view whatever channel it is listening to. +3. Open a PR adding the channel to `channels.json` with `projectBotHandlers`. Art Bot needs View Channel + Send Messages. -**Example Config** +**Example** ```json "880280317477404713": { - "name": "doodle-labs-the-lab", - "projectBotHandlers": { - "default": "0-DOODLE", - "stringTriggers": { - "1-DOODLE": [ - "slider" - ], - "2-DOODLE": [ - "neo", - "neogen" - ] - } + "name": "example-partner-channel", + "projectBotHandlers": { + "default": "0-PLOTTABLES", + "stringTriggers": { + "1-PLOTTABLES": ["slider"] } + } } ``` -4. Please update the [contract names](#contract-names) table in the README if you added a new contract to `partnerContract.json`. -5. Once the pull request goes in you should then be able to query Art Bot for configured projects in the relevant channel(s). +4. Update the [partner contract names](#partner-contract-names) table in this README if you added a new public partner. +5. After merge + deploy, query Art Bot in the configured channel(s). + +Mint feed routing for partners also requires `mintBotConfig.json` (and often `activityTriager.ts` for sales). See `docs/MAINTENANCE.md`. ## Contributing to artbot -For now, Artbot development is coordinated informally over Discord. Please reach out to grant#6616, purplehat.eth#7327, or ryley-o.eth#5272 if you think you might be interested in helping out. +Development is coordinated through Art Blocks eng / Discord and GitHub PRs. CODEOWNERS: `@ArtBlocks/Eng-Approvers-Product`. + +Useful commands: `yarn lint-and-format`, `yarn codegen`. CI smoke-boots the process with `PRODUCTION_MODE=false`. -Anyone who contributes to Artbot will be eligible to claim a [GitPOAP](https://www.gitpoap.io/gh/ArtBlocks/artbot) +Anyone who contributes to Artbot will be eligible to claim a [GitPOAP](https://www.gitpoap.io/gh/ArtBlocks/artbot). diff --git a/docs/MAINTENANCE.md b/docs/MAINTENANCE.md new file mode 100644 index 00000000..cba94aa6 --- /dev/null +++ b/docs/MAINTENANCE.md @@ -0,0 +1,122 @@ +# ArtBot Maintenance Playbook + +Step-by-step tasks for humans and AI agents. Channel IDs: Discord → Developer Mode → right-click channel → Copy ID. + +Always update **both** prod and dev configs when the change should be testable on the a-t server: `channels.json` + `channels_dev.json`, `projectBots.json` + `projectBots_dev.json`. + +## Add a curated artist channel + +Issue template: `.github/ISSUE_TEMPLATE/curated-project-bot-support.md` + +1. Confirm ArtBot can **View Channel** and **Send Messages** in the target channel. +2. Add to `src/ProjectConfig/channels.json` (and `channels_dev.json` if testing): + +```json +"CHANNEL_ID": { + "name": "artist-channel-slug", + "projectBotHandlers": { + "default": "PROJECT_ID" + } +} +``` + +3. Open a PR. After merge + deploy, test `#1` (or `#?`) in that channel. + +## Add playground / keyword routing + +Issue template: `.github/ISSUE_TEMPLATE/playground-project-bot-support.md` + +Add `stringTriggers` under the channel's `projectBotHandlers`: + +```json +"projectBotHandlers": { + "default": "0", + "stringTriggers": { + "123": ["keyword", "alias"] + } +} +``` + +## Add named singles or sets + +Issue templates: `new-project-singles.md`, `new-project-set.md` + +1. Create `src/NamedMappings/Singles.json` and/or `Sets.json`. + - Singles: `{ "goose": "879" }` (token invocation / id as used by the project) + - Sets: `{ "perfects": [109, 879, 1024] }` +2. Reference in `projectBots.json` under the bot ID: + +```json +"13": { + "namedMappings": { + "singles": "ringerSingles.json", + "sets": "ringerSets.json" + } +} +``` + +`singles` → Singles file; `sets` → Sets file. (Older README text had these swapped.) + +## Add a project alias + +Issue template: `.github/ISSUE_TEMPLATE/new-project-alias.md` + +Edit `src/ProjectConfig/project_aliases.json`: + +```json +"squig": "Chromie Squiggle" +``` + +## Add a partner / Engine contract + +1. Add **lowercase** address to `src/ProjectConfig/partnerContracts.json`. +2. Ensure it is not incorrectly listed in `blockedEngineContracts.json` if it should appear in Engine indexing. +3. For mint posts: add a key in `mintBotConfig.json` mapping to existing channel **names** from `channels.json` (e.g. `"studio-mints"`), or add a new mint channel entry in `channels.json` first. +4. For sales/listings routing into the right Discord channels: check/update `src/Utils/activityTriager.ts`. +5. Update the partner contracts table in `README.md` if you document public partners. +6. Bot IDs for that contract use `{projectNumber}-{CONTRACT_NAME}` (e.g. `0-PLOTTABLES`). + +## Add a smartBot / FAQ command + +Issue template: `.github/ISSUE_TEMPLATE/new-command.md` + +1. Implement in `src/Utils/smartBotResponse.ts`. +2. If adding a new `#` command surface, also update help text in `ArtIndexerBot` (`HASHTAG_MESSAGE` / related). + +## Change GraphQL queries + +1. Edit `src/Data/graphql/*.graphql`. +2. Run `yarn codegen`. +3. Update wrappers in `src/Data/queryGraphQL.ts` to use generated documents/types from `generated/graphql.ts`. + +## Replay missed mints + +```bash +# Inspect first +yarn ts-node scripts/replay-mints.ts --project-id --dry-run + +# Post (example channel name) +yarn ts-node scripts/replay-mints.ts --project-id --channel studio-mints +``` + +Requires Discord + Hasura access consistent with the environment you target. Prefer `--dry-run` first. + +## Local Discord testing checklist + +1. Copy `.env.example` → `.env`; fill secrets (ask Eng for shared artbot-jr / test tokens). +2. Set `ARTBOT_IS_PROD=false`, `PRODUCTION_MODE=true`. +3. `yarn install && yarn start`. +4. Use the a-t Discord server: https://discord.gg/W6eYPpEk3a +5. Confirm logs show `ARTBOT_IS_PROD: false` and successful Discord login. + +## Deploy / restart notes + +- Host: Render.com (process: `yarn start`). Config/secrets live in the host dashboard, not this repo. +- On restart: Hasura project index rebuilds; OpenSea stream reconnects; mint queue starts empty (in-flight mints may need replay). +- No dedicated `/health` route — use process health + application logs. +- Discord 429s on shared Render IPs: login uses retries/backoff in `src/index.ts`. + +## Ownership / review + +- CODEOWNERS: `@ArtBlocks/Eng-Approvers-Product` +- Coordinate informally in Art Blocks eng Discord for operational incidents (OpenSea outages, mint webhook failures, Discord permission issues).