...
 
Commits (184)
Gajim 0.99.1 (08 February 2018)
Gajim 1.0.3 (20 May 2018)
Bugs fixed:
* 8296 Fix errors on roster updates after stream management resume
* 9106 Convert font weight from pango to css values
* 9124 Bring ChatControl to front when notification is clicked
* Set no-store hint on groupchat chatstates
* Dont show OOB uri if message body is the same
* Add missing bybonjour dependency for Windows zeroconf
Flatpak:
* Limit dbus access
Gajim 1.0.2 (30 April 2018)
Bugs fixed:
* 7879 Server name is rejected for group chat bookmarks
* 8964 setup.py install misses some files if used with "--skip-build"
* 9017 Password was sometimes stored in plaintext
* 9022 Dont show error when receiving invalid avatars
* 9031 Windows: Always hide roster window on X
* 9038 No License in About dialog
* 9039 Encode filenames before sending
* 9044 Catch invalid IQ stanzas and log them
* 9049 XMPP logo in "Add New Contact" window instead Gajim logo
* 9050 Mark some strings as translatable
* 9054 Error on file send completion
* 9055 Removing a bookmark causes error
* 9057 Avatar is deleted when updating vCard
* 9065 Account label isn't change in tooltip of notification area icon
* 9066 Placeholder text does't disappear
* 9068 Missing pulseaudio in Flatpak image
* 9070 Fix History Manager search
* 9074 Proxy comobobox in accounts/connections doesn't get update after ManageProxies
* 9094 problem receiving file
* 9101 Notification never autohides in gnome
* Correctly reload Plugins
* Save history export with utf8 encoding
* Dont allow plain BOSH by default
Gajim 1.0.1 (1 April 2018)
* Improve MAM support
* Image preview in file chooser dialog
* Groupchat: Set minimize on auto join default True
* Delete bookmark when we destroy a room
* Fix account deletion
* Fix custom font handling
* Fix OpenPGP message decryption
* Fix window position restore on multi-head setups
* Fix scrolling in message window
* Improve Windows build and build for 64 bits
Gajim 1.0.0 (17 March 2018)
* Ported to GTK3 / Python3
* Integrate HTTPUpload
* Add Navigation buttons in History Window
* Added XEP-0368 (SRV records for XMPP over TLS)
* Improvements for HiDPI Screens
* Move Chat Menu button so we are not forced to use CSD
* Depend on the python keyring package for password storage
* Bug fixes
Gajim 0.98.2 (17 December 2017)
* Fix DB migration
Gajim 0.98.1 (15 December 2017)
* Ported to GTK3 / Python3
* Flatpak support
* Lots of refactoring
* New Emoji support
......@@ -27,14 +75,23 @@ Gajim 0.98.1 (15 December 2017)
* Added mam:1 and mam:2 support (mam:0 was removed)
* Added MAM for MUCs support
* Added support for showing XEP-0084 Avatars
* Add support for geo: URIs
* Added xmpp URI handling directly in Gajim
* Removed Gajim-Remote
* Removed XEP-0012 (Last Activity)
* Removed XEP-0136 (Message Archiving)
* Added XEP-0156 (Discovering Alternative XMPP Connection Methods)
* Added XEP-0319 (Last User Interaction in Presence)
* Added XEP-0368 (SRV records for XMPP over TLS)
* Added XEP-0380 (Explicit Message Encryption)
* Added Jingle FT:5 support
* Lots of other small bugfixes
KNOWN ISSUES:
- Meta Contacts: Filtering the roster could lead to a crash in some circumstances. Use CTRL + N for starting new chats as a workaround
- Audio/Video support is currently not maintained and most likely not working
- Windows: Translation is not working currently
Gajim 0.16.9 (30 November 2017)
......
......@@ -3,10 +3,10 @@
### Runtime Requirements
- python3.4 or higher
- python3.5 or higher
- python3-gi
- python3-gi-cairo
- gir1.2-gtk-3.0
- gir1.2-gtk-3.0 (>=3.22)
- python3-nbxmpp
- python3-openssl (>=0.14)
- python3-pyasn1
......@@ -14,6 +14,7 @@
### Optional Runtime Requirements
- python3-keyring for saving your password to your system keyring
- python3-pil (pillow) for support of webp avatars
- python3-crypto to enable End to end encryption
- python3-gnupg to enable GPG encryption
......
environment:
matrix:
- MSYS: C:/msys64/mingw32
- MSYSTEM: MINGW64
MSYS_ARCH: "x86_64"
ARCH: "64bit"
- MSYSTEM: MINGW32
MSYS_ARCH: "i686"
ARCH: "32bit"
branches:
only:
- master
- gajim_1.0
clone_depth: 1
# init:
# - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
install:
build_script:
- C:\msys64\usr\bin\pacman -Syu --needed --noconfirm --ask=20
- ps: |
$env:MSYSTEM="MINGW32"
$env:TIME_STRING=(get-date -UFormat "%Y-%m-%d").ToString()
$env:BUILDROOT="C:\msys64\home\appveyor\gajim\win\_build_root"
......@@ -23,13 +28,10 @@ install:
C:\msys64\usr\bin\sh.exe --login -c $command
}
bash 'pacman -Sy --noconfirm git'
bash 'git clone C:/projects/gajim C:/msys64/home/appveyor/gajim'
bash 'C:/msys64/home/appveyor/gajim/win/build.sh'
Push-AppveyorArtifact "$($env:BUILDROOT)/Gajim.exe" -FileName "Gajim-Master-$($env:TIME_STRING).exe"
Push-AppveyorArtifact "$($env:BUILDROOT)/Gajim-Portable.exe" -FileName "Gajim-Portable-Master-$($env:TIME_STRING).exe"
build: off
bash "git clone C:/projects/gajim C:/msys64/home/appveyor/gajim"
bash "C:/msys64/home/appveyor/gajim/win/build.sh $($env:MSYS_ARCH)"
Push-AppveyorArtifact "$($env:BUILDROOT)/Gajim.exe" -FileName "Gajim-1.0.3-$($env:ARCH)-$($env:TIME_STRING).exe"
Push-AppveyorArtifact "$($env:BUILDROOT)/Gajim-Portable.exe" -FileName "Gajim-Portable-1.0.3-$($env:ARCH)-$($env:TIME_STRING).exe"
# on_finish:
# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
......
Run the following steps from a directory containing the gajim source dir.
Install gajim flatpak repo
--------------------------
1. `flatpak --user remote-add --from gnome https://sdk.gnome.org/gnome.flatpakrepo`
1. `flatpak --user install gnome org.gnome.Platform//3.24`
1. `flatpak --user install gnome org.gnome.Sdk//3.24`
1. `flatpak-builder --repo=repo directory gajim/org.gajim.Gajim.json`
1. `flatpak --user remote-add --no-gpg-verify repo repo`
1. `flatpak --user install repo org.gajim.Gajim`
1. `flatpak run org.gajim.Gajim`
Update gajim flatpak repo
-------------------------
1. update your gajim source repository
1. `rm -r directory`
1. `flatpak-builder --repo=repo directory gajim/org.gajim.Gajim.json`
1. `flatpak --user update`
Note: remove `--user` if you want a system-wide installation
# Install Gajim via Flatpak
**Prerequisites:**
You need to have `flatpak` and `flatpak-builder` installed. For this example, we use `git` for downloading/updating Gajim's sources.
### Download Gajim's sources
In this example, we do a `git clone` of the repository, so you need to have `git` installed. Alternatively, you can also download the sources from our Gitlab via webbrowser.
`git clone https://dev.gajim.org/gajim/gajim.git ~/Gajim`
`cd ~/Gajim`
*Note: Source tarballs and snapshots do _not_ include 'org.gajim.Gajim.json', which is necessary for installation via Flatpak.*
### Install Gajim and dependencies
Replace install path `~/Gajim/gajim_flatpak` with an install path of your choice.
*Note: Remove `--user` if you want a system-wide installation.*
1. `flatpak --user remote-add --from gnome https://sdk.gnome.org/gnome.flatpakrepo`
2. `flatpak --user install gnome org.gnome.Platform//3.28`
3. `flatpak --user install gnome org.gnome.Sdk//3.28`
4. `flatpak-builder --repo=gajim_flatpak_repo ~/Gajim/gajim_flatpak ~/Gajim/org.gajim.Gajim.json`
5. `flatpak --user remote-add --no-gpg-verify gajim_flatpak_repo gajim_flatpak_repo`
6. `flatpak --user install gajim_flatpak_repo org.gajim.Gajim`
7. `flatpak run org.gajim.Gajim`
Thats it, you are now running Gajim via Flatpak!
## How to update
### Update Gajim's sources
In this example, we use `git` to update the repository. You can also download the sources from our Gitlab via webbrowser.
`cd ~/Gajim`
`git pull --rebase`
### Remove previous Flatpak directory
`rm -r ~/Gajim/gajim_flatpak`
### Install and update Gajim
1. `flatpak-builder --repo=gajim_flatpak_repo ~/Gajim/gajim_flatpak ~/Gajim/org.gajim.Gajim.json`
2. `flatpak --user update`
3. `flatpak run org.gajim.Gajim`
Gajim is now updated.
import subprocess
__version__ = "0.99.1"
__version__ = "1.0.3"
try:
node = subprocess.Popen('git rev-parse --short=12 HEAD', shell=True,
stdout=subprocess.PIPE).communicate()[0]
p = subprocess.Popen('git rev-parse --short=12 HEAD', shell=True,
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
node = p.communicate()[0]
if node:
__version__ += '+' + node.decode('utf-8').strip()
except Exception:
pass
......@@ -113,10 +113,10 @@ class AccountsWindow(Gtk.ApplicationWindow):
self.stack.set_visible_child(page)
def update_proxy_list(self):
page = self.stack.get_child_by_name('connetion')
page = self.stack.get_child_by_name('connection')
if page is None:
return
page.options['proxy'].update_values()
page.listbox.get_option('proxy').update_values()
def check_relogin(self):
for account in self.need_relogin:
......@@ -177,7 +177,7 @@ class AccountsWindow(Gtk.ApplicationWindow):
def on_remove_account(self, button, account):
if app.events.get_events(account):
app.interface.raise_dialog('unread-events-on-remove')
app.interface.raise_dialog('unread-events-on-remove-account')
return
if app.config.get_per('accounts', account, 'is_zeroconf'):
......@@ -211,7 +211,8 @@ class AccountsWindow(Gtk.ApplicationWindow):
_('You have opened chat in account %s') % account,
_('All chat and groupchat windows will be closed. '
'Do you want to continue?'),
on_response_ok=(remove, account))
on_response_ok=(remove, account),
transient_for=self)
else:
remove(account)
......@@ -233,6 +234,7 @@ class AccountsWindow(Gtk.ApplicationWindow):
def select_account(self, account):
for row in self.account_list.get_children():
if row.get_child().account == account:
self.account_list.select_row(row)
self.account_list.emit('row-activated', row)
break
......@@ -685,7 +687,7 @@ class LoginDialog(OptionsDialog):
self.connect('destroy', self.on_destroy)
def on_password_change(self, new_password, data):
self.get_option('password').entry.set_text(new_password)
passwords.save_password(self.account, new_password)
def on_destroy(self, *args):
savepass = app.config.get_per('accounts', self.account, 'savepass')
......
......@@ -286,3 +286,13 @@ class AppActions():
win.present()
else:
app.interface.create_ipython_window()
def show_next_pending_event(self, action, param):
"""
Show the window(s) with next pending event in tabbed/group chats
"""
if app.events.get_nb_events():
account, jid, event = app.events.get_first_systray_event()
if not event:
return
app.interface.handle_event(account, jid, event.type_)
......@@ -238,8 +238,11 @@ class ChatControl(ChatControlBase):
self._nec_chatstate_received)
app.ged.register_event_handler('caps-received', ged.GUI1,
self._nec_caps_received)
app.ged.register_event_handler('stanza-message-outgoing', ged.OUT_POSTCORE,
app.ged.register_event_handler('message-sent', ged.OUT_POSTCORE,
self._message_sent)
app.ged.register_event_handler(
'mam-decrypted-message-received',
ged.GUI1, self._nec_mam_decrypted_message_received)
# PluginSystem: adding GUI extension point for this ChatControl
# instance object
......@@ -599,7 +602,7 @@ class ChatControl(ChatControlBase):
if 'location' in self.contact.pep:
location = self.contact.pep['location']._pep_specific_data
if ('lat' in location) and ('lon' in location):
uri = 'http://www.openstreetmap.org/?' + \
uri = 'https://www.openstreetmap.org/?' + \
'mlat=%(lat)s&mlon=%(lon)s&zoom=16' % {'lat': location['lat'],
'lon': location['lon']}
helpers.launch_browser_mailer('url', uri)
......@@ -817,6 +820,20 @@ class ChatControl(ChatControlBase):
app.plugin_manager.extension_point(
'encryption_dialog' + self.encryption, self)
def _nec_mam_decrypted_message_received(self, obj):
if obj.conn.name != self.account:
return
if obj.with_ != self.contact.jid:
return
kind = '' # incoming
if obj.kind == KindConstant.CHAT_MSG_SENT:
kind = 'outgoing'
self.print_conversation(obj.msgtxt, kind, tim=obj.timestamp,
encrypted=obj.encrypted, correct_id=obj.correct_id,
msg_stanza_id=obj.message_id, additional_data=obj.additional_data)
def _message_sent(self, obj):
if obj.conn.name != self.account:
return
......@@ -1154,8 +1171,11 @@ class ChatControl(ChatControlBase):
self._nec_chatstate_received)
app.ged.remove_event_handler('caps-received', ged.GUI1,
self._nec_caps_received)
app.ged.remove_event_handler('stanza-message-outgoing', ged.OUT_POSTCORE,
app.ged.remove_event_handler('message-sent', ged.OUT_POSTCORE,
self._message_sent)
app.ged.remove_event_handler(
'mam-decrypted-message-received',
ged.GUI1, self._nec_mam_decrypted_message_received)
self.unsubscribe_events()
......@@ -1576,7 +1596,7 @@ class ChatControl(ChatControlBase):
markup += ' (%s)' % file_props.desc
markup += '\n%s: %s' % (_('Size'), helpers.convert_bytes(
file_props.size))
b1 = Gtk.Button(_('_Accept'))
b1 = Gtk.Button(_('Accept'))
b1.connect('clicked', self._on_accept_file_request, file_props)
b2 = Gtk.Button(stock=Gtk.STOCK_CANCEL)
b2.connect('clicked', self._on_cancel_file_request, file_props)
......
......@@ -305,13 +305,15 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
'conversation_scrolledwindow')
self.conv_scrolledwindow.add(self.conv_textview.tv)
widget = self.conv_scrolledwindow.get_vadjustment()
id_ = widget.connect('value-changed',
self.on_conversation_vadjustment_value_changed)
self.handlers[id_] = widget
id_ = widget.connect('changed',
self.on_conversation_vadjustment_changed)
self.handlers[id_] = widget
self.was_at_the_end = True
vscrollbar = self.conv_scrolledwindow.get_vscrollbar()
id_ = vscrollbar.connect('button-release-event',
self._on_scrollbar_button_release)
self.handlers[id_] = vscrollbar
self.correcting = False
self.last_sent_msg = None
......@@ -600,9 +602,12 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
valid, entries = self.keymap.get_entries_for_keyval(event.keyval)
keycode = entries[0].keycode
if (event.get_state() & Gdk.ModifierType.CONTROL_MASK and keycode in (
self.keycode_c, self.keycode_ins)) or (
event.get_state() & Gdk.ModifierType.SHIFT_MASK and \
event.keyval in (Gdk.KEY_Page_Down, Gdk.KEY_Page_Up)):
self.keycode_c, self.keycode_ins)):
return False
if event.get_state() & Gdk.ModifierType.SHIFT_MASK and \
event.keyval in (Gdk.KEY_Page_Down, Gdk.KEY_Page_Up):
self._on_scroll(None, event.keyval)
return False
self.parent_win.notebook.event(event)
return True
......@@ -965,7 +970,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
full_jid = self.get_full_jid()
textview = self.conv_textview
end = False
if self.was_at_the_end or kind == 'outgoing':
if self.conv_textview.autoscroll or kind == 'outgoing':
end = True
if other_tags_for_name is None:
......@@ -1201,7 +1206,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
if state:
self.set_emoticon_popover()
jid = self.contact.jid
if self.was_at_the_end:
if self.conv_textview.autoscroll:
# we are at the end
type_ = ['printed_' + self.type_id]
if self.type_id == message_control.TYPE_GC:
......@@ -1221,21 +1226,15 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
else:
self.send_chatstate('inactive', self.contact)
def scroll_to_end_iter(self):
self.conv_textview.scroll_to_end_iter()
return False
def scroll_to_end(self, force=False):
self.conv_textview.scroll_to_end(force)
def on_conversation_vadjustment_changed(self, adjustment):
# used to stay at the end of the textview when we shrink conversation
# textview.
if self.was_at_the_end:
self.scroll_to_end_iter()
self.was_at_the_end = (adjustment.get_upper() - adjustment.get_value()\
- adjustment.get_page_size()) < 18
def on_conversation_vadjustment_value_changed(self, adjustment):
self.was_at_the_end = (adjustment.get_upper() - adjustment.get_value() \
- adjustment.get_page_size()) < 18
def _on_edge_reached(self, scrolledwindow, pos):
if pos != Gtk.PositionType.BOTTOM:
return
# Remove all events and set autoscroll True
app.log('autoscroll').info('Autoscroll enabled')
self.conv_textview.autoscroll = True
if self.resource:
jid = self.contact.get_full_jid()
else:
......@@ -1252,8 +1251,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
return
if not self.parent_win:
return
if self.conv_textview.at_the_end() and \
self.parent_win.get_active_control() == self and \
if self.parent_win.get_active_control() == self and \
self.parent_win.window.is_active():
# we are at the end
if self.type_id == message_control.TYPE_GC:
......@@ -1264,6 +1262,57 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
# There were events to remove
self.redraw_after_event_removed(jid)
def _on_scrollbar_button_release(self, scrollbar, event):
if event.get_button()[1] != 1:
# We want only to catch the left mouse button
return
if not gtkgui_helpers.at_the_end(scrollbar.get_parent()):
app.log('autoscroll').info('Autoscroll disabled')
self.conv_textview.autoscroll = False
def _on_scroll(self, widget, event):
if not self.conv_textview.autoscroll:
# autoscroll is already disabled
return
if widget is None:
# call from _conv_textview_key_press_event()
# SHIFT + Gdk.KEY_Page_Up
if event != Gdk.KEY_Page_Up:
return
else:
# On scrolliung UP disable autoscroll
# get_scroll_direction() sets has_direction only TRUE
# if smooth scrolling is deactivated. If we have smooth
# smooth scrolling we have to use get_scroll_deltas()
has_direction, direction = event.get_scroll_direction()
if not has_direction:
direction = None
smooth, delta_x, delta_y = event.get_scroll_deltas()
if smooth:
if delta_y < 0:
direction = Gdk.ScrollDirection.UP
elif delta_y > 0:
direction = Gdk.ScrollDirection.DOWN
elif delta_x < 0:
direction = Gdk.ScrollDirection.LEFT
elif delta_x > 0:
direction = Gdk.ScrollDirection.RIGHT
else:
app.log('autoscroll').warning(
'Scroll directions cant be determined')
if direction != Gdk.ScrollDirection.UP:
return
# Check if we have a Scrollbar
adjustment = self.conv_scrolledwindow.get_vadjustment()
if adjustment.get_upper() != adjustment.get_page_size():
app.log('autoscroll').info('Autoscroll disabled')
self.conv_textview.autoscroll = False
def on_conversation_vadjustment_changed(self, adjustment):
self.scroll_to_end()
def redraw_after_event_removed(self, jid):
"""
We just removed a 'printed_*' event, redraw contact in roster or
......@@ -1344,7 +1393,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
"""
# make the last message visible, when changing to "full view"
if not state:
GLib.idle_add(self.conv_textview.scroll_to_end_iter)
self.scroll_to_end()
widget.set_no_show_all(state)
if state:
......@@ -1380,13 +1429,12 @@ class ScrolledWindow(Gtk.ScrolledWindow):
def do_get_preferred_height(self):
min_height, natural_height = Gtk.ScrolledWindow.do_get_preferred_height(self)
child = self.get_child()
# Gtk Bug: If policy is set to Automatic, the ScrolledWindow
# has a min size of around 46 depending on the System. Because
# we want it smaller, we set policy NEVER if the height is < 50
# has a min size of around 46-82 depending on the System. Because
# we want it smaller, we set policy NEVER if the height is < 90
# so the ScrolledWindow will shrink to around 26 (1 line heigh).
# Once it gets over 50 its no problem to restore the policy.
if natural_height < 50:
# Once it gets over 90 its no problem to restore the policy.
if natural_height < 90:
GLib.idle_add(self.set_policy,
Gtk.PolicyType.AUTOMATIC,
Gtk.PolicyType.NEVER)
......
......@@ -142,7 +142,7 @@ class CommandProcessor(object):
def list_commands(self):
commands = list_commands(self.COMMAND_HOST)
commands = dict(commands)
return sorted(list(commands.values()), key=lambda k: k.__repr__())
return sorted(set(commands.values()), key=lambda k: k.__repr__())
class Command(object):
......
......@@ -228,7 +228,8 @@ try:
session = conference.new_session(Farstream.MediaType.AUDIO)
del session
del conference
except GLib.GError:
except Exception as e:
glog.info(e)
HAVE_FARSTREAM = False
except (ImportError, ValueError):
......
......@@ -305,6 +305,7 @@ class Config:
'use_keyring': [opt_bool, True, _('If True, Gajim will use the Systems Keyring to store account passwords.')],
'pgp_encoding': [ opt_str, '', _('Sets the encoding used by python-gnupg'), True],
'remote_commands': [opt_bool, False, _('If True, Gajim will execute XEP-0146 Commands.')],
'mam_blacklist': [opt_str, '', _('All non-compliant MAM Groupchats')],
}, {})
__options_per_key = {
......
......@@ -64,12 +64,16 @@ from gajim.common import exceptions
from gajim.common import check_X509
from gajim.common.connection_handlers import *
from gajim.common.helpers import version_condition
from gajim.common.contacts import GC_Contact
from gajim.gtkgui_helpers import get_action
if app.HAVE_PYOPENSSL:
import OpenSSL.crypto
if os.name == 'nt':
import certifi
from nbxmpp import Smacks
from string import Template
import logging
......@@ -373,6 +377,10 @@ class CommonConnection:
contact = app.contacts.get_contact_with_highest_priority(
self.name, obj.jid)
# Mark Message as MUC PM
if isinstance(contact, GC_Contact):
msg_iq.setTag('x', namespace=nbxmpp.NS_MUC_USER)
# chatstates - if peer supports xep85, send chatstates
# please note that the only valid tag inside a message containing a
# <body> tag is the active event
......@@ -687,6 +695,7 @@ class Connection(CommonConnection, ConnectionHandlers):
self.privacy_rules_requested = False
self.streamError = ''
self.secret_hmac = str(random.random())[2:].encode('utf-8')
self.removing_account = False
self.sm = Smacks(self) # Stream Management
......@@ -704,11 +713,6 @@ class Connection(CommonConnection, ConnectionHandlers):
self._nec_gc_stanza_message_outgoing)
app.ged.register_event_handler('stanza-message-outgoing',
ged.OUT_CORE, self._nec_stanza_message_outgoing)
h = app.config.get_per('accounts', self.name, 'hostname')
if h:
app.resolver.resolve('_xmppconnect.' + helpers.idn_to_ascii(h),
self._on_resolve_txt, type_='txt')
# END __init__
def cleanup(self):
......@@ -866,6 +870,8 @@ class Connection(CommonConnection, ConnectionHandlers):
def _connection_lost(self):
log.debug('_connection_lost')
self.disconnect(on_purpose = False)
if self.removing_account:
return
app.nec.push_incoming_event(ConnectionLostEvent(None, conn=self,
title=_('Connection with account "%s" has been lost') % self.name,
msg=_('Reconnect manually.')))
......@@ -892,8 +898,9 @@ class Connection(CommonConnection, ConnectionHandlers):
if self.new_account_form:
def _on_register_result(result):
if not nbxmpp.isResultNode(result):
reason = result.getErrorMsg() or result.getError()
app.nec.push_incoming_event(AccountNotCreatedEvent(
None, conn=self, reason=result.getError()))
None, conn=self, reason=reason))
return
if app.HAVE_GPG:
self.USE_GPG = True
......@@ -1088,6 +1095,10 @@ class Connection(CommonConnection, ConnectionHandlers):
]
self._hostname = hostname
if h:
app.resolver.resolve('_xmppconnect.' + helpers.idn_to_ascii(h),
self._on_resolve_txt, type_='txt')
if use_srv and self._proxy is None:
self._srv_hosts = []
......@@ -1184,15 +1195,18 @@ class Connection(CommonConnection, ConnectionHandlers):
if scheme == 'https':
connection_types = ['ssl']
else:
connection_types = ['plain']
if allow_plaintext_connection:
connection_types = ['plain']
else:
connection_types = []
host = self._select_next_host(self._hosts)
self._hosts.remove(host)
# Skip record if connection type is not supported.
if host['type'] not in connection_types:
log.debug("Skipping connection record with unsupported type: %s" %
host['type'])
log.info("Skipping connection record with unsupported type: %s",
host['type'])
self._connect_to_next_host(retry)
return
......@@ -1206,21 +1220,16 @@ class Connection(CommonConnection, ConnectionHandlers):
self._current_type = self._current_host['type']
port = self._current_host['port']
cacerts = os.path.join(common.app.DATA_DIR, 'other', 'cacerts.pem')
if not os.path.exists(cacerts):
cacerts = ''
cacerts = ''
if os.name == 'nt':
cacerts = certifi.where()
mycerts = common.app.MY_CACERTS
tls_version = app.config.get_per('accounts', self.name,
'tls_version')
cipher_list = app.config.get_per('accounts', self.name,
'cipher_list')
if version_condition(nbxmpp.__version__, '0.6.3'):
secure_tuple = (self._current_type, cacerts, mycerts, tls_version,
cipher_list, self._current_host['alpn'])
else:
secure_tuple = (self._current_type, cacerts, mycerts, tls_version,
cipher_list)
tls_version = app.config.get_per('accounts', self.name, 'tls_version')
cipher_list = app.config.get_per('accounts', self.name, 'cipher_list')
secure_tuple = (self._current_type, cacerts, mycerts, tls_version,
cipher_list, self._current_host['alpn'])
con = nbxmpp.NonBlockingClient(
domain=self._hostname,
......@@ -1843,6 +1852,8 @@ class Connection(CommonConnection, ConnectionHandlers):
our_server = app.config.get_per('accounts', self.name, 'hostname')
self.discoverInfo(our_jid, id_prefix='Gajim_')
self.discoverInfo(our_server, id_prefix='Gajim_')
else:
self.request_roster(resume=True)
self.sm.resuming = False # back to previous state
# Discover Stun server(s)
......@@ -2070,10 +2081,7 @@ class Connection(CommonConnection, ConnectionHandlers):
obj.timestamp = time.time()
obj.stanza_id = self.connection.send(obj.msg_iq, now=obj.now)
app.nec.push_incoming_event(MessageSentEvent(
None, conn=self, jid=obj.jid, message=obj.message, keyID=obj.keyID,
chatstate=obj.chatstate, automatic_message=obj.automatic_message,
stanza_id=obj.stanza_id, additional_data=obj.additional_data))
app.nec.push_incoming_event(MessageSentEvent(None, **vars(obj)))
if isinstance(obj.jid, list):
for j in obj.jid:
......@@ -2544,16 +2552,25 @@ class Connection(CommonConnection, ConnectionHandlers):
iq3.addChild(name='meta', attrs=dict_)
self.connection.send(iq)
def request_roster(self):
def request_roster(self, resume=False):
version = None
features = self.connection.Dispatcher.Stream.features
if features and features.getTag('ver',
namespace=nbxmpp.NS_ROSTER_VER):
version = app.config.get_per('accounts', self.name,
'roster_version')
if features and features.getTag('ver', namespace=nbxmpp.NS_ROSTER_VER):
version = app.config.get_per(
'accounts', self.name, 'roster_version')
iq_id = self.connection.initRoster(version=version,
request=not resume)
if resume:
self._init_roster_from_db()
else:
self.awaiting_answers[iq_id] = (ROSTER_ARRIVED, )
iq_id = self.connection.initRoster(version=version)
self.awaiting_answers[iq_id] = (ROSTER_ARRIVED, )
def _init_roster_from_db(self):
account_jid = app.get_jid_from_account(self.name)
roster_data = app.logger.get_roster(account_jid)
roster = self.connection.getRoster(force=True)
roster.setRaw(roster_data)
def send_agent_status(self, agent, ptype):
if not app.account_is_connected(self.name):
......@@ -2677,6 +2694,8 @@ class Connection(CommonConnection, ConnectionHandlers):
if obj.chatstate:
msg_iq.setTag(obj.chatstate, namespace=nbxmpp.NS_CHATSTATES)
if not obj.message:
msg_iq.setTag('no-store', namespace=nbxmpp.NS_MSG_HINTS)
if obj.label is not None:
msg_iq.addChild(node=obj.label)
......@@ -2728,6 +2747,13 @@ class Connection(CommonConnection, ConnectionHandlers):
if jid:
destroy.setAttr('jid', jid)
self.connection.send(iq)
i = 0
for bm in self.bookmarks:
if bm['jid'] == room_jid:
del self.bookmarks[i]
break
i += 1
self.store_bookmarks()
def send_gc_status(self, nick, jid, show, status, auto=False):
if not app.account_is_connected(self.name):
......@@ -2890,6 +2916,7 @@ class Connection(CommonConnection, ConnectionHandlers):
# on_remove_success as a class property as pass it as an argument
def _on_unregister_account_connect(con):
self.on_connect_auth = None
self.removing_account = True
if app.account_is_connected(self.name):
hostname = app.config.get_per('accounts', self.name, 'hostname')
iq = nbxmpp.Iq(typ='set', to=hostname)
......@@ -2908,6 +2935,7 @@ class Connection(CommonConnection, ConnectionHandlers):
con.SendAndWaitForResponse(iq)
return
on_remove_success(False)
self.removing_account = False
if self.connected == 0:
self.on_connect_auth = _on_unregister_account_connect
self.connect_and_auth()
......
......@@ -511,6 +511,7 @@ class ConnectionVcard:
'not-allowed'):
app.log('avatar').info('vCard not available: %s %s',
frm_jid, stanza_error)
callback(jid, resource, room, {})
return
vcard_node = stanza.getTag('vCard', namespace=nbxmpp.NS_VCARD)
......@@ -850,10 +851,9 @@ class ConnectionHandlersBase:
obj.contact.contact_name = obj.contact_nickname
obj.need_redraw = True
if obj.old_show == obj.new_show and obj.contact.status == \
obj.status and obj.contact.priority == obj.prio and \
obj.contact.idle_time == obj.idle_time: # no change
return True
elif obj.old_show != obj.new_show or obj.contact.status != \
obj.status:
obj.need_redraw = True
else:
obj.contact = app.contacts.get_first_contact_from_jid(account,
jid)
......@@ -1066,9 +1066,17 @@ class ConnectionHandlersBase:
conn=self, msg_obj=obj, stanza_id=obj.unique_id))
return True
def _check_for_mam_compliance(self, room_jid, stanza_id):
namespace = muc_caps_cache.get_mam_namespace(room_jid)
if stanza_id is None and namespace == nbxmpp.NS_MAM_2:
helpers.add_to_mam_blacklist(room_jid)
def _nec_gc_message_received(self, obj):
if obj.conn.name != self.name:
return
self._check_for_mam_compliance(obj.jid, obj.unique_id)
if (app.config.should_log(obj.conn.name, obj.jid) and
obj.msgtxt and obj.nick):
# if not obj.nick, it means message comes from room itself
......@@ -1489,10 +1497,7 @@ ConnectionHTTPUpload):
elif self.awaiting_answers[id_][0] == ROSTER_ARRIVED:
if iq_obj.getType() == 'result':
if not iq_obj.getTag('query'):
account_jid = app.get_jid_from_account(self.name)
roster_data = app.logger.get_roster(account_jid)
roster = self.connection.getRoster(force=True)
roster.setRaw(roster_data)
self._init_roster_from_db()
self._getRoster()
elif iq_obj.getType() == 'error':
self.roster_supported = False
......
This diff is collapsed.
......@@ -509,6 +509,15 @@ class Contacts():
return c
return self._contacts[jid][0]
def get_contact_strict(self, jid, resource):
"""
Return the contact instance for the given resource or None
"""
if jid in self._contacts:
for c in self._contacts[jid]:
if c.resource == resource:
return c
def get_avatar(self, jid, size=None, scale=None):
if jid not in self._contacts:
return None
......@@ -554,7 +563,7 @@ class Contacts():
Get Contact object for specific resource of given jid
"""
barejid, resource = common.app.get_room_and_nick_from_fjid(fjid)
return self.get_contact(barejid, resource)
return self.get_contact_strict(barejid, resource)
def get_first_contact_from_jid(self, jid):
if jid in self._contacts:
......
......@@ -73,7 +73,7 @@ if app.HAVE_GPG:
result = super(GnuPG, self).decrypt(data.encode('utf8'),
passphrase=self.passphrase)
return str(result)
return result.data.decode('utf8')
def sign(self, str_, keyID):
result = super(GnuPG, self).sign(str_.encode('utf8'), keyid=keyID, detach=True,
......
......@@ -432,7 +432,7 @@ def get_uf_sub(sub):
elif sub == 'both':
uf_sub = _('Both')
else:
uf_sub = sub
uf_sub = _('Unknown')
return uf_sub
......@@ -1240,8 +1240,13 @@ def get_accounts_info():
message = message.strip()
if message != '':
single_line += ': ' + message
accounts.append({'name': account, 'status_line': single_line,
'show': status, 'message': message})
account_label = app.config.get_per(
'accounts', account, 'account_label')
accounts.append({'name': account,
'account_label': account_label or account,
'status_line': single_line,
'show': status,
'message': message})
return accounts
......@@ -1592,3 +1597,53 @@ def version_condition(current_version, required_version):
if V(current_version) < V(required_version):
return False
return True
def get_available_emoticon_themes():
emoticons_themes = []
emoticons_data_path = os.path.join(app.DATA_DIR, 'emoticons')
font_theme_path = os.path.join(
app.DATA_DIR, 'emoticons', 'font-emoticons', 'emoticons_theme.py')
folders = os.listdir(emoticons_data_path)
if os.path.isdir(app.MY_EMOTS_PATH):
folders += os.listdir(app.MY_EMOTS_PATH)
file = 'emoticons_theme.py'
if os.name == 'nt' and not os.path.exists(font_theme_path):
# When starting Gajim from source .py files are available
# We test this with font-emoticons and fallback to .pyc files otherwise
file = 'emoticons_theme.pyc'
for theme in folders:
theme_path = os.path.join(emoticons_data_path, theme, file)
if os.path.exists(theme_path):
emoticons_themes.append(theme)
emoticons_themes.sort()
return emoticons_themes
def get_emoticon_theme_path(theme):
emoticons_data_path = os.path.join(app.DATA_DIR, 'emoticons', theme)
if os.path.exists(emoticons_data_path):
return emoticons_data_path
emoticons_user_path = os.path.join(app.MY_EMOTS_PATH, theme)
if os.path.exists(emoticons_user_path):
return emoticons_user_path
def add_to_mam_blacklist(jid):
config_value = app.config.get('mam_blacklist')
if not config_value:
config_value = [jid]
else:
if jid in config_value:
return
config_value = config_value.split(',')
config_value.append(jid)
log.warning('Found not-compliant MUC. %s added to MAM Blacklist', jid)
app.config.set('mam_blacklist', ','.join(config_value))
def get_mam_blacklist():
config_value = app.config.get('mam_blacklist')
if not config_value:
return []
return config_value.split(',')
......@@ -19,7 +19,7 @@ import threading
import ssl
import urllib
from urllib.request import Request, urlopen
from urllib.parse import urlparse
from urllib.parse import urlparse, quote
import io
import mimetypes
import logging
......@@ -187,7 +187,7 @@ class ConnectionHTTPUpload:
id_ = app.get_an_id()
iq.setID(id_)
request = iq.setTag(name="request", namespace=NS_HTTPUPLOAD)
request.addChild('filename', payload=os.path.basename(file.path))
request.addChild('filename', payload=quote(os.path.basename(file.path)))
request.addChild('size', payload=file.size)
request.addChild('content-type', payload=file.mime)
......
......@@ -523,6 +523,16 @@ class JingleSession:
# error.
# Lets check what kind of jingle session does the peer want
contents, contents_rejected, reason_txt = self.__parse_contents(jingle)
# If there's no content we understand...
if not contents:
# TODO: http://xmpp.org/extensions/xep-0166.html#session-terminate
reason = nbxmpp.Node('reason')
reason.setTag(reason_txt)
self.__ack(stanza, jingle, error, action)
self._session_terminate(reason)
raise nbxmpp.NodeProcessed
# If we are not receivin a file
# Check if there's already a session with this user:
if contents[0].media != 'file':
......@@ -553,14 +563,7 @@ class JingleSession:
'it is not allowed to request', pjid)
self.decline_session()
raise nbxmpp.NodeProcessed
# If there's no content we understand...
if not contents:
# TODO: http://xmpp.org/extensions/xep-0166.html#session-terminate
reason = nbxmpp.Node('reason')
reason.setTag(reason_txt)
self.__ack(stanza, jingle, error, action)
self._session_terminate(reason)
raise nbxmpp.NodeProcessed
self.state = JingleStates.PENDING
# Send event about starting a session
app.nec.push_incoming_event(JingleRequestReceivedEvent(None,
......
......@@ -525,7 +525,7 @@ class Logger:
''' % msg_log_id
)
results = self.cur.fetchone()
if len(results) == 0:
if results is None:
# Log line is no more in logs table. remove it from unread_messages
self.set_read_messages([msg_log_id])
continue
......@@ -1104,8 +1104,12 @@ class Logger:
:param account_jid: The jid of the account
"""
jid_id = self.get_jid_id(account_jid)
try:
jid_id = self.get_jid_id(account_jid)
except ValueError:
# This happens if the JID never made it to the Database
# because the account was never connected
return
sql = '''
DELETE FROM roster_entry WHERE account_jid_id = {jid_id};
......@@ -1158,6 +1162,7 @@ class Logger:
:param account: The account
:param archive_jid: The jid of the archive the stanza-id belongs to
only used if groupchat=True
:param stanza_id: The stanza-id
......@@ -1182,7 +1187,7 @@ class Logger:
if groupchat:
# Stanza ID is only unique within a specific archive.
# So a Stanza ID could be repeated in different MUCs, so we
# filter also for the archive JID
# filter also for the archive JID which is the bare MUC jid.
sql = '''
SELECT stanza_id FROM logs
WHERE stanza_id IN ({values})
......@@ -1193,14 +1198,14 @@ class Logger:
else:
sql = '''
SELECT stanza_id FROM logs
WHERE stanza_id IN ({values}) AND account_id = ? LIMIT 1
WHERE stanza_id IN ({values}) AND account_id = ? AND kind != ? LIMIT 1
'''.format(values=', '.join('?' * len(ids)))
result = self.con.execute(
sql, tuple(ids) + (account_id,)).fetchone()
sql, tuple(ids) + (account_id, KindConstant.GC_MSG)).fetchone()
if result is not None:
log.info('Found duplicated message, stanza-id: %s, origin-id: %s, '
'archive-jid: %s, account: %s', stanza_id, origin_id, archive_id, account_id)
'archive-jid: %s, account: %s', stanza_id, origin_id, archive_jid, account_id)
return True
return False
......
......@@ -25,6 +25,7 @@ import nbxmpp
from gajim.common import app
from gajim.common import ged
from gajim.common import helpers
from gajim.common.logger import KindConstant, JIDConstant
from gajim.common.const import ArchiveState
from gajim.common.caps_cache import muc_caps_cache
......@@ -200,13 +201,21 @@ class ConnectionArchive313:
def _nec_mam_decrypted_message_received(self, obj):
if obj.conn.name != self.name:
return
# if self.archiving_namespace != nbxmpp.NS_MAM_2:
# Fallback duplicate search without stanza-id
duplicate = app.logger.search_for_duplicate(
self.name, obj.with_, obj.timestamp, obj.msgtxt)
if duplicate:
# dont propagate the event further
return True
namespace = self.archiving_namespace
blacklisted = False
if obj.groupchat:
namespace = muc_caps_cache.get_mam_namespace(obj.room_jid)
blacklisted = obj.room_jid in helpers.get_mam_blacklist()
if namespace != nbxmpp.NS_MAM_2 or blacklisted:
# Fallback duplicate search without stanza-id
duplicate = app.logger.search_for_duplicate(
self.name, obj.with_, obj.timestamp, obj.msgtxt)
if duplicate:
# dont propagate the event further
return True
app.logger.insert_into_logs(self.name,
obj.with_,
obj.timestamp,
......
......@@ -14,9 +14,12 @@
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import gi
import logging
gi.require_version('Gst', '1.0')
from gi.repository import Gst
log = logging.getLogger('gajim.c.multimedia_helpers')
class DeviceManager(object):
def __init__(self):
......@@ -33,12 +36,17 @@ class DeviceManager(object):
def detect_element(self, name, text, pipe='%s'):
if Gst.ElementFactory.find(name):
element = Gst.ElementFactory.make(name, '%spresencetest' % name)
if element is None:
log.warning('could not create %spresencetest', name)
return
if hasattr(element.props, 'device'):
element.set_state(Gst.State.READY)
devices = element.get_properties('device')
if devices:
self.devices[text % _('Default device')] = pipe % name
for device in devices:
if device is None:
continue
element.set_state(Gst.State.NULL)
element.set_property('device', device)
element.set_state(Gst.State.READY)
......@@ -49,7 +57,7 @@ class DeviceManager(object):
else:
self.devices[text] = pipe % name
else:
print('element \'%s\' not found' % name)
log.info('element %s not found', name)
class AudioInputManager(DeviceManager):
......@@ -108,4 +116,3 @@ class VideoOutputManager(DeviceManager):
# ximagesink
self.detect_element('ximagesink', _('X Window System (without Xv)'))
self.detect_element('autovideosink', _('Autodetect'))
......@@ -48,7 +48,7 @@ class OptionsParser:
def read(self):
try:
fd = open(self.__filename)
fd = open(self.__filename, encoding='utf-8')
except Exception:
if os.path.exists(self.__filename):
#we talk about a file
......@@ -251,6 +251,8 @@ class OptionsParser:
self.update_config_to_0982()
if old < [0, 98, 3] and new >= [0, 98, 3]:
self.update_config_to_0983()
if old < [0, 99, 2] and new >= [0, 99, 2]:
self.update_config_to_0992()
app.logger.init_vars()
app.logger.attach_cache_database()
......@@ -935,3 +937,12 @@ class OptionsParser:
elif password == "libsecret:":
app.config.set_per('accounts', account, 'password', '')
app.config.set('version', '0.98.3')
def update_config_to_0992(self):
self.call_sql(logger.LOG_DB_PATH,
'''
CREATE INDEX IF NOT EXISTS
idx_logs_stanza_id ON logs (stanza_id);
'''
)
app.config.set('version', '0.99.2')
......@@ -483,6 +483,10 @@ class AvatarNotificationPEP(AbstractPEP):
def _extract_info(self, items):
self.avatar = None
for item in items.getTags('item'):
metadata = item.getTag('metadata')
if metadata is None:
app.log('avatar').warning('Invalid avatar stanza:\n%s', items)
break
info = item.getTag('metadata').getTag('info')
if info is not None:
self.avatar = info.getAttrs()
......@@ -494,7 +498,7 @@ class AvatarNotificationPEP(AbstractPEP):
con = app.connections[account]
if self.avatar is None:
# Remove avatar
app.log('avatar').debug('Remove (Pubsub): %s', jid)
app.log('avatar').info('Remove (Pubsub): %s', jid)
app.contacts.set_avatar(account, jid, None)
own_jid = con.get_own_jid().getStripped()
app.logger.set_avatar_sha(own_jid, jid, None)
......
......@@ -673,7 +673,10 @@ class ConnectionSocks5Bytestream(ConnectionBytestream):
# if we want to respect xep-0065 we have to check for proxy
# activation result in any result iq
real_id = iq_obj.getAttr('id')
if not real_id.startswith('au_'):
if real_id is None:
log.warning('Invalid IQ without id attribute:\n%s', iq_obj)
raise nbxmpp.NodeProcessed
if real_id is None or not real_id.startswith('au_'):
return
frm = self._ft_get_from(iq_obj)
id_ = real_id[3:]
......
......@@ -319,7 +319,7 @@ class SocksQueue:
self.process_result(result, sender)
def send_file(self, file_props, account, mode):
for key in self.senders.keys():
for key in list(self.senders.keys()):
if file_props.name in key and file_props.transport_sid in key \
and self.senders[key].mode == mode:
log.info('socks5: sending file')
......@@ -423,7 +423,7 @@ class SocksQueue:
connections with 1
"""
if idx != -1:
for key in self.senders.keys():
for key in list(self.senders.keys()):
if idx in key:
self.remove_sender_by_key(key, do_disconnect=do_disconnect)
if not remove_all:
......
......@@ -64,6 +64,7 @@ class ConnectionZeroconf(CommonConnection, ConnectionHandlersZeroconf):
# we don't need a password, but must be non-empty
self.password = 'zeroconf'
self.autoconnect = False
self.httpupload = False
CommonConnection.__init__(self, name)
self.is_zeroconf = True
......@@ -76,24 +77,6 @@ class ConnectionZeroconf(CommonConnection, ConnectionHandlersZeroconf):
Get name, host, port from config, or create zeroconf account with default
values
"""
if not app.config.get_per('accounts', app.ZEROCONF_ACC_NAME, 'name'):
log.debug('Creating zeroconf account')
app.config.add_per('accounts', app.ZEROCONF_ACC_NAME)
app.config.set_per('accounts', app.ZEROCONF_ACC_NAME,
'autoconnect', True)
app.config.set_per('accounts', app.ZEROCONF_ACC_NAME, 'no_log_for',
'')
app.config.set_per('accounts', app.ZEROCONF_ACC_NAME, 'password',
'zeroconf')
app.config.set_per('accounts', app.ZEROCONF_ACC_NAME,
'sync_with_global_status', True)
app.config.set_per('accounts', app.ZEROCONF_ACC_NAME,
'custom_port', 5298)
app.config.set_per('accounts', app.ZEROCONF_ACC_NAME,
'is_zeroconf', True)
app.config.set_per('accounts', app.ZEROCONF_ACC_NAME,
'use_ft_proxies', False)
self.host = socket.gethostname()
app.config.set_per('accounts', app.ZEROCONF_ACC_NAME, 'hostname',
self.host)
...