2021-09-19 16:20:56 +02:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# pyright: reportUndefinedVariable=false
2021-09-26 12:56:53 +02:00
import os , base64 , traceback
2021-09-20 15:05:07 +02:00
from lxml import etree
2021-10-04 15:36:25 +02:00
import time , datetime
2021-09-19 16:20:56 +02:00
from PyQt5 . Qt import ( Qt , QWidget , QHBoxLayout , QVBoxLayout , QLabel , QLineEdit ,
2021-09-20 15:05:07 +02:00
QGroupBox , QPushButton , QListWidget , QListWidgetItem , QInputDialog ,
QLineEdit , QAbstractItemView , QIcon , QDialog , QDialogButtonBox , QUrl )
2021-09-19 16:20:56 +02:00
2021-10-04 15:36:25 +02:00
from PyQt5 import QtCore
2021-09-19 16:20:56 +02:00
from PyQt5 import Qt as QtGui
from zipfile import ZipFile
# calibre modules and constants.
2021-09-28 18:43:14 +02:00
from calibre . gui2 import ( question_dialog , error_dialog , info_dialog , choose_save_file , choose_files ) # type: ignore
2021-09-19 16:20:56 +02:00
# modules from this plugin's zipfile.
from calibre_plugins . deacsm . __init__ import PLUGIN_NAME , PLUGIN_VERSION # type: ignore
import calibre_plugins . deacsm . prefs as prefs # type: ignore
2021-09-20 11:24:11 +02:00
from calibre . utils . config import config_dir # type: ignore
2021-09-19 16:20:56 +02:00
class ConfigWidget ( QWidget ) :
def __init__ ( self , plugin_path ) :
QWidget . __init__ ( self )
self . plugin_path = plugin_path
# get the prefs
self . deacsmprefs = prefs . DeACSM_Prefs ( )
# make a local copy
self . tempdeacsmprefs = { }
self . tempdeacsmprefs [ ' path_to_account_data ' ] = self . deacsmprefs [ ' path_to_account_data ' ]
2021-10-04 15:36:25 +02:00
self . tempdeacsmprefs [ ' notify_fulfillment ' ] = self . deacsmprefs [ ' notify_fulfillment ' ]
self . tempdeacsmprefs [ ' list_of_rented_books ' ] = self . deacsmprefs [ ' list_of_rented_books ' ]
2021-09-19 16:20:56 +02:00
# Start Qt Gui dialog layout
layout = QVBoxLayout ( self )
self . setLayout ( layout )
2021-09-20 15:05:07 +02:00
ua_group_box = QGroupBox ( _ ( ' Account information: ' ) , self )
2021-09-19 16:20:56 +02:00
layout . addWidget ( ua_group_box )
ua_group_box_layout = QVBoxLayout ( )
ua_group_box . setLayout ( ua_group_box_layout )
2021-09-28 18:43:14 +02:00
info_string , activated , mail = self . get_account_info ( )
2021-09-20 15:05:07 +02:00
self . lblAccInfo = QtGui . QLabel ( self )
self . lblAccInfo . setText ( info_string )
ua_group_box_layout . addWidget ( self . lblAccInfo )
if not activated :
self . button_link_account = QtGui . QPushButton ( self )
self . button_link_account . setText ( _ ( " Link to ADE account " ) )
self . button_link_account . clicked . connect ( self . link_account )
ua_group_box_layout . addWidget ( self . button_link_account )
2021-09-19 16:20:56 +02:00
2021-09-28 18:43:14 +02:00
self . button_import_activation = QtGui . QPushButton ( self )
self . button_import_activation . setText ( _ ( " Import existing activation data (ZIP) " ) )
self . button_import_activation . clicked . connect ( self . import_activation )
ua_group_box_layout . addWidget ( self . button_import_activation )
2021-09-20 11:24:11 +02:00
self . button_export_key = QtGui . QPushButton ( self )
2021-09-20 15:05:07 +02:00
self . button_export_key . setText ( _ ( " Export account encryption key " ) )
2021-09-20 11:24:11 +02:00
self . button_export_key . clicked . connect ( self . export_key )
2021-09-20 15:05:07 +02:00
self . button_export_key . setEnabled ( activated )
2021-09-20 11:24:11 +02:00
ua_group_box_layout . addWidget ( self . button_export_key )
2021-09-20 15:05:07 +02:00
self . button_export_activation = QtGui . QPushButton ( self )
self . button_export_activation . setText ( _ ( " Export account activation data " ) )
self . button_export_activation . clicked . connect ( self . export_activation )
self . button_export_activation . setEnabled ( activated )
ua_group_box_layout . addWidget ( self . button_export_activation )
2021-10-04 15:36:25 +02:00
self . button_rented_books = QtGui . QPushButton ( self )
self . button_rented_books . setText ( _ ( " Show loaned books " ) )
self . button_rented_books . clicked . connect ( self . show_rented_books )
self . button_rented_books . setEnabled ( activated )
ua_group_box_layout . addWidget ( self . button_rented_books )
if ( len ( self . deacsmprefs [ " list_of_rented_books " ] ) == 0 ) :
self . button_rented_books . setEnabled ( False )
self . chkNotifyFulfillment = QtGui . QCheckBox ( " Notify ACS server after successful fulfillment " )
self . chkNotifyFulfillment . setToolTip ( " Default: True \n \n If this is enabled, the ACS server will receive a notification once the ACSM has successfully been converted. \n This is not strictly necessary, but it is what ADE does, so it ' s probably safer to just do it as well. \n Also, it is required to be able to return loaned books. " )
self . chkNotifyFulfillment . setChecked ( self . tempdeacsmprefs [ " notify_fulfillment " ] )
layout . addWidget ( self . chkNotifyFulfillment )
2021-09-28 18:43:14 +02:00
2021-09-25 16:24:03 +02:00
try :
from calibre_plugins . deacsm . libadobe import VAR_HOBBES_VERSION , createDeviceKeyFile , update_account_path
from calibre_plugins . deacsm . libadobeAccount import createDeviceFile , createUser , signIn , activateDevice
except :
try :
from libadobe import VAR_HOBBES_VERSION , createDeviceKeyFile , update_account_path
from libadobeAccount import createDeviceFile , createUser , signIn , activateDevice
except :
2021-09-26 12:56:53 +02:00
print ( " {0} v {1} : Error while importing Account stuff " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
traceback . print_exc ( )
2021-09-25 16:24:03 +02:00
update_account_path ( self . deacsmprefs [ " path_to_account_data " ] )
2021-09-19 16:20:56 +02:00
self . resize ( self . sizeHint ( ) )
2021-09-20 15:05:07 +02:00
def get_account_info ( self ) :
activation_xml_path = os . path . join ( self . deacsmprefs [ " path_to_account_data " ] , " activation.xml " )
device_xml_path = os . path . join ( self . deacsmprefs [ " path_to_account_data " ] , " device.xml " )
container = None
try :
container = etree . parse ( activation_xml_path )
containerdev = etree . parse ( device_xml_path )
except ( FileNotFoundError , OSError ) as e :
2021-09-28 18:43:14 +02:00
return " Not authorized for any ADE ID " , False , None
2021-09-20 15:05:07 +02:00
try :
adeptNS = lambda tag : ' { %s } %s ' % ( ' http://ns.adobe.com/adept ' , tag )
usernameXML = container . find ( adeptNS ( " credentials " ) ) . find ( adeptNS ( " username " ) )
devicenameXML = containerdev . find ( adeptNS ( " deviceName " ) )
ade_type = usernameXML . get ( ' method ' , " unknown " )
ade_mail = usernameXML . text
ade_device_name = devicenameXML . text
2021-09-25 16:24:03 +02:00
if container . find ( adeptNS ( " activationToken " ) ) == None :
2021-09-28 18:43:14 +02:00
return " ADE authorization seems to be corrupted (activationToken missing) " , False , None
2021-09-25 16:24:03 +02:00
if container . find ( adeptNS ( " credentials " ) ) . find ( adeptNS ( " pkcs12 " ) ) == None :
2021-09-28 18:43:14 +02:00
return " ADE authorization seems to be corrupted (pkcs12 missing) " , False , None
2021-09-25 16:24:03 +02:00
2021-09-28 18:43:14 +02:00
return " Authorized with ADE ID ( " + ade_type + " ) " + ade_mail + " \n on device " + ade_device_name , True , ade_mail
2021-09-20 15:05:07 +02:00
except :
2021-09-28 18:43:14 +02:00
return " ADE authorization seems to be corrupted " , False , None
2021-09-20 15:05:07 +02:00
def export_activation ( self ) :
filters = [ ( " ZIP " , [ " zip " ] ) ]
2021-09-28 18:43:14 +02:00
filename = choose_save_file ( self , " Export ADE activation files " , _ ( " Export ADE activation files " ) ,
filters , all_files = False , initial_filename = " adobe_account_backup.zip " )
2021-09-20 15:05:07 +02:00
2021-09-20 15:18:12 +02:00
if ( filename is None ) :
return
2021-09-26 12:56:53 +02:00
print ( " {0} v {1} : Exporting activation data to {2} " . format ( PLUGIN_NAME , PLUGIN_VERSION , filename ) )
2021-09-20 15:05:07 +02:00
try :
with ZipFile ( filename , ' w ' ) as zipfile :
zipfile . write ( os . path . join ( self . deacsmprefs [ " path_to_account_data " ] , " device.xml " ) , " device.xml " )
zipfile . write ( os . path . join ( self . deacsmprefs [ " path_to_account_data " ] , " activation.xml " ) , " activation.xml " )
zipfile . write ( os . path . join ( self . deacsmprefs [ " path_to_account_data " ] , " devicesalt " ) , " devicesalt " )
except :
return error_dialog ( None , " Export failed " , " Export failed. " , show = True , show_copy_button = False )
2021-09-28 18:43:14 +02:00
def import_activation ( self ) :
filters = [ ( " ZIP " , [ " zip " ] ) ]
filenames = choose_files ( self , " Import ADE activation file (ZIP) " , _ ( " Import ADE activation file (ZIP) " ) ,
filters , all_files = False , select_only_single_file = True )
try :
filename = filenames [ 0 ]
if ( filename is None ) :
return
except :
return
print ( " {0} v {1} : Importing activation data from {2} " . format ( PLUGIN_NAME , PLUGIN_VERSION , filename ) )
with ZipFile ( filename , ' r ' ) as zipfile :
try :
device = zipfile . read ( " device.xml " )
activation = zipfile . read ( " activation.xml " )
salt = zipfile . read ( " devicesalt " )
except :
return error_dialog ( None , " Import failed " , " Can ' t find required files in this ZIP " )
try :
output_device = open ( os . path . join ( self . deacsmprefs [ " path_to_account_data " ] , " device.xml " ) , " w " )
output_device . write ( device . decode ( " utf-8 " ) )
output_device . close ( )
output_activation = open ( os . path . join ( self . deacsmprefs [ " path_to_account_data " ] , " activation.xml " ) , " w " )
output_activation . write ( activation . decode ( " utf-8 " ) )
output_activation . close ( )
output_salt = open ( os . path . join ( self . deacsmprefs [ " path_to_account_data " ] , " devicesalt " ) , " wb " )
output_salt . write ( salt )
output_salt . close ( )
except :
2021-10-01 14:32:46 +02:00
err = traceback . format_exc ( )
2021-09-28 18:43:14 +02:00
return error_dialog ( None , " Import failed " , " Can ' t write file " , show = True , det_msg = err , show_copy_button = False )
# update display
info_string , activated , ade_mail = self . get_account_info ( )
self . lblAccInfo . setText ( info_string )
self . button_link_account . setEnabled ( not activated )
self . button_import_activation . setEnabled ( not activated )
self . button_export_key . setEnabled ( activated )
self . button_export_activation . setEnabled ( activated )
info_dialog ( None , " Done " , " Successfully imported authorization for " + ade_mail , show = True , show_copy_button = False )
2021-09-20 15:05:07 +02:00
def link_account ( self ) :
2021-09-25 16:24:03 +02:00
try :
from calibre_plugins . deacsm . libadobe import VAR_HOBBES_VERSION , createDeviceKeyFile , update_account_path
from calibre_plugins . deacsm . libadobeAccount import createDeviceFile , createUser , signIn , activateDevice
except :
2021-09-20 15:05:07 +02:00
try :
2021-09-25 16:24:03 +02:00
from libadobe import VAR_HOBBES_VERSION , createDeviceKeyFile , update_account_path
from libadobeAccount import createDeviceFile , createUser , signIn , activateDevice
2021-09-20 15:05:07 +02:00
except :
2021-09-26 12:56:53 +02:00
print ( " {0} v {1} : Error while importing Account stuff " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
traceback . print_exc ( )
2021-09-20 15:05:07 +02:00
2021-09-25 16:24:03 +02:00
update_account_path ( self . deacsmprefs [ " path_to_account_data " ] )
mail , ok = QInputDialog . getText ( self , " Authorizing ADE account " , " Please enter mail address " )
2021-09-20 15:05:07 +02:00
2021-09-25 16:24:03 +02:00
if ( not ok or mail is None or len ( mail ) == 0 ) :
return
2021-09-20 15:05:07 +02:00
2021-09-25 16:24:03 +02:00
passwd , ok = QInputDialog . getText ( self , " Authorizing ADE account " , " Please enter password " , QLineEdit . Password )
2021-09-20 15:05:07 +02:00
2021-09-25 16:24:03 +02:00
if ( not ok or passwd is None or len ( passwd ) == 0 ) :
return
createDeviceKeyFile ( )
createDeviceFile ( VAR_HOBBES_VERSION , False )
success , resp = createUser ( )
if ( success is False ) :
return error_dialog ( None , " ADE activation failed " , " Couldn ' t create user " , det_msg = str ( resp ) , show = True , show_copy_button = True )
2021-09-20 15:05:07 +02:00
2021-09-25 16:24:03 +02:00
success , resp = signIn ( mail , passwd )
if ( success is False ) :
return error_dialog ( None , " ADE activation failed " , " Login unsuccessful " , det_msg = str ( resp ) , show = True , show_copy_button = True )
2021-09-20 15:05:07 +02:00
2021-09-25 16:24:03 +02:00
success , resp = activateDevice ( )
if ( success is False ) :
return error_dialog ( None , " ADE activation failed " , " Couldn ' t activate device " , det_msg = str ( resp ) , show = True , show_copy_button = True )
2021-09-20 15:05:07 +02:00
2021-09-25 16:24:03 +02:00
print ( " Authorized to account " + mail )
2021-09-20 15:05:07 +02:00
# update display
2021-09-28 18:43:14 +02:00
info_string , activated , mail = self . get_account_info ( )
2021-09-20 15:05:07 +02:00
self . lblAccInfo . setText ( info_string )
self . button_link_account . setEnabled ( False )
2021-09-28 18:43:14 +02:00
self . button_import_activation . setEnabled ( False )
2021-09-20 15:05:07 +02:00
self . button_export_key . setEnabled ( True )
self . button_export_activation . setEnabled ( True )
2021-09-25 16:24:03 +02:00
info_dialog ( None , " Done " , " Authorized to account " + mail , show = True , show_copy_button = False )
2021-09-20 15:05:07 +02:00
2021-09-20 11:24:11 +02:00
def export_key ( self ) :
2021-09-26 12:56:53 +02:00
try :
from calibre_plugins . deacsm . libadobe import update_account_path
from calibre_plugins . deacsm . libadobeAccount import exportAccountEncryptionKeyDER
except :
try :
from libadobe import update_account_path
from libadobeAccount import exportAccountEncryptionKeyDER
except :
print ( " {0} v {1} : Error while importing Account stuff " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
traceback . print_exc ( )
update_account_path ( self . deacsmprefs [ " path_to_account_data " ] )
2021-09-20 11:24:11 +02:00
filters = [ ( " DER Files " , [ " der " ] ) ]
filename = choose_save_file ( self , " Export ADE keys " , _ ( " Export ADE keys " ) , filters , all_files = False )
2021-09-20 15:18:12 +02:00
if ( filename is None ) :
return
2021-09-26 12:56:53 +02:00
print ( " {0} v {1} : Exporting encryption key to {2} " . format ( PLUGIN_NAME , PLUGIN_VERSION , filename ) )
2021-09-20 11:24:11 +02:00
2021-09-26 12:56:53 +02:00
ret = exportAccountEncryptionKeyDER ( filename )
2021-09-20 11:24:11 +02:00
2021-09-26 12:56:53 +02:00
if ret :
return info_dialog ( None , " Done " , " Key successfully exported " , show = True , show_copy_button = False )
else :
return error_dialog ( None , " Export failed " , " Export failed " , show = True , show_copy_button = False )
2021-09-20 11:24:11 +02:00
2021-09-19 16:20:56 +02:00
def save_settings ( self ) :
2021-09-20 15:05:07 +02:00
#self.deacsmprefs.set('path_to_account_data', self.txtboxUA.text())
2021-10-04 15:36:25 +02:00
self . deacsmprefs . set ( ' notify_fulfillment ' , self . chkNotifyFulfillment . isChecked ( ) )
2021-09-19 16:20:56 +02:00
self . deacsmprefs . writeprefs ( )
def load_resource ( self , name ) :
with ZipFile ( self . plugin_path , ' r ' ) as zf :
if name in zf . namelist ( ) :
return zf . read ( name ) . decode ( ' utf-8 ' )
return " "
2021-10-04 15:36:25 +02:00
def show_rented_books ( self ) :
d = RentedBooksDialog ( self , self . deacsmprefs [ " list_of_rented_books " ] )
d . exec_ ( )
class RentedBooksDialog ( QDialog ) :
def __init__ ( self , parent , booklist ) :
QDialog . __init__ ( self , parent )
self . parent = parent
self . setWindowTitle ( " DeACSM: Manage loaned Books " )
# Start Qt Gui dialog layout
layout = QVBoxLayout ( self )
self . setLayout ( layout )
keys_group_box = QGroupBox ( " List of loaned books " , self )
layout . addWidget ( keys_group_box )
keys_group_box_layout = QHBoxLayout ( )
keys_group_box . setLayout ( keys_group_box_layout )
self . listy = QListWidget ( self )
self . listy . setToolTip ( " List of loaned books " )
self . listy . setSelectionMode ( QAbstractItemView . SingleSelection )
self . populate_list ( )
keys_group_box_layout . addWidget ( self . listy )
button_layout = QVBoxLayout ( )
keys_group_box_layout . addLayout ( button_layout )
self . _add_key_button = QtGui . QToolButton ( self )
self . _add_key_button . setIcon ( QIcon ( I ( ' view-refresh.png ' ) ) )
self . _add_key_button . setToolTip ( " Return book to library " )
self . _add_key_button . clicked . connect ( self . return_book )
button_layout . addWidget ( self . _add_key_button )
self . _delete_key_button = QtGui . QToolButton ( self )
self . _delete_key_button . setToolTip ( _ ( " Delete book entry from list " ) )
self . _delete_key_button . setIcon ( QIcon ( I ( ' list_remove.png ' ) ) )
self . _delete_key_button . clicked . connect ( self . delete_book_entry )
button_layout . addWidget ( self . _delete_key_button )
self . lblAccInfo = QtGui . QLabel ( self )
self . lblAccInfo . setText ( " Click the arrow button to return a loaned book to the library. \n Click the red X to delete the loan record without returning the book. " )
layout . addWidget ( self . lblAccInfo )
self . button_box = QDialogButtonBox ( QDialogButtonBox . Ok | QDialogButtonBox . Cancel )
self . button_box . accepted . connect ( self . accept )
self . button_box . rejected . connect ( self . reject )
layout . addWidget ( self . button_box )
self . resize ( self . sizeHint ( ) )
def td_format ( self , td_object ) :
seconds = int ( td_object . total_seconds ( ) )
periods = [
( ' y ' , 60 * 60 * 24 * 365 ) ,
( ' M ' , 60 * 60 * 24 * 30 ) ,
( ' d ' , 60 * 60 * 24 ) ,
( ' h ' , 60 * 60 ) ,
( ' m ' , 60 ) ,
( ' s ' , 1 )
]
strings = [ ]
tick = 0
for period_name , period_seconds in periods :
if seconds > period_seconds :
period_value , seconds = divmod ( seconds , period_seconds )
strings . append ( " %s %s " % ( period_value , period_name ) )
tick + = 1
if tick > = 2 :
break
return " " . join ( strings )
def populate_list ( self ) :
self . listy . clear ( )
overdue_books = [ ]
for book in self . parent . deacsmprefs [ " list_of_rented_books " ] :
try :
book_time_stamp = book [ " validUntil " ]
timestamp = datetime . datetime . strptime ( book_time_stamp , " % Y- % m- %d T % H: % M: % SZ " )
currenttime = datetime . datetime . utcnow ( )
except :
print ( " Invalid book timestamp " )
continue
if ( timestamp < = currenttime ) :
# Book is overdue, no need to return. Delete from list.
overdue_books . append ( book )
continue
else :
info = " ( " + self . td_format ( timestamp - currenttime )
info + = " remaining) "
item = QListWidgetItem ( book [ " book_name " ] + " " + info )
item . setData ( QtCore . Qt . UserRole , book [ " loanID " ] )
self . listy . addItem ( item )
for book in overdue_books :
self . parent . deacsmprefs [ " list_of_rented_books " ] . remove ( book )
self . parent . deacsmprefs . writeprefs ( )
def return_book ( self ) :
if not self . listy . currentItem ( ) :
return
userdata = str ( self . listy . currentItem ( ) . data ( QtCore . Qt . UserRole ) )
print ( " Returning book %s (ID %s ) " % ( self . listy . currentItem ( ) . text ( ) , userdata ) )
try :
from calibre_plugins . deacsm . libadobeFulfill import tryReturnBook
except :
try :
from libadobeFulfill import tryReturnBook
except :
print ( " {0} v {1} : Error while importing book return stuff " . format ( PLUGIN_NAME , PLUGIN_VERSION ) )
traceback . print_exc ( )
Ret_book = None
for book in self . parent . deacsmprefs [ " list_of_rented_books " ] :
if book [ " loanID " ] == userdata :
Ret_book = book
break
if Ret_book is None :
return
ret , msg = tryReturnBook ( Ret_book )
if ( ret ) :
print ( " Book successfully returned: " )
print ( msg )
self . delete_book_entry ( nomsg = True )
self . populate_list ( )
return info_dialog ( None , " Done " , " Book successfully returned " , show = True , show_copy_button = False )
else :
print ( " Book return failed: " )
print ( msg )
return error_dialog ( None , " Error " , " Book return failed " , det_msg = msg , show = True , show_copy_button = False )
def delete_book_entry ( self , nomsg = False ) :
if not self . listy . currentItem ( ) :
return
userdata = str ( self . listy . currentItem ( ) . data ( QtCore . Qt . UserRole ) )
print ( " Deleting book entry %s (ID %s ) " % ( self . listy . currentItem ( ) . text ( ) , userdata ) )
success = False
for book in self . parent . deacsmprefs [ " list_of_rented_books " ] :
if book [ " loanID " ] == userdata :
self . parent . deacsmprefs [ " list_of_rented_books " ] . remove ( book )
success = True
break
self . populate_list ( )
if success and not nomsg :
return info_dialog ( None , " Done " , " Book entry deleted without returning. " , show = True , show_copy_button = False )
if not nomsg :
return error_dialog ( None , " Error " , " Error while deleting book entry " , show = True , show_copy_button = False )