Add annotations and fix pylint/mypy errors
This commit is contained in:
parent
a62f348a21
commit
d54d4bc232
|
@ -273,7 +273,8 @@ class GajimApplication(Gtk.Application):
|
|||
self.activate()
|
||||
return 0
|
||||
|
||||
def _handle_local_options(self, application,
|
||||
def _handle_local_options(self,
|
||||
application: Gtk.Application,
|
||||
options: GLib.VariantDict) -> int:
|
||||
# Parse all options that have to be executed before ::startup
|
||||
if options.contains('profile'):
|
||||
|
|
|
@ -413,7 +413,7 @@ class ChatControl(ChatControlBase):
|
|||
img = self._pep_images[pep_type]
|
||||
if pep_type in pep:
|
||||
img.set_from_pixbuf(gtkgui_helpers.get_pep_as_pixbuf(pep[pep_type]))
|
||||
img.set_tooltip_markup(pep[pep_type].asMarkupText())
|
||||
img.set_tooltip_markup(pep[pep_type].as_markup_text())
|
||||
img.show()
|
||||
else:
|
||||
img.hide()
|
||||
|
|
|
@ -31,62 +31,77 @@ to automatic discovery and dispatching, also features manual control
|
|||
over the process.
|
||||
"""
|
||||
|
||||
from typing import Any # pylint: disable=unused-import
|
||||
from typing import Dict # pylint: disable=unused-import
|
||||
|
||||
from gajim.command_system.tools import remove
|
||||
|
||||
COMMANDS = {}
|
||||
CONTAINERS = {}
|
||||
COMMANDS = {} # type: Dict[Any, Any]
|
||||
CONTAINERS = {} # type: Dict[Any, Any]
|
||||
|
||||
|
||||
def add_host(host):
|
||||
CONTAINERS[host] = []
|
||||
|
||||
|
||||
def remove_host(host):
|
||||
remove(CONTAINERS, host)
|
||||
|
||||
|
||||
def add_container(container):
|
||||
for host in container.HOSTS:
|
||||
CONTAINERS[host].append(container)
|
||||
|
||||
|
||||
def remove_container(container):
|
||||
for host in container.HOSTS:
|
||||
remove(CONTAINERS[host], container)
|
||||
|
||||
|
||||
def add_commands(container):
|
||||
commands = COMMANDS.setdefault(container, {})
|
||||
for command in traverse_commands(container):
|
||||
for name in command.names:
|
||||
commands[name] = command
|
||||
|
||||
|
||||
def remove_commands(container):
|
||||
remove(COMMANDS, container)
|
||||
|
||||
|
||||
def traverse_commands(container):
|
||||
for name in dir(container):
|
||||
attribute = getattr(container, name)
|
||||
if is_command(attribute):
|
||||
yield attribute
|
||||
|
||||
|
||||
def is_command(attribute):
|
||||
from gajim.command_system.framework import Command
|
||||
return isinstance(attribute, Command)
|
||||
|
||||
|
||||
def is_root(namespace):
|
||||
metaclass = namespace.get("__metaclass__", None)
|
||||
if not metaclass:
|
||||
return False
|
||||
return issubclass(metaclass, Dispatchable)
|
||||
|
||||
|
||||
def get_command(host, name):
|
||||
for container in CONTAINERS[host]:
|
||||
command = COMMANDS[container].get(name)
|
||||
if command:
|
||||
return command
|
||||
|
||||
|
||||
def list_commands(host):
|
||||
for container in CONTAINERS[host]:
|
||||
commands = COMMANDS[container]
|
||||
for name, command in commands.items():
|
||||
yield name, command
|
||||
|
||||
|
||||
class Dispatchable(type):
|
||||
# pylint: disable=no-value-for-parameter
|
||||
def __init__(self, name, bases, namespace):
|
||||
|
@ -99,6 +114,7 @@ class Dispatchable(type):
|
|||
if self.AUTOMATIC:
|
||||
self.enable()
|
||||
|
||||
|
||||
class Host(Dispatchable):
|
||||
|
||||
def enable(self):
|
||||
|
@ -107,6 +123,7 @@ class Host(Dispatchable):
|
|||
def disable(self):
|
||||
remove_host(self)
|
||||
|
||||
|
||||
class Container(Dispatchable):
|
||||
|
||||
def enable(self):
|
||||
|
|
|
@ -34,6 +34,7 @@ code in here will not be executed and commands defined here will not be
|
|||
detected.
|
||||
"""
|
||||
|
||||
from gajim.common.i18n import _
|
||||
from gajim.command_system.framework import CommandContainer, command, doc
|
||||
from gajim.command_system.implementation.hosts import ChatCommands, PrivateChatCommands, GroupChatCommands
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ from os.path import expanduser
|
|||
|
||||
from gi.repository import GLib
|
||||
|
||||
from gajim.common.i18n import _
|
||||
from gajim.command_system.framework import CommandContainer, command, doc
|
||||
from gajim.command_system.implementation.hosts import ChatCommands, PrivateChatCommands, GroupChatCommands
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ from datetime import date
|
|||
from gajim import dialogs
|
||||
from gajim.common import app
|
||||
from gajim.common import helpers
|
||||
from gajim.common.i18n import _
|
||||
from gajim.common.exceptions import GajimGeneralException
|
||||
from gajim.common.const import KindConstant
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ from gajim.common.contacts import LegacyContactsAPI
|
|||
from gajim.common.events import Events
|
||||
from gajim.common.css_config import CSSConfig
|
||||
|
||||
interface = None # The actual interface (the gtk one for the moment)
|
||||
interface = None # type: gajim.interface.Interface
|
||||
thread_interface = lambda *args: None # Interface to run a thread and then a callback
|
||||
config = c_config.Config()
|
||||
version = gajim.__version__
|
||||
|
@ -52,10 +52,10 @@ ipython_window = None
|
|||
app = None # Gtk.Application
|
||||
|
||||
ged = ged_module.GlobalEventsDispatcher() # Global Events Dispatcher
|
||||
nec = None # Network Events Controller
|
||||
nec = None # type: gajim.common.nec.NetworkEventsController
|
||||
plugin_manager = None # Plugins Manager
|
||||
|
||||
logger = None
|
||||
logger = None # type: gajim.common.logger.Logger
|
||||
|
||||
# For backwards compatibility needed
|
||||
# some plugins use that
|
||||
|
@ -122,7 +122,7 @@ SHOW_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd',
|
|||
ZEROCONF_ACC_NAME = 'Local'
|
||||
|
||||
# These will be set in app.gui_interface.
|
||||
idlequeue = None
|
||||
idlequeue = None # type: nbxmpp.idlequeue.IdleQueue
|
||||
socks5queue = None
|
||||
|
||||
gajim_identity = {'type': 'pc', 'category': 'client', 'name': 'Gajim'}
|
||||
|
|
|
@ -32,6 +32,7 @@ from gi.repository import GLib
|
|||
from enum import IntEnum, unique
|
||||
|
||||
import gajim
|
||||
from gajim.common.i18n import _
|
||||
|
||||
|
||||
@unique
|
||||
|
|
|
@ -19,82 +19,90 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Dict # pylint: disable=unused-import
|
||||
from typing import List
|
||||
from typing import Generator
|
||||
from typing import Optional # pylint: disable=unused-import
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import gajim
|
||||
from gajim.common.i18n import _
|
||||
from gajim.common.const import PathType, PathLocation
|
||||
from gajim.common.types import PathTuple
|
||||
|
||||
|
||||
def get(key):
|
||||
def get(key: str) -> Union[str, List[str]]:
|
||||
if key == 'PLUGINS_DIRS':
|
||||
if gajim.IS_FLATPAK:
|
||||
return ['/app/plugins',
|
||||
_paths['PLUGINS_BASE']]
|
||||
else:
|
||||
return [_paths['PLUGINS_BASE'],
|
||||
_paths['PLUGINS_USER']]
|
||||
return [_paths['PLUGINS_BASE'],
|
||||
_paths['PLUGINS_USER']]
|
||||
return _paths[key]
|
||||
|
||||
|
||||
def get_paths(type_):
|
||||
def get_paths(type_: PathType) -> Generator[str, None, None]:
|
||||
for key, value in _paths.items():
|
||||
location, path, path_type = value
|
||||
path_type = value[2]
|
||||
if type_ != path_type:
|
||||
continue
|
||||
yield _paths[key]
|
||||
|
||||
|
||||
def override_path(*args, **kwargs):
|
||||
_paths._add(*args, **kwargs)
|
||||
_paths.add(*args, **kwargs)
|
||||
|
||||
|
||||
def set_separation(active: bool):
|
||||
def set_separation(active: bool) -> None:
|
||||
_paths.profile_separation = active
|
||||
|
||||
|
||||
def set_profile(profile: str):
|
||||
def set_profile(profile: str) -> None:
|
||||
_paths.profile = profile
|
||||
|
||||
|
||||
def set_config_root(config_root: str):
|
||||
def set_config_root(config_root: str) -> None:
|
||||
_paths.custom_config_root = config_root
|
||||
|
||||
|
||||
def init():
|
||||
def init() -> None:
|
||||
_paths.init()
|
||||
|
||||
|
||||
def create_paths():
|
||||
def create_paths() -> None:
|
||||
for path in get_paths(PathType.FOLDER):
|
||||
if not isinstance(path, Path):
|
||||
path = Path(path)
|
||||
path_ = Path(path)
|
||||
|
||||
if path.is_file():
|
||||
print(_('%s is a file but it should be a directory') % path)
|
||||
if path_.is_file():
|
||||
print(_('%s is a file but it should be a directory') % path_)
|
||||
print(_('Gajim will now exit'))
|
||||
sys.exit()
|
||||
|
||||
if not path.exists():
|
||||
for parent_path in reversed(path.parents):
|
||||
if not path_.exists():
|
||||
for parent_path in reversed(path_.parents):
|
||||
# Create all parent folders
|
||||
# don't use mkdir(parent=True), as it ignores `mode`
|
||||
# when creating the parents
|
||||
if not parent_path.exists():
|
||||
print(('creating %s directory') % parent_path)
|
||||
parent_path.mkdir(mode=0o700)
|
||||
print(('creating %s directory') % path)
|
||||
path.mkdir(mode=0o700)
|
||||
print(('creating %s directory') % path_)
|
||||
path_.mkdir(mode=0o700)
|
||||
|
||||
|
||||
class ConfigPaths:
|
||||
def __init__(self):
|
||||
self._paths = {}
|
||||
def __init__(self) -> None:
|
||||
self._paths = {} # type: Dict[str, PathTuple]
|
||||
self.profile = ''
|
||||
self.profile_separation = False
|
||||
self.custom_config_root = None
|
||||
self.custom_config_root = None # type: Optional[str]
|
||||
|
||||
if os.name == 'nt':
|
||||
try:
|
||||
|
@ -133,23 +141,23 @@ class ConfigPaths:
|
|||
]
|
||||
|
||||
for path in source_paths:
|
||||
self._add(*path)
|
||||
self.add(*path)
|
||||
|
||||
def __getitem__(self, key):
|
||||
def __getitem__(self, key: str) -> str:
|
||||
location, path, _ = self._paths[key]
|
||||
if location == PathLocation.CONFIG:
|
||||
return os.path.join(self.config_root, path)
|
||||
elif location == PathLocation.CACHE:
|
||||
if location == PathLocation.CACHE:
|
||||
return os.path.join(self.cache_root, path)
|
||||
elif location == PathLocation.DATA:
|
||||
if location == PathLocation.DATA:
|
||||
return os.path.join(self.data_root, path)
|
||||
return path
|
||||
|
||||
def items(self):
|
||||
def items(self) -> Generator[Tuple[str, PathTuple], None, None]:
|
||||
for key, value in self._paths.items():
|
||||
yield (key, value)
|
||||
|
||||
def _prepare(self, path, unique):
|
||||
def _prepare(self, path: str, unique: bool) -> str:
|
||||
if os.name == 'nt':
|
||||
path = path.capitalize()
|
||||
if self.profile:
|
||||
|
@ -157,7 +165,12 @@ class ConfigPaths:
|
|||
return '%s.%s' % (path, self.profile)
|
||||
return path
|
||||
|
||||
def _add(self, name, path, location=None, path_type=None, unique=False):
|
||||
def add(self,
|
||||
name: str,
|
||||
path: str,
|
||||
location: PathLocation = None,
|
||||
path_type: PathType = None,
|
||||
unique: bool = False) -> None:
|
||||
if path and location is not None:
|
||||
path = self._prepare(path, unique)
|
||||
self._paths[name] = (location, path, path_type)
|
||||
|
@ -175,7 +188,7 @@ class ConfigPaths:
|
|||
]
|
||||
|
||||
for path in user_dir_paths:
|
||||
self._add(*path)
|
||||
self.add(*path)
|
||||
|
||||
# These paths are unique per profile
|
||||
unique_profile_paths = [
|
||||
|
@ -191,7 +204,7 @@ class ConfigPaths:
|
|||
]
|
||||
|
||||
for path in unique_profile_paths:
|
||||
self._add(*path, unique=True)
|
||||
self.add(*path, unique=True)
|
||||
|
||||
# These paths are only unique per profile if the commandline arg
|
||||
# `separate` is passed
|
||||
|
@ -219,7 +232,7 @@ class ConfigPaths:
|
|||
]
|
||||
|
||||
for path in paths:
|
||||
self._add(*path)
|
||||
self.add(*path)
|
||||
|
||||
|
||||
_paths = ConfigPaths()
|
||||
|
|
|
@ -176,7 +176,6 @@ class HelperEvent:
|
|||
|
||||
class IqErrorReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
|
||||
name = 'iq-error-received'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
self.get_id()
|
||||
|
@ -187,7 +186,6 @@ class IqErrorReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
|
|||
|
||||
class StreamReceivedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'stream-received'
|
||||
base_network_events = []
|
||||
|
||||
class StreamConflictReceivedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'stream-conflict-received'
|
||||
|
@ -321,7 +319,6 @@ PresenceHelperEvent):
|
|||
|
||||
class ZeroconfPresenceReceivedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'presence-received'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
self.jid, self.resource = app.get_room_and_nick_from_fjid(self.fjid)
|
||||
|
@ -348,7 +345,6 @@ class ZeroconfPresenceReceivedEvent(nec.NetworkIncomingEvent):
|
|||
|
||||
class GcPresenceReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
|
||||
name = 'gc-presence-received'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
self.ptype = self.presence_obj.ptype
|
||||
|
@ -436,15 +432,12 @@ class GcPresenceReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
|
|||
|
||||
class OurShowEvent(nec.NetworkIncomingEvent):
|
||||
name = 'our-show'
|
||||
base_network_events = []
|
||||
|
||||
class BeforeChangeShowEvent(nec.NetworkIncomingEvent):
|
||||
name = 'before-change-show'
|
||||
base_network_events = []
|
||||
|
||||
class ChatstateReceivedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'chatstate-received'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
self.stanza = self.msg_obj.stanza
|
||||
|
@ -456,7 +449,6 @@ class ChatstateReceivedEvent(nec.NetworkIncomingEvent):
|
|||
|
||||
class GcMessageReceivedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'gc-message-received'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
self.stanza = self.msg_obj.stanza
|
||||
|
@ -556,7 +548,6 @@ class GcMessageReceivedEvent(nec.NetworkIncomingEvent):
|
|||
|
||||
class GcConfigChangedReceivedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'gc-config-changed-received'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
self.conn = self.msg_event.conn
|
||||
|
@ -567,7 +558,6 @@ class GcConfigChangedReceivedEvent(nec.NetworkIncomingEvent):
|
|||
|
||||
class MessageSentEvent(nec.NetworkIncomingEvent):
|
||||
name = 'message-sent'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
if not self.automatic_message:
|
||||
|
@ -579,11 +569,9 @@ class MessageSentEvent(nec.NetworkIncomingEvent):
|
|||
|
||||
class MessageNotSentEvent(nec.NetworkIncomingEvent):
|
||||
name = 'message-not-sent'
|
||||
base_network_events = []
|
||||
|
||||
class MessageErrorEvent(nec.NetworkIncomingEvent, HelperEvent):
|
||||
name = 'message-error'
|
||||
base_network_events = []
|
||||
|
||||
def init(self):
|
||||
self.zeroconf = False
|
||||
|
@ -600,11 +588,9 @@ class MessageErrorEvent(nec.NetworkIncomingEvent, HelperEvent):
|
|||
|
||||
class AnonymousAuthEvent(nec.NetworkIncomingEvent):
|
||||
name = 'anonymous-auth'
|
||||
base_network_events = []
|
||||
|
||||
class JingleRequestReceivedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'jingle-request-received'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
self.fjid = self.jingle_session.peerjid
|
||||
|
@ -614,7 +600,6 @@ class JingleRequestReceivedEvent(nec.NetworkIncomingEvent):
|
|||
|
||||
class JingleConnectedReceivedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'jingle-connected-received'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
self.fjid = self.jingle_session.peerjid
|
||||
|
@ -624,7 +609,6 @@ class JingleConnectedReceivedEvent(nec.NetworkIncomingEvent):
|
|||
|
||||
class JingleDisconnectedReceivedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'jingle-disconnected-received'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
self.fjid = self.jingle_session.peerjid
|
||||
|
@ -634,7 +618,6 @@ class JingleDisconnectedReceivedEvent(nec.NetworkIncomingEvent):
|
|||
|
||||
class JingleTransferCancelledEvent(nec.NetworkIncomingEvent):
|
||||
name = 'jingleFT-cancelled-received'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
self.fjid = self.jingle_session.peerjid
|
||||
|
@ -644,7 +627,6 @@ class JingleTransferCancelledEvent(nec.NetworkIncomingEvent):
|
|||
|
||||
class JingleErrorReceivedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'jingle-error-received'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
self.fjid = self.jingle_session.peerjid
|
||||
|
@ -654,15 +636,12 @@ class JingleErrorReceivedEvent(nec.NetworkIncomingEvent):
|
|||
|
||||
class AccountCreatedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'account-created'
|
||||
base_network_events = []
|
||||
|
||||
class AccountNotCreatedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'account-not-created'
|
||||
base_network_events = []
|
||||
|
||||
class NewAccountConnectedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'new-account-connected'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
try:
|
||||
|
@ -686,15 +665,12 @@ class NewAccountConnectedEvent(nec.NetworkIncomingEvent):
|
|||
|
||||
class NewAccountNotConnectedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'new-account-not-connected'
|
||||
base_network_events = []
|
||||
|
||||
class ConnectionTypeEvent(nec.NetworkIncomingEvent):
|
||||
name = 'connection-type'
|
||||
base_network_events = []
|
||||
|
||||
class StanzaReceivedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'stanza-received'
|
||||
base_network_events = []
|
||||
|
||||
def init(self):
|
||||
self.additional_data = {}
|
||||
|
@ -704,14 +680,12 @@ class StanzaReceivedEvent(nec.NetworkIncomingEvent):
|
|||
|
||||
class StanzaSentEvent(nec.NetworkIncomingEvent):
|
||||
name = 'stanza-sent'
|
||||
base_network_events = []
|
||||
|
||||
def init(self):
|
||||
self.additional_data = {}
|
||||
|
||||
class AgentRemovedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'agent-removed'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
self.jid_list = []
|
||||
|
@ -722,7 +696,6 @@ class AgentRemovedEvent(nec.NetworkIncomingEvent):
|
|||
|
||||
class BadGPGPassphraseEvent(nec.NetworkIncomingEvent):
|
||||
name = 'bad-gpg-passphrase'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
self.account = self.conn.name
|
||||
|
@ -732,7 +705,6 @@ class BadGPGPassphraseEvent(nec.NetworkIncomingEvent):
|
|||
|
||||
class ConnectionLostEvent(nec.NetworkIncomingEvent):
|
||||
name = 'connection-lost'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
app.nec.push_incoming_event(OurShowEvent(None, conn=self.conn,
|
||||
|
@ -741,11 +713,9 @@ class ConnectionLostEvent(nec.NetworkIncomingEvent):
|
|||
|
||||
class GPGTrustKeyEvent(nec.NetworkIncomingEvent):
|
||||
name = 'gpg-trust-key'
|
||||
base_network_events = []
|
||||
|
||||
class GPGPasswordRequiredEvent(nec.NetworkIncomingEvent):
|
||||
name = 'gpg-password-required'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
self.keyid = app.config.get_per('accounts', self.conn.name, 'keyid')
|
||||
|
@ -753,7 +723,6 @@ class GPGPasswordRequiredEvent(nec.NetworkIncomingEvent):
|
|||
|
||||
class PEPReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
|
||||
name = 'pep-received'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
if not self.stanza.getTag('event'):
|
||||
|
@ -778,72 +747,57 @@ class PEPReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
|
|||
|
||||
class PlainConnectionEvent(nec.NetworkIncomingEvent):
|
||||
name = 'plain-connection'
|
||||
base_network_events = []
|
||||
|
||||
class InsecurePasswordEvent(nec.NetworkIncomingEvent):
|
||||
name = 'insecure-password'
|
||||
base_network_events = []
|
||||
|
||||
class InsecureSSLConnectionEvent(nec.NetworkIncomingEvent):
|
||||
name = 'insecure-ssl-connection'
|
||||
base_network_events = []
|
||||
|
||||
class SSLErrorEvent(nec.NetworkIncomingEvent):
|
||||
name = 'ssl-error'
|
||||
base_network_events = []
|
||||
|
||||
class UniqueRoomIdSupportedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'unique-room-id-supported'
|
||||
base_network_events = []
|
||||
|
||||
class UniqueRoomIdNotSupportedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'unique-room-id-not-supported'
|
||||
base_network_events = []
|
||||
|
||||
class NonAnonymousServerErrorEvent(nec.NetworkIncomingEvent):
|
||||
name = 'non-anonymous-server-error'
|
||||
base_network_events = []
|
||||
|
||||
class UpdateGCAvatarEvent(nec.NetworkIncomingEvent):
|
||||
name = 'update-gc-avatar'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
return True
|
||||
|
||||
class UpdateRosterAvatarEvent(nec.NetworkIncomingEvent):
|
||||
name = 'update-roster-avatar'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
return True
|
||||
|
||||
class UpdateRoomAvatarEvent(nec.NetworkIncomingEvent):
|
||||
name = 'update-room-avatar'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
return True
|
||||
|
||||
class ZeroconfNameConflictEvent(nec.NetworkIncomingEvent):
|
||||
name = 'zeroconf-name-conflict'
|
||||
base_network_events = []
|
||||
|
||||
class PasswordRequiredEvent(nec.NetworkIncomingEvent):
|
||||
name = 'password-required'
|
||||
base_network_events = []
|
||||
|
||||
class Oauth2CredentialsRequiredEvent(nec.NetworkIncomingEvent):
|
||||
name = 'oauth2-credentials-required'
|
||||
base_network_events = []
|
||||
|
||||
class SignedInEvent(nec.NetworkIncomingEvent):
|
||||
name = 'signed-in'
|
||||
base_network_events = []
|
||||
|
||||
class FileRequestReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
|
||||
name = 'file-request-received'
|
||||
base_network_events = []
|
||||
|
||||
def init(self):
|
||||
self.jingle_content = None
|
||||
|
@ -960,7 +914,6 @@ class FileRequestReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
|
|||
|
||||
class FileRequestErrorEvent(nec.NetworkIncomingEvent):
|
||||
name = 'file-request-error'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
self.jid = app.get_jid_without_resource(self.jid)
|
||||
|
@ -968,7 +921,6 @@ class FileRequestErrorEvent(nec.NetworkIncomingEvent):
|
|||
|
||||
class FileTransferCompletedEvent(nec.NetworkIncomingEvent):
|
||||
name = 'file-transfer-completed'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
jid = str(self.file_props.receiver)
|
||||
|
@ -977,7 +929,6 @@ class FileTransferCompletedEvent(nec.NetworkIncomingEvent):
|
|||
|
||||
class GatewayPromptReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
|
||||
name = 'gateway-prompt-received'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
self.get_jid_resource()
|
||||
|
@ -1251,7 +1202,6 @@ class NotificationEvent(nec.NetworkIncomingEvent):
|
|||
|
||||
class MessageOutgoingEvent(nec.NetworkOutgoingEvent):
|
||||
name = 'message-outgoing'
|
||||
base_network_events = []
|
||||
|
||||
def init(self):
|
||||
self.additional_data = {}
|
||||
|
@ -1297,21 +1247,18 @@ class MessageOutgoingEvent(nec.NetworkOutgoingEvent):
|
|||
|
||||
class StanzaMessageOutgoingEvent(nec.NetworkOutgoingEvent):
|
||||
name='stanza-message-outgoing'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
return True
|
||||
|
||||
class GcStanzaMessageOutgoingEvent(nec.NetworkOutgoingEvent):
|
||||
name='gc-stanza-message-outgoing'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
return True
|
||||
|
||||
class GcMessageOutgoingEvent(nec.NetworkOutgoingEvent):
|
||||
name = 'gc-message-outgoing'
|
||||
base_network_events = []
|
||||
|
||||
def init(self):
|
||||
self.additional_data = {}
|
||||
|
@ -1333,11 +1280,9 @@ class GcMessageOutgoingEvent(nec.NetworkOutgoingEvent):
|
|||
|
||||
class ClientCertPassphraseEvent(nec.NetworkIncomingEvent):
|
||||
name = 'client-cert-passphrase'
|
||||
base_network_events = []
|
||||
|
||||
class InformationEvent(nec.NetworkIncomingEvent):
|
||||
name = 'information'
|
||||
base_network_events = []
|
||||
|
||||
def init(self):
|
||||
self.args = None
|
||||
|
@ -1355,7 +1300,6 @@ class InformationEvent(nec.NetworkIncomingEvent):
|
|||
|
||||
class StyleChanged(nec.NetworkIncomingEvent):
|
||||
name = 'style-changed'
|
||||
base_network_events = []
|
||||
|
||||
def generate(self):
|
||||
return True
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from enum import IntEnum, Enum, unique
|
||||
from collections import namedtuple
|
||||
|
||||
from gajim.common.i18n import _
|
||||
|
||||
Option = namedtuple('Option', 'kind label type value name callback data desc enabledif props')
|
||||
Option.__new__.__defaults__ = (None,) * len(Option._fields)
|
||||
|
||||
|
@ -168,13 +170,14 @@ class PEPHandlerType(IntEnum):
|
|||
|
||||
@unique
|
||||
class PEPEventType(IntEnum):
|
||||
ACTIVITY = 0
|
||||
TUNE = 1
|
||||
MOOD = 2
|
||||
LOCATION = 3
|
||||
NICKNAME = 4
|
||||
AVATAR = 5
|
||||
ATOM = 6
|
||||
ABSTRACT = 0
|
||||
ACTIVITY = 1
|
||||
TUNE = 2
|
||||
MOOD = 3
|
||||
LOCATION = 4
|
||||
NICKNAME = 5
|
||||
AVATAR = 6
|
||||
ATOM = 7
|
||||
|
||||
|
||||
ACTIVITIES = {
|
||||
|
|
|
@ -25,6 +25,7 @@ import logging
|
|||
|
||||
from gajim.common import app
|
||||
from gajim.common import exceptions
|
||||
from gajim.common.i18n import _
|
||||
|
||||
_GAJIM_ERROR_IFACE = 'org.gajim.dbus.Error'
|
||||
|
||||
|
|
|
@ -48,6 +48,7 @@ from string import Template
|
|||
import nbxmpp
|
||||
|
||||
from gajim.common.i18n import Q_
|
||||
from gajim.common.i18n import _
|
||||
from gajim.common.i18n import ngettext
|
||||
from gajim.common import configpaths
|
||||
|
||||
|
|
|
@ -132,7 +132,7 @@ if os.name == 'nt':
|
|||
|
||||
_localedir = get_locale_dir()
|
||||
if hasattr(locale, 'bindtextdomain'):
|
||||
locale.bindtextdomain(DOMAIN, _localedir)
|
||||
locale.bindtextdomain(DOMAIN, _localedir) # type: ignore
|
||||
gettext.textdomain(DOMAIN)
|
||||
|
||||
gettext.install(DOMAIN, _localedir)
|
||||
|
@ -142,7 +142,7 @@ try:
|
|||
except OSError:
|
||||
_ = gettext.gettext
|
||||
|
||||
if gettext._translations:
|
||||
_translations = list(gettext._translations.values())[0]
|
||||
if gettext._translations: # type: ignore
|
||||
_translations = list(gettext._translations.values())[0] # type: ignore
|
||||
else:
|
||||
_translations = gettext.NullTranslations()
|
||||
|
|
|
@ -12,14 +12,18 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict # pylint: disable=unused-import
|
||||
from typing import List
|
||||
from typing import Tuple
|
||||
|
||||
import logging
|
||||
from typing import List # noqa
|
||||
from typing import Dict # noqa
|
||||
from typing import Any # noqa
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from gajim.common.types import ConnectionT
|
||||
|
||||
log = logging.getLogger('gajim.c.m')
|
||||
|
||||
ZEROCONF_MODULES = ['adhoc_commands', 'receipts', 'discovery']
|
||||
|
@ -70,7 +74,7 @@ class ModuleMock:
|
|||
return MagicMock()
|
||||
|
||||
|
||||
def register(con, *args, **kwargs):
|
||||
def register(con: ConnectionT, *args: Any, **kwargs: Any) -> None:
|
||||
if con in _modules:
|
||||
return
|
||||
_modules[con.name] = {}
|
||||
|
@ -83,20 +87,20 @@ def register(con, *args, **kwargs):
|
|||
_modules[con.name][name] = instance
|
||||
|
||||
|
||||
def register_single(con, instance, name):
|
||||
def register_single(con: ConnectionT, instance: Any, name: str) -> None:
|
||||
if con.name not in _modules:
|
||||
raise ValueError('Unknown account name: %s' % con.name)
|
||||
_modules[con.name][name] = instance
|
||||
|
||||
|
||||
def unregister(con):
|
||||
def unregister(con: ConnectionT) -> None:
|
||||
for instance in _modules[con.name].values():
|
||||
if hasattr(instance, 'cleanup'):
|
||||
instance.cleanup()
|
||||
del _modules[con.name]
|
||||
|
||||
|
||||
def unregister_single(con, name):
|
||||
def unregister_single(con: ConnectionT, name: str) -> None:
|
||||
if con.name not in _modules:
|
||||
return
|
||||
if name not in _modules[con.name]:
|
||||
|
@ -111,8 +115,8 @@ def get(account: str, name: str) -> Any:
|
|||
return ModuleMock(name)
|
||||
|
||||
|
||||
def get_handlers(con):
|
||||
handlers = []
|
||||
def get_handlers(con: ConnectionT) -> List[Tuple[Any, ...]]:
|
||||
handlers = [] # type: List[Tuple[Any, ...]]
|
||||
for module in _modules[con.name].values():
|
||||
handlers += module.handlers
|
||||
return handlers
|
||||
|
|
|
@ -14,24 +14,30 @@
|
|||
|
||||
# XEP-0145: Annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict # pylint: disable=unused-import
|
||||
from typing import List # pylint: disable=unused-import
|
||||
from typing import Tuple
|
||||
|
||||
import logging
|
||||
|
||||
import nbxmpp
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common import helpers
|
||||
from gajim.common.types import ConnectionT
|
||||
|
||||
log = logging.getLogger('gajim.c.m.annotations')
|
||||
|
||||
|
||||
class Annotations:
|
||||
def __init__(self, con):
|
||||
def __init__(self, con: ConnectionT) -> None:
|
||||
self._con = con
|
||||
self._account = con.name
|
||||
self._server = self._con.get_own_jid().getDomain()
|
||||
|
||||
self.handlers = []
|
||||
self.annotations = {}
|
||||
self.handlers = [] # type: List[Tuple[Any, ...]]
|
||||
self.annotations = {} # type: Dict[str, str]
|
||||
|
||||
def get_annotations(self) -> None:
|
||||
if not app.account_is_connected(self._account):
|
||||
|
@ -93,5 +99,5 @@ class Annotations:
|
|||
log.info('Storing rosternotes successful')
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
def get_instance(*args: Any, **kwargs: Any) -> Tuple[Annotations, str]:
|
||||
return Annotations(*args, **kwargs), 'Annotations'
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
|
||||
# XEP-0163: Personal Eventing Protocol
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Tuple
|
||||
|
||||
import logging
|
||||
|
||||
import nbxmpp
|
||||
|
@ -22,12 +27,16 @@ from gajim.common import app
|
|||
from gajim.common.exceptions import StanzaMalformed
|
||||
from gajim.common.nec import NetworkIncomingEvent
|
||||
from gajim.common.const import PEPHandlerType, PEPEventType
|
||||
from gajim.common.types import ConnectionT
|
||||
from gajim.common.types import PEPHandlersDict # pylint: disable=unused-import
|
||||
from gajim.common.types import PEPNotifyCallback
|
||||
from gajim.common.types import PEPRetractCallback
|
||||
|
||||
log = logging.getLogger('gajim.c.m.pep')
|
||||
|
||||
|
||||
class PEP:
|
||||
def __init__(self, con):
|
||||
def __init__(self, con: ConnectionT) -> None:
|
||||
self._con = con
|
||||
self._account = con.name
|
||||
|
||||
|
@ -37,29 +46,40 @@ class PEP:
|
|||
]
|
||||
|
||||
self.supported = False
|
||||
self._pep_handlers = {}
|
||||
self._store_publish_modules = []
|
||||
self._pep_handlers = {} # type: PEPHandlersDict
|
||||
self._store_publish_modules = [] # type: List[Any]
|
||||
|
||||
def pass_disco(self, from_, identities, _features, _data, _node):
|
||||
def pass_disco(self,
|
||||
from_: nbxmpp.JID,
|
||||
identities: List[Dict[str, str]],
|
||||
_features: List[str],
|
||||
_data: List[nbxmpp.DataForm],
|
||||
_node: str) -> None:
|
||||
for identity in identities:
|
||||
if identity['category'] == 'pubsub':
|
||||
if identity.get('type') == 'pep':
|
||||
log.info('Discovered PEP support: %s', from_)
|
||||
self.supported = True
|
||||
|
||||
def register_pep_handler(self, namespace, notify_handler, retract_handler):
|
||||
def register_pep_handler(
|
||||
self,
|
||||
namespace: str,
|
||||
notify_handler: PEPNotifyCallback,
|
||||
retract_handler: PEPRetractCallback) -> None:
|
||||
if namespace in self._pep_handlers:
|
||||
self._pep_handlers[namespace].append(
|
||||
(notify_handler, retract_handler))
|
||||
else:
|
||||
self._pep_handlers[namespace] = [(notify_handler, retract_handler)]
|
||||
if notify_handler:
|
||||
module_instance = notify_handler.__self__
|
||||
module_instance = notify_handler.__self__ # type: ignore
|
||||
if module_instance.store_publish:
|
||||
if module_instance not in self._store_publish_modules:
|
||||
self._store_publish_modules.append(module_instance)
|
||||
|
||||
def _pep_event_received(self, _con, stanza):
|
||||
def _pep_event_received(self,
|
||||
_con: ConnectionT,
|
||||
stanza: nbxmpp.Message) -> None:
|
||||
jid = stanza.getFrom()
|
||||
event = stanza.getTag('event', namespace=nbxmpp.NS_PUBSUB_EVENT)
|
||||
items = event.getTag('items')
|
||||
|
@ -98,17 +118,47 @@ class PEP:
|
|||
handler[PEPHandlerType.NOTIFY](jid, items_[0])
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def send_stored_publish(self):
|
||||
def send_stored_publish(self) -> None:
|
||||
for module in self._store_publish_modules:
|
||||
module.send_stored_publish()
|
||||
|
||||
def reset_stored_publish(self):
|
||||
def reset_stored_publish(self) -> None:
|
||||
for module in self._store_publish_modules:
|
||||
module.reset_stored_publish()
|
||||
|
||||
|
||||
class AbstractPEPData:
|
||||
|
||||
type_ = PEPEventType.ABSTRACT
|
||||
|
||||
def __init__(self, data: Any) -> None:
|
||||
self.data = data
|
||||
|
||||
def as_markup_text(self) -> str: # pylint: disable=no-self-use
|
||||
'''SHOULD be implemented by subclasses'''
|
||||
return ''
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return other == self.type_
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return self.data is not None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.data)
|
||||
|
||||
|
||||
class AbstractPEPModule:
|
||||
def __init__(self, con, account):
|
||||
|
||||
name = ''
|
||||
namespace = ''
|
||||
pep_class = AbstractPEPData
|
||||
store_publish = True
|
||||
_log = log
|
||||
|
||||
def __init__(self,
|
||||
con: ConnectionT,
|
||||
account: str) -> None:
|
||||
self._account = account
|
||||
self._con = con
|
||||
|
||||
|
@ -119,7 +169,7 @@ class AbstractPEPModule:
|
|||
self._pep_notify_received,
|
||||
self._pep_retract_received)
|
||||
|
||||
def _pep_notify_received(self, jid, item):
|
||||
def _pep_notify_received(self, jid: nbxmpp.JID, item: nbxmpp.Node) -> None:
|
||||
try:
|
||||
data = self._extract_info(item)
|
||||
except StanzaMalformed as error:
|
||||
|
@ -129,19 +179,19 @@ class AbstractPEPModule:
|
|||
self._log.info('Received: %s %s', jid, data)
|
||||
self._push_event(jid, self.pep_class(data))
|
||||
|
||||
def _pep_retract_received(self, jid, id_):
|
||||
def _pep_retract_received(self, jid: nbxmpp.JID, id_: str) -> None:
|
||||
self._log.info('Retract: %s %s', jid, id_)
|
||||
self._push_event(jid, self.pep_class(None))
|
||||
|
||||
def _extract_info(self, item):
|
||||
def _extract_info(self, item: nbxmpp.Node) -> Any:
|
||||
'''To be implemented by subclasses'''
|
||||
raise NotImplementedError
|
||||
|
||||
def _build_node(self, data):
|
||||
def _build_node(self, data: Any) -> nbxmpp.Node:
|
||||
'''To be implemented by subclasses'''
|
||||
raise NotImplementedError
|
||||
|
||||
def _push_event(self, jid, user_pep):
|
||||
def _push_event(self, jid: nbxmpp.JID, user_pep: Any) -> None:
|
||||
self._notification_received(jid, user_pep)
|
||||
app.nec.push_incoming_event(
|
||||
PEPReceivedEvent(None, conn=self._con,
|
||||
|
@ -149,7 +199,7 @@ class AbstractPEPModule:
|
|||
pep_type=self.name,
|
||||
user_pep=user_pep))
|
||||
|
||||
def _notification_received(self, jid, user_pep):
|
||||
def _notification_received(self, jid: nbxmpp.JID, user_pep: Any) -> None:
|
||||
for contact in app.contacts.get_contacts(self._account, str(jid)):
|
||||
if user_pep:
|
||||
contact.pep[self.name] = user_pep
|
||||
|
@ -162,17 +212,17 @@ class AbstractPEPModule:
|
|||
else:
|
||||
self._con.pep.pop(self.name, None)
|
||||
|
||||
def send_stored_publish(self):
|
||||
def send_stored_publish(self) -> None:
|
||||
if self._stored_publish is not None:
|
||||
self._log.info('Send stored publish')
|
||||
self.send(self._stored_publish)
|
||||
self._stored_publish = None
|
||||
|
||||
def reset_stored_publish(self):
|
||||
def reset_stored_publish(self) -> None:
|
||||
self._log.info('Reset stored publish')
|
||||
self._stored_publish = None
|
||||
|
||||
def send(self, data):
|
||||
def send(self, data: Any) -> None:
|
||||
if not self._con.get_module('PEP').supported:
|
||||
return
|
||||
|
||||
|
@ -191,7 +241,7 @@ class AbstractPEPModule:
|
|||
self._con.get_module('PubSub').send_pb_publish(
|
||||
'', self.namespace, item, 'current')
|
||||
|
||||
def retract(self):
|
||||
def retract(self) -> None:
|
||||
if not self._con.get_module('PEP').supported:
|
||||
return
|
||||
self.send(None)
|
||||
|
@ -199,27 +249,9 @@ class AbstractPEPModule:
|
|||
'', self.namespace, 'current')
|
||||
|
||||
|
||||
class AbstractPEPData:
|
||||
|
||||
type_ = PEPEventType
|
||||
|
||||
def asMarkupText(self):
|
||||
'''SHOULD be implemented by subclasses'''
|
||||
return ''
|
||||
|
||||
def __eq__(self, other):
|
||||
return other == self.type_
|
||||
|
||||
def __bool__(self):
|
||||
return self.data is not None
|
||||
|
||||
def __str__(self):
|
||||
return str(self.data)
|
||||
|
||||
|
||||
class PEPReceivedEvent(NetworkIncomingEvent):
|
||||
name = 'pep-received'
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
def get_instance(*args: Any, **kwargs: Any) -> Tuple[PEP, str]:
|
||||
return PEP(*args, **kwargs), 'PEP'
|
||||
|
|
|
@ -14,6 +14,9 @@
|
|||
|
||||
# XEP-0199: XMPP Ping
|
||||
|
||||
from typing import Any
|
||||
from typing import Tuple
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
|
@ -21,12 +24,14 @@ import nbxmpp
|
|||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import NetworkIncomingEvent
|
||||
from gajim.common.types import ConnectionT
|
||||
from gajim.common.types import ContactT
|
||||
|
||||
log = logging.getLogger('gajim.c.m.ping')
|
||||
|
||||
|
||||
class Ping:
|
||||
def __init__(self, con):
|
||||
def __init__(self, con: ConnectionT) -> None:
|
||||
self._con = con
|
||||
self._account = con.name
|
||||
self._alarm_time = None
|
||||
|
@ -68,7 +73,7 @@ class Ping:
|
|||
log.warning('No reply received for keepalive ping. Reconnecting...')
|
||||
self._con.disconnectedReconnCB()
|
||||
|
||||
def send_ping(self, contact):
|
||||
def send_ping(self, contact: ContactT) -> None:
|
||||
if not app.account_is_connected(self._account):
|
||||
return
|
||||
|
||||
|
@ -84,7 +89,11 @@ class Ping:
|
|||
app.nec.push_incoming_event(
|
||||
PingSentEvent(None, conn=self._con, contact=contact))
|
||||
|
||||
def _pong_received(self, _con, stanza, ping_time, contact):
|
||||
def _pong_received(self,
|
||||
_con: ConnectionT,
|
||||
stanza: nbxmpp.Iq,
|
||||
ping_time: int,
|
||||
contact: ContactT) -> None:
|
||||
if not nbxmpp.isResultNode(stanza):
|
||||
log.info('Error: %s', stanza.getError())
|
||||
app.nec.push_incoming_event(
|
||||
|
@ -98,7 +107,9 @@ class Ping:
|
|||
contact=contact,
|
||||
seconds=diff))
|
||||
|
||||
def _answer_request(self, _con, stanza):
|
||||
def _answer_request(self,
|
||||
_con: ConnectionT,
|
||||
stanza: nbxmpp.Iq) -> None:
|
||||
iq = stanza.buildReply('result')
|
||||
ping = iq.getTag('ping')
|
||||
if ping is not None:
|
||||
|
@ -120,5 +131,5 @@ class PingErrorEvent(NetworkIncomingEvent):
|
|||
name = 'ping-error'
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
def get_instance(*args: Any, **kwargs: Any) -> Tuple[Ping, str]:
|
||||
return Ping(*args, **kwargs), 'Ping'
|
||||
|
|
|
@ -33,7 +33,7 @@ class UserActivityData(AbstractPEPData):
|
|||
def __init__(self, activity):
|
||||
self.data = activity
|
||||
|
||||
def asMarkupText(self):
|
||||
def as_markup_text(self):
|
||||
pep = self.data
|
||||
activity = pep['activity']
|
||||
subactivity = pep['subactivity'] if 'subactivity' in pep else None
|
||||
|
|
|
@ -35,7 +35,7 @@ class UserLocationData(AbstractPEPData):
|
|||
self._pep_specific_data = location
|
||||
self.data = location
|
||||
|
||||
def asMarkupText(self):
|
||||
def as_markup_text(self):
|
||||
location = self.data
|
||||
location_string = ''
|
||||
|
||||
|
|
|
@ -14,6 +14,12 @@
|
|||
|
||||
# XEP-0107: User Mood
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List # pylint: disable=unused-import
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
|
||||
import logging
|
||||
|
||||
import nbxmpp
|
||||
|
@ -22,6 +28,7 @@ from gi.repository import GLib
|
|||
from gajim.common.const import PEPEventType, MOODS
|
||||
from gajim.common.exceptions import StanzaMalformed
|
||||
from gajim.common.modules.pep import AbstractPEPModule, AbstractPEPData
|
||||
from gajim.common.types import ConnectionT
|
||||
|
||||
log = logging.getLogger('gajim.c.m.user_mood')
|
||||
|
||||
|
@ -30,10 +37,12 @@ class UserMoodData(AbstractPEPData):
|
|||
|
||||
type_ = PEPEventType.MOOD
|
||||
|
||||
def __init__(self, mood):
|
||||
def __init__(self, mood: Optional[Dict[str, str]]) -> None:
|
||||
self.data = mood
|
||||
|
||||
def asMarkupText(self):
|
||||
def as_markup_text(self) -> str:
|
||||
if self.data is None:
|
||||
return ''
|
||||
mood = self._translate_mood(self.data['mood'])
|
||||
markuptext = '<b>%s</b>' % GLib.markup_escape_text(mood)
|
||||
if 'text' in self.data:
|
||||
|
@ -42,7 +51,7 @@ class UserMoodData(AbstractPEPData):
|
|||
return markuptext
|
||||
|
||||
@staticmethod
|
||||
def _translate_mood(mood):
|
||||
def _translate_mood(mood: str) -> str:
|
||||
if mood in MOODS:
|
||||
return MOODS[mood]
|
||||
return mood
|
||||
|
@ -56,12 +65,12 @@ class UserMood(AbstractPEPModule):
|
|||
store_publish = True
|
||||
_log = log
|
||||
|
||||
def __init__(self, con):
|
||||
def __init__(self, con: ConnectionT) -> None:
|
||||
AbstractPEPModule.__init__(self, con, con.name)
|
||||
|
||||
self.handlers = []
|
||||
self.handlers = [] # type: List[Tuple[Any, ...]]
|
||||
|
||||
def _extract_info(self, item):
|
||||
def _extract_info(self, item: nbxmpp.Node) -> Optional[Dict[str, str]]:
|
||||
mood_dict = {}
|
||||
mood_tag = item.getTag('mood', namespace=nbxmpp.NS_MOOD)
|
||||
if mood_tag is None:
|
||||
|
@ -76,7 +85,7 @@ class UserMood(AbstractPEPModule):
|
|||
|
||||
return mood_dict or None
|
||||
|
||||
def _build_node(self, data):
|
||||
def _build_node(self, data: Optional[Tuple[str, str]]) -> nbxmpp.Node:
|
||||
item = nbxmpp.Node('mood', {'xmlns': nbxmpp.NS_MOOD})
|
||||
if data is None:
|
||||
return
|
||||
|
@ -88,5 +97,5 @@ class UserMood(AbstractPEPModule):
|
|||
return item
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
def get_instance(*args: Any, **kwargs: Any) -> Tuple[UserMood, str]:
|
||||
return UserMood(*args, **kwargs), 'UserMood'
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
|
||||
# XEP-0172: User Nickname
|
||||
|
||||
from typing import Any
|
||||
from typing import List # pylint: disable=unused-import
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
|
||||
import logging
|
||||
|
||||
import nbxmpp
|
||||
|
@ -22,6 +27,7 @@ from gajim.common import app
|
|||
from gajim.common.const import PEPEventType
|
||||
from gajim.common.exceptions import StanzaMalformed
|
||||
from gajim.common.modules.pep import AbstractPEPModule, AbstractPEPData
|
||||
from gajim.common.types import ConnectionT
|
||||
|
||||
log = logging.getLogger('gajim.c.m.user_nickname')
|
||||
|
||||
|
@ -30,10 +36,10 @@ class UserNicknameData(AbstractPEPData):
|
|||
|
||||
type_ = PEPEventType.NICKNAME
|
||||
|
||||
def __init__(self, nickname):
|
||||
def __init__(self, nickname: Optional[str]) -> None:
|
||||
self.data = nickname
|
||||
|
||||
def get_nick(self):
|
||||
def get_nick(self) -> str:
|
||||
return self.data or ''
|
||||
|
||||
|
||||
|
@ -45,12 +51,12 @@ class UserNickname(AbstractPEPModule):
|
|||
store_publish = True
|
||||
_log = log
|
||||
|
||||
def __init__(self, con):
|
||||
def __init__(self, con: ConnectionT) -> None:
|
||||
AbstractPEPModule.__init__(self, con, con.name)
|
||||
|
||||
self.handlers = []
|
||||
self.handlers = [] # type: List[Tuple[Any, ...]]
|
||||
|
||||
def _extract_info(self, item):
|
||||
def _extract_info(self, item: nbxmpp.Node) -> Optional[str]:
|
||||
nick = ''
|
||||
child = item.getTag('nick', namespace=nbxmpp.NS_NICK)
|
||||
if child is None:
|
||||
|
@ -59,14 +65,16 @@ class UserNickname(AbstractPEPModule):
|
|||
|
||||
return nick or None
|
||||
|
||||
def _build_node(self, data):
|
||||
def _build_node(self, data: Optional[str]) -> Optional[nbxmpp.Node]:
|
||||
item = nbxmpp.Node('nick', {'xmlns': nbxmpp.NS_NICK})
|
||||
if data is None:
|
||||
return
|
||||
return None
|
||||
item.addData(data)
|
||||
return item
|
||||
|
||||
def _notification_received(self, jid, user_pep):
|
||||
def _notification_received(self,
|
||||
jid: nbxmpp.JID,
|
||||
user_pep: UserNicknameData) -> None:
|
||||
for contact in app.contacts.get_contacts(self._account, str(jid)):
|
||||
contact.contact_name = user_pep.get_nick()
|
||||
|
||||
|
@ -78,12 +86,12 @@ class UserNickname(AbstractPEPModule):
|
|||
'accounts', self._account, 'name')
|
||||
|
||||
|
||||
def parse_nickname(stanza):
|
||||
def parse_nickname(stanza: nbxmpp.Node) -> str:
|
||||
nick = stanza.getTag('nick', namespace=nbxmpp.NS_NICK)
|
||||
if nick is None:
|
||||
return ''
|
||||
return nick.getData()
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
def get_instance(*args: Any, **kwargs: Any) -> Tuple[UserNickname, str]:
|
||||
return UserNickname(*args, **kwargs), 'UserNickname'
|
||||
|
|
|
@ -14,14 +14,23 @@
|
|||
|
||||
# XEP-0118: User Tune
|
||||
|
||||
from typing import Any
|
||||
from typing import List # pylint: disable=unused-import
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
|
||||
import logging
|
||||
|
||||
import nbxmpp
|
||||
from gi.repository import GLib
|
||||
|
||||
from gajim.common.i18n import _
|
||||
from gajim.common.const import PEPEventType
|
||||
from gajim.common.exceptions import StanzaMalformed
|
||||
from gajim.common.modules.pep import AbstractPEPModule, AbstractPEPData
|
||||
from gajim.common.types import ConnectionT
|
||||
from gajim.common.types import UserTuneDataT
|
||||
|
||||
log = logging.getLogger('gajim.c.m.user_tune')
|
||||
|
||||
|
@ -30,10 +39,13 @@ class UserTuneData(AbstractPEPData):
|
|||
|
||||
type_ = PEPEventType.TUNE
|
||||
|
||||
def __init__(self, tune):
|
||||
def __init__(self, tune: Optional[Dict[str, str]]) -> None:
|
||||
self.data = tune
|
||||
|
||||
def asMarkupText(self):
|
||||
def as_markup_text(self) -> str:
|
||||
if self.data is None:
|
||||
return ''
|
||||
|
||||
tune = self.data
|
||||
|
||||
artist = tune.get('artist', _('Unknown Artist'))
|
||||
|
@ -60,12 +72,12 @@ class UserTune(AbstractPEPModule):
|
|||
store_publish = True
|
||||
_log = log
|
||||
|
||||
def __init__(self, con):
|
||||
def __init__(self, con: ConnectionT) -> None:
|
||||
AbstractPEPModule.__init__(self, con, con.name)
|
||||
|
||||
self.handlers = []
|
||||
self.handlers = [] # type: List[Tuple[Any, ...]]
|
||||
|
||||
def _extract_info(self, item):
|
||||
def _extract_info(self, item: nbxmpp.Node) -> Optional[Dict[str, str]]:
|
||||
tune_dict = {}
|
||||
tune_tag = item.getTag('tune', namespace=self.namespace)
|
||||
if tune_tag is None:
|
||||
|
@ -80,7 +92,7 @@ class UserTune(AbstractPEPModule):
|
|||
|
||||
return tune_dict or None
|
||||
|
||||
def _build_node(self, data):
|
||||
def _build_node(self, data: UserTuneDataT) -> nbxmpp.Node:
|
||||
item = nbxmpp.Node('tune', {'xmlns': nbxmpp.NS_TUNE})
|
||||
if data is None:
|
||||
return item
|
||||
|
@ -98,5 +110,5 @@ class UserTune(AbstractPEPModule):
|
|||
return item
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
def get_instance(*args: Any, **kwargs: Any) -> Tuple[UserTune, str]:
|
||||
return UserTune(*args, **kwargs), 'UserTune'
|
||||
|
|
|
@ -22,11 +22,12 @@ Network Events Controller.
|
|||
:license: GPL
|
||||
'''
|
||||
|
||||
#from plugins.helpers import log
|
||||
from typing import List # pylint: disable=unused-import
|
||||
|
||||
from gajim.common import app
|
||||
|
||||
class NetworkEventsController(object):
|
||||
|
||||
class NetworkEventsController:
|
||||
def __init__(self):
|
||||
self.incoming_events_generators = {}
|
||||
'''
|
||||
|
@ -45,7 +46,7 @@ class NetworkEventsController(object):
|
|||
for base_event_name in event_class.base_network_events:
|
||||
event_list = self.incoming_events_generators.setdefault(
|
||||
base_event_name, [])
|
||||
if not event_class in event_list:
|
||||
if event_class not in event_list:
|
||||
event_list.append(event_class)
|
||||
|
||||
def unregister_incoming_event(self, event_class):
|
||||
|
@ -58,7 +59,7 @@ class NetworkEventsController(object):
|
|||
for base_event_name in event_class.base_network_events:
|
||||
event_list = self.outgoing_events_generators.setdefault(
|
||||
base_event_name, [])
|
||||
if not event_class in event_list:
|
||||
if event_class not in event_list:
|
||||
event_list.append(event_class)
|
||||
|
||||
def unregister_outgoing_event(self, event_class):
|
||||
|
@ -89,12 +90,12 @@ class NetworkEventsController(object):
|
|||
base_event_name = event_object.name
|
||||
if base_event_name in self.incoming_events_generators:
|
||||
for new_event_class in self.incoming_events_generators[
|
||||
base_event_name]:
|
||||
new_event_object = new_event_class(None,
|
||||
base_event=event_object)
|
||||
base_event_name]:
|
||||
new_event_object = new_event_class(
|
||||
None, base_event=event_object)
|
||||
if new_event_object.generate():
|
||||
if not app.ged.raise_event(new_event_object.name,
|
||||
new_event_object):
|
||||
new_event_object):
|
||||
self._generate_events_based_on_incoming_event(
|
||||
new_event_object)
|
||||
|
||||
|
@ -110,16 +111,17 @@ class NetworkEventsController(object):
|
|||
base_event_name = event_object.name
|
||||
if base_event_name in self.outgoing_events_generators:
|
||||
for new_event_class in self.outgoing_events_generators[
|
||||
base_event_name]:
|
||||
new_event_object = new_event_class(None,
|
||||
base_event=event_object)
|
||||
base_event_name]:
|
||||
new_event_object = new_event_class(
|
||||
None, base_event=event_object)
|
||||
if new_event_object.generate():
|
||||
if not app.ged.raise_event(new_event_object.name,
|
||||
new_event_object):
|
||||
new_event_object):
|
||||
self._generate_events_based_on_outgoing_event(
|
||||
new_event_object)
|
||||
|
||||
class NetworkEvent(object):
|
||||
|
||||
class NetworkEvent:
|
||||
name = ''
|
||||
|
||||
def __init__(self, new_name, **kwargs):
|
||||
|
@ -133,14 +135,14 @@ class NetworkEvent(object):
|
|||
def init(self):
|
||||
pass
|
||||
|
||||
|
||||
def generate(self):
|
||||
'''
|
||||
Generates new event (sets it's attributes) based on event object.
|
||||
|
||||
Base event object name is one of those in `base_network_events`.
|
||||
|
||||
Reference to base event object is stored in `self.base_event` attribute.
|
||||
Reference to base event object is stored in `self.base_event`
|
||||
attribute.
|
||||
|
||||
Note that this is a reference, so modifications to that event object
|
||||
are possible before dispatching to Global Events Dispatcher.
|
||||
|
@ -159,15 +161,16 @@ class NetworkEvent(object):
|
|||
if k not in ('name', 'base_network_events'):
|
||||
setattr(self, k, v)
|
||||
|
||||
|
||||
class NetworkIncomingEvent(NetworkEvent):
|
||||
base_network_events = []
|
||||
base_network_events = [] # type: List[str]
|
||||
'''
|
||||
Names of base network events that new event is going to be generated on.
|
||||
'''
|
||||
|
||||
|
||||
class NetworkOutgoingEvent(NetworkEvent):
|
||||
base_network_events = []
|
||||
base_network_events = [] # type: List[str]
|
||||
'''
|
||||
Names of base network events that new event is going to be generated on.
|
||||
'''
|
||||
|
|
|
@ -67,7 +67,7 @@ class AbstractPEP(object):
|
|||
else:
|
||||
acc.pep[self.type_] = self
|
||||
|
||||
def asMarkupText(self):
|
||||
def as_markup_text(self):
|
||||
'''SHOULD be implemented by subclasses'''
|
||||
return ''
|
||||
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
# 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/>.
|
||||
|
||||
# Types for typechecking
|
||||
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import nbxmpp
|
||||
|
||||
from gajim.common.const import PathType, PathLocation
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# pylint: disable=unused-import
|
||||
from gajim.common.connection import Connection
|
||||
from gajim.common.zeroconf.connection_zeroconf import ConnectionZeroconf
|
||||
from gajim.common.contacts import Contact
|
||||
from gajim.common.contacts import GC_Contact
|
||||
|
||||
ConnectionT = Union['Connection', 'ConnectionZeroconf']
|
||||
ContactT = Union['Contact', 'GC_Contact']
|
||||
|
||||
UserTuneDataT = Optional[Tuple[str, str, str, str, str]]
|
||||
|
||||
# PEP
|
||||
PEPNotifyCallback = Callable[[nbxmpp.JID, nbxmpp.Node], None]
|
||||
PEPRetractCallback = Callable[[nbxmpp.JID, str], None]
|
||||
PEPHandlersDict = Dict[str, List[Tuple[PEPNotifyCallback, PEPRetractCallback]]]
|
||||
|
||||
# Configpaths
|
||||
PathTuple = Tuple[Optional[PathLocation], str, Optional[PathType]]
|
|
@ -20,6 +20,7 @@ from collections import namedtuple
|
|||
from gi.repository import GLib
|
||||
|
||||
from gajim.common.app import app
|
||||
from gajim.common.i18n import _
|
||||
from gajim.gtk import ErrorDialog
|
||||
from gajim.gtk import InformationDialog
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ from gajim import dataforms_widget
|
|||
|
||||
from random import randrange
|
||||
from gajim.common import ged
|
||||
from gajim.common.i18n import _
|
||||
from gajim.common.const import ACTIVITIES
|
||||
from gajim.common.const import MOODS
|
||||
|
||||
|
|
|
@ -42,11 +42,12 @@
|
|||
|
||||
import types
|
||||
import weakref
|
||||
|
||||
import nbxmpp
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import GdkPixbuf
|
||||
from gi.repository import Pango
|
||||
|
||||
from gajim.gtk import ErrorDialog
|
||||
from gajim.gtk import InformationDialog
|
||||
|
@ -54,13 +55,11 @@ from gajim import gtkgui_helpers
|
|||
from gajim import groups
|
||||
from gajim import adhoc_commands
|
||||
from gajim import search_window
|
||||
from gajim import gui_menu_builder
|
||||
from gajim.gtk import ServiceRegistration
|
||||
|
||||
from gajim.common import app
|
||||
import nbxmpp
|
||||
from gajim.common.i18n import _
|
||||
from gajim.common import helpers
|
||||
from gajim.common import ged
|
||||
from gajim.common.const import StyleAttr
|
||||
|
||||
LABELS = {
|
||||
|
|
|
@ -28,13 +28,13 @@ import os
|
|||
import sys
|
||||
import locale
|
||||
import signal
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL) # ^C exits the application
|
||||
|
||||
|
||||
from gajim.common import exceptions
|
||||
from gajim.common import i18n # This installs _() function
|
||||
from gajim.common.i18n import _
|
||||
from gajim.common.i18n import Q_
|
||||
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL) # ^C exits the application
|
||||
|
||||
try:
|
||||
PREFERRED_ENCODING = locale.getpreferredencoding()
|
||||
except Exception:
|
||||
|
|
|
@ -18,6 +18,7 @@ from gi.repository import Gtk
|
|||
from gajim.common import app
|
||||
from gajim.common import ged
|
||||
from gajim.common import helpers
|
||||
from gajim.common.i18n import _
|
||||
from gajim.gtk import ErrorDialog
|
||||
from gajim.gtk.util import get_builder
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ from gi.repository import GdkPixbuf
|
|||
from gi.repository import GObject
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.i18n import _
|
||||
|
||||
Filter = namedtuple('Filter', 'name pattern default')
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ from gi.repository import Gtk
|
|||
|
||||
from gajim.common import app
|
||||
from gajim.common.modules import dataforms
|
||||
from gajim.common.i18n import _
|
||||
from gajim.gtk.dataform import DataFormWidget
|
||||
|
||||
log = logging.getLogger('gajim.gtk.registration')
|
||||
|
|
|
@ -21,6 +21,7 @@ from gi.repository import Gtk
|
|||
from gi.repository import Gdk
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.i18n import _
|
||||
from gajim.common.const import StyleAttr, DialogButton, ButtonAction
|
||||
from gajim.common.connection_handlers_events import StyleChanged
|
||||
from gajim.gtk import ErrorDialog
|
||||
|
|
|
@ -2127,7 +2127,7 @@ class Interface:
|
|||
if app.connections[acct].music_track_info == music_track_info:
|
||||
continue
|
||||
app.connections[acct].get_module('UserTune').send(
|
||||
(artist, title, source, None, None))
|
||||
(artist, title, source, '', ''))
|
||||
app.connections[acct].music_track_info = music_track_info
|
||||
|
||||
def read_sleepy(self):
|
||||
|
|
|
@ -79,6 +79,7 @@ if is_standalone():
|
|||
configpaths.init()
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.i18n import _
|
||||
from gajim.common.const import JIDConstant, KindConstant
|
||||
from gajim.common import helpers
|
||||
from gajim.gtk import YesNoDialog
|
||||
|
|
|
@ -24,6 +24,7 @@ from gi.repository import GLib
|
|||
from gi.repository import Pango
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.i18n import _
|
||||
from gajim import gtkgui_helpers
|
||||
|
||||
if app.is_installed('GSPELL'):
|
||||
|
|
|
@ -14,15 +14,18 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from typing import cast
|
||||
|
||||
import os
|
||||
import locale
|
||||
import gettext
|
||||
from os import path as os_path
|
||||
import os
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common import configpaths
|
||||
|
||||
APP = 'gajim_plugins'
|
||||
plugins_locale_dir = os_path.join(configpaths.get('PLUGINS_USER'), 'locale')
|
||||
plugin_user_dir = cast(str, configpaths.get('PLUGINS_USER'))
|
||||
plugins_locale_dir = os.path.join(plugin_user_dir, 'locale')
|
||||
|
||||
if os.name != 'nt':
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
|
|
|
@ -528,25 +528,25 @@ class RosterTooltip(Gtk.Window, StatusTable):
|
|||
to the given property list.
|
||||
"""
|
||||
if 'mood' in contact.pep:
|
||||
mood = contact.pep['mood'].asMarkupText()
|
||||
mood = contact.pep['mood'].as_markup_text()
|
||||
self.mood.set_markup(mood)
|
||||
self.mood.show()
|
||||
self.mood_label.show()
|
||||
|
||||
if 'activity' in contact.pep:
|
||||
activity = contact.pep['activity'].asMarkupText()
|
||||
activity = contact.pep['activity'].as_markup_text()
|
||||
self.activity.set_markup(activity)
|
||||
self.activity.show()
|
||||
self.activity_label.show()
|
||||
|
||||
if 'tune' in contact.pep:
|
||||
tune = contact.pep['tune'].asMarkupText()
|
||||
tune = contact.pep['tune'].as_markup_text()
|
||||
self.tune.set_markup(tune)
|
||||
self.tune.show()
|
||||
self.tune_label.show()
|
||||
|
||||
if 'geoloc' in contact.pep:
|
||||
location = contact.pep['geoloc'].asMarkupText()
|
||||
location = contact.pep['geoloc'].as_markup_text()
|
||||
self.location.set_markup(location)
|
||||
self.location.show()
|
||||
self.location_label.show()
|
||||
|
|
Loading…
Reference in New Issue