Skip to content

Commit 8e12d5b

Browse files
committed
Initial version of the donation command
1 parent b6e63d0 commit 8e12d5b

File tree

6 files changed

+440
-2
lines changed

6 files changed

+440
-2
lines changed

.env.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,11 @@ TG_PATHS='{"download": "${TG_DOWNLOADS_DIR}", "upload": "${TG_UPLOADS_DIR}"}'
3333

3434
# Webhook secrets
3535
TG_WEBHOOK_SECRET_GITHUB='github-secret'
36+
37+
# Telegram Payments
38+
TG_PAYMENT_PROVIDER_TOKEN='123:TEST:abc'
39+
40+
# URLs
41+
TG_URL_DONATE='https://...'
42+
TG_URL_PATREON='https://...'
43+
TG_URL_TIDELIFT='https://...'

commands/CallbackqueryCommand.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the PHP Telegram Support Bot.
5+
*
6+
* (c) PHP Telegram Bot Team (https://github.com/php-telegram-bot)
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Longman\TelegramBot\Commands\SystemCommands;
13+
14+
use Longman\TelegramBot\Commands\SystemCommand;
15+
use Longman\TelegramBot\Commands\UserCommands\DonateCommand;
16+
use Longman\TelegramBot\Entities\ServerResponse;
17+
18+
/**
19+
* Callback query command
20+
*
21+
* This command handles all callback queries sent via inline keyboard buttons.
22+
*
23+
* @see InlinekeyboardCommand.php
24+
*/
25+
class CallbackqueryCommand extends SystemCommand
26+
{
27+
/**
28+
* @var string
29+
*/
30+
protected $name = 'callbackquery';
31+
32+
/**
33+
* @var string
34+
*/
35+
protected $description = 'Reply to callback query';
36+
37+
/**
38+
* @var string
39+
*/
40+
protected $version = '0.1.0';
41+
42+
/**
43+
* Command execute method
44+
*
45+
* @return ServerResponse
46+
*/
47+
public function execute(): ServerResponse
48+
{
49+
$callback_query = $this->getCallbackQuery();
50+
parse_str($callback_query->getData(), $data);
51+
52+
if ('donate' === $data['command']) {
53+
DonateCommand::createPaymentInvoice(
54+
$callback_query->getFrom()->getId(),
55+
$data['amount'],
56+
$data['currency']
57+
);
58+
59+
return $callback_query->answer([
60+
'text' => 'Awesome, an invoice has been sent to you.',
61+
]);
62+
}
63+
64+
return $callback_query->answer();
65+
}
66+
}

commands/DonateCommand.php

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the PHP Telegram Support Bot.
5+
*
6+
* (c) PHP Telegram Bot Team (https://github.com/php-telegram-bot)
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Longman\TelegramBot\Commands\UserCommands;
15+
16+
use JsonException;
17+
use LitEmoji\LitEmoji;
18+
use Longman\TelegramBot\Commands\UserCommand;
19+
use Longman\TelegramBot\Entities\InlineKeyboard;
20+
use Longman\TelegramBot\Entities\Payments\LabeledPrice;
21+
use Longman\TelegramBot\Entities\Payments\SuccessfulPayment;
22+
use Longman\TelegramBot\Entities\ServerResponse;
23+
use Longman\TelegramBot\Exception\TelegramException;
24+
use Longman\TelegramBot\Request;
25+
26+
use function TelegramBot\SupportBot\cache;
27+
28+
/**
29+
* Donate using Telegram Payments.
30+
*/
31+
class DonateCommand extends UserCommand
32+
{
33+
public const DEFAULT_CURRENCY = 'EUR';
34+
35+
/**
36+
* @var string
37+
*/
38+
protected $name = 'donate';
39+
40+
/**
41+
* @var string
42+
*/
43+
protected $description = 'Donate to the PHP Telegram Bot project';
44+
45+
/**
46+
* @var string
47+
*/
48+
protected $usage = '/donate <amount> <currency>';
49+
50+
/**
51+
* @var string
52+
*/
53+
protected $version = '0.1.0';
54+
55+
/**
56+
* @var bool
57+
*/
58+
protected $private_only = true;
59+
60+
/**
61+
* @return ServerResponse
62+
* @throws TelegramException
63+
*/
64+
public function preExecute(): ServerResponse
65+
{
66+
$this->isPrivateOnly() && $this->removeNonPrivateMessage();
67+
68+
// Make sure we only reply to messages.
69+
if (!$this->getMessage()) {
70+
return Request::emptyResponse();
71+
}
72+
73+
return $this->execute();
74+
}
75+
76+
/**
77+
* Execute command
78+
*
79+
* @return ServerResponse
80+
* @throws TelegramException
81+
*/
82+
public function execute(): ServerResponse
83+
{
84+
$currencies = $this->validateCurrencyFetching();
85+
if ($currencies instanceof ServerResponse) {
86+
return $currencies;
87+
}
88+
89+
$message = $this->getMessage();
90+
$user_id = $message->getFrom()->getId();
91+
92+
$text = trim($message->getText(true));
93+
if ('' === $text) {
94+
return $this->sendBaseDonationMessage();
95+
}
96+
97+
// Fetch currency and amount being donated.
98+
// Hack: https://stackoverflow.com/a/1807896
99+
[$amount, $currency_code] = preg_split('/\s+/', "$text ");
100+
101+
$currency = $this->validateCurrency($currency_code);
102+
if ($currency instanceof ServerResponse) {
103+
return $currency;
104+
}
105+
106+
$amount = $this->validateAmount($amount, $currency);
107+
if ($amount instanceof ServerResponse) {
108+
return $amount;
109+
}
110+
111+
return self::createPaymentInvoice($user_id, $amount, $currency['code']);
112+
}
113+
114+
/**
115+
* Fetch the list of official currencies supported by Telegram Payments.
116+
*
117+
* @return array
118+
*/
119+
protected function fetchCurrenciesFromTelegram(): array
120+
{
121+
try {
122+
$currencies = cache()->get('telegram_bot_currencies.json');
123+
if (empty($currencies)) {
124+
$currencies = file_get_contents('https://core.telegram.org/bots/payments/currencies.json');
125+
cache()->set('telegram_bot_currencies.json', $currencies, 86400);
126+
}
127+
128+
return json_decode($currencies, true, 512, JSON_THROW_ON_ERROR);
129+
} catch (JsonException $e) {
130+
return [];
131+
}
132+
}
133+
134+
/**
135+
* Create an invoice for the passed parameters and return the response.
136+
*
137+
* @param int $chat_id
138+
* @param int $amount
139+
* @param string $currency_code
140+
*
141+
* @return ServerResponse
142+
*/
143+
public static function createPaymentInvoice(int $chat_id, int $amount, string $currency_code = self::DEFAULT_CURRENCY): ServerResponse
144+
{
145+
$price = new LabeledPrice(['label' => 'Donation', 'amount' => $amount]);
146+
147+
return Request::sendInvoice([
148+
'chat_id' => $chat_id,
149+
'title' => 'Donation to the PHP Telegram Bot library',
150+
'description' => LitEmoji::encodeUnicode(
151+
':rainbow: Support the well-being of this great project and help it progress.' . PHP_EOL .
152+
PHP_EOL .
153+
':heart: With much appreciation, your donation will flow back into making the PHP Telegram Bot library even better!'
154+
),
155+
'payload' => "donation_{$amount}_{$currency_code}",
156+
'provider_token' => getenv('TG_PAYMENT_PROVIDER_TOKEN'),
157+
'start_parameter' => 'donation',
158+
'currency' => strtoupper($currency_code),
159+
'prices' => [$price],
160+
'reply_markup' => new InlineKeyboard([
161+
['text' => LitEmoji::encodeUnicode(':money_with_wings: Donate Now'), 'pay' => true],
162+
['text' => LitEmoji::encodeUnicode(':gem: Become a Patron'), 'url' => getenv('TG_URL_PATREON')],
163+
]),
164+
]);
165+
}
166+
167+
/**
168+
* Make sure the currencies can be retrieved and cached correctly.
169+
*
170+
* @return array|ServerResponse
171+
* @throws TelegramException
172+
*/
173+
protected function validateCurrencyFetching()
174+
{
175+
if ($currencies = $this->fetchCurrenciesFromTelegram()) {
176+
return $currencies;
177+
}
178+
179+
return $this->replyToUser(
180+
LitEmoji::encodeUnicode(
181+
'Donations via the Support Bot are not available at this time :confused:' . PHP_EOL .
182+
PHP_EOL .
183+
'Try again later or see [other ways to donate](' . getenv('TG_URL_DONATE') . ')'
184+
),
185+
['parse_mode' => 'markdown']
186+
);
187+
}
188+
189+
/**
190+
* Ensure the currency is valid and return the currency data array.
191+
*
192+
* @param string $currency_code
193+
*
194+
* @return array|ServerResponse
195+
* @throws TelegramException
196+
*/
197+
protected function validateCurrency(string $currency_code)
198+
{
199+
$currencies = $this->fetchCurrenciesFromTelegram();
200+
201+
'' !== $currency_code || $currency_code = self::DEFAULT_CURRENCY;
202+
$currency_code = strtoupper($currency_code);
203+
204+
if ($currency = $currencies[$currency_code] ?? null) {
205+
return $currency;
206+
}
207+
208+
return $this->replyToUser(
209+
"Currency *{$currency_code}* not supported." . PHP_EOL .
210+
PHP_EOL .
211+
'[Check supported currencies](https://core.telegram.org/bots/payments#supported-currencies)',
212+
['parse_mode' => 'markdown', 'disable_web_page_preview' => true]
213+
);
214+
}
215+
216+
/**
217+
* Ensure the amount is valid and return the clean integer to use for the invoice.
218+
*
219+
* @param string $amount
220+
* @param array $currency
221+
*
222+
* @return int|ServerResponse
223+
* @throws TelegramException
224+
*/
225+
protected function validateAmount(string $amount, $currency)
226+
{
227+
$int_amount = (int) ceil((float) $amount);
228+
229+
// Check that the donation amount is valid.
230+
$multiplier = 10 ** (int) $currency['exp'];
231+
232+
// Let's ignore the fractions and round to the next whole.
233+
$min_amount = (int) ceil($currency['min_amount'] / $multiplier);
234+
$max_amount = (int) floor($currency['max_amount'] / $multiplier);
235+
236+
if ($int_amount >= $min_amount && $int_amount <= $max_amount) {
237+
return $int_amount * $multiplier;
238+
}
239+
240+
return $this->replyToUser(
241+
sprintf(
242+
'Donations in %1$s must be between %2$s and %3$s.' . PHP_EOL .
243+
PHP_EOL .
244+
'[Check currency limits](https://core.telegram.org/bots/payments#supported-currencies)',
245+
$currency['title'],
246+
$min_amount,
247+
$max_amount
248+
),
249+
['parse_mode' => 'markdown', 'disable_web_page_preview' => true]
250+
);
251+
}
252+
253+
/**
254+
* Send a message with an inline keyboard listing predefined amounts.
255+
*
256+
* @return ServerResponse
257+
* @throws TelegramException
258+
*/
259+
protected function sendBaseDonationMessage(): ServerResponse
260+
{
261+
return $this->replyToUser(
262+
LitEmoji::encodeUnicode(
263+
":smiley: So great that you're considering a donation to the PHP Telegram Bot project." . PHP_EOL .
264+
PHP_EOL .
265+
':+1: Simply select one of the predefined amounts listed below.' . PHP_EOL .
266+
PHP_EOL .
267+
'Alternatively, you can also define a custom amount using:' . PHP_EOL .
268+
'`' . $this->usage . '`' . PHP_EOL
269+
) . PHP_EOL .
270+
'[Check supported currencies](https://core.telegram.org/bots/payments#supported-currencies) (Default is: *' . self::DEFAULT_CURRENCY . '*)',
271+
[
272+
'parse_mode' => 'markdown',
273+
'disable_web_page_preview' => true,
274+
'reply_markup' => new InlineKeyboard([
275+
['text' => '5€', 'callback_data' => 'command=donate&amount=500&currency=EUR'],
276+
['text' => '10€', 'callback_data' => 'command=donate&amount=1000&currency=EUR'],
277+
['text' => '20€', 'callback_data' => 'command=donate&amount=2000&currency=EUR'],
278+
['text' => '50€', 'callback_data' => 'command=donate&amount=5000&currency=EUR'],
279+
], [
280+
['text' => '$5', 'callback_data' => 'command=donate&amount=500&currency=USD'],
281+
['text' => '$10', 'callback_data' => 'command=donate&amount=1000&currency=USD'],
282+
['text' => '$20', 'callback_data' => 'command=donate&amount=2000&currency=USD'],
283+
['text' => '$50', 'callback_data' => 'command=donate&amount=5000&currency=USD'],
284+
], [
285+
['text' => LitEmoji::encodeUnicode(':gem: Patreon'), 'url' => getenv('TG_URL_PATREON')],
286+
['text' => LitEmoji::encodeUnicode(':cyclone: Tidelift'), 'url' => getenv('TG_URL_TIDELIFT')],
287+
['text' => 'More options...', 'url' => getenv('TG_URL_DONATE')],
288+
]),
289+
]
290+
);
291+
}
292+
293+
/**
294+
* Send "Thank you" message to user who donated.
295+
*
296+
* @param SuccessfulPayment $payment
297+
* @param int $user_id
298+
*
299+
* @return ServerResponse
300+
* @throws TelegramException
301+
*/
302+
public static function handleSuccessfulPayment(SuccessfulPayment $payment, int $user_id): ServerResponse
303+
{
304+
return Request::sendMessage([
305+
'chat_id' => $user_id,
306+
'text' => LitEmoji::encodeUnicode(
307+
':pray: Thank you for joining our growing list of donors.' . PHP_EOL .
308+
':star: Your support helps a lot to keep this project alive!'
309+
),
310+
]);
311+
}
312+
}

0 commit comments

Comments
 (0)