diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e1809bc7acb..c836e2e62ac 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -83,7 +83,7 @@ jobs: exit ${status} env: JOB_INDEX: ${{ strategy.job-index }} - BOTS: W3sidG9rZW4iOiAiNjk2MTg4NzMyOkFBR1Z3RUtmSEhsTmpzY3hFRE5LQXdraEdzdFpfa28xbUMwIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WldGaU1UUmxNbVF5TnpNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMi43IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMzkwOTgzOTk3IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzI3X2JvdCJ9LCB7InRva2VuIjogIjY3MTQ2ODg4NjpBQUdQR2ZjaVJJQlVORmU4MjR1SVZkcTdKZTNfWW5BVE5HdyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpHWXdPVGxrTXpNeE4yWTIiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTQ0NjAyMjUyMiIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zNF9ib3QifSwgeyJ0b2tlbiI6ICI2MjkzMjY1Mzg6QUFGUnJaSnJCN29CM211ekdzR0pYVXZHRTVDUXpNNUNVNG8iLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpNbU01WVdKaFl6a3hNMlUxIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgQ1B5dGhvbiAzLjUiLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDE0OTY5MTc3NTAiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX2NweXRob25fMzVfYm90In0sIHsidG9rZW4iOiAiNjQwMjA4OTQzOkFBRmhCalFwOXFtM1JUeFN6VXBZekJRakNsZS1Kano1aGNrIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WXpoa1pUZzFOamMxWXpWbCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMy42IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMzMzODcxNDYxIiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzM2X2JvdCJ9LCB7InRva2VuIjogIjY5NTEwNDA4ODpBQUhmenlsSU9qU0lJUy1lT25JMjB5MkUyMEhvZEhzZnotMCIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk9HUTFNRGd3WmpJd1pqRmwiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNyIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTQ3ODI5MzcxNCIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zN19ib3QifSwgeyJ0b2tlbiI6ICI2OTE0MjM1NTQ6QUFGOFdrakNaYm5IcVBfaTZHaFRZaXJGRWxackdhWU9oWDAiLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpZamM1TlRoaU1tUXlNV1ZoIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgUHlQeSAyLjciLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDEzNjM5MzI1NzMiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX3B5cHlfMjdfYm90In0sIHsidG9rZW4iOiAiNjg0MzM5OTg0OkFBRk1nRUVqcDAxcjVyQjAwN3lDZFZOc2c4QWxOc2FVLWNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TVRBek1UWTNNR1V5TmpnMCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIFB5UHkgMy41IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDA3ODM2NjA1IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19weXB5XzM1X2JvdCJ9LCB7InRva2VuIjogIjY5MDA5MTM0NzpBQUZMbVI1cEFCNVljcGVfbU9oN3pNNEpGQk9oMHozVDBUbyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpEaGxOekU1TURrd1lXSmkiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIEFwcFZleW9yIHVzaW5nIENQeXRob24gMy40IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMjc5NjAwMDI2IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX2FwcHZleW9yX2NweXRob25fMzRfYm90In0sIHsidG9rZW4iOiAiNjk0MzA4MDUyOkFBRUIyX3NvbkNrNTVMWTlCRzlBTy1IOGp4aVBTNTVvb0JBIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WW1aaVlXWm1NakpoWkdNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gQXBwVmV5b3IgdXNpbmcgQ1B5dGhvbiAyLjciLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDEyOTMwNzkxNjUiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfYXBwdmV5b3JfY3B5dGhvbl8yN19ib3QifSwgeyJ0b2tlbiI6ICIxMDU1Mzk3NDcxOkFBRzE4bkJfUzJXQXd1SjNnN29oS0JWZ1hYY2VNbklPeVNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpBd056QXpZalZpTkdOayIsICJuYW1lIjogIlBUQiB0ZXN0cyBbMF0iLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDExODU1MDk2MzYiLCAidXNlcm5hbWUiOiAicHRiXzBfYm90In0sIHsidG9rZW4iOiAiMTA0NzMyNjc3MTpBQUY4bk90ODFGcFg4bGJidno4VWV3UVF2UmZUYkZmQnZ1SSIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOllUVTFOVEk0WkdSallqbGkiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzFdIiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDg0Nzk3NjEyIiwgInVzZXJuYW1lIjogInB0Yl8xX2JvdCJ9LCB7InRva2VuIjogIjk3MTk5Mjc0NTpBQUdPa09hVzBOSGpnSXY1LTlqUWJPajR2R3FkaFNGLVV1cyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk5XWmtNV1ZoWWpsallqVTUiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzJdIiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDAyMjU1MDcwIiwgInVzZXJuYW1lIjogInB0Yl8yX2JvdCJ9LCB7InRva2VuIjogIjU1MTg2NDU0MTE6QUFHdzBxaEs3ZTRHbmoxWjJjc1BBQzdaYWtvTWs1NkVKZmsiLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpNRE0wT1RCbE9UUXpNVEU1IiwgIm5hbWUiOiAiUFRCIFRlc3QgQm90IFszXSIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTgwMzgxMDE5NiIsICJ1c2VybmFtZSI6ICJwdGJfdGVzdF8wM19ib3QifSwgeyJ0b2tlbiI6ICI1NzM3MDE4MzU2OkFBSDEzOFN1aUtRRjBMRENXc2ZnV2VYZmpKNWQ2M2tDV0xBIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TjJWaVpqUmxaak01TlRNdyIsICJuYW1lIjogIlBUQiBUZXN0IEJvdCBbNF0iLCAidXNlcm5hbWUiOiAicHRiX3Rlc3RfMDRfYm90IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxODQyNDM5NjQxIn0sIHsidG9rZW4iOiAiNTc0NDY0NDUyMjpBQUVBZHNyRjBoQzZwNkhVTzBQMDFROGJfakNoVTUyWEctTSIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpqSmtZVGd5TmpnMlpHRTAiLCAibmFtZSI6ICJQVEIgVGVzdCBCb3QgWzVdIiwgInVzZXJuYW1lIjogInB0Yl90ZXN0XzA1X2JvdCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTg1NTM2MDk4NiJ9XQ== + BOTS: W3sidG9rZW4iOiAiNjk2MTg4NzMyOkFBR1Z3RUtmSEhsTmpzY3hFRE5LQXdraEdzdFpfa28xbUMwIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WldGaU1UUmxNbVF5TnpNeSIsICJuYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgQ1B5dGhvbiAyLjciLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDEzOTA5ODM5OTciLCAidXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8yN19ib3QiLCAiZm9ydW1fZ3JvdXBfaWQiOiAiLTEwMDE3MTA4NTA4MjIifSwgeyJ0b2tlbiI6ICI2NzE0Njg4ODY6QUFHUEdmY2lSSUJVTkZlODI0dUlWZHE3SmUzX1luQVROR3ciLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpaR1l3T1Rsa016TXhOMlkyIiwgIm5hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTQ0NjAyMjUyMiIsICJ1c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzM0X2JvdCIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTg5MTQ0MTc5MSJ9LCB7InRva2VuIjogIjYyOTMyNjUzODpBQUZSclpKckI3b0IzbXV6R3NHSlhVdkdFNUNRek01Q1U0byIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk1tTTVZV0poWXpreE0yVTEiLCAibmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMy41IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDk2OTE3NzUwIiwgInVzZXJuYW1lIjogIkBwdGJfdHJhdmlzX2NweXRob25fMzVfYm90IiwgImZvcnVtX2dyb3VwX2lkIjogIi0xMDAxNTc3NTA0Nzg3In0sIHsidG9rZW4iOiAiNjQwMjA4OTQzOkFBRmhCalFwOXFtM1JUeFN6VXBZekJRakNsZS1Kano1aGNrIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WXpoa1pUZzFOamMxWXpWbCIsICJuYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgQ1B5dGhvbiAzLjYiLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDEzMzM4NzE0NjEiLCAidXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zNl9ib3QiLCAiZm9ydW1fZ3JvdXBfaWQiOiAiLTEwMDE4Njc5MDExNzIifSwgeyJ0b2tlbiI6ICI2OTUxMDQwODg6QUFIZnp5bElPalNJSVMtZU9uSTIweTJFMjBIb2RIc2Z6LTAiLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpPR1ExTURnd1pqSXdaakZsIiwgIm5hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNyIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTQ3ODI5MzcxNCIsICJ1c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzM3X2JvdCIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTg2NDA1NDg3OSJ9LCB7InRva2VuIjogIjY5MTQyMzU1NDpBQUY4V2tqQ1pibkhxUF9pNkdoVFlpckZFbFpyR2FZT2hYMCIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOllqYzVOVGhpTW1ReU1XVmgiLCAibmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIFB5UHkgMi43IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMzYzOTMyNTczIiwgInVzZXJuYW1lIjogIkBwdGJfdHJhdmlzX3B5cHlfMjdfYm90IiwgImZvcnVtX2dyb3VwX2lkIjogIi0xMDAxODY3ODU1OTM2In0sIHsidG9rZW4iOiAiNjg0MzM5OTg0OkFBRk1nRUVqcDAxcjVyQjAwN3lDZFZOc2c4QWxOc2FVLWNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TVRBek1UWTNNR1V5TmpnMCIsICJuYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgUHlQeSAzLjUiLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDE0MDc4MzY2MDUiLCAidXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfcHlweV8zNV9ib3QiLCAiZm9ydW1fZ3JvdXBfaWQiOiAiLTEwMDE1NTg5OTAyODIifSwgeyJ0b2tlbiI6ICI2OTAwOTEzNDc6QUFGTG1SNXBBQjVZY3BlX21PaDd6TTRKRkJPaDB6M1QwVG8iLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpaRGhsTnpFNU1Ea3dZV0ppIiwgIm5hbWUiOiAiUFRCIHRlc3RzIG9uIEFwcFZleW9yIHVzaW5nIENQeXRob24gMy40IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMjc5NjAwMDI2IiwgInVzZXJuYW1lIjogIkBwdGJfYXBwdmV5b3JfY3B5dGhvbl8zNF9ib3QiLCAiZm9ydW1fZ3JvdXBfaWQiOiAiLTEwMDE3MjU2OTEzODcifSwgeyJ0b2tlbiI6ICI2OTQzMDgwNTI6QUFFQjJfc29uQ2s1NUxZOUJHOUFPLUg4anhpUFM1NW9vQkEiLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpZbVppWVdabU1qSmhaR015IiwgIm5hbWUiOiAiUFRCIHRlc3RzIG9uIEFwcFZleW9yIHVzaW5nIENQeXRob24gMi43IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMjkzMDc5MTY1IiwgInVzZXJuYW1lIjogIkBwdGJfYXBwdmV5b3JfY3B5dGhvbl8yN19ib3QiLCAiZm9ydW1fZ3JvdXBfaWQiOiAiLTEwMDE1NjU4NTU5ODcifSwgeyJ0b2tlbiI6ICIxMDU1Mzk3NDcxOkFBRzE4bkJfUzJXQXd1SjNnN29oS0JWZ1hYY2VNbklPeVNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpBd056QXpZalZpTkdOayIsICJuYW1lIjogIlBUQiB0ZXN0cyBbMF0iLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDExODU1MDk2MzYiLCAidXNlcm5hbWUiOiAicHRiXzBfYm90IiwgImZvcnVtX2dyb3VwX2lkIjogIi0xMDAxODE5MDM3MzExIn0sIHsidG9rZW4iOiAiMTA0NzMyNjc3MTpBQUY4bk90ODFGcFg4bGJidno4VWV3UVF2UmZUYkZmQnZ1SSIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOllUVTFOVEk0WkdSallqbGkiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzFdIiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDg0Nzk3NjEyIiwgInVzZXJuYW1lIjogInB0Yl8xX2JvdCIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTc5NzMwODQ0NCJ9LCB7InRva2VuIjogIjk3MTk5Mjc0NTpBQUdPa09hVzBOSGpnSXY1LTlqUWJPajR2R3FkaFNGLVV1cyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk5XWmtNV1ZoWWpsallqVTUiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzJdIiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDAyMjU1MDcwIiwgInVzZXJuYW1lIjogInB0Yl8yX2JvdCIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTUyMzU3NTA3MiJ9LCB7InRva2VuIjogIjU1MTg2NDU0MTE6QUFHdzBxaEs3ZTRHbmoxWjJjc1BBQzdaYWtvTWs1NkVKZmsiLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpNRE0wT1RCbE9UUXpNVEU1IiwgIm5hbWUiOiAiUFRCIFRlc3QgQm90IFszXSIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTgwMzgxMDE5NiIsICJ1c2VybmFtZSI6ICJwdGJfdGVzdF8wM19ib3QiLCAiZm9ydW1fZ3JvdXBfaWQiOiAiLTEwMDE2MTk2NzMyNjEifSwgeyJ0b2tlbiI6ICI1NzM3MDE4MzU2OkFBSDEzOFN1aUtRRjBMRENXc2ZnV2VYZmpKNWQ2M2tDV0xBIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TjJWaVpqUmxaak01TlRNdyIsICJuYW1lIjogIlBUQiBUZXN0IEJvdCBbNF0iLCAidXNlcm5hbWUiOiAicHRiX3Rlc3RfMDRfYm90IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxODQyNDM5NjQxIiwgImZvcnVtX2dyb3VwX2lkIjogIi0xMDAxODQyOTk2MTk5In0sIHsidG9rZW4iOiAiNTc0NDY0NDUyMjpBQUVBZHNyRjBoQzZwNkhVTzBQMDFROGJfakNoVTUyWEctTSIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpqSmtZVGd5TmpnMlpHRTAiLCAibmFtZSI6ICJQVEIgVGVzdCBCb3QgWzVdIiwgInVzZXJuYW1lIjogInB0Yl90ZXN0XzA1X2JvdCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTg1NTM2MDk4NiIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTY0NDM2NjkwMiJ9XQ== TEST_WITH_OPT_DEPS : "false" TEST_BUILD: "true" shell: bash --noprofile --norc {0} diff --git a/README.rst b/README.rst index e461f198733..b5c8c2a4a88 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-6.3-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-6.4-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions @@ -93,7 +93,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju Telegram API support ==================== -All types and methods of the Telegram Bot API **6.2** are supported. +All types and methods of the Telegram Bot API **6.4** are supported. Installing ========== diff --git a/README_RAW.rst b/README_RAW.rst index 5db4d5c8794..4b529fd1153 100644 --- a/README_RAW.rst +++ b/README_RAW.rst @@ -14,7 +14,7 @@ :target: https://pypi.org/project/python-telegram-bot-raw/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-6.3-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-6.4-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions @@ -89,7 +89,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju Telegram API support ==================== -All types and methods of the Telegram Bot API **6.2** are supported. +All types and methods of the Telegram Bot API **6.4** are supported. Installing ========== diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index aff45cc164f..742cde020ed 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -267,16 +267,26 @@ * - :meth:`~telegram.Bot.close_forum_topic` - Used for closing a forum topic + * - :meth:`~telegram.Bot.close_general_forum_topic` + - Used for closing the general forum topic * - :meth:`~telegram.Bot.create_forum_topic` - Used to create a topic * - :meth:`~telegram.Bot.delete_forum_topic` - Used for deleting a forum topic * - :meth:`~telegram.Bot.edit_forum_topic` - Used to edit a topic - * - :meth:`~telegram.Bot.reopen_forum_topic` - - Used to reopen a topic + * - :meth:`~telegram.Bot.edit_general_forum_topic` + - Used to edit the general topic * - :meth:`~telegram.Bot.get_forum_topic_icon_stickers` - Used to get custom emojis to use as topic icons + * - :meth:`~telegram.Bot.hide_general_forum_topic` + - Used to hide the general topic + * - :meth:`~telegram.Bot.unhide_general_forum_topic` + - Used to unhide the general topic + * - :meth:`~telegram.Bot.reopen_forum_topic` + - Used to reopen a topic + * - :meth:`~telegram.Bot.reopen_general_forum_topic` + - Used to reopen the general topic * - :meth:`~telegram.Bot.unpin_all_forum_topic_messages` - Used to unpin all messages in a forum topic diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index 122df9befcc..dd9f1c44266 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -39,7 +39,10 @@ Available Types telegram.forumtopic telegram.forumtopicclosed telegram.forumtopiccreated + telegram.forumtopicedited telegram.forumtopicreopened + telegram.generalforumtopichidden + telegram.generalforumtopicunhidden telegram.inlinekeyboardbutton telegram.inlinekeyboardmarkup telegram.inputfile @@ -84,4 +87,5 @@ Available Types telegram.webappdata telegram.webappinfo telegram.webhookinfo + telegram.writeaccessallowed diff --git a/docs/source/telegram.forumtopicedited.rst b/docs/source/telegram.forumtopicedited.rst new file mode 100644 index 00000000000..77dfb349170 --- /dev/null +++ b/docs/source/telegram.forumtopicedited.rst @@ -0,0 +1,6 @@ +telegram.ForumTopicEdited +========================= + +.. autoclass:: telegram.ForumTopicEdited + :members: + :show-inheritance: diff --git a/docs/source/telegram.generalforumtopichidden.rst b/docs/source/telegram.generalforumtopichidden.rst new file mode 100644 index 00000000000..d3843ab6ce5 --- /dev/null +++ b/docs/source/telegram.generalforumtopichidden.rst @@ -0,0 +1,6 @@ +telegram.GeneralForumTopicHidden +================================ + +.. autoclass:: telegram.GeneralForumTopicHidden + :members: + :show-inheritance: diff --git a/docs/source/telegram.generalforumtopicunhidden.rst b/docs/source/telegram.generalforumtopicunhidden.rst new file mode 100644 index 00000000000..924ddc74262 --- /dev/null +++ b/docs/source/telegram.generalforumtopicunhidden.rst @@ -0,0 +1,6 @@ +telegram.GeneralForumTopicUnhidden +================================== + +.. autoclass:: telegram.GeneralForumTopicUnhidden + :members: + :show-inheritance: diff --git a/docs/source/telegram.writeaccessallowed.rst b/docs/source/telegram.writeaccessallowed.rst new file mode 100644 index 00000000000..9487a24b2d6 --- /dev/null +++ b/docs/source/telegram.writeaccessallowed.rst @@ -0,0 +1,6 @@ +telegram.WriteAccessAllowed +=========================== + +.. autoclass:: telegram.WriteAccessAllowed + :members: + :show-inheritance: diff --git a/docs/substitutions/global.rst b/docs/substitutions/global.rst index ad590831b4e..0add0a9392b 100644 --- a/docs/substitutions/global.rst +++ b/docs/substitutions/global.rst @@ -50,4 +50,4 @@ .. |sequenceargs| replace:: Accepts any :class:`collections.abc.Sequence` as input instead of just a list. -.. |captionentitiesattr| replace:: Tuple of special entities that appear in the caption, which can be specified instead of ``parse_mode``. \ No newline at end of file +.. |captionentitiesattr| replace:: Tuple of special entities that appear in the caption, which can be specified instead of ``parse_mode``. diff --git a/telegram/__init__.py b/telegram/__init__.py index c0230f076c8..60eda7ab225 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -70,9 +70,12 @@ "ForumTopic", "ForumTopicClosed", "ForumTopicCreated", + "ForumTopicEdited", "ForumTopicReopened", "Game", "GameHighScore", + "GeneralForumTopicHidden", + "GeneralForumTopicUnhidden", "helpers", "IdDocumentData", "InlineKeyboardButton", @@ -176,6 +179,7 @@ "WebAppData", "WebAppInfo", "WebhookInfo", + "WriteAccessAllowed", ) @@ -234,7 +238,15 @@ from ._files.videonote import VideoNote from ._files.voice import Voice from ._forcereply import ForceReply -from ._forumtopic import ForumTopic, ForumTopicClosed, ForumTopicCreated, ForumTopicReopened +from ._forumtopic import ( + ForumTopic, + ForumTopicClosed, + ForumTopicCreated, + ForumTopicEdited, + ForumTopicReopened, + GeneralForumTopicHidden, + GeneralForumTopicUnhidden, +) from ._games.callbackgame import CallbackGame from ._games.game import Game from ._games.gamehighscore import GameHighScore @@ -326,6 +338,7 @@ from ._webappdata import WebAppData from ._webappinfo import WebAppInfo from ._webhookinfo import WebhookInfo +from ._writeaccessallowed import WriteAccessAllowed #: :obj:`str`: The version of the `python-telegram-bot` library as string. #: To get detailed information about the version number, please use :data:`__version_info__` diff --git a/telegram/_bot.py b/telegram/_bot.py index bac228a26ac..f4991a2f5f6 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -919,6 +919,7 @@ async def send_photo( caption_entities: Sequence["MessageEntity"] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: int = None, + has_spoiler: bool = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -973,6 +974,10 @@ async def send_photo( :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + has_spoiler (:obj:`bool`, optional): Pass :obj:`True` if the photo needs to be covered + with a spoiler animation. + + .. versionadded:: 20.0 Keyword Args: filename (:obj:`str`, optional): Custom file name for the photo, when uploading a @@ -991,6 +996,7 @@ async def send_photo( data: JSONDict = { "chat_id": chat_id, "photo": self._parse_file_input(photo, PhotoSize, filename=filename), + "has_spoiler": has_spoiler, } return await self._send_message( @@ -1363,6 +1369,7 @@ async def send_video( caption_entities: Sequence["MessageEntity"] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: int = None, + has_spoiler: bool = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1438,6 +1445,10 @@ async def send_video( .. versionchanged:: 20.0 File paths as input is also accepted for bots *not* running in :paramref:`~telegram.Bot.local_mode`. + has_spoiler (:obj:`bool`, optional): Pass :obj:`True` if the video needs to be covered + with a spoiler animation. + + .. versionadded:: 20.0 Keyword Args: filename (:obj:`str`, optional): Custom file name for the video, when uploading a @@ -1461,6 +1472,7 @@ async def send_video( "height": height, "supports_streaming": supports_streaming, "thumb": self._parse_file_input(thumb, attach=True) if thumb else None, + "has_spoiler": has_spoiler, } return await self._send_message( @@ -1617,6 +1629,7 @@ async def send_animation( caption_entities: Sequence["MessageEntity"] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: int = None, + has_spoiler: bool = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1687,6 +1700,10 @@ async def send_animation( :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + has_spoiler (:obj:`bool`, optional): Pass :obj:`True` if the animation needs to be + covered with a spoiler animation. + + .. versionadded:: 20.0 Keyword Args: filename (:obj:`str`, optional): Custom file name for the animation, when uploading a @@ -1709,6 +1726,7 @@ async def send_animation( "width": width, "height": height, "thumb": self._parse_file_input(thumb, attach=True) if thumb else None, + "has_spoiler": has_spoiler, } return await self._send_message( @@ -2553,6 +2571,7 @@ async def send_chat_action( self, chat_id: Union[str, int], action: str, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2574,6 +2593,9 @@ async def send_chat_action( action(:obj:`str`): Type of action to broadcast. Choose one, depending on what the user is about to receive. For convenience look at the constants in :class:`telegram.constants.ChatAction`. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 Returns: :obj:`bool`: On success, :obj:`True` is returned. @@ -2582,7 +2604,11 @@ async def send_chat_action( :class:`telegram.error.TelegramError` """ - data: JSONDict = {"chat_id": chat_id, "action": action} + data: JSONDict = { + "chat_id": chat_id, + "action": action, + "message_thread_id": message_thread_id, + } result = await self._post( "sendChatAction", data, @@ -3913,7 +3939,8 @@ async def get_chat_member( pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> ChatMember: - """Use this method to get information about a member of a chat. + """Use this method to get information about a member of a chat. The method is guaranteed + to work only if the bot is an administrator in the chat. .. seealso:: :meth:`telegram.Chat.get_member` @@ -6970,8 +6997,8 @@ async def edit_forum_topic( self, chat_id: Union[str, int], message_thread_id: int, - name: str, - icon_custom_emoji_id: str, + name: str = None, + icon_custom_emoji_id: str = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -6993,12 +7020,14 @@ async def edit_forum_topic( Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_group| message_thread_id (:obj:`int`): |message_thread_id| - name (:obj:`str`): New topic name, + name (:obj:`str`, optional): New topic name, :tg-const:`telegram.constants.ForumTopicLimit.MIN_NAME_LENGTH`- - :tg-const:`telegram.constants.ForumTopicLimit.MAX_NAME_LENGTH` characters. - icon_custom_emoji_id (:obj:`str`): New unique identifier of the custom emoji shown as - the topic icon. Use :meth:`~telegram.Bot.get_forum_topic_icon_stickers` to get all - allowed custom emoji identifiers. + :tg-const:`telegram.constants.ForumTopicLimit.MAX_NAME_LENGTH` characters. If + not specified or empty, the current name of the topic will be kept. + icon_custom_emoji_id (:obj:`str`, optional): New unique identifier of the custom emoji + shown as the topic icon. Use :meth:`~telegram.Bot.get_forum_topic_icon_stickers` + to get all allowed custom emoji identifiers.Pass an empty string to remove the + icon. If not specified, the current icon will be kept. Returns: :obj:`bool`: On success, :obj:`True` is returned. @@ -7214,6 +7243,222 @@ async def unpin_all_forum_topic_messages( api_kwargs=api_kwargs, ) + @_log + async def edit_general_forum_topic( + self, + chat_id: Union[str, int], + name: str, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to edit the name of the 'General' topic in a forum supergroup chat. The bot + must be an administrator in the chat for this to work and must have + :attr:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights. + + .. seealso:: :meth:`telegram.Chat.edit_general_forum_topic` + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + name (:obj:`str`): New topic name, + :tg-const:`telegram.constants.ForumTopicLimit.MIN_NAME_LENGTH`- + :tg-const:`telegram.constants.ForumTopicLimit.MAX_NAME_LENGTH` characters. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id, "name": name} + + return await self._post( + "editGeneralForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + @_log + async def close_general_forum_topic( + self, + chat_id: Union[str, int], + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to close an open 'General' topic in a forum supergroup chat. The bot must + be an administrator in the chat for this to work and must have + :attr:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights. + + .. seealso:: :meth:`telegram.Chat.close_general_forum_topic` + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id} + + return await self._post( + "closeGeneralForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + @_log + async def reopen_general_forum_topic( + self, + chat_id: Union[str, int], + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to reopen a closed 'General' topic in a forum supergroup chat. The bot must + be an administrator in the chat for this to work and must have + :attr:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights. + The topic will be automatically unhidden if it was hidden. + + .. seealso:: :meth:`telegram.Chat.reopen_general_forum_topic` + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id} + + return await self._post( + "reopenGeneralForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + @_log + async def hide_general_forum_topic( + self, + chat_id: Union[str, int], + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to hide the 'General' topic in a forum supergroup chat. The bot must + be an administrator in the chat for this to work and must have + :attr:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights. + The topic will be automatically closed if it was open. + + .. seealso:: :meth:`telegram.Chat.hide_general_forum_topic` + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id} + + return await self._post( + "hideGeneralForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + @_log + async def unhide_general_forum_topic( + self, + chat_id: Union[str, int], + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to unhide the 'General' topic in a forum supergroup chat. The bot must + be an administrator in the chat for this to work and must have + :attr:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights. + + .. seealso:: :meth:`telegram.Chat.unhide_general_forum_topic` + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id} + + return await self._post( + "unhideGeneralForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + def to_dict(self, recursive: bool = True) -> JSONDict: # skipcq: PYL-W0613 """See :meth:`telegram.TelegramObject.to_dict`.""" data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name} @@ -7422,3 +7667,13 @@ def __hash__(self) -> int: """Alias for :meth:`delete_forum_topic`""" unpinAllForumTopicMessages = unpin_all_forum_topic_messages """Alias for :meth:`unpin_all_forum_topic_messages`""" + editGeneralForumTopic = edit_general_forum_topic + """Alias for :meth:`edit_general_forum_topic`""" + closeGeneralForumTopic = close_general_forum_topic + """Alias for :meth:`close_general_forum_topic`""" + reopenGeneralForumTopic = reopen_general_forum_topic + """Alias for :meth:`reopen_general_forum_topic`""" + hideGeneralForumTopic = hide_general_forum_topic + """Alias for :meth:`hide_general_forum_topic`""" + unhideGeneralForumTopic = unhide_general_forum_topic + """Alias for :meth:`unhide_general_forum_topic`""" diff --git a/telegram/_chat.py b/telegram/_chat.py index 4206710244f..c9586979e6c 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -164,6 +164,16 @@ class Chat(TelegramObject): status of the other party in a private chat. Returned only in :meth:`telegram.Bot.get_chat`. + .. versionadded:: 20.0 + has_aggressive_anti_spam_enabled (:obj:`bool`, optional): :obj:`True`, if aggressive + anti-spam checks are enabled in the supergroup. The field is only available to chat + administrators. Returned only in :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 20.0 + has_hidden_members (:obj:`bool`, optional): :obj:`True`, if non-administrators can only + get the list of bots and administrators in the chat. Returned only in + :meth:`telegram.Bot.get_chat`. + .. versionadded:: 20.0 Attributes: @@ -247,6 +257,16 @@ class Chat(TelegramObject): status of the other party in a private chat. Returned only in :meth:`telegram.Bot.get_chat`. + .. versionadded:: 20.0 + has_aggressive_anti_spam_enabled (:obj:`bool`): Optional. :obj:`True`, if aggressive + anti-spam checks are enabled in the supergroup. The field is only available to chat + administrators. Returned only in :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 20.0 + has_hidden_members (:obj:`bool`): Optional. :obj:`True`, if non-administrators can only + get the list of bots and administrators in the chat. Returned only in + :meth:`telegram.Bot.get_chat`. + .. versionadded:: 20.0 .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups @@ -279,6 +299,8 @@ class Chat(TelegramObject): "is_forum", "active_usernames", "emoji_status_custom_emoji_id", + "has_hidden_members", + "has_aggressive_anti_spam_enabled", ) SENDER: ClassVar[str] = constants.ChatType.SENDER @@ -323,6 +345,8 @@ def __init__( is_forum: bool = None, active_usernames: Sequence[str] = None, emoji_status_custom_emoji_id: str = None, + has_aggressive_anti_spam_enabled: bool = None, + has_hidden_members: bool = None, *, api_kwargs: JSONDict = None, ): @@ -357,6 +381,8 @@ def __init__( self.is_forum = is_forum self.active_usernames = parse_sequence_arg(active_usernames) self.emoji_status_custom_emoji_id = emoji_status_custom_emoji_id + self.has_aggressive_anti_spam_enabled = has_aggressive_anti_spam_enabled + self.has_hidden_members = has_hidden_members self._id_attrs = (self.id,) @@ -1330,6 +1356,7 @@ async def send_media_group( async def send_chat_action( self, action: str, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1350,6 +1377,7 @@ async def send_chat_action( return await self.get_bot().send_chat_action( chat_id=self.id, action=action, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1372,6 +1400,7 @@ async def send_photo( caption_entities: Sequence["MessageEntity"] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: int = None, + has_spoiler: bool = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1408,6 +1437,7 @@ async def send_photo( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + has_spoiler=has_spoiler, ) async def send_contact( @@ -1818,6 +1848,7 @@ async def send_animation( caption_entities: Sequence["MessageEntity"] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: int = None, + has_spoiler: bool = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1858,6 +1889,7 @@ async def send_animation( filename=filename, protect_content=protect_content, message_thread_id=message_thread_id, + has_spoiler=has_spoiler, ) async def send_sticker( @@ -1977,6 +2009,7 @@ async def send_video( caption_entities: Sequence["MessageEntity"] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: int = None, + has_spoiler: bool = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2018,6 +2051,7 @@ async def send_video( filename=filename, protect_content=protect_content, message_thread_id=message_thread_id, + has_spoiler=has_spoiler, ) async def send_video_note( @@ -2663,8 +2697,8 @@ async def create_forum_topic( async def edit_forum_topic( self, message_thread_id: int, - name: str, - icon_custom_emoji_id: str, + name: str = None, + icon_custom_emoji_id: str = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2825,6 +2859,164 @@ async def unpin_all_forum_topic_messages( api_kwargs=api_kwargs, ) + async def edit_general_forum_topic( + self, + name: str, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.edit_general_forum_topic( + chat_id=update.effective_chat.id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_general_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().edit_general_forum_topic( + chat_id=self.id, + name=name, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def close_general_forum_topic( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.close_general_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.close_general_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().close_general_forum_topic( + chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def reopen_general_forum_topic( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.reopen_general_forum_topic( + chat_id=update.effective_chat.id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.reopen_general_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().reopen_general_forum_topic( + chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def hide_general_forum_topic( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.hide_general_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.hide_general_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().hide_general_forum_topic( + chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def unhide_general_forum_topic( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.unhide_general_forum_topic ( + chat_id=update.effective_chat.id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.unhide_general_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().unhide_general_forum_topic( + chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def get_menu_button( self, *, diff --git a/telegram/_files/inputmedia.py b/telegram/_files/inputmedia.py index 7fa4eb6763d..fd921853a20 100644 --- a/telegram/_files/inputmedia.py +++ b/telegram/_files/inputmedia.py @@ -150,6 +150,10 @@ class InputMediaAnimation(InputMedia): width (:obj:`int`, optional): Animation width. height (:obj:`int`, optional): Animation height. duration (:obj:`int`, optional): Animation duration in seconds. + has_spoiler (:obj:`bool`, optional): Pass :obj:`True`, if the animation needs to be covered + with a spoiler animation. + + .. versionadded:: 20.0 Attributes: type (:obj:`str`): :tg-const:`telegram.constants.InputMediaType.ANIMATION`. @@ -168,10 +172,13 @@ class InputMediaAnimation(InputMedia): width (:obj:`int`): Optional. Animation width. height (:obj:`int`): Optional. Animation height. duration (:obj:`int`): Optional. Animation duration in seconds. + has_spoiler (:obj:`bool`): Optional. :obj:`True`, if the animation is covered with a + spoiler animation. + .. versionadded:: 20.0 """ - __slots__ = ("duration", "height", "thumb", "width") + __slots__ = ("duration", "height", "thumb", "width", "has_spoiler") def __init__( self, @@ -184,6 +191,7 @@ def __init__( duration: int = None, caption_entities: Sequence[MessageEntity] = None, filename: str = None, + has_spoiler: bool = None, *, api_kwargs: JSONDict = None, ): @@ -210,6 +218,7 @@ def __init__( self.width = width self.height = height self.duration = duration + self.has_spoiler = has_spoiler class InputMediaPhoto(InputMedia): @@ -237,6 +246,10 @@ class InputMediaPhoto(InputMedia): .. versionchanged:: 20.0 |sequenceclassargs| + has_spoiler (:obj:`bool`, optional): Pass :obj:`True`, if the photo needs to be covered + with a spoiler animation. + + .. versionadded:: 20.0 Attributes: type (:obj:`str`): :tg-const:`telegram.constants.InputMediaType.PHOTO`. @@ -251,10 +264,13 @@ class InputMediaPhoto(InputMedia): * |tupleclassattrs| * |alwaystuple| + has_spoiler (:obj:`bool`): Optional. :obj:`True`, if the photo is covered with a + spoiler animation. + .. versionadded:: 20.0 """ - __slots__ = () + __slots__ = ("has_spoiler",) def __init__( self, @@ -263,6 +279,7 @@ def __init__( parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Sequence[MessageEntity] = None, filename: str = None, + has_spoiler: bool = None, *, api_kwargs: JSONDict = None, ): @@ -278,7 +295,8 @@ def __init__( api_kwargs=api_kwargs, ) - self._freeze() + with self._unfrozen(): + self.has_spoiler = has_spoiler class InputMediaVideo(InputMedia): @@ -325,6 +343,10 @@ class InputMediaVideo(InputMedia): .. versionchanged:: 13.2 Accept :obj:`bytes` as input. + has_spoiler (:obj:`bool`, optional): Pass :obj:`True`, if the video needs to be covered + with a spoiler animation. + + .. versionadded:: 20.0 Attributes: type (:obj:`str`): :tg-const:`telegram.constants.InputMediaType.VIDEO`. @@ -345,10 +367,13 @@ class InputMediaVideo(InputMedia): supports_streaming (:obj:`bool`): Optional. :obj:`True`, if the uploaded video is suitable for streaming. thumb (:class:`telegram.InputFile`): Optional. |thumbdocstringbase| + has_spoiler (:obj:`bool`): Optional. :obj:`True`, if the video is covered with a + spoiler animation. + .. versionadded:: 20.0 """ - __slots__ = ("duration", "height", "thumb", "supports_streaming", "width") + __slots__ = ("duration", "height", "thumb", "supports_streaming", "width", "has_spoiler") def __init__( self, @@ -362,6 +387,7 @@ def __init__( thumb: FileInput = None, caption_entities: Sequence[MessageEntity] = None, filename: str = None, + has_spoiler: bool = None, *, api_kwargs: JSONDict = None, ): @@ -390,6 +416,7 @@ def __init__( self.duration = duration self.thumb = self._parse_thumb_input(thumb) self.supports_streaming = supports_streaming + self.has_spoiler = has_spoiler class InputMediaAudio(InputMedia): diff --git a/telegram/_forumtopic.py b/telegram/_forumtopic.py index cc77febe15b..35bf6a864b8 100644 --- a/telegram/_forumtopic.py +++ b/telegram/_forumtopic.py @@ -142,3 +142,73 @@ def __init__(self, *, api_kwargs: JSONDict = None) -> None: super().__init__(api_kwargs=api_kwargs) self._freeze() + + +class ForumTopicEdited(TelegramObject): + """ + This object represents a service message about an edited forum topic. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`name` and :attr:`icon_custom_emoji_id` are equal. + + .. versionadded:: 20.0 + + Args: + name (:obj:`str`, optional): New name of the topic, if it was edited. + icon_custom_emoji_id (:obj:`str`, optional): New identifier of the custom emoji shown as + the topic icon, if it was edited; an empty string if the icon was removed. + + Attributes: + name (:obj:`str`): Optional. New name of the topic, if it was edited. + icon_custom_emoji_id (:obj:`str`): Optional. New identifier of the custom emoji shown as + the topic icon, if it was edited; an empty string if the icon was removed. + """ + + __slots__ = ("name", "icon_custom_emoji_id") + + def __init__( + self, + name: str = None, + icon_custom_emoji_id: str = None, + *, + api_kwargs: JSONDict = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.name = name + self.icon_custom_emoji_id = icon_custom_emoji_id + + self._id_attrs = (self.name, self.icon_custom_emoji_id) + + self._freeze() + + +class GeneralForumTopicHidden(TelegramObject): + """ + This object represents a service message about General forum topic hidden in the chat. + Currently holds no information. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: JSONDict = None): + super().__init__(api_kwargs=api_kwargs) + + self._freeze() + + +class GeneralForumTopicUnhidden(TelegramObject): + """ + This object represents a service message about General forum topic unhidden in the chat. + Currently holds no information. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: JSONDict = None): + super().__init__(api_kwargs=api_kwargs) + + self._freeze() diff --git a/telegram/_message.py b/telegram/_message.py index 38562260ffc..f383bfef006 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -36,7 +36,14 @@ from telegram._files.video import Video from telegram._files.videonote import VideoNote from telegram._files.voice import Voice -from telegram._forumtopic import ForumTopicClosed, ForumTopicCreated, ForumTopicReopened +from telegram._forumtopic import ( + ForumTopicClosed, + ForumTopicCreated, + ForumTopicEdited, + ForumTopicReopened, + GeneralForumTopicHidden, + GeneralForumTopicUnhidden, +) from telegram._games.game import Game from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._messageautodeletetimerchanged import MessageAutoDeleteTimerChanged @@ -59,6 +66,7 @@ VideoChatStarted, ) from telegram._webappdata import WebAppData +from telegram._writeaccessallowed import WriteAccessAllowed from telegram.constants import MessageAttachmentType, ParseMode from telegram.helpers import escape_markdown @@ -277,15 +285,35 @@ class Message(TelegramObject): .. versionadded:: 20.0 forum_topic_created (:class:`telegram.ForumTopicCreated`, optional): Service message: - forum topic created + forum topic created. .. versionadded:: 20.0 forum_topic_closed (:class:`telegram.ForumTopicClosed`, optional): Service message: - forum topic closed + forum topic closed. .. versionadded:: 20.0 forum_topic_reopened (:class:`telegram.ForumTopicReopened`, optional): Service message: - forum topic reopened + forum topic reopened. + + .. versionadded:: 20.0 + forum_topic_edited (:class:`telegram.ForumTopicEdited`, optional): Service message: + forum topic edited. + + .. versionadded:: 20.0 + general_forum_topic_hidden (:class:`telegram.GeneralForumTopicHidden`, optional): + Service message: General forum topic hidden. + + .. versionadded:: 20.0 + general_forum_topic_unhidden (:class:`telegram.GeneralForumTopicUnhidden`, optional): + Service message: General forum topic unhidden. + + .. versionadded:: 20.0 + write_access_allowed (:class:`telegram.WriteAccessAllowed`, optional): Service message: + the user allowed the bot added to the attachment menu to write messages. + + .. versionadded:: 20.0 + has_media_spoiler (:obj:`bool`, optional): :obj:`True`, if the message media is covered + by a spoiler animation. .. versionadded:: 20.0 @@ -484,15 +512,35 @@ class Message(TelegramObject): .. versionadded:: 20.0 forum_topic_created (:class:`telegram.ForumTopicCreated`): Optional. Service message: - forum topic created + forum topic created. .. versionadded:: 20.0 forum_topic_closed (:class:`telegram.ForumTopicClosed`): Optional. Service message: - forum topic closed + forum topic closed. .. versionadded:: 20.0 forum_topic_reopened (:class:`telegram.ForumTopicReopened`): Optional. Service message: - forum topic reopened + forum topic reopened. + + .. versionadded:: 20.0 + forum_topic_edited (:class:`telegram.ForumTopicEdited`): Optional. Service message: + forum topic edited. + + .. versionadded:: 20.0 + general_forum_topic_hidden (:class:`telegram.GeneralForumTopicHidden`): Optional. + Service message: General forum topic hidden. + + .. versionadded:: 20.0 + general_forum_topic_unhidden (:class:`telegram.GeneralForumTopicUnhidden`): Optional. + Service message: General forum topic unhidden. + + .. versionadded:: 20.0 + write_access_allowed (:class:`telegram.WriteAccessAllowed`): Optional. Service message: + the user allowed the bot added to the attachment menu to write messages. + + .. versionadded:: 20.0 + has_media_spoiler (:obj:`bool`): Optional. :obj:`True`, if the message media is covered + by a spoiler animation. .. versionadded:: 20.0 @@ -567,6 +615,11 @@ class Message(TelegramObject): "forum_topic_created", "forum_topic_closed", "forum_topic_reopened", + "forum_topic_edited", + "general_forum_topic_hidden", + "general_forum_topic_unhidden", + "write_access_allowed", + "has_media_spoiler", ) def __init__( @@ -635,6 +688,11 @@ def __init__( forum_topic_created: ForumTopicCreated = None, forum_topic_closed: ForumTopicClosed = None, forum_topic_reopened: ForumTopicReopened = None, + forum_topic_edited: ForumTopicEdited = None, + general_forum_topic_hidden: GeneralForumTopicHidden = None, + general_forum_topic_unhidden: GeneralForumTopicUnhidden = None, + write_access_allowed: WriteAccessAllowed = None, + has_media_spoiler: bool = None, *, api_kwargs: JSONDict = None, ): @@ -706,6 +764,11 @@ def __init__( self.forum_topic_created = forum_topic_created self.forum_topic_closed = forum_topic_closed self.forum_topic_reopened = forum_topic_reopened + self.forum_topic_edited = forum_topic_edited + self.general_forum_topic_hidden = general_forum_topic_hidden + self.general_forum_topic_unhidden = general_forum_topic_unhidden + self.write_access_allowed = write_access_allowed + self.has_media_spoiler = has_media_spoiler self._effective_attachment = DEFAULT_NONE @@ -805,6 +868,16 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Message"]: data["forum_topic_reopened"] = ForumTopicReopened.de_json( data.get("forum_topic_reopened"), bot ) + data["forum_topic_edited"] = ForumTopicEdited.de_json(data.get("forum_topic_edited"), bot) + data["general_forum_topic_hidden"] = GeneralForumTopicHidden.de_json( + data.get("general_forum_topic_hidden"), bot + ) + data["general_forum_topic_unhidden"] = GeneralForumTopicUnhidden.de_json( + data.get("general_forum_topic_unhidden"), bot + ) + data["write_access_allowed"] = WriteAccessAllowed.de_json( + data.get("write_access_allowed"), bot + ) return super().de_json(data=data, bot=bot) @@ -1203,6 +1276,7 @@ async def reply_photo( caption_entities: Sequence["MessageEntity"] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: int = None, + has_spoiler: bool = None, *, filename: str = None, quote: bool = None, @@ -1246,6 +1320,7 @@ async def reply_photo( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + has_spoiler=has_spoiler, ) async def reply_audio( @@ -1390,6 +1465,7 @@ async def reply_animation( caption_entities: Sequence["MessageEntity"] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: int = None, + has_spoiler: bool = None, *, filename: str = None, quote: bool = None, @@ -1438,6 +1514,7 @@ async def reply_animation( filename=filename, protect_content=protect_content, message_thread_id=message_thread_id, + has_spoiler=has_spoiler, ) async def reply_sticker( @@ -1506,6 +1583,7 @@ async def reply_video( caption_entities: Sequence["MessageEntity"] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: int = None, + has_spoiler: bool = None, *, filename: str = None, quote: bool = None, @@ -1554,6 +1632,7 @@ async def reply_video( filename=filename, protect_content=protect_content, message_thread_id=message_thread_id, + has_spoiler=has_spoiler, ) async def reply_video_note( @@ -1980,6 +2059,7 @@ async def reply_dice( async def reply_chat_action( self, action: str, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2001,6 +2081,7 @@ async def reply_chat_action( """ return await self.get_bot().send_chat_action( chat_id=self.chat_id, + message_thread_id=message_thread_id, action=action, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2815,8 +2896,8 @@ async def unpin( async def edit_forum_topic( self, - name: str, - icon_custom_emoji_id: str, + name: str = None, + icon_custom_emoji_id: str = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, diff --git a/telegram/_replykeyboardmarkup.py b/telegram/_replykeyboardmarkup.py index 66b5271be97..a13c8c2f7a6 100644 --- a/telegram/_replykeyboardmarkup.py +++ b/telegram/_replykeyboardmarkup.py @@ -68,6 +68,11 @@ class ReplyKeyboardMarkup(TelegramObject): characters. .. versionadded:: 13.7 + is_persistent (:obj:`bool`, optional): Requests clients to always show the keyboard when + the regular keyboard is hidden. Defaults to :obj:`False`, in which case the custom + keyboard can be hidden and opened with a keyboard icon. + + .. versionadded:: 20.0 Attributes: keyboard (Tuple[Tuple[:class:`telegram.KeyboardButton`]]): Array of button rows, @@ -97,6 +102,11 @@ class ReplyKeyboardMarkup(TelegramObject): characters. .. versionadded:: 13.7 + is_persistent (:obj:`bool`): Optional. Requests clients to always show the keyboard when + the regular keyboard is hidden. If :obj:`False`, the custom keyboard can be hidden and + opened with a keyboard icon. + + .. versionadded:: 20.0 """ @@ -106,6 +116,7 @@ class ReplyKeyboardMarkup(TelegramObject): "resize_keyboard", "one_time_keyboard", "input_field_placeholder", + "is_persistent", ) def __init__( @@ -115,6 +126,7 @@ def __init__( one_time_keyboard: bool = None, selective: bool = None, input_field_placeholder: str = None, + is_persistent: bool = None, *, api_kwargs: JSONDict = None, ): @@ -136,6 +148,7 @@ def __init__( self.one_time_keyboard = one_time_keyboard self.selective = selective self.input_field_placeholder = input_field_placeholder + self.is_persistent = is_persistent self._id_attrs = (self.keyboard,) @@ -149,6 +162,7 @@ def from_button( one_time_keyboard: bool = False, selective: bool = False, input_field_placeholder: str = None, + is_persistent: bool = None, **kwargs: object, ) -> "ReplyKeyboardMarkup": """Shortcut for:: @@ -182,6 +196,11 @@ def from_button( field when the reply is active. .. versionadded:: 13.7 + is_persistent (:obj:`bool`): Optional. Requests clients to always show the keyboard + when the regular keyboard is hidden. Defaults to :obj:`False`, in which case the + custom keyboard can be hidden and opened with a keyboard icon. + + .. versionadded:: 20.0 """ return cls( [[button]], @@ -189,6 +208,7 @@ def from_button( one_time_keyboard=one_time_keyboard, selective=selective, input_field_placeholder=input_field_placeholder, + is_persistent=is_persistent, **kwargs, # type: ignore[arg-type] ) @@ -200,6 +220,7 @@ def from_row( one_time_keyboard: bool = False, selective: bool = False, input_field_placeholder: str = None, + is_persistent: bool = None, **kwargs: object, ) -> "ReplyKeyboardMarkup": """Shortcut for:: @@ -236,6 +257,11 @@ def from_row( field when the reply is active. .. versionadded:: 13.7 + is_persistent (:obj:`bool`): Optional. Requests clients to always show the keyboard + when the regular keyboard is hidden. Defaults to :obj:`False`, in which case the + custom keyboard can be hidden and opened with a keyboard icon. + + .. versionadded:: 20.0 """ return cls( @@ -244,6 +270,7 @@ def from_row( one_time_keyboard=one_time_keyboard, selective=selective, input_field_placeholder=input_field_placeholder, + is_persistent=is_persistent, **kwargs, # type: ignore[arg-type] ) @@ -255,6 +282,7 @@ def from_column( one_time_keyboard: bool = False, selective: bool = False, input_field_placeholder: str = None, + is_persistent: bool = None, **kwargs: object, ) -> "ReplyKeyboardMarkup": """Shortcut for:: @@ -291,6 +319,11 @@ def from_column( field when the reply is active. .. versionadded:: 13.7 + is_persistent (:obj:`bool`): Optional. Requests clients to always show the keyboard + when the regular keyboard is hidden. Defaults to :obj:`False`, in which case the + custom keyboard can be hidden and opened with a keyboard icon. + + .. versionadded:: 20.0 """ button_grid = [[button] for button in button_column] @@ -300,6 +333,7 @@ def from_column( one_time_keyboard=one_time_keyboard, selective=selective, input_field_placeholder=input_field_placeholder, + is_persistent=is_persistent, **kwargs, # type: ignore[arg-type] ) diff --git a/telegram/_user.py b/telegram/_user.py index 2fe88b69561..a6bc4263698 100644 --- a/telegram/_user.py +++ b/telegram/_user.py @@ -427,6 +427,7 @@ async def send_photo( caption_entities: Sequence["MessageEntity"] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: int = None, + has_spoiler: bool = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -463,6 +464,7 @@ async def send_photo( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + has_spoiler=has_spoiler, ) async def send_media_group( @@ -575,6 +577,7 @@ async def send_audio( async def send_chat_action( self, action: str, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -595,6 +598,7 @@ async def send_chat_action( return await self.get_bot().send_chat_action( chat_id=self.id, action=action, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -955,6 +959,7 @@ async def send_animation( caption_entities: Sequence["MessageEntity"] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: int = None, + has_spoiler: bool = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -995,6 +1000,7 @@ async def send_animation( filename=filename, protect_content=protect_content, message_thread_id=message_thread_id, + has_spoiler=has_spoiler, ) async def send_sticker( @@ -1056,6 +1062,7 @@ async def send_video( caption_entities: Sequence["MessageEntity"] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: int = None, + has_spoiler: bool = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1097,6 +1104,7 @@ async def send_video( filename=filename, protect_content=protect_content, message_thread_id=message_thread_id, + has_spoiler=has_spoiler, ) async def send_venue( diff --git a/telegram/_writeaccessallowed.py b/telegram/_writeaccessallowed.py new file mode 100644 index 00000000000..389a2d59eb4 --- /dev/null +++ b/telegram/_writeaccessallowed.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains objects related to the write access allowed service message.""" +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class WriteAccessAllowed(TelegramObject): + """ + This object represents a service message about a user allowing a bot added to the attachment + menu to write messages. Currently holds no information. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: JSONDict = None): + super().__init__(api_kwargs=api_kwargs) + + self._freeze() diff --git a/telegram/constants.py b/telegram/constants.py index 3fae673e0d0..6936f9003b4 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -111,7 +111,7 @@ def __str__(self) -> str: #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=3) +BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=4) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. @@ -1568,14 +1568,22 @@ class ForumTopicLimit(IntEnum): __slots__ = () MIN_NAME_LENGTH = 1 - """:obj:`int`: Minimum length of a :obj:`str` passed as the - :paramref:`~telegram.Bot.create_forum_topic.name` parameter of - :meth:`telegram.Bot.create_forum_topic` and :paramref:`~telegram.Bot.edit_forum_topic.name` - parameter of :meth:`telegram.Bot.edit_forum_topic`. + """:obj:`int`: Minimum length of a :obj:`str` passed as: + + * :paramref:`~telegram.Bot.create_forum_topic.name` parameter of + :meth:`telegram.Bot.create_forum_topic` + * :paramref:`~telegram.Bot.edit_forum_topic.name` parameter of + :meth:`telegram.Bot.edit_forum_topic` + * :paramref:`~telegram.Bot.edit_general_forum_topic.name` parameter of + :meth:`telegram.Bot.edit_general_forum_topic` """ MAX_NAME_LENGTH = 128 - """:obj:`int`: Maximum length of a :obj:`str` passed as the - :paramref:`~telegram.Bot.create_forum_topic.name` parameter of - :meth:`telegram.Bot.create_forum_topic` and :paramref:`~telegram.Bot.edit_forum_topic.name` - parameter of :meth:`telegram.Bot.edit_forum_topic`. + """:obj:`int`: Maximum length of a :obj:`str` passed as: + + * :paramref:`~telegram.Bot.create_forum_topic.name` parameter of + :meth:`telegram.Bot.create_forum_topic` + * :paramref:`~telegram.Bot.edit_forum_topic.name` parameter of + :meth:`telegram.Bot.edit_forum_topic` + * :paramref:`~telegram.Bot.edit_general_forum_topic.name` parameter of + :meth:`telegram.Bot.edit_general_forum_topic` """ diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index e0708b3e60b..ac5194b3d74 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -1264,8 +1264,8 @@ async def edit_forum_topic( self, chat_id: Union[str, int], message_thread_id: int, - name: str, - icon_custom_emoji_id: str, + name: str = None, + icon_custom_emoji_id: str = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1286,6 +1286,28 @@ async def edit_forum_topic( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def edit_general_forum_topic( + self, + chat_id: Union[str, int], + name: str, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + rate_limit_args: RLARGS = None, + ) -> bool: + return await super().edit_general_forum_topic( + chat_id=chat_id, + name=name, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + async def edit_message_caption( self, chat_id: Union[str, int] = None, @@ -1862,6 +1884,26 @@ async def close_forum_topic( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def close_general_forum_topic( + self, + chat_id: Union[str, int], + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + rate_limit_args: RLARGS = None, + ) -> bool: + return await super().close_general_forum_topic( + chat_id=chat_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + async def create_forum_topic( self, chat_id: Union[str, int], @@ -1888,6 +1930,66 @@ async def create_forum_topic( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def reopen_general_forum_topic( + self, + chat_id: Union[str, int], + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + rate_limit_args: RLARGS = None, + ) -> bool: + return await super().reopen_general_forum_topic( + chat_id=chat_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def hide_general_forum_topic( + self, + chat_id: Union[str, int], + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + rate_limit_args: RLARGS = None, + ) -> bool: + return await super().hide_general_forum_topic( + chat_id=chat_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def unhide_general_forum_topic( + self, + chat_id: Union[str, int], + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + rate_limit_args: RLARGS = None, + ) -> bool: + return await super().unhide_general_forum_topic( + chat_id=chat_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + async def pin_chat_message( self, chat_id: Union[str, int], @@ -2045,6 +2147,7 @@ async def send_animation( caption_entities: Sequence["MessageEntity"] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: int = None, + has_spoiler: bool = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2070,6 +2173,7 @@ async def send_animation( caption_entities=caption_entities, protect_content=protect_content, message_thread_id=message_thread_id, + has_spoiler=has_spoiler, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2132,6 +2236,7 @@ async def send_chat_action( self, chat_id: Union[str, int], action: str, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2143,6 +2248,7 @@ async def send_chat_action( return await super().send_chat_action( chat_id=chat_id, action=action, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2519,6 +2625,7 @@ async def send_photo( caption_entities: Sequence["MessageEntity"] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: int = None, + has_spoiler: bool = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2540,6 +2647,7 @@ async def send_photo( caption_entities=caption_entities, protect_content=protect_content, message_thread_id=message_thread_id, + has_spoiler=has_spoiler, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2706,6 +2814,7 @@ async def send_video( caption_entities: Sequence["MessageEntity"] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, message_thread_id: int = None, + has_spoiler: bool = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2732,6 +2841,7 @@ async def send_video( caption_entities=caption_entities, protect_content=protect_content, message_thread_id=message_thread_id, + has_spoiler=has_spoiler, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, @@ -3414,3 +3524,8 @@ async def upload_sticker_file( reopenForumTopic = reopen_forum_topic deleteForumTopic = delete_forum_topic unpinAllForumTopicMessages = unpin_all_forum_topic_messages + editGeneralForumTopic = edit_general_forum_topic + closeGeneralForumTopic = close_general_forum_topic + reopenGeneralForumTopic = reopen_general_forum_topic + hideGeneralForumTopic = hide_general_forum_topic + unhideGeneralForumTopic = unhide_general_forum_topic diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index f60bddd61ce..3824b0be0ab 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -58,6 +58,7 @@ "FORWARDED", "ForwardedFrom", "GAME", + "HAS_MEDIA_SPOILER", "HAS_PROTECTED_CONTENT", "INVOICE", "IS_AUTOMATIC_FORWARD", @@ -1383,6 +1384,20 @@ def filter(self, message: Message) -> bool: """Messages that contain :attr:`telegram.Message.game`.""" +class _HasMediaSpoiler(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.has_media_spoiler) + + +HAS_MEDIA_SPOILER = _HasMediaSpoiler(name="filters.HAS_MEDIA_SPOILER") +"""Messages that contain :attr:`telegram.Message.has_media_spoiler`. + + .. versionadded:: 20.0 +""" + + class _HasProtectedContent(MessageFilter): __slots__ = () @@ -1721,6 +1736,10 @@ def filter(self, update: Update) -> bool: or StatusUpdate.FORUM_TOPIC_CREATED.check_update(update) or StatusUpdate.FORUM_TOPIC_CLOSED.check_update(update) or StatusUpdate.FORUM_TOPIC_REOPENED.check_update(update) + or StatusUpdate.FORUM_TOPIC_EDITED.check_update(update) + or StatusUpdate.GENERAL_FORUM_TOPIC_HIDDEN.check_update(update) + or StatusUpdate.GENERAL_FORUM_TOPIC_UNHIDDEN.check_update(update) + or StatusUpdate.WRITE_ACCESS_ALLOWED.check_update(update) ) ALL = _All(name="filters.StatusUpdate.ALL") @@ -1783,6 +1802,18 @@ def filter(self, message: Message) -> bool: .. versionadded:: 20.0 """ + class _ForumTopicEdited(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.forum_topic_edited) + + FORUM_TOPIC_EDITED = _ForumTopicEdited(name="filters.StatusUpdate.FORUM_TOPIC_EDITED") + """Messages that contain :attr:`telegram.Message.forum_topic_edited`. + + .. versionadded:: 20.0 + """ + class _ForumTopicReopened(MessageFilter): __slots__ = () @@ -1795,6 +1826,34 @@ def filter(self, message: Message) -> bool: .. versionadded:: 20.0 """ + class _GeneralForumTopicHidden(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.general_forum_topic_hidden) + + GENERAL_FORUM_TOPIC_HIDDEN = _GeneralForumTopicHidden( + name="filters.StatusUpdate.GENERAL_FORUM_TOPIC_HIDDEN" + ) + """Messages that contain :attr:`telegram.Message.general_forum_topic_hidden`. + + .. versionadded:: 20.0 + """ + + class _GeneralForumTopicUnhidden(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.general_forum_topic_unhidden) + + GENERAL_FORUM_TOPIC_UNHIDDEN = _GeneralForumTopicUnhidden( + name="filters.StatusUpdate.GENERAL_FORUM_TOPIC_UNHIDDEN" + ) + """Messages that contain :attr:`telegram.Message.general_forum_topic_unhidden`. + + .. versionadded:: 20.0 + """ + class _LeftChatMember(MessageFilter): __slots__ = () @@ -1945,6 +2004,18 @@ def filter(self, message: Message) -> bool: .. versionadded:: 20.0 """ + class _WriteAccessAllowed(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.write_access_allowed) + + WRITE_ACCESS_ALLOWED = _WriteAccessAllowed(name="filters.StatusUpdate.WRITE_ACCESS_ALLOWED") + """Messages that contain :attr:`telegram.Message.write_access_allowed`. + + .. versionadded:: 20.0 + """ + class Sticker: """Filters messages which contain a sticker. diff --git a/tests/bots.py b/tests/bots.py index 4ed5160a5ac..97cdd0ed665 100644 --- a/tests/bots.py +++ b/tests/bots.py @@ -27,15 +27,15 @@ # purposes than testing. FALLBACKS = ( "W3sidG9rZW4iOiAiNTc5Njk0NzE0OkFBRnBLOHc2emtrVXJENHhTZVl3RjNNTzhlLTRHcm1jeTdjIiwgInBheW1lbnRfc" - "HJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpRME5qWmxOekk1WWpKaSIsICJjaGF0X2lkIjogIjY3NTY2Nj" - "IyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTMxMDkxMTEzNSIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTYxOTE" - "1OTQwNCIsICJjaGFubmVsX2lkIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0" - "cyBmYWxsYmFjayAxIiwgImJvdF91c2VybmFtZSI6ICJAcHRiX2ZhbGxiYWNrXzFfYm90In0sIHsidG9rZW4iOiAiNTU4M" - "Tk0MDY2OkFBRndEUElGbHpHVWxDYVdIdFRPRVg0UkZyWDh1OURNcWZvIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOi" - "AiMjg0Njg1MDYzOlRFU1Q6WWpFd09EUXdNVEZtTkRjeSIsICJjaGF0X2lkIjogIjY3NTY2NjIyNCIsICJzdXBlcl9ncm9" - "1cF9pZCI6ICItMTAwMTIyMTIxNjgzMCIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTYxOTE1OTQwNCIsICJjaGFubmVs" - "X2lkIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBmYWxsYmFjayAyIiwgI" - "mJvdF91c2VybmFtZSI6ICJAcHRiX2ZhbGxiYWNrXzJfYm90In1d " + "HJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpRME5qWmxOekk1WWpKaSIsICJjaGF0X2 lkIjogIjY3NTY2N" + "jIyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTMxMDkxMTEzNSIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTgzOD" + "AwNDU3NyIsICJjaGFubmVsX2lkIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIi wgIm5hbWUiOiAiUFRCIHRlc3RzIG" + "ZhbGxiYWNrIDEiLCAidXNlcm5hbWUiOiAiQHB0Yl9mYWxsYmFja18xX2JvdCJ9LCB7InRva2VuIjogIjU1ODE5NDA2Njp" + "BQUZ3RFBJRmx6R1VsQ2FXSHRUT0VYNFJGclg4dTlETXFmbyIsIC JwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY" + "4NTA2MzpURVNUOllqRXdPRFF3TVRGbU5EY3kiLCAiY2hhdF9pZCI6ICI2NzU2NjYyMjQiLCAic3VwZXJfZ3JvdXBfaWQi" + "OiAiLTEwMDEyMjEyMTY4MzAiLCAiZm9ydW1fZ3 JvdXBfaWQiOiAiLTEwMDE4NTc4NDgzMTQiLCAiY2hhbm5lbF9pZCI6" + "ICJAcHl0aG9udGVsZWdyYW1ib3R0ZXN0cyIsICJuYW1lIjogIlBUQiB0ZXN0cyBmYWxsYmFjayAyIiwgInVzZXJuYW1lI" + "jogIkBwdGJfZmFsbGJhY2tfMl9ib3QifV0=" ) GITHUB_ACTION = os.getenv("GITHUB_ACTION", None) diff --git a/tests/test_animation.py b/tests/test_animation.py index 98c2184eaba..fe64033e040 100644 --- a/tests/test_animation.py +++ b/tests/test_animation.py @@ -93,6 +93,7 @@ async def test_send_all_args(self, bot, chat_id, animation_file, animation, thum disable_notification=False, protect_content=True, thumb=thumb_file, + has_spoiler=True, ) assert isinstance(message.animation, Animation) @@ -106,6 +107,10 @@ async def test_send_all_args(self, bot, chat_id, animation_file, animation, thum assert message.animation.thumb.width == self.width assert message.animation.thumb.height == self.height assert message.has_protected_content + try: + assert message.has_media_spoiler + except AssertionError: + pytest.xfail("This is a bug on Telegram's end") @pytest.mark.flaky(3, 1) async def test_send_animation_custom_filename(self, bot, chat_id, animation_file, monkeypatch): diff --git a/tests/test_bot.py b/tests/test_bot.py index 501c4960332..a02f422b2c2 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -967,6 +967,18 @@ async def test_wrong_chat_action(self, bot, chat_id): with pytest.raises(BadRequest, match="Wrong parameter action"): await bot.send_chat_action(chat_id, "unknown action") + async def test_send_chat_action_all_args(self, bot, chat_id, provider_token, monkeypatch): + async def make_assertion(*args, **_): + kwargs = args[1] + return ( + kwargs["chat_id"] == chat_id + and kwargs["action"] == "action" + and kwargs["message_thread_id"] == 1 + ) + + monkeypatch.setattr(bot, "_post", make_assertion) + assert await bot.send_chat_action(chat_id, "action", 1) + @pytest.mark.asyncio async def test_answer_web_app_query(self, bot, raw_bot, monkeypatch): params = False @@ -2525,7 +2537,7 @@ async def test_pin_and_unpin_message(self, bot, super_group_id): # set_sticker_position_in_set, delete_sticker_from_set and get_custom_emoji_stickers # are tested in the test_sticker module. - # get_forum_topic_icon_stickers, edit_forum_topic, etc... + # get_forum_topic_icon_stickers, edit_forum_topic, general_forum etc... # are tested in the test_forum module. async def test_timeout_propagation_explicit(self, monkeypatch, bot, chat_id): diff --git a/tests/test_chat.py b/tests/test_chat.py index a8418ec9f6b..bd8d0661d30 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -51,6 +51,8 @@ def chat(bot): is_forum=True, active_usernames=TestChat.active_usernames, emoji_status_custom_emoji_id=TestChat.emoji_status_custom_emoji_id, + has_aggressive_anti_spam_enabled=TestChat.has_aggressive_anti_spam_enabled, + has_hidden_members=TestChat.has_hidden_members, ) chat.set_bot(bot) chat._unfreeze() @@ -82,6 +84,8 @@ class TestChat: is_forum = True active_usernames = ["These", "Are", "Usernames!"] emoji_status_custom_emoji_id = "VeryUniqueCustomEmojiID" + has_aggressive_anti_spam_enabled = True + has_hidden_members = True def test_slot_behaviour(self, chat, mro_slots): for attr in chat.__slots__: @@ -112,6 +116,8 @@ def test_de_json(self, bot): "is_forum": self.is_forum, "active_usernames": self.active_usernames, "emoji_status_custom_emoji_id": self.emoji_status_custom_emoji_id, + "has_aggressive_anti_spam_enabled": self.has_aggressive_anti_spam_enabled, + "has_hidden_members": self.has_hidden_members, } chat = Chat.de_json(json_dict, bot) @@ -141,6 +147,8 @@ def test_de_json(self, bot): assert chat.is_forum == self.is_forum assert chat.active_usernames == tuple(self.active_usernames) assert chat.emoji_status_custom_emoji_id == self.emoji_status_custom_emoji_id + assert chat.has_aggressive_anti_spam_enabled == self.has_aggressive_anti_spam_enabled + assert chat.has_hidden_members == self.has_hidden_members def test_to_dict(self, chat): chat_dict = chat.to_dict() @@ -166,6 +174,10 @@ def test_to_dict(self, chat): assert chat_dict["is_forum"] == chat.is_forum assert chat_dict["active_usernames"] == list(chat.active_usernames) assert chat_dict["emoji_status_custom_emoji_id"] == chat.emoji_status_custom_emoji_id + assert ( + chat_dict["has_aggressive_anti_spam_enabled"] == chat.has_aggressive_anti_spam_enabled + ) + assert chat_dict["has_hidden_members"] == chat.has_hidden_members def test_always_tuples_attributes(self): chat = Chat( @@ -1032,6 +1044,111 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(chat.get_bot(), "unpin_all_forum_topic_messages", make_assertion) assert await chat.unpin_all_forum_topic_messages(message_thread_id=42) + async def test_edit_general_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["name"] == "WhatAName" + + assert check_shortcut_signature( + Chat.edit_general_forum_topic, + Bot.edit_general_forum_topic, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.edit_general_forum_topic, + chat.get_bot(), + "edit_general_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.edit_general_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "edit_general_forum_topic", make_assertion) + assert await chat.edit_general_forum_topic(name="WhatAName") + + async def test_close_general_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id + + assert check_shortcut_signature( + Chat.close_general_forum_topic, + Bot.close_general_forum_topic, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.close_general_forum_topic, + chat.get_bot(), + "close_general_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.close_general_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "close_general_forum_topic", make_assertion) + assert await chat.close_general_forum_topic() + + async def test_reopen_general_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id + + assert check_shortcut_signature( + Chat.reopen_general_forum_topic, + Bot.reopen_general_forum_topic, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.reopen_general_forum_topic, + chat.get_bot(), + "reopen_general_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.reopen_general_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "reopen_general_forum_topic", make_assertion) + assert await chat.reopen_general_forum_topic() + + async def test_hide_general_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id + + assert check_shortcut_signature( + Chat.hide_general_forum_topic, + Bot.hide_general_forum_topic, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.hide_general_forum_topic, + chat.get_bot(), + "hide_general_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.hide_general_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "hide_general_forum_topic", make_assertion) + assert await chat.hide_general_forum_topic() + + async def test_unhide_general_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id + + assert check_shortcut_signature( + Chat.unhide_general_forum_topic, + Bot.unhide_general_forum_topic, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.unhide_general_forum_topic, + chat.get_bot(), + "unhide_general_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.unhide_general_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "unhide_general_forum_topic", make_assertion) + assert await chat.unhide_general_forum_topic() + def test_mention_html(self): with pytest.raises(TypeError, match="Can not create a mention to a private group chat"): chat = Chat(id=1, type="foo") diff --git a/tests/test_filters.py b/tests/test_filters.py index 41ed1cecd09..dd02b8f62d9 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1026,6 +1026,26 @@ def test_filters_status_update(self, update): assert filters.StatusUpdate.FORUM_TOPIC_REOPENED.check_update(update) update.message.forum_topic_reopened = None + update.message.forum_topic_edited = "topic_edited" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.FORUM_TOPIC_EDITED.check_update(update) + update.message.forum_topic_edited = None + + update.message.general_forum_topic_hidden = "topic_hidden" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.GENERAL_FORUM_TOPIC_HIDDEN.check_update(update) + update.message.general_forum_topic_hidden = None + + update.message.general_forum_topic_unhidden = "topic_unhidden" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.GENERAL_FORUM_TOPIC_UNHIDDEN.check_update(update) + update.message.general_forum_topic_unhidden = None + + update.message.write_access_allowed = "allowed" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.WRITE_ACCESS_ALLOWED.check_update(update) + update.message.write_access_allowed = None + def test_filters_forwarded(self, update): assert not filters.FORWARDED.check_update(update) update.message.forward_date = datetime.datetime.utcnow() @@ -1815,6 +1835,11 @@ def test_filters_is_topic_message(self, update): update.message.is_topic_message = True assert filters.IS_TOPIC_MESSAGE.check_update(update) + def test_filters_has_media_spoiler(self, update): + assert not filters.HAS_MEDIA_SPOILER.check_update(update) + update.message.has_media_spoiler = True + assert filters.HAS_MEDIA_SPOILER.check_update(update) + def test_filters_has_protected_content(self, update): assert not filters.HAS_PROTECTED_CONTENT.check_update(update) update.message.has_protected_content = True diff --git a/tests/test_forum.py b/tests/test_forum.py index d903d3e3d1a..c37e1a3d5ca 100644 --- a/tests/test_forum.py +++ b/tests/test_forum.py @@ -16,9 +16,20 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +import datetime + import pytest -from telegram import ForumTopic, ForumTopicClosed, ForumTopicCreated, ForumTopicReopened, Sticker +from telegram import ( + ForumTopic, + ForumTopicClosed, + ForumTopicCreated, + ForumTopicEdited, + ForumTopicReopened, + GeneralForumTopicHidden, + GeneralForumTopicUnhidden, + Sticker, +) TEST_MSG_TEXT = "Topics are forever" TEST_TOPIC_ICON_COLOR = 0x6FB9F0 @@ -139,7 +150,9 @@ def test_equality(self, emoji_id, forum_group_id): assert a != e assert hash(a) != hash(e) - @pytest.mark.flaky(3, 1) + +@pytest.mark.flaky(3, 1) +class TestForumMethods: async def test_create_forum_topic(self, real_topic): result = real_topic assert isinstance(result, ForumTopic) @@ -161,7 +174,6 @@ async def test_create_forum_topic_with_only_required_args(self, bot, forum_group ) assert result is True, "Failed to delete forum topic" - @pytest.mark.flaky(3, 1) async def test_get_forum_topic_icon_stickers(self, bot): emoji_sticker_list = await bot.get_forum_topic_icon_stickers() first_sticker = emoji_sticker_list[0] @@ -194,7 +206,6 @@ async def test_edit_forum_topic(self, emoji_id, forum_group_id, bot, real_topic) assert result is True, "Failed to edit forum topic" # no way of checking the edited name, just the boolean result - @pytest.mark.flaky(3, 1) async def test_send_message_to_topic(self, bot, forum_group_id, real_topic): message_thread_id = real_topic.message_thread_id @@ -223,7 +234,6 @@ async def test_close_and_reopen_forum_topic(self, bot, forum_group_id, real_topi ) assert result is True, "Failed to reopen forum topic" - @pytest.mark.xfail(reason="Can fail due to race conditions in GH actions CI") async def test_unpin_all_forum_topic_messages(self, bot, forum_group_id, real_topic): message_thread_id = real_topic.message_thread_id @@ -244,6 +254,43 @@ async def test_unpin_all_forum_topic_messages(self, bot, forum_group_id, real_to ) assert result is True, "Failed to unpin all the messages in forum topic" + async def test_edit_general_forum_topic(self, bot, forum_group_id): + result = await bot.edit_general_forum_topic( + chat_id=forum_group_id, + name=f"GENERAL_{datetime.datetime.now().timestamp()}", + ) + assert result is True, "Failed to edit general forum topic" + # no way of checking the edited name, just the boolean result + + async def test_close_and_reopen_general_forum_topic(self, bot, forum_group_id): + result = await bot.close_general_forum_topic( + chat_id=forum_group_id, + ) + assert result is True, "Failed to close general forum topic" + + result = await bot.reopen_general_forum_topic( + chat_id=forum_group_id, + ) + assert result is True, "Failed to reopen general forum topic" + + async def test_hide_and_unhide_general_forum_topic(self, bot, forum_group_id): + + result = await bot.hide_general_forum_topic( + chat_id=forum_group_id, + ) + assert result is True, "Failed to hide general forum topic" + + result = await bot.unhide_general_forum_topic( + chat_id=forum_group_id, + ) + assert result is True, "Failed to unhide general forum topic" + + # hiding the general topic also closes it, so we reopen it + result = await bot.reopen_general_forum_topic( + chat_id=forum_group_id, + ) + assert result is True, "Failed to reopen general forum topic" + @pytest.fixture def topic_created(): @@ -333,3 +380,95 @@ def test_to_dict(self): action = ForumTopicReopened() action_dict = action.to_dict() assert action_dict == {} + + +@pytest.fixture(scope="module") +def topic_edited(emoji_id): + return ForumTopicEdited(name=TEST_TOPIC_NAME, icon_custom_emoji_id=emoji_id) + + +class TestForumTopicEdited: + def test_slot_behaviour(self, topic_edited, mro_slots): + for attr in topic_edited.__slots__: + assert getattr(topic_edited, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(topic_edited)) == len(set(mro_slots(topic_edited))), "duplicate slot" + + def test_expected_values(self, topic_edited, emoji_id): + assert topic_edited.name == TEST_TOPIC_NAME + assert topic_edited.icon_custom_emoji_id == emoji_id + + def test_de_json(self, bot, emoji_id): + assert ForumTopicEdited.de_json(None, bot=bot) is None + + json_dict = {"name": TEST_TOPIC_NAME, "icon_custom_emoji_id": emoji_id} + action = ForumTopicEdited.de_json(json_dict, bot) + assert action.api_kwargs == {} + + assert action.name == TEST_TOPIC_NAME + assert action.icon_custom_emoji_id == emoji_id + # special test since it is mentioned in the docs that icon_custom_emoji_id can be an + # empty string + json_dict = {"icon_custom_emoji_id": ""} + action = ForumTopicEdited.de_json(json_dict, bot) + assert action.icon_custom_emoji_id == "" + + def test_to_dict(self, topic_edited, emoji_id): + action_dict = topic_edited.to_dict() + + assert isinstance(action_dict, dict) + assert action_dict["name"] == TEST_TOPIC_NAME + assert action_dict["icon_custom_emoji_id"] == emoji_id + + def test_equality(self, emoji_id): + a = ForumTopicEdited(name=TEST_TOPIC_NAME, icon_custom_emoji_id="") + b = ForumTopicEdited( + name=TEST_TOPIC_NAME, + icon_custom_emoji_id="", + ) + c = ForumTopicEdited(name=f"{TEST_TOPIC_NAME}!", icon_custom_emoji_id=emoji_id) + d = ForumTopicEdited(icon_custom_emoji_id="") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + +class TestGeneralForumTopicHidden: + def test_slot_behaviour(self, mro_slots): + action = GeneralForumTopicHidden() + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + def test_de_json(self): + action = GeneralForumTopicHidden.de_json({}, None) + assert action.api_kwargs == {} + assert isinstance(action, GeneralForumTopicHidden) + + def test_to_dict(self): + action = GeneralForumTopicHidden() + action_dict = action.to_dict() + assert action_dict == {} + + +class TestGeneralForumTopicUnhidden: + def test_slot_behaviour(self, mro_slots): + action = GeneralForumTopicUnhidden() + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + def test_de_json(self): + action = GeneralForumTopicUnhidden.de_json({}, None) + assert action.api_kwargs == {} + assert isinstance(action, GeneralForumTopicUnhidden) + + def test_to_dict(self): + action = GeneralForumTopicUnhidden() + action_dict = action.to_dict() + assert action_dict == {} diff --git a/tests/test_inputmedia.py b/tests/test_inputmedia.py index 3658853399d..8b15c3a560f 100644 --- a/tests/test_inputmedia.py +++ b/tests/test_inputmedia.py @@ -68,6 +68,7 @@ def input_media_video(class_thumb_file): caption_entities=TestInputMediaVideo.caption_entities, thumb=class_thumb_file, supports_streaming=TestInputMediaVideo.supports_streaming, + has_spoiler=TestInputMediaVideo.has_spoiler, ) @@ -78,6 +79,7 @@ def input_media_photo(class_thumb_file): caption=TestInputMediaPhoto.caption, parse_mode=TestInputMediaPhoto.parse_mode, caption_entities=TestInputMediaPhoto.caption_entities, + has_spoiler=TestInputMediaPhoto.has_spoiler, ) @@ -92,6 +94,7 @@ def input_media_animation(class_thumb_file): height=TestInputMediaAnimation.height, thumb=class_thumb_file, duration=TestInputMediaAnimation.duration, + has_spoiler=TestInputMediaAnimation.has_spoiler, ) @@ -142,6 +145,7 @@ class TestInputMediaVideo: parse_mode = "HTML" supports_streaming = True caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)] + has_spoiler = True def test_slot_behaviour(self, input_media_video, mro_slots): inst = input_media_video @@ -160,6 +164,7 @@ def test_expected_values(self, input_media_video): assert input_media_video.caption_entities == tuple(self.caption_entities) assert input_media_video.supports_streaming == self.supports_streaming assert isinstance(input_media_video.thumb, InputFile) + assert input_media_video.has_spoiler == self.has_spoiler def test_caption_entities_always_tuple(self): input_media_video = InputMediaVideo(self.media) @@ -178,6 +183,7 @@ def test_to_dict(self, input_media_video): ce.to_dict() for ce in input_media_video.caption_entities ] assert input_media_video_dict["supports_streaming"] == input_media_video.supports_streaming + assert input_media_video_dict["has_spoiler"] == input_media_video.has_spoiler def test_with_video(self, video): # noqa: F811 # fixture found in test_video @@ -210,6 +216,7 @@ class TestInputMediaPhoto: caption = "My Caption" parse_mode = "Markdown" caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)] + has_spoiler = True def test_slot_behaviour(self, input_media_photo, mro_slots): inst = input_media_photo @@ -223,6 +230,7 @@ def test_expected_values(self, input_media_photo): assert input_media_photo.caption == self.caption assert input_media_photo.parse_mode == self.parse_mode assert input_media_photo.caption_entities == tuple(self.caption_entities) + assert input_media_photo.has_spoiler == self.has_spoiler def test_caption_entities_always_tuple(self): input_media_photo = InputMediaPhoto(self.media) @@ -237,6 +245,7 @@ def test_to_dict(self, input_media_photo): assert input_media_photo_dict["caption_entities"] == [ ce.to_dict() for ce in input_media_photo.caption_entities ] + assert input_media_photo_dict["has_spoiler"] == input_media_photo.has_spoiler def test_with_photo(self, photo): # noqa: F811 # fixture found in test_photo @@ -266,6 +275,7 @@ class TestInputMediaAnimation: width = 30 height = 30 duration = 1 + has_spoiler = True def test_slot_behaviour(self, input_media_animation, mro_slots): inst = input_media_animation @@ -280,6 +290,7 @@ def test_expected_values(self, input_media_animation): assert input_media_animation.parse_mode == self.parse_mode assert input_media_animation.caption_entities == tuple(self.caption_entities) assert isinstance(input_media_animation.thumb, InputFile) + assert input_media_animation.has_spoiler == self.has_spoiler def test_caption_entities_always_tuple(self): input_media_animation = InputMediaAnimation(self.media) @@ -297,6 +308,7 @@ def test_to_dict(self, input_media_animation): assert input_media_animation_dict["width"] == input_media_animation.width assert input_media_animation_dict["height"] == input_media_animation.height assert input_media_animation_dict["duration"] == input_media_animation.duration + assert input_media_animation_dict["has_spoiler"] == input_media_animation.has_spoiler def test_with_animation(self, animation): # noqa: F811 # fixture found in test_animation @@ -621,6 +633,22 @@ async def test_send_media_group_all_args(self, bot, raw_bot, chat_id, media_grou ) assert all(mes.has_protected_content for mes in messages) + @pytest.mark.flaky(3, 1) + async def test_send_media_group_with_spoiler( + self, bot, chat_id, photo_file, video_file # noqa: F811 + ): + # Media groups can't contain Animations, so that is tested in test_animation.py + media = [ + InputMediaPhoto(photo_file, has_spoiler=True), + InputMediaVideo(video_file, has_spoiler=True), + ] + messages = await bot.send_media_group(chat_id, media) + assert isinstance(messages, tuple) + assert len(messages) == 2 + assert all(isinstance(mes, Message) for mes in messages) + assert all(mes.media_group_id == messages[0].media_group_id for mes in messages) + assert all(mes.has_media_spoiler for mes in messages) + @pytest.mark.flaky(3, 1) async def test_send_media_group_custom_filename( self, diff --git a/tests/test_photo.py b/tests/test_photo.py index 4d7baeeec48..5238cf1c3b6 100644 --- a/tests/test_photo.py +++ b/tests/test_photo.py @@ -107,6 +107,7 @@ async def test_send_photo_all_args(self, bot, chat_id, photo_file, thumb, photo) disable_notification=False, protect_content=True, parse_mode="Markdown", + has_spoiler=True, ) assert isinstance(message.photo[-2], PhotoSize) @@ -123,6 +124,7 @@ async def test_send_photo_all_args(self, bot, chat_id, photo_file, thumb, photo) assert message.caption == TestPhoto.caption.replace("*", "") assert message.has_protected_content + assert message.has_media_spoiler @pytest.mark.flaky(3, 1) async def test_send_photo_custom_filename(self, bot, chat_id, photo_file, monkeypatch): diff --git a/tests/test_replykeyboardmarkup.py b/tests/test_replykeyboardmarkup.py index 56b1366e016..7f7a67159af 100644 --- a/tests/test_replykeyboardmarkup.py +++ b/tests/test_replykeyboardmarkup.py @@ -29,6 +29,7 @@ def reply_keyboard_markup(): resize_keyboard=TestReplyKeyboardMarkup.resize_keyboard, one_time_keyboard=TestReplyKeyboardMarkup.one_time_keyboard, selective=TestReplyKeyboardMarkup.selective, + is_persistent=TestReplyKeyboardMarkup.is_persistent, ) @@ -37,6 +38,7 @@ class TestReplyKeyboardMarkup: resize_keyboard = True one_time_keyboard = True selective = True + is_persistent = True def test_slot_behaviour(self, reply_keyboard_markup, mro_slots): inst = reply_keyboard_markup @@ -103,6 +105,7 @@ def test_expected_values(self, reply_keyboard_markup): assert reply_keyboard_markup.resize_keyboard == self.resize_keyboard assert reply_keyboard_markup.one_time_keyboard == self.one_time_keyboard assert reply_keyboard_markup.selective == self.selective + assert reply_keyboard_markup.is_persistent == self.is_persistent def test_wrong_keyboard_inputs(self): with pytest.raises(ValueError): @@ -134,6 +137,7 @@ def test_to_dict(self, reply_keyboard_markup): == reply_keyboard_markup.one_time_keyboard ) assert reply_keyboard_markup_dict["selective"] == reply_keyboard_markup.selective + assert reply_keyboard_markup_dict["is_persistent"] == reply_keyboard_markup.is_persistent def test_equality(self): a = ReplyKeyboardMarkup.from_column(["button1", "button2", "button3"]) diff --git a/tests/test_video.py b/tests/test_video.py index 80f68cd57bd..3c59c17ee09 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -105,6 +105,7 @@ async def test_send_all_args(self, bot, chat_id, video_file, video, thumb_file): height=video.height, parse_mode="Markdown", thumb=thumb_file, + has_spoiler=True, ) assert isinstance(message.video, Video) @@ -125,6 +126,7 @@ async def test_send_all_args(self, bot, chat_id, video_file, video, thumb_file): assert message.video.file_name == self.file_name assert message.has_protected_content + assert message.has_media_spoiler @pytest.mark.flaky(3, 1) async def test_send_video_custom_filename(self, bot, chat_id, video_file, monkeypatch): diff --git a/tests/test_writeaccessallowed.py b/tests/test_writeaccessallowed.py new file mode 100644 index 00000000000..f53079d8bbc --- /dev/null +++ b/tests/test_writeaccessallowed.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +from telegram import WriteAccessAllowed + + +class TestWriteAccessAllowed: + def test_slot_behaviour(self, mro_slots): + action = WriteAccessAllowed() + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + def test_de_json(self): + action = WriteAccessAllowed.de_json({}, None) + assert action.api_kwargs == {} + assert isinstance(action, WriteAccessAllowed) + + def test_to_dict(self): + action = WriteAccessAllowed() + action_dict = action.to_dict() + assert action_dict == {}