npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

nestjs-social-quest

v1.0.0

Published

NestJS library for social media quests (X, Discord, Telegram) via official API auth and twitterapi.io verification

Downloads

107

Readme

nestjs-social-quest

A production-ready NestJS library for building social media quest campaigns — verify follows, reposts, likes, Discord roles, and Telegram membership using official APIs. Designed for Web3 and marketing platforms that reward users for completing social tasks.

Why nestjs-social-quest?

| Feature | Details | |---------|---------| | 🐦 X (Twitter) | OAuth 2.0 PKCE login + follow/repost/quote via twitterapi.io + like via official Twitter v2 API | | 🎮 Discord | OAuth 2.0 login + guild membership and role verification via Discord Bot API | | ✈️ Telegram | Login Widget validation + channel/group membership via Telegram Bot API | | 🗄️ CRUD + DB | Optional TypeORM integration for managing quests and tracking user completions | | 🧪 Mock Mode | Skip real API calls per-quest with a runtime flag — great for testing and development | | ⚡ CLI Migrate | One command to create DB tables — no migration files to manage | | 🚨 Typed Exceptions | SocialQuestException, RateLimitException, AccountNotLinkedException for precise error handling |


Table of Contents


Installation

npm install nestjs-social-quest

# Required peer deps
npm install @nestjs/common @nestjs/core axios

# If using database features
npm install typeorm @nestjs/typeorm

# If using the migrate CLI
npm install pg

Quick Start

// app.module.ts
import { Module } from '@nestjs/common';
import { SocialQuestModule } from 'nestjs-social-quest';

@Module({
  imports: [
    SocialQuestModule.forRoot({
      twitter: {
        clientId: process.env.X_CLIENT_ID,
        clientSecret: process.env.X_CLIENT_SECRET,
        callbackUrl: 'http://localhost:3000/auth/twitter/callback',
      },
      twitterapiIo: {
        apiKey: process.env.TWITTERAPI_IO_KEY,
      },
      discord: {
        botToken: process.env.DISCORD_BOT_TOKEN,
        clientId: process.env.DISCORD_CLIENT_ID,
        clientSecret: process.env.DISCORD_CLIENT_SECRET,
        callbackUrl: 'http://localhost:3000/auth/discord/callback',
      },
      telegram: {
        botToken: process.env.TELEGRAM_BOT_TOKEN,
      },
      // Optional: enable DB-backed quest CRUD
      database: { enabled: true },
    }),
  ],
})
export class AppModule {}

Database Migration

If database: { enabled: true }, run the CLI once to create the required tables:

npx social-quest migrate

Env vars (all optional, with defaults):

| Variable | Default | Description | |---------------|----------------|------------------------| | DB_HOST | localhost | PostgreSQL host | | DB_PORT | 5432 | PostgreSQL port | | DB_USERNAME | postgres | Database user | | DB_PASSWORD | postgres | Database password | | DB_DATABASE | social_quest | Database name |

# With explicit credentials
DB_HOST=myhost DB_USERNAME=myuser DB_PASSWORD=secret DB_DATABASE=mydb npx social-quest migrate

Idempotent — safe to run multiple times. Uses CREATE TABLE IF NOT EXISTS.

Alternatively, if your client app already uses TypeORM with synchronize: true, the tables will be created automatically on startup (development only).


Module Configuration

Full configuration reference:

SocialQuestModule.forRoot({
  twitter: {
    clientId: 'YOUR_X_CLIENT_ID',
    clientSecret: 'YOUR_X_CLIENT_SECRET',
    callbackUrl: 'https://yourapp.com/auth/twitter/callback',
  },

  twitterapiIo: {
    apiKey: 'YOUR_TWITTERAPI_IO_KEY',
    // Optional: override twitterapi.io base URL
    baseUrl: 'https://api.twitterapi.io/twitter',
    // Optional: override individual verification endpoints
    endpoints: {
      verifyFollow: '/user/following',   // default
      verifyRepost: '/tweet/retweeters', // default
      verifyQuote: '/tweet/quotes',      // default
    },
  },

  discord: {
    botToken: 'YOUR_DISCORD_BOT_TOKEN',
    clientId: 'YOUR_DISCORD_CLIENT_ID',
    clientSecret: 'YOUR_DISCORD_CLIENT_SECRET',
    callbackUrl: 'https://yourapp.com/auth/discord/callback',
  },

  telegram: {
    botToken: 'YOUR_TELEGRAM_BOT_TOKEN',
  },

  // Optional: enable database-backed quest management
  database: { enabled: true },
})

Authentication Flows

X (Twitter) OAuth 2.0

The library uses OAuth 2.0 PKCE — no client secret is sent over the browser.

import { XAuthService } from 'nestjs-social-quest';

@Controller('auth/twitter')
export class TwitterAuthController {
  constructor(private readonly xAuth: XAuthService) {}

  @Get('url')
  getAuthUrl() {
    const { codeVerifier, codeChallenge } = this.xAuth.generatePKCE();
    const state = 'random-state-string'; // store in session
    // store codeVerifier in session for use in callback

    return {
      url: this.xAuth.generateAuthUrl(state, codeChallenge),
      // Optional: custom scopes
      // url: this.xAuth.generateAuthUrl(state, codeChallenge, ['tweet.read', 'users.read', 'like.read']),
    };
  }

  @Get('callback')
  async callback(@Query('code') code: string, @Query('state') state: string) {
    const codeVerifier = '...'; // retrieve from session

    // Returns: { accessToken, refreshToken, expiresIn, userId, username }
    const result = await this.xAuth.handleCallback(code, codeVerifier);

    // Store result.accessToken and result.userId for later quest verification
    return result;
  }
}

XAuthCallbackResult: | Field | Type | Description | |-------|------|-------------| | accessToken | string | Bearer token for Twitter API v2 | | refreshToken | string? | Refresh token (if offline.access scope requested) | | expiresIn | number? | Token lifetime in seconds | | userId | string | Twitter user ID | | username | string | Twitter handle (@username) |


Discord OAuth 2.0

import { DiscordAuthService } from 'nestjs-social-quest';

@Controller('auth/discord')
export class DiscordAuthController {
  constructor(private readonly discordAuth: DiscordAuthService) {}

  @Get('url')
  getAuthUrl() {
    const state = 'random-state-string'; // store in session
    return {
      url: this.discordAuth.generateAuthUrl(state),
      // Optional custom scopes — default: ['identify', 'guilds', 'guilds.join']
      // url: this.discordAuth.generateAuthUrl(state, ['identify', 'guilds']),
    };
  }

  @Get('callback')
  async callback(@Query('code') code: string) {
    // Returns: { accessToken, refreshToken, expiresIn, userId, username }
    const result = await this.discordAuth.handleCallback(code);

    // Store result.userId as the Discord user ID for quest verification
    return result;
  }
}

DiscordAuthCallbackResult: | Field | Type | Description | |-------|------|-------------| | accessToken | string | Discord user access token | | refreshToken | string | Refresh token | | expiresIn | number | Token lifetime in seconds | | userId | string | Discord user snowflake ID | | username | string | Discord username |


Telegram Login Widget

Telegram uses a widget-based login — no redirect flow. The frontend widget sends signed user data directly to your backend.

import { TelegramAuthService, TelegramAuthResult } from 'nestjs-social-quest';

@Controller('auth/telegram')
export class TelegramAuthController {
  constructor(private readonly telegramAuth: TelegramAuthService) {}

  @Post('callback')
  validateLogin(@Body() data: TelegramAuthResult) {
    // Validates HMAC-SHA256 signature from Telegram Login Widget
    const isValid = this.telegramAuth.validateAuthData(data);

    if (!isValid) {
      throw new UnauthorizedException('Invalid Telegram auth data');
    }

    // data.id is the Telegram user ID — store it for quest verification
    return { userId: data.id, username: data.username };
  }
}

TelegramAuthResult (sent by the widget): | Field | Type | Required | |-------|------|----------| | id | number | ✅ Telegram user ID | | first_name | string | ✅ | | last_name | string? | optional | | username | string? | optional | | photo_url | string? | optional | | auth_date | number | ✅ Unix timestamp | | hash | string | ✅ HMAC signature |


Quest Verification

X (Twitter) Quests

import { XQuestService } from 'nestjs-social-quest';

@Injectable()
export class CampaignService {
  constructor(private readonly xQuest: XQuestService) {}

  // Check if user follows @brand — via twitterapi.io
  async checkFollow(twitterUserId: string) {
    return this.xQuest.verifyFollow(twitterUserId, 'brand');
    // Returns: boolean
  }

  // Check if user reposted a tweet — via twitterapi.io
  async checkRepost(twitterUserId: string, tweetId: string) {
    return this.xQuest.verifyRepost(twitterUserId, tweetId);
  }

  // Check if user quoted a tweet — via twitterapi.io
  async checkQuote(twitterUserId: string, tweetId: string) {
    return this.xQuest.verifyQuote(twitterUserId, tweetId);
  }

  // Check if user liked a tweet — via Twitter API v2 (requires user's accessToken)
  async checkLike(twitterUserId: string, tweetId: string, accessToken: string) {
    return this.xQuest.verifyLike(twitterUserId, tweetId, accessToken);
    // Throws RateLimitException on HTTP 429
  }
}

Discord Quests

import { DiscordQuestService } from 'nestjs-social-quest';

@Injectable()
export class CampaignService {
  constructor(private readonly discordQuest: DiscordQuestService) {}

  // Check if user is a member of a guild
  async checkGuild(discordUserId: string, guildId: string) {
    return this.discordQuest.verifyGuildMember(discordUserId, guildId);
    // Returns: boolean (false if user not found, throws on API errors)
  }

  // Check if user has a specific role in a guild
  async checkRole(discordUserId: string, guildId: string, roleId: string) {
    return this.discordQuest.verifyRole(discordUserId, guildId, roleId);
  }
}

Telegram Quests

import { TelegramQuestService } from 'nestjs-social-quest';

@Injectable()
export class CampaignService {
  constructor(private readonly telegramQuest: TelegramQuestService) {}

  // Check if user is a member of a channel or group
  async checkMembership(telegramUserId: number, chatId: string) {
    return this.telegramQuest.verifyChatMember(telegramUserId, chatId);
    // chatId: '@mychannel' or numeric chat ID like '-1001234567890'
    // Returns: boolean
  }
}

CRUD Quest Management

Enable with database: { enabled: true }. Requires TypeORM + PostgreSQL.

import { QuestCrudService } from 'nestjs-social-quest/dist/services/quest/quest-crud.service';
import { Quest } from 'nestjs-social-quest/dist/entities/quest.entity';
import { UserQuest } from 'nestjs-social-quest/dist/entities/user-quest.entity';

Create a Quest

const quest = await questCrud.create({
  id: 'q-follow-brand',     // your custom ID
  name: 'Follow @brand on X',
  type: 'x_follow',
  metadata: { targetUsername: 'brand' },
  url: 'https://x.com/brand',
  point: 10,
});

Quest Types & Metadata Reference

| Type | Required Metadata | Verification API | Notes | |------|-------------------|-----------------|-------| | x_follow | { targetUsername: string } | twitterapi.io | | | x_repost | { tweetId: string } | twitterapi.io | | | x_quote | { tweetId: string } | twitterapi.io | | | x_like | { tweetId: string } | Twitter API v2 | Requires accessToken in SubmitQuestDto | | discord_guild | { guildId: string } | Discord Bot API | | | discord_role | { guildId: string, roleId: string } | Discord Bot API | | | telegram_member | { chatId: string } | Telegram Bot API | |

CRUD Operations

// List all quests (optional filter by type)
const quests: Quest[] = await questCrud.findAll();
const xQuests: Quest[] = await questCrud.findAll({ type: 'x_follow' });

// Get by ID
const quest: Quest = await questCrud.findById('q-follow-brand');

// Update
const updated: Quest = await questCrud.update('q-follow-brand', {
  name: 'Follow @newbrand',
  point: 20,
});

// Delete
await questCrud.delete('q-follow-brand');

Submit & Verify a Quest

// Real verification
const result: UserQuest = await questCrud.submitQuest({
  questId: 'q-follow-brand',
  userId: 'user-123',      // your internal user ID
  socialId: '987654321',   // Twitter user ID
});

// For x_like, also pass the user's Twitter accessToken
const result = await questCrud.submitQuest({
  questId: 'q-like-tweet',
  userId: 'user-123',
  socialId: '987654321',
  accessToken: 'user-bearer-token',
});

// Mock verification — skips API call, always marks as complete
const result = await questCrud.submitQuest({
  questId: 'q-follow-brand',
  userId: 'user-123',
  mockVerify: true,
});

submitQuest is idempotent: if the user already completed the quest, it returns the existing UserQuest record without re-verifying.

DB Schema

CREATE TABLE quest (
  id          varchar(44)  PRIMARY KEY,
  name        varchar(255) NOT NULL,
  type        varchar(255) NOT NULL,
  metadata    jsonb,
  url         varchar(255),
  point       int          NOT NULL DEFAULT 0,
  created_at  timestamp    NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at  timestamp
);

CREATE TABLE user_quests (
  user_id      varchar(44)  NOT NULL,
  quest_id     varchar(44)  NOT NULL,
  social_id    varchar(255),
  completed_at timestamp,
  PRIMARY KEY (user_id, quest_id),
  CONSTRAINT fk_user_quests_quest
    FOREIGN KEY (quest_id) REFERENCES quest(id) ON DELETE CASCADE
);

Error Handling

The library throws typed exceptions you can catch in NestJS exception filters or service code:

import {
  SocialQuestException,
  RateLimitException,
  AccountNotLinkedException,
} from 'nestjs-social-quest';

try {
  await xQuest.verifyLike(userId, tweetId, accessToken);
} catch (err) {
  if (err instanceof RateLimitException) {
    // Twitter API v2 returned HTTP 429
    // Suggest retry-after or queue the check
  } else if (err instanceof AccountNotLinkedException) {
    // User's social account is not connected
  } else if (err instanceof SocialQuestException) {
    // General quest error (quest not found, verification failed, etc.)
  }
}

| Exception | When thrown | |-----------|-------------| | SocialQuestException | Base exception — general errors (quest not found, auth failure, missing metadata, etc.) | | RateLimitException | API returned HTTP 429 (rate limited) | | AccountNotLinkedException | User's social account is not linked |


API Reference

XAuthService

| Method | Signature | Description | |--------|-----------|-------------| | generatePKCE() | () => { codeVerifier, codeChallenge } | Generate PKCE pair for OAuth flow | | generateAuthUrl() | (state, codeChallenge, scopes?) => string | Build Twitter OAuth 2.0 authorization URL | | handleCallback() | (code, codeVerifier) => Promise<XAuthCallbackResult> | Exchange code for access token + user info |

DiscordAuthService

| Method | Signature | Description | |--------|-----------|-------------| | generateAuthUrl() | (state, scopes?) => string | Build Discord OAuth 2.0 authorization URL | | handleCallback() | (code) => Promise<DiscordAuthCallbackResult> | Exchange code for access token + user info |

TelegramAuthService

| Method | Signature | Description | |--------|-----------|-------------| | validateAuthData() | (data: TelegramAuthResult) => boolean | Validate Telegram Login Widget HMAC signature |

XQuestService

| Method | Signature | Description | |--------|-----------|-------------| | verifyFollow() | (userId, targetUsername) => Promise<boolean> | Check follow via twitterapi.io | | verifyRepost() | (userId, tweetId) => Promise<boolean> | Check repost via twitterapi.io | | verifyQuote() | (userId, tweetId) => Promise<boolean> | Check quote via twitterapi.io | | verifyLike() | (userId, tweetId, accessToken) => Promise<boolean> | Check like via Twitter API v2 |

DiscordQuestService

| Method | Signature | Description | |--------|-----------|-------------| | verifyGuildMember() | (userId, guildId) => Promise<boolean> | Check guild membership | | verifyRole() | (userId, guildId, roleId) => Promise<boolean> | Check role membership |

TelegramQuestService

| Method | Signature | Description | |--------|-----------|-------------| | verifyChatMember() | (userId, chatId) => Promise<boolean> | Check channel/group membership |

QuestCrudService (requires database: { enabled: true })

| Method | Signature | Description | |--------|-----------|-------------| | create() | (dto: CreateQuestDto) => Promise<Quest> | Create a quest | | findAll() | (filter?: { type? }) => Promise<Quest[]> | List all quests, optionally filtered | | findById() | (id) => Promise<Quest> | Get quest by ID | | update() | (id, dto: UpdateQuestDto) => Promise<Quest> | Update quest fields | | delete() | (id) => Promise<void> | Delete quest | | submitQuest() | (dto: SubmitQuestDto) => Promise<UserQuest> | Verify and record quest completion |