Improve groupchat functions to handle with erroneous cases

This commit is contained in:
Schimon Jehudah 2024-02-14 17:09:54 +00:00
parent 5c2ee8d51c
commit c8cd5e1b09
8 changed files with 149 additions and 76 deletions

View file

@ -113,6 +113,7 @@ class JabberComponent:
xmpp.register_plugin('xep_0084') # User Avatar xmpp.register_plugin('xep_0084') # User Avatar
xmpp.register_plugin('xep_0085') # Chat State Notifications xmpp.register_plugin('xep_0085') # Chat State Notifications
xmpp.register_plugin('xep_0115') # Entity Capabilities xmpp.register_plugin('xep_0115') # Entity Capabilities
xmpp.register_plugin('xep_0122') # Data Forms Validation
xmpp.register_plugin('xep_0153') # vCard-Based Avatars xmpp.register_plugin('xep_0153') # vCard-Based Avatars
xmpp.register_plugin('xep_0199', {'keepalive': True}) # XMPP Ping xmpp.register_plugin('xep_0199', {'keepalive': True}) # XMPP Ping
xmpp.register_plugin('xep_0249') # Direct MUC Invitations xmpp.register_plugin('xep_0249') # Direct MUC Invitations
@ -127,7 +128,6 @@ class JabberClient:
def __init__(self, jid, password, hostname=None, port=None, alias=None): def __init__(self, jid, password, hostname=None, port=None, alias=None):
xmpp = Slixfeed(jid, password, hostname, port, alias) xmpp = Slixfeed(jid, password, hostname, port, alias)
xmpp.register_plugin('xep_0004') # Data Forms xmpp.register_plugin('xep_0004') # Data Forms
xmpp.register_plugin('xep_0122') # Data Forms Validation
xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0045') # Multi-User Chat xmpp.register_plugin('xep_0045') # Multi-User Chat
xmpp.register_plugin('xep_0048') # Bookmarks xmpp.register_plugin('xep_0048') # Bookmarks
@ -140,6 +140,7 @@ class JabberClient:
xmpp.register_plugin('xep_0084') # User Avatar xmpp.register_plugin('xep_0084') # User Avatar
xmpp.register_plugin('xep_0085') # Chat State Notifications xmpp.register_plugin('xep_0085') # Chat State Notifications
xmpp.register_plugin('xep_0115') # Entity Capabilities xmpp.register_plugin('xep_0115') # Entity Capabilities
xmpp.register_plugin('xep_0122') # Data Forms Validation
xmpp.register_plugin('xep_0153') # vCard-Based Avatars xmpp.register_plugin('xep_0153') # vCard-Based Avatars
xmpp.register_plugin('xep_0199', {'keepalive': True}) # XMPP Ping xmpp.register_plugin('xep_0199', {'keepalive': True}) # XMPP Ping
xmpp.register_plugin('xep_0249') # Direct MUC Invitations xmpp.register_plugin('xep_0249') # Direct MUC Invitations

View file

@ -1,13 +1,15 @@
about = """ about = """
Slixfeed Slixfeed is a news broker bot for syndicated news which aims to be \
A Syndication bot for the XMPP communication network. an easy to use and fully-featured news aggregator bot.
Slixfeed is a news broker which aims to be an easy to use and fully-\ Slixfeed provides a convenient access to Blogs, News websites and \
featured news aggregator bot. It provides a convenient access to \ even Fediverse instances, along with filtering and other privacy \
Blogs, News websites and even Fediverse instances, along with \ driven functionalities.
filtering functionality.
Slixfeed is primarily designed for XMPP (aka Jabber). \ Slixfeed is designed primarily for the XMPP communication network \
Visit https://xmpp.org/software/ for more information. (aka Jabber). Visit https://xmpp.org/software/ for more information.
https://gitgud.io/sjehuda/slixfeed
""" """
authors = """ authors = """
@ -58,7 +60,8 @@ No operator was specified for this instance.
platforms = """ platforms = """
Supported platforms: XMPP Supported platforms: XMPP
Platforms to be added in future: ActivityPub, Briar, Email, IRC, LXMF, Matrix, MQTT, Nostr, Session, Tox. Platforms to be added in future: ActivityPub, Briar, Email, IRC, LXMF, \
Matrix, MQTT, Nostr, Session, Tox.
For ideal experience, we recommend using XMPP. For ideal experience, we recommend using XMPP.
""" """
@ -86,6 +89,24 @@ XMPP
https://xmpp.org/about/ https://xmpp.org/about/
""" """
sleekxmpp = """
SleekXMPP is an MIT licensed XMPP library for Python 2.6/3.1+, and is featured \
in examples in the book XMPP: The Definitive Guide by Kevin Smith, Remko Tronçon, \
and Peter Saint-Andre.
https://codeberg.org/fritzy/SleekXMPP
"""
slixmpp = """
Slixmpp is an MIT licensed XMPP library for Python 3.7+. It is a fork of SleekXMPP.
Slixmpp's goals is to only rewrite the core of the SleekXMPP library \
(the low level socket handling, the timers, the events dispatching) \
in order to remove all threads.
https://codeberg.org/poezio/slixmpp
"""
terms = """ terms = """
Slixfeed is free software; you can redistribute it and/or \ Slixfeed is free software; you can redistribute it and/or \
modify it under the terms of the MIT License. modify it under the terms of the MIT License.
@ -129,6 +150,7 @@ Raphael Groner (Fedora, Germany); \
Remko Tronçon <mko.re> (Psi , Belgium); \ Remko Tronçon <mko.re> (Psi , Belgium); \
Simone "roughnecks" Canaletti <woodpeckersnest.space> (Italy); \ Simone "roughnecks" Canaletti <woodpeckersnest.space> (Italy); \
Richard Lapointe (SalixOS, Connecticut); \ Richard Lapointe (SalixOS, Connecticut); \
Stephen Paul Weber <singpolyma.net>; \
Strix from Loqi; \ Strix from Loqi; \
Thibaud Guerin (SalixOS); \ Thibaud Guerin (SalixOS); \
Thorsten Fröhlich (France); \ Thorsten Fröhlich (France); \
@ -144,6 +166,5 @@ chat, voice and video calls, collaboration, lightweight \
middleware, content syndication, and generalized routing of XML \ middleware, content syndication, and generalized routing of XML \
data. data.
Visit https://xmpp.org/about/ for more information on the XMPP \ https://xmpp.org/about/
protocol.
""" """

View file

@ -1,2 +1,2 @@
__version__ = '0.1.6' __version__ = '0.1.7'
__version_info__ = (0, 1, 6) __version_info__ = (0, 1, 7)

View file

@ -44,6 +44,8 @@ class XmppBookmark:
groupchats.extend([conference]) groupchats.extend([conference])
if properties: if properties:
properties['jid'] = properties['room'] + '@' + properties['host'] properties['jid'] = properties['room'] + '@' + properties['host']
if not properties['alias']: properties['alias'] = self.alias
else: else:
properties = { properties = {
'jid' : jid, 'jid' : jid,

View file

@ -167,7 +167,7 @@ class Slixfeed(slixmpp.ClientXMPP):
inviter = message['from'].bare inviter = message['from'].bare
muc_jid = message['groupchat_invite']['jid'] muc_jid = message['groupchat_invite']['jid']
await XmppBookmark.add(self, muc_jid) await XmppBookmark.add(self, muc_jid)
await XmppGroupchat.join(self, inviter, muc_jid) XmppGroupchat.join(self, inviter, muc_jid)
message_body = ('Greetings! I am {}, the news anchor.\n' message_body = ('Greetings! I am {}, the news anchor.\n'
'My job is to bring you the latest ' 'My job is to bring you the latest '
'news from sources you provide me with.\n' 'news from sources you provide me with.\n'
@ -181,7 +181,7 @@ class Slixfeed(slixmpp.ClientXMPP):
inviter = message['from'].bare inviter = message['from'].bare
muc_jid = message['groupchat_invite']['jid'] muc_jid = message['groupchat_invite']['jid']
await XmppBookmark.add(self, muc_jid) await XmppBookmark.add(self, muc_jid)
await XmppGroupchat.join(self, inviter, muc_jid) XmppGroupchat.join(self, inviter, muc_jid)
message_body = ('Greetings! I am {}, the news anchor.\n' message_body = ('Greetings! I am {}, the news anchor.\n'
'My job is to bring you the latest ' 'My job is to bring you the latest '
'news from sources you provide me with.\n' 'news from sources you provide me with.\n'
@ -207,22 +207,23 @@ class Slixfeed(slixmpp.ClientXMPP):
self.service_reactions() self.service_reactions()
await self['xep_0115'].update_caps() await self['xep_0115'].update_caps()
await self.get_roster() await self.get_roster()
await XmppGroupchat.autojoin(self)
await profile.update(self) await profile.update(self)
task.task_ping(self) task.task_ping(self)
bookmarks = await self.plugin['xep_0048'].get_bookmarks()
XmppGroupchat.autojoin(self, bookmarks)
# Service.commands(self) # Service.commands(self)
# Service.reactions(self) # Service.reactions(self)
async def on_session_resumed(self, event): def on_session_resumed(self, event):
# self.send_presence() # self.send_presence()
profile.set_identity(self, 'client') profile.set_identity(self, 'client')
# self.service_commands() # self.service_commands()
# self.service_reactions() # self.service_reactions()
self['xep_0115'].update_caps() self['xep_0115'].update_caps()
await XmppGroupchat.autojoin(self) XmppGroupchat.autojoin(self)
# Service.commands(self) # Service.commands(self)
# Service.reactions(self) # Service.reactions(self)
@ -358,7 +359,7 @@ class Slixfeed(slixmpp.ClientXMPP):
return return
if message['type'] in ('chat', 'normal'): if message['type'] in ('chat', 'normal'):
# NOTE: Required for Cheogram # NOTE: Required for Cheogram
await self['xep_0115'].update_caps(jid=jid) # await self['xep_0115'].update_caps(jid=jid)
# self.send_presence(pto=jid) # self.send_presence(pto=jid)
# task.clean_tasks_xmpp(self, jid, ['status']) # task.clean_tasks_xmpp(self, jid, ['status'])
await asyncio.sleep(5) await asyncio.sleep(5)
@ -369,7 +370,7 @@ class Slixfeed(slixmpp.ClientXMPP):
if message['type'] in ('chat', 'normal'): if message['type'] in ('chat', 'normal'):
jid = message['from'].bare jid = message['from'].bare
# NOTE: Required for Cheogram # NOTE: Required for Cheogram
await self['xep_0115'].update_caps(jid=jid) # await self['xep_0115'].update_caps(jid=jid)
# self.send_presence(pto=jid) # self.send_presence(pto=jid)
# task.clean_tasks_xmpp(self, jid, ['status']) # task.clean_tasks_xmpp(self, jid, ['status'])
await asyncio.sleep(5) await asyncio.sleep(5)
@ -466,36 +467,42 @@ class Slixfeed(slixmpp.ClientXMPP):
# ) # )
# if jid == config.get_value('accounts', 'XMPP', 'operator'): # if jid == config.get_value('accounts', 'XMPP', 'operator'):
self['xep_0050'].add_command(node='settings',
name='📮️ Edit settings',
handler=self._handle_settings)
self['xep_0050'].add_command(node='filters',
name='🕸️ Manage filters',
handler=self._handle_filters)
self['xep_0050'].add_command(node='bookmarks',
name='📔️ Organize bookmarks - Restricted',
handler=self._handle_bookmarks)
self['xep_0050'].add_command(node='roster',
name='🧾️ Organize roster - Restricted',
handler=self._handle_roster)
self['xep_0050'].add_command(node='subscriptions', self['xep_0050'].add_command(node='subscriptions',
name='📰️ Subscriptions - All', name='📰️ Subscriptions',
handler=self._handle_subscriptions) handler=self._handle_subscriptions)
self['xep_0050'].add_command(node='subscriptions_cat', self['xep_0050'].add_command(node='subscriptions_cat',
name='🔖️ Subscriptions - Categories', name='🔖️ Categories',
handler=self._handle_subscription) handler=self._handle_subscription)
self['xep_0050'].add_command(node='subscriptions_tag', self['xep_0050'].add_command(node='subscriptions_tag',
name='🏷️ Subscriptions - Tags', name='🏷️ Tags',
handler=self._handle_subscription) handler=self._handle_subscription)
self['xep_0050'].add_command(node='subscriptions_index', self['xep_0050'].add_command(node='subscriptions_index',
name='📑️ Subscriptions - Indexed', name='📑️ Index (A - Z)',
handler=self._handle_subscription) handler=self._handle_subscription)
self['xep_0050'].add_command(node='credit', self['xep_0050'].add_command(node='settings',
name='💡️ Credit', name='📮️ Settings',
handler=self._handle_credit) handler=self._handle_settings)
self['xep_0050'].add_command(node='filters',
name='🛡️ Filters',
handler=self._handle_filters)
self['xep_0050'].add_command(node='bookmarks',
name='📕 Bookmarks',
handler=self._handle_bookmarks)
self['xep_0050'].add_command(node='roster',
name='📓 Roster', # 📋
handler=self._handle_roster)
self['xep_0050'].add_command(node='help', self['xep_0050'].add_command(node='help',
name='🛟️ Help', name='📔️ Manual',
handler=self._handle_help) handler=self._handle_help)
self['xep_0050'].add_command(node='motd',
name='🗓️ MOTD',
handler=self._handle_motd)
self['xep_0050'].add_command(node='credit',
name='Credits', # 💡️
handler=self._handle_credit)
self['xep_0050'].add_command(node='about',
name='About', # 📜️
handler=self._handle_about)
# self['xep_0050'].add_command(node='search', # self['xep_0050'].add_command(node='search',
# name='Search', # name='Search',
# handler=self._handle_search) # handler=self._handle_search)
@ -547,7 +554,7 @@ class Slixfeed(slixmpp.ClientXMPP):
jid = session['from'].bare jid = session['from'].bare
form = self['xep_0004'].make_form('form', 'Filters for {}'.format(jid)) form = self['xep_0004'].make_form('form', 'Filters for {}'.format(jid))
form['instructions'] = ('🛡 Filters have been updated') form['instructions'] = (' Filters have been updated')
jid_file = jid jid_file = jid
db_file = config.get_pathname_to_database(jid_file) db_file = config.get_pathname_to_database(jid_file)
# In this case (as is typical), the payload is a form # In this case (as is typical), the payload is a form
@ -769,18 +776,55 @@ class Slixfeed(slixmpp.ClientXMPP):
pass pass
async def _handle_about(self, iq, session):
import slixfeed.action as action
# form = self['xep_0004'].make_form('result', 'Thanks')
# form['instructions'] = action.manual('information.toml', 'thanks')
# session['payload'] = form
# text = '💡️ About Slixfeed, slixmpp and XMPP\n\n'
# text += '\n\n'
# form = self['xep_0004'].make_form('result', 'About')
text = 'Slixfeed\n\n'
text += ''.join(action.manual('information.toml', 'about'))
text += '\n\n'
text += 'Slixmpp\n\n'
text += ''.join(action.manual('information.toml', 'slixmpp'))
text += '\n\n'
text += 'SleekXMPP\n\n'
text += ''.join(action.manual('information.toml', 'sleekxmpp'))
text += '\n\n'
text += 'XMPP\n\n'
text += ''.join(action.manual('information.toml', 'xmpp'))
session['notes'] = [['info', text]]
# form.add_field(var='about',
# ftype='text-multi',
# label='About',
# value=text)
# session['payload'] = form
return session
async def _handle_motd(self, iq, session):
# TODO add functionality to attach image.
text = ('Here you can add groupchat rules,post schedule, tasks or '
'anything elaborated you might deem fit. Good luck!')
session['notes'] = [['info', text]]
return session
async def _handle_credit(self, iq, session): async def _handle_credit(self, iq, session):
import slixfeed.action as action import slixfeed.action as action
# form = self['xep_0004'].make_form('result', 'Thanks') # form = self['xep_0004'].make_form('result', 'Thanks')
# form['instructions'] = action.manual('information.toml', 'thanks') # form['instructions'] = action.manual('information.toml', 'thanks')
# session['payload'] = form # session['payload'] = form
text = '💡️ We are Jabber\n\n' text = '💡️ We are XMPP\n\n'
fren = action.manual('information.toml', 'thanks') fren = action.manual('information.toml', 'thanks')
fren = "".join(fren) fren = "".join(fren)
fren = fren.split(';') fren = fren.split(';')
fren = "\n".join(fren) fren = "\n".join(fren)
text += fren text += fren
text += '\n\nYOU!\n\n🫵️\n\n- Join us -\n\n🤝️' # text += '\n\nYOU!\n\n🫵\n\n- Join us -\n\n🤝'
text += '\n\nYOU!\n\n🫵️\n\n- Join us -'
session['notes'] = [['info', text]] session['notes'] = [['info', text]]
return session return session
@ -819,8 +863,6 @@ class Slixfeed(slixmpp.ClientXMPP):
ftype='text-multi', ftype='text-multi',
label=key, label=key,
value=value) value=value)
session['payload'] = form session['payload'] = form
return session return session
@ -864,7 +906,8 @@ class Slixfeed(slixmpp.ClientXMPP):
form.addField(var='name', form.addField(var='name',
ftype='text-single', ftype='text-single',
label='Name', label='Name',
value=properties['name']) value=properties['name'],
required=True)
form.addField(var='room', form.addField(var='room',
ftype='text-single', ftype='text-single',
label='Room', label='Room',
@ -878,7 +921,8 @@ class Slixfeed(slixmpp.ClientXMPP):
form.addField(var='alias', form.addField(var='alias',
ftype='text-single', ftype='text-single',
label='Alias', label='Alias',
value=properties['nick']) value=properties['nick'],
required=True)
form.addField(var='password', form.addField(var='password',
ftype='text-private', ftype='text-private',
label='Password', label='Password',
@ -920,13 +964,14 @@ class Slixfeed(slixmpp.ClientXMPP):
""" """
form = self['xep_0004'].make_form('result', 'Bookmarks') form = self['xep_0004'].make_form('result', 'Bookmarks')
form['instructions'] = ('🛡 Bookmark has been saved') form['instructions'] = (' Bookmark has been saved')
# In this case (as is typical), the payload is a form # In this case (as is typical), the payload is a form
values = payload['values'] values = payload['values']
await XmppBookmark.add(self, properties=values) await XmppBookmark.add(self, properties=values)
for value in values: for value in values:
key = str(value) key = str(value)
val = str(values[value]) val = str(values[value])
if not val: val = 'None' # '(empty)'
form.add_field(var=key, form.add_field(var=key,
ftype='text-single', ftype='text-single',
label=key.capitalize(), label=key.capitalize(),
@ -1067,7 +1112,7 @@ class Slixfeed(slixmpp.ClientXMPP):
jid = session['from'].bare jid = session['from'].bare
form = self['xep_0004'].make_form('form', form = self['xep_0004'].make_form('form',
'Settings for {}'.format(jid)) 'Settings for {}'.format(jid))
form['instructions'] = ('🛡 Settings have been saved') form['instructions'] = (' Settings have been saved')
jid_file = jid jid_file = jid
db_file = config.get_pathname_to_database(jid_file) db_file = config.get_pathname_to_database(jid_file)

View file

@ -27,21 +27,22 @@ class XmppGroupchat:
# jid = message['groupchat_invite']['jid'] # jid = message['groupchat_invite']['jid']
# else: # else:
# jid = message # jid = message
async def accept_invitation(self, message): def accept_invitation(self, message):
# operator muc_chat # operator muc_chat
inviter = message["from"].bare inviter = message["from"].bare
muc_jid = message['groupchat_invite']['jid'] jid = message['groupchat_invite']['jid']
await self.join(self, inviter, muc_jid) self.join(self, inviter, jid)
async def autojoin(self): def autojoin(self, bookmarks):
result = await self.plugin['xep_0048'].get_bookmarks() conferences = bookmarks["private"]["bookmarks"]["conferences"]
bookmarks = result["private"]["bookmarks"]
conferences = bookmarks["conferences"]
for conference in conferences: for conference in conferences:
if conference["autojoin"]: if conference["jid"] and conference["autojoin"]:
muc_jid = conference["jid"] if not conference["nick"]:
self.plugin['xep_0045'].join_muc(muc_jid, conference["nick"] = self.alias
logging.error('Alias (i.e. Nicknname) is missing for '
'bookmark {}'.format(conference['name']))
self.plugin['xep_0045'].join_muc(conference["jid"],
conference["nick"], conference["nick"],
# If a room password is needed, use: # If a room password is needed, use:
# password=the_room_password, # password=the_room_password,
@ -51,11 +52,14 @@ class XmppGroupchat:
'JID : {}\n' 'JID : {}\n'
'Alias : {}\n' 'Alias : {}\n'
.format(conference["name"], .format(conference["name"],
muc_jid, conference["jid"],
conference["nick"])) conference["nick"]))
elif not conference["jid"]:
logging.error('JID is missing for bookmark {}'
.format(conference['name']))
async def join(self, inviter, muc_jid): def join(self, inviter, jid):
# token = await initdb( # token = await initdb(
# muc_jid, # muc_jid,
# get_settings_value, # get_settings_value,
@ -78,27 +82,26 @@ class XmppGroupchat:
logging.info('Joining groupchat\n' logging.info('Joining groupchat\n'
'JID : {}\n' 'JID : {}\n'
'Inviter : {}\n' 'Inviter : {}\n'
.format(muc_jid, inviter)) .format(jid, inviter))
self.plugin['xep_0045'].join_muc(muc_jid, self.plugin['xep_0045'].join_muc(jid,
self.alias, self.alias,
# If a room password is needed, use: # If a room password is needed, use:
# password=the_room_password, # password=the_room_password,
) )
async def leave(self, muc_jid): def leave(self, jid):
jid = self.boundjid.bare
message = ('This news bot will now leave this groupchat.\n' message = ('This news bot will now leave this groupchat.\n'
'The JID of this news bot is xmpp:{}?message' 'The JID of this news bot is xmpp:{}?message'
.format(jid)) .format(self.boundjid.bare))
status_message = ('This bot has left the group. ' status_message = ('This bot has left the group. '
'It can be reached directly via {}' 'It can be reached directly via {}'
.format(jid)) .format(self.boundjid.bare))
self.send_message(mto=muc_jid, self.send_message(mto=jid,
mfrom=self.boundjid.bare, mfrom=self.boundjid,
mbody=message, mbody=message,
mtype='groupchat') mtype='groupchat')
self.plugin['xep_0045'].leave_muc(muc_jid, self.plugin['xep_0045'].leave_muc(jid,
self.alias, self.alias,
status_message, status_message,
self.boundjid.bare) self.boundjid)

View file

@ -17,8 +17,9 @@ class XmppPresence:
def send(self, jid, status_message, presence_type=None, status_type=None): def send(self, jid, status_message, presence_type=None, status_type=None):
jid_from = str(self.boundjid) if self.is_component else None
self.send_presence(pto=jid, self.send_presence(pto=jid,
pfrom=self.boundjid, pfrom=jid_from,
pshow=status_type, pshow=status_type,
pstatus=status_message, pstatus=status_message,
ptype=presence_type) ptype=presence_type)

View file

@ -565,7 +565,7 @@ async def message(self, message):
XmppMessage.send_reply(self, message, response) XmppMessage.send_reply(self, message, response)
case 'goodbye': case 'goodbye':
if message['type'] == 'groupchat': if message['type'] == 'groupchat':
await XmppGroupchat.leave(self, jid) XmppGroupchat.leave(self, jid)
await XmppBookmark.remove(self, jid) await XmppBookmark.remove(self, jid)
else: else:
response = 'This command is valid in groupchat only.' response = 'This command is valid in groupchat only.'
@ -585,7 +585,7 @@ async def message(self, message):
muc_jid = uri.check_xmpp_uri(message_text[5:]) muc_jid = uri.check_xmpp_uri(message_text[5:])
if muc_jid: if muc_jid:
# TODO probe JID and confirm it's a groupchat # TODO probe JID and confirm it's a groupchat
await XmppGroupchat.join(self, jid, muc_jid) XmppGroupchat.join(self, jid, muc_jid)
# await XmppBookmark.add(self, jid=muc_jid) # await XmppBookmark.add(self, jid=muc_jid)
response = ('Joined groupchat {}' response = ('Joined groupchat {}'
.format(message_text)) .format(message_text))
@ -923,7 +923,7 @@ async def message(self, message):
muc_jid = uri.check_xmpp_uri(message_text) muc_jid = uri.check_xmpp_uri(message_text)
if muc_jid: if muc_jid:
# TODO probe JID and confirm it's a groupchat # TODO probe JID and confirm it's a groupchat
await XmppGroupchat.join(self, jid, muc_jid) XmppGroupchat.join(self, jid, muc_jid)
# await XmppBookmark.add(self, jid=muc_jid) # await XmppBookmark.add(self, jid=muc_jid)
response = ('Joined groupchat {}' response = ('Joined groupchat {}'
.format(message_text)) .format(message_text))