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

image

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