برنامه نویسی

ربات تلگرام خودتان در 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

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا