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
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
|
|
|