Commit 9885203c authored by mathieui's avatar mathieui

Update the plugins to use the PluginAPI

Also:
- Add get_conversation_messages() to PluginAPI
- Make plugins_autoload colon-separated instead of space-separated
    (for consistency)
- Replace a JID() with a safeJID() in the uptime plugin
parent dbde08a5
......@@ -222,7 +222,7 @@ plugins_dir =
# $XDG_CONFIG_HOME/poezio/plugins. You can specify another directory here.
plugins_conf_dir =
# Space separated list of plugins to load on startup
# Colon-separated list of plugins to load on startup
plugins_autoload =
# the full path to the photo (avatar) you want to use
......
......@@ -253,7 +253,7 @@ section of this documentation.
*plugins_autoload*:: [empty]
Space separated list of plugins to load on startup.
Colon-separated list of plugins to load on startup.
*plugins_conf_dir*:: [empty]
......
......@@ -14,49 +14,49 @@ class Plugin(BasePlugin):
"""
def init(self):
for role in ('visitor', 'participant' , 'moderator'):
self.add_tab_command(MucTab, role, self.role(role),
self.api.add_tab_command(MucTab, role, self.role(role),
help='Set the role of a nick to %s' % role,
usage= '<nick>',
short='Set the role to %s' % role,
completion=self.complete_nick)
for aff in ('member', 'owner', 'admin'):
self.add_tab_command(MucTab, aff, self.affiliation(aff),
self.api.add_tab_command(MucTab, aff, self.affiliation(aff),
usage='<nick>',
help='Set the affiliation of a nick to %s' % aff,
short='Set the affiliation to %s' % aff,
completion=self.complete_nick)
self.add_tab_command(MucTab, 'noaffiliation', self.affiliation('none'),
self.api.add_tab_command(MucTab, 'noaffiliation', self.affiliation('none'),
usage='<nick>',
help='Set the affiliation of a nick to none.',
short='Set the affiliation to none.',
completion=self.complete_nick)
self.add_tab_command(MucTab, 'voice', self.affiliation('member'),
self.api.add_tab_command(MucTab, 'voice', self.affiliation('member'),
usage='<nick>',
help='Set the affiliation of a nick to member.',
short='Set the affiliation to member.',
completion=self.complete_nick)
self.add_tab_command(MucTab, 'op', self.role('moderator'),
self.api.add_tab_command(MucTab, 'op', self.role('moderator'),
usage='<nick>',
help='Set the role of a nick to moderator.',
short='Set the role to moderator.',
completion=self.complete_nick)
self.add_tab_command(MucTab, 'mute', self.role('visitor'),
self.api.add_tab_command(MucTab, 'mute', self.role('visitor'),
usage='<nick>',
help='Set the role of a nick to visitor.',
short='Set the role to visitor.',
completion=self.complete_nick)
def role(self, role):
return lambda args: self.core.current_tab().command_role(args+' '+role)
return lambda args: self.api.current_tab().command_role(args+' '+role)
def affiliation(self, affiliation):
return lambda args: self.core.current_tab().command_affiliation(
return lambda args: self.api.current_tab().command_affiliation(
args+' '+affiliation)
def complete_nick(self, the_input):
tab = self.core.current_tab()
tab = self.api.current_tab()
compare_users = lambda x: x.last_talked
word_list = [user.nick for user in sorted(tab.users, key=compare_users, reverse=True)\
if user.nick != tab.own_nick]
......
......@@ -10,11 +10,11 @@ from common import shell_split
class Plugin(BasePlugin):
def init(self):
self.add_command('alias', self.command_alias,
self.api.add_command('alias', self.command_alias,
usage='<alias> <command> [args]',
short='Create an alias command',
help='Create an alias for <command> with [args].')
self.add_command('unalias', self.command_unalias,
self.api.add_command('unalias', self.command_unalias,
usage='<alias>',
help='Remove a previously created alias',
short='Remove an alias',
......@@ -36,8 +36,9 @@ class Plugin(BasePlugin):
if alias in self.core.commands or alias in self.commands:
self.api.information('Alias: command already exists', 'Error')
return
self.commands[alias] = lambda arg: self.get_command(command)(tmp_args.format(*shell_split(arg)))
self.add_command(alias, self.commands[alias], 'This command is an alias for /%s %s' %( command, tmp_args))
self.commands[alias] = lambda arg: self.get_command(command)(tmp_args + arg)
self.api.add_command(alias, self.commands[alias], 'This command is an alias for /%s %s' %( command, tmp_args))
self.api.information('Alias /%s successfuly created' % alias, 'Info')
def command_unalias(self, alias):
......@@ -46,7 +47,7 @@ class Plugin(BasePlugin):
"""
if alias in self.commands:
del self.commands[alias]
self.del_command(alias)
self.api.del_command(alias)
self.api.information('Alias /%s successfuly deleted' % alias, 'Info')
def completion_unalias(self, the_input):
......
......@@ -5,7 +5,7 @@ from tabs import MucTab
class Plugin(BasePlugin):
def init(self):
self.add_command('amsg', self.command_amsg,
self.api.add_command('amsg', self.command_amsg,
usage='<message>',
short='Broadcast a message',
help='Broadcast the message to all the joined rooms.')
......
......@@ -9,13 +9,13 @@ class Plugin(BasePlugin):
self.schedule_event()
def cleanup(self):
self.core.remove_timed_event(self.next_event)
self.api.remove_timed_event(self.next_event)
def schedule_event(self):
day_change = datetime.datetime.combine(datetime.date.today(), datetime.time())
day_change += datetime.timedelta(1)
self.next_event = timed_events.TimedEvent(day_change, self.day_change)
self.core.add_timed_event(self.next_event)
self.api.add_timed_event(self.next_event)
def day_change(self):
msg = datetime.date.today().strftime(_("Day changed to %x"))
......
......@@ -8,14 +8,14 @@ import tabs
class Plugin(BasePlugin):
def init(self):
for tab_type in (tabs.MucTab, tabs.PrivateTab, tabs.ConversationTab):
self.add_tab_command(tab_type, 'display_corrections',
self.api.add_tab_command(tab_type, 'display_corrections',
handler=self.command_display_corrections,
usage='<number>',
help='Display all the corrections of the number-th last corrected message.',
short='Display the corrections of a message')
def find_corrected(self, nb):
messages = self.core.get_conversation_messages()
messages = self.api.get_conversation_messages()
if not messages:
return None
for message in messages[::-1]:
......@@ -32,7 +32,7 @@ class Plugin(BasePlugin):
try:
nb = int(args[0])
except:
return self.core.command_help('display_corrections')
return self.api.run_command('/help display_corrections')
else:
nb = 1
message = self.find_corrected(nb)
......@@ -41,9 +41,9 @@ class Plugin(BasePlugin):
while message:
display.append('%s %s%s%s %s' % (message.str_time, '* ' if message.me else '', message.nickname, '' if message.me else '>', message.txt))
message = message.old_message
self.core.information('Older versions:\n' + '\n'.join(display[::-1]), 'Info')
self.api.information('Older versions:\n' + '\n'.join(display[::-1]), 'Info')
else:
self.core.information('No corrected message found.', 'Warning')
self.api.information('No corrected message found.', 'Warning')
def cleanup(self):
del self.config
......@@ -2,7 +2,7 @@ from plugin import BasePlugin
class Plugin(BasePlugin):
def init(self):
self.add_event_handler('muc_say', self.double)
self.api.add_event_handler('muc_say', self.double)
def double(self, msg, tab):
split = msg['body'].split()
......
......@@ -8,7 +8,7 @@ import subprocess
class Plugin(BasePlugin):
def init(self):
self.add_command('exec', self.command_exec,
self.api.add_command('exec', self.command_exec,
usage='[-o|-O] <command>',
help='Execute a shell command and prints the result in the information buffer. The command should be ONE argument, that means it should be between \"\". The first argument (before the command) can be -o or -O. If -o is specified, it sends the result in the current conversation. If -O is specified, it sends the command and its result in the current conversation.\nExample: /exec -O \"uptime\" will send “uptime\n20:36:19 up 3:47, 4 users, load average: 0.09, 0.13, 0.09” in the current conversation.',
short='Execute a command')
......@@ -22,20 +22,20 @@ class Plugin(BasePlugin):
command = args[1]
arg = args[0]
else:
self.core.command_help('exec')
self.api.run_command('/help exec')
return
try:
process = subprocess.Popen(['sh', '-c', command], stdout=subprocess.PIPE)
except OSError as e:
self.core.information('Failed to execute command: %s' % (e,), 'Error')
self.api.information('Failed to execute command: %s' % (e,), 'Error')
return
result = process.communicate()[0].decode('utf-8')
if arg and arg == '-o':
if not self.core.send_message('%s' % (result,)):
self.core.information('Cannot send result (%s), this is not a conversation tab' % result)
if not self.api.send_message('%s' % (result,)):
self.api.information('Cannot send result (%s), this is not a conversation tab' % result)
elif arg and arg == '-O':
if not self.core.send_message('%s:\n%s' % (command, result)):
self.core.information('Cannot send result (%s), this is not a conversation tab' % result)
if not self.api.send_message('%s:\n%s' % (command, result)):
self.api.information('Cannot send result (%s), this is not a conversation tab' % result)
else:
self.core.information('%s:\n%s' % (command, result), 'Info')
self.api.information('%s:\n%s' % (command, result), 'Info')
return
......@@ -3,9 +3,9 @@ import subprocess
class Plugin(BasePlugin):
def init(self):
self.add_event_handler('muc_say', self.figletize)
self.add_event_handler('conversation_say', self.figletize)
self.add_event_handler('private_say', self.figletize)
self.api.add_event_handler('muc_say', self.figletize)
self.api.add_event_handler('conversation_say', self.figletize)
self.api.add_event_handler('private_say', self.figletize)
def figletize(self, msg, tab):
process = subprocess.Popen(['figlet', '--', msg['body']], stdout=subprocess.PIPE)
......
......@@ -7,7 +7,7 @@ class Plugin(BasePlugin):
self.core.xmpp.register_handler(Callback('Iq_show', StanzaPath('iq'), self.handle_iq))
def handle_iq(self, iq):
self.core.information('%s' % iq, 'Iq')
self.api.information('%s' % iq, 'Iq')
def cleanup(self):
self.core.xmpp.remove_handler('Iq_show')
......@@ -13,13 +13,13 @@ url_pattern = re.compile(r'\b(http[s]?://(?:\S+))\b', re.I|re.U)
class Plugin(BasePlugin):
def init(self):
for _class in (tabs.MucTab, tabs.PrivateTab, tabs.ConversationTab):
self.add_tab_command(_class, 'link', self.command_link,
self.api.add_tab_command(_class, 'link', self.command_link,
usage='[num]',
help='Opens the last link from the conversation into a browser.\nIf [num] is given, then it will open the num-th link displayed.',
short='Open links into a browser')
def find_link(self, nb):
messages = self.core.get_conversation_messages()
messages = self.api.get_conversation_messages()
if not messages:
return None
for message in messages[::-1]:
......@@ -38,14 +38,14 @@ class Plugin(BasePlugin):
try:
nb = int(args[0])
except:
return self.core.command_help('link')
return self.api.run_command('/help link')
else:
nb = 1
link = self.find_link(nb)
if link:
self.core.exec_command([self.config.get('browser', 'firefox'), link])
else:
self.core.information('No URL found.', 'Warning')
self.api.information('No URL found.', 'Warning')
def cleanup(self):
del self.config
......@@ -9,7 +9,7 @@ import mpd
class Plugin(BasePlugin):
def init(self):
for _class in (tabs.ConversationTab, tabs.MucTab, tabs.PrivateTab):
self.add_tab_command(_class, 'mpd', self.command_mpd,
self.api.add_tab_command(_class, 'mpd', self.command_mpd,
usage='[full]',
help='Sends a message showing the current song of an MPD instance. If full is provided, the message is more verbose.',
short='Send the MPD status',
......@@ -34,8 +34,8 @@ class Plugin(BasePlugin):
current_time = float(c.status()['elapsed'])
pourcentage = int(current_time / float(current['time']) * 10)
s += ' \x192}[\x191}' + '-'*(pourcentage-1) + '\x193}+' + '\x191}' + '-' * (10-pourcentage-1) + '\x192}]\x19o'
if not self.core.send_message('%s' % (s,)):
self.core.information('Cannot send result (%s)' % s, 'Error')
if not self.api.send_message('%s' % (s,)):
self.api.information('Cannot send result (%s)' % s, 'Error')
def completion_mpd(self, the_input):
return the_input.auto_completion(['full'], quotify=False)
......@@ -13,10 +13,10 @@ class Plugin(BasePlugin):
def init(self):
self.contacts = {}
# a dict of {full-JID: OTR object}
self.add_event_handler('conversation_say_after', self.on_conversation_say)
self.add_event_handler('conversation_msg', self.on_conversation_msg)
self.api.add_event_handler('conversation_say_after', self.on_conversation_say)
self.api.add_event_handler('conversation_msg', self.on_conversation_msg)
self.add_tab_command(ConversationTab, 'otr', self.command_otr,
self.api.add_tab_command(ConversationTab, 'otr', self.command_otr,
usage='<start|end|fpr>',
help='Start or stop OTR for the current conversation.',
short='Manage OTR status',
......@@ -113,27 +113,27 @@ class Plugin(BasePlugin):
"""
args = args.split()
if not args:
return self.core.command_help("otr")
if isinstance(self.core.current_tab(), ConversationTab):
jid = JID(self.core.current_tab().get_name())
return self.api.run_command("/help otr")
if isinstance(self.api.current_tab(), ConversationTab):
jid = JID(self.api.current_tab().get_name())
command = args[0]
if command == 'start':
otr_state = self.get_otr(self.core.current_tab())
self.otr_say(self.core.current_tab(), otr_state.start().decode())
otr_state = self.get_otr(self.api.current_tab())
self.otr_say(self.api.current_tab(), otr_state.start().decode())
elif command == 'end':
otr_state = self.get_otr(self.core.current_tab())
otr_state = self.get_otr(self.api.current_tab())
msg = otr_state.end()
if msg is not None:
self.otr_say(self.core.current_tab(), msg.decode())
self.otr_say(self.api.current_tab(), msg.decode())
elif command == 'fpr':
otr_state = self.get_otr(self.core.current_tab())
otr_state = self.get_otr(self.api.current_tab())
our = otr_state.our_fpr
if our:
our = hex(int.from_bytes(our, 'big'))[2:].ljust(40).upper()
their = otr_state.their_fpr
if their:
their = hex(int.from_bytes(their, 'big'))[2:].ljust(40).upper()
self.core.current_tab().add_message('Your: %s Their: %s' % (our, their))
self.api.current_tab().add_message('Your: %s Their: %s' % (our, their))
self.core.refresh_window()
def otr_completion(self, the_input):
......
......@@ -5,13 +5,13 @@ from plugin import BasePlugin
class Plugin(BasePlugin):
def init(self):
self.add_command('pacokick', self.command_kick,
self.api.add_command('pacokick', self.command_kick,
usage='',
help='Kick a random user.',
short='Kick a random user')
def command_kick(self, arg):
tab = self.core.current_tab()
tab = self.api.current_tab()
if isinstance(tab, MucTab):
kickable = list(filter(lambda x: x.affiliation in ('none', 'member'), tab.users))
if kickable:
......@@ -20,4 +20,4 @@ class Plugin(BasePlugin):
to_kick = to_kick.nick
tab.command_kick(to_kick + ' ' +arg)
else:
self.core.information('No one to kick :(', 'Info')
self.api.information('No one to kick :(', 'Info')
......@@ -8,18 +8,18 @@ import tabs
class Plugin(BasePlugin):
def init(self):
self.core.xmpp.register_plugin('xep_0199')
self.add_command('ping', self.command_ping,
self.api.add_command('ping', self.command_ping,
usage='<jid>',
help='Send a XMPP ping to jid (see XEP-0199).',
short='Send a ping',
completion=self.completion_ping)
self.add_tab_command(tabs.MucTab, 'ping', self.command_muc_ping,
self.api.add_tab_command(tabs.MucTab, 'ping', self.command_muc_ping,
usage='<jid|nick>',
help='Send a XMPP ping to jid or nick (see XEP-0199).',
short='Send a ping.',
completion=self.completion_muc_ping)
for _class in (tabs.PrivateTab, tabs.ConversationTab):
self.add_tab_command(_class, 'ping', self.command_private_ping,
self.api.add_tab_command(_class, 'ping', self.command_private_ping,
usage='[jid]',
help='Send a XMPP ping to the current interlocutor or the given JID.',
short='Send a ping',
......@@ -34,12 +34,12 @@ class Plugin(BasePlugin):
except:
delay = None
if delay is not None:
self.core.information('%s responded to ping after %s s' % (jid, round(delay, 4)), 'Info')
self.api.information('%s responded to ping after %s s' % (jid, round(delay, 4)), 'Info')
else:
self.core.information('%s did not respond to ping' % jid, 'Info')
self.api.information('%s did not respond to ping' % jid, 'Info')
def completion_muc_ping(self, the_input):
users = [user.nick for user in self.core.current_tab().users]
users = [user.nick for user in self.api.current_tab().users]
l = [contact.bare_jid for contact in roster.get_contacts()]
users.extend(l)
return the_input.auto_completion(users, '', quotify=False)
......@@ -47,14 +47,14 @@ class Plugin(BasePlugin):
def command_private_ping(self, arg):
if arg:
return self.command_ping(arg)
self.command_ping(self.core.current_tab().get_name())
self.command_ping(self.api.current_tab().get_name())
def command_muc_ping(self, arg):
if not arg.strip():
return
user = self.core.current_tab().get_user_by_name(arg)
user = self.api.current_tab().get_user_by_name(arg)
if user:
jid = safeJID(self.core.current_tab().get_name())
jid = safeJID(self.api.current_tab().get_name())
jid.resource = user.nick
else:
jid = safeJID(arg)
......
......@@ -13,7 +13,7 @@ log = logging.getLogger(__name__)
class Plugin(BasePlugin):
def init(self):
for _class in (tabs.MucTab, tabs.ConversationTab, tabs.PrivateTab):
self.add_tab_command(_class, 'quote', self.command_quote,
self.api.add_tab_command(_class, 'quote', self.command_quote,
usage='<timestamp>',
help='Takes the message received at <timestamp> and insert it in the input, to quote it.',
short='Quote a message from a timestamp',
......@@ -24,9 +24,9 @@ class Plugin(BasePlugin):
if len(args) in (1, 2):
timestamp = args[-1]
else:
return self.core.command_help('quote')
return self.api.run_command('/help quote')
if re.match(timestamp_re, timestamp) is None:
return self.core.information('Timestamp has a wrong format.', 'Warning')
return self.api.information('Timestamp has a wrong format.', 'Warning')
message = self.find_message_with_timestamp(timestamp)
if message:
before = self.config.get('before_quote', '') % {'nick': message.nickname or '',
......@@ -37,12 +37,12 @@ class Plugin(BasePlugin):
'quote': clean_text(message.txt),
'after': after.replace('\\n', '\n').replace('[SP]', ' ')})
else:
self.core.information('No message found for timestamp %s.' % timestamp, 'Warning')
self.api.information('No message found for timestamp %s.' % timestamp, 'Warning')
def find_message_with_timestamp(self, timestamp):
# TODO: handle messages with the same
# timestamp but not the same day
messages = self.core.get_conversation_messages()
messages = self.api.get_conversation_messages()
if not messages:
return None
for message in messages[::-1]:
......@@ -57,7 +57,7 @@ class Plugin(BasePlugin):
return msg.nickname.lower().startswith(nick.lower())
def time_match(msg):
return msg.str_time.endswith(time)
messages = self.core.get_conversation_messages()
messages = self.api.get_conversation_messages()
if not messages:
return
text = the_input.get_text()
......
......@@ -12,9 +12,9 @@ def rand_color():
class Plugin(BasePlugin):
def init(self):
self.add_event_handler('muc_say', self.rainbowize)
self.add_event_handler('private_say', self.rainbowize)
self.add_event_handler('conversation_say', self.rainbowize)
self.api.add_event_handler('muc_say', self.rainbowize)
self.api.add_event_handler('private_say', self.rainbowize)
self.api.add_event_handler('conversation_say', self.rainbowize)
def rainbowize(self, msg, tab):
msg['body'] = ''.join(['%s%s' % (rand_color(),char,) for char in xhtml.clean_text(msg['body'])])
......@@ -6,17 +6,17 @@ import timed_events
class Plugin(BasePlugin):
def init(self):
self.add_command('remind', self.command_remind,
self.api.add_command('remind', self.command_remind,
usage='<seconds> <todo>',
help='Remind you of <todo> every <time> seconds.',
short='Remind you of a task',
completion=self.completion_remind)
self.add_command('done', self.command_done,
self.api.add_command('done', self.command_done,
usage='<id>',
help='Stop reminding you do the task identified by <id>.',
short='Remove a task',
completion=self.completion_done)
self.add_command('tasks', self.command_tasks,
self.api.add_command('tasks', self.command_tasks,
usage='',
help='List all the current tasks and their ids.',
short='List current tasks')
......@@ -46,8 +46,8 @@ class Plugin(BasePlugin):
self.tasks[self.count] = (time, args[1])
timed_event = timed_events.DelayedEvent(time, self.remind, self.count)
self.core.add_timed_event(timed_event)
self.core.information('Task %s added: %s every %s.' % (self.count, args[1],
self.api.add_timed_event(timed_event)
self.api.information('Task %s added: %s every %s.' % (self.count, args[1],
common.parse_secs_to_str(time)), 'Info')
self.count += 1
......@@ -71,7 +71,7 @@ class Plugin(BasePlugin):
if not id in self.tasks:
return
self.core.information('Task %s: %s [DONE]' % (id, self.tasks[id][1]), 'Info')
self.api.information('Task %s: %s [DONE]' % (id, self.tasks[id][1]), 'Info')
del self.tasks[id]
def command_tasks(self, arg, nocommand=None):
......@@ -83,16 +83,16 @@ class Plugin(BasePlugin):
s += 'Task %s: %s every %s.\n' % (key, repr(self.tasks[key][1]),
common.parse_secs_to_str(self.tasks[key][0]))
if s:
self.core.information(s, 'Info')
self.api.information(s, 'Info')
def remind(self, id=0):
if not id in self.tasks:
return
self.core.information('Task %s: %s' % (id, self.tasks[id][1]), 'Info')
self.api.information('Task %s: %s' % (id, self.tasks[id][1]), 'Info')
if self.config.get('beep', '') == 'true':
curses.beep()
timed_event = timed_events.DelayedEvent(self.tasks[id][0], self.remind, id)
self.core.add_timed_event(timed_event)
self.api.add_timed_event(timed_event)
def cleanup(self):
if self.tasks:
......
......@@ -13,9 +13,9 @@ from sleekxmpp.xmlstream.stanzabase import JID
class Plugin(BasePlugin):
def init(self):
self.patterns = {}
self.add_event_handler('conversation_say', self.replace_pattern)
self.add_event_handler('muc_say', self.replace_pattern)
self.add_event_handler('private_say', self.replace_pattern)
self.api.add_event_handler('conversation_say', self.replace_pattern)
self.api.add_event_handler('muc_say', self.replace_pattern)
self.api.add_event_handler('private_say', self.replace_pattern)
self.patterns['time'] = replace_time
self.patterns['date'] = replace_date
self.patterns['datetime'] = replace_datetime
......
......@@ -2,7 +2,7 @@ from plugin import BasePlugin
class Plugin(BasePlugin):
def init(self):
self.add_event_handler('muc_say', self.revstr)
self.api.add_event_handler('muc_say', self.revstr)
def revstr(self, msg, tab):
msg['body'] = msg['body'][::-1]
......@@ -7,7 +7,7 @@ class Plugin(BasePlugin):
def init(self):
for _class in (tabs.PrivateTab, tabs.ConversationTab, tabs.MucTab):
self.add_tab_command(_class, 'send_delayed', self.command_delayed,
self.api.add_tab_command(_class, 'send_delayed', self.command_delayed,
usage='<delay> <message>',
help='Send <message> with a delay of <delay> seconds.',
short='Send a message later',
......@@ -21,9 +21,9 @@ class Plugin(BasePlugin):
if not delay:
return
tab = self.core.current_tab()
tab = self.api.current_tab()
timed_event = timed_events.DelayedEvent(delay, self.say, (tab, args[1]))
self.core.add_timed_event(timed_event)
self.api.add_timed_event(timed_event)
def completion_delay(self, the_input):
txt = the_input.get_text()
......
......@@ -3,7 +3,7 @@ from random import shuffle
class Plugin(BasePlugin):
def init(self):
self.add_event_handler('muc_say', self.shuffle)
self.api.add_event_handler('muc_say', self.shuffle)
def shuffle(self, msg, tab):
split = msg['body'].split()
......
......@@ -5,9 +5,9 @@ import shlex
class Plugin(BasePlugin):