📜 ⬆️ ⬇️

Transport-bot Jabber conferences for Telegram



Good day.

One fine day, after a significant break, fate again pushed me into jabber conferences. True, no one uses jabber among friends, 2007 has sunk into oblivion, Telegram has become the main means of communication. XMPP support on mobile devices left a lot to be desired - clients on Android are good every one of them, with iOS and WP, ​​to put it mildly, not very much. And features of the protocol also affect autonomy. Therefore, the thought arose: wouldn’t the bot be able to translate messages from conferences into a Telegram chat room?
')
As tools used:


Key features and dependencies


From the ready implementations, only a jabbergram was found, but it allows you to work with only one user. There is also an implementation on Go, with which there was no experience, so this option was not considered and I can’t say anything about the functionality.

The choice of libraries is mainly due to the desire to work with asyncio.

Initially, a single-user tet-a-tet dialogue was developed, which was later expanded using XMPP Components for group chats, with a separate xmpp-user for each participant.

The bot is configured so that it cannot be added to the chat with another user, therefore it is impossible to consider it as a universal implementation.

Why is this done? The bot API greatly limits the number of incoming / outgoing requests in a short time, and with a fairly intensive exchange of messages, errors will occur.

What is in general:


However, there are differences between the two versions:


When developing it is convenient to use virtual environments, so you can create one:

$ python3.5 -m venv venv $ . venv/bin/activate 

To use, you need to install from pip aiohttp, slixmpp and ujson. If desired, you can add gunicorn. With or without environment, all packages are in PyPI:

 $ pip3 install aiohttp slixmpp ujson 

At the end of the post there are links to bitbucket repositories with source code.

Telegram history


First it should be noted that the ready-made frameworks for the Telegram API were not used for a number of reasons:


So a simple wrapper was made over the main objects and methods of bots api , requests are sent using requests , json is parsed by ujson , because it is faster.

Setting up the bot is done via the config-script:

config.py
 VERSION = "0.1" TG_WH_URL = "https://yourdomain.tld/path/123456" TG_TOKEN = "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" TG_CHAT_ID = 12345678 XMPP_JID = "jid@domain.tld" XMPP_PASS = "yourpassword" XMPP_MUC = "muc@conference.domain.tld" XMPP_NICK = "nickname" DB_FILENAME = "bot.db" LOG_FILENAME = "bot.log" ISIDA_NICK = "IsidaBot" #        xmpp  UPLOADER_URL = "example.com/upload" #   #     XMPP_JID/XMPP_PASS/XMPP_NICK     : # TG_INVITE_URL = "https://telegram.me/joinchat/ABCDefGHblahblah" #     # COMPONENT_JID = "tg.xmpp.domain.tld" # COMPONENT_PASS = "password" # XMPP_HOST = "xmpp.domain.tld" # XMPP_PORT = 5347 



The representation of objects looks like this:

mapping.py
 class User(object): def __str__(self): return '<User id={} first_name="{}" last_name="{}" username={}>'.format(self.id, self.first_name, self.last_name, self.username) def __init__(self, obj): self.id = obj.get('id') self.first_name = obj.get('first_name') self.last_name = obj.get('last_name') self.username = obj.get('username') 



Bot class for querying:
bind.py
 class Bot(object): def _post(self, method, payload=None): r = requests.post(self.__apiUrl + method, payload).text return ujson.loads(r) ... def getMe(self): r = self._post('getMe') return User(r.get('result')) if r.get('ok') else None ... @property def token(self): return self.__token ... def __init__(self, token): self.__token = token ... 



All requests are processed using webhukov, which come to the address TG_WH_URL.
RequestHandler.handle () - coroutine to handle aiohttp requests.

handler.py
 from aiohttp import web import asyncio import tgworker as tg #     bots api import mucbot as mb #    xmpp import tinyorm as orm #    sqlite3 class RequestHandler(object): ... async def handle(self, request): r = await request.text() try: ... update = tg.Update(ujson.loads(r)) log.debug("TG Update object: {}".format(ujson.loads(r))) ... except: log.error("Unexpected error: {}".format(sys.exc_info())) ... raise finally: return web.Response(status=200) def __init__(self, db: orm.TableMapper, mucBot: mb.MUCBot, tgBot: tg.Bot, tgChatId, loop): self.__db = db self.__tg = tgBot self.__mb = mucBot self.__chat_id = tgChatId self.__loop = loop ... ... loop = asyncio.get_event_loop() whHandler = RequestHandler(db, mucBot, tgBot, TG_CHAT_ID, loop) app = web.Application(loop=loop) app.router.add_route('POST', '/', whHandler.handle) ... 



During processing, text messages are sent to the conference. Either as a private message, if it is an answer to a private message or when the response is added, the / pm command is added.

Files are uploaded to a third-party server before being sent, and a link to the file is sent to the conference. Most likely, for general use, such an approach will not work and you will have to do a download on Imgur or another service provided by the API. Now the files are simply sent to the jTalk server. With the permission of the developer, of course. But, since it is still for personal use, the address is in the config.

Stickers are simply replaced by their emoji presentation.

Opus on xmpp


At one time, python had two very popular libraries - SleekXMPP and xmpppy. The second is already outdated and not supported, and SleekXMPP asynchrony is implemented by threads. Of the libraries that support asyncio, there is aioxmpp and slixmpp .

Aioxmpp is still very raw and does not have comprehensive documentation. However, the first version of the bot used aioxmpp, but then rewritten for slixmpp.

Slixmpp is SleekXMPP on asyncio, the interface is the same there, respectively, most plugins will work. It is used in the console jabber client Poezio .
In addition, slixmpp has great support, which helped solve some problems with the library.

The single-user version uses slixmpp.ClientXMPP as the base class, while the multi-user version uses slixmpp.ComponentXMPP

The XMPP event handler looks like this:
mucbot.py
 import slixmpp as sx class MUCBot(sx.ClientXMPP): # class MUCBot(sx.ComponentXMPP): #     ... # # Event handlers # def _sessionStart(self, event): self.get_roster() self.send_presence(ptype='available') self.plugin['xep_0045'].joinMUC(self.__mucjid, self.__nick, wait=True) #        ... # # Message handler # def _message(self, msg: sx.Message): log.debug("Got message: {}".format(str(msg).replace('\n', ' '))) ... # # Presence handler # def _presence(self, presence: sx.Presence): log.debug("Got Presence {}".format(str(presence).replace('\n', ' '))) ... # # Initialization # def __init__(self, db, tgBot, tgChatId, jid, password, mucjid, nick): super().__init__(jid, password) self.__jid = sx.JID(jid) self.__mucjid = sx.JID(mucjid) self.__nick = nick self.__tg = tgBot self.__db = db self.__chat_id = tgChatId ... #     XEP self.register_plugin('xep_XXXX') # Service Discovery ... #    xmlstream self.add_event_handler("session_start", self._sessionStart) self.add_event_handler("message", self._message) self.add_event_handler("muc::{}::presence".format(mucjid), self._presence) ... 



Obviously, it will be mandatory to connect XEP-0045 for MUC, XEP-0199 for pings and XEP-0092 will also be useful to show everyone what we are cool about .

Messages from xmpp are simply sent to the chat from the user (or group chat) with the TG_CHAT_ID from the config.

Setting up an XMPP server to work with components


An interesting feature is the use of xmpp components to dynamically create users. You do not need to create a separate object for each user and store data for authorization. The downside is that you will not be able to use your primary account.

For reasons of ease and simplicity, Prosody was chosen as the xmpp server.

I will not describe the configuration, the only difference from the template is the inclusion of the component (COMPONENT_JID from the bot config):

 Component "tg.xmpp.domain.tld" component_secret = "password" 


Prosody configuration

In general, this is the whole xmpp setup. It remains only to restart prosody.

The Tale of gunicorn and nginx


If it is so coincidental that nginx is looking out by chance, you should add a directive to the server section.

nginx.cfg
 location /path/to/123456 { error_log /path/to/www/logs/bot_error.log; access_log /path/to/www/logs/bot_access.log; alias /path/to/www/bot/public; proxy_pass http://unix:/path/to/www/bot/bot.sock:/; } 



I think it’s not worth setting the HTTPS setting, but the certificates were obtained via letsencrypt .

The configuration for the example took from this comment . The full config can be viewed here , the parameters for encryption were selected in the Mozilla SSL Generator

This whole construct of ... sticks works on VPS with Debian 8.5, so for systemd, a service is written that launches gunicorn:

bot.service
[Unit]
After=network.target

[Service]
PIDFile=/path/to/www/bot/bot.pid
User=service
Group=www-data
WorkingDirectory=/path/to/www/bot
ExecStart=/path/to/venv/bin/gunicorn --pid bot.pid --workers 1 --bind unix:bot.sock -m 007 bot:app --worker-class aiohttp.worker.GunicornWebWorker
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID
PrivateTmp=true

[Install]
WantedBy=multi-user.target



Of course, it doesn’t hurt to perform systemctl daemon-reload and systemctl enable bot.

Source links




PS For the award the most beautiful code of the year I do not pretend. Of course, I wanted to do well, but it turned out as always.
PPS Development of a version for group chats is abandoned due to lack of desire, time and a number of problems with the Telegram API.

Source: https://habr.com/ru/post/305858/


All Articles