#!/usr/bin/python

# This is a part of the external Twitter applet for Cairo-Dock
#
# Author: Eduardo Mucelli Rezende Oliveira
# E-mail: edumucelli@gmail.com or eduardom@dcc.ufmg.br
#
# 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.

# This applet provides Cairo-Dock with an interface with Twitter

# This applet supports both Twitter, and Identi.ca. You can send tweets, direct messages, see your timeline, retweet, and answer direct messages. It will alert you when there are new tweets, and directed messages. On the first time, you need to choose which network to add, once at a time by right-click -> Add account -> Twitter or Identi.ca. The applet is going to ask your nickname and authorization to connect on the networks. The network authorization page will be open. As soon as you authorize it, a PIN number will be shown on the page, copy this number\nPaste this number on the next dialog box will be shown. The plugin is going to inform that you are successfully connected.
# To see the received tweets right-click on the icon -> Twitter -> [New] tweets. You can retweet any of them by left-clicking on it.
# To see the received direct messages right-click on the icon -> Twitter -> [New] direct messages. You can reply any of them by left-clicking on it.
# To see some user's info right-click on the icon -> Twitter -> Info

# TODO: Add more methods to the Identi.ca API

import os, webbrowser
try:
  import queue
except:
  import Queue as queue

from interface import Interface                                                 # interface.py
from twitter import Twitter
from identica import Identica
import emblem, menu                                                             # emblem.py, menu.py
from message import DirectMessage, Tweet                                        # message.py
from util import *                                                              # util.py

from CDApplet import CDApplet, _

# Twitter ----> Interface |----> twitter  ---> Oauth, TwitterAPI, TwitterStreamAPI
#                         |
#                         |----> identica ---> Oauth, IdenticaAPI

class Applet(CDApplet):

  def inform_start_of_waiting_process(self):
    self.icon.SetQuickInfo("...")

  def inform_end_of_waiting_process(self):
    self.icon.SetQuickInfo("")

  def refresh_emblem_counter(self):
    counter = self.tweet_stream.qsize() + self.message_stream.qsize()
    if counter > 0:
      self.emblem.update(counter)                                                               # create the emblem with the counter
      self.icon.SetIcon(self.emblem.emblem)
    else:
      self.update_visual_for_active_networks()
    
  def refresh_emblem_size(self):
    if self.config['emblem_size'] == "Small":
      self.emblem.set_size_small()
    elif self.config['emblem_size'] == "Medium":
      self.emblem.set_size_medium()
    else:
      self.emblem.set_size_large()

  def play_alert_sound(self):
    os.popen('aplay %s' % os.path.abspath("./data/alert.wav"))
  
  def animate_icon(self, animation):
    if len(animation) == 0:
      animation = 'default'
    self.icon.DemandsAttention(True, animation)

  def send_alert(self):
    if self.config['sound']:
      self.play_alert_sound()
    if self.config['animation']:
      self.animate_icon(self.config['animation_type'])

  # Twitter methods

  # This method is a callback that is called as soon as a new entry arrives on the stream
  # The method is passed as parameter when creating the instance of the twitter API, with get_api method.
  def on_receive_new_entry_into_stream_callback(self, entry):
    if 'user' in entry:                                                                     # tweet
      if not entry['user']['screen_name'] == self.twitter.user.screen_name:                         # not sent by the own user
        logp("Inserting new tweet on the stream Queue: %s" % entry)
        self.tweet_stream.put(entry)                                                        # put the new tweet on the stream queue
        self.refresh_emblem_counter()
        if self.config['alert']:                                                            # user wants alert?
          self.send_alert()
    elif 'direct_message' in entry:                                                         # direct messages
      if not entry['direct_message']['sender']['screen_name'] == self.twitter.user.screen_name:     # not sent by the own user
        logp("Inserting new message on the message Queue: %s" % entry)
        self.message_stream.put(entry)                                                      # put the new message on the message queue
        self.refresh_emblem_counter()
        if self.config['alert']:                                                            # user wants alert?
          self.send_alert()

  def show_user_timeline(self):
    self.inform_start_of_waiting_process()
    
    try:
      timeline = self.twitter.api.user_timeline()
      if len(timeline) > 0:
        ut_menu = menu.Menu(self.icon)                                                        # callback not set since there is no action when clicking ...
        for status in timeline:                                                               # ... on the menu generated from this list
          text = status['text']
          sender = status['user']['name']
          uid = status['id_str']
          ut_menu.add(Tweet(text, sender, uid, self.twitter.name))
        ut_menu.pop_up()
      else:
        message = _("Oh, dear, your timeline is empty :-(")
        dialog = {}
        self.show_popup_message(message, dialog)
    except:
      message = _("Connection problem")
      dialog = {'time-length': 4}
      self.show_popup_message(message, dialog)

    self.inform_end_of_waiting_process()

  def show_new_tweets(self):
    self.inform_start_of_waiting_process()
    
    nt_menu = menu.Menu(self.icon, self.on_tweet_list_menu_clicked)
    while not self.tweet_stream.empty():                                                    # iterate on the stream composing the message
      tweet = self.tweet_stream.get()
      text = tweet['text']
      sender = tweet['user']['name']
      uid = tweet['id_str']
      nt_menu.add(Tweet(text, sender, uid, self.twitter.name))
    nt_menu.pop_up()
    
    self.refresh_emblem_counter() 
    self.inform_end_of_waiting_process()

  def show_home_timeline(self):
    self.inform_start_of_waiting_process()
   
    home_timeline_menu = menu.Menu(self.icon, self.on_tweet_list_menu_clicked)
    
    for network in self.networks:
      try:
        timeline = network.api.home_timeline()
      except:
        continue
      if len(timeline) > 0:
        for status in timeline:
          text = status['text']
          sender = status['user']['screen_name']
          if network == self.twitter:
            uid = status['id_str']
          else:
            uid = str(status['statusnet_conversation_id'])
          home_timeline_menu.add(Tweet(text, sender, uid, network.name))
        home_timeline_menu.pop_up()                                                         # pop-uped up twice?
      
    self.inform_end_of_waiting_process()

  def show_new_direct_messages(self):
    self.inform_start_of_waiting_process()
    
    dm_menu = menu.Menu(self.icon, self.on_direct_messages_list_menu_clicked)
    while not self.message_stream.empty():                                                  # iterate on the stream composing the message
      direct_message = self.message_stream.get()
      text = direct_message['direct_message']['text']
      sender = direct_message['direct_message']['sender']['screen_name']
      dm_menu.add(DirectMessage(text, sender, self.twitter.name))
    dm_menu.pop_up()

    self.refresh_emblem_counter()
    self.inform_end_of_waiting_process()
    
  def show_direct_messages(self):
    self.inform_start_of_waiting_process()

    try:
      messages = self.twitter.api.direct_messages()
      if len(messages) > 0:
        dm_menu = menu.Menu(self.icon, self.on_direct_messages_list_menu_clicked)
        for status in messages:
          text = status['text']
          sender = status['sender']['screen_name']
          dm_menu.add(DirectMessage(text, sender, self.twitter.name))
        dm_menu.pop_up()
      else:
        dialog = {'use-markup':True}
        message = _("Oh, dear, you do not have direct messages :-(")
        self.show_popup_message(message, dialog)
    except:
      message = _('Connection problem')
      dialog = {'time-length': 4}
      self.show_popup_message(message, dialog)

    self.inform_end_of_waiting_process()
    
  def on_direct_messages_list_menu_clicked(self, widget):
    self.ask_for_direct_message_reply(widget.get_label())                           # label contains the sender of the message, reply to him/her now
    
  def on_tweet_list_menu_clicked(self, widget):
    self.ask_for_retweet(widget.get_label())                                        # label contains the id of the tweet needed to retweet it

  def show_credentials(self):
    self.inform_start_of_waiting_process()
    try:
      credentials = self.twitter.api.verify_credentials()
      dialog = {'use-markup':True}
      message = "%s [<b>%s</b>]\n%s: %s\n%s: %s\n%s: %s\n" % (credentials['name'], credentials['screen_name'], _('Followers'), credentials['followers_count'], _('Friends'), credentials['friends_count'], _('Tweets'), credentials['statuses_count'])
    except:
      message = _('Connection problem')
      dialog = {'time-length': 4}
      
    self.inform_end_of_waiting_process()
    self.show_popup_message(message, dialog)

  # Dialogs for some kind of input

  def ask_for_direct_message_reply(self, destinatary):
    dialog = {'buttons':'ok;cancel'}
    widget = {'widget-type':'text-entry', 'nb-chars':self.tweet_length + 1}         # 140 characters max, a direct message
    self.show_popup_message("%s, write a reply to %s" % (self.twitter.user.screen_name, destinatary), dialog, widget)
    self.dialog_type = self.responding_sending_direct_message_reply
    self.replying_direct_message_to = destinatary
    
  def responding_sending_direct_message_reply(self, content):
    logp("Sending a direct message '%s'" % content)
    self.twitter.api.new_direct_message(content, self.replying_direct_message_to)

  # Opens the dialog to be filled with the tweet and also informs about tweet longer than 140 characters
  def ask_for_tweet(self, default="", warning=False):
    if not warning:
      message = _("%s, send a tweet") % (self.twitter.user.screen_name or self.identica.user.screen_name)
    else:                                                                                             # user entered more than 140 characters
      message = _("Your tweet has more than %d characters, please make it shorter") % self.tweet_length
    dialog = {'buttons':'ok;cancel'}
    widget = {'widget-type':'text-entry', 'nb-chars':self.tweet_length + 1, 'initial-value':default}  # 140 characters max, a tweet :)
    self.dialog_type = self.responding_tweet
    self.show_popup_message(message, dialog, widget)
    
  def responding_to_tweet(self, content):
    if len(content) > self.tweet_length:
      self.ask_for_tweet(content, warning=True)
    else:
      for network in self.networks:
        logp("Sending a tweet with %s '%s'" % (network.name, content))
        if not network.api.tweet(content):
          self.show_popup_message(_("Problem to send this tweet on %s, I think you already tried to send the same one few minutes ago") % network.name)
    
  def ask_for_retweet(self, tweet_id):
    dialog = {'buttons':'ok;cancel'}
    self.show_popup_message((_("%s, retweet?")) % self.twitter.user.screen_name, dialog)
    self.dialog_type = self.responding_retweet
    self.retweet_tweet_id = tweet_id
    
  def responding_to_retweet(self):
    logp("Retweeting")
    self.twitter.api.retweet(self.retweet_tweet_id)

  # Dialogs for the initial wizard to authorize the user
  # Initial Information -> Ask for screen name (username) -> Ask for authorization (open browser) -> Insert PIN -> Succesful connected
  
  def show_inexistence_of_networks(self):
    self.emblem.update("!!!")                                                               # create the emblem with and alert mark
    self.icon.SetIcon(self.emblem.emblem)
    message = _("This applet supports Twitter, and Identi.ca.\nRight-click -> Add account -> Twitter.\nRight-click -> Add account -> Identi.ca.")
    self.show_popup_message(message)

  def show_initial_informations(self):
    name = camelcase(self.new_network.name)
    message = _("The applet needs your nickname, and an authorization\nthat you accept it to connect on your %s account") % name
    dialog = {'buttons':'next'}
    self.show_popup_message(message, dialog)
    self.dialog_type = self.responding_initial_informations
    
  def responding_to_initial_informations(self):
    self.ask_for_screen_name()

  def ask_for_screen_name(self):
    message = _("What is your %s nickname?") % camelcase(self.new_network.name)
    dialog = {'buttons':'next'}
    widget = {'widget-type':'text-entry'}
    self.show_popup_message(message, dialog, widget)
    self.dialog_type = self.responding_screen_name

  def responding_to_screen_name(self, content):
    logp("Receiving screen name '%s'" % content)
    self.new_network.user.screen_name = content
    self.ask_for_authorization()

  def ask_for_authorization(self):
    authorization_url = self.new_network.auth.get_authorization_url()
    logp("Opening the auth URL '%s'" % authorization_url)
    dialog = {'buttons':'next'}
    try:
      webbrowser.open(authorization_url)
      message = _("%s applet needs you to give the authorization. Authorization page was opened on your browser. As soon as you authorize it, copy the PIN number that will be shown, and go to the next dialog") % camelcase(self.new_network.name)
      self.show_popup_message(message, dialog)
    except webbrowser.Error:    
      message = _("%s applet needs you to give the authorization. Copy the address bellow and access it with your browser. Copy the PIN number that will be shown as soon as you authorize") % camelcase(self.new_network.name)
      widget = {'widget-type':'text-entry', 'initial-value':authorization_url}
      self.show_popup_message(message, dialog, widget)
    self.dialog_type = self.responding_authorization

  def responding_to_authorization(self):
    logp("Asking for PIN")
    self.ask_for_pin_number()                                                           # ask for the PIN number received when acessed the auth URL

  def ask_for_pin_number(self):
    message = _("Enter the PIN number shown on the authorization page")
    dialog = {'buttons':'next'}
    widget = {'widget-type':'text-entry'}
    self.show_popup_message(message, dialog, widget)
    self.dialog_type = self.responding_pin
    
  def responding_to_pin(self, content):
    logp("Receiving PIN: %s" % content)
    self.new_network.user.access_key, self.new_network.user.access_secret = self.new_network.auth.get_access_token_and_secret(content)
    self.new_network.user.write()                                                               # writing user's access token and access secret to be ...
    if isinstance(self.new_network, Twitter):
      if self.new_network.get_api(self.on_receive_new_entry_into_stream_callback):              # ... used here, check twitter.py method get_api
        message = _("Successfully connected with Twitter")
    else:
      if self.new_network.get_api():                                                            # ... used here, check identica.py method get_api
        message = _("Successfully connected with Identi.ca")
    self.update_active_networks()
    self.update_visual_for_active_networks()
    self.show_popup_message(message)

  def show_popup_message(self, message, dialog={}, widget={}):
    dialog_attributes = {'message':message}
    widget_attributes = {}
    dialog_attributes.update(dialog)
    widget_attributes.update(widget)
    self.icon.PopupDialog (dialog_attributes, widget_attributes)

  # Menus

  def build_direct_messages_menu(self):
    direct_messages_menu = []
    if self.message_stream.empty():
      label = _("Direct messages")
    else:
      label = _("New direct messages (%d)") % self.message_stream.qsize()
    direct_messages_menu.append ({
        'type'  : CDApplet.MENU_ENTRY,
        'label' : label,
        'id'    : self.direct_messages_menu_id,
        'icon'  : os.path.abspath("./data/message.png")
    })
    self.icon.AddMenuItems(direct_messages_menu)

  def build_credentials_menu(self):
    credentials_menu = []
    credentials_menu.append ({
        'type'  : CDApplet.MENU_ENTRY,
        'label' : _("Info"),
        'id'    : self.credentials_menu_id,
        'icon'  : os.path.abspath("./data/credentials.png")
    })
    self.icon.AddMenuItems(credentials_menu)
    
  def build_tweets_menu(self):
    tweets_menu = []
    if self.tweet_stream.empty():
      label = _("Tweets")
    else:
      label = _("New tweets (%d)") % self.tweet_stream.qsize()
    tweets_menu.append ({
        'type'  : CDApplet.MENU_ENTRY,
        'label' : label,
        'id'    : self.tweets_menu_id,
        'icon'  : os.path.abspath("./data/tweet.png")
    })
    self.icon.AddMenuItems(tweets_menu)
    
  def build_user_timeline_menu(self):
    user_timeline_menu = []
    
    user_timeline_menu.append ({
        'type'  : CDApplet.MENU_ENTRY,
        'label' : _("My tweets"),
        'id'    : self.user_timeline_menu_id,
        'icon'  : os.path.abspath("./data/tweet.png")
    })
    self.icon.AddMenuItems(user_timeline_menu)

  def build_add_twitter_and_identica_account_menus(self):
    self.icon.AddMenuItems([self.build_add_account_menu(),self.build_add_twitter_account_menu(),self.build_add_identica_account_menu()])
    
  def build_add_only_twitter_account_menu(self):
    self.icon.AddMenuItems([self.build_add_account_menu(),self.build_add_twitter_account_menu()])
    
  def build_add_only_identica_account_menu(self):
    self.icon.AddMenuItems([self.build_add_account_menu(),self.build_add_identica_account_menu()])

  def build_add_account_menu(self):
    add_account_menu = {
        'type'  : CDApplet.MENU_SUB_MENU,
        'label' : _("Add account"),
        'id'    : self.add_account_menu_id
    }
    return add_account_menu

  def build_add_twitter_account_menu(self):
    twitter_menu = {
        'type'  : CDApplet.MENU_ENTRY,
        'label' : _("Twitter"),
        'id'    : self.add_twitter_account_menu_id,
        'icon'  : os.path.abspath("./data/twitter.png"),
        'menu'  : self.add_account_menu_id
    }
    return twitter_menu

  def build_add_identica_account_menu(self):
    identica_menu = {
        'type'  : CDApplet.MENU_ENTRY,
        'label' : _("Identi.ca"),
        'id'    : self.add_identica_account_menu_id,
        'icon'  : os.path.abspath("./data/identica.png"),
        'menu'  : self.add_account_menu_id
    }
    return identica_menu
    
  def any_network_is_active(self):
    return len(self.networks) > 0
    
  def update_active_networks(self):
    if self.twitter.api:
      self.networks.append(self.twitter)
    if self.identica.api:
      self.networks.append(self.identica)
        
  def update_visual_for_active_networks(self):
    if self.twitter.api:
      self.icon.SetIcon(os.path.abspath("./icon"))                                  # overwrite the alert mark "!!!"
      if self.identica.api:
        self.icon.SetLabel("Twitter and Identi.ca")
        self.icon.SetIcon(os.path.abspath("./data/twitter_and_identica.png"))
    else:
      if self.identica.api:
        self.icon.SetLabel("Identi.ca")
        self.icon.SetIcon(os.path.abspath("./data/identica.png"))

  def __init__(self):
    
    self.interface = Interface()
    
    self.twitter = self.interface.twitter()
    self.identica = self.interface.identica()
    
    self.networks = []                                                                        # active networks
    
    (self.responding_screen_name, self.responding_authorization, self.responding_pin,
    self.responding_success, self.responding_tweet, self.responding_initial_informations,
    self.responding_sending_direct_message_reply, self.responding_retweet) = list(range(8))
    self.dialog_type = None

    self.direct_messages_menu_id  = 1000
    self.credentials_menu_id      = 2000
    self.tweets_menu_id           = 3000
    self.user_timeline_menu_id    = 4000
    
    self.add_account_menu_id              = 5000
    self.add_identica_account_menu_id     = 5001
    self.add_twitter_account_menu_id      = 5002
    
    self.tweet_length             = 140
    
    self.tweet_stream   = queue.Queue()
    self.message_stream = queue.Queue()

    CDApplet.__init__(self)                                                                             # call CDApplet interface init

    path = self.cConfFile[0:-12] + "emblem.svg" # "Twitter.conf"
    self.emblem = emblem.Emblem(path) # emblem maker, see emblem.py

  # Inherited methods from CDApplet
  def begin(self):
    logp("Looking for user ...")
    if self.twitter.get_api(self.on_receive_new_entry_into_stream_callback):                            # pass stream api callback, ugly yet!
      self.identica.get_api()
    elif not self.identica.get_api():
      logm("User not found")
      self.show_inexistence_of_networks()   
    self.update_active_networks()
    self.update_visual_for_active_networks()

  def get_config(self, keyfile):
    self.config['emblem_size'] = keyfile.get('Configuration', 'emblem_size')
    self.config['alert'] = keyfile.getboolean('Configuration', 'alert')
    self.config['sound'] = keyfile.getboolean('Configuration', 'sound')
    self.config['animation'] = keyfile.getboolean('Configuration', 'animation')
    self.config['animation_type'] = keyfile.get('Configuration', 'animation_type')

  def reload(self):
    self.refresh_emblem_counter()
    self.refresh_emblem_size()

  # Callbacks
  def on_answer_dialog(self, key, content):
    if (key == 0 or key == -1):                                                                       # ... and pressed Ok or Enter
      if self.dialog_type == self.responding_initial_informations:
        self.responding_to_initial_informations()
      elif self.dialog_type == self.responding_screen_name:                                           # user typed screen name ...
        self.responding_to_screen_name(content)
      elif self.dialog_type == self.responding_authorization:
        self.responding_to_authorization()
      elif self.dialog_type == self.responding_pin:                                                   # user typed the PIN number
        self.responding_to_pin(content)
      elif self.dialog_type == self.responding_tweet:
        self.responding_to_tweet(content)
      elif self.dialog_type == self.responding_sending_direct_message_reply:
        self.responding_to_sending_direct_message_reply(content)
      elif self.dialog_type == self.responding_retweet:
        self.responding_to_retweet()

  # If there is nothing to be seen, the left click opens the dialog to write a tweet
  # Otherwise we check if there is something new, likely to be what the user wants
  # to see quickly
  def on_click(self, key):
    if not self.tweet_stream.empty():
      self.show_new_tweets()
    elif not self.message_stream.empty():
      self.show_new_direct_messages()
    else:
      if self.any_network_is_active():
        self.ask_for_tweet()

  def on_middle_click(self):
    self.show_home_timeline()

  def on_build_menu(self):
    if self.twitter.api:
      self.build_credentials_menu()
      self.build_user_timeline_menu()
      self.build_direct_messages_menu()
      self.build_tweets_menu()
      if self.identica.api:
        logp("All accounts added")
      else:
        self.build_add_only_identica_account_menu()
    else:
      if self.identica.api:
        self.build_add_only_twitter_account_menu()
      else:
        self.build_add_twitter_and_identica_account_menus()  

  def on_menu_select(self, selected_menu):
    if selected_menu == self.direct_messages_menu_id:
      if self.message_stream.empty():                                                 # there are not new direct messages
        self.show_direct_messages()                                                   # show the last 20
      else:                                                                           # new direct messages on the stream
        self.show_new_direct_messages()                                               # show new direct messages
    elif selected_menu == self.credentials_menu_id:
      self.show_credentials()
    elif selected_menu == self.tweets_menu_id:
      if self.tweet_stream.empty():                                                   # there are not new tweets
        self.show_home_timeline()                                                     # show the last 20 tweets on the home timeline
      else:                                                                           # new tweets on the stream
        self.show_new_tweets()                                                        # show new tweets
    elif selected_menu == self.user_timeline_menu_id:
      self.show_user_timeline()
    elif selected_menu == self.add_identica_account_menu_id:
      self.new_network = self.identica
      self.show_initial_informations()
    elif selected_menu == self.add_twitter_account_menu_id:
      self.new_network = self.twitter
      self.show_initial_informations()
      

if __name__ == '__main__':
  Applet().run()
