Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • gajim/gajim-plugins
  • lovetox/gajim-plugins
  • ag/gajim-plugins
  • FlorianMuenchbach/gajim-plugins
  • rom1dep/gajim-plugins
  • pitchum/gajim-plugins
  • wurstsalat/gajim-plugins
  • Dicson/gajim-plugins
  • andre/gajim-plugins
  • link2xt/gajim-plugins
  • marmistrz/gajim-plugins
  • Jens/gajim-plugins
  • muelli/gajim-plugins
  • asterix/gajim-plugins
  • orhideous/gajim-plugins
  • ngvelprz/gajim-plugins
  • appleorange1/gajim-plugins
  • Martin/gajim-plugins
  • maltel/gajim-plugins
  • Seve/gajim-plugins
  • evert-mouw/gajim-plugins
  • Yuki/gajim-plugins
  • mxre/gajim-plugins
  • ValdikSS/gajim-plugins
  • SaltyBones/gajim-plugins
  • comradekingu/gajim-plugins
  • ritzmann/gajim-plugins
  • genofire/gajim-plugins
  • jjrh/gajim-plugins
  • yarmak/gajim-plugins
  • PapaTutuWawa/gajim-plugins
  • weblate/gajim-plugins
  • XutaxKamay/gajim-plugins
  • nekk/gajim-plugins
  • principis/gajim-plugins
  • cbix/gajim-plugins
  • bodqhrohro/gajim-plugins
  • airtower-luna/gajim-plugins
  • toms/gajim-plugins
  • mesonium/gajim-plugins
  • lissine/gajim-plugins
  • anviar/gajim-plugins
42 results
Show changes
Showing
with 672 additions and 307 deletions
File added
[info]
name: Emoticons pack
short_name: emoticons_pack
version: 0.1.5
description: Install, update and view detailed legend of emoticons
authors: Denis Fomin <fominde@gmail.com>
homepage: http://trac-plugins.gajim.org/wiki/
min_gajim_version: 0.16
max_gajim_version: 0.16.9
import sqlite3 import sqlite3
from common import gajim
import sys import sys
import os import os
'''
TODO:
1-) Modify the database class to use models instead of method arguments
2-) Normalize the database. Don't save dirs and files in the same table
'''
class FilesharingDatabase: class FilesharingDatabase:
def __init__(self, plugin):
self.plugin = plugin def __init__(self, FILE_PATH=None):
path_l = os.path.split(plugin.config.FILE_PATH) if not FILE_PATH:
return
path_l = os.path.split(FILE_PATH)
path = os.path.join(path_l[0], 'shared_files.db') path = os.path.join(path_l[0], 'shared_files.db')
db_exist = os.path.exists(path) db_exist = os.path.exists(path)
self.conn = sqlite3.connect(path) self.conn = sqlite3.connect(path)
...@@ -31,13 +38,13 @@ class FilesharingDatabase: ...@@ -31,13 +38,13 @@ class FilesharingDatabase:
self.conn.commit() self.conn.commit()
c.close() c.close()
def get_toplevel_files(self, account, requester): def get_toplevel_dirs(self, account, requester):
c = self.conn.cursor() c = self.conn.cursor()
data = (account, requester) data = (account, requester)
c.execute("SELECT relative_path, hash_sha1, size, description, " + c.execute("SELECT relative_path, hash_sha1, size, description, " +
"mod_date, is_dir FROM (files JOIN permissions ON" + "mod_date, is_dir FROM (files JOIN permissions ON" +
" files.fid=permissions.fid) WHERE account=? AND requester=?" + " files.fid=permissions.fid) WHERE account=? AND requester=?" +
" AND relative_path NOT LIKE '%/%'", data) " AND is_dir=1 AND relative_path NOT LIKE '%/%'", data)
result = c.fetchall() result = c.fetchall()
c.close() c.close()
return result return result
...@@ -88,16 +95,15 @@ class FilesharingDatabase: ...@@ -88,16 +95,15 @@ class FilesharingDatabase:
else: else:
data = (account, requester, name) data = (account, requester, name)
sql = "SELECT relative_path, hash_sha1, size, description, " + \ sql = "SELECT relative_path, hash_sha1, size, description, " + \
"mod_date, file_path FROM (files JOIN permissions ON" + \ "mod_date, file_path, is_dir FROM (files JOIN permissions ON" + \
" files.fid=permissions.fid) WHERE account=? AND requester=?" +\ " files.fid=permissions.fid) WHERE account=? AND requester=?" +\
" AND relative_path=?" " AND relative_path=?"
c.execute(sql, data) c.execute(sql, data)
result = c.fetchall() result = c.fetchall()
c.close() c.close()
if result == []: if result != []:
return None
else:
return result[0] return result[0]
return result
def get_files_name(self, account, requester): def get_files_name(self, account, requester):
result = self.get_files(account, requester) result = self.get_files(account, requester)
...@@ -115,7 +121,8 @@ class FilesharingDatabase: ...@@ -115,7 +121,8 @@ class FilesharingDatabase:
>>> _delete_file(1) >>> _delete_file(1)
""" """
self._check_duplicate(account, requester, file_) self._check_duplicate(account, requester, file_)
requester = gajim.get_jid_without_resource(requester) if requester.find('/') != -1:
raise Exception('The requester must be given without a resource attached')
c = self.conn.cursor() c = self.conn.cursor()
c.execute("INSERT INTO files (file_path, " + c.execute("INSERT INTO files (file_path, " +
"relative_path, hash_sha1, size, description, mod_date, " + "relative_path, hash_sha1, size, description, mod_date, " +
...@@ -138,7 +145,7 @@ class FilesharingDatabase: ...@@ -138,7 +145,7 @@ class FilesharingDatabase:
data = (account, requester, file_[2]) data = (account, requester, file_[2])
c.execute("SELECT * FROM (files JOIN permissions ON" + c.execute("SELECT * FROM (files JOIN permissions ON" +
" files.fid=permissions.fid) WHERE account=? AND requester=?" + " files.fid=permissions.fid) WHERE account=? AND requester=?" +
" AND hash_sha1=?)", data) " AND hash_sha1=?", data)
result.extend(c.fetchall()) result.extend(c.fetchall())
if len(result) > 0: if len(result) > 0:
raise Exception('Duplicated entry') raise Exception('Duplicated entry')
......
...@@ -14,7 +14,7 @@ from fileshare_window import FileShareWindow ...@@ -14,7 +14,7 @@ from fileshare_window import FileShareWindow
import fshare_protocol import fshare_protocol
from common import ged from common import ged
from common import caps_cache from common import caps_cache
from common import xmpp from common import nbxmpp
from plugins.gui import GajimPluginConfigDialog from plugins.gui import GajimPluginConfigDialog
...@@ -26,17 +26,15 @@ class FileSharePlugin(GajimPlugin): ...@@ -26,17 +26,15 @@ class FileSharePlugin(GajimPlugin):
@log_calls('FileSharePlugin') @log_calls('FileSharePlugin')
def init(self): def init(self):
self.activated = False self.activated = False
self.description = _('This plugin allows you to share folders'
' with a peer using jingle file transfer.')
self.config_dialog = FileSharePluginConfigDialog(self) self.config_dialog = FileSharePluginConfigDialog(self)
home_path = os.path.expanduser('~/') home_path = os.path.expanduser('~/')
self.config_default_values = {'incoming_dir': (home_path, '')} self.config_default_values = {'incoming_dir': (home_path, '')}
self.database = database.FilesharingDatabase(self) self.database = database.FilesharingDatabase(self.config.FILE_PATH)
# Create one protocol handler per account # Create one protocol handler per account
accounts = gajim.contacts.get_accounts() accounts = gajim.contacts.get_accounts()
for account in gajim.contacts.get_accounts(): for account in gajim.contacts.get_accounts():
FileSharePlugin.prohandler[account] = \ FileSharePlugin.prohandler[account] = \
fshare_protocol.Protocol(account, self) fshare_protocol.ProtocolDispatcher(account, self)
self.events_handlers = { self.events_handlers = {
'raw-iq-received': (ged.CORE, self._nec_raw_iq) 'raw-iq-received': (ged.CORE, self._nec_raw_iq)
} }
...@@ -81,12 +79,13 @@ class FileSharePlugin(GajimPlugin): ...@@ -81,12 +79,13 @@ class FileSharePlugin(GajimPlugin):
gajim.connections[a].status) gajim.connections[a].status)
def _nec_raw_iq(self, obj): def _nec_raw_iq(self, obj):
if obj.stanza.getTag('match', if obj.stanza.getTag('query',
namespace=fshare_protocol.NS_FILE_SHARING) and self.activated: namespace=fshare_protocol.NS_FILE_SHARING) and self.activated:
account = obj.conn.name account = obj.conn.name
pro = FileSharePlugin.prohandler[account] pro = FileSharePlugin.prohandler[account]
pro.handler(obj.stanza) peerjid = obj.stanza.getFrom()
raise xmpp.NodeProcessed pro.handler(obj.stanza, str(peerjid))
raise nbxmpp.NodeProcessed
def __get_contact_menu(self, contact, account): def __get_contact_menu(self, contact, account):
raise NotImplementedError raise NotImplementedError
...@@ -176,7 +175,7 @@ class FileSharePluginConfigDialog(GajimPluginConfigDialog): ...@@ -176,7 +175,7 @@ class FileSharePluginConfigDialog(GajimPluginConfigDialog):
def on_run(self): def on_run(self):
widget = self.xml.get_object('dl_folder') widget = self.xml.get_object('dl_folder')
widget.set_text(str(self.plugin.config['incoming_dir'])) widget.set_text(str(self.plugin.config['incoming_dir']))
def on_hide(self, widget): def on_hide(self, widget):
widget = self.xml.get_object('dl_folder') widget = self.xml.get_object('dl_folder')
self.plugin.config['incoming_dir'] = widget.get_text() self.plugin.config['incoming_dir'] = widget.get_text()
from common import xmpp import nbxmpp
from common import helpers from nbxmpp import Hashes
from common import gajim
from common import XMPPDispatcher try:
from common.xmpp import Hashes from common import helpers
from common import gajim
except ImportError:
print "Import Error: Ignore if we are testing"
# Namespace for file sharing # Namespace for file sharing
NS_FILE_SHARING = 'http://gajim.org/protocol/filesharing' NS_FILE_SHARING = 'urn:xmpp:fis:0'
class Protocol(): class Protocol():
'''
Creates and extracts information from stanzas
'''
def __init__(self, ourjid):
# set our jid with resource
self.ourjid = ourjid
def convert_dbformat(self, records):
# Converts db output format to the one expected by the Protocol methods
formatted = []
for record in records:
r = {'name' : record[0]}
if record[-1] == 0:
r['type'] = 'file'
if record[1] != None or record[1] != '':
r['hash'] = record[1]
if record[2] != None or record[2] != '':
r['size'] = record[2]
if record[3] != None or record[3] != '':
r['desc'] = record[3]
if record[4] != None or record[4] != '':
r['date'] = record[4]
else:
r['type'] = 'directory'
formatted.append(r)
return formatted
def request(self, contact, path=None):
iq = nbxmpp.Iq(typ='get', to=contact, frm=self.ourjid)
query = iq.setQuery()
query.setNamespace(NS_FILE_SHARING)
if path:
query.setAttr('node', path)
return iq
def buildFileNode(self, file_info):
node = nbxmpp.Node(tag='file')
node.setNamespace(nbxmpp.NS_JINGLE_FILE_TRANSFER)
if not 'name' in file_info:
raise Exception("Child name is required.")
node.addChild(name='name').setData(file_info['name'])
if 'date' in file_info:
node.addChild(name='date').setData(file_info['date'])
if 'desc' in file_info:
node.addChild(name='desc').setData(file_info['desc'])
if 'size' in file_info:
node.addChild(name='size').setData(file_info['size'])
if 'hash' in file_info:
h = Hashes()
h.addHash(file_info['hash'], 'sha-1')
node.addChild(node=h)
return node
def offer(self, id_, contact, node, items):
iq = nbxmpp.Iq(typ='result', to=contact, frm=self.ourjid,
attrs={'id': id_})
query = iq.setQuery()
query.setNamespace(NS_FILE_SHARING)
if node:
query.setAttr('node', node)
for item in items:
if item['type'] == 'file':
fn = self.buildFileNode(item)
query.addChild(node=fn)
elif item['type'] == 'directory':
query.addChild(name='directory', attrs={'name': item['name']})
else:
raise Exception("Unexpected Type")
return iq
class ProtocolDispatcher(Protocol):
'''
Sends and receives stanzas
'''
def __init__(self, account, plugin): def __init__(self, account, plugin):
self.account = account self.account = account
self.plugin = plugin self.plugin = plugin
self.conn = gajim.connections[self.account] self.conn = gajim.connections[self.account]
# get our jid with resource # get our jid with resource
self.ourjid = gajim.get_jid_from_account(self.account) ourjid = gajim.get_jid_from_account(self.account)
Protocol.__init__(self, ourjid)
self.fsw = None self.fsw = None
def set_window(self, window):
self.fsw = window
def request(self, contact, name=None, isFile=False):
iq = xmpp.Iq(typ='get', to=contact, frm=self.ourjid)
match = iq.addChild(name='match', namespace=NS_FILE_SHARING)
request = match.addChild(name='request')
if not isFile and name is None:
request.addChild(name='directory')
elif not isFile and name is not None:
dir_ = request.addChild(name='directory')
dir_.addChild(name='name').addData('/' + name)
elif isFile:
pass
return iq
def __buildReply(self, typ, stanza): def set_window(self, fsw):
iq = xmpp.Iq(typ, to=stanza.getFrom(), frm=stanza.getTo(), self.fsw = fsw
attrs={'id': stanza.getID()})
iq.addChild(name='match', namespace=NS_FILE_SHARING)
return iq
def on_request(self, stanza):
try: def handler(self, stanza, fjid):
fjid = helpers.get_full_jid_from_iq(stanza) # handles incoming stanza
except helpers.InvalidFormat: # TODO: Stanza checking
# A message from a non-valid JID arrived, it has been ignored. if stanza.getType() == 'get':
return offer = self.on_request(stanza, fjid)
if stanza.getTag('error'): self.conn.connection.send(offer)
# TODO: better handle this elif stanza.getType() == 'result':
return return self.on_offer(stanza, fjid)
jid = gajim.get_jid_without_resource(fjid)
req = stanza.getTag('match').getTag('request')
if req.getTag('directory') and not \
req.getTag('directory').getChildren():
# We just received a toplevel directory request
files = self.plugin.database.get_toplevel_files(self.account, jid)
response = self.offer(stanza.getID(), fjid, files)
self.conn.connection.send(response)
elif req.getTag('directory') and req.getTag('directory').getTag('name'):
dir_ = req.getTag('directory').getTag('name').getData()[1:]
files = self.plugin.database.get_files_from_dir(self.account, jid, dir_)
response = self.offer(stanza.getID(), fjid, files)
self.conn.connection.send(response)
def on_offer(self, stanza):
# We just got a stanza offering files
fjid = helpers.get_full_jid_from_iq(stanza)
info = get_files_info(stanza)
if fjid not in self.fsw.browse_jid or not info:
# We weren't expecting anything from this contact, do nothing
# Or we didn't receive any offering files
return
flist = []
for f in info[0]:
flist.append(f['name'])
flist.extend(info[1])
self.fsw.browse_fref = self.fsw.add_file_list(flist, self.fsw.ts_search,
self.fsw.browse_fref,
self.fsw.browse_jid[fjid]
)
for f in info[0]:
iter_ = self.fsw.browse_fref[f['name']]
path = self.fsw.ts_search.get_path(iter_)
self.fsw.brw_file_info[path] = (f['name'], f['date'], f['size'],
f['hash'], f['desc'])
# TODO: add tooltip
'''
for f in info[0]:
r = self.fsw.browse_fref[f['name']]
path = self.fsw.ts_search.get_path(r)
# AM HERE WORKING ON THE TOOLTIP
tooltip.set_text('noooo')
self.fsw.tv_search.set_tooltip_row(tooltip, path)
'''
for dir_ in info[1]:
if dir_ not in self.fsw.empty_row_child:
parent = self.fsw.browse_fref[dir_]
row = self.fsw.ts_search.append(parent, ('',))
self.fsw.empty_row_child[dir_] = row
def handler(self, stanza):
# handles incoming match stanza
if stanza.getTag('match').getTag('offer'):
self.on_offer(stanza)
elif stanza.getTag('match').getTag('request'):
self.on_request(stanza)
else: else:
# TODO: reply with malformed stanza error # TODO: reply with malformed stanza error
pass pass
def offer(self, id_, contact, items): def on_toplevel_request(self, stanza, fjid):
iq = xmpp.Iq(typ='result', to=contact, frm=self.ourjid, jid = get_jid_without_resource(fjid)
attrs={'id': id_}) roots = self.plugin.database.get_toplevel_dirs(self.account, jid)
match = iq.addChild(name='match', namespace=NS_FILE_SHARING) items = []
offer = match.addChild(name='offer') for root in roots:
if len(items) == 0: items.append({'type' : 'directory',
offer.addChild(name='directory') 'name' : root[0]
})
return self.offer(stanza.getID(), fjid, None, items)
def on_dir_request(self, stanza, fjid, jid, dir_):
result = self.plugin.database.get_files_from_dir(self.account, jid, dir_)
result = self.convert_dbformat(result)
return self.offer(stanza.getID(), fjid, dir_, result)
def on_request(self, stanza, fjid):
jid = get_jid_without_resource(fjid)
if stanza.getTag('error'):
# TODO: better handle this
return -1
node = stanza.getQuery().getAttr('node')
if node is None:
return self.on_toplevel_request(stanza, fjid)
result = self.plugin.database.get_file(self.account, jid, None, node)
if result == []:
return self.offer(stanza.getID(), fjid, node, result)
if result[-1] == 1:
# The peer asked for the content of a dir
reply = self.on_dir_request(self, stanza, fjid)
else: else:
for i in items: # The peer asked for more information on a file
# if it is a directory # TODO: Refactor, make method to convert db format to expect file
if i[5] == True: # format
item = offer.addChild(name='directory') file_ = [{'type' : 'file',
name = item.addChild('name') 'name' : result[0].split('/')[-1],
name.setData('/' + i[0]) 'hash' : result[1],
else: 'size' : result[2],
item = offer.addChild(name='file') 'desc' : result[3],
item.addChild('name').setData('/' + i[0]) 'date' : result[4],
if i[1] != '': }]
h = Hashes() reply = self.offer(stanza.getID(), fjid, node, file_)
h.addHash(i[1], 'sha-1') return reply
item.addChild(node=h)
item.addChild('size').setData(i[2])
item.addChild('desc').setData(i[3])
item.addChild('date').setData(i[4])
return iq
def set_window(self, fsw): def on_offer(self, stanza, fjid):
self.fsw = fsw offered = []
query = stanza.getQuery()
for child in query.getChildren():
if child.getName() == 'directory':
offered.append({'name' : child.getAttr('name'),
'type' : 'directory'})
elif child.getName() == 'file':
attrs = {'type' : 'file'}
grandchildren = child.getChildren()
for grandchild in grandchildren:
attrs[grandchild.getName()] = grandchild.getData()
offered.append(attrs)
else:
print 'File sharing. Cant handle unknown type: ' + str(child)
return offered
def get_files_info(stanza): def get_jid_without_resource(jid):
# Crawls the stanza in search for file and dir structure. return jid.split('/')[0]
files = []
dirs = []
children = stanza.getTag('match').getTag('offer').getChildren()
for c in children:
if c.getName() == 'file':
f = {'name' : \
c.getTag('name').getData()[1:] if c.getTag('name') else '',
'size' : c.getTag('size').getData() if c.getTag('size') else '',
'date' : c.getTag('date').getData() if c.getTag('date') else '',
'desc' : c.getTag('desc').getData() if c.getTag('desc') else '',
# TODO: handle different hash algo
'hash' : c.getTag('hash').getData() if c.getTag('hash') else '',
}
files.append(f)
else:
dirname = c.getTag('name')
if dirname is None:
return None
dirs.append(dirname.getData()[1:])
return (files, dirs)
[info] [info]
name: File Sharing name: File Sharing
short_name: fshare short_name: fshare
#version: 0.1 #version: 0.1.1
description: This plugin allows you to share folders with your peers using jingle file transfer. description: This plugin allows you to share folders with your peers using jingle file transfer.
authors: Jefry Lagrange <jefry.reyes@gmail.com> authors: Jefry Lagrange <jefry.reyes@gmail.com>
homepage: www.google.com homepage: www.gajim.org
max_gajim_version: 0.16.9
#/usr/bin/python
import unittest
from mock import Mock
import sys, os
sys.path.append(os.path.abspath(sys.path[0]) + '/../')
import fshare_protocol
import nbxmpp
class TestProtocol(unittest.TestCase):
def setUp(self):
self.protocol = fshare_protocol.Protocol('test@gajim.org/test')
def test_request(self):
iq = self.protocol.request('peer@gajim.org/test', 'documents/test2.txt')
self.assertEqual(iq.getType(), 'get')
self.assertEqual(iq.getQuery().getName(), 'query')
self.assertEqual(iq.getQuery().getNamespace(), fshare_protocol.NS_FILE_SHARING)
self.assertEqual(iq.getQuery().getAttr('node'), 'documents/test2.txt')
def test_convert_dbformat(self):
file_ = [(u'relative_path', u'hash', 999, u'description',
u'date', u'file_path', 0)]
formatted = self.protocol.convert_dbformat(file_)
self.assertNotEqual(len(formatted), 0)
for item in formatted:
self.assertEqual(type(item), type({}))
self.assertEqual(formatted[0]['type'], 'file')
def test_buildFileNode(self):
file_info = {'name' : 'test2.text',
'desc' : 'test',
'hash' : '00000',
'size' : '00000',
'type' : 'file'
}
node = self.protocol.buildFileNode(file_info)
self.assertEqual(node.getName(), 'file')
self.assertEqual(node.getNamespace(), nbxmpp.NS_JINGLE_FILE_TRANSFER)
self.assertEqual(len(node.getChildren()), 4)
def test_offer(self):
items = [ {'name' : 'test2.text',
'desc' : 'test',
'hash' : '00000',
'size' : '00000',
'type' : 'file'
},
{
'name' : 'secret docs',
'type' : 'directory'
}
]
iq = self.protocol.offer('1234', 'peer@gajim.org/test',
'documents', items)
self.assertEqual(iq.getType(), 'result')
self.assertNotEqual(iq.getID(), None)
self.assertEqual(iq.getQuery().getName(), 'query')
self.assertEqual(iq.getQuery().getNamespace(), fshare_protocol.NS_FILE_SHARING)
self.assertEqual(iq.getQuery().getAttr('node'), 'documents')
node = iq.getQuery()
self.assertEqual(len(node.getChildren()), 2)
# Mock modules
gajim = Mock()
attr = {'get_jid_from_account.return_value': 'test@gajim.org/test'}
gajim.configure_mock(**attr)
fshare_protocol.gajim = gajim
fshare_protocol.helpers = Mock()
class TestProtocolDispatcher(unittest.TestCase):
def setUp(self):
self.account = 'test@gajim.org'
self.protocol = fshare_protocol.Protocol(self.account)
testc = {self.account : Mock()}
fshare_protocol.gajim.connections = testc
database = Mock()
top_dirs = [(u'relative_path1', None, None, None, None, 1),
(u'relative_path2', None, None, None, None, 1)]
file_ = (u'file1', u'hash', 999, u'description',
u'date', u'file_path', 0)
attr = {'get_toplevel_dirs.return_value': top_dirs,
'get_file.return_value': file_,
'get_files_from_dir.return_value' : [file_, top_dirs[0]]
}
database.configure_mock(**attr)
plugin = Mock()
plugin.database = database
self.dispatcher = fshare_protocol.ProtocolDispatcher(
self.account, plugin)
def test_handler(self):
iq = self.protocol.request('peer@gajim.org/test',
'documents/test2.txt')
#offer = self.dispatcher.on_offer
request = self.dispatcher.on_request
#self.dispatcher.on_offer = Mock()
self.dispatcher.on_request = Mock()
self.dispatcher.handler(iq, 'peer@gajim.org/test')
assert(self.dispatcher.on_request.called)
self.dispatcher.on_request = request
def test_on_offer(self):
items = [ {'name' : 'test2.text',
'type' : 'file'
},
{
'name' : 'secret docs',
'type' : 'directory'
}
]
iq = self.protocol.offer('1234', 'peer@gajim.org/test',
'documents', items)
offered_files = self.dispatcher.on_offer(iq, 'peer@gajim.org/test')
self.assertEqual(len(offered_files), 2)
def test_on_dir_request(self):
iq = self.protocol.request('peer@gajim.org/test', 'documents')
response = self.dispatcher.on_dir_request(iq, 'peer@gajim.org/test',
'peer@gajim.org', 'documents')
self.assertEqual(response.getType(), 'result')
self.assertEqual(response.getQuery().getName(), 'query')
self.assertEqual(response.getQuery().getNamespace(), fshare_protocol.NS_FILE_SHARING)
self.assertEqual(response.getQuery().getAttr('node'), 'documents')
node = response.getQuery()
self.assertEqual(len(node.getChildren()), 2)
def test_on_request(self):
iq = self.protocol.request('peer@gajim.org/test','documents/file1.txt')
response = self.dispatcher.on_request(iq, 'peer@gajim.org/test')
self.assertEqual(response.getType(), 'result')
self.assertEqual(response.getQuery().getName(), 'query')
self.assertEqual(response.getQuery().getNamespace(), fshare_protocol.NS_FILE_SHARING)
self.assertEqual(response.getQuery().getAttr('node'), 'documents/file1.txt')
node = response.getQuery()
self.assertEqual(len(node.getChildren()), 1)
def test_on_toplevel_request(self):
iq = self.protocol.request('peer@gajim.org/test')
response = self.dispatcher.on_toplevel_request(iq, 'peer@gajim.org')
self.assertEqual(response.getType(), 'result')
self.assertEqual(response.getQuery().getName(), 'query')
self.assertEqual(response.getQuery().getNamespace(), fshare_protocol.NS_FILE_SHARING)
node = response.getQuery()
self.assertEqual(len(node.getChildren()), 2)
if __name__ == '__main__':
unittest.main()
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<object class="GtkTable" id="config_table"> <object class="GtkTable" id="config_table">
<property name="visible">True</property> <property name="visible">True</property>
<property name="border_width">6</property> <property name="border_width">6</property>
<property name="n_rows">2</property> <property name="n_rows">3</property>
<property name="n_columns">2</property> <property name="n_columns">2</property>
<property name="column_spacing">7</property> <property name="column_spacing">7</property>
<property name="row_spacing">5</property> <property name="row_spacing">5</property>
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
<packing> <packing>
<property name="left_attach">1</property> <property name="left_attach">1</property>
<property name="right_attach">2</property> <property name="right_attach">2</property>
<property name="y_options"></property> <property name="y_options"/>
</packing> </packing>
</child> </child>
<child> <child>
...@@ -59,9 +59,28 @@ ...@@ -59,9 +59,28 @@
<property name="right_attach">2</property> <property name="right_attach">2</property>
<property name="top_attach">1</property> <property name="top_attach">1</property>
<property name="bottom_attach">2</property> <property name="bottom_attach">2</property>
<property name="y_options"></property> <property name="y_options"/>
</packing> </packing>
</child> </child>
<child>
<object class="GtkCheckButton" id="flash_cb">
<property name="label" translatable="yes">Do not flash the LED, only switch it on.</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
<property name="y_options"/>
</packing>
</child>
<child>
<placeholder/>
</child>
</object> </object>
</child> </child>
</object> </object>
......
...@@ -13,11 +13,12 @@ from plugins.gui import GajimPluginConfigDialog ...@@ -13,11 +13,12 @@ from plugins.gui import GajimPluginConfigDialog
class FlashingKeyboard(GajimPlugin): class FlashingKeyboard(GajimPlugin):
@log_calls('FlashingKeyboard') @log_calls('FlashingKeyboard')
def init(self): def init(self):
self.description = _('Flashing keyboard led when there are unread messages.')
self.config_dialog = FlashingKeyboardPluginConfigDialog(self) self.config_dialog = FlashingKeyboardPluginConfigDialog(self)
self.config_default_values = { self.config_default_values = {
'command1': ("xset led named 'Scroll Lock'", ''), 'command1': ("xset led named 'Scroll Lock'", ''),
'command2': ("xset -led named 'Scroll Lock'", '')} 'command2': ("xset -led named 'Scroll Lock'", ''),
'flash': (True, ''),
}
self.is_active = None self.is_active = None
self.timeout = 500 self.timeout = 500
...@@ -35,15 +36,22 @@ class FlashingKeyboard(GajimPlugin): ...@@ -35,15 +36,22 @@ class FlashingKeyboard(GajimPlugin):
if gajim.events.get_nb_systray_events(): if gajim.events.get_nb_systray_events():
if self.id_0: if self.id_0:
return return
self.id_0 = gobject.timeout_add(self.timeout, self.led_on) if self.config['flash']:
self.id_0 = gobject.timeout_add(self.timeout, self.led_on)
else:
self.led_on()
self.id_0 = True
else: else:
if self.id_0: if self.id_0:
gobject.source_remove(self.id_0) if self.config['flash']:
gobject.source_remove(self.id_0)
self.id_0 = None self.id_0 = None
self.led_off()
def led_on(self): def led_on(self):
subprocess.Popen('%s' % self.config['command1'], shell=True).wait() subprocess.Popen('%s' % self.config['command1'], shell=True).wait()
gobject.timeout_add(self.timeout_off, self.led_off) if self.config['flash']:
gobject.timeout_add(self.timeout_off, self.led_off)
return True return True
def led_off(self): def led_off(self):
...@@ -54,7 +62,11 @@ class FlashingKeyboard(GajimPlugin): ...@@ -54,7 +62,11 @@ class FlashingKeyboard(GajimPlugin):
gajim.events.event_added_subscribe(self.on_event_added) gajim.events.event_added_subscribe(self.on_event_added)
gajim.events.event_removed_subscribe(self.on_event_removed) gajim.events.event_removed_subscribe(self.on_event_removed)
if gajim.events.get_nb_systray_events(): if gajim.events.get_nb_systray_events():
self.id_0 = gobject.timeout_add(self.timeout, self.led_on) if self.config['flash']:
self.id_0 = gobject.timeout_add(self.timeout, self.led_on)
else:
self.led_on()
self.id_0 = True
@log_calls('FlashingKeyboard') @log_calls('FlashingKeyboard')
def deactivate(self): def deactivate(self):
...@@ -62,6 +74,7 @@ class FlashingKeyboard(GajimPlugin): ...@@ -62,6 +74,7 @@ class FlashingKeyboard(GajimPlugin):
gajim.events.event_removed_unsubscribe(self.on_event_removed) gajim.events.event_removed_unsubscribe(self.on_event_removed)
if self.id_0: if self.id_0:
gobject.source_remove(self.id_0) gobject.source_remove(self.id_0)
self.led_off()
class FlashingKeyboardPluginConfigDialog(GajimPluginConfigDialog): class FlashingKeyboardPluginConfigDialog(GajimPluginConfigDialog):
...@@ -81,14 +94,19 @@ class FlashingKeyboardPluginConfigDialog(GajimPluginConfigDialog): ...@@ -81,14 +94,19 @@ class FlashingKeyboardPluginConfigDialog(GajimPluginConfigDialog):
self.isactive = self.plugin.active self.isactive = self.plugin.active
if self.plugin.active: if self.plugin.active:
gajim.plugin_manager.deactivate_plugin(self.plugin) gajim.plugin_manager.deactivate_plugin(self.plugin)
for name in self.plugin.config_default_values: for name in ('command1', 'command2'):
widget = self.xml.get_object(name) widget = self.xml.get_object(name)
widget.set_text(self.plugin.config[name]) widget.set_text(self.plugin.config[name])
widget = self.xml.get_object('flash_cb')
widget.set_active(not self.plugin.config['flash'])
def on_close_button_clicked(self, widget): def on_close_button_clicked(self, widget):
widget = self.xml.get_object('command1') widget = self.xml.get_object('command1')
self.plugin.config['command1'] = widget.get_text() self.plugin.config['command1'] = widget.get_text()
widget = self.xml.get_object('command2') widget = self.xml.get_object('command2')
self.plugin.config['command2'] = widget.get_text() self.plugin.config['command2'] = widget.get_text()
widget = self.xml.get_object('flash_cb')
self.plugin.config['flash'] = not widget.get_active()
if self.isactive: if self.isactive:
gajim.plugin_manager.activate_plugin(self.plugin) gajim.plugin_manager.activate_plugin(self.plugin)
self.hide()
[info] [info]
name: Flashing Keyboard name: Flashing Keyboard
short_name: flashing_keyboard short_name: flashing_keyboard
version: 0.1.3 version: 0.1.5
description: Flashing keyboard led when there are unread messages. description: Flashing keyboard led when there are unread messages.
authors: Denis Fomin <fominde@gmail.com> authors: Denis Fomin <fominde@gmail.com>
homepage: http://trac-plugins.gajim.org/wiki/FlashingKeyboardPlugin homepage: http://trac-plugins.gajim.org/wiki/FlashingKeyboardPlugin
max_gajim_version: 0.16.9
[info] [info]
name: GNOME SessionManager name: GNOME SessionManager
short_name: gnome_session_manager short_name: gnome_session_manager
version: 0.1.2 version: 0.1.3
description: Set and react on GNOME Session presence settings description: Set and react on GNOME Session presence settings
authors: Philippe Normand <phil@base-art.net> authors: Philippe Normand <phil@base-art.net>
homepage: http://base-art.net homepage: http://base-art.net
max_gajim_version: 0.16.9
...@@ -32,7 +32,6 @@ class GnomeSessionManagerPlugin(GajimPlugin): ...@@ -32,7 +32,6 @@ class GnomeSessionManagerPlugin(GajimPlugin):
@log_calls('GnomeSessionManagerPlugin') @log_calls('GnomeSessionManagerPlugin')
def init(self): def init(self):
self.description = _('Set and react on GNOME Session presence settings')
self.config_dialog = None self.config_dialog = None
self.events_handlers = {} self.events_handlers = {}
......
[info] [info]
name: Google Translation name: Google Translation
short_name: google_translation short_name: google_translation
version: 0.3.1 version: 0.3.2
description: Translates (currently only incoming) messages using Google Translate. description: Translates (currently only incoming) messages using Google Translate.
authors = Mateusz Biliński <mateusz@bilinski.it> authors = Mateusz Biliński <mateusz@bilinski.it>
mrDoctorWho <mrdoctorwho@gmail.com> mrDoctorWho <mrdoctorwho@gmail.com>
homepage = http://trac-plugins.gajim.org/wiki/GoogleTranslationPlugin homepage = http://trac-plugins.gajim.org/wiki/GoogleTranslationPlugin
max_gajim_version: 0.16.9
...@@ -109,8 +109,6 @@ class GoogleTranslationPlugin(GajimPlugin): ...@@ -109,8 +109,6 @@ class GoogleTranslationPlugin(GajimPlugin):
@log_calls('GoogleTranslationPlugin') @log_calls('GoogleTranslationPlugin')
def init(self): def init(self):
self.description = _('Translates (currently only incoming)'
'messages using Google Translate.')
self.config_dialog = None self.config_dialog = None
self.config_default_values = { self.config_default_values = {
......
[info] [info]
name: Off-The-Record Encryption name: Off-The-Record Encryption
short_name: gotr short_name: gotr
version: 1.7.1 version: 1.9.6
description: Provide OTR encryption description: Provide OTR encryption. Read <a href="https://github.com/python-otr/gajim-otr/wiki">https://github.com/python-otr/gajim-otr/wiki</a> before use.
authors: Kjell Braden <afflux.gajim@pentabarf.de> authors: Kjell Braden <afflux.gajim@pentabarf.de>
homepage: http://gajim-otr.pentabarf.de homepage: http://gajim-otr.pentabarf.de
max_gajim_version: 0.16.9
...@@ -46,6 +46,8 @@ DEFAULTFLAGS = { ...@@ -46,6 +46,8 @@ DEFAULTFLAGS = {
MMS = 1024 MMS = 1024
PROTOCOL = 'xmpp' PROTOCOL = 'xmpp'
MINVERSION_OUTGOING_MSG_STAZA = "0.16.4"
enc_tip = 'A private chat session <i>is established</i> to this contact ' \ enc_tip = 'A private chat session <i>is established</i> to this contact ' \
'with this fingerprint' 'with this fingerprint'
unused_tip = 'A private chat session is established to this contact using ' \ unused_tip = 'A private chat session is established to this contact using ' \
...@@ -53,14 +55,19 @@ unused_tip = 'A private chat session is established to this contact using ' \ ...@@ -53,14 +55,19 @@ unused_tip = 'A private chat session is established to this contact using ' \
ended_tip = 'The private chat session to this contact has <i>ended</i>' ended_tip = 'The private chat session to this contact has <i>ended</i>'
inactive_tip = 'Communication to this contact is currently ' \ inactive_tip = 'Communication to this contact is currently ' \
'<i>unencrypted</i>' '<i>unencrypted</i>'
msg_not_send = _('Your message was not send. Either end '
'your private conversation, or restart it')
import logging import logging
import nbxmpp
import os import os
import pickle import pickle
import time import time
import sys import sys
from pprint import pformat
from distutils.version import LooseVersion
import common.xmpp
from common import gajim from common import gajim
from common import ged from common import ged
from common.connection_handlers_events import MessageOutgoingEvent from common.connection_handlers_events import MessageOutgoingEvent
...@@ -75,6 +82,7 @@ sys.path.insert(0, os.path.dirname(ui.__file__)) ...@@ -75,6 +82,7 @@ sys.path.insert(0, os.path.dirname(ui.__file__))
from HTMLParser import HTMLParser from HTMLParser import HTMLParser
from htmlentitydefs import name2codepoint from htmlentitydefs import name2codepoint
name2codepoint['apos'] = 0x0027
HAS_CRYPTO = True HAS_CRYPTO = True
try: try:
...@@ -85,6 +93,8 @@ try: ...@@ -85,6 +93,8 @@ try:
except ImportError: except ImportError:
HAS_CRYPTO = False HAS_CRYPTO = False
import nbxmpp
HAS_POTR = True HAS_POTR = True
try: try:
import potr import potr
...@@ -101,7 +111,7 @@ try: ...@@ -101,7 +111,7 @@ try:
potrrootlog.addHandler(h) potrrootlog.addHandler(h)
def get_jid_from_fjid(fjid): def get_jid_from_fjid(fjid):
return gajim.get_room_and_nick_from_fjid(fjid)[0] return gajim.get_room_and_nick_from_fjid(str(fjid))[0]
class GajimContext(potr.context.Context): class GajimContext(potr.context.Context):
# self.peer is fjid # self.peer is fjid
...@@ -119,11 +129,12 @@ try: ...@@ -119,11 +129,12 @@ try:
msg = unicode(msg) msg = unicode(msg)
account = self.user.accountname account = self.user.accountname
stanza = common.xmpp.Message(to=self.peer, body=msg, typ='chat') stanza = nbxmpp.Message(to=self.peer, body=msg, typ='chat')
if appdata is not None: if appdata is not None:
session = appdata.get('session', None) session = appdata.get('session', None)
if session is not None: if session is not None:
stanza.setThread(session.thread_id) stanza.setThread(session.thread_id)
add_message_processing_hints(stanza)
gajim.connections[account].connection.send(stanza, now=True) gajim.connections[account].connection.send(stanza, now=True)
def setState(self, newstate): def setState(self, newstate):
...@@ -244,11 +255,11 @@ except ImportError: ...@@ -244,11 +255,11 @@ except ImportError:
def otr_dialog_destroy(widget, *args, **kwargs): def otr_dialog_destroy(widget, *args, **kwargs):
widget.destroy() widget.destroy()
class OtrPlugin(GajimPlugin): class OtrPlugin(GajimPlugin):
otr = None otr = None
def init(self): def init(self):
self.description = _('See http://www.cypherpunks.ca/otr/')
self.us = {} self.us = {}
...@@ -268,8 +279,14 @@ class OtrPlugin(GajimPlugin): ...@@ -268,8 +279,14 @@ class OtrPlugin(GajimPlugin):
self.events_handlers = {} self.events_handlers = {}
self.events_handlers['message-received'] = (ged.PRECORE, self.events_handlers['message-received'] = (ged.PRECORE,
self.handle_incoming_msg) self.handle_incoming_msg)
self.events_handlers['message-outgoing'] = (ged.OUT_PRECORE, self.events_handlers['before-change-show'] = (ged.PRECORE,
self.handle_outgoing_msg) self.handle_change_show)
if LooseVersion(gajim.config.get('version')) < LooseVersion(MINVERSION_OUTGOING_MSG_STAZA):
self.events_handlers['message-outgoing'] = (ged.OUT_PRECORE,
self.handle_outgoing_msg)
else:
self.events_handlers['stanza-message-outgoing'] = (ged.OUT_PRECORE,
self.handle_outgoing_msg_stanza)
self.gui_extension_points = { self.gui_extension_points = {
'chat_control' : (self.cc_connect, self.cc_disconnect) 'chat_control' : (self.cc_connect, self.cc_disconnect)
...@@ -471,13 +488,14 @@ class OtrPlugin(GajimPlugin): ...@@ -471,13 +488,14 @@ class OtrPlugin(GajimPlugin):
if ctrl: if ctrl:
ctrl.print_conversation_line(u'[OTR] %s' % msg, 'status', ctrl.print_conversation_line(u'[OTR] %s' % msg, 'status',
'', None) '', None)
id = gajim.logger.write('chat_msg_recv', fjid, if gajim.config.should_log(account, jid):
message=u'[OTR: %s]' % msg, tim=tim) id = gajim.logger.write('chat_msg_recv', fjid,
# gajim.logger.write() only marks a message as unread (and so message=u'[OTR: %s]' % msg, tim=tim)
# only returns an id) when fjid is a real contact (NOT if it's a # gajim.logger.write() only marks a message as unread (and so
# GC private chat) # only returns an id) when fjid is a real contact (NOT if it's a
if id: # GC private chat)
gajim.logger.set_read_messages([id]) if id:
gajim.logger.set_read_messages([id])
else: else:
session = gajim.connections[account].get_or_create_session(fjid, session = gajim.connections[account].get_or_create_session(fjid,
thread_id) thread_id)
...@@ -491,10 +509,11 @@ class OtrPlugin(GajimPlugin): ...@@ -491,10 +509,11 @@ class OtrPlugin(GajimPlugin):
session.control = ctrl session.control = ctrl
session.control.set_session(session) session.control.set_session(session)
msg_id = gajim.logger.write('chat_msg_recv', fjid, if gajim.config.should_log(account, jid):
message=u'[OTR: %s]' % msg, tim=tim) msg_id = gajim.logger.write('chat_msg_recv', fjid,
session.roster_message(jid, msg, tim=tim, msg_id=msg_id, message=u'[OTR: %s]' % msg, tim=tim)
msg_type='chat', resource=resource) session.roster_message(jid, msg, tim=tim, msg_id=msg_id,
msg_type='chat', resource=resource)
@classmethod @classmethod
def update_otr(cls, user, acc, print_status=False): def update_otr(cls, user, acc, print_status=False):
...@@ -517,18 +536,30 @@ class OtrPlugin(GajimPlugin): ...@@ -517,18 +536,30 @@ class OtrPlugin(GajimPlugin):
if ctrl and ctrl.TYPE_ID == TYPE_CHAT: if ctrl and ctrl.TYPE_ID == TYPE_CHAT:
return ctrl return ctrl
def handle_change_show(self, event):
account = event.conn.name
if event.show == 'offline':
for us in self.us.itervalues():
for fjid, ctx in us.ctxs.iteritems():
if ctx.state == potr.context.STATE_ENCRYPTED:
self.us[account].getContext(fjid).disconnect()
return PASS
def handle_incoming_msg(self, event): def handle_incoming_msg(self, event):
ctx = None ctx = None
account = event.conn.name account = event.conn.name
accjid = gajim.get_jid_from_account(account) accjid = gajim.get_jid_from_account(account)
if event.encrypted is not False or not event.stanza.getTag('body') \ if event.encrypted is not False or not event.stanza.getTag('body') \
or not isinstance(event.stanza.getBody(), unicode): or not isinstance(event.stanza.getBody(), unicode) \
or event.mtype != 'chat':
return PASS return PASS
try: try:
ctx = self.us[account].getContext(event.fjid) ctx = self.us[account].getContext(event.fjid)
msgtxt, tlvs = ctx.receiveMessage(event.msgtxt, msgtxt, tlvs = ctx.receiveMessage(event.msgtxt.encode('utf8'),
appdata={'session':event.session}) appdata={'session':event.session})
except potr.context.NotOTRMessage, e: except potr.context.NotOTRMessage, e:
# received message was not OTR - pass it on # received message was not OTR - pass it on
...@@ -574,52 +605,94 @@ class OtrPlugin(GajimPlugin): ...@@ -574,52 +605,94 @@ class OtrPlugin(GajimPlugin):
ctx.smpWindow.handle_tlv(tlvs) ctx.smpWindow.handle_tlv(tlvs)
stripper = HTMLStripper() stripper = HTMLStripper()
stripper.feed(unicode(msgtxt or '')) stripper.feed((msgtxt or '').decode('utf8'))
event.msgtxt = stripper.stripped_data event.msgtxt = stripper.stripped_data
event.stanza.setBody(event.msgtxt) event.stanza.setBody(event.msgtxt)
event.stanza.setXHTML(msgtxt) event.stanza.setXHTML((msgtxt or '').decode('utf8'))
return PASS return PASS
def handle_outgoing_msg(self, event): def handle_outgoing_msg_stanza(self, event):
if hasattr(event, 'otrmessage'): xhtml = event.msg_iq.getXHTML()
return PASS body = event.msg_iq.getBody()
encrypted = False
xep_200 = bool(event.session) and event.session.enable_encryption
if xep_200 or not event.message:
return PASS
if event.session:
fjid = event.session.get_to()
else:
fjid = event.jid
if event.resource:
fjid += '/' + event.resource
message = event.xhtml or escape(event.message)
try: try:
newmsg = self.us[event.account].getContext(fjid).sendMessage( if xhtml:
potr.context.FRAGMENT_SEND_ALL_BUT_LAST, message, xhtml = xhtml.encode('utf8')
appdata={'session':event.session}) encrypted_msg = self.us[event.conn.name].\
getContext(event.msg_iq.getTo()).\
sendMessage(potr.context.FRAGMENT_SEND_ALL_BUT_LAST, xhtml)
if xhtml != encrypted_msg.strip(): #.strip() because sendMessage() adds whitespaces
encrypted = True
event.msg_iq.delChild('html')
event.msg_iq.setBody(encrypted_msg)
elif body:
body = escape(body).encode('utf8')
encrypted_msg = self.us[event.conn.name].\
getContext(event.msg_iq.getTo()).\
sendMessage(potr.context.FRAGMENT_SEND_ALL_BUT_LAST, body)
if body != encrypted_msg.strip():
encrypted = True
event.msg_iq.setBody(encrypted_msg)
except potr.context.NotEncryptedError, e: except potr.context.NotEncryptedError, e:
if e.args[0] == potr.context.EXC_FINISHED: if e.args[0] == potr.context.EXC_FINISHED:
self.gajim_log(_('Your message was not send. Either end ' self.gajim_log(msg_not_send, event.conn.name, event.msg_iq.getTo())
'your private conversation, or restart it'), event.account,
fjid)
return IGNORE return IGNORE
else: else:
raise e raise e
if event.xhtml: # if we had html before, replace with new content if encrypted:
event.xhtml = newmsg add_message_processing_hints(event.msg_iq)
stripper = HTMLStripper()
stripper.feed(unicode(newmsg or ''))
event.message = stripper.stripped_data
return PASS return PASS
def handle_outgoing_msg(self, event):
try:
if hasattr(event, 'otrmessage'):
return PASS
xep_200 = bool(event.session) and event.session.enable_encryption
potrrootlog.debug('got event {0} xep_200={1}'.format(pformat(event.__dict__), xep_200))
if xep_200 or not event.message:
return PASS
if event.session:
fjid = event.session.get_to()
else:
fjid = event.jid
if event.resource:
fjid += '/' + event.resource
message = event.xhtml or escape(event.message)
message = message.encode('utf8')
potrrootlog.debug('processing message={0!r} from fjid={1!r}'.format(message, fjid))
try:
newmsg = self.us[event.account].getContext(fjid).sendMessage(
potr.context.FRAGMENT_SEND_ALL_BUT_LAST, message,
appdata={'session':event.session})
potrrootlog.debug('processed message={0!r}'.format(newmsg))
except potr.context.NotEncryptedError, e:
if e.args[0] == potr.context.EXC_FINISHED:
self.gajim_log(msg_not_send, event.account, fjid)
return IGNORE
else:
raise e
if event.xhtml: # if we had html before, replace with new content
event.xhtml = newmsg
stripper = HTMLStripper()
stripper.feed((newmsg or '').decode('utf8'))
event.message = stripper.stripped_data
return PASS
except:
potrrootlog.exception('exception in outgoing message handler, message (hopefully) discarded')
return IGNORE
class HTMLStripper(HTMLParser): class HTMLStripper(HTMLParser):
def reset(self): def reset(self):
...@@ -634,8 +707,12 @@ class HTMLStripper(HTMLParser): ...@@ -634,8 +707,12 @@ class HTMLStripper(HTMLParser):
self.stripped_data += '\n' self.stripped_data += '\n'
def handle_entityref(self, name): def handle_entityref(self, name):
c = unichr(name2codepoint[name]) try:
c = unichr(name2codepoint[name])
except KeyError:
c = '&{};'.format(name)
self.stripped_data += c self.stripped_data += c
def handle_charref(self, name): def handle_charref(self, name):
if name.startswith('x'): if name.startswith('x'):
c = unichr(int(name[1:], 16)) c = unichr(int(name[1:], 16))
...@@ -661,5 +738,7 @@ def escape(s): ...@@ -661,5 +738,7 @@ def escape(s):
s = s.replace("\n", "<br/>") s = s.replace("\n", "<br/>")
return s return s
## TODO: def add_message_processing_hints(stanza):
## - disconnect ctxs on disconnect stanza.addChild(name='private', namespace=nbxmpp.NS_CARBONS)
stanza.addChild(name='no-permanent-store', namespace=nbxmpp.NS_MSG_HINTS)
stanza.addChild(name='no-copy', namespace=nbxmpp.NS_MSG_HINTS)
...@@ -24,4 +24,4 @@ from potr.utils import human_hash ...@@ -24,4 +24,4 @@ from potr.utils import human_hash
''' version is: (major, minor, patch, sub) with sub being one of 'alpha', ''' version is: (major, minor, patch, sub) with sub being one of 'alpha',
'beta', 'final' ''' 'beta', 'final' '''
VERSION = (1, 0, 0, 'beta5') VERSION = (1, 0, 0, 'beta7')
...@@ -26,8 +26,8 @@ from potr.utils import human_hash, bytes_to_long, unpack, pack_mpi ...@@ -26,8 +26,8 @@ from potr.utils import human_hash, bytes_to_long, unpack, pack_mpi
DEFAULT_KEYTYPE = 0x0000 DEFAULT_KEYTYPE = 0x0000
pkTypes = {} pkTypes = {}
def registerkeytype(cls): def registerkeytype(cls):
if not hasattr(cls, 'parsePayload'): if cls.keyType is None:
raise TypeError('registered key types need parsePayload()') raise TypeError('registered key class needs a type value')
pkTypes[cls.keyType] = cls pkTypes[cls.keyType] = cls
return cls return cls
...@@ -35,12 +35,16 @@ def generateDefaultKey(): ...@@ -35,12 +35,16 @@ def generateDefaultKey():
return pkTypes[DEFAULT_KEYTYPE].generate() return pkTypes[DEFAULT_KEYTYPE].generate()
class PK(object): class PK(object):
__slots__ = [] keyType = None
@classmethod @classmethod
def generate(cls): def generate(cls):
raise NotImplementedError raise NotImplementedError
@classmethod
def parsePayload(cls, data, private=False):
raise NotImplementedError
def sign(self, data): def sign(self, data):
raise NotImplementedError raise NotImplementedError
def verify(self, data): def verify(self, data):
...@@ -80,13 +84,13 @@ class PK(object): ...@@ -80,13 +84,13 @@ class PK(object):
@classmethod @classmethod
def parsePrivateKey(cls, data): def parsePrivateKey(cls, data):
implCls, data = cls.getImplementation(data) implCls, data = cls.getImplementation(data)
logging.debug('Got privkey of type %r' % implCls) logging.debug('Got privkey of type %r', implCls)
return implCls.parsePayload(data, private=True) return implCls.parsePayload(data, private=True)
@classmethod @classmethod
def parsePublicKey(cls, data): def parsePublicKey(cls, data):
implCls, data = cls.getImplementation(data) implCls, data = cls.getImplementation(data)
logging.debug('Got pubkey of type %r' % implCls) logging.debug('Got pubkey of type %r', implCls)
return implCls.parsePayload(data) return implCls.parsePayload(data)
def __str__(self): def __str__(self):
......
...@@ -15,18 +15,16 @@ ...@@ -15,18 +15,16 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with this library. If not, see <http://www.gnu.org/licenses/>. # along with this library. If not, see <http://www.gnu.org/licenses/>.
from Crypto import Cipher, Random from Crypto import Cipher
from Crypto.Hash import SHA256 as _SHA256 from Crypto.Hash import SHA256 as _SHA256
from Crypto.Hash import SHA as _SHA1 from Crypto.Hash import SHA as _SHA1
from Crypto.Hash import HMAC as _HMAC from Crypto.Hash import HMAC as _HMAC
from Crypto.PublicKey import DSA from Crypto.PublicKey import DSA
from Crypto.Random import random
from numbers import Number from numbers import Number
from potr.compatcrypto import common from potr.compatcrypto import common
from potr.utils import pack_mpi, read_mpi, bytes_to_long, long_to_bytes from potr.utils import read_mpi, bytes_to_long, long_to_bytes
# XXX atfork?
RNG = Random.new()
def SHA256(data): def SHA256(data):
return _SHA256.new(data).digest() return _SHA256.new(data).digest()
...@@ -54,7 +52,6 @@ def AESCTR(key, counter=0): ...@@ -54,7 +52,6 @@ def AESCTR(key, counter=0):
return Cipher.AES.new(key, Cipher.AES.MODE_CTR, counter=counter) return Cipher.AES.new(key, Cipher.AES.MODE_CTR, counter=counter)
class Counter(object): class Counter(object):
__slots__ = ['prefix', 'val']
def __init__(self, prefix): def __init__(self, prefix):
self.prefix = prefix self.prefix = prefix
self.val = 0 self.val = 0
...@@ -72,17 +69,15 @@ class Counter(object): ...@@ -72,17 +69,15 @@ class Counter(object):
return '<Counter(p={p!r},v={v!r})>'.format(p=self.prefix, v=self.val) return '<Counter(p={p!r},v={v!r})>'.format(p=self.prefix, v=self.val)
def byteprefix(self): def byteprefix(self):
return long_to_bytes(self.prefix).rjust(8, b'\0') return long_to_bytes(self.prefix, 8)
def __call__(self): def __call__(self):
val = long_to_bytes(self.val) bytesuffix = long_to_bytes(self.val, 8)
prefix = long_to_bytes(self.prefix)
self.val += 1 self.val += 1
return self.byteprefix() + val.rjust(8, b'\0') return self.byteprefix() + bytesuffix
@common.registerkeytype @common.registerkeytype
class DSAKey(common.PK): class DSAKey(common.PK):
__slots__ = ['priv', 'pub']
keyType = 0x0000 keyType = 0x0000
def __init__(self, key=None, private=False): def __init__(self, key=None, private=False):
...@@ -111,10 +106,10 @@ class DSAKey(common.PK): ...@@ -111,10 +106,10 @@ class DSAKey(common.PK):
return SHA1(self.getSerializedPublicPayload()) return SHA1(self.getSerializedPublicPayload())
def sign(self, data): def sign(self, data):
# 2 <= K <= q = 160bit = 20 byte # 2 <= K <= q
K = bytes_to_long(RNG.read(19)) + 2 K = random.randrange(2, self.priv.q)
r, s = self.priv.sign(data, K) r, s = self.priv.sign(data, K)
return long_to_bytes(r) + long_to_bytes(s) return long_to_bytes(r, 20) + long_to_bytes(s, 20)
def verify(self, data, sig): def verify(self, data, sig):
r, s = bytes_to_long(sig[:20]), bytes_to_long(sig[20:]) r, s = bytes_to_long(sig[:20]), bytes_to_long(sig[20:])
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
try: try:
basestring = basestring type(basestring)
except NameError: except NameError:
# all strings are unicode in python3k # all strings are unicode in python3k
basestring = str basestring = str
...@@ -27,7 +27,7 @@ except NameError: ...@@ -27,7 +27,7 @@ except NameError:
# callable is not available in python 3.0 and 3.1 # callable is not available in python 3.0 and 3.1
try: try:
callable = callable type(callable)
except NameError: except NameError:
from collections import Callable from collections import Callable
def callable(x): def callable(x):
...@@ -42,6 +42,7 @@ logger = logging.getLogger(__name__) ...@@ -42,6 +42,7 @@ logger = logging.getLogger(__name__)
from potr import crypt from potr import crypt
from potr import proto from potr import proto
from potr import compatcrypto
from time import time from time import time
...@@ -62,16 +63,11 @@ OFFER_REJECTED = 2 ...@@ -62,16 +63,11 @@ OFFER_REJECTED = 2
OFFER_ACCEPTED = 3 OFFER_ACCEPTED = 3
class Context(object): class Context(object):
__slots__ = ['user', 'policy', 'crypto', 'tagOffer', 'lastSend',
'lastMessage', 'mayRetransmit', 'fragment', 'fragmentInfo', 'state',
'inject', 'trust', 'peer', 'trustName']
def __init__(self, account, peername): def __init__(self, account, peername):
self.user = account self.user = account
self.peer = peername self.peer = peername
self.policy = {} self.policy = {}
self.crypto = crypt.CryptEngine(self) self.crypto = crypt.CryptEngine(self)
self.discardFragment()
self.tagOffer = OFFER_NOTSENT self.tagOffer = OFFER_NOTSENT
self.mayRetransmit = 0 self.mayRetransmit = 0
self.lastSend = 0 self.lastSend = 0
...@@ -79,6 +75,10 @@ class Context(object): ...@@ -79,6 +75,10 @@ class Context(object):
self.state = STATE_PLAINTEXT self.state = STATE_PLAINTEXT
self.trustName = self.peer self.trustName = self.peer
self.fragmentInfo = None
self.fragment = None
self.discardFragment()
def getPolicy(self, key): def getPolicy(self, key):
raise NotImplementedError raise NotImplementedError
...@@ -100,13 +100,19 @@ class Context(object): ...@@ -100,13 +100,19 @@ class Context(object):
params = message.split(b',') params = message.split(b',')
if len(params) < 5 or not params[1].isdigit() or not params[2].isdigit(): if len(params) < 5 or not params[1].isdigit() or not params[2].isdigit():
logger.warning('invalid formed fragmented message: %r', params) logger.warning('invalid formed fragmented message: %r', params)
return None self.discardFragment()
return message
K, N = self.fragmentInfo K, N = self.fragmentInfo
try:
k = int(params[1])
n = int(params[2])
except ValueError:
logger.warning('invalid formed fragmented message: %r', params)
self.discardFragment()
return message
k = int(params[1])
n = int(params[2])
fragData = params[3] fragData = params[3]
logger.debug(params) logger.debug(params)
...@@ -114,17 +120,17 @@ class Context(object): ...@@ -114,17 +120,17 @@ class Context(object):
if n >= k == 1: if n >= k == 1:
# first fragment # first fragment
self.discardFragment() self.discardFragment()
self.fragmentInfo = (k,n) self.fragmentInfo = (k, n)
self.fragment.append(fragData) self.fragment.append(fragData)
elif N == n >= k > 1 and k == K+1: elif N == n >= k > 1 and k == K+1:
# accumulate # accumulate
self.fragmentInfo = (k,n) self.fragmentInfo = (k, n)
self.fragment.append(fragData) self.fragment.append(fragData)
else: else:
# bad, discard # bad, discard
self.discardFragment() self.discardFragment()
logger.warning('invalid fragmented message: %r', params) logger.warning('invalid fragmented message: %r', params)
return None return message
if n == k > 0: if n == k > 0:
assembled = b''.join(self.fragment) assembled = b''.join(self.fragment)
...@@ -210,7 +216,7 @@ class Context(object): ...@@ -210,7 +216,7 @@ class Context(object):
if self.state != STATE_ENCRYPTED: if self.state != STATE_ENCRYPTED:
self.sendInternal(proto.Error( self.sendInternal(proto.Error(
'You sent encrypted to {user}, who wasn\'t expecting it.' 'You sent encrypted to {user}, who wasn\'t expecting it.'
.format(user=self.user.name)), appdata=appdata) .format(user=self.user.name).encode('utf-8')), appdata=appdata)
if ignore: if ignore:
return IGN return IGN
raise NotEncryptedError(EXC_UNREADABLE_MESSAGE) raise NotEncryptedError(EXC_UNREADABLE_MESSAGE)
...@@ -263,12 +269,13 @@ class Context(object): ...@@ -263,12 +269,13 @@ class Context(object):
return msg return msg
def processOutgoingMessage(self, msg, flags, tlvs=[]): def processOutgoingMessage(self, msg, flags, tlvs=[]):
if isinstance(self.parse(msg), proto.Query): isQuery = self.parseExplicitQuery(msg) is not None
if isQuery:
return self.user.getDefaultQueryMessage(self.getPolicy) return self.user.getDefaultQueryMessage(self.getPolicy)
if self.state == STATE_PLAINTEXT: if self.state == STATE_PLAINTEXT:
if self.getPolicy('REQUIRE_ENCRYPTION'): if self.getPolicy('REQUIRE_ENCRYPTION'):
if not isinstance(self.parse(msg), proto.Query): if not isQuery:
self.lastMessage = msg self.lastMessage = msg
self.lastSend = time() self.lastSend = time()
self.mayRetransmit = 2 self.mayRetransmit = 2
...@@ -277,8 +284,12 @@ class Context(object): ...@@ -277,8 +284,12 @@ class Context(object):
return msg return msg
if self.getPolicy('SEND_TAG') and self.tagOffer != OFFER_REJECTED: if self.getPolicy('SEND_TAG') and self.tagOffer != OFFER_REJECTED:
self.tagOffer = OFFER_SENT self.tagOffer = OFFER_SENT
return proto.TaggedPlaintext(msg, self.getPolicy('ALLOW_V1'), versions = set()
self.getPolicy('ALLOW_V2')) if self.getPolicy('ALLOW_V1'):
versions.add(1)
if self.getPolicy('ALLOW_V2'):
versions.add(2)
return proto.TaggedPlaintext(msg, versions)
return msg return msg
if self.state == STATE_ENCRYPTED: if self.state == STATE_ENCRYPTED:
msg = self.crypto.createDataMessage(msg, flags, tlvs) msg = self.crypto.createDataMessage(msg, flags, tlvs)
...@@ -304,9 +315,9 @@ class Context(object): ...@@ -304,9 +315,9 @@ class Context(object):
def sendFragmented(self, msg, policy=FRAGMENT_SEND_ALL, appdata=None): def sendFragmented(self, msg, policy=FRAGMENT_SEND_ALL, appdata=None):
mms = self.maxMessageSize(appdata) mms = self.maxMessageSize(appdata)
msgLen = len(msg) msgLen = len(msg)
if mms != 0 and len(msg) > mms: if mms != 0 and msgLen > mms:
fms = mms - 19 fms = mms - 19
fragments = [ msg[i:i+fms] for i in range(0, len(msg), fms) ] fragments = [ msg[i:i+fms] for i in range(0, msgLen, fms) ]
fc = len(fragments) fc = len(fragments)
...@@ -375,9 +386,9 @@ class Context(object): ...@@ -375,9 +386,9 @@ class Context(object):
self.crypto.smpSecret(secret, question=question, appdata=appdata) self.crypto.smpSecret(secret, question=question, appdata=appdata)
def handleQuery(self, message, appdata=None): def handleQuery(self, message, appdata=None):
if message.v2 and self.getPolicy('ALLOW_V2'): if 2 in message.versions and self.getPolicy('ALLOW_V2'):
self.authStartV2(appdata=appdata) self.authStartV2(appdata=appdata)
elif message.v1 and self.getPolicy('ALLOW_V1'): elif 1 in message.versions and self.getPolicy('ALLOW_V1'):
self.authStartV1(appdata=appdata) self.authStartV1(appdata=appdata)
def authStartV1(self, appdata=None): def authStartV1(self, appdata=None):
...@@ -386,7 +397,33 @@ class Context(object): ...@@ -386,7 +397,33 @@ class Context(object):
def authStartV2(self, appdata=None): def authStartV2(self, appdata=None):
self.crypto.startAKE(appdata=appdata) self.crypto.startAKE(appdata=appdata)
def parse(self, message): def parseExplicitQuery(self, message):
otrTagPos = message.find(proto.OTRTAG)
if otrTagPos == -1:
return None
indexBase = otrTagPos + len(proto.OTRTAG)
if len(message) <= indexBase:
return None
compare = message[indexBase]
hasq = compare == b'?'[0]
hasv = compare == b'v'[0]
if not hasq and not hasv:
return None
hasv |= len(message) > indexBase+1 and message[indexBase+1] == b'v'[0]
if hasv:
end = message.find(b'?', indexBase+1)
else:
end = indexBase+1
return message[indexBase:end]
def parse(self, message, nofragment=False):
otrTagPos = message.find(proto.OTRTAG) otrTagPos = message.find(proto.OTRTAG)
if otrTagPos == -1: if otrTagPos == -1:
if proto.MESSAGE_TAG_BASE in message: if proto.MESSAGE_TAG_BASE in message:
...@@ -395,38 +432,40 @@ class Context(object): ...@@ -395,38 +432,40 @@ class Context(object):
return message return message
indexBase = otrTagPos + len(proto.OTRTAG) indexBase = otrTagPos + len(proto.OTRTAG)
if len(message) <= indexBase:
return message
compare = message[indexBase] compare = message[indexBase]
if compare == b','[0]: if nofragment is False and compare == b','[0]:
message = self.fragmentAccumulate(message[indexBase:]) message = self.fragmentAccumulate(message[indexBase:])
if message is None: if message is None:
return None return None
else: else:
return self.parse(message) return self.parse(message, nofragment=True)
else: else:
self.discardFragment() self.discardFragment()
hasq = compare == b'?'[0] queryPayload = self.parseExplicitQuery(message)
hasv = compare == b'v'[0] if queryPayload is not None:
if hasq or hasv: return proto.Query.parse(queryPayload)
hasv |= len(message) > indexBase+1 and \
message[indexBase+1] == b'v'[0]
if hasv:
end = message.find(b'?', indexBase+1)
else:
end = indexBase+1
payload = message[indexBase:end]
return proto.Query.parse(payload)
if compare == b':'[0] and len(message) > indexBase + 4: if compare == b':'[0] and len(message) > indexBase + 4:
infoTag = base64.b64decode(message[indexBase+1:indexBase+5]) try:
classInfo = struct.unpack(b'!HB', infoTag) infoTag = base64.b64decode(message[indexBase+1:indexBase+5])
cls = proto.messageClasses.get(classInfo, None) classInfo = struct.unpack(b'!HB', infoTag)
if cls is None:
cls = proto.messageClasses.get(classInfo, None)
if cls is None:
return message
logger.debug('{user} got msg {typ!r}' \
.format(user=self.user.name, typ=cls))
return cls.parsePayload(message[indexBase+5:])
except (TypeError, struct.error):
logger.exception('could not parse OTR message %s', message)
return message return message
logger.debug('{user} got msg {typ!r}' \
.format(user=self.user.name, typ=cls))
return cls.parsePayload(message[indexBase+5:])
if message[indexBase:indexBase+7] == b' Error:': if message[indexBase:indexBase+7] == b' Error:':
return proto.Error(message[indexBase+7:]) return proto.Error(message[indexBase+7:])
...@@ -437,6 +476,22 @@ class Context(object): ...@@ -437,6 +476,22 @@ class Context(object):
"""Return the max message size for this context.""" """Return the max message size for this context."""
return self.user.maxMessageSize return self.user.maxMessageSize
def getExtraKey(self, extraKeyAppId=None, extraKeyAppData=None, appdata=None):
""" retrieves the generated extra symmetric key.
if extraKeyAppId is set, notifies the chat partner about intended
usage (additional application specific information can be supplied in
extraKeyAppData).
returns the 256 bit symmetric key """
if self.state != STATE_ENCRYPTED:
raise NotEncryptedError
if extraKeyAppId is not None:
tlvs = [proto.ExtraKeyTLV(extraKeyAppId, extraKeyAppData)]
self.sendInternal(b'', tlvs=tlvs, appdata=appdata)
return self.crypto.extraKey
class Account(object): class Account(object):
contextclass = Context contextclass = Context
def __init__(self, name, protocol, maxMessageSize, privkey=None): def __init__(self, name, protocol, maxMessageSize, privkey=None):
...@@ -447,10 +502,10 @@ class Account(object): ...@@ -447,10 +502,10 @@ class Account(object):
self.ctxs = {} self.ctxs = {}
self.trusts = {} self.trusts = {}
self.maxMessageSize = maxMessageSize self.maxMessageSize = maxMessageSize
self.defaultQuery = b'?OTRv{versions}?\n{accountname} has requested ' \ self.defaultQuery = '?OTRv{versions}?\n{accountname} has requested ' \
b'an Off-the-Record private conversation. However, you ' \ 'an Off-the-Record private conversation. However, you ' \
b'do not have a plugin to support that.\nSee '\ 'do not have a plugin to support that.\nSee '\
b'http://otr.cypherpunks.ca/ for more information.'; 'http://otr.cypherpunks.ca/ for more information.'
def __repr__(self): def __repr__(self):
return '<{cls}(name={name!r})>'.format(cls=self.__class__.__name__, return '<{cls}(name={name!r})>'.format(cls=self.__class__.__name__,
...@@ -461,7 +516,7 @@ class Account(object): ...@@ -461,7 +516,7 @@ class Account(object):
self.privkey = self.loadPrivkey() self.privkey = self.loadPrivkey()
if self.privkey is None: if self.privkey is None:
if autogen is True: if autogen is True:
self.privkey = crypt.generateDefaultKey() self.privkey = compatcrypto.generateDefaultKey()
self.savePrivkey() self.savePrivkey()
else: else:
raise LookupError raise LookupError
...@@ -484,8 +539,9 @@ class Account(object): ...@@ -484,8 +539,9 @@ class Account(object):
return self.ctxs[uid] return self.ctxs[uid]
def getDefaultQueryMessage(self, policy): def getDefaultQueryMessage(self, policy):
v = b'2' if policy('ALLOW_V2') else b'' v = '2' if policy('ALLOW_V2') else ''
return self.defaultQuery.format(accountname=self.name, versions=v) msg = self.defaultQuery.format(accountname=self.name, versions=v)
return msg.encode('ascii')
def setTrust(self, key, fingerprint, trustLevel): def setTrust(self, key, fingerprint, trustLevel):
if key not in self.trusts: if key not in self.trusts:
......