Removed legacy key support, added custom key support
[mevemon] / package / src / mevemon.py
1 #!/usr/bin/env python
2 #
3 # mEveMon - A character monitor for EVE Online
4 # Copyright (c) 2010  Ryan and Danny Campbell, and the mEveMon Team
5 #
6 # mEveMon is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # mEveMon is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 #
19
20 import os
21 import time
22 import sys
23 import logging
24 import logging.handlers
25 import util
26
27 import hildon
28 import gtk
29 #conic is used for connection handling
30 import conic
31
32 from eveapi import eveapi
33 import fetchimg
34 import apicache
35 import file_settings as settings
36 from constants import LOGPATH, MAXBYTES, LOGCOUNT, CONFIG_DIR, IMG_CACHE_PATH
37 from constants import APICACHE_PATH
38 from constants import REQUIRED_ACCESS_MASK
39
40 #ugly hack to check maemo version. any better way?
41 if hasattr(hildon, "StackableWindow"):
42     from ui.fremantle import gui
43 else:
44     from ui.diablo import gui
45
46 class mEveMon:
47     """ The controller class for mEvemon. The intent is to help
48         abstract the EVE API and settings code from the UI code.
49     """
50     def __init__(self):
51         self.program = hildon.Program()
52         self.connect_to_network()
53         self.settings = settings.Settings()
54         self.cached_api = eveapi.EVEAPIConnection( cacheHandler = \
55                 apicache.cache_handler(debug=False))
56         self.gui = gui.mEveMonUI(self)
57         self.gui.run()
58
59     def run(self):
60         gtk.main()
61     
62     def quit(self, *args):
63         gtk.main_quit()
64
65     def get_auth(self, key_id):
66         """ Returns an authentication object to be used for eveapi calls
67             that require authentication.
68         """
69         ver_code = self.settings.get_ver_code(key_id)
70
71         try:
72             auth = self.cached_api.auth(keyID=key_id, vCode=ver_code)
73         except Exception, e:
74             self.gui.report_error(str(e))
75             logging.getLogger('mevemon').exception("Failed to get character name")
76             return None
77
78         return auth
79
80     def get_char_sheet(self, key_id, char_id):
81         """ Returns an object containing information about the character specified
82             by the provided character ID.
83         """
84         try:
85             sheet = self.get_auth(key_id).character(char_id).CharacterSheet()
86         except Exception, e:
87             self.gui.report_error(str(e))
88             logging.getLogger('mevemon').exception("Failed to get character name")
89             return None
90
91         return sheet
92
93     def charid2key_id(self, char_id):
94         """ Takes a character ID and returns the user ID of the account containing
95             the character.
96
97             Returns None if the character isn't found in any of the registered accounts.
98
99         """
100         acct_dict = self.settings.get_accounts()
101         
102         for key_id, ver_code in acct_dict.items():
103             auth = self.cached_api.auth(keyID=key_id, vCode=ver_code)
104             try:
105                 api_char_list = auth.account.Characters()
106                 characters = api_char_list.characters
107             except:
108                 characters = []
109
110             for character in characters:
111                 if character.characterID == char_id:
112                     return key_id
113
114     
115     def char_id2name(self, char_id):
116         """ Takes a character ID and returns the character name associated with
117             that ID.
118             The EVE API accepts a comma-separated list of IDs, but for now we
119             will just handle a single ID.
120         """
121         try:
122             chars = self.cached_api.eve.CharacterName(ids=char_id).characters
123             name = chars[0].characterName
124         except Exception, e:
125             self.gui.report_error(str(e))
126             logging.getLogger('mevemon').exception("Failed to get character name")
127             return None
128
129         return name
130
131     def char_name2id(self, name):
132         """ Takes the name of an EVE character and returns the characterID.
133         
134             The EVE api accepts a comma separated list of names, but for now
135             we will just handle single names/
136         """
137         try:
138             chars = self.cached_api.eve.CharacterID(names=name).characters
139             char_id = chars[0].characterID
140             char_name = chars[0].name
141         except Exception, e:
142             self.gui.report_error(str(e))
143             logging.getLogger('mevemon').exception("Failed to get ID")
144             return None
145
146         return char_id
147
148     def get_chars_from_acct(self, key_id):
149         """ Returns a list of characters associated with the provided user ID.
150         """
151         auth = self.get_auth(key_id)
152         if not auth:
153             return None
154         else:
155             try:
156                 api_char_list = auth.account.Characters()
157                 char_name_list = [char.name for char in api_char_list.characters]
158                 char_id_list = [char.characterID for char in api_char_list.characters]
159             except Exception, e:
160                 self.gui.report_error(str(e))
161                 logging.getLogger('mevemon').exception("Failed to get character list")
162                 return None
163
164         return char_name_list, char_id_list
165
166     def get_characters(self):
167         """ Returns a list of (character_name, image_path, key_id) tuples from all the
168             accounts that are registered to mEveMon.
169         
170             If there is an authentication issue, then instead of adding a valid
171             pair to the list, it appends an 'error message' 
172         """
173
174         ui_char_list = []
175         err_img = "/usr/share/mevemon/imgs/error.jpg"
176         err_txt = "Problem fetching info for account (or no accounts added)"
177         bad_key = "Incorrect key access. Your access mask should be %s." % REQUIRED_ACCESS_MASK
178
179         placeholder_chars = (err_txt, err_img, None)
180         
181         acct_dict = self.settings.get_accounts()
182         if not acct_dict:
183             return [placeholder_chars]
184
185         for key_id, ver_code in acct_dict.items():
186
187             char_names, char_ids = self.get_chars_from_acct(key_id)
188             
189             if not char_names:
190                 ui_char_list.append((err_txt + "\t(KEY_ID: %s)" % key_id, err_img, None))
191             else:
192
193
194                 # since there are char names, let's check the key
195                 # access and if it's bad we'll generate a key URL for
196                 # each character
197                 for char_name, char_id in zip(char_names, char_ids):
198                     if self.get_access_mask(key_id, ver_code) != REQUIRED_ACCESS_MASK:
199                         key_url = self.generate_access_mask_url(char_id)
200                         ui_char_list.append((bad_key, err_img, None))
201                     else:
202                         # append each char we get to the list we'll
203                         # return to the UI --danny
204                         ui_char_list.append((char_name, self.get_portrait(char_name, 64), key_id))
205         
206         return ui_char_list
207
208     def get_portrait(self, char_name, size):
209         """ Returns the file path of the retrieved portrait
210         """
211         char_id = self.char_name2id(char_name)
212         
213         return fetchimg.portrait_filename(char_id, size)
214
215     def get_skill_tree(self):
216         """ Returns an object from eveapi containing skill tree info
217         """
218         try:
219             tree = self.cached_api.eve.SkillTree()
220         except Exception, e:
221             self.gui.report_error(str(e))
222             logging.getLogger('mevemon').exception("Failed to get skill-in-training:")
223             return None
224         
225         return tree
226
227     def get_skill_in_training(self, key_id, char_id):
228         """ Returns an object from eveapi containing information about the
229             current skill in training
230         """
231         try:
232             # should this be accessing the cached object? (confused) --danny
233             skill = self.get_auth(key_id).character(char_id).SkillInTraining()
234         except Exception, e:
235             self.gui.report_error(str(e))
236             logging.getLogger('mevemon').exception("Failed to get skill-in-training:")
237             return None
238
239         return skill
240
241     def connection_cb(self, connection, event, mgc):
242         """ I'm not sure why we need this, but connection.connect() won't work
243             without it, even empty.
244         """
245         pass    
246
247
248     def connect_to_network(self):
249         """ This will connect to the default network if avaliable, or pop up the
250             connection dialog to select a connection.
251             Running this when we start the program ensures we are connected to a
252             network.
253         """
254         connection = conic.Connection()
255         #why 0xAA55?
256         connection.connect("connection-event", self.connection_cb, 0xAA55)
257         assert(connection.request_connection(conic.CONNECT_FLAG_NONE))
258
259
260     def get_sp(self, key_id, char_id):
261         """ Adds up the SP for all known skills, then calculates the SP gained
262             from an in-training skill.
263         """
264         actual_sp = 0
265         
266         sheet = self.get_char_sheet(key_id, char_id)
267         for skill in sheet.skills:
268             actual_sp += skill.skillpoints
269
270         live_sp = actual_sp + self.get_training_sp(key_id, char_id)
271
272         return live_sp
273
274     def get_spps(self, key_id, char_id):
275         """ Calculate and returns the skill points per hour for the given character.
276         """
277         skill = self.get_skill_in_training(key_id, char_id)
278         
279         if not skill.skillInTraining:
280             return (0, 0)
281
282         total_sp = skill.trainingDestinationSP - skill.trainingStartSP
283         total_time = skill.trainingEndTime - skill.trainingStartTime
284         
285         spps = float(total_sp) / total_time
286     
287         return (spps, skill.trainingStartTime)
288
289     def get_training_sp(self, key_id, char_id):
290         """ returns the additional SP that the in-training skill has acquired
291         """
292         spps_tuple = self.get_spps(key_id, char_id)
293         
294         if not spps_tuple:
295             return 0
296         spps, start_time = spps_tuple
297         eve_time = time.time() #evetime is utc, right?
298         time_diff =  eve_time - start_time
299
300         return (spps * time_diff) 
301
302     def clear_cache(self):
303         """ Clears all cached data (images and eveapi cache) """
304         try:
305             util.clean_dir(IMG_CACHE_PATH)
306             util.clean_dir(APICACHE_PATH)
307         except OSError, e:
308             logging.getLogger('mevemon').exception("Failed to clear cache")
309
310     def get_access_mask(self, key_id, ver_code):
311
312         """
313         Returns the access mask that determines what data we have
314         access to on the account.
315
316         """
317
318         return self.cached_api.account.APIKeyInfo(keyID=key_id, vCode=ver_code).key.accessMask
319
320     def generate_access_mask_url(self, char_id):
321
322         """
323         Generates a URL to send the user to the page that will help
324         them create an access key with exactly the access mevemon
325         needs.
326         """
327
328         return "https://supporttest.eveonline.com/api/Key/CreatePredefined/%s/%s/false" % (REQUIRED_ACCESS_MASK, char_id)
329
330 def excepthook(ex_type, value, tb):
331     """ a replacement for the default exception handler that logs errors"""
332     logging.getLogger("mevemon").error('Uncaught exception:', 
333                       exc_info=(ex_type, value, tb))
334
335 def setupLogger():
336     """ sets up the logging """
337     if not os.path.exists(CONFIG_DIR):
338         os.makedirs(CONFIG_DIR)
339
340     logger = logging.getLogger("mevemon")
341     logger.setLevel(logging.DEBUG)
342     
343     fileHandler = logging.handlers.RotatingFileHandler(LOGPATH,
344                                                     maxBytes=MAXBYTES,
345                                                     backupCount=LOGCOUNT)
346     file_fmt = logging.Formatter('%(asctime)s %(name)-10s %(levelname)-5s %(message)s')
347     console_fmt = logging.Formatter('%(name)-10s %(levelname)-5s %(message)s')
348     fileHandler.setFormatter(file_fmt)
349     logger.addHandler(fileHandler)
350
351     #create console handler
352     console = logging.StreamHandler()
353     console.setLevel(logging.DEBUG)
354     console.setFormatter(console_fmt)
355     logger.addHandler(console)
356     logger.debug("Logging successfully set-up.")
357
358
359 if __name__ == "__main__":
360     setupLogger()
361     sys.excepthook = excepthook
362     app = mEveMon()
363     try:
364         app.run()
365     except KeyboardInterrupt:
366         sys.exit(0)