#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 . 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()) from PyQt5.QtGui import * from PyQt5.QtCore import * from PyQt5.QtWidgets import * from notifypy import Notify import sys, threading, queue, asyncio, urllib, appdirs, os, time, pleroma, re, magic, monkeypatch, requests, misc, json, timeconvert, videowidget, audiowidget CACHE = appdirs.AppDirs('plchat', 'plchat').user_cache_dir APPDATA = appdirs.AppDirs('plchat', 'plchat').user_data_dir THREADS = {} STATIC_PREF = '' ICON_PATH = os.path.join(os.path.dirname(__file__), "fedi.svg") ICON_PATH_COLOR = os.path.join(os.path.dirname(__file__), "fedi_color.svg") NOTIF_SOUND = os.path.join(os.path.dirname(__file__), 'notif.wav') class App(QMainWindow): settings = QSettings(APPDATA+"/settings.ini") _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) if self.settings.value('colorIcon', type=bool): self.setWindowIcon(QIcon(QPixmap(ICON_PATH_COLOR))) else: self.setWindowIcon(QIcon(QPixmap(ICON_PATH))) 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) if self.settings.value('silenceNotifications', type=bool): Notification._notification_audio = None self._exit = False self.totpReady = False 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") self.colorIconAction = QAction("Use Color Icon", self, checkable=True, triggered=self.useColorIcon) self.colorIconAction.setChecked(self.settings.value('colorIcon', type=bool)) self.sendNotificationsAction = QAction("Send Notifications", self, checkable=True) self.sendNotificationsAction.setChecked(self.settings.value('sendNotifications', type=bool)) self.silenceNotifsAction = QAction("Silence Notifications", self, checkable=True, triggered=self.silenceNotifs) self.silenceNotifsAction.setChecked(self.settings.value('silenceNotifications', type=bool)) 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) systraymenu.addAction(self.silenceNotifsAction) systraymenu.addAction(exitAction) self.trayIcon = QSystemTrayIcon() if self.settings.value('colorIcon', type=bool): self.trayIcon.setIcon(QIcon(ICON_PATH_COLOR)) else: self.trayIcon.setIcon(QIcon(ICON_PATH)) 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) prefsmenu.addAction(self.colorIconAction) prefsmenu.addAction(self.sendNotificationsAction) prefsmenu.addAction(self.silenceNotifsAction) 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 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) def systrayClicked(self, reason): if reason == QSystemTrayIcon.Trigger: if self.isVisible(): self.hide() else: self.show() 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)) 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()) self.settings.setValue('colorIcon', self.colorIconAction.isChecked()) self.settings.setValue('silenceNotifications', self.silenceNotifsAction.isChecked()) 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] def badLogin(self, name): self.Err.showMessage("Bad login info for: "+name) self.newAcctDialog() 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.\nIt 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() 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() 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: acct = pleroma.Account(instance, username, password, totpFunc=self.getTotp) 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, clientSecret=clientSecret, totpFunc=self.getTotp ) 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() if u and i: CallThread(self.accts[u+i].listChats, self.populateChats).start() 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 = -1 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 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) if tmp > 0: self._eventloop.call_soon_threadsafe(self.setUrgent, tmp) else: u, i = ex.getCurrentAcc() tmp = self.tabs.count() closedList = self.settings.value('closed'+u+i, type=list) or [] for ind in range(0,(len(closedList))): if closedList[ind] == payload['account']['acct']: del closedList[ind] break self.settings.setValue('closed'+u+i, closedList) self._eventloop.call_soon_threadsafe(self.populateChats, payload) while self.tabs.tabIcon(tmp).isNull(): time.sleep(0.3) 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() icon.addPixmap(QPixmap(os.path.join(os.path.dirname(__file__), "unread.svg"))) 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: 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))) 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: 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))) 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() if not self.last_read_id or not u or not i: return acc = ex.accts[u+i] CallThread(acc.markChatRead, None, self.chatID, self.last_read_id).start() 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'] break 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') p = p.scaledToHeight(round(QDesktopWidget().screenGeometry(-1).height() / 21.6), mode=Qt.SmoothTransformation) self.userPixmap = p def setConvoPixmap(self, path): p = QPixmap(path) if p.isNull(): p = QPixmap(APPDATA+'/'+ex.getCurrentAcc()[1]+'avi.png') p = p.scaledToHeight(round(QDesktopWidget().screenGeometry(-1).height() / 21.6), mode=Qt.SmoothTransformation) 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) sendButton = QPushButton(QIcon(QPixmap(os.path.join(os.path.dirname(__file__), "send.svg"))), '') 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() 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() 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\nContact me in the following places (in order of preference) with issues") self.fedi = QLabel("Fedi: @knotteye@waldn.net") self.fedi.setOpenExternalLinks(True) self.xmpp = QLabel('XMPP: knotteye@telekem.net') self.xmpp.setOpenExternalLinks(True) self.email = QLabel('Email: knotteye@airmail.cc') 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() class LicenseArea(QScrollArea): def __init__(self, licenseText): super().__init__() label = QLabel(licenseText) self.setWidget(label) 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) plchatlabel = QLabel("Repository") plchatlabel.setTextFormat(Qt.RichText) plchatlabel.setOpenExternalLinks(True) plchatLicense = QLabel(misc.plchatLicense) plchatscroll = QScrollArea() plchatscroll.setWidget(plchatLicense) plchatlayout.addWidget(plchatlabel) plchatlayout.addWidget(plchatscroll) self.tabs.addTab(plchatW, 'PlChat') self.tabs.addTab(LicenseArea(misc.pyqt5License), "PyQt5") self.tabs.addTab(LicenseArea(misc.videowidgetlicense), "videowidget.py") self.tabs.addTab(LicenseArea(misc.videowidgetlicense), "audiowidget.py") 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") self.layout.addWidget(self.tabs) self.layout.addWidget(self.buttonBox) self.setLayout(self.layout) self.show() self.raise_() self.activateWindow() def sizeHint(self): return QSize(650, 850) 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) label.setWordWrap(True) 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() try: self.acct.login() except ValueError: ex._eventloop.call_soon_threadsafe(ex.badLogin, self.acct.username+'@'+self.acct.instance) return 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", default_notification_icon=ICON_PATH, default_notification_audio=NOTIF_SOUND ) 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_' if __name__ == '__main__': _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) sys.exit(ex._eventloop.run_forever())