diff --git a/.github/workflows/galacteek-deploy.yml b/.github/workflows/galacteek-deploy.yml index 4f2ac678..23da0c37 100644 --- a/.github/workflows/galacteek-deploy.yml +++ b/.github/workflows/galacteek-deploy.yml @@ -17,8 +17,7 @@ jobs: strategy: fail-fast: false matrix: - # os: [ubuntu-18.04, macos-latest, windows-latest] - os: [windows-latest] + os: [ubuntu-18.04, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: diff --git a/AppImage/AppRun b/AppImage/AppRun index efa79e93..c1f6e2ee 100755 --- a/AppImage/AppRun +++ b/AppImage/AppRun @@ -6,6 +6,8 @@ export PATH=${HERE}/usr/bin:$PATH export LD_LIBRARY_PATH=${HERE}/usr/lib:$LD_LIBRARY_PATH export PYTHONPATH=$PYTHONPATH:${HERE}/usr/lib/python3.7/site-packages export QT_QPA_PLATFORM_PLUGIN_PATH=${HERE}/usr/lib/python3.7/site-packages/PyQt5/Qt/plugins/platforms +export QT_STYLE_OVERRIDE=Fusion + APPIQT_LIBPATH=${HERE}/usr/lib/python3.7/site-packages/PyQt5/Qt/lib if [ ! -z "${container}" ] && [ x"${container}" == x"firejail" ]; then diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ebc9e5c..9fb35ed9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,10 @@ This is build 42. This makes the boot process faster. - Change default fonts for web engine widgets +### Fixed +- Memory leak in the BrowserTab object +- UTF-8 rendering of blog posts + ## [0.4.41] - 2020-12-04 ### Added - Tor support diff --git a/Makefile b/Makefile index d18c5408..dab274c7 100644 --- a/Makefile +++ b/Makefile @@ -24,3 +24,9 @@ tox: upload: dists twine upload --repository-url https://upload.pypi.org/legacy/ dist/* + +themes: + @python setup.py build_ui --tasks=themes + +ui: + @python setup.py build_ui diff --git a/README.rst b/README.rst index 7f72bfd7..11938c80 100644 --- a/README.rst +++ b/README.rst @@ -42,6 +42,8 @@ See the releases_ page for all releases. Sponsor this project ==================== +See the sponsor_ page for all the possible ways to donate to this project. + .. image:: https://raw.githubusercontent.com/pinnaculum/galacteek/master/share/icons/github-mark.png :target: https://github.com/sponsors/pinnaculum :alt: Sponsor with Github Sponsors @@ -52,13 +54,6 @@ Sponsor this project :alt: Sponsor with Liberapay :align: left -.. image:: https://github.githubassets.com/images/modules/site/icons/funding_platforms/patreon.svg - :target: https://patreon.com/galacteek - :alt: Sponsor with Patreon - :align: left - :width: 90 - :height: 90 - Screencasts =========== @@ -220,6 +215,7 @@ from the ipfs-logo_ project's repository is included, unchanged. .. _aioipfs: https://gitlab.com/cipres/aioipfs .. _aiomonitor: https://github.com/aio-libs/aiomonitor .. _asyncqt: https://github.com/gmarull/asyncqt +.. _sponsor: https://github.com/pinnaculum/galacteek/SPONSOR.rst .. _quamash: https://github.com/harvimt/quamash .. _go-ipfs: https://github.com/ipfs/go-ipfs .. _dist.ipfs.io: https://dist.ipfs.io @@ -239,4 +235,4 @@ from the ipfs-logo_ project's repository is included, unchanged. .. _IPID: https://github.com/jonnycrunch/ipid .. _wasmer: https://wasmer.io/ .. _cyber: https://cybercongress.ai -.. _Bitmessage: https://wiki.bitmessage.org/Bitmessage%20Technical%20Paper.pdf +.. _Bitmessage: https://wiki.bitmessage.org/ diff --git a/SPONSOR.rst b/SPONSOR.rst new file mode 100644 index 00000000..59ad3492 --- /dev/null +++ b/SPONSOR.rst @@ -0,0 +1,23 @@ +Github and Liberapay +==================== + +.. image:: https://raw.githubusercontent.com/pinnaculum/galacteek/master/share/icons/github-mark.png + :target: https://github.com/sponsors/pinnaculum + :alt: Sponsor with Github Sponsors + :align: left + +.. image:: https://raw.githubusercontent.com/pinnaculum/galacteek/master/share/icons/liberapay.png + :target: https://liberapay.com/galacteek/donate + :alt: Sponsor with Liberapay + :align: left + +Cryptocurrency donations +======================== + +You can also make a donation with Bitcoin: + +.. image:: https://raw.githubusercontent.com/pinnaculum/galacteek/master/share/crypto/btc/btc-donate.png + :alt: Donate with Bitcoin + :align: left + +BTC: **1Cgwbzi6R4TgWp7AG67BPwYY1iz15ATR7A** diff --git a/galacteek.pro b/galacteek.pro index 80f3fb78..50a81210 100644 --- a/galacteek.pro +++ b/galacteek.pro @@ -1,6 +1,6 @@ -SOURCES = galacteek/application.py \ +SOURCES = galacteek/application/__init__.py \ galacteek/appsettings.py \ - galacteek/ui/browser.py \ + galacteek/ui/browser/__init__.py \ galacteek/ui/camera.py \ galacteek/ui/chat.py \ galacteek/ui/clipboard.py \ @@ -10,14 +10,14 @@ SOURCES = galacteek/application.py \ galacteek/ui/dids.py \ galacteek/ui/downloads.py \ galacteek/ui/feeds.py \ - galacteek/ui/files.py \ + galacteek/ui/files/__init__.py \ galacteek/ui/hashmarks.py \ galacteek/ui/helpers.py \ - galacteek/ui/history.py \ + galacteek/ui/history/__init__.py \ galacteek/ui/i18n.py \ galacteek/ui/imgview.py \ galacteek/ui/ipfssearch.py \ - galacteek/ui/unixfs.py \ + galacteek/ui/files/unixfs.py \ galacteek/ui/keys.py \ galacteek/ui/mainui.py \ galacteek/ui/mediaplayer.py \ @@ -30,7 +30,7 @@ SOURCES = galacteek/application.py \ galacteek/ui/settings.py \ galacteek/ui/textedit.py \ galacteek/ui/userwebsite.py \ - galacteek/ui/widgets.py + galacteek/ui/widgets/__init__.py FORMS += galacteek/ui/forms/addkeydialog.ui \ galacteek/ui/forms/addhashmarkdialog.ui \ @@ -42,6 +42,7 @@ FORMS += galacteek/ui/forms/addkeydialog.ui \ galacteek/ui/forms/chatchannelslist.ui \ galacteek/ui/forms/chatroom.ui \ galacteek/ui/forms/dagview.ui \ + galacteek/ui/forms/dmessenger.ui \ galacteek/ui/forms/donatedialog.ui \ galacteek/ui/forms/files.ui\ galacteek/ui/forms/ipfscidinputdialog.ui \ @@ -61,4 +62,5 @@ FORMS += galacteek/ui/forms/addkeydialog.ui \ galacteek/ui/forms/timeframeselector.ui TRANSLATIONS = share/translations/galacteek_en.ts \ - share/translations/galacteek_fr.ts \ + share/translations/galacteek_es.ts \ + share/translations/galacteek_fr.ts diff --git a/galacteek/application.py b/galacteek/application/__init__.py similarity index 95% rename from galacteek/application.py rename to galacteek/application/__init__.py index 3aaa9ad1..4a57cb21 100644 --- a/galacteek/application.py +++ b/galacteek/application/__init__.py @@ -54,8 +54,8 @@ from galacteek.core.asynclib import asyncify from galacteek.core.asynclib import cancelAllTasks -from galacteek.core.aservice import GService -from galacteek.core.aservice import cached_property + +from galacteek.services import cached_property from galacteek.core.ctx import IPFSContext from galacteek.core.profile import UserProfile from galacteek.core.multihashmetadb import IPFSObjectMetadataDatabase @@ -106,11 +106,6 @@ from galacteek.ipdapps.loader import DappsRegistry -from galacteek.browser.webprofiles import IPFSProfile -from galacteek.browser.webprofiles import Web3Profile -from galacteek.browser.webprofiles import MinimalProfile -from galacteek.browser.webprofiles import AnonymousProfile - from galacteek.dweb.webscripts import ipfsClientScripts from galacteek.dweb.render import defaultJinjaEnv @@ -131,10 +126,7 @@ from galacteek.appsettings import * from galacteek.core.ipfsmarks import IPFSMarks -from galacteek.services.bitmessage.service import BitMessageClientService -from galacteek.services.tor.service import TorService -from galacteek.services.tor.service import TorServiceRuntimeConfig -from galacteek.services.ethereum.service import EthereumService +from galacteek.services.app import AppService from yarl import URL @@ -232,58 +224,6 @@ def gatewayUrl(self): return self._gatewayUrl -class AppService(GService): - # Bitmessage service - bmService: BitMessageClientService = None - - # Tor service - torService: TorService = None - - # Eth - ethService: EthereumService = None - - def __init__(self, *args, **kw): - self.app = kw.pop('app') - - super().__init__(*args, **kw) - - @cached_property - def bmService(self) -> BitMessageClientService: - return BitMessageClientService( - self.app._bitMessageDataLocation - ) - - @cached_property - def ethService(self) -> EthereumService: - return EthereumService( - self.app._ethDataLocation - ) - - @cached_property - def torService(self) -> TorService: - return TorService( - self.app.dataPathForService('tor'), - TorServiceRuntimeConfig( - cfgLocation=self.app._torConfigLocation, - dataLocation=self.app._torDataDirLocation - ) - ) - - async def on_start(self) -> None: - log.debug('Starting main application service') - - # Dependencies - - log.debug('Adding runtime dependencies') - - await self.add_runtime_dependency(self.bmService) - await self.add_runtime_dependency(self.torService) - await self.add_runtime_dependency(self.ethService) - - async def on_stop(self) -> None: - log.debug('Stopping main application service') - - class GalacteekApplication(QApplication): """ Galacteek application class @@ -310,6 +250,7 @@ def __init__(self, debug=False, profile='main', sslverify=True, self.setQuitOnLastWindowClosed(False) self._mode = mode + self._theme = None self._cmdArgs = cmdArgs self._debugEnabled = debug self._appProfile = profile @@ -362,6 +303,15 @@ def s(self) -> AppService: def eth(self): return self.s.ethService + @property + def theme(self): + return self._theme + + @theme.setter + def theme(self, theme): + log.debug(f'Changing theme: {theme!r}') + self._theme = theme + @property def cmdArgs(self): return self._cmdArgs @@ -510,10 +460,10 @@ def readQSSFile(self, path): qFile.open(QFile.ReadOnly) styleSheetBa = qFile.readAll() return styleSheetBa.data().decode('utf-8') - except BaseException: + except BaseException as err: # that would probably occur if the QSS is not # in the resources file.. set some default stylesheet here? - pass + log.debug(f'readQSSFile error: {err}') def themeChange(self, name=None): theme = cGet('theme', mod='galacteek.ui') @@ -627,11 +577,14 @@ def setupTranslator(self): if self.translator: QApplication.removeTranslator(self.translator) + lang = cGet('language') + qmPath = f':/share/translations/{GALACTEEK_NAME}_{lang}.qm' + self.translator = QTranslator() - QApplication.installTranslator(self.translator) - lang = self.settingsMgr.getSetting(CFG_SECTION_UI, CFG_KEY_LANG) - self.translator.load(':/share/translations/galacteek_{0}.qm'.format( - lang)) + + if self.translator.load(qmPath): + log.debug(f'Translator load OK from: {qmPath}') + self.installTranslator(self.translator) def createMainWindow(self, show=True): self.mainWindow = mainui.MainWindow(self) @@ -877,11 +830,7 @@ async def setupOrmDb(self, dbpath: Path): ensure(self.sqliteDb.setup()) self.modelAtomFeeds = AtomFeedsModel(self.sqliteDb.feeds, parent=self) - self.urlHistory = history.URLHistory( - self.sqliteDb, - enabled=self.settingsMgr.urlHistoryEnabled, - parent=self - ) + self.urlHistory = history.URLHistory(parent=self) if not await database.initOrm(str(dbpath)): await self.dbConfigured.emit(False) @@ -984,9 +933,7 @@ def setupMainObjects(self): self.solarSystem = SolarSystem() self.mimeTypeIcons = preloadMimeIcons() self.hmSynchronizer = HashmarksSynchronizer() - self.ipidManager = IPIDManager( - resolveTimeout=self.settingsMgr.ipidIpnsTimeout - ) + self.ipidManager = IPIDManager() self.towers = { 'dags': DAGSignalsTower(self), @@ -1340,6 +1287,7 @@ async def setupProfileAndRepo(self, ipfsop): pDialog.stop() pDialog.log('Ready to roll') + pDialog.showProgress(False) await ipfsop.sleep(0.5) @@ -1448,14 +1396,12 @@ def getClipboardText(self): return self.clipTracker.getText() def initWebProfiles(self): + from galacteek.browser.webprofiles import wpRegisterFromConfig + self.scriptsIpfs = ipfsClientScripts(self.getIpfsConnectionParams()) - self.webProfiles = { - 'minimal': MinimalProfile(parent=self), - 'ipfs': IPFSProfile(parent=self), - 'web3': Web3Profile(parent=self), - 'anonymous': AnonymousProfile(parent=self) - } + # Register web profiles + wpRegisterFromConfig(self) def allWebProfilesSetAttribute(self, attribute, val): for pName, profile in self.webProfiles.items(): diff --git a/galacteek/application/config.yaml b/galacteek/application/config.yaml new file mode 100644 index 00000000..42ac3273 --- /dev/null +++ b/galacteek/application/config.yaml @@ -0,0 +1,10 @@ +envs: + default: + language: en + + languagesAvailable: + - code: en + displayName: 'English' + + - code: es + displayName: 'Spanish (Castilian)' diff --git a/galacteek/blockchain/ethereum/contract.py b/galacteek/blockchain/ethereum/contract.py index af388fed..09348295 100644 --- a/galacteek/blockchain/ethereum/contract.py +++ b/galacteek/blockchain/ethereum/contract.py @@ -1,7 +1,7 @@ import asyncio import attr from galacteek import log -from galacteek.core.aservice import GService +from galacteek.services import GService from galacteek.core.ps import makeKeySmartContract from galacteek.core.ps import gHub from web3 import Account diff --git a/galacteek/blockchain/ethereum/infura/endpoints.py b/galacteek/blockchain/ethereum/infura/endpoints.py new file mode 100644 index 00000000..cc42aa5a --- /dev/null +++ b/galacteek/blockchain/ethereum/infura/endpoints.py @@ -0,0 +1,8 @@ +INFURA_MAINNET_DOMAIN = 'mainnet.infura.io' +INFURA_ROPSTEN_DOMAIN = 'ropsten.infura.io' +INFURA_GOERLI_DOMAIN = 'goerli.infura.io' +INFURA_RINKEBY_DOMAIN = 'rinkeby.infura.io' +INFURA_KOVAN_DOMAIN = 'kovan.infura.io' + +WEBSOCKET_SCHEME = 'wss' +HTTP_SCHEME = 'https' diff --git a/galacteek/browser/webprofiles.py b/galacteek/browser/webprofiles.py deleted file mode 100644 index 2b703df7..00000000 --- a/galacteek/browser/webprofiles.py +++ /dev/null @@ -1,188 +0,0 @@ -from PyQt5.QtWidgets import QApplication -from PyQt5.QtWebEngineWidgets import QWebEngineProfile -from PyQt5.QtWebEngineWidgets import QWebEngineSettings -from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInterceptor - -from galacteek.dweb.webscripts import ethereumClientScripts - -from galacteek.browser.schemes import SCHEME_DWEB -from galacteek.browser.schemes import SCHEME_ENS -from galacteek.browser.schemes import SCHEME_ENSR -from galacteek.browser.schemes import SCHEME_FS -from galacteek.browser.schemes import SCHEME_IPFS -from galacteek.browser.schemes import SCHEME_IPNS -from galacteek.browser.schemes import SCHEME_Q -from galacteek.browser.schemes import SCHEME_GALACTEEK -from galacteek.browser.schemes import isIpfsUrl - - -WP_NAME_ANON = 'anonymous' -WP_NAME_MINIMAL = 'minimal' -WP_NAME_IPFS = 'ipfs' -WP_NAME_WEB3 = 'web3' - - -webProfilesPrio = { - WP_NAME_MINIMAL: 0, - WP_NAME_IPFS: 1, - WP_NAME_WEB3: 2 -} - - -class IPFSRequestInterceptor(QWebEngineUrlRequestInterceptor): - def interceptRequest(self, info): - url = info.requestUrl() - - if url and url.isValid() and isIpfsUrl(url): - path = url.path() - - # Force Content-type for JS modules - if path and path.endswith('.js'): - info.setHttpHeader( - 'Content-Type'.encode(), - 'text/javascript'.encode() - ) - - -class BaseProfile(QWebEngineProfile): - def __init__(self, storageName='base', parent=None): - super(BaseProfile, self).__init__(storageName, parent) - - self.app = QApplication.instance() - self.webScripts = self.scripts() - self.webSettings = self.settings() - self.profileName = storageName - self.iceptor = IPFSRequestInterceptor(self) - self.setUrlRequestInterceptor(self.iceptor) - self.setSettings() - self.installIpfsSchemeHandlers() - self.installScripts() - - self.downloadRequested.connect( - self.app.downloadsManager.onDownloadRequested) - - def setSettings(self): - self.webSettings.setAttribute( - QWebEngineSettings.FullScreenSupportEnabled, - True - ) - - self.webSettings.setAttribute(QWebEngineSettings.PluginsEnabled, - True) - self.webSettings.setAttribute(QWebEngineSettings.LocalStorageEnabled, - True) - - self.webSettings.setFontFamily( - QWebEngineSettings.StandardFont, - 'Inter UI' - ) - self.webSettings.setFontSize( - QWebEngineSettings.MinimumFontSize, - 14 - ) - self.webSettings.setFontSize( - QWebEngineSettings.DefaultFontSize, - 14 - ) - self.webSettings.setUnknownUrlSchemePolicy( - QWebEngineSettings.DisallowUnknownUrlSchemes - ) - - self.setHttpCacheType(QWebEngineProfile.NoCache) - - def installHandler(self, scheme, handler): - sch = scheme if isinstance(scheme, bytes) else scheme.encode() - self.installUrlSchemeHandler(sch, handler) - - def installScripts(self): - pass - - def installIpfsSchemeHandlers(self): - # XXX Remove fs: soon - for scheme in [SCHEME_DWEB, SCHEME_FS]: - self.installHandler(scheme, self.app.dwebSchemeHandler) - - for scheme in [SCHEME_IPFS, SCHEME_IPNS]: - self.installHandler(scheme, self.app.nativeIpfsSchemeHandler) - - self.installHandler(SCHEME_ENS, self.app.ensProxySchemeHandler) - self.installHandler(SCHEME_ENSR, self.app.ensSchemeHandler) - self.installHandler(SCHEME_Q, self.app.qSchemeHandler) - # self.installHandler(SCHEME_GALACTEEK, self.app.gSchemeHandler) - - -class MinimalProfile(BaseProfile): - def __init__(self, storageName=WP_NAME_MINIMAL, parent=None): - super(MinimalProfile, self).__init__(storageName, parent) - - -class AnonymousProfile(BaseProfile): - """ - Anonymous web profile. No JS, no cache, no cookies. - """ - def __init__(self, storageName=WP_NAME_ANON, parent=None): - super(AnonymousProfile, self).__init__(storageName, parent) - - def setSettings(self): - super().setSettings() - self.webSettings.setAttribute(QWebEngineSettings.JavascriptEnabled, - False) - self.webSettings.setAttribute(QWebEngineSettings.XSSAuditingEnabled, - True) - self.setPersistentCookiesPolicy(QWebEngineProfile.NoPersistentCookies) - self.setHttpCacheType(QWebEngineProfile.NoCache) - - -class IPFSProfile(BaseProfile): - """ - IPFS web profile - """ - - def __init__(self, storageName=WP_NAME_IPFS, parent=None): - super(IPFSProfile, self).__init__(storageName, parent) - - def setSettings(self): - super().setSettings() - self.webSettings.setAttribute(QWebEngineSettings.PluginsEnabled, - True) - self.webSettings.setAttribute(QWebEngineSettings.LocalStorageEnabled, - True) - - def installScripts(self): - exSc = self.webScripts.findScript('ipfs-http-client') - if self.app.settingsMgr.jsIpfsApi is True and exSc.isNull(): - for script in self.app.scriptsIpfs: - self.webScripts.insert(script) - - def installIpfsSchemeHandlersOld(self): - # XXX Remove fs: soon - for scheme in [SCHEME_DWEB, SCHEME_FS]: - self.installUrlSchemeHandler( - scheme.encode(), self.app.ipfsSchemeHandler) - - for scheme in [SCHEME_IPFS, SCHEME_IPNS]: - self.installUrlSchemeHandler( - scheme.encode(), self.app.nativeIpfsSchemeHandler) - - self.installUrlSchemeHandler( - SCHEME_ENS.encode(), self.app.ensSchemeHandler) - - -class Web3Profile(IPFSProfile): - """ - Web3 profile. Derives from the IPFS profile and - adds an injection script to provide window.web3 - """ - - def __init__(self, storageName=WP_NAME_WEB3, parent=None): - super(Web3Profile, self).__init__(storageName, parent) - - self.installWeb3Scripts() - - def installWeb3Scripts(self): - if self.app.settingsMgr.ethereumEnabled: - ethereumScripts = ethereumClientScripts( - self.app.getEthParams()) - if ethereumScripts: - [self.webScripts.insert(script) for script in - ethereumScripts] diff --git a/galacteek/browser/webprofiles/__init__.py b/galacteek/browser/webprofiles/__init__.py new file mode 100644 index 00000000..bc3f4eb6 --- /dev/null +++ b/galacteek/browser/webprofiles/__init__.py @@ -0,0 +1,235 @@ +from PyQt5.QtWidgets import QApplication +from PyQt5.QtWebEngineWidgets import QWebEngineProfile +from PyQt5.QtWebEngineWidgets import QWebEngineSettings +from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInterceptor + +from galacteek.dweb.webscripts import ethereumClientScripts + +from galacteek.browser.schemes import SCHEME_DWEB +from galacteek.browser.schemes import SCHEME_ENS +from galacteek.browser.schemes import SCHEME_ENSR +from galacteek.browser.schemes import SCHEME_FS +from galacteek.browser.schemes import SCHEME_IPFS +from galacteek.browser.schemes import SCHEME_IPNS +from galacteek.browser.schemes import SCHEME_Q +# from galacteek.browser.schemes import SCHEME_GALACTEEK +from galacteek.browser.schemes import isIpfsUrl + +from galacteek.config import cGet +from galacteek.config import cSet + + +WP_NAME_ANON = 'anonymous' +WP_NAME_MINIMAL = 'minimal' +WP_NAME_IPFS = 'ipfs' +WP_NAME_WEB3 = 'web3' + + +webProfilesPrio = { + WP_NAME_MINIMAL: 0, + WP_NAME_IPFS: 1, + WP_NAME_WEB3: 2 +} + + +class IPFSRequestInterceptor(QWebEngineUrlRequestInterceptor): + """ + IPFS requests interceptor + """ + + def interceptRequest(self, info): + url = info.requestUrl() + + if url and url.isValid() and isIpfsUrl(url): + path = url.path() + + # Force Content-type for JS modules + if path and path.endswith('.js'): + info.setHttpHeader( + 'Content-Type'.encode(), + 'text/javascript'.encode() + ) + + +class BaseProfile(QWebEngineProfile): + def __init__(self, name, config, parent=None): + super(BaseProfile, self).__init__( + name, parent) + + self.config = config + self.profileName = name + + self.app = QApplication.instance() + self.webScripts = self.scripts() + self.webSettings = self.settings() + + self.iceptor = IPFSRequestInterceptor(self) + self.setUrlRequestInterceptor(self.iceptor) + self.installIpfsSchemeHandlers() + self.installScripts() + + self.downloadRequested.connect( + self.app.downloadsManager.onDownloadRequested) + + def installHandler(self, scheme, handler): + sch = scheme if isinstance(scheme, bytes) else scheme.encode() + self.installUrlSchemeHandler(sch, handler) + + def installScripts(self): + scriptsList = self.config.get('scripts') + + if not scriptsList: + return + + for script in scriptsList: + if script.get('type') == 'builtin': + if script.get('name') == 'js-ipfs-client': + self.installIpfsClientScript() + elif script.get('name') == 'ethereum-web3': + self.installIpfsClientScript() + + def installIpfsClientScript(self): + exSc = self.webScripts.findScript('ipfs-http-client') + if exSc.isNull(): + for script in self.app.scriptsIpfs: + self.webScripts.insert(script) + + def installWeb3Scripts(self): + ethereumScripts = ethereumClientScripts( + self.app.getEthParams()) + if ethereumScripts: + [self.webScripts.insert(script) for script in + ethereumScripts] + + def installIpfsSchemeHandlers(self): + # XXX Remove fs: soon + for scheme in [SCHEME_DWEB, SCHEME_FS]: + self.installHandler(scheme, self.app.dwebSchemeHandler) + + for scheme in [SCHEME_IPFS, SCHEME_IPNS]: + self.installHandler(scheme, self.app.nativeIpfsSchemeHandler) + + self.installHandler(SCHEME_ENS, self.app.ensProxySchemeHandler) + self.installHandler(SCHEME_ENSR, self.app.ensSchemeHandler) + self.installHandler(SCHEME_Q, self.app.qSchemeHandler) + # self.installHandler(SCHEME_GALACTEEK, self.app.gSchemeHandler) + + def profileSetting(self, defaults, name, default=False): + return self.config.settings.get( + name, + defaults.settings.get(name, default) + ) + + def profileJsSetting(self, defaults, name, default=False): + return self.config.settings.javascript.get( + name, + defaults.settings.javascript.get(name, default) + ) + + def profileFont(self, name, default=None): + return self.config.fonts.get(name, default) + + def configure(self, defaults={}): + self.webSettings.setAttribute( + QWebEngineSettings.FullScreenSupportEnabled, + self.profileSetting(defaults, 'fullScreenSupport') + ) + + self.webSettings.setAttribute( + QWebEngineSettings.PluginsEnabled, + self.profileSetting(defaults, 'plugins') + ) + + self.webSettings.setAttribute( + QWebEngineSettings.LocalStorageEnabled, + self.profileSetting(defaults, 'localStorage', True) + ) + + self.webSettings.setFontFamily( + QWebEngineSettings.StandardFont, + self.profileFont('standard') + ) + self.webSettings.setFontFamily( + QWebEngineSettings.FixedFont, + self.profileFont('fixed') + ) + self.webSettings.setFontFamily( + QWebEngineSettings.SerifFont, + self.profileFont('serif') + ) + self.webSettings.setFontSize( + QWebEngineSettings.MinimumFontSize, + self.profileSetting(defaults, 'minFontSize', 12) + ) + self.webSettings.setFontSize( + QWebEngineSettings.DefaultFontSize, + self.profileSetting(defaults, 'defaultFontSize', 12) + ) + self.webSettings.setUnknownUrlSchemePolicy( + QWebEngineSettings.DisallowUnknownUrlSchemes + ) + + cacheType = self.profileSetting(defaults, 'cacheType', 'nocache') + + if cacheType == 'nocache': + self.setHttpCacheType(QWebEngineProfile.NoCache) + elif cacheType == 'memory': + self.setHttpCacheType(QWebEngineProfile.MemoryHttpCache) + elif cacheType == 'disk': + self.setHttpCacheType(QWebEngineProfile.DiskHttpCache) + else: + self.setHttpCacheType(QWebEngineProfile.NoCache) + + cookiesPolicy = self.profileSetting( + defaults, 'cookiesPolicy', 'none') + + if cookiesPolicy == 'none': + self.setPersistentCookiesPolicy( + QWebEngineProfile.NoPersistentCookies) + elif cookiesPolicy == 'allow': + self.setPersistentCookiesPolicy( + QWebEngineProfile.AllowPersistentCookies) + elif cookiesPolicy == 'force': + self.setPersistentCookiesPolicy( + QWebEngineProfile.ForcePersistentCookies) + + self.webSettings.setAttribute( + QWebEngineSettings.JavascriptEnabled, + self.profileJsSetting(defaults, 'enabled') + ) + self.webSettings.setAttribute( + QWebEngineSettings.JavascriptCanOpenWindows, + self.profileJsSetting(defaults, 'canOpenWindows') + ) + self.webSettings.setAttribute( + QWebEngineSettings.JavascriptCanAccessClipboard, + self.profileJsSetting(defaults, 'canAccessClipboard') + ) + + self.webSettings.setAttribute( + QWebEngineSettings.XSSAuditingEnabled, + self.profileSetting(defaults, 'xssAuditing') + ) + + +def wpRegisterFromConfig(app): + from galacteek.config import merge + + cfgWpList = cGet('webprofiles') + + defaults = cfgWpList.get('defaultProfile', None) + + for wpName, config in cfgWpList.items(): + if wpName == 'defaultProfile': + continue + + if defaults: + config = merge(config, defaults) + cfgWpList[wpName] = config + + cSet(f'webprofiles.{wpName}', config, merge=True) + + wp = BaseProfile(wpName, config, parent=app) + wp.configure(defaults=defaults) + + app.webProfiles[wpName] = wp diff --git a/galacteek/browser/webprofiles/config.yaml b/galacteek/browser/webprofiles/config.yaml new file mode 100644 index 00000000..c92775c6 --- /dev/null +++ b/galacteek/browser/webprofiles/config.yaml @@ -0,0 +1,68 @@ +envs: + default: + defaultWebProfile: 'minimal' + + # Web profiles definition + webprofiles: + anonymous: + descr: Anonymous profile + + settings: + javascript: + enabled: False + + cacheType: 'nocache' + cookiesPolicy: 'deny' + offTheRecord: True + + contexts: + tor: + settings: + javascript: + enabled: False + + ipfs: + descr: IPFS profile + + scripts: + - type: 'builtin' + name: 'js-ipfs-client' + + web3: + descr: Web3 profile + scripts: + - type: 'builtin' + name: 'js-ipfs-client' + - type: 'builtin' + name: 'ethereum-web3' + + minimal: + descr: Minimal profile + + defaultProfile: + descr: Default profile + + settings: + fullScreenSupport: True + plugins: True + localStorage: True + + minFontSize: 14 + defaultFontSize: 14 + + xssAuditing: False + cacheType: 'nocache' + cookiesPolicy: 'allow' + offTheRecord: False + + javascript: + enabled: True + canOpenWindows: False + canAccessClipboard: False + allowWindowActivation: False + + fonts: + standard: 'Inter UI' + fixed: 'Inter UI' + serif: 'Inter UI' + sansSerif: 'Inter UI' diff --git a/galacteek/config/__init__.py b/galacteek/config/__init__.py index 55acedc7..99ac1305 100644 --- a/galacteek/config/__init__.py +++ b/galacteek/config/__init__.py @@ -1,3 +1,4 @@ +import weakref import attr import inspect from pathlib import Path @@ -14,6 +15,7 @@ from .util import configFromFile from .util import environment from .util import empty +from .util import dictDotLeaves class NestedNamespace(SimpleNamespace): @@ -26,7 +28,6 @@ def __init__(self, dictionary, **kwargs): self.__setattr__(key, value) -gConf = OmegaConf.create({}) cCache = {} configSaveRootPath = None @@ -83,8 +84,6 @@ def regConfigFromFile(pkgName: str, fpath: str): # Merge existing eCfgAll, eCfg = configFromFile(str(savePath)) if eCfg: - # cfg = merge(eCfg, cfg) - # cfg = merge(cfg, eCfg) cfgAll = merge(cfgAll, eCfgAll) savePath.parent.mkdir(parents=True, exist_ok=True) @@ -93,7 +92,8 @@ def regConfigFromFile(pkgName: str, fpath: str): cCache[pkgName] = { 'configAll': cfgAll, 'config': cfg, - 'path': savePath + 'path': savePath, + 'callbacks': [] } return cCache[pkgName] @@ -127,37 +127,53 @@ def regConfigFromPyPkg(pkgName: str): regConfigFromFile(modName, fpath) -def cAttr(mod, cfg, attr, value=None): - environ = environment() - env = environ['env'] +def configModLeafAttributes(pkgName: str): + global cCache - cfgMove = cfg['envs'].setdefault(env, empty()) + eConf = cCache.get(pkgName, None) + if not eConf: + return - attrs = attr.split('.') + conf = eConf['config'] - if len(attrs) == 0: - return cfg.get(attr) + try: + return dictDotLeaves(OmegaConf.to_container(conf)) + except Exception: + return [] - aCur = attrs.pop(0) - while aCur: - try: - val = getattr(cfgMove, aCur) - except Exception: - return None - try: - aCur = attrs.pop(0) - except: - break +def configModules(): + global cCache + return cCache.keys() + + +def configLeafAttributes(): + global cCache + + for modName, cEntry in cCache.items(): + attrs = configModLeafAttributes(modName) + + for attribute in attrs: + yield modName, attribute + - if val: - cfgMove = val +def cAttr(mod, cfg, attr, value=None): + environ = environment() + env = environ['env'] + + conf = cfg['envs'].setdefault(env, empty()) - if value: - setattr(cfgMove, aCur, value) - configSavePackage(mod) + if value is not None: + # Set + try: + OmegaConf.update(conf, attr, value, merge=False) + except Exception: + log.debug(f'{mod}: cannot set value for attribute {attr}') + else: + configSavePackage(mod) else: - return getattr(cfgMove, aCur) + # Get + return OmegaConf.select(conf, attr) def callerMod(): @@ -197,6 +213,17 @@ def cSet(attr: str, value, mod=None): if cEntry: cAttr(mod, cEntry['configAll'], attr, value) + for ref in cEntry['callbacks']: + try: + callback = ref() + callback() + except Exception: + pass + + +def cModuleSave(): + configSavePackage(callerMod()) + @attr.s(auto_attribs=True) class ModuleConfigContext: @@ -220,6 +247,16 @@ def cModuleContext(mod: str): raise Exception(f'No config for module {mod}') +def configModRegCallback(callback): + global cCache + + pkgName = callerMod() + + eConf = cCache.get(pkgName) + if eConf: + eConf['callbacks'].append(weakref.ref(callback)) + + def initFromTable(): from galacteek.config.table import cfgInitTable @@ -228,8 +265,8 @@ def initFromTable(): try: regConfigFromPyPkg(pkg) - except Exception: - log.debug(f'Failed to load config from package: {pkg}') + except Exception as err: + log.debug(f'Failed to load config from package {pkg}: {err}') continue return True diff --git a/galacteek/config/table.py b/galacteek/config/table.py index 5e6c100c..ccbb1d57 100644 --- a/galacteek/config/table.py +++ b/galacteek/config/table.py @@ -1,6 +1,19 @@ cfgInitTable = [ - 'galacteek.services.bitmessage', - 'galacteek.services.tor', + 'galacteek.application', + 'galacteek.core.ctx', + 'galacteek.did.ipid', + 'galacteek.ipfs', + 'galacteek.ipfs.ipfsops', + 'galacteek.services.net.bitmessage', + 'galacteek.services.net.tor', 'galacteek.browser.schemes', - 'galacteek.ui' + 'galacteek.ui', + 'galacteek.ui.files', + 'galacteek.ui.history', + 'galacteek.ui.messenger', + + # browser + 'galacteek.browser.webprofiles', + + 'galacteek.ui.browser' ] diff --git a/galacteek/config/util.py b/galacteek/config/util.py index 9d88ecc5..f2f0fc9b 100644 --- a/galacteek/config/util.py +++ b/galacteek/config/util.py @@ -96,3 +96,29 @@ def configFromFile(fpath: Path, return None return configAll, top + + +def dictDotLeaves(dic: dict, parent=None, leaves=None): + """ + For a dictionary, return the list of leaf nodes, + using a dot-style notation + + :rtype: list + + """ + leaves = leaves if isinstance(leaves, list) else [] + + # for node in dic.keys(): + for node, subnode in dic.items(): + # subnode = dic[node] + + if isinstance(subnode, dict): + dictDotLeaves( + subnode, + parent=f'{parent}.{node}' if parent else node, + leaves=leaves + ) + else: + leaves.append(f'{parent}.{node}' if parent else node) + + return leaves diff --git a/galacteek/core/__init__.py b/galacteek/core/__init__.py index 4e94b307..b6ff93a7 100644 --- a/galacteek/core/__init__.py +++ b/galacteek/core/__init__.py @@ -58,7 +58,7 @@ def pkgResourcesListDir(pkg: str, rscName: str): def pkgResourcesDirEntries(pkg: str): for entry in pkgResourcesListDir(pkg, ''): - if entry.startswith('__'): + if entry.startswith('_'): continue yield entry @@ -87,6 +87,16 @@ def readQrcTextFile(path): pass +def readQrcFileRaw(path): + try: + qFile = QFile(path) + qFile.open(QFile.ReadOnly) + ba = qFile.readAll() + return bytes(ba.data()) + except BaseException: + pass + + def qrcWriteToTemp(path, delete=False): try: tmpf = tempfile.NamedTemporaryFile(delete=delete) diff --git a/galacteek/core/asynclib.py b/galacteek/core/asynclib.py index cf01a521..a4773d34 100644 --- a/galacteek/core/asynclib.py +++ b/galacteek/core/asynclib.py @@ -25,9 +25,12 @@ class AsyncSignal(UserList): def __init__(self, *signature, **kw): super().__init__() - self._id = kw.pop('_id', 'no id') + self.eventFired = asyncio.Event() + + self._id = kw.pop('_id', 'No ID') self._sig = signature self._loop = asyncio.get_event_loop() # loop attached to this signal + self._emitCount = 0 def __str__(self): return ''.format( @@ -53,6 +56,14 @@ def emitSafe(self, *args, **kwargs): self._loop ) + async def fire(self): + from galacteek import log + try: + self.eventFired.clear() + self.eventFired.set() + except Exception as err: + log.debug(f'Could not fire signal event: {err}') + async def emit(self, *args, **kwargs): from galacteek import log @@ -87,6 +98,9 @@ async def emit(self, *args, **kwargs): self, cbk=receiver, e=str(err))) traceback.print_exc() continue + else: + self._emitCount += 0 + await self.fire() def loopTime(): @@ -102,6 +116,20 @@ def ensureGenericCallback(future): traceback.print_exc() +def ensureSafe(coro, **kw): + loop = kw.pop('loop', asyncio.get_event_loop()) + callback = kw.pop('futcallback', ensureGenericCallback) + + future = asyncio.run_coroutine_threadsafe( + coro, + loop + ) + if callback: + future.add_done_callback(callback) + + return future + + def ensure(coro, **kw): """ 'futcallback' should not be used in the coroutine's kwargs """ @@ -127,19 +155,10 @@ def ensure(coro, **kw): return future -def ensureSafe(coro, *kw): - loop = asyncio.get_event_loop() - return asyncio.run_coroutine_threadsafe( - coro, - loop - ) - - -def partialEnsure(coro, *args, **kw): +def partialEnsureSafe(coro, *args, **kw): loop = asyncio.get_event_loop() def _pwrapper(coro, *args, **kw): - # ensure(coro(*args, **kw)) asyncio.run_coroutine_threadsafe( coro(*args, **kw), loop @@ -148,6 +167,9 @@ def _pwrapper(coro, *args, **kw): return functools.partial(_pwrapper, coro, *args, **kw) +partialEnsure = partialEnsureSafe + + def soonish(cbk, *args, **kw): """ Soon. Or a bit later .. """ loop = kw.pop('loop', asyncio.get_event_loop()) @@ -267,11 +289,15 @@ async def asyncWriteFile(path, data, mode='w+b'): return None -async def threadExec(fn, *args): +async def threadExec(fn, *args, processor=None): loop = asyncio.get_event_loop() with QThreadExecutor(1) as texec: - return await loop.run_in_executor(texec, fn, *args) + res = await loop.run_in_executor(texec, fn, *args) + if asyncio.iscoroutinefunction(processor): + return await processor(res) + + return res def _all_tasks(loop=None): @@ -323,12 +349,19 @@ def _warn_pending(): async def asyncRmTree(path: str): + from galacteek import log + loop = asyncio.get_event_loop() + def _rmtree(dirpath): + shutil.rmtree(dirpath, ignore_errors=True) + if os.path.isdir(path): + log.debug(f'asyncRmTree: {path}') + return await loop.run_in_executor( None, - shutil.rmtree, + _rmtree, path ) diff --git a/galacteek/core/ctx.py b/galacteek/core/ctx/__init__.py similarity index 100% rename from galacteek/core/ctx.py rename to galacteek/core/ctx/__init__.py diff --git a/galacteek/core/ctx/config.yaml b/galacteek/core/ctx/config.yaml new file mode 100644 index 00000000..f4d3ea57 --- /dev/null +++ b/galacteek/core/ctx/config.yaml @@ -0,0 +1,14 @@ +envs: + default: + peers: + didLoadTimeout: 10 + didLoadAttempts: 5 + + liveness: + pingDelay: 300 + + didFail: + sleepInterval: 60 + + watcherTask: + sleepInterval: 180 diff --git a/galacteek/core/models/bm.py b/galacteek/core/models/bm.py new file mode 100644 index 00000000..3733e97c --- /dev/null +++ b/galacteek/core/models/bm.py @@ -0,0 +1,40 @@ +from PyQt5.QtCore import QModelIndex +from PyQt5.QtCore import Qt + +from PyQt5.QtGui import QStandardItemModel +from PyQt5.QtGui import QStandardItem + +from galacteek import database + + +class BMContactsModel(QStandardItemModel): + def __init__(self, parent=None): + super(BMContactsModel, self).__init__(parent) + + @property + def root(self): + return self.invisibleRootItem() + + async def update(self): + contacts = await database.bmContactAll() + + try: + for contact in contacts: + idxList = self.match( + self.index(0, 0, QModelIndex()), + Qt.DisplayRole, + contact.bmAddress, + -1, + Qt.MatchFixedString | Qt.MatchWrap | Qt.MatchRecursive + ) + + if len(idxList) > 0: + # Already have this contact + continue + + self.root.appendRow([ + QStandardItem(contact.bmAddress), + QStandardItem(contact.fullname) + ]) + except Exception: + pass diff --git a/galacteek/core/ps.py b/galacteek/core/ps.py index 0365b330..338e5706 100644 --- a/galacteek/core/ps.py +++ b/galacteek/core/ps.py @@ -60,11 +60,15 @@ def psSubscriber(sid): keyServices = aiopubsub.Key('g', 'services') keySmartContracts = aiopubsub.Key('g', 'smartcontracts', '*') +# The answer to everything +key42 = aiopubsub.Key('g', '42') + mSubscriber = psSubscriber('main') def hubPublish(key, message): - log.debug(f'hubPublish \\ {key} // {message}') + log.debug(f'hubPublish ({key}) : {message}') + gHub.publish(key, message) diff --git a/galacteek/crypto/qrcode.py b/galacteek/crypto/qrcode/__init__.py similarity index 77% rename from galacteek/crypto/qrcode.py rename to galacteek/crypto/qrcode/__init__.py index 3d81eae9..0c7675fe 100644 --- a/galacteek/crypto/qrcode.py +++ b/galacteek/crypto/qrcode/__init__.py @@ -2,6 +2,9 @@ import io import functools import platform +import re + +from urllib.parse import parse_qs from ctypes import cdll from ctypes.util import find_library @@ -80,6 +83,69 @@ def _getImage(self, data): return image +class ZbarCryptoCurrencyQrDecoder(ImageReader): + """ + Decodes Crypto QR codes with the zbar library + """ + + def decode(self, data): + """ + :param bytes data: Raw image data or Pillow image + :rtype: list + """ + + if isinstance(data, bytes): + image = self._getImage(data) + elif isinstance(data, Image.Image): + image = data + else: + raise Exception('Need bytes or PIL.Image') + + if image is None: + return None + + try: + objects = zbar_decode(image) + + addrs = [] + for obj in objects: + if not isinstance(obj.data, bytes): + continue + + try: + decoded = obj.data.decode() + except BaseException as err: + log.debug(f'pyzbar decode: error decoding item: {err}') + continue + + # BTC + ma = re.match( + r'^bitcoin:(?P[\w]{27,42})\??(?P[\w\=\.\?]*)?', + decoded + ) + if ma: + gd = ma.groupdict() + query = parse_qs(gd.get('qs')) + + amountl = query.get('amount') if query else None + try: + amount = float(amountl.pop()) + except Exception: + amount = None + + addrs.append({ + 'currency': 'BTC', + 'address': gd.get('addr'), + 'amount': amount + }) + + if len(addrs) > 0: # don't return empty list + return addrs + except Exception as err: + log.debug(f'pyzbar decode error: {err}') + return None + + class ZbarIPFSQrDecoder(ImageReader): """ Decodes IPFS QR codes with the zbar library @@ -350,6 +416,46 @@ def _encodeAllAppend(self, version=12): return imgMosaic +class CCQrEncoder: + def _newQrCode(self, qrVersion=1, border=0, boxsize=16, **custom): + return qrcode.QRCode( + version=qrVersion, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=boxsize, + border=border, + + **custom + ) + + def encodeUri(self, url, fillColor='black', backColor='white', + qrVersion=1, size=None): + """ Encode an URL and return a PIL image """ + qr = self._newQrCode(qrVersion=qrVersion) + qr.add_data(url) + qr.make() + + qrImage = qr.make_image(fill_color=fillColor, back_color=backColor) + img = qrImage.get_image() + + if size: + return img.resize(size) + + return img + + async def encode(self, url, + loop=None, executor=None, + version=1, + size=(256, 256)): + """ + Runs the encoding in an asyncio executor + """ + loop = loop if loop else asyncio.get_event_loop() + return await loop.run_in_executor( + executor, + functools.partial(self.encodeUri, url, qrVersion=version, + size=size)) + + def IPFSQrDecoder(): # Returns a suitable QR decoder depending on the available libs if haveZbar: diff --git a/galacteek/database/__init__.py b/galacteek/database/__init__.py index 861c24be..128cc032 100644 --- a/galacteek/database/__init__.py +++ b/galacteek/database/__init__.py @@ -16,8 +16,9 @@ from galacteek.core.asynclib import loopTime from galacteek.ipfs.cidhelpers import IPFSPath +from galacteek.database.models import * # noqa -from galacteek.database.models import * +from galacteek.database.ops.bm import * # noqa databaseLock = asyncio.Lock() @@ -510,33 +511,3 @@ async def browserFeaturePermissionAdd(url, featureCode, permission): ) await p.save() return p - - -# BM - -async def bmMailBoxRegister(bmAddr, label, mDirRelativePath, default=False): - mb = BitMessageMailBox( - bmAddress=bmAddr, - label=label, - mDirRelativePath=mDirRelativePath, - default=default - ) - - await mb.save() - return mb - - -async def bmMailBoxList(): - return await BitMessageMailBox.all() - - -async def bmMailBoxCount(): - return await BitMessageMailBox.all().count() - - -async def bmMailBoxGetDefault(): - return await BitMessageMailBox.filter(default=True).first() - - -async def bmMailBoxGet(bmAddress): - return await BitMessageMailBox.filter(bmAddress=bmAddress).first() diff --git a/galacteek/database/models/bm.py b/galacteek/database/models/bm.py index ebd5f6d9..deaa2155 100644 --- a/galacteek/database/models/bm.py +++ b/galacteek/database/models/bm.py @@ -9,24 +9,93 @@ class BitMessageMailBox(Model): id = fields.IntField(pk=True) bmAddress = fields.CharField(max_length=128, unique=True) - mDirType = fields.IntField(default=0) # Label and nickname associated label = fields.CharField(max_length=32, unique=True) nickname = fields.CharField(max_length=32, null=True) - fullname = fields.CharField(max_length=64, null=True) + fullname = fields.CharField(max_length=96, null=True) # Icon iconCid = fields.CharField(max_length=128, null=True) - # Sig - signature = fields.CharField(max_length=512, null=True) - - # Maildir path, relative to the root path where we store mailboxes + # Maildir type and path, relative to the root path where we store mailboxes + mDirType = fields.IntField(default=TYPE_REGULAR) mDirRelativePath = fields.CharField(max_length=128) active = fields.BooleanField(default=True) default = fields.BooleanField(default=False) + # Preferences + prefs: fields.OneToOneRelation["BitMessageMailBoxPrefs"] = \ + fields.OneToOneField( + 'models.BitMessageMailBoxPrefs', + on_delete=fields.CASCADE, + related_name='prefs' + ) + dateCreated = fields.DatetimeField(auto_now_add=True) dateALast = fields.DatetimeField(auto_now_add=True) + + +class BitMessageMailBoxPrefs(Model): + id = fields.IntField(pk=True) + + # Sig + signature = fields.CharField(max_length=512, null=True) + + # Default content-type when composing + cContentType = fields.CharField(max_length=32, default='text/plain') + markupType = fields.CharField(max_length=32, default='markdown') + + # Compose options + cIgnoreNoSubject = fields.BooleanField(default=True) + + # Links options + linksOpen = fields.BooleanField(default=True) + + +class BitMessageContactGroup(Model): + id = fields.IntField(pk=True) + + # Group name + name = fields.CharField(max_length=64, unique=True) + + +class BitMessageBlackList(Model): + id = fields.IntField(pk=True) + + # Black-listed contact's BM address + bmAddress = fields.CharField(max_length=128, unique=True) + + # Black-listed rule counter + hitCount = fields.IntField(default=0) + + enabled = fields.BooleanField(default=True) + dateCreated = fields.DatetimeField(auto_now_add=True) + + +class BitMessageContact(Model): + id = fields.IntField(pk=True) + + # Contact's BM address + bmAddress = fields.CharField(max_length=128, unique=True) + + # fullname + fullname = fields.CharField(max_length=96) + + # Associated icon + iconCid = fields.CharField(max_length=128, null=True) + + cSeparator = fields.CharField(max_length=3, default='') + + did = fields.CharField(max_length=256, null=True) + + dateCreated = fields.DatetimeField(auto_now_add=True) + dateALast = fields.DatetimeField(auto_now_add=True) + + group = fields.ForeignKeyField( + 'models.BitMessageContactGroup', + related_name='group', + through='bm_contact_group', + null=True, + description='BM group') diff --git a/galacteek/services/tor/__init__.py b/galacteek/database/ops/__init__.py similarity index 100% rename from galacteek/services/tor/__init__.py rename to galacteek/database/ops/__init__.py diff --git a/galacteek/database/ops/bm.py b/galacteek/database/ops/bm.py new file mode 100644 index 00000000..2a77f5d8 --- /dev/null +++ b/galacteek/database/ops/bm.py @@ -0,0 +1,146 @@ +from tortoise.query_utils import Q + +from galacteek import log + +from galacteek.database.models.bm import BitMessageMailBox +from galacteek.database.models.bm import BitMessageMailBoxPrefs +from galacteek.database.models.bm import BitMessageContact +from galacteek.database.models.bm import BitMessageContactGroup + +# BM + + +async def bmMailBoxList(): + return await BitMessageMailBox.all() + + +async def bmMailBoxCount(): + return await BitMessageMailBox.all().count() + + +async def bmMailBoxGetDefault(): + """ + Return the default BM account, if there's any + """ + return await BitMessageMailBox.filter( + default=True).prefetch_related('prefs').first() + + +async def bmMailBoxSetDefault(bmAddress: str, + mailbox: BitMessageMailBox = None): + """ + + Set the default BM account + """ + + mbox = mailbox if mailbox else await bmMailBoxGet(bmAddress) + if mbox: + try: + await BitMessageMailBox.all().update(default=False) + mbox.default = True + await mbox.save() + except Exception as err: + log.debug(f'Could not set default BM account {bmAddress}: {err}') + return False + else: + return True + + +async def bmMailBoxGet(bmAddress: str): + return await BitMessageMailBox.filter( + bmAddress=bmAddress).prefetch_related('prefs').first() + + +async def bmMailBoxRegister(bmAddress: str, + label: str, + mDirRelativePath: str, + default=False, + cContentType='text/plain', + cTextMarkup='markdown', + iconCid=None): + try: + prefs = BitMessageMailBoxPrefs( + cContentType=cContentType, + markupType=cTextMarkup + ) + await prefs.save() + + mb = BitMessageMailBox( + bmAddress=bmAddress, + label=label, + mDirRelativePath=mDirRelativePath, + iconCid=iconCid, + prefs_id=prefs.id + ) + + await mb.save() + await mb.fetch_related('prefs') + + if default is True: + await bmMailBoxSetDefault( + bmAddress, + mailbox=mb + ) + + return mb + except Exception as cerr: + log.debug(f'Could not create BM account {bmAddress}: {cerr}') + + +# Groups + + +# Contacts + + +async def bmContactAdd(bmAddress: str, + fullname: str, + separator: str = '', + groupName: str = '', + did=None): + from galacteek.services.net.bitmessage import bmAddressValid + + try: + assert bmAddressValid(bmAddress) is True + assert len(fullname) in range(1, 96) + + group = None + if groupName: + group = await BitMessageContactGroup.filter( + name=groupName).first() + if not group: + group = BitMessageContactGroup(name=groupName) + await group.save() + + contact = BitMessageContact( + bmAddress=bmAddress, + fullname=fullname, + cSeparator=separator, + did=did, + group=group + ) + await contact.save() + except Exception as cerr: + log.debug(f'Could not add BM contact {bmAddress}: {cerr}') + else: + return contact + + +async def bmContactFilter(name: str, + separator: str = ''): + return BitMessageContact.filter( + Q(fullname__icontains=name) & Q(cSeparator=separator)) + + +async def bmContactByName(name: str, + separator: str = ''): + return await (await bmContactFilter(name, separator)).all() + + +async def bmContactByNameFirst(name: str, + separator: str = ''): + return await (await bmContactFilter(name, separator)).first() + + +async def bmContactAll(): + return await BitMessageContact.all() diff --git a/galacteek/did/ipid/__init__.py b/galacteek/did/ipid/__init__.py index 2a10704a..0c104cad 100644 --- a/galacteek/did/ipid/__init__.py +++ b/galacteek/did/ipid/__init__.py @@ -20,6 +20,7 @@ from galacteek import ensureLater from galacteek import log from galacteek.core import SingletonDecorator +from galacteek.config import cGet from galacteek.ipfs.wrappers import ipfsOp from galacteek.ipfs.cidhelpers import cidValid @@ -654,10 +655,17 @@ async def updateDocument(self, ipfsop, document, publish=False): self.message('Could not inject new DID document!') @ipfsOp - async def resolve(self, ipfsop, resolveTimeout=30): + async def resolve(self, ipfsop, resolveTimeout=None): + resolveTimeout = resolveTimeout if resolveTimeout else \ + cGet('resolve.timeout') + + if self.local: + maxLifetime = cGet('resolve.cacheLifetime.local') + else: + maxLifetime = cGet('resolve.cacheLifetime.default') + useCache = 'always' cache = 'always' - maxLifetime = 86400 * 20 if self.local else 86400 * 30 self.message('DID resolve: {did} (using cache: {usecache})'.format( did=self.ipnsKey, usecache=useCache)) @@ -682,7 +690,7 @@ async def refresh(self): async def load(self, ipfsop, pin=True, initialCid=None, resolveTimeout=30): if not initialCid: - resolved = await self.resolve(resolveTimeout=resolveTimeout) + resolved = await self.resolve() if not resolved: self.message('Failed to resolve ?') @@ -726,7 +734,7 @@ async def load(self, ipfsop, pin=True, initialCid=None, return False @ipfsOp - async def publish(self, ipfsop, timeout=60 * 5, autoRepublish=False): + async def publish(self, ipfsop, timeout=None): """ Publish the DID document to the IPNS key @@ -736,22 +744,29 @@ async def publish(self, ipfsop, timeout=60 * 5, autoRepublish=False): :rtype: bool """ + # Get config settings + timeout = timeout if timeout else cGet('publish.ipns.timeout') + autoRepublish = cGet('publish.autoRepublish') + republishDelay = cGet('publish.autoRepublishDelay') + + ipnsLifetime = cGet('publish.ipns.lifetime') + ipnsTtl = cGet('publish.ipns.ttl') + if not self.docCid: return False if autoRepublish is True: # Auto republish for local IPIDs ensureLater( - 60 * 10, - self.publish, - autoRepublish=autoRepublish, - timeout=timeout + republishDelay, + self.publish ) try: if await ipfsop.publish(self.docCid, key=self.ipnsKey, - lifetime='96h', + lifetime=ipnsLifetime, + ttl=ipnsTtl, cache='always', cacheOrigin='ipidmanager', timeout=timeout): @@ -881,8 +896,7 @@ async def removeServiceById(self, _id: str): self._document['service'].remove(srv) await self.flush() except Exception as err: - print(str(err)) - pass + log.debug(str(err)) async def avatarService(self): avatarServiceId = self.didUrl(path='/avatar') @@ -933,10 +947,10 @@ def __str__(self): @SingletonDecorator class IPIDManager: - def __init__(self, resolveTimeout=60 * 5): + def __init__(self): self._managedIdentifiers = {} self._lock = asyncio.Lock() - self._resolveTimeout = resolveTimeout + self._resolveTimeout = cGet('resolve.timeout') self._rsaExec = RSAExecutor() # JSON-LD cache @@ -1013,7 +1027,7 @@ async def load(self, ipfsop, if localIdentifier: log.debug('Publishing (first load) local IPID: {}'.format(did)) - ensure(ipid.publish(autoRepublish=True)) + ensure(ipid.publish()) return ipid @@ -1079,7 +1093,7 @@ async def create(self, ipfsop, }) # Publish the DID document to the key - ensure(identifier.publish(autoRepublish=True)) + ensure(identifier.publish()) if track: await self.track(identifier) @@ -1127,8 +1141,6 @@ async def didAuthPerform(self, ipfsop, streamCtx, ipid, token=None): req['ident_token'] = token try: - # log.debug(f'didAuthPerform, sending request: {req}') - async with streamCtx.session as session: async with session.post( streamCtx.httpUrl('/auth'), diff --git a/galacteek/did/ipid/config.yaml b/galacteek/did/ipid/config.yaml new file mode 100644 index 00000000..2aeb7a7f --- /dev/null +++ b/galacteek/did/ipid/config.yaml @@ -0,0 +1,21 @@ +envs: + default: + publish: + # Automatically republish local IPID + autoRepublish: True + + # Republish delay (seconds) + autoRepublishDelay: 600 + + ipns: + timeout: 300 + lifetime: '48h' + ttl: '12h' + + resolve: + # Timeout in seconds for resolving IPID documents + timeout: 30 + + cacheLifetime: + default: 5184000 + local: 1728000 diff --git a/galacteek/docs/manual/en/messenger.rst b/galacteek/docs/manual/en/messenger.rst index 617a7c3e..a6b26a5d 100644 --- a/galacteek/docs/manual/en/messenger.rst +++ b/galacteek/docs/manual/en/messenger.rst @@ -3,12 +3,18 @@ Messenger ========= -.. image:: ../../../../share/dmessenger/dmessenger.png +.. image:: ../../../../share/icons/dmessenger/dmessenger.png :width: 64 :height: 64 The decentralized messenger uses the Bitmessage_ protocol to send and receive mails. -.. _Bitmessage: https://wiki.bitmessage.org/Bitmessage%20Technical%20Paper.pdf +Sending +------- + +The bitmessage client will always compute a Proof-of-work when sending +a message. You'll notice a CPU spike in the *notbit* process, which +is normal. +.. _Bitmessage: https://wiki.bitmessage.org/Bitmessage%20Technical%20Paper.pdf diff --git a/galacteek/docs/manual/en/themes/galacteek/static/galacteek.css_t b/galacteek/docs/manual/en/themes/galacteek/static/galacteek.css_t index d57bb607..2dcb04fb 100644 --- a/galacteek/docs/manual/en/themes/galacteek/static/galacteek.css_t +++ b/galacteek/docs/manual/en/themes/galacteek/static/galacteek.css_t @@ -8,12 +8,18 @@ */ @import url("basic.css"); + +@font-face { + font-family: 'Segoe UI'; + src: url('../../../../share/static/fonts/SegoeUI.woff2') format('woff2'); + font-weight: normal; + font-style: normal; +} /* -- page layout ----------------------------------------------------------- */ body { - font-family: "Nobile", sans-serif; - font-size: 110%; + font: 12pt "Segoe UI"; background-color: #fff; color: #555; margin: 0; diff --git a/galacteek/dweb/chanobjects/eth.py b/galacteek/dweb/chanobjects/eth.py index 8f3ede8d..d93008a5 100644 --- a/galacteek/dweb/chanobjects/eth.py +++ b/galacteek/dweb/chanobjects/eth.py @@ -1,7 +1,7 @@ from galacteek import log from galacteek.dweb.page import BaseHandler from galacteek.dweb.page import pyqtSlot -from galacteek.core.aservice import cached_property +from galacteek.services import cached_property from mode.utils.objects import cached_property # noqa diff --git a/galacteek/dweb/page.py b/galacteek/dweb/page.py index b7a08aec..689f6c81 100644 --- a/galacteek/dweb/page.py +++ b/galacteek/dweb/page.py @@ -6,6 +6,7 @@ from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal from PyQt5.QtCore import QJsonValue, QVariant, QUrl from PyQt5.QtCore import QTimer +from PyQt5.QtGui import QColor from PyQt5.QtWidgets import QApplication @@ -13,6 +14,7 @@ from galacteek.ui.helpers import questionBox from galacteek import ensure from galacteek import log +from galacteek.core import runningApp from galacteek.dweb.render import renderTemplate from galacteek.dweb.webscripts import ipfsClientScripts from galacteek.dweb.webscripts import orbitScripts @@ -267,8 +269,13 @@ def __init__(self, ipfsPath, parent=None): class DWebView(QWebEngineView): + """ + TODO: deprecate asap + """ + def __init__(self, page=None, parent=None): super(DWebView, self).__init__(parent) + self.app = runningApp() self.channel = QWebChannel() self.p = page self.show() @@ -286,6 +293,8 @@ def p(self, page): if page: self._currentPage = page self.setPage(page) + self.page().setBackgroundColor( + QColor(self.app.theme.colors.webEngineBackground)) class WebTab(GalacteekTab): diff --git a/galacteek/guientrypoint.py b/galacteek/guientrypoint.py index 75425cc2..91e3bb58 100644 --- a/galacteek/guientrypoint.py +++ b/galacteek/guientrypoint.py @@ -5,6 +5,8 @@ import sys import platform import os +import gc +import tracemalloc from PyQt5.QtCore import QProcess from PyQt5.QtWebEngine import QtWebEngine @@ -49,6 +51,8 @@ def startProcess(self, args): def galacteekGui(args): progName = args.binaryname if args.binaryname else sys.argv[0] + gc.enable() + if inPyInstaller(): folder = pyInstallerBundleFolder() binPath = folder.joinpath('bin') @@ -56,6 +60,9 @@ def galacteekGui(args): os.chdir(str(folder)) + if args.mallocdebug: + tracemalloc.start() + if args.debug: faulthandler.enable() @@ -185,11 +192,27 @@ def buildArgsParser(fromParser=None): action='store_true', dest='enablequest', help="Enable quest connector") + parser.add_argument( + '--force-fusion-style', + action='store_true', + default=True, + dest='forceFusion', + help="Force fusion style") parser.add_argument( '--goipfs-debug', action='store_true', dest='goipfsdebug', help="Enable go-ipfs daemon debug output") + parser.add_argument( + '--malloc-debug', + action='store_true', + dest='mallocdebug', + help="Enable malloc statistics") + parser.add_argument( + '--memory-profiling', + action='store_true', + dest='memprofiling', + help="Enable memory profiling") parser.add_argument( '--asyncio-tasks-debug', action='store_true', @@ -201,10 +224,9 @@ def buildArgsParser(fromParser=None): default='mainnet', dest='ethnet', help="ETH network name") - parser.add_argument( '--env', - default='mainnet', + default='main', dest='env', help="Environment") @@ -226,6 +248,9 @@ def gArgsParse(): os.environ['GALACTEEK_ETHNETWORK_ENV'] = args.ethnet os.environ['GALACTEEK_ENV'] = args.env + if args.forceFusion and 'QT_STYLE_OVERRIDE' not in os.environ: + os.environ['QT_STYLE_OVERRIDE'] = 'Fusion' + return args diff --git a/galacteek/i18n/__init__.py b/galacteek/i18n/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/galacteek/i18n/actions.py b/galacteek/i18n/actions.py new file mode 100644 index 00000000..c8c2cf74 --- /dev/null +++ b/galacteek/i18n/actions.py @@ -0,0 +1,8 @@ +from PyQt5.QtCore import QCoreApplication + + +def iClose(): + return QCoreApplication.translate( + 'Galacteek', + 'Close' + ) diff --git a/galacteek/i18n/blackhole.py b/galacteek/i18n/blackhole.py new file mode 100644 index 00000000..469a3364 --- /dev/null +++ b/galacteek/i18n/blackhole.py @@ -0,0 +1,211 @@ +from PyQt5.QtCore import QCoreApplication + +# Uncategorized translations, don't add anything here + + +def iNoStatus(): + return QCoreApplication.translate('GalacteekWindow', 'No status') + + +def iGeneralError(msg): + return QCoreApplication.translate('GalacteekWindow', + 'General error: {0}').format(msg) + + +def iPubSubSniff(): + return QCoreApplication.translate('GalacteekWindow', 'Pubsub sniffer') + + +def iBrowse(): + return QCoreApplication.translate('GalacteekWindow', 'Browse') + + +def iHelp(): + return QCoreApplication.translate('GalacteekWindow', 'Help') + + +def iFollow(): + return QCoreApplication.translate('BrowserTabForm', 'Follow') + + +def iMinimized(): + return QCoreApplication.translate( + 'GalacteekWindow', + 'galacteek was minimized to the system tray') + + +def iManual(): + return QCoreApplication.translate('GalacteekWindow', 'Manual') + + +def iFileManager(): + return QCoreApplication.translate('GalacteekWindow', 'File Manager') + + +def iTextEditor(): + return QCoreApplication.translate('GalacteekWindow', 'Editor') + + +def iKeys(): + return QCoreApplication.translate('GalacteekWindow', 'IPFS Keys') + + +def iSettings(): + return QCoreApplication.translate('GalacteekWindow', 'Settings') + + +def iEventLog(): + return QCoreApplication.translate('GalacteekWindow', 'Event Log') + + +def iNewProfile(): + return QCoreApplication.translate('GalacteekWindow', 'New Profile') + + +def iSwitchedProfile(): + return QCoreApplication.translate('GalacteekWindow', + 'Successfully switched profile') + + +def iDagViewer(): + return QCoreApplication.translate('GalacteekWindow', 'DAG viewer') + + +def iDagView(): + return QCoreApplication.translate('GalacteekWindow', 'DAG view') + + +def iParentDagView(): + return QCoreApplication.translate( + 'GalacteekWindow', 'DAG view (parent node)') + + +def iOpen(): + return QCoreApplication.translate('GalacteekWindow', 'Open') + + +def iNewBlogPost(): + return QCoreApplication.translate('GalacteekWindow', 'New blog post') + + +def iExploreDirectory(): + return QCoreApplication.translate('GalacteekWindow', 'Explore directory') + + +def iEditObject(): + return QCoreApplication.translate('GalacteekWindow', 'Edit') + + +def iEditInputObject(): + return QCoreApplication.translate('GalacteekWindow', 'Edit input') + + +def iDownload(): + return QCoreApplication.translate('GalacteekWindow', 'Download') + + +def iDownloads(): + return QCoreApplication.translate('GalacteekWindow', 'Downloads') + + +def iDownloadDirectory(): + return QCoreApplication.translate('GalacteekWindow', 'Download directory') + + +def iDownloadOpenDialog(): + return QCoreApplication.translate( + 'GalacteekWindow', 'Download or open IPFS object') + + +def iCancel(): + return QCoreApplication.translate('GalacteekWindow', 'Cancel') + + +def iChat(): + return QCoreApplication.translate('GalacteekWindow', 'Chat') + + +def iChatMessageNotification(channel, handle): + return QCoreApplication.translate( + 'GalacteekWindow', + '{0}: new message from {1}').format(channel, handle) + + +def iDelete(): + return QCoreApplication.translate('Galacteek', 'Delete') + + +def iRemove(): + return QCoreApplication.translate('Galacteek', 'Remove') + + +def iSeed(): + return QCoreApplication.translate('Galacteek', 'Seed') + + +def iShareFiles(): + return QCoreApplication.translate('Galacteek', 'Share files') + + +def iFileSharing(): + return QCoreApplication.translate('Galacteek', 'File sharing') + + +def iRemoveFileAsk(): + return QCoreApplication.translate( + 'Galacteek', + 'Are you sure you want to remove this file/directory ?' + ) + + +def iInvalidInput(): + return QCoreApplication.translate('GalacteekWindow', 'Invalid input') + + +def iKey(): + return QCoreApplication.translate('Galacteek', 'Key') + + +def iValue(): + return QCoreApplication.translate('Galacteek', 'Value') + + +def iTitle(): + return QCoreApplication.translate('GalacteekWindow', 'Title') + + +def iNoTitleProvided(): + return QCoreApplication.translate('GalacteekWindow', + 'Please specify a title') + + +def iFinished(): + return QCoreApplication.translate('GalacteekWindow', 'Finished') + + +def iDownloadOnly(): + return QCoreApplication.translate('GalacteekWindow', 'Download only') + + +def iZoomIn(): + return QCoreApplication.translate('GalacteekWindow', 'Zoom in') + + +def iZoomOut(): + return QCoreApplication.translate('GalacteekWindow', 'Zoom out') + + +def iQuit(): + return QCoreApplication.translate('GalacteekWindow', 'Quit') + + +def iRestart(): + return QCoreApplication.translate('GalacteekWindow', 'Restart') + + +def iClearHistory(): + return QCoreApplication.translate('GalacteekWindow', 'Clear history') + + +def iAtomFeeds(): + return QCoreApplication.translate('GalacteekWindow', 'Atom feeds') diff --git a/galacteek/i18n/browser.py b/galacteek/i18n/browser.py new file mode 100644 index 00000000..fecf9923 --- /dev/null +++ b/galacteek/i18n/browser.py @@ -0,0 +1,225 @@ +from PyQt5.QtCore import QCoreApplication + + +def iOpenInTab(): + return QCoreApplication.translate('BrowserTabForm', 'Open link in new tab') + + +def iOpenHttpInTab(): + return QCoreApplication.translate('BrowserTabForm', + 'Open http/https link in new tab') + + +def iOpenLinkInTab(): + return QCoreApplication.translate('BrowserTabForm', + 'Open link in new tab') + + +def iOpenWith(): + return QCoreApplication.translate('BrowserTabForm', 'Open with') + + +def iDownload(): + return QCoreApplication.translate('BrowserTabForm', 'Download') + + +def iSaveContainedWebPage(): + return QCoreApplication.translate( + 'BrowserTabForm', 'Save full webpage to the "Web Pages" folder') + + +def iPrintWebPageText(): + return QCoreApplication.translate( + 'BrowserTabForm', 'Print (text)') + + +def iSaveWebPageToPdfFile(): + return QCoreApplication.translate( + 'BrowserTabForm', 'Save webpage to PDF') + + +def iSaveWebPageToPdfFileError(): + return QCoreApplication.translate( + 'BrowserTabForm', 'Error saving webpage to PDF file') + + +def iSaveWebPageToPdfFileOk(path): + return QCoreApplication.translate( + 'BrowserTabForm', 'Saved to PDF file: {0}').format(path) + + +def iJsConsole(): + return QCoreApplication.translate('BrowserTabForm', 'Javascript console') + + +def iPin(): + return QCoreApplication.translate('BrowserTabForm', 'PIN') + + +def iPinThisPage(): + return QCoreApplication.translate('BrowserTabForm', 'PIN (this page)') + + +def iPinRecursive(): + return QCoreApplication.translate('BrowserTabForm', 'PIN (recursive)') + + +def iSaveSelectedText(): + return QCoreApplication.translate( + 'BrowserTabForm', 'Save selected text to IPFS') + + +def iLinkToQaToolbar(): + return QCoreApplication.translate( + 'BrowserTabForm', 'Link to Quick Access toolbar') + + +def iFollowIpns(): + return QCoreApplication.translate('BrowserTabForm', + 'Follow IPNS resource') + + +def iEnterIpfsCID(): + return QCoreApplication.translate('BrowserTabForm', 'Enter an IPFS CID') + + +def iBrowseHomePage(): + return QCoreApplication.translate('BrowserTabForm', + 'Go to home page') + + +def iBrowseIpfsCID(): + return QCoreApplication.translate('BrowserTabForm', + 'Browse IPFS resource (CID)') + + +def iBrowseIpfsMultipleCID(): + return QCoreApplication.translate('BrowserTabForm', + 'Browse multiple IPFS resources (CID)') + + +def iEnterIpfsCIDDialog(): + return QCoreApplication.translate('BrowserTabForm', + 'Load IPFS CID dialog') + + +def iFollowIpnsDialog(): + return QCoreApplication.translate('BrowserTabForm', + 'IPNS add feed dialog') + + +def iBrowseIpnsHash(): + return QCoreApplication.translate('BrowserTabForm', + 'Browse IPNS resource from hash/name') + + +def iBrowseCurrentClipItem(): + return QCoreApplication.translate('BrowserTabForm', + 'Browse current clipboard item') + + +def iEnterIpns(): + return QCoreApplication.translate('BrowserTabForm', + 'Enter an IPNS hash/name') + + +def iEnterIpnsDialog(): + return QCoreApplication.translate('BrowserTabForm', + 'Load IPNS key dialog') + + +def iCreateQaMapping(): + return QCoreApplication.translate('BrowserTabForm', + 'Create quick-access mapping') + + +def iHashmarked(path): + return QCoreApplication.translate('BrowserTabForm', + 'Hashmarked {0}').format(path) + + +def iHashmarkTitleDialog(): + return QCoreApplication.translate('BrowserTabForm', + 'Hashmark title') + + +def iInvalidUrl(text): + return QCoreApplication.translate('BrowserTabForm', + 'Invalid URL: {0}').format(text) + + +def iUnsupportedUrl(): + return QCoreApplication.translate('BrowserTabForm', + 'Unsupported URL type') + + +def iInvalidObjectPath(text): + return QCoreApplication.translate( + 'BrowserTabForm', + 'Invalid IPFS object path: {0}').format(text) + + +def iInvalidCID(text): + return QCoreApplication.translate( + 'BrowserTabForm', + '{0} is an invalid IPFS CID (Content IDentifier)').format(text) + + +def iNotAnIpfsResource(): + return QCoreApplication.translate( + 'BrowserTabForm', + 'Not an IPFS resource') + + +def iWebProfileMinimal(): + return QCoreApplication.translate( + 'BrowserTabForm', + 'Minimal profile') + + +def iWebProfileIpfs(): + return QCoreApplication.translate( + 'BrowserTabForm', + 'IPFS profile') + + +def iWebProfileWeb3(): + return QCoreApplication.translate( + 'BrowserTabForm', + 'Web3 profile') + + +def iCidTooltipMessage(icon, rootCidV, more, rootCid, thisCid): + return QCoreApplication.translate('BrowserTabForm', + ''' +

+ +

+ +

Root CID: CIDv{1} {2} {3}

+ +

This page's CID: {4}

+ +
+

+ Click on the cube to get a view of the DAG for this page +

+
+''').format(icon, rootCidV, more, rootCid, thisCid) + + +def iIpnsTooltipMessage(icon, ipnsKey): + return QCoreApplication.translate('BrowserTabForm', + ''' +

+ +

+ +

IPNS domain/key {1}

+ +
+

+ Click on the cube to get a view of the DAG for this page +

+
+ ''').format(icon, ipnsKey) diff --git a/galacteek/i18n/bt.py b/galacteek/i18n/bt.py new file mode 100644 index 00000000..d7bf373c --- /dev/null +++ b/galacteek/i18n/bt.py @@ -0,0 +1,29 @@ +from PyQt5.QtCore import QCoreApplication + + +def iBitTorrent(): + return QCoreApplication.translate( + 'Galacteek', + 'BitTorrent' + ) + + +def iBitTorrentClient(): + return QCoreApplication.translate( + 'Galacteek', + 'BitTorrent client' + ) + + +def iBtAddFromTorrentFile(): + return QCoreApplication.translate( + 'Galacteek', + 'Add from torrent file' + ) + + +def iBtAddFromMagnetLink(): + return QCoreApplication.translate( + 'Galacteek', + 'Add from magnet link' + ) diff --git a/galacteek/i18n/conn.py b/galacteek/i18n/conn.py new file mode 100644 index 00000000..ad7d7d2f --- /dev/null +++ b/galacteek/i18n/conn.py @@ -0,0 +1,21 @@ +from PyQt5.QtCore import QCoreApplication + + +def iErrNoCx(): + return QCoreApplication.translate( + 'GalacteekWindow', + 'No connection available') + + +def iCxButNoPeers(id, agent): + return QCoreApplication.translate( + 'GalacteekWindow', + 'IPFS ({1}): not connected to any peers').format( + id, agent) + + +def iConnectStatus(id, agent, peerscount): + return QCoreApplication.translate( + 'GalacteekWindow', + 'IPFS ({1}): connected to {2} peer(s)').format( + id, agent, peerscount) diff --git a/galacteek/i18n/did.py b/galacteek/i18n/did.py new file mode 100644 index 00000000..56ad2156 --- /dev/null +++ b/galacteek/i18n/did.py @@ -0,0 +1,27 @@ +from PyQt5.QtCore import QCoreApplication + +# Decentralized identifiers (IPID) + + +def iDID(): + return QCoreApplication.translate('GalacteekWindow', 'DID') + + +def iDIDLong(): + return QCoreApplication.translate( + 'GalacteekWindow', 'Decentralized Identifier') + + +def iIPID(): + return QCoreApplication.translate( + 'GalacteekWindow', 'IPID') + + +def iIPIDLong(): + return QCoreApplication.translate( + 'GalacteekWindow', 'InterPlanetary Identifier') + + +def iIPServices(): + return QCoreApplication.translate( + 'GalacteekWindow', 'InterPlanetary Services') diff --git a/galacteek/i18n/hashmark.py b/galacteek/i18n/hashmark.py new file mode 100644 index 00000000..2e4f8d66 --- /dev/null +++ b/galacteek/i18n/hashmark.py @@ -0,0 +1,158 @@ +from PyQt5.QtCore import QCoreApplication + +from .misc import iUnknown + + +def iNoTitle(): + return QCoreApplication.translate('GalacteekWindow', 'No title') + + +def iNoCategory(): + return QCoreApplication.translate('GalacteekWindow', 'No category') + + +def iNoDescription(): + return QCoreApplication.translate('GalacteekWindow', 'No description') + + +def iHashmark(): + return QCoreApplication.translate('GalacteekWindow', 'Hashmark') + + +def iHashmarkSources(): + return QCoreApplication.translate('GalacteekWindow', 'Hashmark sources') + + +def iHashmarkSourceAlreadyRegistered(): + return QCoreApplication.translate( + 'GalacteekWindow', 'This hashmarks source is already registered') + + +def iHashmarkSourcesDbSync(): + return QCoreApplication.translate( + 'GalacteekWindow', + 'Synchronize database' + ) + + +def iHashmarkSourcesAddGitRepo(): + return QCoreApplication.translate( + 'GalacteekWindow', 'Add hashmarks repository source (Git)') + + +def iHashmarkSourcesAddLegacyIpfsMarks(): + return QCoreApplication.translate( + 'GalacteekWindow', 'Add ipfsmarks source (old format)') + + +def iHashmarkThisPage(): + return QCoreApplication.translate('GalacteekWindow', 'Hashmark this page') + + +def iHashmarkRootObject(): + return QCoreApplication.translate( + 'GalacteekWindow', 'Hashmark root object') + + +def iHashmarks(): + return QCoreApplication.translate('GalacteekWindow', 'Hashmarks') + + +def iLocalHashmarks(): + return QCoreApplication.translate('GalacteekWindow', 'Local hashmarks') + + +def iSharedHashmarks(): + return QCoreApplication.translate('GalacteekWindow', 'Shared hashmarks') + + +def iSearchHashmarks(): + return QCoreApplication.translate('GalacteekWindow', 'Search hashmarks') + + +def iSearchHashmarksAllAcross(): + return QCoreApplication.translate( + 'GalacteekWindow', + ''' +
+

Search the hashmarks database

+ +

Search by tags using #mytag or @Planet#mytag

+ +
    +
  • @Earth#ipfs
  • +
  • #dapp
  • +
  • #icon
  • +
+ +

Press Shift + Return to validate your search

+
+ ''') + + +def iHashmarksManager(): + return QCoreApplication.translate('GalacteekWindow', 'Hashmarks manager') + + +def iHashmarkInfoToolTipShort(mark): + return QCoreApplication.translate( + 'GalacteekWindow', + ''' + +

Title: {0}

+

Description: {1}

+ ''').format(mark.title if mark.title else iNoTitle(), + mark.description if mark.description else iNoDescription(), + ) + + +def iHashmarkInfoToolTip(mark): + return QCoreApplication.translate( + 'GalacteekWindow', + ''' + +

{0}

+ +

Title: {1}

+

Description: {2}

+ +

Creation date: {3}

+ +

Hashmark source: {4}

+ ''').format(mark.uri, + mark.title if mark.title else iNoTitle(), + mark.description if mark.description else iNoDescription(), + mark.datecreated, + str(mark.source) if mark.source else iUnknown() + ) + + +def iHashmarksLibraryCountAvailable(count): + return QCoreApplication.translate( + 'GalacteekWindow', + 'Hashmarks library: {0} hashmarks available' + ).format(count) + + +def iHashmarksLibrary(): + return QCoreApplication.translate('GalacteekWindow', + ''' + + +

Hashmarks library

+ ''') + + +def iLocalHashmarksCount(count): + return QCoreApplication.translate( + 'GalacteekWindow', + 'Local hashmarks: {0} hashmarks available' + ).format(count) + + +def iHashmarksDatabase(): + return QCoreApplication.translate( + 'GalacteekWindow', + 'Hashmarks database' + ) diff --git a/galacteek/i18n/ip.py b/galacteek/i18n/ip.py new file mode 100644 index 00000000..ea476b86 --- /dev/null +++ b/galacteek/i18n/ip.py @@ -0,0 +1,36 @@ +from PyQt5.QtCore import QCoreApplication + + +# IP Tags + + +def iIPTag(): + return QCoreApplication.translate('GalacteekWindow', 'IPTag') + + +def iIPTagLong(): + return QCoreApplication.translate( + 'GalacteekWindow', 'InterPlanetary tag') + + +def iHashmarkIPTagsEdit(): + return QCoreApplication.translate( + 'GalacteekWindow', 'Edit hashmark IP tags') + + +# Space jargon + + +def iIPHandle(): + return QCoreApplication.translate( + 'GalacteekWindow', 'IP handle') + + +def iConstellation(): + return QCoreApplication.translate( + 'GalacteekWindow', 'Constellation') + + +def iVirtualPlanet(): + return QCoreApplication.translate( + 'GalacteekWindow', 'Virtual Planet') diff --git a/galacteek/i18n/ipfs.py b/galacteek/i18n/ipfs.py new file mode 100644 index 00000000..ed089666 --- /dev/null +++ b/galacteek/i18n/ipfs.py @@ -0,0 +1,140 @@ +from PyQt5.QtCore import QCoreApplication + +# CID + + +def iCID(): + return QCoreApplication.translate('GalacteekWindow', 'CID') + + +def iCIDv0(): + return QCoreApplication.translate('GalacteekWindow', 'CIDv0') + + +def iCIDv1(): + return QCoreApplication.translate('GalacteekWindow', 'CIDv1') + + +def iInvalidCID(): + return QCoreApplication.translate('GalacteekWindow', 'Invalid CID') + + +def iP2PKey(): + return QCoreApplication.translate('GalacteekWindow', 'P2P key') + + +def iUnixFSNode(): + return QCoreApplication.translate('GalacteekWindow', 'UnixFS node') + + +def iUnixFSFileToolTip(eInfo): + return QCoreApplication.translate( + 'IPFSHashExplorer', + ''' +

{0}

+

MIME type: {1}

+

CID: {2}

+

Size: {3}

+ ''').format( + eInfo.filename, + eInfo.mimeType, + eInfo.cid, + eInfo.sizeFormatted + ) + + +# URL types + +def iIPFSUrlTypeNative(): + return QCoreApplication.translate('GalacteekWindow', 'IPFS URL: native') + + +def iIPFSUrlTypeHttpGateway(): + return QCoreApplication.translate( + 'GalacteekWindow', 'IPFS URL: gatewayed') + + +def iMerkleLink(): + return QCoreApplication.translate('Galacteek', 'Merkle link') + + +def iIpfsInfos(): + return QCoreApplication.translate('GalacteekWindow', 'IPFS informations') + + +def iIpfsQrCodes(): + return QCoreApplication.translate('GalacteekWindow', 'IPFS QR codes') + + +def iIpfsQrEncode(): + return QCoreApplication.translate('GalacteekWindow', 'QR encoding') + + +def iProvidedByPeers(count): + return QCoreApplication.translate( + 'Galacteek', + 'Provided by {} peer(s)' + ).format(count) + + +def iProvidedByAtLeastPeers(count): + return QCoreApplication.translate( + 'Galacteek', + 'Provided by at least {} peer(s)' + ).format(count) + + +def iNotProvidedByAnyPeers(): + return QCoreApplication.translate( + 'Galacteek', + 'No one seems to have this file ..' + ) + + +def iSearchingProviders(): + return QCoreApplication.translate( + 'Galacteek', + 'Searching providers ..' + ) + + +def iGarbageCollectRun(): + return QCoreApplication.translate( + 'GalacteekWindow', 'Run the garbage collector') + + +def iGarbageCollectRunAsk(): + return QCoreApplication.translate( + 'GalacteekWindow', + 'Do you want to run the garbage collector ' + 'on your repository ?' + ) + + +def iGarbageCollector(): + return QCoreApplication.translate( + 'GalacteekWindow', 'Garbage collector') + + +def iGCPurgedObject(cid): + return QCoreApplication.translate( + 'GalacteekWindow', + 'Purged object with CID: {0}').format(cid) + + +def iIpfsError(msg): + return QCoreApplication.translate('GalacteekWindow', + 'IPFS error: {0}').format(msg) + + +def iCannotResolve(objPath): + return QCoreApplication.translate( + 'Galacteek', + 'Cannot resolve object: {}').format(objPath) + + +def iResourceCannotOpen(path): + return QCoreApplication.translate( + 'ResourceOpener', + '{}: unable to determine resource type' + ).format(path) diff --git a/galacteek/i18n/ipfsd.py b/galacteek/i18n/ipfsd.py new file mode 100644 index 00000000..3c457575 --- /dev/null +++ b/galacteek/i18n/ipfsd.py @@ -0,0 +1,107 @@ +from PyQt5.QtCore import QCoreApplication + + +# IPFS daemon messages + + +def iIpfsDaemon(): + return QCoreApplication.translate('Galacteek', 'IPFS daemon') + + +def iIpfsDaemonProcessControl(): + return QCoreApplication.translate('Galacteek', 'IPFS daemon process') + + +def iIpfsDaemonCPUPriority(): + return QCoreApplication.translate('Galacteek', 'CPU priority (nice)') + + +def iIpfsDaemonIOPriority(): + return QCoreApplication.translate('Galacteek', 'IO priority') + + +def iIpfsDaemonStarted(): + return QCoreApplication.translate('Galacteek', 'IPFS daemon started') + + +def iIpfsDaemonResumed(): + return QCoreApplication.translate( + 'Galacteek', + 'IPFS daemon was already running (no start)' + ) + + +def iIpfsDaemonGwStarted(): + return QCoreApplication.translate('Galacteek', + "IPFS daemon's gateway started") + + +def iIpfsDaemonReady(): + return QCoreApplication.translate('Galacteek', 'IPFS daemon is ready') + + +def iIpfsDaemonProblem(): + return QCoreApplication.translate('Galacteek', + 'Problem starting IPFS daemon') + + +def iIpfsDaemonInitProblem(): + return QCoreApplication.translate( + 'Galacteek', + 'Problem initializing the IPFS daemon (check the ports configuration)') + + +def iIpfsDaemonWaiting(count): + return QCoreApplication.translate( + 'Galacteek', + 'IPFS daemon: waiting for connection (try {0})'.format(count)) + + +def iIpfsDaemonKeepRunningAsk(): + return QCoreApplication.translate( + 'Galacteek', + 'Do you want to keep the IPFS daemon running ?' + ) + + +def iFsRepoMigrateNotFound(): + return QCoreApplication.translate( + 'Galacteek', + 'Warning: could not find IPFS repository migration tool') + + +def iGoIpfsNotFound(): + return QCoreApplication.translate( + 'Galacteek', + 'Error: Could not find go-ipfs on your system') + + +def iGoIpfsTooOld(): + return QCoreApplication.translate( + 'Galacteek', + 'Error: go-ipfs version found on your system is too old') + + +def iGoIpfsFetchAsk(): + return QCoreApplication.translate( + 'Galacteek', + 'go-ipfs was not found on your system: download ' + 'binary from IPFS distributions website (https://dist.ipfs.io) ?') + + +def iGoIpfsFetchTimeout(): + return QCoreApplication.translate( + 'Galacteek', + 'Timeout while fetching go-ipfs distribution') + + +def iGoIpfsFetchSuccess(): + return QCoreApplication.translate( + 'Galacteek', + 'go-ipfs was installed successfully') + + +def iGoIpfsFetchError(): + return QCoreApplication.translate( + 'Galacteek', + 'Error while fetching go-ipfs distribution') diff --git a/galacteek/i18n/lang.py b/galacteek/i18n/lang.py new file mode 100644 index 00000000..7e0490b9 --- /dev/null +++ b/galacteek/i18n/lang.py @@ -0,0 +1,20 @@ +from PyQt5.QtCore import QCoreApplication + + +def iLanguageChanged(): + return QCoreApplication.translate( + 'Galacteek', + "The application's language was changed" + ) + + +def iLangEnglish(): + return QCoreApplication.translate('Galacteek', 'English') + + +def iLangFrench(): + return QCoreApplication.translate('Galacteek', 'French') + + +def iLangCastilianSpanish(): + return QCoreApplication.translate('Galacteek', 'Spanish (Castilian)') diff --git a/galacteek/i18n/mfs.py b/galacteek/i18n/mfs.py new file mode 100644 index 00000000..3cbe0e6b --- /dev/null +++ b/galacteek/i18n/mfs.py @@ -0,0 +1,54 @@ +from PyQt5.QtCore import QCoreApplication + + +def iMusic(): + return QCoreApplication.translate('FileManagerForm', 'Music') + + +def iPictures(): + return QCoreApplication.translate('FileManagerForm', 'Pictures') + + +def iImages(): + return QCoreApplication.translate('FileManagerForm', 'Images') + + +def iTemporaryFiles(): + return QCoreApplication.translate('FileManagerForm', 'Temporary') + + +def iEncryptedFiles(): + return QCoreApplication.translate('FileManagerForm', 'Encrypted files') + + +def iVideos(): + return QCoreApplication.translate('FileManagerForm', 'Videos') + + +def iHome(): + return QCoreApplication.translate('FileManagerForm', 'Home') + + +def iCode(): + return QCoreApplication.translate('FileManagerForm', 'Code') + + +def iDocuments(): + return QCoreApplication.translate('FileManagerForm', 'Documents') + + +def iWebPages(): + return QCoreApplication.translate('FileManagerForm', 'Web Pages') + + +def iDWebApps(): + return QCoreApplication.translate('FileManagerForm', 'Dapps') + + +def iQrCodes(): + return QCoreApplication.translate('FileManagerForm', 'QR codes') + + +def iLinkToMfsFolder(): + return QCoreApplication.translate( + 'GalacteekWindow', 'Link to folder (MFS)') diff --git a/galacteek/i18n/misc.py b/galacteek/i18n/misc.py new file mode 100644 index 00000000..db5dde96 --- /dev/null +++ b/galacteek/i18n/misc.py @@ -0,0 +1,47 @@ +from PyQt5.QtCore import QCoreApplication + + +def iThemes(): + return QCoreApplication.translate( + 'Galacteek', + 'Themes' + ) + + +def iUnknown(): + return QCoreApplication.translate('GalacteekWindow', 'Unknown') + + +def iUnknownAgent(): + return QCoreApplication.translate('GalacteekWindow', 'Unknown agent') + + +def iNewReleaseAvailable(): + return QCoreApplication.translate( + 'Galacteek', + 'New release available: upgrade with pip install -U galacteek') + + +def iYes(): + return QCoreApplication.translate('Galacteek', 'yes') + + +def iNo(): + return QCoreApplication.translate('Galacteek', 'no') + + +def iDonate(): + return QCoreApplication.translate('Galacteek', 'Donate') + + +def iDonateBitcoin(): + return QCoreApplication.translate('Galacteek', 'Donate with Bitcoin') + + +def iDonateLiberaPay(): + return QCoreApplication.translate('Galacteek', 'Donate with LiberaPay') + + +def iDonateGithubSponsors(): + return QCoreApplication.translate( + 'Galacteek', 'Donate with Github Sponsors') diff --git a/galacteek/i18n/mplayer.py b/galacteek/i18n/mplayer.py new file mode 100644 index 00000000..fd94fa19 --- /dev/null +++ b/galacteek/i18n/mplayer.py @@ -0,0 +1,10 @@ +from PyQt5.QtCore import QCoreApplication + + +def iMediaPlayer(): + return QCoreApplication.translate('GalacteekWindow', 'Media Player') + + +def iMediaPlayerQueue(): + return QCoreApplication.translate( + 'GalacteekWindow', 'Queue in media player') diff --git a/galacteek/i18n/peers.py b/galacteek/i18n/peers.py new file mode 100644 index 00000000..34beb90b --- /dev/null +++ b/galacteek/i18n/peers.py @@ -0,0 +1,9 @@ +from PyQt5.QtCore import QCoreApplication + + +def iPeers(): + return QCoreApplication.translate('GalacteekWindow', 'Peers') + + +def iPeersCount(): + return QCoreApplication.translate('GalacteekWindow', 'Peers count') diff --git a/galacteek/i18n/pin.py b/galacteek/i18n/pin.py new file mode 100644 index 00000000..8383c0dc --- /dev/null +++ b/galacteek/i18n/pin.py @@ -0,0 +1,95 @@ +from PyQt5.QtCore import QCoreApplication + + +def iItemsInPinningQueue(itemsCount): + return QCoreApplication.translate( + 'GalacteekWindow', + 'Items queued for pinning: {}'.format(itemsCount)) + + +def iBrowseAutoPin(): + return QCoreApplication.translate('GalacteekWindow', 'Browse (auto-pin)') + + +def iPinSuccess(path): + return QCoreApplication.translate( + 'GalacteekWindow', + '{0} was pinned successfully').format(path) + + +def iPinError(path, errmsg): + return QCoreApplication.translate( + 'GalacteekWindow', + 'Error pinning {0}: {1}').format(path, errmsg) + + +def iPinningStatus(): + return QCoreApplication.translate('GalacteekWindow', 'Pinning status') + + +def iPinned(): + return QCoreApplication.translate('GalacteekWindow', 'Pinned') + + +def iPinning(): + return QCoreApplication.translate('GalacteekWindow', 'Pinning') + + +def iPinningProgress(nodes, secsSinceUpdate): + return QCoreApplication.translate( + 'GalacteekWindow', + 'Pin: {0} nodes retrieved ({1}s since last update)' + ).format(nodes, secsSinceUpdate) + + +def iPinningStalled(): + return QCoreApplication.translate( + 'GalacteekWindow', + 'Pinning stalled' + ) + + +def iPin(): + return QCoreApplication.translate('GalacteekWindow', 'Pin') + + +def iPinAndDownload(): + return QCoreApplication.translate('GalacteekWindow', 'Pin and download') + + +def iPinDirectory(): + return QCoreApplication.translate('GalacteekWindow', 'Pin directory') + + +def iUnpin(): + return QCoreApplication.translate('GalacteekWindow', 'Unpin') + + +def iPinSingle(): + return QCoreApplication.translate('GalacteekWindow', 'Pin (single)') + + +def iPinRecursive(): + return QCoreApplication.translate('GalacteekWindow', 'Pin (recursive)') + + +def iPinRecursiveParent(): + return QCoreApplication.translate('GalacteekWindow', + 'Pin parent (recursive)') + + +def iPinPageLinks(): + return QCoreApplication.translate('GalacteekWindow', "Pin page's links") + + +def iBatchPin(): + return QCoreApplication.translate('GalacteekWindow', 'Batch pin') + + +def iDoNotPin(): + return QCoreApplication.translate('GalacteekWindow', 'Do not pin') + + +def iGlobalAutoPinning(): + return QCoreApplication.translate('GalacteekWindow', + 'Global automatic pinning') diff --git a/galacteek/i18n/search.py b/galacteek/i18n/search.py new file mode 100644 index 00000000..c19dccec --- /dev/null +++ b/galacteek/i18n/search.py @@ -0,0 +1,29 @@ +from PyQt5.QtCore import QCoreApplication + + +def iSearchIpfsContent(): + return QCoreApplication.translate( + 'GalacteekWindow', + 'Search content on IPFS (ipfs-search/cyber)' + ) + + +def iSearchUseShiftReturn(): + return QCoreApplication.translate( + 'GalacteekWindow', + '

Press Shift + Return to run a search

') + + +def iIpfsSearchText(text): + return QCoreApplication.translate('GalacteekWindow', + 'Search: {0}').format(text) + + +def iIpfsSearch(): + return QCoreApplication.translate('GalacteekWindow', + 'IPFS Search') + + +def iSearch(): + return QCoreApplication.translate('GalacteekWindow', + 'Search') diff --git a/galacteek/i18n/treeview.py b/galacteek/i18n/treeview.py new file mode 100644 index 00000000..b6b63496 --- /dev/null +++ b/galacteek/i18n/treeview.py @@ -0,0 +1,31 @@ +from PyQt5.QtCore import QCoreApplication + +# Headers used in the various tree widgets + + +def iPath(): + return QCoreApplication.translate('IPFSTreeView', 'Path') + + +def iCidOrPath(): + return QCoreApplication.translate('IPFSTreeView', 'CID or path') + + +def iFileName(): + return QCoreApplication.translate('IPFSTreeView', 'Name') + + +def iFileSize(): + return QCoreApplication.translate('IPFSTreeView', 'Size') + + +def iFileHash(): + return QCoreApplication.translate('IPFSTreeView', 'Hash') + + +def iMultihash(): + return QCoreApplication.translate('IPFSTreeView', 'Multihash') + + +def iMimeType(): + return QCoreApplication.translate('IPFSTreeView', 'MIME type') diff --git a/galacteek/ipdapps/__init__.py b/galacteek/ipdapps/__init__.py index 96a8c6ff..bf035ec3 100644 --- a/galacteek/ipdapps/__init__.py +++ b/galacteek/ipdapps/__init__.py @@ -10,7 +10,7 @@ from galacteek.core import runningApp from galacteek.core import pkgResourcesListDir from galacteek.core import pkgResourcesRscFilename -from galacteek.core.aservice import GService +from galacteek.services import GService from galacteek.config import cGet from galacteek.dweb import render from galacteek.ipfs import ipfsOp diff --git a/galacteek/ipfs/config.yaml b/galacteek/ipfs/config.yaml new file mode 100644 index 00000000..c450c996 --- /dev/null +++ b/galacteek/ipfs/config.yaml @@ -0,0 +1,22 @@ +envs: + default: + search: + ipfsSearch: + pageResultsTimeout: 12.0 + getMetadataTimeout: 10.0 + + unixfs: + dirWrapRules: + # Rules that determine which UnixFS files/directories will be + # directory-wrapped. + # The 'types' list can contain either 'file' or 'directory' + # The 'mfsTranslate' attribute sets the replacement regular + # expression for the MFS entry name + + - match: '(.*)' + types: + - 'file' + + mfsTranslate: '\1.dirw' + enabled: True + priority: 0 diff --git a/galacteek/ipfs/ipfsops.py b/galacteek/ipfs/ipfsops/__init__.py similarity index 94% rename from galacteek/ipfs/ipfsops.py rename to galacteek/ipfs/ipfsops/__init__.py index d119deda..911021aa 100644 --- a/galacteek/ipfs/ipfsops.py +++ b/galacteek/ipfs/ipfsops/__init__.py @@ -31,6 +31,8 @@ from galacteek.ipfs.multi import multiAddrTcp4 from galacteek.ipfs.stat import StatInfo +from galacteek.config import cGet + from galacteek.core.asynccache import amlrucache from galacteek.core.asynccache import cachedcoromethod from galacteek.core.asynclib import async_enterable @@ -330,6 +332,14 @@ def availCommands(self): """ Cached property: available IPFS commands """ return self._commands + @property + def unixFsWrapRules(self): + return cGet('unixfs.dirWrapRules', + mod='galacteek.ipfs') + + def opConfig(self, opName): + return cGet(f'ops.{opName}') + def debug(self, msg): log.debug('IPFSOp({0}): {1}'.format(self.uid, msg)) @@ -478,11 +488,12 @@ async def filesList(self, path, sort=False): return listing['Entries'] - async def filesStat(self, path, timeout=3): + async def filesStat(self, path, timeout=None): try: + cfg = self.opConfig('filesStat') return await self.waitFor( self.client.files.stat(path), - timeout + timeout if timeout else cfg.timeout ) except aioipfs.APIError as err: self.debug(err.message) @@ -673,14 +684,25 @@ async def keysRemove(self, name): return False return True - async def publish(self, path, key='self', timeout=60 * 6, - allow_offline=None, lifetime='24h', - resolve=True, + async def publish(self, path, key='self', timeout=None, + allow_offline=None, lifetime=None, + resolve=None, ttl=None, cache=None, cacheOrigin='unknown'): + cfg = self.opConfig('publish') + usingCache = cache == 'always' or \ (cache == 'offline' and self.noPeers and 0) - aOffline = allow_offline if isinstance(allow_offline, bool) else \ - self.noPeers + + if self.noPeers: + aOffline = self.noPeers + else: + aOffline = cfg.allowOffline + + timeout = timeout if timeout else cfg.timeout + lifetime = lifetime if lifetime else cfg.lifetime + ttl = ttl if ttl else cfg.ttl + resolve = resolve if isinstance(resolve, bool) else cfg.resolve + try: if usingCache: self.debug('Caching IPNS key: {key} (origin: {origin})'.format( @@ -690,10 +712,9 @@ async def publish(self, path, key='self', timeout=60 * 6, joinIpns(key), path, origin=cacheOrigin) self.debug( - 'Publishing {path} to {dst} ' - '(cache: {cache}/{cacheOrigin}, allowoffline: {off})'.format( - path=path, dst=key, off=aOffline, - cache=cache, cacheOrigin=cacheOrigin) + f'Publishing {path} to {key} ' + f'cache: {cache}/{cacheOrigin}, allowoffline: {aOffline}, ' + f'TTL: {ttl}, lifetime: {lifetime}' ) result = await self.waitFor( @@ -712,14 +733,19 @@ async def publish(self, path, key='self', timeout=60 * 6, else: return result - async def resolve(self, path, timeout=20, recursive=False): + async def resolve(self, path, timeout=None, recursive=False): """ Use /api/vx/resolve to resolve pretty much anything """ + cfg = self.opConfig('resolve') try: resolved = await self.waitFor( - self.client.core.resolve(await self.objectPathMapper(path), - recursive=recursive), timeout) + self.client.core.resolve( + await self.objectPathMapper(path), + recursive=recursive + ), + timeout if timeout else cfg.timeout + ) except asyncio.TimeoutError: self.debug('resolve timeout for {0}'.format(path)) return None @@ -789,14 +815,22 @@ async def nsCacheSet(self, path, resolved, origin=None): await self.nsCacheSave() - async def nameResolve(self, path, timeout=20, recursive=False, + async def nameResolve(self, path, + timeout=None, + recursive=False, useCache='never', cache='never', maxCacheLifetime=None, cacheOrigin='unknown'): + cfg = self.opConfig('nameResolve') + + timeout = timeout if timeout else cfg.timeout + recursive = recursive if isinstance(recursive, bool) else cfg.recursive + usingCache = useCache == 'always' or \ (useCache == 'offline' and self.noPeers and 0) cache = cache == 'always' or (cache == 'offline' and self.noPeers) + try: if usingCache: # The NS cache is used only for IPIDs when offline @@ -875,8 +909,8 @@ async def nameResolveStreamLegacy(self, path, count=3, except aioipfs.APIError as e: self.debug('streamed resolve error: {}'.format(e.message)) - async def nameResolveStream(self, path, count=3, - timeout=20, + async def nameResolveStream(self, path, count=None, + timeout=None, useCache='never', cache='never', cacheOrigin='unknown', @@ -888,6 +922,11 @@ async def nameResolveStream(self, path, count=3, NS cache is used as last option (used by local IPID). """ + cfg = self.opConfig('nameResolveStream') + + recCount = count if count else cfg.recordCount + timeout = timeout if timeout else cfg.timeout + usingCache = useCache == 'always' or \ (useCache == 'offline' and self.noPeers and 0) cache = cache == 'always' or (cache == 'offline' and self.noPeers) @@ -900,7 +939,7 @@ async def nameResolveStream(self, path, count=3, name=path, recursive=recursive, stream=True, - dht_record_count=count, + dht_record_count=recCount, dht_timeout=rTimeout): if cache: self.debug(f'nameResolveStream ({path}): caching {nentry}') @@ -938,12 +977,13 @@ async def nameResolveStream(self, path, count=3, 'Path': rPath } - async def nameResolveStreamFirst(self, path, count=2, - timeout=10, + async def nameResolveStreamFirst(self, path, + count=None, + timeout=None, cache='never', cacheOrigin='unknown', useCache='never', - maxCacheLifetime=60 * 10, + maxCacheLifetime=None, debug=True): """ A wrapper around the nameResolveStream async gen, @@ -951,10 +991,19 @@ async def nameResolveStreamFirst(self, path, count=2, :rtype: dict """ + + cfg = self.opConfig('nameResolveStreamFirst') + + recCount = count if count else cfg.recordCount + timeout = timeout if timeout else cfg.timeout + maxCacheLifetime = maxCacheLifetime if maxCacheLifetime else \ + cfg.maxCacheLifetime + matches = [] async for entry in self.nameResolveStream( path, timeout=timeout, + count=recCount, cache=cache, cacheOrigin=cacheOrigin, maxCacheLifetime=maxCacheLifetime, useCache=useCache, @@ -963,7 +1012,7 @@ async def nameResolveStreamFirst(self, path, count=2, if found: matches.append(found) - if len(matches) >= count: + if len(matches) >= recCount: break if len(matches) > 0: @@ -1220,7 +1269,45 @@ async def hashComputePath(self, path, recursive=True): async def hashComputeString(self, s, **kw): return await self.addString(s, only_hash=True, **kw) + def unixFsWrapRuleMatch(self, rules, path: Path): + isFile = path.is_file() + isDir = path.is_dir() + + erules = sorted( + [r for r in rules if r.enabled is True], + reverse=True, + key=lambda rule: rule.priority + ) + paths = str(path) + + for rule in erules: + if isFile and 'file' not in rule.types: + continue + if isDir and 'directory' not in rule.types: + continue + + try: + match = re.search(rule.match, paths) + if match: + tr = rule.get('mfsTranslate') + xform = tr if tr else r'\1.dirw' + + wName = re.sub( + rule.match, + xform, + path.name, + count=1 + ) + assert wName is not None + + return rule, wName + except Exception: + continue + + return None, None + async def addPath(self, path, recursive=True, wrap=False, + wrapAuto=False, callback=None, cidversion=1, offline=False, dagformat='balanced', rawleaves=False, hashfunc='sha2-256', @@ -1309,6 +1396,12 @@ async def addPath(self, path, recursive=True, wrap=False, self.debug( f'Error symlinking {linkPath} to the filestore: {err}') + if wrapAuto is True: + rule, wName = self.unixFsWrapRuleMatch( + self.unixFsWrapRules, origPath) + if rule: + wrap = True + try: async for entry in self.client.add(path, quiet=True, recursive=recursive, @@ -2063,7 +2156,6 @@ async def didPing(self, peerId, did, token): async def videoRendezVous(self, didService): remotePeerId, serviceName = self.p2pEndpointExplode( didService.endpoint) - print('got', remotePeerId, serviceName) req = { 'peer': self.ctx.node.id diff --git a/galacteek/ipfs/ipfsops/config.yaml b/galacteek/ipfs/ipfsops/config.yaml new file mode 100644 index 00000000..91b6a499 --- /dev/null +++ b/galacteek/ipfs/ipfsops/config.yaml @@ -0,0 +1,37 @@ +envs: + default: + ops: + # nameResolveStream (streamed resolve) + nameResolveStream: + # resolve timeout (seconds) + timeout: 30 + # how many records to ask for + recordCount: 3 + # how long we cache results by default (seconds) + maxCacheLifetime: 600 + # recursive lookups + recursive: True + + # nameResolveStreamFirst (streamed resolve, first) + nameResolveStreamFirst: + timeout: 10 + recordCount: 2 + maxCacheLifetime: 600 + + publish: + timeout: 300 + allowOffline: False + lifetime: '48h' + ttl: '24h' + resolve: True + + resolve: + timeout: 300 + recursive: False + + filesStat: + timeout: 5 + + nameResolve: + timeout: 20 + recursive: False diff --git a/galacteek/ipfs/ipfssearch.py b/galacteek/ipfs/ipfssearch.py index 3e8a9f12..ef79569f 100755 --- a/galacteek/ipfs/ipfssearch.py +++ b/galacteek/ipfs/ipfssearch.py @@ -1,6 +1,7 @@ from galacteek import log from galacteek.core.asynclib import clientSessionWithProxy +from galacteek.config import cParentGet import aiohttp import async_timeout @@ -58,7 +59,8 @@ async def searchPage(query, page, filters={}, sslverify=True, async def getPageResults(query, page, filters={}, sslverify=True, proxyUrl=None, - timeout=12): + timeout=None): + timeout = cParentGet('search.ipfsSearch.pageResultsTimeout') try: with async_timeout.timeout(timeout): results = await searchPage(query, page, filters=filters, @@ -70,10 +72,12 @@ async def getPageResults(query, page, filters={}, sslverify=True, return emptyResults -async def objectMetadata(cid, timeout=10, sslverify=True): +async def objectMetadata(cid, timeout=None, sslverify=True): """ Returns object metadata for a CID from ipfs-search.com """ + + timeout = cParentGet('search.ipfsSearch.getMetadataTimeout') try: with async_timeout.timeout(timeout): async with aiohttp.ClientSession() as session: diff --git a/galacteek/services/__init__.py b/galacteek/services/__init__.py index e69de29b..29bec8d3 100644 --- a/galacteek/services/__init__.py +++ b/galacteek/services/__init__.py @@ -0,0 +1,68 @@ +import asyncio +import shutil + +from pathlib import Path +from mode import Service +from mode.utils.objects import cached_property # noqa + +from galacteek import log +from galacteek.core import runningApp +from galacteek.core.ps import hubPublish +from galacteek.core.ps import makeKeyService +from galacteek.config import configModRegCallback +from galacteek.config import configForModule + + +class GService(Service): + name: str = 'gservice' + ident: str = None + configModuleName: str = None + + def __init__(self, dataPath: Path = None, runtimeConfig=None): + super().__init__() + + self.app = runningApp() + self.rtCfg = runtimeConfig + + if dataPath: + self.rootPath = dataPath + else: + self.rootPath = self.app.dataPathForService('tmp') + + self.psKey = makeKeyService(self.name) + + self.serviceConfigBind() + + @property + def serviceConfig(self): + return configForModule(self.configModuleName) + + def serviceConfigBind(self, mod=None): + configModRegCallback(self.onConfigChangedAsync, + mod=self.configModuleName) + + async def onConfigChangedAsync(self): + pass + + def which(self, prog): + return shutil.which(prog) + + async def psPublish(self, event): + """ + Publish a service event message on the pubsub hub, to the + service's PS key + + TODO: Use JSON-LD for all service messages + """ + + hubPublish(self.psKey, { + 'serviceIdent': self.ident, + 'event': event + }) + + await asyncio.sleep(0.01) + + async def on_start(self) -> None: + log.debug(f'Creating service directory: {self.rootPath}') + + self.rootPath.mkdir(parents=True, exist_ok=True) diff --git a/galacteek/services/app/__init__.py b/galacteek/services/app/__init__.py new file mode 100644 index 00000000..38a393bf --- /dev/null +++ b/galacteek/services/app/__init__.py @@ -0,0 +1,84 @@ +import asyncio + +from galacteek import log +from galacteek.services import GService +from galacteek.services import cached_property +from galacteek.services.net.bitmessage.service import BitMessageClientService +from galacteek.services.net.tor.service import TorService +from galacteek.services.net.tor.service import TorServiceRuntimeConfig +from galacteek.services.ethereum.service import EthereumService + + +class AppService(GService): + """ + Main service + """ + + # Bitmessage service + bmService: BitMessageClientService = None + + # Tor service + torService: TorService = None + + # Eth + ethService: EthereumService = None + + def __init__(self, *args, **kw): + self.app = kw.pop('app') + + super().__init__(*args, **kw) + + @cached_property + def bmService(self) -> BitMessageClientService: + return BitMessageClientService( + self.app._bitMessageDataLocation + ) + + @cached_property + def ethService(self) -> EthereumService: + return EthereumService( + self.app._ethDataLocation + ) + + @cached_property + def torService(self) -> TorService: + return TorService( + self.app.dataPathForService('tor'), + TorServiceRuntimeConfig( + cfgLocation=self.app._torConfigLocation, + dataLocation=self.app._torDataDirLocation + ) + ) + + async def on_start(self) -> None: + log.debug('Starting main application service') + + # Dependencies + + log.debug('Adding runtime dependencies') + + await self.add_runtime_dependency(self.bmService) + await self.add_runtime_dependency(self.torService) + await self.add_runtime_dependency(self.ethService) + + async def on_stop(self) -> None: + log.debug('Stopping main application service') + + @GService.task + async def mProfileTask(self): + try: + from memory_profiler import memory_usage + assert self.app.cmdArgs.memprofiling is True + except (ImportError, Exception): + pass + + while not self.should_stop: + await asyncio.sleep(10) + + lt = int(self.app.loop.time()) + + usage = memory_usage(-1, interval=.2, timeout=1) + if usage: + log.debug( + f'Memory Usage (LT: {lt}): {usage[0]}' + ) diff --git a/galacteek/services/bitmessage/config.yaml b/galacteek/services/bitmessage/config.yaml deleted file mode 100644 index b2a61d00..00000000 --- a/galacteek/services/bitmessage/config.yaml +++ /dev/null @@ -1,9 +0,0 @@ -envs: - default: - enabled: True - notbit: - useTor: False - listenPort: 8444 - - # Purge objects directory before starting notbit - purgeObjectsOnStartup: True diff --git a/galacteek/services/ethereum/service.py b/galacteek/services/ethereum/service.py index 2184c688..02aae13b 100644 --- a/galacteek/services/ethereum/service.py +++ b/galacteek/services/ethereum/service.py @@ -1,13 +1,12 @@ from galacteek import log from galacteek import cached_property -from galacteek.config import cParentGet -from galacteek.core.aservice import GService +from galacteek.services import GService from galacteek.blockchain.ethereum import MockEthereumController from galacteek.blockchain.ethereum import ethConnConfigParams -from galacteek.services.ethereum import PS_EVENT_CONTRACTLOADED +# from galacteek.services.ethereum import PS_EVENT_CONTRACTLOADED class EthereumService(GService): @@ -20,11 +19,14 @@ def __init__(self, *args, **kw): def ctrl(self): try: from galacteek.blockchain.ethereum.ctrl import EthereumController + + raise Exception('Disabled') + return EthereumController(ethConnConfigParams( self.app.cmdArgs.ethnet), parent=self.app, executor=self.app.executor) - except ImportError: + except (ImportError, BaseException): # Don't have the web3 package return MockEthereumController() diff --git a/galacteek/services/net/__init__.py b/galacteek/services/net/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/galacteek/services/bitmessage/__init__.py b/galacteek/services/net/bitmessage/__init__.py similarity index 85% rename from galacteek/services/bitmessage/__init__.py rename to galacteek/services/net/bitmessage/__init__.py index e822af2d..7c281024 100644 --- a/galacteek/services/bitmessage/__init__.py +++ b/galacteek/services/net/bitmessage/__init__.py @@ -5,7 +5,7 @@ def bmAddressValid(addr): - return bmAddrRe.match(addr) + return bmAddrRe.match(addr) is not None def bmAddressExtract(email): diff --git a/galacteek/services/net/bitmessage/config.yaml b/galacteek/services/net/bitmessage/config.yaml new file mode 100644 index 00000000..e4d72c2e --- /dev/null +++ b/galacteek/services/net/bitmessage/config.yaml @@ -0,0 +1,23 @@ +envs: + default: + enabled: True + notbit: + useTor: False + listenPort: 8444 + + # Purge objects directory before starting notbit + purgeObjectsOnStartup: True + + bmCoreContacts: + - name: 'galacteek-support' + address: 'BM-87bMT2xqtucvbs7TPJb9nrEKumP7NVpWFdv' + group: 'galacteek' + + messages: + welcome: + body: > + Be sure to [check the manual](manual:/messenger.html) + + mdirWatcher: + # Settings for notbit's maildir watcher task + sleepInterval: 60 diff --git a/galacteek/services/bitmessage/service.py b/galacteek/services/net/bitmessage/service.py similarity index 91% rename from galacteek/services/bitmessage/service.py rename to galacteek/services/net/bitmessage/service.py index 3f584c2c..4c6b82a0 100644 --- a/galacteek/services/bitmessage/service.py +++ b/galacteek/services/net/bitmessage/service.py @@ -10,6 +10,8 @@ from galacteek import log from galacteek import AsyncSignal from galacteek import cached_property +from galacteek import database + from galacteek.config import cParentGet from galacteek.core.process import ProcessLauncher from galacteek.core.process import Process @@ -18,10 +20,10 @@ from galacteek.core.asynclib import asyncRmTree from galacteek.core.asynclib import asyncReadFile from galacteek.core.asynclib import asyncWriteFile -from galacteek.core.aservice import GService +from galacteek.services import GService -from galacteek.services.bitmessage import bmAddressValid -from galacteek.services.bitmessage.storage import RegularMailDir +from galacteek.services.net.bitmessage import bmAddressValid +from galacteek.services.net.bitmessage.storage import RegularMailDir NOTBIT = 'notbit' @@ -79,7 +81,8 @@ async def startProcess(self): if self.system == 'Windows': args = [ '-m', str(self.toCygwinPath(self.mDirPath)), - '-D', str(self.toCygwinPath(self.dataPath)) + '-D', str(self.toCygwinPath(self.dataPath)), + '-l', str(self.logFilePath) ] else: args = [ @@ -88,8 +91,7 @@ async def startProcess(self): ] args += [ - '-p', str(self.listenPort), - '-l', str(self.logFilePath) + '-p', str(self.listenPort) ] if self.useTor: @@ -154,7 +156,7 @@ async def on_start(self) -> None: @GService.task async def watchMailDir(self): while not self.should_stop: - await asyncio.sleep(2) + await asyncio.sleep(cParentGet('mdirWatcher.sleepInterval')) for key in self.clearMailDir.iterkeys(): try: @@ -263,6 +265,7 @@ async def createMailBox(self): if not key: # raise Exception('Could not generate BM key') + log.debug('Could not generate BM key') return None, None fp = self.mailBoxesPath.joinpath(key) @@ -313,6 +316,8 @@ async def on_start(self) -> None: self.notBitDataPath.mkdir(parents=True, exist_ok=True) self.mailBoxesPath.mkdir(parents=True, exist_ok=True) + await self.importBmContacts() + await self.add_runtime_dependency(self.mailer) if self.which(NOTBIT): @@ -332,6 +337,21 @@ async def on_start(self) -> None: else: log.debug('Notbit could not be found, not starting process') + async def importBmContacts(self): + contacts = cParentGet('bmCoreContacts') + + if not contacts: + return + + for contact in contacts: + log.debug(f'Storing contact {contact}') + + await database.bmContactAdd( + contact.get('address'), + contact.get('name'), + groupName=contact.get('group') + ) + async def on_stop(self) -> None: log.debug('Stopping bitmessage client') diff --git a/galacteek/services/bitmessage/storage.py b/galacteek/services/net/bitmessage/storage.py similarity index 86% rename from galacteek/services/bitmessage/storage.py rename to galacteek/services/net/bitmessage/storage.py index 65c265e3..c8ab041b 100644 --- a/galacteek/services/bitmessage/storage.py +++ b/galacteek/services/net/bitmessage/storage.py @@ -11,10 +11,14 @@ from galacteek import log from galacteek import AsyncSignal +from galacteek import database + +from galacteek.config import cParentGet class BitMessageMailDir: bmAddress: str = None + folderInbox: Maildir = None folderSent: Maildir = None folderTrash: Maildir = None @@ -36,11 +40,23 @@ async def emitNewMessage(self, key, msg): await self.sNewMessage.emit(key, msg) async def storeWelcome(self): + contact = await database.bmContactByNameFirst( + 'galacteek-support' + ) + + if not contact: + # not found + return + + body = cParentGet('messages.welcome.body') + msg = EmailMessage() - msg['From'] = 'admin@localhost' - msg['To'] = self.bmAddress - msg['Subject'] = 'Welcome to BM' - msg.set_payload('Welcome!') + msg['From'] = f'{contact.bmAddress}@bitmessage' + msg['To'] = f'{self.bmAddress}@bitmessage' + msg['Subject'] = 'BitMessage is easy' + + msg.set_payload(body) + await self.store(msg) async def yieldNewMessages(self): @@ -55,6 +71,15 @@ async def store(self, message): async def storeSent(self, message): raise Exception('Not implemented') + def msgRemoveInbox(self, messageId): + try: + self.folderInbox.remove(messageId) + self.maildir.flush() + return True + except Exception as err: + log.debug(f'Could not remove inbox message {messageId}: {err}') + return False + def updateMessage(self, mKey, msg): try: self.folderInbox[mKey] = msg diff --git a/galacteek/services/net/tor/__init__.py b/galacteek/services/net/tor/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/galacteek/services/tor/config.yaml b/galacteek/services/net/tor/config.yaml similarity index 73% rename from galacteek/services/tor/config.yaml rename to galacteek/services/net/tor/config.yaml index de5fc780..ec4c3e6d 100644 --- a/galacteek/services/tor/config.yaml +++ b/galacteek/services/net/tor/config.yaml @@ -1,6 +1,9 @@ envs: default: - enabled: True + enabled: False + + proxyHttpAutoUse: True + socksPortRange: minimum: 9050 maximum: 9060 diff --git a/galacteek/services/tor/process.py b/galacteek/services/net/tor/process.py similarity index 97% rename from galacteek/services/tor/process.py rename to galacteek/services/net/tor/process.py index 09dd8181..3e042b56 100644 --- a/galacteek/services/tor/process.py +++ b/galacteek/services/net/tor/process.py @@ -218,7 +218,13 @@ def pid(self): @property def running(self): - return self.pid is not None + try: + return self.process.status() in [ + psutil.STATUS_RUNNING, + psutil.STATUS_SLEEPING + ] + except Exception: + return False def message(self, msg): log.debug(msg) @@ -272,6 +278,7 @@ async def start(self): except Exception as err: log.debug(f'Starting TOR failed on port {socksPort} : ' f'error {err}') + self._procPid, self.process = None, None self.transport.close() continue else: diff --git a/galacteek/services/net/tor/service.py b/galacteek/services/net/tor/service.py new file mode 100644 index 00000000..63acbc78 --- /dev/null +++ b/galacteek/services/net/tor/service.py @@ -0,0 +1,43 @@ +import attr +from pathlib import Path + +from galacteek.config import cParentGet + +from galacteek.services import GService +from galacteek.services import cached_property +from galacteek.services.net.tor.process import TorLauncher + + +@attr.s(auto_attribs=True) +class TorServiceRuntimeConfig: + cfgLocation: Path + dataLocation: Path + + +class TorService(GService): + configModuleName = 'galacteek.services.net.tor' + + @cached_property + def proc(self): + return TorLauncher( + str(self.rtCfg.cfgLocation), + str(self.rtCfg.dataLocation) + ) + + async def on_start(self): + if cParentGet('enabled') is True: + await self.proc.start() + + async def on_stop(self): + try: + await self.proc.stop() + except Exception: + pass + + async def onConfigChangedAsync(self): + if self.proc.running is True: + if not self.serviceConfig.enabled: + await self.stop() + else: + if self.serviceConfig.enabled: + await self.restart() diff --git a/galacteek/services/tor/service.py b/galacteek/services/tor/service.py deleted file mode 100644 index 165b6ac2..00000000 --- a/galacteek/services/tor/service.py +++ /dev/null @@ -1,29 +0,0 @@ -import attr -from pathlib import Path - -from galacteek.config import cParentGet -from galacteek.core.aservice import GService -from galacteek.core.aservice import cached_property -from galacteek.services.tor.process import TorLauncher - - -@attr.s(auto_attribs=True) -class TorServiceRuntimeConfig: - cfgLocation: Path - dataLocation: Path - - -class TorService(GService): - @cached_property - def proc(self): - return TorLauncher( - str(self.rtCfg.cfgLocation), - str(self.rtCfg.dataLocation) - ) - - async def on_start(self): - if cParentGet('enabled') is True: - await self.proc.start() - - async def on_stop(self): - await self.proc.stop() diff --git a/galacteek/templates/ipfssearch.html b/galacteek/templates/ipfssearch.html index 453be808..4ecf8a10 100644 --- a/galacteek/templates/ipfssearch.html +++ b/galacteek/templates/ipfssearch.html @@ -1,204 +1,41 @@ - - +