Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Call discarded after confirming #3981

Closed
2 of 3 tasks
huco95 opened this issue Nov 23, 2022 · 9 comments
Closed
2 of 3 tasks

Call discarded after confirming #3981

huco95 opened this issue Nov 23, 2022 · 9 comments

Comments

@huco95
Copy link

huco95 commented Nov 23, 2022

Checklist

  • The error is in the library's code, and not in my own.
  • I have searched for this issue before posting it and there isn't a duplicate.
  • I ran pip install -U https://github.com/LonamiWebs/Telethon/archive/v1.zip and triggered the bug in the latest version.

I have updated the code from Telethon-calls to the lastest version, I managed to request and confirm the call, but after confirming the call it is disconnected. Any idea why this is happening? Thanks!

Code that causes the issue

import logging
logging.basicConfig(format='[%(levelname) 5s/%(asctime)s] %(name)s: %(message)s',
                    level=logging.WARNING)

import asyncio
import hashlib
import os
import random

from telethon import TelegramClient, events
from telethon.tl.functions.messages import GetDhConfigRequest
from telethon.tl.functions.phone import  RequestCallRequest, ConfirmCallRequest
from telethon.tl.types import PhoneCallEmpty, PhoneCallWaiting, \
    PhoneCallRequested, PhoneCallDiscarded, PhoneCall, PhoneCallAccepted, \
    UpdatePhoneCall, PhoneCallProtocol, InputPhoneCall, Updates, UpdateShort
from pyrogram.raw import types

api_id = 0
api_hash = ''
phone_number = '' 

call_state = None
client = TelegramClient('arm_call_test2', api_id, api_hash)

@client.on(events.Raw)
async def handler(update):
    await process_update(update)

class DH:
    def __init__(self, dhc: types.messages.DhConfig):
        self.p = bytes_to_integer(dhc.p)
        self.g = dhc.g
        self.resp = dhc

class DynamicDict:
    def __setattr__(self, key, value):
        self.__dict__[key] = value

def get_rand_bytes(dh_config, length=256):
    return bytes(x ^ y for x, y in zip(
        os.urandom(length), dh_config.resp.random
    ))

def bytes_to_integer(bytes):
    return int.from_bytes(bytes, 'big')

def integer_to_bytes(integer):
    return int.to_bytes(
        integer,
        length=(integer.bit_length() + 8 - 1) // 8,  # 8 bits per byte,
        byteorder='big',
        signed=False
    )

def calc_fingerprint(key):
    return int.from_bytes(
        bytes(hashlib.sha1(key).digest()[-8:]), 'little', signed=True
    )

async def process_update(update):
    if isinstance(update, UpdatePhoneCall):
        print('[CALL] Phone call update:', update.phone_call)
        await process_phone_call(update.phone_call)
    elif isinstance(update, Updates):
        for u in update.updates:
            await process_update(u)
    elif isinstance(update, UpdateShort):
        await process_update(update.update)
    else:
        pass
        #print('Ignoring', type(update).__name__)

async def process_phone_call(call):
    global call_state
    if isinstance(call, PhoneCallEmpty):
        pass
    elif isinstance(call, PhoneCallWaiting):
        print('[CALL] Waiting for', call.participant_id, 'to answer...')
    elif isinstance(call, PhoneCallRequested):
        print('[CALL] Incoming call from', call.participant_id)
        # accept_call(call)
    elif isinstance(call, PhoneCallDiscarded):
            state = call_state
            print('[CALL]', state.user_id, 'discarded your call because of', type(call.reason).__name__)        # del calls[call.id]
            client.disconnect()
    elif isinstance(call, PhoneCall):
        print('[CALL] Call', call.id, 'is now', type(call).__name__)
        process_full_call(call)
    elif isinstance(call, PhoneCallAccepted):
        print('[CALL] Call accepted by', call.participant_id, '! Doing stage 2')
        await call_stage_two(call)
    else:
        print('[call] ignoring', type(call), call)

async def call_stage_two(call):
    global call_state
    state = call_state
    state.prt_proto = call.protocol  # TODO Hm, why not my_proto?
    print('prt', state.prt_proto)
    print('myp', state.my_proto)

    state.g_b = int.from_bytes(call.g_b, 'little')
    state.key = pow(state.g_b, state.a, state.p)
    state.key_fingerprint = calc_fingerprint(integer_to_bytes(state.key))

    call_state = state

    phone_call = await client(ConfirmCallRequest(
        peer=InputPhoneCall(call.id, call.access_hash),
        g_a=integer_to_bytes(state.g_a),
        key_fingerprint=state.key_fingerprint,
        protocol=state.prt_proto
    ))

    print('[CALL] Call confirmed:', phone_call)
    return await process_phone_call(phone_call.phone_call)

def process_full_call(call):
    global call_state
    state = call_state
    print('[CALL] Processing full call', call)
    print('[CALL] Full state currently', state.__dict__)
    if state.incoming:
        print('[CALL] Got more info about call from', call.admin_id, 'we have accepted.')
        state.g_a = int.from_bytes(call.g_a_or_b, 'big')

        state.pt_g_a_hash = hashlib.sha256(call.g_a_or_b).digest()
        if state.pt_g_a_hash != state.g_a_hash:
            print('[CALL] HASH(G_A) != G_A_HASH!', state.pt_g_a_hash, state.g_a_hash)
        else:
            print('[CALL] g_a hash is correct!')

        state.key = pow(state.g_a, state.b, state.p)
        state.key_fingerprint = calc_fingerprint(integer_to_bytes(state.key))
        print('[CALL] Calculated fingerprint', repr(state.key_fingerprint), 'with key', repr(state.key))

        state.pt_key_fingerprint = call.key_fingerprint
        if state.pt_key_fingerprint != state.key_fingerprint:
            print('[CALL] Fingerprint mismatch! Got', state.pt_key_fingerprint, 'expected', state.key_fingerprint)

    state.connections = call.connections
    # state.alternative_connections = call.alternative_connections

    call_state = state

    print('[CALL] Call #', call.id, 'ready!')
    
async def main():    
    await client.start()

    dhc = DH(await client(GetDhConfigRequest(0, 256)))
    state = DynamicDict()
    state.incoming = False

    state.user_id = "@user"
    state.random_id = random.randint(0, 0x7fffffff - 1)

    state.g = dhc.g
    state.p = dhc.p

    state.a = 0
    while not (1 < state.a < state.p - 1):
        # "A chooses a random value of a, 1 < a < p-1"
        state.a = int.from_bytes(get_rand_bytes(dh_config=dhc), 'little')

    state.a = random.randint(2, state.p - 1)
    state.g_a = pow(state.g, state.a, state.p)
    state.g_a_hash = hashlib.sha256(integer_to_bytes(state.g_a)).digest()
    state.my_proto = PhoneCallProtocol(
            min_layer=92,
            max_layer=92,
            udp_p2p=True,
            udp_reflector=True,
            library_versions=['3.0.0'],
        )

    phone_call = await client(RequestCallRequest(
        user_id=state.user_id,
        random_id=state.random_id,
        g_a_hash=state.g_a_hash,
        protocol=state.my_proto
    ))

    print(phone_call)
    print(phone_call.phone_call.id)
    state.peer = InputPhoneCall(phone_call.phone_call.id, phone_call.phone_call.access_hash)
    global call_state
    call_state = state
    await process_phone_call(phone_call.phone_call)

    await client.run_until_disconnected()

asyncio.run(main())

Logs

[CALL] Waiting for xxx to answer...
[CALL] Phone call update:
[CALL] Waiting for xxx to answer...
[CALL] Phone call update
[CALL] Call accepted by xxx ! Doing stage 2
[CALL] Call confirmed
[CALL] Call xxx is now PhoneCall
[CALL] Processing full call 
[CALL] @user discarded your call because of PhoneCallDiscardReasonDisconnect

@Lonami
Copy link
Member

Lonami commented Nov 23, 2022

I do not have experience with Telegram calls so unfortunately I can't really help. There's nothing in Telethon in particular that would be declining the calls automatically, so I'm not sure what could be wrong. Maybe it's auto-declined if some parameter is wrong or something, no idea.

@Lonami Lonami closed this as completed Nov 23, 2022
@Lonami
Copy link
Member

Lonami commented Nov 23, 2022

Well I guess I can keep this open and try to take a closer look eventually.

@Lonami Lonami reopened this Nov 23, 2022
@Lonami
Copy link
Member

Lonami commented Nov 23, 2022

I took a look and noticed the following:

  • process_update tries to handle both Updates and UpdatesShort but the library shouldn't be dispatching these via events.Raw (not that this should affect the program's outcome in this case).
  • Phone call update: seems cut, not sure if it's intentional. I guess it doesn't matter since process_phone_call has more prints which help tell which update it was.
  • I'm not sure if this is the way calls work but seeing two PhoneCallWaiting seems strange.

Other than that, yeah, I have no idea. Does this behavior occur if Telethon calls Telegram Desktop? Telegram Android? Telegram X? Pyrogram? Does this happen if those call Telethon instead?

@Lonami
Copy link
Member

Lonami commented Nov 23, 2022

I tried the above code to call another account of mine on Telegram X and this is the output:

PhoneCall(phone_call=PhoneCallWaiting(id=100001, access_hash=100002, date=datetime.datetime(2022, 11, 23, 19, 24, 13, tzinfo=datetime.timezone.utc), admin_id=1001, participant_id=1002, protocol=PhoneCallProtocol(min_layer=92, max_layer=92, library_versions=['2.4.4'], udp_p2p=True, udp_reflector=True), video=False, receive_date=None), users=[User(id=1001, is_self=True, contact=True, mutual_contact=False, deleted=False, bot=False, bot_chat_history=False, bot_nochats=False, verified=False, restricted=False, min=False, bot_inline_geo=False, support=False, scam=False, apply_min_photo=True, fake=False, bot_attach_menu=False, premium=True, attach_menu_enabled=False, access_hash=100000003, first_name='lonami', last_name=None, username='lonami', phone='34600000001', photo=UserProfilePhoto(photo_id=10000004, dc_id=4, has_video=True, stripped_thumb=b'\x01\x08\x08\xd1\xcb\x8b\x8c\x10v\xfa\xd1E\x14\x86\x7f'), status=UserStatusOnline(expires=datetime.datetime(2022, 11, 23, 19, 28, 48, tzinfo=datetime.timezone.utc)), bot_info_version=None, restriction_reason=[], bot_inline_placeholder=None, lang_code=None, emoji_status=EmojiStatus(document_id=1000005)), User(id=1002, is_self=False, contact=True, mutual_contact=True, deleted=False, bot=False, bot_chat_history=False, bot_nochats=False, verified=False, restricted=False, min=False, bot_inline_geo=False, support=False, scam=False, apply_min_photo=False, fake=False, bot_attach_menu=False, premium=False, attach_menu_enabled=False, access_hash=1000006, first_name='alt', last_name=None, username='-', phone='34600000002', photo=None, status=UserStatusRecently(), bot_info_version=None, restriction_reason=[], bot_inline_placeholder=None, lang_code=None, emoji_status=None)])
100001
[CALL] Waiting for 1002 to answer...
[CALL] Phone call update: PhoneCallWaiting(id=100001, access_hash=100002, date=datetime.datetime(2022, 11, 23, 19, 24, 13, tzinfo=datetime.timezone.utc), admin_id=1001, participant_id=1002, protocol=PhoneCallProtocol(min_layer=92, max_layer=92, library_versions=['2.4.4'], udp_p2p=True, udp_reflector=True), video=False, receive_date=datetime.datetime(2022, 11, 23, 19, 24, 13, tzinfo=datetime.timezone.utc))
[CALL] Waiting for 1002 to answer...
[CALL] Phone call update: PhoneCallDiscarded(id=100001, need_rating=False, need_debug=False, video=False, reason=PhoneCallDiscardReasonMissed(), duration=None)
[CALL] @- discarded your call because of PhoneCallDiscardReasonMissed

If I do it but discard:

...
[CALL] Phone call update: PhoneCallDiscarded(id=100009, need_rating=False, need_debug=False, video=False, reason=PhoneCallDiscardReasonBusy(), duration=None)
[CALL] @- discarded your call because of PhoneCallDiscardReasonBusy

If I do it accepting with Telegram I do reach PhoneCallDiscardReasonDisconnect.

If I do it discarding with Telegram I get PhoneCallDiscardReasonHangup.

So yeah it's really strange.

@Lonami
Copy link
Member

Lonami commented Nov 23, 2022

Actually PhoneCallProtocol is setting udp_p2p. Shouldn't this mean there should be a peer-to-peer connection to the other client? I can't see any. On the other hand setting that to False doesn't seem to change much....

@huco95
Copy link
Author

huco95 commented Nov 24, 2022

Thanks a lot for your time @Lonami!!
I made some little chanes based on your messages.
I have also checked the telegram documentation about calls, https://core.telegram.org/api/end-to-end/voice-calls, becasuse I think the problem can be related with the g_a, g_a_hash and key_fingerprint generation but I can't figure it out.

import logging
logging.basicConfig(format='[%(levelname) 5s/%(asctime)s] %(name)s: %(message)s', level=logging.WARNING)

import asyncio
import hashlib
import os
import random

from telethon import TelegramClient, events
from telethon.tl.functions.messages import GetDhConfigRequest
from telethon.tl.functions.phone import  RequestCallRequest, ConfirmCallRequest
from telethon.tl.types import PhoneCallEmpty, PhoneCallWaiting, \
    PhoneCallRequested, PhoneCallDiscarded, PhoneCall, PhoneCallAccepted, \
    UpdatePhoneCall, PhoneCallProtocol, InputPhoneCall, Updates, UpdateShort
from pyrogram.raw import types

api_id = 0
api_hash = ''
phone_number = '' 

call_state = None
client = TelegramClient('call_test', api_id, api_hash)

@client.on(events.Raw)
async def handler(update):
    await process_update(update)

class DH:
    def __init__(self, dhc: types.messages.DhConfig):
        self.p = bytes_to_integer(dhc.p)
        self.g = dhc.g
        self.resp = dhc

class DynamicDict:
    def __setattr__(self, key, value):
        self.__dict__[key] = value

def get_rand_bytes(dh_config, length=256):
    return bytes(x ^ y for x, y in zip(
        os.urandom(length), dh_config.resp.random
    ))

def bytes_to_integer(bytes):
    return int.from_bytes(bytes, 'big')

def integer_to_bytes(integer):
    return int.to_bytes(
        integer,
        length=(integer.bit_length() + 8 - 1) // 8,  # 8 bits per byte,
        byteorder='big',
        signed=False
    )

def calc_fingerprint(key):
    return int.from_bytes(
        bytes(hashlib.sha1(key).digest()[-8:]), 'little', signed=True
    )

async def process_update(update):
    if isinstance(update, UpdatePhoneCall):
        print('[CALL] Phone call update:', update.phone_call)
        await process_phone_call(update.phone_call)
    else:
        print('Ignoring', type(update).__name__)

async def process_phone_call(call):
    global call_state
    if isinstance(call, PhoneCallEmpty):
        print('[CALL] Phone call empty')
    elif isinstance(call, PhoneCallWaiting):
        print('[CALL] Waiting for', call.participant_id, 'to answer...')
    elif isinstance(call, PhoneCallRequested):
        print('[CALL] Incoming call from', call.participant_id)
        # accept_call(call)
    elif isinstance(call, PhoneCallDiscarded):
            state = call_state
            print('[CALL]', state.user_id, 'discarded your call because of', type(call.reason).__name__)        # del calls[call.id]
            client.disconnect()
    elif isinstance(call, PhoneCall):
        print('[CALL] Call', call.id, 'is now', type(call).__name__)
        process_full_call(call)
    elif isinstance(call, PhoneCallAccepted):
        print('[CALL] Call accepted by', call.participant_id, '! Doing stage 2')
        await call_stage_two(call)
    else:
        print('[call] ignoring', type(call), call)

async def call_stage_two(call):
    global call_state
    state = call_state
    state.prt_proto = call.protocol  # TODO Hm, why not my_proto?
    print('prt', state.prt_proto)
    print('myp', state.my_proto)

    state.g_b = int.from_bytes(call.g_b, 'big')
    state.key = pow(base=state.g_b, exp=state.a, mod=state.p)
    state.key_fingerprint = calc_fingerprint(integer_to_bytes(state.key))

    call_state = state

    phone_call = await client(ConfirmCallRequest(
        peer=InputPhoneCall(call.id, call.access_hash),
        g_a=integer_to_bytes(state.g_a),
        key_fingerprint=state.key_fingerprint,
        protocol=state.prt_proto
    ))

    print('[CALL] Call confirmed:', phone_call)

def process_full_call(call):
    global call_state
    state = call_state
    print('[CALL] Processing full call', call)
    print('[CALL] Full state currently', state.__dict__)

    state.connections = call.connections

    # Validate fingerprint
    state.key = pow(state.g_b, state.a, state.p)
    state.key_fingerprint = calc_fingerprint(integer_to_bytes(state.key))
    state.pt_key_fingerprint = call.key_fingerprint
    if state.pt_key_fingerprint != state.key_fingerprint:
        print('[CALL] Fingerprint mismatch! Got', state.pt_key_fingerprint, 'expected', state.key_fingerprint)
    else:
        print('[CALL] Fingerprint is correct!')

    call_state = state

    print('[CALL] Call #', call.id, 'ready!')
    
async def main():    
    await client.start()

    dhc = DH(await client(GetDhConfigRequest(0, 256)))
    state = DynamicDict()
    state.incoming = False

    state.user_id = "@user"
    state.random_id = random.randint(0, 0x7fffffff - 1)

    state.g = dhc.g
    state.p = dhc.p

    state.a = 0
    while not (1 < state.a < state.p - 1):
        # "A chooses a random value of a, 1 < a < p-1"
        state.a = int.from_bytes(get_rand_bytes(dh_config=dhc), 'big')

    state.a = random.randint(2, state.p - 1)
    state.g_a = pow(base=state.g, exp=state.a, mod=state.p)
    state.g_a_hash = hashlib.sha256(integer_to_bytes(state.g_a)).digest()
    state.my_proto = PhoneCallProtocol(
            min_layer=92,
            max_layer=92,
            udp_p2p=True,
            udp_reflector=True,
            library_versions=['3.0.0'],
        )

    phone_call = await client(RequestCallRequest(
        user_id=state.user_id,
        random_id=state.random_id,
        g_a_hash=state.g_a_hash,
        protocol=state.my_proto
    ))

    print("Phone call (", phone_call.phone_call.id, ") request sent, waiting for updates...")
    state.peer = InputPhoneCall(phone_call.phone_call.id, phone_call.phone_call.access_hash)
    global call_state
    call_state = state

    await client.run_until_disconnected()

asyncio.run(main())

@Lonami
Copy link
Member

Lonami commented Jan 14, 2023

I'm going through the list of issues to fix some of them and found this again. I don't think this issue is actionable from my side. Were you able to make any progress? Would you mind if I closed this issue here (if it's not something I can "fix" in Telethon, it doesn't make much sense to keep an issue open for it)?

If you have an issue for this elsewhere, I can watch that issue, so I can participate there instead.

@Lonami
Copy link
Member

Lonami commented Apr 6, 2023

Closing due to the lack of response, but we can reopen if you think there's a need.

@Lonami Lonami closed this as completed Apr 6, 2023
@nardonycc
Copy link

to switch off

How would I disconnect the call?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants