1# Copyright (C) 2009-2010  Alexander Cherniuk <ts33kr@gmail.com>
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
16"""
17Provides an actual implementation for the standard commands.
18"""
19
20from time import localtime
21from time import strftime
22from datetime import date
23
24from gi.repository import GLib
25
26from gajim.common import app
27from gajim.common import helpers
28from gajim.common.i18n import _
29from gajim.common.const import KindConstant
30
31from gajim.command_system.errors import CommandError
32from gajim.command_system.framework import CommandContainer
33from gajim.command_system.framework import command
34from gajim.command_system.framework import doc
35from gajim.command_system.mapping import generate_usage
36
37from gajim.command_system.implementation.hosts import ChatCommands
38from gajim.command_system.implementation.hosts import PrivateChatCommands
39from gajim.command_system.implementation.hosts import GroupChatCommands
40
41
42class StandardCommonCommands(CommandContainer):
43    """
44    This command container contains standard commands which are common
45    to all - chat, private chat, group chat.
46    """
47
48    AUTOMATIC = True
49    HOSTS = ChatCommands, PrivateChatCommands, GroupChatCommands
50
51    @command(overlap=True)
52    @doc(_("Show help on a given command or a list of available commands if "
53           "-a is given"))
54    def help(self, cmd=None, all_=False):
55        if cmd:
56            cmd = self.get_command(cmd)
57
58            documentation = _(cmd.extract_documentation())
59            usage = generate_usage(cmd)
60
61            text = []
62
63            if documentation:
64                text.append(documentation)
65            if cmd.usage:
66                text.append(usage)
67
68            return '\n\n'.join(text)
69
70        if all_:
71            for cmd_ in self.list_commands():
72                names = ', '.join(cmd_.names)
73                description = cmd_.extract_description()
74
75                self.echo("%s - %s" % (names, description))
76        else:
77            help_ = self.get_command('help')
78            self.echo(help_(self, 'help'))
79
80    @command(raw=True)
81    @doc(_("Send a message to the contact"))
82    def say(self, message):
83        self.send(message)
84
85    @command(raw=True)
86    @doc(_("Send action (in the third person) to the current chat"))
87    def me(self, action):
88        self.send("/me %s" % action)
89
90    @command('lastlog', overlap=True)
91    @doc(_("Show logged messages which mention given text"))
92    def grep(self, text, limit=None):
93        results = app.storage.archive.search_log(self.account, self.contact.jid, text)
94
95        if not results:
96            raise CommandError(_("%s: Nothing found") % text)
97
98        if limit:
99            try:
100                results = results[len(results) - int(limit):]
101            except ValueError:
102                raise CommandError(_("Limit must be an integer"))
103
104        for row in results:
105            contact = row.contact_name
106            if not contact:
107                if row.kind == KindConstant.CHAT_MSG_SENT:
108                    contact = app.nicks[self.account]
109                else:
110                    contact = self.contact.name
111
112            time_obj = localtime(row.time)
113            date_obj = date.fromtimestamp(row.time)
114            date_ = strftime('%Y-%m-%d', time_obj)
115            time_ = strftime('%H:%M:%S', time_obj)
116
117            if date_obj == date.today():
118                formatted = "[%s] %s: %s" % (time_, contact, row.message)
119            else:
120                formatted = "[%s, %s] %s: %s" % (
121                    date_, time_, contact, row.message)
122
123            self.echo(formatted)
124
125    @command(raw=True, empty=True)
126    # Do not translate online, away, chat, xa, dnd
127    @doc(_("""
128    Set the current status
129
130    Status can be given as one of the following values:
131    online, away, chat, xa, dnd.
132    """))
133    def status(self, status, message):
134        if status not in ('online', 'away', 'chat', 'xa', 'dnd'):
135            raise CommandError("Invalid status given")
136        for connection in app.connections.values():
137            if not app.settings.get_account_setting(connection.name,
138                                                    'sync_with_global_status'):
139                continue
140            if not connection.state.is_available:
141                continue
142            connection.change_status(status, message)
143
144    @command(raw=True, empty=True)
145    @doc(_("Set the current status to away"))
146    def away(self, message):
147        if not message:
148            message = _("Away")
149
150        for connection in app.connections.values():
151            if not app.settings.get_account_setting(connection.name,
152                                                    'sync_with_global_status'):
153                continue
154            if not connection.state.is_available:
155                continue
156            connection.change_status('away', message)
157
158    @command('back', raw=True, empty=True)
159    @doc(_("Set the current status to online"))
160    def online(self, message):
161        if not message:
162            message = _("Available")
163
164        for connection in app.connections.values():
165            if not app.settings.get_account_setting(connection.name,
166                                                    'sync_with_global_status'):
167                continue
168            if not connection.state.is_available:
169                continue
170            connection.change_status('online', message)
171
172    @command
173    @doc(_("Send a disco info request"))
174    def disco(self):
175        client = app.get_client(self.account)
176        if not client.state.is_available:
177            return
178
179        client.get_module('Discovery').disco_contact(self.contact)
180
181
182class StandardCommonChatCommands(CommandContainer):
183    """
184    This command container contains standard commands, which are common
185    to a chat and a private chat only.
186    """
187
188    AUTOMATIC = True
189    HOSTS = ChatCommands, PrivateChatCommands
190
191    @command
192    @doc(_("Clear the text window"))
193    def clear(self):
194        self.conv_textview.clear()
195
196    @command
197    @doc(_("Send a ping to the contact"))
198    def ping(self):
199        if self.account == app.ZEROCONF_ACC_NAME:
200            raise CommandError(
201                _('Command is not supported for zeroconf accounts'))
202        app.connections[self.account].get_module('Ping').send_ping(self.contact)
203
204    @command
205    @doc(_("Send DTMF sequence through an open voice chat"))
206    def dtmf(self, sequence):
207        if not self.audio_sid:
208            raise CommandError(_("No open voice chats with the contact"))
209        for tone in sequence:
210            if not (tone in ("*", "#") or tone.isdigit()):
211                raise CommandError(_("%s is not a valid tone") % tone)
212        gjs = self.connection.get_module('Jingle').get_jingle_session
213        session = gjs(self.full_jid, self.audio_sid)
214        content = session.get_content("audio")
215        content.batch_dtmf(sequence)
216
217    @command
218    @doc(_("Toggle Voice Chat"))
219    def audio(self):
220        if not self.audio_available:
221            raise CommandError(_("Voice chats are not available"))
222        # An audio session is toggled by inverting the state of the
223        # appropriate button.
224        state = self._audio_button.get_active()
225        self._audio_button.set_active(not state)
226
227    @command
228    @doc(_("Toggle Video Chat"))
229    def video(self):
230        if not self.video_available:
231            raise CommandError(_("Video chats are not available"))
232        # A video session is toggled by inverting the state of the
233        # appropriate button.
234        state = self._video_button.get_active()
235        self._video_button.set_active(not state)
236
237    @command(raw=True)
238    @doc(_("Send a message to the contact that will attract their attention"))
239    def attention(self, message):
240        self.send_message(message, process_commands=False, attention=True)
241
242
243class StandardChatCommands(CommandContainer):
244    """
245    This command container contains standard commands which are unique
246    to a chat.
247    """
248
249    AUTOMATIC = True
250    HOSTS = (ChatCommands,)
251
252
253class StandardPrivateChatCommands(CommandContainer):
254    """
255    This command container contains standard commands which are unique
256    to a private chat.
257    """
258
259    AUTOMATIC = True
260    HOSTS = (PrivateChatCommands,)
261
262
263class StandardGroupChatCommands(CommandContainer):
264    """
265    This command container contains standard commands which are unique
266    to a group chat.
267    """
268
269    AUTOMATIC = True
270    HOSTS = (GroupChatCommands,)
271
272    @command
273    @doc(_("Clear the text window"))
274    def clear(self):
275        self.conv_textview.clear()
276
277    @command(raw=True)
278    @doc(_("Change your nickname in a group chat"))
279    def nick(self, new_nick):
280        try:
281            new_nick = helpers.parse_resource(new_nick)
282        except Exception:
283            raise CommandError(_("Invalid nickname"))
284        # FIXME: Check state of MUC
285        self.connection.get_module('MUC').change_nick(
286            self.room_jid, new_nick)
287        self.new_nick = new_nick
288
289    @command('query', raw=True)
290    @doc(_("Open a private chat window with a specified participant"))
291    def chat(self, nick):
292        nicks = app.contacts.get_nick_list(self.account, self.room_jid)
293        if nick in nicks:
294            self.send_pm(nick)
295        else:
296            raise CommandError(_("Nickname not found"))
297
298    @command('msg', raw=True)
299    @doc(_("Open a private chat window with a specified participant and send "
300           "him a message"))
301    def message(self, nick, message):
302        nicks = app.contacts.get_nick_list(self.account, self.room_jid)
303        if nick in nicks:
304            self.send_pm(nick, message)
305        else:
306            raise CommandError(_("Nickname not found"))
307
308    @command(raw=True, empty=True)
309    @doc(_("Display or change a group chat topic"))
310    def topic(self, new_topic):
311        if new_topic:
312            self.connection.get_module('MUC').set_subject(
313                self.room_jid, new_topic)
314        else:
315            return self.subject
316
317    @command(raw=True, empty=True)
318    @doc(_("Invite a user to a group chat for a reason"))
319    def invite(self, jid, reason):
320        control = app.get_groupchat_control(self.account, self.room_jid)
321        if control is not None:
322            control.invite(jid)
323
324    @command(raw=True, empty=True)
325    @doc(_("Join a group chat given by an XMPP Address"))
326    def join(self, jid):
327        if '@' not in jid:
328            jid = jid + '@' + app.get_server_from_jid(self.room_jid)
329
330        app.app.activate_action(
331            'groupchat-join',
332            GLib.Variant('as', [self.account, jid]))
333
334    @command('part', 'close', raw=True, empty=True)
335    @doc(_("Leave the group chat, optionally giving a reason, and close tab or "
336           "window"))
337    def leave(self, reason):
338        self.leave(reason=reason)
339
340    @command(raw=True, empty=True)
341    @doc(_("""
342    Ban user by a nick or a JID from a groupchat
343
344    If given nickname is not found it will be treated as a JID.
345    """))
346    def ban(self, who, reason=''):
347        if who in app.contacts.get_nick_list(self.account, self.room_jid):
348            contact = app.contacts.get_gc_contact(
349                self.account, self.room_jid, who)
350            who = contact.jid
351        self.connection.get_module('MUC').set_affiliation(
352            self.room_jid,
353            {who: {'affiliation': 'outcast',
354                   'reason': reason}})
355
356    @command(raw=True, empty=True)
357    @doc(_("Kick user from group chat by nickname"))
358    def kick(self, who, reason):
359        if who not in app.contacts.get_nick_list(self.account, self.room_jid):
360            raise CommandError(_("Nickname not found"))
361        self.connection.get_module('MUC').set_role(
362            self.room_jid, who, 'none', reason)
363
364    @command(raw=True)
365    # Do not translate moderator, participant, visitor, none
366    @doc(_("""Set participant role in group chat.
367    Role can be given as one of the following values:
368    moderator, participant, visitor, none"""))
369    def role(self, who, role):
370        if role not in ('moderator', 'participant', 'visitor', 'none'):
371            raise CommandError(_("Invalid role given"))
372        if who not in app.contacts.get_nick_list(self.account, self.room_jid):
373            raise CommandError(_("Nickname not found"))
374        self.connection.get_module('MUC').set_role(self.room_jid, who, role)
375
376    @command(raw=True)
377    # Do not translate owner, admin, member, outcast, none
378    @doc(_("""Set participant affiliation in group chat.
379    Affiliation can be given as one of the following values:
380    owner, admin, member, outcast, none"""))
381    def affiliate(self, who, affiliation):
382        if affiliation not in ('owner', 'admin', 'member', 'outcast', 'none'):
383            raise CommandError(_("Invalid affiliation given"))
384        if who not in app.contacts.get_nick_list(self.account, self.room_jid):
385            raise CommandError(_("Nickname not found"))
386        contact = app.contacts.get_gc_contact(self.account, self.room_jid, who)
387
388        self.connection.get_module('MUC').set_affiliation(
389            self.room_jid,
390            {contact.jid: {'affiliation': affiliation}})
391
392    @command
393    @doc(_("Display names of all group chat participants"))
394    def names(self, verbose=False):
395        ggc = app.contacts.get_gc_contact
396        gnl = app.contacts.get_nick_list
397
398        get_contact = lambda nick: ggc(self.account, self.room_jid, nick)
399        get_role = lambda nick: get_contact(nick).role
400        nicks = gnl(self.account, self.room_jid)
401
402        nicks = sorted(nicks)
403        nicks = sorted(nicks, key=get_role)
404
405        if not verbose:
406            return ", ".join(nicks)
407
408        for nick in nicks:
409            contact = get_contact(nick)
410            role = helpers.get_uf_role(contact.role)
411            affiliation = helpers.get_uf_affiliation(contact.affiliation)
412            self.echo("%s - %s - %s" % (nick, role, affiliation))
413
414    @command('ignore', raw=True)
415    @doc(_("Forbid a participant to send you public or private messages"))
416    def block(self, who):
417        self.on_block(None, who)
418
419    @command('unignore', raw=True)
420    @doc(_("Allow a participant to send you public or private messages"))
421    def unblock(self, who):
422        self.on_unblock(None, who)
423
424    @command
425    @doc(_("Send a ping to the contact"))
426    def ping(self, nick):
427        if self.account == app.ZEROCONF_ACC_NAME:
428            raise CommandError(
429                _('Command is not supported for zeroconf accounts'))
430        gc_c = app.contacts.get_gc_contact(self.account, self.room_jid, nick)
431        if gc_c is None:
432            raise CommandError(_("Unknown nickname"))
433        app.connections[self.account].get_module('Ping').send_ping(gc_c)
434