Monetization Statistics: Implement verification modal for withdraw button (#4929)

This commit is contained in:
Alexander Zinchuk 2024-09-19 20:43:16 +02:00
parent 505f67674a
commit 09203ae76e
25 changed files with 439 additions and 26 deletions

View File

@ -77,6 +77,7 @@ export interface GramJsAppConfig extends LimitsConfig {
group_transcribe_level_min?: number;
new_noncontact_peers_require_premium_without_ownpremium?: boolean;
channel_restrict_sponsored_level_min?: number;
channel_revenue_withdrawal_enabled?: boolean;
// Upload premium notifications
upload_premium_speedup_notify_period?: number;
upload_premium_speedup_download?: number;
@ -161,5 +162,6 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
bandwidthPremiumUploadSpeedup: appConfig.upload_premium_speedup_upload,
bandwidthPremiumDownloadSpeedup: appConfig.upload_premium_speedup_download,
channelRestrictAdsLevelMin: appConfig.channel_restrict_sponsored_level_min,
isChannelRevenueWithdrawalEnabled: appConfig.channel_revenue_withdrawal_enabled,
};
}

View File

@ -2,7 +2,7 @@ import {
Api as GramJs,
sessions,
} from '../../../lib/gramjs';
import type { TwoFaParams } from '../../../lib/gramjs/client/2fa';
import type { TwoFaParams, TwoFaPasswordParams } from '../../../lib/gramjs/client/2fa';
import TelegramClient from '../../../lib/gramjs/client/TelegramClient';
import { Logger as GramJsLogger } from '../../../lib/gramjs/extensions/index';
@ -369,6 +369,10 @@ export function getTmpPassword(currentPassword: string, ttl?: number) {
return client.getTmpPassword(currentPassword, ttl);
}
export function getCurrentPassword(params: TwoFaPasswordParams) {
return client.getCurrentPassword(params);
}
export function abortChatRequests(params: { chatId: string; threadId?: ThreadId }) {
const { chatId, threadId } = params;
const controller = CHAT_ABORT_CONTROLLERS.get(chatId);

View File

@ -17,6 +17,7 @@ import {
} from '../apiBuilders/statistics';
import { buildInputEntity, buildInputPeer } from '../gramjsBuilders';
import { invokeRequest } from './client';
import { getPassword, onPasswordError } from './twoFaSettings';
export async function fetchChannelStatistics({
chat, dcId,
@ -211,3 +212,32 @@ export async function fetchStoryPublicForwards({
nextOffset: result.nextOffset,
};
}
export async function loadMonetizationRevenueWithdrawalUrl({
chat, currentPassword,
}: { chat: ApiChat; currentPassword: string }) {
try {
const password = await getPassword(currentPassword);
if (!password || 'error' in password) {
return undefined;
}
const result = await invokeRequest(new GramJs.stats.GetBroadcastRevenueWithdrawalUrl({
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
password,
}), {
shouldThrow: true,
});
if (!result) {
return undefined;
}
return result;
} catch (err: any) {
onPasswordError(err);
}
return undefined;
}

View File

@ -2,7 +2,9 @@ import { Api as GramJs, errors } from '../../../lib/gramjs';
import { DEBUG } from '../../../config';
import { sendApiUpdate } from '../updates/apiUpdateEmitter';
import { getTmpPassword, invokeRequest, updateTwoFaSettings } from './client';
import {
getCurrentPassword, getTmpPassword, invokeRequest, updateTwoFaSettings,
} from './client';
const ApiErrors: { [k: string]: string } = {
EMAIL_UNCONFIRMED: 'Email unconfirmed',
@ -11,6 +13,7 @@ const ApiErrors: { [k: string]: string } = {
NEW_SETTINGS_INVALID: 'The new password settings are invalid',
CODE_INVALID: 'Invalid Code',
PASSWORD_HASH_INVALID: 'Invalid Password',
PASSWORD_MISSING: 'You must enable 2FA before executing this operation',
};
const emailCodeController: {
@ -45,6 +48,18 @@ export function getTemporaryPaymentPassword(password: string, ttl?: number) {
return getTmpPassword(password, ttl);
}
export function getPassword(password: string) {
try {
return getCurrentPassword({
currentPassword: password,
onPasswordCodeError: onPasswordError,
});
} catch (err: any) {
onPasswordError(err);
return undefined;
}
}
export async function checkPassword(currentPassword: string) {
try {
await updateTwoFaSettings({ isCheckPassword: true, currentPassword });
@ -134,3 +149,28 @@ function onError(err: Error) {
message,
});
}
export function onPasswordError(err: Error) {
let message: string;
if (err instanceof errors.PasswordModifiedError) {
const hours = Math.ceil(Number(err.seconds) / 60 / 60);
message = `Too many attempts. Try again in ${hours > 1 ? `${hours} hours` : 'an hour'}`;
} else {
message = ApiErrors[err.message];
}
if (!message) {
message = 'Unexpected Error';
if (DEBUG) {
// eslint-disable-next-line no-console
console.error(err);
}
}
sendApiUpdate({
'@type': 'updatePasswordError',
error: message,
});
}

View File

@ -210,6 +210,7 @@ export interface ApiAppConfig {
bandwidthPremiumUploadSpeedup?: number;
bandwidthPremiumDownloadSpeedup?: number;
channelRestrictAdsLevelMin?: number;
isChannelRevenueWithdrawalEnabled?: boolean;
}
export interface ApiConfig {

View File

@ -481,6 +481,11 @@ export type ApiUpdateTwoFaError = {
message: string;
};
export type ApiUpdatePasswordError = {
'@type': 'updatePasswordError';
error: string;
};
export type ApiUpdateNotifySettings = {
'@type': 'updateNotifySettings';
peerType: 'contact' | 'group' | 'broadcast';
@ -776,7 +781,7 @@ export type ApiUpdate = (
ApiUpdateRecentStickers | ApiUpdateSavedGifs | ApiUpdateNewScheduledMessage | ApiUpdateMoveStickerSetToTop |
ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage |
ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateMessageTranslations |
ApiUpdateTwoFaError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent |
ApiUpdateTwoFaError | ApiUpdatePasswordError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent |
ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy |
ApiUpdateServerTimeOffset | ApiUpdateShowInvite | ApiUpdateMessageReactions | ApiUpdateSavedReactionTags |
ApiUpdateGroupCallParticipants | ApiUpdateGroupCallConnection | ApiUpdateGroupCall | ApiUpdateGroupCallStreams |

View File

@ -32,6 +32,7 @@ export { default as StarsTransactionInfoModal } from '../components/modals/stars
export { default as AboutAdsModal } from '../components/common/AboutAdsModal';
export { default as AboutMonetizationModal } from '../components/common/AboutMonetizationModal';
export { default as VerificationMonetizationModal } from '../components/common/VerificationMonetizationModal';
export { default as ReportAdModal } from '../components/modals/reportAd/ReportAdModal';
export { default as CalendarModal } from '../components/common/CalendarModal';
export { default as DeleteMessageModal } from '../components/common/DeleteMessageModal';

View File

@ -0,0 +1,18 @@
import type { FC } from '../../lib/teact/teact';
import React from '../../lib/teact/teact';
import type { OwnProps } from './VerificationMonetizationModal';
import { Bundles } from '../../util/moduleLoader';
import useModuleLoader from '../../hooks/useModuleLoader';
const VerificationMonetizationModalAsync: FC<OwnProps> = (props) => {
const { isOpen } = props;
const VerificationMonetizationModal = useModuleLoader(Bundles.Extra, 'VerificationMonetizationModal', !isOpen);
// eslint-disable-next-line react/jsx-props-no-spreading
return VerificationMonetizationModal ? <VerificationMonetizationModal {...props} /> : undefined;
};
export default VerificationMonetizationModalAsync;

View File

@ -0,0 +1,9 @@
.root {
:global(.modal-dialog) {
max-width: 25rem;
}
}
.content {
padding-top: 0.5rem;
}

View File

@ -0,0 +1,87 @@
import type { FC } from '../../lib/teact/teact';
import React, {
memo,
useState,
} from '../../lib/teact/teact';
import { getActions } from '../../global';
import buildClassName from '../../util/buildClassName';
import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
import Modal from '../ui/Modal';
import PasswordForm from './PasswordForm';
import styles from './VerificationMonetizationModal.module.scss';
export type OwnProps = {
isOpen: boolean;
onClose: NoneToVoidFunction;
chatId: string;
passwordHint?: string;
error?: string;
isLoading?: boolean;
};
const VerificationMonetizationModal: FC<OwnProps> = ({
isOpen,
chatId,
onClose,
passwordHint,
error,
isLoading,
}) => {
const {
clearMonetizationInfo, loadMonetizationRevenueWithdrawalUrl,
} = getActions();
const lang = useOldLang();
const [shouldShowPassword, setShouldShowPassword] = useState(false);
const handleSubmit = useLastCallback((password: string) => {
loadMonetizationRevenueWithdrawalUrl({
chatId,
currentPassword: password,
onSuccess: () => {
onClose();
},
});
});
const handleClearError = useLastCallback(() => {
clearMonetizationInfo();
});
return (
<Modal
isOpen={isOpen}
hasCloseButton
title={lang('EnterPassword')}
className={styles.root}
contentClassName={styles.content}
onClose={onClose}
>
<div className={buildClassName(styles.content, 'settings-content password-form custom-scroll')}>
<div className="settings-item pt-0">
<PasswordForm
shouldShowSubmit
placeholder={lang('Password')}
error={error && lang(error)}
description={lang('Channel.OwnershipTransfer.EnterPasswordText')}
clearError={handleClearError}
isLoading={isLoading}
hint={passwordHint}
isPasswordVisible={shouldShowPassword}
shouldResetValue={isOpen}
onChangePasswordVisibility={setShouldShowPassword}
onSubmit={handleSubmit}
/>
</div>
</div>
</Modal>
);
};
export default memo(VerificationMonetizationModal);

View File

@ -5,19 +5,22 @@ import { getActions, withGlobal } from '../../../global';
import type { ApiChannelMonetizationStatistics, StatisticsGraph } from '../../../api/types';
import { FRAGMENT_ADS_URL } from '../../../config';
import { selectChat, selectChatFullInfo, selectTabState } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import renderText from '../../common/helpers/renderText';
import useFlag from '../../../hooks/useFlag';
import useForceUpdate from '../../../hooks/useForceUpdate';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import AboutMonetizationModal from '../../common/AboutMonetizationModal.async';
import Icon from '../../common/icons/Icon';
import SafeLink from '../../common/SafeLink';
import VerificationMonetizationModal from '../../common/VerificationMonetizationModal.async';
import Button from '../../ui/Button';
import ConfirmDialog from '../../ui/ConfirmDialog';
import Link from '../../ui/Link';
import Loading from '../../ui/Loading';
import StatisticsOverview from './StatisticsOverview';
@ -47,29 +50,46 @@ type StateProps = {
chatId: string;
dcId?: number;
statistics?: ApiChannelMonetizationStatistics;
canCollect?: boolean;
isCreator?: boolean;
isChannelRevenueWithdrawalEnabled?: boolean;
hasPassword?: boolean;
passwordHint?: string;
error?: string;
isLoading?: boolean;
};
const MonetizationStatistics = ({
chatId,
dcId,
statistics,
canCollect,
isCreator,
isChannelRevenueWithdrawalEnabled,
hasPassword,
passwordHint,
error,
isLoading,
}: StateProps) => {
const { loadChannelMonetizationStatistics } = getActions();
const { loadChannelMonetizationStatistics, loadPasswordInfo } = getActions();
const oldLang = useOldLang();
const lang = useLang();
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
const [isReady, setIsReady] = useState(false);
const loadedCharts = useRef<string[]>([]);
const forceUpdate = useForceUpdate();
const [isAboutMonetizationModalOpen, openAboutMonetizationModal, closeAboutMonetizationModal] = useFlag(false);
const hasAvailableBalance = Boolean(statistics?.balances?.availableBalance !== 0);
const [
isVerificationMonetizationModalOpen, openVerificationMonetizationModal, closeVerificationMonetizationModal,
] = useFlag(false);
const [isConfirmPasswordDialogOpen, openConfirmPasswordDialogOpen, closeConfirmPasswordDialogOpen] = useFlag();
const availableBalance = statistics?.balances?.availableBalance;
const canWithdraw = isCreator && isChannelRevenueWithdrawalEnabled && Boolean(availableBalance);
useEffect(() => {
if (chatId) {
loadChannelMonetizationStatistics({ chatId });
loadPasswordInfo();
}
}, [chatId, loadChannelMonetizationStatistics]);
@ -86,7 +106,7 @@ const MonetizationStatistics = ({
return;
}
MONETIZATION_GRAPHS.forEach((name, index: number) => {
MONETIZATION_GRAPHS.filter(Boolean).forEach((name, index: number) => {
const graph = statistics[name as keyof typeof statistics];
const isAsync = typeof graph === 'string';
@ -115,7 +135,6 @@ const MonetizationStatistics = ({
}, [isReady, statistics, oldLang, chatId, dcId, forceUpdate]);
function renderAvailableReward() {
const availableBalance = statistics?.balances?.availableBalance;
const [integerTonPart, decimalTonPart] = availableBalance ? availableBalance.toFixed(4).split('.') : [0];
const [integerUsdPart, decimalUsdPart] = availableBalance
&& statistics?.usdRate ? (availableBalance * statistics.usdRate).toFixed(2).split('.') : [0];
@ -172,6 +191,14 @@ const MonetizationStatistics = ({
);
}, [lang, oldLang]);
const verificationMonetizationHandler = useLastCallback(() => {
if (hasPassword) {
openVerificationMonetizationModal();
} else {
openConfirmPasswordDialogOpen();
}
});
if (!isReady || !statistics) {
return <Loading />;
}
@ -190,29 +217,49 @@ const MonetizationStatistics = ({
{!loadedCharts.current.length && <Loading />}
<div ref={containerRef} className={styles.section}>
{MONETIZATION_GRAPHS.map((graph) => (
{MONETIZATION_GRAPHS.filter(Boolean).map((graph) => (
<div key={graph} className={buildClassName(styles.graph, styles.hidden)} />
))}
</div>
{hasAvailableBalance && (
<div className={styles.section}>
{oldLang('lng_channel_earn_balance_title')}
<div className={styles.section}>
{oldLang('lng_channel_earn_balance_title')}
{renderAvailableReward()}
{renderAvailableReward()}
<Button size="smaller" type="button" href={FRAGMENT_ADS_URL} disabled={canCollect && hasAvailableBalance}>
{oldLang('MonetizationWithdraw')}
</Button>
<Button
size="smaller"
type="button"
onClick={verificationMonetizationHandler}
disabled={!canWithdraw}
>
{oldLang('MonetizationWithdraw')}
</Button>
<div className={styles.textBottom}>{rewardsText}</div>
</div>
)}
<div className={styles.textBottom}>{rewardsText}</div>
</div>
<AboutMonetizationModal
isOpen={isAboutMonetizationModalOpen}
onClose={closeAboutMonetizationModal}
/>
<VerificationMonetizationModal
chatId={chatId}
isOpen={isVerificationMonetizationModalOpen}
onClose={closeVerificationMonetizationModal}
passwordHint={passwordHint}
error={error}
isLoading={isLoading}
/>
<ConfirmDialog
isOnlyConfirm
isOpen={isConfirmPasswordDialogOpen}
onClose={closeConfirmPasswordDialogOpen}
confirmHandler={closeConfirmPasswordDialogOpen}
confirmLabel={lang('OK')}
>
<p>{renderText(oldLang('Monetization.Withdraw.Error.Text'), ['br'])}</p>
</ConfirmDialog>
</div>
);
};
@ -220,18 +267,38 @@ const MonetizationStatistics = ({
export default memo(withGlobal(
(global): StateProps => {
const tabState = selectTabState(global);
const {
settings: {
byKey: {
hasPassword,
},
},
twoFaSettings: {
hint: passwordHint,
},
} = global;
const isLoading = global.monetizationInfo?.isLoading;
const error = global.monetizationInfo?.error;
const monetizationStatistics = tabState.monetizationStatistics;
const chatId = monetizationStatistics && monetizationStatistics.chatId;
const chat = chatId ? selectChat(global, chatId) : undefined;
const canCollect = chat && chat.isCreator;
const dcId = selectChatFullInfo(global, chatId!)?.statisticsDcId;
const isCreator = Boolean(chat?.isCreator);
const statistics = tabState.statistics.monetization;
const isChannelRevenueWithdrawalEnabled = global.appConfig?.isChannelRevenueWithdrawalEnabled;
return {
chatId: chatId!,
dcId,
statistics,
canCollect,
isCreator,
isChannelRevenueWithdrawalEnabled,
hasPassword,
passwordHint,
error,
isLoading,
};
},
)(MonetizationStatistics));

View File

@ -38,5 +38,6 @@ import './apiUpdaters/symbols';
import './apiUpdaters/misc';
import './apiUpdaters/settings';
import './apiUpdaters/twoFaSettings';
import './apiUpdaters/password';
import './apiUpdaters/calls';
import './apiUpdaters/payments';

View File

@ -1,3 +1,5 @@
import type { ActionReturnType } from '../../types';
import { areDeepEqual } from '../../../util/areDeepEqual';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { callApi } from '../../../api/gramjs';
@ -5,6 +7,7 @@ import { addActionHandler, getGlobal, setGlobal } from '../../index';
import {
updateChannelMonetizationStatistics,
updateMessageStatistics,
updateMonetizationInfo,
updateStatistics,
updateStatisticsGraph,
updateStoryStatistics,
@ -224,3 +227,41 @@ addActionHandler('loadStoryPublicForwards', async (global, actions, payload): Pr
}, tabId);
setGlobal(global);
});
addActionHandler('loadMonetizationRevenueWithdrawalUrl', async (global, actions, payload): Promise<void> => {
const {
chatId, currentPassword, onSuccess, tabId = getCurrentTabId(),
} = payload;
global = updateMonetizationInfo(global, { isLoading: true, error: undefined });
setGlobal(global);
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
const result = await callApi('loadMonetizationRevenueWithdrawalUrl', { chat, currentPassword });
if (!result) {
return;
}
global = getGlobal();
global = updateMonetizationInfo(global, { isLoading: false });
setGlobal(global);
if (result) {
onSuccess();
actions.openUrl({
url: result.url,
shouldSkipModal: true,
tabId,
ignoreDeepLinks: true,
});
}
});
addActionHandler('clearMonetizationInfo', (global): ActionReturnType => {
return updateMonetizationInfo(global, { error: undefined });
});

View File

@ -0,0 +1,20 @@
import type { ActionReturnType } from '../../types';
import { addActionHandler } from '../../index';
addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
switch (update['@type']) {
case 'updatePasswordError': {
return {
...global,
monetizationInfo: {
...global.monetizationInfo,
isLoading: false,
error: update.error,
},
};
}
}
return undefined;
});

View File

@ -297,6 +297,8 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
isMinimized: false,
isHidden: false,
},
monetizationInfo: {},
};
export const INITIAL_TAB_STATE: TabState = {

View File

@ -12,4 +12,5 @@ export * from './payments';
export * from './statistics';
export * from './stories';
export * from './translations';
export * from './password';
export * from './general';

View File

@ -0,0 +1,14 @@
import type { GlobalState } from '../types';
export function updateMonetizationInfo<T extends GlobalState>(
global: T,
update: GlobalState['monetizationInfo'],
): T {
return {
...global,
monetizationInfo: {
...global.monetizationInfo,
...update,
},
};
}

View File

@ -872,6 +872,11 @@ export type GlobalState = {
waitingEmailCodeLength?: number;
};
monetizationInfo: {
isLoading?: boolean;
error?: string;
};
attachmentSettings: {
shouldCompress: boolean;
shouldSendGrouped: boolean;
@ -1285,6 +1290,7 @@ export interface ActionPayloads {
updatePerformanceSettings: Partial<PerformanceType>;
loadPasswordInfo: undefined;
clearTwoFaError: undefined;
clearMonetizationInfo: undefined;
updatePassword: {
currentPassword: string;
password: string;
@ -1825,6 +1831,12 @@ export interface ActionPayloads {
chatId: string;
} & WithTabId;
loadMonetizationRevenueWithdrawalUrl: {
chatId: string;
currentPassword: string;
onSuccess: VoidFunction;
} & WithTabId;
// ui
dismissDialog: WithTabId | undefined;
setNewChatMembersDialogState: {

View File

@ -1,9 +1,11 @@
import type TelegramClient from './TelegramClient';
import errors from '../errors';
// eslint-disable-next-line import/no-named-default
import { default as Api } from '../tl/api';
import { generateRandomBytes } from '../Helpers';
import { computeCheck, computeDigest } from '../Password';
import errors from '../errors';
export interface TwoFaParams {
isCheckPassword?: boolean;
@ -15,7 +17,13 @@ export interface TwoFaParams {
onEmailCodeError?: (err: Error) => void;
}
export interface TwoFaPasswordParams {
currentPassword?: string;
onPasswordCodeError?: (err: Error) => void;
}
export type TmpPasswordResult = Api.account.TmpPassword | { error: string } | undefined;
export type PasswordResult = Api.account.Password | { error: string } | undefined;
/**
* Changes the 2FA settings of the logged in user.
@ -147,3 +155,29 @@ export async function getTmpPassword(client: TelegramClient, currentPassword: st
throw err;
}
}
export async function getCurrentPassword(
client: TelegramClient,
{
currentPassword,
onPasswordCodeError,
}: TwoFaPasswordParams,
) {
const pwd = await client.invoke(new Api.account.GetPassword());
if (!pwd) {
return undefined;
}
try {
return currentPassword ? await computeCheck(pwd, currentPassword!) : new Api.InputCheckPasswordEmpty();
} catch (err: any) {
if (err instanceof errors.PasswordModifiedError) {
return onPasswordCodeError!(err);
} else if (err.message === 'PASSWORD_HASH_INVALID') {
return { error: err.message };
} else {
throw err;
}
}
}

View File

@ -1,4 +1,6 @@
import type { TmpPasswordResult, TwoFaParams, updateTwoFaSettings } from './2fa';
import type {
PasswordResult, TmpPasswordResult, TwoFaParams, TwoFaPasswordParams, updateTwoFaSettings,
} from './2fa';
import type { BotAuthParams, UserAuthParams } from './auth';
import type { downloadFile, DownloadFileParams } from './downloadFile';
import type { uploadFile, UploadFileParams } from './uploadFile';
@ -24,6 +26,8 @@ declare class TelegramClient {
async getTmpPassword(currentPassword: string, ttl?: number): Promise<TmpPasswordResult>;
async getCurrentPassword(Params: TwoFaPasswordParams): Promise<PasswordResult>;
setPingCallback(callback: () => Promise<void>);
setForceHttpTransport: (forceHttpTransport: boolean) => void;

View File

@ -26,6 +26,7 @@ const { uploadFile } = require('./uploadFile');
const {
updateTwoFaSettings,
getTmpPassword,
getCurrentPassword,
} = require('./2fa');
const RequestState = require('../network/RequestState');
const Deferred = require('../../../util/Deferred').default;
@ -1118,6 +1119,10 @@ class TelegramClient {
return getTmpPassword(this, currentPassword, ttl);
}
getCurrentPassword(currentPassword) {
return getCurrentPassword(this, currentPassword);
}
// event region
addEventHandler(callback, event) {
this._eventBuilders.push([event, callback]);

View File

@ -101,6 +101,16 @@ class EmailUnconfirmedError extends BadRequestError {
}
}
class PasswordModifiedError extends BadRequestError {
constructor(args) {
const seconds = Number(args.capture || 0);
super(`The password was modified less than 24 hours ago, try again in ${seconds} seconds.`);
// eslint-disable-next-line max-len
this.message = `The password was modified less than 24 hours ago, try again in ${seconds} seconds.`;
this.seconds = seconds;
}
}
const rpcErrorRe = [
[/FILE_MIGRATE_(\d+)/, FileMigrateError],
[/FLOOD_TEST_PHONE_WAIT_(\d+)/, FloodTestPhoneWaitError],
@ -112,6 +122,7 @@ const rpcErrorRe = [
[/USER_MIGRATE_(\d+)/, UserMigrateError],
[/NETWORK_MIGRATE_(\d+)/, NetworkMigrateError],
[/EMAIL_UNCONFIRMED_(\d+)/, EmailUnconfirmedError],
[/PASSWORD_TOO_FRESH_(\d+)/, PasswordModifiedError],
[/^Timeout$/, TimedOutError],
];
module.exports = {
@ -126,4 +137,5 @@ module.exports = {
NetworkMigrateError,
MsgWaitError,
EmailUnconfirmedError,
PasswordModifiedError,
};

View File

@ -1624,8 +1624,8 @@ bots.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:fla
bots.canSendMessage#1359f4e6 bot:InputUser = Bool;
bots.allowSendMessage#f132e3ef bot:InputUser = Updates;
bots.invokeWebViewCustomMethod#87fc5e7 bot:InputUser custom_method:string params:DataJSON = DataJSON;
bots.getPreviewMedias#a2a5594d bot:InputUser = Vector<BotPreviewMedia>;
bots.getPopularAppBots#c2510192 offset:string limit:int = bots.PopularAppBots;
bots.getPreviewMedias#a2a5594d bot:InputUser = Vector<BotPreviewMedia>;
payments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm;
payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt;
payments.validateRequestedInfo#b6c8f12b flags:# save:flags.0?true invoice:InputInvoice info:PaymentRequestedInfo = payments.ValidatedRequestedInfo;
@ -1676,6 +1676,7 @@ stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel ms
stats.getStoryStats#374fef40 flags:# dark:flags.0?true peer:InputPeer id:int = stats.StoryStats;
stats.getStoryPublicForwards#a6437ef6 peer:InputPeer id:int offset:string limit:int = stats.PublicForwards;
stats.getBroadcastRevenueStats#75dfb671 flags:# dark:flags.0?true channel:InputChannel = stats.BroadcastRevenueStats;
stats.getBroadcastRevenueWithdrawalUrl#2a65ef73 channel:InputChannel password:InputCheckPasswordSRP = stats.BroadcastRevenueWithdrawalUrl;
chatlists.exportChatlistInvite#8472478e chatlist:InputChatlist title:string peers:Vector<InputPeer> = chatlists.ExportedChatlistInvite;
chatlists.deleteExportedInvite#719c5c5e chatlist:InputChatlist slug:string = Bool;
chatlists.editExportedInvite#653db63d flags:# chatlist:InputChatlist slug:string title:flags.1?string peers:flags.2?Vector<InputPeer> = ExportedChatlistInvite;

View File

@ -282,6 +282,7 @@
"stats.getStoryStats",
"stats.getStoryPublicForwards",
"stats.loadAsyncGraph",
"stats.getBroadcastRevenueWithdrawalUrl",
"messages.getAttachMenuBots",
"messages.getAttachMenuBot",
"messages.toggleBotInAttachMenu",

View File

@ -20,6 +20,7 @@ const READABLE_ERROR_MESSAGES: Record<string, string> = {
MEDIA_NEW_INVALID: 'Failed to replace new media',
MESSAGE_NOT_MODIFIED: 'Message not modified. The new content is identical to the current one.',
MEDIA_INVALID: 'Media invalid',
PASSWORD_HASH_INVALID: 'Incorrect password',
PHOTO_EXT_INVALID: 'The extension of the photo is invalid',
PHOTO_INVALID_DIMENSIONS: 'The photo dimensions are invalid',
PHOTO_SAVE_FILE_INVALID: 'Internal issues, try again later',