remove not ported plugins
@@ -1 +0,0 @@
|
|||||||
from fshare import FileSharePlugin
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<interface>
|
|
||||||
<requires lib="gtk+" version="2.16"/>
|
|
||||||
<!-- interface-naming-policy toplevel-contextual -->
|
|
||||||
<object class="GtkWindow" id="window1">
|
|
||||||
<child>
|
|
||||||
<object class="GtkVBox" id="hbox111">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkHBox" id="hbox1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="label1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="label" translatable="yes">Incoming folder:</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkEntry" id="dl_folder">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="invisible_char">●</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</interface>
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
import sqlite3
|
|
||||||
from common import app
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
class FilesharingDatabase:
|
|
||||||
def __init__(self, plugin):
|
|
||||||
self.plugin = plugin
|
|
||||||
path_l = os.path.split(plugin.config.FILE_PATH)
|
|
||||||
path = os.path.join(path_l[0], 'shared_files.db')
|
|
||||||
db_exist = os.path.exists(path)
|
|
||||||
self.conn = sqlite3.connect(path)
|
|
||||||
# Enable foreign keys contraints
|
|
||||||
self.conn.cursor().execute("pragma foreign_keys = on")
|
|
||||||
if not db_exist:
|
|
||||||
self.create_database()
|
|
||||||
|
|
||||||
# NOTE: Make sure we are getting and setting the requester without its
|
|
||||||
# resource
|
|
||||||
def create_database(self):
|
|
||||||
c = self.conn.cursor()
|
|
||||||
# Create tables
|
|
||||||
c.execute("CREATE TABLE permissions" +
|
|
||||||
"(fid integer REFERENCES files(fid) ON DELETE CASCADE, " +
|
|
||||||
"account text, requester text)")
|
|
||||||
c.execute("CREATE TABLE files" +
|
|
||||||
"(fid INTEGER PRIMARY KEY AUTOINCREMENT," +
|
|
||||||
" file_path text, relative_path text, hash_sha1 text," +
|
|
||||||
"size numeric, description text, mod_date text, is_dir boolean)")
|
|
||||||
# Save (commit) the changes
|
|
||||||
self.conn.commit()
|
|
||||||
c.close()
|
|
||||||
|
|
||||||
def get_toplevel_files(self, account, requester):
|
|
||||||
c = self.conn.cursor()
|
|
||||||
data = (account, requester)
|
|
||||||
c.execute("SELECT relative_path, hash_sha1, size, description, " +
|
|
||||||
"mod_date, is_dir FROM (files JOIN permissions ON" +
|
|
||||||
" files.fid=permissions.fid) WHERE account=? AND requester=?" +
|
|
||||||
" AND relative_path NOT LIKE '%/%'", data)
|
|
||||||
result = c.fetchall()
|
|
||||||
c.close()
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_files_from_dir(self, account, requester, dir_):
|
|
||||||
c = self.conn.cursor()
|
|
||||||
data = (account, requester, dir_ + '/%')
|
|
||||||
c.execute("SELECT relative_path, hash_sha1, size, description, " +
|
|
||||||
"mod_date, is_dir FROM (files JOIN permissions ON" +
|
|
||||||
" files.fid=permissions.fid) WHERE account=? AND requester=?" +
|
|
||||||
" AND relative_path LIKE ?", data)
|
|
||||||
result = c.fetchall()
|
|
||||||
c.close()
|
|
||||||
fresult = []
|
|
||||||
for r in result:
|
|
||||||
name = r[0][len(dir_) + 1:]
|
|
||||||
if '/' not in name:
|
|
||||||
fresult.append(r)
|
|
||||||
return fresult
|
|
||||||
|
|
||||||
def get_files(self, account, requester):
|
|
||||||
"""
|
|
||||||
>>> file_ = ('file_path', 'relative_path', 'hash', 999, 'description', \
|
|
||||||
'date', False)
|
|
||||||
>>> foo = add_file('account@gajim', 'requester@jabber', file_)
|
|
||||||
>>> result = get_files('account@gajim', 'requester@jabber')
|
|
||||||
>>> len(result)
|
|
||||||
1
|
|
||||||
>>> _delete_file(1)
|
|
||||||
"""
|
|
||||||
c = self.conn.cursor()
|
|
||||||
data = (account, requester)
|
|
||||||
c.execute("SELECT relative_path, hash_sha1, size, description, " +
|
|
||||||
"mod_date, is_dir FROM (files JOIN permissions ON" +
|
|
||||||
" files.fid=permissions.fid) WHERE account=? AND requester=?", data)
|
|
||||||
result = c.fetchall()
|
|
||||||
c.close()
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_file(self, account, requester, hash_, name):
|
|
||||||
c = self.conn.cursor()
|
|
||||||
if hash_:
|
|
||||||
data = (account, requester, hash_)
|
|
||||||
sql = "SELECT relative_path, hash_sha1, size, description, " + \
|
|
||||||
"mod_date, file_path FROM (files JOIN permissions ON" + \
|
|
||||||
" files.fid=permissions.fid) WHERE account=? AND requester=?" +\
|
|
||||||
" AND hash_sha1=?"
|
|
||||||
else:
|
|
||||||
data = (account, requester, name)
|
|
||||||
sql = "SELECT relative_path, hash_sha1, size, description, " + \
|
|
||||||
"mod_date, file_path FROM (files JOIN permissions ON" + \
|
|
||||||
" files.fid=permissions.fid) WHERE account=? AND requester=?" +\
|
|
||||||
" AND relative_path=?"
|
|
||||||
c.execute(sql, data)
|
|
||||||
result = c.fetchall()
|
|
||||||
c.close()
|
|
||||||
if result == []:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return result[0]
|
|
||||||
|
|
||||||
def get_files_name(self, account, requester):
|
|
||||||
result = self.get_files(account, requester)
|
|
||||||
flist = []
|
|
||||||
for r in result:
|
|
||||||
flist.append(r[0])
|
|
||||||
return flist
|
|
||||||
|
|
||||||
def add_file(self, account, requester, file_):
|
|
||||||
"""
|
|
||||||
>>> file_ = ('file_path', 'relative_path', 'hash', 999, 'description', \
|
|
||||||
'date', False)
|
|
||||||
>>> add_file('account@gajim', 'requester@jabber', file_)
|
|
||||||
1
|
|
||||||
>>> _delete_file(1)
|
|
||||||
"""
|
|
||||||
self._check_duplicate(account, requester, file_)
|
|
||||||
requester = app.get_jid_without_resource(requester)
|
|
||||||
c = self.conn.cursor()
|
|
||||||
c.execute("INSERT INTO files (file_path, " +
|
|
||||||
"relative_path, hash_sha1, size, description, mod_date, " +
|
|
||||||
" is_dir) VALUES (?,?,?,?,?,?,?)", file_)
|
|
||||||
fid = c.lastrowid
|
|
||||||
permission_data = (fid, account, requester)
|
|
||||||
c.execute("INSERT INTO permissions VALUES (?,?,?)", permission_data)
|
|
||||||
self.conn.commit()
|
|
||||||
c.close()
|
|
||||||
return fid
|
|
||||||
|
|
||||||
def _check_duplicate(self, account, requester, file_):
|
|
||||||
c = self.conn.cursor()
|
|
||||||
data = (account, requester, file_[1])
|
|
||||||
c.execute("SELECT * FROM (files JOIN permissions ON" +
|
|
||||||
" files.fid=permissions.fid) WHERE account=? AND requester=?" +
|
|
||||||
" AND relative_path=? ", data)
|
|
||||||
result = c.fetchall()
|
|
||||||
if file_[2] != '':
|
|
||||||
data = (account, requester, file_[2])
|
|
||||||
c.execute("SELECT * FROM (files JOIN permissions ON" +
|
|
||||||
" files.fid=permissions.fid) WHERE account=? AND requester=?" +
|
|
||||||
" AND hash_sha1=?)", data)
|
|
||||||
result.extend(c.fetchall())
|
|
||||||
if len(result) > 0:
|
|
||||||
raise Exception('Duplicated entry')
|
|
||||||
c.close()
|
|
||||||
|
|
||||||
def _delete_file(self, fid):
|
|
||||||
c = self.conn.cursor()
|
|
||||||
data = (fid, )
|
|
||||||
c.execute("DELETE FROM files WHERE fid=?", data)
|
|
||||||
self.conn.commit()
|
|
||||||
c.close()
|
|
||||||
|
|
||||||
def _delete_dir(self, dir_, account, requester):
|
|
||||||
c = self.conn.cursor()
|
|
||||||
data = (account, requester, dir_, dir_ + '/%')
|
|
||||||
sql = "DELETE FROM files WHERE fid IN " + \
|
|
||||||
" (SELECT files.fid FROM files, permissions WHERE" + \
|
|
||||||
" files.fid=permissions.fid AND account=?"+ \
|
|
||||||
" AND requester=? AND (relative_path=? OR relative_path LIKE ?))"
|
|
||||||
c.execute(sql, data)
|
|
||||||
self.conn.commit()
|
|
||||||
c.close()
|
|
||||||
|
|
||||||
def delete(self, account, requester, relative_path):
|
|
||||||
c = self.conn.cursor()
|
|
||||||
data = (account, requester, relative_path)
|
|
||||||
c.execute("SELECT files.fid, is_dir FROM (files JOIN permissions ON" +
|
|
||||||
" files.fid=permissions.fid) WHERE account=? AND requester=? AND " +
|
|
||||||
"relative_path=? ", data)
|
|
||||||
result = c.fetchone()
|
|
||||||
c.close()
|
|
||||||
if result[1] == 0:
|
|
||||||
self._delete_file(result[0])
|
|
||||||
else:
|
|
||||||
self._delete_dir(relative_path, account, requester)
|
|
||||||
|
|
||||||
def delete_all(self, account, requester):
|
|
||||||
c = self.conn.cursor()
|
|
||||||
data = (account, requester)
|
|
||||||
sql = "DELETE FROM files WHERE fid IN (SELECT fid FROM permissions" + \
|
|
||||||
" WHERE account=? AND requester=?)"
|
|
||||||
c.execute(sql, data)
|
|
||||||
self.conn.commit()
|
|
||||||
c.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
"""
|
|
||||||
DELETE DATABASE FILE BEFORE RUNNING TESTS
|
|
||||||
"""
|
|
||||||
import doctest
|
|
||||||
path = sys.path[0]
|
|
||||||
path = path + '/' + 'shared_files.db'
|
|
||||||
conn = sqlite3.connect(path)
|
|
||||||
# Enable foreign keys contraints
|
|
||||||
conn.cursor().execute("pragma foreign_keys = on")
|
|
||||||
FilesharingDatabase.create_database()
|
|
||||||
doctest.testmod()
|
|
||||||
@@ -1,399 +0,0 @@
|
|||||||
import gtk
|
|
||||||
import gobject
|
|
||||||
from common import gajim
|
|
||||||
from common import helpers
|
|
||||||
from common.file_props import FilesProp
|
|
||||||
import fshare
|
|
||||||
import os
|
|
||||||
import fshare_protocol
|
|
||||||
|
|
||||||
class FileShareWindow(gtk.Window):
|
|
||||||
|
|
||||||
def __init__(self, plugin):
|
|
||||||
self.plugin = plugin
|
|
||||||
gtk.Window.__init__(self)
|
|
||||||
self.set_title('File Share')
|
|
||||||
self.connect('delete_event', self.delete_event)
|
|
||||||
self.set_position(gtk.WIN_POS_CENTER)
|
|
||||||
self.set_default_size(400, 400)
|
|
||||||
# Children
|
|
||||||
self.notebook = gtk.Notebook()
|
|
||||||
# Browse page
|
|
||||||
self.bt_search = gtk.Button('Search')
|
|
||||||
self.bt_search.connect('clicked', self.on_bt_search_clicked)
|
|
||||||
self.entry_search = gtk.Entry(max=0)
|
|
||||||
self.entry_search.set_size_request(300, -1)
|
|
||||||
self.exp_advance = gtk.Expander('Advance')
|
|
||||||
self.ts_search = gtk.TreeStore(gobject.TYPE_STRING)
|
|
||||||
self.tv_search = gtk.TreeView(self.ts_search)
|
|
||||||
self.tv_search.connect('row-expanded', self.row_expanded)
|
|
||||||
self.tv_search.connect('button-press-event',
|
|
||||||
self.on_treeview_button_press_event)
|
|
||||||
self.browse_popup = gtk.Menu()
|
|
||||||
mi_download = gtk.MenuItem('Download')
|
|
||||||
mi_property = gtk.MenuItem('Property')
|
|
||||||
mi_download.show()
|
|
||||||
mi_property.show()
|
|
||||||
mi_download.connect('activate', self.on_download_clicked)
|
|
||||||
self.browse_popup.append(mi_download)
|
|
||||||
self.browse_popup.append(mi_property)
|
|
||||||
self.browse_sw = gtk.ScrolledWindow()
|
|
||||||
self.browse_sw.add(self.tv_search)
|
|
||||||
self.tvcolumn_browse = gtk.TreeViewColumn('')
|
|
||||||
self.tv_search.append_column(self.tvcolumn_browse)
|
|
||||||
self.cell_browse = gtk.CellRendererText()
|
|
||||||
self.tvcolumn_browse.pack_start(self.cell_browse, True)
|
|
||||||
self.tvcolumn_browse.add_attribute(self.cell_browse, 'text', 0)
|
|
||||||
self.lbl_browse = gtk.Label('Browse')
|
|
||||||
self.browse_hbox = gtk.HBox()
|
|
||||||
self.browse_hbox2 = gtk.HBox()
|
|
||||||
self.browse_vbox = gtk.VBox()
|
|
||||||
self.browse_hbox.pack_start(self.entry_search, True, True, 10)
|
|
||||||
self.browse_hbox.pack_start(self.bt_search, False, False, 10)
|
|
||||||
self.browse_vbox.pack_start(self.browse_hbox, False, False, 10)
|
|
||||||
self.browse_vbox.pack_start(self.exp_advance, False, False, 10)
|
|
||||||
self.browse_vbox.pack_start(self.browse_sw, True, True, 10)
|
|
||||||
self.notebook.append_page(self.browse_vbox, self.lbl_browse)
|
|
||||||
# file references for tv_search
|
|
||||||
self.browse_fref = {}
|
|
||||||
# contact references for tv_search
|
|
||||||
self.browse_jid = {}
|
|
||||||
# Information of the files inserted in the treeview: name, size, etc
|
|
||||||
self.brw_file_info = {}
|
|
||||||
# dummy row children so that we can get expanders
|
|
||||||
self.empty_row_child = {}
|
|
||||||
# Manage page
|
|
||||||
self.bt_add_file = gtk.Button('Add file')
|
|
||||||
self.bt_add_file.connect('clicked', self.add_file)
|
|
||||||
self.bt_add_dir = gtk.Button('Add directory')
|
|
||||||
self.bt_add_dir.connect('clicked', self.add_directory)
|
|
||||||
self.bt_remove = gtk.Button('Remove')
|
|
||||||
self.bt_remove.connect('clicked', self.remove_file_clicked)
|
|
||||||
self.bt_remove_all = gtk.Button('Remove all')
|
|
||||||
self.bt_remove_all.connect('clicked', self.remove_all_clicked)
|
|
||||||
self.ts_contacts = gtk.TreeStore(gobject.TYPE_STRING)
|
|
||||||
self.cbb_contacts = gtk.ComboBoxEntry(self.ts_contacts)
|
|
||||||
self.cbb_contacts.connect('changed', self.__check_combo_edit)
|
|
||||||
cbb_entry = self.cbb_contacts.child
|
|
||||||
self.cbb_completion = gtk.EntryCompletion()
|
|
||||||
cbb_entry.set_completion(self.cbb_completion)
|
|
||||||
self.cbb_completion.set_model(self.ts_contacts)
|
|
||||||
self.cbb_completion.set_text_column(0)
|
|
||||||
self.bt_stophash = gtk.Button('Calculating hash')
|
|
||||||
self.lbl_manage = gtk.Label('Manage Shared Files')
|
|
||||||
self.ts_files = gtk.TreeStore(gobject.TYPE_STRING)
|
|
||||||
self.tv_files = gtk.TreeView(self.ts_files)
|
|
||||||
self.treeSelection_files = self.tv_files.get_selection()
|
|
||||||
self.treeSelection_files.connect('changed', self.row_selected)
|
|
||||||
self.manage_sw = gtk.ScrolledWindow()
|
|
||||||
self.manage_sw.add(self.tv_files)
|
|
||||||
self.tvcolumn = gtk.TreeViewColumn('')
|
|
||||||
self.tv_files.append_column(self.tvcolumn)
|
|
||||||
self.cell = gtk.CellRendererText()
|
|
||||||
self.tvcolumn.pack_start(self.cell, True)
|
|
||||||
self.tvcolumn.add_attribute(self.cell, 'text', 0)
|
|
||||||
self.pb_filehash = gtk.ProgressBar()
|
|
||||||
self.manage_hbox = gtk.HBox()
|
|
||||||
self.manage_hbox2 = gtk.HBox()
|
|
||||||
self.manage_vbox = gtk.VBox()
|
|
||||||
self.manage_vbox2 = gtk.VBox()
|
|
||||||
self.manage_hbox.pack_start(self.bt_stophash , False, False, 10)
|
|
||||||
self.manage_hbox.pack_start(self.pb_filehash, True, True, 10)
|
|
||||||
self.manage_hbox.set_sensitive(False)
|
|
||||||
self.manage_vbox.pack_start(self.cbb_contacts, False, False, 10)
|
|
||||||
self.manage_vbox.pack_start(self.manage_hbox, False, False, 10)
|
|
||||||
self.manage_hbox2.pack_start(self.manage_sw, True, True, 10)
|
|
||||||
self.manage_hbox2.pack_start(self.manage_vbox2, False, False, 10)
|
|
||||||
self.manage_vbox2.pack_start(self.bt_add_file, False, False, 10)
|
|
||||||
self.manage_vbox2.pack_start(self.bt_add_dir, False, False, 10)
|
|
||||||
self.manage_vbox2.pack_start(self.bt_remove, False, False, 10)
|
|
||||||
self.manage_vbox2.pack_start(self.bt_remove_all, False, False, 10)
|
|
||||||
self.manage_vbox2.set_sensitive(False)
|
|
||||||
self.manage_vbox.pack_start(self.manage_hbox2, True, True, 10)
|
|
||||||
self.notebook.append_page(self.manage_vbox, self.lbl_manage)
|
|
||||||
# Preferences page
|
|
||||||
self.lbl_pref = gtk.Label('Preferences')
|
|
||||||
self.entry_dir_pref = gtk.Entry(max=0)
|
|
||||||
self.entry_dir_pref.set_text(self.plugin.config['incoming_dir'])
|
|
||||||
self.bt_sel_dir_pref = gtk.Button('Select dir', gtk.STOCK_OPEN)
|
|
||||||
self.bt_sel_dir_pref.connect('clicked', self.on_bt_sel_dir_pref_clicked)
|
|
||||||
self.frm_dir_pref = gtk.Frame('Incoming files directory')
|
|
||||||
self.frm_dir_pref.set_shadow_type(gtk.SHADOW_IN)
|
|
||||||
self.pref_hbox = gtk.HBox()
|
|
||||||
self.pref_vbox = gtk.VBox()
|
|
||||||
self.pref_hbox.pack_start(self.entry_dir_pref, True, True, 10)
|
|
||||||
self.pref_hbox.pack_start(self.bt_sel_dir_pref, False, False, 10)
|
|
||||||
self.frm_dir_pref.add(self.pref_hbox)
|
|
||||||
self.pref_vbox.pack_start(self.frm_dir_pref, False, False, 10)
|
|
||||||
self.notebook.append_page(self.pref_vbox , self.lbl_pref)
|
|
||||||
self.add(self.notebook)
|
|
||||||
self.show_all()
|
|
||||||
|
|
||||||
def set_account(self, account):
|
|
||||||
self.account = account
|
|
||||||
# connect window to protocol
|
|
||||||
pro = fshare.FileSharePlugin.prohandler[self.account]
|
|
||||||
pro.set_window(self)
|
|
||||||
|
|
||||||
def add_file(self, widget):
|
|
||||||
dialog = gtk.FileChooserDialog('Add file to be shared', self,
|
|
||||||
gtk.FILE_CHOOSER_ACTION_OPEN,
|
|
||||||
(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN,
|
|
||||||
gtk.RESPONSE_OK)
|
|
||||||
)
|
|
||||||
dialog.set_select_multiple(True)
|
|
||||||
response = dialog.run()
|
|
||||||
if response == gtk.RESPONSE_OK:
|
|
||||||
file_list = dialog.get_filenames()
|
|
||||||
self.add_items_tvcontacts(file_list)
|
|
||||||
dialog.destroy()
|
|
||||||
|
|
||||||
def add_directory(self, widget):
|
|
||||||
dialog = gtk.FileChooserDialog('Add directory to be shared', self,
|
|
||||||
gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
|
|
||||||
(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN,
|
|
||||||
gtk.RESPONSE_ACCEPT)
|
|
||||||
)
|
|
||||||
response = dialog.run()
|
|
||||||
if response == gtk.RESPONSE_ACCEPT:
|
|
||||||
file_list = []
|
|
||||||
file_list.append(dialog.get_filename())
|
|
||||||
self.add_items_tvcontacts(file_list)
|
|
||||||
dialog.destroy()
|
|
||||||
|
|
||||||
def add_file_list(self, flist, treestore, fref = {}, root = None):
|
|
||||||
# file references to their in the treeStore
|
|
||||||
for f in flist:
|
|
||||||
# keeps track of the relative dir path
|
|
||||||
tail = ''
|
|
||||||
# keeps track of the parent row
|
|
||||||
parent = None
|
|
||||||
dirpath = f.split('/')
|
|
||||||
if len(dirpath) == 1:
|
|
||||||
# Top level file, it doesnt have parent, add it right away
|
|
||||||
fref[dirpath[0]] = treestore.insert(root, 0, (dirpath[0],))
|
|
||||||
else:
|
|
||||||
for dir_ in dirpath:
|
|
||||||
if tail + dir_ not in fref:
|
|
||||||
fref[tail + dir_] = treestore.append(parent, (dir_,))
|
|
||||||
parent = fref[tail + dir_]
|
|
||||||
tail = tail + dir_ + '/'
|
|
||||||
return fref
|
|
||||||
|
|
||||||
def __convert_date(self, epoch):
|
|
||||||
# Converts date-time from seconds from epoch to iso 8601
|
|
||||||
import time, datetime
|
|
||||||
ts = time.gmtime(epoch)
|
|
||||||
dt = datetime.datetime(ts.tm_year, ts.tm_mon, ts.tm_mday, ts.tm_hour,
|
|
||||||
ts.tm_min, ts.tm_sec)
|
|
||||||
return dt.isoformat()
|
|
||||||
|
|
||||||
def add_items_tvcontacts(self, file_list, parentdir = None, parent = None):
|
|
||||||
# TODO: execute this method inside of a thread
|
|
||||||
for f in file_list:
|
|
||||||
# Relative name to be used internally in the shared folders
|
|
||||||
relative_name = f.split('/')[-1]
|
|
||||||
if parentdir:
|
|
||||||
relative_name = parentdir + '/' + relative_name
|
|
||||||
short_name = relative_name.split('/')[-1]
|
|
||||||
if short_name[0] == '.':
|
|
||||||
# Return if it is a hidden file
|
|
||||||
return
|
|
||||||
if parent:
|
|
||||||
row = self.ts_files.append(parent, (short_name,))
|
|
||||||
else:
|
|
||||||
row = self.ts_files.insert(None, 0, (short_name,))
|
|
||||||
# File info
|
|
||||||
size = os.path.getsize(f)
|
|
||||||
is_dir = os.path.isdir(f)
|
|
||||||
mod_date = os.path.getmtime(f)
|
|
||||||
mod_date = self.__convert_date(mod_date)
|
|
||||||
# TODO: add hash
|
|
||||||
file_ = (f, relative_name, '', size, '', mod_date, is_dir)
|
|
||||||
requester = self.cbb_contacts.get_active_text()
|
|
||||||
try:
|
|
||||||
fid = self.plugin.database.add_file(self.account, requester,
|
|
||||||
file_)
|
|
||||||
except Exception, e:
|
|
||||||
if e == 'Duplicated entry':
|
|
||||||
print 'Error: ' + e
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
raise Exception(e)
|
|
||||||
if is_dir:
|
|
||||||
tmpfl = os.listdir(f)
|
|
||||||
fl = []
|
|
||||||
for item in tmpfl:
|
|
||||||
fl.append(f + '/' + item)
|
|
||||||
self.add_items_tvcontacts(fl, relative_name, row)
|
|
||||||
|
|
||||||
def add_contact_browse(self, contact):
|
|
||||||
fjid = contact.get_full_jid()
|
|
||||||
jid = gajim.get_jid_without_resource(fjid)
|
|
||||||
contacts = gajim.contacts.get_contacts(self.account, jid)
|
|
||||||
for con in contacts:
|
|
||||||
if con.show in ('offline', 'error') and not \
|
|
||||||
con.supports(fshare_protocol.NS_FILE_SHARING):
|
|
||||||
break
|
|
||||||
cjid = con.get_full_jid()
|
|
||||||
r = self.ts_search.insert(None, 0, (cjid, ))
|
|
||||||
self.browse_jid[cjid] = r
|
|
||||||
pro = fshare.FileSharePlugin.prohandler[self.account]
|
|
||||||
# Request list of files from peer
|
|
||||||
stanza = pro.request(cjid)
|
|
||||||
if pro.conn.connection:
|
|
||||||
pro.conn.connection.send(stanza)
|
|
||||||
|
|
||||||
def add_contact_manage(self, contact):
|
|
||||||
self.contacts_rows = []
|
|
||||||
self.cbb_contacts.grab_focus()
|
|
||||||
for c in gajim.contacts.iter_contacts(self.account):
|
|
||||||
jid = gajim.get_jid_without_resource(c.get_full_jid())
|
|
||||||
r = self.ts_contacts.insert(None, len(self.ts_contacts), (jid, ))
|
|
||||||
if c.get_full_jid() == contact.get_full_jid():
|
|
||||||
self.cbb_contacts.set_active_iter(r)
|
|
||||||
self.contacts_rows.append(r)
|
|
||||||
self.manage_vbox2.set_sensitive(True)
|
|
||||||
self.bt_remove.set_sensitive(False)
|
|
||||||
self.add_file_list(self.plugin.database.get_files_name(self.account,
|
|
||||||
gajim.get_jid_without_resource(contact.get_full_jid())),
|
|
||||||
self.ts_files)
|
|
||||||
|
|
||||||
def delete_event(self, widget, data=None):
|
|
||||||
fshare.FileSharePlugin.filesharewindow = {}
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __check_combo_edit(self, widget, data=None):
|
|
||||||
self.ts_files.clear()
|
|
||||||
entry = self.cbb_contacts.child
|
|
||||||
contact = entry.get_text()
|
|
||||||
self.manage_vbox2.set_sensitive(False)
|
|
||||||
for i in self.contacts_rows:
|
|
||||||
# If the contact in the comboboxentry is include inside of the
|
|
||||||
# combobox
|
|
||||||
if contact == self.ts_contacts.get_value(i, 0):
|
|
||||||
self.add_file_list(self.plugin.database.get_files_name(
|
|
||||||
self.account, contact), self.ts_files)
|
|
||||||
self.manage_vbox2.set_sensitive(True)
|
|
||||||
self.bt_remove.set_sensitive(False)
|
|
||||||
break
|
|
||||||
|
|
||||||
def remove_file_clicked(self, widget, data=None):
|
|
||||||
entry = self.cbb_contacts.child
|
|
||||||
contact = entry.get_text()
|
|
||||||
sel = self.treeSelection_files.get_selected()
|
|
||||||
relative_name = self.ts_files.get_value(sel[1], 0)
|
|
||||||
self.ts_files.remove(sel[1])
|
|
||||||
self.plugin.database.delete(self.account, contact, relative_name)
|
|
||||||
widget.set_sensitive(False)
|
|
||||||
|
|
||||||
def remove_all_clicked(self, widget, data=None):
|
|
||||||
entry = self.cbb_contacts.child
|
|
||||||
contact = entry.get_text()
|
|
||||||
self.plugin.database.delete_all(self.account, contact)
|
|
||||||
self.ts_files.clear()
|
|
||||||
|
|
||||||
def row_selected(self, widget, data=None):
|
|
||||||
# When row is selected in tv_files
|
|
||||||
sel = self.treeSelection_files.get_selected()
|
|
||||||
if not sel[1]:
|
|
||||||
return
|
|
||||||
depth = self.ts_files.iter_depth(sel[1])
|
|
||||||
# Don't remove file and dirs that aren't at the root level
|
|
||||||
if depth == 0:
|
|
||||||
self.bt_remove.set_sensitive(True)
|
|
||||||
else:
|
|
||||||
self.bt_remove.set_sensitive(False)
|
|
||||||
|
|
||||||
def row_expanded(self, widget, iter_, path, data=None):
|
|
||||||
name = None
|
|
||||||
for key in self.empty_row_child:
|
|
||||||
parent = self.ts_search.iter_parent(self.empty_row_child[key])
|
|
||||||
p = self.ts_search.get_path(parent)
|
|
||||||
if p == path:
|
|
||||||
name = key
|
|
||||||
break
|
|
||||||
if name:
|
|
||||||
# if we found that the expanded row is the parent of the empty row
|
|
||||||
# remove it from the treestore and empty_row dictionary. Then ask
|
|
||||||
# peer for list of files of that directory
|
|
||||||
i = self.empty_row_child[name]
|
|
||||||
pro = fshare.FileSharePlugin.prohandler[self.account]
|
|
||||||
contact = self.get_contact_from_iter(self.ts_search, i)
|
|
||||||
contact = gajim.contacts.get_contact_with_highest_priority(
|
|
||||||
self.account, contact)
|
|
||||||
stanza = pro.request(contact.get_full_jid(), name, isFile=False)
|
|
||||||
if pro.conn.connection:
|
|
||||||
pro.conn.connection.send(stanza)
|
|
||||||
self.ts_search.remove(i)
|
|
||||||
del self.empty_row_child[name]
|
|
||||||
|
|
||||||
def get_contact_from_iter(self, treestore, iter_):
|
|
||||||
toplevel = treestore.get_iter_root()
|
|
||||||
while toplevel:
|
|
||||||
if treestore.is_ancestor(toplevel, iter_):
|
|
||||||
return gajim.get_jid_without_resource(treestore.get_value(
|
|
||||||
toplevel, 0))
|
|
||||||
toplevel = treestore.iter_next(toplevel)
|
|
||||||
|
|
||||||
def on_treeview_button_press_event(self, treeview, event):
|
|
||||||
if event.button == 3:
|
|
||||||
x = int(event.x)
|
|
||||||
y = int(event.y)
|
|
||||||
time = event.time
|
|
||||||
pthinfo = treeview.get_path_at_pos(x, y)
|
|
||||||
if pthinfo is not None:
|
|
||||||
path, col, cellx, celly = pthinfo
|
|
||||||
treeview.grab_focus()
|
|
||||||
treeview.set_cursor( path, col, 0)
|
|
||||||
treestore = treeview.get_model()
|
|
||||||
it = treestore.get_iter(path)
|
|
||||||
if treestore.iter_depth(it) != 0:
|
|
||||||
self.browse_popup.popup(None, None, None, event.button, time)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def on_bt_search_clicked(self, widget, data=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def on_download_clicked(self, widget, data=None):
|
|
||||||
tree, row = self.tv_search.get_selection().get_selected()
|
|
||||||
path = tree.get_path(row)
|
|
||||||
file_info = self.brw_file_info[path]
|
|
||||||
fjid = self.get_contact_from_iter(tree, row)
|
|
||||||
# Request the file
|
|
||||||
file_path = os.path.join(self.plugin.config['incoming_dir'],
|
|
||||||
file_info[0])
|
|
||||||
sid = helpers.get_random_string_16()
|
|
||||||
new_file_props = FilesProp.getNewFileProp(self.account, sid)
|
|
||||||
new_file_props.file_name = file_path
|
|
||||||
print file_path
|
|
||||||
new_file_props.name = file_info[0]
|
|
||||||
new_file_props.desc = file_info[4]
|
|
||||||
new_file_props.size = file_info[2]
|
|
||||||
new_file_props.date = file_info[1]
|
|
||||||
new_file_props.hash_ = None if file_info[3] == '' else file_info[3]
|
|
||||||
new_file_props.type_ = 'r'
|
|
||||||
tsid = gajim.connections[self.account].start_file_transfer(fjid,
|
|
||||||
new_file_props, True)
|
|
||||||
new_file_props.transport_sid = tsid
|
|
||||||
ft_window = gajim.interface.instances['file_transfers']
|
|
||||||
contact = gajim.contacts.get_contact_from_full_jid(self.account, fjid)
|
|
||||||
ft_window .add_transfer(self.account, contact, new_file_props)
|
|
||||||
|
|
||||||
def on_bt_sel_dir_pref_clicked(self, widget, data=None):
|
|
||||||
chooser = gtk.FileChooserDialog(title='Incoming files directory',
|
|
||||||
action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
|
|
||||||
buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN,
|
|
||||||
gtk.RESPONSE_OK))
|
|
||||||
response = chooser.run()
|
|
||||||
if response == gtk.RESPONSE_OK:
|
|
||||||
file_name = chooser.get_filename()
|
|
||||||
self.entry_dir_pref.set_text(file_name)
|
|
||||||
self.plugin.config['incoming_dir'] = file_name
|
|
||||||
chooser.destroy()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
f = FileShareWindow(None)
|
|
||||||
f.show()
|
|
||||||
gtk.main()
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
##
|
|
||||||
import database
|
|
||||||
import gtk
|
|
||||||
import os
|
|
||||||
import base64
|
|
||||||
import urllib2
|
|
||||||
from plugins import GajimPlugin
|
|
||||||
from plugins.helpers import log_calls
|
|
||||||
import gui_menu_builder
|
|
||||||
import gtkgui_helpers
|
|
||||||
from common import gajim
|
|
||||||
from fileshare_window import FileShareWindow
|
|
||||||
import fshare_protocol
|
|
||||||
from common import ged
|
|
||||||
from common import caps_cache
|
|
||||||
from common import xmpp
|
|
||||||
from plugins.gui import GajimPluginConfigDialog
|
|
||||||
|
|
||||||
|
|
||||||
class FileSharePlugin(GajimPlugin):
|
|
||||||
|
|
||||||
filesharewindow = {}
|
|
||||||
prohandler = {}
|
|
||||||
|
|
||||||
@log_calls('FileSharePlugin')
|
|
||||||
def init(self):
|
|
||||||
self.activated = False
|
|
||||||
self.description = _('This plugin allows you to share folders'
|
|
||||||
' with a peer using jingle file transfer.')
|
|
||||||
self.config_dialog = FileSharePluginConfigDialog(self)
|
|
||||||
home_path = os.path.expanduser('~/')
|
|
||||||
self.config_default_values = {'incoming_dir': (home_path, '')}
|
|
||||||
self.database = database.FilesharingDatabase(self)
|
|
||||||
# Create one protocol handler per account
|
|
||||||
accounts = gajim.contacts.get_accounts()
|
|
||||||
for account in gajim.contacts.get_accounts():
|
|
||||||
FileSharePlugin.prohandler[account] = \
|
|
||||||
fshare_protocol.Protocol(account, self)
|
|
||||||
self.events_handlers = {
|
|
||||||
'raw-iq-received': (ged.CORE, self._nec_raw_iq)
|
|
||||||
}
|
|
||||||
|
|
||||||
def activate(self):
|
|
||||||
self.activated = True
|
|
||||||
# Add fs feature
|
|
||||||
if fshare_protocol.NS_FILE_SHARING not in gajim.gajim_common_features:
|
|
||||||
gajim.gajim_common_features.append(fshare_protocol.NS_FILE_SHARING)
|
|
||||||
self._compute_caps_hash()
|
|
||||||
# Replace the contact menu
|
|
||||||
self.__get_contact_menu = gui_menu_builder.get_contact_menu
|
|
||||||
gui_menu_builder.get_contact_menu = self.contact_menu
|
|
||||||
# Replace get_file_info
|
|
||||||
for account in gajim.contacts.get_accounts():
|
|
||||||
conn = gajim.connections[account]
|
|
||||||
self._get_file_info = conn.get_file_info
|
|
||||||
conn.get_file_info = self.get_file_info
|
|
||||||
|
|
||||||
def deactivate(self):
|
|
||||||
self.activated = False
|
|
||||||
# Remove fs feature
|
|
||||||
if fshare_protocol.NS_FILE_SHARING not in gajim.gajim_common_features:
|
|
||||||
gajim.gajim_common_features.remove(fshare_protocol.NS_FILE_SHARING)
|
|
||||||
self._compute_caps_hash()
|
|
||||||
# Restore the contact menu
|
|
||||||
gui_menu_builder.get_contact_menu = self.__get_contact_menu
|
|
||||||
# Restore get_file_info
|
|
||||||
for account in gajim.contacts.get_accounts():
|
|
||||||
conn = gajim.connections[account]
|
|
||||||
conn.get_file_info = self._get_file_info
|
|
||||||
|
|
||||||
def _compute_caps_hash(self):
|
|
||||||
for a in gajim.connections:
|
|
||||||
gajim.caps_hash[a] = caps_cache.compute_caps_hash([
|
|
||||||
gajim.gajim_identity], gajim.gajim_common_features + \
|
|
||||||
gajim.gajim_optional_features[a])
|
|
||||||
# re-send presence with new hash
|
|
||||||
connected = gajim.connections[a].connected
|
|
||||||
if connected > 1 and gajim.SHOW_LIST[connected] != 'invisible':
|
|
||||||
gajim.connections[a].change_status(gajim.SHOW_LIST[connected],
|
|
||||||
gajim.connections[a].status)
|
|
||||||
|
|
||||||
def _nec_raw_iq(self, obj):
|
|
||||||
if obj.stanza.getTag('match',
|
|
||||||
namespace=fshare_protocol.NS_FILE_SHARING) and self.activated:
|
|
||||||
account = obj.conn.name
|
|
||||||
pro = FileSharePlugin.prohandler[account]
|
|
||||||
pro.handler(obj.stanza)
|
|
||||||
raise xmpp.NodeProcessed
|
|
||||||
|
|
||||||
def __get_contact_menu(self, contact, account):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def contact_menu(self, contact, account):
|
|
||||||
menu = self.__get_contact_menu(contact, account)
|
|
||||||
fs = gtk.MenuItem('File sharing')
|
|
||||||
submenu = gtk.Menu()
|
|
||||||
fs.set_submenu(submenu)
|
|
||||||
bf = gtk.MenuItem('Browse files')
|
|
||||||
bf.connect('activate', self.browse_menu_clicked, account, contact)
|
|
||||||
msf = gtk.MenuItem('Manage shared files')
|
|
||||||
msf.connect('activate', self.manage_menu_clicked, account, contact)
|
|
||||||
enable_fs = gtk.CheckMenuItem('Enable file sharing')
|
|
||||||
enable_fs.set_active(True)
|
|
||||||
submenu.attach(bf, 0, 1, 0, 1)
|
|
||||||
submenu.attach(msf, 0, 1, 1, 2)
|
|
||||||
submenu.attach(enable_fs, 0, 1, 2, 3)
|
|
||||||
if gajim.account_is_disconnected(account) or \
|
|
||||||
contact.show in ('offline', 'error') or not \
|
|
||||||
contact.supports(fshare_protocol.NS_FILE_SHARING):
|
|
||||||
bf.set_sensitive(False)
|
|
||||||
submenu.show()
|
|
||||||
bf.show()
|
|
||||||
msf.show()
|
|
||||||
enable_fs.show()
|
|
||||||
fs.show()
|
|
||||||
menu.attach(fs, 0, 1, 3, 4)
|
|
||||||
return menu
|
|
||||||
|
|
||||||
def _get_file_info(self, peerjid, hash_=None, name=None, account=None):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def get_file_info(self, peerjid, hash_=None, name=None, account=None):
|
|
||||||
file_info = self._get_file_info(hash_, name)
|
|
||||||
if file_info:
|
|
||||||
return file_info
|
|
||||||
raw_info = self.database.get_file(account, peerjid, hash_, name)
|
|
||||||
file_info = {'name': raw_info[0],
|
|
||||||
'file-name' : raw_info[5],
|
|
||||||
'hash' : raw_info[1],
|
|
||||||
'size' : raw_info[2],
|
|
||||||
'date' : raw_info[4],
|
|
||||||
'peerjid' : peerjid
|
|
||||||
}
|
|
||||||
|
|
||||||
return file_info
|
|
||||||
|
|
||||||
def __get_fsw_instance(self, account):
|
|
||||||
# Makes sure we only have one instance of the window per account
|
|
||||||
if account not in FileSharePlugin.filesharewindow:
|
|
||||||
FileSharePlugin.filesharewindow[account] = fsw = FileShareWindow(
|
|
||||||
self)
|
|
||||||
FileSharePlugin.prohandler[account].set_window(fsw)
|
|
||||||
return FileSharePlugin.filesharewindow[account]
|
|
||||||
|
|
||||||
def __init_window(self, account, contact):
|
|
||||||
fsw = self.__get_fsw_instance(account)
|
|
||||||
fsw.set_account(account)
|
|
||||||
fsw.contacts_rows = []
|
|
||||||
fsw.ts_contacts.clear()
|
|
||||||
fsw.ts_search.clear()
|
|
||||||
# Add information to widgets
|
|
||||||
fsw.add_contact_manage(contact)
|
|
||||||
fsw.add_contact_browse(contact)
|
|
||||||
return fsw
|
|
||||||
|
|
||||||
def manage_menu_clicked(self, widget, account, contact):
|
|
||||||
fsw = self.__init_window(account, contact)
|
|
||||||
fsw.notebook.set_current_page(1)
|
|
||||||
|
|
||||||
def browse_menu_clicked(self, widget, account, contact):
|
|
||||||
fsw = self.__init_window(account, contact)
|
|
||||||
fsw.notebook.set_current_page(0)
|
|
||||||
|
|
||||||
class FileSharePluginConfigDialog(GajimPluginConfigDialog):
|
|
||||||
def init(self):
|
|
||||||
self.GTK_BUILDER_FILE_PATH = self.plugin.local_file_path(
|
|
||||||
'config_dialog.ui')
|
|
||||||
self.xml = gtk.Builder()
|
|
||||||
self.xml.set_translation_domain('gajim_plugins')
|
|
||||||
self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH, ['hbox111'])
|
|
||||||
hbox = self.xml.get_object('hbox111')
|
|
||||||
self.child.pack_start(hbox)
|
|
||||||
self.connect('hide', self.on_hide)
|
|
||||||
|
|
||||||
def on_run(self):
|
|
||||||
widget = self.xml.get_object('dl_folder')
|
|
||||||
widget.set_text(str(self.plugin.config['incoming_dir']))
|
|
||||||
|
|
||||||
def on_hide(self, widget):
|
|
||||||
widget = self.xml.get_object('dl_folder')
|
|
||||||
self.plugin.config['incoming_dir'] = widget.get_text()
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
from common import xmpp
|
|
||||||
from common import helpers
|
|
||||||
from common import gajim
|
|
||||||
from common import XMPPDispatcher
|
|
||||||
from common.xmpp import Hashes
|
|
||||||
# Namespace for file sharing
|
|
||||||
NS_FILE_SHARING = 'http://gajim.org/protocol/filesharing'
|
|
||||||
|
|
||||||
class Protocol():
|
|
||||||
|
|
||||||
def __init__(self, account, plugin):
|
|
||||||
self.account = account
|
|
||||||
self.plugin = plugin
|
|
||||||
self.conn = gajim.connections[self.account]
|
|
||||||
# get our jid with resource
|
|
||||||
self.ourjid = gajim.get_jid_from_account(self.account)
|
|
||||||
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):
|
|
||||||
iq = xmpp.Iq(typ, to=stanza.getFrom(), frm=stanza.getTo(),
|
|
||||||
attrs={'id': stanza.getID()})
|
|
||||||
iq.addChild(name='match', namespace=NS_FILE_SHARING)
|
|
||||||
return iq
|
|
||||||
|
|
||||||
def on_request(self, stanza):
|
|
||||||
try:
|
|
||||||
fjid = helpers.get_full_jid_from_iq(stanza)
|
|
||||||
except helpers.InvalidFormat:
|
|
||||||
# A message from a non-valid JID arrived, it has been ignored.
|
|
||||||
return
|
|
||||||
if stanza.getTag('error'):
|
|
||||||
# TODO: better handle this
|
|
||||||
return
|
|
||||||
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:
|
|
||||||
# TODO: reply with malformed stanza error
|
|
||||||
pass
|
|
||||||
|
|
||||||
def offer(self, id_, contact, items):
|
|
||||||
iq = xmpp.Iq(typ='result', to=contact, frm=self.ourjid,
|
|
||||||
attrs={'id': id_})
|
|
||||||
match = iq.addChild(name='match', namespace=NS_FILE_SHARING)
|
|
||||||
offer = match.addChild(name='offer')
|
|
||||||
if len(items) == 0:
|
|
||||||
offer.addChild(name='directory')
|
|
||||||
else:
|
|
||||||
for i in items:
|
|
||||||
# if it is a directory
|
|
||||||
if i[5] == True:
|
|
||||||
item = offer.addChild(name='directory')
|
|
||||||
name = item.addChild('name')
|
|
||||||
name.setData('/' + i[0])
|
|
||||||
else:
|
|
||||||
item = offer.addChild(name='file')
|
|
||||||
item.addChild('name').setData('/' + i[0])
|
|
||||||
if i[1] != '':
|
|
||||||
h = Hashes()
|
|
||||||
h.addHash(i[1], 'sha-1')
|
|
||||||
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):
|
|
||||||
self.fsw = fsw
|
|
||||||
|
|
||||||
|
|
||||||
def get_files_info(stanza):
|
|
||||||
# Crawls the stanza in search for file and dir structure.
|
|
||||||
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)
|
|
||||||
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
[info]
|
|
||||||
name: File Sharing
|
|
||||||
short_name: fshare
|
|
||||||
#version: 0.1.1
|
|
||||||
description: This plugin allows you to share folders with your peers using jingle file transfer.
|
|
||||||
authors: Jefry Lagrange <jefry.reyes@gmail.com>
|
|
||||||
homepage: www.google.com
|
|
||||||
|
|
||||||
max_gajim_version: 0.15.9
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from plugin import GnomeSessionManagerPlugin
|
|
||||||
|
Before Width: | Height: | Size: 714 B |
@@ -1,8 +0,0 @@
|
|||||||
[info]
|
|
||||||
name: GNOME SessionManager
|
|
||||||
short_name: gnome_session_manager
|
|
||||||
version: 0.1.3
|
|
||||||
description: Set and react on GNOME Session presence settings
|
|
||||||
authors: Philippe Normand <phil@base-art.net>
|
|
||||||
homepage: http://base-art.net
|
|
||||||
max_gajim_version: 0.15.9
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
## Copyright (C) 2010 Philippe Normand <phil@base-art.net>
|
|
||||||
##
|
|
||||||
## This file is part of Gajim.
|
|
||||||
##
|
|
||||||
## Gajim is free software; you can redistribute it and/or modify
|
|
||||||
## it under the terms of the GNU General Public License as published
|
|
||||||
## by the Free Software Foundation; version 3 only.
|
|
||||||
##
|
|
||||||
## Gajim is distributed in the hope that it will be useful,
|
|
||||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
## GNU General Public License for more details.
|
|
||||||
##
|
|
||||||
## You should have received a copy of the GNU General Public License
|
|
||||||
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
##
|
|
||||||
|
|
||||||
import dbus
|
|
||||||
from common import app
|
|
||||||
from common import ged
|
|
||||||
from common import dbus_support
|
|
||||||
import gui_interface
|
|
||||||
from plugins import GajimPlugin
|
|
||||||
from plugins.helpers import log_calls, log
|
|
||||||
|
|
||||||
GNOME_STATUS = [u'online', u'invisible', u'dnd', u'idle']
|
|
||||||
PRESENCE_INTERFACE = "org.gnome.SessionManager.Presence"
|
|
||||||
|
|
||||||
class GnomeSessionManagerPlugin(GajimPlugin):
|
|
||||||
|
|
||||||
@log_calls('GnomeSessionManagerPlugin')
|
|
||||||
def init(self):
|
|
||||||
self.description = _('Set and react on GNOME Session presence settings')
|
|
||||||
self.config_dialog = None
|
|
||||||
self.events_handlers = {}
|
|
||||||
|
|
||||||
@log_calls('GnomeSessionManagerPlugin')
|
|
||||||
def activate(self):
|
|
||||||
if not dbus_support.supported:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.bus = dbus_support.session_bus.SessionBus()
|
|
||||||
try:
|
|
||||||
self.session_presence = self.bus.get_object("org.gnome.SessionManager",
|
|
||||||
"/org/gnome/SessionManager/Presence")
|
|
||||||
except:
|
|
||||||
app.log.debug("GNOME SessionManager D-Bus service not found")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.active = True
|
|
||||||
app.ged.register_event_handler('our-show', ged.POSTGUI,
|
|
||||||
self.on_our_status)
|
|
||||||
self.bus.add_signal_receiver(self.gnome_presence_changed,
|
|
||||||
"StatusChanged", PRESENCE_INTERFACE)
|
|
||||||
|
|
||||||
@log_calls('GnomeSessionManagerPlugin')
|
|
||||||
def deactivate(self):
|
|
||||||
if not dbus_support.supported or not self.active:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.active = False
|
|
||||||
self.bus.remove_signal_receiver(self.gnome_presence_changed, "StatusChanged",
|
|
||||||
dbus_interface=PRESENCE_INTERFACE)
|
|
||||||
app.ged.remove_event_handler('our-show', ged.POSTGUI, self.on_our_status)
|
|
||||||
|
|
||||||
|
|
||||||
def gnome_presence_changed(self, status, *args, **kw):
|
|
||||||
if not app.interface.remote_ctrl:
|
|
||||||
try:
|
|
||||||
import remote_control
|
|
||||||
app.interface.remote_ctrl = remote_control.Remote()
|
|
||||||
except:
|
|
||||||
return
|
|
||||||
remote_gajim = app.interface.remote_ctrl.signal_object
|
|
||||||
gajim_status = GNOME_STATUS[status]
|
|
||||||
accounts = remote_gajim.list_accounts()
|
|
||||||
for account in accounts:
|
|
||||||
message = remote_gajim.get_status_message(account)
|
|
||||||
remote_gajim.change_status(gajim_status, message, account)
|
|
||||||
|
|
||||||
def on_our_status(self, network_event):
|
|
||||||
try:
|
|
||||||
gnome_status = GNOME_STATUS.index(network_event.show)
|
|
||||||
except ValueError:
|
|
||||||
print "GNOME SessionManager doesn't support %r status" % network_event.show
|
|
||||||
else:
|
|
||||||
self.session_presence.SetStatus(dbus.UInt32(gnome_status),
|
|
||||||
dbus_interface=PRESENCE_INTERFACE)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from otrmodule import OtrPlugin
|
|
||||||
@@ -1,382 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<interface>
|
|
||||||
<requires lib="gtk+" version="2.16"/>
|
|
||||||
<!-- interface-naming-policy toplevel-contextual -->
|
|
||||||
<object class="GtkListStore" id="account_store">
|
|
||||||
<columns>
|
|
||||||
<!-- column-name accountname -->
|
|
||||||
<column type="gchararray"/>
|
|
||||||
</columns>
|
|
||||||
</object>
|
|
||||||
<object class="GtkListStore" id="fingerprint_store">
|
|
||||||
<columns>
|
|
||||||
<!-- column-name screenname -->
|
|
||||||
<column type="gchararray"/>
|
|
||||||
<!-- column-name status -->
|
|
||||||
<column type="gchararray"/>
|
|
||||||
<!-- column-name verified -->
|
|
||||||
<column type="gboolean"/>
|
|
||||||
<!-- column-name fingerprint -->
|
|
||||||
<column type="gchararray"/>
|
|
||||||
<!-- column-name account -->
|
|
||||||
<column type="gchararray"/>
|
|
||||||
<!-- column-name tooltip -->
|
|
||||||
<column type="gchararray"/>
|
|
||||||
</columns>
|
|
||||||
</object>
|
|
||||||
<object class="GtkMenu" id="fprclipboard_menu">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">False</property>
|
|
||||||
<property name="ubuntu_local">True</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkMenuItem" id="copyfprclipboard_item">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">False</property>
|
|
||||||
<property name="use_action_appearance">False</property>
|
|
||||||
<property name="label" translatable="yes" comments="Context menu item">Copy to clipboard</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<signal name="activate" handler="clipboard_button_cb"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<object class="GtkNotebook" id="notebook1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkVBox" id="vbox1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkFrame" id="frame1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">12</property>
|
|
||||||
<property name="label_xalign">0</property>
|
|
||||||
<property name="shadow_type">none</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAlignment" id="alignment1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="left_padding">12</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkVBox" id="vbox2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<property name="spacing">5</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkHBox" id="hbox1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="spacing">5</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="fingerprint_label_desc">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label" translatable="yes" comments="Descriptive label">Fingerprint:</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="fingerprint_label">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label"><tt>-------- -------- -------- -------- -------- </tt></property>
|
|
||||||
<property name="use_markup">True</property>
|
|
||||||
<property name="selectable">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="regenerate_button">
|
|
||||||
<property name="label" translatable="yes" comments="Generate Fingerprint button">(Re-)generate</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">True</property>
|
|
||||||
<signal name="clicked" handler="regenerate_button_clicked_cb"/>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkFrame" id="frame2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">12</property>
|
|
||||||
<property name="label_xalign">0</property>
|
|
||||||
<property name="shadow_type">none</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAlignment" id="alignment2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="left_padding">12</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkVBox" id="vbox3">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCheckButton" id="enable_check">
|
|
||||||
<property name="label" translatable="yes" comments="checkbox">Enable private (Off-the-Record) messaging</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">False</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
<signal name="toggled" handler="flags_toggled_cb" after="yes"/>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCheckButton" id="advertise_check">
|
|
||||||
<property name="label" translatable="yes" comments="checkbox">Advertise Off-the-Record messaging support</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">False</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
<signal name="toggled" handler="flags_toggled_cb" after="yes"/>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCheckButton" id="autoinitiate_check">
|
|
||||||
<property name="label" translatable="yes" comments="checkbox">Automatically start private messaging</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">False</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
<signal name="toggled" handler="flags_toggled_cb" after="yes"/>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCheckButton" id="require_check">
|
|
||||||
<property name="label" translatable="yes" comments="checkbox">Require private messaging</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">False</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
<signal name="toggled" handler="flags_toggled_cb" after="yes"/>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">3</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child type="label">
|
|
||||||
<object class="GtkLabel" id="label3">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label" translatable="yes" comments="title above options"><b>Default OTR Settings</b></property>
|
|
||||||
<property name="use_markup">True</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child type="label">
|
|
||||||
<object class="GtkHBox" id="hbox2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="spacing">5</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="label4">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label" translatable="yes" comments="label for account selector"><b>Off-the-Record settings for:</b></property>
|
|
||||||
<property name="use_markup">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkComboBox" id="account_combobox">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="model">account_store</property>
|
|
||||||
<signal name="changed" handler="account_combobox_changed_cb"/>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCellRendererText" id="cellrenderertext1"/>
|
|
||||||
<attributes>
|
|
||||||
<attribute name="text">0</attribute>
|
|
||||||
</attributes>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child type="tab">
|
|
||||||
<object class="GtkLabel" id="label1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label" translatable="yes" comments="tab label">OTR Settings</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="tab_fill">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkVBox" id="vbox4">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">12</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkScrolledWindow" id="scrolledwindow1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="hscrollbar_policy">automatic</property>
|
|
||||||
<property name="vscrollbar_policy">automatic</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkTreeView" id="fingerprint_view">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="model">fingerprint_store</property>
|
|
||||||
<property name="search_column">0</property>
|
|
||||||
<property name="tooltip_column">5</property>
|
|
||||||
<signal name="button-press-event" handler="fpr_button_pressed_cb" swapped="no"/>
|
|
||||||
<child>
|
|
||||||
<object class="GtkTreeViewColumn" id="name_column">
|
|
||||||
<property name="resizable">True</property>
|
|
||||||
<property name="title">Name</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCellRendererText" id="cellrenderertext2"/>
|
|
||||||
<attributes>
|
|
||||||
<attribute name="text">0</attribute>
|
|
||||||
</attributes>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkTreeViewColumn" id="status_column">
|
|
||||||
<property name="resizable">True</property>
|
|
||||||
<property name="title">Status</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCellRendererText" id="cellrenderertext3"/>
|
|
||||||
<attributes>
|
|
||||||
<attribute name="text">1</attribute>
|
|
||||||
</attributes>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkTreeViewColumn" id="validated_column">
|
|
||||||
<property name="resizable">True</property>
|
|
||||||
<property name="title">Validated</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCellRendererToggle" id="cellrenderertoggle1"/>
|
|
||||||
<attributes>
|
|
||||||
<attribute name="active">2</attribute>
|
|
||||||
</attributes>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkTreeViewColumn" id="fingerprint_column">
|
|
||||||
<property name="resizable">True</property>
|
|
||||||
<property name="title">Fingerprint</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCellRendererText" id="cellrenderertext4"/>
|
|
||||||
<attributes>
|
|
||||||
<attribute name="markup">3</attribute>
|
|
||||||
</attributes>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkTreeViewColumn" id="account_column">
|
|
||||||
<property name="resizable">True</property>
|
|
||||||
<property name="title">Account</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCellRendererText" id="cellrenderertext5"/>
|
|
||||||
<attributes>
|
|
||||||
<attribute name="text">4</attribute>
|
|
||||||
</attributes>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkHBox" id="hbox3">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="spacing">5</property>
|
|
||||||
<property name="homogeneous">True</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="verify_button">
|
|
||||||
<property name="label" translatable="yes" comments="button">Verify Fingerprint</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">True</property>
|
|
||||||
<signal name="clicked" handler="verify_button_clicked_cb"/>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="forget_button">
|
|
||||||
<property name="label" translatable="yes" comments="button">Forget Fingerprint</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">True</property>
|
|
||||||
<signal name="clicked" handler="forget_button_clicked_cb"/>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child type="tab">
|
|
||||||
<object class="GtkLabel" id="label2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label" translatable="yes" comments="tab label">Known Fingerprints</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
<property name="tab_fill">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</interface>
|
|
||||||
@@ -1,355 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<interface>
|
|
||||||
<requires lib="gtk+" version="2.16"/>
|
|
||||||
<!-- interface-naming-policy toplevel-contextual -->
|
|
||||||
<object class="GtkNotebook" id="otr_settings_notebook">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkVBox" id="otr_fp_vbox">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<property name="spacing">5</property>
|
|
||||||
<property name="homogeneous">True</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="our_fp_label">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="label">Your fingerprint:
|
|
||||||
<span weight="bold" face="monospace">01234567 89ABCDEF 01234567 89ABCDEF 01234567</span></property>
|
|
||||||
<property name="use_markup">True</property>
|
|
||||||
<property name="selectable">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="their_fp_label">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="label">Purported fingerprint for asdfasdf@xyzxyzxyz.de:
|
|
||||||
<span weight="bold" face="monospace">01234567 89ABCDEF 01234567 89ABCDEF 01234567</span></property>
|
|
||||||
<property name="use_markup">True</property>
|
|
||||||
<property name="selectable">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkComboBox" id="verified_combobox">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="model">verifiedmodel</property>
|
|
||||||
<property name="active">0</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCellRendererText" id="cellrenderertext1"/>
|
|
||||||
<attributes>
|
|
||||||
<attribute name="text">0</attribute>
|
|
||||||
</attributes>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child type="tab">
|
|
||||||
<object class="GtkLabel" id="label1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label" translatable="yes" comments="tab label">Authentication</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
<property name="tab_fill">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkFrame" id="frame2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label_xalign">0</property>
|
|
||||||
<property name="shadow_type">none</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkVBox" id="otr_settings_vbox">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<property name="spacing">5</property>
|
|
||||||
<property name="homogeneous">True</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCheckButton" id="otr_policy_allow_v2_checkbutton">
|
|
||||||
<property name="label" translatable="yes" comments="checkbox">OTR version 2 allowed</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">False</property>
|
|
||||||
<property name="active">True</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCheckButton" id="otr_policy_require_checkbutton">
|
|
||||||
<property name="label" translatable="yes" comments="checkbox">Encryption required</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="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCheckButton" id="otr_policy_send_tag_checkbutton">
|
|
||||||
<property name="label" translatable="yes" comments="checkbox">Show others we understand OTR</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">False</property>
|
|
||||||
<property name="active">True</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">3</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCheckButton" id="otr_policy_start_on_tag_checkbutton">
|
|
||||||
<property name="label" translatable="yes" comments="checkbox">Automatically initiate encryption if partner understands OTR</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">False</property>
|
|
||||||
<property name="active">True</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">4</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child type="label">
|
|
||||||
<object class="GtkCheckButton" id="otr_default_checkbutton">
|
|
||||||
<property name="label" translatable="yes" comments="checkbox">Use the default settings</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">False</property>
|
|
||||||
<property name="active">True</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child type="tab">
|
|
||||||
<object class="GtkLabel" id="label2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label" translatable="yes" comments="tab label">OTR Settings</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
<property name="tab_fill">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<object class="GtkListStore" id="verifiedmodel">
|
|
||||||
<columns>
|
|
||||||
<!-- column-name verified -->
|
|
||||||
<column type="gchararray"/>
|
|
||||||
</columns>
|
|
||||||
<data>
|
|
||||||
<row>
|
|
||||||
<col id="0" translatable="yes" comments="unverified option (dropdown label)">I have NOT verified that the purported fingerprint is in fact the correct fingerprint for that contact.</col>
|
|
||||||
</row>
|
|
||||||
<row>
|
|
||||||
<col id="0" translatable="yes" comments="verified option (dropdown label)">I have verified that the purported fingerprint is in fact the correct fingerprint for that contact.</col>
|
|
||||||
</row>
|
|
||||||
</data>
|
|
||||||
</object>
|
|
||||||
<object class="GtkWindow" id="otr_smp_window">
|
|
||||||
<property name="resizable">False</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkVBox" id="vbox3">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkVBox" id="vbox4">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<property name="spacing">5</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="desclabel1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label">label</property>
|
|
||||||
<property name="use_markup">True</property>
|
|
||||||
<property name="wrap">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkHBox" id="hbox2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCheckButton" id="qcheckbutton">
|
|
||||||
<property name="label" translatable="yes" comments="checkbox for socialist millionaire protocol with question support">Use question: </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="expand">False</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkEntry" id="qentry">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="desclabel2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label">label</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkEntry" id="secret_entry">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">3</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkHBox" id="hbox3">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkProgressBar" id="progressbar">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkHButtonBox" id="hbuttonbox2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="layout_style">end</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="smp_cancel_button">
|
|
||||||
<property name="label">gtk-cancel</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">True</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="smp_ok_button">
|
|
||||||
<property name="label">gtk-ok</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">True</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<object class="GtkMenuItem" id="otr_submenu">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="sensitive">False</property>
|
|
||||||
<property name="label" translatable="yes" comments="contact's submenu entry">Off-the-Record Encryption</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<child type="submenu">
|
|
||||||
<object class="GtkMenu" id="otr_submenu_menu">
|
|
||||||
<child>
|
|
||||||
<object class="GtkMenuItem" id="otr_settings_menuitem">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label" translatable="yes" comments="menu entry for contact's otr settings">OTR settings / fingerprint</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<signal name="activate" handler="_on_otr_settings_menuitem_activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkMenuItem" id="smp_otr_menuitem">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label" translatable="yes" comments="menu entry for SMP authentication">Authenticate contact</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<signal name="activate" handler="_on_smp_otr_menuitem_activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkMenuItem" id="start_otr_menuitem">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label" translatable="yes" comments="menu entry for starting OTR session">Start / Refresh OTR</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<signal name="activate" handler="_on_start_otr_menuitem_activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkMenuItem" id="end_otr_menuitem">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="sensitive">False</property>
|
|
||||||
<property name="label" translatable="yes" comments="menu entry for killing an OTR session">End OTR</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<signal name="activate" handler="_on_end_otr_menuitem_activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</interface>
|
|
||||||
BIN
gotr/gotr.png
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -1,8 +0,0 @@
|
|||||||
[info]
|
|
||||||
name: Off-The-Record Encryption
|
|
||||||
short_name: gotr
|
|
||||||
version: 1.7.2
|
|
||||||
description: Provide OTR encryption
|
|
||||||
authors: Kjell Braden <afflux.gajim@pentabarf.de>
|
|
||||||
homepage: http://gajim-otr.pentabarf.de
|
|
||||||
max_gajim_version: 0.15.9
|
|
||||||
@@ -1,665 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
## otrmodule.py
|
|
||||||
##
|
|
||||||
## Copyright 2008-2012 Kjell Braden <afflux@pentabarf.de>
|
|
||||||
##
|
|
||||||
## This file is part of Gajim.
|
|
||||||
##
|
|
||||||
## Gajim is free software; you can redistribute it and/or modify
|
|
||||||
## it under the terms of the GNU General Public License as published
|
|
||||||
## by the Free Software Foundation; version 3 only.
|
|
||||||
##
|
|
||||||
## Gajim is distributed in the hope that it will be useful,
|
|
||||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
## GNU General Public License for more details.
|
|
||||||
##
|
|
||||||
## You should have received a copy of the GNU General Public License
|
|
||||||
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
##
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
Off-The-Record encryption plugin.
|
|
||||||
|
|
||||||
:author: Kjell self.Braden <kb.otr@pentabarf.de>
|
|
||||||
:since: 2008
|
|
||||||
:copyright: Copyright 2008-2012 Kjell Braden <afflux@pentabarf.de>
|
|
||||||
:license: GPL
|
|
||||||
'''
|
|
||||||
|
|
||||||
MINVERSION = (1,0,0,'beta5')
|
|
||||||
MINCRYPTOVERSION = (2,1,0,'final',0)
|
|
||||||
IGNORE = True
|
|
||||||
PASS = False
|
|
||||||
|
|
||||||
DEFAULTFLAGS = {
|
|
||||||
'ALLOW_V1':False,
|
|
||||||
'ALLOW_V2':True,
|
|
||||||
'REQUIRE_ENCRYPTION':False,
|
|
||||||
'SEND_TAG':True,
|
|
||||||
'WHITESPACE_START_AKE':True,
|
|
||||||
'ERROR_START_AKE':True,
|
|
||||||
}
|
|
||||||
|
|
||||||
MMS = 1024
|
|
||||||
PROTOCOL = 'xmpp'
|
|
||||||
|
|
||||||
enc_tip = 'A private chat session <i>is established</i> to this contact ' \
|
|
||||||
'with this fingerprint'
|
|
||||||
unused_tip = 'A private chat session is established to this contact using ' \
|
|
||||||
'<i>another</i> fingerprint'
|
|
||||||
ended_tip = 'The private chat session to this contact has <i>ended</i>'
|
|
||||||
inactive_tip = 'Communication to this contact is currently ' \
|
|
||||||
'<i>unencrypted</i>'
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import pickle
|
|
||||||
import time
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import common.xmpp
|
|
||||||
from common import gajim
|
|
||||||
from common import ged
|
|
||||||
from common.connection_handlers_events import MessageOutgoingEvent
|
|
||||||
from plugins import GajimPlugin
|
|
||||||
from message_control import TYPE_CHAT, MessageControl
|
|
||||||
from plugins.helpers import log_calls, log
|
|
||||||
from plugins.plugin import GajimPluginException
|
|
||||||
|
|
||||||
import ui
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(ui.__file__))
|
|
||||||
|
|
||||||
from HTMLParser import HTMLParser
|
|
||||||
from htmlentitydefs import name2codepoint
|
|
||||||
|
|
||||||
HAS_CRYPTO = True
|
|
||||||
try:
|
|
||||||
import Crypto
|
|
||||||
if not hasattr(Crypto, 'version_info') \
|
|
||||||
or Crypto.version_info < MINCRYPTOVERSION:
|
|
||||||
raise ImportError('PyCrypto not found or too old')
|
|
||||||
except ImportError:
|
|
||||||
HAS_CRYPTO = False
|
|
||||||
|
|
||||||
HAS_POTR = True
|
|
||||||
try:
|
|
||||||
import potr
|
|
||||||
import potr.crypt
|
|
||||||
import potr.context
|
|
||||||
if not hasattr(potr, 'VERSION') or potr.VERSION < MINVERSION:
|
|
||||||
raise ImportError('old / unsupported python-otr version')
|
|
||||||
|
|
||||||
potrrootlog = logging.getLogger('potr')
|
|
||||||
potrrootlog.handlers = []
|
|
||||||
potrrootlog.propagate = False
|
|
||||||
gajimrootlog = logging.getLogger('gajim')
|
|
||||||
for h in gajimrootlog.handlers:
|
|
||||||
potrrootlog.addHandler(h)
|
|
||||||
|
|
||||||
def get_jid_from_fjid(fjid):
|
|
||||||
return gajim.get_room_and_nick_from_fjid(fjid)[0]
|
|
||||||
|
|
||||||
class GajimContext(potr.context.Context):
|
|
||||||
# self.peer is fjid
|
|
||||||
# self.jid does not contain resource
|
|
||||||
__slots__ = ['smpWindow', 'jid']
|
|
||||||
|
|
||||||
def __init__(self, account, peer):
|
|
||||||
super(GajimContext, self).__init__(account, peer)
|
|
||||||
self.jid = get_jid_from_fjid(peer)
|
|
||||||
self.trustName = self.jid
|
|
||||||
self.smpWindow = ui.ContactOtrSmpWindow(self)
|
|
||||||
|
|
||||||
def inject(self, msg, appdata=None):
|
|
||||||
log.debug('inject(appdata=%s)', appdata)
|
|
||||||
msg = unicode(msg)
|
|
||||||
account = self.user.accountname
|
|
||||||
|
|
||||||
stanza = common.xmpp.Message(to=self.peer, body=msg, typ='chat')
|
|
||||||
if appdata is not None:
|
|
||||||
session = appdata.get('session', None)
|
|
||||||
if session is not None:
|
|
||||||
stanza.setThread(session.thread_id)
|
|
||||||
gajim.connections[account].connection.send(stanza, now=True)
|
|
||||||
|
|
||||||
def setState(self, newstate):
|
|
||||||
if self.state == potr.context.STATE_ENCRYPTED:
|
|
||||||
# we were encrypted
|
|
||||||
if newstate == potr.context.STATE_ENCRYPTED:
|
|
||||||
# and are still -> it's just a refresh
|
|
||||||
OtrPlugin.gajim_log(
|
|
||||||
_('Private conversation with %s refreshed.') % self.peer,
|
|
||||||
self.user.accountname, self.peer)
|
|
||||||
elif newstate == potr.context.STATE_FINISHED:
|
|
||||||
# and aren't anymore -> other side disconnected
|
|
||||||
OtrPlugin.gajim_log(_('%s has ended his/her private '
|
|
||||||
'conversation with you. You should do the same.')
|
|
||||||
% self.peer, self.user.accountname, self.peer)
|
|
||||||
else:
|
|
||||||
if newstate == potr.context.STATE_ENCRYPTED:
|
|
||||||
# we are now encrypted
|
|
||||||
trust = self.getCurrentTrust()
|
|
||||||
if trust is None:
|
|
||||||
fpr = str(self.getCurrentKey())
|
|
||||||
OtrPlugin.gajim_log(_('New fingerprint for %(peer)s: %(fpr)s')
|
|
||||||
% {'peer': self.peer, 'fpr': fpr},
|
|
||||||
self.user.accountname, self.peer)
|
|
||||||
self.setCurrentTrust('')
|
|
||||||
trustStr = 'authenticated' if bool(trust) else '*unauthenticated*'
|
|
||||||
OtrPlugin.gajim_log(
|
|
||||||
_('%(trustStr)s secured OTR conversation with %(peer)s started')
|
|
||||||
% {'trustStr': trustStr, 'peer': self.peer},
|
|
||||||
self.user.accountname, self.peer)
|
|
||||||
|
|
||||||
if self.state != potr.context.STATE_PLAINTEXT and \
|
|
||||||
newstate == potr.context.STATE_PLAINTEXT:
|
|
||||||
# we are now plaintext
|
|
||||||
OtrPlugin.gajim_log(
|
|
||||||
_('Private conversation with %s lost.') % self.peer,
|
|
||||||
self.user.accountname, self.peer)
|
|
||||||
|
|
||||||
super(GajimContext, self).setState(newstate)
|
|
||||||
OtrPlugin.update_otr(self.peer, self.user.accountname)
|
|
||||||
self.user.plugin.update_context_list()
|
|
||||||
|
|
||||||
def getPolicy(self, key):
|
|
||||||
ret = self.user.plugin.get_flags(self.user.accountname, self.jid)[key]
|
|
||||||
log.debug('getPolicy(key=%s) = %s', key, ret)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
class GajimOtrAccount(potr.context.Account):
|
|
||||||
contextclass = GajimContext
|
|
||||||
def __init__(self, plugin, accountname):
|
|
||||||
global PROTOCOL, MMS
|
|
||||||
self.plugin = plugin
|
|
||||||
self.accountname = accountname
|
|
||||||
name = gajim.get_jid_from_account(accountname)
|
|
||||||
super(GajimOtrAccount, self).__init__(name, PROTOCOL, MMS)
|
|
||||||
self.keyFilePath = os.path.join(gajim.gajimpaths.data_root, accountname)
|
|
||||||
|
|
||||||
def dropPrivkey(self):
|
|
||||||
try:
|
|
||||||
os.remove(self.keyFilePath + '.key3')
|
|
||||||
except IOError, e:
|
|
||||||
if e.errno != 2:
|
|
||||||
log.exception('IOError occurred when removing key file for %s',
|
|
||||||
self.name)
|
|
||||||
self.privkey = None
|
|
||||||
|
|
||||||
def loadPrivkey(self):
|
|
||||||
try:
|
|
||||||
with open(self.keyFilePath + '.key3', 'rb') as keyFile:
|
|
||||||
return potr.crypt.PK.parsePrivateKey(keyFile.read())[0]
|
|
||||||
except IOError, e:
|
|
||||||
if e.errno != 2:
|
|
||||||
log.exception('IOError occurred when loading key file for %s',
|
|
||||||
self.name)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def savePrivkey(self):
|
|
||||||
try:
|
|
||||||
with open(self.keyFilePath + '.key3', 'wb') as keyFile:
|
|
||||||
keyFile.write(self.getPrivkey().serializePrivateKey())
|
|
||||||
except IOError, e:
|
|
||||||
log.exception('IOError occurred when loading key file for %s',
|
|
||||||
self.name)
|
|
||||||
|
|
||||||
def loadTrusts(self, newCtxCb=None):
|
|
||||||
''' load the fingerprint trustdb '''
|
|
||||||
# it has the same format as libotr, therefore the
|
|
||||||
# redundant account / proto field
|
|
||||||
try:
|
|
||||||
with open(self.keyFilePath + '.fpr', 'r') as fprFile:
|
|
||||||
for line in fprFile:
|
|
||||||
ctx, acc, proto, fpr, trust = line[:-1].split('\t')
|
|
||||||
|
|
||||||
if acc != self.name or proto != PROTOCOL:
|
|
||||||
continue
|
|
||||||
|
|
||||||
jid = get_jid_from_fjid(ctx)
|
|
||||||
self.setTrust(jid, fpr, trust)
|
|
||||||
except IOError, e:
|
|
||||||
if e.errno != 2:
|
|
||||||
log.exception('IOError occurred when loading fpr file for %s',
|
|
||||||
self.name)
|
|
||||||
|
|
||||||
def saveTrusts(self):
|
|
||||||
try:
|
|
||||||
with open(self.keyFilePath + '.fpr', 'w') as fprFile:
|
|
||||||
for uid, trusts in self.trusts.iteritems():
|
|
||||||
for fpr, trustVal in trusts.iteritems():
|
|
||||||
fprFile.write('\t'.join(
|
|
||||||
(uid, self.name, PROTOCOL, fpr, trustVal)))
|
|
||||||
fprFile.write('\n')
|
|
||||||
except IOError, e:
|
|
||||||
log.exception('IOError occurred when loading fpr file for %s',
|
|
||||||
self.name)
|
|
||||||
except ImportError:
|
|
||||||
HAS_POTR = False
|
|
||||||
|
|
||||||
def otr_dialog_destroy(widget, *args, **kwargs):
|
|
||||||
widget.destroy()
|
|
||||||
|
|
||||||
class OtrPlugin(GajimPlugin):
|
|
||||||
otr = None
|
|
||||||
def init(self):
|
|
||||||
|
|
||||||
self.description = _('See http://www.cypherpunks.ca/otr/')
|
|
||||||
self.us = {}
|
|
||||||
|
|
||||||
|
|
||||||
if not HAS_POTR:
|
|
||||||
self.activatable = False
|
|
||||||
self.available_text = _('Can\'t find potr. Verify this ' \
|
|
||||||
'plugin\'s integrity.')
|
|
||||||
return
|
|
||||||
|
|
||||||
if not HAS_CRYPTO:
|
|
||||||
self.activatable = False
|
|
||||||
self.available_text = _('PyCrypto not installed or too old.')
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
self.config_dialog = ui.OtrPluginConfigDialog(self)
|
|
||||||
self.events_handlers = {}
|
|
||||||
self.events_handlers['message-received'] = (ged.PRECORE,
|
|
||||||
self.handle_incoming_msg)
|
|
||||||
self.events_handlers['message-outgoing'] = (ged.OUT_PRECORE,
|
|
||||||
self.handle_outgoing_msg)
|
|
||||||
|
|
||||||
self.gui_extension_points = {
|
|
||||||
'chat_control' : (self.cc_connect, self.cc_disconnect)
|
|
||||||
}
|
|
||||||
|
|
||||||
for acc in gajim.contacts.get_accounts():
|
|
||||||
self.us[acc] = GajimOtrAccount(self, acc)
|
|
||||||
self.us[acc].loadTrusts()
|
|
||||||
|
|
||||||
acc = str(acc)
|
|
||||||
if acc not in self.config or None not in self.config[acc]:
|
|
||||||
self.config[acc] = {None:DEFAULTFLAGS.copy()}
|
|
||||||
self.update_context_list()
|
|
||||||
|
|
||||||
@log_calls('OtrPlugin')
|
|
||||||
def activate(self):
|
|
||||||
if not HAS_CRYPTO or not HAS_POTR or not hasattr(potr, 'VERSION') \
|
|
||||||
or potr.VERSION < MINVERSION:
|
|
||||||
raise GajimPluginException(self.available_text)
|
|
||||||
|
|
||||||
def get_otr_status(self, account, contact):
|
|
||||||
ctx = self.us[account].getContext(contact.get_full_jid())
|
|
||||||
|
|
||||||
finished = ctx.state == potr.context.STATE_FINISHED
|
|
||||||
encrypted = finished or ctx.state == potr.context.STATE_ENCRYPTED
|
|
||||||
trusted = encrypted and bool(ctx.getCurrentTrust())
|
|
||||||
return (encrypted, trusted, finished)
|
|
||||||
|
|
||||||
def cc_connect(self, cc):
|
|
||||||
def update_otr(print_status=False):
|
|
||||||
enc_status, authenticated, finished = \
|
|
||||||
self.get_otr_status(cc.account, cc.contact)
|
|
||||||
otr_status_text = ''
|
|
||||||
|
|
||||||
if finished:
|
|
||||||
otr_status_text = u'finished OTR connection'
|
|
||||||
elif authenticated:
|
|
||||||
otr_status_text = u'authenticated secure OTR connection'
|
|
||||||
elif enc_status:
|
|
||||||
otr_status_text = u'*unauthenticated* secure OTR connection'
|
|
||||||
|
|
||||||
cc._show_lock_image(enc_status, u'OTR', enc_status, True,
|
|
||||||
authenticated)
|
|
||||||
if print_status and otr_status_text:
|
|
||||||
cc.print_conversation_line(u'[OTR] %s' % otr_status_text,
|
|
||||||
'status', '', None)
|
|
||||||
cc.update_otr = update_otr
|
|
||||||
cc.update_otr(True)
|
|
||||||
|
|
||||||
# hijack authentication button with our submenu
|
|
||||||
def authbutton_cb(widget):
|
|
||||||
if not cc.gpg_is_active and not (cc.session and
|
|
||||||
cc.session.enable_encryption):
|
|
||||||
ui.get_otr_submenu(self, cc).get_submenu().popup(None,
|
|
||||||
None, None, 0, 0)
|
|
||||||
else:
|
|
||||||
cc._on_authentication_button_clicked(widget)
|
|
||||||
self.overwrite_handler(cc, cc.authentication_button, authbutton_cb)
|
|
||||||
|
|
||||||
# hijack context menu
|
|
||||||
cc.orig_prepare_context_menu = cc.prepare_context_menu
|
|
||||||
def inject_menu(hide_buttonbar_items=False):
|
|
||||||
menu = cc.orig_prepare_context_menu(hide_buttonbar_items)
|
|
||||||
menu.insert(ui.get_otr_submenu(self, cc), 8)
|
|
||||||
return menu
|
|
||||||
cc.prepare_context_menu = inject_menu
|
|
||||||
|
|
||||||
def cc_disconnect(self, cc):
|
|
||||||
try:
|
|
||||||
self.overwrite_handler(cc, cc.authentication_button,
|
|
||||||
cc._on_authentication_button_clicked)
|
|
||||||
cc.prepare_context_menu = cc.orig_prepare_context_menu
|
|
||||||
del cc.update_otr
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def menu_settings_cb(self, item, control):
|
|
||||||
ctx = self.us[control.account].getContext(control.contact.get_full_jid())
|
|
||||||
dlg = ui.ContactOtrWindow(self, ctx)
|
|
||||||
dlg.run()
|
|
||||||
dlg.destroy()
|
|
||||||
|
|
||||||
def menu_start_cb(self, item, control):
|
|
||||||
gajim.nec.push_outgoing_event(MessageOutgoingEvent(None,
|
|
||||||
account=control.account, jid=control.contact.jid,
|
|
||||||
message=u'?OTRv?', type_='chat',
|
|
||||||
resource=control.contact.resource, is_loggable=False))
|
|
||||||
|
|
||||||
def menu_end_cb(self, item, control):
|
|
||||||
fjid = control.contact.get_full_jid()
|
|
||||||
thread_id = control.session.thread_id if control.session else None
|
|
||||||
|
|
||||||
self.us[control.account].getContext(fjid).disconnect(
|
|
||||||
appdata={'session':control.session})
|
|
||||||
|
|
||||||
def menu_smp_cb(self, item, control):
|
|
||||||
ctx = self.us[control.account].getContext(control.contact.get_full_jid())
|
|
||||||
ctx.smpWindow.show(False)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def overwrite_handler(window, control, handler):
|
|
||||||
for id_, v in window.handlers.iteritems():
|
|
||||||
if v == control:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise LookupError
|
|
||||||
|
|
||||||
del window.handlers[id_]
|
|
||||||
control.disconnect(id_)
|
|
||||||
id_ = control.connect('clicked', handler)
|
|
||||||
window.handlers[id_] = control
|
|
||||||
|
|
||||||
def set_flags(self, value, account=None, contact=None):
|
|
||||||
if isinstance(account, unicode):
|
|
||||||
account = account.encode()
|
|
||||||
|
|
||||||
if account not in self.config:
|
|
||||||
self.config[account] = {None:DEFAULTFLAGS.copy()}
|
|
||||||
|
|
||||||
if account is None and contact is not None:
|
|
||||||
# don't set per-contact options without account
|
|
||||||
raise Exception("can't set contact flags without account")
|
|
||||||
|
|
||||||
config = self.config[account]
|
|
||||||
config[contact] = value
|
|
||||||
|
|
||||||
self.config[account] = config
|
|
||||||
|
|
||||||
def get_flags(self, account=None, contact=None, fallback=True):
|
|
||||||
if isinstance(account, unicode):
|
|
||||||
account = account.encode()
|
|
||||||
|
|
||||||
setting = DEFAULTFLAGS.copy()
|
|
||||||
if account in self.config:
|
|
||||||
setting.update(self.config[account][None])
|
|
||||||
if contact in self.config[account] \
|
|
||||||
and self.config[account][contact] is not None:
|
|
||||||
setting.update(self.config[account][contact])
|
|
||||||
elif not fallback:
|
|
||||||
return None
|
|
||||||
return setting
|
|
||||||
|
|
||||||
def update_context_list(self):
|
|
||||||
self.config_dialog.fpr_model.clear()
|
|
||||||
for us in self.us.itervalues():
|
|
||||||
usedFpr = set()
|
|
||||||
for fjid, ctx in us.ctxs.iteritems():
|
|
||||||
# get active contexts first
|
|
||||||
key = ctx.getCurrentKey()
|
|
||||||
if not key:
|
|
||||||
continue
|
|
||||||
fpr = key.cfingerprint()
|
|
||||||
usedFpr.add(fpr)
|
|
||||||
|
|
||||||
human_hash = potr.human_hash(fpr)
|
|
||||||
trust = bool(us.getTrust(ctx.trustName, fpr))
|
|
||||||
|
|
||||||
if ctx.state == potr.context.STATE_ENCRYPTED:
|
|
||||||
state = "encrypted"
|
|
||||||
tip = enc_tip
|
|
||||||
elif ctx.state == potr.context.STATE_FINISHED:
|
|
||||||
state = "finished"
|
|
||||||
tip = ended_tip
|
|
||||||
else:
|
|
||||||
state = 'inactive'
|
|
||||||
tip = inactive_tip
|
|
||||||
|
|
||||||
self.config_dialog.fpr_model.append((fjid, state, trust,
|
|
||||||
'<tt>%s</tt>' % human_hash, us.name, tip, fpr))
|
|
||||||
|
|
||||||
for uid, trusts in us.trusts.iteritems():
|
|
||||||
for fpr, trust in trusts.iteritems():
|
|
||||||
if fpr in usedFpr:
|
|
||||||
continue
|
|
||||||
|
|
||||||
state = 'inactive'
|
|
||||||
tip = inactive_tip
|
|
||||||
|
|
||||||
human_hash = potr.human_hash(fpr)
|
|
||||||
|
|
||||||
self.config_dialog.fpr_model.append((uid, state, bool(trust),
|
|
||||||
'<tt>%s</tt>' % human_hash, us.name, tip, fpr))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def gajim_log(cls, msg, account, fjid, no_print=False,
|
|
||||||
is_status_message=True, thread_id=None):
|
|
||||||
if not isinstance(fjid, unicode):
|
|
||||||
fjid = unicode(fjid)
|
|
||||||
if not isinstance(account, unicode):
|
|
||||||
account = unicode(account)
|
|
||||||
|
|
||||||
resource = gajim.get_resource_from_jid(fjid)
|
|
||||||
jid = gajim.get_jid_without_resource(fjid)
|
|
||||||
tim = time.localtime()
|
|
||||||
|
|
||||||
if is_status_message is True:
|
|
||||||
if not no_print:
|
|
||||||
ctrl = cls.get_control(fjid, account)
|
|
||||||
if ctrl:
|
|
||||||
ctrl.print_conversation_line(u'[OTR] %s' % msg, 'status',
|
|
||||||
'', None)
|
|
||||||
id = gajim.logger.write('chat_msg_recv', fjid,
|
|
||||||
message=u'[OTR: %s]' % msg, tim=tim)
|
|
||||||
# gajim.logger.write() only marks a message as unread (and so
|
|
||||||
# only returns an id) when fjid is a real contact (NOT if it's a
|
|
||||||
# GC private chat)
|
|
||||||
if id:
|
|
||||||
gajim.logger.set_read_messages([id])
|
|
||||||
else:
|
|
||||||
session = gajim.connections[account].get_or_create_session(fjid,
|
|
||||||
thread_id)
|
|
||||||
session.received_thread_id |= bool(thread_id)
|
|
||||||
session.last_receive = time.time()
|
|
||||||
|
|
||||||
if not session.control:
|
|
||||||
# look for an existing chat control without a session
|
|
||||||
ctrl = cls.get_control(fjid, account)
|
|
||||||
if ctrl:
|
|
||||||
session.control = ctrl
|
|
||||||
session.control.set_session(session)
|
|
||||||
|
|
||||||
msg_id = gajim.logger.write('chat_msg_recv', fjid,
|
|
||||||
message=u'[OTR: %s]' % msg, tim=tim)
|
|
||||||
session.roster_message(jid, msg, tim=tim, msg_id=msg_id,
|
|
||||||
msg_type='chat', resource=resource)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def update_otr(cls, user, acc, print_status=False):
|
|
||||||
ctrl = cls.get_control(user, acc)
|
|
||||||
if ctrl:
|
|
||||||
ctrl.update_otr(print_status)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_control(fjid, account):
|
|
||||||
# first try to get the window with the full jid
|
|
||||||
ctrl = gajim.interface.msg_win_mgr.get_control(fjid, account)
|
|
||||||
if ctrl:
|
|
||||||
# got one, be happy
|
|
||||||
return ctrl
|
|
||||||
|
|
||||||
# otherwise try without the resource
|
|
||||||
ctrl = gajim.interface.msg_win_mgr.get_control(
|
|
||||||
gajim.get_jid_without_resource(fjid), account)
|
|
||||||
# but only use it when it's not a GC window
|
|
||||||
if ctrl and ctrl.TYPE_ID == TYPE_CHAT:
|
|
||||||
return ctrl
|
|
||||||
|
|
||||||
def handle_incoming_msg(self, event):
|
|
||||||
ctx = None
|
|
||||||
account = event.conn.name
|
|
||||||
accjid = gajim.get_jid_from_account(account)
|
|
||||||
|
|
||||||
if event.encrypted is not False or not event.stanza.getTag('body') \
|
|
||||||
or not isinstance(event.stanza.getBody(), unicode):
|
|
||||||
return PASS
|
|
||||||
|
|
||||||
try:
|
|
||||||
ctx = self.us[account].getContext(event.fjid)
|
|
||||||
msgtxt, tlvs = ctx.receiveMessage(event.msgtxt,
|
|
||||||
appdata={'session':event.session})
|
|
||||||
except potr.context.NotOTRMessage, e:
|
|
||||||
# received message was not OTR - pass it on
|
|
||||||
return PASS
|
|
||||||
except potr.context.UnencryptedMessage, e:
|
|
||||||
# we are encrypted but got some plaintext
|
|
||||||
# display it with a warning
|
|
||||||
tlvs = []
|
|
||||||
msgtxt = _('The following message received from %(jid)s was '
|
|
||||||
'*not encrypted*: [%(error)s]') % {'jid': event.fjid,
|
|
||||||
'error': e.args[0]}
|
|
||||||
except potr.context.NotEncryptedError, e:
|
|
||||||
# we got some encrypted data
|
|
||||||
# but we don't have an encrypted session
|
|
||||||
self.gajim_log(_('The encrypted message received from %s is '
|
|
||||||
'unreadable, as you are not currently communicating '
|
|
||||||
'privately') % event.fjid, account, event.fjid)
|
|
||||||
return IGNORE
|
|
||||||
except potr.context.ErrorReceived, e:
|
|
||||||
# got a protocol error
|
|
||||||
self.gajim_log(_('We received the following OTR error '
|
|
||||||
'message from %(jid)s: [%(error)s]') % {'jid': event.fjid,
|
|
||||||
'error': e.args[0].error},
|
|
||||||
account, event.fjid)
|
|
||||||
return IGNORE
|
|
||||||
except potr.crypt.InvalidParameterError, e:
|
|
||||||
# received a packet we cannot process (probably tampered or
|
|
||||||
# sent to wrong session)
|
|
||||||
self.gajim_log(_('We received an unreadable OTR message '
|
|
||||||
'from %(jid)s. It has probably been tampered with, '
|
|
||||||
'or was sent from an older OTR session.')
|
|
||||||
% {'jid':event.fjid}, account, event.fjid)
|
|
||||||
return IGNORE
|
|
||||||
except RuntimeError, e:
|
|
||||||
# generic library bug?
|
|
||||||
self.gajim_log(_('The following error occurred when trying to '
|
|
||||||
'decrypt a message from %(jid)s: [%(error)s]') % {
|
|
||||||
'jid': event.fjid, 'error': e},
|
|
||||||
account, event.fjid)
|
|
||||||
return IGNORE
|
|
||||||
|
|
||||||
if ctx is not None:
|
|
||||||
ctx.smpWindow.handle_tlv(tlvs)
|
|
||||||
|
|
||||||
stripper = HTMLStripper()
|
|
||||||
stripper.feed(unicode(msgtxt or ''))
|
|
||||||
event.msgtxt = stripper.stripped_data
|
|
||||||
event.stanza.setBody(event.msgtxt)
|
|
||||||
event.stanza.setXHTML(msgtxt)
|
|
||||||
|
|
||||||
return PASS
|
|
||||||
|
|
||||||
def handle_outgoing_msg(self, event):
|
|
||||||
if hasattr(event, 'otrmessage'):
|
|
||||||
return PASS
|
|
||||||
|
|
||||||
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:
|
|
||||||
newmsg = self.us[event.account].getContext(fjid).sendMessage(
|
|
||||||
potr.context.FRAGMENT_SEND_ALL_BUT_LAST, message,
|
|
||||||
appdata={'session':event.session})
|
|
||||||
except potr.context.NotEncryptedError, e:
|
|
||||||
if e.args[0] == potr.context.EXC_FINISHED:
|
|
||||||
self.gajim_log(_('Your message was not send. Either end '
|
|
||||||
'your private conversation, or restart it'), 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(unicode(newmsg or ''))
|
|
||||||
event.message = stripper.stripped_data
|
|
||||||
|
|
||||||
return PASS
|
|
||||||
|
|
||||||
|
|
||||||
class HTMLStripper(HTMLParser):
|
|
||||||
def reset(self):
|
|
||||||
self.stripped_data = ''
|
|
||||||
HTMLParser.reset(self)
|
|
||||||
|
|
||||||
def handle_data(self, data):
|
|
||||||
self.stripped_data += data
|
|
||||||
|
|
||||||
def handle_starttag(self, tag, attrs):
|
|
||||||
if tag == 'br':
|
|
||||||
self.stripped_data += '\n'
|
|
||||||
|
|
||||||
def handle_entityref(self, name):
|
|
||||||
c = unichr(name2codepoint[name])
|
|
||||||
self.stripped_data += c
|
|
||||||
def handle_charref(self, name):
|
|
||||||
if name.startswith('x'):
|
|
||||||
c = unichr(int(name[1:], 16))
|
|
||||||
else:
|
|
||||||
c = unichr(int(name))
|
|
||||||
self.stripped_data += c
|
|
||||||
|
|
||||||
def unknown_decl(self, data):
|
|
||||||
if data.startswith('CDATA['):
|
|
||||||
self.stripped_data += data[6:]
|
|
||||||
|
|
||||||
def feed(self, data):
|
|
||||||
data = data.replace('\n', '')
|
|
||||||
HTMLParser.feed(self, data)
|
|
||||||
|
|
||||||
def escape(s):
|
|
||||||
'''Replace special characters "&", "<" and ">" to HTML-safe sequences.
|
|
||||||
If the optional flag quote is true, the quotation mark character (")
|
|
||||||
is also translated.'''
|
|
||||||
s = s.replace("&", "&") # Must be done first!
|
|
||||||
s = s.replace("<", "<")
|
|
||||||
s = s.replace(">", ">")
|
|
||||||
s = s.replace("\n", "<br/>")
|
|
||||||
return s
|
|
||||||
|
|
||||||
## TODO:
|
|
||||||
## - disconnect ctxs on disconnect
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# Copyright 2011-2012 Kjell Braden <afflux@pentabarf.de>
|
|
||||||
#
|
|
||||||
# This file is part of the python-potr library.
|
|
||||||
#
|
|
||||||
# python-potr is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
# the Free Software Foundation; either version 3 of the License, or
|
|
||||||
# any later version.
|
|
||||||
#
|
|
||||||
# python-potr is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
# some python3 compatibilty
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from potr import context
|
|
||||||
from potr import proto
|
|
||||||
from potr.utils import human_hash
|
|
||||||
|
|
||||||
''' version is: (major, minor, patch, sub) with sub being one of 'alpha',
|
|
||||||
'beta', 'final' '''
|
|
||||||
VERSION = (1, 0, 0, 'beta7')
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Copyright 2012 Kjell Braden <afflux@pentabarf.de>
|
|
||||||
#
|
|
||||||
# This file is part of the python-potr library.
|
|
||||||
#
|
|
||||||
# python-potr is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
# the Free Software Foundation; either version 3 of the License, or
|
|
||||||
# any later version.
|
|
||||||
#
|
|
||||||
# python-potr is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
|
|
||||||
from potr.compatcrypto.common import *
|
|
||||||
|
|
||||||
from potr.compatcrypto.pycrypto import *
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
# Copyright 2012 Kjell Braden <afflux@pentabarf.de>
|
|
||||||
#
|
|
||||||
# This file is part of the python-potr library.
|
|
||||||
#
|
|
||||||
# python-potr is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
# the Free Software Foundation; either version 3 of the License, or
|
|
||||||
# any later version.
|
|
||||||
#
|
|
||||||
# python-potr is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
# some python3 compatibilty
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import struct
|
|
||||||
|
|
||||||
from potr.utils import human_hash, bytes_to_long, unpack, pack_mpi
|
|
||||||
|
|
||||||
DEFAULT_KEYTYPE = 0x0000
|
|
||||||
pkTypes = {}
|
|
||||||
def registerkeytype(cls):
|
|
||||||
if cls.keyType is None:
|
|
||||||
raise TypeError('registered key class needs a type value')
|
|
||||||
pkTypes[cls.keyType] = cls
|
|
||||||
return cls
|
|
||||||
|
|
||||||
def generateDefaultKey():
|
|
||||||
return pkTypes[DEFAULT_KEYTYPE].generate()
|
|
||||||
|
|
||||||
class PK(object):
|
|
||||||
keyType = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def generate(cls):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parsePayload(cls, data, private=False):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def sign(self, data):
|
|
||||||
raise NotImplementedError
|
|
||||||
def verify(self, data):
|
|
||||||
raise NotImplementedError
|
|
||||||
def fingerprint(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def serializePublicKey(self):
|
|
||||||
return struct.pack(b'!H', self.keyType) \
|
|
||||||
+ self.getSerializedPublicPayload()
|
|
||||||
|
|
||||||
def getSerializedPublicPayload(self):
|
|
||||||
buf = b''
|
|
||||||
for x in self.getPublicPayload():
|
|
||||||
buf += pack_mpi(x)
|
|
||||||
return buf
|
|
||||||
|
|
||||||
def getPublicPayload(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def serializePrivateKey(self):
|
|
||||||
return struct.pack(b'!H', self.keyType) \
|
|
||||||
+ self.getSerializedPrivatePayload()
|
|
||||||
|
|
||||||
def getSerializedPrivatePayload(self):
|
|
||||||
buf = b''
|
|
||||||
for x in self.getPrivatePayload():
|
|
||||||
buf += pack_mpi(x)
|
|
||||||
return buf
|
|
||||||
|
|
||||||
def getPrivatePayload(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def cfingerprint(self):
|
|
||||||
return '{0:040x}'.format(bytes_to_long(self.fingerprint()))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parsePrivateKey(cls, data):
|
|
||||||
implCls, data = cls.getImplementation(data)
|
|
||||||
logging.debug('Got privkey of type %r', implCls)
|
|
||||||
return implCls.parsePayload(data, private=True)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parsePublicKey(cls, data):
|
|
||||||
implCls, data = cls.getImplementation(data)
|
|
||||||
logging.debug('Got pubkey of type %r', implCls)
|
|
||||||
return implCls.parsePayload(data)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return human_hash(self.cfingerprint())
|
|
||||||
def __repr__(self):
|
|
||||||
return '<{cls}(fpr=\'{fpr}\')>'.format(
|
|
||||||
cls=self.__class__.__name__, fpr=str(self))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def getImplementation(data):
|
|
||||||
typeid, data = unpack(b'!H', data)
|
|
||||||
cls = pkTypes.get(typeid, None)
|
|
||||||
if cls is None:
|
|
||||||
raise NotImplementedError('unknown typeid %r' % typeid)
|
|
||||||
return cls, data
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
# Copyright 2012 Kjell Braden <afflux@pentabarf.de>
|
|
||||||
#
|
|
||||||
# This file is part of the python-potr library.
|
|
||||||
#
|
|
||||||
# python-potr is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
# the Free Software Foundation; either version 3 of the License, or
|
|
||||||
# any later version.
|
|
||||||
#
|
|
||||||
# python-potr is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
from Crypto import Cipher
|
|
||||||
from Crypto.Hash import SHA256 as _SHA256
|
|
||||||
from Crypto.Hash import SHA as _SHA1
|
|
||||||
from Crypto.Hash import HMAC as _HMAC
|
|
||||||
from Crypto.PublicKey import DSA
|
|
||||||
from Crypto.Random import random
|
|
||||||
from numbers import Number
|
|
||||||
|
|
||||||
from potr.compatcrypto import common
|
|
||||||
from potr.utils import read_mpi, bytes_to_long, long_to_bytes
|
|
||||||
|
|
||||||
def SHA256(data):
|
|
||||||
return _SHA256.new(data).digest()
|
|
||||||
|
|
||||||
def SHA1(data):
|
|
||||||
return _SHA1.new(data).digest()
|
|
||||||
|
|
||||||
def HMAC(key, data, mod):
|
|
||||||
return _HMAC.new(key, msg=data, digestmod=mod).digest()
|
|
||||||
|
|
||||||
def SHA1HMAC(key, data):
|
|
||||||
return HMAC(key, data, _SHA1)
|
|
||||||
|
|
||||||
def SHA256HMAC(key, data):
|
|
||||||
return HMAC(key, data, _SHA256)
|
|
||||||
|
|
||||||
def SHA256HMAC160(key, data):
|
|
||||||
return SHA256HMAC(key, data)[:20]
|
|
||||||
|
|
||||||
def AESCTR(key, counter=0):
|
|
||||||
if isinstance(counter, Number):
|
|
||||||
counter = Counter(counter)
|
|
||||||
if not isinstance(counter, Counter):
|
|
||||||
raise TypeError
|
|
||||||
return Cipher.AES.new(key, Cipher.AES.MODE_CTR, counter=counter)
|
|
||||||
|
|
||||||
class Counter(object):
|
|
||||||
def __init__(self, prefix):
|
|
||||||
self.prefix = prefix
|
|
||||||
self.val = 0
|
|
||||||
|
|
||||||
def inc(self):
|
|
||||||
self.prefix += 1
|
|
||||||
self.val = 0
|
|
||||||
|
|
||||||
def __setattr__(self, attr, val):
|
|
||||||
if attr == 'prefix':
|
|
||||||
self.val = 0
|
|
||||||
super(Counter, self).__setattr__(attr, val)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<Counter(p={p!r},v={v!r})>'.format(p=self.prefix, v=self.val)
|
|
||||||
|
|
||||||
def byteprefix(self):
|
|
||||||
return long_to_bytes(self.prefix, 8)
|
|
||||||
|
|
||||||
def __call__(self):
|
|
||||||
bytesuffix = long_to_bytes(self.val, 8)
|
|
||||||
self.val += 1
|
|
||||||
return self.byteprefix() + bytesuffix
|
|
||||||
|
|
||||||
@common.registerkeytype
|
|
||||||
class DSAKey(common.PK):
|
|
||||||
keyType = 0x0000
|
|
||||||
|
|
||||||
def __init__(self, key=None, private=False):
|
|
||||||
self.priv = self.pub = None
|
|
||||||
|
|
||||||
if not isinstance(key, tuple):
|
|
||||||
raise TypeError('4/5-tuple required for key')
|
|
||||||
|
|
||||||
if len(key) == 5 and private:
|
|
||||||
self.priv = DSA.construct(key)
|
|
||||||
self.pub = self.priv.publickey()
|
|
||||||
elif len(key) == 4 and not private:
|
|
||||||
self.pub = DSA.construct(key)
|
|
||||||
else:
|
|
||||||
raise TypeError('wrong number of arguments for ' \
|
|
||||||
'private={0!r}: got {1} '
|
|
||||||
.format(private, len(key)))
|
|
||||||
|
|
||||||
def getPublicPayload(self):
|
|
||||||
return (self.pub.p, self.pub.q, self.pub.g, self.pub.y)
|
|
||||||
|
|
||||||
def getPrivatePayload(self):
|
|
||||||
return (self.priv.p, self.priv.q, self.priv.g, self.priv.y, self.priv.x)
|
|
||||||
|
|
||||||
def fingerprint(self):
|
|
||||||
return SHA1(self.getSerializedPublicPayload())
|
|
||||||
|
|
||||||
def sign(self, data):
|
|
||||||
# 2 <= K <= q
|
|
||||||
K = random.randrange(2, self.priv.q)
|
|
||||||
r, s = self.priv.sign(data, K)
|
|
||||||
return long_to_bytes(r, 20) + long_to_bytes(s, 20)
|
|
||||||
|
|
||||||
def verify(self, data, sig):
|
|
||||||
r, s = bytes_to_long(sig[:20]), bytes_to_long(sig[20:])
|
|
||||||
return self.pub.verify(data, (r, s))
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return bytes_to_long(self.fingerprint())
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
if not isinstance(other, type(self)):
|
|
||||||
return False
|
|
||||||
return self.fingerprint() == other.fingerprint()
|
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
return not (self == other)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def generate(cls):
|
|
||||||
privkey = DSA.generate(1024)
|
|
||||||
return cls((privkey.key.y, privkey.key.g, privkey.key.p, privkey.key.q,
|
|
||||||
privkey.key.x), private=True)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parsePayload(cls, data, private=False):
|
|
||||||
p, data = read_mpi(data)
|
|
||||||
q, data = read_mpi(data)
|
|
||||||
g, data = read_mpi(data)
|
|
||||||
y, data = read_mpi(data)
|
|
||||||
if private:
|
|
||||||
x, data = read_mpi(data)
|
|
||||||
return cls((y, g, p, q, x), private=True), data
|
|
||||||
return cls((y, g, p, q), private=False), data
|
|
||||||
@@ -1,568 +0,0 @@
|
|||||||
# Copyright 2011-2012 Kjell Braden <afflux@pentabarf.de>
|
|
||||||
#
|
|
||||||
# This file is part of the python-potr library.
|
|
||||||
#
|
|
||||||
# python-potr is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
# the Free Software Foundation; either version 3 of the License, or
|
|
||||||
# any later version.
|
|
||||||
#
|
|
||||||
# python-potr is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
# some python3 compatibilty
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
try:
|
|
||||||
type(basestring)
|
|
||||||
except NameError:
|
|
||||||
# all strings are unicode in python3k
|
|
||||||
basestring = str
|
|
||||||
unicode = str
|
|
||||||
|
|
||||||
# callable is not available in python 3.0 and 3.1
|
|
||||||
try:
|
|
||||||
type(callable)
|
|
||||||
except NameError:
|
|
||||||
from collections import Callable
|
|
||||||
def callable(x):
|
|
||||||
return isinstance(x, Callable)
|
|
||||||
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import logging
|
|
||||||
import struct
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
from potr import crypt
|
|
||||||
from potr import proto
|
|
||||||
from potr import compatcrypto
|
|
||||||
|
|
||||||
from time import time
|
|
||||||
|
|
||||||
EXC_UNREADABLE_MESSAGE = 1
|
|
||||||
EXC_FINISHED = 2
|
|
||||||
|
|
||||||
HEARTBEAT_INTERVAL = 60
|
|
||||||
STATE_PLAINTEXT = 0
|
|
||||||
STATE_ENCRYPTED = 1
|
|
||||||
STATE_FINISHED = 2
|
|
||||||
FRAGMENT_SEND_ALL = 0
|
|
||||||
FRAGMENT_SEND_ALL_BUT_FIRST = 1
|
|
||||||
FRAGMENT_SEND_ALL_BUT_LAST = 2
|
|
||||||
|
|
||||||
OFFER_NOTSENT = 0
|
|
||||||
OFFER_SENT = 1
|
|
||||||
OFFER_REJECTED = 2
|
|
||||||
OFFER_ACCEPTED = 3
|
|
||||||
|
|
||||||
class Context(object):
|
|
||||||
def __init__(self, account, peername):
|
|
||||||
self.user = account
|
|
||||||
self.peer = peername
|
|
||||||
self.policy = {}
|
|
||||||
self.crypto = crypt.CryptEngine(self)
|
|
||||||
self.tagOffer = OFFER_NOTSENT
|
|
||||||
self.mayRetransmit = 0
|
|
||||||
self.lastSend = 0
|
|
||||||
self.lastMessage = None
|
|
||||||
self.state = STATE_PLAINTEXT
|
|
||||||
self.trustName = self.peer
|
|
||||||
|
|
||||||
self.fragmentInfo = None
|
|
||||||
self.fragment = None
|
|
||||||
self.discardFragment()
|
|
||||||
|
|
||||||
def getPolicy(self, key):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def inject(self, msg, appdata=None):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def policyOtrEnabled(self):
|
|
||||||
return self.getPolicy('ALLOW_V2') or self.getPolicy('ALLOW_V1')
|
|
||||||
|
|
||||||
def discardFragment(self):
|
|
||||||
self.fragmentInfo = (0, 0)
|
|
||||||
self.fragment = []
|
|
||||||
|
|
||||||
def fragmentAccumulate(self, message):
|
|
||||||
'''Accumulate a fragmented message. Returns None if the fragment is
|
|
||||||
to be ignored, returns a string if the message is ready for further
|
|
||||||
processing'''
|
|
||||||
|
|
||||||
params = message.split(b',')
|
|
||||||
if len(params) < 5 or not params[1].isdigit() or not params[2].isdigit():
|
|
||||||
logger.warning('invalid formed fragmented message: %r', params)
|
|
||||||
self.discardFragment()
|
|
||||||
return message
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
fragData = params[3]
|
|
||||||
|
|
||||||
logger.debug(params)
|
|
||||||
|
|
||||||
if n >= k == 1:
|
|
||||||
# first fragment
|
|
||||||
self.discardFragment()
|
|
||||||
self.fragmentInfo = (k, n)
|
|
||||||
self.fragment.append(fragData)
|
|
||||||
elif N == n >= k > 1 and k == K+1:
|
|
||||||
# accumulate
|
|
||||||
self.fragmentInfo = (k, n)
|
|
||||||
self.fragment.append(fragData)
|
|
||||||
else:
|
|
||||||
# bad, discard
|
|
||||||
self.discardFragment()
|
|
||||||
logger.warning('invalid fragmented message: %r', params)
|
|
||||||
return message
|
|
||||||
|
|
||||||
if n == k > 0:
|
|
||||||
assembled = b''.join(self.fragment)
|
|
||||||
self.discardFragment()
|
|
||||||
return assembled
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def removeFingerprint(self, fingerprint):
|
|
||||||
self.user.removeFingerprint(self.trustName, fingerprint)
|
|
||||||
|
|
||||||
def setTrust(self, fingerprint, trustLevel):
|
|
||||||
''' sets the trust level for the given fingerprint.
|
|
||||||
trust is usually:
|
|
||||||
- the empty string for known but untrusted keys
|
|
||||||
- 'verified' for manually verified keys
|
|
||||||
- 'smp' for smp-style verified keys '''
|
|
||||||
self.user.setTrust(self.trustName, fingerprint, trustLevel)
|
|
||||||
|
|
||||||
def getTrust(self, fingerprint, default=None):
|
|
||||||
return self.user.getTrust(self.trustName, fingerprint, default)
|
|
||||||
|
|
||||||
def setCurrentTrust(self, trustLevel):
|
|
||||||
self.setTrust(self.crypto.theirPubkey.cfingerprint(), trustLevel)
|
|
||||||
|
|
||||||
def getCurrentKey(self):
|
|
||||||
return self.crypto.theirPubkey
|
|
||||||
|
|
||||||
def getCurrentTrust(self):
|
|
||||||
''' returns a 2-tuple: first element is the current fingerprint,
|
|
||||||
second is:
|
|
||||||
- None if the key is unknown yet
|
|
||||||
- a non-empty string if the key is trusted
|
|
||||||
- an empty string if the key is untrusted '''
|
|
||||||
if self.crypto.theirPubkey is None:
|
|
||||||
return None
|
|
||||||
return self.getTrust(self.crypto.theirPubkey.cfingerprint(), None)
|
|
||||||
|
|
||||||
def receiveMessage(self, messageData, appdata=None):
|
|
||||||
IGN = None, []
|
|
||||||
|
|
||||||
if not self.policyOtrEnabled():
|
|
||||||
raise NotOTRMessage(messageData)
|
|
||||||
|
|
||||||
message = self.parse(messageData)
|
|
||||||
|
|
||||||
if message is None:
|
|
||||||
# nothing to see. move along.
|
|
||||||
return IGN
|
|
||||||
|
|
||||||
logger.debug(repr(message))
|
|
||||||
|
|
||||||
if self.getPolicy('SEND_TAG'):
|
|
||||||
if isinstance(message, basestring):
|
|
||||||
# received a plaintext message without tag
|
|
||||||
# we should not tag anymore
|
|
||||||
self.tagOffer = OFFER_REJECTED
|
|
||||||
else:
|
|
||||||
# got something OTR-ish, cool!
|
|
||||||
self.tagOffer = OFFER_ACCEPTED
|
|
||||||
|
|
||||||
if isinstance(message, proto.Query):
|
|
||||||
self.handleQuery(message, appdata=appdata)
|
|
||||||
|
|
||||||
if isinstance(message, proto.TaggedPlaintext):
|
|
||||||
# it's actually a plaintext message
|
|
||||||
if self.state != STATE_PLAINTEXT or \
|
|
||||||
self.getPolicy('REQUIRE_ENCRYPTION'):
|
|
||||||
# but we don't want plaintexts
|
|
||||||
raise UnencryptedMessage(message.msg)
|
|
||||||
|
|
||||||
raise NotOTRMessage(message.msg)
|
|
||||||
|
|
||||||
return IGN
|
|
||||||
|
|
||||||
if isinstance(message, proto.AKEMessage):
|
|
||||||
self.crypto.handleAKE(message, appdata=appdata)
|
|
||||||
return IGN
|
|
||||||
|
|
||||||
if isinstance(message, proto.DataMessage):
|
|
||||||
ignore = message.flags & proto.MSGFLAGS_IGNORE_UNREADABLE
|
|
||||||
|
|
||||||
if self.state != STATE_ENCRYPTED:
|
|
||||||
self.sendInternal(proto.Error(
|
|
||||||
'You sent encrypted to {user}, who wasn\'t expecting it.'
|
|
||||||
.format(user=self.user.name).encode('utf-8')), appdata=appdata)
|
|
||||||
if ignore:
|
|
||||||
return IGN
|
|
||||||
raise NotEncryptedError(EXC_UNREADABLE_MESSAGE)
|
|
||||||
|
|
||||||
try:
|
|
||||||
plaintext, tlvs = self.crypto.handleDataMessage(message)
|
|
||||||
self.processTLVs(tlvs, appdata=appdata)
|
|
||||||
if plaintext and self.lastSend < time() - HEARTBEAT_INTERVAL:
|
|
||||||
self.sendInternal(b'', appdata=appdata)
|
|
||||||
return plaintext or None, tlvs
|
|
||||||
except crypt.InvalidParameterError:
|
|
||||||
if ignore:
|
|
||||||
return IGN
|
|
||||||
logger.exception('decryption failed')
|
|
||||||
raise
|
|
||||||
if isinstance(message, basestring):
|
|
||||||
if self.state != STATE_PLAINTEXT or \
|
|
||||||
self.getPolicy('REQUIRE_ENCRYPTION'):
|
|
||||||
raise UnencryptedMessage(message)
|
|
||||||
|
|
||||||
if isinstance(message, proto.Error):
|
|
||||||
raise ErrorReceived(message)
|
|
||||||
|
|
||||||
raise NotOTRMessage(messageData)
|
|
||||||
|
|
||||||
def sendInternal(self, msg, tlvs=[], appdata=None):
|
|
||||||
self.sendMessage(FRAGMENT_SEND_ALL, msg, tlvs=tlvs, appdata=appdata,
|
|
||||||
flags=proto.MSGFLAGS_IGNORE_UNREADABLE)
|
|
||||||
|
|
||||||
def sendMessage(self, sendPolicy, msg, flags=0, tlvs=[], appdata=None):
|
|
||||||
if self.policyOtrEnabled():
|
|
||||||
self.lastSend = time()
|
|
||||||
|
|
||||||
if isinstance(msg, proto.OTRMessage):
|
|
||||||
# we want to send a protocol message (probably internal)
|
|
||||||
# so we don't need further protocol encryption
|
|
||||||
# also we can't add TLVs to arbitrary protocol messages
|
|
||||||
if tlvs:
|
|
||||||
raise TypeError('can\'t add tlvs to protocol message')
|
|
||||||
else:
|
|
||||||
# we got plaintext to send. encrypt it
|
|
||||||
msg = self.processOutgoingMessage(msg, flags, tlvs)
|
|
||||||
|
|
||||||
if isinstance(msg, proto.OTRMessage) \
|
|
||||||
and not isinstance(msg, proto.Query):
|
|
||||||
# if it's a query message, it must not get fragmented
|
|
||||||
return self.sendFragmented(bytes(msg), policy=sendPolicy, appdata=appdata)
|
|
||||||
else:
|
|
||||||
msg = bytes(msg)
|
|
||||||
return msg
|
|
||||||
|
|
||||||
def processOutgoingMessage(self, msg, flags, tlvs=[]):
|
|
||||||
isQuery = self.parseExplicitQuery(msg) is not None
|
|
||||||
if isQuery:
|
|
||||||
return self.user.getDefaultQueryMessage(self.getPolicy)
|
|
||||||
|
|
||||||
if self.state == STATE_PLAINTEXT:
|
|
||||||
if self.getPolicy('REQUIRE_ENCRYPTION'):
|
|
||||||
if not isQuery:
|
|
||||||
self.lastMessage = msg
|
|
||||||
self.lastSend = time()
|
|
||||||
self.mayRetransmit = 2
|
|
||||||
# TODO notify
|
|
||||||
msg = self.user.getDefaultQueryMessage(self.getPolicy)
|
|
||||||
return msg
|
|
||||||
if self.getPolicy('SEND_TAG') and self.tagOffer != OFFER_REJECTED:
|
|
||||||
self.tagOffer = OFFER_SENT
|
|
||||||
versions = set()
|
|
||||||
if self.getPolicy('ALLOW_V1'):
|
|
||||||
versions.add(1)
|
|
||||||
if self.getPolicy('ALLOW_V2'):
|
|
||||||
versions.add(2)
|
|
||||||
return proto.TaggedPlaintext(msg, versions)
|
|
||||||
return msg
|
|
||||||
if self.state == STATE_ENCRYPTED:
|
|
||||||
msg = self.crypto.createDataMessage(msg, flags, tlvs)
|
|
||||||
self.lastSend = time()
|
|
||||||
return msg
|
|
||||||
if self.state == STATE_FINISHED:
|
|
||||||
raise NotEncryptedError(EXC_FINISHED)
|
|
||||||
|
|
||||||
def disconnect(self, appdata=None):
|
|
||||||
if self.state != STATE_FINISHED:
|
|
||||||
self.sendInternal(b'', tlvs=[proto.DisconnectTLV()], appdata=appdata)
|
|
||||||
self.setState(STATE_PLAINTEXT)
|
|
||||||
self.crypto.finished()
|
|
||||||
else:
|
|
||||||
self.setState(STATE_PLAINTEXT)
|
|
||||||
|
|
||||||
def setState(self, newstate):
|
|
||||||
self.state = newstate
|
|
||||||
|
|
||||||
def _wentEncrypted(self):
|
|
||||||
self.setState(STATE_ENCRYPTED)
|
|
||||||
|
|
||||||
def sendFragmented(self, msg, policy=FRAGMENT_SEND_ALL, appdata=None):
|
|
||||||
mms = self.maxMessageSize(appdata)
|
|
||||||
msgLen = len(msg)
|
|
||||||
if mms != 0 and msgLen > mms:
|
|
||||||
fms = mms - 19
|
|
||||||
fragments = [ msg[i:i+fms] for i in range(0, msgLen, fms) ]
|
|
||||||
|
|
||||||
fc = len(fragments)
|
|
||||||
|
|
||||||
if fc > 65535:
|
|
||||||
raise OverflowError('too many fragments')
|
|
||||||
|
|
||||||
for fi in range(len(fragments)):
|
|
||||||
ctr = unicode(fi+1) + ',' + unicode(fc) + ','
|
|
||||||
fragments[fi] = b'?OTR,' + ctr.encode('ascii') \
|
|
||||||
+ fragments[fi] + b','
|
|
||||||
|
|
||||||
if policy == FRAGMENT_SEND_ALL:
|
|
||||||
for f in fragments:
|
|
||||||
self.inject(f, appdata=appdata)
|
|
||||||
return None
|
|
||||||
elif policy == FRAGMENT_SEND_ALL_BUT_FIRST:
|
|
||||||
for f in fragments[1:]:
|
|
||||||
self.inject(f, appdata=appdata)
|
|
||||||
return fragments[0]
|
|
||||||
elif policy == FRAGMENT_SEND_ALL_BUT_LAST:
|
|
||||||
for f in fragments[:-1]:
|
|
||||||
self.inject(f, appdata=appdata)
|
|
||||||
return fragments[-1]
|
|
||||||
|
|
||||||
else:
|
|
||||||
if policy == FRAGMENT_SEND_ALL:
|
|
||||||
self.inject(msg, appdata=appdata)
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return msg
|
|
||||||
|
|
||||||
def processTLVs(self, tlvs, appdata=None):
|
|
||||||
for tlv in tlvs:
|
|
||||||
if isinstance(tlv, proto.DisconnectTLV):
|
|
||||||
logger.info('got disconnect tlv, forcing finished state')
|
|
||||||
self.setState(STATE_FINISHED)
|
|
||||||
self.crypto.finished()
|
|
||||||
# TODO cleanup
|
|
||||||
continue
|
|
||||||
if isinstance(tlv, proto.SMPTLV):
|
|
||||||
self.crypto.smpHandle(tlv, appdata=appdata)
|
|
||||||
continue
|
|
||||||
logger.info('got unhandled tlv: {0!r}'.format(tlv))
|
|
||||||
|
|
||||||
def smpAbort(self, appdata=None):
|
|
||||||
if self.state != STATE_ENCRYPTED:
|
|
||||||
raise NotEncryptedError
|
|
||||||
self.crypto.smpAbort(appdata=appdata)
|
|
||||||
|
|
||||||
def smpIsValid(self):
|
|
||||||
return self.crypto.smp and self.crypto.smp.prog != crypt.SMPPROG_CHEATED
|
|
||||||
|
|
||||||
def smpIsSuccess(self):
|
|
||||||
return self.crypto.smp.prog == crypt.SMPPROG_SUCCEEDED \
|
|
||||||
if self.crypto.smp else None
|
|
||||||
|
|
||||||
def smpGotSecret(self, secret, question=None, appdata=None):
|
|
||||||
if self.state != STATE_ENCRYPTED:
|
|
||||||
raise NotEncryptedError
|
|
||||||
self.crypto.smpSecret(secret, question=question, appdata=appdata)
|
|
||||||
|
|
||||||
def smpInit(self, secret, question=None, appdata=None):
|
|
||||||
if self.state != STATE_ENCRYPTED:
|
|
||||||
raise NotEncryptedError
|
|
||||||
self.crypto.smp = None
|
|
||||||
self.crypto.smpSecret(secret, question=question, appdata=appdata)
|
|
||||||
|
|
||||||
def handleQuery(self, message, appdata=None):
|
|
||||||
if 2 in message.versions and self.getPolicy('ALLOW_V2'):
|
|
||||||
self.authStartV2(appdata=appdata)
|
|
||||||
elif 1 in message.versions and self.getPolicy('ALLOW_V1'):
|
|
||||||
self.authStartV1(appdata=appdata)
|
|
||||||
|
|
||||||
def authStartV1(self, appdata=None):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def authStartV2(self, appdata=None):
|
|
||||||
self.crypto.startAKE(appdata=appdata)
|
|
||||||
|
|
||||||
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)
|
|
||||||
if otrTagPos == -1:
|
|
||||||
if proto.MESSAGE_TAG_BASE in message:
|
|
||||||
return proto.TaggedPlaintext.parse(message)
|
|
||||||
else:
|
|
||||||
return message
|
|
||||||
|
|
||||||
indexBase = otrTagPos + len(proto.OTRTAG)
|
|
||||||
|
|
||||||
if len(message) <= indexBase:
|
|
||||||
return message
|
|
||||||
|
|
||||||
compare = message[indexBase]
|
|
||||||
|
|
||||||
if nofragment is False and compare == b','[0]:
|
|
||||||
message = self.fragmentAccumulate(message[indexBase:])
|
|
||||||
if message is None:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return self.parse(message, nofragment=True)
|
|
||||||
else:
|
|
||||||
self.discardFragment()
|
|
||||||
|
|
||||||
queryPayload = self.parseExplicitQuery(message)
|
|
||||||
if queryPayload is not None:
|
|
||||||
return proto.Query.parse(queryPayload)
|
|
||||||
|
|
||||||
if compare == b':'[0] and len(message) > indexBase + 4:
|
|
||||||
try:
|
|
||||||
infoTag = base64.b64decode(message[indexBase+1:indexBase+5])
|
|
||||||
classInfo = struct.unpack(b'!HB', infoTag)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
if message[indexBase:indexBase+7] == b' Error:':
|
|
||||||
return proto.Error(message[indexBase+7:])
|
|
||||||
|
|
||||||
return message
|
|
||||||
|
|
||||||
def maxMessageSize(self, appdata=None):
|
|
||||||
"""Return the max message size for this context."""
|
|
||||||
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):
|
|
||||||
contextclass = Context
|
|
||||||
def __init__(self, name, protocol, maxMessageSize, privkey=None):
|
|
||||||
self.name = name
|
|
||||||
self.privkey = privkey
|
|
||||||
self.policy = {}
|
|
||||||
self.protocol = protocol
|
|
||||||
self.ctxs = {}
|
|
||||||
self.trusts = {}
|
|
||||||
self.maxMessageSize = maxMessageSize
|
|
||||||
self.defaultQuery = '?OTRv{versions}?\n{accountname} has requested ' \
|
|
||||||
'an Off-the-Record private conversation. However, you ' \
|
|
||||||
'do not have a plugin to support that.\nSee '\
|
|
||||||
'http://otr.cypherpunks.ca/ for more information.'
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<{cls}(name={name!r})>'.format(cls=self.__class__.__name__,
|
|
||||||
name=self.name)
|
|
||||||
|
|
||||||
def getPrivkey(self, autogen=True):
|
|
||||||
if self.privkey is None:
|
|
||||||
self.privkey = self.loadPrivkey()
|
|
||||||
if self.privkey is None:
|
|
||||||
if autogen is True:
|
|
||||||
self.privkey = compatcrypto.generateDefaultKey()
|
|
||||||
self.savePrivkey()
|
|
||||||
else:
|
|
||||||
raise LookupError
|
|
||||||
return self.privkey
|
|
||||||
|
|
||||||
def loadPrivkey(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def savePrivkey(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def saveTrusts(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def getContext(self, uid, newCtxCb=None):
|
|
||||||
if uid not in self.ctxs:
|
|
||||||
self.ctxs[uid] = self.contextclass(self, uid)
|
|
||||||
if callable(newCtxCb):
|
|
||||||
newCtxCb(self.ctxs[uid])
|
|
||||||
return self.ctxs[uid]
|
|
||||||
|
|
||||||
def getDefaultQueryMessage(self, policy):
|
|
||||||
v = '2' if policy('ALLOW_V2') else ''
|
|
||||||
msg = self.defaultQuery.format(accountname=self.name, versions=v)
|
|
||||||
return msg.encode('ascii')
|
|
||||||
|
|
||||||
def setTrust(self, key, fingerprint, trustLevel):
|
|
||||||
if key not in self.trusts:
|
|
||||||
self.trusts[key] = {}
|
|
||||||
self.trusts[key][fingerprint] = trustLevel
|
|
||||||
self.saveTrusts()
|
|
||||||
|
|
||||||
def getTrust(self, key, fingerprint, default=None):
|
|
||||||
if key not in self.trusts:
|
|
||||||
return default
|
|
||||||
return self.trusts[key].get(fingerprint, default)
|
|
||||||
|
|
||||||
def removeFingerprint(self, key, fingerprint):
|
|
||||||
if key in self.trusts and fingerprint in self.trusts[key]:
|
|
||||||
del self.trusts[key][fingerprint]
|
|
||||||
|
|
||||||
class NotEncryptedError(RuntimeError):
|
|
||||||
pass
|
|
||||||
class UnencryptedMessage(RuntimeError):
|
|
||||||
pass
|
|
||||||
class ErrorReceived(RuntimeError):
|
|
||||||
pass
|
|
||||||
class NotOTRMessage(RuntimeError):
|
|
||||||
pass
|
|
||||||
@@ -1,795 +0,0 @@
|
|||||||
# Copyright 2011-2012 Kjell Braden <afflux@pentabarf.de>
|
|
||||||
#
|
|
||||||
# This file is part of the python-potr library.
|
|
||||||
#
|
|
||||||
# python-potr is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
# the Free Software Foundation; either version 3 of the License, or
|
|
||||||
# any later version.
|
|
||||||
#
|
|
||||||
# python-potr is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
# some python3 compatibilty
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import struct
|
|
||||||
|
|
||||||
|
|
||||||
from potr.compatcrypto import SHA256, SHA1, SHA1HMAC, SHA256HMAC, \
|
|
||||||
SHA256HMAC160, Counter, AESCTR, PK, random
|
|
||||||
from potr.utils import bytes_to_long, long_to_bytes, pack_mpi, read_mpi
|
|
||||||
from potr import proto
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
STATE_NONE = 0
|
|
||||||
STATE_AWAITING_DHKEY = 1
|
|
||||||
STATE_AWAITING_REVEALSIG = 2
|
|
||||||
STATE_AWAITING_SIG = 4
|
|
||||||
STATE_V1_SETUP = 5
|
|
||||||
|
|
||||||
|
|
||||||
DH_MODULUS = 2410312426921032588552076022197566074856950548502459942654116941958108831682612228890093858261341614673227141477904012196503648957050582631942730706805009223062734745341073406696246014589361659774041027169249453200378729434170325843778659198143763193776859869524088940195577346119843545301547043747207749969763750084308926339295559968882457872412993810129130294592999947926365264059284647209730384947211681434464714438488520940127459844288859336526896320919633919
|
|
||||||
DH_MODULUS_2 = DH_MODULUS-2
|
|
||||||
DH_GENERATOR = 2
|
|
||||||
DH_BITS = 1536
|
|
||||||
DH_MAX = 2**DH_BITS
|
|
||||||
SM_ORDER = (DH_MODULUS - 1) // 2
|
|
||||||
|
|
||||||
def check_group(n):
|
|
||||||
return 2 <= n <= DH_MODULUS_2
|
|
||||||
def check_exp(n):
|
|
||||||
return 1 <= n < SM_ORDER
|
|
||||||
|
|
||||||
class DH(object):
|
|
||||||
@classmethod
|
|
||||||
def set_params(cls, prime, gen):
|
|
||||||
cls.prime = prime
|
|
||||||
cls.gen = gen
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.priv = random.randrange(2, 2**320)
|
|
||||||
self.pub = pow(self.gen, self.priv, self.prime)
|
|
||||||
|
|
||||||
DH.set_params(DH_MODULUS, DH_GENERATOR)
|
|
||||||
|
|
||||||
class DHSession(object):
|
|
||||||
def __init__(self, sendenc, sendmac, rcvenc, rcvmac):
|
|
||||||
self.sendenc = sendenc
|
|
||||||
self.sendmac = sendmac
|
|
||||||
self.rcvenc = rcvenc
|
|
||||||
self.rcvmac = rcvmac
|
|
||||||
self.sendctr = Counter(0)
|
|
||||||
self.rcvctr = Counter(0)
|
|
||||||
self.sendmacused = False
|
|
||||||
self.rcvmacused = False
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<{cls}(send={s!r},rcv={r!r})>' \
|
|
||||||
.format(cls=self.__class__.__name__,
|
|
||||||
s=self.sendmac, r=self.rcvmac)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create(cls, dh, y):
|
|
||||||
s = pow(y, dh.priv, DH_MODULUS)
|
|
||||||
sb = pack_mpi(s)
|
|
||||||
|
|
||||||
if dh.pub > y:
|
|
||||||
sendbyte = b'\1'
|
|
||||||
rcvbyte = b'\2'
|
|
||||||
else:
|
|
||||||
sendbyte = b'\2'
|
|
||||||
rcvbyte = b'\1'
|
|
||||||
|
|
||||||
sendenc = SHA1(sendbyte + sb)[:16]
|
|
||||||
sendmac = SHA1(sendenc)
|
|
||||||
rcvenc = SHA1(rcvbyte + sb)[:16]
|
|
||||||
rcvmac = SHA1(rcvenc)
|
|
||||||
return cls(sendenc, sendmac, rcvenc, rcvmac)
|
|
||||||
|
|
||||||
class CryptEngine(object):
|
|
||||||
def __init__(self, ctx):
|
|
||||||
self.ctx = ctx
|
|
||||||
self.ake = None
|
|
||||||
|
|
||||||
self.sessionId = None
|
|
||||||
self.sessionIdHalf = False
|
|
||||||
self.theirKeyid = 0
|
|
||||||
self.theirY = None
|
|
||||||
self.theirOldY = None
|
|
||||||
|
|
||||||
self.ourOldDHKey = None
|
|
||||||
self.ourDHKey = None
|
|
||||||
self.ourKeyid = 0
|
|
||||||
|
|
||||||
self.sessionkeys = {0:{0:None, 1:None}, 1:{0:None, 1:None}}
|
|
||||||
self.theirPubkey = None
|
|
||||||
self.savedMacKeys = []
|
|
||||||
|
|
||||||
self.smp = None
|
|
||||||
self.extraKey = None
|
|
||||||
|
|
||||||
def revealMacs(self, ours=True):
|
|
||||||
if ours:
|
|
||||||
dhs = self.sessionkeys[1].values()
|
|
||||||
else:
|
|
||||||
dhs = ( v[1] for v in self.sessionkeys.values() )
|
|
||||||
for v in dhs:
|
|
||||||
if v is not None:
|
|
||||||
if v.rcvmacused:
|
|
||||||
self.savedMacKeys.append(v.rcvmac)
|
|
||||||
if v.sendmacused:
|
|
||||||
self.savedMacKeys.append(v.sendmac)
|
|
||||||
|
|
||||||
def rotateDHKeys(self):
|
|
||||||
self.revealMacs(ours=True)
|
|
||||||
self.ourOldDHKey = self.ourDHKey
|
|
||||||
self.sessionkeys[1] = self.sessionkeys[0].copy()
|
|
||||||
self.ourDHKey = DH()
|
|
||||||
self.ourKeyid += 1
|
|
||||||
|
|
||||||
self.sessionkeys[0][0] = None if self.theirY is None else \
|
|
||||||
DHSession.create(self.ourDHKey, self.theirY)
|
|
||||||
self.sessionkeys[0][1] = None if self.theirOldY is None else \
|
|
||||||
DHSession.create(self.ourDHKey, self.theirOldY)
|
|
||||||
|
|
||||||
logger.debug('{0}: Refreshing ourkey to {1} {2}'.format(
|
|
||||||
self.ctx.user.name, self.ourKeyid, self.sessionkeys))
|
|
||||||
|
|
||||||
def rotateYKeys(self, new_y):
|
|
||||||
self.theirOldY = self.theirY
|
|
||||||
self.revealMacs(ours=False)
|
|
||||||
self.sessionkeys[0][1] = self.sessionkeys[0][0]
|
|
||||||
self.sessionkeys[1][1] = self.sessionkeys[1][0]
|
|
||||||
self.theirY = new_y
|
|
||||||
self.theirKeyid += 1
|
|
||||||
|
|
||||||
self.sessionkeys[0][0] = DHSession.create(self.ourDHKey, self.theirY)
|
|
||||||
self.sessionkeys[1][0] = DHSession.create(self.ourOldDHKey, self.theirY)
|
|
||||||
|
|
||||||
logger.debug('{0}: Refreshing theirkey to {1} {2}'.format(
|
|
||||||
self.ctx.user.name, self.theirKeyid, self.sessionkeys))
|
|
||||||
|
|
||||||
def handleDataMessage(self, msg):
|
|
||||||
if self.saneKeyIds(msg) is False:
|
|
||||||
raise InvalidParameterError
|
|
||||||
|
|
||||||
sesskey = self.sessionkeys[self.ourKeyid - msg.rkeyid] \
|
|
||||||
[self.theirKeyid - msg.skeyid]
|
|
||||||
|
|
||||||
logger.debug('sesskeys: {0!r}, our={1}, r={2}, their={3}, s={4}' \
|
|
||||||
.format(self.sessionkeys, self.ourKeyid, msg.rkeyid,
|
|
||||||
self.theirKeyid, msg.skeyid))
|
|
||||||
|
|
||||||
if msg.mac != SHA1HMAC(sesskey.rcvmac, msg.getMacedData()):
|
|
||||||
logger.error('HMACs don\'t match')
|
|
||||||
raise InvalidParameterError
|
|
||||||
sesskey.rcvmacused = True
|
|
||||||
|
|
||||||
newCtrPrefix = bytes_to_long(msg.ctr)
|
|
||||||
if newCtrPrefix <= sesskey.rcvctr.prefix:
|
|
||||||
logger.error('CTR must increase (old %r, new %r)',
|
|
||||||
sesskey.rcvctr.prefix, newCtrPrefix)
|
|
||||||
raise InvalidParameterError
|
|
||||||
|
|
||||||
sesskey.rcvctr.prefix = newCtrPrefix
|
|
||||||
|
|
||||||
logger.debug('handle: enc={0!r} mac={1!r} ctr={2!r}' \
|
|
||||||
.format(sesskey.rcvenc, sesskey.rcvmac, sesskey.rcvctr))
|
|
||||||
|
|
||||||
plaintextData = AESCTR(sesskey.rcvenc, sesskey.rcvctr) \
|
|
||||||
.decrypt(msg.encmsg)
|
|
||||||
|
|
||||||
if b'\0' in plaintextData:
|
|
||||||
plaintext, tlvData = plaintextData.split(b'\0', 1)
|
|
||||||
tlvs = proto.TLV.parse(tlvData)
|
|
||||||
else:
|
|
||||||
plaintext = plaintextData
|
|
||||||
tlvs = []
|
|
||||||
|
|
||||||
if msg.rkeyid == self.ourKeyid:
|
|
||||||
self.rotateDHKeys()
|
|
||||||
if msg.skeyid == self.theirKeyid:
|
|
||||||
self.rotateYKeys(bytes_to_long(msg.dhy))
|
|
||||||
|
|
||||||
return plaintext, tlvs
|
|
||||||
|
|
||||||
def smpSecret(self, secret, question=None, appdata=None):
|
|
||||||
if self.smp is None:
|
|
||||||
logger.debug('Creating SMPHandler')
|
|
||||||
self.smp = SMPHandler(self)
|
|
||||||
|
|
||||||
self.smp.gotSecret(secret, question=question, appdata=appdata)
|
|
||||||
|
|
||||||
def smpHandle(self, tlv, appdata=None):
|
|
||||||
if self.smp is None:
|
|
||||||
logger.debug('Creating SMPHandler')
|
|
||||||
self.smp = SMPHandler(self)
|
|
||||||
self.smp.handle(tlv, appdata=appdata)
|
|
||||||
|
|
||||||
def smpAbort(self, appdata=None):
|
|
||||||
if self.smp is None:
|
|
||||||
logger.debug('Creating SMPHandler')
|
|
||||||
self.smp = SMPHandler(self)
|
|
||||||
self.smp.abort(appdata=appdata)
|
|
||||||
|
|
||||||
def createDataMessage(self, message, flags=0, tlvs=None):
|
|
||||||
# check MSGSTATE
|
|
||||||
if self.theirKeyid == 0:
|
|
||||||
raise InvalidParameterError
|
|
||||||
|
|
||||||
if tlvs is None:
|
|
||||||
tlvs = []
|
|
||||||
|
|
||||||
sess = self.sessionkeys[1][0]
|
|
||||||
sess.sendctr.inc()
|
|
||||||
|
|
||||||
logger.debug('create: enc={0!r} mac={1!r} ctr={2!r}' \
|
|
||||||
.format(sess.sendenc, sess.sendmac, sess.sendctr))
|
|
||||||
|
|
||||||
# plaintext + TLVS
|
|
||||||
plainBuf = message + b'\0' + b''.join([ bytes(t) for t in tlvs])
|
|
||||||
encmsg = AESCTR(sess.sendenc, sess.sendctr).encrypt(plainBuf)
|
|
||||||
|
|
||||||
msg = proto.DataMessage(flags, self.ourKeyid-1, self.theirKeyid,
|
|
||||||
long_to_bytes(self.ourDHKey.pub), sess.sendctr.byteprefix(),
|
|
||||||
encmsg, b'', b''.join(self.savedMacKeys))
|
|
||||||
msg.mac = SHA1HMAC(sess.sendmac, msg.getMacedData())
|
|
||||||
return msg
|
|
||||||
|
|
||||||
def saneKeyIds(self, msg):
|
|
||||||
anyzero = self.theirKeyid == 0 or msg.skeyid == 0 or msg.rkeyid == 0
|
|
||||||
if anyzero or (msg.skeyid != self.theirKeyid and \
|
|
||||||
msg.skeyid != self.theirKeyid - 1) or \
|
|
||||||
(msg.rkeyid != self.ourKeyid and msg.rkeyid != self.ourKeyid - 1):
|
|
||||||
return False
|
|
||||||
if self.theirOldY is None and msg.skeyid == self.theirKeyid - 1:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def startAKE(self, appdata=None):
|
|
||||||
self.ake = AuthKeyExchange(self.ctx.user.getPrivkey(), self.goEncrypted)
|
|
||||||
outMsg = self.ake.startAKE()
|
|
||||||
self.ctx.sendInternal(outMsg, appdata=appdata)
|
|
||||||
|
|
||||||
def handleAKE(self, inMsg, appdata=None):
|
|
||||||
outMsg = None
|
|
||||||
|
|
||||||
if not self.ctx.getPolicy('ALLOW_V2'):
|
|
||||||
return
|
|
||||||
|
|
||||||
if isinstance(inMsg, proto.DHCommit):
|
|
||||||
if self.ake is None or self.ake.state != STATE_AWAITING_REVEALSIG:
|
|
||||||
self.ake = AuthKeyExchange(self.ctx.user.getPrivkey(),
|
|
||||||
self.goEncrypted)
|
|
||||||
outMsg = self.ake.handleDHCommit(inMsg)
|
|
||||||
|
|
||||||
elif isinstance(inMsg, proto.DHKey):
|
|
||||||
if self.ake is None:
|
|
||||||
return # ignore
|
|
||||||
outMsg = self.ake.handleDHKey(inMsg)
|
|
||||||
|
|
||||||
elif isinstance(inMsg, proto.RevealSig):
|
|
||||||
if self.ake is None:
|
|
||||||
return # ignore
|
|
||||||
outMsg = self.ake.handleRevealSig(inMsg)
|
|
||||||
|
|
||||||
elif isinstance(inMsg, proto.Signature):
|
|
||||||
if self.ake is None:
|
|
||||||
return # ignore
|
|
||||||
self.ake.handleSignature(inMsg)
|
|
||||||
|
|
||||||
if outMsg is not None:
|
|
||||||
self.ctx.sendInternal(outMsg, appdata=appdata)
|
|
||||||
|
|
||||||
def goEncrypted(self, ake):
|
|
||||||
if ake.dh.pub == ake.gy:
|
|
||||||
logger.warning('We are receiving our own messages')
|
|
||||||
raise InvalidParameterError
|
|
||||||
|
|
||||||
# TODO handle new fingerprint
|
|
||||||
self.theirPubkey = ake.theirPubkey
|
|
||||||
|
|
||||||
self.sessionId = ake.sessionId
|
|
||||||
self.sessionIdHalf = ake.sessionIdHalf
|
|
||||||
self.theirKeyid = ake.theirKeyid
|
|
||||||
self.ourKeyid = ake.ourKeyid
|
|
||||||
self.theirY = ake.gy
|
|
||||||
self.theirOldY = None
|
|
||||||
self.extraKey = ake.extraKey
|
|
||||||
|
|
||||||
if self.ourKeyid != ake.ourKeyid + 1 or self.ourOldDHKey != ake.dh.pub:
|
|
||||||
self.ourDHKey = ake.dh
|
|
||||||
self.sessionkeys[0][0] = DHSession.create(self.ourDHKey, self.theirY)
|
|
||||||
self.rotateDHKeys()
|
|
||||||
|
|
||||||
# we don't need the AKE anymore, free the reference
|
|
||||||
self.ake = None
|
|
||||||
|
|
||||||
self.ctx._wentEncrypted()
|
|
||||||
logger.info('went encrypted with {0}'.format(self.theirPubkey))
|
|
||||||
|
|
||||||
def finished(self):
|
|
||||||
self.smp = None
|
|
||||||
|
|
||||||
class AuthKeyExchange(object):
|
|
||||||
def __init__(self, privkey, onSuccess):
|
|
||||||
self.privkey = privkey
|
|
||||||
self.state = STATE_NONE
|
|
||||||
self.r = None
|
|
||||||
self.encgx = None
|
|
||||||
self.hashgx = None
|
|
||||||
self.ourKeyid = 1
|
|
||||||
self.theirPubkey = None
|
|
||||||
self.theirKeyid = 1
|
|
||||||
self.enc_c = None
|
|
||||||
self.enc_cp = None
|
|
||||||
self.mac_m1 = None
|
|
||||||
self.mac_m1p = None
|
|
||||||
self.mac_m2 = None
|
|
||||||
self.mac_m2p = None
|
|
||||||
self.sessionId = None
|
|
||||||
self.sessionIdHalf = False
|
|
||||||
self.dh = DH()
|
|
||||||
self.onSuccess = onSuccess
|
|
||||||
self.gy = None
|
|
||||||
self.extraKey = None
|
|
||||||
self.lastmsg = None
|
|
||||||
|
|
||||||
def startAKE(self):
|
|
||||||
self.r = long_to_bytes(random.getrandbits(128))
|
|
||||||
|
|
||||||
gxmpi = pack_mpi(self.dh.pub)
|
|
||||||
|
|
||||||
self.hashgx = SHA256(gxmpi)
|
|
||||||
self.encgx = AESCTR(self.r).encrypt(gxmpi)
|
|
||||||
|
|
||||||
self.state = STATE_AWAITING_DHKEY
|
|
||||||
|
|
||||||
return proto.DHCommit(self.encgx, self.hashgx)
|
|
||||||
|
|
||||||
def handleDHCommit(self, msg):
|
|
||||||
self.encgx = msg.encgx
|
|
||||||
self.hashgx = msg.hashgx
|
|
||||||
|
|
||||||
self.state = STATE_AWAITING_REVEALSIG
|
|
||||||
return proto.DHKey(long_to_bytes(self.dh.pub))
|
|
||||||
|
|
||||||
def handleDHKey(self, msg):
|
|
||||||
if self.state == STATE_AWAITING_DHKEY:
|
|
||||||
self.gy = bytes_to_long(msg.gy)
|
|
||||||
|
|
||||||
# check 2 <= g**y <= p-2
|
|
||||||
if not check_group(self.gy):
|
|
||||||
logger.error('Invalid g**y received: %r', self.gy)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.createAuthKeys()
|
|
||||||
|
|
||||||
aesxb = self.calculatePubkeyAuth(self.enc_c, self.mac_m1)
|
|
||||||
|
|
||||||
self.state = STATE_AWAITING_SIG
|
|
||||||
|
|
||||||
self.lastmsg = proto.RevealSig(self.r, aesxb, b'')
|
|
||||||
self.lastmsg.mac = SHA256HMAC160(self.mac_m2,
|
|
||||||
self.lastmsg.getMacedData())
|
|
||||||
return self.lastmsg
|
|
||||||
|
|
||||||
elif self.state == STATE_AWAITING_SIG:
|
|
||||||
logger.info('received DHKey while not awaiting DHKEY')
|
|
||||||
if msg.gy == self.gy:
|
|
||||||
logger.info('resending revealsig')
|
|
||||||
return self.lastmsg
|
|
||||||
else:
|
|
||||||
logger.info('bad state for DHKey')
|
|
||||||
|
|
||||||
def handleRevealSig(self, msg):
|
|
||||||
if self.state != STATE_AWAITING_REVEALSIG:
|
|
||||||
logger.error('bad state for RevealSig')
|
|
||||||
raise InvalidParameterError
|
|
||||||
|
|
||||||
self.r = msg.rkey
|
|
||||||
gxmpi = AESCTR(self.r).decrypt(self.encgx)
|
|
||||||
if SHA256(gxmpi) != self.hashgx:
|
|
||||||
logger.error('Hashes don\'t match')
|
|
||||||
logger.info('r=%r, hashgx=%r, computed hash=%r, gxmpi=%r',
|
|
||||||
self.r, self.hashgx, SHA256(gxmpi), gxmpi)
|
|
||||||
raise InvalidParameterError
|
|
||||||
|
|
||||||
self.gy = read_mpi(gxmpi)[0]
|
|
||||||
self.createAuthKeys()
|
|
||||||
|
|
||||||
if msg.mac != SHA256HMAC160(self.mac_m2, msg.getMacedData()):
|
|
||||||
logger.error('HMACs don\'t match')
|
|
||||||
logger.info('mac=%r, mac_m2=%r, data=%r', msg.mac, self.mac_m2,
|
|
||||||
msg.getMacedData())
|
|
||||||
raise InvalidParameterError
|
|
||||||
|
|
||||||
self.checkPubkeyAuth(self.enc_c, self.mac_m1, msg.encsig)
|
|
||||||
|
|
||||||
aesxb = self.calculatePubkeyAuth(self.enc_cp, self.mac_m1p)
|
|
||||||
self.sessionIdHalf = True
|
|
||||||
|
|
||||||
self.onSuccess(self)
|
|
||||||
|
|
||||||
self.ourKeyid = 0
|
|
||||||
self.state = STATE_NONE
|
|
||||||
|
|
||||||
cmpmac = struct.pack(b'!I', len(aesxb)) + aesxb
|
|
||||||
|
|
||||||
return proto.Signature(aesxb, SHA256HMAC160(self.mac_m2p, cmpmac))
|
|
||||||
|
|
||||||
def handleSignature(self, msg):
|
|
||||||
if self.state != STATE_AWAITING_SIG:
|
|
||||||
logger.error('bad state (%d) for Signature', self.state)
|
|
||||||
raise InvalidParameterError
|
|
||||||
|
|
||||||
if msg.mac != SHA256HMAC160(self.mac_m2p, msg.getMacedData()):
|
|
||||||
logger.error('HMACs don\'t match')
|
|
||||||
raise InvalidParameterError
|
|
||||||
|
|
||||||
self.checkPubkeyAuth(self.enc_cp, self.mac_m1p, msg.encsig)
|
|
||||||
|
|
||||||
self.sessionIdHalf = False
|
|
||||||
|
|
||||||
self.onSuccess(self)
|
|
||||||
|
|
||||||
self.ourKeyid = 0
|
|
||||||
self.state = STATE_NONE
|
|
||||||
|
|
||||||
def createAuthKeys(self):
|
|
||||||
s = pow(self.gy, self.dh.priv, DH_MODULUS)
|
|
||||||
sbyte = pack_mpi(s)
|
|
||||||
self.sessionId = SHA256(b'\x00' + sbyte)[:8]
|
|
||||||
enc = SHA256(b'\x01' + sbyte)
|
|
||||||
self.enc_c = enc[:16]
|
|
||||||
self.enc_cp = enc[16:]
|
|
||||||
self.mac_m1 = SHA256(b'\x02' + sbyte)
|
|
||||||
self.mac_m2 = SHA256(b'\x03' + sbyte)
|
|
||||||
self.mac_m1p = SHA256(b'\x04' + sbyte)
|
|
||||||
self.mac_m2p = SHA256(b'\x05' + sbyte)
|
|
||||||
self.extraKey = SHA256(b'\xff' + sbyte)
|
|
||||||
|
|
||||||
def calculatePubkeyAuth(self, key, mackey):
|
|
||||||
pubkey = self.privkey.serializePublicKey()
|
|
||||||
buf = pack_mpi(self.dh.pub)
|
|
||||||
buf += pack_mpi(self.gy)
|
|
||||||
buf += pubkey
|
|
||||||
buf += struct.pack(b'!I', self.ourKeyid)
|
|
||||||
MB = self.privkey.sign(SHA256HMAC(mackey, buf))
|
|
||||||
|
|
||||||
buf = pubkey
|
|
||||||
buf += struct.pack(b'!I', self.ourKeyid)
|
|
||||||
buf += MB
|
|
||||||
return AESCTR(key).encrypt(buf)
|
|
||||||
|
|
||||||
def checkPubkeyAuth(self, key, mackey, encsig):
|
|
||||||
auth = AESCTR(key).decrypt(encsig)
|
|
||||||
self.theirPubkey, auth = PK.parsePublicKey(auth)
|
|
||||||
|
|
||||||
receivedKeyid, auth = proto.unpack(b'!I', auth)
|
|
||||||
if receivedKeyid == 0:
|
|
||||||
raise InvalidParameterError
|
|
||||||
|
|
||||||
authbuf = pack_mpi(self.gy)
|
|
||||||
authbuf += pack_mpi(self.dh.pub)
|
|
||||||
authbuf += self.theirPubkey.serializePublicKey()
|
|
||||||
authbuf += struct.pack(b'!I', receivedKeyid)
|
|
||||||
|
|
||||||
if self.theirPubkey.verify(SHA256HMAC(mackey, authbuf), auth) is False:
|
|
||||||
raise InvalidParameterError
|
|
||||||
self.theirKeyid = receivedKeyid
|
|
||||||
|
|
||||||
SMPPROG_OK = 0
|
|
||||||
SMPPROG_CHEATED = -2
|
|
||||||
SMPPROG_FAILED = -1
|
|
||||||
SMPPROG_SUCCEEDED = 1
|
|
||||||
|
|
||||||
class SMPHandler:
|
|
||||||
def __init__(self, crypto):
|
|
||||||
self.crypto = crypto
|
|
||||||
self.state = 1
|
|
||||||
self.g1 = DH_GENERATOR
|
|
||||||
self.g2 = None
|
|
||||||
self.g3 = None
|
|
||||||
self.g3o = None
|
|
||||||
self.x2 = None
|
|
||||||
self.x3 = None
|
|
||||||
self.prog = SMPPROG_OK
|
|
||||||
self.pab = None
|
|
||||||
self.qab = None
|
|
||||||
self.questionReceived = False
|
|
||||||
self.secret = None
|
|
||||||
self.p = None
|
|
||||||
self.q = None
|
|
||||||
|
|
||||||
def abort(self, appdata=None):
|
|
||||||
self.state = 1
|
|
||||||
self.sendTLV(proto.SMPABORTTLV(), appdata=appdata)
|
|
||||||
|
|
||||||
def sendTLV(self, tlv, appdata=None):
|
|
||||||
self.crypto.ctx.sendInternal(b'', tlvs=[tlv], appdata=appdata)
|
|
||||||
|
|
||||||
def handle(self, tlv, appdata=None):
|
|
||||||
logger.debug('handling TLV {0.__class__.__name__}'.format(tlv))
|
|
||||||
self.prog = SMPPROG_CHEATED
|
|
||||||
if isinstance(tlv, proto.SMPABORTTLV):
|
|
||||||
self.state = 1
|
|
||||||
return
|
|
||||||
is1qTlv = isinstance(tlv, proto.SMP1QTLV)
|
|
||||||
if isinstance(tlv, proto.SMP1TLV) or is1qTlv:
|
|
||||||
if self.state != 1:
|
|
||||||
self.abort(appdata=appdata)
|
|
||||||
return
|
|
||||||
|
|
||||||
msg = tlv.mpis
|
|
||||||
|
|
||||||
if not check_group(msg[0]) or not check_group(msg[3]) \
|
|
||||||
or not check_exp(msg[2]) or not check_exp(msg[5]) \
|
|
||||||
or not check_known_log(msg[1], msg[2], self.g1, msg[0], 1) \
|
|
||||||
or not check_known_log(msg[4], msg[5], self.g1, msg[3], 2):
|
|
||||||
logger.error('invalid SMP1TLV received')
|
|
||||||
self.abort(appdata=appdata)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.questionReceived = is1qTlv
|
|
||||||
|
|
||||||
self.g3o = msg[3]
|
|
||||||
|
|
||||||
self.x2 = random.randrange(2, DH_MAX)
|
|
||||||
self.x3 = random.randrange(2, DH_MAX)
|
|
||||||
|
|
||||||
self.g2 = pow(msg[0], self.x2, DH_MODULUS)
|
|
||||||
self.g3 = pow(msg[3], self.x3, DH_MODULUS)
|
|
||||||
|
|
||||||
self.prog = SMPPROG_OK
|
|
||||||
self.state = 0
|
|
||||||
return
|
|
||||||
if isinstance(tlv, proto.SMP2TLV):
|
|
||||||
if self.state != 2:
|
|
||||||
self.abort(appdata=appdata)
|
|
||||||
return
|
|
||||||
|
|
||||||
msg = tlv.mpis
|
|
||||||
mp = msg[6]
|
|
||||||
mq = msg[7]
|
|
||||||
|
|
||||||
if not check_group(msg[0]) or not check_group(msg[3]) \
|
|
||||||
or not check_group(msg[6]) or not check_group(msg[7]) \
|
|
||||||
or not check_exp(msg[2]) or not check_exp(msg[5]) \
|
|
||||||
or not check_exp(msg[9]) or not check_exp(msg[10]) \
|
|
||||||
or not check_known_log(msg[1], msg[2], self.g1, msg[0], 3) \
|
|
||||||
or not check_known_log(msg[4], msg[5], self.g1, msg[3], 4):
|
|
||||||
logger.error('invalid SMP2TLV received')
|
|
||||||
self.abort(appdata=appdata)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.g3o = msg[3]
|
|
||||||
self.g2 = pow(msg[0], self.x2, DH_MODULUS)
|
|
||||||
self.g3 = pow(msg[3], self.x3, DH_MODULUS)
|
|
||||||
|
|
||||||
if not self.check_equal_coords(msg[6:11], 5):
|
|
||||||
logger.error('invalid SMP2TLV received')
|
|
||||||
self.abort(appdata=appdata)
|
|
||||||
return
|
|
||||||
|
|
||||||
r = random.randrange(2, DH_MAX)
|
|
||||||
self.p = pow(self.g3, r, DH_MODULUS)
|
|
||||||
msg = [self.p]
|
|
||||||
qa1 = pow(self.g1, r, DH_MODULUS)
|
|
||||||
qa2 = pow(self.g2, self.secret, DH_MODULUS)
|
|
||||||
self.q = qa1*qa2 % DH_MODULUS
|
|
||||||
msg.append(self.q)
|
|
||||||
msg += self.proof_equal_coords(r, 6)
|
|
||||||
|
|
||||||
inv = invMod(mp)
|
|
||||||
self.pab = self.p * inv % DH_MODULUS
|
|
||||||
inv = invMod(mq)
|
|
||||||
self.qab = self.q * inv % DH_MODULUS
|
|
||||||
|
|
||||||
msg.append(pow(self.qab, self.x3, DH_MODULUS))
|
|
||||||
msg += self.proof_equal_logs(7)
|
|
||||||
|
|
||||||
self.state = 4
|
|
||||||
self.prog = SMPPROG_OK
|
|
||||||
self.sendTLV(proto.SMP3TLV(msg), appdata=appdata)
|
|
||||||
return
|
|
||||||
if isinstance(tlv, proto.SMP3TLV):
|
|
||||||
if self.state != 3:
|
|
||||||
self.abort(appdata=appdata)
|
|
||||||
return
|
|
||||||
|
|
||||||
msg = tlv.mpis
|
|
||||||
|
|
||||||
if not check_group(msg[0]) or not check_group(msg[1]) \
|
|
||||||
or not check_group(msg[5]) or not check_exp(msg[3]) \
|
|
||||||
or not check_exp(msg[4]) or not check_exp(msg[7]) \
|
|
||||||
or not self.check_equal_coords(msg[:5], 6):
|
|
||||||
logger.error('invalid SMP3TLV received')
|
|
||||||
self.abort(appdata=appdata)
|
|
||||||
return
|
|
||||||
|
|
||||||
inv = invMod(self.p)
|
|
||||||
self.pab = msg[0] * inv % DH_MODULUS
|
|
||||||
inv = invMod(self.q)
|
|
||||||
self.qab = msg[1] * inv % DH_MODULUS
|
|
||||||
|
|
||||||
if not self.check_equal_logs(msg[5:8], 7):
|
|
||||||
logger.error('invalid SMP3TLV received')
|
|
||||||
self.abort(appdata=appdata)
|
|
||||||
return
|
|
||||||
|
|
||||||
md = msg[5]
|
|
||||||
msg = [pow(self.qab, self.x3, DH_MODULUS)]
|
|
||||||
msg += self.proof_equal_logs(8)
|
|
||||||
|
|
||||||
rab = pow(md, self.x3, DH_MODULUS)
|
|
||||||
self.prog = SMPPROG_SUCCEEDED if self.pab == rab else SMPPROG_FAILED
|
|
||||||
|
|
||||||
if self.prog != SMPPROG_SUCCEEDED:
|
|
||||||
logger.error('secrets don\'t match')
|
|
||||||
self.abort(appdata=appdata)
|
|
||||||
self.crypto.ctx.setCurrentTrust('')
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info('secrets matched')
|
|
||||||
if not self.questionReceived:
|
|
||||||
self.crypto.ctx.setCurrentTrust('smp')
|
|
||||||
self.state = 1
|
|
||||||
self.sendTLV(proto.SMP4TLV(msg), appdata=appdata)
|
|
||||||
return
|
|
||||||
if isinstance(tlv, proto.SMP4TLV):
|
|
||||||
if self.state != 4:
|
|
||||||
self.abort(appdata=appdata)
|
|
||||||
return
|
|
||||||
|
|
||||||
msg = tlv.mpis
|
|
||||||
|
|
||||||
if not check_group(msg[0]) or not check_exp(msg[2]) \
|
|
||||||
or not self.check_equal_logs(msg[:3], 8):
|
|
||||||
logger.error('invalid SMP4TLV received')
|
|
||||||
self.abort(appdata=appdata)
|
|
||||||
return
|
|
||||||
|
|
||||||
rab = pow(msg[0], self.x3, DH_MODULUS)
|
|
||||||
|
|
||||||
self.prog = SMPPROG_SUCCEEDED if self.pab == rab else SMPPROG_FAILED
|
|
||||||
|
|
||||||
if self.prog != SMPPROG_SUCCEEDED:
|
|
||||||
logger.error('secrets don\'t match')
|
|
||||||
self.abort(appdata=appdata)
|
|
||||||
self.crypto.ctx.setCurrentTrust('')
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info('secrets matched')
|
|
||||||
self.crypto.ctx.setCurrentTrust('smp')
|
|
||||||
self.state = 1
|
|
||||||
return
|
|
||||||
|
|
||||||
def gotSecret(self, secret, question=None, appdata=None):
|
|
||||||
ourFP = self.crypto.ctx.user.getPrivkey().fingerprint()
|
|
||||||
if self.state == 1:
|
|
||||||
# first secret -> SMP1TLV
|
|
||||||
combSecret = SHA256(b'\1' + ourFP +
|
|
||||||
self.crypto.theirPubkey.fingerprint() +
|
|
||||||
self.crypto.sessionId + secret)
|
|
||||||
|
|
||||||
self.secret = bytes_to_long(combSecret)
|
|
||||||
|
|
||||||
self.x2 = random.randrange(2, DH_MAX)
|
|
||||||
self.x3 = random.randrange(2, DH_MAX)
|
|
||||||
|
|
||||||
msg = [pow(self.g1, self.x2, DH_MODULUS)]
|
|
||||||
msg += proof_known_log(self.g1, self.x2, 1)
|
|
||||||
msg.append(pow(self.g1, self.x3, DH_MODULUS))
|
|
||||||
msg += proof_known_log(self.g1, self.x3, 2)
|
|
||||||
|
|
||||||
self.prog = SMPPROG_OK
|
|
||||||
self.state = 2
|
|
||||||
if question is None:
|
|
||||||
self.sendTLV(proto.SMP1TLV(msg), appdata=appdata)
|
|
||||||
else:
|
|
||||||
self.sendTLV(proto.SMP1QTLV(question, msg), appdata=appdata)
|
|
||||||
if self.state == 0:
|
|
||||||
# response secret -> SMP2TLV
|
|
||||||
combSecret = SHA256(b'\1' + self.crypto.theirPubkey.fingerprint() +
|
|
||||||
ourFP + self.crypto.sessionId + secret)
|
|
||||||
|
|
||||||
self.secret = bytes_to_long(combSecret)
|
|
||||||
|
|
||||||
msg = [pow(self.g1, self.x2, DH_MODULUS)]
|
|
||||||
msg += proof_known_log(self.g1, self.x2, 3)
|
|
||||||
msg.append(pow(self.g1, self.x3, DH_MODULUS))
|
|
||||||
msg += proof_known_log(self.g1, self.x3, 4)
|
|
||||||
|
|
||||||
r = random.randrange(2, DH_MAX)
|
|
||||||
|
|
||||||
self.p = pow(self.g3, r, DH_MODULUS)
|
|
||||||
msg.append(self.p)
|
|
||||||
|
|
||||||
qb1 = pow(self.g1, r, DH_MODULUS)
|
|
||||||
qb2 = pow(self.g2, self.secret, DH_MODULUS)
|
|
||||||
self.q = qb1 * qb2 % DH_MODULUS
|
|
||||||
msg.append(self.q)
|
|
||||||
|
|
||||||
msg += self.proof_equal_coords(r, 5)
|
|
||||||
|
|
||||||
self.state = 3
|
|
||||||
self.sendTLV(proto.SMP2TLV(msg), appdata=appdata)
|
|
||||||
|
|
||||||
def proof_equal_coords(self, r, v):
|
|
||||||
r1 = random.randrange(2, DH_MAX)
|
|
||||||
r2 = random.randrange(2, DH_MAX)
|
|
||||||
temp2 = pow(self.g1, r1, DH_MODULUS) \
|
|
||||||
* pow(self.g2, r2, DH_MODULUS) % DH_MODULUS
|
|
||||||
temp1 = pow(self.g3, r1, DH_MODULUS)
|
|
||||||
|
|
||||||
cb = SHA256(struct.pack(b'B', v) + pack_mpi(temp1) + pack_mpi(temp2))
|
|
||||||
c = bytes_to_long(cb)
|
|
||||||
|
|
||||||
temp1 = r * c % SM_ORDER
|
|
||||||
d1 = (r1-temp1) % SM_ORDER
|
|
||||||
|
|
||||||
temp1 = self.secret * c % SM_ORDER
|
|
||||||
d2 = (r2 - temp1) % SM_ORDER
|
|
||||||
return c, d1, d2
|
|
||||||
|
|
||||||
def check_equal_coords(self, coords, v):
|
|
||||||
(p, q, c, d1, d2) = coords
|
|
||||||
temp1 = pow(self.g3, d1, DH_MODULUS) * pow(p, c, DH_MODULUS) \
|
|
||||||
% DH_MODULUS
|
|
||||||
|
|
||||||
temp2 = pow(self.g1, d1, DH_MODULUS) \
|
|
||||||
* pow(self.g2, d2, DH_MODULUS) \
|
|
||||||
* pow(q, c, DH_MODULUS) % DH_MODULUS
|
|
||||||
|
|
||||||
cprime = SHA256(struct.pack(b'B', v) + pack_mpi(temp1) + pack_mpi(temp2))
|
|
||||||
|
|
||||||
return long_to_bytes(c, 32) == cprime
|
|
||||||
|
|
||||||
def proof_equal_logs(self, v):
|
|
||||||
r = random.randrange(2, DH_MAX)
|
|
||||||
temp1 = pow(self.g1, r, DH_MODULUS)
|
|
||||||
temp2 = pow(self.qab, r, DH_MODULUS)
|
|
||||||
|
|
||||||
cb = SHA256(struct.pack(b'B', v) + pack_mpi(temp1) + pack_mpi(temp2))
|
|
||||||
c = bytes_to_long(cb)
|
|
||||||
temp1 = self.x3 * c % SM_ORDER
|
|
||||||
d = (r - temp1) % SM_ORDER
|
|
||||||
return c, d
|
|
||||||
|
|
||||||
def check_equal_logs(self, logs, v):
|
|
||||||
(r, c, d) = logs
|
|
||||||
temp1 = pow(self.g1, d, DH_MODULUS) \
|
|
||||||
* pow(self.g3o, c, DH_MODULUS) % DH_MODULUS
|
|
||||||
|
|
||||||
temp2 = pow(self.qab, d, DH_MODULUS) \
|
|
||||||
* pow(r, c, DH_MODULUS) % DH_MODULUS
|
|
||||||
|
|
||||||
cprime = SHA256(struct.pack(b'B', v) + pack_mpi(temp1) + pack_mpi(temp2))
|
|
||||||
return long_to_bytes(c, 32) == cprime
|
|
||||||
|
|
||||||
def proof_known_log(g, x, v):
|
|
||||||
r = random.randrange(2, DH_MAX)
|
|
||||||
c = bytes_to_long(SHA256(struct.pack(b'B', v) + pack_mpi(pow(g, r, DH_MODULUS))))
|
|
||||||
temp = x * c % SM_ORDER
|
|
||||||
return c, (r-temp) % SM_ORDER
|
|
||||||
|
|
||||||
def check_known_log(c, d, g, x, v):
|
|
||||||
gd = pow(g, d, DH_MODULUS)
|
|
||||||
xc = pow(x, c, DH_MODULUS)
|
|
||||||
gdxc = gd * xc % DH_MODULUS
|
|
||||||
return SHA256(struct.pack(b'B', v) + pack_mpi(gdxc)) == long_to_bytes(c, 32)
|
|
||||||
|
|
||||||
def invMod(n):
|
|
||||||
return pow(n, DH_MODULUS_2, DH_MODULUS)
|
|
||||||
|
|
||||||
class InvalidParameterError(RuntimeError):
|
|
||||||
pass
|
|
||||||
@@ -1,465 +0,0 @@
|
|||||||
# Copyright 2011-2012 Kjell Braden <afflux@pentabarf.de>
|
|
||||||
#
|
|
||||||
# This file is part of the python-potr library.
|
|
||||||
#
|
|
||||||
# python-potr is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
# the Free Software Foundation; either version 3 of the License, or
|
|
||||||
# any later version.
|
|
||||||
#
|
|
||||||
# python-potr is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
# some python3 compatibilty
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import struct
|
|
||||||
from potr.utils import pack_mpi, read_mpi, pack_data, read_data, unpack
|
|
||||||
|
|
||||||
OTRTAG = b'?OTR'
|
|
||||||
MESSAGE_TAG_BASE = b' \t \t\t\t\t \t \t \t '
|
|
||||||
MESSAGE_TAGS = {
|
|
||||||
1:b' \t \t \t ',
|
|
||||||
2:b' \t\t \t ',
|
|
||||||
3:b' \t\t \t\t',
|
|
||||||
}
|
|
||||||
|
|
||||||
MSGTYPE_NOTOTR = 0
|
|
||||||
MSGTYPE_TAGGEDPLAINTEXT = 1
|
|
||||||
MSGTYPE_QUERY = 2
|
|
||||||
MSGTYPE_DH_COMMIT = 3
|
|
||||||
MSGTYPE_DH_KEY = 4
|
|
||||||
MSGTYPE_REVEALSIG = 5
|
|
||||||
MSGTYPE_SIGNATURE = 6
|
|
||||||
MSGTYPE_V1_KEYEXCH = 7
|
|
||||||
MSGTYPE_DATA = 8
|
|
||||||
MSGTYPE_ERROR = 9
|
|
||||||
MSGTYPE_UNKNOWN = -1
|
|
||||||
|
|
||||||
MSGFLAGS_IGNORE_UNREADABLE = 1
|
|
||||||
|
|
||||||
tlvClasses = {}
|
|
||||||
messageClasses = {}
|
|
||||||
|
|
||||||
hasByteStr = bytes == str
|
|
||||||
def bytesAndStrings(cls):
|
|
||||||
if hasByteStr:
|
|
||||||
cls.__str__ = lambda self: self.__bytes__()
|
|
||||||
else:
|
|
||||||
cls.__str__ = lambda self: str(self.__bytes__(), encoding='ascii')
|
|
||||||
return cls
|
|
||||||
|
|
||||||
def registermessage(cls):
|
|
||||||
if not hasattr(cls, 'parsePayload'):
|
|
||||||
raise TypeError('registered message types need parsePayload()')
|
|
||||||
messageClasses[cls.version, cls.msgtype] = cls
|
|
||||||
return cls
|
|
||||||
|
|
||||||
def registertlv(cls):
|
|
||||||
if not hasattr(cls, 'parsePayload'):
|
|
||||||
raise TypeError('registered tlv types need parsePayload()')
|
|
||||||
if cls.typ is None:
|
|
||||||
raise TypeError('registered tlv type needs type ID')
|
|
||||||
tlvClasses[cls.typ] = cls
|
|
||||||
return cls
|
|
||||||
|
|
||||||
|
|
||||||
def getslots(cls, base):
|
|
||||||
''' helper to collect all the message slots from ancestors '''
|
|
||||||
clss = [cls]
|
|
||||||
|
|
||||||
for cls in clss:
|
|
||||||
if cls == base:
|
|
||||||
continue
|
|
||||||
|
|
||||||
clss.extend(cls.__bases__)
|
|
||||||
|
|
||||||
for slot in cls.__slots__:
|
|
||||||
yield slot
|
|
||||||
|
|
||||||
@bytesAndStrings
|
|
||||||
class OTRMessage(object):
|
|
||||||
__slots__ = ['payload']
|
|
||||||
version = 0x0002
|
|
||||||
msgtype = 0
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
if not isinstance(other, self.__class__):
|
|
||||||
return False
|
|
||||||
|
|
||||||
for slot in getslots(self.__class__, OTRMessage):
|
|
||||||
if getattr(self, slot) != getattr(other, slot):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def __neq__(self, other):
|
|
||||||
return not self.__eq__(other)
|
|
||||||
|
|
||||||
class Error(OTRMessage):
|
|
||||||
__slots__ = ['error']
|
|
||||||
def __init__(self, error):
|
|
||||||
super(Error, self).__init__()
|
|
||||||
self.error = error
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<proto.Error(%r)>' % self.error
|
|
||||||
|
|
||||||
def __bytes__(self):
|
|
||||||
return b'?OTR Error:' + self.error
|
|
||||||
|
|
||||||
class Query(OTRMessage):
|
|
||||||
__slots__ = ['versions']
|
|
||||||
def __init__(self, versions=set()):
|
|
||||||
super(Query, self).__init__()
|
|
||||||
self.versions = versions
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parse(cls, data):
|
|
||||||
if not isinstance(data, bytes):
|
|
||||||
raise TypeError('can only parse bytes')
|
|
||||||
udata = data.decode('ascii', errors='replace')
|
|
||||||
|
|
||||||
versions = set()
|
|
||||||
if len(udata) > 0 and udata[0] == '?':
|
|
||||||
udata = udata[1:]
|
|
||||||
versions.add(1)
|
|
||||||
|
|
||||||
if len(udata) > 0 and udata[0] == 'v':
|
|
||||||
versions.update(( int(c) for c in udata if c.isdigit() ))
|
|
||||||
return cls(versions)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<proto.Query(versions=%r)>' % (self.versions)
|
|
||||||
|
|
||||||
def __bytes__(self):
|
|
||||||
d = b'?OTR'
|
|
||||||
if 1 in self.versions:
|
|
||||||
d += b'?'
|
|
||||||
d += b'v'
|
|
||||||
|
|
||||||
# in python3 there is only int->unicode conversion
|
|
||||||
# so I convert to unicode and encode it to a byte string
|
|
||||||
versions = [ '%d' % v for v in self.versions if v != 1 ]
|
|
||||||
d += ''.join(versions).encode('ascii')
|
|
||||||
|
|
||||||
d += b'?'
|
|
||||||
return d
|
|
||||||
|
|
||||||
class TaggedPlaintext(Query):
|
|
||||||
__slots__ = ['msg']
|
|
||||||
def __init__(self, msg, versions):
|
|
||||||
super(TaggedPlaintext, self).__init__(versions)
|
|
||||||
self.msg = msg
|
|
||||||
|
|
||||||
def __bytes__(self):
|
|
||||||
data = self.msg + MESSAGE_TAG_BASE
|
|
||||||
for v in self.versions:
|
|
||||||
data += MESSAGE_TAGS[v]
|
|
||||||
return data
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<proto.TaggedPlaintext(versions={versions!r},msg={msg!r})>' \
|
|
||||||
.format(versions=self.versions, msg=self.msg)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parse(cls, data):
|
|
||||||
tagPos = data.find(MESSAGE_TAG_BASE)
|
|
||||||
if tagPos < 0:
|
|
||||||
raise TypeError(
|
|
||||||
'this is not a tagged plaintext ({0!r:.20})'.format(data))
|
|
||||||
|
|
||||||
tags = [ data[i:i+8] for i in range(tagPos, len(data), 8) ]
|
|
||||||
versions = set([ version for version, tag in MESSAGE_TAGS.items() if tag
|
|
||||||
in tags ])
|
|
||||||
|
|
||||||
return TaggedPlaintext(data[:tagPos], versions)
|
|
||||||
|
|
||||||
class GenericOTRMessage(OTRMessage):
|
|
||||||
__slots__ = ['data']
|
|
||||||
fields = []
|
|
||||||
|
|
||||||
def __init__(self, *args):
|
|
||||||
super(GenericOTRMessage, self).__init__()
|
|
||||||
if len(args) != len(self.fields):
|
|
||||||
raise TypeError('%s needs %d arguments, got %d' %
|
|
||||||
(self.__class__.__name__, len(self.fields), len(args)))
|
|
||||||
|
|
||||||
super(GenericOTRMessage, self).__setattr__('data',
|
|
||||||
dict(zip((f[0] for f in self.fields), args)))
|
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
if attr in self.data:
|
|
||||||
return self.data[attr]
|
|
||||||
raise AttributeError(
|
|
||||||
"'{t!r}' object has no attribute '{attr!r}'".format(attr=attr,
|
|
||||||
t=self.__class__.__name__))
|
|
||||||
|
|
||||||
def __setattr__(self, attr, val):
|
|
||||||
if attr in self.__slots__:
|
|
||||||
super(GenericOTRMessage, self).__setattr__(attr, val)
|
|
||||||
else:
|
|
||||||
self.__getattr__(attr) # existence check
|
|
||||||
self.data[attr] = val
|
|
||||||
|
|
||||||
def __bytes__(self):
|
|
||||||
data = struct.pack(b'!HB', self.version, self.msgtype) \
|
|
||||||
+ self.getPayload()
|
|
||||||
return b'?OTR:' + base64.b64encode(data) + b'.'
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
name = self.__class__.__name__
|
|
||||||
data = ''
|
|
||||||
for k, _ in self.fields:
|
|
||||||
data += '%s=%r,' % (k, self.data[k])
|
|
||||||
return '<proto.%s(%s)>' % (name, data)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parsePayload(cls, data):
|
|
||||||
data = base64.b64decode(data)
|
|
||||||
args = []
|
|
||||||
for _, ftype in cls.fields:
|
|
||||||
if ftype == 'data':
|
|
||||||
value, data = read_data(data)
|
|
||||||
elif isinstance(ftype, bytes):
|
|
||||||
value, data = unpack(ftype, data)
|
|
||||||
elif isinstance(ftype, int):
|
|
||||||
value, data = data[:ftype], data[ftype:]
|
|
||||||
args.append(value)
|
|
||||||
return cls(*args)
|
|
||||||
|
|
||||||
def getPayload(self, *ffilter):
|
|
||||||
payload = b''
|
|
||||||
for k, ftype in self.fields:
|
|
||||||
if k in ffilter:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if ftype == 'data':
|
|
||||||
payload += pack_data(self.data[k])
|
|
||||||
elif isinstance(ftype, bytes):
|
|
||||||
payload += struct.pack(ftype, self.data[k])
|
|
||||||
else:
|
|
||||||
payload += self.data[k]
|
|
||||||
return payload
|
|
||||||
|
|
||||||
class AKEMessage(GenericOTRMessage):
|
|
||||||
__slots__ = []
|
|
||||||
|
|
||||||
@registermessage
|
|
||||||
class DHCommit(AKEMessage):
|
|
||||||
__slots__ = []
|
|
||||||
msgtype = 0x02
|
|
||||||
fields = [('encgx', 'data'), ('hashgx', 'data'), ]
|
|
||||||
|
|
||||||
@registermessage
|
|
||||||
class DHKey(AKEMessage):
|
|
||||||
__slots__ = []
|
|
||||||
msgtype = 0x0a
|
|
||||||
fields = [('gy', 'data'), ]
|
|
||||||
|
|
||||||
@registermessage
|
|
||||||
class RevealSig(AKEMessage):
|
|
||||||
__slots__ = []
|
|
||||||
msgtype = 0x11
|
|
||||||
fields = [('rkey', 'data'), ('encsig', 'data'), ('mac', 20),]
|
|
||||||
|
|
||||||
def getMacedData(self):
|
|
||||||
p = self.encsig
|
|
||||||
return struct.pack(b'!I', len(p)) + p
|
|
||||||
|
|
||||||
@registermessage
|
|
||||||
class Signature(AKEMessage):
|
|
||||||
__slots__ = []
|
|
||||||
msgtype = 0x12
|
|
||||||
fields = [('encsig', 'data'), ('mac', 20)]
|
|
||||||
|
|
||||||
def getMacedData(self):
|
|
||||||
p = self.encsig
|
|
||||||
return struct.pack(b'!I', len(p)) + p
|
|
||||||
|
|
||||||
@registermessage
|
|
||||||
class DataMessage(GenericOTRMessage):
|
|
||||||
__slots__ = []
|
|
||||||
msgtype = 0x03
|
|
||||||
fields = [('flags', b'!B'), ('skeyid', b'!I'), ('rkeyid', b'!I'),
|
|
||||||
('dhy', 'data'), ('ctr', 8), ('encmsg', 'data'), ('mac', 20),
|
|
||||||
('oldmacs', 'data'), ]
|
|
||||||
|
|
||||||
def getMacedData(self):
|
|
||||||
return struct.pack(b'!HB', self.version, self.msgtype) + \
|
|
||||||
self.getPayload('mac', 'oldmacs')
|
|
||||||
|
|
||||||
@bytesAndStrings
|
|
||||||
class TLV(object):
|
|
||||||
__slots__ = []
|
|
||||||
typ = None
|
|
||||||
|
|
||||||
def getPayload(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
val = self.getPayload()
|
|
||||||
return '<{cls}(typ={t},len={l},val={v!r})>'.format(t=self.typ,
|
|
||||||
l=len(val), v=val, cls=self.__class__.__name__)
|
|
||||||
|
|
||||||
def __bytes__(self):
|
|
||||||
val = self.getPayload()
|
|
||||||
return struct.pack(b'!HH', self.typ, len(val)) + val
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parse(cls, data):
|
|
||||||
if not data:
|
|
||||||
return []
|
|
||||||
typ, length, data = unpack(b'!HH', data)
|
|
||||||
return [tlvClasses[typ].parsePayload(data[:length])] \
|
|
||||||
+ cls.parse(data[length:])
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
if not isinstance(other, self.__class__):
|
|
||||||
return False
|
|
||||||
|
|
||||||
for slot in getslots(self.__class__, TLV):
|
|
||||||
if getattr(self, slot) != getattr(other, slot):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def __neq__(self, other):
|
|
||||||
return not self.__eq__(other)
|
|
||||||
|
|
||||||
@registertlv
|
|
||||||
class PaddingTLV(TLV):
|
|
||||||
typ = 0
|
|
||||||
|
|
||||||
__slots__ = ['padding']
|
|
||||||
|
|
||||||
def __init__(self, padding):
|
|
||||||
super(PaddingTLV, self).__init__()
|
|
||||||
self.padding = padding
|
|
||||||
|
|
||||||
def getPayload(self):
|
|
||||||
return self.padding
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parsePayload(cls, data):
|
|
||||||
return cls(data)
|
|
||||||
|
|
||||||
@registertlv
|
|
||||||
class DisconnectTLV(TLV):
|
|
||||||
typ = 1
|
|
||||||
def __init__(self):
|
|
||||||
super(DisconnectTLV, self).__init__()
|
|
||||||
|
|
||||||
def getPayload(self):
|
|
||||||
return b''
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parsePayload(cls, data):
|
|
||||||
if len(data) > 0:
|
|
||||||
raise TypeError('DisconnectTLV must not contain data. got {0!r}'
|
|
||||||
.format(data))
|
|
||||||
return cls()
|
|
||||||
|
|
||||||
class SMPTLV(TLV):
|
|
||||||
__slots__ = ['mpis']
|
|
||||||
dlen = None
|
|
||||||
|
|
||||||
def __init__(self, mpis=None):
|
|
||||||
super(SMPTLV, self).__init__()
|
|
||||||
if mpis is None:
|
|
||||||
mpis = []
|
|
||||||
if self.dlen is None:
|
|
||||||
raise TypeError('no amount of mpis specified in dlen')
|
|
||||||
if len(mpis) != self.dlen:
|
|
||||||
raise TypeError('expected {0} mpis, got {1}'
|
|
||||||
.format(self.dlen, len(mpis)))
|
|
||||||
self.mpis = mpis
|
|
||||||
|
|
||||||
def getPayload(self):
|
|
||||||
d = struct.pack(b'!I', len(self.mpis))
|
|
||||||
for n in self.mpis:
|
|
||||||
d += pack_mpi(n)
|
|
||||||
return d
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parsePayload(cls, data):
|
|
||||||
mpis = []
|
|
||||||
if cls.dlen > 0:
|
|
||||||
count, data = unpack(b'!I', data)
|
|
||||||
for _ in range(count):
|
|
||||||
n, data = read_mpi(data)
|
|
||||||
mpis.append(n)
|
|
||||||
if len(data) > 0:
|
|
||||||
raise TypeError('too much data for {0} mpis'.format(cls.dlen))
|
|
||||||
return cls(mpis)
|
|
||||||
|
|
||||||
@registertlv
|
|
||||||
class SMP1TLV(SMPTLV):
|
|
||||||
typ = 2
|
|
||||||
dlen = 6
|
|
||||||
|
|
||||||
@registertlv
|
|
||||||
class SMP1QTLV(SMPTLV):
|
|
||||||
typ = 7
|
|
||||||
dlen = 6
|
|
||||||
__slots__ = ['msg']
|
|
||||||
|
|
||||||
def __init__(self, msg, mpis):
|
|
||||||
self.msg = msg
|
|
||||||
super(SMP1QTLV, self).__init__(mpis)
|
|
||||||
|
|
||||||
def getPayload(self):
|
|
||||||
return self.msg + b'\0' + super(SMP1QTLV, self).getPayload()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parsePayload(cls, data):
|
|
||||||
msg, data = data.split(b'\0', 1)
|
|
||||||
mpis = SMP1TLV.parsePayload(data).mpis
|
|
||||||
return cls(msg, mpis)
|
|
||||||
|
|
||||||
@registertlv
|
|
||||||
class SMP2TLV(SMPTLV):
|
|
||||||
typ = 3
|
|
||||||
dlen = 11
|
|
||||||
|
|
||||||
@registertlv
|
|
||||||
class SMP3TLV(SMPTLV):
|
|
||||||
typ = 4
|
|
||||||
dlen = 8
|
|
||||||
|
|
||||||
@registertlv
|
|
||||||
class SMP4TLV(SMPTLV):
|
|
||||||
typ = 5
|
|
||||||
dlen = 3
|
|
||||||
|
|
||||||
@registertlv
|
|
||||||
class SMPABORTTLV(SMPTLV):
|
|
||||||
typ = 6
|
|
||||||
dlen = 0
|
|
||||||
|
|
||||||
def getPayload(self):
|
|
||||||
return b''
|
|
||||||
|
|
||||||
@registertlv
|
|
||||||
class ExtraKeyTLV(TLV):
|
|
||||||
typ = 8
|
|
||||||
|
|
||||||
__slots__ = ['appid', 'appdata']
|
|
||||||
|
|
||||||
def __init__(self, appid, appdata):
|
|
||||||
super(ExtraKeyTLV, self).__init__()
|
|
||||||
self.appid = appid
|
|
||||||
self.appdata = appdata
|
|
||||||
if appdata is None:
|
|
||||||
self.appdata = b''
|
|
||||||
|
|
||||||
def getPayload(self):
|
|
||||||
return self.appid + self.appdata
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parsePayload(cls, data):
|
|
||||||
return cls(data[:4], data[4:])
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
# Copyright 2012 Kjell Braden <afflux@pentabarf.de>
|
|
||||||
#
|
|
||||||
# This file is part of the python-potr library.
|
|
||||||
#
|
|
||||||
# python-potr is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
# the Free Software Foundation; either version 3 of the License, or
|
|
||||||
# any later version.
|
|
||||||
#
|
|
||||||
# python-potr is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
# some python3 compatibilty
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
|
|
||||||
import struct
|
|
||||||
|
|
||||||
def pack_mpi(n):
|
|
||||||
return pack_data(long_to_bytes(n))
|
|
||||||
def read_mpi(data):
|
|
||||||
n, data = read_data(data)
|
|
||||||
return bytes_to_long(n), data
|
|
||||||
def pack_data(data):
|
|
||||||
return struct.pack(b'!I', len(data)) + data
|
|
||||||
def read_data(data):
|
|
||||||
datalen, data = unpack(b'!I', data)
|
|
||||||
return data[:datalen], data[datalen:]
|
|
||||||
def unpack(fmt, buf):
|
|
||||||
s = struct.Struct(fmt)
|
|
||||||
return s.unpack(buf[:s.size]) + (buf[s.size:],)
|
|
||||||
|
|
||||||
|
|
||||||
def bytes_to_long(b):
|
|
||||||
l = len(b)
|
|
||||||
s = 0
|
|
||||||
for i in range(l):
|
|
||||||
s += byte_to_long(b[i:i+1]) << 8*(l-i-1)
|
|
||||||
return s
|
|
||||||
|
|
||||||
def long_to_bytes(l, n=0):
|
|
||||||
b = b''
|
|
||||||
while l != 0 or n > 0:
|
|
||||||
b = long_to_byte(l & 0xff) + b
|
|
||||||
l >>= 8
|
|
||||||
n -= 1
|
|
||||||
return b
|
|
||||||
|
|
||||||
def byte_to_long(b):
|
|
||||||
return struct.unpack(b'B', b)[0]
|
|
||||||
def long_to_byte(l):
|
|
||||||
return struct.pack(b'B', l)
|
|
||||||
|
|
||||||
def human_hash(fp):
|
|
||||||
fp = fp.upper()
|
|
||||||
fplen = len(fp)
|
|
||||||
wordsize = fplen//5
|
|
||||||
buf = ''
|
|
||||||
for w in range(0, fplen, wordsize):
|
|
||||||
buf += '{0} '.format(fp[w:w+wordsize])
|
|
||||||
return buf.rstrip()
|
|
||||||
581
gotr/ui.py
@@ -1,581 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
## ui.py
|
|
||||||
##
|
|
||||||
## Copyright 2008-2012 Kjell Braden <afflux@pentabarf.de>
|
|
||||||
##
|
|
||||||
## This file is part of Gajim.
|
|
||||||
##
|
|
||||||
## Gajim is free software; you can redistribute it and/or modify
|
|
||||||
## it under the terms of the GNU General Public License as published
|
|
||||||
## by the Free Software Foundation; version 3 only.
|
|
||||||
##
|
|
||||||
## Gajim is distributed in the hope that it will be useful,
|
|
||||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
## GNU General Public License for more details.
|
|
||||||
##
|
|
||||||
## You should have received a copy of the GNU General Public License
|
|
||||||
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
##
|
|
||||||
import gobject
|
|
||||||
import gtk
|
|
||||||
from common import gajim
|
|
||||||
from plugins.gui import GajimPluginConfigDialog
|
|
||||||
|
|
||||||
import otrmodule
|
|
||||||
try:
|
|
||||||
import potr
|
|
||||||
import potr.proto
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class OtrPluginConfigDialog(GajimPluginConfigDialog):
|
|
||||||
def init(self):
|
|
||||||
self.GTK_BUILDER_FILE_PATH = \
|
|
||||||
self.plugin.local_file_path('config_dialog.ui')
|
|
||||||
self.B = gtk.Builder()
|
|
||||||
self.B.set_translation_domain('gajim_plugins')
|
|
||||||
self.B.add_from_file(self.GTK_BUILDER_FILE_PATH)
|
|
||||||
|
|
||||||
self.fpr_model = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING,
|
|
||||||
gobject.TYPE_BOOLEAN, gobject.TYPE_STRING, gobject.TYPE_STRING,
|
|
||||||
gobject.TYPE_STRING, gobject.TYPE_STRING)
|
|
||||||
|
|
||||||
self.otr_account_store = self.B.get_object('account_store')
|
|
||||||
|
|
||||||
for account in sorted(gajim.contacts.get_accounts()):
|
|
||||||
self.otr_account_store.append(row=(account,))
|
|
||||||
|
|
||||||
self.fpr_view = self.B.get_object('fingerprint_view')
|
|
||||||
self.fpr_view.set_model(self.fpr_model)
|
|
||||||
self.fpr_view.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
|
|
||||||
|
|
||||||
if len(self.otr_account_store) > 0:
|
|
||||||
self.B.get_object('account_combobox').set_active(0)
|
|
||||||
|
|
||||||
self.child.pack_start(self.B.get_object('notebook1'))
|
|
||||||
|
|
||||||
self.flags = dict()
|
|
||||||
flagList = (
|
|
||||||
('ALLOW_V2', 'enable_check'),
|
|
||||||
('SEND_TAG', 'advertise_check'),
|
|
||||||
('WHITESPACE_START_AKE', 'autoinitiate_check'),
|
|
||||||
('REQUIRE_ENCRYPTION', 'require_check')
|
|
||||||
)
|
|
||||||
for flagName, checkBoxName in flagList:
|
|
||||||
self.flags[flagName] = self.B.get_object(checkBoxName)
|
|
||||||
|
|
||||||
self.B.connect_signals(self)
|
|
||||||
|
|
||||||
def on_run(self):
|
|
||||||
self.plugin.update_context_list()
|
|
||||||
self.account_combobox_changed_cb(self.B.get_object('account_combobox'))
|
|
||||||
|
|
||||||
def fpr_button_pressed_cb(self, tw, event):
|
|
||||||
if event.button == 3:
|
|
||||||
pthinfo = tw.get_path_at_pos(int(event.x), int(event.y))
|
|
||||||
|
|
||||||
if pthinfo is None:
|
|
||||||
# only show the popup when we right clicked on list content
|
|
||||||
# ie. don't show it when we click at empty rows
|
|
||||||
return False
|
|
||||||
|
|
||||||
# if the row under the mouse is already selected, we keep the
|
|
||||||
# selection, otherwise we only select the new item
|
|
||||||
keep_selection = tw.get_selection().path_is_selected(pthinfo[0])
|
|
||||||
|
|
||||||
pop = self.B.get_object('fprclipboard_menu')
|
|
||||||
pop.popup(None, None, None, event.button, event.time)
|
|
||||||
|
|
||||||
# keep_selection=True -> no further processing of click event
|
|
||||||
# keep_selection=False-> further processing -> GTK usually selects
|
|
||||||
# the item below the cursor
|
|
||||||
return keep_selection
|
|
||||||
|
|
||||||
def clipboard_button_cb(self, menuitem):
|
|
||||||
mod, paths = self.fpr_view.get_selection().get_selected_rows()
|
|
||||||
|
|
||||||
fprs = []
|
|
||||||
for path in paths:
|
|
||||||
it = mod.get_iter(path)
|
|
||||||
jid, fpr = mod.get(it, 0, 6)
|
|
||||||
fprs.append('%s: %s' % (jid, potr.human_hash(fpr)))
|
|
||||||
gtk.Clipboard().set_text('\n'.join(fprs))
|
|
||||||
gtk.Clipboard(selection='PRIMARY').set_text('\n'.join(fprs))
|
|
||||||
|
|
||||||
def flags_toggled_cb(self, button):
|
|
||||||
if button == self.B.get_object('enable_check'):
|
|
||||||
new_status = button.get_active()
|
|
||||||
self.B.get_object('advertise_check').set_sensitive(new_status)
|
|
||||||
self.B.get_object('autoinitiate_check').set_sensitive(new_status)
|
|
||||||
self.B.get_object('require_check').set_sensitive(new_status)
|
|
||||||
|
|
||||||
if new_status is False:
|
|
||||||
self.B.get_object('advertise_check').set_active(False)
|
|
||||||
self.B.get_object('autoinitiate_check').set_active(False)
|
|
||||||
self.B.get_object('require_check').set_active(False)
|
|
||||||
|
|
||||||
box = self.B.get_object('account_combobox')
|
|
||||||
active = box.get_active()
|
|
||||||
if active > -1:
|
|
||||||
account = self.otr_account_store[active][0]
|
|
||||||
|
|
||||||
flagValues = {}
|
|
||||||
for key, box in self.flags.iteritems():
|
|
||||||
flagValues[key] = box.get_active()
|
|
||||||
self.plugin.set_flags(flagValues, account)
|
|
||||||
|
|
||||||
def account_combobox_changed_cb(self, box, *args):
|
|
||||||
fpr_label = self.B.get_object('fingerprint_label')
|
|
||||||
regen_button = self.B.get_object('regenerate_button')
|
|
||||||
|
|
||||||
active = box.get_active()
|
|
||||||
fpr = '-------- -------- -------- -------- --------'
|
|
||||||
try:
|
|
||||||
if active > -1:
|
|
||||||
regen_button.set_sensitive(True)
|
|
||||||
account = self.otr_account_store[active][0]
|
|
||||||
|
|
||||||
otr_flags = self.plugin.get_flags(account)
|
|
||||||
for key, box in self.flags.iteritems():
|
|
||||||
box.set_active(otr_flags[key])
|
|
||||||
|
|
||||||
fpr = str(self.plugin.us[account].getPrivkey(autogen=False))
|
|
||||||
regen_button.set_label('Regenerate')
|
|
||||||
else:
|
|
||||||
regen_button.set_sensitive(False)
|
|
||||||
except LookupError, e:
|
|
||||||
# Account not found, no private key available - display the
|
|
||||||
# empty one
|
|
||||||
regen_button.set_label('Generate')
|
|
||||||
finally:
|
|
||||||
self.B.get_object('fingerprint_label').set_markup('<tt>%s</tt>'%fpr)
|
|
||||||
|
|
||||||
def forget_button_clicked_cb(self, button, *args):
|
|
||||||
accounts = {}
|
|
||||||
for acc in gajim.connections.iterkeys():
|
|
||||||
accounts[gajim.get_jid_from_account(acc)] = acc
|
|
||||||
|
|
||||||
|
|
||||||
mod, paths = self.fpr_view.get_selection().get_selected_rows()
|
|
||||||
|
|
||||||
for path in paths:
|
|
||||||
it = mod.get_iter(path)
|
|
||||||
user, human_fpr, a, fpr = mod.get(it, 0, 3, 4, 6)
|
|
||||||
|
|
||||||
dlg = gtk.Dialog('Confirm removal of fingerprint', self,
|
|
||||||
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
|
|
||||||
(gtk.STOCK_YES, gtk.RESPONSE_YES,
|
|
||||||
gtk.STOCK_NO, gtk.RESPONSE_NO)
|
|
||||||
)
|
|
||||||
l = gtk.Label()
|
|
||||||
l.set_markup('Are you sure you want remove the following '
|
|
||||||
'fingerprint for the contact %s on the account %s?'
|
|
||||||
'\n\n%s' % (user, a, human_fpr))
|
|
||||||
l.set_line_wrap(True)
|
|
||||||
dlg.vbox.pack_start(l)
|
|
||||||
dlg.show_all()
|
|
||||||
|
|
||||||
if dlg.run() == gtk.RESPONSE_YES:
|
|
||||||
ctx = self.plugin.us[accounts[a]].getContext(user)
|
|
||||||
ctx.removeFingerprint(fpr)
|
|
||||||
dlg.destroy()
|
|
||||||
self.plugin.us[accounts[a]].saveTrusts()
|
|
||||||
|
|
||||||
self.plugin.update_context_list()
|
|
||||||
|
|
||||||
def verify_button_clicked_cb(self, button, *args):
|
|
||||||
accounts = {}
|
|
||||||
for acc in gajim.connections.iterkeys():
|
|
||||||
accounts[gajim.get_jid_from_account(acc)] = acc
|
|
||||||
|
|
||||||
mod, paths = self.fpr_view.get_selection().get_selected_rows()
|
|
||||||
|
|
||||||
# open the window for the first selected row
|
|
||||||
for path in paths[0:1]:
|
|
||||||
it = mod.get_iter(path)
|
|
||||||
fjid, fpr, a = mod.get(it, 0, 6, 4)
|
|
||||||
|
|
||||||
ctx = self.plugin.us[accounts[a]].getContext(fjid)
|
|
||||||
|
|
||||||
dlg = ContactOtrWindow(self.plugin, ctx, fpr=fpr, parent=self)
|
|
||||||
dlg.run()
|
|
||||||
dlg.destroy()
|
|
||||||
break
|
|
||||||
|
|
||||||
def regenerate_button_clicked_cb(self, button, *args):
|
|
||||||
box = self.B.get_object('account_combobox')
|
|
||||||
active = box.get_active()
|
|
||||||
if active > -1:
|
|
||||||
account = self.otr_account_store[active][0]
|
|
||||||
button.set_sensitive(False)
|
|
||||||
try:
|
|
||||||
self.plugin.us[account].getPrivkey(autogen=False)
|
|
||||||
self.plugin.us[account].dropPrivkey()
|
|
||||||
except LookupError:
|
|
||||||
pass
|
|
||||||
self.plugin.us[account].getPrivkey(autogen=True)
|
|
||||||
self.account_combobox_changed_cb(box, *args)
|
|
||||||
button.set_sensitive(True)
|
|
||||||
|
|
||||||
|
|
||||||
import gtkgui_helpers
|
|
||||||
from common import gajim
|
|
||||||
|
|
||||||
our_fp_text = _('Your fingerprint:\n' \
|
|
||||||
'<span weight="bold" face="monospace">%s</span>')
|
|
||||||
their_fp_text = _('Purported fingerprint for %(jid)s:\n' \
|
|
||||||
'<span weight="bold" face="monospace">%(fp)s</span>')
|
|
||||||
|
|
||||||
another_q = _('You may want to authenticate your buddy as well by asking'\
|
|
||||||
'your own question.')
|
|
||||||
smp_query = _('<b>%s is trying to authenticate you using a secret only known '\
|
|
||||||
'to him/her and you.</b>')
|
|
||||||
smp_q_query = _('<b>%s has chosen a question for you to answer to '\
|
|
||||||
'authenticate yourself:</b>')
|
|
||||||
enter_secret = _('Please enter your secret below.')
|
|
||||||
|
|
||||||
smp_init = _('<b>You are trying to authenticate %s using a secret only known ' \
|
|
||||||
'to him/her and yourself.</b>')
|
|
||||||
choose_q = _('You can choose a question as a hint for your buddy below.')
|
|
||||||
|
|
||||||
class ContactOtrSmpWindow:
|
|
||||||
def gw(self, n):
|
|
||||||
return self.xml.get_object(n)
|
|
||||||
|
|
||||||
def __init__(self, ctx):
|
|
||||||
self.question = None
|
|
||||||
self.smp_running = False
|
|
||||||
self.ctx = ctx
|
|
||||||
self.account = ctx.user.accountname
|
|
||||||
|
|
||||||
self.plugin = ctx.user.plugin
|
|
||||||
|
|
||||||
self.GTK_BUILDER_FILE_PATH = \
|
|
||||||
self.plugin.local_file_path('contact_otr_window.ui')
|
|
||||||
self.xml = gtk.Builder()
|
|
||||||
self.xml.set_translation_domain('gajim_plugins')
|
|
||||||
self.xml.add_from_file(self.GTK_BUILDER_FILE_PATH)
|
|
||||||
|
|
||||||
self.window = self.gw('otr_smp_window')
|
|
||||||
self.window.set_title(_('OTR settings for %s') % ctx.peer)
|
|
||||||
|
|
||||||
# the lambda thing is an anonymous helper that just discards the
|
|
||||||
# parameters and calls hide_on_delete on clicking the window's
|
|
||||||
# close button
|
|
||||||
self.window.connect('delete-event', lambda d,o:
|
|
||||||
self.window.hide_on_delete())
|
|
||||||
|
|
||||||
self.gw('smp_cancel_button').connect('clicked', self._on_destroy)
|
|
||||||
self.gw('smp_ok_button').connect('clicked', self._apply)
|
|
||||||
self.gw('qcheckbutton').connect('toggled', self._toggle)
|
|
||||||
|
|
||||||
self.gw('qcheckbutton').set_no_show_all(False)
|
|
||||||
self.gw('qentry').set_no_show_all(False)
|
|
||||||
self.gw('desclabel2').set_no_show_all(False)
|
|
||||||
|
|
||||||
def _toggle(self, w, *args):
|
|
||||||
self.gw('qentry').set_sensitive(w.get_active())
|
|
||||||
|
|
||||||
def show(self, response):
|
|
||||||
self.smp_running = False
|
|
||||||
self.finished = False
|
|
||||||
|
|
||||||
self.gw('smp_cancel_button').set_sensitive(True)
|
|
||||||
self.gw('smp_ok_button').set_sensitive(True)
|
|
||||||
self.gw('progressbar').set_fraction(0)
|
|
||||||
self.gw('secret_entry').set_text('')
|
|
||||||
|
|
||||||
self.response = response
|
|
||||||
self.window.show_all()
|
|
||||||
if response:
|
|
||||||
self.gw('qcheckbutton').set_sensitive(False)
|
|
||||||
if self.question is None:
|
|
||||||
self.gw('qcheckbutton').set_active(False)
|
|
||||||
self.gw('qcheckbutton').hide()
|
|
||||||
self.gw('qentry').hide()
|
|
||||||
self.gw('desclabel2').hide()
|
|
||||||
self.gw('qcheckbutton').set_sensitive(False)
|
|
||||||
self.gw('desclabel1').set_markup((smp_query % self.ctx.peer)
|
|
||||||
+ ' ' + enter_secret)
|
|
||||||
else:
|
|
||||||
self.gw('qcheckbutton').set_active(True)
|
|
||||||
self.gw('qcheckbutton').show()
|
|
||||||
self.gw('qentry').show()
|
|
||||||
self.gw('qentry').set_sensitive(True)
|
|
||||||
self.gw('qentry').set_editable(False)
|
|
||||||
self.gw('desclabel2').show()
|
|
||||||
self.gw('qentry').set_text(self.question)
|
|
||||||
|
|
||||||
self.gw('desclabel1').set_markup(smp_q_query % self.ctx.peer)
|
|
||||||
self.gw('desclabel2').set_markup(enter_secret)
|
|
||||||
else:
|
|
||||||
self.gw('qcheckbutton').show()
|
|
||||||
self.gw('qcheckbutton').set_active(True)
|
|
||||||
self.gw('qcheckbutton').set_mode(True)
|
|
||||||
self.gw('qcheckbutton').set_sensitive(True)
|
|
||||||
self.gw('qentry').set_sensitive(True)
|
|
||||||
self.gw('qentry').show()
|
|
||||||
self.gw('qentry').set_text("")
|
|
||||||
|
|
||||||
self.gw('qentry').set_editable(True)
|
|
||||||
self.gw('qentry').set_sensitive(True)
|
|
||||||
|
|
||||||
self.gw('desclabel2').show()
|
|
||||||
self.gw('desclabel1').set_markup((smp_init % self.ctx.peer) + ' '
|
|
||||||
+ choose_q)
|
|
||||||
self.gw('desclabel2').set_markup(enter_secret)
|
|
||||||
|
|
||||||
def _abort(self, text=None, appdata=None):
|
|
||||||
self.smp_running = False
|
|
||||||
|
|
||||||
self.ctx.smpAbort(appdata=appdata)
|
|
||||||
if text:
|
|
||||||
self.plugin.gajim_log(text, self.account, self.ctx.peer)
|
|
||||||
|
|
||||||
def _finish(self, text):
|
|
||||||
self.smp_running = False
|
|
||||||
self.finished = True
|
|
||||||
|
|
||||||
self.gw('qcheckbutton').set_active(False)
|
|
||||||
self.gw('qcheckbutton').hide()
|
|
||||||
self.gw('qentry').hide()
|
|
||||||
self.gw('desclabel2').hide()
|
|
||||||
|
|
||||||
self.gw('qcheckbutton').set_sensitive(False)
|
|
||||||
self.gw('smp_cancel_button').set_sensitive(False)
|
|
||||||
self.gw('smp_ok_button').set_sensitive(True)
|
|
||||||
self.gw('progressbar').set_fraction(1)
|
|
||||||
self.plugin.gajim_log(text, self.account, self.ctx.peer)
|
|
||||||
self.gw('desclabel1').set_markup(text)
|
|
||||||
|
|
||||||
self.plugin.update_otr(self.ctx.peer, self.account, True)
|
|
||||||
self.ctx.user.saveTrusts()
|
|
||||||
self.plugin.update_context_list()
|
|
||||||
|
|
||||||
def get_tlv(self, tlvs, check):
|
|
||||||
for tlv in tlvs:
|
|
||||||
if isinstance(tlv, check):
|
|
||||||
return tlv
|
|
||||||
return None
|
|
||||||
|
|
||||||
def handle_tlv(self, tlvs):
|
|
||||||
if tlvs:
|
|
||||||
is1qtlv = self.get_tlv(tlvs, potr.proto.SMP1QTLV)
|
|
||||||
# check for TLV_SMP_ABORT or state = CHEATED
|
|
||||||
if self.smp_running and not self.ctx.smpIsValid():
|
|
||||||
self._finish(_('SMP verifying aborted'))
|
|
||||||
|
|
||||||
# check for TLV_SMP1
|
|
||||||
elif self.get_tlv(tlvs, potr.proto.SMP1TLV):
|
|
||||||
self.smp_running = True
|
|
||||||
self.question = None
|
|
||||||
self.show(True)
|
|
||||||
self.gw('progressbar').set_fraction(0.3)
|
|
||||||
|
|
||||||
# check for TLV_SMP1Q
|
|
||||||
elif is1qtlv:
|
|
||||||
self.smp_running = True
|
|
||||||
self.question = is1qtlv.msg
|
|
||||||
self.show(True)
|
|
||||||
self.gw('progressbar').set_fraction(0.3)
|
|
||||||
|
|
||||||
# check for TLV_SMP2
|
|
||||||
elif self.get_tlv(tlvs, potr.proto.SMP2TLV):
|
|
||||||
self.gw('progressbar').set_fraction(0.6)
|
|
||||||
|
|
||||||
# check for TLV_SMP3
|
|
||||||
elif self.get_tlv(tlvs, potr.proto.SMP3TLV):
|
|
||||||
if self.ctx.smpIsSuccess():
|
|
||||||
text = _('SMP verifying succeeded')
|
|
||||||
if self.question is not None:
|
|
||||||
text += ' '+another_q
|
|
||||||
self._finish(text)
|
|
||||||
else:
|
|
||||||
self._finish(_('SMP verifying failed'))
|
|
||||||
|
|
||||||
# check for TLV_SMP4
|
|
||||||
elif self.get_tlv(tlvs, potr.proto.SMP4TLV):
|
|
||||||
if self.ctx.smpIsSuccess():
|
|
||||||
text = _('SMP verifying succeeded')
|
|
||||||
if self.question is not None:
|
|
||||||
text += ' '+another_q
|
|
||||||
self._finish(text)
|
|
||||||
else:
|
|
||||||
self._finish(_('SMP verifying failed'))
|
|
||||||
|
|
||||||
def _on_destroy(self, widget):
|
|
||||||
if self.smp_running:
|
|
||||||
self._abort(_('user aborted SMP authentication'))
|
|
||||||
self.window.hide_all()
|
|
||||||
|
|
||||||
def _apply(self, widget, appdata=None):
|
|
||||||
if self.finished:
|
|
||||||
self.window.hide_all()
|
|
||||||
return
|
|
||||||
secret = self.gw('secret_entry').get_text()
|
|
||||||
if self.response:
|
|
||||||
self.ctx.smpGotSecret(secret, appdata=appdata)
|
|
||||||
else:
|
|
||||||
if self.gw('qcheckbutton').get_active():
|
|
||||||
qtext = self.gw('qentry').get_text()
|
|
||||||
self.ctx.smpInit(secret, question=qtext, appdata=appdata)
|
|
||||||
else:
|
|
||||||
self.ctx.smpInit(secret, appdata=appdata)
|
|
||||||
self.gw('progressbar').set_fraction(0.3)
|
|
||||||
self.smp_running = True
|
|
||||||
widget.set_sensitive(False)
|
|
||||||
|
|
||||||
class ContactOtrWindow(gtk.Dialog):
|
|
||||||
def gw(self, n):
|
|
||||||
return self.xml.get_object(n)
|
|
||||||
|
|
||||||
def __init__(self, plugin, ctx, fpr=None, parent=None):
|
|
||||||
fjid = ctx.peer
|
|
||||||
gtk.Dialog.__init__(self, title=_('OTR settings for %s') % fjid,
|
|
||||||
parent=parent,
|
|
||||||
flags=gtk.DIALOG_DESTROY_WITH_PARENT,
|
|
||||||
buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
|
|
||||||
gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
|
|
||||||
|
|
||||||
self.ctx = ctx
|
|
||||||
self.fjid = fjid
|
|
||||||
self.jid = gajim.get_room_and_nick_from_fjid(self.fjid)[0]
|
|
||||||
self.account = ctx.user.accountname
|
|
||||||
self.fpr = fpr
|
|
||||||
self.plugin = plugin
|
|
||||||
|
|
||||||
if self.fpr is None:
|
|
||||||
key = self.ctx.getCurrentKey()
|
|
||||||
if key is not None:
|
|
||||||
self.fpr = key.cfingerprint()
|
|
||||||
|
|
||||||
self.GTK_BUILDER_FILE_PATH = \
|
|
||||||
self.plugin.local_file_path('contact_otr_window.ui')
|
|
||||||
self.xml = gtk.Builder()
|
|
||||||
self.xml.set_translation_domain('gajim_plugins')
|
|
||||||
self.xml.add_from_file(self.GTK_BUILDER_FILE_PATH)
|
|
||||||
self.notebook = self.gw('otr_settings_notebook')
|
|
||||||
self.child.pack_start(self.notebook)
|
|
||||||
|
|
||||||
self.connect('response', self.on_response)
|
|
||||||
self.gw('otr_default_checkbutton').connect('toggled',
|
|
||||||
self._otr_default_checkbutton_toggled)
|
|
||||||
|
|
||||||
# always set the label containing our fingerprint
|
|
||||||
self.gw('our_fp_label').set_markup(our_fp_text % ctx.user.getPrivkey())
|
|
||||||
|
|
||||||
if self.fpr is None:
|
|
||||||
# make the fingerprint widgets insensitive
|
|
||||||
# when not encrypted
|
|
||||||
for widget in self.gw('otr_fp_vbox').get_children():
|
|
||||||
widget.set_sensitive(False)
|
|
||||||
# show that the fingerprint is unknown
|
|
||||||
self.gw('their_fp_label').set_markup(their_fp_text % {
|
|
||||||
'jid': self.fjid, 'fp': _('unknown')})
|
|
||||||
self.gw('verified_combobox').set_active(-1)
|
|
||||||
else:
|
|
||||||
# make the fingerprint widgets sensitive when encrypted
|
|
||||||
for widget in self.gw('otr_fp_vbox').get_children():
|
|
||||||
widget.set_sensitive(True)
|
|
||||||
# show their fingerprint
|
|
||||||
fp = potr.human_hash(self.fpr)
|
|
||||||
self.gw('their_fp_label').set_markup(their_fp_text % {
|
|
||||||
'jid': self.fjid, 'fp': fp})
|
|
||||||
# set the trust combobox
|
|
||||||
if ctx.getCurrentTrust():
|
|
||||||
self.gw('verified_combobox').set_active(1)
|
|
||||||
else:
|
|
||||||
self.gw('verified_combobox').set_active(0)
|
|
||||||
|
|
||||||
otr_flags = self.plugin.get_flags(self.account, self.jid, fallback=False)
|
|
||||||
|
|
||||||
if otr_flags is not None:
|
|
||||||
self.gw('otr_default_checkbutton').set_active(0)
|
|
||||||
for w in self.gw('otr_settings_vbox').get_children():
|
|
||||||
w.set_sensitive(True)
|
|
||||||
else:
|
|
||||||
# per-user settings not available,
|
|
||||||
# using default settings
|
|
||||||
otr_flags = self.plugin.get_flags(self.account)
|
|
||||||
self.gw('otr_default_checkbutton').set_active(1)
|
|
||||||
for w in self.gw('otr_settings_vbox').get_children():
|
|
||||||
w.set_sensitive(False)
|
|
||||||
|
|
||||||
self.gw('otr_policy_allow_v2_checkbutton').set_active(
|
|
||||||
otr_flags['ALLOW_V2'])
|
|
||||||
self.gw('otr_policy_require_checkbutton').set_active(
|
|
||||||
otr_flags['REQUIRE_ENCRYPTION'])
|
|
||||||
self.gw('otr_policy_send_tag_checkbutton').set_active(
|
|
||||||
otr_flags['SEND_TAG'])
|
|
||||||
self.gw('otr_policy_start_on_tag_checkbutton').set_active(
|
|
||||||
otr_flags['WHITESPACE_START_AKE'])
|
|
||||||
|
|
||||||
self.child.show_all()
|
|
||||||
|
|
||||||
def on_response(self, dlg, response, *args):
|
|
||||||
if response != gtk.RESPONSE_ACCEPT:
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
# -1 when nothing is selected
|
|
||||||
# (ie. the connection is not encrypted)
|
|
||||||
trust_state = self.gw('verified_combobox').get_active()
|
|
||||||
if trust_state == 1 and not self.ctx.getTrust(self.fpr):
|
|
||||||
self.ctx.setTrust(self.fpr, 'verified')
|
|
||||||
self.ctx.user.saveTrusts()
|
|
||||||
self.plugin.update_context_list()
|
|
||||||
elif trust_state == 0:
|
|
||||||
self.ctx.setTrust(self.fpr, '')
|
|
||||||
self.ctx.user.saveTrusts()
|
|
||||||
self.plugin.update_context_list()
|
|
||||||
|
|
||||||
self.plugin.update_otr(self.ctx.peer, self.ctx.user.accountname, True)
|
|
||||||
|
|
||||||
if self.gw('otr_default_checkbutton').get_active():
|
|
||||||
# default is enabled, so remove any user-specific
|
|
||||||
# settings if available
|
|
||||||
self.plugin.set_flags(None, self.account, self.jid)
|
|
||||||
else:
|
|
||||||
# build the flags using the checkboxes
|
|
||||||
flags = {}
|
|
||||||
flags['ALLOW_V2'] = \
|
|
||||||
self.gw('otr_policy_allow_v2_checkbutton').get_active()
|
|
||||||
flags['REQUIRE_ENCRYPTION'] = \
|
|
||||||
self.gw('otr_policy_require_checkbutton').get_active()
|
|
||||||
flags['SEND_TAG'] = \
|
|
||||||
self.gw('otr_policy_send_tag_checkbutton').get_active()
|
|
||||||
flags['WHITESPACE_START_AKE'] = \
|
|
||||||
self.gw('otr_policy_start_on_tag_checkbutton').get_active()
|
|
||||||
|
|
||||||
self.plugin.set_flags(flags, self.account, self.jid)
|
|
||||||
|
|
||||||
def _otr_default_checkbutton_toggled(self, widget):
|
|
||||||
for w in self.gw('otr_settings_vbox').get_children():
|
|
||||||
w.set_sensitive(not widget.get_active())
|
|
||||||
|
|
||||||
def get_otr_submenu(plugin, control):
|
|
||||||
GTK_BUILDER_FILE_PATH = \
|
|
||||||
plugin.local_file_path('contact_otr_window.ui')
|
|
||||||
xml = gtk.Builder()
|
|
||||||
xml.set_translation_domain('gajim_plugins')
|
|
||||||
xml.add_from_file(GTK_BUILDER_FILE_PATH)
|
|
||||||
|
|
||||||
otr_submenu = xml.get_object('otr_submenu')
|
|
||||||
otr_settings_menuitem, smp_otr_menuitem, start_otr_menuitem, \
|
|
||||||
end_otr_menuitem = otr_submenu.get_submenu().get_children()
|
|
||||||
|
|
||||||
otr_submenu.set_sensitive(True)
|
|
||||||
otr_settings_menuitem.connect('activate', plugin.menu_settings_cb, control)
|
|
||||||
start_otr_menuitem.connect('activate', plugin.menu_start_cb, control)
|
|
||||||
end_otr_menuitem.connect('activate', plugin.menu_end_cb, control)
|
|
||||||
smp_otr_menuitem.connect('activate', plugin.menu_smp_cb, control)
|
|
||||||
|
|
||||||
enc, _, fin = plugin.get_otr_status(control.account, control.contact)
|
|
||||||
# can end only when not in PLAINTEXT
|
|
||||||
end_otr_menuitem.set_sensitive(enc)
|
|
||||||
# can SMP only when ENCRYPTED
|
|
||||||
smp_otr_menuitem.set_sensitive(enc and not fin)
|
|
||||||
return otr_submenu
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
|
|
||||||
from offline_bookmarks import OfflineBookmarksPlugin
|
|
||||||
@@ -1,458 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<interface>
|
|
||||||
<requires lib="gtk+" version="2.16"/>
|
|
||||||
<!-- interface-naming-policy toplevel-contextual -->
|
|
||||||
<object class="GtkWindow" id="manage_bookmarks_window">
|
|
||||||
<property name="border_width">12</property>
|
|
||||||
<property name="title" translatable="yes">Manage Bookmarks</property>
|
|
||||||
<property name="default_width">550</property>
|
|
||||||
<property name="default_height">300</property>
|
|
||||||
<property name="type_hint">dialog</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkVBox" id="vbox86">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<property name="spacing">12</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkHBox" id="hbox2965">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="spacing">12</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkVBox" id="vbox94">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<property name="spacing">6</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkScrolledWindow" id="scrolledwindow37">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="hscrollbar_policy">automatic</property>
|
|
||||||
<property name="vscrollbar_policy">automatic</property>
|
|
||||||
<property name="shadow_type">in</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkTreeView" id="bookmarks_treeview">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="headers_visible">False</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkHButtonBox" id="hbuttonbox25">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="spacing">6</property>
|
|
||||||
<property name="layout_style">end</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="add_bookmark_button">
|
|
||||||
<property name="label">gtk-add</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="can_default">True</property>
|
|
||||||
<property name="receives_default">False</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
<signal name="clicked" handler="on_add_bookmark_button_clicked"/>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="remove_bookmark_button">
|
|
||||||
<property name="label">gtk-remove</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="can_default">True</property>
|
|
||||||
<property name="receives_default">False</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
<signal name="clicked" handler="on_remove_bookmark_button_clicked"/>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkTable" id="table33">
|
|
||||||
<property name="n_rows">11</property>
|
|
||||||
<property name="n_columns">2</property>
|
|
||||||
<property name="column_spacing">12</property>
|
|
||||||
<property name="row_spacing">6</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="label318">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="label" translatable="yes">_Password:</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="top_attach">4</property>
|
|
||||||
<property name="bottom_attach">5</property>
|
|
||||||
<property name="x_options">GTK_FILL</property>
|
|
||||||
<property name="y_options"/>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkEntry" id="pass_entry">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="sensitive">False</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="visibility">False</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="top_attach">4</property>
|
|
||||||
<property name="bottom_attach">5</property>
|
|
||||||
<property name="y_options"/>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkEntry" id="server_entry">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="sensitive">False</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="top_attach">3</property>
|
|
||||||
<property name="bottom_attach">4</property>
|
|
||||||
<property name="y_options"/>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="label317">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="label" translatable="yes">_Server:</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="top_attach">3</property>
|
|
||||||
<property name="bottom_attach">4</property>
|
|
||||||
<property name="x_options">GTK_FILL</property>
|
|
||||||
<property name="y_options"/>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="label316">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="label" translatable="yes">Roo_m:</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="top_attach">2</property>
|
|
||||||
<property name="bottom_attach">3</property>
|
|
||||||
<property name="x_options">GTK_FILL</property>
|
|
||||||
<property name="y_options"/>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkEntry" id="room_entry">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="sensitive">False</property>
|
|
||||||
<property name="can_focus">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>
|
|
||||||
<object class="GtkEntry" id="nick_entry">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="sensitive">False</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="top_attach">1</property>
|
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
<property name="y_options"/>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="label315">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="label" translatable="yes">_Nickname:</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="top_attach">1</property>
|
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
<property name="x_options">GTK_FILL</property>
|
|
||||||
<property name="y_options"/>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="label325">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="label" translatable="yes">_Title:</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="x_options">GTK_FILL</property>
|
|
||||||
<property name="y_options"/>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkEntry" id="title_entry">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="sensitive">False</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="y_options"/>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="label326">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="label" translatable="yes">Pr_int status:</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="top_attach">6</property>
|
|
||||||
<property name="bottom_attach">7</property>
|
|
||||||
<property name="x_options">GTK_FILL</property>
|
|
||||||
<property name="y_options"/>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkComboBox" id="print_status_combobox">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="sensitive">False</property>
|
|
||||||
<signal name="changed" handler="on_print_status_combobox_changed"/>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCellRendererText" id="cellrenderertext1"/>
|
|
||||||
<attributes>
|
|
||||||
<attribute name="text">0</attribute>
|
|
||||||
</attributes>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="top_attach">6</property>
|
|
||||||
<property name="bottom_attach">7</property>
|
|
||||||
<property name="x_options">GTK_FILL</property>
|
|
||||||
<property name="y_options">GTK_FILL</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkHBox" id="hbox1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCheckButton" id="autojoin_checkbutton">
|
|
||||||
<property name="label" translatable="yes">A_uto join</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="sensitive">False</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">False</property>
|
|
||||||
<property name="tooltip_text" translatable="yes">If checked, Gajim will join this group chat on startup</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
<signal name="toggled" handler="on_autojoin_checkbutton_toggled"/>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCheckButton" id="minimize_checkbutton">
|
|
||||||
<property name="label" translatable="yes">Minimi_ze on Auto Join</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="sensitive">False</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">False</property>
|
|
||||||
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
<signal name="toggled" handler="on_minimize_checkbutton_toggled"/>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="top_attach">5</property>
|
|
||||||
<property name="bottom_attach">6</property>
|
|
||||||
<property name="y_options">GTK_FILL</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="label1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="yalign">1</property>
|
|
||||||
<property name="ypad">4</property>
|
|
||||||
<property name="label" translatable="yes">Import bookmarks:</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="top_attach">7</property>
|
|
||||||
<property name="bottom_attach">8</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="label2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="label" translatable="yes">Import from:</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="top_attach">9</property>
|
|
||||||
<property name="bottom_attach">10</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="import_button">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="sensitive">False</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">True</property>
|
|
||||||
<property name="focus_on_click">False</property>
|
|
||||||
<signal name="clicked" handler="on_import_button_clicked"/>
|
|
||||||
<child>
|
|
||||||
<object class="GtkHBox" id="hbox2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkImage" id="image1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xalign">1</property>
|
|
||||||
<property name="stock">gtk-add</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="label4">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="label" translatable="yes">Import</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="pack_type">end</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="top_attach">10</property>
|
|
||||||
<property name="bottom_attach">11</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkComboBox" id="import_from">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="focus_on_click">False</property>
|
|
||||||
<signal name="changed" handler="on_import_from_changed"/>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCellRendererText" id="cellrenderertext2"/>
|
|
||||||
<attributes>
|
|
||||||
<attribute name="text">0</attribute>
|
|
||||||
</attributes>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="top_attach">9</property>
|
|
||||||
<property name="bottom_attach">10</property>
|
|
||||||
<property name="x_options">GTK_FILL</property>
|
|
||||||
<property name="y_options">GTK_FILL</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="label3">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="label" translatable="yes">Import to:</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="top_attach">8</property>
|
|
||||||
<property name="bottom_attach">9</property>
|
|
||||||
<property name="x_options">GTK_FILL</property>
|
|
||||||
<property name="y_options"/>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<placeholder/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkComboBox" id="import_to">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="focus_on_click">False</property>
|
|
||||||
<signal name="changed" handler="on_import_to_changed"/>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCellRendererText" id="cellrenderertext3"/>
|
|
||||||
<attributes>
|
|
||||||
<attribute name="text">0</attribute>
|
|
||||||
</attributes>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="top_attach">8</property>
|
|
||||||
<property name="bottom_attach">9</property>
|
|
||||||
<property name="x_options">GTK_FILL</property>
|
|
||||||
<property name="y_options">GTK_FILL</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<placeholder/>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</interface>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
[info]
|
|
||||||
name: Offline Bookmarks
|
|
||||||
short_name: offline_bookmarks
|
|
||||||
version: 0.2.1
|
|
||||||
description: Saving bookmarks inside the plugin configuration file. Allows the use of locally stored bookmarks if the server does not support the storage of bookmarks (eg talk.google.com).
|
|
||||||
Support to import bookmarks from one account to another.
|
|
||||||
authors = Denis Fomin <fominde@gmail.com>
|
|
||||||
homepage = http://trac-plugins.gajim.org/wiki/OfflineBookmarksPlugin
|
|
||||||
max_gajim_version: 0.15.9
|
|
||||||
|
Before Width: | Height: | Size: 686 B |
@@ -1,371 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
##
|
|
||||||
|
|
||||||
import gtk
|
|
||||||
|
|
||||||
import gtkgui_helpers
|
|
||||||
from plugins.gui import GajimPluginConfigDialog
|
|
||||||
from plugins import GajimPlugin
|
|
||||||
from plugins.helpers import log_calls
|
|
||||||
from common import ged
|
|
||||||
from common import app
|
|
||||||
from common.i18n import Q_
|
|
||||||
from config import ManageBookmarksWindow
|
|
||||||
|
|
||||||
|
|
||||||
class OfflineBookmarksPlugin(GajimPlugin):
|
|
||||||
|
|
||||||
@log_calls('OfflineBookmarksPlugin')
|
|
||||||
def init(self):
|
|
||||||
self.description = _('Saving bookmarks inside the plugin configuration '
|
|
||||||
'file. Allows the use of locally stored bookmarks if the server '
|
|
||||||
'does not support the storage of bookmarks (eg talk.google.com).\n'
|
|
||||||
'Support to import bookmarks from one account to another.')
|
|
||||||
|
|
||||||
self.events_handlers = {
|
|
||||||
'bookmarks-received': (ged.POSTGUI, self.bookmarks_received),
|
|
||||||
'signed-in': (ged.POSTGUI, self.handle_event_signed_in),}
|
|
||||||
|
|
||||||
self.gui_extension_points = {
|
|
||||||
'groupchat_control': (self.connect_with_gc_control,
|
|
||||||
self.disconnect_from_gc_control),}
|
|
||||||
self.controls = []
|
|
||||||
self.config_dialog = OfflineBookmarksPluginConfigDialog(self)
|
|
||||||
|
|
||||||
@log_calls('OfflineBookmarksPlugin')
|
|
||||||
def activate(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@log_calls('OfflineBookmarksPlugin')
|
|
||||||
def deactivate(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def save_bookmarks(self, account, bookmarks):
|
|
||||||
jid = app.get_jid_from_account(account)
|
|
||||||
if jid not in self.config:
|
|
||||||
self.config[jid] = {}
|
|
||||||
self.config[jid] = bookmarks
|
|
||||||
|
|
||||||
def bookmarks_received(self, obj):
|
|
||||||
self.save_bookmarks(obj.conn.name, obj.bookmarks)
|
|
||||||
|
|
||||||
def handle_event_signed_in(self, obj):
|
|
||||||
account = obj.conn.name
|
|
||||||
connection = app.connections[account]
|
|
||||||
jid = app.get_jid_from_account(obj.conn.name)
|
|
||||||
bm_jids = [b['jid'] for b in connection.bookmarks]
|
|
||||||
if jid in self.config:
|
|
||||||
for bm in self.config[jid]:
|
|
||||||
if bm['jid'] not in bm_jids:
|
|
||||||
connection.bookmarks.append(bm)
|
|
||||||
invisible_show = app.SHOW_LIST.index('invisible')
|
|
||||||
# do not autojoin if we are invisible
|
|
||||||
if connection.connected == invisible_show:
|
|
||||||
return
|
|
||||||
# do not autojoin if bookmarks supported
|
|
||||||
bookmarks_supported = self.is_bookmark_supported(
|
|
||||||
app.connections[account])
|
|
||||||
if not bookmarks_supported:
|
|
||||||
app.interface.auto_join_bookmarks(connection.name)
|
|
||||||
|
|
||||||
def connect_with_gc_control(self, gc_control):
|
|
||||||
control = Base(self, gc_control)
|
|
||||||
self.controls.append(control)
|
|
||||||
|
|
||||||
def disconnect_from_gc_control(self, gc_control):
|
|
||||||
for control in self.controls:
|
|
||||||
control.disconnect_from_gc_control()
|
|
||||||
self.controls = []
|
|
||||||
|
|
||||||
def is_bookmark_supported(self, account):
|
|
||||||
if account.is_zeroconf:
|
|
||||||
return False
|
|
||||||
return (account.private_storage_supported or (
|
|
||||||
account.pubsub_supported and account.pubsub_publish_options_supported))
|
|
||||||
|
|
||||||
|
|
||||||
class Base(object):
|
|
||||||
def __init__(self, plugin, gc_control):
|
|
||||||
self.plugin = plugin
|
|
||||||
self.gc_control = gc_control
|
|
||||||
self.create_buttons()
|
|
||||||
|
|
||||||
def create_buttons(self):
|
|
||||||
# create button
|
|
||||||
actions_hbox = self.gc_control.xml.get_object('actions_hbox')
|
|
||||||
self.button = gtk.Button(label=None, stock=None, use_underline=True)
|
|
||||||
self.button.set_property('relief', gtk.RELIEF_NONE)
|
|
||||||
self.button.set_property('can-focus', False)
|
|
||||||
img = gtk.Image()
|
|
||||||
if gtkgui_helpers.gtk_icon_theme.has_icon('bookmark-new'):
|
|
||||||
img.set_from_icon_name('bookmark-new', gtk.ICON_SIZE_MENU)
|
|
||||||
else:
|
|
||||||
img.set_from_stock('gtk-add', gtk.ICON_SIZE_MENU)
|
|
||||||
self.button.set_image(img)
|
|
||||||
self.button.set_tooltip_text(_('Bookmark this room(local)'))
|
|
||||||
send_button = self.gc_control.xml.get_object('send_button')
|
|
||||||
send_button_pos = actions_hbox.child_get_property(send_button,
|
|
||||||
'position')
|
|
||||||
actions_hbox.add_with_properties(self.button, 'position',
|
|
||||||
send_button_pos - 1, 'expand', False)
|
|
||||||
self.button.set_no_show_all(True)
|
|
||||||
id_ = self.button.connect('clicked', self.add_bookmark_button_clicked)
|
|
||||||
self.gc_control.handlers[id_] = self.button
|
|
||||||
for bm in app.connections[self.gc_control.account].bookmarks:
|
|
||||||
if bm['jid'] == self.gc_control.contact.jid:
|
|
||||||
self.button.hide()
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
account = self.gc_control.account
|
|
||||||
bookmarks_supported = self.plugin.is_bookmark_supported(
|
|
||||||
app.connections[account])
|
|
||||||
self.button.set_sensitive(not bookmarks_supported)
|
|
||||||
self.button.set_visible(not bookmarks_supported)
|
|
||||||
|
|
||||||
def add_bookmark_button_clicked(self, widget):
|
|
||||||
"""
|
|
||||||
Bookmark the room, without autojoin and not minimized
|
|
||||||
"""
|
|
||||||
from dialogs import ErrorDialog, InformationDialog
|
|
||||||
password = app.gc_passwords.get(self.gc_control.room_jid, '')
|
|
||||||
account = self.gc_control.account
|
|
||||||
|
|
||||||
bm = {'name': self.gc_control.name,
|
|
||||||
'jid': self.gc_control.room_jid,
|
|
||||||
'autojoin': 0,
|
|
||||||
'minimize': 0,
|
|
||||||
'password': password,
|
|
||||||
'nick': self.gc_control.nick}
|
|
||||||
|
|
||||||
place_found = False
|
|
||||||
index = 0
|
|
||||||
# check for duplicate entry and respect alpha order
|
|
||||||
for bookmark in app.connections[account].bookmarks:
|
|
||||||
if bookmark['jid'] == bm['jid']:
|
|
||||||
ErrorDialog(
|
|
||||||
_('Bookmark already set'),
|
|
||||||
_('Group Chat "%s" is already in your bookmarks.') % \
|
|
||||||
bm['jid'])
|
|
||||||
return
|
|
||||||
if bookmark['name'] > bm['name']:
|
|
||||||
place_found = True
|
|
||||||
break
|
|
||||||
index += 1
|
|
||||||
if place_found:
|
|
||||||
app.connections[account].bookmarks.insert(index, bm)
|
|
||||||
else:
|
|
||||||
app.connections[account].bookmarks.append(bm)
|
|
||||||
self.plugin.save_bookmarks(account, app.connections[account].bookmarks)
|
|
||||||
app.interface.roster.set_actions_menu_needs_rebuild()
|
|
||||||
InformationDialog(
|
|
||||||
_('Bookmark has been added successfully'),
|
|
||||||
_('You can manage your bookmarks via Actions menu in your roster.'))
|
|
||||||
|
|
||||||
def disconnect_from_gc_control(self):
|
|
||||||
actions_hbox = self.gc_control.xml.get_object('actions_hbox')
|
|
||||||
actions_hbox.remove(self.button)
|
|
||||||
|
|
||||||
|
|
||||||
class OfflineBookmarksPluginConfigDialog(GajimPluginConfigDialog,
|
|
||||||
ManageBookmarksWindow):
|
|
||||||
def init(self):
|
|
||||||
self.GTK_BUILDER_FILE_PATH = self.plugin.local_file_path(
|
|
||||||
'config_dialog.ui')
|
|
||||||
self.xml = gtk.Builder()
|
|
||||||
self.xml.set_translation_domain('gajim_plugins')
|
|
||||||
self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH,
|
|
||||||
['vbox86'])
|
|
||||||
vbox = self.xml.get_object('vbox86')
|
|
||||||
self.child.pack_start(vbox)
|
|
||||||
self.import_from_combo = self.xml.get_object('import_from')
|
|
||||||
self.import_to_combo = self.xml.get_object('import_to')
|
|
||||||
|
|
||||||
def on_run(self):
|
|
||||||
self.fill_treeview()
|
|
||||||
|
|
||||||
#Prepare comboboxes
|
|
||||||
self.print_status_combobox = self.xml.get_object('print_status_combobox')
|
|
||||||
model = gtk.ListStore(str, str)
|
|
||||||
self.option_list = {'': _('Default'), 'all': Q_('?print_status:All'),
|
|
||||||
'in_and_out': _('Enter and leave only'),
|
|
||||||
'none': Q_('?print_status:None')}
|
|
||||||
opts = sorted(self.option_list.keys())
|
|
||||||
for opt in opts:
|
|
||||||
model.append([self.option_list[opt], opt])
|
|
||||||
self.print_status_combobox.set_model(model)
|
|
||||||
self.print_status_combobox.set_active(1)
|
|
||||||
#Prepare import_from combobox
|
|
||||||
model = gtk.ListStore(str)
|
|
||||||
for account in self.accounts:
|
|
||||||
model.append([account,])
|
|
||||||
for account_jid in self.plugin.config:
|
|
||||||
if account_jid not in self.plugin.config_default_values and \
|
|
||||||
account_jid not in self.jids:
|
|
||||||
model.append([account_jid,])
|
|
||||||
self.import_from_combo.set_model(model)
|
|
||||||
#Prepare import_to combobox
|
|
||||||
model = gtk.ListStore(str)
|
|
||||||
for account in self.accounts:
|
|
||||||
model.append([account,])
|
|
||||||
self.import_to_combo.set_model(model)
|
|
||||||
|
|
||||||
self.selection = self.view.get_selection()
|
|
||||||
self.selection.connect('changed', self.bookmark_selected)
|
|
||||||
|
|
||||||
#Prepare input fields
|
|
||||||
self.title_entry = self.xml.get_object('title_entry')
|
|
||||||
self.title_entry.connect('changed', self.on_title_entry_changed)
|
|
||||||
self.nick_entry = self.xml.get_object('nick_entry')
|
|
||||||
self.nick_entry.connect('changed', self.on_nick_entry_changed)
|
|
||||||
self.server_entry = self.xml.get_object('server_entry')
|
|
||||||
self.server_entry.connect('changed', self.on_server_entry_changed)
|
|
||||||
self.room_entry = self.xml.get_object('room_entry')
|
|
||||||
self.room_entry.connect('changed', self.on_room_entry_changed)
|
|
||||||
self.pass_entry = self.xml.get_object('pass_entry')
|
|
||||||
self.pass_entry.connect('changed', self.on_pass_entry_changed)
|
|
||||||
self.autojoin_checkbutton = self.xml.get_object('autojoin_checkbutton')
|
|
||||||
self.minimize_checkbutton = self.xml.get_object('minimize_checkbutton')
|
|
||||||
|
|
||||||
self.xml.connect_signals(self)
|
|
||||||
self.connect('hide', self.on_hide)
|
|
||||||
|
|
||||||
|
|
||||||
self.show_all()
|
|
||||||
self.view.set_cursor((0,))
|
|
||||||
|
|
||||||
def fill_treeview(self):
|
|
||||||
# Account-JID, RoomName, Room-JID, Autojoin, Minimize, Passowrd, Nick,
|
|
||||||
# Show_Status
|
|
||||||
self.treestore = gtk.TreeStore(str, str, str, bool, bool, str, str, str)
|
|
||||||
self.treestore.set_sort_column_id(1, gtk.SORT_ASCENDING)
|
|
||||||
self.accounts = []
|
|
||||||
self.jids = []
|
|
||||||
|
|
||||||
# Store bookmarks in treeview.
|
|
||||||
for account in app.connections:
|
|
||||||
if app.connections[account].connected <= 1:
|
|
||||||
continue
|
|
||||||
if app.connections[account].is_zeroconf:
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.accounts.append(account)
|
|
||||||
self.jids.append(app.get_jid_from_account(account))
|
|
||||||
iter_ = self.treestore.append(None, [None, account, None, None,
|
|
||||||
None, None, None, None])
|
|
||||||
|
|
||||||
for bookmark in app.connections[account].bookmarks:
|
|
||||||
if bookmark['name'] == '':
|
|
||||||
# No name was given for this bookmark.
|
|
||||||
# Use the first part of JID instead...
|
|
||||||
name = bookmark['jid'].split("@")[0]
|
|
||||||
bookmark['name'] = name
|
|
||||||
from common import helpers
|
|
||||||
# make '1', '0', 'true', 'false' (or other) to True/False
|
|
||||||
autojoin = helpers.from_xs_boolean_to_python_boolean(
|
|
||||||
bookmark['autojoin'])
|
|
||||||
|
|
||||||
minimize = helpers.from_xs_boolean_to_python_boolean(
|
|
||||||
bookmark['minimize'])
|
|
||||||
|
|
||||||
print_status = bookmark.get('print_status', '')
|
|
||||||
if print_status not in ('', 'all', 'in_and_out', 'none'):
|
|
||||||
print_status = ''
|
|
||||||
self.treestore.append(iter_, [
|
|
||||||
account,
|
|
||||||
bookmark['name'],
|
|
||||||
bookmark['jid'],
|
|
||||||
autojoin,
|
|
||||||
minimize,
|
|
||||||
bookmark['password'],
|
|
||||||
bookmark['nick'],
|
|
||||||
print_status ])
|
|
||||||
|
|
||||||
self.view = self.xml.get_object('bookmarks_treeview')
|
|
||||||
self.view.set_model(self.treestore)
|
|
||||||
self.view.expand_all()
|
|
||||||
|
|
||||||
renderer = gtk.CellRendererText()
|
|
||||||
column = gtk.TreeViewColumn('Bookmarks', renderer, text=1)
|
|
||||||
if self.view.get_column(0):
|
|
||||||
self.view.remove_column(self.view.get_column(0))
|
|
||||||
self.view.append_column(column)
|
|
||||||
|
|
||||||
def on_hide(self, widget):
|
|
||||||
"""
|
|
||||||
Parse the treestore data into our new bookmarks array, then send the new
|
|
||||||
bookmarks to the server.
|
|
||||||
"""
|
|
||||||
(model, iter_) = self.selection.get_selected()
|
|
||||||
if iter_ and model.iter_parent(iter_):
|
|
||||||
#bookmark selected, check it
|
|
||||||
if not self.check_valid_bookmark():
|
|
||||||
return
|
|
||||||
|
|
||||||
for account in self.treestore:
|
|
||||||
account_unicode = account[1].decode('utf-8')
|
|
||||||
app.connections[account_unicode].bookmarks = []
|
|
||||||
|
|
||||||
for bm in account.iterchildren():
|
|
||||||
# Convert True/False/None to '1' or '0'
|
|
||||||
autojoin = unicode(int(bm[3]))
|
|
||||||
minimize = unicode(int(bm[4]))
|
|
||||||
name = bm[1]
|
|
||||||
if name:
|
|
||||||
name = name.decode('utf-8')
|
|
||||||
jid = bm[2]
|
|
||||||
if jid:
|
|
||||||
jid = jid.decode('utf-8')
|
|
||||||
pw = bm[5]
|
|
||||||
if pw:
|
|
||||||
pw = pw.decode('utf-8')
|
|
||||||
nick = bm[6]
|
|
||||||
if nick:
|
|
||||||
nick = nick.decode('utf-8')
|
|
||||||
|
|
||||||
# create the bookmark-dict
|
|
||||||
bmdict = { 'name': name, 'jid': jid, 'autojoin': autojoin,
|
|
||||||
'minimize': minimize, 'password': pw, 'nick': nick,
|
|
||||||
'print_status': bm[7]}
|
|
||||||
|
|
||||||
app.connections[account_unicode].bookmarks.append(bmdict)
|
|
||||||
|
|
||||||
bookmarks_supported = self.plugin.is_bookmark_supported(
|
|
||||||
app.connections[account_unicode])
|
|
||||||
if bookmarks_supported:
|
|
||||||
app.connections[account_unicode].store_bookmarks()
|
|
||||||
self.plugin.save_bookmarks(account_unicode,
|
|
||||||
app.connections[account_unicode].bookmarks)
|
|
||||||
app.interface.roster.set_actions_menu_needs_rebuild()
|
|
||||||
|
|
||||||
def on_import_to_changed(self, treeview):
|
|
||||||
self.on_import_from_changed(self.import_from_combo)
|
|
||||||
|
|
||||||
def on_import_from_changed(self, widget):
|
|
||||||
if widget.get_active() == -1 or self.import_to_combo.get_active() == -1:
|
|
||||||
self.xml.get_object('import_button').set_sensitive(False)
|
|
||||||
else:
|
|
||||||
if widget.get_active_text() != self.import_to_combo.get_active_text():
|
|
||||||
self.xml.get_object('import_button').set_sensitive(True)
|
|
||||||
else:
|
|
||||||
self.xml.get_object('import_button').set_sensitive(False)
|
|
||||||
|
|
||||||
def on_import_button_clicked(self, widget):
|
|
||||||
from_ = self.import_from_combo.get_active_text()
|
|
||||||
to_connection = app.connections[self.import_to_combo.get_active_text()]
|
|
||||||
to_bookmarks = to_connection.bookmarks
|
|
||||||
|
|
||||||
if from_ in self.accounts:
|
|
||||||
from_bookmarks = app.connections[from_].bookmarks
|
|
||||||
else:
|
|
||||||
from_bookmarks = self.plugin.config[from_]
|
|
||||||
for bm in from_bookmarks:
|
|
||||||
for bookmark in to_bookmarks:
|
|
||||||
if bookmark['jid'] == bm['jid']:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
to_bookmarks.append(bm)
|
|
||||||
|
|
||||||
self.fill_treeview()
|
|
||||||
self.view.set_cursor((0,))
|
|
||||||
self.import_from_combo.set_active(-1)
|
|
||||||
self.import_to_combo.set_active(-1)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from plugin import SnarlNotificationsPlugin
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
[info]
|
|
||||||
name: Snarl Notifications
|
|
||||||
short_name: snarl_notifications
|
|
||||||
version: 0.1.2
|
|
||||||
description: Shows events notification using Snarl (http://www.fullphat.net/) under Windows. Snarl needs to be installed in system.
|
|
||||||
PySnarl bindings are used (http://sourceforge.net/projects/pysnarl/).
|
|
||||||
authors = Yann Leboulanger <asterix@lagaule.org>
|
|
||||||
homepage = http://trac-plugins.gajim.org/wiki/SnarlNotificationsPlugin
|
|
||||||
max_gajim_version: 0.15.9
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
##
|
|
||||||
## This file is part of Gajim.
|
|
||||||
##
|
|
||||||
## Gajim is free software; you can redistribute it and/or modify
|
|
||||||
## it under the terms of the GNU General Public License as published
|
|
||||||
## by the Free Software Foundation; version 3 only.
|
|
||||||
##
|
|
||||||
## Gajim is distributed in the hope that it will be useful,
|
|
||||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
## GNU General Public License for more details.
|
|
||||||
##
|
|
||||||
## You should have received a copy of the GNU General Public License
|
|
||||||
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
##
|
|
||||||
'''
|
|
||||||
Events notifications using Snarl
|
|
||||||
|
|
||||||
Fancy events notifications under Windows using Snarl infrastructure.
|
|
||||||
|
|
||||||
:note: plugin is at proof-of-concept state.
|
|
||||||
|
|
||||||
:author: Yann Leboulanger <asterix@lagaule.org>
|
|
||||||
:since: 08 April 2012
|
|
||||||
:copyright: Copyright (2012) Yann Leboulanger <asterix@lagaule.org>
|
|
||||||
:license: GPL
|
|
||||||
'''
|
|
||||||
|
|
||||||
import pySnarl
|
|
||||||
|
|
||||||
from common import gajim
|
|
||||||
from plugins import GajimPlugin
|
|
||||||
from plugins.helpers import log_calls, log
|
|
||||||
from common import ged
|
|
||||||
import os.path
|
|
||||||
|
|
||||||
class SnarlActionHandler(pySnarl.EventHandler):
|
|
||||||
def OnNotificationInvoked(self, uid):
|
|
||||||
account, jid, msg_type = uid.split()
|
|
||||||
gajim.interface.handle_event(account, jid, msg_type)
|
|
||||||
|
|
||||||
class SnarlNotificationsPlugin(GajimPlugin):
|
|
||||||
|
|
||||||
@log_calls('SnarlNotificationsPlugin')
|
|
||||||
def init(self):
|
|
||||||
self.description = _('Shows events notification using Snarl '
|
|
||||||
'(http://www.fullphat.net/) under Windows. '
|
|
||||||
'Snarl needs to be installed in system.\n'
|
|
||||||
'PySnarl bindings are used (http://code.google.com/p/pysnarl/).')
|
|
||||||
self.config_dialog = None
|
|
||||||
self.h = SnarlActionHandler
|
|
||||||
self.snarl_win = pySnarl.SnarlApp(
|
|
||||||
"pySnarl/Gajim", # app signature
|
|
||||||
"Gajim", # app title
|
|
||||||
os.path.abspath("..\data\pixmaps\gajim.ico"), # icon
|
|
||||||
"", # config Tool
|
|
||||||
"Gajim will use Snarl to display notifications", # hint
|
|
||||||
False, # IsDaemon
|
|
||||||
self.h, # event handler
|
|
||||||
[] # classes
|
|
||||||
)
|
|
||||||
|
|
||||||
self.events_handlers = {'notification' : (ged.PRECORE, self.notif)}
|
|
||||||
|
|
||||||
@log_calls('SnarlNotificationsPlugin')
|
|
||||||
def notif(self, obj):
|
|
||||||
if obj.do_popup:
|
|
||||||
uid = obj.conn.name + " " + obj.jid + " " + obj.popup_msg_type
|
|
||||||
self.snarl_win.notify(
|
|
||||||
[], # actions
|
|
||||||
"", # callbackScript
|
|
||||||
"", # callbackScriptType
|
|
||||||
"", # class
|
|
||||||
"", # defaultCallback
|
|
||||||
5, # duration
|
|
||||||
os.path.abspath(obj.popup_image),#r"C:\Documents and Settings\Administrateur\Mes documents\gajim\data\pixmaps\gajim.ico", # icon
|
|
||||||
"", # mergeUID
|
|
||||||
0, # priority
|
|
||||||
"", # replaceUID
|
|
||||||
obj.popup_text,
|
|
||||||
obj.popup_title,
|
|
||||||
uid, # UID
|
|
||||||
"", # sound
|
|
||||||
-1, # percent
|
|
||||||
0, # log
|
|
||||||
64, # sensitivity
|
|
||||||
)
|
|
||||||
obj.do_popup = False
|
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# ActiveX/COM Snarl python library
|
|
||||||
# For proper functioning requires Snarl 2.5.1 or later !
|
|
||||||
# Version 0.0.2
|
|
||||||
|
|
||||||
# Changelog (in reverse chronological order):
|
|
||||||
# -------------------------------------------
|
|
||||||
# 0.0.2 by Pako 2012-01-30 14:12 UTC+1
|
|
||||||
# - base64 icon format is now supported
|
|
||||||
# 0.0.1 by Pako 2012-01-06 15:30 UTC+1
|
|
||||||
# - initial version
|
|
||||||
#-------------------------------------------------------
|
|
||||||
|
|
||||||
from win32com.client import constants, gencache, Dispatch, DispatchWithEvents
|
|
||||||
#===============================================================================
|
|
||||||
|
|
||||||
def IsRunning():
|
|
||||||
from win32gui import FindWindow
|
|
||||||
return FindWindow("w>Snarl", "Snarl")
|
|
||||||
#===============================================================================
|
|
||||||
|
|
||||||
class EventHandler:
|
|
||||||
|
|
||||||
def OnActivated(self):
|
|
||||||
self.fn("DaemonActivated")
|
|
||||||
|
|
||||||
def OnNotificationActionSelected(self, uid, command):
|
|
||||||
self.fn("ActionSelected.%s" % command, payload = uid)
|
|
||||||
|
|
||||||
def OnNotificationClosed(self, uid):
|
|
||||||
self.fn("Closed", payload = uid)
|
|
||||||
|
|
||||||
def OnNotificationExpired(self, uid):
|
|
||||||
self.fn("Expired", payload = uid)
|
|
||||||
|
|
||||||
def OnNotificationInvoked(self, uid):
|
|
||||||
self.fn("Invoked", payload = uid)
|
|
||||||
|
|
||||||
def OnQuit(self):
|
|
||||||
self.fn("AppQuit")
|
|
||||||
|
|
||||||
def OnShowAbout(self):
|
|
||||||
self.fn("ShowAbout")
|
|
||||||
|
|
||||||
def OnShowConfig(self):
|
|
||||||
self.fn("ShowConfig")
|
|
||||||
|
|
||||||
def OnSnarlLaunched(self):
|
|
||||||
self.fn("Launched")
|
|
||||||
|
|
||||||
def OnSnarlQuit(self):
|
|
||||||
self.fn("Quit")
|
|
||||||
|
|
||||||
def OnSnarlStarted(self):
|
|
||||||
self.fn("Started")
|
|
||||||
|
|
||||||
def OnSnarlStopped(self):
|
|
||||||
self.fn("Stopped")
|
|
||||||
|
|
||||||
def OnUserAway(self):
|
|
||||||
self.fn("UserAway")
|
|
||||||
|
|
||||||
def OnUserReturned(self):
|
|
||||||
self.fn("UserReturned")
|
|
||||||
#===============================================================================
|
|
||||||
|
|
||||||
class SnarlApp(object):
|
|
||||||
"""Creates an SnarlApp object"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
signature,
|
|
||||||
title,
|
|
||||||
icon = "",
|
|
||||||
configTool = "",
|
|
||||||
hint = "",
|
|
||||||
isDaemon = False,
|
|
||||||
eventHandler = None,
|
|
||||||
classes = []
|
|
||||||
):
|
|
||||||
self.app = gencache.EnsureDispatch("libsnarl25.SnarlApp")
|
|
||||||
for d in constants.__dicts__: # we must find the right dictionary ...
|
|
||||||
if 'ERROR_NOTIFICATION_NOT_FOUND' in d: # this is it !
|
|
||||||
break
|
|
||||||
codes = d.items()
|
|
||||||
codes.sort(reverse = True)
|
|
||||||
self.statCodes = dict([[v,k] for k,v in codes[6:]])
|
|
||||||
self.statCodes[codes[2][1]] = codes[2][0] # added SUCCESS
|
|
||||||
self.classes = NotifClasses(classes)
|
|
||||||
if eventHandler:
|
|
||||||
self.SetEventHandler(eventHandler)
|
|
||||||
self.SetTo(
|
|
||||||
configTool,
|
|
||||||
hint,
|
|
||||||
icon,
|
|
||||||
isDaemon,
|
|
||||||
signature,
|
|
||||||
title
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def SetTo(
|
|
||||||
self,
|
|
||||||
configTool,
|
|
||||||
hint,
|
|
||||||
icon,
|
|
||||||
isDaemon,
|
|
||||||
signature,
|
|
||||||
title
|
|
||||||
):
|
|
||||||
self.app.Classes = self.classes.Classes()
|
|
||||||
self.app.ConfigTool = configTool
|
|
||||||
self.app.Hint = hint
|
|
||||||
self.app.Icon = icon
|
|
||||||
self.app.IsDaemon = isDaemon
|
|
||||||
self.app.Signature = signature
|
|
||||||
self.app.Title = title
|
|
||||||
|
|
||||||
|
|
||||||
def GetStatusCode(self, code):
|
|
||||||
return self.statCodes[code]
|
|
||||||
|
|
||||||
|
|
||||||
def SetEventHandler(self, handler):
|
|
||||||
self.events = DispatchWithEvents(self.app, handler)
|
|
||||||
|
|
||||||
|
|
||||||
def register(self):
|
|
||||||
return self.GetStatusCode(self.app.Register())
|
|
||||||
|
|
||||||
|
|
||||||
def unregister(self):
|
|
||||||
return self.GetStatusCode(self.app.Unregister())
|
|
||||||
|
|
||||||
|
|
||||||
def tidyUp(self):
|
|
||||||
self.app.TidyUp()
|
|
||||||
|
|
||||||
|
|
||||||
def addClass(
|
|
||||||
self,
|
|
||||||
classId,
|
|
||||||
name,
|
|
||||||
enabled = True,
|
|
||||||
title = "",
|
|
||||||
text = "",
|
|
||||||
icon = "",
|
|
||||||
callback = "",
|
|
||||||
duration = -1,
|
|
||||||
sound = "",
|
|
||||||
):
|
|
||||||
cntOld = self.classes.count()
|
|
||||||
cntNew = self.classes.add(classId, name, enabled, title, text, icon, callback, duration, sound)
|
|
||||||
return int(not cntNew == cntOld + 1)
|
|
||||||
|
|
||||||
|
|
||||||
def remClass(self, id):
|
|
||||||
cntOld = self.classes.count()
|
|
||||||
cntNew = self.classes.remove(id)
|
|
||||||
return int(not cntNew == cntOld - 1)
|
|
||||||
|
|
||||||
|
|
||||||
def clearClasses(self):
|
|
||||||
return self.classes.makeEmpty()
|
|
||||||
|
|
||||||
|
|
||||||
def classesCount(self):
|
|
||||||
return self.classes.count()
|
|
||||||
|
|
||||||
|
|
||||||
def notify(
|
|
||||||
self,
|
|
||||||
actions,
|
|
||||||
callbackScript,
|
|
||||||
callbackScriptType,
|
|
||||||
cls,
|
|
||||||
defaultCallback,
|
|
||||||
duration,
|
|
||||||
icon,
|
|
||||||
mergeUID,
|
|
||||||
priority,
|
|
||||||
replaceUID,
|
|
||||||
text,
|
|
||||||
title,
|
|
||||||
uid,
|
|
||||||
sound = None,
|
|
||||||
percent = None,
|
|
||||||
log = None,
|
|
||||||
sensitivity = None
|
|
||||||
):
|
|
||||||
note = Notification(
|
|
||||||
actions,
|
|
||||||
callbackScript,
|
|
||||||
callbackScriptType,
|
|
||||||
cls,
|
|
||||||
defaultCallback,
|
|
||||||
duration,
|
|
||||||
"",
|
|
||||||
mergeUID,
|
|
||||||
priority,
|
|
||||||
replaceUID,
|
|
||||||
text,
|
|
||||||
title,
|
|
||||||
uid,
|
|
||||||
)
|
|
||||||
if icon:
|
|
||||||
if icon[1] == ":":
|
|
||||||
note.Add("icon", icon, True)
|
|
||||||
else:
|
|
||||||
note.Add("icon-base64", icon.replace("=","%"), True)
|
|
||||||
if sound:
|
|
||||||
note.Add("sound", sound, True)
|
|
||||||
if percent is not None and percent > -1:
|
|
||||||
note.Add("value-percent", str(percent), True)
|
|
||||||
if log is not None:
|
|
||||||
note.Add("log", str(log), True)
|
|
||||||
if sensitivity is not None:
|
|
||||||
note.Add("sensitivity", str(sensitivity), True)
|
|
||||||
return self.GetStatusCode(self.app.Show(note.Note())[0])
|
|
||||||
|
|
||||||
|
|
||||||
def hideNotification(self, uid):
|
|
||||||
return self.GetStatusCode(self.app.Hide(uid))
|
|
||||||
|
|
||||||
|
|
||||||
def isVisible(self, uid):
|
|
||||||
return self.GetStatusCode(self.app.IsVisible(uid))
|
|
||||||
|
|
||||||
|
|
||||||
def getEtcPath(self):
|
|
||||||
return self.app.GetEtcPath()
|
|
||||||
|
|
||||||
|
|
||||||
def makePath(self, pth):
|
|
||||||
return self.app.GetEtcPath(pth)
|
|
||||||
|
|
||||||
|
|
||||||
def isInstalled(self):
|
|
||||||
return self.app.IsSnarlInstalled()
|
|
||||||
|
|
||||||
|
|
||||||
def isRunning(self):
|
|
||||||
return self.app.IsSnarlRunning()
|
|
||||||
|
|
||||||
|
|
||||||
def version(self):
|
|
||||||
ver = self.app.SnarlVersion()
|
|
||||||
return ver if ver > 0 else self.GetStatusCode(-ver)
|
|
||||||
|
|
||||||
|
|
||||||
def isConnected(self):
|
|
||||||
return self.app.IsConnected
|
|
||||||
|
|
||||||
|
|
||||||
def getLibVersion(self):
|
|
||||||
return self.app.LibVersion
|
|
||||||
|
|
||||||
|
|
||||||
def getLibRevision(self):
|
|
||||||
return self.app.LibRevision
|
|
||||||
|
|
||||||
|
|
||||||
def Destroy(self):
|
|
||||||
self.app.TidyUp()
|
|
||||||
if self.events:
|
|
||||||
del self.events
|
|
||||||
del self.app
|
|
||||||
#===============================================================================
|
|
||||||
|
|
||||||
class NotifClasses(object):
|
|
||||||
def __init__(self, classes = []):
|
|
||||||
self.clss = Dispatch("libsnarl25.Classes")
|
|
||||||
for cls in classes:
|
|
||||||
self.add(*cls)
|
|
||||||
|
|
||||||
def count(self):
|
|
||||||
return self.clss.Count()
|
|
||||||
|
|
||||||
def add(self, *cls):
|
|
||||||
self.clss.Add(*cls)
|
|
||||||
return self.clss.Count()
|
|
||||||
|
|
||||||
def remove(self, cls):
|
|
||||||
self.clss.Remove(cls)
|
|
||||||
return self.clss.Count()
|
|
||||||
|
|
||||||
def makeEmpty(self):
|
|
||||||
self.clss.MakeEmpty()
|
|
||||||
return self.clss.Count()
|
|
||||||
|
|
||||||
def Classes(self):
|
|
||||||
return self.clss
|
|
||||||
#===============================================================================
|
|
||||||
|
|
||||||
class Notification(object):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
actions,
|
|
||||||
callbackScript,
|
|
||||||
callbackScriptType,
|
|
||||||
cls,
|
|
||||||
defaultCallback,
|
|
||||||
duration,
|
|
||||||
icon,
|
|
||||||
mergeUID,
|
|
||||||
priority,
|
|
||||||
replaceUID,
|
|
||||||
text,
|
|
||||||
title,
|
|
||||||
uid,
|
|
||||||
):
|
|
||||||
nt = Dispatch("libsnarl25.Notification")
|
|
||||||
nt.Actions = NotifActions(actions).Actions()
|
|
||||||
nt.CallbackScript = callbackScript
|
|
||||||
nt.CallbackScriptType = callbackScriptType
|
|
||||||
nt.Class = cls
|
|
||||||
nt.DefaultCallback = defaultCallback
|
|
||||||
nt.Duration = duration
|
|
||||||
if icon:
|
|
||||||
nt.Icon = icon
|
|
||||||
nt.MergeUID = mergeUID
|
|
||||||
nt.Priority = priority
|
|
||||||
nt.ReplaceUID = replaceUID
|
|
||||||
nt.Text = text
|
|
||||||
nt.Title = title
|
|
||||||
nt.UID = uid
|
|
||||||
self.nt = nt
|
|
||||||
|
|
||||||
def Add(self, name, value, update):
|
|
||||||
self.nt.Add(name, value, True)
|
|
||||||
|
|
||||||
def Note(self):
|
|
||||||
return self.nt
|
|
||||||
#===============================================================================
|
|
||||||
|
|
||||||
class NotifActions(object):
|
|
||||||
def __init__(self, actions=[]):
|
|
||||||
self.actns = Dispatch("libsnarl25.Actions")
|
|
||||||
self.actions = actions
|
|
||||||
for action in actions:
|
|
||||||
self.actns.Add(*action)
|
|
||||||
|
|
||||||
def add(self, actn):
|
|
||||||
self.actns.Add(*actn)
|
|
||||||
self.actions.append(actn)
|
|
||||||
return self.actns.Count()
|
|
||||||
|
|
||||||
def remove(self, actn):
|
|
||||||
if actn in self.actions:
|
|
||||||
ix = self.actions.index(actn)
|
|
||||||
self.actns.Remove(ix + 1)
|
|
||||||
self.actions.pop(ix)
|
|
||||||
return self.actns.Count()
|
|
||||||
|
|
||||||
def makeEmpty(self):
|
|
||||||
self.actns.MakeEmpty()
|
|
||||||
self.actions = []
|
|
||||||
return self.actns.Count()
|
|
||||||
|
|
||||||
def Actions(self):
|
|
||||||
return self.actns
|
|
||||||
#===============================================================================
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1 +0,0 @@
|
|||||||
from plugin import UbuntuIntegrationPlugin
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,12 +0,0 @@
|
|||||||
[info]
|
|
||||||
name: Ubuntu Ayatana Integration
|
|
||||||
short_name: ubuntu_integration
|
|
||||||
version: 0.1.4
|
|
||||||
description: This plugin integrates Gajim with the Ubuntu Messaging Menu.
|
|
||||||
|
|
||||||
You must have python-indicate and python-xdg (and Gajim obviously) installed to enable this plugin.
|
|
||||||
|
|
||||||
Many thanks to the guys from gajim@conference.gajim.org for answering my questions :)
|
|
||||||
authors: Michael Kainer <kaini@linuxlovers.at>
|
|
||||||
homepage: http://trac-plugins.gajim.org/wiki/UbuntuIntegrationPlugin
|
|
||||||
max_gajim_version: 0.15.9
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Ubuntu Ayatana Integration plugin.
|
|
||||||
|
|
||||||
TODO:
|
|
||||||
* handle gc-invitation, subscription_request: it looks like they don't fire
|
|
||||||
* nice error if plugin can't load
|
|
||||||
* me menu
|
|
||||||
* permanent integration into the messaging menu after quitting gajim
|
|
||||||
* show/hide gajim on root menu entry
|
|
||||||
* switch workspace on click on events
|
|
||||||
* corrent group chat handling
|
|
||||||
* hide gajim if the plugin is disabled
|
|
||||||
|
|
||||||
:author: Michael Kainer <kaini@linuxlovers.at>
|
|
||||||
:since: 21st October 2010
|
|
||||||
:copyright: Copyright (2010) Michael Kainer <kaini1123@gmail.com>
|
|
||||||
:license: GPLv3
|
|
||||||
"""
|
|
||||||
# Python
|
|
||||||
import time
|
|
||||||
# Gajim
|
|
||||||
from plugins import GajimPlugin
|
|
||||||
from plugins.plugin import GajimPluginException
|
|
||||||
from plugins.helpers import log_calls
|
|
||||||
from common import gajim
|
|
||||||
import gtkgui_helpers
|
|
||||||
try:
|
|
||||||
from xdg.BaseDirectory import load_data_paths
|
|
||||||
import indicate
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class UbuntuIntegrationPlugin(GajimPlugin):
|
|
||||||
"""
|
|
||||||
Class for Messaging Menu and Me Menu.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@log_calls("UbuntuIntegrationPlugin")
|
|
||||||
def init(self):
|
|
||||||
"""
|
|
||||||
Does nothing.
|
|
||||||
"""
|
|
||||||
self.description = _('This plugin integrates Gajim '
|
|
||||||
'with the Ubuntu Messaging Menu.\n\n'
|
|
||||||
'You must have python-indicate and python-xdg (and Gajim obviously)'
|
|
||||||
' installed to enable this plugin.\n\n'
|
|
||||||
'Many thanks to the guys from gajim@conference.gajim.org for '
|
|
||||||
'answering my questions :)')
|
|
||||||
self.config_dialog = None
|
|
||||||
self.test_activatable()
|
|
||||||
|
|
||||||
def test_activatable(self):
|
|
||||||
self.available_text = ''
|
|
||||||
try:
|
|
||||||
from xdg.BaseDirectory import load_data_paths
|
|
||||||
except ImportError:
|
|
||||||
self.activatable = False
|
|
||||||
self.available_text += _('python-xdg is missing! '
|
|
||||||
'Install python-xdg.\n')
|
|
||||||
try:
|
|
||||||
import indicate
|
|
||||||
except ImportError:
|
|
||||||
self.activatable = False
|
|
||||||
self.available_text += _('python-indicate is missing! '
|
|
||||||
'Install python-indicate.')
|
|
||||||
|
|
||||||
@log_calls("UbuntuIntegrationPlugin")
|
|
||||||
def activate(self):
|
|
||||||
"""
|
|
||||||
Displays gajim in the Messaging Menu.
|
|
||||||
"""
|
|
||||||
# {(account, jid): (indicator, [event, ...]), ...}
|
|
||||||
self.events = {}
|
|
||||||
|
|
||||||
version = gajim.version.split('-')[0]
|
|
||||||
if version == '0.15' and self.available_text:
|
|
||||||
raise GajimPluginException(self.available_text)
|
|
||||||
|
|
||||||
self.server = indicate.indicate_server_ref_default()
|
|
||||||
self.server.set_type("message.im")
|
|
||||||
dfile = ""
|
|
||||||
for file in load_data_paths("applications/gajim.desktop"):
|
|
||||||
dfile = file
|
|
||||||
break
|
|
||||||
if not dfile:
|
|
||||||
raise GajimPluginException("Can't locate gajim.desktop!")
|
|
||||||
self.server.set_desktop_file(dfile)
|
|
||||||
self.server.show()
|
|
||||||
|
|
||||||
gajim.events.event_added_subscribe(self.on_event_added)
|
|
||||||
gajim.events.event_removed_subscribe(self.on_event_removed)
|
|
||||||
|
|
||||||
@log_calls("UbuntuIntegrationPlugin")
|
|
||||||
def deactivate(self):
|
|
||||||
"""
|
|
||||||
Cleaning up.
|
|
||||||
"""
|
|
||||||
gajim.events.event_added_unsubscribe(self.on_event_added)
|
|
||||||
gajim.events.event_removed_unsubscribe(self.on_event_removed)
|
|
||||||
|
|
||||||
if hasattr(self, 'server'):
|
|
||||||
self.server.hide()
|
|
||||||
del self.server
|
|
||||||
|
|
||||||
if hasattr(self, 'events'):
|
|
||||||
for (_, event) in self.events:
|
|
||||||
event[0].hide()
|
|
||||||
del self.events
|
|
||||||
|
|
||||||
def on_indicator_activate(self, indicator, _):
|
|
||||||
"""
|
|
||||||
Forwards the action to gajims event handler.
|
|
||||||
"""
|
|
||||||
key = indicator.key
|
|
||||||
event = self.events[key][1][0]
|
|
||||||
gajim.interface.handle_event(event.account, event.jid, event.type_)
|
|
||||||
|
|
||||||
def on_event_added(self, event):
|
|
||||||
"""
|
|
||||||
Adds "Nickname Time" to the Messaging menu.
|
|
||||||
"""
|
|
||||||
print "----", event.type_
|
|
||||||
|
|
||||||
# Basic variables
|
|
||||||
account = event.account
|
|
||||||
jid = event.jid
|
|
||||||
when = time.time()
|
|
||||||
contact = ""
|
|
||||||
key = (account, jid)
|
|
||||||
|
|
||||||
# Check if the event is valid and modify the variables
|
|
||||||
if event.type_ == "chat" or \
|
|
||||||
event.type_ == "printed_chat" or \
|
|
||||||
event.type_ == "normal" or \
|
|
||||||
event.type_ == "printed_normal" or \
|
|
||||||
event.type_ == "file-request" or \
|
|
||||||
event.type_ == "jingle-incoming":
|
|
||||||
contact = gajim.contacts.get_contact_from_full_jid(account, jid)
|
|
||||||
if contact:
|
|
||||||
contact = contact.get_shown_name()
|
|
||||||
else:
|
|
||||||
contact = jid
|
|
||||||
elif event.type_ == "pm" or event.type_ == "printed_pm":
|
|
||||||
contact = gajim.get_nick_from_jid(gajim.get_room_from_fjid(jid)) +\
|
|
||||||
"/" + gajim.get_room_and_nick_from_fjid(jid)[1]
|
|
||||||
elif event.type_ == "printed_marked_gc_msg":
|
|
||||||
contact = gajim.get_nick_from_jid(gajim.get_room_from_fjid(jid))
|
|
||||||
else:
|
|
||||||
print "ignored"
|
|
||||||
return
|
|
||||||
|
|
||||||
print account, jid, when, contact
|
|
||||||
|
|
||||||
# Add a new indicator if necessary
|
|
||||||
if key not in self.events:
|
|
||||||
indicator = indicate.Indicator()
|
|
||||||
indicator.set_property("name", contact)
|
|
||||||
indicator.set_property_time("time", when)
|
|
||||||
indicator.set_property_bool("draw-attention", True)
|
|
||||||
if gajim.config.get("show_avatars_in_roster"):
|
|
||||||
pixbuf = gtkgui_helpers.get_avatar_pixbuf_from_cache(jid)
|
|
||||||
if pixbuf not in (None, "ask"):
|
|
||||||
indicator.set_property_icon("icon", pixbuf)
|
|
||||||
indicator.connect("user-display", self.on_indicator_activate)
|
|
||||||
indicator.show()
|
|
||||||
indicator.key = key
|
|
||||||
self.events[key] = (indicator, [])
|
|
||||||
|
|
||||||
# Prepare the event and save it
|
|
||||||
event.time = when
|
|
||||||
self.events[key][1].append(event)
|
|
||||||
|
|
||||||
def on_event_removed(self, events):
|
|
||||||
"""
|
|
||||||
Goes through the events and removes them from the array and
|
|
||||||
the indicator if there are no longer any events pending.
|
|
||||||
"""
|
|
||||||
for event in events:
|
|
||||||
print "====", event.type_
|
|
||||||
|
|
||||||
key = (event.account, event.jid)
|
|
||||||
|
|
||||||
if key not in self.events and \
|
|
||||||
event in self.events[key][1]:
|
|
||||||
self.events[key][1].remove(event)
|
|
||||||
|
|
||||||
if len(self.events[key][1]) == 0: # remove indicator
|
|
||||||
self.events[key][0].hide()
|
|
||||||
del self.events[key]
|
|
||||||
else: # set the indicator time to the text event
|
|
||||||
self.events[key][0].set_property_time("time",
|
|
||||||
self.events[key][1][0].time)
|
|
||||||
else:
|
|
||||||
print "ignored"
|
|
||||||
|
Before Width: | Height: | Size: 817 B |
@@ -1 +0,0 @@
|
|||||||
from url_shortener import UrlShortenerPlugin
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<interface>
|
|
||||||
<requires lib="gtk+" version="2.16"/>
|
|
||||||
<!-- interface-naming-policy toplevel-contextual -->
|
|
||||||
<object class="GtkWindow" id="window1">
|
|
||||||
<child>
|
|
||||||
<object class="GtkVBox" id="vbox1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkFrame" id="frame1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label_xalign">0</property>
|
|
||||||
<property name="shadow_type">none</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkTable" id="table1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="n_rows">2</property>
|
|
||||||
<property name="n_columns">2</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkSpinButton" id="max_chars">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="invisible_char">●</property>
|
|
||||||
<property name="width_chars">6</property>
|
|
||||||
<property name="snap_to_ticks">True</property>
|
|
||||||
<property name="numeric">True</property>
|
|
||||||
<signal name="value_changed" handler="avatar_size_value_changed"/>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="y_options"/>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkSpinButton" id="in_max_chars">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="invisible_char">●</property>
|
|
||||||
<property name="width_chars">6</property>
|
|
||||||
<property name="snap_to_ticks">True</property>
|
|
||||||
<property name="numeric">True</property>
|
|
||||||
<signal name="value_changed" handler="on_in_max_chars_value_changed"/>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="top_attach">1</property>
|
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
<property name="y_options">GTK_EXPAND</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="incoming message">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="xpad">13</property>
|
|
||||||
<property name="label" translatable="yes">incoming message</property>
|
|
||||||
<property name="track_visited_links">False</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="top_attach">1</property>
|
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
<property name="y_options">GTK_EXPAND</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="avatar_size_lebel">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="xpad">12</property>
|
|
||||||
<property name="label" translatable="yes">outgoing message</property>
|
|
||||||
<property name="track_visited_links">False</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="y_options">GTK_EXPAND</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child type="label">
|
|
||||||
<object class="GtkLabel" id="label1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label" translatable="yes"><b>The maximum length not be shortened links(chars):</b></property>
|
|
||||||
<property name="use_markup">True</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="padding">6</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkCheckButton" id="shorten_outgoing">
|
|
||||||
<property name="label" translatable="yes">shorten links in outgoing messages</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">False</property>
|
|
||||||
<property name="active">True</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
<signal name="toggled" handler="shorten_outgoing_toggled"/>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</interface>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
[info]
|
|
||||||
name: Url Shortener
|
|
||||||
short_name: url_shortener
|
|
||||||
version: 0.3.5
|
|
||||||
description: Plugin that allows users to shorten a long URL in messages.
|
|
||||||
For example, you can turn this link:
|
|
||||||
https://trac.gajim.org/timeline
|
|
||||||
Into this link:
|
|
||||||
http://bit.ly/THy6ZK
|
|
||||||
authors: Denis Fomin <fominde@gmail.com>
|
|
||||||
homepage: http://trac-plugins.gajim.org/wiki/UrlShortenerPlugin
|
|
||||||
max_gajim_version: 0.15.9
|
|
||||||
|
Before Width: | Height: | Size: 373 B |
@@ -1,232 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import gtk
|
|
||||||
import json
|
|
||||||
import urllib
|
|
||||||
import urllib2
|
|
||||||
from common import app
|
|
||||||
from common import ged
|
|
||||||
from plugins import GajimPlugin
|
|
||||||
from plugins.helpers import log_calls
|
|
||||||
from plugins.gui import GajimPluginConfigDialog
|
|
||||||
|
|
||||||
APIKEY = 'R_fcba926fc7978bd19acbca73ec82b2be'
|
|
||||||
USER = 'dicson'
|
|
||||||
|
|
||||||
class UrlShortenerPlugin(GajimPlugin):
|
|
||||||
@log_calls('UrlShortenerPlugin')
|
|
||||||
def init(self):
|
|
||||||
self.description = _('Plugin that allows users to shorten a long URL '
|
|
||||||
'in received messages.\n'
|
|
||||||
'For example, you can turn this link:\n'
|
|
||||||
'https://trac.gajim.org/timeline\n'
|
|
||||||
'Into this link:\n'
|
|
||||||
'http://bit.ly/THy6ZK')
|
|
||||||
self.config_dialog = UrlShortenerPluginConfigDialog(self)
|
|
||||||
self.gui_extension_points = {
|
|
||||||
'chat_control_base': (self.connect_with_chat_control,
|
|
||||||
self.disconnect_from_chat_control),
|
|
||||||
'print_special_text': (self.print_special_text,
|
|
||||||
self.print_special_text1),}
|
|
||||||
self.config_default_values = {
|
|
||||||
'MAX_CHARS': (50, ('MAX_CHARS(30-...)')),
|
|
||||||
'IN_MAX_CHARS': (50, ('MAX_CHARS(30-...)')),
|
|
||||||
'SHORTEN_OUTGOING': (False, ''),}
|
|
||||||
self.events_handlers = {'message-outgoing': (ged.OUT_PRECORE,
|
|
||||||
self.handle_outgoing_msg),
|
|
||||||
'gc-message-outgoing': (ged.OUT_PRECORE,
|
|
||||||
self.handle_outgoing_msg)}
|
|
||||||
self.chat_control = None
|
|
||||||
self.controls = []
|
|
||||||
|
|
||||||
def handle_outgoing_msg(self, event):
|
|
||||||
if not self.active:
|
|
||||||
return
|
|
||||||
if not event.message:
|
|
||||||
return
|
|
||||||
if not self.config['SHORTEN_OUTGOING']:
|
|
||||||
return
|
|
||||||
if hasattr(event, 'shortened'):
|
|
||||||
return
|
|
||||||
|
|
||||||
iterator = app.interface.basic_pattern_re.finditer(event.message)
|
|
||||||
for match in iterator:
|
|
||||||
start, end = match.span()
|
|
||||||
link = event.message[start:end]
|
|
||||||
if len(link) < self.config['MAX_CHARS']:
|
|
||||||
continue
|
|
||||||
short_link = None
|
|
||||||
try:
|
|
||||||
params = urllib.urlencode({'longUrl': link,
|
|
||||||
'login': USER,
|
|
||||||
'apiKey': APIKEY,
|
|
||||||
'format': 'json'})
|
|
||||||
req = urllib2.Request('http://api.bit.ly/v3/shorten?%s' % params)
|
|
||||||
response = urllib2.urlopen(req)
|
|
||||||
j = json.load(response)
|
|
||||||
if j['status_code'] == 200:
|
|
||||||
short_link = j['data']['url']
|
|
||||||
except urllib2.HTTPError, e:
|
|
||||||
pass
|
|
||||||
if short_link:
|
|
||||||
event.message = event.message.replace(link, short_link)
|
|
||||||
event.callback_args[1] = event.message
|
|
||||||
event.shortened = True
|
|
||||||
|
|
||||||
@log_calls('UrlShortenerPlugin')
|
|
||||||
def connect_with_chat_control(self, chat_control):
|
|
||||||
self.chat_control = chat_control
|
|
||||||
control = Base(self, self.chat_control)
|
|
||||||
self.controls.append(control)
|
|
||||||
|
|
||||||
@log_calls('UrlShortenerPlugin')
|
|
||||||
def disconnect_from_chat_control(self, chat_control):
|
|
||||||
for control in self.controls:
|
|
||||||
control.disconnect_from_chat_control()
|
|
||||||
self.controls = []
|
|
||||||
|
|
||||||
def print_special_text(self, tv, special_text, other_tags, graphics=True,
|
|
||||||
additional_data={}):
|
|
||||||
for control in self.controls:
|
|
||||||
if control.chat_control.conv_textview != tv:
|
|
||||||
continue
|
|
||||||
control.print_special_text(special_text, other_tags, graphics=True)
|
|
||||||
|
|
||||||
def print_special_text1(self, chat_control, special_text, other_tags=None,
|
|
||||||
graphics=True, additional_data={}):
|
|
||||||
for control in self.controls:
|
|
||||||
if control.chat_control == chat_control:
|
|
||||||
control.disconnect_from_chat_control()
|
|
||||||
self.controls.remove(control)
|
|
||||||
|
|
||||||
|
|
||||||
class Base(object):
|
|
||||||
def __init__(self, plugin, chat_control):
|
|
||||||
self.plugin = plugin
|
|
||||||
self.chat_control = chat_control
|
|
||||||
self.textview = self.chat_control.conv_textview
|
|
||||||
|
|
||||||
self.id_ = self.textview.tv.connect('motion_notify_event',
|
|
||||||
self.on_textview_motion_notify_event)
|
|
||||||
self.chat_control.handlers[self.id_] = self.textview.tv
|
|
||||||
|
|
||||||
def print_special_text(self, special_text, other_tags, graphics=True):
|
|
||||||
if not self.plugin.active:
|
|
||||||
return
|
|
||||||
is_xhtml_link = None
|
|
||||||
text_is_valid_uri = False
|
|
||||||
buffer_ = self.textview.tv.get_buffer()
|
|
||||||
|
|
||||||
# Detect XHTML-IM link
|
|
||||||
ttt = buffer_.get_tag_table()
|
|
||||||
tags_ = [(ttt.lookup(t) if isinstance(t, str) else t) for t in other_tags]
|
|
||||||
for t in tags_:
|
|
||||||
is_xhtml_link = getattr(t, 'href', None)
|
|
||||||
if is_xhtml_link:
|
|
||||||
break
|
|
||||||
# Check if we accept this as an uri
|
|
||||||
schemes = app.config.get('uri_schemes').split()
|
|
||||||
for scheme in schemes:
|
|
||||||
if special_text.startswith(scheme):
|
|
||||||
text_is_valid_uri = True
|
|
||||||
if special_text.startswith('www.') or special_text.startswith('ftp.') \
|
|
||||||
or text_is_valid_uri and not is_xhtml_link:
|
|
||||||
if len(special_text) < self.plugin.config['IN_MAX_CHARS']:
|
|
||||||
return
|
|
||||||
end_iter = buffer_.get_end_iter()
|
|
||||||
mark = buffer_.create_mark(None, end_iter, True)
|
|
||||||
app.thread_interface(self.insert_hyperlink, [mark, special_text,
|
|
||||||
ttt])
|
|
||||||
self.textview.plugin_modified = True
|
|
||||||
|
|
||||||
def insert_hyperlink(self, mark, special_text, ttt):
|
|
||||||
try:
|
|
||||||
params = urllib.urlencode({'longUrl': special_text,
|
|
||||||
'login': USER,
|
|
||||||
'apiKey': APIKEY,
|
|
||||||
'format': 'json'})
|
|
||||||
req = urllib2.Request('http://api.bit.ly/v3/shorten?%s' % params)
|
|
||||||
response = urllib2.urlopen(req)
|
|
||||||
j = json.load(response)
|
|
||||||
if j['status_code'] == 200:
|
|
||||||
special_text = j['data']['url']
|
|
||||||
except urllib2.HTTPError, e:
|
|
||||||
pass
|
|
||||||
|
|
||||||
buffer_ = mark.get_buffer()
|
|
||||||
end_iter = buffer_.get_iter_at_mark(mark)
|
|
||||||
buffer_.insert_with_tags(end_iter, special_text, ttt.lookup('url'))
|
|
||||||
|
|
||||||
def on_textview_motion_notify_event(self, widget, event):
|
|
||||||
pointer_x, pointer_y = self.textview.tv.window.get_pointer()[0:2]
|
|
||||||
x, y = self.textview.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT,
|
|
||||||
pointer_x, pointer_y)
|
|
||||||
tags = self.textview.tv.get_iter_at_location(x, y).get_tags()
|
|
||||||
tag_table = self.textview.tv.get_buffer().get_tag_table()
|
|
||||||
buffer_ = self.textview.tv.get_buffer()
|
|
||||||
for tag in tags:
|
|
||||||
if tag != tag_table.lookup('url'):
|
|
||||||
continue
|
|
||||||
it = self.textview.tv.get_iter_at_location(x, y)
|
|
||||||
st = it.copy()
|
|
||||||
st.backward_to_tag_toggle(tag_table.lookup('url'))
|
|
||||||
it.forward_to_tag_toggle(tag_table.lookup('url'))
|
|
||||||
text = buffer_.get_text(st, it, include_hidden_chars=True)
|
|
||||||
if text.startswith('http://bit.ly/'):
|
|
||||||
try:
|
|
||||||
params = urllib.urlencode({'shortUrl': text,
|
|
||||||
'login': USER,
|
|
||||||
'apiKey': APIKEY,
|
|
||||||
'format': 'json'})
|
|
||||||
req = urllib2.Request('http://api.bit.ly/v3/expand?%s' \
|
|
||||||
% params)
|
|
||||||
response = urllib2.urlopen(req)
|
|
||||||
j = json.load(response)
|
|
||||||
if j['status_code'] != 200:
|
|
||||||
raise Exception('%s'%j['status_txt'])
|
|
||||||
txt = j['data']['expand'][0]['long_url']
|
|
||||||
self.textview.tv.set_tooltip_text(txt)
|
|
||||||
self.textview.on_textview_motion_notify_event(widget, event)
|
|
||||||
return
|
|
||||||
except Exception, e:
|
|
||||||
break
|
|
||||||
|
|
||||||
self.textview.tv.set_tooltip_text('')
|
|
||||||
self.textview.on_textview_motion_notify_event(widget, event)
|
|
||||||
|
|
||||||
def disconnect_from_chat_control(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class UrlShortenerPluginConfigDialog(GajimPluginConfigDialog):
|
|
||||||
def init(self):
|
|
||||||
self.GTK_BUILDER_FILE_PATH = self.plugin.local_file_path(
|
|
||||||
'config_dialog.ui')
|
|
||||||
self.xml = gtk.Builder()
|
|
||||||
self.xml.set_translation_domain('gajim_plugins')
|
|
||||||
self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH, ['vbox1'])
|
|
||||||
self.max_chars_spinbutton = self.xml.get_object('max_chars')
|
|
||||||
self.max_chars_spinbutton.get_adjustment().set_all(30, 30, 99999, 1,
|
|
||||||
10, 0)
|
|
||||||
self.in_max_chars_spinbutton = self.xml.get_object('in_max_chars')
|
|
||||||
self.in_max_chars_spinbutton.get_adjustment().set_all(30, 30, 99999, 1,
|
|
||||||
10, 0)
|
|
||||||
self.shorten_outgoing = self.xml.get_object('shorten_outgoing')
|
|
||||||
hbox = self.xml.get_object('vbox1')
|
|
||||||
self.child.pack_start(hbox)
|
|
||||||
|
|
||||||
self.xml.connect_signals(self)
|
|
||||||
|
|
||||||
def on_run(self):
|
|
||||||
self.max_chars_spinbutton.set_value(self.plugin.config['MAX_CHARS'])
|
|
||||||
self.in_max_chars_spinbutton.set_value(self.plugin.config['IN_MAX_CHARS'])
|
|
||||||
self.shorten_outgoing.set_active(self.plugin.config['SHORTEN_OUTGOING'])
|
|
||||||
|
|
||||||
def avatar_size_value_changed(self, spinbutton):
|
|
||||||
self.plugin.config['MAX_CHARS'] = spinbutton.get_value()
|
|
||||||
|
|
||||||
def on_in_max_chars_value_changed(self, spinbutton):
|
|
||||||
self.plugin.config['IN_MAX_CHARS'] = spinbutton.get_value()
|
|
||||||
|
|
||||||
def shorten_outgoing_toggled(self, checkbutton):
|
|
||||||
self.plugin.config['SHORTEN_OUTGOING'] = checkbutton.get_active()
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from plugin import WhiteboardPlugin
|
|
||||||
|
Before Width: | Height: | Size: 806 B |
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,8 +0,0 @@
|
|||||||
[info]
|
|
||||||
name: Whiteboard
|
|
||||||
short_name: whiteboard
|
|
||||||
version: 0.2.2
|
|
||||||
description: Shows a whiteboard in chat. python-pygoocanvas is required.
|
|
||||||
authors = Yann Leboulanger <asterix@lagaule.org>
|
|
||||||
homepage = http://trac-plugins.gajim.org/wiki/WhiteboardPlugin
|
|
||||||
max_gajim_version: 0.15.9
|
|
||||||
|
Before Width: | Height: | Size: 989 B |
@@ -1,486 +0,0 @@
|
|||||||
## plugins/whiteboard/plugin.py
|
|
||||||
##
|
|
||||||
## Copyright (C) 2009 Jeff Ling <jeff.ummu AT gmail.com>
|
|
||||||
## Copyright (C) 2010 Yann Leboulanger <asterix AT lagaule.org>
|
|
||||||
##
|
|
||||||
## This file is part of Gajim.
|
|
||||||
##
|
|
||||||
## Gajim is free software; you can redistribute it and/or modify
|
|
||||||
## it under the terms of the GNU General Public License as published
|
|
||||||
## by the Free Software Foundation; version 3 only.
|
|
||||||
##
|
|
||||||
## Gajim is distributed in the hope that it will be useful,
|
|
||||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
## GNU General Public License for more details.
|
|
||||||
##
|
|
||||||
## You should have received a copy of the GNU General Public License
|
|
||||||
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
##
|
|
||||||
|
|
||||||
'''
|
|
||||||
Whiteboard plugin.
|
|
||||||
|
|
||||||
:author: Yann Leboulanger <asterix@lagaule.org>
|
|
||||||
:since: 1st November 2010
|
|
||||||
:copyright: Copyright (2010) Yann Leboulanger <asterix@lagaule.org>
|
|
||||||
:license: GPL
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
from common import helpers
|
|
||||||
from common import gajim
|
|
||||||
from plugins import GajimPlugin
|
|
||||||
from plugins.plugin import GajimPluginException
|
|
||||||
from plugins.helpers import log_calls, log
|
|
||||||
import common.xmpp
|
|
||||||
import gtk
|
|
||||||
import chat_control
|
|
||||||
from common import ged
|
|
||||||
from common.jingle_session import JingleSession
|
|
||||||
from common.jingle_content import JingleContent
|
|
||||||
from common.jingle_transport import JingleTransport, TransportType
|
|
||||||
import dialogs
|
|
||||||
from whiteboard_widget import Whiteboard, HAS_GOOCANVAS
|
|
||||||
from common import xmpp
|
|
||||||
from common import caps_cache
|
|
||||||
|
|
||||||
NS_JINGLE_XHTML = 'urn:xmpp:tmp:jingle:apps:xhtml'
|
|
||||||
NS_JINGLE_SXE = 'urn:xmpp:tmp:jingle:transports:sxe'
|
|
||||||
NS_SXE = 'urn:xmpp:sxe:0'
|
|
||||||
|
|
||||||
class WhiteboardPlugin(GajimPlugin):
|
|
||||||
@log_calls('WhiteboardPlugin')
|
|
||||||
def init(self):
|
|
||||||
self.description = _('Shows a whiteboard in chat.'
|
|
||||||
' python-pygoocanvas is required.')
|
|
||||||
self.config_dialog = None
|
|
||||||
self.events_handlers = {
|
|
||||||
'jingle-request-received': (ged.GUI1, self._nec_jingle_received),
|
|
||||||
'jingle-connected-received': (ged.GUI1, self._nec_jingle_connected),
|
|
||||||
'jingle-disconnected-received': (ged.GUI1,
|
|
||||||
self._nec_jingle_disconnected),
|
|
||||||
'raw-message-received': (ged.GUI1, self._nec_raw_message),
|
|
||||||
}
|
|
||||||
self.gui_extension_points = {
|
|
||||||
'chat_control_base' : (self.connect_with_chat_control,
|
|
||||||
self.disconnect_from_chat_control),
|
|
||||||
'chat_control_base_update_toolbar': (self.update_button_state,
|
|
||||||
None),
|
|
||||||
}
|
|
||||||
self.controls = []
|
|
||||||
self.sid = None
|
|
||||||
|
|
||||||
@log_calls('WhiteboardPlugin')
|
|
||||||
def _compute_caps_hash(self):
|
|
||||||
for a in gajim.connections:
|
|
||||||
gajim.caps_hash[a] = caps_cache.compute_caps_hash([
|
|
||||||
gajim.gajim_identity], gajim.gajim_common_features + \
|
|
||||||
gajim.gajim_optional_features[a])
|
|
||||||
# re-send presence with new hash
|
|
||||||
connected = gajim.connections[a].connected
|
|
||||||
if connected > 1 and gajim.SHOW_LIST[connected] != 'invisible':
|
|
||||||
gajim.connections[a].change_status(gajim.SHOW_LIST[connected],
|
|
||||||
gajim.connections[a].status)
|
|
||||||
|
|
||||||
@log_calls('WhiteboardPlugin')
|
|
||||||
def activate(self):
|
|
||||||
if not HAS_GOOCANVAS:
|
|
||||||
raise GajimPluginException('python-pygoocanvas is missing!')
|
|
||||||
if NS_JINGLE_SXE not in gajim.gajim_common_features:
|
|
||||||
gajim.gajim_common_features.append(NS_JINGLE_SXE)
|
|
||||||
if NS_SXE not in gajim.gajim_common_features:
|
|
||||||
gajim.gajim_common_features.append(NS_SXE)
|
|
||||||
self._compute_caps_hash()
|
|
||||||
|
|
||||||
@log_calls('WhiteboardPlugin')
|
|
||||||
def deactivate(self):
|
|
||||||
if NS_JINGLE_SXE in gajim.gajim_common_features:
|
|
||||||
gajim.gajim_common_features.remove(NS_JINGLE_SXE)
|
|
||||||
if NS_SXE in gajim.gajim_common_features:
|
|
||||||
gajim.gajim_common_features.remove(NS_SXE)
|
|
||||||
self._compute_caps_hash()
|
|
||||||
|
|
||||||
@log_calls('WhiteboardPlugin')
|
|
||||||
def connect_with_chat_control(self, control):
|
|
||||||
if isinstance(control, chat_control.ChatControl):
|
|
||||||
base = Base(self, control)
|
|
||||||
self.controls.append(base)
|
|
||||||
|
|
||||||
@log_calls('WhiteboardPlugin')
|
|
||||||
def disconnect_from_chat_control(self, chat_control):
|
|
||||||
for base in self.controls:
|
|
||||||
base.disconnect_from_chat_control()
|
|
||||||
self.controls = []
|
|
||||||
|
|
||||||
@log_calls('WhiteboardPlugin')
|
|
||||||
def update_button_state(self, control):
|
|
||||||
for base in self.controls:
|
|
||||||
if base.chat_control == control:
|
|
||||||
if control.contact.supports(NS_JINGLE_SXE) and \
|
|
||||||
control.contact.supports(NS_SXE):
|
|
||||||
base.button.set_sensitive(True)
|
|
||||||
tooltip_text = _('Show whiteboard')
|
|
||||||
else:
|
|
||||||
base.button.set_sensitive(False)
|
|
||||||
tooltip_text = _('Client on the other side '
|
|
||||||
'does not support the whiteboard')
|
|
||||||
base.button.set_tooltip_text(tooltip_text)
|
|
||||||
|
|
||||||
@log_calls('WhiteboardPlugin')
|
|
||||||
def show_request_dialog(self, account, fjid, jid, sid, content_types):
|
|
||||||
def on_ok():
|
|
||||||
session = gajim.connections[account].get_jingle_session(fjid, sid)
|
|
||||||
self.sid = session.sid
|
|
||||||
if not session.accepted:
|
|
||||||
session.approve_session()
|
|
||||||
for content in content_types:
|
|
||||||
session.approve_content(content)
|
|
||||||
for _jid in (fjid, jid):
|
|
||||||
ctrl = gajim.interface.msg_win_mgr.get_control(_jid, account)
|
|
||||||
if ctrl:
|
|
||||||
break
|
|
||||||
if not ctrl:
|
|
||||||
# create it
|
|
||||||
gajim.interface.new_chat_from_jid(account, jid)
|
|
||||||
ctrl = gajim.interface.msg_win_mgr.get_control(jid, account)
|
|
||||||
session = session.contents[('initiator', 'xhtml')]
|
|
||||||
ctrl.draw_whiteboard(session)
|
|
||||||
|
|
||||||
def on_cancel():
|
|
||||||
session = gajim.connections[account].get_jingle_session(fjid, sid)
|
|
||||||
session.decline_session()
|
|
||||||
|
|
||||||
contact = gajim.contacts.get_first_contact_from_jid(account, jid)
|
|
||||||
if contact:
|
|
||||||
name = contact.get_shown_name()
|
|
||||||
else:
|
|
||||||
name = jid
|
|
||||||
pritext = _('Incoming Whiteboard')
|
|
||||||
sectext = _('%(name)s (%(jid)s) wants to start a whiteboard with '
|
|
||||||
'you. Do you want to accept?') % {'name': name, 'jid': jid}
|
|
||||||
dialog = dialogs.NonModalConfirmationDialog(pritext, sectext=sectext,
|
|
||||||
on_response_ok=on_ok, on_response_cancel=on_cancel)
|
|
||||||
dialog.popup()
|
|
||||||
|
|
||||||
@log_calls('WhiteboardPlugin')
|
|
||||||
def _nec_jingle_received(self, obj):
|
|
||||||
if not HAS_GOOCANVAS:
|
|
||||||
return
|
|
||||||
content_types = set(c[0] for c in obj.contents)
|
|
||||||
if 'xhtml' not in content_types:
|
|
||||||
return
|
|
||||||
self.show_request_dialog(obj.conn.name, obj.fjid, obj.jid, obj.sid,
|
|
||||||
content_types)
|
|
||||||
|
|
||||||
@log_calls('WhiteboardPlugin')
|
|
||||||
def _nec_jingle_connected(self, obj):
|
|
||||||
if not HAS_GOOCANVAS:
|
|
||||||
return
|
|
||||||
account = obj.conn.name
|
|
||||||
ctrl = (gajim.interface.msg_win_mgr.get_control(obj.fjid, account)
|
|
||||||
or gajim.interface.msg_win_mgr.get_control(obj.jid, account))
|
|
||||||
if not ctrl:
|
|
||||||
return
|
|
||||||
session = gajim.connections[obj.conn.name].get_jingle_session(obj.fjid,
|
|
||||||
obj.sid)
|
|
||||||
|
|
||||||
if ('initiator', 'xhtml') not in session.contents:
|
|
||||||
return
|
|
||||||
|
|
||||||
session = session.contents[('initiator', 'xhtml')]
|
|
||||||
ctrl.draw_whiteboard(session)
|
|
||||||
|
|
||||||
@log_calls('WhiteboardPlugin')
|
|
||||||
def _nec_jingle_disconnected(self, obj):
|
|
||||||
for base in self.controls:
|
|
||||||
if base.sid == obj.sid:
|
|
||||||
base.stop_whiteboard(reason = obj.reason)
|
|
||||||
|
|
||||||
@log_calls('WhiteboardPlugin')
|
|
||||||
def _nec_raw_message(self, obj):
|
|
||||||
if not HAS_GOOCANVAS:
|
|
||||||
return
|
|
||||||
if obj.stanza.getTag('sxe', namespace=NS_SXE):
|
|
||||||
account = obj.conn.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
fjid = helpers.get_full_jid_from_iq(obj.stanza)
|
|
||||||
except helpers.InvalidFormat:
|
|
||||||
obj.conn.dispatch('ERROR', (_('Invalid Jabber ID'),
|
|
||||||
_('A message from a non-valid JID arrived, it has been '
|
|
||||||
'ignored.')))
|
|
||||||
|
|
||||||
jid = gajim.get_jid_without_resource(fjid)
|
|
||||||
ctrl = (gajim.interface.msg_win_mgr.get_control(fjid, account)
|
|
||||||
or gajim.interface.msg_win_mgr.get_control(jid, account))
|
|
||||||
if not ctrl:
|
|
||||||
return
|
|
||||||
sxe = obj.stanza.getTag('sxe')
|
|
||||||
if not sxe:
|
|
||||||
return
|
|
||||||
sid = sxe.getAttr('session')
|
|
||||||
if (jid, sid) not in obj.conn._sessions:
|
|
||||||
pass
|
|
||||||
# newjingle = JingleSession(con=self, weinitiate=False, jid=jid, sid=sid)
|
|
||||||
# self.addJingle(newjingle)
|
|
||||||
|
|
||||||
# we already have such session in dispatcher...
|
|
||||||
session = obj.conn.get_jingle_session(fjid, sid)
|
|
||||||
cn = session.contents[('initiator', 'xhtml')]
|
|
||||||
error = obj.stanza.getTag('error')
|
|
||||||
if error:
|
|
||||||
action = 'iq-error'
|
|
||||||
else:
|
|
||||||
action = 'edit'
|
|
||||||
|
|
||||||
cn.on_stanza(obj.stanza, sxe, error, action)
|
|
||||||
# def __editCB(self, stanza, content, error, action):
|
|
||||||
#new_tags = sxe.getTags('new')
|
|
||||||
#remove_tags = sxe.getTags('remove')
|
|
||||||
|
|
||||||
#if new_tags is not None:
|
|
||||||
## Process new elements
|
|
||||||
#for tag in new_tags:
|
|
||||||
#if tag.getAttr('type') == 'element':
|
|
||||||
#ctrl.whiteboard.recieve_element(tag)
|
|
||||||
#elif tag.getAttr('type') == 'attr':
|
|
||||||
#ctrl.whiteboard.recieve_attr(tag)
|
|
||||||
#ctrl.whiteboard.apply_new()
|
|
||||||
|
|
||||||
#if remove_tags is not None:
|
|
||||||
## Delete rids
|
|
||||||
#for tag in remove_tags:
|
|
||||||
#target = tag.getAttr('target')
|
|
||||||
#ctrl.whiteboard.image.del_rid(target)
|
|
||||||
|
|
||||||
# Stop propagating this event, it's handled
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class Base(object):
|
|
||||||
def __init__(self, plugin, chat_control):
|
|
||||||
self.plugin = plugin
|
|
||||||
self.chat_control = chat_control
|
|
||||||
self.chat_control.draw_whiteboard = self.draw_whiteboard
|
|
||||||
self.contact = self.chat_control.contact
|
|
||||||
self.account = self.chat_control.account
|
|
||||||
self.jid = self.contact.get_full_jid()
|
|
||||||
self.create_buttons()
|
|
||||||
self.whiteboard = None
|
|
||||||
self.sid = None
|
|
||||||
|
|
||||||
def create_buttons(self):
|
|
||||||
# create whiteboard button
|
|
||||||
actions_hbox = self.chat_control.xml.get_object('actions_hbox')
|
|
||||||
self.button = gtk.ToggleButton(label=None, use_underline=True)
|
|
||||||
self.button.set_property('relief', gtk.RELIEF_NONE)
|
|
||||||
self.button.set_property('can-focus', False)
|
|
||||||
img = gtk.Image()
|
|
||||||
img_path = self.plugin.local_file_path('whiteboard.png')
|
|
||||||
pixbuf = gtk.gdk.pixbuf_new_from_file(img_path)
|
|
||||||
iconset = gtk.IconSet(pixbuf=pixbuf)
|
|
||||||
factory = gtk.IconFactory()
|
|
||||||
factory.add('whiteboard', iconset)
|
|
||||||
img_path = self.plugin.local_file_path('brush_tool.png')
|
|
||||||
pixbuf = gtk.gdk.pixbuf_new_from_file(img_path)
|
|
||||||
iconset = gtk.IconSet(pixbuf=pixbuf)
|
|
||||||
factory.add('brush_tool', iconset)
|
|
||||||
img_path = self.plugin.local_file_path('line_tool.png')
|
|
||||||
pixbuf = gtk.gdk.pixbuf_new_from_file(img_path)
|
|
||||||
iconset = gtk.IconSet(pixbuf=pixbuf)
|
|
||||||
factory.add('line_tool', iconset)
|
|
||||||
img_path = self.plugin.local_file_path('oval_tool.png')
|
|
||||||
pixbuf = gtk.gdk.pixbuf_new_from_file(img_path)
|
|
||||||
iconset = gtk.IconSet(pixbuf=pixbuf)
|
|
||||||
factory.add('oval_tool', iconset)
|
|
||||||
factory.add_default()
|
|
||||||
img.set_from_stock('whiteboard', gtk.ICON_SIZE_MENU)
|
|
||||||
self.button.set_image(img)
|
|
||||||
send_button = self.chat_control.xml.get_object('send_button')
|
|
||||||
send_button_pos = actions_hbox.child_get_property(send_button,
|
|
||||||
'position')
|
|
||||||
actions_hbox.add_with_properties(self.button, 'position',
|
|
||||||
send_button_pos - 1, 'expand', False)
|
|
||||||
id_ = self.button.connect('toggled', self.on_whiteboard_button_toggled)
|
|
||||||
self.chat_control.handlers[id_] = self.button
|
|
||||||
self.button.show()
|
|
||||||
|
|
||||||
def draw_whiteboard(self, content):
|
|
||||||
hbox = self.chat_control.xml.get_object('chat_control_hbox')
|
|
||||||
if len(hbox.get_children()) == 1:
|
|
||||||
self.whiteboard = Whiteboard(self.account, self.contact, content,
|
|
||||||
self.plugin)
|
|
||||||
# set minimum size
|
|
||||||
self.whiteboard.hbox.set_size_request(300, 0)
|
|
||||||
hbox.pack_start(self.whiteboard.hbox, expand=False, fill=False)
|
|
||||||
self.whiteboard.hbox.show_all()
|
|
||||||
self.button.set_active(True)
|
|
||||||
content.control = self
|
|
||||||
self.sid = content.session.sid
|
|
||||||
|
|
||||||
def on_whiteboard_button_toggled(self, widget):
|
|
||||||
"""
|
|
||||||
Popup whiteboard
|
|
||||||
"""
|
|
||||||
if widget.get_active():
|
|
||||||
if not self.whiteboard:
|
|
||||||
self.start_whiteboard()
|
|
||||||
else:
|
|
||||||
self.stop_whiteboard()
|
|
||||||
|
|
||||||
def start_whiteboard(self):
|
|
||||||
conn = gajim.connections[self.chat_control.account]
|
|
||||||
jingle = JingleSession(conn, weinitiate=True, jid=self.jid)
|
|
||||||
self.sid = jingle.sid
|
|
||||||
conn._sessions[jingle.sid] = jingle
|
|
||||||
content = JingleWhiteboard(jingle)
|
|
||||||
content.control = self
|
|
||||||
jingle.add_content('xhtml', content)
|
|
||||||
jingle.start_session()
|
|
||||||
|
|
||||||
def stop_whiteboard(self, reason=None):
|
|
||||||
conn = gajim.connections[self.chat_control.account]
|
|
||||||
self.sid = None
|
|
||||||
session = conn.get_jingle_session(self.jid, media='xhtml')
|
|
||||||
if session:
|
|
||||||
session.end_session()
|
|
||||||
self.button.set_active(False)
|
|
||||||
if reason:
|
|
||||||
txt = _('Whiteboard stopped: %(reason)s') % {'reason': reason}
|
|
||||||
self.chat_control.print_conversation(txt, 'info')
|
|
||||||
if not self.whiteboard:
|
|
||||||
return
|
|
||||||
hbox = self.chat_control.xml.get_object('chat_control_hbox')
|
|
||||||
if self.whiteboard.hbox in hbox.get_children():
|
|
||||||
if hasattr(self.whiteboard, 'hbox'):
|
|
||||||
hbox.remove(self.whiteboard.hbox)
|
|
||||||
self.whiteboard = None
|
|
||||||
|
|
||||||
def disconnect_from_chat_control(self):
|
|
||||||
actions_hbox = self.chat_control.xml.get_object('actions_hbox')
|
|
||||||
actions_hbox.remove(self.button)
|
|
||||||
|
|
||||||
class JingleWhiteboard(JingleContent):
|
|
||||||
''' Jingle Whiteboard sessions consist of xhtml content'''
|
|
||||||
def __init__(self, session, transport=None):
|
|
||||||
if not transport:
|
|
||||||
transport = JingleTransportSXE()
|
|
||||||
JingleContent.__init__(self, session, transport)
|
|
||||||
self.media = 'xhtml'
|
|
||||||
self.negotiated = True # there is nothing to negotiate
|
|
||||||
self.last_rid = 0
|
|
||||||
self.callbacks['session-accept'] += [self._sessionAcceptCB]
|
|
||||||
self.callbacks['session-terminate'] += [self._stop]
|
|
||||||
self.callbacks['session-terminate-sent'] += [self._stop]
|
|
||||||
self.callbacks['edit'] = [self._EditCB]
|
|
||||||
|
|
||||||
def _EditCB(self, stanza, content, error, action):
|
|
||||||
new_tags = content.getTags('new')
|
|
||||||
remove_tags = content.getTags('remove')
|
|
||||||
|
|
||||||
if new_tags is not None:
|
|
||||||
# Process new elements
|
|
||||||
for tag in new_tags:
|
|
||||||
if tag.getAttr('type') == 'element':
|
|
||||||
self.control.whiteboard.recieve_element(tag)
|
|
||||||
elif tag.getAttr('type') == 'attr':
|
|
||||||
self.control.whiteboard.recieve_attr(tag)
|
|
||||||
self.control.whiteboard.apply_new()
|
|
||||||
|
|
||||||
if remove_tags is not None:
|
|
||||||
# Delete rids
|
|
||||||
for tag in remove_tags:
|
|
||||||
target = tag.getAttr('target')
|
|
||||||
self.control.whiteboard.image.del_rid(target)
|
|
||||||
|
|
||||||
def _sessionAcceptCB(self, stanza, content, error, action):
|
|
||||||
log.debug('session accepted')
|
|
||||||
self.session.connection.dispatch('WHITEBOARD_ACCEPTED',
|
|
||||||
(self.session.peerjid, self.session.sid))
|
|
||||||
|
|
||||||
def generate_rids(self, x):
|
|
||||||
# generates x number of rids and returns in list
|
|
||||||
rids = []
|
|
||||||
for x in range(x):
|
|
||||||
rids.append(str(self.last_rid))
|
|
||||||
self.last_rid += 1
|
|
||||||
return rids
|
|
||||||
|
|
||||||
def send_whiteboard_node(self, items, rids):
|
|
||||||
# takes int rid and dict items and sends it as a node
|
|
||||||
# sends new item
|
|
||||||
jid = self.session.peerjid
|
|
||||||
sid = self.session.sid
|
|
||||||
message = xmpp.Message(to=jid)
|
|
||||||
sxe = message.addChild(name='sxe', attrs={'session': sid},
|
|
||||||
namespace=NS_SXE)
|
|
||||||
|
|
||||||
for x in rids:
|
|
||||||
if items[x]['type'] == 'element':
|
|
||||||
parent = x
|
|
||||||
attrs = {'rid': x,
|
|
||||||
'name': items[x]['data'][0].getName(),
|
|
||||||
'type': items[x]['type']}
|
|
||||||
sxe.addChild(name='new', attrs=attrs)
|
|
||||||
if items[x]['type'] == 'attr':
|
|
||||||
attr_name = items[x]['data']
|
|
||||||
chdata = items[parent]['data'][0].getAttr(attr_name)
|
|
||||||
attrs = {'rid': x,
|
|
||||||
'name': attr_name,
|
|
||||||
'type': items[x]['type'],
|
|
||||||
'chdata': chdata,
|
|
||||||
'parent': parent}
|
|
||||||
sxe.addChild(name='new', attrs=attrs)
|
|
||||||
self.session.connection.connection.send(message)
|
|
||||||
|
|
||||||
def delete_whiteboard_node(self, rids):
|
|
||||||
message = xmpp.Message(to=self.session.peerjid)
|
|
||||||
sxe = message.addChild(name='sxe', attrs={'session': self.session.sid},
|
|
||||||
namespace=NS_SXE)
|
|
||||||
|
|
||||||
for x in rids:
|
|
||||||
sxe.addChild(name='remove', attrs = {'target': x})
|
|
||||||
self.session.connection.connection.send(message)
|
|
||||||
|
|
||||||
def send_items(self, items, rids):
|
|
||||||
# recieves dict items and a list of rids of items to send
|
|
||||||
# TODO: is there a less clumsy way that doesn't involve passing
|
|
||||||
# whole list
|
|
||||||
self.send_whiteboard_node(items, rids)
|
|
||||||
|
|
||||||
def del_item(self, rids):
|
|
||||||
self.delete_whiteboard_node(rids)
|
|
||||||
|
|
||||||
def encode(self, xml):
|
|
||||||
# encodes it sendable string
|
|
||||||
return 'data:text/xml,' + urllib.quote(xml)
|
|
||||||
|
|
||||||
def _fill_content(self, content):
|
|
||||||
content.addChild(NS_JINGLE_XHTML + ' description')
|
|
||||||
|
|
||||||
def _stop(self, *things):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_content(desc):
|
|
||||||
return JingleWhiteboard
|
|
||||||
|
|
||||||
common.jingle_content.contents[NS_JINGLE_XHTML] = get_content
|
|
||||||
|
|
||||||
class JingleTransportSXE(JingleTransport):
|
|
||||||
def __init__(self, node=None):
|
|
||||||
if gajim.config.get('version') == '0.15':
|
|
||||||
JingleTransport.__init__(self, TransportType.streaming)
|
|
||||||
else:
|
|
||||||
JingleTransport.__init__(self, TransportType.SOCKS5)
|
|
||||||
|
|
||||||
def make_transport(self, candidates=None):
|
|
||||||
transport = JingleTransport.make_transport(self, candidates)
|
|
||||||
transport.setNamespace(NS_JINGLE_SXE)
|
|
||||||
transport.setTagData('host', 'TODO')
|
|
||||||
return transport
|
|
||||||
|
|
||||||
common.jingle_transport.transports[NS_JINGLE_SXE] = JingleTransportSXE
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,418 +0,0 @@
|
|||||||
## plugins/whiteboard/whiteboard_widget.py
|
|
||||||
##
|
|
||||||
## Copyright (C) 2009 Jeff Ling <jeff.ummu AT gmail.com>
|
|
||||||
## Copyright (C) 2010 Yann Leboulanger <asterix AT lagaule.org>
|
|
||||||
##
|
|
||||||
## This file is part of Gajim.
|
|
||||||
##
|
|
||||||
## Gajim is free software; you can redistribute it and/or modify
|
|
||||||
## it under the terms of the GNU General Public License as published
|
|
||||||
## by the Free Software Foundation; version 3 only.
|
|
||||||
##
|
|
||||||
## Gajim is distributed in the hope that it will be useful,
|
|
||||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
## GNU General Public License for more details.
|
|
||||||
##
|
|
||||||
## You should have received a copy of the GNU General Public License
|
|
||||||
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
##
|
|
||||||
|
|
||||||
import gtk
|
|
||||||
import gtkgui_helpers
|
|
||||||
try:
|
|
||||||
import goocanvas
|
|
||||||
HAS_GOOCANVAS = True
|
|
||||||
except:
|
|
||||||
HAS_GOOCANVAS = False
|
|
||||||
from common.xmpp import Node
|
|
||||||
from common import gajim
|
|
||||||
from dialogs import FileChooserDialog
|
|
||||||
|
|
||||||
'''
|
|
||||||
A whiteboard widget made for Gajim.
|
|
||||||
- Ummu
|
|
||||||
'''
|
|
||||||
|
|
||||||
class Whiteboard(object):
|
|
||||||
def __init__(self, account, contact, session, plugin):
|
|
||||||
self.plugin = plugin
|
|
||||||
file_path = plugin.local_file_path('whiteboard_widget.ui')
|
|
||||||
xml = gtk.Builder()
|
|
||||||
xml.set_translation_domain('gajim_plugins')
|
|
||||||
xml.add_from_file(file_path)
|
|
||||||
self.hbox = xml.get_object('whiteboard_hbox')
|
|
||||||
self.canevas = goocanvas.Canvas()
|
|
||||||
self.hbox.pack_start(self.canevas)
|
|
||||||
self.hbox.reorder_child(self.canevas, 0)
|
|
||||||
self.canevas.set_flags(gtk.CAN_FOCUS)
|
|
||||||
self.fg_color_select_button = xml.get_object('fg_color_button')
|
|
||||||
self.root = self.canevas.get_root_item()
|
|
||||||
self.tool_buttons = []
|
|
||||||
for tool in ('brush', 'oval', 'line', 'delete'):
|
|
||||||
self.tool_buttons.append(xml.get_object(tool + '_button'))
|
|
||||||
xml.get_object('brush_button').set_active(True)
|
|
||||||
|
|
||||||
# Events
|
|
||||||
self.canevas.connect('button-press-event', self.button_press_event)
|
|
||||||
self.canevas.connect('button-release-event', self.button_release_event)
|
|
||||||
self.canevas.connect('motion-notify-event', self.motion_notify_event)
|
|
||||||
self.canevas.connect('item-created', self.item_created)
|
|
||||||
|
|
||||||
# Config
|
|
||||||
self.line_width = 2
|
|
||||||
xml.get_object('size_scale').set_value(2)
|
|
||||||
self.color = str(self.fg_color_select_button.get_color())
|
|
||||||
|
|
||||||
# SVG Storage
|
|
||||||
self.image = SVGObject(self.root, session)
|
|
||||||
|
|
||||||
xml.connect_signals(self)
|
|
||||||
|
|
||||||
# Temporary Variables for items
|
|
||||||
self.item_temp = None
|
|
||||||
self.item_temp_coords = (0, 0)
|
|
||||||
self.item_data = None
|
|
||||||
|
|
||||||
# Will be {ID: {type:'element', data:[node, goocanvas]}, ID2: {}} instance
|
|
||||||
self.recieving = {}
|
|
||||||
|
|
||||||
def on_tool_button_toggled(self, widget):
|
|
||||||
for btn in self.tool_buttons:
|
|
||||||
if btn == widget:
|
|
||||||
continue
|
|
||||||
btn.set_active(False)
|
|
||||||
|
|
||||||
def on_brush_button_toggled(self, widget):
|
|
||||||
if widget.get_active():
|
|
||||||
self.image.draw_tool = 'brush'
|
|
||||||
self.on_tool_button_toggled(widget)
|
|
||||||
|
|
||||||
def on_oval_button_toggled(self, widget):
|
|
||||||
if widget.get_active():
|
|
||||||
self.image.draw_tool = 'oval'
|
|
||||||
self.on_tool_button_toggled(widget)
|
|
||||||
|
|
||||||
def on_line_button_toggled(self, widget):
|
|
||||||
if widget.get_active():
|
|
||||||
self.image.draw_tool = 'line'
|
|
||||||
self.on_tool_button_toggled(widget)
|
|
||||||
|
|
||||||
def on_delete_button_toggled(self, widget):
|
|
||||||
if widget.get_active():
|
|
||||||
self.image.draw_tool = 'delete'
|
|
||||||
self.on_tool_button_toggled(widget)
|
|
||||||
|
|
||||||
def on_clear_button_clicked(self, widget):
|
|
||||||
self.image.clear_canvas()
|
|
||||||
|
|
||||||
def on_export_button_clicked(self, widget):
|
|
||||||
SvgChooserDialog(self.image.export_svg)
|
|
||||||
|
|
||||||
def on_fg_color_button_color_set(self, widget):
|
|
||||||
self.color = str(self.fg_color_select_button.get_color())
|
|
||||||
|
|
||||||
def item_created(self, canvas, item, model):
|
|
||||||
print 'item created'
|
|
||||||
item.connect('button-press-event', self.item_button_press_events)
|
|
||||||
|
|
||||||
def item_button_press_events(self, item, target_item, event):
|
|
||||||
if self.image.draw_tool == 'delete':
|
|
||||||
self.image.del_item(item)
|
|
||||||
|
|
||||||
def on_size_scale_format_value(self, widget):
|
|
||||||
self.line_width = int(widget.get_value())
|
|
||||||
|
|
||||||
def button_press_event(self, widget, event):
|
|
||||||
x = event.x
|
|
||||||
y = event.y
|
|
||||||
state = event.state
|
|
||||||
self.item_temp_coords = (x, y)
|
|
||||||
|
|
||||||
if self.image.draw_tool == 'brush':
|
|
||||||
self.item_temp = goocanvas.Ellipse(parent=self.root,
|
|
||||||
center_x=x,
|
|
||||||
center_y=y,
|
|
||||||
radius_x=1,
|
|
||||||
radius_y=1,
|
|
||||||
stroke_color=self.color,
|
|
||||||
fill_color=self.color,
|
|
||||||
line_width=self.line_width)
|
|
||||||
self.item_data = 'M %s,%s L ' % (x, y)
|
|
||||||
|
|
||||||
elif self.image.draw_tool == 'oval':
|
|
||||||
self.item_data = True
|
|
||||||
|
|
||||||
if self.image.draw_tool == 'line':
|
|
||||||
self.item_data = 'M %s,%s L' % (x, y)
|
|
||||||
|
|
||||||
def motion_notify_event(self, widget, event):
|
|
||||||
x = event.x
|
|
||||||
y = event.y
|
|
||||||
state = event.state
|
|
||||||
if self.item_temp is not None:
|
|
||||||
self.item_temp.remove()
|
|
||||||
|
|
||||||
if self.item_data is not None:
|
|
||||||
if self.image.draw_tool == 'brush':
|
|
||||||
self.item_data = self.item_data + '%s,%s ' % (x, y)
|
|
||||||
self.item_temp = goocanvas.Path(parent=self.root,
|
|
||||||
data=self.item_data, line_width=self.line_width,
|
|
||||||
stroke_color=self.color)
|
|
||||||
elif self.image.draw_tool == 'oval':
|
|
||||||
self.item_temp = goocanvas.Ellipse(parent=self.root,
|
|
||||||
center_x=self.item_temp_coords[0] + (x - self.item_temp_coords[0]) / 2,
|
|
||||||
center_y=self.item_temp_coords[1] + (y - self.item_temp_coords[1]) / 2,
|
|
||||||
radius_x=abs(x - self.item_temp_coords[0]) / 2,
|
|
||||||
radius_y=abs(y - self.item_temp_coords[1]) / 2,
|
|
||||||
stroke_color=self.color,
|
|
||||||
line_width=self.line_width)
|
|
||||||
elif self.image.draw_tool == 'line':
|
|
||||||
self.item_data = 'M %s,%s L' % self.item_temp_coords
|
|
||||||
self.item_data = self.item_data + ' %s,%s' % (x, y)
|
|
||||||
self.item_temp = goocanvas.Path(parent=self.root,
|
|
||||||
data=self.item_data, line_width=self.line_width,
|
|
||||||
stroke_color=self.color)
|
|
||||||
|
|
||||||
def button_release_event(self, widget, event):
|
|
||||||
x = event.x
|
|
||||||
y = event.y
|
|
||||||
state = event.state
|
|
||||||
|
|
||||||
if self.image.draw_tool == 'brush':
|
|
||||||
self.item_data = self.item_data + '%s,%s' % (x, y)
|
|
||||||
if x == self.item_temp_coords[0] and y == self.item_temp_coords[1]:
|
|
||||||
goocanvas.Ellipse(parent=self.root,
|
|
||||||
center_x=x,
|
|
||||||
center_y=y,
|
|
||||||
radius_x=1,
|
|
||||||
radius_y=1,
|
|
||||||
stroke_color=self.color,
|
|
||||||
fill_color=self.color,
|
|
||||||
line_width=self.line_width)
|
|
||||||
self.image.add_path(self.item_data, self.line_width, self.color)
|
|
||||||
|
|
||||||
if self.image.draw_tool == 'oval':
|
|
||||||
cx = self.item_temp_coords[0] + (x - self.item_temp_coords[0]) / 2
|
|
||||||
cy = self.item_temp_coords[1] + (y - self.item_temp_coords[1]) / 2
|
|
||||||
rx = abs(x - self.item_temp_coords[0]) / 2
|
|
||||||
ry = abs(y - self.item_temp_coords[1]) / 2
|
|
||||||
self.image.add_ellipse(cx, cy, rx, ry, self.line_width, self.color)
|
|
||||||
|
|
||||||
if self.image.draw_tool == 'line':
|
|
||||||
self.item_data = 'M %s,%s L' % self.item_temp_coords
|
|
||||||
self.item_data = self.item_data + ' %s,%s' % (x, y)
|
|
||||||
if x == self.item_temp_coords[0] and y == self.item_temp_coords[1]:
|
|
||||||
goocanvas.Ellipse(parent=self.root,
|
|
||||||
center_x=x,
|
|
||||||
center_y=y,
|
|
||||||
radius_x=1,
|
|
||||||
radius_y=1,
|
|
||||||
stroke_color='black',
|
|
||||||
fill_color='black',
|
|
||||||
line_width=self.line_width)
|
|
||||||
self.image.add_path(self.item_data, self.line_width, self.color)
|
|
||||||
|
|
||||||
if self.image.draw_tool == 'delete':
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.item_data = None
|
|
||||||
if self.item_temp is not None:
|
|
||||||
self.item_temp.remove()
|
|
||||||
self.item_temp = None
|
|
||||||
|
|
||||||
def recieve_element(self, element):
|
|
||||||
node = self.image.g.addChild(name=element.getAttr('name'))
|
|
||||||
self.image.g.addChild(node=node)
|
|
||||||
self.recieving[element.getAttr('rid')] = {'type':'element',
|
|
||||||
'data':[node],
|
|
||||||
'children':[]}
|
|
||||||
|
|
||||||
def recieve_attr(self, element):
|
|
||||||
node = self.recieving[element.getAttr('parent')]['data'][0]
|
|
||||||
node.setAttr(element.getAttr('name'), element.getAttr('chdata'))
|
|
||||||
|
|
||||||
self.recieving[element.getAttr('rid')] = {'type':'attr',
|
|
||||||
'data':element.getAttr('name'),
|
|
||||||
'parent':node}
|
|
||||||
self.recieving[element.getAttr('parent')]['children'].append(element.getAttr('rid'))
|
|
||||||
|
|
||||||
def apply_new(self):
|
|
||||||
for x in self.recieving.keys():
|
|
||||||
if self.recieving[x]['type'] == 'element':
|
|
||||||
self.image.add_recieved(x, self.recieving)
|
|
||||||
|
|
||||||
self.recieving = {}
|
|
||||||
|
|
||||||
class SvgChooserDialog(FileChooserDialog):
|
|
||||||
def __init__(self, on_response_ok=None, on_response_cancel=None):
|
|
||||||
'''
|
|
||||||
Choose in which SVG file to store the image
|
|
||||||
'''
|
|
||||||
def on_ok(widget, callback):
|
|
||||||
'''
|
|
||||||
check if file exists and call callback
|
|
||||||
'''
|
|
||||||
path_to_file = self.get_filename()
|
|
||||||
path_to_file = gtkgui_helpers.decode_filechooser_file_paths(
|
|
||||||
(path_to_file,))[0]
|
|
||||||
widget.destroy()
|
|
||||||
callback(path_to_file)
|
|
||||||
|
|
||||||
FileChooserDialog.__init__(self,
|
|
||||||
title_text=_('Save Image as...'),
|
|
||||||
action=gtk.FILE_CHOOSER_ACTION_SAVE,
|
|
||||||
buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE,
|
|
||||||
gtk.RESPONSE_OK),
|
|
||||||
current_folder='',
|
|
||||||
default_response=gtk.RESPONSE_OK,
|
|
||||||
on_response_ok=(on_ok, on_response_ok),
|
|
||||||
on_response_cancel=on_response_cancel)
|
|
||||||
|
|
||||||
filter_ = gtk.FileFilter()
|
|
||||||
filter_.set_name(_('All files'))
|
|
||||||
filter_.add_pattern('*')
|
|
||||||
self.add_filter(filter_)
|
|
||||||
|
|
||||||
filter_ = gtk.FileFilter()
|
|
||||||
filter_.set_name(_('SVG Files'))
|
|
||||||
filter_.add_pattern('*.svg')
|
|
||||||
self.add_filter(filter_)
|
|
||||||
self.set_filter(filter_)
|
|
||||||
|
|
||||||
|
|
||||||
class SVGObject():
|
|
||||||
''' A class to store the svg document and make changes to it.'''
|
|
||||||
|
|
||||||
def __init__(self, root, session, height=300, width=300):
|
|
||||||
# Will be {ID: {type:'element', data:[node, goocanvas]}, ID2: {}} instance
|
|
||||||
self.items = {}
|
|
||||||
self.root = root
|
|
||||||
self.draw_tool = 'brush'
|
|
||||||
|
|
||||||
# sxe session
|
|
||||||
self.session = session
|
|
||||||
|
|
||||||
# initialize svg document
|
|
||||||
self.svg = Node(node='<svg/>')
|
|
||||||
self.svg.setAttr('version', '1.1')
|
|
||||||
self.svg.setAttr('height', str(height))
|
|
||||||
self.svg.setAttr('width', str(width))
|
|
||||||
self.svg.setAttr('xmlns', 'http://www.w3.org/2000/svg')
|
|
||||||
# TODO: make this settable
|
|
||||||
self.g = self.svg.addChild(name='g')
|
|
||||||
self.g.setAttr('fill', 'none')
|
|
||||||
self.g.setAttr('stroke-linecap', 'round')
|
|
||||||
|
|
||||||
def add_path(self, data, line_width, color):
|
|
||||||
''' adds the path to the items listing, both minidom node and goocanvas
|
|
||||||
object in a tuple '''
|
|
||||||
|
|
||||||
goocanvas_obj = goocanvas.Path(parent=self.root, data=data,
|
|
||||||
line_width=line_width, stroke_color=color)
|
|
||||||
goocanvas_obj.connect('button-press-event', self.item_button_press_events)
|
|
||||||
|
|
||||||
node = self.g.addChild(name='path')
|
|
||||||
node.setAttr('d', data)
|
|
||||||
node.setAttr('stroke-width', str(line_width))
|
|
||||||
node.setAttr('stroke', color)
|
|
||||||
self.g.addChild(node=node)
|
|
||||||
|
|
||||||
rids = self.session.generate_rids(4)
|
|
||||||
self.items[rids[0]] = {'type':'element', 'data':[node, goocanvas_obj], 'children':rids[1:]}
|
|
||||||
self.items[rids[1]] = {'type':'attr', 'data':'d', 'parent':node}
|
|
||||||
self.items[rids[2]] = {'type':'attr', 'data':'stroke-width', 'parent':node}
|
|
||||||
self.items[rids[3]] = {'type':'attr', 'data':'stroke', 'parent':node}
|
|
||||||
|
|
||||||
self.session.send_items(self.items, rids)
|
|
||||||
|
|
||||||
def add_recieved(self, parent_rid, new_items):
|
|
||||||
''' adds the path to the items listing, both minidom node and goocanvas
|
|
||||||
object in a tuple '''
|
|
||||||
node = new_items[parent_rid]['data'][0]
|
|
||||||
|
|
||||||
self.items[parent_rid] = new_items[parent_rid]
|
|
||||||
for x in new_items[parent_rid]['children']:
|
|
||||||
self.items[x] = new_items[x]
|
|
||||||
|
|
||||||
if node.getName() == 'path':
|
|
||||||
goocanvas_obj = goocanvas.Path(parent=self.root,
|
|
||||||
data=node.getAttr('d'),
|
|
||||||
line_width=int(node.getAttr('stroke-width')),
|
|
||||||
stroke_color=node.getAttr('stroke'))
|
|
||||||
|
|
||||||
if node.getName() == 'ellipse':
|
|
||||||
goocanvas_obj = goocanvas.Ellipse(parent=self.root,
|
|
||||||
center_x=float(node.getAttr('cx')),
|
|
||||||
center_y=float(node.getAttr('cy')),
|
|
||||||
radius_x=float(node.getAttr('rx')),
|
|
||||||
radius_y=float(node.getAttr('ry')),
|
|
||||||
stroke_color=node.getAttr('stroke'),
|
|
||||||
line_width=float(node.getAttr('stroke-width')))
|
|
||||||
|
|
||||||
self.items[parent_rid]['data'].append(goocanvas_obj)
|
|
||||||
goocanvas_obj.connect('button-press-event', self.item_button_press_events)
|
|
||||||
|
|
||||||
def add_ellipse(self, cx, cy, rx, ry, line_width, stroke_color):
|
|
||||||
''' adds the ellipse to the items listing, both minidom node and goocanvas
|
|
||||||
object in a tuple '''
|
|
||||||
|
|
||||||
goocanvas_obj = goocanvas.Ellipse(parent=self.root,
|
|
||||||
center_x=cx,
|
|
||||||
center_y=cy,
|
|
||||||
radius_x=rx,
|
|
||||||
radius_y=ry,
|
|
||||||
stroke_color=stroke_color,
|
|
||||||
line_width=line_width)
|
|
||||||
goocanvas_obj.connect('button-press-event', self.item_button_press_events)
|
|
||||||
|
|
||||||
node = self.g.addChild(name='ellipse')
|
|
||||||
node.setAttr('cx', str(cx))
|
|
||||||
node.setAttr('cy', str(cy))
|
|
||||||
node.setAttr('rx', str(rx))
|
|
||||||
node.setAttr('ry', str(ry))
|
|
||||||
node.setAttr('stroke-width', str(line_width))
|
|
||||||
node.setAttr('stroke', stroke_color)
|
|
||||||
self.g.addChild(node=node)
|
|
||||||
|
|
||||||
rids = self.session.generate_rids(7)
|
|
||||||
self.items[rids[0]] = {'type':'element', 'data':[node, goocanvas_obj], 'children':rids[1:]}
|
|
||||||
self.items[rids[1]] = {'type':'attr', 'data':'cx', 'parent':node}
|
|
||||||
self.items[rids[2]] = {'type':'attr', 'data':'cy', 'parent':node}
|
|
||||||
self.items[rids[3]] = {'type':'attr', 'data':'rx', 'parent':node}
|
|
||||||
self.items[rids[4]] = {'type':'attr', 'data':'ry', 'parent':node}
|
|
||||||
self.items[rids[5]] = {'type':'attr', 'data':'stroke-width', 'parent':node}
|
|
||||||
self.items[rids[6]] = {'type':'attr', 'data':'stroke', 'parent':node}
|
|
||||||
|
|
||||||
self.session.send_items(self.items, rids)
|
|
||||||
|
|
||||||
def del_item(self, item):
|
|
||||||
rids = []
|
|
||||||
for x in self.items.keys():
|
|
||||||
if self.items[x]['type'] == 'element':
|
|
||||||
if self.items[x]['data'][1] == item:
|
|
||||||
for y in self.items[x]['children']:
|
|
||||||
rids.append(y)
|
|
||||||
self.del_rid(y)
|
|
||||||
rids.append(x)
|
|
||||||
self.del_rid(x)
|
|
||||||
break
|
|
||||||
self.session.del_item(rids)
|
|
||||||
|
|
||||||
def clear_canvas(self):
|
|
||||||
for x in self.items.keys():
|
|
||||||
if self.items[x]['type'] == 'element':
|
|
||||||
self.del_rid(x)
|
|
||||||
|
|
||||||
def del_rid(self, rid):
|
|
||||||
if self.items[rid]['type'] == 'element':
|
|
||||||
self.items[rid]['data'][1].remove()
|
|
||||||
del self.items[rid]
|
|
||||||
|
|
||||||
def export_svg(self, filename):
|
|
||||||
f = open(filename, 'w')
|
|
||||||
f.writelines(str(self.svg))
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
def item_button_press_events(self, item, target_item, event):
|
|
||||||
self.del_item(item)
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<interface>
|
|
||||||
<requires lib="gtk+" version="2.16"/>
|
|
||||||
<!-- interface-naming-policy project-wide -->
|
|
||||||
<object class="GtkHBox" id="whiteboard_hbox">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">3</property>
|
|
||||||
<property name="spacing">6</property>
|
|
||||||
<child>
|
|
||||||
<placeholder/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkVBox" id="vbuttonbox1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">6</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<property name="spacing">6</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkToggleButton" id="brush_button">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">True</property>
|
|
||||||
<property name="tooltip_text" translatable="yes">Brush Tool: Draw freehand lines</property>
|
|
||||||
<signal name="toggled" handler="on_brush_button_toggled"/>
|
|
||||||
<child>
|
|
||||||
<object class="GtkImage" id="image5">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="stock">brush_tool</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkToggleButton" id="oval_button">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">True</property>
|
|
||||||
<property name="tooltip_text" translatable="yes">Oval Tool: Draw circles and ellipses</property>
|
|
||||||
<signal name="toggled" handler="on_oval_button_toggled"/>
|
|
||||||
<child>
|
|
||||||
<object class="GtkImage" id="image6">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="stock">oval_tool</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkToggleButton" id="line_button">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">True</property>
|
|
||||||
<property name="tooltip_text" translatable="yes">Line Tool: Draw straight lines</property>
|
|
||||||
<signal name="toggled" handler="on_line_button_toggled"/>
|
|
||||||
<child>
|
|
||||||
<object class="GtkImage" id="image7">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="stock">line_tool</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkToggleButton" id="delete_button">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">True</property>
|
|
||||||
<property name="tooltip_text" translatable="yes">Delete Tool: Remove individual figures</property>
|
|
||||||
<signal name="toggled" handler="on_delete_button_toggled"/>
|
|
||||||
<child>
|
|
||||||
<object class="GtkImage" id="image2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="stock">gtk-delete</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">3</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="clear_button">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">True</property>
|
|
||||||
<property name="tooltip_text" translatable="yes">Clear Canvas: Cleanup canvas</property>
|
|
||||||
<signal name="clicked" handler="on_clear_button_clicked"/>
|
|
||||||
<child>
|
|
||||||
<object class="GtkImage" id="image3">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="stock">gtk-clear</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">4</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="export_button">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">True</property>
|
|
||||||
<property name="tooltip_text" translatable="yes">Export Image: Save image to svg file</property>
|
|
||||||
<signal name="clicked" handler="on_export_button_clicked"/>
|
|
||||||
<child>
|
|
||||||
<object class="GtkImage" id="image4">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="stock">gtk-save-as</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">5</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkVScale" id="size_scale">
|
|
||||||
<property name="height_request">68</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="tooltip_text" translatable="yes">Line width</property>
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<property name="adjustment">adjustment1</property>
|
|
||||||
<property name="inverted">True</property>
|
|
||||||
<property name="digits">0</property>
|
|
||||||
<property name="value_pos">bottom</property>
|
|
||||||
<signal name="value_changed" handler="on_size_scale_format_value"/>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">6</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkColorButton" id="fg_color_button">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="receives_default">True</property>
|
|
||||||
<property name="tooltip_text" translatable="yes">Foreground color</property>
|
|
||||||
<property name="color">#000000000000</property>
|
|
||||||
<signal name="color_set" handler="on_fg_color_button_color_set"/>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">7</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
<object class="GtkImage" id="image1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="stock">gtk-delete</property>
|
|
||||||
</object>
|
|
||||||
<object class="GtkAdjustment" id="adjustment1">
|
|
||||||
<property name="value">2</property>
|
|
||||||
<property name="lower">1</property>
|
|
||||||
<property name="upper">110</property>
|
|
||||||
<property name="step_increment">1</property>
|
|
||||||
<property name="page_increment">10</property>
|
|
||||||
<property name="page_size">10</property>
|
|
||||||
</object>
|
|
||||||
</interface>
|
|
||||||