2017-06-09 17:13:39 +03:00
|
|
|
"""
|
|
|
|
Utilities for working with the Telegram API itself (such as handy methods
|
|
|
|
to convert between an entity like an User, Chat, etc. into its Input version)
|
|
|
|
"""
|
2017-10-01 14:24:04 +03:00
|
|
|
import math
|
2018-02-12 12:33:51 +03:00
|
|
|
import mimetypes
|
2018-03-15 11:52:45 +03:00
|
|
|
import os
|
2017-12-27 02:50:09 +03:00
|
|
|
import re
|
2018-02-26 16:12:21 +03:00
|
|
|
import types
|
2018-03-04 02:23:13 +03:00
|
|
|
from collections import UserList
|
2018-03-15 11:52:45 +03:00
|
|
|
from mimetypes import guess_extension
|
2017-12-27 02:50:09 +03:00
|
|
|
|
2017-06-09 17:13:39 +03:00
|
|
|
from .tl.types import (
|
2017-06-15 18:03:59 +03:00
|
|
|
Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull,
|
2017-07-10 17:09:20 +03:00
|
|
|
ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty,
|
2017-07-07 10:48:06 +03:00
|
|
|
MessageMediaDocument, MessageMediaPhoto, PeerChannel, InputChannel,
|
2017-07-10 17:09:20 +03:00
|
|
|
UserEmpty, InputUser, InputUserEmpty, InputUserSelf, InputPeerSelf,
|
2017-09-25 14:43:03 +03:00
|
|
|
PeerChat, PeerUser, User, UserFull, UserProfilePhoto, Document,
|
|
|
|
MessageMediaContact, MessageMediaEmpty, MessageMediaGame, MessageMediaGeo,
|
|
|
|
MessageMediaUnsupported, MessageMediaVenue, InputMediaContact,
|
|
|
|
InputMediaDocument, InputMediaEmpty, InputMediaGame,
|
|
|
|
InputMediaGeoPoint, InputMediaPhoto, InputMediaVenue, InputDocument,
|
|
|
|
DocumentEmpty, InputDocumentEmpty, Message, GeoPoint, InputGeoPoint,
|
|
|
|
GeoPointEmpty, InputGeoPointEmpty, Photo, InputPhoto, PhotoEmpty,
|
|
|
|
InputPhotoEmpty, FileLocation, ChatPhotoEmpty, UserProfilePhotoEmpty,
|
2017-11-12 20:15:32 +03:00
|
|
|
FileLocationUnavailable, InputMediaUploadedDocument, ChannelFull,
|
2018-01-21 13:04:46 +03:00
|
|
|
InputMediaUploadedPhoto, DocumentAttributeFilename, photos,
|
2018-04-23 12:05:38 +03:00
|
|
|
TopPeer, InputNotifyPeer, InputMessageID
|
2017-10-08 14:45:14 +03:00
|
|
|
)
|
2018-02-12 12:33:51 +03:00
|
|
|
from .tl.types.contacts import ResolvedPeer
|
2016-10-09 13:57:38 +03:00
|
|
|
|
2017-12-27 02:50:09 +03:00
|
|
|
USERNAME_RE = re.compile(
|
2018-02-22 12:27:12 +03:00
|
|
|
r'@|(?:https?://)?(?:www\.)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?'
|
2017-12-27 02:50:09 +03:00
|
|
|
)
|
|
|
|
|
2018-02-19 23:03:33 +03:00
|
|
|
VALID_USERNAME_RE = re.compile(r'^[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]$')
|
|
|
|
|
2017-12-27 02:50:09 +03:00
|
|
|
|
2016-10-09 13:57:38 +03:00
|
|
|
def get_display_name(entity):
|
2018-03-01 15:21:28 +03:00
|
|
|
"""
|
2018-03-23 23:40:24 +03:00
|
|
|
Gets the display name for the given entity, if it's an :tl:`User`,
|
|
|
|
:tl:`Chat` or :tl:`Channel`. Returns an empty string otherwise.
|
2018-03-01 15:21:28 +03:00
|
|
|
"""
|
2016-10-09 13:57:38 +03:00
|
|
|
if isinstance(entity, User):
|
2017-06-16 10:11:49 +03:00
|
|
|
if entity.last_name and entity.first_name:
|
2016-10-09 13:57:38 +03:00
|
|
|
return '{} {}'.format(entity.first_name, entity.last_name)
|
2017-06-16 10:11:49 +03:00
|
|
|
elif entity.first_name:
|
|
|
|
return entity.first_name
|
|
|
|
elif entity.last_name:
|
|
|
|
return entity.last_name
|
|
|
|
else:
|
2017-12-24 18:18:09 +03:00
|
|
|
return ''
|
2016-10-09 13:57:38 +03:00
|
|
|
|
2017-12-24 18:18:09 +03:00
|
|
|
elif isinstance(entity, (Chat, Channel)):
|
2016-10-09 13:57:38 +03:00
|
|
|
return entity.title
|
|
|
|
|
2017-12-24 18:18:09 +03:00
|
|
|
return ''
|
2017-05-23 10:45:48 +03:00
|
|
|
|
2016-10-09 13:57:38 +03:00
|
|
|
|
|
|
|
def get_extension(media):
|
2018-03-23 23:40:24 +03:00
|
|
|
"""Gets the corresponding extension for any Telegram media."""
|
2016-10-09 13:57:38 +03:00
|
|
|
|
|
|
|
# Photos are always compressed as .jpg by Telegram
|
2017-10-13 12:38:12 +03:00
|
|
|
if isinstance(media, (UserProfilePhoto, ChatPhoto, MessageMediaPhoto)):
|
2016-10-09 13:57:38 +03:00
|
|
|
return '.jpg'
|
|
|
|
|
2017-08-24 18:44:38 +03:00
|
|
|
# Documents will come with a mime type
|
2016-10-09 13:57:38 +03:00
|
|
|
if isinstance(media, MessageMediaDocument):
|
2018-01-23 14:10:23 +03:00
|
|
|
media = media.document
|
|
|
|
if isinstance(media, Document):
|
|
|
|
if media.mime_type == 'application/octet-stream':
|
|
|
|
# Octet stream are just bytes, which have no default extension
|
|
|
|
return ''
|
|
|
|
else:
|
|
|
|
return guess_extension(media.mime_type) or ''
|
2017-08-24 18:44:38 +03:00
|
|
|
|
|
|
|
return ''
|
2016-10-09 13:57:38 +03:00
|
|
|
|
|
|
|
|
2017-08-30 12:12:25 +03:00
|
|
|
def _raise_cast_fail(entity, target):
|
2017-12-28 02:22:28 +03:00
|
|
|
raise TypeError('Cannot cast {} to any kind of {}.'.format(
|
|
|
|
type(entity).__name__, target))
|
2017-08-30 12:12:25 +03:00
|
|
|
|
|
|
|
|
2017-10-05 14:14:54 +03:00
|
|
|
def get_input_peer(entity, allow_self=True):
|
2018-03-23 23:40:24 +03:00
|
|
|
"""
|
|
|
|
Gets the input peer for the given "entity" (user, chat or channel).
|
|
|
|
A ``TypeError`` is raised if the given entity isn't a supported type.
|
|
|
|
"""
|
2018-01-19 15:00:17 +03:00
|
|
|
try:
|
|
|
|
if entity.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer')
|
|
|
|
return entity
|
|
|
|
except AttributeError:
|
2018-04-13 14:08:29 +03:00
|
|
|
# e.g. custom.Dialog (can't cyclic import).
|
|
|
|
if allow_self and hasattr(entity, 'input_entity'):
|
2018-04-06 20:11:31 +03:00
|
|
|
return entity.input_entity
|
2018-04-13 14:08:29 +03:00
|
|
|
elif hasattr(entity, 'entity'):
|
|
|
|
return get_input_peer(entity.entity)
|
2018-04-06 20:11:31 +03:00
|
|
|
else:
|
|
|
|
_raise_cast_fail(entity, 'InputPeer')
|
2017-08-30 12:12:25 +03:00
|
|
|
|
2016-10-09 13:57:38 +03:00
|
|
|
if isinstance(entity, User):
|
2017-10-05 14:14:54 +03:00
|
|
|
if entity.is_self and allow_self:
|
2017-07-10 17:09:20 +03:00
|
|
|
return InputPeerSelf()
|
|
|
|
else:
|
2017-10-30 12:33:45 +03:00
|
|
|
return InputPeerUser(entity.id, entity.access_hash or 0)
|
2017-06-15 18:03:59 +03:00
|
|
|
|
2017-10-13 12:38:12 +03:00
|
|
|
if isinstance(entity, (Chat, ChatEmpty, ChatForbidden)):
|
2016-10-09 13:57:38 +03:00
|
|
|
return InputPeerChat(entity.id)
|
2017-06-15 18:03:59 +03:00
|
|
|
|
2017-10-13 12:38:12 +03:00
|
|
|
if isinstance(entity, (Channel, ChannelForbidden)):
|
2017-10-30 12:33:45 +03:00
|
|
|
return InputPeerChannel(entity.id, entity.access_hash or 0)
|
2016-10-09 13:57:38 +03:00
|
|
|
|
2017-07-10 17:09:20 +03:00
|
|
|
if isinstance(entity, InputUser):
|
|
|
|
return InputPeerUser(entity.user_id, entity.access_hash)
|
|
|
|
|
2018-01-20 21:29:05 +03:00
|
|
|
if isinstance(entity, InputChannel):
|
|
|
|
return InputPeerChannel(entity.channel_id, entity.access_hash)
|
|
|
|
|
2017-11-12 20:03:42 +03:00
|
|
|
if isinstance(entity, InputUserSelf):
|
|
|
|
return InputPeerSelf()
|
|
|
|
|
2018-01-20 21:29:05 +03:00
|
|
|
if isinstance(entity, UserEmpty):
|
|
|
|
return InputPeerEmpty()
|
|
|
|
|
2017-06-15 18:03:59 +03:00
|
|
|
if isinstance(entity, UserFull):
|
2017-07-10 17:09:20 +03:00
|
|
|
return get_input_peer(entity.user)
|
2017-06-15 18:03:59 +03:00
|
|
|
|
|
|
|
if isinstance(entity, ChatFull):
|
|
|
|
return InputPeerChat(entity.id)
|
|
|
|
|
2017-07-04 22:18:35 +03:00
|
|
|
if isinstance(entity, PeerChat):
|
|
|
|
return InputPeerChat(entity.chat_id)
|
|
|
|
|
2017-08-30 12:12:25 +03:00
|
|
|
_raise_cast_fail(entity, 'InputPeer')
|
2017-05-23 10:45:48 +03:00
|
|
|
|
2016-10-09 13:57:38 +03:00
|
|
|
|
2017-07-07 10:48:06 +03:00
|
|
|
def get_input_channel(entity):
|
2018-03-23 23:40:24 +03:00
|
|
|
"""Similar to :meth:`get_input_peer`, but for :tl:`InputChannel`'s alone."""
|
2018-01-19 15:00:17 +03:00
|
|
|
try:
|
|
|
|
if entity.SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel')
|
|
|
|
return entity
|
|
|
|
except AttributeError:
|
2017-08-30 12:12:25 +03:00
|
|
|
_raise_cast_fail(entity, 'InputChannel')
|
|
|
|
|
2017-10-13 12:38:12 +03:00
|
|
|
if isinstance(entity, (Channel, ChannelForbidden)):
|
2017-10-30 12:33:45 +03:00
|
|
|
return InputChannel(entity.id, entity.access_hash or 0)
|
2017-07-07 10:48:06 +03:00
|
|
|
|
2017-08-05 10:37:34 +03:00
|
|
|
if isinstance(entity, InputPeerChannel):
|
|
|
|
return InputChannel(entity.channel_id, entity.access_hash)
|
|
|
|
|
2017-08-30 12:12:25 +03:00
|
|
|
_raise_cast_fail(entity, 'InputChannel')
|
2017-07-07 10:48:06 +03:00
|
|
|
|
|
|
|
|
2017-07-10 17:04:10 +03:00
|
|
|
def get_input_user(entity):
|
2018-03-23 23:40:24 +03:00
|
|
|
"""Similar to :meth:`get_input_peer`, but for :tl:`InputUser`'s alone."""
|
2018-01-19 15:00:17 +03:00
|
|
|
try:
|
|
|
|
if entity.SUBCLASS_OF_ID == 0xe669bf46: # crc32(b'InputUser'):
|
|
|
|
return entity
|
|
|
|
except AttributeError:
|
2017-08-30 12:12:25 +03:00
|
|
|
_raise_cast_fail(entity, 'InputUser')
|
|
|
|
|
2017-07-10 17:04:10 +03:00
|
|
|
if isinstance(entity, User):
|
|
|
|
if entity.is_self:
|
|
|
|
return InputUserSelf()
|
|
|
|
else:
|
2017-10-30 12:33:45 +03:00
|
|
|
return InputUser(entity.id, entity.access_hash or 0)
|
2017-07-10 17:04:10 +03:00
|
|
|
|
2017-10-24 10:42:51 +03:00
|
|
|
if isinstance(entity, InputPeerSelf):
|
|
|
|
return InputUserSelf()
|
|
|
|
|
|
|
|
if isinstance(entity, (UserEmpty, InputPeerEmpty)):
|
2017-07-10 17:04:10 +03:00
|
|
|
return InputUserEmpty()
|
|
|
|
|
|
|
|
if isinstance(entity, UserFull):
|
|
|
|
return get_input_user(entity.user)
|
|
|
|
|
2017-07-10 17:09:20 +03:00
|
|
|
if isinstance(entity, InputPeerUser):
|
|
|
|
return InputUser(entity.user_id, entity.access_hash)
|
|
|
|
|
2017-08-30 12:12:25 +03:00
|
|
|
_raise_cast_fail(entity, 'InputUser')
|
2017-07-10 17:04:10 +03:00
|
|
|
|
|
|
|
|
2017-09-25 14:43:03 +03:00
|
|
|
def get_input_document(document):
|
2018-03-23 23:40:24 +03:00
|
|
|
"""Similar to :meth:`get_input_peer`, but for documents"""
|
2018-01-19 15:00:17 +03:00
|
|
|
try:
|
|
|
|
if document.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument'):
|
|
|
|
return document
|
|
|
|
except AttributeError:
|
2017-09-25 14:43:03 +03:00
|
|
|
_raise_cast_fail(document, 'InputDocument')
|
|
|
|
|
|
|
|
if isinstance(document, Document):
|
|
|
|
return InputDocument(id=document.id, access_hash=document.access_hash)
|
|
|
|
|
|
|
|
if isinstance(document, DocumentEmpty):
|
|
|
|
return InputDocumentEmpty()
|
|
|
|
|
|
|
|
if isinstance(document, MessageMediaDocument):
|
|
|
|
return get_input_document(document.document)
|
|
|
|
|
|
|
|
if isinstance(document, Message):
|
|
|
|
return get_input_document(document.media)
|
|
|
|
|
|
|
|
_raise_cast_fail(document, 'InputDocument')
|
|
|
|
|
|
|
|
|
|
|
|
def get_input_photo(photo):
|
2018-03-23 23:40:24 +03:00
|
|
|
"""Similar to :meth:`get_input_peer`, but for photos"""
|
2018-01-19 15:00:17 +03:00
|
|
|
try:
|
|
|
|
if photo.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto'):
|
|
|
|
return photo
|
|
|
|
except AttributeError:
|
2017-09-25 14:43:03 +03:00
|
|
|
_raise_cast_fail(photo, 'InputPhoto')
|
|
|
|
|
2017-10-08 14:45:14 +03:00
|
|
|
if isinstance(photo, photos.Photo):
|
|
|
|
photo = photo.photo
|
|
|
|
|
2017-09-25 14:43:03 +03:00
|
|
|
if isinstance(photo, Photo):
|
|
|
|
return InputPhoto(id=photo.id, access_hash=photo.access_hash)
|
|
|
|
|
|
|
|
if isinstance(photo, PhotoEmpty):
|
|
|
|
return InputPhotoEmpty()
|
|
|
|
|
|
|
|
_raise_cast_fail(photo, 'InputPhoto')
|
|
|
|
|
|
|
|
|
|
|
|
def get_input_geo(geo):
|
2018-03-23 23:40:24 +03:00
|
|
|
"""Similar to :meth:`get_input_peer`, but for geo points"""
|
2018-01-19 15:00:17 +03:00
|
|
|
try:
|
|
|
|
if geo.SUBCLASS_OF_ID == 0x430d225: # crc32(b'InputGeoPoint'):
|
|
|
|
return geo
|
|
|
|
except AttributeError:
|
2017-09-25 14:43:03 +03:00
|
|
|
_raise_cast_fail(geo, 'InputGeoPoint')
|
|
|
|
|
|
|
|
if isinstance(geo, GeoPoint):
|
|
|
|
return InputGeoPoint(lat=geo.lat, long=geo.long)
|
|
|
|
|
|
|
|
if isinstance(geo, GeoPointEmpty):
|
|
|
|
return InputGeoPointEmpty()
|
|
|
|
|
|
|
|
if isinstance(geo, MessageMediaGeo):
|
|
|
|
return get_input_geo(geo.geo)
|
|
|
|
|
|
|
|
if isinstance(geo, Message):
|
|
|
|
return get_input_geo(geo.media)
|
|
|
|
|
|
|
|
_raise_cast_fail(geo, 'InputGeoPoint')
|
|
|
|
|
|
|
|
|
2018-03-02 23:28:33 +03:00
|
|
|
def get_input_media(media, is_photo=False):
|
2018-03-23 23:40:24 +03:00
|
|
|
"""
|
|
|
|
Similar to :meth:`get_input_peer`, but for media.
|
2017-09-25 14:43:03 +03:00
|
|
|
|
2018-03-23 23:40:24 +03:00
|
|
|
If the media is a file location and ``is_photo`` is known to be ``True``,
|
|
|
|
it will be treated as an :tl:`InputMediaUploadedPhoto`.
|
2017-09-25 14:43:03 +03:00
|
|
|
"""
|
2018-01-19 15:00:17 +03:00
|
|
|
try:
|
2018-04-23 16:33:44 +03:00
|
|
|
if media.SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia')
|
2018-01-19 15:00:17 +03:00
|
|
|
return media
|
2018-04-23 16:33:44 +03:00
|
|
|
elif media.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto')
|
|
|
|
return InputMediaPhoto(media)
|
|
|
|
elif media.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument')
|
|
|
|
return InputMediaDocument(media)
|
2018-01-19 15:00:17 +03:00
|
|
|
except AttributeError:
|
2017-09-25 14:43:03 +03:00
|
|
|
_raise_cast_fail(media, 'InputMedia')
|
|
|
|
|
|
|
|
if isinstance(media, MessageMediaPhoto):
|
|
|
|
return InputMediaPhoto(
|
|
|
|
id=get_input_photo(media.photo),
|
2018-03-02 23:28:33 +03:00
|
|
|
ttl_seconds=media.ttl_seconds
|
2017-09-25 14:43:03 +03:00
|
|
|
)
|
|
|
|
|
2018-03-27 12:22:31 +03:00
|
|
|
if isinstance(media, (Photo, photos.Photo, PhotoEmpty)):
|
|
|
|
return InputMediaPhoto(
|
|
|
|
id=get_input_photo(media)
|
|
|
|
)
|
|
|
|
|
2017-09-25 14:43:03 +03:00
|
|
|
if isinstance(media, MessageMediaDocument):
|
|
|
|
return InputMediaDocument(
|
|
|
|
id=get_input_document(media.document),
|
2018-03-02 23:28:33 +03:00
|
|
|
ttl_seconds=media.ttl_seconds
|
2017-09-25 14:43:03 +03:00
|
|
|
)
|
|
|
|
|
2018-03-27 12:22:31 +03:00
|
|
|
if isinstance(media, (Document, DocumentEmpty)):
|
|
|
|
return InputMediaDocument(
|
|
|
|
id=get_input_document(media)
|
|
|
|
)
|
|
|
|
|
2017-09-25 14:43:03 +03:00
|
|
|
if isinstance(media, FileLocation):
|
|
|
|
if is_photo:
|
2018-03-02 23:28:33 +03:00
|
|
|
return InputMediaUploadedPhoto(file=media)
|
2017-09-25 14:43:03 +03:00
|
|
|
else:
|
|
|
|
return InputMediaUploadedDocument(
|
|
|
|
file=media,
|
|
|
|
mime_type='application/octet-stream', # unknown, assume bytes
|
2018-03-02 23:28:33 +03:00
|
|
|
attributes=[DocumentAttributeFilename('unnamed')]
|
2017-09-25 14:43:03 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
if isinstance(media, MessageMediaGame):
|
|
|
|
return InputMediaGame(id=media.game.id)
|
|
|
|
|
2017-10-13 12:38:12 +03:00
|
|
|
if isinstance(media, (ChatPhoto, UserProfilePhoto)):
|
2017-09-25 14:43:03 +03:00
|
|
|
if isinstance(media.photo_big, FileLocationUnavailable):
|
2018-01-23 14:04:35 +03:00
|
|
|
media = media.photo_small
|
2017-09-25 14:43:03 +03:00
|
|
|
else:
|
2018-01-23 14:04:35 +03:00
|
|
|
media = media.photo_big
|
2018-03-02 23:28:33 +03:00
|
|
|
return get_input_media(media, is_photo=True)
|
2017-09-25 14:43:03 +03:00
|
|
|
|
|
|
|
if isinstance(media, MessageMediaContact):
|
|
|
|
return InputMediaContact(
|
|
|
|
phone_number=media.phone_number,
|
|
|
|
first_name=media.first_name,
|
|
|
|
last_name=media.last_name
|
|
|
|
)
|
|
|
|
|
|
|
|
if isinstance(media, MessageMediaGeo):
|
|
|
|
return InputMediaGeoPoint(geo_point=get_input_geo(media.geo))
|
|
|
|
|
|
|
|
if isinstance(media, MessageMediaVenue):
|
|
|
|
return InputMediaVenue(
|
|
|
|
geo_point=get_input_geo(media.geo),
|
|
|
|
title=media.title,
|
|
|
|
address=media.address,
|
|
|
|
provider=media.provider,
|
2018-01-16 16:01:14 +03:00
|
|
|
venue_id=media.venue_id,
|
|
|
|
venue_type=''
|
2017-09-25 14:43:03 +03:00
|
|
|
)
|
|
|
|
|
2017-10-13 12:38:12 +03:00
|
|
|
if isinstance(media, (
|
2017-09-25 14:43:03 +03:00
|
|
|
MessageMediaEmpty, MessageMediaUnsupported,
|
2017-10-13 12:38:12 +03:00
|
|
|
ChatPhotoEmpty, UserProfilePhotoEmpty, FileLocationUnavailable)):
|
2017-09-25 14:43:03 +03:00
|
|
|
return InputMediaEmpty()
|
|
|
|
|
|
|
|
if isinstance(media, Message):
|
2018-03-02 23:28:33 +03:00
|
|
|
return get_input_media(media.media, is_photo=is_photo)
|
2017-09-25 14:43:03 +03:00
|
|
|
|
|
|
|
_raise_cast_fail(media, 'InputMedia')
|
|
|
|
|
|
|
|
|
2018-04-23 12:05:38 +03:00
|
|
|
def get_input_message(message):
|
|
|
|
"""Similar to :meth:`get_input_peer`, but for input messages."""
|
|
|
|
try:
|
|
|
|
if isinstance(message, int): # This case is really common too
|
|
|
|
return InputMessageID(message)
|
|
|
|
elif message.SUBCLASS_OF_ID == 0x54b6bcc5: # crc32(b'InputMessage'):
|
|
|
|
return message
|
|
|
|
elif message.SUBCLASS_OF_ID == 0x790009e3: # crc32(b'Message'):
|
|
|
|
return InputMessageID(message.id)
|
|
|
|
except AttributeError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
_raise_cast_fail(message, 'InputMedia')
|
|
|
|
|
|
|
|
|
2018-01-15 20:15:30 +03:00
|
|
|
def is_image(file):
|
2018-03-15 11:52:45 +03:00
|
|
|
"""
|
2018-03-23 23:40:24 +03:00
|
|
|
Returns ``True`` if the file extension looks like an image file to Telegram.
|
2018-03-15 11:52:45 +03:00
|
|
|
"""
|
2018-03-11 11:38:52 +03:00
|
|
|
if not isinstance(file, str):
|
|
|
|
return False
|
2018-03-15 11:52:45 +03:00
|
|
|
_, ext = os.path.splitext(file)
|
2018-03-15 12:13:57 +03:00
|
|
|
return re.match(r'\.(png|jpe?g)', ext, re.IGNORECASE)
|
2018-02-12 12:33:51 +03:00
|
|
|
|
2018-02-19 23:03:33 +03:00
|
|
|
|
2018-02-17 15:00:58 +03:00
|
|
|
def is_audio(file):
|
2018-03-23 23:40:24 +03:00
|
|
|
"""Returns ``True`` if the file extension looks like an audio file."""
|
2018-02-19 17:29:32 +03:00
|
|
|
return (isinstance(file, str) and
|
|
|
|
(mimetypes.guess_type(file)[0] or '').startswith('audio/'))
|
2018-02-17 15:00:58 +03:00
|
|
|
|
|
|
|
|
2018-02-12 12:33:51 +03:00
|
|
|
def is_video(file):
|
2018-03-23 23:40:24 +03:00
|
|
|
"""Returns ``True`` if the file extension looks like a video file."""
|
2018-02-19 17:29:32 +03:00
|
|
|
return (isinstance(file, str) and
|
|
|
|
(mimetypes.guess_type(file)[0] or '').startswith('video/'))
|
2018-01-15 20:15:30 +03:00
|
|
|
|
|
|
|
|
2018-02-26 16:12:21 +03:00
|
|
|
def is_list_like(obj):
|
|
|
|
"""
|
2018-03-23 23:40:24 +03:00
|
|
|
Returns ``True`` if the given object looks like a list.
|
2018-02-26 16:12:21 +03:00
|
|
|
|
2018-03-23 23:40:24 +03:00
|
|
|
Checking ``if hasattr(obj, '__iter__')`` and ignoring ``str/bytes`` is not
|
|
|
|
enough. Things like ``open()`` are also iterable (and probably many
|
2018-02-26 16:12:21 +03:00
|
|
|
other things), so just support the commonly known list-like objects.
|
|
|
|
"""
|
2018-03-04 02:23:13 +03:00
|
|
|
return isinstance(obj, (list, tuple, set, dict,
|
|
|
|
UserList, types.GeneratorType))
|
2018-02-26 16:12:21 +03:00
|
|
|
|
|
|
|
|
2017-12-27 02:50:09 +03:00
|
|
|
def parse_phone(phone):
|
2018-03-23 23:40:24 +03:00
|
|
|
"""Parses the given phone, or returns ``None`` if it's invalid."""
|
2017-12-27 02:50:09 +03:00
|
|
|
if isinstance(phone, int):
|
|
|
|
return str(phone)
|
|
|
|
else:
|
|
|
|
phone = re.sub(r'[+()\s-]', '', str(phone))
|
|
|
|
if phone.isdigit():
|
|
|
|
return phone
|
|
|
|
|
|
|
|
|
|
|
|
def parse_username(username):
|
|
|
|
"""Parses the given username or channel access hash, given
|
|
|
|
a string, username or URL. Returns a tuple consisting of
|
2017-12-27 13:54:08 +03:00
|
|
|
both the stripped, lowercase username and whether it is
|
|
|
|
a joinchat/ hash (in which case is not lowercase'd).
|
2018-02-19 23:03:33 +03:00
|
|
|
|
2018-03-23 23:40:24 +03:00
|
|
|
Returns ``None`` if the ``username`` is not valid.
|
2017-12-27 02:50:09 +03:00
|
|
|
"""
|
|
|
|
username = username.strip()
|
|
|
|
m = USERNAME_RE.match(username)
|
|
|
|
if m:
|
2018-02-19 23:03:33 +03:00
|
|
|
username = username[m.end():]
|
2017-12-27 13:54:08 +03:00
|
|
|
is_invite = bool(m.group(1))
|
2018-02-19 23:03:33 +03:00
|
|
|
if is_invite:
|
|
|
|
return username, True
|
2018-02-22 12:27:12 +03:00
|
|
|
else:
|
|
|
|
username = username.rstrip('/')
|
2018-02-19 23:03:33 +03:00
|
|
|
|
|
|
|
if VALID_USERNAME_RE.match(username):
|
2017-12-27 13:54:08 +03:00
|
|
|
return username.lower(), False
|
2018-02-19 23:03:33 +03:00
|
|
|
else:
|
|
|
|
return None, False
|
2017-12-27 02:50:09 +03:00
|
|
|
|
|
|
|
|
2018-03-10 14:13:17 +03:00
|
|
|
def _fix_peer_id(peer_id):
|
|
|
|
"""
|
|
|
|
Fixes the peer ID for chats and channels, in case the users
|
2018-03-23 23:40:24 +03:00
|
|
|
mix marking the ID with the :tl:`Peer` constructors.
|
2018-03-10 14:13:17 +03:00
|
|
|
"""
|
|
|
|
peer_id = abs(peer_id)
|
|
|
|
if str(peer_id).startswith('100'):
|
|
|
|
peer_id = str(peer_id)[3:]
|
|
|
|
return int(peer_id)
|
|
|
|
|
|
|
|
|
2017-12-28 15:31:43 +03:00
|
|
|
def get_peer_id(peer):
|
|
|
|
"""
|
|
|
|
Finds the ID of the given peer, and converts it to the "bot api" format
|
|
|
|
so it the peer can be identified back. User ID is left unmodified,
|
|
|
|
chat ID is negated, and channel ID is prefixed with -100.
|
|
|
|
|
|
|
|
The original ID and the peer type class can be returned with
|
2018-03-23 23:40:24 +03:00
|
|
|
a call to :meth:`resolve_id(marked_id)`.
|
2017-10-01 14:24:04 +03:00
|
|
|
"""
|
2017-10-06 22:47:10 +03:00
|
|
|
# First we assert it's a Peer TLObject, or early return for integers
|
2018-01-19 15:00:17 +03:00
|
|
|
if isinstance(peer, int):
|
|
|
|
return peer
|
|
|
|
|
|
|
|
try:
|
|
|
|
if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6):
|
2018-01-21 13:04:46 +03:00
|
|
|
if isinstance(peer, (ResolvedPeer, InputNotifyPeer, TopPeer)):
|
|
|
|
peer = peer.peer
|
|
|
|
else:
|
|
|
|
# Not a Peer or an InputPeer, so first get its Input version
|
|
|
|
peer = get_input_peer(peer, allow_self=False)
|
2018-01-19 15:00:17 +03:00
|
|
|
except AttributeError:
|
|
|
|
_raise_cast_fail(peer, 'int')
|
2017-10-05 13:59:44 +03:00
|
|
|
|
2017-10-06 22:47:10 +03:00
|
|
|
# Set the right ID/kind, or raise if the TLObject is not recognised
|
2017-10-13 12:38:12 +03:00
|
|
|
if isinstance(peer, (PeerUser, InputPeerUser)):
|
2017-10-09 20:40:39 +03:00
|
|
|
return peer.user_id
|
2017-10-13 12:38:12 +03:00
|
|
|
elif isinstance(peer, (PeerChat, InputPeerChat)):
|
2018-03-10 14:13:17 +03:00
|
|
|
# Check in case the user mixed things up to avoid blowing up
|
|
|
|
if not (0 < peer.chat_id <= 0x7fffffff):
|
|
|
|
peer.chat_id = _fix_peer_id(peer.chat_id)
|
|
|
|
|
2017-12-28 15:31:43 +03:00
|
|
|
return -peer.chat_id
|
2017-11-12 20:15:32 +03:00
|
|
|
elif isinstance(peer, (PeerChannel, InputPeerChannel, ChannelFull)):
|
|
|
|
if isinstance(peer, ChannelFull):
|
|
|
|
# Special case: .get_input_peer can't return InputChannel from
|
|
|
|
# ChannelFull since it doesn't have an .access_hash attribute.
|
|
|
|
i = peer.id
|
|
|
|
else:
|
|
|
|
i = peer.channel_id
|
2018-03-10 14:13:17 +03:00
|
|
|
|
|
|
|
# Check in case the user mixed things up to avoid blowing up
|
|
|
|
if not (0 < i <= 0x7fffffff):
|
|
|
|
i = _fix_peer_id(i)
|
|
|
|
if isinstance(peer, ChannelFull):
|
|
|
|
peer.id = i
|
|
|
|
else:
|
|
|
|
peer.channel_id = i
|
|
|
|
|
2017-12-28 15:31:43 +03:00
|
|
|
# Concat -100 through math tricks, .to_supergroup() on Madeline
|
|
|
|
# IDs will be strictly positive -> log works
|
|
|
|
return -(i + pow(10, math.floor(math.log10(i) + 3)))
|
2017-10-06 22:42:04 +03:00
|
|
|
|
2017-10-09 20:40:39 +03:00
|
|
|
_raise_cast_fail(peer, 'int')
|
2017-10-01 14:24:04 +03:00
|
|
|
|
|
|
|
|
|
|
|
def resolve_id(marked_id):
|
2018-03-23 23:40:24 +03:00
|
|
|
"""Given a marked ID, returns the original ID and its :tl:`Peer` type."""
|
2017-10-01 14:24:04 +03:00
|
|
|
if marked_id >= 0:
|
|
|
|
return marked_id, PeerUser
|
|
|
|
|
|
|
|
if str(marked_id).startswith('-100'):
|
|
|
|
return int(str(marked_id)[4:]), PeerChannel
|
|
|
|
|
|
|
|
return -marked_id, PeerChat
|
|
|
|
|
|
|
|
|
2017-05-21 14:59:16 +03:00
|
|
|
def get_appropriated_part_size(file_size):
|
2018-03-23 23:40:24 +03:00
|
|
|
"""
|
|
|
|
Gets the appropriated part size when uploading or downloading files,
|
|
|
|
given an initial file size.
|
|
|
|
"""
|
2017-10-09 14:19:03 +03:00
|
|
|
if file_size <= 104857600: # 100MB
|
2016-10-09 13:57:38 +03:00
|
|
|
return 128
|
|
|
|
if file_size <= 786432000: # 750MB
|
|
|
|
return 256
|
|
|
|
if file_size <= 1572864000: # 1500MB
|
|
|
|
return 512
|
|
|
|
|
|
|
|
raise ValueError('File size too large')
|