Novedades en CMD Twitt

No hay mucho que decir pero ha habido algunos avances significativos desde que escribí el primer post. He añadido algunas opciones más para que el programa sea más útil, he corregido dos pequeños bugs relativos a la gestión de errores y he simplificado la gestión de la codificación de texto gracias a una característica de la librería python-twitter que me había pasado desapercibida. También he creado alguna función más que me ha permitido simplificar un poco más el código.

Lo que me queda por hacer:

  1. Añadir un modo interactivo, lo programaré con la librería curses
  2. Aprovechar la capacidad de internacionalización del programa y traducirlo al castellano y al catalán
  3. Empaquetarlo para Debian
  4. Si a alguien se le ocurre algo más, tiene todo el derecho del mundo a decirlo :) .

Algunas cosillas más :) :

Añadí el proyecto a Launchpad ( CMD Twitt ) ya que gracias a ésto ahora tengo un bug tracker donde todo el mundo puede indicar los bugs que encuentre en el programa :) y también dejar sugerencias de mejora en los Blue Prints.

Saludos!

Cmd Twitt

Hace tres días empecé a programar Cmd Twitt (y lo acabé ayer, bueno, supongo que todavía se puede mejorar pero no le voy a dedicar mucho más tiempo a no ser que se reporten bugs alguien tenga buenas ideas para mejorarlo). Éste programa es un simple programa de consola que permite acceder a los servicios de la popular red social Twitter.

Lo programé en Python usando la librería python-twitter, que se puede instalar mediante aptitude o apt-get en sistemas Debian o Ubuntu. Hay algunos detalles interesantes del programa sobre los que me gustaría escribir porque aprendí algunas cosillas mientras lo estaba haciendo, entre otras cosas las siguientes:

  • Internacionalización de aplicaciones Python
  • Correcto funcionamiento con diferentes codificaciones de texto
  • Gestión sencilla de ficheros de configuración
  • Poner colorines en la consola, jeje
  • Esto tiene poco que ver con la programación en sí, pero es interesante también: Trabajar con git y gitorious

Os cuelgo el código y acto seguido vamos con la explicación:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
#!/usr/bin/env python
# encoding: utf-8
# -*- coding: utf-8 -*-
 
############################################################################
#                     --- cmd-twitt 2009.08.16 ---                         #
#                                                                          #
#   Copyright (C) 2009 by Andreu Correa Casablanca  (Original Author)      #
#                                                                          #
#   Email: castarco@gmail.com (Andreu Correa Casablanca)                   #
#                                                                          #
#   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, write to the                          #
#   Free Software Foundation, Inc.,                                        #
#   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.              #
############################################################################
 
import ConfigParser
import getpass
import gettext
import locale
import optparse
import os
import sys
import twitter
import urllib2
 
# An alias that allows us to imitate the touch command
touch = lambda file: open(file, "w").close()
 
cmd_usage   = 'usage: %prog [option] [num_of_twits_per_timeline]'
cmd_version = '2009.08.16'
 
conf_dir  = os.environ["HOME"] + "/.cmdtwitt/"
conf_file = conf_dir + "user.conf"
 
encoding  = locale.getpreferredencoding().lower()
 
user = ''
pswd = ''
 
color = {}
color['green']   = '\33[1;32m'
color['nocolor'] = '\33[1;0m'
color['red']     = '\33[1;31m'
color['white']   = '\33[1;37m'
color['yellow']  = '\33[1;33m'
 
# Translate some global vars
def init_vars():
        global cmd_usage
 
        cmd_usage   = _(cmd_usage)
 
# Load the account settings
def load_config():
        global user, pswd
 
        cfg = ConfigParser.ConfigParser()
 
        try:
                cfg.readfp(file(conf_file))
        except:
              raise
 
        user = cfg.get ('TWIT_ACCOUNT', 'User')
        pswd = cfg.get ('TWIT_ACCOUNT', 'Pswd')
 
#Set the account settings
def set_config():
        global conf_dir, conf_file
 
        print _("Settings:")
 
        cfg = ConfigParser.ConfigParser()
        cfg.add_section('TWIT_ACCOUNT')
 
        cfg.set('TWIT_ACCOUNT', 'User', raw_input(_('tUser: ')) )
 
        pswd_not_set = True
        while pswd_not_set:
                pswd1 = getpass.getpass(_('tPassword       : '))
                pswd2 = getpass.getpass(_('tRepeat password: '))
 
                if pswd1 == pswd2:
                        pswd_not_set = False
                else:
                        print _('The passwords don't match. Please try to write it again.')
 
        cfg.set('TWIT_ACCOUNT', 'Pswd', pswd1)
 
        if os.access(conf_file, os.F_OK) == False:
                try:
                        touch(conf_file)
                except IOError:
                        try:
                                os.mkdir(conf_dir, 0700)
                        except IOError:
                                print _('It was impossible to create the config dir.')
                                raise
 
                        try:
                                touch(conf_file)
                        except:
                                print _('It was impossible to create the config file.')
                                raise
 
        try:
                cfg.write(file(conf_file, 'w'))
        except:
                raise
 
# Manage connection errors
def manage_connection_error(e, code):
        if e.getcode() == code:
                print _('tUnauthorized acces, you should set a correct username and password.')
        else:
                print _('It seems twitter is down. Try it later.')
 
# Sends a message to a user
def send_msg_to_user(_user):
        global user, pswd, cmd_version
 
        api = twitter.Api(username=user, password=pswd)
 
        msg = unicode(raw_input( _('Type your message:nt') ), encoding)[:140]
 
        try:
                api.PostDirectMessage(_user, msg)
 
        except urllib2.HTTPError, e:
                print _('Error sending the message:')
 
                manage_connection_error(e, 401)
 
# Show the user status
def show_status():
        global user, pswd
 
        api = twitter.Api(username=user, password=pswd)
 
        try:
                status = api.GetUserTimeline(user)[0].text
 
                print _('Your status is:nt' + status)
        except urllib2.HTTPError, e:
                print _('Error reading your status:')
 
                manage_connection_error(e, 404)
 
# Format the timeline to show in the console
def show_timeline(num_msgs, statuses):
        global color
 
        _status = statuses[0]
        print color['green']+_status.user.name + ' :n' + color['yellow'] + _status.GetRelativeCreatedAt() + ' > ' + color['nocolor'] + _status.text + 'n'
 
        for status in statuses[1:num_msgs]:
                if status.user.name == _status.user.name:
                        name = color['yellow'] + status.GetRelativeCreatedAt()+' > ' + color['nocolor']
                else:
                        name = color['green']+status.user.name + ' :n' + color['yellow'] + status.GetRelativeCreatedAt() + ' > ' + color['nocolor']
 
                print name + status.text + 'n'
                _status = status
 
# Shows the timeline of a specific twitter user
def show_usertimeline(_user, num_msgs):
        global user, pswd, color
 
        api = twitter.Api(username=user, password=pswd)
 
        try:
                statuses = api.GetUserTimeline(_user)
 
                print color['red'] + _user + _(' Timeline:n') + color['nocolor']
 
                show_timeline(num_msgs, statuses)
 
        except urllib2.HTTPError, e:
                print _('Error reading the timeline:')
 
                manage_connection_error(e, 401)
 
# Shows the Home timeline
def show_friendstimeline(num_msgs):
        global user, pswd, color
 
        api = twitter.Api(username=user, password=pswd)
 
        try:
                statuses = api.GetFriendsTimeline(user)
 
                print color['red'] +_('Home Timeline:n') + color['nocolor']
 
                show_timeline(num_msgs, statuses)
 
        except urllib2.HTTPError, e:
                print _('Error reading the timeline:')
 
                manage_connection_error(e, 401)
 
# Shows the Home timeline
def show_publictimeline(num_msgs):
        global user, pswd
 
        api = twitter.Api(username=user, password=pswd)
 
        try:
                statuses = api.GetPublicTimeline()
 
                print color['red'] + _('Public Timeline:n') + color['nocolor']
 
                show_timeline(num_msgs, statuses)
 
        except urllib2.HTTPError, e:
                print _('Error reading the timeline:')
 
                manage_connection_error(e, 401)
 
# Send a message to twitter
def send_msg(_user):
        global user, pswd, cmd_version
 
        api = twitter.Api(username=user, password=pswd)
 
        try:
                if _user != '':
                        _user = '@'+_user+' '
 
                api.PostUpdate( ( _user + unicode(raw_input( _('Write your message and press Enter:nt') ), encoding) )[:140] )
 
        except urllib2.HTTPError, e:
                print _('Error sending the message:')
 
                manage_connection_error(e, 401)
 
# The main program
def main(argv=None):
        global user, pswd
 
        if argv == None:
                argv = sys.argv
 
        # Internationalization
        gettext.install('cmd-twitt')
 
        init_vars()
 
        cmd_parser = optparse.OptionParser(usage=cmd_usage, version=cmd_version, conflict_handler='resolve')
 
        cmd_parser.add_option('-h', '--help',    action='help',    help=_('print this help text and exit'))
        cmd_parser.add_option('-v', '--version', action='version', help=_('print program version and exit'))
 
        cmd_parser.add_option('-c', '--config', dest='config', action='store_true', help=_('Configure your twitter account'))
        cmd_parser.add_option('-s', '--status', dest='status', action='store_true', help=_('Shows your status'))
 
        cmd_parser.add_option('-p', '--publictimeline',  dest='publictimeline',  action='store_true', help=_('Shows the public timeline'))
        cmd_parser.add_option('-u', '--usertimeline',    dest='usertimeline',    action='store_true', help=_('Shows the user timeline'))
        cmd_parser.add_option('-F', '--friendtimeline',  dest='friendtimeline',  metavar='FRIEND',    help=_('Shows the timeline of a friend'))
        cmd_parser.add_option('-f', '--friendstimeline', dest='friendstimeline', action='store_true', help=_('Shows the friends timeline'))
 
        cmd_parser.add_option('-m', '--private-message', dest='private_msg', metavar='USER', help=_('Sends a private message to a twitter user'))
        cmd_parser.add_option('-M', '--public-message',  dest='public_msg',  metavar='USER', help=_('Sends a public message to a twitter user'))
 
        (cmd_opts, cmd_args) = cmd_parser.parse_args()
 
        if cmd_opts.config:
                try:
                        set_config()
                except:
                        print _("It was impossible to set the settings.")
                        return 1
        else:
                try:
                        load_config()
                except:
                        print _('There is not a config file, you should set your user and password in the settings option.n')
                        cmd_parser.print_help()
                        return 1
 
                try:
                        num_msgs = int(cmd_args[0])
                except:
                        num_msgs = 10
 
                if cmd_opts.private_msg:
                        send_msg_to_user(cmd_opts.private_msg)
                elif cmd_opts.public_msg:
                        send_msg(cmd_opts.public_msg)
                elif cmd_opts.status:
                        show_status()
                elif cmd_opts.usertimeline:
                        show_usertimeline(user, num_msgs)
                elif cmd_opts.friendtimeline:
                        show_usertimeline(cmd_opts.friendtimeline, num_msgs)
                elif cmd_opts.friendstimeline:
                        show_friendstimeline(num_msgs)
                elif cmd_opts.publictimeline:
                        show_publictimeline (num_msgs)
                else:
                        send_msg('')
 
if __name__ == "__main__":
        sys.exit(main())

Internacionalización

Lo primero que se tiene que hacer es escribir la linia import gettext para cargar el módulo que nos ayudará en nuestro quehacer. La segunda línea (no del texto, sino referente a la internacionalización, la podemos encontrar en la función main) gettext.install('cmd-twitt') se dedica a cargar la traducción al idioma por defecto del sistema para las cadenas de texto que usa el programa. La traducción se cargará desde el directorio /usr/share/locale/[abreviatura para el lenguaje por defecto del sistema]/LC_MESSAGES/cmd-twitt.mo.

Por último (en cuanto se refiere al código) lo que se tiene que hacer para que todas las cadenas que queramos sean susceptibles de traducción a través del módulo gettext es escribirlas de ésta forma _('cadena'). La función _() es un alias para gettext.gettext().

Quedan otros pasos que no tienen mucho que ver con el código, sinó con la traducción en sí. Para empezar a traducir el programa lo primero que debemos hacer es obtener un fichero especial sobre el que se basarán las traducciones. Lo haremos así:

gettext cmd-twitt.py

Ésto generará un fichero llamado messages.po que contendrá parejas de cadenas, las originales junto con sus traducciones. Para empezar la traducción al castellano generamos un nuevo fichero a partir de messages.po que será el que contendrá la traducción en sí (messages.po lo guardamos como base para traducciones a otros lenguajes). Lo haremos como se sigue:

msginit --locale=es -i messages.po

Éste programa nos pedirá algunos datos acerca de nosotros (para que quede constancia de quienes son los traductores,  está bien que se sepa de quien es la autoría) y finalmente creará un fichero llamado es.po , que es el que nos dedicaremos a modificar.

Finalmente el fichero puede ser editado por herramientas tales como gtranslator o ktranslator para luego ser compilado a un fichero con extensión .mo que será el que irá al directorio de traducciones que mencionamos anteriormente. (Cambiando su nombre es.mo a cmd-twitt.mo)

Codificaciones de texto

Éste es un tema que ha dado mucho por saco, la verdad sea dicha. De hecho la librería python-twitter no funciona demasiado bien en este aspecto, falta que la pulan un poco (y no solo por lo de la codificación de texto, yo ya he tenido que corregir algunos bugs en la versión de mi sistema, tengo que ver ahora si es la versión de Ubuntu o la del repositorio oficial de código tiene los mismos errores).

Percibí el problema en cuanto intenté enviar mensajes con acentos a través de Cmd Twitt a Twitter, ¿Cual era el problema? El programa reventaba. Mi primer logro consistió en hacer que funcionara para mi sistema, pero luego ajusté un poco mejor el funcionamiento.. y CREO (no estoy del todo seguro) que funcionará en sistemas con diferentes codificaciones de texto.

Primera solución:

Supongamos que str es una cadena que hemos obtenido a través de la función raw_input() , la solución consistía en usar la cadena resultantde de aplicar la función unicode() a str, de la siguiente forma: unicode(str, 'utf-8') . Si no se añade el segundo argumento la función unicode() intenta leer str como si fuera ASCII y el programa revienta igualmente (que es justo lo que se hace en la librería python-twitter, un error un poco burdo, se nota que no deben hablar otras lenguas los creadores de la librería).

Ésta solución funciona en mi sistema porque usa UTF-8 para la codificación de las cadenas... pero eso no es necesariamente así en todos los sistemas.

Segunda solución:

La segunda solución pasa por el uso del módulo locale, así que tenemos que hacer un import de éste. Simplemente tenemos que sustituir la cadena 'utf-8' por el resultado de locale.getpreferredencoding().lower() .

Ficheros de configuración

No me extenderé mucho con ésto. He usado el módulo ConfigParser , y en cuanto a la introducción de la contraseña de usuario para twitter he utilizado un módulo llamado getpass que permite introducir caracteres sin tener que mostrarlos por pantalla. És fácil de aprender viendo el código como ejemplo.

Colorines

Lo reconozco, hay que estar un poco zumbado para dedicar una sección entera de la explicación ssólo para los colorines, qué le vamos a hacer, soy así. Aunque se reduce a algo tan simple como añadir al principio de la cadena que queremos colorear ciertas cadenas características que harán cambiar el color del texto de la consola (incluso después de haberse cerrado el programa). Ésto solo funciona para algunas consolas concretas.. no las he provado, pero aseguro que funciona con Bash, y también aseguro que no funciona en MS Windows.

Las cadenas son de la forma '\33[1;32m' en la que para escoger el color sólo se tienen que cambiar los dos números del final, ésta en particular es para el color verde. Al final del artículo encontraréis un enlace para ver la tabla de códigos y sus colores correspondientes.

Git y Gitorious

Me da pereza seguir explicándolo todo como si fuera un cuento, voy al grano:

  • Crear una cuenta en gitorious.org
  • Subir una clave pública de ssh a Gitorious, si ya existe en vuestro sistema la tendréis en el fichero /home/usuario/.ssh/id_rsa.pub . En caso de no tenerla, lo podéis hacer mediante el comando ssh-keygen -t rsa .
  • Crear un proyecto y un repositorio, es todo guiado así que resulta muy sencillo.
  • Ahora toca la parte de configurar el repositorio en nuestro ordenador de trabajo. Tendremos que tener instalados ssh y git antes que nada. Los pasos que seguiremos son:
    • cd directorio_proyecto
    • git init  # Creamos el repositorio local
    • git add . # Ésto añade el directorio a la lista de ficheros del repositorio git
    • git commit -a -m "Y se hizo la luz"  # El comentario no es realmente importante (almenos en el primer commit, vamos)
    • git remote add origin git@gitorious.org:miproyecto/mainline.git # Esto nos servirá para "conectar" nuestro repositorio local con el remoto de Gitorious.org, en la documentación que encontré ponían origen en vez de origin pero luego el siguiente paso no se podía hacer de la manera "corta" que seguidamente indico
    • git push origin master # Con ésto subimos los cambios que hemos hecho al repositorio remoto, las próximas veces sólo tendremos que escribir git push

Ahora os dejo las fuentes que me han servido para informarme un poco:

Internacionalización:

Ficheros de configuración en Python:

Colorines:

Configuración de Git y Gitorious:

Hasta otra :) .