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 +21 -0
- package/README.md +130 -0
- package/out/deps.node.d.ts +2 -0
- package/out/deps.node.js +1 -0
- package/out/mod.d.ts +180 -0
- package/out/mod.js +165 -0
- package/out/storage.d.ts +14 -0
- package/out/storage.js +38 -0
- package/package.json +44 -0
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
|
+
```
|
package/out/deps.node.js
ADDED
|
@@ -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;
|
package/out/storage.d.ts
ADDED
|
@@ -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
|
+
}
|