Skip to content

Instantly share code, notes, and snippets.

@WomB0ComB0
Created September 4, 2025 17:03
Show Gist options
  • Select an option

  • Save WomB0ComB0/44cf8e7b48565e4c7c70ab200015e7c1 to your computer and use it in GitHub Desktop.

Select an option

Save WomB0ComB0/44cf8e7b48565e4c7c70ab200015e7c1 to your computer and use it in GitHub Desktop.
twitch-alert - Enhanced with AI-generated documentation
import axios from 'axios';
import { TwitterApi } from 'twitter-api-v2';
import { BskyAgent } from '@atproto/api';
import * as fs from 'node:fs';
import * as path from 'node:path';
interface Config {
twitch: {
clientId: string;
clientSecret: string;
channelName: string;
};
twitter: {
appKey: string;
appSecret: string;
accessToken: string;
accessSecret: string;
};
bluesky: {
identifier: string; // your handle or email
password: string; // app password
};
checkInterval: number; // in milliseconds (default: 60000 = 1 minute)
messageTemplate: string;
}
// Stream status interface
interface StreamStatus {
isLive: boolean;
title?: string;
game?: string;
viewerCount?: number;
thumbnailUrl?: string;
}
// State management
interface BotState {
lastKnownStatus: boolean;
lastNotificationTime: number;
}
class TwitchStreamBot {
private config: Config;
private twitchAccessToken: string = '';
private twitterClient: TwitterApi;
private blueskyAgent: BskyAgent;
private state: BotState;
private stateFile: string;
constructor(configPath: string) {
this.config = this.loadConfig(configPath);
this.stateFile = path.join(__dirname, 'bot-state.json');
this.state = this.loadState();
// Initialize Twitter client
this.twitterClient = new TwitterApi({
appKey: this.config.twitter.appKey,
appSecret: this.config.twitter.appSecret,
accessToken: this.config.twitter.accessToken,
accessSecret: this.config.twitter.accessSecret,
});
// Initialize Bluesky client
this.blueskyAgent = new BskyAgent({
service: 'https://bsky.social',
});
}
private loadConfig(configPath: string): Config {
try {
const configData = fs.readFileSync(configPath, 'utf-8');
return JSON.parse(configData);
} catch (error) {
console.error('Error loading config:', error);
throw new Error('Failed to load configuration file');
}
}
private loadState(): BotState {
try {
if (fs.existsSync(this.stateFile)) {
const stateData = fs.readFileSync(this.stateFile, 'utf-8');
return JSON.parse(stateData);
}
} catch (error) {
console.log('No existing state file found, creating new state');
}
return {
lastKnownStatus: false,
lastNotificationTime: 0,
};
}
private saveState(): void {
try {
fs.writeFileSync(this.stateFile, JSON.stringify(this.state, null, 2));
} catch (error) {
console.error('Error saving state:', error);
}
}
private async getTwitchAccessToken(): Promise<void> {
try {
const response = await axios.post('https://id.twitch.tv/oauth2/token', null, {
params: {
client_id: this.config.twitch.clientId,
client_secret: this.config.twitch.clientSecret,
grant_type: 'client_credentials',
},
});
this.twitchAccessToken = response.data.access_token;
console.log('✅ Twitch access token obtained');
} catch (error) {
console.error('❌ Error getting Twitch access token:', error);
throw error;
}
}
private async getStreamStatus(): Promise<StreamStatus> {
try {
if (!this.twitchAccessToken) {
await this.getTwitchAccessToken();
}
const response = await axios.get('https://api.twitch.tv/helix/streams', {
headers: {
'Client-ID': this.config.twitch.clientId,
'Authorization': `Bearer ${this.twitchAccessToken}`,
},
params: {
user_login: this.config.twitch.channelName,
},
});
const streams = response.data.data;
if (streams.length === 0) {
return { isLive: false };
}
const stream = streams[0];
return {
isLive: true,
title: stream.title,
game: stream.game_name,
viewerCount: stream.viewer_count,
thumbnailUrl: stream.thumbnail_url.replace('{width}', '1280').replace('{height}', '720'),
};
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
console.log('🔄 Access token expired, refreshing...');
await this.getTwitchAccessToken();
return this.getStreamStatus();
}
console.error('❌ Error checking stream status:', error);
return { isLive: false };
}
}
private formatMessage(streamStatus: StreamStatus): string {
let message = this.config.messageTemplate;
message = message.replace('{channelName}', this.config.twitch.channelName);
message = message.replace('{title}', streamStatus.title || 'Live Stream');
message = message.replace('{game}', streamStatus.game || 'Just Chatting');
message = message.replace('{viewerCount}', streamStatus.viewerCount?.toString() || '0');
message = message.replace('{url}', `https://twitch.tv/${this.config.twitch.channelName}`);
return message;
}
private async postToTwitter(message: string): Promise<void> {
try {
await this.twitterClient.v2.tweet(message);
console.log('✅ Posted to Twitter successfully');
} catch (error) {
console.error('❌ Error posting to Twitter:', error);
}
}
private async postToBluesky(message: string): Promise<void> {
try {
if (!this.blueskyAgent.session) {
await this.blueskyAgent.login({
identifier: this.config.bluesky.identifier,
password: this.config.bluesky.password,
});
}
await this.blueskyAgent.post({
text: message,
createdAt: new Date().toISOString(),
});
console.log('✅ Posted to Bluesky successfully');
} catch (error) {
console.error('❌ Error posting to Bluesky:', error);
}
}
private async handleStreamStart(streamStatus: StreamStatus): Promise<void> {
console.log('🔴 Stream went live! Posting notifications...');
const message = this.formatMessage(streamStatus);
// Post to both platforms simultaneously
await Promise.allSettled([
this.postToTwitter(message),
this.postToBluesky(message),
]);
this.state.lastKnownStatus = true;
this.state.lastNotificationTime = Date.now();
this.saveState();
}
private async handleStreamEnd(): Promise<void> {
console.log('⚫ Stream ended');
this.state.lastKnownStatus = false;
this.saveState();
}
private async checkAndNotify(): Promise<void> {
try {
const streamStatus = await this.getStreamStatus();
// Stream just started
if (streamStatus.isLive && !this.state.lastKnownStatus) {
await this.handleStreamStart(streamStatus);
}
// Stream just ended
else if (!streamStatus.isLive && this.state.lastKnownStatus) {
await this.handleStreamEnd();
}
// Stream is live (ongoing)
else if (streamStatus.isLive) {
console.log(`📺 Stream is live: "${streamStatus.title}" - ${streamStatus.viewerCount} viewers`);
}
// Stream is offline
else {
console.log('💤 Stream is offline');
}
} catch (error) {
console.error('❌ Error in check cycle:', error);
}
}
public async start(): Promise<void> {
console.log('🚀 Starting Twitch Stream Notification Bot...');
console.log(`📺 Monitoring channel: ${this.config.twitch.channelName}`);
console.log(`⏱️ Check interval: ${this.config.checkInterval / 1000} seconds`);
// Initial authentication
try {
await this.getTwitchAccessToken();
await this.blueskyAgent.login({
identifier: this.config.bluesky.identifier,
password: this.config.bluesky.password,
});
console.log('✅ Authenticated with all services');
} catch (error) {
console.error('❌ Failed to authenticate with services:', error);
return;
}
// Initial check
await this.checkAndNotify();
// Set up periodic checks
setInterval(() => {
this.checkAndNotify();
}, this.config.checkInterval);
console.log('✅ Bot is running! Press Ctrl+C to stop.');
}
public stop(): void {
console.log('🛑 Stopping bot...');
this.saveState();
process.exit(0);
}
}
// Handle graceful shutdown
process.on('SIGINT', () => {
console.log('\n🛑 Received SIGINT, shutting down gracefully...');
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\n🛑 Received SIGTERM, shutting down gracefully...');
process.exit(0);
});
// Main execution
async function main() {
const configPath = process.argv[2] || './config.json';
try {
const bot = new TwitchStreamBot(configPath);
await bot.start();
} catch (error) {
console.error('❌ Failed to start bot:', error);
process.exit(1);
}
}
// Run the bot
if (require.main === module) {
main();
}
export { TwitchStreamBot };

twitch-alert.ts

File Type: TS
Lines: 316
Size: 8.7 KB
Generated: 9/4/2025, 1:03:43 PM


Code Analysis: twitch-alert.ts

This TypeScript file implements a bot that monitors a Twitch channel and posts notifications to Twitter and Bluesky when the channel goes live. It uses the axios library for HTTP requests, twitter-api-v2 for Twitter integration, and @atproto/api for Bluesky integration. The bot reads configuration from a JSON file and maintains state in a local file to avoid redundant notifications.

Key Components:

  • Configuration (Config interface): Defines the structure for the configuration file, including Twitch API credentials, Twitter API keys, Bluesky credentials, check interval, and a message template.
  • Stream Status (StreamStatus interface): Represents the status of the Twitch stream, including whether it's live, the title, game, viewer count, and thumbnail URL.
  • Bot State (BotState interface): Stores the bot's state, including the last known stream status and the last notification time. This is persisted to a file (bot-state.json) to maintain state across restarts.
  • TwitchStreamBot Class:
    • Constructor: Initializes the bot with the configuration, Twitter and Bluesky clients, and loads the bot state.
    • loadConfig(configPath: string): Config: Reads and parses the configuration file from the specified path. Handles potential errors during file reading or JSON parsing.
    • loadState(): BotState: Loads the bot state from the bot-state.json file. If the file doesn't exist, it initializes a new state.
    • saveState(): void: Saves the bot state to the bot-state.json file.
    • getTwitchAccessToken(): Promise<void>: Obtains a Twitch access token using the client ID and client secret.
    • getStreamStatus(): Promise<StreamStatus>: Retrieves the stream status from the Twitch API. It handles token expiration and retries the request if necessary.
    • formatMessage(streamStatus: StreamStatus): string: Formats the notification message using the configured template and the stream status.
    • postToTwitter(message: string): Promise<void>: Posts the message to Twitter using the twitter-api-v2 library.
    • postToBluesky(message: string): Promise<void>: Posts the message to Bluesky using the @atproto/api library. It handles authentication if necessary.
    • handleStreamStart(streamStatus: StreamStatus): Promise<void>: Handles the event when the stream goes live. It posts notifications to Twitter and Bluesky and updates the bot state.
    • handleStreamEnd(): Promise<void>: Handles the event when the stream ends. It updates the bot state.
    • checkAndNotify(): Promise<void>: Checks the stream status and posts notifications if the status has changed.
    • start(): Promise<void>: Starts the bot by authenticating with the services, performing an initial check, and setting up periodic checks using setInterval.

Workflow:

  1. Initialization: The bot loads the configuration and state from files.
  2. Authentication: The bot authenticates with the Twitch, Twitter, and Bluesky APIs.
  3. Periodic Checks: The bot periodically checks the stream status using the Twitch API.
  4. Notification: If the stream status changes (goes live or ends), the bot posts a notification to Twitter and Bluesky.
  5. State Management: The bot updates its state to reflect the current stream status and saves the state to a file.

Dependencies:

  • axios: For making HTTP requests to the Twitch API.
  • twitter-api-v2: For interacting with the Twitter API.
  • @atproto/api: For interacting with the Bluesky API.
  • node:fs: For reading and writing files (configuration and state).
  • node:path: For constructing file paths.

Strengths:

  • Clear separation of concerns: The code is well-organized into functions and classes, making it easy to understand and maintain.
  • Error handling: The code includes error handling for various operations, such as loading the configuration file, obtaining the Twitch access token, and posting to Twitter and Bluesky.
  • State management: The bot maintains state to avoid redundant notifications.
  • Asynchronous operations: The code uses asynchronous operations to avoid blocking the main thread.
  • Configuration-driven: The bot's behavior is configured through a JSON file, making it easy to customize.

Potential Improvements:

  • More robust error handling: Consider adding more specific error handling and logging for different types of errors. For example, distinguish between network errors and API errors.
  • Rate limiting: Implement rate limiting to avoid exceeding the API limits of Twitter and Bluesky.
  • Configuration validation: Add validation to the configuration file to ensure that it contains all the required fields and that the values are valid.
  • Logging: Implement a more comprehensive logging system to track the bot's activity and errors. Consider using a dedicated logging library.
  • Dependency Injection: Consider using dependency injection to make the code more testable.
  • Environment Variables: Instead of relying solely on a config file, consider using environment variables for sensitive information like API keys and secrets. This is generally a more secure practice.
  • Health Checks: Implement a health check endpoint that can be used to monitor the bot's status.
  • Consider using a dedicated task scheduler: Instead of setInterval, a task scheduler library could provide more control and flexibility over the timing of checks.

Usage:

  1. Install the dependencies: npm install axios twitter-api-v2 @atproto/api
  2. Create a configuration file (e.g., config.json) with the required credentials.
  3. Run the bot: ts-node twitch-alert.ts

This analysis provides a comprehensive overview of the twitch-alert.ts file, including its purpose, key components, workflow, strengths, potential improvements, and usage. It should be helpful for developers who want to understand, modify, or extend the bot's functionality.


Description generated using AI analysis

@WomB0ComB0
Copy link
Author

Twitch Stream Notification Bot

A TypeScript bot that automatically monitors your Twitch stream and posts notifications to both X (Twitter) and Bluesky when you go live.

Features

  • Real-time monitoring of your Twitch channel status
  • Automatic posting to both X and Bluesky when you go live
  • Customizable message templates with dynamic placeholders
  • State persistence to avoid duplicate notifications
  • Error handling and automatic token refresh
  • Graceful shutdown handling

Prerequisites

  • Node.js (v16 or higher) or Bun runtime
  • A Twitch account and channel
  • X (Twitter) Developer account
  • Bluesky account

Installation

  1. Clone or download the script files

  2. Install dependencies:

    npm init -y
    npm install axios twitter-api-v2 @atproto/api
    npm install -D @types/node typescript ts-node

    Or with Bun:

    bun install axios twitter-api-v2 @atproto/api

API Setup

1. Twitch API Setup

  1. Go to [Twitch Developer Console](https://dev.twitch.tv/console)
  2. Click "Create New Application"
  3. Fill in the details:
    • Name: Your bot name (e.g., "Stream Notification Bot")
    • OAuth Redirect URLs: http://localhost:3000 (this won't actually be used)
    • Category: "Application Integration" or "Other"
  4. Save and note down your Client ID and Client Secret

2. X (Twitter) API Setup

  1. Go to [Twitter Developer Portal](https://developer.twitter.com/en/portal/dashboard)
  2. Create a new project/app
  3. IMPORTANT: Set up User Authentication:
    • Click "Set Up" in the "User authentication settings" section
    • App permissions: Select "Read and write"
    • Type of App: Choose "Web App, Automated App or Bot"
    • Callback URI: http://localhost:3000
    • Website URL: Your Twitch channel URL or any website
  4. Go to "Keys and tokens" tab
  5. Generate Access Token and Secret (do this AFTER setting up user authentication)
  6. Note down:
    • API Key (App Key)
    • API Secret (App Secret)
    • Access Token
    • Access Token Secret

3. Bluesky API Setup

  1. Log into your Bluesky account
  2. Go to Settings → App Passwords
  3. Generate a new app password
  4. Note down your handle (e.g., yourname.bsky.social) and app password

Configuration

Create a config.json file in your project directory:

{
  "twitch": {
    "clientId": "your_twitch_client_id",
    "clientSecret": "your_twitch_client_secret",
    "channelName": "your_twitch_username"
  },
  "twitter": {
    "appKey": "your_twitter_api_key",
    "appSecret": "your_twitter_api_secret",
    "accessToken": "your_twitter_access_token",
    "accessSecret": "your_twitter_access_secret"
  },
  "bluesky": {
    "identifier": "your.handle.bsky.social",
    "password": "your_app_password"
  },
  "checkInterval": 60000,
  "messageTemplate": "🔴 LIVE NOW! \n\n📺 {title}\n🎮 Playing: {game}\n👥 {viewerCount} viewers\n\n🔗 {url}"
}

Message Template Variables

You can customize the messageTemplate with these placeholders:

  • {channelName} - Your Twitch username
  • {title} - Stream title
  • {game} - Game/category being played
  • {viewerCount} - Current viewer count
  • {url} - Direct link to your Twitch channel

Configuration Options

  • checkInterval: How often to check stream status in milliseconds (60000 = 1 minute)
  • messageTemplate: The message format posted to social media

Usage

  1. Make sure your config.json is properly filled out

  2. Run the bot:

    npx ts-node twitch-alert.ts

    Or with Bun:

    bun twitch-alert.ts
  3. The bot will start monitoring and show status messages:

    🚀 Starting Twitch Stream Notification Bot...
    📺 Monitoring channel: your_username
    ⏱️  Check interval: 60 seconds
    ✅ Twitch access token obtained
    ✅ Authenticated with all services
    💤 Stream is offline
    ✅ Bot is running! Press Ctrl+C to stop.
    
  4. When you go live, you'll see:

    🔴 Stream went live! Posting notifications...
    ✅ Posted to Twitter successfully
    ✅ Posted to Bluesky successfully
    
  5. To stop the bot, press Ctrl+C

How It Works

  1. Monitoring: The bot checks your Twitch channel status every minute (configurable)
  2. State Tracking: It remembers whether you were live or offline to only post when status changes
  3. Smart Posting: Only posts when you go live, not when you're already streaming
  4. Dual Platform: Posts to both X and Bluesky simultaneously
  5. Persistence: Saves state to avoid duplicate notifications if the bot restarts

Troubleshooting

Common Issues

403 Error on Twitter/X:

  • Make sure you've set up User Authentication in your Twitter app
  • Verify app permissions are set to "Read and write"
  • Regenerate Access Token and Secret after changing permissions

Twitch Authentication Issues:

  • Double-check your Client ID and Client Secret
  • Ensure your channel name is correct (case-sensitive)

Bluesky Authentication Issues:

  • Make sure you're using an App Password, not your regular password
  • Verify your handle format (should include .bsky.social)

Bot not detecting stream changes:

  • Check your Twitch channel name in config
  • Verify the bot has network access
  • Look for error messages in the console

File Structure

your-project/
├── twitch-alert.ts     # Main bot script
├── config.json         # Your configuration
├── bot-state.json      # Auto-generated state file
└── package.json        # Dependencies

Security Notes

  • Keep your config.json file private and never commit it to version control
  • Use environment variables for production deployments
  • Regularly rotate your API keys and passwords

License

This project is open source and available under the MIT License.

Support

If you encounter issues:

  1. Check the troubleshooting section above
  2. Verify all API credentials are correct
  3. Ensure all services have proper permissions set up
  4. Check the console output for specific error messages

@WomB0ComB0
Copy link
Author

Screenshot From 2025-09-04 12-44-09

@WomB0ComB0
Copy link
Author

image

@WomB0ComB0
Copy link
Author

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment