Приветствую всех в своем уроке по написанию Telegram бота на языке программирования C#. В этом уроке я постараюсь максимально просто и понятно объяснить как написать своего бота с нуля. Конечно, здесь не будет какого-то трудного проекта, я дам вам необходимую базу, с помощью которой вы сможете писать своих ботов.
Начало
Для начала нам требуется зарегистрировать нашего бота. Делается это через "Ботобатю" (кликабельно) в самом Telegram.
Переходим по ссылке и попадаем в диалог с BotFather и жмем "Запустить". BotFather - это официальный бот Telegram`а, через которого проходит регистрация и настройка ваших ботов.
От вашего лица должна отправиться команда /start , на которую бот ответит большим списком команд с описаниями. Подробно изучить каждую Вы можете сами, а мы двигаемся к шагу 3.
Используем команду /newbot. На этом шаге нужно отправить боту имя Вашего будущего бота. Например, пусть это будет Александр Николаевич.
Далее нас попросят ввести username бота, учтите, что он должен быть написан строго на латинице, а также содержать bot или _bot в конце. Username - это уникальный идентификатор аккаунта (вашего или бота), по которому люди могут найти аккаунт в поиске. В случае, если username уже занят, вас попросят использовать другой.
После всех выполнений действий, Ботобатя присылает нам небольшое сообщение, в котором говорится об успешном создании бота. Из этого сообщения нам нужен token (уникальный ключ), с помощью которого мы будем авторизировать нашего бота в дальнейшем, он имеет примерно такой вид: 13523457:AFAgd_SDFG6sd7f6asdf67asdf78. Учтите, что абсолютно никому нельзя присылать этот token! Это ваш секретный ключ для доступа к боту.
Создание проекта в IDE
После того, как мы создали бота, перейдем к созданию проекта в вашей IDE. У меня это Rider от компании JetBrains. Вы можете использовать эту же IDE, либо Visual Studio от компании Microsoft, либо все от той же компании Visual Studio Code.
По сути, вы можете создать любой тип проекта, будь то консольное приложение или же какой-нибудь WinForms. Я всегда создаю консольное приложение, так как в будущем делаю деплой на Linux, да и как-то не вижу смысла в создании бота с программным интерфейсом.
Если же вы до сих пор не знаете, что такое IDE, как создавать проекты, то вам явно рано писать ботов, займитесь для начала изучением языка!
После того, как мы создали проект, нам нужно установить библиотеку Telegram.Bot (GitHub библиотеки, Nuget пакет). Сделать это можно либо через терминал в IDE, написав команду
dotnet add Telegram.Bot
либо же использовать графический интерфейс. На момент написания статьи была установлена самая последняя и самая актуальная версия пакета (19.0.0).
У библиотеки есть своя документация, можете посмотреть ее здесь.
Написание бота
Теперь приступим к написанию бота, для начала напишем стандартный класс Program. Добавим туда объект интерфейса ITelegramBotClient и в методе Main создадим стандартные переменные и присвоим им соответствующие значения.
class Program
{
// Это клиент для работы с Telegram Bot API, который позволяет отправлять сообщения, управлять ботом, подписываться на обновления и многое другое.
private static ITelegramBotClient _botClient;
// Это объект с настройками работы бота. Здесь мы будем указывать, какие типы Update мы будем получать, Timeout бота и так далее.
private static ReceiverOptions _receiverOptions;
static async Task Main()
{
_botClient = new TelegramBotClient("<token>"); // Присваиваем нашей переменной значение, в параметре передаем Token, полученный от BotFather
_receiverOptions = new ReceiverOptions // Также присваем значение настройкам бота
{
AllowedUpdates = new[] // Тут указываем типы получаемых Update`ов, о них подробнее расказано тут https://core.telegram.org/bots/api#update
{
UpdateType.Message, // Сообщения (текст, фото/видео, голосовые/видео сообщения и т.д.)
},
// Параметр, отвечающий за обработку сообщений, пришедших за то время, когда ваш бот был оффлайн
// True - не обрабатывать, False (стоит по умолчанию) - обрабаывать
ThrowPendingUpdates = true,
};
using var cts = new CancellationTokenSource();
// UpdateHander - обработчик приходящих Update`ов
// ErrorHandler - обработчик ошибок, связанных с Bot API
_botClient.StartReceiving(UpdateHandler, ErrorHandler, _receiverOptions, cts.Token); // Запускаем бота
var me = await _botClient.GetMeAsync(); // Создаем переменную, в которую помещаем информацию о нашем боте.
Console.WriteLine($"{me.FirstName} запущен!");
await Task.Delay(-1); // Устанавливаем бесконечную задержку, чтобы наш бот работал постоянно
}
}
Теперь давайте в этом же классе (можно и в другом) напишем методы UpdateHandler и ErrorHandler.
private static async Task UpdateHandler(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
{
// Обязательно ставим блок try-catch, чтобы наш бот не "падал" в случае каких-либо ошибок
try
{
// Сразу же ставим конструкцию switch, чтобы обрабатывать приходящие Update
switch (update.Type)
{
case UpdateType.Message:
{
Console.WriteLine("Пришло сообщение!");
return;
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
private static Task ErrorHandler(ITelegramBotClient botClient, Exception error, CancellationToken cancellationToken)
{
// Тут создадим переменную, в которую поместим код ошибки и её сообщение
var ErrorMessage = error switch
{
ApiRequestException apiRequestException
=> $"Telegram API Error:\n[{apiRequestException.ErrorCode}]\n{apiRequestException.Message}",
_ => error.ToString()
};
Console.WriteLine(ErrorMessage);
return Task.CompletedTask;
}
Теперь давайте посмотрим, работает ли наш код ? Жмем на кнопку Debug и проверяем :)
Как мы видим, бот успешно запустился, теперь давайте напишем пару сообщений, чтобы понять, получает ли он сообщения
И тут тоже все прекрасно, теперь давайте дополним наш метод UpdateHandler и напишем эхо бота. Также чуть-чуть попозже расскажу немного о типах Update.
private static async Task UpdateHandler(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
{
// Обязательно ставим блок try-catch, чтобы наш бот не "падал" в случае каких-либо ошибок
try
{
// Сразу же ставим конструкцию switch, чтобы обрабатывать приходящие Update
switch (update.Type)
{
case UpdateType.Message:
{
// эта переменная будет содержать в себе все связанное с сообщениями
var message = update.Message;
// From - это от кого пришло сообщение (или любой другой Update)
var user = message.From;
// Выводим на экран то, что пишут нашему боту, а также небольшую информацию об отправителе
Console.WriteLine($"{user.FirstName} ({user.Id}) написал сообщение: {message.Text}");
// Chat - содержит всю информацию о чате
var chat = message.Chat;
await botClient.SendTextMessageAsync(
chat.Id,
message.Text, // отправляем то, что написал пользователь
replyToMessageId: message.MessageId // по желанию можем поставить этот параметр, отвечающий за "ответ" на сообщение
);
return;
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
Теперь проверяем.
Как мы видим, все работает идеально. Теперь расскажу о типах Update, так как дальше мы напишем кое-что посложнее. Итак, типы Update:
Message - принимает в себя все сообщения. Обычные текстовые, фото или видео, аудио или видео сообщения (кружочки), стикеры, контакты, геопозицию, голосование и так далее. Всё, что мы отправляем в чат - это все Message.
EditedMessage - тут все просто: этот тип принимает в себя любое обновление сообщения. Схож с Message.
ChannelPost - как и Message, но направлен на каналы.
EditedChannelPost - аналогичен EditedMessage, но также направлен на каналы.
CallbackQuery - отвечает за Inline кнопки, они висят под сообщением, возможно, вы их уже видели в других ботах.
Poll - получает все связанное с голосованием.
PollAnswner - а этот тип работает только тогда, когда пользователь изменил свой ответ в голосовании.
ChatMember - всё, что касается людей в чате/канале: зашел, вышел, повысили, понизили, замьютили и т.д.
MyChatMember - всё, что касается бота в диалоге между пользователем и ботом, т.е. изменения в личных сообщениях.
ChatJoinRequest - получение информации о поданной заявки на вступление в чат/канал.
InlineQuery - получение входящих inline запросов. Inline запрос - это, когда вы в чате используете @ и username бота и вводите какой-то запрос, а результат выполнения отправляется в чат от вашего лица с надписью "сделано с помощью....".
ChosenInlineResult - а это уже то, что как раз таки выбрал пользователь. Т.е. InlineQuery это просто как разрешение использовать эту функцию, а ChosenInlineResult получает выбор пользователя и обрабатывает его. Знаю, что вы думаете "Они что, совсем идиоты ? Не могли сделать нормально ?", но привыкайте, такого будет полно)
PreCheckoutQuery - сюда приходит информация о платеже, который начал оплачивать пользователь.
ShippingQuery - а это срабатывает тогда, когда успешно сработал PreCheckoutQuery , т.е. этот update уже подтверждает успешную оплату пользователем.
Фух, ну вроде все, если желаете посмотреть оригинал, то он находится здесь.
Теперь давайте напишем что-нибудь посложнее. Добавим в AllowedUpdates тип CallbackQuery:
AllowedUpdates = new[] // Тут указываем типы получаемых Update`ов, о них подробнее расказано тут https://core.telegram.org/bots/api#update
{
UpdateType.Message, // Сообщения (текст, фото/видео, голосовые/видео сообщения и т.д.)
UpdateType.CallbackQuery // Inline кнопки
},
Теперь в нашем UpdateHandler добавим обработку команды /start и сделаем там несколько клавиатур, чтобы вы поняли, как работать с разными типами update, а также увидели еще одну клавиатуру, которая называется Reply клавиатура.
private static async Task UpdateHandler(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
{
// Обязательно ставим блок try-catch, чтобы наш бот не "падал" в случае каких-либо ошибок
try
{
// Сразу же ставим конструкцию switch, чтобы обрабатывать приходящие Update
switch (update.Type)
{
case UpdateType.Message:
{
// эта переменная будет содержать в себе все связанное с сообщениями
var message = update.Message;
// From - это от кого пришло сообщение
var user = message.From;
// Выводим на экран то, что пишут нашему боту, а также небольшую информацию об отправителе
Console.WriteLine($"{user.FirstName} ({user.Id}) написал сообщение: {message.Text}");
// Chat - содержит всю информацию о чате
var chat = message.Chat;
// Добавляем проверку на тип Message
switch (message.Type)
{
// Тут понятно, текстовый тип
case MessageType.Text:
{
// тут обрабатываем команду /start, остальные аналогично
if (message.Text == "/start")
{
await botClient.SendTextMessageAsync(
chat.Id,
"Выбери клавиатуру:\n" +
"/inline\n" +
"/reply\n");
return;
}
if (message.Text == "/inline")
{
// Тут создаем нашу клавиатуру
var inlineKeyboard = new InlineKeyboardMarkup(
new List<InlineKeyboardButton[]>() // здесь создаем лист (массив), который содрежит в себе массив из класса кнопок
{
// Каждый новый массив - это дополнительные строки,
// а каждая дополнительная строка (кнопка) в массиве - это добавление ряда
new InlineKeyboardButton[] // тут создаем массив кнопок
{
InlineKeyboardButton.WithUrl("Это кнопка с сайтом", "https://habr.com/"),
InlineKeyboardButton.WithCallbackData("А это просто кнопка", "button1"),
},
new InlineKeyboardButton[]
{
InlineKeyboardButton.WithCallbackData("Тут еще одна", "button2"),
InlineKeyboardButton.WithCallbackData("И здесь", "button3"),
},
});
await botClient.SendTextMessageAsync(
chat.Id,
"Это inline клавиатура!",
replyMarkup: inlineKeyboard); // Все клавиатуры передаются в параметр replyMarkup
return;
}
if (message.Text == "/reply")
{
// Тут все аналогично Inline клавиатуре, только меняются классы
// НО! Тут потребуется дополнительно указать один параметр, чтобы
// клавиатура выглядела нормально, а не как абы что
var replyKeyboard = new ReplyKeyboardMarkup(
new List<KeyboardButton[]>()
{
new KeyboardButton[]
{
new KeyboardButton("Привет!"),
new KeyboardButton("Пока!"),
},
new KeyboardButton[]
{
new KeyboardButton("Позвони мне!")
},
new KeyboardButton[]
{
new KeyboardButton("Напиши моему соседу!")
}
})
{
// автоматическое изменение размера клавиатуры, если не стоит true,
// тогда клавиатура растягивается чуть ли не до луны,
// проверить можете сами
ResizeKeyboard = true,
};
await botClient.SendTextMessageAsync(
chat.Id,
"Это reply клавиатура!",
replyMarkup: replyKeyboard); // опять передаем клавиатуру в параметр replyMarkup
return;
}
return;
}
// Добавил default , чтобы показать вам разницу типов Message
default:
{
await botClient.SendTextMessageAsync(
chat.Id,
"Используй только текст!");
return;
}
}
return;
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
Как мы видим, бот теперь реагирует на /start:
Вот так выглядит inline клавиатура:
А вот так reply клавиатура:
Причем, если вы понажимаете на кнопки, то как вы уже поняли, ничего не произойдет:)
Конечно, это логично, ведь мы не добавили обработчики этих кнопок. Как вы могли заметить, reply клавиатура - это просто как заготовленный текст для пользователя, поэтому с обработкой этих кнопок у вас не должно возникнуть проблем. Так как это просто дополнительные if в блоке case MessageType.Text. Но я все же покажу, как это сделать, после перейдем к кейсу с Inline клавиатурой.
private static async Task UpdateHandler(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
{
// Обязательно ставим блок try-catch, чтобы наш бот не "падал" в случае каких-либо ошибок
try
{
// Сразу же ставим конструкцию switch, чтобы обрабатывать приходящие Update
switch (update.Type)
{
case UpdateType.Message:
{
// тут все переменные
// Добавляем проверку на тип Message
switch (message.Type)
{
// Тут понятно, текстовый тип
case MessageType.Text:
{
// а тут обработчики команд
if (message.Text == "Позвони мне!")
{
await botClient.SendTextMessageAsync(
chat.Id,
"Хорошо, присылай номер!",
replyToMessageId: message.MessageId);
return;
}
if (message.Text == "Напиши моему соседу!")
{
await botClient.SendTextMessageAsync(
chat.Id,
"А самому что, трудно что-ли ?",
replyToMessageId: message.MessageId);
return;
}
return;
}
// тут остальной код
}
return;
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
Ну, вот так как-то вышло:
Теперь перейдем к блоку с Inline клавиатурами. Для обработки этой клавиатуры нам потребуется добавить в
switch(update.Type)
{
case UpdateType.Message:
{
// тут весь код из примеров выше
}
}
следующий код:
case UpdateType.CallbackQuery:
{
// Переменная, которая будет содержать в себе всю информацию о кнопке, которую нажали
var callbackQuery = update.CallbackQuery;
// Аналогично и с Message мы можем получить информацию о чате, о пользователе и т.д.
var user = callbackQuery.From;
// Выводим на экран нажатие кнопки
Console.WriteLine($"{user.FirstName} ({user.Id}) нажал на кнопку: {callbackQuery.Data}");
// Вот тут нужно уже быть немножко внимательным и не путаться!
// Мы пишем не callbackQuery.Chat , а callbackQuery.Message.Chat , так как
// кнопка привязана к сообщению, то мы берем информацию от сообщения.
var chat = callbackQuery.Message.Chat;
// Добавляем блок switch для проверки кнопок
switch (callbackQuery.Data)
{
// Data - это придуманный нами id кнопки, мы его указывали в параметре
// callbackData при создании кнопок. У меня это button1, button2 и button3
case "button1":
{
// В этом типе клавиатуры обязательно нужно использовать следующий метод
await botClient.AnswerCallbackQueryAsync(callbackQuery.Id);
// Для того, чтобы отправить телеграмму запрос, что мы нажали на кнопку
await botClient.SendTextMessageAsync(
chat.Id,
$"Вы нажали на {callbackQuery.Data}");
return;
}
case "button2":
{
// А здесь мы добавляем наш сообственный текст, который заменит слово "загрузка", когда мы нажмем на кнопку
await botClient.AnswerCallbackQueryAsync(callbackQuery.Id, "Тут может быть ваш текст!");
await botClient.SendTextMessageAsync(
chat.Id,
$"Вы нажали на {callbackQuery.Data}");
return;
}
case "button3":
{
// А тут мы добавили еще showAlert, чтобы отобразить пользователю полноценное окно
await botClient.AnswerCallbackQueryAsync(callbackQuery.Id, "А это полноэкранный текст!", showAlert: true);
await botClient.SendTextMessageAsync(
chat.Id,
$"Вы нажали на {callbackQuery.Data}");
return;
}
}
return;
}
В конечном счете UpdateHandler должен выглядеть вот так:
private static async Task UpdateHandler(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
{
// Обязательно ставим блок try-catch, чтобы наш бот не "падал" в случае каких-либо ошибок
try
{
// Сразу же ставим конструкцию switch, чтобы обрабатывать приходящие Update
switch (update.Type)
{
case UpdateType.Message:
{
// Эта переменная будет содержать в себе все связанное с сообщениями
var message = update.Message;
// From - это от кого пришло сообщение (или любой другой Update)
var user = message.From;
// Выводим на экран то, что пишут нашему боту, а также небольшую информацию об отправителе
Console.WriteLine($"{user.FirstName} ({user.Id}) написал сообщение: {message.Text}");
// Chat - содержит всю информацию о чате
var chat = message.Chat;
// Добавляем проверку на тип Message
switch (message.Type)
{
// Тут понятно, текстовый тип
case MessageType.Text:
{
// тут обрабатываем команду /start, остальные аналогично
if (message.Text == "/start")
{
await botClient.SendTextMessageAsync(
chat.Id,
"Выбери клавиатуру:\n" +
"/inline\n" +
"/reply\n");
return;
}
if (message.Text == "/inline")
{
// Тут создаем нашу клавиатуру
var inlineKeyboard = new InlineKeyboardMarkup(
new List<InlineKeyboardButton[]>() // здесь создаем лист (массив), который содрежит в себе массив из класса кнопок
{
// Каждый новый массив - это дополнительные строки,
// а каждая дополнительная кнопка в массиве - это добавление ряда
new InlineKeyboardButton[] // тут создаем массив кнопок
{
InlineKeyboardButton.WithUrl("Это кнопка с сайтом", "https://habr.com/"),
InlineKeyboardButton.WithCallbackData("А это просто кнопка", "button1"),
},
new InlineKeyboardButton[]
{
InlineKeyboardButton.WithCallbackData("Тут еще одна", "button2"),
InlineKeyboardButton.WithCallbackData("И здесь", "button3"),
},
});
await botClient.SendTextMessageAsync(
chat.Id,
"Это inline клавиатура!",
replyMarkup: inlineKeyboard); // Все клавиатуры передаются в параметр replyMarkup
return;
}
if (message.Text == "/reply")
{
// Тут все аналогично Inline клавиатуре, только меняются классы
// НО! Тут потребуется дополнительно указать один параметр, чтобы
// клавиатура выглядела нормально, а не как абы что
var replyKeyboard = new ReplyKeyboardMarkup(
new List<KeyboardButton[]>()
{
new KeyboardButton[]
{
new KeyboardButton("Привет!"),
new KeyboardButton("Пока!"),
},
new KeyboardButton[]
{
new KeyboardButton("Позвони мне!")
},
new KeyboardButton[]
{
new KeyboardButton("Напиши моему соседу!")
}
})
{
// автоматическое изменение размера клавиатуры, если не стоит true,
// тогда клавиатура растягивается чуть ли не до луны,
// проверить можете сами
ResizeKeyboard = true,
};
await botClient.SendTextMessageAsync(
chat.Id,
"Это reply клавиатура!",
replyMarkup: replyKeyboard); // опять передаем клавиатуру в параметр replyMarkup
return;
}
if (message.Text == "Позвони мне!")
{
await botClient.SendTextMessageAsync(
chat.Id,
"Хорошо, присылай номер!",
replyToMessageId: message.MessageId);
return;
}
if (message.Text == "Напиши моему соседу!")
{
await botClient.SendTextMessageAsync(
chat.Id,
"А самому что, трудно что-ли ?",
replyToMessageId: message.MessageId);
return;
}
return;
}
// Добавил default , чтобы показать вам разницу типов Message
default:
{
await botClient.SendTextMessageAsync(
chat.Id,
"Используй только текст!");
return;
}
}
return;
}
case UpdateType.CallbackQuery:
{
// Переменная, которая будет содержать в себе всю информацию о кнопке, которую нажали
var callbackQuery = update.CallbackQuery;
// Аналогично и с Message мы можем получить информацию о чате, о пользователе и т.д.
var user = callbackQuery.From;
// Выводим на экран нажатие кнопки
Console.WriteLine($"{user.FirstName} ({user.Id}) нажал на кнопку: {callbackQuery.Data}");
// Вот тут нужно уже быть немножко внимательным и не путаться!
// Мы пишем не callbackQuery.Chat , а callbackQuery.Message.Chat , так как
// кнопка привязана к сообщению, то мы берем информацию от сообщения.
var chat = callbackQuery.Message.Chat;
// Добавляем блок switch для проверки кнопок
switch (callbackQuery.Data)
{
// Data - это придуманный нами id кнопки, мы его указывали в параметре
// callbackData при создании кнопок. У меня это button1, button2 и button3
case "button1":
{
// В этом типе клавиатуры обязательно нужно использовать следующий метод
await botClient.AnswerCallbackQueryAsync(callbackQuery.Id);
// Для того, чтобы отправить телеграмму запрос, что мы нажали на кнопку
await botClient.SendTextMessageAsync(
chat.Id,
$"Вы нажали на {callbackQuery.Data}");
return;
}
case "button2":
{
// А здесь мы добавляем наш сообственный текст, который заменит слово "загрузка", когда мы нажмем на кнопку
await botClient.AnswerCallbackQueryAsync(callbackQuery.Id, "Тут может быть ваш текст!");
await botClient.SendTextMessageAsync(
chat.Id,
$"Вы нажали на {callbackQuery.Data}");
return;
}
case "button3":
{
// А тут мы добавили еще showAlert, чтобы отобразить пользователю полноценное окно
await botClient.AnswerCallbackQueryAsync(callbackQuery.Id, "А это полноэкранный текст!", showAlert: true);
await botClient.SendTextMessageAsync(
chat.Id,
$"Вы нажали на {callbackQuery.Data}");
return;
}
}
return;
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
Теперь запускаем проект и проверяем кнопки!
Заключение
К сожалению, на этом пока всё. Скорее всего будет вторая часть этой статьи или полноценный видеоурок на Youtube, но пока вот так.
Прошу оценить мою статью и оставить комментарий, так как это первая моя подобная работа, до этого мне не доводилось писать статьи или что-нибудь подобное.
Комментарии (42)
NCNecros
25.08.2023 02:49+1Мне кажется было бы нагляднее если бы код раскидали по методам хотя бы. Вся логика в одном методе это такое себе.
Zara6502
25.08.2023 02:49-2а мне ООП тем и не нравится что раскидывается по одной строчке по миллиону методов и вообще непонятно что и зачем. И вместо того чтобы созерцать единое целое приходится всё запихивать в голову и структуру держать в голове, отвлекся на другой проект и всё, приходишь - а фиг его знает что там и как устроено.
NCNecros
25.08.2023 02:49+2Похоже вы никогда не видели и не пытались изменить внутренность .Select() размером в 300+ строк.
lair
25.08.2023 02:49+1а мне ООП тем и не нравится что раскидывается по одной строчке по миллиону методов и вообще непонятно что и зачем.
Это не ООП, это плохой код. Вы про структурное программирование не слышали?
Zara6502
25.08.2023 02:49-3Это не ООП, это плохой код. Вы про структурное программирование не слышали?
я как раз и пишу таким способом, а вот вы читать не умеете. я пишу про ООП, а не про код автора (уж не знаю с чего вы решили что я пишу про него), так как комментирующий NCNecros как раз и хотел бы видеть код написанный в стиле ООП.
то что я вижу у автора мне понятно, если раскидать по методам то читабельность ухудшится. Кстати в 90-е, когда я изучал программирование об этом как раз говорилось как явный минус ООП. Так как ООП раскрывается на большом коде, так как происходит структурирование данных но объектам и его методам и вы изучаете код как функцию объекта, а не как совокупность команд. Я слава богу такой код не пишу и не читаю. Как-то хотел на github взять для себя исходник чтения файлов типа QOI - это был просто трындец, переносимость нулевая, невозможно взять код за основу и перенести на другой язык. В общем не люблю я ООП, но понимаю чем он полезен и что без него никак.
lair
25.08.2023 02:49+2я как раз и пишу таким способом,
Каким "таким"?
я пишу про ООП, а не про код автора (уж не знаю с чего вы решили что я пишу про него)
Я тоже не про код автора.
так как комментирующий NCNecros как раз и хотел бы видеть код написанный в стиле ООП.
Цитирую дословно: "было бы нагляднее если бы код раскидали по методам хотя бы". Это не про ООП, это про структурное программирование.
Zara6502
25.08.2023 02:49-3Это не про ООП, это про структурное программирование
Угу. Только при процедурном у вас, внезапно, процедуры. Методы и классы - это признаки ООП.
Ме́тод в объектно-ориентированном программировании — это функция или процедура, принадлежащая[1] какому-то классу или объекту.
Как и процедура в процедурном программировании, метод состоит из некоторого количества операторов для выполнения какого-то действия и имеет набор входных аргументов.
lair
25.08.2023 02:49+2Я, вроде, нигде не упоминал процедурное программирование, я оба раза написал о структурном.
Zara6502
25.08.2023 02:49-2я воспринял ваши слова как то, что вы не понимаете о чем пишете (похоже это так), поэтому предположил что вы путаете структурное и процедурное. но раз вы сами признались что ничего не перепутали, то у меня вопросов еще больше к вам, только обсуждать это не вижу смысла, так как парадигма структурного программирования изжила себя очень давно и сейчас она в измененном виде уже подразумевается во всех языках как основа (разве только где-то, где безусловный переход является частью самого ЯП, например ассемблер). В современных языках безусловные переходы реализуются через разные прерывающие конструкции вроде return, break, continue и т.п. Например на платформе ATARI BASIC это в принципе реализовать невозможно, но это и ЯП конца 70-х.
C# это ЯООП и вы либо раскидываете всё по классам и методам либо валите код в один метод реализуя процедурное программирование. Это фуфуфу и говнокод, но это сделать можно.
Тот код который выложил автор уже написан по принципам структурного программирования (а на C# можно это сделать иначе? да что C#, с середины 80-х уже ЯП реализовывали этот подход как основной, я учился в вузе в 90-е и нам уже давали материал именно в контексте структурного программирования).
lair
25.08.2023 02:49Тот код который выложил автор уже написан по принципам структурного программирования
Я вроде бы и не говорил обратного.
Я всего лишь сказал, что "раскидывается по одной строчке по миллиону методов и вообще непонятно что и зачем" - это следствие не ООП, а плохого кода. ООП такого не требует.
А умение корректно декомпоновать задачу на иерархические блоки - это как раз и есть основа структурного программирования. Если этого не уметь, не важно, что является блоком - метод или функция.
Zara6502
25.08.2023 02:49это следствие не ООП, а плохого кода
так написано всё ООПэшное, по собственной статистике скажу, что в среднем на одну позицию информации в ООП приходится 5-7 строк кода, в процедурном 1.5
Чем меньше операций осуществляется в методе тем выше вес обязательного кода только для оформления. С этим постоянно идёт борьба, во всяком случае в .NET. Что не влияет никак на читаемость кода (или влияет минимально), это только вопрос скорости разработки и компиляции..
А умение корректно декомпоновать задачу на иерархические блоки - это как раз и есть основа структурного программирования. Если этого не уметь, не важно, что является блоком - метод или функция
Подходы изначально разные, нет тождественности между методом и функцией. Разные принципы работы с памятью, то есть с переменными. Тот же "Hello World!" в ООП из одной строки превращается в 20. При этом класс helloworld уже размещают в другом файле. При вызове сначала создает объект скласса helloworld, а потом вызывают метод который выводит текст на экран. И то, если разработчик не пойдет дальше и не сделать отдельный интерфейс по работе с экраном, который используется классом helloworld. Оно выглядит всегда красиво, блоки-блоки-классы-объекты-методы, а на практике - больше кода Богу кода. Но не стану спорить что сейчас без этого некуда.
Когда работаешь с консолью или канвасом там вообще по 3-4 строки кода может быть чтобы что-то вывести на экран (а порой и несколько разных объектов создать дополнительно). Я обычно пишу функцию отдельную где в качестве параметра принимаю все необходимые настройки, тогда хоть код не замусоривается.
lair
25.08.2023 02:49так написано всё ООПэшное
Это утверждение нуждается в формализации и доказательстве.
по собственной статистике скажу, что в среднем на одну позицию информации в ООП приходится 5-7 строк кода, в процедурном 1.5
К сожалению, без формальной методики подсчета это исключительно ваше субъективное мнение.
Подходы изначально разные, нет тождественности между методом и функцией.
Инструменты разные. А подход к иерархической декомпозиции один и тот же - я это говорю, как человек, который писал и в ООП, и в ФП, и в процедурной парадигме.
Можно раскидать по одной строчке по миллиону методов, можно раскидать по одной строчке по миллиону функций, а можно раскидать по одной строчке по миллиону процедур. А можно - во всех трех случаях - этого не делать.
Тот же "Hello World!" в ООП из одной строки превращается в 20. При этом класс helloworld уже размещают в другом файле. При вызове сначала создает объект скласса helloworld, а потом вызывают метод который выводит текст на экран.
Я просто оставлю это здесь:
using System; namespace ConsoleApp1 { internal class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); } } }
Это вот прямо из шабона приложение. И это я еще не стал брать новый .NET, где есть top-level statements.
mambastick Автор
25.08.2023 02:49Да, я изначально так и писал, но потом передумал, так как проект показался мне простым. В следующей статье (она будет продолжением этой), я уже буду использовать разные классы и методы, например, хендлер (обработчик) inline-клавиатуры, хендлер сообщений и т.д.
Protos
25.08.2023 02:49Спасибо, для начала все понятно
mambastick Автор
25.08.2023 02:49-1Благодарю! Это моя первая статья, боялся, что будет что-то непонятно)
Скоро выпущу вторую часть этой статьи :З
mikegordan
25.08.2023 02:49-1Меня больше в таких программах интересует параллельное выполнение, и многопоточность (ведь как я понял тут "закидываются" в твой код на колбэки, не дожидаясь ответов?!)
Являются ли все созданные объекты от этой библиотеки потокобезопасными?
в отличии от придирчивости @lair
Zara6502
25.08.2023 02:49не совсем понял что именно вы хотите распараллелить? система работает по принципу "вопрос-ответ".
lair
25.08.2023 02:49Например, что будет если в момент отправки одного ответа придет следующий "вопрос"?
Zara6502
25.08.2023 02:49это вопрос к Дурову, я не знаю как именно сервер ТГ обрабатывает запросы. И главное - как вы своим кодом можете на это влиять.
lair
25.08.2023 02:49-1это вопрос к Дурову, я не знаю как именно сервер ТГ обрабатывает запросы.
Сервер ТГ тут не при чем - он, очевидно, имеет достаточную степень паралеллизма, особенно учитывая, что их заведомо больше одного.
И главное - как вы своим кодом можете на это влиять.
Это тоже весьма понятно: если мой бот блокирует получение новых сообщений от сервера на время обработки уже полученных, параллелизма точно не будет. А если не блокирует, а обрабатывает их в отдельном потоке - то уже можно говорить о параллелизме в той или иной степени. Ну а дальше есть всякие варианты того, как это реализовать и какие есть ограничения.
Только в посте про это ни слова, автор поста целиком полагается на библиотеку, которую использует.
mikegordan
25.08.2023 02:49ну я ИлонМаск который через бота веду свой блог. В Боте можно лайкать , из него репостить , голосовашки разные, добавлять комменты.
Аудитория 150 млн человек. После поста в первые 20 секунд больше лайков чем у Леди Гаги около ~270k + прибавляем все остальные выше перечисленные действия.
Что я должен делать? Твой код подойдет для меня (ИлонМаска)?
Если как ты пишешь ""вопрос-ответ", то что миллионы пользователей будут жать на кнопку , ничего не будет происходить т.к. они все встанут последовательно в очередь на ответ?
Zara6502
25.08.2023 02:49-1я не специалист в этом, только сделаю предположение, что это задача сервера Telegram, он создаёт инстанс на каждый запрос - то есть копию обработчика и каждый запрос обрабатывается с разными ID Threads. Если API изначально задумывался как то, что будет работать на стороне владельца бота, то у меня для вас печальные новости, если вы захотите чтобы ваш бот работал с миллионами одновременных запросов, то у вас как минимум три проблемы которые нужно будет решить - провайдер, железо, а уже потом софт. Без первых двух абсолютно не важно насколько правильно и красиво у вас будет написан бот - он все равно не сможет обрабатывать все запросы (точнее, он будет обрабатывать только то, что будет успевать провайдер и ваше железо)
lair
25.08.2023 02:49-1Если API изначально задумывался как то, что будет работать на стороне владельца бота
Какой API?
Очевидно, что код бота выполняется на той стороне, где бот развернут.
если вы захотите чтобы ваш бот работал с миллионами одновременных запросов, то у вас как минимум три проблемы которые нужно будет решить - провайдер, железо, а уже потом софт.
Как раз провайдер и "железо" в наш век облачной инфраструктуры - не проблема. Проблема - написать софт так, чтобы он это все эффективно использовал.
Zara6502
25.08.2023 02:49-1Как раз провайдер и "железо" в наш век облачной инфраструктуры - не проблема
Это не вопрос конечной реализации мощностей, на вашей площадке или на чужой, это вопрос финансов. На Марс слетать тоже вроде можно, но что-то очереди из обывателей пока нет.
lair
25.08.2023 02:49это вопрос финансов
Предположительно, если мы пишем на аудиторию в 150 млн. человек, то финансы на мощности есть. Что возвращает нас к вопросу "как написать код, чтобы он справлялся".
mambastick Автор
25.08.2023 02:49Этот проект не рассчитывался как что-то большое, для большой аудитории. В начале статьи сказано, что в этой статье я даю необходимую базу, чтобы вы писали своих ботов, используя эту базу. Поэтому здесь идет акцент больше на теоретических знаний, что, куда и почему.
lair
25.08.2023 02:49Поэтому здесь идет акцент больше на теоретических знаний, что, куда и почему.
"Как обрабатывать многопоточность" - это как раз вполне себе теоретическая база.
mambastick Автор
25.08.2023 02:49В данном коде все будет работать последовательно, параллельной работы программы нет. Чтобы код работал в параллельном режиме, нужно использовать треды (thread) или таски (task), я предпочитаю таски, ставлю Task.Run в UpdateHandler и все работает прекрасно.
vliashko
25.08.2023 02:49Интересно было бы посмотреть как это вяжется с ИИ, а не хардкод «ответ-вопрос». Есть над чем подумать в продолжении данной темы. В целом есть немного замечаний по коду, так как чувствуется немного универских винформ, но для начала пойдет. Также вопрос насколько система будет работать с полным потоком пользователей? Были ли мысли об этом на этапе разработки?
mambastick Автор
25.08.2023 02:49В смысле вяжется с ИИ ? Немного не понял вопроса. Продолжение обязательно будет.
В целом есть немного замечаний по коду, так как чувствуется немного универских винформ, но для начала пойдет.
Честно говоря, в универе меня учили только C и после C++, C# я учу сам, используя разные ресурсы (книги, видеоуроки, статьи, интернет-материалы), поэтому я был бы очень рад получать критику моему коду, чтобы совершенствоваться!
Также вопрос насколько система будет работать с полным потоком пользователей? Были ли мысли об этом на этапе разработки?
Конкретно этот пример работает в последовательном режиме, параллельности тут нет, но ее легко добавить. В следующей статье обязательно рассмотрим этот пример. Насчет пользователей, за моей спиной уже несколько ботов и пока полеты отличные, пользователей не так много, где-то 500-1000 на бота и все отлично. Может, при увеличении пользователей где-то будут провалы, но пока все стабильно.
lair
25.08.2023 02:49где-то 500-1000 на бота и все отлично
Тут вопрос не в том, сколько пользователей на бота. Тут вопрос в том, сколько сообщений в квант времени.
lair
После первого же примера кода расхотелось читать дальше:
А зачем это поля, а не переменные? Почему они статические?
Зачем создавать
CancellationTokenSource
, если вы его никогда не активируете?...а выходить из программы не надо?
Zara6502
Мне кажется проблема Хабра в том, что те кто знают и умеют хорошо - ничего не пишут. А те кто знают и умеют, но допускают ошибки - пишут и получают шишки.
На Хабре про Телеграм бота на C# тут, тут и тут. Считаю что - больше статей Богу статей, прошлые три мне не помогли.
UndefinedRef
Нет никакой проблемы. Подобных статей написано на разных ресурсах, включая хабр, огромное количество и все они под копирку. Но помимо хабра и статей, если другие источники, в которых можно посмотреть как и что делается и как делают другие люди.
Тем более у dotnet хорошее комьюнити у которого всегда можно поспрашивать вопросы и попросить примеры.
ИМХО более управляемо и понятнее стартовать бота подобным образом:
https://github.com/ImoutoChan/GreenCaptchaBot
https://github.com/ForNeVeR/Megadeth
И в дальнейшем будет более проще им управлять (например, прикрутить админку или сорт оф)
Zara6502
И ни одна не работает. За лето так и не нашел работающий вариант.
Совсем не понятно что это и как с этим работать.
lair
Так это же хорошо. Смысл "шишек" в том, чтобы люди, которые допускают ошибки, исправлялись, а те, которые читают - видели, что это ошибки, и так делать не надо.
mambastick Автор
Приветствую. Спасибо за критику, ваши замечания действительно верны, просто изначально я хотел написать кучу методов и классов, а потом передумал, так как посчитал это неуместным для первой статьи, вот и забыл некоторые моменты подправить. Еще раз спасибо за критику, во второй статье (она будет продолжением этой) я поправлю код.
Ctrl+C или простое закрытие консоли уже не работает ?
mambastick Автор
Он используется в старте бота, в обработчике апдейтов и ошибок, а также его можно использовать, чтобы остановить бота, например, поставить
while(!cts.Token.IsCancellationRequested)
lair
Нет, не используется. Все, что вы делаете - это передаете токен (не source) в
StartReceiving
. Но поскольку вы никогда не активируете самCancellationTokenSource
, это совершенно бесполезно, и можно с тем же успехом передатьCancellationToken.None
.Где и зачем?
lair
Конечно, нет. Это аварийное закрытие программы, а надо обрабатывать еще и нормальное.