Пока писал различные транспорты / сервисы для jabber’а – часто приходилось выискивать примеры кода, чтобы понять, какие блоки для чего использовать, чтобы вырисовать нужный XML. С PyXMPP было проще – под рукой было 2 проекта с неплохим объемом кода, на котором можно было учиться – наверное, поэтому мне больше PyXMPP и полюбился. С XMPPpy было сложнее, но в итоге разобрался.
В данной заметке – куски кода из моих проектов, реализующие ту или иную функцию. Код, в основном, скопирован “как есть” – разве что с удалением часто повторяющихся одинаковых блоков. Основная задача – показать последовательность действий, которая нужна для “рисования” нужного XML’а.
Первый пример – запрос статистики, XEP-0039: Statistics Gathering. Как указано мной в комментариях для PyXMPP, это неправильная реализация. В правильной должен быть запрос на список фич и дальше работа с этим списком – т.е., все общение идет в 2 шага и в итоге должен возвращаться только список запрошенных фич. По факту в ботах (а, фактически, такая статистика проверялась мной только на ботах) на запрос сразу возвращался полный список – в итоге и я так реализовывал для совместимости.
Сам список в довольно свободной форме – делается что-то типа категорий и значений.
XMPP stats example for PyXMPP
def get_stats(self, iq):
# It's incorrect implementation of XEP-0039, it should be in 2 steps (like search)
iq = iq.make_result_response()
q = iq.new_query("http://jabber.org/protocol/stats")
upt = q.newChild(None, "stat", None)
upt.setProp("name", 'time/uptime')
upt.setProp("units", 'seconds')
upt.setProp("value", str(int(time.time()) - self.start_time))
reqsh = q.newChild(None, "stat", None)
reqsh.setProp("name", 'messages/hourly')
reqsh.setProp("units", 'messages')
reqsh.setProp("value", str(hourly))
self.stream.send(iq)
return 1
В погодном транспорте я статистику не реализовывал, так что примера кода нет – но в целом, это несложно сделать по образцу остального кода.
Второй пример – последняя активность или запрос аптайма (в случае с сервером или транспортом) – XEP-0012: Last Activity.
Тут все довольно просто:
Третий пример – XEP-0199: XMPP Ping. Еще проще:
Четвертый – XEP-0202: Entity Time.
XMPP time example for PyXMPP
def get_time(self, iq):
iq = iq.make_result_response()
q = iq.xmlnode.newChild(None, "time", None)
q.setProp("xmlns", "urn:xmpp:time")
q.newTextChild(q.ns(), "tzo", "+02:00")
q.newTextChild(q.ns(), "utc", time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()))
self.stream.send(iq)
return 1
XMPP time example for XMPPpy
def iq_time_handler(self, iq):
repl = iq.buildReply('result')
query = Node('time')
query.setTagData(tag='tzo', val="+02:00")
query.setTagData(tag='utc', val=time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()))
repl.setPayload([query])
self.jabber.send(repl)
raise NodeProcessed
Пятый – XEP-0054: vcard-temp. Фактически, в основном линейная XML’ка. Со всеми опциями можно ознакомиться в XEP’е. Вложенным параметров (из популярных) является только фото – по тому же принципу можно конструировать и другие многоуровневые данные.
В принципе, тем же кодом можно реализовать и XEP-0292: vCard4 Over XMPP – там немного отличается структура, но в основном – все то же.
XMPP vcard-temp example for PyXMPP
def get_vCard(self,iq):
iqmr=iq.make_result_response()
q=iqmr.xmlnode.newChild(None,"vCard",None)
q.setProp("xmlns","vcard-temp")
transav=q.newTextChild(None,"PHOTO", None)
transav.newTextChild(None, "BINVAL", self.gttlogo)
transav.newTextChild(None, "TYPE", 'image/png')
q.newTextChild(None,"BDAY","2022-12-07")
q.newTextChild(None,"URL","https://github.com/jabberworld/gtt")
if iq.get_to() == self.name:
q.newTextChild(None,"FN","Google Translate Transport")
q.newTextChild(None,"NICKNAME","Google Translate")
q.newTextChild(None,"DESC","Google Translate Transport")
q.newTextChild(None,"ROLE","Создаю ботов для получения перевода текста через Google Translate")
self.stream.send(iqmr)
return 1
XMPP vcard-temp example for XMPPpy
def iq_vcard_handler(self, iq):
repl = iq.buildReply('result')
query = xmpp.Node('vCard', attrs={'xmlns':xmpp.NS_VCARD})
if str(iq.getTo()) == self.domain:
query.setTagData(tag='NICKNAME', val='Weather')
query.setTagData(tag='FN', val='Jabber Weather Transport')
query.setTagData(tag='BDAY', val='2022-07-22')
query.setTagData(tag='DESC', val='gismeteo.ru and meteonova.ru weather service')
query.setTagData(tag='ROLE', val='Создаю ботов для получения погоды с '+datasrc)
query.setTagData(tag='URL', val='https://github.com/jabberworld/gismeteo')
transav = query.addChild('PHOTO')
transav.setTagData(tag='BINVAL', val=self.weatherlogo)
transav.setTagData(tag='TYPE', val='image/png')
repl.setPayload([query])
self.jabber.send(repl)
raise NodeProcessed
Шестой пример – XEP-0092: Software Version. В варианте для XMPPpy чуть упрощенный пример, но разобраться несложно.
XMPP version example for PyXMPP
def get_version(self,iq):
global programmVersion
iq=iq.make_result_response()
q=iq.new_query("jabber:iq:version")
q.newTextChild(q.ns(), "name", "Google Translate Transport")
q.newTextChild(q.ns(), "version", programmVersion + " (" + str(len(self.LANGUAGES)) + " languages)")
q.newTextChild(q.ns(), "os", "Python "+sys.version.split()[0]+" + PyXMPP")
self.stream.send(iq)
return 1
XMPP version example for XMPPpy
def iq_version_handler(self, iq):
name = Node('name')
name.setData("Jabber Weather Service")
version = Node('version')
version.setData(self.version)
repl = iq.buildReply('result')
query = xmpp.Node('query', attrs={'xmlns':xmpp.NS_VERSION})
query.addChild(node=name)
query.addChild(node=version)
repl.setPayload([query])
self.jabber.send(repl)
raise NodeProcessed
Ну и напоследок – работа с формами: XEP-0004: Data Forms – может быть полезно при реализации функции поиска или регистрации. Общение двухэтапное, поэтому сначала идет функция получения формы (get search или get register), а дальше на полученные поля и переключатели идет вторая передача данных с установленными параметрами – set search / set register – ну и возврат ответ от сервера. Возможно, этапов может быть больше (последовательное получение нескольких форм) – не знаю, не пробовал.
XMPP form example for PyXMPP
def get_search(self,iq):
iq=iq.make_result_response()
q=iq.xmlnode.newChild(None,"query",None)
q.setProp("xmlns","jabber:iq:search")
q.newTextChild(None,"instructions","Enter a keyword")
form=q.newChild(None,"x",None)
form.setProp("xmlns","jabber:x:data")
form.setProp("type","form")
formType=form.newChild(None,"field",None)
formType.setProp("type","hidden")
formType.setProp("var","FORM_TYPE")
formType.newTextChild(None,"value","jabber:iq:search")
text=form.newChild(None,"field",None)
text.setProp("type","text-single")
text.setProp("label","Search")
text.setProp("var","searchField")
self.stream.send(iq)
return 1
def set_search(self, iq):
fromjid = iq.get_from().bare()
searchField=iq.xpath_eval("//r:field[@var='searchField']/r:value",{"r":"jabber:x:data"})
if searchField:
searchField='%'+searchField[0].getContent().replace("%","\\%")+'%'
else:
return
if searchField=='%%' or len(searchField)<5:
self.stream.send(iq.make_error_response("not-acceptable"))
return
self.dbCurST.execute("COMMIT")
self.dbCurST.execute("SELECT feedname, description, url, subscribers, timeout FROM feeds WHERE (feedname LIKE %s OR description LIKE %s OR url LIKE %s OR tags LIKE %s) AND (private = '0' OR (private = '1' AND registrar = %s))", (searchField, searchField, searchField, searchField, fromjid))
a=self.dbCurST.fetchall()
print (a)
iq=iq.make_result_response()
q=iq.new_query("jabber:iq:search")
form=q.newChild(None,"x",None)
form.setProp("xmlns","jabber:x:data")
form.setProp("type","result")
formType=form.newChild(None,"field",None)
formType.setProp("type","hidden")
formType.setProp("var","FORM_TYPE")
formType.newTextChild(None,"value","jabber:iq:search")
reported=form.newChild(None,"reported",None)
reportedJid=reported.newChild(None,"field",None)
reportedJid.setProp("var","jid")
reportedJid.setProp("label","JID")
reportedJid.setProp("type","jid-single")
reportedUrl=reported.newChild(None,"field",None)
reportedUrl.setProp("var","url")
reportedUrl.setProp("label","URL")
reportedUrl.setProp("type","text-single")
*****
for d in a:
item=form.newChild(None, "item", None)
jidField=item.newChild(None, "field", None)
jidField.setProp("var", "jid")
jiddata = d[0]+"@"+self.name
jidField.newTextChild(None, "value", jiddata.encode('utf-8'))
urlField=item.newChild(None, "field", None)
urlField.setProp("var", "url")
urlField.newTextChild(None, "value", d[2].encode('utf-8'))
*****
self.stream.send(iq)
return 1
XMPP form example for XMPPpy
def iq_search_handler(self, iq):
typ = iq.getType()
iq_children = iq.getQueryChildren()
if (typ=='get') and (not iq_children):
repl = iq.buildReply('result')
repl.setQueryPayload(self.get_register_form())
self.jabber.send(repl)
raise NodeProcessed
elif (typ=='set') and iq_children:
self.set_register_form(iq)
raise NodeProcessed
def get_register_form(self):
ft = DataForm('form')
ft.addChild(node=DataField(name='FORM_TYPE',value=NS_SEARCH, typ='hidden'))
ft.addChild(node=DataField(name='city', label='Город', typ='text-single'))
return [ft]
def set_register_form(self, iq):
iq_children = iq.getQueryChildren()
for nod in iq_children:
for k in nod.getChildren():
if k.getAttr('var') == 'city':
for j in k.getChildren():
searchField = j.getData()
if searchField:
searchField='%'+searchField.replace("%","\\%")+'%'
else:
return
if searchField=='%%' or len(searchField)<4:
self.send_bad_request(iq)
return
data = cur.execute("SELECT * FROM cityindex WHERE idx LIKE (?) OR country_en LIKE (?) OR country_ru LIKE (?) OR name_en LIKE (?) OR name_ru LIKE (?) OR keywords LIKE (?) LIMIT 100", (searchField, searchField, searchField, searchField, searchField, searchField))
data = cur.fetchall()
print(data)
repl = iq.buildReply('result')
query = xmpp.Node('query', attrs={'xmlns':xmpp.NS_SEARCH})
rprt = Node('reported', payload=[
DataField(label='JID' ,name='jid' ,typ='jid-single'),
DataField(label='Index' ,name='idx' ,typ='text-single'),
DataField(label='Страна' ,name='cru' ,typ='text-single'),
DataField(label='Регион' ,name='rru' ,typ='text-single'),
DataField(label='Город' ,name='nru' ,typ='text-single'),
DataField(label='Country' ,name='cen' ,typ='text-single'),
DataField(label='Region' ,name='ren' ,typ='text-single'),
DataField(label='City' ,name='nen' ,typ='text-single'),
DataField(label='Lat.' ,name='lat' ,typ='text-single'),
DataField(label='Long.' ,name='lon' ,typ='text-single'),
DataField(label='Alt.' ,name='alt' ,typ='text-single'),
DataField(label='Keywords' ,name='kwr' ,typ='text-single')])
form = DataForm('result')
form.addChild(node=DataField(name='FORM_TYPE', value=NS_SEARCH, typ='hidden'))
form.addChild(node=rprt)
for flds in data:
rpl = Node('item', payload=[
DataField(name='jid', value=JID(node=unicode(str(flds[0]), "utf-8"), domain=self.domain)),
DataField(name='idx', value=flds[0]),
DataField(name='cru', value=flds[1]),
DataField(name='rru', value=flds[2]),
DataField(name='cen', value=flds[3]),
DataField(name='ren', value=flds[4]),
DataField(name='nru', value=flds[6]),
DataField(name='nen', value=flds[5]),
DataField(name='lat', value=flds[7]),
DataField(name='lon', value=flds[8]),
DataField(name='alt', value=flds[9]),
DataField(name='kwr', value=flds[10])])
form.addChild(node=rpl)
query.addChild(node=form)
repl.setPayload([query])
self.jabber.send(repl)
Также между этими библиотеками различается и разбор переданных параметров – xpath_eval в PyXMPP и проход по дереву в XMPPpy.