Blog

Reddit API: Mensajes Privados e Integración de Bandeja de Entrada

Aprende cómo integrar mensajes de Reddit API en tu aplicación. Guía completa para enviar DMs, obtener la bandeja de entrada y gestionar conversaciones con ejemplos en TypeScript.

Por

+8

Publica en todo. Una API.

Try Free

Construir una integración de Reddit que maneje mensajes privados requiere entender el enfoque único de Reddit hacia la mensajería. El sistema de reddit api messages utiliza una combinación de endpoints de bandeja de entrada, identificadores fullname y scopes de OAuth que difieren de las APIs de mensajería típicas. Esta guía te lleva a través de todo lo que necesitas para implementar una integración completa de DM de Reddit.

El sistema de mensajería de Reddit maneja tanto mensajes privados entre usuarios como respuestas a comentarios en una bandeja de entrada unificada. Tu aplicación necesita distinguir entre estos tipos, gestionar hilos de conversación y manejar los límites de tasa de Reddit apropiadamente.

Introducción a Reddit API

Reddit proporciona una API completa para acceder a datos de usuarios, publicar contenido y gestionar mensajes privados. La API utiliza OAuth 2.0 para autenticación y requiere scopes específicos para diferentes operaciones.

Para la funcionalidad de mensajería, trabajarás con estos endpoints principales:

EndpointPropósitoMétodo
/message/inboxTodos los elementos de la bandeja de entrada (mensajes + respuestas a comentarios)GET
/message/messagesSolo mensajes privadosGET
/message/sentMensajes enviadosGET
/api/composeEnviar nuevo mensajePOST
/api/commentResponder a mensajePOST
/api/read_messageMarcar como leídoPOST

La reddit inbox api devuelve mensajes en un formato de listado con soporte de paginación. Cada mensaje incluye metadatos sobre el remitente, destinatario, asunto e hilo de conversación.

Arquitectura de Reddit API

Autenticación OAuth 2.0 para Reddit

Antes de acceder a la reddit private messages api, necesitas autenticar usuarios con los scopes de OAuth correctos. El scope privatemessages otorga acceso para leer y enviar mensajes directos.

interface RedditAuthConfig {
  clientId: string;
  clientSecret: string;
  redirectUri: string;
  scopes: string[];
}

const config: RedditAuthConfig = {
  clientId: process.env.REDDIT_CLIENT_ID || '',
  clientSecret: process.env.REDDIT_CLIENT_SECRET || '',
  redirectUri: process.env.REDDIT_REDIRECT_URI || '',
  scopes: ['identity', 'privatemessages', 'read', 'history']
};

function getAuthUrl(state: string): string {
  const params = new URLSearchParams({
    client_id: config.clientId,
    response_type: 'code',
    state: state,
    redirect_uri: config.redirectUri,
    duration: 'permanent',
    scope: config.scopes.join(' '),
  });
  
  return `https://www.reddit.com/api/v1/authorize?${params.toString()}`;
}

async function exchangeCodeForToken(code: string): Promise<TokenResponse> {
  const credentials = Buffer.from(
    `${config.clientId}:${config.clientSecret}`
  ).toString('base64');
  
  const response = await fetch('https://www.reddit.com/api/v1/access_token', {
    method: 'POST',
    headers: {
      'Authorization': `Basic ${credentials}`,
      'Content-Type': 'application/x-www-form-urlencoded',
      'User-Agent': 'YourApp/1.0.0',
    },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      redirect_uri: config.redirectUri,
    }).toString(),
  });
  
  if (!response.ok) {
    const error = await response.text();
    throw new Error(`Token exchange failed: ${response.status} ${error}`);
  }
  
  return response.json();
}

interface TokenResponse {
  access_token: string;
  refresh_token: string;
  expires_in: number;
  scope: string;
  token_type: string;
}

Nota: Reddit requiere un encabezado User-Agent descriptivo para todas las solicitudes de API. Usar un User-Agent genérico o faltante resultará en limitación de tasa o solicitudes bloqueadas.

Entendiendo los Fullnames (t1_, t3_, t4_)

Reddit utiliza un sistema de "fullname" para identificar todos los objetos en la API. Entender estos prefijos es crítico cuando trabajas con la reddit dm api:

PrefijoTipo de ObjetoEjemplo
t1_Comentariot1_abc123
t2_Cuentat2_xyz789
t3_Enlace/Publicaciónt3_def456
t4_Mensajet4_ghi012
t5_Subredditt5_jkl345

Al responder mensajes o marcarlos como leídos, debes usar el fullname completo incluyendo el prefijo:

function normalizeFullname(id: string, type: 't1' | 't3' | 't4'): string {
  if (id.startsWith(`${type}_`)) {
    return id;
  }
  return `${type}_${id}`;
}

// Ejemplos de uso
const messageFullname = normalizeFullname('abc123', 't4'); // Devuelve: t4_abc123
const commentFullname = normalizeFullname('t1_xyz789', 't1'); // Devuelve: t1_xyz789

Obteniendo Mensajes de la Bandeja de Entrada

El endpoint de bandeja de entrada devuelve todos los elementos incluyendo mensajes privados y respuestas a comentarios. Para obtener solo mensajes privados, usa el endpoint /message/messages en su lugar.

interface RedditMessage {
  id: string;
  name: string; // Fullname (t4_xxx)
  author: string;
  dest: string;
  subject: string;
  body: string;
  created_utc: number;
  new: boolean;
  was_comment: boolean;
  first_message_name: string | null;
}

interface InboxResponse {
  messages: RedditMessage[];
  after: string | null;
}

async function fetchInboxMessages(
  accessToken: string,
  options: { limit?: number; after?: string } = {}
): Promise<InboxResponse> {
  const limit = Math.min(options.limit || 25, 100);
  let url = `https://oauth.reddit.com/message/messages?limit=${limit}&raw_json=1`;
  
  if (options.after) {
    url += `&after=${options.after}`;
  }
  
  const response = await fetch(url, {
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'User-Agent': 'YourApp/1.0.0',
    },
  });
  
  if (!response.ok) {
    const error = await response.text();
    throw new Error(`Failed to fetch inbox: ${response.status} ${error}`);
  }
  
  const data = await response.json();
  
  const messages: RedditMessage[] = (data.data?.children || [])
    .filter((item: any) => !item.data.was_comment) // Filtrar respuestas a comentarios
    .map((item: any) => ({
      id: item.data.id,
      name: item.data.name,
      author: item.data.author,
      dest: item.data.dest,
      subject: item.data.subject,
      body: item.data.body,
      created_utc: item.data.created_utc,
      new: item.data.new,
      was_comment: item.data.was_comment,
      first_message_name: item.data.first_message_name,
    }));
  
  return {
    messages,
    after: data.data?.after || null,
  };
}

El campo first_message_name es importante para el threading. Los mensajes en la misma conversación comparten este valor, permitiéndote agruparlos juntos.

Build faster with Late

One API call to post everywhere. No OAuth headaches. No platform-specific code.

Free tier • No credit card • 99.97% uptime

Enviando Mensajes Privados con Reddit API

Para enviar un nuevo mensaje privado, usa el endpoint /api/compose. Este endpoint requiere el nombre de usuario del destinatario, una línea de asunto y el cuerpo del mensaje.

interface SendMessageParams {
  to: string;
  subject: string;
  text: string;
}

interface SendMessageResult {
  success: boolean;
  errors?: string[];
}

async function sendPrivateMessage(
  accessToken: string,
  params: SendMessageParams
): Promise<SendMessageResult> {
  const { to, subject, text } = params;
  
  // Validar entradas
  if (!to || !subject || !text) {
    return {
      success: false,
      errors: ['Destinatario, asunto y texto del mensaje son requeridos'],
    };
  }
  
  if (subject.length > 100) {
    return {
      success: false,
      errors: ['El asunto debe tener 100 caracteres o menos'],
    };
  }
  
  const response = await fetch('https://oauth.reddit.com/api/compose', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'User-Agent': 'YourApp/1.0.0',
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      api_type: 'json',
      to,
      subject,
      text,
    }).toString(),
  });
  
  if (!response.ok) {
    const error = await response.text();
    throw new Error(`Failed to send message: ${response.status} ${error}`);
  }
  
  const data = await response.json();
  const errors = data.json?.errors || [];
  
  if (errors.length > 0) {
    return {
      success: false,
      errors: errors.map((e: any[]) => e.join(': ')),
    };
  }
  
  return { success: true };
}

Nota: Reddit limita a las cuentas nuevas de enviar mensajes privados. Los usuarios pueden necesitar verificar su correo electrónico y tener cierta antigüedad de cuenta antes de que la función de mensajería esté disponible.

Respondiendo a Mensajes

Responder a un mensaje existente usa el endpoint /api/comment (el mismo endpoint usado para respuestas a comentarios). Necesitas el fullname del mensaje al que estás respondiendo.

interface ReplyResult {
  success: boolean;
  replyId?: string;
  errors?: string[];
}

async function replyToMessage(
  accessToken: string,
  messageFullname: string,
  text: string
): Promise<ReplyResult> {
  // Asegurar formato de fullname correcto
  const fullname = messageFullname.startsWith('t4_') 
    ? messageFullname 
    : `t4_${messageFullname}`;
  
  const response = await fetch('https://oauth.reddit.com/api/comment', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'User-Agent': 'YourApp/1.0.0',
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      api_type: 'json',
      thing_id: fullname,
      text,
    }).toString(),
  });
  
  if (!response.ok) {
    const error = await response.text();
    throw new Error(`Failed to reply: ${response.status} ${error}`);
  }
  
  const data = await response.json();
  const errors = data.json?.errors || [];
  
  if (errors.length > 0) {
    return {
      success: false,
      errors: errors.map((e: any[]) => e.join(': ')),
    };
  }
  
  const replyData = data.json?.data?.things?.[0]?.data;
  
  return {
    success: true,
    replyId: replyData?.id,
  };
}

Threading y Agrupación de Mensajes

Reddit agrupa mensajes en conversaciones usando el campo first_message_name. Construir una vista de conversación requiere obtener mensajes tanto de las carpetas de bandeja de entrada como de enviados, luego agruparlos por hilo.

interface Conversation {
  id: string;
  participantUsername: string;
  subject: string;
  messages: ThreadMessage[];
  lastUpdated: Date;
  hasUnread: boolean;
}

interface ThreadMessage {
  id: string;
  fullname: string;
  text: string;
  sentAt: Date;
  isFromMe: boolean;
  senderUsername: string;
}

async function getConversations(
  accessToken: string,
  myUsername: string
): Promise<Conversation[]> {
  // Obtener tanto mensajes de bandeja de entrada como enviados
  const [inboxResponse, sentResponse] = await Promise.all([
    fetch('https://oauth.reddit.com/message/messages?limit=100&raw_json=1', {
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'User-Agent': 'YourApp/1.0.0',
      },
    }),
    fetch('https://oauth.reddit.com/message/sent?limit=100&raw_json=1', {
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'User-Agent': 'YourApp/1.0.0',
      },
    }),
  ]);
  
  const inboxData = await inboxResponse.json();
  const sentData = await sentResponse.json();
  
  const allMessages: any[] = [
    ...(inboxData.data?.children || []),
    ...(sentData.data?.children || []),
  ];
  
  // Agrupar por conversación
  const conversationsMap = new Map<string, Conversation>();
  const seenMessageIds = new Set<string>();
  
  for (const item of allMessages) {
    const msg = item.data;
    
    // Omitir duplicados y respuestas a comentarios
    if (seenMessageIds.has(msg.name) || msg.was_comment) {
      continue;
    }
    seenMessageIds.add(msg.name);
    
    const conversationId = msg.first_message_name || msg.name;
    const isFromMe = msg.author?.toLowerCase() === myUsername.toLowerCase();
    const otherParticipant = isFromMe ? msg.dest : msg.author;
    
    const threadMessage: ThreadMessage = {
      id: msg.id,
      fullname: msg.name,
      text: msg.body,
      sentAt: new Date(msg.created_utc * 1000),
      isFromMe,
      senderUsername: msg.author,
    };
    
    if (!conversationsMap.has(conversationId)) {
      conversationsMap.set(conversationId, {
        id: conversationId,
        participantUsername: otherParticipant,
        subject: msg.subject,
        messages: [threadMessage],
        lastUpdated: threadMessage.sentAt,
        hasUnread: msg.new && !isFromMe,
      });
    } else {
      const conversation = conversationsMap.get(conversationId)!;
      conversation.messages.push(threadMessage);
      
      if (threadMessage.sentAt > conversation.lastUpdated) {
        conversation.lastUpdated = threadMessage.sentAt;
      }
      
      if (msg.new && !isFromMe) {
        conversation.hasUnread = true;
      }
    }
  }
  
  // Ordenar mensajes dentro de cada conversación y ordenar conversaciones por última actualización
  const conversations = Array.from(conversationsMap.values());
  
  for (const conv of conversations) {
    conv.messages.sort((a, b) => a.sentAt.getTime() - b.sentAt.getTime());
  }
  
  conversations.sort((a, b) => b.lastUpdated.getTime() - a.lastUpdated.getTime());
  
  return conversations;
}

Marcando Mensajes como Leídos

El endpoint /api/read_message acepta una lista de fullnames separados por comas, permitiéndote marcar múltiples mensajes como leídos en una sola solicitud.

async function markMessagesAsRead(
  acc

Una API. 13+ plataformas.

Integra redes sociales en minutos, no semanas.

Diseñada para desarrolladores. Usada por agencias. Más de 6,325 usuarios.