Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions .cursor/rules/discord-bot/RULE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
description: "Discord.js patterns and bot class architecture for ArtBot"
alwaysApply: false
---

# Discord Bot Patterns

## Bot Class Architecture

Bot classes follow a consistent pattern:

```typescript
export class SomeBot {
// Private fields first
private bot: Client
private someState: Map<string, Value> = 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)
}

// Public handler methods
async handleSomeEvent(event: EventType) {
try {
// Handle the event
} catch (err) {
console.error('Error handling event:', err)
}
}

// Private helper methods
private helperMethod(): void {
// Implementation
}

// Cleanup method for resource management
cleanup() {
// Clear intervals, close connections, reset state
this.someState.clear()
}
}
```

## Message Handling

Check if channel is sendable before sending messages:

```typescript
async handleNumberMessage(msg: Message) {
if (!msg.channel.isSendable()) {
return
}

// Process message
msg.channel.send('Response')
}
```

## Embed Messages

Use `EmbedBuilder` for rich message formatting:

```typescript
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] })
```

## Event Listeners

Handle Discord events via the client exported from index.ts:

```typescript
import { discordClient } from '..'

discordClient.on(Events.MessageCreate, async (msg) => {
const content = msg.content
const channelID = msg.channel.id

if (content.startsWith('#')) {
// Handle # commands
}
})

discordClient.on('ready', () => {
console.log(`Logged in as ${discordClient.user?.tag}!`)
})
```

## Process Signal Handling

Register cleanup handlers for graceful shutdown:

```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)
})
```

## Message Routing

The `#` prefix triggers project/token queries:

- `#42 fidenza` - Get Fidenza #42
- `#? squiggle` - Random Chromie Squiggle
- `#entry curated` - Lowest priced Curated token
- `#set AB500` - Set collection data

Messages are routed through `ArtIndexerBot.handleNumberMessage()` which normalizes project names via `toProjectKey()` and dispatches to the appropriate `ProjectBot`.
158 changes: 158 additions & 0 deletions .cursor/rules/graphql-hasura/RULE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
---
description: "GraphQL and Hasura patterns for ArtBot data queries"
alwaysApply: false
---

# GraphQL & Hasura Patterns

## Query File Location

Define all GraphQL queries in `.graphql` files under `src/Data/graphql/`:

```graphql
# src/Data/graphql/artbot-hasura-queries.graphql

fragment ProjectDetail on projects_metadata {
id
project_id
name
artist_name
contract_address
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:

```typescript
import {
ProjectDetailFragment,
GetProjectDocument,
TokenDetailFragment,
} from '../../generated/graphql'
```

## Query Function Pattern

Create wrapper functions in `src/Data/queryGraphQL.ts`:

```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<ProjectDetailFragment> {
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]
}
```

## Pagination Pattern

For queries that may return many results, use pagination:

```typescript
const maxProjectsPerQuery = 1000

export async function getAllProjects(): Promise<ProjectDetailFragment[]> {
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
}
```

## Fragment Reuse

Use fragments for consistent field selection across queries:

```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 }
}

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
}
}
```
Loading