🤖 Código del Bot de Telegram (Node.js)
const TelegramBot = require('node-telegram-bot-api');
const axios = require('axios');
const cron = require('node-cron');
// Configuración
const config = {
telegramToken: 'TU_TOKEN_AQUI',
lastFmApiKey: 'TU_API_KEY_LASTFM',
songkickApiKey: 'TU_API_KEY_SONGKICK',
googleMapsApiKey: 'TU_API_KEY_GOOGLE_MAPS',
adminUserId: 'TU_USER_ID'
};
// Inicializar bot
const bot = new TelegramBot(config.telegramToken, { polling: true });
// Base de datos simple (en producción usa MongoDB o similar)
const users = new Map();
const concerts = new Map();
// Comando /start
bot.onText(/\/start/, (msg) => {
const chatId = msg.chat.id;
const welcomeMessage = `
🤖 *Bienvenido a ConciertoBot!*
Te avisaré cuando tus artistas favoritos tengan conciertos cerca de ti.
*Comandos disponibles:*
• /register - Registrarse
• /addartist [nombre] - Añadir artista
• /removeartist [nombre] - Eliminar artista
• /listartists - Ver tus artistas
• /setlocation [ciudad] - Establecer ubicación
• /settings - Ver configuración
• /help - Ayuda
`;
bot.sendMessage(chatId, welcomeMessage, { parse_mode: 'Markdown' });
});
// Comando /register
bot.onText(/\/register/, (msg) => {
const chatId = msg.chat.id;
users.set(chatId, {
id: chatId,
username: msg.from.username,
artists: [],
location: null,
radius: 100, // km
advanceNotice: 14, // días
genres: ['metal', 'rock', 'punk'],
active: true,
createdAt: new Date()
});
bot.sendMessage(chatId, '✅ Registro completado. Usa /addartist para añadir tus artistas favoritos.');
});
// Comando /addartist
bot.onText(/\/addartist (.+)/, async (msg, match) => {
const chatId = msg.chat.id;
const artistName = match[1].trim();
const user = users.get(chatId);
if (!user) {
return bot.sendMessage(chatId, '❌ Primero debes registrarte con /register');
}
try {
// Buscar artista en Last.fm
const artistInfo = await searchArtist(artistName);
if (artistInfo) {
user.artists.push({
name: artistInfo.name,
mbid: artistInfo.mbid,
image: artistInfo.image
});
bot.sendMessage(chatId, `✅ Artista añadido: *${artistInfo.name}*`, { parse_mode: 'Markdown' });
// Buscar conciertos inmediatamente
await checkArtistConcerts(artistInfo, user);
} else {
bot.sendMessage(chatId, '❌ No se encontró el artista. Verifica el nombre.');
}
} catch (error) {
console.error('Error adding artist:', error);
bot.sendMessage(chatId, '❌ Error al añadir el artista.');
}
});
// Función para buscar artistas
async function searchArtist(artistName) {
try {
const response = await axios.get('https://ws.audioscrobbler.com/2.0/', {
params: {
method: 'artist.search',
artist: artistName,
api_key: config.lastFmApiKey,
format: 'json',
limit: 1
}
});
const artists = response.data.results?.artistmatches?.artist;
if (artists && artists.length > 0) {
return {
name: artists[0].name,
mbid: artists[0].mbid,
image: artists[0].image?.[3]?.['#text']
};
}
return null;
} catch (error) {
console.error('Error searching artist:', error);
return null;
}
}
// Función para verificar conciertos
async function checkArtistConcerts(artist, user) {
try {
// Buscar en Songkick
const songkickResponse = await axios.get(`https://api.songkick.com/api/3.0/events.json`, {
params: {
apikey: config.songkickApiKey,
artist_name: artist.name,
location: 'clientip' // Usar ubicación del usuario
}
});
const events = songkickResponse.data.resultsPage?.results?.event || [];
for (const event of events) {
const concertKey = `${artist.mbid}-${event.id}`;
if (!concerts.has(concertKey)) {
concerts.set(concertKey, {
id: event.id,
artist: artist.name,
venue: event.venue.displayName,
city: event.location.city,
date: event.start.date,
time: event.start.time,
url: event.uri,
lat: event.venue.lat,
lng: event.venue.lng
});
// Calcular distancia
const distance = await calculateDistance(
user.location,
{ lat: event.venue.lat, lng: event.venue.lng }
);
// Si está dentro del radio, enviar notificación
if (distance <= user.radius) {
await sendConcertAlert(user, artist, event, distance);
}
}
}
} catch (error) {
console.error('Error checking concerts:', error);
}
}
// Función para enviar alerta de concierto
async function sendConcertAlert(user, artist, event, distance) {
const message = `
🎸 *¡Nuevo Concierto de ${artist.name}!*
📅 *Fecha:* ${new Date(event.start.date).toLocaleDateString('es-ES')}
🕐 *Hora:* ${event.start.time || 'Por confirmar'}
📍 *Lugar:* ${event.venue.displayName}, ${event.location.city}
🎫 *Entradas:* [Comprar aquí](${event.uri})
📏 *Distancia:* ${distance.toFixed(1)} km
¿Te interesa este concierto?
`;
const options = {
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [[
{ text: '✅ Sí, avísame', callback_data: `interested_${event.id}` },
{ text: '❌ No, gracias', callback_data: `not_interested_${event.id}` }
]]
}
};
await bot.sendMessage(user.id, message, options);
}
// Función para calcular distancia
async function calculateDistance(location1, location2) {
// Si no hay ubicación del usuario, usar Madrid como default
if (!location1 || !location1.lat || !location1.lng) {
location1 = { lat: 40.4168, lng: -3.7038 }; // Madrid
}
// Fórmula de Haversine
const R = 6371; // Radio de la Tierra en km
const dLat = (location2.lat - location1.lat) * Math.PI / 180;
const dLng = (location2.lng - location1.lng) * Math.PI / 180;
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(location1.lat * Math.PI / 180) * Math.cos(location2.lat * Math.PI / 180) *
Math.sin(dLng/2) * Math.sin(dLng/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
const distance = R * c;
return distance;
}
// Tarea programada para verificar conciertos
cron.schedule('0 */6 * * *', async () => {
console.log('Running scheduled concert check...');
for (const [userId, user] of users) {
if (user.active && user.artists.length > 0) {
for (const artist of user.artists) {
await checkArtistConcerts(artist, user);
}
}
}
});
console.log('🤖 ConciertoBot iniciado');
🗄️ Base de Datos y API Integration
// MongoDB Schema (Mongoose)
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
telegramId: { type: String, unique: true, required: true },
username: String,
firstName: String,
lastName: String,
location: {
city: String,
country: String,
coordinates: {
lat: Number,
lng: Number
}
},
preferences: {
radius: { type: Number, default: 100 }, // km
advanceNotice: { type: Number, default: 14 }, // days
genres: [{ type: String, enum: ['metal', 'rock', 'punk', 'industrial', 'alternative'] }],
notificationFrequency: { type: String, enum: ['immediate', 'daily', 'weekly'], default: 'weekly' }
},
artists: [{
name: String,
mbid: String, // MusicBrainz ID
image: String,
addedAt: { type: Date, default: Date.now }
}],
alerts: [{
concertId: String,
sentAt: { type: Date, default: Date.now },
status: { type: String, enum: ['sent', 'interested', 'not_interested', 'attending'], default: 'sent' }
}],
active: { type: Boolean, default: true },
createdAt: { type: Date, default: Date.now },
lastActivity: { type: Date, default: Date.now }
});
const concertSchema = new mongoose.Schema({
externalId: String,
artist: {
name: String,
mbid: String,
image: String
},
venue: {
name: String,
address: String,
city: String,
country: String,
coordinates: {
lat: Number,
lng: Number
}
},
date: Date,
time: String,
ticketUrl: String,
price: {
min: Number,
max: Number,
currency: String
},
attendees: [{
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
status: { type: String, enum: ['interested', 'attending', 'not_interested'] },
notifiedAt: { type: Date, default: Date.now }
}],
source: { type: String, enum: ['songkick', 'lastfm', 'bandsintown', 'ticketmaster'] },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
// API Integration Service
class ConcertService {
constructor() {
this.apis = {
songkick: {
baseUrl: 'https://api.songkick.com/api/3.0',
key: process.env.SONGKICK_API_KEY
},
bandsintown: {
baseUrl: 'https://rest.bandsintown.com',
key: process.env.BANDSINTOWN_API_KEY
},
ticketmaster: {
baseUrl: 'https://app.ticketmaster.com/discovery/v2',
key: process.env.TICKETMASTER_API_KEY
}
};
}
async searchConcerts(artist, location, radius = 100) {
const concerts = [];
// Buscar en múltiples APIs
const promises = [
this.searchSongkick(artist, location, radius),
this.searchBandsintown(artist, location, radius),
this.searchTicketmaster(artist, location, radius)
];
const results = await Promise.allSettled(promises);
for (const result of results) {
if (result.status === 'fulfilled' && result.value) {
concerts.push(...result.value);
}
}
// Eliminar duplicados
return this.removeDuplicates(concerts);
}
async searchSongkick(artist, location, radius) {
try {
const response = await axios.get(`${this.apis.songkick.baseUrl}/events.json`, {
params: {
apikey: this.apis.songkick.key,
artist_name: artist.name,
location: `geo:${location.lat},${location.lng}`,
radius: radius
}
});
return response.data.resultsPage?.results?.event?.map(this.normalizeSongkickEvent) || [];
} catch (error) {
console.error('Songkick API error:', error);
return [];
}
}
async searchBandsintown(artist, location, radius) {
try {
const response = await axios.get(`${this.apis.bandsintown.baseUrl}/artists/${artist.name}/events`, {
params: {
app_id: this.apis.bandsintown.key,
date: 'upcoming'
}
});
return response.data?.map(this.normalizeBandsintownEvent) || [];
} catch (error) {
console.error('Bandsintown API error:', error);
return [];
}
}
normalizeSongkickEvent(event) {
return {
externalId: `songkick_${event.id}`,
artist: {
name: event.performance[0]?.artist?.displayName,
mbid: event.performance[0]?.artist?.identifier?.[0]?.mbid
},
venue: {
name: event.venue.displayName,
city: event.location.city,
coordinates: {
lat: event.venue.lat,
lng: event.venue.lng
}
},
date: new Date(event.start.date),
time: event.start.time,
ticketUrl: event.uri,
source: 'songkick'
};
}
removeDuplicates(concerts) {
const seen = new Set();
return concerts.filter(concert => {
const key = `${concert.artist.name}-${concert.date.getTime()}-${concert.venue.name}`;
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
}
// Notification Service
class NotificationService {
constructor(bot) {
this.bot = bot;
}
async sendConcertAlert(user, concert, distance) {
const message = this.formatConcertMessage(concert, distance);
const keyboard = this.createConcertKeyboard(concert);
try {
await this.bot.sendMessage(user.telegramId, message, {
parse_mode: 'Markdown',
reply_markup: keyboard
});
// Guardar notificación en base de datos
await this.saveNotification(user, concert, 'sent');
} catch (error) {
console.error('Error sending notification:', error);
throw error;
}
}
formatConcertMessage(concert, distance) {
const date = concert.date.toLocaleDateString('es-ES', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
return `
🎸 *¡Concierto de ${concert.artist.name}!*
📅 *Fecha:* ${date}
🕐 *Hora:* ${concert.time || 'Por confirmar'}
📍 *Lugar:* ${concert.venue.name}, ${concert.venue.city}
📏 *Distancia:* ${distance.toFixed(1)} km de tu ubicación
${concert.ticketUrl ? `🎫 [Comprar Entradas](${concert.ticketUrl})` : ''}
¿Te interesa este concierto?
`.trim();
}
createConcertKeyboard(concert) {
return {
inline_keyboard: [
[
{ text: '✅ Me interesa', callback_data: `interested_${concert._id}` },
{ text: '🎫 Comprar', url: concert.ticketUrl }
],
[
{ text: '❌ No me interesa', callback_data: `not_interested_${concert._id}` },
{ text: '🔕 Silenciar artista', callback_data: `mute_${concert.artist.name}` }
]
]
};
}
}