Skip to content

Commit cf633fa

Browse files
committed
Deep Link: Support bot creation link (#7413)
1 parent e5b8e43 commit cf633fa

28 files changed

Lines changed: 757 additions & 26 deletions

File tree

‎src/api/gramjs/apiBuilders/appConfig.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ type Limit =
2929
| 'recommended_channels_limit'
3030
| 'saved_dialogs_pinned_limit'
3131
| 'reactions_user_max'
32-
| 'aicompose_tone_saved_limit';
32+
| 'aicompose_tone_saved_limit'
33+
| 'bots_create_limit';
3334
type LimitKey = `${Limit}_${LimitType}`;
3435
type LimitsConfig = Record<LimitKey, number>;
3536

@@ -150,6 +151,7 @@ export interface GramJsAppConfig extends LimitsConfig {
150151
aicompose_tone_examples_num?: number;
151152
aicompose_tone_title_length_max?: number;
152153
aicompose_tone_prompt_length_max?: number;
154+
bot_allowed_suffixes?: string[];
153155
}
154156

155157
function buildEmojiSounds(appConfig: GramJsAppConfig) {
@@ -237,6 +239,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
237239
maxReactions: getLimit(appConfig, 'reactions_user_max', 'maxReactions'),
238240
moreAccounts: DEFAULT_LIMITS.moreAccounts,
239241
aiComposeToneSaved: getLimit(appConfig, 'aicompose_tone_saved_limit', 'aiComposeToneSaved'),
242+
botsCreate: getLimit(appConfig, 'bots_create_limit', 'botsCreate'),
240243
},
241244
richMessageLengthLimit: appConfig.rich_message_length_limit ?? DEFAULT_APP_CONFIG.richMessageLengthLimit,
242245
richMessageMaxBlocks: appConfig.rich_message_max_blocks ?? DEFAULT_APP_CONFIG.richMessageMaxBlocks,
@@ -318,6 +321,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
318321
aiComposeToneExamplesNum: appConfig.aicompose_tone_examples_num,
319322
aiComposeToneTitleLengthMax: appConfig.aicompose_tone_title_length_max,
320323
aiComposeTonePromptLengthMax: appConfig.aicompose_tone_prompt_length_max,
324+
botAllowedSuffixes: appConfig.bot_allowed_suffixes,
321325
};
322326

323327
return {

‎src/api/gramjs/apiBuilders/messageActions.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,13 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess
207207
peerIds: action.peers.map(getApiChatIdFromMtpPeer),
208208
};
209209
}
210+
if (action instanceof GramJs.MessageActionManagedBotCreated) {
211+
return {
212+
mediaType: 'action',
213+
type: 'managedBotCreated',
214+
botId: buildApiPeerId(action.botId, 'user'),
215+
};
216+
}
210217
if (action instanceof GramJs.MessageActionBoostApply) {
211218
const { boosts } = action;
212219
return {

‎src/api/gramjs/apiBuilders/users.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
4040
botCanManageEmojiStatus, settings, sendPaidMessagesStars, displayGiftsButton, disallowedGifts,
4141
starsRating, starsMyPendingRating, starsMyPendingRatingDate, mainTab, savedMusic, note,
4242
noforwardsMyEnabled, noforwardsPeerEnabled, unofficialSecurityRisk, privateForwardName,
43-
ttlPeriod,
43+
ttlPeriod, botManagerId,
4444
},
4545
users,
4646
} = mtpUserFull;
@@ -87,6 +87,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
8787
noForwardsMyEnabled: noforwardsMyEnabled,
8888
noForwardsPeerEnabled: noforwardsPeerEnabled,
8989
isUnofficialSecurityRisk: unofficialSecurityRisk,
90+
botManagerId: botManagerId !== undefined ? buildApiPeerId(botManagerId, 'user') : undefined,
9091
};
9192
}
9293

@@ -123,7 +124,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
123124
id, firstName, lastName, fake, scam, support, closeFriend, storiesUnavailable,
124125
botActiveUsers, botVerificationIcon, botInlinePlaceholder, botAttachMenu, botCanEdit,
125126
sendPaidMessagesStars, profileColor, botForumView, botForumCanManageTopics, botGuestchat,
126-
botGuard,
127+
botGuard, botCanManageBots,
127128
} = mtpUser;
128129
const storiesMaxId = mtpUser.storiesMaxId?.maxId;
129130
const hasVideoAvatar = mtpUser.photo instanceof GramJs.UserProfilePhoto ? Boolean(mtpUser.photo.hasVideo) : undefined;
@@ -169,6 +170,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
169170
paidMessagesStars: toJSNumber(sendPaidMessagesStars),
170171
isBotForum: botForumView,
171172
canManageBotForumTopics: botForumCanManageTopics,
173+
canManageBots: botCanManageBots,
172174
isGuestChatBot: botGuestchat,
173175
isGuardBot: botGuard || undefined,
174176
};

‎src/api/gramjs/methods/bots.ts‎

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,16 @@ import {
4141
addDocumentToLocalDb,
4242
addInlineBotRepairInfo,
4343
addPhotoToLocalDb,
44+
addUserToLocalDb,
4445
addWebDocumentToLocalDb,
4546
} from '../helpers/localDb';
46-
import { deserializeBytes } from '../helpers/misc';
47+
import { checkErrorType, deserializeBytes, wrapError } from '../helpers/misc';
4748
import { sendApiUpdate } from '../updates/apiUpdateEmitter';
4849
import { invokeRequest } from './client';
4950
import { returnTonConnectOauthSession, takeTonConnectOauthSession } from './tonConnect';
5051

52+
const BOT_USERNAME_ERRORS = new Set(['USERNAME_OCCUPIED', 'USERNAME_INVALID', 'USERNAME_SUFFIX_MISSING']);
53+
5154
export async function answerCallbackButton({
5255
chatId, accessHash, messageId, data, isGame,
5356
}: {
@@ -749,6 +752,51 @@ export async function fetchBotsRecommendations({ user }: { user: ApiChat }) {
749752
};
750753
}
751754

755+
export async function checkBotUsername({ username }: { username: string }) {
756+
try {
757+
const result = await invokeRequest(new GramJs.bots.CheckUsername({ username }), {
758+
shouldThrow: true,
759+
});
760+
761+
return { result, error: undefined };
762+
} catch (err: unknown) {
763+
if (err instanceof RPCError && BOT_USERNAME_ERRORS.has(err.errorMessage)) {
764+
return { result: false, error: err.errorMessage };
765+
}
766+
767+
throw err;
768+
}
769+
}
770+
771+
export async function createBot({
772+
manager, name, username, isViaDeepLink,
773+
}: {
774+
manager: ApiUser;
775+
name: string;
776+
username: string;
777+
isViaDeepLink?: true;
778+
}) {
779+
try {
780+
const result = await invokeRequest(new GramJs.bots.CreateBot({
781+
managerId: buildInputUser(manager.id, manager.accessHash),
782+
name,
783+
username,
784+
viaDeeplink: isViaDeepLink,
785+
}), {
786+
shouldThrow: true,
787+
});
788+
789+
if (!(result instanceof GramJs.User)) return undefined;
790+
791+
addUserToLocalDb(result);
792+
793+
return buildApiUser(result);
794+
} catch (err) {
795+
if (!checkErrorType(err)) return undefined;
796+
return wrapError(err);
797+
}
798+
}
799+
752800
async function invokeUrlAuthRequest(
753801
request: GramJs.messages.RequestUrlAuth | GramJs.messages.AcceptUrlAuth,
754802
): Promise<ApiUrlAuthResult | undefined> {

‎src/api/types/messageActions.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ export interface ApiMessageActionRequestedPeer extends ActionMediaType {
128128
peerIds: string[];
129129
}
130130

131+
export interface ApiMessageActionManagedBotCreated extends ActionMediaType {
132+
type: 'managedBotCreated';
133+
botId: string;
134+
}
135+
131136
export interface ApiMessageActionContactSignUp extends ActionMediaType {
132137
type: 'contactSignUp';
133138
}
@@ -443,7 +448,7 @@ export type ApiMessageAction = ApiMessageActionUnsupported | ApiMessageActionCha
443448
| ApiMessageActionGameScore | ApiMessageActionPaymentSent | ApiMessageActionPhoneCall
444449
| ApiMessageActionScreenshotTaken | ApiMessageActionCustomAction | ApiMessageActionBotAllowed
445450
| ApiMessageActionBoostApply | ApiMessageActionContactSignUp | ApiMessageActionExpiredContent
446-
| ApiMessageActionRequestedPeer
451+
| ApiMessageActionRequestedPeer | ApiMessageActionManagedBotCreated
447452
| ApiMessageActionGroupCall | ApiMessageActionInviteToGroupCall | ApiMessageActionGroupCallScheduled
448453
| ApiMessageActionChatJoinedByRequest | ApiMessageActionChatJoinedViaCommunity
449454
| ApiMessageActionWebViewDataSent | ApiMessageActionGiftPremium

‎src/api/types/misc.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ export interface ApiAppConfig {
377377
aiComposeToneExamplesNum?: number;
378378
aiComposeToneTitleLengthMax?: number;
379379
aiComposeTonePromptLengthMax?: number;
380+
botAllowedSuffixes?: string[];
380381
}
381382

382383
export interface ApiConfig {
@@ -511,17 +512,18 @@ export type ApiLimitType =
511512
| 'savedDialogsPinned'
512513
| 'maxReactions'
513514
| 'moreAccounts'
514-
| 'aiComposeToneSaved';
515+
| 'aiComposeToneSaved'
516+
| 'botsCreate';
515517

516518
export type ApiLimitTypeWithModal = Exclude<ApiLimitType, (
517519
'messageLength' | 'captionLength' | 'aboutLength' | 'stickersFaved' | 'savedGifs' | 'recommendedChannels'
518520
| 'moreAccounts'
519-
| 'maxReactions' | 'aiComposeToneSaved'
521+
| 'maxReactions' | 'aiComposeToneSaved' | 'botsCreate'
520522
)>;
521523

522524
export type ApiLimitTypeForPromo = Exclude<ApiLimitType,
523525
'uploadMaxFileparts' | 'messageLength' | 'chatlistInvites' | 'chatlistJoined' | 'savedDialogsPinned'
524-
| 'maxReactions' | 'aiComposeToneSaved'
526+
| 'maxReactions' | 'aiComposeToneSaved' | 'botsCreate'
525527
>;
526528

527529
export type ApiPeerNotifySettings = {

‎src/api/types/users.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export interface ApiUser {
4949
paidMessagesStars?: number;
5050
isBotForum?: boolean;
5151
canManageBotForumTopics?: boolean;
52+
canManageBots?: boolean;
5253
isGuestChatBot?: boolean;
5354
isGuardBot?: boolean;
5455
}
@@ -93,6 +94,7 @@ export interface ApiUserFullInfo {
9394
noForwardsMyEnabled?: boolean;
9495
noForwardsPeerEnabled?: boolean;
9596
isUnofficialSecurityRisk?: boolean;
97+
botManagerId?: string;
9698
}
9799

98100
export type ApiUserType = 'userTypeBot' | 'userTypeRegular' | 'userTypeDeleted' | 'userTypeUnknown';

‎src/assets/localization/fallback.strings‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,12 +263,44 @@
263263
"RequestPeerChooseChannel_one" = "Choose a Channel";
264264
"RequestPeerChooseChannel_other" = "Choose Channels";
265265
"ActionRequestedPeer" = "You shared {peer} with {bot}";
266+
"ProfileBotManagedBy" = "Created and managed by {bot}";
267+
"ActionManagedBotCreated" = "{from} created {bot}";
268+
"ActionManagedBotCreatedYou" = "You created {bot}";
266269
"RequestPeerConfirm" = "Are you sure you want to send **{peer}** to **{bot}**?";
267270
"RequestPeerConfirmAddBot" = "This will also add **{bot}** to **{peer}**.";
268271
"RequestPeerConfirmBotRights" = "This will also add **{bot}** to **{peer}** with the following admin rights: {rights}.";
269272
"RequestPeerSelectLimit_one" = "You can select up to {count}.";
270273
"RequestPeerSelectLimit_other" = "You can select up to {count}.";
271274
"RequestPeerRequirements" = "Requirements";
275+
"RequestPeerReqPremium" = "The user should have a Premium subscription.";
276+
"RequestPeerReqNoPremium" = "The user should not have a Premium subscription.";
277+
"RequestPeerReqPublicChannel" = "The channel should be public.";
278+
"RequestPeerReqPrivateChannel" = "The channel should be private.";
279+
"RequestPeerReqPublicGroup" = "The group should be public.";
280+
"RequestPeerReqPrivateGroup" = "The group should be private.";
281+
"RequestPeerReqForum" = "The group should have topics turned on.";
282+
"RequestPeerReqNoForum" = "The group should have topics turned off.";
283+
"RequestPeerReqOwnerChannel" = "You should be the owner of the channel.";
284+
"RequestPeerReqOwnerGroup" = "You should be the owner of the group.";
285+
"RequestPeerReqAdminRights" = "You should have these admin rights: {rights}.";
286+
"RequestPeerReqBotMember" = "Bot should be in the group.";
287+
"CreateBotTitle" = "Create Bot";
288+
"CreateBotText" = "{bot} would like to create and manage a chatbot on your behalf.";
289+
"CreateBotName" = "Bot Name";
290+
"CreateBotUsername" = "Bot Username";
291+
"CreateBotUsernameChecking" = "Checking...";
292+
"CreateBotUsernameSuffix" = "Sorry, the username must end in 'bot'.";
293+
"CreateBotUsernameHint" = "Must end in 'bot' – or {suffixes} with {premium}.";
294+
"CreateBotUsernamePremium" = "Usernames ending in '{suffix}' require Telegram Premium. {link}";
295+
"CreateBotUsernameSubscribe" = "Subscribe";
296+
"CreateBotUsernameHelp" = "Choose a username for your bot. It must end in 'bot'. Like this, for example: TetrisBot or tetris_bot.";
297+
"CreateBotCreatedTitle" = "{name} created!";
298+
"CreateBotCreatedText" = "**{bot}** will manage this bot for you.";
299+
"CreateBotUnsupported" = "**{bot}** doesn't support Bot Management mode.";
300+
"CreateBotLimitTitle" = "Bot Limit Exceeded";
301+
"CreateBotLimitText_one" = "You can create up to {count} bot, or delete your current ones via @BotFather";
302+
"CreateBotLimitText_other" = "You can create up to {count} bots, or delete your current ones via @BotFather";
303+
"CreateBotLimitTextPremium" = "Subscribe to **Premium** to create up to {premiumLimit} bots, or delete one of your {limit} bots via @BotFather";
272304
"RequestPeerReqPremium" = "The user must have a Premium subscription.";
273305
"RequestPeerReqNoPremium" = "The user must not have a Premium subscription.";
274306
"RequestPeerReqPublicChannel" = "The channel must be public.";
@@ -1298,6 +1330,11 @@
12981330
"AttachGiveawayResults" = "Giveaway Results";
12991331
"ServiceNotifications" = "service notifications";
13001332
"Bot" = "bot";
1333+
"BotStatusApp" = "App";
1334+
"BotStatusAgent" = "Agent";
1335+
"BotStatusAi" = "AI";
1336+
"BotStatusGame" = "Game";
1337+
"BotStatusNonBinary" = "Non-binary";
13011338
"ALongTimeAgo" = "last seen a long time ago";
13021339
"WithinAMonth" = "last seen within a month";
13031340
"WithinAWeek" = "last seen within a week";

‎src/bundles/extra.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,5 @@ export { default as LeaveGroupModal } from '../components/modals/leaveGroup/Leav
123123
export { default as TwoFaCheckModal } from '../components/modals/twoFaCheck/TwoFaCheckModal';
124124
export { default as QuickChatPickerModal } from '../components/modals/quickChatPicker/QuickChatPickerModal';
125125
export { default as RequestPeerModal } from '../components/modals/requestPeer/RequestPeerModal';
126+
export { default as BotCreationModal } from '../components/modals/botCreation/BotCreationModal';
126127
export { default as CocoonModal } from '../components/modals/cocoon/CocoonModal';

‎src/components/common/profile/ChatExtra.module.scss‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@
5656
@include mixins.with-vt-type('chatExtra');
5757
}
5858

59+
.botManager {
60+
@include mixins.with-vt-type('chatExtra');
61+
}
62+
63+
.botManagerIcon {
64+
margin-inline-end: 0.25rem;
65+
vertical-align: -0.125rem;
66+
}
67+
5968
.botVerificationIcon {
6069
--custom-emoji-size: 1rem;
6170

0 commit comments

Comments
 (0)