Repository: python-telegram-bot/python-telegram-bot
Pull request: Bot api 3.1
See #697 for changes

This should implement everything.

Missing tests (how much can we even test?)

Closes #697 

Problem statement:
API changes - v3.1 - June 30, 2017
This issue is to keep track of development process.

- [x] New method `restrictChatMember`.
  - [ ] unitest
- [x] New method `promoteChatMember`.
  - [ ] unitest
- [x] New parameter `until_date` added to `kickChatMember` for temporary bans.
- [x] New method `exportChatInviteLink`.
  - [ ] unitest
- [x] New method `setChatPhoto`.
  - [ ] unitest
- [x] New method `deleteChatPhoto`.
  - [ ] unitest
- [x] New method `setChatTitle`.
  - [ ] unitest
- [x] New method `setChatDescription`.
  - [ ] unitest
- [x] New method `pinChatMessage`.
  - [ ] unitest
- [x] New method `unpinChatMessage`.
  - [ ] unitest
- [x] New fields `photo`, `description` and `invite_link` added to the `Chat` object.
- [x] New fields `until_date`, `can_be_edited`, `can_change_info`, `can_post_messages`, `can_edit_messages`, `can_delete_messages`, `can_invite_users`, `can_restrict_members`, `can_pin_messages`, `can_promote_members`, `can_send_messages`, `can_send_media_messages`, `can_send_other_messages` and `can_add_web_page_previews` added to the `ChatMember` object.
  - [ ] unitest (only for attributes we unitest their respective method above)

Hints from issue discussion:

Commit message: Use to_timestamp from utils.helpers in kick and restrict methods in bot

This makes it actually works on python < 3.3

Patch to review:
diff --git a/telegram/bot.py b/telegram/bot.py
index 25850064eae..5d378016247 100644
--- a/telegram/bot.py
+++ b/telegram/bot.py
@@ -22,10 +22,12 @@
 import functools
 import logging
 import warnings
+from datetime import datetime
 
 from telegram import (User, Message, Update, Chat, ChatMember, UserProfilePhotos, File,
                       ReplyMarkup, TelegramObject, WebhookInfo, GameHighScore)
 from telegram.error import InvalidToken, TelegramError
+from telegram.utils.helpers import to_timestamp
 from telegram.utils.request import Request
 
 logging.getLogger(__name__).addHandler(logging.NullHandler())
@@ -1039,12 +1041,12 @@ def get_file(self, file_id, timeout=None, **kwargs):
         return File.de_json(result, self)
 
     @log
-    def kick_chat_member(self, chat_id, user_id, timeout=None, **kwargs):
-        """Use this method to kick a user from a group or a supergroup.
+    def kick_chat_member(self, chat_id, user_id, timeout=None, until_date=None, **kwargs):
+        """Use this method to kick a user from a group, a supergroup or a channel.
 
-        In the case of supergroups, the user will not be able to return to the group on their own
-        using invite links, etc., unless unbanned first. The bot must be an administrator in the
-        group for this to work.
+        In the case of supergroups and channels, the user will not be able to return to the group
+        on their own using invite links, etc., unless unbanned first. The bot must be an
+        administrator in the chat for this to work and must have the appropriate admin rights.
 
         Args:
             chat_id (int|str): Unique identifier for the target group or username of the target
@@ -1053,8 +1055,16 @@ def kick_chat_member(self, chat_id, user_id, timeout=None, **kwargs):
             timeout (Optional[int|float]): If this value is specified, use it as the read timeout
                 from the server (instead of the one specified during creation of the connection
                 pool).
+            until_date (Optional[int|datetime]): Date when the user will be unbanned,
+                unix time. If user is banned for more than 366 days or less than 30 seconds from
+                the current time they are considered to be banned forever
             **kwargs (dict): Arbitrary keyword arguments.
 
+        Note:
+            In regular groups (non-supergroups), this method will only work if the
+            ‘All Members Are Admins’ setting is off in the target group. Otherwise
+            members may only be removed by the group's creator or by the member that added them.
+
         Returns:
             bool: On success, `True` is returned.
 
@@ -1066,6 +1076,11 @@ def kick_chat_member(self, chat_id, user_id, timeout=None, **kwargs):
 
         data = {'chat_id': chat_id, 'user_id': user_id}
 
+        if until_date is not None:
+            if isinstance(until_date, datetime):
+                until_date = to_timestamp(until_date)
+            data['until_date'] = until_date
+
         result = self._request.post(url, data, timeout=timeout)
 
         return result
@@ -1993,6 +2008,366 @@ def answer_pre_checkout_query(self, pre_checkout_query_id, ok,
 
         return result
 
+    @log
+    def restrict_chat_member(self, chat_id, user_id, until_date=None, can_send_messages=None,
+                             can_send_media_messages=None, can_send_other_messages=None,
+                             can_add_web_page_previews=None, timeout=None, **kwargs):
+        """Use this method to restrict a user in a supergroup.
+
+        The bot must be an administrator in the supergroup for this to work and must have the
+        appropriate admin rights. Pass True for all boolean parameters to lift restrictions
+        from a user.
+
+        Args:
+            chat_id (int|str): Unique identifier for the target chat or username of the target
+                supergroup (in the format @supergroupusername)
+            user_id (int): Unique identifier of the target user
+            until_date (Optional[int|datetime]): Date when restrictions will be lifted for the
+                user, unix time. If user is restricted for more than 366 days or less than 30
+                seconds from the current time, they are considered to be restricted forever
+            can_send_messages (Optional[boolean]): Pass True, if the user can send text messages,
+                contacts, locations and venues
+            can_send_media_messages (Optional[boolean]): Pass True, if the user can send audios,
+                documents, photos, videos, video notes and voice notes, implies can_send_messages
+            can_send_other_messages (Optional[boolean]): Pass True, if the user can send
+                animations, games, stickers and use inline bots, implies can_send_media_messages
+            can_add_web_page_previews (Optional[boolean]): Pass True, if the user may add web page
+                previews to their messages, implies can_send_media_messages
+            timeout (Optional[int|float]): If this value is specified, use it as the read timeout
+                from the server (instead of the one specified during creation of the connection
+                pool).
+            **kwargs (dict): Arbitrary keyword arguments
+
+        Returns:
+            bool: On success, `True` is returned.
+
+        Raises:
+            :class:`telegram.TelegramError`
+
+        """
+        url = '{0}/restrictChatMember'.format(self.base_url)
+
+        data = {'chat_id': chat_id, 'user_id': user_id}
+
+        if until_date is not None:
+            if isinstance(until_date, datetime):
+                until_date = to_timestamp(until_date)
+            data['until_date'] = until_date
+        if can_send_messages is not None:
+            data['can_send_messages'] = can_send_messages
+        if can_send_media_messages is not None:
+            data['can_send_media_messages'] = can_send_media_messages
+        if can_send_other_messages is not None:
+            data['can_send_other_messages'] = can_send_other_messages
+        if can_add_web_page_previews is not None:
+            data['can_add_web_page_previews'] = can_add_web_page_previews
+
+        result = self._request.post(url, data, timeout=timeout)
+
+        return result
+
+    @log
+    def promote_chat_member(self, chat_id, user_id, can_change_info=None,
+                            can_post_messages=None, can_edit_messages=None,
+                            can_delete_messages=None, can_invite_users=None,
+                            can_restrict_members=None, can_pin_messages=None,
+                            can_promote_members=None, timeout=None, **kwargs):
+        """Use this method to promote or demote a user in a supergroup or a channel.
+
+        The bot must be an administrator in the chat for this to work and must have the
+        appropriate admin rights. Pass False for all boolean parameters to demote a user
+
+        Args:
+            chat_id (int|str): Unique identifier for the target chat or username of the target
+                supergroup (in the format @supergroupusername)
+            user_id (int): Unique identifier of the target user
+            can_change_info (Optional[boolean]): Pass True, if the administrator can change chat
+                title, photo and other settings
+            can_post_messages (Optional[boolean]): Pass True, if the administrator can create
+                channel posts, channels only
+            can_edit_messages (Optional[boolean]): Pass True, if the administrator can edit
+                messages of other users, channels only
+            can_delete_messages (Optional[boolean]): Pass True, if the administrator can delete
+                messages of other users
+            can_invite_users (Optional[boolean]): Pass True, if the administrator can invite new
+                users to the chat
+            can_restrict_members (Optional[boolean]): Pass True, if the administrator can restrict,
+                ban or unban chat members
+            can_pin_messages (Optional[boolean]): Pass True, if the administrator can pin messages,
+                supergroups only
+            can_promote_members (Optional[boolean]): Pass True, if the administrator can add new
+                administrators with a subset of his own privileges or demote administrators that
+                he has promoted, directly or indirectly (promoted by administrators that were
+                appointed by him)
+            timeout (Optional[int|float]): If this value is specified, use it as the read timeout
+                from the server (instead of the one specified during creation of the connection
+                pool).
+            **kwargs (dict): Arbitrary keyword arguments
+
+        Returns:
+            bool: On success, `True` is returned.
+
+        Raises:
+            :class:`telegram.TelegramError`
+
+        """
+        url = '{0}/promoteChatMember'.format(self.base_url)
+
+        data = {'chat_id': chat_id, 'user_id': user_id}
+
+        if can_change_info is not None:
+            data['can_change_info'] = can_change_info
+        if can_post_messages is not None:
+            data['can_post_messages'] = can_post_messages
+        if can_edit_messages is not None:
+            data['can_edit_messages'] = can_edit_messages
+        if can_delete_messages is not None:
+            data['can_delete_messages'] = can_delete_messages
+        if can_invite_users is not None:
+            data['can_invite_users'] = can_invite_users
+        if can_restrict_members is not None:
+            data['can_restrict_members'] = can_restrict_members
+        if can_pin_messages is not None:
+            data['can_pin_messages'] = can_pin_messages
+        if can_promote_members is not None:
+            data['can_promote_members'] = can_promote_members
+
+        result = self._request.post(url, data, timeout=timeout)
+
+        return result
+
+    @log
+    def export_chat_invite_link(self, chat_id, timeout=None, **kwargs):
+        """Use this method to export an invite link to a supergroup or a channel.
+
+        The bot must be an administrator in the chat for this to work and must have the
+        appropriate admin rights.
+
+        Args:
+            chat_id (int|str): Unique identifier for the target chat or username of the target
+                channel (in the format @channelusername)
+            timeout (Optional[int|float]): If this value is specified, use it as the read timeout
+                from the server (instead of the one specified during creation of the connection
+                pool).
+            **kwargs (dict): Arbitrary keyword arguments
+
+        Returns:
+            str: On success the exported invite link is returned
+
+        Raises:
+            :class:`telegram.TelegramError`
+
+        """
+        url = '{0}/exportChatInviteLink'.format(self.base_url)
+
+        data = {'chat_id': chat_id}
+
+        result = self._request.post(url, data, timeout=timeout)
+
+        return result
+
+    @log
+    def set_chat_photo(self, chat_id, photo, timeout=None, **kwargs):
+        """Use this method to set a new profile photo for the chat.
+
+        Photos can't be changed for private chats. The bot must be an administrator in the chat
+        for this to work and must have the appropriate admin rights.
+
+        Args:
+            chat_id (int|str): Unique identifier for the target chat or username of the target
+                channel (in the format @channelusername)
+            photo (`telegram.InputFile`): New chat photo
+            timeout (Optional[int|float]): If this value is specified, use it as the read timeout
+                from the server (instead of the one specified during creation of the connection
+                pool).
+            **kwargs (dict): Arbitrary keyword arguments
+
+        Note:
+            In regular groups (non-supergroups), this method will only work if the
+            ‘All Members Are Admins’ setting is off in the target group.
+
+        Returns:
+            bool: On success, `True` is returned.
+
+        Raises:
+            :class:`telegram.TelegramError`
+
+        """
+        url = '{0}/setChatPhoto'.format(self.base_url)
+
+        data = {'chat_id': chat_id, 'photo': photo}
+
+        result = self._request.post(url, data, timeout=timeout)
+
+        return result
+
+    @log
+    def delete_chat_photo(self, chat_id, timeout=None, **kwargs):
+        """Use this method to delete a chat photo.
+
+        Photos can't be changed for private chats. The bot must be an administrator in the chat
+        for this to work and must have the appropriate admin rights.
+
+        Args:
+            chat_id (int|str): Unique identifier for the target chat or username of the target
+                channel (in the format @channelusername)
+            timeout (Optional[int|float]): If this value is specified, use it as the read timeout
+                from the server (instead of the one specified during creation of the connection
+                pool).
+            **kwargs (dict): Arbitrary keyword arguments
+
+        Note:
+            In regular groups (non-supergroups), this method will only work if the
+            ‘All Members Are Admins’ setting is off in the target group.
+
+        Returns:
+            bool: On success, `True` is returned.
+
+        Raises:
+            :class:`telegram.TelegramError`
+
+        """
+        url = '{0}/deleteChatPhoto'.format(self.base_url)
+
+        data = {'chat_id': chat_id}
+
+        result = self._request.post(url, data, timeout=timeout)
+
+        return result
+
+    @log
+    def set_chat_title(self, chat_id, title, timeout=None, **kwargs):
+        """Use this method to change the title of a chat.
+
+        Titles can't be changed for private chats. The bot must be an administrator in the chat
+        for this to work and must have the appropriate admin rights.
+
+        Args:
+            chat_id (int|str): Unique identifier for the target chat or username of the target
+                channel (in the format @channelusername)
+            title (str): New chat title, 1-255 characters
+            timeout (Optional[int|float]): If this value is specified, use it as the read timeout
+                from the server (instead of the one specified during creation of the connection
+                pool).
+            **kwargs (dict): Arbitrary keyword arguments
+
+        Note:
+            In regular groups (non-supergroups), this method will only work if the
+            ‘All Members Are Admins’ setting is off in the target group.
+
+        Returns:
+            bool: On success, `True` is returned.
+
+        Raises:
+            :class:`telegram.TelegramError`
+
+        """
+        url = '{0}/setChatTitle'.format(self.base_url)
+
+        data = {'chat_id': chat_id, 'title': title}
+
+        result = self._request.post(url, data, timeout=timeout)
+
+        return result
+
+    @log
+    def set_chat_description(self, chat_id, description, timeout=None, **kwargs):
+        """Use this method to change the description of a supergroup or a channel.
+
+        The bot must be an administrator in the chat for this to work and must have the
+            appropriate admin rights.
+
+        Args:
+            chat_id (int|str): Unique identifier for the target chat or username of the target
+                channel (in the format @channelusername)
+            description (str): New chat description, 1-255 characters
+            timeout (Optional[int|float]): If this value is specified, use it as the read timeout
+                from the server (instead of the one specified during creation of the connection
+                pool).
+            **kwargs (dict): Arbitrary keyword arguments
+
+        Returns:
+            bool: On success, `True` is returned.
+
+        Raises:
+            :class:`telegram.TelegramError`
+
+        """
+        url = '{0}/setChatDescription'.format(self.base_url)
+
+        data = {'chat_id': chat_id, 'description': description}
+
+        result = self._request.post(url, data, timeout=timeout)
+
+        return result
+
+    @log
+    def pin_chat_message(self, chat_id, message_id, disable_notification=None, timeout=None,
+                         **kwargs):
+        """Use this method to pin a message in a supergroup.
+
+        The bot must be an administrator in the chat for this to work and must have the
+            appropriate admin rights.
+
+        Args:
+            chat_id (int|str): Unique identifier for the target chat or username of the target
+                channel (in the format @channelusername)
+            message_id (int): Identifier of a message to pin
+            disable_notification (boolean): Pass True, if it is not necessary to send a
+                notification to all group members about the new pinned message
+            timeout (Optional[int|float]): If this value is specified, use it as the read timeout
+                from the server (instead of the one specified during creation of the connection
+                pool).
+            **kwargs (dict): Arbitrary keyword arguments
+
+        Returns:
+            bool: On success, `True` is returned.
+
+        Raises:
+            :class:`telegram.TelegramError`
+
+        """
+        url = '{0}/pinChatMessage'.format(self.base_url)
+
+        data = {'chat_id': chat_id, 'message_id': message_id}
+
+        if disable_notification is not None:
+            data['disable_notification'] = disable_notification
+
+        result = self._request.post(url, data, timeout=timeout)
+
+        return result
+
+    @log
+    def unpin_chat_message(self, chat_id, timeout=None, **kwargs):
+        """Use this method to unpin a message in a supergroup.
+
+        The bot must be an administrator in the chat for this to work and must have the
+            appropriate admin rights.
+
+        Args:
+            chat_id (int|str): Unique identifier for the target chat or username of the target
+                channel (in the format @channelusername)
+            timeout (Optional[int|float]): If this value is specified, use it as the read timeout
+                from the server (instead of the one specified during creation of the connection
+                pool).
+            **kwargs (dict): Arbitrary keyword arguments
+
+        Returns:
+            bool: On success, `True` is returned.
+
+        Raises:
+            :class:`telegram.TelegramError`
+
+        """
+        url = '{0}/unpinChatMessage'.format(self.base_url)
+
+        data = {'chat_id': chat_id}
+
+        result = self._request.post(url, data, timeout=timeout)
+
+        return result
+
     @staticmethod
     def de_json(data, bot):
         data = super(Bot, Bot).de_json(data, bot)
@@ -2051,3 +2426,12 @@ def __reduce__(self):
     sendInvoice = send_invoice
     answerShippingQuery = answer_shipping_query
     answerPreCheckoutQuery = answer_pre_checkout_query
+    restrictChatMember = restrict_chat_member
+    promoteChatMemmber = promote_chat_member
+    exportChatInviteLink = export_chat_invite_link
+    setChatPhoto = set_chat_photo
+    deleteChatPhoto = delete_chat_photo
+    setChatTitle = set_chat_title
+    setChatDescription = set_chat_description
+    pinChatMessage = pin_chat_message
+    unpinChatMessage = unpin_chat_message
diff --git a/telegram/chat.py b/telegram/chat.py
index a1efa528de7..93f1a0ba7c3 100644
--- a/telegram/chat.py
+++ b/telegram/chat.py
@@ -33,14 +33,13 @@ class Chat(TelegramObject):
         first_name (str): First name of the other party in a private chat
         last_name (str): Last name of the other party in a private chat
         all_members_are_administrators (bool): True if group has 'All Members Are Administrators'
+        photo (Optional[`telegram.ChatPhoto`]): Chat photo. Returned only in getChat.
+        description (str): Description, for supergroups and channel chats. Returned
+            only in getChat.
+        invite_link (str): Chat invite link, for supergroups and channel chats. Returned
+            only in getChat.
 
     Args:
-        id (int):
-        type (str):
-        title (Optional[str]):
-        username(Optional[str]):
-        first_name(Optional[str]):
-        last_name(Optional[str]):
         bot (Optional[telegram.Bot]): The Bot to use for instance methods
         **kwargs (dict): Arbitrary keyword arguments.
 
@@ -59,6 +58,9 @@ def __init__(self,
                  last_name=None,
                  all_members_are_administrators=None,
                  bot=None,
+                 photo=None,
+                 description=None,
+                 invite_link=None,
                  **kwargs):
         # Required
         self.id = int(id)
@@ -69,6 +71,9 @@ def __init__(self,
         self.first_name = first_name
         self.last_name = last_name
         self.all_members_are_administrators = all_members_are_administrators
+        self.photo = photo
+        self.description = description
+        self.invite_link = invite_link
 
         self.bot = bot
         self._id_attrs = (self.id,)
diff --git a/telegram/chatmember.py b/telegram/chatmember.py
index 1ec9377c0b7..1606cbb40a3 100644
--- a/telegram/chatmember.py
+++ b/telegram/chatmember.py
@@ -19,6 +19,7 @@
 """This module contains an object that represents a Telegram ChatMember."""
 
 from telegram import User, TelegramObject
+from telegram.utils.helpers import to_timestamp, from_timestamp
 
 
 class ChatMember(TelegramObject):
@@ -28,10 +29,39 @@ class ChatMember(TelegramObject):
         user (:class:`telegram.User`): Information about the user.
         status (str): The member's status in the chat. Can be 'creator', 'administrator', 'member',
             'left' or 'kicked'.
+        until_date (Optional[:class:`datetime.datetime`]): Restricted and kicked only. Date when
+            restrictions will be lifted for this user.
+        can_be_edited (Optional[boolean]): Administrators only. True, if the bot is allowed to
+            edit administrator privileges of that user
+        can_change_info (Optional[boolean]): Administrators only. True, if the administrator can
+            change the chat title, photo and other settings
+        can_post_messages (Optional[boolean]): Administrators only. True, if the administrator can
+            post in the channel, channels only
+        can_edit_messages (Optional[boolean]): Administrators only. True, if the administrator can
+            edit messages of other users, channels only
+        can_delete_messages (Optional[boolean]): Administrators only. True, if the administrator
+            can delete messages of other user
+        can_invite_users (Optional[boolean]): Administrators only. True, if the administrator can
+            invite new users to the chat
+        can_restrict_members (Optional[boolean]): Administrators only. True, if the administrator
+            can restrict, ban or unban chat members
+        can_pin_messages (Optional[boolean]): Administrators only. True, if the administrator can
+            pin messages, supergroups only
+        can_promote_members (Optional[boolean]): Administrators only. True, if the administrator
+            can add new administrators with a subset of his own privileges or demote administrators
+            that he has promoted, directly or indirectly (promoted by administrators that were
+            appointed by the user)
+        can_send_messages (Optional[boolean]): Restricted only. True, if the user can send text
+            messages, contacts, locations and venues
+        can_send_media_messages (Optional[boolean]): Restricted only. True, if the user can send
+            audios, documents, photos, videos, video notes and voice notes,
+            implies can_send_messages
+        can_send_other_messages (Optional[boolean]): Restricted only. True, if the user can send
+            animations, games, stickers and use inline bots, implies can_send_media_messages
+        can_add_web_page_previews (Optional[boolean]): Restricted only. True, if user may add
+            web page previews to his messages, implies can_send_media_messages
 
     Args:
-        user (:class:`telegram.User`):
-        status (str):
         **kwargs (dict): Arbitrary keyword arguments.
 
     """
@@ -41,10 +71,30 @@ class ChatMember(TelegramObject):
     LEFT = 'left'
     KICKED = 'kicked'
 
-    def __init__(self, user, status, **kwargs):
+    def __init__(self, user, status, until_date=None, can_be_edited=None,
+                 can_change_info=None, can_post_messages=None, can_edit_messages=None,
+                 can_delete_messages=None, can_invite_users=None,
+                 can_restrict_members=None, can_pin_messages=None,
+                 can_promote_members=None, can_send_messages=None,
+                 can_send_media_messages=None, can_send_other_messages=None,
+                 can_add_web_page_previews=None, **kwargs):
         # Required
         self.user = user
         self.status = status
+        self.until_date = until_date
+        self.can_be_edited = can_be_edited
+        self.can_change_info = can_change_info
+        self.can_post_messages = can_post_messages
+        self.can_edit_messages = can_edit_messages
+        self.can_delete_messages = can_delete_messages
+        self.can_invite_users = can_invite_users
+        self.can_restrict_members = can_restrict_members
+        self.can_pin_messages = can_pin_messages
+        self.can_promote_members = can_promote_members
+        self.can_send_messages = can_send_messages
+        self.can_send_media_messages = can_send_media_messages
+        self.can_send_other_messages = can_send_other_messages
+        self.can_add_web_page_previews = can_add_web_page_previews
 
         self._id_attrs = (self.user, self.status)
 
@@ -64,5 +114,17 @@ def de_json(data, bot):
         data = super(ChatMember, ChatMember).de_json(data, bot)
 
         data['user'] = User.de_json(data.get('user'), bot)
+        data['until_date'] = from_timestamp(data.get('until_date', None))
 
         return ChatMember(**data)
+
+    def to_dict(self):
+        """
+        Returns:
+            dict:
+        """
+        data = super(ChatMember, self).to_dict()
+
+        data['until_date'] = to_timestamp(self.until_date)
+
+        return data
diff --git a/telegram/message.py b/telegram/message.py
index d63b636d7f6..7ff279dda42 100644
--- a/telegram/message.py
+++ b/telegram/message.py
@@ -19,14 +19,12 @@
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 """This module contains an object that represents a Telegram Message."""
 import sys
-from datetime import datetime
-from time import mktime
 
 from telegram import (Audio, Contact, Document, Chat, Location, PhotoSize, Sticker, TelegramObject,
                       User, Video, Voice, Venue, MessageEntity, Game, Invoice, SuccessfulPayment,
                       VideoNote)
 from telegram.utils.deprecate import warn_deprecate_obj
-from telegram.utils.helpers import escape_html, escape_markdown
+from telegram.utils.helpers import escape_html, escape_markdown, to_timestamp, from_timestamp
 
 
 class Message(TelegramObject):
@@ -215,14 +213,14 @@ def de_json(data, bot):
         data = super(Message, Message).de_json(data, bot)
 
         data['from_user'] = User.de_json(data.get('from'), bot)
-        data['date'] = Message._fromtimestamp(data['date'])
+        data['date'] = from_timestamp(data['date'])
         data['chat'] = Chat.de_json(data.get('chat'), bot)
         data['entities'] = MessageEntity.de_list(data.get('entities'), bot)
         data['forward_from'] = User.de_json(data.get('forward_from'), bot)
         data['forward_from_chat'] = Chat.de_json(data.get('forward_from_chat'), bot)
-        data['forward_date'] = Message._fromtimestamp(data.get('forward_date'))
+        data['forward_date'] = from_timestamp(data.get('forward_date'))
         data['reply_to_message'] = Message.de_json(data.get('reply_to_message'), bot)
-        data['edit_date'] = Message._fromtimestamp(data.get('edit_date'))
+        data['edit_date'] = from_timestamp(data.get('edit_date'))
         data['audio'] = Audio.de_json(data.get('audio'), bot)
         data['document'] = Document.de_json(data.get('document'), bot)
         data['game'] = Game.de_json(data.get('game'), bot)
@@ -259,12 +257,12 @@ def to_dict(self):
 
         # Required
         data['from'] = data.pop('from_user', None)
-        data['date'] = self._totimestamp(self.date)
+        data['date'] = to_timestamp(self.date)
         # Optionals
         if self.forward_date:
-            data['forward_date'] = self._totimestamp(self.forward_date)
+            data['forward_date'] = to_timestamp(self.forward_date)
         if self.edit_date:
-            data['edit_date'] = self._totimestamp(self.edit_date)
+            data['edit_date'] = to_timestamp(self.edit_date)
         if self.photo:
             data['photo'] = [p.to_dict() for p in self.photo]
         if self.entities:
@@ -277,39 +275,6 @@ def to_dict(self):
 
         return data
 
-    @staticmethod
-    def _fromtimestamp(unixtime):
-        """
-        Args:
-            unixtime (int):
-
-        Returns:
-            datetime.datetime:
-        """
-        if not unixtime:
-            return None
-
-        return datetime.fromtimestamp(unixtime)
-
-    @staticmethod
-    def _totimestamp(dt_obj):
-        """
-        Args:
-            dt_obj (:class:`datetime.datetime`):
-
-        Returns:
-            int:
-        """
-        if not dt_obj:
-            return None
-
-        try:
-            # Python 3.3+
-            return int(dt_obj.timestamp())
-        except AttributeError:
-            # Python 3 (< 3.3) and Python 2
-            return int(mktime(dt_obj.timetuple()))
-
     def _quote(self, kwargs):
         """Modify kwargs for replying with or without quoting"""
 
diff --git a/telegram/utils/helpers.py b/telegram/utils/helpers.py
index 9d6ed3a4d70..ba6587966a1 100644
--- a/telegram/utils/helpers.py
+++ b/telegram/utils/helpers.py
@@ -19,6 +19,8 @@
 """ This module contains helper functions """
 
 import re
+from datetime import datetime
+from time import mktime
 
 try:
     from html import escape as escape_html  # noqa: F401
@@ -30,3 +32,36 @@ def escape_markdown(text):
     """Helper function to escape telegram markup symbols"""
     escape_chars = '\*_`\['
     return re.sub(r'([%s])' % escape_chars, r'\\\1', text)
+
+
+def to_timestamp(dt_obj):
+    """
+    Args:
+        dt_obj (:class:`datetime.datetime`):
+
+    Returns:
+        int:
+    """
+    if not dt_obj:
+        return None
+
+    try:
+        # Python 3.3+
+        return int(dt_obj.timestamp())
+    except AttributeError:
+        # Python 3 (< 3.3) and Python 2
+        return int(mktime(dt_obj.timetuple()))
+
+
+def from_timestamp(unixtime):
+    """
+    Args:
+        unixtime (int):
+
+    Returns:
+        datetime.datetime:
+    """
+    if not unixtime:
+        return None
+
+    return datetime.fromtimestamp(unixtime)


Code review comments:
diff_hunk: @@ -2051,3 +2426,12 @@ def __reduce__(self):
     sendInvoice = send_invoice
     answerShippingQuery = answer_shipping_query
     answerPreCheckoutQuery = answer_pre_checkout_query
+    restrictChatMember = restrict_chat_member
+    promoteChatMemmber = promote_chat_member
path: telegram/bot.py
text: @user1:
You've mistyped, there should be `promoteChatMember` (that's why `tests.test_official` [fails](https://travis-ci.org/python-telegram-bot/python-telegram-bot/jobs/248911924#L1403) here)