spluspy 2.2.0__tar.gz → 2.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. spluspy-2.2.2/PKG-INFO +3290 -0
  2. spluspy-2.2.2/README.md +3232 -0
  3. spluspy-2.2.2/docs/index.html +1392 -0
  4. spluspy-2.2.2/docs/logo.svg +0 -0
  5. {spluspy-2.2.0 → spluspy-2.2.2}/pyproject.toml +2 -2
  6. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/__init__.py +2 -0
  7. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/__version__.py +1 -1
  8. spluspy-2.2.2/spluspy/_engine/crypto/aesctr.py +70 -0
  9. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/crypto/factorization.py +4 -4
  10. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/crypto/libssl.py +11 -0
  11. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/crypto/rsa.py +8 -0
  12. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/extensions/binaryreader.py +15 -3
  13. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/network/connection/connection.py +24 -0
  14. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/network/connection/tcpfull.py +2 -0
  15. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/network/connection/tcpmtproxy.py +2 -1
  16. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/network/connection/tcpobfuscated.py +2 -1
  17. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/tlobject.py +3 -0
  18. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/version.py +1 -1
  19. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/admin.py +3 -2
  20. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/client/chat_mixin.py +290 -1
  21. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/client/client.py +548 -30
  22. spluspy-2.2.2/spluspy/client/media_mixin.py +1021 -0
  23. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/config.py +2 -2
  24. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/filters/filters.py +20 -0
  25. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/models/__init__.py +4 -0
  26. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/models/enums.py +2 -0
  27. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/models/media.py +32 -0
  28. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/models/message.py +188 -0
  29. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/network/transport.py +12 -0
  30. spluspy-2.2.2/spluspy/py.typed +0 -0
  31. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/session/sqlite.py +6 -7
  32. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/storage/entity_cache.py +55 -0
  33. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/storage/sqlite.py +5 -4
  34. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/utils/file_transfer.py +92 -2
  35. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/utils/logger.py +117 -1
  36. spluspy-2.2.0/PKG-INFO +0 -1406
  37. spluspy-2.2.0/README.md +0 -1349
  38. spluspy-2.2.0/spluspy/_engine/crypto/aesctr.py +0 -42
  39. spluspy-2.2.0/spluspy/client/media_mixin.py +0 -531
  40. {spluspy-2.2.0 → spluspy-2.2.2}/.gitignore +0 -0
  41. {spluspy-2.2.0 → spluspy-2.2.2}/CHANGELOG.md +0 -0
  42. {spluspy-2.2.0 → spluspy-2.2.2}/CONTRIBUTING.md +0 -0
  43. {spluspy-2.2.0 → spluspy-2.2.2}/Dockerfile +0 -0
  44. {spluspy-2.2.0 → spluspy-2.2.2}/LICENSE +0 -0
  45. {spluspy-2.2.0 → spluspy-2.2.2}/MANIFEST.in +0 -0
  46. {spluspy-2.2.0 → spluspy-2.2.2}/README.rst +0 -0
  47. /spluspy-2.2.0/spluspy/py.typed → /spluspy-2.2.2/docs/.nojekyll +0 -0
  48. {spluspy-2.2.0 → spluspy-2.2.2}/examples/01_hello_bot.py +0 -0
  49. {spluspy-2.2.0 → spluspy-2.2.2}/examples/02_echo_bot.py +0 -0
  50. {spluspy-2.2.0 → spluspy-2.2.2}/examples/03_command_bot.py +0 -0
  51. {spluspy-2.2.0 → spluspy-2.2.2}/examples/04_private_only.py +0 -0
  52. {spluspy-2.2.0 → spluspy-2.2.2}/examples/05_group_only.py +0 -0
  53. {spluspy-2.2.0 → spluspy-2.2.2}/examples/06_regex_bot.py +0 -0
  54. {spluspy-2.2.0 → spluspy-2.2.2}/examples/07_user_filter.py +0 -0
  55. {spluspy-2.2.0 → spluspy-2.2.2}/examples/08_combined_filters.py +0 -0
  56. {spluspy-2.2.0 → spluspy-2.2.2}/examples/09_inline_keyboard.py +0 -0
  57. {spluspy-2.2.0 → spluspy-2.2.2}/examples/10_reply_keyboard.py +0 -0
  58. {spluspy-2.2.0 → spluspy-2.2.2}/examples/11_message_actions.py +0 -0
  59. {spluspy-2.2.0 → spluspy-2.2.2}/examples/12_media_bot.py +0 -0
  60. {spluspy-2.2.0 → spluspy-2.2.2}/examples/13_fsm_bot.py +0 -0
  61. {spluspy-2.2.0 → spluspy-2.2.2}/examples/14_plugin_bot.py +0 -0
  62. {spluspy-2.2.0 → spluspy-2.2.2}/examples/15_middleware_bot.py +0 -0
  63. {spluspy-2.2.0 → spluspy-2.2.2}/examples/16_scheduler_bot.py +0 -0
  64. {spluspy-2.2.0 → spluspy-2.2.2}/examples/17_conversation.py +0 -0
  65. {spluspy-2.2.0 → spluspy-2.2.2}/examples/18_chat_management.py +0 -0
  66. {spluspy-2.2.0 → spluspy-2.2.2}/examples/19_event_types.py +0 -0
  67. {spluspy-2.2.0 → spluspy-2.2.2}/examples/20_sync_bot.py +0 -0
  68. {spluspy-2.2.0 → spluspy-2.2.2}/examples/21_album_handler.py +0 -0
  69. {spluspy-2.2.0 → spluspy-2.2.2}/examples/22_download_bot.py +0 -0
  70. {spluspy-2.2.0 → spluspy-2.2.2}/examples/23_url_button.py +0 -0
  71. {spluspy-2.2.0 → spluspy-2.2.2}/examples/24_string_session.py +0 -0
  72. {spluspy-2.2.0 → spluspy-2.2.2}/examples/25_error_handling.py +0 -0
  73. {spluspy-2.2.0 → spluspy-2.2.2}/examples/25_working_bot.py +0 -0
  74. {spluspy-2.2.0 → spluspy-2.2.2}/examples/26_message_entities.py +0 -0
  75. {spluspy-2.2.0 → spluspy-2.2.2}/examples/27_search_messages.py +0 -0
  76. {spluspy-2.2.0 → spluspy-2.2.2}/examples/28_poll_bot.py +0 -0
  77. {spluspy-2.2.0 → spluspy-2.2.2}/examples/29_dice_bot.py +0 -0
  78. {spluspy-2.2.0 → spluspy-2.2.2}/examples/30_cache_bot.py +0 -0
  79. {spluspy-2.2.0 → spluspy-2.2.2}/examples/31_reaction_bot.py +0 -0
  80. {spluspy-2.2.0 → spluspy-2.2.2}/examples/32_mark_read_bot.py +0 -0
  81. {spluspy-2.2.0 → spluspy-2.2.2}/examples/33_context_manager.py +0 -0
  82. {spluspy-2.2.0 → spluspy-2.2.2}/examples/34_user_info.py +0 -0
  83. {spluspy-2.2.0 → spluspy-2.2.2}/examples/35_chat_info.py +0 -0
  84. {spluspy-2.2.0 → spluspy-2.2.2}/examples/36_forward_copy.py +0 -0
  85. {spluspy-2.2.0 → spluspy-2.2.2}/examples/37_admin_bot.py +0 -0
  86. {spluspy-2.2.0 → spluspy-2.2.2}/examples/38_media_filters.py +0 -0
  87. {spluspy-2.2.0 → spluspy-2.2.2}/examples/39_status_bot.py +0 -0
  88. {spluspy-2.2.0 → spluspy-2.2.2}/examples/40_delete_detection.py +0 -0
  89. {spluspy-2.2.0 → spluspy-2.2.2}/examples/41_read_detection.py +0 -0
  90. {spluspy-2.2.0 → spluspy-2.2.2}/examples/42_raw_events.py +0 -0
  91. {spluspy-2.2.0 → spluspy-2.2.2}/examples/43_poll_handler.py +0 -0
  92. {spluspy-2.2.0 → spluspy-2.2.2}/examples/44_reaction_handler.py +0 -0
  93. {spluspy-2.2.0 → spluspy-2.2.2}/examples/45_typing_handler.py +0 -0
  94. {spluspy-2.2.0 → spluspy-2.2.2}/examples/46_get_messages.py +0 -0
  95. {spluspy-2.2.0 → spluspy-2.2.2}/examples/47_iter_messages.py +0 -0
  96. {spluspy-2.2.0 → spluspy-2.2.2}/examples/48_get_members.py +0 -0
  97. {spluspy-2.2.0 → spluspy-2.2.2}/examples/49_resolve_username.py +0 -0
  98. {spluspy-2.2.0 → spluspy-2.2.2}/examples/50_invoke.py +0 -0
  99. {spluspy-2.2.0 → spluspy-2.2.2}/setup.py +0 -0
  100. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/__init__.py +0 -0
  101. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/_updates/__init__.py +0 -0
  102. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/_updates/entitycache.py +0 -0
  103. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/_updates/messagebox.py +0 -0
  104. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/_updates/session.py +0 -0
  105. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/client/__init__.py +0 -0
  106. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/client/account.py +0 -0
  107. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/client/auth.py +0 -0
  108. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/client/bots.py +0 -0
  109. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/client/buttons.py +0 -0
  110. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/client/chats.py +0 -0
  111. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/client/dialogs.py +0 -0
  112. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/client/downloads.py +0 -0
  113. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/client/messageparse.py +0 -0
  114. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/client/messages.py +0 -0
  115. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/client/soroushclient.py +0 -0
  116. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/client/telegrambaseclient.py +0 -0
  117. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/client/updates.py +0 -0
  118. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/client/uploads.py +0 -0
  119. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/client/users.py +0 -0
  120. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/crypto/__init__.py +0 -0
  121. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/crypto/aes.py +0 -0
  122. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/crypto/authkey.py +0 -0
  123. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/crypto/cdndecrypter.py +0 -0
  124. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/custom.py +0 -0
  125. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/errors/__init__.py +0 -0
  126. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/errors/common.py +0 -0
  127. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/errors/rpcbaseerrors.py +0 -0
  128. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/errors/rpcerrorlist.py +0 -0
  129. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/events/__init__.py +0 -0
  130. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/events/album.py +0 -0
  131. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/events/callbackquery.py +0 -0
  132. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/events/chataction.py +0 -0
  133. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/events/common.py +0 -0
  134. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/events/inlinequery.py +0 -0
  135. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/events/messagedeleted.py +0 -0
  136. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/events/messageedited.py +0 -0
  137. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/events/messageread.py +0 -0
  138. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/events/newmessage.py +0 -0
  139. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/events/raw.py +0 -0
  140. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/events/userupdate.py +0 -0
  141. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/extensions/__init__.py +0 -0
  142. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/extensions/html.py +0 -0
  143. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/extensions/markdown.py +0 -0
  144. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/extensions/messagepacker.py +0 -0
  145. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/functions.py +0 -0
  146. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/helpers.py +0 -0
  147. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/hints.py +0 -0
  148. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/network/__init__.py +0 -0
  149. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/network/authenticator.py +0 -0
  150. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/network/connection/__init__.py +0 -0
  151. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/network/connection/http.py +0 -0
  152. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/network/connection/tcpabridged.py +0 -0
  153. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/network/connection/tcpintermediate.py +0 -0
  154. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/network/connection/websocket.py +0 -0
  155. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/network/mtprotoplainsender.py +0 -0
  156. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/network/mtprotosender.py +0 -0
  157. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/network/mtprotostate.py +0 -0
  158. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/network/requeststate.py +0 -0
  159. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/password.py +0 -0
  160. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/requestiter.py +0 -0
  161. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/sessions/__init__.py +0 -0
  162. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/sessions/abstract.py +0 -0
  163. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/sessions/memory.py +0 -0
  164. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/sessions/sqlite.py +0 -0
  165. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/sessions/string.py +0 -0
  166. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/sync.py +0 -0
  167. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/__init__.py +0 -0
  168. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/alltlobjects.py +0 -0
  169. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/core/__init__.py +0 -0
  170. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/core/gzippacked.py +0 -0
  171. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/core/messagecontainer.py +0 -0
  172. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/core/rpcresult.py +0 -0
  173. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/core/tlmessage.py +0 -0
  174. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/custom/__init__.py +0 -0
  175. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/custom/adminlogevent.py +0 -0
  176. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/custom/button.py +0 -0
  177. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/custom/chatgetter.py +0 -0
  178. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/custom/conversation.py +0 -0
  179. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/custom/dialog.py +0 -0
  180. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/custom/draft.py +0 -0
  181. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/custom/file.py +0 -0
  182. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/custom/forward.py +0 -0
  183. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/custom/inlinebuilder.py +0 -0
  184. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/custom/inlineresult.py +0 -0
  185. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/custom/inlineresults.py +0 -0
  186. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/custom/inputsizedfile.py +0 -0
  187. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/custom/message.py +0 -0
  188. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/custom/messagebutton.py +0 -0
  189. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/custom/participantpermissions.py +0 -0
  190. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/custom/qrlogin.py +0 -0
  191. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/custom/sendergetter.py +0 -0
  192. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/custom/types.py +0 -0
  193. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/__init__.py +0 -0
  194. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/account.py +0 -0
  195. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/auth.py +0 -0
  196. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/bots.py +0 -0
  197. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/channels.py +0 -0
  198. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/chatlists.py +0 -0
  199. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/conference.py +0 -0
  200. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/contacts.py +0 -0
  201. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/folders.py +0 -0
  202. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/help.py +0 -0
  203. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/langpack.py +0 -0
  204. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/messages.py +0 -0
  205. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/payments.py +0 -0
  206. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/phone.py +0 -0
  207. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/photos.py +0 -0
  208. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/premium.py +0 -0
  209. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/stats.py +0 -0
  210. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/stories.py +0 -0
  211. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/thirdParty.py +0 -0
  212. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/updates.py +0 -0
  213. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/upload.py +0 -0
  214. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/functions/users.py +0 -0
  215. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/patched/__init__.py +0 -0
  216. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/__init__.py +0 -0
  217. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/account.py +0 -0
  218. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/auth.py +0 -0
  219. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/bots.py +0 -0
  220. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/channels.py +0 -0
  221. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/chatlists.py +0 -0
  222. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/conference.py +0 -0
  223. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/contacts.py +0 -0
  224. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/help.py +0 -0
  225. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/messages.py +0 -0
  226. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/payments.py +0 -0
  227. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/phone.py +0 -0
  228. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/photos.py +0 -0
  229. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/premium.py +0 -0
  230. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/stats.py +0 -0
  231. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/stickers.py +0 -0
  232. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/storage.py +0 -0
  233. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/stories.py +0 -0
  234. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/thirdParty.py +0 -0
  235. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/update.py +0 -0
  236. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/updates.py +0 -0
  237. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/upload.py +0 -0
  238. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/tl/types/users.py +0 -0
  239. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/types.py +0 -0
  240. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/_engine/utils.py +0 -0
  241. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/afk.py +0 -0
  242. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/cli.py +0 -0
  243. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/client/__init__.py +0 -0
  244. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/client/conversation.py +0 -0
  245. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/compat.py +0 -0
  246. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/errors/__init__.py +0 -0
  247. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/errors/exceptions.py +0 -0
  248. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/events/__init__.py +0 -0
  249. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/events/album.py +0 -0
  250. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/events/base.py +0 -0
  251. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/events/callback.py +0 -0
  252. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/events/chat_action.py +0 -0
  253. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/events/edited.py +0 -0
  254. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/events/inline.py +0 -0
  255. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/events/message.py +0 -0
  256. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/events/message_deleted.py +0 -0
  257. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/events/message_read.py +0 -0
  258. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/events/user_update.py +0 -0
  259. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/filters/__init__.py +0 -0
  260. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/fsm/__init__.py +0 -0
  261. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/fsm/state.py +0 -0
  262. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/middleware/__init__.py +0 -0
  263. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/middleware/base.py +0 -0
  264. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/middleware/rate_limit.py +0 -0
  265. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/mirror.py +0 -0
  266. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/models/bot.py +0 -0
  267. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/models/chat.py +0 -0
  268. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/models/objects.py +0 -0
  269. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/models/user.py +0 -0
  270. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/network/__init__.py +0 -0
  271. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/network/client.py +0 -0
  272. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/network/connection.py +0 -0
  273. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/network/http_client.py +0 -0
  274. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/network/mtproto.py +0 -0
  275. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/network/protocols.py +0 -0
  276. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/network/spluspy_adapter.py +0 -0
  277. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/plugins/__init__.py +0 -0
  278. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/plugins/loader.py +0 -0
  279. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/scheduler/__init__.py +0 -0
  280. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/scheduler/scheduler.py +0 -0
  281. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/session/__init__.py +0 -0
  282. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/session/base.py +0 -0
  283. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/session/memory.py +0 -0
  284. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/session/string.py +0 -0
  285. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/storage/__init__.py +0 -0
  286. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/storage/base.py +0 -0
  287. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/storage/memory.py +0 -0
  288. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/storage/postgres.py +0 -0
  289. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/storage/redis.py +0 -0
  290. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/sync/__init__.py +0 -0
  291. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/sync/client.py +0 -0
  292. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/utils/__init__.py +0 -0
  293. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/utils/batch.py +0 -0
  294. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/utils/cache.py +0 -0
  295. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/utils/chat_lock.py +0 -0
  296. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/utils/helpers.py +0 -0
  297. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/utils/rate_limiter.py +0 -0
  298. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/utils/resolver.py +0 -0
  299. {spluspy-2.2.0 → spluspy-2.2.2}/spluspy/utils/version_check.py +0 -0
  300. {spluspy-2.2.0 → spluspy-2.2.2}/tests/__init__.py +0 -0
  301. {spluspy-2.2.0 → spluspy-2.2.2}/tests/test_client.py +0 -0
  302. {spluspy-2.2.0 → spluspy-2.2.2}/tests/test_filters.py +0 -0
  303. {spluspy-2.2.0 → spluspy-2.2.2}/tests/test_fsm.py +0 -0
  304. {spluspy-2.2.0 → spluspy-2.2.2}/tests/test_fsm_enhanced.py +0 -0
  305. {spluspy-2.2.0 → spluspy-2.2.2}/tests/test_middleware.py +0 -0
  306. {spluspy-2.2.0 → spluspy-2.2.2}/tests/test_rate_limiter.py +0 -0
  307. {spluspy-2.2.0 → spluspy-2.2.2}/tests/test_resolver.py +0 -0
  308. {spluspy-2.2.0 → spluspy-2.2.2}/tests/test_scheduler.py +0 -0
  309. {spluspy-2.2.0 → spluspy-2.2.2}/tests/test_session.py +0 -0
  310. {spluspy-2.2.0 → spluspy-2.2.2}/tests/test_storage.py +0 -0
  311. {spluspy-2.2.0 → spluspy-2.2.2}/tests/test_types.py +0 -0
  312. {spluspy-2.2.0 → spluspy-2.2.2}/tests/test_utils.py +0 -0
spluspy-2.2.2/PKG-INFO ADDED
@@ -0,0 +1,3290 @@
1
+ Metadata-Version: 2.4
2
+ Name: spluspy
3
+ Version: 2.2.2
4
+ Summary: A modern async Python library for interacting with Soroush Plus
5
+ Project-URL: Homepage, https://github.com/Itskillmaster/spluspy
6
+ Project-URL: Documentation, https://github.com/Itskillmaster/spluspy#readme
7
+ Project-URL: Repository, https://github.com/Itskillmaster/spluspy
8
+ Project-URL: Issues, https://github.com/Itskillmaster/spluspy/issues
9
+ Author: Ali Mirshekari
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: async,bot,mtproto,soroush,splus,userbot
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Communications :: Chat
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: aiohttp>=3.9.0
27
+ Requires-Dist: aiosqlite>=0.19.0
28
+ Requires-Dist: pyaes>=1.6
29
+ Requires-Dist: rsa>=4.7
30
+ Provides-Extra: all
31
+ Requires-Dist: asyncpg>=0.27.0; extra == 'all'
32
+ Requires-Dist: black>=23.0; extra == 'all'
33
+ Requires-Dist: cryptg; extra == 'all'
34
+ Requires-Dist: mypy>=1.0; extra == 'all'
35
+ Requires-Dist: orjson>=3.9.0; extra == 'all'
36
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'all'
37
+ Requires-Dist: pytest-cov>=4.0; extra == 'all'
38
+ Requires-Dist: pytest>=7.0; extra == 'all'
39
+ Requires-Dist: redis>=4.0; extra == 'all'
40
+ Requires-Dist: ruff>=0.1.0; extra == 'all'
41
+ Requires-Dist: uvloop>=0.19.0; (sys_platform != 'win32') and extra == 'all'
42
+ Provides-Extra: dev
43
+ Requires-Dist: black>=23.0; extra == 'dev'
44
+ Requires-Dist: mypy>=1.0; extra == 'dev'
45
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
46
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
47
+ Requires-Dist: pytest>=7.0; extra == 'dev'
48
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
49
+ Provides-Extra: postgres
50
+ Requires-Dist: asyncpg>=0.27.0; extra == 'postgres'
51
+ Provides-Extra: redis
52
+ Requires-Dist: redis>=4.0; extra == 'redis'
53
+ Provides-Extra: speed
54
+ Requires-Dist: cryptg; extra == 'speed'
55
+ Requires-Dist: orjson>=3.9.0; extra == 'speed'
56
+ Requires-Dist: uvloop>=0.19.0; (sys_platform != 'win32') and extra == 'speed'
57
+ Description-Content-Type: text/markdown
58
+
59
+ <div align="center">
60
+
61
+ # SplusPy
62
+
63
+ **A modern async Python library for Soroush Plus**
64
+
65
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/Itskillmaster/spluspy/blob/main/LICENSE)
66
+ [![PyPI version](https://img.shields.io/pypi/v/spluspy)](https://pypi.org/project/spluspy/)
67
+ [![Python versions](https://img.shields.io/pypi/pyversions/spluspy)](https://pypi.org/project/spluspy/)
68
+ [![Downloads](https://img.shields.io/pypi/dm/spluspy)](https://pypi.org/project/spluspy/)
69
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
70
+ [![Type checked: mypy](https://img.shields.io/badge/type%20checked-mypy-blue.svg)](https://mypy-lang.org/)
71
+
72
+ [English](#-features) | [فارسی](#-ویژگی‌ها)
73
+
74
+ ---
75
+
76
+ **SplusPy** is a modern, asynchronous Python library for interacting with [Soroush Plus](https://web.splus.ir) — both as a user account and a bot account.
77
+
78
+ Built from the ground up with clean architecture, it's designed to feel like [Telethon](https://github.com/LonamiWebs/Telethon) or [Pyrogram](https://github.com/pyrogram/pyrogram), but specifically for the Soroush Plus ecosystem.
79
+
80
+ **SplusPy** یک کتابخانه پایتون مدرن و ناهمگام (Asynchronous) برای تعامل با [سروش پلاس](https://web.splus.ir) است — هم به عنوان حساب کاربری و هم به عنوان حساب ربات.
81
+
82
+ این کتابخانه از صفر با معماری تمیز طراحی شده و الهام گرفته از [Telethon](https://github.com/LonamiWebs/Telethon) و [Pyrogram](https://github.com/pyrogram/pyrogram) است، اما مخصوص اکوسیستم سروش پلاس می‌باشد.
83
+
84
+ </div>
85
+
86
+ ---
87
+
88
+ ## English
89
+
90
+ ### Features
91
+
92
+ | Feature | Description |
93
+ |---------|-------------|
94
+ | **No API Key Required** | Built-in Soroush Plus credentials |
95
+ | **Fully Asynchronous** | Built with Python's `asyncio` |
96
+ | **Sync Support** | Use without `async/await` via `spluspy.sync` |
97
+ | **Bot & User Support** | Both account types |
98
+ | **Event-Driven Handlers** | Powerful event system with decorators |
99
+ | **Filter System** | Composable filters (`&`, `\|`, `~`) |
100
+ | **Inline & Reply Buttons** | Interactive keyboards |
101
+ | **Conversation API** | For interactive bot flows |
102
+ | **FSM (Finite State Machine)** | Built-in state management for bots |
103
+ | **Plugin System** | Dynamic plugin loading |
104
+ | **Middleware** | Pre/post processing of updates |
105
+ | **Scheduler** | Built-in task scheduler |
106
+ | **Multiple Storage Backends** | Memory, SQLite, Redis, PostgreSQL |
107
+ | **Rate Limiting** | Token bucket algorithm with flood wait handling |
108
+ | **AFK Auto-Reply** | Smart auto-responder with per-chat rate limiting |
109
+ | **Chat Administration** | Ban, mute, pin, purge — high-level admin API |
110
+ | **Message Mirroring** | Real-time message cloning between chats |
111
+ | **Batch Operations** | Send, delete, forward multiple messages at once |
112
+ | **File Transfer with Progress** | Upload/download with progress tracking |
113
+ | **Professional Logging** | Structured, namespaced loggers |
114
+ | **Type Hints Everywhere** | Full type safety |
115
+ | **Clean Architecture** | SOLID principles, modular design |
116
+
117
+ ### Requirements
118
+
119
+ - Python 3.10+
120
+ - No external API keys needed (built-in Soroush Plus credentials)
121
+
122
+ ### Installation
123
+
124
+ ```bash
125
+ pip install spluspy
126
+ ```
127
+
128
+ For faster encryption:
129
+
130
+ ```bash
131
+ pip install spluspy[speed]
132
+ ```
133
+
134
+ With Redis backend:
135
+
136
+ ```bash
137
+ pip install spluspy[redis]
138
+ ```
139
+
140
+ With PostgreSQL backend:
141
+
142
+ ```bash
143
+ pip install spluspy[postgres]
144
+ ```
145
+
146
+ All optional dependencies:
147
+
148
+ ```bash
149
+ pip install spluspy[all]
150
+ ```
151
+
152
+ ### Quick Start
153
+
154
+ #### Simplest Bot
155
+
156
+ ```python
157
+ from spluspy import Client
158
+
159
+ bot = Client("my_session")
160
+
161
+ @bot.on_message()
162
+ async def handler(client, message):
163
+ await message.reply("Hello!")
164
+
165
+ bot.run()
166
+ ```
167
+
168
+ #### User Account
169
+
170
+ ```python
171
+ from spluspy import Client
172
+
173
+ client = Client("session_name")
174
+
175
+ @client.on_message()
176
+ async def handler(client, message):
177
+ await message.reply("Hey there!")
178
+
179
+ async def main():
180
+ await client.start(phone="+98XXXXXXXXXX")
181
+ await client.run_until_disconnected()
182
+
183
+ import asyncio
184
+ asyncio.run(main())
185
+ ```
186
+
187
+ #### Sync Usage (No Async/Await)
188
+
189
+ ```python
190
+ from spluspy.sync import Client
191
+
192
+ bot = Client("session")
193
+
194
+ @bot.on_message()
195
+ def handler(client, message):
196
+ message.reply("Hello!")
197
+
198
+ bot.run()
199
+ ```
200
+
201
+ ---
202
+
203
+ ## Complete API Reference
204
+
205
+ ---
206
+
207
+ ### Client
208
+
209
+ The main entry point for all SplusPy operations. Manages authentication, event dispatching, middleware, plugins, and API interactions. Supports both bot mode (`bot_token`) and user mode (`phone`).
210
+
211
+ #### Constructor
212
+
213
+ ```python
214
+ Client(
215
+ session: Union[str, Session] = "spluspy",
216
+ session_name: Optional[str] = None,
217
+ *,
218
+ session_string: Optional[str] = None,
219
+ api_id: Optional[int] = None,
220
+ api_hash: Optional[str] = None,
221
+ bot_token: Optional[str] = None,
222
+ phone: Optional[str] = None,
223
+ proxy: Optional[dict[str, Any]] = None,
224
+ flood_sleep_threshold: int = 60,
225
+ max_retries: Optional[int] = None,
226
+ request_timeout: float = 30.0,
227
+ log_level: int = logging.CRITICAL,
228
+ log_file: Optional[str] = None,
229
+ log_dir: str = "logs",
230
+ errors_only: bool = False,
231
+ ) -> None
232
+ ```
233
+
234
+ | Parameter | Type | Default | Description |
235
+ |-----------|------|---------|-------------|
236
+ | `session` | `str \| Session` | `"spluspy"` | Session name or a `Session` object. Used to persist authentication data. |
237
+ | `session_name` | `str \| None` | `None` | Alternative name for the session file. Overrides `session` if provided. |
238
+ | `session_string` | `str \| None` | `None` | A portable base64 session string for deploying without files. |
239
+ | `api_id` | `int \| None` | `None` | Custom API ID. If `None`, uses built-in Soroush Plus credentials. |
240
+ | `api_hash` | `str \| None` | `None` | Custom API hash. If `None`, uses built-in Soroush Plus credentials. |
241
+ | `bot_token` | `str \| None` | `None` | Bot token for bot mode. If `None`, runs in user mode. |
242
+ | `phone` | `str \| None` | `None` | Phone number for user mode authentication. |
243
+ | `proxy` | `dict \| None` | `None` | Proxy configuration (e.g. `{"proxy_type": "socks5", "addr": "127.0.0.1", "port": 1080}`). |
244
+ | `flood_sleep_threshold` | `int` | `60` | Seconds to auto-sleep on `FloodWait` errors before raising. |
245
+ | `max_retries` | `int \| None` | `None` | Maximum retry attempts for failed requests. `None` = infinite. |
246
+ | `request_timeout` | `float` | `30.0` | Timeout in seconds for API requests. |
247
+ | `log_level` | `int` | `CRITICAL` | Logging level (e.g. `logging.INFO`, `logging.DEBUG`). |
248
+ | `log_file` | `str \| None` | `None` | Path to log file. `None` = console only. |
249
+ | `log_dir` | `str` | `"logs"` | Directory for log files. |
250
+ | `errors_only` | `bool` | `False` | If `True`, only logs errors (suppresses info/debug). |
251
+
252
+ #### Properties
253
+
254
+ | Property | Type | Description |
255
+ |----------|------|-------------|
256
+ | `session_name` | `str` | Returns the session name. |
257
+ | `is_connected` | `bool` | Whether the client is currently connected to the server. |
258
+ | `middleware` | `MiddlewareManager` | Access the middleware manager to add/remove middleware. |
259
+ | `plugins` | `PluginManager` | Access the plugin manager to load/unload plugins. |
260
+ | `scheduler` | `Scheduler` | Access the built-in task scheduler. |
261
+
262
+ #### Event Registration Methods
263
+
264
+ ##### `on(event, **kwargs) -> Callable`
265
+
266
+ Register a handler for any event type. The `event` parameter accepts an `EventBuilder` or an `Event` subclass. Optional `priority` kwarg controls execution order (lower = first).
267
+
268
+ ```python
269
+ @bot.on(NewMessage(), priority=HandlerPriority.HIGH)
270
+ async def handler(client, event):
271
+ pass
272
+ ```
273
+
274
+ ##### `on_message(*message_filters, **kwargs) -> Callable`
275
+
276
+ Register a handler that fires on new messages. Multiple filter arguments are ANDed together. The handler receives `(client, message)`.
277
+
278
+ | Parameter | Type | Description |
279
+ |-----------|------|-------------|
280
+ | `*message_filters` | `Filter` | One or more filters. All must pass for the handler to fire. |
281
+ | `priority` | `int` | Execution priority. Default: `HandlerPriority.NORMAL` (50). |
282
+
283
+ ```python
284
+ @bot.on_message(filters.text & filters.private)
285
+ async def handler(client, message):
286
+ await message.reply("Hello!")
287
+ ```
288
+
289
+ ##### `on_edited_message(*message_filters, **kwargs) -> Callable`
290
+
291
+ Register a handler for edited messages. Same filter/priority API as `on_message`.
292
+
293
+ ##### `on_callback_query(**kwargs) -> Callable`
294
+
295
+ Register a handler for inline button callback queries. Handler receives `(client, callback_query)`.
296
+
297
+ > **Note:** Not supported on Soroush+ user sessions. Exists for API compatibility.
298
+
299
+ ##### `on_inline_query(**kwargs) -> Callable`
300
+
301
+ Register a handler for inline queries. Handler receives `(client, inline_query)`.
302
+
303
+ > **Note:** Not supported on Soroush+ user sessions. Exists for API compatibility.
304
+
305
+ ##### `on_chat_action(**kwargs) -> Callable`
306
+
307
+ Register a handler for chat actions (joins, leaves, pins, etc.). Handler receives `(client, action_event)`.
308
+
309
+ ##### `on_user_update(**kwargs) -> Callable`
310
+
311
+ Register a handler for user status changes (online/offline/typing). Handler receives `(client, user_update)`.
312
+
313
+ ##### `on_message_deleted(**kwargs) -> Callable`
314
+
315
+ Register a handler for deleted messages. Handler receives `(client, deleted_event)`.
316
+
317
+ ##### `on_message_read(**kwargs) -> Callable`
318
+
319
+ Register a handler for read receipts. Handler receives `(client, read_event)`.
320
+
321
+ ##### `on_error(*exception_types, priority=HandlerPriority.LOW) -> Callable`
322
+
323
+ Register a global error handler. If no exception types are specified, catches all errors. The handler receives an `ErrorEvent` object.
324
+
325
+ ```python
326
+ @bot.on_error(FloodWait, Unauthorized)
327
+ async def error_handler(client, event):
328
+ print(f"Error: {event.exception}")
329
+ ```
330
+
331
+ #### Connection Lifecycle
332
+
333
+ ##### `async connect() -> None`
334
+
335
+ Connect to the Soroush Plus server. Attempts adapter-based connection first, falls back to built-in MTProto. Must be called before sending/receiving messages in user mode.
336
+
337
+ ##### `async disconnect() -> None`
338
+
339
+ Disconnect from the server. Stops the scheduler, closes middleware and sessions.
340
+
341
+ ##### `async start() -> None`
342
+
343
+ Start the client with interactive prompts for phone/password/code authentication. Automatically connects and handles the full auth flow.
344
+
345
+ ##### `async stop() -> None`
346
+
347
+ Alias for `disconnect()`. Stops the client.
348
+
349
+ ##### `async run_until_disconnected() -> None`
350
+
351
+ Run the event loop until the client is disconnected. Handles transient disconnections gracefully with automatic reconnection.
352
+
353
+ ##### `run() -> None`
354
+
355
+ **Blocking** entry point. Connects, runs until disconnected, then cleans up. Tries `uvloop` for better performance. This is the simplest way to run a bot:
356
+
357
+ ```python
358
+ bot = Client("session")
359
+ bot.run() # Blocks forever
360
+ ```
361
+
362
+ #### Message Sending
363
+
364
+ ##### `async send_message(chat_id, text=None, *, reply_to=None, parse_mode=None, link_preview=True, **kwargs) -> Message`
365
+
366
+ Send a text message to a chat.
367
+
368
+ | Parameter | Type | Default | Description |
369
+ |-----------|------|---------|-------------|
370
+ | `chat_id` | `int` | (required) | Target chat/user ID. |
371
+ | `text` | `str \| None` | `None` | Message text. |
372
+ | `reply_to` | `int \| None` | `None` | Message ID to reply to. |
373
+ | `parse_mode` | `str \| None` | `None` | `"html"`, `"markdown"`, or `None` for raw text. |
374
+ | `link_preview` | `bool` | `True` | Whether to show link previews. |
375
+
376
+ **Returns:** `Message` — the sent message object.
377
+
378
+ ##### `async edit_message(message, *, text=None, parse_mode=None, delay=0, **kwargs) -> Message`
379
+
380
+ Edit a previously sent message's text. If `delay` is set, waits that many seconds before editing (non-blocking).
381
+
382
+ | Parameter | Type | Default | Description |
383
+ |-----------|------|---------|-------------|
384
+ | `message` | `Message \| int` | (required) | Message object or message ID. |
385
+ | `text` | `str \| None` | `None` | New text content. |
386
+ | `delay` | `int` | `0` | Seconds to wait before editing. |
387
+
388
+ ##### `async delete_messages(*messages, **kwargs) -> bool`
389
+
390
+ Delete one or more messages. Accepts `Message` objects or integer IDs.
391
+
392
+ **Returns:** `True` on success.
393
+
394
+ ##### `async delete_message(chat_id, message_id, *, delay=0) -> bool`
395
+
396
+ Delete a single message with an optional delay before deletion.
397
+
398
+ ##### `async forward_messages(chat_id, *messages) -> Message`
399
+
400
+ Forward one or more messages to another chat. Returns a placeholder `Message`.
401
+
402
+ ##### `async send_photo(chat_id, photo, *, caption=None, **kwargs) -> Message`
403
+
404
+ Send a photo. `photo` can be a file path (`str`), `bytes`, or a file-like object.
405
+
406
+ | Parameter | Type | Default | Description |
407
+ |-----------|------|---------|-------------|
408
+ | `chat_id` | `int` | (required) | Target chat/user ID. |
409
+ | `photo` | `str \| bytes \| IO` | (required) | Photo file path, bytes, or file-like object. |
410
+ | `caption` | `str \| None` | `None` | Caption text for the photo. |
411
+
412
+ ##### `async send_video(chat_id, video, *, caption=None, duration=0, width=0, height=0, supports_streaming=True, thumb=None, force_document=False, progress_callback=None, reply_to=None, **kwargs) -> Message`
413
+
414
+ Send a video. Duration, width, and height are auto-detected if left at `0`.
415
+
416
+ | Parameter | Type | Default | Description |
417
+ |-----------|------|---------|-------------|
418
+ | `video` | `str \| bytes \| IO` | (required) | Video file. |
419
+ | `caption` | `str \| None` | `None` | Video caption. |
420
+ | `duration` | `int` | `0` | Duration in seconds (auto-detected if 0). |
421
+ | `width` | `int` | `0` | Width in pixels (auto-detected if 0). |
422
+ | `height` | `int` | `0` | Height in pixels (auto-detected if 0). |
423
+ | `supports_streaming` | `bool` | `True` | Whether the video supports streaming. |
424
+ | `progress_callback` | `Callable \| None` | `None` | Callback for upload progress: `fn(current, total)`. |
425
+
426
+ ##### `async send_voice(chat_id, voice, *, caption=None, duration=0, progress_callback=None, reply_to=None, **kwargs) -> Message`
427
+
428
+ Send a voice note (audio message).
429
+
430
+ ##### `async send_document(chat_id, document, *, caption=None, file_name=None, force_document=True, progress_callback=None, reply_to=None, **kwargs) -> Message`
431
+
432
+ Send a document/file. `force_document=True` ensures the file is sent as a document, not auto-detected.
433
+
434
+ ##### `async send_audio(chat_id, audio, *, caption=None, duration=0, performer=None, title=None, progress_callback=None, reply_to=None, **kwargs) -> Message`
435
+
436
+ Send an audio file with optional metadata (performer, title).
437
+
438
+ ##### `async send_animation(chat_id, animation, *, caption=None, duration=0, width=0, height=0, thumb=None, progress_callback=None, reply_to=None, **kwargs) -> Message`
439
+
440
+ Send an animation (GIF).
441
+
442
+ ##### `async send_location(chat_id, lat, lon, *, reply_to=None) -> Message`
443
+
444
+ Send a geographic location.
445
+
446
+ | Parameter | Type | Description |
447
+ |-----------|------|-------------|
448
+ | `lat` | `float` | Latitude (-90 to 90). |
449
+ | `lon` | `float` | Longitude (-180 to 180). |
450
+
451
+ ##### `async send_contact(chat_id, phone, first_name, last_name="", *, vcard="", reply_to=None) -> Message`
452
+
453
+ Send a contact card.
454
+
455
+ ##### `async send_poll(chat_id, question, options, *, correct_option=None, explanation=None, is_anonymous=True, allows_multiple_answers=False) -> Message`
456
+
457
+ Send a poll to a chat.
458
+
459
+ | Parameter | Type | Default | Description |
460
+ |-----------|------|---------|-------------|
461
+ | `question` | `str` | (required) | Poll question. |
462
+ | `options` | `list[str]` | (required) | List of answer options (2-10). |
463
+ | `correct_option` | `int \| None` | `None` | Index of the correct answer (for quizzes). |
464
+ | `explanation` | `str \| None` | `None` | Explanation shown after voting. |
465
+ | `is_anonymous` | `bool` | `True` | Whether the poll is anonymous. |
466
+ | `allows_multiple_answers` | `bool` | `False` | Whether users can select multiple options. |
467
+
468
+ ##### `async send_dice(chat_id, emoji="🎲", *, reply_to=None) -> Message`
469
+
470
+ Send a dice animation. Supported emojis: `🎲` (dice), `🎯` (darts), `🏀` (basketball), `⚽` (football), `🎳` (bowling), `🎰` (slot machine).
471
+
472
+ ##### `async send_reaction(chat_id, message, emoji) -> bool`
473
+
474
+ React to a message with an emoji.
475
+
476
+ **Returns:** `True` on success.
477
+
478
+ ##### `async send_sticker(chat_id, sticker, *, emoji=None, sticker_set=None, progress_callback=None, reply_to=None, **kwargs) -> Message`
479
+
480
+ Send a sticker.
481
+
482
+ ##### `async send_video_note(chat_id, video, *, caption=None, duration=0, width=0, height=0, progress_callback=None, reply_to=None, **kwargs) -> Message`
483
+
484
+ Send a round video note.
485
+
486
+ ##### `async send_chat_action(chat_id, action="typing") -> bool`
487
+
488
+ Send a typing indicator or upload progress status.
489
+
490
+ Supported actions: `"typing"`, `"upload_photo"`, `"record_video"`, `"upload_video"`, `"record_audio"`, `"upload_audio"`, `"upload_document"`, `"find_location"`, `"record_voice"`, `"upload_voice"`, `"choose_sticker"`.
491
+
492
+ #### Chat Management
493
+
494
+ ##### `async ban_user(chat_id, user_id) -> bool`
495
+
496
+ Ban a user from a chat. The user cannot rejoin unless unbanned.
497
+
498
+ ##### `async unban_user(chat_id, user_id) -> bool`
499
+
500
+ Unban a previously banned user, allowing them to rejoin the chat.
501
+
502
+ ##### `async mute_user(chat_id, user_id) -> bool`
503
+
504
+ Mute a user (restrict them from sending any messages in the chat).
505
+
506
+ ##### `async unmute_user(chat_id, user_id) -> bool`
507
+
508
+ Unmute a user, restoring their ability to send messages.
509
+
510
+ ##### `async safe_ban_user(chat_id, user_id) -> bool`
511
+
512
+ Ban a user, catching permission errors gracefully. Never raises — returns `False` on failure.
513
+
514
+ ##### `async safe_send_message(chat_id, text=None, *, reply_to=None, parse_mode=None, link_preview=True, **kwargs) -> Message`
515
+
516
+ Send a message, catching permission errors gracefully. Never raises — returns a fallback `Message` on failure.
517
+
518
+ ##### `async unblock_chat_member(chat_id, user_id) -> bool`
519
+
520
+ Remove a user from the account's block list.
521
+
522
+ ##### `async get_chat_blocked_users(chat_id) -> list[dict]`
523
+
524
+ Fetch all blocked users in a chat. Returns a list of dicts with `user_id` and `date` keys.
525
+
526
+ ##### `async join_chat(chat_id) -> Any`
527
+
528
+ Join a chat by ID, username, or invite link. Returns a `Chat` object.
529
+
530
+ **Raises typed exceptions:** `InvalidInviteLinkError`, `InviteLinkExpiredError`, `ChatFullError`, `ChatDeactivatedError`, `MembershipRequiredError`, `FloodWait`, `ChatNotFound`, `JoinChatError`.
531
+
532
+ ##### `async leave_chat(chat_id) -> bool`
533
+
534
+ Leave a chat. Auto-detects whether it's a channel or basic group.
535
+
536
+ ##### `async leave_group(chat_id) -> bool`
537
+
538
+ Leave a basic group specifically.
539
+
540
+ ##### `async leave_channel(chat_id) -> bool`
541
+
542
+ Leave a channel or supergroup specifically.
543
+
544
+ ##### `async get_all_groups() -> list[dict]`
545
+
546
+ Get all groups and channels the user is a member of. Returns list of dicts with `id`, `title`, `type` keys.
547
+
548
+ ##### `async leave_all_groups(exclude_ids=None) -> dict`
549
+
550
+ Leave all basic groups. Returns a dict with `success`, `failed`, `skipped` counts and `details`.
551
+
552
+ ##### `async leave_all_channels(exclude_ids=None) -> dict`
553
+
554
+ Leave all channels. Returns a dict with `success`, `failed`, `skipped` counts and `details`.
555
+
556
+ ##### `async leave_all(exclude_ids=None) -> dict`
557
+
558
+ Leave all groups and channels at once. Returns summary dict.
559
+
560
+ ##### `async delete_all_private(*, revoke=False, exclude_ids=None, delay=1.0, on_progress=None) -> dict`
561
+
562
+ Delete all private chat dialogs. Returns summary dict with counts and details.
563
+
564
+ ##### `async pin_chat_message(chat_id, message_id, notify=True) -> bool`
565
+
566
+ Pin a message by ID. If `notify=True`, all members are notified.
567
+
568
+ ##### `async unpin_chat_message(chat_id, message_id) -> bool`
569
+
570
+ Unpin a specific message by ID.
571
+
572
+ ##### `async pin_message(chat_id, message, *, notify=False) -> bool`
573
+
574
+ Pin a message. Accepts either a `Message` object or an integer message ID.
575
+
576
+ ##### `async unpin_message(chat_id, message) -> bool`
577
+
578
+ Unpin a message. Accepts either a `Message` object or an integer message ID.
579
+
580
+ ##### `async unpin_all_messages(chat_id) -> bool`
581
+
582
+ Unpin all pinned messages in a chat at once.
583
+
584
+ ##### `async mark_read(chat_id, message=None) -> bool`
585
+
586
+ Mark a specific message (or all messages in a chat) as read.
587
+
588
+ #### Info Retrieval
589
+
590
+ ##### `async get_me() -> Any`
591
+
592
+ Get the currently authenticated user. Returns a `User` object or `None`.
593
+
594
+ ##### `async get_chat(chat_id) -> Any`
595
+
596
+ Get a chat entity by ID or username.
597
+
598
+ ##### `async get_full_chat(chat_id) -> Optional[Chat]`
599
+
600
+ Get full chat info including title and `member_count`. Supports numeric IDs, usernames, and invite link hashes.
601
+
602
+ ##### `async is_chat_member(chat_id) -> bool`
603
+
604
+ Check if the current user is a member of the specified chat.
605
+
606
+ ##### `async get_user(user_id) -> Optional[User]`
607
+
608
+ Get full user info by ID or `@username`. Returns a `User` with bio, status, and all other fields.
609
+
610
+ ##### `async get_bio(user_id) -> Optional[str]`
611
+
612
+ Get a user's biography/about text.
613
+
614
+ ##### `async get_first_name(user_id) -> Optional[str]`
615
+
616
+ Get a user's first name.
617
+
618
+ ##### `async get_last_name(user_id) -> Optional[str]`
619
+
620
+ Get a user's last name.
621
+
622
+ ##### `async get_full_name(user_id) -> Optional[str]`
623
+
624
+ Get a user's full name (first + last name combined).
625
+
626
+ ##### `async get_username(user_id) -> Optional[str]`
627
+
628
+ Get a user's username without the `@` prefix.
629
+
630
+ ##### `async get_user_link(user_id) -> Optional[str]`
631
+
632
+ Get a `t.me/` profile link for the user.
633
+
634
+ ##### `async get_description(chat_id) -> Optional[str]`
635
+
636
+ Get a group/channel description (about text).
637
+
638
+ ##### `async get_group_link(chat_id) -> Optional[str]`
639
+
640
+ Get the `t.me/` invite link for a group or channel.
641
+
642
+ ##### `async get_group_id(username) -> Optional[int]`
643
+
644
+ Resolve a `@username` to a chat/channel integer ID.
645
+
646
+ ##### `async get_chat_info(chat_id) -> Optional[dict]`
647
+
648
+ Get comprehensive chat info as a dictionary with keys: `id`, `title`, `username`, `link`, `description`, `member_count`, `type`.
649
+
650
+ ##### `async get_user_info(user_id) -> Optional[dict]`
651
+
652
+ Get comprehensive user info as a dictionary with keys: `id`, `first_name`, `last_name`, `full_name`, `username`, `link`, `bio`, `phone`, `is_bot`, `is_premium`, `status`.
653
+
654
+ ##### `async get_messages(chat_id, *, limit=100, **kwargs) -> list[Message]`
655
+
656
+ Get messages from a chat. Returns list of `Message` objects, newest first.
657
+
658
+ ##### `async iter_messages(chat_id, *, limit=1000, offset_id=0, search=None, batch_size=100) -> AsyncGenerator[Message, None]`
659
+
660
+ Async generator yielding messages in memory-efficient batches. Ideal for processing large histories without loading everything into memory.
661
+
662
+ | Parameter | Type | Default | Description |
663
+ |-----------|------|---------|-------------|
664
+ | `chat_id` | `int \| str` | (required) | Chat to iterate messages from. |
665
+ | `limit` | `int` | `1000` | Maximum number of messages to yield. |
666
+ | `offset_id` | `int` | `0` | Start from this message ID (newer messages). |
667
+ | `search` | `str \| None` | `None` | Search query to filter messages. |
668
+ | `batch_size` | `int` | `100` | Number of messages to fetch per API call. |
669
+
670
+ ##### `async search_messages(chat_id, query, *, limit=100) -> list[Message]`
671
+
672
+ Search for messages in a chat by text query.
673
+
674
+ ##### `async get_history(chat_id, *, limit=100) -> list[Message]`
675
+
676
+ Alias for `get_messages()`.
677
+
678
+ ##### `async get_members(chat_id, *, limit=100) -> list[ChatMember]`
679
+
680
+ Alias for `get_chat_members()`.
681
+
682
+ ##### `async iter_chat_members(chat_id, *, limit=200, batch_size=100) -> AsyncGenerator[ChatMember, None]`
683
+
684
+ Async generator yielding chat members in batches.
685
+
686
+ ##### `async iter_members(chat_id, *, limit=200, batch_size=100) -> AsyncGenerator[ChatMember, None]`
687
+
688
+ Alias for `iter_chat_members()`.
689
+
690
+ ##### `async iter_dialogs(*, limit=500, batch_size=100) -> AsyncGenerator[Chat, None]`
691
+
692
+ Async generator yielding dialogs (chats) in batches.
693
+
694
+ ##### `async get_contacts() -> list[User]`
695
+
696
+ Get the account's contact list as `User` objects.
697
+
698
+ ##### `async resolve_username(username) -> int`
699
+
700
+ Resolve a username to a numeric ID. Returns `0` if not found.
701
+
702
+ ##### `async resolve_peer(peer) -> Any`
703
+
704
+ Resolve a peer identifier to an MTProto `InputPeer` object. Supports integer IDs, `@username`, and phone numbers.
705
+
706
+ #### Profile Management
707
+
708
+ ##### `async update_profile(first_name=None, last_name=None, about=None, bio=None) -> bool`
709
+
710
+ Update the current user's profile fields. Returns `True` on success.
711
+
712
+ ##### `async set_profile_photo(photo) -> bool`
713
+
714
+ Upload and set a new profile photo. `photo` can be a file path or `bytes`.
715
+
716
+ ##### `async delete_profile_photo(photo_id=None) -> bool`
717
+
718
+ Delete a profile photo. Pass `None` to delete the most recent one.
719
+
720
+ ##### `async set_username(username) -> bool`
721
+
722
+ Change the current user's username. Pass an empty string `""` to remove it.
723
+
724
+ ##### `async set_offline(offline=True) -> bool`
725
+
726
+ Set the user's online/offline appearance. `True` = appear offline.
727
+
728
+ #### Low-level
729
+
730
+ ##### `async invoke(*args, **kwargs) -> Any`
731
+
732
+ Invoke a raw MTProto API method. This is a low-level escape hatch for API methods not covered by the high-level interface.
733
+
734
+ ##### `async export_session_string() -> str`
735
+
736
+ Export the current session as a portable base64 string. Useful for deploying without session files.
737
+
738
+ #### Conversation
739
+
740
+ ##### `conversation(chat_id, *, timeout=30.0, exclusive=True) -> Conversation`
741
+
742
+ Create an interactive conversation context manager for request/response style messaging. See [Conversation API](#conversation-api) below.
743
+
744
+ ---
745
+
746
+ ### Conversation API
747
+
748
+ A context-managed conversational flow for interactive bot dialogs. Used with `async with client.conversation(peer) as conv:`.
749
+
750
+ #### Constructor
751
+
752
+ ```python
753
+ Conversation(
754
+ client: Client,
755
+ peer: Union[int, str],
756
+ timeout: Optional[float] = 30.0,
757
+ exclusive: bool = True,
758
+ )
759
+ ```
760
+
761
+ | Parameter | Type | Default | Description |
762
+ |-----------|------|---------|-------------|
763
+ | `client` | `Client` | (required) | The SplusPy client instance. |
764
+ | `peer` | `int \| str` | (required) | Chat ID or username to converse with. |
765
+ | `timeout` | `float` | `30.0` | Default timeout in seconds for waiting responses. |
766
+ | `exclusive` | `bool` | `True` | If `True`, only captures messages from the target peer. |
767
+
768
+ #### Methods
769
+
770
+ ##### `async send_message(text, **kwargs) -> Message`
771
+
772
+ Send a message in this conversation.
773
+
774
+ ##### `async get_response(timeout=None) -> Message`
775
+
776
+ Wait for the next incoming message from the conversation peer. Raises `ConversationTimeoutError` if no message arrives within the timeout.
777
+
778
+ ##### `async wait_response(timeout=None) -> Message`
779
+
780
+ Alias for `get_response()`.
781
+
782
+ ##### `async send_and_wait(text, *, timeout=None, **kwargs) -> Message`
783
+
784
+ Send a message and wait for a reply in a single call. Convenience method combining `send_message` + `get_response`.
785
+
786
+ ##### `empty() -> bool`
787
+
788
+ Returns `True` if the internal message queue is empty.
789
+
790
+ ##### `clear() -> None`
791
+
792
+ Discard any unread messages in the internal queue.
793
+
794
+ #### Usage Example
795
+
796
+ ```python
797
+ async with client.conversation(chat_id, timeout=30) as conv:
798
+ await conv.send_message("What is your name?")
799
+ response = await conv.get_response()
800
+ await conv.send_and_wait(f"Nice to meet you, {response.text}!")
801
+ ```
802
+
803
+ ---
804
+
805
+ ### Client Chat Management Mixin Methods
806
+
807
+ Additional chat management methods available on the `Client` instance.
808
+
809
+ ##### `async get_chat_members(chat_id, limit=200) -> list[Any]`
810
+
811
+ Get a list of members in a channel/megagroup using raw MTProto. Returns `User` objects.
812
+
813
+ ##### `async restrict_user(chat_id, user_id, send_messages=False, until_date=0, *, send_media=False, send_stickers=False, send_gifs=False, send_inline=False, embed_links=False, send_polls=False, change_info=False, invite_users=False, pin_messages=False, manage_topics=False) -> bool`
814
+
815
+ Restrict a user in a megagroup/channel with fine-grained permission control.
816
+
817
+ | Parameter | Type | Default | Description |
818
+ |-----------|------|---------|-------------|
819
+ | `send_messages` | `bool` | `False` | Allow sending messages. |
820
+ | `send_media` | `bool` | `False` | Allow sending media. |
821
+ | `send_stickers` | `bool` | `False` | Allow sending stickers. |
822
+ | `send_gifs` | `bool` | `False` | Allow sending GIFs. |
823
+ | `send_inline` | `bool` | `False` | Allow using inline bots. |
824
+ | `embed_links` | `bool` | `False` | Allow embedding links. |
825
+ | `send_polls` | `bool` | `False` | Allow sending polls. |
826
+ | `change_info` | `bool` | `False` | Allow changing chat info. |
827
+ | `invite_users` | `bool` | `False` | Allow inviting users. |
828
+ | `pin_messages` | `bool` | `False` | Allow pinning messages. |
829
+ | `manage_topics` | `bool` | `False` | Allow managing topics. |
830
+ | `until_date` | `int` | `0` | Restriction expiry timestamp. `0` = permanent. |
831
+
832
+ ##### `async promote_member(chat_id, user_id, is_admin=True, privileges=None, *, rank="") -> bool`
833
+
834
+ Promote or demote a user in a megagroup/channel. `privileges` is a dict of permission booleans. `rank` sets the admin rank title.
835
+
836
+ ##### `async set_chat_permissions(chat_id, send_messages=None, send_media=None, send_stickers=None, send_gifs=None, send_polls=None, embed_links=None, invite_users=None, change_info=None, pin_messages=None) -> bool`
837
+
838
+ Set the default chat permissions for all non-admin members. `True` = allowed, `False` = restricted, `None` = no change.
839
+
840
+ ##### `async set_chat_title(chat_id, title) -> bool`
841
+
842
+ Change the title of a channel or supergroup.
843
+
844
+ ##### `async set_chat_about(chat_id, about) -> bool`
845
+
846
+ Change the about/description text of a chat (max 70 characters).
847
+
848
+ ##### `async set_chat_photo(chat_id, file_path) -> bool`
849
+
850
+ Upload and set a new photo for a channel or supergroup.
851
+
852
+ ##### `async start_group_call(chat_id, title=None) -> bool`
853
+
854
+ Start a group call (voice chat) in a supergroup. Requires admin rights with `manage_call` permission.
855
+
856
+ ##### `async stop_group_call(chat_id) -> bool`
857
+
858
+ Stop (discard) an active group call in a supergroup.
859
+
860
+ ---
861
+
862
+ ### Client Media Mixin Methods
863
+
864
+ Low-level media operations using raw MTProto.
865
+
866
+ | Constant | Value | Description |
867
+ |----------|-------|-------------|
868
+ | `UPLOAD_CHUNK_SIZE` | `524288` (512 KB) | Default upload chunk size. |
869
+ | `DOWNLOAD_CHUNK_SIZE` | `1048576` (1 MB) | Default download chunk size. |
870
+ | `MAX_SMALL_FILE_SIZE` | `10485760` (10 MB) | Max size for small file uploads. |
871
+
872
+ ##### `async send_file(chat_id, file_path, caption="", progress_callback=None, *, force_document=False, force_photo=False, thumb=None, attributes=None, voice_note=False, video_note=False, background=False, clear_draft=False, noforwards=False, schedule_date=None, reply_to=None, duration=0, width=0, height=0, performer=None, title=None, supports_streaming=True, sticker_emoji=None, sticker_set=None) -> Any`
873
+
874
+ Send a file using raw MTProto upload with chunked transfer. Auto-detects photo vs document based on file type.
875
+
876
+ ##### `async send_location_media(chat_id, lat, lon, *, reply_to=None) -> Any`
877
+
878
+ Send a geographic location using raw MTProto.
879
+
880
+ ##### `async send_contact_media(chat_id, phone, first_name, last_name="", vcard="", *, reply_to=None) -> Any`
881
+
882
+ Send a contact card using raw MTProto.
883
+
884
+ ##### `async send_dice_media(chat_id, emoticon="🎲", *, reply_to=None) -> Any`
885
+
886
+ Send a dice animation using raw MTProto.
887
+
888
+ ##### `async download_media(message, file_name=None, progress_callback=None, *, dc_id=None) -> Optional[str]`
889
+
890
+ Download a file from a message. Returns the path to the downloaded file, or `None` on failure.
891
+
892
+ | Parameter | Type | Default | Description |
893
+ |-----------|------|---------|-------------|
894
+ | `message` | `Message \| Any` | (required) | Message containing the media. |
895
+ | `file_name` | `str \| None` | `None` | Custom file name for the download. |
896
+ | `progress_callback` | `Callable \| None` | `None` | Progress callback: `fn(current, total)`. |
897
+ | `dc_id` | `int \| None` | `None` | Data center ID override. |
898
+
899
+ ---
900
+
901
+ ### Message Model
902
+
903
+ The `Message` dataclass represents a received or sent message.
904
+
905
+ #### Attributes
906
+
907
+ | Field | Type | Default | Description |
908
+ |-------|------|---------|-------------|
909
+ | `id` | `int` | `0` | Unique message identifier. |
910
+ | `text` | `str` | `""` | Message text or media caption. |
911
+ | `date` | `datetime \| None` | `None` | Message timestamp. |
912
+ | `chat_id` | `int` | `0` | Chat this message belongs to. |
913
+ | `chat` | `Chat \| None` | `None` | Resolved `Chat` object. |
914
+ | `sender_id` | `int \| None` | `None` | Sender user ID. |
915
+ | `sender` | `User \| None` | `None` | Resolved `User` object. |
916
+ | `media` | `Media \| None` | `None` | Attached media object. |
917
+ | `media_type` | `MessageMediaType` | `NONE` | Kind of media (`PHOTO`, `VIDEO`, etc.). |
918
+ | `reply_to` | `int \| None` | `None` | ID of the message being replied to. |
919
+ | `reply_to_sender_id` | `int \| None` | `None` | Sender ID from the reply header. |
920
+ | `reply_to_message` | `Message \| None` | `None` | Full replied-to message object. |
921
+ | `entities` | `list[MessageEntity]` | `[]` | Formatting entities (bold, links, etc.). |
922
+ | `forward_origin` | `dict \| None` | `None` | Forward origin information. |
923
+ | `views` | `int` | `0` | View count for channel messages. |
924
+ | `edit_date` | `datetime \| None` | `None` | Last edit timestamp. |
925
+ | `is_group` | `bool` | `False` | Whether from a group chat. |
926
+ | `is_channel` | `bool` | `False` | Whether from a channel. |
927
+ | `is_private` | `bool` | `False` | Whether from a private chat. |
928
+ | `new_chat_members` | `list` | `[]` | Users who joined (service messages). |
929
+ | `left_chat_member` | `User \| None` | `None` | User who left (service messages). |
930
+ | `service_type` | `str \| None` | `None` | Type of service event. |
931
+
932
+ #### Properties
933
+
934
+ | Property | Return Type | Description |
935
+ |----------|-------------|-------------|
936
+ | `message_id` | `int` | Alias for `id`. |
937
+ | `is_forwarded` | `bool` | Whether message was forwarded. |
938
+ | `is_reply` | `bool` | Whether message is a reply to another message. |
939
+ | `is_media` | `bool` | Whether message contains any media attachment. |
940
+
941
+ #### Methods
942
+
943
+ ##### `async reply(text=None, *, parse_mode=None, link_preview=True, buttons=None, file=None, **kwargs) -> Message`
944
+
945
+ Reply to this message (sends with reply header).
946
+
947
+ ##### `async edit(text=None, *, parse_mode=None, link_preview=True, buttons=None, delay=0) -> Message`
948
+
949
+ Edit this message's text. If `delay > 0`, waits before editing.
950
+
951
+ ##### `async delete(*, delay=0) -> bool`
952
+
953
+ Delete this message. If `delay > 0`, waits before deleting.
954
+
955
+ ##### `async forward(chat_id) -> Message`
956
+
957
+ Forward this message to another chat (keeps the forward header).
958
+
959
+ ##### `async copy(chat_id) -> Message`
960
+
961
+ Copy this message to another chat (no forward header).
962
+
963
+ ##### `async get_reply_message() -> Optional[Message]`
964
+
965
+ Fetch the full message that this message replies to.
966
+
967
+ ##### `async pin(notify=True) -> bool`
968
+
969
+ Pin this message in its chat.
970
+
971
+ ##### `async unpin() -> bool`
972
+
973
+ Unpin this message from its chat.
974
+
975
+ ##### `async react(emoji) -> bool`
976
+
977
+ React to this message with an emoji.
978
+
979
+ ##### `async mark_read() -> bool`
980
+
981
+ Mark this message as read.
982
+
983
+ ##### `async unblock_sender() -> bool`
984
+
985
+ Unblock the sender of this message.
986
+
987
+ ##### `async download(file_path=None, *, progress=None) -> Optional[str]`
988
+
989
+ Download the media attached to this message. Returns the local file path.
990
+
991
+ Aliases: `download_media()`, `download_file()`, `save()`.
992
+
993
+ ##### `async reply_photo(photo, caption=None, **kwargs) -> Message`
994
+
995
+ Reply with a photo.
996
+
997
+ ##### `async reply_video(video, caption=None, **kwargs) -> Message`
998
+
999
+ Reply with a video.
1000
+
1001
+ ##### `async reply_voice(voice, caption=None, **kwargs) -> Message`
1002
+
1003
+ Reply with a voice note.
1004
+
1005
+ ##### `async reply_document(document, caption=None, **kwargs) -> Message`
1006
+
1007
+ Reply with a document.
1008
+
1009
+ ##### `async reply_audio(audio, caption=None, *, duration=0, performer=None, title=None, **kwargs) -> Message`
1010
+
1011
+ Reply with an audio file.
1012
+
1013
+ ##### `async reply_animation(animation, caption=None, **kwargs) -> Message`
1014
+
1015
+ Reply with a GIF/animation.
1016
+
1017
+ ##### `async reply_sticker(sticker, **kwargs) -> Message`
1018
+
1019
+ Reply with a sticker.
1020
+
1021
+ ##### `async reply_location(lat, lon) -> Message`
1022
+
1023
+ Reply with a geographic location.
1024
+
1025
+ ##### `async reply_contact(phone, first_name, last_name="", *, vcard="") -> Message`
1026
+
1027
+ Reply with a contact card.
1028
+
1029
+ ##### `async reply_poll(question, options, *, is_anonymous=True, allows_multiple_answers=False) -> Message`
1030
+
1031
+ Reply with a poll.
1032
+
1033
+ ##### `async reply_dice(emoji="🎲") -> Message`
1034
+
1035
+ Reply with a dice animation.
1036
+
1037
+ ##### `async reply_video_note(video, *, caption=None, duration=0, **kwargs) -> Message`
1038
+
1039
+ Reply with a round video note.
1040
+
1041
+ ---
1042
+
1043
+ ### User Model
1044
+
1045
+ Represents a Soroush Plus user.
1046
+
1047
+ #### Attributes
1048
+
1049
+ | Field | Type | Default | Description |
1050
+ |-------|------|---------|-------------|
1051
+ | `id` | `int` | (required) | Unique user identifier. |
1052
+ | `first_name` | `str` | `""` | First name. |
1053
+ | `last_name` | `str` | `""` | Last name. |
1054
+ | `username` | `str \| None` | `None` | Username without `@`. |
1055
+ | `phone` | `str \| None` | `None` | Phone number. |
1056
+ | `bio` | `str \| None` | `None` | Biography/about text. |
1057
+ | `is_bot` | `bool` | `False` | Whether this is a bot account. |
1058
+ | `is_self` | `bool` | `False` | Whether this is the current authenticated user. |
1059
+ | `is_premium` | `bool` | `False` | Whether user has premium subscription. |
1060
+ | `status` | `UserStatus` | `EMPTY` | Online status. |
1061
+
1062
+ #### Properties
1063
+
1064
+ | Property | Return Type | Description |
1065
+ |----------|-------------|-------------|
1066
+ | `full_name` | `str` | Combined first + last name. |
1067
+ | `mention` | `str` | `@username` if available, otherwise full name. |
1068
+ | `link` | `str \| None` | `https://t.me/username` profile link. `None` if no username. |
1069
+
1070
+ ---
1071
+
1072
+ ### Chat Model
1073
+
1074
+ Represents a Soroush Plus chat (private, group, supergroup, or channel).
1075
+
1076
+ #### Attributes
1077
+
1078
+ | Field | Type | Default | Description |
1079
+ |-------|------|---------|-------------|
1080
+ | `id` | `int` | (required) | Unique chat identifier. |
1081
+ | `title` | `str` | `""` | Chat title. |
1082
+ | `type` | `ChatType` | `PRIVATE` | Type of chat. |
1083
+ | `username` | `str \| None` | `None` | Public username. |
1084
+ | `description` | `str \| None` | `None` | Chat description. |
1085
+ | `member_count` | `int` | `0` | Number of members. |
1086
+
1087
+ #### Properties
1088
+
1089
+ | Property | Return Type | Description |
1090
+ |----------|-------------|-------------|
1091
+ | `is_private` | `bool` | Whether it's a private (1-to-1) chat. |
1092
+ | `is_group` | `bool` | Whether it's a group or supergroup. |
1093
+ | `is_supergroup` | `bool` | Whether it's a supergroup. |
1094
+ | `is_channel` | `bool` | Whether it's a channel. |
1095
+ | `link` | `str \| None` | Deep link to the chat. |
1096
+ | `display_name` | `str` | Human-readable name (title or username). |
1097
+
1098
+ #### Subclasses
1099
+
1100
+ - `Channel(Chat)` — Pre-set type to `CHANNEL`.
1101
+ - `Group(Chat)` — Pre-set type to `GROUP` or `SUPERGROUP`.
1102
+
1103
+ ---
1104
+
1105
+ ### ChatMember Model
1106
+
1107
+ Represents a member of a chat.
1108
+
1109
+ #### Attributes
1110
+
1111
+ | Field | Type | Description |
1112
+ |-------|------|-------------|
1113
+ | `user` | `User` | The user object. |
1114
+ | `status` | `ChatMemberStatus` | Membership status. |
1115
+ | `joined_date` | `datetime \| None` | When they joined. |
1116
+ | `invited_by` | `int \| None` | Who invited them. |
1117
+ | `restricted_until` | `int \| None` | Restriction expiry timestamp. |
1118
+
1119
+ #### Properties
1120
+
1121
+ | Property | Return Type | Description |
1122
+ |----------|-------------|-------------|
1123
+ | `user_id` | `int` | Shortcut to `user.id`. |
1124
+ | `is_creator` | `bool` | Whether this is the chat creator/owner. |
1125
+ | `is_admin` | `bool` | Whether they have admin rights (includes creator). |
1126
+ | `is_banned` | `bool` | Whether they are banned. |
1127
+ | `is_left` | `bool` | Whether they have left the chat. |
1128
+
1129
+ ---
1130
+
1131
+ ### Button Factory
1132
+
1133
+ Static factory methods for creating keyboard buttons. All methods are `@staticmethod`.
1134
+
1135
+ #### Inline Buttons
1136
+
1137
+ ##### `Button.inline(text, callback_data) -> InlineKeyboardButton`
1138
+
1139
+ Create an inline keyboard button with callback data.
1140
+
1141
+ | Parameter | Type | Description |
1142
+ |-----------|------|-------------|
1143
+ | `text` | `str` | Button display text. |
1144
+ | `callback_data` | `bytes` | Data sent back when button is pressed. |
1145
+
1146
+ ##### `Button.url(text, url) -> InlineKeyboardButton`
1147
+
1148
+ Create an inline button that opens a URL.
1149
+
1150
+ ##### `Button.switch_inline(text, query="") -> InlineKeyboardButton`
1151
+
1152
+ Create an inline button that switches to inline mode with a pre-filled query.
1153
+
1154
+ ##### `Button.switch_inline_current(text, query="") -> InlineKeyboardButton`
1155
+
1156
+ Same as `switch_inline` but keeps the user in the current chat.
1157
+
1158
+ #### Reply Buttons
1159
+
1160
+ ##### `Button.text(text, resize=True, one_time=False) -> ReplyMarkup`
1161
+
1162
+ Create a single-button reply keyboard.
1163
+
1164
+ #### Special Buttons
1165
+
1166
+ ##### `Button.request_location(text="Share Location") -> KeyboardButton`
1167
+
1168
+ Create a button that requests the user's location.
1169
+
1170
+ ##### `Button.request_phone(text="Share Phone") -> KeyboardButton`
1171
+
1172
+ Create a button that requests the user's phone number.
1173
+
1174
+ ##### `Button.clear() -> ReplyMarkup`
1175
+
1176
+ Create a special reply markup that removes/hides the current keyboard.
1177
+
1178
+ #### Keyboard Builders
1179
+
1180
+ ##### `Button.build_inline(*rows) -> ReplyMarkup`
1181
+
1182
+ Build an inline keyboard from rows of buttons.
1183
+
1184
+ ```python
1185
+ keyboard = Button.build_inline(
1186
+ [Button.inline("Option 1", b"opt1"), Button.inline("Option 2", b"opt2")],
1187
+ [Button.url("Visit", "https://example.com")]
1188
+ )
1189
+ ```
1190
+
1191
+ ##### `Button.build_reply(*rows, resize=True, one_time=False) -> ReplyMarkup`
1192
+
1193
+ Build a reply keyboard from rows of buttons.
1194
+
1195
+ ```python
1196
+ kb = Button.build_reply(
1197
+ [Button.text("Menu"), Button.text("Settings")],
1198
+ [Button.text("Help")]
1199
+ )
1200
+ ```
1201
+
1202
+ ---
1203
+
1204
+ ### CallbackQuery Model
1205
+
1206
+ Represents a callback query from an inline button press.
1207
+
1208
+ #### Attributes
1209
+
1210
+ | Field | Type | Description |
1211
+ |-------|------|-------------|
1212
+ | `id` | `str` | Unique callback query identifier. |
1213
+ | `data` | `bytes \| None` | Raw callback data bytes. |
1214
+ | `chat_instance` | `str` | Chat instance identifier. |
1215
+ | `from_user` | `User` | The user who pressed the button. |
1216
+ | `message` | `Message \| None` | The message containing the button. |
1217
+
1218
+ #### Properties
1219
+
1220
+ | Property | Return Type | Description |
1221
+ |----------|-------------|-------------|
1222
+ | `text` | `str \| None` | Decoded callback data as UTF-8 string. |
1223
+
1224
+ #### Methods
1225
+
1226
+ ##### `async answer(text=None, show_alert=False, cache_time=0) -> bool`
1227
+
1228
+ Answer the callback query. Shows a popup notification if `show_alert=True`.
1229
+
1230
+ ##### `async edit_message(text=None, reply_markup=None) -> bool`
1231
+
1232
+ Edit the message that contains the inline keyboard.
1233
+
1234
+ ---
1235
+
1236
+ ### InlineQuery Model
1237
+
1238
+ Represents an inline query from a user.
1239
+
1240
+ #### Attributes
1241
+
1242
+ | Field | Type | Description |
1243
+ |-------|------|-------------|
1244
+ | `id` | `str` | Unique inline query identifier. |
1245
+ | `query` | `str` | The search query text. |
1246
+ | `offset` | `str` | Pagination offset. |
1247
+ | `chat_type` | `str \| None` | Chat type context. |
1248
+ | `from_user` | `User` | The user who sent the query. |
1249
+
1250
+ #### Methods
1251
+
1252
+ ##### `async answer(results, cache_time=300, is_personal=True, next_offset=None) -> bool`
1253
+
1254
+ Answer the inline query with a list of result dictionaries.
1255
+
1256
+ ---
1257
+
1258
+ ### Events
1259
+
1260
+ #### Event (Base Class)
1261
+
1262
+ Abstract base for all events. All events support propagation control.
1263
+
1264
+ ##### `stop_propagation() -> None`
1265
+
1266
+ Stop the event from being passed to subsequent handlers with lower priority.
1267
+
1268
+ ##### `is_propagation_stopped` (property) -> `bool`
1269
+
1270
+ Returns `True` if propagation was stopped by a handler.
1271
+
1272
+ #### HandlerPriority
1273
+
1274
+ Controls the order in which handlers are executed.
1275
+
1276
+ | Value | Int | Description |
1277
+ |-------|-----|-------------|
1278
+ | `FIRST` | `0` | Executes first, before all others. |
1279
+ | `HIGH` | `10` | Executes early. |
1280
+ | `NORMAL` | `50` | Default priority. |
1281
+ | `LOW` | `100` | Executes late. |
1282
+ | `LAST` | `200` | Executes last, after all others. |
1283
+
1284
+ #### NewMessage Event
1285
+
1286
+ Fired when a new message is received.
1287
+
1288
+ ##### Fields
1289
+
1290
+ | Field | Type | Description |
1291
+ |-------|------|-------------|
1292
+ | `message` | `Message` | The incoming message. |
1293
+ | `pattern_match` | `Pattern.Match \| None` | Regex match result (if `filters.regex` was used). |
1294
+
1295
+ ##### Properties
1296
+
1297
+ `text`, `chat_id`, `sender_id`, `is_private`, `is_group`, `is_channel`, `raw_text`, `sender`, `chat`, `reply_to`, `reply_to_sender_id`, `reply_to_message`, `media`, `photo`, `video`, `voice`, `document`, `sticker`, `forward_origin`, `new_chat_members`, `left_chat_member`, `service_type`.
1298
+
1299
+ ##### Methods
1300
+
1301
+ ```python
1302
+ async def reply(self, text: str, **kwargs) -> Message
1303
+ async def respond(self, text: str, **kwargs) -> Message # Send without reply header
1304
+ async def delete(self, **kwargs) -> bool
1305
+ async def edit(self, text: str, **kwargs) -> Message
1306
+ async def forward(self, chat_id: int) -> Message
1307
+ async def pin(self, notify=False) -> bool
1308
+ async def react(self, emoji: str) -> bool
1309
+ async def mark_read(self) -> bool
1310
+ async def download(self, file_path=None) -> Optional[str]
1311
+ ```
1312
+
1313
+ #### ErrorEvent
1314
+
1315
+ Fired when a handler raises an exception.
1316
+
1317
+ | Field | Type | Description |
1318
+ |-------|------|-------------|
1319
+ | `exception` | `Exception` | The raised exception. |
1320
+ | `handler` | `Callable \| None` | The handler that failed. |
1321
+ | `update` | `Any` | The original update. |
1322
+
1323
+ | Property | Type | Description |
1324
+ |----------|------|-------------|
1325
+ | `message` | `str` | Error message string. |
1326
+ | `type` | `str` | Exception class name. |
1327
+
1328
+ #### ChatAction Event
1329
+
1330
+ Fired on chat actions (joins, leaves, pins).
1331
+
1332
+ | Property | Type | Description |
1333
+ |----------|------|-------------|
1334
+ | `is_join` | `bool` | Whether a user joined. |
1335
+ | `is_leave` | `bool` | Whether a user left. |
1336
+ | `is_pin` | `bool` | Whether a message was pinned. |
1337
+ | `user_name` | `str` | Display name of the user. |
1338
+
1339
+ #### UserUpdate Event
1340
+
1341
+ Fired when a user's online status changes.
1342
+
1343
+ | Property | Type | Description |
1344
+ |----------|------|-------------|
1345
+ | `is_online` | `bool` | Whether the user came online. |
1346
+ | `is_offline` | `bool` | Whether the user went offline. |
1347
+
1348
+ #### MessageDeleted Event
1349
+
1350
+ Fired when messages are deleted. Fields: `deleted_ids` (list of ints), `chat_id`.
1351
+
1352
+ #### MessageRead Event
1353
+
1354
+ Fired when messages are read. Fields: `read_ids` (list of ints), `chat_id`, `max_id`.
1355
+
1356
+ #### Album Event
1357
+
1358
+ Fired when a group of media messages is received (e.g., multiple photos sent at once).
1359
+
1360
+ | Field | Type | Description |
1361
+ |-------|------|-------------|
1362
+ | `messages` | `list[Message]` | All messages in the album. |
1363
+ | `chat_id` | `int` | The chat where the album was received. |
1364
+ | `group_id` | `str` | Album group identifier. |
1365
+
1366
+ | Property | Type | Description |
1367
+ |----------||-------------|
1368
+ | `total` | `int` | Number of messages in the album. |
1369
+
1370
+ ---
1371
+
1372
+ ### Filters
1373
+
1374
+ Filters are composable predicates that determine which messages trigger a handler. Use `&` (AND), `|` (OR), and `~` (NOT) to combine them.
1375
+
1376
+ #### Chat-Type Filters
1377
+
1378
+ | Filter | Matches |
1379
+ |--------|---------|
1380
+ | `filters.private` | Messages from private (1-to-1) chats. |
1381
+ | `filters.group` | Messages from group or supergroup chats. |
1382
+ | `filters.channel` | Messages from channels. |
1383
+
1384
+ #### Content-Type Filters
1385
+
1386
+ | Filter | Matches |
1387
+ |--------|---------|
1388
+ | `filters.text` | Messages with non-empty text. |
1389
+ | `filters.photo` | Messages containing a photo. |
1390
+ | `filters.video` | Messages containing a video. |
1391
+ | `filters.audio` | Messages containing audio. |
1392
+ | `filters.voice` | Messages containing a voice note. |
1393
+ | `filters.document` | Messages containing a document. |
1394
+ | `filters.sticker` | Messages containing a sticker. |
1395
+ | `filters.animation` | Messages containing a GIF. |
1396
+ | `filters.contact` | Messages containing a contact card. |
1397
+ | `filters.location` | Messages containing a location. |
1398
+ | `filters.poll` | Messages containing a poll. |
1399
+ | `filters.video_note` | Messages containing a video note. |
1400
+ | `filters.dice` | Messages containing a dice animation. |
1401
+ | `filters.media` | Messages with any media attachment. |
1402
+
1403
+ #### State Filters
1404
+
1405
+ | Filter | Matches |
1406
+ |--------|---------|
1407
+ | `filters.reply` | Messages that are replies to other messages. |
1408
+ | `filters.forwarded` | Forwarded messages. |
1409
+ | `filters.me` | Messages sent by the current user. |
1410
+ | `filters.bot` | Messages sent by bots. |
1411
+ | `filters.outgoing` | Outgoing (outbound) messages. |
1412
+ | `filters.incoming` | Incoming (inbound) messages. |
1413
+ | `filters.mentioned` | Messages where the current user is mentioned. |
1414
+ | `filters.new_chat_members` | Service messages: users joined. |
1415
+ | `filters.left_chat_member` | Service messages: user left/was removed. |
1416
+ | `filters.service` | Any system/service message. |
1417
+
1418
+ #### Factory Functions
1419
+
1420
+ ##### `filters.command(*commands, prefixes=None) -> Command`
1421
+
1422
+ Create a command filter. Matches messages starting with `/` or `!` followed by the given command names.
1423
+
1424
+ ```python
1425
+ @bot.on_message(filters.command("start", "help"))
1426
+ async def handler(client, message):
1427
+ # Matches /start, /help, !start, !help
1428
+ pass
1429
+ ```
1430
+
1431
+ | Parameter | Type | Default | Description |
1432
+ |-----------|------|---------|-------------|
1433
+ | `*commands` | `str` | (required) | Command names to match. |
1434
+ | `prefixes` | `list[str] \| None` | `None` | Custom prefixes. Default: `["/", "!"]`. |
1435
+
1436
+ ##### `filters.regex(pattern, flags=0) -> Regex`
1437
+
1438
+ Create a regex filter. The match object is stored on `message.pattern_match`.
1439
+
1440
+ ```python
1441
+ @bot.on_message(filters.regex(r"(\d+)"))
1442
+ async def handler(client, message):
1443
+ number = message.pattern_match.group(1)
1444
+ ```
1445
+
1446
+ ##### `filters.user(*user_ids) -> UserFilter`
1447
+
1448
+ Create a user ID filter. Matches messages from specific users.
1449
+
1450
+ ##### `filters.chat(*chat_ids) -> ChatFilter`
1451
+
1452
+ Create a chat ID filter. Matches messages from specific chats.
1453
+
1454
+ ##### `filters.text_contains(substring, case_sensitive=True) -> TextContains`
1455
+
1456
+ Filter messages that contain a specific substring.
1457
+
1458
+ ##### `filters.text_startswith(prefix) -> TextStartsWith`
1459
+
1460
+ Filter messages that start with a specific prefix.
1461
+
1462
+ ##### `filters.text_endswith(suffix) -> TextEndsWith`
1463
+
1464
+ Filter messages that end with a specific suffix.
1465
+
1466
+ ##### `filters.length(min=0, max=999999) -> Length`
1467
+
1468
+ Filter messages by text length.
1469
+
1470
+ ##### `filters.from_callable(func) -> Filter`
1471
+
1472
+ Wrap any callable as a filter. If the argument is already a `Filter`, returns it as-is.
1473
+
1474
+ #### Composition Examples
1475
+
1476
+ ```python
1477
+ # AND: private AND text
1478
+ @bot.on_message(filters.private & filters.text)
1479
+
1480
+ # OR: photo OR video
1481
+ @bot.on_message(filters.photo | filters.video)
1482
+
1483
+ # NOT: NOT outgoing
1484
+ @bot.on_message(~filters.outgoing)
1485
+
1486
+ # Complex: (private OR group) AND text AND NOT bot
1487
+ @bot.on_message((filters.private | filters.group) & filters.text & ~filters.bot)
1488
+ ```
1489
+
1490
+ ---
1491
+
1492
+ ### FSM (Finite State Machine)
1493
+
1494
+ #### State
1495
+
1496
+ Represents a single FSM state. Auto-named via the `__set_name__` descriptor protocol when assigned as a class attribute.
1497
+
1498
+ ```python
1499
+ State(name: Optional[str] = None)
1500
+ ```
1501
+
1502
+ | Property | Type | Description |
1503
+ |----------|------|-------------|
1504
+ | `name` | `str` | The state name (auto-derived from class attribute name). |
1505
+
1506
+ #### StateTransition
1507
+
1508
+ Represents a state transition with an optional async guard function.
1509
+
1510
+ ```python
1511
+ StateTransition(target: State, guard: Optional[Callable] = None)
1512
+ ```
1513
+
1514
+ ##### `async check(update) -> bool`
1515
+
1516
+ Check whether this transition is allowed. If no guard is set, always returns `True`.
1517
+
1518
+ #### FSMContext
1519
+
1520
+ Per-user context for reading/writing FSM state and data.
1521
+
1522
+ ```python
1523
+ FSMContext(storage: Storage, user_id: int, prefix: str = "fsm")
1524
+ ```
1525
+
1526
+ | Property | Type | Description |
1527
+ |----------|------|-------------|
1528
+ | `key` | `str` | Full storage key including prefix and user ID. |
1529
+ | `state_key` | `str` | Key for storing the current state. |
1530
+ | `data_key` | `str` | Key for storing user data. |
1531
+ | `user_id` | `int` | The user this context belongs to. |
1532
+
1533
+ ##### `async get_state() -> Optional[State]`
1534
+
1535
+ Get the current state for this user. Returns `None` if no state is set.
1536
+
1537
+ ##### `async set_state(state) -> None`
1538
+
1539
+ Set the current state for this user. Pass `None` to clear the state.
1540
+
1541
+ ##### `async get_data() -> dict[str, Any]`
1542
+
1543
+ Get all stored data for this user in the current state.
1544
+
1545
+ ##### `async set_data(**kwargs) -> None`
1546
+
1547
+ Update the stored data with the provided key-value pairs.
1548
+
1549
+ ##### `async get(key, default=None) -> Any`
1550
+
1551
+ Get a single data value by key.
1552
+
1553
+ ##### `async set(key, value) -> None`
1554
+
1555
+ Set a single data value.
1556
+
1557
+ ##### `async reset() -> None`
1558
+
1559
+ Clear both the state and all data for this user.
1560
+
1561
+ ##### `async finish() -> None`
1562
+
1563
+ Alias for `reset()`. Clears state and data.
1564
+
1565
+ #### StateMachine
1566
+
1567
+ Factory for `FSMContext` with decorator-based state routing.
1568
+
1569
+ ```python
1570
+ StateMachine(storage: Storage)
1571
+ ```
1572
+
1573
+ ##### `def state(state, *, on_enter=None, on_leave=None) -> Callable`
1574
+
1575
+ Decorator that registers a handler for a specific state.
1576
+
1577
+ ```python
1578
+ @sm.state(Form.name)
1579
+ async def handle_name(ctx, message):
1580
+ await ctx.set(name=message.text)
1581
+ return StateTransition(to=Form.age)
1582
+ ```
1583
+
1584
+ ##### `def on_enter(state) -> Callable`
1585
+
1586
+ Decorator that registers a callback when entering a state.
1587
+
1588
+ ##### `def on_leave(state) -> Callable`
1589
+
1590
+ Decorator that registers a callback when leaving a state.
1591
+
1592
+ ##### `def fallback() -> Callable`
1593
+
1594
+ Decorator that registers a handler for unmatched states (no registered handler for the current state).
1595
+
1596
+ ##### `def context(user_id, prefix="fsm") -> FSMContext`
1597
+
1598
+ Create an `FSMContext` for a specific user.
1599
+
1600
+ ##### `async handle(update, user_id) -> bool`
1601
+
1602
+ Route an update to the appropriate state handler. Returns `True` if a handler was found and executed.
1603
+
1604
+ #### Usage Example
1605
+
1606
+ ```python
1607
+ from spluspy.fsm import State, StateMachine, StateTransition
1608
+ from spluspy.storage import SQLiteStorage
1609
+
1610
+ storage = SQLiteStorage("fsm.db")
1611
+ sm = StateMachine(storage)
1612
+
1613
+ class Registration:
1614
+ name = State()
1615
+ age = State()
1616
+ complete = State()
1617
+
1618
+ @sm.state(Registration.name)
1619
+ async def handle_name(ctx, message):
1620
+ await ctx.set(name=message.text)
1621
+ return StateTransition(to=Registration.age)
1622
+
1623
+ @sm.state(Registration.age)
1624
+ async def handle_age(ctx, message):
1625
+ await ctx.set(age=message.text)
1626
+ return StateTransition(to=Registration.complete)
1627
+
1628
+ @sm.state(Registration.complete)
1629
+ async def handle_complete(ctx, message):
1630
+ data = await ctx.get()
1631
+ await message.reply(f"Done: {data}")
1632
+ await ctx.finish()
1633
+ ```
1634
+
1635
+ ---
1636
+
1637
+ ### Storage Backends
1638
+
1639
+ #### Storage (Abstract Base)
1640
+
1641
+ All storage backends implement this interface.
1642
+
1643
+ ```python
1644
+ async def get(key: str) -> Optional[Any]
1645
+ async def set(key: str, value: Any, ttl: Optional[int] = None) -> None
1646
+ async def delete(key: str) -> bool
1647
+ async def exists(key: str) -> bool
1648
+ async def clear() -> None
1649
+ ```
1650
+
1651
+ #### MemoryStorage
1652
+
1653
+ In-memory non-persistent storage with optional TTL support. Data is lost when the process exits.
1654
+
1655
+ ```python
1656
+ MemoryStorage()
1657
+ ```
1658
+
1659
+ **Use case:** Development, testing, short-lived sessions.
1660
+
1661
+ #### SQLiteStorage
1662
+
1663
+ Persistent SQLite-backed storage. Automatically creates the database and table on `start()`.
1664
+
1665
+ ```python
1666
+ SQLiteStorage(db_path: str = "spluspy_storage.db")
1667
+ ```
1668
+
1669
+ | Method | Description |
1670
+ |--------|-------------|
1671
+ | `async start()` | Open the database and create the table if needed. |
1672
+ | `async close()` | Close the database connection. |
1673
+
1674
+ **Use case:** Single-user bots, local persistence.
1675
+
1676
+ #### RedisStorage
1677
+
1678
+ Persistent Redis-backed storage. Requires `pip install spluspy[redis]`.
1679
+
1680
+ ```python
1681
+ RedisStorage(
1682
+ url: str = "redis://localhost:6379/0",
1683
+ prefix: str = "spluspy:",
1684
+ default_ttl: Optional[int] = None,
1685
+ )
1686
+ ```
1687
+
1688
+ | Extra Method | Description |
1689
+ |--------------|-------------|
1690
+ | `async increment(key, amount=1) -> int` | Atomically increment a counter. |
1691
+ | `async set_hash(key, mapping, ttl=None)` | Store a dict as a Redis hash. |
1692
+ | `async get_hash(key) -> dict` | Retrieve a Redis hash as a dict. |
1693
+
1694
+ **Use case:** Multi-process deployments, shared state.
1695
+
1696
+ #### PostgresStorage
1697
+
1698
+ Persistent PostgreSQL-backed storage. Requires `pip install spluspy[postgres]`.
1699
+
1700
+ ```python
1701
+ PostgresStorage(
1702
+ dsn: str = "postgresql://localhost/spluspy",
1703
+ table: str = "spluspy_kv",
1704
+ prefix: str = "",
1705
+ )
1706
+ ```
1707
+
1708
+ | Extra Method | Description |
1709
+ |--------------|-------------|
1710
+ | `async cleanup_expired() -> int` | Remove expired entries. Returns count removed. |
1711
+ | `async keys(pattern="*") -> list[str]` | List all keys matching a pattern. |
1712
+
1713
+ **Use case:** Enterprise deployments, existing PostgreSQL infrastructure.
1714
+
1715
+ #### Factory Function
1716
+
1717
+ ```python
1718
+ def get_storage(backend: str = "memory", **kwargs) -> Storage
1719
+ ```
1720
+
1721
+ Create a storage by name: `"memory"`, `"sqlite"`, `"redis"`, `"postgres"`.
1722
+
1723
+ #### EntityCache (Storage)
1724
+
1725
+ SQLite-backed peer resolution cache for caching user/chat entities.
1726
+
1727
+ ```python
1728
+ EntityCache(storage: Optional[Storage] = None)
1729
+ ```
1730
+
1731
+ | Method | Description |
1732
+ |--------|-------------|
1733
+ | `async start()` | Initialize the cache. |
1734
+ | `async get(entity_id) -> Optional[dict]` | Get entity by ID. |
1735
+ | `async get_by_username(username) -> Optional[dict]` | Get entity by username. |
1736
+ | `async get_by_phone(phone) -> Optional[dict]` | Get entity by phone. |
1737
+ | `async put(entity_id, access_hash, entity_type="user", *, username=None, phone=None)` | Store an entity. |
1738
+ | `async put_entity(entity)` | Store an entity from an object. |
1739
+ | `async put_many(entities) -> int` | Store multiple entities. Returns count stored. |
1740
+ | `async remove(entity_id) -> bool` | Remove an entity by ID. |
1741
+ | `async clear()` | Remove all cached entities. |
1742
+ | `async count() -> int` | Get the number of cached entities. |
1743
+
1744
+ ---
1745
+
1746
+ ### Middleware
1747
+
1748
+ #### Middleware (Abstract Base)
1749
+
1750
+ All middleware must implement `on_update`. The handler chain is executed in FIFO order.
1751
+
1752
+ ```python
1753
+ class Middleware(ABC):
1754
+ @abstractmethod
1755
+ async def on_update(self, update: Any, handler: Handler) -> Any
1756
+
1757
+ async def on_startup(self) -> None # Called when client starts
1758
+ async def on_shutdown(self) -> None # Called when client stops
1759
+ ```
1760
+
1761
+ #### MiddlewareManager
1762
+
1763
+ Manages an ordered chain of middleware. Errors in one middleware are isolated and don't break the chain.
1764
+
1765
+ ```python
1766
+ MiddlewareManager()
1767
+ ```
1768
+
1769
+ | Method | Description |
1770
+ |--------|-------------|
1771
+ | `add(mw: Middleware)` | Add middleware to the end of the chain. |
1772
+ | `remove(mw: Middleware)` | Remove middleware from the chain. |
1773
+ | `async execute(update, final_handler) -> Any` | Execute the middleware chain. |
1774
+ | `async startup()` | Notify all middleware of client startup. |
1775
+ | `async shutdown()` | Notify all middleware of client shutdown. |
1776
+
1777
+ #### RateLimitMiddleware
1778
+
1779
+ Rate limiting middleware for automatic flood wait handling.
1780
+
1781
+ ```python
1782
+ RateLimitMiddleware(
1783
+ rate: float = 1.0,
1784
+ capacity: float = 1.0,
1785
+ flood_sleep_threshold: int = 60,
1786
+ )
1787
+ ```
1788
+
1789
+ | Parameter | Type | Default | Description |
1790
+ |-----------|------|---------|-------------|
1791
+ | `rate` | `float` | `1.0` | Token refill rate (tokens per second). |
1792
+ | `capacity` | `float` | `1.0` | Maximum burst capacity. |
1793
+ | `flood_sleep_threshold` | `int` | `60` | Max seconds to sleep on flood wait. |
1794
+
1795
+ | Method | Description |
1796
+ |--------|-------------|
1797
+ | `async on_update(update, handler) -> Any` | Process update through rate limiter. |
1798
+ | `handle_flood_wait(method, seconds)` | Register a flood wait event. |
1799
+ | `get_stats() -> dict` | Get rate limiter statistics. |
1800
+
1801
+ #### Usage Example
1802
+
1803
+ ```python
1804
+ from spluspy.middleware import Middleware
1805
+
1806
+ class LoggingMiddleware(Middleware):
1807
+ async def on_update(self, update, handler):
1808
+ print(f"Update: {update}")
1809
+ result = await handler(update)
1810
+ print("Handler completed")
1811
+ return result
1812
+
1813
+ bot.middleware.add(LoggingMiddleware())
1814
+ ```
1815
+
1816
+ ---
1817
+
1818
+ ### Utils
1819
+
1820
+ #### LRUCache
1821
+
1822
+ Thread-safe LRU cache with optional TTL (time-to-live) for automatic expiration.
1823
+
1824
+ ```python
1825
+ LRUCache(max_size: int = 1024, default_ttl: Optional[float] = None)
1826
+ ```
1827
+
1828
+ | Method | Description |
1829
+ |--------|-------------|
1830
+ | `async get(key) -> Optional[V]` | Get a value by key. Returns `None` if not found or expired. |
1831
+ | `async set(key, value, ttl=None)` | Set a key-value pair. Optional per-entry TTL override. |
1832
+ | `async delete(key) -> bool` | Delete a key. Returns `True` if the key existed. |
1833
+ | `async exists(key) -> bool` | Check if a key exists and is not expired. |
1834
+ | `async clear()` | Remove all entries. |
1835
+ | `async size() -> int` | Get the current number of entries. |
1836
+ | `get_stats() -> dict` | Returns `size`, `max_size`, `hits`, `misses`, `hit_rate`. |
1837
+
1838
+ ```python
1839
+ cache = LRUCache(max_size=1000, ttl=300) # 5-minute TTL
1840
+ await cache.set("key", "value")
1841
+ value = await cache.get("key")
1842
+ stats = cache.get_stats() # {'hits': 42, 'misses': 3, 'hit_rate': 0.933, ...}
1843
+ ```
1844
+
1845
+ #### RateLimiter
1846
+
1847
+ Multi-endpoint rate limiter using the token bucket algorithm with flood wait handling.
1848
+
1849
+ ```python
1850
+ RateLimiter(default_rate: float = 1.0, default_capacity: float = 1.0, flood_sleep_threshold: int = 60)
1851
+ ```
1852
+
1853
+ | Method | Description |
1854
+ |--------|-------------|
1855
+ | `async acquire(method="default")` | Wait until a token is available for the given method. |
1856
+ | `register_flood_wait(method, seconds)` | Register a flood wait for the method (prevents requests until the wait expires). |
1857
+ | `limit(method="default", rate=None, capacity=None) -> Callable` | Decorator that rate-limits a function. |
1858
+ | `get_wait_time(method="default") -> float` | Get remaining wait time for the method (0 = ready). |
1859
+ | `clear_flood_wait(method)` | Clear the flood wait state for a method. |
1860
+
1861
+ #### TokenBucket
1862
+
1863
+ Low-level token bucket implementation.
1864
+
1865
+ ```python
1866
+ TokenBucket(rate: float = 1.0, capacity: float = 1.0)
1867
+ ```
1868
+
1869
+ | Method | Description |
1870
+ |--------|-------------|
1871
+ | `async acquire(tokens=1.0, blocking=True) -> bool` | Acquire tokens. If `blocking=True`, waits until available. Returns `True` on success. |
1872
+
1873
+ #### ChatLockManager
1874
+
1875
+ Per-chat granular locking to eliminate global lock contention in concurrent operations.
1876
+
1877
+ ```python
1878
+ ChatLockManager()
1879
+ ```
1880
+
1881
+ | Method | Description |
1882
+ |--------|-------------|
1883
+ | `acquire(key: int)` | Acquire a lock for a specific chat (async context manager). |
1884
+ | `release(key: int)` | Release the lock for a chat. |
1885
+ | `is_locked(key: int) -> bool` | Check if a chat is currently locked. |
1886
+ | `locked_count() -> int` | Number of currently locked chats. |
1887
+ | `pending_count() -> int` | Number of pending lock requests. |
1888
+
1889
+ #### Helper Functions
1890
+
1891
+ ```python
1892
+ def generate_random_id() -> int
1893
+ # Generate a random 64-bit integer ID.
1894
+
1895
+ def timestamp_to_datetime(ts: Optional[int]) -> Optional[datetime]
1896
+ # Convert a Unix timestamp to a datetime object.
1897
+
1898
+ def datetime_to_timestamp(dt: Optional[Union[datetime, date]]) -> Optional[int]
1899
+ # Convert a datetime/date to a Unix timestamp.
1900
+
1901
+ def sanitize_filename(name: str) -> str
1902
+ # Remove or replace unsafe characters from a filename.
1903
+
1904
+ def parse_mode(mode: Optional[str]) -> Optional[str]
1905
+ # Normalize a parse mode string.
1906
+
1907
+ def chunk_list(items: list, size: int) -> list[list]
1908
+ # Split a list into chunks of the given size.
1909
+
1910
+ def truncate(text: str, max_length: int = 4096) -> str
1911
+ # Truncate text to a maximum length, adding "..." if truncated.
1912
+ ```
1913
+
1914
+ #### Batch Operations
1915
+
1916
+ ```python
1917
+ async def batch_send(client, chat_id, messages, *, delay=0.1, parse_mode=None) -> list[Message]
1918
+ # Send multiple text messages sequentially with a delay between each.
1919
+
1920
+ async def batch_delete(client, chat_id, messages, *, batch_size=100) -> bool
1921
+ # Delete multiple messages in batches.
1922
+
1923
+ async def batch_forward(client, target_chat, source_chat, messages, *, batch_size=100, delay=0.1) -> list[Message]
1924
+ # Forward multiple messages in batches.
1925
+
1926
+ async def batch_get_messages(client, chat_id, message_ids, *, batch_size=100) -> list[Message]
1927
+ # Fetch multiple messages by ID in batches.
1928
+ ```
1929
+
1930
+ #### File Transfer
1931
+
1932
+ ```python
1933
+ class TransferProgress:
1934
+ total: int # Total file size in bytes
1935
+ transferred: int # Bytes transferred so far
1936
+ speed: float # Transfer speed in bytes/sec
1937
+ elapsed: float # Elapsed time in seconds
1938
+ eta: float # Estimated time remaining in seconds
1939
+ percent -> float # Completion percentage (0-100)
1940
+
1941
+ class ProgressTracker:
1942
+ ProgressTracker(callback=None, update_interval=0.1)
1943
+ def update(self, chunk_size: int) -> None # Called after each chunk
1944
+ def finish(self) -> None # Called when transfer completes
1945
+ ```
1946
+
1947
+ ```python
1948
+ # Upload with progress
1949
+ tracker = ProgressTracker(on_progress=lambda p: print(f"{p.percent}%"))
1950
+ await bot.send_document(chat_id, "large_file.zip", progress=tracker)
1951
+
1952
+ # Download with progress
1953
+ await message.download(progress=tracker)
1954
+ ```
1955
+
1956
+ #### TargetResolver
1957
+
1958
+ Resolves message targets from various sources (reply, username, ID, etc.).
1959
+
1960
+ ```python
1961
+ TargetResolver(client: Client)
1962
+ ```
1963
+
1964
+ | Method | Description |
1965
+ |--------|-------------|
1966
+ | `async resolve(message, *, args=None) -> TargetResolution` | Resolve a target from a message context. |
1967
+
1968
+ ```python
1969
+ @dataclass
1970
+ class TargetResolution:
1971
+ user_id: Optional[int] # Resolved user ID
1972
+ message_id: Optional[int] # Resolved message ID
1973
+ source: str # Resolution source: "reply", "username", "id", etc.
1974
+ display_name: Optional[str] # Human-readable name
1975
+ ```
1976
+
1977
+ ```python
1978
+ def resolve_reply_sender(message: Message) -> Optional[int]
1979
+ # Synchronously extract the sender ID from a reply header.
1980
+ ```
1981
+
1982
+ #### Logger Utilities
1983
+
1984
+ ```python
1985
+ def setup_logging(level="INFO", log_file=None, log_dir="logs", max_bytes=10*1024*1024,
1986
+ backup_count=5, use_color=True, console_output=True,
1987
+ reconnect_only=False, errors_only=False) -> None
1988
+ # Configure the logging system.
1989
+
1990
+ def get_logger(name=None, level=None) -> logging.Logger
1991
+ # Get a named logger instance.
1992
+
1993
+ def set_level(level: Union[str, int]) -> None
1994
+ # Change the global log level.
1995
+
1996
+ def log() -> logging.Logger
1997
+ # Get the default root logger.
1998
+
1999
+ def log_event(logger, event: str, **data) -> None
2000
+ # Log a structured event.
2001
+
2002
+ def log_request(logger, method: str, **params) -> None
2003
+ # Log an API request.
2004
+
2005
+ def log_response(logger, method: str, status="OK", **data) -> None
2006
+ # Log an API response.
2007
+
2008
+ def log_error(logger, error: Exception, context="") -> None
2009
+ # Log an error with context.
2010
+
2011
+ def log_user_action(logger, action: str, user_id: int, chat_id=0, target_id=0, detail="") -> None
2012
+ # Log a user action.
2013
+
2014
+ def log_security(logger, event: str, user_id: int, chat_id=0, reason="") -> None
2015
+ # Log a security-related event.
2016
+
2017
+ def log_performance(logger, operation: str, duration_ms: float, **extra) -> None
2018
+ # Log a performance measurement.
2019
+ ```
2020
+
2021
+ #### Version Check
2022
+
2023
+ ```python
2024
+ async def check_for_update() -> Optional[str]
2025
+ # Check PyPI for a newer version. Returns version string or None.
2026
+
2027
+ def print_update_notice(new_version: str) -> None
2028
+ # Print a formatted update notice to the console.
2029
+
2030
+ async def check_and_notify() -> None
2031
+ # Check and print update notice if available. Call at startup.
2032
+ ```
2033
+
2034
+ ---
2035
+
2036
+ ### AfkManager
2037
+
2038
+ Smart auto-responder for AFK (Away From Keyboard) mode with per-chat rate limiting.
2039
+
2040
+ #### Constructor
2041
+
2042
+ ```python
2043
+ AfkManager(
2044
+ client: Client,
2045
+ message: str = "I'm currently AFK. I'll reply when I'm back.",
2046
+ *,
2047
+ cooldown: float = 60.0,
2048
+ max_replies: Optional[int] = 10,
2049
+ only_private: bool = False,
2050
+ only_mentions: bool = False,
2051
+ )
2052
+ ```
2053
+
2054
+ | Parameter | Type | Default | Description |
2055
+ |-----------|------|---------|-------------|
2056
+ | `client` | `Client` | (required) | The SplusPy client instance. |
2057
+ | `message` | `str` | auto-reply text | Default AFK response message. |
2058
+ | `cooldown` | `float` | `60.0` | Minimum seconds between replies to the same user. |
2059
+ | `max_replies` | `int \| None` | `10` | Maximum total auto-replies per AFK session. `None` = unlimited. |
2060
+ | `only_private` | `bool` | `False` | If `True`, only auto-reply in private chats. |
2061
+ | `only_mentions` | `bool` | `False` | If `True`, only auto-reply when mentioned. |
2062
+
2063
+ #### Properties
2064
+
2065
+ | Property | Type | Description |
2066
+ |----------|------|-------------|
2067
+ | `is_afk` | `bool` | Whether AFK mode is currently active. |
2068
+ | `afk_reason` | `str` | The reason for being AFK. |
2069
+ | `afk_since` | `float \| None` | Unix timestamp when AFK mode started. |
2070
+ | `total_replies` | `int` | Total number of auto-replies sent during this AFK session. |
2071
+
2072
+ #### Methods
2073
+
2074
+ ##### `set_afk(enabled, *, reason="", message=None) -> None`
2075
+
2076
+ Enable or disable AFK mode.
2077
+
2078
+ | Parameter | Type | Default | Description |
2079
+ |-----------|------|---------|-------------|
2080
+ | `enabled` | `bool` | (required) | `True` to activate AFK, `False` to deactivate. |
2081
+ | `reason` | `str` | `""` | Reason for being AFK (shown in the auto-reply). |
2082
+ | `message` | `str \| None` | `None` | Override the default AFK message for this session. |
2083
+
2084
+ ##### `async handle(message) -> bool`
2085
+
2086
+ Process an incoming message while AFK. Handles rate limiting and auto-reply. Returns `True` if a reply was sent.
2087
+
2088
+ ##### `get_stats() -> dict`
2089
+
2090
+ Returns a dictionary with keys: `is_afk`, `afk_reason`, `afk_since`, `total_replies`, `active_chats`, `cooldown`, `max_replies`.
2091
+
2092
+ #### Usage Example
2093
+
2094
+ ```python
2095
+ from spluspy import Client, filters
2096
+ from spluspy.afk import AfkManager
2097
+
2098
+ bot = Client("my_account")
2099
+ afk = AfkManager(bot, message="I'm currently AFK. Back soon!")
2100
+
2101
+ @bot.on_message(filters.command("afk"))
2102
+ async def set_afk(client, message):
2103
+ afk.set_afk(True, reason="Lunch break")
2104
+ await message.reply("I'm now AFK!")
2105
+
2106
+ @bot.on_message(filters.command("back"))
2107
+ async def unset_afk(client, message):
2108
+ afk.set_afk(False)
2109
+ await message.reply(f"Back! Sent {afk.total_replies} auto-replies.")
2110
+
2111
+ @bot.on_message(filters.private & filters.incoming)
2112
+ async def auto_reply(client, message):
2113
+ if afk.is_afk:
2114
+ await afk.handle(message)
2115
+ ```
2116
+
2117
+ ---
2118
+
2119
+ ### ChatAdmin
2120
+
2121
+ High-level chat administration API with batch operations and admin logging.
2122
+
2123
+ #### Constructor
2124
+
2125
+ ```python
2126
+ ChatAdmin(client: Client)
2127
+ ```
2128
+
2129
+ #### Methods
2130
+
2131
+ ##### `async ban_user(chat_id, user_id, *, delete_messages=False) -> bool`
2132
+
2133
+ Ban a user from a chat. Optionally delete their recent messages.
2134
+
2135
+ ##### `async unban_user(chat_id, user_id) -> bool`
2136
+
2137
+ Unban a user, allowing them to rejoin.
2138
+
2139
+ ##### `async mute_user(chat_id, user_id, *, duration=None) -> bool`
2140
+
2141
+ Mute a user. `duration` in seconds; `None` = permanent.
2142
+
2143
+ ##### `async unmute_user(chat_id, user_id) -> bool`
2144
+
2145
+ Unmute a user.
2146
+
2147
+ ##### `async pin_chat_message(chat_id, message_id, notify=True) -> bool`
2148
+
2149
+ Pin a message by ID.
2150
+
2151
+ ##### `async unpin_chat_message(chat_id, message_id) -> bool`
2152
+
2153
+ Unpin a message by ID.
2154
+
2155
+ ##### `async pin_message(chat_id, message, *, notify=False) -> bool`
2156
+
2157
+ Pin a message (accepts `Message` object or int).
2158
+
2159
+ ##### `async unpin_message(chat_id, message) -> bool`
2160
+
2161
+ Unpin a message (accepts `Message` object or int).
2162
+
2163
+ ##### `async unpin_all(chat_id) -> bool`
2164
+
2165
+ Unpin all pinned messages in a chat.
2166
+
2167
+ ##### `async purge_messages(chat_id, *, limit=100, before=None, after=None) -> int`
2168
+
2169
+ Delete a batch of messages. Returns the count of deleted messages.
2170
+
2171
+ | Parameter | Type | Default | Description |
2172
+ |-----------|------|---------|-------------|
2173
+ | `chat_id` | `int` | (required) | Target chat. |
2174
+ | `limit` | `int` | `100` | Maximum messages to delete. |
2175
+ | `before` | `int \| None` | `None` | Only delete messages with ID less than this. |
2176
+ | `after` | `int \| None` | `None` | Only delete messages with ID greater than this. |
2177
+
2178
+ ##### `async get_admin_log(chat_id, *, limit=100) -> list[dict]`
2179
+
2180
+ Get admin event log. Returns list of dicts with `id`, `date`, `user_id`, `action` keys.
2181
+
2182
+ ##### `async bulk_action(chat_id, action, user_ids, *, delay=0.5, **kwargs) -> dict[int, bool]`
2183
+
2184
+ Perform bulk ban/unban/mute/unmute. Returns a dict mapping `user_id -> success`.
2185
+
2186
+ | Parameter | Type | Description |
2187
+ |-----------|------|-------------|
2188
+ | `action` | `str` | One of: `"ban"`, `"unban"`, `"mute"`, `"unmute"`. |
2189
+ | `user_ids` | `list[int]` | List of user IDs to action. |
2190
+ | `delay` | `float` | Delay between each action (to avoid flood). |
2191
+
2192
+ ---
2193
+
2194
+ ### MessageMirror
2195
+
2196
+ Real-time message cloning between chats.
2197
+
2198
+ #### Constructor
2199
+
2200
+ ```python
2201
+ MessageMirror(client: Client)
2202
+ ```
2203
+
2204
+ #### Methods
2205
+
2206
+ ##### `add_route(source, targets, *, filter_func=None, strip_forward=True, strip_sender=False, add_prefix=None) -> None`
2207
+
2208
+ Add a complete mirroring route from a source chat to one or more target chats.
2209
+
2210
+ | Parameter | Type | Default | Description |
2211
+ |-----------|------|---------|-------------|
2212
+ | `source` | `int` | (required) | Source chat ID to mirror from. |
2213
+ | `targets` | `list[int]` | (required) | Target chat IDs to mirror to. |
2214
+ | `filter_func` | `Callable \| None` | `None` | Optional filter function: `fn(message) -> bool`. |
2215
+ | `strip_forward` | `bool` | `True` | Remove forward headers from mirrored messages. |
2216
+ | `strip_sender` | `bool` | `False` | Remove sender info from mirrored messages. |
2217
+ | `add_prefix` | `str \| None` | `None` | Add a text prefix to mirrored messages. |
2218
+
2219
+ ##### `add_source(source) -> None`
2220
+
2221
+ Add a source chat for mirroring.
2222
+
2223
+ ##### `add_target(target, *, source=None) -> None`
2224
+
2225
+ Add a target chat. If `source` is `None`, targets the most recently added source.
2226
+
2227
+ ##### `remove_route(source) -> None`
2228
+
2229
+ Remove a mirroring route by source chat ID.
2230
+
2231
+ ##### `async start() -> None`
2232
+
2233
+ Start the mirroring engine. Begins listening for new messages.
2234
+
2235
+ ##### `async stop() -> None`
2236
+
2237
+ Stop the mirroring engine.
2238
+
2239
+ ##### `get_stats() -> dict`
2240
+
2241
+ Returns: `active` (bool), `routes` (int), `total_mirrored` (int), `sources` (list of ints).
2242
+
2243
+ #### Usage Example
2244
+
2245
+ ```python
2246
+ from spluspy.mirror import MessageMirror
2247
+
2248
+ mirror = MessageMirror(bot)
2249
+
2250
+ mirror.add_route(
2251
+ source=-1001234567890,
2252
+ targets=[-1009876543210, -1001112223334],
2253
+ strip_forward=True,
2254
+ strip_sender=False,
2255
+ add_prefix="[Mirror]"
2256
+ )
2257
+
2258
+ await mirror.start()
2259
+ ```
2260
+
2261
+ ---
2262
+
2263
+ ### Scheduler
2264
+
2265
+ #### Scheduler (Low-level)
2266
+
2267
+ Manages scheduled and recurring tasks.
2268
+
2269
+ ```python
2270
+ Scheduler()
2271
+ ```
2272
+
2273
+ | Property | Type | Description |
2274
+ |----------|------|-------------|
2275
+ | `running` | `bool` | Whether the scheduler is running. |
2276
+
2277
+ | Method | Description |
2278
+ |--------|-------------|
2279
+ | `add(name, callback, *, interval=None, delay=0.0, max_runs=None) -> ScheduledTask` | Schedule a task. |
2280
+ | `remove(name) -> bool` | Remove a task by name. |
2281
+ | `get(name) -> Optional[ScheduledTask]` | Get a task by name. |
2282
+ | `async start()` | Start the scheduler loop. |
2283
+ | `async stop()` | Stop the scheduler loop. |
2284
+
2285
+ #### MessageScheduler
2286
+
2287
+ High-level message scheduling for bots.
2288
+
2289
+ ```python
2290
+ MessageScheduler(client: Client)
2291
+ ```
2292
+
2293
+ | Property | Type | Description |
2294
+ |----------|------|-------------|
2295
+ | `pending_count` | `int` | Number of pending scheduled tasks. |
2296
+
2297
+ | Method | Description |
2298
+ |--------|-------------|
2299
+ | `async start()` | Start the scheduler. |
2300
+ | `async stop()` | Stop the scheduler. |
2301
+ | `schedule_message(chat_id, text, *, delay=None, when=None, name=None) -> ScheduledTask` | Schedule a single message. |
2302
+ | `schedule_interval(chat_id, text, *, interval, name=None, max_runs=None) -> ScheduledTask` | Schedule a recurring message. |
2303
+ | `schedule_callback(name, callback, *, delay=None, interval=None, max_runs=None) -> ScheduledTask` | Schedule a custom callback. |
2304
+ | `cancel(name) -> bool` | Cancel a scheduled task by name. |
2305
+
2306
+ #### Usage Example
2307
+
2308
+ ```python
2309
+ from spluspy.scheduler.scheduler import MessageScheduler
2310
+
2311
+ scheduler = MessageScheduler(bot)
2312
+
2313
+ # Send a message every hour
2314
+ scheduler.schedule_interval("hourly_greeting", chat_id, "Hello!", interval=3600)
2315
+
2316
+ # Send a message after a delay (5 minutes)
2317
+ scheduler.schedule_once("reminder", chat_id, "Don't forget!", delay=300)
2318
+
2319
+ # Cancel a scheduled task
2320
+ scheduler.cancel("hourly_greeting")
2321
+ ```
2322
+
2323
+ ---
2324
+
2325
+ ### Error Hierarchy
2326
+
2327
+ All errors inherit from `SplusPyError(Exception)`.
2328
+
2329
+ ```
2330
+ SplusPyError
2331
+ ├── SoroushPlusAPIError(message, code=0)
2332
+ │ ├── RPCError(message, code=0)
2333
+ │ │ ├── FloodWait(seconds, message="") # Has .seconds attribute
2334
+ │ │ ├── Unauthorized(message, code=0)
2335
+ │ │ ├── Forbidden(message, code=0)
2336
+ │ │ ├── BadRequest(message, code=0)
2337
+ │ │ │ ├── ChatNotFound(message, code=0)
2338
+ │ │ │ ├── UserNotFound(message, code=0)
2339
+ │ │ │ └── MessageNotFound(message, code=0)
2340
+ │ │ └── ...
2341
+ │ ├── SessionExpiredError(message)
2342
+ │ └── SessionError(message)
2343
+ ├── AuthError(message)
2344
+ ├── ValidationError(message)
2345
+ ├── TimeoutError(message)
2346
+ ├── ConnectionError(message)
2347
+ ├── PluginError(message)
2348
+ ├── FSMError(message)
2349
+ ├── StorageError(message)
2350
+ ├── JoinChatError(message, link="")
2351
+ │ ├── InvalidInviteLinkError(link, reason="...")
2352
+ │ ├── InviteLinkExpiredError(link="", reason="...")
2353
+ │ ├── ChatFullError(chat_id="", limit=0)
2354
+ │ ├── ChatDeactivatedError(chat_id="")
2355
+ │ └── MembershipRequiredError(chat_id="", reason="...")
2356
+ └── FloodWaitError(message) # From rate limiter
2357
+ ```
2358
+
2359
+ #### Error Handling Example
2360
+
2361
+ ```python
2362
+ from spluspy.errors import FloodWait, Unauthorized, BadRequest
2363
+
2364
+ @bot.on_message()
2365
+ async def safe_handler(client, message):
2366
+ try:
2367
+ await message.reply("Hello!")
2368
+ except FloodWait as e:
2369
+ await asyncio.sleep(e.seconds) # Wait the required time
2370
+ except Unauthorized:
2371
+ await message.reply("Not authorized!")
2372
+ except BadRequest as e:
2373
+ print(f"Bad request: {e}")
2374
+ ```
2375
+
2376
+ #### Global Error Handler
2377
+
2378
+ ```python
2379
+ from spluspy.events import ErrorHandlerBuilder
2380
+
2381
+ error_handler = ErrorHandlerBuilder()
2382
+ error_handler.on(FloodWait)(lambda e: print(f"Flood wait: {e.seconds}s"))
2383
+ error_handler.on(Unauthorized)(lambda e: print("Unauthorized"))
2384
+
2385
+ bot.on_error(error_handler.build())
2386
+ ```
2387
+
2388
+ ---
2389
+
2390
+ ### Plugin System
2391
+
2392
+ #### Plugin
2393
+
2394
+ Represents a loaded plugin.
2395
+
2396
+ ```python
2397
+ Plugin(name: str, module: Any, enabled: bool = True)
2398
+ ```
2399
+
2400
+ | Attribute | Type | Description |
2401
+ |-----------|------|-------------|
2402
+ | `name` | `str` | Plugin name (derived from module filename). |
2403
+ | `module` | `Any` | The loaded Python module. |
2404
+ | `enabled` | `bool` | Whether the plugin is enabled. |
2405
+
2406
+ #### PluginManager
2407
+
2408
+ Discovers, loads, and manages plugins.
2409
+
2410
+ ```python
2411
+ PluginManager(client: Client)
2412
+ ```
2413
+
2414
+ | Property | Type | Description |
2415
+ |----------|------|-------------|
2416
+ | `plugins` | `dict[str, Plugin]` | Mapping of loaded plugins by name. |
2417
+
2418
+ | Method | Description |
2419
+ |--------|-------------|
2420
+ | `load(path) -> list[Plugin]` | Load all plugins from a directory or single module file. |
2421
+ | `unload(name) -> bool` | Unload a plugin by name. Returns `True` if unloaded. |
2422
+ | `reload(name) -> Optional[Plugin]` | Reload a plugin. Returns the reloaded `Plugin` or `None`. |
2423
+
2424
+ #### Plugin Structure
2425
+
2426
+ Plugins are Python modules that define a `register(client)` function:
2427
+
2428
+ ```python
2429
+ # plugins/hello.py
2430
+ def register(client):
2431
+ @client.on_message(filters.command("hello"))
2432
+ async def hello_handler(client, message):
2433
+ await message.reply("Hello from plugin!")
2434
+ ```
2435
+
2436
+ ```python
2437
+ # main.py
2438
+ from spluspy import Client
2439
+
2440
+ bot = Client("session")
2441
+ bot.plugins.load("plugins") # Load all plugins from the "plugins" directory
2442
+ bot.run()
2443
+ ```
2444
+
2445
+ ---
2446
+
2447
+ ### Sync Wrapper
2448
+
2449
+ The `spluspy.sync` module provides a synchronous wrapper around the async `Client`. Every async method is wrapped so it can be called without `await`.
2450
+
2451
+ ```python
2452
+ from spluspy.sync import Client
2453
+
2454
+ bot = Client("session")
2455
+
2456
+ @bot.on_message()
2457
+ def handler(client, message):
2458
+ # No async/await needed!
2459
+ message.reply("Hello!")
2460
+
2461
+ bot.run()
2462
+ ```
2463
+
2464
+ The sync `Client` has the same constructor and all the same methods as the async `Client`, but without `async/await`:
2465
+
2466
+ ```python
2467
+ # Lifecycle
2468
+ bot.start()
2469
+ bot.stop()
2470
+ bot.connect()
2471
+ bot.disconnect()
2472
+ bot.run()
2473
+
2474
+ # Messages (no await!)
2475
+ bot.send_message(chat_id, "Hello!")
2476
+ bot.edit_message(message, text="New text")
2477
+ bot.delete_messages(msg1, msg2)
2478
+
2479
+ # All other methods work the same way
2480
+ bot.get_me()
2481
+ bot.get_messages(chat_id)
2482
+ bot.ban_user(chat_id, user_id)
2483
+ ```
2484
+
2485
+ ---
2486
+
2487
+ ### Enums
2488
+
2489
+ ```python
2490
+ class ChatType(str, Enum):
2491
+ PRIVATE = "private"
2492
+ GROUP = "group"
2493
+ SUPERGROUP = "supergroup"
2494
+ CHANNEL = "channel"
2495
+
2496
+ class MessageMediaType(str, Enum):
2497
+ NONE = "none"
2498
+ PHOTO = "photo"
2499
+ VIDEO = "video"
2500
+ AUDIO = "audio"
2501
+ DOCUMENT = "document"
2502
+ VOICE = "voice"
2503
+ VIDEO_NOTE = "video_note"
2504
+ STICKER = "sticker"
2505
+ ANIMATION = "animation"
2506
+ CONTACT = "contact"
2507
+ LOCATION = "location"
2508
+ POLL = "poll"
2509
+ DICE = "dice"
2510
+
2511
+ class ParseMode(str, Enum):
2512
+ NONE = "none"
2513
+ MARKDOWN = "markdown"
2514
+ MARKDOWN_V2 = "markdown_v2"
2515
+ HTML = "html"
2516
+
2517
+ class UserStatus(str, Enum):
2518
+ ONLINE = "online"
2519
+ OFFLINE = "offline"
2520
+ LAST_SEEN = "last_seen"
2521
+ RECENTLY = "recently"
2522
+ LAST_WEEK = "last_week"
2523
+ LAST_MONTH = "last_month"
2524
+ LONG_AGO = "long_ago"
2525
+ EMPTY = "empty"
2526
+
2527
+ class ChatMemberStatus(IntEnum):
2528
+ CREATOR = 4
2529
+ ADMIN = 3
2530
+ MEMBER = 2
2531
+ RESTRICTED = 1
2532
+ LEFT = 0
2533
+ BANNED = -1
2534
+ ```
2535
+
2536
+ ---
2537
+
2538
+ ### Media Types
2539
+
2540
+ All media types are dataclasses with `slots=True`, inheriting from `Media`.
2541
+
2542
+ | Class | Key Attributes |
2543
+ |-------|----------------|
2544
+ | `Media` | `media_type`, `raw` |
2545
+ | `Photo` | `width`, `height`, `file_id`, `file_size`, `file_name` |
2546
+ | `Video` | `width`, `height`, `duration`, `file_id`, `file_size`, `file_name`, `thumb` |
2547
+ | `Audio` | `duration`, `performer`, `title`, `file_id`, `file_size`, `file_name` |
2548
+ | `Document` | `file_id`, `file_size`, `file_name`, `mime_type` |
2549
+ | `Voice` | `duration`, `file_id`, `file_size`, `mime_type` |
2550
+ | `VideoNote` | `duration`, `file_id`, `file_size`, `mime_type` |
2551
+ | `Sticker` | `sticker_id`, `emoji`, `set_name`, `width`, `height`, `is_animated` |
2552
+ | `Animation` | `file_name`, `width`, `height`, `duration`, `file_id`, `file_size` |
2553
+ | `Contact` | `phone_number`, `first_name`, `last_name`, `user_id` |
2554
+ | `Location` | `latitude`, `longitude` |
2555
+ | `PollMedia` / `Poll` | `question`, `options`, `is_anonymous`, `total_voter_count` |
2556
+ | `Dice` | `emoji`, `value` |
2557
+
2558
+ ---
2559
+
2560
+ ### Object Types
2561
+
2562
+ | Class | Attributes |
2563
+ |-------|------------|
2564
+ | `MessageEntity` | `type`, `offset`, `length`, `url`, `user_id`, `language`, `custom_emoji_id` |
2565
+ | `InlineKeyboardButton` | `text`, `callback_data`, `url`, `switch_inline_query`, `switch_inline_query_current_chat` |
2566
+ | `KeyboardButton` | `text`, `request_contact`, `request_location` |
2567
+ | `ReplyMarkup` | `inline_keyboard`, `keyboard`, `is_one_time`, `is_resize`, `selective`, `placeholder` |
2568
+ | `ForceReply` | `selective`, `placeholder` |
2569
+ | `MessageReplyHeader` | `reply_to_msg_id`, `reply_to_peer_id`, `quote` |
2570
+
2571
+ ---
2572
+
2573
+ ### Project Structure
2574
+
2575
+ ```
2576
+ spluspy/
2577
+ ├── __init__.py # Public API
2578
+ ├── __version__.py # Version info
2579
+ ├── cli.py # CLI entry point
2580
+ ├── config.py # Configuration
2581
+ ├── compat.py # Compatibility layer
2582
+ ├── afk.py # AFK auto-responder
2583
+ ├── admin.py # Chat administration
2584
+ ├── mirror.py # Message mirroring engine
2585
+ ├── client/ # Client and conversation API
2586
+ │ ├── client.py # Main Client class
2587
+ │ ├── conversation.py # Conversation API
2588
+ │ ├── chat_mixin.py # Chat management mixin
2589
+ │ └── media_mixin.py # Media operations mixin
2590
+ ├── models/ # Domain models (Message, User, Chat, Media, etc.)
2591
+ │ ├── message.py # Message dataclass
2592
+ │ ├── user.py # User dataclass
2593
+ │ ├── chat.py # Chat, Channel, Group dataclasses
2594
+ │ ├── bot.py # Button factory
2595
+ │ ├── media.py # Media type dataclasses
2596
+ │ ├── objects.py # MessageEntity, ReplyMarkup, etc.
2597
+ │ └── enums.py # ChatType, UserStatus, etc.
2598
+ ├── events/ # Event types and builders
2599
+ ├── filters/ # Composable message filters
2600
+ ├── errors/ # Custom exception hierarchy
2601
+ ├── session/ # Session backends (SQLite, Memory, String)
2602
+ ├── network/ # TCP connections and connection pool
2603
+ ├── storage/ # Key-value storage backends
2604
+ ├── plugins/ # Plugin loader
2605
+ ├── middleware/ # Middleware system
2606
+ ├── fsm/ # Finite state machine
2607
+ ├── scheduler/ # Task scheduler
2608
+ ├── utils/ # Logger, cache, helpers
2609
+ ├── sync/ # Synchronous client wrapper
2610
+ └── _engine/ # Low-level MTProto engine
2611
+ ```
2612
+
2613
+ ---
2614
+
2615
+ ### CLI
2616
+
2617
+ ```bash
2618
+ # Run a bot
2619
+ spluspy run bot.py
2620
+
2621
+ # Run with custom session name
2622
+ spluspy run bot.py --session my_bot
2623
+
2624
+ # Get session info
2625
+ spluspy session-info my_session.session
2626
+
2627
+ # Show version
2628
+ spluspy version
2629
+
2630
+ # Validate a bot script for syntax errors
2631
+ spluspy validate bot.py
2632
+ ```
2633
+
2634
+ ### Docker
2635
+
2636
+ ```dockerfile
2637
+ FROM python:3.12-slim
2638
+
2639
+ WORKDIR /app
2640
+
2641
+ # Install system dependencies
2642
+ RUN apt-get update && apt-get install -y --no-install-recommends \
2643
+ gcc \
2644
+ && rm -rf /var/lib/apt/lists/*
2645
+
2646
+ # Install spluspy
2647
+ RUN pip install --no-cache-dir spluspy[all]
2648
+
2649
+ # Copy your bot script
2650
+ COPY bot.py .
2651
+
2652
+ # Run the bot
2653
+ CMD ["python", "bot.py"]
2654
+ ```
2655
+
2656
+ Build and run:
2657
+
2658
+ ```bash
2659
+ docker build -t my-spluspy-bot .
2660
+ docker run -v ./sessions:/app/sessions my-spluspy-bot
2661
+ ```
2662
+
2663
+ ---
2664
+
2665
+ ## فارسی
2666
+
2667
+ ### ویژگی‌ها
2668
+
2669
+ | ویژگی | توضیحات |
2670
+ |-------|---------|
2671
+ | **بدون نیاز به API Key** | کلیدهای سروش پلاس به صورت داخلی |
2672
+ | **ناهمگام کامل** | ساخته شده با `asyncio` پایتون |
2673
+ | **پشتیبانی از حالت همگام** | استفاده بدون `async/await` از طریق `spluspy.sync` |
2674
+ | **پشتیبانی ربات و کاربر** | هر دو نوع حساب |
2675
+ | **هندلرهای رویدادمحور** | سیستم رویداد قدرتمند با دکوراتورها |
2676
+ | **سیستم فیلتر** | فیلترهای قابل ترکیب (`&`, `\|`, `~`) |
2677
+ | **دکمه‌های اینلاین و ریپلای** | کیبوردهای تعاملی |
2678
+ | **API مکالمه** | برای جریان‌های تعاملی ربات |
2679
+ | **FSM (ماشین حالت محدود)** | مدیریت وضعیت داخلی برای ربات‌ها |
2680
+ | **سیستم پلاگین** | بارگذاری پویای پلاگین‌ها |
2681
+ | **میان‌افزار (Middleware)** | پردازش قبل/بعد از به‌روزرسانی‌ها |
2682
+ | **زمان‌بند (Scheduler)** | زمان‌بندی داخلی وظایف |
2683
+ | **بک‌اندهای ذخیره‌سازی متعدد** | حافظه، SQLite، Redis، PostgreSQL |
2684
+ | **محدودیت نرخ (Rate Limiting)** | الگوریتم سطل توکن با مدیریت انتظار سیلاب |
2685
+ | **پاسخ خودکار AFK** | پاسخگوی هوشمند با محدودیت نرخ به ازای هر چت |
2686
+ | **مدیریت چت** | مسدود کردن، بی‌صدا کردن، سنجاق، پاکسازی — API مدیریتی |
2687
+ | **آینه‌سازی پیام** | کلون کردن پیام و رسانه به صورت بلادرنگ بین چت‌ها |
2688
+ | **عملیات دسته‌ای** | ارسال، حذف، فوروارد چندین پیام به صورت همزمان |
2689
+ | **انتقال فایل با پیشرفت** | آپلود/دانلود با ردیابی پیشرفت |
2690
+ | **لاگ حرفه‌ای** | لاگرهای ساختاریافته و فضای نام‌دار |
2691
+ | **نوع‌نویسی در همه جا** | ایمنی کامل نوع |
2692
+ | **معماری تمیز** | اصول SOLID، طراحی ماژولار |
2693
+
2694
+ ### پیش‌نیازها
2695
+
2696
+ - پایتون 3.10 به بالا
2697
+ - نیازی به کلید API خارجی نیست (کلیدهای سروش پلاس به صورت داخلی)
2698
+
2699
+ ### نصب
2700
+
2701
+ ```bash
2702
+ pip install spluspy
2703
+ ```
2704
+
2705
+ برای رمزگذاری سریع‌تر:
2706
+
2707
+ ```bash
2708
+ pip install spluspy[speed]
2709
+ ```
2710
+
2711
+ با بک‌اند Redis:
2712
+
2713
+ ```bash
2714
+ pip install spluspy[redis]
2715
+ ```
2716
+
2717
+ با بک‌اند PostgreSQL:
2718
+
2719
+ ```bash
2720
+ pip install spluspy[postgres]
2721
+ ```
2722
+
2723
+ همه وابستگی‌های اختیاری:
2724
+
2725
+ ```bash
2726
+ pip install spluspy[all]
2727
+ ```
2728
+
2729
+ ### شروع سریع
2730
+
2731
+ #### ساده‌ترین ربات
2732
+
2733
+ ```python
2734
+ from spluspy import Client
2735
+
2736
+ bot = Client("my_session")
2737
+
2738
+ @bot.on_message()
2739
+ async def handler(client, message):
2740
+ await message.reply("سلام!")
2741
+
2742
+ bot.run()
2743
+ ```
2744
+
2745
+ #### حساب کاربری
2746
+
2747
+ ```python
2748
+ from spluspy import Client
2749
+
2750
+ client = Client("session_name")
2751
+
2752
+ @client.on_message()
2753
+ async def handler(client, message):
2754
+ await message.reply("سلام دنیا!")
2755
+
2756
+ async def main():
2757
+ await client.start(phone="+98XXXXXXXXXX")
2758
+ await client.run_until_disconnected()
2759
+
2760
+ import asyncio
2761
+ asyncio.run(main())
2762
+ ```
2763
+
2764
+ #### استفاده همگام (بدون Async/Await)
2765
+
2766
+ ```python
2767
+ from spluspy.sync import Client
2768
+
2769
+ bot = Client("session")
2770
+
2771
+ @bot.on_message()
2772
+ def handler(client, message):
2773
+ message.reply("سلام!")
2774
+
2775
+ bot.run()
2776
+ ```
2777
+
2778
+ ### رویدادها
2779
+
2780
+ | دکوراتور | رویداد |
2781
+ |----------|--------|
2782
+ | `@bot.on_message()` | پیام جدید |
2783
+ | `@bot.on_edited_message()` | ویرایش پیام |
2784
+ | `@bot.on_callback_query()` | کلیک دکمه اینلاین |
2785
+ | `@bot.on_inline_query()` | کوئری اینلاین |
2786
+ | `@bot.on_chat_action()` | پیوستن/خروج/سنجاق |
2787
+ | `@bot.on_user_update()` | تغییر وضعیت |
2788
+ | `@bot.on_message_deleted()` | حذف پیام |
2789
+ | `@bot.on_message_read()` | رسید خواندن |
2790
+ | `@bot.on_error()` | مدیریت خطای سراسری |
2791
+
2792
+ #### اولویت رویداد
2793
+
2794
+ ```python
2795
+ from spluspy.events import HandlerPriority
2796
+
2797
+ @bot.on_message(priority=HandlerPriority.FIRST)
2798
+ async def high_priority_handler(client, message):
2799
+ pass
2800
+
2801
+ @bot.on_message(priority=HandlerPriority.LOW)
2802
+ async def low_priority_handler(client, message):
2803
+ pass
2804
+ ```
2805
+
2806
+ #### توقف انتشار
2807
+
2808
+ ```python
2809
+ @bot.on_message()
2810
+ async def stopper(client, message):
2811
+ if message.text == "/stop":
2812
+ message.stop_propagation()
2813
+ await message.reply("توقف!")
2814
+ ```
2815
+
2816
+ ### فیلترها
2817
+
2818
+ ```python
2819
+ from spluspy import filters
2820
+
2821
+ @bot.on_message(filters.text) # فقط متن
2822
+ @bot.on_message(filters.private) # چت‌های خصوصی
2823
+ @bot.on_message(filters.group) # گروه‌ها
2824
+ @bot.on_message(filters.command("start")) # دستور /start
2825
+ @bot.on_message(filters.regex(r"\d+")) # تطابق با عبارت باقاعده
2826
+ @bot.on_message(filters.user(123)) # کاربر خاص
2827
+ @bot.on_message(filters.text & filters.private) # ترکیبی
2828
+ @bot.on_message(filters.photo | filters.video) # عکس یا ویدیو
2829
+ ```
2830
+
2831
+ ### متدهای پیام
2832
+
2833
+ ```python
2834
+ await message.reply("سلام") # پاسخ
2835
+ await message.edit("متن جدید") # ویرایش
2836
+ await message.delete() # حذف
2837
+ await message.forward(chat_id) # فوروارد
2838
+ await message.copy(chat_id) # کپی (بدون هدر فوروارد)
2839
+ await message.pin() # سنجاق
2840
+ await message.react("❤️") # واکنش
2841
+ await message.mark_read() # علامت خواندن
2842
+ await message.download() # دانلود رسانه
2843
+ await message.reply_photo("photo.jpg") # پاسخ با عکس
2844
+ await message.reply_video("video.mp4") # پاسخ با ویدیو
2845
+ await message.reply_document("file.pdf") # پاسخ با سند
2846
+ ```
2847
+
2848
+ ### دکمه‌ها
2849
+
2850
+ ```python
2851
+ from spluspy import Button
2852
+
2853
+ # کیبورد اینلاین
2854
+ keyboard = Button.build_inline([
2855
+ Button.inline("گزینه ۱", b"opt1"),
2856
+ Button.inline("گزینه ۲", b"opt2")
2857
+ ])
2858
+ await bot.send_message(chat_id, "انتخاب کنید:", buttons=keyboard)
2859
+
2860
+ # کیبورد ریپلای
2861
+ kb = Button.build_reply([
2862
+ Button.text("منو"),
2863
+ Button.text("تنظیمات")
2864
+ ])
2865
+ await bot.send_message(chat_id, "انتخاب کنید:", buttons=kb)
2866
+
2867
+ # حذف کیبورد
2868
+ await bot.send_message(chat_id, "تمام", buttons=Button.clear())
2869
+ ```
2870
+
2871
+ ### FSM (ماشین حالت محدود)
2872
+
2873
+ ```python
2874
+ from spluspy.fsm import State, StateMachine
2875
+ from spluspy.storage import MemoryStorage
2876
+
2877
+ storage = MemoryStorage()
2878
+ fsm = StateMachine(storage)
2879
+
2880
+ class Form:
2881
+ name = State()
2882
+ age = State()
2883
+
2884
+ @bot.on_message(filters.command("register"))
2885
+ async def start_register(client, message):
2886
+ ctx = fsm.context(message.sender_id)
2887
+ await ctx.set_state(Form.name)
2888
+ await message.reply("نام شما چیست؟")
2889
+
2890
+ @bot.on_message(filters.private)
2891
+ async def process_form(client, message):
2892
+ ctx = fsm.context(message.sender_id)
2893
+ state = await ctx.get_state()
2894
+
2895
+ if state == Form.name:
2896
+ await ctx.set_data(name=message.text)
2897
+ await ctx.set_state(Form.age)
2898
+ await message.reply("سن شما چقدر است؟")
2899
+ elif state == Form.age:
2900
+ data = await ctx.get_data()
2901
+ await ctx.reset()
2902
+ await message.reply(f"ثبت شد! نام: {data.get('name')}, سن: {message.text}")
2903
+ ```
2904
+
2905
+ ### بک‌اندهای ذخیره‌سازی
2906
+
2907
+ ```python
2908
+ from spluspy.storage import MemoryStorage, SQLiteStorage, RedisStorage, PostgresStorage, get_storage
2909
+
2910
+ # حافظه (پیش‌فرض)
2911
+ storage = MemoryStorage()
2912
+
2913
+ # SQLite
2914
+ storage = SQLiteStorage("data.db")
2915
+
2916
+ # Redis
2917
+ storage = RedisStorage(host="localhost", port=6379, db=0)
2918
+
2919
+ # PostgreSQL
2920
+ storage = PostgresStorage(dsn="postgresql://user:pass@localhost/db")
2921
+
2922
+ # تابع کارخانه
2923
+ storage = get_storage("redis", host="localhost")
2924
+ ```
2925
+
2926
+ ### سیستم پلاگین
2927
+
2928
+ ```python
2929
+ # plugins/hello.py
2930
+ def register(client):
2931
+ @client.on_message(filters.command("hello"))
2932
+ async def hello_handler(client, message):
2933
+ await message.reply("سلام از پلاگین!")
2934
+ ```
2935
+
2936
+ ```python
2937
+ # main.py
2938
+ from spluspy import Client
2939
+
2940
+ bot = Client("session")
2941
+ bot.plugins.load("plugins")
2942
+ bot.run()
2943
+ ```
2944
+
2945
+ ### میان‌افزار (Middleware)
2946
+
2947
+ ```python
2948
+ from spluspy.middleware import Middleware
2949
+
2950
+ class LoggingMiddleware(Middleware):
2951
+ async def on_update(self, update, handler):
2952
+ print(f"به‌روزرسانی دریافت شد: {update}")
2953
+ result = await handler(update)
2954
+ print(f"هندلر تکمیل شد")
2955
+ return result
2956
+
2957
+ bot.middleware.add(LoggingMiddleware())
2958
+ ```
2959
+
2960
+ ### محدودیت نرخ
2961
+
2962
+ ```python
2963
+ from spluspy.utils import RateLimiter
2964
+
2965
+ limiter = RateLimiter(max_calls=10, period=60)
2966
+
2967
+ @bot.on_message()
2968
+ async def limited_handler(client, message):
2969
+ if not limiter.allow():
2970
+ await message.reply("محدودیت نرخ! دوباره تلاش کنید.")
2971
+ return
2972
+ await message.reply("باشه")
2973
+ ```
2974
+
2975
+ ### پاسخ خودکار AFK
2976
+
2977
+ ```python
2978
+ from spluspy import Client, filters
2979
+ from spluspy.afk import AfkManager
2980
+
2981
+ bot = Client("my_account")
2982
+ afk = AfkManager(bot, message="الان AFK هستم. زود برمی‌گردم!")
2983
+
2984
+ @bot.on_message(filters.command("afk"))
2985
+ async def set_afk(client, message):
2986
+ afk.set_afk(True, reason="ناهار")
2987
+ await message.reply("الان AFK هستم!")
2988
+
2989
+ @bot.on_message(filters.command("back"))
2990
+ async def unset_afk(client, message):
2991
+ afk.set_afk(False)
2992
+ await message.reply(f"برگشتم! {afk.total_replies} پاسخ خودکار ارسال شد.")
2993
+ ```
2994
+
2995
+ ### مدیریت چت
2996
+
2997
+ ```python
2998
+ from spluspy.admin import ChatAdmin
2999
+
3000
+ admin = ChatAdmin(bot)
3001
+
3002
+ await admin.ban_user(chat_id, user_id)
3003
+ await admin.unban_user(chat_id, user_id)
3004
+ await admin.mute_user(chat_id, user_id)
3005
+ await admin.unmute_user(chat_id, user_id)
3006
+ await admin.pin_message(chat_id, message)
3007
+ await admin.unpin_message(chat_id, message)
3008
+ await admin.unpin_all(chat_id)
3009
+ await admin.purge_messages(chat_id, limit=100)
3010
+
3011
+ # عملیات دسته‌ای
3012
+ await admin.bulk_action(chat_id, "ban", [user_id1, user_id2])
3013
+
3014
+ # لاگ مدیریتی
3015
+ events = await admin.get_admin_log(chat_id, limit=50)
3016
+ ```
3017
+
3018
+ ### آینه‌سازی پیام
3019
+
3020
+ ```python
3021
+ from spluspy.mirror import MessageMirror
3022
+
3023
+ mirror = MessageMirror(bot)
3024
+
3025
+ mirror.add_route(
3026
+ source=-1001234567890,
3027
+ targets=[-1009876543210, -1001112223334],
3028
+ strip_forward=True,
3029
+ strip_sender=False,
3030
+ add_prefix="[Mirror]"
3031
+ )
3032
+
3033
+ await mirror.start()
3034
+ ```
3035
+
3036
+ ### زمان‌بند
3037
+
3038
+ ```python
3039
+ from spluspy.scheduler.scheduler import MessageScheduler
3040
+
3041
+ scheduler = MessageScheduler(bot)
3042
+
3043
+ # ارسال پیام هر ساعت
3044
+ scheduler.schedule_interval("hourly_greeting", chat_id, "سلام!", interval=3600)
3045
+
3046
+ # ارسال پیام با تاخیر
3047
+ scheduler.schedule_once("reminder", chat_id, "فراموش نکن!", delay=300)
3048
+
3049
+ # لغو وظیفه زمان‌بندی شده
3050
+ scheduler.cancel("hourly_greeting")
3051
+ ```
3052
+
3053
+ ### مدیریت خطا
3054
+
3055
+ ```python
3056
+ from spluspy import filters
3057
+ from spluspy.errors import FloodWait, Unauthorized, BadRequest
3058
+
3059
+ @bot.on_message()
3060
+ async def safe_handler(client, message):
3061
+ try:
3062
+ await message.reply("سلام!")
3063
+ except FloodWait as e:
3064
+ await asyncio.sleep(e.seconds)
3065
+ except Unauthorized:
3066
+ await message.reply("غیرمجاز!")
3067
+ except BadRequest as e:
3068
+ print(f"درخواست نادرست: {e}")
3069
+ ```
3070
+
3071
+ ### عملیات دسته‌ای
3072
+
3073
+ ```python
3074
+ # ارسال چندین پیام
3075
+ messages = ["سلام ۱", "سلام ۲", "سلام ۳"]
3076
+ results = await bot.batch_send(chat_id, messages)
3077
+
3078
+ # حذف چندین پیام
3079
+ await bot.batch_delete(chat_id, [msg1, msg2, msg3])
3080
+
3081
+ # فوروارد چندین پیام
3082
+ await bot.batch_forward(chat_id, [msg1, msg2])
3083
+ ```
3084
+
3085
+ ### انتقال فایل با پیشرفت
3086
+
3087
+ ```python
3088
+ from spluspy.utils import ProgressTracker
3089
+
3090
+ # آپلود با پیشرفت
3091
+ tracker = ProgressTracker(on_progress=lambda p: print(f"{p.percent}%"))
3092
+ await bot.send_document(chat_id, "فایل_بزرگ.zip", progress=tracker)
3093
+
3094
+ # دانلود با پیشرفت
3095
+ await message.download(progress=tracker)
3096
+ ```
3097
+
3098
+ ### رابط خط فرمان (CLI)
3099
+
3100
+ ```bash
3101
+ # اجرای ربات
3102
+ spluspy run bot.py
3103
+
3104
+ # اجرا با نشست سفارشی
3105
+ spluspy run bot.py --session my_bot
3106
+
3107
+ # اطلاعات نشست
3108
+ spluspy session-info my_session.session
3109
+
3110
+ # نمایش نسخه
3111
+ spluspy version
3112
+
3113
+ # اعتبارسنجی اسکریپت ربات
3114
+ spluspy validate bot.py
3115
+ ```
3116
+
3117
+ ### Docker
3118
+
3119
+ ```dockerfile
3120
+ FROM python:3.12-slim
3121
+
3122
+ WORKDIR /app
3123
+
3124
+ RUN apt-get update && apt-get install -y --no-install-recommends \
3125
+ gcc \
3126
+ && rm -rf /var/lib/apt/lists/*
3127
+
3128
+ RUN pip install --no-cache-dir spluspy[all]
3129
+
3130
+ COPY bot.py .
3131
+
3132
+ CMD ["python", "bot.py"]
3133
+ ```
3134
+
3135
+ ساخت و اجرا:
3136
+
3137
+ ```bash
3138
+ docker build -t my-spluspy-bot .
3139
+ docker run -v ./sessions:/app/sessions my-spluspy-bot
3140
+ ```
3141
+
3142
+ ### ساختار پروژه
3143
+
3144
+ ```
3145
+ spluspy/
3146
+ ├── __init__.py # API عمومی
3147
+ ├── __version__.py # اطلاعات نسخه
3148
+ ├── cli.py # نقطه ورود CLI
3149
+ ├── config.py # پیکربندی
3150
+ ├── compat.py # لایه سازگاری
3151
+ ├── afk.py # پاسخگوی خودکار AFK
3152
+ ├── admin.py # مدیریت چت
3153
+ ├── mirror.py # موتور آینه‌سازی پیام
3154
+ ├── client/ # کلاینت و API مکالمه
3155
+ │ ├── client.py # کلاس اصلی Client
3156
+ │ ├── conversation.py # API مکالمه
3157
+ │ ├── chat_mixin.py # میکسین مدیریت چت
3158
+ │ └── media_mixin.py # میکسین عملیات رسانه
3159
+ ├── models/ # مدل‌های دامنه
3160
+ │ ├── message.py # داده‌کلاس Message
3161
+ │ ├── user.py # داده‌کلاس User
3162
+ │ ├── chat.py # داده‌کلاس Chat, Channel, Group
3163
+ │ ├── bot.py # کارخانه Button
3164
+ │ ├── media.py # انواع رسانه
3165
+ │ ├── objects.py # MessageEntity, ReplyMarkup, و غیره
3166
+ │ └── enums.py # ChatType, UserStatus, و غیره
3167
+ ├── events/ # انواع رویداد و بیلدرها
3168
+ ├── filters/ # فیلترهای قابل ترکیب پیام
3169
+ ├── errors/ # سلسله مراتب استثنای سفارشی
3170
+ ├── session/ # بک‌اندهای نشست
3171
+ ├── network/ # اتصالات TCP و استخر اتصال
3172
+ ├── storage/ # بک‌اندهای ذخیره‌سازی کلید-مقدار
3173
+ ├── plugins/ # بارگذار پلاگین
3174
+ ├── middleware/ # سیستم میان‌افزار
3175
+ ├── fsm/ # ماشین حالت محدود
3176
+ ├── scheduler/ # زمان‌بند وظایف
3177
+ ├── utils/ # لاگر، کش، کمک‌کننده‌ها
3178
+ ├── sync/ # کلاینت همگام‌سازی شده
3179
+ └── _engine/ # موتور MTProto سطح پایین
3180
+ ```
3181
+
3182
+ ### سلسله مراتب خطاها
3183
+
3184
+ ```
3185
+ SplusPyError
3186
+ ├── SoroushPlusAPIError
3187
+ │ ├── RPCError
3188
+ │ │ ├── FloodWait
3189
+ │ │ ├── Unauthorized
3190
+ │ │ ├── Forbidden
3191
+ │ │ ├── BadRequest
3192
+ │ │ │ ├── ChatNotFound
3193
+ │ │ │ ├── UserNotFound
3194
+ │ │ │ └── MessageNotFound
3195
+ │ │ └── ...
3196
+ │ ├── SessionExpiredError
3197
+ │ └── SessionError
3198
+ ├── AuthError
3199
+ ├── ValidationError
3200
+ ├── TimeoutError
3201
+ ├── ConnectionError
3202
+ ├── PluginError
3203
+ ├── FSMError
3204
+ ├── StorageError
3205
+ ├── JoinChatError
3206
+ │ ├── InvalidInviteLinkError
3207
+ │ ├── InviteLinkExpiredError
3208
+ │ ├── ChatFullError
3209
+ │ ├── ChatDeactivatedError
3210
+ │ └── MembershipRequiredError
3211
+ └── FloodWaitError (محدودیت نرخ)
3212
+ ```
3213
+
3214
+ ---
3215
+
3216
+ ## Contributing / مشارکت
3217
+
3218
+ ### English
3219
+
3220
+ 1. Fork the repository
3221
+ 2. Create a feature branch (`git checkout -b feature/your-feature`)
3222
+ 3. Set up the development environment:
3223
+ ```bash
3224
+ python -m venv venv
3225
+ source venv/bin/activate
3226
+ pip install -e ".[dev]"
3227
+ ```
3228
+ 4. Make your changes
3229
+ 5. Run linting and formatting:
3230
+ ```bash
3231
+ ruff check spluspy/
3232
+ black spluspy/
3233
+ ```
3234
+ 6. Run type checking:
3235
+ ```bash
3236
+ mypy spluspy/
3237
+ ```
3238
+ 7. Run tests: `pytest`
3239
+ 8. Submit a pull request
3240
+
3241
+ ### فارسی
3242
+
3243
+ 1. مخزن را Fork کنید
3244
+ 2. شاخه ویژگی بسازید (`git checkout -b feature/your-feature`)
3245
+ 3. محیط توسعه را راه‌اندازی کنید:
3246
+ ```bash
3247
+ python -m venv venv
3248
+ source venv/bin/activate
3249
+ pip install -e ".[dev]"
3250
+ ```
3251
+ 4. تغییرات خود را اعمال کنید
3252
+ 5. لینتر و فرمت‌کننده را اجرا کنید:
3253
+ ```bash
3254
+ ruff check spluspy/
3255
+ black spluspy/
3256
+ ```
3257
+ 6. بررسی نوع را اجرا کنید:
3258
+ ```bash
3259
+ mypy spluspy/
3260
+ ```
3261
+ 7. تست‌ها را اجرا کنید: `pytest`
3262
+ 8. درخواست Pull ارسال کنید
3263
+
3264
+ ---
3265
+
3266
+ ## License / مجوز
3267
+
3268
+ MIT License — see [LICENSE](LICENSE) for details.
3269
+
3270
+ مجوز MIT — جزئیات را در [LICENSE](LICENSE) مشاهده کنید.
3271
+
3272
+ ---
3273
+
3274
+ ## Disclaimer / سلب مسئولیت
3275
+
3276
+ **English:**
3277
+ SPlusPy is an unofficial third-party library. Use it responsibly and ensure your applications comply with Soroush Plus's Terms of Service.
3278
+
3279
+ **فارسی:**
3280
+ SPlusPy یک کتابخانه غیررسمی و شخص ثالث است. مسئولانه از آن استفاده کنید و مطمئن شوید برنامه‌های شما با شرایط استفاده سروش پلاس مطابقت دارند.
3281
+
3282
+ ---
3283
+
3284
+ <div align="center">
3285
+
3286
+ **Made with ❤️ for the Soroush Plus community**
3287
+
3288
+ **ساخته شده با ❤️ برای جامعه سروش پلاس**
3289
+
3290
+ </div>