ربات تلگرام خودتان در NodeJS با TypeScript، Telegraf و Fastify (قسمت 2)

در مقاله قبلی من، پروژه را پیکربندی کردیم و یک ربات ساده و قابل اجرا نوشتیم. ما می خواهیم به آن آموزش دهیم که به پیام های کاربران واکنش نشان دهد و حافظه ای برای تعامل با پیش نویس ها داشته باشد.
انواع
زمان آن فرا رسیده است که برخی از نهادها را که نماینده برنامه ربات ما هستند تعریف کنیم. آن را در زیر دایرکتوری منبع قرار داده و در آن قرار دهید types.ts
فایل
// src/types.ts
import { Context as TelegrafContext } from 'telegraf';
import type { Message as TGMessage } from 'telegraf/types';
export type Message = {
text?: TGMessage.TextMessage;
photo?: TGMessage.PhotoMessage;
video?: TGMessage.VideoMessage;
};
export type Session = {
messages: Message[];
mediaGroupIds: string[];
};
export type Context = TelegrafContext & {
session: Session;
};
ما داریم Message
نهادی که هر پیام کاربر را قبل از انتشار نشان می دهد. این می تواند یک پیام متنی، چند عکس یا فیلم باشد.
این Session
موجودیت یک حافظه ربات را برای هر اتاق گفتگو نشان می دهد، که به ما کمک می کند تا پیش نویس پیام های دریافتی را برای انتشار آینده جمع آوری کنیم.
Context
موجودیت عمومی گسترش می یابد Telegraf
متن و یک شی جلسه اضافه می کند.
حافظه – جلسه تلگراف
این Telegraf
بسته یک جلسه برای حفظ اطلاعات بین پیام ها در چت کاربر فراهم می کند. ما از آن برای افزودن حافظه برای ربات خود استفاده می کنیم و پیام ها را با عکس ها و ویدیوها تا زمانی که منتشر شوند نگه می داریم.
بیایید یک فروشگاه جلسه ایجاد کنیم که با حافظه ربات ارتباط برقرار کند. در حال حاضر، این یک مجموعه شی ساده با شناسه چت به عنوان یک کلید خواهد بود که آن را در فایل اصلی خود قرار می دهیم. src/index.ts
.
ابتدا، واردات را به روز کنید telegraf
بسته با جدید session
تابع میان افزار و SessionStore
نوع
// src/index.ts
import { session, Telegraf, type SessionStore } from 'telegraf';
سپس، یک را ایجاد کنید storage
نقشه ای که پیام ها را نگه می دارد و الف store
شی که اجرا می کند SessionStore
با تعریف قبلی ما تایپ کنید Session
به عنوان یک پارامتر عمومی:
// src/index.ts
import { Session } from './types';
const storage = new Map<string, string>();
const store: SessionStore<Session> = {
get(key) {
const value = storage.get(key);
return value ? JSON.parse(value) : null;
},
set(key, value) {
storage.set(key, JSON.stringify(value));
},
delete(key) {
storage.delete(key);
},
};
این یک فروشگاه همزمان با سه روش است: دریافت کنید، مجموعه، و حذف کنید. شما می توانید از هر چیزی که نیاز دارید استفاده کنید، اما ما آن را برای این قسمت در حافظه کامپیوتر نگه می داریم.
برای بخش آخر، ما به یک تابع کمکی نیاز داریم تا وضعیت پیشفرض جلسه را ایجاد کنیم – زمانی که ربات خود را مقداردهی اولیه میکنیم و زمانی که نیاز به تازه کردن دادهها داریم. یک دایرکتوری جدید در داخل پوشه منبع به نام ایجاد کنید helpers
و اضافه کنید getDefaultSession.ts
فایل:
// src/helpers/getDefaultSession.ts
import type { Session } from '../types';
export const getDefaultSession = (): Session => {
return {
mediaGroupIds: [],
messages: [],
};
};
ما توابع کمکی بیشتری را در آینده اضافه خواهیم کرد، بنابراین افزودن یک تمرین خوب خواهد بود index.ts
فایل و هر آنچه را که نیاز دارید را دوباره صادر کنید helpers
دایرکتوری:
// src/helpers/index.ts
export * from './getDefaultSession';
بازگشت به src/index.ts
و وارد کنید getDefaultSession
تابع
// src/index.ts
import { getDefaultSession } from './helpers';
از فروشگاه تازه ایجاد شده با session
میان افزار هر میان افزار را می توان با یک اضافه کرد use
روش در Telegraf
نمونه پس از نمونه سازی آن را اضافه کنید bot
.
// src/index.ts
const bot = new Telegraf(' ');
bot.use(
session({
defaultSession: getDefaultSession,
store,
})
);
دستورات
بیایید دستوراتی را برای ربات خود تعریف کنیم. هر دستور نشان دهنده یک عملکرد واحد از ربات است و در یک فهرست جداگانه قرار دارد – /commands
. آنها توابع ناهمزمان با زمینه به عنوان یک آرگومان هستند. ابتدا یک پوشه ایجاد کنید:
mkdir commands
لغو فرمان
دستور لغو پیشنویس پیام یا پست آینده کاربر را حذف میکند و به او اجازه میدهد جلسه فعلی را پاک کند.
// src/commands/cancel.ts
import { getDefaultSession } from '../helpers';
import type { Context } from '../types';
const cancel = async (ctx: Context) => {
await ctx.reply('Draft has been deleted. You can start over.');
ctx.session = getDefaultSession();
};
export default cancel;
ما استفاده می کنیم getDefaultSession
تابع برای تنظیم جلسه جاری به حالت خالی. همچنین باید با پیام موفقیت آمیز به کاربر پاسخ دهیم.
سلام فرمان
ما قبلاً یک فرمان برای کاربران ورودی به عنوان پیام خوش آمدگویی داریم، اما اجازه دهید آن را برای یکپارچگی به یک فایل جداگانه منتقل کنیم.
// src/commands/hi.ts
import { fmt } from 'telegraf/format';
import { Context } from '../types';
const hi = async (ctx: Context) => {
await ctx.reply(fmt`
Hello! Welcome to Publish Bot. I can help you publish your content to multiple channels at once.
1. Write a message or send a photo/video.
2. Check the preview of the message.
3. If everything looks good, publish it to the channels.
`);
};
export default hi;
بهتر به نظر می رسد و دستورالعمل های دقیقی در مورد استفاده از ربات برای انتشار پیام ها دارد. یکی از بهترین روش ها برای قالب بندی پیام ها استفاده از یک است fmt
تابع قالب بندی این تابع کمکهای زیادی دارد، اما در حال حاضر فقط باید یک پیام چند خطی بنویسیم.
دستور پیش نمایش
یک فرمان پیش نمایش به کاربران اجازه می دهد تا پیام های خود را قبل از انتشار بررسی کنند. برخلاف برنامه های کاربردی وب یا تلفن همراه، این عملکرد ضروری است زیرا نمی توان آن را در زمان واقعی مشاهده کرد.
بیایید با یک تابع کمکی دیگر به نام شروع کنیم groupMessages
. این عملکرد همه عکسها، ویدیوها و پیامهای متنی را در گروههای جداگانه ترکیب میکند تا به ما در تهیه یک پیام پیشنمایش در تلگرام کمک کند.
// src/helpers/groupMessages.ts
import type { Message as TGMessage } from 'telegraf/types';
import type { Message } from '../types';
export const groupMessages = (messages: Message[]) => {
const photos: TGMessage.PhotoMessage[] = [];
const videos: TGMessage.VideoMessage[] = [];
const text: TGMessage.TextMessage[] = [];
for (const message of messages) {
if (message.photo) {
photos.push(message.photo);
}
if (message.video) {
videos.push(message.video);
}
if (message.text) {
text.push(message.text);
}
}
return {
photos,
videos,
text,
};
};
تابع کمکی بعدی a است canPublish
. نیازی نیست به کاربران اجازه انتشار پیام های کوتاه یا خالی را بدهیم. فرض کنید برای انتشار به حداقل 10 کاراکتر نیاز داریم.
// src/helpers/canPublish.ts
import type { Context } from '../types';
const MINIMUM_TEXT_LENGTH = 10;
export const canPublish = (ctx: Context) => {
const { messages } = ctx.session;
const hasMedia = messages.some(message => message.photo || message.video);
const hasText = messages.some(message => message.text?.text.length ?? 0 > MINIMUM_TEXT_LENGTH);
return hasMedia || hasText;
};
همیشه داشتن یک ثابت برای شرایط یا مقادیر پیکربندی خوب است. ما می توانیم آنها را به یک فایل پیکربندی جداگانه منتقل کنیم یا در آینده به راحتی آنها را به روز کنیم.
قبل از رفتن به خود دستور، به آخرین تابع کمکی نیاز داریم. تلگرام به ما این امکان را می دهد که برای هر پیام دکمه های درون خطی ارسال کنیم تا کاربران بتوانند با اپلیکیشن شما تعامل داشته باشند.
// src/helpers/getInlineKeyboard.ts
import { Markup } from 'telegraf';
import { canPublish } from './canPublish';
import type { Context } from '../types';
type Options = {
hidePreview?: boolean;
};
const PreviewButton = Markup.button.callback('Preview', 'preview');
const PublishButton = Markup.button.callback('Publish', 'publish');
const CancelButton = Markup.button.callback('Cancel', 'cancel');
export const getInlineKeyboard = (ctx: Context, options: Options = {}) => {
const canPublishNow = canPublish(ctx);
const { hidePreview } = options;
if (hidePreview) {
return Markup.inlineKeyboard([
canPublishNow ? [PublishButton] : [],
[CancelButton],
]);
}
return Markup.inlineKeyboard([
[PreviewButton, CancelButton],
canPublishNow ? [PublishButton] : [],
]);
};
ما دکمهها را بهعنوان دکمههای تماس از پیش تعریف میکنیم و آنها را به متغیرهای جداگانه برای استفاده مجدد اختصاص میدهیم. آرگومان اول عنوان یک دکمه و آرگومان دوم نام callback است که در زیر تعریف خواهد شد. این تابع یک گزینه برای مخفی کردن دکمه پیش نمایش در صورت عدم نیاز به آن در برخی سناریوها دارد.
فراموش نکنید که فایل فهرست را در پوشه helpers به روز کنید:
// src/helpers/index.ts
export * from './getDefaultSession';
export * from './getInlineKeyboard';
export * from './groupMessages';
ما همه چیزهایی را داریم که برای اجرای دستور پیش نمایش نیاز داریم.
// src/commands/preview.ts
import type { InputMediaPhoto, InputMediaVideo } from 'telegraf/types';
import { FmtString, join } from 'telegraf/format';
import { groupMessages, getInlineKeyboard } from '../helpers';
import type { Context } from '../types';
const preview = async (ctx: Context) => {
if (ctx.session.messages.length > 0) {
const { photos, videos, text } = groupMessages(ctx.session.messages);
const fullText = join(
text.map(t => new FmtString(t.text, t.entities)),
'\n'
);
if (photos.length || videos.length) {
const media = [
...videos.map<InputMediaVideo>(video => ({
type: 'video',
media: video.video.file_id,
...video,
})),
...photos.map<InputMediaPhoto>(photo => ({
type: 'photo',
media: photo.photo[photo.photo.length - 1].file_id,
...photo,
})),
];
if (fullText.text) {
media[media.length - 1].caption = fullText.text;
media[media.length - 1].caption_entities = fullText.entities;
}
return ctx.replyWithMediaGroup(media);
}
return ctx.reply(fullText, getInlineKeyboard(ctx, { hidePreview: true }));
}
return ctx.reply('No messages to preview.');
};
export default preview;
بیایید کد را مرور کنیم:
- اگر هیچ پیامی در جلسه نداریم، به کاربر گزارش دهید که هیچ پیامی برای پیش نمایش وجود ندارد.
- ما همه پیامها را بر اساس نوع گروهبندی میکنیم تا اخیراً کد را برای پاسخ با پیوستها یا با پیام متنی تقسیم کنیم.
- واردات
FmtString
کلاس وjoin
عملکرد ترکیب همه پیامهای متنی و زیرنویسها در یک پیام واحد در حالی که تمام قالببندیها را حفظ میکند. - اگر عکس یا ویدیو داریم، باید با گروه رسانه پاسخ دهیم و آرایه ای از رسانه ها بسازیم. اگر پیامهای متنی وجود دارد، آنها را به عنوان گروه رسانه با موجودیتها اضافه کنید (قالببندی رشته را توضیح دهید).
- با پیام های متنی ترکیبی پاسخ دهید و یک صفحه کلید درون خطی برای اقدامات بیشتر اضافه کنید. در نظر بگیرید که یک وجود دارد
hidePreview
گزینه زیرا ما قبلا به پیش نمایش پیش نویس نگاه کرده بودیم. ### دستور انتشار
فرمان مرکزی ربات ما. در حال حاضر، ما هیچ قابلیتی برای دستکاری پیش نویس اضافه نمی کنیم. این به شما بستگی دارد که می خواهید با آن چه کار کنید. در قسمت بعدی یکی از انواع آن را خواهیم دید.
// src/commands/publish.ts
import { getDefaultSession, groupMessages } from '../helpers';
import type { Context } from '../types';
const publish = async (ctx: Context) => {
if (ctx.session.messages.length > 0) {
const { photos, videos, text } = groupMessages(ctx.session.messages);
const draft = {
author: ctx.from,
photos,
videos,
text,
};
try {
// Do something with the draft
draft;
} catch (e) {
console.error(e);
return ctx.reply('There was an error while trying to publish your messages.');
}
ctx.session = getDefaultSession();
return ctx.reply('Your messages has been published.');
}
return ctx.reply('No messages to publish.');
};
export default publish;
در داخل دستور، آبجکت پیش نویس خود را آماده می کنیم که حاوی پیام ها و اطلاعات مربوط به نویسنده از متن است. در try-catch
بخش، ما باید کاری را با پیش نویس آماده شده خود انجام دهیم – آن را در یک پایگاه داده ذخیره کنیم یا آن را به صف پیام ارسال کنیم. هر کاری میخوای باهاش بکن و البته فراموش نکنید که داده های جلسه را با تماس گرفتن پاک کنید getDefaultSession
تابع و اعمال آن بر روی شی جلسه.
لمس های نهایی
یک فایل فهرست برای دستورات ایجاد کنید تا آنها را از یک مکان وارد کنید.
// src/commands/index.ts
export { default as cancel } from './cancel';
export { default as hi } from './hi';
export { default as preview } from './preview';
export { default as publish } from './publish';
هر فرمان میتواند چندین متن یا تماسهای مختلف داشته باشد که کاربران میتوانند به ربات ارسال کنند. من توصیه می کنم از عبارات منظم برای رسیدگی به همه آنها استفاده کنید. همچنین، شما باید یک را وارد کنید message
از فیلترهای Telegraf فیلتر کنید تا پیام های دریافتی را بر اساس نوع آنها مدیریت کنید.
// src/index.ts
import { message } from 'telegraf/filters';
زمان آن رسیده است که فایل اصلی برنامه را تغییر دهید تا تمام عملکردهای نوشته شده قبلی اعمال شود.
// src/index.ts
const hiRegExp = /^(hi|hello)$/i;
const previewRegExp = /^(preview|status)$/i;
const publishRegExp = /^(publish|send)$/i;
const cancelRegExp = /^(cancel|clear|delete)$/i;
const bot = new Telegraf<Context>(' ');
bot.use(
session({
defaultSession: getDefaultSession,
store,
})
);
bot.start(hi);
bot.hears(hiRegExp, hi);
bot.command(hiRegExp, hi);
bot.action(hiRegExp, hi);
bot.hears(previewRegExp, preview);
bot.command(previewRegExp, preview);
bot.action(previewRegExp, async ctx => {
await ctx.answerCbQuery();
await preview(ctx);
});
bot.hears(cancelRegExp, cancel);
bot.command(cancelRegExp, cancel);
bot.action(cancelRegExp, async ctx => {
await ctx.answerCbQuery();
await cancel(ctx);
});
bot.hears(publishRegExp, publish);
bot.command(publishRegExp, publish);
bot.action(publishRegExp, async ctx => {
await ctx.answerCbQuery();
await publish(ctx);
});
bot.on(message('text'), async ctx => {
ctx.session.messages.push({ text: ctx.message });
});
bot.on(message('photo'), async ctx => {
ctx.session.messages.push({ photo: ctx.message });
});
bot.on(message('video'), async ctx => {
ctx.session.messages.push({ video: ctx.message });
});
لطفاً توجه داشته باشید که ما با افزودن ربات نمونه سازی را به روز کرده ایم Context
نوع تعریف شده قبلی
می توانید کد خود را دوباره بسازید و آن را در تلفن همراه یا نسخه وب تلگرام امتحان کنید. در اینجا نمونه ای از چت کردن با ربات من است:
در قسمت آخر مجموعه، آن را آماده تولید و اضافه می کنیم Fastify
و برخی پلاگین ها برای حفظ اطلاعات بین راه اندازی مجدد و مدیریت پیکربندی.
عکس از mariyan rajesh در Unsplash