├── .dockerignore ├── .gitattributes ├── .gitignore ├── .netlify ├── Makefile ├── requirements.txt ├── runtime.txt └── website ├── .travis.yml ├── AUTHORS ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.md ├── botogram ├── __init__.py ├── api.py ├── bot.py ├── callbacks.py ├── commands.py ├── components.py ├── context.py ├── converters.py ├── crypto.py ├── decorators.py ├── defaults.py ├── exceptions.py ├── frozenbot.py ├── hooks.py ├── inline.py ├── messages.py ├── objects │ ├── __init__.py │ ├── base.py │ ├── callbacks.py │ ├── chats.py │ ├── inline.py │ ├── markup.py │ ├── media.py │ ├── messages.py │ ├── mixins.py │ ├── polls.py │ └── updates.py ├── runner │ ├── __init__.py │ ├── ipc.py │ ├── jobs.py │ ├── processes.py │ └── shared.py ├── shared.py ├── syntaxes.py ├── tasks.py ├── updates.py └── utils │ ├── __init__.py │ ├── calls.py │ ├── deprecations.py │ ├── startup.py │ └── strings.py ├── docs ├── _ext │ └── botogramext.py ├── _templates │ └── links.html ├── _themes │ └── botogram │ │ ├── layout.html │ │ ├── static │ │ ├── botogram.css │ │ ├── font-awesome.css │ │ └── fonts │ │ │ └── bitter.ttf │ │ └── theme.conf ├── api │ ├── bot.rst │ ├── buttons.rst │ ├── channels.rst │ ├── components.rst │ ├── index.rst │ ├── inline.rst │ ├── telegram.rst │ └── utility.rst ├── bot-creation.rst ├── bot-structure.rst ├── buildthedocs.yml ├── buttons.rst ├── changelog │ ├── 0.1.rst │ ├── 0.2.rst │ ├── 0.3.rst │ ├── 0.4.rst │ ├── 0.5.rst │ ├── 0.6.rst │ ├── 0.7.rst │ └── index.rst ├── channels.rst ├── conf.py ├── custom-components.rst ├── deploy │ ├── common.rst │ ├── gnu-screen.rst │ ├── index.rst │ └── supervisord.rst ├── groups-management.rst ├── i18n.rst ├── index.rst ├── inline.rst ├── install.rst ├── license.rst ├── quickstart │ ├── api-key.rst │ ├── better-help.rst │ ├── chat-messages.rst │ ├── hello-world.rst │ ├── index.rst │ └── skeleton.rst ├── shared-memory.rst ├── tasks.rst ├── tricks.rst └── unavailable-chats.rst ├── i18n ├── botogram.pot └── langs │ ├── br.po │ ├── en.po │ ├── it.po │ └── pt.po ├── nginx-doc.conf ├── requirements-build.in ├── requirements-build.txt ├── requirements-docs.in ├── requirements-docs.txt ├── requirements-lint.in ├── requirements-lint.txt ├── requirements-test.in ├── requirements-test.txt ├── requirements-tools.in ├── requirements-tools.txt ├── setup.py ├── tasks.py ├── tests ├── conftest.py ├── test_api.py ├── test_bot.py ├── test_callbacks.py ├── test_commands.py ├── test_components.py ├── test_crypto.py ├── test_decorators.py ├── test_frozenbot.py ├── test_inline.py ├── test_objects.py ├── test_objects_base.py ├── test_objects_messages.py ├── test_shared.py ├── test_syntaxes.py ├── test_tasks.py ├── test_utils_calls.py └── test_utils_strings.py └── website ├── 404.html ├── _static └── style.css ├── favicon.ico ├── fonts ├── bitter │ ├── bitter-400.ttf │ ├── bitter-700.ttf │ └── include.min.css ├── dejavu-sans │ ├── dejavu-sans-400.ttf │ └── include.min.css └── hack │ ├── hack-400.ttf │ ├── hack-700.ttf │ └── include.min.css └── index.html /.dockerignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Fix code percentages on GitHub 2 | /docs/_themes linguist-vendored 3 | /Makefile linguist-vendored 4 | /website linguist-vendored 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /botogram.egg-info 2 | /botogram2.egg-info 3 | /build 4 | 5 | /botogram/i18n/*.mo 6 | 7 | __pycache__ 8 | *.py[oc] 9 | 10 | /.cache 11 | 12 | /.netlify/build 13 | 14 | \.pytest_cache/ 15 | -------------------------------------------------------------------------------- /.netlify/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build website docs clean 2 | 3 | build: website docs 4 | 5 | website: $(patsubst website/%,build/%,$(shell find website/ -type f)) 6 | 7 | docs: 8 | cd .. && invoke docs 9 | @rm -rf build/docs 10 | cp -r ../build/docs build/docs 11 | 12 | build/%: website/% 13 | @mkdir -p $(dir $@) 14 | cp $< $@ 15 | 16 | clean: 17 | rm -rf build 18 | -------------------------------------------------------------------------------- /.netlify/requirements.txt: -------------------------------------------------------------------------------- 1 | invoke==0.11.1 2 | -------------------------------------------------------------------------------- /.netlify/runtime.txt: -------------------------------------------------------------------------------- 1 | 3.6 2 | -------------------------------------------------------------------------------- /.netlify/website: -------------------------------------------------------------------------------- 1 | ../website -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | # In order to run Python 3.7 or higher, we need xenial 4 | # https://docs.travis-ci.com/user/languages/python/#python-37-and-higher 5 | dist: xenial 6 | 7 | python: 8 | - "3.4" 9 | - "3.5" 10 | - "3.6" 11 | - "3.7" 12 | 13 | sudo: false 14 | cache: pip 15 | 16 | install: 17 | - pip install invoke 18 | 19 | script: 20 | - invoke build 21 | - invoke test 22 | - invoke lint 23 | 24 | notifications: 25 | email: false 26 | irc: 27 | channels: 28 | - "chat.freenode.net#pietroalbini" 29 | template: 30 | - "Build %{result} for %{repository_slug} on branch %{branch} (%{commit})." 31 | - "More details: %{build_url}" 32 | use_notice: true 33 | skip_join: true 34 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Project maintainers: 2 | 3 | Marco Aceti 4 | Matteo Bocci 5 | Paolo Barbolini 6 | Renato Fiorenza 7 | 8 | Contributors: 9 | 10 | Alberto Coscia 11 | Alex Soglia 12 | Brad Christensen 13 | Fedor Gogolev 14 | Fernando Possebon 15 | Gabriel Hearot 16 | Ilya Otyutskiy 17 | Stefano Teodorani 18 | Francesco Zimbolo 19 | Daniele Ceribelli 20 | 21 | Original author: 22 | 23 | Pietro Albini 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # Documentation released under the MIT license (see LICENSE) 3 | 4 | # Image to build doc 5 | FROM python:3.6-alpine3.6 as BUILDER 6 | RUN apk update \ 7 | && apk add git bash make 8 | RUN pip install --upgrade pip 9 | RUN pip install invoke virtualenv 10 | COPY ./requirements-docs.txt /requirements-docs.txt 11 | RUN pip install -r /requirements-docs.txt 12 | COPY ./ /botogram 13 | RUN cd /botogram && invoke docs && cd /botogram/.netlify && make 14 | 15 | # Image final 16 | FROM nginx:1.17.0-alpine 17 | RUN rm /etc/nginx/conf.d/default.conf 18 | COPY /nginx-doc.conf /etc/nginx/conf.d/default.conf 19 | COPY --from=BUILDER /botogram/.netlify/build/ ./botogram 20 | ARG botogram_version=dev 21 | ENV env_botogram_version=$botogram_version 22 | RUN sed 's/RELEASE/'"$env_botogram_version"'/g' -i /etc/nginx/conf.d/default.conf 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE Makefile 2 | recursive-include docs *.rst 3 | recursive-include botogram/i18n *.mo 4 | recursive-include i18n/langs *.po 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## botogram [![Build Status](https://travis-ci.com/python-botogram/botogram.svg?branch=master)](https://travis-ci.com/python-botogram/botogram) [![News channel](https://img.shields.io/badge/telegram_channel-@botogram__framework-0d86d7.svg?style=flat)][channel] 2 | 3 | _Just focus on your bots._ 4 | 5 | botogram is a Python framework, which allows you to focus just on creating your 6 | [Telegram bots][1], without worrying about the underlying Bots API. 7 | 8 | While most of the libraries for Telegram out there just wrap the Bots API, 9 | botogram focuses heavily on the development experience, aiming to provide you 10 | the best API possible. Most of the Telegram implementation details are managed 11 | by botogram, so you can just focus on your bot. 12 | 13 | ```python 14 | import botogram 15 | bot = botogram.create("YOUR-API-KEY") 16 | 17 | @bot.command("hello") 18 | def hello_command(chat, message, args): 19 | """Say hello to the world!""" 20 | chat.send("Hello world") 21 | 22 | if __name__ == "__main__": 23 | bot.run() 24 | ``` 25 | 26 | You can find the documentation at [botogram.dev][2]. Also, you can 27 | get all the news about botogram in its [Telegram channel][channel]. 28 | 29 | > Please note botogram currently doesn't support some of the upstream API 30 | > features. All of them will be implemented in botogram 1.0 31 | 32 | **Supported Python versions**: 3.4+ 33 | **License**: MIT 34 | 35 | ### Installation 36 | 37 | You can install easily botogram with pip (be sure to have Python 3.4 or higher 38 | installed): 39 | 40 | $ python3 -m pip install botogram2 41 | 42 | If you want to install from the source code, you can clone the repository and 43 | install it with setuptools. Be sure to have Python 3.4 (or a newer version), 44 | pip, virtualenv, setuptools and [invoke][3] installed: 45 | 46 | $ git clone https://github.com/python-botogram/botogram.git 47 | $ cd botogram 48 | $ invoke install 49 | 50 | On some Linux systems you might need to wrap the ``invoke install`` command with 51 | ``sudo``, if you don't have root privileges. 52 | 53 | [1]: https://core.telegram.org/bots 54 | [2]: https://botogram.dev/docs 55 | [3]: http://www.pyinvoke.org 56 | [channel]: https://telegram.me/botogram_framework 57 | -------------------------------------------------------------------------------- /botogram/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | # Prepare the logger 22 | from .utils import configure_logger 23 | configure_logger() 24 | del configure_logger 25 | 26 | 27 | # flake8: noqa 28 | 29 | from .api import APIError, ChatUnavailableError 30 | from .bot import Bot, create, channel 31 | from .frozenbot import FrozenBotError 32 | from .components import Component 33 | from .decorators import pass_bot, pass_shared, help_message_for 34 | from .runner import run 35 | from .objects import * 36 | from .utils import usernames_in 37 | from .callbacks import Buttons, ButtonsRow 38 | from .inline import ( 39 | InlineInputMessage, 40 | InlineInputLocation, 41 | InlineInputVenue, 42 | InlineInputContact, 43 | ) 44 | 45 | 46 | # This code will simulate the Windows' multiprocessing behavior if the 47 | # BOTOGRAM_SIMULATE_WINDOWS environment variable is set 48 | import os 49 | import multiprocessing 50 | 51 | if "BOTOGRAM_SIMULATE_WINDOWS" in os.environ: 52 | multiprocessing.set_start_method("spawn", force=True) 53 | 54 | del os, multiprocessing 55 | -------------------------------------------------------------------------------- /botogram/commands.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | from inspect import Parameter 22 | 23 | 24 | class Command: 25 | """Representation of a single command""" 26 | 27 | def __init__(self, hook, _bot=None): 28 | # Get some parameters from the hook 29 | self.name = hook._name 30 | self.hidden = hook._hidden 31 | self.order = hook._order 32 | 33 | self._hook = hook 34 | self._component_id = hook.component_id 35 | 36 | self._bot = _bot 37 | 38 | def __reduce__(self): 39 | return rebuild_command, (self._hook,) 40 | 41 | def for_bot(self, bot): 42 | """Get the command instance for a specific bot""" 43 | return self.__class__(self._hook, _bot=bot) 44 | 45 | @property 46 | def raw_docstring(self): 47 | """Get the raw docstring of this command""" 48 | func = self._hook.func 49 | 50 | if hasattr(func, "_botogram_help_message"): 51 | if self._bot is not None: 52 | return self._bot._call(func._botogram_help_message, 53 | self._component_id) 54 | else: 55 | return func._botogram_help_message() 56 | elif func.__doc__: 57 | return func.__doc__ 58 | 59 | return 60 | 61 | @property 62 | def docstring(self): 63 | """Get the docstring of this command""" 64 | docstring = self.raw_docstring 65 | if docstring is None: 66 | return 67 | 68 | result = [] 69 | for line in self.raw_docstring.split("\n"): 70 | # Remove leading whitespaces 71 | line = line.strip() 72 | 73 | # Allow only a single blackline 74 | if line == "" and len(result) and result[-1] == "": 75 | continue 76 | 77 | result.append(line) 78 | 79 | # Remove empty lines at the end or at the start of the docstring 80 | for pos in 0, -1: 81 | if result[pos] == "": 82 | result.pop(pos) 83 | 84 | return "\n".join(result) 85 | 86 | @property 87 | def parameters_list(self): 88 | """Get the parameters list of this single command""" 89 | if not self._hook._parameters: 90 | return None 91 | 92 | params_list = "" 93 | 94 | for parameter in self._hook._parameters.values(): 95 | params_list += "[" + parameter.name 96 | 97 | if parameter.annotation is not Parameter.empty: 98 | params_list += ":" + parameter.annotation.__name__ 99 | 100 | if parameter.default is not Parameter.empty: 101 | params_list += "=" + str(parameter.default) 102 | 103 | params_list += "] " 104 | 105 | return params_list.strip() 106 | 107 | @property 108 | def summary(self): 109 | """Get a summary of the command""" 110 | docstring = self.docstring 111 | if docstring is None: 112 | return 113 | 114 | return docstring.split("\n", 1)[0] 115 | 116 | 117 | def rebuild_command(hook): 118 | """Rebuild a Command after being pickled""" 119 | return Command(hook) 120 | -------------------------------------------------------------------------------- /botogram/context.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | import threading 22 | 23 | 24 | _local = threading.local() 25 | _local._botogram_context = [] 26 | 27 | 28 | class Context: 29 | """Context of an hook call""" 30 | 31 | def __init__(self, bot, hook, update): 32 | self.bot = bot 33 | self.hook = hook 34 | self.update = update 35 | 36 | def __enter__(self): 37 | _local._botogram_context.append(self) 38 | 39 | def __exit__(self, *_): 40 | _local._botogram_context.pop() 41 | 42 | def bot_username(self): 43 | """Get the username of the bot""" 44 | return self.bot.itself.username 45 | 46 | def component_name(self): 47 | """Get the name of the current component""" 48 | return self.hook.component.component_name 49 | 50 | def chat(self): 51 | """Get the current chat""" 52 | if self.update: 53 | return self.update.chat() 54 | 55 | 56 | def ctx(): 57 | """Get the current context""" 58 | if _local._botogram_context: 59 | return _local._botogram_context[-1] 60 | -------------------------------------------------------------------------------- /botogram/converters.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | import typing 22 | 23 | 24 | def _convert_to_bool(argument: str) -> bool: 25 | """Convert the given argument in a boolean value""" 26 | lowered = argument.lower() 27 | if lowered in ('yes', 'y', 'true', 't', '1', 'enable', 'on'): 28 | return True 29 | elif lowered in ('no', 'n', 'false', 'f', '0', 'disable', 'off'): 30 | return False 31 | else: 32 | raise ValueError(lowered + ' is not a recognised boolean option') 33 | 34 | 35 | def _parameters_conversion(converter: callable, 36 | argument: str, parameter) -> typing.Any: 37 | """Convert an argument using a given converter""" 38 | if converter is bool: 39 | return _convert_to_bool(argument) 40 | 41 | try: 42 | return converter(argument) 43 | except Exception as exc: 44 | try: 45 | name = converter.__name__ 46 | except AttributeError: 47 | name = converter.__class__.__name__ 48 | 49 | raise ValueError('Converting to "{}" failed ' 50 | 'for parameter "{}".'.format( 51 | name, parameter)) from exc 52 | -------------------------------------------------------------------------------- /botogram/crypto.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | import hmac 22 | 23 | 24 | DIGEST = "md5" 25 | DIGEST_LEN = 16 26 | 27 | 28 | class TamperedMessageError(Exception): 29 | pass 30 | 31 | 32 | def generate_secret_key(bot): 33 | """Generate the secret key of the bot""" 34 | mac = hmac.new(bot.api.token.encode("utf-8"), digestmod=DIGEST) 35 | mac.update(b"botogram" + bot.itself.username.encode("utf-8")) 36 | return mac.digest() 37 | 38 | 39 | def get_hmac(bot, data): 40 | """Get the HMAC of a piece of data""" 41 | mac = hmac.new(generate_secret_key(bot), digestmod=DIGEST) 42 | mac.update(data) 43 | return mac.digest() 44 | 45 | 46 | def sign_data(bot, data): 47 | """Return a signed version of the data, to prevent tampering with it""" 48 | return get_hmac(bot, data) + data 49 | 50 | 51 | def verify_signature(bot, untrusted): 52 | """Check if the untrusted data is correctly signed, and return it""" 53 | if len(untrusted) < DIGEST_LEN: 54 | raise TamperedMessageError 55 | 56 | signature = untrusted[:DIGEST_LEN] 57 | data = untrusted[DIGEST_LEN:] 58 | 59 | if not hmac.compare_digest(get_hmac(bot, data), signature): 60 | raise TamperedMessageError 61 | 62 | return data 63 | 64 | 65 | def compare(a, b): 66 | """Safely compare two values""" 67 | return hmac.compare_digest(a, b) 68 | -------------------------------------------------------------------------------- /botogram/decorators.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | from . import utils 22 | 23 | 24 | @utils.deprecated("@botogram.pass_bot", "1.0", "Just remove the decorator") 25 | def pass_bot(func): 26 | """This decorator is deprecated, and it does nothing""" 27 | # What this decorator did is now done by default 28 | return func 29 | 30 | 31 | @utils.deprecated("@botogram.pass_shared", "1.0", "Just remove the decorator") 32 | def pass_shared(func): 33 | """This decorator is deprecated, and it does nothing""" 34 | # What this decorator did is now done by default 35 | return func 36 | 37 | 38 | def help_message_for(func): 39 | """The return of the decorated function will be the help message of the 40 | function provided as an argument.""" 41 | def decorator(help_func): 42 | func._botogram_help_message = help_func 43 | return help_func 44 | return decorator 45 | -------------------------------------------------------------------------------- /botogram/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2020 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | 22 | class InlineMessageUnsupportedActionException(Exception): 23 | pass 24 | -------------------------------------------------------------------------------- /botogram/messages.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | 22 | def process_message(bot, chains, update): 23 | """Process a message sent to the bot""" 24 | for hook in chains["messages"]: 25 | bot.logger.debug("Processing update #%s with the hook %s..." % 26 | (update.update_id, hook.name)) 27 | 28 | result = hook.call(bot, update) 29 | if result is True: 30 | bot.logger.debug("Update #%s was just processed by the %s hook." % 31 | (update.update_id, hook.name)) 32 | return 33 | 34 | bot.logger.debug("No hook actually processed the #%s update." % 35 | update.update_id) 36 | 37 | 38 | def process_edited_message(bot, chains, update): 39 | """Process an edited message""" 40 | for hook in chains["messages_edited"]: 41 | bot.logger.debug("Processing edited message in update #%s with the " 42 | "hook %s..." % (update.update_id, hook.name)) 43 | 44 | result = hook.call(bot, update) 45 | if result is True: 46 | bot.logger.debug("Update %s was just processed by the %s hook." % 47 | (update.update_id, hook.name)) 48 | return 49 | 50 | bot.logger.debug("No hook actually processed the #%s update." % 51 | update.update_id) 52 | 53 | 54 | def process_channel_post(bot, chains, update): 55 | """Process a channel post""" 56 | for hook in chains["channel_post"]: 57 | bot.logger.debug("Processing channel post in update #%s with the " 58 | "hook %s..." % (update.update_id, hook.name)) 59 | 60 | result = hook.call(bot, update) 61 | if result is True: 62 | bot.logger.debug("Update %s was just processed by the %s hook." % 63 | (update.update_id, hook.name)) 64 | return 65 | 66 | bot.logger.debug("No hook actually processed the #%s update." % 67 | update.update_id) 68 | 69 | 70 | def process_channel_post_edited(bot, chains, update): 71 | """Process an edited channel post""" 72 | for hook in chains["channel_post_edited"]: 73 | bot.logger.debug("Processing edited channel post in update #%s with" 74 | "the hook %s..." % (update.update_id, hook.name)) 75 | 76 | result = hook.call(bot, update) 77 | if result is True: 78 | bot.logger.debug("Update %s was just processed by the %s hook." % 79 | (update.update_id, hook.name)) 80 | return 81 | 82 | bot.logger.debug("No hook actually processed the #%s update." % 83 | update.update_id) 84 | 85 | 86 | def process_poll_update(bot, chains, update): 87 | """Process a poll update""" 88 | for hook in chains["poll_updates"]: 89 | bot.logger.debug("Processing poll update in update #%s with" 90 | "the hook %s..." % (update.update_id, hook.name)) 91 | 92 | result = hook.call(bot, update) 93 | if result is True: 94 | bot.logger.debug("Update %s was just processed by the %s hook." % 95 | (update.update_id, hook.name)) 96 | return 97 | 98 | bot.logger.debug("No hook actually processed the #%s update." % 99 | update.update_id) 100 | -------------------------------------------------------------------------------- /botogram/objects/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | # flake8: noqa 22 | 23 | from .chats import User, Chat, UserProfilePhotos, Permissions 24 | from .media import PhotoSize, Photo, Audio, Voice, Document, Sticker, \ 25 | Video, VideoNote, Animation, Contact, Location, Venue 26 | from .messages import Message 27 | from .markup import ReplyKeyboardMarkup, ReplyKeyboardHide, ForceReply 28 | from .polls import Poll, PollOption 29 | from .updates import Update, Updates 30 | from .mixins import Album 31 | 32 | __all__ = [ 33 | # Chats-related objects 34 | "User", 35 | "Chat", 36 | "UserProfilePhotos", 37 | 38 | # Media-related objects 39 | "PhotoSize", 40 | "Photo", 41 | "Audio", 42 | "Voice", 43 | "Document", 44 | "Sticker", 45 | "Video", 46 | "VideoNote", 47 | "Animation", 48 | "Contact", 49 | "Location", 50 | "Venue", 51 | "Album", 52 | 53 | # Messages-related objects 54 | "Message", 55 | 56 | # Markup-related objects 57 | "ReplyKeyboardMarkup", 58 | "ReplyKeyboardHide", 59 | "ForceReply", 60 | 61 | # Polls-related objects 62 | "Poll", 63 | "PollOption", 64 | 65 | # Updates-related objects 66 | "Update", 67 | "Updates", 68 | ] 69 | -------------------------------------------------------------------------------- /botogram/objects/callbacks.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | from .base import BaseObject 22 | from ..context import ctx 23 | from .messages import User, Message 24 | from .mixins import _require_api 25 | 26 | 27 | class CallbackQuery(BaseObject): 28 | """Telegram API representation of a callback query 29 | 30 | https://core.telegram.org/bots/api#callbackquery 31 | """ 32 | 33 | required = { 34 | "id": str, 35 | "from": User, 36 | "chat_instance": str, 37 | } 38 | optional = { 39 | "message": Message, 40 | "inline_message_id": str, 41 | "data": str, 42 | "game_short_name": str, 43 | } 44 | replace_keys = { 45 | "from": "sender", 46 | "data": "_data", 47 | } 48 | 49 | def __init__(self, *args, **kwargs): 50 | super().__init__(*args, **kwargs) 51 | if self.inline_message_id is not None: 52 | self.is_inline = True 53 | data = {'inline_message_id': self.inline_message_id} 54 | self.message = Message(data) 55 | else: 56 | self.is_inline = False 57 | 58 | self._answered = False 59 | 60 | @_require_api 61 | def notify(self, text, alert=False, cache=0): 62 | """Send a notification or an alert to the user""" 63 | self._answered = True 64 | 65 | self._api.call("answerCallbackQuery", { 66 | "callback_query_id": self.id, 67 | "text": text, 68 | "show_alert": alert, 69 | "cache_time": cache, 70 | }) 71 | 72 | @_require_api 73 | def open_url(self, url, cache=0): 74 | """Tell the user's client to open an URL""" 75 | self._answered = True 76 | 77 | self._api.call("answerCallbackQuery", { 78 | "callback_query_id": self.id, 79 | "url": url, 80 | "cache_time": cache, 81 | }) 82 | 83 | @_require_api 84 | def open_private_chat(self, start_arg, cache=0): 85 | """Open the bot private chat with the user""" 86 | self._answered = True 87 | 88 | # Telegram doesn't support opening private chats with empty parameters, 89 | # so here we present the developer a friendlier message 90 | if not start_arg: 91 | raise ValueError("You must provide a non-empty start argument") 92 | 93 | # Construct the correct link 94 | url = "https://t.me/" + ctx().bot_username() + "?start=" + start_arg 95 | 96 | self._api.call("answerCallbackQuery", { 97 | "callback_query_id": self.id, 98 | "url": url, 99 | "cache_time": cache, 100 | }) 101 | 102 | @_require_api 103 | def _maybe_send_noop(self): 104 | """Internal function to hide the spinner if needed""" 105 | if self._answered: 106 | return 107 | 108 | # Only call this if the query wasn't answered before 109 | self._api.call("answerCallbackQuery", { 110 | "callback_query_id": self.id, 111 | "cache_time": 0 112 | }) 113 | -------------------------------------------------------------------------------- /botogram/objects/inline.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2020 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | from .base import BaseObject 22 | from .messages import User, Location 23 | from . import mixins 24 | from .messages import Message 25 | 26 | 27 | class InlineQuery(BaseObject, mixins.InlineMixin): 28 | required = { 29 | "id": str, 30 | "from": User, 31 | "query": str, 32 | } 33 | optional = { 34 | "location": Location, 35 | "offset": str, 36 | } 37 | replace_keys = { 38 | "from": "sender" 39 | } 40 | 41 | def __init__(self, data): 42 | super().__init__(data) 43 | self._switch_pm_text = None 44 | self._switch_pm_parameter = None 45 | 46 | def switch_pm(self, text, parameter): 47 | """Helper to set the switch_pm_text and switch_pm_parameter""" 48 | self._switch_pm_text = text 49 | self._switch_pm_parameter = parameter 50 | 51 | 52 | class InlineFeedback(BaseObject): 53 | required = { 54 | "result_id": str, 55 | "from": User, 56 | "query": str 57 | } 58 | optional = { 59 | "location": Location, 60 | "inline_message_id": str, 61 | } 62 | replace_keys = { 63 | "from": "sender", 64 | "inline_message_id": "message" 65 | } 66 | 67 | def __init__(self, data, api=None): 68 | super().__init__(data, api) 69 | self.message = Message({"inline_message_id": self.message}, api) 70 | -------------------------------------------------------------------------------- /botogram/objects/markup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | from .base import BaseObject, multiple 22 | 23 | 24 | class ReplyKeyboardMarkup(BaseObject): 25 | """Telegram API representation of a custom keyboard 26 | 27 | https://core.telegram.org/bots/api#replykeyboardmarkup 28 | """ 29 | 30 | required = { 31 | "keyboard": multiple(multiple(str)), 32 | } 33 | optional = { 34 | "resize_keyboard": bool, 35 | "one_time_keyboard": bool, 36 | "selective": bool, 37 | } 38 | 39 | 40 | class ReplyKeyboardHide(BaseObject): 41 | """Telegram API special object which hides a custom keyboard 42 | 43 | https://core.telegram.org/bots/api#replykeyboardhide 44 | """ 45 | 46 | required = { 47 | "hide_keyboard": bool, 48 | } 49 | optional = { 50 | "selective": bool, 51 | } 52 | 53 | 54 | class ForceReply(BaseObject): 55 | """Telegram API special object which forces the user to reply to a message 56 | 57 | https://core.telegram.org/bots/api#forcereply 58 | """ 59 | 60 | required = { 61 | "force_reply": bool, 62 | } 63 | optional = { 64 | "selective": bool, 65 | } 66 | -------------------------------------------------------------------------------- /botogram/objects/polls.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | from .base import BaseObject, multiple 22 | 23 | 24 | class PollOption(BaseObject): 25 | required = { 26 | "text": str, 27 | "voter_count": int, 28 | } 29 | 30 | 31 | class Poll(BaseObject): 32 | required = { 33 | "id": str, 34 | "question": str, 35 | "options": multiple(PollOption), 36 | "is_closed": bool, 37 | } 38 | -------------------------------------------------------------------------------- /botogram/objects/updates.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | from .base import BaseObject, multiple 22 | 23 | from .callbacks import CallbackQuery 24 | from .messages import Message 25 | from .polls import Poll 26 | from .inline import InlineQuery, InlineFeedback 27 | 28 | 29 | class Update(BaseObject): 30 | """Telegram API representation of an update 31 | 32 | https://core.telegram.org/bots/api#update 33 | """ 34 | 35 | # Please update the chat method below when adding new types, thanks! 36 | 37 | required = { 38 | "update_id": int, 39 | } 40 | optional = { 41 | "message": Message, 42 | "edited_message": Message, 43 | "channel_post": Message, 44 | "edited_channel_post": Message, 45 | "callback_query": CallbackQuery, 46 | "poll": Poll, 47 | "inline_query": InlineQuery, 48 | "chosen_inline_result": InlineFeedback, 49 | } 50 | _check_equality_ = "update_id" 51 | 52 | def chat(self): 53 | """Get the chat related to this update""" 54 | if self.message is not None: 55 | return self.message.chat 56 | 57 | if self.edited_message is not None: 58 | return self.edited_message.chat 59 | 60 | if self.channel_post is not None: 61 | return self.channel_post.chat 62 | 63 | if self.edited_channel_post is not None: 64 | return self.edited_channel_post.chat 65 | 66 | if self.callback_query is not None: 67 | return self.callback_query.message.chat 68 | 69 | raise NotImplementedError 70 | 71 | 72 | # Shortcut for the Updates type 73 | Updates = multiple(Update) 74 | -------------------------------------------------------------------------------- /botogram/syntaxes.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | import re 22 | 23 | from . import utils 24 | 25 | 26 | _markdown_re = re.compile(r".*(" 27 | r"\*(.*)\*|" 28 | r"_(.*)_|" 29 | r"\[(.*)\]\((.*)\)|" 30 | r"`(.*)`|" 31 | r"```(.*)```" 32 | r").*") 33 | 34 | _html_re = re.compile(r".*(" 35 | r"(.*)<\/b>|" 36 | r"(.*)<\/strong>|" 37 | r"(.*)<\/i>|" 38 | r"(.*)<\/em>|" 39 | r"(.*)<\/a>|" 40 | r"(.*)<\/code>|" 41 | r"
(.*)<\/pre>"
42 |                       r").*")
43 | 
44 | 
45 | def is_markdown(message):
46 |     """Check if a string is actually markdown"""
47 |     # Don't mark part of URLs or email addresses as Markdown
48 |     message = utils.strip_urls(message)
49 | 
50 |     return bool(_markdown_re.match(message.replace("\n", "")))
51 | 
52 | 
53 | def is_html(message):
54 |     """Check if a string is actually HTML"""
55 |     # Here URLs are not stripped because no sane URL contains HTML tags in it,
56 |     # and for a few cases the speed penality is not worth
57 |     return bool(_html_re.match(message.replace("\n", "")))
58 | 
59 | 
60 | def guess_syntax(message, provided):
61 |     """Guess the right syntax for a message"""
62 |     if provided is None:
63 |         if is_markdown(message):
64 |             return "Markdown"
65 |         elif is_html(message):
66 |             return "HTML"
67 |         else:
68 |             return None
69 | 
70 |     if provided in ("plain",):
71 |         return None
72 |     elif provided in ("md", "markdown", "Markdown"):
73 |         return "Markdown"
74 |     elif provided in ("html", "HTML"):
75 |         return "HTML"
76 |     else:
77 |         raise ValueError("Invalid syntax: %s" % provided)
78 | 


--------------------------------------------------------------------------------
/botogram/tasks.py:
--------------------------------------------------------------------------------
 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS)
 2 | #
 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy
 4 | # of this software and associated documentation files (the "Software"), to deal
 5 | # in the Software without restriction, including without limitation the rights
 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 7 | # copies of the Software, and to permit persons to whom the Software is
 8 | # furnished to do so, subject to the following conditions:
 9 | #
10 | #   The above copyright notice and this permission notice shall be included in
11 | #   all copies or substantial portions of the Software.
12 | #
13 | #   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | #   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | #   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | #   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | #   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | #   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | #   DEALINGS IN THE SOFTWARE.
20 | 
21 | import time
22 | 
23 | 
24 | class BaseTask:
25 |     """A basic task"""
26 | 
27 |     def __init__(self, hook):
28 |         self.hook = hook
29 | 
30 |     def process(self, bot):
31 |         """Process the task"""
32 |         if hasattr(self.hook, "call"):
33 |             return self.hook.call(bot)
34 |         return self.hook(bot)
35 | 
36 | 
37 | class TimerTask(BaseTask):
38 |     """Representation of a single timer"""
39 | 
40 |     def __init__(self, interval, hook):
41 |         self.interval = interval
42 |         self.last_run = -interval
43 | 
44 |         super(TimerTask, self).__init__(hook)
45 | 
46 |     def now(self, current=None):
47 |         """Check if the timer should be ran now"""
48 |         # Allow to provide a dummy time
49 |         if current is None:
50 |             current = time.time()
51 | 
52 |         res = self.last_run + self.interval <= current
53 | 
54 |         # Increment the last_run if the result is True
55 |         if res:
56 |             self.last_run = current
57 | 
58 |         return res
59 | 
60 | 
61 | class Scheduler:
62 |     """Schedule all the tasks"""
63 | 
64 |     def __init__(self):
65 |         # Each component will add its own list here
66 |         self.tasks_lists = []
67 | 
68 |         self.tasks = []
69 |         self.tasks_lists.append(self.tasks)
70 | 
71 |     def add(self, task):
72 |         """Add a task to the scheduler"""
73 |         self.tasks.append(task)
74 | 
75 |     def register_tasks_list(self, tasks):
76 |         """Register a new list of tasks"""
77 |         self.tasks_lists.append(tasks)
78 | 
79 |     def now(self, current=None):
80 |         """Return which tasks should be scheduled now"""
81 |         # Allow to provide a dummy time
82 |         if current is None:
83 |             current = time.time()
84 | 
85 |         # Return all the tasks which should be executed now
86 |         for tasks in self.tasks_lists:
87 |             for task in tasks:
88 |                 if not task.now(current):
89 |                     continue
90 |                 yield task
91 | 


--------------------------------------------------------------------------------
/botogram/updates.py:
--------------------------------------------------------------------------------
  1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS)
  2 | #
  3 | # Permission is hereby granted, free of charge, to any person obtaining a copy
  4 | # of this software and associated documentation files (the "Software"), to deal
  5 | # in the Software without restriction, including without limitation the rights
  6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  7 | # copies of the Software, and to permit persons to whom the Software is
  8 | # furnished to do so, subject to the following conditions:
  9 | #
 10 | #   The above copyright notice and this permission notice shall be included in
 11 | #   all copies or substantial portions of the Software.
 12 | #
 13 | #   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 14 | #   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 15 | #   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 16 | #   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 17 | #   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 18 | #   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 19 | #   DEALINGS IN THE SOFTWARE.
 20 | 
 21 | from . import objects
 22 | from . import api
 23 | 
 24 | 
 25 | class FetchError(api.APIError):
 26 |     """Something went wrong while fetching updates"""
 27 |     pass
 28 | 
 29 | 
 30 | class AnotherInstanceRunningError(FetchError):
 31 |     """Another instance of your bot is running somewhere else"""
 32 | 
 33 |     def __init__(self):
 34 |         Exception.__init__(self, "Request terminated because of another long "
 35 |                            "pooling or webhook active")
 36 | 
 37 | 
 38 | class UpdatesFetcher:
 39 |     """Logic for fetching updates"""
 40 | 
 41 |     def __init__(self, bot):
 42 |         self._bot = bot
 43 |         self._last_id = -1
 44 |         self._backlog_processed = False
 45 | 
 46 |         # Don't treat backlog as backlog if bot.process_backlog is True
 47 |         if bot.process_backlog:
 48 |             self._backlog_processed = True
 49 | 
 50 |     def _fetch_updates(self, timeout):
 51 |         """Low level function to just fetch updates from Telegram"""
 52 |         try:
 53 |             return self._bot.api.call("getUpdates", {
 54 |                 "offset": self._last_id + 1,
 55 |                 "timeout": timeout,
 56 |             }, expect=objects.Updates)
 57 |         except api.APIError as e:
 58 |             # Raise a specific exception if another instance is running
 59 |             if e.error_code == 409 and "conflict" in e.description.lower():
 60 |                 raise AnotherInstanceRunningError()
 61 |             raise
 62 |         except ValueError:
 63 |             raise FetchError("Got an invalid response from Telegram!")
 64 | 
 65 |     def fetch(self, timeout=1):
 66 |         """Fetch the latest updates"""
 67 |         if not self._backlog_processed:
 68 |             # Just erase all the previous messages
 69 |             last = self._bot.api.call("getUpdates", {
 70 |                 "offset": -1,
 71 |                 "timeout": 0,
 72 |             }, expect=objects.Updates)
 73 | 
 74 |             # Be sure to skip also the last update
 75 |             if last:
 76 |                 self._last_id = last[-1].update_id
 77 |             else:
 78 |                 self._last_id = 0
 79 | 
 80 |             self._backlog_processed = True
 81 | 
 82 |         updates = self._fetch_updates(timeout)
 83 | 
 84 |         # If there are no updates just ignore this block
 85 |         try:
 86 |             self._last_id = updates[-1].update_id
 87 |         except IndexError:
 88 |             pass
 89 | 
 90 |         return updates
 91 | 
 92 |     def block_until_alone(self, treshold=4, check_timeout=1, when_stop=None):
 93 |         """Returns when this one is the only instance of the bot"""
 94 |         checks_count = 0
 95 | 
 96 |         while checks_count < treshold:
 97 |             # This provides an artificial end to the blocking
 98 |             if when_stop is not None and when_stop():
 99 |                 return False
100 | 
101 |             try:
102 |                 updates = self._fetch_updates(check_timeout)
103 |             except AnotherInstanceRunningError:
104 |                 # Reset the count
105 |                 checks_count = 0
106 |                 continue
107 | 
108 |             # Update the last_id
109 |             try:
110 |                 self._last_id = updates[-1].update_id
111 |             except IndexError:
112 |                 pass
113 | 
114 |             # Don't count requests with new updates, since they don't tell if
115 |             # another instance is running, they only make noise
116 |             if updates:
117 |                 continue
118 | 
119 |             # Increment the count every time a request succedes, so the whole
120 |             # function exits when checks_needed is reached
121 |             checks_count += 1
122 | 
123 |         return True
124 | 
125 |     @property
126 |     def backlog_processed(self):
127 |         return self._backlog_processed
128 | 


--------------------------------------------------------------------------------
/botogram/utils/__init__.py:
--------------------------------------------------------------------------------
 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS)
 2 | #
 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy
 4 | # of this software and associated documentation files (the "Software"), to deal
 5 | # in the Software without restriction, including without limitation the rights
 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 7 | # copies of the Software, and to permit persons to whom the Software is
 8 | # furnished to do so, subject to the following conditions:
 9 | #
10 | #   The above copyright notice and this permission notice shall be included in
11 | #   all copies or substantial portions of the Software.
12 | #
13 | #   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | #   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | #   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | #   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | #   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | #   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | #   DEALINGS IN THE SOFTWARE.
20 | 
21 | # flake8: noqa
22 | 
23 | from .deprecations import deprecated, DeprecatedAttributes, warn
24 | from .strings import strip_urls, usernames_in
25 | from .startup import get_language, configure_logger
26 | from .calls import wraps, CallLazyArgument, call
27 | 


--------------------------------------------------------------------------------
/botogram/utils/calls.py:
--------------------------------------------------------------------------------
 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS)
 2 | #
 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy
 4 | # of this software and associated documentation files (the "Software"), to deal
 5 | # in the Software without restriction, including without limitation the rights
 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 7 | # copies of the Software, and to permit persons to whom the Software is
 8 | # furnished to do so, subject to the following conditions:
 9 | #
10 | #   The above copyright notice and this permission notice shall be included in
11 | #   all copies or substantial portions of the Software.
12 | #
13 | #   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | #   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | #   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | #   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | #   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | #   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | #   DEALINGS IN THE SOFTWARE.
20 | 
21 | import inspect
22 | 
23 | import functools
24 | 
25 | 
26 | def wraps(func):
27 |     """Update a wrapper function to looks like the wrapped one"""
28 |     # A custom implementation of functools.wraps is needed because we need some
29 |     # more metadata on the returned function
30 |     def updater(original):
31 |         # Here the original signature is needed in order to call the function
32 |         # with the right set of arguments in Bot._call
33 |         original_signature = inspect.signature(original)
34 | 
35 |         updated = functools.update_wrapper(original, func)
36 |         updated._botogram_original_signature = original_signature
37 |         return updated
38 |     return updater
39 | 
40 | 
41 | class CallLazyArgument:
42 |     """A special argument which is loaded lazily"""
43 | 
44 |     _botogram_call_lazy_argument = True
45 | 
46 |     def __init__(self, loader):
47 |         self.loader = loader
48 | 
49 |     def load(self):
50 |         return self.loader()
51 | 
52 | 
53 | def call(func, **available):
54 |     """Call a function with a dynamic set of arguments"""
55 |     # Get the correct function signature
56 |     # _botogram_original_signature contains the signature used before wrapping
57 |     # a function with @utils.wraps, so the arguments gets resolved correctly
58 |     if hasattr(func, "_botogram_original_signature"):
59 |         signature = func._botogram_original_signature
60 |     else:
61 |         signature = inspect.signature(func)
62 | 
63 |     kwargs = {}
64 | 
65 |     for parameter in signature.parameters.values():
66 |         if parameter.name not in available:
67 |             if parameter.default is not inspect.Parameter.empty:
68 |                 continue
69 | 
70 |             raise TypeError("botogram doesn't know what to provide for %s"
71 |                             % parameter.name)
72 | 
73 |         # If the argument is lazily loaded wake him up
74 |         arg = available[parameter.name]
75 |         if hasattr(arg, "_botogram_call_lazy_argument"):
76 |             arg = arg.load()
77 | 
78 |         kwargs[parameter.name] = arg
79 | 
80 |     return func(**kwargs)
81 | 


--------------------------------------------------------------------------------
/botogram/utils/deprecations.py:
--------------------------------------------------------------------------------
 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS)
 2 | #
 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy
 4 | # of this software and associated documentation files (the "Software"), to deal
 5 | # in the Software without restriction, including without limitation the rights
 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 7 | # copies of the Software, and to permit persons to whom the Software is
 8 | # furnished to do so, subject to the following conditions:
 9 | #
10 | #   The above copyright notice and this permission notice shall be included in
11 | #   all copies or substantial portions of the Software.
12 | #
13 | #   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | #   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | #   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | #   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | #   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | #   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | #   DEALINGS IN THE SOFTWARE.
20 | 
21 | import sys
22 | import traceback
23 | 
24 | import logbook
25 | 
26 | 
27 | warn_logger = logbook.Logger("botogram's code warnings")
28 | 
29 | 
30 | def _deprecated_message(name, removed_on, fix, back):
31 |     before = "%s will be removed in botogram %s." % (name, removed_on)
32 |     after = "Fix: %s" % fix
33 |     warn(back - 1, before, after)
34 | 
35 | 
36 | def deprecated(name, removed_on, fix, back=0):
37 |     """Mark a function as deprecated"""
38 |     def decorator(func):
39 |         def wrapper(*args, **kwargs):
40 |             _deprecated_message(name, removed_on, fix, -2 - back)
41 |             return func(*args, **kwargs)
42 |         return wrapper
43 |     return decorator
44 | 
45 | 
46 | class DeprecatedAttributes:
47 |     """Mark a class attribute as deprecated"""
48 | 
49 |     _deprecated_ = {}
50 | 
51 |     def __getattribute__(self, key):
52 |         def get(k):
53 |             return object.__getattribute__(self, k)
54 | 
55 |         deprecated = get("_deprecated_")
56 | 
57 |         if key in deprecated:
58 |             _deprecated_message(
59 |                 get("__class__").__name__ + "." + key,
60 |                 deprecated[key]["removed_on"],
61 |                 deprecated[key]["fix"],
62 |                 -2,
63 |             )
64 |             if "callback" in deprecated[key]:
65 |                 return deprecated[key]["callback"]()
66 | 
67 |         return object.__getattribute__(self, key)
68 | 
69 | 
70 | def warn(stack_pos, before_message, after_message=None):
71 |     """Issue a warning caused by user code"""
72 |     # This is a workaround for http://bugs.python.org/issue25108
73 |     # In Python 3.5.0, traceback.extract_stack returns an additional internal
74 |     # stack frame, which causes a lot of trouble around there.
75 |     if sys.version_info[:3] == (3, 5, 0):
76 |         stack_pos -= 1
77 | 
78 |     frame = traceback.extract_stack()[stack_pos - 1]
79 |     at_message = "At: %s (line %s)" % (frame[0], frame[1])
80 | 
81 |     warn_logger.warn(before_message)
82 |     if after_message is not None:
83 |         warn_logger.warn(at_message)
84 |         warn_logger.warn(after_message + "\n")
85 |     else:
86 |         warn_logger.warn(at_message + "\n")
87 | 


--------------------------------------------------------------------------------
/botogram/utils/startup.py:
--------------------------------------------------------------------------------
 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS)
 2 | #
 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy
 4 | # of this software and associated documentation files (the "Software"), to deal
 5 | # in the Software without restriction, including without limitation the rights
 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 7 | # copies of the Software, and to permit persons to whom the Software is
 8 | # furnished to do so, subject to the following conditions:
 9 | #
10 | #   The above copyright notice and this permission notice shall be included in
11 | #   all copies or substantial portions of the Software.
12 | #
13 | #   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | #   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | #   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | #   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | #   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | #   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | #   DEALINGS IN THE SOFTWARE.
20 | 
21 | import os
22 | import sys
23 | import gettext
24 | 
25 | import pkg_resources
26 | import logbook
27 | 
28 | 
29 | # This small piece of global state will track if logbook was configured
30 | _logger_configured = False
31 | 
32 | 
33 | def get_language(lang):
34 |     """Get the GNUTranslations instance of a specific language"""
35 |     path = pkg_resources.resource_filename("botogram", "i18n/%s.mo" % lang)
36 |     if not os.path.exists(path):
37 |         raise ValueError('Language "%s" is not supported by botogram' % lang)
38 | 
39 |     with open(path, "rb") as f:
40 |         gt = gettext.GNUTranslations(f)
41 | 
42 |     return gt
43 | 
44 | 
45 | def configure_logger():
46 |     """Configure a logger object"""
47 |     global _logger_configured
48 | 
49 |     # Don't configure the logger multiple times
50 |     if _logger_configured:
51 |         return
52 | 
53 |     # The StreamHandler will log everything to stdout
54 |     min_level = 'DEBUG' if 'BOTOGRAM_DEBUG' in os.environ else 'INFO'
55 |     handler = logbook.StreamHandler(sys.stdout, level=min_level)
56 |     handler.format_string = '{record.time.hour:0>2}:{record.time.minute:0>2}' \
57 |                             '.{record.time.second:0>2} - ' \
58 |                             '{record.level_name:^9} - {record.message}'
59 |     handler.push_application()
60 | 
61 |     # Don't reconfigure the logger, thanks
62 |     _logger_configured = True
63 | 


--------------------------------------------------------------------------------
/botogram/utils/strings.py:
--------------------------------------------------------------------------------
 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS)
 2 | #
 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy
 4 | # of this software and associated documentation files (the "Software"), to deal
 5 | # in the Software without restriction, including without limitation the rights
 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 7 | # copies of the Software, and to permit persons to whom the Software is
 8 | # furnished to do so, subject to the following conditions:
 9 | #
10 | #   The above copyright notice and this permission notice shall be included in
11 | #   all copies or substantial portions of the Software.
12 | #
13 | #   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | #   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | #   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | #   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | #   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | #   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | #   DEALINGS IN THE SOFTWARE.
20 | 
21 | import re
22 | 
23 | 
24 | # URLs regex created by http://twitter.com/imme_emosol
25 | 
26 | _username_re = re.compile(r"\@([a-zA-Z0-9_]{5}[a-zA-Z0-9_]*)")
27 | _command_re = re.compile(r"^\/[a-zA-Z0-9_]+(\@[a-zA-Z0-9_]{5}[a-zA-Z0-9_]*)?$")
28 | _email_re = re.compile(r"[a-zA-Z0-9_\.\+\-]+\@[a-zA-Z0-9_\.\-]+\.[a-zA-Z]+")
29 | _url_re = re.compile(r"https?://(-\.)?([^\s/?\.#]+\.?)+(/[^\s]*)?")
30 | 
31 | 
32 | def strip_urls(string):
33 |     """Strip URLs and emails from a string"""
34 |     string = _url_re.sub("", string)
35 |     string = _email_re.sub("", string)
36 |     return string
37 | 
38 | 
39 | def usernames_in(message):
40 |     """Return all the matched usernames in the message"""
41 |     # Don't parse usernames in the commands
42 |     if _command_re.match(message.split(" ", 1)[0]):
43 |         message = message.split(" ", 1)[1]
44 | 
45 |     # Strip email addresses from the message, in order to avoid matching the
46 |     # user's domain. Also strip URLs, in order to avoid usernames in them.
47 |     message = strip_urls(message)
48 | 
49 |     results = []
50 |     for result in _username_re.finditer(message):
51 |         if result.group(1):
52 |             results.append(result.group(1))
53 | 
54 |     return results
55 | 


--------------------------------------------------------------------------------
/docs/_ext/botogramext.py:
--------------------------------------------------------------------------------
 1 | from pygments import style
 2 | from pygments import token
 3 | 
 4 | 
 5 | KEYWORD = "bold #179CDE"
 6 | STRING = "#00af00"
 7 | NAMES = "#000"
 8 | NUMBERS = "#8700ff"
 9 | COMMENTS = "#888"
10 | OPERATORS = "#444"
11 | 
12 | 
13 | class BotogramStyle(style.Style):
14 |     """Pygments style for the botogram documentation"""
15 |     background_color = "#fff"
16 |     highlight_color = "#f3f3f3"
17 |     default_style = ""
18 | 
19 |     styles = {
20 |         token.Whitespace:                "underline #f8f8f8",      # w
21 |         token.Error:                     "#a40000 border:#ef2929", # err
22 |         token.Other:                     NAMES,                # class x
23 | 
24 |         token.Comment:                   COMMENTS, # c
25 | 
26 |         token.Keyword:                   KEYWORD, # k
27 | 
28 |         token.Operator:                  OPERATORS, # o
29 |         token.Operator.Word:             KEYWORD, # ow
30 | 
31 |         token.Punctuation:               NAMES, # p
32 | 
33 |         token.Name:                      NAMES, # n
34 |         token.Name.Decorator:            "bold "+OPERATORS, # nd
35 |         token.Name.Entity:               "#ce5c00", # ni
36 |         token.Name.Tag:                  KEYWORD, # nt
37 | 
38 |         token.Number:                    NUMBERS, # m
39 | 
40 |         token.Literal:                   NAMES, # l
41 | 
42 |         token.String:                    STRING, # s
43 |         token.String.Doc:                COMMENTS, # sd
44 | 
45 |         token.Generic:                   NAMES,        # g
46 |         token.Generic.Deleted:           "#a40000",        # gd
47 |         token.Generic.Emph:              "italic #000000", # ge
48 |         token.Generic.Error:             "#ef2929",        # gr
49 |         token.Generic.Heading:           "bold #000080",   # gh
50 |         token.Generic.Inserted:          "#00A000",        # gi
51 |         token.Generic.Output:            "#888",           # go
52 |         token.Generic.Prompt:            "#745334",        # gp
53 |         token.Generic.Strong:            "bold #000000",   # gs
54 |         token.Generic.Subheading:        "bold #800080",   # gu
55 |         token.Generic.Traceback:         "bold #a40000",   # gt
56 |     }
57 | 


--------------------------------------------------------------------------------
/docs/_templates/links.html:
--------------------------------------------------------------------------------
 1 | 
18 | 


--------------------------------------------------------------------------------
/docs/_themes/botogram/layout.html:
--------------------------------------------------------------------------------
1 | {%- extends "pietroalbini/layout.html" -%}
2 | 
3 | {%- block theme_footer_extra -%}
4 |     
  • 5 | Documentation released under the 6 | MIT license 7 |
  • 8 | {%- endblock -%} 9 | -------------------------------------------------------------------------------- /docs/_themes/botogram/static/botogram.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Bitter'; 3 | font-style: normal; 4 | font-weight: 700; 5 | src: url('fonts/bitter.ttf') format('truetype'); 6 | } 7 | -------------------------------------------------------------------------------- /docs/_themes/botogram/static/fonts/bitter.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-botogram/botogram/ac30be0267dc2df1919ed781682eb45002b3884c/docs/_themes/botogram/static/fonts/bitter.ttf -------------------------------------------------------------------------------- /docs/_themes/botogram/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = pietroalbini 3 | stylesheet = botogram.css 4 | 5 | [options] 6 | accent_color = #179cde 7 | night_accent_color = #36eaff 8 | 9 | titles_font = Bitter 10 | -------------------------------------------------------------------------------- /docs/api/channels.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. _api-channels: 5 | 6 | ============ 7 | Channels API 8 | ============ 9 | 10 | The Channels API provides a lightweight way to interact with channels. If you 11 | just want to send messages to a channel, it's better if you use this. 12 | 13 | 14 | .. py:function:: botogram.channel(name, api_key) 15 | 16 | Create a new :py:class:`~botogram.Chat` object which points to the channel. 17 | You need to provide the channel name, prefixed with ``@``, and your bot's 18 | API key. Please refer to the :ref:`channels-preparation` 19 | section if you don't know how to get it. 20 | 21 | :param str name: The channel name. 22 | :param str api_key: Your bot's API key. 23 | :return: The corresponding Chat object. 24 | :rtype: botogram.Chat 25 | -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. _api: 5 | 6 | ============= 7 | API reference 8 | ============= 9 | 10 | This section will cover the full reference of botogram's public API. The 11 | API will be kept backward compatible for at least the next release, even if 12 | the development will be made trying to not break anything. 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | 17 | utility 18 | bot 19 | buttons 20 | channels 21 | inline 22 | components 23 | telegram 24 | -------------------------------------------------------------------------------- /docs/api/utility.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. api-utility:: 5 | 6 | ================================ 7 | Utility functions and decorators 8 | ================================ 9 | 10 | botogram ships with some functions and decorators aimed to simplify the bots 11 | development. Feel free to use them when you need them. 12 | 13 | 14 | .. py:function:: botogram.run(*bots[, workers=2]) 15 | 16 | This function allows you to run multiple bots in the same runner. You just 17 | need to provide all the bots you want to run, and the options you would 18 | normally provide to the bot's run method. 19 | 20 | Remember this function is blocking, so it prevents the execution of the code 21 | after the call until the runner is closed. Use a thread or a process if you 22 | want to execute things other than just running the runner. 23 | 24 | .. code-block:: python 25 | 26 | import botogram 27 | from file1 import bot as bot1 28 | from file2 import bot as bot2 29 | 30 | if __name__ == "__main__": 31 | botogram.run(bot1, bot2) 32 | 33 | :param botogram.Bot \*bots: The bots you want to run. 34 | :param int workers: The number of workers you want to use. 35 | 36 | .. py:function:: botogram.usernames_in(message) 37 | 38 | Returns a list of usernames contained in the message you provide. The 39 | function automatically excludes commands, email addresses and URLs. Remember 40 | that returned usernames aren't prefixed with a ``@``. 41 | 42 | :param str message: The message which contains the usernames. 43 | :return: The list of usernames contained in the message. 44 | :rtype: list of str 45 | 46 | 47 | .. py:decorator:: botogram.pass_shared 48 | 49 | This decorator does nothing currently. If you still use it, just remove all 50 | the references to it in your source code. Your bot will still work 51 | flawlessly. 52 | 53 | .. deprecated:: pre-0.1 it will be removed in botogram 1.0 54 | 55 | .. py:decorator:: botogram.pass_bot 56 | 57 | This decorator does nothing currently. If you still use it, just remove all 58 | the references to it in your source code. Your bot will still work 59 | flawlessly. 60 | 61 | .. deprecated:: pre-0.1 it will be removed in botogram 1.0 62 | 63 | .. py:decorator:: botogram.help_message_for(func) 64 | 65 | The return value of the decorated function will be treated as the help 66 | message of the function you pass to the decorator. The decorated function is 67 | called each time the help message is needed: this way you can create dynamic 68 | help messages, but it's advisable to cache the value if it's expensive to 69 | calculate it. 70 | 71 | :param callable func: The function which needs the help message. 72 | 73 | 74 | .. _picklable objects: https://docs.python.org/3/library/pickle.html#what-can-be-pickled-and-unpickled 75 | -------------------------------------------------------------------------------- /docs/bot-creation.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. _bot-creation: 5 | 6 | ========================= 7 | Create a new Telegram bot 8 | ========================= 9 | 10 | Before you start writing any code, you should create your bot on Telegram. This 11 | reserves your bot an username, and gives you the API key you need to control 12 | your bot. This chapter explains you how to do so. 13 | 14 | .. _bot-creation-naming: 15 | 16 | Choose a good username for your bot 17 | =================================== 18 | 19 | The username of your bot is really important: users will use it to tell their 20 | friends about your bot, and it will appear on telegram.me links. Also, you're 21 | not allowed to change username without recreating your bot, and so without 22 | losing users. 23 | 24 | Bot usernames must adhere to the following rules: 25 | 26 | * The username must be long at least five characters 27 | * The username can only contain letters, digits and underscores 28 | * The username must end with ``bot`` 29 | 30 | For example, all the following usernames are valid: ``my_awesome_bot``, 31 | ``SpamBot``, ``test123bot``. 32 | 33 | .. _bot-creation-botfather: 34 | 35 | Create the bot with @botfather 36 | ============================== 37 | 38 | Currently, you can only create a new bot... with another bot. With your 39 | Telegram client open, contact `@botfather`_, start it and execute the 40 | ``/newbot`` command. It will ask you some questions about your bot. 41 | 42 | Then it will give you an unique API key, which you can use to communicate with 43 | your bot. **Be sure to keep this key secret!** Everyone with your API key can 44 | take full control of your bot, and that's not a fun thing. 45 | 46 | .. _bot-creation-customization: 47 | 48 | Customize your bot 49 | ================== 50 | 51 | Other than allowing you to create it, `@botfather`_ also permits you to 52 | customize your bot. For example, you can use it to change your bot's avatar, 53 | its name, or its description. In order to see what you can do, just use the 54 | ``/help`` command on @botfather. Then execute the command for the thing you 55 | want to customize. 56 | 57 | If you want to use your bot just to manage a channel, you probably don't need 58 | to do this. 59 | 60 | .. _@botfather: https://telegram.me/botfather 61 | -------------------------------------------------------------------------------- /docs/bot-structure.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. _bot-structure: 5 | 6 | =========================== 7 | Structure of a botogram bot 8 | =========================== 9 | 10 | botogram isn't just a library you can integrate with your code, but it's a 11 | full framework which aims to help you creating bots, and to take full advantage 12 | of that you need to follow some simple rules while creating your bot. 13 | 14 | .. _bot-structure-skeleton: 15 | 16 | A bot's skeleton 17 | ================ 18 | 19 | Even if you write two completly different bots, you'll have the same 20 | boilerplate code in both of them. This is because you need to create the bot 21 | instance, and start the runner after you executed all of your bot's code. 22 | 23 | botogram tries to reduce the boilerplate code as much as possible, but some of 24 | it is needed anyway. Here there is all the required code: 25 | 26 | .. code-block:: python 27 | 28 | import botogram 29 | bot = botogram.create("YOUR-API-KEY") 30 | 31 | # Your code goes here 32 | 33 | if __name__ == "__main__": 34 | bot.run() 35 | 36 | As you can see, there are two required code blocks in botogram: 37 | 38 | * The first one imports botogram in your program, and creates a bot instance 39 | you can start to customize. 40 | 41 | * The second one executes the bot. The if clausole is needed only if you plan 42 | to run your bot on Windows, so if you're using Linux or OSX you can omit it. 43 | 44 | So, that's all the code required to create a bot with botogram. You can now 45 | start the actual bot creation! 46 | 47 | .. _bot-structure-hooks: 48 | 49 | Introduction to hooks 50 | ===================== 51 | 52 | Hooks are the way to go to add some logic to your bot: they allows you to react 53 | when an user calls a command, write something or a lot more situations. Hooks 54 | are added to your bot with decorated functions, or :ref:`with components 55 | `. 56 | 57 | For example, this is an hook which is executed when someone calls the ``/test`` 58 | command: 59 | 60 | .. code-block:: python 61 | 62 | @bot.command("test") 63 | def test_command(chat, message, args): 64 | # Your hook's code 65 | 66 | You can put any code you want in each hook, but remember hooks might be 67 | executed in different processes by the runner, so avoid using global variables. 68 | If you need to store global state check out :ref:`shared memory 69 | `. 70 | 71 | .. _bot-structure-hooks-args: 72 | 73 | Arguments for the command hook 74 | ============================== 75 | 76 | There's a special parameter called ``args``, which is used to get the list of arguments 77 | provided to the bot when the command is called. 78 | 79 | For example, this is an hook which is executed when someone calls the ``/hello`` 80 | command: 81 | 82 | .. code-block:: python 83 | 84 | @bot.command("hello") 85 | def hello_command(chat, message, args): 86 | chat.send("Hello " + ", ".join(args)) 87 | 88 | If you send ``/hello foo bot``, you will receive *Hello foo, bot* as a message. 89 | 90 | You can even decide which arguments are required by the command to work properly. 91 | 92 | For example, this is a command hook which requires two arguments (``from_who`` & ``to_wbo``): 93 | 94 | .. code-block:: python 95 | 96 | @bot.command("say_hello") 97 | def say_hello_command(chat, from_who: str, to_who: str): 98 | chat.send("%s says hello to %s!" % (from_who, to_who)) 99 | 100 | If you send ``/say_hello foo bot``, you will receive *foo says hello to bot* as a message. 101 | 102 | As you can see, you can use *type annotations*! If you use them, the bot will automatically 103 | convert the arguments to the provided types. 104 | 105 | You can use *default arguments* as well. 106 | 107 | .. versionchanged:: 0.7 108 | 109 | Added the support for optional arguments and type annotations. 110 | 111 | Dynamic hooks arguments 112 | ======================= 113 | 114 | Hooks are usually called with a lot of useful information, but you don't need 115 | all of it every time. For example, you might not need shared memory in a whole 116 | bot, but you have to use it everytime in another. 117 | 118 | In order to avoid having to write everytime a really long list of arguments, 119 | botogram is smart enought to figure out what you need and provide only that. 120 | So, if in a command you just need the message and the shared memory, you can 121 | define your hook this way: 122 | 123 | .. code-block:: python 124 | 125 | @bot.command("test") 126 | def test_command(message, shared): 127 | # Your hook's code 128 | 129 | In addition to the arguments provided by each hook, botogram allows you to 130 | request those additional arguments: 131 | 132 | * **bot**, which is an instance of the current bot. 133 | 134 | * **shared**, which is an instance of your bot/component's :ref:`shared memory 135 | `. 136 | -------------------------------------------------------------------------------- /docs/buildthedocs.yml: -------------------------------------------------------------------------------- 1 | versions: 2 | 3 | - name: "dev" 4 | source: 5 | provider: local 6 | path: docs 7 | directory: . 8 | title: Development 9 | notice: unstable 10 | warning: This documentation is for the unstable and in-development version of botogram! 11 | 12 | - name: "0.6.1" 13 | source: 14 | provider: git 15 | url: . 16 | checkout: v0.6.1 17 | directory: docs 18 | title: botogram 0.6.1 19 | notice: alpha 20 | warning: null 21 | 22 | 23 | - name: "0.5" 24 | source: 25 | provider: git 26 | url: . 27 | checkout: v0.5 28 | directory: docs 29 | title: botogram 0.5 30 | notice: alpha 31 | warning: null 32 | 33 | 34 | - name: "0.4" 35 | source: 36 | provider: git 37 | url: . 38 | checkout: v0.4 39 | directory: docs 40 | title: botogram 0.4 41 | notice: alpha 42 | warning: null 43 | -------------------------------------------------------------------------------- /docs/changelog/0.1.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | =========================== 5 | Changelog of botogram 0.1.x 6 | =========================== 7 | 8 | Here you can find all the changes in the botogram 0.1.x releases. 9 | 10 | .. _changelog-0.1.2: 11 | 12 | botogram 0.1.2 13 | ============== 14 | 15 | *Bugfix release, released on February 25th, 2016* 16 | 17 | * Add a way to disable the syntax detector (`issue 27`_) 18 | * Fix automatic syntax detector recognizing markdown in URLs (`issue 28`_) 19 | 20 | .. _issue 27: https://github.com/pietroalbini/botogram/issues/27 21 | .. _issue 28: https://github.com/pietroalbini/botogram/issues/28 22 | 23 | .. _changelog-0.1.1: 24 | 25 | botogram 0.1.1 26 | ============== 27 | 28 | *Bugfix release, released on February 21th, 2016* 29 | 30 | * Fix automatic syntax detector not working sometimes (`issue 26`_) 31 | * Fix "unknown command" message not showing up in private chats (`issue 25`_) 32 | 33 | .. _issue 25: https://github.com/pietroalbini/botogram/issues/25 34 | .. _issue 26: https://github.com/pietroalbini/botogram/issues/26 35 | 36 | .. _changelog-0.1: 37 | 38 | botogram 0.1 39 | ============ 40 | 41 | *Alpha release, released on February 18th, 2016* 42 | 43 | This is the initial alpha release of botogram. 44 | -------------------------------------------------------------------------------- /docs/changelog/0.2.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | =========================== 5 | Changelog of botogram 0.2.x 6 | =========================== 7 | 8 | Here you can find all the changes in the botogram 0.2.x releases. 9 | 10 | .. _changelog-0.2.2: 11 | 12 | botogram 0.2.2 13 | ============== 14 | 15 | *Bugfix release, released on July 2nd, 2016* 16 | 17 | * Fix botogram crashing if someone edits a message (`issue 70`_) 18 | 19 | .. _issue 70: https://github.com/pietroalbini/botogram/issues/70 20 | 21 | .. _changelog-0.2.1: 22 | 23 | botogram 0.2.1 24 | ============== 25 | 26 | *Bugfix release, released on March 31th, 2016* 27 | 28 | * Fix ``/help`` command crash if using markdown bits in the docstring (`issue 29 | 51`_) 30 | 31 | .. _issue 51: https://github.com/pietroalbini/botogram/issues/51 32 | 33 | .. _changelog-0.2: 34 | 35 | botogram 0.2 36 | ============= 37 | 38 | *Alpha release, released on March 27th, 2016* 39 | 40 | botogram 0.2 is the second alpha release of botogram. It features an increased 41 | support for the upstream Telegram API, and also some bugfixes here and there. 42 | 43 | This release also does some cleanup in the API, providing better methods and 44 | deprecating the old ones. The deprecated methods will be available until 45 | botogram 1.0, and warnings are in place to notify you where to change what. 46 | 47 | New features 48 | ------------ 49 | 50 | * Added the ability to send messages without notifying the user 51 | 52 | * New argument ``notify`` on multiple methods of :py:class:`botogram.User` 53 | * New argument ``notify`` on multiple methods of :py:class:`botogram.Chat` 54 | * New argument ``notify`` on multiple methods of :py:class:`botogram.Message` 55 | * New argument ``notify`` on multiple methods of :py:class:`botogram.Bot` 56 | 57 | * Added the ability to send stickers 58 | 59 | * New method :py:meth:`botogram.User.send_sticker` 60 | * New method :py:meth:`botogram.Chat.send_sticker` 61 | * New method :py:meth:`botogram.Bot.send_sticker` 62 | * New method :py:meth:`botogram.Message.reply_with_sticker` 63 | 64 | * Added the :py:attr:`botogram.User.name` computed attribute 65 | * Added the :py:attr:`botogram.Chat.name` computed attribute 66 | * Added the :py:attr:`botogram.User.avatar` attribute 67 | * Added the :py:meth:`botogram.User.avatar_history` method 68 | 69 | Changes 70 | ------- 71 | 72 | * Renamed ``Message.from_`` to :py:attr:`botogram.Message.sender` 73 | * Renamed ``Bot.init_shared_memory`` to :py:meth:`botogram.Bot.prepare_memory` 74 | * Renamed ``Component.add_shared_memory_initializer`` to 75 | :py:meth:`botogram.Component.add_memory_preparer` 76 | * Changed default messages to include rich formatting 77 | 78 | Bug fixes 79 | --------- 80 | 81 | * Fix the syntax detector checking URLs with dashes in the domain (`issue 32`_) 82 | * Fix the syntax detector checking only the first line of a message (`issue 83 | 40`_) 84 | * Fix inability to send messages to channels from a running bot (`issue 35`_) 85 | * Fix inability to download stickers (`issue 36`_) 86 | * Fix commands with newlines in the arguments not recognized as such (`issue 87 | 41`_) 88 | * Remove empty items from the commands' arguments (`issue 42`_) 89 | 90 | Deprecated features 91 | ------------------- 92 | 93 | Deprecated features will be removed in botogram 1.0! 94 | 95 | * ``Message.from_`` is now deprecated 96 | * ``Bot.init_shared_memory`` is now deprecated 97 | * ``Component.add_shared_memory_initializer`` is now deprecated 98 | 99 | .. _issue 32: https://github.com/pietroalbini/botogram/issues/32 100 | .. _issue 35: https://github.com/pietroalbini/botogram/issues/35 101 | .. _issue 36: https://github.com/pietroalbini/botogram/issues/36 102 | .. _issue 40: https://github.com/pietroalbini/botogram/issues/40 103 | .. _issue 41: https://github.com/pietroalbini/botogram/issues/41 104 | .. _issue 42: https://github.com/pietroalbini/botogram/issues/42 105 | -------------------------------------------------------------------------------- /docs/changelog/0.4.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | =========================== 5 | Changelog of botogram 0.4.x 6 | =========================== 7 | 8 | Here you can find all the changes in the botogram 0.4.x releases. 9 | 10 | .. _changelog-0.4.1: 11 | 12 | botogram 0.4.1 13 | ============== 14 | 15 | *Alpha release, not yet released.* 16 | 17 | Release description not yet written. 18 | 19 | Bug fixes 20 | --------- 21 | 22 | * Accounts deleted by inactivity incorrectly raised an APIError instead of a ChatUnavailableError 23 | 24 | .. _changelog-0.4: 25 | 26 | botogram 0.4 27 | ============ 28 | 29 | *Alpha release, not yet released.* 30 | 31 | botogram 0.4 is the fourth alpha release of botogram. It adds support for 32 | buttons and a few new APIs added in the meantime by Telegram. It also includes 33 | big performance improvements and a few deprecations to keep the API clean. 34 | 35 | New features 36 | ------------ 37 | 38 | * Added support for :ref:`buttons and callbacks ` 39 | 40 | * New attribute :py:attr:`botogram.Bot.validate_callback_signatures` 41 | * New class :py:class:`botogram.Buttons` 42 | * New class :py:class:`botogram.ButtonsRow` 43 | * New class :py:class:`botogram.CallbackQuery` 44 | * New decorator :py:meth:`botogram.Bot.callback` 45 | * New method :py:meth:`botogram.Component.add_callback` 46 | 47 | * Added support for receiving messages sent to channels 48 | 49 | * New decorator :py:meth:`botogram.Bot.channel_post` 50 | * New decorator :py:meth:`botogram.Bot.channel_post_edited` 51 | * New method :py:meth:`botogram.Component.add_channel_post_hook` 52 | * New method :py:meth:`botogram.Component.add_channel_post_edited_hook` 53 | 54 | * Added ability to disable the link preview in ``/help``. 55 | 56 | * New parameter :py:attr:`botogram.Bot.link_preview_in_help` 57 | 58 | * Added ability to reorder commands in ``/help``. 59 | 60 | * New argument ``order`` in :py:meth:`botogram.Bot.command` 61 | * New argument ``order`` in :py:meth:`botogram.Component.add_command` 62 | 63 | * Added ability to delete messages 64 | 65 | * New method :py:meth:`botogram.User.delete_message` 66 | * New method :py:meth:`botogram.Chat.delete_message` 67 | * New method :py:meth:`botogram.Message.delete` 68 | 69 | * Added the ``attach`` argument to all the send methods. 70 | 71 | * New argument ``attach`` on multiple methods of :py:class:`botogram.User` 72 | * New argument ``attach`` on multiple methods of :py:class:`botogram.Chat` 73 | * New argument ``attach`` on multiple methods of :py:class:`botogram.Message` 74 | 75 | * Added ability to edit message attachments 76 | 77 | * New method :py:meth:`botogram.Message.edit_attach` 78 | 79 | * Added new attributes on the :py:class:`~botogram.Message` object: 80 | 81 | * New attribute :py:attr:`botogram.Message.channel_post_author` 82 | * New attribute :py:attr:`botogram.Message.forward_from_message_id` 83 | 84 | Performance improvements 85 | ------------------------ 86 | 87 | * botogram now tries to reuse existing connections to Telegram when possible 88 | 89 | Bug fixes 90 | --------- 91 | 92 | * Fix inability to fetch updates and stop the runner after an internet 93 | connection outage. 94 | * Fix :py:attr:`botogram.Message.forward_from` giving wrong information with 95 | signed channel posts (`issue 80`_) 96 | 97 | .. _issue 80: https://github.com/python-botogram/botogram/issues/80 98 | 99 | 100 | Deprecated features 101 | ------------------- 102 | 103 | Deprecated features will be removed in botogram 1.0! 104 | 105 | * The ``extra`` attribute on all the send methods is now deprecated 106 | -------------------------------------------------------------------------------- /docs/changelog/0.5.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | =========================== 5 | Changelog of botogram 0.5.x 6 | =========================== 7 | 8 | Here you can find all the changes in the botogram 0.5.x releases. 9 | 10 | .. _changelog-0.5: 11 | 12 | botogram 0.5 13 | ============ 14 | 15 | *Alpha release, released on February 15th, 2018.* 16 | 17 | Release description not yet written. 18 | 19 | New features 20 | ------------ 21 | 22 | * Added Brazilian Portuguese translation 23 | * Added Portuguese translation 24 | * Added ability to override default i18n messages 25 | 26 | * New attribute :py:attr:`botogram.Bot.override_i18n` 27 | 28 | * Added support for captions in audios, files and voices 29 | 30 | * New argument ``caption`` in :py:meth:`botogram.Chat.send_audio` 31 | * New argument ``caption`` in :py:meth:`botogram.Chat.send_file` 32 | * New argument ``caption`` in :py:meth:`botogram.Chat.send_voice` 33 | * New argument ``caption`` in :py:meth:`botogram.User.send_audio` 34 | * New argument ``caption`` in :py:meth:`botogram.User.send_file` 35 | * New argument ``caption`` in :py:meth:`botogram.User.send_voice` 36 | 37 | * Added support for sending media by ID or URL 38 | 39 | * New arguments ``file_id`` and ``url`` in :py:meth:`botogram.Chat.send_photo` 40 | * New arguments ``file_id`` and ``url`` in :py:meth:`botogram.User.send_photo` 41 | * New arguments ``file_id`` and ``url`` in :py:meth:`botogram.Chat.send_audio` 42 | * New arguments ``file_id`` and ``url`` in :py:meth:`botogram.User.send_audio` 43 | * New arguments ``file_id`` and ``url`` in :py:meth:`botogram.Chat.send_voice` 44 | * New arguments ``file_id`` and ``url`` in :py:meth:`botogram.User.send_voice` 45 | * New arguments ``file_id`` and ``url`` in :py:meth:`botogram.Chat.send_video` 46 | * New arguments ``file_id`` and ``url`` in :py:meth:`botogram.User.send_video` 47 | * New arguments ``file_id`` and ``url`` in :py:meth:`botogram.Chat.send_file` 48 | * New arguments ``file_id`` and ``url`` in :py:meth:`botogram.User.send_file` 49 | * New arguments ``file_id`` and ``url`` in :py:meth:`botogram.Chat.send_photo` 50 | * New arguments ``file_id`` and ``url`` in :py:meth:`botogram.User.send_photo` 51 | 52 | Bug fixes 53 | --------- 54 | 55 | * Fixed invalid callbacks sent to chats different than the current one 56 | -------------------------------------------------------------------------------- /docs/changelog/0.7.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | =========================== 5 | Changelog of botogram 0.7.x 6 | =========================== 7 | 8 | Here you can find all the changes in the botogram 0.7.x releases. 9 | 10 | .. _changelog-0.7: 11 | 12 | botogram 0.7 13 | ============ 14 | 15 | *Alpha release, not yet released.* 16 | 17 | Release description not yet written. 18 | 19 | New features 20 | ------------ 21 | 22 | 23 | * Added support for inline mode 24 | 25 | * New decorator :py:decoratormethod:`botogram.Bot.inline` 26 | * New decorator :py:decoratormethod:`botogram.Bot.inline_feedback` 27 | * New attribute :py:attr:`botogram.Update.inline_query` 28 | * New attribute :py:attr:`botogram.Update.chosen_inline_result` 29 | * New :py:class:`botogram.InlineQuery` class 30 | * New :py:class:`botogram.InlineFeedback` class 31 | * New :py:class:`botogram.InlineInputMessage` class 32 | * New :py:class:`botogram.InlineInputLocation` class 33 | * New :py:class:`botogram.InlineInputVenue` class 34 | * New :py:class:`botogram.InlineInputContact` class 35 | * New :py:class:`botogram.InlineMessageUnsupported` 36 | * New attribute :py:attr:`botogram.Message.is_inline` 37 | * New attribute :py:attr:`botogram.Message.inline_message_id` 38 | 39 | * Added support for animations (GIFs) 40 | 41 | * New :py:class:`botogram.Animation` class 42 | * New attribute :py:attr:`botogram.Message.animation` 43 | * New method :py:meth:`botogram.Chat.send_gif` 44 | * New method :py:meth:`botogram.User.send_gif` 45 | * New method :py:meth:`botogram.Message.reply_with_gif` 46 | 47 | * Added support for polls 48 | 49 | * New :py:class:`botogram.Poll` class 50 | * New :py:class:`botogram.PollOption` class 51 | * New method :py:meth:`botogram.Chat.send_poll` 52 | * New method :py:meth:`botogram.Message.reply_with_poll` 53 | * New method :py:meth:`botogram.Message.stop_poll` 54 | 55 | * Added support for thumbs 56 | 57 | * New argument ``thumb`` in :py:meth:`botogram.Chat.send_audio` 58 | * New argument ``thumb`` in :py:meth:`botogram.Chat.send_video` 59 | * New argument ``thumb`` in :py:meth:`botogram.Chat.send_video_note` 60 | * New argument ``thumb`` in :py:meth:`botogram.Chat.send_file` 61 | * New argument ``thumb`` in :py:meth:`botogram.User.send_audio` 62 | * New argument ``thumb`` in :py:meth:`botogram.User.send_video` 63 | * New argument ``thumb`` in :py:meth:`botogram.User.send_video_note` 64 | * New argument ``thumb`` in :py:meth:`botogram.User.send_file` 65 | * New argument ``thumb`` in :py:meth:`botogram.Message.reply_with_audio` 66 | * New argument ``thumb`` in :py:meth:`botogram.Message.reply_with_video` 67 | * New argument ``thumb`` in :py:meth:`botogram.Message.reply_with_video_note` 68 | * New argument ``thumb`` in :py:meth:`botogram.Message.reply_with_file` 69 | 70 | * Enhancements in message forwarding 71 | 72 | * New return type for :py:attr:`Message.forward_from` 73 | * New attribute :py:attr:`Message.forward_hidden` 74 | * New attribute :py:attr:`Message.forward_signature` 75 | 76 | * Added support for live locations 77 | 78 | * New parameter `live_period` for :py:meth:`Chat.send_location` and :py:meth:`User.send_location` 79 | * New parameter `live_period` for :py:meth:`Message.reply_with_location` 80 | * New method :py:meth:`Message.edit_live_location` 81 | * New method :py:meth:`Message.stop_live_location` 82 | 83 | * Added support for editing, getting and removing the chat photo 84 | 85 | * New :py:class:`~botogram.ChatPhoto` class 86 | * New method :py:meth:`Chat.set_photo` 87 | * New method :py:meth:`Chat.remove_photo` 88 | * New attribute :py:attr:`Chat.photo` 89 | 90 | * Added support for animated stickers 91 | 92 | * New attribute :py:attr:`botogram.Sticker.is_animated` 93 | 94 | * Added support for vcards in contacts 95 | * New argument ``vcard`` in :py:meth:`botogram.Chat.send_contact` 96 | * New argument ``vcard`` in :py:meth:`botogram.User.send_contact` 97 | * New argument ``vcard`` in :py:meth:`botogram.Message.reply_with_contact` 98 | * New attribute :py:attr:`Contact.vcard` 99 | 100 | * Added automatic type conversion for command arguments 101 | 102 | Bug fixes 103 | --------- 104 | 105 | * Fixed :py:meth:`botogram.Message.edit_attach` to work with inline callbacks 106 | -------------------------------------------------------------------------------- /docs/changelog/index.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. _changelog: 5 | 6 | =================== 7 | botogram changelogs 8 | =================== 9 | 10 | Here you can see what changed in every botogram release. 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | 15 | 0.6 16 | 0.5 17 | 0.4 18 | 0.3 19 | 0.2 20 | 0.1 21 | -------------------------------------------------------------------------------- /docs/channels.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. _channels: 5 | 6 | ===================== 7 | Working with channels 8 | ===================== 9 | 10 | Telegram Channels provides a way to broadcast a message to multiple users. You 11 | can manually send messages to them with your preferred Telegram client, but 12 | sometimes you would want to send them with a script, or from a bot. 13 | 14 | With botogram you can easily do that, without even the need to run the bot. 15 | 16 | .. _channels-preparation: 17 | 18 | Preparation 19 | =========== 20 | 21 | Before you can start working with channels, you need to create a bot which will 22 | be able to send messages to your channel. If you haven't done that already, 23 | check the :ref:`bot creation ` chapter of this documentation. 24 | 25 | After that you need to allow your bot to send messages to your channel. Open 26 | your favorite Telegram client, and go to the administrators' section of your 27 | channel. From there you should add your bot, and then you're ready. 28 | 29 | .. _channels-standalone: 30 | 31 | Manage without a full bot 32 | ========================= 33 | 34 | Because a bot's initialization is quite an heavy process, you can use a 35 | lightweight API to just interact with channels. First of all you should import 36 | botogram, and then call the :py:func:`botogram.channel` function to get a 37 | channel object: 38 | 39 | .. code-block:: python 40 | 41 | import botogram 42 | 43 | chan = botogram.channel("@my_channel", "YOUR_API_KEY") 44 | 45 | You need to replace ``@my_channel`` with your channel's public name, and 46 | ``YOUR_API_KEY`` with the key you got before. Then you can use all the methods 47 | of the :py:class:`~botogram.Chat` object with the instance you are returned. 48 | For example, if you want to send a text message you should do: 49 | 50 | .. code-block:: python 51 | :emphasize-lines: 2 52 | 53 | chan = botogram.channel("@my_channel", "YOUR_API_KEY") 54 | chan.send("Hello world") 55 | 56 | .. _channels-bot: 57 | 58 | Manage from a running bot 59 | ========================= 60 | 61 | If you want to control a channel from your bot, you can use all the methods 62 | which sends messages with the channel name as the user ID. For example, if you 63 | want to forward all the messages your bot receives to the ``@my_channel`` 64 | channel, you can do something like this: 65 | 66 | .. code-block:: python 67 | 68 | @bot.process_message 69 | def forward_messages(chat, message): 70 | message.forward_to("@my_channel") 71 | 72 | If instead you want to announce to the ``@my_channel`` channel when someone 73 | cites botogram in a chat, you can do this: 74 | 75 | .. code-block:: python 76 | 77 | @bot.message_contains("botogram") 78 | def we_are_famous(bot, chat, message): 79 | user = "Someone" 80 | if message.sender.username is not None: 81 | user = message.sender.username 82 | 83 | bot.chat("@my_channel").send("%s mentioned botogram!" % user) 84 | 85 | .. _@botfather: https://telegram.me/botfather 86 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | # DEALINGS IN THE SOFTWARE. 21 | 22 | import sys 23 | import os 24 | import shlex 25 | 26 | import pietroalbini_sphinx_themes 27 | 28 | sys.path.append(os.path.abspath('_ext')) 29 | 30 | extensions = [] 31 | templates_path = ["_templates"] 32 | source_suffix = ".rst" 33 | master_doc = "index" 34 | 35 | project = "botogram" 36 | copyright = "2015-2019 The Botogram Authors" 37 | author = "Botogram-dev" 38 | 39 | version = "0.6" 40 | release = "0.6" 41 | 42 | language = None 43 | 44 | exclude_patterns = ["_build"] 45 | 46 | #pygments_style = "botogramext.BotogramStyle" 47 | 48 | todo_include_todos = False 49 | 50 | 51 | ## HTML output 52 | 53 | html_theme = "botogram" 54 | html_theme_path = ["_themes", pietroalbini_sphinx_themes.themes_path()] 55 | html_static_path = ["_static"] 56 | 57 | html_sidebars = { 58 | "**": ["links.html"], 59 | } 60 | #html_additional_pages = {} 61 | #html_domain_indices = True 62 | #html_use_index = True 63 | #html_split_index = False 64 | #html_show_sourcelink = True 65 | #html_show_sphinx = True 66 | #html_show_copyright = True 67 | #html_search_language = "en" 68 | #html_search_options = {"type": "default"} 69 | #html_search_scorer = "scorer.js" 70 | 71 | htmlhelp_basename = "botogramdoc" 72 | 73 | 74 | ## Latex output 75 | 76 | latex_elements = { 77 | # "papersize": "letterpaper", 78 | # "pointsize": "10pt", 79 | # "preamble": "", 80 | # "figure_align": "htbp", 81 | } 82 | 83 | latex_documents = [ 84 | (master_doc, "botogram.tex", "botogram Documentation", 85 | "Pietro Albini", "manual"), 86 | ] 87 | 88 | #latex_logo = None 89 | #latex_use_parts = False 90 | #latex_show_pagerefs = False 91 | #latex_show_urls = False 92 | #latex_appendices = [] 93 | #latex_domain_indices = True 94 | 95 | 96 | ## Manpages output 97 | 98 | man_pages = [ 99 | (master_doc, "botogram", "botogram Documentation", 100 | [author], 1) 101 | ] 102 | #man_show_urls = False 103 | 104 | 105 | ## Texinfo output 106 | 107 | texinfo_documents = [ 108 | (master_doc, "botogram", "botogram Documentation", 109 | author, "botogram", "One line description of project.", 110 | "Miscellaneous"), 111 | ] 112 | #texinfo_appendices = [] 113 | #texinfo_domain_indices = True 114 | #texinfo_show_urls = "footnote" 115 | #texinfo_no_detailmenu = False 116 | -------------------------------------------------------------------------------- /docs/deploy/common.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. _deploy-common: 5 | 6 | ============================= 7 | Common deployment information 8 | ============================= 9 | 10 | Even if different deployment techniques are *really* different, all of them 11 | have two essential things in common: server security and application 12 | configuration. 13 | 14 | .. _deploy-common-security: 15 | 16 | Server security considerations 17 | ============================== 18 | 19 | In a perfect world, you wouldn't need to worry about securing your servers, 20 | because no one would attempt to break them. Unfortunately, we don't live in 21 | that world, so you need to protect your server from the ones who love breaking 22 | others' things. 23 | 24 | * **You need to have experience in servers management**. Servers aren't easy to 25 | configure (especially if you want to do it in a secure way) and maintain: be 26 | sure to know how to do that before diving into servers management. It's the 27 | wild west out there. 28 | 29 | * **Never run anything from root**. Even if (hopefully) 30 | there are no security vulnerabilities in botogram, it's always better to run 31 | any service from another user. This way if a service is compromised the 32 | attacker won't be able to do too much damage on your system. 33 | 34 | * **Keeping services separated is a good idea**. In order to restrict even more 35 | what an hypothetical attacker can do, you can create a separate user for 36 | *each* service you have on your server. This way an attacker can't even 37 | compromise other applications. 38 | 39 | * **Restrict what services can do**. Allowing a bot to 40 | SSH other servers isn't a good idea, right? botogram by default only needs to 41 | communicate to ``api.telegram.org`` with HTTPS. You can block everything else 42 | your bot doesn't use. 43 | 44 | .. _deploy-common-multiple: 45 | 46 | Running multiple bots in the same process 47 | ========================================= 48 | 49 | A botogram runner uses system resources, even if the bot is doing nothing. With 50 | only one bot this isn't a problem, but if you have a lot of small bots with low 51 | traffic (for example bots only for a single group chat) this might become 52 | annoying. 53 | 54 | The solution is to run multiple bots in a single runner, since adding bots to a 55 | runner is way more inexpensive than creating a runner per bot. Be warned 56 | that if you add too many bots to a single runner, the workers might not be able 57 | to keep up with the incoming requests. 58 | 59 | In order to make this happen, you need to have each bot in an importable 60 | Python file, and then create a file which calls :py:func:`botogram.run` with 61 | the bots' instances. For example, if you have two bots in ``mybot1.py`` and 62 | ``mybot2.py``, and the bot instance in all of them is called ``bot``, you can 63 | use this script to run both of them in a single runner: 64 | 65 | .. code-block:: python 66 | 67 | import botogram 68 | from mybot1 import bot as bot1 69 | from mybot2 import bot as bot2 70 | 71 | if __name__ == "__main__": 72 | botogram.run(bot1.bot, bot2.bot) 73 | -------------------------------------------------------------------------------- /docs/deploy/gnu-screen.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. _deploy-screen: 5 | 6 | ================================ 7 | Quick deployment with GNU screen 8 | ================================ 9 | 10 | `GNU screen`_ is an handy tool which lets you create shell sessions detached 11 | from your current session. This means they will keep running even if you log 12 | out from the server. With it, you're able to run your bot in the exact same 13 | way you're running it in your development machine, without worrying about 14 | anything else. 15 | 16 | .. warning:: 17 | 18 | This deployment method is **not** recommended for real-world deployments, 19 | because it doesn't try to keep alive your bot if it crashes. It's explained 20 | here because this method is useful if you just want to test how the bot runs 21 | on the server. 22 | 23 | The first thing you need to do is to install the tool. On Debian/Ubuntu you can 24 | execute the following command (from **root**):: 25 | 26 | $ apt-get install screen 27 | 28 | Instead, on CentOS/Fedora you need to run this command (from **root**):: 29 | 30 | $ yum install screen 31 | 32 | Perfect, you now have GNU screen installed. You can create a new screen with 33 | the following command (replace ``screen_name`` with the name you want to assign 34 | to the screen):: 35 | 36 | $ screen -S screen_name 37 | 38 | And you're into your screen. Now, :ref:`install botogram ` (if you 39 | didn't do that before) and run your bot in it, as you would do on your local 40 | machine. You can exit the screen anytime by pressing ``CTRL+A`` and then ``D``. 41 | If you want to resume it later, run the following command:: 42 | 43 | $ screen -x screen_name 44 | 45 | Remember that if you don't enable the multiuser feature, you can't attach to 46 | screens of other users, so be sure to be logged in with the same account as you 47 | started the screen. 48 | 49 | .. _GNU screen: https://www.gnu.org/software/screen/ 50 | -------------------------------------------------------------------------------- /docs/deploy/index.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. _deploy: 5 | 6 | =============== 7 | Deploy your bot 8 | =============== 9 | 10 | Well, you've created your first bot. It works perfectly, your friends are 11 | loving it, and you are happy about what you've done. The problem is, you 12 | can't keep it running on your computer. You need to deploy it to a server. 13 | 14 | This section will teach you how to do that on a Linux server, in a secure and 15 | reliable way. It will also explain how to quickly run an instance of it, 16 | without a full-fledged deployment. Even if this guide exists, be sure to have a 17 | bit of experience on managing Linux servers. 18 | 19 | .. toctree:: 20 | :maxdepth: 2 21 | 22 | common 23 | gnu-screen 24 | supervisord 25 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. _index: 5 | 6 | ====================== 7 | botogram documentation 8 | ====================== 9 | 10 | *Just focus on your bots.* 11 | 12 | botogram is a Python framework, which allows you to focus just on creating your 13 | `Telegram bots`_, without worrying about the underlying Bots API. 14 | 15 | While most of the libraries for Telegram out there just wrap the Bots API, 16 | botogram focuses heavily on the development experience, aiming to provide you 17 | the best API possible. Most of the Telegram implementation details are managed 18 | by botogram, so you can just focus on your bot. 19 | 20 | botogram is released under the MIT license. 21 | 22 | :: 23 | 24 | import botogram 25 | bot = botogram.create("YOUR-API-KEY") 26 | 27 | @bot.command("hello") 28 | def hello_command(chat, message, args): 29 | """Say hello to the world!""" 30 | chat.send("Hello world") 31 | 32 | if __name__ == "__main__": 33 | bot.run() 34 | 35 | .. _index-introduction: 36 | 37 | Introduction to botogram 38 | ======================== 39 | 40 | .. toctree:: 41 | :maxdepth: 2 42 | 43 | install 44 | quickstart/index 45 | tricks 46 | 47 | .. _index-narrative: 48 | 49 | Narrative documentation 50 | ======================= 51 | 52 | .. toctree:: 53 | :maxdepth: 2 54 | 55 | bot-creation 56 | bot-structure 57 | unavailable-chats 58 | groups-management 59 | buttons 60 | inline 61 | shared-memory 62 | tasks 63 | custom-components 64 | deploy/index 65 | channels 66 | i18n 67 | 68 | .. _index-reference: 69 | 70 | Reference 71 | ========= 72 | 73 | .. toctree:: 74 | :maxdepth: 2 75 | 76 | api/index 77 | 78 | .. _index-notes: 79 | 80 | Side notes 81 | ========== 82 | 83 | .. toctree:: 84 | :maxdepth: 2 85 | 86 | changelog/index 87 | license 88 | 89 | .. _Telegram bots: https://core.telegram.org/bots 90 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. _install: 5 | 6 | ======================== 7 | Installation of botogram 8 | ======================== 9 | 10 | botogram is available `on the Python Packages Index`_, so you can install it 11 | really easily with the `pip`_ command-line utility. Before installing it, be 12 | sure to have Python_ 3.4 (or a newer version), pip_, virtualenv_ and 13 | setuptools_ installed on your system. Then, issue the following command:: 14 | 15 | $ python3 -m pip install botogram2 16 | 17 | Perfect, botogram is now installed! Now, you can follow the 18 | ":ref:`tutorial`" chapter if you want to create a bot right now! 19 | 20 | .. _install-edge: 21 | 22 | Living on the edge 23 | ================== 24 | 25 | If you don't mind having some instability or bugs, and you want the latest 26 | features not yet released, you can clone the `botogram git repository`_, 27 | install `virtualenv`_, `invoke`_ and execute the installation from source:: 28 | 29 | $ git clone https://github.com/python-botogram/botogram.git 30 | $ cd botogram 31 | $ invoke install 32 | 33 | Remember that something can change without notice, and even be removed, until 34 | the feature is released, so don't use a non-released version in production. 35 | 36 | .. _install-venvs: 37 | 38 | About virtual environments 39 | ========================== 40 | 41 | Installing Python packages globally isn't a good practice. A large part of the 42 | Python community solves this problem with a tool called virtualenv_, which 43 | creates a small, isolated Python installation for each project. This allows 44 | you to experiment in a project without affecting other ones. 45 | 46 | In order to create a virtual environment, first of all you need to `install 47 | virtualenv`_. Next, you can issue the following command to create a virtual 48 | environment in ``env/``:: 49 | 50 | $ virtualenv -p python3 env 51 | 52 | So, you've now a virtual environment with Python 3 in it. In order to activate 53 | it, you can source into the terminal the ``bin/activate`` script:: 54 | 55 | $ source env/bin/activate 56 | 57 | Now, everything you install with pip will be confined in that environment. 58 | This means you need to enter the virtual environment every time you want to 59 | run the script. When you're done working in the virtual environment, you can 60 | exit by calling the ``deactivate`` command:: 61 | 62 | $ deactivate 63 | 64 | .. _install-troubleshooting: 65 | 66 | Troubleshooting 67 | =============== 68 | 69 | You might encounter some errors while installing botogram. Here is explained how 70 | to fix the most recurring ones: 71 | 72 | Insufficient permissions 73 | ------------------------ 74 | 75 | On some linux systems, you usually don't have enough privileges to install 76 | something globally. In this case, you can ask your system administrator to 77 | execute the above command, or you can wrap the command with sudo, if you 78 | are allowed to do so:: 79 | 80 | $ sudo python3 -m pip install botogram2 81 | 82 | If you installed from source, you need to use this command instead of the last 83 | one:: 84 | 85 | $ sudo invoke install 86 | 87 | .. _on the Python Packages Index: https://pypi.python.org/pypi/botogram 88 | .. _pip: https://pip.pypa.io 89 | .. _Python: https://www.python.org 90 | .. _setuptools: https://setuptools.pypa.io 91 | .. _botogram git repository: https://github.com/pietroalbini/botogram 92 | .. _virtualenv: https://virtualenv.pypa.io 93 | .. _invoke: https://www.pyinvoke.org 94 | .. _install virtualenv: https://virtualenv.pypa.io/en/latest/installation.html 95 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. _license: 5 | 6 | =============== 7 | The MIT license 8 | =============== 9 | 10 | botogram is released under the MIT license. This means you can use it as you 11 | want, change it and redistribute it, but you must keep the following copyright 12 | notice: 13 | 14 | :: 15 | 16 | Copyright (c) 2015-2019 Pietro Albini 17 | 18 | Permission is hereby granted, free of charge, to any person obtaining a copy 19 | of this software and associated documentation files (the "Software"), to 20 | deal in the Software without restriction, including without limitation the 21 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 22 | sell copies of the Software, and to permit persons to whom the Software is 23 | furnished to do so, subject to the following conditions: 24 | 25 | The above copyright notice and this permission notice shall be included in 26 | all copies or substantial portions of the Software. 27 | 28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 33 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 34 | IN THE SOFTWARE. 35 | -------------------------------------------------------------------------------- /docs/quickstart/api-key.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. _tutorial-api-key: 5 | 6 | ==================== 7 | Obtaining an API key 8 | ==================== 9 | 10 | Before diving in the source code of the bot, you need to create the bot in 11 | Telegram and get its API key. In order to do so, open your favourite Telegram 12 | client and start a chat with `@botfather`_. That's a bot which will help you 13 | to register a new bot. When you've opened the chat, type the following 14 | command:: 15 | 16 | /newbot 17 | 18 | And follow the instructions it gives to you. When you've given it everything 19 | it needs to know, it will give you an API key, like this fake one:: 20 | 21 | 123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi 22 | 23 | Write it down somewhere, but **be sure to keep it secret!** Everyone with your 24 | API key can take full control of your bot, and that's not a fun thing. 25 | 26 | If you want to learn more about creating and customizing bots, check the 27 | :ref:`bot creation ` chapter of this documentation. 28 | 29 | .. _@botfather: https://telegram.me/botfather 30 | -------------------------------------------------------------------------------- /docs/quickstart/better-help.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. _quickstart-better-help: 5 | 6 | ===================== 7 | A better help command 8 | ===================== 9 | 10 | When you previously ran the bot, you may have noticed botogram automatically 11 | generated the ``/help`` command along side any commands you have registered. 12 | That's one of the features of botogram, and you can obviously customize it. 13 | 14 | .. _quickstart-better-help-about: 15 | 16 | Information about the bot 17 | ========================= 18 | 19 | When someone uses your bot, he might be wondering what it does, or who its 20 | owner is. botogram provides an easy way to add these two pieces of information 21 | to the bot's help message. Add these two lines of code just below the creation 22 | of the bot: 23 | 24 | .. code-block:: python 25 | :emphasize-lines: 2,3 26 | 27 | bot = botogram.create("YOUR-API-KEY") 28 | bot.about = "This bot is just the one from botogram's tutorial" 29 | bot.owner = "@yourusername" 30 | 31 | Feel free to change the about message and the owner's name as you want. If you 32 | run the bot now, you'll see this information when you issue the ``/help`` 33 | command. 34 | 35 | .. note:: 36 | 37 | The about message will also automatically be added to the ``/start`` 38 | command. 39 | 40 | .. _quickstart-better-help-commands: 41 | 42 | Help about commands 43 | =================== 44 | 45 | Usually bots provide commands, and the users of the bot need to know which 46 | commands are available and how to use them. The first part is transparently 47 | handled by botogram, but if you want to provide the commands' description, you 48 | need to provide it. 49 | 50 | To apply a description to a command, simply put a docstring to the command's 51 | function: 52 | 53 | .. code-block:: python 54 | :emphasize-lines: 3,4,5 55 | 56 | @bot.command("hello") 57 | def hello_command(chat, message, args): 58 | """Say hello to the world! 59 | This command sends "Hello world" to the current chat 60 | """ 61 | chat.send("Hello world") 62 | 63 | The first non-empty line of the docstring will be shown in the commands list 64 | when issuing ``/help``, and the whole docstring will be shown only if 65 | someone asks for more detailed help on a command. For instance, issuing 66 | ``/help hello``, with the command name as an argument. 67 | 68 | .. _quickstart-better-help-custom: 69 | 70 | Custom help messages 71 | ==================== 72 | 73 | If your bot doesn't execute only commands, but it does something else, it's 74 | better to write what it does in the help message. In order to do so, you can 75 | append messages before and after the commands list, with either the 76 | :py:attr:`botogram.Bot.before_help` or :py:attr:`botogram.Bot.after_help` 77 | attributes. These attributes should contain a list of messages, and each 78 | message will be sent as one paragraph. 79 | 80 | In this case, we're going to add a message after the commands, which explains 81 | that the bot will also parse chat messages to send useful information (we're 82 | going to implement this afterwards): 83 | 84 | .. code-block:: python 85 | :emphasize-lines: 3,4,5 86 | 87 | bot.owner = "@yourusername" 88 | 89 | bot.after_help = [ 90 | "This bot also parses the chat in order to send you useful information.", 91 | ] 92 | 93 | .. _quickstart-better-help-source: 94 | 95 | Bot's source code until now 96 | =========================== 97 | 98 | .. code-block:: python 99 | 100 | import botogram 101 | 102 | bot = botogram.create("YOUR-API-KEY") 103 | bot.about = "This bot is just the one from botogram's tutorial" 104 | bot.owner = "@yourusername" 105 | 106 | bot.after_help = [ 107 | "This bot also parses the chat in order to send you useful information.", 108 | ] 109 | 110 | @bot.command("hello") 111 | def hello_command(chat, message, args): 112 | """Say hello to the world! 113 | This command sends "Hello world" to the current chat 114 | """ 115 | chat.send("Hello world") 116 | 117 | if __name__ == "__main__": 118 | bot.run() 119 | -------------------------------------------------------------------------------- /docs/quickstart/hello-world.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. _quickstart-hello-world: 5 | 6 | ============================ 7 | A simple Hello world command 8 | ============================ 9 | 10 | Every time a programmer learns a new programming language or a new framework, 11 | it's tradition to write a "hello world" program. In this case, we'll add a 12 | ``/hello`` command, which will simply reply "Hello world" in the chat or 13 | group chat. 14 | 15 | In botogram, a command is a function decorated with the 16 | :py:meth:`botogram.Bot.command` decorator. So, let's add this snippet: 17 | 18 | .. code-block:: python 19 | :emphasize-lines: 3,4,5 20 | 21 | bot = botogram.create("YOUR-API-KEY") 22 | 23 | @bot.command("hello") 24 | def hello_command(chat, message, args): 25 | pass 26 | 27 | The first line is a decorator, which will register the function under it as 28 | the ``/hello`` command. The function can have an arbitrary name, and it may 29 | accept any of these three :ref:`tricks-dynamic-arguments`: 30 | 31 | * The chat where the command was issued (an instance of 32 | :py:class:`botogram.Chat`) 33 | * The representation of the sent message (an instance of 34 | :py:class:`botogram.Message`) 35 | * The list of the parsed command arguments 36 | 37 | So, we'd like to reply with "Hello world" when someone issue the command. 38 | Thankfully, there is an handy method on the chat object: 39 | 40 | .. code-block:: python 41 | :emphasize-lines: 3 42 | 43 | @bot.command("hello") 44 | def hello_command(chat, message, args): 45 | chat.send("Hello world") 46 | 47 | Perfect, now the command is ready! Try to run the bot, and send it ``/hello``: 48 | it will reply with "Hello world". 49 | 50 | .. _quickstart-hello-world-source: 51 | 52 | Bot's source code until now 53 | =========================== 54 | 55 | .. code-block:: python 56 | 57 | import botogram 58 | 59 | bot = botogram.create("YOUR-API-KEY") 60 | 61 | @bot.command("hello") 62 | def hello_command(chat, message, args): 63 | chat.send("Hello world") 64 | 65 | if __name__ == "__main__": 66 | bot.run() 67 | -------------------------------------------------------------------------------- /docs/quickstart/index.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. _quickstart: 5 | 6 | ===================== 7 | Create your first bot 8 | ===================== 9 | 10 | In this chapter, you'll create a brand new bot, which will use a lot of the 11 | botogram capabilities. Be sure to have botogram installed, otherwise be sure to 12 | check the ":ref:`install`" chapter. 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | 17 | api-key 18 | skeleton 19 | hello-world 20 | better-help 21 | chat-messages 22 | -------------------------------------------------------------------------------- /docs/quickstart/skeleton.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. _quickstart-skeleton: 5 | 6 | ================================ 7 | Creating the skeleton of the bot 8 | ================================ 9 | 10 | So, let's start with the actual creation of the bot. During this chapter, that 11 | file will be called ``mybot.py``, so if you want to call the program in a 12 | different way, remember to change all the references to it. 13 | 14 | Open ``mybot.py`` with your favourite text editor, and insert this skeleton in 15 | it: 16 | 17 | .. code-block:: python 18 | 19 | import botogram 20 | 21 | bot = botogram.create("YOUR-API-KEY") 22 | 23 | if __name__ == "__main__": 24 | bot.run() 25 | 26 | The first line imports the botogram microframework, allowing you to use it in 27 | the program. The second one will create a brand new bot, with ``YOUR-API-KEY`` 28 | as the API key. Change that with the one you received in the previous step. 29 | Finally, the third line will run the bot. 30 | 31 | Even if right now the bot does nothing, you can run it by executing its file:: 32 | 33 | $ python3 mybot.py 34 | 35 | The bot will start running, and it will reply to each request you make to it. 36 | In order to stop it, press ``Ctrl+C`` and, when it finishes processing the old 37 | requests, it will close itself. 38 | -------------------------------------------------------------------------------- /docs/tasks.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. _tasks: 5 | 6 | ========================= 7 | Execute independent tasks 8 | ========================= 9 | 10 | Unfortunately, not everything can be executed after a user input, especially 11 | if you need periodic execution, or background cleanup. In order to solve this, 12 | botogram provides you the tasks system, which allows for the execution of 13 | independent tasks. 14 | 15 | .. _tasks-repeated: 16 | 17 | Repeated execution 18 | ================== 19 | 20 | There might be the case when you need to execute a specific function 21 | periodically. For example if you want to implement alerts, or if you need to do 22 | some internal cleanup in your bot. Timers offer an easy and reliable way to 23 | implement this, with a resolution of one second. Just use the 24 | :py:meth:`~botogram.Bot.timer` decorator: 25 | 26 | .. code-block:: python 27 | 28 | @bot.prepare_memory 29 | def init(shared): 30 | shared["subs"] = [] 31 | 32 | @bot.command("subscribe") 33 | def subscribe_command(shared, chat, message, args): 34 | """Subscribe to the hourly BONG""" 35 | subs = shared["subs"] 36 | subs.append(chat.id) 37 | shared["subs"] = subs 38 | 39 | @bot.timer(3600) 40 | def BONG(bot, shared): 41 | for chat in shared["subs"]: 42 | bot.chat(chat).send("BONG") 43 | 44 | If you're working with components, you can instead use the 45 | :py:meth:`~botogram.Component.add_timer` method of the component instance. 46 | -------------------------------------------------------------------------------- /docs/tricks.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. _tricks: 5 | 6 | =============== 7 | Tips and tricks 8 | =============== 9 | 10 | The whole point of botogram is to make your life easier when creating Telegram 11 | bots. In order to do so in a better way, there are some tricks you can use to 12 | speed the development up. 13 | 14 | .. _tricks-messages-syntax: 15 | 16 | Rich formatting with message syntaxes 17 | ===================================== 18 | 19 | Plain text messages can be boring for your users, and also hard to read if 20 | those messages are full with information. Because of that, Telegram allows bots 21 | to use rich formatting in their messages. Currently Telegram only supports `a 22 | subset of`_ Markdown and HTML. 23 | 24 | In order to use rich formatting in your messages you don't need to do anything: 25 | botogram is smart enough to detect when a message uses rich formatting as well 26 | as which syntax is used. If for whatever reason that detection fails, you can 27 | specify the syntax you're using by providing it to the ``syntax`` parameter of 28 | the :py:meth:`~botogram.Chat.send` method: 29 | 30 | .. code-block:: python 31 | 32 | chat.send("*This is Markdown!*", syntax="markdown") 33 | 34 | That parameter accepts the following values: 35 | 36 | * ``markdown``, or its aliases ``md`` and ``Markdown`` 37 | * ``html``, or its alias ``HTML`` 38 | 39 | Also, if you don't want to use any rich formatting but the detector spots 40 | something, you can disable it providing the special syntax ``plain`` to it: 41 | 42 | .. code-block:: python 43 | 44 | chat.send("*I don't want this to be detected*", syntax="plain") 45 | 46 | .. note:: 47 | 48 | Support for rich formatting depends on your users' Telegram client. All 49 | official clients are supported, where unofficial clients may not support 50 | all or any rich formatting syntax. 51 | 52 | .. _a subset of: https://core.telegram.org/bots/api#formatting-options 53 | -------------------------------------------------------------------------------- /docs/unavailable-chats.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | Documentation released under the MIT license (see LICENSE) 3 | 4 | .. _unavailable-chats: 5 | 6 | ============================== 7 | Dealing with unavailable chats 8 | ============================== 9 | 10 | Your bot can't send messages to everyone, because there are some restrictions 11 | in place enforced by Telegram: you can't send messages to people who never 12 | spoken with you before, people who blocked your bot and some more cases. 13 | 14 | When you try to do that, botogram aborts the hook processing, and allows you to 15 | react to that event: for example, if you're sending bulk messages to users and 16 | one of them blocked your bot, you can remove it from the list of recipients. 17 | 18 | .. versionadded:: 0.3 19 | 20 | .. _unavailable-chats-reasons: 21 | 22 | Possible reasons why a chat is not available 23 | ============================================ 24 | 25 | Here there is a list of the current reasons why a chat isn't available: 26 | 27 | * **blocked**: the user blocked your bot, probably because he don't want to 28 | interact with your bot anymore. 29 | 30 | * **account_deleted**: the user deleted his account, so you won't be able to 31 | send messages to him anymore. 32 | 33 | * **chat_moved**: the chat was moved to a new ID. This currently happens if the 34 | group was converted into a supergroup. 35 | 36 | * **not_found**: the chat ID you're trying to contact doesn't exist! 37 | 38 | * **kicked**: your bot was kicked from the group chat you're trying to send 39 | messages to. 40 | 41 | 42 | .. _unavailable-chats-react: 43 | 44 | Take action when a chat isn't available 45 | ======================================= 46 | 47 | Reacting to an unavailable chat is really easy in botogram: you just need to 48 | decorate the function which will take action with the 49 | :py:meth:`~botogram.Bot.chat_unavailable` decorator: 50 | 51 | .. code-block:: python 52 | 53 | @bot.chat_unavailable 54 | def remove_user(chat_id, reason): 55 | # Remove the user from our database 56 | db_connection.query("DELETE FROM users WHERE id = ?", chat_id) 57 | 58 | Keep in mind that your function will be called even if the message was sent 59 | from a different component, so check if you're keeping track of that ID before 60 | trying to delete it. The function will be supplied with two arguments: 61 | 62 | * **chat_id**: the ID of the chat which you can't contact. 63 | 64 | * **reason**: the reason why you can't contact the chat, as a string. You can 65 | see the list of all the possible reason in the :ref:`section above 66 | `. 67 | 68 | If you're writing a component, you can instead call the 69 | :py:meth:`~botogram.Component.add_chat_unavailable_hook` method of your 70 | component: 71 | 72 | .. code-block:: python 73 | 74 | class RemoveUserComponent(botogram.Component): 75 | component_name = "remove-user" 76 | 77 | def __init__(self, db): 78 | self.db = db 79 | self.add_chat_unavailable_hook(self.remove_user) 80 | 81 | def remove_user(self, chat_id, reason): 82 | """Remove the user from the database""" 83 | self.db.query("DELETE FROM users WHERE id = ?", chat_id) 84 | 85 | .. _unavailable-chats-catch: 86 | 87 | Directly catch the exception while processing the update 88 | ======================================================== 89 | 90 | The global :py:meth:`~botogram.Bot.chat_unavailable` decorator is handy because 91 | you don't have to deal with unavailable chats everytime you send a message. The 92 | bad thing is, it aborts the update processing, so it's not suitable to use if 93 | you're sending bulk messages to multiple users. 94 | 95 | In those cases, you can directly catch the exception raised by botogram, so you 96 | can take action without aborting the update processing: 97 | 98 | .. code-block:: python 99 | 100 | @bot.command("send") 101 | def send_command(bot, chat, message, args): 102 | """Send a messages to a list of users""" 103 | message = " ".join(args) 104 | users = [12345, 67890, 54321] 105 | 106 | for user in users: 107 | try: 108 | bot.chat(user).send(message) 109 | except botogram.ChatUnavailableError as e: 110 | print("Can't send messages to %s (reason: %s)" % 111 | (e.chat_id, e.reason)) 112 | users.remove(user) 113 | -------------------------------------------------------------------------------- /i18n/botogram.pot: -------------------------------------------------------------------------------- 1 | # Translations template for botogram. 2 | # Copyright (C) 2018 Pietro Albini 3 | # This file is distributed under the same license as the botogram project. 4 | # FIRST AUTHOR , 2018. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: botogram 1.0.dev0\n" 10 | "Report-Msgid-Bugs-To: https://github.com/pietroalbini/botogram/issues\n" 11 | "POT-Creation-Date: 2017-10-06 19:21+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 2.3.4\n" 19 | 20 | #: botogram/defaults.py:46 21 | msgid "Use /help to get a list of all the commands." 22 | msgstr "" 23 | 24 | #: botogram/defaults.py:53 25 | msgid "Start using the bot." 26 | msgstr "" 27 | 28 | #: botogram/defaults.py:54 29 | msgid "This shows a greeting message." 30 | msgstr "" 31 | 32 | #: botogram/defaults.py:62 33 | msgid "Error! The /help command allows up to one argument." 34 | msgstr "" 35 | 36 | #: botogram/defaults.py:68 botogram/defaults.py:167 37 | #, python-format 38 | msgid "Unknown command: /%(name)s" 39 | msgstr "" 40 | 41 | #: botogram/defaults.py:71 botogram/defaults.py:169 42 | msgid "Use /help to get a list of the commands." 43 | msgstr "" 44 | 45 | #: botogram/defaults.py:93 46 | msgid "This bot supports those commands:" 47 | msgstr "" 48 | 49 | #: botogram/defaults.py:97 botogram/defaults.py:127 50 | msgid "No description available." 51 | msgstr "" 52 | 53 | #: botogram/defaults.py:101 botogram/defaults.py:144 54 | msgid "" 55 | "You can also use /help <command> to get help about a " 56 | "specific command." 57 | msgstr "" 58 | 59 | #: botogram/defaults.py:105 60 | msgid "This bot has no commands." 61 | msgstr "" 62 | 63 | #: botogram/defaults.py:114 botogram/defaults.py:133 64 | #, python-format 65 | msgid "Please contact %(owner)s if you have problems with this bot." 66 | msgstr "" 67 | 68 | #: botogram/defaults.py:143 69 | msgid "Show this help message." 70 | msgstr "" 71 | 72 | -------------------------------------------------------------------------------- /i18n/langs/br.po: -------------------------------------------------------------------------------- 1 | # English translations for botogram. 2 | # Copyright (C) 2018 Pietro Albini 3 | # This file is distributed under the same license as the botogram project. 4 | # FIRST AUTHOR , 2018. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: botogram 1.0.dev0\n" 9 | "Report-Msgid-Bugs-To: https://github.com/pietroalbini/botogram/issues\n" 10 | "POT-Creation-Date: 2017-12-26 21:27+0200\n" 11 | "PO-Revision-Date: 2017-12-28 11:44+0200\n" 12 | "Last-Translator: Fernando Possebon \n" 13 | "Language: br\n" 14 | "Language-Team: br\n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.3.4\n" 20 | 21 | #: botogram/defaults.py:46 22 | msgid "Use /help to get a list of all the commands." 23 | msgstr "Use /help para obter uma lista de todos os comandos." 24 | 25 | #: botogram/defaults.py:53 26 | msgid "Start using the bot." 27 | msgstr "Comece utilizando o bot." 28 | 29 | #: botogram/defaults.py:54 30 | msgid "This shows a greeting message." 31 | msgstr "Isto exibe uma mensagem de saudação." 32 | 33 | #: botogram/defaults.py:62 34 | msgid "Error! The /help command allows up to one argument." 35 | msgstr "Erro! O comando /help aceita até um argumento." 36 | 37 | #: botogram/defaults.py:68 botogram/defaults.py:167 38 | #, python-format 39 | msgstr "Comando desconhecido: /%(name)s" 40 | 41 | #: botogram/defaults.py:71 botogram/defaults.py:169 42 | msgid "Use /help para obter uma lista de todos os comandos." 43 | msgstr "" 44 | 45 | #: botogram/defaults.py:93 46 | msgid "This bot supports those commands:" 47 | msgstr "Este bot possui os seguintes comandos:" 48 | 49 | #: botogram/defaults.py:97 botogram/defaults.py:127 50 | msgid "No description available." 51 | msgstr "Descrição não disponível." 52 | 53 | #: botogram/defaults.py:101 botogram/defaults.py:144 54 | msgid "" 55 | "You can also use /help <command> to get help about a " 56 | "specific command." 57 | msgstr "" 58 | "Você também pode utilizar o comando /help <command> para obter " 59 | "ajuda de um comando específico." 60 | 61 | #: botogram/defaults.py:105 62 | msgid "This bot has no commands." 63 | msgstr "Este bot não possui comandos." 64 | 65 | #: botogram/defaults.py:114 botogram/defaults.py:133 66 | #, python-format 67 | msgid "Please contact %(owner)s if you have problems with this bot." 68 | msgstr "Por favor entre em contato com %(owner)s se você tiver problemas com esse bot." 69 | 70 | #: botogram/defaults.py:143 71 | msgid "Show this help message." 72 | msgstr "Exibe essa mensagem de ajuda." 73 | -------------------------------------------------------------------------------- /i18n/langs/en.po: -------------------------------------------------------------------------------- 1 | # English translations for botogram. 2 | # Copyright (C) 2018 Pietro Albini 3 | # This file is distributed under the same license as the botogram project. 4 | # FIRST AUTHOR , 2018. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: botogram 1.0.dev0\n" 9 | "Report-Msgid-Bugs-To: https://github.com/pietroalbini/botogram/issues\n" 10 | "POT-Creation-Date: 2017-10-06 19:21+0200\n" 11 | "PO-Revision-Date: 2015-08-01 17:20+0200\n" 12 | "Last-Translator: Pietro Albini \n" 13 | "Language: en\n" 14 | "Language-Team: en\n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.3.4\n" 20 | 21 | #: botogram/defaults.py:46 22 | msgid "Use /help to get a list of all the commands." 23 | msgstr "" 24 | 25 | #: botogram/defaults.py:53 26 | msgid "Start using the bot." 27 | msgstr "" 28 | 29 | #: botogram/defaults.py:54 30 | msgid "This shows a greeting message." 31 | msgstr "" 32 | 33 | #: botogram/defaults.py:62 34 | msgid "Error! The /help command allows up to one argument." 35 | msgstr "" 36 | 37 | #: botogram/defaults.py:68 botogram/defaults.py:167 38 | #, python-format 39 | msgid "Unknown command: /%(name)s" 40 | msgstr "" 41 | 42 | #: botogram/defaults.py:71 botogram/defaults.py:169 43 | msgid "Use /help to get a list of the commands." 44 | msgstr "" 45 | 46 | #: botogram/defaults.py:93 47 | msgid "This bot supports those commands:" 48 | msgstr "" 49 | 50 | #: botogram/defaults.py:97 botogram/defaults.py:127 51 | msgid "No description available." 52 | msgstr "" 53 | 54 | #: botogram/defaults.py:101 botogram/defaults.py:144 55 | msgid "" 56 | "You can also use /help <command> to get help about a " 57 | "specific command." 58 | msgstr "" 59 | 60 | #: botogram/defaults.py:105 61 | msgid "This bot has no commands." 62 | msgstr "" 63 | 64 | #: botogram/defaults.py:114 botogram/defaults.py:133 65 | #, python-format 66 | msgid "Please contact %(owner)s if you have problems with this bot." 67 | msgstr "" 68 | 69 | #: botogram/defaults.py:143 70 | msgid "Show this help message." 71 | msgstr "" 72 | 73 | -------------------------------------------------------------------------------- /i18n/langs/it.po: -------------------------------------------------------------------------------- 1 | # Italian translations for botogram. 2 | # Copyright (C) 2018 Pietro Albini 3 | # This file is distributed under the same license as the botogram project. 4 | # FIRST AUTHOR , 2018. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: botogram 1.0.dev0\n" 9 | "Report-Msgid-Bugs-To: https://github.com/pietroalbini/botogram/issues\n" 10 | "POT-Creation-Date: 2017-10-06 19:21+0200\n" 11 | "PO-Revision-Date: 2015-08-01 17:06+0200\n" 12 | "Last-Translator: Pietro Albini \n" 13 | "Language: it\n" 14 | "Language-Team: it\n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.3.4\n" 20 | 21 | #: botogram/defaults.py:46 22 | msgid "Use /help to get a list of all the commands." 23 | msgstr "Utilizza /help per ottenere la lista di tutti i comandi." 24 | 25 | #: botogram/defaults.py:53 26 | msgid "Start using the bot." 27 | msgstr "Inizia ad usare il bot." 28 | 29 | #: botogram/defaults.py:54 30 | msgid "This shows a greeting message." 31 | msgstr "Questo comando visualizza un messaggio di benvenuto." 32 | 33 | #: botogram/defaults.py:62 34 | msgid "Error! The /help command allows up to one argument." 35 | msgstr "" 36 | "Errore! Il comando /help non accetta più di un " 37 | "argomento." 38 | 39 | #: botogram/defaults.py:68 botogram/defaults.py:167 40 | #, python-format 41 | msgid "Unknown command: /%(name)s" 42 | msgstr "Comando sconosciuto: /%(name)s" 43 | 44 | #: botogram/defaults.py:71 botogram/defaults.py:169 45 | msgid "Use /help to get a list of the commands." 46 | msgstr "Utilizza /help per ottenere la lista dei comandi." 47 | 48 | #: botogram/defaults.py:93 49 | msgid "This bot supports those commands:" 50 | msgstr "Questo bot supporta i seguenti comandi:" 51 | 52 | #: botogram/defaults.py:97 botogram/defaults.py:127 53 | msgid "No description available." 54 | msgstr "Nessuna descrizione disponibile." 55 | 56 | #: botogram/defaults.py:101 botogram/defaults.py:144 57 | msgid "" 58 | "You can also use /help <command> to get help about a " 59 | "specific command." 60 | msgstr "" 61 | "Puoi anche usare /help <comando> per ottenere aiuto su" 62 | " un comando." 63 | 64 | #: botogram/defaults.py:105 65 | msgid "This bot has no commands." 66 | msgstr "Questo bot non ha comandi." 67 | 68 | #: botogram/defaults.py:114 botogram/defaults.py:133 69 | #, python-format 70 | msgid "Please contact %(owner)s if you have problems with this bot." 71 | msgstr "Contatta %(owner)s se hai problemi con questo bot." 72 | 73 | #: botogram/defaults.py:143 74 | msgid "Show this help message." 75 | msgstr "Visualizza questo messaggio di aiuto." 76 | 77 | -------------------------------------------------------------------------------- /i18n/langs/pt.po: -------------------------------------------------------------------------------- 1 | # English translations for botogram. 2 | # Copyright (C) 2018 Pietro Albini 3 | # This file is distributed under the same license as the botogram project. 4 | # FIRST AUTHOR , 2018. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: botogram 1.0.dev0\n" 9 | "Report-Msgid-Bugs-To: https://github.com/pietroalbini/botogram/issues\n" 10 | "POT-Creation-Date: 2017-12-26 21:27+0200\n" 11 | "PO-Revision-Date: 2017-12-28 11:44+0200\n" 12 | "Last-Translator: Fernando Possebon \n" 13 | "Language: pt\n" 14 | "Language-Team: pt\n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.3.4\n" 20 | 21 | #: botogram/defaults.py:46 22 | msgid "Use /help to get a list of all the commands." 23 | msgstr "Use /help para obter uma lista de todos os comandos." 24 | 25 | #: botogram/defaults.py:53 26 | msgid "Start using the bot." 27 | msgstr "Comece a utilizar o bot." 28 | 29 | #: botogram/defaults.py:54 30 | msgid "This shows a greeting message." 31 | msgstr "Isto exibe uma mensagem de saudação." 32 | 33 | #: botogram/defaults.py:62 34 | msgid "Error! The /help command allows up to one argument." 35 | msgstr "Erro! O comando /help aceita até um argumento." 36 | 37 | #: botogram/defaults.py:68 botogram/defaults.py:167 38 | #, python-format 39 | msgstr "Comando desconhecido: /%(name)s" 40 | 41 | #: botogram/defaults.py:71 botogram/defaults.py:169 42 | msgid "Use /help para obter uma lista de todos os comandos." 43 | msgstr "" 44 | 45 | #: botogram/defaults.py:93 46 | msgid "This bot supports those commands:" 47 | msgstr "Este bot possui os seguintes comandos:" 48 | 49 | #: botogram/defaults.py:97 botogram/defaults.py:127 50 | msgid "No description available." 51 | msgstr "Descrição não disponível." 52 | 53 | #: botogram/defaults.py:101 botogram/defaults.py:144 54 | msgid "" 55 | "You can also use /help <command> to get help about a " 56 | "specific command." 57 | msgstr "" 58 | "Você também pode utilizar o comando /help <command> para obter " 59 | "ajuda de um comando específico." 60 | 61 | #: botogram/defaults.py:105 62 | msgid "This bot has no commands." 63 | msgstr "Este bot não possui comandos." 64 | 65 | #: botogram/defaults.py:114 botogram/defaults.py:133 66 | #, python-format 67 | msgid "Please contact %(owner)s if you have problems with this bot." 68 | msgstr "Por favor entre em contacto com %(owner)s se você tiver problemas com esse bot." 69 | 70 | #: botogram/defaults.py:143 71 | msgid "Show this help message." 72 | msgstr "Exibe essa mensagem de ajuda." 73 | -------------------------------------------------------------------------------- /nginx-doc.conf: -------------------------------------------------------------------------------- 1 | server { 2 | index index.php index.html; 3 | listen 80 default_server; 4 | root /botogram; 5 | error_log /var/log/nginx/error.log; 6 | access_log /var/log/nginx/access.log; 7 | keepalive_timeout 70; 8 | location = /favicon.ico { 9 | alias /botogram/favicon.ico; 10 | } 11 | location / { 12 | index index.php index.html index.htm; 13 | autoindex on; 14 | autoindex_exact_size off; 15 | autoindex_localtime on; 16 | } 17 | 18 | location = /docs { 19 | return 301 /docs/RELEASE/; 20 | } 21 | 22 | error_page 404 /404.html; 23 | 24 | location = /404.html { 25 | root /botogram/; 26 | internal; 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /requirements-build.in: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile requirements-build.in 6 | # 7 | babel==2.3.4 8 | pytz==2016.4 # via babel 9 | typing==3.7.4.1 10 | wheel==0.29.0 11 | -------------------------------------------------------------------------------- /requirements-build.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile requirements-build.in 6 | # 7 | babel==2.3.4 8 | pytz==2016.4 # via babel 9 | typing==3.7.4.1 10 | wheel==0.29.0 11 | -------------------------------------------------------------------------------- /requirements-docs.in: -------------------------------------------------------------------------------- 1 | buildthedocs 2 | pietroalbini-sphinx-themes 3 | -------------------------------------------------------------------------------- /requirements-docs.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile requirements-docs.in 6 | # 7 | alabaster==0.7.8 # via sphinx 8 | babel==2.3.4 # via sphinx 9 | buildthedocs==1.0.3 # via -r requirements-docs.in 10 | click==6.6 # via buildthedocs 11 | colorama==0.4.1 # via sphinx 12 | docutils==0.12 # via sphinx 13 | imagesize==0.7.1 # via sphinx 14 | jinja2==2.10.1 # via buildthedocs, sphinx 15 | markupsafe==0.23 # via jinja2 16 | pietroalbini-sphinx-themes==1.3.1 # via -r requirements-docs.in 17 | pygments==2.1.3 # via sphinx 18 | pytz==2016.4 # via babel 19 | pyyaml==5.1 # via buildthedocs 20 | six==1.10.0 # via sphinx 21 | snowballstemmer==1.2.1 # via sphinx 22 | sphinx==1.4.3 # via buildthedocs, pietroalbini-sphinx-themes 23 | -------------------------------------------------------------------------------- /requirements-lint.in: -------------------------------------------------------------------------------- 1 | flake8 2 | -------------------------------------------------------------------------------- /requirements-lint.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile requirements-lint.in 6 | # 7 | flake8==2.5.4 8 | mccabe==0.4.0 # via flake8 9 | pep8==1.7.0 # via flake8 10 | pyflakes==1.0.0 # via flake8 11 | -------------------------------------------------------------------------------- /requirements-test.in: -------------------------------------------------------------------------------- 1 | pytest 2 | responses 3 | typing 4 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile requirements-test.in 6 | # 7 | certifi==2019.3.9 # via requests 8 | chardet==3.0.4 # via requests 9 | cookies==2.2.1 # via responses 10 | idna==2.7 # via requests 11 | py==1.4.31 # via pytest 12 | pytest==2.9.2 13 | requests==2.20.0 # via responses 14 | responses==0.5.1 15 | six==1.10.0 # via responses 16 | urllib3==1.24.3 # via requests 17 | -------------------------------------------------------------------------------- /requirements-tools.in: -------------------------------------------------------------------------------- 1 | pip-tools 2 | -------------------------------------------------------------------------------- /requirements-tools.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile requirements-tools.in 6 | # 7 | click==6.6 # via pip-tools 8 | first==2.0.1 # via pip-tools 9 | pip-tools==1.6.5 10 | six==1.10.0 # via pip-tools 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | ''' 3 | ======== 4 | botogram 5 | ======== 6 | 7 | botogram is a Python framework, which allows you to focus just on 8 | creating your `Telegram bots`_, without worrying about the underlying 9 | Bots API. 10 | 11 | While most of the libraries for Telegram out there just wrap the Bots 12 | API, botogram focuses heavily on the development experience, aiming to 13 | provide you the best API possible. Most of the Telegram implementation 14 | details are managed by botogram, so you can just focus on your bot. 15 | 16 | :: 17 | 18 | import botogram 19 | bot = botogram.create("API-KEY") 20 | 21 | @bot.command("hello") 22 | def hello_command(chat, message, args): 23 | """Say hello to the world!""" 24 | chat.send("Hello world") 25 | 26 | if __name__ == "__main__": 27 | bot.run() 28 | 29 | Want to get started? `Go to the documentation`_ 30 | 31 | .. _Telegram bots: https://core.telegram.org/bots 32 | .. _Go to the documentation: https://botogram.dev/docs 33 | ''' 34 | 35 | import setuptools 36 | 37 | 38 | setuptools.setup( 39 | name = "botogram2", 40 | version = "0.6.1", 41 | url = "https://botogram.dev", 42 | 43 | license = "MIT", 44 | 45 | author = "The botogram authors", 46 | 47 | description = "A Python framework for Telegram bots", 48 | long_description = __doc__, 49 | 50 | packages = [ 51 | "botogram", 52 | "botogram.objects", 53 | "botogram.runner", 54 | "botogram.utils", 55 | ], 56 | 57 | install_requires = [ 58 | "logbook", 59 | "requests", 60 | "typing" 61 | ], 62 | 63 | include_package_data = True, 64 | zip_safe = False, 65 | 66 | classifiers = [ 67 | "Development Status :: 3 - Alpha", 68 | "Environment :: Other Environment", 69 | "Intended Audience :: Developers", 70 | "License :: OSI Approved :: MIT License", 71 | "Programming Language :: Python :: 3", 72 | "Topic :: Communications :: Chat", 73 | "Topic :: Software Development :: Libraries :: Application Frameworks", 74 | ], 75 | ) 76 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | import json 22 | 23 | import pytest 24 | import responses 25 | 26 | import botogram.api 27 | import botogram.bot 28 | import botogram.objects 29 | 30 | 31 | API_KEY = "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi" 32 | 33 | 34 | @pytest.fixture() 35 | def mock_req(request): 36 | """Shortcut for mocking a request""" 37 | def __(requests): 38 | mocker = responses.RequestsMock(assert_all_requests_are_fired=False) 39 | mocker.start() 40 | 41 | request.addfinalizer(lambda: mocker.stop()) 42 | 43 | for method, response in requests.items(): 44 | mocker.add("GET", 45 | "https://api.telegram.org/bot"+API_KEY+"/"+method, 46 | content_type="application/json", 47 | body=json.dumps(response)) 48 | 49 | return __ 50 | 51 | 52 | @pytest.fixture() 53 | def api(request): 54 | return botogram.api.TelegramAPI(API_KEY) 55 | 56 | 57 | @pytest.fixture() 58 | def bot(request): 59 | mocker = responses.RequestsMock() 60 | mocker.add("GET", "https://api.telegram.org/bot"+API_KEY+"/getMe", 61 | content_type="application/json", body=json.dumps({ 62 | "ok": True, "result": {"id": 1, "first_name": "test", 63 | "username": "test_bot"}})) 64 | 65 | with mocker: 66 | bot = botogram.bot.create(API_KEY) 67 | return bot 68 | 69 | 70 | @pytest.fixture() 71 | def frozenbot(bot): 72 | return bot.freeze() 73 | 74 | 75 | @pytest.fixture() 76 | def sample_update(request): 77 | return botogram.objects.Update({ 78 | "update_id": 1, 79 | "message": { 80 | "message_id": 2, 81 | "chat": { 82 | "id": -1, 83 | "type": "group", 84 | "title": "test", 85 | }, 86 | "from": { 87 | "id": 3, 88 | "first_name": "test", 89 | }, 90 | "date": 4, 91 | "text": "test", 92 | }, 93 | }) 94 | -------------------------------------------------------------------------------- /tests/test_bot.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | import copy 22 | 23 | import botogram.bot 24 | import botogram.components 25 | import botogram.utils 26 | 27 | import conftest 28 | 29 | 30 | def test_bot_creation(api, mock_req): 31 | # Mock the getMe request 32 | mock_req({ 33 | "getMe": {"ok": True, "result": {"id": 1, "first_name": "test", 34 | "username": "test_bot"}}, 35 | }) 36 | 37 | bot1 = botogram.bot.create(conftest.API_KEY) 38 | bot2 = botogram.bot.Bot(api) 39 | for bot in bot1, bot2: 40 | assert bot.itself.id == 1 41 | assert bot.itself.first_name == "test" 42 | 43 | 44 | def test_use_components(bot, sample_update): 45 | comp1 = botogram.components.Component() 46 | comp2 = botogram.components.Component() 47 | 48 | hook1_called = False 49 | hook2_called = False 50 | 51 | def hook1(chat, message): 52 | nonlocal hook1_called 53 | hook1_called = True 54 | 55 | def hook2(chat, message): 56 | nonlocal hook2_called 57 | hook2_called = True 58 | 59 | # This should be executed before hook_1 60 | assert not hook1_called 61 | 62 | comp1.add_process_message_hook(hook1) 63 | comp2.add_process_message_hook(hook2) 64 | 65 | # comp2 is registered after comp1, so their hooks should be called before 66 | # the comp1's ones 67 | bot.use(comp1) 68 | bot.use(comp2) 69 | bot.process(sample_update) 70 | 71 | assert hook1_called 72 | assert hook2_called 73 | 74 | 75 | def test_bot_freeze(bot): 76 | # Create the frozen bot instance 77 | frozen = bot.freeze() 78 | 79 | assert bot == frozen 80 | 81 | 82 | def test_i18n_override(bot): 83 | default_message = botogram.utils.get_language("en") \ 84 | .gettext("Use /help to get a list of all the commands.") 85 | override_message = "git gud" 86 | 87 | bot.override_i18n = { 88 | default_message: override_message 89 | } 90 | 91 | assert bot._("Use /help to get a list of all the commands.") \ 92 | == override_message 93 | 94 | bot.override_i18n = {} 95 | 96 | assert bot._("Use /help to get a list of all the commands.") \ 97 | == default_message 98 | -------------------------------------------------------------------------------- /tests/test_callbacks.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | import json 22 | 23 | import pytest 24 | 25 | from botogram.callbacks import Buttons, parse_callback_data, get_callback_data 26 | from botogram.callbacks import hashed_callback_name 27 | from botogram.components import Component 28 | from botogram.context import Context 29 | from botogram.crypto import TamperedMessageError 30 | from botogram.hooks import Hook 31 | 32 | 33 | def test_buttons(bot, sample_update): 34 | component = Component("test") 35 | hook = Hook(lambda: None, component) 36 | 37 | buttons = Buttons() 38 | buttons[0].url("test 1", "http://example.com") 39 | buttons[0].callback("test 2", "test_callback") 40 | buttons[3].callback("test 3", "another_callback", "data") 41 | buttons[2].switch_inline_query("test 4") 42 | buttons[2].switch_inline_query("test 5", "wow", current_chat=True) 43 | 44 | with Context(bot, hook, sample_update): 45 | assert buttons._serialize_attachment(sample_update.chat()) == { 46 | "inline_keyboard": [ 47 | [ 48 | {"text": "test 1", "url": "http://example.com"}, 49 | { 50 | "text": "test 2", 51 | "callback_data": get_callback_data( 52 | bot, sample_update.chat(), "test:test_callback", 53 | ), 54 | }, 55 | ], 56 | [ 57 | {"text": "test 4", "switch_inline_query": ""}, 58 | { 59 | "text": "test 5", 60 | "switch_inline_query_current_chat": "wow" 61 | }, 62 | ], 63 | [ 64 | { 65 | "text": "test 3", 66 | "callback_data": get_callback_data( 67 | bot, sample_update.chat(), "test:another_callback", 68 | "data", 69 | ), 70 | }, 71 | ], 72 | ], 73 | } 74 | 75 | 76 | def test_parse_callback_data(bot, sample_update): 77 | c = sample_update.chat() 78 | 79 | raw = get_callback_data(bot, c, "test_callback", "this is some data!") 80 | assert parse_callback_data(bot, c, raw) == ( 81 | hashed_callback_name("test_callback"), 82 | "this is some data!", 83 | ) 84 | 85 | raw = get_callback_data(bot, c, "test_callback") 86 | assert parse_callback_data(bot, c, raw) == ( 87 | hashed_callback_name("test_callback"), 88 | None, 89 | ) 90 | 91 | with pytest.raises(TamperedMessageError): 92 | raw = get_callback_data(bot, c, "test_callback", "data") + "!" 93 | parse_callback_data(bot, c, raw) 94 | 95 | # Now test with disabled signature verification 96 | bot.validate_callback_signatures = False 97 | 98 | raw = get_callback_data(bot, c, "test_callback", "data") + "!" 99 | assert parse_callback_data(bot, c, raw) == ( 100 | hashed_callback_name("test_callback"), 101 | "data!" 102 | ) 103 | -------------------------------------------------------------------------------- /tests/test_commands.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | import pickle 22 | 23 | import botogram.commands 24 | import botogram.decorators 25 | 26 | 27 | BAD_DOCSTRING = """ a 28 | 29 | 30 | b 31 | 32 | """ 33 | 34 | 35 | def global_command(): 36 | pass 37 | 38 | 39 | def create_command(bot, name, func=None): 40 | """Create a new command""" 41 | if func is None: 42 | def func(): 43 | pass 44 | 45 | bot.command(name)(func) 46 | 47 | all_commands = {cmd.name: cmd for cmd in bot.available_commands(True)} 48 | return func, all_commands[name] 49 | 50 | 51 | def test_command_pickling(bot): 52 | # First of all create a dummy Command 53 | func, cmd = create_command(bot, "test", func=global_command) 54 | 55 | assert cmd.name == pickle.loads(pickle.dumps(cmd)).name 56 | 57 | 58 | def test_command_for_bot(bot): 59 | # First of all create a dummy Command and an instance of it without a bot 60 | func, cmd1 = create_command(bot, "test") 61 | cmd2 = cmd1.for_bot(None) # Strip away the bot 62 | 63 | assert cmd1._bot == bot 64 | assert cmd2._bot == None 65 | assert cmd1 is not cmd2 66 | 67 | 68 | def test_command_raw_docstring(bot): 69 | global_bot = bot 70 | 71 | # First of all create a command 72 | func, cmd = create_command(bot, "test") 73 | 74 | # With no docstring 75 | assert cmd.raw_docstring is None 76 | 77 | # With a basic docstring 78 | func.__doc__ = "test1" 79 | assert cmd.raw_docstring == "test1" 80 | 81 | # With a custom help message 82 | @botogram.decorators.help_message_for(func) 83 | def test2(bot): 84 | # Check if arguments are provided correctly 85 | assert bot == global_bot 86 | 87 | return "test2" 88 | 89 | assert cmd.raw_docstring == "test2" 90 | 91 | # With a custom help message but without a bot 92 | cmd = cmd.for_bot(None) 93 | 94 | @botogram.decorators.help_message_for(func) 95 | def test3(): 96 | return "test3" 97 | 98 | assert cmd.raw_docstring == "test3" 99 | 100 | 101 | def test_command_docstring(bot): 102 | # First of all create a command 103 | func, cmd = create_command(bot, "test") 104 | 105 | # A docstring which contains every bad thing 106 | func.__doc__ = BAD_DOCSTRING 107 | assert cmd.docstring == "a\n\nb" 108 | 109 | # A simple but correct docstring 110 | func.__doc__ = "a\nb" 111 | assert cmd.docstring == "a\nb" 112 | 113 | 114 | def test_command_parameters_list(bot): 115 | def test4(x: int, y: float, q, z: bool = True, w="h"): 116 | pass 117 | 118 | # First of all create a command 119 | func, cmd = create_command(bot, "test4", func=test4) 120 | 121 | # Check the parameters list 122 | assert cmd.parameters_list == "[x:int] [y:float] [q] [z:bool=True] [w=h]" 123 | 124 | 125 | def test_command_summary(bot): 126 | # First of all create a command 127 | func, cmd = create_command(bot, "test") 128 | 129 | # Summary of a docstring with only one line 130 | func.__doc__ = "This is a test" 131 | assert cmd.summary == "This is a test" 132 | 133 | # Summary of a docstring with more than one lines 134 | func.__doc__ = "This is a\nlong test" 135 | assert cmd.summary == "This is a" 136 | -------------------------------------------------------------------------------- /tests/test_crypto.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | import pytest 22 | 23 | from botogram.crypto import generate_secret_key, get_hmac, sign_data 24 | from botogram.crypto import TamperedMessageError, verify_signature 25 | 26 | 27 | def test_generate_secret_key(bot): 28 | # Check if the generated key for the test bot is correct 29 | key = generate_secret_key(bot) 30 | assert key == b'\\\x93aA\xe8\x8d\x9aL\x8c\xfd\x81,D\xeaj\xd0' 31 | 32 | 33 | def test_hmac(bot): 34 | expect = b'q\x06\x9c\xc1\xfa\xd1n\xe8\xef\x17\xf6\xd7Z\xb0G\x7f' 35 | assert get_hmac(bot, b'test data') == expect 36 | 37 | signed = sign_data(bot, b'test string') 38 | assert verify_signature(bot, signed) == b'test string' 39 | 40 | signed += b'a' 41 | with pytest.raises(TamperedMessageError): 42 | verify_signature(bot, signed) 43 | 44 | with pytest.raises(TamperedMessageError): 45 | verify_signature(bot, b'a') 46 | -------------------------------------------------------------------------------- /tests/test_decorators.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | import botogram.decorators 22 | 23 | 24 | def test_help_message_for(bot): 25 | 26 | @bot.command("test") 27 | def func(): 28 | """docstring""" 29 | pass 30 | 31 | cmd = {cmd.name: cmd for cmd in bot.available_commands()}["test"] 32 | 33 | assert cmd.raw_docstring == "docstring" 34 | 35 | @botogram.decorators.help_message_for(func) 36 | def help_func(): 37 | return "function" 38 | 39 | assert cmd.raw_docstring == "function" 40 | -------------------------------------------------------------------------------- /tests/test_frozenbot.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | import pickle 22 | 23 | import pytest 24 | 25 | 26 | def test_calling_functions(frozenbot): 27 | subset_called = False 28 | all_called = False 29 | 30 | # A subset of the arguments 31 | def subset(bot, a): 32 | nonlocal subset_called 33 | subset_called = True 34 | 35 | assert a == "foo" 36 | assert bot == frozenbot 37 | 38 | # All the arguments 39 | def all(bot, a, shared, b): 40 | nonlocal all_called 41 | all_called = True 42 | 43 | assert a == "foo" 44 | assert b == 42 45 | assert bot == frozenbot 46 | 47 | # It's not possible to check from the public API if the shared instance 48 | # is the right one 49 | assert shared is not None 50 | 51 | # One extra argument 52 | def more(bot, a, c): 53 | assert 2 + 2 == 5 54 | 55 | for func in subset, all: 56 | frozenbot._call(func, "comptest", a="foo", b=42) 57 | 58 | # More should raise a TypeError for the "c" argument 59 | with pytest.raises(TypeError): 60 | frozenbot._call(more, "comptest", a="foo", b=42) 61 | 62 | assert subset_called 63 | assert all_called 64 | 65 | 66 | def test_available_commands(bot): 67 | # Create a bunch of dummy commands 68 | @bot.command("test1", order=10) 69 | def test1(): 70 | pass 71 | 72 | @bot.command("test2") 73 | def test2(): 74 | pass 75 | 76 | @bot.command("test3", hidden=True) 77 | def test3(): 78 | pass 79 | 80 | assert [cmd.name for cmd in bot.available_commands()] == [ 81 | "help", 82 | "test2", 83 | "test1", 84 | ] 85 | 86 | assert [cmd.name for cmd in bot.available_commands(all=True)] == [ 87 | "help", 88 | "start", 89 | "test2", 90 | "test3", 91 | "test1", 92 | ] 93 | 94 | 95 | def test_pickle_frozenbot(frozenbot): 96 | # This will pickle and unpickle the frozen bot 97 | pickled = pickle.loads(pickle.dumps(frozenbot)) 98 | assert frozenbot == pickled 99 | -------------------------------------------------------------------------------- /tests/test_inline.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | import json 21 | import botogram 22 | from botogram.components import Component 23 | from botogram.objects.updates import Update 24 | from botogram.hooks import InlineHook 25 | from botogram.runner.jobs import _inline_assign_worker 26 | 27 | 28 | def call_overwrite(*args): 29 | for arg in args: 30 | if type(arg) is dict: 31 | if "results" in arg: 32 | arg["results"] = json.loads(arg["results"]) 33 | return args 34 | 35 | 36 | def update_inline(id_update, offset='', query=''): 37 | return Update({ 38 | 'update_id': id_update, 39 | 'inline_query': { 40 | 'id': '123', 41 | 'from': { 42 | 'id': 321, 43 | 'is_bot': False, 44 | 'first_name': 'test_name' 45 | }, 46 | 'query': query, 47 | 'offset': offset 48 | } 49 | }) 50 | 51 | 52 | def expected_result_inline(old_offset=0): 53 | r = [] 54 | for i in range(old_offset, old_offset+10): 55 | r.append({"id": i, 56 | "title": "test title", 57 | "input_message_content": { 58 | "message_text": "test message", 59 | "disable_web_page_preview": False}, 60 | "type": "article"}) 61 | 62 | return {'inline_query_id': '123', 63 | 'cache_time': 0, 64 | 'is_personal': True, 65 | 'results': r, 66 | 'next_offset': old_offset+10} 67 | 68 | 69 | def inline_default(bot): 70 | def inline_test(inline): 71 | for i in range(50): 72 | yield inline.article("test title", 73 | botogram.InlineInputMessage("test message")) 74 | inline_func = bot.inline()(inline_test) 75 | return {'inline': [InlineHook(inline_func, 76 | Component(), 77 | {'cache': 0, 'private': True, 78 | 'paginate': 10, 'timer': 60})]} 79 | 80 | 81 | def test_worker_assignment(): 82 | update_1 = update_inline(1) 83 | assert _inline_assign_worker(update_1, 5) == 4 84 | update_2 = update_inline(2, offset='10', 85 | query='Test query with some unicode 🅱️🤯🥶') 86 | assert _inline_assign_worker(update_2, 3) == 2 87 | 88 | 89 | def test_inline_hook(bot, mock_req): 90 | chains = inline_default(bot) 91 | update = update_inline(1) 92 | bot.api.call = call_overwrite 93 | for hook in chains["inline"]: 94 | result = hook.call(bot, update) 95 | assert result[0] == 'answerInlineQuery' 96 | assert result[1] == expected_result_inline(0) 97 | 98 | 99 | def test_paginate(bot, mock_req): 100 | chains = inline_default(bot) 101 | bot.api.call = call_overwrite 102 | 103 | for i in range(0, 5): 104 | update = update_inline(i, str(i*10)) 105 | for hook in chains["inline"]: 106 | result = hook.call(bot, update) 107 | assert result[0] == 'answerInlineQuery' 108 | assert result[1] == expected_result_inline(i*10) 109 | 110 | result = None 111 | update = update_inline(5, str(5*10)) 112 | for hook in chains['inline']: 113 | result = hook.call(bot, update) 114 | assert result is None 115 | -------------------------------------------------------------------------------- /tests/test_objects_base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | import pytest 22 | 23 | import botogram.objects.base as objectsbase 24 | 25 | 26 | class AnObject(objectsbase.BaseObject): 27 | 28 | required = { 29 | "test1": int, 30 | } 31 | 32 | 33 | class ObjectToTest(objectsbase.BaseObject): 34 | 35 | required = { 36 | "test1": int, 37 | "test2": AnObject, 38 | } 39 | optional = { 40 | "test3": str, 41 | "test4": objectsbase.multiple(AnObject), 42 | "test5": AnObject, 43 | } 44 | 45 | 46 | def test_object_creation(): 47 | # First of all provide everything needed plus an optional field 48 | obj = ObjectToTest({"test1": 42, "test2": {"test1": 98}, "test3": "test"}) 49 | assert obj.test1 == 42 50 | assert isinstance(obj.test2, AnObject) 51 | assert obj.test2.test1 == 98 52 | assert obj.test3 == "test" 53 | 54 | # Next, try to leave out the optional field 55 | obj = ObjectToTest({"test1": 42, "test2": {"test1": 98}}) 56 | assert obj.test3 is None 57 | 58 | # And then try to leave out something required 59 | with pytest.raises(ValueError): 60 | obj = ObjectToTest({"test1": 42}) 61 | 62 | # Or to provide an invalid type 63 | with pytest.raises(ValueError): 64 | obj = ObjectToTest({"test1": 42, "test2": "nope"}) 65 | with pytest.raises(ValueError): 66 | obj = ObjectToTest({"test1": "nope", "test2": {"test1": 98}}) 67 | 68 | # And then provide multiple things 69 | obj = ObjectToTest({"test1": 42, "test2": {"test1": 98}, "test4": 70 | [{"test1": 1}, {"test1": 2}, {"test1": 3}]}) 71 | assert obj.test4[0].test1 == 1 72 | assert obj.test4[1].test1 == 2 73 | assert obj.test4[2].test1 == 3 74 | 75 | 76 | def test_provide_api(api): 77 | data = {"test1": 42, "test2": {"test1": 98}, "test4": [{"test1": 1}, 78 | {"test1": 2}, {"test1": 3}], "test5": {"test1": 4}} 79 | 80 | # Try either to provide the API at initialization time, or after 81 | obj1 = ObjectToTest(data, api) 82 | obj2 = ObjectToTest(data) 83 | obj2.set_api(api) 84 | 85 | for obj in obj1, obj2: 86 | assert obj._api == api 87 | assert obj.test2._api == api 88 | assert obj.test4[0]._api == api 89 | assert obj.test5._api == api 90 | 91 | 92 | def test_serialize(): 93 | data = {"test1": 42, "test2": {"test1": 98}, "test4": [{"test1": 1}, 94 | {"test1": 2}, {"test1": 3}], "test5": {"test1": 4}} 95 | 96 | # Try to load this and serialize it 97 | assert ObjectToTest(data).serialize() == data 98 | -------------------------------------------------------------------------------- /tests/test_shared.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | import pickle 22 | 23 | import botogram.shared 24 | import botogram.hooks 25 | 26 | 27 | def test_shared_memory_creation(): 28 | shared = botogram.shared.SharedMemory() 29 | 30 | comp1 = shared.of("bot1", "comp1") 31 | comp2 = shared.of("bot1", "comp2") 32 | comp1b = shared.of("bot1", "comp1") 33 | comp1sub = shared.of("bot1", "comp1", "sub") 34 | 35 | comp1["test"] = "test" 36 | 37 | assert "test" in comp1 38 | assert "test" in comp1b 39 | assert not comp2 # empty 40 | assert comp1["test"] == comp1b["test"] 41 | assert not comp1sub # empty 42 | 43 | # memory3sub has more than two parts in a name, so special shared memory 44 | # methods should not have been applied 45 | assert not hasattr(comp1sub, "lock") 46 | 47 | 48 | def test_shared_memory_preparers(): 49 | shared = botogram.shared.SharedMemory() 50 | 51 | def init1(shared): 52 | if "a" in shared: 53 | shared["a"] = 1 54 | else: 55 | shared["a"] = 0 56 | 57 | def init2(shared): 58 | shared["b"] = 1 59 | 60 | comp = botogram.Component() 61 | init1_hook = botogram.hooks.MemoryPreparerHook(init1, comp) 62 | init2_hook = botogram.hooks.MemoryPreparerHook(init2, comp) 63 | 64 | shared.register_preparers_list("comp1", [init1_hook, init2_hook]) 65 | shared.register_preparers_list("comp2", [init1_hook]) 66 | 67 | memory1 = shared.of("bot1", "comp1") 68 | memory2 = shared.of("bot1", "comp2") 69 | memory3 = shared.of("bot2", "comp1") 70 | memory3sub = shared.of("bot2", "comp1", "sub") 71 | 72 | assert memory1["a"] == 0 73 | assert memory1["b"] == 1 74 | assert memory2["a"] == 0 75 | assert "b" not in memory2 76 | # memory3["a"] should be 1 if the initializer was called multiple times 77 | assert memory3["a"] == 0 78 | 79 | # memory3sub has more than two parts in a name, so no preparer should have 80 | # been applied 81 | assert "a" not in memory3sub 82 | 83 | # memory1b["a"] should be 1 if the initializer was called multiple times 84 | memory1b = shared.of("bot1", "comp1") 85 | assert memory1b["a"] == 0 86 | 87 | 88 | def test_shared_memory_pickleable(): 89 | shared = botogram.shared.SharedMemory() 90 | shared.of("bot1", "comp1")["test"] = "test" 91 | 92 | pickled = pickle.loads(pickle.dumps(shared)) 93 | 94 | original = shared.of("bot1", "comp1")["test"] 95 | assert original == pickled.of("bot1", "comp1")["test"] 96 | 97 | 98 | def test_switch_driver(): 99 | shared = botogram.shared.SharedMemory() 100 | 101 | # Initialize the memory with some dummy data 102 | shared.of("bot1", "test1")["a"] = "b" 103 | shared.of("bot1", "test2")["b"] = "c" 104 | 105 | # Create a new driver 106 | driver = botogram.shared.LocalDriver() 107 | shared.switch_driver(driver) 108 | 109 | assert shared.driver == driver 110 | assert shared.of("bot1", "test1")["a"] == "b" 111 | assert shared.of("bot1", "test2")["b"] == "c" 112 | -------------------------------------------------------------------------------- /tests/test_syntaxes.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | import pytest 22 | 23 | import botogram.syntaxes 24 | 25 | 26 | def test_is_markdown(): 27 | assert not botogram.syntaxes.is_markdown("not markdown, sorry!") 28 | assert not botogram.syntaxes.is_markdown("*a [wonderfully](broken syntax`") 29 | 30 | for delimiter in "*", "_", "`", "```": 31 | assert botogram.syntaxes.is_markdown(delimiter+"a"+delimiter) 32 | assert botogram.syntaxes.is_markdown("!"+delimiter+"a"+delimiter+"!") 33 | assert botogram.syntaxes.is_markdown("a\n"+delimiter+"a"+delimiter) 34 | assert botogram.syntaxes.is_markdown("a"+delimiter+"a\na"+delimiter) 35 | 36 | assert botogram.syntaxes.is_markdown("[a](b)") 37 | assert botogram.syntaxes.is_markdown("![a](b)!") 38 | assert botogram.syntaxes.is_markdown("a\n[a](b)") 39 | assert botogram.syntaxes.is_markdown("a[a\na](b)") 40 | assert botogram.syntaxes.is_markdown("a[a](b\nb)") 41 | 42 | assert not botogram.syntaxes.is_markdown("hey@this_is_awesome.com") 43 | assert not botogram.syntaxes.is_markdown("https://www.this_is_awesome.com") 44 | 45 | 46 | def test_is_html(): 47 | assert not botogram.syntaxes.is_html("not HTML, sorry!") 48 | assert not botogram.syntaxes.is_html(" roken html
    ") 49 | 50 | for tag in "b", "strong", "i", "em", "pre", "code": 51 | assert botogram.syntaxes.is_html("<"+tag+">a") 52 | assert botogram.syntaxes.is_html("!<"+tag+">a!") 53 | assert botogram.syntaxes.is_html("a\n<"+tag+">a") 54 | assert botogram.syntaxes.is_html("a<"+tag+">a\na") 55 | 56 | assert not botogram.syntaxes.is_html("a") 57 | assert not botogram.syntaxes.is_html("a") 58 | assert botogram.syntaxes.is_html("a") 59 | assert botogram.syntaxes.is_html("!a!") 60 | assert botogram.syntaxes.is_html("a\na") 61 | assert botogram.syntaxes.is_html("aa\na") 62 | assert botogram.syntaxes.is_html("aa") 63 | 64 | 65 | def test_guess_syntax(): 66 | # Provided syntax name 67 | for name in ("plain",): 68 | assert botogram.syntaxes.guess_syntax("", name) is None 69 | 70 | for name in ("md", "markdown", "Markdown"): 71 | assert botogram.syntaxes.guess_syntax("", name) == "Markdown" 72 | 73 | for name in ("html", "HTML"): 74 | assert botogram.syntaxes.guess_syntax("", name) == "HTML" 75 | 76 | with pytest.raises(ValueError): 77 | botogram.syntaxes.guess_syntax("", "invalid") 78 | 79 | # Let's guess it 80 | assert botogram.syntaxes.guess_syntax("no syntax, sorry!", None) is None 81 | assert botogram.syntaxes.guess_syntax("*markdown*", None) == "Markdown" 82 | assert botogram.syntaxes.guess_syntax("html", None) == "HTML" 83 | -------------------------------------------------------------------------------- /tests/test_tasks.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | import botogram 22 | import botogram.tasks 23 | 24 | 25 | @botogram.pass_bot 26 | def sample_timer(bot=None): 27 | return bot 28 | 29 | 30 | def test_timer_now(bot): 31 | timer = botogram.tasks.TimerTask(5, sample_timer) 32 | assert timer.now(current=0) == True 33 | assert timer.now(current=3) == False 34 | assert timer.now(current=5) == True 35 | assert timer.now(current=6) == False 36 | assert timer.now(current=8) == False 37 | assert timer.now(current=10) == True 38 | 39 | 40 | def test_timer_process(bot): 41 | timer = botogram.tasks.TimerTask(5, sample_timer) 42 | assert timer.process(bot) == bot 43 | 44 | 45 | def test_scheduler(bot): 46 | timer1 = botogram.tasks.TimerTask(5, sample_timer) 47 | timer2 = botogram.tasks.TimerTask(3, sample_timer) 48 | 49 | scheduler = botogram.tasks.Scheduler() 50 | scheduler.add(timer1) 51 | scheduler.add(timer2) 52 | 53 | assert list(scheduler.now(current=0)) == [timer1, timer2] 54 | assert list(scheduler.now(current=2)) == [] 55 | assert list(scheduler.now(current=3)) == [timer2] 56 | assert list(scheduler.now(current=5)) == [timer1] 57 | assert list(scheduler.now(current=7)) == [timer2] 58 | assert list(scheduler.now(current=8)) == [] 59 | assert list(scheduler.now(current=10)) == [timer1, timer2] 60 | -------------------------------------------------------------------------------- /tests/test_utils_calls.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | import pytest 22 | 23 | import botogram.utils 24 | 25 | 26 | def test_call(): 27 | testfunc_called = 0 28 | 29 | # Just a test function 30 | def testfunc(a, b, c): 31 | nonlocal testfunc_called 32 | testfunc_called += 1 33 | 34 | assert a == 1 35 | assert b == 2 36 | assert c == 3 37 | 38 | # Call the function with the exact list of arguments 39 | botogram.utils.call(testfunc, a=1, b=2, c=3) 40 | assert testfunc_called == 1 41 | 42 | # Call the function with more arguments than the needed ones 43 | botogram.utils.call(testfunc, a=1, b=2, c=3, d=4) 44 | assert testfunc_called == 2 45 | 46 | # Call the function with less arguments than the needed ones 47 | with pytest.raises(TypeError): 48 | botogram.utils.call(testfunc, a=1, b=2) 49 | assert testfunc_called == 2 50 | 51 | 52 | def test_call_with_wraps(): 53 | mywrapper_called = False 54 | myfunc_called = False 55 | 56 | def myfunc(a, b): 57 | nonlocal myfunc_called 58 | myfunc_called = True 59 | 60 | assert a == 1 61 | assert b == 2 62 | 63 | @botogram.utils.wraps(myfunc) 64 | def mywrapper(c): 65 | nonlocal mywrapper_called 66 | mywrapper_called = True 67 | 68 | assert c == 3 69 | 70 | botogram.utils.call(myfunc, a=1, b=2, c=3) 71 | 72 | botogram.utils.call(mywrapper, a=1, b=2, c=3) 73 | assert mywrapper_called 74 | assert myfunc_called 75 | 76 | 77 | def test_call_lazy_arguments(): 78 | myfunc1_called = False 79 | myfunc2_called = False 80 | myarg_called = False 81 | 82 | def myfunc1(a): 83 | nonlocal myfunc1_called 84 | myfunc1_called = True 85 | 86 | assert a == 1 87 | 88 | def myfunc2(a, b): 89 | nonlocal myfunc2_called 90 | myfunc2_called = True 91 | 92 | assert a == 1 93 | assert b == 2 94 | 95 | def myarg(): 96 | nonlocal myarg_called 97 | myarg_called = True 98 | 99 | return 2 100 | 101 | lazy = botogram.utils.CallLazyArgument(myarg) 102 | 103 | botogram.utils.call(myfunc1, a=1, b=lazy) 104 | assert myfunc1_called 105 | assert not myarg_called 106 | 107 | botogram.utils.call(myfunc2, a=1, b=lazy) 108 | assert myfunc2_called 109 | assert myarg_called 110 | -------------------------------------------------------------------------------- /tests/test_utils_strings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-2019 The Botogram Authors (see AUTHORS) 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | import botogram.utils 22 | 23 | 24 | def test_strip_urls(): 25 | # Standard HTTP url 26 | assert botogram.utils.strip_urls("http://www.example.com") == "" 27 | 28 | # Standard HTTPS url 29 | assert botogram.utils.strip_urls("https://www.example.com") == "" 30 | 31 | # HTTP url with dashes in the domain (issue #32) 32 | assert botogram.utils.strip_urls("http://www.ubuntu-it.org") == "" 33 | 34 | # HTTP url with a path 35 | assert botogram.utils.strip_urls("http://example.com/~john/d/a.txt") == "" 36 | 37 | # Standard email address 38 | assert botogram.utils.strip_urls("john.doe@example.com") == "" 39 | 40 | # Email address with a comment (+something) 41 | assert botogram.utils.strip_urls("john.doe+botogram@example.com") == "" 42 | 43 | # Email address with subdomains 44 | assert botogram.utils.strip_urls("john.doe@something.example.com") == "" 45 | 46 | # Email address with dashes in the domain name (issue #32) 47 | assert botogram.utils.strip_urls("pietroalbini@ubuntu-it.org") == "" 48 | 49 | 50 | def test_usernames_in(): 51 | assert botogram.utils.usernames_in("Hi, what's up?") == [] 52 | assert botogram.utils.usernames_in("Hi @johndoe!") == ["johndoe"] 53 | 54 | multiple = botogram.utils.usernames_in("Hi @johndoe, I'm @pietroalbini") 55 | assert multiple == ["johndoe", "pietroalbini"] 56 | 57 | command = botogram.utils.usernames_in("/say@saybot I'm @johndoe") 58 | assert command == ["johndoe"] 59 | 60 | email = botogram.utils.usernames_in("My address is john.doe@example.com") 61 | assert email == [] 62 | 63 | username_url = botogram.utils.usernames_in("http://pwd:john@example.com") 64 | assert username_url == [] 65 | -------------------------------------------------------------------------------- /website/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | botogram - Page not found 7 | 8 | 9 | 10 | 11 |
    12 | 13 |
    14 |
    15 |

    botogram

    16 |

    Python framework for Telegram bots

    17 |
    18 |
    19 |
    20 |
    21 |
    22 |
    23 |

    404 - Page not found

    24 |
    25 |
    26 | 33 |
    34 | 35 |
    36 |
      37 |
    • Copyright © 2015-2019 38 | The botogram authors 39 |
    • 40 |
    • botogram is released under the MIT license.
    • 41 |
    42 |
    43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /website/_static/style.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | body { 4 | font-family: DejaVu Sans; 5 | color: #333; 6 | margin: 0; 7 | } 8 | 9 | div.color-bar { 10 | height: 0.2em; 11 | background: #179cde; 12 | } 13 | 14 | div.wrapper { 15 | width: 50em; 16 | margin: auto; 17 | } 18 | 19 | div.row { 20 | padding: 0.5em 0; 21 | } 22 | 23 | hr { 24 | border: 0; 25 | border-bottom: 0.1em dotted #ccc; 26 | margin: 0; 27 | } 28 | 29 | 30 | h1 { 31 | text-align: center; 32 | font-size: 2.5em; 33 | font-family: 'Bitter', serif; 34 | margin: 0.3em 0 0 0; 35 | } 36 | 37 | h2 { 38 | text-align: center; 39 | font-size: 1.2em; 40 | font-family: 'Bitter', serif; 41 | font-weight: 400; 42 | line-height: 1.3em; 43 | word-spacing: 0.1em; 44 | margin: 0.3em 0 1em 0; 45 | color: #aaa; 46 | } 47 | 48 | 49 | p { 50 | line-height: 1.5em; 51 | } 52 | 53 | 54 | 55 | div.cleared:after { 56 | content: ""; 57 | height: 0; 58 | visibility: hidden; 59 | display: block; 60 | clear: both; 61 | } 62 | 63 | div.cleared div.left { 64 | width: 48%; 65 | float: left; 66 | } 67 | 68 | div.cleared div.right { 69 | width: 48%; 70 | float: right; 71 | } 72 | 73 | div.vcenter-text { 74 | display: table; 75 | } 76 | 77 | div.vcenter-text > p { 78 | display: table-cell; 79 | vertical-align: middle; 80 | text-align: center; 81 | } 82 | 83 | 84 | 85 | div.motto { 86 | height: 13.4em; 87 | } 88 | 89 | div.motto p { 90 | font-weight: 300; 91 | font-size: 1.8em; 92 | } 93 | 94 | div.error { 95 | text-align: center; 96 | width: 100%; 97 | } 98 | 99 | nav ul { 100 | text-align: center; 101 | margin: 0; 102 | padding: 0; 103 | } 104 | 105 | nav ul li { 106 | list-style: none; 107 | display: inline-block; 108 | } 109 | 110 | nav ul li a { 111 | color: #179cde; 112 | padding: 0.5em 0.7em; 113 | display: block; 114 | font-family: 'Bitter', serif; 115 | font-size: 1.2em; 116 | text-decoration: none; 117 | word-spacing: 0.1em; 118 | transition: .2s color ease-in-out; 119 | } 120 | 121 | nav ul li a:hover { 122 | color: #54a9eb; 123 | } 124 | 125 | footer ul { 126 | margin: 0 0 1em 0; 127 | text-align: center; 128 | padding: 0; 129 | } 130 | 131 | footer ul li { 132 | list-style: none; 133 | display: inline-block; 134 | font-size: 0.91em; 135 | color: #aaa; 136 | margin: 0 0.5em; 137 | } 138 | 139 | footer ul li a { 140 | color: #aaa; 141 | border-bottom: 0.1em dotted #ccc; 142 | text-decoration: none; 143 | } 144 | 145 | footer ul li a:hover { 146 | border-bottom-color: #aaa; 147 | } 148 | 149 | 150 | 151 | pre { 152 | font-family: "Hack", monospace; 153 | font-size: 0.81em; 154 | line-height: 1.4em; 155 | } 156 | 157 | pre span.ck { 158 | font-weight: bold; 159 | color: #179cde; 160 | } 161 | 162 | pre span.cs { 163 | color: #00af00; 164 | } 165 | 166 | pre span.cc { 167 | color: #888; 168 | } 169 | 170 | pre span.cd { 171 | font-weight: bold; 172 | color: #444; 173 | } 174 | 175 | pre span.less { 176 | opacity: 0.4; 177 | transition: .2s opacity ease-out; 178 | } 179 | 180 | pre:hover span.less { 181 | opacity: 0.7; 182 | } 183 | 184 | 185 | 186 | @media all and (max-width: 52em) { 187 | 188 | div.wrapper { 189 | width: auto; 190 | padding: 0 1em; 191 | } 192 | 193 | div.cleared div.left, div.cleared div.right { 194 | float: none; 195 | width: 100%; 196 | } 197 | 198 | div.pre-container { 199 | text-align: center; 200 | overflow-x: auto; 201 | margin: 0 -1em; 202 | padding: 0 1em; 203 | } 204 | 205 | div.pre-container pre { 206 | display: inline-block; 207 | text-align: left; 208 | } 209 | 210 | div.motto { 211 | height: auto; 212 | padding: 0.8em 0; 213 | } 214 | 215 | nav ul { 216 | margin: 0.3em 0 0.7em 0; 217 | } 218 | 219 | nav ul li, footer ul li { 220 | display: inline-block; 221 | } 222 | 223 | nav ul li a { 224 | padding: 0.2em 0.7em; 225 | } 226 | 227 | nav ul li:last-child, footer ul li:last-child { 228 | margin-bottom: 0; 229 | } 230 | 231 | footer { 232 | margin-bottom: 2em; 233 | } 234 | 235 | footer ul li { 236 | margin: 0.3em 0.5em; 237 | } 238 | 239 | } 240 | -------------------------------------------------------------------------------- /website/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-botogram/botogram/ac30be0267dc2df1919ed781682eb45002b3884c/website/favicon.ico -------------------------------------------------------------------------------- /website/fonts/bitter/bitter-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-botogram/botogram/ac30be0267dc2df1919ed781682eb45002b3884c/website/fonts/bitter/bitter-400.ttf -------------------------------------------------------------------------------- /website/fonts/bitter/bitter-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-botogram/botogram/ac30be0267dc2df1919ed781682eb45002b3884c/website/fonts/bitter/bitter-700.ttf -------------------------------------------------------------------------------- /website/fonts/bitter/include.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:'Bitter';font-weight:400;font-style:normal;src:url('bitter-400.eof');src:url('bitter-400.ttf') format('truetype'),url('bitter-400.woff') format('woff')}@font-face{font-family:'Bitter';font-weight:400;font-style:italic;src:url('bitter-400-italic.eof');src:url('bitter-400-italic.ttf') format('truetype'),url('bitter-400-italic.woff') format('woff')}@font-face{font-family:'Bitter';font-weight:700;font-style:normal;src:url('bitter-700.eof');src:url('bitter-700.ttf') format('truetype'),url('bitter-700.woff') format('woff')}@font-face{font-family:'Bitter';font-weight:700;font-style:italic;src:url('bitter-700-italic.eof');src:url('bitter-700-italic.ttf') format('truetype'),url('bitter-700-italic.woff') format('woff')} -------------------------------------------------------------------------------- /website/fonts/dejavu-sans/dejavu-sans-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-botogram/botogram/ac30be0267dc2df1919ed781682eb45002b3884c/website/fonts/dejavu-sans/dejavu-sans-400.ttf -------------------------------------------------------------------------------- /website/fonts/dejavu-sans/include.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:'DejaVu Sans';font-weight:400;font-style:normal;src:url('dejavu-sans-400.eof');src:url('dejavu-sans-400.ttf') format('truetype'),url('dejavu-sans-400.woff') format('woff')}@font-face{font-family:'DejaVu Sans';font-weight:400;font-style:italic;src:url('dejavu-sans-400-italic.eof');src:url('dejavu-sans-400-italic.ttf') format('truetype'),url('dejavu-sans-400-italic.woff') format('woff')}@font-face{font-family:'DejaVu Sans';font-weight:700;font-style:normal;src:url('dejavu-sans-700.eof');src:url('dejavu-sans-700.ttf') format('truetype'),url('dejavu-sans-700.woff') format('woff')}@font-face{font-family:'DejaVu Sans';font-weight:700;font-style:italic;src:url('dejavu-sans-700-italic.eof');src:url('dejavu-sans-700-italic.ttf') format('truetype'),url('dejavu-sans-700-italic.woff') format('woff')} -------------------------------------------------------------------------------- /website/fonts/hack/hack-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-botogram/botogram/ac30be0267dc2df1919ed781682eb45002b3884c/website/fonts/hack/hack-400.ttf -------------------------------------------------------------------------------- /website/fonts/hack/hack-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-botogram/botogram/ac30be0267dc2df1919ed781682eb45002b3884c/website/fonts/hack/hack-700.ttf -------------------------------------------------------------------------------- /website/fonts/hack/include.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:'Hack';font-weight:400;font-style:normal;src:url('hack-400.eof');src:url('hack-400.ttf') format('truetype'),url('hack-400.woff') format('woff')}@font-face{font-family:'Hack';font-weight:400;font-style:italic;src:url('hack-400-italic.eof');src:url('hack-400-italic.ttf') format('truetype'),url('hack-400-italic.woff') format('woff')}@font-face{font-family:'Hack';font-weight:700;font-style:normal;src:url('hack-700.eof');src:url('hack-700.ttf') format('truetype'),url('hack-700.woff') format('woff')}@font-face{font-family:'Hack';font-weight:700;font-style:italic;src:url('hack-700-italic.eof');src:url('hack-700-italic.ttf') format('truetype'),url('hack-700-italic.woff') format('woff')} -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | botogram - Python framework for Telegram bots 7 | 8 | 9 | 10 | 11 |
    12 | 13 |
    14 |
    15 |

    botogram

    16 |

    Python framework for Telegram bots

    17 |
    18 |
    19 |
    20 |
    21 |
    22 |
    23 |
    24 |
    25 | import botogram
    26 | bot = botogram.create("API-KEY")
    27 | 
    28 | @bot.command("hello")
    29 | def hello_command(chat, message, args):
    30 |     """Say hello to the world!"""
    31 |     chat.send("Hello world")
    32 | 
    33 | if __name__ == "__main__":
    34 |     bot.run()
    35 | 
    36 |
    37 |
    38 |

    Just focus on your bots.

    39 |
    40 |
    41 | 54 |
    55 |
    56 |
    57 |
    58 |
    59 |

    60 | botogram is a Python framework, which allows you to focus 61 | just on creating your Telegram bots, without worrying 62 | about the underlying Bots API. 63 |

    64 |

    65 | While most of the libraries for Telegram out there just 66 | wrap the Bots API, botogram focuses heavily on the 67 | development experience, aiming to provide you the best API 68 | possible. Most of the Telegram implementation details are 69 | managed by botogram, so you can just focus on your bot. 70 |

    71 |
    72 |
    73 |
    74 |
      75 |
    • Copyright © 2015-2019 76 | The botogram authors 77 |
    • 78 |
    • botogram is released under the MIT license.
    • 79 |
    80 |
    81 | 82 | 83 | 84 | 85 | 86 | 87 | --------------------------------------------------------------------------------