Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Serhii Dylda 2020-11-03 17:03:43 +01:00
commit 403008ac9e
40 changed files with 695 additions and 1869 deletions

110
.gitignore vendored
View File

@ -1,7 +1,3 @@
# Docs
/_build/
/docs/
# Generated code
/telethon/tl/functions/
/telethon/tl/types/
@ -11,102 +7,18 @@
# User session
*.session
usermedia/
/usermedia/
# Quick tests should live in this file
example.py
# Byte-compiled / optimized / DLL files
# Builds and testing
__pycache__/
*.py[cod]
*$py.class
/dist/
/build/
/*.egg-info/
/readthedocs/_build/
/.tox/
# C extensions
*.so
# API reference docs
/docs/
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
/docs/_build/
# PyBuilder
target/
# IPython Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# dotenv
.env
# virtualenv
.venv/
venv/
ENV/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject
# Nix build results
result
result-*
# File used to manually test new changes, contains sensitive data
/example.py

View File

@ -1,4 +0,0 @@
include LICENSE
include README.rst
recursive-include telethon *

View File

@ -1,126 +0,0 @@
# A NUR-compatible package specification.
{ pkgs ? import <nixpkgs> {}, useRelease ? true }:
rec {
# The `lib`, `modules`, and `overlay` names are special
lib = ({ pkgs }: { }) { inherit pkgs; }; # functions
modules = { }; # NixOS modules
overlays = { }; # nixpkgs overlays
# # development
# ## development.python-modules
# use in a shell like
# ```nix
# ((pkgs.python3.override {
# packageOverrides = pythonPackageOverrides;
# }).withPackages (ps: [ ps.telethon ])).env
# ```
pythonPackageOverrides = self: super: let
defaultTelethonArgs = { inherit useRelease; };
telethonPkg = v: args: self.callPackage (./nix/telethon + "/${v}.nix")
(defaultTelethonArgs // args);
in rec {
telethon = telethon_1;
telethon-devel = self.callPackage ./nix/telethon/devel.nix { };
telethon_1 = telethon_1_10;
telethon_1_10 = telethon_1_10_1;
telethon_1_10_1 = telethonPkg "1.10" { version = "1.10.1"; };
telethon_1_10_0 = telethonPkg "1.10" { version = "1.10.0"; };
telethon_1_9 = telethon_1_9_0;
telethon_1_9_0 = telethonPkg "1.9" { version = "1.9.0"; };
telethon_1_8 = telethon_1_8_0;
telethon_1_8_0 = telethonPkg "1.8" { version = "1.8.0"; };
telethon_1_7 = telethon_1_7_7;
telethon_1_7_7 = telethonPkg "1.7" { version = "1.7.7"; };
telethon_1_7_6 = telethonPkg "1.7" { version = "1.7.6"; };
telethon_1_7_5 = telethonPkg "1.7" { version = "1.7.5"; };
telethon_1_7_4 = telethonPkg "1.7" { version = "1.7.4"; };
telethon_1_7_3 = telethonPkg "1.7" { version = "1.7.3"; };
telethon_1_7_2 = telethonPkg "1.7" { version = "1.7.2"; };
telethon_1_7_1 = telethonPkg "1.7" { version = "1.7.1"; };
telethon_1_7_0 = telethonPkg "1.7" { version = "1.7.0"; };
telethon_1_6 = telethon_1_6_2;
telethon_1_6_2 = telethonPkg "1.6" { version = "1.6.2"; };
# 1.6.1.post1: hotpatch that fixed Telethon.egg-info dir perms
telethon_1_6_1 = telethonPkg "1.6" { version = "1.6.1"; };
telethon_1_6_0 = telethonPkg "1.6" { version = "1.6.0"; };
telethon_1_5 = telethon_1_5_5;
telethon_1_5_5 = telethonPkg "1.5" { version = "1.5.5"; };
telethon_1_5_4 = telethonPkg "1.5" { version = "1.5.4"; };
telethon_1_5_3 = telethonPkg "1.5" { version = "1.5.3"; };
telethon_1_5_2 = telethonPkg "1.5" { version = "1.5.2"; };
telethon_1_5_1 = telethonPkg "1.5" { version = "1.5.1"; };
telethon_1_5_0 = telethonPkg "1.5" { version = "1.5.0"; };
telethon_1_4 = telethon_1_4_3;
telethon_1_4_3 = telethonPkg "1.4" { version = "1.4.3"; };
telethon_1_4_2 = telethonPkg "1.4" { version = "1.4.2"; };
telethon_1_4_1 = telethonPkg "1.4" { version = "1.4.1"; };
telethon_1_4_0 = telethonPkg "1.4" { version = "1.4.0"; };
#telethon_1_3_0
#telethon_1_2_0
#telethon_1_1_1
#telethon_1_1_0
#telethon_1_0_4
#telethon_1_0_3
#telethon_1_0_2
#telethon_1_0_1
#telethon_1_0_0-rc1
#telethon_1_0_0
#telethon_0_19_1
#telethon_0_19_0
#telethon_0_18_3
#telethon_0_18_2
#telethon_0_18_1
#telethon_0_18_0
#telethon_0_17_4
#telethon_0_17_3
#telethon_0_17_2
#telethon_0_17_1
#telethon_0_17_0
#telethon_0_16_2
#telethon_0_16_1
#telethon_0_16_0
#telethon_0_15_5
#telethon_0_15_4
#telethon_0_15_3
#telethon_0_15_2
#telethon_0_15_1
#telethon_0_15_0
#telethon_0_14_2
#telethon_0_14_1
#telethon_0_14_0
#telethon_0_13_6
#telethon_0_13_5
#telethon_0_13_4
#telethon_0_13_3
#telethon_0_13_2
#telethon_0_13_1
#telethon_0_13_0
#telethon_0_12_2
#telethon_0_12_1
#telethon_0_12_0
#telethon_0_11_5
#telethon_0_11_4
#telethon_0_11_3
#telethon_0_11_2
#telethon_0_11_1
#telethon_0_11_0
#telethon_0_10_1
#telethon_0_10_0
#telethon_0_9_1
#telethon_0_9_0
#telethon_0_8_0
#telethon_0_7_1
#telethon_0_7_0
#telethon_0_6_0
#telethon_0_5_0
#telethon_0_4_0
#telethon_0_3_0
#telethon_0_2_0
#telethon_0_1_0
};
}

View File

@ -1,59 +0,0 @@
# This file provides all the buildable and cacheable packages and
# package outputs in you package set. These are what gets built by CI,
# so if you correctly mark packages as
#
# - broken (using `meta.broken`),
# - unfree (using `meta.license.free`), and
# - locally built (using `preferLocalBuild`)
#
# then your CI will be able to build and cache only those packages for
# which this is possible.
{ pkgs ? import <nixpkgs> {}, enableEnvs ? false }:
with builtins;
let
isReserved = n: n == "lib" || n == "overlays" || n == "modules";
isDerivation = p: isAttrs p && p ? type && p.type == "derivation";
isBuildable = p: !(p.meta.broken or false) && p.meta.license.free or true;
isCacheable = p: !(p.preferLocalBuild or false);
shouldRecurseForDerivations = p:
isAttrs p && p.recurseForDerivations or false;
nameValuePair = n: v: { name = n; value = v; };
concatMap = builtins.concatMap or (f: xs: concatLists (map f xs));
flattenPkgs = s:
let
f = p:
if shouldRecurseForDerivations p then flattenPkgs p
else if isDerivation p then [p]
else [];
in
concatMap f (attrValues s);
outputsOf = p: map (o: p.${o}) p.outputs;
# build & test packages across Python versions
# (withPackages "distributions" are also generated for testing)
nurAttrs = import ./extended.nix { inherit pkgs enableEnvs; };
nurPkgs =
flattenPkgs
(listToAttrs
(map (n: nameValuePair n nurAttrs.${n})
(filter (n: !isReserved n)
(attrNames nurAttrs))));
in
rec {
buildPkgs = filter isBuildable nurPkgs;
cachePkgs = filter isCacheable buildPkgs;
buildOutputs = concatMap outputsOf buildPkgs;
cacheOutputs = concatMap outputsOf cachePkgs;
}

View File

@ -1,86 +0,0 @@
{ pkgs ? import <nixpkgs> { }, enableEnvs ? true, useRelease ? true }:
# packages built against all Python versions (along with withPackages
# environments for testing)
# to use for testing, you'll probably want a variant of:
# ```sh
# nix-shell nix/extended.nix -A telethon-devel-python37 --run "python"
# ```
let
inherit (pkgs.lib) attrNames attrValues concatMap head listToAttrs
mapAttrsToList optional optionals tail;
nurAttrs = import ../default.nix { inherit pkgs useRelease; };
pyVersions = concatMap (n: optional (pkgs ? ${n}) n) [
"python3"
"python35"
"python36"
"python37"
# "pypy3"
# "pypy35"
# "pypy36"
# "pypy37"
];
pyPkgEnvs = [
[ "telethon" "telethon" ]
[ "telethon-devel" "telethon-devel" ]
[ "telethon_1" "telethon_1" ]
[ "telethon_1_10" "telethon_1_10" ]
[ "telethon_1_10_1" "telethon_1_10_1" ]
[ "telethon_1_10_0" "telethon_1_10_0" ]
[ "telethon_1_9" "telethon_1_9" ]
[ "telethon_1_9_0" "telethon_1_9_0" ]
[ "telethon_1_8" "telethon_1_8" ]
[ "telethon_1_8_0" "telethon_1_8_0" ]
[ "telethon_1_7" "telethon_1_7" ]
[ "telethon_1_7_7" "telethon_1_7_7" ]
[ "telethon_1_7_6" "telethon_1_7_6" ]
[ "telethon_1_7_5" "telethon_1_7_5" ]
[ "telethon_1_7_4" "telethon_1_7_4" ]
[ "telethon_1_7_3" "telethon_1_7_3" ]
[ "telethon_1_7_2" "telethon_1_7_2" ]
[ "telethon_1_7_1" "telethon_1_7_1" ]
[ "telethon_1_7_0" "telethon_1_7_0" ]
[ "telethon_1_6" "telethon_1_6" ]
[ "telethon_1_6_2" "telethon_1_6_2" ]
[ "telethon_1_6_1" "telethon_1_6_1" ]
[ "telethon_1_6_0" "telethon_1_6_0" ]
[ "telethon_1_5" "telethon_1_5" ]
[ "telethon_1_5_5" "telethon_1_5_5" ]
[ "telethon_1_5_4" "telethon_1_5_4" ]
[ "telethon_1_5_3" "telethon_1_5_3" ]
[ "telethon_1_5_2" "telethon_1_5_2" ]
[ "telethon_1_5_1" "telethon_1_5_1" ]
[ "telethon_1_5_0" "telethon_1_5_0" ]
[ "telethon_1_4" "telethon_1_4" ]
[ "telethon_1_4_3" "telethon_1_4_3" ]
# [ "telethon_1_4_2" "telethon_1_4_2" ]
# [ "telethon_1_4_1" "telethon_1_4_1" ]
# [ "telethon_1_4_0" "telethon_1_4_0" ]
];
getPkgPair = pkgs: n: let p = pkgs.${n}; in { name = n; value = p; };
getPkgPairs = pkgs: map (getPkgPair pkgs);
pyPkgPairs = py:
concatMap (d: map (getPkgPair py.pkgs) (tail d)) pyPkgEnvs;
pyPkgEnvPair = pyNm: py: envNm: env: {
name = "${envNm}-env-${pyNm}";
value = (py.withPackages (ps: map (pn: ps.${pn}) env)).overrideAttrs (o: {
name = "${envNm}-${py.name}-env";
preferLocalBuild = true;
});
};
pyNurPairs = pyNm: py:
map ({ name, value }: { name = "${name}-${pyNm}"; inherit value; })
(pyPkgPairs py) ++
optionals enableEnvs
(map (d: pyPkgEnvPair pyNm py (head d) (tail d)) pyPkgEnvs);
in nurAttrs // (listToAttrs (concatMap (py: let
python = pkgs.${py}.override {
packageOverrides = nurAttrs.pythonPackageOverrides;
}; in
pyNurPairs py python) pyVersions))

View File

@ -1,18 +0,0 @@
# You can use this file as a nixpkgs overlay. This is useful in the
# case where you don't want to add the whole NUR namespace to your
# configuration.
self: super:
let
isReserved = n: n == "lib" || n == "overlays" || n == "modules";
nameValuePair = n: v: { name = n; value = v; };
nurAttrs = import ./default.nix { pkgs = super; };
in
builtins.listToAttrs
(map (n: nameValuePair n nurAttrs.${n})
(builtins.filter (n: !isReserved n)
(builtins.attrNames nurAttrs)))

View File

@ -1,39 +0,0 @@
{ lib, buildPythonPackage, pythonOlder
, fetchFromGitHub ? null, fetchPypi ? null, fetchpatch ? null
, pyaes, rsa
, version
, useRelease ? true
}:
assert useRelease -> fetchPypi != null;
assert !useRelease -> fetchFromGitHub != null;
let
common = import ./common.nix {
inherit lib fetchFromGitHub fetchPypi fetchpatch;
};
versions = {
"1.10.1" = {
pypiSha256 = "1ql8ai01c6v3l13lh3csh37jjkrb33gj50jyvdfi3qjn60qs2rfl";
sourceSha256 = "1skckq4lai51p476r3shgld89x5yg5snrcrzjfxxxai00lm65cbv";
};
"1.10.0" = {
pypiSha256 = "1n2g2r5w44nlhn229r8kamhwjxggv16gl3jxq25bpg5y4qgrxzd8";
sourceSha256 = "1rvrc63j6i7yr887g2csciv4zyy407yhdn4n8q2q00dkildh64qw";
};
};
in buildPythonPackage rec {
pname = "telethon";
inherit version;
src = common.fetchTelethon {
inherit useRelease version;
versionData = versions.${version};
};
propagatedBuildInputs = [ rsa pyaes ];
doCheck = false; # No tests available
disabled = pythonOlder "3.5";
meta = common.meta;
}

View File

@ -1,56 +0,0 @@
{ lib, buildPythonPackage, pythonOlder
, fetchFromGitHub ? null, fetchPypi ? null, fetchpatch ? null
, async_generator, pyaes, rsa
, version
, useRelease ? true
}:
assert useRelease -> fetchPypi != null;
assert !useRelease -> fetchFromGitHub != null && fetchpatch != null;
let
common = import ./common.nix {
inherit lib fetchFromGitHub fetchPypi fetchpatch;
};
versions = {
"1.4.3" = {
pypiSha256 = "1igslvhd743qy9p4kfs7lg09s8d5vhn9jhzngpv12797569p4lcj";
sourceSha256 = "19vz0ppk7lq1dmqzf47n6h023i08pqvcwnixvm28vrijykq0z315";
};
"1.4.2" = {
pypiSha256 = "1f4ncyfzqj4b6zib0417r01pgnd0hb1p4aiinhlkxkmk7vy5fqfy";
sourceSha256 = "0rsbz5kqp0d10gasadir3mgalc9aqq4fcv8xa1p7fg263f43rjl4";
};
"1.4.1" = {
pypiSha256 = "1n0jhdqflinyamzy5krnww7hc0s7pw9yfck1p7816pdbgir74qsw";
sourceSha256 = "07q48gw4ry3wf9yzi6kf8lw3b23a0dvk9r8sabpxwrlqy7gnksxx";
};
"1.4.0" = {
version = "1.4";
pypiSha256 = "1g7rznwmj87n9k86zby9i75h570hm84izrv0srhsmxi52pjan1ml";
sourceSha256 = "14nv86yrj01wmlj5cfg6iq5w03ssl67av1arfy9mq1935mly5nly";
};
};
in buildPythonPackage rec {
pname = "telethon";
inherit version;
src = common.fetchTelethon {
inherit useRelease version;
versionData = versions.${version};
};
patches = lib.optionals (!useRelease) [
(if (lib.versionOlder version "1.4.3") then
common.patches.generator-use-pathlib-to-1_4_3
else
common.patches.generator-use-pathlib-from-1_4_3-to-1_5_0)
common.patches.generator-use-pathlib-open-to-1_5_3
common.patches.sort-generated-tlobjects-to-1_7_1
];
propagatedBuildInputs = [ async_generator rsa pyaes ];
doCheck = false; # No tests available
disabled = pythonOlder "3.5";
meta = common.meta;
}

View File

@ -1,60 +0,0 @@
{ lib, buildPythonPackage, pythonOlder
, fetchFromGitHub ? null, fetchPypi ? null, fetchpatch ? null
, async_generator, pyaes, rsa
, version
, useRelease ? true
}:
assert useRelease -> fetchPypi != null;
assert !useRelease -> fetchFromGitHub != null && fetchpatch != null;
let
common = import ./common.nix {
inherit lib fetchFromGitHub fetchPypi fetchpatch;
};
versions = {
"1.5.5" = {
pypiSha256 = "1qpc4vc3lidhlp1c7521nxizjr6y5c3l9x41knqv02x8n3l9knxa";
sourceSha256 = "1x5niscjbrg5a0cg261z6awln57v3nn8si5j58vhsnckws2c48a5";
};
"1.5.4" = {
pypiSha256 = "1kjqi3wy4hswsf3vmrjg7z5c3f9wpdfk4wz1yfsqmj9ppwllkjsj";
sourceSha256 = "0rmp9zk7a354nb39c01mjcrhi2j6v9im40xmdcvmizx990vlv476";
};
"1.5.3" = {
pypiSha256 = "11xd5ni0chzsfny0vwwqyh37mvmrwrk2bmkhwp1ipbxyis8jjjia";
sourceSha256 = "1l3i6wx3fgcy3vmr75qdbv5fvc5qnk0j47hv7jszsqq9rvqvz2xs";
};
"1.5.2" = {
pypiSha256 = "0ymv6l9xn41sgpkilqkivwbjna89m43i0a728lak2cppp7i1i1h7";
sourceSha256 = "0gnqvlhh3qyvibl7icn6774rshlx1nnhb5f78609da44743lyv17";
};
"1.5.1" = {
pypiSha256 = "1ypxpsfj814gzln4fl7z17l1l6q0bzd5p1ivas85yim3a992ixww";
sourceSha256 = "15w5nshvmj8hgqdcbpw0fjcf1cspaci8dldm9ml1pmijw7zgmpdg";
};
"1.5.0" = {
version = "1.5";
pypiSha256 = "1kzkzcxyz7adjzvm2ml9faz2c5yx469j211yvi5xfvjwp58ic2jc";
sourceSha256 = "12232d3xfv0bbykk9xaxpxsr3656ywjx4ra1q5q99rpp6wv438n1";
};
};
in buildPythonPackage rec {
pname = "telethon";
inherit version;
src = common.fetchTelethon {
inherit useRelease version;
versionData = versions.${version};
};
patches = lib.optionals (!useRelease) ([
common.patches.sort-generated-tlobjects-to-1_7_1
] ++ lib.optional (lib.versionOlder version "1.5.3")
common.patches.generator-use-pathlib-open-to-1_5_3);
propagatedBuildInputs = [ async_generator rsa pyaes ];
doCheck = false; # No tests available
disabled = pythonOlder "3.5";
meta = common.meta;
}

View File

@ -1,50 +0,0 @@
{ lib, buildPythonPackage, pythonOlder
, fetchFromGitHub ? null, fetchPypi ? null, fetchpatch ? null
, pyaes, rsa
, version
, useRelease ? true
}:
assert useRelease -> fetchPypi != null;
assert !useRelease -> fetchFromGitHub != null && fetchpatch != null;
let
common = import ./common.nix {
inherit lib fetchFromGitHub fetchPypi fetchpatch;
};
versions = {
"1.6.2" = {
pypiSha256 = "074h5gj0c330rb1nxzpqm31fp1vw7calh1cdkapbjx90j769iz18";
sourceSha256 = "1daqlb4sva5qkljzbjr8xvjfgp7bdcrl2li1i4434za6a0isgd3j";
};
"1.6.1" = {
# hotpatch with missing .pyc files and fixed Telethon.egg-info perms
pypiVersion = "1.6.1.post1";
pypiSha256 = "17s1qp69bbj6jniam9wbcpaj60ah56sjw0q3kr8ca28y17s88si7";
# pypiVersion = "1.6.1";
# pypiSha256 = "036lhr1jr79np74c6ih51c4pjy828r3lvwcq07q5wynyjprm1qbz";
sourceSha256 = "1hk1bpnk51rpsifb67s31c2qph5hmw28i2vgh97i4i56vynx2yxz";
};
"1.6.0" = {
version = "1.6";
pypiSha256 = "06prmld9068zcm9rfmq3rpq1szw72c6dkxl62b035i9w8wdpvg0m";
sourceSha256 = "0qk14mrnvv9a043ik0y2w6q97l83abvbvn441zn2jl00w4ykfqrh";
};
};
in buildPythonPackage rec {
pname = "telethon";
inherit version;
src = common.fetchTelethon {
inherit useRelease version;
versionData = versions.${version};
};
patches = lib.optional (!useRelease)
common.patches.sort-generated-tlobjects-to-1_7_1;
propagatedBuildInputs = [ rsa pyaes ];
doCheck = false; # No tests available
disabled = pythonOlder "3.5";
meta = common.meta;
}

View File

@ -1,66 +0,0 @@
{ lib, buildPythonPackage, pythonOlder
, fetchFromGitHub ? null, fetchPypi ? null, fetchpatch ? null
, pyaes, rsa
, version
, useRelease ? true
}:
assert useRelease -> fetchPypi != null;
assert !useRelease -> fetchFromGitHub != null;
let
common = import ./common.nix {
inherit lib fetchFromGitHub fetchPypi fetchpatch;
};
versions = {
"1.7.7" = {
pypiSha256 = "0mgpihjc7g4gfrq57srripdavxbsgivn4qsjanv3yds5drskciv0";
sourceSha256 = "08c3iakd7fyacc79pg8hyzpa6zx3gbp7xivi10af34zj775lp2pi";
};
"1.7.6" = {
pypiSha256 = "192xda98685s3hmz7ircxpsn7yq913y0r1kmqrsav90m4g4djn4j";
sourceSha256 = "1ss2pfpd3hby25g9ighbr7ccp66awfzda4srsnvr9s6i28har6ag";
};
"1.7.5" = {
pypiSha256 = "0i5s7ahicw5k0s1i7pi26vc6rp6ppr1gr848sa61yh3qqa4c0qnr";
sourceSha256 = "1rssh0l466h9y6v0z095c9aa63nz9im7gg5771jjj5w70mkpm5w6";
};
"1.7.4" = {
pypiSha256 = "1qpc9f1y559zdwz59qqz4hbf1mrynjjbcg357nzaa2x5a2q4lz0s";
sourceSha256 = "1q43lwfp67q4skfcrb6sdlnjw4ajrpizf08fd9wjrw521kkd8g4y";
};
"1.7.3" = {
pypiSha256 = "0s8qmsarlfgpb0k3w50siv354hpa7b1dnrjjd0iqz7vc5bc7ni84";
sourceSha256 = "0c393smp1qm8kk39r0k31p74p89qzvjdjxq4bxq75h07a1yqbs8x";
};
"1.7.2" = {
pypiSha256 = "0465dwikhpbka2sj1g952rac03jkixq497gbmmyx2i9xb594db27";
sourceSha256 = "1gw09zbaqvn074skwjhmm4yp8p75rw9njwjbkcfvqb4gr6dg8wpq";
};
"1.7.1" = {
pypiSha256 = "186z6imf7zqy8vf4yv2w2kxpd7lxmfppa1qi8nxjdgq8rz7wbglf";
sourceSha256 = "05mpqfj4w5qxyl1ai5p0f31pkagz55xxh8060r8y9i3d44j9bn1c";
};
"1.7.0" = {
version = "1.7";
pypiSha256 = "06cqb121k2y0h3x7gvckyvbsn97wc1a25pghinxz2vb7vg8wwxvw";
sourceSha256 = "0myx32hqax71ijfw6ksxvk27cb6x06kbz8jb7ib9d1cayr2viir6";
};
};
in buildPythonPackage rec {
pname = "telethon";
inherit version;
src = common.fetchTelethon {
inherit useRelease version;
versionData = versions.${version};
};
patches = lib.optional (!useRelease && lib.versionOlder version "1.7.1")
common.patches.sort-generated-tlobjects-to-1_7_1;
propagatedBuildInputs = [ rsa pyaes ];
doCheck = false; # No tests available
disabled = pythonOlder "3.5";
meta = common.meta;
}

View File

@ -1,35 +0,0 @@
{ lib, buildPythonPackage, pythonOlder
, fetchFromGitHub ? null, fetchPypi ? null, fetchpatch ? null
, pyaes, rsa
, version
, useRelease ? true
}:
assert useRelease -> fetchPypi != null;
assert !useRelease -> fetchFromGitHub != null;
let
common = import ./common.nix {
inherit lib fetchFromGitHub fetchPypi fetchpatch;
};
versions = {
"1.8.0" = {
pypiSha256 = "099br8ldjrfzwipv7g202lnjghmqj79j6gicgx11s0vawb5mb3vf";
sourceSha256 = "1q5mcijmjw2m2v3ilw28xnavmcdck5md0k98kwnz0kyx4iqckcv0";
};
};
in buildPythonPackage rec {
pname = "telethon";
inherit version;
src = common.fetchTelethon {
inherit useRelease version;
versionData = versions.${version};
};
propagatedBuildInputs = [ rsa pyaes ];
doCheck = false; # No tests available
disabled = pythonOlder "3.5";
meta = common.meta;
}

View File

@ -1,35 +0,0 @@
{ lib, buildPythonPackage, pythonOlder
, fetchFromGitHub ? null, fetchPypi ? null, fetchpatch ? null
, pyaes, rsa
, version
, useRelease ? true
}:
assert useRelease -> fetchPypi != null;
assert !useRelease -> fetchFromGitHub != null;
let
common = import ./common.nix {
inherit lib fetchFromGitHub fetchPypi fetchpatch;
};
versions = {
"1.9.0" = {
pypiSha256 = "1p4y4qd1ndzi1lg4fhnvq1rqz7611yrwnwwvzh63aazfpzaplyd8";
sourceSha256 = "1g6khxc7mvm3q8rqksw9dwn4l2w8wzvr3zb74n2lb7g5ilpxsadd";
};
};
in buildPythonPackage rec {
pname = "telethon";
inherit version;
src = common.fetchTelethon {
inherit useRelease version;
versionData = versions.${version};
};
propagatedBuildInputs = [ rsa pyaes ];
doCheck = false; # No tests available
disabled = pythonOlder "3.5";
meta = common.meta;
}

View File

@ -1,60 +0,0 @@
{ lib, fetchFromGitHub ? null, fetchPypi ? null, fetchpatch ? null }:
rec {
fetchTelethon = { useRelease, version, versionData }:
if useRelease then assert versionData.pypiSha256 != null; fetchPypi {
pname = "Telethon";
version = versionData.pypiVersion or (versionData.version or version);
sha256 = versionData.pypiSha256;
} else assert versionData.sourceSha256 != null; fetchFromGitHub {
owner = "LonamiWebs";
repo = "Telethon";
rev = versionData.rev or "v${versionData.version or version}";
sha256 = versionData.sourceSha256;
};
fetchpatchTelethon = { rev, ... } @ args:
fetchpatch ({
url = "https://github.com/LonamiWebs/Telethon/commit/${rev}.patch";
} // (builtins.removeAttrs args [ "rev" ]));
# sorted by name, then by logical version range
patches = rec {
generator-use-pathlib-to-1_4_3 = ./generator-use-pathlib-to-1_4_3.patch;
generator-use-pathlib-from-1_4_3-to-1_5_0 = [
(fetchpatchTelethon {
rev = "e71c556ca71aec11166dc66f949a05e700aeb24f";
sha256 = "058phfaggf22j0cjpy9j17y63zgd9m8j4qf7ldsg0jqm1vrym76w";
})
(fetchpatchTelethon {
rev = "8224e5aabf18bb31c6af8c460c38ced11756f080";
sha256 = "0x3xfkld4d2kc0a1a8ldxy85pi57zaipq3b401b16r6rzbi4sh1j";
})
(fetchpatchTelethon {
rev = "aefa429236d28ae68bec4e4ef9f12d13f647dfe6";
sha256 = "043hks8hg5sli1amfv5453h831nwy4dgyw8xr4xxfaxh74754icx";
})
];
generator-use-pathlib-open-to-1_5_3 = fetchpatchTelethon {
rev = "b57e3e3e0a752903fe7d539fb87787ec6712a3d9";
sha256 = "1rl3lkwfi3h62ppzglrmz13zfai8i8cchzqgbjccr4l7nzh1n6nq";
};
sort-generated-tlobjects-to-1_7_1 = fetchpatchTelethon {
rev = "08f8aa3c526c043c107ec1b489b89c011555722f";
sha256 = "1lkvvjzhm9jfrxpm4hbvvysz5f3qi0v4f7vqnfmrzawl73s8qk80";
};
};
meta = let inherit (lib) licenses maintainers; in {
description = "Full-featured Telegram client library for Python 3";
fullDescription = ''
Telegram is a popular messaging application. This library is meant to
make it easy for you to write Python programs that can interact with
Telegram. Think of it as a wrapper that has already done the heavy job
for you, so you can focus on developing an application.
'';
homepage = https://github.com/LonamiWebs/Telethon;
license = licenses.mit;
maintainers = [ maintainers.bb010g maintainers.nyanloutre ];
};
}

View File

@ -1,27 +0,0 @@
{ lib, buildPythonPackage, nix-gitignore, pythonOlder
, async_generator, pyaes, rsa
}:
let
common = import ./common.nix { inherit lib; };
in buildPythonPackage rec {
pname = "telethon";
# If pinning to a specific commit, use the following output instead:
# ```sh
# TZ=UTC git show -s --format=format:%cd --date=short-local
# ```
version = "HEAD";
src = nix-gitignore.gitignoreSource ''
/.git
/default.nix
/nix
'' ../..;
propagatedBuildInputs = [ async_generator rsa pyaes ];
doCheck = false; # No tests available
disabled = pythonOlder "3.5";
meta = common.meta;
}

View File

@ -1,819 +0,0 @@
--- a/setup.py
+++ b/setup.py
@@ -12,10 +12,11 @@
import itertools
import json
-import os
import re
import shutil
-from codecs import open
+from os import chdir
+from pathlib import Path
+from subprocess import run
from sys import argv
from setuptools import find_packages, setup
@@ -29,30 +30,29 @@
self.original = None
def __enter__(self):
- self.original = os.path.abspath(os.path.curdir)
- os.chdir(os.path.abspath(os.path.dirname(__file__)))
+ self.original = Path('.')
+ chdir(str(Path(__file__).parent))
return self
def __exit__(self, *args):
- os.chdir(self.original)
+ chdir(str(self.original))
-GENERATOR_DIR = 'telethon_generator'
-LIBRARY_DIR = 'telethon'
+GENERATOR_DIR = Path('telethon_generator')
+LIBRARY_DIR = Path('telethon')
-ERRORS_IN_JSON = os.path.join(GENERATOR_DIR, 'data', 'errors.json')
-ERRORS_IN_DESC = os.path.join(GENERATOR_DIR, 'data', 'error_descriptions')
-ERRORS_OUT = os.path.join(LIBRARY_DIR, 'errors', 'rpcerrorlist.py')
+ERRORS_IN_JSON = GENERATOR_DIR / 'data/errors.json'
+ERRORS_IN_DESC = GENERATOR_DIR / 'data/error_descriptions'
+ERRORS_OUT = LIBRARY_DIR / 'errors/rpcerrorlist.py'
-INVALID_BM_IN = os.path.join(GENERATOR_DIR, 'data', 'invalid_bot_methods.json')
+INVALID_BM_IN = GENERATOR_DIR / 'data/invalid_bot_methods.json'
-TLOBJECT_IN_CORE_TL = os.path.join(GENERATOR_DIR, 'data', 'mtproto_api.tl')
-TLOBJECT_IN_TL = os.path.join(GENERATOR_DIR, 'data', 'telegram_api.tl')
-TLOBJECT_OUT = os.path.join(LIBRARY_DIR, 'tl')
+TLOBJECT_IN_TLS = [Path(x) for x in GENERATOR_DIR.glob('data/*.tl')]
+TLOBJECT_OUT = LIBRARY_DIR / 'tl'
IMPORT_DEPTH = 2
-DOCS_IN_RES = os.path.join(GENERATOR_DIR, 'data', 'html')
-DOCS_OUT = 'docs'
+DOCS_IN_RES = GENERATOR_DIR / 'data/html'
+DOCS_OUT = Path('docs')
def generate(which):
@@ -60,15 +60,12 @@
from telethon_generator.generators import\
generate_errors, generate_tlobjects, generate_docs, clean_tlobjects
- # Older Python versions open the file as bytes instead (3.4.2)
- with open(INVALID_BM_IN, 'r') as f:
+ with INVALID_BM_IN.open('r') as f:
invalid_bot_methods = set(json.load(f))
-
- layer = find_layer(TLOBJECT_IN_TL)
+ layer = next(filter(None, map(find_layer, TLOBJECT_IN_TLS)))
errors = list(parse_errors(ERRORS_IN_JSON, ERRORS_IN_DESC))
- tlobjects = list(itertools.chain(
- parse_tl(TLOBJECT_IN_CORE_TL, layer, invalid_bot_methods),
- parse_tl(TLOBJECT_IN_TL, layer, invalid_bot_methods)))
+ tlobjects = list(itertools.chain(*(
+ parse_tl(file, layer, invalid_bot_methods) for file in TLOBJECT_IN_TLS)))
if not which:
which.extend(('tl', 'errors'))
@@ -96,30 +93,29 @@
which.remove('errors')
print(action, 'RPCErrors...')
if clean:
- if os.path.isfile(ERRORS_OUT):
- os.remove(ERRORS_OUT)
+ if ERRORS_OUT.is_file():
+ ERRORS_OUT.unlink()
else:
- with open(ERRORS_OUT, 'w', encoding='utf-8') as file:
+ with ERRORS_OUT.open('w') as file:
generate_errors(errors, file)
if 'docs' in which:
which.remove('docs')
print(action, 'documentation...')
if clean:
- if os.path.isdir(DOCS_OUT):
- shutil.rmtree(DOCS_OUT)
+ if DOCS_OUT.is_dir():
+ shutil.rmtree(str(DOCS_OUT))
else:
generate_docs(tlobjects, methods, layer, DOCS_IN_RES, DOCS_OUT)
if 'json' in which:
which.remove('json')
print(action, 'JSON schema...')
- mtproto = 'mtproto_api.json'
- telegram = 'telegram_api.json'
+ json_files = [x.with_suffix('.json') for x in TLOBJECT_IN_TLS]
if clean:
- for x in (mtproto, telegram):
- if os.path.isfile(x):
- os.remove(x)
+ for file in json_files:
+ if file.is_file():
+ file.unlink()
else:
def gen_json(fin, fout):
methods = []
@@ -131,8 +130,8 @@
with open(fout, 'w') as f:
json.dump(what, f, indent=2)
- gen_json(TLOBJECT_IN_CORE_TL, mtproto)
- gen_json(TLOBJECT_IN_TL, telegram)
+ for fin, fout in zip(TLOBJECT_IN_TLS, json_files):
+ gen_json(fin, fout)
if which:
print('The following items were not understood:', which)
@@ -156,22 +155,17 @@
print('Packaging for PyPi aborted, importing the module failed.')
return
- # Need python3.5 or higher, but Telethon is supposed to support 3.x
- # Place it here since noone should be running ./setup.py pypi anyway
- from subprocess import run
- from shutil import rmtree
-
for x in ('build', 'dist', 'Telethon.egg-info'):
- rmtree(x, ignore_errors=True)
+ shutil.rmtree(x, ignore_errors=True)
run('python3 setup.py sdist', shell=True)
run('python3 setup.py bdist_wheel', shell=True)
run('twine upload dist/*', shell=True)
for x in ('build', 'dist', 'Telethon.egg-info'):
- rmtree(x, ignore_errors=True)
+ shutil.rmtree(x, ignore_errors=True)
else:
# e.g. install from GitHub
- if os.path.isdir(GENERATOR_DIR):
+ if GENERATOR_DIR.is_dir():
generate(['tl', 'errors'])
# Get the long description from the README file
--- a/telethon_generator/docswriter.py
+++ b/telethon_generator/docswriter.py
@@ -2,0 +2,0 @@
class DocsWriter:
- """Utility class used to write the HTML files used on the documentation"""
- def __init__(self, filename, type_to_path):
- """Initializes the writer to the specified output file,
- creating the parent directories when used if required.
-
- 'type_to_path_function' should be a function which, given a type
- name and a named argument relative_to, returns the file path for
- the specified type, relative to the given filename
+ """
+ Utility class used to write the HTML files used on the documentation.
+ """
+ def __init__(self, root, filename, type_to_path):
"""
+ Initializes the writer to the specified output file,
+ creating the parent directories when used if required.
+ """
+ self.root = root
self.filename = filename
+ self._parent = str(self.filename.parent)
self.handle = None
+ self.title = ''
# Should be set before calling adding items to the menu
self.menu_separator_tag = None
- # Utility functions TODO There must be a better way
- self.type_to_path = lambda t: type_to_path(
- t, relative_to=self.filename
- )
+ # Utility functions
+ self.type_to_path = lambda t: self._rel(type_to_path(t))
# Control signals
self.menu_began = False
@@ -30,11 +30,20 @@
self.write_copy_script = False
self._script = ''
+ def _rel(self, path):
+ """
+ Get the relative path for the given path from the current
+ file by working around https://bugs.python.org/issue20012.
+ """
+ return os.path.relpath(str(path), self._parent)
+
# High level writing
- def write_head(self, title, relative_css_path, default_css):
+ def write_head(self, title, css_path, default_css):
"""Writes the head part for the generated document,
with the given title and CSS
"""
+ #
+ self.title = title
self.write(
'''<!DOCTYPE html>
<html>
@@ -54,17 +63,17 @@
<body>
<div id="main_div">''',
title=title,
- rel_css=relative_css_path.rstrip('/'),
+ rel_css=self._rel(css_path),
def_css=default_css
)
- def set_menu_separator(self, relative_image_path):
+ def set_menu_separator(self, img):
"""Sets the menu separator.
Must be called before adding entries to the menu
"""
- if relative_image_path:
- self.menu_separator_tag = \
- '<img src="{}" alt="/" />'.format(relative_image_path)
+ if img:
+ self.menu_separator_tag = '<img src="{}" alt="/" />'.format(
+ self._rel(img))
else:
self.menu_separator_tag = None
@@ -80,7 +89,7 @@
self.write('<li>')
if link:
- self.write('<a href="{}">', link)
+ self.write('<a href="{}">', self._rel(link))
# Write the real menu entry text
self.write(name)
@@ -210,7 +219,7 @@
if bold:
self.write('<b>')
if link:
- self.write('<a href="{}">', link)
+ self.write('<a href="{}">', self._rel(link))
# Finally write the real table data, the given text
self.write(text)
@@ -278,10 +287,7 @@
# With block
def __enter__(self):
# Sanity check
- parent = os.path.dirname(self.filename)
- if parent:
- os.makedirs(parent, exist_ok=True)
-
+ self.filename.parent.mkdir(parents=True, exist_ok=True)
self.handle = open(self.filename, 'w', encoding='utf-8')
return self
--- a/telethon_generator/generators/docs.py
+++ b/telethon_generator/generators/docs.py
@@ -1,7 +1,6 @@
#!/usr/bin/env python3
-import csv
import functools
-import os
import re
import shutil
from collections import defaultdict
+from pathlib import Path
from ..docswriter import DocsWriter
from ..parsers import TLObject, Usability
@@ -35,41 +34,33 @@
def _get_create_path_for(root, tlobject, make=True):
"""Creates and returns the path for the given TLObject at root."""
- out_dir = 'methods' if tlobject.is_function else 'constructors'
+ # TODO Can we pre-create all required directories?
+ out_dir = root / ('methods' if tlobject.is_function else 'constructors')
if tlobject.namespace:
- out_dir = os.path.join(out_dir, tlobject.namespace)
+ out_dir /= tlobject.namespace
- out_dir = os.path.join(root, out_dir)
if make:
- os.makedirs(out_dir, exist_ok=True)
- return os.path.join(out_dir, _get_file_name(tlobject))
+ out_dir.mkdir(parents=True, exist_ok=True)
+ return out_dir / _get_file_name(tlobject)
-def _get_path_for_type(root, type_, relative_to='.'):
+
+def _get_path_for_type(type_):
"""Similar to `_get_create_path_for` but for only type names."""
if type_.lower() in CORE_TYPES:
- path = 'index.html#%s' % type_.lower()
+ return Path('index.html#%s' % type_.lower())
elif '.' in type_:
namespace, name = type_.split('.')
- path = 'types/%s/%s' % (namespace, _get_file_name(name))
+ return Path('types', namespace, _get_file_name(name))
else:
- path = 'types/%s' % _get_file_name(type_)
-
- return _get_relative_path(os.path.join(root, path), relative_to)
-
-
-def _get_relative_path(destination, relative_to, folder=False):
- """Return the relative path to destination from relative_to."""
- if not folder:
- relative_to = os.path.dirname(relative_to)
-
- return os.path.relpath(destination, start=relative_to)
+ return Path('types', _get_file_name(type_))
def _find_title(html_file):
"""Finds the <title> for the given HTML file, or (Unknown)."""
- with open(html_file, 'r') as fp:
- for line in fp:
+ # TODO Is it necessary to read files like this?
+ with html_file.open() as f:
+ for line in f:
if '<title>' in line:
# + 7 to skip len('<title>')
return line[line.index('<title>') + 7:line.index('</title>')]
@@ -77,25 +68,27 @@
return '(Unknown)'
-def _build_menu(docs, filename, root, relative_main_index):
- """Builds the menu using the given DocumentWriter up to 'filename',
- which must be a file (it cannot be a directory)"""
- filename = _get_relative_path(filename, root)
- docs.add_menu('API', relative_main_index)
-
- items = filename.split('/')
- for i in range(len(items) - 1):
- item = items[i]
- link = '../' * (len(items) - (i + 2))
- link += 'index.html'
- docs.add_menu(item.title(), link=link)
+def _build_menu(docs):
+ """
+ Builds the menu used for the current ``DocumentWriter``.
+ """
+
+ paths = []
+ current = docs.filename
+ while current != docs.root:
+ current = current.parent
+ paths.append(current)
+
+ for path in reversed(paths):
+ docs.add_menu(path.stem.title(), link=path / 'index.html')
+
+ if docs.filename.stem != 'index':
+ docs.add_menu(docs.title, link=docs.filename)
- if items[-1] != 'index.html':
- docs.add_menu(os.path.splitext(items[-1])[0])
docs.end_menu()
-def _generate_index(folder, original_paths, root,
+def _generate_index(root, folder, paths,
bots_index=False, bots_index_paths=()):
"""Generates the index file for the specified folder"""
# Determine the namespaces listed here (as sub folders)
@@ -105,38 +98,24 @@
INDEX = 'index.html'
BOT_INDEX = 'botindex.html'
- if not bots_index:
- for item in os.listdir(folder):
- if os.path.isdir(os.path.join(folder, item)):
- namespaces.append(item)
- elif item not in (INDEX, BOT_INDEX):
- files.append(item)
- else:
- # bots_index_paths should be a list of "namespace/method.html"
- # or "method.html"
- for item in bots_index_paths:
- dirname = os.path.dirname(item)
- if dirname and dirname not in namespaces:
- namespaces.append(dirname)
- elif not dirname and item not in (INDEX, BOT_INDEX):
- files.append(item)
-
- paths = {k: _get_relative_path(v, folder, folder=True)
- for k, v in original_paths.items()}
+ for item in (bots_index_paths or folder.iterdir()):
+ if item.is_dir():
+ namespaces.append(item)
+ elif item.name not in (INDEX, BOT_INDEX):
+ files.append(item)
# Now that everything is setup, write the index.html file
- filename = os.path.join(folder, BOT_INDEX if bots_index else INDEX)
- with DocsWriter(filename, type_to_path=_get_path_for_type) as docs:
+ filename = folder / (BOT_INDEX if bots_index else INDEX)
+ with DocsWriter(root, filename, _get_path_for_type) as docs:
# Title should be the current folder name
- docs.write_head(folder.title(),
- relative_css_path=paths['css'],
- default_css=original_paths['default_css'])
+ docs.write_head(str(folder).title(),
+ css_path=paths['css'],
+ default_css=paths['default_css'])
docs.set_menu_separator(paths['arrow'])
- _build_menu(docs, filename, root,
- relative_main_index=paths['index_all'])
+ _build_menu(docs)
+ docs.write_title(str(filename.parent.relative_to(root)).title())
- docs.write_title(_get_relative_path(folder, root, folder=True).title())
if bots_index:
docs.write_text('These are the methods that you may be able to '
'use as a bot. Click <a href="{}">here</a> to '
@@ -153,24 +132,22 @@
namespace_paths = []
if bots_index:
for item in bots_index_paths:
- if os.path.dirname(item) == namespace:
- namespace_paths.append(os.path.basename(item))
- _generate_index(os.path.join(folder, namespace),
- original_paths, root,
+ if item.parent == namespace:
+ namespace_paths.append(item)
+
+ _generate_index(root, namespace, paths,
bots_index, namespace_paths)
- if bots_index:
- docs.add_row(namespace.title(),
- link=os.path.join(namespace, BOT_INDEX))
- else:
- docs.add_row(namespace.title(),
- link=os.path.join(namespace, INDEX))
+
+ docs.add_row(
+ namespace.stem.title(),
+ link=namespace / (BOT_INDEX if bots_index else INDEX))
docs.end_table()
docs.write_title('Available items')
docs.begin_table(2)
- files = [(f, _find_title(os.path.join(folder, f))) for f in files]
+ files = [(f, _find_title(f)) for f in files]
files.sort(key=lambda t: t[1])
for file, title in files:
@@ -231,7 +208,7 @@
))
-def _write_html_pages(tlobjects, methods, layer, input_res, output_dir):
+def _write_html_pages(root, tlobjects, methods, layer, input_res):
"""
Generates the documentation HTML files from from ``scheme.tl``
to ``/methods`` and ``/constructors``, etc.
@@ -239,21 +216,18 @@
# Save 'Type: [Constructors]' for use in both:
# * Seeing the return type or constructors belonging to the same type.
# * Generating the types documentation, showing available constructors.
- original_paths = {
- 'css': 'css',
- 'arrow': 'img/arrow.svg',
- 'search.js': 'js/search.js',
- '404': '404.html',
- 'index_all': 'index.html',
- 'bot_index': 'botindex.html',
- 'index_types': 'types/index.html',
- 'index_methods': 'methods/index.html',
- 'index_constructors': 'constructors/index.html'
- }
- original_paths = {k: os.path.join(output_dir, v)
- for k, v in original_paths.items()}
-
- original_paths['default_css'] = 'light' # docs.<name>.css, local path
+ paths = {k: root / v for k, v in (
+ ('css', 'css'),
+ ('arrow', 'img/arrow.svg'),
+ ('search.js', 'js/search.js'),
+ ('404', '404.html'),
+ ('index_all', 'index.html'),
+ ('bot_index', 'botindex.html'),
+ ('index_types', 'types/index.html'),
+ ('index_methods', 'methods/index.html'),
+ ('index_constructors', 'constructors/index.html')
+ )}
+ paths['default_css'] = 'light' # docs.<name>.css, local path
type_to_constructors = defaultdict(list)
type_to_functions = defaultdict(list)
for tlobject in tlobjects:
@@ -266,24 +240,20 @@
methods = {m.name: m for m in methods}
# Since the output directory is needed everywhere partially apply it now
- create_path_for = functools.partial(_get_create_path_for, output_dir)
- path_for_type = functools.partial(_get_path_for_type, output_dir)
+ create_path_for = functools.partial(_get_create_path_for, root)
+ path_for_type = lambda t: root / _get_path_for_type(t)
bot_docs_paths = []
for tlobject in tlobjects:
filename = create_path_for(tlobject)
- paths = {k: _get_relative_path(v, filename)
- for k, v in original_paths.items()}
-
- with DocsWriter(filename, type_to_path=path_for_type) as docs:
+ with DocsWriter(root, filename, path_for_type) as docs:
docs.write_head(title=tlobject.class_name,
- relative_css_path=paths['css'],
- default_css=original_paths['default_css'])
+ css_path=paths['css'],
+ default_css=paths['default_css'])
# Create the menu (path to the current TLObject)
docs.set_menu_separator(paths['arrow'])
- _build_menu(docs, filename, output_dir,
- relative_main_index=paths['index_all'])
+ _build_menu(docs)
# Create the page title
docs.write_title(tlobject.class_name)
@@ -333,9 +303,7 @@
inner = tlobject.result
docs.begin_table(column_count=1)
- docs.add_row(inner, link=path_for_type(
- inner, relative_to=filename
- ))
+ docs.add_row(inner, link=path_for_type(inner))
docs.end_table()
cs = type_to_constructors.get(inner, [])
@@ -349,7 +317,6 @@
docs.begin_table(column_count=2)
for constructor in cs:
link = create_path_for(constructor)
- link = _get_relative_path(link, relative_to=filename)
docs.add_row(constructor.class_name, link=link)
docs.end_table()
@@ -380,8 +347,8 @@
docs.add_row('!' + friendly_type, align='center')
else:
docs.add_row(
- friendly_type, align='center', link=
- path_for_type(arg.type, relative_to=filename)
+ friendly_type, align='center',
+ link=path_for_type(arg.type)
)
# Add a description for this argument
@@ -441,18 +408,13 @@
docs.add_script(relative_src=paths['search.js'])
docs.end_body()
- temp = []
- for item in bot_docs_paths:
- temp.append(os.path.sep.join(item.split(os.path.sep)[2:]))
- bot_docs_paths = temp
-
# Find all the available types (which are not the same as the constructors)
# Each type has a list of constructors associated to it, hence is a map
for t, cs in type_to_constructors.items():
filename = path_for_type(t)
- out_dir = os.path.dirname(filename)
+ out_dir = filename.parent
if out_dir:
- os.makedirs(out_dir, exist_ok=True)
+ out_dir.mkdir(parents=True, exist_ok=True)
# Since we don't have access to the full TLObject, split the type
if '.' in t:
@@ -460,17 +422,13 @@
else:
namespace, name = None, t
- paths = {k: _get_relative_path(v, out_dir, folder=True)
- for k, v in original_paths.items()}
-
- with DocsWriter(filename, type_to_path=path_for_type) as docs:
+ with DocsWriter(root, filename, path_for_type) as docs:
docs.write_head(title=snake_to_camel_case(name),
- relative_css_path=paths['css'],
- default_css=original_paths['default_css'])
+ css_path=paths['css'],
+ default_css=paths['default_css'])
docs.set_menu_separator(paths['arrow'])
- _build_menu(docs, filename, output_dir,
- relative_main_index=paths['index_all'])
+ _build_menu(docs)
# Main file title
docs.write_title(snake_to_camel_case(name))
@@ -489,7 +447,6 @@
for constructor in cs:
# Constructor full name
link = create_path_for(constructor)
- link = _get_relative_path(link, relative_to=filename)
docs.add_row(constructor.class_name, link=link)
docs.end_table()
@@ -509,7 +466,6 @@
docs.begin_table(2)
for func in functions:
link = create_path_for(func)
- link = _get_relative_path(link, relative_to=filename)
docs.add_row(func.class_name, link=link)
docs.end_table()
@@ -534,7 +490,6 @@
docs.begin_table(2)
for ot in other_methods:
link = create_path_for(ot)
- link = _get_relative_path(link, relative_to=filename)
docs.add_row(ot.class_name, link=link)
docs.end_table()
@@ -560,7 +515,6 @@
docs.begin_table(2)
for ot in other_types:
link = create_path_for(ot)
- link = _get_relative_path(link, relative_to=filename)
docs.add_row(ot.class_name, link=link)
docs.end_table()
docs.end_body()
@@ -570,11 +524,10 @@
# information that we have available, simply a file listing all the others
# accessible by clicking on their title
for folder in ['types', 'methods', 'constructors']:
- _generate_index(os.path.join(output_dir, folder), original_paths,
- output_dir)
+ _generate_index(root, root / folder, paths)
- _generate_index(os.path.join(output_dir, 'methods'), original_paths,
- output_dir, True, bot_docs_paths)
+ _generate_index(root, root / 'methods', paths, True,
+ bot_docs_paths)
# Write the final core index, the main index for the rest of files
types = set()
@@ -596,9 +549,8 @@
methods = sorted(methods, key=lambda m: m.name)
cs = sorted(cs, key=lambda c: c.name)
- shutil.copy(os.path.join(input_res, '404.html'), original_paths['404'])
- _copy_replace(os.path.join(input_res, 'core.html'),
- original_paths['index_all'], {
+ shutil.copy(str(input_res / '404.html'), str(paths['404']))
+ _copy_replace(input_res / 'core.html', paths['index_all'], {
'{type_count}': len(types),
'{method_count}': len(methods),
'{constructor_count}': len(tlobjects) - len(methods),
@@ -624,17 +576,15 @@
type_names = fmt(types, formatter=lambda x: x)
# Local URLs shouldn't rely on the output's root, so set empty root
- create_path_for = functools.partial(_get_create_path_for, '', make=False)
- path_for_type = functools.partial(_get_path_for_type, '')
+ create_path_for = functools.partial(
+ _get_create_path_for, Path(), make=False)
+
request_urls = fmt(methods, create_path_for)
- type_urls = fmt(types, path_for_type)
+ type_urls = fmt(types, _get_path_for_type)
constructor_urls = fmt(cs, create_path_for)
- os.makedirs(os.path.abspath(os.path.join(
- original_paths['search.js'], os.path.pardir
- )), exist_ok=True)
- _copy_replace(os.path.join(input_res, 'js', 'search.js'),
- original_paths['search.js'], {
+ paths['search.js'].parent.mkdir(parents=True, exist_ok=True)
+ _copy_replace(input_res / 'js/search.js', paths['search.js'], {
'{request_names}': request_names,
'{type_names}': type_names,
'{constructor_names}': constructor_names,
@@ -649,11 +599,11 @@
('img', ['arrow.svg'])]:
- dirpath = os.path.join(out_dir, dirname)
- os.makedirs(dirpath, exist_ok=True)
+ dirpath = out_dir / dirname
+ dirpath.mkdir(parents=True, exist_ok=True)
for file in files:
- shutil.copy(os.path.join(res_dir, dirname, file), dirpath)
+ shutil.copy(str(res_dir / dirname / file), str(dirpath))
def generate_docs(tlobjects, methods, layer, input_res, output_dir):
- os.makedirs(output_dir, exist_ok=True)
- _write_html_pages(tlobjects, methods, layer, input_res, output_dir)
+ output_dir.mkdir(parents=True, exist_ok=True)
+ _write_html_pages(output_dir, tlobjects, methods, layer, input_res)
_copy_resources(input_res, output_dir)
--- a/telethon_generator/generators/tlobject.py
+++ b/telethon_generator/generators/tlobject.py
@@ -48,9 +48,8 @@
def _write_modules(
out_dir, depth, kind, namespace_tlobjects, type_constructors):
# namespace_tlobjects: {'namespace', [TLObject]}
- os.makedirs(out_dir, exist_ok=True)
+ out_dir.mkdir(parents=True, exist_ok=True)
for ns, tlobjects in namespace_tlobjects.items():
- file = os.path.join(out_dir, '{}.py'.format(ns or '__init__'))
- with open(file, 'w', encoding='utf-8') as f,\
- SourceBuilder(f) as builder:
+ file = out_dir / '{}.py'.format(ns or '__init__')
+ with file.open('w') as f, SourceBuilder(f) as builder:
builder.writeln(AUTO_GEN_NOTICE)
builder.writeln('from {}.tl.tlobject import TLObject', '.' * depth)
@@ -635,11 +634,10 @@
def _write_patched(out_dir, namespace_tlobjects):
- os.makedirs(out_dir, exist_ok=True)
+ out_dir.mkdir(parents=True, exist_ok=True)
for ns, tlobjects in namespace_tlobjects.items():
- file = os.path.join(out_dir, '{}.py'.format(ns or '__init__'))
- with open(file, 'w', encoding='utf-8') as f,\
- SourceBuilder(f) as builder:
+ file = out_dir / '{}.py'.format(ns or '__init__')
+ with file.open('w') as f, SourceBuilder(f) as builder:
builder.writeln(AUTO_GEN_NOTICE)
builder.writeln('import struct')
@@ -715,26 +713,24 @@
if tlobject.fullname in PATCHED_TYPES:
namespace_patched[tlobject.namespace].append(tlobject)
- get_file = functools.partial(os.path.join, output_dir)
- _write_modules(get_file('functions'), import_depth, 'TLRequest',
+ _write_modules(output_dir / 'functions', import_depth, 'TLRequest',
namespace_functions, type_constructors)
- _write_modules(get_file('types'), import_depth, 'TLObject',
+ _write_modules(output_dir / 'types', import_depth, 'TLObject',
namespace_types, type_constructors)
- _write_patched(get_file('patched'), namespace_patched)
+ _write_patched(output_dir / 'patched', namespace_patched)
- filename = os.path.join(get_file('alltlobjects.py'))
- with open(filename, 'w', encoding='utf-8') as file:
+ filename = output_dir / 'alltlobjects.py'
+ with filename.open('w') as file:
with SourceBuilder(file) as builder:
_write_all_tlobjects(tlobjects, layer, builder)
def clean_tlobjects(output_dir):
- get_file = functools.partial(os.path.join, output_dir)
for d in ('functions', 'types'):
- d = get_file(d)
- if os.path.isdir(d):
- shutil.rmtree(d)
+ d = output_dir / d
+ if d.is_dir():
+ shutil.rmtree(str(d))
- tl = get_file('alltlobjects.py')
- if os.path.isfile(tl):
- os.remove(tl)
+ tl = output_dir / 'alltlobjects.py'
+ if tl.is_file():
+ tl.unlink()
--- a/telethon_generator/parsers/errors.py
+++ b/telethon_generator/parsers/errors.py
@@ -57,7 +57,7 @@
Parses the input CSV file with columns (name, error codes, description)
and yields `Error` instances as a result.
"""
- with open(csv_file, newline='') as f:
+ with csv_file.open(newline='') as f:
f = csv.reader(f)
next(f, None) # header
for line, (name, codes, description) in enumerate(f, start=2):
--- a/telethon_generator/parsers/methods.py
+++ b/telethon_generator/parsers/methods.py
@@ -30,7 +30,7 @@
Parses the input CSV file with columns (method, usability, errors)
and yields `MethodInfo` instances as a result.
"""
- with open(csv_file, newline='') as f:
+ with csv_file.open(newline='') as f:
f = csv.reader(f)
next(f, None) # header
for line, (method, usability, errors) in enumerate(f, start=2):
--- a/telethon_generator/parsers/tlobject/parser.py
+++ b/telethon_generator/parsers/tlobject/parser.py
@@ -86,7 +86,7 @@
obj_all = []
obj_by_name = {}
obj_by_type = collections.defaultdict(list)
- with open(file_path, 'r', encoding='utf-8') as file:
+ with file_path.open() as file:
is_function = False
for line in file:
comment_index = line.find('//')

View File

@ -19,7 +19,7 @@ use these if possible.
# Getting information about yourself
me = await client.get_me()
# "me" is an User object. You can pretty-print
# "me" is a user object. You can pretty-print
# any Telegram object with the "stringify" method:
print(me.stringify())

View File

@ -289,7 +289,7 @@ applications"? Now do the same with the library. Use what applies:
# (These examples assume you are inside an "async def")
async with client:
# Does it have an username? Use it!
# Does it have a username? Use it!
entity = await client.get_entity(username)
# Do you have a conversation open with them? Get dialogs.

View File

@ -2,81 +2,12 @@
Telegram API in Other Languages
===============================
Telethon was made for **Python**, and it has inspired other libraries such as
`gramjs <https://github.com/gram-js/gramjs>`__ (JavaScript) and `grammers
<https://github.com/Lonami/grammers>`__ (Rust). But there is a lot more beyond
those, made independently by different developers.
Telethon was made for **Python**, and as far as I know, there is no
*exact* port to other languages. However, there *are* other
implementations made by awesome people (one needs to be awesome to
understand the official Telegram documentation) on several languages
(even more Python too), listed below:
C
=
Possibly the most well-known unofficial open source implementation out
there by `@vysheng <https://github.com/vysheng>`__,
`tgl <https://github.com/vysheng/tgl>`__, and its console client
`telegram-cli <https://github.com/vysheng/tg>`__. Latest development
has been moved to `BitBucket <https://bitbucket.org/vysheng/tdcli>`__.
C++
===
The newest (and official) library, written from scratch, is called
`tdlib <https://github.com/tdlib/td>`__ and is what the Telegram X
uses. You can find more information in the official documentation,
published `here <https://core.telegram.org/tdlib/docs/>`__.
JavaScript
==========
`Ali Gasymov <https://github.com/alik0211>`__ made the `@mtproto/core <https://github.com/alik0211/mtproto-core>`__ library for the browser and nodejs installable via `npm <https://www.npmjs.com/package/@mtproto/core>`__.
`painor <https://github.com/painor>`__ is the primary author of `gramjs <https://github.com/gram-js/gramjs>`__,
a Telegram client implementation in JavaScript.
Kotlin
======
`Kotlogram <https://github.com/badoualy/kotlogram>`__ is a Telegram
implementation written in Kotlin (one of the
`official <https://blog.jetbrains.com/kotlin/2017/05/kotlin-on-android-now-official/>`__
languages for
`Android <https://developer.android.com/kotlin/index.html>`__) by
`@badoualy <https://github.com/badoualy>`__, currently as a beta
yet working.
Language-Agnostic
=================
`Taas <https://www.t-a-a-s.ru/>`__ is a service that lets you use Telegram API with any HTTP client via API. Using tdlib under the hood, Taas is commercial service, but allows free access if you use under 5000 requests per month.
PHP
===
A PHP implementation is also available thanks to
`@danog <https://github.com/danog>`__ and his
`MadelineProto <https://github.com/danog/MadelineProto>`__ project, with
a very nice `online
documentation <https://daniil.it/MadelineProto/API_docs/>`__ too.
Python
======
A fairly new (as of the end of 2017) Telegram library written from the
ground up in Python by
`@delivrance <https://github.com/delivrance>`__ and his
`Pyrogram <https://github.com/pyrogram/pyrogram>`__ library.
There isn't really a reason to pick it over Telethon and it'd be kinda
sad to see you go, but it would be nice to know what you miss from each
other library in either one so both can improve.
Rust
====
The `grammers <https://github.com/Lonami/grammers>`__ library is made by
the `same author as Telethon's <https://github.com/Lonami>`__! If you are
looking for a Telethon alternative written in Rust, this is a valid option!
Another older, work-in-progress implementation, on Rust is made by
`@JuanPotato <https://github.com/JuanPotato>`__ under the fancy
name of `Vail <https://github.com/JuanPotato/Vail>`__.
If you're looking for something like Telethon but in a different programming
language, head over to `Telegram API in Other Languages in the official wiki
<https://github.com/LonamiWebs/Telethon/wiki/Telegram-API-in-Other-Languages>`__
for a (mostly) up-to-date list.

View File

@ -13,6 +13,136 @@ it can take advantage of new goodies!
.. contents:: List of All Versions
Channel comments and Anonymous Admins (v1.17)
=============================================
+------------------------+
| Scheme layer used: 119 |
+------------------------+
New minor version, new layer change! This time is a good one to remind every
consumer of Python libraries that **you should always specify fixed versions
of your dependencies**! If you're using a ``requirements.txt`` file and you
want to stick with the old version (or any version) for the time being, you
can `use the following syntax <https://pip.pypa.io/en/stable/user_guide/>`__:
.. code-block:: text
telethon~=1.16.0
This will install any version compatible with the written version (so, any in
the ``1.16`` series). Patch releases will never break your code (and if they
do, it's a bug). You can also use that syntax in ``pip install``. Your code
can't know what new versions will look like, so saying it will work with all
versions is a lie and will cause issues.
The reason to bring this up is that Telegram has changed things again, and
with the introduction of anonymous administrators and channel comments, the
sender of a message may not be a :tl:`User`! To accomodate for this, the field
is now a :tl:`Peer` and not `int`. As a reminder, it's always a good idea to
use Telethon's friendly methods and custom properties, which have a higher
stability guarantee than accessing raw API fields.
Even if you don't update, your code will still need to account for the fact
that the sender of a message might be one of the accounts Telegram introduced
to preserve backwards compatibility, because this is a server-side change, so
it's better to update and not lag behind. As it's mostly just a single person
driving the project on their free time, bug-fixes are not backported.
This version also updates the format of SQLite sessions (the default), so
after upgrading and using an old session, the session will be updated, which
means trying to use it back in older versions of the library won't work.
For backwards-compatibility sake, the library has introduced the properties
`Message.reply_to_msg_id <telethon.tl.custom.message.Message.reply_to_msg_id>`
and `Message.to_id <telethon.tl.custom.message.Message.to_id>` that behave
like they did before (Telegram has renamed and changed how these fields work).
Breaking Changes
~~~~~~~~~~~~~~~~
* ``Message.from_id`` is now a :tl:`Peer`, not `int`! If you want the marked
sender ID (much like old behaviour), replace all uses of ``.from_id`` with
``.sender_id``. This will mostly work, but of course in old and new versions
you have to account for the fact that this sender may no longer be a user.
* You can no longer assign to `Message.reply_to_msg_id
<telethon.tl.custom.message.Message.reply_to_msg_id>` and `Message.to_id
<telethon.tl.custom.message.Message.to_id>` because these are now properties
that offer a "view" to the real value from a different field.
* Answering inline queries with a ``photo`` or ``document`` will now send the
photo or document used in the resulting message by default. Not sending the
media was technically a bug, but some people may be relying on this old
behaviour. You can use the old behaviour with ``include_media=False``.
Additions
~~~~~~~~~
* New ``raise_last_call_error`` parameter in the client constructor to raise
the same error produced by the last failing call, rather than a generic
`ValueError`.
* New ``formatting_entities`` parameter in `client.send_message()
<telethon.client.messages.MessageMethods.send_message>`, and
`client.send_file() <telethon.client.uploads.UploadMethods.send_file>`
to bypass the parse mode and manually specify the formatting entities.
* New `client.get_permissions() <telethon.client.chats.ChatMethods.get_permissions>`
method to query a participant's permissions in a group or channel. This
request is slightly expensive in small group chats because it has to fetch
the entire chat to check just a user, so use of a cache is advised.
* `Message.click() <telethon.tl.custom.message.Message.click>` now works on
normal polls!
* New ``local_addr`` parameter in the client constructor to use a specific
local network address when connecting to Telegram.
* `client.inline_query() <telethon.client.bots.BotMethods.inline_query>` now
lets you specify the chat where the query is being made from, which some
bots need to provide certain functionality.
* You can now get comments in a channel post with the ``reply_to`` parameter in
`client.iter_messages() <telethon.client.messages.MessageMethods.iter_messages>`.
Comments are messages that "reply to" a specific channel message, hence the
name (which is consistent with how Telegram's API calls it).
Enhancements
~~~~~~~~~~~~
* Updated documentation and list of known errors.
* If ``hachoir`` is available, the file metadata can now be extracted from
streams and in-memory bytes.
* The default parameters used to initialize a connection now match the format
of those used by Telegram Desktop.
* Specifying 0 retries will no longer cause the library to attempt to reconnect.
* The library should now be able to reliably download very large files.
* Global search should work more reliably now.
* Old usernames are evicted from cache, so getting entities by cached username
should now be more reliable.
* Slightly less noisy logs.
* Stability regarding transport-level errors (transport flood, authorization
key not found) should be improved. In particular, you should no longer be
getting unnecessarily logged out.
* Reconnection should no longer occur if the client gets logged out (for
example, another client revokes the session).
Bug fixes
~~~~~~~~~
* In some cases, there were issues when using `events.Album
<telethon.events.album.Album>` together with `events.Raw
<telethon.events.raw.Raw>`.
* For some channels, one of their channel photos would not show up in
`client.iter_profile_photos() <telethon.client.chats.ChatMethods.iter_profile_photos>`.
* In some cases, a request that failed to be sent would be forgotten, causing
the original caller to be "locked" forever for a response that would never
arrive. Failing requests should now consistently be automatically re-sent.
* The library should more reliably handle certain updates with "empty" data.
* Sending documents in inline queries should now work fine.
* Manually using `client.sign_up <telethon.client.auth.AuthMethods.sign_up>`
should now work correctly, instead of claiming "code invalid".
Special mention to some of the other changes in the 1.16.x series:
* The ``thumb`` for ``download_media`` now supports both `str` and :tl:`VideoSize`.
* Thumbnails are sorted, so ``-1`` is always the largest.
Bug Fixes (v1.16.1)
===================
@ -3676,7 +3806,7 @@ things with the ``InteractiveTelegramClient``:
- **Download** any message's media (photos, documents or even contacts!).
- **Receive message updates** as you talk (i.e., someone sent you a message).
It actually is an usable-enough client for your day by day. You could
It actually is a usable-enough client for your day by day. You could
even add ``libnotify`` and pop, you're done! A great cli-client with
desktop notifications.

View File

@ -9,7 +9,7 @@ you may need when using Telethon. They are sorted by relevance and are not in
alphabetical order.
You should use this page to learn about which methods are available, and
if you need an usage example or further description of the arguments, be
if you need a usage example or further description of the arguments, be
sure to follow the links.
.. contents::

View File

@ -165,8 +165,14 @@ def main(argv):
print('Packaging for PyPi aborted, importing the module failed.')
return
for x in ('build', 'dist', 'Telethon.egg-info'):
remove_dirs = ['__pycache__', 'build', 'dist', 'Telethon.egg-info']
for root, _dirs, _files in os.walk(LIBRARY_DIR, topdown=False):
# setuptools is including __pycache__ for some reason (#1605)
if root.endswith('/__pycache__'):
remove_dirs.append(root)
for x in remove_dirs:
shutil.rmtree(x, ignore_errors=True)
run('python3 setup.py sdist', shell=True)
run('python3 setup.py bdist_wheel', shell=True)
run('twine upload dist/*', shell=True)
@ -218,11 +224,13 @@ def main(argv):
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6'
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
],
keywords='telegram api chat client library messaging mtproto',
packages=find_packages(exclude=[
'telethon_*', 'run_tests.py', 'try_telethon.py'
'telethon_*', 'tests*'
]),
install_requires=['pyaes', 'rsa'],
extras_require={

View File

@ -92,7 +92,7 @@ class AuthMethods:
# Starting as a bot account
await client.start(bot_token=bot_token)
# Starting as an user account
# Starting as a user account
await client.start(phone)
# Please enter the code you received: 12345
# Please enter your password: *******
@ -427,6 +427,23 @@ class AuthMethods:
if me:
return me
# To prevent abuse, one has to try to sign in before signing up. This
# is the current way in which Telegram validates the code to sign up.
#
# `sign_in` will set `_tos`, so if it's set we don't need to call it
# because the user already tried to sign in.
#
# We're emulating pre-layer 104 behaviour so except the right error:
if not self._tos:
try:
return await self.sign_in(
phone=phone,
code=code,
phone_code_hash=phone_code_hash,
)
except errors.PhoneNumberUnoccupiedError:
pass # code is correct and was used, now need to sign in
if self._tos and self._tos.text:
if self.parse_mode:
t = self.parse_mode.unparse(self._tos.text, self._tos.entities)

View File

@ -1167,6 +1167,11 @@ class ChatMethods:
"""
Fetches the permissions of a user in a specific chat or channel.
.. note::
This request has to fetch the entire chat for small group chats,
which can get somewhat expensive, so use of a cache is advised.
Arguments
entity (`entity`):
The channel or chat the user is participant of.

View File

@ -193,7 +193,13 @@ class MessageParseMethods:
mapping = sched_to_message
opposite = id_to_message # scheduled may be treated as normal, though
random_id = request if isinstance(request, (int, list)) else request.random_id
random_id = request if isinstance(request, (int, list)) else getattr(request, 'random_id', None)
if random_id is None:
# Can happen when pinning a message does not actually produce a service message.
self._log[__name__].warning(
'No random_id in %s to map to, returning None message for %s', request, result)
return None
if not utils.is_list_like(random_id):
msg = mapping.get(random_to_id.get(random_id))
if not msg:

View File

@ -19,7 +19,7 @@ class _MessagesIter(RequestIter):
"""
async def _init(
self, entity, offset_id, min_id, max_id,
from_user, offset_date, add_offset, filter, search
from_user, offset_date, add_offset, filter, search, reply_to
):
# Note that entity being `None` will perform a global search.
if entity:
@ -58,11 +58,6 @@ class _MessagesIter(RequestIter):
if from_user:
from_user = await self.client.get_input_entity(from_user)
ty = helpers._entity_type(from_user)
if ty != helpers._EntityType.USER:
from_user = None # Ignore from_user unless it's a user
if from_user:
self.from_id = await self.client.get_peer_id(from_user)
else:
self.from_id = None
@ -87,6 +82,18 @@ class _MessagesIter(RequestIter):
offset_id=offset_id,
limit=1
)
elif reply_to is not None:
self.request = functions.messages.GetRepliesRequest(
peer=self.entity,
msg_id=reply_to,
offset_id=offset_id,
offset_date=offset_date,
add_offset=add_offset,
limit=1,
max_id=0,
min_id=0,
hash=0
)
elif search is not None or filter or from_user:
# Telegram completely ignores `from_id` in private chats
ty = helpers._entity_type(self.entity)
@ -121,7 +128,8 @@ class _MessagesIter(RequestIter):
#
# Even better, using `filter` and `from_id` seems to always
# trigger `RPC_CALL_FAIL` which is "internal issues"...
if filter and offset_date and not search and not offset_id:
if not isinstance(filter, types.InputMessagesFilterEmpty) \
and offset_date and not search and not offset_id:
async for m in self.client.iter_messages(
self.entity, 1, offset_date=offset_date):
self.request.offset_id = m.id + 1
@ -236,7 +244,7 @@ class _MessagesIter(RequestIter):
# (only for the first request), it's safe to just clear it off.
self.request.max_date = None
else:
# getHistory and searchGlobal call it offset_date
# getHistory, searchGlobal and getReplies call it offset_date
self.request.offset_date = last_message.date
if isinstance(self.request, functions.messages.SearchGlobalRequest):
@ -278,7 +286,7 @@ class _IDsIter(RequestIter):
else:
r = await self.client(functions.messages.GetMessagesRequest(ids))
if self._entity:
from_id = utils.get_peer(self._entity)
from_id = await self.client._get_peer(self._entity)
if isinstance(r, types.messages.MessagesNotModified):
self.buffer.extend(None for _ in ids)
@ -325,7 +333,8 @@ class MessageMethods:
from_user: 'hints.EntityLike' = None,
wait_time: float = None,
ids: 'typing.Union[int, typing.Sequence[int]]' = None,
reverse: bool = False
reverse: bool = False,
reply_to: int = None
) -> 'typing.Union[_MessagesIter, _IDsIter]':
"""
Iterator over the messages for the given chat.
@ -392,8 +401,7 @@ class MessageMethods:
containing photos.
from_user (`entity`):
Only messages from this user will be returned.
This parameter will be ignored if it is not an user.
Only messages from this entity will be returned.
wait_time (`int`):
Wait time (in seconds) between different
@ -433,6 +441,26 @@ class MessageMethods:
You cannot use this if both `entity` and `ids` are `None`.
reply_to (`int`, optional):
If set to a message ID, the messages that reply to this ID
will be returned. This feature is also known as comments in
posts of broadcast channels, or viewing threads in groups.
This feature can only be used in broadcast channels and their
linked megagroups. Using it in a chat or private conversation
will result in ``telethon.errors.PeerIdInvalidError`` to occur.
When using this parameter, the ``filter`` and ``search``
parameters have no effect, since Telegram's API doesn't
support searching messages in replies.
.. note::
This feature is used to get replies to a message in the
*discussion* group. If the same broadcast channel sends
a message and replies to it itself, that reply will not
be included in the results.
Yields
Instances of `Message <telethon.tl.custom.message.Message>`.
@ -459,6 +487,10 @@ class MessageMethods:
from telethon.tl.types import InputMessagesFilterPhotos
async for message in client.iter_messages(chat, filter=InputMessagesFilterPhotos):
print(message.photo)
# Getting comments from a post in a channel:
async for message in client.iter_messages(channel, reply_to=123):
print(message.chat.title, message.text)
"""
if ids is not None:
if not utils.is_list_like(ids):
@ -486,7 +518,8 @@ class MessageMethods:
offset_date=offset_date,
add_offset=add_offset,
filter=filter,
search=search
search=search,
reply_to=reply_to
)
async def get_messages(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList':
@ -766,7 +799,7 @@ class MessageMethods:
if isinstance(result, types.UpdateShortSentMessage):
message = types.Message(
id=result.id,
peer_id=utils.get_peer(entity),
peer_id=await self._get_peer(entity),
message=message,
date=result.date,
out=result.out,
@ -1208,7 +1241,7 @@ class MessageMethods:
notify: bool = False
):
"""
Pins or unpins a message in a chat.
Pins a message in a chat.
The default behaviour is to *not* notify members, unlike the
official applications.
@ -1221,7 +1254,7 @@ class MessageMethods:
message (`int` | `Message <telethon.tl.custom.message.Message>`):
The message or the message ID to pin. If it's
`None`, the message will be unpinned instead.
`None`, all messages will be unpinned instead.
notify (`bool`, optional):
Whether the pin should notify people or not.
@ -1233,18 +1266,55 @@ class MessageMethods:
message = await client.send_message(chat, 'Pinotifying is fun!')
await client.pin_message(chat, message, notify=True)
"""
return await self._pin(entity, message, unpin=False, notify=notify)
async def unpin_message(
self: 'TelegramClient',
entity: 'hints.EntityLike',
message: 'typing.Optional[hints.MessageIDLike]' = None,
*,
notify: bool = False
):
"""
Unpins a message in a chat.
If no message ID is specified, all pinned messages will be unpinned.
See also `Message.unpin() <telethon.tl.custom.message.Message.unpin>`.
Arguments
entity (`entity`):
The chat where the message should be pinned.
message (`int` | `Message <telethon.tl.custom.message.Message>`):
The message or the message ID to unpin. If it's
`None`, all messages will be unpinned instead.
Example
.. code-block:: python
# Unpin all messages from a chat
await client.unpin_message(chat)
"""
return await self._pin(entity, message, unpin=True, notify=notify)
async def _pin(self, entity, message, *, unpin, notify=False):
message = utils.get_message_id(message) or 0
entity = await self.get_input_entity(entity)
if message <= 0: # old behaviour accepted negative IDs to unpin
await self(functions.messages.UnpinAllMessagesRequest(entity))
return
request = functions.messages.UpdatePinnedMessageRequest(
peer=entity,
id=message,
silent=not notify
silent=not notify,
unpin=unpin,
)
result = await self(request)
# Unpinning does not produce a service message, and technically
# users can pass negative IDs which seem to behave as unpinning too.
if message <= 0:
# Unpinning does not produce a service message
if unpin:
return
# Pinning in User chats (just with yourself really) does not produce a service message

View File

@ -463,6 +463,10 @@ class UserMethods:
.format(peer)
)
async def _get_peer(self: 'TelegramClient', peer: 'hints.EntityLike'):
i, cls = utils.resolve_id(await self.get_peer_id(peer))
return cls(i)
async def get_peer_id(
self: 'TelegramClient',
peer: 'hints.EntityLike',

View File

@ -31,16 +31,15 @@ class ChatAction(EventBuilder):
"""
@classmethod
def build(cls, update, others=None, self_id=None):
if isinstance(update, types.UpdateChannelPinnedMessage) and update.id == 0:
# Telegram does not always send
# UpdateChannelPinnedMessage for new pins
# but always for unpin, with update.id = 0
if isinstance(update, types.UpdatePinnedChannelMessages):
return cls.Event(types.PeerChannel(update.channel_id),
unpin=True)
pin_ids=update.messages,
pin=update.pinned)
elif isinstance(update, types.UpdateChatPinnedMessage) and update.id == 0:
return cls.Event(types.PeerChat(update.chat_id),
unpin=True)
elif isinstance(update, types.UpdatePinnedMessages):
return cls.Event(update.peer,
pin_ids=update.messages,
pin=update.pinned)
elif isinstance(update, types.UpdateChatParticipantAdd):
return cls.Event(types.PeerChat(update.chat_id),
@ -108,12 +107,8 @@ class ChatAction(EventBuilder):
return cls.Event(msg,
users=msg.from_id,
new_photo=True)
elif isinstance(action, types.MessageActionPinMessage) and msg.reply_to:
# Seems to not be reliable on unpins, but when pinning
# we prefer this because we know who caused it.
return cls.Event(msg,
users=msg.from_id,
new_pin=msg.reply_to.reply_to_msg_id)
# Handled by specific updates
# elif isinstance(action, types.MessageActionPinMessage) and msg.reply_to:
class Event(EventCommon):
"""
@ -153,19 +148,22 @@ class ChatAction(EventBuilder):
unpin (`bool`):
`True` if the existing pin gets unpinned.
"""
def __init__(self, where, new_pin=None, new_photo=None,
def __init__(self, where, new_photo=None,
added_by=None, kicked_by=None, created=None,
users=None, new_title=None, unpin=None):
users=None, new_title=None, pin_ids=None, pin=None):
if isinstance(where, types.MessageService):
self.action_message = where
where = where.peer_id
else:
self.action_message = None
super().__init__(chat_peer=where, msg_id=new_pin)
# TODO needs some testing (can there be more than one id, and do they follow pin order?)
# same in get_pinned_message
super().__init__(chat_peer=where, msg_id=pin_ids[0] if pin_ids else None)
self.new_pin = isinstance(new_pin, int)
self._pinned_message = new_pin
self.new_pin = pin_ids is not None
self._pin_ids = pin_ids
self._pinned_messages = None
self.new_photo = new_photo is not None
self.photo = \
@ -202,7 +200,7 @@ class ChatAction(EventBuilder):
self._users = None
self._input_users = None
self.new_title = new_title
self.unpin = unpin
self.unpin = not pin
def _set_client(self, client):
super()._set_client(client)
@ -256,16 +254,26 @@ class ChatAction(EventBuilder):
If ``new_pin`` is `True`, this returns the `Message
<telethon.tl.custom.message.Message>` object that was pinned.
"""
if self._pinned_message == 0:
return None
if self._pinned_messages is None:
await self.get_pinned_messages()
if isinstance(self._pinned_message, int)\
and await self.get_input_chat():
self._pinned_message = await self._client.get_messages(
self._input_chat, ids=self._pinned_message)
if self._pinned_messages:
return self._pinned_messages[0]
if isinstance(self._pinned_message, types.Message):
return self._pinned_message
async def get_pinned_messages(self):
"""
If ``new_pin`` is `True`, this returns a `list` of `Message
<telethon.tl.custom.message.Message>` objects that were pinned.
"""
if not self._pin_ids:
return self._pin_ids # either None or empty list
chat = await self.get_input_chat()
if chat:
self._pinned_messages = await self._client.get_messages(
self._input_chat, ids=self._pin_ids)
return self._pinned_messages
@property
def added_by(self):

View File

@ -147,6 +147,9 @@ class InlineQuery(EventBuilder):
"""
Answers the inline query with the given results.
See the documentation for `builder` to know what kind of answers
can be given.
Args:
results (`list`, optional):
A list of :tl:`InputBotInlineResult` to use.

View File

@ -1,6 +1,6 @@
"""
Several extensions Python is missing, such as a proper class to handle a TCP
communication with support for cancelling the operation, and an utility class
communication with support for cancelling the operation, and a utility class
to read arbitrary binary data in a more comfortable way, with int/strings/etc.
"""
from .binaryreader import BinaryReader

View File

@ -95,7 +95,7 @@ class TcpMTProxy(ObfuscatedConnection):
obfuscated_io = MTProxyIO
# noinspection PyUnusedLocal
def __init__(self, ip, port, dc_id, *, loggers, proxy=None):
def __init__(self, ip, port, dc_id, *, loggers, proxy=None, local_addr=None):
# connect to proxy's host and port instead of telegram's ones
proxy_host, proxy_port = self.address_info(proxy)
self._secret = bytes.fromhex(proxy[2])

View File

@ -20,12 +20,14 @@ class Button:
instances instead making them yourself (i.e. don't do ``Button(...)``
but instead use methods line `Button.inline(...) <inline>` etc.
You can use `inline`, `switch_inline` and `url`
You can use `inline`, `switch_inline`, `url` and `auth`
together to create inline buttons (under the message).
You can use `text`, `request_location` and `request_phone`
You can use `text`, `request_location`, `request_phone` and `request_poll`
together to create a reply markup (replaces the user keyboard).
You can also configure the aspect of the reply with these.
The latest message with a reply markup will be the one shown to the user
(messages contain the buttons, not the chat itself).
You **cannot** mix the two type of buttons together,
and it will error if you try to do so.
@ -63,6 +65,13 @@ class Button:
Note that the given `data` must be less or equal to 64 bytes.
If more than 64 bytes are passed as data, ``ValueError`` is raised.
If you need to store more than 64 bytes, consider saving the real
data in a database and a reference to that data inside the button.
When the user clicks this button, `events.CallbackQuery
<telethon.events.callbackquery.CallbackQuery>` will trigger with the
same data that the button contained, so that you can determine which
button was pressed.
"""
if not data:
data = text.encode('utf-8')
@ -85,6 +94,10 @@ class Button:
If ``same_peer is True`` the inline query will directly be
set under the currently opened chat. Otherwise, the user will
have to select a different dialog to make the query.
When the user clicks this button, after a chat is selected, their
input field will be filled with the username of your bot followed
by the query text, ready to make inline queries.
"""
return types.KeyboardButtonSwitchInline(text, query, same_peer)
@ -96,6 +109,11 @@ class Button:
If no `url` is given, the `text` will be used as said URL instead.
You cannot detect that the user clicked this button directly.
When the user clicks this button, a confirmation box will be shown
to the user asking whether they want to open the displayed URL unless
the domain is trusted, and once confirmed the URL will open in their
device.
"""
return types.KeyboardButtonUrl(text, url or text)
@ -133,6 +151,9 @@ class Button:
fwd_text (`str`):
The new text to show in the button if the message is
forwarded. By default, the button text will be the same.
When the user clicks this button, a confirmation box will be shown
to the user asking whether they want to login to the specified domain.
"""
return types.InputKeyboardButtonUrlAuth(
text=text,
@ -161,6 +182,12 @@ class Button:
be "selective". The keyboard will be shown only to specific
users. It will target users that are @mentioned in the text
of the message or to the sender of the message you reply to.
When the user clicks this button, a text message with the same text
as the button will be sent, and can be handled with `events.NewMessage
<telethon.events.newmessage.NewMessage>`. You cannot distinguish
between a button press and the user typing and sending exactly the
same text on their own.
"""
return cls(types.KeyboardButton(text),
resize=resize, single_use=single_use, selective=selective)
@ -172,6 +199,10 @@ class Button:
Creates a new keyboard button to request the user's location on click.
``resize``, ``single_use`` and ``selective`` are documented in `text`.
When the user clicks this button, a confirmation box will be shown
to the user asking whether they want to share their location with the
bot, and if confirmed a message with geo media will be sent.
"""
return cls(types.KeyboardButtonRequestGeoLocation(text),
resize=resize, single_use=single_use, selective=selective)
@ -183,6 +214,10 @@ class Button:
Creates a new keyboard button to request the user's phone on click.
``resize``, ``single_use`` and ``selective`` are documented in `text`.
When the user clicks this button, a confirmation box will be shown
to the user asking whether they want to share their phone with the
bot, and if confirmed a message with contact media will be sent.
"""
return cls(types.KeyboardButtonRequestPhone(text),
resize=resize, single_use=single_use, selective=selective)
@ -202,6 +237,9 @@ class Button:
the vote, and the pol might be multiple choice.
``resize``, ``single_use`` and ``selective`` are documented in `text`.
When the user clicks this button, a screen letting the user create a
poll will be shown, and if they do create one, the poll will be sent.
"""
return cls(types.KeyboardButtonRequestPoll(text, quiz=force_quiz),
resize=resize, single_use=single_use, selective=selective)

View File

@ -3,6 +3,20 @@ import hashlib
from .. import functions, types
from ... import utils
_TYPE_TO_MIMES = {
'gif': ['image/gif'], # 'video/mp4' too, but that's used for video
'article': ['text/html'],
'audio': ['audio/mpeg'],
'contact': [],
'file': ['application/pdf', 'application/zip'], # actually any
'geo': [],
'photo': ['image/jpeg'],
'sticker': ['image/webp', 'application/x-tgsticker'],
'venue': [],
'video': ['video/mp4'], # tdlib includes text/html for some reason
'voice': ['audio/ogg'],
}
class InlineBuilder:
"""
@ -83,6 +97,31 @@ class InlineBuilder:
content (:tl:`InputWebDocument`, optional):
The content to be shown for this result.
For now it has to be a :tl:`InputWebDocument` if present.
Example:
.. code-block:: python
results = [
# Option with title and description sending a message.
builder.article(
title='First option',
description='This is the first option',
text='Text sent after clicking this option',
),
# Option with title URL to be opened when clicked.
builder.article(
title='Second option',
url='https://example.com',
text='Text sent if the user clicks the option and not the URL',
),
# Sending a message with buttons.
# You can use a list or a list of lists to include more buttons.
builder.article(
title='Third option',
text='Text sent with buttons below',
buttons=Button.url('https://example.com'),
),
]
"""
# TODO Does 'article' work always?
# article, photo, gif, mpeg4_gif, video, audio,
@ -110,7 +149,7 @@ class InlineBuilder:
# noinspection PyIncorrectDocstring
async def photo(
self, file, *, id=None,
self, file, *, id=None, include_media=True,
text=None, parse_mode=(), link_preview=True,
geo=None, period=60, contact=None, game=False, buttons=None
):
@ -118,9 +157,36 @@ class InlineBuilder:
Creates a new inline result of photo type.
Args:
include_media (`bool`, optional):
Whether the photo file used to display the result should be
included in the message itself or not. By default, the photo
is included, and the text parameter alters the caption.
file (`obj`, optional):
Same as ``file`` for `client.send_file()
<telethon.client.uploads.UploadMethods.send_file>`.
Example:
.. code-block:: python
results = [
# Sending just the photo when the user selects it.
builder.photo('/path/to/photo.jpg'),
# Including a caption with some in-memory photo.
photo_bytesio = ...
builder.photo(
photo_bytesio,
text='This will be the caption of the sent photo',
),
# Sending just the message without including the photo.
builder.photo(
photo,
text='This will be a normal text message',
include_media=False,
),
]
"""
try:
fh = utils.get_input_photo(file)
@ -144,6 +210,7 @@ class InlineBuilder:
text=text or '',
parse_mode=parse_mode,
link_preview=link_preview,
media=include_media,
geo=geo,
period=period,
contact=contact,
@ -162,7 +229,8 @@ class InlineBuilder:
mime_type=None, attributes=None, force_document=False,
voice_note=False, video_note=False, use_cache=True, id=None,
text=None, parse_mode=(), link_preview=True,
geo=None, period=60, contact=None, game=False, buttons=None
geo=None, period=60, contact=None, game=False, buttons=None,
include_media=True
):
"""
Creates a new inline result of document type.
@ -183,16 +251,50 @@ class InlineBuilder:
Further explanation of what this result means.
type (`str`, optional):
The type of the document. May be one of: photo, gif,
mpeg4_gif, video, audio, voice, document, sticker.
The type of the document. May be one of: article, audio,
contact, file, geo, gif, photo, sticker, venue, video, voice.
It will be automatically set if ``mime_type`` is specified,
and default to ``'file'`` if no matching mime type is found.
See "Type of the result" in https://core.telegram.org/bots/api.
include_media (`bool`, optional):
Whether the document file used to display the result should be
included in the message itself or not. By default, the document
is included, and the text parameter alters the caption.
Example:
.. code-block:: python
results = [
# Sending just the file when the user selects it.
builder.document('/path/to/file.pdf'),
# Including a caption with some in-memory file.
file_bytesio = ...
builder.document(
file_bytesio,
text='This will be the caption of the sent file',
),
# Sending just the message without including the file.
builder.document(
photo,
text='This will be a normal text message',
include_media=False,
),
]
"""
if type is None:
if voice_note:
type = 'voice'
else:
type = 'document'
elif mime_type:
for ty, mimes in _TYPE_TO_MIMES.items():
for mime in mimes:
if mime_type == mime:
type = ty
break
if type is None:
type = 'file'
try:
fh = utils.get_input_document(file)
@ -225,6 +327,7 @@ class InlineBuilder:
text=text or '',
parse_mode=parse_mode,
link_preview=link_preview,
media=include_media,
geo=geo,
period=period,
contact=contact,
@ -270,7 +373,7 @@ class InlineBuilder:
async def _message(
self, *,
text=None, parse_mode=(), link_preview=True,
text=None, parse_mode=(), link_preview=True, media=False,
geo=None, period=60, contact=None, game=False, buttons=None
):
# Empty strings are valid but false-y; if they're empty use dummy '\0'
@ -284,18 +387,25 @@ class InlineBuilder:
markup = self._client.build_reply_markup(buttons, inline_only=True)
if text is not None:
if not text: # Automatic media on empty string, like stickers
return types.InputBotInlineMessageMediaAuto('')
text, msg_entities = await self._client._parse_message_text(
text, parse_mode
)
return types.InputBotInlineMessageText(
message=text,
no_webpage=not link_preview,
entities=msg_entities,
reply_markup=markup
)
if media:
# "MediaAuto" means it will use whatever media the inline
# result itself has (stickers, photos, or documents), while
# respecting the user's text (caption) and formatting.
return types.InputBotInlineMessageMediaAuto(
message=text,
entities=msg_entities,
reply_markup=markup
)
else:
return types.InputBotInlineMessageText(
message=text,
no_webpage=not link_preview,
entities=msg_entities,
reply_markup=markup
)
elif isinstance(geo, (types.InputGeoPoint, types.GeoPoint)):
return types.InputBotInlineMessageMediaGeo(
geo_point=utils.get_input_geo(geo),

View File

@ -13,6 +13,10 @@ class InlineResult:
result (:tl:`BotInlineResult`):
The original :tl:`BotInlineResult` object.
"""
# tdlib types are the following (InlineQueriesManager::answer_inline_query @ 1a4a834):
# gif, article, audio, contact, file, geo, photo, sticker, venue, video, voice
#
# However, those documented in https://core.telegram.org/bots/api#inline-mode are different.
ARTICLE = 'article'
PHOTO = 'photo'
GIF = 'gif'

View File

@ -21,10 +21,6 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC):
have access to all their sender and chat properties and methods.
Members:
id (`int`):
The ID of this message. This field is *always* present.
Any other member is optional and may be `None`.
out (`bool`):
Whether the message is outgoing (i.e. you sent it from
another session) or incoming (i.e. someone else sent it).
@ -44,7 +40,6 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC):
Whether you have read the media in this message
or not, e.g. listened to the voice note media.
silent (`bool`):
Whether the message should notify people with sound or not.
Previously used in channels, but since 9 August 2019, it can
@ -62,11 +57,38 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC):
legacy (`bool`):
Whether this is a legacy message or not.
edit_hide (`bool`):
Whether the edited mark of this message is edited
should be hidden (e.g. in GUI clients) or shown.
pinned (`bool`):
Whether this message is currently pinned or not.
id (`int`):
The ID of this message. This field is *always* present.
Any other member is optional and may be `None`.
from_id (:tl:`Peer`):
The peer who sent this message, which is either
:tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`.
This value will be `None` for anonymous messages.
peer_id (:tl:`Peer`):
The peer to which this message was sent, which is either
:tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`. This
will always be present except for empty messages.
fwd_from (:tl:`MessageFwdHeader`):
The original forward header if this message is a forward.
You should probably use the `forward` property instead.
via_bot_id (`int`):
The ID of the bot used to send this message
through its inline mode (e.g. "via @like").
reply_to (:tl:`MessageReplyHeader`):
The original reply header if this message is replying to another.
date (`datetime`):
The UTC+0 `datetime` object indicating when this message
was sent. This will always be present except for empty
@ -77,26 +99,6 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC):
<telethon.tl.custom.message.Message>` instances,
which will be `None` for other types of messages.
action (:tl:`MessageAction`):
The message action object of the message for :tl:`MessageService`
instances, which will be `None` for other types of messages.
from_id (:tl:`Peer`):
The peer who sent this message, which is either
:tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`.
This value will be `None` for anonymous messages.
reply_to (:tl:`MessageReplyHeader`):
The original reply header if this message is replying to another.
fwd_from (:tl:`MessageFwdHeader`):
The original forward header if this message is a forward.
You should probably use the `forward` property instead.
via_bot_id (`int`):
The ID of the bot used to send this message
through its inline mode (e.g. "via @like").
media (:tl:`MessageMedia`):
The media sent with this message if any (such as
photos, videos, documents, gifs, stickers, etc.).
@ -120,13 +122,15 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC):
The number of views this message from a broadcast
channel has. This is also present in forwards.
forwards (`int`):
The number of times this message has been forwarded.
replies (`int`):
The number of times another message has replied to this message.
edit_date (`datetime`):
The date when this message was last edited.
edit_hide (`bool`):
Whether the edited mark of this message is edited
should be hidden (e.g. in GUI clients) or shown.
post_author (`str`):
The display name of the message sender to
show in messages sent to broadcast channels.
@ -139,6 +143,10 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC):
restriction_reason (List[:tl:`RestrictionReason`])
An optional list of reasons why this message was restricted.
If the list is `None`, this message has not been restricted.
action (:tl:`MessageAction`):
The message action object of the message for :tl:`MessageService`
instances, which will be `None` for other types of messages.
"""
# region Initialization
@ -161,40 +169,39 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC):
fwd_from=None, via_bot_id=None, media=None, reply_markup=None,
entities=None, views=None, edit_date=None, post_author=None,
grouped_id=None, from_scheduled=None, legacy=None,
edit_hide=None, restriction_reason=None, forwards=None,
replies=None,
edit_hide=None, pinned=None, restriction_reason=None,
forwards=None, replies=None,
# For MessageAction (mandatory)
action=None):
# Common properties to all messages
self.id = id
self.peer_id = peer_id
self.date = date
# Common properties to messages, then to service (in the order they're defined in the `.tl`)
self.out = out
self.mentioned = mentioned
self.media_unread = media_unread
self.silent = silent
self.post = post
self.from_id = from_id
self.reply_to = reply_to
self.message = message
self.fwd_from = fwd_from
self.via_bot_id = via_bot_id
self.media = None if isinstance(
media, types.MessageMediaEmpty) else media
self.reply_markup = reply_markup
self.entities = entities
self.views = views
self.edit_date = edit_date
self.post_author = post_author
self.grouped_id = grouped_id
self.from_scheduled = from_scheduled
self.legacy = legacy
self.edit_hide = edit_hide
self.restriction_reason = restriction_reason
self.id = id
self.from_id = from_id
self.peer_id = peer_id
self.fwd_from = fwd_from
self.via_bot_id = via_bot_id
self.reply_to = reply_to
self.date = date
self.message = message
self.media = None if isinstance(media, types.MessageMediaEmpty) else media
self.reply_markup = reply_markup
self.entities = entities
self.views = views
self.forwards = forwards
self.replies = replies
self.edit_date = edit_date
self.pinned = pinned
self.post_author = post_author
self.grouped_id = grouped_id
self.restriction_reason = restriction_reason
self.action = action
# Convenient storage for custom functions
@ -1004,6 +1011,16 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC):
return await self._client.pin_message(
await self.get_input_chat(), self.id, notify=notify)
async def unpin(self):
"""
Unpins the message. Shorthand for
`telethon.client.messages.MessageMethods.unpin_message`
with both ``entity`` and ``message`` already set.
"""
if self._client:
return await self._client.unpin_message(
await self.get_input_chat(), self.id)
# endregion Public Methods
# region Private Methods

View File

@ -20,7 +20,7 @@ from mimetypes import guess_extension
from types import GeneratorType
from .extensions import markdown, html
from .helpers import add_surrogate, del_surrogate
from .helpers import add_surrogate, del_surrogate, strip_text
from .tl import types
try:
@ -1072,7 +1072,7 @@ def _rle_encode(string):
def _decode_telegram_base64(string):
"""
Decodes an url-safe base64-encoded string into its bytes
Decodes a url-safe base64-encoded string into its bytes
by first adding the stripped necessary padding characters.
This is the way Telegram shares binary data as strings,
@ -1384,6 +1384,101 @@ def decode_waveform(waveform):
return bytes(result)
def split_text(text, entities, *, limit=4096, max_entities=100, split_at=(r'\n', r'\s', '.')):
"""
Split a message text and entities into multiple messages, each with their
own set of entities. This allows sending a very large message as multiple
messages while respecting the formatting.
Arguments
text (`str`):
The message text.
entities (List[:tl:`MessageEntity`])
The formatting entities.
limit (`int`):
The maximum message length of each individual message.
max_entities (`int`):
The maximum amount of entities that will be present in each
individual message.
split_at (Tuplel[`str`]):
The list of regular expressions that will determine where to split
the text. By default, a newline is searched. If no newline is
present, a space is searched. If no space is found, the split will
be made at any character.
The last expression should always match a character, or else the
text will stop being splitted and the resulting text may be larger
than the limit.
Yields
Pairs of ``(str, entities)`` with the split message.
Example
.. code-block:: python
from telethon import utils
from telethon.extensions import markdown
very_long_markdown_text = "..."
text, entities = markdown.parse(very_long_markdown_text)
for text, entities in utils.split_text(text, entities):
await client.send_message(chat, text, formatting_entities=entities)
"""
# TODO add test cases (multiple entities beyond cutoff, at cutoff, splitting at emoji)
# TODO try to optimize this a bit more? (avoid new_ent, smarter update method)
def update(ent, **updates):
kwargs = ent.to_dict()
del kwargs['_']
kwargs.update(updates)
return ent.__class__(**kwargs)
text = add_surrogate(text)
split_at = tuple(map(re.compile, split_at))
while True:
if len(entities) > max_entities:
last_ent = entities[max_entities - 1]
cur_limit = min(limit, last_ent.offset + last_ent.length)
else:
cur_limit = limit
if len(text) <= cur_limit:
break
for split in split_at:
for i in reversed(range(cur_limit)):
m = split.match(text, pos=i)
if m:
cur_text, new_text = text[:m.end()], text[m.end():]
cur_ent, new_ent = [], []
for ent in entities:
if ent.offset < m.end():
if ent.offset + ent.length > m.end():
cur_ent.append(update(ent, length=m.end() - ent.offset))
new_ent.append(update(ent, offset=0, length=ent.offset + ent.length - m.end()))
else:
cur_ent.append(ent)
else:
new_ent.append(update(ent, offset=ent.offset - m.end()))
yield del_surrogate(cur_text), cur_ent
text, entities = new_text, new_ent
break
else:
continue
break
else:
# Can't find where to split, just return the remaining text and entities
break
yield del_surrogate(text), entities
class AsyncClassWrapper:
def __init__(self, wrapped):
self.wrapped = wrapped

View File

@ -1,3 +1,3 @@
# Versions should comply with PEP440.
# This line is parsed in setup.py:
__version__ = '1.16.4'
__version__ = '1.17.5'

View File

@ -69,7 +69,7 @@ inputMediaPhotoExternal#e5bbfe1a flags:# url:string ttl_seconds:flags.0?int = In
inputMediaDocumentExternal#fb52dc99 flags:# url:string ttl_seconds:flags.0?int = InputMedia;
inputMediaGame#d33f43f3 id:InputGame = InputMedia;
inputMediaInvoice#f4e096c3 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string = InputMedia;
inputMediaGeoLive#ce4e82fd flags:# stopped:flags.0?true geo_point:InputGeoPoint period:flags.1?int = InputMedia;
inputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint heading:flags.2?int period:flags.1?int proximity_notification_radius:flags.3?int = InputMedia;
inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector<bytes> solution:flags.1?string solution_entities:flags.1?Vector<MessageEntity> = InputMedia;
inputMediaDice#e66fbf7b emoticon:string = InputMedia;
@ -78,7 +78,7 @@ inputChatUploadedPhoto#c642724e flags:# file:flags.0?InputFile video:flags.1?Inp
inputChatPhoto#8953ad37 id:InputPhoto = InputChatPhoto;
inputGeoPointEmpty#e4c123d6 = InputGeoPoint;
inputGeoPoint#f3b7acc9 lat:double long:double = InputGeoPoint;
inputGeoPoint#48222faf flags:# lat:double long:double accuracy_radius:flags.0?int = InputGeoPoint;
inputPhotoEmpty#1cd7bf0d = InputPhoto;
inputPhoto#3bb3b94a id:long access_hash:long file_reference:bytes = InputPhoto;
@ -141,7 +141,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto;
chatPhoto#d20b9f3c flags:# has_video:flags.0?true photo_small:FileLocation photo_big:FileLocation dc_id:int = ChatPhoto;
messageEmpty#83e5de54 id:int = Message;
message#58ae39c9 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true id:int from_id:flags.8?Peer peer_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long restriction_reason:flags.22?Vector<RestrictionReason> = Message;
message#58ae39c9 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true id:int from_id:flags.8?Peer peer_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long restriction_reason:flags.22?Vector<RestrictionReason> = Message;
messageService#286fa604 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction = Message;
messageMediaEmpty#3ded6320 = MessageMedia;
@ -154,7 +154,7 @@ messageMediaWebPage#a32dd600 webpage:WebPage = MessageMedia;
messageMediaVenue#2ec0533f geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MessageMedia;
messageMediaGame#fdb19008 game:Game = MessageMedia;
messageMediaInvoice#84551347 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument receipt_msg_id:flags.2?int currency:string total_amount:long start_param:string = MessageMedia;
messageMediaGeoLive#7c3c2609 geo:GeoPoint period:int = MessageMedia;
messageMediaGeoLive#b940c666 flags:# geo:GeoPoint heading:flags.0?int period:int proximity_notification_radius:flags.1?int = MessageMedia;
messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia;
messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia;
@ -181,6 +181,7 @@ messageActionBotAllowed#abe9affe domain:string = MessageAction;
messageActionSecureValuesSentMe#1b287353 values:Vector<SecureValue> credentials:SecureCredentialsEncrypted = MessageAction;
messageActionSecureValuesSent#d95c6154 types:Vector<SecureValueType> = MessageAction;
messageActionContactSignUp#f3f25f76 = MessageAction;
messageActionGeoProximityReached#98e0d697 from_id:Peer to_id:Peer distance:int = MessageAction;
dialog#2c171f72 flags:# pinned:flags.2?true unread_mark:flags.3?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int = Dialog;
dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog;
@ -195,7 +196,7 @@ photoStrippedSize#e0b0bc2e type:string bytes:bytes = PhotoSize;
photoSizeProgressive#5aa86a51 type:string location:FileLocation w:int h:int sizes:Vector<int> = PhotoSize;
geoPointEmpty#1117dd5f = GeoPoint;
geoPoint#296f104 long:double lat:double access_hash:long = GeoPoint;
geoPoint#b2a2f663 flags:# long:double lat:double access_hash:long accuracy_radius:flags.0?int = GeoPoint;
auth.sentCode#5e002502 flags:# type:auth.SentCodeType phone_code_hash:string next_type:flags.1?auth.CodeType timeout:flags.2?int = auth.SentCode;
@ -247,8 +248,8 @@ messages.dialogsSlice#71e094f3 count:int dialogs:Vector<Dialog> messages:Vector<
messages.dialogsNotModified#f0e3e596 count:int = messages.Dialogs;
messages.messages#8c718e87 messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.Messages;
messages.messagesSlice#c8edce1e flags:# inexact:flags.1?true count:int next_rate:flags.0?int messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.Messages;
messages.channelMessages#99262e37 flags:# inexact:flags.1?true pts:int count:int messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.Messages;
messages.messagesSlice#3a54685e flags:# inexact:flags.1?true count:int next_rate:flags.0?int offset_id_offset:flags.2?int messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.Messages;
messages.channelMessages#64479808 flags:# inexact:flags.1?true pts:int count:int offset_id_offset:flags.2?int messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.Messages;
messages.messagesNotModified#74535f21 count:int = messages.Messages;
messages.chats#64ff9fd5 chats:Vector<Chat> = messages.Chats;
@ -274,6 +275,7 @@ inputMessagesFilterRoundVideo#b549da53 = MessagesFilter;
inputMessagesFilterMyMentions#c1f8e69a = MessagesFilter;
inputMessagesFilterGeo#e7026d0d = MessagesFilter;
inputMessagesFilterContacts#e062db83 = MessagesFilter;
inputMessagesFilterPinned#1bb00451 = MessagesFilter;
updateNewMessage#1f2b0afd message:Message pts:int pts_count:int = Update;
updateMessageID#4e90bfd6 id:int random_id:long = Update;
@ -313,7 +315,6 @@ updateSavedGifs#9375341e = Update;
updateBotInlineQuery#54826690 flags:# query_id:long user_id:int query:string geo:flags.0?GeoPoint offset:string = Update;
updateBotInlineSend#e48f964 flags:# user_id:int query:string geo:flags.0?GeoPoint id:string msg_id:flags.1?InputBotInlineMessageID = Update;
updateEditChannelMessage#1b3f4df7 message:Message pts:int pts_count:int = Update;
updateChannelPinnedMessage#98592475 channel_id:int id:int = Update;
updateBotCallbackQuery#e73547e1 flags:# query_id:long user_id:int peer:Peer msg_id:int chat_instance:long data:flags.0?bytes game_short_name:flags.1?string = Update;
updateEditMessage#e40370a3 message:Message pts:int pts_count:int = Update;
updateInlineBotCallbackQuery#f9d27a5a flags:# query_id:long user_id:int msg_id:InputBotInlineMessageID chat_instance:long data:flags.0?bytes game_short_name:flags.1?string = Update;
@ -338,8 +339,6 @@ updateChannelReadMessagesContents#89893b45 channel_id:int messages:Vector<int> =
updateContactsReset#7084a7be = Update;
updateChannelAvailableMessages#70db6837 channel_id:int available_min_id:int = Update;
updateDialogUnreadMark#e16459c3 flags:# unread:flags.0?true peer:DialogPeer = Update;
updateUserPinnedMessage#4c43da18 user_id:int id:int = Update;
updateChatPinnedMessage#e10db349 chat_id:int id:int version:int = Update;
updateMessagePoll#aca1657b flags:# poll_id:long poll:flags.0?Poll results:PollResults = Update;
updateChatDefaultBannedRights#54c01850 peer:Peer default_banned_rights:ChatBannedRights version:int = Update;
updateFolderPeers#19360dc0 folder_peers:Vector<FolderPeer> pts:int pts_count:int = Update;
@ -361,6 +360,8 @@ updateReadChannelDiscussionInbox#1cc7de54 flags:# channel_id:int top_msg_id:int
updateReadChannelDiscussionOutbox#4638a26c channel_id:int top_msg_id:int read_max_id:int = Update;
updatePeerBlocked#246a4b22 peer_id:Peer blocked:Bool = Update;
updateChannelUserTyping#ff2abe9f flags:# channel_id:int top_msg_id:flags.0?int user_id:int action:SendMessageAction = Update;
updatePinnedMessages#ed85eab5 flags:# pinned:flags.0?true peer:Peer messages:Vector<int> pts:int pts_count:int = Update;
updatePinnedChannelMessages#8588878b flags:# pinned:flags.0?true channel_id:int messages:Vector<int> pts:int pts_count:int = Update;
updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;
@ -607,6 +608,7 @@ channelParticipantSelf#a3289a6d user_id:int inviter_id:int date:int = ChannelPar
channelParticipantCreator#447dca4b flags:# user_id:int admin_rights:ChatAdminRights rank:flags.0?string = ChannelParticipant;
channelParticipantAdmin#ccbebbaf flags:# can_edit:flags.0?true self:flags.1?true user_id:int inviter_id:flags.1?int promoted_by:int date:int admin_rights:ChatAdminRights rank:flags.2?string = ChannelParticipant;
channelParticipantBanned#1c0facaf flags:# left:flags.0?true user_id:int kicked_by:int date:int banned_rights:ChatBannedRights = ChannelParticipant;
channelParticipantLeft#c3c6796b user_id:int = ChannelParticipant;
channelParticipantsRecent#de3f3c79 = ChannelParticipantsFilter;
channelParticipantsAdmins#b4608969 = ChannelParticipantsFilter;
@ -615,6 +617,7 @@ channelParticipantsBots#b0d1865b = ChannelParticipantsFilter;
channelParticipantsBanned#1427a5e1 q:string = ChannelParticipantsFilter;
channelParticipantsSearch#656ac4b q:string = ChannelParticipantsFilter;
channelParticipantsContacts#bb6ae88d q:string = ChannelParticipantsFilter;
channelParticipantsMentions#e04b5ceb flags:# q:flags.0?string top_msg_id:flags.1?int = ChannelParticipantsFilter;
channels.channelParticipants#f56ee2a8 count:int participants:Vector<ChannelParticipant> users:Vector<User> = channels.ChannelParticipants;
channels.channelParticipantsNotModified#f0173fe9 = channels.ChannelParticipants;
@ -628,7 +631,7 @@ messages.savedGifs#2e0709a5 hash:int gifs:Vector<Document> = messages.SavedGifs;
inputBotInlineMessageMediaAuto#3380c786 flags:# message:string entities:flags.1?Vector<MessageEntity> reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
inputBotInlineMessageText#3dcd7a87 flags:# no_webpage:flags.0?true message:string entities:flags.1?Vector<MessageEntity> reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
inputBotInlineMessageMediaGeo#c1b15d65 flags:# geo_point:InputGeoPoint period:int reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
inputBotInlineMessageMediaGeo#96929a85 flags:# geo_point:InputGeoPoint heading:flags.0?int period:flags.1?int proximity_notification_radius:flags.3?int reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
inputBotInlineMessageMediaVenue#417bbf11 flags:# geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
inputBotInlineMessageMediaContact#a6edbffd flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
inputBotInlineMessageGame#4b425864 flags:# reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
@ -640,7 +643,7 @@ inputBotInlineResultGame#4fa417f2 id:string short_name:string send_message:Input
botInlineMessageMediaAuto#764cf810 flags:# message:string entities:flags.1?Vector<MessageEntity> reply_markup:flags.2?ReplyMarkup = BotInlineMessage;
botInlineMessageText#8c7f65e2 flags:# no_webpage:flags.0?true message:string entities:flags.1?Vector<MessageEntity> reply_markup:flags.2?ReplyMarkup = BotInlineMessage;
botInlineMessageMediaGeo#b722de65 flags:# geo:GeoPoint period:int reply_markup:flags.2?ReplyMarkup = BotInlineMessage;
botInlineMessageMediaGeo#51846fd flags:# geo:GeoPoint heading:flags.0?int period:flags.1?int proximity_notification_radius:flags.3?int reply_markup:flags.2?ReplyMarkup = BotInlineMessage;
botInlineMessageMediaVenue#8a86659c flags:# geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage;
botInlineMessageMediaContact#18d1cdc2 flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage;
@ -1164,8 +1167,6 @@ messageViews#455b853d flags:# views:flags.0?int forwards:flags.1?int replies:fla
messages.messageViews#b6c4f543 views:Vector<MessageViews> chats:Vector<Chat> users:Vector<User> = messages.MessageViews;
stats.messageStats#8999f295 views_graph:StatsGraph = stats.MessageStats;
messages.discussionMessage#f5dd8f9d flags:# messages:Vector<Message> max_id:flags.0?int read_inbox_max_id:flags.1?int read_outbox_max_id:flags.2?int chats:Vector<Chat> users:Vector<User> = messages.DiscussionMessage;
messageReplyHeader#a6d57763 flags:# reply_to_msg_id:int reply_to_peer_id:flags.0?Peer reply_to_top_id:flags.1?int = MessageReplyHeader;
@ -1174,6 +1175,8 @@ messageReplies#4128faac flags:# comments:flags.0?true replies:int replies_pts:in
peerBlocked#e8fd8014 peer_id:Peer date:int = PeerBlocked;
stats.messageStats#8999f295 views_graph:StatsGraph = stats.MessageStats;
---functions---
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
@ -1299,7 +1302,7 @@ contacts.blockFromReplies#29a8962c flags:# delete_message:flags.0?true delete_hi
messages.getMessages#63c66506 id:Vector<InputMessage> = messages.Messages;
messages.getDialogs#a0ee3b73 flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:int = messages.Dialogs;
messages.getHistory#dcbb8260 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:int = messages.Messages;
messages.search#4e17810b flags:# peer:InputPeer q:string from_id:flags.0?InputUser top_msg_id:flags.1?int filter:MessagesFilter min_date:int max_date:int offset_id:int add_offset:int limit:int max_id:int min_id:int hash:int = messages.Messages;
messages.search#c352eec flags:# peer:InputPeer q:string from_id:flags.0?InputPeer top_msg_id:flags.1?int filter:MessagesFilter min_date:int max_date:int offset_id:int add_offset:int limit:int max_id:int min_id:int hash:int = messages.Messages;
messages.readHistory#e306d3a peer:InputPeer max_id:int = messages.AffectedMessages;
messages.deleteHistory#1c015b09 flags:# just_clear:flags.0?true revoke:flags.1?true peer:InputPeer max_id:int = messages.AffectedHistory;
messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector<int> = messages.AffectedMessages;
@ -1393,7 +1396,7 @@ messages.getSplitRanges#1cff7e08 = Vector<MessageRange>;
messages.markDialogUnread#c286d98f flags:# unread:flags.0?true peer:InputDialogPeer = Bool;
messages.getDialogUnreadMarks#22e24e22 = Vector<DialogPeer>;
messages.clearAllDrafts#7e58ee9c = Bool;
messages.updatePinnedMessage#d2aaf7ec flags:# silent:flags.0?true peer:InputPeer id:int = Updates;
messages.updatePinnedMessage#d2aaf7ec flags:# silent:flags.0?true unpin:flags.1?true pm_oneside:flags.2?true peer:InputPeer id:int = Updates;
messages.sendVote#10ea6184 peer:InputPeer msg_id:int options:Vector<bytes> = Updates;
messages.getPollResults#73bb643b peer:InputPeer msg_id:int = Updates;
messages.getOnlines#6e2be050 peer:InputPeer = ChatOnlines;
@ -1422,6 +1425,7 @@ messages.getOldFeaturedStickers#5fe7025b offset:int limit:int hash:int = message
messages.getReplies#24b581ba peer:InputPeer msg_id:int offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:int = messages.Messages;
messages.getDiscussionMessage#446972fd peer:InputPeer msg_id:int = messages.DiscussionMessage;
messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool;
messages.unpinAllMessages#f025bc8b peer:InputPeer = messages.AffectedHistory;
updates.getState#edd4882a = updates.State;
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
@ -1543,4 +1547,4 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel
stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;
stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats;
// LAYER 119
// LAYER 120

View File

@ -9,6 +9,7 @@ ADMIN_RANK_INVALID,400,The given admin title or rank was invalid (possibly large
API_ID_INVALID,400,The api_id/api_hash combination is invalid
API_ID_PUBLISHED_FLOOD,400,"This API id was published somewhere, you can't use it now"
ARTICLE_TITLE_EMPTY,400,The title of the article is empty
AUDIO_TITLE_EMPTY,400,The title attribute of the audio must be non-empty
AUTH_BYTES_INVALID,400,The provided authorization is invalid
AUTH_KEY_DUPLICATED,406,"The authorization key (session file) was used under two different IP addresses simultaneously, and can no longer be used. Use the same session exclusively, or use different sessions"
AUTH_KEY_INVALID,401,The key is invalid
@ -22,6 +23,7 @@ BANNED_RIGHTS_INVALID,400,"You cannot use that set of permissions in this reques
BOTS_TOO_MUCH,400,There are too many bots in this chat/channel
BOT_CHANNELS_NA,400,Bots can't edit admin privileges
BOT_COMMAND_DESCRIPTION_INVALID,400,"The command description was empty, too long or had invalid characters used"
BOT_DOMAIN_INVALID,400,The domain used for the auth button does not match the one configured in @BotFather
BOT_GAMES_DISABLED,400,Bot games cannot be used in this type of chat
BOT_GROUPS_BLOCKED,400,This bot can't be added to groups
BOT_INLINE_DISABLED,400,This bot can't be used in inline mode
@ -88,6 +90,7 @@ EMAIL_INVALID,400,The given email is invalid
EMAIL_UNCONFIRMED_X,400,"Email unconfirmed, the length of the code must be {code_length}"
EMOTICON_EMPTY,400,The emoticon field cannot be empty
EMOTICON_INVALID,400,The specified emoticon cannot be used or was not a emoticon
EMOTICON_STICKERPACK_MISSING,400,The emoticon sticker pack you are trying to get is missing
ENCRYPTED_MESSAGE_INVALID,400,Encrypted message invalid
ENCRYPTION_ALREADY_ACCEPTED,400,Secret chat already accepted
ENCRYPTION_ALREADY_DECLINED,400,The secret chat was already declined
@ -272,6 +275,7 @@ START_PARAM_EMPTY,400,The start parameter is empty
START_PARAM_INVALID,400,Start parameter invalid
STATS_MIGRATE_X,303,The channel statistics must be fetched from DC {dc}
STICKERSET_INVALID,400,The provided sticker set is invalid
STICKERSET_OWNER_ANONYMOUS,406,This sticker set can't be used as the group's official stickers because it was created by one of its anonymous admins
STICKERS_EMPTY,400,No sticker provided
STICKER_DOCUMENT_INVALID,400,"The sticker file was invalid (this file has failed Telegram internal checks, make sure to use the correct format and comply with https://core.telegram.org/animated_stickers)"
STICKER_EMOJI_INVALID,400,Sticker emoji invalid
@ -294,7 +298,7 @@ TYPES_EMPTY,400,The types field is empty
TYPE_CONSTRUCTOR_INVALID,,The type constructor is invalid
UNKNOWN_METHOD,500,The method you tried to call cannot be called on non-CDN DCs
UNTIL_DATE_INVALID,400,That date cannot be specified in this request (try using None)
URL_INVALID,400,The URL used was invalid (e.g. when answering a callback with an URL that's not t.me/yourbot or your game's URL)
URL_INVALID,400,The URL used was invalid (e.g. when answering a callback with a URL that's not t.me/yourbot or your game's URL)
USERNAME_INVALID,400,"Nobody is using this username, or the username is unacceptable. If the latter, it must match r""[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]"""
USERNAME_NOT_MODIFIED,400,The username is not different from the current username
USERNAME_NOT_OCCUPIED,400,The username is not in use by anyone else yet

1 name codes description
9 API_ID_INVALID 400 The api_id/api_hash combination is invalid
10 API_ID_PUBLISHED_FLOOD 400 This API id was published somewhere, you can't use it now
11 ARTICLE_TITLE_EMPTY 400 The title of the article is empty
12 AUDIO_TITLE_EMPTY 400 The title attribute of the audio must be non-empty
13 AUTH_BYTES_INVALID 400 The provided authorization is invalid
14 AUTH_KEY_DUPLICATED 406 The authorization key (session file) was used under two different IP addresses simultaneously, and can no longer be used. Use the same session exclusively, or use different sessions
15 AUTH_KEY_INVALID 401 The key is invalid
23 BOTS_TOO_MUCH 400 There are too many bots in this chat/channel
24 BOT_CHANNELS_NA 400 Bots can't edit admin privileges
25 BOT_COMMAND_DESCRIPTION_INVALID 400 The command description was empty, too long or had invalid characters used
26 BOT_DOMAIN_INVALID 400 The domain used for the auth button does not match the one configured in @BotFather
27 BOT_GAMES_DISABLED 400 Bot games cannot be used in this type of chat
28 BOT_GROUPS_BLOCKED 400 This bot can't be added to groups
29 BOT_INLINE_DISABLED 400 This bot can't be used in inline mode
90 EMAIL_UNCONFIRMED_X 400 Email unconfirmed, the length of the code must be {code_length}
91 EMOTICON_EMPTY 400 The emoticon field cannot be empty
92 EMOTICON_INVALID 400 The specified emoticon cannot be used or was not a emoticon
93 EMOTICON_STICKERPACK_MISSING 400 The emoticon sticker pack you are trying to get is missing
94 ENCRYPTED_MESSAGE_INVALID 400 Encrypted message invalid
95 ENCRYPTION_ALREADY_ACCEPTED 400 Secret chat already accepted
96 ENCRYPTION_ALREADY_DECLINED 400 The secret chat was already declined
275 START_PARAM_INVALID 400 Start parameter invalid
276 STATS_MIGRATE_X 303 The channel statistics must be fetched from DC {dc}
277 STICKERSET_INVALID 400 The provided sticker set is invalid
278 STICKERSET_OWNER_ANONYMOUS 406 This sticker set can't be used as the group's official stickers because it was created by one of its anonymous admins
279 STICKERS_EMPTY 400 No sticker provided
280 STICKER_DOCUMENT_INVALID 400 The sticker file was invalid (this file has failed Telegram internal checks, make sure to use the correct format and comply with https://core.telegram.org/animated_stickers)
281 STICKER_EMOJI_INVALID 400 Sticker emoji invalid
298 TYPE_CONSTRUCTOR_INVALID The type constructor is invalid
299 UNKNOWN_METHOD 500 The method you tried to call cannot be called on non-CDN DCs
300 UNTIL_DATE_INVALID 400 That date cannot be specified in this request (try using None)
301 URL_INVALID 400 The URL used was invalid (e.g. when answering a callback with an URL that's not t.me/yourbot or your game's URL) The URL used was invalid (e.g. when answering a callback with a URL that's not t.me/yourbot or your game's URL)
302 USERNAME_INVALID 400 Nobody is using this username, or the username is unacceptable. If the latter, it must match r"[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]"
303 USERNAME_NOT_MODIFIED 400 The username is not different from the current username
304 USERNAME_NOT_OCCUPIED 400 The username is not in use by anyone else yet

View File

@ -114,7 +114,7 @@ channels.readHistory,user,CHANNEL_INVALID CHANNEL_PRIVATE
channels.readMessageContents,user,CHANNEL_INVALID CHANNEL_PRIVATE
channels.reportSpam,user,CHANNEL_INVALID INPUT_USER_DEACTIVATED
channels.setDiscussionGroup,user,BROADCAST_ID_INVALID LINK_NOT_MODIFIED MEGAGROUP_ID_INVALID MEGAGROUP_PREHISTORY_HIDDEN
channels.setStickers,both,CHANNEL_INVALID PARTICIPANTS_TOO_FEW
channels.setStickers,both,CHANNEL_INVALID PARTICIPANTS_TOO_FEW STICKERSET_OWNER_ANONYMOUS
channels.togglePreHistoryHidden,user,CHAT_LINK_EXISTS
channels.toggleSignatures,user,CHANNEL_INVALID
channels.toggleSlowMode,user,SECONDS_INVALID
@ -236,7 +236,7 @@ messages.getScheduledMessages,user,
messages.getSearchCounters,user,
messages.getSplitRanges,user,
messages.getStatsURL,user,
messages.getStickerSet,both,STICKERSET_INVALID
messages.getStickerSet,both,EMOTICON_STICKERPACK_MISSING STICKERSET_INVALID
messages.getStickers,user,EMOTICON_EMPTY
messages.getSuggestedDialogFilters,user,
messages.getUnreadMentions,user,PEER_ID_INVALID
@ -273,7 +273,7 @@ messages.sendEncryptedFile,user,MSG_WAIT_FAILED
messages.sendEncryptedService,user,DATA_INVALID ENCRYPTION_DECLINED MSG_WAIT_FAILED USER_IS_BLOCKED
messages.sendInlineBotResult,user,CHAT_SEND_INLINE_FORBIDDEN CHAT_WRITE_FORBIDDEN INLINE_RESULT_EXPIRED PEER_ID_INVALID QUERY_ID_EMPTY SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY
messages.sendMedia,both,BOT_PAYMENTS_DISABLED BOT_POLLS_DISABLED BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_SEND_MEDIA_FORBIDDEN CHAT_WRITE_FORBIDDEN EMOTICON_INVALID EXTERNAL_URL_INVALID FILE_PARTS_INVALID FILE_PART_LENGTH_INVALID FILE_REFERENCE_EMPTY FILE_REFERENCE_EXPIRED GAME_BOT_INVALID INPUT_USER_DEACTIVATED MEDIA_CAPTION_TOO_LONG MEDIA_EMPTY PAYMENT_PROVIDER_INVALID PEER_ID_INVALID PHOTO_EXT_INVALID PHOTO_INVALID_DIMENSIONS PHOTO_SAVE_FILE_INVALID POLL_ANSWERS_INVALID POLL_OPTION_DUPLICATE POLL_QUESTION_INVALID QUIZ_CORRECT_ANSWERS_EMPTY QUIZ_CORRECT_ANSWERS_TOO_MUCH QUIZ_CORRECT_ANSWER_INVALID QUIZ_MULTIPLE_INVALID RANDOM_ID_DUPLICATE SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH STORAGE_CHECK_FAILED Timeout USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT VIDEO_CONTENT_TYPE_INVALID WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY
messages.sendMessage,both,AUTH_KEY_DUPLICATED BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_RESTRICTED CHAT_WRITE_FORBIDDEN ENTITIES_TOO_LONG ENTITY_MENTION_USER_INVALID INPUT_USER_DEACTIVATED MESSAGE_EMPTY MESSAGE_TOO_LONG MSG_ID_INVALID PEER_ID_INVALID POLL_OPTION_INVALID RANDOM_ID_DUPLICATE REPLY_MARKUP_INVALID REPLY_MARKUP_TOO_LONG SCHEDULE_BOT_NOT_ALLOWED SCHEDULE_DATE_TOO_LATE SCHEDULE_STATUS_PRIVATE SCHEDULE_TOO_MUCH Timeout USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER
messages.sendMessage,both,AUTH_KEY_DUPLICATED BOT_DOMAIN_INVALID BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_RESTRICTED CHAT_WRITE_FORBIDDEN ENTITIES_TOO_LONG ENTITY_MENTION_USER_INVALID INPUT_USER_DEACTIVATED MESSAGE_EMPTY MESSAGE_TOO_LONG MSG_ID_INVALID PEER_ID_INVALID POLL_OPTION_INVALID RANDOM_ID_DUPLICATE REPLY_MARKUP_INVALID REPLY_MARKUP_TOO_LONG SCHEDULE_BOT_NOT_ALLOWED SCHEDULE_DATE_TOO_LATE SCHEDULE_STATUS_PRIVATE SCHEDULE_TOO_MUCH Timeout USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER
messages.sendMultiMedia,both,SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH
messages.sendScheduledMessages,user,
messages.sendVote,user,MESSAGE_POLL_CLOSED OPTION_INVALID
@ -282,7 +282,7 @@ messages.setBotPrecheckoutResults,both,ERROR_TEXT_EMPTY
messages.setBotShippingResults,both,QUERY_ID_INVALID
messages.setEncryptedTyping,user,CHAT_ID_INVALID
messages.setGameScore,bot,PEER_ID_INVALID USER_BOT_REQUIRED
messages.setInlineBotResults,bot,ARTICLE_TITLE_EMPTY BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID MESSAGE_EMPTY PHOTO_CONTENT_URL_EMPTY PHOTO_THUMB_URL_EMPTY QUERY_ID_INVALID REPLY_MARKUP_INVALID RESULT_TYPE_INVALID SEND_MESSAGE_MEDIA_INVALID SEND_MESSAGE_TYPE_INVALID START_PARAM_INVALID STICKER_DOCUMENT_INVALID USER_BOT_INVALID WEBDOCUMENT_URL_INVALID
messages.setInlineBotResults,bot,ARTICLE_TITLE_EMPTY AUDIO_TITLE_EMPTY BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID MESSAGE_EMPTY PHOTO_CONTENT_URL_EMPTY PHOTO_THUMB_URL_EMPTY QUERY_ID_INVALID REPLY_MARKUP_INVALID RESULT_TYPE_INVALID SEND_MESSAGE_MEDIA_INVALID SEND_MESSAGE_TYPE_INVALID START_PARAM_INVALID STICKER_DOCUMENT_INVALID USER_BOT_INVALID WEBDOCUMENT_URL_INVALID
messages.setInlineGameScore,bot,MESSAGE_ID_INVALID USER_BOT_REQUIRED
messages.setTyping,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ID_INVALID CHAT_WRITE_FORBIDDEN PEER_ID_INVALID USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT
messages.startBot,user,BOT_INVALID PEER_ID_INVALID START_PARAM_EMPTY START_PARAM_INVALID

1 method usability errors
114 channels.readMessageContents user CHANNEL_INVALID CHANNEL_PRIVATE
115 channels.reportSpam user CHANNEL_INVALID INPUT_USER_DEACTIVATED
116 channels.setDiscussionGroup user BROADCAST_ID_INVALID LINK_NOT_MODIFIED MEGAGROUP_ID_INVALID MEGAGROUP_PREHISTORY_HIDDEN
117 channels.setStickers both CHANNEL_INVALID PARTICIPANTS_TOO_FEW CHANNEL_INVALID PARTICIPANTS_TOO_FEW STICKERSET_OWNER_ANONYMOUS
118 channels.togglePreHistoryHidden user CHAT_LINK_EXISTS
119 channels.toggleSignatures user CHANNEL_INVALID
120 channels.toggleSlowMode user SECONDS_INVALID
236 messages.getSearchCounters user
237 messages.getSplitRanges user
238 messages.getStatsURL user
239 messages.getStickerSet both STICKERSET_INVALID EMOTICON_STICKERPACK_MISSING STICKERSET_INVALID
240 messages.getStickers user EMOTICON_EMPTY
241 messages.getSuggestedDialogFilters user
242 messages.getUnreadMentions user PEER_ID_INVALID
273 messages.sendEncryptedService user DATA_INVALID ENCRYPTION_DECLINED MSG_WAIT_FAILED USER_IS_BLOCKED
274 messages.sendInlineBotResult user CHAT_SEND_INLINE_FORBIDDEN CHAT_WRITE_FORBIDDEN INLINE_RESULT_EXPIRED PEER_ID_INVALID QUERY_ID_EMPTY SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY
275 messages.sendMedia both BOT_PAYMENTS_DISABLED BOT_POLLS_DISABLED BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_SEND_MEDIA_FORBIDDEN CHAT_WRITE_FORBIDDEN EMOTICON_INVALID EXTERNAL_URL_INVALID FILE_PARTS_INVALID FILE_PART_LENGTH_INVALID FILE_REFERENCE_EMPTY FILE_REFERENCE_EXPIRED GAME_BOT_INVALID INPUT_USER_DEACTIVATED MEDIA_CAPTION_TOO_LONG MEDIA_EMPTY PAYMENT_PROVIDER_INVALID PEER_ID_INVALID PHOTO_EXT_INVALID PHOTO_INVALID_DIMENSIONS PHOTO_SAVE_FILE_INVALID POLL_ANSWERS_INVALID POLL_OPTION_DUPLICATE POLL_QUESTION_INVALID QUIZ_CORRECT_ANSWERS_EMPTY QUIZ_CORRECT_ANSWERS_TOO_MUCH QUIZ_CORRECT_ANSWER_INVALID QUIZ_MULTIPLE_INVALID RANDOM_ID_DUPLICATE SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH STORAGE_CHECK_FAILED Timeout USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT VIDEO_CONTENT_TYPE_INVALID WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY
276 messages.sendMessage both AUTH_KEY_DUPLICATED BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_RESTRICTED CHAT_WRITE_FORBIDDEN ENTITIES_TOO_LONG ENTITY_MENTION_USER_INVALID INPUT_USER_DEACTIVATED MESSAGE_EMPTY MESSAGE_TOO_LONG MSG_ID_INVALID PEER_ID_INVALID POLL_OPTION_INVALID RANDOM_ID_DUPLICATE REPLY_MARKUP_INVALID REPLY_MARKUP_TOO_LONG SCHEDULE_BOT_NOT_ALLOWED SCHEDULE_DATE_TOO_LATE SCHEDULE_STATUS_PRIVATE SCHEDULE_TOO_MUCH Timeout USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER AUTH_KEY_DUPLICATED BOT_DOMAIN_INVALID BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_RESTRICTED CHAT_WRITE_FORBIDDEN ENTITIES_TOO_LONG ENTITY_MENTION_USER_INVALID INPUT_USER_DEACTIVATED MESSAGE_EMPTY MESSAGE_TOO_LONG MSG_ID_INVALID PEER_ID_INVALID POLL_OPTION_INVALID RANDOM_ID_DUPLICATE REPLY_MARKUP_INVALID REPLY_MARKUP_TOO_LONG SCHEDULE_BOT_NOT_ALLOWED SCHEDULE_DATE_TOO_LATE SCHEDULE_STATUS_PRIVATE SCHEDULE_TOO_MUCH Timeout USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER
277 messages.sendMultiMedia both SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH
278 messages.sendScheduledMessages user
279 messages.sendVote user MESSAGE_POLL_CLOSED OPTION_INVALID
282 messages.setBotShippingResults both QUERY_ID_INVALID
283 messages.setEncryptedTyping user CHAT_ID_INVALID
284 messages.setGameScore bot PEER_ID_INVALID USER_BOT_REQUIRED
285 messages.setInlineBotResults bot ARTICLE_TITLE_EMPTY BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID MESSAGE_EMPTY PHOTO_CONTENT_URL_EMPTY PHOTO_THUMB_URL_EMPTY QUERY_ID_INVALID REPLY_MARKUP_INVALID RESULT_TYPE_INVALID SEND_MESSAGE_MEDIA_INVALID SEND_MESSAGE_TYPE_INVALID START_PARAM_INVALID STICKER_DOCUMENT_INVALID USER_BOT_INVALID WEBDOCUMENT_URL_INVALID ARTICLE_TITLE_EMPTY AUDIO_TITLE_EMPTY BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID MESSAGE_EMPTY PHOTO_CONTENT_URL_EMPTY PHOTO_THUMB_URL_EMPTY QUERY_ID_INVALID REPLY_MARKUP_INVALID RESULT_TYPE_INVALID SEND_MESSAGE_MEDIA_INVALID SEND_MESSAGE_TYPE_INVALID START_PARAM_INVALID STICKER_DOCUMENT_INVALID USER_BOT_INVALID WEBDOCUMENT_URL_INVALID
286 messages.setInlineGameScore bot MESSAGE_ID_INVALID USER_BOT_REQUIRED
287 messages.setTyping both CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ID_INVALID CHAT_WRITE_FORBIDDEN PEER_ID_INVALID USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT
288 messages.startBot user BOT_INVALID PEER_ID_INVALID START_PARAM_EMPTY START_PARAM_INVALID