2021-04-17 10:51:56 -05:00
#PlChat is a pleroma chat client
# Copyright (C) 2021 Knott Eye
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
2021-04-16 21:00:21 -05:00
import platform , keyring
os = platform . system ( )
if os == " Windows " :
import keyring . backends . Windows
keyring . set_keyring ( keyring . backends . Windows . WinVaultKeyring ( ) )
elif os == " Darwin " :
import keyring . backends . OS_X
keyring . set_keyring ( keyring . backends . OS_X . Keyring ( ) )
else :
# Probably linux or BSD so we'll just go for it
import keyring . backends . SecretService
keyring . set_keyring ( keyring . backends . SecretService . Keyring ( ) )
2021-04-17 10:51:56 -05:00
from PyQt5 . QtGui import *
from PyQt5 . QtCore import *
from PyQt5 . QtWidgets import *
2021-04-15 15:16:38 -05:00
from notifypy import Notify
2021-04-16 21:00:21 -05:00
import sys , threading , queue , asyncio , urllib , appdirs , os , time , pleroma , re , magic , monkeypatch , requests , misc , json , timeconvert , videowidget , audiowidget
2021-04-15 15:16:38 -05:00
CACHE = appdirs . AppDirs ( ' plchat ' , ' plchat ' ) . user_cache_dir
APPDATA = appdirs . AppDirs ( ' plchat ' , ' plchat ' ) . user_data_dir
THREADS = { }
STATIC_PREF = ' '
2021-04-17 11:13:26 -05:00
ICON_PATH = os . path . join ( os . path . dirname ( __file__ ) , " fedi.svg " )
2021-04-25 15:48:15 -05:00
ICON_PATH_COLOR = os . path . join ( os . path . dirname ( __file__ ) , " fedi_color.svg " )
NOTIF_SOUND = os . path . join ( os . path . dirname ( __file__ ) , ' notif.wav ' )
2021-04-15 15:16:38 -05:00
class App ( QMainWindow ) :
2021-04-17 10:40:06 -05:00
settings = QSettings ( APPDATA + " /settings.ini " )
2021-04-15 15:16:38 -05:00
_eventloop = asyncio . new_event_loop ( )
accts = { }
def __init__ ( self , processEvents ) :
super ( ) . __init__ ( )
self . title = ' PlChat '
self . processEvents = processEvents
self . initUI ( )
def initUI ( self ) :
self . setWindowTitle ( self . title )
2021-04-25 15:48:15 -05:00
if self . settings . value ( ' colorIcon ' , type = bool ) :
self . setWindowIcon ( QIcon ( QPixmap ( ICON_PATH_COLOR ) ) )
else :
self . setWindowIcon ( QIcon ( QPixmap ( ICON_PATH ) ) )
2021-04-15 15:16:38 -05:00
self . setGeometry ( self . settings . value ( ' left ' , type = int ) or 10 , self . settings . value ( ' top ' , type = int ) or 10 , self . settings . value ( ' width ' , type = int ) or 640 , self . settings . value ( ' height ' , type = int ) or 480 )
self . _exit = False
2021-04-17 23:34:34 -05:00
self . totpReady = False
2021-04-15 15:16:38 -05:00
self . Err = QErrorMessage ( )
exitAction = QAction ( ' &Exit ' , self )
exitAction . setShortcut ( ' Ctrl+Q ' )
exitAction . setToolTip ( ' Exit application ' )
exitAction . triggered . connect ( self . exitActionTrigger )
newChatAction = QAction ( ' New Chat ' , self , triggered = self . newChatDialog )
newChatAction . setShortcut ( ' Ctrl+N ' )
newChatAction . setToolTip ( ' Add a new chat ' )
closeChatAction = QAction ( ' Close Chat ' , self , triggered = self . closeTab )
closeChatAction . setShortcut ( ' Ctrl+W ' )
closeChatAction . setToolTip ( ' Close the current chat tab ' )
newAcctAction = QAction ( ' New Account ' , self , triggered = self . newAcctDialog )
newAcctAction . setShortcut ( ' Ctrl+Shift+N ' )
newAcctAction . setToolTip ( ' Add a new account ' )
logoutAction = QAction ( ' Logout ' , self , triggered = self . logout )
logoutAction . setToolTip ( ' Log out of the current account ' )
reopenAction = QAction ( ' Reopen All Chats ' , self , triggered = self . reopenAll )
reopenAction . setToolTip ( ' In case something breaks ' )
contactAction = QAction ( ' Contact ' , self , triggered = self . contactDialog )
licenseAction = QAction ( ' License ' , self , triggered = self . licenseDialog )
self . closeToTrayAction = QAction ( ' Close To System Tray ' , self , checkable = True )
self . closeToTrayAction . setChecked ( self . settings . value ( ' closeToTray ' , type = bool ) )
self . openInTrayAction = QAction ( ' Open In System Tray ' , self , checkable = True )
self . openInTrayAction . setChecked ( self . settings . value ( ' openInTray ' , type = bool ) )
self . openInTrayAction . setToolTip ( ' Does nothing if not also closing to tray. ' )
self . animatePicturesAction = QAction ( ' Display Animations ' , self , checkable = True )
self . animatePicturesAction . setChecked ( self . settings . value ( ' animatePictures ' , type = bool ) )
self . animatePicturesAction . changed . connect ( updateAnimationPref )
self . fetchBackgroundsAction = QAction ( " Fetch Backgrounds " , self , checkable = True )
self . fetchBackgroundsAction . setChecked ( self . settings . value ( ' fetchBackgrounds ' , type = bool ) )
self . fetchBackgroundsAction . setToolTip ( " Most instances do not set this in a way that PlChat can check for. It will (probably) not work like you expect, but there is nothing I can do about it. " )
self . darkModeAction = QAction ( " Dark Mode " , self , checkable = True )
self . darkModeAction . setChecked ( self . settings . value ( ' darkMode ' , type = bool ) )
self . darkModeAction . setToolTip ( " Only affects chat bubbles " )
2021-04-25 15:48:15 -05:00
self . colorIconAction = QAction ( " Use Color Icon " , self , checkable = True , triggered = self . useColorIcon )
self . colorIconAction . setChecked ( self . settings . value ( ' colorIcon ' , type = bool ) )
2021-04-15 15:16:38 -05:00
self . sendNotificationsAction = QAction ( " Send Notifications " , self , checkable = True )
self . sendNotificationsAction . setChecked ( self . settings . value ( ' sendNotifications ' , type = bool ) )
2021-04-25 15:48:15 -05:00
self . silenceNotifsAction = QAction ( " Silence Notifications " , self , checkable = True , triggered = self . silenceNotifs )
self . silenceNotifsAction . setChecked ( self . settings . value ( ' silenceNotifications ' , type = bool ) )
2021-04-15 15:16:38 -05:00
self . fetchHeadersAction = QAction ( " Fetch Headers " , self , checkable = True )
self . fetchHeadersAction . setChecked ( self . settings . value ( ' fetchHeaders ' , type = bool ) )
self . twoFourTimeAction = QAction ( " Display 24-Hour Time " , self , checkable = True )
self . twoFourTimeAction . setChecked ( self . settings . value ( ' twoFourTime ' , type = bool ) )
self . acctComboBox = QComboBox ( self )
self . acctComboBox . currentIndexChanged . connect ( self . acctSwitch )
acctComboBoxAction = QWidgetAction ( self )
acctComboBoxAction . setDefaultWidget ( self . acctComboBox )
self . acctComboBox . show ( )
menubar = self . menuBar ( )
if self . closeToTrayAction . isChecked ( ) :
systraymenu = QMenu ( )
hideAction = QAction ( " Hide " , self , triggered = self . hide )
showAction = QAction ( " Show " , self , triggered = self . show )
systraymenu . addAction ( showAction )
systraymenu . addAction ( hideAction )
2021-04-25 15:48:15 -05:00
systraymenu . addAction ( self . silenceNotifsAction )
2021-04-15 15:16:38 -05:00
systraymenu . addAction ( exitAction )
self . trayIcon = QSystemTrayIcon ( )
2021-04-25 15:48:15 -05:00
if self . settings . value ( ' colorIcon ' , type = bool ) :
self . trayIcon . setIcon ( QIcon ( ICON_PATH_COLOR ) )
else :
self . trayIcon . setIcon ( QIcon ( ICON_PATH ) )
2021-04-15 15:16:38 -05:00
self . trayIcon . setVisible ( True )
self . trayIcon . setToolTip ( " PlChat " )
self . trayIcon . setContextMenu ( systraymenu )
self . trayIcon . activated . connect ( self . systrayClicked )
else :
self . trayIcon = None
filemenu = menubar . addMenu ( " File " )
filemenu . addAction ( newChatAction )
filemenu . addAction ( closeChatAction )
filemenu . addAction ( newAcctAction )
filemenu . addAction ( exitAction )
editmenu = menubar . addMenu ( " Edit " )
editmenu . addAction ( reopenAction )
editmenu . setToolTipsVisible ( True )
prefsmenu = editmenu . addMenu ( " Preferences " )
prefsmenu . addAction ( self . closeToTrayAction )
prefsmenu . addAction ( self . openInTrayAction )
prefsmenu . addAction ( self . animatePicturesAction )
prefsmenu . addAction ( self . darkModeAction )
2021-04-25 15:48:15 -05:00
prefsmenu . addAction ( self . colorIconAction )
2021-04-15 15:16:38 -05:00
prefsmenu . addAction ( self . sendNotificationsAction )
2021-04-25 15:48:15 -05:00
prefsmenu . addAction ( self . silenceNotifsAction )
2021-04-15 15:16:38 -05:00
prefsmenu . addAction ( self . fetchBackgroundsAction )
prefsmenu . addAction ( self . fetchHeadersAction )
prefsmenu . addAction ( self . twoFourTimeAction )
prefsmenu . setToolTipsVisible ( True )
accountmenu = menubar . addMenu ( ' Accounts ' )
accountmenu . addAction ( acctComboBoxAction )
accountmenu . addSeparator ( )
accountmenu . addAction ( logoutAction )
aboutmenu = menubar . addMenu ( ' About ' )
aboutmenu . addAction ( contactAction )
aboutmenu . addAction ( licenseAction )
self . tabs = QTabWidget ( )
self . tabs . setMovable ( True )
self . tabs . setTabsClosable ( True )
self . tabs . tabCloseRequested . connect ( self . closeTab )
self . tabs . currentChanged . connect ( self . changeTab )
self . setFocusProxy ( self . tabs )
self . setCentralWidget ( self . tabs )
if not self . openInTrayAction . isChecked ( ) or not self . closeToTrayAction . isChecked ( ) :
self . show ( )
self . HeaderFont = QFont ( )
self . HeaderFont . setPointSize ( 16 )
self . headerEmojiFontSize = QFontMetrics ( self . HeaderFont ) . height ( )
self . defaultFontMetrics = QFontMetrics ( QFont ( ) )
self . emojiFontSize = self . defaultFontMetrics . height ( )
self . TimestampFont = QFont ( )
self . TimestampFont . setPointSize ( 7 )
self . installEventFilter ( self )
asyncio . set_event_loop ( self . _eventloop )
self . _eventloop . call_soon ( self . eventLoop )
acctList = self . settings . value ( ' acctList ' , type = list )
if not acctList :
self . newAcctDialog ( )
return
2021-04-25 15:06:00 -05:00
for ind in range ( 0 , len ( acctList ) ) :
CallThread ( getAvi , None , acctList [ ind ] [ ' instance ' ] )
try :
self . initAcct ( acctList [ ind ] [ ' instance ' ] , acctList [ ind ] [ ' username ' ] )
except :
print ( " account info corrupted, deleting " )
del acctList [ ind ]
if acctList :
self . settings . setValue ( ' acctList ' , acctList )
2021-04-15 15:16:38 -05:00
def systrayClicked ( self , reason ) :
if reason == QSystemTrayIcon . Trigger :
if self . isVisible ( ) :
self . hide ( )
else :
self . show ( )
2021-04-25 15:48:15 -05:00
def silenceNotifs ( self , dothing ) :
if dothing :
# Definitely not supposed to be poking around in the internals like this lul
Notification . _notification_audio = None
else :
Notification . audio = NOTIF_SOUND
def useColorIcon ( self , dothing ) :
if dothing :
self . setWindowIcon ( QIcon ( QPixmap ( ICON_PATH_COLOR ) ) )
if self . trayIcon :
self . trayIcon . setIcon ( QIcon ( ICON_PATH_COLOR ) )
else :
self . setWindowIcon ( QIcon ( QPixmap ( ICON_PATH ) ) )
if self . trayIcon :
self . trayIcon . setIcon ( QIcon ( ICON_PATH ) )
2021-04-15 15:16:38 -05:00
def eventLoop ( self ) :
# Custom event loop to process queue events
self . processEvents ( )
self . _eventloop . call_soon ( self . eventLoop )
monkeypatch . sleep ( 0.01 )
def exitActionTrigger ( self ) :
self . _exit = True
self . close ( )
def closeEvent ( self , event ) :
if not self . _exit and self . closeToTrayAction . isChecked ( ) :
self . hide ( )
return
self . settings . setValue ( " left " , self . x ( ) )
self . settings . setValue ( " top " , self . y ( ) )
self . settings . setValue ( " width " , self . width ( ) )
self . settings . setValue ( " height " , self . height ( ) )
self . settings . setValue ( " closeToTray " , self . closeToTrayAction . isChecked ( ) )
self . settings . setValue ( " openInTray " , self . openInTrayAction . isChecked ( ) )
self . settings . setValue ( " animatePictures " , self . animatePicturesAction . isChecked ( ) )
self . settings . setValue ( " darkMode " , self . darkModeAction . isChecked ( ) )
self . settings . setValue ( " fetchBackgrounds " , self . fetchBackgroundsAction . isChecked ( ) )
self . settings . setValue ( " sendNotifications " , self . sendNotificationsAction . isChecked ( ) )
self . settings . setValue ( " fetchHeaders " , self . fetchHeadersAction . isChecked ( ) )
self . settings . setValue ( " twoFourTime " , self . twoFourTimeAction . isChecked ( ) )
2021-04-25 15:48:15 -05:00
self . settings . setValue ( ' colorIcon ' , self . colorIconAction . isChecked ( ) )
self . settings . setValue ( ' silenceNotifications ' , self . silenceNotifsAction . isChecked ( ) )
2021-04-15 15:16:38 -05:00
event . accept ( )
self . _eventloop . stop ( )
def getCurrentAcc ( self ) :
if self . acctComboBox . currentIndex ( ) == - 1 or not self . accts :
return ( False , False )
# Returns username, instance
return self . acctComboBox . currentText ( ) . split ( ' @ ' ) [ 1 ] , self . acctComboBox . currentText ( ) . split ( ' @ ' ) [ 2 ]
2021-04-25 16:03:55 -05:00
def badLogin ( self , name ) :
self . Err . showMessage ( " Bad login info for: " + name )
self . newAcctDialog ( )
2021-04-15 15:16:38 -05:00
def newAcctDialog ( self ) :
dialog = LoginDialog ( self )
dialog . getInput ( self . initAcct )
def newChatDialog ( self ) :
u , i = self . getCurrentAcc ( )
text , ok = QInputDialog . getText ( self , ' Start A New Chat ' , " Username: " , QLineEdit . Normal , " " )
if ok :
if len ( text ) < 1 :
self . Err . showMessage ( " No text provided " )
else :
if text [ 0 ] == ' @ ' :
text = text [ 1 : ]
if text . find ( ' @ ' + i ) != - 1 :
text = text . replace ( ' @ ' + i , ' ' )
CallThread ( self . accts [ u + i ] . getAcctInfo , self . newChatReady , text ) . start ( )
def newChatReady ( self , result ) :
if result is None :
self . Err . showMessage ( " I couldn ' t find that user! " )
elif not result [ ' pleroma ' ] [ ' accepts_chat_messages ' ] :
self . Err . showMessage ( " This user is on instance that does not support pleroma chats. \n It may be too old, or it may not be pleroma. " )
else :
u , i = self . getCurrentAcc ( )
closedList = self . settings . value ( ' closed ' + u + i , type = list ) or [ ]
for ind in range ( 0 , ( len ( closedList ) ) ) :
if closedList [ ind ] == result [ ' acct ' ] :
del closedList [ ind ]
break
self . settings . setValue ( ' closed ' + u + i , closedList )
CallThread ( self . accts [ u + i ] . addChat , self . populateChats , result [ ' id ' ] ) . start ( )
2021-04-17 23:34:34 -05:00
def getTotp ( self , acct , challenge_type ) :
if type ( challenge_type ) != list :
challenge_type = [ challenge_type , ' recovery ' ]
self . _eventloop . call_soon_threadsafe ( self . makeTotpCard , ' @ ' + acct . username + ' @ ' + acct . instance , challenge_type )
while not self . totpReady :
monkeypatch . sleep ( 0.2 )
t = self . totpReady
self . totpReady = None
return t
def makeTotpCard ( self , acct , challenge_type ) :
dialog = TotpCard ( acct , challenge_type )
dialog . getInput ( )
2021-04-15 15:16:38 -05:00
def contactDialog ( self ) :
dialog = ContactCard ( self )
dialog . show ( )
def licenseDialog ( self ) :
dialog = LicenseCard ( self )
dialog . show ( )
def reopenAll ( self ) :
u , i = self . getCurrentAcc ( )
if not u or not i :
return
self . settings . setValue ( ' closed ' + u + i , [ ] )
CallThread ( self . accts [ u + i ] . listChats , self . populateChats ) . start ( )
def initAcct ( self , instance , username , password = None ) :
if password :
2021-04-17 23:34:34 -05:00
acct = pleroma . Account ( instance , username , password , totpFunc = self . getTotp )
2021-04-15 15:16:38 -05:00
else :
token = keyring . get_password ( ' plchat ' , instance + username + ' access_token ' )
refresh_token = keyring . get_password ( ' plchat ' , instance + username + ' refresh_token ' )
clientID = keyring . get_password ( ' plchat ' , instance + username + ' clientID ' )
clientSecret = keyring . get_password ( ' plchat ' , instance + username + ' clientSecret ' )
acct = pleroma . Account ( instance , username ,
token = token ,
refresh_token = refresh_token ,
clientID = clientID ,
2021-04-17 23:34:34 -05:00
clientSecret = clientSecret ,
totpFunc = self . getTotp
2021-04-15 15:16:38 -05:00
)
RegisterThread ( acct , self . doneRegister ) . start ( )
def doneRegister ( self , acct ) :
self . accts [ acct . username + acct . instance ] = acct
self . acctComboBox . addItem ( ' @ ' + acct . username + ' @ ' + acct . instance )
acctList = self . settings . value ( ' acctList ' , type = list ) or [ ]
concat = True
for acc in acctList :
if acc [ ' username ' ] == acct . username and acc [ ' instance ' ] == acct . instance :
concat = False
if concat :
acctList . append ( { " username " : acct . username , " instance " : acct . instance } )
self . settings . setValue ( ' acctList ' , acctList )
keyring . set_password ( ' plchat ' , acct . instance + acct . username + ' access_token ' , acct . token )
keyring . set_password ( ' plchat ' , acct . instance + acct . username + ' refresh_token ' , acct . refresh_token )
keyring . set_password ( ' plchat ' , acct . instance + acct . username + ' clientID ' , acct . clientID )
keyring . set_password ( ' plchat ' , acct . instance + acct . username + ' clientSecret ' , acct . clientSecret )
CallThread ( getAvi , None , acct . instance ) . start ( )
CallThread ( acct . getInstanceInfo , None ) . start ( )
def logout ( self ) :
u , i = self . getCurrentAcc ( )
if not u or not i :
print ( " Couldn ' t log out " )
return
self . acctComboBox . removeItem ( self . acctComboBox . currentIndex ( ) )
del self . accts [ u + i ]
acctList = self . settings . value ( ' acctList ' , type = list )
for j in range ( len ( acctList ) ) :
if acctList [ j ] [ ' username ' ] == u and acctList [ j ] [ ' instance ' ] == i :
keyring . delete_password ( " plchat " , i + u + ' access_token ' )
keyring . delete_password ( " plchat " , i + u + ' refresh_token ' )
keyring . delete_password ( " plchat " , i + u + ' clientID ' )
keyring . delete_password ( " plchat " , i + u + ' clientSecret ' )
del acctList [ j ]
self . settings . setValue ( ' acctList ' , acctList )
def acctSwitch ( self ) :
u , i = self . getCurrentAcc ( )
if u + i in self . accts and ' title ' in self . accts [ u + i ] . getInstanceInfo ( ) :
self . setWindowTitle ( self . accts [ u + i ] . getInstanceInfo ( ) [ ' title ' ] + ' Chat ' )
else :
self . setWindowTitle ( ' PlChat ' )
self . tabs . clear ( )
2021-04-17 23:34:34 -05:00
if u and i :
CallThread ( self . accts [ u + i ] . listChats , self . populateChats ) . start ( )
2021-04-15 15:16:38 -05:00
def populateChats ( self , chatList ) :
if type ( chatList ) == dict :
chatList = [ chatList ]
u , i = self . getCurrentAcc ( )
if not u or not i :
return
closedList = self . settings . value ( ' closed ' + u + i , type = list ) or [ ]
for chat in chatList :
c = False
for ind in range ( 0 , self . tabs . count ( ) ) :
if self . tabs . widget ( ind ) . acct == chat [ ' account ' ] [ ' acct ' ] :
c = True
for entry in closedList :
if chat [ ' account ' ] [ ' acct ' ] == entry :
c = True
if c :
continue
ctab = ChatArea ( chat , self . tabs )
self . tabs . addTab ( ctab , chat [ ' account ' ] [ ' display_name ' ] )
def closeTab ( self , index = None ) :
if not index :
index = self . tabs . currentIndex ( )
act = self . tabs . widget ( index ) . acct
u , i = self . getCurrentAcc ( )
closedList = self . settings . value ( ' closed ' + u + i , type = list ) or [ ]
f = True
for entry in closedList :
if entry == act :
f = False
if f :
closedList . append ( act )
self . settings . setValue ( ' closed ' + u + i , closedList )
self . tabs . removeTab ( index )
def handlePleromaEvent ( self , acct , event ) :
if event [ ' event ' ] == ' pleroma:chat_update ' :
payload = json . loads ( event [ ' payload ' ] )
tmp = 0
for ind in range ( 0 , self . tabs . count ( ) ) :
if self . tabs . widget ( ind ) . acct == payload [ ' account ' ] [ ' acct ' ] :
self . _eventloop . call_soon_threadsafe ( self . tabs . widget ( ind ) . addMessage , payload [ ' last_message ' ] )
tmp = ind
#self.tabs.widget(ind).addMessage(payload['last_message'])
if payload [ ' last_message ' ] [ ' account_id ' ] != acct . flakeid and ( not self . hasFocus ( ) or payload [ ' account ' ] [ ' acct ' ] != self . tabs . widget ( self . tabs . currentIndex ( ) ) . acct ) :
if self . sendNotificationsAction . isChecked ( ) :
CallThread ( self . makeNotification , None , payload [ ' last_message ' ] [ ' content ' ] , payload [ ' account ' ] [ ' acct ' ] , payload [ ' account ' ] [ ' avatar_static ' ] ) . start ( )
app . alert ( self , 0 )
self . _eventloop . call_soon_threadsafe ( self . setUrgent , tmp )
def makeNotification ( self , content , user , url ) :
path = getPic ( url )
sendNotification ( content , title = user , icon = path )
def setUrgent ( self , ind ) :
icon = QIcon ( )
2021-04-17 11:13:26 -05:00
icon . addPixmap ( QPixmap ( os . path . join ( os . path . dirname ( __file__ ) , " unread.svg " ) ) )
2021-04-15 15:16:38 -05:00
self . tabs . setTabIcon ( ind , icon )
self . tabs . widget ( ind ) . unread = True
if self . trayIcon :
self . trayIcon . setIcon ( icon )
self . setWindowIcon ( icon )
def changeTab ( self , ind ) :
if not self . tabs . widget ( ind ) :
return
if self . tabs . widget ( ind ) . unread :
CallThread ( getPic , self . tabs . widget ( ind ) . useAvi , self . tabs . widget ( ind ) . avaURL ) . start ( )
self . tabs . widget ( ind ) . unread = False
flip = True
for A in range ( 0 , self . tabs . count ( ) ) :
if self . tabs . widget ( A ) . unread :
flip = False
if flip :
if self . trayIcon :
2021-04-25 15:48:15 -05:00
if self . settings . value ( ' colorIcon ' , type = bool ) :
self . trayIcon . setIcon ( QIcon ( ICON_PATH_COLOR ) )
else :
self . trayIcon . setIcon ( QIcon ( ICON_PATH ) )
if self . settings . value ( ' colorIcon ' , type = bool ) :
self . setWindowIcon ( QIcon ( QPixmap ( ICON_PATH_COLOR ) ) )
else :
self . setWindowIcon ( QIcon ( QPixmap ( ICON_PATH ) ) )
2021-04-15 15:16:38 -05:00
self . tabs . widget ( ind ) . markRead ( )
self . tabs . widget ( ind ) . setFocus ( Qt . NoFocusReason )
def eventFilter ( self , object , event ) :
if event . type ( ) == QEvent . WindowActivate :
self . windowActivate ( )
return False
def windowActivate ( self ) :
ind = self . tabs . currentIndex ( )
if not self . tabs . widget ( ind ) :
return
if self . tabs . widget ( ind ) . unread :
CallThread ( getPic , self . tabs . widget ( ind ) . useAvi , self . tabs . widget ( ind ) . avaURL ) . start ( )
self . tabs . widget ( ind ) . unread = False
flip = True
for A in range ( 0 , self . tabs . count ( ) ) :
if self . tabs . widget ( A ) . unread :
flip = False
if flip :
if self . trayIcon :
2021-04-25 15:48:15 -05:00
if self . settings . value ( ' colorIcon ' , type = bool ) :
self . trayIcon . setIcon ( QIcon ( ICON_PATH_COLOR ) )
else :
self . trayIcon . setIcon ( QIcon ( ICON_PATH ) )
if self . settings . value ( ' colorIcon ' , type = bool ) :
self . setWindowIcon ( QIcon ( QPixmap ( ICON_PATH_COLOR ) ) )
else :
self . setWindowIcon ( QIcon ( QPixmap ( ICON_PATH ) ) )
2021-04-15 15:16:38 -05:00
self . tabs . widget ( ind ) . markRead ( )
self . tabs . widget ( ind ) . setFocus ( Qt . NoFocusReason )
class BGWidget ( QWidget ) :
def __init__ ( self , path = None ) :
super ( ) . __init__ ( )
self . path = path
self . pixmap = QPixmap ( )
def setPath ( self , path ) :
self . path = path
def paintEvent ( self , event ) :
super ( ) . paintEvent ( event )
if self . pixmap . isNull ( ) and self . path :
self . pixmap = QPixmap ( self . path )
painter = QPainter ( self )
if not self . pixmap . isNull ( ) :
self . pixmap = self . pixmap . scaled ( self . width ( ) , self . height ( ) , Qt . KeepAspectRatioByExpanding , Qt . SmoothTransformation )
painter . drawPixmap ( 0 , 0 , self . pixmap )
class ChatArea ( QWidget ) :
def __init__ ( self , chat , parent = None ) :
super ( ) . __init__ ( parent = parent )
self . acct = chat [ ' account ' ] [ ' acct ' ]
self . parent = parent
self . chatID = chat [ ' id ' ]
self . account = chat [ ' account ' ]
self . unread = False
if ex . animatePicturesAction . isChecked ( ) :
avatar_prop = ' avatar '
header_prop = ' header '
else :
avatar_prop = ' avatar_static '
header_prop = ' header_static '
self . avaURL = chat [ ' account ' ] [ avatar_prop ]
CallThread ( getPic , self . useAvi , self . avaURL ) . start ( )
if ex . fetchHeadersAction . isChecked ( ) :
CallThread ( getPic , self . headerReady , chat [ ' account ' ] [ header_prop ] ) . start ( )
self . wrapper = BGWidget ( )
wrapperlayout = QHBoxLayout ( )
wrapperlayout . setContentsMargins ( 0 , 0 , 0 , 0 )
wrapperlayout . setSpacing ( 0 )
self . wrapper . setLayout ( wrapperlayout )
self . header = QFrame ( )
headerlayout = QHBoxLayout ( )
headernest = QVBoxLayout ( )
headernestwidget = QWidget ( )
headernestwidget . setContentsMargins ( 0 , 0 , 0 , 0 )
self . header . setContentsMargins ( 0 , 0 , 0 , 0 )
wrapperlayout . addWidget ( self . header )
self . header . setLayout ( headerlayout )
headernestwidget . setLayout ( headernest )
uDN = EmojiText ( chat [ ' account ' ] [ ' display_name ' ] , chat [ ' account ' ] [ ' emojis ' ] , header = True )
if self . acct . find ( ' @ ' ) == - 1 :
uACT = LinkLabel ( chat [ ' account ' ] [ ' url ' ] , ' @ ' + self . acct + ' @ ' + ex . getCurrentAcc ( ) [ 1 ] )
else :
uACT = LinkLabel ( chat [ ' account ' ] [ ' url ' ] , ' @ ' + self . acct )
uACT . setMargin ( 0 )
#uACT.setStyleSheet("color: white;")
headernest . addWidget ( uDN )
headernest . addWidget ( uACT )
self . avatar = QLabel ( )
self . avatar . setMargin ( 0 )
self . avatar . setFixedHeight ( round ( QDesktopWidget ( ) . screenGeometry ( - 1 ) . height ( ) * 0.06 ) )
headerlayout . addWidget ( self . avatar )
headerlayout . addWidget ( headernestwidget , 100 )
self . messageArea = MessageArea ( self . chatID , self . account , self . callMe )
self . msgs = MovingScroll ( self . messageArea )
if ex . fetchBackgroundsAction . isChecked ( ) :
if self . acct . find ( ' @ ' ) == - 1 :
remoteURL = ex . getCurrentAcc ( ) [ 1 ]
else :
remoteURL = self . acct . split ( ' @ ' ) [ 1 ]
self . msgs . remoteURL = remoteURL
CallThread ( requests . get , self . msgs . parseNodeInfo , ' https:// ' + remoteURL + ' /api/v1/instance ' ) . start ( )
del remoteURL
self . msgs . setHorizontalScrollBarPolicy ( Qt . ScrollBarAlwaysOff )
self . msgs . setVerticalScrollBarPolicy ( Qt . ScrollBarAlwaysOn )
self . msgs . setWidgetResizable ( True )
self . messageArea . setFixedWidth ( self . msgs . getChildWidth ( ) )
self . messageArea . update ( )
send = SendArea ( self . chatID )
layout = QVBoxLayout ( )
layout . addWidget ( self . wrapper , 2 )
layout . addWidget ( self . msgs , 35 )
layout . addWidget ( send , 1 )
self . setFocusProxy ( send . getTextbox ( ) )
self . setLayout ( layout )
def callMe ( self ) :
self . msgs . setWidget ( self . messageArea )
def markRead ( self ) :
self . messageArea . markRead ( )
def headerReady ( self , path ) :
self . wrapper . setPath ( path )
def addMessage ( self , message ) :
self . messageArea . messages . insert ( 0 , message )
self . messageArea . _update ( self . messageArea . messages )
def useAvi ( self , path ) :
pic = QPixmap ( path )
ava = QPixmap ( path )
if pic . isNull ( ) :
pic = QPixmap ( APPDATA + ' / ' + ex . getCurrentAcc ( ) [ 1 ] + ' avi.png ' )
ava = QPixmap ( APPDATA + ' / ' + ex . getCurrentAcc ( ) [ 1 ] + ' avi.png ' )
icon = QIcon ( )
icon . addPixmap ( pic )
self . parent . setTabIcon ( self . parent . indexOf ( self ) , icon )
ava = ava . scaledToHeight ( round ( QDesktopWidget ( ) . screenGeometry ( - 1 ) . height ( ) * 0.06 ) , mode = Qt . SmoothTransformation )
self . avatar . resize ( ava . width ( ) , ava . height ( ) )
self . avatar . setPixmap ( ava )
self . avatar . setFixedWidth ( self . avatar . width ( ) )
def paintEvent ( self , event ) :
super ( ) . paintEvent ( event )
if not self . wrapper . pixmap . isNull ( ) :
self . header . setStyleSheet ( " .QFrame { background-color: rgba(0, 0, 0, 0.3);} QFrame { color: white;} " )
self . paintEvent = super ( ) . paintEvent
class MovingScroll ( QScrollArea ) :
def __init__ ( self , child , path = None ) :
super ( ) . __init__ ( )
self . child = child
self . remoteURL = ' '
self . verticalScrollBar ( ) . rangeChanged . connect ( self . scrollToBottom )
self . verticalScrollBar ( ) . valueChanged . connect ( self . handleValue )
def resizeEvent ( self , event ) :
self . child . setFixedWidth ( self . getChildWidth ( ) )
def scrollToBottom ( self ) :
self . verticalScrollBar ( ) . triggerAction ( QAbstractSlider . SliderToMaximum )
def getChildWidth ( self ) :
return self . width ( ) - self . verticalScrollBar ( ) . width ( ) - 5
def handleValue ( self , pos ) :
if pos < ( self . verticalScrollBar ( ) . maximum ( ) / 10 ) :
self . child . addPage ( )
def parseNodeInfo ( self , response ) :
try :
CallThread ( getPic , self . child . setPath , ' https:// ' + self . remoteURL + response . json ( ) [ ' background_image ' ] ) . start ( )
except :
print ( " Couldn ' t download background image for: " + remoteURL )
class MessageArea ( QWidget ) :
def __init__ ( self , chatID , account , callback ) :
super ( ) . __init__ ( )
self . chatID = chatID
self . account = account
self . callback = callback
self . path = None
self . pixmap = QPixmap ( )
self . layout = QVBoxLayout ( )
self . layout . setContentsMargins ( 10 , 0 , - 5 , 0 )
self . last_read_id = None
self . messages = [ ]
self . page = 0
self . fetchingPage = False
def update ( self ) :
u , i = ex . getCurrentAcc ( )
CallThread ( ex . accts [ u + i ] . getMessages , self . _update , self . chatID ) . start ( )
def markRead ( self ) :
u , i = ex . getCurrentAcc ( )
2021-04-17 23:34:34 -05:00
if not self . last_read_id or not u or not i :
return
2021-04-15 15:16:38 -05:00
acc = ex . accts [ u + i ]
acc . markChatRead ( self . chatID , self . last_read_id )
def addPage ( self ) :
if self . fetchingPage :
return
self . fetchingPage = True
u , i = ex . getCurrentAcc ( )
self . page + = 1
CallThread ( ex . accts [ u + i ] . getMessages , self . pageReady , self . chatID , past = self . messages [ len ( self . messages ) - 1 ] [ ' id ' ] ) . start ( )
def pageReady ( self , messages ) :
for message in messages :
self . messages . append ( message )
self . _update ( self . messages )
def _update ( self , messages ) :
self . messages = messages
for message in messages :
if message [ ' account_id ' ] == self . account [ ' id ' ] :
self . last_read_id = message [ ' id ' ]
for i in reversed ( range ( self . layout . count ( ) ) ) :
self . layout . itemAt ( i ) . widget ( ) . setParent ( None )
for message in reversed ( messages ) :
#for line in re.split(newlineRegex, message['content']):
# if line.strip() == '':
# continue
self . layout . addWidget ( SingleMessage ( message , self . account , self ) , 0 , Qt . AlignBottom )
self . setLayout ( self . layout )
self . callback ( )
if self . fetchingPage :
self . fetchingPage = False
def getMessages ( self ) :
return self . messages or [ ]
def setPath ( self , path ) :
self . path = path
def paintEvent ( self , event ) :
super ( ) . paintEvent ( event )
if self . pixmap . isNull ( ) and self . path :
self . pixmap = QPixmap ( self . path )
painter = QPainter ( self )
if not self . pixmap . isNull ( ) :
self . pixmap = self . pixmap . scaled ( self . width ( ) , self . height ( ) , Qt . KeepAspectRatioByExpanding , Qt . SmoothTransformation )
painter . drawPixmap ( 0 , 0 , self . pixmap )
newlineRegex = re . compile ( ' < \ /?br \ /?>| \n ' )
class SingleMessage ( QWidget ) :
def __init__ ( self , message , account , parent ) :
super ( ) . __init__ ( parent = parent )
self . parent = parent
self . message = message
self . account = account
self . layout = QHBoxLayout ( )
self . layout . setContentsMargins ( 0 , 0 , 0 , 0 )
self . layout . setSpacing ( 0 )
self . setStyleSheet ( " MessageAvatar { margin: 0em 0.2em 0em 0.2em;border: 1px solid black;} " )
if ex . animatePicturesAction . isChecked ( ) :
avatar_prop = ' avatar '
header_prop = ' header '
else :
avatar_prop = ' avatar_static '
header_prop = ' header_static '
self . avaURL = self . account [ avatar_prop ]
u , i = ex . getCurrentAcc ( )
self . userPixmap = None
self . convoPixmap = None
CallThread ( getPic , self . setUserPixmap , ex . accts [ u + i ] . acct [ avatar_prop ] ) . start ( )
CallThread ( getPic , self . setConvoPixmap , self . avaURL ) . start ( )
if self . message [ ' account_id ' ] == self . account [ ' id ' ] :
self . layout . addWidget ( MessageAvatar ( ' convo ' , self ) , 0 , ( Qt . AlignLeft | Qt . AlignTop ) )
self . layout . addWidget ( InternalMessage ( self . message , self . account , self ) , 1 , Qt . AlignLeft )
#spacer = QWidget()
#spacer.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
#self.layout.addWidget(spacer, 9000)
else :
#spacer = QWidget()
#spacer.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
#self.layout.addWidget(spacer, 9000)
self . layout . addWidget ( InternalMessage ( self . message , self . account , self ) , 1 , Qt . AlignRight )
self . layout . addWidget ( MessageAvatar ( ' user ' , self ) , 0 , ( Qt . AlignRight | Qt . AlignTop ) )
self . setLayout ( self . layout )
def setUserPixmap ( self , path ) :
p = QPixmap ( path )
if p . isNull ( ) :
p = QPixmap ( APPDATA + ' / ' + ex . getCurrentAcc ( ) [ 1 ] + ' avi.png ' )
2021-04-25 15:48:15 -05:00
p = p . scaledToHeight ( round ( QDesktopWidget ( ) . screenGeometry ( - 1 ) . height ( ) / 21.6 ) , mode = Qt . SmoothTransformation )
2021-04-15 15:16:38 -05:00
self . userPixmap = p
def setConvoPixmap ( self , path ) :
p = QPixmap ( path )
if p . isNull ( ) :
p = QPixmap ( APPDATA + ' / ' + ex . getCurrentAcc ( ) [ 1 ] + ' avi.png ' )
2021-04-25 15:48:15 -05:00
p = p . scaledToHeight ( round ( QDesktopWidget ( ) . screenGeometry ( - 1 ) . height ( ) / 21.6 ) , mode = Qt . SmoothTransformation )
2021-04-15 15:16:38 -05:00
self . convoPixmap = p
class MessageAvatar ( QLabel ) :
def __init__ ( self , futurepixmap , parent ) :
super ( ) . __init__ ( parent = parent )
self . parent = parent
self . pixmap = QPixmap ( )
self . setMargin ( 0 )
self . setSizePolicy ( QSizePolicy . Fixed , QSizePolicy . Fixed )
CallThread ( self . awaitPixmap , None , futurepixmap ) . start ( )
#self.setPixmap(self.pixmap)
def setPath ( self , path ) :
#if not self.pixmap.isNull():
# return
#self.pixmap = QPixmap(path)
#if self.pixmap.isNull():
# self.pixmap = QPixmap(APPDATA+'/'+ex.getCurrentAcc()[1]+'avi.png')
#self.pixmap = self.pixmap.scaledToHeight(50, mode=Qt.SmoothTransformation)
self . pixmap = path
self . resize ( self . pixmap . width ( ) , self . pixmap . height ( ) )
self . setPixmap ( self . pixmap )
self . setFixedWidth ( self . pixmap . width ( ) )
def awaitPixmap ( self , p ) :
if p == ' user ' :
while not self . parent . userPixmap :
monkeypatch . sleep ( 0.2 )
self . resize ( self . parent . userPixmap . width ( ) , self . parent . userPixmap . height ( ) )
self . setPixmap ( self . parent . userPixmap )
self . setFixedWidth ( self . parent . userPixmap . width ( ) )
else :
while not self . parent . convoPixmap :
monkeypatch . sleep ( 0.2 )
self . resize ( self . parent . convoPixmap . width ( ) , self . parent . convoPixmap . height ( ) )
self . setPixmap ( self . parent . convoPixmap )
self . setFixedWidth ( self . parent . convoPixmap . width ( ) )
class InternalMessage ( QFrame ) :
def __init__ ( self , message , account , parent ) :
super ( ) . __init__ ( parent = parent )
self . parent = parent
self . message = message
self . account = account
self . layout = QHBoxLayout ( )
self . layout . setContentsMargins ( 0 , 0 , 0 , 0 )
self . setSizePolicy ( QSizePolicy . MinimumExpanding , QSizePolicy . MinimumExpanding )
self . attachment = None
if ex . darkModeAction . isChecked ( ) :
color_text = " #ffffff "
color_bg = " #000000 "
else :
color_text = " #000000 "
color_bg = " #ffffff "
self . setStyleSheet ( " InternalMessage { background-color: " + color_bg + " ;border-radius: 1em;padding: 0.5em;border: 1px solid " + color_text + " ;} QWidget { color: " + color_text + " ;} " )
if self . message [ ' content ' ] :
self . label = EmojiText ( self . message [ ' content ' ] , self . message [ ' emojis ' ] )
self . label . setSizePolicy ( QSizePolicy . MinimumExpanding , QSizePolicy . MinimumExpanding )
else :
self . label = None
self . timestamp = QLabel ( timeconvert . utc_to_local ( self . message [ ' created_at ' ] , ex . twoFourTimeAction . isChecked ( ) ) )
self . timestamp . setFont ( ex . TimestampFont )
self . timestamp . setSizePolicy ( QSizePolicy . Fixed , QSizePolicy . Fixed )
if self . label :
self . layout . addWidget ( self . label )
if not self . message [ ' attachment ' ] :
self . layout . addWidget ( self . timestamp , 1 , ( Qt . AlignRight | Qt . AlignBottom ) )
else :
CallThread ( self . getMime , self . addAttachment , self . message [ ' attachment ' ] [ ' url ' ] ) . start ( )
#self.setMinimumSize(QSize(50, computedHeight))
self . setLayout ( self . layout )
def getMime ( self , url ) :
with urllib . request . urlopen ( url ) as res :
# pleroma sux balls at mime types
mime = magic . from_buffer ( res . read ( 2048 ) , mime = True )
return mime
def addAttachment ( self , mime ) :
self . attachment = MediaAttachment ( self . message [ ' attachment ' ] , mime )
self . layout . addWidget ( self . attachment )
self . layout . addWidget ( self . timestamp , 1 , ( Qt . AlignRight | Qt . AlignBottom ) )
def sizeHint ( self ) :
multiplier = 2
additive = 0
if self . label :
multiplier + = self . label . layoutfinal . count ( )
if self . attachment :
additive + = self . attachment . getMedia ( ) . height ( )
computedHeight = ( ex . defaultFontMetrics . height ( ) * multiplier ) + additive
computedWidth = 50
if self . attachment :
if self . attachment . getMedia ( ) . width ( ) > computedWidth :
computedWidth = self . attachment . getMedia ( ) . width ( )
return QSize ( computedWidth , computedHeight )
def MediaAttachment ( media , mime ) :
if mime . find ( " image/ " ) != - 1 :
return ImageAttachment ( media )
elif mime . find ( " video/ " ) != - 1 :
return VideoAttachment ( media )
elif mime . find ( " audio/ " ) != - 1 :
return AudioAttachmentWrapper ( media )
else :
return FileAttachment ( media )
class FileAttachment ( QPushButton ) :
def __init__ ( self , media ) :
super ( FileAttachment , self ) . __init__ ( )
self . url = QUrl ( media [ ' url ' ] )
self . setCursor ( Qt . PointingHandCursor )
self . setIcon ( self . style ( ) . standardIcon ( QStyle . SP_FileLinkIcon ) )
self . setFlat ( True )
def mouseReleaseEvent ( self , event ) :
if event . modifiers ( ) == Qt . NoModifier and event . button ( ) == Qt . LeftButton :
QDesktopServices . openUrl ( self . url )
def getMedia ( self ) :
return self
class AudioAttachmentWrapper ( QWidget ) :
def __init__ ( self , media ) :
super ( AudioAttachmentWrapper , self ) . __init__ ( )
layout = QHBoxLayout ( )
self . file = FileAttachment ( media )
self . audio = AudioAttachment ( media )
layout . addWidget ( self . file )
layout . addWidget ( self . audio )
self . setLayout ( layout )
def getMedia ( self ) :
return QSize ( self . file . getMedia ( ) . width ( ) + self . audio . getMedia ( ) . width ( ) , self . file . getMedia ( ) . height ( ) + self . audio . getMedia ( ) . height ( ) )
class AudioAttachment ( audiowidget . AudioPlayer ) :
def __init__ ( self , media ) :
super ( AudioAttachment , self ) . __init__ ( )
#CallThread(self.openFile, None, media['url']).start()
self . openFile ( media [ ' url ' ] )
def getMedia ( self ) :
return super ( ) . sizeHint ( )
class LinkLabel ( QLabel ) :
def __init__ ( self , url , * args , * * kwargs ) :
super ( LinkLabel , self ) . __init__ ( * args , * * kwargs )
self . url = QUrl ( url )
self . setCursor ( Qt . PointingHandCursor )
def mouseReleaseEvent ( self , event ) :
bttn = event . button ( )
modif = event . modifiers ( )
if modif == Qt . NoModifier and bttn == Qt . LeftButton :
QDesktopServices . openUrl ( self . url )
class VideoAttachment ( videowidget . VideoPlayer ) :
def __init__ ( self , media , * args , * * kwargs ) :
super ( ) . __init__ ( media [ ' url ' ] , * args , * * kwargs )
#CallThread(self.openFile, None, media['url']).start()
self . openFile ( media [ ' url ' ] )
def getMedia ( self ) :
return QSize ( self . width ( ) , self . videoWidget . height ( ) + 50 )
class ImageAttachment ( LinkLabel ) :
def __init__ ( self , media ) :
super ( ) . __init__ ( media [ ' remote_url ' ] )
self . pixmap = QPixmap ( )
CallThread ( getPic , self . setPath , media [ ' url ' ] ) . start ( )
def setPath ( self , path ) :
self . pixmap = QPixmap ( path )
if round ( ex . width ( ) * 0.7 ) < self . pixmap . width ( ) :
self . pixmap = self . pixmap . scaledToWidth ( round ( ex . width ( ) * 0.7 ) , mode = Qt . SmoothTransformation )
self . resize ( self . pixmap . width ( ) , self . pixmap . height ( ) )
self . setPixmap ( self . pixmap )
#self.setFixedHeight(self.height())
self . url = QUrl ( path )
def getMedia ( self ) :
return self . pixmap
class SendArea ( QWidget ) :
def __init__ ( self , chatID ) :
super ( ) . __init__ ( )
self . layout = QHBoxLayout ( )
self . lastFile = None
self . mediaPreview = None
self . mediaID = None
self . Err = QErrorMessage ( )
self . chatID = chatID
self . textbox = TextBox ( )
self . mediaStack = QStackedWidget ( )
self . attachButton = QPushButton ( self . style ( ) . standardIcon ( QStyle . SP_FileDialogStart ) , ' Attach File ' )
self . attachButton . clicked . connect ( self . newFileDialog )
self . attachButton . setSizePolicy ( QSizePolicy . Fixed , QSizePolicy . MinimumExpanding )
2021-04-17 11:13:26 -05:00
sendButton = QPushButton ( QIcon ( QPixmap ( os . path . join ( os . path . dirname ( __file__ ) , " send.svg " ) ) ) , ' ' )
2021-04-15 15:16:38 -05:00
sendButton . clicked . connect ( self . sendMessage )
sendButton . setSizePolicy ( QSizePolicy . Fixed , QSizePolicy . MinimumExpanding )
self . mediaStack . addWidget ( self . attachButton )
self . mediaStack . setCurrentWidget ( self . attachButton )
self . layout . addWidget ( self . textbox , 8 )
self . layout . addWidget ( self . mediaStack , 1 )
self . layout . addWidget ( sendButton , 1 )
self . setFocusProxy ( self . textbox )
self . setLayout ( self . layout )
self . textbox . setSendFunction ( self . sendMessage )
def getTextbox ( self ) :
return self . textbox
def newFileDialog ( self ) :
if self . mediaPreview == ' uploading ' :
self . Err . showMessage ( " Already uploading a file! " )
return
qfd = QFileDialog ( self )
qfd . setFileMode ( QFileDialog . ExistingFile )
qfd . setNameFilters ( [ " All Files (*) " ] )
qfd . filesSelected . connect ( self . attachFile )
qfd . show ( )
def attachFile ( self , paths ) :
if paths [ 0 ] == self . lastFile :
return
elif self . mediaPreview :
self . Err . showMessage ( " File already attached! " )
return
u , i = ex . getCurrentAcc ( )
CallThread ( ex . accts [ u + i ] . uploadMedia , self . doneAttachFile , paths [ 0 ] ) . start ( )
self . mediaPreview = ' uploading '
self . lastFile = paths [ 0 ]
def doneAttachFile ( self , media ) :
if not ' id ' in media :
self . Err . showMessage ( ' Error uploading file ' )
self . detachFile ( )
return
self . mediaID = media [ ' id ' ]
self . mediaPreview = QPushButton ( ex . style ( ) . standardIcon ( QStyle . SP_FileIcon ) , ' ' )
self . mediaPreview . clicked . connect ( self . detachDialog )
self . mediaPreview . setSizePolicy ( QSizePolicy . Fixed , QSizePolicy . MinimumExpanding )
self . mediaStack . addWidget ( self . mediaPreview )
self . mediaStack . setCurrentWidget ( self . mediaPreview )
def detachDialog ( self ) :
dialog = DetachDialog ( )
dialog . accepted . connect ( self . detachFile )
dialog . getInput ( )
def detachFile ( self ) :
if self . mediaPreview and type ( self . mediaPreview ) != str :
self . mediaStack . removeWidget ( self . mediaPreview )
self . mediaStack . setCurrentWidget ( self . attachButton )
self . mediaPreview = None
self . lastFile = None
self . mediaID = None
def sendMessage ( self ) :
u , i = ex . getCurrentAcc ( )
acc = ex . accts [ u + i ]
if self . textbox . toPlainText ( ) == ' ' :
text = None
else :
text = self . textbox . toPlainText ( )
CallThread ( acc . sendMessage , None , self . chatID , content = text , media = self . mediaID ) . start ( )
self . textbox . setText ( ' ' )
if self . mediaPreview :
self . detachFile ( )
self . textbox . setFocus ( Qt . NoFocusReason )
class TextBox ( QTextEdit ) :
def __init__ ( self , sendFunction = None ) :
super ( ) . __init__ ( )
self . sendFunction = sendFunction or self . noop
def keyPressEvent ( self , event ) :
if not event . matches ( QKeySequence . InsertParagraphSeparator ) :
super ( ) . keyPressEvent ( event )
else :
self . sendFunction ( )
def setSendFunction ( self , sendFunction ) :
self . sendFunction = sendFunction
def noop ( self ) :
return
class DetachDialog ( QDialog ) :
def __init__ ( self , parent = None ) :
super ( ) . __init__ ( parent = parent )
self . setWindowTitle ( " Detach Dialog " )
QBtn = QDialogButtonBox . Ok | QDialogButtonBox . Cancel
self . buttonBox = QDialogButtonBox ( QBtn )
self . buttonBox . accepted . connect ( self . accept )
self . buttonBox . rejected . connect ( self . reject )
self . layout = QVBoxLayout ( )
self . message = QLabel ( " Detach this file? " )
self . layout . addWidget ( self . message )
self . layout . addWidget ( self . buttonBox )
self . setLayout ( self . layout )
def getInput ( self ) :
self . exec_ ( )
self . show ( )
self . raise_ ( )
self . activateWindow ( )
2021-04-17 23:34:34 -05:00
class TotpCard ( QDialog ) :
def __init__ ( self , acct , challenge_types , parent = None , ) :
super ( ) . __init__ ( parent = parent )
self . setWindowTitle ( " MFA Request " )
self . result = None
self . waiting = False
QBtn = QDialogButtonBox . Ok
layout = QVBoxLayout ( )
self . buttonBox = QDialogButtonBox ( QBtn )
self . buttonBox . accepted . connect ( self . accept )
self . message = QLabel ( " 2FA Required for " + acct )
tmp = QHBoxLayout ( )
tmpw = QWidget ( )
tmpw . setLayout ( tmp )
self . comboboxlabel = QLabel ( " 2FA Code Type: " )
self . combobox = QComboBox ( )
self . combobox . addItems ( challenge_types )
self . combobox . setDuplicatesEnabled ( False )
self . combobox . setEditable ( False )
tmp . addWidget ( self . comboboxlabel )
tmp . addWidget ( self . combobox )
self . code = QLineEdit ( )
self . code . setPlaceholderText ( ' 2FA Code ' )
layout . addWidget ( self . message )
layout . addWidget ( tmpw )
layout . addWidget ( self . code )
layout . addWidget ( self . buttonBox )
self . setLayout ( layout )
self . setFocusProxy ( self . code )
self . accepted . connect ( self . _finished )
def _finished ( self ) :
ex . totpReady = ( self . code . text ( ) , self . combobox . currentText ( ) )
def getInput ( self , * args , * * kwargs ) :
self . exec_ ( )
self . show ( )
self . raise_ ( )
self . activateWindow ( )
2021-04-15 15:16:38 -05:00
class LoginDialog ( QDialog ) :
def __init__ ( self , parent = None ) :
super ( ) . __init__ ( parent = parent )
self . setWindowTitle ( " Log In " )
QBtn = QDialogButtonBox . Ok | QDialogButtonBox . Cancel
self . buttonBox = QDialogButtonBox ( QBtn )
self . buttonBox . accepted . connect ( self . accept )
self . buttonBox . rejected . connect ( self . reject )
self . layout = QVBoxLayout ( )
self . message = QLabel ( " Log in to your fediverse account " )
self . username = QLineEdit ( )
self . username . setPlaceholderText ( ' Username (e.g. Moon) ' )
self . instance = QLineEdit ( )
self . instance . setPlaceholderText ( ' Instance (e.g. shitposter.club) ' )
self . password = QLineEdit ( )
self . password . setPlaceholderText ( ' Password ' )
self . password . setEchoMode ( QLineEdit . Password )
self . layout . addWidget ( self . message )
self . layout . addWidget ( self . username )
self . layout . addWidget ( self . instance )
self . layout . addWidget ( self . password )
self . layout . addWidget ( self . buttonBox )
self . setLayout ( self . layout )
self . setFocusProxy ( self . username )
def getInput ( self , func ) :
self . show ( )
self . raise_ ( )
self . activateWindow ( )
self . accepted . connect ( self . _finished )
self . finishedFunc = func
def _finished ( self ) :
instance = self . instance . text ( )
instance = instance . replace ( ' http:// ' , ' ' )
instance = instance . replace ( ' https:// ' , ' ' )
if instance [ len ( instance ) - 1 ] == ' / ' :
instance = instance [ 0 : ( len ( instance ) - 2 ) ]
self . finishedFunc ( self . instance . text ( ) , self . username . text ( ) , password = self . password . text ( ) )
class ContactCard ( QDialog ) :
def __init__ ( self , parent = None ) :
super ( ) . __init__ ( parent = parent )
self . setWindowTitle ( " Contact Info " )
QBtn = QDialogButtonBox . Ok
self . buttonBox = QDialogButtonBox ( QBtn )
self . buttonBox . accepted . connect ( self . accept )
self . layout = QVBoxLayout ( )
self . message = QLabel ( " Written by knotteye \n Contact me in the following places (in order of preference) with issues " )
self . fedi = QLabel ( " Fedi: <a href= ' https://waldn.net/users/knotteye ' >@knotteye@waldn.net</a> " )
self . fedi . setOpenExternalLinks ( True )
self . xmpp = QLabel ( ' XMPP: <a href= " xmpp://knotteye@telekem.net?message " >knotteye@telekem.net</a> ' )
self . xmpp . setOpenExternalLinks ( True )
self . email = QLabel ( ' Email: <a href= " mailto:knotteye@airmail.cc " >knotteye@airmail.cc</a> ' )
self . email . setOpenExternalLinks ( True )
self . layout . addWidget ( self . message )
self . layout . addWidget ( self . fedi )
self . layout . addWidget ( self . xmpp )
self . layout . addWidget ( self . email )
self . layout . addWidget ( self . buttonBox )
self . setLayout ( self . layout )
self . show ( )
self . raise_ ( )
self . activateWindow ( )
2021-04-15 20:42:40 -05:00
class LicenseArea ( QScrollArea ) :
def __init__ ( self , licenseText ) :
super ( ) . __init__ ( )
label = QLabel ( licenseText )
self . setWidget ( label )
2021-04-15 15:16:38 -05:00
class LicenseCard ( QDialog ) :
def __init__ ( self , parent = None ) :
super ( ) . __init__ ( parent = parent )
self . setWindowTitle ( " Required Licensing Information " )
self . setSizePolicy ( QSizePolicy . MinimumExpanding , QSizePolicy . MinimumExpanding )
QBtn = QDialogButtonBox . Ok
self . buttonBox = QDialogButtonBox ( QBtn )
self . buttonBox . accepted . connect ( self . accept )
self . layout = QVBoxLayout ( )
self . tabs = QTabWidget ( )
plchatlayout = QVBoxLayout ( )
plchatW = QWidget ( )
plchatW . setLayout ( plchatlayout )
2021-04-15 20:42:40 -05:00
plchatlabel = QLabel ( " <a href= ' https://git.waldn.net/git/knotteye/plchat/ ' >Repository</a> " )
2021-04-15 15:16:38 -05:00
plchatlabel . setTextFormat ( Qt . RichText )
plchatlabel . setOpenExternalLinks ( True )
2021-04-15 20:42:40 -05:00
plchatLicense = QLabel ( misc . plchatLicense )
2021-04-15 15:16:38 -05:00
plchatscroll = QScrollArea ( )
plchatscroll . setWidget ( plchatLicense )
plchatlayout . addWidget ( plchatlabel )
plchatlayout . addWidget ( plchatscroll )
self . tabs . addTab ( plchatW , ' PlChat ' )
2021-04-17 10:51:56 -05:00
self . tabs . addTab ( LicenseArea ( misc . pyqt5License ) , " PyQt5 " )
2021-04-15 20:42:40 -05:00
self . tabs . addTab ( LicenseArea ( misc . videowidgetlicense ) , " videowidget.py " )
self . tabs . addTab ( LicenseArea ( misc . videowidgetlicense ) , " audiowidget.py " )
2021-04-15 21:14:59 -05:00
self . tabs . addTab ( LicenseArea ( misc . notifypylicense ) , " notify-py " )
self . tabs . addTab ( LicenseArea ( misc . logurulicense ) , " loguru " )
self . tabs . addTab ( LicenseArea ( misc . websocketslicense ) , " websockets " )
self . tabs . addTab ( LicenseArea ( misc . sixlicense ) , " six " )
self . tabs . addTab ( LicenseArea ( misc . magiclicense ) , " python-magic " )
self . tabs . addTab ( LicenseArea ( misc . dateutillicense ) , " dateutil " )
self . tabs . addTab ( LicenseArea ( misc . keyringlicense ) , " keyring " )
2021-04-15 15:16:38 -05:00
self . layout . addWidget ( self . tabs )
self . layout . addWidget ( self . buttonBox )
self . setLayout ( self . layout )
self . show ( )
self . raise_ ( )
self . activateWindow ( )
def sizeHint ( self ) :
2021-04-15 21:14:59 -05:00
return QSize ( 650 , 850 )
2021-04-15 15:16:38 -05:00
class EmojiText ( QWidget ) :
ceRegex = re . compile ( ' (:[^:]+:) ' )
def __init__ ( self , text , emojiList , header = False ) :
super ( ) . __init__ ( )
self . layoutfinal = QVBoxLayout ( )
self . layoutfinal . setSpacing ( 0 )
self . layoutfinal . setContentsMargins ( 0 , 0 , 0 , 0 )
for line in re . split ( newlineRegex , text ) :
self . layoutfinal . addWidget ( self . makeLine ( line , emojiList , header ) )
self . setLayout ( self . layoutfinal )
def makeLine ( self , text , emojiList , header ) :
widget = QWidget ( )
layout = QWidget ( )
layout = QHBoxLayout ( )
layout . setSpacing ( 0 )
layout . setContentsMargins ( 0 , 0 , 0 , 0 )
widget . setContentsMargins ( 0 , 0 , 0 , 0 )
customEmojis = [ ]
if header :
size = ex . headerEmojiFontSize
else :
size = ex . emojiFontSize
for em in re . findall ( self . ceRegex , text ) :
for emoji in emojiList :
if emoji [ ' shortcode ' ] == em [ 1 : len ( em ) - 1 ] :
cemurl = emoji [ STATIC_PREF + ' url ' ]
customEmojis . append ( CustomEmoji ( em [ 1 : len ( em ) - 1 ] , cemurl , size ) )
i = 0
for stringpart in re . split ( self . ceRegex , text ) :
if len ( stringpart ) > 2 and stringpart [ 0 ] == ' : ' and stringpart [ len ( stringpart ) - 1 ] == ' : ' :
layout . addWidget ( customEmojis [ i ] , 1 )
i + = 1
else :
label = QLabel ( stringpart )
if header :
label . setFont ( ex . HeaderFont )
label . setTextFormat ( Qt . RichText )
2021-04-16 21:33:42 -05:00
label . setWordWrap ( True )
2021-04-15 15:16:38 -05:00
label . setTextInteractionFlags ( ( Qt . TextBrowserInteraction ) )
label . setOpenExternalLinks ( True )
layout . addWidget ( label )
layout . addWidget ( QWidget ( ) , 9000 )
widget . setLayout ( layout )
return widget
class CustomEmoji ( QLabel ) :
def __init__ ( self , shortcode , url = None , height = 12 ) :
super ( ) . __init__ ( )
self . setScaledContents = True
if url :
CallThread ( getPic , self . loadPic , url ) . start ( )
self . mheight = height
self . pixmap = None
self . setMargin ( 0 )
def loadPic ( self , path ) :
self . pixmap = QPixmap ( path )
self . setPixmap ( self . pixmap . scaledToHeight ( self . mheight , mode = Qt . SmoothTransformation ) )
class CallThread ( threading . Thread ) :
# Generic thread object that takes some function, a callback and a list of arguments
# The entire purpose of this object is to be a wrapper around running functions in a new thread
def __init__ ( self , function , callback , * args , * * kwargs ) :
super ( CallThread , self ) . __init__ ( )
self . function = function
self . args = args or ( )
self . kwargs = kwargs or { }
self . callback = callback
def run ( self ) :
result = self . function ( * self . args , * * self . kwargs )
if not self . callback :
return
ex . _eventloop . call_soon_threadsafe ( self . callback , result )
class RegisterThread ( threading . Thread ) :
def __init__ ( self , acct , callback ) :
super ( RegisterThread , self ) . __init__ ( )
self . acct = acct
self . callback = callback
def run ( self ) :
self . acct . register ( )
2021-04-25 16:03:55 -05:00
try :
self . acct . login ( )
except ValueError :
ex . _eventloop . call_soon_threadsafe ( ex . badLogin , self . acct . username + ' @ ' + self . acct . instance )
return
2021-04-15 15:16:38 -05:00
NotifThread ( self . acct ) . start ( )
self . acct . setChatUpdate ( ex . handlePleromaEvent )
self . callback ( self . acct )
class NotifThread ( threading . Thread ) :
def __init__ ( self , acct ) :
super ( NotifThread , self ) . __init__ ( )
self . acct = acct
self . loop = asyncio . new_event_loop ( )
self . daemon = True
asyncio . set_event_loop ( self . loop )
def run ( self ) :
self . loop . run_until_complete ( self . acct . startStreaming ( ) )
class HLine ( QFrame ) :
def __init__ ( self ) :
super ( HLine , self ) . __init__ ( )
self . setFrameShape ( self . HLine | self . Sunken )
class VLine ( QFrame ) :
def __init__ ( self ) :
super ( VLine , self ) . __init__ ( )
self . setFrameShape ( self . VLine | self . Sunken )
class IconLabel ( QLabel ) :
def __init__ ( self , icon , w = 30 , h = None ) :
super ( IconLabel , self ) . __init__ ( )
if not h :
h = w
#self.icon = self.style().standardIcon(icon)
self . icon = icon
self . setPixmap ( self . icon . pixmap ( QSize ( w , h ) ) )
Notification = Notify (
default_notification_title = " PlChat " ,
2021-04-25 15:48:15 -05:00
default_notification_icon = ICON_PATH ,
default_notification_audio = NOTIF_SOUND
2021-04-15 15:16:38 -05:00
)
def sendNotification ( message , title = None , icon = None ) :
Notification . message = message
if title :
Notification . title = title
if icon :
Notification . icon = icon
Notification . send ( block = False )
def getPic ( url ) :
filename = urllib . parse . urlparse ( url ) . netloc + os . path . basename ( urllib . parse . urlparse ( url ) . path )
filepath = CACHE + ' /img/ ' + filename
try :
f = open ( filepath , mode = ' xb+ ' )
THREADS [ filepath ] = threading . current_thread ( )
except FileExistsError :
try :
while THREADS [ filepath ] . is_alive ( ) :
monkeypatch . sleep ( 0.2 )
return filepath
except KeyError :
return filepath
try :
imgdata = urllib . request . urlopen ( url ) . read ( )
except Exception as E :
print ( " Got " + str ( E ) + " while downloading file: " + url )
return filepath
f . write ( imgdata )
f . close ( )
return filepath
def getVid ( url ) :
filename = urllib . parse . urlparse ( url ) . netloc + os . path . basename ( urllib . parse . urlparse ( url ) . path )
filepath = CACHE + ' /vid/ ' + filename
try :
f = open ( filepath , mode = ' xb+ ' )
THREADS [ filepath ] = threading . current_thread ( )
except FileExistsError :
try :
while THREADS [ filepath ] . is_alive ( ) :
monkeypatch . sleep ( 0.2 )
return filepath
except KeyError :
return filepath
try :
viddata = urllib . request . urlopen ( url ) . read ( )
except Exception as E :
print ( " Got " + str ( E ) + " while downloading file: " + url )
return filepath
f . write ( viddata )
f . close ( )
return filepath
def getAudio ( url ) :
filename = urllib . parse . urlparse ( url ) . netloc + os . path . basename ( urllib . parse . urlparse ( url ) . path )
filepath = CACHE + ' /audio/ ' + filename
try :
f = open ( filepath , mode = ' xb+ ' )
THREADS [ filepath ] = threading . current_thread ( )
except FileExistsError :
try :
while THREADS [ filepath ] . is_alive ( ) :
monkeypatch . sleep ( 0.2 )
return filepath
except KeyError :
return filepath
try :
viddata = urllib . request . urlopen ( url ) . read ( )
except Exception as E :
print ( " Got " + str ( E ) + " while downloading file: " + url )
return filepath
f . write ( viddata )
f . close ( )
return filepath
def _mkdir ( _dir ) :
if os . path . isdir ( _dir ) : pass
elif os . path . isfile ( _dir ) :
raise OSError ( " %s exists as a regular file. " % _dir )
else :
parent , directory = os . path . split ( _dir )
if parent and not os . path . isdir ( parent ) : _mkdir ( parent )
if directory : os . mkdir ( _dir )
def getAvi ( instance ) :
filename = instance + ' avi.png '
filepath = APPDATA + ' / ' + filename
try :
f = open ( filepath , mode = ' xb+ ' )
except FileExistsError :
return
try :
imgdata = urllib . request . urlopen ( ' https:// ' + instance + ' /images/avi.png ' ) . read ( )
except Exception as E :
print ( " Couldn ' t get default avi for " + instance + " , error: " + str ( E ) )
return
f . write ( imgdata )
f . close ( )
return filepath
def updateAnimationPref ( ) :
if ex . animatePicturesAction . isChecked ( ) :
STATIC_PREF = ' '
else :
STATIC_PREF = ' static_ '
2021-04-17 10:40:06 -05:00
if __name__ == ' __main__ ' :
2021-04-15 15:16:38 -05:00
_mkdir ( CACHE + ' /img/ ' )
_mkdir ( CACHE + ' /vid/ ' )
_mkdir ( CACHE + ' /audio/ ' )
_mkdir ( APPDATA )
app = QApplication ( sys . argv )
app . setOrganizationName ( " plchat " )
app . setApplicationName ( " plchat " )
ex = App ( app . processEvents )
2021-04-17 10:40:06 -05:00
sys . exit ( ex . _eventloop . run_forever ( ) )