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

grammy-media-groups

v0.0.5

Published

Media Groups Storage Plugin for grammY

Readme

Media Groups Storage Plugin for grammY

A grammY plugin that stores media group messages using the storages protocol. It collects all messages that share the same media_group_id from both incoming updates and outgoing API responses, and lets you retrieve the full group at any time.

Features

  • Middleware — automatically stores every incoming message that has a media_group_id.
  • Transformer — intercepts Telegram API responses (sendMediaGroup, forwardMessage, editMessageMedia, editMessageCaption, editMessageReplyMarkup) and stores returned messages.
  • Context hydration — adds ctx.mediaGroups.getForMsg() to fetch the current message's media group.
  • Reply/pinned helpersctx.mediaGroups.getForReply() and ctx.mediaGroups.getForPinned() for sub-messages.
  • Programmatic access — the returned composer exposes getMediaGroup(mediaGroupId) for use outside of middleware.
  • Manual mode — pass { autoStore: false } to disable automatic storing and use ctx.mediaGroups.store(message) for full control.
  • Deletectx.mediaGroups.delete(mediaGroupId) or mg.deleteMediaGroup(mediaGroupId) removes a media group from storage.
  • ConverttoInputMedia(messages) or ctx.mediaGroups.toInputMedia(messages) converts stored messages into InputMedia[] ready for sendMediaGroup. Supports photo, video, document, audio and animation, with optional caption/parse_mode override.

Installation

Node.js

npm install grammy-media-groups

Deno

import {
    mediaGroups,
    type MediaGroupsFlavor,
    toInputMedia,
} from "npm:grammy-media-groups";

Usage

import { Bot, Context, InlineKeyboard } from "grammy";
import { mediaGroups, type MediaGroupsFlavor } from "grammy-media-groups";

type MyContext = Context & MediaGroupsFlavor;

const bot = new Bot<MyContext>("<your-bot-token>");

// Uses MemorySessionStorage by default — pass a custom adapter for persistence
const mg = mediaGroups();
bot.use(mg);

// Install transformer for outgoing API responses
bot.api.config.use(mg.transformer);

// Reply once when the first message of a media group arrives
bot.on("message", async (ctx) => {
    const group = await ctx.mediaGroups.getForMsg();
    if (group?.length === 1) {
        await ctx.reply("Media group detected", {
            reply_parameters: { message_id: ctx.msg.message_id },
            reply_markup: new InlineKeyboard().text("Copy", "copy"),
        });
    }
});

// Handle inline keyboard button to resend a media group
bot.callbackQuery("copy", async (ctx) => {
    const group = await ctx.mediaGroups.getForReply();
    if (group) {
        await ctx.replyWithMediaGroup(
            ctx.mediaGroups.toInputMedia(group),
        );
    }
    await ctx.answerCallbackQuery();
});

// Reply to an album message with /copy to resend the full media group
bot.command("copy", async (ctx) => {
    const group = await ctx.mediaGroups.getForReply();
    if (group) {
        await ctx.replyWithMediaGroup(
            ctx.mediaGroups.toInputMedia(group),
        );
    }
});

// Programmatic access outside middleware
const messages = await mg.getMediaGroup("some-media-group-id");

Manual Mode

To disable automatic storing, pass { autoStore: false }. This gives you full control over which messages get stored via ctx.mediaGroups.store():

const mg = mediaGroups(undefined, { autoStore: false });
bot.use(mg);

bot.on("message", async (ctx) => {
    // Only store messages you care about
    if (ctx.msg.media_group_id) {
        await ctx.mediaGroups.store(ctx.msg);
    }

    // You can also manually store reply_to_message
    const reply = ctx.msg.reply_to_message;
    if (reply?.media_group_id) {
        await ctx.mediaGroups.store(reply);
    }

    // Delete a media group when no longer needed
    // await ctx.mediaGroups.delete("some-media-group-id");
});

// Delete from outside middleware
// await mg.deleteMediaGroup("some-media-group-id");