Compare commits

...

217 Commits

Author SHA1 Message Date
Daniil Gentili d8319e39d3
Bugfix 2020-11-07 20:07:07 +01:00
Daniil Gentili 8164279427
Update 2020-10-27 20:53:13 +01:00
Daniil Gentili 806f50bc2d
Refactoring 2020-10-26 23:13:05 +01:00
Daniil Gentili d0189b1ef2
Add OGG OPUS parser 2020-10-26 21:38:23 +01:00
Daniil Gentili 47b808ca9b
Psalm improvements 2020-10-23 18:19:20 +02:00
Daniil Gentili f7ecafb086
Update example 2020-10-23 18:07:18 +02:00
Daniil Gentili 901dc39d93
Create new postgres pool 2020-10-23 18:05:29 +02:00
Daniil Gentili b0d25f53cb
Use prepared statements, and update to new postgres API 2020-10-23 00:14:40 +02:00
Daniil Gentili fb20b2d213
Provide size in secret chat files 2020-10-19 19:55:36 +02:00
Daniil Gentili dc165eb30a
Do not send layer notification every time 2020-10-19 19:08:32 +02:00
Daniil Gentili 34d46ea717
Merge 2020-10-19 18:49:46 +02:00
Daniil Gentili fb1df72a21
Secret chat improvements 2020-10-19 18:48:59 +02:00
Daniil Gentili 26a10f012a uploadEncryptedFile 2020-10-18 20:43:07 +02:00
Daniil Gentili bfc8009778
Remove useless stuff from example 2020-10-18 18:57:44 +02:00
Daniil Gentili 725265969a
Fallback to normal constructors when serializing secret chat TL 2020-10-18 18:50:38 +02:00
Daniil Gentili f516a59082
Properly clear up messages before resending a copy 2020-10-18 18:41:53 +02:00
Daniil Gentili a9bf26c9c0
Fix floodWait management 2020-10-18 18:21:04 +02:00
Daniil Gentili 73384f0bb4
Implement secret chat feeder loop 2020-10-18 18:08:39 +02:00
Daniil Gentili f03705e4aa
Various improvements 2020-10-18 16:02:08 +02:00
Daniil Gentili b0b850efe4
Decrease log verbosity for feeders 2020-10-18 15:50:14 +02:00
Daniil Gentili 8445455e64
Automatically upload files to @me with uploadMedia 2020-10-18 15:43:30 +02:00
Daniil Gentili ad85b0a6c0
Improvements for examples 2020-10-18 15:41:06 +02:00
Daniil Gentili 906a0ab888
Fix parallelization 2020-10-18 15:23:12 +02:00
Daniil Gentili bbc25e9aec
Psalm fixes 2020-10-18 15:04:43 +02:00
Daniil Gentili bda520122d
Some templates 2020-10-18 15:02:37 +02:00
Daniil Gentili eca8542be4
Complete overhaul of MTProto message system, parallelized getPwrChat 2020-10-18 14:46:34 +02:00
Daniil Gentili a376f0bf82
Logger and connection improvements 2020-10-17 12:49:26 +02:00
Daniil Gentili 8b2caed419 Change process group in IPC worker 2020-10-17 01:23:52 +02:00
Daniil Gentili 77c7d81e33 Allow setting -1 log size 2020-10-17 01:07:46 +02:00
Daniil Gentili 3139f8a211
Make a few methods internal 2020-10-15 19:18:47 +02:00
Daniil Gentili f4cb2d83a1
Finalize documentation 2020-10-15 19:07:37 +02:00
davtur19 3d956b9002 fix typo 2020-10-14 12:16:47 +02:00
Daniil Gentili 26bb35e921
Remove altervista when done 2020-10-13 21:56:25 +02:00
Daniil Gentili 37d3f94af2
Use getLogger in APILoop 2020-10-13 21:54:25 +02:00
Daniil Gentili def6199437
PHPDOC 2020-10-13 10:03:35 +02:00
Daniil Gentili ac47a23639
PHP 8 fixes 2020-10-08 21:35:32 +02:00
Daniil Gentili a937d1d994
Coroutine bugfix 2020-10-08 15:48:59 +02:00
Daniil Gentili 5dc330119b
Merge branch 'master' of github.com:danog/MadelineProto into master 2020-10-08 11:28:31 +02:00
Daniil Gentili 662f521409
Some more improvements 2020-10-08 11:27:37 +02:00
Alexander Pankratov d814943168 cacheTtl fix 2020-10-08 00:45:10 +03:00
Daniil Gentili 787f06a314
Return warning when serializing coroutine 2020-10-07 20:41:23 +02:00
Daniil Gentili 17ed5c0991
Add IPC support to buttons, start writing PHPDOC documentor 2020-10-07 19:31:52 +02:00
Daniil Gentili 0d1ef0aebe
Allow binding to specific address and port 2020-10-07 17:19:46 +02:00
Daniil Gentili d74efbfd08
Add DoH setting 2020-10-07 17:15:51 +02:00
Daniil Gentili 0a2e325229
Bugfix in merging settings 2020-10-07 16:58:48 +02:00
Daniil Gentili 0ca0707529
Bugfix for cache TTL 2020-10-07 16:54:11 +02:00
Daniil Gentili 6ce4de6091
Properly update database settings when deserializing; also enable optional database backend for event handler variables 2020-10-07 16:48:34 +02:00
Daniil Gentili 7e281c1589
Disable garbage collection logs when reading from console and polyfill from_id 2020-10-06 22:02:04 +02:00
Daniil Gentili 6eedd54ad3
Update annotations 2020-10-06 20:37:11 +02:00
Daniil Gentili da88415e8d
Template fix 2020-10-06 18:06:34 +02:00
Daniil Gentili 14d14fdc3c
More improvements 2020-10-06 18:03:36 +02:00
Daniil Gentili 1d16cf807b
Various improvements 2020-10-06 17:33:18 +02:00
Daniil Gentili b22a193014
IPC and peer fixes 2020-10-06 11:55:19 +02:00
Daniil Gentili ba59131926
Use wait just this once 2020-10-06 11:11:49 +02:00
Daniil Gentili 9511ebab38
Initialize reference database on cleanup 2020-10-06 10:44:53 +02:00
Daniil Gentili e5ab5cabac
merge branch 'master' of github.com:danog/MadelineProto into master 2020-10-05 19:29:15 +02:00
Daniil Gentili 43bd176908
Bugfix 2020-10-05 19:28:52 +02:00
Alexander Pankratov e4aa39c624 Allow simple proxy_extra 2020-10-04 20:25:49 +03:00
Daniil Gentili a21f8205e5
Fix hints 2020-10-04 16:35:33 +02:00
Daniil Gentili d31b798df9
Final fixes, and update to latest version of websocket lib 2020-10-04 16:27:22 +02:00
Daniil Gentili d49f59eec5
Various psalm fixes 2020-10-04 14:55:05 +02:00
Daniil Gentili e3f3e19cf5
Resolve contacts, too 2020-10-04 13:37:27 +02:00
Daniil Gentili cdeb93b165
Polyfill to_id 2020-10-03 17:41:21 +02:00
Daniil Gentili 118cf244b1
More type improvements 2020-10-03 15:36:03 +02:00
Daniil Gentili 4f69701cfc
Update even more typehints 2020-10-03 15:04:35 +02:00
Daniil Gentili 6076118320
Merge branch 'master' of github.com:danog/MadelineProto into master 2020-10-03 12:37:10 +02:00
Daniil Gentili 967c9b5a93
Improvements for static typing 2020-10-03 12:36:08 +02:00
Alexander Pankratov 463e807dbe
Merge pull request #858 from danog/banned_session_handler_fix
Fix bugs in banned session handler
2020-10-03 02:49:46 +03:00
Alexander Pankratov 34f34964a2 Fix bugs in banned session handler
https://t.me/pwrtelegramgroupru/35066
2020-10-03 02:30:18 +03:00
Alexander Pankratov 58a3efacc6 Set ipc slow mode from settings array 2020-10-03 01:44:29 +03:00
Daniil Gentili 76ca2b19ab
Manual psalm fixes 2020-10-02 16:13:19 +02:00
Daniil Gentili 1da9cad6b9
Improved typehints 2020-10-01 21:36:07 +02:00
Daniil Gentili 76c4fe480c
Further automated improvements 2020-10-01 21:06:37 +02:00
Daniil Gentili 297dca7f2c
Fix smore typehints 2020-10-01 21:04:53 +02:00
Daniil Gentili ca2cbe4ecb
Add missing return typehints 2020-10-01 21:03:25 +02:00
Daniil Gentili 05a859ea50
Update docs 2020-10-01 20:48:22 +02:00
Daniil Gentili f6772e49de
Update docs 2020-10-01 18:19:20 +02:00
Daniil Gentili 4fcfe1ccd2
Update to layer 117 2020-10-01 18:02:54 +02:00
Daniil Gentili d792263e86
Fixes 2020-09-28 23:20:12 +02:00
Daniil Gentili db24e8e871
Fix nullCache backends 2020-09-28 21:09:10 +02:00
Daniil Gentili f18e197319
Avoid closing connection if in worker 2020-09-28 17:54:36 +02:00
Daniil Gentili e5c430dfc9
Minor fixes 2020-09-28 17:19:41 +02:00
Daniil Gentili f5e4d29db1
Fix serialization 2020-09-28 11:56:14 +02:00
Daniil Gentili a6b4c0f5cf Further improvements 2020-09-27 23:50:27 +02:00
Daniil Gentili 16040141ae Try setting memory limit 2020-09-27 23:06:57 +02:00
Daniil Gentili 05b6266669 Fix HTTPS 2020-09-27 22:57:52 +02:00
Daniil Gentili 1cd41cc159 Dark magic to allow serializing session to database on shutdown 2020-09-27 22:15:16 +02:00
Daniil Gentili 5560b140f5 Multiple bugfixes 2020-09-27 22:15:16 +02:00
Daniil Gentili 86fc8b6e6e Localize login 2020-09-27 22:15:16 +02:00
Daniil Gentili c9ebdd0fa8 Add PSR logger, remove CombinedAPI 2020-09-27 22:15:16 +02:00
Daniil Gentili b0bafaf431 fix 2020-09-27 22:15:16 +02:00
Daniil Gentili 63c52fef50 Add configs to db properties 2020-09-27 22:15:16 +02:00
Daniil Gentili afaf80d549 Shutdown wrappers, too 2020-09-27 22:15:16 +02:00
Daniil Gentili 5779732cd0 Shutdown wrappers, too 2020-09-27 22:15:16 +02:00
Daniil Gentili d558d17d1f Implement downloadTo* and uploadFrom* via IPC 2020-09-27 22:15:16 +02:00
Daniil Gentili 2b387b5c0f Implement IPC callbacks 2020-09-27 22:15:16 +02:00
Daniil Gentili 3d9d975f79 Avoid bit width compatibility problems, fix fallback to normal session 2020-09-27 22:15:16 +02:00
Daniil Gentili 6984893103 Update MTProto.php 2020-09-27 22:15:16 +02:00
Daniil Gentili 50ee77c229 Store main session in database 2020-09-27 22:15:16 +02:00
Daniil Gentili 4123941df3 Remove legacy FastAPI 2020-09-27 22:15:16 +02:00
Daniil Gentili f90159c8bd Finalize IPC API 2020-09-27 22:15:16 +02:00
Daniil Gentili 164000b591 Final IPC improvements 2020-09-27 22:15:16 +02:00
Daniil Gentili 7a550311ac Windows support 2020-09-27 22:15:16 +02:00
Daniil Gentili bc13e61526 Final fixes 2020-09-27 22:15:16 +02:00
Daniil Gentili a3a331542b IPC improvements 2020-09-27 22:15:16 +02:00
Daniil Gentili 1c99fd2658 Implement FastAPI in API 2020-09-27 22:15:16 +02:00
Daniil Gentili bcadbcbe53 Update SqlAbstract.php 2020-09-27 22:15:16 +02:00
Daniil Gentili 89da6d5a45 Serialization and database improvements 2020-09-27 22:15:16 +02:00
Daniil Gentili 0001a45cd2 Completely refactor settings, remove old (internal and external) APIs, generic code cleanup 2020-09-27 22:15:16 +02:00
Daniil Gentili 26d746d749
Update MysqlArray.php 2020-09-18 22:27:11 +02:00
Daniil Gentili 0cc1627220
Fix IPC server 2020-09-18 20:44:30 +02:00
Alexander Pankratov c643cd063a Fix missing yield 2020-09-17 19:14:41 +03:00
Alexander Pankratov 2b4398bee1
support multiple schemas for photoStrippedSize (#853) 2020-09-13 10:38:13 +02:00
Daniil Gentili d3dc7a0155 Avoid JSON issues with request field and inflated photo payloads 2020-09-13 01:06:08 +02:00
Daniil Gentili 22dd19a9de
Fix table renaming 2020-09-12 20:14:58 +02:00
Daniil Gentili e85fef23a5
Yield from reference database 2020-09-12 20:01:34 +02:00
Daniil Gentili 1883416c7a
Remove redis backend from example 2020-09-12 19:08:14 +02:00
Daniil Gentili 9df1e27780
Add redis backend 2020-09-12 19:06:42 +02:00
Daniil Gentili 5a5c02e56b
Improve caching mechanism, deduplicate SQL code 2020-09-12 17:12:23 +02:00
Daniil Gentili 5567490c7a
Improve performance of memory backend and misc fixes 2020-09-12 15:14:26 +02:00
Daniil Gentili 1c45c6c65b
Async reference/min database 2020-09-12 14:24:57 +02:00
Daniil Gentili 07248a4803
Merge branch 'new_db_properties' of https://github.com/xtrime-ru/MadelineProto into xtrime-ru-new_db_properties 2020-09-12 14:03:56 +02:00
Daniil Gentili e188d647c6
Increase version 2020-09-12 13:59:57 +02:00
Daniil Gentili ce41b6b60b
Avoid database issues in async TL callbacks 2020-09-12 13:50:49 +02:00
Daniil Gentili 2e7f8b38a7
Update to layer 117 2020-09-12 12:50:22 +02:00
Daniil Gentili bb1ab50246 Restore schemas 2020-09-12 00:27:11 +02:00
Daniil Gentili c5d12ac95d Do not use typehint to avoid 32-bit issues 2020-09-12 00:21:42 +02:00
Daniil Gentili 16719ec28f
Check output buffering level 2020-09-11 09:30:08 +02:00
Alexander Pankratov 2f81a14b95 Encode non-utf-8 symbols in keys 2020-09-10 12:00:40 +03:00
Alexander Pankratov b90e92ee68 Move min and reference databases from memory 2020-09-10 12:00:40 +03:00
Daniil Gentili 9e5027a4a7
Add codespaces config 2020-09-02 20:16:24 +02:00
Daniil Gentili 1d323a7e4b
Do not unset file references 2020-09-02 14:00:38 +02:00
Daniil Gentili a5e18e0afd
Change a few defaults 2020-08-29 17:58:02 +02:00
Daniil Gentili d4ca5f1d47
Use internalLoop 2020-08-27 19:23:34 +02:00
Daniil Gentili c4cf967198
Try setting signal handler before passing control to loop 2020-08-27 18:04:55 +02:00
Daniil Gentili a81cd4c221
Merge 2020-08-10 14:16:30 +02:00
Daniil Gentili d07e290ca6
Start web version too 2020-08-10 14:07:40 +02:00
Daniil Gentili f9868af1ff
Update composer.json 2020-08-09 23:27:16 +02:00
Daniil Gentili 01f9e67732
Fix requirements 2020-08-09 14:44:41 +02:00
Daniil Gentili b103ac3087
Merge branch 'master' of github.com:danog/MadelineProto into master 2020-07-31 20:40:10 +02:00
Daniil Gentili 8eac5cc0ac
Proper exception handling in wsstream 2020-07-31 20:40:04 +02:00
Daniil 226cb3bbd8
Postgres support for sessions (#846) 2020-07-30 19:51:16 +02:00
Daniil Gentili e618bf36d4
Remove debug 2020-07-28 20:59:29 +02:00
Daniil Gentili 444d55355a
Switch to external loop 2020-07-28 20:39:32 +02:00
Daniil Gentili 553e4c4da0
Fix CDN DCs 2020-07-23 18:08:14 +02:00
Daniil Gentili cd1716645d
Merge #840 2020-07-14 12:36:40 +02:00
Daniil Gentili d177762371 Final improvements 2020-07-12 14:08:36 +00:00
Daniil Gentili bb6f221939 Fixes 2020-07-12 13:04:22 +00:00
Daniil Gentili 86f6887148
IPC improvements 2020-07-12 14:55:21 +02:00
Daniil Gentili 06e347ca85
Move logs 2020-07-12 11:42:53 +02:00
Daniil Gentili 3e15063da0
Fixes 2020-07-12 01:31:51 +02:00
Daniil Gentili a6fdf813c4
Fixes 2020-07-12 01:27:26 +02:00
Daniil Gentili 45067af046
Final fixes 2020-07-12 01:12:20 +02:00
Daniil Gentili 84ba71475d
Fix redirect 2020-07-12 00:34:09 +02:00
Daniil Gentili 2463fd7261
IPC fixes 2020-07-12 00:17:47 +02:00
Daniil Gentili 699f0660e0
Finalize IPC 2020-07-11 20:13:06 +02:00
Daniil Gentili c8cdf328c0
Implement IPC 2020-07-11 20:01:54 +02:00
Daniil Gentili 7d29bd84bc
Fixes 2020-07-10 14:50:00 +02:00
Daniil Gentili bd07a2b4b9
Add missing files 2020-07-10 13:39:56 +02:00
Daniil Gentili 488a42e6c5
Use global session lock 2020-07-09 21:42:06 +02:00
Daniil Gentili 1e690d1a8f
Avoid certain issues 2020-07-09 20:53:01 +02:00
Daniil Gentili 4923eec9cd
2GB file limit, async RPC reporting, file reference and MTProto fixes 2020-07-09 18:23:16 +02:00
Daniil Gentili bfd08f5744
Fix shutdown loop, automatically inflate stripped photoSizes, improve composer reporter 2020-06-23 20:30:40 +02:00
Daniil Gentili 58e0985d33
Misc fixes 2020-06-20 13:03:15 +02:00
Daniil Gentili 838cf11ff6
Funding 2020-06-16 18:00:15 +02:00
Daniil Gentili d9a471678b
fix 2020-06-16 17:54:25 +02:00
Daniil Gentili 69f33dce34
Layer 113 2020-06-16 17:52:55 +02:00
Daniil Gentili 0e9f107754
Merge branch 'master' of github.com:danog/MadelineProto 2020-06-15 20:08:06 +02:00
Daniil Gentili e147fe3695
Clean up 2020-06-15 20:06:48 +02:00
Alexander Pankratov cbd1a6ce00
Merge pull request #808 from xtrime-ru/db
Store session in DB
2020-06-14 14:52:24 +03:00
Alexander Pankratov 321787f718 Disable isset, optimize getInstance, concurrent offsetSet 2020-06-11 00:23:35 +03:00
Alexander Pankratov ca03bc662a New mysql cache cleanup 2020-06-09 01:48:06 +03:00
Alexander Pankratov 2aab5e8cc8 getParticipantsHash generator fix 2020-06-07 21:56:54 +03:00
Alexander Pankratov 1e23970ab3 DbArray refactoring and convertation improvement 2020-06-07 01:20:03 +03:00
Alexander Pankratov e0832390ce Type fixes 2020-05-23 15:10:47 +03:00
Alexander Pankratov 7c1b602779 Fix Undefined index: value in ArrayCacheTrait.php:34 2020-05-23 15:10:47 +03:00
Alexander Pankratov d0e57f2535 Decrease default cache_ttl 2020-05-23 15:10:47 +03:00
Alexander Pankratov fea71efc99 Upgrade for MysqlArray initialization 2020-05-23 15:10:47 +03:00
Alexander Pankratov 9016a7c0dd Allow dots in mysql table names 2020-05-23 15:10:47 +03:00
Alexander Pankratov fa1d73009d Add more async requests 2020-05-23 15:10:47 +03:00
Alexander Pankratov 123e3fadfd Remove unavailable method call 2020-05-23 15:10:47 +03:00
Alexander Pankratov 912577617e Mysql try ... catch 2020-05-23 15:10:47 +03:00
Alexander Pankratov 8254360d2f MemoryArray undefined offset fix 2020-05-23 15:10:47 +03:00
Alexander Pankratov 6cf1ef504f Always fill usernames cache. Cache fixes. 2020-05-23 15:10:47 +03:00
Alexander Pankratov dedf20ea1b Update create table statements. 2020-05-23 15:10:47 +03:00
Alexander Pankratov ab168ce655 Additional mysql settings 2020-05-23 15:10:47 +03:00
Alexander Pankratov ae9edd5512 Mysql errors catch 2020-05-23 15:10:47 +03:00
Alexander Pankratov 0c59e6ef42 Load submodules 2020-05-23 15:10:47 +03:00
Alexander Pankratov 46f7e63734 Async refactor 2020-05-23 15:10:47 +03:00
Alexander Pankratov a883684f05 Cache for Mysql Data Provider 2020-05-23 15:10:47 +03:00
Alexander Pankratov 26abf9f04e Bugfixes and optimizations 2020-05-23 15:10:47 +03:00
Alexander Pankratov 0d191f4157 Bugfixes and optimizations 2020-05-23 15:10:47 +03:00
Alexander Pankratov ed493330a6 Database properties types 2020-05-23 15:10:47 +03:00
Alexander Pankratov ff7d93f52d phpDoc fix 2020-05-23 15:10:47 +03:00
Alexander Pankratov 21750f8d68 Allow downloadToResponse for media without size 2020-05-23 15:10:47 +03:00
Alexander Pankratov ddc81d8764
Allow downloadToResponse for media without size (#817) 2020-05-23 14:08:04 +02:00
Alexander Pankratov d5b2cbefd3
Fix memory leaks when downloading files to callback. (#816) 2020-05-22 12:30:14 +02:00
Alexander Pankratov 83802fe944
Allow git modules install without ssh key (#810)
(cherry picked from commit 8cb18edc6ee12e8f8a47f6dc7ed9f3ea946bf043)
2020-05-10 19:17:15 +02:00
Daniil Gentili 68ab47de32
Update bot.php 2020-05-08 11:53:47 +02:00
Daniil Gentili 17e85d361c
Merge 2020-04-29 14:57:21 +02:00
Daniil Gentili 995a4228c9
Report memory usage 2020-04-29 14:43:43 +02:00
giuseppeM99 1f71ef8258
Pollfix (#805)
* Add support for quiz solutions
also throws execeptions

* Style fix
2020-04-24 20:14:42 +02:00
giuseppeM99 234065987a
Update to layer v112 (#804)
* Update to layer v112
TL file taken from tdlib

* Added TL file
2020-04-24 11:16:10 +02:00
Daniil Gentili 77a955c25e
More psalm fixes 2020-04-05 22:22:47 +02:00
Daniil Gentili a4d094d4c9
More psalm fixes 2020-04-05 22:22:36 +02:00
Daniil Gentili 8b80396ddb
Further psalm fixes 2020-04-05 15:33:01 +02:00
Daniil Gentili 23e3c5b12c
Apply misc fixes 2020-04-05 14:57:33 +02:00
Daniil Gentili 979d7575fc
Update docs 2020-04-03 14:10:46 +02:00
giuseppeM99 eb43707118
Add support for layer 111 (#782) 2020-03-31 17:30:25 +02:00
Daniil Gentili 832aff00da
Properly set call layer 2020-03-28 17:10:35 +01:00
Daniil Gentili 6720ffff79
32-bit filename bugfix 2020-03-28 11:42:02 +01:00
iDavidef ae00a307a0
Bugfix that fixes stuff (#779) 2020-03-20 16:24:20 +01:00
Daniil Gentili 68e3f99757
Bugfix 2020-03-20 15:13:40 +01:00
Daniil Gentili 93bd9073e1
Bugfix 2020-03-14 17:29:49 +01:00
Daniil Gentili ce0e12c747
More fixes 2020-03-08 17:36:29 +01:00
Daniil Gentili 0a467beb53
Misc bugfixes 2020-03-08 17:24:31 +01:00
Daniil Gentili ec2719db4e
Small cleanups 2020-03-07 21:45:50 +01:00
298 changed files with 21519 additions and 151744 deletions

15
.devcontainer/Dockerfile Normal file
View File

@ -0,0 +1,15 @@
# [Choice] PHP version: 7, 7.4, 7.3
ARG VARIANT=7
FROM mcr.microsoft.com/vscode/devcontainers/php:${VARIANT}
# [Option] Install Node.js
ARG INSTALL_NODE="true"
ARG NODE_VERSION="lts/*"
RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1

View File

@ -0,0 +1,44 @@
# [Choice] PHP version: 7, 7.4, 7.3
ARG VARIANT=7
FROM php:${VARIANT}-cli
# [Option] Install zsh
ARG INSTALL_ZSH="true"
# [Option] Upgrade OS packages to their latest versions
ARG UPGRADE_PACKAGES="true"
# Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies.
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID
COPY library-scripts/common-debian.sh /tmp/library-scripts/
RUN apt-get update && bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" \
&& apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts
# Install xdebug
RUN yes | pecl install xdebug \
&& echo "zend_extension=$(find /usr/local/lib/php/extensions/ -name xdebug.so)" > /usr/local/etc/php/conf.d/xdebug.ini \
&& echo "xdebug.remote_enable=on" >> /usr/local/etc/php/conf.d/xdebug.ini \
&& echo "xdebug.remote_autostart=on" >> /usr/local/etc/php/conf.d/xdebug.ini \
&& rm -rf /tmp/pear
# Install composer
RUN curl -sSL https://getcomposer.org/installer | php \
&& chmod +x composer.phar \
&& mv composer.phar /usr/local/bin/composer
# [Option] Install Node.js
ARG INSTALL_NODE="true"
ARG NODE_VERSION="none"
ENV NVM_DIR=/usr/local/share/nvm
ENV NVM_SYMLINK_CURRENT=true \
PATH=${NVM_DIR}/current/bin:${PATH}
COPY library-scripts/node-debian.sh /tmp/library-scripts/
RUN if [ "$INSTALL_NODE" = "true" ]; then /bin/bash /tmp/library-scripts/node-debian.sh "${NVM_DIR}" "${NODE_VERSION}" "${USERNAME}"; fi \
&& apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts
# [Optional] Uncomment this section to install additional packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>

View File

@ -0,0 +1,34 @@
{
"name": "PHP",
"build": {
"dockerfile": "Dockerfile",
"args": {
// Update VARIANT to pick a PHP version: 7, 7.4, 7.3
"VARIANT": "7.4",
"INSTALL_NODE": "true",
"NODE_VERSION": "lts/*"
}
},
// Set *default* container specific settings.json values on container create.
"settings": {
"terminal.integrated.shell.linux": "/bin/bash"
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"felixfbecker.php-debug",
"felixfbecker.php-intellisense",
"getpsalm.psalm-vscode-plugin",
"junstyle.php-cs-fixer"
]
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "php -v",
// Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root.
// "remoteUser": "vscode"
}

View File

@ -0,0 +1,5 @@
# Warning: Folder contents may be replaced
The contents of this folder will be automatically replaced with a file of the same name in the [vscode-dev-containers](https://github.com/microsoft/vscode-dev-containers) repository's [script-library folder](https://github.com/microsoft/vscode-dev-containers/tree/master/script-library) whenever the repository is packaged.
To retain your edits, move the file to a different location. You may also delete the files if they are not needed.

View File

@ -0,0 +1,187 @@
#!/usr/bin/env bash
#-------------------------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
#-------------------------------------------------------------------------------------------------------------
# Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag]
INSTALL_ZSH=${1:-"true"}
USERNAME=${2:-"vscode"}
USER_UID=${3:-1000}
USER_GID=${4:-1000}
UPGRADE_PACKAGES=${5:-"true"}
set -e
if [ "$(id -u)" -ne 0 ]; then
echo -e 'Script must be run a root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.'
exit 1
fi
# Treat a user name of "none" as root
if [ "${USERNAME}" = "none" ] || [ "${USERNAME}" = "root" ]; then
USERNAME=root
USER_UID=0
USER_GID=0
fi
# Load markers to see which steps have already run
MARKER_FILE="/usr/local/etc/vscode-dev-containers/common"
if [ -f "${MARKER_FILE}" ]; then
echo "Marker file found:"
cat "${MARKER_FILE}"
source "${MARKER_FILE}"
fi
# Ensure apt is in non-interactive to avoid prompts
export DEBIAN_FRONTEND=noninteractive
# Function to call apt-get if needed
apt-get-update-if-needed()
{
if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then
echo "Running apt-get update..."
apt-get update
else
echo "Skipping apt-get update."
fi
}
# Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies
if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then
apt-get-update-if-needed
PACKAGE_LIST="apt-utils \
git \
openssh-client \
less \
iproute2 \
procps \
curl \
wget \
unzip \
zip \
nano \
jq \
lsb-release \
ca-certificates \
apt-transport-https \
dialog \
gnupg2 \
libc6 \
libgcc1 \
libgssapi-krb5-2 \
libicu[0-9][0-9] \
liblttng-ust0 \
libstdc++6 \
zlib1g \
locales \
sudo"
# Install libssl1.1 if available
if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then
PACKAGE_LIST="${PACKAGE_LIST} libssl1.1"
fi
# Install appropriate version of libssl1.0.x if available
LIBSSL=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '')
if [ "$(echo "$LIBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then
if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then
# Debian 9
PACKAGE_LIST="${PACKAGE_LIST} libssl1.0.2"
elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then
# Ubuntu 18.04, 16.04, earlier
PACKAGE_LIST="${PACKAGE_LIST} libssl1.0.0"
fi
fi
echo "Packages to verify are installed: ${PACKAGE_LIST}"
apt-get -y install --no-install-recommends ${PACKAGE_LIST} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 )
PACKAGES_ALREADY_INSTALLED="true"
fi
# Get to latest versions of all packages
if [ "${UPGRADE_PACKAGES}" = "true" ]; then
apt-get-update-if-needed
apt-get -y upgrade --no-install-recommends
apt-get autoremove -y
fi
# Ensure at least the en_US.UTF-8 UTF-8 locale is available.
# Common need for both applications and things like the agnoster ZSH theme.
if [ "${LOCALE_ALREADY_SET}" != "true" ]; then
echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen
locale-gen
LOCALE_ALREADY_SET="true"
fi
# Create or update a non-root user to match UID/GID - see https://aka.ms/vscode-remote/containers/non-root-user.
if id -u $USERNAME > /dev/null 2>&1; then
# User exists, update if needed
if [ "$USER_GID" != "$(id -G $USERNAME)" ]; then
groupmod --gid $USER_GID $USERNAME
usermod --gid $USER_GID $USERNAME
fi
if [ "$USER_UID" != "$(id -u $USERNAME)" ]; then
usermod --uid $USER_UID $USERNAME
fi
else
# Create user
groupadd --gid $USER_GID $USERNAME
useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME
fi
# Add add sudo support for non-root user
if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then
echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME
chmod 0440 /etc/sudoers.d/$USERNAME
EXISTING_NON_ROOT_USER="${USERNAME}"
fi
# .bashrc/.zshrc snippet
RC_SNIPPET="$(cat << EOF
export USER=\$(whoami)
export PATH=\$PATH:\$HOME/.local/bin
if [[ \$(which code-insiders 2>&1) && ! \$(which code 2>&1) ]]; then
alias code=code-insiders
fi
EOF
)"
# Ensure ~/.local/bin is in the PATH for root and non-root users for bash. (zsh is later)
if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then
echo "${RC_SNIPPET}" >> /etc/bash.bashrc
RC_SNIPPET_ALREADY_ADDED="true"
fi
# Optionally install and configure zsh
if [ "${INSTALL_ZSH}" = "true" ] && [ ! -d "/root/.oh-my-zsh" ] && [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then
apt-get-update-if-needed
apt-get install -y zsh
curl -fsSLo- https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh | bash 2>&1
echo "${RC_SNIPPET}" >> /etc/zsh/zshrc
echo -e "DEFAULT_USER=\$USER\nprompt_context(){}" >> /root/.zshrc
cp -fR /root/.oh-my-zsh /etc/skel
cp -f /root/.zshrc /etc/skel
sed -i -e "s/\/root\/.oh-my-zsh/\/home\/\$(whoami)\/.oh-my-zsh/g" /etc/skel/.zshrc
if [ "${USERNAME}" != "root" ]; then
cp -fR /etc/skel/.oh-my-zsh /etc/skel/.zshrc /home/$USERNAME
chown -R $USER_UID:$USER_GID /home/$USERNAME/.oh-my-zsh /home/$USERNAME/.zshrc
fi
ZSH_ALREADY_INSTALLED="true"
fi
# Write marker file
mkdir -p "$(dirname "${MARKER_FILE}")"
echo -e "\
PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\
LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\
EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\
RC_SNIPPET_ALREADY_ADDED=${RC_SNIPPET_ALREADY_ADDED}\n\
ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}" > "${MARKER_FILE}"
echo "Done!"

View File

@ -0,0 +1,105 @@
#!/bin/bash
#-------------------------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
#-------------------------------------------------------------------------------------------------------------
# Syntax: ./node-debian.sh [directory to install nvm] [node version to install (use "none" to skip)] [non-root user]
export NVM_DIR=${1:-"/usr/local/share/nvm"}
export NODE_VERSION=${2:-"lts/*"}
USERNAME=${3:-"vscode"}
UPDATE_RC=${4:-"true"}
set -e
if [ "$(id -u)" -ne 0 ]; then
echo -e 'Script must be run a root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.'
exit 1
fi
# Treat a user name of "none" or non-existant user as root
if [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then
USERNAME=root
fi
if [ "${NODE_VERSION}" = "none" ]; then
export NODE_VERSION=
fi
# Ensure apt is in non-interactive to avoid prompts
export DEBIAN_FRONTEND=noninteractive
# Install curl, apt-transport-https, tar, or gpg if missing
if ! dpkg -s apt-transport-https curl ca-certificates tar > /dev/null 2>&1 || ! type gpg > /dev/null 2>&1; then
if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then
apt-get update
fi
apt-get -y install --no-install-recommends apt-transport-https curl ca-certificates tar gnupg2
fi
# Install yarn
if type yarn > /dev/null 2>&1; then
echo "Yarn already installed."
else
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | (OUT=$(apt-key add - 2>&1) || echo $OUT)
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
apt-get update
apt-get -y install --no-install-recommends yarn
fi
# Install the specified node version if NVM directory already exists, then exit
if [ -d "${NVM_DIR}" ]; then
echo "NVM already installed."
if [ "${NODE_VERSION}" != "" ]; then
su ${USERNAME} -c "source $NVM_DIR/nvm.sh && nvm install ${NODE_VERSION} && nvm clear-cache"
fi
exit 0
fi
# Run NVM installer as non-root if needed
mkdir -p ${NVM_DIR}
chown ${USERNAME} ${NVM_DIR}
su ${USERNAME} -c "$(cat << EOF
set -e
# Do not update profile - we'll do this manually
export PROFILE=/dev/null
curl -so- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
source ${NVM_DIR}/nvm.sh
if [ "${NODE_VERSION}" != "" ]; then
nvm alias default ${NODE_VERSION}
fi
nvm clear-cache
EOF
)" 2>&1
if [ "${UPDATE_RC}" = "true" ]; then
echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc with NVM scripts..."
(cat <<EOF
export NVM_DIR="${NVM_DIR}"
sudoIf()
{
if [ "\$(id -u)" -ne 0 ]; then
sudo "\$@"
else
"\$@"
fi
}
if [ "\$(stat -c '%U' \$NVM_DIR)" != "${USERNAME}" ]; then
if [ "\$(id -u)" -eq 0 ] || type sudo > /dev/null 2>&1; then
echo "Fixing permissions of \"\$NVM_DIR\"..."
sudoIf chown -R ${USERNAME}:root \$NVM_DIR
else
echo "Warning: NVM directory is not owned by ${USERNAME} and sudo is not installed. Unable to correct permissions."
fi
fi
[ -s "\$NVM_DIR/nvm.sh" ] && . "\$NVM_DIR/nvm.sh"
[ -s "\$NVM_DIR/bash_completion" ] && . "\$NVM_DIR/bash_completion"
EOF
) | tee -a /etc/bash.bashrc >> /etc/zsh/zshrc
fi
echo "Done!"

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
github: danog

2
.gitignore vendored
View File

@ -124,3 +124,5 @@ coverage
tempConv
extracted.json
.phpunit.result.cache
ponyScripts

9
.gitmodules vendored
View File

@ -1,9 +1,12 @@
[submodule "docs"]
path = docs
url = git@github.com:danog/MadelineProtoDocs
url = https://github.com/danog/MadelineProtoDocs.git
[submodule "examples/magnaluna"]
path = examples/magnaluna
url = git@github.com:danog/magnaluna
url = https://github.com/danog/magnaluna.git
[submodule "examples/pipesbot"]
path = examples/pipesbot
url = git@github.com:danog/pipesbot
url = https://github.com/danog/pipesbot.git
[submodule "schemas"]
path = schemas
url = https://github.com/danog/schemas.git

View File

@ -5,8 +5,7 @@ $config->getFinder()
->in(__DIR__ . '/src')
->in(__DIR__ . '/tests')
->in(__DIR__ . '/examples')
->in(__DIR__ . '/tools')
->in(__DIR__);
->in(__DIR__ . '/tools');
$cacheDir = getenv('TRAVIS') ? getenv('HOME') . '/.php-cs-fixer' : __DIR__;

110
README.md
View File

@ -86,8 +86,7 @@ You can find examples for nearly every MadelineProto function in
* [Handling updates (new messages)](https://docs.madelineproto.xyz/docs/UPDATES.html)
* [Self-restart on webhosts](https://docs.madelineproto.xyz/docs/UPDATES.html#self-restart-on-webhosts)
* [Async Event driven](https://docs.madelineproto.xyz/docs/UPDATES.html#async-event-driven)
* [Multi-account: Async Event driven](https://docs.madelineproto.xyz/docs/UPDATES.html#async-event-driven)
* [Async Callback](https://docs.madelineproto.xyz/docs/UPDATES.html#async-callback)
* [Async Event driven multi-account](https://docs.madelineproto.xyz/docs/UPDATES.html#async-event-driven-multiaccount)
* [Noop (default)](https://docs.madelineproto.xyz/docs/UPDATES.html#noop)
* [Fetch all updates from the beginning](https://docs.madelineproto.xyz/docs/UPDATES.html#fetch-all-updates-from-the-beginning)
* [Settings](https://docs.madelineproto.xyz/docs/SETTINGS.html)
@ -133,7 +132,7 @@ You can find examples for nearly every MadelineProto function in
* [Full chat info with full list of participants](https://docs.madelineproto.xyz/docs/CHAT_INFO.html#getPwrChat-now-fully-async)
* [Full chat info](https://docs.madelineproto.xyz/docs/CHAT_INFO.html#getFullInfo-now-fully-async)
* [Reduced chat info (very fast)](https://docs.madelineproto.xyz/docs/CHAT_INFO.html#getInfo-now-fully-async)
* [Just the chat ID (extremely fast)](https://docs.madelineproto.xyz/docs/CHAT_INFO.html#get_id-now-fully-async)
* [Just the chat ID (extremely fast)](https://docs.madelineproto.xyz/docs/CHAT_INFO.html#getId-now-fully-async)
* [Getting all chats (dialogs)](https://docs.madelineproto.xyz/docs/DIALOGS.html)
* [Dialog list](https://docs.madelineproto.xyz/docs/DIALOGS.html#getDialogs-now-fully-async)
* [Full dialog info](https://docs.madelineproto.xyz/docs/DIALOGS.html#getFullDialogs-now-fully-async)
@ -191,6 +190,8 @@ You can find examples for nearly every MadelineProto function in
* [Upload or download files up to 1.5 GB](https://docs.madelineproto.xyz/docs/FILES.html)
* [Make a phone call and play a song](https://docs.madelineproto.xyz/docs/CALLS.html)
* [Create a secret chat bot](https://docs.madelineproto.xyz/docs/SECRET_CHATS.html)
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.acceptLoginToken.html" name="auth.acceptLoginToken">Accept QR code login token, logging in the app that generated it: auth.acceptLoginToken</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/phone.acceptCall.html" name="phone.acceptCall">Accept incoming call: phone.acceptCall</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.acceptTermsOfService.html" name="help.acceptTermsOfService">Accept the new terms of service: help.acceptTermsOfService</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.saveGif.html" name="messages.saveGif">Add GIF to saved gifs list: messages.saveGif</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/stickers.addStickerToSet.html" name="stickers.addStickerToSet">Add a sticker to a stickerset, bots only. The sticker set must have been created by the bot: stickers.addStickerToSet</a>
@ -200,11 +201,14 @@ You can find examples for nearly every MadelineProto function in
* <a href="https://docs.madelineproto.xyz/API_docs/methods/contacts.block.html" name="contacts.block">Adds the user to the blacklist: contacts.block</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.setInlineBotResults.html" name="messages.setInlineBotResults">Answer an inline query, for bots only: messages.setInlineBotResults</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/bots.answerWebhookJSONQuery.html" name="bots.answerWebhookJSONQuery">Answers a custom query; for bots only: bots.answerWebhookJSONQuery</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.toggleStickerSets.html" name="messages.toggleStickerSets">Apply changes to multiple stickersets: messages.toggleStickerSets</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.setDiscussionGroup.html" name="channels.setDiscussionGroup">Associate a group to a channel as discussion group for that channel: channels.setDiscussionGroup</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.setStickers.html" name="channels.setStickers">Associate a stickerset to the supergroup: channels.setStickers</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.editBanned.html" name="channels.editBanned">Ban/unban/kick a user in a supergroup/channel: channels.editBanned</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.bindTempAuthKey.html" name="auth.bindTempAuthKey">Binds a temporary authorization key temp_auth_key_id to the permanent authorization key perm_auth_key_id. Each permanent key may only be bound to one temporary key at a time, binding a new temporary key overwrites the previous one: auth.bindTempAuthKey</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.cancelPasswordEmail.html" name="account.cancelPasswordEmail">Cancel the code that was sent to verify an email to use as 2FA recovery method: account.cancelPasswordEmail</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.cancelCode.html" name="auth.cancelCode">Cancel the login verification code: auth.cancelCode</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.discardEncryption.html" name="messages.discardEncryption">Cancels a request for creation and/or delete info on secret chat: messages.discardEncryption</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.editChatTitle.html" name="messages.editChatTitle">Chanages chat name and sends a service message on it: messages.editChatTitle</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.saveAutoDownloadSettings.html" name="account.saveAutoDownloadSettings">Change media autodownload settings: account.saveAutoDownloadSettings</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.setPrivacy.html" name="account.setPrivacy">Change privacy settings of current account: account.setPrivacy</a>
@ -220,14 +224,16 @@ You can find examples for nearly every MadelineProto function in
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.clearRecentStickers.html" name="messages.clearRecentStickers">Clear recent stickers: messages.clearRecentStickers</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/payments.clearSavedInfo.html" name="payments.clearSavedInfo">Clear saved payment information: payments.clearSavedInfo</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.confirmPhone.html" name="account.confirmPhone">Confirm a phone number to cancel account deletion, for more info click here »: account.confirmPhone</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.acceptEncryption.html" name="messages.acceptEncryption">Confirms creation of a secret chat: messages.acceptEncryption</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.receivedMessages.html" name="messages.receivedMessages">Confirms receipt of messages by a client, cancels PUSH-notification sending: messages.receivedMessages</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.receivedQueue.html" name="messages.receivedQueue">Confirms receipt of messages in a secret chat by client, cancels push notifications: messages.receivedQueue</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.createChannel.html" name="channels.createChannel">Create a supergroup/channel: channels.createChannel</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/stickers.createStickerSet.html" name="stickers.createStickerSet">Create a stickerset, bots only: stickers.createStickerSet</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.createTheme.html" name="account.createTheme">Create a theme: account.createTheme</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.uploadWallPaper.html" name="account.uploadWallPaper">Create and upload a new wallpaper: account.uploadWallPaper</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.createChat.html" name="messages.createChat">Creates a new chat: messages.createChat</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.deleteChannel.html" name="channels.deleteChannel">Delete a channel/supergroup: channels.deleteChannel</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/folders.deleteFolder.html" name="folders.deleteFolder">Delete a folder: folders.deleteFolder</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/folders.deleteFolder.html" name="folders.deleteFolder">Delete a peer folder: folders.deleteFolder</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.deleteUserHistory.html" name="channels.deleteUserHistory">Delete all messages sent by a certain user in a supergroup: channels.deleteUserHistory</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.dropTempAuthKeys.html" name="auth.dropTempAuthKeys">Delete all temporary authorization keys except for the ones specified: auth.dropTempAuthKeys</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/contacts.deleteByPhones.html" name="contacts.deleteByPhones">Delete contacts by phone number: contacts.deleteByPhones</a>
@ -245,10 +251,11 @@ You can find examples for nearly every MadelineProto function in
* <a href="https://docs.madelineproto.xyz/API_docs/methods/photos.deletePhotos.html" name="photos.deletePhotos">Deletes profile photos: photos.deletePhotos</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/contacts.deleteContacts.html" name="contacts.deleteContacts">Deletes several contacts from the list: contacts.deleteContacts</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/contacts.unblock.html" name="contacts.unblock">Deletes the user from the blacklist: contacts.unblock</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/upload.getCdnFile.html" name="upload.getCdnFile">Download a CDN file: upload.getCdnFile</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.editInlineBotMessage.html" name="messages.editInlineBotMessage">Edit an inline bot message: messages.editInlineBotMessage</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.editLocation.html" name="channels.editLocation">Edit location of geogroup: channels.editLocation</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.editMessage.html" name="messages.editMessage">Edit message: messages.editMessage</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/folders.editPeerFolders.html" name="folders.editPeerFolders">Edit peers in folder: folders.editPeerFolders</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/folders.editPeerFolders.html" name="folders.editPeerFolders">Edit peers in peer folder: folders.editPeerFolders</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.editChatDefaultBannedRights.html" name="messages.editChatDefaultBannedRights">Edit the default banned rights of a channel/supergroup/group: messages.editChatDefaultBannedRights</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.editChatAbout.html" name="messages.editChatAbout">Edit the description of a group/supergroup/channel: messages.editChatAbout</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.editTitle.html" name="channels.editTitle">Edit the name of a channel/supergroup: channels.editTitle</a>
@ -259,10 +266,17 @@ You can find examples for nearly every MadelineProto function in
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getMessageEditData.html" name="messages.getMessageEditData">Find out if a media message's caption can be edited: messages.getMessageEditData</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.finishTakeoutSession.html" name="account.finishTakeoutSession">Finish account takeout session: account.finishTakeoutSession</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.forwardMessages.html" name="messages.forwardMessages">Forwards messages by their IDs: messages.forwardMessages</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.exportLoginToken.html" name="auth.exportLoginToken">Generate a login token, for login via QR code. : auth.exportLoginToken</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.getPromoData.html" name="help.getPromoData">Get MTProxy/Public Service Announcement information: help.getPromoData</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/upload.getCdnFileHashes.html" name="upload.getCdnFileHashes">Get SHA256 hashes for verifying downloaded CDN files: upload.getCdnFileHashes</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/upload.getFileHashes.html" name="upload.getFileHashes">Get SHA256 hashes for verifying downloaded files: upload.getFileHashes</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/stats.getBroadcastStats.html" name="stats.getBroadcastStats">Get channel statistics: stats.getBroadcastStats</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.getMessages.html" name="channels.getMessages">Get channel/supergroup messages: channels.getMessages</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.getAdminedPublicChannels.html" name="channels.getAdminedPublicChannels">Get channels/supergroups/geogroups we're admin in. Usually called when the user exceeds the limit for owned public channels/supergroups/geogroups, and the user is given the choice to remove one of his channels/supergroups/geogroups: channels.getAdminedPublicChannels</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getDialogFilters.html" name="messages.getDialogFilters">Get folders: messages.getDialogFilters</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getWebPage.html" name="messages.getWebPage">Get instant view page: messages.getWebPage</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.getPassportConfig.html" name="help.getPassportConfig">Get passport configuration: help.getPassportConfig</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getSuggestedDialogFilters.html" name="messages.getSuggestedDialogFilters">Get suggested folders: messages.getSuggestedDialogFilters</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getDocumentByHash.html" name="messages.getDocumentByHash">Get a document by its SHA256 hash, mainly used for gifs: messages.getDocumentByHash</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.getLeftChannels.html" name="channels.getLeftChannels">Get a list of channels/supergroups we left: channels.getLeftChannels</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/payments.getPaymentForm.html" name="payments.getPaymentForm">Get a payment form: payments.getPaymentForm</a>
@ -273,9 +287,9 @@ You can find examples for nearly every MadelineProto function in
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getAllStickers.html" name="messages.getAllStickers">Get all installed stickers: messages.getAllStickers</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.getAllSecureValues.html" name="account.getAllSecureValues">Get all saved Telegram Passport documents, for more info see the passport docs »: account.getAllSecureValues</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getMessagesViews.html" name="messages.getMessagesViews">Get and increase the view counter of a message sent or forwarded from a channel: messages.getMessagesViews</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.getAppConfig.html" name="help.getAppConfig">Get app-specific configuration: help.getAppConfig</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.getAppConfig.html" name="help.getAppConfig">Get app-specific configuration, see client configuration for more info on the result: help.getAppConfig</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getEmojiKeywordsDifference.html" name="messages.getEmojiKeywordsDifference">Get changed emoji keywords: messages.getEmojiKeywordsDifference</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.getAppChangelog.html" name="help.getAppChangelog">Get changelog of current app: help.getAppChangelog</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.getAppChangelog.html" name="help.getAppChangelog">Get changelog of current app. : help.getAppChangelog</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getCommonChats.html" name="messages.getCommonChats">Get chats in common with a user: messages.getCommonChats</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.getCdnConfig.html" name="help.getCdnConfig">Get configuration for CDN file downloads: help.getCdnConfig</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/contacts.getContactIDs.html" name="contacts.getContactIDs">Get contact by telegram IDs: contacts.getContactIDs</a>
@ -286,14 +300,18 @@ You can find examples for nearly every MadelineProto function in
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getDialogUnreadMarks.html" name="messages.getDialogUnreadMarks">Get dialogs manually marked as unread: messages.getDialogUnreadMarks</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getFavedStickers.html" name="messages.getFavedStickers">Get faved stickers: messages.getFavedStickers</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getFeaturedStickers.html" name="messages.getFeaturedStickers">Get featured stickers: messages.getFeaturedStickers</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.getFullChannel.html" name="channels.getFullChannel">Get full info about a channel: channels.getFullChannel</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getInlineGameHighScores.html" name="messages.getInlineGameHighScores">Get highscores of a game sent using an inline bot: messages.getInlineGameHighScores</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getGameHighScores.html" name="messages.getGameHighScores">Get highscores of a game: messages.getGameHighScores</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.getInactiveChannels.html" name="channels.getInactiveChannels">Get inactive channels and supergroups: channels.getInactiveChannels</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.getChannels.html" name="channels.getChannels">Get info about channels/supergroups: channels.getChannels</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.getParticipant.html" name="channels.getParticipant">Get info about a channel/supergroup participant: channels.getParticipant</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.getDeepLinkInfo.html" name="help.getDeepLinkInfo">Get info about a t.me link: help.getDeepLinkInfo</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.getWallPaper.html" name="account.getWallPaper">Get info about a certain wallpaper: account.getWallPaper</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/payments.getBankCardData.html" name="payments.getBankCardData">Get info about a credit card: payments.getBankCardData</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getStickerSet.html" name="messages.getStickerSet">Get info about a stickerset: messages.getStickerSet</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getEmojiKeywordsLanguages.html" name="messages.getEmojiKeywordsLanguages">Get info about an emoji keyword localization: messages.getEmojiKeywordsLanguages</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.getMultiWallPapers.html" name="account.getMultiWallPapers">Get info about multiple wallpapers: account.getMultiWallPapers</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/langpack.getLanguage.html" name="langpack.getLanguage">Get information about a language in a localization pack: langpack.getLanguage</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/langpack.getLanguages.html" name="langpack.getLanguages">Get information about all languages in a localization pack: langpack.getLanguages</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getMaskStickers.html" name="messages.getMaskStickers">Get installed mask stickers: messages.getMaskStickers</a>
@ -308,15 +326,17 @@ You can find examples for nearly every MadelineProto function in
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getSplitRanges.html" name="messages.getSplitRanges">Get message ranges for saving the user's chat history: messages.getSplitRanges</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.requestUrlAuth.html" name="messages.requestUrlAuth">Get more info about a Seamless Telegram Login authorization request, for more info click here »: messages.requestUrlAuth</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/contacts.getTopPeers.html" name="contacts.getTopPeers">Get most used peers: contacts.getTopPeers</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/updates.getDifference.html" name="updates.getDifference">Get new updates: updates.getDifference</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/langpack.getDifference.html" name="langpack.getDifference">Get new strings in languagepack: langpack.getDifference</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/payments.getPaymentReceipt.html" name="payments.getPaymentReceipt">Get payment receipt: payments.getPaymentReceipt</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getPeerSettings.html" name="messages.getPeerSettings">Get peer settings: messages.getPeerSettings</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/phone.getCallConfig.html" name="phone.getCallConfig">Get phone call configuration to be passed to libtgvoip's shared config: phone.getCallConfig</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getPinnedDialogs.html" name="messages.getPinnedDialogs">Get pinned dialogs: messages.getPinnedDialogs</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getPollVotes.html" name="messages.getPollVotes">Get poll results for non-anonymous polls: messages.getPollVotes</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getPollResults.html" name="messages.getPollResults">Get poll results: messages.getPollResults</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getWebPagePreview.html" name="messages.getWebPagePreview">Get preview of webpage: messages.getWebPagePreview</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.getPrivacy.html" name="account.getPrivacy">Get privacy settings of current account: account.getPrivacy</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.getProxyData.html" name="help.getProxyData">Get promotion info of the currently-used MTProxy: help.getProxyData</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.getPasswordSettings.html" name="account.getPasswordSettings">Get private info associated to the password info (recovery email, telegram passport info & so on): account.getPasswordSettings</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getRecentStickers.html" name="messages.getRecentStickers">Get recent stickers: messages.getRecentStickers</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.getRecentMeUrls.html" name="help.getRecentMeUrls">Get recently used t.me links: help.getRecentMeUrls</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getSavedGifs.html" name="messages.getSavedGifs">Get saved GIFs: messages.getSavedGifs</a>
@ -324,19 +344,21 @@ You can find examples for nearly every MadelineProto function in
* <a href="https://docs.madelineproto.xyz/API_docs/methods/payments.getSavedInfo.html" name="payments.getSavedInfo">Get saved payment information: payments.getSavedInfo</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getScheduledHistory.html" name="messages.getScheduledHistory">Get scheduled messages: messages.getScheduledHistory</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getScheduledMessages.html" name="messages.getScheduledMessages">Get scheduled messages: messages.getScheduledMessages</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.getContentSettings.html" name="account.getContentSettings">Get sensitive content settings: account.getContentSettings</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getAttachedStickers.html" name="messages.getAttachedStickers">Get stickers attached to a photo or video: messages.getAttachedStickers</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getStickers.html" name="messages.getStickers">Get stickers by emoji: messages.getStickers</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/langpack.getStrings.html" name="langpack.getStrings">Get strings from a language pack: langpack.getStrings</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.getTmpPassword.html" name="account.getTmpPassword">Get temporary payment password: account.getTmpPassword</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.getAdminLog.html" name="channels.getAdminLog">Get the admin log of a channel/supergroup: channels.getAdminLog</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getSearchCounters.html" name="messages.getSearchCounters">Get the number of results that would be found by a messages.search call with the same parameters: messages.getSearchCounters</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.getParticipants.html" name="channels.getParticipants">Get the participants of a channel: channels.getParticipants</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.getParticipants.html" name="channels.getParticipants">Get the participants of a supergroup/channel: channels.getParticipants</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.getTheme.html" name="account.getTheme">Get theme information: account.getTheme</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getUnreadMentions.html" name="messages.getUnreadMentions">Get unread messages where we were mentioned: messages.getUnreadMentions</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.getWebAuthorizations.html" name="account.getWebAuthorizations">Get web login widget authorizations: account.getWebAuthorizations</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.search.html" name="messages.search">Gets back found messages: messages.search</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getHistory.html" name="messages.getHistory">Gets back the conversation history with one interlocutor / within a chat: messages.getHistory</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.getNotifySettings.html" name="account.getNotifySettings">Gets current notification settings for a given user/group, from all users/all groups: account.getNotifySettings</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.hidePromoData.html" name="help.hidePromoData">Hide MTProxy/Public Service Announcement information: help.hidePromoData</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.togglePreHistoryHidden.html" name="channels.togglePreHistoryHidden">Hide/unhide message history for new channel/supergroup users: channels.togglePreHistoryHidden</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/contacts.acceptContact.html" name="contacts.acceptContact">If the peer settings of a new user allow us to add him as contact, add that user as contact: contacts.acceptContact</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.setBotShippingResults.html" name="messages.setBotShippingResults">If you sent an invoice requesting a shipping address and the parameter is_flexible was specified, the bot will receive an updateBotShippingQuery update. Use this method to reply to shipping queries: messages.setBotShippingResults</a>
@ -361,8 +383,13 @@ You can find examples for nearly every MadelineProto function in
* <a href="https://docs.madelineproto.xyz/API_docs/methods/invokeAfterMsg.html" name="invokeAfterMsg">Invokes a query after successfull completion of one of the previous queries: invokeAfterMsg</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.joinChannel.html" name="channels.joinChannel">Join a channel/supergroup: channels.joinChannel</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.leaveChannel.html" name="channels.leaveChannel">Leave a channel/supergroup: channels.leaveChannel</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/stats.loadAsyncGraph.html" name="stats.loadAsyncGraph">Load channel statistics graph asynchronously: stats.loadAsyncGraph</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.resetAuthorization.html" name="account.resetAuthorization">Log out an active authorized session by its hash: account.resetAuthorization</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.resetWebAuthorization.html" name="account.resetWebAuthorization">Log out an active web telegram login session: account.resetWebAuthorization</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.importBotAuthorization.html" name="auth.importBotAuthorization">Login as a bot: auth.importBotAuthorization</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.importLoginToken.html" name="auth.importLoginToken">Login using a redirected login token, generated in case of DC mismatch during QR code login: auth.importLoginToken</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.importAuthorization.html" name="auth.importAuthorization">Logs in a user using a key transmitted from his native data-centre: auth.importAuthorization</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.logOut.html" name="auth.logOut">Logs out the user: auth.logOut</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.getTermsOfServiceUpdate.html" name="help.getTermsOfServiceUpdate">Look for updates of telegram's terms of service: help.getTermsOfServiceUpdate</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.editChatAdmin.html" name="messages.editChatAdmin">Make a user admin in a legacy group: messages.editChatAdmin</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.markDialogUnread.html" name="messages.markDialogUnread">Manually mark dialog as unread: messages.markDialogUnread</a>
@ -373,6 +400,7 @@ You can find examples for nearly every MadelineProto function in
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.readFeaturedStickers.html" name="messages.readFeaturedStickers">Mark new featured stickers as read: messages.readFeaturedStickers</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.readHistory.html" name="messages.readHistory">Marks message history as read: messages.readHistory</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.readEncryptedHistory.html" name="messages.readEncryptedHistory">Marks message history within a secret chat as read: messages.readEncryptedHistory</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getOldFeaturedStickers.html" name="messages.getOldFeaturedStickers">Method for fetching previously featured stickers: messages.getOldFeaturedStickers</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.editAdmin.html" name="channels.editAdmin">Modify the admin rights of a user in a supergroup/channel: channels.editAdmin</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.readMessageContents.html" name="messages.readMessageContents">Notifies the sender about the recipient having listened a voice message or watched a video: messages.readMessageContents</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.sendScreenshotNotification.html" name="messages.sendScreenshotNotification">Notify the other user in a private chat that a screenshot of the chat was taken: messages.sendScreenshotNotification</a>
@ -385,8 +413,11 @@ You can find examples for nearly every MadelineProto function in
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getBotCallbackAnswer.html" name="messages.getBotCallbackAnswer">Press an inline callback button and get a callback answer from the bot: messages.getBotCallbackAnswer</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getInlineBotResults.html" name="messages.getInlineBotResults">Query an inline bot: messages.getInlineBotResults</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/phone.setCallRating.html" name="phone.setCallRating">Rate a call: phone.setCallRating</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/phone.discardCall.html" name="phone.discardCall">Refuse or end running call: phone.discardCall</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.registerDevice.html" name="account.registerDevice">Register device to receive PUSH notifications: account.registerDevice</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.signUp.html" name="auth.signUp">Registers a validated phone number in the system: auth.signUp</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/stickers.removeStickerFromSet.html" name="stickers.removeStickerFromSet">Remove a sticker from the set where it belongs, bots only. The sticker set must have been created by the bot: stickers.removeStickerFromSet</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.updateDialogFiltersOrder.html" name="messages.updateDialogFiltersOrder">Reorder folders: messages.updateDialogFiltersOrder</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.reorderStickerSets.html" name="messages.reorderStickerSets">Reorder installed stickersets: messages.reorderStickerSets</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.reorderPinnedDialogs.html" name="messages.reorderPinnedDialogs">Reorder pinned dialogs: messages.reorderPinnedDialogs</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.report.html" name="messages.report">Report a message in a chat for violation of telegram's Terms of Service: messages.report</a>
@ -394,6 +425,7 @@ You can find examples for nearly every MadelineProto function in
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.reportPeer.html" name="account.reportPeer">Report a peer for violation of telegram's Terms of Service: account.reportPeer</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.reportEncryptedSpam.html" name="messages.reportEncryptedSpam">Report a secret chat for spam: messages.reportEncryptedSpam</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.reportSpam.html" name="channels.reportSpam">Reports some messages from a user in a supergroup as spam; requires administrator rights in the supergroup: channels.reportSpam</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/upload.reuploadCdnFile.html" name="upload.reuploadCdnFile">Request a reupload of a certain file to a CDN DC: upload.reuploadCdnFile</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.requestPasswordRecovery.html" name="auth.requestPasswordRecovery">Request recovery code of a 2FA password, only for accounts with a recovery email configured: auth.requestPasswordRecovery</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.resendPasswordEmail.html" name="account.resendPasswordEmail">Resend the code to verify an email to use as 2FA recovery method: account.resendPasswordEmail</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.resendCode.html" name="auth.resendCode">Resend the login code via another medium, the phone code type is determined by the return value of the previous auth.sendCode/auth.resendCode: see login for more info: auth.resendCode</a>
@ -401,20 +433,28 @@ You can find examples for nearly every MadelineProto function in
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.resetWebAuthorizations.html" name="account.resetWebAuthorizations">Reset all active web telegram login sessions: account.resetWebAuthorizations</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.recoverPassword.html" name="auth.recoverPassword">Reset the 2FA password using the recovery code sent using auth.requestPasswordRecovery: auth.recoverPassword</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.resetNotifySettings.html" name="account.resetNotifySettings">Resets all notification settings from users and groups: account.resetNotifySettings</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/contacts.resolveUsername.html" name="contacts.resolveUsername">Resolve a @username to get peer info: contacts.resolveUsername</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getStatsURL.html" name="messages.getStatsURL">Returns URL with the chat statistics. Currently this method can be used only for channels: messages.getStatsURL</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.getAuthorizationForm.html" name="account.getAuthorizationForm">Returns a Telegram Passport authorization form for sharing data with a service: account.getAuthorizationForm</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/updates.getState.html" name="updates.getState">Returns a current state of updates: updates.getState</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.getWallPapers.html" name="account.getWallPapers">Returns a list of available wallpapers: account.getWallPapers</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getEmojiURL.html" name="messages.getEmojiURL">Returns an HTTP URL which can be used to automatically log in into translation platform and suggest new emoji replacements. The URL will be valid for 30 seconds after generation: messages.getEmojiURL</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/users.getUsers.html" name="users.getUsers">Returns basic user info according to their identifiers: users.getUsers</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getChats.html" name="messages.getChats">Returns chat basic info on their IDs: messages.getChats</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getDhConfig.html" name="messages.getDhConfig">Returns configuration parameters for Diffie-Hellman key generation. Can also return a random sequence of bytes of required length: messages.getDhConfig</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/upload.getFile.html" name="upload.getFile">Returns content of a whole file or its part: upload.getFile</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/upload.getWebFile.html" name="upload.getWebFile">Returns content of an HTTP file or a part, by proxying the request through telegram: upload.getWebFile</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.getConfig.html" name="help.getConfig">Returns current configuration, icluding data center configuration: help.getConfig</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.getConfig.html" name="help.getConfig">Returns current configuration, including data center configuration: help.getConfig</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.exportAuthorization.html" name="auth.exportAuthorization">Returns data for copying authorization to another data-centre: auth.exportAuthorization</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/users.getFullUser.html" name="users.getFullUser">Returns extended user info by ID: users.getFullUser</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getFullChat.html" name="messages.getFullChat">Returns full chat info according to its ID: messages.getFullChat</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.getNearestDc.html" name="help.getNearestDc">Returns info on data centre nearest to the user: help.getNearestDc</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.getAppUpdate.html" name="help.getAppUpdate">Returns information on update availability for the current application: help.getAppUpdate</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.getNotifyExceptions.html" name="account.getNotifyExceptions">Returns list of chats with non-default notification settings: account.getNotifyExceptions</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.getInviteText.html" name="help.getInviteText">Returns text of a text message with an invitation: help.getInviteText</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.getInviteText.html" name="help.getInviteText">Returns localized text of a text message with an invitation: help.getInviteText</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getDialogs.html" name="messages.getDialogs">Returns the current user dialog list: messages.getDialogs</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/contacts.getContacts.html" name="contacts.getContacts">Returns the current user's contact list: contacts.getContacts</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/updates.getChannelDifference.html" name="updates.getChannelDifference">Returns the difference between the current state of updates of a certain channel and transmitted: updates.getChannelDifference</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/contacts.getBlocked.html" name="contacts.getBlocked">Returns the list of blocked users: contacts.getBlocked</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/contacts.getStatuses.html" name="contacts.getStatuses">Returns the list of contact statuses: contacts.getStatuses</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getMessages.html" name="messages.getMessages">Returns the list of messages by their IDs: messages.getMessages</a>
@ -424,8 +464,9 @@ You can find examples for nearly every MadelineProto function in
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.saveDraft.html" name="messages.saveDraft">Save a message draft associated to a chat: messages.saveDraft</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.saveTheme.html" name="account.saveTheme">Save a theme: account.saveTheme</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getAllDrafts.html" name="messages.getAllDrafts">Save get all message drafts: messages.getAllDrafts</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/upload.saveBigFilePart.html" name="upload.saveBigFilePart">Saves a part of a large file (over 10Mb in size) to be later passed to one of the methods: upload.saveBigFilePart</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/upload.saveFilePart.html" name="upload.saveFilePart">Saves a part of file for futher sending to one of the methods: upload.saveFilePart</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.saveAppLog.html" name="help.saveAppLog">Saves logs of application on the server: help.saveAppLog</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.searchGifs.html" name="messages.searchGifs">Search for GIFs: messages.searchGifs</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.searchGlobal.html" name="messages.searchGlobal">Search for messages and peers globally: messages.searchGlobal</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.searchStickerSets.html" name="messages.searchStickerSets">Search for stickersets: messages.searchStickerSets</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.saveSecureValue.html" name="account.saveSecureValue">Securely save Telegram Passport document, for more info see the passport docs »: account.saveSecureValue</a>
@ -436,6 +477,7 @@ You can find examples for nearly every MadelineProto function in
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.sendConfirmPhoneCode.html" name="account.sendConfirmPhoneCode">Send confirmation code to cancel account deletion, for more info click here »: account.sendConfirmPhoneCode</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/phone.saveCallDebug.html" name="phone.saveCallDebug">Send phone call debug data to server: phone.saveCallDebug</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.sendScheduledMessages.html" name="messages.sendScheduledMessages">Send scheduled messages right away: messages.sendScheduledMessages</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.sendCode.html" name="auth.sendCode">Send the verification code for login: auth.sendCode</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.sendVerifyEmailCode.html" name="account.sendVerifyEmailCode">Send the verification email code for telegram passport: account.sendVerifyEmailCode</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.sendVerifyPhoneCode.html" name="account.sendVerifyPhoneCode">Send the verification phone code for telegram passport: account.sendVerifyPhoneCode</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.setEncryptedTyping.html" name="messages.setEncryptedTyping">Send typing event by the current user to a secret chat: messages.setEncryptedTyping</a>
@ -444,19 +486,28 @@ You can find examples for nearly every MadelineProto function in
* <a href="https://docs.madelineproto.xyz/API_docs/methods/bots.sendCustomRequest.html" name="bots.sendCustomRequest">Sends a custom request; for bots only: bots.sendCustomRequest</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.sendMessage.html" name="messages.sendMessage">Sends a message to a chat: messages.sendMessage</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.sendEncryptedFile.html" name="messages.sendEncryptedFile">Sends a message with a file attachment to a secret chat: messages.sendEncryptedFile</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.requestEncryption.html" name="messages.requestEncryption">Sends a request to start a secret chat to the user: messages.requestEncryption</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.sendEncryptedService.html" name="messages.sendEncryptedService">Sends a service message to a secret chat: messages.sendEncryptedService</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.sendEncrypted.html" name="messages.sendEncrypted">Sends a text message to a secret chat: messages.sendEncrypted</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.updatePasswordSettings.html" name="account.updatePasswordSettings">Set a new 2FA password: account.updatePasswordSettings</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.setAccountTTL.html" name="account.setAccountTTL">Set account self-destruction period: account.setAccountTTL</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/bots.setBotCommands.html" name="bots.setBotCommands">Set bot command list: bots.setBotCommands</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.setContentSettings.html" name="account.setContentSettings">Set sensitive content settings (for viewing or hiding NSFW content): account.setContentSettings</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/stickers.setStickerSetThumb.html" name="stickers.setStickerSetThumb">Set stickerset thumbnail: stickers.setStickerSetThumb</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.setBotCallbackAnswer.html" name="messages.setBotCallbackAnswer">Set the callback answer to a user button press (bots only): messages.setBotCallbackAnswer</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.hidePeerSettingsBar.html" name="messages.hidePeerSettingsBar">Should be called after the user hides the report spam/add as contact bar of a new chat, effectively prevents the user from executing the actions specified in the peer's settings: messages.hidePeerSettingsBar</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.signIn.html" name="auth.signIn">Signs in a user with a validated phone number: auth.signIn</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.startBot.html" name="messages.startBot">Start a conversation with a bot using a deep linking parameter: messages.startBot</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/phone.requestCall.html" name="phone.requestCall">Start a telegram phone call: phone.requestCall</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/payments.validateRequestedInfo.html" name="payments.validateRequestedInfo">Submit requested order information for validation: payments.validateRequestedInfo</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.resetAuthorizations.html" name="auth.resetAuthorizations">Terminates all user's authorized sessions except for the current one: auth.resetAuthorizations</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.setContactSignUpNotification.html" name="account.setContactSignUpNotification">Toggle contact sign up notifications: account.setContactSignUpNotification</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.toggleSlowMode.html" name="channels.toggleSlowMode">Toggle supergroup slow mode: if enabled, users will only be able to send one message every seconds seconds: channels.toggleSlowMode</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.editCreator.html" name="channels.editCreator">Transfer channel ownership: channels.editCreator</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.checkPassword.html" name="auth.checkPassword">Try logging to an account protected by a 2FA password: auth.checkPassword</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.migrateChat.html" name="messages.migrateChat">Turn a legacy group into a supergroup: messages.migrateChat</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.uninstallStickerSet.html" name="messages.uninstallStickerSet">Uninstall a stickerset: messages.uninstallStickerSet</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.updateDialogFilter.html" name="messages.updateDialogFilter">Update folder: messages.updateDialogFilter</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.updateTheme.html" name="account.updateTheme">Update theme: account.updateTheme</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/photos.uploadProfilePhoto.html" name="photos.uploadProfilePhoto">Updates current user profile photo: photos.uploadProfilePhoto</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.updateStatus.html" name="account.updateStatus">Updates online user status: account.updateStatus</a>
@ -475,40 +526,7 @@ You can find examples for nearly every MadelineProto function in
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.sendVote.html" name="messages.sendVote">Vote in a poll: messages.sendVote</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.updateDeviceLocked.html" name="account.updateDeviceLocked">When client-side passcode lock feature is enabled, will not show message texts in incoming PUSH notifications: account.updateDeviceLocked</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.getContactSignUpNotification.html" name="account.getContactSignUpNotification">Whether the user will receive notifications when contacts sign up: account.getContactSignUpNotification</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.bindTempAuthKey.html" name="auth.bindTempAuthKey">You cannot use this method directly, instead modify the PFS and default_temp_auth_key_expires_in settings, see https://docs.madelineproto.xyz/docs/SETTINGS.html for more info: auth.bindTempAuthKey</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getDhConfig.html" name="messages.getDhConfig">You cannot use this method directly, instead use $MadelineProto->getDhConfig();: messages.getDhConfig</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.acceptEncryption.html" name="messages.acceptEncryption">You cannot use this method directly, see https://docs.madelineproto.xyz for more info on handling secret chats: messages.acceptEncryption</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.discardEncryption.html" name="messages.discardEncryption">You cannot use this method directly, see https://docs.madelineproto.xyz for more info on handling secret chats: messages.discardEncryption</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.requestEncryption.html" name="messages.requestEncryption">You cannot use this method directly, see https://docs.madelineproto.xyz for more info on handling secret chats: messages.requestEncryption</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/updates.getChannelDifference.html" name="updates.getChannelDifference">You cannot use this method directly, see https://docs.madelineproto.xyz for more info on handling updates: updates.getChannelDifference</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/updates.getDifference.html" name="updates.getDifference">You cannot use this method directly, see https://docs.madelineproto.xyz for more info on handling updates: updates.getDifference</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/updates.getState.html" name="updates.getState">You cannot use this method directly, see https://docs.madelineproto.xyz for more info on handling updates: updates.getState</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/phone.acceptCall.html" name="phone.acceptCall">You cannot use this method directly, see https://docs.madelineproto.xyz#calls for more info on handling calls: phone.acceptCall</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/phone.confirmCall.html" name="phone.confirmCall">You cannot use this method directly, see https://docs.madelineproto.xyz#calls for more info on handling calls: phone.confirmCall</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/phone.discardCall.html" name="phone.discardCall">You cannot use this method directly, see https://docs.madelineproto.xyz#calls for more info on handling calls: phone.discardCall</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/phone.requestCall.html" name="phone.requestCall">You cannot use this method directly, see https://docs.madelineproto.xyz#calls for more info on handling calls: phone.requestCall</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.exportAuthorization.html" name="auth.exportAuthorization">You cannot use this method directly, use $MadelineProto->exportAuthorization() instead, see https://docs.madelineproto.xyz/docs/LOGIN.html: auth.exportAuthorization</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.importAuthorization.html" name="auth.importAuthorization">You cannot use this method directly, use $MadelineProto->importAuthorization($authorization) instead, see https://docs.madelineproto.xyz/docs/LOGIN.html: auth.importAuthorization</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.importBotAuthorization.html" name="auth.importBotAuthorization">You cannot use this method directly, use the botLogin method instead (see https://docs.madelineproto.xyz for more info): auth.importBotAuthorization</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.checkPassword.html" name="auth.checkPassword">You cannot use this method directly, use the complete2falogin method instead (see https://docs.madelineproto.xyz for more info): auth.checkPassword</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.signIn.html" name="auth.signIn">You cannot use this method directly, use the completePhoneLogin method instead (see https://docs.madelineproto.xyz for more info): auth.signIn</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.signUp.html" name="auth.signUp">You cannot use this method directly, use the completeSignup method instead (see https://docs.madelineproto.xyz for more info): auth.signUp</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.getFullChannel.html" name="channels.getFullChannel">You cannot use this method directly, use the getPwrChat, getInfo, getFullInfo methods instead (see https://docs.madelineproto.xyz for more info): channels.getFullChannel</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getFullChat.html" name="messages.getFullChat">You cannot use this method directly, use the getPwrChat, getInfo, getFullInfo methods instead (see https://docs.madelineproto.xyz for more info): messages.getFullChat</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/users.getFullUser.html" name="users.getFullUser">You cannot use this method directly, use the getPwrChat, getInfo, getFullInfo methods instead (see https://docs.madelineproto.xyz for more info): users.getFullUser</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.logOut.html" name="auth.logOut">You cannot use this method directly, use the logout method instead (see https://docs.madelineproto.xyz for more info): auth.logOut</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.sendCode.html" name="auth.sendCode">You cannot use this method directly, use the phoneLogin method instead (see https://docs.madelineproto.xyz for more info): auth.sendCode</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/contacts.resolveUsername.html" name="contacts.resolveUsername">You cannot use this method directly, use the resolveUsername, getPwrChat, getInfo, getFullInfo methods instead (see https://docs.madelineproto.xyz for more info): contacts.resolveUsername</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/upload.getCdnFile.html" name="upload.getCdnFile">You cannot use this method directly, use the upload, downloadToStream, downloadToFile, downloadToDir methods instead; see https://docs.madelineproto.xyz for more info: upload.getCdnFile</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/upload.getCdnFileHashes.html" name="upload.getCdnFileHashes">You cannot use this method directly, use the upload, downloadToStream, downloadToFile, downloadToDir methods instead; see https://docs.madelineproto.xyz for more info: upload.getCdnFileHashes</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/upload.getFile.html" name="upload.getFile">You cannot use this method directly, use the upload, downloadToStream, downloadToFile, downloadToDir methods instead; see https://docs.madelineproto.xyz for more info: upload.getFile</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/upload.getFileHashes.html" name="upload.getFileHashes">You cannot use this method directly, use the upload, downloadToStream, downloadToFile, downloadToDir methods instead; see https://docs.madelineproto.xyz for more info: upload.getFileHashes</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/upload.reuploadCdnFile.html" name="upload.reuploadCdnFile">You cannot use this method directly, use the upload, downloadToStream, downloadToFile, downloadToDir methods instead; see https://docs.madelineproto.xyz for more info: upload.reuploadCdnFile</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/upload.saveBigFilePart.html" name="upload.saveBigFilePart">You cannot use this method directly, use the upload, downloadToStream, downloadToFile, downloadToDir methods instead; see https://docs.madelineproto.xyz for more info: upload.saveBigFilePart</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/upload.saveFilePart.html" name="upload.saveFilePart">You cannot use this method directly, use the upload, downloadToStream, downloadToFile, downloadToDir methods instead; see https://docs.madelineproto.xyz for more info: upload.saveFilePart</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.receivedQueue.html" name="messages.receivedQueue">You cannot use this method directly: messages.receivedQueue</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.getPasswordSettings.html" name="account.getPasswordSettings">You cannot use this method directly; use $MadelineProto->update2fa($params), instead (see https://docs.madelineproto.xyz for more info): account.getPasswordSettings</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.updatePasswordSettings.html" name="account.updatePasswordSettings">You cannot use this method directly; use $MadelineProto->update2fa($params), instead (see https://docs.madelineproto.xyz for more info): account.updatePasswordSettings</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/phone.confirmCall.html" name="phone.confirmCall">Complete phone call E2E encryption key exchange »: phone.confirmCall</a>
* [Peers](https://docs.madelineproto.xyz/docs/USING_METHODS.html#peers)
* [Files](https://docs.madelineproto.xyz/docs/FILES.html)
* [Secret chats](https://docs.madelineproto.xyz/docs/USING_METHODS.html#secret-chats)

View File

@ -6,13 +6,14 @@
"homepage": "https://docs.madelineproto.xyz",
"keywords": ["telegram", "mtproto", "protocol", "bytes", "messenger", "client", "PHP", "video", "stickers", "audio", "files", "GB"],
"conflict": {
"krakjoe/pthreads-polyfill": "*"
"krakjoe/pthreads-polyfill": "*",
"ext-pthreads": "*"
},
"require": {
"php": ">=7.4.0",
"danog/primemodule": "^1",
"danog/tgseclib": "^3",
"erusev/parsedown": "^1.7",
"symfony/polyfill-mbstring": "*",
"ext-mbstring": "*",
"ext-json": "*",
"ext-xml": "*",
@ -25,30 +26,38 @@
"amphp/http-client": "^4",
"amphp/socket": "^1",
"amphp/dns": "^1",
"amphp/file": "^1",
"amphp/byte-stream": "^1",
"amphp/file": "^1",
"amphp/mysql": "^2.0",
"amphp/postgres": "^1.2",
"danog/dns-over-https": "^0.2",
"amphp/http-client-cookies": "^1",
"danog/tg-file-decoder": "^0.1",
"danog/magicalserializer": "^1.0",
"league/uri": "^6",
"danog/ipc": "^0.1",
"tivie/htaccess-parser": "^0.2.3"
"amphp/log": "^1.1",
"danog/loop": "^0.1.0",
"danog/tgseclib": "^3",
"amphp/redis": "^1.0",
"symfony/polyfill-php80": "^1.18",
"amphp/websocket-client": "^1.0"
},
"require-dev": {
"phpdocumentor/reflection-docblock": "^5.2",
"vlucas/phpdotenv": "^3",
"phpdocumentor/reflection-docblock": "^4.3",
"ennexa/amp-update-cache": "dev-master",
"phpunit/phpunit": "^8",
"phpunit/phpunit": "^9",
"amphp/php-cs-fixer-config": "dev-master",
"haydenpierce/class-finder": "^0.4",
"amphp/http-server": "dev-master",
"amphp/http": "^1.6",
"amphp/websocket-client": "dev-master as 1",
"amphp/websocket": "dev-master as 1",
"ext-ctype": "*",
"danog/7to70": "^1",
"danog/7to5": "^1"
"danog/7to5": "^1",
"vimeo/psalm": "dev-master",
"phpstan/phpstan": "^0.12.14",
"friendsofphp/php-cs-fixer": "^2.16",
"danog/phpdoc": "^0.1.7"
},
"suggest": {
"ext-libtgvoip": "Install the php-libtgvoip extension to make phone calls (https://github.com/danog/php-libtgvoip)"
@ -64,23 +73,37 @@
"files": [
"src/BigIntegor.php",
"src/YieldReturnValue.php",
"src/ReflectionGenerator.php",
"src/danog/MadelineProto/Ipc/Runner/entry.php",
"src/polyfill.php"
]
},
"autoload-dev": {
"psr-4": {
"danog\\MadelineProto\\Test\\": "tests/danog/"
}
},
"files": [
"tools/build_docs/schemas.php",
"tools/build_docs/merge.php",
"tools/build_docs/layerUpgrade.php"
]
},
"repositories": [{
"type": "git",
"url": "https://github.com/danog/dns"
}],
"scripts": {
"post-autoload-dump": [
"git submodule init && git submodule update"
],
"build": [
"@docs",
"@cs-fix"
"@phpdoc",
"@cs-fix",
"@psalm"
],
"phpdoc": [
"@phpdocInternal",
"@phpdocMain"
],
"check": [
"@cs",
@ -90,6 +113,9 @@
"test-php56": "tests/test-conversion.sh 5",
"cs": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix -v --diff --dry-run",
"cs-fix": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix -v --diff",
"psalm": "psalm",
"phpdocMain": "php tools/phpdoc.php",
"phpdocInternal": "phpdoc docs/docs/PHPInternal",
"docs": "php tools/build_docs.php",
"test": "@php -dzend.assertions=1 -dassert.exception=1 ./vendor/bin/phpunit --coverage-text --config tests/phpunit.xml"
}

2
docs

@ -1 +1 @@
Subproject commit 1e1967391fe643b6ecc0e6f22a800386cbd22e5d
Subproject commit e83c3ea026fc2142b6558378d0c9636c8c29f248

View File

@ -21,9 +21,11 @@
use danog\MadelineProto\API;
use danog\MadelineProto\EventHandler;
use danog\MadelineProto\Exception;
use danog\MadelineProto\Logger;
use danog\MadelineProto\RPCErrorException;
use danog\MadelineProto\Settings;
use danog\MadelineProto\Settings\Database\Mysql;
use danog\MadelineProto\Settings\Database\Postgres;
use danog\MadelineProto\Settings\Database\Redis;
/*
* Various ways to load MadelineProto
@ -34,6 +36,9 @@ if (\file_exists('vendor/autoload.php')) {
if (!\file_exists('madeline.php')) {
\copy('https://phar.madelineproto.xyz/madeline.php', 'madeline.php');
}
/**
* @psalm-suppress MissingFile
*/
include 'madeline.php';
}
@ -78,29 +83,22 @@ class MyEventHandler extends EventHandler
if ($update['message']['_'] === 'messageEmpty' || $update['message']['out'] ?? false) {
return;
}
$res = \json_encode($update, JSON_PRETTY_PRINT);
try {
yield $this->messages->sendMessage(['peer' => $update, 'message' => "<code>$res</code>", 'reply_to_msg_id' => isset($update['message']['id']) ? $update['message']['id'] : null, 'parse_mode' => 'HTML']);
if (isset($update['message']['media']) && $update['message']['media']['_'] !== 'messageMediaGame') {
yield $this->messages->sendMedia(['peer' => $update, 'message' => $update['message']['message'], 'media' => $update]);
}
} catch (RPCErrorException $e) {
$this->report("Surfaced: $e");
} catch (Exception $e) {
if (\stripos($e->getMessage(), 'invalid constructor given') === false) {
$this->report("Surfaced: $e");
}
yield $this->messages->sendMessage(['peer' => $update, 'message' => "<code>$res</code>", 'reply_to_msg_id' => isset($update['message']['id']) ? $update['message']['id'] : null, 'parse_mode' => 'HTML']);
if (isset($update['message']['media']) && $update['message']['media']['_'] !== 'messageMediaGame') {
yield $this->messages->sendMedia(['peer' => $update, 'message' => $update['message']['message'], 'media' => $update]);
}
}
}
$settings = [
'logger' => [
'logger_level' => Logger::VERBOSE
],
'serialization' => [
'serialization_interval' => 30,
],
];
$settings = new Settings;
$settings->getLogger()->setLevel(Logger::LEVEL_ULTRA_VERBOSE);
// You can also use Redis, MySQL or PostgreSQL
// $settings->setDb((new Redis)->setDatabase(0)->setPassword('pony'));
// $settings->setDb((new Postgres)->setDatabase('MadelineProto')->setUsername('daniil')->setPassword('pony'));
// $settings->setDb((new Mysql)->setDatabase('MadelineProto')->setUsername('daniil')->setPassword('pony'));
$MadelineProto = new API('bot.madeline', $settings);

View File

@ -21,9 +21,7 @@
use danog\MadelineProto\API;
use danog\MadelineProto\EventHandler;
use danog\MadelineProto\Exception;
use danog\MadelineProto\Logger;
use danog\MadelineProto\RPCErrorException;
/*
* Various ways to load MadelineProto
@ -60,7 +58,7 @@ class MyEventHandler extends EventHandler
*
* @param array $update Update
*
* @return void
* @return \Generator
*/
public function onUpdateNewChannelMessage(array $update): \Generator
{
@ -80,17 +78,9 @@ class MyEventHandler extends EventHandler
}
$res = \json_encode($update, JSON_PRETTY_PRINT);
try {
yield $this->messages->sendMessage(['peer' => $update, 'message' => "<code>$res</code>", 'reply_to_msg_id' => isset($update['message']['id']) ? $update['message']['id'] : null, 'parse_mode' => 'HTML']);
if (isset($update['message']['media']) && $update['message']['media']['_'] !== 'messageMediaGame') {
yield $this->messages->sendMedia(['peer' => $update, 'message' => $update['message']['message'], 'media' => $update]);
}
} catch (RPCErrorException $e) {
$this->report("Surfaced: $e");
} catch (Exception $e) {
if (\stripos($e->getMessage(), 'invalid constructor given') === false) {
$this->report("Surfaced: $e");
}
yield $this->messages->sendMessage(['peer' => $update, 'message' => "<code>$res</code>", 'reply_to_msg_id' => isset($update['message']['id']) ? $update['message']['id'] : null, 'parse_mode' => 'HTML']);
if (isset($update['message']['media']) && $update['message']['media']['_'] !== 'messageMediaGame') {
yield $this->messages->sendMedia(['peer' => $update, 'message' => $update['message']['message'], 'media' => $update]);
}
}
}

View File

@ -19,6 +19,11 @@
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
if (\function_exists('memprof_enable')) {
\memprof_enable();
}
use Amp\Http\Server\HttpServer;
use danog\MadelineProto\API;
use danog\MadelineProto\APIWrapper;
@ -86,6 +91,10 @@ class MyEventHandler extends \danog\MadelineProto\EventHandler
$this->UPLOAD = \class_exists(HttpServer::class);
parent::__construct($API);
}
public function onStart()
{
$this->adminId = yield $this->getInfo(self::ADMIN)['bot_api_id'];
}
/**
* Handle updates from channels and supergroups.
*
@ -126,6 +135,13 @@ class MyEventHandler extends \danog\MadelineProto\EventHandler
if ($update['message']['message'] === '/start') {
return $this->messages->sendMessage(['peer' => $peerId, 'message' => self::START, 'parse_mode' => 'Markdown', 'reply_to_msg_id' => $messageId]);
}
if ($update['message']['message'] === '/report' && $peerId === $this->adminId) {
\memprof_dump_callgrind($stm = \fopen("php://memory", "w"));
\fseek($stm, 0);
yield $this->messages->sendMedia(['peer' => $peerId, 'media' => ['_' => 'inputMediaUploadedDocument', 'file' => $stm, 'attributes' => [['_' => 'documentAttributeFilename', 'file_name' => 'callgrind.out']]]]);
\fclose($stm);
return;
}
if (isset($update['message']['media']['_']) && $update['message']['media']['_'] !== 'messageMediaWebPage') {
if ($this->UPLOAD && ($this->states[$peerId] ?? false) === $this->UPLOAD) {
unset($this->states[$peerId]);

View File

@ -20,7 +20,7 @@
require '../vendor/autoload.php';
$MadelineProto = new \danog\MadelineProto\API('session.madeline');
$MadelineProto = new \danog\MadelineProto\API('index.madeline');
$me = $MadelineProto->start();
$me = $MadelineProto->getSelf();

View File

@ -21,8 +21,6 @@
use danog\MadelineProto\APIWrapper;
\set_include_path(\get_include_path().':'.\realpath(\dirname(__FILE__).'/MadelineProto/'));
/*
* Various ways to load MadelineProto
*/
@ -68,6 +66,9 @@ class SecretHandler extends \danog\MadelineProto\EventHandler
if ($update['message']['message'] === 'request') {
yield $this->requestSecretChat($update);
}
if ($update['message']['message'] === 'ping') {
yield $this->messages->sendMessage(['message' => 'lmao', 'peer' => $update]);
}
}
/**
* Handle secret chat messages.
@ -78,55 +79,49 @@ class SecretHandler extends \danog\MadelineProto\EventHandler
*/
public function onUpdateNewEncryptedMessage(array $update): \Generator
{
try {
if (isset($update['message']['decrypted_message']['media'])) {
$this->logger(yield $this->downloadToDir($update, '.'));
}
if (isset($this->sent[$update['message']['chat_id']])) {
return;
}
$secret_media = [];
// Photo uploaded as document, secret chat
$secret_media['document_photo'] = ['peer' => $update, 'file' => 'tests/faust.jpg', 'message' => ['_' => 'decryptedMessage', 'ttl' => 0, 'message' => '', 'media' => ['_' => 'decryptedMessageMediaDocument', 'thumb' => \file_get_contents('tests/faust.preview.jpg'), 'thumb_w' => 90, 'thumb_h' => 90, 'mime_type' => \mime_content_type('tests/faust.jpg'), 'caption' => 'This file was uploaded using MadelineProto', 'file_name' => 'faust.jpg', 'size' => \filesize('tests/faust.jpg'), 'attributes' => [['_' => 'documentAttributeImageSize', 'w' => 1280, 'h' => 914]]]]];
// Photo, secret chat
$secret_media['photo'] = ['peer' => $update, 'file' => 'tests/faust.jpg', 'message' => ['_' => 'decryptedMessage', 'ttl' => 0, 'message' => '', 'media' => ['_' => 'decryptedMessageMediaPhoto', 'thumb' => \file_get_contents('tests/faust.preview.jpg'), 'thumb_w' => 90, 'thumb_h' => 90, 'caption' => 'This file was uploaded using MadelineProto', 'size' => \filesize('tests/faust.jpg'), 'w' => 1280, 'h' => 914]]];
// GIF, secret chat
$secret_media['gif'] = ['peer' => $update, 'file' => 'tests/pony.mp4', 'message' => ['_' => 'decryptedMessage', 'ttl' => 0, 'message' => '', 'media' => ['_' => 'decryptedMessageMediaDocument', 'thumb' => \file_get_contents('tests/pony.preview.jpg'), 'thumb_w' => 90, 'thumb_h' => 90, 'mime_type' => \mime_content_type('tests/pony.mp4'), 'caption' => 'test', 'file_name' => 'pony.mp4', 'size' => \filesize('tests/faust.jpg'), 'attributes' => [['_' => 'documentAttributeAnimated']]]]];
// Sticker, secret chat
$secret_media['sticker'] = ['peer' => $update, 'file' => 'tests/lel.webp', 'message' => ['_' => 'decryptedMessage', 'ttl' => 0, 'message' => '', 'media' => ['_' => 'decryptedMessageMediaDocument', 'thumb' => \file_get_contents('tests/lel.preview.jpg'), 'thumb_w' => 90, 'thumb_h' => 90, 'mime_type' => \mime_content_type('tests/lel.webp'), 'caption' => 'test', 'file_name' => 'lel.webp', 'size' => \filesize('tests/lel.webp'), 'attributes' => [['_' => 'documentAttributeSticker', 'alt' => 'LEL', 'stickerset' => ['_' => 'inputStickerSetEmpty']]]]]];
// Document, secrey chat
$secret_media['document'] = ['peer' => $update, 'file' => 'tests/60', 'message' => ['_' => 'decryptedMessage', 'ttl' => 0, 'message' => '', 'media' => ['_' => 'decryptedMessageMediaDocument', 'thumb' => \file_get_contents('tests/faust.preview.jpg'), 'thumb_w' => 90, 'thumb_h' => 90, 'mime_type' => 'magic/magic', 'caption' => 'test', 'file_name' => 'magic.magic', 'size' => \filesize('tests/60'), 'attributes' => [['_' => 'documentAttributeFilename', 'file_name' => 'fairy']]]]];
// Video, secret chat
$secret_media['video'] = ['peer' => $update, 'file' => 'tests/swing.mp4', 'message' => ['_' => 'decryptedMessage', 'ttl' => 0, 'message' => '', 'media' => ['_' => 'decryptedMessageMediaDocument', 'thumb' => \file_get_contents('tests/swing.preview.jpg'), 'thumb_w' => 90, 'thumb_h' => 90, 'mime_type' => \mime_content_type('tests/swing.mp4'), 'caption' => 'test', 'file_name' => 'swing.mp4', 'size' => \filesize('tests/swing.mp4'), 'attributes' => [['_' => 'documentAttributeVideo', 'duration' => 5, 'w' => 1280, 'h' => 720]]]]];
// audio, secret chat
$secret_media['audio'] = ['peer' => $update, 'file' => 'tests/mosconi.mp3', 'message' => ['_' => 'decryptedMessage', 'ttl' => 0, 'message' => '', 'media' => ['_' => 'decryptedMessageMediaDocument', 'thumb' => \file_get_contents('tests/faust.preview.jpg'), 'thumb_w' => 90, 'thumb_h' => 90, 'mime_type' => \mime_content_type('tests/mosconi.mp3'), 'caption' => 'test', 'file_name' => 'mosconi.mp3', 'size' => \filesize('tests/mosconi.mp3'), 'attributes' => [['_' => 'documentAttributeAudio', 'voice' => false, 'duration' => 1, 'title' => 'AH NON LO SO IO', 'performer' => 'IL DIO GERMANO MOSCONI']]]]];
$secret_media['voice'] = ['peer' => $update, 'file' => 'tests/mosconi.mp3', 'message' => ['_' => 'decryptedMessage', 'ttl' => 0, 'message' => '', 'media' => ['_' => 'decryptedMessageMediaDocument', 'thumb' => \file_get_contents('tests/faust.preview.jpg'), 'thumb_w' => 90, 'thumb_h' => 90, 'mime_type' => \mime_content_type('tests/mosconi.mp3'), 'caption' => 'test', 'file_name' => 'mosconi.mp3', 'size' => \filesize('tests/mosconi.mp3'), 'attributes' => [['_' => 'documentAttributeAudio', 'voice' => true, 'duration' => 1, 'title' => 'AH NON LO SO IO', 'performer' => 'IL DIO GERMANO MOSCONI']]]]];
foreach ($secret_media as $type => $smessage) {
$promises = $this->messages->sendEncryptedFile($smessage);
}
yield $promises;
$i = 0;
while ($i < 10) {
$this->logger("SENDING MESSAGE $i TO ".$update['message']['chat_id']);
// You can also use the sendEncrypted parameter for more options in secret chats
yield $this->messages->sendMessage(['peer' => $update, 'message' => (string) ($i++)]);
}
$this->sent[$update['message']['chat_id']] = true;
} catch (\danog\MadelineProto\RPCErrorException $e) {
\danog\MadelineProto\Logger::log($e);
} catch (\danog\MadelineProto\Exception $e) {
\danog\MadelineProto\Logger::log($e);
if (isset($update['message']['decrypted_message']['media'])) {
$this->logger(yield $this->downloadToDir($update, '.'));
}
if (isset($this->sent[$update['message']['chat_id']])) {
return;
}
$secret_media = [];
// Photo uploaded as document, secret chat
$secret_media['document_photo'] = ['peer' => $update, 'file' => 'tests/faust.jpg', 'message' => ['_' => 'decryptedMessage', 'ttl' => 0, 'message' => '', 'media' => ['_' => 'decryptedMessageMediaDocument', 'thumb' => \file_get_contents('tests/faust.preview.jpg'), 'thumb_w' => 90, 'thumb_h' => 90, 'mime_type' => \mime_content_type('tests/faust.jpg'), 'caption' => 'This file was uploaded using MadelineProto', 'file_name' => 'faust.jpg', 'size' => \filesize('tests/faust.jpg'), 'attributes' => [['_' => 'documentAttributeImageSize', 'w' => 1280, 'h' => 914]]]]];
// Photo, secret chat
$secret_media['photo'] = ['peer' => $update, 'file' => 'tests/faust.jpg', 'message' => ['_' => 'decryptedMessage', 'ttl' => 0, 'message' => '', 'media' => ['_' => 'decryptedMessageMediaPhoto', 'thumb' => \file_get_contents('tests/faust.preview.jpg'), 'thumb_w' => 90, 'thumb_h' => 90, 'caption' => 'This file was uploaded using MadelineProto', 'size' => \filesize('tests/faust.jpg'), 'w' => 1280, 'h' => 914]]];
// GIF, secret chat
$secret_media['gif'] = ['peer' => $update, 'file' => 'tests/pony.mp4', 'message' => ['_' => 'decryptedMessage', 'ttl' => 0, 'message' => '', 'media' => ['_' => 'decryptedMessageMediaDocument', 'thumb' => \file_get_contents('tests/pony.preview.jpg'), 'thumb_w' => 90, 'thumb_h' => 90, 'mime_type' => \mime_content_type('tests/pony.mp4'), 'caption' => 'test', 'file_name' => 'pony.mp4', 'size' => \filesize('tests/faust.jpg'), 'attributes' => [['_' => 'documentAttributeAnimated']]]]];
// Sticker, secret chat
$secret_media['sticker'] = ['peer' => $update, 'file' => 'tests/lel.webp', 'message' => ['_' => 'decryptedMessage', 'ttl' => 0, 'message' => '', 'media' => ['_' => 'decryptedMessageMediaDocument', 'thumb' => \file_get_contents('tests/lel.preview.jpg'), 'thumb_w' => 90, 'thumb_h' => 90, 'mime_type' => \mime_content_type('tests/lel.webp'), 'caption' => 'test', 'file_name' => 'lel.webp', 'size' => \filesize('tests/lel.webp'), 'attributes' => [['_' => 'documentAttributeSticker', 'alt' => 'LEL', 'stickerset' => ['_' => 'inputStickerSetEmpty']]]]]];
// Document, secrey chat
$secret_media['document'] = ['peer' => $update, 'file' => 'tests/60', 'message' => ['_' => 'decryptedMessage', 'ttl' => 0, 'message' => '', 'media' => ['_' => 'decryptedMessageMediaDocument', 'thumb' => \file_get_contents('tests/faust.preview.jpg'), 'thumb_w' => 90, 'thumb_h' => 90, 'mime_type' => 'magic/magic', 'caption' => 'test', 'file_name' => 'magic.magic', 'size' => \filesize('tests/60'), 'attributes' => [['_' => 'documentAttributeFilename', 'file_name' => 'fairy']]]]];
// Video, secret chat
$secret_media['video'] = ['peer' => $update, 'file' => 'tests/swing.mp4', 'message' => ['_' => 'decryptedMessage', 'ttl' => 0, 'message' => '', 'media' => ['_' => 'decryptedMessageMediaDocument', 'thumb' => \file_get_contents('tests/swing.preview.jpg'), 'thumb_w' => 90, 'thumb_h' => 90, 'mime_type' => \mime_content_type('tests/swing.mp4'), 'caption' => 'test', 'file_name' => 'swing.mp4', 'size' => \filesize('tests/swing.mp4'), 'attributes' => [['_' => 'documentAttributeVideo', 'duration' => 5, 'w' => 1280, 'h' => 720]]]]];
// audio, secret chat
$secret_media['audio'] = ['peer' => $update, 'file' => 'tests/mosconi.mp3', 'message' => ['_' => 'decryptedMessage', 'ttl' => 0, 'message' => '', 'media' => ['_' => 'decryptedMessageMediaDocument', 'thumb' => \file_get_contents('tests/faust.preview.jpg'), 'thumb_w' => 90, 'thumb_h' => 90, 'mime_type' => \mime_content_type('tests/mosconi.mp3'), 'caption' => 'test', 'file_name' => 'mosconi.mp3', 'size' => \filesize('tests/mosconi.mp3'), 'attributes' => [['_' => 'documentAttributeAudio', 'voice' => false, 'duration' => 1, 'title' => 'AH NON LO SO IO', 'performer' => 'IL DIO GERMANO MOSCONI']]]]];
$secret_media['voice'] = ['peer' => $update, 'file' => 'tests/mosconi.mp3', 'message' => ['_' => 'decryptedMessage', 'ttl' => 0, 'message' => '', 'media' => ['_' => 'decryptedMessageMediaDocument', 'thumb' => \file_get_contents('tests/faust.preview.jpg'), 'thumb_w' => 90, 'thumb_h' => 90, 'mime_type' => \mime_content_type('tests/mosconi.mp3'), 'caption' => 'test', 'file_name' => 'mosconi.mp3', 'size' => \filesize('tests/mosconi.mp3'), 'attributes' => [['_' => 'documentAttributeAudio', 'voice' => true, 'duration' => 1, 'title' => 'AH NON LO SO IO', 'performer' => 'IL DIO GERMANO MOSCONI']]]]];
foreach ($secret_media as $type => $smessage) {
$promises = $this->messages->sendEncryptedFile($smessage);
}
yield $promises;
$i = 0;
while ($i < 10) {
$this->logger("SENDING MESSAGE $i TO ".$update['message']['chat_id']);
// You can also use the sendEncrypted parameter for more options in secret chats
yield $this->messages->sendMessage(['peer' => $update, 'message' => (string) ($i++)]);
}
$this->sent[$update['message']['chat_id']] = true;
}
}
@ -139,7 +134,7 @@ if (\file_exists('.env')) {
echo 'Loading settings...'.PHP_EOL;
$settings = \json_decode(\getenv('MTPROTO_SETTINGS'), true) ?: [];
$MadelineProto = new \danog\MadelineProto\API('s.madeline', $settings);
$MadelineProto = new \danog\MadelineProto\API('secret.madeline', $settings);
// Reduce boilerplate with new wrapper method
$MadelineProto->startAndLoop(SecretHandler::class);

18
psalm.xml Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0"?>
<psalm
totallyTyped="false"
errorLevel="6"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="src" />
<ignoreFiles>
<directory name="vendor" />
<file name="src/danog/MadelineProto/InternalDoc.php" />
<file name="src/danog/MadelineProto/TON/InternalDoc.php" />
</ignoreFiles>
</projectFiles>
</psalm>

1
schemas Submodule

@ -0,0 +1 @@
Subproject commit 8fd40f0a4170769465e4943e4a5da37bd58f7e88

View File

@ -1,37 +0,0 @@
<?php
if (!\class_exists('ReflectionGenerator')) {
class ReflectionGenerator
{
private $generator;
public function __construct(Generator $generator)
{
$this->generator = $generator;
}
public function getExecutingFile(): string
{
return '';
}
public function getExecutingGenerator(): Generator
{
return $this->generator;
}
public function getExecutingLine(): int
{
return 0;
}
public function getFunction(): ReflectionFunctionAbstract
{
return new ReflectionFunction(function () {
});
}
public function getThis(): object
{
return $this;
}
public function getTrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT): array
{
return [];
}
}
}

View File

@ -19,33 +19,86 @@
namespace danog\MadelineProto;
use Amp\Promise;
if (!\defined('MADELINEPROTO_TEST')) {
\define('MADELINEPROTO_TEST', 'NOT PONY');
}
use Amp\Ipc\Sync\ChannelledSocket;
use Amp\Loop;
use danog\MadelineProto\Ipc\Client;
use danog\MadelineProto\Ipc\Server;
use danog\MadelineProto\Settings\Ipc as SettingsIpc;
use danog\MadelineProto\Settings\Logger as SettingsLogger;
/**
* Main API wrapper for MadelineProto.
*/
class API extends InternalDoc
{
/**
* Release version.
*
* @var string
*/
const RELEASE = MTProto::RELEASE;
/**
* We're not logged in.
*
* @var int
*/
const NOT_LOGGED_IN = MTProto::NOT_LOGGED_IN;
/**
* We're waiting for the login code.
*
* @var int
*/
const WAITING_CODE = MTProto::WAITING_CODE;
/**
* We're waiting for parameters to sign up.
*
* @var int
*/
const WAITING_SIGNUP = MTProto::WAITING_SIGNUP;
/**
* We're waiting for the 2FA password.
*
* @var int
*/
const WAITING_PASSWORD = MTProto::WAITING_PASSWORD;
/**
* We're logged in.
*
* @var int
*/
const LOGGED_IN = MTProto::LOGGED_IN;
/**
* Secret chat was not found.
*
* @var int
*/
const SECRET_EMPTY = MTProto::SECRET_EMPTY;
/**
* Secret chat was requested.
*
* @var int
*/
const SECRET_REQUESTED = MTProto::SECRET_REQUESTED;
/**
* Secret chat was found.
*
* @var int
*/
const SECRET_READY = MTProto::SECRET_READY;
use \danog\Serializable;
use \danog\MadelineProto\ApiWrappers\Start;
use \danog\MadelineProto\ApiWrappers\Templates;
/**
* Session path.
* Session paths.
*
* @internal
*
* @var string
*/
public string $session = '';
public SessionPaths $session;
/**
* Instance of MadelineProto.
*
* @var ?MTProto
* @var null|MTProto|Client
*/
public $API;
@ -70,7 +123,7 @@ class API extends InternalDoc
*
* @internal
*
* @var ?MyTelegramOrgWrapper
* @var null|MyTelegramOrgWrapper
*/
private $myTelegramOrgWrapper;
@ -87,7 +140,6 @@ class API extends InternalDoc
*/
private bool $destructing = false;
/**
* API wrapper (to avoid circular references).
*
@ -95,21 +147,29 @@ class API extends InternalDoc
*/
private $wrapper;
/**
* Unlock callback.
*
* @var ?callable
*/
private $unlock = null;
/**
* Magic constructor function.
*
* @param string $session Session name
* @param array $settings Settings
* @param string $session Session name
* @param array|Settings $settings Settings
*
* @return void
*/
public function __magic_construct(string $session, array $settings = []): void
public function __magic_construct(string $session, $settings = []): void
{
Magic::classExists(true);
$settings = Settings::parseFromLegacy($settings);
$this->session = new SessionPaths($session);
$this->wrapper = new APIWrapper($this, $this->exportNamespace());
Magic::classExists();
$this->setInitPromise($this->__construct_async($session, $settings));
$this->setInitPromise($this->internalInitAPI($settings));
foreach (\get_class_vars(APIFactory::class) as $key => $var) {
if (\in_array($key, ['namespace', 'API', 'lua', 'async', 'asyncAPIPromise', 'methods'])) {
continue;
@ -122,49 +182,119 @@ class API extends InternalDoc
/**
* Async constructor function.
*
* @param string $session Session name
* @param array $settings Settings
* @param Settings|SettingsEmpty|SettingsIpc $settings Settings
*
* @return \Generator
*/
public function __construct_async(string $session, array $settings = []): \Generator
private function internalInitAPI(SettingsAbstract $settings): \Generator
{
Logger::constructorFromSettings($settings);
$this->session = $session = Absolute::absolute($session);
if ($unserialized = yield from Serialization::legacyUnserialize($session)) {
Logger::constructorFromSettings($settings instanceof Settings
? $settings->getLogger()
: ($settings instanceof SettingsLogger ? $settings : new SettingsLogger));
if (yield from $this->connectToMadelineProto($settings)) {
return; // OK
}
if (!$settings instanceof Settings) {
$newSettings = new Settings;
$newSettings->merge($settings);
$settings = $newSettings;
}
$appInfo = $settings->getAppInfo();
if (!$appInfo->hasApiInfo()) {
$app = yield from $this->APIStart($settings);
if (!$app) {
$this->forceInit(true);
die();
}
$appInfo->setApiId($app['api_id']);
$appInfo->setApiHash($app['api_hash']);
}
$this->API = new MTProto($settings, $this->wrapper);
yield from $this->API->initAsynchronously();
$this->APIFactory();
$this->logger->logger(Lang::$current_lang['madelineproto_ready'], Logger::NOTICE);
}
/**
* Reconnect to full instance.
*
* @return \Generator
*/
protected function reconnectFull(): \Generator
{
if ($this->API instanceof Client) {
yield $this->API->stopIpcServer();
yield $this->API->disconnect();
yield from $this->connectToMadelineProto(new SettingsEmpty, true);
}
}
/**
* Connect to MadelineProto.
*
* @param SettingsAbstract $settings Settings
* @param bool $forceFull Whether to force full initialization
*
* @return \Generator
*/
protected function connectToMadelineProto(SettingsAbstract $settings, bool $forceFull = false): \Generator
{
if ($settings instanceof SettingsIpc) {
$forceFull = $forceFull || $settings->getSlow();
} elseif ($settings instanceof Settings) {
$forceFull = $forceFull || $settings->getIpc()->getSlow();
}
[$unserialized, $this->unlock] = yield Tools::timeoutWithDefault(
Serialization::unserialize($this->session, $settings, $forceFull),
30000,
[0, null]
);
if ($unserialized === 0) {
// Timeout
throw new \RuntimeException("Could not connect to MadelineProto, please check the logs for more details.");
} elseif ($unserialized instanceof \Throwable) {
// IPC server error, try fetching full session
return yield from $this->connectToMadelineProto($settings, true);
} elseif ($unserialized instanceof ChannelledSocket) {
// Success, IPC client
$this->API = new Client($unserialized, $this->session, Logger::$default);
$this->APIFactory();
return true;
} elseif ($unserialized) {
// Success, full session
$unserialized->storage = $unserialized->storage ?? [];
$unserialized->session = $session;
$unserialized->session = $this->session;
APIWrapper::link($this, $unserialized);
APIWrapper::link($this->wrapper, $this);
AbstractAPIFactory::link($this->wrapper->getFactory(), $this);
if (isset($this->API)) {
$this->storage = $this->API->storage ?? $this->storage;
$unserialized->oldInstance = true;
unset($unserialized);
yield from $this->API->initAsynchronously();
if ($settings instanceof SettingsIpc) {
$settings = new SettingsEmpty;
}
yield from $this->API->wakeup($settings, $this->wrapper);
$this->APIFactory();
$this->logger->logger(Lang::$current_lang['madelineproto_ready'], Logger::NOTICE);
return;
return true;
}
}
if (!isset($settings['app_info']['api_id']) || !$settings['app_info']['api_id']) {
$app = (yield from $this->APIStart($settings));
if (!$app) {
$this->forceInit(true);
die();
}
$settings['app_info']['api_id'] = $app['api_id'];
$settings['app_info']['api_hash'] = $app['api_hash'];
}
$this->API = new MTProto($settings);
yield from $this->API->initAsynchronously();
$this->APIFactory();
$this->logger->logger(Lang::$current_lang['madelineproto_ready'], Logger::NOTICE);
return false;
}
/**
* Wakeup function.
*
* @return void
*/
public function __wakeup(): void
{
$this->oldInstance = true;
}
/**
* Destruct function.
*
@ -174,13 +304,22 @@ class API extends InternalDoc
{
$this->init();
if (!$this->oldInstance) {
$this->logger->logger('Shutting down MadelineProto (API)');
$this->logger->logger('Shutting down MadelineProto ('.static::class.')');
$this->destructing = true;
if ($this->API) {
$this->API->destructing = true;
if ($this->API instanceof Tools) {
$this->API->destructing = true;
}
$this->API->unreference();
}
$this->destructing = true;
Tools::wait($this->wrapper->serialize(), true);
if (isset($this->wrapper) && (!Magic::$signaled || $this->gettingApiId)) {
$this->logger->logger('Prompting final serialization...');
Tools::wait($this->wrapper->serialize());
$this->logger->logger('Done final serialization!');
}
if ($this->unlock) {
($this->unlock)();
}
} else {
$this->logger->logger('Shutting down MadelineProto (old deserialized instance of API)');
}
@ -193,16 +332,14 @@ class API extends InternalDoc
private function APIFactory(): void
{
if ($this->API && $this->API->inited()) {
foreach ($this->API->getMethodNamespaces() as $namespace) {
if (!$this->{$namespace}) {
$this->{$namespace} = $this->exportNamespace($namespace);
if ($this->API instanceof MTProto) {
foreach ($this->API->getMethodNamespaces() as $namespace) {
if (!$this->{$namespace}) {
$this->{$namespace} = $this->exportNamespace($namespace);
}
}
}
$this->methods = self::getInternalMethodList($this->API);
$this->API->wrapper = $this->wrapper;
if ($this->API->event_handler && \class_exists($this->API->event_handler) && \is_subclass_of($this->API->event_handler, EventHandler::class)) {
$this->API->setEventHandler($this->API->event_handler);
}
$this->methods = self::getInternalMethodList($this->API, MTProto::class);
}
}
@ -234,7 +371,7 @@ class API extends InternalDoc
* @param API[] $instances Instances of madeline
* @param string[]|string $eventHandler Event handler(s)
*
* @return Promise
* @return void
*/
public static function startAndLoopMulti(array $instances, $eventHandler): void
{
@ -248,7 +385,7 @@ class API extends InternalDoc
$promises = [];
foreach ($instances as $k => $instance) {
$instance->start(['async' => false]);
$promises []= Tools::call($instance->startAndLoopAsync($eventHandler[$k]));
$promises []= $instance->startAndLoopAsync($eventHandler[$k]);
}
Tools::wait(Tools::all($promises));
return;
@ -267,18 +404,90 @@ class API extends InternalDoc
*
* @return \Generator
*/
private function startAndLoopAsync(string $eventHandler): \Generator
public function startAndLoopAsync(string $eventHandler): \Generator
{
$errors = [];
$this->async(true);
if ($this->API instanceof Client) {
yield $this->API->stopIpcServer();
yield $this->API->disconnect();
yield from $this->connectToMadelineProto(new SettingsEmpty, true);
}
$started = false;
while (true) {
try {
yield $this->start();
yield $this->setEventHandler($eventHandler);
$started = true;
return yield from $this->API->loop();
} catch (\Throwable $e) {
$errors = [\time() => $errors[\time()] ?? 0];
$errors[\time()]++;
if ($errors[\time()] > 10 && (!$this->inited() || !$started)) {
$this->logger->logger("More than 10 errors in a second and not inited, exiting!", Logger::FATAL_ERROR);
return;
}
echo $e;
$this->logger->logger((string) $e, Logger::FATAL_ERROR);
$this->report("Surfaced: $e");
}
}
}
/**
* Get attribute.
*
* @param string $name Attribute nam
*
* @internal
*
* @return mixed
*/
public function &__get(string $name)
{
if ($name === 'logger') {
if (isset($this->API)) {
return $this->API->logger;
}
return Logger::$default;
}
return $this->storage[$name];
}
/**
* Set an attribute.
*
* @param string $name Name
* @param mixed $value Value
*
* @internal
*
* @return mixed
*/
public function __set(string $name, $value)
{
return $this->storage[$name] = $value;
}
/**
* Whether an attribute exists.
*
* @param string $name Attribute name
*
* @return boolean
*/
public function __isset(string $name): bool
{
return isset($this->storage[$name]);
}
/**
* Unset attribute.
*
* @param string $name Attribute name
*
* @return void
*/
public function __unset(string $name): void
{
unset($this->storage[$name]);
}
}

View File

@ -21,6 +21,12 @@ namespace danog\MadelineProto;
class APIFactory extends AbstractAPIFactory
{
/**
* @internal this is a internal property generated by build_docs.php, don't change manually
*
* @var stats
*/
public $stats;
/**
* @internal this is a internal property generated by build_docs.php, don't change manually
*

View File

@ -20,25 +20,23 @@ namespace danog\MadelineProto;
use Amp\Promise;
use Amp\Success;
use danog\MadelineProto\Ipc\Client;
use function Amp\File\put;
use function Amp\File\rename as renameAsync;
use function Amp\File\open;
final class APIWrapper
{
/**
* MTProto instance.
*
* @var ?MTProto
* @var MTProto|null|Client
*/
private ?MTProto $API = null;
private $API = null;
/**
* Session path.
*
* @var string
*/
public string $session = '';
public SessionPaths $session;
/**
* Getting API ID flag.
@ -90,6 +88,11 @@ final class APIWrapper
* @var AbstractAPIFactory
*/
private AbstractAPIFactory $factory;
/**
* Property storage.
*/
public array $storage = [];
/**
* API wrapper.
*
@ -112,30 +115,38 @@ final class APIWrapper
*/
public static function link($a, $b): void
{
foreach (self::__sleep() as $var) {
foreach (self::properties() as $var) {
Tools::setVar($a, $var, Tools::getVar($b, $var));
}
Tools::setVar($a, 'session', Tools::getVar($b, 'session'));
}
/**
* Sleep function.
*
* @internal
* Property list.
*
* @return array
*/
public static function __sleep(): array
public static function properties(): array
{
return ['API', 'webApiTemplate', 'gettingApiId', 'myTelegramOrgWrapper', 'storage', 'lua', 'async'];
return ['API', 'webApiTemplate', 'gettingApiId', 'myTelegramOrgWrapper', 'storage', 'lua'];
}
/**
* Sleep function.
*
* @return array
*/
public function __sleep(): array
{
return self::properties();
}
/**
* Get MTProto instance.
*
* @return MTProto|null
* @return Client|MTProto|null
*/
public function &getAPI(): ?MTProto
public function &getAPI()
{
return $this->API;
}
@ -161,54 +172,60 @@ final class APIWrapper
}
/**
* Serialie session.
* Get IPC path.
*
* @return Promise
* @internal
*
* @return string
*/
public function getIpcPath(): string
{
return $this->session->getIpcPath();
}
/**
* Serialize session.
*
* @return Promise<bool>
*/
public function serialize(): Promise
{
if (!$this->session) {
Logger::log("Not serializing, no session");
return new Success();
if ($this->API === null && !$this->gettingApiId) {
return new Success(false);
}
if ($this->API instanceof Client) {
return new Success(false);
}
return Tools::callFork((function (): \Generator {
if (isset($this->API->flushSettings) && $this->API->flushSettings) {
$this->API->flushSettings = false;
$this->API->__construct($this->API->settings);
}
if ($this->API === null && !$this->gettingApiId) {
return false;
}
if ($this->API) {
yield from $this->API->initAsynchronously();
}
$this->serialized = \time();
$realpaths = Serialization::realpaths($this->session);
//Logger::log('Waiting for exclusive lock of serialization lockfile...');
$unlock = yield Tools::flock($realpaths['lockfile'], LOCK_EX);
//Logger::log('Lock acquired, serializing');
try {
if (!$this->gettingApiId) {
$update_closure = $this->API->settings['updates']['callback'];
if ($this->API->settings['updates']['callback'] instanceof \Closure) {
$this->API->settings['updates']['callback'] = [$this->API, 'noop'];
}
$logger_closure = $this->API->settings['logger']['logger_param'];
if ($this->API->settings['logger']['logger_param'] instanceof \Closure) {
$this->API->settings['logger']['logger_param'] = [$this->API, 'noop'];
}
}
$wrote = yield put($realpaths['tempfile'], \serialize($this));
yield renameAsync($realpaths['tempfile'], $realpaths['file']);
} finally {
if (!$this->gettingApiId) {
$this->API->settings['updates']['callback'] = $update_closure;
$this->API->settings['logger']['logger_param'] = $logger_closure;
}
$unlock();
yield from $this->session->serialize(
$this->API ? yield from $this->API->serializeSession($this) : $this,
$this->session->getSessionPath()
);
if ($this->API) {
yield from $this->session->storeLightState($this->API);
}
//Logger::log('Done serializing');
return $wrote;
// Truncate legacy session
yield (yield open($this->session->getLegacySessionPath(), 'w'))->close();
Logger::log('Saved session!');
return true;
})());
}
/**
* Get session path.
*
* @return SessionPaths
*/
public function getSession(): SessionPaths
{
return $this->session;
}
}

View File

@ -20,6 +20,7 @@
namespace danog\MadelineProto;
use danog\MadelineProto\Async\AsyncConstruct;
use danog\MadelineProto\Ipc\Client;
abstract class AbstractAPIFactory extends AsyncConstruct
{
@ -31,14 +32,6 @@ abstract class AbstractAPIFactory extends AsyncConstruct
* @var string
*/
private string $namespace = '';
/**
* MTProto instance.
*
* @internal
*
* @var MTProto
*/
public $API;
/**
* Whether lua is being used.
*
@ -58,9 +51,15 @@ abstract class AbstractAPIFactory extends AsyncConstruct
/**
* Method list.
*
* @var string[]
* @var array<string, callable>
*/
protected array $methods = [];
/**
* Main API instance.
*/
private API $mainAPI;
/**
* Export APIFactory instance with the specified namespace.
*
@ -88,10 +87,21 @@ abstract class AbstractAPIFactory extends AsyncConstruct
*/
protected static function link(self $a, self $b): void
{
$a->API =& $b->API;
$a->lua =& $b->lua;
$a->async =& $b->async;
$a->methods =& $b->methods;
if ($b instanceof API) {
$a->mainAPI = $b;
$b->mainAPI = $b;
} elseif ($a instanceof API) {
$a->mainAPI = $a;
$b->mainAPI = $a;
} else {
$a->mainAPI =& $b->mainAPI;
}
if (!$b->inited()) {
$a->setInitPromise($b->initAsynchronously());
}
}
/**
* Enable or disable async.
@ -142,13 +152,24 @@ abstract class AbstractAPIFactory extends AsyncConstruct
*/
public function __debugInfo(): array
{
$keys = APIWrapper::__sleep();
$keys = APIWrapper::properties();
$res = [];
foreach ($keys as $key) {
$res[$key] = Tools::getVar($this, $key);
if (Tools::hasVar($this, $key)) {
$res[$key] = Tools::getVar($this, $key);
}
}
return $res;
}
/**
* Sleep function.
*
* @return array
*/
public function __sleep()
{
return [];
}
/**
* Call async wrapper function.
*
@ -162,15 +183,18 @@ abstract class AbstractAPIFactory extends AsyncConstruct
public function __call_async(string $name, array $arguments): \Generator
{
yield from $this->initAsynchronously();
$lower_name = \strtolower($name);
if ($this->namespace !== '' || !isset($this->methods[$lower_name])) {
$name = $this->namespace.$name;
$aargs = isset($arguments[1]) && \is_array($arguments[1]) ? $arguments[1] : [];
$aargs['apifactory'] = true;
$aargs['datacenter'] = $this->API->datacenter->curdc;
$args = isset($arguments[0]) && \is_array($arguments[0]) ? $arguments[0] : [];
return yield from $this->API->methodCallAsyncRead($name, $args, $aargs);
return yield from $this->mainAPI->API->methodCallAsyncRead($name, $args, $aargs);
}
if ($lower_name === 'seteventhandler'
|| ($lower_name === 'loop' && !isset($arguments[0]))
) {
yield from $this->mainAPI->reconnectFull();
}
$res = $this->methods[$lower_name](...$arguments);
return $res instanceof \Generator ? yield from $res : yield $res;
@ -178,24 +202,29 @@ abstract class AbstractAPIFactory extends AsyncConstruct
/**
* Get fully resolved method list for object, including snake_case and camelCase variants.
*
* @param API $value Value
* @param API|MTProto|Client $value Value
* @param string $class Custom class name
*
* @return array
*/
protected static function getInternalMethodList($value): array
protected static function getInternalMethodList($value, string $class = null): array
{
return \array_map(fn ($method) => [$value, $method], self::getInternalMethodListClass($class ?? \get_class($value)));
}
/**
* Get fully resolved method list for object, including snake_case and camelCase variants.
*
* @param string $class Class name
*
* @return array
*/
protected static function getInternalMethodListClass(string $class): array
{
static $cache = [];
$class = \get_class($value);
if (isset($cache[$class])) {
return \array_map(
static function ($v) use ($value) {
return [$value, $v];
},
$cache[$class]
);
return $cache[$class];
}
$methods = \get_class_methods($value);
$methods = \get_class_methods($class);
foreach ($methods as $method) {
if ($method == 'methodCallAsyncRead') {
unset($methods[\array_search('methodCall', $methods)]);
@ -223,66 +252,11 @@ abstract class AbstractAPIFactory extends AsyncConstruct
if (\strpos($method, '_') !== false) {
$finalMethods[\strtolower(\str_replace('_', '', $method))] = $actual_method;
} else {
$finalMethods[\strtolower(Tools::toSnakeCase($method))] = $actual_method;
$finalMethods[\strtolower(StrTools::toSnakeCase($method))] = $actual_method;
}
}
$cache[$class] = $finalMethods;
return self::getInternalMethodList($value);
}
/**
* Get attribute.
*
* @param string $name Attribute nam
*
* @internal
*
* @return mixed
*/
public function &__get(string $name)
{
if ($name === 'logger') {
if (isset($this->API)) {
return $this->API->logger;
}
return Logger::$default;
}
return $this->storage[$name];
}
/**
* Set an attribute.
*
* @param string $name Name
* @param mixed $value Value
*
* @internal
*
* @return mixed
*/
public function __set(string $name, $value)
{
return $this->storage[$name] = $value;
}
/**
* Whether an attribute exists.
*
* @param string $name Attribute name
*
* @return boolean
*/
public function __isset(string $name): bool
{
return isset($this->storage[$name]);
}
/**
* Unset attribute.
*
* @param string $name Attribute name
*
* @return void
*/
public function __unset(string $name): void
{
unset($this->storage[$name]);
return $finalMethods;
}
}

View File

@ -20,29 +20,57 @@
namespace danog\MadelineProto;
use Amp\Promise;
use danog\MadelineProto\Settings\TLSchema;
use danog\MadelineProto\TL\TL;
use danog\MadelineProto\TL\TLCallback;
use phpDocumentor\Reflection\DocBlockFactory;
class AnnotationsBuilder
{
use Tools;
/**
* Reflection classes.
*/
private array $reflectionClasses = [];
/**
* Logger.
*/
private Logger $logger;
/**
* Namespace.
*/
private string $namespace;
/**
* TL instance.
*/
private TL $TL;
/**
* Settings.
*/
private array $settings;
/**
* Output file.
*/
private string $output;
public function __construct(Logger $logger, array $settings, string $output, array $reflectionClasses, string $namespace)
{
$this->reflectionClasses = $reflectionClasses;
$this->logger = $logger;
$this->namespace = $namespace;
/** @psalm-suppress InvalidArgument */
$this->TL = new TL(new class($logger) {
public function __construct($logger)
public Logger $logger;
public function __construct(Logger $logger)
{
$this->logger = $logger;
}
});
$this->TL->init($settings['tl_schema']);
$tlSchema = new TLSchema;
$tlSchema->mergeArray($settings);
$this->TL->init($tlSchema);
$this->settings = $settings;
$this->output = $output;
}
public function mkAnnotations()
public function mkAnnotations(): void
{
\danog\MadelineProto\Logger::log('Generating annotations...', \danog\MadelineProto\Logger::NOTICE);
$this->setProperties();
@ -65,12 +93,12 @@ class AnnotationsBuilder
if ($raw_docblock = $property->getDocComment()) {
$docblock = $fixture->create($raw_docblock);
if ($docblock->hasTag('internal')) {
$content = \str_replace("\n " . $raw_docblock . "\n public \$" . $property->getName() . ';', '', $content);
$content = \str_replace("\n ".$raw_docblock."\n public \$".$property->getName().';', '', $content);
}
}
}
foreach ($this->TL->getMethodNamespaces() as $namespace) {
$content = \preg_replace('/(class( \\w+[,]?){0,}\\n{\\n)/', '${1}' . " /**\n" . " * @internal this is a internal property generated by build_docs.php, don't change manually\n" . " *\n" . " * @var {$namespace}\n" . " */\n" . " public \${$namespace};\n", $content);
$content = \preg_replace('/(class( \\w+[,]?){0,}\\n{\\n)/', '${1}'." /**\n"." * @internal this is a internal property generated by build_docs.php, don't change manually\n"." *\n"." * @var {$namespace}\n"." */\n"." public \${$namespace};\n", $content);
}
\file_put_contents($filename, $content);
}
@ -106,7 +134,7 @@ class AnnotationsBuilder
if (!\in_array($namespace, $this->TL->getMethodNamespaces())) {
continue;
}
$internalDoc[$namespace][$method]['title'] = \str_replace(['](../', '.md'], ['](https://docs.madelineproto.xyz/API_docs/', '.html'], Lang::$current_lang["method_{$data['method']}"] ?? '');
$internalDoc[$namespace][$method]['title'] = \str_replace(['](../', '.md'], ['](https://docs.madelineproto.xyz/API_docs/', '.html'], Lang::$lang['en']["method_{$data['method']}"] ?? '');
$type = \str_ireplace(['vector<', '>'], [' of ', '[]'], $data['type']);
foreach ($data['params'] as $param) {
if (\in_array($param['name'], ['flags', 'random_id', 'random_bytes'])) {
@ -136,7 +164,7 @@ class AnnotationsBuilder
}
$ptype = $stype === 'type' ? $ptype : "[{$ptype}]";
$opt = $param['pow'] ?? false ? 'Optional: ' : '';
$internalDoc[$namespace][$method]['attr'][$param['name']] = ['type' => $ptype, 'description' => \str_replace(['](../', '.md'], ['](https://docs.madelineproto.xyz/API_docs/', '.html'], $opt . (Lang::$current_lang["method_{$data['method']}_param_{$param['name']}_type_{$param['type']}"] ?? ''))];
$internalDoc[$namespace][$method]['attr'][$param['name']] = ['type' => $ptype, 'description' => \str_replace(['](../', '.md'], ['](https://docs.madelineproto.xyz/API_docs/', '.html'], $opt.(Lang::$lang['en']["method_{$data['method']}_param_{$param['name']}_type_{$param['type']}"] ?? ''))];
}
if ($type === 'Bool') {
$type = \strtolower($type);
@ -144,7 +172,9 @@ class AnnotationsBuilder
$internalDoc[$namespace][$method]['return'] = $type;
}
$class = new \ReflectionClass($this->reflectionClasses['MTProto']);
$methods = $class->getMethods(\ReflectionMethod::IS_STATIC | \ReflectionMethod::IS_PUBLIC);
$methods = $class->getMethods((\ReflectionMethod::IS_STATIC & \ReflectionMethod::IS_PUBLIC) | \ReflectionMethod::IS_PUBLIC);
$class = new \ReflectionClass(Tools::class);
$methods = \array_merge($methods, $class->getMethods((\ReflectionMethod::IS_STATIC & \ReflectionMethod::IS_PUBLIC) | \ReflectionMethod::IS_PUBLIC));
foreach ($methods as $key => $method) {
$name = $method->getName();
if ($method == 'methodCallAsyncRead') {
@ -159,6 +189,14 @@ class AnnotationsBuilder
}
}
}
$sortedMethods = [];
foreach ($methods as $method) {
$sortedMethods[$method->getName()] = $method;
}
\ksort($sortedMethods);
$methods = \array_values($sortedMethods);
foreach ($methods as $method) {
$name = $method->getName();
if (isset($ignoreMethods[$name])) {
@ -184,7 +222,7 @@ class AnnotationsBuilder
$name = \str_ireplace('async', '', $name);
}
}
$name = Tools::toCamelCase($name);
$name = StrTools::toCamelCase($name);
$name = \str_ireplace(['mtproto', 'api'], ['MTProto', 'API'], $name);
$doc = 'public function ';
$doc .= $name;
@ -205,7 +243,7 @@ class AnnotationsBuilder
$doc .= $type->getName();
$doc .= ' ';
} else {
Logger::log($name . '.' . $param->getName() . " has no type!", Logger::WARNING);
Logger::log($name.'.'.$param->getName()." has no type!", Logger::WARNING);
}
if ($param->isVariadic()) {
$doc .= '...';
@ -218,7 +256,7 @@ class AnnotationsBuilder
if ($param->isOptional() && !$param->isVariadic()) {
$doc .= ' = ';
if ($param->isDefaultValueConstant()) {
$doc .= '\\' . \str_replace(['NULL', 'self'], ['null', 'danog\\MadelineProto\\MTProto'], $param->getDefaultValueConstantName());
$doc .= '\\'.\str_replace(['NULL', 'self'], ['null', 'danog\\MadelineProto\\MTProto'], $param->getDefaultValueConstantName());
} else {
$doc .= \str_replace('NULL', 'null', \var_export($param->getDefaultValue(), true));
}
@ -228,7 +266,7 @@ class AnnotationsBuilder
$hasVariadic = true;
$paramList .= '...';
}
$paramList .= '$' . $param->getName() . ', ';
$paramList .= '$'.$param->getName().', ';
}
$hasReturnValue = ($type = $method->getReturnType()) && !\in_array($type->getName(), [\Generator::class, Promise::class]);
if (!$hasVariadic && !$static && !$hasReturnValue) {
@ -239,7 +277,7 @@ class AnnotationsBuilder
$paramList = \rtrim($paramList, ', ');
$doc .= ")";
$async = true;
if ($hasReturnValue) {
if ($hasReturnValue && $static) {
$doc .= ': ';
if ($type->allowsNull()) {
$doc .= '?';
@ -250,6 +288,9 @@ class AnnotationsBuilder
$doc .= $type->getName() === 'self' ? $this->reflectionClasses['API'] : $type->getName();
$async = false;
}
if ($method->getDeclaringClass()->getName() == Tools::class) {
$async = false;
}
$finalParamList = $hasVariadic ? "Tools::arr({$paramList})" : "[{$paramList}]";
$ret = $type && \in_array($type->getName(), ['self', 'void']) ? '' : 'return';
$doc .= "\n{\n";
@ -258,7 +299,7 @@ class AnnotationsBuilder
} elseif (!$static) {
$doc .= " {$ret} \$this->API->{$name}({$paramList});\n";
} else {
$doc .= " {$ret} \\" . $method->getDeclaringClass()->getName() . "::" . $name . "({$paramList});\n";
$doc .= " {$ret} \\".$method->getDeclaringClass()->getName()."::".$name."({$paramList});\n";
}
if (!$ret && $type->getName() === 'self') {
$doc .= " return \$this;\n";
@ -270,8 +311,36 @@ class AnnotationsBuilder
if (!$type) {
Logger::log("{$name} has no return type!", Logger::FATAL_ERROR);
}
$internalDoc['InternalDoc'][$name]['method'] = $method->getDocComment() ?? '';
$internalDoc['InternalDoc'][$name]['method'] .= "\n " . \implode("\n ", \explode("\n", $doc));
$promise = '\\'.Promise::class;
$phpdoc = $method->getDocComment() ?? '';
$phpdoc = \str_replace("@return \\Generator", "@return $promise", $phpdoc);
$phpdoc = \str_replace("@return \\Promise", "@return $promise", $phpdoc);
$phpdoc = \str_replace("@return Promise", "@return $promise", $phpdoc);
if ($hasReturnValue && $async && \preg_match("/@return (.*)/", $phpdoc, $matches)) {
$ret = $matches[1];
$new = $ret;
if ($type && !str_contains($ret, '<')) {
$new = '';
if ($type->allowsNull()) {
$new .= '?';
}
if (!$type->isBuiltin()) {
$new .= '\\';
}
$new .= $type->getName() === 'self' ? $this->reflectionClasses['API'] : $type->getName();
}
$phpdoc = \str_replace("@return ".$ret, "@return mixed", $phpdoc);
if (!str_contains($phpdoc, '@psalm-return')) {
$phpdoc = \str_replace("@return ", "@psalm-return $new|$promise<$new>\n * @return ", $phpdoc);
}
}
$phpdoc = \preg_replace(
"/@psalm-return \\\\Generator<(?:[^,]+), (?:[^,]+), (?:[^,]+), (.+)>/",
"@psalm-return $promise<$1>",
$phpdoc
);
$internalDoc['InternalDoc'][$name]['method'] = $phpdoc;
$internalDoc['InternalDoc'][$name]['method'] .= "\n ".\implode("\n ", \explode("\n", $doc));
}
\fwrite($handle, "<?php\n");
\fwrite($handle, "/**\n");
@ -304,8 +373,8 @@ class AnnotationsBuilder
$longest[2] = \max($longest[2], \strlen($param['description']));
}
foreach ($properties['attr'] as $name => $param) {
$param['type'] = \str_pad('`' . $param['type'] . '`', $longest[0] + 2);
$name = \str_pad('**' . $name . '**', $longest[1] + 4);
$param['type'] = \str_pad('`'.$param['type'].'`', $longest[0] + 2);
$name = \str_pad('**'.$name.'**', $longest[1] + 4);
$param['description'] = \str_pad($param['description'], $longest[2]);
\fwrite($handle, " * * {$param['type']} {$name} - {$param['description']}\n");
}

View File

@ -19,7 +19,10 @@
namespace danog\MadelineProto\ApiWrappers;
use danog\MadelineProto\Lang;
use danog\MadelineProto\Magic;
use danog\MadelineProto\MyTelegramOrgWrapper;
use danog\MadelineProto\Settings;
use danog\MadelineProto\Tools;
use function Amp\ByteStream\getStdout;
@ -31,37 +34,47 @@ trait Start
/**
* Start API ID generation process.
*
* @param array $settings Settings
* @param Settings $settings Settings
*
* @return \Generator
*/
private function APIStart(array $settings): \Generator
private function APIStart(Settings $settings): \Generator
{
if (PHP_SAPI === 'cli') {
if (Magic::$isIpcWorker) {
throw new \danog\MadelineProto\Exception('Not inited!');
}
if ($this->getWebAPITemplate() === 'legacy') {
$this->setWebAPITemplate($settings->getTemplates()->getHtmlTemplate());
}
$app = null;
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
$stdout = getStdout();
yield $stdout->write('You did not define a valid API ID/API hash. Do you want to define it now manually, or automatically? (m/a)
Note that you can also provide the API parameters directly in the code using the settings: https://docs.madelineproto.xyz/docs/SETTINGS.html#settingsapp_infoapi_id' . PHP_EOL);
if (\strpos(yield Tools::readLine('Your choice (m/a): '), 'm') !== false) {
yield $stdout->write('1) Login to my.telegram.org
2) Go to API development tools
3) App title: your app\'s name, can be anything
Short name: your app\'s short name, can be anything
URL: your app/website\'s URL, or t.me/yourusername
Platform: anything
Description: Describe your app here
4) Click on create application' . PHP_EOL);
$app['api_id'] = yield Tools::readLine('5) Enter your API ID: ');
$app['api_hash'] = yield Tools::readLine('6) Enter your API hash: ');
$prepare = Lang::$current_lang['apiChooseManualAuto'].PHP_EOL;
$prepare .= \sprintf(Lang::$current_lang['apiChooseManualAutoTip'], 'https://docs.madelineproto.xyz/docs/SETTINGS.html#settingsapp_infoapi_id');
$prepare .= PHP_EOL;
yield $stdout->write($prepare);
if (\strpos(yield Tools::readLine(Lang::$current_lang['apiChoosePrompt']), 'm') !== false) {
yield $stdout->write("1) ".Lang::$current_lang['apiManualInstructions0'].PHP_EOL);
yield $stdout->write("2) ".Lang::$current_lang['apiManualInstructions1'].PHP_EOL);
yield $stdout->write("3) ");
foreach (['App title', 'Short name', 'URL', 'Platform', 'Description'] as $k => $key) {
yield $stdout->write($k ? " $key: " : "$key: ");
yield $stdout->write(Lang::$current_lang["apiAppInstructionsManual$k"].PHP_EOL);
}
yield $stdout->write("4) ".Lang::$current_lang['apiManualInstructions2'].PHP_EOL);
$app['api_id'] = yield Tools::readLine("5) ".Lang::$current_lang['apiManualPrompt0']);
$app['api_hash'] = yield Tools::readLine("6) ".Lang::$current_lang['apiManualPrompt1']);
return $app;
}
$this->myTelegramOrgWrapper = new \danog\MadelineProto\MyTelegramOrgWrapper($settings);
yield from $this->myTelegramOrgWrapper->login(yield Tools::readLine('Enter a phone number that is already registered on Telegram: '));
yield from $this->myTelegramOrgWrapper->completeLogin(yield Tools::readLine('Enter the verification code you received in telegram: '));
yield from $this->myTelegramOrgWrapper->login(yield Tools::readLine(Lang::$current_lang['apiAutoPrompt0']));
yield from $this->myTelegramOrgWrapper->completeLogin(yield Tools::readLine(Lang::$current_lang['apiAutoPrompt1']));
if (!(yield from $this->myTelegramOrgWrapper->hasApp())) {
$app_title = yield Tools::readLine('Enter the app\'s name, can be anything: ');
$short_name = yield Tools::readLine('Enter the app\'s short name, can be anything: ');
$url = yield Tools::readLine('Enter the app/website\'s URL, or t.me/yourusername: ');
$description = yield Tools::readLine('Describe your app: ');
$app_title = yield Tools::readLine(Lang::$current_lang['apiAppInstructionsAuto0']);
$short_name = yield Tools::readLine(Lang::$current_lang['apiAppInstructionsAuto1']);
$url = yield Tools::readLine(Lang::$current_lang['apiAppInstructionsAuto2']);
$description = yield Tools::readLine(Lang::$current_lang['apiAppInstructionsAuto4']);
$app = (yield from $this->myTelegramOrgWrapper->createApp(['app_title' => $app_title, 'app_shortname' => $short_name, 'app_url' => $url, 'app_platform' => 'web', 'app_desc' => $description]));
} else {
$app = (yield from $this->myTelegramOrgWrapper->getApp());
@ -78,7 +91,7 @@ Note that you can also provide the API parameters directly in the code using the
} elseif (isset($_POST['phone_number'])) {
yield from $this->webAPIPhoneLogin($settings);
} else {
yield from $this->webAPIEcho();
yield $this->webAPIEcho();
}
} elseif (!$this->myTelegramOrgWrapper->loggedIn()) {
if (isset($_POST['code'])) {
@ -86,7 +99,7 @@ Note that you can also provide the API parameters directly in the code using the
if (yield from $this->myTelegramOrgWrapper->hasApp()) {
return yield from $this->myTelegramOrgWrapper->getApp();
}
yield from $this->webAPIEcho();
yield $this->webAPIEcho();
} elseif (isset($_POST['api_id']) && isset($_POST['api_hash'])) {
$app['api_id'] = (int) $_POST['api_id'];
$app['api_hash'] = $_POST['api_hash'];
@ -96,7 +109,7 @@ Note that you can also provide the API parameters directly in the code using the
yield from $this->webAPIPhoneLogin($settings);
} else {
$this->myTelegramOrgWrapper = null;
yield from $this->webAPIEcho();
yield $this->webAPIEcho();
}
} else {
if (isset($_POST['app_title'], $_POST['app_shortname'], $_POST['app_url'], $_POST['app_platform'], $_POST['app_desc'])) {
@ -104,18 +117,18 @@ Note that you can also provide the API parameters directly in the code using the
$this->gettingApiId = false;
return $app;
}
yield from $this->webAPIEcho("You didn't provide all of the required parameters!");
yield $this->webAPIEcho(Lang::$current_lang['apiParamsError']);
}
return null;
}
private function webAPIPhoneLogin(array $settings): \Generator
private function webAPIPhoneLogin(Settings $settings): \Generator
{
try {
$this->myTelegramOrgWrapper = new MyTelegramOrgWrapper($settings);
yield from $this->myTelegramOrgWrapper->login($_POST['phone_number']);
yield from $this->webAPIEcho();
yield $this->webAPIEcho();
} catch (\Throwable $e) {
yield from $this->webAPIEcho('ERROR: ' . $e->getMessage() . '. Try again.');
yield $this->webAPIEcho(\sprintf(Lang::$current_lang['apiError'], $e->getMessage()));
}
}
private function webAPICompleteLogin(): \Generator
@ -123,9 +136,9 @@ Note that you can also provide the API parameters directly in the code using the
try {
yield from $this->myTelegramOrgWrapper->completeLogin($_POST['code']);
} catch (\danog\MadelineProto\RPCErrorException $e) {
yield from $this->webAPIEcho('ERROR: ' . $e->getMessage() . '. Try again.');
yield $this->webAPIEcho(\sprintf(Lang::$current_lang['apiError'], $e->getMessage()));
} catch (\danog\MadelineProto\Exception $e) {
yield from $this->webAPIEcho('ERROR: ' . $e->getMessage() . '. Try again.');
yield $this->webAPIEcho(\sprintf(Lang::$current_lang['apiError'], $e->getMessage()));
}
}
private function webAPICreateApp(): \Generator
@ -136,9 +149,9 @@ Note that you can also provide the API parameters directly in the code using the
$app = (yield from $this->myTelegramOrgWrapper->createApp($params));
return $app;
} catch (\danog\MadelineProto\RPCErrorException $e) {
yield from $this->webAPIEcho('ERROR: ' . $e->getMessage() . ' Try again.');
yield $this->webAPIEcho(\sprintf(Lang::$current_lang['apiError'], $e->getMessage()));
} catch (\danog\MadelineProto\Exception $e) {
yield from $this->webAPIEcho('ERROR: ' . $e->getMessage() . ' Try again.');
yield $this->webAPIEcho(\sprintf(Lang::$current_lang['apiError'], $e->getMessage()));
}
}
}

View File

@ -19,6 +19,9 @@
namespace danog\MadelineProto\ApiWrappers;
use Amp\Promise;
use danog\MadelineProto\Lang;
use function Amp\ByteStream\getOutputBufferStream;
trait Templates
@ -28,7 +31,7 @@ trait Templates
*
* @var string
*/
private $webApiTemplate = '<!DOCTYPE html><html><head><title>MadelineProto</title></head><body><h1>MadelineProto</h1><p>%s</p><form method="POST">%s<button type="submit"/>Go</button></form></body></html>';
private $webApiTemplate = 'legacy';
/**
* Generate page from template.
*
@ -39,7 +42,7 @@ trait Templates
*/
private function webAPIEchoTemplate(string $message, string $form): string
{
return \sprintf($this->webApiTemplate, $message, $form);
return \sprintf($this->webApiTemplate, $message, $form, Lang::$current_lang['go']);
}
/**
* Get web API login HTML template string.
@ -53,7 +56,7 @@ trait Templates
/**
* Set web API login HTML template string.
*
* @return string
* @return void
*/
public function setWebAPITemplate(string $template): void
{
@ -64,72 +67,86 @@ trait Templates
*
* @param string $message Message to echo
*
* @return \Generator
* @return Promise
*/
private function webAPIEcho(string $message = ''): \Generator
private function webAPIEcho(string $message = ''): Promise
{
$stdout = getOutputBufferStream();
$message = \htmlentities($message);
if (!isset($this->myTelegramOrgWrapper)) {
if (isset($_POST['type'])) {
if ($_POST['type'] === 'manual') {
yield $stdout->write($this->webAPIEchoTemplate('Enter your API ID and API hash<br><b>'.$message.'</b><ol>
<li>Login to my.telegram.org</li>
<li>Go to API development tools</li>
<li>
<ul>
<li>App title: your app&apos;s name, can be anything</li>
<li>Short name: your app&apos;s short name, only numbers and letters</li>
<li>Platform: Web</li>
<li>Description: describe your app here</li>
</ul>
</li>
<li>Click on create application</li>
</ol>', '<input type="string" name="api_id" placeholder="API ID" required/><input type="string" name="api_hash" placeholder="API hash" required/>'));
$title = \htmlentities(Lang::$current_lang['apiManualWeb']);
$title .= "<br><b>$message</b>";
$title .= "<ol>";
$title .= "<li>".\htmlentities(Lang::$current_lang['apiManualInstructions0'])."</li>";
$title .= "<li>".\htmlentities(Lang::$current_lang['apiManualInstructions1'])."</li>";
$title .= "<li><ul>";
foreach (['App title', 'Short name', 'URL', 'Platform', 'Description'] as $k => $key) {
$title .= "<li>$key: ";
$title .= \htmlentities(Lang::$current_lang["apiAppInstructionsManual$k"]);
$title .= "</li>";
}
$title .= "</li></ul>";
$title .= "<li>".\htmlentities(Lang::$current_lang['apiManualInstructions2'])."</li>";
$title .= "</ol>";
$form = '<input type="string" name="api_id" placeholder="API ID" required/>';
$form .= '<input type="string" name="api_hash" placeholder="API hash" required/>';
} else {
yield $stdout->write($this->webAPIEchoTemplate('Enter a phone number that is <b>already registered</b> on telegram to get the API ID<br><b>'.$message.'</b>', '<input type="text" name="phone_number" placeholder="Phone number" required/>'));
$title = Lang::$current_lang['apiAutoWeb'];
$title .= "<br><b>$message</b>";
$phone = \htmlentities(Lang::$current_lang['loginUserPhoneWeb']);
$form = "<input type='text' name='phone_number' placeholder='$phone' required/>";
}
} else {
if ($message) {
$message = '<br><br>'.$message;
}
yield $stdout->write($this->webAPIEchoTemplate('Do you want to enter the API id and the API hash manually or automatically?<br>Note that you can also provide it directly in the code using the <a href="https://docs.madelineproto.xyz/docs/SETTINGS.html#settingsapp_infoapi_id">settings</a>.<b>'.$message.'</b>', '<select name="type"><option value="automatic">Automatically</option><option value="manual">Manually</option></select>'));
$title = \htmlentities(Lang::$current_lang['apiChooseManualAutoWeb']);
$title .= "<br>";
$title .= \sprintf(Lang::$current_lang['apiChooseManualAutoTipWeb'], 'https://docs.madelineproto.xyz/docs/SETTINGS.html#settingsapp_infoapi_id');
$title .= "<b>$message</b>";
$automatically = \htmlentities(Lang::$current_lang['apiChooseAutomaticallyWeb']);
$manually = \htmlentities(Lang::$current_lang['apiChooseManuallyWeb']);
$form = "<select name='type'><option value='automatic'>$automatically</option><option value='manual'>$manually</option></select>";
}
} else {
if (!$this->myTelegramOrgWrapper->loggedIn()) {
yield $stdout->write($this->webAPIEchoTemplate('Enter your code<br><b>'.$message.'</b>', '<input type="text" name="code" placeholder="Code" required/>'));
$title = \htmlentities(Lang::$current_lang['loginUserCode']);
$title .= "<br><b>$message</b>";
$code = \htmlentities(Lang::$current_lang['loginUserPhoneCodeWeb']);
$form = "<input type='text' name='code' placeholder='$code' required/>";
} else {
yield $stdout->write($this->webAPIEchoTemplate('Enter the API info<br><b>'.$message.'</b>', '<input type="hidden" name="creating_app" value="yes" required/>
Enter the app name, can be anything: <br><input type="text" name="app_title" required/><br>
<br>Enter the app&apos;s short name, alphanumeric, 5-32 chars: <br><input type="text" name="app_shortname" required/><br>
<br>Enter the app/website URL, or https://t.me/yourusername: <br><input type="text" name="app_url" required/><br>
<br>Enter the app platform: <br>
<label>
<input type="radio" name="app_platform" value="android" checked> Android
</label>
<label>
<input type="radio" name="app_platform" value="ios"> iOS
</label>
<label>
<input type="radio" name="app_platform" value="wp"> Windows Phone
</label>
<label>
<input type="radio" name="app_platform" value="bb"> BlackBerry
</label>
<label>
<input type="radio" name="app_platform" value="desktop"> Desktop
</label>
<label>
<input type="radio" name="app_platform" value="web"> Web
</label>
<label>
<input type="radio" name="app_platform" value="ubp"> Ubuntu phone
</label>
<label>
<input type="radio" name="app_platform" value="other"> Other (specify in description)
</label>
<br><br>Enter the app description, can be anything: <br><textarea name="app_desc" required></textarea><br><br>
'));
$title = \htmlentities(Lang::$current_lang['apiAppWeb']);
$title .= "<br><b>$message</b>";
$form = '<input type="hidden" name="creating_app" value="yes" required/>';
foreach (['app_title', 'app_shortname', 'app_url', 'app_platform', 'app_desc'] as $k => $field) {
$desc = \htmlentities(Lang::$current_lang["apiAppInstructionsAuto$k"]);
if ($field == 'app_platform') {
$form .= "$desc<br>";
foreach ([
'android' => 'Android',
'ios' => 'iOS',
'wp' => 'Windows Phone',
'bb' => 'BlackBerry',
'desktop' => 'Desktop',
'web' => 'Web',
'ubp' => 'Ubuntu phone',
'other' => \htmlentities(Lang::$current_lang['apiAppInstructionsAutoTypeOther'])
] as $key => $desc) {
$form .= "<label><input type='radio' name='app_platform' value='$key' checked> $desc</label>";
}
} elseif ($field === 'app_desc') {
$form .= "$desc<br><textarea name='$field' required></textarea><br><br>";
} else {
$form .= "$desc<br><input type='text' name='$field' required/><br><br>";
}
}
}
}
return getOutputBufferStream()->write($this->webAPIEchoTemplate($title, $form));
}
}

View File

@ -19,6 +19,7 @@
namespace danog\MadelineProto\Async;
use Amp\Promise;
use danog\MadelineProto\Tools;
/**
@ -33,7 +34,7 @@ class AsyncConstruct
/**
* Async init promise.
*
* @var Promise
* @var Promise|null|boolean
*/
private $asyncInitPromise;
/**
@ -92,10 +93,9 @@ class AsyncConstruct
$this->asyncInitPromise = Tools::call($promise);
$this->asyncInitPromise->onResolve(
function (?\Throwable $error, $result): void {
if ($error) {
throw $error;
if (!$error) {
$this->asyncInitPromise = null;
}
$this->asyncInitPromise = null;
}
);
}

View File

@ -1,227 +0,0 @@
<?php
/**
* CombinedAPI module.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
*
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\MadelineProto;
use Amp\Loop;
use function Amp\Promise\all;
class CombinedAPI
{
use \danog\Serializable;
use Tools;
public $session;
public $instance_paths = [];
public $instances = [];
public $timeout = 5;
public $serialization_interval = 30;
public $serialized = 0;
protected $async;
public function __magic_construct($session, $paths = [])
{
\set_error_handler(['\\danog\\MadelineProto\\Exception', 'ExceptionErrorHandler']);
\danog\MadelineProto\Magic::classExists();
$realpaths = Serialization::realpaths($session);
$this->session = $realpaths['file'];
foreach ($paths as $path => $settings) {
$this->addInstance($path, $settings);
}
if (\file_exists($realpaths['file'])) {
if (!\file_exists($realpaths['lockfile'])) {
\touch($realpaths['lockfile']);
\clearstatcache();
}
$realpaths['lockfile'] = \fopen($realpaths['lockfile'], 'r');
\danog\MadelineProto\Logger::log('Waiting for shared lock of serialization lockfile...');
\flock($realpaths['lockfile'], LOCK_SH);
\danog\MadelineProto\Logger::log('Shared lock acquired, deserializing...');
try {
$tounserialize = \file_get_contents($realpaths['file']);
} finally {
\flock($realpaths['lockfile'], LOCK_UN);
\fclose($realpaths['lockfile']);
}
$deserialized = \unserialize($tounserialize);
/*foreach ($deserialized['instance_paths'] as $path) {
$this->addInstance($path, isset($paths[$path]) ? $paths[$path] : []);
}*/
$this->event_handler = $deserialized['event_handler'];
$this->event_handler_instance = $deserialized['event_handler_instance'];
if ($this->event_handler !== null) {
$this->setEventHandler($this->event_handler);
}
}
foreach ($paths as $path => $settings) {
$this->addInstance($path, $settings);
}
}
public function addInstance($path, $settings = [])
{
if (isset($this->instances[$path]) && isset($this->instance_paths[$path])) {
if (isset($this->event_handler_instance)) {
$this->event_handler_instance->referenceInstance($path);
}
return;
}
\danog\MadelineProto\Logger::constructor(3);
\danog\MadelineProto\Logger::log("INSTANTIATING {$path}...");
$instance = new \danog\MadelineProto\API($path, $settings);
$this->instance_paths[$path] = $path;
$this->instances[$path] = $instance;
if (isset($this->event_handler_instance)) {
$this->event_handler_instance->referenceInstance($path);
}
}
public function removeInstance($path)
{
if (isset($this->instance_paths[$path])) {
unset($this->instance_paths[$path]);
}
if (isset($this->instances[$path])) {
unset($this->instances[$path]);
}
if (isset($this->event_handler_instance)) {
$this->event_handler_instance->removeInstance($path);
}
}
public function __destruct()
{
if (\danog\MadelineProto\Magic::$has_thread && \is_object(\Thread::getCurrentThread()) || Magic::isFork()) {
return;
}
$this->serialize();
}
public function serialize($filename = '')
{
/*foreach ($this->instances as $instance) {
$instance->serialize();
}*/
if (\is_null($this->session)) {
return;
}
if ($filename === '') {
$filename = $this->session;
}
Logger::log(\danog\MadelineProto\Lang::$current_lang['serializing_madelineproto']);
$realpaths = Serialization::realpaths($filename);
if (!\file_exists($realpaths['lockfile'])) {
\touch($realpaths['lockfile']);
\clearstatcache();
}
$realpaths['lockfile'] = \fopen($realpaths['lockfile'], 'w');
\danog\MadelineProto\Logger::log('Waiting for exclusive lock of serialization lockfile...');
\flock($realpaths['lockfile'], LOCK_EX);
\danog\MadelineProto\Logger::log('Lock acquired, serializing');
try {
$wrote = \file_put_contents($realpaths['tempfile'], \serialize(['event_handler' => $this->event_handler, 'event_handler_instance' => $this->event_handler_instance, 'instance_paths' => $this->instance_paths]));
\rename($realpaths['tempfile'], $realpaths['file']);
} finally {
\flock($realpaths['lockfile'], LOCK_UN);
\fclose($realpaths['lockfile']);
}
$this->serialized = \time();
return $wrote;
}
public $event_handler;
private $event_handler_instance;
private $event_handler_methods = [];
public function getEventHandler()
{
return $this->event_handler_instance;
}
public function setEventHandler($event_handler)
{
if (!\class_exists($event_handler) || !\is_subclass_of($event_handler, '\\danog\\MadelineProto\\CombinedEventHandler')) {
throw new \danog\MadelineProto\Exception('Wrong event handler was defined');
}
$this->event_handler = $event_handler;
if (!$this->event_handler_instance instanceof $this->event_handler) {
$class_name = $this->event_handler;
$this->event_handler_instance = new $class_name($this);
} else {
$this->event_handler_instance->__construct($this);
}
$this->event_handler_methods = [];
foreach (\get_class_methods($this->event_handler) as $method) {
if ($method === 'onLoop') {
$this->loop_callback = [$this->event_handler_instance, 'onLoop'];
} elseif ($method === 'onAny') {
foreach (\end($this->instances)->API->getTL()->getConstructors()->by_id as $constructor) {
if ($constructor['type'] === 'Update' && !isset($this->event_handler_methods[$constructor['predicate']])) {
$this->event_handler_methods[$constructor['predicate']] = [$this->event_handler_instance, 'onAny'];
}
}
} else {
$method_name = \lcfirst(\substr($method, 2));
$this->event_handler_methods[$method_name] = [$this->event_handler_instance, $method];
}
}
}
public function eventUpdateHandler($update, $instance)
{
if (isset($this->event_handler_methods[$update['_']])) {
return $this->event_handler_methods[$update['_']]($update, $instance);
}
}
private $loop_callback;
public function async($async)
{
$this->async = $async;
foreach ($this->instances as $instance) {
$instance->async($async);
}
}
public function setLoopCallback($callback)
{
$this->loop_callback = $callback;
}
public function getUpdates($params = [])
{
}
public function loop($max_forks = 0)
{
if (\is_callable($max_forks)) {
return \danog\MadelineProto\Tools::wait($max_forks());
}
$loops = [];
foreach ($this->instances as $path => $instance) {
\danog\MadelineProto\Tools::wait($instance->initAsynchronously());
if ($instance->API->authorized !== MTProto::LOGGED_IN) {
continue;
}
if (!$instance->API->settings['updates']['handle_updates']) {
$instance->API->settings['updates']['handle_updates'] = true;
$instance->API->startUpdateSystem();
}
$instance->setCallback(function ($update) use ($path) {
return $this->eventUpdateHandler($update, $path);
}, ['async' => false]);
if ($this->loop_callback !== null) {
$instance->setLoopCallback($this->loop_callback, ['async' => false]);
}
$loops[] = \danog\MadelineProto\Tools::call($instance->loop(0, ['async' => true]));
}
Loop::repeat($this->serialization_interval * 1000, function () {
\danog\MadelineProto\Logger::log('Serializing combined event handler');
$this->serialize();
});
\danog\MadelineProto\Logger::log('Started update loop', \danog\MadelineProto\Logger::NOTICE);
\danog\MadelineProto\Tools::wait(all($loops));
}
}

View File

@ -1,59 +0,0 @@
<?php
/**
* CombinedEventHandler module.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
*
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\MadelineProto;
abstract class CombinedEventHandler
{
private $CombinedAPI;
public function __construct($CombinedAPI)
{
$this->CombinedAPI = $CombinedAPI;
foreach ($CombinedAPI->instances as $path => $instance) {
$this->referenceInstance($path);
}
}
final public function __sleep()
{
$keys = \method_exists($this, '__magic_sleep') ? $this->__magic_sleep() : \get_object_vars($this);
unset($keys['CombinedAPI']);
if (isset($this->CombinedAPI) && $this->CombinedAPI instanceof CombinedAPI) {
foreach ($this->CombinedAPI->instance_paths as $path) {
unset($keys[$path]);
}
} else {
foreach ($keys as $key => $value) {
if ($value instanceof API && $key === $value->session) {
unset($keys[$key]);
}
}
}
return \array_keys($keys);
}
final public function referenceInstance($path)
{
$this->{$path} = $this->CombinedAPI->instances[$path];
}
final public function removeInstance($path)
{
if (isset($this->{$path})) {
unset($this->{$path});
}
}
}

View File

@ -21,15 +21,18 @@ namespace danog\MadelineProto;
use Amp\ByteStream\ClosedException;
use Amp\Deferred;
use Amp\Failure;
use danog\MadelineProto\Loop\Connection\CheckLoop;
use danog\MadelineProto\Loop\Connection\HttpWaitLoop;
use danog\MadelineProto\Loop\Connection\PingLoop;
use danog\MadelineProto\Loop\Connection\ReadLoop;
use danog\MadelineProto\Loop\Connection\WriteLoop;
use danog\MadelineProto\MTProto\OutgoingMessage;
use danog\MadelineProto\MTProtoSession\Session;
use danog\MadelineProto\Stream\ConnectionContext;
use danog\MadelineProto\Stream\MTProtoTransport\HttpsStream;
use danog\MadelineProto\Stream\MTProtoTransport\HttpStream;
use danog\MadelineProto\Stream\StreamInterface;
use danog\MadelineProto\Stream\Transport\WssStream;
use danog\MadelineProto\Stream\Transport\WsStream;
@ -38,12 +41,14 @@ use danog\MadelineProto\Stream\Transport\WsStream;
*
* Manages connection to Telegram datacenters
*
* @internal
*
* @author Daniil Gentili <daniil@daniil.it>
*/
class Connection extends Session
class Connection
{
use Session;
use \danog\Serializable;
use Tools;
/**
* Writer loop.
*
@ -77,7 +82,7 @@ class Connection extends Session
/**
* The actual socket.
*
* @var Stream
* @var StreamInterface
*/
public $stream;
/**
@ -101,9 +106,9 @@ class Connection extends Session
/**
* Date of last chunk received.
*
* @var integer
* @var float
*/
private $lastChunk = 0;
private float $lastChunk = 0;
/**
* Logger instance.
*
@ -166,24 +171,6 @@ class Connection extends Session
{
return $this->needsReconnect;
}
/**
* Check if the socket is writing stuff.
*
* @return boolean
*/
public function isWriting(): bool
{
return $this->writing;
}
/**
* Check if the socket is reading stuff.
*
* @return boolean
*/
public function isReading(): bool
{
return $this->reading;
}
/**
* Set writing boolean.
*
@ -191,7 +178,7 @@ class Connection extends Session
*
* @return void
*/
public function writing(bool $writing)
public function writing(bool $writing): void
{
$this->shared->writing($writing, $this->id);
}
@ -202,7 +189,7 @@ class Connection extends Session
*
* @return void
*/
public function reading(bool $reading)
public function reading(bool $reading): void
{
$this->shared->reading($reading, $this->id);
}
@ -218,9 +205,9 @@ class Connection extends Session
/**
* Get the receive date of the latest chunk of data from the socket.
*
* @return int
* @return float
*/
public function getLastChunk(): int
public function getLastChunk(): float
{
return $this->lastChunk;
}
@ -294,7 +281,7 @@ class Connection extends Session
*/
public function isHttp(): bool
{
return \in_array($this->ctx->getStreamName(), [HttpStream::getName(), HttpsStream::getName()]);
return \in_array($this->ctx->getStreamName(), [HttpStream::class, HttpsStream::class]);
}
/**
* Check if is a media connection.
@ -319,13 +306,15 @@ class Connection extends Session
*
* @param ConnectionContext $ctx Connection context
*
* @return \Amp\Promise
* @return \Generator
*
* @psalm-return \Generator<mixed, StreamInterface, mixed, void>
*/
public function connect(ConnectionContext $ctx): \Generator
{
$this->ctx = $ctx->getCtx();
$this->datacenter = $ctx->getDc();
$this->datacenterId = $this->datacenter . '.' . $this->id;
$this->datacenterId = $this->datacenter.'.'.$this->id;
$this->API->logger->logger("Connecting to DC {$this->datacenterId}", \danog\MadelineProto\Logger::WARNING);
$this->createSession();
$ctx->setReadCallback([$this, 'haveRead']);
@ -348,14 +337,15 @@ class Connection extends Session
if (!isset($this->waiter)) {
$this->waiter = new HttpWaitLoop($this);
}
if (!isset($this->pinger) && ($this->ctx->hasStreamName(WssStream::getName()) || $this->ctx->hasStreamName(WsStream::getName()))) {
if (!isset($this->pinger) && ($this->ctx->hasStreamName(WssStream::class) || $this->ctx->hasStreamName(WsStream::class))) {
$this->pinger = new PingLoop($this);
}
foreach ($this->new_outgoing as $message_id) {
if ($this->outgoing_messages[$message_id]['unencrypted']) {
$promise = $this->outgoing_messages[$message_id]['promise'];
\Amp\Loop::defer(function () use ($promise) {
$promise->fail(new Exception('Restart because we were reconnected'));
foreach ($this->new_outgoing as $message_id => $message) {
if ($message->isUnencrypted()) {
\Amp\Loop::defer(function () use ($message) {
if (!($message->getState() & OutgoingMessage::STATE_REPLIED)) {
$message->reply(new Failure(new Exception('Restart because we were reconnected')));
}
});
unset($this->new_outgoing[$message_id], $this->outgoing_messages[$message_id]);
}
@ -370,68 +360,133 @@ class Connection extends Session
$this->pinger->start();
}
}
/**
* Apply method abstractions.
*
* @param string $method Method name
* @param array $arguments Arguments
*
* @return \Generator Whether we need to resolve a queue promise
*/
private function methodAbstractions(string &$method, array &$arguments): \Generator
{
if ($method === 'messages.importChatInvite' && isset($arguments['hash']) && \is_string($arguments['hash']) && \preg_match('@(?:t|telegram)\\.(?:me|dog)/(joinchat/)?([a-z0-9_-]*)@i', $arguments['hash'], $matches)) {
if ($matches[1] === '') {
$method = 'channels.joinChannel';
$arguments['channel'] = $matches[2];
} else {
$arguments['hash'] = $matches[2];
}
} elseif ($method === 'messages.checkChatInvite' && isset($arguments['hash']) && \is_string($arguments['hash']) && \preg_match('@(?:t|telegram)\\.(?:me|dog)/joinchat/([a-z0-9_-]*)@i', $arguments['hash'], $matches)) {
$arguments['hash'] = $matches[1];
} elseif ($method === 'channels.joinChannel' && isset($arguments['channel']) && \is_string($arguments['channel']) && \preg_match('@(?:t|telegram)\\.(?:me|dog)/(joinchat/)?([a-z0-9_-]*)@i', $arguments['channel'], $matches)) {
if ($matches[1] !== '') {
$method = 'messages.importChatInvite';
$arguments['hash'] = $matches[2];
}
} elseif ($method === 'messages.sendMessage' && isset($arguments['peer']['_']) && \in_array($arguments['peer']['_'], ['inputEncryptedChat', 'updateEncryption', 'updateEncryptedChatTyping', 'updateEncryptedMessagesRead', 'updateNewEncryptedMessage', 'encryptedMessage', 'encryptedMessageService'])) {
$method = 'messages.sendEncrypted';
$arguments = ['peer' => $arguments['peer'], 'message' => $arguments];
if (!isset($arguments['message']['_'])) {
$arguments['message']['_'] = 'decryptedMessage';
}
if (!isset($arguments['message']['ttl'])) {
$arguments['message']['ttl'] = 0;
}
if (isset($arguments['message']['reply_to_msg_id'])) {
$arguments['message']['reply_to_random_id'] = $arguments['message']['reply_to_msg_id'];
}
} elseif ($method === 'messages.sendEncryptedFile' || $method === 'messages.uploadEncryptedFile') {
if (isset($arguments['file'])) {
if ((!\is_array($arguments['file']) || !(isset($arguments['file']['_']) && $this->API->getTL()->getConstructors()->findByPredicate($arguments['file']['_']) === 'InputEncryptedFile')) && $this->API->getSettings()->getFiles()->getAllowAutomaticUpload()) {
$arguments['file'] = (yield from $this->API->uploadEncrypted($arguments['file']));
}
if (isset($arguments['file']['key'])) {
$arguments['message']['media']['key'] = $arguments['file']['key'];
}
if (isset($arguments['file']['iv'])) {
$arguments['message']['media']['iv'] = $arguments['file']['iv'];
}
if (isset($arguments['file']['size'])) {
$arguments['message']['media']['size'] = $arguments['file']['size'];
}
}
$arguments['queuePromise'] = new Deferred;
return $arguments['queuePromise'];
} elseif (\in_array($method, ['messages.addChatUser', 'messages.deleteChatUser', 'messages.editChatAdmin', 'messages.editChatPhoto', 'messages.editChatTitle', 'messages.getFullChat', 'messages.exportChatInvite', 'messages.editChatAdmin', 'messages.migrateChat']) && isset($arguments['chat_id']) && (!\is_numeric($arguments['chat_id']) || $arguments['chat_id'] < 0)) {
$res = (yield from $this->API->getInfo($arguments['chat_id']));
if ($res['type'] !== 'chat') {
throw new \danog\MadelineProto\Exception('chat_id is not a chat id (only normal groups allowed, not supergroups)!');
}
$arguments['chat_id'] = $res['chat_id'];
} elseif ($method === 'photos.updateProfilePhoto') {
if (isset($arguments['id'])) {
if (!\is_array($arguments['id'])) {
$method = 'photos.uploadProfilePhoto';
$arguments['file'] = $arguments['id'];
}
} elseif (isset($arguments['file'])) {
$method = 'photos.uploadProfilePhoto';
}
} elseif ($method === 'photos.uploadProfilePhoto') {
if (isset($arguments['file'])) {
if (\is_array($arguments['file']) && !\in_array($arguments['file']['_'], ['inputFile', 'inputFileBig'])) {
$method = 'photos.uploadProfilePhoto';
$arguments['id'] = $arguments['file'];
}
} elseif (isset($arguments['id'])) {
$method = 'photos.updateProfilePhoto';
}
} elseif ($method === 'messages.uploadMedia') {
if (!isset($arguments['peer']) && !$this->API->getSelf()['bot']) {
$arguments['peer'] = 'me';
}
}
if ($method === 'messages.sendEncrypted' || $method === 'messages.sendEncryptedService') {
$arguments['queuePromise'] = new Deferred;
return $arguments['queuePromise'];
}
return null;
}
/**
* Send an MTProto message.
*
* Structure of message array:
* [
* // only in outgoing messages
* 'body' => deserialized body, (optional if container)
* 'serialized_body' => 'serialized body', (optional if container)
* 'contentRelated' => bool,
* '_' => 'predicate',
* 'promise' => deferred promise that gets resolved when a response to the message is received (optional),
* 'send_promise' => deferred promise that gets resolved when the message is sent (optional),
* 'file' => bool (optional),
* 'type' => 'type' (optional),
* 'queue' => queue ID (optional),
* 'container' => [message ids] (optional),
*
* // only in incoming messages
* 'content' => deserialized body,
* 'seq_no' => number (optional),
* 'from_container' => bool (optional),
*
* // can be present in both
* 'response' => message id (optional),
* 'msg_id' => message id (optional),
* 'sent' => timestamp,
* 'tries' => number
* ]
*
* @param array $message The message to send
* @param boolean $flush Whether to flush the message right away
* @param OutgoingMessage $message The message to send
* @param boolean $flush Whether to flush the message right away
*
* @return \Generator
*/
public function sendMessage(array $message, bool $flush = true): \Generator
public function sendMessage(OutgoingMessage $message, bool $flush = true): \Generator
{
$deferred = new Deferred();
if (!isset($message['serialized_body'])) {
$body = \is_object($message['body']) ? yield from $message['body'] : $message['body'];
$refreshNext = isset($message['refreshNext']) && $message['refreshNext'];
//$refreshNext = true;
if ($refreshNext) {
$message->trySend();
$promise = $message->getSendPromise();
if (!$message->hasSerializedBody() || $message->shouldRefreshReferences()) {
$body = yield from $message->getBody();
if ($message->shouldRefreshReferences()) {
$this->API->referenceDatabase->refreshNext(true);
}
if ($message['method']) {
$body = (yield from $this->API->getTL()->serializeMethod($message['_'], $body));
if ($message->isMethod()) {
$method = $message->getConstructor();
$queuePromise = yield from $this->methodAbstractions($method, $body);
$body = yield from $this->API->getTL()->serializeMethod($method, $body);
} else {
$body['_'] = $message['_'];
$body = (yield from $this->API->getTL()->serializeObject(['type' => ''], $body, $message['_']));
$body['_'] = $message->getConstructor();
$body = yield from $this->API->getTL()->serializeObject(['type' => ''], $body, $message->getConstructor());
}
if ($refreshNext) {
if ($message->shouldRefreshReferences()) {
$this->API->referenceDatabase->refreshNext(false);
}
$message['serialized_body'] = $body;
$message->setSerializedBody($body);
unset($body);
}
$message['send_promise'] = $deferred;
$this->pending_outgoing[$this->pending_outgoing_key++] = $message;
$this->pendingOutgoing[$this->pendingOutgoingKey++] = $message;
if (isset($queuePromise)) {
$queuePromise->resolve();
}
if ($flush && isset($this->writer)) {
$this->writer->resume();
}
return yield $deferred->promise();
return yield $promise;
}
/**
* Flush pending packets.
@ -478,7 +533,7 @@ class Connection extends Session
*
* @return MTProto
*/
public function getExtra()
public function getExtra(): MTProto
{
return $this->API;
}

View File

@ -37,10 +37,10 @@ class ContextConnector implements Connector
$this->fromDns = $fromDns;
$this->logger = $dataCenter->getAPI()->getLogger();
}
public function connect(string $uri, ?ConnectContext $ctx = null, ?CancellationToken $token = null): Promise
public function connect(string $uri, ?ConnectContext $context = null, ?CancellationToken $token = null): Promise
{
return Tools::call((function () use ($uri, $ctx, $token): \Generator {
$ctx = $ctx ?? new ConnectContext();
return Tools::call((function () use ($uri, $context, $token): \Generator {
$ctx = $context ?? new ConnectContext();
$token = $token ?? new NullCancellationToken();
$ctxs = $this->dataCenter->generateContexts(0, $uri, $ctx);
if (empty($ctxs)) {
@ -55,13 +55,13 @@ class ContextConnector implements Connector
$this->logger->logger('OK!', \danog\MadelineProto\Logger::WARNING);
return $result->getSocket();
} catch (\Throwable $e) {
if (\MADELINEPROTO_TEST === 'pony') {
if (\defined('MADELINEPROTO_TEST') && \constant("MADELINEPROTO_TEST") === 'pony') {
throw $e;
}
$this->logger->logger('Connection failed: ' . $e, \danog\MadelineProto\Logger::ERROR);
$this->logger->logger('Connection failed: '.$e, \danog\MadelineProto\Logger::ERROR);
if ($e instanceof MultiReasonException) {
foreach ($e->getReasons() as $reason) {
$this->logger->logger('Multireason: ' . $reason, \danog\MadelineProto\Logger::ERROR);
$this->logger->logger('Multireason: '.$reason, \danog\MadelineProto\Logger::ERROR);
}
}
}

View File

@ -34,6 +34,7 @@ use Amp\Failure;
use Amp\Internal;
use Amp\Promise;
use Amp\Success;
use JsonSerializable;
use ReflectionGenerator;
/**
@ -43,7 +44,7 @@ use ReflectionGenerator;
* value is sent into the generator, while a failure reason is thrown into the generator. Using a coroutine,
* asynchronous code can be written without callbacks and be structured like synchronous code.
*/
final class Coroutine implements Promise, \ArrayAccess
final class Coroutine implements Promise, \ArrayAccess, JsonSerializable
{
use Internal\Placeholder {
fail as internalFail;
@ -99,7 +100,7 @@ final class Coroutine implements Promise, \ArrayAccess
* @param \Throwable|null $exception Exception to be thrown into the generator.
* @param mixed $value Value to be sent into the generator.
*/
$this->onResolve = function ($exception, $value) {
$this->onResolve = function ($exception, $value): void {
$this->exception = $exception;
$this->value = $value;
if (!$this->immediate) {
@ -176,8 +177,10 @@ final class Coroutine implements Promise, \ArrayAccess
}
/**
* @param \Throwable $reason Failure reason.
*
* @return void
*/
public function fail(\Throwable $reason)
public function fail(\Throwable $reason): void
{
$this->resolve(new Failure($reason));
}
@ -199,7 +202,7 @@ final class Coroutine implements Promise, \ArrayAccess
return $result[$offset];
})());
}
public function offsetSet($offset, $value)
public function offsetSet($offset, $value): Promise
{
return Tools::call((function () use ($offset, $value): \Generator {
$result = yield $this;
@ -209,7 +212,7 @@ final class Coroutine implements Promise, \ArrayAccess
return $result[$offset] = $value;
})());
}
public function offsetUnset($offset)
public function offsetUnset($offset): Promise
{
return Tools::call((function () use ($offset): \Generator {
$result = yield $this;
@ -234,7 +237,7 @@ final class Coroutine implements Promise, \ArrayAccess
{
return Tools::call((function () use ($name, $arguments): \Generator {
$result = yield $this;
return $result->{$name}($arguments);
return $result->{$name}(...$arguments);
})());
}
/**
@ -270,4 +273,20 @@ final class Coroutine implements Promise, \ArrayAccess
}
return [];
}
private const WARNING = 'To obtain a result from a Coroutine object, yield the result or disable async (not recommended). See https://docs.madelineproto.xyz/docs/ASYNC.html for more information on async.';
public function __debugInfo()
{
return [self::WARNING];
}
/**
* Obtain.
*
* @return string
*/
public function jsonSerialize(): string
{
return self::WARNING;
}
}

View File

@ -29,7 +29,7 @@ use Amp\Http\Client\Connection\UnlimitedConnectionPool;
use Amp\Http\Client\Cookie\CookieInterceptor;
use Amp\Http\Client\Cookie\CookieJar;
use Amp\Http\Client\Cookie\InMemoryCookieJar;
use Amp\Http\Client\DelegateHttpClient;
use Amp\Http\Client\HttpClient;
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use Amp\Socket\ConnectContext;
@ -38,7 +38,9 @@ use Amp\Websocket\Client\Handshake;
use Amp\Websocket\Client\Rfc6455Connector;
use danog\MadelineProto\MTProto\PermAuthKey;
use danog\MadelineProto\MTProto\TempAuthKey;
use danog\MadelineProto\Settings\Connection as ConnectionSettings;
use danog\MadelineProto\Stream\Common\BufferedRawStream;
use danog\MadelineProto\Stream\Common\UdpBufferedStream;
use danog\MadelineProto\Stream\ConnectionContext;
use danog\MadelineProto\Stream\MTProtoTransport\AbridgedStream;
use danog\MadelineProto\Stream\MTProtoTransport\FullStream;
@ -47,9 +49,6 @@ use danog\MadelineProto\Stream\MTProtoTransport\HttpStream;
use danog\MadelineProto\Stream\MTProtoTransport\IntermediatePaddedStream;
use danog\MadelineProto\Stream\MTProtoTransport\IntermediateStream;
use danog\MadelineProto\Stream\MTProtoTransport\ObfuscatedStream;
use danog\MadelineProto\Stream\Proxy\HttpProxy;
use danog\MadelineProto\Stream\Proxy\SocksProxy;
use danog\MadelineProto\Stream\StreamInterface;
use danog\MadelineProto\Stream\Transport\DefaultStream;
use danog\MadelineProto\Stream\Transport\WssStream;
use danog\MadelineProto\Stream\Transport\WsStream;
@ -59,18 +58,17 @@ use danog\MadelineProto\Stream\Transport\WsStream;
*/
class DataCenter
{
use \danog\MadelineProto\Tools;
use \danog\Serializable;
/**
* All socket connections to DCs.
*
* @var array<string, DataCenterConnection>
* @var array<string|int, DataCenterConnection>
*/
public $sockets = [];
/**
* Current DC ID.
*
* @var string
* @var string|int
*/
public $curdc = 0;
/**
@ -88,25 +86,25 @@ class DataCenter
/**
* Settings.
*
* @var array
* @var ConnectionSettings
*/
private $settings = [];
private $settings;
/**
* HTTP client.
*
* @var \Amp\Http\Client\DelegateHttpClient
* @var \Amp\Http\Client\HttpClient
*/
private $HTTPClient;
/**
* DNS over HTTPS client.
*
* @var \Amp\DoH\Rfc8484StubResolver
* @var Rfc8484StubResolver|Rfc1035StubResolver
*/
private $DoHClient;
/**
* Non-proxied DNS over HTTPS client.
*
* @var \Amp\DoH\Rfc8484StubResolver
* @var Rfc8484StubResolver|Rfc1035StubResolver
*/
private $nonProxiedDoHClient;
/**
@ -124,7 +122,7 @@ class DataCenter
/**
* DoH connector.
*/
private Rfc6455Connector $webSocketConnnector;
private Rfc6455Connector $webSocketConnector;
public function __sleep()
{
@ -132,16 +130,23 @@ class DataCenter
}
public function __wakeup()
{
if (\is_array($this->settings)) {
$settings = new ConnectionSettings;
$settings->mergeArray(['connection_settings' => $this->settings]);
$this->settings = $settings;
}
$array = [];
foreach ($this->sockets as $id => $socket) {
if ($socket instanceof Connection) {
if ($socket->temp_auth_key) {
if ($socket instanceof \danog\MadelineProto\Connection) {
if (isset($socket->temp_auth_key) && $socket->temp_auth_key) {
$array[$id]['tempAuthKey'] = $socket->temp_auth_key;
}
if ($socket->auth_key) {
if (isset($socket->auth_key) && $socket->auth_key) {
$array[$id]['permAuthKey'] = $socket->auth_key;
/** @psalm-suppress UndefinedPropertyFetch */
$array[$id]['permAuthKey']['authorized'] = $socket->authorized;
}
$array[$id] = [];
}
}
$this->setDataCenterConnections($array);
@ -185,19 +190,19 @@ class DataCenter
/**
* Constructor function.
*
* @param MTProto $API Main MTProto instance
* @param array $dclist DC IP list
* @param array $settings Settings
* @param boolean $reconnectAll Whether to reconnect to all DCs or just to changed ones
* @param CookieJar $jar Cookie jar
* @param MTProto $API Main MTProto instance
* @param array $dclist DC IP list
* @param ConnectionSettings $settings Settings
* @param boolean $reconnectAll Whether to reconnect to all DCs or just to changed ones
* @param CookieJar $jar Cookie jar
*
* @return void
*/
public function __magic_construct($API, array $dclist, array $settings, bool $reconnectAll = true, CookieJar $jar = null)
public function __magic_construct($API, array $dclist, ConnectionSettings $settings, bool $reconnectAll = true, CookieJar $jar = null)
{
$this->API = $API;
$changed = [];
$changedSettings = $this->settings !== $settings;
$changedSettings = $settings->hasChanged();
if (!$reconnectAll) {
$changed = [];
$test = $API->getCachedConfig()['test_mode'] ?? false ? 'test' : 'main';
@ -213,7 +218,6 @@ class DataCenter
$this->settings = $settings;
foreach ($this->sockets as $key => $socket) {
if ($socket instanceof DataCenterConnection && !\strpos($key, '_bk')) {
//$this->API->logger->logger(\sprintf(Lang::$current_lang['dc_con_stop'], $key), \danog\MadelineProto\Logger::VERBOSE);
if ($reconnectAll || isset($changed[$id])) {
$this->API->logger->logger("Disconnecting all before reconnect!");
$socket->needReconnect(true);
@ -230,15 +234,36 @@ class DataCenter
$DoHHTTPClient = (new HttpClientBuilder())->interceptNetwork(new CookieInterceptor($this->CookieJar))->usingPool(new UnlimitedConnectionPool(new DefaultConnectionFactory(new ContextConnector($this, true))))->build();
$DoHConfig = new DoHConfig([new Nameserver('https://mozilla.cloudflare-dns.com/dns-query'), new Nameserver('https://dns.google/resolve')], $DoHHTTPClient);
$nonProxiedDoHConfig = new DoHConfig([new Nameserver('https://mozilla.cloudflare-dns.com/dns-query'), new Nameserver('https://dns.google/resolve')]);
$this->DoHClient = Magic::$altervista || Magic::$zerowebhost ? new Rfc1035StubResolver() : new Rfc8484StubResolver($DoHConfig);
$this->nonProxiedDoHClient = Magic::$altervista || Magic::$zerowebhost ? new Rfc1035StubResolver() : new Rfc8484StubResolver($nonProxiedDoHConfig);
$this->DoHClient = Magic::$altervista || Magic::$zerowebhost || !$settings->getUseDoH()
? new Rfc1035StubResolver()
: new Rfc8484StubResolver($DoHConfig);
$this->nonProxiedDoHClient = Magic::$altervista || Magic::$zerowebhost || !$settings->getUseDoH()
? new Rfc1035StubResolver()
: new Rfc8484StubResolver($nonProxiedDoHConfig);
$this->dnsConnector = new DnsConnector(new Rfc1035StubResolver());
if (\class_exists(Rfc6455Connector::class)) {
$this->webSocketConnnector = new Rfc6455Connector($this->HTTPClient);
}
$this->webSocketConnector = new Rfc6455Connector($this->HTTPClient);
}
$this->settings->applyChanges();
}
/**
* Set VoIP endpoints.
*
* @param array $endpoints Endpoints
*
* @return void
*/
public function setVoIPEndpoints(array $endpoints): void
{
}
/**
* Connect to specified DC.
*
* @param string $dc_number DC to connect to
* @param integer $id Connection ID to re-establish (optional)
*
* @return \Generator<bool>
*/
public function dcConnect(string $dc_number, int $id = -1): \Generator
{
$old = isset($this->sockets[$dc_number]) && ($this->sockets[$dc_number]->shouldReconnect() || $id !== -1 && $this->sockets[$dc_number]->hasConnection($id) && $this->sockets[$dc_number]->getConnection($id)->shouldReconnect());
@ -253,170 +278,153 @@ class DataCenter
foreach ($ctxs as $ctx) {
try {
if ($old) {
$this->API->logger->logger("Reconnecting to DC {$dc_number} ({$id}) from existing", \danog\MadelineProto\Logger::WARNING);
$this->API->logger->logger("Reconnecting to DC {$dc_number} ({$id}) from existing", Logger::WARNING);
$this->sockets[$dc_number]->setExtra($this->API);
yield from $this->sockets[$dc_number]->connect($ctx, $id);
} else {
$this->API->logger->logger("Connecting to DC {$dc_number} from scratch", \danog\MadelineProto\Logger::WARNING);
$this->API->logger->logger("Connecting to DC {$dc_number} from scratch", Logger::WARNING);
$this->sockets[$dc_number] = new DataCenterConnection();
$this->sockets[$dc_number]->setExtra($this->API);
yield from $this->sockets[$dc_number]->connect($ctx);
}
$this->API->logger->logger('OK!', \danog\MadelineProto\Logger::WARNING);
$this->API->logger->logger('OK!', Logger::WARNING);
return true;
} catch (\Throwable $e) {
if (\MADELINEPROTO_TEST === 'pony') {
if (\defined("MADELINEPROTO_TEST") && \constant("MADELINEPROTO_TEST") === 'pony') {
throw $e;
}
$this->API->logger->logger("Connection failed ({$dc_number}): ".$e->getMessage(), \danog\MadelineProto\Logger::ERROR);
$this->API->logger->logger("Connection failed ({$dc_number}): ".$e->getMessage(), Logger::ERROR);
}
}
throw new Exception("Could not connect to DC {$dc_number}");
}
public function generateContexts($dc_number = 0, string $uri = '', ConnectContext $context = null)
/**
* Generate contexts.
*
* @param integer $dc_number DC ID to generate contexts for
* @param string $uri URI
* @param ConnectContext $context Connection context
*
* @return array
*/
public function generateContexts($dc_number = 0, string $uri = '', ConnectContext $context = null): array
{
$ctxs = [];
$combos = [];
$dc_config_number = isset($this->settings[$dc_number]) ? $dc_number : 'all';
$test = $this->settings[$dc_config_number]['test_mode'] ? 'test' : 'main';
$ipv6 = $this->settings[$dc_config_number]['ipv6'] ? 'ipv6' : 'ipv4';
switch ($this->settings[$dc_config_number]['protocol']) {
case 'abridged':
case 'tcp_abridged':
$default = [[DefaultStream::getName(), []], [BufferedRawStream::getName(), []], [AbridgedStream::getName(), []]];
$test = $this->settings->getTestMode() ? 'test' : 'main';
$ipv6 = $this->settings->getIpv6() ? 'ipv6' : 'ipv4';
switch ($this->settings->getProtocol()) {
case AbridgedStream::class:
$default = [[DefaultStream::class, []], [BufferedRawStream::class, []], [AbridgedStream::class, []]];
break;
case 'intermediate':
case 'tcp_intermediate':
$default = [[DefaultStream::getName(), []], [BufferedRawStream::getName(), []], [IntermediateStream::getName(), []]];
case IntermediateStream::class:
$default = [[DefaultStream::class, []], [BufferedRawStream::class, []], [IntermediateStream::class, []]];
break;
case 'obfuscated2':
$this->settings[$dc_config_number]['protocol'] = 'tcp_intermediate_padded';
$this->settings[$dc_config_number]['obfuscated'] = true;
// no break
case 'intermediate_padded':
case 'tcp_intermediate_padded':
$default = [[DefaultStream::getName(), []], [BufferedRawStream::getName(), []], [IntermediatePaddedStream::getName(), []]];
case IntermediatePaddedStream::class:
$default = [[DefaultStream::class, []], [BufferedRawStream::class, []], [IntermediatePaddedStream::class, []]];
break;
case 'full':
case 'tcp_full':
$default = [[DefaultStream::getName(), []], [BufferedRawStream::getName(), []], [FullStream::getName(), []]];
case FullStream::class:
$default = [[DefaultStream::class, []], [BufferedRawStream::class, []], [FullStream::class, []]];
break;
case 'http':
$default = [[DefaultStream::getName(), []], [BufferedRawStream::getName(), []], [HttpStream::getName(), []]];
case HttpStream::class:
$default = [[DefaultStream::class, []], [BufferedRawStream::class, []], [HttpStream::class, []]];
break;
case 'https':
$default = [[DefaultStream::getName(), []], [BufferedRawStream::getName(), []], [HttpsStream::getName(), []]];
case HttpsStream::class:
$default = [[DefaultStream::class, []], [BufferedRawStream::class, []], [HttpsStream::class, []]];
break;
case UdpBufferedStream::class:
$default = [[DefaultStream::class, []], [UdpBufferedStream::class, []]];
break;
default:
throw new Exception(Lang::$current_lang['protocol_invalid']);
}
if ($this->settings[$dc_config_number]['obfuscated'] && !\in_array($default[2][0], [HttpsStream::getName(), HttpStream::getName()])) {
$default = [[DefaultStream::getName(), []], [BufferedRawStream::getName(), []], [ObfuscatedStream::getName(), []], \end($default)];
if ($this->settings->getObfuscated() && !\in_array($default[2][0], [HttpsStream::class, HttpStream::class])) {
$default = [[DefaultStream::class, []], [BufferedRawStream::class, []], [ObfuscatedStream::class, []], \end($default)];
}
if ($this->settings[$dc_config_number]['transport'] && !\in_array($default[2][0], [HttpsStream::getName(), HttpStream::getName()])) {
switch ($this->settings[$dc_config_number]['transport']) {
case 'tcp':
if ($this->settings[$dc_config_number]['obfuscated']) {
$default = [[DefaultStream::getName(), []], [BufferedRawStream::getName(), []], [ObfuscatedStream::getName(), []], \end($default)];
if ($this->settings->getTransport() && !\in_array($default[2][0], [HttpsStream::class, HttpStream::class])) {
switch ($this->settings->getTransport()) {
case DefaultStream::class:
if ($this->settings->getObfuscated()) {
$default = [[DefaultStream::class, []], [BufferedRawStream::class, []], [ObfuscatedStream::class, []], \end($default)];
}
break;
case 'wss':
$default = [[DefaultStream::getName(), []], [WssStream::getName(), []], [BufferedRawStream::getName(), []], [ObfuscatedStream::getName(), []], \end($default)];
case WssStream::class:
$default = [[DefaultStream::class, []], [WssStream::class, []], [BufferedRawStream::class, []], [ObfuscatedStream::class, []], \end($default)];
break;
case 'ws':
$default = [[DefaultStream::getName(), []], [WsStream::getName(), []], [BufferedRawStream::getName(), []], [ObfuscatedStream::getName(), []], \end($default)];
case WsStream::class:
$default = [[DefaultStream::class, []], [WsStream::class, []], [BufferedRawStream::class, []], [ObfuscatedStream::class, []], \end($default)];
break;
}
}
if (!$dc_number) {
$default = [[DefaultStream::getName(), []], [BufferedRawStream::getName(), []]];
$default = [[DefaultStream::class, []], [BufferedRawStream::class, []]];
}
$combos[] = $default;
if (!isset($this->settings[$dc_config_number]['do_not_retry'])) {
if ($this->settings->getRetry()) {
if (isset($this->dclist[$test][$ipv6][$dc_number]['tcpo_only']) && $this->dclist[$test][$ipv6][$dc_number]['tcpo_only'] || isset($this->dclist[$test][$ipv6][$dc_number]['secret'])) {
$extra = isset($this->dclist[$test][$ipv6][$dc_number]['secret']) ? ['secret' => $this->dclist[$test][$ipv6][$dc_number]['secret']] : [];
$combos[] = [[DefaultStream::getName(), []], [BufferedRawStream::getName(), []], [ObfuscatedStream::getName(), $extra], [IntermediatePaddedStream::getName(), []]];
$combos[] = [[DefaultStream::class, []], [BufferedRawStream::class, []], [ObfuscatedStream::class, $extra], [IntermediatePaddedStream::class, []]];
}
if (\is_iterable($this->settings[$dc_config_number]['proxy'])) {
$proxies = $this->settings[$dc_config_number]['proxy'];
$proxy_extras = $this->settings[$dc_config_number]['proxy_extra'];
} else {
$proxies = [$this->settings[$dc_config_number]['proxy']];
$proxy_extras = [$this->settings[$dc_config_number]['proxy_extra']];
}
foreach ($proxies as $key => $proxy) {
// Convert old settings
if ($proxy === '\\Socket') {
$proxy = DefaultStream::getName();
}
if ($proxy === '\\SocksProxy') {
$proxy = SocksProxy::getName();
}
if ($proxy === '\\HttpProxy') {
$proxy = HttpProxy::getName();
}
if ($proxy === '\\MTProxySocket') {
$proxy = ObfuscatedStream::getName();
}
if ($proxy === DefaultStream::getName()) {
$proxyCombos = [];
foreach ($this->settings->getProxies() as $proxy => $extras) {
if (!$dc_number && $proxy === ObfuscatedStream::class) {
continue;
}
if (!$dc_number && $proxy === ObfuscatedStream::getName()) {
continue;
}
$extra = $proxy_extras[$key];
if (!isset(\class_implements($proxy)[StreamInterface::class])) {
throw new Exception(Lang::$current_lang['proxy_class_invalid']);
}
if ($proxy === ObfuscatedStream::getName() && \in_array(\strlen($extra['secret']), [17, 34])) {
$combos[] = [[DefaultStream::getName(), []], [BufferedRawStream::getName(), []], [$proxy, $extra], [IntermediatePaddedStream::getName(), []]];
}
foreach ($combos as $k => $orig) {
$combo = [];
if ($proxy === ObfuscatedStream::getName()) {
$combo = $orig;
if ($combo[\count($combo) - 2][0] === ObfuscatedStream::getName()) {
$combo[\count($combo) - 2][1] = $extra;
} else {
$mtproto = \end($combo);
$combo[\count($combo) - 1] = [$proxy, $extra];
$combo[] = $mtproto;
}
} else {
if ($orig[1][0] === BufferedRawStream::getName()) {
list($first, $second) = [\array_slice($orig, 0, 2), \array_slice($orig, 2)];
$first[] = [$proxy, $extra];
$combo = \array_merge($first, $second);
} elseif (\in_array($orig[1][0], [WsStream::getName(), WssStream::getName()])) {
list($first, $second) = [\array_slice($orig, 0, 1), \array_slice($orig, 1)];
$first[] = [BufferedRawStream::getName(), []];
$first[] = [$proxy, $extra];
$combo = \array_merge($first, $second);
}
foreach ($extras as $extra) {
if ($proxy === ObfuscatedStream::class && \in_array(\strlen($extra['secret']), [17, 34])) {
$combos[] = [[DefaultStream::class, []], [BufferedRawStream::class, []], [$proxy, $extra], [IntermediatePaddedStream::class, []]];
}
foreach ($combos as $orig) {
$combo = [];
if ($proxy === ObfuscatedStream::class) {
$combo = $orig;
if ($combo[\count($combo) - 2][0] === ObfuscatedStream::class) {
$combo[\count($combo) - 2][1] = $extra;
} else {
$mtproto = \end($combo);
$combo[\count($combo) - 1] = [$proxy, $extra];
$combo[] = $mtproto;
}
} else {
if ($orig[1][0] === BufferedRawStream::class) {
[$first, $second] = [\array_slice($orig, 0, 2), \array_slice($orig, 2)];
$first[] = [$proxy, $extra];
$combo = \array_merge($first, $second);
} elseif (\in_array($orig[1][0], [WsStream::class, WssStream::class])) {
[$first, $second] = [\array_slice($orig, 0, 1), \array_slice($orig, 1)];
$first[] = [BufferedRawStream::class, []];
$first[] = [$proxy, $extra];
$combo = \array_merge($first, $second);
}
}
$proxyCombos []= $combo;
}
\array_unshift($combos, $combo);
//unset($combos[$k]);
}
}
$combos = \array_merge($proxyCombos, $combos);
if ($dc_number) {
$combos[] = [[DefaultStream::getName(), []], [BufferedRawStream::getName(), []], [HttpsStream::getName(), []]];
$combos[] = [[DefaultStream::class, []], [BufferedRawStream::class, []], [HttpsStream::class, []]];
}
$combos = \array_unique($combos, SORT_REGULAR);
}
/* @var $context \Amp\ConnectContext */
$context = $context ?? (new ConnectContext())->withMaxAttempts(1)->withConnectTimeout(1000 * $this->settings[$dc_config_number]['timeout']);
$context = $context ?? (new ConnectContext())->withMaxAttempts(1)->withConnectTimeout(1000 * $this->settings->getTimeout())->withBindTo($this->settings->getBindTo());
foreach ($combos as $combo) {
$ipv6 = [$this->settings[$dc_config_number]['ipv6'] ? 'ipv6' : 'ipv4', $this->settings[$dc_config_number]['ipv6'] ? 'ipv4' : 'ipv6'];
foreach ($ipv6 as $ipv6) {
foreach ([true, false] as $useDoH) {
foreach ([true, false] as $useDoH) {
$ipv6Combos = [
$this->settings->getIpv6() ? 'ipv6' : 'ipv4',
$this->settings->getIpv6() ? 'ipv4' : 'ipv6'
];
foreach ($ipv6Combos as $ipv6) {
// This is only for non-MTProto connections
if (!$dc_number) {
/* @var $ctx \danog\MadelineProto\Stream\ConnectionContext */
$ctx = (new ConnectionContext())->setSocketContext($context)->setUri($uri)->setIpv6($ipv6 === 'ipv6');
foreach ($combo as $stream) {
if ($stream[0] === DefaultStream::getName() && $stream[1] === []) {
if ($stream[0] === DefaultStream::class && $stream[1] === []) {
$stream[1] = $useDoH ? new DoHConnector($this, $ctx) : $this->dnsConnector;
}
/** @var array{0: class-string, 1: mixed} $stream */
$ctx->addStream(...$stream);
}
$ctxs[] = $ctx;
@ -436,46 +444,50 @@ class DataCenter
$port = $this->dclist[$test][$ipv6][$dc_number]['port'];
foreach (\array_unique([$port, 443, 80, 88, 5222]) as $port) {
$stream = \end($combo)[0];
if ($stream === HttpsStream::getName()) {
$subdomain = $this->dclist['ssl_subdomains'][\preg_replace('/\\D+/', '', $dc_number)];
if ($stream === HttpsStream::class) {
if (\strpos($dc_number, '_cdn') !== false) {
continue;
}
$subdomain = $this->settings->getSslSubdomains()[\preg_replace('/\\D+/', '', $dc_number)];
if (\strpos($dc_number, '_media') !== false) {
$subdomain .= '-1';
}
$path = $this->settings[$dc_config_number]['test_mode'] ? 'apiw_test1' : 'apiw1';
$path = $this->settings->getTestMode() ? 'apiw_test1' : 'apiw1';
$uri = 'tcp://'.$subdomain.'.web.telegram.org:'.$port.'/'.$path;
} elseif ($stream === HttpStream::getName()) {
} elseif ($stream === HttpStream::class) {
$uri = 'tcp://'.$address.':'.$port.'/api';
} else {
$uri = 'tcp://'.$address.':'.$port;
}
if ($combo[1][0] === WssStream::getName()) {
$subdomain = $this->dclist['ssl_subdomains'][\preg_replace('/\\D+/', '', $dc_number)];
if ($combo[1][0] === WssStream::class) {
$subdomain = $this->settings->getSslSubdomains()[\preg_replace('/\\D+/', '', $dc_number)];
if (\strpos($dc_number, '_media') !== false) {
$subdomain .= '-1';
}
$path = $this->settings[$dc_config_number]['test_mode'] ? 'apiws_test' : 'apiws';
$path = $this->settings->getTestMode() ? 'apiws_test' : 'apiws';
$uri = 'tcp://'.$subdomain.'.web.telegram.org:'.$port.'/'.$path;
} elseif ($combo[1][0] === WsStream::getName()) {
$subdomain = $this->dclist['ssl_subdomains'][\preg_replace('/\\D+/', '', $dc_number)];
} elseif ($combo[1][0] === WsStream::class) {
$subdomain = $this->settings->getSslSubdomains()[\preg_replace('/\\D+/', '', $dc_number)];
if (\strpos($dc_number, '_media') !== false) {
$subdomain .= '-1';
}
$path = $this->settings[$dc_config_number]['test_mode'] ? 'apiws_test' : 'apiws';
$path = $this->settings->getTestMode() ? 'apiws_test' : 'apiws';
//$uri = 'tcp://' . $subdomain . '.web.telegram.org:' . $port . '/' . $path;
$uri = 'tcp://'.$address.':'.$port.'/'.$path;
}
/* @var $ctx \danog\MadelineProto\Stream\ConnectionContext */
$ctx = (new ConnectionContext())->setDc($dc_number)->setTest($this->settings[$dc_config_number]['test_mode'])->setSocketContext($context)->setUri($uri)->setIpv6($ipv6 === 'ipv6');
$ctx = (new ConnectionContext())->setDc($dc_number)->setTest($this->settings->getTestMode())->setSocketContext($context)->setUri($uri)->setIpv6($ipv6 === 'ipv6');
foreach ($combo as $stream) {
if ($stream[0] === DefaultStream::getName() && $stream[1] === []) {
if ($stream[0] === DefaultStream::class && $stream[1] === []) {
$stream[1] = $useDoH ? new DoHConnector($this, $ctx) : $this->dnsConnector;
}
if (\in_array($stream[0], [WsStream::class, WssStream::class]) && $stream[1] === []) {
if (!\class_exists(Handshake::class)) {
throw new Exception('Please install amphp/websocket-client by running "composer require amphp/websocket-client:dev-master"');
}
$stream[1] = $this->webSocketConnnector;
$stream[1] = $this->webSocketConnector;
}
/** @var array{0: class-string, 1: mixed} $stream */
$ctx->addStream(...$stream);
}
$ctxs[] = $ctx;
@ -483,13 +495,10 @@ class DataCenter
}
}
}
if (isset($this->dclist[$test][$ipv6][$dc_number.'_bk']['ip_address'])) {
$ctxs = \array_merge($ctxs, $this->generateContexts($dc_number.'_bk'));
}
if (empty($ctxs)) {
unset($this->sockets[$dc_number]);
$this->API->logger->logger("No info for DC {$dc_number}", \danog\MadelineProto\Logger::ERROR);
} elseif (\MADELINEPROTO_TEST === 'pony') {
$this->API->logger->logger("No info for DC {$dc_number}", Logger::ERROR);
} elseif (\defined('MADELINEPROTO_TEST') && \constant("MADELINEPROTO_TEST") === 'pony') {
return [$ctxs[0]];
}
return $ctxs;
@ -506,9 +515,9 @@ class DataCenter
/**
* Get async HTTP client.
*
* @return \Amp\Http\Client\DelegateHttpClient
* @return \Amp\Http\Client\HttpClient
*/
public function getHTTPClient(): DelegateHttpClient
public function getHTTPClient(): HttpClient
{
return $this->HTTPClient;
}
@ -544,7 +553,9 @@ class DataCenter
*
* @param string $url URL to fetch
*
* @return \Generator<string>
* @return \Generator
*
* @psalm-return \Generator<int, \Amp\Promise<string>, mixed, string>
*/
public function fileGetContents(string $url): \Generator
{
@ -577,7 +588,9 @@ class DataCenter
*
* @param string $dc DC ID
*
* @return \Generator<Connection>
* @return \Generator
*
* @psalm-return \Generator<int, \Amp\Promise, mixed, Connection>
*/
public function waitGetConnection(string $dc): \Generator
{
@ -597,7 +610,7 @@ class DataCenter
/**
* Get all DataCenterConnection instances.
*
* @return array<string, DataCenterConnection>
* @return array<int|string, DataCenterConnection>
*/
public function getDataCenterConnections(): array
{
@ -645,8 +658,8 @@ class DataCenter
*/
public function getDcs($all = true): array
{
$test = $this->settings['all']['test_mode'] ? 'test' : 'main';
$ipv6 = $this->settings['all']['ipv6'] ? 'ipv6' : 'ipv4';
$test = $this->settings->getTestMode() ? 'test' : 'main';
$ipv6 = $this->settings->getIpv6() ? 'ipv6' : 'ipv4';
return $all ? \array_keys((array) $this->dclist[$test][$ipv6]) : \array_keys((array) $this->sockets);
}
}

View File

@ -22,16 +22,21 @@ namespace danog\MadelineProto;
use Amp\Deferred;
use Amp\Promise;
use Amp\Success;
use danog\MadelineProto\Loop\Generic\PeriodicLoop;
use danog\MadelineProto\Loop\Generic\PeriodicLoopInternal;
use danog\MadelineProto\MTProto\AuthKey;
use danog\MadelineProto\MTProto\OutgoingMessage;
use danog\MadelineProto\MTProto\PermAuthKey;
use danog\MadelineProto\MTProto\TempAuthKey;
use danog\MadelineProto\Settings\Connection as ConnectionSettings;
use danog\MadelineProto\Stream\ConnectionContext;
use danog\MadelineProto\Stream\MTProtoTransport\HttpsStream;
use danog\MadelineProto\Stream\MTProtoTransport\HttpStream;
use danog\MadelineProto\Stream\Transport\WssStream;
use JsonSerializable;
/**
* Datacenter connection.
*/
class DataCenterConnection implements JsonSerializable
{
const READ_WEIGHT = 1;
@ -64,13 +69,13 @@ class DataCenterConnection implements JsonSerializable
/**
* Connections open to a certain DC.
*
* @var array<string, Connection>
* @var array<int, Connection>
*/
private $connections = [];
/**
* Connection weights.
*
* @var array<string, int>
* @var array<int, int>
*/
private $availableConnections = [];
/**
@ -99,10 +104,8 @@ class DataCenterConnection implements JsonSerializable
private $linked;
/**
* Loop to keep weights at sane value.
*
* @var \danog\MadelineProto\Loop\Generic\PeriodicLoop
*/
private $robinLoop;
private ?PeriodicLoopInternal $robinLoop = null;
/**
* Decrement roundrobin weight by this value if busy reading.
*
@ -161,7 +164,7 @@ class DataCenterConnection implements JsonSerializable
/**
* Check if auth key is present.
*
* @param boolean|null $temp Whether to fetch the temporary auth key
* @param bool $temp
*
* @return bool
*/
@ -173,18 +176,18 @@ class DataCenterConnection implements JsonSerializable
* Set auth key.
*
* @param AuthKey|null $key The auth key
* @param boolean|null $temp Whether to set the temporary auth key
* @param bool $temp
*
* @return void
*/
public function setAuthKey(?AuthKey $key, bool $temp = true)
public function setAuthKey(?AuthKey $key, bool $temp = true): void
{
$this->{$temp ? 'tempAuthKey' : 'permAuthKey'} = $key;
}
/**
* Get temporary authorization key.
*
* @return AuthKey
* @return TempAuthKey
*/
public function getTempAuthKey(): TempAuthKey
{
@ -193,7 +196,7 @@ class DataCenterConnection implements JsonSerializable
/**
* Get permanent authorization key.
*
* @return AuthKey
* @return PermAuthKey
*/
public function getPermAuthKey(): PermAuthKey
{
@ -224,9 +227,9 @@ class DataCenterConnection implements JsonSerializable
*
* @return void
*/
public function setTempAuthKey(?TempAuthKey $key)
public function setTempAuthKey(?TempAuthKey $key): void
{
return $this->setAuthKey($key, true);
$this->setAuthKey($key, true);
}
/**
* Set permanent authorization key.
@ -235,9 +238,9 @@ class DataCenterConnection implements JsonSerializable
*
* @return void
*/
public function setPermAuthKey(?PermAuthKey $key)
public function setPermAuthKey(?PermAuthKey $key): void
{
return $this->setAuthKey($key, false);
$this->setAuthKey($key, false);
}
/**
* Bind temporary and permanent auth keys.
@ -325,7 +328,7 @@ class DataCenterConnection implements JsonSerializable
*
* @return void
*/
public function flush()
public function flush(): void
{
foreach ($this->connections as $socket) {
$socket->flush();
@ -363,10 +366,10 @@ class DataCenterConnection implements JsonSerializable
$this->ctx = $ctx->getCtx();
$this->datacenter = $ctx->getDc();
$media = $ctx->isMedia() || $ctx->isCDN();
$count = $media ? $this->API->settings['connection_settings']['media_socket_count']['min'] : 1;
$count = $media ? $this->API->getSettings()->getConnection()->getMinMediaSocketCount() : 1;
if ($count > 1) {
if (!$this->robinLoop) {
$this->robinLoop = new PeriodicLoop($this->API, [$this, 'even'], "robin loop DC {$this->datacenter}", $this->API->settings['connection_settings']['robin_period']);
$this->robinLoop = new PeriodicLoopInternal($this->API, [$this, 'even'], "robin loop DC {$this->datacenter}", $this->API->getSettings()->getConnection()->getRobinPeriod() * 1000);
}
$this->robinLoop->start();
}
@ -374,7 +377,7 @@ class DataCenterConnection implements JsonSerializable
$this->decWrite = self::WRITE_WEIGHT;
if ($id === -1 || !isset($this->connections[$id])) {
if ($this->connections) {
$this->API->logger("Already connected!", Logger::WARNING);
$this->API->logger->logger("Already connected!", Logger::WARNING);
return;
}
yield from $this->connectMore($count);
@ -395,7 +398,7 @@ class DataCenterConnection implements JsonSerializable
*
* @param integer $count Number of sockets to open
*
* @return void
* @return \Generator
*/
private function connectMore(int $count): \Generator
{
@ -422,11 +425,11 @@ class DataCenterConnection implements JsonSerializable
$backup = $this->connections[$id]->backupSession();
$list = '';
foreach ($backup as $k => $message) {
if (($message['_'] ?? '') === 'msgs_state_req') {
if ($message->getConstructor() === 'msgs_state_req') {
unset($backup[$k]);
continue;
}
$list .= $message['_'] ?? '-';
$list .= $message->getConstructor();
$list .= ', ';
}
$this->API->logger->logger("Backed up {$list} from DC {$this->datacenter}.{$id}");
@ -478,8 +481,15 @@ class DataCenterConnection implements JsonSerializable
$this->backup = [];
$count = \count($backup);
$this->API->logger->logger("Restoring {$count} messages to DC {$this->datacenter}");
/** @var OutgoingMessage */
foreach ($backup as $message) {
if (isset($message['body'])) {
if ($message->hasSeqno()) {
$message->setSeqno(null);
}
if ($message->hasMsgId()) {
$message->setMsgId(null);
}
if (!($message->getState() & OutgoingMessage::STATE_REPLIED)) {
Tools::callFork($this->getConnection()->sendMessage($message, false));
}
}
@ -499,16 +509,18 @@ class DataCenterConnection implements JsonSerializable
*
* @param integer $id Connection ID
*
* @return boolean
* @return bool|int
*/
public function hasConnection(int $id = -1): bool
public function hasConnection(int $id = -1)
{
return $id < 0 ? \count($this->connections) : isset($this->connections[$id]);
}
/**
* Get best socket in round robin, asynchronously.
*
* @return \Generator<Connection>
* @return \Generator
*
* @psalm-return \Generator<int, Promise, mixed, Connection>
*/
public function waitGetConnection(): \Generator
{
@ -554,7 +566,7 @@ class DataCenterConnection implements JsonSerializable
$count += 50;
}
} elseif ($min < 100) {
$max = $this->isMedia() || $this->isCDN() ? $this->API->settings['connection_settings']['media_socket_count']['max'] : 1;
$max = $this->isMedia() || $this->isCDN() ? $this->API->getSettings()->getConnection()->getMaxMediaSocketCount() : 1;
if (\count($this->availableConnections) < $max) {
$this->connectMore(2);
} else {
@ -615,7 +627,7 @@ class DataCenterConnection implements JsonSerializable
*/
public function isHttp(): bool
{
return \in_array($this->ctx->getStreamName(), [HttpStream::getName(), HttpsStream::getName()]);
return \in_array($this->ctx->getStreamName(), [HttpStream::class, HttpsStream::class]);
}
/**
* Check if is connected directly by IP address.
@ -624,7 +636,7 @@ class DataCenterConnection implements JsonSerializable
*/
public function byIPAddress(): bool
{
return !$this->ctx->hasStreamName(WssStream::getName()) && !$this->ctx->hasStreamName(HttpsStream::getName());
return !$this->ctx->hasStreamName(WssStream::class) && !$this->ctx->hasStreamName(HttpsStream::class);
}
/**
* Check if is a media connection.
@ -647,12 +659,20 @@ class DataCenterConnection implements JsonSerializable
/**
* Get DC-specific settings.
*
* @return array
* @return ConnectionSettings
*/
public function getSettings(): array
public function getSettings(): ConnectionSettings
{
$dc_config_number = isset($this->API->settings['connection_settings'][$this->datacenter]) ? $this->datacenter : 'all';
return $this->API->settings['connection_settings'][$dc_config_number];
return $this->API->getSettings()->getConnection();
}
/**
* Get global settings.
*
* @return Settings
*/
public function getGenericSettings(): Settings
{
return $this->API->getSettings();
}
/**
* JSON serialize function.

View File

@ -0,0 +1,103 @@
<?php
namespace danog\MadelineProto\Db;
use Amp\Loop;
use danog\MadelineProto\Logger;
/**
* Array caching trait.
*/
trait ArrayCacheTrait
{
/**
* @var array<mixed>
*/
protected array $cache = [];
/**
* @var array<int>
*/
protected array $ttlValues = [];
/**
* TTL interval.
*/
protected int $ttl = 5 * 60;
/**
* TTL cleanup interval.
*/
private int $ttlCheckInterval = 60;
/**
* Cache cleanup watcher ID.
*/
private ?string $cacheCleanupId = null;
protected function getCache(string $key, $default = null)
{
if (!isset($this->ttlValues[$key])) {
return $default;
}
$this->ttlValues[$key] = \time() + $this->ttl;
return $this->cache[$key];
}
/**
* Save item in cache.
*
* @param string $key
* @param $value
*/
protected function setCache(string $key, $value): void
{
$this->cache[$key] = $value;
$this->ttlValues[$key] = \time() + $this->ttl;
}
/**
* Remove key from cache.
*
* @param string $key
*/
protected function unsetCache(string $key): void
{
unset($this->cache[$key], $this->ttlValues[$key]);
}
protected function startCacheCleanupLoop(): void
{
$this->cacheCleanupId = Loop::repeat($this->ttlCheckInterval * 1000, fn () => $this->cleanupCache());
}
protected function stopCacheCleanupLoop(): void
{
if ($this->cacheCleanupId) {
Loop::cancel($this->cacheCleanupId);
$this->cacheCleanupId = null;
}
}
/**
* Remove all keys from cache.
*/
protected function cleanupCache(): void
{
$now = \time();
$oldCount = 0;
foreach ($this->ttlValues as $cacheKey => $ttl) {
if ($ttl < $now) {
$this->unsetCache($cacheKey);
$oldCount++;
}
}
Logger::log(
\sprintf(
"cache for table: %s; keys left: %s; keys removed: %s",
(string) $this,
\count($this->cache),
$oldCount
),
Logger::VERBOSE
);
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace danog\MadelineProto\Db;
use Amp\Producer;
use Amp\Promise;
/**
* DB array interface.
*
* @template T as mixed
*/
interface DbArray extends DbType, \ArrayAccess, \Countable
{
/**
* Get Array copy.
*
* @psalm-return Promise<array<string|int, T>>
*
* @return Promise
*/
public function getArrayCopy(): Promise;
/**
* Check if element is set.
*
* @param string|int $key
*
* @psalm-return Promise<bool>
*
* @return Promise
*/
public function isset($key): Promise;
/**
* Get element.
*
* @param string|int $index
*
* @psalm-return Promise<T>
*
* @return Promise
*/
public function offsetGet($index): Promise;
/**
* Set element.
*
* @param string|int $index
* @param mixed $value
*
* @psalm-param T $value
*
* @return void
*/
public function offsetSet($index, $value);
/**
* Unset element.
*
* @param string|int $index Offset
* @return Promise
*/
public function offsetUnset($index): Promise;
/**
* Count number of elements.
*
* @return Promise<integer>
*/
public function count(): Promise;
/**
* Get iterator.
*
* @return Producer<array{0: string|int, 1: T}>
*/
public function getIterator(): Producer;
/**
* @deprecated
* @internal
* @see DbArray::isset();
*
* @param mixed $index
*
* @return bool
*/
public function offsetExists($index);
}

View File

@ -0,0 +1,76 @@
<?php
namespace danog\MadelineProto\Db;
use Amp\Promise;
use danog\MadelineProto\Settings\Database\Memory;
use danog\MadelineProto\Settings\Database\Mysql;
use danog\MadelineProto\Settings\Database\Postgres;
use danog\MadelineProto\Settings\Database\Redis;
use danog\MadelineProto\Settings\DatabaseAbstract;
/**
* This factory class initializes the correct database backend for MadelineProto.
*/
abstract class DbPropertiesFactory
{
/**
* Indicates a simple K-V array stored in a database backend.
* Values can be objects or other arrays, but when lots of nesting is required, it's best to split the array into multiple arrays.
*/
const TYPE_ARRAY = 'array';
/**
* @param DatabaseAbstract $dbSettings
* @param string $table
* @param self::TYPE_*|array $propertyType
* @param $value
* @param DriverArray|null $value
*
* @return Promise<DbType>
*
* @internal
*
* @uses \danog\MadelineProto\Db\MemoryArray
* @uses \danog\MadelineProto\Db\MysqlArray
* @uses \danog\MadelineProto\Db\PostgresArray
* @uses \danog\MadelineProto\Db\RedisArray
*/
public static function get(DatabaseAbstract $dbSettings, string $table, $propertyType, $value = null): Promise
{
$config = $propertyType['config'] ?? [];
$propertyType = \is_array($propertyType) ? $propertyType['type'] : $propertyType;
$propertyType = \strtolower($propertyType);
$class = !($config['enableCache'] ?? true) && !$dbSettings instanceof Memory
? __NAMESPACE__.'\\NullCache'
: __NAMESPACE__;
switch (true) {
case $dbSettings instanceof Memory:
$class .= '\\Memory';
break;
case $dbSettings instanceof Mysql:
$class .= '\\Mysql';
break;
case $dbSettings instanceof Postgres:
$class .= '\\Postgres';
break;
case $dbSettings instanceof Redis:
$class .= '\\Redis';
break;
default:
throw new \InvalidArgumentException("Unknown dbType: ".\get_class($dbSettings));
}
/** @var DbType $class */
switch (\strtolower($propertyType)) {
case self::TYPE_ARRAY:
$class .= 'Array';
break;
default:
throw new \InvalidArgumentException("Unknown $propertyType: {$propertyType}");
}
return $class::getInstance($table, $value, $dbSettings);
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace danog\MadelineProto\Db;
use danog\MadelineProto\MTProto;
use danog\MadelineProto\Tools;
/**
* Include this trait and call DbPropertiesTrait::initDb to use MadelineProto's database backend for properties.
*
* You will have to define a `$dbProperties` static array property, with a list of properties you want to store to a database.
*
* @see DbPropertiesFactory For a list of allowed property types
*
* @property array<string, DbPropertiesFactory::TYPE_*> $dbProperties
*/
trait DbPropertiesTrait
{
/**
* Initialize database instance.
*
* @internal
*
* @param MTProto $MadelineProto
* @param boolean $reset
* @return \Generator
*/
public function initDb(MTProto $MadelineProto, bool $reset = false): \Generator
{
if (empty(static::$dbProperties)) {
throw new \LogicException(static::class.' must have $dbProperties');
}
$dbSettings = $MadelineProto->settings->getDb();
$prefix = static::getSessionId($MadelineProto);
$promises = [];
foreach (static::$dbProperties as $property => $type) {
if ($reset) {
unset($this->{$property});
} else {
$table = "{$prefix}_{$property}";
$promises[$property] = DbPropertiesFactory::get($dbSettings, $table, $type, $this->{$property});
}
}
$promises = yield Tools::all($promises);
foreach ($promises as $key => $data) {
$this->{$key} = $data;
}
}
private static function getSessionId(MTProto $madelineProto): string
{
$result = $madelineProto->getSelf()['id'] ?? null;
if (!$result) {
$result = 'tmp_';
$result .= \str_replace('0', '', \spl_object_hash($madelineProto));
}
$className = \explode('\\', static::class);
$result .= '_'.\end($className);
return $result;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace danog\MadelineProto\Db;
use Amp\Promise;
use danog\MadelineProto\Settings\Database\DatabaseAbstract;
interface DbType
{
/**
* @param string $table
* @param null|DbType|array $previous
* @param DatabaseAbstract $settings
*
* @return Promise<self>
*/
public static function getInstance(string $table, $previous, $settings): Promise;
}

View File

@ -0,0 +1,78 @@
<?php
namespace danog\MadelineProto\Db\Driver;
use Amp\Mysql\ConnectionConfig;
use Amp\Mysql\Pool;
use danog\MadelineProto\Logger;
use danog\MadelineProto\Settings\Database\Mysql as DatabaseMysql;
use function Amp\Mysql\Pool;
/**
* MySQL driver wrapper.
*
* @internal
*/
class Mysql
{
/** @var Pool[] */
private static array $connections = [];
/**
* @param string $host
* @param int $port
* @param string $user
* @param string $password
* @param string $db
*
* @param int $maxConnections
* @param int $idleTimeout
*
* @throws \Amp\Sql\ConnectionException
* @throws \Amp\Sql\FailureException
* @throws \Throwable
*
* @return \Generator<Pool>
*/
public static function getConnection(DatabaseMysql $settings): \Generator
{
$dbKey = $settings->getKey();
if (empty(static::$connections[$dbKey])) {
$config = ConnectionConfig::fromString("host=".\str_replace("tcp://", "", $settings->getUri()))
->withUser($settings->getUsername())
->withPassword($settings->getPassword())
->withDatabase($settings->getDatabase());
yield from static::createDb($config);
static::$connections[$dbKey] = new Pool($config, $settings->getMaxConnections(), $settings->getIdleTimeout());
}
return static::$connections[$dbKey];
}
/**
* @param ConnectionConfig $config
*
* @throws \Amp\Sql\ConnectionException
* @throws \Amp\Sql\FailureException
* @throws \Throwable
*
* @return \Generator
*/
private static function createDb(ConnectionConfig $config): \Generator
{
try {
$db = $config->getDatabase();
$connection = pool($config->withDatabase(null));
yield $connection->query("
CREATE DATABASE IF NOT EXISTS `{$db}`
CHARACTER SET 'utf8mb4'
COLLATE 'utf8mb4_general_ci'
");
$connection->close();
} catch (\Throwable $e) {
Logger::log($e->getMessage(), Logger::ERROR);
}
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace danog\MadelineProto\Db\Driver;
use Amp\Postgres\ConnectionConfig;
use Amp\Postgres\Pool;
use danog\MadelineProto\Logger;
use danog\MadelineProto\Settings\Database\Postgres as DatabasePostgres;
use function Amp\Postgres\Pool;
/**
* Postgres driver wrapper.
*
* @internal
*/
class Postgres
{
/** @var Pool[] */
private static array $connections = [];
/**
* @param string $host
* @param int $port
* @param string $user
* @param string $password
* @param string $db
*
* @param int $maxConnections
* @param int $idleTimeout
*
* @throws \Amp\Sql\ConnectionException
* @throws \Amp\Sql\FailureException
* @throws \Throwable
*
* @return \Generator<Pool>
*/
public static function getConnection(DatabasePostgres $settings): \Generator
{
$dbKey = $settings->getKey();
if (empty(static::$connections[$dbKey])) {
$config = ConnectionConfig::fromString("host=".\str_replace("tcp://", "", $settings->getUri()))
->withUser($settings->getUsername())
->withPassword($settings->getPassword())
->withDatabase($settings->getDatabase());
yield from static::createDb($config);
static::$connections[$dbKey] = new Pool($config, $settings->getMaxConnections(), $settings->getIdleTimeout());
}
return static::$connections[$dbKey];
}
/**
* @param ConnectionConfig $config
*
* @throws \Amp\Sql\ConnectionException
* @throws \Amp\Sql\FailureException
* @throws \Throwable
*/
private static function createDb(ConnectionConfig $config): \Generator
{
try {
$db = $config->getDatabase();
$user = $config->getUser();
$connection = pool($config->withDatabase(null));
$result = yield $connection->query("SELECT * FROM pg_database WHERE datname = '{$db}'");
while (yield $result->advance()) {
$row = $result->getCurrent();
if ($row===false) {
yield $connection->query("
CREATE DATABASE {$db}
OWNER {$user}
ENCODING utf8
");
}
}
yield $connection->query("
CREATE OR REPLACE FUNCTION update_ts()
RETURNS TRIGGER AS $$
BEGIN
IF row(NEW.*) IS DISTINCT FROM row(OLD.*) THEN
NEW.ts = now();
RETURN NEW;
ELSE
RETURN OLD;
END IF;
END;
$$ language 'plpgsql'
");
$connection->close();
} catch (\Throwable $e) {
Logger::log($e->getMessage(), Logger::ERROR);
}
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace danog\MadelineProto\Db\Driver;
use Amp\Redis\Config;
use Amp\Redis\Redis as RedisRedis;
use Amp\Redis\RemoteExecutorFactory;
use danog\MadelineProto\Settings\Database\Redis as DatabaseRedis;
/**
* Redis driver wrapper.
*
* @internal
*/
class Redis
{
/** @var RedisRedis[] */
private static array $connections = [];
/**
* @param string $host
* @param int $port
* @param string $user
* @param string $password
* @param string $db
* @param int $maxConnections
* @param int $idleTimeout
*
* @throws \Amp\Sql\ConnectionException
* @throws \Amp\Sql\FailureException
* @throws \Throwable
*
* @return \Generator
*
* @psalm-return \Generator<int, \Amp\Promise<void>, mixed, RedisRedis>
*/
public static function getConnection(DatabaseRedis $settings): \Generator
{
$dbKey = $settings->getKey();
if (empty(static::$connections[$dbKey])) {
$config = Config::fromUri($settings->getUri())
->withPassword($settings->getPassword())
->withDatabase($settings->getDatabase());
static::$connections[$dbKey] = new RedisRedis((new RemoteExecutorFactory($config))->createQueryExecutor());
yield static::$connections[$dbKey]->ping();
}
return static::$connections[$dbKey];
}
}

View File

@ -0,0 +1,221 @@
<?php
namespace danog\MadelineProto\Db;
use Amp\Promise;
use danog\MadelineProto\Logger;
use danog\MadelineProto\Settings\Database\DatabaseAbstract;
use danog\MadelineProto\SettingsAbstract;
use ReflectionClass;
use function Amp\call;
/**
* Array caching trait.
*/
abstract class DriverArray implements DbArray
{
protected string $table;
use ArrayCacheTrait;
/**
* Initialize connection.
*/
abstract public function initConnection(DatabaseAbstract $settings): \Generator;
/**
* Initialize on startup.
*
* @return \Generator
*/
abstract public function initStartup(): \Generator;
/**
* Create table for property.
*
* @return \Generator
*
* @throws \Throwable
*/
abstract protected function prepareTable(): \Generator;
/**
* Rename table.
*
* @param string $from
* @param string $to
* @return \Generator
*/
abstract protected function renameTable(string $from, string $to): \Generator;
/**
* Get the value of table.
*
* @return string
*/
public function getTable(): string
{
return $this->table;
}
/**
* Set the value of table.
*
* @param string $table
*
* @return self
*/
public function setTable(string $table): self
{
$this->table = $table;
return $this;
}
/**
* @param string $table
* @param DbArray|array|null $previous
* @param DatabaseAbstract $settings
*
* @return Promise
*
* @psalm-return Promise<static>
*/
public static function getInstance(string $table, $previous, $settings): Promise
{
if ($previous instanceof static && $previous->getTable() === $table) {
$instance = &$previous;
} else {
$instance = new static();
$instance->setTable($table);
}
/** @psalm-suppress UndefinedPropertyAssignment */
$instance->dbSettings = $settings;
$instance->ttl = $settings->getCacheTtl();
$instance->startCacheCleanupLoop();
return call(static function () use ($instance, $previous, $settings) {
yield from $instance->initConnection($settings);
yield from $instance->prepareTable();
if ($instance !== $previous) {
if ($previous instanceof DriverArray) {
yield from $previous->initStartup();
}
yield from static::renameTmpTable($instance, $previous);
if ($instance instanceof SqlArray) {
Logger::log("Preparing statements...");
yield from $instance->prepareStatements();
}
yield from static::migrateDataToDb($instance, $previous);
} elseif ($instance instanceof SqlArray) {
Logger::log("Preparing statements...");
yield from $instance->prepareStatements();
}
return $instance;
});
}
/**
* Rename table of old database, if the new one is not a temporary table name.
*
* Otherwise, simply change name of table in new database to match old table name.
*
* @param self $new New db
* @param DbArray|array|null $old Old db
*
* @return \Generator
*/
protected static function renameTmpTable(self $new, $old): \Generator
{
if ($old instanceof static && $old->getTable()) {
if (
$old->getTable() !== $new->getTable() &&
\mb_strpos($new->getTable(), 'tmp') !== 0
) {
yield from $new->renameTable($old->getTable(), $new->getTable());
} else {
$new->setTable($old->getTable());
}
}
}
/**
* @param self $new
* @param DbArray|array|null $old
*
* @return \Generator
* @throws \Throwable
*/
protected static function migrateDataToDb(self $new, $old): \Generator
{
if (!empty($old) && !$old instanceof static) {
Logger::log('Converting database to '.\get_class($new), Logger::ERROR);
if ($old instanceof DbArray) {
$old = yield $old->getArrayCopy();
} else {
$old = (array) $old;
}
$counter = 0;
$total = \count($old);
foreach ($old as $key => $item) {
$counter++;
if ($counter % 500 === 0) {
yield $new->offsetSet($key, $item);
Logger::log("Loading data to table {$new}: $counter/$total", Logger::WARNING);
} else {
$new->offsetSet($key, $item);
}
}
Logger::log('Converting database done.', Logger::ERROR);
}
}
public function __destruct()
{
$this->stopCacheCleanupLoop();
}
/**
* Get the value of table.
*
* @return string
*/
public function __toString(): string
{
return $this->table;
}
/**
* Sleep function.
*
* @return array
*/
public function __sleep(): array
{
return ['table', 'dbSettings'];
}
public function __wakeup()
{
if (isset($this->settings) && \is_array($this->settings)) {
$clazz = (new ReflectionClass($this))->getProperty('dbSettings')->getType()->getName();
/**
* @var SettingsAbstract
* @psalm-suppress UndefinedThisPropertyAssignment
*/
$this->dbSettings = new $clazz;
$this->dbSettings->mergeArray($this->settings);
unset($this->settings);
}
}
public function offsetExists($index): bool
{
throw new \RuntimeException('Native isset not support promises. Use isset method');
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace danog\MadelineProto\Db;
use Amp\Producer;
use Amp\Promise;
use Amp\Success;
use danog\MadelineProto\Logger;
use danog\MadelineProto\Settings\Database\Memory;
use function Amp\call;
/**
* Memory database backend.
*/
class MemoryArray extends \ArrayIterator implements DbArray
{
protected function __construct($array = [], $flags = 0)
{
parent::__construct((array) $array, $flags | self::STD_PROP_LIST);
}
/**
* Get instance.
*
* @param string $table
* @param mixed $previous
* @param Memory $settings
* @return Promise<self>
*/
public static function getInstance(string $table, $previous, $settings): Promise
{
return call(static function () use ($previous) {
if ($previous instanceof MemoryArray) {
return $previous;
}
if ($previous instanceof DbArray) {
Logger::log("Loading database to memory. Please wait.", Logger::WARNING);
if ($previous instanceof DriverArray) {
yield from $previous->initStartup();
}
$previous = yield $previous->getArrayCopy();
}
return new static($previous);
});
}
public function offsetExists($offset)
{
throw new \RuntimeException('Native isset not support promises. Use isset method');
}
public function isset($key): Promise
{
return new Success(parent::offsetExists($key));
}
public function offsetGet($offset): Promise
{
return new Success(parent::offsetExists($offset) ? parent::offsetGet($offset) : null);
}
public function offsetUnset($offset): Promise
{
return new Success(parent::offsetUnset($offset));
}
public function count(): Promise
{
return new Success(parent::count());
}
public function getArrayCopy(): Promise
{
return new Success(parent::getArrayCopy());
}
public function getIterator(): Producer
{
return new Producer(function (callable $emit) {
foreach ($this as $key => $value) {
yield $emit([$key, $value]);
}
});
}
}

View File

@ -0,0 +1,128 @@
<?php
namespace danog\MadelineProto\Db;
use Amp\Mysql\Pool;
use Amp\Promise;
use danog\MadelineProto\Db\Driver\Mysql;
use danog\MadelineProto\Logger;
use danog\MadelineProto\Settings\Database\Mysql as DatabaseMysql;
/**
* MySQL database backend.
*/
class MysqlArray extends SqlArray
{
protected DatabaseMysql $dbSettings;
private Pool $db;
// Legacy
protected array $settings;
/**
* Initialize on startup.
*
* @return \Generator
*/
public function initStartup(): \Generator
{
yield from $this->initConnection($this->dbSettings);
yield from $this->prepareStatements();
}
/**
* Prepare statements.
*
* @return \Generator
*/
protected function prepareStatements(): \Generator
{
$this->get = yield $this->db->prepare(
"SELECT `value` FROM `{$this->table}` WHERE `key` = :index LIMIT 1"
);
$this->set = yield $this->db->prepare("
INSERT INTO `{$this->table}`
SET `key` = :index, `value` = :value
ON DUPLICATE KEY UPDATE `value` = :value
");
$this->unset = yield $this->db->prepare("
DELETE FROM `{$this->table}`
WHERE `key` = :index
");
$this->count = yield $this->db->prepare("
SELECT count(`key`) as `count` FROM `{$this->table}`
");
$this->iterate = yield $this->db->prepare("
SELECT `key`, `value` FROM `{$this->table}`
");
}
/**
* Get value from row.
*
* @param array $row
* @return null|mixed
*/
protected function getValue(array $row)
{
if ($row) {
if (!empty($row[0]['value'])) {
$row = \reset($row);
}
return \unserialize($row['value']);
}
return null;
}
/**
* Initialize connection.
*
* @param DatabaseMysql $settings
* @return \Generator
*/
public function initConnection($settings): \Generator
{
if (!isset($this->db)) {
$this->db = yield from Mysql::getConnection($settings);
}
}
/**
* Create table for property.
*
* @return \Generator
*
* @throws \Throwable
*
* @psalm-return \Generator<int, Promise, mixed, mixed>
*/
protected function prepareTable(): \Generator
{
Logger::log("Creating/checking table {$this->table}", Logger::WARNING);
return yield $this->db->query("
CREATE TABLE IF NOT EXISTS `{$this->table}`
(
`key` VARCHAR(255) NOT NULL,
`value` MEDIUMBLOB NULL,
`ts` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`key`)
)
ENGINE = InnoDB
CHARACTER SET 'utf8mb4'
COLLATE 'utf8mb4_general_ci'
");
}
protected function renameTable(string $from, string $to): \Generator
{
Logger::log("Renaming table {$from} to {$to}", Logger::WARNING);
yield $this->db->query("
DROP TABLE IF EXISTS `{$to}`;
");
yield $this->db->query("
ALTER TABLE `{$from}` RENAME TO `{$to}`;
");
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace danog\MadelineProto\Db\NullCache;
use danog\MadelineProto\Db\MysqlArray as DbMysqlArray;
/**
* MySQL database backend, no caching.
*
* @internal
*/
class MysqlArray extends DbMysqlArray
{
use NullCacheTrait;
}

View File

@ -0,0 +1,44 @@
<?php
namespace danog\MadelineProto\Db\NullCache;
/**
* Trait that disables database caching.
*
* @internal
*/
trait NullCacheTrait
{
/**
* @return void
*/
protected function getCache(string $key, $default = null)
{
}
/**
* Save item in cache.
*
* @param string $key
* @param $value
*/
protected function setCache(string $key, $value): void
{
}
/**
* Remove key from cache.
*
* @param string $key
*/
protected function unsetCache(string $key): void
{
}
protected function startCacheCleanupLoop(): void
{
}
protected function stopCacheCleanupLoop(): void
{
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace danog\MadelineProto\Db\NullCache;
use danog\MadelineProto\Db\PostgresArray as DbPostgresArray;
/**
* Postgres database backend, no caching.
*
* @internal
*/
class PostgresArray extends DbPostgresArray
{
use NullCacheTrait;
}

View File

@ -0,0 +1,15 @@
<?php
namespace danog\MadelineProto\Db\NullCache;
use danog\MadelineProto\Db\RedisArray as DbRedisArray;
/**
* Redis database backend, no caching.
*
* @internal
*/
class RedisArray extends DbRedisArray
{
use NullCacheTrait;
}

View File

@ -0,0 +1,176 @@
<?php
namespace danog\MadelineProto\Db;
use Amp\Postgres\ByteA;
use Amp\Postgres\Pool;
use Amp\Promise;
use Amp\Success;
use danog\MadelineProto\Db\Driver\Postgres;
use danog\MadelineProto\Logger;
use danog\MadelineProto\Settings\Database\Postgres as DatabasePostgres;
/**
* Postgres database backend.
*/
class PostgresArray extends SqlArray
{
public DatabasePostgres $dbSettings;
private Pool $db;
// Legacy
protected array $settings;
/**
* Prepare statements.
*
* @return \Generator
*/
protected function prepareStatements(): \Generator
{
$this->get = yield $this->db->prepare(
"SELECT value FROM \"{$this->table}\" WHERE key = :index LIMIT 1",
);
$this->set = yield $this->db->prepare("
INSERT INTO \"{$this->table}\"
(key,value)
VALUES (:index, :value)
ON CONFLICT (key) DO UPDATE SET value = :value
");
$this->unset = yield $this->db->prepare("
DELETE FROM \"{$this->table}\"
WHERE key = :index
");
$this->count = yield $this->db->prepare("
SELECT count(key) as count FROM \"{$this->table}\"
");
$this->iterate = yield $this->db->prepare("
SELECT key, value FROM \"{$this->table}\"
");
}
/**
* Initialize on startup.
*
* @return \Generator
*/
public function initStartup(): \Generator
{
yield from $this->initConnection($this->dbSettings);
yield from $this->prepareStatements();
}
/**
* Initialize connection.
*
* @param DatabasePostgres $settings
* @return \Generator
*/
public function initConnection($settings): \Generator
{
if (!isset($this->db)) {
$this->db = yield from Postgres::getConnection($settings);
}
}
/**
* Get value from row.
*
* @param array $row
* @return null|mixed
*/
protected function getValue(array $row)
{
if ($row) {
if (!empty($row[0]['value'])) {
$row = \reset($row);
}
if (!$row['value']) {
return $row['value'];
}
if ($row['value'][0] === '\\') {
$row['value'] = \hex2bin(\substr($row['value'], 2));
}
return \unserialize($row['value']);
}
return null;
}
/**
* Set value for an offset.
*
* @link https://php.net/manual/en/arrayiterator.offsetset.php
*
* @param string|int $index <p>
* The index to set for.
* </p>
* @param $value
*
* @throws \Throwable
*/
public function offsetSet($index, $value): Promise
{
if ($this->getCache($index) === $value) {
return new Success();
}
$this->setCache($index, $value);
$request = $this->execute(
$this->set,
[
'index' => $index,
'value' => new ByteA(\serialize($value)),
]
);
//Ensure that cache is synced with latest insert in case of concurrent requests.
$request->onResolve(fn () => $this->setCache($index, $value));
return $request;
}
/**
* Create table for property.
*
* @return \Generator
*
* @throws \Throwable
*
* @psalm-return \Generator<int, Promise, mixed, void>
*/
protected function prepareTable(): \Generator
{
Logger::log("Creating/checking table {$this->table}", Logger::WARNING);
yield $this->db->query("
CREATE TABLE IF NOT EXISTS \"{$this->table}\"
(
\"key\" VARCHAR(255) NOT NULL,
\"value\" BYTEA NULL,
\"ts\" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT \"{$this->table}_pkey\" PRIMARY KEY(\"key\")
);
");
yield $this->db->query("
DROP TRIGGER IF exists \"{$this->table}_update_ts_trigger\" ON \"{$this->table}\";
");
yield $this->db->query("
CREATE TRIGGER \"{$this->table}_update_ts_trigger\" BEFORE UPDATE ON \"{$this->table}\" FOR EACH ROW EXECUTE PROCEDURE update_ts();
");
}
protected function renameTable(string $from, string $to): \Generator
{
Logger::log("Renaming table {$from} to {$to}", Logger::WARNING);
yield $this->db->query("
DROP TABLE IF EXISTS \"{$to}\";
");
yield $this->db->query("
ALTER TABLE \"{$from}\" RENAME TO \"{$to}\";
");
}
}

View File

@ -0,0 +1,243 @@
<?php
namespace danog\MadelineProto\Db;
use Amp\Producer;
use Amp\Promise;
use Amp\Redis\Redis as RedisRedis;
use Amp\Success;
use danog\MadelineProto\Db\Driver\Redis as Redis;
use danog\MadelineProto\Logger;
use danog\MadelineProto\Settings\Database\Redis as DatabaseRedis;
use Generator;
use function Amp\call;
/**
* Redis database backend.
*/
class RedisArray extends DriverArray
{
protected DatabaseRedis $dbSettings;
private RedisRedis $db;
// Legacy
protected array $settings;
/**
* Initialize on startup.
*
* @return \Generator
*/
public function initStartup(): \Generator
{
return $this->initConnection($this->dbSettings);
}
/**
* @return Generator
*
* @psalm-return Generator<int, Success<null>, mixed, void>
*/
protected function prepareTable(): Generator
{
yield new Success;
}
protected function renameTable(string $from, string $to): \Generator
{
Logger::log("Renaming table {$from} to {$to}", Logger::WARNING);
$from = "va:$from";
$to = "va:$to";
$request = $this->db->scan($from.'*');
$lenK = \strlen($from);
while (yield $request->advance()) {
$key = $request->getCurrent();
yield $this->db->rename($key, $to.\substr($key, $lenK));
}
}
/**
* Initialize connection.
*
* @param DatabaseRedis $settings
* @return \Generator
*/
public function initConnection($settings): \Generator
{
if (!isset($this->db)) {
$this->db = yield from Redis::getConnection($settings);
}
}
/**
* Get redis key name.
*
* @param string $key
* @return string
*/
private function rKey(string $key): string
{
return 'va:'.$this->table.':'.$key;
}
/**
* Get redis ts name.
*
* @param string $key
* @return string
*/
private function tsKey(string $key): string
{
return 'ts:'.$this->table.$key;
}
/**
* Get iterator key.
*
* @return string
*/
private function itKey(): string
{
return 'va:'.$this->table.'*';
}
/**
* Set value for an offset.
*
* @link https://php.net/manual/en/arrayiterator.offsetset.php
*
* @param string $index <p>
* The index to set for.
* </p>
* @param $value
*
* @throws \Throwable
*/
public function offsetSet($index, $value): Promise
{
if ($this->getCache($index) === $value) {
return new Success();
}
$this->setCache($index, $value);
/*
$request = $this->db->setMultiple(
[
$this->rKey($index) => \serialize($value),
$this->tsKey($index) => \time()
]
);*/
$request = $this->db->set($this->rKey($index), \serialize($value));
//Ensure that cache is synced with latest insert in case of concurrent requests.
$request->onResolve(fn () => $this->setCache($index, $value));
return $request;
}
/**
* Check if key isset.
*
* @param $key
*
* @return Promise<bool> true if the offset exists, otherwise false
*/
public function isset($key): Promise
{
return $this->db->has($this->rKey($key));
}
public function offsetGet($offset): Promise
{
return call(function () use ($offset) {
if ($cached = $this->getCache($offset)) {
return $cached;
}
$value = yield $this->db->get($this->rKey($offset));
if ($value = \unserialize($value)) {
$this->setCache($offset, $value);
}
return $value;
});
}
/**
* Unset value for an offset.
*
* @link https://php.net/manual/en/arrayiterator.offsetunset.php
*
* @param string $index <p>
* The offset to unset.
* </p>
*
* @return Promise
* @throws \Throwable
*/
public function offsetUnset($index): Promise
{
$this->unsetCache($index);
return $this->db->delete($this->rkey($index));
}
/**
* Get array copy.
*
* @return Promise<array>
* @throws \Throwable
*/
public function getArrayCopy(): Promise
{
return call(function () {
$iterator = $this->getIterator();
$result = [];
while (yield $iterator->advance()) {
[$key, $value] = $iterator->getCurrent();
$result[$key] = $value;
}
return $result;
});
}
public function getIterator(): Producer
{
return new Producer(function (callable $emit) {
$request = $this->db->scan($this->itKey());
$len = \strlen($this->rKey(''));
while (yield $request->advance()) {
$key = $request->getCurrent();
yield $emit([\substr($key, $len), \unserialize(yield $this->db->get($key))]);
}
});
}
/**
* Count elements.
*
* @link https://php.net/manual/en/arrayiterator.count.php
* @return Promise<int> The number of elements or public properties in the associated
* array or object, respectively.
* @throws \Throwable
*/
public function count(): Promise
{
return call(function () {
$request = $this->db->scan($this->itKey());
$count = 0;
while (yield $request->advance()) {
$count++;
}
return $count;
});
}
}

View File

@ -0,0 +1,204 @@
<?php
namespace danog\MadelineProto\Db;
use Amp\Producer;
use Amp\Promise;
use Amp\Sql\ResultSet;
use Amp\Sql\Statement;
use Amp\Success;
use danog\MadelineProto\Logger;
use function Amp\call;
/**
* Generic SQL database backend.
*/
abstract class SqlArray extends DriverArray
{
protected Statement $get;
protected Statement $set;
protected Statement $unset;
protected Statement $count;
protected Statement $iterate;
/**
* Prepare statements.
*
* @return \Generator
*/
abstract protected function prepareStatements(): \Generator;
/**
* Get value from row.
*
* @param array $row
* @return null|mixed
*/
abstract protected function getValue(array $row);
public function getIterator(): Producer
{
return new Producer(function (callable $emit) {
$request = yield $this->iterate->execute();
while (yield $request->advance()) {
$row = $request->getCurrent();
yield $emit([$row['key'], $this->getValue($row)]);
}
});
}
public function getArrayCopy(): Promise
{
return call(function () {
$iterator = $this->getIterator();
$result = [];
while (yield $iterator->advance()) {
[$key, $value] = $iterator->getCurrent();
$result[$key] = $value;
}
return $result;
});
}
/**
* Check if key isset.
*
* @param $key
*
* @return Promise<bool> true if the offset exists, otherwise false
*/
public function isset($key): Promise
{
return call(fn () => yield $this->offsetGet($key) !== null);
}
/**
* Unset value for an offset.
*
* @link https://php.net/manual/en/arrayiterator.offsetunset.php
*
* @param string|int $index <p>
* The offset to unset.
* </p>
*
* @return Promise
* @throws \Throwable
*/
public function offsetUnset($index): Promise
{
$this->unsetCache($index);
return $this->execute(
$this->unset,
['index' => $index]
);
}
/**
* Count elements.
*
* @link https://php.net/manual/en/arrayiterator.count.php
* @return Promise<int> The number of elements or public properties in the associated
* array or object, respectively.
* @throws \Throwable
*/
public function count(): Promise
{
return call(function () {
$row = yield $this->execute($this->count);
return $row[0]['count'] ?? 0;
});
}
public function offsetGet($offset): Promise
{
return call(function () use ($offset) {
if ($cached = $this->getCache($offset)) {
return $cached;
}
$row = yield $this->execute($this->get, ['index' => $offset]);
if ($value = $this->getValue($row)) {
$this->setCache($offset, $value);
}
return $value;
});
}
/**
* Set value for an offset.
*
* @link https://php.net/manual/en/arrayiterator.offsetset.php
*
* @param string|int $index <p>
* The index to set for.
* </p>
* @param $value
*
* @throws \Throwable
*/
public function offsetSet($index, $value): Promise
{
if ($this->getCache($index) === $value) {
return new Success();
}
$this->setCache($index, $value);
$request = $this->execute(
$this->set,
[
'index' => $index,
'value' => \serialize($value),
]
);
//Ensure that cache is synced with latest insert in case of concurrent requests.
$request->onResolve(fn () => $this->setCache($index, $value));
return $request;
}
/**
* Perform async request to db.
*
* @param Statement $query
* @param array $params
*
* @return Promise
* @throws \Throwable
*/
protected function execute(Statement $stmt, array $params = []): Promise
{
return call(function () use ($stmt, $params) {
if (
!empty($params['index'])
&& !\mb_check_encoding($params['index'], 'UTF-8')
) {
$params['index'] = \mb_convert_encoding($params['index'], 'UTF-8');
}
try {
$request = yield $stmt->execute($params);
} catch (\Throwable $e) {
Logger::log($e->getMessage(), Logger::ERROR);
return [];
}
$result = [];
if ($request instanceof ResultSet) {
while (yield $request->advance()) {
$result[] = $request->getCurrent();
}
}
return $result;
});
}
}

View File

@ -52,10 +52,10 @@ class DoHConnector implements Connector
$this->dataCenter = $dataCenter;
$this->ctx = $ctx;
}
public function connect(string $uri, ?ConnectContext $socketContext = null, ?CancellationToken $token = null): Promise
public function connect(string $uri, ?ConnectContext $context = null, ?CancellationToken $token = null): Promise
{
return Tools::call((function () use ($uri, $socketContext, $token): \Generator {
$socketContext = $socketContext ?? new ConnectContext();
return Tools::call((function () use ($uri, $context, $token): \Generator {
$socketContext = $context ?? new ConnectContext();
$token = $token ?? new NullCancellationToken();
$attempt = 0;
$uris = [];
@ -113,20 +113,23 @@ class DoHConnector implements Connector
}
$flags = \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT;
$timeout = $socketContext->getConnectTimeout();
$e = null;
foreach ($uris as $builtUri) {
try {
$streamContext = \stream_context_create($socketContext->withoutTlsContext()->toStreamContextArray());
/** @psalm-suppress NullArgument */
if (!($socket = @\stream_socket_client($builtUri, $errno, $errstr, null, $flags, $streamContext))) {
throw new ConnectException(\sprintf('Connection to %s failed: [Error #%d] %s%s', $uri, $errno, $errstr, $failures ? '; previous attempts: ' . \implode($failures) : ''), $errno);
throw new ConnectException(\sprintf('Connection to %s failed: [Error #%d] %s%s', $uri, $errno, $errstr, $failures ? '; previous attempts: '.\implode($failures) : ''), $errno);
}
\stream_set_blocking($socket, false);
$deferred = new Deferred();
/** @psalm-suppress InvalidArgument */
$watcher = Loop::onWritable($socket, [$deferred, 'resolve']);
$id = $token->subscribe([$deferred, 'fail']);
try {
yield Promise\timeout($deferred->promise(), $timeout);
} catch (TimeoutException $e) {
throw new ConnectException(\sprintf('Connecting to %s failed: timeout exceeded (%d ms)%s', $uri, $timeout, $failures ? '; previous attempts: ' . \implode($failures) : ''), 110);
throw new ConnectException(\sprintf('Connecting to %s failed: timeout exceeded (%d ms)%s', $uri, $timeout, $failures ? '; previous attempts: '.\implode($failures) : ''), 110);
// See ETIMEDOUT in http://www.virtsync.com/c-error-codes-include-errno
} finally {
Loop::cancel($watcher);
@ -135,7 +138,7 @@ class DoHConnector implements Connector
// The following hack looks like the only way to detect connection refused errors with PHP's stream sockets.
if (\stream_socket_get_name($socket, true) === false) {
\fclose($socket);
throw new ConnectException(\sprintf('Connection to %s refused%s', $uri, $failures ? '; previous attempts: ' . \implode($failures) : ''), 111);
throw new ConnectException(\sprintf('Connection to %s refused%s', $uri, $failures ? '; previous attempts: '.\implode($failures) : ''), 111);
// See ECONNREFUSED in http://www.virtsync.com/c-error-codes-include-errno
}
} catch (ConnectException $e) {
@ -143,7 +146,7 @@ class DoHConnector implements Connector
// In fact, this might show a confusing error message on OS families that return 110 or 111 by itself.
$knownReasons = [110 => 'connection timeout', 111 => 'connection refused'];
$code = $e->getCode();
$reason = $knownReasons[$code] ?? 'Error #' . $code;
$reason = $knownReasons[$code] ?? 'Error #'.$code;
if (++$attempt === $socketContext->getMaxAttempts()) {
break;
}
@ -153,9 +156,12 @@ class DoHConnector implements Connector
}
return ResourceSocket::fromClientSocket($socket, $socketContext->getTlsContext());
}
// This is reached if either all URIs failed or the maximum number of attempts is reached.
/** @noinspection PhpUndefinedVariableInspection */
throw $e;
if ($e) {
throw $e;
}
})());
}
}

View File

@ -19,26 +19,45 @@
namespace danog\MadelineProto;
use danog\MadelineProto\Settings\TLSchema;
use danog\MadelineProto\TL\TL;
// This code was written a few years ago: it is garbage, and has to be rewritten
class DocsBuilder
{
const DEFAULT_TEMPLATES = [
'User' => ['User', 'InputUser', 'Chat', 'InputChannel', 'Peer', 'InputDialogPeer', 'DialogPeer', 'InputPeer', 'NotifyPeer', 'InputNotifyPeer'],
'InputFile' => ['InputFile', 'InputEncryptedFile'],
'InputEncryptedChat' => ['InputEncryptedChat'],
'PhoneCall' => ['PhoneCall'],
'InputPhoto' => ['InputPhoto'],
'InputDocument' => ['InputDocument'],
'InputMedia' => ['InputMedia'],
'InputMessage' => ['InputMessage'],
'KeyboardButton' => ['KeyboardButton'],
];
use \danog\MadelineProto\DocsBuilder\Methods;
use \danog\MadelineProto\DocsBuilder\Constructors;
use Tools;
public $td = false;
public function __construct($logger, $settings)
protected array $settings;
protected string $index;
protected Logger $logger;
protected TL $TL;
protected array $tdDescriptions;
public function __construct(Logger $logger, array $settings)
{
$this->logger = $logger;
\set_error_handler(['\\danog\\MadelineProto\\Exception', 'ExceptionErrorHandler']);
/** @psalm-suppress InvalidArgument */
$this->TL = new TL(new class($logger) {
public function __construct($logger)
{
$this->logger = $logger;
}
});
$this->TL->init($settings['tl_schema']);
$new = new TLSchema;
$new->mergeArray($settings);
$this->TL->init($new);
if (isset($settings['tl_schema']['td']) && !isset($settings['tl_schema']['telegram'])) {
$this->td = true;
}
@ -48,28 +67,26 @@ class DocsBuilder
}
\chdir($this->settings['output_dir']);
$this->index = $settings['readme'] ? 'README.md' : 'index.md';
foreach (\glob($this->settings['template']."/*") as $template) {
$this->templates[\basename($template, '.md')] = \file_get_contents($template);
}
}
/**
* Documentation templates.
*
* @var array
*/
protected $templates = [];
public $types = [];
public $any = '*';
public function mkDocs()
public function mkDocs(): void
{
\danog\MadelineProto\Logger::log('Generating documentation index...', \danog\MadelineProto\Logger::NOTICE);
\file_put_contents($this->index, '---
title: ' . $this->settings['title'] . '
description: ' . $this->settings['description'] . '
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
# ' . $this->settings['description'] . '
\file_put_contents($this->index, $this->template('index', $this->settings['title'], $this->settings['description']));
[Back to main documentation](..)
[Methods](methods/)
[Constructors](constructors/)
[Types](types/)');
$this->mkmethodS();
$this->mkMethods();
$this->mkConstructors();
foreach (\glob('types/*') as $unlink) {
\unlink($unlink);
@ -81,548 +98,75 @@ image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
\ksort($this->types);
$index = '';
\danog\MadelineProto\Logger::log('Generating types documentation...', \danog\MadelineProto\Logger::NOTICE);
$last_namespace = '';
foreach ($this->types as $otype => $keys) {
$new_namespace = \preg_replace('/_.*/', '', $otype);
//$br = $new_namespace != $last_namespace ? '***<br><br>' : '';
$type = \str_replace(['<', '>'], ['_of_', ''], $otype);
$type = \preg_replace('/.*_of_/', '', $type);
$index .= '[' . \str_replace('_', '\\_', $type) . '](' . $type . '.md)<a name="' . $type . '"></a>
$type = StrTools::typeEscape($otype);
$index .= '['.StrTools::markdownEscape($type).']('.$type.'.md)<a name="'.$type.'"></a>
';
$constructors = '';
foreach ($keys['constructors'] as $data) {
$predicate = $data['predicate'] . (isset($data['layer']) && $data['layer'] !== '' ? '_' . $data['layer'] : '');
$md_predicate = \str_replace('_', '\\_', $predicate);
$constructors .= '[' . $md_predicate . '](../constructors/' . $predicate . '.md)
';
$predicate = $data['predicate'].(isset($data['layer']) && $data['layer'] !== '' ? '_'.$data['layer'] : '');
$md_predicate = StrTools::markdownEscape($predicate);
$constructors .= "[$md_predicate](../constructors/$predicate.md) \n\n";
}
$methods = '';
foreach ($keys['methods'] as $data) {
$name = $data['method'];
$md_name = \str_replace(['.', '_'], ['->', '\\_'], $name);
$methods .= '[$MadelineProto->' . $md_name . '](../methods/' . $name . '.md)
';
$methods .= "[\$MadelineProto->$md_name](../methods/$name.md) \n\n";
}
$description = isset($this->td_descriptions['types'][$otype]) ? $this->td_descriptions['types'][$otype] : 'constructors and methods of type ' . $type;
$symFile = \str_replace('.', '_', $type);
$redir = $symFile !== $type ? "\nredirect_from: /API_docs/types/{$symFile}.html" : '';
$header = '---
title: ' . $type . '
description: constructors and methods of type ' . $type . '
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png' . $redir . '
---
# Type: ' . \str_replace('_', '\\_', $type) . '
[Back to types index](index.md)
';
$header .= isset($this->td_descriptions['types'][$otype]) ? $this->td_descriptions['types'][$otype] . PHP_EOL . PHP_EOL : '';
$header = '';
if (!isset($this->settings['td'])) {
if (\in_array($type, ['User', 'InputUser', 'Chat', 'InputChannel', 'Peer', 'InputDialogPeer', 'DialogPeer', 'InputPeer', 'NotifyPeer', 'InputNotifyPeer'])) {
$header .= 'You can directly provide the [Update](Update.md) or [Message](Message.md) object here, MadelineProto will automatically extract the destination chat id.
The following syntaxes can also be used:
```
$' . $type . " = '@username'; // Username\n\n\$" . $type . " = 'me'; // The currently logged-in user\n\n\$" . $type . ' = 44700; // bot API id (users)
$' . $type . ' = -492772765; // bot API id (chats)
$' . $type . ' = -10038575794; // bot API id (channels)
$' . $type . " = 'https://t.me/danogentili'; // t.me URLs\n\$" . $type . " = 'https://t.me/joinchat/asfln1-21fa_'; // t.me invite links\n\n\$" . $type . " = 'user#44700'; // tg-cli style id (users)\n\$" . $type . " = 'chat#492772765'; // tg-cli style id (chats)\n\$" . $type . " = 'channel#38575794'; // tg-cli style id (channels)\n```\n\nA [Chat](Chat.md), a [User](User.md), an [InputPeer](InputPeer.md), an [InputDialogPeer](InputDialogPeer.md), an [InputNotifyPeer](InputNotifyPeer.md), an [InputUser](InputUser.md), an [InputChannel](InputChannel.md), a [Peer](Peer.md), an [DialogPeer](DialogPeer.md), [NotifyPeer](NotifyPeer.md), or a [Chat](Chat.md) object can also be used.\n\n\n";
}
if (\in_array($type, ['InputEncryptedChat'])) {
$header .= 'You can directly provide the [Update](Update.md) or [EncryptedMessage](EncryptedMessage.md) object here, MadelineProto will automatically extract the destination chat id.
The following syntax can also be used:
```
$' . $type . ' = -147286699; // Numeric chat id returned by requestSecretChat, can be positive or negative
```
';
}
if (\in_array($type, ['InputFile', 'InputEncryptedFile'])) {
$header .= 'The following syntax can also be used:
```
$' . $type . ' = \'filename.mp4\'; // The file path can also be used
```
';
}
if (\in_array($type, ['InputPhoto'])) {
$header .= 'You can also provide a [MessageMedia](MessageMedia.md), [Message](Message.md), [Update](Update.md), [Photo](Photo.md) here, MadelineProto will automatically convert it to the right type.
';
}
if (\in_array($type, ['InputDocument'])) {
$header .= 'You can also provide a [MessageMedia](MessageMedia.md), [Message](Message.md), [Update](Update.md), [Document](Document.md) here, MadelineProto will automatically convert it to the right type.
';
}
if (\in_array($type, ['InputMedia'])) {
$header .= 'You can also provide a [MessageMedia](MessageMedia.md), [Message](Message.md), [Update](Update.md), [Document](Document.md), [Photo](Photo.md), [InputDocument](InputDocument.md), [InputPhoto](InputPhoto.md) here, MadelineProto will automatically convert it to the right type.
';
}
if (\in_array($type, ['InputMessage'])) {
$header .= 'The following syntax can also be used:
```
$' . $type . ' = 142; // Numeric message ID
```
';
}
if (\in_array($type, ['KeyboardButton'])) {
$header .= 'Clicking these buttons:
To click these buttons simply run the `click` method:
```
$result = $' . $type . '->click();
```
`$result` can be one of the following:
* A string - If the button is a keyboardButtonUrl
* [Updates](Updates.md) - If the button is a keyboardButton, the message will be sent to the chat, in reply to the message with the keyboard
* [messages.BotCallbackAnswer](messages.BotCallbackAnswer.md) - If the button is a keyboardButtonCallback or a keyboardButtonGame the button will be pressed and the result will be returned
* `false` - If the button is an unsupported button, like keyboardButtonRequestPhone, keyboardButtonRequestGeoLocation, keyboardButtonSwitchInlinekeyboardButtonBuy; you will have to parse data from these buttons manually
You can also access the properties of the constructor as a normal array, for example $button[\'name\']
';
foreach (self::DEFAULT_TEMPLATES as $template => $types) {
if (\in_array($type, $types)) {
$header .= $this->template($template, $type);
}
}
}
$constructors = '### Possible values (constructors):
' . $constructors . '
';
$methods = '### Methods that return an object of this type (methods):
' . $methods . '
';
if (!isset($this->settings['td'])) {
if (\in_array($type, ['PhoneCall'])) {
$methods = '';
$constructors = '';
$header .= 'This is an object of type `\\danog\\MadelineProto\\VoIP`.
It will only be available if the [php-libtgvoip](https://github.com/danog/php-libtgvoip) extension is installed, see [the main docs](https://docs.madelineproto.xyz#calls) for an easy installation script.
You MUST know [OOP](http://php.net/manual/en/language.oop5.php) to use this class.
## Constants:
VoIPController states (these constants are incrementing integers, thus can be compared like numbers):
* `STATE_CREATED` - controller created
* `STATE_WAIT_INIT` - controller inited
* `STATE_WAIT_INIT_ACK` - controller inited
* `STATE_ESTABLISHED` - connection established
* `STATE_FAILED` - connection failed
* `STATE_RECONNECTING` - reconnecting
VoIPController errors:
* `TGVOIP_ERROR_UNKNOWN` - An unknown error occurred
* `TGVOIP_ERROR_INCOMPATIBLE` - The other side is using an unsupported client/protocol
* `TGVOIP_ERROR_TIMEOUT` - A timeout occurred
* `TGVOIP_ERROR_AUDIO_IO` - An I/O error occurred
Network types (these constants are incrementing integers, thus can be compared like numbers):
* `NET_TYPE_UNKNOWN` - Unknown network type
* `NET_TYPE_GPRS` - GPRS connection
* `NET_TYPE_EDGE` - EDGE connection
* `NET_TYPE_3G` - 3G connection
* `NET_TYPE_HSPA` - HSPA connection
* `NET_TYPE_LTE` - LTE connection
* `NET_TYPE_WIFI` - WIFI connection
* `NET_TYPE_ETHERNET` - Ethernet connection (this guarantees high audio quality)
* `NET_TYPE_OTHER_HIGH_SPEED` - Other high speed connection
* `NET_TYPE_OTHER_LOW_SPEED` - Other low speed connection
* `NET_TYPE_DIALUP` - Dialup connection
* `NET_TYPE_OTHER_MOBILE` - Other mobile network connection
Data saving modes (these constants are incrementing integers, thus can be compared like numbers):
* `DATA_SAVING_NEVER` - Never save data (this guarantees high audio quality)
* `DATA_SAVING_MOBILE` - Use mobile data saving profiles
* `DATA_SAVING_ALWAYS` - Always use data saving profiles
Proxy settings (these constants are incrementing integers, thus can be compared like numbers):
* `PROXY_NONE` - No proxy
* `PROXY_SOCKS5` - Use the socks5 protocol
Audio states (these constants are incrementing integers, thus can be compared like numbers):
* `AUDIO_STATE_NONE` - The audio module was not created yet
* `AUDIO_STATE_CREATED` - The audio module was created
* `AUDIO_STATE_CONFIGURED` - The audio module was configured
* `AUDIO_STATE_RUNNING` - The audio module is running
Call states (these constants are incrementing integers, thus can be compared like numbers):
* `CALL_STATE_NONE` - The call was not created yet
* `CALL_STATE_REQUESTED` - This is an outgoing call
* `CALL_STATE_INCOMING` - This is an incoming call
* `CALL_STATE_ACCEPTED` - The incoming call was accepted, but not yet ready
* `CALL_STATE_CONFIRMED` - The outgoing call was accepted, but not yet ready
* `CALL_STATE_READY` - The call is ready. Audio data is being sent and received
* `CALL_STATE_ENDED` - The call is over.
## Methods:
* `getState()` - Gets the controller state, as a VoIPController state constant
* `getCallState()` - Gets the call state, as a call state constant
* `getVisualization()` - Gets the visualization of the encryption key, as an array of emojis, can be called only when the call state is bigger than or equal to `CALL_STATE_READY`. If called sooner, returns false.
* `getStats()` Gets connection stats
* `getOtherID()` - Gets the id of the other call participant, as a bot API ID
* `getProtocol()` - Gets the protocol used by the current call, as a [PhoneCallProtocol](https://docs.madelineproto.xyz/API_docs/types/PhoneCallProtocol.html) object
* `getCallID()` - Gets the call ID, as an [InputPhoneCall](https://docs.madelineproto.xyz/API_docs/types/InputPhoneCall.html) object
* `isCreator()` - Returns a boolean that indicates whether you are the creator of the call
* `whenCreated()` - Returns the unix timestamp of when the call was started (when was the call state set to `CALL_STATE_READY`)
* `getOutputState()` - Returns the state of the audio output module, as an audio state constant
* `getInputState()` - Returns the state of the audio input module, as an audio state constant
* `getDebugLog()` - Gets VoIPController debug log
* `getDebugString()` - Gets VoIPController debug string
* `getLastError()` - Gets the last error as a VoIPController error constant
* `getVersion()` - Gets VoIPController version
* `getSignalBarsCount()` - Gets number of signal bars (0-4)
* `parseConfig()` - Parses the configuration
* `accept()` - Accepts the phone call, returns `$this`
* `discard($reason = ["_" => "phoneCallDiscardReasonDisconnect"], $rating = [])` - Ends the phone call.
Accepts two optional parameters:
`$reason` - can be a [PhoneCallDiscardReason](https://docs.madelineproto.xyz/API_docs/types/PhoneCallDiscardReason.html) object (defaults to a [phoneCallDiscardReasonDisconnect](https://docs.madelineproto.xyz/API_docs/constructors/phoneCallDiscardReasonDisconnect.html) object).
`$rating` - Can be an array that must contain a rating, and a comment (`["rating" => 5, "comment" => "MadelineProto is very easy to use!"]). Defaults to an empty array.`
* `getOutputParams()` - Returns the output audio configuration
MadelineProto works using raw signed PCM audio, internally split in packets with `sampleNumber` samples.
The audio configuration is an array structured in the following way:
```
[
"bitsPerSample" => int. // Bits in each PCM sample
"sampleRate" => int, // PCM sample rate
"channels" => int, // Number of PCM audio channels
"sampleNumber" => int, // The audio data is internally split in packets, each having this number of samples
"samplePeriod" => double, // PCM sample period in seconds, useful if you want to generate audio data manually
"writePeriod" => double, // PCM write period in seconds (samplePeriod*sampleNumber), useful if you want to generate audio data manually
"samplesSize" => int, // The audio data is internally split in packets, each having this number of bytes (sampleNumber*bitsPerSample/8)
"level" => int // idk
];
```
* `getInputParams()` - Returns the input audio configuration
MadelineProto works using raw signed PCM audio, internally split in packets with `sampleNumber` samples.
The audio configuration is an array structured in the following way:
```
[
"bitsPerSample" => int. // Bits in each PCM sample
"sampleRate" => int, // PCM sample rate
"channels" => int, // Number of PCM audio channels
"sampleNumber" => int, // The audio data is internally split in packets, each having this number of samples
"samplePeriod" => double, // PCM sample period in seconds, useful if you want to generate audio data manually
"writePeriod" => double, // PCM write period in seconds (samplePeriod*sampleNumber), useful if you want to generate audio data manually
"samplesSize" => int, // The audio data is internally split in packets, each having this number of bytes (sampleNumber*bitsPerSample/8)
];
```
* `play(string $file)` and `then(string $file)` - Play a certain audio file encoded in PCM, with the audio input configuration, returns `$this`
* `playOnHold(array $files)` - Array of audio files encoded in PCM, with the audio input configuration to loop on hold (when the files given with play/then have finished playing). If not called, no data will be played, returns `$this`
* `isPlaying()` - Returns true if MadelineProto is still playing the files given with play/then, false if the hold files (or nothing) is being played
* `setMicMute(bool $mute)` - Stops/resumes playing files/hold files, returns `$this`
* `setOutputFile(string $outputfile)` - Writes incoming audio data to file encoded in PCM, with the audio output configuration, returns `$this`
* `unsetOutputFile()` - Stops writing audio data to previously set file, returns `$this`
## Properties:
* `storage`: An array that can be used to store data related to this call.
Easy as pie:
```
$call->storage["pony"] = "fluttershy";
\\danog\\MadelineProto\\Logger::log($call->storage["pony"]); // fluttershy
```
Note: when modifying this property, *never* overwrite the previous values. Always either modify the values of the array separately like showed above, or use array_merge.
* `configuration`: An array containing the libtgvoip configuration.
You can only modify the data saving mode, the network type, the logging file path and the stats dump file path:
Example:
```
$call->configuration["log_file_path"] = "logs".$call->getOtherID().".log"; // Default is /dev/null
$call->configuration["stats_dump_file_path"] = "stats".$call->getOtherID().".log"; // Default is /dev/null
$call->configuration["network_type"] = \\danog\\MadelineProto\\VoIP::NET_TYPE_WIFI; // Default is NET_TYPE_ETHERNET
$call->configuration["data_saving"] = \\danog\\MadelineProto\\VoIP::DATA_SAVING_MOBILE; // Default is DATA_SAVING_NEVER
$call->parseConfig(); // Always call this after changing settings
```
Note: when modifying this property, *never* overwrite the previous values. Always either modify the values of the array separately like showed above, or use array_merge.
After modifying it, you must always parse the new configuration with a call to `parseConfig`.
';
}
if (isset($this->tdDescriptions['types'][$otype])) {
$header = "{$this->tdDescriptions['types'][$otype]}\n\n$header";
}
if (\file_exists('types/' . $type . '.md')) {
\danog\MadelineProto\Logger::log($type);
}
\file_put_contents('types/' . $type . '.md', $header . $constructors . $methods);
$last_namespace = $new_namespace;
$header = \sprintf(
$this->templates['Type'],
$type,
$redir,
StrTools::markdownEscape($type),
$header,
$constructors,
$methods
);
\file_put_contents('types/'.$type.'.md', $header.$constructors.$methods);
}
\danog\MadelineProto\Logger::log('Generating types index...', \danog\MadelineProto\Logger::NOTICE);
\file_put_contents('types/' . $this->index, '---
title: Types
description: List of types
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
# Types
[Back to API documentation index](..)
\file_put_contents('types/'.$this->index, $this->templates['types-index'].$index);
' . $index);
\danog\MadelineProto\Logger::log('Generating additional types...', \danog\MadelineProto\Logger::NOTICE);
\file_put_contents('types/string.md', '---
title: string
description: A UTF8 string of variable length
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
## Type: string
[Back to constructor index](index.md)
A UTF8 string of variable length. The total length in bytes of the string must not be bigger than 16777215.
');
\file_put_contents('types/bytes.md', '---
title: bytes
description: A string of variable length
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
## Type: bytes
[Back to constructor index](index.md)
```php
$bytes = "simple string of bytes";
```
Internally, an object of type `\\danog\\MadelineProto\\TL\\Types\\Bytes`.
When casted to string, turns into a string of bytes of variable length, with length smaller than or equal to 16777215.
When JSON-serialized, turns into an array of the following format:
```
[
\'_\' => \'bytes\',
\'bytes\' => base64_encode($contents)
];
```
');
\file_put_contents('types/int.md', '---
title: integer
description: A 32 bit signed integer ranging from -2147483648 to 2147483647
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
## Type: int
[Back to constructor index](index.md)
A 32 bit signed integer ranging from `-2147483648` to `2147483647`.
');
\file_put_contents('types/int53.md', '---
title: integer
description: A 53 bit signed integer
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
## Type: int53
[Back to constructor index](index.md)
A 53 bit signed integer.
');
\file_put_contents('types/long.md', '---
title: long
description: A 32 bit signed integer ranging from -9223372036854775808 to 9223372036854775807
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
## Type: long
[Back to constructor index](index.md)
A 64 bit signed integer ranging from `-9223372036854775808` to `9223372036854775807`.
');
\file_put_contents('types/int128.md', '---
title: int128
description: A 128 bit signed integer
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
## Type: int128
[Back to constructor index](index.md)
A 128 bit signed integer represented in little-endian base256 (`string`) format.
');
\file_put_contents('types/int256.md', '---
title: int256
description: A 256 bit signed integer
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
## Type: int256
[Back to constructor index](index.md)
A 256 bit signed integer represented in little-endian base256 (`string`) format.
');
\file_put_contents('types/int512.md', '---
title: int512
description: A 512 bit signed integer
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
## Type: int512
[Back to constructor index](index.md)
A 512 bit signed integer represented in little-endian base256 (`string`) format.
');
\file_put_contents('types/double.md', '---
title: double
description: A double precision floating point number
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
## Type: double
[Back to constructor index](index.md)
A double precision floating point number, single precision can also be used (float).
');
\file_put_contents('types/!X.md', '---
title: !X
description: Represents a TL serialized payload
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
## Type: !X
[Back to constructor index](index.md)
Represents a TL serialized payload.
');
\file_put_contents('types/X.md', '---
title: X
description: Represents a TL serialized payload
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
## Type: X
[Back to constructor index](index.md)
Represents a TL serialized payload.
');
\file_put_contents('constructors/boolFalse.md', '---
title: boolFalse
description: Represents a boolean with value equal to false
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
# boolFalse
[Back to constructor index](index.md)
Represents a boolean with value equal to `false`.
');
\file_put_contents('constructors/boolTrue.md', '---
title: boolTrue
description: Represents a boolean with value equal to true
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
# boolTrue
[Back to constructor index](index.md)
Represents a boolean with value equal to `true`.
');
\file_put_contents('constructors/null.md', '---
title: null
description: Represents a null value
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
# null
[Back to constructor index](index.md)
Represents a `null` value.
');
\file_put_contents('types/Bool.md', '---
title: Bool
description: Represents a boolean.
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
# Bool
[Back to types index](index.md)
Represents a boolean.
');
\file_put_contents('types/DataJSON.md', '---
title: DataJSON
description: Any json-encodable data
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
## Type: DataJSON
[Back to constructor index](index.md)
Any json-encodable data.
');
foreach (['string', 'bytes', 'int', 'int53', 'long', 'int128', 'int256', 'int512', 'double', 'Bool', 'DataJSON'] as $type) {
\file_put_contents("types/$type.md", $this->templates[$type]);
}
foreach (['boolFalse', 'boolTrue', 'null', 'photoStrippedSize'] as $constructor) {
\file_put_contents("constructors/$constructor.md", $this->templates[$constructor]);
}
\danog\MadelineProto\Logger::log('Done!', \danog\MadelineProto\Logger::NOTICE);
}
public static $template = '<?php
/**
* Lang module
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\\MadelineProto;
class Lang
{
public static $lang = %s;
// THIS WILL BE OVERWRITTEN BY $lang["en"]
public static $current_lang = %s;
}';
public static function addToLang(string $key, string $value = '', bool $force = false)
public static function addToLang(string $key, string $value = '', bool $force = false): void
{
if (!isset(\danog\MadelineProto\Lang::$lang['en'][$key]) || $force) {
\danog\MadelineProto\Lang::$lang['en'][$key] = $value;
\file_put_contents(__DIR__ . '/Lang.php', \sprintf(self::$template, \var_export(\danog\MadelineProto\Lang::$lang, true), \var_export(\danog\MadelineProto\Lang::$lang['en'], true)));
}
}
/**
* Get formatted template string.
*
* @param string $name Template name
* @param string ...$params Params
*
* @return string
*/
protected function template(string $name, string ...$params): string
{
return \sprintf($this->templates[$name], ...$params);
}
}

View File

@ -19,13 +19,14 @@
namespace danog\MadelineProto\DocsBuilder;
use danog\MadelineProto\StrTools;
use danog\MadelineProto\Tools;
trait Constructors
{
public function mkConstructors()
public function mkConstructors(): void
{
foreach (\glob('constructors/' . $this->any) as $unlink) {
foreach (\glob('constructors/'.$this->any) as $unlink) {
\unlink($unlink);
}
if (\file_exists('constructors')) {
@ -41,15 +42,11 @@ trait Constructors
$data['layer'] = '';
}
$got[$id] = '';
/*
if (preg_match('/%/', $type)) {
$type = $this->TL->getConstructors($this->td)->findByType(str_replace('%', '', $type))['predicate'];
}*/
$layer = isset($data['layer']) && $data['layer'] !== '' ? '_' . $data['layer'] : '';
$type = \str_replace(['<', '>'], ['_of_', ''], $data['type']);
$php_type = \preg_replace('/.*_of_/', '', $type);
$constructor = \str_replace(['<', '>'], ['_of_', ''], $data['predicate']);
$php_constructor = \preg_replace('/.*_of_/', '', $constructor);
$layer = isset($data['layer']) && $data['layer'] !== '' ? '_'.$data['layer'] : '';
$type = $data['type'];
$constructor = $data['predicate'];
$php_type = Tools::typeEscape($type);
$php_constructor = Tools::typeEscape($constructor);
if (!isset($this->types[$php_type])) {
$this->types[$php_type] = ['constructors' => [], 'methods' => []];
}
@ -74,12 +71,12 @@ trait Constructors
if (\substr($param[$type_or_subtype], -1) === '>') {
$param[$type_or_subtype] = \substr($param[$type_or_subtype], 0, -1);
}
$params .= "'" . $param['name'] . "' => ";
$param[$type_or_subtype] = '[' . Tools::markdownEscape($param[$type_or_subtype]) . '](../' . $type_or_bare_type . '/' . $param[$type_or_subtype] . '.md)';
$params .= (isset($param['subtype']) ? '\\[' . $param[$type_or_subtype] . '\\]' : $param[$type_or_subtype]) . ', ';
$params .= "'".$param['name']."' => ";
$param[$type_or_subtype] = '['.Tools::markdownEscape($param[$type_or_subtype]).'](../'.$type_or_bare_type.'/'.$param[$type_or_subtype].'.md)';
$params .= (isset($param['subtype']) ? '\\['.$param[$type_or_subtype].'\\]' : $param[$type_or_subtype]).', ';
}
$md_constructor = \str_replace('_', '\\_', $constructor . $layer);
$this->docs_constructors[$constructor] = '[$' . $md_constructor . '](../constructors/' . $php_constructor . $layer . '.md) = \\[' . $params . '\\];<a name="' . $constructor . $layer . '"></a>
$md_constructor = StrTools::markdownEscape($constructor.$layer);
$this->docs_constructors[$constructor] = '[$'.$md_constructor.'](../constructors/'.$php_constructor.$layer.'.md) = \\['.$params.'\\];<a name="'.$constructor.$layer.'"></a>
';
$table = empty($data['params']) ? '' : '### Attributes:
@ -87,13 +84,14 @@ trait Constructors
| Name | Type | Required |
|----------|---------------|----------|
';
if (!isset($this->TL->getDescriptions()['constructors'][$data['predicate']])) {
$this->addToLang('object_' . $data['predicate']);
if (\danog\MadelineProto\Lang::$lang['en']['object_' . $data['predicate']] !== '') {
$this->TL->getDescriptions()['constructors'][$data['predicate']]['description'] = \danog\MadelineProto\Lang::$lang['en']['object_' . $data['predicate']];
if (!isset($this->TL->getDescriptions()['constructors'][$constructor])) {
$this->addToLang('object_'.$constructor);
if (\danog\MadelineProto\Lang::$lang['en']['object_'.$constructor] !== '') {
/** @psalm-suppress InvalidArrayAssignment */
$this->TL->getDescriptions()['constructors'][$constructor]['description'] = \danog\MadelineProto\Lang::$lang['en']['object_'.$constructor];
}
}
if (isset($this->TL->getDescriptions()['constructors'][$data['predicate']]) && !empty($data['params'])) {
if (isset($this->TL->getDescriptions()['constructors'][$constructor]) && !empty($data['params'])) {
$table = '### Attributes:
| Name | Type | Required | Description |
@ -114,16 +112,10 @@ trait Constructors
$param['type'] = 'DecryptedMessage';
}
if ($type === 'DecryptedMessageMedia' && \in_array($param['name'], ['key', 'iv'])) {
unset(\danog\MadelineProto\Lang::$lang['en']['object_' . $data['predicate'] . '_param_' . $param['name'] . '_type_' . $param['type']]);
unset(\danog\MadelineProto\Lang::$lang['en']['object_'.$constructor.'_param_'.$param['name'].'_type_'.$param['type']]);
continue;
}
$ptype = $param[isset($param['subtype']) ? 'subtype' : 'type'];
//$type_or_bare_type = 'types';
/*if (isset($param['subtype'])) {
if ($param['type'] === 'vector') {
$type_or_bare_type = 'constructors';
}
}*/
if (\preg_match('/%/', $ptype)) {
$ptype = $this->TL->getConstructors($this->td)->findByType(\str_replace('%', '', $ptype))['predicate'];
}
@ -138,58 +130,60 @@ trait Constructors
}
$human_ptype = $ptype;
if (\strpos($type, 'Input') === 0 && \in_array($ptype, ['User', 'InputUser', 'Chat', 'InputChannel', 'Peer', 'InputDialogPeer', 'DialogPeer', 'NotifyPeer', 'InputNotifyPeer', 'InputPeer']) && !isset($this->settings['td'])) {
$human_ptype = 'Username, chat ID, Update, Message or ' . $ptype;
$human_ptype = 'Username, chat ID, Update, Message or '.$ptype;
}
if (\strpos($type, 'Input') === 0 && \in_array($ptype, ['InputMedia', 'InputDocument', 'InputPhoto']) && !isset($this->settings['td'])) {
$human_ptype = 'MessageMedia, Message, Update or ' . $ptype;
$human_ptype = 'MessageMedia, Message, Update or '.$ptype;
}
if (\in_array($ptype, ['InputMessage']) && !isset($this->settings['td'])) {
$human_ptype = 'Message ID or ' . $ptype;
$human_ptype = 'Message ID or '.$ptype;
}
if (\in_array($ptype, ['InputEncryptedChat']) && !isset($this->settings['td'])) {
$human_ptype = 'Secret chat ID, Update, EncryptedMessage or ' . $ptype;
$human_ptype = 'Secret chat ID, Update, EncryptedMessage or '.$ptype;
}
if (\in_array($ptype, ['InputFile']) && !isset($this->settings['td'])) {
$human_ptype = 'File path or ' . $ptype;
$human_ptype = 'File path or '.$ptype;
}
if (\in_array($ptype, ['InputEncryptedFile']) && !isset($this->settings['td'])) {
$human_ptype = 'File path or ' . $ptype;
$human_ptype = 'File path or '.$ptype;
}
$table .= '|' . \str_replace('_', '\\_', $param['name']) . '|' . (isset($param['subtype']) ? 'Array of ' : '') . '[' . \str_replace('_', '\\_', $human_ptype) . '](../' . $type_or_bare_type . '/' . $ptype . '.md) | ' . (isset($param['pow']) || $this->TL->getConstructors($this->td)->findByPredicate(\lcfirst($param['type']) . 'Empty') || $data['type'] === 'InputMedia' && $param['name'] === 'mime_type' || $data['type'] === 'DocumentAttribute' && \in_array($param['name'], ['w', 'h', 'duration']) ? 'Optional' : 'Yes') . '|';
if (!isset($this->TL->getDescriptions()['constructors'][$data['predicate']]['params'][$param['name']])) {
$this->addToLang('object_' . $data['predicate'] . '_param_' . $param['name'] . '_type_' . $param['type']);
if (isset($this->TL->getDescriptions()['constructors'][$data['predicate']]['description'])) {
$this->TL->getDescriptions()['constructors'][$data['predicate']]['params'][$param['name']] = \danog\MadelineProto\Lang::$lang['en']['object_' . $data['predicate'] . '_param_' . $param['name'] . '_type_' . $param['type']];
$table .= '|'.StrTools::markdownEscape($param['name']).'|'.(isset($param['subtype']) ? 'Array of ' : '').'['.StrTools::markdownEscape($human_ptype).'](../'.$type_or_bare_type.'/'.$ptype.'.md) | '.(isset($param['pow']) || $this->TL->getConstructors($this->td)->findByPredicate(\lcfirst($param['type']).'Empty') || $data['type'] === 'InputMedia' && $param['name'] === 'mime_type' || $data['type'] === 'DocumentAttribute' && \in_array($param['name'], ['w', 'h', 'duration']) ? 'Optional' : 'Yes').'|';
if (!isset($this->TL->getDescriptions()['constructors'][$constructor]['params'][$param['name']])) {
$this->addToLang('object_'.$constructor.'_param_'.$param['name'].'_type_'.$param['type']);
if (isset($this->TL->getDescriptions()['constructors'][$constructor]['description'])) {
/** @psalm-suppress InvalidArrayAssignment */
$this->TL->getDescriptions()['constructors'][$constructor]['params'][$param['name']] = \danog\MadelineProto\Lang::$lang['en']['object_'.$constructor.'_param_'.$param['name'].'_type_'.$param['type']];
}
}
if (isset($this->TL->getDescriptions()['constructors'][$data['predicate']]['params'][$param['name']])) {
$table .= $this->TL->getDescriptions()['constructors'][$data['predicate']]['params'][$param['name']] . '|';
if (isset($this->TL->getDescriptions()['constructors'][$constructor]['params'][$param['name']]) && $this->TL->getDescriptions()['constructors'][$constructor]['params'][$param['name']]) {
$table .= $this->TL->getDescriptions()['constructors'][$constructor]['params'][$param['name']].'|';
}
$table .= PHP_EOL;
$pptype = \in_array($ptype, ['string', 'bytes']) ? "'" . $ptype . "'" : $ptype;
$ppptype = \in_array($ptype, ['string']) ? '"' . $ptype . '"' : $ptype;
$ppptype = \in_array($ptype, ['bytes']) ? '{"_": "bytes", "bytes":"base64 encoded ' . $ptype . '"}' : $ppptype;
$params .= ", '" . $param['name'] . "' => ";
$params .= isset($param['subtype']) ? '[' . $pptype . ', ' . $pptype . ']' : $pptype;
$lua_params .= ', ' . $param['name'] . '=';
$lua_params .= isset($param['subtype']) ? '{' . $pptype . '}' : $pptype;
$pwr_params .= ', "' . $param['name'] . '": ' . (isset($param['subtype']) ? '[' . $ppptype . ']' : $ppptype);
$pptype = \in_array($ptype, ['string', 'bytes']) ? "'".$ptype."'" : $ptype;
$ppptype = \in_array($ptype, ['string']) ? '"'.$ptype.'"' : $ptype;
$ppptype = \in_array($ptype, ['bytes']) ? '{"_": "bytes", "bytes":"base64 encoded '.$ptype.'"}' : $ppptype;
$params .= ", '".$param['name']."' => ";
$params .= isset($param['subtype']) ? '['.$pptype.', '.$pptype.']' : $pptype;
$lua_params .= ', '.$param['name'].'=';
$lua_params .= isset($param['subtype']) ? '{'.$pptype.'}' : $pptype;
$pwr_params .= ', "'.$param['name'].'": '.(isset($param['subtype']) ? '['.$ppptype.']' : $ppptype);
if ($param['name'] === 'reply_markup') {
$hasreplymarkup = true;
}
}
$params = "['_' => '" . $data['predicate'] . "'" . $params . ']';
$lua_params = "{_='" . $data['predicate'] . "'" . $lua_params . '}';
$pwr_params = '{"_": "' . $data['predicate'] . '"' . $pwr_params . '}';
$description = isset($this->TL->getDescriptions()['constructors'][$data['predicate']]) ? $this->TL->getDescriptions()['constructors'][$data['predicate']]['description'] : $constructor . ' attributes, type and example';
$symFile = \str_replace('.', '_', $constructor . $layer);
$redir = $symFile !== $constructor . $layer ? "\nredirect_from: /API_docs/constructors/{$symFile}.html" : '';
$params = "['_' => '".$constructor."'".$params.']';
$lua_params = "{_='".$constructor."'".$lua_params.'}';
$pwr_params = '{"_": "'.$constructor.'"'.$pwr_params.'}';
$description = isset($this->TL->getDescriptions()['constructors'][$constructor]) ? $this->TL->getDescriptions()['constructors'][$constructor]['description'] : $constructor.' attributes, type and example';
$symFile = \str_replace('.', '_', $constructor.$layer);
$redir = $symFile !== $constructor.$layer ? "\nredirect_from: /API_docs/constructors/{$symFile}.html" : '';
$description = \rtrim(\explode("\n", $description)[0], ':');
$header = '---
title: ' . $data['predicate'] . '
description: ' . $description . '
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png' . $redir . '
title: '.$constructor.'
description: '.$description.'
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png'.$redir.'
---
# Constructor: ' . \str_replace('_', '\\_', $data['predicate'] . $layer) . '
# Constructor: '.StrTools::markdownEscape($constructor.$layer).'
[Back to constructors index](index.md)
@ -199,95 +193,34 @@ image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png' . $re
';
if (isset($this->TL->getDescriptions()['constructors'][$data['predicate']])) {
$header .= $this->TL->getDescriptions()['constructors'][$data['predicate']]['description'] . PHP_EOL . PHP_EOL;
if (isset($this->TL->getDescriptions()['constructors'][$constructor])) {
$header .= $this->TL->getDescriptions()['constructors'][$constructor]['description'].PHP_EOL.PHP_EOL;
}
$type = '### Type: [' . \str_replace('_', '\\_', $php_type) . '](../types/' . $php_type . '.md)
$type = '### Type: ['.StrTools::markdownEscape($php_type).'](../types/'.$php_type.'.md)
';
$example = '';
if (!isset($this->settings['td'])) {
$example = '### Example:
```php
$' . $constructor . $layer . ' = ' . $params . ';
```
Or, if you\'re into Lua:
```lua
' . $constructor . $layer . '=' . $lua_params . '
```
';
$example = $this->template('constructor-example', $constructor.$layer, $params, $lua_params);
if ($hasreplymarkup) {
$example .= '
## Usage of reply_markup
You can provide bot API reply_markup objects here.
';
$example .= $this->template('reply_markup');
}
if ($hasentities) {
$example .= '
## Usage of parseMode:
Set parseMode to html to enable HTML parsing of the message.
Set parseMode to Markdown to enable markown AND html parsing of the message.
The following tags are currently supported:
```html
<br>a newline
<b><i>bold works ok, internal tags are stripped</i> </b>
<strong>bold</strong>
<em>italic</em>
<i>italic</i>
<code>inline fixed-width code</code>
<pre>pre-formatted fixed-width code block</pre>
<a href="https://github.com">URL</a>
<a href="mention:@danogentili">Mention by username</a>
<a href="mention:186785362">Mention by user id</a>
<pre language="json">Pre tags can have a language attribute</pre>
```
You can also use normal markdown, note that to create mentions you must use the `mention:` syntax like in html:
```markdown
[Mention by username](mention:@danogentili)
[Mention by user id](mention:186785362)
```
MadelineProto supports all html entities supported by [html_entity_decode](http://php.net/manual/en/function.html-entity-decode.php).
';
$example .= $this->template('parse_mode');
}
}
\file_put_contents('constructors/' . $constructor . $layer . '.md', $header . $table . $type . $example);
\file_put_contents('constructors/'.$constructor.$layer.'.md', $header.$table.$type.$example);
}
$this->logger->logger('Generating constructors index...', \danog\MadelineProto\Logger::NOTICE);
\ksort($this->docs_constructors);
$last_namespace = '';
foreach ($this->docs_constructors as $constructor => &$value) {
$new_namespace = \preg_replace('/_.*/', '', $constructor);
$br = $new_namespace != $last_namespace ? '***
<br><br>' : '';
$value = $br . $value;
$br = $new_namespace != $last_namespace ? "***\n<br><br>" : '';
$value = $br.$value;
$last_namespace = $new_namespace;
}
\file_put_contents('constructors/' . $this->index, '---
title: Constructors
description: List of constructors
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
# Constructors
[Back to API documentation index](..)
' . \implode('', $this->docs_constructors));
\file_put_contents('constructors/'.$this->index, $this->template('constructors-index', \implode('', $this->docs_constructors)));
}
}

View File

@ -19,11 +19,12 @@
namespace danog\MadelineProto\DocsBuilder;
use danog\MadelineProto\StrTools;
use danog\MadelineProto\Tools;
trait Methods
{
public function mkMethods()
public function mkMethods(): void
{
static $bots;
if (!$bots) {
@ -44,7 +45,7 @@ trait Methods
}
}
}
foreach (\glob('methods/' . $this->any) as $unlink) {
foreach (\glob('methods/'.$this->any) as $unlink) {
\unlink($unlink);
}
if (\file_exists('methods')) {
@ -56,7 +57,7 @@ trait Methods
$this->logger->logger('Generating methods documentation...', \danog\MadelineProto\Logger::NOTICE);
foreach ($this->TL->getMethods($this->td)->by_id as $id => $data) {
$method = $data['method'];
$php_method = \str_replace('.', '->', $data['method']);
$phpMethod = StrTools::methodEscape($method);
$type = \str_replace(['<', '>'], ['_of_', ''], $data['type']);
$php_type = \preg_replace('/.*_of_/', '', $type);
if (!isset($this->types[$php_type])) {
@ -74,31 +75,31 @@ trait Methods
$param['name'] = 'message';
$param['type'] = 'DecryptedMessage';
}
if ($param['name'] === 'chat_id' && $data['method'] !== 'messages.discardEncryption' && !isset($this->settings['td'])) {
if ($param['name'] === 'chat_id' && $method !== 'messages.discardEncryption' && !isset($this->settings['td'])) {
$param['type'] = 'InputPeer';
}
$type_or_subtype = isset($param['subtype']) ? 'subtype' : 'type';
$type_or_bare_type = \ctype_upper(Tools::end(\explode('.', $param[$type_or_subtype]))[0]) || \in_array($param[$type_or_subtype], ['!X', 'X', 'bytes', 'true', 'false', 'double', 'string', 'Bool', 'int', 'long', 'int128', 'int256', 'int512', 'int53']) ? 'types' : 'constructors';
$param[$type_or_subtype] = \str_replace(['true', 'false'], ['Bool', 'Bool'], $param[$type_or_subtype]);
$param[$type_or_subtype] = '[' . Tools::markdownEscape($param[$type_or_subtype]) . '](../' . $type_or_bare_type . '/' . $param[$type_or_subtype] . '.md)';
$params .= "'" . $param['name'] . "' => " . (isset($param['subtype']) ? '\\[' . $param[$type_or_subtype] . '\\]' : $param[$type_or_subtype]) . ', ';
$param[$type_or_subtype] = '['.StrTools::markdownEscape($param[$type_or_subtype]).'](../'.$type_or_bare_type.'/'.$param[$type_or_subtype].'.md)';
$params .= "'".$param['name']."' => ".(isset($param['subtype']) ? '\\['.$param[$type_or_subtype].'\\]' : $param[$type_or_subtype]).', ';
}
if (!isset($this->td_descriptions['methods'][$data['method']])) {
$this->addToLang('method_' . $data['method']);
if (\danog\MadelineProto\Lang::$lang['en']['method_' . $data['method']] !== '') {
$this->td_descriptions['methods'][$data['method']]['description'] = \danog\MadelineProto\Lang::$lang['en']['method_' . $data['method']];
if (!isset($this->tdDescriptions['methods'][$method])) {
$this->addToLang('method_'.$method);
if (\danog\MadelineProto\Lang::$lang['en']['method_'.$method] !== '') {
$this->tdDescriptions['methods'][$method]['description'] = \danog\MadelineProto\Lang::$lang['en']['method_'.$method];
}
}
$md_method = '[' . $php_method . '](' . $method . '.md)';
$this->docs_methods[$method] = '$MadelineProto->' . $md_method . '(\\[' . $params . '\\]) === [$' . \str_replace('_', '\\_', $type) . '](../types/' . $php_type . '.md)<a name="' . $method . '"></a>
$md_method = '['.$phpMethod.']('.$method.'.md)';
$this->docs_methods[$method] = '$MadelineProto->'.$md_method.'(\\['.$params.'\\]) === [$'.StrTools::markdownEscape($type).'](../types/'.$php_type.'.md)<a name="'.$method.'"></a>
';
if (isset($this->td_descriptions['methods'][$data['method']])) {
$desc = \Parsedown::instance()->line(\trim(\explode("\n", $this->td_descriptions['methods'][$data['method']]['description'])[0], '.'));
if (isset($this->tdDescriptions['methods'][$method])) {
$desc = \Parsedown::instance()->line(\trim(\explode("\n", $this->tdDescriptions['methods'][$method]['description'])[0], '.'));
$dom = new \DOMDocument();
$dom->loadHTML(\mb_convert_encoding($desc, 'HTML-ENTITIES', 'UTF-8'));
$desc = $dom->textContent;
$this->human_docs_methods[$this->td_descriptions['methods'][$data['method']]['description'] . ': ' . $data['method']] = '* <a href="' . $method . '.html" name="' . $method . '">' . $desc . ': ' . $data['method'] . '</a>
$this->human_docs_methods[$this->tdDescriptions['methods'][$method]['description'].': '.$method] = '* <a href="'.$method.'.html" name="'.$method.'">'.$desc.': '.$method.'</a>
';
}
@ -111,7 +112,7 @@ trait Methods
| Name | Type | Required |
|----------|---------------|----------|
';
if (isset($this->td_descriptions['methods'][$data['method']]) && !empty($data['params'])) {
if (isset($this->tdDescriptions['methods'][$method]) && !empty($data['params'])) {
$table = '### Parameters:
| Name | Type | Description | Required |
@ -129,7 +130,7 @@ trait Methods
$param['name'] = 'message';
$param['type'] = 'DecryptedMessage';
}
if ($param['name'] === 'chat_id' && $data['method'] !== 'messages.discardEncryption' && !isset($this->settings['td'])) {
if ($param['name'] === 'chat_id' && $method !== 'messages.discardEncryption' && !isset($this->settings['td'])) {
$param['type'] = 'InputPeer';
}
if ($param['name'] === 'hash' && $param['type'] === 'int') {
@ -145,45 +146,44 @@ trait Methods
}
$human_ptype = $ptype;
if (\in_array($ptype, ['InputDialogPeer', 'DialogPeer', 'NotifyPeer', 'InputNotifyPeer', 'User', 'InputUser', 'Chat', 'InputChannel', 'Peer', 'InputPeer']) && !isset($this->settings['td'])) {
$human_ptype = 'Username, chat ID, Update, Message or ' . $ptype;
$human_ptype = 'Username, chat ID, Update, Message or '.$ptype;
}
if (\in_array($ptype, ['InputMedia', 'InputPhoto', 'InputDocument']) && !isset($this->settings['td'])) {
$human_ptype = 'MessageMedia, Update, Message or ' . $ptype;
$human_ptype = 'MessageMedia, Update, Message or '.$ptype;
}
if (\in_array($ptype, ['InputMessage']) && !isset($this->settings['td'])) {
$human_ptype = 'Message ID or ' . $ptype;
$human_ptype = 'Message ID or '.$ptype;
}
if (\in_array($ptype, ['InputEncryptedChat']) && !isset($this->settings['td'])) {
$human_ptype = 'Secret chat ID, Update, EncryptedMessage or ' . $ptype;
$human_ptype = 'Secret chat ID, Update, EncryptedMessage or '.$ptype;
}
if (\in_array($ptype, ['InputFile']) && !isset($this->settings['td'])) {
$human_ptype = 'File path or ' . $ptype;
$human_ptype = 'File path or '.$ptype;
}
if (\in_array($ptype, ['InputEncryptedFile']) && !isset($this->settings['td'])) {
$human_ptype = 'File path or ' . $ptype;
$human_ptype = 'File path or '.$ptype;
}
$type_or_bare_type = \ctype_upper(Tools::end(\explode('.', $param[$type_or_subtype]))[0]) || \in_array($param[$type_or_subtype], ['!X', 'X', 'bytes', 'true', 'false', 'double', 'string', 'Bool', 'int', 'long', 'int128', 'int256', 'int512', 'int53']) ? 'types' : 'constructors';
if (!isset($this->td_descriptions['methods'][$data['method']]['params'][$param['name']])) {
$this->addToLang('method_' . $data['method'] . '_param_' . $param['name'] . '_type_' . $param['type']);
if (isset($this->td_descriptions['methods'][$data['method']]['description'])) {
$this->td_descriptions['methods'][$data['method']]['params'][$param['name']] = \danog\MadelineProto\Lang::$lang['en']['method_' . $data['method'] . '_param_' . $param['name'] . '_type_' . $param['type']];
if (!isset($this->tdDescriptions['methods'][$method]['params'][$param['name']])) {
if (isset($this->tdDescriptions['methods'][$method]['description'])) {
$this->tdDescriptions['methods'][$method]['params'][$param['name']] = \danog\MadelineProto\Lang::$lang['en']['method_'.$method.'_param_'.$param['name'].'_type_'.$param['type']] ?? '';
}
}
if (isset($this->td_descriptions['methods'][$data['method']])) {
$table .= '|' . \str_replace('_', '\\_', $param['name']) . '|' . (isset($param['subtype']) ? 'Array of ' : '') . '[' . \str_replace('_', '\\_', $human_ptype) . '](../' . $type_or_bare_type . '/' . $ptype . '.md) | ' . $this->td_descriptions['methods'][$data['method']]['params'][$param['name']] . ' | ' . (isset($param['pow']) || ($id = $this->TL->getConstructors($this->td)->findByPredicate(\lcfirst($param['type']) . 'Empty')) && $id['type'] === $param['type'] || ($id = $this->TL->getConstructors($this->td)->findByPredicate('input' . $param['type'] . 'Empty')) && $id['type'] === $param['type'] ? 'Optional' : 'Yes') . '|';
if (isset($this->tdDescriptions['methods'][$method])) {
$table .= '|'.StrTools::markdownEscape($param['name']).'|'.(isset($param['subtype']) ? 'Array of ' : '').'['.StrTools::markdownEscape($human_ptype).'](../'.$type_or_bare_type.'/'.$ptype.'.md) | '.$this->tdDescriptions['methods'][$method]['params'][$param['name']].' | '.(isset($param['pow']) || ($id = $this->TL->getConstructors($this->td)->findByPredicate(\lcfirst($param['type']).'Empty')) && $id['type'] === $param['type'] || ($id = $this->TL->getConstructors($this->td)->findByPredicate('input'.$param['type'].'Empty')) && $id['type'] === $param['type'] ? 'Optional' : 'Yes').'|';
} else {
$table .= '|' . \str_replace('_', '\\_', $param['name']) . '|' . (isset($param['subtype']) ? 'Array of ' : '') . '[' . \str_replace('_', '\\_', $human_ptype) . '](../' . $type_or_bare_type . '/' . $ptype . '.md) | ' . (isset($param['pow']) || ($id = $this->TL->getConstructors($this->td)->findByPredicate(\lcfirst($param['type']) . 'Empty')) && $id['type'] === $param['type'] || ($id = $this->TL->getConstructors($this->td)->findByPredicate('input' . $param['type'] . 'Empty')) && $id['type'] === $param['type'] ? 'Optional' : 'Yes') . '|';
$table .= '|'.StrTools::markdownEscape($param['name']).'|'.(isset($param['subtype']) ? 'Array of ' : '').'['.StrTools::markdownEscape($human_ptype).'](../'.$type_or_bare_type.'/'.$ptype.'.md) | '.(isset($param['pow']) || ($id = $this->TL->getConstructors($this->td)->findByPredicate(\lcfirst($param['type']).'Empty')) && $id['type'] === $param['type'] || ($id = $this->TL->getConstructors($this->td)->findByPredicate('input'.$param['type'].'Empty')) && $id['type'] === $param['type'] ? 'Optional' : 'Yes').'|';
}
$table .= PHP_EOL;
$pptype = \in_array($ptype, ['string', 'bytes']) ? "'" . $ptype . "'" : $ptype;
$ppptype = \in_array($ptype, ['string']) ? '"' . $ptype . '"' : $ptype;
$ppptype = \in_array($ptype, ['bytes']) ? '{"_": "bytes", "bytes":"base64 encoded ' . $ptype . '"}' : $ppptype;
$params .= "'" . $param['name'] . "' => ";
$params .= (isset($param['subtype']) ? '[' . $pptype . ', ' . $pptype . ']' : $pptype) . ', ';
$json_params .= '"' . $param['name'] . '": ' . (isset($param['subtype']) ? '[' . $ppptype . ']' : $ppptype) . ', ';
$pwr_params .= $param['name'] . ' - Json encoded ' . (isset($param['subtype']) ? ' array of ' . $ptype : $ptype) . "\n\n";
$lua_params .= $param['name'] . '=';
$lua_params .= (isset($param['subtype']) ? '{' . $pptype . '}' : $pptype) . ', ';
$pptype = \in_array($ptype, ['string', 'bytes']) ? "'".$ptype."'" : $ptype;
$ppptype = \in_array($ptype, ['string']) ? '"'.$ptype.'"' : $ptype;
$ppptype = \in_array($ptype, ['bytes']) ? '{"_": "bytes", "bytes":"base64 encoded '.$ptype.'"}' : $ppptype;
$params .= "'".$param['name']."' => ";
$params .= (isset($param['subtype']) ? '['.$pptype.', '.$pptype.']' : $pptype).', ';
$json_params .= '"'.$param['name'].'": '.(isset($param['subtype']) ? '['.$ppptype.']' : $ppptype).', ';
$pwr_params .= $param['name'].' - Json encoded '.(isset($param['subtype']) ? ' array of '.$ptype : $ptype)."\n\n";
$lua_params .= $param['name'].'=';
$lua_params .= (isset($param['subtype']) ? '{'.$pptype.'}' : $pptype).', ';
if ($param['name'] === 'reply_markup') {
$hasreplymarkup = true;
}
@ -200,135 +200,49 @@ trait Methods
$pwr_params = "parseMode - string\n";
}
}
$description = isset($this->td_descriptions['methods'][$data['method']]) ? $this->td_descriptions['methods'][$data['method']]['description'] : $data['method'] . ' parameters, return type and example';
$description = isset($this->tdDescriptions['methods'][$method]) ? $this->tdDescriptions['methods'][$method]['description'] : $method.' parameters, return type and example';
$symFile = \str_replace('.', '_', $method);
$redir = $symFile !== $method ? "\nredirect_from: /API_docs/methods/{$symFile}.html" : '';
$header = '---
title: ' . $data['method'] . '
description: ' . $description . '
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png' . $redir . '
---
# Method: ' . \str_replace('_', '\\_', $data['method']) . '
[Back to methods index](index.md)
';
/*
if (isset(\danog\MadelineProto\MTProto::DISALLOWED_METHODS[$data['method']])) {
$header .= '**'.\danog\MadelineProto\MTProto::DISALLOWED_METHODS[$data['method']]."**\n\n\n\n\n";
file_put_contents('methods/'.$method.'.md', $header);
continue;
}*/
$description = \rtrim(\explode("\n", $description)[0], ':');
$header = $this->template('Method', $method, $description, $redir, StrTools::markdownEscape($method));
if ($this->td) {
$header .= 'YOU CANNOT USE THIS METHOD IN MADELINEPROTO
';
$header .= "YOU CANNOT USE THIS METHOD IN MADELINEPROTO\n\n\n\n\n";
}
$header .= isset($this->td_descriptions['methods'][$data['method']]) ? $this->td_descriptions['methods'][$data['method']]['description'] . PHP_EOL . PHP_EOL : '';
$header .= isset($this->tdDescriptions['methods'][$method]) ? $this->tdDescriptions['methods'][$method]['description'].PHP_EOL.PHP_EOL : '';
$table .= '
';
$return = '### Return type: [' . \str_replace('_', '\\_', $type) . '](../types/' . $php_type . '.md)
$return = '### Return type: ['.StrTools::markdownEscape($type).'](../types/'.$php_type.'.md)
';
$bot = !\in_array($data['method'], $bots);
$bot = !\in_array($method, $bots);
$example = '';
if (!isset($this->settings['td'])) {
$example .= '### Can bots use this method: **' . ($bot ? 'YES' : 'NO') . "**\n\n\n";
$example .= \str_replace('[]', '', '### MadelineProto Example ([now async for huge speed and parallelism!](https://docs.madelineproto.xyz/docs/ASYNC.html)):
```php
if (!file_exists(\'madeline.php\')) {
copy(\'https://phar.madelineproto.xyz/madeline.php\', \'madeline.php\');
}
include \'madeline.php\';
$MadelineProto = new \\danog\\MadelineProto\\API(\'session.madeline\');
$MadelineProto->start();
$' . $type . ' = $MadelineProto->' . $php_method . '([' . $params . ']);
```
Or, if you\'re into Lua:
```lua
' . $type . ' = ' . $data['method'] . '({' . $lua_params . '})
```
');
$example .= '### Can bots use this method: **'.($bot ? 'YES' : 'NO')."**\n\n\n";
$example .= \str_replace('[]', '', $this->template('method-example', $type, $phpMethod, $params, $method, $lua_params));
if ($hasreplymarkup) {
$example .= '
## Usage of reply_markup
You can provide bot API reply_markup objects here.
';
$example .= $this->template('reply_markup');
}
if ($hasmessage) {
$example .= '
## Return value
If the length of the provided message is bigger than 4096, the message will be split in chunks and the method will be called multiple times, with the same parameters (except for the message), and an array of [' . \str_replace('_', '\\_', $type) . '](../types/' . $php_type . '.md) will be returned instead.
';
$example .= $this->template('chunks', StrTools::markdownEscape($type), $php_type);
}
if ($hasentities) {
$example .= '
## Usage of parseMode:
Set parseMode to html to enable HTML parsing of the message.
Set parseMode to Markdown to enable markown AND html parsing of the message.
The following tags are currently supported:
```html
<br>a newline
<b><i>bold works ok, internal tags are stripped</i> </b>
<strong>bold</strong>
<em>italic</em>
<i>italic</i>
<u>underline</u>
<s>strikethrough</s>
<del>strikethrough</del>
<strike>strikethrough</strike>
<code>inline fixed-width code</code>
<pre>pre-formatted fixed-width code block</pre>
<blockquote>pre-formatted fixed-width code block</blockquote>
<a href="https://github.com">URL</a>
<a href="mention:@danogentili">Mention by username</a>
<a href="mention:186785362">Mention by user id</a>
<pre language="json">Pre tags can have a language attribute</pre>
```
You can also use normal markdown, note that to create mentions you must use the `mention:` syntax like in html:
```markdown
[Mention by username](mention:@danogentili)
[Mention by user id](mention:186785362)
```
MadelineProto supports all html entities supported by [html_entity_decode](http://php.net/manual/en/function.html-entity-decode.php).
';
$example .= $this->template('parse_mode');
}
if (isset($new['result'][$data['method']])) {
if (isset($new['result'][$method])) {
$example .= '### Errors
| Code | Type | Description |
|------|----------|---------------|
';
foreach ($new['result'][$data['method']] as $error) {
foreach ($new['result'][$method] as $error) {
[$error, $code] = $error;
$example .= "|{$code}|{$error}|" . $errors['human_result'][$error][0] . '|' . "\n";
$example .= "|{$code}|{$error}|".$errors['human_result'][$error][0].'|'."\n";
}
$example .= "\n\n";
}
}
\file_put_contents('methods/' . $method . '.md', $header . $table . $return . $example);
\file_put_contents('methods/'.$method.'.md', $header.$table.$return.$example);
}
$this->logger->logger('Generating methods index...', \danog\MadelineProto\Logger::NOTICE);
\ksort($this->docs_methods);
@ -339,78 +253,10 @@ MadelineProto supports all html entities supported by [html_entity_decode](http:
$br = $new_namespace != $last_namespace ? '***
<br><br>
' : '';
$value = $br . $value;
$value = $br.$value;
$last_namespace = $new_namespace;
}
\file_put_contents('methods/api_' . $this->index, '---
title: Methods
description: List of methods
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
# Methods
[Back to API documentation index](..)
[Go to the new description-version method index](' . $this->index . ')
$MadelineProto->[logout](https://docs.madelineproto.xyz/logout.html)();
$MadelineProto->[phoneLogin](https://docs.madelineproto.xyz/phoneLogin.html)($number);
$MadelineProto->[completePhoneLogin](https://docs.madelineproto.xyz/completePhoneLogin.html)($code);
$MadelineProto->[complete2FALogin](https://docs.madelineproto.xyz/complete2FAlogin.html)($password);
$MadelineProto->[botLogin](https://docs.madelineproto.xyz/botLogin.html)($token);
$MadelineProto->[getDialogs](https://docs.madelineproto.xyz/getDialogs.html)();
$MadelineProto->[getPwrChat](https://docs.madelineproto.xyz/getPwrChat.html)($id);
$MadelineProto->[getInfo](https://docs.madelineproto.xyz/getInfo.html)($id);
$MadelineProto->[getFullInfo](https://docs.madelineproto.xyz/getFullInfo.html)($id);
$MadelineProto->[getSelf](https://docs.madelineproto.xyz/getSelf.html)();
$MadelineProto->[requestCall](https://docs.madelineproto.xyz/requestCall.html)($id);
$MadelineProto->[requestSecretChat](https://docs.madelineproto.xyz/requestSecretChat.html)($id);
' . \implode('', $this->docs_methods));
\file_put_contents('methods/' . $this->index, '---
title: Methods
description: What do you want to do?
image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
---
# What do you want to do?
[Go back to API documentation index](..)
[Go to the old code-version method index](api_' . $this->index . ')
* [Logout](https://docs.madelineproto.xyz/logout.html)
* [Login](https://docs.madelineproto.xyz/docs/LOGIN.html)
* [Change 2FA password](https://docs.madelineproto.xyz/update2fa.html)
* [Get all chats, broadcast a message to all chats](https://docs.madelineproto.xyz/docs/DIALOGS.html)
* [Get the full participant list of a channel/group/supergroup](https://docs.madelineproto.xyz/getPwrChat.html)
* [Get full info about a user/chat/supergroup/channel](https://docs.madelineproto.xyz/getFullInfo.html)
* [Get info about a user/chat/supergroup/channel](https://docs.madelineproto.xyz/getInfo.html)
* [Get info about the currently logged-in user](https://docs.madelineproto.xyz/getSelf.html)
* [Upload or download files up to 1.5 GB](https://docs.madelineproto.xyz/docs/FILES.html)
* [Make a phone call and play a song](https://docs.madelineproto.xyz/docs/CALLS.html)
* [Create a secret chat bot](https://docs.madelineproto.xyz/docs/SECRET_CHATS.html)
' . \implode('', $this->human_docs_methods));
\file_put_contents('methods/api_'.$this->index, $this->template('methods-api-index', $this->index, \implode('', $this->docs_methods)));
\file_put_contents('methods/'.$this->index, $this->template('methods-index', $this->index, \implode('', $this->human_docs_methods)));
}
}

View File

@ -19,27 +19,64 @@
namespace danog\MadelineProto;
use danog\MadelineProto\Db\DbPropertiesTrait;
/**
* Event handler.
*/
class EventHandler extends InternalDoc
abstract class EventHandler extends InternalDoc
{
use DbPropertiesTrait {
DbPropertiesTrait::initDb as private internalInitDb;
}
/**
* Constructor.
*
* @param APIWrapper|null $MadelineProto MadelineProto instance
* Whether the event handler was started.
*/
public function __construct(?APIWrapper $MadelineProto)
private bool $startedInternal = false;
/**
* API instance.
*/
protected MTProto $API;
public function __construct($API) // BC
{
}
/**
* Internal constructor.
*
* @internal
*
* @param APIWrapper $MadelineProto MadelineProto instance
*
* @return void
*/
public function initInternal(APIWrapper $MadelineProto): void
{
if (!$MadelineProto) {
return;
}
self::link($this, $MadelineProto->getFactory());
$this->API =& $MadelineProto->getAPI();
foreach ($this->API->getMethodNamespaces() as $namespace) {
$this->{$namespace} = $this->exportNamespace($namespace);
}
}
/**
* Start method handler.
*
* @internal
*
* @return \Generator
*/
public function startInternal(): \Generator
{
if ($this->startedInternal) {
return;
}
if (isset(static::$dbProperties)) {
yield from $this->internalInitDb($this->API);
}
if (\method_exists($this, 'onStart')) {
yield $this->onStart();
}
$this->startedInternal = true;
}
/**
* Get peers where to send error reports.
*
@ -49,4 +86,14 @@ class EventHandler extends InternalDoc
{
return [];
}
/**
* Get API instance.
*
* @return MTProto
*/
public function getAPI(): MTProto
{
return $this->API;
}
}

View File

@ -19,10 +19,12 @@
namespace danog\MadelineProto;
/**
* Basic exception.
*/
class Exception extends \Exception
{
use TL\PrettyException;
public static $rollbar = true;
public function __toString()
{
return $this->file === 'MadelineProto' ? $this->message : '\\danog\\MadelineProto\\Exception'.($this->message !== '' ? ': ' : '').$this->message.' in '.$this->file.':'.$this->line.PHP_EOL.\danog\MadelineProto\Magic::$revision.PHP_EOL.'TL Trace:'.PHP_EOL.$this->getTLTrace();
@ -48,9 +50,6 @@ class Exception extends \Exception
if (\strpos($message, 'pg_query') !== false || \strpos($message, 'Undefined variable: ') !== false || \strpos($message, 'socket_write') !== false || \strpos($message, 'socket_read') !== false || \strpos($message, 'Received request to switch to DC ') !== false || \strpos($message, "Couldn't get response") !== false || \strpos($message, 'Re-executing query...') !== false || \strpos($message, "Couldn't find peer by provided") !== false || \strpos($message, 'id.pwrtelegram.xyz') !== false || \strpos($message, 'Please update ') !== false || \strpos($message, 'posix_isatty') !== false) {
return;
}
if (self::$rollbar && \class_exists('\\Rollbar\\Rollbar')) {
\Rollbar\Rollbar::log(\Rollbar\Payload\Level::error(), $this, \debug_backtrace(0));
}
}
/**
* Complain about missing extensions.
@ -68,7 +67,7 @@ class Exception extends \Exception
$additional = 'Follow the instructions @ https://prime.madelineproto.xyz to install it.';
}
$message = 'MadelineProto requires the '.$extensionName.' extension to run. '.$additional;
if (PHP_SAPI !== 'cli') {
if (PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg') {
echo $message.'<br>';
}
$file = 'MadelineProto';
@ -79,8 +78,10 @@ class Exception extends \Exception
* ExceptionErrorHandler.
*
* Error handler
*
* @return false
*/
public static function exceptionErrorHandler($errno = 0, $errstr = null, $errfile = null, $errline = null)
public static function exceptionErrorHandler($errno = 0, $errstr = null, $errfile = null, $errline = null): bool
{
// If error is suppressed with @, don't throw an exception
if (\error_reporting() === 0 || \strpos($errstr, 'headers already sent') || $errfile && (\strpos($errfile, 'vendor/amphp') !== false || \strpos($errfile, 'vendor/league') !== false)) {
@ -92,8 +93,10 @@ class Exception extends \Exception
* ExceptionErrorHandler.
*
* Error handler
*
* @return void
*/
public static function exceptionHandler($exception)
public static function exceptionHandler($exception): void
{
Logger::log($exception, Logger::FATAL_ERROR);
Magic::shutdown(1);

View File

@ -1,7 +1,7 @@
<?php
/**
* Absolute module.
* API module.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@ -19,16 +19,4 @@
namespace danog\MadelineProto;
/**
* Manages serialization of the MadelineProto instance.
*/
class Absolute
{
public static function absolute($file)
{
if (($file[0] ?? '') !== '/' && ($file[1] ?? '') !== ':' && !\in_array(\substr($file, 0, 4), ['phar', 'http'])) {
$file = Magic::getcwd() . '/' . $file;
}
return $file;
}
}
\class_alias(API::class, '\\danog\\MadelineProto\\FastAPI');

View File

@ -33,9 +33,9 @@ interface FileCallbackInterface
/**
* Invoke callback.
*
* @param int $percent Percent
* @param int $speed Speed in mbps
* @param int $time Time
* @param float $percent Percent
* @param float $speed Speed in mbps
* @param float $time Time
*
* @return mixed
*/

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,278 @@
<?php
/**
* API wrapper module.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
*
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\MadelineProto\Ipc;
use Amp\Ipc\Sync\ChannelledSocket;
use Amp\Promise;
use danog\MadelineProto\API;
use danog\MadelineProto\Exception;
use danog\MadelineProto\FileCallbackInterface;
use danog\MadelineProto\Logger;
use danog\MadelineProto\MTProtoTools\FilesLogic;
use danog\MadelineProto\SessionPaths;
use danog\MadelineProto\Tools;
/**
* IPC client.
*/
class Client extends ClientAbstract
{
use \danog\MadelineProto\Wrappers\Start;
use \danog\MadelineProto\Wrappers\Templates;
use FilesLogic;
/**
* Session.
*/
protected SessionPaths $session;
/**
* Constructor function.
*
* @param ChannelledSocket $socket IPC client socket
* @param SessionPaths $session Session paths
* @param Logger $logger Logger
*/
public function __construct(ChannelledSocket $server, SessionPaths $session, Logger $logger)
{
$this->logger = $logger;
$this->server = $server;
$this->session = $session;
Tools::callFork($this->loopInternal());
}
/**
* Run the provided async callable.
*
* @param callable $callback Async callable to run
*
* @return \Generator
*/
public function loop(callable $callback): \Generator
{
return yield $callback();
}
/**
* Unreference.
*
* @return void
*/
public function unreference(): void
{
Tools::wait($this->disconnect());
}
/**
* Stop IPC server instance.
*
* @internal
*/
public function stopIpcServer(): Promise
{
$this->run = false;
return $this->server->send(Server::SHUTDOWN);
}
/**
* Restart IPC server instance.
*
* @internal
*/
public function restartIpcServer(): Promise
{
return $this->server->send(Server::SHUTDOWN);
}
/**
* Whether we're an IPC client instance.
*
* @return boolean
*/
public function isIpc(): bool
{
return true;
}
/**
* Upload file from URL.
*
* @param string|FileCallbackInterface $url URL of file
* @param integer $size Size of file
* @param string $fileName File name
* @param callable $cb Callback (DEPRECATED, use FileCallbackInterface)
* @param boolean $encrypted Whether to encrypt file for secret chats
*
* @return \Generator
*/
public function uploadFromUrl($url, int $size = 0, string $fileName = '', $cb = null, bool $encrypted = false): \Generator
{
if (\is_object($url) && $url instanceof FileCallbackInterface) {
$cb = $url;
$url = $url->getFile();
}
$params = [$url, $size, $fileName, &$cb, $encrypted];
$wrapper = yield from Wrapper::create($params, $this->session, $this->logger);
$wrapper->wrap($cb, false);
return yield from $this->__call('uploadFromUrl', $wrapper);
}
/**
* Upload file from callable.
*
* The callable must accept two parameters: int $offset, int $size
* The callable must return a string with the contest of the file at the specified offset and size.
*
* @param mixed $callable Callable
* @param integer $size File size
* @param string $mime Mime type
* @param string $fileName File name
* @param callable $cb Callback (DEPRECATED, use FileCallbackInterface)
* @param boolean $seekable Whether chunks can be fetched out of order
* @param boolean $encrypted Whether to encrypt file for secret chats
*
* @return \Generator
*
* @psalm-return \Generator<int, Promise<ChannelledSocket>|Promise<mixed>, mixed, mixed>
*/
public function uploadFromCallable(callable $callable, int $size, string $mime, string $fileName = '', $cb = null, bool $seekable = true, bool $encrypted = false): \Generator
{
if (\is_object($callable) && $callable instanceof FileCallbackInterface) {
$cb = $callable;
$callable = $callable->getFile();
}
$params = [&$callable, $size, $mime, $fileName, &$cb, $seekable, $encrypted];
$wrapper = yield from Wrapper::create($params, $this->session, $this->logger);
$wrapper->wrap($cb, false);
$wrapper->wrap($callable, false);
return yield from $this->__call('uploadFromCallable', $wrapper);
}
/**
* Reupload telegram file.
*
* @param mixed $media Telegram file
* @param callable $cb Callback (DEPRECATED, use FileCallbackInterface)
* @param boolean $encrypted Whether to encrypt file for secret chats
*
* @return \Generator
*
* @psalm-return \Generator<int, Promise<ChannelledSocket>|Promise<mixed>, mixed, mixed>
*/
public function uploadFromTgfile($media, $cb = null, bool $encrypted = false): \Generator
{
if (\is_object($media) && $media instanceof FileCallbackInterface) {
$cb = $media;
$media = $media->getFile();
}
$params = [$media, &$cb, $encrypted];
$wrapper = yield from Wrapper::create($params, $this->session, $this->logger);
$wrapper->wrap($cb, false);
return yield from $this->__call('uploadFromTgfile', $wrapper);
}
/**
* Download file to directory.
*
* @param mixed $messageMedia File to download
* @param string|FileCallbackInterface $dir Directory where to download the file
* @param callable $cb Callback (DEPRECATED, use FileCallbackInterface)
*
* @return \Generator Downloaded file path
*
* @psalm-return \Generator<int, Promise<ChannelledSocket>|Promise<mixed>, mixed, mixed>
*/
public function downloadToDir($messageMedia, $dir, $cb = null): \Generator
{
if (\is_object($dir) && $dir instanceof FileCallbackInterface) {
$cb = $dir;
$dir = $dir->getFile();
}
$params = [$messageMedia, $dir, &$cb];
$wrapper = yield from Wrapper::create($params, $this->session, $this->logger);
$wrapper->wrap($cb, false);
return yield from $this->__call('downloadToDir', $wrapper);
}
/**
* Download file.
*
* @param mixed $messageMedia File to download
* @param string|FileCallbackInterface $file Downloaded file path
* @param callable $cb Callback (DEPRECATED, use FileCallbackInterface)
*
* @return \Generator Downloaded file path
*
* @psalm-return \Generator<int, Promise<ChannelledSocket>|Promise<mixed>, mixed, mixed>
*/
public function downloadToFile($messageMedia, $file, $cb = null): \Generator
{
if (\is_object($file) && $file instanceof FileCallbackInterface) {
$cb = $file;
$file = $file->getFile();
}
$params = [$messageMedia, $file, &$cb];
$wrapper = yield from Wrapper::create($params, $this->session, $this->logger);
$wrapper->wrap($cb, false);
return yield from $this->__call('downloadToFile', $wrapper);
}
/**
* Download file to callable.
* The callable must accept two parameters: string $payload, int $offset
* The callable will be called (possibly out of order, depending on the value of $seekable).
* The callable should return the number of written bytes.
*
* @param mixed $messageMedia File to download
* @param callable|FileCallbackInterface $callable Chunk callback
* @param callable $cb Status callback (DEPRECATED, use FileCallbackInterface)
* @param bool $seekable Whether the callable can be called out of order
* @param int $offset Offset where to start downloading
* @param int $end Offset where to stop downloading (inclusive)
* @param int $part_size Size of each chunk
*
* @return \Generator
*
* @psalm-return \Generator<int, Promise<ChannelledSocket>|Promise<mixed>, mixed, mixed>
*/
public function downloadToCallable($messageMedia, callable $callable, $cb = null, bool $seekable = true, int $offset = 0, int $end = -1, int $part_size = null): \Generator
{
$messageMedia = (yield from $this->getDownloadInfo($messageMedia));
if (\is_object($callable) && $callable instanceof FileCallbackInterface) {
$cb = $callable;
$callable = $callable->getFile();
}
$params = [$messageMedia, &$callable, &$cb, $seekable, $offset, $end, $part_size, ];
$wrapper = yield from Wrapper::create($params, $this->session, $this->logger);
$wrapper->wrap($callable, false);
$wrapper->wrap($cb, false);
return yield from $this->__call('downloadToCallable', $wrapper);
}
/**
* Placeholder.
*
* @param mixed ...$params Params
*
* @return void
*/
public function setEventHandler(...$params): void
{
throw new Exception("Can't use ".__FUNCTION__." in an IPC client instance, please use a full ".API::class." instance, instead!");
}
/**
* Placeholder.
*
* @param mixed ...$params Params
*
* @return void
*/
public function getEventHandler(...$params): void
{
throw new Exception("Can't use ".__FUNCTION__." in an IPC client instance, please use a full ".API::class." instance, instead!");
}
}

View File

@ -0,0 +1,148 @@
<?php
/**
* API wrapper module.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
*
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\MadelineProto\Ipc;
use Amp\Deferred;
use Amp\Ipc\Sync\ChannelledSocket;
use Amp\Promise;
use danog\MadelineProto\Logger;
use function Amp\Ipc\connect;
/**
* IPC client.
*/
abstract class ClientAbstract
{
/**
* IPC server socket.
*/
protected ChannelledSocket $server;
/**
* Requests promise array.
*
* @var Deferred[]
*/
private array $requests = [];
/**
* Wrappers array.
*
* @var Wrapper[]
*/
private array $wrappers = [];
/**
* Whether to run loop.
*/
protected bool $run = true;
/**
* Logger instance.
*/
public Logger $logger;
protected function __construct()
{
}
/**
* Logger.
*
* @param string $param Parameter
* @param int $level Logging level
* @param string $file File where the message originated
*
* @return void
*/
public function logger($param, int $level = Logger::NOTICE, string $file = ''): void
{
if ($file === null) {
$file = \basename(\debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]['file'], '.php');
}
isset($this->logger) ? $this->logger->logger($param, $level, $file) : Logger::$default->logger($param, $level, $file);
}
/**
* Main loop.
*
* @return \Generator
*/
protected function loopInternal(): \Generator
{
do {
while ($payload = yield $this->server->receive()) {
[$id, $payload] = $payload;
if (!isset($this->requests[$id])) {
Logger::log("Got response for non-existing ID $id!");
} else {
$promise = $this->requests[$id];
unset($this->requests[$id]);
if (isset($this->wrappers[$id])) {
yield $this->wrappers[$id]->disconnect();
unset($this->wrappers[$id]);
}
if ($payload instanceof ExitFailure) {
$promise->fail($payload->getException());
} else {
$promise->resolve($payload);
}
unset($promise);
}
}
if ($this->run) {
$this->logger("Reconnecting to IPC server!");
yield $this->server->disconnect();
if ($this instanceof Client) {
Server::startMe($this->session);
$this->server = yield connect($this->session->getIpcPath());
} else {
return;
}
}
} while ($this->run);
}
/**
* Disconnect cleanly from main instance.
*
* @return \Generator
*
* @psalm-return \Generator<int, Promise, mixed, void>
*/
public function disconnect(): \Generator
{
$this->run = false;
yield $this->server->disconnect();
foreach ($this->wrappers as $w) {
yield from $w->disconnect();
}
}
/**
* Call function.
*
* @param string|int $function Function name
* @param array|Wrapper $arguments Arguments
*
* @return \Generator
*/
public function __call($function, $arguments): \Generator
{
$this->requests []= $deferred = new Deferred;
if ($arguments instanceof Wrapper) {
$this->wrappers[\count($this->requests) - 1] = $arguments;
}
yield $this->server->send([$function, $arguments]);
return yield $deferred->promise();
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace danog\MadelineProto\Ipc;
use danog\MadelineProto\RPCErrorException;
use function Amp\Parallel\Sync\flattenThrowableBacktrace;
final class ExitFailure
{
/** @var string */
private $type;
/** @var string */
private $message;
/** @var int|string */
private $code;
/** @var string[] */
private $trace;
/** @var string */
private $tlTrace;
/** @var self|null */
private $previous;
/** @var string|null */
private $localized;
public function __construct(\Throwable $exception)
{
$this->type = \get_class($exception);
$this->message = $exception->getMessage();
$this->code = $exception->getCode();
$this->trace = flattenThrowableBacktrace($exception);
if (\method_exists($exception, 'getTLTrace')) {
$this->tlTrace = $exception->getTLTrace();
}
if ($exception instanceof RPCErrorException) {
$this->localized = $exception->getLocalization();
}
if ($previous = $exception->getPrevious()) {
$this->previous = new self($previous);
}
}
public function getException(): object
{
$previous = $this->previous ? $this->previous->getException() : null;
$exception = new $this->type($this->message, $this->code, $previous);
if ($this->tlTrace) {
$exception->setTLTrace($this->tlTrace);
}
if ($this->localized) {
$exception->setLocalization($this->localized);
}
return $exception;
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace danog\MadelineProto\Ipc;
/**
* IPC state class.
*/
final class IpcState
{
/**
* Startup time.
*/
private float $startupTime;
/**
* Startup ID.
*/
private int $startupId;
/**
* Exception.
*/
private ?ExitFailure $exception;
/**
* Construct.
*
* @param integer $startupId
* @param \Throwable $exception
*/
public function __construct(int $startupId, \Throwable $exception = null)
{
$this->startupTime = \microtime(true);
$this->startupId = $startupId;
$this->exception = $exception ? new ExitFailure($exception) : null;
}
/**
* Get startup time.
*
* @return float
*/
public function getStartupTime(): float
{
return $this->startupTime;
}
/**
* Get startup ID.
*
* @return int
*/
public function getStartupId(): int
{
return $this->startupId;
}
/**
* Get exception.
*
* @return ?\Throwable
*/
public function getException(): ?\Throwable
{
return $this->exception ? $this->exception->getException() : null;
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace danog\MadelineProto\Ipc\Runner;
use danog\MadelineProto\Logger;
final class ProcessRunner extends RunnerAbstract
{
/** @var string|null Cached path to located PHP binary. */
private static $binaryPath;
/**
* Resources.
*/
private static array $resources = [];
/**
* Runner.
*
* @param string $session Session path
*
* @return void
*/
public static function start(string $session, int $startupId): void
{
if (\PHP_SAPI === "cli") {
$binary = \PHP_BINARY;
} else {
$binary = self::$binaryPath ?? self::locateBinary();
}
$options = [
"html_errors" => "0",
"display_errors" => "0",
"log_errors" => "1",
];
$runner = self::getScriptPath();
if (\strtolower(\substr(PHP_OS, 0, 3)) === 'win') {
$binary = \str_replace("Program Files", "PROGRA~1", $binary);
// Pray there are no spaces in the name, escapeshellarg would help but windows doesn't want quotes in the program name
} else {
$binary = \escapeshellarg($binary);
}
$command = \implode(" ", [
$binary,
self::formatOptions($options),
$runner,
'madeline-ipc',
\escapeshellarg($session),
$startupId
]);
Logger::log("Starting process with $command");
self::$resources []= \proc_open($command, [], $foo);
}
private static function locateBinary(): string
{
$executable = \strncasecmp(\PHP_OS, "WIN", 3) === 0 ? "php.exe" : "php";
$paths = \array_filter(\explode(\PATH_SEPARATOR, \getenv("PATH")));
$paths[] = \PHP_BINDIR;
$paths = \array_unique($paths);
foreach ($paths as $path) {
$path .= \DIRECTORY_SEPARATOR.$executable;
if (\is_executable($path)) {
return self::$binaryPath = $path;
}
}
throw new \Error("Could not locate PHP executable binary");
}
private static function formatOptions(array $options): string
{
$result = [];
foreach ($options as $option => $value) {
$result[] = \sprintf("-d%s=%s", $option, $value);
}
return \implode(" ", $result);
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace danog\MadelineProto\Ipc\Runner;
abstract class RunnerAbstract
{
const SCRIPT_PATH = __DIR__."/entry.php";
/** @var string|null External version of SCRIPT_PATH if inside a PHAR. */
protected static $pharScriptPath;
/** @var string|null PHAR path with a '.phar' extension. */
protected static $pharCopy;
protected static function getScriptPath(string $alternateTmpDir = '')
{
/**
* If using madeline.php, simply return madeline.php path.
*/
if (\defined('MADELINE_PHP')) {
return \MADELINE_PHP;
}
// Write process runner to external file if inside a PHAR different from madeline.phar,
// because PHP can't open files inside a PHAR directly except for the stub.
if (\strpos(self::SCRIPT_PATH, "phar://") === 0) {
$alternateTmpDir = $alternateTmpDir ?: \sys_get_temp_dir();
if (self::$pharScriptPath) {
$scriptPath = self::$pharScriptPath;
} else {
$path = \dirname(self::SCRIPT_PATH);
if (\substr(\Phar::running(false), -5) !== ".phar") {
self::$pharCopy = $alternateTmpDir."/phar-".\bin2hex(\random_bytes(10)).".phar";
\copy(\Phar::running(false), self::$pharCopy);
\register_shutdown_function(static function (): void {
@\unlink(self::$pharCopy);
});
$path = "phar://".self::$pharCopy."/".\substr($path, \strlen(\Phar::running(true)));
}
$contents = \file_get_contents(self::SCRIPT_PATH);
$contents = \str_replace("__DIR__", \var_export($path, true), $contents);
$suffix = \bin2hex(\random_bytes(10));
self::$pharScriptPath = $scriptPath = $alternateTmpDir."/madeline-ipc-".$suffix.".php";
\file_put_contents($scriptPath, $contents);
\register_shutdown_function(static function (): void {
@\unlink(self::$pharScriptPath);
});
}
} else {
$scriptPath = self::SCRIPT_PATH;
}
return $scriptPath;
}
/**
* Runner.
*
* @param string $session Session path
* @param int $startup ID
*
* @return void
*/
abstract public static function start(string $session, int $startupId): void;
}

View File

@ -0,0 +1,96 @@
<?php
namespace danog\MadelineProto\Ipc\Runner;
use Amp\Parallel\Context\ContextException;
use danog\MadelineProto\Magic;
final class WebRunner extends RunnerAbstract
{
/** @var string|null Cached path to the runner script. */
private static $runPath;
/**
* Resources.
*/
private static array $resources = [];
/**
* Start.
*
* @param string $session Session path
*
* @return void
*/
public static function start(string $session, int $startupId): void
{
if (!isset($_SERVER['SERVER_NAME'])) {
return;
}
if (!self::$runPath) {
$uri = \parse_url('tcp://'.$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI'], PHP_URL_PATH);
if (\substr($uri, -1) === '/') { // http://example.com/path/ (assumed index.php)
$uri .= 'index'; // Add fake file name
}
$uri = \str_replace('//', '/', $uri);
$rootDir = \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$rootDir = \end($rootDir)['file'] ?? '';
if (!$rootDir) {
throw new ContextException('Could not get entry file!');
}
$rootDir = \dirname($rootDir).DIRECTORY_SEPARATOR;
$uriDir = \dirname($uri);
if ($uriDir !== '/' && $uriDir !== '\\') {
$uriDir .= DIRECTORY_SEPARATOR;
}
if (\substr($rootDir, -\strlen($uriDir)) !== $uriDir) {
throw new ContextException("Mismatch between absolute root dir ($rootDir) and URI dir ($uriDir)");
}
// Absolute root of (presumably) readable document root
$absoluteRootDir = \substr($rootDir, 0, \strlen($rootDir)-\strlen($uriDir)).DIRECTORY_SEPARATOR;
$runPath = self::getScriptPath($absoluteRootDir);
if (\substr($runPath, 0, \strlen($absoluteRootDir)) === $absoluteRootDir) { // Process runner is within readable document root
self::$runPath = \substr($runPath, \strlen($absoluteRootDir)-1);
} else {
$contents = \file_get_contents(self::SCRIPT_PATH);
$contents = \str_replace("__DIR__", \var_export($absoluteRootDir, true), $contents);
$suffix = \bin2hex(\random_bytes(10));
$runPath = $absoluteRootDir."/madeline-ipc-".$suffix.".php";
\file_put_contents($runPath, $contents);
self::$runPath = \substr($runPath, \strlen($absoluteRootDir)-1);
\register_shutdown_function(static function () use ($runPath): void {
@\unlink($runPath);
});
}
self::$runPath = \str_replace(DIRECTORY_SEPARATOR, '/', self::$runPath);
self::$runPath = \str_replace('//', '/', self::$runPath);
}
$params = [
'argv' => ['madeline-ipc', $session, $startupId],
'cwd' => Magic::getcwd()
];
$params = \http_build_query($params);
$address = ($_SERVER['HTTPS'] ?? false ? 'tls' : 'tcp').'://'.$_SERVER['SERVER_NAME'];
$port = $_SERVER['SERVER_PORT'];
$uri = self::$runPath.'?'.$params;
$payload = "GET $uri HTTP/1.1\r\nHost: ${_SERVER['SERVER_NAME']}\r\n\r\n";
// We don't care for results or timeouts here, PHP doesn't count IOwait time as execution time anyway
// Technically should use amphp/socket, but I guess it's OK to not introduce another dependency just for a socket that will be used once.
\fwrite($res = \fsockopen($address, $port), $payload);
self::$resources []= $res;
}
}

View File

@ -0,0 +1,130 @@
<?php
/**
* IPC server entry module.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
*
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
use danog\MadelineProto\API;
use danog\MadelineProto\Ipc\IpcState;
use danog\MadelineProto\Ipc\Server;
use danog\MadelineProto\Logger;
use danog\MadelineProto\Magic;
use danog\MadelineProto\SessionPaths;
use danog\MadelineProto\Settings\Ipc;
use danog\MadelineProto\Tools;
(static function (): void {
if (\defined('MADELINE_ENTRY')) {
// Already called
return;
}
\define('MADELINE_ENTRY', 1);
if (!\defined('MADELINE_WORKER_TYPE')) {
if (\count(\debug_backtrace(0)) !== 1) {
// We're not being included directly
return;
}
$arguments = [];
if (isset($GLOBALS['argv']) && !empty($GLOBALS['argv'])) {
$arguments = \array_slice($GLOBALS['argv'], 1);
} elseif (isset($_GET['argv']) && !empty($_GET['argv'])) {
$arguments = $_GET['argv'];
}
if (\count($arguments) < 2) {
\trigger_error("Not enough arguments!", E_USER_ERROR);
exit(1);
}
\define('MADELINE_WORKER_TYPE', \array_shift($arguments));
\define('MADELINE_WORKER_ARGS', $arguments);
}
if (\defined('SIGHUP')) {
try {
\pcntl_signal(SIGHUP, fn () => null);
} catch (\Throwable $e) {
}
}
if (!\class_exists(API::class)) {
$paths = [
\dirname(__DIR__, 7)."/autoload.php",
\dirname(__DIR__, 5)."/vendor/autoload.php",
];
foreach ($paths as $path) {
if (\file_exists($path)) {
$autoloadPath = $path;
break;
}
}
if (!isset($autoloadPath)) {
\trigger_error("Could not locate autoload.php in any of the following files: ".\implode(", ", $paths), E_USER_ERROR);
exit(1);
}
include $autoloadPath;
}
if (\MADELINE_WORKER_TYPE === 'madeline-ipc') {
$ipcPath = \MADELINE_WORKER_ARGS[0];
if (!\file_exists($ipcPath)) {
\trigger_error("IPC session $ipcPath does not exist!", E_USER_ERROR);
exit(1);
}
if (\function_exists('cli_set_process_title')) {
@\cli_set_process_title("MadelineProto worker $ipcPath");
}
if (\function_exists('posix_setsid')) {
@\posix_setsid();
}
if (isset($_GET['cwd'])) {
@\chdir($_GET['cwd']);
}
\define('MADELINE_WORKER', 1);
$runnerId = \MADELINE_WORKER_ARGS[1];
$session = new SessionPaths($ipcPath);
try {
Magic::classExists();
Magic::$script_cwd = $_GET['cwd'] ?? Magic::getcwd();
$API = new API($ipcPath, (new Ipc)->setSlow(true));
$API->init();
$API->initSelfRestart();
while (true) {
try {
Tools::wait($session->storeIpcState(new IpcState($runnerId)));
Tools::wait(Server::waitShutdown());
return;
} catch (\Throwable $e) {
Logger::log((string) $e, Logger::FATAL_ERROR);
Tools::wait($API->report("Surfaced: $e"));
}
}
} catch (\Throwable $e) {
Logger::log("$e", Logger::FATAL_ERROR);
Logger::log("Got exception in IPC server, exiting...", Logger::FATAL_ERROR);
$ipc = Tools::wait($session->getIpcState());
if (!($ipc && $ipc->getStartupId() === $runnerId && !$ipc->getException())) {
Logger::log("Reporting error!");
Tools::wait($session->storeIpcState(new IpcState($runnerId, $e)));
Logger::log("Reported error!");
} else {
Logger::log("Not reporting error!");
}
}
}
})();

View File

@ -0,0 +1,247 @@
<?php
/**
* API wrapper module.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
*
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\MadelineProto\Ipc;
use Amp\Deferred;
use Amp\Ipc\IpcServer;
use Amp\Ipc\Sync\ChannelledSocket;
use Amp\Promise;
use danog\Loop\SignalLoop;
use danog\MadelineProto\Ipc\Runner\ProcessRunner;
use danog\MadelineProto\Ipc\Runner\WebRunner;
use danog\MadelineProto\Logger;
use danog\MadelineProto\Loop\InternalLoop;
use danog\MadelineProto\SessionPaths;
use danog\MadelineProto\Settings\Ipc;
use danog\MadelineProto\Tools;
/**
* IPC server.
*/
class Server extends SignalLoop
{
use InternalLoop;
/**
* Shutdown server.
*/
const SHUTDOWN = 0;
/**
* Boolean to shut down worker, if started.
*/
private static bool $shutdown = false;
/**
* Deferred to shut down worker, if started.
*/
private static ?Deferred $shutdownDeferred = null;
/**
* IPC server.
*/
protected IpcServer $server;
/**
* Callback IPC server.
*/
private ServerCallback $callback;
/**
* IPC settings.
*/
private Ipc $settings;
/**
* Set IPC path.
*
* @param SessionPaths $session Session
*
* @return void
*/
public function setIpcPath(SessionPaths $session): void
{
self::$shutdownDeferred = new Deferred;
$this->server = new IpcServer($session->getIpcPath());
$this->callback = new ServerCallback($this->API);
$this->callback->setIpcPath($session);
}
public function start(): bool
{
return $this instanceof ServerCallback ? parent::start() : $this->callback->start() && parent::start();
}
/**
* Start IPC server in background.
*
* @param SessionPaths $session Session path
*
* @return Promise
*/
public static function startMe(SessionPaths $session): Promise
{
$id = Tools::randomInt(2000000000);
try {
Logger::log("Starting IPC server $session (process)");
ProcessRunner::start($session, $id);
WebRunner::start($session, $id);
return Tools::call(self::monitor($session, $id));
} catch (\Throwable $e) {
Logger::log($e);
}
try {
Logger::log("Starting IPC server $session (web)");
WebRunner::start($session, $id);
} catch (\Throwable $e) {
Logger::log($e);
}
return Tools::call(self::monitor($session, $id));
}
/**
* Monitor session.
*
* @param SessionPaths $session
* @param int $id
*
* @return \Generator
*/
private static function monitor(SessionPaths $session, int $id): \Generator
{
while (true) {
$state = yield $session->getIpcState();
if ($state && $state->getStartupId() === $id) {
if ($e = $state->getException()) {
Logger::log("IPC server got exception $e");
return $e;
}
Logger::log("IPC server started successfully!");
return true;
}
yield Tools::sleep(1);
}
return false;
}
/**
* Wait for shutdown.
*
* @return Promise
*/
public static function waitShutdown(): Promise
{
return self::$shutdownDeferred->promise();
}
/**
* Main loop.
*
* @return \Generator
*/
public function loop(): \Generator
{
while ($socket = yield $this->waitSignal($this->server->accept())) {
Tools::callFork($this->clientLoop($socket));
}
$this->server->close();
if (isset($this->callback)) {
$this->callback->signal(null);
}
}
/**
* Client handler loop.
*
* @param ChannelledSocket $socket Client
*
* @return \Generator|Promise
*/
protected function clientLoop(ChannelledSocket $socket)
{
$this->API->logger("Accepted IPC client connection!");
$id = 0;
$payload = null;
try {
while ($payload = yield $socket->receive()) {
Tools::callFork($this->clientRequest($socket, $id++, $payload));
}
} finally {
yield $socket->disconnect();
if ($payload === self::SHUTDOWN) {
$this->signal(null);
if (self::$shutdownDeferred) {
self::$shutdownDeferred->resolve();
}
}
}
}
/**
* Handle client request.
*
* @param ChannelledSocket $socket Socket
* @param integer $id Request ID
* @param array|Wrapper $payload Payload
*
* @return \Generator
*/
private function clientRequest(ChannelledSocket $socket, int $id, $payload): \Generator
{
try {
if ($payload[1] instanceof Wrapper) {
$wrapper = $payload[1];
$payload[1] = $this->callback->unwrap($wrapper);
}
$result = $this->API->{$payload[0]}(...$payload[1]);
$result = $result instanceof \Generator
? yield from $result
: ($result instanceof Promise
? yield $result
: $result);
} catch (\Throwable $e) {
$this->API->logger("Got error while calling IPC method: $e", Logger::ERROR);
$result = new ExitFailure($e);
} finally {
if (isset($wrapper)) {
yield $wrapper->disconnect();
}
}
try {
yield $socket->send([$id, $result]);
} catch (\Throwable $e) {
$this->API->logger("Got error while trying to send result of ${payload[0]}: $e", Logger::ERROR);
try {
yield $socket->send([$id, new ExitFailure($e)]);
} catch (\Throwable $e) {
$this->API->logger("Got error while trying to send error of error of ${payload[0]}: $e", Logger::ERROR);
}
}
}
/**
* Get the name of the loop.
*
* @return string
*/
public function __toString(): string
{
return "IPC server";
}
/**
* Set IPC settings.
*
* @param Ipc $settings IPC settings
*
* @return self
*/
public function setSettings(Ipc $settings): self
{
$this->settings = $settings;
return $this;
}
}

View File

@ -0,0 +1,97 @@
<?php
/**
* IPC callback server.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
*
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\MadelineProto\Ipc;
use Amp\Ipc\IpcServer;
use Amp\Ipc\Sync\ChannelledSocket;
use Amp\Loop;
use Amp\Promise;
use danog\MadelineProto\Exception;
use danog\MadelineProto\SessionPaths;
/**
* IPC callback server.
*/
class ServerCallback extends Server
{
/**
* Timeout watcher list, indexed by socket ID.
*
* @var array<int, string>
*/
private $watcherList = [];
/**
* Timeout watcher list, indexed by socket ID.
*
* @var array<int, ChannelledSocket>
*/
private $socketList = [];
/**
* Counter.
*/
private int $id = 0;
/**
* Set IPC path.
*
* @param SessionPaths $session Session
*
* @return void
*/
public function setIpcPath(SessionPaths $session): void
{
$this->server = new IpcServer($session->getIpcCallbackPath());
}
/**
* Client handler loop.
*
* @param ChannelledSocket $socket Client
*
* @return Promise
*/
protected function clientLoop(ChannelledSocket $socket)
{
$id = $this->id++;
$this->API->logger("Accepted IPC callback connection, assigning ID $id!");
$this->socketList[$id] = $socket;
$this->watcherList[$id] = Loop::delay(30*1000, function () use ($id) {
unset($this->watcherList[$id], $this->socketList[$id]);
});
return $socket->send($id);
}
/**
* Unwrap value.
*
* @param Wrapper $wrapper
* @return mixed
*/
protected function unwrap(Wrapper $wrapper)
{
$id = $wrapper->getRemoteId();
if (!isset($this->socketList[$id])) {
throw new Exception("IPC timeout, could not find callback socket!");
}
$socket = $this->socketList[$id];
Loop::cancel($this->watcherList[$id]);
unset($this->watcherList[$id], $this->socketList[$id]);
return $wrapper->unwrap($socket);
}
}

View File

@ -0,0 +1,214 @@
<?php
namespace danog\MadelineProto\Ipc;
use Amp\ByteStream\InputStream as ByteStreamInputStream;
use Amp\ByteStream\OutputStream as ByteStreamOutputStream;
use Amp\Ipc\Sync\ChannelledSocket;
use Amp\Parallel\Sync\ExitFailure;
use Amp\Promise;
use danog\MadelineProto\Ipc\Wrapper\InputStream;
use danog\MadelineProto\Ipc\Wrapper\Obj;
use danog\MadelineProto\Ipc\Wrapper\OutputStream;
use danog\MadelineProto\Logger;
use danog\MadelineProto\SessionPaths;
use danog\MadelineProto\Tools;
use function Amp\Ipc\connect;
/**
* Callback payload wrapper.
*/
class Wrapper extends ClientAbstract
{
/**
* Payload data.
*
* @var mixed
*/
private $data;
/**
* Callbacks.
*
* @var callable[]
*/
private array $callbacks = [];
/**
* Callbacks IDs.
*
* @var (int|array{0: class-string<Obj>, array<string, int>})[]
*/
private array $callbackIds = [];
/**
* Callback ID.
*/
private int $id = 0;
/**
* Remote socket ID.
*/
private int $remoteId = 0;
/**
* Constructor.
*
* @param mixed $data Payload data
* @param SessionPaths $ipc IPC URI
*
* @return \Generator
* @psalm-return \Generator<int, Promise<ChannelledSocket>|Promise<mixed>, mixed, Wrapper>
*/
public static function create(&$data, SessionPaths $session, Logger $logger): \Generator
{
$instance = new self;
$instance->data = &$data;
$instance->logger = $logger;
$instance->run = false;
$logger->logger("Connecting to callback IPC server...");
$instance->server = yield connect($session->getIpcCallbackPath());
$logger->logger("Connected to callback IPC server!");
$instance->remoteId = yield $instance->server->receive();
$logger->logger("Got ID {$instance->remoteId} from callback IPC server!");
Tools::callFork($instance->receiverLoop());
return $instance;
}
/**
* Serialization function.
*
* @return array
*/
public function __sleep(): array
{
return ['data', 'callbackIds', 'remoteId'];
}
/**
* Wrap a certain callback object.
*
* @param object|callable $callback Callback to wrap
* @param bool $wrapObjects Whether to wrap object methods, too
*
* @param-out int $callback Callback ID
*
* @return void
*/
public function wrap(&$callback, bool $wrapObjects = true): void
{
if (\is_object($callback) && $wrapObjects) {
$ids = [];
foreach (\get_class_methods($callback) as $method) {
$id = $this->id++;
$this->callbacks[$id] = [$callback, $method];
$ids[$method] = $id;
}
$class = Obj::class;
if ($callback instanceof ByteStreamInputStream) {
$class = InputStream::class;
} elseif ($callback instanceof ByteStreamOutputStream) {
$class = OutputStream::class;
}
if ($class !== Obj::class && \method_exists($callback, 'seek')) {
$class = "Seekable$class";
}
$callback = [$class, $ids]; // Will be re-filled later
$this->callbackIds[] = &$callback;
} elseif (\is_callable($callback)) {
$id = $this->id++;
$this->callbacks[$id] = self::copy($callback);
$callback = $id;
$this->callbackIds[] = &$callback;
}
}
/**
* Get copy of data.
*
* @param mixed $data
* @return mixed
*/
private static function copy($data)
{
return $data;
}
/**
* Receiver loop.
*
* @return \Generator
*/
private function receiverLoop(): \Generator
{
$id = 0;
$payload = null;
try {
while ($payload = yield $this->server->receive()) {
Tools::callFork($this->clientRequest($id++, $payload));
}
} finally {
yield $this->server->disconnect();
}
}
/**
* Handle client request.
*
* @param integer $id Request ID
* @param array $payload Payload
*
* @return \Generator
*/
private function clientRequest(int $id, $payload): \Generator
{
try {
$result = $this->callbacks[$payload[0]](...$payload[1]);
$result = $result instanceof \Generator ? yield from $result : yield $result;
} catch (\Throwable $e) {
$this->logger->logger("Got error while calling reverse IPC method: $e", Logger::ERROR);
$result = new ExitFailure($e);
}
try {
yield $this->server->send([$id, $result]);
} catch (\Throwable $e) {
$this->logger->logger("Got error while trying to send result of reverse method: $e", Logger::ERROR);
try {
yield $this->server->send([$id, new ExitFailure($e)]);
} catch (\Throwable $e) {
$this->logger->logger("Got error while trying to send error of error of reverse method: $e", Logger::ERROR);
}
}
}
/**
* Get remote socket ID.
*
* @internal
*
* @return int
*/
public function getRemoteId(): int
{
return $this->remoteId;
}
/**
* Set socket and unwrap data.
*
* @param ChannelledSocket $server Socket.
*
* @internal
*
* @return mixed
*/
public function unwrap(ChannelledSocket $server)
{
$this->server = $server;
Tools::callFork($this->loopInternal());
foreach ($this->callbackIds as &$id) {
if (\is_int($id)) {
$id = fn (...$args): \Generator => $this->__call($id, $args);
} else {
[$class, $ids] = $id;
$id = new $class($this, $ids);
}
}
return $this->data;
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace danog\MadelineProto\Ipc\Wrapper;
use danog\MadelineProto\FileCallbackInterface;
class FileCallback extends Obj implements FileCallbackInterface
{
/**
* Get file.
*
* @return mixed
*/
public function getFile()
{
return $this->__call('getFile');
}
/**
* Invoke callback.
*
* @param float $percent Percent
* @param float $speed Speed in mbps
* @param float $time Time
*
* @psalm-suppress MethodSignatureMismatch
*
* @return mixed
*/
public function __invoke($percent, $speed, $time)
{
return $this->__call('__invoke', [$percent, $speed, $time]);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace danog\MadelineProto\Ipc\Wrapper;
use Amp\ByteStream\InputStream as AmpInputStream;
use Amp\ByteStream\PendingReadError;
use Amp\Promise;
use danog\MadelineProto\Tools;
class InputStream extends Obj implements AmpInputStream
{
/**
* Reads data from the stream.
*
* @return Promise Resolves with a string when new data is available or `null` if the stream has closed.
*
* @psalm-return Promise<string|null>
*
* @throws PendingReadError Thrown if another read operation is still pending.
*/
public function read(): Promise
{
return Tools::call($this->__call('read'));
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace danog\MadelineProto\Ipc\Wrapper;
use danog\MadelineProto\Ipc\Wrapper;
/**
* Generic callback wrapper object.
*/
class Obj
{
/**
* Method list.
*
* @var array<string, int>
*/
private array $methods = [];
/**
* Wrapper.
*/
private Wrapper $wrapper;
/**
* Constructor.
*
* @param Wrapper $wrapper
* @param array $methods
*/
public function __construct(Wrapper $wrapper, array $methods)
{
$this->wrapper = $wrapper;
$this->methods = $methods;
}
/**
* Call method.
*
* @param string $name
* @param array $arguments
*
* @return \Generator<mixed, mixed, mixed, mixed>
*/
public function __call(string $name, array $arguments = []): \Generator
{
return $this->wrapper->__call($this->methods[$name], $arguments);
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace danog\MadelineProto\Ipc\Wrapper;
use Amp\ByteStream\ClosedException;
use Amp\ByteStream\OutputStream as AmpOutputStream;
use Amp\ByteStream\StreamException;
use Amp\Promise;
use danog\MadelineProto\Tools;
class OutputStream extends Obj implements AmpOutputStream
{
/**
* Writes data to the stream.
*
* @param string $data Bytes to write.
*
* @return Promise Succeeds once the data has been successfully written to the stream.
*
* @throws ClosedException If the stream has already been closed.
* @throws StreamException If writing to the stream fails.
*/
public function write(string $data): Promise
{
return Tools::call($this->__call('write', [$data]));
}
/**
* Marks the stream as no longer writable. Optionally writes a final data chunk before. Note that this is not the
* same as forcefully closing the stream. This method waits for all pending writes to complete before closing the
* stream. Socket streams implementing this interface should only close the writable side of the stream.
*
* @param string $finalData Bytes to write.
*
* @return Promise Succeeds once the data has been successfully written to the stream.
*
* @throws ClosedException If the stream has already been closed.
* @throws StreamException If writing to the stream fails.
*/
public function end(string $finalData = ""): Promise
{
return Tools::call($this->__call('write', [$finalData]));
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace danog\MadelineProto\Ipc\Wrapper;
class SeekableInputStream extends InputStream
{
use SeekableTrait;
}

View File

@ -0,0 +1,8 @@
<?php
namespace danog\MadelineProto\Ipc\Wrapper;
class SeekableOutputStream extends OutputStream
{
use SeekableTrait;
}

View File

@ -0,0 +1,27 @@
<?php
namespace danog\MadelineProto\Ipc\Wrapper;
use Amp\Promise;
use danog\MadelineProto\Tools;
trait SeekableTrait
{
/**
* Set the handle's internal pointer position.
*
* $whence values:
*
* SEEK_SET - Set position equal to offset bytes.
* SEEK_CUR - Set position to current location plus offset.
* SEEK_END - Set position to end-of-file plus offset.
*
* @param int $position
* @param int $whence
* @return \Amp\Promise<int> New offset position.
*/
public function seek(int $position, int $whence = \SEEK_SET): Promise
{
return Tools::call($this->__call('seek', [$position, $whence]));
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace danog\MadelineProto\Ipc\Wrapper;
use danog\MadelineProto\Ipc\Wrapper;
trait WrapMethodTrait
{
abstract public function __call($name, $args);
public function wrap(...$args): \Generator
{
$new = yield from Wrapper::create($args, $this->session->getIpcCallbackPath(), $this->logger);
foreach ($args as &$arg) {
$new->wrap($arg);
}
return $this->__call(__FUNCTION__, $new);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,61 @@
<?php
/**
* IPC light state.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
*
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\MadelineProto;
/**
* Light state.
*
* @internal
*/
final class LightState
{
/**
* Event handler class name.
*
* @var null|class-string<EventHandler>
*/
private ?string $eventHandler = null;
public function __construct(MTProto $API)
{
if ($API->hasEventHandler()) {
$this->eventHandler = \get_class($API->getEventHandler());
}
}
/**
* Check whether we can start IPC.
*
* @return boolean
*/
public function canStartIpc(): bool
{
return !$this->eventHandler || \class_exists($this->eventHandler);
}
/**
* Get event handler class name.
*
* @return null|class-string<EventHandler>
*/
public function getEventHandler(): ?string
{
return $this->eventHandler;
}
}

View File

@ -21,6 +21,10 @@ namespace danog\MadelineProto;
use Amp\ByteStream\ResourceOutputStream;
use Amp\Failure;
use Amp\Loop;
use danog\MadelineProto\Settings\Logger as SettingsLogger;
use Psr\Log\LoggerInterface;
use function Amp\ByteStream\getStderr;
use function Amp\ByteStream\getStdout;
@ -29,10 +33,21 @@ use function Amp\ByteStream\getStdout;
*/
class Logger
{
use Tools;
/**
* @internal ANSI foreground color escapes
*/
const FOREGROUND = ['default' => 39, 'black' => 30, 'red' => 31, 'green' => 32, 'yellow' => 33, 'blue' => 34, 'magenta' => 35, 'cyan' => 36, 'light_gray' => 37, 'dark_gray' => 90, 'light_red' => 91, 'light_green' => 92, 'light_yellow' => 93, 'light_blue' => 94, 'light_magenta' => 95, 'light_cyan' => 96, 'white' => 97];
/**
* @internal ANSI background color escapes
*/
const BACKGROUND = ['default' => 49, 'black' => 40, 'red' => 41, 'magenta' => 45, 'yellow' => 43, 'green' => 42, 'blue' => 44, 'cyan' => 46, 'light_gray' => 47, 'dark_gray' => 100, 'light_red' => 101, 'light_green' => 102, 'light_yellow' => 103, 'light_blue' => 104, 'light_magenta' => 105, 'light_cyan' => 106, 'white' => 107];
/**
* @internal ANSI modifier escapes
*/
const SET = ['bold' => 1, 'dim' => 2, 'underlined' => 3, 'blink' => 4, 'reverse' => 5, 'hidden' => 6];
/**
* @internal ANSI reset modifier escapes
*/
const RESET = ['all' => 0, 'bold' => 21, 'dim' => 22, 'underlined' => 24, 'blink' => 25, 'reverse' => 26, 'hidden' => 28];
/**
* Logging mode.
@ -43,7 +58,7 @@ class Logger
/**
* Optional logger parameter.
*
* @var mixed
* @var null|string|callable
*/
public $optional = null;
/**
@ -88,112 +103,164 @@ class Logger
* @var boolean
*/
public static $printed = false;
/**
* Log rotation loop ID.
*/
private string $rotateId = '';
/**
* PSR logger.
*/
private PsrLogger $psr;
/**
* Ultra verbose logging.
*
* @internal
*/
const ULTRA_VERBOSE = 5;
/**
* Verbose logging.
*
* @internal
*/
const VERBOSE = 4;
/**
* Notice logging.
*
* @internal
*/
const NOTICE = 3;
/**
* Warning logging.
*
* @internal
*/
const WARNING = 2;
/**
* Error logging.
*
* @internal
*/
const ERROR = 1;
/**
* Log only fatal errors.
*
* @internal
*/
const FATAL_ERROR = 0;
/**
* Disable logger (DEPRECATED).
*
* @internal
* @deprecated
*/
const NO_LOGGER = 0;
/**
* Default logger (syslog).
*
* @internal
*/
const DEFAULT_LOGGER = 1;
/**
* File logger.
*
* @internal
*/
const FILE_LOGGER = 2;
/**
* Echo logger.
*
* @internal
*/
const ECHO_LOGGER = 3;
/**
* Callable logger.
*
* @internal
*/
const CALLABLE_LOGGER = 4;
/**
* Ultra verbose level.
*/
const LEVEL_ULTRA_VERBOSE = self::ULTRA_VERBOSE;
/**
* Verbose level.
*/
const LEVEL_VERBOSE = self::VERBOSE;
/**
* Notice level.
*/
const LEVEL_NOTICE = self::NOTICE;
/**
* Warning level.
*/
const LEVEL_WARNING = self::WARNING;
/**
* Error level.
*/
const LEVEL_ERROR = self::ERROR;
/**
* Fatal error level.
*/
const LEVEL_FATAL = self::FATAL_ERROR;
/**
* Default logger (syslog).
*/
const LOGGER_DEFAULT = self::DEFAULT_LOGGER;
/**
* Echo logger.
*/
const LOGGER_ECHO = self::ECHO_LOGGER;
/**
* File logger.
*/
const LOGGER_FILE = self::FILE_LOGGER;
/**
* Callable logger.
*/
const LOGGER_CALLABLE = self::CALLABLE_LOGGER;
/**
* Construct global static logger from MadelineProto settings.
*
* @param array $settings Settings array
*
* @return void
*/
public static function constructorFromSettings(array $settings)
{
if (!self::$default) {
// The getLogger function will automatically init the static logger, but we'll do it again anyway
self::$default = self::getLoggerFromSettings(MTProto::parseSettings($settings));
}
}
/**
* Get logger from MadelineProto settings.
*
* @param array $settings Settings array
* @param string $prefix Optional prefix for log messages
* @param SettingsLogger $settings Settings instance
*
* @return self
*/
public static function getLoggerFromSettings(array $settings, string $prefix = ''): self
public static function constructorFromSettings(SettingsLogger $settings): self
{
if (isset($settings['logger']['rollbar_token']) && $settings['logger']['rollbar_token'] !== '' && \class_exists(\Rollbar\Rollbar::class)) {
@\Rollbar\Rollbar::init(['environment' => 'production', 'root' => __DIR__, 'access_token' => isset($settings['logger']['rollbar_token']) && !\in_array($settings['logger']['rollbar_token'], ['f9fff6689aea4905b58eec73f66c791d', '300afd7ccef346ea84d0c185ae831718', '11a8c2fe4c474328b40a28193f8d63f5', 'beef2d426496462ba34dcaad33d44a14']) || $settings['pwr']['pwr'] ? $settings['logger']['rollbar_token'] : 'c07d9b2f73c2461297b0beaef6c1662f'], false, false);
} else {
Exception::$rollbar = false;
RPCErrorException::$rollbar = false;
}
if (!isset($settings['logger']['logger_param']) && isset($settings['logger']['param'])) {
$settings['logger']['logger_param'] = $settings['logger']['param'];
}
if (PHP_SAPI !== 'cli' && isset($settings['logger']['logger_param']) && $settings['logger']['logger_param'] === 'MadelineProto.log') {
$settings['logger']['logger_param'] = Magic::$script_cwd . '/MadelineProto.log';
}
$logger = new self($settings['logger']['logger'], $settings['logger']['logger_param'] ?? '', $prefix, $settings['logger']['logger_level'] ?? Logger::VERBOSE, $settings['logger']['max_size'] ?? 100 * 1024 * 1024);
if (!self::$default) {
self::$default = $logger;
}
if (PHP_SAPI !== 'cli') {
try {
\error_reporting(E_ALL);
\ini_set('log_errors', 1);
\ini_set('error_log', $settings['logger']['logger'] === self::FILE_LOGGER ? $settings['logger']['logger_param'] : Magic::$script_cwd . '/MadelineProto.log');
\error_log('Enabled PHP logging');
} catch (\danog\MadelineProto\Exception $e) {
$logger->logger('Could not enable PHP logging');
return self::$default = new self($settings);
}
/**
* Construct logger.
*
* @param SettingsLogger $settings
* @param string $prefix
*/
public function __construct(SettingsLogger $settings, string $prefix = '')
{
$this->psr = new PsrLogger($this);
$this->prefix = $prefix === '' ? '' : ', '.$prefix;
$this->mode = $settings->getType();
$this->optional = $settings->getExtra();
$this->level = $settings->getLevel();
$maxSize = $settings->getMaxSize();
if ($this->mode === self::FILE_LOGGER) {
if (!\file_exists(\pathinfo($this->optional, PATHINFO_DIRNAME))) {
$this->optional = Magic::$script_cwd.'/MadelineProto.log';
}
if (!str_ends_with($this->optional, '.log')) {
$this->optional .= '.log';
}
if ($maxSize !== -1 && \file_exists($this->optional) && \filesize($this->optional) > $maxSize) {
\unlink($this->optional);
}
}
return $logger;
}
/**
* Construct global logger.
*
* @param int $mode One of the logger constants
* @param mixed $optional Optional parameter for logger
* @param string $prefix Prefix for log messages
* @param int $level Default logging level
* @param int $max_size Maximum size for logfile
*
* @return void
*/
public static function constructor(int $mode, $optional = null, string $prefix = '', int $level = self::NOTICE, int $max_size = 100 * 1024 * 1024)
{
self::$default = new self($mode, $optional, $prefix, $level, $max_size);
}
/**
* Construct global logger.
*
* @param int $mode One of the logger constants
* @param mixed $optional Optional parameter for logger
* @param string $prefix Prefix for log messages
* @param int $level Default logging level
* @param int $max_size Maximum size for logfile
*
* @return void
*/
public function __construct(int $mode, $optional = null, string $prefix = '', int $level = self::NOTICE, int $max_size = 100 * 1024 * 1024)
{
if ($mode === null) {
throw new Exception(\danog\MadelineProto\Lang::$current_lang['no_mode_specified']);
}
$this->mode = $mode;
$this->optional = $mode == 2 ? Absolute::absolute($optional) : $optional;
$this->prefix = $prefix === '' ? '' : ', ' . $prefix;
$this->level = $level;
if ($this->mode === 2 && !\file_exists(\pathinfo($this->optional, PATHINFO_DIRNAME))) {
$this->optional = Magic::$script_cwd . '/MadelineProto.log';
}
if ($this->mode === 2 && !\preg_match('/\\.log$/', $this->optional)) {
$this->optional .= '.log';
}
if ($mode === 2 && $max_size !== -1 && \file_exists($this->optional) && \filesize($this->optional) > $max_size) {
\unlink($this->optional);
}
$this->colors[self::ULTRA_VERBOSE] = \implode(';', [self::FOREGROUND['light_gray'], self::SET['dim']]);
$this->colors[self::VERBOSE] = \implode(';', [self::FOREGROUND['green'], self::SET['bold']]);
@ -202,14 +269,33 @@ class Logger
$this->colors[self::ERROR] = \implode(';', [self::FOREGROUND['white'], self::SET['bold'], self::BACKGROUND['red']]);
$this->colors[self::FATAL_ERROR] = \implode(';', [self::FOREGROUND['red'], self::SET['bold'], self::BACKGROUND['light_gray']]);
$this->newline = PHP_EOL;
if ($this->mode === 3) {
if ($this->mode === self::ECHO_LOGGER) {
$this->stdout = getStdout();
if (PHP_SAPI !== 'cli') {
$this->newline = '<br>' . $this->newline;
if (PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg') {
$this->newline = '<br>'.$this->newline;
}
} elseif ($this->mode === 2) {
$this->stdout = new ResourceOutputStream(\fopen($this->optional, 'a+'));
} elseif ($this->mode === 1) {
} elseif ($this->mode === self::FILE_LOGGER) {
Snitch::logFile($this->optional);
$this->stdout = new ResourceOutputStream(\fopen($this->optional, 'a'));
if ($maxSize !== -1) {
$optional = &$this->optional;
$stdout = &$this->stdout;
$this->rotateId = Loop::repeat(
10*1000,
static function () use ($maxSize, $optional, &$stdout) {
\clearstatcache(true, $optional);
if (\file_exists($optional) && \filesize($optional) >= $maxSize) {
$stdout->close();
$stdout = null;
\unlink($optional);
$stdout = new ResourceOutputStream(\fopen($optional, 'a'));
self::log("Automatically truncated logfile to $maxSize");
}
}
);
Loop::unreference($this->rotateId);
}
} elseif ($this->mode === self::DEFAULT_LOGGER) {
$result = @\ini_get('error_log');
if ($result === 'syslog') {
$this->stdout = getStderr();
@ -219,6 +305,29 @@ class Logger
$this->stdout = getStderr();
}
}
self::$default = $this;
if (PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg') {
try {
\error_reporting(E_ALL);
\ini_set('log_errors', "1");
\ini_set('error_log', $this->mode === self::FILE_LOGGER
? $this->optional
: Magic::$script_cwd.'/MadelineProto.log');
\error_log('Enabled PHP logging');
} catch (\danog\MadelineProto\Exception $e) {
$this->logger('Could not enable PHP logging');
}
}
}
/**
* Destructor function.
*/
public function __destruct()
{
if ($this->rotateId) {
Loop::cancel($this->rotateId);
}
}
/**
* Log a message.
@ -233,7 +342,7 @@ class Logger
if (!\is_null(self::$default)) {
self::$default->logger($param, $level, \basename(\debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]['file'], '.php'));
} else {
echo $param . PHP_EOL;
echo $param.PHP_EOL;
}
}
/**
@ -247,7 +356,7 @@ class Logger
*/
public function logger($param, int $level = self::NOTICE, string $file = ''): void
{
if ($level > $this->level || $this->mode === 0) {
if ($level > $this->level || $this->mode === self::NO_LOGGER) {
return;
}
if (!self::$printed) {
@ -259,14 +368,11 @@ class Logger
$this->logger('https://github.com/danog/MadelineProto');
$this->colors[self::NOTICE] = \implode(';', [self::FOREGROUND['yellow'], self::SET['bold']]);
}
if ($this->mode === 4) {
if ($this->mode === self::CALLABLE_LOGGER) {
\call_user_func_array($this->optional, [$param, $level]);
return;
}
$prefix = $this->prefix;
if (\danog\MadelineProto\Magic::$has_thread && \is_object(\Thread::getCurrentThread())) {
$prefix .= ' (t)';
}
if ($param instanceof \Throwable) {
$param = (string) $param;
} elseif (!\is_string($param)) {
@ -275,26 +381,33 @@ class Logger
if (empty($file)) {
$file = \basename(\debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]['file'], '.php');
}
$param = \str_pad($file . $prefix . ': ', 16 + \strlen($prefix)) . "\t" . $param;
switch ($this->mode) {
case 1:
if ($this->stdout->write($param . $this->newline) instanceof Failure) {
\error_log($param);
}
break;
default:
$param = Magic::$isatty ? "\33[" . $this->colors[$level] . 'm' . $param . "\33[0m" . $this->newline : $param . $this->newline;
if ($this->stdout->write($param) instanceof Failure) {
switch ($this->mode) {
case 3:
echo $param;
break;
case 2:
\file_put_contents($this->optional, $param, FILE_APPEND);
break;
}
}
break;
$param = \str_pad($file.$prefix.': ', 16 + \strlen($prefix))."\t".$param;
if ($this->mode === self::DEFAULT_LOGGER) {
if ($this->stdout->write($param.$this->newline) instanceof Failure) {
\error_log($param);
}
return;
}
$param = Magic::$isatty ? "\33[".$this->colors[$level].'m'.$param."\33[0m".$this->newline : $param.$this->newline;
if ($this->stdout->write($param) instanceof Failure) {
switch ($this->mode) {
case self::ECHO_LOGGER:
echo $param;
break;
case self::FILE_LOGGER:
\file_put_contents($this->optional, $param, FILE_APPEND);
break;
}
}
}
/**
* Get PSR logger.
*
* @return LoggerInterface
*/
public function getPsrLogger(): LoggerInterface
{
return $this->psr;
}
}

View File

@ -1,7 +1,6 @@
<?php
/**
* Signal loop interface.
* Internal loop trait.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@ -19,29 +18,30 @@
namespace danog\MadelineProto\Loop;
use Amp\Promise;
use danog\MadelineProto\EventHandler;
use danog\MadelineProto\InternalDoc;
/**
* Signal loop interface.
*
* @author Daniil Gentili <daniil@daniil.it>
* API loop trait.
*/
interface SignalLoopInterface extends LoopInterface
trait APILoop
{
use LoggerLoop {
__construct as private setLogger;
}
/**
* Resolve the promise or return|throw the signal.
*
* @param Promise $promise The origin promise
*
* @return Promise
* API instance.
*/
public function waitSignal($promise): Promise;
protected InternalDoc $API;
/**
* Send a signal to the the loop.
* Constructor.
*
* @param Exception|any $data Signal to send
*
* @return void
* @param InternalDoc $API API instance
*/
public function signal($data);
public function __construct(InternalDoc $API)
{
$this->API = $API;
$this->setLogger($API instanceof EventHandler ? $API->getAPI()->getLogger() : $API->logger);
}
}

View File

@ -21,8 +21,7 @@ namespace danog\MadelineProto\Loop\Connection;
use Amp\Deferred;
use Amp\Loop;
use danog\MadelineProto\Connection;
use danog\MadelineProto\Loop\Impl\ResumableSignalLoop;
use danog\Loop\ResumableSignalLoop;
use danog\MadelineProto\Tools;
/**
@ -32,38 +31,20 @@ use danog\MadelineProto\Tools;
*/
class CheckLoop extends ResumableSignalLoop
{
use Common;
/**
* Connection instance.
* Main loop.
*
* @var \danog\MadelineProto\Connection
* @return \Generator
*/
protected $connection;
/**
* DC ID.
*
* @var string
*/
protected $datacenter;
/**
* DataCenterConnection instance.
*
* @var \danog\MadelineProto\DataCenterConnection
*/
protected $datacenterConnection;
public function __construct(Connection $connection)
{
$this->connection = $connection;
$this->API = $connection->getExtra();
$this->datacenter = $connection->getDatacenterID();
$this->datacenterConnection = $connection->getShared();
}
public function loop(): \Generator
{
$API = $this->API;
$datacenter = $this->datacenter;
$connection = $this->connection;
$shared = $this->datacenterConnection;
$timeout = $shared->getSettings()['timeout'];
$timeout = $shared->getSettings()->getTimeout();
$timeoutMs = $timeout * 1000;
$timeoutResend = $timeout * $timeout;
// Typically 25 seconds, good enough
while (true) {
@ -72,104 +53,115 @@ class CheckLoop extends ResumableSignalLoop
return;
}
}
if ($connection->hasPendingCalls()) {
$last_msgid = $connection->msgIdHandler->getMaxId(true);
$last_chunk = $connection->getLastChunk();
if ($shared->hasTempAuthKey()) {
$full_message_ids = $connection->getPendingCalls();
//array_values($connection->new_outgoing);
foreach (\array_chunk($full_message_ids, 8192) as $message_ids) {
$deferred = new Deferred();
$deferred->promise()->onResolve(function ($e, $result) use ($message_ids, $API, $connection, $datacenter, $timeoutResend) {
if ($e) {
$API->logger("Got exception in check loop for DC {$datacenter}");
$API->logger((string) $e);
return;
if (!$connection->hasPendingCalls()) {
if (yield $this->waitSignal($this->pause($timeoutMs))) {
return;
}
continue;
}
$last_msgid = $connection->msgIdHandler->getMaxId(true);
$last_chunk = $connection->getLastChunk();
if ($shared->hasTempAuthKey()) {
$full_message_ids = $connection->getPendingCalls();
foreach (\array_chunk($full_message_ids, 8192) as $message_ids) {
$deferred = new Deferred();
$deferred->promise()->onResolve(function ($e, $result) use ($message_ids, $API, $connection, $datacenter, $timeoutResend) {
if ($e) {
$API->logger("Got exception in check loop for DC {$datacenter}");
$API->logger((string) $e);
return;
}
$reply = [];
foreach (\str_split($result['info']) as $key => $chr) {
$message_id = $message_ids[$key];
if (!isset($connection->outgoing_messages[$message_id])) {
$API->logger->logger('Already got response for and forgot about message ID '.$message_id);
continue;
}
$reply = [];
foreach (\str_split($result['info']) as $key => $chr) {
$message_id = $message_ids[$key];
if (!isset($connection->outgoing_messages[$message_id])) {
$API->logger->logger('Already got response for and forgot about message ID ' . $message_id);
continue;
}
if (!isset($connection->new_outgoing[$message_id])) {
$API->logger->logger('Already got response for ' . $connection->outgoing_messages[$message_id]['_'] . ' with message ID ' . $message_id);
continue;
}
$chr = \ord($chr);
switch ($chr & 7) {
if (!isset($connection->new_outgoing[$message_id])) {
$API->logger->logger('Already got response for '.$connection->outgoing_messages[$message_id]);
continue;
}
$message = $connection->new_outgoing[$message_id];
$chr = \ord($chr);
switch ($chr & 7) {
case 0:
$API->logger->logger('Wrong message status 0 for ' . $connection->outgoing_messages[$message_id]['_'], \danog\MadelineProto\Logger::FATAL_ERROR);
$API->logger->logger("Wrong message status 0 for $message", \danog\MadelineProto\Logger::FATAL_ERROR);
break;
case 1:
case 2:
case 3:
if ($connection->outgoing_messages[$message_id]['_'] === 'msgs_state_req') {
$connection->gotResponseForOutgoingMessageId($message_id);
if ($message->getConstructor() === 'msgs_state_req') {
$connection->gotResponseForOutgoingMessage($message);
break;
}
$API->logger->logger('Message ' . $connection->outgoing_messages[$message_id]['_'] . ' with message ID ' . $message_id . ' not received by server, resending...', \danog\MadelineProto\Logger::ERROR);
$API->logger->logger("Message $message not received by server, resending...", \danog\MadelineProto\Logger::ERROR);
$connection->methodRecall('watcherId', ['message_id' => $message_id, 'postpone' => true]);
break;
case 4:
if ($chr & 32) {
if ($connection->outgoing_messages[$message_id]['sent'] + $timeoutResend < \time()) {
$API->logger->logger('Message ' . $connection->outgoing_messages[$message_id]['_'] . ' with message ID ' . $message_id . ' received by server and is being processed for way too long, resending request...', \danog\MadelineProto\Logger::ERROR);
if ($message->getSent() + $timeoutResend < \time()) {
$API->logger->logger("Message $message received by server and is being processed for way too long, resending request...", \danog\MadelineProto\Logger::ERROR);
$connection->methodRecall('', ['message_id' => $message_id, 'postpone' => true]);
} else {
$API->logger->logger('Message ' . $connection->outgoing_messages[$message_id]['_'] . ' with message ID ' . $message_id . ' received by server and is being processed, waiting...', \danog\MadelineProto\Logger::ERROR);
$API->logger->logger("Message $message received by server and is being processed, waiting...", \danog\MadelineProto\Logger::ERROR);
}
} elseif ($chr & 64) {
$API->logger->logger('Message ' . $connection->outgoing_messages[$message_id]['_'] . ' with message ID ' . $message_id . ' received by server and was already processed, requesting reply...', \danog\MadelineProto\Logger::ERROR);
$API->logger->logger("Message $message received by server and was already processed, requesting reply...", \danog\MadelineProto\Logger::ERROR);
$reply[] = $message_id;
} elseif ($chr & 128) {
$API->logger->logger('Message ' . $connection->outgoing_messages[$message_id]['_'] . ' with message ID ' . $message_id . ' received by server and was already sent, requesting reply...', \danog\MadelineProto\Logger::ERROR);
$API->logger->logger("Message $message received by server and was already sent, requesting reply...", \danog\MadelineProto\Logger::ERROR);
$reply[] = $message_id;
} else {
$API->logger->logger('Message ' . $connection->outgoing_messages[$message_id]['_'] . ' with message ID ' . $message_id . ' received by server, requesting reply...', \danog\MadelineProto\Logger::ERROR);
$API->logger->logger("Message $message received by server, waiting...", \danog\MadelineProto\Logger::ERROR);
$reply[] = $message_id;
}
}
}
if ($reply) {
\danog\MadelineProto\Tools::callFork($connection->objectCall('msg_resend_ans_req', ['msg_ids' => $reply], ['postpone' => true]));
}
$connection->flush();
});
$list = '';
// Don't edit this here pls
foreach ($message_ids as $message_id) {
$list .= $connection->outgoing_messages[$message_id]['_'] . ', ';
}
$API->logger->logger("Still missing {$list} on DC {$datacenter}, sending state request", \danog\MadelineProto\Logger::ERROR);
yield from $connection->objectCall('msgs_state_req', ['msg_ids' => $message_ids], ['promise' => $deferred]);
/*
if ($reply) {
$deferred= new Deferred;
$deferred->promise()->onResolve(fn($e, $res) => var_dump(ord($res['info'][0])));
\danog\MadelineProto\Tools::callFork($connection->objectCall('msg_resend_req', ['msg_ids' => $reply], ['postpone' => true, 'promise' => $deferred]));
}*/
$connection->flush();
});
$list = '';
// Don't edit this here pls
foreach ($message_ids as $message_id) {
$list .= $connection->outgoing_messages[$message_id]->getConstructor().', ';
}
} else {
foreach ($connection->new_outgoing as $message_id) {
if (isset($connection->outgoing_messages[$message_id]['sent']) && $connection->outgoing_messages[$message_id]['sent'] + $timeout < \time() && $connection->outgoing_messages[$message_id]['unencrypted']) {
$API->logger->logger('Still missing ' . $connection->outgoing_messages[$message_id]['_'] . ' with message id ' . $message_id . " on DC {$datacenter}, resending", \danog\MadelineProto\Logger::ERROR);
$connection->methodRecall('', ['message_id' => $message_id, 'postpone' => true]);
}
}
$connection->flush();
}
if (yield $this->waitSignal($this->pause($timeout))) {
return;
}
if ($connection->msgIdHandler->getMaxId(true) === $last_msgid && $connection->getLastChunk() === $last_chunk) {
$API->logger->logger("We did not receive a response for {$timeout} seconds: reconnecting and exiting check loop on DC {$datacenter}");
//$this->exitedLoop();
Tools::callForkDefer($connection->reconnect());
return;
$API->logger->logger("Still missing {$list} on DC {$datacenter}, sending state request", \danog\MadelineProto\Logger::ERROR);
yield from $connection->objectCall('msgs_state_req', ['msg_ids' => $message_ids], ['promise' => $deferred]);
}
} else {
if (yield $this->waitSignal($this->pause($timeout))) {
return;
foreach ($connection->new_outgoing as $message_id => $message) {
if ($message->wasSent()
&& $message->getSent() + $timeout < \time()
&& $message->isUnencrypted()
) {
$API->logger->logger("Still missing $message on DC $datacenter, resending", \danog\MadelineProto\Logger::ERROR);
$connection->methodRecall('', ['message_id' => $message->getMsgId(), 'postpone' => true]);
}
}
$connection->flush();
}
if (yield $this->waitSignal($this->pause($timeoutMs))) {
return;
}
if ($connection->msgIdHandler->getMaxId(true) === $last_msgid && $connection->getLastChunk() === $last_chunk) {
$API->logger->logger("We did not receive a response for {$timeout} seconds: reconnecting and exiting check loop on DC {$datacenter}");
//$this->exitedLoop();
Tools::callForkDefer($connection->reconnect());
return;
}
}
}
/**
* Loop name.
*
* @return string
*/
public function __toString(): string
{
return "check loop in DC {$this->datacenter}";

View File

@ -1,7 +1,6 @@
<?php
/**
* Async parameters class.
* Common abstract class for all connection loops.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@ -17,49 +16,46 @@
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\MadelineProto\Async;
namespace danog\MadelineProto\Loop\Connection;
use danog\MadelineProto\Connection;
use danog\MadelineProto\DataCenterConnection;
use danog\MadelineProto\Loop\InternalLoop;
/**
* Async parameters class.
*
* Manages asynchronous generation of method parameters
* RPC call status check loop.
*
* @author Daniil Gentili <daniil@daniil.it>
*/
class AsyncParameters
trait Common
{
/**
* Async callable.
*
* @var callable
*/
private $callable;
/**
* Create async parameters.
*
* @param callable $callable Async callable that will return parameters
*/
public function __construct(callable $callable)
{
$this->callable = $callable;
use InternalLoop {
__construct as private init;
}
/**
* Create async parameters.
*
* @param callable $callable Async callable that will return parameters
* Connection instance.
*/
public function setCallable(callable $callable): void
{
$this->callable = $callable;
}
protected Connection $connection;
/**
* Get parameters asynchronously.
* DC ID.
*
* @return \Generator<array>|\Amp\Promise<array>
* @var string
*/
public function getParameters()
protected string $datacenter;
/**
* DataCenterConnection instance.
*/
protected DataCenterConnection $datacenterConnection;
/**
* Constructor function.
*
* @param Connection $connection Connection
*/
public function __construct(Connection $connection)
{
$callable = $this->callable;
return $callable();
$this->init($connection->getExtra());
$this->connection = $connection;
$this->datacenter = $connection->getDatacenterID();
$this->datacenterConnection = $connection->getShared();
}
}

View File

@ -19,8 +19,8 @@
namespace danog\MadelineProto\Loop\Connection;
use danog\MadelineProto\Connection;
use danog\MadelineProto\Loop\Impl\ResumableSignalLoop;
use danog\Loop\ResumableSignalLoop;
use danog\MadelineProto\MTProto\OutgoingMessage;
/**
* HttpWait loop.
@ -29,31 +29,12 @@ use danog\MadelineProto\Loop\Impl\ResumableSignalLoop;
*/
class HttpWaitLoop extends ResumableSignalLoop
{
use Common;
/**
* Connection instance.
* Main loop.
*
* @var \danog\MadelineProto\Connection
* @return \Generator
*/
protected $connection;
/**
* DC ID.
*
* @var string
*/
protected $datacenter;
/**
* DataCenterConnection instance.
*
* @var \danog\MadelineProto\DataCenterConnection
*/
protected $datacenterConnection;
public function __construct(Connection $connection)
{
$this->connection = $connection;
$this->API = $connection->getExtra();
$this->datacenter = $connection->getDatacenterID();
$this->datacenterConnection = $connection->getShared();
}
public function loop(): \Generator
{
$API = $this->API;
@ -76,12 +57,25 @@ class HttpWaitLoop extends ResumableSignalLoop
}
}
$API->logger->logger("DC {$datacenter}: request {$connection->countHttpSent()}, response {$connection->countHttpReceived()}");
if ($connection->countHttpSent() === $connection->countHttpReceived() && (!empty($connection->pending_outgoing) || !empty($connection->new_outgoing) && !$connection->hasPendingCalls())) {
yield from $connection->sendMessage(['_' => 'http_wait', 'body' => ['max_wait' => 30000, 'wait_after' => 0, 'max_delay' => 0], 'contentRelated' => true, 'unencrypted' => false, 'method' => false]);
if ($connection->countHttpSent() === $connection->countHttpReceived() && (!empty($connection->pendingOutgoing) || !empty($connection->new_outgoing) && !$connection->hasPendingCalls())) {
yield from $connection->sendMessage(
new OutgoingMessage(
['max_wait' => 30000, 'wait_after' => 0, 'max_delay' => 0],
'http_wait',
'',
false,
false
)
);
}
$API->logger->logger("DC {$datacenter}: request {$connection->countHttpSent()}, response {$connection->countHttpReceived()}");
}
}
/**
* Loop name.
*
* @return string
*/
public function __toString(): string
{
return "HTTP wait loop in DC {$this->datacenter}";

View File

@ -19,8 +19,7 @@
namespace danog\MadelineProto\Loop\Connection;
use danog\MadelineProto\Connection;
use danog\MadelineProto\Loop\Impl\ResumableSignalLoop;
use danog\Loop\ResumableSignalLoop;
/**
* Ping loop.
@ -29,45 +28,27 @@ use danog\MadelineProto\Loop\Impl\ResumableSignalLoop;
*/
class PingLoop extends ResumableSignalLoop
{
use Common;
/**
* Connection instance.
* Main loop.
*
* @var \danog\MadelineProto\Connection
* @return \Generator
*/
protected $connection;
/**
* DC ID.
*
* @var string
*/
protected $datacenter;
/**
* DataCenterConnection instance.
*
* @var \danog\MadelineProto\DataCenterConnection
*/
protected $datacenterConnection;
public function __construct(Connection $connection)
{
$this->connection = $connection;
$this->API = $connection->getExtra();
$this->datacenter = $connection->getDatacenterID();
$this->datacenterConnection = $connection->getShared();
}
public function loop(): \Generator
{
$API = $this->API;
$datacenter = $this->datacenter;
$connection = $this->connection;
$shared = $this->datacenterConnection;
$timeout = $shared->getSettings()['timeout'];
$timeout = $shared->getSettings()->getTimeout();
$timeoutMs = $timeout * 1000;
while (true) {
while (!$shared->hasTempAuthKey()) {
if (yield $this->waitSignal($this->pause())) {
return;
}
}
if (yield $this->waitSignal($this->pause($timeout))) {
if (yield $this->waitSignal($this->pause($timeoutMs))) {
return;
}
if (\time() - $connection->getLastChunk() >= $timeout) {
@ -81,6 +62,11 @@ class PingLoop extends ResumableSignalLoop
}
}
}
/**
* Get loop name.
*
* @return string
*/
public function __toString(): string
{
return "Ping loop in DC {$this->datacenter}";

View File

@ -23,9 +23,9 @@ use Amp\ByteStream\PendingReadError;
use Amp\ByteStream\StreamException;
use Amp\Loop;
use Amp\Websocket\ClosedException;
use danog\MadelineProto\Connection;
use danog\Loop\SignalLoop;
use danog\MadelineProto\Logger;
use danog\MadelineProto\Loop\Impl\SignalLoop;
use danog\MadelineProto\MTProto\IncomingMessage;
use danog\MadelineProto\MTProtoTools\Crypt;
use danog\MadelineProto\NothingInTheSocketException;
use danog\MadelineProto\Tools;
@ -37,33 +37,12 @@ use danog\MadelineProto\Tools;
*/
class ReadLoop extends SignalLoop
{
use Tools;
use Crypt;
use Common;
/**
* Connection instance.
* Main loop.
*
* @var \danog\MadelineProto\Connection
* @return \Generator
*/
protected $connection;
/**
* DataCenterConnection instance.
*
* @var \danog\MadelineProto\DataCenterConnection
*/
protected $datacenterConnection;
/**
* DC ID.
*
* @var string
*/
protected $datacenter;
public function __construct(Connection $connection)
{
$this->connection = $connection;
$this->API = $connection->getExtra();
$this->datacenter = $connection->getDatacenterID();
$this->datacenterConnection = $connection->getShared();
}
public function loop(): \Generator
{
$API = $this->API;
@ -89,11 +68,11 @@ class ReadLoop extends SignalLoop
Tools::callForkDefer((function () use ($error, $shared, $connection, $datacenter, $API): \Generator {
if ($error === -404) {
if ($shared->hasTempAuthKey()) {
$API->logger->logger("WARNING: Resetting auth key in DC {$datacenter}...", \danog\MadelineProto\Logger::WARNING);
$API->logger->logger("WARNING: Resetting auth key in DC {$datacenter}...", Logger::WARNING);
$shared->setTempAuthKey(null);
$shared->resetSession();
foreach ($connection->new_outgoing as $message_id) {
$connection->outgoing_messages[$message_id]['sent'] = 0;
foreach ($connection->new_outgoing as $message) {
$message->resetSent();
}
yield from $shared->reconnect();
yield from $API->initAuthorization();
@ -101,14 +80,14 @@ class ReadLoop extends SignalLoop
yield from $connection->reconnect();
}
} elseif ($error === -1) {
$API->logger->logger("WARNING: Got quick ack from DC {$datacenter}", \danog\MadelineProto\Logger::WARNING);
$API->logger->logger("WARNING: Got quick ack from DC {$datacenter}", Logger::WARNING);
yield from $connection->reconnect();
} elseif ($error === 0) {
$API->logger->logger("Got NOOP from DC {$datacenter}", \danog\MadelineProto\Logger::WARNING);
$API->logger->logger("Got NOOP from DC {$datacenter}", Logger::WARNING);
yield from $connection->reconnect();
} elseif ($error === -429) {
$API->logger->logger("Got -429 from DC {$datacenter}", \danog\MadelineProto\Logger::WARNING);
yield Tools::sleep(1);
$API->logger->logger("Got -429 from DC {$datacenter}", Logger::WARNING);
yield Tools::sleep(3);
yield from $connection->reconnect();
} else {
yield from $connection->reconnect();
@ -140,14 +119,14 @@ class ReadLoop extends SignalLoop
$API->logger->logger($e->getReason());
if (\strpos($e->getReason(), ' ') === 0) {
$payload = -\substr($e->getReason(), 7);
$API->logger->logger("Received {$payload} from DC " . $datacenter, \danog\MadelineProto\Logger::ERROR);
$API->logger->logger("Received {$payload} from DC ".$datacenter, Logger::ERROR);
return $payload;
}
throw $e;
}
if ($payload_length === 4) {
$payload = \danog\MadelineProto\Tools::unpackSignedInt(yield $buffer->bufferRead(4));
$API->logger->logger("Received {$payload} from DC " . $datacenter, \danog\MadelineProto\Logger::ULTRA_VERBOSE);
$API->logger->logger("Received {$payload} from DC ".$datacenter, Logger::ULTRA_VERBOSE);
return $payload;
}
$connection->reading(true);
@ -162,26 +141,25 @@ class ReadLoop extends SignalLoop
$message_data = yield $buffer->bufferRead($message_length);
$left = $payload_length - $message_length - 4 - 8 - 8;
if ($left) {
$API->logger->logger('Padded unencrypted message', \danog\MadelineProto\Logger::ULTRA_VERBOSE);
$API->logger->logger('Padded unencrypted message', Logger::ULTRA_VERBOSE);
if ($left < (-$message_length & 15)) {
$API->logger->logger('Protocol padded unencrypted message', \danog\MadelineProto\Logger::ULTRA_VERBOSE);
$API->logger->logger('Protocol padded unencrypted message', Logger::ULTRA_VERBOSE);
}
yield $buffer->bufferRead($left);
}
$connection->incoming_messages[$message_id] = [];
} elseif ($auth_key_id === $shared->getTempAuthKey()->getID()) {
$message_key = yield $buffer->bufferRead(16);
list($aes_key, $aes_iv) = $this->aesCalculate($message_key, $shared->getTempAuthKey()->getAuthKey(), false);
list($aes_key, $aes_iv) = Crypt::aesCalculate($message_key, $shared->getTempAuthKey()->getAuthKey(), false);
$encrypted_data = yield $buffer->bufferRead($payload_length - 24);
$protocol_padding = \strlen($encrypted_data) % 16;
if ($protocol_padding) {
$encrypted_data = \substr($encrypted_data, 0, -$protocol_padding);
}
$decrypted_data = $this->igeDecrypt($encrypted_data, $aes_key, $aes_iv);
$decrypted_data = Crypt::igeDecrypt($encrypted_data, $aes_key, $aes_iv);
/*
$server_salt = substr($decrypted_data, 0, 8);
if ($server_salt != $shared->getTempAuthKey()->getServerSalt()) {
$API->logger->logger('WARNING: Server salt mismatch (my server salt '.$shared->getTempAuthKey()->getServerSalt().' is not equal to server server salt '.$server_salt.').', \danog\MadelineProto\Logger::WARNING);
$API->logger->logger('WARNING: Server salt mismatch (my server salt '.$shared->getTempAuthKey()->getServerSalt().' is not equal to server server salt '.$server_salt.').', Logger::WARNING);
}
*/
$session_id = \substr($decrypted_data, 8, 8);
@ -210,26 +188,36 @@ class ReadLoop extends SignalLoop
throw new \danog\MadelineProto\SecurityException('message_data_length not divisible by 4');
}
$message_data = \substr($decrypted_data, 32, $message_data_length);
if ($message_key != \substr(\hash('sha256', \substr($shared->getTempAuthKey()->getAuthKey(), 96, 32) . $decrypted_data, true), 8, 16)) {
if ($message_key != \substr(\hash('sha256', \substr($shared->getTempAuthKey()->getAuthKey(), 96, 32).$decrypted_data, true), 8, 16)) {
throw new \danog\MadelineProto\SecurityException('msg_key mismatch');
}
$connection->incoming_messages[$message_id] = ['seq_no' => $seq_no];
} else {
$API->logger->logger('Got unknown auth_key id', \danog\MadelineProto\Logger::ERROR);
$API->logger->logger('Got unknown auth_key id', Logger::ERROR);
return -404;
}
$deserialized = $API->getTL()->deserialize($message_data, ['type' => '', 'connection' => $connection]);
$API->referenceDatabase->reset();
$connection->incoming_messages[$message_id]['content'] = $deserialized;
$connection->incoming_messages[$message_id]['response'] = -1;
$connection->new_incoming[$message_id] = $message_id;
//$connection->last_http_wait = 0;
$API->logger->logger('Received payload from DC ' . $datacenter, \danog\MadelineProto\Logger::ULTRA_VERBOSE);
[$deserialized, $sideEffects] = $API->getTL()->deserialize($message_data, ['type' => '', 'connection' => $connection]);
if (isset($API->referenceDatabase)) {
$API->referenceDatabase->reset();
}
$message = new IncomingMessage($deserialized, $message_id);
if (isset($seq_no)) {
$message->setSeqNo($seq_no);
}
if ($sideEffects) {
$message->setSideEffects($sideEffects);
}
$connection->new_incoming[$message_id] = $connection->incoming_messages[$message_id] = $message;
$API->logger->logger('Received payload from DC '.$datacenter, Logger::ULTRA_VERBOSE);
} finally {
$connection->reading(false);
}
return true;
}
/**
* Get loop name.
*
* @return string
*/
public function __toString(): string
{
return "read loop in DC {$this->datacenter}";

View File

@ -20,9 +20,10 @@
namespace danog\MadelineProto\Loop\Connection;
use Amp\ByteStream\StreamException;
use danog\MadelineProto\Connection;
use Amp\Loop;
use danog\Loop\ResumableSignalLoop;
use danog\MadelineProto\Logger;
use danog\MadelineProto\Loop\Impl\ResumableSignalLoop;
use danog\MadelineProto\MTProto\Container;
use danog\MadelineProto\MTProtoTools\Crypt;
use danog\MadelineProto\Tools;
@ -33,34 +34,16 @@ use danog\MadelineProto\Tools;
*/
class WriteLoop extends ResumableSignalLoop
{
use Crypt;
use Tools;
const MAX_COUNT = 1020;
private const MAX_SIZE = 1 << 15;
const MAX_IDS = 8192;
use Common;
/**
* Connection instance.
* Main loop.
*
* @var \danog\MadelineProto\Connection
* @return \Generator
*/
protected $connection;
/**
* DataCenterConnection instance.
*
* @var \danog\MadelineProto\DataCenterConnection
*/
protected $datacenterConnection;
/**
* DC ID.
*
* @var string
*/
protected $datacenter;
public function __construct(Connection $connection)
{
$this->connection = $connection;
$this->datacenterConnection = $connection->getShared();
$this->API = $connection->getExtra();
$ctx = $connection->getCtx();
$this->datacenter = $connection->getDatacenterID();
}
public function loop(): \Generator
{
$API = $this->API;
@ -69,7 +52,7 @@ class WriteLoop extends ResumableSignalLoop
$datacenter = $this->datacenter;
$please_wait = false;
while (true) {
while (empty($connection->pending_outgoing) || $please_wait) {
while (empty($connection->pendingOutgoing) || $please_wait) {
if ($connection->shouldReconnect()) {
$API->logger->logger('Not writing because connection is old');
return;
@ -88,7 +71,7 @@ class WriteLoop extends ResumableSignalLoop
}
$connection->writing(true);
try {
$please_wait = yield $this->{$shared->hasTempAuthKey() ? 'encryptedWriteLoop' : 'unencryptedWriteLoop'}();
$please_wait = yield from $this->{$shared->hasTempAuthKey() ? 'encryptedWriteLoop' : 'unencryptedWriteLoop'}();
} catch (StreamException $e) {
if ($connection->shouldReconnect()) {
return;
@ -111,35 +94,35 @@ class WriteLoop extends ResumableSignalLoop
$datacenter = $this->datacenter;
$connection = $this->connection;
$shared = $this->datacenterConnection;
while ($connection->pending_outgoing) {
while ($connection->pendingOutgoing) {
$skipped_all = true;
foreach ($connection->pending_outgoing as $k => $message) {
foreach ($connection->pendingOutgoing as $k => $message) {
if ($shared->hasTempAuthKey()) {
return;
}
if (!$message['unencrypted']) {
if ($message->isEncrypted()) {
continue;
}
$skipped_all = false;
$API->logger->logger("Sending {$message['_']} as unencrypted message to DC {$datacenter}", \danog\MadelineProto\Logger::ULTRA_VERBOSE);
$message_id = isset($message['msg_id']) ? $message['msg_id'] : $connection->msgIdHandler->generateMessageId();
$length = \strlen($message['serialized_body']);
$API->logger->logger("Sending $message as unencrypted message to DC $datacenter", \danog\MadelineProto\Logger::ULTRA_VERBOSE);
$message_id = $message->getMsgId() ?? $connection->msgIdHandler->generateMessageId();
$length = \strlen($message->getSerializedBody());
$pad_length = -$length & 15;
$pad_length += 16 * \danog\MadelineProto\Tools::randomInt($modulus = 16);
$pad = \danog\MadelineProto\Tools::random($pad_length);
$buffer = yield $connection->stream->getWriteBuffer(8 + 8 + 4 + $pad_length + $length);
yield $buffer->bufferWrite("\0\0\0\0\0\0\0\0" . $message_id . \danog\MadelineProto\Tools::packUnsignedInt($length) . $message['serialized_body'] . $pad);
yield $buffer->bufferWrite("\0\0\0\0\0\0\0\0".$message_id.\danog\MadelineProto\Tools::packUnsignedInt($length).$message->getSerializedBody().$pad);
//var_dump("plain ".bin2hex($message_id));
$connection->httpSent();
$API->logger->logger("Sent $message as unencrypted message to DC $datacenter!", \danog\MadelineProto\Logger::ULTRA_VERBOSE);
unset($connection->pendingOutgoing[$k]);
$message->setMsgId($message_id);
$connection->outgoing_messages[$message_id] = $message;
$connection->outgoing_messages[$message_id]['sent'] = \time();
$connection->outgoing_messages[$message_id]['tries'] = 0;
$connection->outgoing_messages[$message_id]['unencrypted'] = true;
$connection->new_outgoing[$message_id] = $message_id;
unset($connection->pending_outgoing[$k]);
$API->logger->logger("Sent {$message['_']} as unencrypted message to DC {$datacenter}!", \danog\MadelineProto\Logger::ULTRA_VERBOSE);
$message['send_promise']->resolve(isset($message['promise']) ? $message['promise'] : true);
unset($message['send_promise']);
$connection->new_outgoing[$message_id] = $message;
$message->sent();
}
if ($skipped_all) {
return true;
@ -156,88 +139,102 @@ class WriteLoop extends ResumableSignalLoop
if (!$shared->hasTempAuthKey()) {
return;
}
if ($shared->isHttp() && empty($connection->pending_outgoing)) {
if ($shared->isHttp() && empty($connection->pendingOutgoing)) {
return;
}
$temporary_keys = [];
if (\count($to_ack = $connection->ack_queue)) {
foreach (\array_chunk($connection->ack_queue, 8192) as $acks) {
$connection->pending_outgoing[$connection->pending_outgoing_key] = ['_' => 'msgs_ack', 'serialized_body' => yield from $this->API->getTL()->serializeObject(['type' => ''], ['_' => 'msgs_ack', 'msg_ids' => $acks], 'msgs_ack'), 'contentRelated' => false, 'unencrypted' => false, 'method' => false];
$temporary_keys[$connection->pending_outgoing_key] = true;
$API->logger->logger("Adding msgs_ack {$connection->pending_outgoing_key}", Logger::ULTRA_VERBOSE);
$connection->pending_outgoing_key++;
}
}
$has_http_wait = false;
\ksort($connection->pendingOutgoing);
$messages = [];
$keys = [];
if ($shared->isHttp()) {
foreach ($connection->pending_outgoing as $message) {
if ($message['_'] === 'http_wait') {
$has_http_wait = true;
break;
}
}
if (!$has_http_wait) {
$API->logger->logger("Adding http_wait {$connection->pending_outgoing_key}", Logger::ULTRA_VERBOSE);
$connection->pending_outgoing[$connection->pending_outgoing_key] = ['_' => 'http_wait', 'serialized_body' => yield from $this->API->getTL()->serializeObject(['type' => ''], ['_' => 'http_wait', 'max_wait' => 30000, 'wait_after' => 0, 'max_delay' => 0], 'http_wait'), 'contentRelated' => true, 'unencrypted' => false, 'method' => true];
$temporary_keys[$connection->pending_outgoing_key] = true;
$connection->pending_outgoing_key++;
}
}
$total_length = 0;
$count = 0;
\ksort($connection->pending_outgoing);
$skipped = false;
$inited = false;
foreach ($connection->pending_outgoing as $k => $message) {
if ($message['unencrypted']) {
$has_seq = false;
$has_state = false;
$has_resend = false;
$has_http_wait = false;
foreach ($connection->pendingOutgoing as $k => $message) {
if ($message->isUnencrypted()) {
continue;
}
if (isset($message['container'])) {
unset($connection->pending_outgoing[$k]);
if ($message instanceof Container) {
unset($connection->pendingOutgoing[$k]);
continue;
}
if ($shared->getSettings()['pfs'] && !$shared->isBound() && !$connection->isCDN() && !\in_array($message['_'], ['http_wait', 'auth.bindTempAuthKey']) && $message['method']) {
$API->logger->logger("Skipping {$message['_']} due to unbound keys in DC {$datacenter}");
$constructor = $message->getConstructor();
if ($shared->getGenericSettings()->getAuth()->getPfs() && !$shared->isBound() && !$connection->isCDN() && $message->isMethod() && !\in_array($constructor, ['http_wait', 'auth.bindTempAuthKey'])) {
$API->logger->logger("Skipping $message due to unbound keys in DC $datacenter");
$skipped = true;
continue;
}
$body_length = \strlen($message['serialized_body']);
if ($constructor === 'http_wait') {
$has_http_wait = true;
}
if ($constructor === 'msgs_state_req') {
if ($has_state) {
$API->logger->logger("Already have a state request queued for the current container in DC {$datacenter}");
continue;
}
$has_state = true;
}
if ($constructor === 'msg_resend_req') {
if ($has_resend) {
continue;
}
$has_resend = true;
}
$body_length = \strlen($message->getSerializedBody());
$actual_length = $body_length + 32;
if ($total_length && $total_length + $actual_length > 32760 || $count >= 1020) {
$API->logger->logger('Length overflow, postponing part of payload', \danog\MadelineProto\Logger::ULTRA_VERBOSE);
break;
}
$message_id = isset($message['msg_id']) ? $message['msg_id'] : $connection->msgIdHandler->generateMessageId();
$API->logger->logger("Sending {$message['_']} as encrypted message to DC {$datacenter}", \danog\MadelineProto\Logger::ULTRA_VERBOSE);
$MTmessage = ['_' => 'MTmessage', 'msg_id' => $message_id, 'body' => $message['serialized_body'], 'seqno' => $connection->generateOutSeqNo($message['contentRelated'])];
if (isset($message['method']) && $message['method'] && $message['_'] !== 'http_wait') {
if (!$shared->getTempAuthKey()->isInited() && $message['_'] !== 'auth.bindTempAuthKey' && !$inited) {
if ($message->hasSeqNo()) {
$has_seq = true;
}
$message_id = $message->getMsgId() ?? $connection->msgIdHandler->generateMessageId();
$API->logger->logger("Sending $message as encrypted message to DC $datacenter", \danog\MadelineProto\Logger::ULTRA_VERBOSE);
$MTmessage = [
'_' => 'MTmessage',
'msg_id' => $message_id,
'body' => $message->getSerializedBody(),
'seqno' => $message->getSeqNo() ?? $connection->generateOutSeqNo($message->isContentRelated())
];
if ($message->isMethod() && $constructor !== 'http_wait') {
if (!$shared->getTempAuthKey()->isInited() && $constructor !== 'auth.bindTempAuthKey' && !$inited) {
$inited = true;
$API->logger->logger(\sprintf(\danog\MadelineProto\Lang::$current_lang['write_client_info'], $message['_']), \danog\MadelineProto\Logger::NOTICE);
$MTmessage['body'] = (yield from $API->getTL()->serializeMethod('invokeWithLayer', ['layer' => $API->settings['tl_schema']['layer'], 'query' => yield from $API->getTL()->serializeMethod('initConnection', ['api_id' => $API->settings['app_info']['api_id'], 'api_hash' => $API->settings['app_info']['api_hash'], 'device_model' => !$connection->isCDN() ? $API->settings['app_info']['device_model'] : 'n/a', 'system_version' => !$connection->isCDN() ? $API->settings['app_info']['system_version'] : 'n/a', 'app_version' => $API->settings['app_info']['app_version'], 'system_lang_code' => $API->settings['app_info']['lang_code'], 'lang_code' => $API->settings['app_info']['lang_code'], 'lang_pack' => $API->settings['app_info']['lang_pack'], 'proxy' => $connection->getCtx()->getInputClientProxy(), 'query' => $MTmessage['body']])]));
$API->logger->logger(\sprintf(\danog\MadelineProto\Lang::$current_lang['write_client_info'], $constructor), \danog\MadelineProto\Logger::NOTICE);
$MTmessage['body'] = (yield from $API->getTL()->serializeMethod('invokeWithLayer', ['layer' => $API->settings->getSchema()->getLayer(), 'query' => yield from $API->getTL()->serializeMethod('initConnection', ['api_id' => $API->settings->getAppInfo()->getApiId(), 'api_hash' => $API->settings->getAppInfo()->getApiHash(), 'device_model' => !$connection->isCDN() ? $API->settings->getAppInfo()->getDeviceModel() : 'n/a', 'system_version' => !$connection->isCDN() ? $API->settings->getAppInfo()->getSystemVersion() : 'n/a', 'app_version' => $API->settings->getAppInfo()->getAppVersion(), 'system_lang_code' => $API->settings->getAppInfo()->getLangCode(), 'lang_code' => $API->settings->getAppInfo()->getLangCode(), 'lang_pack' => $API->settings->getAppInfo()->getLangPack(), 'proxy' => $connection->getCtx()->getInputClientProxy(), 'query' => $MTmessage['body']])]));
} else {
if (isset($message['queue'])) {
if (!isset($connection->call_queue[$message['queue']])) {
$connection->call_queue[$message['queue']] = [];
if ($message->hasQueue()) {
$queueId = $message->getQueueId();
if (!isset($connection->call_queue[$queueId])) {
$connection->call_queue[$queueId] = [];
}
$MTmessage['body'] = (yield from $API->getTL()->serializeMethod('invokeAfterMsgs', ['msg_ids' => $connection->call_queue[$message['queue']], 'query' => $MTmessage['body']]));
$connection->call_queue[$message['queue']][$message_id] = $message_id;
if (\count($connection->call_queue[$message['queue']]) > $API->settings['msg_array_limit']['call_queue']) {
\reset($connection->call_queue[$message['queue']]);
$key = \key($connection->call_queue[$message['queue']]);
unset($connection->call_queue[$message['queue']][$key]);
$MTmessage['body'] = (yield from $API->getTL()->serializeMethod('invokeAfterMsgs', ['msg_ids' => $connection->call_queue[$queueId], 'query' => $MTmessage['body']]));
$connection->call_queue[$queueId][$message_id] = $message_id;
if (\count($connection->call_queue[$queueId]) > $API->settings->getRpc()->getLimitCallQueue()) {
\reset($connection->call_queue[$queueId]);
$key = \key($connection->call_queue[$queueId]);
unset($connection->call_queue[$queueId][$key]);
}
}
// TODO
/* if ($API->settings['requests']['gzip_encode_if_gt'] !== -1 && ($l = strlen($MTmessage['body'])) > $API->settings['requests']['gzip_encode_if_gt']) {
if (($g = strlen($gzipped = gzencode($MTmessage['body']))) < $l) {
$MTmessage['body'] = yield $API->getTL()->serializeObject(['type' => ''], ['_' => 'gzip_packed', 'packed_data' => $gzipped], 'gzipped data');
$API->logger->logger('Using GZIP compression for ' . $message['_'] . ', saved ' . ($l - $g) . ' bytes of data, reduced call size by ' . $g * 100 / $l . '%', \danog\MadelineProto\Logger::ULTRA_VERBOSE);
}
unset($gzipped);
}*/
/*
if ($API->settings['requests']['gzip_encode_if_gt'] !== -1 && ($l = strlen($MTmessage['body'])) > $API->settings['requests']['gzip_encode_if_gt']) {
if (($g = strlen($gzipped = gzencode($MTmessage['body']))) < $l) {
$MTmessage['body'] = yield $API->getTL()->serializeObject(['type' => ''], ['_' => 'gzip_packed', 'packed_data' => $gzipped], 'gzipped data');
$API->logger->logger('Using GZIP compression for ' . $constructor . ', saved ' . ($l - $g) . ' bytes of data, reduced call size by ' . $g * 100 / $l . '%', \danog\MadelineProto\Logger::ULTRA_VERBOSE);
}
unset($gzipped);
}*/
}
}
$body_length = \strlen($MTmessage['body']);
@ -251,22 +248,47 @@ class WriteLoop extends ResumableSignalLoop
$MTmessage['bytes'] = $body_length;
$messages[] = $MTmessage;
$keys[$k] = $message_id;
}
if ($shared->isHttp() && $skipped && $count === \count($temporary_keys)) {
foreach ($temporary_keys as $key => $true) {
$API->logger->logger("Removing temporary {$connection->pending_outgoing[$key]['_']} by {$key}", Logger::ULTRA_VERBOSE);
unset($connection->pending_outgoing[$key]);
$count--;
}
$message->setSeqNo($MTmessage['seqno'])
->setMsgId($MTmessage['msg_id']);
}
$MTmessage = null;
if ($count > 1) {
$acks = \array_slice($connection->ack_queue, 0, self::MAX_COUNT);
if ($ackCount = \count($acks)) {
$API->logger->logger("Adding msgs_ack", Logger::ULTRA_VERBOSE);
$body = yield from $this->API->getTL()->serializeObject(['type' => ''], ['_' => 'msgs_ack', 'msg_ids' => $acks], 'msgs_ack');
$messages []= [
'_' => 'MTmessage',
'msg_id' => $connection->msgIdHandler->generateMessageId(),
'body' => $body,
'seqno' => $connection->generateOutSeqNo(false),
'bytes' => \strlen($body)
];
$count++;
unset($acks, $body);
}
if ($shared->isHttp() && !$has_http_wait) {
$API->logger->logger("Adding http_wait", Logger::ULTRA_VERBOSE);
$body = yield from $this->API->getTL()->serializeObject(['type' => ''], ['_' => 'http_wait', 'max_wait' => 30000, 'wait_after' => 0, 'max_delay' => 0], 'http_wait');
$messages []= [
'_' => 'MTmessage',
'msg_id' => $connection->msgIdHandler->generateMessageId(),
'body' => $body,
'seqno' => $connection->generateOutSeqNo(true),
'bytes' => \strlen($body)
];
$count++;
unset($body);
}
if ($count > 1 || $has_seq) {
$API->logger->logger("Wrapping in msg_container ({$count} messages of total size {$total_length}) as encrypted message for DC {$datacenter}", \danog\MadelineProto\Logger::ULTRA_VERBOSE);
$message_id = $connection->msgIdHandler->generateMessageId();
$connection->pending_outgoing[$connection->pending_outgoing_key] = ['_' => 'msg_container', 'container' => \array_values($keys), 'contentRelated' => false, 'method' => false, 'unencrypted' => false];
//var_dumP("container ".bin2hex($message_id));
$keys[$connection->pending_outgoing_key++] = $message_id;
$message_data = (yield from $API->getTL()->serializeObject(['type' => ''], ['_' => 'msg_container', 'messages' => $messages], 'container'));
$connection->pendingOutgoing[$connection->pendingOutgoingKey] = new Container(\array_values($keys));
$keys[$connection->pendingOutgoingKey++] = $message_id;
$message_data = yield from $API->getTL()->serializeObject(['type' => ''], ['_' => 'msg_container', 'messages' => $messages], 'container');
$message_data_length = \strlen($message_data);
$seq_no = $connection->generateOutSeqNo(false);
} elseif ($count) {
@ -280,45 +302,45 @@ class WriteLoop extends ResumableSignalLoop
return true;
}
unset($messages);
$plaintext = $shared->getTempAuthKey()->getServerSalt() . $connection->session_id . $message_id . \pack('VV', $seq_no, $message_data_length) . $message_data;
$plaintext = $shared->getTempAuthKey()->getServerSalt().$connection->session_id.$message_id.\pack('VV', $seq_no, $message_data_length).$message_data;
$padding = \danog\MadelineProto\Tools::posmod(-\strlen($plaintext), 16);
if ($padding < 12) {
$padding += 16;
}
$padding = \danog\MadelineProto\Tools::random($padding);
$message_key = \substr(\hash('sha256', \substr($shared->getTempAuthKey()->getAuthKey(), 88, 32) . $plaintext . $padding, true), 8, 16);
list($aes_key, $aes_iv) = $this->aesCalculate($message_key, $shared->getTempAuthKey()->getAuthKey());
$message = $shared->getTempAuthKey()->getID() . $message_key . $this->igeEncrypt($plaintext . $padding, $aes_key, $aes_iv);
$buffer = yield $connection->stream->getWriteBuffer($len = \strlen($message));
//$t = \microtime(true);
$message_key = \substr(\hash('sha256', \substr($shared->getTempAuthKey()->getAuthKey(), 88, 32).$plaintext.$padding, true), 8, 16);
list($aes_key, $aes_iv) = Crypt::aesCalculate($message_key, $shared->getTempAuthKey()->getAuthKey());
$message = $shared->getTempAuthKey()->getID().$message_key.Crypt::igeEncrypt($plaintext.$padding, $aes_key, $aes_iv);
$buffer = yield $connection->stream->getWriteBuffer(\strlen($message));
yield $buffer->bufferWrite($message);
$connection->httpSent();
$API->logger->logger("Sent encrypted payload to DC {$datacenter}", \danog\MadelineProto\Logger::ULTRA_VERBOSE);
$sent = \time();
if ($to_ack) {
$connection->ack_queue = [];
if ($ackCount) {
$connection->ack_queue = \array_slice($connection->ack_queue, $ackCount);
}
foreach ($keys as $key => $message_id) {
$connection->outgoing_messages[$message_id] =& $connection->pending_outgoing[$key];
if (isset($connection->outgoing_messages[$message_id]['promise'])) {
$connection->new_outgoing[$message_id] = $message_id;
$connection->outgoing_messages[$message_id]['sent'] = $sent;
$connection->outgoing_messages[$message_id]['tries'] = 0;
$message = $connection->pendingOutgoing[$key];
unset($connection->pendingOutgoing[$key]);
$connection->outgoing_messages[$message_id] = $message;
if ($message->hasPromise()) {
$connection->new_outgoing[$message_id] = $message;
}
if (isset($connection->outgoing_messages[$message_id]['send_promise'])) {
$connection->outgoing_messages[$message_id]['send_promise']->resolve(isset($connection->outgoing_messages[$message_id]['promise']) ? $connection->outgoing_messages[$message_id]['promise'] : true);
unset($connection->outgoing_messages[$message_id]['send_promise']);
}
//var_dumP("encrypted ".bin2hex($message_id)." ".$connection->outgoing_messages[$message_id]['_']);
unset($connection->pending_outgoing[$key]);
$message->sent();
}
//if (!empty($connection->pending_outgoing)) $connection->select();
} while (!empty($connection->pending_outgoing) && !$skipped);
if (empty($connection->pending_outgoing)) {
$connection->pending_outgoing_key = 'a';
} while ($connection->pendingOutgoing && !$skipped);
if (empty($connection->pendingOutgoing)) {
$connection->pendingOutgoingKey = 'a';
}
return $skipped;
}
/**
* Get loop name.
*
* @return string
*/
public function __toString(): string
{
return "write loop in DC {$this->datacenter}";

View File

@ -1,7 +1,7 @@
<?php
/**
* Generic loop.
* Update feeder loop.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@ -19,57 +19,30 @@
namespace danog\MadelineProto\Loop\Generic;
use danog\MadelineProto\Loop\Impl\ResumableSignalLoop;
use danog\Loop\Generic\GenericLoop as GenericGenericLoop;
use danog\MadelineProto\InternalDoc;
use danog\MadelineProto\Loop\APILoop;
/**
* Generic loop.
* {@inheritDoc}
*
* @author Daniil Gentili <daniil@daniil.it>
* @deprecated Use the danog/loop API instead
*/
class GenericLoop extends ResumableSignalLoop
class GenericLoop extends GenericGenericLoop
{
const STOP = -1;
const PAUSE = null;
const CONTINUE = 0;
protected $callback;
protected $name;
use APILoop {
__construct as private init;
}
/**
* Constructor.
*
* The callback will be bound to the GenericLoop instance: this means that you will be able to use `$this` as if the callback were actually the `loop` function (you can access the API property, use the pause/waitSignal methods & so on).
* The return value of the callable can be:
* A number - the loop will be paused for the specified number of seconds
* GenericLoop::STOP - The loop will stop
* GenericLoop::PAUSE - The loop will pause forever (or until the `resume` method is called on the loop object from outside the loop)
* GenericLoop::CONTINUE - Return this if you want to rerun the loop without waiting
*
* @param \danog\MadelineProto\API $API Instance of MadelineProto
* @param callable $callback Callback to run
* @param string $name Fetcher name
* @param InternalDoc $API API instance
* @param callable $callable Method
* @param string $name Loop name
*/
public function __construct($API, $callback, $name)
public function __construct(InternalDoc $API, callable $callable, string $name)
{
$this->API = $API;
$this->callback = $callback->bindTo($this);
$this->name = $name;
}
public function loop(): \Generator
{
$callback = $this->callback;
while (true) {
$timeout = yield $callback();
if ($timeout === self::PAUSE) {
$this->API->logger->logger("Pausing {$this}", \danog\MadelineProto\Logger::VERBOSE);
} elseif ($timeout > 0) {
$this->API->logger->logger("Pausing {$this} for {$timeout}", \danog\MadelineProto\Logger::VERBOSE);
}
if ($timeout === self::STOP || yield $this->waitSignal($this->pause($timeout))) {
return;
}
}
}
public function __toString(): string
{
return $this->name;
$this->init($API);
parent::__construct($callable, $name);
}
}

View File

@ -1,7 +1,7 @@
<?php
/**
* Periodic loop.
* Update feeder loop.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@ -19,64 +19,31 @@
namespace danog\MadelineProto\Loop\Generic;
use danog\MadelineProto\Loop\Impl\ResumableSignalLoop;
use danog\MadelineProto\MTProto;
use danog\Loop\Generic\PeriodicLoop as GenericPeriodicLoop;
use danog\MadelineProto\InternalDoc;
use danog\MadelineProto\Loop\APILoop;
/**
* Periodic loop.
* {@inheritDoc}
*
* @author Daniil Gentili <daniil@daniil.it>
* @deprecated Use the danog/loop API instead
*/
class PeriodicLoop extends ResumableSignalLoop
class PeriodicLoop extends GenericPeriodicLoop
{
/**
* Callback.
*
* @var callable
*/
private $callback;
/**
* Loop name.
*
* @var string
*/
private string $name;
/**
* Loop timeeout.
*
* @var int
*/
private $timeout;
use APILoop {
__construct as private init;
}
/**
* Constructor.
*
* @param \danog\MadelineProto\API $API Instance of MTProto class
* @param callable $callback Callback to call
* @param string $name Loop name
* @param int|float $timeout Loop timeout
* @param InternalDoc $API API instance
* @param callable $callable Method
* @param string $name Loop name
* @param ?int $interval Interval
*/
public function __construct($API, callable $callback, string $name, $timeout)
public function __construct(InternalDoc $API, callable $callable, string $name, ?int $interval)
{
$this->API = $API;
$this->callback = $callback;
$this->name = $name;
$this->timeout = $timeout;
}
public function loop(): \Generator
{
$callback = $this->callback;
$logger = $this->API->logger;
while (true) {
$result = yield $this->waitSignal($this->pause($this->timeout));
if ($result) {
$logger->logger("Got signal in {$this}, exiting");
return;
}
yield $callback();
}
}
public function __toString(): string
{
return $this->name;
$this->init($API);
parent::__construct($callable, $name, $interval === null ? $interval : $interval * 1000);
}
}

View File

@ -0,0 +1,49 @@
<?php
/**
* Update feeder loop.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
*
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\MadelineProto\Loop\Generic;
use danog\Loop\Generic\PeriodicLoop as GenericPeriodicLoop;
use danog\MadelineProto\Loop\InternalLoop;
use danog\MadelineProto\MTProto;
/**
* {@inheritDoc}
*
* @internal For internal use
*/
class PeriodicLoopInternal extends GenericPeriodicLoop
{
use InternalLoop {
__construct as private init;
}
/**
* Constructor.
*
* @param MTProto $API API instance
* @param callable $callable Method
* @param string $name Loop name
* @param int|null $interval Interval
*/
public function __construct(MTProto $API, callable $callable, string $name, ?int $interval)
{
$this->init($API);
parent::__construct($callable, $name, $interval);
}
}

View File

@ -1,81 +0,0 @@
<?php
/**
* Loop helper trait.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
*
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\MadelineProto\Loop\Impl;
use Amp\Promise;
use danog\MadelineProto\Logger;
use danog\MadelineProto\Loop\LoopInterface;
/**
* Loop helper trait.
*
* Wraps the asynchronous generator methods with asynchronous promise-based methods
*
* @author Daniil Gentili <daniil@daniil.it>
*/
abstract class Loop implements LoopInterface
{
use \danog\MadelineProto\Tools;
private $count = 0;
/**
* MTProto instance.
*
* @var \danog\MadelineProto\MTProto
*/
public $API;
public function __construct($API)
{
$this->API = $API;
}
public function start()
{
if ($this->count) {
//$this->API->logger->logger("NOT entering $this with running count {$this->count}", Logger::ERROR);
return false;
}
return \danog\MadelineProto\Tools::callFork($this->loopImpl());
}
private function loopImpl(): \Generator
{
$this->startedLoop();
$this->API->logger->logger("Entered {$this}", Logger::ULTRA_VERBOSE);
try {
yield from $this->loop();
} finally {
$this->exitedLoop();
$this->API->logger->logger("Physically exited {$this}", Logger::ULTRA_VERBOSE);
}
}
public function exitedLoop()
{
if ($this->count) {
$this->API->logger->logger("Exited {$this}", Logger::ULTRA_VERBOSE);
$this->count--;
}
}
public function startedLoop()
{
$this->count++;
}
public function isRunning()
{
return $this->count;
}
}

View File

@ -1,92 +0,0 @@
<?php
/**
* Loop helper trait.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
*
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\MadelineProto\Loop\Impl;
use Amp\Deferred;
use Amp\Loop;
use Amp\Promise;
use Amp\Success;
use danog\MadelineProto\Loop\ResumableLoopInterface;
use danog\MadelineProto\Tools;
/**
* Resumable signal loop helper trait.
*
* @author Daniil Gentili <daniil@daniil.it>
*/
abstract class ResumableSignalLoop extends SignalLoop implements ResumableLoopInterface
{
use Tools;
private $resume;
private $pause;
protected $resumeWatcher;
public function pause($time = null): Promise
{
if (!\is_null($time)) {
if ($time <= 0) {
return new Success(0);
}
$resume = \microtime(true) + $time;
if ($this->resumeWatcher) {
Loop::cancel($this->resumeWatcher);
$this->resumeWatcher = null;
}
$this->resumeWatcher = Loop::delay((int) ($time * 1000), [$this, 'resume'], $resume);
}
$this->resume = new Deferred();
$pause = $this->pause;
$this->pause = new Deferred();
if ($pause) {
Loop::defer([$pause, 'resolve']);
}
return $this->resume->promise();
}
public function resume($watcherId = null, $expected = 0)
{
if ($this->resumeWatcher) {
$storedWatcherId = $this->resumeWatcher;
Loop::cancel($storedWatcherId);
$this->resumeWatcher = null;
if ($watcherId && $storedWatcherId !== $watcherId) {
return;
}
}
if ($this->resume) {
$resume = $this->resume;
$this->resume = null;
$resume->resolve();
return $this->pause ? $this->pause->promise() : null;
}
}
public function resumeDefer()
{
Loop::defer([$this, 'resume']);
return $this->pause ? $this->pause->promise() : null;
}
public function exitedLoop()
{
parent::exitedLoop();
if ($this->resumeWatcher) {
Loop::cancel($this->resumeWatcher);
$this->resumeWatcher = null;
}
}
}

View File

@ -1,63 +0,0 @@
<?php
/**
* Loop helper trait.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
*
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\MadelineProto\Loop\Impl;
use Amp\Deferred;
use Amp\Promise;
use danog\MadelineProto\Coroutine;
use danog\MadelineProto\Loop\SignalLoopInterface;
/**
* Signal loop helper trait.
*
* @author Daniil Gentili <daniil@daniil.it>
*/
abstract class SignalLoop extends Loop implements SignalLoopInterface
{
private $signalDeferred;
public function signal($what)
{
if ($this->signalDeferred) {
$deferred = $this->signalDeferred;
$this->signalDeferred = null;
if ($what instanceof \Exception || $what instanceof \Throwable) {
$deferred->fail($what);
} else {
$deferred->resolve($what);
}
}
}
public function waitSignal($promise): Promise
{
if ($promise instanceof \Generator) {
$promise = new Coroutine($promise);
}
$this->signalDeferred = new Deferred();
$dpromise = $this->signalDeferred->promise();
$promise->onResolve(function () use ($promise) {
if ($this->signalDeferred !== null) {
$deferred = $this->signalDeferred;
$this->signalDeferred = null;
$deferred->resolve($promise);
}
});
return $dpromise;
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* Internal loop trait.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
*
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\MadelineProto\Loop;
use danog\MadelineProto\MTProto;
trait InternalLoop
{
use LoggerLoop {
__construct as private setLogger;
}
/**
* API instance.
*/
protected MTProto $API;
/**
* Constructor.
*
* @param MTProto $API API instance
*/
public function __construct(MTProto $API)
{
$this->API = $API;
$this->setLogger($API->getLogger());
}
}

Some files were not shown because too many files have changed in this diff Show More