Skip to content
Open
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
11 changes: 11 additions & 0 deletions integrations/telegram/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# SolFoundry API base URL
SOLFOUNDRY_API_URL=https://solfoundry.io

# Telegram Bot Token (from @BotFather)
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here

# Polling interval in minutes (default: 5)
POLL_INTERVAL_MINUTES=5

# Database path (default: ./data/subscribers.db)
DB_PATH=./data/subscribers.db
18 changes: 18 additions & 0 deletions integrations/telegram/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM node:20-slim

WORKDIR /app

RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*

COPY package.json ./
RUN npm install --production

COPY tsconfig.json ./
COPY src ./src
RUN npm run build

COPY .env.example .env.example

VOLUME ["/app/data"]

CMD ["node", "dist/index.js"]
75 changes: 75 additions & 0 deletions integrations/telegram/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# SolFoundry Telegram Bot

Telegram bot for SolFoundry bounty notifications. Get notified when new bounties are posted, browse open bounties, and track the leaderboard — all from Telegram.

## Setup

### 1. Configuration

```bash
cp .env.example .env
# Edit .env with your values
```

| Variable | Description | Default |
|---|---|---|
| `TELEGRAM_BOT_TOKEN` | Bot token from [@BotFather](https://t.me/BotFather) | *required* |
| `SOLFOUNDRY_API_URL` | SolFoundry API base URL | `https://solfoundry.io` |
| `POLL_INTERVAL_MINUTES` | Bounty polling interval | `5` |
| `DB_PATH` | SQLite database path | `./data/subscribers.db` |

### 2. Install & Run

```bash
npm install
npm run build
npm start
```

Or with Docker:

```bash
docker build -t solfoundry-telegram .
docker run -d --name solfoundry-bot \
-e TELEGRAM_BOT_TOKEN=your_token \
-v $(pwd)/data:/app/data \
solfoundry-telegram
```

## Commands

| Command | Description |
|---|---|
| `/start` | Welcome message & bot overview |
| `/bounties` | List top 10 open bounties |
| `/bounty <id>` | Get details for a specific bounty |
| `/subscribe` | Subscribe to new bounty notifications |
| `/unsubscribe` | Unsubscribe from notifications |
| `/filter <tier\|token\|skill>` | Set notification filters (e.g., `T1;USDC;rust`) |
| `/leaderboard` | Show top contributors |
| `/stats` | Platform statistics |

## Features

- **Auto-notifications**: Polls for new bounties every 5 minutes and notifies subscribed users
- **Inline keyboards**: Quick "View Details" and "Claim" buttons on bounty messages
- **Per-user filters**: Filter notifications by tier (T0-T3), token, or skill
- **SQLite storage**: Persistent subscriber data, no external DB needed
- **Docker ready**: Single-container deployment with volume mount for data

## Architecture

```
src/
├── index.ts # Entry point — bot init, signal handling
├── commands.ts # Telegram command handlers
├── api.ts # SolFoundry API client
├── notifier.ts # Cron-based new bounty polling & notification
├── storage.ts # SQLite subscriber storage
├── messages.ts # Message formatting helpers
└── types.ts # TypeScript type definitions
```

## Bounty

Built for SolFoundry Bounty #847 — Telegram Bot for New Bounty Notifications (500K $FNDRY).
25 changes: 25 additions & 0 deletions integrations/telegram/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@solfoundry/telegram-bot",
"version": "1.0.0",
"description": "Telegram Bot for SolFoundry bounty notifications",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts"
},
"dependencies": {
"node-telegram-bot-api": "^0.66.0",
"node-cron": "^3.0.3",
"dotenv": "^16.4.5",
"better-sqlite3": "^11.0.0"
},
"devDependencies": {
"typescript": "^5.4.0",
"@types/node": "^20.12.0",
"@types/node-telegram-bot-api": "^0.64.0",
"@types/node-cron": "^3.0.11",
"@types/better-sqlite3": "^7.6.8",
"ts-node": "^10.9.2"
}
}
26 changes: 26 additions & 0 deletions integrations/telegram/src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import axios from 'axios';
import { Bounty, ListBountiesParams, ListBountiesResponse, LeaderboardEntry, PlatformStats } from './types';

const API_BASE = process.env.SOLFOUNDRY_API_URL || 'https://solfoundry.io';

const client = axios.create({ baseURL: API_BASE, timeout: 15000 });

export async function listBounties(params?: ListBountiesParams): Promise<ListBountiesResponse> {
const { data } = await client.get('/api/bounties', { params });
return data;
}

export async function getBounty(id: string): Promise<Bounty> {
const { data } = await client.get(`/api/bounties/${id}`);
return data;
}

export async function getLeaderboard(): Promise<LeaderboardEntry[]> {
const { data } = await client.get('/api/leaderboard');
return data;
}

export async function getStats(): Promise<PlatformStats> {
const { data } = await client.get('/api/stats');
return data;
}
123 changes: 123 additions & 0 deletions integrations/telegram/src/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import TelegramBot from 'node-telegram-bot-api';
import * as api from './api';
import * as storage from './storage';
import { formatBountyCard, formatBountyDetail, formatLeaderboard, formatStats } from './messages';

export function registerCommands(bot: TelegramBot): void {
bot.onText(/\/start/, (msg) => {
const chatId = msg.chat.id;
const subbed = storage.isSubscribed(chatId);
const text = [
'👋 *Welcome to SolFoundry Bounty Bot!*',
'',
'Stay on top of the latest bounties on SolFoundry.',
'',
subbed ? '✅ You are subscribed to notifications.' : '🔔 Use /subscribe to get notified of new bounties.',
'',
'*Commands:*',
'/bounties — List latest open bounties',
'/bounty <id> — Get bounty details',
'/subscribe — Enable notifications',
'/unsubscribe — Disable notifications',
'/filter <tier|token|skill> — Set filters',
'/leaderboard — Top contributors',
'/stats — Platform statistics',
].join('\n');
bot.sendMessage(chatId, text, { parse_mode: 'Markdown' });
});

bot.onText(/\/bounties/, async (msg) => {
const chatId = msg.chat.id;
try {
const res = await api.listBounties({ limit: 10, status: 'open' });
if (!res.bounties.length) {
bot.sendMessage(chatId, 'No open bounties found.');
return;
}
const text = res.bounties.map((b, i) => `${i + 1}. ${formatBountyCard(b)}`).join('\n\n');
bot.sendMessage(chatId, `📋 *Latest Open Bounties*\n\n${text}`, {
parse_mode: 'Markdown',
disable_web_page_preview: true,
});
} catch {
bot.sendMessage(chatId, '❌ Failed to fetch bounties. Try again later.');
}
});

bot.onText(/\/bounty\s+(.+)/, async (msg, match) => {
const chatId = msg.chat.id;
const id = match![1].trim();
try {
const bounty = await api.getBounty(id);
bot.sendMessage(chatId, formatBountyDetail(bounty), {
parse_mode: 'Markdown',
disable_web_page_preview: true,
reply_markup: {
inline_keyboard: [
[
{ text: '🔗 View Details', url: bounty.url },
{ text: '✅ Claim', url: `${bounty.url}/claim` },
],
],
},
});
} catch {
bot.sendMessage(chatId, `❌ Bounty "${id}" not found.`);
}
});

bot.onText(/\/subscribe/, (msg) => {
const chatId = msg.chat.id;
if (storage.isSubscribed(chatId)) {
bot.sendMessage(chatId, '✅ You are already subscribed!');
return;
}
storage.addSubscriber(chatId, msg.from?.username);
bot.sendMessage(chatId, [
'🎉 *Subscribed!*',
'',
'You\'ll receive notifications for new bounties.',
'Use /filter to customize which bounties you see.',
].join('\n'), { parse_mode: 'Markdown' });
});

bot.onText(/\/unsubscribe/, (msg) => {
const chatId = msg.chat.id;
if (storage.removeSubscriber(chatId)) {
bot.sendMessage(chatId, '👋 Unsubscribed. You won\'t receive notifications anymore.');
} else {
bot.sendMessage(chatId, 'You weren\'t subscribed.');
}
});

bot.onText(/\/filter\s+(.+)/, (msg, match) => {
const chatId = msg.chat.id;
const filters = match![1].trim();
if (!storage.isSubscribed(chatId)) {
bot.sendMessage(chatId, '⚠️ Subscribe first with /subscribe');
return;
}
storage.setFilters(chatId, filters);
bot.sendMessage(chatId, `🔧 Filters updated: \`${filters}\`\n\nExamples: \`T1\`, \`USDC\`, \`rust\`, \`T1;USDC\``, { parse_mode: 'Markdown' });
});

bot.onText(/\/leaderboard/, async (msg) => {
const chatId = msg.chat.id;
try {
const entries = await api.getLeaderboard();
bot.sendMessage(chatId, formatLeaderboard(entries), { parse_mode: 'Markdown' });
} catch {
bot.sendMessage(chatId, '❌ Failed to fetch leaderboard.');
}
});

bot.onText(/\/stats/, async (msg) => {
const chatId = msg.chat.id;
try {
const stats = await api.getStats();
bot.sendMessage(chatId, formatStats(stats), { parse_mode: 'Markdown' });
} catch {
bot.sendMessage(chatId, '❌ Failed to fetch stats.');
}
});
}
34 changes: 34 additions & 0 deletions integrations/telegram/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import dotenv from 'dotenv';
import TelegramBot from 'node-telegram-bot-api';
import { initStorage, closeStorage } from './storage';
import { registerCommands } from './commands';
import { startNotifier } from './notifier';

dotenv.config();

const TOKEN = process.env.TELEGRAM_BOT_TOKEN;
if (!TOKEN) {
console.error('Missing TELEGRAM_BOT_TOKEN env var');
process.exit(1);
}

const bot = new TelegramBot(TOKEN, { polling: true });

initStorage();
registerCommands(bot);
startNotifier(bot);

console.log('🤖 SolFoundry Telegram Bot is running');

process.on('SIGINT', () => {
console.log('Shutting down...');
bot.stopPolling();
closeStorage();
process.exit(0);
});

process.on('SIGTERM', () => {
bot.stopPolling();
closeStorage();
process.exit(0);
});
62 changes: 62 additions & 0 deletions integrations/telegram/src/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Bounty, LeaderboardEntry, PlatformStats } from './types';

export function formatBountyCard(bounty: Bounty): string {
const tierEmoji: Record<string, string> = { T0: '🔴', T1: '🟠', T2: '🟡', T3: '🟢' };
const statusEmoji: Record<string, string> = { open: '🟢', in_progress: '🟡', completed: '✅', cancelled: '❌' };
return [
`${tierEmoji[bounty.tier] || '⚪'} *${bounty.title}*`,
`💰 Reward: ${bounty.reward} ${bounty.token}`,
`🏷 Tier: ${bounty.tier} | ${statusEmoji[bounty.status] || ''} ${bounty.status}`,
bounty.skills?.length ? `🔧 Skills: ${bounty.skills.join(', ')}` : '',
`📅 Deadline: ${bounty.deadline || 'No deadline'}`,
].filter(Boolean).join('\n');
}

export function formatBountyDetail(bounty: Bounty): string {
return [
`📋 *${bounty.title}*`,
'',
bounty.description,
'',
`💰 *Reward:* ${bounty.reward} ${bounty.token}`,
`🏷 *Tier:* ${bounty.tier}`,
`📊 *Status:* ${bounty.status}`,
bounty.skills?.length ? `🔧 *Skills:* ${bounty.skills.join(', ')}` : '',
bounty.assignee ? `👤 *Assignee:* ${bounty.assignee}` : '',
`📅 *Created:* ${bounty.created_at}`,
bounty.deadline ? `⏰ *Deadline:* ${bounty.deadline}` : '',
`🔗 [View on SolFoundry](${bounty.url})`,
].filter(Boolean).join('\n');
}

export function formatLeaderboard(entries: LeaderboardEntry[]): string {
if (!entries.length) return '🏆 No leaderboard data available.';
const medals = ['🥇', '🥈', '🥉'];
const rows = entries.slice(0, 10).map((e, i) => {
const medal = medals[i] || `${e.rank}.`;
return `${medal} @${e.username} — ${e.bounties_completed} bounties — ${e.total_earned}`;
});
return `🏆 *Leaderboard*\n\n${rows.join('\n')}`;
}

export function formatStats(stats: PlatformStats): string {
return [
'📊 *SolFoundry Platform Stats*',
'',
`📦 Total Bounties: ${stats.total_bounties}`,
`🟢 Open Bounties: ${stats.open_bounties}`,
`✅ Completed: ${stats.bounties_completed}`,
`💰 Total Reward Pool: ${stats.total_reward_pool}`,
`👥 Contributors: ${stats.total_contributors}`,
].join('\n');
}

export function formatNotification(bounty: Bounty): string {
return [
'🆕 *New Bounty Alert!*',
'',
formatBountyCard(bounty),
'',
`🔗 [View & Claim](${bounty.url})`,
].join('\n');
}
Loading
Loading