grammy-media-groups 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021-2023 grammyjs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # Media Groups Storage Plugin for grammY
2
+
3
+ A [grammY](https://grammy.dev/) plugin that stores media group messages using
4
+ the [storages](https://github.com/grammyjs/storages) protocol. It collects all
5
+ messages that share the same `media_group_id` from both incoming updates and
6
+ outgoing API responses, and lets you retrieve the full group at any time.
7
+
8
+ ## Features
9
+
10
+ - **Middleware** — automatically stores every incoming message that has a
11
+ `media_group_id`.
12
+ - **Transformer** — intercepts Telegram API responses (`sendMediaGroup`,
13
+ `forwardMessage`, `editMessageMedia`, `editMessageCaption`,
14
+ `editMessageReplyMarkup`) and stores returned messages.
15
+ - **Context hydration** — adds `ctx.mediaGroups.getForMsg()` to fetch the
16
+ current message's media group.
17
+ - **Reply/pinned helpers** — `ctx.mediaGroups.getForReply()` and
18
+ `ctx.mediaGroups.getForPinned()` for sub-messages.
19
+ - **Programmatic access** — the returned composer exposes
20
+ `getMediaGroup(mediaGroupId)` for use outside of middleware.
21
+ - **Manual mode** — pass `{ autoStore: false }` to disable automatic storing
22
+ and use `ctx.mediaGroups.store(message)` for full control.
23
+ - **Delete** — `ctx.mediaGroups.delete(mediaGroupId)` or
24
+ `mg.deleteMediaGroup(mediaGroupId)` removes a media group from storage.
25
+
26
+ ## Installation
27
+
28
+ ### Node.js
29
+
30
+ ```bash
31
+ npm install github:PonomareVlad/grammy-media-groups
32
+ ```
33
+
34
+ ### Deno
35
+
36
+ ```typescript
37
+ import {
38
+ mediaGroups,
39
+ type MediaGroupsFlavor,
40
+ } from "https://raw.githubusercontent.com/PonomareVlad/grammy-media-groups/main/src/mod.ts";
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ```typescript
46
+ import { Bot, Context, InputMediaBuilder } from "grammy";
47
+ import { mediaGroups, type MediaGroupsFlavor } from "@grammyjs/media-groups";
48
+
49
+ type MyContext = Context & MediaGroupsFlavor;
50
+
51
+ const bot = new Bot<MyContext>("<your-bot-token>");
52
+
53
+ // Uses MemorySessionStorage by default — pass a custom adapter for persistence
54
+ const mg = mediaGroups();
55
+ bot.use(mg);
56
+
57
+ // Install transformer for outgoing API responses
58
+ bot.api.config.use(mg.transformer);
59
+
60
+ // Retrieve the media group of the current message
61
+ bot.on("message", async (ctx) => {
62
+ const group = await ctx.mediaGroups.getForMsg();
63
+ if (group) {
64
+ console.log(`Media group has ${group.length} messages`);
65
+ }
66
+ });
67
+
68
+ // Reply to an album message with /album to resend the full media group
69
+ bot.command("album", async (ctx) => {
70
+ const group = await ctx.mediaGroups.getForReply();
71
+ if (group) {
72
+ await ctx.replyWithMediaGroup(
73
+ group
74
+ .map((msg) => {
75
+ const opts = {
76
+ caption: msg.caption,
77
+ caption_entities: msg.caption_entities,
78
+ };
79
+ switch (true) {
80
+ case "photo" in msg: {
81
+ const id = msg.photo?.at(-1)?.file_id;
82
+ return InputMediaBuilder.photo(id, opts);
83
+ }
84
+ case "video" in msg: {
85
+ const id = msg.video?.file_id;
86
+ return InputMediaBuilder.video(id, opts);
87
+ }
88
+ case "document" in msg: {
89
+ const id = msg.document?.file_id;
90
+ return InputMediaBuilder.document(id, opts);
91
+ }
92
+ }
93
+ })
94
+ .filter(Boolean),
95
+ );
96
+ }
97
+ });
98
+
99
+ // Programmatic access outside middleware
100
+ const messages = await mg.getMediaGroup("some-media-group-id");
101
+ ```
102
+
103
+ ### Manual Mode
104
+
105
+ To disable automatic storing, pass `{ autoStore: false }`. This gives you full
106
+ control over which messages get stored via `ctx.mediaGroups.store()`:
107
+
108
+ ```typescript
109
+ const mg = mediaGroups(undefined, { autoStore: false });
110
+ bot.use(mg);
111
+
112
+ bot.on("message", async (ctx) => {
113
+ // Only store messages you care about
114
+ if (ctx.msg.media_group_id) {
115
+ await ctx.mediaGroups.store(ctx.msg);
116
+ }
117
+
118
+ // You can also manually store reply_to_message
119
+ const reply = ctx.msg.reply_to_message;
120
+ if (reply?.media_group_id) {
121
+ await ctx.mediaGroups.store(reply);
122
+ }
123
+
124
+ // Delete a media group when no longer needed
125
+ // await ctx.mediaGroups.delete("some-media-group-id");
126
+ });
127
+
128
+ // Delete from outside middleware
129
+ // await mg.deleteMediaGroup("some-media-group-id");
130
+ ```
@@ -0,0 +1,2 @@
1
+ export { Api, Composer, Context, MemorySessionStorage, type StorageAdapter, type Transformer, } from "grammy";
2
+ export type { Message, UserFromGetMe } from "grammy/types";
@@ -0,0 +1 @@
1
+ export { Api, Composer, Context, MemorySessionStorage, } from "grammy";
package/out/mod.d.ts ADDED
@@ -0,0 +1,180 @@
1
+ import type { Message } from "./deps.node.js";
2
+ import { Composer, Context, type StorageAdapter, type Transformer } from "./deps.node.js";
3
+ export { MEDIA_GROUP_METHODS, storeMessages } from "./storage.js";
4
+ /**
5
+ * Options for the media groups plugin.
6
+ */
7
+ export interface MediaGroupsOptions {
8
+ /**
9
+ * When `true` (default), the middleware automatically stores every
10
+ * incoming message that has a `media_group_id` (including
11
+ * `reply_to_message` and `pinned_message`).
12
+ *
13
+ * Set to `false` to disable automatic storing. In that case use
14
+ * `ctx.mediaGroups.store(message)` to store messages manually.
15
+ */
16
+ autoStore?: boolean;
17
+ }
18
+ /**
19
+ * Flavor for context that adds media group methods.
20
+ */
21
+ export type MediaGroupsFlavor = {
22
+ /**
23
+ * Namespace of the `media-groups` plugin
24
+ */
25
+ mediaGroups: {
26
+ /**
27
+ * Gets all messages belonging to the current message's media group.
28
+ * Returns `undefined` if the current message is not part of a media group.
29
+ */
30
+ getForMsg: () => Promise<Message[] | undefined>;
31
+ /**
32
+ * Gets the media group of the message being replied to.
33
+ * Returns `undefined` if there is no reply or it is not part of a media group.
34
+ */
35
+ getForReply: () => Promise<Message[] | undefined>;
36
+ /**
37
+ * Gets the media group of the pinned message.
38
+ * Returns `undefined` if there is no pinned message or it is not part of a media group.
39
+ */
40
+ getForPinned: () => Promise<Message[] | undefined>;
41
+ /**
42
+ * Manually stores a message in its media group.
43
+ * The message must have a `media_group_id` to be stored.
44
+ *
45
+ * @param message The message to store
46
+ */
47
+ store: (message: Message) => Promise<void>;
48
+ /**
49
+ * Deletes a media group from storage by its ID.
50
+ *
51
+ * @param mediaGroupId The media group ID to delete
52
+ */
53
+ delete: (mediaGroupId: string) => Promise<void>;
54
+ };
55
+ };
56
+ type MediaGroupsContext = Context & MediaGroupsFlavor;
57
+ /**
58
+ * Creates a transformer that intercepts outgoing API responses and
59
+ * stores returned messages containing `media_group_id`.
60
+ *
61
+ * Install it manually on your bot's API:
62
+ *
63
+ * ```typescript
64
+ * bot.api.config.use(mediaGroupTransformer(adapter));
65
+ * ```
66
+ *
67
+ * @param adapter Storage adapter for persisting media group data
68
+ * @returns An API transformer
69
+ */
70
+ export declare function mediaGroupTransformer(adapter: StorageAdapter<Message[]>): Transformer<any>;
71
+ /**
72
+ * Creates middleware that collects media group messages from incoming
73
+ * updates and hydrates the context with methods to retrieve stored
74
+ * media groups.
75
+ *
76
+ * @param adapter Storage adapter for persisting media group data (defaults to `MemorySessionStorage`)
77
+ * @param options Plugin options
78
+ * @returns A composer with middleware installed
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * import { Bot, Context, InputMediaBuilder } from "grammy";
83
+ * import {
84
+ * type MediaGroupsFlavor,
85
+ * mediaGroups,
86
+ * } from "@grammyjs/media-groups";
87
+ *
88
+ * type MyContext = Context & MediaGroupsFlavor;
89
+ *
90
+ * const bot = new Bot<MyContext>("<token>");
91
+ *
92
+ * // Uses MemorySessionStorage by default
93
+ * const mg = mediaGroups();
94
+ * bot.use(mg);
95
+ *
96
+ * // Install transformer for outgoing API responses
97
+ * bot.api.config.use(mg.transformer);
98
+ *
99
+ * // Programmatic access
100
+ * const messages = await mg.getMediaGroup("some-media-group-id");
101
+ *
102
+ * // In a command handler replying to a media group message
103
+ * bot.command("album", async (ctx) => {
104
+ * const group = await ctx.mediaGroups.getForReply();
105
+ * if (group) {
106
+ * await ctx.replyWithMediaGroup(
107
+ * group
108
+ * .map((msg) => {
109
+ * const opts = {
110
+ * caption: msg.caption,
111
+ * caption_entities: msg.caption_entities,
112
+ * };
113
+ * switch (true) {
114
+ * case "photo" in msg: {
115
+ * const id = msg.photo?.at(-1)?.file_id;
116
+ * return InputMediaBuilder.photo(id, opts);
117
+ * }
118
+ * case "video" in msg: {
119
+ * const id = msg.video?.file_id;
120
+ * return InputMediaBuilder.video(id, opts);
121
+ * }
122
+ * case "document" in msg: {
123
+ * const id = msg.document?.file_id;
124
+ * return InputMediaBuilder.document(id, opts);
125
+ * }
126
+ * }
127
+ * })
128
+ * .filter(Boolean),
129
+ * );
130
+ * }
131
+ * });
132
+ *
133
+ * // Access media group of current message via namespace
134
+ * bot.on("message", async (ctx) => {
135
+ * const group = await ctx.mediaGroups.getForMsg();
136
+ * if (group) console.log(`Album has ${group.length} items`);
137
+ * });
138
+ * ```
139
+ *
140
+ * @example Manual mode — disable automatic storing and use `ctx.mediaGroups.store()` instead:
141
+ * ```typescript
142
+ * const mg = mediaGroups(undefined, { autoStore: false });
143
+ * bot.use(mg);
144
+ *
145
+ * bot.on("message", async (ctx) => {
146
+ * if (ctx.msg.media_group_id) {
147
+ * await ctx.mediaGroups.store(ctx.msg);
148
+ * }
149
+ * });
150
+ * ```
151
+ *
152
+ * @example Deleting a media group from storage:
153
+ * ```typescript
154
+ * // From within middleware
155
+ * await ctx.mediaGroups.delete("some-media-group-id");
156
+ *
157
+ * // From outside middleware
158
+ * await mg.deleteMediaGroup("some-media-group-id");
159
+ * ```
160
+ */
161
+ export declare function mediaGroups(adapter?: StorageAdapter<Message[]>, options?: MediaGroupsOptions): Composer<MediaGroupsContext> & {
162
+ /** The storage adapter used by the plugin. */
163
+ adapter: StorageAdapter<Message[]>;
164
+ /** Pre-built API transformer. Install via `bot.api.config.use(mg.transformer)`. */
165
+ transformer: Transformer<any>;
166
+ /**
167
+ * Fetches a media group by its ID from storage.
168
+ *
169
+ * @param mediaGroupId The media group ID to look up
170
+ * @returns Array of messages in the media group, or `undefined` if not found
171
+ */
172
+ getMediaGroup: (mediaGroupId: string) => Promise<Message[] | undefined>;
173
+ /**
174
+ * Deletes a media group from storage by its ID.
175
+ *
176
+ * @param mediaGroupId The media group ID to delete
177
+ */
178
+ deleteMediaGroup: (mediaGroupId: string) => Promise<void>;
179
+ };
180
+ export default mediaGroups;
package/out/mod.js ADDED
@@ -0,0 +1,165 @@
1
+ import { Composer, MemorySessionStorage, } from "./deps.node.js";
2
+ import { MEDIA_GROUP_METHODS, storeMessages } from "./storage.js";
3
+ export { MEDIA_GROUP_METHODS, storeMessages } from "./storage.js";
4
+ /**
5
+ * Creates a transformer that intercepts outgoing API responses and
6
+ * stores returned messages containing `media_group_id`.
7
+ *
8
+ * Install it manually on your bot's API:
9
+ *
10
+ * ```typescript
11
+ * bot.api.config.use(mediaGroupTransformer(adapter));
12
+ * ```
13
+ *
14
+ * @param adapter Storage adapter for persisting media group data
15
+ * @returns An API transformer
16
+ */
17
+ export function mediaGroupTransformer(adapter) {
18
+ return async (prev, method, payload, signal) => {
19
+ const res = await prev(method, payload, signal);
20
+ const extractor = MEDIA_GROUP_METHODS[method];
21
+ if (res.ok && extractor) {
22
+ await storeMessages(adapter, extractor(res.result));
23
+ }
24
+ return res;
25
+ };
26
+ }
27
+ /**
28
+ * Creates middleware that collects media group messages from incoming
29
+ * updates and hydrates the context with methods to retrieve stored
30
+ * media groups.
31
+ *
32
+ * @param adapter Storage adapter for persisting media group data (defaults to `MemorySessionStorage`)
33
+ * @param options Plugin options
34
+ * @returns A composer with middleware installed
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * import { Bot, Context, InputMediaBuilder } from "grammy";
39
+ * import {
40
+ * type MediaGroupsFlavor,
41
+ * mediaGroups,
42
+ * } from "@grammyjs/media-groups";
43
+ *
44
+ * type MyContext = Context & MediaGroupsFlavor;
45
+ *
46
+ * const bot = new Bot<MyContext>("<token>");
47
+ *
48
+ * // Uses MemorySessionStorage by default
49
+ * const mg = mediaGroups();
50
+ * bot.use(mg);
51
+ *
52
+ * // Install transformer for outgoing API responses
53
+ * bot.api.config.use(mg.transformer);
54
+ *
55
+ * // Programmatic access
56
+ * const messages = await mg.getMediaGroup("some-media-group-id");
57
+ *
58
+ * // In a command handler replying to a media group message
59
+ * bot.command("album", async (ctx) => {
60
+ * const group = await ctx.mediaGroups.getForReply();
61
+ * if (group) {
62
+ * await ctx.replyWithMediaGroup(
63
+ * group
64
+ * .map((msg) => {
65
+ * const opts = {
66
+ * caption: msg.caption,
67
+ * caption_entities: msg.caption_entities,
68
+ * };
69
+ * switch (true) {
70
+ * case "photo" in msg: {
71
+ * const id = msg.photo?.at(-1)?.file_id;
72
+ * return InputMediaBuilder.photo(id, opts);
73
+ * }
74
+ * case "video" in msg: {
75
+ * const id = msg.video?.file_id;
76
+ * return InputMediaBuilder.video(id, opts);
77
+ * }
78
+ * case "document" in msg: {
79
+ * const id = msg.document?.file_id;
80
+ * return InputMediaBuilder.document(id, opts);
81
+ * }
82
+ * }
83
+ * })
84
+ * .filter(Boolean),
85
+ * );
86
+ * }
87
+ * });
88
+ *
89
+ * // Access media group of current message via namespace
90
+ * bot.on("message", async (ctx) => {
91
+ * const group = await ctx.mediaGroups.getForMsg();
92
+ * if (group) console.log(`Album has ${group.length} items`);
93
+ * });
94
+ * ```
95
+ *
96
+ * @example Manual mode — disable automatic storing and use `ctx.mediaGroups.store()` instead:
97
+ * ```typescript
98
+ * const mg = mediaGroups(undefined, { autoStore: false });
99
+ * bot.use(mg);
100
+ *
101
+ * bot.on("message", async (ctx) => {
102
+ * if (ctx.msg.media_group_id) {
103
+ * await ctx.mediaGroups.store(ctx.msg);
104
+ * }
105
+ * });
106
+ * ```
107
+ *
108
+ * @example Deleting a media group from storage:
109
+ * ```typescript
110
+ * // From within middleware
111
+ * await ctx.mediaGroups.delete("some-media-group-id");
112
+ *
113
+ * // From outside middleware
114
+ * await mg.deleteMediaGroup("some-media-group-id");
115
+ * ```
116
+ */
117
+ export function mediaGroups(adapter = new MemorySessionStorage(), options = {}) {
118
+ const { autoStore = true } = options;
119
+ const composer = new Composer();
120
+ const getMediaGroup = async (mediaGroupId) => {
121
+ return await adapter.read(mediaGroupId);
122
+ };
123
+ const store = (message) => storeMessages(adapter, [message]);
124
+ const deleteMediaGroup = (mediaGroupId) => Promise.resolve(adapter.delete(mediaGroupId));
125
+ // Hydrate context and store incoming messages
126
+ composer.use(async (ctx, next) => {
127
+ // Resolve a media_group_id from a nested message
128
+ const getGroupFor = (nested) => {
129
+ const id = nested?.media_group_id;
130
+ return id ? getMediaGroup(id) : Promise.resolve(undefined);
131
+ };
132
+ ctx.mediaGroups = {
133
+ getForMsg: () => getGroupFor(ctx.msg),
134
+ getForReply: () => getGroupFor(ctx.msg?.reply_to_message),
135
+ getForPinned: () => getGroupFor(ctx.msg?.pinned_message),
136
+ store,
137
+ delete: deleteMediaGroup,
138
+ };
139
+ if (autoStore) {
140
+ const msg = ctx.msg;
141
+ const replyMsg = msg?.reply_to_message;
142
+ const pinnedMsg = msg?.pinned_message;
143
+ // Collect messages to store in batch
144
+ const toStore = [];
145
+ if (msg?.media_group_id)
146
+ toStore.push(msg);
147
+ if (replyMsg?.media_group_id)
148
+ toStore.push(replyMsg);
149
+ if (pinnedMsg?.media_group_id)
150
+ toStore.push(pinnedMsg);
151
+ if (toStore.length > 0) {
152
+ await storeMessages(adapter, toStore);
153
+ }
154
+ }
155
+ return next();
156
+ });
157
+ // Attach standalone helpers
158
+ const result = composer;
159
+ result.adapter = adapter;
160
+ result.transformer = mediaGroupTransformer(adapter);
161
+ result.getMediaGroup = getMediaGroup;
162
+ result.deleteMediaGroup = deleteMediaGroup;
163
+ return result;
164
+ }
165
+ export default mediaGroups;
@@ -0,0 +1,14 @@
1
+ import type { Message, StorageAdapter } from "./deps.node.js";
2
+ /**
3
+ * Static mapping of API methods to their result extraction logic.
4
+ * Keys are method names whose responses may contain messages with `media_group_id`.
5
+ * Values are functions that extract `Message[]` from the raw API result.
6
+ */
7
+ export declare const MEDIA_GROUP_METHODS: Record<string, (result: any) => Message[]>;
8
+ /**
9
+ * Stores messages in batch, grouped by `media_group_id`.
10
+ * Performs one read and one write per group instead of per message.
11
+ * Messages without `media_group_id` are skipped.
12
+ * Existing entries with the same `(message_id, chat.id)` are replaced in-place.
13
+ */
14
+ export declare function storeMessages(adapter: StorageAdapter<Message[]>, messages: Message[]): Promise<void>;
package/out/storage.js ADDED
@@ -0,0 +1,38 @@
1
+ /** Wraps a single Message in an array. */
2
+ const toArray = (result) => [result];
3
+ /** Returns a Message in an array if it's an object, or empty if `true` (inline edit). */
4
+ // deno-lint-ignore no-explicit-any
5
+ const toArrayIfObject = (result) => result !== null && typeof result === "object" ? [result] : [];
6
+ /**
7
+ * Static mapping of API methods to their result extraction logic.
8
+ * Keys are method names whose responses may contain messages with `media_group_id`.
9
+ * Values are functions that extract `Message[]` from the raw API result.
10
+ */
11
+ // deno-lint-ignore no-explicit-any
12
+ export const MEDIA_GROUP_METHODS = {
13
+ sendMediaGroup: (result) => (Array.isArray(result) ? result : []),
14
+ forwardMessage: toArray,
15
+ editMessageMedia: toArrayIfObject,
16
+ editMessageCaption: toArrayIfObject,
17
+ editMessageReplyMarkup: toArrayIfObject,
18
+ };
19
+ /**
20
+ * Stores messages in batch, grouped by `media_group_id`.
21
+ * Performs one read and one write per group instead of per message.
22
+ * Messages without `media_group_id` are skipped.
23
+ * Existing entries with the same `(message_id, chat.id)` are replaced in-place.
24
+ */
25
+ export async function storeMessages(adapter, messages) {
26
+ const groups = {};
27
+ for (const message of messages) {
28
+ const { media_group_id } = message;
29
+ if (!media_group_id)
30
+ continue;
31
+ const group = (groups[media_group_id] ??= (await adapter.read(media_group_id)) ??
32
+ []);
33
+ const index = group.findIndex((m) => m.message_id === message.message_id &&
34
+ m.chat.id === message.chat.id);
35
+ group[index >= 0 ? index : group.length] = message;
36
+ }
37
+ await Promise.all(Object.entries(groups).map(([key, value]) => adapter.write(key, value)));
38
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "grammy-media-groups",
3
+ "version": "0.0.0",
4
+ "description": "Media Groups Storage Plugin for grammY",
5
+ "homepage": "https://github.com/PonomareVlad/grammy-media-groups",
6
+ "main": "./out/mod.js",
7
+ "types": "./out/mod.d.ts",
8
+ "type": "module",
9
+ "scripts": {
10
+ "prepare": "npm run build",
11
+ "build": "deno2node tsconfig.json"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/PonomareVlad/grammy-media-groups.git"
16
+ },
17
+ "exports": {
18
+ ".": {
19
+ "types": "./out/mod.d.ts",
20
+ "default": "./out/mod.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "out/"
25
+ ],
26
+ "author": "PonomareVlad",
27
+ "license": "MIT",
28
+ "bugs": {
29
+ "url": "https://github.com/PonomareVlad/grammy-media-groups/issues"
30
+ },
31
+ "peerDependencies": {
32
+ "grammy": "^1.15.2"
33
+ },
34
+ "devDependencies": {
35
+ "deno2node": "^1.3.0"
36
+ },
37
+ "keywords": [
38
+ "grammY",
39
+ "Telegram bot framework",
40
+ "plugin",
41
+ "media group",
42
+ "album"
43
+ ]
44
+ }