Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7a4d67cfb | ||
|
|
688275ea81 | ||
|
|
266102df2f | ||
|
|
f7aadc352b | ||
|
|
1ae3af0e80 | ||
|
|
97d27fe869 | ||
|
|
fcd2e28abb | ||
|
|
2522e66969 | ||
|
|
b9250edb33 | ||
|
|
54cab2c5a5 | ||
|
|
6231db1411 | ||
|
|
ef5a395c60 | ||
|
|
a200771868 | ||
|
|
914e043abe | ||
|
|
9d66f9cc03 | ||
|
|
e708065446 | ||
|
|
9c64cdb9c4 | ||
|
|
9f8d61ab2f | ||
|
|
0143fd28af | ||
|
|
844d030332 | ||
|
|
ae18ece549 | ||
|
|
10a0c6a086 | ||
|
|
51189fd244 | ||
|
|
1e10529f20 | ||
|
|
8d701ebb4f | ||
|
|
269bb94138 | ||
|
|
9b98ff52ea | ||
|
|
52f2f96f36 | ||
|
|
b53e40f1bf | ||
|
|
4e0d11f517 | ||
|
|
1a24ba857c | ||
|
|
1e254b958e | ||
|
|
23140b3d6a | ||
|
|
8c901d8f71 | ||
|
|
856356ce75 | ||
|
|
b5a65a4519 | ||
|
|
d056c00c67 | ||
|
|
36fb6dac89 | ||
|
|
fcda883878 | ||
|
|
40c0286942 | ||
|
|
59df447fed | ||
|
|
a253d34c00 | ||
|
|
0a5eac50be | ||
|
|
156c3d288c | ||
|
|
154e5660de | ||
|
|
13e6b91ac7 | ||
|
|
a89ad5d0fb | ||
|
|
e11c27048b | ||
|
|
e8dd277a00 | ||
|
|
329db0d8e9 | ||
|
|
eaf3ea9289 | ||
|
|
4156beaa3c | ||
|
|
4115d3d13d | ||
|
|
5f01751660 | ||
|
|
7d4e23448e | ||
|
|
c7aa5ed544 | ||
|
|
1bfe409c93 | ||
|
|
e8d619c740 | ||
|
|
2dc3ec955a | ||
|
|
07ff7c6cb0 | ||
|
|
8d52ca6be6 | ||
|
|
df91b2bfeb | ||
|
|
9d02e539c8 | ||
|
|
241fee80a7 | ||
|
|
83786ddeaf | ||
|
|
0e9793b845 | ||
|
|
2b36dd660b | ||
|
|
35ffc03988 | ||
|
|
5a9d1a3fce | ||
|
|
6776d88688 | ||
|
|
affe9defb5 | ||
|
|
d3fdf433cd | ||
|
|
cec8114b99 | ||
|
|
0a1a5ed70e | ||
|
|
c587c011d2 | ||
|
|
c2fa149ffd | ||
|
|
c63c75018d | ||
|
|
b0077d98f0 | ||
|
|
b5bc7a22af | ||
|
|
e60311811b | ||
|
|
8700c1b08f | ||
|
|
d8b51670e7 | ||
|
|
68c2f563c6 | ||
|
|
bf775cb4ca | ||
|
|
5c4b81434e | ||
|
|
cd1c7c56d3 | ||
|
|
427ceb9a9a | ||
|
|
2a110f0d3e | ||
|
|
d0ed75f3b5 |
@@ -1,5 +1,5 @@
|
||||
diff --git a/pylib/gyp/generator/cmake.py b/pylib/gyp/generator/cmake.py
|
||||
index a2b9629..68d7020 100644
|
||||
index a2b9629..ac59461 100644
|
||||
--- a/pylib/gyp/generator/cmake.py
|
||||
+++ b/pylib/gyp/generator/cmake.py
|
||||
@@ -1070,6 +1070,23 @@ def WriteTarget(namer, qualified_target, target_dicts, build_dir, config_to_use,
|
||||
@@ -26,6 +26,43 @@ index a2b9629..68d7020 100644
|
||||
UnsetVariable(output, 'TOOLSET')
|
||||
UnsetVariable(output, 'TARGET')
|
||||
|
||||
@@ -1112,6 +1129,8 @@ def GenerateOutputForConfig(target_list, target_dicts, data,
|
||||
SetVariable(output, 'configuration', config_to_use)
|
||||
|
||||
ar = None
|
||||
+ ranlib = None
|
||||
+ nm = None
|
||||
cc = None
|
||||
cxx = None
|
||||
|
||||
@@ -1121,17 +1140,27 @@ def GenerateOutputForConfig(target_list, target_dicts, data,
|
||||
for key, value in make_global_settings:
|
||||
if key == 'AR':
|
||||
ar = os.path.join(build_to_top, value)
|
||||
+ if key == 'RANLIB':
|
||||
+ ranlib = os.path.join(build_to_top, value)
|
||||
+ if key == 'NM':
|
||||
+ nm = os.path.join(build_to_top, value)
|
||||
if key == 'CC':
|
||||
cc = os.path.join(build_to_top, value)
|
||||
if key == 'CXX':
|
||||
cxx = os.path.join(build_to_top, value)
|
||||
|
||||
ar = gyp.common.GetEnvironFallback(['AR_target', 'AR'], ar)
|
||||
+ ranlib = gyp.common.GetEnvironFallback(['RANLIB_target', 'RANLIB'], ranlib)
|
||||
+ nm = gyp.common.GetEnvironFallback(['NM_target', 'NM'], nm)
|
||||
cc = gyp.common.GetEnvironFallback(['CC_target', 'CC'], cc)
|
||||
cxx = gyp.common.GetEnvironFallback(['CXX_target', 'CXX'], cxx)
|
||||
|
||||
if ar:
|
||||
SetVariable(output, 'CMAKE_AR', ar)
|
||||
+ if ranlib:
|
||||
+ SetVariable(output, 'CMAKE_RANLIB', ranlib)
|
||||
+ if nm:
|
||||
+ SetVariable(output, 'CMAKE_NM', nm)
|
||||
if cc:
|
||||
SetVariable(output, 'CMAKE_C_COMPILER', cc)
|
||||
if cxx:
|
||||
diff --git a/pylib/gyp/generator/xcode.py b/pylib/gyp/generator/xcode.py
|
||||
index db99d6a..8d56baf 100644
|
||||
--- a/pylib/gyp/generator/xcode.py
|
||||
|
||||
3
Telegram/Resources/css/export_style.css
Normal file
3
Telegram/Resources/css/export_style.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.page_wrap {
|
||||
background-color: #fff;
|
||||
}
|
||||
@@ -469,6 +469,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_settings_calls_privacy" = "Phone calls privacy";
|
||||
"lng_settings_groups_invite_privacy" = "Group invite settings";
|
||||
"lng_settings_show_sessions" = "Show all sessions";
|
||||
"lng_settings_export_data" = "Export Telegram data";
|
||||
"lng_settings_self_destruct" = "Account self-destruct settings";
|
||||
"lng_settings_change_phone" = "Change phone number";
|
||||
|
||||
@@ -1567,7 +1568,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_passport_address_agreement" = "Tenancy agreement";
|
||||
"lng_passport_address_agreement_upload" = "Upload a scan of your tenancy agreement";
|
||||
"lng_passport_address_registration" = "Passport registration";
|
||||
"lng_passport_address_registration_upload" = "Upload a scan of your passport registration";
|
||||
"lng_passport_address_registration_upload" = "Upload a scan of your passport registration page";
|
||||
"lng_passport_address_temporary" = "Temporary registration";
|
||||
"lng_passport_address_temporary_upload" = "Upload a scan of your temporary registration";
|
||||
"lng_passport_address_about" = "To confirm your address, please upload a scan or photo of the selected document (all pages).";
|
||||
@@ -1594,12 +1595,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_passport_upload_selfie" = "Upload selfie";
|
||||
"lng_passport_front_side_title" = "Front side";
|
||||
"lng_passport_front_side_name" = "Scan";
|
||||
"lng_passport_front_side_description" = "Upload front side of your document.";
|
||||
"lng_passport_upload_front_side" = "Upload front side scan";
|
||||
"lng_passport_front_side_description" = "Upload the front side of your document.";
|
||||
"lng_passport_upload_front_side" = "Upload a scan of the front side";
|
||||
"lng_passport_reverse_side_title" = "Reverse side";
|
||||
"lng_passport_reverse_side_name" = "Scan";
|
||||
"lng_passport_reverse_side_description" = "Upload reverse side of your document.";
|
||||
"lng_passport_upload_reverse_side" = "Upload reverse side scan";
|
||||
"lng_passport_reverse_side_description" = "Upload the reverse side of your document.";
|
||||
"lng_passport_upload_reverse_side" = "Upload a scan of the reverse side";
|
||||
"lng_passport_personal_details" = "Personal details";
|
||||
"lng_passport_personal_details_enter" = "Enter your personal details";
|
||||
"lng_passport_choose_image" = "Choose scan image";
|
||||
@@ -1648,10 +1649,79 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_passport_stop" = "Stop";
|
||||
"lng_passport_restart_sure" = "An unexpected error has occurred. Perhaps some changes were made from a different Telegram application. Would you like to restart this authorization?";
|
||||
"lng_passport_restart" = "Restart";
|
||||
"lng_passport_error_too_large" = "This file is too large.";
|
||||
"lng_passport_error_bad_size" = "This image has bad dimensions.";
|
||||
"lng_passport_error_too_large" = "Sorry, this file is too large.";
|
||||
"lng_passport_error_bad_size" = "Sorry, this image has wrong dimensions.";
|
||||
"lng_passport_error_cant_read" = "Can't read this file. Please choose an image.";
|
||||
"lng_passport_bad_name" = "Use latin characters only.";
|
||||
"lng_passport_bad_name" = "Please use latin characters only.";
|
||||
|
||||
"lng_export_title" = "Export Personal Data";
|
||||
"lng_export_progress_title" = "Exporting personal data";
|
||||
"lng_export_option_info" = "Account information";
|
||||
"lng_export_option_info_about" = "Your chosen screen name, username, phone number and profile pictures.";
|
||||
"lng_export_option_contacts" = "Contacts list";
|
||||
"lng_export_option_contacts_about" = "If you allow access, contacts are continuously synced with Telegram. You can adjust this in Settings > Privacy & Security on mobile devices.";
|
||||
"lng_export_option_sessions" = "Active sessions";
|
||||
"lng_export_option_sessions_about" = "We store this to display your connected devices in Settings > Privacy & Security > Active Sessions.";
|
||||
"lng_export_header_other" = "Other";
|
||||
"lng_export_option_other" = "Miscellaneous data";
|
||||
"lng_export_option_other_about" = "Other types of data not mentioned above (beta).";
|
||||
"lng_export_header_chats" = "Chat export settings";
|
||||
"lng_export_option_personal_chats" = "Personal chats";
|
||||
"lng_export_option_bot_chats" = "Bot chats";
|
||||
"lng_export_option_private_groups" = "Private groups";
|
||||
"lng_export_option_private_channels" = "Private channels";
|
||||
"lng_export_option_public_groups" = "Public groups";
|
||||
"lng_export_option_public_channels" = "Public channels";
|
||||
"lng_export_option_only_my" = "Only my messages";
|
||||
"lng_export_header_media" = "Media export settings";
|
||||
"lng_export_option_photos" = "Photos";
|
||||
"lng_export_option_video_files" = "Video files";
|
||||
"lng_export_option_voice_messages" = "Voice messages";
|
||||
"lng_export_option_video_messages" = "Round video messages";
|
||||
"lng_export_option_stickers" = "Stickers";
|
||||
"lng_export_option_gifs" = "Animated GIFs";
|
||||
"lng_export_option_files" = "Files";
|
||||
"lng_export_option_size_limit" = "Size limit: {size}";
|
||||
"lng_export_header_format" = "Location and format";
|
||||
"lng_export_option_location" = "Download path: {path}";
|
||||
"lng_export_option_html" = "Human-readable HTML";
|
||||
"lng_export_option_json" = "Machine-readable JSON";
|
||||
"lng_export_start" = "Export";
|
||||
"lng_export_state_initializing" = "Initializing...";
|
||||
"lng_export_state_userpics" = "Profile pictures";
|
||||
"lng_export_state_chats_list" = "Processing chats...";
|
||||
"lng_export_state_chats" = "Chats";
|
||||
"lng_export_state_progress" = "{count} / {total}";
|
||||
"lng_export_state_photo" = "Photo";
|
||||
"lng_export_state_video_file" = "Video file";
|
||||
"lng_export_state_voice_message" = "Voice message";
|
||||
"lng_export_state_video_message" = "Round video message";
|
||||
"lng_export_state_sticker" = "Sticker";
|
||||
"lng_export_state_gif" = "Animated GIF";
|
||||
"lng_export_progress" = "You can close this window now. Please don't quit Telegram until the data export is completed.";
|
||||
"lng_export_stop" = "Stop";
|
||||
"lng_export_sure_stop" = "Are you sure you want to stop exporting your data?\n\nIf you do, you'll need to start over.";
|
||||
"lng_export_about_done" = "Your data was successfully exported.";
|
||||
"lng_export_done" = "Show my data";
|
||||
"lng_export_finished" = "Data export completed.";
|
||||
"lng_export_total_files" = "Total files: {count}.";
|
||||
"lng_export_total_size" = "Total size: {size}.";
|
||||
"lng_export_folder" = "Choose export folder";
|
||||
"lng_export_invalid" = "Sorry, you have started a new data export, so this data export is now cancelled.";
|
||||
"lng_export_delay" = "Sorry, for security reasons, you will be able to begin downloading your data in {hours}. We have notified all your devices about the export request to make sure it's authorized and to give you time to react if it's not.\n\nPlease come back on {date} and repeat the request using the same device.";
|
||||
"lng_export_delay_less_than_hour" = "less than an hour";
|
||||
"lng_export_delay_hours#one" = "{count} hour";
|
||||
"lng_export_delay_hours#other" = "{count} hours";
|
||||
"lng_export_suggest_title" = "Data export ready";
|
||||
"lng_export_suggest_text" = "You can now download the data you requested. Start exporting data?";
|
||||
"lng_export_suggest_cancel" = "Not now";
|
||||
"lng_export_about_telegram" = "Here is all the data you requested. Remember: we don’t use your data for ad targeting, we don’t sell it to others, and we’re not part of any “family of companies.”\n\nTelegram only keeps the information it needs to function as a feature-rich cloud service – for example, your cloud chats so that you can access them from any devices without using third-party backups, or your contacts so that you can rely on your existing social graph when messaging people on Telegram.\n\nCheck out Settings > Privacy & Security on Telegram's mobile apps for relevant settings.";
|
||||
"lng_export_about_contacts" = "If you allow access, your contacts are continuously synced with Telegram. Thanks to this, you can easily switch to Telegram without losing your existing social graph – and connect with friends across all your devices. We use data about your contacts to let you know when they join Telegram. We also use it to make sure that you see the names you have in your phone book instead of the screen names people choose for themselves.\n\nYou can disable contacts syncing or delete your stored contacts in Settings > Privacy & Security on Telegram's mobile apps.";
|
||||
"lng_export_about_frequent" = "This rating shows which people you are likelier to message frequently. Telegram uses this data to populate the 'People' box at the top of the Search section. The rating is also calculated for inline bots so that the app can suggest you the bots you are most likely to use in the attachment menu (or when you start a new message with \"@\").\n\nTo delete this data, go to Settings > Privacy & Security and disable 'Suggest Frequent Contacts' (requires Telegram for iOS v.4.8.3 or Telegram for Android v.4.8.10 or higher). See this page for more information: https://telegram.org/faq_export";
|
||||
"lng_export_about_sessions" = "We store session info to display your connected devices in Settings > Privacy & Security > Active Sessions.";
|
||||
"lng_export_about_web_sessions" = "We store web login info to display you the websites where you used Telegram to log in in Settings > Privacy & Security > Active Sessions. Disconnecting a website removes this data from Telegram servers.";
|
||||
"lng_export_about_chats" = "This page lists all chats from this export and where to look for their data.";
|
||||
"lng_export_about_left_chats" = "This page lists all supergroups and channels from this export that you've left and where to look for their data.\n\nNote that when you leave a channel or supergroup you've created, you have the option to either delete it, or simply leave (in case you want to rejoin later, or keep the community alive despite not being a member).";
|
||||
|
||||
// Wnd specific
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<RCC>
|
||||
<qresource prefix="/export">
|
||||
<file alias="css/style.css">../css/export_style.css</file>
|
||||
</qresource>
|
||||
<qresource prefix="/gui">
|
||||
<file alias="fonts/OpenSans-Regular.ttf">../fonts/OpenSans-Regular.ttf</file>
|
||||
<file alias="fonts/OpenSans-Bold.ttf">../fonts/OpenSans-Bold.ttf</file>
|
||||
|
||||
@@ -192,6 +192,7 @@ inputFileLocation#14637196 volume_id:long local_id:int secret:long = InputFileLo
|
||||
inputEncryptedFileLocation#f5235d55 id:long access_hash:long = InputFileLocation;
|
||||
inputDocumentFileLocation#430f0724 id:long access_hash:long version:int = InputFileLocation;
|
||||
inputSecureFileLocation#cbc7ee28 id:long access_hash:long = InputFileLocation;
|
||||
inputTakeoutFileLocation#29be5899 = InputFileLocation;
|
||||
|
||||
inputAppEvent#770656a8 time:double type:string peer:long data:string = InputAppEvent;
|
||||
|
||||
@@ -843,8 +844,6 @@ webDocumentNoProxy#f9c8bcc6 url:string size:int mime_type:string attributes:Vect
|
||||
inputWebDocument#9bed434d url:string size:int mime_type:string attributes:Vector<DocumentAttribute> = InputWebDocument;
|
||||
|
||||
inputWebFileLocation#c239d686 url:string access_hash:long = InputWebFileLocation;
|
||||
inputWebFileGeoPointLocation#66275a62 geo_point:InputGeoPoint w:int h:int zoom:int scale:int = InputWebFileLocation;
|
||||
inputWebFileGeoMessageLocation#553f32eb peer:InputPeer msg_id:int w:int h:int zoom:int scale:int = InputWebFileLocation;
|
||||
|
||||
upload.webFile#21e753bc size:int mime_type:string file_type:storage.FileType mtime:int bytes:bytes = upload.WebFile;
|
||||
|
||||
@@ -1014,6 +1013,10 @@ account.sentEmailCode#811f854f email_pattern:string length:int = account.SentEma
|
||||
help.deepLinkInfoEmpty#66afa166 = help.DeepLinkInfo;
|
||||
help.deepLinkInfo#6a4ee832 flags:# update_app:flags.0?true message:string entities:flags.1?Vector<MessageEntity> = help.DeepLinkInfo;
|
||||
|
||||
savedPhoneContact#1142bd56 phone:string first_name:string last_name:string date:int = SavedContact;
|
||||
|
||||
account.takeout#4dba4501 id:long = account.Takeout;
|
||||
|
||||
---functions---
|
||||
|
||||
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
|
||||
@@ -1021,13 +1024,14 @@ invokeAfterMsgs#3dc4b4f0 {X:Type} msg_ids:Vector<long> query:!X = X;
|
||||
initConnection#785188b8 {X:Type} flags:# api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string proxy:flags.0?InputClientProxy query:!X = X;
|
||||
invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X;
|
||||
invokeWithoutUpdates#bf9459b7 {X:Type} query:!X = X;
|
||||
invokeWithMessagesRange#365275f2 {X:Type} range:MessageRange query:!X = X;
|
||||
invokeWithTakeout#aca9fd2e {X:Type} takeout_id:long query:!X = X;
|
||||
|
||||
auth.sendCode#86aef0ec flags:# allow_flashcall:flags.0?true phone_number:string current_number:flags.0?Bool api_id:int api_hash:string = auth.SentCode;
|
||||
auth.signUp#1b067634 phone_number:string phone_code_hash:string phone_code:string first_name:string last_name:string = auth.Authorization;
|
||||
auth.signIn#bcd51581 phone_number:string phone_code_hash:string phone_code:string = auth.Authorization;
|
||||
auth.logOut#5717da40 = Bool;
|
||||
auth.resetAuthorizations#9fab0d1a = Bool;
|
||||
auth.sendInvites#771c1d97 phone_numbers:Vector<string> message:string = Bool;
|
||||
auth.exportAuthorization#e5bfffcd dc_id:int = auth.ExportedAuthorization;
|
||||
auth.importAuthorization#e3ef9613 id:int bytes:bytes = auth.Authorization;
|
||||
auth.bindTempAuthKey#cdd42a05 perm_auth_key_id:long nonce:long expires_at:int encrypted_message:bytes = Bool;
|
||||
@@ -1079,6 +1083,8 @@ account.sendVerifyPhoneCode#823380b4 flags:# allow_flashcall:flags.0?true phone_
|
||||
account.verifyPhone#4dd3a7f6 phone_number:string phone_code_hash:string phone_code:string = Bool;
|
||||
account.sendVerifyEmailCode#7011509f email:string = account.SentEmailCode;
|
||||
account.verifyEmail#ecba39db email:string code:string = Bool;
|
||||
account.initTakeoutSession#f05b4804 flags:# contacts:flags.0?true message_users:flags.1?true message_chats:flags.2?true message_megagroups:flags.3?true message_channels:flags.4?true files:flags.5?true file_max_size:flags.5?int = account.Takeout;
|
||||
account.finishTakeoutSession#1d2652ee flags:# success:flags.0?true = Bool;
|
||||
|
||||
users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
|
||||
users.getFullUser#ca30a5b1 id:InputUser = UserFull;
|
||||
@@ -1099,6 +1105,7 @@ contacts.resolveUsername#f93ccba3 username:string = contacts.ResolvedPeer;
|
||||
contacts.getTopPeers#d4982db5 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true phone_calls:flags.3?true groups:flags.10?true channels:flags.15?true offset:int limit:int hash:int = contacts.TopPeers;
|
||||
contacts.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool;
|
||||
contacts.resetSaved#879537f1 = Bool;
|
||||
contacts.getSaved#82f1e39f = Vector<SavedContact>;
|
||||
|
||||
messages.getMessages#63c66506 id:Vector<InputMessage> = messages.Messages;
|
||||
messages.getDialogs#191ba9c5 flags:# exclude_pinned:flags.0?true offset_date:int offset_id:int offset_peer:InputPeer limit:int = messages.Dialogs;
|
||||
@@ -1196,6 +1203,7 @@ messages.getRecentLocations#bbc45b09 peer:InputPeer limit:int hash:int = message
|
||||
messages.sendMultiMedia#2095512f flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int multi_media:Vector<InputSingleMedia> = Updates;
|
||||
messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile;
|
||||
messages.searchStickerSets#c2b7d08b flags:# exclude_featured:flags.0?true q:string hash:int = messages.FoundStickerSets;
|
||||
messages.getSplitRanges#1cff7e08 = Vector<MessageRange>;
|
||||
|
||||
updates.getState#edd4882a = updates.State;
|
||||
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
|
||||
@@ -1262,6 +1270,7 @@ channels.setStickers#ea8ca4f9 channel:InputChannel stickerset:InputStickerSet =
|
||||
channels.readMessageContents#eab5dc38 channel:InputChannel id:Vector<int> = Bool;
|
||||
channels.deleteHistory#af369d42 channel:InputChannel max_id:int = Bool;
|
||||
channels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Updates;
|
||||
channels.getLeftChannels#8341ecc0 offset:int = messages.Chats;
|
||||
|
||||
bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON;
|
||||
bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="1.3.4.0" />
|
||||
Version="1.3.8.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
|
||||
|
||||
@@ -34,8 +34,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 1,3,4,0
|
||||
PRODUCTVERSION 1,3,4,0
|
||||
FILEVERSION 1,3,8,0
|
||||
PRODUCTVERSION 1,3,8,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -52,10 +52,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram Messenger LLP"
|
||||
VALUE "FileDescription", "Telegram Desktop"
|
||||
VALUE "FileVersion", "1.3.4.0"
|
||||
VALUE "FileVersion", "1.3.8.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2018"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "1.3.4.0"
|
||||
VALUE "ProductVersion", "1.3.8.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -25,8 +25,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 1,3,4,0
|
||||
PRODUCTVERSION 1,3,4,0
|
||||
FILEVERSION 1,3,8,0
|
||||
PRODUCTVERSION 1,3,8,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -43,10 +43,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram Messenger LLP"
|
||||
VALUE "FileDescription", "Telegram Desktop Updater"
|
||||
VALUE "FileVersion", "1.3.4.0"
|
||||
VALUE "FileVersion", "1.3.8.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2018"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "1.3.4.0"
|
||||
VALUE "ProductVersion", "1.3.8.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -270,8 +270,9 @@ void ApiWrap::requestTermsUpdate() {
|
||||
_termsUpdateRequestId = 0;
|
||||
|
||||
const auto requestNext = [&](auto &&data) {
|
||||
const auto timeout = (data.vexpires.v - unixtime());
|
||||
_termsUpdateSendAt = getms(true) + snap(
|
||||
TimeMs(data.vexpires.v - unixtime()),
|
||||
timeout * TimeMs(1000),
|
||||
kTermsUpdateTimeoutMin,
|
||||
kTermsUpdateTimeoutMax);
|
||||
requestTermsUpdate();
|
||||
@@ -2055,10 +2056,21 @@ void ApiWrap::saveDraftsToCloud() {
|
||||
if (!textWithTags.tags.isEmpty()) {
|
||||
flags |= MTPmessages_SaveDraft::Flag::f_entities;
|
||||
}
|
||||
auto entities = TextUtilities::EntitiesToMTP(ConvertTextTagsToEntities(textWithTags.tags), TextUtilities::ConvertOption::SkipLocal);
|
||||
auto entities = TextUtilities::EntitiesToMTP(
|
||||
ConvertTextTagsToEntities(textWithTags.tags),
|
||||
TextUtilities::ConvertOption::SkipLocal);
|
||||
|
||||
cloudDraft->saveRequestId = request(MTPmessages_SaveDraft(MTP_flags(flags), MTP_int(cloudDraft->msgId), history->peer->input, MTP_string(textWithTags.text), entities)).done([this, history](const MTPBool &result, mtpRequestId requestId) {
|
||||
if (auto cloudDraft = history->cloudDraft()) {
|
||||
history->setSentDraftText(textWithTags.text);
|
||||
cloudDraft->saveRequestId = request(MTPmessages_SaveDraft(
|
||||
MTP_flags(flags),
|
||||
MTP_int(cloudDraft->msgId),
|
||||
history->peer->input,
|
||||
MTP_string(textWithTags.text),
|
||||
entities
|
||||
)).done([=](const MTPBool &result, mtpRequestId requestId) {
|
||||
history->clearSentDraftText();
|
||||
|
||||
if (const auto cloudDraft = history->cloudDraft()) {
|
||||
if (cloudDraft->saveRequestId == requestId) {
|
||||
cloudDraft->saveRequestId = 0;
|
||||
history->draftSavedToCloud();
|
||||
@@ -2069,8 +2081,10 @@ void ApiWrap::saveDraftsToCloud() {
|
||||
_draftsSaveRequestIds.erase(history);
|
||||
checkQuitPreventFinished();
|
||||
}
|
||||
}).fail([this, history](const RPCError &error, mtpRequestId requestId) {
|
||||
if (auto cloudDraft = history->cloudDraft()) {
|
||||
}).fail([=](const RPCError &error, mtpRequestId requestId) {
|
||||
history->clearSentDraftText();
|
||||
|
||||
if (const auto cloudDraft = history->cloudDraft()) {
|
||||
if (cloudDraft->saveRequestId == requestId) {
|
||||
history->clearCloudDraft();
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "styles/style_history.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "boxes/confirm_box.h"
|
||||
#include "data/data_abstract_structure.h"
|
||||
#include "data/data_media_types.h"
|
||||
#include "data/data_session.h"
|
||||
@@ -50,8 +51,7 @@ namespace {
|
||||
|
||||
UserData *self = nullptr;
|
||||
|
||||
using PeersData = QHash<PeerId, PeerData*>;
|
||||
PeersData peersData;
|
||||
std::unordered_map<PeerId, std::unique_ptr<PeerData>> peersData;
|
||||
|
||||
using LocationsData = QHash<LocationCoords, LocationData*>;
|
||||
LocationsData locationsData;
|
||||
@@ -747,7 +747,7 @@ namespace App {
|
||||
} else if (chat->version <= d.vversion.v && chat->count > 0) {
|
||||
chat->version = d.vversion.v;
|
||||
auto canEdit = chat->canEdit();
|
||||
UserData *user = App::userLoaded(d.vuser_id.v);
|
||||
const auto user = App::userLoaded(d.vuser_id.v);
|
||||
if (user) {
|
||||
if (chat->participants.empty()) {
|
||||
if (chat->count > 0) {
|
||||
@@ -1074,40 +1074,43 @@ namespace App {
|
||||
}
|
||||
|
||||
PeerData *peer(const PeerId &id, PeerData::LoadedStatus restriction) {
|
||||
if (!id) return nullptr;
|
||||
if (!id) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto i = peersData.constFind(id);
|
||||
auto i = peersData.find(id);
|
||||
if (i == peersData.cend()) {
|
||||
PeerData *newData = nullptr;
|
||||
if (peerIsUser(id)) {
|
||||
newData = new UserData(id);
|
||||
} else if (peerIsChat(id)) {
|
||||
newData = new ChatData(id);
|
||||
} else if (peerIsChannel(id)) {
|
||||
newData = new ChannelData(id);
|
||||
}
|
||||
Assert(newData != nullptr);
|
||||
auto newData = [&]() -> std::unique_ptr<PeerData> {
|
||||
if (peerIsUser(id)) {
|
||||
return std::make_unique<UserData>(id);
|
||||
} else if (peerIsChat(id)) {
|
||||
return std::make_unique<ChatData>(id);
|
||||
} else if (peerIsChannel(id)) {
|
||||
return std::make_unique<ChannelData>(id);
|
||||
}
|
||||
Unexpected("Peer id type.");
|
||||
}();
|
||||
|
||||
newData->input = MTPinputPeer(MTP_inputPeerEmpty());
|
||||
i = peersData.insert(id, newData);
|
||||
i = peersData.emplace(id, std::move(newData)).first;
|
||||
}
|
||||
switch (restriction) {
|
||||
case PeerData::MinimalLoaded: {
|
||||
if (i.value()->loadedStatus == PeerData::NotLoaded) {
|
||||
if (i->second->loadedStatus == PeerData::NotLoaded) {
|
||||
return nullptr;
|
||||
}
|
||||
} break;
|
||||
case PeerData::FullLoaded: {
|
||||
if (i.value()->loadedStatus != PeerData::FullLoaded) {
|
||||
if (i->second->loadedStatus != PeerData::FullLoaded) {
|
||||
return nullptr;
|
||||
}
|
||||
} break;
|
||||
}
|
||||
return i.value();
|
||||
return i->second.get();
|
||||
}
|
||||
|
||||
void enumerateUsers(Fn<void(not_null<UserData*>)> action) {
|
||||
for_const (const auto peer, peersData) {
|
||||
for (const auto &[peerId, peer] : peersData) {
|
||||
if (const auto user = peer->asUser()) {
|
||||
action(user);
|
||||
}
|
||||
@@ -1116,9 +1119,9 @@ namespace App {
|
||||
|
||||
void enumerateChatsChannels(
|
||||
Fn<void(not_null<PeerData*>)> action) {
|
||||
for_const (const auto peer, peersData) {
|
||||
for (const auto &[peerId, peer] : peersData) {
|
||||
if (!peer->isUser()) {
|
||||
action(peer);
|
||||
action(peer.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1128,10 +1131,10 @@ namespace App {
|
||||
}
|
||||
|
||||
PeerData *peerByName(const QString &username) {
|
||||
QString uname(username.trimmed());
|
||||
for_const (PeerData *peer, peersData) {
|
||||
const auto uname = username.trimmed();
|
||||
for (const auto &[peerId, peer] : peersData) {
|
||||
if (!peer->userName().compare(uname, Qt::CaseInsensitive)) {
|
||||
return peer;
|
||||
return peer.get();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
@@ -1246,24 +1249,16 @@ namespace App {
|
||||
void historyClearMsgs() {
|
||||
::dependentItems.clear();
|
||||
|
||||
QVector<HistoryItem*> toDelete;
|
||||
for_const (auto item, msgsData) {
|
||||
if (!item->mainView()) {
|
||||
toDelete.push_back(item);
|
||||
}
|
||||
}
|
||||
for_const (auto &chMsgsData, channelMsgsData) {
|
||||
for_const (auto item, chMsgsData) {
|
||||
if (!item->mainView()) {
|
||||
toDelete.push_back(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
msgsData.clear();
|
||||
channelMsgsData.clear();
|
||||
for_const (auto item, toDelete) {
|
||||
const auto oldData = base::take(msgsData);
|
||||
const auto oldChannelData = base::take(channelMsgsData);
|
||||
for (const auto item : oldData) {
|
||||
delete item;
|
||||
}
|
||||
for (const auto &data : oldChannelData) {
|
||||
for (const auto item : data) {
|
||||
delete item;
|
||||
}
|
||||
}
|
||||
|
||||
clearMousedItems();
|
||||
}
|
||||
@@ -1275,10 +1270,7 @@ namespace App {
|
||||
cSetSavedPeersByTime(SavedPeersByTime());
|
||||
cSetRecentInlineBots(RecentInlineBots());
|
||||
|
||||
for_const (auto peer, ::peersData) {
|
||||
delete peer;
|
||||
}
|
||||
::peersData.clear();
|
||||
peersData.clear();
|
||||
|
||||
if (AuthSession::Exists()) {
|
||||
Auth().api().clearWebPageRequests();
|
||||
@@ -1598,7 +1590,13 @@ namespace App {
|
||||
}
|
||||
|
||||
void quit() {
|
||||
if (quitting()) return;
|
||||
if (quitting()) {
|
||||
return;
|
||||
} else if (AuthSession::Exists()
|
||||
&& Auth().data().exportInProgress()) {
|
||||
Auth().data().stopExportWithConfirmation([] { App::quit(); });
|
||||
return;
|
||||
}
|
||||
setLaunchState(QuitRequested);
|
||||
|
||||
if (auto window = wnd()) {
|
||||
|
||||
@@ -134,4 +134,7 @@ vector concatenate(SpanRange args) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Implemented in base/openssl_help.h
|
||||
void set_random(span destination);
|
||||
|
||||
} // namespace bytes
|
||||
|
||||
28
Telegram/SourceFiles/base/match_method.h
Normal file
28
Telegram/SourceFiles/base/match_method.h
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <rpl/details/callable.h>
|
||||
|
||||
namespace base {
|
||||
|
||||
template <typename Data, typename Method, typename ...Methods>
|
||||
inline decltype(auto) match_method(
|
||||
Data &&data,
|
||||
Method &&method,
|
||||
Methods &&...methods) {
|
||||
if constexpr (rpl::details::is_callable_plain_v<Method, Data&&>) {
|
||||
return std::forward<Method>(method)(std::forward<Data>(data));
|
||||
} else {
|
||||
return match_method(
|
||||
std::forward<Data>(data),
|
||||
std::forward<Methods>(methods)...);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace base
|
||||
@@ -109,6 +109,15 @@ public:
|
||||
return _impl.template get_unchecked<T>();
|
||||
}
|
||||
|
||||
template <typename ...Methods>
|
||||
decltype(auto) match(Methods &&...methods) {
|
||||
return base::match(_impl, std::forward<Methods>(methods)...);
|
||||
}
|
||||
template <typename ...Methods>
|
||||
decltype(auto) match(Methods &&...methods) const {
|
||||
return base::match(_impl, std::forward<Methods>(methods)...);
|
||||
}
|
||||
|
||||
private:
|
||||
variant<none_type, Types...> _impl;
|
||||
|
||||
@@ -124,6 +133,20 @@ inline const T *get_if(const optional_variant<Types...> *v) {
|
||||
return (v && v->template is<T>()) ? &v->template get_unchecked<T>() : nullptr;
|
||||
}
|
||||
|
||||
template <typename ...Types, typename ...Methods>
|
||||
inline decltype(auto) match(
|
||||
optional_variant<Types...> &value,
|
||||
Methods &&...methods) {
|
||||
return value.match(std::forward<Methods>(methods)...);
|
||||
}
|
||||
|
||||
template <typename ...Types, typename ...Methods>
|
||||
inline decltype(auto) match(
|
||||
const optional_variant<Types...> &value,
|
||||
Methods &&...methods) {
|
||||
return value.match(std::forward<Methods>(methods)...);
|
||||
}
|
||||
|
||||
template <typename Type>
|
||||
class optional;
|
||||
|
||||
@@ -160,12 +183,17 @@ class optional : public optional_variant<Type> {
|
||||
public:
|
||||
using parent::parent;
|
||||
|
||||
Type &operator*() {
|
||||
Type &operator*() & {
|
||||
Expects(parent::template is<Type>());
|
||||
|
||||
return parent::template get_unchecked<Type>();
|
||||
}
|
||||
const Type &operator*() const {
|
||||
Type &&operator*() && {
|
||||
Expects(parent::template is<Type>());
|
||||
|
||||
return std::move(parent::template get_unchecked<Type>());
|
||||
}
|
||||
const Type &operator*() const & {
|
||||
Expects(parent::template is<Type>());
|
||||
|
||||
return parent::template get_unchecked<Type>();
|
||||
|
||||
@@ -7,7 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <QtCore/QObject>
|
||||
#include <QtCore/QThread>
|
||||
#include "base/observer.h"
|
||||
#include "base/flat_map.h"
|
||||
|
||||
namespace base {
|
||||
|
||||
|
||||
@@ -69,11 +69,11 @@ public:
|
||||
|
||||
unique_any(const unique_any &other) = delete;
|
||||
unique_any &operator=(const unique_any &other) = delete;
|
||||
|
||||
|
||||
unique_any(unique_any &&other) noexcept
|
||||
: _impl(std::move(other._impl)) {
|
||||
}
|
||||
|
||||
|
||||
unique_any &operator=(unique_any &&other) noexcept {
|
||||
_impl = std::move(other._impl);
|
||||
return *this;
|
||||
@@ -88,7 +88,7 @@ public:
|
||||
std::forward<Value>(other),
|
||||
std::is_copy_constructible<std::decay_t<Value>>()) {
|
||||
}
|
||||
|
||||
|
||||
template <
|
||||
typename Value,
|
||||
typename = std::enable_if_t<
|
||||
@@ -106,7 +106,7 @@ public:
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
template <
|
||||
typename Value,
|
||||
typename ...Args,
|
||||
@@ -143,7 +143,7 @@ private:
|
||||
unique_any(Value &&other, std::true_type)
|
||||
: _impl(std::forward<Value>(other)) {
|
||||
}
|
||||
|
||||
|
||||
template <
|
||||
typename Value,
|
||||
typename = std::enable_if_t<
|
||||
@@ -177,7 +177,7 @@ inline void swap(unique_any &a, unique_any &b) noexcept {
|
||||
template <
|
||||
typename Value,
|
||||
typename ...Args>
|
||||
inline auto make_any(Args &&...args)
|
||||
inline auto make_any(Args &&...args)
|
||||
-> std::enable_if_t<
|
||||
std::is_copy_constructible_v<std::decay_t<Value>>,
|
||||
unique_any> {
|
||||
@@ -187,7 +187,7 @@ inline auto make_any(Args &&...args)
|
||||
template <
|
||||
typename Value,
|
||||
typename ...Args>
|
||||
inline auto make_any(Args &&...args)
|
||||
inline auto make_any(Args &&...args)
|
||||
-> std::enable_if_t<
|
||||
!std::is_copy_constructible_v<std::decay_t<Value>>
|
||||
&& std::is_move_constructible_v<std::decay_t<Value>>,
|
||||
|
||||
@@ -124,7 +124,10 @@ public:
|
||||
_impl.swap(other._impl);
|
||||
}
|
||||
|
||||
template <typename ...OtherArgs>
|
||||
template <
|
||||
typename ...OtherArgs,
|
||||
typename = decltype(std::declval<std::function<Return(Args...)>>()(
|
||||
std::declval<OtherArgs>()...))>
|
||||
Return operator()(OtherArgs &&...args) {
|
||||
return _impl(std::forward<OtherArgs>(args)...);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace base {
|
||||
namespace details {
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#pragma once
|
||||
|
||||
#include <mapbox/variant.hpp>
|
||||
#include <rpl/details/type_list.h>
|
||||
#include "base/match_method.h"
|
||||
#include "base/assertion.h"
|
||||
|
||||
// We use base::variant<> alias and base::get_if() helper while we don't have std::variant<>.
|
||||
namespace base {
|
||||
@@ -25,20 +28,79 @@ inline const T *get_if(const variant<Types...> *v) {
|
||||
return (v && v->template is<T>()) ? &v->template get_unchecked<T>() : nullptr;
|
||||
}
|
||||
|
||||
// Simplified visit
|
||||
template <typename Method, typename... Types>
|
||||
inline auto visit(Method &&method, const variant<Types...> &value) {
|
||||
return value.match(std::forward<Method>(method));
|
||||
namespace type_list = rpl::details::type_list;
|
||||
|
||||
template <typename ...Types>
|
||||
struct normalized_variant {
|
||||
using list = type_list::list<Types...>;
|
||||
using distinct = type_list::distinct_t<list>;
|
||||
using type = std::conditional_t<
|
||||
type_list::size_v<distinct> == 1,
|
||||
type_list::get_t<0, distinct>,
|
||||
type_list::extract_to_t<distinct, base::variant>>;
|
||||
};
|
||||
|
||||
template <typename ...Types>
|
||||
using normalized_variant_t
|
||||
= typename normalized_variant<Types...>::type;
|
||||
|
||||
template <typename TypeList, typename Variant, typename ...Methods>
|
||||
struct match_helper;
|
||||
|
||||
template <
|
||||
typename Type,
|
||||
typename ...Types,
|
||||
typename Variant,
|
||||
typename ...Methods>
|
||||
struct match_helper<type_list::list<Type, Types...>, Variant, Methods...> {
|
||||
static decltype(auto) call(Variant &value, Methods &&...methods) {
|
||||
if (const auto v = get_if<Type>(&value)) {
|
||||
return match_method(
|
||||
*v,
|
||||
std::forward<Methods>(methods)...);
|
||||
}
|
||||
return match_helper<
|
||||
type_list::list<Types...>,
|
||||
Variant,
|
||||
Methods...>::call(
|
||||
value,
|
||||
std::forward<Methods>(methods)...);
|
||||
}
|
||||
};
|
||||
|
||||
template <
|
||||
typename Type,
|
||||
typename Variant,
|
||||
typename ...Methods>
|
||||
struct match_helper<type_list::list<Type>, Variant, Methods...> {
|
||||
static decltype(auto) call(Variant &value, Methods &&...methods) {
|
||||
if (const auto v = get_if<Type>(&value)) {
|
||||
return match_method(
|
||||
*v,
|
||||
std::forward<Methods>(methods)...);
|
||||
}
|
||||
Unexpected("Valueless variant in base::match().");
|
||||
}
|
||||
};
|
||||
|
||||
template <typename ...Types, typename ...Methods>
|
||||
inline decltype(auto) match(
|
||||
variant<Types...> &value,
|
||||
Methods &&...methods) {
|
||||
return match_helper<
|
||||
type_list::list<Types...>,
|
||||
variant<Types...>,
|
||||
Methods...>::call(value, std::forward<Methods>(methods)...);
|
||||
}
|
||||
|
||||
template <typename Method, typename... Types>
|
||||
inline auto visit(Method &&method, variant<Types...> &value) {
|
||||
return value.match(std::forward<Method>(method));
|
||||
}
|
||||
|
||||
template <typename Method, typename... Types>
|
||||
inline auto visit(Method &&method, variant<Types...> &&value) {
|
||||
return value.match(std::forward<Method>(method));
|
||||
template <typename ...Types, typename ...Methods>
|
||||
inline decltype(auto) match(
|
||||
const variant<Types...> &value,
|
||||
Methods &&...methods) {
|
||||
return match_helper<
|
||||
type_list::list<Types...>,
|
||||
const variant<Types...>,
|
||||
Methods...>::call(value, std::forward<Methods>(methods)...);
|
||||
}
|
||||
|
||||
} // namespace base
|
||||
|
||||
@@ -315,24 +315,3 @@ struct guard_traits<
|
||||
};
|
||||
|
||||
} // namespace crl
|
||||
|
||||
#ifdef QT_VERSION
|
||||
template <typename Lambda>
|
||||
inline void InvokeQueued(const base::has_weak_ptr *context, Lambda &&lambda) {
|
||||
auto callback = [
|
||||
guard = base::make_weak(context),
|
||||
lambda = std::forward<Lambda>(lambda)
|
||||
] {
|
||||
if (guard) {
|
||||
lambda();
|
||||
}
|
||||
};
|
||||
QObject proxy;
|
||||
QObject::connect(
|
||||
&proxy,
|
||||
&QObject::destroyed,
|
||||
QCoreApplication::instance(),
|
||||
std::move(callback),
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
#endif // QT_VERSION
|
||||
|
||||
@@ -31,7 +31,7 @@ void AboutBox::prepare() {
|
||||
|
||||
addButton(langFactory(lng_close), [this] { closeBox(); });
|
||||
|
||||
const auto linkHook = [](const ClickHandlerPtr &link, auto button) {
|
||||
const auto linkFilter = [](const ClickHandlerPtr &link, auto button) {
|
||||
if (const auto url = dynamic_cast<UrlClickHandler*>(link.get())) {
|
||||
url->UrlClickHandler::onClick(button);
|
||||
return false;
|
||||
@@ -40,9 +40,9 @@ void AboutBox::prepare() {
|
||||
};
|
||||
|
||||
_text3->setRichText(lng_about_text_3(lt_faq_open, qsl("[a href=\"%1\"]").arg(telegramFaqLink()), lt_faq_close, qsl("[/a]")));
|
||||
_text1->setClickHandlerHook(linkHook);
|
||||
_text2->setClickHandlerHook(linkHook);
|
||||
_text3->setClickHandlerHook(linkHook);
|
||||
_text1->setClickHandlerFilter(linkFilter);
|
||||
_text2->setClickHandlerFilter(linkFilter);
|
||||
_text3->setClickHandlerFilter(linkFilter);
|
||||
|
||||
_version->setClickedCallback([this] { showVersionHistory(); });
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ void DownloadPathBox::onEditPath() {
|
||||
}();
|
||||
const auto handleFolder = [=](const QString &result) {
|
||||
if (!result.isEmpty()) {
|
||||
_path = result + '/';
|
||||
_path = result.endsWith('/') ? result : (result + '/');
|
||||
_pathBookmark = psDownloadPathBookmark(_path);
|
||||
setPathText(QDir::toNativeSeparators(_path));
|
||||
_group->setValue(Directory::Custom);
|
||||
|
||||
@@ -21,6 +21,10 @@ constexpr auto kForeverHours = 24 * 365;
|
||||
|
||||
} // namespace
|
||||
|
||||
MuteSettingsBox::MuteSettingsBox(QWidget *parent, not_null<PeerData*> peer)
|
||||
: _peer(peer) {
|
||||
}
|
||||
|
||||
void MuteSettingsBox::prepare() {
|
||||
setTitle(langFactory(lng_disable_notifications_from_tray));
|
||||
auto y = 0;
|
||||
@@ -67,15 +71,25 @@ void MuteSettingsBox::prepare() {
|
||||
- st::boxOptionListSkip
|
||||
+ st::defaultCheckbox.margin.bottom();
|
||||
|
||||
addButton(langFactory(lng_box_ok), [this, group] {
|
||||
auto muteForSeconds = group->value() * 3600;
|
||||
_save = [=] {
|
||||
const auto muteForSeconds = group->value() * 3600;
|
||||
Auth().data().updateNotifySettings(
|
||||
_peer,
|
||||
muteForSeconds);
|
||||
closeBox();
|
||||
});
|
||||
};
|
||||
addButton(langFactory(lng_box_ok), _save);
|
||||
addButton(langFactory(lng_cancel), [this] { closeBox(); });
|
||||
|
||||
setDimensions(st::boxWidth, y);
|
||||
}
|
||||
|
||||
void MuteSettingsBox::keyPressEvent(QKeyEvent *e) {
|
||||
if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
|
||||
if (_save) {
|
||||
_save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// vi: ts=4 tw=80
|
||||
|
||||
@@ -13,17 +13,17 @@ Copyright (C) 2017, Nicholas Guriev <guriev-ns@ya.ru>
|
||||
* turning off notifications from a chat. The widget is opened by a context menu
|
||||
* in the left list of dialogues. */
|
||||
class MuteSettingsBox : public BoxContent {
|
||||
Q_OBJECT
|
||||
public:
|
||||
MuteSettingsBox(QWidget *parent, not_null<PeerData*> peer);
|
||||
|
||||
public:
|
||||
MuteSettingsBox(QWidget *parent, not_null<PeerData*> peer)
|
||||
: _peer(peer) {
|
||||
}
|
||||
|
||||
protected:
|
||||
protected:
|
||||
void prepare() override;
|
||||
|
||||
private:
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
|
||||
private:
|
||||
not_null<PeerData*> _peer;
|
||||
Fn<void()> _save;
|
||||
|
||||
};
|
||||
// vi: ts=4 tw=80
|
||||
|
||||
@@ -710,7 +710,7 @@ object_ptr<Ui::RpWidget> Controller::createInviteLinkEdit() {
|
||||
_controls.inviteLink->setSelectable(true);
|
||||
_controls.inviteLink->setContextCopyText(QString());
|
||||
_controls.inviteLink->setBreakEverywhere(true);
|
||||
_controls.inviteLink->setClickHandlerHook([this](auto&&...) {
|
||||
_controls.inviteLink->setClickHandlerFilter([=](auto&&...) {
|
||||
Application::clipboard()->setText(inviteLinkText());
|
||||
Ui::Toast::Show(lang(lng_group_invite_copied));
|
||||
return false;
|
||||
|
||||
@@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "history/history_media_types.h"
|
||||
#include "chat_helpers/message_field.h"
|
||||
#include "core/file_utilities.h"
|
||||
#include "core/mime_type.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/input_fields.h"
|
||||
@@ -801,7 +802,7 @@ void SingleFilePreview::preparePreview(const Storage::PreparedFile &file) {
|
||||
} else {
|
||||
auto fileinfo = QFileInfo(filepath);
|
||||
auto filename = fileinfo.fileName();
|
||||
_fileIsImage = fileIsImage(filename, mimeTypeForFile(fileinfo).name());
|
||||
_fileIsImage = fileIsImage(filename, Core::MimeTypeForFile(fileinfo).name());
|
||||
|
||||
auto songTitle = QString();
|
||||
auto songPerformer = QString();
|
||||
|
||||
@@ -632,7 +632,7 @@ void Call::handleControllerBarCountChange(
|
||||
// Expects(controller == _controller.get());
|
||||
Expects(controller->implData == static_cast<void*>(this));
|
||||
|
||||
InvokeQueued(this, [=] {
|
||||
crl::on_main(this, [=] {
|
||||
setSignalBarCount(count);
|
||||
});
|
||||
}
|
||||
@@ -763,10 +763,17 @@ void Call::finish(FinishType type, const MTPPhoneCallDiscardReason &reason) {
|
||||
auto duration = getDurationMs() / 1000;
|
||||
auto connectionId = _controller ? _controller->GetPreferredRelayID() : 0;
|
||||
_finishByTimeoutTimer.call(kHangupTimeoutMs, [this, finalState] { setState(finalState); });
|
||||
request(MTPphone_DiscardCall(MTP_inputPhoneCall(MTP_long(_id), MTP_long(_accessHash)), MTP_int(duration), reason, MTP_long(connectionId))).done([this, finalState](const MTPUpdates &result) {
|
||||
request(MTPphone_DiscardCall(
|
||||
MTP_inputPhoneCall(
|
||||
MTP_long(_id),
|
||||
MTP_long(_accessHash)),
|
||||
MTP_int(duration),
|
||||
reason,
|
||||
MTP_long(connectionId)
|
||||
)).done([=](const MTPUpdates &result) {
|
||||
// This could be destroyed by updates, so we set Ended after
|
||||
// updates being handled, but in a guarded way.
|
||||
InvokeQueued(this, [this, finalState] { setState(finalState); });
|
||||
crl::on_main(this, [=] { setState(finalState); });
|
||||
App::main()->sentUpdatesReceived(result);
|
||||
}).fail([this, finalState](const RPCError &error) {
|
||||
setState(finalState);
|
||||
@@ -774,13 +781,13 @@ void Call::finish(FinishType type, const MTPPhoneCallDiscardReason &reason) {
|
||||
}
|
||||
|
||||
void Call::setStateQueued(State state) {
|
||||
InvokeQueued(this, [=] {
|
||||
crl::on_main(this, [=] {
|
||||
setState(state);
|
||||
});
|
||||
}
|
||||
|
||||
void Call::setFailedQueued(int error) {
|
||||
InvokeQueued(this, [=] {
|
||||
crl::on_main(this, [=] {
|
||||
handleControllerError(error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ factories = '';
|
||||
flagOperators = '';
|
||||
methods = '';
|
||||
inlineMethods = '';
|
||||
visitorMethods = '';
|
||||
textSerializeInit = '';
|
||||
textSerializeMethods = '';
|
||||
forwards = '';
|
||||
@@ -577,6 +578,7 @@ for restype in typesList:
|
||||
switchLines = '';
|
||||
friendDecl = '';
|
||||
getters = '';
|
||||
visitor = '';
|
||||
reader = '';
|
||||
writer = '';
|
||||
sizeList = [];
|
||||
@@ -594,9 +596,14 @@ for restype in typesList:
|
||||
trivialConditions = data[7];
|
||||
|
||||
dataText = '';
|
||||
dataText += '\nclass MTPD' + name + ' : public MTP::internal::TypeData {\n'; # data class
|
||||
if (len(prms) > len(trivialConditions)):
|
||||
withData = 1;
|
||||
dataText += '\nclass MTPD' + name + ' : public MTP::internal::TypeData {\n'; # data class
|
||||
else:
|
||||
dataText += '\nclass MTPD' + name + ' {\n'; # empty data class for visitors
|
||||
dataText += 'public:\n';
|
||||
|
||||
dataText += '\ttemplate <typename Other>\n';
|
||||
dataText += '\tstatic constexpr bool Is() { return std::is_same_v<std::decay_t<Other>, MTPD' + name + '>; };\n\n';
|
||||
sizeList = [];
|
||||
creatorParams = [];
|
||||
creatorParamsList = [];
|
||||
@@ -626,16 +633,18 @@ for restype in typesList:
|
||||
dataText += '\tbool has_' + paramName + '() const { return v' + hasFlags + '.v & Flag::f_' + paramName + '; }\n';
|
||||
dataText += '\n';
|
||||
|
||||
dataText += '\tMTPD' + name + '() = default;\n'; # default constructor
|
||||
switchLines += '\t\tcase mtpc_' + name + ': '; # for by-type-id type constructor
|
||||
if (len(prms) > len(trivialConditions)):
|
||||
switchLines += 'setData(new MTPD' + name + '()); ';
|
||||
withData = 1;
|
||||
switchLines += '\tcase mtpc_' + name + ': '; # for by-type-id type constructor
|
||||
getters += '\tconst MTPD' + name + ' &c_' + name + '() const;\n'; # const getter
|
||||
visitor += '\tcase mtpc_' + name + ': return base::match_method(c_' + name + '(), std::forward<Method>(method), std::forward<Methods>(methods)...);\n';
|
||||
|
||||
forwards += 'class MTPD' + name + ';\n'; # data class forward declaration
|
||||
if (len(prms) > len(trivialConditions)):
|
||||
dataText += '\tMTPD' + name + '() = default;\n'; # default constructor
|
||||
switchLines += 'setData(new MTPD' + name + '()); ';
|
||||
|
||||
getters += '\tconst MTPD' + name + ' &c_' + name + '() const;\n'; # const getter
|
||||
constructsBodies += 'const MTPD' + name + ' &MTP' + restype + '::c_' + name + '() const {\n';
|
||||
if (withType):
|
||||
constructsBodies += '\tAssert(_type == mtpc_' + name + ');\n';
|
||||
constructsBodies += '\tExpects(_type == mtpc_' + name + ');\n\n';
|
||||
constructsBodies += '\treturn queryData<MTPD' + name + '>();\n';
|
||||
constructsBodies += '}\n';
|
||||
|
||||
@@ -662,8 +671,8 @@ for restype in typesList:
|
||||
creatorParamsList.append('_' + paramName);
|
||||
prmsInit.append('v' + paramName + '(_' + paramName + ')');
|
||||
if (withType):
|
||||
readText += '\t\t';
|
||||
writeText += '\t\t';
|
||||
readText += '\t';
|
||||
writeText += '\t';
|
||||
if (paramName in conditions):
|
||||
readText += '\tif (v->has_' + paramName + '()) { v->v' + paramName + '.read(from, end); } else { v->v' + paramName + ' = MTP' + paramType + '(); }\n';
|
||||
writeText += '\tif (v.has_' + paramName + '()) v.v' + paramName + '.write(to);\n';
|
||||
@@ -673,8 +682,6 @@ for restype in typesList:
|
||||
writeText += '\tv.v' + paramName + '.write(to);\n';
|
||||
sizeList.append('v.v' + paramName + '.innerLength()');
|
||||
|
||||
forwards += 'class MTPD' + name + ';\n'; # data class forward declaration
|
||||
|
||||
dataText += ', '.join(prmsStr) + ');\n';
|
||||
|
||||
constructsBodies += 'MTPD' + name + '::MTPD' + name + '(' + ', '.join(prmsStr) + ') : ' + ', '.join(prmsInit) + ' {\n}\n';
|
||||
@@ -685,20 +692,26 @@ for restype in typesList:
|
||||
continue;
|
||||
paramType = prms[paramName];
|
||||
dataText += '\tMTP' + paramType + ' v' + paramName + ';\n';
|
||||
sizeCases += '\t\tcase mtpc_' + name + ': {\n';
|
||||
sizeCases += '\t\t\tconst MTPD' + name + ' &v(c_' + name + '());\n';
|
||||
sizeCases += '\t\t\treturn ' + ' + '.join(sizeList) + ';\n';
|
||||
sizeCases += '\t\t}\n';
|
||||
sizeCases += '\tcase mtpc_' + name + ': {\n';
|
||||
sizeCases += '\t\tconst MTPD' + name + ' &v(c_' + name + '());\n';
|
||||
sizeCases += '\t\treturn ' + ' + '.join(sizeList) + ';\n';
|
||||
sizeCases += '\t}\n';
|
||||
sizeFast = '\tconst MTPD' + name + ' &v(c_' + name + '());\n\treturn ' + ' + '.join(sizeList) + ';\n';
|
||||
newFast = 'new MTPD' + name + '()';
|
||||
else:
|
||||
constructsBodies += 'const MTPD' + name + ' &MTP' + restype + '::c_' + name + '() const {\n';
|
||||
if (withType):
|
||||
constructsBodies += '\tExpects(_type == mtpc_' + name + ');\n\n';
|
||||
constructsBodies += '\tstatic const MTPD' + name + ' result;\n';
|
||||
constructsBodies += '\treturn result;\n';
|
||||
constructsBodies += '}\n';
|
||||
|
||||
sizeFast = '\treturn 0;\n';
|
||||
|
||||
switchLines += 'break;\n';
|
||||
dataText += '};\n'; # class ending
|
||||
|
||||
if (len(prms) > len(trivialConditions)):
|
||||
dataTexts += dataText; # add data class
|
||||
dataTexts += dataText; # add data class
|
||||
|
||||
if (not friendDecl):
|
||||
friendDecl += '\tfriend class MTP::internal::TypeCreator;\n';
|
||||
@@ -717,18 +730,18 @@ for restype in typesList:
|
||||
creatorsBodies += '}\n';
|
||||
|
||||
if (withType):
|
||||
reader += '\t\tcase mtpc_' + name + ': _type = cons; '; # read switch line
|
||||
reader += '\tcase mtpc_' + name + ': _type = cons; '; # read switch line
|
||||
if (len(prms) > len(trivialConditions)):
|
||||
reader += '{\n';
|
||||
reader += '\t\t\tauto v = new MTPD' + name + '();\n';
|
||||
reader += '\t\t\tsetData(v);\n';
|
||||
reader += '\t\tauto v = new MTPD' + name + '();\n';
|
||||
reader += '\t\tsetData(v);\n';
|
||||
reader += readText;
|
||||
reader += '\t\t} break;\n';
|
||||
reader += '\t} break;\n';
|
||||
|
||||
writer += '\t\tcase mtpc_' + name + ': {\n'; # write switch line
|
||||
writer += '\t\t\tauto &v = c_' + name + '();\n';
|
||||
writer += '\tcase mtpc_' + name + ': {\n'; # write switch line
|
||||
writer += '\t\tauto &v = c_' + name + '();\n';
|
||||
writer += writeText;
|
||||
writer += '\t\t} break;\n';
|
||||
writer += '\t} break;\n';
|
||||
else:
|
||||
reader += 'break;\n';
|
||||
else:
|
||||
@@ -737,7 +750,7 @@ for restype in typesList:
|
||||
reader += '\tsetData(v);\n';
|
||||
reader += readText;
|
||||
|
||||
writer += '\tauto &v = c_' + name + '();\n';
|
||||
writer += '\tconst auto &v = c_' + name + '();\n';
|
||||
writer += writeText;
|
||||
|
||||
forwards += '\n';
|
||||
@@ -754,8 +767,20 @@ for restype in typesList:
|
||||
else:
|
||||
typesText += ' = default;\n';
|
||||
|
||||
if (withData):
|
||||
typesText += getters;
|
||||
typesText += getters;
|
||||
typesText += '\n';
|
||||
typesText += '\ttemplate <typename Method, typename ...Methods>\n';
|
||||
typesText += '\tdecltype(auto) match(Method &&method, Methods &&...methods) const;\n';
|
||||
visitorMethods += 'template <typename Method, typename ...Methods>\n';
|
||||
visitorMethods += 'decltype(auto) MTP' + restype + '::match(Method &&method, Methods &&...methods) const {\n';
|
||||
if (withType):
|
||||
visitorMethods += '\tswitch (_type) {\n';
|
||||
visitorMethods += visitor;
|
||||
visitorMethods += '\t}\n';
|
||||
visitorMethods += '\tUnexpected("Type in MTP' + restype + '::match.");\n';
|
||||
else:
|
||||
visitorMethods += '\treturn base::match_method(c_' + v[0][0] + '(), std::forward<Method>(method), std::forward<Methods>(methods)...);\n';
|
||||
visitorMethods += '}\n\n';
|
||||
|
||||
typesText += '\n\tuint32 innerLength() const;\n'; # size method
|
||||
methods += '\nuint32 MTP' + restype + '::innerLength() const {\n';
|
||||
@@ -771,7 +796,7 @@ for restype in typesList:
|
||||
typesText += '\tmtpTypeId type() const;\n'; # type id method
|
||||
methods += 'mtpTypeId MTP' + restype + '::type() const {\n';
|
||||
if (withType):
|
||||
methods += '\tAssert(_type != 0);\n';
|
||||
methods += '\tExpects(_type != 0);\n\n';
|
||||
methods += '\treturn _type;\n';
|
||||
else:
|
||||
methods += '\treturn mtpc_' + v[0][0] + ';\n';
|
||||
@@ -788,7 +813,7 @@ for restype in typesList:
|
||||
if (withType):
|
||||
methods += '\tswitch (cons) {\n'
|
||||
methods += reader;
|
||||
methods += '\t\tdefault: throw mtpErrorUnexpected(cons, "MTP' + restype + '");\n';
|
||||
methods += '\tdefault: throw mtpErrorUnexpected(cons, "MTP' + restype + '");\n';
|
||||
methods += '\t}\n';
|
||||
else:
|
||||
methods += reader;
|
||||
@@ -814,12 +839,12 @@ for restype in typesList:
|
||||
methods += ' {\n';
|
||||
methods += '\tswitch (type) {\n'; # type id check
|
||||
methods += switchLines;
|
||||
methods += '\t\tdefault: throw mtpErrorBadTypeId(type, "MTP' + restype + '");\n\t}\n';
|
||||
methods += '\tdefault: throw mtpErrorBadTypeId(type, "MTP' + restype + '");\n\t}\n';
|
||||
methods += '}\n'; # by-type-id constructor end
|
||||
|
||||
if (withData):
|
||||
typesText += constructsText;
|
||||
methods += constructsBodies;
|
||||
methods += constructsBodies;
|
||||
|
||||
if (friendDecl):
|
||||
typesText += '\n' + friendDecl;
|
||||
@@ -953,6 +978,8 @@ enum {\n\
|
||||
' + funcsText + '\n\
|
||||
// Template methods definition\n\
|
||||
' + inlineMethods + '\n\
|
||||
// Visitor definition\n\
|
||||
' + visitorMethods + '\n\
|
||||
// Flag operators definition\n\
|
||||
' + flagOperators + '\n\
|
||||
// Factory methods declaration\n\
|
||||
|
||||
@@ -26,8 +26,6 @@ enum {
|
||||
|
||||
MTPKillFileSessionTimeout = 5000, // how much time without upload / download causes additional session kill
|
||||
|
||||
MTPDebugBufferSize = 1024 * 1024, // 1 mb start size
|
||||
|
||||
MaxUsersPerInvite = 100, // max users in one super group invite request
|
||||
|
||||
MTPChannelGetDifferenceLimit = 100,
|
||||
@@ -82,9 +80,6 @@ enum {
|
||||
MaxMessageSize = 4096,
|
||||
|
||||
WriteMapTimeout = 1000,
|
||||
SaveDraftTimeout = 1000, // save draft after 1 secs of not changing text
|
||||
SaveDraftAnywayTimeout = 5000, // or save anyway each 5 secs
|
||||
SaveCloudDraftIdleTimeout = 14000, // save draft to the cloud after 14 more seconds
|
||||
|
||||
SetOnlineAfterActivity = 30, // user with hidden last seen stays online for such amount of seconds in the interface
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include <memory>
|
||||
#include <ctime>
|
||||
#include <functional>
|
||||
#include <gsl/gsl>
|
||||
|
||||
#include <crl/crl.h>
|
||||
#include "base/build_config.h"
|
||||
#include "base/ordered_set.h"
|
||||
#include "base/unique_function.h"
|
||||
@@ -39,5 +39,8 @@ using uint64 = quint64;
|
||||
using float32 = float;
|
||||
using float64 = double;
|
||||
|
||||
using TimeMs = int64;
|
||||
using TimeId = int32;
|
||||
|
||||
#define qsl(s) QStringLiteral(s)
|
||||
#define qstr(s) QLatin1String((s), sizeof(s) - 1)
|
||||
|
||||
@@ -87,7 +87,7 @@ public:
|
||||
HiddenUrlClickHandler(QString url) : UrlClickHandler(url, false) {
|
||||
}
|
||||
QString copyToClipboardContextItemText() const override {
|
||||
return url().isEmpty()
|
||||
return (url().isEmpty() || url().startsWith(qstr("internal:")))
|
||||
? QString()
|
||||
: UrlClickHandler::copyToClipboardContextItemText();
|
||||
}
|
||||
|
||||
@@ -76,8 +76,10 @@ QString filedialogDefaultName(
|
||||
if (skipExistance) {
|
||||
name = base + extension;
|
||||
} else {
|
||||
QDir dir(directoryPath);
|
||||
QString nameBase = dir.absolutePath() + '/' + base;
|
||||
QDir directory(directoryPath);
|
||||
const auto dir = directory.absolutePath();
|
||||
const auto nameBase = (dir.endsWith('/') ? dir : (dir + '/'))
|
||||
+ base;
|
||||
name = nameBase + extension;
|
||||
for (int i = 0; QFileInfo(name).exists(); ++i) {
|
||||
name = nameBase + qsl(" (%1)").arg(i + 2) + extension;
|
||||
@@ -90,14 +92,16 @@ QString filedialogNextFilename(
|
||||
const QString &name,
|
||||
const QString &cur,
|
||||
const QString &path) {
|
||||
QDir dir(path.isEmpty() ? cDialogLastPath() : path);
|
||||
QDir directory(path.isEmpty() ? cDialogLastPath() : path);
|
||||
int32 extIndex = name.lastIndexOf('.');
|
||||
QString prefix = name, extension;
|
||||
if (extIndex >= 0) {
|
||||
extension = name.mid(extIndex);
|
||||
prefix = name.mid(0, extIndex);
|
||||
}
|
||||
QString nameBase = dir.absolutePath() + '/' + prefix, result = nameBase + extension;
|
||||
const auto dir = directory.absolutePath();
|
||||
const auto nameBase = (dir.endsWith('/') ? dir : (dir + '/')) + prefix;
|
||||
auto result = nameBase + extension;
|
||||
for (int i = 0; result.toLower() != cur.toLower() && QFileInfo(result).exists(); ++i) {
|
||||
result = nameBase + qsl(" (%1)").arg(i + 2) + extension;
|
||||
}
|
||||
|
||||
@@ -151,7 +151,8 @@ void Launcher::prepareSettings() {
|
||||
info = info.symLinkTarget();
|
||||
}
|
||||
if (info.exists()) {
|
||||
gExeDir = info.absoluteDir().absolutePath() + '/';
|
||||
const auto dir = info.absoluteDir().absolutePath();
|
||||
gExeDir = (dir.endsWith('/') ? dir : (dir + '/'));
|
||||
gExeName = info.fileName();
|
||||
}
|
||||
}
|
||||
|
||||
93
Telegram/SourceFiles/core/mime_type.cpp
Normal file
93
Telegram/SourceFiles/core/mime_type.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "core/mime_type.h"
|
||||
|
||||
namespace Core {
|
||||
|
||||
MimeType::MimeType(const QMimeType &type) : _typeStruct(type) {
|
||||
}
|
||||
|
||||
MimeType::MimeType(Known type) : _type(type) {
|
||||
}
|
||||
|
||||
QStringList MimeType::globPatterns() const {
|
||||
switch (_type) {
|
||||
case Known::WebP: return QStringList(qsl("*.webp"));
|
||||
case Known::TDesktopTheme: return QStringList(qsl("*.tdesktop-theme"));
|
||||
case Known::TDesktopPalette: return QStringList(qsl("*.tdesktop-palette"));
|
||||
default: break;
|
||||
}
|
||||
return _typeStruct.globPatterns();
|
||||
}
|
||||
|
||||
QString MimeType::filterString() const {
|
||||
switch (_type) {
|
||||
case Known::WebP: return qsl("WebP image (*.webp)");
|
||||
case Known::TDesktopTheme: return qsl("Theme files (*.tdesktop-theme)");
|
||||
case Known::TDesktopPalette: return qsl("Palette files (*.tdesktop-palette)");
|
||||
default: break;
|
||||
}
|
||||
return _typeStruct.filterString();
|
||||
}
|
||||
|
||||
QString MimeType::name() const {
|
||||
switch (_type) {
|
||||
case Known::WebP: return qsl("image/webp");
|
||||
case Known::TDesktopTheme: return qsl("application/x-tdesktop-theme");
|
||||
case Known::TDesktopPalette: return qsl("application/x-tdesktop-palette");
|
||||
default: break;
|
||||
}
|
||||
return _typeStruct.name();
|
||||
}
|
||||
|
||||
MimeType MimeTypeForName(const QString &mime) {
|
||||
if (mime == qsl("image/webp")) {
|
||||
return MimeType(MimeType::Known::WebP);
|
||||
} else if (mime == qsl("application/x-tdesktop-theme")) {
|
||||
return MimeType(MimeType::Known::TDesktopTheme);
|
||||
} else if (mime == qsl("application/x-tdesktop-palette")) {
|
||||
return MimeType(MimeType::Known::TDesktopPalette);
|
||||
}
|
||||
return MimeType(QMimeDatabase().mimeTypeForName(mime));
|
||||
}
|
||||
|
||||
MimeType MimeTypeForFile(const QFileInfo &file) {
|
||||
QString path = file.absoluteFilePath();
|
||||
if (path.endsWith(qstr(".webp"), Qt::CaseInsensitive)) {
|
||||
return MimeType(MimeType::Known::WebP);
|
||||
} else if (path.endsWith(qstr(".tdesktop-theme"), Qt::CaseInsensitive)) {
|
||||
return MimeType(MimeType::Known::TDesktopTheme);
|
||||
} else if (path.endsWith(qstr(".tdesktop-palette"), Qt::CaseInsensitive)) {
|
||||
return MimeType(MimeType::Known::TDesktopPalette);
|
||||
}
|
||||
|
||||
{
|
||||
QFile f(path);
|
||||
if (f.open(QIODevice::ReadOnly)) {
|
||||
QByteArray magic = f.read(12);
|
||||
if (magic.size() >= 12) {
|
||||
if (!memcmp(magic.constData(), "RIFF", 4) && !memcmp(magic.constData() + 8, "WEBP", 4)) {
|
||||
return MimeType(MimeType::Known::WebP);
|
||||
}
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
return MimeType(QMimeDatabase().mimeTypeForFile(file));
|
||||
}
|
||||
|
||||
MimeType MimeTypeForData(const QByteArray &data) {
|
||||
if (data.size() >= 12) {
|
||||
if (!memcmp(data.constData(), "RIFF", 4) && !memcmp(data.constData() + 8, "WEBP", 4)) {
|
||||
return MimeType(MimeType::Known::WebP);
|
||||
}
|
||||
}
|
||||
return MimeType(QMimeDatabase().mimeTypeForData(data));
|
||||
}
|
||||
|
||||
} // namespace Core
|
||||
41
Telegram/SourceFiles/core/mime_type.h
Normal file
41
Telegram/SourceFiles/core/mime_type.h
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QStringList>
|
||||
#include <QtCore/QMimeType>
|
||||
|
||||
namespace Core {
|
||||
|
||||
class MimeType {
|
||||
public:
|
||||
enum class Known {
|
||||
Unknown,
|
||||
TDesktopTheme,
|
||||
TDesktopPalette,
|
||||
WebP,
|
||||
};
|
||||
|
||||
explicit MimeType(const QMimeType &type);
|
||||
explicit MimeType(Known type);
|
||||
QStringList globPatterns() const;
|
||||
QString filterString() const;
|
||||
QString name() const;
|
||||
|
||||
private:
|
||||
QMimeType _typeStruct;
|
||||
Known _type = Known::Unknown;
|
||||
|
||||
};
|
||||
|
||||
MimeType MimeTypeForName(const QString &mime);
|
||||
MimeType MimeTypeForFile(const QFileInfo &file);
|
||||
MimeType MimeTypeForData(const QByteArray &data);
|
||||
|
||||
} // namespace Core
|
||||
@@ -1254,7 +1254,7 @@ MtpChecker::MtpChecker(QPointer<MTP::Instance> instance, bool testing)
|
||||
void MtpChecker::start() {
|
||||
if (!_mtp.valid()) {
|
||||
LOG(("Update Info: MTP is unavailable."));
|
||||
InvokeQueued(this, [=] { fail(); });
|
||||
crl::on_main(this, [=] { fail(); });
|
||||
return;
|
||||
}
|
||||
constexpr auto kFeedUsername = "tdhbcfeed";
|
||||
|
||||
@@ -54,11 +54,13 @@ static_assert(sizeof(MTPdouble) == 8, "Basic types size check failed");
|
||||
// Unixtime functions
|
||||
|
||||
namespace {
|
||||
|
||||
std::atomic<int> GlobalAtomicRequestId = 0;
|
||||
|
||||
QReadWriteLock unixtimeLock;
|
||||
volatile int32 unixtimeDelta = 0;
|
||||
volatile bool unixtimeWasSet = false;
|
||||
volatile uint64 _msgIdStart, _msgIdLocal = 0, _msgIdMsStart;
|
||||
int32 _reqId = 0;
|
||||
|
||||
void _initMsgIdConstants() {
|
||||
#ifdef Q_OS_WIN
|
||||
@@ -433,12 +435,12 @@ uint64 msgid() {
|
||||
return result + (_msgIdLocal += 4);
|
||||
}
|
||||
|
||||
int32 reqid() {
|
||||
QWriteLocker locker(&unixtimeLock);
|
||||
if (_reqId == INT_MAX) {
|
||||
_reqId = 0;
|
||||
int GetNextRequestId() {
|
||||
const auto result = ++GlobalAtomicRequestId;
|
||||
if (result == std::numeric_limits<int>::max() / 2) {
|
||||
GlobalAtomicRequestId = 0;
|
||||
}
|
||||
return ++_reqId;
|
||||
return result;
|
||||
}
|
||||
|
||||
// crc32 hash, taken somewhere from the internet
|
||||
@@ -1003,76 +1005,3 @@ QString rusKeyboardLayoutSwitch(const QString &from) {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QStringList MimeType::globPatterns() const {
|
||||
switch (_type) {
|
||||
case Known::WebP: return QStringList(qsl("*.webp"));
|
||||
case Known::TDesktopTheme: return QStringList(qsl("*.tdesktop-theme"));
|
||||
case Known::TDesktopPalette: return QStringList(qsl("*.tdesktop-palette"));
|
||||
default: break;
|
||||
}
|
||||
return _typeStruct.globPatterns();
|
||||
}
|
||||
QString MimeType::filterString() const {
|
||||
switch (_type) {
|
||||
case Known::WebP: return qsl("WebP image (*.webp)");
|
||||
case Known::TDesktopTheme: return qsl("Theme files (*.tdesktop-theme)");
|
||||
case Known::TDesktopPalette: return qsl("Palette files (*.tdesktop-palette)");
|
||||
default: break;
|
||||
}
|
||||
return _typeStruct.filterString();
|
||||
}
|
||||
QString MimeType::name() const {
|
||||
switch (_type) {
|
||||
case Known::WebP: return qsl("image/webp");
|
||||
case Known::TDesktopTheme: return qsl("application/x-tdesktop-theme");
|
||||
case Known::TDesktopPalette: return qsl("application/x-tdesktop-palette");
|
||||
default: break;
|
||||
}
|
||||
return _typeStruct.name();
|
||||
}
|
||||
|
||||
MimeType mimeTypeForName(const QString &mime) {
|
||||
if (mime == qsl("image/webp")) {
|
||||
return MimeType(MimeType::Known::WebP);
|
||||
} else if (mime == qsl("application/x-tdesktop-theme")) {
|
||||
return MimeType(MimeType::Known::TDesktopTheme);
|
||||
} else if (mime == qsl("application/x-tdesktop-palette")) {
|
||||
return MimeType(MimeType::Known::TDesktopPalette);
|
||||
}
|
||||
return MimeType(QMimeDatabase().mimeTypeForName(mime));
|
||||
}
|
||||
|
||||
MimeType mimeTypeForFile(const QFileInfo &file) {
|
||||
QString path = file.absoluteFilePath();
|
||||
if (path.endsWith(qstr(".webp"), Qt::CaseInsensitive)) {
|
||||
return MimeType(MimeType::Known::WebP);
|
||||
} else if (path.endsWith(qstr(".tdesktop-theme"), Qt::CaseInsensitive)) {
|
||||
return MimeType(MimeType::Known::TDesktopTheme);
|
||||
} else if (path.endsWith(qstr(".tdesktop-palette"), Qt::CaseInsensitive)) {
|
||||
return MimeType(MimeType::Known::TDesktopPalette);
|
||||
}
|
||||
|
||||
{
|
||||
QFile f(path);
|
||||
if (f.open(QIODevice::ReadOnly)) {
|
||||
QByteArray magic = f.read(12);
|
||||
if (magic.size() >= 12) {
|
||||
if (!memcmp(magic.constData(), "RIFF", 4) && !memcmp(magic.constData() + 8, "WEBP", 4)) {
|
||||
return MimeType(MimeType::Known::WebP);
|
||||
}
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
return MimeType(QMimeDatabase().mimeTypeForFile(file));
|
||||
}
|
||||
|
||||
MimeType mimeTypeForData(const QByteArray &data) {
|
||||
if (data.size() >= 12) {
|
||||
if (!memcmp(data.constData(), "RIFF", 4) && !memcmp(data.constData() + 8, "WEBP", 4)) {
|
||||
return MimeType(MimeType::Known::WebP);
|
||||
}
|
||||
}
|
||||
return MimeType(QMimeDatabase().mimeTypeForData(data));
|
||||
}
|
||||
|
||||
@@ -7,9 +7,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "logs.h"
|
||||
#include "core/basic_types.h"
|
||||
#include "base/flags.h"
|
||||
#include "base/algorithm.h"
|
||||
#include "base/assertion.h"
|
||||
|
||||
#include <QtCore/QReadWriteLock>
|
||||
#include <QtCore/QRegularExpression>
|
||||
#include <QtNetwork/QNetworkProxy>
|
||||
|
||||
#include <cmath>
|
||||
#include <set>
|
||||
|
||||
// Define specializations for QByteArray for Qt 5.3.2, because
|
||||
// QByteArray in Qt 5.3.2 doesn't declare "pointer" subtype.
|
||||
@@ -179,33 +188,11 @@ inline void accumulate_max(T &a, const T &b) { if (a < b) a = b; }
|
||||
template <typename T>
|
||||
inline void accumulate_min(T &a, const T &b) { if (a > b) a = b; }
|
||||
|
||||
class Exception : public std::exception {
|
||||
public:
|
||||
Exception(const QString &msg, bool isFatal = true) : _fatal(isFatal), _msg(msg.toUtf8()) {
|
||||
LOG(("Exception: %1").arg(msg));
|
||||
}
|
||||
bool fatal() const {
|
||||
return _fatal;
|
||||
}
|
||||
|
||||
virtual const char *what() const throw() {
|
||||
return _msg.constData();
|
||||
}
|
||||
virtual ~Exception() throw() {
|
||||
}
|
||||
|
||||
private:
|
||||
bool _fatal;
|
||||
QByteArray _msg;
|
||||
|
||||
};
|
||||
|
||||
using TimeId = int32;
|
||||
void unixtimeInit();
|
||||
void unixtimeSet(TimeId serverTime, bool force = false);
|
||||
TimeId unixtime();
|
||||
uint64 msgid();
|
||||
int32 reqid();
|
||||
int GetNextRequestId();
|
||||
|
||||
QDateTime ParseDateTime(TimeId serverTime);
|
||||
|
||||
@@ -224,7 +211,6 @@ void finish();
|
||||
|
||||
}
|
||||
|
||||
using TimeMs = int64;
|
||||
bool checkms(); // returns true if time has changed
|
||||
TimeMs getms(bool checked = false);
|
||||
|
||||
@@ -454,35 +440,6 @@ enum DBIPeerReportSpamStatus {
|
||||
dbiprsRequesting = 5, // requesting the cloud setting right now
|
||||
};
|
||||
|
||||
class MimeType {
|
||||
public:
|
||||
enum class Known {
|
||||
Unknown,
|
||||
TDesktopTheme,
|
||||
TDesktopPalette,
|
||||
WebP,
|
||||
};
|
||||
|
||||
MimeType(const QMimeType &type) : _typeStruct(type) {
|
||||
}
|
||||
MimeType(Known type) : _type(type) {
|
||||
}
|
||||
QStringList globPatterns() const;
|
||||
QString filterString() const;
|
||||
QString name() const;
|
||||
|
||||
private:
|
||||
QMimeType _typeStruct;
|
||||
Known _type = Known::Unknown;
|
||||
|
||||
};
|
||||
|
||||
MimeType mimeTypeForName(const QString &mime);
|
||||
MimeType mimeTypeForFile(const QFileInfo &file);
|
||||
MimeType mimeTypeForData(const QByteArray &data);
|
||||
|
||||
#include <cmath>
|
||||
|
||||
inline int rowscount(int fullCount, int countPerRow) {
|
||||
return (fullCount + countPerRow - 1) / countPerRow;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
#define BETA_VERSION_MACRO (0ULL)
|
||||
|
||||
constexpr int AppVersion = 1003004;
|
||||
constexpr str_const AppVersionStr = "1.3.4";
|
||||
constexpr bool AppAlphaVersion = true;
|
||||
constexpr int AppVersion = 1003008;
|
||||
constexpr str_const AppVersionStr = "1.3.8";
|
||||
constexpr bool AppAlphaVersion = false;
|
||||
constexpr uint64 AppBetaVersion = BETA_VERSION_MACRO;
|
||||
|
||||
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "inline_bots/inline_bot_layout_item.h"
|
||||
#include "mainwidget.h"
|
||||
#include "core/file_utilities.h"
|
||||
#include "core/mime_type.h"
|
||||
#include "media/media_audio.h"
|
||||
#include "storage/localstorage.h"
|
||||
#include "platform/platform_specific.h"
|
||||
@@ -192,7 +193,7 @@ QString documentSaveFilename(const DocumentData *data, bool forceSavingAs = fals
|
||||
}
|
||||
|
||||
QString name, filter, caption, prefix;
|
||||
MimeType mimeType = mimeTypeForName(data->mimeString());
|
||||
const auto mimeType = Core::MimeTypeForName(data->mimeString());
|
||||
QStringList p = mimeType.globPatterns();
|
||||
QString pattern = p.isEmpty() ? QString() : p.front();
|
||||
if (data->isVoiceMessage()) {
|
||||
|
||||
@@ -52,6 +52,9 @@ void applyPeerCloudDraft(PeerId peerId, const MTPDdraftMessage &draft) {
|
||||
? TextUtilities::EntitiesFromMTP(draft.ventities.v)
|
||||
: EntitiesInText())
|
||||
};
|
||||
if (history->skipCloudDraft(textWithTags.text, draft.vdate.v)) {
|
||||
return;
|
||||
}
|
||||
auto replyTo = draft.has_reply_to_msg_id() ? draft.vreply_to_msg_id.v : MsgId(0);
|
||||
auto cloudDraft = std::make_unique<Draft>(textWithTags, replyTo, MessageCursor(QFIXED_MAX, QFIXED_MAX, QFIXED_MAX), draft.is_no_webpage());
|
||||
cloudDraft->date = draft.vdate.v;
|
||||
|
||||
@@ -10,6 +10,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "observer_peer.h"
|
||||
#include "auth_session.h"
|
||||
#include "apiwrap.h"
|
||||
#include "export/export_controller.h"
|
||||
#include "export/view/export_view_panel_controller.h"
|
||||
#include "window/notifications_manager.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item_components.h"
|
||||
@@ -68,6 +70,84 @@ Session::Session(not_null<AuthSession*> session)
|
||||
setupChannelLeavingViewer();
|
||||
}
|
||||
|
||||
void Session::startExport() {
|
||||
if (_exportPanel) {
|
||||
_exportPanel->activatePanel();
|
||||
return;
|
||||
}
|
||||
_export = std::make_unique<Export::ControllerWrap>();
|
||||
_exportPanel = std::make_unique<Export::View::PanelController>(
|
||||
_export.get());
|
||||
|
||||
_exportViewChanges.fire(_exportPanel.get());
|
||||
|
||||
_exportPanel->stopRequests(
|
||||
) | rpl::start_with_next([=] {
|
||||
LOG(("Export Info: Stop requested."));
|
||||
stopExport();
|
||||
}, _export->lifetime());
|
||||
}
|
||||
|
||||
void Session::suggestStartExport(TimeId availableAt) {
|
||||
_exportAvailableAt = availableAt;
|
||||
suggestStartExport();
|
||||
}
|
||||
|
||||
void Session::suggestStartExport() {
|
||||
if (_exportAvailableAt <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto now = unixtime();
|
||||
const auto left = (_exportAvailableAt <= now)
|
||||
? 0
|
||||
: (_exportAvailableAt - now);
|
||||
if (left) {
|
||||
App::CallDelayed(
|
||||
std::min(left + 5, 3600) * TimeMs(1000),
|
||||
_session,
|
||||
[=] { suggestStartExport(); });
|
||||
} else if (_export) {
|
||||
Export::View::ClearSuggestStart();
|
||||
} else {
|
||||
Export::View::SuggestStart();
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<Export::View::PanelController*> Session::currentExportView(
|
||||
) const {
|
||||
return _exportViewChanges.events_starting_with(_exportPanel.get());
|
||||
}
|
||||
|
||||
bool Session::exportInProgress() const {
|
||||
return _export != nullptr;
|
||||
}
|
||||
|
||||
void Session::stopExportWithConfirmation(FnMut<void()> callback) {
|
||||
if (!_exportPanel) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
auto closeAndCall = [=, callback = std::move(callback)]() mutable {
|
||||
auto saved = std::move(callback);
|
||||
LOG(("Export Info: Stop With Confirmation."));
|
||||
stopExport();
|
||||
if (saved) {
|
||||
saved();
|
||||
}
|
||||
};
|
||||
_exportPanel->stopWithConfirmation(std::move(closeAndCall));
|
||||
}
|
||||
|
||||
void Session::stopExport() {
|
||||
if (_exportPanel) {
|
||||
LOG(("Export Info: Destroying."));
|
||||
_exportPanel = nullptr;
|
||||
_exportViewChanges.fire(nullptr);
|
||||
}
|
||||
_export = nullptr;
|
||||
}
|
||||
|
||||
void Session::setupContactViewsViewer() {
|
||||
Notify::PeerUpdateViewer(
|
||||
Notify::PeerUpdate::Flag::UserIsContact
|
||||
|
||||
@@ -27,6 +27,13 @@ class Reader;
|
||||
} // namespace Clip
|
||||
} // namespace Media
|
||||
|
||||
namespace Export {
|
||||
class ControllerWrap;
|
||||
namespace View {
|
||||
class PanelController;
|
||||
} // namespace View
|
||||
} // namespace Export
|
||||
|
||||
namespace Data {
|
||||
|
||||
class Feed;
|
||||
@@ -44,6 +51,13 @@ public:
|
||||
return *_session;
|
||||
}
|
||||
|
||||
void startExport();
|
||||
void suggestStartExport(TimeId availableAt);
|
||||
rpl::producer<Export::View::PanelController*> currentExportView() const;
|
||||
bool exportInProgress() const;
|
||||
void stopExportWithConfirmation(FnMut<void()> callback);
|
||||
void stopExport();
|
||||
|
||||
[[nodiscard]] base::Variable<bool> &contactsLoaded() {
|
||||
return _contactsLoaded;
|
||||
}
|
||||
@@ -395,6 +409,8 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
void suggestStartExport();
|
||||
|
||||
void setupContactViewsViewer();
|
||||
void setupChannelLeavingViewer();
|
||||
void photoApplyFields(
|
||||
@@ -489,6 +505,11 @@ private:
|
||||
|
||||
not_null<AuthSession*> _session;
|
||||
|
||||
std::unique_ptr<Export::ControllerWrap> _export;
|
||||
std::unique_ptr<Export::View::PanelController> _exportPanel;
|
||||
rpl::event_stream<Export::View::PanelController*> _exportViewChanges;
|
||||
TimeId _exportAvailableAt = 0;
|
||||
|
||||
base::Variable<bool> _contactsLoaded = { false };
|
||||
base::Variable<bool> _allChatsLoaded = { false };
|
||||
base::Observable<void> _moreChatsLoaded;
|
||||
|
||||
1277
Telegram/SourceFiles/export/data/export_data_types.cpp
Normal file
1277
Telegram/SourceFiles/export/data/export_data_types.cpp
Normal file
File diff suppressed because it is too large
Load Diff
533
Telegram/SourceFiles/export/data/export_data_types.h
Normal file
533
Telegram/SourceFiles/export/data/export_data_types.h
Normal file
@@ -0,0 +1,533 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "scheme.h"
|
||||
#include "base/optional.h"
|
||||
#include "base/variant.h"
|
||||
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QByteArray>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace Export {
|
||||
struct Settings;
|
||||
namespace Data {
|
||||
|
||||
using Utf8String = QByteArray;
|
||||
using PeerId = uint64;
|
||||
|
||||
PeerId UserPeerId(int32 userId);
|
||||
PeerId ChatPeerId(int32 chatId);
|
||||
int32 BarePeerId(PeerId peerId);
|
||||
|
||||
Utf8String ParseString(const MTPstring &data);
|
||||
|
||||
Utf8String FillLeft(const Utf8String &data, int length, char filler);
|
||||
|
||||
template <typename Type>
|
||||
inline auto NumberToString(Type value, int length = 0, char filler = '0')
|
||||
-> std::enable_if_t<std::is_arithmetic_v<Type>, Utf8String> {
|
||||
const auto result = std::to_string(value);
|
||||
return FillLeft(
|
||||
Utf8String(result.data(), int(result.size())),
|
||||
length,
|
||||
filler);
|
||||
}
|
||||
|
||||
struct UserpicsInfo {
|
||||
int count = 0;
|
||||
};
|
||||
|
||||
struct FileLocation {
|
||||
int dcId = 0;
|
||||
MTPInputFileLocation data;
|
||||
|
||||
explicit operator bool() const {
|
||||
return dcId != 0;
|
||||
}
|
||||
};
|
||||
|
||||
struct File {
|
||||
enum class SkipReason {
|
||||
None,
|
||||
Unavailable,
|
||||
FileType,
|
||||
FileSize,
|
||||
};
|
||||
FileLocation location;
|
||||
int size = 0;
|
||||
QByteArray content;
|
||||
|
||||
QString suggestedPath;
|
||||
|
||||
QString relativePath;
|
||||
SkipReason skipReason = SkipReason::None;
|
||||
};
|
||||
|
||||
struct Image {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
File file;
|
||||
};
|
||||
|
||||
struct Photo {
|
||||
uint64 id = 0;
|
||||
TimeId date = 0;
|
||||
|
||||
Image image;
|
||||
};
|
||||
|
||||
struct Document {
|
||||
uint64 id = 0;
|
||||
TimeId date = 0;
|
||||
|
||||
File file;
|
||||
|
||||
Utf8String name;
|
||||
Utf8String mime;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
Utf8String stickerEmoji;
|
||||
Utf8String songPerformer;
|
||||
Utf8String songTitle;
|
||||
int duration = 0;
|
||||
|
||||
bool isSticker = false;
|
||||
bool isAnimated = false;
|
||||
bool isVideoMessage = false;
|
||||
bool isVoiceMessage = false;
|
||||
bool isVideoFile = false;
|
||||
bool isAudioFile = false;
|
||||
};
|
||||
|
||||
struct GeoPoint {
|
||||
float64 latitude = 0.;
|
||||
float64 longitude = 0.;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
struct Venue {
|
||||
GeoPoint point;
|
||||
Utf8String title;
|
||||
Utf8String address;
|
||||
};
|
||||
|
||||
struct Game {
|
||||
uint64 id = 0;
|
||||
Utf8String shortName;
|
||||
Utf8String title;
|
||||
Utf8String description;
|
||||
|
||||
int32 botId = 0;
|
||||
};
|
||||
|
||||
struct Invoice {
|
||||
Utf8String title;
|
||||
Utf8String description;
|
||||
Utf8String currency;
|
||||
uint64 amount = 0;
|
||||
int32 receiptMsgId = 0;
|
||||
};
|
||||
|
||||
struct UserpicsSlice {
|
||||
std::vector<Photo> list;
|
||||
};
|
||||
|
||||
UserpicsSlice ParseUserpicsSlice(
|
||||
const MTPVector<MTPPhoto> &data,
|
||||
int baseIndex);
|
||||
|
||||
struct ContactInfo {
|
||||
int32 userId = 0;
|
||||
Utf8String firstName;
|
||||
Utf8String lastName;
|
||||
Utf8String phoneNumber;
|
||||
TimeId date = 0;
|
||||
|
||||
Utf8String name() const;
|
||||
};
|
||||
|
||||
ContactInfo ParseContactInfo(const MTPUser &data);
|
||||
ContactInfo ParseContactInfo(const MTPDmessageMediaContact &data);
|
||||
|
||||
struct User {
|
||||
ContactInfo info;
|
||||
Utf8String username;
|
||||
bool isBot = false;
|
||||
bool isSelf = false;
|
||||
|
||||
MTPInputUser input = MTP_inputUserEmpty();
|
||||
|
||||
Utf8String name() const;
|
||||
};
|
||||
|
||||
User ParseUser(const MTPUser &data);
|
||||
std::map<int32, User> ParseUsersList(const MTPVector<MTPUser> &data);
|
||||
|
||||
struct Chat {
|
||||
int32 id = 0;
|
||||
Utf8String title;
|
||||
Utf8String username;
|
||||
bool isBroadcast = false;
|
||||
bool isSupergroup = false;
|
||||
|
||||
MTPInputPeer input = MTP_inputPeerEmpty();
|
||||
};
|
||||
|
||||
Chat ParseChat(const MTPChat &data);
|
||||
std::map<int32, Chat> ParseChatsList(const MTPVector<MTPChat> &data);
|
||||
|
||||
struct Peer {
|
||||
PeerId id() const;
|
||||
Utf8String name() const;
|
||||
MTPInputPeer input() const;
|
||||
|
||||
const User *user() const;
|
||||
const Chat *chat() const;
|
||||
|
||||
base::variant<User, Chat> data;
|
||||
|
||||
};
|
||||
|
||||
std::map<PeerId, Peer> ParsePeersLists(
|
||||
const MTPVector<MTPUser> &users,
|
||||
const MTPVector<MTPChat> &chats);
|
||||
|
||||
struct PersonalInfo {
|
||||
User user;
|
||||
Utf8String bio;
|
||||
};
|
||||
|
||||
PersonalInfo ParsePersonalInfo(const MTPUserFull &data);
|
||||
|
||||
struct TopPeer {
|
||||
Peer peer;
|
||||
float64 rating = 0.;
|
||||
};
|
||||
|
||||
struct ContactsList {
|
||||
std::vector<ContactInfo> list;
|
||||
std::vector<TopPeer> correspondents;
|
||||
std::vector<TopPeer> inlineBots;
|
||||
std::vector<TopPeer> phoneCalls;
|
||||
};
|
||||
|
||||
ContactsList ParseContactsList(const MTPcontacts_Contacts &data);
|
||||
ContactsList ParseContactsList(const MTPVector<MTPSavedContact> &data);
|
||||
std::vector<int> SortedContactsIndices(const ContactsList &data);
|
||||
bool AppendTopPeers(ContactsList &to, const MTPcontacts_TopPeers &data);
|
||||
|
||||
struct Session {
|
||||
Utf8String platform;
|
||||
Utf8String deviceModel;
|
||||
Utf8String systemVersion;
|
||||
Utf8String applicationName;
|
||||
Utf8String applicationVersion;
|
||||
TimeId created = 0;
|
||||
TimeId lastActive = 0;
|
||||
Utf8String ip;
|
||||
Utf8String country;
|
||||
Utf8String region;
|
||||
};
|
||||
|
||||
struct WebSession {
|
||||
Utf8String botUsername;
|
||||
Utf8String domain;
|
||||
Utf8String browser;
|
||||
Utf8String platform;
|
||||
TimeId created = 0;
|
||||
TimeId lastActive = 0;
|
||||
Utf8String ip;
|
||||
Utf8String region;
|
||||
};
|
||||
|
||||
struct SessionsList {
|
||||
std::vector<Session> list;
|
||||
std::vector<WebSession> webList;
|
||||
};
|
||||
|
||||
SessionsList ParseSessionsList(const MTPaccount_Authorizations &data);
|
||||
SessionsList ParseWebSessionsList(const MTPaccount_WebAuthorizations &data);
|
||||
|
||||
struct UnsupportedMedia {
|
||||
};
|
||||
|
||||
struct Media {
|
||||
base::optional_variant<
|
||||
Photo,
|
||||
Document,
|
||||
ContactInfo,
|
||||
GeoPoint,
|
||||
Venue,
|
||||
Game,
|
||||
Invoice,
|
||||
UnsupportedMedia> content;
|
||||
TimeId ttl = 0;
|
||||
|
||||
File &file();
|
||||
const File &file() const;
|
||||
};
|
||||
|
||||
struct ParseMediaContext {
|
||||
int photos = 0;
|
||||
int audios = 0;
|
||||
int videos = 0;
|
||||
int files = 0;
|
||||
int32 botId = 0;
|
||||
};
|
||||
|
||||
Media ParseMedia(
|
||||
ParseMediaContext &context,
|
||||
const MTPMessageMedia &data,
|
||||
const QString &folder);
|
||||
|
||||
struct ActionChatCreate {
|
||||
Utf8String title;
|
||||
std::vector<int32> userIds;
|
||||
};
|
||||
|
||||
struct ActionChatEditTitle {
|
||||
Utf8String title;
|
||||
};
|
||||
|
||||
struct ActionChatEditPhoto {
|
||||
Photo photo;
|
||||
};
|
||||
|
||||
struct ActionChatDeletePhoto {
|
||||
};
|
||||
|
||||
struct ActionChatAddUser {
|
||||
std::vector<int32> userIds;
|
||||
};
|
||||
|
||||
struct ActionChatDeleteUser {
|
||||
int32 userId = 0;
|
||||
};
|
||||
|
||||
struct ActionChatJoinedByLink {
|
||||
int32 inviterId = 0;
|
||||
};
|
||||
|
||||
struct ActionChannelCreate {
|
||||
Utf8String title;
|
||||
};
|
||||
|
||||
struct ActionChatMigrateTo {
|
||||
int32 channelId = 0;
|
||||
};
|
||||
|
||||
struct ActionChannelMigrateFrom {
|
||||
Utf8String title;
|
||||
int32 chatId = 0;
|
||||
};
|
||||
|
||||
struct ActionPinMessage {
|
||||
};
|
||||
|
||||
struct ActionHistoryClear {
|
||||
};
|
||||
|
||||
struct ActionGameScore {
|
||||
uint64 gameId = 0;
|
||||
int score = 0;
|
||||
};
|
||||
|
||||
struct ActionPaymentSent {
|
||||
Utf8String currency;
|
||||
uint64 amount = 0;
|
||||
};
|
||||
|
||||
struct ActionPhoneCall {
|
||||
enum class DiscardReason {
|
||||
Unknown,
|
||||
Missed,
|
||||
Disconnect,
|
||||
Hangup,
|
||||
Busy,
|
||||
};
|
||||
DiscardReason discardReason = DiscardReason::Unknown;
|
||||
int duration = 0;
|
||||
};
|
||||
|
||||
struct ActionScreenshotTaken {
|
||||
};
|
||||
|
||||
struct ActionCustomAction {
|
||||
Utf8String message;
|
||||
};
|
||||
|
||||
struct ActionBotAllowed {
|
||||
Utf8String domain;
|
||||
};
|
||||
|
||||
struct ActionSecureValuesSent {
|
||||
enum class Type {
|
||||
PersonalDetails,
|
||||
Passport,
|
||||
DriverLicense,
|
||||
IdentityCard,
|
||||
InternalPassport,
|
||||
Address,
|
||||
UtilityBill,
|
||||
BankStatement,
|
||||
RentalAgreement,
|
||||
PassportRegistration,
|
||||
TemporaryRegistration,
|
||||
Phone,
|
||||
Email,
|
||||
};
|
||||
std::vector<Type> types;
|
||||
};
|
||||
|
||||
struct ServiceAction {
|
||||
base::optional_variant<
|
||||
ActionChatCreate,
|
||||
ActionChatEditTitle,
|
||||
ActionChatEditPhoto,
|
||||
ActionChatDeletePhoto,
|
||||
ActionChatAddUser,
|
||||
ActionChatDeleteUser,
|
||||
ActionChatJoinedByLink,
|
||||
ActionChannelCreate,
|
||||
ActionChatMigrateTo,
|
||||
ActionChannelMigrateFrom,
|
||||
ActionPinMessage,
|
||||
ActionHistoryClear,
|
||||
ActionGameScore,
|
||||
ActionPaymentSent,
|
||||
ActionPhoneCall,
|
||||
ActionScreenshotTaken,
|
||||
ActionCustomAction,
|
||||
ActionBotAllowed,
|
||||
ActionSecureValuesSent> content;
|
||||
};
|
||||
|
||||
ServiceAction ParseServiceAction(
|
||||
ParseMediaContext &context,
|
||||
const MTPMessageAction &data,
|
||||
const QString &mediaFolder);
|
||||
|
||||
struct TextPart {
|
||||
enum class Type {
|
||||
Text,
|
||||
Unknown,
|
||||
Mention,
|
||||
Hashtag,
|
||||
BotCommand,
|
||||
Url,
|
||||
Email,
|
||||
Bold,
|
||||
Italic,
|
||||
Code,
|
||||
Pre,
|
||||
TextUrl,
|
||||
MentionName,
|
||||
Phone,
|
||||
Cashtag,
|
||||
};
|
||||
Type type = Type::Text;
|
||||
Utf8String text;
|
||||
Utf8String additional;
|
||||
};
|
||||
|
||||
struct Message {
|
||||
int32 id = 0;
|
||||
int32 chatId = 0;
|
||||
TimeId date = 0;
|
||||
TimeId edited = 0;
|
||||
int32 fromId = 0;
|
||||
PeerId forwardedFromId = 0;
|
||||
Utf8String signature;
|
||||
int32 viaBotId = 0;
|
||||
int32 replyToMsgId = 0;
|
||||
std::vector<TextPart> text;
|
||||
Media media;
|
||||
ServiceAction action;
|
||||
|
||||
File &file();
|
||||
const File &file() const;
|
||||
};
|
||||
|
||||
Message ParseMessage(
|
||||
ParseMediaContext &context,
|
||||
const MTPMessage &data,
|
||||
const QString &mediaFolder);
|
||||
std::map<uint64, Message> ParseMessagesList(
|
||||
const MTPVector<MTPMessage> &data,
|
||||
const QString &mediaFolder);
|
||||
|
||||
struct DialogInfo {
|
||||
enum class Type {
|
||||
Unknown,
|
||||
Self,
|
||||
Personal,
|
||||
Bot,
|
||||
PrivateGroup,
|
||||
PrivateSupergroup,
|
||||
PublicSupergroup,
|
||||
PrivateChannel,
|
||||
PublicChannel,
|
||||
};
|
||||
Type type = Type::Unknown;
|
||||
Utf8String name;
|
||||
|
||||
MTPInputPeer input = MTP_inputPeerEmpty();
|
||||
int32 topMessageId = 0;
|
||||
TimeId topMessageDate = 0;
|
||||
PeerId peerId;
|
||||
|
||||
// User messages splits which contained that dialog.
|
||||
std::vector<int> splits;
|
||||
|
||||
// Filled after the whole dialogs list is accumulated.
|
||||
bool onlyMyMessages = false;
|
||||
QString relativePath;
|
||||
|
||||
// Filled when requesting dialog messages.
|
||||
std::vector<int> messagesCountPerSplit;
|
||||
};
|
||||
|
||||
struct DialogsInfo {
|
||||
std::vector<DialogInfo> list;
|
||||
};
|
||||
|
||||
DialogInfo::Type DialogTypeFromChat(const Chat &chat);
|
||||
|
||||
DialogsInfo ParseDialogsInfo(const MTPmessages_Dialogs &data);
|
||||
DialogsInfo ParseLeftChannelsInfo(const MTPmessages_Chats &data);
|
||||
void FinalizeDialogsInfo(DialogsInfo &info, const Settings &settings);
|
||||
void FinalizeLeftChannelsInfo(DialogsInfo &info, const Settings &settings);
|
||||
|
||||
struct MessagesSlice {
|
||||
std::vector<Message> list;
|
||||
std::map<PeerId, Peer> peers;
|
||||
};
|
||||
|
||||
MessagesSlice ParseMessagesSlice(
|
||||
ParseMediaContext &context,
|
||||
const MTPVector<MTPMessage> &data,
|
||||
const MTPVector<MTPUser> &users,
|
||||
const MTPVector<MTPChat> &chats,
|
||||
const QString &mediaFolder);
|
||||
|
||||
Utf8String FormatPhoneNumber(const Utf8String &phoneNumber);
|
||||
|
||||
Utf8String FormatDateTime(
|
||||
TimeId date,
|
||||
QChar dateSeparator = QChar('.'),
|
||||
QChar timeSeparator = QChar(':'),
|
||||
QChar separator = QChar(' '));
|
||||
|
||||
Utf8String FormatMoneyAmount(uint64 amount, const Utf8String ¤cy);
|
||||
|
||||
} // namespace Data
|
||||
} // namespace Export
|
||||
1393
Telegram/SourceFiles/export/export_api_wrap.cpp
Normal file
1393
Telegram/SourceFiles/export/export_api_wrap.cpp
Normal file
File diff suppressed because it is too large
Load Diff
209
Telegram/SourceFiles/export/export_api_wrap.h
Normal file
209
Telegram/SourceFiles/export/export_api_wrap.h
Normal file
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "mtproto/concurrent_sender.h"
|
||||
|
||||
namespace Export {
|
||||
namespace Data {
|
||||
struct File;
|
||||
struct Chat;
|
||||
struct FileLocation;
|
||||
struct PersonalInfo;
|
||||
struct UserpicsInfo;
|
||||
struct UserpicsSlice;
|
||||
struct ContactsList;
|
||||
struct SessionsList;
|
||||
struct DialogsInfo;
|
||||
struct DialogInfo;
|
||||
struct MessagesSlice;
|
||||
struct Message;
|
||||
} // namespace Data
|
||||
|
||||
namespace Output {
|
||||
struct Result;
|
||||
class Stats;
|
||||
} // namespace Output
|
||||
|
||||
struct Settings;
|
||||
|
||||
class ApiWrap {
|
||||
public:
|
||||
explicit ApiWrap(Fn<void(FnMut<void()>)> runner);
|
||||
|
||||
rpl::producer<RPCError> errors() const;
|
||||
rpl::producer<Output::Result> ioErrors() const;
|
||||
|
||||
struct StartInfo {
|
||||
int userpicsCount = 0;
|
||||
int dialogsCount = 0;
|
||||
int leftChannelsCount = 0;
|
||||
};
|
||||
void startExport(
|
||||
const Settings &settings,
|
||||
Output::Stats *stats,
|
||||
FnMut<void(StartInfo)> done);
|
||||
|
||||
void requestLeftChannelsList(
|
||||
Fn<bool(int count)> progress,
|
||||
FnMut<void(Data::DialogsInfo&&)> done);
|
||||
void requestDialogsList(
|
||||
Fn<bool(int count)> progress,
|
||||
FnMut<void(Data::DialogsInfo&&)> done);
|
||||
|
||||
void requestPersonalInfo(FnMut<void(Data::PersonalInfo&&)> done);
|
||||
|
||||
void requestOtherData(
|
||||
const QString &suggestedPath,
|
||||
FnMut<void(Data::File&&)> done);
|
||||
|
||||
struct DownloadProgress {
|
||||
QString path;
|
||||
int itemIndex = 0;
|
||||
int ready = 0;
|
||||
int total = 0;
|
||||
};
|
||||
void requestUserpics(
|
||||
FnMut<bool(Data::UserpicsInfo&&)> start,
|
||||
Fn<bool(DownloadProgress)> progress,
|
||||
Fn<bool(Data::UserpicsSlice&&)> slice,
|
||||
FnMut<void()> finish);
|
||||
|
||||
void requestContacts(FnMut<void(Data::ContactsList&&)> done);
|
||||
|
||||
void requestSessions(FnMut<void(Data::SessionsList&&)> done);
|
||||
|
||||
void requestMessages(
|
||||
const Data::DialogInfo &info,
|
||||
FnMut<bool(const Data::DialogInfo &)> start,
|
||||
Fn<bool(DownloadProgress)> progress,
|
||||
Fn<bool(Data::MessagesSlice&&)> slice,
|
||||
FnMut<void()> done);
|
||||
|
||||
void finishExport(FnMut<void()> done);
|
||||
void cancelExportFast();
|
||||
|
||||
~ApiWrap();
|
||||
|
||||
private:
|
||||
class LoadedFileCache;
|
||||
struct StartProcess;
|
||||
struct ContactsProcess;
|
||||
struct UserpicsProcess;
|
||||
struct OtherDataProcess;
|
||||
struct FileProcess;
|
||||
struct FileProgress;
|
||||
struct ChatsProcess;
|
||||
struct LeftChannelsProcess;
|
||||
struct DialogsProcess;
|
||||
struct ChatProcess;
|
||||
|
||||
void startMainSession(FnMut<void()> done);
|
||||
void sendNextStartRequest();
|
||||
void requestUserpicsCount();
|
||||
void requestSplitRanges();
|
||||
void requestDialogsCount();
|
||||
void requestLeftChannelsCount();
|
||||
void finishStartProcess();
|
||||
|
||||
void requestTopPeersSlice();
|
||||
|
||||
void handleUserpicsSlice(const MTPphotos_Photos &result);
|
||||
void loadUserpicsFiles(Data::UserpicsSlice &&slice);
|
||||
void loadNextUserpic();
|
||||
bool loadUserpicProgress(FileProgress value);
|
||||
void loadUserpicDone(const QString &relativePath);
|
||||
void finishUserpicsSlice();
|
||||
void finishUserpics();
|
||||
|
||||
void otherDataDone(const QString &relativePath);
|
||||
|
||||
bool useOnlyLastSplit() const;
|
||||
|
||||
void requestDialogsSlice();
|
||||
void appendDialogsSlice(Data::DialogsInfo &&info);
|
||||
void finishDialogsList();
|
||||
|
||||
void requestLeftChannelsSliceGeneric(FnMut<void()> done);
|
||||
void requestLeftChannelsSlice();
|
||||
void appendLeftChannelsSlice(Data::DialogsInfo &&info);
|
||||
|
||||
void appendChatsSlice(
|
||||
ChatsProcess &to,
|
||||
Data::DialogsInfo &&info,
|
||||
int splitIndex);
|
||||
|
||||
void requestMessagesCount(int localSplitIndex);
|
||||
void requestMessagesSlice();
|
||||
void requestChatMessages(
|
||||
int splitIndex,
|
||||
int offsetId,
|
||||
int addOffset,
|
||||
int limit,
|
||||
FnMut<void(MTPmessages_Messages&&)> done);
|
||||
void loadMessagesFiles(Data::MessagesSlice &&slice);
|
||||
void loadNextMessageFile();
|
||||
bool loadMessageFileProgress(FileProgress value);
|
||||
void loadMessageFileDone(const QString &relativePath);
|
||||
void finishMessagesSlice();
|
||||
void finishMessages();
|
||||
|
||||
bool processFileLoad(
|
||||
Data::File &file,
|
||||
Fn<bool(FileProgress)> progress,
|
||||
FnMut<void(QString)> done,
|
||||
Data::Message *message = nullptr);
|
||||
std::unique_ptr<FileProcess> prepareFileProcess(
|
||||
const Data::File &file) const;
|
||||
bool writePreloadedFile(Data::File &file);
|
||||
void loadFile(
|
||||
const Data::File &file,
|
||||
Fn<bool(FileProgress)> progress,
|
||||
FnMut<void(QString)> done);
|
||||
void loadFilePart();
|
||||
void filePartDone(int offset, const MTPupload_File &result);
|
||||
void filePartUnavailable();
|
||||
|
||||
template <typename Request>
|
||||
[[nodiscard]] auto mainRequest(Request &&request);
|
||||
|
||||
template <typename Request>
|
||||
[[nodiscard]] auto splitRequest(int index, Request &&request);
|
||||
|
||||
[[nodiscard]] auto fileRequest(
|
||||
const Data::FileLocation &location,
|
||||
int offset);
|
||||
|
||||
void error(RPCError &&error);
|
||||
void error(const QString &text);
|
||||
void ioError(const Output::Result &result);
|
||||
|
||||
MTP::ConcurrentSender _mtp;
|
||||
base::optional<uint64> _takeoutId;
|
||||
Output::Stats *_stats = nullptr;
|
||||
|
||||
std::unique_ptr<Settings> _settings;
|
||||
MTPInputUser _user = MTP_inputUserSelf();
|
||||
|
||||
std::unique_ptr<StartProcess> _startProcess;
|
||||
std::unique_ptr<LoadedFileCache> _fileCache;
|
||||
std::unique_ptr<ContactsProcess> _contactsProcess;
|
||||
std::unique_ptr<UserpicsProcess> _userpicsProcess;
|
||||
std::unique_ptr<OtherDataProcess> _otherDataProcess;
|
||||
std::unique_ptr<FileProcess> _fileProcess;
|
||||
std::unique_ptr<LeftChannelsProcess> _leftChannelsProcess;
|
||||
std::unique_ptr<DialogsProcess> _dialogsProcess;
|
||||
std::unique_ptr<ChatProcess> _chatProcess;
|
||||
QVector<MTPMessageRange> _splits;
|
||||
|
||||
rpl::event_stream<RPCError> _errors;
|
||||
rpl::event_stream<Output::Result> _ioErrors;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Export
|
||||
747
Telegram/SourceFiles/export/export_controller.cpp
Normal file
747
Telegram/SourceFiles/export/export_controller.cpp
Normal file
@@ -0,0 +1,747 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "export/export_controller.h"
|
||||
|
||||
#include "export/export_api_wrap.h"
|
||||
#include "export/export_settings.h"
|
||||
#include "export/data/export_data_types.h"
|
||||
#include "export/output/export_output_abstract.h"
|
||||
#include "export/output/export_output_result.h"
|
||||
#include "export/output/export_output_stats.h"
|
||||
|
||||
namespace Export {
|
||||
|
||||
auto kNullStateCallback = [](ProcessingState&) {};
|
||||
|
||||
class Controller {
|
||||
public:
|
||||
Controller(crl::weak_on_queue<Controller> weak);
|
||||
|
||||
rpl::producer<State> state() const;
|
||||
|
||||
// Password step.
|
||||
//void submitPassword(const QString &password);
|
||||
//void requestPasswordRecover();
|
||||
//rpl::producer<PasswordUpdate> passwordUpdate() const;
|
||||
//void reloadPasswordState();
|
||||
//void cancelUnconfirmedPassword();
|
||||
|
||||
// Processing step.
|
||||
void startExport(
|
||||
const Settings &settings,
|
||||
const Environment &environment);
|
||||
void cancelExportFast();
|
||||
|
||||
private:
|
||||
using Step = ProcessingState::Step;
|
||||
using DownloadProgress = ApiWrap::DownloadProgress;
|
||||
|
||||
void setState(State &&state);
|
||||
void ioError(const QString &path);
|
||||
bool ioCatchError(Output::Result result);
|
||||
void setFinishedState();
|
||||
|
||||
//void requestPasswordState();
|
||||
//void passwordStateDone(const MTPaccount_Password &password);
|
||||
|
||||
void fillExportSteps();
|
||||
void fillSubstepsInSteps(const ApiWrap::StartInfo &info);
|
||||
void exportNext();
|
||||
void initialize();
|
||||
void initialized(const ApiWrap::StartInfo &info);
|
||||
void collectLeftChannels();
|
||||
void collectDialogsList();
|
||||
void exportPersonalInfo();
|
||||
void exportUserpics();
|
||||
void exportContacts();
|
||||
void exportSessions();
|
||||
void exportOtherData();
|
||||
void exportDialogs();
|
||||
void exportNextDialog();
|
||||
void exportLeftChannels();
|
||||
void exportNextLeftChannel();
|
||||
|
||||
template <typename Callback = const decltype(kNullStateCallback) &>
|
||||
ProcessingState prepareState(
|
||||
Step step,
|
||||
Callback &&callback = kNullStateCallback) const;
|
||||
ProcessingState stateInitializing() const;
|
||||
ProcessingState stateLeftChannelsList(int processed) const;
|
||||
ProcessingState stateDialogsList(int processed) const;
|
||||
ProcessingState statePersonalInfo() const;
|
||||
ProcessingState stateUserpics(const DownloadProgress &progress) const;
|
||||
ProcessingState stateContacts() const;
|
||||
ProcessingState stateSessions() const;
|
||||
ProcessingState stateOtherData() const;
|
||||
ProcessingState stateLeftChannels(
|
||||
const DownloadProgress &progress) const;
|
||||
ProcessingState stateDialogs(const DownloadProgress &progress) const;
|
||||
void fillMessagesState(
|
||||
ProcessingState &result,
|
||||
const Data::DialogsInfo &info,
|
||||
int index,
|
||||
const DownloadProgress &progress,
|
||||
int addIndex,
|
||||
int addCount) const;
|
||||
|
||||
int substepsInStep(Step step) const;
|
||||
|
||||
bool normalizePath();
|
||||
|
||||
ApiWrap _api;
|
||||
Settings _settings;
|
||||
Environment _environment;
|
||||
|
||||
Data::DialogsInfo _leftChannelsInfo;
|
||||
int _leftChannelIndex = -1;
|
||||
|
||||
Data::DialogsInfo _dialogsInfo;
|
||||
int _dialogIndex = -1;
|
||||
|
||||
int _messagesWritten = 0;
|
||||
int _messagesCount = 0;
|
||||
|
||||
int _userpicsWritten = 0;
|
||||
int _userpicsCount = 0;
|
||||
|
||||
// rpl::variable<State> fails to compile in MSVC :(
|
||||
State _state;
|
||||
rpl::event_stream<State> _stateChanges;
|
||||
|
||||
Output::Stats _stats;
|
||||
|
||||
std::vector<int> _substepsInStep;
|
||||
int _substepsTotal = 0;
|
||||
mutable int _substepsPassed = 0;
|
||||
mutable Step _lastProcessingStep = Step::Initializing;
|
||||
|
||||
std::unique_ptr<Output::AbstractWriter> _writer;
|
||||
std::vector<Step> _steps;
|
||||
int _stepIndex = -1;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
Controller::Controller(crl::weak_on_queue<Controller> weak)
|
||||
: _api(weak.runner())
|
||||
, _state(PasswordCheckState{}) {
|
||||
_api.errors(
|
||||
) | rpl::start_with_next([=](RPCError &&error) {
|
||||
setState(ApiErrorState{ std::move(error) });
|
||||
}, _lifetime);
|
||||
|
||||
_api.ioErrors(
|
||||
) | rpl::start_with_next([=](const Output::Result &result) {
|
||||
ioCatchError(result);
|
||||
}, _lifetime);
|
||||
|
||||
//requestPasswordState();
|
||||
auto state = PasswordCheckState();
|
||||
state.checked = false;
|
||||
state.requesting = false;
|
||||
setState(std::move(state));
|
||||
}
|
||||
|
||||
rpl::producer<State> Controller::state() const {
|
||||
return rpl::single(
|
||||
_state
|
||||
) | rpl::then(
|
||||
_stateChanges.events()
|
||||
) | rpl::filter([](const State &state) {
|
||||
const auto password = base::get_if<PasswordCheckState>(&state);
|
||||
return !password || !password->requesting;
|
||||
});
|
||||
}
|
||||
|
||||
void Controller::setState(State &&state) {
|
||||
if (_state.is<CancelledState>()) {
|
||||
return;
|
||||
}
|
||||
_state = std::move(state);
|
||||
_stateChanges.fire_copy(_state);
|
||||
}
|
||||
|
||||
void Controller::ioError(const QString &path) {
|
||||
setState(OutputErrorState{ path });
|
||||
}
|
||||
|
||||
bool Controller::ioCatchError(Output::Result result) {
|
||||
if (!result) {
|
||||
ioError(result.path);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//void Controller::submitPassword(const QString &password) {
|
||||
//
|
||||
//}
|
||||
//
|
||||
//void Controller::requestPasswordRecover() {
|
||||
//
|
||||
//}
|
||||
//
|
||||
//rpl::producer<PasswordUpdate> Controller::passwordUpdate() const {
|
||||
// return rpl::never<PasswordUpdate>();
|
||||
//}
|
||||
//
|
||||
//void Controller::reloadPasswordState() {
|
||||
// //_mtp.request(base::take(_passwordRequestId)).cancel();
|
||||
// requestPasswordState();
|
||||
//}
|
||||
//
|
||||
//void Controller::requestPasswordState() {
|
||||
// if (_passwordRequestId) {
|
||||
// return;
|
||||
// }
|
||||
// //_passwordRequestId = _mtp.request(MTPaccount_GetPassword(
|
||||
// //)).done([=](const MTPaccount_Password &result) {
|
||||
// // _passwordRequestId = 0;
|
||||
// // passwordStateDone(result);
|
||||
// //}).fail([=](const RPCError &error) {
|
||||
// // apiError(error);
|
||||
// //}).send();
|
||||
//}
|
||||
//
|
||||
//void Controller::passwordStateDone(const MTPaccount_Password &result) {
|
||||
// auto state = PasswordCheckState();
|
||||
// state.checked = false;
|
||||
// state.requesting = false;
|
||||
// state.hasPassword;
|
||||
// state.hint;
|
||||
// state.unconfirmedPattern;
|
||||
// setState(std::move(state));
|
||||
//}
|
||||
//
|
||||
//void Controller::cancelUnconfirmedPassword() {
|
||||
//
|
||||
//}
|
||||
|
||||
void Controller::startExport(
|
||||
const Settings &settings,
|
||||
const Environment &environment) {
|
||||
if (!_settings.path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
_settings = base::duplicate(settings);
|
||||
_environment = environment;
|
||||
|
||||
_settings.path = Output::NormalizePath(_settings.path);
|
||||
_writer = Output::CreateWriter(_settings.format);
|
||||
fillExportSteps();
|
||||
exportNext();
|
||||
}
|
||||
|
||||
void Controller::fillExportSteps() {
|
||||
using Type = Settings::Type;
|
||||
_steps.push_back(Step::Initializing);
|
||||
if (_settings.types & Type::GroupsChannelsMask) {
|
||||
_steps.push_back(Step::LeftChannelsList);
|
||||
}
|
||||
if (_settings.types & Type::AnyChatsMask) {
|
||||
_steps.push_back(Step::DialogsList);
|
||||
}
|
||||
if (_settings.types & Type::PersonalInfo) {
|
||||
_steps.push_back(Step::PersonalInfo);
|
||||
}
|
||||
if (_settings.types & Type::Userpics) {
|
||||
_steps.push_back(Step::Userpics);
|
||||
}
|
||||
if (_settings.types & Type::Contacts) {
|
||||
_steps.push_back(Step::Contacts);
|
||||
}
|
||||
if (_settings.types & Type::Sessions) {
|
||||
_steps.push_back(Step::Sessions);
|
||||
}
|
||||
if (_settings.types & Type::OtherData) {
|
||||
_steps.push_back(Step::OtherData);
|
||||
}
|
||||
if (_settings.types & Type::AnyChatsMask) {
|
||||
_steps.push_back(Step::Dialogs);
|
||||
}
|
||||
if (_settings.types & Type::GroupsChannelsMask) {
|
||||
_steps.push_back(Step::LeftChannels);
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::fillSubstepsInSteps(const ApiWrap::StartInfo &info) {
|
||||
auto result = std::vector<int>();
|
||||
const auto push = [&](Step step, int count) {
|
||||
const auto index = static_cast<int>(step);
|
||||
if (index >= result.size()) {
|
||||
result.resize(index + 1, 0);
|
||||
}
|
||||
result[index] = count;
|
||||
};
|
||||
push(Step::Initializing, 1);
|
||||
if (_settings.types & Settings::Type::GroupsChannelsMask) {
|
||||
push(Step::LeftChannelsList, 1);
|
||||
}
|
||||
if (_settings.types & Settings::Type::AnyChatsMask) {
|
||||
push(Step::DialogsList, 1);
|
||||
}
|
||||
if (_settings.types & Settings::Type::PersonalInfo) {
|
||||
push(Step::PersonalInfo, 1);
|
||||
}
|
||||
if (_settings.types & Settings::Type::Userpics) {
|
||||
push(Step::Userpics, 1);
|
||||
}
|
||||
if (_settings.types & Settings::Type::Contacts) {
|
||||
push(Step::Contacts, 1);
|
||||
}
|
||||
if (_settings.types & Settings::Type::Sessions) {
|
||||
push(Step::Sessions, 1);
|
||||
}
|
||||
if (_settings.types & Settings::Type::OtherData) {
|
||||
push(Step::OtherData, 1);
|
||||
}
|
||||
if (_settings.types & Settings::Type::GroupsChannelsMask) {
|
||||
push(Step::LeftChannels, info.leftChannelsCount);
|
||||
}
|
||||
if (_settings.types & Settings::Type::AnyChatsMask) {
|
||||
push(Step::Dialogs, info.dialogsCount);
|
||||
}
|
||||
_substepsInStep = std::move(result);
|
||||
_substepsTotal = ranges::accumulate(_substepsInStep, 0);
|
||||
}
|
||||
|
||||
void Controller::cancelExportFast() {
|
||||
_api.cancelExportFast();
|
||||
setState(CancelledState());
|
||||
}
|
||||
|
||||
void Controller::exportNext() {
|
||||
if (++_stepIndex >= _steps.size()) {
|
||||
if (ioCatchError(_writer->finish())) {
|
||||
return;
|
||||
}
|
||||
_api.finishExport([=] {
|
||||
setFinishedState();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const auto step = _steps[_stepIndex];
|
||||
switch (step) {
|
||||
case Step::Initializing: return initialize();
|
||||
case Step::LeftChannelsList: return collectLeftChannels();
|
||||
case Step::DialogsList: return collectDialogsList();
|
||||
case Step::PersonalInfo: return exportPersonalInfo();
|
||||
case Step::Userpics: return exportUserpics();
|
||||
case Step::Contacts: return exportContacts();
|
||||
case Step::Sessions: return exportSessions();
|
||||
case Step::OtherData: return exportOtherData();
|
||||
case Step::LeftChannels: return exportLeftChannels();
|
||||
case Step::Dialogs: return exportDialogs();
|
||||
}
|
||||
Unexpected("Step in Controller::exportNext.");
|
||||
}
|
||||
|
||||
void Controller::initialize() {
|
||||
setState(stateInitializing());
|
||||
_api.startExport(_settings, &_stats, [=](ApiWrap::StartInfo info) {
|
||||
initialized(info);
|
||||
});
|
||||
}
|
||||
|
||||
void Controller::initialized(const ApiWrap::StartInfo &info) {
|
||||
if (ioCatchError(_writer->start(_settings, _environment, &_stats))) {
|
||||
return;
|
||||
}
|
||||
fillSubstepsInSteps(info);
|
||||
exportNext();
|
||||
}
|
||||
|
||||
void Controller::collectLeftChannels() {
|
||||
setState(stateLeftChannelsList(0));
|
||||
_api.requestLeftChannelsList([=](int count) {
|
||||
setState(stateLeftChannelsList(count));
|
||||
return true;
|
||||
}, [=](Data::DialogsInfo &&result) {
|
||||
_leftChannelsInfo = std::move(result);
|
||||
exportNext();
|
||||
});
|
||||
}
|
||||
|
||||
void Controller::collectDialogsList() {
|
||||
setState(stateDialogsList(0));
|
||||
_api.requestDialogsList([=](int count) {
|
||||
setState(stateDialogsList(count));
|
||||
return true;
|
||||
}, [=](Data::DialogsInfo &&result) {
|
||||
_dialogsInfo = std::move(result);
|
||||
exportNext();
|
||||
});
|
||||
}
|
||||
|
||||
void Controller::exportPersonalInfo() {
|
||||
setState(statePersonalInfo());
|
||||
_api.requestPersonalInfo([=](Data::PersonalInfo &&result) {
|
||||
if (ioCatchError(_writer->writePersonal(result))) {
|
||||
return;
|
||||
}
|
||||
exportNext();
|
||||
});
|
||||
}
|
||||
|
||||
void Controller::exportUserpics() {
|
||||
_api.requestUserpics([=](Data::UserpicsInfo &&start) {
|
||||
if (ioCatchError(_writer->writeUserpicsStart(start))) {
|
||||
return false;
|
||||
}
|
||||
_userpicsWritten = 0;
|
||||
_userpicsCount = start.count;
|
||||
return true;
|
||||
}, [=](DownloadProgress progress) {
|
||||
setState(stateUserpics(progress));
|
||||
return true;
|
||||
}, [=](Data::UserpicsSlice &&slice) {
|
||||
if (ioCatchError(_writer->writeUserpicsSlice(slice))) {
|
||||
return false;
|
||||
}
|
||||
_userpicsWritten += slice.list.size();
|
||||
setState(stateUserpics(DownloadProgress()));
|
||||
return true;
|
||||
}, [=] {
|
||||
if (ioCatchError(_writer->writeUserpicsEnd())) {
|
||||
return;
|
||||
}
|
||||
exportNext();
|
||||
});
|
||||
}
|
||||
|
||||
void Controller::exportContacts() {
|
||||
setState(stateContacts());
|
||||
_api.requestContacts([=](Data::ContactsList &&result) {
|
||||
if (ioCatchError(_writer->writeContactsList(result))) {
|
||||
return;
|
||||
}
|
||||
exportNext();
|
||||
});
|
||||
}
|
||||
|
||||
void Controller::exportSessions() {
|
||||
setState(stateSessions());
|
||||
_api.requestSessions([=](Data::SessionsList &&result) {
|
||||
if (ioCatchError(_writer->writeSessionsList(result))) {
|
||||
return;
|
||||
}
|
||||
exportNext();
|
||||
});
|
||||
}
|
||||
|
||||
void Controller::exportOtherData() {
|
||||
setState(stateOtherData());
|
||||
const auto relativePath = "lists/other_data.json";
|
||||
_api.requestOtherData(relativePath, [=](Data::File &&result) {
|
||||
if (ioCatchError(_writer->writeOtherData(result))) {
|
||||
return;
|
||||
}
|
||||
exportNext();
|
||||
});
|
||||
}
|
||||
|
||||
void Controller::exportDialogs() {
|
||||
if (ioCatchError(_writer->writeDialogsStart(_dialogsInfo))) {
|
||||
return;
|
||||
}
|
||||
|
||||
exportNextDialog();
|
||||
}
|
||||
|
||||
void Controller::exportNextDialog() {
|
||||
const auto index = ++_dialogIndex;
|
||||
if (index < _dialogsInfo.list.size()) {
|
||||
const auto &info = _dialogsInfo.list[index];
|
||||
_api.requestMessages(info, [=](const Data::DialogInfo &info) {
|
||||
if (ioCatchError(_writer->writeDialogStart(info))) {
|
||||
return false;
|
||||
}
|
||||
_messagesWritten = 0;
|
||||
_messagesCount = ranges::accumulate(
|
||||
info.messagesCountPerSplit,
|
||||
0);
|
||||
setState(stateDialogs(DownloadProgress()));
|
||||
return true;
|
||||
}, [=](DownloadProgress progress) {
|
||||
setState(stateDialogs(progress));
|
||||
return true;
|
||||
}, [=](Data::MessagesSlice &&result) {
|
||||
if (ioCatchError(_writer->writeDialogSlice(result))) {
|
||||
return false;
|
||||
}
|
||||
_messagesWritten += result.list.size();
|
||||
setState(stateDialogs(DownloadProgress()));
|
||||
return true;
|
||||
}, [=] {
|
||||
if (ioCatchError(_writer->writeDialogEnd())) {
|
||||
return;
|
||||
}
|
||||
exportNextDialog();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (ioCatchError(_writer->writeDialogsEnd())) {
|
||||
return;
|
||||
}
|
||||
exportNext();
|
||||
}
|
||||
|
||||
void Controller::exportLeftChannels() {
|
||||
if (ioCatchError(_writer->writeLeftChannelsStart(_leftChannelsInfo))) {
|
||||
return;
|
||||
}
|
||||
|
||||
exportNextLeftChannel();
|
||||
}
|
||||
|
||||
void Controller::exportNextLeftChannel() {
|
||||
const auto index = ++_leftChannelIndex;
|
||||
if (index < _leftChannelsInfo.list.size()) {
|
||||
const auto &info = _leftChannelsInfo.list[index];
|
||||
_api.requestMessages(info, [=](const Data::DialogInfo &info) {
|
||||
if (ioCatchError(_writer->writeLeftChannelStart(info))) {
|
||||
return false;
|
||||
}
|
||||
_messagesWritten = 0;
|
||||
_messagesCount = ranges::accumulate(
|
||||
info.messagesCountPerSplit,
|
||||
0);
|
||||
setState(stateLeftChannels(DownloadProgress()));
|
||||
return true;
|
||||
}, [=](DownloadProgress progress) {
|
||||
setState(stateLeftChannels(progress));
|
||||
return true;
|
||||
}, [=](Data::MessagesSlice &&result) {
|
||||
if (ioCatchError(_writer->writeLeftChannelSlice(result))) {
|
||||
return false;
|
||||
}
|
||||
_messagesWritten += result.list.size();
|
||||
setState(stateLeftChannels(DownloadProgress()));
|
||||
return true;
|
||||
}, [=] {
|
||||
if (ioCatchError(_writer->writeLeftChannelEnd())) {
|
||||
return;
|
||||
}
|
||||
exportNextLeftChannel();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (ioCatchError(_writer->writeLeftChannelsEnd())) {
|
||||
return;
|
||||
}
|
||||
exportNext();
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
ProcessingState Controller::prepareState(
|
||||
Step step,
|
||||
Callback &&callback) const {
|
||||
if (step != _lastProcessingStep) {
|
||||
_substepsPassed += substepsInStep(_lastProcessingStep);
|
||||
_lastProcessingStep = step;
|
||||
}
|
||||
|
||||
auto result = ProcessingState();
|
||||
callback(result);
|
||||
result.step = step;
|
||||
result.substepsPassed = _substepsPassed;
|
||||
result.substepsNow = substepsInStep(_lastProcessingStep);
|
||||
result.substepsTotal = _substepsTotal;
|
||||
return result;
|
||||
}
|
||||
|
||||
ProcessingState Controller::stateInitializing() const {
|
||||
return ProcessingState();
|
||||
}
|
||||
|
||||
ProcessingState Controller::stateLeftChannelsList(int processed) const {
|
||||
return prepareState(Step::LeftChannelsList, [&](
|
||||
ProcessingState &result) {
|
||||
result.entityIndex = processed;
|
||||
result.entityCount = std::max(
|
||||
processed,
|
||||
substepsInStep(Step::LeftChannels))
|
||||
+ substepsInStep(Step::Dialogs);
|
||||
});
|
||||
}
|
||||
|
||||
ProcessingState Controller::stateDialogsList(int processed) const {
|
||||
const auto step = Step::DialogsList;
|
||||
return prepareState(step, [&](ProcessingState &result) {
|
||||
result.entityIndex = substepsInStep(Step::LeftChannels) + processed;
|
||||
result.entityCount = substepsInStep(Step::LeftChannels) + std::max(
|
||||
processed,
|
||||
substepsInStep(Step::Dialogs));
|
||||
});
|
||||
}
|
||||
ProcessingState Controller::statePersonalInfo() const {
|
||||
return prepareState(Step::PersonalInfo);
|
||||
}
|
||||
|
||||
ProcessingState Controller::stateUserpics(
|
||||
const DownloadProgress &progress) const {
|
||||
return prepareState(Step::Userpics, [&](ProcessingState &result) {
|
||||
result.entityIndex = _userpicsWritten + progress.itemIndex;
|
||||
result.entityCount = std::max(_userpicsCount, result.entityIndex);
|
||||
result.bytesType = ProcessingState::FileType::Photo;
|
||||
if (!progress.path.isEmpty()) {
|
||||
const auto last = progress.path.lastIndexOf('/');
|
||||
result.bytesName = progress.path.mid(last + 1);
|
||||
}
|
||||
result.bytesLoaded = progress.ready;
|
||||
result.bytesCount = progress.total;
|
||||
});
|
||||
}
|
||||
|
||||
ProcessingState Controller::stateContacts() const {
|
||||
return prepareState(Step::Contacts);
|
||||
}
|
||||
|
||||
ProcessingState Controller::stateSessions() const {
|
||||
return prepareState(Step::Sessions);
|
||||
}
|
||||
|
||||
ProcessingState Controller::stateOtherData() const {
|
||||
return prepareState(Step::OtherData);
|
||||
}
|
||||
|
||||
ProcessingState Controller::stateLeftChannels(
|
||||
const DownloadProgress & progress) const {
|
||||
const auto step = Step::LeftChannels;
|
||||
return prepareState(step, [&](ProcessingState &result) {
|
||||
const auto addIndex = _dialogsInfo.list.size();
|
||||
const auto addCount = addIndex;
|
||||
fillMessagesState(
|
||||
result,
|
||||
_leftChannelsInfo,
|
||||
_leftChannelIndex,
|
||||
progress,
|
||||
addIndex,
|
||||
addCount);
|
||||
});
|
||||
}
|
||||
|
||||
ProcessingState Controller::stateDialogs(
|
||||
const DownloadProgress &progress) const {
|
||||
const auto step = Step::Dialogs;
|
||||
return prepareState(step, [&](ProcessingState &result) {
|
||||
const auto addIndex = 0;
|
||||
const auto addCount = _leftChannelsInfo.list.size();
|
||||
fillMessagesState(
|
||||
result,
|
||||
_dialogsInfo,
|
||||
_dialogIndex,
|
||||
progress,
|
||||
addIndex,
|
||||
addCount);
|
||||
});
|
||||
}
|
||||
|
||||
void Controller::fillMessagesState(
|
||||
ProcessingState &result,
|
||||
const Data::DialogsInfo &info,
|
||||
int index,
|
||||
const DownloadProgress &progress,
|
||||
int addIndex,
|
||||
int addCount) const {
|
||||
const auto &dialog = info.list[index];
|
||||
result.entityIndex = index + addIndex;
|
||||
result.entityCount = info.list.size() + addCount;
|
||||
result.entityName = dialog.name;
|
||||
result.itemIndex = _messagesWritten + progress.itemIndex;
|
||||
result.itemCount = std::max(_messagesCount, result.itemIndex);
|
||||
result.bytesType = ProcessingState::FileType::File; // TODO
|
||||
if (!progress.path.isEmpty()) {
|
||||
const auto last = progress.path.lastIndexOf('/');
|
||||
result.bytesName = progress.path.mid(last + 1);
|
||||
}
|
||||
result.bytesLoaded = progress.ready;
|
||||
result.bytesCount = progress.total;
|
||||
}
|
||||
|
||||
int Controller::substepsInStep(Step step) const {
|
||||
Expects(_substepsInStep.size() > static_cast<int>(step));
|
||||
|
||||
return _substepsInStep[static_cast<int>(step)];
|
||||
}
|
||||
|
||||
void Controller::setFinishedState() {
|
||||
setState(FinishedState{
|
||||
_writer->mainFilePath(),
|
||||
_stats.filesCount(),
|
||||
_stats.bytesCount() });
|
||||
}
|
||||
|
||||
ControllerWrap::ControllerWrap() {
|
||||
}
|
||||
|
||||
rpl::producer<State> ControllerWrap::state() const {
|
||||
return _wrapped.producer_on_main([=](const Controller &controller) {
|
||||
return controller.state();
|
||||
});
|
||||
}
|
||||
|
||||
//void ControllerWrap::submitPassword(const QString &password) {
|
||||
// _wrapped.with([=](Controller &controller) {
|
||||
// controller.submitPassword(password);
|
||||
// });
|
||||
//}
|
||||
//
|
||||
//void ControllerWrap::requestPasswordRecover() {
|
||||
// _wrapped.with([=](Controller &controller) {
|
||||
// controller.requestPasswordRecover();
|
||||
// });
|
||||
//}
|
||||
//
|
||||
//rpl::producer<PasswordUpdate> ControllerWrap::passwordUpdate() const {
|
||||
// return _wrapped.producer_on_main([=](const Controller &controller) {
|
||||
// return controller.passwordUpdate();
|
||||
// });
|
||||
//}
|
||||
//
|
||||
//void ControllerWrap::reloadPasswordState() {
|
||||
// _wrapped.with([=](Controller &controller) {
|
||||
// controller.reloadPasswordState();
|
||||
// });
|
||||
//}
|
||||
//
|
||||
//void ControllerWrap::cancelUnconfirmedPassword() {
|
||||
// _wrapped.with([=](Controller &controller) {
|
||||
// controller.cancelUnconfirmedPassword();
|
||||
// });
|
||||
//}
|
||||
|
||||
void ControllerWrap::startExport(
|
||||
const Settings &settings,
|
||||
const Environment &environment) {
|
||||
LOG(("Export Info: Started export to '%1'.").arg(settings.path));
|
||||
|
||||
_wrapped.with([=](Controller &controller) {
|
||||
controller.startExport(settings, environment);
|
||||
});
|
||||
}
|
||||
|
||||
void ControllerWrap::cancelExportFast() {
|
||||
LOG(("Export Info: Cancelled export."));
|
||||
|
||||
_wrapped.with([=](Controller &controller) {
|
||||
controller.cancelExportFast();
|
||||
});
|
||||
}
|
||||
|
||||
rpl::lifetime &ControllerWrap::lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
ControllerWrap::~ControllerWrap() {
|
||||
LOG(("Export Info: Controller destroyed."));
|
||||
}
|
||||
|
||||
} // namespace Export
|
||||
136
Telegram/SourceFiles/export/export_controller.h
Normal file
136
Telegram/SourceFiles/export/export_controller.h
Normal file
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <crl/crl_object_on_queue.h>
|
||||
#include "base/variant.h"
|
||||
#include "mtproto/rpc_sender.h"
|
||||
|
||||
namespace Export {
|
||||
|
||||
class Controller;
|
||||
struct Settings;
|
||||
struct Environment;
|
||||
|
||||
struct PasswordCheckState {
|
||||
QString hint;
|
||||
QString unconfirmedPattern;
|
||||
bool requesting = true;
|
||||
bool hasPassword = false;
|
||||
bool checked = false;
|
||||
};
|
||||
|
||||
struct ProcessingState {
|
||||
enum class Step {
|
||||
Initializing,
|
||||
LeftChannelsList,
|
||||
DialogsList,
|
||||
PersonalInfo,
|
||||
Userpics,
|
||||
Contacts,
|
||||
Sessions,
|
||||
OtherData,
|
||||
LeftChannels,
|
||||
Dialogs,
|
||||
};
|
||||
enum class FileType {
|
||||
None,
|
||||
Photo,
|
||||
Video,
|
||||
VoiceMessage,
|
||||
VideoMessage,
|
||||
Sticker,
|
||||
GIF,
|
||||
File,
|
||||
};
|
||||
|
||||
Step step = Step::Initializing;
|
||||
|
||||
int substepsPassed = 0;
|
||||
int substepsNow = 0;
|
||||
int substepsTotal = 0;
|
||||
|
||||
QString entityName;
|
||||
int entityIndex = 0;
|
||||
int entityCount = 0;
|
||||
|
||||
int itemIndex = 0;
|
||||
int itemCount = 0;
|
||||
|
||||
FileType bytesType = FileType::None;
|
||||
QString bytesName;
|
||||
int bytesLoaded = 0;
|
||||
int bytesCount = 0;
|
||||
};
|
||||
|
||||
struct ApiErrorState {
|
||||
RPCError data;
|
||||
};
|
||||
|
||||
struct OutputErrorState {
|
||||
QString path;
|
||||
};
|
||||
|
||||
struct CancelledState {
|
||||
};
|
||||
|
||||
struct FinishedState {
|
||||
QString path;
|
||||
int filesCount = 0;
|
||||
int64 bytesCount = 0;
|
||||
};
|
||||
|
||||
using State = base::optional_variant<
|
||||
PasswordCheckState,
|
||||
ProcessingState,
|
||||
ApiErrorState,
|
||||
OutputErrorState,
|
||||
CancelledState,
|
||||
FinishedState>;
|
||||
|
||||
//struct PasswordUpdate {
|
||||
// enum class Type {
|
||||
// CheckSucceed,
|
||||
// WrongPassword,
|
||||
// FloodLimit,
|
||||
// RecoverUnavailable,
|
||||
// };
|
||||
// Type type = Type::WrongPassword;
|
||||
//
|
||||
//};
|
||||
|
||||
class ControllerWrap {
|
||||
public:
|
||||
ControllerWrap();
|
||||
|
||||
rpl::producer<State> state() const;
|
||||
|
||||
// Password step.
|
||||
//void submitPassword(const QString &password);
|
||||
//void requestPasswordRecover();
|
||||
//rpl::producer<PasswordUpdate> passwordUpdate() const;
|
||||
//void reloadPasswordState();
|
||||
//void cancelUnconfirmedPassword();
|
||||
|
||||
// Processing step.
|
||||
void startExport(
|
||||
const Settings &settings,
|
||||
const Environment &environment);
|
||||
void cancelExportFast();
|
||||
|
||||
rpl::lifetime &lifetime();
|
||||
|
||||
~ControllerWrap();
|
||||
|
||||
private:
|
||||
crl::object_on_queue<Controller> _wrapped;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Export
|
||||
9
Telegram/SourceFiles/export/export_pch.cpp
Normal file
9
Telegram/SourceFiles/export/export_pch.cpp
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "export/export_pch.h"
|
||||
|
||||
37
Telegram/SourceFiles/export/export_pch.h
Normal file
37
Telegram/SourceFiles/export/export_pch.h
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QFileInfo>
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QTextStream>
|
||||
#include <QtCore/QDateTime>
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QByteArray>
|
||||
#include <QtCore/QReadWriteLock>
|
||||
#include <QtCore/QRegularExpression>
|
||||
|
||||
#include <crl/crl.h>
|
||||
#include <rpl/rpl.h>
|
||||
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <deque>
|
||||
#include <atomic>
|
||||
|
||||
#include <range/v3/all.hpp>
|
||||
#ifdef Q_OS_WIN
|
||||
#include "platform/win/windows_range_v3_helpers.h"
|
||||
#endif // Q_OS_WIN
|
||||
|
||||
#include "base/flat_map.h"
|
||||
#include "base/flat_set.h"
|
||||
|
||||
#include "scheme.h"
|
||||
#include "logs.h"
|
||||
48
Telegram/SourceFiles/export/export_settings.cpp
Normal file
48
Telegram/SourceFiles/export/export_settings.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "export/export_settings.h"
|
||||
|
||||
#include "export/output/export_output_abstract.h"
|
||||
|
||||
namespace Export {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxFileSize = 1500 * 1024 * 1024;
|
||||
|
||||
} // namespace
|
||||
|
||||
bool MediaSettings::validate() const {
|
||||
if ((types | Type::AllMask) != Type::AllMask) {
|
||||
return false;
|
||||
} else if (sizeLimit < 0 || sizeLimit > kMaxFileSize) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Settings::validate() const {
|
||||
using Format = Output::Format;
|
||||
const auto MustBeFull = Type::PersonalChats | Type::BotChats;
|
||||
const auto MustNotBeFull = Type::PublicGroups | Type::PublicChannels;
|
||||
if ((types | Type::AllMask) != Type::AllMask) {
|
||||
return false;
|
||||
} else if ((fullChats | Type::AllMask) != Type::AllMask) {
|
||||
return false;
|
||||
} else if ((fullChats & MustBeFull) != MustBeFull) {
|
||||
return false;
|
||||
} else if ((fullChats & MustNotBeFull) != 0) {
|
||||
return false;
|
||||
} else if (format != Format::Html && format != Format::Json) {
|
||||
return false;
|
||||
} else if (!media.validate()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
} // namespace Export
|
||||
107
Telegram/SourceFiles/export/export_settings.h
Normal file
107
Telegram/SourceFiles/export/export_settings.h
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/flags.h"
|
||||
#include "base/flat_map.h"
|
||||
|
||||
namespace Export {
|
||||
namespace Output {
|
||||
enum class Format;
|
||||
} // namespace Output
|
||||
|
||||
struct MediaSettings {
|
||||
bool validate() const;
|
||||
|
||||
enum class Type {
|
||||
Photo = 0x01,
|
||||
Video = 0x02,
|
||||
VoiceMessage = 0x04,
|
||||
VideoMessage = 0x08,
|
||||
Sticker = 0x10,
|
||||
GIF = 0x20,
|
||||
File = 0x40,
|
||||
|
||||
MediaMask = Photo | Video | VoiceMessage | VideoMessage,
|
||||
AllMask = MediaMask | Sticker | GIF | File,
|
||||
};
|
||||
using Types = base::flags<Type>;
|
||||
friend inline constexpr auto is_flag_type(Type) { return true; };
|
||||
|
||||
Types types = DefaultTypes();
|
||||
int sizeLimit = 8 * 1024 * 1024;
|
||||
|
||||
static inline Types DefaultTypes() {
|
||||
return Type::Photo;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
struct Settings {
|
||||
bool validate() const;
|
||||
|
||||
enum class Type {
|
||||
PersonalInfo = 0x001,
|
||||
Userpics = 0x002,
|
||||
Contacts = 0x004,
|
||||
Sessions = 0x008,
|
||||
OtherData = 0x010,
|
||||
PersonalChats = 0x020,
|
||||
BotChats = 0x040,
|
||||
PrivateGroups = 0x080,
|
||||
PublicGroups = 0x100,
|
||||
PrivateChannels = 0x200,
|
||||
PublicChannels = 0x400,
|
||||
|
||||
GroupsMask = PrivateGroups | PublicGroups,
|
||||
ChannelsMask = PrivateChannels | PublicChannels,
|
||||
GroupsChannelsMask = GroupsMask | ChannelsMask,
|
||||
NonChannelChatsMask = PersonalChats | BotChats | PrivateGroups,
|
||||
AnyChatsMask = PersonalChats | BotChats | GroupsChannelsMask,
|
||||
NonChatsMask = PersonalInfo | Userpics | Contacts | Sessions,
|
||||
AllMask = NonChatsMask | OtherData | AnyChatsMask,
|
||||
};
|
||||
using Types = base::flags<Type>;
|
||||
friend inline constexpr auto is_flag_type(Type) { return true; };
|
||||
|
||||
QString path;
|
||||
Output::Format format = Output::Format();
|
||||
|
||||
Types types = DefaultTypes();
|
||||
Types fullChats = DefaultFullChats();
|
||||
MediaSettings media;
|
||||
|
||||
TimeId availableAt = 0;
|
||||
|
||||
static inline Types DefaultTypes() {
|
||||
return Type::PersonalInfo
|
||||
| Type::Userpics
|
||||
| Type::Contacts
|
||||
| Type::PersonalChats
|
||||
| Type::PrivateGroups;
|
||||
}
|
||||
|
||||
static inline Types DefaultFullChats() {
|
||||
return Type::PersonalChats
|
||||
| Type::BotChats;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
struct Environment {
|
||||
QString internalLinksDomain;
|
||||
QByteArray aboutTelegram;
|
||||
QByteArray aboutContacts;
|
||||
QByteArray aboutFrequent;
|
||||
QByteArray aboutSessions;
|
||||
QByteArray aboutWebSessions;
|
||||
QByteArray aboutChats;
|
||||
QByteArray aboutLeftChats;
|
||||
};
|
||||
|
||||
} // namespace Export
|
||||
496
Telegram/SourceFiles/export/output/export_output_abstract.cpp
Normal file
496
Telegram/SourceFiles/export/output/export_output_abstract.cpp
Normal file
@@ -0,0 +1,496 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "export/output/export_output_abstract.h"
|
||||
|
||||
#include "export/output/export_output_text.h"
|
||||
#include "export/output/export_output_html.h"
|
||||
#include "export/output/export_output_json.h"
|
||||
#include "export/output/export_output_stats.h"
|
||||
#include "export/output/export_output_result.h"
|
||||
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QDate>
|
||||
|
||||
namespace Export {
|
||||
namespace Output {
|
||||
|
||||
QString NormalizePath(const QString &source) {
|
||||
QDir folder(source);
|
||||
const auto path = folder.absolutePath();
|
||||
auto result = path.endsWith('/') ? path : (path + '/');
|
||||
if (!folder.exists()) {
|
||||
return result;
|
||||
}
|
||||
const auto mode = QDir::AllEntries | QDir::NoDotAndDotDot;
|
||||
const auto list = folder.entryInfoList(mode);
|
||||
if (list.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
const auto date = QDate::currentDate();
|
||||
const auto base = QString("DataExport_%1_%2_%3"
|
||||
).arg(date.day(), 2, 10, QChar('0')
|
||||
).arg(date.month(), 2, 10, QChar('0')
|
||||
).arg(date.year());
|
||||
const auto add = [&](int i) {
|
||||
return base + (i ? " (" + QString::number(i) + ')' : QString());
|
||||
};
|
||||
auto index = 0;
|
||||
while (QDir(result + add(index)).exists()) {
|
||||
++index;
|
||||
}
|
||||
result += add(index) + '/';
|
||||
return result;
|
||||
}
|
||||
|
||||
std::unique_ptr<AbstractWriter> CreateWriter(Format format) {
|
||||
switch (format) {
|
||||
case Format::Html: return std::make_unique<HtmlWriter>();
|
||||
case Format::Text: return std::make_unique<TextWriter>();
|
||||
case Format::Json: return std::make_unique<JsonWriter>();
|
||||
}
|
||||
Unexpected("Format in Export::Output::CreateWriter.");
|
||||
}
|
||||
|
||||
Stats AbstractWriter::produceTestExample(const QString &path) {
|
||||
auto result = Stats();
|
||||
const auto folder = QDir(path).absolutePath();
|
||||
auto environment = Environment();
|
||||
environment.internalLinksDomain = "https://t.me/";
|
||||
environment.aboutTelegram = "About Telegram";
|
||||
environment.aboutContacts = "About contacts";
|
||||
environment.aboutFrequent = "About frequent";
|
||||
environment.aboutSessions = "About sessions";
|
||||
environment.aboutWebSessions = "About web sessions";
|
||||
environment.aboutChats = "About chats";
|
||||
environment.aboutLeftChats = "About left chats";
|
||||
|
||||
auto settings = Settings();
|
||||
settings.format = format();
|
||||
settings.path = (folder.endsWith('/') ? folder : (folder + '/'))
|
||||
+ "ExportExample/";
|
||||
settings.types = Settings::Type::AllMask;
|
||||
settings.fullChats = Settings::Type::AllMask
|
||||
& ~(Settings::Type::PublicChannels | Settings::Type::PublicGroups);
|
||||
settings.media.types = MediaSettings::Type::AllMask;
|
||||
settings.media.sizeLimit = 1024 * 1024;
|
||||
|
||||
const auto check = [](Result result) {
|
||||
Assert(result.isSuccess());
|
||||
};
|
||||
|
||||
check(start(settings, environment, &result));
|
||||
|
||||
const auto counter = [&] {
|
||||
static auto GlobalCounter = 0;
|
||||
return ++GlobalCounter;
|
||||
};
|
||||
const auto date = [&] {
|
||||
return time(nullptr) - 86400 + counter();
|
||||
};
|
||||
const auto prevdate = [&] {
|
||||
return date() - 86400;
|
||||
};
|
||||
|
||||
auto personal = Data::PersonalInfo();
|
||||
personal.bio = "Nice text about me.";
|
||||
personal.user.info.firstName = "John";
|
||||
personal.user.info.lastName = "Preston";
|
||||
personal.user.info.phoneNumber = "447400000000";
|
||||
personal.user.info.date = date();
|
||||
personal.user.username = "preston";
|
||||
personal.user.info.userId = counter();
|
||||
personal.user.isBot = false;
|
||||
personal.user.isSelf = true;
|
||||
check(writePersonal(personal));
|
||||
|
||||
const auto generatePhoto = [&] {
|
||||
static auto index = 0;
|
||||
auto result = Data::Photo();
|
||||
result.date = date();
|
||||
result.id = counter();
|
||||
result.image.width = 512;
|
||||
result.image.height = 512;
|
||||
result.image.file.relativePath = "files/photo_"
|
||||
+ QString::number(++index)
|
||||
+ ".jpg";
|
||||
return result;
|
||||
};
|
||||
|
||||
auto userpics = Data::UserpicsInfo();
|
||||
userpics.count = 3;
|
||||
auto userpicsSlice1 = Data::UserpicsSlice();
|
||||
userpicsSlice1.list.push_back(generatePhoto());
|
||||
userpicsSlice1.list.push_back(generatePhoto());
|
||||
auto userpicsSlice2 = Data::UserpicsSlice();
|
||||
userpicsSlice2.list.push_back(generatePhoto());
|
||||
check(writeUserpicsStart(userpics));
|
||||
check(writeUserpicsSlice(userpicsSlice1));
|
||||
check(writeUserpicsSlice(userpicsSlice2));
|
||||
check(writeUserpicsEnd());
|
||||
|
||||
auto contacts = Data::ContactsList();
|
||||
auto topUser = Data::TopPeer();
|
||||
auto user = personal.user;
|
||||
auto peerUser = Data::Peer{ user };
|
||||
topUser.peer = peerUser;
|
||||
topUser.rating = 0.5;
|
||||
auto topChat = Data::TopPeer();
|
||||
auto chat = Data::Chat();
|
||||
chat.id = counter();
|
||||
chat.title = "Group chat";
|
||||
auto peerChat = Data::Peer{ chat };
|
||||
topChat.peer = peerChat;
|
||||
topChat.rating = 0.25;
|
||||
auto topBot = Data::TopPeer();
|
||||
auto bot = Data::User();
|
||||
bot.info.date = date();
|
||||
bot.isBot = true;
|
||||
bot.info.firstName = "Bot";
|
||||
bot.info.lastName = "Father";
|
||||
bot.info.userId = counter();
|
||||
bot.username = "botfather";
|
||||
auto peerBot = Data::Peer{ bot };
|
||||
topBot.peer = peerBot;
|
||||
topBot.rating = 0.125;
|
||||
|
||||
auto peers = std::map<Data::PeerId, Data::Peer>();
|
||||
peers.emplace(peerUser.id(), peerUser);
|
||||
peers.emplace(peerBot.id(), peerBot);
|
||||
peers.emplace(peerChat.id(), peerChat);
|
||||
|
||||
contacts.correspondents.push_back(topUser);
|
||||
contacts.correspondents.push_back(topChat);
|
||||
contacts.inlineBots.push_back(topBot);
|
||||
contacts.inlineBots.push_back(topBot);
|
||||
contacts.phoneCalls.push_back(topUser);
|
||||
contacts.list.push_back(user.info);
|
||||
contacts.list.push_back(bot.info);
|
||||
|
||||
check(writeContactsList(contacts));
|
||||
|
||||
auto sessions = Data::SessionsList();
|
||||
auto session = Data::Session();
|
||||
session.applicationName = "Telegram Desktop";
|
||||
session.applicationVersion = "1.3.8";
|
||||
session.country = "GB";
|
||||
session.created = date();
|
||||
session.deviceModel = "PC";
|
||||
session.ip = "127.0.0.1";
|
||||
session.lastActive = date();
|
||||
session.platform = "Windows";
|
||||
session.region = "London";
|
||||
session.systemVersion = "10";
|
||||
sessions.list.push_back(session);
|
||||
sessions.list.push_back(session);
|
||||
auto webSession = Data::WebSession();
|
||||
webSession.botUsername = "botfather";
|
||||
webSession.browser = "Google Chrome";
|
||||
webSession.created = date();
|
||||
webSession.domain = "telegram.org";
|
||||
webSession.ip = "127.0.0.1";
|
||||
webSession.lastActive = date();
|
||||
webSession.platform = "Windows";
|
||||
webSession.region = "London, GB";
|
||||
sessions.webList.push_back(webSession);
|
||||
sessions.webList.push_back(webSession);
|
||||
check(writeSessionsList(sessions));
|
||||
|
||||
auto sampleMessage = [&] {
|
||||
auto message = Data::Message();
|
||||
message.id = counter();
|
||||
message.date = prevdate();
|
||||
message.edited = date();
|
||||
message.forwardedFromId = user.info.userId;
|
||||
message.fromId = user.info.userId;
|
||||
message.replyToMsgId = counter();
|
||||
message.viaBotId = bot.info.userId;
|
||||
message.text.push_back(Data::TextPart{
|
||||
Data::TextPart::Type::Text,
|
||||
("Text message " + QString::number(counter())).toUtf8()
|
||||
});
|
||||
return message;
|
||||
};
|
||||
auto sliceBot1 = Data::MessagesSlice();
|
||||
sliceBot1.peers = peers;
|
||||
sliceBot1.list.push_back(sampleMessage());
|
||||
sliceBot1.list.push_back([&] {
|
||||
auto message = sampleMessage();
|
||||
message.media.content = generatePhoto();
|
||||
message.media.ttl = counter();
|
||||
return message;
|
||||
}());
|
||||
sliceBot1.list.push_back([&] {
|
||||
auto message = sampleMessage();
|
||||
auto document = Data::Document();
|
||||
document.date = prevdate();
|
||||
document.duration = counter();
|
||||
auto photo = generatePhoto();
|
||||
document.file = photo.image.file;
|
||||
document.width = photo.image.width;
|
||||
document.height = photo.image.height;
|
||||
document.id = counter();
|
||||
message.media.content = document;
|
||||
return message;
|
||||
}());
|
||||
sliceBot1.list.push_back([&] {
|
||||
auto message = sampleMessage();
|
||||
message.media.content = user.info;
|
||||
return message;
|
||||
}());
|
||||
auto sliceBot2 = Data::MessagesSlice();
|
||||
sliceBot2.peers = peers;
|
||||
sliceBot2.list.push_back([&] {
|
||||
auto message = sampleMessage();
|
||||
auto point = Data::GeoPoint();
|
||||
point.latitude = 1.5;
|
||||
point.longitude = 2.8;
|
||||
point.valid = true;
|
||||
message.media.content = point;
|
||||
message.media.ttl = counter();
|
||||
return message;
|
||||
}());
|
||||
sliceBot2.list.push_back([&] {
|
||||
auto message = sampleMessage();
|
||||
message.replyToMsgId = sliceBot1.list.back().id;
|
||||
auto venue = Data::Venue();
|
||||
venue.point.latitude = 1.5;
|
||||
venue.point.longitude = 2.8;
|
||||
venue.point.valid = true;
|
||||
venue.address = "Test address";
|
||||
venue.title = "Test venue";
|
||||
message.media.content = venue;
|
||||
return message;
|
||||
}());
|
||||
sliceBot2.list.push_back([&] {
|
||||
auto message = sampleMessage();
|
||||
auto game = Data::Game();
|
||||
game.botId = bot.info.userId;
|
||||
game.title = "Test game";
|
||||
game.description = "Test game description";
|
||||
game.id = counter();
|
||||
game.shortName = "testgame";
|
||||
message.media.content = game;
|
||||
return message;
|
||||
}());
|
||||
sliceBot2.list.push_back([&] {
|
||||
auto message = sampleMessage();
|
||||
auto invoice = Data::Invoice();
|
||||
invoice.amount = counter();
|
||||
invoice.currency = "GBP";
|
||||
invoice.title = "Huge invoice.";
|
||||
invoice.description = "So money.";
|
||||
invoice.receiptMsgId = sliceBot2.list.front().id;
|
||||
message.media.content = invoice;
|
||||
return message;
|
||||
}());
|
||||
auto serviceMessage = [&] {
|
||||
auto message = Data::Message();
|
||||
message.id = counter();
|
||||
message.date = prevdate();
|
||||
message.fromId = user.info.userId;
|
||||
return message;
|
||||
};
|
||||
auto sliceChat1 = Data::MessagesSlice();
|
||||
sliceChat1.peers = peers;
|
||||
sliceChat1.list.push_back([&] {
|
||||
auto message = serviceMessage();
|
||||
auto action = Data::ActionChatCreate();
|
||||
action.title = "Test chat";
|
||||
action.userIds.push_back(user.info.userId);
|
||||
action.userIds.push_back(bot.info.userId);
|
||||
message.action.content = action;
|
||||
return message;
|
||||
}());
|
||||
sliceChat1.list.push_back([&] {
|
||||
auto message = serviceMessage();
|
||||
auto action = Data::ActionChatEditTitle();
|
||||
action.title = "New title";
|
||||
message.action.content = action;
|
||||
return message;
|
||||
}());
|
||||
sliceChat1.list.push_back([&] {
|
||||
auto message = serviceMessage();
|
||||
auto action = Data::ActionChatEditPhoto();
|
||||
action.photo = generatePhoto();
|
||||
message.action.content = action;
|
||||
return message;
|
||||
}());
|
||||
sliceChat1.list.push_back([&] {
|
||||
auto message = serviceMessage();
|
||||
auto action = Data::ActionChatDeletePhoto();
|
||||
message.action.content = action;
|
||||
return message;
|
||||
}());
|
||||
sliceChat1.list.push_back([&] {
|
||||
auto message = serviceMessage();
|
||||
auto action = Data::ActionChatAddUser();
|
||||
action.userIds.push_back(user.info.userId);
|
||||
action.userIds.push_back(bot.info.userId);
|
||||
message.action.content = action;
|
||||
return message;
|
||||
}());
|
||||
sliceChat1.list.push_back([&] {
|
||||
auto message = serviceMessage();
|
||||
auto action = Data::ActionChatDeleteUser();
|
||||
action.userId = bot.info.userId;
|
||||
message.action.content = action;
|
||||
return message;
|
||||
}());
|
||||
sliceChat1.list.push_back([&] {
|
||||
auto message = serviceMessage();
|
||||
auto action = Data::ActionChatJoinedByLink();
|
||||
action.inviterId = bot.info.userId;
|
||||
message.action.content = action;
|
||||
return message;
|
||||
}());
|
||||
sliceChat1.list.push_back([&] {
|
||||
auto message = serviceMessage();
|
||||
auto action = Data::ActionChannelCreate();
|
||||
action.title = "Channel name";
|
||||
message.action.content = action;
|
||||
return message;
|
||||
}());
|
||||
sliceChat1.list.push_back([&] {
|
||||
auto message = serviceMessage();
|
||||
auto action = Data::ActionChatMigrateTo();
|
||||
action.channelId = chat.id;
|
||||
message.action.content = action;
|
||||
return message;
|
||||
}());
|
||||
sliceChat1.list.push_back([&] {
|
||||
auto message = serviceMessage();
|
||||
auto action = Data::ActionChannelMigrateFrom();
|
||||
action.chatId = chat.id;
|
||||
action.title = "Supergroup now";
|
||||
message.action.content = action;
|
||||
return message;
|
||||
}());
|
||||
auto sliceChat2 = Data::MessagesSlice();
|
||||
sliceChat2.peers = peers;
|
||||
sliceChat2.list.push_back([&] {
|
||||
auto message = serviceMessage();
|
||||
auto action = Data::ActionPinMessage();
|
||||
message.replyToMsgId = sliceChat1.list.back().id;
|
||||
message.action.content = action;
|
||||
return message;
|
||||
}());
|
||||
sliceChat2.list.push_back([&] {
|
||||
auto message = serviceMessage();
|
||||
auto action = Data::ActionHistoryClear();
|
||||
message.action.content = action;
|
||||
return message;
|
||||
}());
|
||||
sliceChat2.list.push_back([&] {
|
||||
auto message = serviceMessage();
|
||||
auto action = Data::ActionGameScore();
|
||||
action.score = counter();
|
||||
action.gameId = counter();
|
||||
message.replyToMsgId = sliceChat2.list.back().id;
|
||||
message.action.content = action;
|
||||
return message;
|
||||
}());
|
||||
sliceChat2.list.push_back([&] {
|
||||
auto message = serviceMessage();
|
||||
auto action = Data::ActionPaymentSent();
|
||||
action.amount = counter();
|
||||
action.currency = "GBP";
|
||||
message.replyToMsgId = sliceChat2.list.front().id;
|
||||
message.action.content = action;
|
||||
return message;
|
||||
}());
|
||||
sliceChat2.list.push_back([&] {
|
||||
auto message = serviceMessage();
|
||||
auto action = Data::ActionPhoneCall();
|
||||
action.duration = counter();
|
||||
action.discardReason = Data::ActionPhoneCall::DiscardReason::Busy;
|
||||
message.action.content = action;
|
||||
return message;
|
||||
}());
|
||||
sliceChat2.list.push_back([&] {
|
||||
auto message = serviceMessage();
|
||||
auto action = Data::ActionScreenshotTaken();
|
||||
message.action.content = action;
|
||||
return message;
|
||||
}());
|
||||
sliceChat2.list.push_back([&] {
|
||||
auto message = serviceMessage();
|
||||
auto action = Data::ActionCustomAction();
|
||||
action.message = "Custom chat action.";
|
||||
message.action.content = action;
|
||||
return message;
|
||||
}());
|
||||
sliceChat2.list.push_back([&] {
|
||||
auto message = serviceMessage();
|
||||
auto action = Data::ActionBotAllowed();
|
||||
action.domain = "telegram.org";
|
||||
message.action.content = action;
|
||||
return message;
|
||||
}());
|
||||
sliceChat2.list.push_back([&] {
|
||||
auto message = serviceMessage();
|
||||
auto action = Data::ActionSecureValuesSent();
|
||||
using Type = Data::ActionSecureValuesSent::Type;
|
||||
action.types.push_back(Type::BankStatement);
|
||||
action.types.push_back(Type::Phone);
|
||||
message.action.content = action;
|
||||
return message;
|
||||
}());
|
||||
auto dialogs = Data::DialogsInfo();
|
||||
auto dialogBot = Data::DialogInfo();
|
||||
dialogBot.messagesCountPerSplit.push_back(sliceBot1.list.size());
|
||||
dialogBot.messagesCountPerSplit.push_back(sliceBot2.list.size());
|
||||
dialogBot.type = Data::DialogInfo::Type::Bot;
|
||||
dialogBot.name = peerBot.name();
|
||||
dialogBot.onlyMyMessages = false;
|
||||
dialogBot.peerId = peerBot.id();
|
||||
dialogBot.relativePath = "chats/chat_"
|
||||
+ QString::number(counter())
|
||||
+ '/';
|
||||
dialogBot.splits.push_back(0);
|
||||
dialogBot.splits.push_back(1);
|
||||
dialogBot.topMessageDate = sliceBot2.list.back().date;
|
||||
dialogBot.topMessageId = sliceBot2.list.back().id;
|
||||
auto dialogChat = Data::DialogInfo();
|
||||
dialogChat.messagesCountPerSplit.push_back(sliceChat1.list.size());
|
||||
dialogChat.messagesCountPerSplit.push_back(sliceChat2.list.size());
|
||||
dialogChat.type = Data::DialogInfo::Type::PrivateGroup;
|
||||
dialogChat.name = peerChat.name();
|
||||
dialogChat.onlyMyMessages = true;
|
||||
dialogChat.peerId = peerChat.id();
|
||||
dialogChat.relativePath = "chats/chat_"
|
||||
+ QString::number(counter())
|
||||
+ '/';
|
||||
dialogChat.splits.push_back(0);
|
||||
dialogChat.splits.push_back(1);
|
||||
dialogChat.topMessageDate = sliceChat2.list.back().date;
|
||||
dialogChat.topMessageId = sliceChat2.list.back().id;
|
||||
dialogs.list.push_back(dialogBot);
|
||||
dialogs.list.push_back(dialogChat);
|
||||
|
||||
check(writeDialogsStart(dialogs));
|
||||
check(writeDialogStart(dialogBot));
|
||||
check(writeDialogSlice(sliceBot1));
|
||||
check(writeDialogSlice(sliceBot2));
|
||||
check(writeDialogEnd());
|
||||
check(writeDialogStart(dialogChat));
|
||||
check(writeDialogSlice(sliceChat1));
|
||||
check(writeDialogSlice(sliceChat2));
|
||||
check(writeDialogEnd());
|
||||
check(writeDialogsEnd());
|
||||
|
||||
check(writeLeftChannelsStart(Data::DialogsInfo()));
|
||||
check(writeLeftChannelsEnd());
|
||||
|
||||
check(finish());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
} // namespace Output
|
||||
} // namespace Export
|
||||
100
Telegram/SourceFiles/export/output/export_output_abstract.h
Normal file
100
Telegram/SourceFiles/export/output/export_output_abstract.h
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <QtCore/QString>
|
||||
|
||||
namespace Export {
|
||||
namespace Data {
|
||||
struct PersonalInfo;
|
||||
struct UserpicsInfo;
|
||||
struct UserpicsSlice;
|
||||
struct ContactsList;
|
||||
struct SessionsList;
|
||||
struct DialogsInfo;
|
||||
struct DialogInfo;
|
||||
struct MessagesSlice;
|
||||
struct File;
|
||||
} // namespace Data
|
||||
|
||||
struct Settings;
|
||||
struct Environment;
|
||||
|
||||
namespace Output {
|
||||
|
||||
QString NormalizePath(const QString &source);
|
||||
|
||||
struct Result;
|
||||
class Stats;
|
||||
|
||||
enum class Format {
|
||||
Html,
|
||||
Json,
|
||||
Text,
|
||||
Yaml,
|
||||
};
|
||||
|
||||
class AbstractWriter {
|
||||
public:
|
||||
[[nodiscard]] virtual Format format() = 0;
|
||||
|
||||
[[nodiscard]] virtual Result start(
|
||||
const Settings &settings,
|
||||
const Environment &environment,
|
||||
Stats *stats) = 0;
|
||||
|
||||
[[nodiscard]] virtual Result writePersonal(
|
||||
const Data::PersonalInfo &data) = 0;
|
||||
|
||||
[[nodiscard]] virtual Result writeUserpicsStart(
|
||||
const Data::UserpicsInfo &data) = 0;
|
||||
[[nodiscard]] virtual Result writeUserpicsSlice(
|
||||
const Data::UserpicsSlice &data) = 0;
|
||||
[[nodiscard]] virtual Result writeUserpicsEnd() = 0;
|
||||
|
||||
[[nodiscard]] virtual Result writeContactsList(
|
||||
const Data::ContactsList &data) = 0;
|
||||
|
||||
[[nodiscard]] virtual Result writeSessionsList(
|
||||
const Data::SessionsList &data) = 0;
|
||||
|
||||
[[nodiscard]] virtual Result writeOtherData(
|
||||
const Data::File &data) = 0;
|
||||
|
||||
[[nodiscard]] virtual Result writeDialogsStart(
|
||||
const Data::DialogsInfo &data) = 0;
|
||||
[[nodiscard]] virtual Result writeDialogStart(
|
||||
const Data::DialogInfo &data) = 0;
|
||||
[[nodiscard]] virtual Result writeDialogSlice(
|
||||
const Data::MessagesSlice &data) = 0;
|
||||
[[nodiscard]] virtual Result writeDialogEnd() = 0;
|
||||
[[nodiscard]] virtual Result writeDialogsEnd() = 0;
|
||||
|
||||
[[nodiscard]] virtual Result writeLeftChannelsStart(
|
||||
const Data::DialogsInfo &data) = 0;
|
||||
[[nodiscard]] virtual Result writeLeftChannelStart(
|
||||
const Data::DialogInfo &data) = 0;
|
||||
[[nodiscard]] virtual Result writeLeftChannelSlice(
|
||||
const Data::MessagesSlice &data) = 0;
|
||||
[[nodiscard]] virtual Result writeLeftChannelEnd() = 0;
|
||||
[[nodiscard]] virtual Result writeLeftChannelsEnd() = 0;
|
||||
|
||||
[[nodiscard]] virtual Result finish() = 0;
|
||||
|
||||
[[nodiscard]] virtual QString mainFilePath() = 0;
|
||||
|
||||
virtual ~AbstractWriter() = default;
|
||||
|
||||
Stats produceTestExample(const QString &path);
|
||||
|
||||
};
|
||||
|
||||
std::unique_ptr<AbstractWriter> CreateWriter(Format format);
|
||||
|
||||
} // namespace Output
|
||||
} // namespace Export
|
||||
139
Telegram/SourceFiles/export/output/export_output_file.cpp
Normal file
139
Telegram/SourceFiles/export/output/export_output_file.cpp
Normal file
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "export/output/export_output_file.h"
|
||||
|
||||
#include "export/output/export_output_result.h"
|
||||
#include "export/output/export_output_stats.h"
|
||||
|
||||
#include <QtCore/QFileInfo>
|
||||
#include <QtCore/QDir>
|
||||
|
||||
#include <gsl/gsl_util>
|
||||
|
||||
namespace Export {
|
||||
namespace Output {
|
||||
|
||||
File::File(const QString &path, Stats *stats) : _path(path), _stats(stats) {
|
||||
}
|
||||
|
||||
int File::size() const {
|
||||
return _offset;
|
||||
}
|
||||
|
||||
bool File::empty() const {
|
||||
return !_offset;
|
||||
}
|
||||
|
||||
Result File::writeBlock(const QByteArray &block) {
|
||||
const auto result = writeBlockAttempt(block);
|
||||
if (!result) {
|
||||
_file.clear();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Result File::writeBlockAttempt(const QByteArray &block) {
|
||||
if (_stats && !_inStats) {
|
||||
_inStats = true;
|
||||
_stats->incrementFiles();
|
||||
}
|
||||
if (const auto result = reopen(); !result) {
|
||||
return result;
|
||||
}
|
||||
const auto size = block.size();
|
||||
if (!size) {
|
||||
return Result::Success();
|
||||
}
|
||||
if (_file->write(block) == size && _file->flush()) {
|
||||
_offset += size;
|
||||
if (_stats) {
|
||||
_stats->incrementBytes(size);
|
||||
}
|
||||
return Result::Success();
|
||||
}
|
||||
return error();
|
||||
}
|
||||
|
||||
Result File::reopen() {
|
||||
if (_file && _file->isOpen()) {
|
||||
return Result::Success();
|
||||
}
|
||||
_file.emplace(_path);
|
||||
if (_file->exists()) {
|
||||
if (_file->size() < _offset) {
|
||||
return fatalError();
|
||||
} else if (!_file->resize(_offset)) {
|
||||
return error();
|
||||
}
|
||||
} else if (_offset > 0) {
|
||||
return fatalError();
|
||||
}
|
||||
if (_file->open(QIODevice::Append)) {
|
||||
return Result::Success();
|
||||
}
|
||||
const auto info = QFileInfo(_path);
|
||||
const auto dir = info.absoluteDir();
|
||||
return (!dir.exists()
|
||||
&& dir.mkpath(dir.absolutePath())
|
||||
&& _file->open(QIODevice::Append))
|
||||
? Result::Success()
|
||||
: error();
|
||||
}
|
||||
|
||||
Result File::error() const {
|
||||
return Result(Result::Type::Error, _path);
|
||||
}
|
||||
|
||||
Result File::fatalError() const {
|
||||
return Result(Result::Type::FatalError, _path);
|
||||
}
|
||||
|
||||
QString File::PrepareRelativePath(
|
||||
const QString &folder,
|
||||
const QString &suggested) {
|
||||
if (!QFile::exists(folder + suggested)) {
|
||||
return suggested;
|
||||
}
|
||||
|
||||
// Not lastIndexOf('.') so that "file.tar.xz" won't be messed up.
|
||||
const auto position = suggested.indexOf('.');
|
||||
const auto base = suggested.midRef(0, position).toString();
|
||||
const auto extension = (position >= 0)
|
||||
? suggested.midRef(position)
|
||||
: QStringRef();
|
||||
const auto relativePart = [&](int attempt) {
|
||||
auto result = base + QString(" (%1)").arg(attempt);
|
||||
result.append(extension);
|
||||
return result;
|
||||
};
|
||||
auto attempt = 0;
|
||||
while (true) {
|
||||
const auto relativePath = relativePart(++attempt);
|
||||
if (!QFile::exists(folder + relativePath)) {
|
||||
return relativePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Result File::Copy(
|
||||
const QString &source,
|
||||
const QString &path,
|
||||
Stats *stats) {
|
||||
QFile f(source);
|
||||
if (!f.exists() || !f.open(QIODevice::ReadOnly)) {
|
||||
return Result(Result::Type::FatalError, source);
|
||||
}
|
||||
const auto bytes = f.readAll();
|
||||
if (bytes.size() != f.size()) {
|
||||
return Result(Result::Type::FatalError, source);
|
||||
}
|
||||
return File(path, stats).writeBlock(bytes);
|
||||
}
|
||||
|
||||
} // namespace Output
|
||||
} // namespace File
|
||||
57
Telegram/SourceFiles/export/output/export_output_file.h
Normal file
57
Telegram/SourceFiles/export/output/export_output_file.h
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/optional.h"
|
||||
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QByteArray>
|
||||
|
||||
namespace Export {
|
||||
namespace Output {
|
||||
|
||||
struct Result;
|
||||
class Stats;
|
||||
|
||||
class File {
|
||||
public:
|
||||
File(const QString &path, Stats *stats);
|
||||
|
||||
[[nodiscard]] int size() const;
|
||||
[[nodiscard]] bool empty() const;
|
||||
|
||||
[[nodiscard]] Result writeBlock(const QByteArray &block);
|
||||
|
||||
[[nodiscard]] static QString PrepareRelativePath(
|
||||
const QString &folder,
|
||||
const QString &suggested);
|
||||
|
||||
[[nodiscard]] static Result Copy(
|
||||
const QString &source,
|
||||
const QString &path,
|
||||
Stats *stats);
|
||||
|
||||
private:
|
||||
[[nodiscard]] Result reopen();
|
||||
[[nodiscard]] Result writeBlockAttempt(const QByteArray &block);
|
||||
|
||||
[[nodiscard]] Result error() const;
|
||||
[[nodiscard]] Result fatalError() const;
|
||||
|
||||
QString _path;
|
||||
int _offset = 0;
|
||||
base::optional<QFile> _file;
|
||||
|
||||
Stats *_stats = nullptr;
|
||||
bool _inStats = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Output
|
||||
} // namespace File
|
||||
1337
Telegram/SourceFiles/export/output/export_output_html.cpp
Normal file
1337
Telegram/SourceFiles/export/output/export_output_html.cpp
Normal file
File diff suppressed because it is too large
Load Diff
110
Telegram/SourceFiles/export/output/export_output_html.h
Normal file
110
Telegram/SourceFiles/export/output/export_output_html.h
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "export/output/export_output_abstract.h"
|
||||
#include "export/output/export_output_file.h"
|
||||
#include "export/export_settings.h"
|
||||
#include "export/data/export_data_types.h"
|
||||
|
||||
namespace Export {
|
||||
namespace Output {
|
||||
|
||||
class HtmlWriter : public AbstractWriter {
|
||||
public:
|
||||
HtmlWriter();
|
||||
|
||||
Format format() override {
|
||||
return Format::Html;
|
||||
}
|
||||
|
||||
Result start(
|
||||
const Settings &settings,
|
||||
const Environment &environment,
|
||||
Stats *stats) override;
|
||||
|
||||
Result writePersonal(const Data::PersonalInfo &data) override;
|
||||
|
||||
Result writeUserpicsStart(const Data::UserpicsInfo &data) override;
|
||||
Result writeUserpicsSlice(const Data::UserpicsSlice &data) override;
|
||||
Result writeUserpicsEnd() override;
|
||||
|
||||
Result writeContactsList(const Data::ContactsList &data) override;
|
||||
|
||||
Result writeSessionsList(const Data::SessionsList &data) override;
|
||||
|
||||
Result writeOtherData(const Data::File &data) override;
|
||||
|
||||
Result writeDialogsStart(const Data::DialogsInfo &data) override;
|
||||
Result writeDialogStart(const Data::DialogInfo &data) override;
|
||||
Result writeDialogSlice(const Data::MessagesSlice &data) override;
|
||||
Result writeDialogEnd() override;
|
||||
Result writeDialogsEnd() override;
|
||||
|
||||
Result writeLeftChannelsStart(const Data::DialogsInfo &data) override;
|
||||
Result writeLeftChannelStart(const Data::DialogInfo &data) override;
|
||||
Result writeLeftChannelSlice(const Data::MessagesSlice &data) override;
|
||||
Result writeLeftChannelEnd() override;
|
||||
Result writeLeftChannelsEnd() override;
|
||||
|
||||
Result finish() override;
|
||||
|
||||
QString mainFilePath() override;
|
||||
|
||||
~HtmlWriter();
|
||||
|
||||
private:
|
||||
class Wrap;
|
||||
|
||||
Result copyFile(
|
||||
const QString &source,
|
||||
const QString &relativePath) const;
|
||||
|
||||
QString mainFileRelativePath() const;
|
||||
QString pathWithRelativePath(const QString &path) const;
|
||||
std::unique_ptr<Wrap> fileWithRelativePath(const QString &path) const;
|
||||
QString messagesFile(int index) const;
|
||||
|
||||
Result writeSavedContacts(const Data::ContactsList &data);
|
||||
Result writeFrequentContacts(const Data::ContactsList &data);
|
||||
|
||||
Result writeSessions(const Data::SessionsList &data);
|
||||
Result writeWebSessions(const Data::SessionsList &data);
|
||||
|
||||
Result writeChatsStart(
|
||||
const Data::DialogsInfo &data,
|
||||
const QByteArray &listName,
|
||||
const QByteArray &about,
|
||||
const QString &fileName);
|
||||
Result writeChatStart(const Data::DialogInfo &data);
|
||||
Result writeChatSlice(const Data::MessagesSlice &data);
|
||||
Result writeChatEnd();
|
||||
Result writeChatsEnd();
|
||||
Result switchToNextChatFile(int index);
|
||||
|
||||
Settings _settings;
|
||||
Environment _environment;
|
||||
Stats *_stats = nullptr;
|
||||
|
||||
std::unique_ptr<Wrap> _summary;
|
||||
|
||||
int _userpicsCount = 0;
|
||||
std::unique_ptr<Wrap> _userpics;
|
||||
|
||||
int _dialogsCount = 0;
|
||||
int _dialogIndex = 0;
|
||||
Data::DialogInfo _dialog;
|
||||
|
||||
int _messagesCount = 0;
|
||||
std::unique_ptr<Wrap> _chats;
|
||||
std::unique_ptr<Wrap> _chat;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Output
|
||||
} // namespace Export
|
||||
1070
Telegram/SourceFiles/export/output/export_output_json.cpp
Normal file
1070
Telegram/SourceFiles/export/output/export_output_json.cpp
Normal file
File diff suppressed because it is too large
Load Diff
107
Telegram/SourceFiles/export/output/export_output_json.h
Normal file
107
Telegram/SourceFiles/export/output/export_output_json.h
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "export/output/export_output_abstract.h"
|
||||
#include "export/output/export_output_file.h"
|
||||
#include "export/export_settings.h"
|
||||
#include "export/data/export_data_types.h"
|
||||
|
||||
namespace Export {
|
||||
namespace Output {
|
||||
namespace details {
|
||||
|
||||
struct JsonContext {
|
||||
using Type = bool;
|
||||
static constexpr auto kObject = Type(true);
|
||||
static constexpr auto kArray = Type(false);
|
||||
|
||||
// Always fun to use std::vector<bool>.
|
||||
std::vector<Type> nesting;
|
||||
};
|
||||
|
||||
} // namespace details
|
||||
|
||||
class JsonWriter : public AbstractWriter {
|
||||
public:
|
||||
Format format() override {
|
||||
return Format::Json;
|
||||
}
|
||||
|
||||
Result start(
|
||||
const Settings &settings,
|
||||
const Environment &environment,
|
||||
Stats *stats) override;
|
||||
|
||||
Result writePersonal(const Data::PersonalInfo &data) override;
|
||||
|
||||
Result writeUserpicsStart(const Data::UserpicsInfo &data) override;
|
||||
Result writeUserpicsSlice(const Data::UserpicsSlice &data) override;
|
||||
Result writeUserpicsEnd() override;
|
||||
|
||||
Result writeContactsList(const Data::ContactsList &data) override;
|
||||
|
||||
Result writeSessionsList(const Data::SessionsList &data) override;
|
||||
|
||||
Result writeOtherData(const Data::File &data) override;
|
||||
|
||||
Result writeDialogsStart(const Data::DialogsInfo &data) override;
|
||||
Result writeDialogStart(const Data::DialogInfo &data) override;
|
||||
Result writeDialogSlice(const Data::MessagesSlice &data) override;
|
||||
Result writeDialogEnd() override;
|
||||
Result writeDialogsEnd() override;
|
||||
|
||||
Result writeLeftChannelsStart(const Data::DialogsInfo &data) override;
|
||||
Result writeLeftChannelStart(const Data::DialogInfo &data) override;
|
||||
Result writeLeftChannelSlice(const Data::MessagesSlice &data) override;
|
||||
Result writeLeftChannelEnd() override;
|
||||
Result writeLeftChannelsEnd() override;
|
||||
|
||||
Result finish() override;
|
||||
|
||||
QString mainFilePath() override;
|
||||
|
||||
private:
|
||||
using Context = details::JsonContext;
|
||||
|
||||
[[nodiscard]] QByteArray pushNesting(Context::Type type);
|
||||
[[nodiscard]] QByteArray prepareObjectItemStart(const QByteArray &key);
|
||||
[[nodiscard]] QByteArray prepareArrayItemStart();
|
||||
[[nodiscard]] QByteArray popNesting();
|
||||
|
||||
QString mainFileRelativePath() const;
|
||||
QString pathWithRelativePath(const QString &path) const;
|
||||
std::unique_ptr<File> fileWithRelativePath(const QString &path) const;
|
||||
|
||||
Result writeSavedContacts(const Data::ContactsList &data);
|
||||
Result writeFrequentContacts(const Data::ContactsList &data);
|
||||
|
||||
Result writeSessions(const Data::SessionsList &data);
|
||||
Result writeWebSessions(const Data::SessionsList &data);
|
||||
|
||||
Result writeChatsStart(
|
||||
const Data::DialogsInfo &data,
|
||||
const QByteArray &listName);
|
||||
Result writeChatStart(const Data::DialogInfo &data);
|
||||
Result writeChatSlice(const Data::MessagesSlice &data);
|
||||
Result writeChatEnd();
|
||||
Result writeChatsEnd();
|
||||
|
||||
Settings _settings;
|
||||
Environment _environment;
|
||||
Stats *_stats = nullptr;
|
||||
|
||||
Context _context;
|
||||
bool _currentNestingHadItem = false;
|
||||
|
||||
std::unique_ptr<File> _output;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Output
|
||||
} // namespace Export
|
||||
48
Telegram/SourceFiles/export/output/export_output_result.h
Normal file
48
Telegram/SourceFiles/export/output/export_output_result.h
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <QtCore/QString>
|
||||
|
||||
namespace Export {
|
||||
namespace Output {
|
||||
|
||||
struct Result {
|
||||
enum class Type : char {
|
||||
Success,
|
||||
Error,
|
||||
FatalError
|
||||
};
|
||||
|
||||
Result(Type type, QString path) : path(path), type(type) {
|
||||
}
|
||||
|
||||
static Result Success() {
|
||||
return Result(Type::Success, QString());
|
||||
}
|
||||
|
||||
bool isSuccess() const {
|
||||
return type == Type::Success;
|
||||
}
|
||||
bool isError() const {
|
||||
return (type == Type::Error) || (type == Type::FatalError);
|
||||
}
|
||||
bool isFatalError() const {
|
||||
return (type == Type::FatalError);
|
||||
}
|
||||
explicit operator bool() const {
|
||||
return isSuccess();
|
||||
}
|
||||
|
||||
QString path;
|
||||
Type type;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Output
|
||||
} // namespace Export
|
||||
35
Telegram/SourceFiles/export/output/export_output_stats.cpp
Normal file
35
Telegram/SourceFiles/export/output/export_output_stats.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "export/output/export_output_stats.h"
|
||||
|
||||
namespace Export {
|
||||
namespace Output {
|
||||
|
||||
Stats::Stats(const Stats &other)
|
||||
: _files(other._files.load())
|
||||
, _bytes(other._bytes.load()) {
|
||||
}
|
||||
|
||||
void Stats::incrementFiles() {
|
||||
++_files;
|
||||
}
|
||||
|
||||
void Stats::incrementBytes(int count) {
|
||||
_bytes += count;
|
||||
}
|
||||
|
||||
int Stats::filesCount() const {
|
||||
return _files;
|
||||
}
|
||||
|
||||
int64 Stats::bytesCount() const {
|
||||
return _bytes;
|
||||
}
|
||||
|
||||
} // namespace Output
|
||||
} // namespace Export
|
||||
33
Telegram/SourceFiles/export/output/export_output_stats.h
Normal file
33
Telegram/SourceFiles/export/output/export_output_stats.h
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace Export {
|
||||
namespace Output {
|
||||
|
||||
class Stats {
|
||||
public:
|
||||
Stats() = default;
|
||||
Stats(const Stats &other);
|
||||
|
||||
void incrementFiles();
|
||||
void incrementBytes(int count);
|
||||
|
||||
int filesCount() const;
|
||||
int64 bytesCount() const;
|
||||
|
||||
private:
|
||||
std::atomic<int> _files;
|
||||
std::atomic<int64> _bytes;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Output
|
||||
} // namespace Export
|
||||
978
Telegram/SourceFiles/export/output/export_output_text.cpp
Normal file
978
Telegram/SourceFiles/export/output/export_output_text.cpp
Normal file
@@ -0,0 +1,978 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "export/output/export_output_text.h"
|
||||
|
||||
#include "export/output/export_output_result.h"
|
||||
#include "export/data/export_data_types.h"
|
||||
#include "core/utils.h"
|
||||
|
||||
#include <QtCore/QFile>
|
||||
|
||||
namespace Export {
|
||||
namespace Output {
|
||||
namespace {
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
const auto kLineBreak = QByteArrayLiteral("\r\n");
|
||||
#else // Q_OS_WIN
|
||||
const auto kLineBreak = QByteArrayLiteral("\n");
|
||||
#endif // Q_OS_WIN
|
||||
|
||||
void SerializeMultiline(
|
||||
QByteArray &appendTo,
|
||||
const QByteArray &value,
|
||||
int newline) {
|
||||
const auto data = value.data();
|
||||
auto offset = 0;
|
||||
do {
|
||||
appendTo.append("> ");
|
||||
const auto win = (newline > 0 && *(data + newline - 1) == '\r');
|
||||
if (win) --newline;
|
||||
appendTo.append(data + offset, newline - offset).append(kLineBreak);
|
||||
if (win) ++newline;
|
||||
offset = newline + 1;
|
||||
newline = value.indexOf('\n', offset);
|
||||
} while (newline > 0);
|
||||
if (const auto size = value.size(); size > offset) {
|
||||
appendTo.append("> ");
|
||||
appendTo.append(data + offset, size - offset).append(kLineBreak);
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray JoinList(
|
||||
const QByteArray &separator,
|
||||
const std::vector<QByteArray> &list) {
|
||||
if (list.empty()) {
|
||||
return QByteArray();
|
||||
} else if (list.size() == 1) {
|
||||
return list[0];
|
||||
}
|
||||
auto size = (list.size() - 1) * separator.size();
|
||||
for (const auto &value : list) {
|
||||
size += value.size();
|
||||
}
|
||||
auto result = QByteArray();
|
||||
result.reserve(size);
|
||||
auto counter = 0;
|
||||
while (true) {
|
||||
result.append(list[counter]);
|
||||
if (++counter == list.size()) {
|
||||
break;
|
||||
} else {
|
||||
result.append(separator);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QByteArray SerializeKeyValue(
|
||||
std::vector<std::pair<QByteArray, QByteArray>> &&values) {
|
||||
auto result = QByteArray();
|
||||
for (const auto &[key, value] : values) {
|
||||
if (value.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
result.append(key);
|
||||
if (const auto newline = value.indexOf('\n'); newline >= 0) {
|
||||
result.append(':').append(kLineBreak);
|
||||
SerializeMultiline(result, value, newline);
|
||||
} else {
|
||||
result.append(": ").append(value).append(kLineBreak);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Data::Utf8String FormatUsername(const Data::Utf8String &username) {
|
||||
return username.isEmpty() ? username : ('@' + username);
|
||||
}
|
||||
|
||||
QByteArray FormatFilePath(const Data::File &file) {
|
||||
return file.relativePath.toUtf8();
|
||||
}
|
||||
|
||||
QByteArray SerializeMessage(
|
||||
const Data::Message &message,
|
||||
const std::map<Data::PeerId, Data::Peer> &peers,
|
||||
const QString &internalLinksDomain) {
|
||||
using namespace Data;
|
||||
|
||||
if (message.media.content.is<UnsupportedMedia>()) {
|
||||
return "Error! This message is not supported "
|
||||
"by this version of Telegram Desktop. "
|
||||
"Please update the application.";
|
||||
}
|
||||
|
||||
const auto peer = [&](PeerId peerId) -> const Peer& {
|
||||
if (const auto i = peers.find(peerId); i != end(peers)) {
|
||||
return i->second;
|
||||
}
|
||||
static auto empty = Peer{ User() };
|
||||
return empty;
|
||||
};
|
||||
const auto user = [&](int32 userId) -> const User& {
|
||||
if (const auto result = peer(UserPeerId(userId)).user()) {
|
||||
return *result;
|
||||
}
|
||||
static auto empty = User();
|
||||
return empty;
|
||||
};
|
||||
const auto chat = [&](int32 chatId) -> const Chat& {
|
||||
if (const auto result = peer(ChatPeerId(chatId)).chat()) {
|
||||
return *result;
|
||||
}
|
||||
static auto empty = Chat();
|
||||
return empty;
|
||||
};
|
||||
|
||||
auto values = std::vector<std::pair<QByteArray, QByteArray>>{
|
||||
{ "ID", NumberToString(message.id) },
|
||||
{ "Date", FormatDateTime(message.date) },
|
||||
{ "Edited", FormatDateTime(message.edited) },
|
||||
};
|
||||
const auto push = [&](const QByteArray &key, const QByteArray &value) {
|
||||
if (!value.isEmpty()) {
|
||||
values.emplace_back(key, value);
|
||||
}
|
||||
};
|
||||
const auto wrapPeerName = [&](PeerId peerId) {
|
||||
const auto result = peer(peerId).name();
|
||||
return result.isEmpty() ? QByteArray("(deleted peer)") : result;
|
||||
};
|
||||
const auto wrapUserName = [&](int32 userId) {
|
||||
const auto result = user(userId).name();
|
||||
return result.isEmpty() ? QByteArray("(deleted user)") : result;
|
||||
};
|
||||
const auto pushFrom = [&](const QByteArray &label = "From") {
|
||||
if (message.fromId) {
|
||||
push(label, wrapUserName(message.fromId));
|
||||
}
|
||||
};
|
||||
const auto pushReplyToMsgId = [&](
|
||||
const QByteArray &label = "Reply to message") {
|
||||
if (message.replyToMsgId) {
|
||||
push(label, "ID-" + NumberToString(message.replyToMsgId));
|
||||
}
|
||||
};
|
||||
const auto pushUserNames = [&](
|
||||
const std::vector<int32> &data,
|
||||
const QByteArray &labelOne = "Member",
|
||||
const QByteArray &labelMany = "Members") {
|
||||
auto list = std::vector<QByteArray>();
|
||||
for (const auto userId : data) {
|
||||
list.push_back(wrapUserName(userId));
|
||||
}
|
||||
if (list.size() == 1) {
|
||||
push(labelOne, list[0]);
|
||||
} else if (!list.empty()) {
|
||||
push(labelMany, JoinList(", ", list));
|
||||
}
|
||||
};
|
||||
const auto pushActor = [&] {
|
||||
pushFrom("Actor");
|
||||
};
|
||||
const auto pushAction = [&](const QByteArray &action) {
|
||||
push("Action", action);
|
||||
};
|
||||
const auto pushTTL = [&](
|
||||
const QByteArray &label = "Self destruct period") {
|
||||
if (const auto ttl = message.media.ttl) {
|
||||
push(label, NumberToString(ttl) + " sec.");
|
||||
}
|
||||
};
|
||||
|
||||
using SkipReason = Data::File::SkipReason;
|
||||
const auto pushPath = [&](
|
||||
const Data::File &file,
|
||||
const QByteArray &label,
|
||||
const QByteArray &name = QByteArray()) {
|
||||
Expects(!file.relativePath.isEmpty()
|
||||
|| file.skipReason != SkipReason::None);
|
||||
|
||||
push(label, [&]() -> QByteArray {
|
||||
const auto pre = name.isEmpty() ? QByteArray() : name + ' ';
|
||||
switch (file.skipReason) {
|
||||
case SkipReason::Unavailable:
|
||||
return pre + "(" + label + " unavailable, "
|
||||
"please try again later)";
|
||||
case SkipReason::FileSize:
|
||||
return pre + "(" + label + " exceeds maximum size. "
|
||||
"Change data exporting settings to download.)";
|
||||
case SkipReason::FileType:
|
||||
return pre + "(" + label + " not included. "
|
||||
"Change data exporting settings to download.)";
|
||||
case SkipReason::None: return FormatFilePath(file);
|
||||
}
|
||||
Unexpected("Skip reason while writing file path.");
|
||||
}());
|
||||
};
|
||||
const auto pushPhoto = [&](const Image &image) {
|
||||
pushPath(image.file, "Photo");
|
||||
if (image.width && image.height) {
|
||||
push("Width", NumberToString(image.width));
|
||||
push("Height", NumberToString(image.height));
|
||||
}
|
||||
};
|
||||
|
||||
message.action.content.match([&](const ActionChatCreate &data) {
|
||||
pushActor();
|
||||
pushAction("Create group");
|
||||
push("Title", data.title);
|
||||
pushUserNames(data.userIds);
|
||||
}, [&](const ActionChatEditTitle &data) {
|
||||
pushActor();
|
||||
pushAction("Edit group title");
|
||||
push("New title", data.title);
|
||||
}, [&](const ActionChatEditPhoto &data) {
|
||||
pushActor();
|
||||
pushAction("Edit group photo");
|
||||
pushPhoto(data.photo.image);
|
||||
}, [&](const ActionChatDeletePhoto &data) {
|
||||
pushActor();
|
||||
pushAction("Delete group photo");
|
||||
}, [&](const ActionChatAddUser &data) {
|
||||
pushActor();
|
||||
pushAction("Invite members");
|
||||
pushUserNames(data.userIds);
|
||||
}, [&](const ActionChatDeleteUser &data) {
|
||||
pushActor();
|
||||
pushAction("Remove members");
|
||||
push("Member", wrapUserName(data.userId));
|
||||
}, [&](const ActionChatJoinedByLink &data) {
|
||||
pushActor();
|
||||
pushAction("Join group by link");
|
||||
push("Inviter", wrapUserName(data.inviterId));
|
||||
}, [&](const ActionChannelCreate &data) {
|
||||
pushActor();
|
||||
pushAction("Create channel");
|
||||
push("Title", data.title);
|
||||
}, [&](const ActionChatMigrateTo &data) {
|
||||
pushActor();
|
||||
pushAction("Convert this group to supergroup");
|
||||
}, [&](const ActionChannelMigrateFrom &data) {
|
||||
pushActor();
|
||||
pushAction("Basic group converted to supergroup");
|
||||
push("Title", data.title);
|
||||
}, [&](const ActionPinMessage &data) {
|
||||
pushActor();
|
||||
pushAction("Pin message");
|
||||
pushReplyToMsgId("Message");
|
||||
}, [&](const ActionHistoryClear &data) {
|
||||
pushActor();
|
||||
pushAction("Clear history");
|
||||
}, [&](const ActionGameScore &data) {
|
||||
pushActor();
|
||||
pushAction("Score in a game");
|
||||
pushReplyToMsgId("Game message");
|
||||
push("Score", NumberToString(data.score));
|
||||
}, [&](const ActionPaymentSent &data) {
|
||||
pushAction("Send payment");
|
||||
push(
|
||||
"Amount",
|
||||
Data::FormatMoneyAmount(data.amount, data.currency));
|
||||
pushReplyToMsgId("Invoice message");
|
||||
}, [&](const ActionPhoneCall &data) {
|
||||
pushActor();
|
||||
pushAction("Phone call");
|
||||
if (data.duration) {
|
||||
push("Duration", NumberToString(data.duration) + " sec.");
|
||||
}
|
||||
using Reason = ActionPhoneCall::DiscardReason;
|
||||
push("Discard reason", [&] {
|
||||
switch (data.discardReason) {
|
||||
case Reason::Busy: return "Busy";
|
||||
case Reason::Disconnect: return "Disconnect";
|
||||
case Reason::Hangup: return "Hangup";
|
||||
case Reason::Missed: return "Missed";
|
||||
}
|
||||
return "";
|
||||
}());
|
||||
}, [&](const ActionScreenshotTaken &data) {
|
||||
pushActor();
|
||||
pushAction("Take screenshot");
|
||||
}, [&](const ActionCustomAction &data) {
|
||||
pushActor();
|
||||
push("Information", data.message);
|
||||
}, [&](const ActionBotAllowed &data) {
|
||||
pushAction("Allow sending messages");
|
||||
push("Reason", "Login on \"" + data.domain + "\"");
|
||||
}, [&](const ActionSecureValuesSent &data) {
|
||||
pushAction("Send Telegram Passport values");
|
||||
auto list = std::vector<QByteArray>();
|
||||
for (const auto type : data.types) {
|
||||
list.push_back([&] {
|
||||
using Type = ActionSecureValuesSent::Type;
|
||||
switch (type) {
|
||||
case Type::PersonalDetails: return "Personal details";
|
||||
case Type::Passport: return "Passport";
|
||||
case Type::DriverLicense: return "Driver license";
|
||||
case Type::IdentityCard: return "Identity card";
|
||||
case Type::InternalPassport: return "Internal passport";
|
||||
case Type::Address: return "Address information";
|
||||
case Type::UtilityBill: return "Utility bill";
|
||||
case Type::BankStatement: return "Bank statement";
|
||||
case Type::RentalAgreement: return "Rental agreement";
|
||||
case Type::PassportRegistration:
|
||||
return "Passport registration";
|
||||
case Type::TemporaryRegistration:
|
||||
return "Temporary registration";
|
||||
case Type::Phone: return "Phone number";
|
||||
case Type::Email: return "Email";
|
||||
}
|
||||
return "";
|
||||
}());
|
||||
}
|
||||
if (list.size() == 1) {
|
||||
push("Value", list[0]);
|
||||
} else if (!list.empty()) {
|
||||
push("Values", JoinList(", ", list));
|
||||
}
|
||||
}, [](const base::none_type &) {});
|
||||
|
||||
if (!message.action.content) {
|
||||
pushFrom();
|
||||
push("Author", message.signature);
|
||||
if (message.forwardedFromId) {
|
||||
push("Forwarded from", wrapPeerName(message.forwardedFromId));
|
||||
}
|
||||
pushReplyToMsgId();
|
||||
if (message.viaBotId) {
|
||||
push("Via", user(message.viaBotId).username);
|
||||
}
|
||||
}
|
||||
|
||||
message.media.content.match([&](const Photo &photo) {
|
||||
pushPhoto(photo.image);
|
||||
pushTTL();
|
||||
}, [&](const Document &data) {
|
||||
const auto pushMyPath = [&](const QByteArray &label) {
|
||||
return pushPath(data.file, label);
|
||||
};
|
||||
if (data.isSticker) {
|
||||
pushMyPath("Sticker");
|
||||
push("Emoji", data.stickerEmoji);
|
||||
} else if (data.isVideoMessage) {
|
||||
pushMyPath("Video message");
|
||||
} else if (data.isVoiceMessage) {
|
||||
pushMyPath("Voice message");
|
||||
} else if (data.isAnimated) {
|
||||
pushMyPath("Animation");
|
||||
} else if (data.isVideoFile) {
|
||||
pushMyPath("Video file");
|
||||
} else if (data.isAudioFile) {
|
||||
pushMyPath("Audio file");
|
||||
push("Performer", data.songPerformer);
|
||||
push("Title", data.songTitle);
|
||||
} else {
|
||||
pushMyPath("File");
|
||||
}
|
||||
if (!data.isSticker) {
|
||||
push("Mime type", data.mime);
|
||||
}
|
||||
if (data.duration) {
|
||||
push("Duration", NumberToString(data.duration) + " sec.");
|
||||
}
|
||||
if (data.width && data.height) {
|
||||
push("Width", NumberToString(data.width));
|
||||
push("Height", NumberToString(data.height));
|
||||
}
|
||||
pushTTL();
|
||||
}, [&](const ContactInfo &data) {
|
||||
push("Contact information", SerializeKeyValue({
|
||||
{ "First name", data.firstName },
|
||||
{ "Last name", data.lastName },
|
||||
{ "Phone number", FormatPhoneNumber(data.phoneNumber) },
|
||||
}));
|
||||
}, [&](const GeoPoint &data) {
|
||||
push("Location", data.valid ? SerializeKeyValue({
|
||||
{ "Latitude", NumberToString(data.latitude) },
|
||||
{ "Longitude", NumberToString(data.longitude) },
|
||||
}) : QByteArray("(empty value)"));
|
||||
pushTTL("Live location period");
|
||||
}, [&](const Venue &data) {
|
||||
push("Place name", data.title);
|
||||
push("Address", data.address);
|
||||
if (data.point.valid) {
|
||||
push("Location", SerializeKeyValue({
|
||||
{ "Latitude", NumberToString(data.point.latitude) },
|
||||
{ "Longitude", NumberToString(data.point.longitude) },
|
||||
}));
|
||||
}
|
||||
}, [&](const Game &data) {
|
||||
push("Game", data.title);
|
||||
push("Description", data.description);
|
||||
if (data.botId != 0 && !data.shortName.isEmpty()) {
|
||||
const auto bot = user(data.botId);
|
||||
if (bot.isBot && !bot.username.isEmpty()) {
|
||||
push("Link", internalLinksDomain.toUtf8()
|
||||
+ bot.username
|
||||
+ "?game="
|
||||
+ data.shortName);
|
||||
}
|
||||
}
|
||||
}, [&](const Invoice &data) {
|
||||
push("Invoice", SerializeKeyValue({
|
||||
{ "Title", data.title },
|
||||
{ "Description", data.description },
|
||||
{
|
||||
"Amount",
|
||||
Data::FormatMoneyAmount(data.amount, data.currency)
|
||||
},
|
||||
{ "Receipt message", (data.receiptMsgId
|
||||
? "ID-" + NumberToString(data.receiptMsgId)
|
||||
: QByteArray()) }
|
||||
}));
|
||||
}, [](const UnsupportedMedia &data) {
|
||||
Unexpected("Unsupported message.");
|
||||
}, [](const base::none_type &) {});
|
||||
|
||||
auto value = JoinList(QByteArray(), ranges::view::all(
|
||||
message.text
|
||||
) | ranges::view::transform([](const Data::TextPart &part) {
|
||||
return part.text;
|
||||
}) | ranges::to_vector);
|
||||
push("Text", value);
|
||||
|
||||
return SerializeKeyValue(std::move(values));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Result TextWriter::start(
|
||||
const Settings &settings,
|
||||
const Environment &environment,
|
||||
Stats *stats) {
|
||||
Expects(settings.path.endsWith('/'));
|
||||
|
||||
_settings = base::duplicate(settings);
|
||||
_environment = environment;
|
||||
_stats = stats;
|
||||
_summary = fileWithRelativePath(mainFileRelativePath());
|
||||
return _summary->writeBlock(_environment.aboutTelegram
|
||||
+ kLineBreak
|
||||
+ kLineBreak);
|
||||
}
|
||||
|
||||
Result TextWriter::writePersonal(const Data::PersonalInfo &data) {
|
||||
Expects(_summary != nullptr);
|
||||
|
||||
const auto &info = data.user.info;
|
||||
const auto serialized = SerializeKeyValue({
|
||||
{ "First name", info.firstName },
|
||||
{ "Last name", info.lastName },
|
||||
{ "Phone number", Data::FormatPhoneNumber(info.phoneNumber) },
|
||||
{ "Username", FormatUsername(data.user.username) },
|
||||
{ "Bio", data.bio },
|
||||
})
|
||||
+ kLineBreak
|
||||
+ kLineBreak;
|
||||
return _summary->writeBlock(serialized);
|
||||
}
|
||||
|
||||
Result TextWriter::writeUserpicsStart(const Data::UserpicsInfo &data) {
|
||||
Expects(_summary != nullptr);
|
||||
Expects(_userpics == nullptr);
|
||||
|
||||
_userpicsCount = data.count;
|
||||
if (!_userpicsCount) {
|
||||
return Result::Success();
|
||||
}
|
||||
const auto filename = "lists/profile_pictures.txt";
|
||||
_userpics = fileWithRelativePath(filename);
|
||||
|
||||
const auto serialized = "Profile pictures"
|
||||
"(" + Data::NumberToString(_userpicsCount) + ") - " + filename
|
||||
+ kLineBreak
|
||||
+ kLineBreak;
|
||||
return _summary->writeBlock(serialized);
|
||||
}
|
||||
|
||||
Result TextWriter::writeUserpicsSlice(const Data::UserpicsSlice &data) {
|
||||
Expects(_userpics != nullptr);
|
||||
Expects(!data.list.empty());
|
||||
|
||||
auto lines = std::vector<QByteArray>();
|
||||
lines.reserve(data.list.size());
|
||||
for (const auto &userpic : data.list) {
|
||||
if (!userpic.date) {
|
||||
lines.push_back("(deleted photo)");
|
||||
} else {
|
||||
using SkipReason = Data::File::SkipReason;
|
||||
const auto &file = userpic.image.file;
|
||||
Assert(!file.relativePath.isEmpty()
|
||||
|| file.skipReason != SkipReason::None);
|
||||
const auto path = [&]() -> Data::Utf8String {
|
||||
switch (file.skipReason) {
|
||||
case SkipReason::Unavailable:
|
||||
return "(Photo unavailable, please try again later)";
|
||||
case SkipReason::FileSize:
|
||||
return "(Photo exceeds maximum size. "
|
||||
"Change data exporting settings to download.)";
|
||||
case SkipReason::FileType:
|
||||
return "(Photo not included. "
|
||||
"Change data exporting settings to download.)";
|
||||
case SkipReason::None: return FormatFilePath(file);
|
||||
}
|
||||
Unexpected("Skip reason while writing photo path.");
|
||||
}();
|
||||
lines.push_back(SerializeKeyValue({
|
||||
{ "Added", Data::FormatDateTime(userpic.date) },
|
||||
{ "Photo", path },
|
||||
}));
|
||||
}
|
||||
}
|
||||
return _userpics->writeBlock(JoinList(kLineBreak, lines) + kLineBreak);
|
||||
}
|
||||
|
||||
Result TextWriter::writeUserpicsEnd() {
|
||||
_userpics = nullptr;
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
Result TextWriter::writeContactsList(const Data::ContactsList &data) {
|
||||
Expects(_summary != nullptr);
|
||||
|
||||
if (const auto result = writeSavedContacts(data); !result) {
|
||||
return result;
|
||||
} else if (const auto result = writeFrequentContacts(data); !result) {
|
||||
return result;
|
||||
}
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
Result TextWriter::writeSavedContacts(const Data::ContactsList &data) {
|
||||
if (data.list.empty()) {
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
const auto filename = "lists/contacts.txt";
|
||||
const auto file = fileWithRelativePath(filename);
|
||||
auto list = std::vector<QByteArray>();
|
||||
list.reserve(data.list.size());
|
||||
for (const auto index : Data::SortedContactsIndices(data)) {
|
||||
const auto &contact = data.list[index];
|
||||
if (contact.firstName.isEmpty()
|
||||
&& contact.lastName.isEmpty()
|
||||
&& contact.phoneNumber.isEmpty()) {
|
||||
list.push_back("(deleted user)" + kLineBreak);
|
||||
} else {
|
||||
list.push_back(SerializeKeyValue({
|
||||
{ "First name", contact.firstName },
|
||||
{ "Last name", contact.lastName },
|
||||
{
|
||||
"Phone number",
|
||||
Data::FormatPhoneNumber(contact.phoneNumber)
|
||||
},
|
||||
{ "Added", Data::FormatDateTime(contact.date) }
|
||||
}));
|
||||
}
|
||||
}
|
||||
const auto full = _environment.aboutContacts
|
||||
+ kLineBreak
|
||||
+ kLineBreak
|
||||
+ JoinList(kLineBreak, list);
|
||||
if (const auto result = file->writeBlock(full); !result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto header = "Contacts "
|
||||
"(" + Data::NumberToString(data.list.size()) + ") - " + filename
|
||||
+ kLineBreak
|
||||
+ kLineBreak;
|
||||
return _summary->writeBlock(header);
|
||||
}
|
||||
|
||||
Result TextWriter::writeFrequentContacts(const Data::ContactsList &data) {
|
||||
const auto size = data.correspondents.size()
|
||||
+ data.inlineBots.size()
|
||||
+ data.phoneCalls.size();
|
||||
if (!size) {
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
const auto filename = "lists/frequent.txt";
|
||||
const auto file = fileWithRelativePath(filename);
|
||||
auto list = std::vector<QByteArray>();
|
||||
list.reserve(size);
|
||||
const auto writeList = [&](
|
||||
const std::vector<Data::TopPeer> &peers,
|
||||
Data::Utf8String category) {
|
||||
for (const auto &top : peers) {
|
||||
const auto user = [&]() -> Data::Utf8String {
|
||||
if (!top.peer.user() || top.peer.user()->isSelf) {
|
||||
return Data::Utf8String();
|
||||
} else if (top.peer.name().isEmpty()) {
|
||||
return "(deleted user)";
|
||||
}
|
||||
return top.peer.name();
|
||||
}();
|
||||
const auto chatType = [&] {
|
||||
if (const auto chat = top.peer.chat()) {
|
||||
return chat->username.isEmpty()
|
||||
? (chat->isBroadcast
|
||||
? "Private channel"
|
||||
: (chat->isSupergroup
|
||||
? "Private supergroup"
|
||||
: "Private group"))
|
||||
: (chat->isBroadcast
|
||||
? "Public channel"
|
||||
: "Public supergroup");
|
||||
}
|
||||
return "";
|
||||
}();
|
||||
const auto chat = [&]() -> Data::Utf8String {
|
||||
if (!top.peer.chat()) {
|
||||
return Data::Utf8String();
|
||||
} else if (top.peer.name().isEmpty()) {
|
||||
return "(deleted chat)";
|
||||
}
|
||||
return top.peer.name();
|
||||
}();
|
||||
const auto saved = [&]() -> Data::Utf8String {
|
||||
if (!top.peer.user() || !top.peer.user()->isSelf) {
|
||||
return Data::Utf8String();
|
||||
}
|
||||
return "Saved messages";
|
||||
}();
|
||||
list.push_back(SerializeKeyValue({
|
||||
{ "Category", category },
|
||||
{ "User", top.peer.user() ? user : QByteArray() },
|
||||
{ "Chat", saved },
|
||||
{ chatType, chat },
|
||||
{ "Rating", Data::NumberToString(top.rating) }
|
||||
}));
|
||||
}
|
||||
};
|
||||
writeList(data.correspondents, "People");
|
||||
writeList(data.inlineBots, "Inline bots");
|
||||
writeList(data.phoneCalls, "Calls");
|
||||
const auto full = _environment.aboutFrequent
|
||||
+ kLineBreak
|
||||
+ kLineBreak
|
||||
+ JoinList(kLineBreak, list);
|
||||
if (const auto result = file->writeBlock(full); !result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto header = "Frequent contacts "
|
||||
"(" + Data::NumberToString(size) + ") - lists/frequent.txt"
|
||||
+ kLineBreak
|
||||
+ kLineBreak;
|
||||
return _summary->writeBlock(header);
|
||||
}
|
||||
|
||||
Result TextWriter::writeSessionsList(const Data::SessionsList &data) {
|
||||
Expects(_summary != nullptr);
|
||||
|
||||
if (const auto result = writeSessions(data); !result) {
|
||||
return result;
|
||||
} else if (const auto result = writeWebSessions(data); !result) {
|
||||
return result;
|
||||
}
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
Result TextWriter::writeSessions(const Data::SessionsList &data) {
|
||||
Expects(_summary != nullptr);
|
||||
|
||||
if (data.list.empty()) {
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
const auto filename = "lists/sessions.txt";
|
||||
const auto file = fileWithRelativePath(filename);
|
||||
auto list = std::vector<QByteArray>();
|
||||
list.reserve(data.list.size());
|
||||
for (const auto &session : data.list) {
|
||||
list.push_back(SerializeKeyValue({
|
||||
{ "Last active", Data::FormatDateTime(session.lastActive) },
|
||||
{ "Last IP address", session.ip },
|
||||
{ "Last country", session.country },
|
||||
{ "Last region", session.region },
|
||||
{
|
||||
"Application name",
|
||||
(session.applicationName.isEmpty()
|
||||
? Data::Utf8String("(unknown)")
|
||||
: session.applicationName)
|
||||
},
|
||||
{ "Application version", session.applicationVersion },
|
||||
{ "Device model", session.deviceModel },
|
||||
{ "Platform", session.platform },
|
||||
{ "System version", session.systemVersion },
|
||||
{ "Created", Data::FormatDateTime(session.created) },
|
||||
}));
|
||||
}
|
||||
const auto full = _environment.aboutSessions
|
||||
+ kLineBreak
|
||||
+ kLineBreak
|
||||
+ JoinList(kLineBreak, list);
|
||||
if (const auto result = file->writeBlock(full); !result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto header = "Sessions "
|
||||
"(" + Data::NumberToString(data.list.size()) + ") - " + filename
|
||||
+ kLineBreak
|
||||
+ kLineBreak;
|
||||
return _summary->writeBlock(header);
|
||||
}
|
||||
|
||||
Result TextWriter::writeWebSessions(const Data::SessionsList &data) {
|
||||
Expects(_summary != nullptr);
|
||||
|
||||
if (data.webList.empty()) {
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
const auto filename = "lists/web_sessions.txt";
|
||||
const auto file = fileWithRelativePath(filename);
|
||||
auto list = std::vector<QByteArray>();
|
||||
list.reserve(data.webList.size());
|
||||
for (const auto &session : data.webList) {
|
||||
list.push_back(SerializeKeyValue({
|
||||
{ "Last active", Data::FormatDateTime(session.lastActive) },
|
||||
{ "Last IP address", session.ip },
|
||||
{ "Last region", session.region },
|
||||
{
|
||||
"Bot username",
|
||||
(session.botUsername.isEmpty()
|
||||
? Data::Utf8String("(unknown)")
|
||||
: session.botUsername)
|
||||
},
|
||||
{
|
||||
"Domain name",
|
||||
(session.domain.isEmpty()
|
||||
? Data::Utf8String("(unknown)")
|
||||
: session.domain)
|
||||
},
|
||||
{ "Browser", session.browser },
|
||||
{ "Platform", session.platform },
|
||||
{ "Created", Data::FormatDateTime(session.created) },
|
||||
}));
|
||||
}
|
||||
const auto full = _environment.aboutWebSessions
|
||||
+ kLineBreak
|
||||
+ kLineBreak
|
||||
+ JoinList(kLineBreak, list);
|
||||
if (const auto result = file->writeBlock(full); !result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto header = "Web sessions "
|
||||
"(" + Data::NumberToString(data.webList.size()) + ") - " + filename
|
||||
+ kLineBreak
|
||||
+ kLineBreak;
|
||||
return _summary->writeBlock(header);
|
||||
}
|
||||
|
||||
Result TextWriter::writeOtherData(const Data::File &data) {
|
||||
Expects(_summary != nullptr);
|
||||
|
||||
const auto header = "Other data - " + data.relativePath.toUtf8()
|
||||
+ kLineBreak
|
||||
+ kLineBreak;
|
||||
return _summary->writeBlock(header);
|
||||
}
|
||||
|
||||
Result TextWriter::writeDialogsStart(const Data::DialogsInfo &data) {
|
||||
return writeChatsStart(
|
||||
data,
|
||||
"Chats",
|
||||
_environment.aboutChats,
|
||||
"lists/chats.txt");
|
||||
}
|
||||
|
||||
Result TextWriter::writeDialogStart(const Data::DialogInfo &data) {
|
||||
return writeChatStart(data);
|
||||
}
|
||||
|
||||
Result TextWriter::writeDialogSlice(const Data::MessagesSlice &data) {
|
||||
return writeChatSlice(data);
|
||||
}
|
||||
|
||||
Result TextWriter::writeDialogEnd() {
|
||||
return writeChatEnd();
|
||||
}
|
||||
|
||||
Result TextWriter::writeDialogsEnd() {
|
||||
return writeChatsEnd();
|
||||
}
|
||||
|
||||
Result TextWriter::writeLeftChannelsStart(const Data::DialogsInfo &data) {
|
||||
return writeChatsStart(
|
||||
data,
|
||||
"Left chats",
|
||||
_environment.aboutLeftChats,
|
||||
"lists/left_chats.txt");
|
||||
}
|
||||
|
||||
Result TextWriter::writeLeftChannelStart(const Data::DialogInfo &data) {
|
||||
return writeChatStart(data);
|
||||
}
|
||||
|
||||
Result TextWriter::writeLeftChannelSlice(const Data::MessagesSlice &data) {
|
||||
return writeChatSlice(data);
|
||||
}
|
||||
|
||||
Result TextWriter::writeLeftChannelEnd() {
|
||||
return writeChatEnd();
|
||||
}
|
||||
|
||||
Result TextWriter::writeLeftChannelsEnd() {
|
||||
return writeChatsEnd();
|
||||
}
|
||||
|
||||
Result TextWriter::writeChatsStart(
|
||||
const Data::DialogsInfo &data,
|
||||
const QByteArray &listName,
|
||||
const QByteArray &about,
|
||||
const QString &fileName) {
|
||||
Expects(_summary != nullptr);
|
||||
Expects(_chats == nullptr);
|
||||
|
||||
if (data.list.empty()) {
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
_chats = fileWithRelativePath(fileName);
|
||||
_dialogIndex = 0;
|
||||
_dialogsCount = data.list.size();
|
||||
|
||||
const auto block = about + kLineBreak;
|
||||
if (const auto result = _chats->writeBlock(block); !result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto header = listName + " "
|
||||
"(" + Data::NumberToString(data.list.size()) + ") - "
|
||||
+ fileName.toUtf8()
|
||||
+ kLineBreak
|
||||
+ kLineBreak;
|
||||
return _summary->writeBlock(header);
|
||||
}
|
||||
|
||||
Result TextWriter::writeChatStart(const Data::DialogInfo &data) {
|
||||
Expects(_chat == nullptr);
|
||||
Expects(_dialogIndex < _dialogsCount);
|
||||
|
||||
const auto digits = Data::NumberToString(_dialogsCount - 1).size();
|
||||
const auto number = Data::NumberToString(++_dialogIndex, digits, '0');
|
||||
_chat = fileWithRelativePath(data.relativePath + "messages.txt");
|
||||
_messagesCount = 0;
|
||||
_dialog = data;
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
Result TextWriter::writeChatSlice(const Data::MessagesSlice &data) {
|
||||
Expects(_chat != nullptr);
|
||||
Expects(!data.list.empty());
|
||||
|
||||
_messagesCount += data.list.size();
|
||||
auto list = std::vector<QByteArray>();
|
||||
list.reserve(data.list.size());
|
||||
for (const auto &message : data.list) {
|
||||
list.push_back(SerializeMessage(
|
||||
message,
|
||||
data.peers,
|
||||
_environment.internalLinksDomain));
|
||||
}
|
||||
const auto full = _chat->empty()
|
||||
? JoinList(kLineBreak, list)
|
||||
: kLineBreak + JoinList(kLineBreak, list);
|
||||
return _chat->writeBlock(full);
|
||||
}
|
||||
|
||||
Result TextWriter::writeChatEnd() {
|
||||
Expects(_chats != nullptr);
|
||||
Expects(_chat != nullptr);
|
||||
|
||||
_chat = nullptr;
|
||||
|
||||
using Type = Data::DialogInfo::Type;
|
||||
const auto TypeString = [](Type type) {
|
||||
switch (type) {
|
||||
case Type::Unknown: return "(unknown)";
|
||||
case Type::Self:
|
||||
case Type::Personal: return "Personal chat";
|
||||
case Type::Bot: return "Bot chat";
|
||||
case Type::PrivateGroup: return "Private group";
|
||||
case Type::PrivateSupergroup: return "Private supergroup";
|
||||
case Type::PublicSupergroup: return "Public supergroup";
|
||||
case Type::PrivateChannel: return "Private channel";
|
||||
case Type::PublicChannel: return "Public channel";
|
||||
}
|
||||
Unexpected("Dialog type in TypeString.");
|
||||
};
|
||||
const auto NameString = [](
|
||||
const Data::DialogInfo &dialog,
|
||||
Type type) -> QByteArray {
|
||||
if (dialog.type == Type::Self) {
|
||||
return "Saved messages";
|
||||
}
|
||||
const auto name = dialog.name;
|
||||
if (!name.isEmpty()) {
|
||||
return name;
|
||||
}
|
||||
switch (type) {
|
||||
case Type::Unknown: return "(unknown)";
|
||||
case Type::Personal: return "(deleted user)";
|
||||
case Type::Bot: return "(deleted bot)";
|
||||
case Type::PrivateGroup:
|
||||
case Type::PrivateSupergroup:
|
||||
case Type::PublicSupergroup: return "(deleted group)";
|
||||
case Type::PrivateChannel:
|
||||
case Type::PublicChannel: return "(deleted channel)";
|
||||
}
|
||||
Unexpected("Dialog type in TypeString.");
|
||||
};
|
||||
return _chats->writeBlock(kLineBreak + SerializeKeyValue({
|
||||
{ "Name", NameString(_dialog, _dialog.type) },
|
||||
{ "Type", TypeString(_dialog.type) },
|
||||
{
|
||||
(_dialog.onlyMyMessages
|
||||
? "Outgoing messages count"
|
||||
: "Messages count"),
|
||||
Data::NumberToString(_messagesCount)
|
||||
},
|
||||
{
|
||||
"Content",
|
||||
(_messagesCount > 0
|
||||
? (_dialog.relativePath + "messages.txt").toUtf8()
|
||||
: QByteArray())
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
Result TextWriter::writeChatsEnd() {
|
||||
_chats = nullptr;
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
Result TextWriter::finish() {
|
||||
return Result::Success();
|
||||
}
|
||||
|
||||
QString TextWriter::mainFilePath() {
|
||||
return pathWithRelativePath(mainFileRelativePath());
|
||||
}
|
||||
|
||||
QString TextWriter::mainFileRelativePath() const {
|
||||
return "export_results.txt";
|
||||
}
|
||||
|
||||
QString TextWriter::pathWithRelativePath(const QString &path) const {
|
||||
return _settings.path + path;
|
||||
}
|
||||
|
||||
std::unique_ptr<File> TextWriter::fileWithRelativePath(
|
||||
const QString &path) const {
|
||||
return std::make_unique<File>(pathWithRelativePath(path), _stats);
|
||||
}
|
||||
|
||||
} // namespace Output
|
||||
} // namespace Export
|
||||
98
Telegram/SourceFiles/export/output/export_output_text.h
Normal file
98
Telegram/SourceFiles/export/output/export_output_text.h
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "export/output/export_output_abstract.h"
|
||||
#include "export/output/export_output_file.h"
|
||||
#include "export/export_settings.h"
|
||||
#include "export/data/export_data_types.h"
|
||||
|
||||
namespace Export {
|
||||
namespace Output {
|
||||
|
||||
class TextWriter : public AbstractWriter {
|
||||
public:
|
||||
Format format() override {
|
||||
return Format::Text;
|
||||
}
|
||||
|
||||
Result start(
|
||||
const Settings &settings,
|
||||
const Environment &environment,
|
||||
Stats *stats) override;
|
||||
|
||||
Result writePersonal(const Data::PersonalInfo &data) override;
|
||||
|
||||
Result writeUserpicsStart(const Data::UserpicsInfo &data) override;
|
||||
Result writeUserpicsSlice(const Data::UserpicsSlice &data) override;
|
||||
Result writeUserpicsEnd() override;
|
||||
|
||||
Result writeContactsList(const Data::ContactsList &data) override;
|
||||
|
||||
Result writeSessionsList(const Data::SessionsList &data) override;
|
||||
|
||||
Result writeOtherData(const Data::File &data) override;
|
||||
|
||||
Result writeDialogsStart(const Data::DialogsInfo &data) override;
|
||||
Result writeDialogStart(const Data::DialogInfo &data) override;
|
||||
Result writeDialogSlice(const Data::MessagesSlice &data) override;
|
||||
Result writeDialogEnd() override;
|
||||
Result writeDialogsEnd() override;
|
||||
|
||||
Result writeLeftChannelsStart(const Data::DialogsInfo &data) override;
|
||||
Result writeLeftChannelStart(const Data::DialogInfo &data) override;
|
||||
Result writeLeftChannelSlice(const Data::MessagesSlice &data) override;
|
||||
Result writeLeftChannelEnd() override;
|
||||
Result writeLeftChannelsEnd() override;
|
||||
|
||||
Result finish() override;
|
||||
|
||||
QString mainFilePath() override;
|
||||
|
||||
private:
|
||||
QString mainFileRelativePath() const;
|
||||
QString pathWithRelativePath(const QString &path) const;
|
||||
std::unique_ptr<File> fileWithRelativePath(const QString &path) const;
|
||||
|
||||
Result writeSavedContacts(const Data::ContactsList &data);
|
||||
Result writeFrequentContacts(const Data::ContactsList &data);
|
||||
|
||||
Result writeSessions(const Data::SessionsList &data);
|
||||
Result writeWebSessions(const Data::SessionsList &data);
|
||||
|
||||
Result writeChatsStart(
|
||||
const Data::DialogsInfo &data,
|
||||
const QByteArray &listName,
|
||||
const QByteArray &about,
|
||||
const QString &fileName);
|
||||
Result writeChatStart(const Data::DialogInfo &data);
|
||||
Result writeChatSlice(const Data::MessagesSlice &data);
|
||||
Result writeChatEnd();
|
||||
Result writeChatsEnd();
|
||||
|
||||
Settings _settings;
|
||||
Environment _environment;
|
||||
Stats *_stats = nullptr;
|
||||
|
||||
std::unique_ptr<File> _summary;
|
||||
|
||||
int _userpicsCount = 0;
|
||||
std::unique_ptr<File> _userpics;
|
||||
|
||||
int _dialogsCount = 0;
|
||||
int _dialogIndex = 0;
|
||||
Data::DialogInfo _dialog;
|
||||
|
||||
int _messagesCount = 0;
|
||||
std::unique_ptr<File> _chats;
|
||||
std::unique_ptr<File> _chat;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Output
|
||||
} // namespace Export
|
||||
103
Telegram/SourceFiles/export/view/export.style
Normal file
103
Telegram/SourceFiles/export/view/export.style
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
using "basic.style";
|
||||
|
||||
using "ui/widgets/widgets.style";
|
||||
using "boxes/boxes.style";
|
||||
|
||||
exportPanelSize: size(364px, 480px);
|
||||
exportSettingPadding: margins(22px, 8px, 22px, 8px);
|
||||
exportSubSettingPadding: margins(56px, 4px, 22px, 12px);
|
||||
exportHeaderLabel: FlatLabel(boxTitle) {
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: font(15px semibold);
|
||||
linkFont: font(15px semibold);
|
||||
linkFontOver: font(15px semibold underline);
|
||||
}
|
||||
}
|
||||
exportHeaderPadding: margins(22px, 20px, 22px, 9px);
|
||||
exportFileSizeSlider: MediaSlider {
|
||||
width: 3px;
|
||||
activeFg: mediaPlayerActiveFg;
|
||||
inactiveFg: mediaPlayerInactiveFg;
|
||||
activeFgOver: mediaPlayerActiveFg;
|
||||
inactiveFgOver: mediaPlayerInactiveFg;
|
||||
activeFgDisabled: mediaPlayerInactiveFg;
|
||||
inactiveFgDisabled: windowBg;
|
||||
seekSize: size(15px, 15px);
|
||||
duration: 150;
|
||||
}
|
||||
exportFileSizeLabel: LabelSimple(defaultLabelSimple) {
|
||||
font: boxTextFont;
|
||||
}
|
||||
exportFileSizePadding: margins(22px, 8px, 22px, 8px);
|
||||
exportFileSizeLabelBottom: 18px;
|
||||
|
||||
exportLocationLabel: FlatLabel(boxLabel) {
|
||||
maxHeight: 21px;
|
||||
}
|
||||
exportLocationPadding: margins(22px, 8px, 22px, 8px);
|
||||
|
||||
exportAboutOptionLabel: FlatLabel(defaultFlatLabel) {
|
||||
textFg: windowSubTextFg;
|
||||
minWidth: 175px;
|
||||
}
|
||||
exportAboutOptionPadding: margins(22px, 0px, 22px, 16px);
|
||||
|
||||
exportErrorLabel: FlatLabel(boxLabel) {
|
||||
minWidth: 175px;
|
||||
align: align(top);
|
||||
textFg: boxTextFgError;
|
||||
}
|
||||
|
||||
exportProgressDuration: 200;
|
||||
exportProgressRowHeight: 30px;
|
||||
exportProgressRowPadding: margins(22px, 10px, 22px, 20px);
|
||||
exportProgressLabel: FlatLabel(boxLabel) {
|
||||
textFg: windowBoldFg;
|
||||
maxHeight: 20px;
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: font(14px semibold);
|
||||
linkFont: font(14px semibold);
|
||||
linkFontOver: font(14px semibold);
|
||||
}
|
||||
}
|
||||
exportProgressInfoLabel: FlatLabel(boxLabel) {
|
||||
textFg: windowSubTextFg;
|
||||
maxHeight: 20px;
|
||||
style: boxTextStyle;
|
||||
}
|
||||
exportProgressWidth: 3px;
|
||||
exportProgressFg: mediaPlayerActiveFg;
|
||||
exportProgressBg: mediaPlayerInactiveFg;
|
||||
|
||||
exportCancelButton: RoundButton(attentionBoxButton) {
|
||||
width: 200px;
|
||||
height: 44px;
|
||||
textTop: 12px;
|
||||
font: font(semibold 15px);
|
||||
}
|
||||
exportCancelBottom: 30px;
|
||||
exportDoneButton: RoundButton(defaultActiveButton) {
|
||||
width: 200px;
|
||||
height: 44px;
|
||||
textTop: 12px;
|
||||
font: font(semibold 15px);
|
||||
}
|
||||
|
||||
exportAboutLabel: FlatLabel(boxLabel) {
|
||||
textFg: windowSubTextFg;
|
||||
}
|
||||
exportAboutPadding: margins(22px, 10px, 22px, 0px);
|
||||
|
||||
exportTopBarLabel: FlatLabel(defaultFlatLabel) {
|
||||
maxHeight: 20px;
|
||||
palette: TextPalette(defaultTextPalette) {
|
||||
linkFg: windowSubTextFg;
|
||||
}
|
||||
}
|
||||
138
Telegram/SourceFiles/export/view/export_view_content.cpp
Normal file
138
Telegram/SourceFiles/export/view/export_view_content.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "export/view/export_view_content.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "layout.h"
|
||||
|
||||
namespace Export {
|
||||
namespace View {
|
||||
|
||||
const QString Content::kDoneId = "done";
|
||||
|
||||
Content ContentFromState(const ProcessingState &state) {
|
||||
using Step = ProcessingState::Step;
|
||||
|
||||
auto result = Content();
|
||||
const auto push = [&](
|
||||
const QString &id,
|
||||
const QString &label,
|
||||
const QString &info,
|
||||
float64 progress) {
|
||||
result.rows.push_back({ id, label, info, progress });
|
||||
};
|
||||
const auto pushMain = [&](const QString &label) {
|
||||
const auto info = (state.entityCount > 0)
|
||||
? (QString::number(state.entityIndex)
|
||||
+ " / "
|
||||
+ QString::number(state.entityCount))
|
||||
: QString();
|
||||
if (!state.substepsTotal) {
|
||||
push("main", label, info, 0.);
|
||||
return;
|
||||
}
|
||||
const auto substepsTotal = state.substepsTotal;
|
||||
const auto step = static_cast<int>(state.step);
|
||||
const auto done = state.substepsPassed;
|
||||
const auto add = state.substepsNow;
|
||||
const auto doneProgress = done / float64(substepsTotal);
|
||||
const auto addProgress = (state.entityCount > 0)
|
||||
? ((float64(add) * state.entityIndex)
|
||||
/ (float64(substepsTotal) * state.entityCount))
|
||||
: 0.;
|
||||
push("main", label, info, doneProgress + addProgress);
|
||||
};
|
||||
const auto pushBytes = [&](const QString &id, const QString &label) {
|
||||
if (!state.bytesCount) {
|
||||
return;
|
||||
}
|
||||
const auto progress = state.bytesLoaded / float64(state.bytesCount);
|
||||
const auto info = formatDownloadText(
|
||||
state.bytesLoaded,
|
||||
state.bytesCount);
|
||||
push(id, label, info, progress);
|
||||
};
|
||||
switch (state.step) {
|
||||
case Step::Initializing:
|
||||
pushMain(lang(lng_export_state_initializing));
|
||||
break;
|
||||
case Step::LeftChannelsList:
|
||||
case Step::DialogsList:
|
||||
pushMain(lang(lng_export_state_chats_list));
|
||||
break;
|
||||
case Step::PersonalInfo:
|
||||
pushMain(lang(lng_export_option_info));
|
||||
break;
|
||||
case Step::Userpics:
|
||||
pushMain(lang(lng_export_state_userpics));
|
||||
pushBytes(
|
||||
"userpic" + QString::number(state.entityIndex),
|
||||
state.bytesName);
|
||||
break;
|
||||
case Step::Contacts:
|
||||
pushMain(lang(lng_export_option_contacts));
|
||||
break;
|
||||
case Step::Sessions:
|
||||
pushMain(lang(lng_export_option_sessions));
|
||||
break;
|
||||
case Step::OtherData:
|
||||
pushMain(lang(lng_export_option_other));
|
||||
break;
|
||||
case Step::LeftChannels:
|
||||
case Step::Dialogs:
|
||||
pushMain(lang(lng_export_state_chats));
|
||||
push(
|
||||
"chat" + QString::number(state.entityIndex),
|
||||
(state.entityName.isEmpty()
|
||||
? lang(lng_deleted)
|
||||
: state.entityName),
|
||||
(state.itemCount > 0
|
||||
? (QString::number(state.itemIndex)
|
||||
+ " / "
|
||||
+ QString::number(state.itemCount))
|
||||
: QString()),
|
||||
(state.itemCount > 0
|
||||
? (state.itemIndex / float64(state.itemCount))
|
||||
: 0.));
|
||||
pushBytes(
|
||||
("file"
|
||||
+ QString::number(state.entityIndex)
|
||||
+ '_'
|
||||
+ QString::number(state.itemIndex)),
|
||||
state.bytesName);
|
||||
break;
|
||||
default: Unexpected("Step in ContentFromState.");
|
||||
}
|
||||
while (result.rows.size() < 3) {
|
||||
result.rows.push_back(Content::Row());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Content ContentFromState(const FinishedState &state) {
|
||||
auto result = Content();
|
||||
result.rows.push_back({
|
||||
Content::kDoneId,
|
||||
lang(lng_export_finished),
|
||||
QString(),
|
||||
1. });
|
||||
result.rows.push_back({
|
||||
Content::kDoneId,
|
||||
lng_export_total_files(lt_count, QString::number(state.filesCount)),
|
||||
QString(),
|
||||
1. });
|
||||
result.rows.push_back({
|
||||
Content::kDoneId,
|
||||
lng_export_total_size(lt_size, formatSizeText(state.bytesCount)),
|
||||
QString(),
|
||||
1. });
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace View
|
||||
} // namespace Export
|
||||
48
Telegram/SourceFiles/export/view/export_view_content.h
Normal file
48
Telegram/SourceFiles/export/view/export_view_content.h
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "export/export_controller.h"
|
||||
|
||||
namespace Export {
|
||||
namespace View {
|
||||
|
||||
struct Content {
|
||||
struct Row {
|
||||
QString id;
|
||||
QString label;
|
||||
QString info;
|
||||
float64 progress = 0.;
|
||||
};
|
||||
|
||||
std::vector<Row> rows;
|
||||
|
||||
static const QString kDoneId;
|
||||
|
||||
};
|
||||
|
||||
Content ContentFromState(const ProcessingState &state);
|
||||
Content ContentFromState(const FinishedState &state);
|
||||
|
||||
inline auto ContentFromState(rpl::producer<State> state) {
|
||||
return std::move(
|
||||
state
|
||||
) | rpl::filter([](const State &state) {
|
||||
return state.is<ProcessingState>() || state.is<FinishedState>();
|
||||
}) | rpl::map([](const State &state) {
|
||||
if (const auto process = base::get_if<ProcessingState>(&state)) {
|
||||
return ContentFromState(*process);
|
||||
} else if (const auto done = base::get_if<FinishedState>(&state)) {
|
||||
return ContentFromState(*done);
|
||||
}
|
||||
Unexpected("State type in ContentFromState.");
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace View
|
||||
} // namespace Export
|
||||
@@ -0,0 +1,373 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "export/view/export_view_panel_controller.h"
|
||||
|
||||
#include "export/view/export_view_settings.h"
|
||||
#include "export/view/export_view_progress.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/separate_panel.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "boxes/confirm_box.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "storage/localstorage.h"
|
||||
#include "core/file_utilities.h"
|
||||
#include "platform/platform_specific.h"
|
||||
#include "auth_session.h"
|
||||
#include "data/data_session.h"
|
||||
#include "styles/style_export.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
namespace Export {
|
||||
namespace View {
|
||||
namespace {
|
||||
|
||||
constexpr auto kSaveSettingsTimeout = TimeMs(1000);
|
||||
|
||||
class SuggestBox : public BoxContent {
|
||||
public:
|
||||
SuggestBox(QWidget*);
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
|
||||
private:
|
||||
bool _cleared = false;
|
||||
|
||||
};
|
||||
|
||||
SuggestBox::SuggestBox(QWidget*) {
|
||||
}
|
||||
|
||||
void SuggestBox::prepare() {
|
||||
setTitle(langFactory(lng_export_suggest_title));
|
||||
|
||||
const auto clear = [=] {
|
||||
if (_cleared) {
|
||||
return;
|
||||
}
|
||||
_cleared = true;
|
||||
ClearSuggestStart();
|
||||
};
|
||||
|
||||
addButton(langFactory(lng_box_ok), [=] {
|
||||
clear();
|
||||
closeBox();
|
||||
Auth().data().startExport();
|
||||
});
|
||||
addButton(langFactory(lng_export_suggest_cancel), [=] { closeBox(); });
|
||||
setCloseByOutsideClick(false);
|
||||
|
||||
const auto content = Ui::CreateChild<Ui::FlatLabel>(
|
||||
this,
|
||||
lang(lng_export_suggest_text),
|
||||
Ui::FlatLabel::InitType::Simple,
|
||||
st::boxLabel);
|
||||
widthValue(
|
||||
) | rpl::start_with_next([=](int width) {
|
||||
const auto contentWidth = width
|
||||
- st::boxPadding.left()
|
||||
- st::boxPadding.right();
|
||||
content->resizeToWidth(contentWidth);
|
||||
content->moveToLeft(st::boxPadding.left(), 0);
|
||||
}, content->lifetime());
|
||||
content->heightValue(
|
||||
) | rpl::start_with_next([=](int height) {
|
||||
setDimensions(st::boxWidth, height + st::boxPadding.bottom());
|
||||
}, content->lifetime());
|
||||
|
||||
boxClosing() | rpl::start_with_next(clear, lifetime());
|
||||
}
|
||||
|
||||
Environment PrepareEnvironment() {
|
||||
auto result = Environment();
|
||||
const auto utfLang = [](LangKey key) {
|
||||
return lang(key).toUtf8();
|
||||
};
|
||||
result.internalLinksDomain = Global::InternalLinksDomain();
|
||||
result.aboutTelegram = utfLang(lng_export_about_telegram);
|
||||
result.aboutContacts = utfLang(lng_export_about_contacts);
|
||||
result.aboutFrequent = utfLang(lng_export_about_frequent);
|
||||
result.aboutSessions = utfLang(lng_export_about_sessions);
|
||||
result.aboutWebSessions = utfLang(lng_export_about_web_sessions);
|
||||
result.aboutChats = utfLang(lng_export_about_chats);
|
||||
result.aboutLeftChats = utfLang(lng_export_about_left_chats);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SuggestStart() {
|
||||
Ui::show(Box<SuggestBox>(), LayerOption::KeepOther);
|
||||
}
|
||||
|
||||
void ClearSuggestStart() {
|
||||
auto settings = Local::ReadExportSettings();
|
||||
if (settings.availableAt) {
|
||||
settings.availableAt = 0;
|
||||
Local::WriteExportSettings(settings);
|
||||
}
|
||||
}
|
||||
|
||||
PanelController::PanelController(not_null<ControllerWrap*> process)
|
||||
: _process(process)
|
||||
, _settings(std::make_unique<Settings>(Local::ReadExportSettings()))
|
||||
, _saveSettingsTimer([=] { saveSettings(); }) {
|
||||
if (_settings->path.isEmpty()) {
|
||||
_settings->path = psDownloadPath();
|
||||
}
|
||||
|
||||
_process->state(
|
||||
) | rpl::start_with_next([=](State &&state) {
|
||||
updateState(std::move(state));
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void PanelController::activatePanel() {
|
||||
_panel->showAndActivate();
|
||||
}
|
||||
|
||||
void PanelController::createPanel() {
|
||||
_panel = base::make_unique_q<Ui::SeparatePanel>();
|
||||
_panel->setTitle(Lang::Viewer(lng_export_title));
|
||||
_panel->setInnerSize(st::exportPanelSize);
|
||||
_panel->closeRequests(
|
||||
) | rpl::start_with_next([=] {
|
||||
LOG(("Export Info: Panel Hide By Close."));
|
||||
_panel->hideGetDuration();
|
||||
}, _panel->lifetime());
|
||||
_panelCloseEvents.fire(_panel->closeEvents());
|
||||
|
||||
showSettings();
|
||||
}
|
||||
|
||||
void PanelController::showSettings() {
|
||||
auto settings = base::make_unique_q<SettingsWidget>(
|
||||
_panel,
|
||||
*_settings);
|
||||
|
||||
settings->startClicks(
|
||||
) | rpl::start_with_next([=]() {
|
||||
showProgress();
|
||||
_process->startExport(*_settings, PrepareEnvironment());
|
||||
}, settings->lifetime());
|
||||
|
||||
settings->cancelClicks(
|
||||
) | rpl::start_with_next([=] {
|
||||
LOG(("Export Info: Panel Hide By Cancel."));
|
||||
_panel->hideGetDuration();
|
||||
}, settings->lifetime());
|
||||
|
||||
settings->changes(
|
||||
) | rpl::start_with_next([=](Settings &&settings) {
|
||||
*_settings = std::move(settings);
|
||||
_saveSettingsTimer.callOnce(kSaveSettingsTimeout);
|
||||
}, settings->lifetime());
|
||||
|
||||
_panel->showInner(std::move(settings));
|
||||
}
|
||||
|
||||
void PanelController::showError(const ApiErrorState &error) {
|
||||
LOG(("Export Info: API Error '%1'.").arg(error.data.type()));
|
||||
|
||||
if (error.data.type() == qstr("TAKEOUT_INVALID")) {
|
||||
showError(lang(lng_export_invalid));
|
||||
} else if (error.data.type().startsWith(qstr("TAKEOUT_INIT_DELAY_"))) {
|
||||
const auto seconds = std::max(error.data.type().mid(
|
||||
qstr("TAKEOUT_INIT_DELAY_").size()).toInt(), 1);
|
||||
const auto now = QDateTime::currentDateTime();
|
||||
const auto when = now.addSecs(seconds);
|
||||
const auto hours = seconds / 3600;
|
||||
const auto hoursText = [&] {
|
||||
if (hours <= 0) {
|
||||
return lang(lng_export_delay_less_than_hour);
|
||||
}
|
||||
return lng_export_delay_hours(lt_count, hours);
|
||||
}();
|
||||
showError(lng_export_delay(
|
||||
lt_hours,
|
||||
hoursText,
|
||||
lt_date,
|
||||
langDateTimeFull(when)));
|
||||
|
||||
_settings->availableAt = unixtime() + seconds;
|
||||
_saveSettingsTimer.callOnce(kSaveSettingsTimeout);
|
||||
|
||||
Auth().data().suggestStartExport(_settings->availableAt);
|
||||
} else {
|
||||
showCriticalError("API Error happened :(\n"
|
||||
+ QString::number(error.data.code()) + ": " + error.data.type()
|
||||
+ "\n" + error.data.description());
|
||||
}
|
||||
}
|
||||
|
||||
void PanelController::showError(const OutputErrorState &error) {
|
||||
showCriticalError("Disk Error happened :(\n"
|
||||
"Could not write path:\n" + error.path);
|
||||
}
|
||||
|
||||
void PanelController::showCriticalError(const QString &text) {
|
||||
auto container = base::make_unique_q<Ui::PaddingWrap<Ui::FlatLabel>>(
|
||||
_panel.get(),
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_panel.get(),
|
||||
text,
|
||||
Ui::FlatLabel::InitType::Simple,
|
||||
st::exportErrorLabel),
|
||||
style::margins(0, st::exportPanelSize.height() / 4, 0, 0));
|
||||
container->widthValue(
|
||||
) | rpl::start_with_next([label = container->entity()](int width) {
|
||||
label->resize(width, label->height());
|
||||
}, container->lifetime());
|
||||
|
||||
_panel->showInner(std::move(container));
|
||||
_panel->setHideOnDeactivate(false);
|
||||
}
|
||||
|
||||
void PanelController::showError(const QString &text) {
|
||||
auto box = Box<InformBox>(text);
|
||||
const auto weak = make_weak(box.data());
|
||||
const auto hidden = _panel->isHidden();
|
||||
_panel->showBox(
|
||||
std::move(box),
|
||||
LayerOption::CloseOther,
|
||||
hidden ? anim::type::instant : anim::type::normal);
|
||||
weak->setCloseByEscape(false);
|
||||
weak->setCloseByOutsideClick(false);
|
||||
weak->boxClosing(
|
||||
) | rpl::start_with_next([=] {
|
||||
LOG(("Export Info: Panel Hide By Error: %1.").arg(text));
|
||||
_panel->hideGetDuration();
|
||||
}, weak->lifetime());
|
||||
if (hidden) {
|
||||
_panel->showAndActivate();
|
||||
}
|
||||
_panel->setHideOnDeactivate(false);
|
||||
}
|
||||
|
||||
void PanelController::showProgress() {
|
||||
_settings->availableAt = 0;
|
||||
ClearSuggestStart();
|
||||
|
||||
_panel->setTitle(Lang::Viewer(lng_export_progress_title));
|
||||
|
||||
auto progress = base::make_unique_q<ProgressWidget>(
|
||||
_panel.get(),
|
||||
rpl::single(
|
||||
ContentFromState(ProcessingState())
|
||||
) | rpl::then(progressState()));
|
||||
|
||||
progress->cancelClicks(
|
||||
) | rpl::start_with_next([=] {
|
||||
stopWithConfirmation();
|
||||
}, progress->lifetime());
|
||||
|
||||
progress->doneClicks(
|
||||
) | rpl::start_with_next([=] {
|
||||
if (const auto finished = base::get_if<FinishedState>(&_state)) {
|
||||
File::ShowInFolder(finished->path);
|
||||
LOG(("Export Info: Panel Hide By Done: %1."
|
||||
).arg(finished->path));
|
||||
_panel->hideGetDuration();
|
||||
}
|
||||
}, progress->lifetime());
|
||||
|
||||
_panel->showInner(std::move(progress));
|
||||
_panel->setHideOnDeactivate(true);
|
||||
}
|
||||
|
||||
void PanelController::stopWithConfirmation(FnMut<void()> callback) {
|
||||
if (!_state.is<ProcessingState>()) {
|
||||
LOG(("Export Info: Stop Panel Without Confirmation."));
|
||||
stopExport();
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
auto stop = [=, callback = std::move(callback)]() mutable {
|
||||
if (auto saved = std::move(callback)) {
|
||||
LOG(("Export Info: Stop Panel With Confirmation."));
|
||||
stopExport();
|
||||
saved();
|
||||
} else {
|
||||
_process->cancelExportFast();
|
||||
}
|
||||
};
|
||||
const auto hidden = _panel->isHidden();
|
||||
const auto old = _confirmStopBox;
|
||||
auto box = Box<ConfirmBox>(
|
||||
lang(lng_export_sure_stop),
|
||||
lang(lng_export_stop),
|
||||
st::attentionBoxButton,
|
||||
std::move(stop));
|
||||
_confirmStopBox = box.data();
|
||||
_panel->showBox(
|
||||
std::move(box),
|
||||
LayerOption::CloseOther,
|
||||
hidden ? anim::type::instant : anim::type::normal);
|
||||
if (hidden) {
|
||||
_panel->showAndActivate();
|
||||
}
|
||||
if (old) {
|
||||
old->closeBox();
|
||||
}
|
||||
}
|
||||
|
||||
void PanelController::stopExport() {
|
||||
_stopRequested = true;
|
||||
_panel->showAndActivate();
|
||||
LOG(("Export Info: Panel Hide By Stop"));
|
||||
_panel->hideGetDuration();
|
||||
}
|
||||
|
||||
rpl::producer<> PanelController::stopRequests() const {
|
||||
return _panelCloseEvents.events(
|
||||
) | rpl::flatten_latest(
|
||||
) | rpl::filter([=] {
|
||||
return !_state.is<ProcessingState>() || _stopRequested;
|
||||
});
|
||||
}
|
||||
|
||||
void PanelController::updateState(State &&state) {
|
||||
if (!_panel) {
|
||||
createPanel();
|
||||
}
|
||||
_state = std::move(state);
|
||||
if (const auto apiError = base::get_if<ApiErrorState>(&_state)) {
|
||||
showError(*apiError);
|
||||
} else if (const auto error = base::get_if<OutputErrorState>(&_state)) {
|
||||
showError(*error);
|
||||
} else if (_state.is<FinishedState>()) {
|
||||
_panel->setTitle(Lang::Viewer(lng_export_title));
|
||||
_panel->setHideOnDeactivate(false);
|
||||
} else if (_state.is<CancelledState>()) {
|
||||
LOG(("Export Info: Stop Panel After Cancel."));
|
||||
stopExport();
|
||||
}
|
||||
}
|
||||
|
||||
void PanelController::saveSettings() const {
|
||||
const auto check = [](const QString &value) {
|
||||
const auto result = value.endsWith('/')
|
||||
? value.mid(0, value.size() - 1)
|
||||
: value;
|
||||
return (cPlatform() == dbipWindows) ? result.toLower() : result;
|
||||
};
|
||||
auto settings = *_settings;
|
||||
if (check(settings.path) == check(psDownloadPath())) {
|
||||
settings.path = QString();
|
||||
}
|
||||
Local::WriteExportSettings(settings);
|
||||
}
|
||||
|
||||
PanelController::~PanelController() {
|
||||
if (_saveSettingsTimer.isActive()) {
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace View
|
||||
} // namespace Export
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "export/export_controller.h"
|
||||
#include "export/view/export_view_content.h"
|
||||
#include "base/unique_qptr.h"
|
||||
#include "base/timer.h"
|
||||
|
||||
class BoxContent;
|
||||
|
||||
namespace Ui {
|
||||
class SeparatePanel;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Export {
|
||||
namespace View {
|
||||
|
||||
void SuggestStart();
|
||||
void ClearSuggestStart();
|
||||
|
||||
class Panel;
|
||||
|
||||
class PanelController {
|
||||
public:
|
||||
PanelController(not_null<ControllerWrap*> process);
|
||||
|
||||
void activatePanel();
|
||||
void stopWithConfirmation(FnMut<void()> callback = nullptr);
|
||||
|
||||
rpl::producer<> stopRequests() const;
|
||||
|
||||
rpl::lifetime &lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
auto progressState() const {
|
||||
return ContentFromState(_process->state());
|
||||
}
|
||||
|
||||
~PanelController();
|
||||
|
||||
private:
|
||||
void stopExport();
|
||||
void createPanel();
|
||||
void updateState(State &&state);
|
||||
void showSettings();
|
||||
void showProgress();
|
||||
void showError(const ApiErrorState &error);
|
||||
void showError(const OutputErrorState &error);
|
||||
void showError(const QString &text);
|
||||
void showCriticalError(const QString &text);
|
||||
|
||||
void saveSettings() const;
|
||||
|
||||
not_null<ControllerWrap*> _process;
|
||||
std::unique_ptr<Settings> _settings;
|
||||
base::Timer _saveSettingsTimer;
|
||||
|
||||
base::unique_qptr<Ui::SeparatePanel> _panel;
|
||||
|
||||
State _state;
|
||||
QPointer<BoxContent> _confirmStopBox;
|
||||
rpl::event_stream<rpl::producer<>> _panelCloseEvents;
|
||||
bool _stopRequested = false;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace View
|
||||
} // namespace Export
|
||||
324
Telegram/SourceFiles/export/view/export_view_progress.cpp
Normal file
324
Telegram/SourceFiles/export/view/export_view_progress.cpp
Normal file
@@ -0,0 +1,324 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "export/view/export_view_progress.h"
|
||||
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_export.h"
|
||||
|
||||
namespace Export {
|
||||
namespace View {
|
||||
|
||||
class ProgressWidget::Row : public Ui::RpWidget {
|
||||
public:
|
||||
Row(QWidget *parent, Content::Row &&data);
|
||||
|
||||
void updateData(Content::Row &&data);
|
||||
|
||||
protected:
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
struct Instance {
|
||||
base::unique_qptr<Ui::FadeWrap<Ui::FlatLabel>> label;
|
||||
base::unique_qptr<Ui::FadeWrap<Ui::FlatLabel>> info;
|
||||
|
||||
float64 value = 0.;
|
||||
Animation progress;
|
||||
|
||||
bool hiding = true;
|
||||
Animation opacity;
|
||||
};
|
||||
|
||||
void fillCurrentInstance();
|
||||
void hideCurrentInstance();
|
||||
void setInstanceProgress(Instance &instance, float64 progress);
|
||||
void toggleInstance(Instance &data, bool shown);
|
||||
void instanceOpacityCallback(QPointer<Ui::FlatLabel> label);
|
||||
void removeOldInstance(QPointer<Ui::FlatLabel> label);
|
||||
void paintInstance(Painter &p, const Instance &data);
|
||||
|
||||
void updateControlsGeometry(int newWidth);
|
||||
void updateInstanceGeometry(const Instance &instance, int newWidth);
|
||||
|
||||
Content::Row _data;
|
||||
Instance _current;
|
||||
std::vector<Instance> _old;
|
||||
|
||||
};
|
||||
|
||||
ProgressWidget::Row::Row(QWidget *parent, Content::Row &&data)
|
||||
: RpWidget(parent)
|
||||
, _data(std::move(data)) {
|
||||
fillCurrentInstance();
|
||||
}
|
||||
|
||||
void ProgressWidget::Row::updateData(Content::Row &&data) {
|
||||
const auto wasId = _data.id;
|
||||
const auto nowId = data.id;
|
||||
_data = std::move(data);
|
||||
if (nowId.isEmpty()) {
|
||||
hideCurrentInstance();
|
||||
} else if (wasId.isEmpty()) {
|
||||
fillCurrentInstance();
|
||||
} else {
|
||||
_current.label->entity()->setText(_data.label);
|
||||
_current.info->entity()->setText(_data.info);
|
||||
setInstanceProgress(_current, _data.progress);
|
||||
if (nowId != wasId) {
|
||||
_current.progress.finish();
|
||||
}
|
||||
}
|
||||
updateControlsGeometry(width());
|
||||
update();
|
||||
}
|
||||
|
||||
void ProgressWidget::Row::fillCurrentInstance() {
|
||||
_current.label = base::make_unique_q<Ui::FadeWrap<Ui::FlatLabel>>(
|
||||
this,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
this,
|
||||
_data.label,
|
||||
Ui::FlatLabel::InitType::Simple,
|
||||
st::exportProgressLabel));
|
||||
_current.info = base::make_unique_q<Ui::FadeWrap<Ui::FlatLabel>>(
|
||||
this,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
this,
|
||||
_data.info,
|
||||
Ui::FlatLabel::InitType::Simple,
|
||||
st::exportProgressInfoLabel));
|
||||
_current.label->hide(anim::type::instant);
|
||||
_current.info->hide(anim::type::instant);
|
||||
|
||||
setInstanceProgress(_current, _data.progress);
|
||||
toggleInstance(_current, true);
|
||||
if (_data.id == "main") {
|
||||
_current.opacity.finish();
|
||||
_current.label->finishAnimating();
|
||||
_current.info->finishAnimating();
|
||||
}
|
||||
}
|
||||
|
||||
void ProgressWidget::Row::hideCurrentInstance() {
|
||||
if (!_current.label) {
|
||||
return;
|
||||
}
|
||||
setInstanceProgress(_current, 1.);
|
||||
toggleInstance(_current, false);
|
||||
_old.push_back(std::move(_current));
|
||||
}
|
||||
|
||||
void ProgressWidget::Row::setInstanceProgress(
|
||||
Instance &instance,
|
||||
float64 progress) {
|
||||
if (_current.value < progress) {
|
||||
_current.progress.start(
|
||||
[=] { update(); },
|
||||
_current.value,
|
||||
progress,
|
||||
st::exportProgressDuration,
|
||||
anim::sineInOut);
|
||||
} else if (_current.value > progress) {
|
||||
_current.progress.finish();
|
||||
}
|
||||
_current.value = progress;
|
||||
}
|
||||
|
||||
void ProgressWidget::Row::toggleInstance(Instance &instance, bool shown) {
|
||||
Expects(instance.label != nullptr);
|
||||
|
||||
if (instance.hiding != shown) {
|
||||
return;
|
||||
}
|
||||
const auto label = make_weak(instance.label->entity());
|
||||
instance.opacity.start(
|
||||
[=] { instanceOpacityCallback(label); },
|
||||
shown ? 0. : 1.,
|
||||
shown ? 1. : 0.,
|
||||
st::exportProgressDuration);
|
||||
instance.hiding = !shown;
|
||||
_current.label->toggle(shown, anim::type::normal);
|
||||
_current.info->toggle(shown, anim::type::normal);
|
||||
}
|
||||
|
||||
void ProgressWidget::Row::instanceOpacityCallback(
|
||||
QPointer<Ui::FlatLabel> label) {
|
||||
update();
|
||||
const auto i = ranges::find(_old, label, [](const Instance &instance) {
|
||||
return make_weak(instance.label->entity());
|
||||
});
|
||||
if (i != end(_old) && i->hiding && !i->opacity.animating()) {
|
||||
crl::on_main(this, [=] {
|
||||
removeOldInstance(label);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ProgressWidget::Row::removeOldInstance(QPointer<Ui::FlatLabel> label) {
|
||||
const auto i = ranges::find(_old, label, [](const Instance &instance) {
|
||||
return make_weak(instance.label->entity());
|
||||
});
|
||||
if (i != end(_old)) {
|
||||
_old.erase(i);
|
||||
}
|
||||
}
|
||||
|
||||
int ProgressWidget::Row::resizeGetHeight(int newWidth) {
|
||||
updateControlsGeometry(newWidth);
|
||||
return st::exportProgressRowHeight;
|
||||
}
|
||||
|
||||
void ProgressWidget::Row::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
|
||||
const auto thickness = st::exportProgressWidth;
|
||||
const auto top = height() - thickness;
|
||||
p.fillRect(0, top, width(), thickness, st::shadowFg);
|
||||
|
||||
for (const auto &instance : _old) {
|
||||
paintInstance(p, instance);
|
||||
}
|
||||
paintInstance(p, _current);
|
||||
}
|
||||
|
||||
void ProgressWidget::Row::paintInstance(Painter &p, const Instance &data) {
|
||||
const auto opacity = data.opacity.current(data.hiding ? 0. : 1.);
|
||||
|
||||
if (!opacity) {
|
||||
return;
|
||||
}
|
||||
p.setOpacity(opacity);
|
||||
|
||||
const auto thickness = st::exportProgressWidth;
|
||||
const auto top = height() - thickness;
|
||||
const auto till = qRound(data.progress.current(data.value) * width());
|
||||
if (till > 0) {
|
||||
p.fillRect(0, top, till, thickness, st::exportProgressFg);
|
||||
}
|
||||
if (till < width()) {
|
||||
const auto left = width() - till;
|
||||
p.fillRect(till, top, left, thickness, st::exportProgressBg);
|
||||
}
|
||||
}
|
||||
|
||||
void ProgressWidget::Row::updateControlsGeometry(int newWidth) {
|
||||
updateInstanceGeometry(_current, newWidth);
|
||||
for (const auto &instance : _old) {
|
||||
updateInstanceGeometry(instance, newWidth);
|
||||
}
|
||||
}
|
||||
|
||||
void ProgressWidget::Row::updateInstanceGeometry(
|
||||
const Instance &instance,
|
||||
int newWidth) {
|
||||
if (!instance.label) {
|
||||
return;
|
||||
}
|
||||
instance.info->resizeToNaturalWidth(newWidth);
|
||||
instance.label->resizeToWidth(newWidth - instance.info->width());
|
||||
instance.info->moveToRight(0, 0, newWidth);
|
||||
instance.label->moveToLeft(0, 0, newWidth);
|
||||
}
|
||||
|
||||
ProgressWidget::ProgressWidget(
|
||||
QWidget *parent,
|
||||
rpl::producer<Content> content)
|
||||
: RpWidget(parent)
|
||||
, _body(this) {
|
||||
widthValue(
|
||||
) | rpl::start_with_next([=](int width) {
|
||||
_body->resizeToWidth(width);
|
||||
_body->moveToLeft(0, 0);
|
||||
}, _body->lifetime());
|
||||
|
||||
_about = _body->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
this,
|
||||
lang(lng_export_progress),
|
||||
Ui::FlatLabel::InitType::Simple,
|
||||
st::exportAboutLabel),
|
||||
st::exportAboutPadding);
|
||||
|
||||
std::move(
|
||||
content
|
||||
) | rpl::start_with_next([=](Content &&content) {
|
||||
updateState(std::move(content));
|
||||
}, lifetime());
|
||||
|
||||
_cancel = base::make_unique_q<Ui::RoundButton>(
|
||||
this,
|
||||
langFactory(lng_export_stop),
|
||||
st::exportCancelButton);
|
||||
setupBottomButton(_cancel.get());
|
||||
}
|
||||
|
||||
rpl::producer<> ProgressWidget::cancelClicks() const {
|
||||
return _cancel
|
||||
? _cancel->clicks()
|
||||
: (rpl::never<>() | rpl::type_erased());
|
||||
}
|
||||
|
||||
rpl::producer<> ProgressWidget::doneClicks() const {
|
||||
return _doneClicks.events();
|
||||
}
|
||||
|
||||
void ProgressWidget::setupBottomButton(not_null<Ui::RoundButton*> button) {
|
||||
button->show();
|
||||
|
||||
sizeValue(
|
||||
) | rpl::start_with_next([=](QSize size) {
|
||||
button->move(
|
||||
(size.width() - button->width()) / 2,
|
||||
(size.height() - st::exportCancelBottom - button->height()));
|
||||
}, button->lifetime());
|
||||
}
|
||||
|
||||
void ProgressWidget::updateState(Content &&content) {
|
||||
if (!content.rows.empty() && content.rows[0].id == Content::kDoneId) {
|
||||
showDone();
|
||||
}
|
||||
|
||||
auto index = 0;
|
||||
for (auto &row : content.rows) {
|
||||
if (index < _rows.size()) {
|
||||
_rows[index]->updateData(std::move(row));
|
||||
} else {
|
||||
_rows.push_back(_body->insert(
|
||||
index,
|
||||
object_ptr<Row>(this, std::move(row)),
|
||||
st::exportProgressRowPadding));
|
||||
}
|
||||
++index;
|
||||
}
|
||||
for (const auto count = _rows.size(); index != count; ++index) {
|
||||
_rows[index]->updateData(Content::Row());
|
||||
}
|
||||
}
|
||||
|
||||
void ProgressWidget::showDone() {
|
||||
_cancel = nullptr;
|
||||
_about->setText(lang(lng_export_about_done));
|
||||
_done = base::make_unique_q<Ui::RoundButton>(
|
||||
this,
|
||||
langFactory(lng_export_done),
|
||||
st::exportDoneButton);
|
||||
_done->clicks() | rpl::start_to_stream(_doneClicks, _done->lifetime());
|
||||
setupBottomButton(_done.get());
|
||||
}
|
||||
|
||||
ProgressWidget::~ProgressWidget() = default;
|
||||
|
||||
} // namespace View
|
||||
} // namespace Export
|
||||
52
Telegram/SourceFiles/export/view/export_view_progress.h
Normal file
52
Telegram/SourceFiles/export/view/export_view_progress.h
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "export/view/export_view_content.h"
|
||||
|
||||
namespace Ui {
|
||||
class VerticalLayout;
|
||||
class RoundButton;
|
||||
class FlatLabel;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Export {
|
||||
namespace View {
|
||||
|
||||
class ProgressWidget : public Ui::RpWidget {
|
||||
public:
|
||||
ProgressWidget(
|
||||
QWidget *parent,
|
||||
rpl::producer<Content> content);
|
||||
|
||||
rpl::producer<> cancelClicks() const;
|
||||
rpl::producer<> doneClicks() const;
|
||||
|
||||
~ProgressWidget();
|
||||
|
||||
private:
|
||||
void setupBottomButton(not_null<Ui::RoundButton*> button);
|
||||
void updateState(Content &&content);
|
||||
void showDone();
|
||||
|
||||
Content _content;
|
||||
|
||||
class Row;
|
||||
object_ptr<Ui::VerticalLayout> _body;
|
||||
std::vector<not_null<Row*>> _rows;
|
||||
|
||||
QPointer<Ui::FlatLabel> _about;
|
||||
base::unique_qptr<Ui::RoundButton> _cancel;
|
||||
base::unique_qptr<Ui::RoundButton> _done;
|
||||
rpl::event_stream<> _doneClicks;
|
||||
|
||||
};
|
||||
|
||||
} // namespace View
|
||||
} // namespace Export
|
||||
544
Telegram/SourceFiles/export/view/export_view_settings.cpp
Normal file
544
Telegram/SourceFiles/export/view/export_view_settings.cpp
Normal file
@@ -0,0 +1,544 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "export/view/export_view_settings.h"
|
||||
|
||||
#include "export/output/export_output_abstract.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/widgets/continuous_sliders.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "platform/platform_specific.h"
|
||||
#include "core/file_utilities.h"
|
||||
#include "styles/style_widgets.h"
|
||||
#include "styles/style_export.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
namespace Export {
|
||||
namespace View {
|
||||
namespace {
|
||||
|
||||
constexpr auto kSizeValueCount = 80;
|
||||
constexpr auto kMegabyte = 1024 * 1024;
|
||||
|
||||
int SizeLimitByIndex(int index) {
|
||||
Expects(index >= 0 && index <= kSizeValueCount);
|
||||
|
||||
const auto megabytes = [&] {
|
||||
if (index <= 10) {
|
||||
return index;
|
||||
} else if (index <= 30) {
|
||||
return 10 + (index - 10) * 2;
|
||||
} else if (index <= 40) {
|
||||
return 50 + (index - 30) * 5;
|
||||
} else if (index <= 60) {
|
||||
return 100 + (index - 40) * 10;
|
||||
} else if (index <= 70) {
|
||||
return 300 + (index - 60) * 20;
|
||||
} else {
|
||||
return 500 + (index - 70) * 100;
|
||||
}
|
||||
};
|
||||
if (!index) {
|
||||
return kMegabyte / 2;
|
||||
}
|
||||
return megabytes() * kMegabyte;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SettingsWidget::SettingsWidget(QWidget *parent, Settings data)
|
||||
: RpWidget(parent)
|
||||
, _internal_data(std::move(data)) {
|
||||
setupContent();
|
||||
}
|
||||
|
||||
const Settings &SettingsWidget::readData() const {
|
||||
return _internal_data;
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
void SettingsWidget::changeData(Callback &&callback) {
|
||||
callback(_internal_data);
|
||||
_changes.fire_copy(_internal_data);
|
||||
}
|
||||
|
||||
void SettingsWidget::setupContent() {
|
||||
const auto scroll = Ui::CreateChild<Ui::ScrollArea>(
|
||||
this,
|
||||
st::boxLayerScroll);
|
||||
const auto wrap = scroll->setOwnedWidget(object_ptr<Ui::IgnoreMargins>(
|
||||
scroll,
|
||||
object_ptr<Ui::VerticalLayout>(scroll)));
|
||||
const auto content = static_cast<Ui::VerticalLayout*>(wrap->entity());
|
||||
|
||||
const auto buttons = setupButtons(scroll, wrap);
|
||||
setupOptions(content);
|
||||
setupPathAndFormat(content);
|
||||
|
||||
sizeValue(
|
||||
) | rpl::start_with_next([=](QSize size) {
|
||||
scroll->resize(size.width(), size.height() - buttons->height());
|
||||
wrap->resizeToWidth(size.width());
|
||||
content->resizeToWidth(size.width());
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void SettingsWidget::setupOptions(not_null<Ui::VerticalLayout*> container) {
|
||||
addOptionWithAbout(
|
||||
container,
|
||||
lng_export_option_info,
|
||||
Type::PersonalInfo | Type::Userpics,
|
||||
lng_export_option_info_about);
|
||||
addOptionWithAbout(
|
||||
container,
|
||||
lng_export_option_contacts,
|
||||
Type::Contacts,
|
||||
lng_export_option_contacts_about);
|
||||
addHeader(container, lng_export_header_chats);
|
||||
addOption(
|
||||
container,
|
||||
lng_export_option_personal_chats,
|
||||
Type::PersonalChats);
|
||||
addOption(container, lng_export_option_bot_chats, Type::BotChats);
|
||||
addChatOption(
|
||||
container,
|
||||
lng_export_option_private_groups,
|
||||
Type::PrivateGroups);
|
||||
addChatOption(
|
||||
container,
|
||||
lng_export_option_private_channels,
|
||||
Type::PrivateChannels);
|
||||
addChatOption(
|
||||
container,
|
||||
lng_export_option_public_groups,
|
||||
Type::PublicGroups);
|
||||
addChatOption(
|
||||
container,
|
||||
lng_export_option_public_channels,
|
||||
Type::PublicChannels);
|
||||
|
||||
setupMediaOptions(container);
|
||||
|
||||
addHeader(container, lng_export_header_other);
|
||||
addOptionWithAbout(
|
||||
container,
|
||||
lng_export_option_sessions,
|
||||
Type::Sessions,
|
||||
lng_export_option_sessions_about);
|
||||
addOptionWithAbout(
|
||||
container,
|
||||
lng_export_option_other,
|
||||
Type::OtherData,
|
||||
lng_export_option_other_about);
|
||||
}
|
||||
|
||||
void SettingsWidget::setupMediaOptions(
|
||||
not_null<Ui::VerticalLayout*> container) {
|
||||
const auto mediaWrap = container->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
container,
|
||||
object_ptr<Ui::VerticalLayout>(container)));
|
||||
const auto media = mediaWrap->entity();
|
||||
addHeader(media, lng_export_header_media);
|
||||
addMediaOption(media, lng_export_option_photos, MediaType::Photo);
|
||||
addMediaOption(media, lng_export_option_video_files, MediaType::Video);
|
||||
addMediaOption(media, lng_export_option_voice_messages, MediaType::VoiceMessage);
|
||||
addMediaOption(media, lng_export_option_video_messages, MediaType::VideoMessage);
|
||||
addMediaOption(media, lng_export_option_stickers, MediaType::Sticker);
|
||||
addMediaOption(media, lng_export_option_gifs, MediaType::GIF);
|
||||
addMediaOption(media, lng_export_option_files, MediaType::File);
|
||||
addSizeSlider(media);
|
||||
|
||||
value() | rpl::map([](const Settings &data) {
|
||||
return data.types;
|
||||
}) | rpl::distinct_until_changed(
|
||||
) | rpl::start_with_next([=](Settings::Types types) {
|
||||
mediaWrap->toggle((types & (Type::PersonalChats
|
||||
| Type::BotChats
|
||||
| Type::PrivateGroups
|
||||
| Type::PrivateChannels
|
||||
| Type::PublicGroups
|
||||
| Type::PublicChannels)) != 0, anim::type::normal);
|
||||
}, mediaWrap->lifetime());
|
||||
|
||||
widthValue(
|
||||
) | rpl::start_with_next([=](int width) {
|
||||
mediaWrap->resizeToWidth(width);
|
||||
}, mediaWrap->lifetime());
|
||||
}
|
||||
|
||||
void SettingsWidget::setupPathAndFormat(
|
||||
not_null<Ui::VerticalLayout*> container) {
|
||||
const auto formatGroup = std::make_shared<Ui::RadioenumGroup<Format>>(
|
||||
readData().format);
|
||||
formatGroup->setChangedCallback([=](Format format) {
|
||||
changeData([&](Settings &data) {
|
||||
data.format = format;
|
||||
});
|
||||
});
|
||||
const auto addFormatOption = [&](LangKey key, Format format) {
|
||||
const auto radio = container->add(
|
||||
object_ptr<Ui::Radioenum<Format>>(
|
||||
container,
|
||||
formatGroup,
|
||||
format,
|
||||
lang(key),
|
||||
st::defaultBoxCheckbox),
|
||||
st::exportSettingPadding);
|
||||
};
|
||||
addHeader(container, lng_export_header_format);
|
||||
addLocationLabel(container);
|
||||
addFormatOption(lng_export_option_html, Format::Html);
|
||||
addFormatOption(lng_export_option_json, Format::Json);
|
||||
}
|
||||
|
||||
void SettingsWidget::addLocationLabel(
|
||||
not_null<Ui::VerticalLayout*> container) {
|
||||
auto pathLabel = value() | rpl::map([](const Settings &data) {
|
||||
return data.path;
|
||||
}) | rpl::distinct_until_changed(
|
||||
) | rpl::map([](const QString &path) {
|
||||
const auto check = [](const QString &value) {
|
||||
const auto result = value.endsWith('/')
|
||||
? value.mid(0, value.size() - 1)
|
||||
: value;
|
||||
return (cPlatform() == dbipWindows) ? result.toLower() : result;
|
||||
};
|
||||
const auto text = (check(path) == check(psDownloadPath()))
|
||||
? QString("Downloads/Telegram Desktop")
|
||||
: path;
|
||||
auto pathLink = TextWithEntities{
|
||||
QDir::toNativeSeparators(text),
|
||||
EntitiesInText()
|
||||
};
|
||||
pathLink.entities.push_back(EntityInText(
|
||||
EntityInTextCustomUrl,
|
||||
0,
|
||||
text.size(),
|
||||
QString("internal:edit_export_path")));
|
||||
return lng_export_option_location__generic<TextWithEntities>(
|
||||
lt_path,
|
||||
pathLink);
|
||||
});
|
||||
const auto label = container->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
std::move(pathLabel),
|
||||
st::exportLocationLabel),
|
||||
st::exportLocationPadding);
|
||||
label->setClickHandlerFilter([=](auto&&...) {
|
||||
chooseFolder();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
not_null<Ui::RpWidget*> SettingsWidget::setupButtons(
|
||||
not_null<Ui::ScrollArea*> scroll,
|
||||
not_null<Ui::RpWidget*> wrap) {
|
||||
using namespace rpl::mappers;
|
||||
|
||||
const auto buttonsPadding = st::boxButtonPadding;
|
||||
const auto buttonsHeight = buttonsPadding.top()
|
||||
+ st::defaultBoxButton.height
|
||||
+ buttonsPadding.bottom();
|
||||
const auto buttons = Ui::CreateChild<Ui::FixedHeightWidget>(
|
||||
this,
|
||||
buttonsHeight);
|
||||
const auto topShadow = Ui::CreateChild<Ui::FadeShadow>(this);
|
||||
const auto bottomShadow = Ui::CreateChild<Ui::FadeShadow>(this);
|
||||
topShadow->toggleOn(scroll->scrollTopValue(
|
||||
) | rpl::map(_1 > 0));
|
||||
bottomShadow->toggleOn(rpl::combine(
|
||||
scroll->heightValue(),
|
||||
scroll->scrollTopValue(),
|
||||
wrap->heightValue(),
|
||||
_2
|
||||
) | rpl::map([=](int top) {
|
||||
return top < scroll->scrollTopMax();
|
||||
}));
|
||||
|
||||
value() | rpl::map([](const Settings &data) {
|
||||
return data.types != Types(0);
|
||||
}) | rpl::distinct_until_changed(
|
||||
) | rpl::start_with_next([=](bool canStart) {
|
||||
refreshButtons(buttons, canStart);
|
||||
topShadow->raise();
|
||||
bottomShadow->raise();
|
||||
}, buttons->lifetime());
|
||||
|
||||
sizeValue(
|
||||
) | rpl::start_with_next([=](QSize size) {
|
||||
buttons->resizeToWidth(size.width());
|
||||
buttons->moveToLeft(0, size.height() - buttons->height());
|
||||
topShadow->resizeToWidth(size.width());
|
||||
topShadow->moveToLeft(0, 0);
|
||||
bottomShadow->resizeToWidth(size.width());
|
||||
bottomShadow->moveToLeft(0, buttons->y() - st::lineWidth);
|
||||
}, buttons->lifetime());
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
void SettingsWidget::addHeader(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
LangKey key) {
|
||||
container->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
lang(key),
|
||||
Ui::FlatLabel::InitType::Simple,
|
||||
st::exportHeaderLabel),
|
||||
st::exportHeaderPadding);
|
||||
}
|
||||
|
||||
not_null<Ui::Checkbox*> SettingsWidget::addOption(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
LangKey key,
|
||||
Types types) {
|
||||
const auto checkbox = container->add(
|
||||
object_ptr<Ui::Checkbox>(
|
||||
container,
|
||||
lang(key),
|
||||
((readData().types & types) == types),
|
||||
st::defaultBoxCheckbox),
|
||||
st::exportSettingPadding);
|
||||
base::ObservableViewer(
|
||||
checkbox->checkedChanged
|
||||
) | rpl::start_with_next([=](bool checked) {
|
||||
changeData([&](Settings &data) {
|
||||
if (checked) {
|
||||
data.types |= types;
|
||||
} else {
|
||||
data.types &= ~types;
|
||||
}
|
||||
});
|
||||
}, lifetime());
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
not_null<Ui::Checkbox*> SettingsWidget::addOptionWithAbout(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
LangKey key,
|
||||
Types types,
|
||||
LangKey about) {
|
||||
const auto result = addOption(container, key, types);
|
||||
const auto label = container->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
lang(about),
|
||||
Ui::FlatLabel::InitType::Simple,
|
||||
st::exportAboutOptionLabel),
|
||||
st::exportAboutOptionPadding);
|
||||
return result;
|
||||
}
|
||||
|
||||
void SettingsWidget::addChatOption(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
LangKey key,
|
||||
Types types) {
|
||||
const auto checkbox = addOption(container, key, types);
|
||||
const auto onlyMy = container->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::Checkbox>>(
|
||||
container,
|
||||
object_ptr<Ui::Checkbox>(
|
||||
container,
|
||||
lang(lng_export_option_only_my),
|
||||
((readData().fullChats & types) != types),
|
||||
st::defaultBoxCheckbox),
|
||||
st::exportSubSettingPadding));
|
||||
|
||||
base::ObservableViewer(
|
||||
onlyMy->entity()->checkedChanged
|
||||
) | rpl::start_with_next([=](bool checked) {
|
||||
changeData([&](Settings &data) {
|
||||
if (checked) {
|
||||
data.fullChats &= ~types;
|
||||
} else {
|
||||
data.fullChats |= types;
|
||||
}
|
||||
});
|
||||
}, checkbox->lifetime());
|
||||
|
||||
onlyMy->toggleOn(base::ObservableViewer(
|
||||
checkbox->checkedChanged
|
||||
));
|
||||
|
||||
onlyMy->toggle(checkbox->checked(), anim::type::instant);
|
||||
|
||||
if (types & (Type::PublicGroups | Type::PublicChannels)) {
|
||||
onlyMy->entity()->setChecked(true);
|
||||
onlyMy->entity()->setDisabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsWidget::addMediaOption(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
LangKey key,
|
||||
MediaType type) {
|
||||
const auto checkbox = container->add(
|
||||
object_ptr<Ui::Checkbox>(
|
||||
container,
|
||||
lang(key),
|
||||
((readData().media.types & type) == type),
|
||||
st::defaultBoxCheckbox),
|
||||
st::exportSettingPadding);
|
||||
base::ObservableViewer(
|
||||
checkbox->checkedChanged
|
||||
) | rpl::start_with_next([=](bool checked) {
|
||||
changeData([&](Settings &data) {
|
||||
if (checked) {
|
||||
data.media.types |= type;
|
||||
} else {
|
||||
data.media.types &= ~type;
|
||||
}
|
||||
});
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void SettingsWidget::addSizeSlider(
|
||||
not_null<Ui::VerticalLayout*> container) {
|
||||
using namespace rpl::mappers;
|
||||
|
||||
const auto slider = container->add(
|
||||
object_ptr<Ui::MediaSlider>(container, st::exportFileSizeSlider),
|
||||
st::exportFileSizePadding);
|
||||
slider->resize(st::exportFileSizeSlider.seekSize);
|
||||
slider->setAlwaysDisplayMarker(true);
|
||||
slider->setDirection(Ui::ContinuousSlider::Direction::Horizontal);
|
||||
for (auto i = 0; i != kSizeValueCount + 1; ++i) {
|
||||
if (readData().media.sizeLimit <= SizeLimitByIndex(i)) {
|
||||
slider->setValue(i / float64(kSizeValueCount));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const auto label = Ui::CreateChild<Ui::LabelSimple>(
|
||||
container.get(),
|
||||
st::exportFileSizeLabel);
|
||||
slider->setAdjustCallback([=](float64 value) {
|
||||
return std::round(value * kSizeValueCount) / kSizeValueCount;
|
||||
});
|
||||
slider->setChangeProgressCallback([=](float64 value) {
|
||||
const auto index = int(std::round(value * kSizeValueCount));
|
||||
changeData([&](Settings &data) {
|
||||
data.media.sizeLimit = SizeLimitByIndex(index);
|
||||
});
|
||||
});
|
||||
value() | rpl::map([](const Settings &data) {
|
||||
return data.media.sizeLimit;
|
||||
}) | rpl::start_with_next([=](int sizeLimit) {
|
||||
const auto limit = sizeLimit / kMegabyte;
|
||||
const auto size = ((limit > 0)
|
||||
? QString::number(limit)
|
||||
: QString::number(float64(sizeLimit) / kMegabyte))
|
||||
+ " MB";
|
||||
const auto text = lng_export_option_size_limit(lt_size, size);
|
||||
label->setText(text);
|
||||
}, slider->lifetime());
|
||||
|
||||
rpl::combine(
|
||||
label->widthValue(),
|
||||
slider->geometryValue(),
|
||||
_2
|
||||
) | rpl::start_with_next([=](QRect geometry) {
|
||||
label->moveToRight(
|
||||
st::exportFileSizePadding.right(),
|
||||
geometry.y() - label->height() - st::exportFileSizeLabelBottom);
|
||||
}, label->lifetime());
|
||||
|
||||
}
|
||||
|
||||
void SettingsWidget::refreshButtons(
|
||||
not_null<Ui::RpWidget*> container,
|
||||
bool canStart) {
|
||||
container->hideChildren();
|
||||
const auto children = container->children();
|
||||
for (const auto child : children) {
|
||||
if (child->isWidgetType()) {
|
||||
child->deleteLater();
|
||||
}
|
||||
}
|
||||
const auto start = canStart
|
||||
? Ui::CreateChild<Ui::RoundButton>(
|
||||
container.get(),
|
||||
langFactory(lng_export_start),
|
||||
st::defaultBoxButton)
|
||||
: nullptr;
|
||||
if (start) {
|
||||
start->show();
|
||||
_startClicks = start->clicks();
|
||||
|
||||
container->sizeValue(
|
||||
) | rpl::start_with_next([=](QSize size) {
|
||||
const auto right = st::boxButtonPadding.right();
|
||||
const auto top = st::boxButtonPadding.top();
|
||||
start->moveToRight(right, top);
|
||||
}, start->lifetime());
|
||||
}
|
||||
|
||||
const auto cancel = Ui::CreateChild<Ui::RoundButton>(
|
||||
container.get(),
|
||||
langFactory(lng_cancel),
|
||||
st::defaultBoxButton);
|
||||
cancel->show();
|
||||
_cancelClicks = cancel->clicks();
|
||||
|
||||
rpl::combine(
|
||||
container->sizeValue(),
|
||||
start ? start->widthValue() : rpl::single(0)
|
||||
) | rpl::start_with_next([=](QSize size, int width) {
|
||||
const auto right = st::boxButtonPadding.right()
|
||||
+ (width ? width + st::boxButtonPadding.left() : 0);
|
||||
const auto top = st::boxButtonPadding.top();
|
||||
cancel->moveToRight(right, top);
|
||||
}, cancel->lifetime());
|
||||
}
|
||||
|
||||
void SettingsWidget::chooseFolder() {
|
||||
const auto callback = [=](QString &&result) {
|
||||
changeData([&](Settings &data) {
|
||||
data.path = std::move(result);
|
||||
});
|
||||
};
|
||||
FileDialog::GetFolder(
|
||||
this,
|
||||
lang(lng_export_folder),
|
||||
readData().path,
|
||||
callback);
|
||||
}
|
||||
|
||||
rpl::producer<Settings> SettingsWidget::changes() const {
|
||||
return _changes.events();
|
||||
}
|
||||
|
||||
rpl::producer<Settings> SettingsWidget::value() const {
|
||||
return rpl::single(readData()) | rpl::then(changes());
|
||||
}
|
||||
|
||||
rpl::producer<> SettingsWidget::startClicks() const {
|
||||
return _startClicks.value(
|
||||
) | rpl::map([](Wrap &&wrap) {
|
||||
return std::move(wrap.value);
|
||||
}) | rpl::flatten_latest();
|
||||
}
|
||||
|
||||
rpl::producer<> SettingsWidget::cancelClicks() const {
|
||||
return _cancelClicks.value(
|
||||
) | rpl::map([](Wrap &&wrap) {
|
||||
return std::move(wrap.value);
|
||||
}) | rpl::flatten_latest();
|
||||
}
|
||||
|
||||
} // namespace View
|
||||
} // namespace Export
|
||||
96
Telegram/SourceFiles/export/view/export_view_settings.h
Normal file
96
Telegram/SourceFiles/export/view/export_view_settings.h
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "export/export_settings.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
enum LangKey : int;
|
||||
|
||||
namespace Ui {
|
||||
class VerticalLayout;
|
||||
class Checkbox;
|
||||
class ScrollArea;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Export {
|
||||
namespace View {
|
||||
|
||||
class SettingsWidget : public Ui::RpWidget {
|
||||
public:
|
||||
SettingsWidget(QWidget *parent, Settings data);
|
||||
|
||||
rpl::producer<Settings> value() const;
|
||||
rpl::producer<Settings> changes() const;
|
||||
rpl::producer<> startClicks() const;
|
||||
rpl::producer<> cancelClicks() const;
|
||||
|
||||
private:
|
||||
using Type = Settings::Type;
|
||||
using Types = Settings::Types;
|
||||
using MediaType = MediaSettings::Type;
|
||||
using MediaTypes = MediaSettings::Types;
|
||||
using Format = Output::Format;
|
||||
|
||||
void setupContent();
|
||||
not_null<Ui::RpWidget*> setupButtons(
|
||||
not_null<Ui::ScrollArea*> scroll,
|
||||
not_null<Ui::RpWidget*> wrap);
|
||||
void setupOptions(not_null<Ui::VerticalLayout*> container);
|
||||
void setupMediaOptions(not_null<Ui::VerticalLayout*> container);
|
||||
void setupPathAndFormat(not_null<Ui::VerticalLayout*> container);
|
||||
void addHeader(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
LangKey key);
|
||||
not_null<Ui::Checkbox*> addOption(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
LangKey key,
|
||||
Types types);
|
||||
not_null<Ui::Checkbox*> addOptionWithAbout(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
LangKey key,
|
||||
Types types,
|
||||
LangKey about);
|
||||
void addChatOption(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
LangKey key,
|
||||
Types types);
|
||||
void addMediaOption(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
LangKey key,
|
||||
MediaType type);
|
||||
void addSizeSlider(not_null<Ui::VerticalLayout*> container);
|
||||
void addLocationLabel(
|
||||
not_null<Ui::VerticalLayout*> container);
|
||||
void chooseFolder();
|
||||
void refreshButtons(
|
||||
not_null<Ui::RpWidget*> container,
|
||||
bool canStart);
|
||||
|
||||
const Settings &readData() const;
|
||||
template <typename Callback>
|
||||
void changeData(Callback &&callback);
|
||||
|
||||
// Use through readData / changeData wrappers.
|
||||
Settings _internal_data;
|
||||
|
||||
struct Wrap {
|
||||
Wrap(rpl::producer<> value = rpl::never<>())
|
||||
: value(std::move(value)) {
|
||||
}
|
||||
|
||||
rpl::producer<> value;
|
||||
};
|
||||
rpl::event_stream<Settings> _changes;
|
||||
rpl::variable<Wrap> _startClicks;
|
||||
rpl::variable<Wrap> _cancelClicks;
|
||||
|
||||
};
|
||||
|
||||
} // namespace View
|
||||
} // namespace Export
|
||||
91
Telegram/SourceFiles/export/view/export_view_top_bar.cpp
Normal file
91
Telegram/SourceFiles/export/view/export_view_top_bar.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "export/view/export_view_top_bar.h"
|
||||
|
||||
#include "export/view/export_view_content.h"
|
||||
#include "ui/widgets/continuous_sliders.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_export.h"
|
||||
#include "styles/style_media_player.h"
|
||||
|
||||
namespace Export {
|
||||
namespace View {
|
||||
|
||||
TopBar::TopBar(QWidget *parent, Content &&content)
|
||||
: RpWidget(parent)
|
||||
, _info(this, st::exportTopBarLabel)
|
||||
, _shadow(this)
|
||||
, _progress(this, st::mediaPlayerPlayback)
|
||||
, _button(this) {
|
||||
resize(width(), st::mediaPlayerHeight + st::lineWidth);
|
||||
_progress->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
updateData(std::move(content));
|
||||
}
|
||||
|
||||
rpl::producer<> TopBar::clicks() const {
|
||||
return _button->clicks();
|
||||
}
|
||||
|
||||
void TopBar::updateData(Content &&content) {
|
||||
if (content.rows.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto &row = content.rows[0];
|
||||
const auto clean = &TextUtilities::Clean;
|
||||
_info->setRichText(textcmdStartSemibold()
|
||||
+ clean(lang(lng_export_progress_title))
|
||||
+ textcmdStopSemibold()
|
||||
+ QString::fromUtf8(" \xe2\x80\x93 ")
|
||||
+ clean(row.label)
|
||||
+ ' '
|
||||
+ textcmdLink(1, clean(row.info)));
|
||||
_progress->setValue(row.progress);
|
||||
}
|
||||
|
||||
void TopBar::resizeEvent(QResizeEvent *e) {
|
||||
_info->moveToLeft(
|
||||
st::mediaPlayerPlayLeft + st::mediaPlayerPadding,
|
||||
st::mediaPlayerNameTop - st::mediaPlayerName.style.font->ascent);
|
||||
_button->setGeometry(0, 0, width(), height() - st::lineWidth);
|
||||
_progress->setGeometry(
|
||||
0,
|
||||
height() - st::mediaPlayerPlayback.fullWidth,
|
||||
width(),
|
||||
st::mediaPlayerPlayback.fullWidth);
|
||||
}
|
||||
|
||||
void TopBar::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
auto fill = e->rect().intersected(
|
||||
QRect(0, 0, width(), st::mediaPlayerHeight));
|
||||
if (!fill.isEmpty()) {
|
||||
p.fillRect(fill, st::mediaPlayerBg);
|
||||
}
|
||||
}
|
||||
|
||||
void TopBar::setShadowGeometryToLeft(int x, int y, int w, int h) {
|
||||
_shadow->setGeometryToLeft(x, y, w, h);
|
||||
}
|
||||
|
||||
void TopBar::showShadow() {
|
||||
_shadow->show();
|
||||
_progress->show();
|
||||
}
|
||||
|
||||
void TopBar::hideShadow() {
|
||||
_shadow->hide();
|
||||
_progress->hide();
|
||||
}
|
||||
|
||||
TopBar::~TopBar() = default;
|
||||
|
||||
} // namespace View
|
||||
} // namespace Export
|
||||
51
Telegram/SourceFiles/export/view/export_view_top_bar.h
Normal file
51
Telegram/SourceFiles/export/view/export_view_top_bar.h
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace Ui {
|
||||
class FlatLabel;
|
||||
class FilledSlider;
|
||||
class AbstractButton;
|
||||
class PlainShadow;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Export {
|
||||
namespace View {
|
||||
|
||||
struct Content;
|
||||
|
||||
class TopBar : public Ui::RpWidget {
|
||||
public:
|
||||
TopBar(QWidget *parent, Content &&content);
|
||||
|
||||
rpl::producer<> clicks() const;
|
||||
|
||||
void updateData(Content &&content);
|
||||
|
||||
void setShadowGeometryToLeft(int x, int y, int w, int h);
|
||||
void showShadow();
|
||||
void hideShadow();
|
||||
|
||||
~TopBar();
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
object_ptr<Ui::FlatLabel> _info;
|
||||
object_ptr<Ui::PlainShadow> _shadow = { nullptr };
|
||||
object_ptr<Ui::FilledSlider> _progress;
|
||||
object_ptr<Ui::AbstractButton> _button;
|
||||
|
||||
};
|
||||
|
||||
} // namespace View
|
||||
} // namespace Export
|
||||
@@ -51,6 +51,7 @@ constexpr auto kStatusShowClientsideChooseContact = 6000;
|
||||
constexpr auto kStatusShowClientsidePlayGame = 10000;
|
||||
constexpr auto kSetMyActionForMs = 10000;
|
||||
constexpr auto kNewBlockEachMessage = 50;
|
||||
constexpr auto kSkipCloudDraftsFor = TimeId(3);
|
||||
|
||||
void checkForSwitchInlineButton(HistoryItem *item) {
|
||||
if (item->out() || !item->hasSwitchInlineButton()) {
|
||||
@@ -98,8 +99,10 @@ not_null<History*> Histories::findOrInsert(PeerId peerId) {
|
||||
}
|
||||
|
||||
void Histories::clear() {
|
||||
for (const auto &[peerId, history] : _map) {
|
||||
history->unloadBlocks();
|
||||
}
|
||||
App::historyClearMsgs();
|
||||
|
||||
_map.clear();
|
||||
|
||||
_unreadFull = _unreadMuted = 0;
|
||||
@@ -405,6 +408,24 @@ Data::Draft *History::createCloudDraft(Data::Draft *fromDraft) {
|
||||
return cloudDraft();
|
||||
}
|
||||
|
||||
bool History::skipCloudDraft(const QString &text, TimeId date) const {
|
||||
if (_lastSentDraftText && *_lastSentDraftText == text) {
|
||||
return true;
|
||||
} else if (date <= _lastSentDraftTime + kSkipCloudDraftsFor) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void History::setSentDraftText(const QString &text) {
|
||||
_lastSentDraftText = text;
|
||||
}
|
||||
|
||||
void History::clearSentDraftText() {
|
||||
_lastSentDraftText = base::none;
|
||||
accumulate_max(_lastSentDraftTime, unixtime());
|
||||
}
|
||||
|
||||
void History::setEditDraft(std::unique_ptr<Data::Draft> &&draft) {
|
||||
_editDraft = std::move(draft);
|
||||
}
|
||||
|
||||
@@ -304,6 +304,9 @@ public:
|
||||
void createLocalDraftFromCloud();
|
||||
void setCloudDraft(std::unique_ptr<Data::Draft> &&draft);
|
||||
Data::Draft *createCloudDraft(Data::Draft *fromDraft);
|
||||
bool skipCloudDraft(const QString &text, TimeId date) const;
|
||||
void setSentDraftText(const QString &text);
|
||||
void clearSentDraftText();
|
||||
void setEditDraft(std::unique_ptr<Data::Draft> &&draft);
|
||||
void clearLocalDraft();
|
||||
void clearCloudDraft();
|
||||
@@ -501,6 +504,8 @@ private:
|
||||
|
||||
std::unique_ptr<Data::Draft> _localDraft, _cloudDraft;
|
||||
std::unique_ptr<Data::Draft> _editDraft;
|
||||
base::optional<QString> _lastSentDraftText;
|
||||
TimeId _lastSentDraftTime = 0;
|
||||
MessageIdsList _forwardDraft;
|
||||
|
||||
using TypingUsers = QMap<UserData*, TimeMs>;
|
||||
|
||||
@@ -97,6 +97,61 @@ std::unique_ptr<HistoryMedia> CreateAttach(
|
||||
|
||||
} // namespace
|
||||
|
||||
QString FillAmountAndCurrency(uint64 amount, const QString ¤cy) {
|
||||
static const auto ShortCurrencyNames = QMap<QString, QString> {
|
||||
{ qsl("USD"), QString::fromUtf8("\x24") },
|
||||
{ qsl("GBP"), QString::fromUtf8("\xC2\xA3") },
|
||||
{ qsl("EUR"), QString::fromUtf8("\xE2\x82\xAC") },
|
||||
{ qsl("JPY"), QString::fromUtf8("\xC2\xA5") },
|
||||
};
|
||||
static const auto Denominators = QMap<QString, int> {
|
||||
{ qsl("CLF"), 10000 },
|
||||
{ qsl("BHD"), 1000 },
|
||||
{ qsl("IQD"), 1000 },
|
||||
{ qsl("JOD"), 1000 },
|
||||
{ qsl("KWD"), 1000 },
|
||||
{ qsl("LYD"), 1000 },
|
||||
{ qsl("OMR"), 1000 },
|
||||
{ qsl("TND"), 1000 },
|
||||
{ qsl("BIF"), 1 },
|
||||
{ qsl("BYR"), 1 },
|
||||
{ qsl("CLP"), 1 },
|
||||
{ qsl("CVE"), 1 },
|
||||
{ qsl("DJF"), 1 },
|
||||
{ qsl("GNF"), 1 },
|
||||
{ qsl("ISK"), 1 },
|
||||
{ qsl("JPY"), 1 },
|
||||
{ qsl("KMF"), 1 },
|
||||
{ qsl("KRW"), 1 },
|
||||
{ qsl("MGA"), 1 },
|
||||
{ qsl("PYG"), 1 },
|
||||
{ qsl("RWF"), 1 },
|
||||
{ qsl("UGX"), 1 },
|
||||
{ qsl("UYI"), 1 },
|
||||
{ qsl("VND"), 1 },
|
||||
{ qsl("VUV"), 1 },
|
||||
{ qsl("XAF"), 1 },
|
||||
{ qsl("XOF"), 1 },
|
||||
{ qsl("XPF"), 1 },
|
||||
{ qsl("MRO"), 10 },
|
||||
};
|
||||
const auto currencyText = ShortCurrencyNames.value(currency, currency);
|
||||
const auto denominator = Denominators.value(currency, 100);
|
||||
const auto currencyValue = amount / float64(denominator);
|
||||
const auto digits = [&] {
|
||||
auto result = 0;
|
||||
for (auto test = 1; test < denominator; test *= 10) {
|
||||
++result;
|
||||
}
|
||||
return result;
|
||||
}();
|
||||
return QLocale::system().toCurrencyString(currencyValue, currencyText);
|
||||
//auto amountBucks = amount / 100;
|
||||
//auto amountCents = amount % 100;
|
||||
//auto amountText = qsl("%1,%2").arg(amountBucks).arg(amountCents, 2, 10, QChar('0'));
|
||||
//return currencyText + amountText;
|
||||
}
|
||||
|
||||
void HistoryFileMedia::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
|
||||
if (p == _savel || p == _cancell) {
|
||||
if (active && !dataLoaded()) {
|
||||
@@ -4259,63 +4314,6 @@ HistoryInvoice::HistoryInvoice(
|
||||
fillFromData(invoice);
|
||||
}
|
||||
|
||||
QString HistoryInvoice::fillAmountAndCurrency(
|
||||
uint64 amount,
|
||||
const QString ¤cy) {
|
||||
static const auto ShortCurrencyNames = QMap<QString, QString> {
|
||||
{ qsl("USD"), QString::fromUtf8("\x24") },
|
||||
{ qsl("GBP"), QString::fromUtf8("\xC2\xA3") },
|
||||
{ qsl("EUR"), QString::fromUtf8("\xE2\x82\xAC") },
|
||||
{ qsl("JPY"), QString::fromUtf8("\xC2\xA5") },
|
||||
};
|
||||
static const auto Denominators = QMap<QString, int> {
|
||||
{ qsl("CLF"), 10000 },
|
||||
{ qsl("BHD"), 1000 },
|
||||
{ qsl("IQD"), 1000 },
|
||||
{ qsl("JOD"), 1000 },
|
||||
{ qsl("KWD"), 1000 },
|
||||
{ qsl("LYD"), 1000 },
|
||||
{ qsl("OMR"), 1000 },
|
||||
{ qsl("TND"), 1000 },
|
||||
{ qsl("BIF"), 1 },
|
||||
{ qsl("BYR"), 1 },
|
||||
{ qsl("CLP"), 1 },
|
||||
{ qsl("CVE"), 1 },
|
||||
{ qsl("DJF"), 1 },
|
||||
{ qsl("GNF"), 1 },
|
||||
{ qsl("ISK"), 1 },
|
||||
{ qsl("JPY"), 1 },
|
||||
{ qsl("KMF"), 1 },
|
||||
{ qsl("KRW"), 1 },
|
||||
{ qsl("MGA"), 1 },
|
||||
{ qsl("PYG"), 1 },
|
||||
{ qsl("RWF"), 1 },
|
||||
{ qsl("UGX"), 1 },
|
||||
{ qsl("UYI"), 1 },
|
||||
{ qsl("VND"), 1 },
|
||||
{ qsl("VUV"), 1 },
|
||||
{ qsl("XAF"), 1 },
|
||||
{ qsl("XOF"), 1 },
|
||||
{ qsl("XPF"), 1 },
|
||||
{ qsl("MRO"), 10 },
|
||||
};
|
||||
const auto currencyText = ShortCurrencyNames.value(currency, currency);
|
||||
const auto denominator = Denominators.value(currency, 100);
|
||||
const auto currencyValue = amount / float64(denominator);
|
||||
const auto digits = [&] {
|
||||
auto result = 0;
|
||||
for (auto test = 1; test < denominator; test *= 10) {
|
||||
++result;
|
||||
}
|
||||
return result;
|
||||
}();
|
||||
return QLocale::system().toCurrencyString(currencyValue, currencyText);
|
||||
//auto amountBucks = amount / 100;
|
||||
//auto amountCents = amount % 100;
|
||||
//auto amountText = qsl("%1,%2").arg(amountBucks).arg(amountCents, 2, 10, QChar('0'));
|
||||
//return currencyText + amountText;
|
||||
}
|
||||
|
||||
void HistoryInvoice::fillFromData(not_null<Data::Invoice*> invoice) {
|
||||
// init attach
|
||||
auto labelText = [&] {
|
||||
@@ -4330,7 +4328,7 @@ void HistoryInvoice::fillFromData(not_null<Data::Invoice*> invoice) {
|
||||
return lang(lng_payments_invoice_label);
|
||||
};
|
||||
auto statusText = TextWithEntities {
|
||||
fillAmountAndCurrency(invoice->amount, invoice->currency),
|
||||
FillAmountAndCurrency(invoice->amount, invoice->currency),
|
||||
EntitiesInText()
|
||||
};
|
||||
statusText.entities.push_back(EntityInText(EntityInTextBold, 0, statusText.text.size()));
|
||||
|
||||
@@ -41,6 +41,8 @@ namespace Ui {
|
||||
class EmptyUserpic;
|
||||
} // namespace Ui
|
||||
|
||||
QString FillAmountAndCurrency(uint64 amount, const QString ¤cy);
|
||||
|
||||
class HistoryFileMedia : public HistoryMedia {
|
||||
public:
|
||||
using HistoryMedia::HistoryMedia;
|
||||
@@ -850,7 +852,6 @@ public:
|
||||
QString getTitle() const {
|
||||
return _title.originalText();
|
||||
}
|
||||
static QString fillAmountAndCurrency(uint64 amount, const QString ¤cy);
|
||||
|
||||
bool hideMessageText() const override {
|
||||
return false;
|
||||
|
||||
@@ -608,7 +608,7 @@ void HistoryService::createFromMtp(const MTPDmessageService &message) {
|
||||
UpdateComponents(HistoryServicePayment::Bit());
|
||||
auto amount = message.vaction.c_messageActionPaymentSent().vtotal_amount.v;
|
||||
auto currency = qs(message.vaction.c_messageActionPaymentSent().vcurrency);
|
||||
Get<HistoryServicePayment>()->amount = HistoryInvoice::fillAmountAndCurrency(amount, currency);
|
||||
Get<HistoryServicePayment>()->amount = FillAmountAndCurrency(amount, currency);
|
||||
}
|
||||
if (message.has_reply_to_msg_id()) {
|
||||
if (message.vaction.type() == mtpc_messageActionPinMessage) {
|
||||
|
||||
@@ -91,6 +91,9 @@ constexpr auto kShowMembersDropdownTimeoutMs = 300;
|
||||
constexpr auto kDisplayEditTimeWarningMs = 300 * 1000;
|
||||
constexpr auto kFullDayInMs = 86400 * 1000;
|
||||
constexpr auto kCancelTypingActionTimeout = TimeMs(5000);
|
||||
constexpr auto kSaveDraftTimeout = 1000;
|
||||
constexpr auto kSaveDraftAnywayTimeout = 5000;
|
||||
constexpr auto kSaveCloudDraftIdleTimeout = 14000;
|
||||
|
||||
ApiWrap::RequestMessageDataCallback replyEditMessageDataCallback() {
|
||||
return [](ChannelData *channel, MsgId msgId) {
|
||||
@@ -1192,9 +1195,9 @@ void HistoryWidget::onDraftSave(bool delayed) {
|
||||
auto ms = getms();
|
||||
if (!_saveDraftStart) {
|
||||
_saveDraftStart = ms;
|
||||
return _saveDraftTimer.start(SaveDraftTimeout);
|
||||
} else if (ms - _saveDraftStart < SaveDraftAnywayTimeout) {
|
||||
return _saveDraftTimer.start(SaveDraftTimeout);
|
||||
return _saveDraftTimer.start(kSaveDraftTimeout);
|
||||
} else if (ms - _saveDraftStart < kSaveDraftAnywayTimeout) {
|
||||
return _saveDraftTimer.start(kSaveDraftTimeout);
|
||||
}
|
||||
}
|
||||
writeDrafts(nullptr, nullptr);
|
||||
@@ -1275,7 +1278,7 @@ void HistoryWidget::writeDrafts(Data::Draft **localDraft, Data::Draft **editDraf
|
||||
}
|
||||
|
||||
if (!_editMsgId && !_inlineBot) {
|
||||
_saveCloudDraftTimer.start(SaveCloudDraftIdleTimeout);
|
||||
_saveCloudDraftTimer.start(kSaveCloudDraftIdleTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -494,8 +494,9 @@ void TopBarWidget::updateControlsGeometry() {
|
||||
buttonsWidth += buttonsLeft + st::topBarActionSkip * 3;
|
||||
|
||||
auto widthLeft = qMin(width() - buttonsWidth, -2 * st::defaultActiveButton.width);
|
||||
_forward->setFullWidth(-(widthLeft / 2));
|
||||
_delete->setFullWidth(-(widthLeft / 2));
|
||||
auto buttonFullWidth = qMin(-(widthLeft / 2), 0);
|
||||
_forward->setFullWidth(buttonFullWidth);
|
||||
_delete->setFullWidth(buttonFullWidth);
|
||||
|
||||
selectedButtonsTop += (height() - _forward->height()) / 2;
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupInfo() {
|
||||
lng_info_link_label,
|
||||
std::move(linkText),
|
||||
QString());
|
||||
link->setClickHandlerHook([peer = _peer](auto&&...) {
|
||||
link->setClickHandlerFilter([peer = _peer](auto&&...) {
|
||||
auto link = Messenger::Instance().createInternalLinkFull(
|
||||
peer->userName());
|
||||
if (!link.isEmpty()) {
|
||||
|
||||
@@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "inline_bots/inline_bot_send_data.h"
|
||||
#include "storage/file_download.h"
|
||||
#include "core/file_utilities.h"
|
||||
#include "core/mime_type.h"
|
||||
#include "mainwidget.h"
|
||||
#include "auth_session.h"
|
||||
|
||||
@@ -427,7 +428,7 @@ MTPVector<MTPDocumentAttribute> Result::adjustAttributes(
|
||||
const auto &fields = audio->c_documentAttributeAudio();
|
||||
if (!exists(mtpc_documentAttributeFilename)
|
||||
&& !(fields.vflags.v & Flag::f_voice)) {
|
||||
const auto p = mimeTypeForName(mime).globPatterns();
|
||||
const auto p = Core::MimeTypeForName(mime).globPatterns();
|
||||
auto pattern = p.isEmpty() ? QString() : p.front();
|
||||
const auto extension = pattern.isEmpty()
|
||||
? qsl(".unknown")
|
||||
|
||||
@@ -69,9 +69,9 @@ public:
|
||||
|
||||
void closeMain() {
|
||||
QMutexLocker lock(_logsMutex(LogDataMain));
|
||||
if (files[LogDataMain]) {
|
||||
streams[LogDataMain].setDevice(0);
|
||||
files[LogDataMain]->close();
|
||||
const auto file = files[LogDataMain].get();
|
||||
if (file && file->isOpen()) {
|
||||
file->close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,11 +80,12 @@ public:
|
||||
}
|
||||
|
||||
QString full() {
|
||||
if (!streams[LogDataMain].device()) {
|
||||
const auto file = files[LogDataMain].get();
|
||||
if (!!file || !file->isOpen()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QFile out(files[LogDataMain]->fileName());
|
||||
QFile out(file->fileName());
|
||||
if (out.open(QIODevice::ReadOnly)) {
|
||||
return QString::fromUtf8(out.readAll());
|
||||
}
|
||||
@@ -93,27 +94,29 @@ public:
|
||||
|
||||
void write(LogDataType type, const QString &msg) {
|
||||
QMutexLocker lock(_logsMutex(type));
|
||||
if (type != LogDataMain) reopenDebug();
|
||||
if (!streams[type].device()) return;
|
||||
|
||||
streams[type] << msg;
|
||||
streams[type].flush();
|
||||
if (type != LogDataMain) {
|
||||
reopenDebug();
|
||||
}
|
||||
const auto file = files[type].get();
|
||||
if (!file || !file->isOpen()) {
|
||||
return;
|
||||
}
|
||||
file->write(msg.toUtf8());
|
||||
file->flush();
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<QFile> files[LogDataCount];
|
||||
QTextStream streams[LogDataCount];
|
||||
|
||||
int32 part = -1;
|
||||
|
||||
bool reopen(LogDataType type, int32 dayIndex, const QString &postfix) {
|
||||
if (streams[type].device()) {
|
||||
if (files[type] && files[type]->isOpen()) {
|
||||
if (type == LogDataMain) {
|
||||
if (!postfix.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
streams[type].setDevice(0);
|
||||
files[type]->close();
|
||||
}
|
||||
}
|
||||
@@ -134,8 +137,6 @@ private:
|
||||
}
|
||||
if (to->open(mode | QIODevice::Append)) {
|
||||
std::swap(files[type], to);
|
||||
streams[type].setDevice(files[type].get());
|
||||
streams[type].setCodec("UTF-8");
|
||||
LOG(("Moved logging from '%1' to '%2'!").arg(to->fileName()).arg(files[type]->fileName()));
|
||||
to->remove();
|
||||
|
||||
@@ -192,17 +193,14 @@ private:
|
||||
}
|
||||
}
|
||||
if (files[type]->open(mode)) {
|
||||
streams[type].setDevice(files[type].get());
|
||||
streams[type].setCodec("UTF-8");
|
||||
|
||||
if (type != LogDataMain) {
|
||||
streams[type] << ((mode & QIODevice::Append)
|
||||
files[type]->write(((mode & QIODevice::Append)
|
||||
? qsl("\
|
||||
----------------------------------------------------------------\n\
|
||||
NEW LOGGING INSTANCE STARTED!!!\n\
|
||||
----------------------------------------------------------------\n")
|
||||
: qsl("%1\n").arg(dayIndex));
|
||||
streams[type].flush();
|
||||
: qsl("%1\n").arg(dayIndex)).toUtf8());
|
||||
files[type]->flush();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -69,7 +69,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "base/qthelp_regex.h"
|
||||
#include "base/qthelp_url.h"
|
||||
#include "base/flat_set.h"
|
||||
#include "window/player_wrap_widget.h"
|
||||
#include "window/window_top_bar_wrap.h"
|
||||
#include "window/notifications_manager.h"
|
||||
#include "window/window_slide_animation.h"
|
||||
#include "window/window_controller.h"
|
||||
@@ -79,6 +79,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "core/update_checker.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "calls/calls_top_bar.h"
|
||||
#include "export/export_settings.h"
|
||||
#include "export/view/export_view_top_bar.h"
|
||||
#include "export/view/export_view_panel_controller.h"
|
||||
#include "auth_session.h"
|
||||
#include "storage/storage_facade.h"
|
||||
#include "storage/storage_shared_media.h"
|
||||
@@ -269,6 +272,12 @@ MainWidget::MainWidget(
|
||||
}
|
||||
});
|
||||
subscribe(Auth().calls().currentCallChanged(), [this](Calls::Call *call) { setCurrentCall(call); });
|
||||
|
||||
Auth().data().currentExportView(
|
||||
) | rpl::start_with_next([=](Export::View::PanelController *view) {
|
||||
setCurrentExportView(view);
|
||||
}, lifetime());
|
||||
|
||||
subscribe(_controller->dialogsListFocused(), [this](bool) {
|
||||
updateDialogsWidthAnimated();
|
||||
});
|
||||
@@ -1578,7 +1587,7 @@ void MainWidget::createPlayer() {
|
||||
return;
|
||||
}
|
||||
if (!_player) {
|
||||
_player.create(this);
|
||||
_player.create(this, object_ptr<Media::Player::Widget>(this));
|
||||
rpl::merge(
|
||||
_player->heightValue() | rpl::map([] { return true; }),
|
||||
_player->shownValue()
|
||||
@@ -1645,6 +1654,7 @@ void MainWidget::setCurrentCall(Calls::Call *call) {
|
||||
|
||||
void MainWidget::createCallTopBar() {
|
||||
Expects(_currentCall != nullptr);
|
||||
|
||||
_callTopBar.create(this, object_ptr<Calls::TopBar>(this, _currentCall));
|
||||
_callTopBar->heightValue(
|
||||
) | rpl::start_with_next([this](int value) {
|
||||
@@ -1679,6 +1689,78 @@ void MainWidget::callTopBarHeightUpdated(int callTopBarHeight) {
|
||||
}
|
||||
}
|
||||
|
||||
void MainWidget::setCurrentExportView(Export::View::PanelController *view) {
|
||||
_currentExportView = view;
|
||||
if (_currentExportView) {
|
||||
_currentExportView->progressState(
|
||||
) | rpl::start_with_next([=](Export::View::Content &&data) {
|
||||
if (!data.rows.empty()
|
||||
&& data.rows[0].id == Export::View::Content::kDoneId) {
|
||||
LOG(("Export Info: Destroy top bar by Done."));
|
||||
destroyExportTopBar();
|
||||
} else if (!_exportTopBar) {
|
||||
LOG(("Export Info: Create top bar by State."));
|
||||
createExportTopBar(std::move(data));
|
||||
} else {
|
||||
_exportTopBar->entity()->updateData(std::move(data));
|
||||
}
|
||||
}, _currentExportView->lifetime());
|
||||
} else {
|
||||
LOG(("Export Info: Destroy top bar by controller removal."));
|
||||
destroyExportTopBar();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWidget::createExportTopBar(Export::View::Content &&data) {
|
||||
_exportTopBar.create(
|
||||
this,
|
||||
object_ptr<Export::View::TopBar>(this, std::move(data)));
|
||||
rpl::merge(
|
||||
_exportTopBar->heightValue() | rpl::map([] { return true; }),
|
||||
_exportTopBar->shownValue()
|
||||
) | rpl::start_with_next([=] {
|
||||
exportTopBarHeightUpdated();
|
||||
}, _exportTopBar->lifetime());
|
||||
_exportTopBar->entity()->clicks(
|
||||
) | rpl::start_with_next([=] {
|
||||
if (_currentExportView) {
|
||||
_currentExportView->activatePanel();
|
||||
}
|
||||
}, _exportTopBar->lifetime());
|
||||
orderWidgets();
|
||||
if (_a_show.animating()) {
|
||||
_exportTopBar->show(anim::type::instant);
|
||||
_exportTopBar->setVisible(false);
|
||||
} else {
|
||||
_exportTopBar->hide(anim::type::instant);
|
||||
_exportTopBar->show(anim::type::normal);
|
||||
_exportTopBarHeight = _contentScrollAddToY = _exportTopBar->contentHeight();
|
||||
updateControlsGeometry();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWidget::destroyExportTopBar() {
|
||||
if (_exportTopBar) {
|
||||
_exportTopBar->hide(anim::type::normal);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWidget::exportTopBarHeightUpdated() {
|
||||
if (!_exportTopBar) {
|
||||
// Player could be already "destroyDelayed", but still handle events.
|
||||
return;
|
||||
}
|
||||
const auto exportTopBarHeight = _exportTopBar->contentHeight();
|
||||
if (exportTopBarHeight != _exportTopBarHeight) {
|
||||
_contentScrollAddToY += exportTopBarHeight - _exportTopBarHeight;
|
||||
_exportTopBarHeight = exportTopBarHeight;
|
||||
updateControlsGeometry();
|
||||
}
|
||||
if (!_exportTopBarHeight && _exportTopBar->isHidden()) {
|
||||
_exportTopBar.destroyDelayed();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWidget::documentLoadProgress(FileLoader *loader) {
|
||||
if (auto documentId = loader ? loader->objId() : 0) {
|
||||
documentLoadProgress(Auth().data().document(documentId));
|
||||
@@ -2560,12 +2642,15 @@ void MainWidget::showBackFromStack(
|
||||
|
||||
void MainWidget::orderWidgets() {
|
||||
_dialogs->raise();
|
||||
if (_callTopBar) {
|
||||
_callTopBar->raise();
|
||||
}
|
||||
if (_player) {
|
||||
_player->raise();
|
||||
}
|
||||
if (_exportTopBar) {
|
||||
_exportTopBar->raise();
|
||||
}
|
||||
if (_callTopBar) {
|
||||
_callTopBar->raise();
|
||||
}
|
||||
if (_playerVolume) {
|
||||
_playerVolume->raise();
|
||||
}
|
||||
@@ -2767,7 +2852,7 @@ void MainWidget::paintEvent(QPaintEvent *e) {
|
||||
}
|
||||
|
||||
int MainWidget::getMainSectionTop() const {
|
||||
return _callTopBarHeight + _playerHeight;
|
||||
return _callTopBarHeight + _exportTopBarHeight + _playerHeight;
|
||||
}
|
||||
|
||||
int MainWidget::getThirdSectionTop() const {
|
||||
@@ -2916,9 +3001,13 @@ void MainWidget::updateControlsGeometry() {
|
||||
_callTopBar->resizeToWidth(dialogsWidth);
|
||||
_callTopBar->moveToLeft(0, 0);
|
||||
}
|
||||
if (_exportTopBar) {
|
||||
_exportTopBar->resizeToWidth(dialogsWidth);
|
||||
_exportTopBar->moveToLeft(0, _callTopBarHeight);
|
||||
}
|
||||
if (_player) {
|
||||
_player->resizeToWidth(dialogsWidth);
|
||||
_player->moveToLeft(0, _callTopBarHeight);
|
||||
_player->moveToLeft(0, _callTopBarHeight + _exportTopBarHeight);
|
||||
}
|
||||
auto mainSectionGeometry = QRect(
|
||||
0,
|
||||
@@ -2954,9 +3043,15 @@ void MainWidget::updateControlsGeometry() {
|
||||
_callTopBar->resizeToWidth(mainSectionWidth);
|
||||
_callTopBar->moveToLeft(dialogsWidth, 0);
|
||||
}
|
||||
if (_exportTopBar) {
|
||||
_exportTopBar->resizeToWidth(mainSectionWidth);
|
||||
_exportTopBar->moveToLeft(dialogsWidth, _callTopBarHeight);
|
||||
}
|
||||
if (_player) {
|
||||
_player->resizeToWidth(mainSectionWidth);
|
||||
_player->moveToLeft(dialogsWidth, _callTopBarHeight);
|
||||
_player->moveToLeft(
|
||||
dialogsWidth,
|
||||
_callTopBarHeight + _exportTopBarHeight);
|
||||
}
|
||||
_history->setGeometryToLeft(dialogsWidth, mainSectionTop, mainSectionWidth, height() - mainSectionTop);
|
||||
if (_hider) {
|
||||
@@ -3730,6 +3825,10 @@ void MainWidget::start(const MTPUser *self) {
|
||||
Local::readRecentStickers();
|
||||
Local::readFavedStickers();
|
||||
Local::readSavedGifs();
|
||||
if (const auto availableAt = Local::ReadExportSettings().availableAt) {
|
||||
Auth().data().suggestStartExport(availableAt);
|
||||
}
|
||||
|
||||
_history->start();
|
||||
|
||||
Messenger::Instance().checkStartUrl();
|
||||
|
||||
@@ -40,6 +40,14 @@ class Float;
|
||||
} // namespace Player
|
||||
} // namespace Media
|
||||
|
||||
namespace Export {
|
||||
namespace View {
|
||||
class TopBar;
|
||||
class PanelController;
|
||||
struct Content;
|
||||
} // namespace View
|
||||
} // namespace Export
|
||||
|
||||
namespace Ui {
|
||||
class ResizeArea;
|
||||
class PlainShadow;
|
||||
@@ -50,7 +58,8 @@ class SlideWrap;
|
||||
|
||||
namespace Window {
|
||||
class Controller;
|
||||
class PlayerWrapWidget;
|
||||
template <typename Inner>
|
||||
class TopBarWrapWidget;
|
||||
class SectionMemento;
|
||||
class SectionWidget;
|
||||
class AbstractSectionWidget;
|
||||
@@ -422,6 +431,11 @@ private:
|
||||
void destroyCallTopBar();
|
||||
void callTopBarHeightUpdated(int callTopBarHeight);
|
||||
|
||||
void setCurrentExportView(Export::View::PanelController *view);
|
||||
void createExportTopBar(Export::View::Content &&data);
|
||||
void destroyExportTopBar();
|
||||
void exportTopBarHeightUpdated();
|
||||
|
||||
void messagesAffected(
|
||||
not_null<PeerData*> peer,
|
||||
const MTPmessages_AffectedMessages &result);
|
||||
@@ -545,7 +559,12 @@ private:
|
||||
base::weak_ptr<Calls::Call> _currentCall;
|
||||
object_ptr<Ui::SlideWrap<Calls::TopBar>> _callTopBar = { nullptr };
|
||||
|
||||
object_ptr<Window::PlayerWrapWidget> _player = { nullptr };
|
||||
Export::View::PanelController *_currentExportView = nullptr;
|
||||
object_ptr<Window::TopBarWrapWidget<Export::View::TopBar>> _exportTopBar
|
||||
= { nullptr };
|
||||
|
||||
object_ptr<Window::TopBarWrapWidget<Media::Player::Widget>> _player
|
||||
= { nullptr };
|
||||
object_ptr<Media::Player::VolumeWidget> _playerVolume = { nullptr };
|
||||
object_ptr<Media::Player::Panel> _playerPlaylist;
|
||||
object_ptr<Media::Player::Panel> _playerPanel;
|
||||
@@ -558,6 +577,7 @@ private:
|
||||
|
||||
int _playerHeight = 0;
|
||||
int _callTopBarHeight = 0;
|
||||
int _exportTopBarHeight = 0;
|
||||
int _contentScrollAddToY = 0;
|
||||
|
||||
int32 updDate = 0;
|
||||
|
||||
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "mainwindow.h"
|
||||
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_session.h"
|
||||
#include "dialogs/dialogs_layout.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
#include "styles/style_window.h"
|
||||
@@ -618,11 +619,22 @@ void MainWindow::onLogout() {
|
||||
showFromTray();
|
||||
}
|
||||
|
||||
const auto logout = [] {
|
||||
Messenger::Instance().logOut();
|
||||
};
|
||||
const auto callback = [=] {
|
||||
if (AuthSession::Exists() && Auth().data().exportInProgress()) {
|
||||
Ui::hideLayer();
|
||||
Auth().data().stopExportWithConfirmation(logout);
|
||||
} else {
|
||||
logout();
|
||||
}
|
||||
};
|
||||
Ui::show(Box<ConfirmBox>(
|
||||
lang(lng_sure_logout),
|
||||
lang(lng_settings_logout),
|
||||
st::attentionBoxButton,
|
||||
[] { Messenger::Instance().logOut(); }));
|
||||
callback));
|
||||
}
|
||||
|
||||
void MainWindow::quitFromTray() {
|
||||
|
||||
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "mainwindow.h"
|
||||
#include "application.h"
|
||||
#include "core/file_utilities.h"
|
||||
#include "core/mime_type.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/text_options.h"
|
||||
@@ -807,7 +808,7 @@ void MediaView::onSaveAs() {
|
||||
QFileInfo alreadyInfo(location.name());
|
||||
QDir alreadyDir(alreadyInfo.dir());
|
||||
QString name = alreadyInfo.fileName(), filter;
|
||||
MimeType mimeType = mimeTypeForName(_doc->mimeString());
|
||||
const auto mimeType = Core::MimeTypeForName(_doc->mimeString());
|
||||
QStringList p = mimeType.globPatterns();
|
||||
QString pattern = p.isEmpty() ? QString() : p.front();
|
||||
if (name.isEmpty()) {
|
||||
|
||||
223
Telegram/SourceFiles/mtproto/concurrent_sender.cpp
Normal file
223
Telegram/SourceFiles/mtproto/concurrent_sender.cpp
Normal file
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "mtproto/concurrent_sender.h"
|
||||
|
||||
#include "mtproto/mtp_instance.h"
|
||||
#include "mtproto/rpc_sender.h"
|
||||
#include "mtproto/session.h"
|
||||
|
||||
namespace MTP {
|
||||
|
||||
class ConcurrentSender::RPCDoneHandler : public RPCAbstractDoneHandler {
|
||||
public:
|
||||
RPCDoneHandler(
|
||||
not_null<ConcurrentSender*> sender,
|
||||
Fn<void(FnMut<void()>)> runner);
|
||||
|
||||
void operator()(
|
||||
mtpRequestId requestId,
|
||||
const mtpPrime *from,
|
||||
const mtpPrime *end) override;
|
||||
|
||||
private:
|
||||
base::weak_ptr<ConcurrentSender> _weak;
|
||||
Fn<void(FnMut<void()>)> _runner;
|
||||
|
||||
};
|
||||
|
||||
class ConcurrentSender::RPCFailHandler : public RPCAbstractFailHandler {
|
||||
public:
|
||||
RPCFailHandler(
|
||||
not_null<ConcurrentSender*> sender,
|
||||
Fn<void(FnMut<void()>)> runner,
|
||||
FailSkipPolicy skipPolicy);
|
||||
|
||||
bool operator()(
|
||||
mtpRequestId requestId,
|
||||
const RPCError &error) override;
|
||||
|
||||
private:
|
||||
base::weak_ptr<ConcurrentSender> _weak;
|
||||
Fn<void(FnMut<void()>)> _runner;
|
||||
FailSkipPolicy _skipPolicy = FailSkipPolicy::Simple;
|
||||
|
||||
};
|
||||
|
||||
ConcurrentSender::RPCDoneHandler::RPCDoneHandler(
|
||||
not_null<ConcurrentSender*> sender,
|
||||
Fn<void(FnMut<void()>)> runner)
|
||||
: _weak(sender)
|
||||
, _runner(std::move(runner)) {
|
||||
}
|
||||
|
||||
void ConcurrentSender::RPCDoneHandler::operator()(
|
||||
mtpRequestId requestId,
|
||||
const mtpPrime *from,
|
||||
const mtpPrime *end) {
|
||||
auto response = gsl::make_span(
|
||||
from,
|
||||
end - from);
|
||||
_runner([=, weak = _weak, moved = bytes::make_vector(response)]() mutable {
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->senderRequestDone(requestId, std::move(moved));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ConcurrentSender::RPCFailHandler::RPCFailHandler(
|
||||
not_null<ConcurrentSender*> sender,
|
||||
Fn<void(FnMut<void()>)> runner,
|
||||
FailSkipPolicy skipPolicy)
|
||||
: _weak(sender)
|
||||
, _runner(std::move(runner))
|
||||
, _skipPolicy(skipPolicy) {
|
||||
}
|
||||
|
||||
bool ConcurrentSender::RPCFailHandler::operator()(
|
||||
mtpRequestId requestId,
|
||||
const RPCError &error) {
|
||||
if (_skipPolicy == FailSkipPolicy::Simple) {
|
||||
if (MTP::isDefaultHandledError(error)) {
|
||||
return false;
|
||||
}
|
||||
} else if (_skipPolicy == FailSkipPolicy::HandleFlood) {
|
||||
if (MTP::isDefaultHandledError(error) && !MTP::isFloodError(error)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
_runner([=, weak = _weak, error = error]() mutable {
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->senderRequestFail(requestId, std::move(error));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename Method>
|
||||
auto ConcurrentSender::with_instance(Method &&method)
|
||||
-> std::enable_if_t<is_callable_v<Method, not_null<Instance*>>> {
|
||||
crl::on_main([method = std::forward<Method>(method)]() mutable {
|
||||
if (const auto instance = MainInstance()) {
|
||||
std::move(method)(instance);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ConcurrentSender::RequestBuilder::RequestBuilder(
|
||||
not_null<ConcurrentSender*> sender,
|
||||
mtpRequest &&serialized) noexcept
|
||||
: _sender(sender)
|
||||
, _serialized(std::move(serialized)) {
|
||||
}
|
||||
|
||||
void ConcurrentSender::RequestBuilder::setToDC(ShiftedDcId dcId) noexcept {
|
||||
_dcId = dcId;
|
||||
}
|
||||
|
||||
void ConcurrentSender::RequestBuilder::setCanWait(TimeMs ms) noexcept {
|
||||
_canWait = ms;
|
||||
}
|
||||
|
||||
void ConcurrentSender::RequestBuilder::setFailSkipPolicy(
|
||||
FailSkipPolicy policy) noexcept {
|
||||
_failSkipPolicy = policy;
|
||||
}
|
||||
|
||||
void ConcurrentSender::RequestBuilder::setAfter(
|
||||
mtpRequestId requestId) noexcept {
|
||||
_afterRequestId = requestId;
|
||||
}
|
||||
|
||||
mtpRequestId ConcurrentSender::RequestBuilder::send() {
|
||||
const auto requestId = GetNextRequestId();
|
||||
const auto dcId = _dcId;
|
||||
const auto msCanWait = _canWait;
|
||||
const auto afterRequestId = _afterRequestId;
|
||||
|
||||
_sender->senderRequestRegister(requestId, std::move(_handlers));
|
||||
_sender->with_instance([
|
||||
=,
|
||||
request = std::move(_serialized),
|
||||
done = std::make_shared<RPCDoneHandler>(_sender, _sender->_runner),
|
||||
fail = std::make_shared<RPCFailHandler>(
|
||||
_sender,
|
||||
_sender->_runner,
|
||||
_failSkipPolicy)
|
||||
](not_null<Instance*> instance) mutable {
|
||||
instance->sendSerialized(
|
||||
requestId,
|
||||
std::move(request),
|
||||
RPCResponseHandler(std::move(done), std::move(fail)),
|
||||
dcId,
|
||||
msCanWait,
|
||||
afterRequestId);
|
||||
});
|
||||
|
||||
return requestId;
|
||||
}
|
||||
|
||||
ConcurrentSender::ConcurrentSender(Fn<void(FnMut<void()>)> runner)
|
||||
: _runner(runner) {
|
||||
}
|
||||
|
||||
ConcurrentSender::~ConcurrentSender() {
|
||||
senderRequestCancelAll();
|
||||
}
|
||||
|
||||
void ConcurrentSender::senderRequestRegister(
|
||||
mtpRequestId requestId,
|
||||
Handlers &&handlers) {
|
||||
_requests.emplace(requestId, std::move(handlers));
|
||||
}
|
||||
|
||||
void ConcurrentSender::senderRequestDone(
|
||||
mtpRequestId requestId,
|
||||
bytes::const_span result) {
|
||||
if (auto handlers = _requests.take(requestId)) {
|
||||
try {
|
||||
std::move(handlers->done)(requestId, result);
|
||||
} catch (Exception &e) {
|
||||
std::move(handlers->fail)(requestId, internal::rpcClientError(
|
||||
"RESPONSE_PARSE_FAILED",
|
||||
QString("exception text: ") + e.what()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConcurrentSender::senderRequestFail(
|
||||
mtpRequestId requestId,
|
||||
RPCError &&error) {
|
||||
if (auto handlers = _requests.take(requestId)) {
|
||||
std::move(handlers->fail)(requestId, std::move(error));
|
||||
}
|
||||
}
|
||||
|
||||
void ConcurrentSender::senderRequestCancel(mtpRequestId requestId) {
|
||||
senderRequestDetach(requestId);
|
||||
with_instance([=](not_null<Instance*> instance) {
|
||||
instance->cancel(requestId);
|
||||
});
|
||||
}
|
||||
|
||||
void ConcurrentSender::senderRequestCancelAll() {
|
||||
auto list = std::vector<mtpRequestId>(_requests.size());
|
||||
for (const auto &pair : base::take(_requests)) {
|
||||
list.push_back(pair.first);
|
||||
}
|
||||
with_instance([list = std::move(list)](not_null<Instance*> instance) {
|
||||
for (const auto requestId : list) {
|
||||
instance->cancel(requestId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ConcurrentSender::senderRequestDetach(mtpRequestId requestId) {
|
||||
_requests.erase(requestId);
|
||||
}
|
||||
|
||||
} // namespace MTP
|
||||
479
Telegram/SourceFiles/mtproto/concurrent_sender.h
Normal file
479
Telegram/SourceFiles/mtproto/concurrent_sender.h
Normal file
@@ -0,0 +1,479 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <rpl/details/callable.h>
|
||||
#include "base/bytes.h"
|
||||
#include "base/weak_ptr.h"
|
||||
#include "base/flat_map.h"
|
||||
#include "mtproto/core_types.h"
|
||||
|
||||
#ifndef _DEBUG
|
||||
#define MTP_SENDER_USE_GENERIC_HANDLERS
|
||||
#endif // !_DEBUG
|
||||
|
||||
class RPCError;
|
||||
|
||||
namespace MTP {
|
||||
|
||||
class Instance;
|
||||
|
||||
class ConcurrentSender : public base::has_weak_ptr {
|
||||
template <typename ...Args>
|
||||
static constexpr bool is_callable_v
|
||||
= rpl::details::is_callable_v<Args...>;
|
||||
|
||||
template <typename Method>
|
||||
auto with_instance(Method &&method)
|
||||
-> std::enable_if_t<is_callable_v<Method, not_null<Instance*>>>;
|
||||
|
||||
using DoneHandler = FnMut<void(
|
||||
mtpRequestId requestId,
|
||||
bytes::const_span result)>;
|
||||
using FailHandler = FnMut<void(
|
||||
mtpRequestId requestId,
|
||||
RPCError &&error)>;
|
||||
struct Handlers {
|
||||
DoneHandler done;
|
||||
FailHandler fail;
|
||||
};
|
||||
|
||||
enum class FailSkipPolicy {
|
||||
Simple,
|
||||
HandleFlood,
|
||||
HandleAll,
|
||||
};
|
||||
|
||||
class RequestBuilder {
|
||||
public:
|
||||
RequestBuilder(const RequestBuilder &other) = delete;
|
||||
RequestBuilder(RequestBuilder &&other) = default;
|
||||
RequestBuilder &operator=(const RequestBuilder &other) = delete;
|
||||
RequestBuilder &operator=(RequestBuilder &&other) = delete;
|
||||
|
||||
mtpRequestId send();
|
||||
|
||||
protected:
|
||||
RequestBuilder(
|
||||
not_null<ConcurrentSender*> sender,
|
||||
mtpRequest &&serialized) noexcept;
|
||||
|
||||
void setToDC(ShiftedDcId dcId) noexcept;
|
||||
void setCanWait(TimeMs ms) noexcept;
|
||||
template <typename Response, typename InvokeFullDone>
|
||||
void setDoneHandler(InvokeFullDone &&invoke) noexcept;
|
||||
template <typename InvokeFullFail>
|
||||
void setFailHandler(InvokeFullFail &&invoke) noexcept;
|
||||
void setFailSkipPolicy(FailSkipPolicy policy) noexcept;
|
||||
void setAfter(mtpRequestId requestId) noexcept;
|
||||
|
||||
private:
|
||||
not_null<ConcurrentSender*> _sender;
|
||||
mtpRequest _serialized;
|
||||
ShiftedDcId _dcId = 0;
|
||||
TimeMs _canWait = 0;
|
||||
|
||||
Handlers _handlers;
|
||||
FailSkipPolicy _failSkipPolicy = FailSkipPolicy::Simple;
|
||||
mtpRequestId _afterRequestId = 0;
|
||||
|
||||
};
|
||||
|
||||
public:
|
||||
ConcurrentSender(Fn<void(FnMut<void()>)> runner);
|
||||
|
||||
template <typename Request>
|
||||
class SpecificRequestBuilder : public RequestBuilder {
|
||||
public:
|
||||
using Response = typename Request::ResponseType;
|
||||
|
||||
SpecificRequestBuilder(
|
||||
const SpecificRequestBuilder &other) = delete;
|
||||
SpecificRequestBuilder(
|
||||
SpecificRequestBuilder &&other) = default;
|
||||
SpecificRequestBuilder &operator=(
|
||||
const SpecificRequestBuilder &other) = delete;
|
||||
SpecificRequestBuilder &operator=(
|
||||
SpecificRequestBuilder &&other) = delete;
|
||||
|
||||
[[nodiscard]] SpecificRequestBuilder &toDC(
|
||||
ShiftedDcId dcId) noexcept;
|
||||
[[nodiscard]] SpecificRequestBuilder &afterDelay(
|
||||
TimeMs ms) noexcept;
|
||||
|
||||
#ifndef MTP_SENDER_USE_GENERIC_HANDLERS
|
||||
// Allow code completion to show response type.
|
||||
[[nodiscard]] SpecificRequestBuilder &done(FnMut<void()> &&handler);
|
||||
[[nodiscard]] SpecificRequestBuilder &done(FnMut<void(
|
||||
mtpRequestId)> &&handler);
|
||||
[[nodiscard]] SpecificRequestBuilder &done(FnMut<void(
|
||||
mtpRequestId,
|
||||
Response &&)> &&handler);
|
||||
[[nodiscard]] SpecificRequestBuilder &done(FnMut<void(
|
||||
Response &&)> &&handler);
|
||||
[[nodiscard]] SpecificRequestBuilder &fail(FnMut<void()> &&handler);
|
||||
[[nodiscard]] SpecificRequestBuilder &fail(FnMut<void(
|
||||
mtpRequestId)> &&handler);
|
||||
[[nodiscard]] SpecificRequestBuilder &fail(FnMut<void(
|
||||
mtpRequestId,
|
||||
RPCError &&)> &&handler);
|
||||
[[nodiscard]] SpecificRequestBuilder &fail(FnMut<void(
|
||||
RPCError &&)> &&handler);
|
||||
#else // !MTP_SENDER_USE_GENERIC_HANDLERS
|
||||
template <typename Handler>
|
||||
[[nodiscard]] SpecificRequestBuilder &done(Handler &&handler);
|
||||
template <typename Handler>
|
||||
[[nodiscard]] SpecificRequestBuilder &fail(Handler &&handler);
|
||||
#endif // MTP_SENDER_USE_GENERIC_HANDLERS
|
||||
|
||||
[[nodiscard]] SpecificRequestBuilder &handleFloodErrors() noexcept;
|
||||
[[nodiscard]] SpecificRequestBuilder &handleAllErrors() noexcept;
|
||||
[[nodiscard]] SpecificRequestBuilder &afterRequest(
|
||||
mtpRequestId requestId) noexcept;
|
||||
|
||||
private:
|
||||
SpecificRequestBuilder(
|
||||
not_null<ConcurrentSender*> sender,
|
||||
Request &&request) noexcept;
|
||||
|
||||
friend class ConcurrentSender;
|
||||
|
||||
};
|
||||
|
||||
class SentRequestWrap {
|
||||
public:
|
||||
void cancel();
|
||||
void detach();
|
||||
|
||||
private:
|
||||
friend class ConcurrentSender;
|
||||
SentRequestWrap(
|
||||
not_null<ConcurrentSender*> sender,
|
||||
mtpRequestId requestId);
|
||||
|
||||
not_null<ConcurrentSender*> _sender;
|
||||
mtpRequestId _requestId = 0;
|
||||
|
||||
};
|
||||
|
||||
template <
|
||||
typename Request,
|
||||
typename = std::enable_if_t<!std::is_reference_v<Request>>,
|
||||
typename = typename Request::Unboxed>
|
||||
[[nodiscard]] SpecificRequestBuilder<Request> request(
|
||||
Request &&request) noexcept;
|
||||
|
||||
[[nodiscard]] SentRequestWrap request(mtpRequestId requestId) noexcept;
|
||||
|
||||
[[nodiscard]] auto requestCanceller() noexcept;
|
||||
|
||||
~ConcurrentSender();
|
||||
|
||||
private:
|
||||
class RPCDoneHandler;
|
||||
friend class RPCDoneHandler;
|
||||
class RPCFailHandler;
|
||||
friend class RPCFailHandler;
|
||||
friend class RequestBuilder;
|
||||
friend class SentRequestWrap;
|
||||
|
||||
void senderRequestRegister(mtpRequestId requestId, Handlers &&handlers);
|
||||
void senderRequestDone(
|
||||
mtpRequestId requestId,
|
||||
bytes::const_span result);
|
||||
void senderRequestFail(
|
||||
mtpRequestId requestId,
|
||||
RPCError &&error);
|
||||
void senderRequestCancel(mtpRequestId requestId);
|
||||
void senderRequestCancelAll();
|
||||
void senderRequestDetach(mtpRequestId requestId);
|
||||
|
||||
const Fn<void(FnMut<void()>)> _runner;
|
||||
base::flat_map<mtpRequestId, Handlers> _requests;
|
||||
|
||||
};
|
||||
|
||||
template <typename Response, typename InvokeFullDone>
|
||||
void ConcurrentSender::RequestBuilder::setDoneHandler(
|
||||
InvokeFullDone &&invoke
|
||||
) noexcept {
|
||||
_handlers.done = [handler = std::move(invoke)](
|
||||
mtpRequestId requestId,
|
||||
bytes::const_span result) mutable {
|
||||
auto from = reinterpret_cast<const mtpPrime*>(result.data());
|
||||
const auto end = from + result.size() / sizeof(mtpPrime);
|
||||
Response data;
|
||||
data.read(from, end);
|
||||
std::move(handler)(requestId, std::move(data));
|
||||
};
|
||||
}
|
||||
|
||||
template <typename InvokeFullFail>
|
||||
void ConcurrentSender::RequestBuilder::setFailHandler(
|
||||
InvokeFullFail &&invoke
|
||||
) noexcept {
|
||||
_handlers.fail = std::move(invoke);
|
||||
}
|
||||
|
||||
template <typename Request>
|
||||
ConcurrentSender::SpecificRequestBuilder<Request>::SpecificRequestBuilder(
|
||||
not_null<ConcurrentSender*> sender,
|
||||
Request &&request
|
||||
) noexcept : RequestBuilder(sender, mtpRequestData::serialize(request)) {
|
||||
}
|
||||
|
||||
template <typename Request>
|
||||
auto ConcurrentSender::SpecificRequestBuilder<Request>::toDC(
|
||||
ShiftedDcId dcId
|
||||
) noexcept -> SpecificRequestBuilder & {
|
||||
setToDC(dcId);
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename Request>
|
||||
auto ConcurrentSender::SpecificRequestBuilder<Request>::afterDelay(
|
||||
TimeMs ms
|
||||
) noexcept -> SpecificRequestBuilder & {
|
||||
setCanWait(ms);
|
||||
return *this;
|
||||
}
|
||||
|
||||
#ifndef MTP_SENDER_USE_GENERIC_HANDLERS
|
||||
// Allow code completion to show response type.
|
||||
template <typename Request>
|
||||
auto ConcurrentSender::SpecificRequestBuilder<Request>::done(
|
||||
FnMut<void(Response &&)> &&handler
|
||||
) -> SpecificRequestBuilder & {
|
||||
setDoneHandler<Response>([handler = std::move(handler)](
|
||||
mtpRequestId requestId,
|
||||
Response &&result) mutable {
|
||||
std::move(handler)(std::move(result));
|
||||
});
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename Request>
|
||||
auto ConcurrentSender::SpecificRequestBuilder<Request>::done(
|
||||
FnMut<void(mtpRequestId, Response &&)> &&handler
|
||||
) -> SpecificRequestBuilder & {
|
||||
setDoneHandler<Response>(std::move(handler));
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename Request>
|
||||
auto ConcurrentSender::SpecificRequestBuilder<Request>::done(
|
||||
FnMut<void(mtpRequestId)> &&handler
|
||||
) -> SpecificRequestBuilder & {
|
||||
setDoneHandler<Response>([handler = std::move(handler)](
|
||||
mtpRequestId requestId,
|
||||
Response &&result) mutable {
|
||||
std::move(handler)(requestId);
|
||||
});
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename Request>
|
||||
auto ConcurrentSender::SpecificRequestBuilder<Request>::done(
|
||||
FnMut<void()> &&handler
|
||||
) -> SpecificRequestBuilder & {
|
||||
setDoneHandler<Response>([handler = std::move(handler)](
|
||||
mtpRequestId requestId,
|
||||
Response &&result) mutable {
|
||||
std::move(handler)();
|
||||
});
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename Request>
|
||||
auto ConcurrentSender::SpecificRequestBuilder<Request>::fail(
|
||||
FnMut<void(RPCError &&)> &&handler
|
||||
) -> SpecificRequestBuilder & {
|
||||
setFailHandler([handler = std::move(handler)](
|
||||
mtpRequestId requestId,
|
||||
RPCError &&error) mutable {
|
||||
std::move(handler)(std::move(error));
|
||||
});
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename Request>
|
||||
auto ConcurrentSender::SpecificRequestBuilder<Request>::fail(
|
||||
FnMut<void(mtpRequestId, RPCError &&)> &&handler
|
||||
) -> SpecificRequestBuilder & {
|
||||
setFailHandler(std::move(handler));
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename Request>
|
||||
auto ConcurrentSender::SpecificRequestBuilder<Request>::fail(
|
||||
FnMut<void(mtpRequestId)> &&handler
|
||||
) -> SpecificRequestBuilder & {
|
||||
setFailHandler([handler = std::move(handler)](
|
||||
mtpRequestId requestId,
|
||||
RPCError &&error) mutable {
|
||||
std::move(handler)(requestId);
|
||||
});
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename Request>
|
||||
auto ConcurrentSender::SpecificRequestBuilder<Request>::fail(
|
||||
FnMut<void()> &&handler
|
||||
) -> SpecificRequestBuilder & {
|
||||
setFailHandler([handler = std::move(handler)](
|
||||
mtpRequestId requestId,
|
||||
RPCError &&error) mutable {
|
||||
std::move(handler)();
|
||||
});
|
||||
return *this;
|
||||
}
|
||||
#else // !MTP_SENDER_USE_GENERIC_HANDLERS
|
||||
template <typename Request>
|
||||
template <typename Handler>
|
||||
auto ConcurrentSender::SpecificRequestBuilder<Request>::done(
|
||||
Handler &&handler
|
||||
) -> SpecificRequestBuilder & {
|
||||
using Response = typename Request::ResponseType;
|
||||
constexpr auto takesFull = rpl::details::is_callable_plain_v<
|
||||
Handler,
|
||||
mtpRequestId,
|
||||
Response>;
|
||||
constexpr auto takesResponse = rpl::details::is_callable_plain_v<
|
||||
Handler,
|
||||
Response>;
|
||||
constexpr auto takesRequestId = rpl::details::is_callable_plain_v<
|
||||
Handler,
|
||||
mtpRequestId>;
|
||||
constexpr auto takesNone = rpl::details::is_callable_plain_v<Handler>;
|
||||
|
||||
if constexpr (takesFull) {
|
||||
setDoneHandler<Response>(std::forward<Handler>(handler));
|
||||
} else if constexpr (takesResponse) {
|
||||
setDoneHandler<Response>([handler = std::forward<Handler>(handler)](
|
||||
mtpRequestId requestId,
|
||||
Response &&result) mutable {
|
||||
std::move(handler)(std::move(result));
|
||||
});
|
||||
} else if constexpr (takesRequestId) {
|
||||
setDoneHandler<Response>([handler = std::forward<Handler>(handler)](
|
||||
mtpRequestId requestId,
|
||||
Response &&result) mutable {
|
||||
std::move(handler)(requestId);
|
||||
});
|
||||
} else if constexpr (takesNone) {
|
||||
setDoneHandler<Response>([handler = std::forward<Handler>(handler)](
|
||||
mtpRequestId requestId,
|
||||
Response &&result) mutable {
|
||||
std::move(handler)();
|
||||
});
|
||||
} else {
|
||||
static_assert(false_t(Handler{}), "Bad done handler.");
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename Request>
|
||||
template <typename Handler>
|
||||
auto ConcurrentSender::SpecificRequestBuilder<Request>::fail(
|
||||
Handler &&handler
|
||||
) -> SpecificRequestBuilder & {
|
||||
constexpr auto takesFull = rpl::details::is_callable_plain_v<
|
||||
Handler,
|
||||
mtpRequestId,
|
||||
RPCError>;
|
||||
constexpr auto takesError = rpl::details::is_callable_plain_v<
|
||||
Handler,
|
||||
RPCError>;
|
||||
constexpr auto takesRequestId = rpl::details::is_callable_plain_v<
|
||||
Handler,
|
||||
mtpRequestId>;
|
||||
constexpr auto takesNone = rpl::details::is_callable_plain_v<Handler>;
|
||||
|
||||
if constexpr (takesFull) {
|
||||
setFailHandler(std::forward<Handler>(handler));
|
||||
} else if constexpr (takesError) {
|
||||
setFailHandler([handler = std::forward<Handler>(handler)](
|
||||
mtpRequestId requestId,
|
||||
RPCError &&error) mutable {
|
||||
std::move(handler)(std::move(error));
|
||||
});
|
||||
} else if constexpr (takesRequestId) {
|
||||
setFailHandler([handler = std::forward<Handler>(handler)](
|
||||
mtpRequestId requestId,
|
||||
RPCError &&error) mutable {
|
||||
std::move(handler)(requestId);
|
||||
});
|
||||
} else if constexpr (takesNone) {
|
||||
setFailHandler([handler = std::forward<Handler>(handler)](
|
||||
mtpRequestId requestId,
|
||||
RPCError &&error) mutable {
|
||||
std::move(handler)();
|
||||
});
|
||||
} else {
|
||||
static_assert(false_t(Handler{}), "Bad fail handler.");
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
#endif // MTP_SENDER_USE_GENERIC_HANDLERS
|
||||
|
||||
template <typename Request>
|
||||
auto ConcurrentSender::SpecificRequestBuilder<Request>::handleFloodErrors(
|
||||
) noexcept -> SpecificRequestBuilder & {
|
||||
setFailSkipPolicy(FailSkipPolicy::HandleFlood);
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename Request>
|
||||
auto ConcurrentSender::SpecificRequestBuilder<Request>::handleAllErrors(
|
||||
) noexcept -> SpecificRequestBuilder & {
|
||||
setFailSkipPolicy(FailSkipPolicy::HandleAll);
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename Request>
|
||||
auto ConcurrentSender::SpecificRequestBuilder<Request>::afterRequest(
|
||||
mtpRequestId requestId
|
||||
) noexcept -> SpecificRequestBuilder & {
|
||||
setAfter(requestId);
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline void ConcurrentSender::SentRequestWrap::cancel() {
|
||||
_sender->senderRequestCancel(_requestId);
|
||||
}
|
||||
|
||||
inline void ConcurrentSender::SentRequestWrap::detach() {
|
||||
_sender->senderRequestDetach(_requestId);
|
||||
}
|
||||
|
||||
inline ConcurrentSender::SentRequestWrap::SentRequestWrap(
|
||||
not_null<ConcurrentSender*> sender,
|
||||
mtpRequestId requestId
|
||||
) : _sender(sender)
|
||||
, _requestId(requestId) {
|
||||
}
|
||||
|
||||
template <typename Request, typename, typename>
|
||||
inline auto ConcurrentSender::request(Request &&request) noexcept
|
||||
-> SpecificRequestBuilder<Request> {
|
||||
return SpecificRequestBuilder<Request>(this, std::move(request));
|
||||
}
|
||||
|
||||
inline auto ConcurrentSender::requestCanceller() noexcept {
|
||||
return [=](mtpRequestId requestId) {
|
||||
request(requestId).cancel();
|
||||
};
|
||||
}
|
||||
|
||||
inline auto ConcurrentSender::request(mtpRequestId requestId) noexcept
|
||||
-> SentRequestWrap {
|
||||
return SentRequestWrap(this, requestId);
|
||||
}
|
||||
|
||||
} // namespace MTP
|
||||
@@ -376,9 +376,9 @@ void ConnectionPrivate::appendTestConnection(
|
||||
}
|
||||
|
||||
int16 ConnectionPrivate::getProtocolDcId() const {
|
||||
const auto dcId = MTP::bareDcId(_shiftedDcId);
|
||||
const auto simpleDcId = MTP::isTemporaryDcId(dcId)
|
||||
? MTP::getRealIdFromTemporaryDcId(dcId)
|
||||
const auto dcId = BareDcId(_shiftedDcId);
|
||||
const auto simpleDcId = isTemporaryDcId(dcId)
|
||||
? getRealIdFromTemporaryDcId(dcId)
|
||||
: dcId;
|
||||
const auto testedDcId = cTestMode()
|
||||
? (kTestModeDcIdShift + simpleDcId)
|
||||
@@ -445,7 +445,7 @@ ConnectionPrivate::ConnectionPrivate(
|
||||
connect(this, SIGNAL(sendMsgsStateInfoAsync(quint64, QByteArray)), sessionData->owner(), SLOT(sendMsgsStateInfo(quint64,QByteArray)), Qt::QueuedConnection);
|
||||
connect(this, SIGNAL(resendAsync(quint64,qint64,bool,bool)), sessionData->owner(), SLOT(resend(quint64,qint64,bool,bool)), Qt::QueuedConnection);
|
||||
connect(this, SIGNAL(resendManyAsync(QVector<quint64>,qint64,bool,bool)), sessionData->owner(), SLOT(resendMany(QVector<quint64>,qint64,bool,bool)), Qt::QueuedConnection);
|
||||
connect(this, SIGNAL(resendAllAsync()), sessionData->owner(), SLOT(resendAll()));
|
||||
connect(this, SIGNAL(resendAllAsync()), sessionData->owner(), SLOT(resendAll()), Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void ConnectionPrivate::onConfigLoaded() {
|
||||
@@ -724,13 +724,13 @@ void ConnectionPrivate::tryToSend() {
|
||||
int32 state = getState();
|
||||
bool prependOnly = (state != ConnectedState);
|
||||
mtpRequest pingRequest;
|
||||
if (_shiftedDcId == bareDcId(_shiftedDcId)) { // main session
|
||||
if (_shiftedDcId == BareDcId(_shiftedDcId)) { // main session
|
||||
if (!prependOnly && !_pingIdToSend && !_pingId && _pingSendAt <= getms(true)) {
|
||||
_pingIdToSend = rand_value<mtpPingId>();
|
||||
}
|
||||
}
|
||||
if (_pingIdToSend) {
|
||||
if (prependOnly || _shiftedDcId != bareDcId(_shiftedDcId)) {
|
||||
if (prependOnly || _shiftedDcId != BareDcId(_shiftedDcId)) {
|
||||
MTPPing ping(MTPping(MTP_long(_pingIdToSend)));
|
||||
uint32 pingSize = ping.innerLength() >> 2; // copy from Session::send
|
||||
pingRequest = mtpRequestData::prepare(pingSize);
|
||||
@@ -748,7 +748,7 @@ void ConnectionPrivate::tryToSend() {
|
||||
_pingSendAt = pingRequest->msDate + kPingSendAfter;
|
||||
pingRequest->requestId = 0; // dont add to haveSent / wereAcked maps
|
||||
|
||||
if (_shiftedDcId == bareDcId(_shiftedDcId) && !prependOnly) { // main session
|
||||
if (_shiftedDcId == BareDcId(_shiftedDcId) && !prependOnly) { // main session
|
||||
_pingSender.callOnce(kPingSendAfterForce);
|
||||
}
|
||||
|
||||
@@ -806,7 +806,7 @@ void ConnectionPrivate::tryToSend() {
|
||||
req.write(*stateRequest);
|
||||
|
||||
stateRequest->msDate = getms(true); // > 0 - can send without container
|
||||
stateRequest->requestId = reqid();// add to haveSent / wereAcked maps, but don't add to requestMap
|
||||
stateRequest->requestId = GetNextRequestId();// add to haveSent / wereAcked maps, but don't add to requestMap
|
||||
}
|
||||
if (_connection->usingHttpWait()) {
|
||||
MTPHttpWait req(MTP_http_wait(MTP_int(100), MTP_int(30), MTP_int(25000)));
|
||||
@@ -916,7 +916,7 @@ void ConnectionPrivate::tryToSend() {
|
||||
mtpRequest wrappedRequest(mtpRequestData::prepare(toSendSize));
|
||||
memcpy(wrappedRequest->data(), toSendRequest->constData(), 7 * sizeof(mtpPrime)); // all except length
|
||||
wrappedRequest->push_back(mtpc_invokeWithLayer);
|
||||
wrappedRequest->push_back(MTP::internal::CurrentLayer);
|
||||
wrappedRequest->push_back(internal::CurrentLayer);
|
||||
initWrapper.write(*wrappedRequest);
|
||||
wrappedRequest->resize(wrappedRequest->size() + noWrapSize);
|
||||
memcpy(wrappedRequest->data() + wrappedRequest->size() - noWrapSize, toSendRequest->constData() + 8, noWrapSize * sizeof(mtpPrime));
|
||||
@@ -948,7 +948,7 @@ void ConnectionPrivate::tryToSend() {
|
||||
if (willNeedInit) {
|
||||
initSerialized.reserve(initSizeInInts);
|
||||
initSerialized.push_back(mtpc_invokeWithLayer);
|
||||
initSerialized.push_back(MTP::internal::CurrentLayer);
|
||||
initSerialized.push_back(internal::CurrentLayer);
|
||||
initWrapper.write(initSerialized);
|
||||
}
|
||||
toSendRequest = mtpRequestData::prepare(containerSize, containerSize + 3 * toSend.size()); // prepare container + each in invoke after
|
||||
@@ -1080,7 +1080,7 @@ void ConnectionPrivate::connectToServer(bool afterConfig) {
|
||||
sessionData->connectionOptions());
|
||||
hasKey = (sessionData->getKey() != nullptr);
|
||||
}
|
||||
auto bareDc = bareDcId(_shiftedDcId);
|
||||
auto bareDc = BareDcId(_shiftedDcId);
|
||||
_dcType = _instance->dcOptions()->dcType(_shiftedDcId);
|
||||
|
||||
// Use media_only addresses only if key for this dc is already created.
|
||||
@@ -2570,7 +2570,7 @@ void ConnectionPrivate::pqAnswered() {
|
||||
}
|
||||
|
||||
auto rsaKey = internal::RSAPublicKey();
|
||||
if (!_instance->dcOptions()->getDcRSAKey(bareDcId(_shiftedDcId), res_pq.c_resPQ().vserver_public_key_fingerprints.v, &rsaKey)) {
|
||||
if (!_instance->dcOptions()->getDcRSAKey(BareDcId(_shiftedDcId), res_pq.c_resPQ().vserver_public_key_fingerprints.v, &rsaKey)) {
|
||||
if (_dcType == DcType::Cdn) {
|
||||
LOG(("Warning: CDN public RSA key not found"));
|
||||
requestCDNConfig();
|
||||
@@ -2587,7 +2587,7 @@ void ConnectionPrivate::pqAnswered() {
|
||||
auto &pq = res_pq_data.vpq.v;
|
||||
auto p = QByteArray();
|
||||
auto q = QByteArray();
|
||||
if (!MTP::internal::parsePQ(pq, p, q)) {
|
||||
if (!internal::parsePQ(pq, p, q)) {
|
||||
LOG(("AuthKey Error: could not factor pq!"));
|
||||
DEBUG_LOG(("AuthKey Error: problematic pq: %1").arg(Logs::mb(pq.constData(), pq.length()).str()));
|
||||
return restart();
|
||||
@@ -2624,7 +2624,7 @@ void ConnectionPrivate::pqAnswered() {
|
||||
|
||||
bytes::vector ConnectionPrivate::encryptPQInnerRSA(
|
||||
const MTPP_Q_inner_data &data,
|
||||
const MTP::internal::RSAPublicKey &key) {
|
||||
const internal::RSAPublicKey &key) {
|
||||
auto p_q_inner_size = data.innerLength();
|
||||
auto encSize = (p_q_inner_size >> 2) + 6;
|
||||
if (encSize >= 65) {
|
||||
@@ -2880,7 +2880,7 @@ void ConnectionPrivate::dhClientParamsAnswered() {
|
||||
uint64 salt1 = _authKeyData->new_nonce.l.l, salt2 = _authKeyData->server_nonce.l, serverSalt = salt1 ^ salt2;
|
||||
sessionData->setSalt(serverSalt);
|
||||
|
||||
auto authKey = std::make_shared<AuthKey>(AuthKey::Type::Generated, bareDcId(_shiftedDcId), _authKeyStrings->auth_key);
|
||||
auto authKey = std::make_shared<AuthKey>(AuthKey::Type::Generated, BareDcId(_shiftedDcId), _authKeyStrings->auth_key);
|
||||
|
||||
DEBUG_LOG(("AuthKey Info: auth key gen succeed, id: %1, server salt: %2").arg(authKey->keyId()).arg(serverSalt));
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ private:
|
||||
|
||||
bool setState(int32 state, int32 ifState = Connection::UpdateAlways);
|
||||
|
||||
bytes::vector encryptPQInnerRSA(const MTPP_Q_inner_data &data, const MTP::internal::RSAPublicKey &key);
|
||||
bytes::vector encryptPQInnerRSA(const MTPP_Q_inner_data &data, const internal::RSAPublicKey &key);
|
||||
std::string encryptClientDHInner(const MTPClient_DH_Inner_Data &data);
|
||||
void appendTestConnection(
|
||||
DcOptions::Variants::Protocol protocol,
|
||||
|
||||
@@ -9,6 +9,36 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
#include "zlib.h"
|
||||
|
||||
Exception::Exception(const QString &msg) noexcept : _msg(msg.toUtf8()) {
|
||||
LOG(("Exception: %1").arg(msg));
|
||||
}
|
||||
|
||||
mtpErrorUnexpected::mtpErrorUnexpected(
|
||||
mtpTypeId typeId,
|
||||
const QString &type) noexcept
|
||||
: Exception(
|
||||
QString("MTP Unexpected type id #%1 read in %2"
|
||||
).arg(uint32(typeId), 0, 16
|
||||
).arg(type)) {
|
||||
}
|
||||
|
||||
mtpErrorInsufficient::mtpErrorInsufficient() noexcept
|
||||
: Exception("MTP Insufficient bytes in input buffer") {
|
||||
}
|
||||
|
||||
mtpErrorBadTypeId::mtpErrorBadTypeId(
|
||||
mtpTypeId typeId,
|
||||
const QString &type) noexcept
|
||||
: Exception(
|
||||
QString("MTP Bad type id #%1 passed to constructor of %2"
|
||||
).arg(uint32(typeId), 0, 16
|
||||
).arg(type)) {
|
||||
}
|
||||
|
||||
const char *Exception::what() const noexcept {
|
||||
return _msg.constData();
|
||||
}
|
||||
|
||||
uint32 MTPstring::innerLength() const {
|
||||
uint32 l = v.length();
|
||||
if (l < 254) {
|
||||
|
||||
@@ -7,9 +7,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <gsl/gsl>
|
||||
#include <QtCore/QVector>
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QByteArray>
|
||||
#include <rpl/details/callable.h>
|
||||
#include "core/basic_types.h"
|
||||
#include "base/match_method.h"
|
||||
#include "base/flags.h"
|
||||
#include "base/bytes.h"
|
||||
#include "base/algorithm.h"
|
||||
#include "base/assertion.h"
|
||||
|
||||
namespace MTP {
|
||||
|
||||
@@ -18,6 +26,29 @@ namespace MTP {
|
||||
using DcId = int32;
|
||||
using ShiftedDcId = int32;
|
||||
|
||||
constexpr auto kDcShift = ShiftedDcId(10000);
|
||||
constexpr auto kConfigDcShift = 0x01;
|
||||
constexpr auto kLogoutDcShift = 0x02;
|
||||
constexpr auto kUpdaterDcShift = 0x03;
|
||||
constexpr auto kExportDcShift = 0x04;
|
||||
constexpr auto kExportMediaDcShift = 0x05;
|
||||
constexpr auto kMaxMediaDcCount = 0x10;
|
||||
constexpr auto kBaseDownloadDcShift = 0x10;
|
||||
constexpr auto kBaseUploadDcShift = 0x20;
|
||||
constexpr auto kDestroyKeyStartDcShift = 0x100;
|
||||
|
||||
constexpr DcId BareDcId(ShiftedDcId shiftedDcId) {
|
||||
return (shiftedDcId % kDcShift);
|
||||
}
|
||||
|
||||
constexpr ShiftedDcId ShiftDcId(DcId dcId, int value) {
|
||||
return dcId + kDcShift * value;
|
||||
}
|
||||
|
||||
constexpr int GetDcIdShift(ShiftedDcId shiftedDcId) {
|
||||
return shiftedDcId / kDcShift;
|
||||
}
|
||||
|
||||
} // namespace MTP
|
||||
|
||||
using mtpPrime = int32;
|
||||
@@ -47,7 +78,7 @@ class mtpRequestData : public mtpBuffer {
|
||||
public:
|
||||
// in toSend: = 0 - must send in container, > 0 - can send without container
|
||||
// in haveSent: = 0 - container with msgIds, > 0 - when was sent
|
||||
TimeMs msDate = 0;
|
||||
int64 msDate = 0;
|
||||
|
||||
mtpRequestId requestId = 0;
|
||||
mtpRequest after;
|
||||
@@ -103,24 +134,32 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
class Exception : public std::exception {
|
||||
public:
|
||||
explicit Exception(const QString &msg) noexcept;
|
||||
|
||||
const char *what() const noexcept override;
|
||||
|
||||
private:
|
||||
QByteArray _msg;
|
||||
|
||||
};
|
||||
|
||||
class mtpErrorUnexpected : public Exception {
|
||||
public:
|
||||
mtpErrorUnexpected(mtpTypeId typeId, const QString &type) : Exception(QString("MTP Unexpected type id #%1 read in %2").arg(uint32(typeId), 0, 16).arg(type), false) { // maybe api changed?..
|
||||
}
|
||||
mtpErrorUnexpected(mtpTypeId typeId, const QString &type) noexcept;
|
||||
|
||||
};
|
||||
|
||||
class mtpErrorInsufficient : public Exception {
|
||||
public:
|
||||
mtpErrorInsufficient() : Exception("MTP Insufficient bytes in input buffer") {
|
||||
}
|
||||
mtpErrorInsufficient() noexcept;
|
||||
|
||||
};
|
||||
|
||||
class mtpErrorBadTypeId : public Exception {
|
||||
public:
|
||||
mtpErrorBadTypeId(mtpTypeId typeId, const QString &type) : Exception(QString("MTP Bad type id %1 passed to constructor of %2").arg(typeId).arg(type)) {
|
||||
}
|
||||
mtpErrorBadTypeId(mtpTypeId typeId, const QString &type) noexcept;
|
||||
|
||||
};
|
||||
|
||||
@@ -190,6 +229,7 @@ protected:
|
||||
template <typename DataType>
|
||||
const DataType &queryData() const {
|
||||
Expects(_data != nullptr);
|
||||
|
||||
return static_cast<const DataType &>(*_data);
|
||||
}
|
||||
|
||||
@@ -656,8 +696,8 @@ public:
|
||||
MTPvector() = default;
|
||||
|
||||
uint32 innerLength() const {
|
||||
uint32 result(sizeof(uint32));
|
||||
for_const (auto &item, v) {
|
||||
auto result = uint32(sizeof(uint32));
|
||||
for (const auto &item : v) {
|
||||
result += item.innerLength();
|
||||
}
|
||||
return result;
|
||||
@@ -678,7 +718,7 @@ public:
|
||||
}
|
||||
void write(mtpBuffer &to) const {
|
||||
to.push_back(v.size());
|
||||
for_const (auto &item, v) {
|
||||
for (const auto &item : v) {
|
||||
item.write(to);
|
||||
}
|
||||
}
|
||||
@@ -730,7 +770,11 @@ inline bool operator!=(const MTPvector<T> &a, const MTPvector<T> &b) {
|
||||
// Human-readable text serialization
|
||||
|
||||
struct MTPStringLogger {
|
||||
MTPStringLogger() : p(new char[MTPDebugBufferSize]), size(0), alloced(MTPDebugBufferSize) {
|
||||
static constexpr auto kBufferSize = 1024 * 1024; // 1 mb start size
|
||||
|
||||
MTPStringLogger()
|
||||
: p(new char[kBufferSize])
|
||||
, alloced(kBufferSize) {
|
||||
}
|
||||
~MTPStringLogger() {
|
||||
delete[] p;
|
||||
@@ -767,15 +811,20 @@ struct MTPStringLogger {
|
||||
if (size + add <= alloced) return;
|
||||
|
||||
int32 newsize = size + add;
|
||||
if (newsize % MTPDebugBufferSize) newsize += MTPDebugBufferSize - (newsize % MTPDebugBufferSize);
|
||||
if (newsize % kBufferSize) {
|
||||
newsize += kBufferSize - (newsize % kBufferSize);
|
||||
}
|
||||
char *b = new char[newsize];
|
||||
memcpy(b, p, size);
|
||||
alloced = newsize;
|
||||
delete[] p;
|
||||
p = b;
|
||||
}
|
||||
char *p;
|
||||
int32 size, alloced;
|
||||
|
||||
char *p = nullptr;
|
||||
int size = 0;
|
||||
int alloced = 0;
|
||||
|
||||
};
|
||||
|
||||
void mtpTextSerializeType(MTPStringLogger &to, const mtpPrime *&from, const mtpPrime *end, mtpPrime cons = 0, uint32 level = 0, mtpPrime vcons = 0);
|
||||
|
||||
@@ -233,7 +233,7 @@ void DcOptions::constructAddOne(
|
||||
int port,
|
||||
const bytes::vector &secret) {
|
||||
WriteLocker lock(this);
|
||||
applyOneGuarded(bareDcId(id), flags, ip, port, secret);
|
||||
applyOneGuarded(BareDcId(id), flags, ip, port, secret);
|
||||
}
|
||||
|
||||
bool DcOptions::applyOneGuarded(
|
||||
@@ -525,7 +525,7 @@ DcType DcOptions::dcType(ShiftedDcId shiftedDcId) const {
|
||||
return DcType::Temporary;
|
||||
}
|
||||
ReadLocker lock(this);
|
||||
if (_cdnDcIds.find(bareDcId(shiftedDcId)) != _cdnDcIds.cend()) {
|
||||
if (_cdnDcIds.find(BareDcId(shiftedDcId)) != _cdnDcIds.cend()) {
|
||||
return DcType::Cdn;
|
||||
}
|
||||
if (isDownloadDcId(shiftedDcId)) {
|
||||
@@ -642,7 +642,7 @@ void DcOptions::computeCdnDcIds() {
|
||||
for (auto &item : _data) {
|
||||
Assert(!item.second.empty());
|
||||
if (item.second.front().flags & Flag::f_cdn) {
|
||||
_cdnDcIds.insert(bareDcId(item.first));
|
||||
_cdnDcIds.insert(BareDcId(item.first));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -675,7 +675,7 @@ bool DcOptions::loadFromFile(const QString &path) {
|
||||
auto ip = components[1];
|
||||
auto port = components[2].toInt();
|
||||
auto host = QHostAddress();
|
||||
if (dcId <= 0 || dcId >= internal::kDcShift || !host.setAddress(ip) || port <= 0) {
|
||||
if (dcId <= 0 || dcId >= kDcShift || !host.setAddress(ip) || port <= 0) {
|
||||
return error();
|
||||
}
|
||||
auto flags = Flags(0);
|
||||
|
||||
@@ -18,15 +18,6 @@ bool paused();
|
||||
void pause();
|
||||
void unpause();
|
||||
|
||||
constexpr auto kDcShift = ShiftedDcId(10000);
|
||||
constexpr auto kConfigDcShift = 0x01;
|
||||
constexpr auto kLogoutDcShift = 0x02;
|
||||
constexpr auto kUpdaterDcShift = 0x03;
|
||||
constexpr auto kMaxMediaDcCount = 0x10;
|
||||
constexpr auto kBaseDownloadDcShift = 0x10;
|
||||
constexpr auto kBaseUploadDcShift = 0x20;
|
||||
constexpr auto kDestroyKeyStartDcShift = 0x100;
|
||||
|
||||
} // namespace internal
|
||||
|
||||
class PauseHolder {
|
||||
@@ -53,29 +44,19 @@ private:
|
||||
|
||||
};
|
||||
|
||||
constexpr DcId bareDcId(ShiftedDcId shiftedDcId) {
|
||||
return (shiftedDcId % internal::kDcShift);
|
||||
}
|
||||
constexpr ShiftedDcId shiftDcId(DcId dcId, int value) {
|
||||
return dcId + internal::kDcShift * value;
|
||||
}
|
||||
constexpr int getDcIdShift(ShiftedDcId shiftedDcId) {
|
||||
return shiftedDcId / internal::kDcShift;
|
||||
}
|
||||
|
||||
// send(MTPhelp_GetConfig(), MTP::configDcId(dc)) - for dc enumeration
|
||||
constexpr ShiftedDcId configDcId(DcId dcId) {
|
||||
return shiftDcId(dcId, internal::kConfigDcShift);
|
||||
return ShiftDcId(dcId, kConfigDcShift);
|
||||
}
|
||||
|
||||
// send(MTPauth_LogOut(), MTP::logoutDcId(dc)) - for logout of guest dcs enumeration
|
||||
constexpr ShiftedDcId logoutDcId(DcId dcId) {
|
||||
return shiftDcId(dcId, internal::kLogoutDcShift);
|
||||
return ShiftDcId(dcId, kLogoutDcShift);
|
||||
}
|
||||
|
||||
// send(MTPupload_GetFile(), MTP::updaterDcId(dc)) - for autoupdater
|
||||
constexpr ShiftedDcId updaterDcId(DcId dcId) {
|
||||
return shiftDcId(dcId, internal::kUpdaterDcShift);
|
||||
return ShiftDcId(dcId, kUpdaterDcShift);
|
||||
}
|
||||
|
||||
constexpr auto kDownloadSessionsCount = 2;
|
||||
@@ -84,8 +65,8 @@ constexpr auto kUploadSessionsCount = 2;
|
||||
namespace internal {
|
||||
|
||||
constexpr ShiftedDcId downloadDcId(DcId dcId, int index) {
|
||||
static_assert(kDownloadSessionsCount < internal::kMaxMediaDcCount, "Too large MTPDownloadSessionsCount!");
|
||||
return shiftDcId(dcId, internal::kBaseDownloadDcShift + index);
|
||||
static_assert(kDownloadSessionsCount < kMaxMediaDcCount, "Too large MTPDownloadSessionsCount!");
|
||||
return ShiftDcId(dcId, kBaseDownloadDcShift + index);
|
||||
};
|
||||
|
||||
} // namespace internal
|
||||
@@ -97,7 +78,7 @@ inline ShiftedDcId downloadDcId(DcId dcId, int index) {
|
||||
}
|
||||
|
||||
inline constexpr bool isDownloadDcId(ShiftedDcId shiftedDcId) {
|
||||
return (shiftedDcId >= internal::downloadDcId(0, 0)) && (shiftedDcId < internal::downloadDcId(0, kDownloadSessionsCount - 1) + internal::kDcShift);
|
||||
return (shiftedDcId >= internal::downloadDcId(0, 0)) && (shiftedDcId < internal::downloadDcId(0, kDownloadSessionsCount - 1) + kDcShift);
|
||||
}
|
||||
|
||||
inline bool isCdnDc(MTPDdcOption::Flags flags) {
|
||||
@@ -105,43 +86,44 @@ inline bool isCdnDc(MTPDdcOption::Flags flags) {
|
||||
}
|
||||
|
||||
inline bool isTemporaryDcId(ShiftedDcId shiftedDcId) {
|
||||
auto dcId = bareDcId(shiftedDcId);
|
||||
auto dcId = BareDcId(shiftedDcId);
|
||||
return (dcId >= Instance::Config::kTemporaryMainDc);
|
||||
}
|
||||
|
||||
inline DcId getRealIdFromTemporaryDcId(ShiftedDcId shiftedDcId) {
|
||||
auto dcId = bareDcId(shiftedDcId);
|
||||
auto dcId = BareDcId(shiftedDcId);
|
||||
return (dcId >= Instance::Config::kTemporaryMainDc) ? (dcId - Instance::Config::kTemporaryMainDc) : 0;
|
||||
}
|
||||
|
||||
inline DcId getTemporaryIdFromRealDcId(ShiftedDcId shiftedDcId) {
|
||||
auto dcId = bareDcId(shiftedDcId);
|
||||
auto dcId = BareDcId(shiftedDcId);
|
||||
return (dcId < Instance::Config::kTemporaryMainDc) ? (dcId + Instance::Config::kTemporaryMainDc) : 0;
|
||||
}
|
||||
|
||||
namespace internal {
|
||||
|
||||
constexpr ShiftedDcId uploadDcId(DcId dcId, int index) {
|
||||
static_assert(kUploadSessionsCount < internal::kMaxMediaDcCount, "Too large MTPUploadSessionsCount!");
|
||||
return shiftDcId(dcId, internal::kBaseUploadDcShift + index);
|
||||
static_assert(kUploadSessionsCount < kMaxMediaDcCount, "Too large MTPUploadSessionsCount!");
|
||||
return ShiftDcId(dcId, kBaseUploadDcShift + index);
|
||||
};
|
||||
|
||||
} // namespace internal
|
||||
|
||||
// send(req, callbacks, MTP::uploadDcId(index)) - for upload shifted dc id
|
||||
// uploading always to the main dc so bareDcId == 0
|
||||
// uploading always to the main dc so BareDcId(result) == 0
|
||||
inline ShiftedDcId uploadDcId(int index) {
|
||||
Expects(index >= 0 && index < kUploadSessionsCount);
|
||||
|
||||
return internal::uploadDcId(0, index);
|
||||
};
|
||||
|
||||
constexpr bool isUploadDcId(ShiftedDcId shiftedDcId) {
|
||||
return (shiftedDcId >= internal::uploadDcId(0, 0)) && (shiftedDcId < internal::uploadDcId(0, kUploadSessionsCount - 1) + internal::kDcShift);
|
||||
return (shiftedDcId >= internal::uploadDcId(0, 0)) && (shiftedDcId < internal::uploadDcId(0, kUploadSessionsCount - 1) + kDcShift);
|
||||
}
|
||||
|
||||
inline ShiftedDcId destroyKeyNextDcId(ShiftedDcId shiftedDcId) {
|
||||
auto shift = getDcIdShift(shiftedDcId);
|
||||
return shiftDcId(bareDcId(shiftedDcId), shift ? (shift + 1) : internal::kDestroyKeyStartDcShift);
|
||||
const auto shift = GetDcIdShift(shiftedDcId);
|
||||
return ShiftDcId(BareDcId(shiftedDcId), shift ? (shift + 1) : kDestroyKeyStartDcShift);
|
||||
}
|
||||
|
||||
enum {
|
||||
|
||||
@@ -76,10 +76,19 @@ public:
|
||||
std::unique_ptr<internal::Connection> &&connection);
|
||||
void connectionFinished(internal::Connection *connection);
|
||||
|
||||
void registerRequest(mtpRequestId requestId, ShiftedDcId dcWithShift);
|
||||
void sendRequest(
|
||||
mtpRequestId requestId,
|
||||
mtpRequest &&request,
|
||||
RPCResponseHandler &&callbacks,
|
||||
ShiftedDcId shiftedDcId,
|
||||
TimeMs msCanWait,
|
||||
bool needsLayer,
|
||||
mtpRequestId afterRequestId);
|
||||
void registerRequest(mtpRequestId requestId, ShiftedDcId shiftedDcId);
|
||||
void unregisterRequest(mtpRequestId requestId);
|
||||
mtpRequestId storeRequest(
|
||||
mtpRequest &request,
|
||||
void storeRequest(
|
||||
mtpRequestId requestId,
|
||||
const mtpRequest &request,
|
||||
RPCResponseHandler &&callbacks);
|
||||
mtpRequest getRequest(mtpRequestId requestId);
|
||||
void clearCallbacksDelayed(std::vector<RPCCallbackClear> &&ids);
|
||||
@@ -87,8 +96,8 @@ public:
|
||||
bool hasCallbacks(mtpRequestId requestId);
|
||||
void globalCallback(const mtpPrime *from, const mtpPrime *end);
|
||||
|
||||
void onStateChange(ShiftedDcId dcWithShift, int32 state);
|
||||
void onSessionReset(ShiftedDcId dcWithShift);
|
||||
void onStateChange(ShiftedDcId shiftedDcId, int32 state);
|
||||
void onSessionReset(ShiftedDcId shiftedDcId);
|
||||
|
||||
// return true if need to clean request data
|
||||
bool rpcErrorOccured(mtpRequestId requestId, const RPCFailHandlerPtr &onFail, const RPCError &err);
|
||||
@@ -102,7 +111,7 @@ public:
|
||||
void setSessionResetHandler(Fn<void(ShiftedDcId shiftedDcId)> handler);
|
||||
void clearGlobalHandlers();
|
||||
|
||||
internal::Session *getSession(ShiftedDcId shiftedDcId);
|
||||
not_null<internal::Session*> getSession(ShiftedDcId shiftedDcId);
|
||||
|
||||
bool isNormal() const {
|
||||
return (_mode == Instance::Mode::Normal);
|
||||
@@ -451,9 +460,9 @@ void Instance::Private::restart() {
|
||||
}
|
||||
|
||||
void Instance::Private::restart(ShiftedDcId shiftedDcId) {
|
||||
auto dcId = bareDcId(shiftedDcId);
|
||||
auto dcId = BareDcId(shiftedDcId);
|
||||
for (auto &session : _sessions) {
|
||||
if (bareDcId(session.second->getDcWithShift()) == dcId) {
|
||||
if (BareDcId(session.second->getDcWithShift()) == dcId) {
|
||||
session.second->restart();
|
||||
}
|
||||
}
|
||||
@@ -465,9 +474,9 @@ int32 Instance::Private::dcstate(ShiftedDcId shiftedDcId) {
|
||||
return _mainSession->getState();
|
||||
}
|
||||
|
||||
if (!bareDcId(shiftedDcId)) {
|
||||
if (!BareDcId(shiftedDcId)) {
|
||||
Assert(_mainSession != nullptr);
|
||||
shiftedDcId += bareDcId(_mainSession->getDcWithShift());
|
||||
shiftedDcId += BareDcId(_mainSession->getDcWithShift());
|
||||
}
|
||||
|
||||
auto it = _sessions.find(shiftedDcId);
|
||||
@@ -483,9 +492,9 @@ QString Instance::Private::dctransport(ShiftedDcId shiftedDcId) {
|
||||
Assert(_mainSession != nullptr);
|
||||
return _mainSession->transport();
|
||||
}
|
||||
if (!bareDcId(shiftedDcId)) {
|
||||
if (!BareDcId(shiftedDcId)) {
|
||||
Assert(_mainSession != nullptr);
|
||||
shiftedDcId += bareDcId(_mainSession->getDcWithShift());
|
||||
shiftedDcId += BareDcId(_mainSession->getDcWithShift());
|
||||
}
|
||||
|
||||
auto it = _sessions.find(shiftedDcId);
|
||||
@@ -497,14 +506,13 @@ QString Instance::Private::dctransport(ShiftedDcId shiftedDcId) {
|
||||
}
|
||||
|
||||
void Instance::Private::ping() {
|
||||
if (auto session = getSession(0)) {
|
||||
session->ping();
|
||||
}
|
||||
getSession(0)->ping();
|
||||
}
|
||||
|
||||
void Instance::Private::cancel(mtpRequestId requestId) {
|
||||
if (!requestId) return;
|
||||
|
||||
DEBUG_LOG(("MTP Info: Cancel request %1.").arg(requestId));
|
||||
const auto shiftedDcId = queryRequestByDc(requestId);
|
||||
auto msgId = mtpMsgId(0);
|
||||
{
|
||||
@@ -517,27 +525,23 @@ void Instance::Private::cancel(mtpRequestId requestId) {
|
||||
}
|
||||
unregisterRequest(requestId);
|
||||
if (shiftedDcId) {
|
||||
if (const auto session = getSession(qAbs(*shiftedDcId))) {
|
||||
session->cancel(requestId, msgId);
|
||||
}
|
||||
const auto session = getSession(qAbs(*shiftedDcId));
|
||||
session->cancel(requestId, msgId);
|
||||
}
|
||||
clearCallbacks(requestId);
|
||||
}
|
||||
|
||||
int32 Instance::Private::state(mtpRequestId requestId) { // < 0 means waiting for such count of ms
|
||||
// result < 0 means waiting for such count of ms.
|
||||
int32 Instance::Private::state(mtpRequestId requestId) {
|
||||
if (requestId > 0) {
|
||||
if (const auto shiftedDcId = queryRequestByDc(requestId)) {
|
||||
if (auto session = getSession(qAbs(*shiftedDcId))) {
|
||||
return session->requestState(requestId);
|
||||
}
|
||||
return MTP::RequestConnecting;
|
||||
const auto session = getSession(qAbs(*shiftedDcId));
|
||||
return session->requestState(requestId);
|
||||
}
|
||||
return MTP::RequestSent;
|
||||
}
|
||||
if (auto session = getSession(-requestId)) {
|
||||
return session->requestState(0);
|
||||
}
|
||||
return MTP::RequestConnecting;
|
||||
const auto session = getSession(-requestId);
|
||||
return session->requestState(0);
|
||||
}
|
||||
|
||||
void Instance::Private::killSession(ShiftedDcId shiftedDcId) {
|
||||
@@ -580,7 +584,7 @@ void Instance::Private::stopSession(ShiftedDcId shiftedDcId) {
|
||||
|
||||
void Instance::Private::reInitConnection(DcId dcId) {
|
||||
for (auto &session : _sessions) {
|
||||
if (bareDcId(session.second->getDcWithShift()) == dcId) {
|
||||
if (BareDcId(session.second->getDcWithShift()) == dcId) {
|
||||
session.second->reInitConnection();
|
||||
}
|
||||
}
|
||||
@@ -629,7 +633,7 @@ bool Instance::Private::logoutGuestDone(mtpRequestId requestId) {
|
||||
std::shared_ptr<internal::Dcenter> Instance::Private::getDcById(ShiftedDcId shiftedDcId) {
|
||||
auto it = _dcenters.find(shiftedDcId);
|
||||
if (it == _dcenters.cend()) {
|
||||
auto dcId = bareDcId(shiftedDcId);
|
||||
auto dcId = BareDcId(shiftedDcId);
|
||||
if (isTemporaryDcId(dcId)) {
|
||||
if (auto realDcId = getRealIdFromTemporaryDcId(dcId)) {
|
||||
dcId = realDcId;
|
||||
@@ -806,7 +810,7 @@ base::optional<ShiftedDcId> Instance::Private::changeRequestByDc(
|
||||
if (it->second < 0) {
|
||||
it->second = -newdc;
|
||||
} else {
|
||||
it->second = shiftDcId(newdc, getDcIdShift(it->second));
|
||||
it->second = ShiftDcId(newdc, GetDcIdShift(it->second));
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
@@ -837,9 +841,8 @@ void Instance::Private::checkDelayedRequests() {
|
||||
}
|
||||
request = it->second;
|
||||
}
|
||||
if (auto session = getSession(qAbs(dcWithShift))) {
|
||||
session->sendPrepared(request);
|
||||
}
|
||||
const auto session = getSession(qAbs(dcWithShift));
|
||||
session->sendPrepared(request);
|
||||
}
|
||||
|
||||
if (!_delayedRequests.empty()) {
|
||||
@@ -847,14 +850,43 @@ void Instance::Private::checkDelayedRequests() {
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::Private::sendRequest(
|
||||
mtpRequestId requestId,
|
||||
mtpRequest &&request,
|
||||
RPCResponseHandler &&callbacks,
|
||||
ShiftedDcId shiftedDcId,
|
||||
TimeMs msCanWait,
|
||||
bool needsLayer,
|
||||
mtpRequestId afterRequestId) {
|
||||
const auto session = getSession(shiftedDcId);
|
||||
|
||||
request->requestId = requestId;
|
||||
storeRequest(requestId, request, std::move(callbacks));
|
||||
|
||||
const auto toMainDc = (shiftedDcId == 0);
|
||||
const auto realShiftedDcId = session->getDcWithShift();
|
||||
const auto signedDcId = toMainDc ? -realShiftedDcId : realShiftedDcId;
|
||||
registerRequest(requestId, signedDcId);
|
||||
|
||||
if (afterRequestId) {
|
||||
request->after = getRequest(afterRequestId);
|
||||
}
|
||||
request->msDate = getms(true); // > 0 - can send without container
|
||||
request->needsLayer = needsLayer;
|
||||
|
||||
session->sendPrepared(request, msCanWait);
|
||||
}
|
||||
|
||||
void Instance::Private::registerRequest(
|
||||
mtpRequestId requestId,
|
||||
ShiftedDcId dcWithShift) {
|
||||
ShiftedDcId shiftedDcId) {
|
||||
QMutexLocker locker(&_requestByDcLock);
|
||||
_requestsByDc.emplace(requestId, dcWithShift);
|
||||
_requestsByDc[requestId] = shiftedDcId;
|
||||
}
|
||||
|
||||
void Instance::Private::unregisterRequest(mtpRequestId requestId) {
|
||||
DEBUG_LOG(("MTP Info: unregistering request %1.").arg(requestId));
|
||||
|
||||
_requestsDelays.erase(requestId);
|
||||
|
||||
{
|
||||
@@ -866,11 +898,10 @@ void Instance::Private::unregisterRequest(mtpRequestId requestId) {
|
||||
_requestsByDc.erase(requestId);
|
||||
}
|
||||
|
||||
mtpRequestId Instance::Private::storeRequest(
|
||||
mtpRequest &request,
|
||||
void Instance::Private::storeRequest(
|
||||
mtpRequestId requestId,
|
||||
const mtpRequest &request,
|
||||
RPCResponseHandler &&callbacks) {
|
||||
const auto requestId = reqid();
|
||||
request->requestId = requestId;
|
||||
if (callbacks.onDone || callbacks.onFail) {
|
||||
QMutexLocker locker(&_parserMapLock);
|
||||
_parserMap.emplace(requestId, std::move(callbacks));
|
||||
@@ -879,7 +910,6 @@ mtpRequestId Instance::Private::storeRequest(
|
||||
QWriteLocker locker(&_requestMapLock);
|
||||
_requestMap.emplace(requestId, request);
|
||||
}
|
||||
return requestId;
|
||||
}
|
||||
|
||||
mtpRequest Instance::Private::getRequest(mtpRequestId requestId) {
|
||||
@@ -941,12 +971,13 @@ void Instance::Private::clearCallbacks(
|
||||
for (const auto &clearRequest : ids) {
|
||||
if (Logs::DebugEnabled()) {
|
||||
QMutexLocker locker(&_parserMapLock);
|
||||
if (_parserMap.find(clearRequest.requestId) != _parserMap.end()) {
|
||||
DEBUG_LOG(("RPC Info: "
|
||||
"clearing delayed callback %1, error code %2"
|
||||
).arg(clearRequest.requestId
|
||||
).arg(clearRequest.errorCode));
|
||||
}
|
||||
const auto hasParsers = (_parserMap.find(clearRequest.requestId)
|
||||
!= _parserMap.end());
|
||||
DEBUG_LOG(("RPC Info: "
|
||||
"clearing delayed callback %1, error code %2, parsers: %3"
|
||||
).arg(clearRequest.requestId
|
||||
).arg(clearRequest.errorCode
|
||||
).arg(Logs::b(hasParsers)));
|
||||
}
|
||||
clearCallbacks(clearRequest.requestId, clearRequest.errorCode);
|
||||
unregisterRequest(clearRequest.requestId);
|
||||
@@ -969,35 +1000,42 @@ void Instance::Private::execCallback(
|
||||
}
|
||||
}
|
||||
if (h.onDone || h.onFail) {
|
||||
const auto handleError = [&](const MTPRpcError &error) {
|
||||
const auto wrapped = RPCError(error);
|
||||
DEBUG_LOG(("RPC Info: "
|
||||
"error received, code %1, type %2, description: %3"
|
||||
).arg(wrapped.code()
|
||||
).arg(wrapped.type()
|
||||
).arg(wrapped.description()));
|
||||
if (rpcErrorOccured(requestId, h, wrapped)) {
|
||||
unregisterRequest(requestId);
|
||||
} else {
|
||||
QMutexLocker locker(&_parserMapLock);
|
||||
_parserMap.emplace(requestId, h);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
if (from >= end) throw mtpErrorInsufficient();
|
||||
|
||||
if (*from == mtpc_rpc_error) {
|
||||
auto mtpError = MTPRpcError();
|
||||
mtpError.read(from, end);
|
||||
auto error = RPCError(mtpError);
|
||||
DEBUG_LOG(("RPC Info: error received, code %1, type %2, description: %3").arg(error.code()).arg(error.type()).arg(error.description()));
|
||||
if (!rpcErrorOccured(requestId, h, error)) {
|
||||
QMutexLocker locker(&_parserMapLock);
|
||||
_parserMap.emplace(requestId, h);
|
||||
return;
|
||||
}
|
||||
auto error = MTPRpcError();
|
||||
error.read(from, end);
|
||||
handleError(error);
|
||||
} else {
|
||||
if (h.onDone) {
|
||||
(*h.onDone)(requestId, from, end);
|
||||
}
|
||||
unregisterRequest(requestId);
|
||||
}
|
||||
} catch (Exception &e) {
|
||||
if (!rpcErrorOccured(requestId, h, internal::rpcClientError("RESPONSE_PARSE_FAILED", QString("exception text: ") + e.what()))) {
|
||||
QMutexLocker locker(&_parserMapLock);
|
||||
_parserMap.emplace(requestId, h);
|
||||
return;
|
||||
}
|
||||
handleError(internal::rpcClientError(
|
||||
"RESPONSE_PARSE_FAILED",
|
||||
QString("exception text: ") + e.what()));
|
||||
}
|
||||
} else {
|
||||
DEBUG_LOG(("RPC Info: parser not found for %1").arg(requestId));
|
||||
unregisterRequest(requestId);
|
||||
}
|
||||
unregisterRequest(requestId);
|
||||
}
|
||||
|
||||
bool Instance::Private::hasCallbacks(mtpRequestId requestId) {
|
||||
@@ -1054,7 +1092,7 @@ void Instance::Private::importDone(const MTPauth_Authorization &result, mtpReque
|
||||
//}
|
||||
return;
|
||||
}
|
||||
auto newdc = bareDcId(*shiftedDcId);
|
||||
auto newdc = BareDcId(*shiftedDcId);
|
||||
|
||||
DEBUG_LOG(("MTP Info: auth import to dc %1 succeeded").arg(newdc));
|
||||
|
||||
@@ -1075,9 +1113,8 @@ void Instance::Private::importDone(const MTPauth_Authorization &result, mtpReque
|
||||
_instance->setMainDcId(newdc);
|
||||
}
|
||||
DEBUG_LOG(("MTP Info: resending request %1 to dc %2 after import auth").arg(waitedRequestId).arg(*shiftedDcId));
|
||||
if (auto session = getSession(*shiftedDcId)) {
|
||||
session->sendPrepared(it->second);
|
||||
}
|
||||
const auto session = getSession(*shiftedDcId);
|
||||
session->sendPrepared(it->second);
|
||||
}
|
||||
waiters.clear();
|
||||
}
|
||||
@@ -1123,7 +1160,7 @@ bool Instance::Private::exportFail(const RPCError &error, mtpRequestId requestId
|
||||
|
||||
auto it = _authExportRequests.find(requestId);
|
||||
if (it != _authExportRequests.cend()) {
|
||||
_authWaiters[bareDcId(it->second)].clear();
|
||||
_authWaiters[BareDcId(it->second)].clear();
|
||||
}
|
||||
//
|
||||
// Don't log out on export/import problems, perhaps this is a server side error.
|
||||
@@ -1177,7 +1214,7 @@ bool Instance::Private::onErrorDefault(mtpRequestId requestId, const RPCError &e
|
||||
_instance->setMainDcId(newdcWithShift);
|
||||
}
|
||||
} else {
|
||||
newdcWithShift = shiftDcId(newdcWithShift, getDcIdShift(dcWithShift));
|
||||
newdcWithShift = ShiftDcId(newdcWithShift, GetDcIdShift(dcWithShift));
|
||||
}
|
||||
|
||||
auto request = mtpRequest();
|
||||
@@ -1190,12 +1227,11 @@ bool Instance::Private::onErrorDefault(mtpRequestId requestId, const RPCError &e
|
||||
}
|
||||
request = it->second;
|
||||
}
|
||||
if (auto session = getSession(newdcWithShift)) {
|
||||
registerRequest(
|
||||
requestId,
|
||||
(dcWithShift < 0) ? -newdcWithShift : newdcWithShift);
|
||||
session->sendPrepared(request);
|
||||
}
|
||||
const auto session = getSession(newdcWithShift);
|
||||
registerRequest(
|
||||
requestId,
|
||||
(dcWithShift < 0) ? -newdcWithShift : newdcWithShift);
|
||||
session->sendPrepared(request);
|
||||
return true;
|
||||
} else if (code < 0 || code >= 500 || (m = QRegularExpression("^FLOOD_WAIT_(\\d+)$").match(err)).hasMatch()) {
|
||||
if (!requestId) return false;
|
||||
@@ -1230,7 +1266,7 @@ bool Instance::Private::onErrorDefault(mtpRequestId requestId, const RPCError &e
|
||||
} else {
|
||||
LOG(("MTP Error: unauthorized request without dc info, requestId %1").arg(requestId));
|
||||
}
|
||||
auto newdc = bareDcId(qAbs(dcWithShift));
|
||||
auto newdc = BareDcId(qAbs(dcWithShift));
|
||||
if (!newdc || newdc == mainDcId() || !hasAuthorization()) {
|
||||
if (!badGuestDc && _globalHandler.onFail) {
|
||||
(*_globalHandler.onFail)(requestId, error); // auth failed in main dc
|
||||
@@ -1270,10 +1306,9 @@ bool Instance::Private::onErrorDefault(mtpRequestId requestId, const RPCError &e
|
||||
}
|
||||
if (!dcWithShift) return false;
|
||||
|
||||
if (auto session = getSession(qAbs(dcWithShift))) {
|
||||
request->needsLayer = true;
|
||||
session->sendPrepared(request);
|
||||
}
|
||||
const auto session = getSession(qAbs(dcWithShift));
|
||||
request->needsLayer = true;
|
||||
session->sendPrepared(request);
|
||||
return true;
|
||||
} else if (err == qstr("CONNECTION_LANG_CODE_INVALID")) {
|
||||
Lang::CurrentCloudManager().resetToDefault();
|
||||
@@ -1308,12 +1343,11 @@ bool Instance::Private::onErrorDefault(mtpRequestId requestId, const RPCError &e
|
||||
if (!dcWithShift) return false;
|
||||
|
||||
if (!request->after) {
|
||||
if (auto session = getSession(qAbs(dcWithShift))) {
|
||||
request->needsLayer = true;
|
||||
session->sendPrepared(request);
|
||||
}
|
||||
const auto session = getSession(qAbs(dcWithShift));
|
||||
request->needsLayer = true;
|
||||
session->sendPrepared(request);
|
||||
} else {
|
||||
auto newdc = bareDcId(qAbs(dcWithShift));
|
||||
auto newdc = BareDcId(qAbs(dcWithShift));
|
||||
auto &waiters(_authWaiters[newdc]);
|
||||
if (base::contains(waiters, request->after->requestId)) {
|
||||
if (!base::contains(waiters, requestId)) {
|
||||
@@ -1343,14 +1377,15 @@ bool Instance::Private::onErrorDefault(mtpRequestId requestId, const RPCError &e
|
||||
return false;
|
||||
}
|
||||
|
||||
internal::Session *Instance::Private::getSession(ShiftedDcId shiftedDcId) {
|
||||
not_null<internal::Session*> Instance::Private::getSession(
|
||||
ShiftedDcId shiftedDcId) {
|
||||
if (!shiftedDcId) {
|
||||
Assert(_mainSession != nullptr);
|
||||
return _mainSession;
|
||||
}
|
||||
if (!bareDcId(shiftedDcId)) {
|
||||
if (!BareDcId(shiftedDcId)) {
|
||||
Assert(_mainSession != nullptr);
|
||||
shiftedDcId += bareDcId(_mainSession->getDcWithShift());
|
||||
shiftedDcId += BareDcId(_mainSession->getDcWithShift());
|
||||
}
|
||||
|
||||
auto it = _sessions.find(shiftedDcId);
|
||||
@@ -1600,28 +1635,12 @@ void Instance::clearGlobalHandlers() {
|
||||
_private->clearGlobalHandlers();
|
||||
}
|
||||
|
||||
void Instance::onStateChange(ShiftedDcId dcWithShift, int32 state) {
|
||||
_private->onStateChange(dcWithShift, state);
|
||||
void Instance::onStateChange(ShiftedDcId shiftedDcId, int32 state) {
|
||||
_private->onStateChange(shiftedDcId, state);
|
||||
}
|
||||
|
||||
void Instance::onSessionReset(ShiftedDcId dcWithShift) {
|
||||
_private->onSessionReset(dcWithShift);
|
||||
}
|
||||
|
||||
void Instance::registerRequest(
|
||||
mtpRequestId requestId,
|
||||
ShiftedDcId dcWithShift) {
|
||||
_private->registerRequest(requestId, dcWithShift);
|
||||
}
|
||||
|
||||
mtpRequestId Instance::storeRequest(
|
||||
mtpRequest &request,
|
||||
RPCResponseHandler &&callbacks) {
|
||||
return _private->storeRequest(request, std::move(callbacks));
|
||||
}
|
||||
|
||||
mtpRequest Instance::getRequest(mtpRequestId requestId) {
|
||||
return _private->getRequest(requestId);
|
||||
void Instance::onSessionReset(ShiftedDcId shiftedDcId) {
|
||||
_private->onSessionReset(shiftedDcId);
|
||||
}
|
||||
|
||||
void Instance::clearCallbacksDelayed(std::vector<RPCCallbackClear> &&ids) {
|
||||
@@ -1655,29 +1674,27 @@ void Instance::scheduleKeyDestroy(ShiftedDcId shiftedDcId) {
|
||||
void Instance::onKeyDestroyed(qint32 shiftedDcId) {
|
||||
_private->completedKeyDestroy(shiftedDcId);
|
||||
}
|
||||
|
||||
mtpRequestId Instance::send(
|
||||
void Instance::sendRequest(
|
||||
mtpRequestId requestId,
|
||||
mtpRequest &&request,
|
||||
RPCResponseHandler &&callbacks,
|
||||
ShiftedDcId dcId,
|
||||
ShiftedDcId shiftedDcId,
|
||||
TimeMs msCanWait,
|
||||
mtpRequestId after) {
|
||||
if (const auto session = _private->getSession(dcId)) {
|
||||
return session->send(
|
||||
mtpRequestData::serialize(request),
|
||||
std::move(callbacks),
|
||||
msCanWait,
|
||||
true,
|
||||
!dcId,
|
||||
after);
|
||||
}
|
||||
return 0;
|
||||
bool needsLayer,
|
||||
mtpRequestId afterRequestId) {
|
||||
return _private->sendRequest(
|
||||
requestId,
|
||||
std::move(request),
|
||||
std::move(callbacks),
|
||||
shiftedDcId,
|
||||
msCanWait,
|
||||
needsLayer,
|
||||
afterRequestId);
|
||||
}
|
||||
|
||||
void Instance::sendAnything(ShiftedDcId dcId, TimeMs msCanWait) {
|
||||
if (const auto session = _private->getSession(dcId)) {
|
||||
session->sendAnything(msCanWait);
|
||||
}
|
||||
void Instance::sendAnything(ShiftedDcId shiftedDcId, TimeMs msCanWait) {
|
||||
const auto session = _private->getSession(shiftedDcId);
|
||||
session->sendAnything(msCanWait);
|
||||
}
|
||||
|
||||
Instance::~Instance() {
|
||||
|
||||
@@ -64,15 +64,18 @@ public:
|
||||
mtpRequestId send(
|
||||
const TRequest &request,
|
||||
RPCResponseHandler &&callbacks = {},
|
||||
ShiftedDcId dcId = 0,
|
||||
ShiftedDcId shiftedDcId = 0,
|
||||
TimeMs msCanWait = 0,
|
||||
mtpRequestId after = 0) {
|
||||
return send(
|
||||
mtpRequestId afterRequestId = 0) {
|
||||
const auto requestId = GetNextRequestId();
|
||||
sendSerialized(
|
||||
requestId,
|
||||
mtpRequestData::serialize(request),
|
||||
std::move(callbacks),
|
||||
dcId,
|
||||
shiftedDcId,
|
||||
msCanWait,
|
||||
after);
|
||||
afterRequestId);
|
||||
return requestId;
|
||||
}
|
||||
|
||||
template <typename TRequest>
|
||||
@@ -80,18 +83,52 @@ public:
|
||||
const TRequest &request,
|
||||
RPCDoneHandlerPtr &&onDone,
|
||||
RPCFailHandlerPtr &&onFail = nullptr,
|
||||
ShiftedDcId dc = 0,
|
||||
ShiftedDcId shiftedDcId = 0,
|
||||
TimeMs msCanWait = 0,
|
||||
mtpRequestId after = 0) {
|
||||
mtpRequestId afterRequestId = 0) {
|
||||
return send(
|
||||
request,
|
||||
RPCResponseHandler(std::move(onDone), std::move(onFail)),
|
||||
dc,
|
||||
shiftedDcId,
|
||||
msCanWait,
|
||||
after);
|
||||
afterRequestId);
|
||||
}
|
||||
|
||||
void sendAnything(ShiftedDcId dcId = 0, TimeMs msCanWait = 0);
|
||||
template <typename TRequest>
|
||||
mtpRequestId sendProtocolMessage(
|
||||
ShiftedDcId shiftedDcId,
|
||||
const TRequest &request) {
|
||||
const auto requestId = GetNextRequestId();
|
||||
sendRequest(
|
||||
requestId,
|
||||
mtpRequestData::serialize(request),
|
||||
{},
|
||||
shiftedDcId,
|
||||
0,
|
||||
false,
|
||||
0);
|
||||
return requestId;
|
||||
}
|
||||
|
||||
void sendSerialized(
|
||||
mtpRequestId requestId,
|
||||
mtpRequest &&request,
|
||||
RPCResponseHandler &&callbacks,
|
||||
ShiftedDcId shiftedDcId,
|
||||
TimeMs msCanWait,
|
||||
mtpRequestId afterRequestId) {
|
||||
const auto needsLayer = true;
|
||||
sendRequest(
|
||||
requestId,
|
||||
std::move(request),
|
||||
std::move(callbacks),
|
||||
shiftedDcId,
|
||||
msCanWait,
|
||||
needsLayer,
|
||||
afterRequestId);
|
||||
}
|
||||
|
||||
void sendAnything(ShiftedDcId shiftedDcId = 0, TimeMs msCanWait = 0);
|
||||
|
||||
void restart();
|
||||
void restart(ShiftedDcId shiftedDcId);
|
||||
@@ -116,14 +153,9 @@ public:
|
||||
void setSessionResetHandler(Fn<void(ShiftedDcId shiftedDcId)> handler);
|
||||
void clearGlobalHandlers();
|
||||
|
||||
void onStateChange(ShiftedDcId dcWithShift, int32 state);
|
||||
void onSessionReset(ShiftedDcId dcWithShift);
|
||||
void onStateChange(ShiftedDcId shiftedDcId, int32 state);
|
||||
void onSessionReset(ShiftedDcId shiftedDcId);
|
||||
|
||||
void registerRequest(mtpRequestId requestId, ShiftedDcId dcWithShift);
|
||||
mtpRequestId storeRequest(
|
||||
mtpRequest &request,
|
||||
RPCResponseHandler &&callbacks);
|
||||
mtpRequest getRequest(mtpRequestId requestId);
|
||||
void clearCallbacksDelayed(std::vector<RPCCallbackClear> &&ids);
|
||||
|
||||
void execCallback(mtpRequestId requestId, const mtpPrime *from, const mtpPrime *end);
|
||||
@@ -161,12 +193,14 @@ private slots:
|
||||
void onKeyDestroyed(qint32 shiftedDcId);
|
||||
|
||||
private:
|
||||
mtpRequestId send(
|
||||
void sendRequest(
|
||||
mtpRequestId requestId,
|
||||
mtpRequest &&request,
|
||||
RPCResponseHandler &&callbacks,
|
||||
ShiftedDcId dcId,
|
||||
ShiftedDcId shiftedDcId,
|
||||
TimeMs msCanWait,
|
||||
mtpRequestId after);
|
||||
bool needsLayer,
|
||||
mtpRequestId afterRequestId);
|
||||
|
||||
class Private;
|
||||
const std::unique_ptr<Private> _private;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user