2
votes

I want to use make a bot that communicates between discord and telegram by using the libraries python-telegram-bot and discord.py (version 1.0.0). However the problem is that discord.py uses async functions and python-telegram-bot threading. With the code below, everything works fine for messages being posted in discord (the bot sends them correctly to telegram), however the other way around does not work (bot gets messages from telegram and sends it to discord). I previously had issues with syntax/runtime errors as I tried to run the discords channel.send function in a sync function (thus either returning only a generator object or complaining that I cannot use await in a sync function). However, at the same time the python-telegram-bot's MessageHandler needs a sync function so when giving him a async function Python complains that "await" was never called for the async function. I now tried to use the async_to_sync method from asgiref library to run my async broadcastMsg from the MessageHandler, however the code still does not send the message to discord! It seems to call the function correctly but only until line print('I get to here'). No error is displayed and no message is poping up in discord. I guess it has something to do with the fact that I have to register the function as a task in the discord.py event loop, however registering is only working when it happens before botDiscord.run(TOKENDISCORD) has been executed which of course has to happen before. So to boil my problem down to one question: How am I able to interact with the discord.py event loop from another thread (which is from the telegram MessageHandler). Or if this is not possible: How can I send a message with discord.py without being within the discord.py event loop?

Thank you for your help

import asyncio
from asgiref.sync import async_to_sync
from telegram import Message as TMessage
from telegram.ext import (Updater,Filters,MessageHandler)
from discord.ext import commands
import discord

TChannelID = 'someTelegramChannelID'
DChannel = 'someDiscordChannelObject'

#%% define functions / commands
prefix = "?"
botDiscord = commands.Bot(command_prefix=prefix)
discordChannels = {}


async def broadcastMsg(medium,channel,message):
    ''' 
    Function to broadcast a message to all linked channels.
    '''
    if isinstance(message,TMessage):
        fromMedium = 'Telegram'
        author = message.from_user.username
        channel = message.chat.title
        content = message.text
    elif isinstance(message,discord.Message):
        fromMedium = 'Discord'
        author = message.author
        channel = message.channel.name
        content = message.content
    # check where message comes from        
    textToSend = '%s wrote on %s %s:\n%s'%(author,fromMedium,channel,content)
    # go through channels and send the message
    if 'telegram' in medium:
        # transform channel to telegram chatID and send
        updaterTelegram.bot.send_message(channel,textToSend)

    elif 'discord' in medium:
        print('I get to here')
        await channel.send(textToSend)

    print("I do not get there")


@botDiscord.event
async def on_message(message):
    await broadcastMsg('telegram',TChannelID,message)

def on_TMessage(bot,update):
    # check if this chat is already known, else save it
    # get channels to send to and send message
    async_to_sync(broadcastMsg)('discord',DChannel,update.message)


#%% initialize telegram and discord bot and run them
messageHandler = MessageHandler(Filters.text, on_TMessage)
updaterTelegram = Updater(token = TOKENTELEGRAM, request_kwargs={'read_timeout': 10, 'connect_timeout': 10})
updaterTelegram.dispatcher.add_handler(messageHandler)
updaterTelegram.start_polling()
botDiscord.run(TOKENDISCORD)  
2

2 Answers

5
votes

How can I send a message with discord.py without being within the discord.py event loop?

To safely schedule a coroutine from outside the event loop thread, use asyncio.run_coroutine_threadsafe:

_loop = asyncio.get_event_loop()

def on_TMessage(bot, update):
    asyncio.run_coroutine_threadsafe(
        broadcastMsg('discord', DChannel, update.message), _loop)
0
votes

you can try split them to 2 separates *.py files.

t2d.py #telegram to discor

import subprocess
from telethon import TelegramClient, events, sync

api_id = '...'
api_hash = '...'

with TelegramClient('name', api_id, api_hash) as client:     
    @client.on(events.NewMessage()) #inside .NewMessage() you can put specific channel like: chats="test_channel"
    async def handler(event):        
        print('test_channel raw text: ', event.raw_text) #this row is not necessary

        msg = event.raw_text

        subprocess.call(["python", "s2d.py", msg])

    client.run_until_disconnected()

s2d.py #send to discord

import discord, sys

my_secret = '...'

clientdiscord = discord.Client()

@clientdiscord.event
async def on_ready():
    #print('We have logged in as {0.user}'.format(clientdiscord)) #this row is not necessary

    channel = clientdiscord.get_channel(123456789) # num of channel where you want to write message

    msg = sys.argv[1] #grab message
    
    msg = 's2d: ' + msg #only for test, you can delete this row 
    
    await channel.send(msg)

    quit() # very important quit this bot 

    
clientdiscord.run(my_secret)

It will be a little bit slower (subprocess will make delay), but very easy solution