diff --git a/.env.example b/.env.example
index 167d3c9..e91ff31 100644
--- a/.env.example
+++ b/.env.example
@@ -42,6 +42,10 @@ TG_GITHUB_AUTH_TOKEN='github-token'
# Telegram Payments
TG_PAYMENT_PROVIDER_TOKEN='123:TEST:abc'
+# Support group activation expiry and ban times
+TG_SUPPORT_GROUP_ACTIVATION_EXPIRE_TIME='15 min'
+TG_SUPPORT_GROUP_BAN_TIME='1 day'
+
# URLs
TG_URL_DONATE='https://...'
TG_URL_PATREON='https://...'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 40edd61..53e191c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ Exclamation symbols (:exclamation:) note something of importance e.g. breaking c
## [Unreleased]
### Added
+- Rules must be agreed to before allowing a user to post in the group.
### Changed
### Deprecated
### Removed
diff --git a/commands/CallbackqueryCommand.php b/commands/CallbackqueryCommand.php
index 05cb8dc..44a1560 100644
--- a/commands/CallbackqueryCommand.php
+++ b/commands/CallbackqueryCommand.php
@@ -13,6 +13,7 @@
use Longman\TelegramBot\Commands\SystemCommand;
use Longman\TelegramBot\Commands\UserCommands\DonateCommand;
+use Longman\TelegramBot\Commands\UserCommands\RulesCommand;
use Longman\TelegramBot\Entities\ServerResponse;
/**
@@ -47,18 +48,14 @@ class CallbackqueryCommand extends SystemCommand
public function execute(): ServerResponse
{
$callback_query = $this->getCallbackQuery();
- parse_str($callback_query->getData(), $data);
+ parse_str($callback_query->getData(), $callback_data);
- if ('donate' === $data['command']) {
- DonateCommand::createPaymentInvoice(
- $callback_query->getFrom()->getId(),
- $data['amount'],
- $data['currency']
- );
+ if ('donate' === $callback_data['command']) {
+ return DonateCommand::handleCallbackQuery($callback_query, $callback_data);
+ }
- return $callback_query->answer([
- 'text' => 'Awesome, an invoice has been sent to you.',
- ]);
+ if ('rules' === $callback_data['command']) {
+ return RulesCommand::handleCallbackQuery($callback_query, $callback_data);
}
return $callback_query->answer();
diff --git a/commands/DonateCommand.php b/commands/DonateCommand.php
index 22ed2ef..5af94be 100644
--- a/commands/DonateCommand.php
+++ b/commands/DonateCommand.php
@@ -16,6 +16,7 @@
use JsonException;
use LitEmoji\LitEmoji;
use Longman\TelegramBot\Commands\UserCommand;
+use Longman\TelegramBot\Entities\CallbackQuery;
use Longman\TelegramBot\Entities\InlineKeyboard;
use Longman\TelegramBot\Entities\Payments\LabeledPrice;
use Longman\TelegramBot\Entities\Payments\SuccessfulPayment;
@@ -57,6 +58,27 @@ class DonateCommand extends UserCommand
*/
protected $private_only = true;
+ /**
+ * Handle the callback queries regarding the /donate command.
+ *
+ * @param CallbackQuery $callback_query
+ * @param array $callback_data
+ *
+ * @return ServerResponse
+ */
+ public static function handleCallbackQuery(CallbackQuery $callback_query, array $callback_data): ServerResponse
+ {
+ self::createPaymentInvoice(
+ $callback_query->getFrom()->getId(),
+ $callback_data['amount'],
+ $callback_data['currency']
+ );
+
+ return $callback_query->answer([
+ 'text' => 'Awesome, an invoice has been sent to you.',
+ ]);
+ }
+
/**
* @return ServerResponse
* @throws TelegramException
diff --git a/commands/GenericmessageCommand.php b/commands/GenericmessageCommand.php
index a96e7b7..e6a236f 100644
--- a/commands/GenericmessageCommand.php
+++ b/commands/GenericmessageCommand.php
@@ -17,6 +17,7 @@
use Longman\TelegramBot\Commands\UserCommands\DonateCommand;
use Longman\TelegramBot\Entities\ServerResponse;
use Longman\TelegramBot\Exception\TelegramException;
+use Longman\TelegramBot\Request;
/**
* Generic message command
@@ -51,8 +52,12 @@ public function execute(): ServerResponse
// Handle new chat members.
if ($message->getNewChatMembers()) {
+ $this->deleteThisMessage(); // Service message.
return $this->getTelegram()->executeCommand('newchatmembers');
}
+ if ($message->getLeftChatMember()) {
+ $this->deleteThisMessage(); // Service message.
+ }
// Handle successful payment of donation.
if ($payment = $message->getSuccessfulPayment()) {
@@ -66,4 +71,17 @@ public function execute(): ServerResponse
return parent::execute();
}
+
+ /**
+ * Delete the current message.
+ *
+ * @return ServerResponse
+ */
+ private function deleteThisMessage(): ServerResponse
+ {
+ return Request::deleteMessage([
+ 'chat_id' => $this->getMessage()->getChat()->getId(),
+ 'message_id' => $this->getMessage()->getMessageId(),
+ ]);
+ }
}
diff --git a/commands/NewchatmembersCommand.php b/commands/NewchatmembersCommand.php
index 9377835..07c7911 100644
--- a/commands/NewchatmembersCommand.php
+++ b/commands/NewchatmembersCommand.php
@@ -13,8 +13,13 @@
namespace Longman\TelegramBot\Commands\SystemCommands;
+use LitEmoji\LitEmoji;
use Longman\TelegramBot\Commands\SystemCommand;
+use Longman\TelegramBot\DB;
use Longman\TelegramBot\Entities\ChatMember;
+use Longman\TelegramBot\Entities\ChatPermissions;
+use Longman\TelegramBot\Entities\InlineKeyboard;
+use Longman\TelegramBot\Entities\Message;
use Longman\TelegramBot\Entities\ServerResponse;
use Longman\TelegramBot\Entities\User;
use Longman\TelegramBot\Exception\TelegramException;
@@ -39,7 +44,12 @@ class NewchatmembersCommand extends SystemCommand
/**
* @var string
*/
- protected $version = '0.4.0';
+ protected $version = '0.5.0';
+
+ /**
+ * @var Message
+ */
+ private $message;
/**
* @var int
@@ -62,17 +72,23 @@ class NewchatmembersCommand extends SystemCommand
*/
public function execute(): ServerResponse
{
- $message = $this->getMessage();
- $this->chat_id = $message->getChat()->getId();
- $this->user_id = $message->getFrom()->getId();
+ $this->message = $this->getMessage();
+ $this->chat_id = $this->message->getChat()->getId();
+ $this->user_id = $this->message->getFrom()->getId();
- $this->group_name = $message->getChat()->getTitle();
+ $this->group_name = $this->message->getChat()->getTitle();
['users' => $new_users, 'bots' => $new_bots] = $this->getNewUsersAndBots();
// Kick bots if they weren't added by an admin.
$this->kickDisallowedBots($new_bots);
+ // Restrict all permissions for new users.
+ $this->restrictNewUsers($new_users);
+
+ // Set the joined date for all new group members.
+ $this->updateUsersJoinedDate($new_users);
+
return $this->refreshWelcomeMessage($new_users);
}
@@ -94,11 +110,20 @@ private function refreshWelcomeMessage(array $new_users): ServerResponse
return '' . filter_var($new_user->getFirstName(), FILTER_SANITIZE_SPECIAL_CHARS) . '';
}, $new_users));
- $text = "Welcome {$new_users_text} to the {$this->group_name} group\n";
- $text .= 'Please remember that this is NOT the Telegram Support Chat.' . PHP_EOL;
- $text .= 'Read the Rules that apply here.';
-
- $welcome_message_sent = $this->replyToChat($text, ['parse_mode' => 'HTML', 'disable_web_page_preview' => true]);
+ $text = ":wave: Welcome {$new_users_text} to the {$this->group_name} group\n\n";
+ $text .= 'Please read and agree to the rules before posting here, thank you!';
+
+ $welcome_message_sent = $this->replyToChat(
+ LitEmoji::encodeUnicode($text),
+ [
+ 'parse_mode' => 'HTML',
+ 'disable_web_page_preview' => true,
+ 'disable_notification' => true,
+ 'reply_markup' => new InlineKeyboard([
+ ['text' => LitEmoji::encodeUnicode(':orange_book: Read the Rules'), 'url' => 'https://t.me/' . getenv('TG_BOT_USERNAME') . '?start=rules'],
+ ]),
+ ]
+ );
if (!$welcome_message_sent->isOk()) {
return Request::emptyResponse();
}
@@ -145,7 +170,7 @@ private function getNewUsersAndBots(): array
$users = [];
$bots = [];
- foreach ($this->getMessage()->getNewChatMembers() as $member) {
+ foreach ($this->message->getNewChatMembers() as $member) {
if ($member->getIsBot()) {
$bots[] = $member;
continue;
@@ -177,4 +202,58 @@ private function kickDisallowedBots(array $bots): void
]);
}
}
+
+ /**
+ * Write users join date to DB.
+ *
+ * @param array $new_users
+ *
+ * @return bool
+ */
+ private function updateUsersJoinedDate($new_users): bool
+ {
+ $new_users_ids = array_map(static function (User $user) {
+ return $user->getId();
+ }, $new_users);
+
+ // Update "Joined Date" for new users.
+ return DB::getPdo()->prepare("
+ UPDATE " . TB_USER . "
+ SET `joined_at` = ?
+ WHERE `id` IN (?)
+ ")->execute([date('Y-m-d H:i:s'), implode(',', $new_users_ids)]);
+ }
+
+ /**
+ * Restrict permissions in support group for passed users.
+ *
+ * @param array $new_users
+ *
+ * @return array
+ */
+ private function restrictNewUsers($new_users): array
+ {
+ $responses = [];
+
+ /** @var User[] $new_users */
+ foreach ($new_users as $new_user) {
+ $user_id = $new_user->getId();
+ $responses[$user_id] = Request::restrictChatMember([
+ 'chat_id' => getenv('TG_SUPPORT_GROUP_ID'),
+ 'user_id' => $user_id,
+ 'permissions' => new ChatPermissions([
+ 'can_send_messages' => false,
+ 'can_send_media_messages' => false,
+ 'can_send_polls' => false,
+ 'can_send_other_messages' => false,
+ 'can_add_web_page_previews' => false,
+ 'can_change_info' => false,
+ 'can_invite_users' => false,
+ 'can_pin_messages' => false,
+ ]),
+ ]);
+ }
+
+ return $responses;
+ }
}
diff --git a/commands/RulesCommand.php b/commands/RulesCommand.php
index fd4e059..3e1d51e 100644
--- a/commands/RulesCommand.php
+++ b/commands/RulesCommand.php
@@ -13,8 +13,14 @@
namespace Longman\TelegramBot\Commands\UserCommands;
+use LitEmoji\LitEmoji;
use Longman\TelegramBot\Commands\UserCommand;
+use Longman\TelegramBot\DB;
+use Longman\TelegramBot\Entities\CallbackQuery;
+use Longman\TelegramBot\Entities\ChatPermissions;
+use Longman\TelegramBot\Entities\InlineKeyboard;
use Longman\TelegramBot\Entities\ServerResponse;
+use Longman\TelegramBot\Request;
/**
* User "/rules" command
@@ -46,28 +52,109 @@ class RulesCommand extends UserCommand
*/
protected $private_only = true;
+ public static function handleCallbackQuery(CallbackQuery $callback_query, array $callback_data): ?ServerResponse
+ {
+ if ('agree' === $callback_data['action'] ?? null) {
+ $message = $callback_query->getMessage();
+ $chat_id = $message->getChat()->getId();
+ $clicked_user_id = $callback_query->getFrom()->getId();
+
+ // If the user is already activated, keep the initial activation date.
+ $activated = DB::getPdo()->prepare("
+ UPDATE " . TB_USER . "
+ SET `activated_at` = ?
+ WHERE `id` = ?
+ AND `activated_at` IS NULL
+ ")->execute([date('Y-m-d H:i:s'), $clicked_user_id]);
+
+ if (!$activated) {
+ return $callback_query->answer([
+ 'text' => 'Something went wrong, please try again later.',
+ 'show_alert' => true,
+ ]);
+ }
+
+ $give_permissions = Request::restrictChatMember([
+ 'chat_id' => getenv('TG_SUPPORT_GROUP_ID'),
+ 'user_id' => $clicked_user_id,
+ 'permissions' => new ChatPermissions([
+ 'can_send_messages' => true,
+ 'can_send_media_messages' => true,
+ 'can_add_web_page_previews' => true,
+ 'can_invite_users' => true,
+ ]),
+ ]);
+
+ Request::editMessageReplyMarkup([
+ 'chat_id' => $chat_id,
+ 'message_id' => $message->getMessageId(),
+ 'reply_markup' => new InlineKeyboard([
+ ['text' => LitEmoji::encodeUnicode(':white_check_mark: Ok! Go to Bot Support group...'), 'url' => 'https://t.me/' . getenv('TG_SUPPORT_GROUP_ID')],
+ ]),
+ ]);
+
+ return $callback_query->answer([
+ 'text' => 'Thanks for agreeing to the rules. You may now post in the support group.',
+ 'show_alert' => true,
+ ]);
+ }
+
+ return $callback_query->answer();
+ }
+
/**
* @inheritdoc
*/
public function execute(): ServerResponse
{
- $text = << 'markdown',
+ 'disable_web_page_preview' => true,
+ ];
- return $this->replyToChat($text, ['parse_mode' => 'markdown']);
+ if (!self::hasUserAgreedToRules($this->getMessage()->getFrom()->getId())) {
+ $text .= PHP_EOL . 'You **must agree** to these rules to post in the support group. Simply click the button below.';
+ $data['reply_markup'] = new InlineKeyboard([
+ ['text' => LitEmoji::encodeUnicode(':+1: I Agree to the Rules'), 'callback_data' => 'command=rules&action=agree'],
+ ]);
+ }
+
+ return $this->replyToChat(LitEmoji::encodeUnicode($text), $data);
+ }
+
+ /**
+ * Check if the passed user has agreed to the rules.
+ *
+ * @param int $user_id
+ *
+ * @return bool
+ */
+ protected static function hasUserAgreedToRules(int $user_id): bool
+ {
+ $statement = DB::getPdo()->prepare('
+ SELECT `activated_at`
+ FROM `' . TB_USER . '`
+ WHERE `id` = ?
+ AND `activated_at` IS NOT NULL
+ ');
+ $statement->execute([$user_id]);
+ $agreed = $statement->fetchAll(\PDO::FETCH_COLUMN, 0);
+
+ return !empty($agreed);
}
}
diff --git a/commands/StartCommand.php b/commands/StartCommand.php
index a613234..1e8d4d4 100644
--- a/commands/StartCommand.php
+++ b/commands/StartCommand.php
@@ -53,6 +53,10 @@ class StartCommand extends SystemCommand
*/
public function execute(): ServerResponse
{
+ if ('activate' === $this->getMessage()->getText(true)) {
+ return $this->getTelegram()->executeCommand('activate');
+ }
+
if ('rules' === $this->getMessage()->getText(true)) {
return $this->getTelegram()->executeCommand('rules');
}
diff --git a/cron.php b/cron.php
new file mode 100644
index 0000000..e3595bf
--- /dev/null
+++ b/cron.php
@@ -0,0 +1,38 @@
+load();
+
+try {
+ $telegram = new Telegram(getenv('TG_API_KEY'), getenv('TG_BOT_USERNAME'));
+ $telegram->enableMySql([
+ 'host' => getenv('TG_DB_HOST'),
+ 'port' => getenv('TG_DB_PORT'),
+ 'user' => getenv('TG_DB_USER'),
+ 'password' => getenv('TG_DB_PASSWORD'),
+ 'database' => getenv('TG_DB_DATABASE'),
+ ]);
+
+ // Handle expired activations.
+ Helpers::handleExpiredActivations();
+} catch (\Throwable $e) {
+ TelegramLog::error($e->getMessage());
+}
diff --git a/src/Helpers.php b/src/Helpers.php
index b3ada87..1c6a0fd 100644
--- a/src/Helpers.php
+++ b/src/Helpers.php
@@ -102,4 +102,56 @@ public static function saveLatestWelcomeMessage($welcome_message_id): void
$new_welcome_message_ids = array_values($welcome_message_ids) + ['latest' => $welcome_message_id];
self::setSimpleOption('welcome_message_ids', $new_welcome_message_ids);
}
+
+ /**
+ * Handle expired activations and kick those users.
+ */
+ public static function handleExpiredActivations(): void
+ {
+ $expiry_time = strtotime(getenv('TG_SUPPORT_GROUP_ACTIVATION_EXPIRE_TIME') ?: '15 min');
+ $expiry_time_in_s = $expiry_time - time();
+
+ // If the user is already activated, keep the initial activation date.
+ $users_to_kick = DB::getPdo()->query("
+ SELECT `id`
+ FROM " . TB_USER . "
+ WHERE `joined_at` < (NOW() - INTERVAL {$expiry_time_in_s} SECOND)
+ AND `activated_at` IS NULL
+ ");
+ foreach ($users_to_kick as $user_to_kick) {
+ self::kickUser((int) $user_to_kick['id']);
+ }
+ }
+
+ /**
+ * Kick the passed user.
+ *
+ * @param int $user_id
+ *
+ * @return bool
+ */
+ protected static function kickUser(int $user_id): bool
+ {
+ try {
+ $ban_time = strtotime(getenv('TG_SUPPORT_GROUP_BAN_TIME') ?: '1 day');
+ $kick_user = Request::kickChatMember([
+ 'chat_id' => getenv('TG_SUPPORT_GROUP_ID'),
+ 'user_id' => $user_id,
+ 'until_date' => $ban_time,
+ ]);
+ if ($kick_user->isOk()) {
+ return DB::getPdo()->prepare("
+ UPDATE " . TB_USER . "
+ SET `activated_at` = NULL,
+ `joined_at` = NULL,
+ `kicked_at` = NOW()
+ WHERE `id` = ?
+ ")->execute([$user_id]);
+ }
+ } catch (\Throwable $e) {
+ // Fail silently.
+ }
+
+ return false;
+ }
}
diff --git a/structure.sql b/structure.sql
index 3ff4928..f1a8ebe 100644
--- a/structure.sql
+++ b/structure.sql
@@ -8,3 +8,8 @@ CREATE TABLE IF NOT EXISTS `simple_options` (
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+-- Version Unreleased
+ALTER TABLE `user` ADD COLUMN `joined_at` TIMESTAMP NULL COMMENT 'Timestamp when the user joined the support group.';
+ALTER TABLE `user` ADD COLUMN `kicked_at` TIMESTAMP NULL COMMENT 'Timestamp when the user was kicked from the support group.';
+ALTER TABLE `user` ADD COLUMN `activated_at` TIMESTAMP NULL COMMENT 'Timestamp when the user has agreed to the rules and has been allowed to post in the support group.';