[gwibber/f14] add support for Sina, Catch errors trying to get Oauth tokendata from Twitter (bz 702940, 700960), D

Tom Callaway spot at fedoraproject.org
Thu May 12 19:04:04 UTC 2011


commit 9b0cb11bea09d051fd7fa167ba53fd8952e1ad05
Author: Tom "spot" Callaway <tcallawa at redhat.com>
Date:   Thu May 12 15:03:52 2011 -0400

    add support for Sina, Catch errors trying to get Oauth tokendata from Twitter (bz 702940, 700960), Do not try to process an empty message (bz 702880), Improve about-dialog handling (bz 700878), Catch and log sqlite errors (bz 702992 678015 699139 698074 700966)

 gwibber-3.0.0.1-empty_msg.patch                    |   28 +
 ...ber-3.0.0.1-improve-about-dialog-handling.patch |   12 +
 gwibber-3.0.0.1-sina-icons.patch                   |  Bin 0 -> 26424 bytes
 gwibber-3.0.0.1-sina.patch                         |  726 ++++++++++++++++++++
 gwibber-3.0.0.1-sqlite-catch_error.patch           |   90 +++
 gwibber-3.0.0.1-twitter-catch_error.patch          |   34 +
 gwibber.spec                                       |   54 ++-
 7 files changed, 936 insertions(+), 8 deletions(-)
---
diff --git a/gwibber-3.0.0.1-empty_msg.patch b/gwibber-3.0.0.1-empty_msg.patch
new file mode 100644
index 0000000..c2abda4
--- /dev/null
+++ b/gwibber-3.0.0.1-empty_msg.patch
@@ -0,0 +1,28 @@
+diff -up gwibber-3.0.0.1/gwibber/client.py.empty_msg gwibber-3.0.0.1/gwibber/client.py
+--- gwibber-3.0.0.1/gwibber/client.py.empty_msg	2011-05-12 12:47:32.510671984 -0400
++++ gwibber-3.0.0.1/gwibber/client.py	2011-05-12 12:48:54.281708288 -0400
+@@ -525,14 +525,16 @@ class GwibberClient(gtk.Window):
+       if a.action.__self__.__name__ == "private" and msg["sender"].get("is_me", 0):
+         continue
+ 
+-      if a.include(self, msg):
+-        image = gtk.image_new_from_icon_name(a.icon, gtk.ICON_SIZE_MENU)
+-        mi = gtk.ImageMenuItem()
+-        mi.set_label(a.label)
+-        mi.set_image(image)
+-        mi.set_property("use_underline", True)
+-        mi.connect("activate", perform_action, a.action, view, msg)
+-        menu.append(mi)
++      # I have no idea how you get here with msg = None, but rhbz #702880 says you can.
++      if msg:
++        if a.include(self, msg):
++          image = gtk.image_new_from_icon_name(a.icon, gtk.ICON_SIZE_MENU)
++          mi = gtk.ImageMenuItem()
++          mi.set_label(a.label)
++          mi.set_image(image)
++          mi.set_property("use_underline", True)
++          mi.connect("activate", perform_action, a.action, view, msg)
++          menu.append(mi)
+ 
+     menu.show_all()
+     menu.popup(None, None, None, 3, 0)
diff --git a/gwibber-3.0.0.1-improve-about-dialog-handling.patch b/gwibber-3.0.0.1-improve-about-dialog-handling.patch
new file mode 100644
index 0000000..624503f
--- /dev/null
+++ b/gwibber-3.0.0.1-improve-about-dialog-handling.patch
@@ -0,0 +1,12 @@
+diff -up gwibber-3.0.0.1/gwibber/client.py.improve-about-dialog-handling gwibber-3.0.0.1/gwibber/client.py
+--- gwibber-3.0.0.1/gwibber/client.py.improve-about-dialog-handling	2011-05-12 12:57:18.409763528 -0400
++++ gwibber-3.0.0.1/gwibber/client.py	2011-05-12 12:57:42.509479199 -0400
+@@ -504,7 +504,7 @@ class GwibberClient(gtk.Window):
+     about_dialog = self.ui.get_object("about_dialog")
+     about_dialog.set_version(str(VERSION_NUMBER))
+     about_dialog.set_transient_for(self)
+-    about_dialog.connect("response", lambda *a: about_dialog.hide())
++    about_dialog.connect("response", lambda d, *a: d.hide())
+     about_dialog.show_all()
+ 
+   def on_close_stream(self, *args):
diff --git a/gwibber-3.0.0.1-sina-icons.patch b/gwibber-3.0.0.1-sina-icons.patch
new file mode 100644
index 0000000..fb5f27f
Binary files /dev/null and b/gwibber-3.0.0.1-sina-icons.patch differ
diff --git a/gwibber-3.0.0.1-sina.patch b/gwibber-3.0.0.1-sina.patch
new file mode 100644
index 0000000..ca5804d
--- /dev/null
+++ b/gwibber-3.0.0.1-sina.patch
@@ -0,0 +1,726 @@
+diff -up gwibber-3.0.0.1/gwibber/microblog/plugins/sina/gtk/__init__.py.sina gwibber-3.0.0.1/gwibber/microblog/plugins/sina/gtk/__init__.py
+--- gwibber-3.0.0.1/gwibber/microblog/plugins/sina/gtk/__init__.py.sina	2011-05-12 11:59:10.214024023 -0400
++++ gwibber-3.0.0.1/gwibber/microblog/plugins/sina/gtk/__init__.py	2011-05-12 11:59:10.214024023 -0400
+@@ -0,0 +1 @@
++ 
+diff -up gwibber-3.0.0.1/gwibber/microblog/plugins/sina/gtk/sina/__init__.py.sina gwibber-3.0.0.1/gwibber/microblog/plugins/sina/gtk/sina/__init__.py
+--- gwibber-3.0.0.1/gwibber/microblog/plugins/sina/gtk/sina/__init__.py.sina	2011-05-12 11:59:10.227023883 -0400
++++ gwibber-3.0.0.1/gwibber/microblog/plugins/sina/gtk/sina/__init__.py	2011-05-12 12:04:16.120705165 -0400
+@@ -0,0 +1,174 @@
++import gtk, pango, webkit, gnomekeyring
++import urllib, urllib2, json, urlparse, uuid
++from oauth import oauth
++
++from gtk import Builder
++from gwibber.microblog.util import resources
++import gettext
++from gettext import gettext as _
++if hasattr(gettext, 'bind_textdomain_codeset'):
++    gettext.bind_textdomain_codeset('gwibber','UTF-8')
++gettext.textdomain('gwibber')
++
++gtk.gdk.threads_init()
++
++sigmeth = oauth.OAuthSignatureMethod_HMAC_SHA1()
++
++class AccountWidget(gtk.VBox):
++  """AccountWidget: A widget that provides a user interface for configuring sina accounts in Gwibber
++  """
++  
++  def __init__(self, account=None, dialog=None):
++    """Creates the account pane for configuring Sina accounts"""
++    gtk.VBox.__init__( self, False, 20 )
++    self.ui = gtk.Builder()
++    self.ui.set_translation_domain ("gwibber")
++    self.ui.add_from_file (resources.get_ui_asset("gwibber-accounts-sina.ui"))
++    self.ui.connect_signals(self)
++    self.vbox_settings = self.ui.get_object("vbox_settings")
++    self.pack_start(self.vbox_settings, False, False)
++    self.show_all()
++
++    self.account = account or {}
++    self.dialog = dialog
++    has_secret_key = True
++    if self.account.has_key("id"):
++      try:
++        value = gnomekeyring.find_items_sync(gnomekeyring.ITEM_GENERIC_SECRET, {"id": str("%s/%s" % (self.account["id"], "secret_token"))})[0].secret
++      except gnomekeyring.NoMatchError:
++        has_secret_key = False
++
++    try:
++      if self.account.has_key("access_token") and self.account.has_key("secret_token") and self.account.has_key("username") and has_secret_key and not self.dialog.condition:
++        self.ui.get_object("hbox_sina_auth").hide()
++        self.ui.get_object("sina_auth_done_label").set_label(_("%s has been authorized by Sina") % self.account["username"])
++        self.ui.get_object("hbox_sina_auth_done").show()
++      else:
++        self.ui.get_object("hbox_sina_auth_done").hide()
++        if self.dialog.ui:
++          self.dialog.ui.get_object('vbox_create').hide()
++    except:
++      self.ui.get_object("hbox_sina_auth_done").hide()
++      if self.dialog.ui:
++        self.dialog.ui.get_object("vbox_create").hide()
++
++
++  def on_sina_auth_clicked(self, widget, data=None):
++    self.winsize = self.window.get_size()
++
++    web = webkit.WebView()
++    web.get_settings().set_property("enable-plugins", False)
++    web.load_html_string(_("<p>Please wait...</p>"), "file:///")
++
++    self.consumer = oauth.OAuthConsumer(*resources.get_sina_keys())
++
++    request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, http_method="POST",
++        http_url="http://api.t.sina.com.cn/oauth/request_token")
++
++    request.sign_request(sigmeth, self.consumer, token=None)
++
++    tokendata = urllib2.urlopen(request.http_url, request.to_postdata()).read()
++    self.token = oauth.OAuthToken.from_string(tokendata)
++
++    url = "http://api.t.sina.com.cn/oauth/authorize?oauth_token=%s&oauth_callback=%s&display=popup" % ( self.token.key, "http://gwibber.com/0/auth.html" )
++
++    web.load_uri(url)
++    web.set_size_request(550, 400)
++    web.connect("title-changed", self.on_sina_auth_title_change)
++
++    self.scroll = gtk.ScrolledWindow()
++    self.scroll.add(web)
++
++    self.pack_start(self.scroll, True, True, 0)
++    self.show_all()
++
++    self.ui.get_object("vbox1").hide()
++    self.ui.get_object("vbox_advanced").hide()
++    self.dialog.infobar.set_message_type(gtk.MESSAGE_INFO)
++
++  def on_sina_auth_title_change(self, web=None, title=None, data=None):
++    saved = False
++    if title.get_title() == "Success":
++
++      if hasattr(self.dialog, "infobar_content_area"):
++        for child in self.dialog.infobar_content_area.get_children(): child.destroy()
++      self.dialog.infobar_content_area = self.dialog.infobar.get_content_area()
++      self.dialog.infobar_content_area.show()
++      self.dialog.infobar.show()
++
++      message_label = gtk.Label(_("Verifying"))
++      message_label.set_use_markup(True)
++      message_label.set_ellipsize(pango.ELLIPSIZE_END)
++      self.dialog.infobar_content_area.add(message_label)
++      self.dialog.infobar.show_all()
++      self.scroll.hide()
++      url = web.get_main_frame().get_uri()
++      data = urlparse.parse_qs(url.split("?", 1)[1])
++
++      self.ui.get_object("vbox1").show()
++      self.ui.get_object("vbox_advanced").show()
++
++      token = data["oauth_token"][0]
++      verifier = data["oauth_verifier"][0]
++
++      request = oauth.OAuthRequest.from_consumer_and_token(
++        self.consumer, self.token,
++        http_url="http://api.t.sina.com.cn/oauth/access_token",
++        parameters={"oauth_verifier": str(verifier)})
++      request.sign_request(sigmeth, self.consumer, self.token)
++
++      tokendata = urllib2.urlopen(request.http_url, request.to_postdata()).read()
++      data = urlparse.parse_qs(tokendata)
++
++      atok = oauth.OAuthToken.from_string(tokendata)
++
++      self.account["access_token"] = data["oauth_token"][0]
++      self.account["secret_token"] = data["oauth_token_secret"][0]
++      self.account["username"] = data["screen_name"][0]
++      self.account["user_id"] = data["user_id"][0]
++
++      apireq = oauth.OAuthRequest.from_consumer_and_token(
++        self.consumer, atok,
++        http_method="GET",
++        http_url="http://api.t.sina.com.cn/account/verify_credentials.json", parameters=None)
++
++      apireq.sign_request(sigmeth, self.consumer, atok)
++
++      account_data = json.loads(urllib2.urlopen(apireq.to_url()).read())
++
++      if isinstance(account_data, dict):
++        if account_data.has_key("id"):
++          saved = self.dialog.on_edit_account_save()
++        else:
++          print "Failed"
++          self.dialog.infobar.set_message_type(gtk.MESSAGE_ERROR)
++          message_label.set_text(_("Authorization failed. Please try again.")) 
++      else:
++        print "Failed"
++        self.dialog.infobar.set_message_type(gtk.MESSAGE_ERROR)
++        message_label.set_text(_("Authorization failed. Please try again."))
++
++      if saved: 
++        message_label.set_text(_("Successful"))
++        self.dialog.infobar.set_message_type(gtk.MESSAGE_INFO)
++        #self.dialog.infobar.hide()
++
++      self.ui.get_object("hbox_sina_auth").hide()
++      self.ui.get_object("sina_auth_done_label").set_label(_("%s has been authorized by Sina") % str(self.account["username"]))
++      self.ui.get_object("hbox_sina_auth_done").show()
++      if self.dialog.ui and self.account.has_key("id") and not saved:
++        self.dialog.ui.get_object("vbox_save").show()
++      elif self.dialog.ui and not saved:
++        self.dialog.ui.get_object("vbox_create").show()
++
++    self.window.resize(*self.winsize)
++
++    if title.get_title() == "Failure":
++      web.hide()
++      self.dialog.infobar.set_message_type(gtk.MESSAGE_ERROR)
++      message_label.set_text(_("Authorization failed. Please try again."))
++      self.dialog.infobar.show_all()
++
++      self.ui.get_object("vbox1").show()
++      self.ui.get_object("vbox_advanced").show()
++      self.window.resize(*self.winsize)
+diff -up gwibber-3.0.0.1/gwibber/microblog/plugins/sina/__init__.py.sina gwibber-3.0.0.1/gwibber/microblog/plugins/sina/__init__.py
+--- gwibber-3.0.0.1/gwibber/microblog/plugins/sina/__init__.py.sina	2011-05-12 11:59:10.233023817 -0400
++++ gwibber-3.0.0.1/gwibber/microblog/plugins/sina/__init__.py	2011-05-12 11:59:10.233023817 -0400
+@@ -0,0 +1,294 @@
++from gwibber.microblog import network, util
++from htmlentitydefs import name2codepoint
++import re
++import gnomekeyring
++from oauth import oauth
++from gwibber.microblog.util import log, resources
++from gettext import lgettext as _
++from kitchen.text.converters import to_unicode
++log.logger.name = "Sina"
++
++PROTOCOL_INFO = {
++  "name": "Sina",
++  "version": "1.0",
++  
++  "config": [
++    "private:secret_token",
++    "access_token",
++    "username",
++    "color",
++    "receive_enabled",
++    "send_enabled",
++  ],
++ 
++  "authtype": "oauth1a",
++  "color": "#E61217",
++
++  "features": [
++    "send",
++    "receive",
++    "search",
++    "tag",
++    "reply",
++    "responses",
++    "private",
++    "public",
++    "delete",
++    "retweet",
++    "like",
++    "send_thread",
++    "send_private",
++    "user_messages",
++    "sinceid",
++    "lists",
++    "list",
++  ],
++
++  "default_streams": [
++    "receive",
++    "images",
++    "responses",
++    "private",
++    "lists",
++  ],
++}
++
++URL_PREFIX = "http://t.sina.com.cn"
++API_PREFIX = "http://api.t.sina.com.cn"
++
++def unescape(s):
++  return re.sub('&(%s);' % '|'.join(name2codepoint), 
++    lambda m: unichr(name2codepoint[m.group(1)]), s)
++
++class Client:
++  def __init__(self, acct):
++    self.service = util.getbus("Service")
++    if acct.has_key("secret_token") and acct.has_key("password"): acct.pop("password")
++    self.account = acct
++
++    if not acct.has_key("access_token") and not acct.has_key("secret_token"):
++      return [{"error": {"type": "auth", "account": self.account, "message": _("Failed to find credentials")}}]
++
++    self.sigmethod = oauth.OAuthSignatureMethod_HMAC_SHA1()
++    self.consumer = oauth.OAuthConsumer(*util.resources.get_sina_keys())
++    self.token = oauth.OAuthToken(acct["access_token"], acct["secret_token"])
++
++  def _common(self, data):
++    m = {};
++    try:
++      m["mid"] = str(data["id"])
++      m["service"] = u"sina"
++      m["account"] = self.account["id"]
++      m["time"] = util.parsetime(data["created_at"])
++      m["text"] = to_unicode(unescape(data["text"]))
++      m["to_me"] = ("@%s" % self.account["username"]) in data["text"]
++
++      m["html"] = to_unicode(util.linkify(data["text"],
++        ((util.PARSE_HASH, '#<a class="hash" href="%s#search?q=\\1">\\1</a>' % URL_PREFIX),
++        (util.PARSE_NICK, '@<a class="nick" href="%s/\\1">\\1</a>' % URL_PREFIX)), escape=False))
++
++      m["content"] = to_unicode(util.linkify(data["text"],
++        ((util.PARSE_HASH, '#<a class="hash" href="gwibber:/tag?acct=%s&query=\\1">\\1</a>' % m["account"]),
++        (util.PARSE_NICK, '@<a class="nick" href="gwibber:/user?acct=%s&name=\\1">\\1</a>' % m["account"])), escape=False))
++
++      if data.has_key("retweeted_status"):
++        m["retweeted_status"] = data["retweeted_status"]
++      else:
++        m["retweeted_status"] = None
++
++      images = util.imgpreview(m["text"])
++      if images:
++        m["images"] = images
++        m["type"] = "photo"
++    except: 
++      log.logger.error("%s failure - %s", PROTOCOL_INFO["name"], data)
++      return {}
++ 
++    return m
++
++  def _user(self, user):
++    return {
++        "name": to_unicode(user["name"]),
++        "nick": to_unicode(user["screen_name"]),
++        "id": user["id"],
++        "location": to_unicode(user["location"]),
++        "followers": user.get("followers", None),
++        "image": to_unicode(user["profile_image_url"]),
++        "url": to_unicode("/".join((URL_PREFIX, user["screen_name"]))),
++        "is_me": user["screen_name"] == self.account["username"],
++    }
++    
++  def _message(self, data):
++    if type(data) == type(None):
++      return []
++
++    m = self._common(data)
++    m["source"] = data.get("source", False)
++    
++    if data.has_key("in_reply_to_status_id"):
++      if data["in_reply_to_status_id"]:
++        m["reply"] = {}
++        m["reply"]["id"] = data["in_reply_to_status_id"]
++        m["reply"]["nick"] = to_unicode(data["in_reply_to_screen_name"])
++        if m["reply"]["id"] and m["reply"]["nick"]:
++          m["reply"]["url"] = to_unicode("/".join((URL_PREFIX, m["reply"]["nick"], "statuses", str(m["reply"]["id"]))))
++        else:
++          m["reply"]["url"] = None
++
++    m["sender"] = self._user(data["user"] if "user" in data else data["sender"])
++    m["url"] = to_unicode("/".join((m["sender"]["url"], "statuses", str(m["mid"]))))
++
++    return m
++
++  def _private(self, data):
++    m = self._message(data)
++    m["private"] = True
++
++    m["recipient"] = {}
++    m["recipient"]["name"] = to_unicode(data["recipient"]["name"])
++    m["recipient"]["nick"] = to_unicode(data["recipient"]["screen_name"])
++    m["recipient"]["id"] = data["recipient"]["id"]
++    m["recipient"]["image"] = to_unicode(data["recipient"]["profile_image_url"])
++    m["recipient"]["location"] = to_unicode(data["recipient"]["location"])
++    m["recipient"]["url"] = to_unicode("/".join((URL_PREFIX, m["recipient"]["nick"])))
++    m["recipient"]["is_me"] = m["recipient"]["nick"] == self.account["username"]
++    m["to_me"] = m["recipient"]["is_me"]
++
++    return m
++
++  def _result(self, data):
++    m = self._common(data)
++    
++    if data["to_user_id"]:
++      m["reply"] = {}
++      m["reply"]["id"] = data["to_user_id"]
++      m["reply"]["nick"] = to_unicode(data["to_user"])
++
++    m["sender"] = {}
++    m["sender"]["nick"] = to_unicode(data["from_user"])
++    m["sender"]["id"] = data["from_user_id"]
++    m["sender"]["image"] = to_unicode(data["profile_image_url"])
++    m["sender"]["url"] = to_unicode("/".join((URL_PREFIX, m["sender"]["nick"])))
++    m["sender"]["is_me"] = m["sender"]["nick"] == self.account["username"]
++    m["url"] = to_unicode("/".join((m["sender"]["url"], "statuses", str(m["mid"]))))
++    return m
++
++  def _list(self, data):
++    return {
++        "mid": data["id"],
++        "service": u"sina",
++        "account": self.account["id"],
++        "time": 0,
++        "text": to_unicode(data["description"]),
++        "html": to_unicode(data["description"]),
++        "content": to_unicode(data["description"]),
++        "url": to_unicode("/".join((URL_PREFIX, data["uri"]))),
++        "sender": to_unicode(self._user(data["user"])),
++        "name": to_unicode(data["name"]),
++        "nick": to_unicode(data["slug"]),
++        "key": data["slug"],
++        "full": to_unicode(data["full_name"]),
++        "uri": to_unicode(data["uri"]),
++        "mode": data["mode"],
++        "members": data["member_count"],
++        "followers": data["subscriber_count"],
++        "kind": u"list",
++    }
++
++  def _get(self, path, parse="message", post=False, single=False, **args):
++    url = "/".join((API_PREFIX, path))
++
++    request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, self.token,
++        http_method="POST" if post else "GET", http_url=url, parameters=util.compact(args))
++    request.sign_request(self.sigmethod, self.consumer, self.token)
++    
++    if post:
++      data = network.Download(request.http_url, None, post, body=request.to_postdata()).get_json()
++      #data = network.Download(request.to_url(), util.compact(args), post).get_json()
++    else:
++      data = network.Download(request.to_url(), None, post).get_json()
++
++    resources.dump(self.account["service"], self.account["id"], data)
++
++    if isinstance(data, dict) and data.get("errors", 0):
++      if "authenticate" in data["errors"][0]["message"]:
++        logstr = """%s: %s - %s""" % (PROTOCOL_INFO["name"], _("Authentication failed"), error["message"])
++        log.logger.error("%s", logstr)
++        return [{"error": {"type": "auth", "account": self.account, "message": data["errors"][0]["message"]}}]
++      else:
++        for error in data["errors"]:
++          logstr = """%s: %s - %s""" % (PROTOCOL_INFO["name"], _("Unknown failure"), error["message"])
++          return [{"error": {"type": "unknown", "account": self.account, "message": error["message"]}}]
++    elif isinstance(data, dict) and data.get("error", 0):
++      if "Incorrect signature" in data["error"]:
++        logstr = """%s: %s - %s""" % (PROTOCOL_INFO["name"], _("Request failed"), data["error"])
++        log.logger.error("%s", logstr)
++        return [{"error": {"type": "auth", "account": self.account, "message": data["error"]}}]
++    elif isinstance(data, str):
++      logstr = """%s: %s - %s""" % (PROTOCOL_INFO["name"], _("Request failed"), data)
++      log.logger.error("%s", logstr)
++      return [{"error": {"type": "request", "account": self.account, "message": data}}]
++    
++    if parse == "list":
++      return [self._list(l) for l in data["lists"]]
++    if single: return [getattr(self, "_%s" % parse)(data)]
++    if parse: return [getattr(self, "_%s" % parse)(m) for m in data]
++    else: return []
++
++  def _search(self, **args):
++    data = network.Download("http://api.t.sina.com.cn/search.json", util.compact(args))
++    data = data.get_json()["results"]
++
++    return [self._result(m) for m in data]
++
++  def __call__(self, opname, **args):
++    return getattr(self, opname)(**args)
++  
++  def receive(self, count=util.COUNT, since=None):
++    return self._get("statuses/home_timeline.json", count=count, since_id=since)
++
++  def user_messages(self, id=None, count=util.COUNT, since=None):
++    return self._get("statuses/user_timeline.json", id=id, count=count, since_id=since)
++
++  def responses(self, count=util.COUNT, since=None):
++    return self._get("statuses/mentions.json", count=count, since_id=since)
++
++  def private(self, count=util.COUNT, since=None):
++    private = self._get("direct_messages.json", "private", count=count, since_id=since) or []
++    private_sent = self._get("direct_messages/sent.json", "private", count=count, since_id=since) or []
++    return private + private_sent
++
++  def public(self):
++    return self._get("statuses/public_timeline.json")
++
++  def lists(self, **args):
++    following = self._get("%s/lists/subscriptions.json" % self.account["username"], "list") or []
++    lists = self._get("%s/lists.json" % self.account["username"], "list") or []
++    return following + lists
++
++  def list(self, user, id, count=util.COUNT, since=None):
++    return self._get("%s/lists/%s/statuses.json" % (user, id), per_page=count, since_id=since)
++
++  def search(self, query, count=util.COUNT, since=None):
++    return self._search(q=query, rpp=count, since_id=since)
++
++  def tag(self, query, count=util.COUNT, since=None):
++    return self._search(q="#%s" % query, count=count, since_id=since)
++
++  def delete(self, message):
++    return self._get("statuses/destroy/%s.json" % message["mid"], None, post=True, do=1)
++
++  def like(self, message):
++    return self._get("favorites/create/%s.json" % message["mid"], None, post=True, do=1)
++
++  def send(self, message):
++    return self._get("statuses/update.json", post=True, single=True,
++        status=message)
++  
++  def send_private(self, message, private):
++    return self._get("direct_messages/new.json", "private", post=True, single=True,
++        text=message, screen_name=private["sender"]["nick"])
++
++  def send_thread(self, message, target):
++    return self._get("statuses/update.json", post=True, single=True,
++        status=message, in_reply_to_status_id=target["mid"])
+diff -up gwibber-3.0.0.1/gwibber/microblog/plugins/sina/ui/gwibber-accounts-sina.ui.sina gwibber-3.0.0.1/gwibber/microblog/plugins/sina/ui/gwibber-accounts-sina.ui
+--- gwibber-3.0.0.1/gwibber/microblog/plugins/sina/ui/gwibber-accounts-sina.ui.sina	2011-05-12 11:59:10.234023806 -0400
++++ gwibber-3.0.0.1/gwibber/microblog/plugins/sina/ui/gwibber-accounts-sina.ui	2011-05-12 11:59:10.233023817 -0400
+@@ -0,0 +1,177 @@
++<?xml version="1.0" encoding="UTF-8"?>
++<interface>
++  <requires lib="gtk+" version="2.16"/>
++  <!-- interface-naming-policy toplevel-contextual -->
++  <object class="GtkVBox" id="vbox_settings">
++    <property name="visible">True</property>
++    <property name="spacing">6</property>
++    <child>
++      <object class="GtkVBox" id="vbox1">
++        <property name="visible">True</property>
++        <child>
++          <object class="GtkHBox" id="hbox_sina_auth">
++            <property name="visible">True</property>
++            <child>
++              <object class="GtkButton" id="sina_auth_button">
++                <property name="label" translatable="yes">_Authorize</property>
++                <property name="visible">True</property>
++                <property name="can_focus">True</property>
++                <property name="receives_default">True</property>
++                <property name="use_underline">True</property>
++                <signal name="clicked" handler="on_sina_auth_clicked"/>
++              </object>
++              <packing>
++                <property name="fill">False</property>
++                <property name="position">0</property>
++              </packing>
++            </child>
++            <child>
++              <object class="GtkLabel" id="sina_auth_label">
++                <property name="visible">True</property>
++                <property name="label" translatable="yes">Authorize with sina</property>
++              </object>
++              <packing>
++                <property name="position">1</property>
++              </packing>
++            </child>
++          </object>
++          <packing>
++            <property name="position">0</property>
++          </packing>
++        </child>
++        <child>
++          <object class="GtkHBox" id="hbox_sina_auth_done">
++            <property name="visible">True</property>
++            <child>
++              <object class="GtkLabel" id="sina_auth_done_label">
++                <property name="visible">True</property>
++                <property name="label" translatable="yes">Sina authorized</property>
++              </object>
++              <packing>
++                <property name="position">0</property>
++              </packing>
++            </child>
++          </object>
++          <packing>
++            <property name="position">1</property>
++          </packing>
++        </child>
++      </object>
++      <packing>
++        <property name="position">0</property>
++      </packing>
++    </child>
++    <child>
++      <object class="GtkHSeparator" id="hseparator1">
++        <property name="visible">True</property>
++      </object>
++      <packing>
++        <property name="expand">False</property>
++        <property name="position">1</property>
++      </packing>
++    </child>
++    <child>
++      <object class="GtkVBox" id="vbox_advanced">
++        <property name="visible">True</property>
++        <property name="spacing">6</property>
++        <child>
++          <object class="GtkLabel" id="label3">
++            <property name="visible">True</property>
++            <property name="xalign">0</property>
++            <property name="label" translatable="yes">Account Settings:</property>
++            <attributes>
++              <attribute name="weight" value="bold"/>
++            </attributes>
++          </object>
++          <packing>
++            <property name="position">0</property>
++          </packing>
++        </child>
++        <child>
++          <object class="GtkTable" id="table_advanced_settings">
++            <property name="visible">True</property>
++            <property name="n_rows">2</property>
++            <property name="n_columns">3</property>
++            <property name="column_spacing">12</property>
++            <property name="row_spacing">6</property>
++            <child>
++              <object class="GtkCheckButton" id="send_enabled">
++                <property name="label" translatable="yes">_Send Messages</property>
++                <property name="visible">True</property>
++                <property name="can_focus">True</property>
++                <property name="receives_default">False</property>
++                <property name="tooltip_text" translatable="yes">Allow sending posts to this account</property>
++                <property name="use_underline">True</property>
++                <property name="active">True</property>
++                <property name="draw_indicator">True</property>
++              </object>
++              <packing>
++                <property name="right_attach">3</property>
++                <property name="top_attach">1</property>
++                <property name="bottom_attach">2</property>
++                <property name="x_options">GTK_FILL</property>
++                <property name="y_options"></property>
++              </packing>
++            </child>
++            <child>
++              <object class="GtkCheckButton" id="receive_enabled">
++                <property name="label" translatable="yes">_Receive Messages</property>
++                <property name="visible">True</property>
++                <property name="can_focus">True</property>
++                <property name="receives_default">False</property>
++                <property name="tooltip_text" translatable="yes">Include this account when downloading messages</property>
++                <property name="use_underline">True</property>
++                <property name="active">True</property>
++                <property name="draw_indicator">True</property>
++              </object>
++              <packing>
++                <property name="right_attach">3</property>
++                <property name="x_options">GTK_FILL</property>
++                <property name="y_options"></property>
++              </packing>
++            </child>
++          </object>
++          <packing>
++            <property name="position">1</property>
++          </packing>
++        </child>
++        <child>
++          <object class="GtkHBox" id="hbox1">
++            <property name="visible">True</property>
++            <property name="homogeneous">True</property>
++            <child>
++              <object class="GtkLabel" id="label4">
++                <property name="visible">True</property>
++                <property name="tooltip_text" translatable="yes">Color used to help distinguish accounts</property>
++                <property name="xalign">0</property>
++                <property name="label" translatable="yes">Account Color:</property>
++              </object>
++              <packing>
++                <property name="position">0</property>
++              </packing>
++            </child>
++            <child>
++              <object class="GtkColorButton" id="color">
++                <property name="visible">True</property>
++                <property name="can_focus">True</property>
++                <property name="receives_default">True</property>
++                <property name="tooltip_text" translatable="yes">Color used to help distinguish accounts</property>
++                <property name="color">#000000000000</property>
++              </object>
++              <packing>
++                <property name="expand">False</property>
++                <property name="position">1</property>
++              </packing>
++            </child>
++          </object>
++          <packing>
++            <property name="position">2</property>
++          </packing>
++        </child>
++      </object>
++      <packing>
++        <property name="position">2</property>
++      </packing>
++    </child>
++  </object>
++</interface>
+diff -up gwibber-3.0.0.1/gwibber/microblog/util/const.py.sina gwibber-3.0.0.1/gwibber/microblog/util/const.py
+--- gwibber-3.0.0.1/gwibber/microblog/util/const.py.sina	2011-05-12 11:59:10.145024769 -0400
++++ gwibber-3.0.0.1/gwibber/microblog/util/const.py	2011-05-12 11:59:10.234023806 -0400
+@@ -15,6 +15,9 @@ else:
+ TWITTER_OAUTH_KEY = "VDOuA5qCJ1XhjaSa4pl76g"
+ TWITTER_OAUTH_SECRET = "BqHlB8sMz5FhZmmFimwgiIdB0RiBr72Y0bio49IVJM"
+ 
++SINA_OAUTH_KEY = "4014350411"
++SINA_OAUTH_SECRET = "92e7877ad12d59a8410d850c19787701"
++
+ # Gwibber
+ MAX_MESSAGE_LENGTH = 140
+ MAX_MESSAGE_COUNT = 20000
+diff -up gwibber-3.0.0.1/gwibber/microblog/util/resources.py.sina gwibber-3.0.0.1/gwibber/microblog/util/resources.py
+--- gwibber-3.0.0.1/gwibber/microblog/util/resources.py.sina	2011-04-05 17:06:50.000000000 -0400
++++ gwibber-3.0.0.1/gwibber/microblog/util/resources.py	2011-05-12 11:59:10.234023806 -0400
+@@ -79,6 +79,10 @@ def get_twitter_keys():
+   # Distros should register their own keys and not rely on the defaults
+   return TWITTER_OAUTH_KEY, TWITTER_OAUTH_SECRET
+ 
++def get_sina_keys():
++  # Distros should register their own keys and not rely on the defaults
++  return SINA_OAUTH_KEY, SINA_OAUTH_SECRET
++
+ def get_avatar_path(url):
+   avatar_cache_dir = realpath(join(CACHE_BASE_DIR, "gwibber", "avatars"))
+   if not isdir(avatar_cache_dir):
+diff -up gwibber-3.0.0.1/po/POTFILES.in.sina gwibber-3.0.0.1/po/POTFILES.in
+--- gwibber-3.0.0.1/po/POTFILES.in.sina	2011-05-12 11:59:10.200024174 -0400
++++ gwibber-3.0.0.1/po/POTFILES.in	2011-05-12 11:59:10.235023795 -0400
+@@ -18,6 +18,7 @@ gwibber/microblog/plugins/buzz/gtk/buzz/
+ gwibber/microblog/plugins/digg/gtk/digg/__init__.py
+ gwibber/microblog/plugins/pingfm/gtk/pingfm/__init__.py
+ gwibber/microblog/plugins/qaiku/gtk/qaiku/__init__.py
++gwibber/microblog/plugins/sina/gtk/sina/__init__.py
+ gwibber/preferences.py
+ gwibber/util.py
+ [type: gettext/glade] ui/gwibber-about-dialog.ui
+@@ -35,6 +36,7 @@ gwibber/util.py
+ [type: gettext/glade] gwibber/microblog/plugins/qaiku/ui/gwibber-accounts-qaiku.ui
+ [type: gettext/glade] gwibber/microblog/plugins/foursquare/ui/gwibber-accounts-foursquare.ui
+ [type: gettext/glade] gwibber/microblog/plugins/gowalla/ui/gwibber-accounts-gowalla.ui
++[type: gettext/glade] gwibber/microblog/plugins/sina/ui/gwibber-accounts-sina.ui
+ ui/templates/base.mako
+ ui/templates/targetbar.mako
+ bin/gwibber-poster
+diff -up gwibber-3.0.0.1/setup.py.sina gwibber-3.0.0.1/setup.py
+--- gwibber-3.0.0.1/setup.py.sina	2011-05-12 11:59:10.201024163 -0400
++++ gwibber-3.0.0.1/setup.py	2011-05-12 11:59:10.235023795 -0400
+@@ -69,6 +69,14 @@ setup(name="gwibber",
+     ('share/gwibber/plugins/gowalla/ui/icons/22x22', glob("gwibber/microblog/plugins/gowalla/ui/icons/22x22/*.*")),
+     ('share/gwibber/plugins/gowalla/ui/icons/32x32', glob("gwibber/microblog/plugins/gowalla/ui/icons/32x32/*.*")),
+     ('share/gwibber/plugins/gowalla/ui/icons/scalable', glob("gwibber/microblog/plugins/gowalla/ui/icons/scalable/*.*")),
++    ('share/gwibber/plugins/sina', glob("gwibber/microblog/plugins/sina/*.*")),
++    ('share/gwibber/plugins/sina/gtk', glob("gwibber/microblog/plugins/sina/gtk/*.*")),
++    ('share/gwibber/plugins/sina/gtk/sina', glob("gwibber/microblog/plugins/sina/gtk/sina/*.*")),
++    ('share/gwibber/plugins/sina/ui', glob("gwibber/microblog/plugins/sina/ui/*.*")),
++    ('share/gwibber/plugins/sina/ui/icons/16x16', glob("gwibber/microblog/plugins/sina/ui/icons/16x16/*.*")),
++    ('share/gwibber/plugins/sina/ui/icons/22x22', glob("gwibber/microblog/plugins/sina/ui/icons/22x22/*.*")),
++    ('share/gwibber/plugins/sina/ui/icons/32x32', glob("gwibber/microblog/plugins/sina/ui/icons/32x32/*.*")),
++    ('share/gwibber/plugins/sina/ui/icons/scalable', glob("gwibber/microblog/plugins/sina/ui/icons/scalable/*.*")),
+     ('share/gwibber/plugins/buzz', glob("gwibber/microblog/plugins/buzz/*.*")),
+     ('share/gwibber/plugins/buzz/gtk', glob("gwibber/microblog/plugins/buzz/gtk/*.*")),
+     ('share/gwibber/plugins/buzz/gtk/buzz', glob("gwibber/microblog/plugins/buzz/gtk/buzz/*.*")),
diff --git a/gwibber-3.0.0.1-sqlite-catch_error.patch b/gwibber-3.0.0.1-sqlite-catch_error.patch
new file mode 100644
index 0000000..0a461b1
--- /dev/null
+++ b/gwibber-3.0.0.1-sqlite-catch_error.patch
@@ -0,0 +1,90 @@
+diff -up gwibber-3.0.0.1/gwibber/microblog/storage.py.catch_sqlite_error gwibber-3.0.0.1/gwibber/microblog/storage.py
+--- gwibber-3.0.0.1/gwibber/microblog/storage.py.catch_sqlite_error	2011-05-12 13:43:10.977009364 -0400
++++ gwibber-3.0.0.1/gwibber/microblog/storage.py	2011-05-12 13:59:46.707973761 -0400
+@@ -41,7 +41,11 @@ class MessageManager(dbus.service.Object
+   def setup_table(self):
+     with self.db:
+       schema = "rowid integer primary key autoincrement," + self.schema
+-      self.db.execute("CREATE TABLE messages (%s)" % schema) 
++      try:
++        self.db.execute("CREATE TABLE messages (%s)" % schema) 
++      except sqlite3.OperationalError, msg:
++        log.logger.info("SQLite threw an error trying to setup the messages table: %s", msg)
++
+       self.db.execute("create unique index idx1 on messages (mid, account, operation, transient)")
+ 
+   def maintenance(self):
+@@ -59,10 +63,16 @@ class MessageManager(dbus.service.Object
+         log.logger.info("Found %d records in the messages stream for account %s", count, acct[0])
+         if count > 2000:
+           log.logger.info("Purging old data for %s", acct[0])
+-          self.db.execute("DELETE FROM messages WHERE account = ? AND operation = 'receive' AND stream = 'messages' AND time IN (SELECT CAST (time AS int) FROM (SELECT time FROM messages WHERE account = ? AND operation = 'receive' AND stream = 'messages' AND time != 0 ORDER BY time ASC LIMIT (SELECT COUNT(time) FROM messages WHERE operation = 'receive' AND stream = 'messages' AND account = ? AND time != 0) - 2000) ORDER BY time ASC)", (acct[0],acct[0],acct[0]))
++          try:
++            self.db.execute("DELETE FROM messages WHERE account = ? AND operation = 'receive' AND stream = 'messages' AND time IN (SELECT CAST (time AS int) FROM (SELECT time FROM messages WHERE account = ? AND operation = 'receive' AND stream = 'messages' AND time != 0 ORDER BY time ASC LIMIT (SELECT COUNT(time) FROM messages WHERE operation = 'receive' AND stream = 'messages' AND account = ? AND time != 0) - 2000) ORDER BY time ASC)", (acct[0],acct[0],acct[0]))
++          except sqlite3.OperationalError, msg:
++            log.logger.info("DB Maintenance: SQLite threw an error: %s", msg)
+       except:
+         pass
+-    self.db.execute("VACUUM")
++    try:
++      self.db.execute("VACUUM")
++    except sqlite3.OperationalError, msg:
++      log.logger.info("DB Maintenance: SQLite threw an error: %s", msg)
+     return
+   
+ 
+@@ -154,14 +164,17 @@ class StreamManager(dbus.service.Object)
+ 
+   def setup_table(self):
+     with self.db:
+-      self.db.execute("""
+-      CREATE TABLE streams (
+-      id text,
+-      name text,
+-      account text,
+-      operation text,
+-      data text)
+-      """)
++      try:
++        self.db.execute("""
++        CREATE TABLE streams (
++        id text,
++        name text,
++        account text,
++        operation text,
++        data text)
++        """)
++      except sqlite3.OperationalError, msg:
++        log.logger.info("SQLite threw an error trying to setup the streams table: %s", msg)
+ 
+   @dbus.service.signal("com.Gwibber.Streams", signature="s")
+   def Updated(self, data):
+@@ -265,15 +278,19 @@ class AccountManager(dbus.service.Object
+ 
+   def setup_table(self):
+     with self.db:
+-      self.db.execute("""
+-      CREATE TABLE accounts (
+-          id text,
+-          service text,
+-          username text,
+-          color text,
+-          send integer,
+-          receive integer,
+-          data text)""")
++      try:
++        self.db.execute("""
++        CREATE TABLE accounts (
++            id text,
++            service text,
++            username text,
++            color text,
++            send integer,
++            receive integer,
++            data text)""")
++      except sqlite3.OperationalError, msg:
++        log.logger.info("SQLite threw an error trying to setup the accounts table: %s", msg)
++
+ 
+   def refresh_password_cache(self):
+     for acct in json.loads(self.List()):
diff --git a/gwibber-3.0.0.1-twitter-catch_error.patch b/gwibber-3.0.0.1-twitter-catch_error.patch
new file mode 100644
index 0000000..8a5bb76
--- /dev/null
+++ b/gwibber-3.0.0.1-twitter-catch_error.patch
@@ -0,0 +1,34 @@
+diff -up gwibber-3.0.0.1/gwibber/microblog/plugins/twitter/gtk/twitter/__init__.py.catch_error gwibber-3.0.0.1/gwibber/microblog/plugins/twitter/gtk/twitter/__init__.py
+--- gwibber-3.0.0.1/gwibber/microblog/plugins/twitter/gtk/twitter/__init__.py.catch_error	2011-04-05 17:06:50.000000000 -0400
++++ gwibber-3.0.0.1/gwibber/microblog/plugins/twitter/gtk/twitter/__init__.py	2011-05-12 13:31:09.380655567 -0400
+@@ -4,6 +4,7 @@ from oauth import oauth
+ 
+ from gtk import Builder
+ from gwibber.microblog.util import resources
++from gwibber import error
+ import gettext
+ from gettext import gettext as _
+ if hasattr(gettext, 'bind_textdomain_codeset'):
+@@ -68,7 +69,21 @@ class AccountWidget(gtk.VBox):
+ 
+     request.sign_request(sigmeth, self.consumer, token=None)
+ 
+-    tokendata = urllib2.urlopen(request.http_url, request.to_postdata()).read()
++    try:
++        tokendata = urllib2.urlopen(request.http_url, request.to_postdata()).read()
++    except urllib2.HTTPError, e:
++        http_error = error.GwibberErrorService()
++        http_error.ShowDialog(message='HTTP error trying to get twitter OAuth tokendata (%s)' % (e.code), title='Uh oh. Twitter FAIL WHALE!', type='network')
++        return
++    except urllib2.URLError, e:
++        url_error = error.GwibberErrorService()
++        url_error.ShowDialog(message='URL error trying to get twitter OAuth tokendata (%s)' % (e.reason), title='Uh oh. Twitter FAIL WHALE!', type='network')
++        return
++    except Exception:
++        unknown_error = error.GwibberErrorService()
++        unknown_error.ShowDialog(message='An unknown error occurred trying to get twitter OAuth tokendata.', title='Uh oh. Twitter FAIL WHALE!', type='network')
++        return
++
+     self.token = oauth.OAuthToken.from_string(tokendata)
+ 
+     url = "http://api.twitter.com/oauth/authorize?oauth_token=" + self.token.key
diff --git a/gwibber.spec b/gwibber.spec
index 35e3d85..4120de8 100644
--- a/gwibber.spec
+++ b/gwibber.spec
@@ -4,7 +4,7 @@
 
 Name:           gwibber
 Version:        3.0.0.1
-Release:        1%{?dist}
+Release:        2%{?dist}
 Epoch:          1
 Summary:        An open source microblogging client for GNOME developed with Python and GTK
 Group:          Applications/Internet
@@ -16,7 +16,6 @@ URL:            https://launchpad.net/gwibber
 #   bzr export -r %{bzr_rev} gwibber-%{bzr_rev}bzr.tar.gz lp:gwibber
 # Source0:        %{name}-%{bzr_rev}bzr.tar.gz
 Source0:        http://launchpad.net/gwibber/trunk/3.0/+download/%{name}-%{version}.tar.gz
-BuildRoot:      %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
 
 # Fix situation where pango_overlay can be undefined
 # https://bugs.launchpad.net/gwibber/+bug/577050
@@ -61,6 +60,22 @@ Patch40:	gwibber-3.0.0-gowalla.patch
 Patch41:	gwibber-2.91.92-gowalla-icons.patch
 Patch42:	gwibber-2.91.92-gowalla-kitchen.patch
 
+# Sina support
+Patch43:	gwibber-3.0.0.1-sina.patch
+Patch44:	gwibber-3.0.0.1-sina-icons.patch
+
+# Catch errors trying to get Twitter Oauth token
+Patch45:	gwibber-3.0.0.1-twitter-catch_error.patch
+
+# Don't try to process empty message (bz 702880)
+Patch46:	gwibber-3.0.0.1-empty_msg.patch
+
+# Improve about-dialog handling (bz 700878)
+Patch47:	gwibber-3.0.0.1-improve-about-dialog-handling.patch
+
+# Catch sqlite errors (bz 702992 678015 699139 698074 700966)
+Patch48:	gwibber-3.0.0.1-sqlite-catch_error.patch
+
 Requires:	libsoup, python-pycurl
 Requires:	python 
 Requires:	dbus-python >= 0.80.2
@@ -115,6 +130,22 @@ and GTK. It supports Twitter, Jaiku, Identi.ca, Facebook, and Digg.
 %patch41 -p1 -b .gowalla-icons
 %patch42 -p1 -b .gowalla-kitchen
 
+# Sina
+%patch43 -p1 -b .sina
+%patch44 -p1 -b .sina-icons
+
+# Catch errors trying to get Oauth tokendata from Twitter
+%patch45 -p1 -b .catch_error
+
+# Do not try to process an empty message (bz 702880)
+%patch46 -p1 -b .empty_msg
+
+# Improve about-dialog handling (bz700878)
+%patch47 -p1 -b .improve-about-dialog-handling
+
+# Catch sqlite errors (bz 702992 678015	699139 698074 700966)
+%patch48 -p1 -b .catch_sqlite_error
+
 # sed -i -e '/^#! \?\//, 1d' $(find %{name} | grep "\.py$")
 
 %build
@@ -122,13 +153,18 @@ and GTK. It supports Twitter, Jaiku, Identi.ca, Facebook, and Digg.
 
 
 %install
-rm -rf %{buildroot}
 %{__python} setup.py install --prefix %{_prefix} -O1 --skip-build --root %{buildroot}
 
 # Clean up patchfiles
 for i in `find %{buildroot} |grep "\.gowalla"`; do
 	rm -f $i
 done
+for i in `find %{buildroot} |grep "\.sina"`; do
+	rm -f $i
+done
+for i in `find %{buildroot} |grep "\.catch_error"`; do
+	rm -f $i
+done
 
 ## Reinstall .desktop file
 rm -rf %{buildroot}%{_datadir}/applications
@@ -139,12 +175,7 @@ cp -a build/mo %{buildroot}%{_datadir}/locale
 %find_lang %{name}
 
  
-%clean
-rm -rf %{buildroot}
-
-
 %files -f %{name}.lang
-%defattr(-,root,root,-)
 %doc AUTHORS COPYING README
 %{python_sitelib}/%{name}
 %{python_sitelib}/%{name}-*.egg-info
@@ -166,6 +197,13 @@ rm -rf %{buildroot}
 %{_datadir}/indicators/messages/applications/gwibber
 
 %changelog
+* Thu May 12 2011 Tom Callaway <spot at fedoraproject.org> - 1:3.0.0.1-2
+- add support for Sina
+- Catch errors trying to get Oauth tokendata from Twitter (bz 702940, 700960)
+- Do not try to	process	an empty message (bz 702880)
+- Improve about-dialog handling (bz 700878)
+- Catch and log sqlite errors (bz 702992 678015	699139 698074 700966)
+
 * Tue Apr 26 2011 Tom Callaway <spot at fedoraproject.org> - 1:3.0.0.1-1
 - update to 3.0.0.1
 


More information about the scm-commits mailing list