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