A micro-blogging tool with multiple interfaces.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

326 lines
11 KiB

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Mitter, micro-blogging client
# Copyright (C) 2007, 2008 the Mitter contributors
#
# This program 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, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import cmd
import mitterlib.ui.console_utils as console_utils
import mitterlib.constants
import datetime
import warnings
from mitterlib.network import NetworksNoNetworkSetupError, NetworksError
from mitterlib.network.networkbase import NetworkError, \
NetworkPermissionDeniedError
namespace = 'cmd' # TODO: rename this var to NAMESPACE (check other files too)
_log = logging.getLogger('ui.cmd')
def options(options):
# no options for this interface
return
class Interface(cmd.Cmd):
"""The command line interface for Mitter."""
# -----------------------------------------------------------------------
# Methods required by cmd.Cmd (our commands)
# -----------------------------------------------------------------------
def do_config(self, line=None):
"""Setup the networks."""
options = self._connection.settings()
console_utils.authorization(options, self._options)
return
def do_timeline(self, line):
"""Return a list of new messages in your friends timeline."""
try:
self._show_messages(self._connection.messages())
except NetworksNoNetworkSetupError:
# call the config
self.do_config()
except NetworkError:
print 'Network failure. Try again in a few minutes.'
return
def do_replies(self, line):
"""Get a list of replies to you."""
try:
self._show_messages(self._connection.replies(), is_timeline=False)
except NetworksNoNetworkSetupError:
self.do_config()
except NetworkError:
print 'Network failure. Try again in a few minutes.'
return
def do_update(self, line):
"""Update your status."""
if self._update(line):
print 'Status updated'
else:
print 'Failed to update your status. Try again in a few minutes.'
def do_exit(self, line):
"""Quit the application."""
_log.debug('Exiting application')
return True
def do_EOF(self, line):
"""Quit the application (it's the same as "exit"). You can also use
Ctrl+D."""
print # Cmd doesn't add an empty line after the ^D
return self.do_exit(None)
def do_rt(self, line):
""""Retweet" a message in your list."""
pos = int(line)
if not self._check_message(pos):
return
original_message = self._messages[pos-1]
if not original_message.message.lower().startswith('rt @'):
new_message = 'RT @%s: %s' % (original_message.username,
original_message.message)
else:
# if it is a retweet already, keep the original information
new_message = original_message.message
return self.do_update(new_message)
def do_r(self, line):
"""Same as "reply"."""
return self.do_reply(line)
def do_reply(self, line):
"""Reply to a message. Use "reply"/"r" <id> <message>."""
line_split = line.split()
pos = int(line_split[0]) # <number> message (cmd strips the
# command already)
if not self._check_message(pos):
return
message = self._messages[pos - 1]
if self._update(' '.join(line_split[1:]), reply_to=message):
print 'Reply sent.'
else:
print "Couldn't send your reply. Try again in a few minutes."
return
def do_delete(self, line):
"""Delete a message. You must provide the number of the displayed\
message."""
message_id = int(line)
real_message_id = self._messages[message_id - 1]
try:
self._connection.delete_message(real_message_id)
except NetworkPermissionDeniedError:
print 'Permission denied.'
return
print 'Message deleted.'
return
def do_thread(self, line):
"""Retrieves the thread about a single message (like a reply.) Be
aware that this may consume a lot of your hourly requests if the
thread is too long."""
message_id = int(line)
_log.debug('Message in pos %d', message_id)
if not self._check_message(message_id):
return
message = self._messages[message_id - 1]
thread = [message]
self._thread(thread, message.parent, message.network)
return
def emptyline(self):
"""Called when the user doesn't call any command. Default is to repeat
the last command; we are going to call timeline() again."""
return self.do_timeline(None)
def default(self, line):
"""Called when we receive an unknown command; default is error
message, we are going to call update() instead."""
return self.do_update(line)
# -----------------------------------------------------------------------
# Helper functions
# -----------------------------------------------------------------------
def _check_message(self, message_id):
"""Check if a message is valid in the current list."""
if message_id < 1 or message_id > len(self._messages):
print
print 'No such message.'
print
return False
return True
def _show_messages(self, data, is_timeline=True):
"""Function called after we receive the list of messages."""
if is_timeline:
self._last_update = datetime.datetime.now()
self._messages = data
console_utils.print_messages(data, self._connection,
show_numbers=True)
self._update_prompt()
return
def _post_delete(self, data, error):
"""Function called after we delete a message."""
if error:
if error == 403:
# Ok, we are *assuming* that, if you get a Forbidden
# error, it means it's not your message.
print "You can't delete this message."
# TODO: we are using Logging.Error in the Twitter
# object when we get this error. So the user will
# see connection errors instead of this simple
# message.
else:
print 'Error deleting message.'
else:
print 'Message deleted.'
self._update_prompt()
return
def _thread(self, thread_list, message_id, network):
"""Build a conversation thread."""
_log.debug('Requesting message %s.%s' % (message_id, network))
try:
message = self._connection.message(message_id, network)
except NetworkError, exc:
_log.debug('Network error:')
_log.debug(exc)
thread_list.insert(0, 'Network error')
self._print_thread(thread_list)
return
# TODO: Catch a permission denied exception and add a proper message
# for it.
thread_list.insert(0, message)
if message.parent:
self._thread(thread_list, message.parent, network)
else:
self._print_thread(thread_list)
return
def _print_thread(self, thread_list):
"""Print the conversation thread."""
pos = 0
_log.debug('%d messages in thread', len(thread_list))
for message in thread_list:
console_utils.print_messages(message, self._connection,
show_numbers=False, indent=pos)
pos += 1
return
def _update(self, status, reply_to=None):
"""Send the update to the server."""
try:
self._connection.update(status, reply_to=reply_to)
except (NetworksError, NetworkError):
# TODO: capture the proper exception.
# TODO: Also, NetworkError's should never get here. Networks
# should catch that (leaving the status kinda messed.)
return False
except MessageTooLongWarning:
print 'Your message is too long. Update NOT send.'
return False
self._update_prompt()
return True
def _update_prompt(self):
"""Update the command line prompt."""
# check the requests limits for every network
requests = self._connection.available_requests()
available = []
for network in requests:
if requests[network] >= 0:
# just show information for networks that count that
available.append('%s (%s): %d' % (
self._connection.name(network),
network,
requests[network]))
if self._last_update:
update_text = self._last_update.strftime('%H:%M')
else:
update_text = 'Never'
self.prompt = ('Last update: %s [%s]\nMitter> ' %
(update_text, ', '.join(available)))
return
# -----------------------------------------------------------------------
# Methods required by the main Mitter code
# -----------------------------------------------------------------------
def __init__(self, connection, options):
"""Class initialization."""
cmd.Cmd.__init__(self)
self._options = options
self._last_update = None
self._connection = connection
self._messages = []
intro = ['Welcome to Mitter %s.' % (mitterlib.constants.version),
'',
'To get a list of available commands, type "help".',
'',
"If you start a line with something that it's not a command, " \
'it will be considered ' \
"a status update (so you don't need to type any commands to " \
'just update your status.',
'',
'An empty line will retrieve the latest updates from your ' \
'friends.',
'',
'']
import textwrap
wrapper = textwrap.TextWrapper()
intros = []
for line in intro:
if not line:
intros.append('') # textwrap doesn't like empty lines
else:
for reident in wrapper.wrap(line):
intros.append(reident)
self.intro = '\n'.join(intros)
self.prompt = 'Mitter> '
return
def __call__(self):
"""Make the object callable; that's the only requirement for
Mitter."""
warnings.simplefilter('error') # Warnings are exceptions
self.cmdloop()
return